Add source caret and block indentation keyboard editing
This commit is contained in:
@@ -18,6 +18,8 @@ mod hosted;
|
|||||||
mod reference_authoring;
|
mod reference_authoring;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod review_navigation_tests;
|
mod review_navigation_tests;
|
||||||
|
#[cfg(test)]
|
||||||
|
mod source_indentation_tests;
|
||||||
pub(crate) mod source_rename;
|
pub(crate) mod source_rename;
|
||||||
mod source_symbols;
|
mod source_symbols;
|
||||||
pub(crate) mod text_workspace;
|
pub(crate) mod text_workspace;
|
||||||
@@ -427,6 +429,7 @@ pub enum Message {
|
|||||||
Pan(iced::Vector),
|
Pan(iced::Vector),
|
||||||
Fit,
|
Fit,
|
||||||
SourceEdit(text_editor::Action),
|
SourceEdit(text_editor::Action),
|
||||||
|
SourceIndent(crate::source_navigation::SymbolContext, bool),
|
||||||
ApplySource,
|
ApplySource,
|
||||||
RevertSource,
|
RevertSource,
|
||||||
SourceUndo,
|
SourceUndo,
|
||||||
@@ -3314,10 +3317,16 @@ impl App {
|
|||||||
self.pan += delta;
|
self.pan += delta;
|
||||||
}
|
}
|
||||||
Message::Fit => self.fit(),
|
Message::Fit => self.fit(),
|
||||||
Message::SourceEdit(action) => {
|
Message::SourceEdit(_) | Message::SourceIndent(..) => {
|
||||||
self.source_jump = None;
|
self.source_jump = None;
|
||||||
let before = self.source_editor.text();
|
let before = self.source_editor.text();
|
||||||
self.source_editor.perform(action);
|
match message {
|
||||||
|
Message::SourceEdit(action) => self.source_editor.perform(action),
|
||||||
|
Message::SourceIndent(_, outdent) => {
|
||||||
|
crate::source_indentation::apply(&mut self.source_editor, outdent);
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
let after = self.source_editor.text();
|
let after = self.source_editor.text();
|
||||||
if after != before {
|
if after != before {
|
||||||
self.source_navigation.refresh(&after);
|
self.source_navigation.refresh(&after);
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
//! Controller acceptance: one indentation gesture is one draft transaction.
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn fixture(source: &str) -> App {
|
||||||
|
let mut app = App::with_project(Project::empty());
|
||||||
|
app.page = Page::Text;
|
||||||
|
app.source_workspace = Workspace::new(
|
||||||
|
oa_core::source_project::SourceBundle {
|
||||||
|
entry: "project.arcadia".into(),
|
||||||
|
files: std::collections::BTreeMap::from([("project.arcadia".into(), source.into())]),
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
app.show_source_file(false);
|
||||||
|
app
|
||||||
|
}
|
||||||
|
fn indent(app: &mut App, outdent: bool) {
|
||||||
|
let message = Message::SourceIndent(app.source_symbol_context(), outdent);
|
||||||
|
let _ = app.update(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn indentation_updates_draft_snapshot_version_and_single_undo_redo_without_model_change() {
|
||||||
|
let source = "雨\r\n🚀\r\n";
|
||||||
|
let mut app = fixture(source);
|
||||||
|
app.source_editor.perform(text_editor::Action::SelectAll);
|
||||||
|
let version = app.source_version;
|
||||||
|
let revision = app.source_workspace.revision();
|
||||||
|
let model = app.document.revision();
|
||||||
|
indent(&mut app, false);
|
||||||
|
let after = " 雨\r\n 🚀\r\n";
|
||||||
|
assert_eq!(app.source_editor.text(), after);
|
||||||
|
assert_eq!(app.source_snapshot.as_ref(), after);
|
||||||
|
assert_eq!(app.source_workspace.files["project.arcadia"].text, after);
|
||||||
|
assert!(app.source_workspace.has_edits());
|
||||||
|
assert_eq!(app.source_version, version + 1);
|
||||||
|
assert_eq!(app.source_workspace.revision(), revision + 1);
|
||||||
|
assert_eq!(app.document.revision(), model);
|
||||||
|
let _ = app.update(Message::SourceUndo);
|
||||||
|
assert_eq!(app.source_editor.text(), source);
|
||||||
|
assert!(!app.source_workspace.has_edits());
|
||||||
|
let _ = app.update(Message::SourceRedo);
|
||||||
|
assert_eq!(app.source_editor.text(), after);
|
||||||
|
assert_eq!(app.document.revision(), model);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn stale_hidden_and_modal_indentation_is_rejected_before_navigation_invalidation() {
|
||||||
|
for rejected in 0..7 {
|
||||||
|
let mut app = fixture("alpha");
|
||||||
|
let mut context = app.source_symbol_context();
|
||||||
|
match rejected {
|
||||||
|
0 => context.path = "elsewhere.arcadia".into(),
|
||||||
|
1 => context.session = Id::new_v4(),
|
||||||
|
2 => app.page = Page::Home,
|
||||||
|
3 => app.busy = true,
|
||||||
|
4 => app.delete_pending = true,
|
||||||
|
5 => app.hosted.visible = true,
|
||||||
|
_ => app.pending = Some(Pending::New),
|
||||||
|
}
|
||||||
|
let interaction = app.source_symbols.interaction;
|
||||||
|
let version = app.source_version;
|
||||||
|
let _ = app.update(Message::SourceIndent(context, false));
|
||||||
|
assert_eq!(app.source_editor.text(), "alpha");
|
||||||
|
assert_eq!(app.source_version, version);
|
||||||
|
assert_eq!(app.source_symbols.interaction, interaction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_op_outdent_does_not_add_history_or_increment_source_version() {
|
||||||
|
let mut app = fixture("alpha");
|
||||||
|
let version = app.source_version;
|
||||||
|
indent(&mut app, true);
|
||||||
|
assert_eq!(app.source_version, version);
|
||||||
|
assert!(!app.source_workspace.undo(false));
|
||||||
|
}
|
||||||
@@ -33,6 +33,9 @@ fn admission_notice(error: query::AdmissionError) -> String {
|
|||||||
impl App {
|
impl App {
|
||||||
pub(super) fn source_input_envelope(&self, message: &Message) -> bool {
|
pub(super) fn source_input_envelope(&self, message: &Message) -> bool {
|
||||||
match message {
|
match message {
|
||||||
|
Message::SourceIndent(context, _) => {
|
||||||
|
context == &self.source_symbol_context() && self.source_navigation_available()
|
||||||
|
}
|
||||||
Message::SourceSymbols(context, event) => {
|
Message::SourceSymbols(context, event) => {
|
||||||
context.session == self.source_workspace.session()
|
context.session == self.source_workspace.session()
|
||||||
&& context.path == self.source_workspace.active
|
&& context.path == self.source_workspace.active
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ mod recovery;
|
|||||||
mod route_edit;
|
mod route_edit;
|
||||||
mod semantic_review;
|
mod semantic_review;
|
||||||
mod source_annotations;
|
mod source_annotations;
|
||||||
|
mod source_indentation;
|
||||||
mod source_navigation;
|
mod source_navigation;
|
||||||
mod source_rename;
|
mod source_rename;
|
||||||
mod source_tree;
|
mod source_tree;
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
//! Real mounted Iced keyboard dispatch, using the production binding function.
|
||||||
|
use crate::{app::Message, source_navigation::SymbolContext, views::source_tests::FontLayout};
|
||||||
|
use iced::{
|
||||||
|
Element, Font, Rectangle, Size,
|
||||||
|
advanced::{Layout, Shell, layout, text, widget as core_widget},
|
||||||
|
keyboard::{
|
||||||
|
self, Key, Modifiers,
|
||||||
|
key::{Code, Named, Physical},
|
||||||
|
},
|
||||||
|
widget::{
|
||||||
|
self,
|
||||||
|
text_editor::{Content, State},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mounted_tab_dispatches_only_for_focused_editable_editor_and_plain_shift_variants() {
|
||||||
|
for focused in [false, true] {
|
||||||
|
for editable in [false, true] {
|
||||||
|
for modifiers in [
|
||||||
|
Modifiers::empty(),
|
||||||
|
Modifiers::SHIFT,
|
||||||
|
Modifiers::CTRL,
|
||||||
|
Modifiers::ALT,
|
||||||
|
Modifiers::LOGO,
|
||||||
|
] {
|
||||||
|
let context = SymbolContext {
|
||||||
|
session: oa_core::Id::new_v4(),
|
||||||
|
path: "project.arcadia".into(),
|
||||||
|
};
|
||||||
|
let content = Content::<FontLayout>::with_text("rain");
|
||||||
|
let mut editor = widget::text_editor(&content)
|
||||||
|
.font(Font::MONOSPACE)
|
||||||
|
.size(14)
|
||||||
|
.padding(16)
|
||||||
|
.height(iced::Fill)
|
||||||
|
.key_binding(|press| crate::views::source_binding(press, &context, None));
|
||||||
|
if editable {
|
||||||
|
editor = editor.on_action(Message::SourceEdit);
|
||||||
|
}
|
||||||
|
let mut view: Element<'_, Message, iced::Theme, FontLayout> = editor.into();
|
||||||
|
let mut tree = core_widget::Tree::new(view.as_widget());
|
||||||
|
let size = Size::new(600., 240.);
|
||||||
|
let node = view.as_widget_mut().layout(
|
||||||
|
&mut tree,
|
||||||
|
&FontLayout,
|
||||||
|
&layout::Limits::new(Size::ZERO, size),
|
||||||
|
);
|
||||||
|
if focused {
|
||||||
|
tree.state
|
||||||
|
.downcast_mut::<State<text::highlighter::PlainText>>()
|
||||||
|
.focus();
|
||||||
|
}
|
||||||
|
let event = iced::Event::Keyboard(keyboard::Event::KeyPressed {
|
||||||
|
key: Key::Named(Named::Tab),
|
||||||
|
modified_key: Key::Named(Named::Tab),
|
||||||
|
physical_key: Physical::Code(Code::Tab),
|
||||||
|
location: keyboard::Location::Standard,
|
||||||
|
modifiers,
|
||||||
|
text: None,
|
||||||
|
repeat: false,
|
||||||
|
});
|
||||||
|
let mut messages = Vec::new();
|
||||||
|
let captured;
|
||||||
|
{
|
||||||
|
let mut shell = Shell::new(&mut messages);
|
||||||
|
view.as_widget_mut().update(
|
||||||
|
&mut tree,
|
||||||
|
&event,
|
||||||
|
Layout::new(&node),
|
||||||
|
iced::mouse::Cursor::Unavailable,
|
||||||
|
&FontLayout,
|
||||||
|
&mut iced::advanced::clipboard::Null,
|
||||||
|
&mut shell,
|
||||||
|
&Rectangle::with_size(size),
|
||||||
|
);
|
||||||
|
captured = shell.is_event_captured();
|
||||||
|
}
|
||||||
|
let plain = modifiers.is_empty() || modifiers == Modifiers::SHIFT;
|
||||||
|
let expected = focused && editable && plain;
|
||||||
|
let indentation: Vec<_> = messages
|
||||||
|
.iter()
|
||||||
|
.filter_map(|message| match message {
|
||||||
|
Message::SourceIndent(emitted, outdent) => Some((emitted, outdent)),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
assert_eq!(
|
||||||
|
indentation.len(),
|
||||||
|
usize::from(expected),
|
||||||
|
"focused={focused}, editable={editable}, modifiers={modifiers:?}"
|
||||||
|
);
|
||||||
|
if expected {
|
||||||
|
assert!(captured, "Tab must not escape to global focus traversal");
|
||||||
|
assert_eq!(indentation[0], (&context, &modifiers.shift()));
|
||||||
|
} else if !focused || !editable {
|
||||||
|
assert!(
|
||||||
|
!captured,
|
||||||
|
"non-editing Tab must remain available to focus traversal"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
use super::*;
|
||||||
|
use iced::widget::text_editor::Motion;
|
||||||
|
|
||||||
|
type Buffer = Content<iced::Renderer>;
|
||||||
|
fn at(line: usize, column: usize) -> Position {
|
||||||
|
Position { line, column }
|
||||||
|
}
|
||||||
|
fn range(start: Position, end: Position, reverse: bool) -> Cursor {
|
||||||
|
Cursor {
|
||||||
|
position: if reverse { start } else { end },
|
||||||
|
selection: Some(if reverse { end } else { start }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn caret_inserts_four_spaces_and_outdent_removes_only_leading_whitespace() {
|
||||||
|
let mut content = Buffer::with_text(" 雨🚀\r\nnext");
|
||||||
|
content.move_to(Cursor {
|
||||||
|
position: at(0, " 雨".len()),
|
||||||
|
selection: None,
|
||||||
|
});
|
||||||
|
apply(&mut content, false);
|
||||||
|
assert_eq!(content.text(), " 雨 🚀\r\nnext");
|
||||||
|
assert_eq!(content.cursor().position, at(0, " 雨 ".len()));
|
||||||
|
apply(&mut content, true);
|
||||||
|
assert_eq!(content.text(), "雨 🚀\r\nnext");
|
||||||
|
assert_eq!(content.cursor().position, at(0, "雨 ".len()));
|
||||||
|
apply(&mut content, true);
|
||||||
|
assert_eq!(content.text(), "雨 🚀\r\nnext");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn directional_blocks_preserve_unicode_endings_and_exclude_final_zero_column() {
|
||||||
|
let source = "雨\r\n🚀\nlast\r\n";
|
||||||
|
for reverse in [false, true] {
|
||||||
|
let mut content = Buffer::with_text(source);
|
||||||
|
let original = range(at(0, 0), at(2, 0), reverse);
|
||||||
|
content.move_to(original);
|
||||||
|
apply(&mut content, false);
|
||||||
|
assert_eq!(content.text(), " 雨\r\n 🚀\nlast\r\n");
|
||||||
|
assert_eq!(content.cursor(), range(at(0, 4), at(2, 0), reverse));
|
||||||
|
apply(&mut content, true);
|
||||||
|
assert_eq!(content.text(), source);
|
||||||
|
assert_eq!(content.cursor(), original);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn outdent_handles_tabs_partial_spaces_and_clamps_endpoint_inside_removed_indent() {
|
||||||
|
for reverse in [false, true] {
|
||||||
|
let mut content = Buffer::with_text("\t雨\r\n 🚀\n fin");
|
||||||
|
content.move_to(range(at(0, 0), at(2, 2), reverse));
|
||||||
|
apply(&mut content, true);
|
||||||
|
assert_eq!(content.text(), "雨\r\n🚀\nfin");
|
||||||
|
assert_eq!(content.cursor(), range(at(0, 0), at(2, 0), reverse));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn word_and_line_selections_indent_effective_range_in_both_directions() {
|
||||||
|
for line_mode in [false, true] {
|
||||||
|
for reverse in [false, true] {
|
||||||
|
let mut content = Buffer::with_text("one alpha\r\ntwo beta\nthree gamma");
|
||||||
|
content.move_to(Cursor {
|
||||||
|
position: if reverse { at(2, 8) } else { at(0, 6) },
|
||||||
|
selection: None,
|
||||||
|
});
|
||||||
|
content.perform(if line_mode {
|
||||||
|
Action::SelectLine
|
||||||
|
} else {
|
||||||
|
Action::SelectWord
|
||||||
|
});
|
||||||
|
content.perform(Action::Select(if reverse {
|
||||||
|
Motion::DocumentStart
|
||||||
|
} else {
|
||||||
|
Motion::DocumentEnd
|
||||||
|
}));
|
||||||
|
let selected = content.selection().unwrap();
|
||||||
|
apply(&mut content, false);
|
||||||
|
assert_eq!(
|
||||||
|
content.text(),
|
||||||
|
" one alpha\r\n two beta\n three gamma"
|
||||||
|
);
|
||||||
|
let cursor = content.cursor();
|
||||||
|
assert_eq!(
|
||||||
|
cursor.position.line < cursor.selection.unwrap().line,
|
||||||
|
reverse
|
||||||
|
);
|
||||||
|
apply(&mut content, true);
|
||||||
|
assert_eq!(content.text(), "one alpha\r\ntwo beta\nthree gamma");
|
||||||
|
assert_eq!(content.selection(), Some(selected));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_op_outdent_preserves_semantic_word_selection_and_last_empty_line_is_excluded() {
|
||||||
|
let mut content = Buffer::with_text("alpha tail\r\n");
|
||||||
|
content.move_to(Cursor {
|
||||||
|
position: at(0, 2),
|
||||||
|
selection: None,
|
||||||
|
});
|
||||||
|
content.perform(Action::SelectWord);
|
||||||
|
let raw = content.cursor();
|
||||||
|
apply(&mut content, true);
|
||||||
|
assert_eq!(content.cursor(), raw);
|
||||||
|
assert_eq!(content.selection().as_deref(), Some("alpha"));
|
||||||
|
content.perform(Action::SelectAll);
|
||||||
|
apply(&mut content, false);
|
||||||
|
assert_eq!(content.text(), " alpha tail\r\n");
|
||||||
|
assert_eq!(content.cursor().position, at(1, 0));
|
||||||
|
}
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
//! Effective selected-byte ranges for a committed departure from a source view.
|
//! Effective selected-byte ranges for committed source departures and edits.
|
||||||
//!
|
//!
|
||||||
//! Iced's `Cursor` omits cosmic-text's Word/Line selection kind. Saving its raw
|
//! Iced's `Cursor` omits cosmic-text's Word/Line selection kind. Saving its raw
|
||||||
//! endpoints can therefore save an empty range for a visibly selected word/line.
|
//! endpoints can therefore save an empty range for a visibly selected word/line.
|
||||||
//! Capture only after navigation admission and destination validation succeed,
|
//! Departure capture requires navigation admission and destination validation,
|
||||||
//! immediately before replacing/leaving the live editor. Never use this to inspect
|
//! immediately before replacing/leaving the live editor. Edit capture requires
|
||||||
//! pending queries, failed/no-op navigation, or restore focus to the same editor.
|
//! an admitted edit with a nonempty patch plan. Neither is a passive query:
|
||||||
|
//! never use these to inspect pending queries, failed/no-op navigation, or restore
|
||||||
|
//! focus to the same editor.
|
||||||
use iced::advanced::{graphics, text};
|
use iced::advanced::{graphics, text};
|
||||||
use iced::widget::text_editor::{Action, Content, Cursor, Motion, Position};
|
use iced::widget::text_editor::{Action, Content, Cursor, Motion, Position};
|
||||||
|
|
||||||
@@ -21,6 +23,23 @@ use iced::widget::text_editor::{Action, Content, Cursor, Motion, Position};
|
|||||||
/// selection joins editor lines with LF regardless of physical CRLF/LF endings.
|
/// selection joins editor lines with LF regardless of physical CRLF/LF endings.
|
||||||
/// Only the selected text is temporarily copied; retained state is one Cursor.
|
/// Only the selected text is temporarily copied; retained state is one Cursor.
|
||||||
pub fn capture<R>(content: &mut Content<R>) -> Cursor
|
pub fn capture<R>(content: &mut Content<R>) -> Cursor
|
||||||
|
where
|
||||||
|
R: text::Renderer<Editor = graphics::text::Editor>,
|
||||||
|
{
|
||||||
|
effective_range(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Capture immediately before a committed indentation edit. Like departure,
|
||||||
|
/// this converts Word/Line selections to their effective directional byte range.
|
||||||
|
/// The caller restores the transformed range after editing the same Content.
|
||||||
|
pub fn capture_for_edit<R>(content: &mut Content<R>) -> Cursor
|
||||||
|
where
|
||||||
|
R: text::Renderer<Editor = graphics::text::Editor>,
|
||||||
|
{
|
||||||
|
effective_range(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn effective_range<R>(content: &mut Content<R>) -> Cursor
|
||||||
where
|
where
|
||||||
R: text::Renderer<Editor = graphics::text::Editor>,
|
R: text::Renderer<Editor = graphics::text::Editor>,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1178,6 +1178,15 @@ pub(crate) fn source_binding(
|
|||||||
use crate::source_navigation::{SymbolEvent, query::Action};
|
use crate::source_navigation::{SymbolEvent, query::Action};
|
||||||
use iced::keyboard::{Key, key::Named};
|
use iced::keyboard::{Key, key::Named};
|
||||||
use widget::text_editor::{Binding, Status};
|
use widget::text_editor::{Binding, Status};
|
||||||
|
if matches!(press.status, Status::Focused { .. })
|
||||||
|
&& press.key == Key::Named(Named::Tab)
|
||||||
|
&& (press.modifiers.is_empty() || press.modifiers == iced::keyboard::Modifiers::SHIFT)
|
||||||
|
{
|
||||||
|
return Some(Binding::Custom(Message::SourceIndent(
|
||||||
|
context.clone(),
|
||||||
|
press.modifiers.shift(),
|
||||||
|
)));
|
||||||
|
}
|
||||||
if matches!(press.status, Status::Focused { .. })
|
if matches!(press.status, Status::Focused { .. })
|
||||||
&& press.modifiers.is_empty()
|
&& press.modifiers.is_empty()
|
||||||
&& press.key == Key::Named(Named::F2)
|
&& press.key == Key::Named(Named::F2)
|
||||||
|
|||||||
Reference in New Issue
Block a user