use alloc::boxed::Box;
use alloc::string::String;
use alloc::vec::Vec;
use core::ops::Range;
use text::segment;
use crate::ed_core::{TextPos, TextSel};
use crate::history::{Edit, History};
use crate::i18n::tr;
use crate::rich_doc::lines_of;
pub const FIND_CASE: u8 = 1;
pub const FIND_WORD: u8 = 2;
pub const FIND_MAX: usize = 100_000;
pub(crate) const CK_NONE: u8 = 0;
pub(crate) const CK_TYPE: u8 = 1;
pub(crate) const CK_BACK: u8 = 2;
pub(crate) const CK_DEL: u8 = 3;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct LineChange {
pub rev: u64,
pub first: usize,
pub old: usize,
pub new: usize,
}
const JOURNAL_MAX: usize = 256;
pub const DIAG_ERROR: u8 = 0;
pub const DIAG_WARNING: u8 = 1;
pub const DIAG_INFO: u8 = 2;
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Diag {
pub line: usize,
pub col: usize,
pub end_line: usize,
pub end_col: usize,
pub severity: u8,
pub msg: String,
}
impl Diag {
pub fn new(line: usize, col: usize, end_col: usize, severity: u8, msg: &str) -> Diag {
Diag { line, col, end_line: line, end_col, severity, msg: String::from(msg) }
}
pub fn span(line: usize, col: usize, end_line: usize, end_col: usize, severity: u8, msg: &str) -> Diag {
Diag { line, col, end_line, end_col, severity, msg: String::from(msg) }
}
}
#[derive(Clone, Debug)]
pub(crate) struct Mark {
pub(crate) a: TextPos,
pub(crate) b: TextPos,
pub(crate) sev: u8,
pub(crate) msg: String,
}
pub const BP_ON: u8 = 0;
pub const BP_DISABLED: u8 = 1;
pub const BP_COND: u8 = 2;
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct TextEdit {
pub a: TextPos,
pub b: TextPos,
pub text: String,
}
impl TextEdit {
pub fn new(a: TextPos, b: TextPos, text: &str) -> TextEdit {
TextEdit { a, b, text: String::from(text) }
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
struct FoldRange {
first: usize,
last: usize,
folded: bool,
}
pub(crate) struct CodeBody {
lines: Vec<String>,
sel: TextSel,
rev: u64,
journal: Vec<LineChange>,
marks: Vec<Mark>,
span: usize,
counts: [usize; 3],
bps: Vec<(usize, u8)>,
exec: Option<usize>,
folds: Vec<FoldRange>,
hidden: Vec<(usize, usize)>,
hidden_n: usize,
}
impl CodeBody {
fn splice(&mut self, first: usize, old: usize, new: &[String]) {
if !self.marks.is_empty() || !self.bps.is_empty() || self.exec.is_some() || !self.folds.is_empty() {
self.follow(first, old, new);
}
self.lines.splice(first..first + old, new.iter().cloned());
if self.lines.is_empty() {
self.lines.push(String::new());
}
self.rev += 1;
if self.journal.len() >= JOURNAL_MAX {
self.journal.drain(..JOURNAL_MAX / 2);
}
self.journal.push(LineChange { rev: self.rev, first, old, new: new.len() });
}
fn follow(&mut self, first: usize, old: usize, new: &[String]) {
let o = &self.lines[first..first + old];
let c = o.len();
let f = if o.is_empty() || new.is_empty() {
None
} else if c == new.len() && c > 1 && new[..c - 1] == o[1..] && new[c - 1] == o[0] {
Some(Follow::Turn(false))
} else if c == new.len() && c > 1 && new[1..] == o[..c - 1] && new[0] == o[c - 1] {
Some(Follow::Turn(true))
} else if c == new.len() {
let each = o.iter().zip(new).map(|(a, b)| (a != b).then(|| moved(core::slice::from_ref(a), core::slice::from_ref(b))));
Some(Follow::Lines(each.collect()))
} else {
Some(Follow::One(moved(o, new)))
};
let d = new.len() as isize - c as isize;
let marks = core::mem::take(&mut self.marks);
self.marks = marks
.into_iter()
.filter_map(|mut m| {
let point = m.a == m.b;
match &f {
Some(f) => {
m.a = f.pos(first, c, m.a, if point { AT_POINT } else { AT_START });
m.b = if point { m.a } else { f.pos(first, c, m.b, AT_END) };
}
None => {
if [m.a, m.b].iter().any(|p| p.line >= first && p.line < first + c) {
return None;
}
for p in [&mut m.a, &mut m.b] {
if p.line >= first + c {
p.line = (p.line as isize + d) as usize;
}
}
}
}
(point || m.a < m.b).then_some(m)
})
.collect();
self.marks.sort_by(|x, y| (x.a, x.b).cmp(&(y.a, y.b)));
self.recount();
let line_of = |l: usize| -> Option<usize> {
match &f {
Some(f) => Some(f.pos(first, c, TextPos::new(l, 0), AT_POINT).line),
None if l >= first + c => Some((l as isize + d) as usize),
None if l >= first => None,
None => Some(l),
}
};
if !self.bps.is_empty() {
let mut v: Vec<(usize, u8)> = self.bps.iter().filter_map(|&(l, k)| line_of(l).map(|l| (l, k))).collect();
v.sort_by_key(|x| x.0);
v.dedup_by_key(|x| x.0);
self.bps = v;
}
self.exec = self.exec.and_then(line_of);
if !self.folds.is_empty() {
let folds = core::mem::take(&mut self.folds);
self.folds = folds
.into_iter()
.filter_map(|r| {
let inside = first + c > r.first + 1 && first <= r.last;
let (a, z) = (line_of(r.first)?, line_of(r.last)?);
(z > a).then_some(FoldRange { first: a, last: z, folded: r.folded && !inside })
})
.collect();
self.folds.sort_by_key(|r| r.first);
self.folds.dedup_by_key(|r| r.first);
self.rehide();
}
}
fn rehide(&mut self) {
self.hidden.clear();
for r in self.folds.iter().filter(|r| r.folded) {
let (a, z) = (r.first + 1, r.last.min(self.lines.len().saturating_sub(1)));
if z < a {
continue;
}
match self.hidden.last_mut() {
Some(h) if a <= h.1 + 1 => h.1 = h.1.max(z),
_ => self.hidden.push((a, z)),
}
}
self.hidden_n = self.hidden.iter().map(|h| h.1 - h.0 + 1).sum();
}
fn recount(&mut self) {
self.counts = [0; 3];
self.span = 0;
for m in &self.marks {
self.counts[m.sev.min(DIAG_INFO) as usize] += 1;
self.span = self.span.max(m.b.line - m.a.line);
}
}
fn mark_range(&self, first: usize, last: usize) -> Range<usize> {
let lo = self.marks.partition_point(|m| m.a.line + self.span < first);
let hi = self.marks.partition_point(|m| m.a.line < last);
lo..hi.max(lo)
}
}
const AT_START: u8 = 0;
const AT_END: u8 = 1;
const AT_POINT: u8 = 2;
enum Follow {
One(Moved),
Lines(Vec<Option<Moved>>),
Turn(bool),
}
impl Follow {
fn pos(&self, first: usize, c: usize, q: TextPos, at: u8) -> TextPos {
if q.line < first {
return q;
}
let k = q.line - first;
match self {
Follow::One(m) => {
let r = m.at(TextPos::new(k, q.byte), at);
TextPos::new(r.line + first, r.byte)
}
Follow::Lines(v) => match v.get(k) {
Some(Some(m)) => TextPos::new(q.line, m.at(TextPos::new(0, q.byte), at).byte),
_ => q,
},
Follow::Turn(down) if k < c => TextPos::new(first + if *down { (k + 1) % c } else { (k + c - 1) % c }, q.byte),
Follow::Turn(_) => q,
}
}
}
#[derive(Clone, Copy)]
struct Moved {
p: TextPos,
os: TextPos,
ns: TextPos,
}
impl Moved {
fn at(&self, q: TextPos, at: u8) -> TextPos {
match at {
AT_END if q <= self.p => q,
AT_END if q <= self.os => self.p,
_ if q < self.p => q,
AT_START if q < self.os => self.ns,
AT_POINT if q < self.os => self.p,
_ => self.shift(q),
}
}
fn shift(&self, q: TextPos) -> TextPos {
if q.line == self.os.line {
TextPos::new(self.ns.line, self.ns.byte + (q.byte - self.os.byte))
} else {
TextPos::new(q.line - self.os.line + self.ns.line, q.byte)
}
}
}
fn moved(o: &[String], n: &[String]) -> Moved {
let m = o.len().min(n.len());
let mut k = 0;
while k < m && o[k] == n[k] {
k += 1;
}
let p = if k < m {
TextPos::new(k, common_prefix(&o[k], &n[k]))
} else if k > 0 {
TextPos::new(k - 1, o[k - 1].len())
} else {
TextPos::new(0, 0)
};
let (mut ol, mut nl) = (o.len() - 1, n.len() - 1);
while ol > p.line && nl > p.line && o[ol] == n[nl] {
ol -= 1;
nl -= 1;
}
let lo = if ol == p.line { p.byte } else { 0 };
let ln = if nl == p.line { p.byte } else { 0 };
let s = common_suffix(&o[ol], &n[nl], (o[ol].len() - lo).min(n[nl].len() - ln));
Moved { p, os: TextPos::new(ol, o[ol].len() - s), ns: TextPos::new(nl, n[nl].len() - s) }
}
fn common_prefix(a: &str, b: &str) -> usize {
let mut k = a.bytes().zip(b.bytes()).take_while(|(x, y)| x == y).count();
while !(a.is_char_boundary(k) && b.is_char_boundary(k)) {
k -= 1;
}
k
}
fn common_suffix(a: &str, b: &str, max: usize) -> usize {
let mut k = a.bytes().rev().zip(b.bytes().rev()).take(max).take_while(|(x, y)| x == y).count();
while !(a.is_char_boundary(a.len() - k) && b.is_char_boundary(b.len() - k)) {
k -= 1;
}
k
}
fn byte_of_char(t: &str, c: usize) -> usize {
t.char_indices().nth(c).map_or(t.len(), |(i, _)| i)
}
struct LineEdit {
first: usize,
old: Vec<String>,
new: Vec<String>,
sel0: TextSel,
sel1: TextSel,
label: String,
kind: u8,
}
impl Edit<CodeBody> for LineEdit {
fn apply(&mut self, s: &mut CodeBody) {
s.splice(self.first, self.old.len(), &self.new);
s.sel = self.sel1;
}
fn revert(&mut self, s: &mut CodeBody) {
s.splice(self.first, self.new.len(), &self.old);
s.sel = self.sel0;
}
fn label(&self) -> &str {
&self.label
}
fn merge(&mut self, next: &mut dyn Edit<CodeBody>) -> bool {
if self.kind == CK_NONE {
return false;
}
match next.value().and_then(|v| v.downcast_ref::<LineEdit>()) {
Some(n) if n.kind == self.kind && n.first == self.first && n.sel0 == self.sel1 && n.old == self.new => {
self.new = n.new.clone();
self.sel1 = n.sel1;
true
}
_ => false,
}
}
fn value(&self) -> Option<&dyn core::any::Any> {
Some(self)
}
}
pub struct CodeDoc {
body: CodeBody,
hist: History<CodeBody>,
uid: u32,
pub tab_width: u8,
pub use_tabs: bool,
}
impl Default for CodeDoc {
fn default() -> CodeDoc {
CodeDoc::new()
}
}
pub(crate) fn is_word(c: char) -> bool {
c.is_alphanumeric() || c == '_'
}
pub(crate) fn fold(c: char) -> char {
if c.is_ascii() {
c.to_ascii_lowercase()
} else {
c.to_lowercase().next().unwrap_or(c)
}
}
pub(crate) fn char_cols(c: char) -> usize {
if segment::char_props(c) & (1 << 16) != 0 { 2 } else { 1 }
}
impl CodeDoc {
pub fn new() -> CodeDoc {
CodeDoc::from_lines(Vec::new())
}
pub fn from_text(s: &str) -> CodeDoc {
CodeDoc::from_lines(lines_of(s).map(String::from).collect())
}
pub fn from_lines(mut lines: Vec<String>) -> CodeDoc {
if lines.is_empty() {
lines.push(String::new());
}
for l in lines.iter_mut() {
if l.contains(['\n', '\r']) {
*l = l.replace(['\n', '\r'], "");
}
}
static NEXT: core::sync::atomic::AtomicU32 = core::sync::atomic::AtomicU32::new(1);
let uid = NEXT.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
let body = CodeBody {
lines,
sel: TextSel::default(),
rev: 0,
journal: Vec::new(),
marks: Vec::new(),
span: 0,
counts: [0; 3],
bps: Vec::new(),
exec: None,
folds: Vec::new(),
hidden: Vec::new(),
hidden_n: 0,
};
CodeDoc { body, hist: History::new(1000), uid, tab_width: 4, use_tabs: false }
}
pub fn text(&self) -> String {
let n: usize = self.body.lines.iter().map(|l| l.len() + 1).sum();
let mut s = String::with_capacity(n);
for (i, l) in self.body.lines.iter().enumerate() {
if i > 0 {
s.push('\n');
}
s.push_str(l);
}
s
}
pub fn line_count(&self) -> usize {
self.body.lines.len()
}
pub fn line(&self, i: usize) -> &str {
&self.body.lines[i.min(self.body.lines.len() - 1)]
}
pub fn lines(&self) -> &[String] {
&self.body.lines
}
pub fn revision(&self) -> u64 {
self.body.rev
}
pub(crate) fn uid(&self) -> u32 {
self.uid
}
pub fn changes_since(&self, rev: u64) -> Option<&[LineChange]> {
if rev == self.body.rev {
return Some(&[]);
}
let j = &self.body.journal;
let i = j.iter().position(|c| c.rev > rev)?;
(j[i].rev == rev + 1).then_some(&j[i..])
}
pub fn selection(&self) -> TextSel {
self.body.sel
}
pub fn set_selection(&mut self, sel: TextSel) {
self.body.sel = TextSel::new(self.clamp(sel.anchor), self.clamp(sel.caret));
}
pub fn clamp(&self, p: TextPos) -> TextPos {
let line = p.line.min(self.body.lines.len() - 1);
let t = &self.body.lines[line];
let mut b = p.byte.min(t.len());
while !t.is_char_boundary(b) {
b -= 1;
}
TextPos::new(line, b)
}
pub fn end(&self) -> TextPos {
let l = self.body.lines.len() - 1;
TextPos::new(l, self.body.lines[l].len())
}
pub fn text_range(&self, a: TextPos, b: TextPos) -> String {
let (a, b) = (self.clamp(a.min(b)), self.clamp(a.max(b)));
let mut s = String::new();
for l in a.line..=b.line {
let t = &self.body.lines[l];
if l > a.line {
s.push('\n');
}
s.push_str(&t[if l == a.line { a.byte } else { 0 }..if l == b.line { b.byte } else { t.len() }]);
}
s
}
pub fn selected_text(&self) -> String {
let (a, b) = self.body.sel.range();
self.text_range(a, b)
}
pub fn flat(&self, p: TextPos) -> usize {
let p = self.clamp(p);
self.body.lines[..p.line].iter().map(|l| l.len() + 1).sum::<usize>() + p.byte
}
pub fn unflat(&self, f: usize) -> TextPos {
let mut f = f;
for (i, l) in self.body.lines.iter().enumerate() {
if f <= l.len() {
return self.clamp(TextPos::new(i, f));
}
f -= l.len() + 1;
}
self.end()
}
pub fn col_of(&self, line: usize, byte: usize) -> usize {
col_in(self.line(line), byte, self.tab_width as usize)
}
pub fn byte_at_col(&self, line: usize, col: usize) -> usize {
byte_in(self.line(line), col, self.tab_width as usize)
}
pub fn can_undo(&self) -> bool {
self.hist.can_undo()
}
pub fn can_redo(&self) -> bool {
self.hist.can_redo()
}
pub fn undo_label(&self) -> Option<&str> {
self.hist.undo_label()
}
pub fn redo_label(&self) -> Option<&str> {
self.hist.redo_label()
}
pub fn undo(&mut self) -> bool {
self.hist.undo(&mut self.body)
}
pub fn redo(&mut self) -> bool {
self.hist.redo(&mut self.body)
}
pub fn is_dirty(&self) -> bool {
self.hist.dirty()
}
pub fn mark_saved(&mut self) {
self.hist.mark_saved();
}
pub fn clear_history(&mut self) {
self.hist.clear();
}
fn edit(&mut self, first: usize, n: usize, mut new: Vec<String>, sel: TextSel, label: &str, kind: u8) -> bool {
if new.is_empty() && n == self.body.lines.len() {
new.push(String::new());
}
if self.body.lines[first..first + n] == new[..] {
self.body.sel = sel;
return false;
}
let old = self.body.lines[first..first + n].to_vec();
let e = LineEdit { first, old, new, sel0: self.body.sel, sel1: sel, label: String::from(label), kind };
self.hist.apply(&mut self.body, Box::new(e));
true
}
fn spliced(&self, a: TextPos, b: TextPos, s: &str) -> (Vec<String>, TextPos) {
let head = &self.body.lines[a.line][..a.byte];
let tail = &self.body.lines[b.line][b.byte..];
let mut out: Vec<String> = Vec::new();
let mut first = true;
for l in lines_of(s) {
if first {
let mut h = String::with_capacity(head.len() + l.len() + tail.len());
h.push_str(head);
h.push_str(l);
out.push(h);
first = false;
} else {
out.push(String::from(l));
}
}
let k = out.len() - 1;
let end = TextPos::new(a.line + k, out[k].len());
out[k].push_str(tail);
(out, end)
}
pub fn replace(&mut self, a: TextPos, b: TextPos, s: &str) -> TextPos {
self.replace_kind(a, b, s, CK_NONE, tr("Ersetzen"))
}
pub(crate) fn replace_kind(&mut self, a: TextPos, b: TextPos, s: &str, kind: u8, label: &str) -> TextPos {
let (a, b) = (self.clamp(a.min(b)), self.clamp(a.max(b)));
let (new, end) = self.spliced(a, b, s);
self.edit(a.line, b.line - a.line + 1, new, TextSel::at(end), label, kind);
end
}
pub fn insert(&mut self, s: &str) -> TextPos {
let (a, b) = self.body.sel.range();
self.replace_kind(a, b, s, CK_NONE, tr("Einfügen"))
}
pub fn delete(&mut self, a: TextPos, b: TextPos) -> bool {
self.delete_kind(a, b, CK_NONE)
}
pub(crate) fn delete_kind(&mut self, a: TextPos, b: TextPos, kind: u8) -> bool {
let (a, b) = (self.clamp(a.min(b)), self.clamp(a.max(b)));
if a == b {
return false;
}
let rev = self.body.rev;
self.replace_kind(a, b, "", kind, tr("Löschen"));
self.body.rev != rev
}
pub fn indentation(&self, line: usize) -> &str {
let t = self.line(line);
&t[..t.len() - t.trim_start_matches([' ', '\t']).len()]
}
pub fn indent_unit(&self) -> String {
if self.use_tabs { String::from("\t") } else { " ".repeat(self.tab_width.clamp(1, 16) as usize) }
}
pub fn type_text(&mut self, s: &str, closers: &str) -> bool {
let (a, b) = self.body.sel.range();
let rev = self.body.rev;
let line = self.line(a.line);
let mut chars = s.chars();
let one = chars.next().filter(|_| chars.next().is_none());
if let Some(c) = one.filter(|c| closers.contains(*c)) {
if a.line == b.line && line[..a.byte].trim().is_empty() && !line[..a.byte].is_empty() {
let ind = &line[..a.byte];
let w = col_in(ind, ind.len(), self.tab_width as usize);
let tw = self.tab_width.max(1) as usize;
let target = if w % tw == 0 { w.saturating_sub(tw) } else { w - w % tw };
let keep = byte_in(ind, target, self.tab_width as usize);
let mut rep = String::from(&ind[..keep]);
rep.push(c);
self.replace_kind(TextPos::new(a.line, 0), b, &rep, CK_NONE, tr("Schreiben"));
return self.body.rev != rev;
}
}
self.replace_kind(a, b, s, CK_TYPE, tr("Schreiben"));
self.body.rev != rev
}
pub fn newline(&mut self, openers: &str) -> bool {
let (a, b) = self.body.sel.range();
let rev = self.body.rev;
let line = self.line(a.line);
let ind = String::from(self.indentation(a.line).get(..a.byte.min(self.indentation(a.line).len())).unwrap_or(""));
let before = line[..a.byte].trim_end();
let after = self.line(b.line)[b.byte..].trim_start();
let opener = before.chars().last().filter(|c| openers.contains(*c));
let mut s = String::from("\n");
s.push_str(&ind);
let mut caret_off = s.len();
if let Some(o) = opener {
s.push_str(&self.indent_unit());
caret_off = s.len();
let close = match o {
'{' => Some('}'),
'[' => Some(']'),
'(' => Some(')'),
_ => None,
};
if close.is_some() && after.starts_with(close.unwrap()) {
s.push('\n');
s.push_str(&ind);
}
}
let trail = self.line(b.line)[b.byte..].len() - after.len();
let b2 = TextPos::new(b.line, b.byte + trail);
let (new, _) = self.spliced(a, b2, &s);
let caret = TextPos::new(a.line + 1, caret_off - 1);
self.edit(a.line, b2.line - a.line + 1, new, TextSel::at(caret), tr("Zeilenumbruch"), CK_NONE);
self.body.rev != rev
}
pub fn tab(&mut self) -> bool {
let (a, b) = self.body.sel.range();
if a.line != b.line {
return self.indent(1);
}
let rev = self.body.rev;
let s = if self.use_tabs {
String::from("\t")
} else {
let col = self.col_of(a.line, a.byte);
let tw = self.tab_width.clamp(1, 16) as usize;
" ".repeat(tw - col % tw)
};
self.replace_kind(a, b, &s, CK_TYPE, tr("Einzug"));
self.body.rev != rev
}
pub fn back_tab(&mut self) -> bool {
self.indent(-1)
}
pub fn indent(&mut self, delta: i32) -> bool {
let sel = self.body.sel;
let (a, b) = sel.range();
let last = if b.line > a.line && b.byte == 0 { b.line - 1 } else { b.line };
let unit = self.indent_unit();
let tw = self.tab_width.clamp(1, 16) as usize;
let mut new = Vec::with_capacity(last - a.line + 1);
let mut shift: Vec<isize> = Vec::with_capacity(last - a.line + 1);
for l in a.line..=last {
let t = &self.body.lines[l];
if delta > 0 {
if t.trim().is_empty() && last > a.line {
new.push(t.clone());
shift.push(0);
} else {
let mut s = unit.clone();
s.push_str(t);
new.push(s);
shift.push(unit.len() as isize);
}
} else {
let n = if t.starts_with('\t') { 1 } else { t.bytes().take(tw).take_while(|&c| c == b' ').count() };
new.push(String::from(&t[n..]));
shift.push(-(n as isize));
}
}
let mv = |p: TextPos| -> TextPos {
if p.line < a.line || p.line > last {
return p;
}
let d = shift[p.line - a.line];
if d >= 0 {
if p.byte == 0 && !sel.is_empty() { p } else { TextPos::new(p.line, p.byte + d as usize) }
} else {
TextPos::new(p.line, p.byte.saturating_sub((-d) as usize))
}
};
let to = TextSel::new(mv(sel.anchor), mv(sel.caret));
self.edit(a.line, last - a.line + 1, new, to, tr("Einzug"), CK_NONE)
}
pub fn toggle_comment(&mut self, prefix: &str) -> bool {
if prefix.is_empty() {
return false;
}
let sel = self.body.sel;
let (a, b) = sel.range();
let last = if b.line > a.line && b.byte == 0 { b.line - 1 } else { b.line };
let lines = &self.body.lines[a.line..=last];
let all = lines.iter().filter(|t| !t.trim().is_empty()).all(|t| t.trim_start().starts_with(prefix));
let min_ind = lines.iter().filter(|t| !t.trim().is_empty()).map(|t| t.len() - t.trim_start().len()).min().unwrap_or(0);
let mut new = Vec::new();
let mut shift = Vec::new();
for t in lines {
if t.trim().is_empty() {
new.push(t.clone());
shift.push((0usize, 0isize));
continue;
}
if all {
let i = t.len() - t.trim_start().len();
let mut n = prefix.len();
if t[i + n..].starts_with(' ') {
n += 1;
}
let mut s = String::from(&t[..i]);
s.push_str(&t[i + n..]);
new.push(s);
shift.push((i, -(n as isize)));
} else {
let mut s = String::from(&t[..min_ind]);
s.push_str(prefix);
s.push(' ');
s.push_str(&t[min_ind..]);
new.push(s);
shift.push((min_ind, prefix.len() as isize + 1));
}
}
let mv = |p: TextPos| -> TextPos {
if p.line < a.line || p.line > last {
return p;
}
let (at, d) = shift[p.line - a.line];
if p.byte < at {
return p;
}
TextPos::new(p.line, if d >= 0 { p.byte + d as usize } else { p.byte.saturating_sub((-d) as usize).max(at) })
};
let to = TextSel::new(mv(sel.anchor), mv(sel.caret));
self.edit(a.line, last - a.line + 1, new, to, tr("Kommentar"), CK_NONE)
}
pub fn move_lines(&mut self, delta: i32) -> bool {
let sel = self.body.sel;
let (a, b) = sel.range();
let last = if b.line > a.line && b.byte == 0 { b.line - 1 } else { b.line };
let n = self.body.lines.len();
if (delta < 0 && a.line == 0) || (delta > 0 && last + 1 >= n) {
return false;
}
let (first, cnt) = if delta < 0 { (a.line - 1, last - a.line + 2) } else { (a.line, last - a.line + 2) };
let mut new: Vec<String> = self.body.lines[first..first + cnt].to_vec();
if delta < 0 {
new.rotate_left(1);
} else {
new.rotate_right(1);
}
let d = if delta < 0 { -1isize } else { 1 };
let mv = |p: TextPos| TextPos::new((p.line as isize + d) as usize, p.byte);
let to = TextSel::new(mv(sel.anchor), mv(sel.caret));
self.edit(first, cnt, new, to, tr("Zeilen verschieben"), CK_NONE)
}
pub fn find_all(&self, q: &str, opts: u8) -> Vec<(TextPos, TextPos)> {
let mut out = Vec::new();
if q.is_empty() || q.contains('\n') {
return out;
}
for (l, t) in self.body.lines.iter().enumerate() {
find_in(t, q, opts, &mut |a, b| {
out.push((TextPos::new(l, a), TextPos::new(l, b)));
out.len() < FIND_MAX
});
if out.len() >= FIND_MAX {
break;
}
}
out
}
pub fn find_lines(&self, q: &str, opts: u8, first: usize, n: usize) -> Vec<(TextPos, TextPos)> {
let mut out = Vec::new();
if q.is_empty() || q.contains('\n') {
return out;
}
let end = (first + n).min(self.body.lines.len());
for l in first.min(end)..end {
find_in(&self.body.lines[l], q, opts, &mut |a, b| {
out.push((TextPos::new(l, a), TextPos::new(l, b)));
true
});
}
out
}
pub fn find_next(&self, from: TextPos, q: &str, opts: u8, backwards: bool) -> Option<(TextPos, TextPos)> {
if q.is_empty() || q.contains('\n') {
return None;
}
let n = self.body.lines.len();
let from = self.clamp(from);
for k in 0..=n {
let l = if backwards { (from.line + n * 2 - k) % n } else { (from.line + k) % n };
let mut hits: Vec<(usize, usize)> = Vec::new();
find_in(&self.body.lines[l], q, opts, &mut |a, b| {
hits.push((a, b));
true
});
let pick = if backwards {
hits.into_iter().rev().find(|h| k > 0 || h.0 < from.byte)
} else {
hits.into_iter().find(|h| k > 0 || h.0 >= from.byte)
};
if let Some((a, b)) = pick {
return Some((TextPos::new(l, a), TextPos::new(l, b)));
}
}
None
}
pub fn sel_is_match(&self, q: &str, opts: u8) -> bool {
let (a, b) = self.body.sel.range();
if a.line != b.line || a == b {
return false;
}
let mut ok = false;
find_in(&self.body.lines[a.line][..], q, opts, &mut |x, y| {
if x == a.byte && y == b.byte {
ok = true;
return false;
}
true
});
ok
}
pub fn replace_next(&mut self, q: &str, rep: &str, opts: u8) -> bool {
let mut did = false;
if self.sel_is_match(q, opts) {
let (a, b) = self.body.sel.range();
self.replace_kind(a, b, rep, CK_NONE, tr("Ersetzen"));
did = true;
}
let from = self.body.sel.end();
if let Some((a, b)) = self.find_next(from, q, opts, false) {
self.body.sel = TextSel::new(a, b);
}
did
}
pub fn replace_all(&mut self, q: &str, rep: &str, opts: u8) -> usize {
if q.is_empty() || q.contains('\n') {
return 0;
}
let mut first = usize::MAX;
let mut last = 0;
let mut count = 0;
let mut new_lines: Vec<(usize, String)> = Vec::new();
for (l, t) in self.body.lines.iter().enumerate() {
let mut hits = Vec::new();
find_in(t, q, opts, &mut |a, b| {
hits.push((a, b));
true
});
if hits.is_empty() {
continue;
}
let mut s = String::with_capacity(t.len());
let mut at = 0;
for (a, b) in &hits {
s.push_str(&t[at..*a]);
s.push_str(rep);
at = *b;
}
s.push_str(&t[at..]);
count += hits.len();
first = first.min(l);
last = l;
new_lines.push((l, s));
}
if count == 0 {
return 0;
}
let mut new: Vec<String> = self.body.lines[first..=last].to_vec();
for (l, s) in new_lines {
new[l - first] = s;
}
let sel = TextSel::at(self.clamp(self.body.sel.caret));
let label = tr("Alle ersetzen");
self.edit(first, last - first + 1, new, sel, label, CK_NONE);
let c = self.clamp(self.body.sel.caret);
self.body.sel = TextSel::at(c);
count
}
pub fn set_diagnostics(&mut self, d: &[Diag]) {
let lines = &self.body.lines;
let pos = |line: usize, col: usize| -> TextPos {
match lines.get(line.max(1) - 1) {
Some(t) => TextPos::new(line.max(1) - 1, byte_of_char(t, col.max(1) - 1)),
None => TextPos::new(lines.len() - 1, lines[lines.len() - 1].len()),
}
};
let mut v: Vec<Mark> = d
.iter()
.map(|x| {
let a = pos(x.line, x.col);
Mark { a, b: pos(x.end_line, x.end_col).max(a), sev: x.severity.min(DIAG_INFO), msg: x.msg.clone() }
})
.collect();
v.sort_by(|x, y| (x.a, x.b).cmp(&(y.a, y.b)));
self.body.marks = v;
self.body.recount();
}
pub fn clear_diagnostics(&mut self) {
self.body.marks.clear();
self.body.recount();
}
pub fn diagnostics(&self) -> Vec<Diag> {
self.body.marks.iter().map(|m| self.diag_of(m)).collect()
}
pub fn diagnostic_count(&self) -> usize {
self.body.marks.len()
}
pub fn diagnostic(&self, i: usize) -> Option<Diag> {
self.body.marks.get(i).map(|m| self.diag_of(m))
}
pub fn diagnostic_counts(&self) -> [usize; 3] {
self.body.counts
}
pub fn diagnostics_at(&self, p: TextPos) -> Vec<Diag> {
self.marks_at(p).into_iter().map(|i| self.diag_of(&self.body.marks[i])).collect()
}
pub fn next_diagnostic(&self, p: TextPos, back: bool) -> Option<TextPos> {
let m = &self.body.marks;
let last = m.last()?;
Some(if back {
let k = m.partition_point(|x| x.a < p);
if k == 0 { last.a } else { m[k - 1].a }
} else {
m.get(m.partition_point(|x| x.a <= p)).map_or(m[0].a, |x| x.a)
})
}
pub fn set_breakpoints(&mut self, bps: &[(usize, u8)]) {
let n = self.body.lines.len();
let mut v: Vec<(usize, u8)> = bps.iter().copied().filter(|b| b.0 < n).collect();
v.sort_by_key(|b| b.0);
v.dedup_by_key(|b| b.0);
self.body.bps = v;
}
pub fn breakpoints(&self) -> &[(usize, u8)] {
&self.body.bps
}
pub fn breakpoint(&self, line: usize) -> Option<u8> {
self.body.bps.binary_search_by_key(&line, |b| b.0).ok().map(|i| self.body.bps[i].1)
}
pub fn set_exec_line(&mut self, line: Option<usize>) {
self.body.exec = line.filter(|&l| l < self.body.lines.len());
}
pub fn exec_line(&self) -> Option<usize> {
self.body.exec
}
pub fn set_fold_ranges(&mut self, ranges: &[(usize, usize)]) {
let n = self.body.lines.len();
let old = core::mem::take(&mut self.body.folds);
let mut v: Vec<FoldRange> = ranges
.iter()
.filter(|r| r.0 < r.1 && r.0 < n)
.map(|&(a, z)| FoldRange { first: a, last: z.min(n - 1), folded: old.iter().any(|o| o.first == a && o.folded) })
.filter(|r| r.last > r.first)
.collect();
v.sort_by_key(|r| r.first);
v.dedup_by_key(|r| r.first);
self.body.folds = v;
self.body.rehide();
}
pub fn fold_count(&self) -> usize {
self.body.folds.len()
}
pub fn fold_ranges(&self) -> Vec<(usize, usize, bool)> {
self.body.folds.iter().map(|r| (r.first, r.last, r.folded)).collect()
}
pub fn fold_at(&self, line: usize) -> Option<(usize, bool)> {
let f = &self.body.folds;
f.binary_search_by_key(&line, |r| r.first).ok().map(|i| (f[i].last, f[i].folded))
}
pub fn set_folded(&mut self, line: usize, folded: bool) -> bool {
let Ok(i) = self.body.folds.binary_search_by_key(&line, |r| r.first) else { return false };
if self.body.folds[i].folded == folded {
return false;
}
self.body.folds[i].folded = folded;
self.body.rehide();
true
}
pub fn toggle_fold(&mut self, line: usize) -> bool {
match self.fold_at(line) {
Some((_, f)) => self.set_folded(line, !f),
None => false,
}
}
pub fn fold_all(&mut self, folded: bool) {
for r in self.body.folds.iter_mut() {
r.folded = folded;
}
self.body.rehide();
}
pub fn fold_around(&self, line: usize) -> Option<(usize, usize)> {
self.body.folds.iter().filter(|r| r.first <= line && line <= r.last).max_by_key(|r| r.first).map(|r| (r.first, r.last))
}
pub fn is_hidden(&self, line: usize) -> bool {
let h = &self.body.hidden;
let k = h.partition_point(|x| x.1 < line);
h.get(k).is_some_and(|x| x.0 <= line)
}
pub fn reveal(&mut self, line: usize) -> bool {
if !self.is_hidden(line) {
return false;
}
for r in self.body.folds.iter_mut() {
if r.folded && r.first < line && line <= r.last {
r.folded = false;
}
}
self.body.rehide();
true
}
pub fn visible_lines(&self) -> usize {
self.body.lines.len() - self.body.hidden_n
}
pub fn row_of_line(&self, line: usize) -> usize {
let mut r = line;
for &(a, z) in &self.body.hidden {
if z < line {
r -= z - a + 1;
} else {
if a <= line {
r -= line - a + 1;
}
break;
}
}
r
}
pub fn line_of_row(&self, row: usize) -> usize {
let mut l = row;
for &(a, z) in &self.body.hidden {
if a <= l {
l += z - a + 1;
} else {
break;
}
}
l.min(self.body.lines.len() - 1)
}
pub fn visible_line(&self, line: usize, down: bool) -> usize {
let h = &self.body.hidden;
let k = h.partition_point(|x| x.1 < line);
match h.get(k) {
Some(&(a, z)) if a <= line => {
if down && z + 1 < self.body.lines.len() {
z + 1
} else {
a - 1
}
}
_ => line,
}
}
pub(crate) fn hidden(&self) -> &[(usize, usize)] {
&self.body.hidden
}
pub fn apply_edits(&mut self, edits: &[TextEdit], label: &str) -> bool {
let mut ed: Vec<TextEdit> = edits
.iter()
.map(|e| TextEdit { a: self.clamp(e.a.min(e.b)), b: self.clamp(e.a.max(e.b)), text: e.text.replace("\r\n", "\n").replace('\r', "\n") })
.collect();
ed.sort_by(|x, y| (x.a, x.b).cmp(&(y.a, y.b)));
if ed.is_empty() || ed.windows(2).any(|w| w[1].a < w[0].b) {
return false;
}
let first = ed[0].a.line;
let last = ed.iter().map(|e| e.b.line).max().unwrap_or(first);
let block = self.text_range(TextPos::new(first, 0), TextPos::new(last, self.body.lines[last].len()));
let base = self.flat(TextPos::new(first, 0));
let mut out = String::with_capacity(block.len() + 16);
let mut at = 0;
let mut map: Vec<(usize, usize, usize)> = Vec::with_capacity(ed.len());
for e in &ed {
let (a, b) = (self.flat(e.a) - base, self.flat(e.b) - base);
out.push_str(&block[at..a]);
out.push_str(&e.text);
map.push((a, b, out.len()));
at = b;
}
out.push_str(&block[at..]);
let new: Vec<String> = out.split('\n').map(String::from).collect();
let dl = new.len() as isize - (last - first + 1) as isize;
let mv = |p: TextPos| -> TextPos {
if p.line < first {
return p;
}
if p.line > last {
return TextPos::new((p.line as isize + dl) as usize, p.byte);
}
let o = self.flat(p) - base;
let mut n = o;
for &(a, b, ne) in &map {
if o >= b {
n = ne + (o - b);
} else {
if o > a {
n = ne;
}
break;
}
}
let line = out[..n].matches('\n').count();
let ls = out[..n].rfind('\n').map_or(0, |k| k + 1);
TextPos::new(first + line, n - ls)
};
let s = self.body.sel;
let sel = TextSel::new(mv(s.anchor), mv(s.caret));
self.edit(first, last - first + 1, new, sel, label, CK_NONE)
}
pub fn replace_text(&mut self, text: &str, label: &str) -> bool {
let new: Vec<String> = lines_of(text).map(String::from).collect();
let old = &self.body.lines;
let pre = old.iter().zip(&new).take_while(|(a, b)| a == b).count();
if pre == old.len() && pre == new.len() {
return false;
}
let room = old.len().min(new.len()) - pre;
let post = old.iter().rev().zip(new.iter().rev()).take(room).take_while(|(a, b)| a == b).count();
let n_old = old.len() - pre - post;
let mid: Vec<String> = new[pre..new.len() - post].to_vec();
let mv = |p: TextPos| -> TextPos {
if p.line < pre {
return p;
}
if p.line >= pre + n_old {
return TextPos::new(p.line - n_old + mid.len(), p.byte);
}
let ink = |c: char| !c.is_whitespace() && c != ';';
let line = &old[p.line][..p.byte.min(old[p.line].len())];
let k: usize = old[pre..p.line].iter().map(|l| l.chars().filter(|&c| ink(c)).count()).sum::<usize>() + line.chars().filter(|&c| ink(c)).count();
let after = line.chars().next_back().is_some_and(ink);
let want = if after { k - 1 } else { k };
let mut seen = 0;
let mut end = TextPos::new(pre, 0);
for (i, l) in mid.iter().enumerate() {
for (b, c) in l.char_indices() {
if !ink(c) {
continue;
}
if seen == want {
return TextPos::new(pre + i, if after { b + c.len_utf8() } else { b });
}
seen += 1;
end = TextPos::new(pre + i, b + c.len_utf8());
}
}
end
};
let s = self.body.sel;
let clamp = |p: TextPos| -> TextPos {
let line = p.line.min(new.len() - 1);
let t = &new[line];
let mut b = p.byte.min(t.len());
while !t.is_char_boundary(b) {
b -= 1;
}
TextPos::new(line, b)
};
let sel = TextSel::new(clamp(mv(s.anchor)), clamp(mv(s.caret)));
self.edit(pre, n_old, mid, sel, label, CK_NONE)
}
pub(crate) fn marks(&self) -> &[Mark] {
&self.body.marks
}
pub(crate) fn mark_range(&self, first: usize, last: usize) -> Range<usize> {
self.body.mark_range(first, last)
}
pub(crate) fn marks_at(&self, p: TextPos) -> Vec<usize> {
let m = &self.body.marks;
let mut v: Vec<usize> = self.body.mark_range(p.line, p.line + 1).filter(|&i| m[i].a <= p && p <= m[i].b).collect();
v.sort_by_key(|&i| m[i].sev);
v
}
pub(crate) fn diag_of(&self, m: &Mark) -> Diag {
let col = |p: TextPos| {
let p = self.clamp(p);
self.body.lines[p.line][..p.byte].chars().count() + 1
};
Diag { line: m.a.line + 1, col: col(m.a), end_line: m.b.line + 1, end_col: col(m.b), severity: m.sev, msg: m.msg.clone() }
}
}
pub(crate) fn find_in(t: &str, q: &str, opts: u8, found: &mut dyn FnMut(usize, usize) -> bool) {
if q.is_empty() {
return;
}
let case = opts & FIND_CASE != 0;
let word = opts & FIND_WORD != 0;
let qc: Vec<char> = q.chars().map(|c| if case { c } else { fold(c) }).collect();
let tb = t.as_bytes();
let first_ascii = q.as_bytes()[0];
let mut i = 0;
while i < tb.len() {
if first_ascii < 0x80 {
let b = tb[i];
let ok = if case { b == first_ascii } else { b.to_ascii_lowercase() == first_ascii.to_ascii_lowercase() };
if !ok {
i += 1;
continue;
}
} else if tb[i] & 0xC0 == 0x80 {
i += 1;
continue;
}
let mut j = i;
let mut ok = true;
for &c in &qc {
let Some(tc) = t[j..].chars().next() else {
ok = false;
break;
};
let tc2 = if case { tc } else { fold(tc) };
if tc2 != c {
ok = false;
break;
}
j += tc.len_utf8();
}
if ok && word {
let before = t[..i].chars().next_back().is_some_and(is_word);
let after = t[j..].chars().next().is_some_and(is_word);
ok = !before && !after;
}
if ok {
if !found(i, j) {
return;
}
i = j.max(i + 1);
} else {
i += 1;
}
while i < tb.len() && tb[i] & 0xC0 == 0x80 {
i += 1;
}
}
}
pub(crate) fn col_in(t: &str, byte: usize, tw: usize) -> usize {
col_from(t, 0, 0, byte, tw)
}
pub(crate) fn byte_in(t: &str, col: usize, tw: usize) -> usize {
byte_from(t, 0, 0, col, tw)
}
pub(crate) fn ascii_run(b: &[u8], i: usize, lim: usize) -> usize {
let mut j = i;
while j < lim && (0x20..0x7F).contains(&b[j]) {
j += 1;
}
if j > i && b.get(j).is_some_and(|&c| c >= 0x80) {
j -= 1;
}
j - i
}
pub(crate) fn cell(t: &str, i: usize, col: usize, tw: usize) -> (usize, usize) {
let n = segment::cluster_end(t, i);
let c = t[i..].chars().next().unwrap_or(' ');
(n, if c == '\t' { tw - col % tw } else { char_cols(c) })
}
pub(crate) fn col_from(t: &str, i: usize, col: usize, byte: usize, tw: usize) -> usize {
let tw = tw.clamp(1, 16);
let (b, byte) = (t.as_bytes(), byte.min(t.len()));
let (mut i, mut col) = (i, col);
while i < byte {
let k = ascii_run(b, i, byte);
if k > 0 {
(i, col) = (i + k, col + k);
continue;
}
let (n, w) = cell(t, i, col, tw);
(i, col) = (n, col + w);
}
col
}
pub(crate) fn byte_from(t: &str, i: usize, c0: usize, col: usize, tw: usize) -> usize {
let (i, c0, n, w) = cell_at(t, i, c0, col, tw);
if i < t.len() && (col - c0) * 2 >= w { n } else { i }
}
pub(crate) fn cell_floor(t: &str, i: usize, c0: usize, col: usize, tw: usize) -> (usize, usize) {
let (i, c0, _, _) = cell_at(t, i, c0, col, tw);
(i, c0)
}
fn cell_at(t: &str, i: usize, c0: usize, col: usize, tw: usize) -> (usize, usize, usize, usize) {
let tw = tw.clamp(1, 16);
let b = t.as_bytes();
let (mut i, mut c0) = (i, c0);
while i < t.len() {
let k = ascii_run(b, i, t.len().min(i + (col - c0) + 1));
if k > 0 {
if col < c0 + k {
let j = i + (col - c0);
return (j, col, j + 1, 1);
}
(i, c0) = (i + k, c0 + k);
continue;
}
let (n, w) = cell(t, i, c0, tw);
if col < c0 + w {
return (i, c0, n, w);
}
(i, c0) = (n, c0 + w);
}
(t.len(), c0, t.len(), 0)
}