919 lines
36 KiB
Rust
919 lines
36 KiB
Rust
//! Editoraktionen auf gemeinsamen Dokumenten und revisionsgebundene Übersetzung.
|
|
use crate::{
|
|
app::{App, Dialog, DialogKind, Field, WindowKind},
|
|
commands::Command,
|
|
documents::{DocumentId, ViewId},
|
|
export::ProjectStamp,
|
|
};
|
|
use anyhow::{anyhow, ensure, Result};
|
|
use crossterm::event::{KeyCode as K, KeyEvent, KeyModifiers as M};
|
|
use std::{collections::BTreeMap, ops::Range, path::PathBuf};
|
|
use tb_frontend::{
|
|
lexer::{self, TokenKind},
|
|
source::SourceUnit,
|
|
Diagnostic,
|
|
};
|
|
use tb_vm::{bytecode::CompiledModule, project::ProjectCompiler};
|
|
use unicode_width::UnicodeWidthChar;
|
|
|
|
#[derive(Default, Clone)]
|
|
pub struct Search {
|
|
pub text: String,
|
|
pub replacement: String,
|
|
pub backwards: bool,
|
|
pub case_sensitive: bool,
|
|
pub whole_word: bool,
|
|
pub scope: Option<Range<usize>>,
|
|
pub document: Option<DocumentId>,
|
|
pub wrap: bool,
|
|
}
|
|
#[derive(Default)]
|
|
pub struct Editor {
|
|
pub clipboard: String,
|
|
pub chord: Option<char>,
|
|
pub search: Search,
|
|
pub history: Vec<(DocumentId, usize)>,
|
|
pub expansions: BTreeMap<ViewId, String>,
|
|
pub checked: BTreeMap<DocumentId, u64>,
|
|
pub compiler: ProjectCompiler,
|
|
pub diagnostics: Vec<Diagnostic>,
|
|
revision: Option<(ProjectStamp, Vec<SourceUnit>, Vec<String>)>,
|
|
compiled: Option<CompiledModule>,
|
|
}
|
|
|
|
pub fn line_start(code: &str, at: usize) -> usize {
|
|
code[..at].rfind('\n').map_or(0, |p| p + 1)
|
|
}
|
|
pub fn line_end(code: &str, at: usize) -> usize {
|
|
code[at..].find('\n').map_or(code.len(), |p| at + p)
|
|
}
|
|
pub fn column(code: &str, tab: usize) -> usize {
|
|
code.chars().fold(0, |n, c| {
|
|
n + if c == '\t' {
|
|
tab - n % tab
|
|
} else {
|
|
c.width().unwrap_or(0)
|
|
}
|
|
})
|
|
}
|
|
pub fn offset(code: &str, col: usize, tab: usize) -> usize {
|
|
let mut width = 0;
|
|
for (i, c) in code.char_indices() {
|
|
let w = if c == '\t' {
|
|
tab - width % tab
|
|
} else {
|
|
c.width().unwrap_or(0)
|
|
};
|
|
if width + w > col {
|
|
return i;
|
|
}
|
|
width += w;
|
|
}
|
|
code.len()
|
|
}
|
|
fn word(c: char) -> bool {
|
|
c.is_alphanumeric() || c == '_' || "%&!#$@".contains(c)
|
|
}
|
|
fn previous(code: &str, at: usize) -> usize {
|
|
if code[..at].ends_with("\r\n") {
|
|
return at - 2;
|
|
}
|
|
code[..at].char_indices().last().map_or(0, |(i, _)| i)
|
|
}
|
|
fn next(code: &str, at: usize) -> usize {
|
|
if code[at..].starts_with("\r\n") {
|
|
return at + 2;
|
|
}
|
|
at + code[at..].chars().next().map_or(0, char::len_utf8)
|
|
}
|
|
fn matches(code: &str, search: &Search, scope: Range<usize>) -> Vec<Range<usize>> {
|
|
if search.text.is_empty() {
|
|
return vec![];
|
|
}
|
|
let mut result = Vec::new();
|
|
let mut end_previous = scope.start;
|
|
for (i, _) in code[scope.clone()].char_indices() {
|
|
let start = scope.start + i;
|
|
if start < end_previous {
|
|
continue;
|
|
}
|
|
let end = code[start..scope.end]
|
|
.char_indices()
|
|
.nth(search.text.chars().count())
|
|
.map_or(scope.end, |(i, _)| start + i);
|
|
let value = &code[start..end];
|
|
let equal = if search.case_sensitive {
|
|
value == search.text
|
|
} else {
|
|
value.to_lowercase() == search.text.to_lowercase()
|
|
};
|
|
if equal
|
|
&& (!search.whole_word
|
|
|| (!code[..start].chars().next_back().is_some_and(word)
|
|
&& !code[end..].chars().next().is_some_and(word)))
|
|
{
|
|
result.push(start..end);
|
|
end_previous = end;
|
|
}
|
|
}
|
|
result
|
|
}
|
|
impl App {
|
|
pub(crate) fn editor_view(&self) -> Result<ViewId> {
|
|
match self.active_window().map(|w| w.kind) {
|
|
Some(WindowKind::Code(v)) => Ok(v),
|
|
_ => Err(anyhow!("Kein Codefenster aktiv")),
|
|
}
|
|
}
|
|
pub(crate) fn editor_insert(&mut self, text: &str) -> Result<()> {
|
|
let v = self.editor_view()?;
|
|
ensure!(
|
|
!self.editor.expansions.contains_key(&v),
|
|
"Included Lines ist schreibgeschützt; Included File öffnet die Originaldatei"
|
|
);
|
|
let view = self.project.view(v)?;
|
|
let id = view.document();
|
|
let range = view.selection().unwrap_or(view.cursor..view.cursor);
|
|
let cursor = range.start + text.len();
|
|
self.project.replace_text(id, range, text)?;
|
|
let view = self.project.view_mut(v)?;
|
|
view.cursor = cursor;
|
|
view.anchor = None;
|
|
self.editor_scroll()?;
|
|
Ok(())
|
|
}
|
|
pub(crate) fn editor_scroll(&mut self) -> Result<()> {
|
|
let v = self.editor_view()?;
|
|
let rect = self.rect(self.active_window().unwrap());
|
|
let view = self.project.view(v)?;
|
|
let code = self.project.document(view.document())?.code();
|
|
let row = code[..view.cursor].bytes().filter(|b| *b == b'\n').count();
|
|
let col = column(
|
|
&code[line_start(code, view.cursor)..view.cursor],
|
|
self.options.tab_width,
|
|
);
|
|
let view = self.project.view_mut(v)?;
|
|
let height = rect.height.saturating_sub(2).max(1) as usize;
|
|
let width = rect.width.saturating_sub(2).max(1) as usize;
|
|
if row < view.scroll_line {
|
|
view.scroll_line = row;
|
|
} else if row >= view.scroll_line + height {
|
|
view.scroll_line = row + 1 - height;
|
|
}
|
|
if col < view.scroll_column {
|
|
view.scroll_column = col;
|
|
} else if col >= view.scroll_column + width {
|
|
view.scroll_column = col + 1 - width;
|
|
}
|
|
Ok(())
|
|
}
|
|
pub(crate) fn editor_key(&mut self, key: KeyEvent) -> Result<()> {
|
|
let v = self.editor_view()?;
|
|
if let Some(expansion) = self.editor.expansions.get(&v) {
|
|
let count = expansion.lines().count();
|
|
let step = self
|
|
.rect(self.active_window().unwrap())
|
|
.height
|
|
.saturating_sub(3)
|
|
.max(1) as usize;
|
|
let view = self.project.view_mut(v)?;
|
|
match key.code {
|
|
K::Up => view.scroll_line = view.scroll_line.saturating_sub(1),
|
|
K::Down => view.scroll_line = (view.scroll_line + 1).min(count.saturating_sub(1)),
|
|
K::PageUp => view.scroll_line = view.scroll_line.saturating_sub(step),
|
|
K::PageDown => {
|
|
view.scroll_line = (view.scroll_line + step).min(count.saturating_sub(1))
|
|
}
|
|
K::Home => view.scroll_line = 0,
|
|
K::End => view.scroll_line = count.saturating_sub(step),
|
|
_ => {}
|
|
}
|
|
return Ok(());
|
|
}
|
|
let view = self.project.view(v)?.clone();
|
|
let id = view.document();
|
|
let code = self.project.document(id)?.code().to_owned();
|
|
let at = view.cursor;
|
|
let ctrl = key.modifiers.contains(M::CONTROL);
|
|
let shift = key.modifiers.contains(M::SHIFT);
|
|
let mut destination;
|
|
if let Some(chord) = self.editor.chord.take() {
|
|
if let K::Char(c) = key.code {
|
|
match (chord, c.to_ascii_lowercase()) {
|
|
('q', 's') => destination = line_start(&code, at),
|
|
('q', 'd') => destination = line_end(&code, at),
|
|
('q', 'r') => destination = 0,
|
|
('q', 'c') => destination = code.len(),
|
|
('k', c @ '0'..='3') => {
|
|
self.project.view_mut(v)?.bookmarks[c as usize - '0' as usize] = Some(at);
|
|
return Ok(());
|
|
}
|
|
('q', c @ '0'..='3') => {
|
|
destination = view.bookmarks[c as usize - '0' as usize].unwrap_or(at)
|
|
}
|
|
_ => return Ok(()),
|
|
}
|
|
} else {
|
|
return Ok(());
|
|
}
|
|
} else {
|
|
match key.code {
|
|
K::Char(c @ ('q' | 'k')) if ctrl => {
|
|
self.editor.chord = Some(c);
|
|
self.message = format!("Ctrl+{} … (Esc bricht ab)", c.to_ascii_uppercase());
|
|
return Ok(());
|
|
}
|
|
K::Char('a') if ctrl => {
|
|
let v = self.project.view_mut(v)?;
|
|
v.anchor = Some(0);
|
|
v.cursor = code.len();
|
|
return self.editor_scroll();
|
|
}
|
|
K::Char('y') if ctrl => {
|
|
let range = line_start(&code, at)..(line_end(&code, at) + 1).min(code.len());
|
|
self.project.replace_text(id, range.clone(), "")?;
|
|
let v = self.project.view_mut(v)?;
|
|
v.cursor = range.start;
|
|
v.anchor = None;
|
|
return self.editor_scroll();
|
|
}
|
|
K::Insert => {
|
|
self.project.view_mut(v)?.overwrite = !view.overwrite;
|
|
self.message = if view.overwrite {
|
|
"Einfügen"
|
|
} else {
|
|
"Überschreiben"
|
|
}
|
|
.into();
|
|
return Ok(());
|
|
}
|
|
K::Char(c) if !key.modifiers.intersects(M::ALT | M::CONTROL) => {
|
|
if view.overwrite
|
|
&& view.selection().is_none()
|
|
&& at < line_end(&code, at)
|
|
&& !code[at..].starts_with("\r\n")
|
|
{
|
|
self.project
|
|
.replace_text(id, at..next(&code, at), &c.to_string())?;
|
|
self.project.view_mut(v)?.cursor = at + c.len_utf8();
|
|
return self.editor_scroll();
|
|
}
|
|
return self.editor_insert(&c.to_string());
|
|
}
|
|
K::Enter => {
|
|
return self.editor_insert(if code.contains("\r\n") { "\r\n" } else { "\n" })
|
|
}
|
|
K::Tab => return self.editor_insert("\t"),
|
|
K::Backspace | K::Delete => {
|
|
let range = view.selection().unwrap_or_else(|| {
|
|
if key.code == K::Backspace {
|
|
let p = previous(&code, at);
|
|
if code.get(p..at) == Some("\n")
|
|
&& p > 0
|
|
&& code.as_bytes()[p - 1] == b'\r'
|
|
{
|
|
p - 1..at
|
|
} else {
|
|
p..at
|
|
}
|
|
} else {
|
|
at..if code[at..].starts_with("\r\n") {
|
|
at + 2
|
|
} else {
|
|
next(&code, at)
|
|
}
|
|
}
|
|
});
|
|
self.project.replace_text(id, range.clone(), "")?;
|
|
let view = self.project.view_mut(v)?;
|
|
view.cursor = range.start;
|
|
view.anchor = None;
|
|
return self.editor_scroll();
|
|
}
|
|
K::Left | K::Right => {
|
|
destination = if key.code == K::Left {
|
|
previous(&code, at)
|
|
} else {
|
|
next(&code, at)
|
|
};
|
|
if ctrl {
|
|
if key.code == K::Left {
|
|
while destination > 0
|
|
&& !code[destination..].chars().next().is_some_and(word)
|
|
{
|
|
destination = previous(&code, destination);
|
|
}
|
|
while destination > 0
|
|
&& code[..destination].chars().next_back().is_some_and(word)
|
|
{
|
|
destination = previous(&code, destination);
|
|
}
|
|
} else {
|
|
destination = at;
|
|
while destination < code.len()
|
|
&& code[destination..].chars().next().is_some_and(word)
|
|
{
|
|
destination = next(&code, destination);
|
|
}
|
|
while destination < code.len()
|
|
&& !code[destination..].chars().next().is_some_and(word)
|
|
{
|
|
destination = next(&code, destination);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
K::Home => destination = if ctrl { 0 } else { line_start(&code, at) },
|
|
K::End => {
|
|
destination = if ctrl {
|
|
code.len()
|
|
} else {
|
|
line_end(&code, at)
|
|
};
|
|
if code[..destination].ends_with('\r') {
|
|
destination -= 1;
|
|
}
|
|
}
|
|
K::Up | K::Down | K::PageUp | K::PageDown => {
|
|
let starts: Vec<_> = std::iter::once(0)
|
|
.chain(code.match_indices('\n').map(|(i, _)| i + 1))
|
|
.collect();
|
|
let row = starts.partition_point(|i| *i <= at) - 1;
|
|
let col = column(&code[starts[row]..at], self.options.tab_width);
|
|
let step = if matches!(key.code, K::PageUp | K::PageDown) {
|
|
self.rect(self.active_window().unwrap())
|
|
.height
|
|
.saturating_sub(3)
|
|
.max(1) as usize
|
|
} else {
|
|
1
|
|
};
|
|
let row = if matches!(key.code, K::Up | K::PageUp) {
|
|
row.saturating_sub(step)
|
|
} else {
|
|
(row + step).min(starts.len() - 1)
|
|
};
|
|
let start = starts[row];
|
|
let end = line_end(&code, start);
|
|
destination = start
|
|
+ offset(
|
|
code[start..end].trim_end_matches('\r'),
|
|
col,
|
|
self.options.tab_width,
|
|
);
|
|
}
|
|
K::Esc => {
|
|
self.project.view_mut(v)?.anchor = None;
|
|
return Ok(());
|
|
}
|
|
_ => return Ok(()),
|
|
}
|
|
}
|
|
if !shift && !ctrl {
|
|
if let Some(range) = view.selection() {
|
|
if key.code == K::Left {
|
|
destination = range.start;
|
|
}
|
|
if key.code == K::Right {
|
|
destination = range.end;
|
|
}
|
|
}
|
|
}
|
|
let view = self.project.view_mut(v)?;
|
|
if shift {
|
|
view.anchor.get_or_insert(at);
|
|
} else {
|
|
view.anchor = None;
|
|
}
|
|
view.cursor = destination;
|
|
self.editor_scroll()
|
|
}
|
|
pub(crate) fn editor_command(&mut self, command: Command) -> Result<()> {
|
|
use Command::*;
|
|
let v = self.editor_view()?;
|
|
let view = self.project.view(v)?.clone();
|
|
let id = view.document();
|
|
let code = self.project.document(id)?.code().to_owned();
|
|
match command {
|
|
Copy | Cut => {
|
|
if let Some(range) = view.selection() {
|
|
self.editor.clipboard = code[range].into();
|
|
if command == Cut {
|
|
self.editor_insert("")?;
|
|
}
|
|
}
|
|
}
|
|
Paste => self.editor_insert(&self.editor.clipboard.clone())?,
|
|
Clear => self.editor_key(KeyEvent::new(K::Delete, M::NONE))?,
|
|
Find | Replace => {
|
|
let s = &self.editor.search;
|
|
self.open_dialog(
|
|
if command == Find { "Find" } else { "Change" },
|
|
DialogKind::Search {
|
|
view: v,
|
|
selection: view.selection(),
|
|
},
|
|
vec![
|
|
Field::text("Suchtext", s.text.clone()),
|
|
Field::text("Ersetzung", s.replacement.clone()),
|
|
Field::toggle("Rückwärts", s.backwards),
|
|
Field::toggle("Groß-/Kleinschreibung", s.case_sensitive),
|
|
Field::toggle("Ganzes Wort", s.whole_word),
|
|
Field::toggle("Nur Auswahl", false),
|
|
Field::choice(
|
|
"Aktion",
|
|
vec![
|
|
"Suchen".into(),
|
|
"Einzeln ersetzen".into(),
|
|
"Alle ersetzen".into(),
|
|
],
|
|
if command == Find { 0 } else { 1 },
|
|
),
|
|
],
|
|
);
|
|
}
|
|
SelectedText => {
|
|
let range = view
|
|
.selection()
|
|
.ok_or_else(|| anyhow!("Kein Text ausgewählt"))?;
|
|
self.editor.search.text = code[range].into();
|
|
self.editor.search.document = Some(id);
|
|
self.editor.search.scope = None;
|
|
self.editor.search.wrap = false;
|
|
self.find_next()?;
|
|
}
|
|
FindNext => self.find_next()?,
|
|
NewSub | NewFunction => self.open_dialog(
|
|
if command == NewSub {
|
|
"New Sub"
|
|
} else {
|
|
"New Function"
|
|
},
|
|
DialogKind::Procedure(command == NewFunction),
|
|
vec![Field::text("Name", "")],
|
|
),
|
|
Procedures => {
|
|
let parsed = tb_frontend::parser::parse("EDITOR", &lexer::lex(&code).tokens);
|
|
let mut positions = vec![0];
|
|
let mut names = vec!["Modulrumpf".into()];
|
|
for p in parsed.module.procs {
|
|
names.push(tb_frontend::editing::signature(&p.sig));
|
|
positions.push(line_offset(&code, p.pos.line));
|
|
}
|
|
self.open_dialog(
|
|
"Prozeduren",
|
|
DialogKind::Procedures(positions),
|
|
vec![Field::choice("Prozedur", names, 0)],
|
|
);
|
|
}
|
|
PreviousCode => {
|
|
if let Some((id, at)) = self.editor.history.pop() {
|
|
self.show_document(id, false)?;
|
|
let v = self.editor_view()?;
|
|
let code = self.project.document(id)?.code();
|
|
let mut at = at.min(code.len());
|
|
while !code.is_char_boundary(at) {
|
|
at -= 1;
|
|
}
|
|
self.project.view_mut(v)?.cursor = at;
|
|
self.editor_scroll()?;
|
|
}
|
|
}
|
|
IncludedFile => {
|
|
let line = &code[line_start(&code, view.cursor)..line_end(&code, view.cursor)];
|
|
let path = lexer::lex(line)
|
|
.tokens
|
|
.into_iter()
|
|
.find_map(|t| {
|
|
if let TokenKind::MetaInclude(p) = t.kind {
|
|
Some(p)
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.ok_or_else(|| anyhow!("Keine Include-Anweisung auf dieser Zeile"))?;
|
|
let doc = self.project.document(id)?;
|
|
let path = self
|
|
.project
|
|
.loader()?
|
|
.resolve(doc.source_path().parent().unwrap(), &path)
|
|
.map_err(|e| anyhow!(e))?;
|
|
self.editor.history.push((id, view.cursor));
|
|
let id = self.project.open_document(&path)?;
|
|
self.show_document(id, false)?;
|
|
}
|
|
IncludedLines => {
|
|
if self.editor.expansions.remove(&v).is_none() {
|
|
let sources = self.project.sources()?;
|
|
let path = self
|
|
.project
|
|
.document(id)?
|
|
.source_path()
|
|
.display()
|
|
.to_string();
|
|
let unit = sources
|
|
.units
|
|
.iter()
|
|
.find(|u| u.segments.iter().any(|s| s.file == path))
|
|
.ok_or_else(|| anyhow!("Dokument gehört zu keiner Übersetzungseinheit"))?;
|
|
let mut text = String::from("Included Lines · SCHREIBGESCHÜTZT\n");
|
|
for s in &unit.segments {
|
|
text.push_str(&format!("' {}:{}\n{}\n", s.file, s.first_line, s.text));
|
|
}
|
|
self.editor.expansions.insert(v, text);
|
|
self.project.view_mut(v)?.scroll_line = 0;
|
|
self.project.view_mut(v)?.scroll_column = 0;
|
|
} else {
|
|
self.editor_scroll()?;
|
|
}
|
|
}
|
|
Diagnostics => self.diagnostic_dialog(),
|
|
_ => unreachable!(),
|
|
}
|
|
Ok(())
|
|
}
|
|
pub(crate) fn editor_submit(&mut self, d: &mut Dialog) -> Result<bool> {
|
|
match d.kind.clone() {
|
|
DialogKind::Search { view, selection } => {
|
|
let id = self.project.view(view)?.document();
|
|
ensure!(!d.fields[0].string().is_empty(), "Suchtext fehlt");
|
|
ensure!(
|
|
!d.fields[5].flag() || selection.is_some(),
|
|
"Keine Auswahl vorhanden"
|
|
);
|
|
self.editor.search = Search {
|
|
text: d.fields[0].string(),
|
|
replacement: d.fields[1].string(),
|
|
backwards: d.fields[2].flag(),
|
|
case_sensitive: d.fields[3].flag(),
|
|
whole_word: d.fields[4].flag(),
|
|
scope: if d.fields[5].flag() { selection } else { None },
|
|
document: Some(id),
|
|
wrap: false,
|
|
};
|
|
match d.fields[6].index() {
|
|
0 => self.find_next()?,
|
|
1 => self.replace_matches(false)?,
|
|
_ => self.replace_matches(true)?,
|
|
}
|
|
}
|
|
DialogKind::Procedure(function) => {
|
|
let v = self.editor_view()?;
|
|
let view = self.project.view(v)?.clone();
|
|
let id = view.document();
|
|
let code = self.project.document(id)?.code();
|
|
let input = self.project.sources()?;
|
|
let name = d.fields[0].string();
|
|
for unit in &input.units {
|
|
let (module, _) = unit.parse(0, &mut Vec::new());
|
|
ensure!(
|
|
!module
|
|
.procs
|
|
.iter()
|
|
.any(|p| p.sig.name.eq_ignore_ascii_case(&name)),
|
|
"Prozedurname bereits im Projekt vorhanden"
|
|
);
|
|
}
|
|
let path = self
|
|
.project
|
|
.document(id)?
|
|
.source_path()
|
|
.display()
|
|
.to_string();
|
|
let unit = input
|
|
.units
|
|
.iter()
|
|
.find(|u| u.segments.iter().any(|s| s.file == path))
|
|
.ok_or_else(|| anyhow!("Keine Übersetzungseinheit"))?;
|
|
let first = match self.project.document(id)?.content() {
|
|
tb_vm::project_io::Content::Form(f) => {
|
|
tb_ui::frm::read_text(&path, &tb_ui::frm::write_text(f))?.code_line()
|
|
}
|
|
_ => 1,
|
|
};
|
|
let line =
|
|
first + code[..view.cursor].bytes().filter(|b| *b == b'\n').count() as u32;
|
|
let col = view.cursor - line_start(code, view.cursor);
|
|
let mut expanded = String::new();
|
|
let mut at = None;
|
|
for segment in &unit.segments {
|
|
for (i, text) in segment.text.split_inclusive('\n').enumerate() {
|
|
if segment.file == path && segment.first_line + i as u32 == line {
|
|
at = Some(
|
|
expanded.len() + col.min(text.trim_end_matches(['\n', '\r']).len()),
|
|
);
|
|
}
|
|
expanded.push_str(text);
|
|
if !text.ends_with('\n') {
|
|
expanded.push('\n');
|
|
}
|
|
}
|
|
}
|
|
let addition = tb_frontend::editing::new_procedure(
|
|
&expanded,
|
|
at.unwrap_or(expanded.len()),
|
|
&name,
|
|
function,
|
|
)
|
|
.map_err(|e| anyhow!(e))?;
|
|
let end = code.len();
|
|
self.project.replace_text(id, end..end, &addition)?;
|
|
self.project.view_mut(v)?.cursor = end + 1;
|
|
self.editor_scroll()?;
|
|
}
|
|
DialogKind::Procedures(positions) => {
|
|
let v = self.editor_view()?;
|
|
let view = self.project.view(v)?;
|
|
self.editor.history.push((view.document(), view.cursor));
|
|
self.project.view_mut(v)?.cursor = positions[d.fields[0].index()];
|
|
self.editor_scroll()?;
|
|
}
|
|
DialogKind::Diagnostics => {
|
|
ensure!(
|
|
self.revision_current(),
|
|
"Quellen geändert; Diagnosen erneut öffnen"
|
|
);
|
|
let diagnostic = self
|
|
.editor
|
|
.diagnostics
|
|
.get(d.fields[0].index())
|
|
.cloned()
|
|
.ok_or_else(|| anyhow!("Keine Diagnose"))?;
|
|
self.goto_diagnostic(&diagnostic)?;
|
|
}
|
|
_ => unreachable!(),
|
|
}
|
|
Ok(false)
|
|
}
|
|
fn find_next(&mut self) -> Result<()> {
|
|
let v = self.editor_view()?;
|
|
let view = self.project.view(v)?.clone();
|
|
let id = view.document();
|
|
let code = self.project.document(id)?.code();
|
|
let s = &mut self.editor.search;
|
|
ensure!(!s.text.is_empty(), "Noch keine Suche; Find verwenden");
|
|
if s.document != Some(id) {
|
|
s.document = Some(id);
|
|
s.scope = None;
|
|
s.wrap = false;
|
|
}
|
|
let scope = s
|
|
.scope
|
|
.clone()
|
|
.filter(|r| code.get(r.clone()).is_some())
|
|
.unwrap_or(0..code.len());
|
|
let ranges = matches(code, s, scope);
|
|
let found = if s.backwards {
|
|
let at = view.selection().map_or(view.cursor, |r| r.start);
|
|
ranges.iter().rev().find(|r| s.wrap || r.end <= at)
|
|
} else {
|
|
let at = view.selection().map_or(view.cursor, |r| r.end);
|
|
ranges.iter().find(|r| s.wrap || r.start >= at)
|
|
};
|
|
if let Some(r) = found {
|
|
let view = self.project.view_mut(v)?;
|
|
view.anchor = Some(r.start);
|
|
view.cursor = r.end;
|
|
s.wrap = false;
|
|
self.message = "Treffer".into();
|
|
self.editor_scroll()?;
|
|
} else {
|
|
s.wrap = true;
|
|
self.message = "Suchbereich beendet · F3 bietet Rücksprung zum anderen Ende".into();
|
|
}
|
|
Ok(())
|
|
}
|
|
fn replace_matches(&mut self, all: bool) -> Result<()> {
|
|
let v = self.editor_view()?;
|
|
let view = self.project.view(v)?.clone();
|
|
let id = view.document();
|
|
ensure!(
|
|
!self.editor.expansions.contains_key(&v),
|
|
"Included Lines ist schreibgeschützt"
|
|
);
|
|
let code = self.project.document(id)?.code().to_owned();
|
|
let s = &self.editor.search;
|
|
let scope = s.scope.clone().unwrap_or(0..code.len());
|
|
ensure!(
|
|
code.get(scope.clone()).is_some(),
|
|
"Suchbereich nicht mehr gültig"
|
|
);
|
|
let ranges = matches(&code, s, scope.clone());
|
|
let ranges = if all {
|
|
ranges
|
|
} else {
|
|
let found = if s.backwards {
|
|
ranges.into_iter().rev().find(|r| r.start <= view.cursor)
|
|
} else {
|
|
ranges
|
|
.into_iter()
|
|
.find(|r| r.end > view.cursor || view.selection().as_ref() == Some(r))
|
|
};
|
|
found.into_iter().collect()
|
|
};
|
|
if ranges.is_empty() {
|
|
self.message = "Keine Treffer".into();
|
|
return Ok(());
|
|
}
|
|
let mut replacement = code[scope.clone()].to_owned();
|
|
for r in ranges.iter().rev() {
|
|
replacement.replace_range(r.start - scope.start..r.end - scope.start, &s.replacement);
|
|
}
|
|
let first = &ranges[0];
|
|
let cursor = first.start + s.replacement.len();
|
|
self.project.replace_text(id, scope.clone(), &replacement)?;
|
|
if self.editor.search.scope.is_some() {
|
|
self.editor.search.scope = Some(scope.start..scope.start + replacement.len());
|
|
}
|
|
let view = self.project.view_mut(v)?;
|
|
view.cursor = cursor;
|
|
view.anchor = None;
|
|
self.message = format!("{} Treffer ersetzt", ranges.len());
|
|
self.editor_scroll()
|
|
}
|
|
pub(crate) fn diagnostic_dialog(&mut self) {
|
|
if !self.revision_current() {
|
|
if let Err(e) = self.compile_current() {
|
|
self.message = e.to_string();
|
|
}
|
|
}
|
|
if self.editor.diagnostics.is_empty() {
|
|
self.message = "Keine Diagnosen".into();
|
|
return;
|
|
}
|
|
self.open_dialog(
|
|
"Diagnosen",
|
|
DialogKind::Diagnostics,
|
|
vec![Field::choice(
|
|
"Datei:Zeile:Spalte",
|
|
self.editor
|
|
.diagnostics
|
|
.iter()
|
|
.map(ToString::to_string)
|
|
.collect(),
|
|
0,
|
|
)],
|
|
);
|
|
}
|
|
pub fn goto_diagnostic(&mut self, d: &Diagnostic) -> Result<()> {
|
|
let path = PathBuf::from(
|
|
d.file
|
|
.as_ref()
|
|
.ok_or_else(|| anyhow!("Diagnose ohne Quelldatei"))?,
|
|
);
|
|
let id = self.project.open_document(&path)?;
|
|
self.show_document(id, false)?;
|
|
let v = self.editor_view()?;
|
|
let doc = self.project.document(id)?;
|
|
let first = if let tb_vm::project_io::Content::Form(f) = doc.content() {
|
|
tb_ui::frm::read_text(&path.display().to_string(), &tb_ui::frm::write_text(f))?
|
|
.code_line()
|
|
.saturating_sub(1)
|
|
} else {
|
|
0
|
|
};
|
|
let code = doc.code();
|
|
let start = line_offset(code, d.pos.line.saturating_sub(first));
|
|
let at = start
|
|
+ code[start..line_end(code, start)]
|
|
.char_indices()
|
|
.nth(d.pos.column.saturating_sub(1) as usize)
|
|
.map_or(line_end(code, start) - start, |(i, _)| i);
|
|
self.editor.expansions.remove(&v);
|
|
self.project.view_mut(v)?.cursor = at;
|
|
self.editor_scroll()
|
|
}
|
|
/// Immer aktuelle Quellen einschließlich externer Includes prüfen; alte Erfolge werden nie herausgegeben.
|
|
pub fn compile_current(&mut self) -> Result<&CompiledModule> {
|
|
self.editor.compiled = None;
|
|
self.editor.revision = None;
|
|
self.editor.diagnostics.clear();
|
|
let sources = self.project.sources()?;
|
|
let mut catalog = tb_frontend::forms::FormCatalog::default();
|
|
for f in &sources.forms {
|
|
catalog.append(&f.catalog());
|
|
}
|
|
self.editor.revision = Some((
|
|
ProjectStamp::capture(&self.project),
|
|
sources.units.clone(),
|
|
sources.forms.iter().map(tb_ui::frm::write_text).collect(),
|
|
));
|
|
match self
|
|
.editor
|
|
.compiler
|
|
.compile("IDE", &sources.units, &catalog, &sources.forms)
|
|
{
|
|
Ok(code) => {
|
|
self.editor.compiled = Some(code);
|
|
Ok(self.editor.compiled.as_ref().unwrap())
|
|
}
|
|
Err(errors) => {
|
|
let text = errors
|
|
.iter()
|
|
.map(ToString::to_string)
|
|
.collect::<Vec<_>>()
|
|
.join("\n");
|
|
self.editor.diagnostics = errors;
|
|
Err(anyhow!(text))
|
|
}
|
|
}
|
|
}
|
|
fn revision_current(&self) -> bool {
|
|
let Some((stamp, units, forms)) = &self.editor.revision else {
|
|
return false;
|
|
};
|
|
if stamp != &ProjectStamp::capture(&self.project) {
|
|
return false;
|
|
}
|
|
let Ok(current) = self.project.sources() else {
|
|
return false;
|
|
};
|
|
units == ¤t.units
|
|
&& forms
|
|
== ¤t
|
|
.forms
|
|
.iter()
|
|
.map(tb_ui::frm::write_text)
|
|
.collect::<Vec<_>>()
|
|
}
|
|
pub fn current_compilation(&self) -> Option<&CompiledModule> {
|
|
self.revision_current()
|
|
.then_some(self.editor.compiled.as_ref())
|
|
.flatten()
|
|
}
|
|
pub fn current_diagnostics(&self) -> Option<&[Diagnostic]> {
|
|
self.revision_current()
|
|
.then_some(self.editor.diagnostics.as_slice())
|
|
}
|
|
pub(crate) fn editor_position(&self) -> Option<(ViewId, DocumentId, usize)> {
|
|
let v = self.editor_view().ok()?;
|
|
let view = self.project.view(v).ok()?;
|
|
let code = self.project.document(view.document()).ok()?.code();
|
|
Some((v, view.document(), line_start(code, view.cursor)))
|
|
}
|
|
pub(crate) fn line_leave(&mut self, old: Option<(ViewId, DocumentId, usize)>) {
|
|
let Some((v, id, start)) = old else {
|
|
return;
|
|
};
|
|
if !self.options.syntax_checking
|
|
|| self.editor.expansions.contains_key(&v)
|
|
|| old == self.editor_position()
|
|
{
|
|
return;
|
|
}
|
|
let Ok(doc) = self.project.document(id) else {
|
|
return;
|
|
};
|
|
if doc.revision() == 0 || self.editor.checked.get(&id) == Some(&doc.revision()) {
|
|
return;
|
|
}
|
|
let code = doc.code().to_owned();
|
|
let revision = doc.revision();
|
|
let lexed = lexer::lex(&code);
|
|
let parsed = tb_frontend::parser::parse("EDITOR", &lexed.tokens);
|
|
if lexed.diagnostics.is_empty()
|
|
&& parsed.diagnostics.is_empty()
|
|
&& code.is_char_boundary(start)
|
|
&& start <= code.len()
|
|
{
|
|
let end = line_end(&code, start);
|
|
let edits = tb_frontend::editing::normalization_edits(&code[start..end])
|
|
.into_iter()
|
|
.map(|(r, t)| (start + r.start..start + r.end, t))
|
|
.collect::<Vec<_>>();
|
|
if let Err(e) = self.project.replace_ranges(id, &edits) {
|
|
self.message = e.to_string();
|
|
return;
|
|
}
|
|
}
|
|
|
|
self.editor.checked.insert(
|
|
id,
|
|
self.project.document(id).map_or(revision, |d| d.revision()),
|
|
);
|
|
let result = self.compile_current().map(|_| ());
|
|
if parsed.incomplete && lexed.diagnostics.is_empty() {
|
|
self.message = "Quelltext noch unvollständig · Eingabe fortsetzen".into();
|
|
} else if let Err(e) = result {
|
|
self.message = e.to_string();
|
|
if self.dialog.is_none() {
|
|
self.diagnostic_dialog();
|
|
}
|
|
} else {
|
|
self.message = format!(
|
|
"Geprüft · {} Module übersetzt, {} wiederverwendet",
|
|
self.editor.compiler.stats.compiled, self.editor.compiler.stats.reused
|
|
);
|
|
}
|
|
}
|
|
}
|
|
pub fn line_offset(code: &str, line: u32) -> usize {
|
|
if line <= 1 {
|
|
0
|
|
} else {
|
|
code.match_indices('\n')
|
|
.nth(line as usize - 2)
|
|
.map_or(code.len(), |(i, _)| i + 1)
|
|
}
|
|
}
|