Add source caret and block indentation keyboard editing
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
//! Source indentation is one workspace edit, with physical line endings intact.
|
||||
use iced::{
|
||||
advanced::{graphics, text},
|
||||
widget::text_editor::{Action, Content, Cursor, Edit, Position},
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
const INDENT: &str = " ";
|
||||
|
||||
/// Plain Tab inserts one canonical four-space level at a caret, or prefixes
|
||||
/// every selected line. Shift+Tab removes one leading tab or up to four spaces.
|
||||
/// A selection ending at column zero excludes that final line. All patches are
|
||||
/// local to lines, so CRLF, LF and Unicode bytes outside indentation survive.
|
||||
/// The caller records the resulting buffer once in workspace undo/history.
|
||||
pub fn apply<R>(content: &mut Content<R>, outdent: bool)
|
||||
where
|
||||
R: text::Renderer<Editor = graphics::text::Editor>,
|
||||
{
|
||||
let raw = content.cursor();
|
||||
let selected = content.selection().is_some_and(|s| !s.is_empty());
|
||||
if !selected && !outdent {
|
||||
content.perform(Action::Edit(Edit::Paste(Arc::new(INDENT.into()))));
|
||||
return;
|
||||
}
|
||||
// Determine affected lines before normalizing Word/Line mode. Those modes
|
||||
// expand columns within their endpoint lines; the copied LF count reveals
|
||||
// whether a zero-column endpoint includes the next line or only its edge.
|
||||
let anchor = raw.selection.unwrap_or(raw.position);
|
||||
let first = raw.position.line.min(anchor.line);
|
||||
let mut last = raw.position.line.max(anchor.line);
|
||||
if selected {
|
||||
let selected_text = content.selection().unwrap();
|
||||
let line_breaks = selected_text.bytes().filter(|b| *b == b'\n').count();
|
||||
// Effective selection starts on the minimum raw endpoint line.
|
||||
last = first + line_breaks;
|
||||
if last > first && selected_text.ends_with('\n') {
|
||||
last -= 1;
|
||||
}
|
||||
}
|
||||
let patches: Vec<_> = (first..=last)
|
||||
.filter_map(|line| {
|
||||
let removed = if outdent {
|
||||
let text = content.line(line)?;
|
||||
if text.text.starts_with('\t') {
|
||||
1
|
||||
} else {
|
||||
text.text.bytes().take(4).take_while(|b| *b == b' ').count()
|
||||
}
|
||||
} else {
|
||||
0
|
||||
};
|
||||
(!outdent || removed > 0).then_some((line, removed))
|
||||
})
|
||||
.collect();
|
||||
if patches.is_empty() {
|
||||
return;
|
||||
}
|
||||
let cursor = crate::source_navigation::selection::capture_for_edit(content);
|
||||
let replacement = Arc::new(if outdent {
|
||||
String::new()
|
||||
} else {
|
||||
INDENT.into()
|
||||
});
|
||||
// Descending order leaves Iced's earliest changed line at the first patch.
|
||||
for &(line, removed) in patches.iter().rev() {
|
||||
content.move_to(Cursor {
|
||||
position: Position { line, column: 0 },
|
||||
selection: Some(Position {
|
||||
line,
|
||||
column: removed,
|
||||
}),
|
||||
});
|
||||
content.perform(Action::Edit(Edit::Paste(replacement.clone())));
|
||||
}
|
||||
let transform = |mut position: Position| {
|
||||
if let Some((_, removed)) = patches.iter().find(|(line, _)| *line == position.line) {
|
||||
position.column = if outdent {
|
||||
position.column.saturating_sub(*removed)
|
||||
} else {
|
||||
position.column + INDENT.len()
|
||||
};
|
||||
}
|
||||
position
|
||||
};
|
||||
content.move_to(Cursor {
|
||||
position: transform(cursor.position),
|
||||
selection: cursor.selection.map(transform),
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(all(test, debug_assertions))]
|
||||
mod keyboard_tests;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
Reference in New Issue
Block a user