aboutsummaryrefslogtreecommitdiff
path: root/helix-term/src
diff options
context:
space:
mode:
Diffstat (limited to 'helix-term/src')
-rw-r--r--helix-term/src/application.rs26
-rw-r--r--helix-term/src/args.rs10
-rw-r--r--helix-term/src/commands.rs277
-rw-r--r--helix-term/src/commands/lsp.rs8
-rw-r--r--helix-term/src/commands/typed.rs130
-rw-r--r--helix-term/src/keymap/default.rs42
-rw-r--r--helix-term/src/ui/completion.rs12
-rw-r--r--helix-term/src/ui/editor.rs191
-rw-r--r--helix-term/src/ui/fuzzy_match.rs74
-rw-r--r--helix-term/src/ui/fuzzy_match/test.rs47
-rw-r--r--helix-term/src/ui/lsp.rs3
-rw-r--r--helix-term/src/ui/menu.rs2
-rw-r--r--helix-term/src/ui/mod.rs42
-rw-r--r--helix-term/src/ui/picker.rs64
-rw-r--r--helix-term/src/ui/statusline.rs18
15 files changed, 669 insertions, 277 deletions
diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs
index cd499f1c..4bb36b59 100644
--- a/helix-term/src/application.rs
+++ b/helix-term/src/application.rs
@@ -224,8 +224,8 @@ impl Application {
#[cfg(windows)]
let signals = futures_util::stream::empty();
#[cfg(not(windows))]
- let signals =
- Signals::new(&[signal::SIGTSTP, signal::SIGCONT]).context("build signal handler")?;
+ let signals = Signals::new(&[signal::SIGTSTP, signal::SIGCONT, signal::SIGUSR1])
+ .context("build signal handler")?;
let app = Self {
compositor,
@@ -426,23 +426,22 @@ impl Application {
self.compositor.load_cursor();
self.render();
}
+ signal::SIGUSR1 => {
+ self.refresh_config();
+ self.render();
+ }
_ => unreachable!(),
}
}
pub fn handle_idle_timeout(&mut self) {
- use crate::compositor::EventResult;
- let editor_view = self
- .compositor
- .find::<ui::EditorView>()
- .expect("expected at least one EditorView");
-
let mut cx = crate::compositor::Context {
editor: &mut self.editor,
jobs: &mut self.jobs,
scroll: None,
};
- if let EventResult::Consumed(_) = editor_view.handle_idle_timeout(&mut cx) {
+ let should_render = self.compositor.handle_event(&Event::IdleTimeout, &mut cx);
+ if should_render {
self.render();
}
}
@@ -866,9 +865,16 @@ impl Application {
}));
self.event_loop(input_stream).await;
- self.close().await?;
+
+ let err = self.close().await.err();
+
restore_term()?;
+ if let Some(err) = err {
+ self.editor.exit_code = 1;
+ eprintln!("Error: {}", err);
+ }
+
Ok(self.editor.exit_code)
}
diff --git a/helix-term/src/args.rs b/helix-term/src/args.rs
index 48c86633..dd787f1f 100644
--- a/helix-term/src/args.rs
+++ b/helix-term/src/args.rs
@@ -32,8 +32,14 @@ impl Args {
"--version" => args.display_version = true,
"--help" => args.display_help = true,
"--tutor" => args.load_tutor = true,
- "--vsplit" => args.split = Some(Layout::Vertical),
- "--hsplit" => args.split = Some(Layout::Horizontal),
+ "--vsplit" => match args.split {
+ Some(_) => anyhow::bail!("can only set a split once of a specific type"),
+ None => args.split = Some(Layout::Vertical),
+ },
+ "--hsplit" => match args.split {
+ Some(_) => anyhow::bail!("can only set a split once of a specific type"),
+ None => args.split = Some(Layout::Horizontal),
+ },
"--health" => {
args.health = true;
args.health_arg = argv.next_if(|opt| !opt.starts_with('-'));
diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs
index fb1a4b38..5073651b 100644
--- a/helix-term/src/commands.rs
+++ b/helix-term/src/commands.rs
@@ -27,6 +27,7 @@ use helix_core::{
SmallVec, Tendril, Transaction,
};
use helix_view::{
+ apply_transaction,
clipboard::ClipboardType,
document::{FormatterError, Mode, SCRATCH_BUFFER_NAME},
editor::{Action, Motion},
@@ -273,8 +274,8 @@ impl MappableCommand {
diagnostics_picker, "Open diagnostic picker",
workspace_diagnostics_picker, "Open workspace diagnostic picker",
last_picker, "Open last picker",
- prepend_to_line, "Insert at start of line",
- append_to_line, "Append to end of line",
+ insert_at_line_start, "Insert at start of line",
+ insert_at_line_end, "Insert at end of line",
open_below, "Open new line below selection",
open_above, "Open new line above selection",
normal_mode, "Enter normal mode",
@@ -346,6 +347,7 @@ impl MappableCommand {
unindent, "Unindent selection",
format_selections, "Format selection",
join_selections, "Join lines inside selection",
+ join_selections_space, "Join lines inside selection and select spaces",
keep_selections, "Keep selections matching regex",
remove_selections, "Remove selections matching regex",
align_selections, "Align selections in column",
@@ -858,7 +860,7 @@ fn align_selections(cx: &mut Context) {
changes.sort_unstable_by_key(|(from, _, _)| *from);
let transaction = Transaction::change(doc.text(), changes.into_iter());
- doc.apply(&transaction, view.id);
+ apply_transaction(&transaction, doc, view);
}
fn goto_window(cx: &mut Context, align: Align) {
@@ -885,8 +887,12 @@ fn goto_window(cx: &mut Context, align: Align) {
.min(last_line.saturating_sub(scrolloff));
let pos = doc.text().line_to_char(line);
-
- doc.set_selection(view.id, Selection::point(pos));
+ let text = doc.text().slice(..);
+ let selection = doc
+ .selection(view.id)
+ .clone()
+ .transform(|range| range.put_cursor(text, pos, cx.editor.mode == Mode::Select));
+ doc.set_selection(view.id, selection);
}
fn goto_window_top(cx: &mut Context) {
@@ -1284,7 +1290,7 @@ fn replace(cx: &mut Context) {
}
});
- doc.apply(&transaction, view.id);
+ apply_transaction(&transaction, doc, view);
}
})
}
@@ -1301,7 +1307,7 @@ where
(range.from(), range.to(), Some(text))
});
- doc.apply(&transaction, view.id);
+ apply_transaction(&transaction, doc, view);
}
fn switch_case(cx: &mut Context) {
@@ -1511,7 +1517,8 @@ fn select_regex(cx: &mut Context) {
"select:".into(),
Some(reg),
ui::completers::none,
- move |view, doc, regex, event| {
+ move |editor, regex, event| {
+ let (view, doc) = current!(editor);
if !matches!(event, PromptEvent::Update | PromptEvent::Validate) {
return;
}
@@ -1532,7 +1539,8 @@ fn split_selection(cx: &mut Context) {
"split:".into(),
Some(reg),
ui::completers::none,
- move |view, doc, regex, event| {
+ move |editor, regex, event| {
+ let (view, doc) = current!(editor);
if !matches!(event, PromptEvent::Update | PromptEvent::Validate) {
return;
}
@@ -1556,15 +1564,16 @@ fn split_selection_on_newline(cx: &mut Context) {
#[allow(clippy::too_many_arguments)]
fn search_impl(
- doc: &mut Document,
- view: &mut View,
+ editor: &mut Editor,
contents: &str,
regex: &Regex,
movement: Movement,
direction: Direction,
scrolloff: usize,
wrap_around: bool,
+ show_warnings: bool,
) {
+ let (view, doc) = current!(editor);
let text = doc.text().slice(..);
let selection = doc.selection(view.id);
@@ -1594,17 +1603,29 @@ fn search_impl(
Direction::Backward => regex.find_iter(&contents[..start]).last(),
};
- if wrap_around && mat.is_none() {
- mat = match direction {
- Direction::Forward => regex.find(contents),
- Direction::Backward => {
- offset = start;
- regex.find_iter(&contents[start..]).last()
+ if mat.is_none() {
+ if wrap_around {
+ mat = match direction {
+ Direction::Forward => regex.find(contents),
+ Direction::Backward => {
+ offset = start;
+ regex.find_iter(&contents[start..]).last()
+ }
+ };
+ }
+ if show_warnings {
+ if wrap_around && mat.is_some() {
+ editor.set_status("Wrapped around document");
+ } else {
+ editor.set_error("No more matches");
}
}
- // TODO: message on wraparound
}
+ let (view, doc) = current!(editor);
+ let text = doc.text().slice(..);
+ let selection = doc.selection(view.id);
+
if let Some(mat) = mat {
let start = text.byte_to_char(mat.start() + offset);
let end = text.byte_to_char(mat.end() + offset);
@@ -1680,19 +1701,19 @@ fn searcher(cx: &mut Context, direction: Direction) {
.map(|comp| (0.., std::borrow::Cow::Owned(comp.clone())))
.collect()
},
- move |view, doc, regex, event| {
+ move |editor, regex, event| {
if !matches!(event, PromptEvent::Update | PromptEvent::Validate) {
return;
}
search_impl(
- doc,
- view,
+ editor,
&contents,
&regex,
Movement::Move,
direction,
scrolloff,
wrap_around,
+ false,
);
},
);
@@ -1702,7 +1723,7 @@ fn search_next_or_prev_impl(cx: &mut Context, movement: Movement, direction: Dir
let count = cx.count();
let config = cx.editor.config();
let scrolloff = config.scrolloff;
- let (view, doc) = current!(cx.editor);
+ let (_, doc) = current!(cx.editor);
let registers = &cx.editor.registers;
if let Some(query) = registers.read('/').and_then(|query| query.last()) {
let contents = doc.text().slice(..).to_string();
@@ -1720,14 +1741,14 @@ fn search_next_or_prev_impl(cx: &mut Context, movement: Movement, direction: Dir
{
for _ in 0..count {
search_impl(
- doc,
- view,
+ cx.editor,
&contents,
&regex,
movement,
direction,
scrolloff,
wrap_around,
+ true,
);
}
} else {
@@ -1825,7 +1846,7 @@ fn global_search(cx: &mut Context) {
.map(|comp| (0.., std::borrow::Cow::Owned(comp.clone())))
.collect()
},
- move |_view, _doc, regex, event| {
+ move |_editor, regex, event| {
if event != PromptEvent::Validate {
return;
}
@@ -1844,10 +1865,15 @@ fn global_search(cx: &mut Context) {
.hidden(file_picker_config.hidden)
.parents(file_picker_config.parents)
.ignore(file_picker_config.ignore)
+ .follow_links(file_picker_config.follow_symlinks)
.git_ignore(file_picker_config.git_ignore)
.git_global(file_picker_config.git_global)
.git_exclude(file_picker_config.git_exclude)
.max_depth(file_picker_config.max_depth)
+ // We always want to ignore the .git directory, otherwise if
+ // `ignore` is turned off above, we end up with a lot of noise
+ // in our picker.
+ .filter_entry(|entry| entry.file_name() != ".git")
.build_parallel()
.run(|| {
let mut searcher = searcher.clone();
@@ -2092,7 +2118,7 @@ fn delete_selection_impl(cx: &mut Context, op: Operation) {
let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
(range.from(), range.to(), None)
});
- doc.apply(&transaction, view.id);
+ apply_transaction(&transaction, doc, view);
match op {
Operation::Delete => {
@@ -2106,14 +2132,11 @@ fn delete_selection_impl(cx: &mut Context, op: Operation) {
}
#[inline]
-fn delete_selection_insert_mode(doc: &mut Document, view: &View, selection: &Selection) {
- let view_id = view.id;
-
- // then delete
+fn delete_selection_insert_mode(doc: &mut Document, view: &mut View, selection: &Selection) {
let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
(range.from(), range.to(), None)
});
- doc.apply(&transaction, view_id);
+ apply_transaction(&transaction, doc, view);
}
fn delete_selection(cx: &mut Context) {
@@ -2161,10 +2184,7 @@ fn ensure_selections_forward(cx: &mut Context) {
let selection = doc
.selection(view.id)
.clone()
- .transform(|r| match r.direction() {
- Direction::Forward => r,
- Direction::Backward => r.flip(),
- });
+ .transform(|r| r.with_direction(Direction::Forward));
doc.set_selection(view.id, selection);
}
@@ -2207,12 +2227,12 @@ fn append_mode(cx: &mut Context) {
.iter()
.last()
.expect("selection should always have at least one range");
- if !last_range.is_empty() && last_range.head == end {
+ if !last_range.is_empty() && last_range.to() == end {
let transaction = Transaction::change(
doc.text(),
[(end, end, Some(doc.line_ending.as_str().into()))].into_iter(),
);
- doc.apply(&transaction, view.id);
+ apply_transaction(&transaction, doc, view);
}
let selection = doc.selection(view.id).clone().transform(|range| {
@@ -2410,12 +2430,12 @@ impl ui::menu::Item for MappableCommand {
match self {
MappableCommand::Typable { doc, name, .. } => match keymap.get(name as &String) {
- Some(bindings) => format!("{} ({})", doc, fmt_binding(bindings)).into(),
- None => doc.as_str().into(),
+ Some(bindings) => format!("{} ({}) [{}]", doc, fmt_binding(bindings), name).into(),
+ None => format!("{} [{}]", doc, name).into(),
},
MappableCommand::Static { doc, name, .. } => match keymap.get(*name) {
- Some(bindings) => format!("{} ({})", doc, fmt_binding(bindings)).into(),
- None => (*doc).into(),
+ Some(bindings) => format!("{} ({}) [{}]", doc, fmt_binding(bindings), name).into(),
+ None => format!("{} [{}]", doc, name).into(),
},
}
}
@@ -2465,13 +2485,13 @@ fn last_picker(cx: &mut Context) {
}
// I inserts at the first nonwhitespace character of each line with a selection
-fn prepend_to_line(cx: &mut Context) {
+fn insert_at_line_start(cx: &mut Context) {
goto_first_nonwhitespace(cx);
enter_insert_mode(cx);
}
// A inserts at the end of each line with a selection
-fn append_to_line(cx: &mut Context) {
+fn insert_at_line_end(cx: &mut Context) {
enter_insert_mode(cx);
let (view, doc) = current!(cx.editor);
@@ -2512,7 +2532,7 @@ async fn make_format_callback(
let doc = doc_mut!(editor, &doc_id);
let view = view_mut!(editor);
if doc.version() == doc_version {
- doc.apply(&format, view.id);
+ apply_transaction(&format, doc, view);
doc.append_changes_to_history(view.id);
doc.detect_indent_and_line_ending();
view.ensure_cursor_in_view(doc, scrolloff);
@@ -2599,7 +2619,7 @@ fn open(cx: &mut Context, open: Open) {
transaction = transaction.with_selection(Selection::new(ranges, selection.primary_index()));
- doc.apply(&transaction, view.id);
+ apply_transaction(&transaction, doc, view);
}
// o inserts a new line after each line with a selection
@@ -2620,7 +2640,7 @@ fn normal_mode(cx: &mut Context) {
cx.editor.mode = Mode::Normal;
let (view, doc) = current!(cx.editor);
- try_restore_indent(doc, view.id);
+ try_restore_indent(doc, view);
// if leaving append mode, move cursor back by 1
if doc.restore_cursor {
@@ -2637,7 +2657,7 @@ fn normal_mode(cx: &mut Context) {
}
}
-fn try_restore_indent(doc: &mut Document, view_id: ViewId) {
+fn try_restore_indent(doc: &mut Document, view: &mut View) {
use helix_core::chars::char_is_whitespace;
use helix_core::Operation;
@@ -2656,18 +2676,18 @@ fn try_restore_indent(doc: &mut Document, view_id: ViewId) {
let doc_changes = doc.changes().changes();
let text = doc.text().slice(..);
- let range = doc.selection(view_id).primary();
+ let range = doc.selection(view.id).primary();
let pos = range.cursor(text);
let line_end_pos = line_end_char_index(&text, range.cursor_line(text));
if inserted_a_new_blank_line(doc_changes, pos, line_end_pos) {
// Removes tailing whitespaces.
let transaction =
- Transaction::change_by_selection(doc.text(), doc.selection(view_id), |range| {
+ Transaction::change_by_selection(doc.text(), doc.selection(view.id), |range| {
let line_start_pos = text.line_to_char(range.cursor_line(text));
(line_start_pos, pos, None)
});
- doc.apply(&transaction, view_id);
+ apply_transaction(&transaction, doc, view);
}
}
@@ -2865,7 +2885,7 @@ pub mod insert {
/// Exclude the cursor in range.
fn exclude_cursor(text: RopeSlice, range: Range, cursor: Range) -> Range {
- if range.to() == cursor.to() {
+ if range.to() == cursor.to() && text.len_chars() != cursor.to() {
Range::new(
range.from(),
graphemes::prev_grapheme_boundary(text, cursor.to()),
@@ -2981,7 +3001,7 @@ pub mod insert {
let (view, doc) = current!(cx.editor);
if let Some(t) = transaction {
- doc.apply(&t, view.id);
+ apply_transaction(&t, doc, view);
}
// TODO: need a post insert hook too for certain triggers (autocomplete, signature help, etc)
@@ -3003,7 +3023,7 @@ pub mod insert {
&doc.selection(view.id).clone().cursors(doc.text().slice(..)),
indent,
);
- doc.apply(&transaction, view.id);
+ apply_transaction(&transaction, doc, view);
}
pub fn insert_newline(cx: &mut Context) {
@@ -3090,7 +3110,7 @@ pub mod insert {
transaction = transaction.with_selection(Selection::new(ranges, selection.primary_index()));
let (view, doc) = current!(cx.editor);
- doc.apply(&transaction, view.id);
+ apply_transaction(&transaction, doc, view);
}
pub fn delete_char_backward(cx: &mut Context) {
@@ -3184,7 +3204,7 @@ pub mod insert {
}
});
let (view, doc) = current!(cx.editor);
- doc.apply(&transaction, view.id);
+ apply_transaction(&transaction, doc, view);
lsp::signature_help_impl(cx, SignatureHelpInvoked::Automatic);
}
@@ -3202,7 +3222,7 @@ pub mod insert {
None,
)
});
- doc.apply(&transaction, view.id);
+ apply_transaction(&transaction, doc, view);
lsp::signature_help_impl(cx, SignatureHelpInvoked::Automatic);
}
@@ -3243,7 +3263,7 @@ fn undo(cx: &mut Context) {
let count = cx.count();
let (view, doc) = current!(cx.editor);
for _ in 0..count {
- if !doc.undo(view.id) {
+ if !doc.undo(view) {
cx.editor.set_status("Already at oldest change");
break;
}
@@ -3254,7 +3274,7 @@ fn redo(cx: &mut Context) {
let count = cx.count();
let (view, doc) = current!(cx.editor);
for _ in 0..count {
- if !doc.redo(view.id) {
+ if !doc.redo(view) {
cx.editor.set_status("Already at newest change");
break;
}
@@ -3266,7 +3286,7 @@ fn earlier(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
for _ in 0..count {
// rather than doing in batch we do this so get error halfway
- if !doc.earlier(view.id, UndoKind::Steps(1)) {
+ if !doc.earlier(view, UndoKind::Steps(1)) {
cx.editor.set_status("Already at oldest change");
break;
}
@@ -3278,7 +3298,7 @@ fn later(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
for _ in 0..count {
// rather than doing in batch we do this so get error halfway
- if !doc.later(view.id, UndoKind::Steps(1)) {
+ if !doc.later(view, UndoKind::Steps(1)) {
cx.editor.set_status("Already at newest change");
break;
}
@@ -3330,9 +3350,15 @@ fn yank_joined_to_clipboard_impl(
.map(Cow::into_owned)
.collect();
+ let clipboard_text = match clipboard_type {
+ ClipboardType::Clipboard => "system clipboard",
+ ClipboardType::Selection => "primary clipboard",
+ };
+
let msg = format!(
- "joined and yanked {} selection(s) to system clipboard",
+ "joined and yanked {} selection(s) to {}",
values.len(),
+ clipboard_text,
);
let joined = values.join(separator);
@@ -3361,6 +3387,11 @@ fn yank_main_selection_to_clipboard_impl(
let (view, doc) = current!(editor);
let text = doc.text().slice(..);
+ let message_text = match clipboard_type {
+ ClipboardType::Clipboard => "yanked main selection to system clipboard",
+ ClipboardType::Selection => "yanked main selection to primary clipboard",
+ };
+
let value = doc.selection(view.id).primary().fragment(text);
if let Err(e) = editor
@@ -3370,7 +3401,7 @@ fn yank_main_selection_to_clipboard_impl(
bail!("Couldn't set system clipboard content: {}", e);
}
- editor.set_status("yanked main selection to system clipboard");
+ editor.set_status(message_text);
Ok(())
}
@@ -3396,7 +3427,7 @@ enum Paste {
Cursor,
}
-fn paste_impl(values: &[String], doc: &mut Document, view: &View, action: Paste, count: usize) {
+fn paste_impl(values: &[String], doc: &mut Document, view: &mut View, action: Paste, count: usize) {
let repeat = std::iter::repeat(
values
.last()
@@ -3439,7 +3470,7 @@ fn paste_impl(values: &[String], doc: &mut Document, view: &View, action: Paste,
};
(pos, pos, values.next())
});
- doc.apply(&transaction, view.id);
+ apply_transaction(&transaction, doc, view);
}
pub(crate) fn paste_bracketed_value(cx: &mut Context, contents: String) {
@@ -3531,7 +3562,7 @@ fn replace_with_yanked(cx: &mut Context) {
}
});
- doc.apply(&transaction, view.id);
+ apply_transaction(&transaction, doc, view);
}
}
}
@@ -3554,7 +3585,7 @@ fn replace_selections_with_clipboard_impl(
)
});
- doc.apply(&transaction, view.id);
+ apply_transaction(&transaction, doc, view);
doc.append_changes_to_history(view.id);
Ok(())
}
@@ -3624,7 +3655,7 @@ fn indent(cx: &mut Context) {
Some((pos, pos, Some(indent.clone())))
}),
);
- doc.apply(&transaction, view.id);
+ apply_transaction(&transaction, doc, view);
}
fn unindent(cx: &mut Context) {
@@ -3663,7 +3694,7 @@ fn unindent(cx: &mut Context) {
let transaction = Transaction::change(doc.text(), changes.into_iter());
- doc.apply(&transaction, view.id);
+ apply_transaction(&transaction, doc, view);
}
fn format_selections(cx: &mut Context) {
@@ -3710,11 +3741,11 @@ fn format_selections(cx: &mut Context) {
// language_server.offset_encoding(),
// );
- // doc.apply(&transaction, view.id);
+ // apply_transaction(&transaction, doc, view);
}
}
-fn join_selections(cx: &mut Context) {
+fn join_selections_inner(cx: &mut Context, select_space: bool) {
use movement::skip_while;
let (view, doc) = current!(cx.editor);
let text = doc.text();
@@ -3749,11 +3780,23 @@ fn join_selections(cx: &mut Context) {
// TODO: joining multiple empty lines should be replaced by a single space.
// need to merge change ranges that touch
- let transaction = Transaction::change(doc.text(), changes.into_iter());
- // TODO: select inserted spaces
- // .with_selection(selection);
+ // select inserted spaces
+ let transaction = if select_space {
+ let ranges: SmallVec<_> = changes
+ .iter()
+ .scan(0, |offset, change| {
+ let range = Range::point(change.0 - *offset);
+ *offset += change.1 - change.0 - 1; // -1 because cursor is 0-sized
+ Some(range)
+ })
+ .collect();
+ let selection = Selection::new(ranges, 0);
+ Transaction::change(doc.text(), changes.into_iter()).with_selection(selection)
+ } else {
+ Transaction::change(doc.text(), changes.into_iter())
+ };
- doc.apply(&transaction, view.id);
+ apply_transaction(&transaction, doc, view);
}
fn keep_or_remove_selections_impl(cx: &mut Context, remove: bool) {
@@ -3764,7 +3807,8 @@ fn keep_or_remove_selections_impl(cx: &mut Context, remove: bool) {
if remove { "remove:" } else { "keep:" }.into(),
Some(reg),
ui::completers::none,
- move |view, doc, regex, event| {
+ move |editor, regex, event| {
+ let (view, doc) = current!(editor);
if !matches!(event, PromptEvent::Update | PromptEvent::Validate) {
return;
}
@@ -3779,6 +3823,14 @@ fn keep_or_remove_selections_impl(cx: &mut Context, remove: bool) {
)
}
+fn join_selections(cx: &mut Context) {
+ join_selections_inner(cx, false)
+}
+
+fn join_selections_space(cx: &mut Context) {
+ join_selections_inner(cx, true)
+}
+
fn keep_selections(cx: &mut Context) {
keep_or_remove_selections_impl(cx, false)
}
@@ -3897,7 +3949,7 @@ fn toggle_comments(cx: &mut Context) {
.map(|tc| tc.as_ref());
let transaction = comment::toggle_line_comments(doc.text(), doc.selection(view.id), token);
- doc.apply(&transaction, view.id);
+ apply_transaction(&transaction, doc, view);
exit_select_mode(cx);
}
@@ -3953,7 +4005,7 @@ fn rotate_selection_contents(cx: &mut Context, direction: Direction) {
.map(|(range, fragment)| (range.from(), range.to(), Some(fragment))),
);
- doc.apply(&transaction, view.id);
+ apply_transaction(&transaction, doc, view);
}
fn rotate_selection_contents_forward(cx: &mut Context) {
@@ -4268,7 +4320,7 @@ fn goto_ts_object_impl(cx: &mut Context, object: &'static str, direction: Direct
let root = syntax.tree().root_node();
let selection = doc.selection(view.id).clone().transform(|range| {
- movement::goto_treesitter_object(
+ let new_range = movement::goto_treesitter_object(
text,
range,
object,
@@ -4276,7 +4328,19 @@ fn goto_ts_object_impl(cx: &mut Context, object: &'static str, direction: Direct
root,
lang_config,
count,
- )
+ );
+
+ if editor.mode == Mode::Select {
+ let head = if new_range.head < range.anchor {
+ new_range.anchor
+ } else {
+ new_range.head
+ };
+
+ Range::new(range.anchor, head)
+ } else {
+ new_range.with_direction(direction)
+ }
});
doc.set_selection(view.id, selection);
@@ -4341,7 +4405,6 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) {
cx.on_next_key(move |cx, event| {
cx.editor.autoinfo = None;
- cx.editor.pseudo_pending = None;
if let Some(ch) = event.char() {
let textobject = move |editor: &mut Editor| {
let (view, doc) = current!(editor);
@@ -4390,33 +4453,25 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) {
}
});
- if let Some((title, abbrev)) = match objtype {
- textobject::TextObject::Inside => Some(("Match inside", "mi")),
- textobject::TextObject::Around => Some(("Match around", "ma")),
+ let title = match objtype {
+ textobject::TextObject::Inside => "Match inside",
+ textobject::TextObject::Around => "Match around",
_ => return,
- } {
- let help_text = [
- ("w", "Word"),
- ("W", "WORD"),
- ("p", "Paragraph"),
- ("c", "Class (tree-sitter)"),
- ("f", "Function (tree-sitter)"),
- ("a", "Argument/parameter (tree-sitter)"),
- ("o", "Comment (tree-sitter)"),
- ("t", "Test (tree-sitter)"),
- ("m", "Closest surrounding pair to cursor"),
- (" ", "... or any character acting as a pair"),
- ];
-
- cx.editor.autoinfo = Some(Info::new(
- title,
- help_text
- .into_iter()
- .map(|(col1, col2)| (col1.to_string(), col2.to_string()))
- .collect(),
- ));
- cx.editor.pseudo_pending = Some(abbrev.to_string());
};
+ let help_text = [
+ ("w", "Word"),
+ ("W", "WORD"),
+ ("p", "Paragraph"),
+ ("c", "Class (tree-sitter)"),
+ ("f", "Function (tree-sitter)"),
+ ("a", "Argument/parameter (tree-sitter)"),
+ ("o", "Comment (tree-sitter)"),
+ ("t", "Test (tree-sitter)"),
+ ("m", "Closest surrounding pair to cursor"),
+ (" ", "... or any character acting as a pair"),
+ ];
+
+ cx.editor.autoinfo = Some(Info::new(title, &help_text));
}
fn surround_add(cx: &mut Context) {
@@ -4440,7 +4495,7 @@ fn surround_add(cx: &mut Context) {
}
let transaction = Transaction::change(doc.text(), changes.into_iter());
- doc.apply(&transaction, view.id);
+ apply_transaction(&transaction, doc, view);
})
}
@@ -4479,7 +4534,7 @@ fn surround_replace(cx: &mut Context) {
(pos, pos + 1, Some(t))
}),
);
- doc.apply(&transaction, view.id);
+ apply_transaction(&transaction, doc, view);
});
})
}
@@ -4506,7 +4561,7 @@ fn surround_delete(cx: &mut Context) {
let transaction =
Transaction::change(doc.text(), change_pos.into_iter().map(|p| (p, p + 1, None)));
- doc.apply(&transaction, view.id);
+ apply_transaction(&transaction, doc, view);
})
}
@@ -4681,7 +4736,7 @@ fn shell(cx: &mut compositor::Context, cmd: &str, behavior: &ShellBehavior) {
if behavior != &ShellBehavior::Ignore {
let transaction = Transaction::change(doc.text(), changes.into_iter());
- doc.apply(&transaction, view.id);
+ apply_transaction(&transaction, doc, view);
doc.append_changes_to_history(view.id);
}
@@ -4744,7 +4799,7 @@ fn add_newline_impl(cx: &mut Context, open: Open) {
});
let transaction = Transaction::change(text, changes);
- doc.apply(&transaction, view.id);
+ apply_transaction(&transaction, doc, view);
}
/// Increment object under cursor by count.
@@ -4837,7 +4892,7 @@ fn increment_impl(cx: &mut Context, amount: i64) {
let transaction = Transaction::change(doc.text(), changes);
let transaction = transaction.with_selection(selection.clone());
- doc.apply(&transaction, view.id);
+ apply_transaction(&transaction, doc, view);
}
}
diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs
index 1113b44e..3fa5c96f 100644
--- a/helix-term/src/commands/lsp.rs
+++ b/helix-term/src/commands/lsp.rs
@@ -9,7 +9,7 @@ use tui::text::{Span, Spans};
use super::{align_view, push_jump, Align, Context, Editor, Open};
use helix_core::{path, Selection};
-use helix_view::{editor::Action, theme::Style};
+use helix_view::{apply_transaction, editor::Action, theme::Style};
use crate::{
compositor::{self, Compositor},
@@ -596,9 +596,7 @@ pub fn apply_workspace_edit(
}
};
- let doc = editor
- .document_mut(doc_id)
- .expect("Document for document_changes not found");
+ let doc = doc_mut!(editor, &doc_id);
// Need to determine a view for apply/append_changes_to_history
let selections = doc.selections();
@@ -619,7 +617,7 @@ pub fn apply_workspace_edit(
text_edits,
offset_encoding,
);
- doc.apply(&transaction, view_id);
+ apply_transaction(&transaction, doc, view_mut!(editor, view_id));
doc.append_changes_to_history(view_id);
};
diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs
index 6d0ced65..1bfc8153 100644
--- a/helix-term/src/commands/typed.rs
+++ b/helix-term/src/commands/typed.rs
@@ -2,7 +2,10 @@ use std::ops::Deref;
use super::*;
-use helix_view::editor::{Action, ConfigEvent};
+use helix_view::{
+ apply_transaction,
+ editor::{Action, CloseError, ConfigEvent},
+};
use ui::completers::{self, Completer};
#[derive(Clone)]
@@ -71,8 +74,29 @@ fn buffer_close_by_ids_impl(
doc_ids: &[DocumentId],
force: bool,
) -> anyhow::Result<()> {
- for &doc_id in doc_ids {
- editor.close_document(doc_id, force)?;
+ let (modified_ids, modified_names): (Vec<_>, Vec<_>) = doc_ids
+ .iter()
+ .filter_map(|&doc_id| {
+ if let Err(CloseError::BufferModified(name)) = editor.close_document(doc_id, force) {
+ Some((doc_id, name))
+ } else {
+ None
+ }
+ })
+ .unzip();
+
+ if let Some(first) = modified_ids.first() {
+ let current = doc!(editor);
+ // If the current document is unmodified, and there are modified
+ // documents, switch focus to the first modified doc.
+ if !modified_ids.contains(&current.id()) {
+ editor.switch(*first, Action::Replace);
+ }
+ bail!(
+ "{} unsaved buffer(s) remaining: {:?}",
+ modified_names.len(),
+ modified_names
+ );
}
Ok(())
@@ -441,7 +465,7 @@ fn set_line_ending(
}
}),
);
- doc.apply(&transaction, view.id);
+ apply_transaction(&transaction, doc, view);
doc.append_changes_to_history(view.id);
Ok(())
@@ -459,7 +483,7 @@ fn earlier(
let uk = args.join(" ").parse::<UndoKind>().map_err(|s| anyhow!(s))?;
let (view, doc) = current!(cx.editor);
- let success = doc.earlier(view.id, uk);
+ let success = doc.earlier(view, uk);
if !success {
cx.editor.set_status("Already at oldest change");
}
@@ -478,7 +502,7 @@ fn later(
let uk = args.join(" ").parse::<UndoKind>().map_err(|s| anyhow!(s))?;
let (view, doc) = current!(cx.editor);
- let success = doc.later(view.id, uk);
+ let success = doc.later(view, uk);
if !success {
cx.editor.set_status("Already at newest change");
}
@@ -513,23 +537,26 @@ fn force_write_quit(
force_quit(cx, &[], event)
}
-/// Results an error if there are modified buffers remaining and sets editor error,
-/// otherwise returns `Ok(())`
+/// Results in an error if there are modified buffers remaining and sets editor
+/// error, otherwise returns `Ok(())`. If the current document is unmodified,
+/// and there are modified documents, switches focus to one of them.
pub(super) fn buffers_remaining_impl(editor: &mut Editor) -> anyhow::Result<()> {
- let modified: Vec<_> = editor
+ let (modified_ids, modified_names): (Vec<_>, Vec<_>) = editor
.documents()
.filter(|doc| doc.is_modified())
- .map(|doc| {
- doc.relative_path()
- .map(|path| path.to_string_lossy().to_string())
- .unwrap_or_else(|| SCRATCH_BUFFER_NAME.into())
- })
- .collect();
- if !modified.is_empty() {
+ .map(|doc| (doc.id(), doc.display_name()))
+ .unzip();
+ if let Some(first) = modified_ids.first() {
+ let current = doc!(editor);
+ // If the current document is unmodified, and there are modified
+ // documents, switch focus to the first modified doc.
+ if !modified_ids.contains(&current.id()) {
+ editor.switch(*first, Action::Replace);
+ }
bail!(
"{} unsaved buffer(s) remaining: {:?}",
- modified.len(),
- modified
+ modified_names.len(),
+ modified_names
);
}
Ok(())
@@ -859,7 +886,7 @@ fn replace_selections_with_clipboard_impl(
(range.from(), range.to(), Some(contents.as_str().into()))
});
- doc.apply(&transaction, view.id);
+ apply_transaction(&transaction, doc, view);
doc.append_changes_to_history(view.id);
Ok(())
}
@@ -980,7 +1007,7 @@ fn reload(
let scrolloff = cx.editor.config().scrolloff;
let (view, doc) = current!(cx.editor);
- doc.reload(view.id).map(|_| {
+ doc.reload(view).map(|_| {
view.ensure_cursor_in_view(doc, scrolloff);
})
}
@@ -1000,7 +1027,7 @@ fn lsp_restart(
.context("LSP not defined for the current document")?;
let scope = config.scope.clone();
- cx.editor.language_servers.restart(config)?;
+ cx.editor.language_servers.restart(config, doc.path())?;
// This collect is needed because refresh_language_server would need to re-borrow editor.
let document_ids_to_refresh: Vec<DocumentId> = cx
@@ -1033,7 +1060,21 @@ fn tree_sitter_scopes(
let pos = doc.selection(view.id).primary().cursor(text);
let scopes = indent::get_scopes(doc.syntax(), text, pos);
- cx.editor.set_status(format!("scopes: {:?}", &scopes));
+
+ let contents = format!("```json\n{:?}\n````", scopes);
+
+ let callback = async move {
+ let call: job::Callback =
+ Box::new(move |editor: &mut Editor, compositor: &mut Compositor| {
+ let contents = ui::Markdown::new(contents, editor.syn_loader.clone());
+ let popup = Popup::new("hover", contents).auto_close(true);
+ compositor.replace_or_push("hover", popup);
+ });
+ Ok(call)
+ };
+
+ cx.jobs.callback(callback);
+
Ok(())
}
@@ -1196,18 +1237,41 @@ pub(super) fn goto_line_number(
args: &[Cow<str>],
event: PromptEvent,
) -> anyhow::Result<()> {
- if event != PromptEvent::Validate {
- return Ok(());
+ match event {
+ PromptEvent::Abort => {
+ if let Some(line_number) = cx.editor.last_line_number {
+ goto_line_impl(cx.editor, NonZeroUsize::new(line_number));
+ let (view, doc) = current!(cx.editor);
+ view.ensure_cursor_in_view(doc, line_number);
+ cx.editor.last_line_number = None;
+ }
+ return Ok(());
+ }
+ PromptEvent::Validate => {
+ ensure!(!args.is_empty(), "Line number required");
+ cx.editor.last_line_number = None;
+ }
+ PromptEvent::Update => {
+ if args.is_empty() {
+ if let Some(line_number) = cx.editor.last_line_number {
+ // When a user hits backspace and there are no numbers left,
+ // we can bring them back to their original line
+ goto_line_impl(cx.editor, NonZeroUsize::new(line_number));
+ let (view, doc) = current!(cx.editor);
+ view.ensure_cursor_in_view(doc, line_number);
+ cx.editor.last_line_number = None;
+ }
+ return Ok(());
+ }
+ let (view, doc) = current!(cx.editor);
+ let text = doc.text().slice(..);
+ let line = doc.selection(view.id).primary().cursor_line(text);
+ cx.editor.last_line_number.get_or_insert(line + 1);
+ }
}
-
- ensure!(!args.is_empty(), "Line number required");
-
let line = args[0].parse::<usize>()?;
-
goto_line_impl(cx.editor, NonZeroUsize::new(line));
-
let (view, doc) = current!(cx.editor);
-
view.ensure_cursor_in_view(doc, line);
Ok(())
}
@@ -1351,7 +1415,7 @@ fn sort_impl(
.map(|(s, fragment)| (s.from(), s.to(), Some(fragment))),
);
- doc.apply(&transaction, view.id);
+ apply_transaction(&transaction, doc, view);
doc.append_changes_to_history(view.id);
Ok(())
@@ -1395,7 +1459,7 @@ fn reflow(
(range.from(), range.to(), Some(reflowed_text))
});
- doc.apply(&transaction, view.id);
+ apply_transaction(&transaction, doc, view);
doc.append_changes_to_history(view.id);
view.ensure_cursor_in_view(doc, scrolloff);
@@ -2021,7 +2085,7 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
TypableCommand {
name: "insert-output",
aliases: &[],
- doc: "Run shell command, inserting output after each selection.",
+ doc: "Run shell command, inserting output before each selection.",
fun: insert_output,
completer: None,
},
diff --git a/helix-term/src/keymap/default.rs b/helix-term/src/keymap/default.rs
index f07d4028..118764d9 100644
--- a/helix-term/src/keymap/default.rs
+++ b/helix-term/src/keymap/default.rs
@@ -59,9 +59,9 @@ pub fn default() -> HashMap<Mode, Keymap> {
":" => command_mode,
"i" => insert_mode,
- "I" => prepend_to_line,
+ "I" => insert_at_line_start,
"a" => append_mode,
- "A" => append_to_line,
+ "A" => insert_at_line_end,
"o" => open_below,
"O" => open_above,
@@ -144,6 +144,7 @@ pub fn default() -> HashMap<Mode, Keymap> {
"<" => unindent,
"=" => format_selections,
"J" => join_selections,
+ "A-J" => join_selections_space,
"K" => keep_selections,
"A-K" => remove_selections,
@@ -208,11 +209,11 @@ pub fn default() -> HashMap<Mode, Keymap> {
"j" => jumplist_picker,
"s" => symbol_picker,
"S" => workspace_symbol_picker,
- "g" => diagnostics_picker,
- "G" => workspace_diagnostics_picker,
+ "d" => diagnostics_picker,
+ "D" => workspace_diagnostics_picker,
"a" => code_action,
"'" => last_picker,
- "d" => { "Debug (experimental)" sticky=true
+ "g" => { "Debug (experimental)" sticky=true
"l" => dap_launch,
"b" => dap_toggle_breakpoint,
"c" => dap_continue,
@@ -342,24 +343,27 @@ pub fn default() -> HashMap<Mode, Keymap> {
let insert = keymap!({ "Insert mode"
"esc" => normal_mode,
- "backspace" => delete_char_backward,
- "C-h" => delete_char_backward,
- "del" => delete_char_forward,
- "C-d" => delete_char_forward,
- "ret" => insert_newline,
- "C-j" => insert_newline,
- "tab" => insert_tab,
- "C-w" => delete_word_backward,
- "A-backspace" => delete_word_backward,
- "A-d" => delete_word_forward,
- "A-del" => delete_word_forward,
"C-s" => commit_undo_checkpoint,
+ "C-x" => completion,
+ "C-r" => insert_register,
- "C-k" => kill_to_line_end,
+ "C-w" | "A-backspace" => delete_word_backward,
+ "A-d" | "A-del" => delete_word_forward,
"C-u" => kill_to_line_start,
+ "C-k" => kill_to_line_end,
+ "C-h" | "backspace" => delete_char_backward,
+ "C-d" | "del" => delete_char_forward,
+ "C-j" | "ret" => insert_newline,
+ "tab" => insert_tab,
- "C-x" => completion,
- "C-r" => insert_register,
+ "up" => move_line_up,
+ "down" => move_line_down,
+ "left" => move_char_left,
+ "right" => move_char_right,
+ "pageup" => page_up,
+ "pagedown" => page_down,
+ "home" => goto_line_start,
+ "end" => goto_line_end_newline,
});
hashmap!(
Mode::Normal => Keymap::new(normal),
diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs
index 2d7d4f92..7348dcf4 100644
--- a/helix-term/src/ui/completion.rs
+++ b/helix-term/src/ui/completion.rs
@@ -1,5 +1,5 @@
use crate::compositor::{Component, Context, Event, EventResult};
-use helix_view::editor::CompleteAction;
+use helix_view::{apply_transaction, editor::CompleteAction};
use tui::buffer::Buffer as Surface;
use tui::text::Spans;
@@ -143,11 +143,11 @@ impl Completion {
let (view, doc) = current!(editor);
// if more text was entered, remove it
- doc.restore(view.id);
+ doc.restore(view);
match event {
PromptEvent::Abort => {
- doc.restore(view.id);
+ doc.restore(view);
editor.last_completion = None;
}
PromptEvent::Update => {
@@ -164,7 +164,7 @@ impl Completion {
// initialize a savepoint
doc.savepoint();
- doc.apply(&transaction, view.id);
+ apply_transaction(&transaction, doc, view);
editor.last_completion = Some(CompleteAction {
trigger_offset,
@@ -183,7 +183,7 @@ impl Completion {
trigger_offset,
);
- doc.apply(&transaction, view.id);
+ apply_transaction(&transaction, doc, view);
editor.last_completion = Some(CompleteAction {
trigger_offset,
@@ -213,7 +213,7 @@ impl Completion {
additional_edits.clone(),
offset_encoding, // TODO: should probably transcode in Client
);
- doc.apply(&transaction, view.id);
+ apply_transaction(&transaction, doc, view);
}
}
}
diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs
index 7cb29c3b..3cd2130a 100644
--- a/helix-term/src/ui/editor.rs
+++ b/helix-term/src/ui/editor.rs
@@ -13,9 +13,10 @@ use helix_core::{
movement::Direction,
syntax::{self, HighlightEvent},
unicode::width::UnicodeWidthStr,
- LineEnding, Position, Range, Selection, Transaction,
+ visual_coords_at_pos, LineEnding, Position, Range, Selection, Transaction,
};
use helix_view::{
+ apply_transaction,
document::{Mode, SCRATCH_BUFFER_NAME},
editor::{CompleteAction, CursorShapeConfig},
graphics::{Color, CursorKind, Modifier, Rect, Style},
@@ -23,7 +24,7 @@ use helix_view::{
keyboard::{KeyCode, KeyModifiers},
Document, Editor, Theme, View,
};
-use std::{borrow::Cow, path::PathBuf};
+use std::{borrow::Cow, cmp::min, path::PathBuf};
use tui::buffer::Buffer as Surface;
@@ -33,6 +34,7 @@ use super::statusline;
pub struct EditorView {
pub keymaps: Keymaps,
on_next_key: Option<Box<dyn FnOnce(&mut commands::Context, KeyEvent)>>,
+ pseudo_pending: Vec<KeyEvent>,
last_insert: (commands::MappableCommand, Vec<InsertEvent>),
pub(crate) completion: Option<Completion>,
spinners: ProgressSpinners,
@@ -56,6 +58,7 @@ impl EditorView {
Self {
keymaps,
on_next_key: None,
+ pseudo_pending: Vec::new(),
last_insert: (commands::MappableCommand::normal_mode, Vec::new()),
completion: None,
spinners: ProgressSpinners::default(),
@@ -116,9 +119,19 @@ impl EditorView {
if is_focused && editor.config().cursorline {
Self::highlight_cursorline(doc, view, surface, theme);
}
+ if is_focused && editor.config().cursorcolumn {
+ Self::highlight_cursorcolumn(doc, view, surface, theme);
+ }
- let highlights = Self::doc_syntax_highlights(doc, view.offset, inner.height, theme);
- let highlights = syntax::merge(highlights, Self::doc_diagnostics_highlights(doc, theme));
+ let mut highlights = Self::doc_syntax_highlights(doc, view.offset, inner.height, theme);
+ for diagnostic in Self::doc_diagnostics_highlights(doc, theme) {
+ // Most of the `diagnostic` Vecs are empty most of the time. Skipping
+ // a merge for any empty Vec saves a significant amount of work.
+ if diagnostic.is_empty() {
+ continue;
+ }
+ highlights = Box::new(syntax::merge(highlights, diagnostic));
+ }
let highlights: Box<dyn Iterator<Item = HighlightEvent>> = if is_focused {
Box::new(syntax::merge(
highlights,
@@ -262,7 +275,7 @@ impl EditorView {
pub fn doc_diagnostics_highlights(
doc: &Document,
theme: &Theme,
- ) -> Vec<(usize, std::ops::Range<usize>)> {
+ ) -> [Vec<(usize, std::ops::Range<usize>)>; 5] {
use helix_core::diagnostic::Severity;
let get_scope_of = |scope| {
theme
@@ -283,22 +296,38 @@ impl EditorView {
let error = get_scope_of("diagnostic.error");
let r#default = get_scope_of("diagnostic"); // this is a bit redundant but should be fine
- doc.diagnostics()
- .iter()
- .map(|diagnostic| {
- let diagnostic_scope = match diagnostic.severity {
- Some(Severity::Info) => info,
- Some(Severity::Hint) => hint,
- Some(Severity::Warning) => warning,
- Some(Severity::Error) => error,
- _ => r#default,
- };
- (
- diagnostic_scope,
- diagnostic.range.start..diagnostic.range.end,
- )
- })
- .collect()
+ let mut default_vec: Vec<(usize, std::ops::Range<usize>)> = Vec::new();
+ let mut info_vec = Vec::new();
+ let mut hint_vec = Vec::new();
+ let mut warning_vec = Vec::new();
+ let mut error_vec = Vec::new();
+
+ for diagnostic in doc.diagnostics() {
+ // Separate diagnostics into different Vecs by severity.
+ let (vec, scope) = match diagnostic.severity {
+ Some(Severity::Info) => (&mut info_vec, info),
+ Some(Severity::Hint) => (&mut hint_vec, hint),
+ Some(Severity::Warning) => (&mut warning_vec, warning),
+ Some(Severity::Error) => (&mut error_vec, error),
+ _ => (&mut default_vec, r#default),
+ };
+
+ // If any diagnostic overlaps ranges with the prior diagnostic,
+ // merge the two together. Otherwise push a new span.
+ match vec.last_mut() {
+ Some((_, range)) if diagnostic.range.start <= range.end => {
+ // This branch merges overlapping diagnostics, assuming that the current
+ // diagnostic starts on range.start or later. If this assertion fails,
+ // we will discard some part of `diagnostic`. This implies that
+ // `doc.diagnostics()` is not sorted by `diagnostic.range`.
+ debug_assert!(range.start <= diagnostic.range.start);
+ range.end = diagnostic.range.end.max(range.end)
+ }
+ _ => vec.push((scope, diagnostic.range.start..diagnostic.range.end)),
+ }
+ }
+
+ [default_vec, info_vec, hint_vec, warning_vec, error_vec]
}
/// Get highlight spans for selections in a document view.
@@ -399,7 +428,7 @@ impl EditorView {
let characters = &whitespace.characters;
let mut spans = Vec::new();
- let mut visual_x = 0u16;
+ let mut visual_x = 0usize;
let mut line = 0u16;
let tab_width = doc.tab_width();
let tab = if whitespace.render.tab() == WhitespaceRenderValue::All {
@@ -436,17 +465,22 @@ impl EditorView {
return;
}
- let starting_indent = (offset.col / tab_width) as u16;
- // TODO: limit to a max indent level too. It doesn't cause visual artifacts but it would avoid some
- // extra loops if the code is deeply nested.
-
- for i in starting_indent..(indent_level / tab_width as u16) {
- surface.set_string(
- viewport.x + (i * tab_width as u16) - offset.col as u16,
- viewport.y + line,
- &indent_guide_char,
- indent_guide_style,
- );
+ let starting_indent =
+ (offset.col / tab_width) + config.indent_guides.skip_levels as usize;
+
+ // Don't draw indent guides outside of view
+ let end_indent = min(
+ indent_level,
+ // Add tab_width - 1 to round up, since the first visible
+ // indent might be a bit after offset.col
+ offset.col + viewport.width as usize + (tab_width - 1),
+ ) / tab_width;
+
+ for i in starting_indent..end_indent {
+ let x = (viewport.x as usize + (i * tab_width) - offset.col) as u16;
+ let y = viewport.y + line;
+ debug_assert!(surface.in_bounds(x, y));
+ surface.set_string(x, y, &indent_guide_char, indent_guide_style);
}
};
@@ -488,14 +522,14 @@ impl EditorView {
use helix_core::graphemes::{grapheme_width, RopeGraphemes};
for grapheme in RopeGraphemes::new(text) {
- let out_of_bounds = visual_x < offset.col as u16
- || visual_x >= viewport.width + offset.col as u16;
+ let out_of_bounds = offset.col > (visual_x as usize)
+ || (visual_x as usize) >= viewport.width as usize + offset.col;
if LineEnding::from_rope_slice(&grapheme).is_some() {
if !out_of_bounds {
// we still want to render an empty cell with the style
surface.set_string(
- viewport.x + visual_x - offset.col as u16,
+ (viewport.x as usize + visual_x - offset.col) as u16,
viewport.y + line,
&newline,
style.patch(whitespace_style),
@@ -543,7 +577,7 @@ impl EditorView {
if !out_of_bounds {
// if we're offscreen just keep going until we hit a new line
surface.set_string(
- viewport.x + visual_x - offset.col as u16,
+ (viewport.x as usize + visual_x - offset.col) as u16,
viewport.y + line,
display_grapheme,
if is_whitespace {
@@ -576,7 +610,7 @@ impl EditorView {
last_line_indent_level = visual_x;
}
- visual_x = visual_x.saturating_add(width as u16);
+ visual_x = visual_x.saturating_add(width);
}
}
}
@@ -696,6 +730,7 @@ impl EditorView {
let mut offset = 0;
let gutter_style = theme.get("ui.gutter");
+ let gutter_selected_style = theme.get("ui.gutter.selected");
// avoid lots of small allocations by reusing a text buffer for each line
let mut text = String::with_capacity(8);
@@ -708,6 +743,12 @@ impl EditorView {
let x = viewport.x + offset;
let y = viewport.y + i as u16;
+ let gutter_style = if selected {
+ gutter_selected_style
+ } else {
+ gutter_style
+ };
+
if let Some(style) = gutter(line, selected, &mut text) {
surface.set_stringn(x, y, &text, *width, gutter_style.patch(style));
} else {
@@ -820,6 +861,53 @@ impl EditorView {
}
}
+ /// Apply the highlighting on the columns where a cursor is active
+ pub fn highlight_cursorcolumn(
+ doc: &Document,
+ view: &View,
+ surface: &mut Surface,
+ theme: &Theme,
+ ) {
+ let text = doc.text().slice(..);
+
+ // Manual fallback behaviour:
+ // ui.cursorcolumn.{p/s} -> ui.cursorcolumn -> ui.cursorline.{p/s}
+ let primary_style = theme
+ .try_get_exact("ui.cursorcolumn.primary")
+ .or_else(|| theme.try_get_exact("ui.cursorcolumn"))
+ .unwrap_or_else(|| theme.get("ui.cursorline.primary"));
+ let secondary_style = theme
+ .try_get_exact("ui.cursorcolumn.secondary")
+ .or_else(|| theme.try_get_exact("ui.cursorcolumn"))
+ .unwrap_or_else(|| theme.get("ui.cursorline.secondary"));
+
+ let inner_area = view.inner_area();
+ let offset = view.offset.col;
+
+ let selection = doc.selection(view.id);
+ let primary = selection.primary();
+ for range in selection.iter() {
+ let is_primary = primary == *range;
+
+ let Position { row: _, col } =
+ visual_coords_at_pos(text, range.cursor(text), doc.tab_width());
+ // if the cursor is horizontally in the view
+ if col >= offset && inner_area.width > (col - offset) as u16 {
+ let area = Rect::new(
+ inner_area.x + (col - offset) as u16,
+ view.area.y,
+ 1,
+ view.area.height,
+ );
+ if is_primary {
+ surface.set_style(area, primary_style)
+ } else {
+ surface.set_style(area, secondary_style)
+ }
+ }
+ }
+ }
+
/// Handle events by looking them up in `self.keymaps`. Returns None
/// if event was handled (a command was executed or a subkeymap was
/// activated). Only KeymapResult::{NotFound, Cancelled} is returned
@@ -831,6 +919,7 @@ impl EditorView {
event: KeyEvent,
) -> Option<KeymapResult> {
let mut last_mode = mode;
+ self.pseudo_pending.extend(self.keymaps.pending());
let key_result = self.keymaps.get(mode, event);
cxt.editor.autoinfo = self.keymaps.sticky().map(|node| node.infobox());
@@ -927,7 +1016,7 @@ impl EditorView {
InsertEvent::CompletionApply(compl) => {
let (view, doc) = current!(cxt.editor);
- doc.restore(view.id);
+ doc.restore(view);
let text = doc.text().slice(..);
let cursor = doc.selection(view.id).primary().cursor(text);
@@ -941,7 +1030,7 @@ impl EditorView {
(shift_position(start), shift_position(end), t)
}),
);
- doc.apply(&tx, view.id);
+ apply_transaction(&tx, doc, view);
}
InsertEvent::TriggerCompletion => {
let (_, doc) = current!(cxt.editor);
@@ -1005,7 +1094,7 @@ impl EditorView {
editor.clear_idle_timer(); // don't retrigger
}
- pub fn handle_idle_timeout(&mut self, cx: &mut crate::compositor::Context) -> EventResult {
+ pub fn handle_idle_timeout(&mut self, cx: &mut commands::Context) -> EventResult {
if self.completion.is_some()
|| cx.editor.mode != Mode::Insert
|| !cx.editor.config().auto_completion
@@ -1013,15 +1102,7 @@ impl EditorView {
return EventResult::Ignored(None);
}
- let mut cx = commands::Context {
- register: None,
- editor: cx.editor,
- jobs: cx.jobs,
- count: None,
- callback: None,
- on_next_key_callback: None,
- };
- crate::commands::insert::idle_completion(&mut cx);
+ crate::commands::insert::idle_completion(cx);
EventResult::Consumed(None)
}
@@ -1308,6 +1389,11 @@ impl Component for EditorView {
}
self.on_next_key = cx.on_next_key_callback.take();
+ match self.on_next_key {
+ Some(_) => self.pseudo_pending.push(key),
+ None => self.pseudo_pending.clear(),
+ }
+
// appease borrowck
let callback = cx.callback.take();
@@ -1337,6 +1423,7 @@ impl Component for EditorView {
}
Event::Mouse(event) => self.handle_mouse_event(event, &mut cx),
+ Event::IdleTimeout => self.handle_idle_timeout(&mut cx),
Event::FocusGained | Event::FocusLost => EventResult::Ignored(None),
}
}
@@ -1408,8 +1495,8 @@ impl Component for EditorView {
for key in self.keymaps.pending() {
disp.push_str(&key.key_sequence_format());
}
- if let Some(pseudo_pending) = &cx.editor.pseudo_pending {
- disp.push_str(pseudo_pending.as_str())
+ for key in &self.pseudo_pending {
+ disp.push_str(&key.key_sequence_format());
}
let style = cx.editor.theme.get("ui.text");
let macro_width = if cx.editor.macro_recording.is_some() {
diff --git a/helix-term/src/ui/fuzzy_match.rs b/helix-term/src/ui/fuzzy_match.rs
new file mode 100644
index 00000000..e25d7328
--- /dev/null
+++ b/helix-term/src/ui/fuzzy_match.rs
@@ -0,0 +1,74 @@
+use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
+use fuzzy_matcher::FuzzyMatcher;
+
+#[cfg(test)]
+mod test;
+
+pub struct FuzzyQuery {
+ queries: Vec<String>,
+}
+
+impl FuzzyQuery {
+ pub fn new(query: &str) -> FuzzyQuery {
+ let mut saw_backslash = false;
+ let queries = query
+ .split(|c| {
+ saw_backslash = match c {
+ ' ' if !saw_backslash => return true,
+ '\\' => true,
+ _ => false,
+ };
+ false
+ })
+ .filter_map(|query| {
+ if query.is_empty() {
+ None
+ } else {
+ Some(query.replace("\\ ", " "))
+ }
+ })
+ .collect();
+ FuzzyQuery { queries }
+ }
+
+ pub fn fuzzy_match(&self, item: &str, matcher: &Matcher) -> Option<i64> {
+ // use the rank of the first query for the rank, because merging ranks is not really possible
+ // this behaviour matches fzf and skim
+ let score = matcher.fuzzy_match(item, self.queries.get(0)?)?;
+ if self
+ .queries
+ .iter()
+ .any(|query| matcher.fuzzy_match(item, query).is_none())
+ {
+ return None;
+ }
+ Some(score)
+ }
+
+ pub fn fuzzy_indicies(&self, item: &str, matcher: &Matcher) -> Option<(i64, Vec<usize>)> {
+ if self.queries.len() == 1 {
+ return matcher.fuzzy_indices(item, &self.queries[0]);
+ }
+
+ // use the rank of the first query for the rank, because merging ranks is not really possible
+ // this behaviour matches fzf and skim
+ let (score, mut indicies) = matcher.fuzzy_indices(item, self.queries.get(0)?)?;
+
+ // fast path for the common case of not using a space
+ // during matching this branch should be free thanks to branch prediction
+ if self.queries.len() == 1 {
+ return Some((score, indicies));
+ }
+
+ for query in &self.queries[1..] {
+ let (_, matched_indicies) = matcher.fuzzy_indices(item, query)?;
+ indicies.extend_from_slice(&matched_indicies);
+ }
+
+ // deadup and remove duplicate matches
+ indicies.sort_unstable();
+ indicies.dedup();
+
+ Some((score, indicies))
+ }
+}
diff --git a/helix-term/src/ui/fuzzy_match/test.rs b/helix-term/src/ui/fuzzy_match/test.rs
new file mode 100644
index 00000000..3f90ef68
--- /dev/null
+++ b/helix-term/src/ui/fuzzy_match/test.rs
@@ -0,0 +1,47 @@
+use crate::ui::fuzzy_match::FuzzyQuery;
+use crate::ui::fuzzy_match::Matcher;
+
+fn run_test<'a>(query: &str, items: &'a [&'a str]) -> Vec<String> {
+ let query = FuzzyQuery::new(query);
+ let matcher = Matcher::default();
+ items
+ .iter()
+ .filter_map(|item| {
+ let (_, indicies) = query.fuzzy_indicies(item, &matcher)?;
+ let matched_string = indicies
+ .iter()
+ .map(|&pos| item.chars().nth(pos).unwrap())
+ .collect();
+ Some(matched_string)
+ })
+ .collect()
+}
+
+#[test]
+fn match_single_value() {
+ let matches = run_test("foo", &["foobar", "foo", "bar"]);
+ assert_eq!(matches, &["foo", "foo"])
+}
+
+#[test]
+fn match_multiple_values() {
+ let matches = run_test(
+ "foo bar",
+ &["foo bar", "foo bar", "bar foo", "bar", "foo"],
+ );
+ assert_eq!(matches, &["foobar", "foobar", "barfoo"])
+}
+
+#[test]
+fn space_escape() {
+ let matches = run_test(r"foo\ bar", &["bar foo", "foo bar", "foobar"]);
+ assert_eq!(matches, &["foo bar"])
+}
+
+#[test]
+fn trim() {
+ let matches = run_test(r" foo bar ", &["bar foo", "foo bar", "foobar"]);
+ assert_eq!(matches, &["barfoo", "foobar", "foobar"]);
+ let matches = run_test(r" foo bar\ ", &["bar foo", "foo bar", "foobar"]);
+ assert_eq!(matches, &["bar foo"])
+}
diff --git a/helix-term/src/ui/lsp.rs b/helix-term/src/ui/lsp.rs
index f2854551..393d24c4 100644
--- a/helix-term/src/ui/lsp.rs
+++ b/helix-term/src/ui/lsp.rs
@@ -68,8 +68,9 @@ impl Component for SignatureHelp {
let (_, sig_text_height) = crate::ui::text::required_size(&sig_text, area.width);
let sig_text_area = area.clip_top(1).with_height(sig_text_height);
+ let sig_text_area = sig_text_area.inner(&margin).intersection(surface.area);
let sig_text_para = Paragraph::new(sig_text).wrap(Wrap { trim: false });
- sig_text_para.render(sig_text_area.inner(&margin), surface);
+ sig_text_para.render(sig_text_area, surface);
if self.signature_doc.is_none() {
return;
diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs
index 1d247b1a..f77f5e80 100644
--- a/helix-term/src/ui/menu.rs
+++ b/helix-term/src/ui/menu.rs
@@ -105,7 +105,7 @@ impl<T: Item> Menu<T> {
.iter()
.enumerate()
.filter_map(|(index, option)| {
- let text: String = option.filter_text(&self.editor_data).into();
+ let text = option.filter_text(&self.editor_data);
// TODO: using fuzzy_indices could give us the char idx for match highlighting
self.matcher
.fuzzy_match(&text, pattern)
diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs
index 60ad3b24..6ac4dbb7 100644
--- a/helix-term/src/ui/mod.rs
+++ b/helix-term/src/ui/mod.rs
@@ -1,5 +1,6 @@
mod completion;
pub(crate) mod editor;
+mod fuzzy_match;
mod info;
pub mod lsp;
mod markdown;
@@ -12,6 +13,8 @@ mod spinner;
mod statusline;
mod text;
+use crate::compositor::{Component, Compositor};
+use crate::job;
pub use completion::Completion;
pub use editor::EditorView;
pub use markdown::Markdown;
@@ -24,7 +27,7 @@ pub use text::Text;
use helix_core::regex::Regex;
use helix_core::regex::RegexBuilder;
-use helix_view::{Document, Editor, View};
+use helix_view::Editor;
use std::path::PathBuf;
@@ -59,7 +62,7 @@ pub fn regex_prompt(
prompt: std::borrow::Cow<'static, str>,
history_register: Option<char>,
completion_fn: impl FnMut(&Editor, &str) -> Vec<prompt::Completion> + 'static,
- fun: impl Fn(&mut View, &mut Document, Regex, PromptEvent) + 'static,
+ fun: impl Fn(&mut Editor, Regex, PromptEvent) + 'static,
) {
let (view, doc) = current!(cx.editor);
let doc_id = view.doc;
@@ -106,11 +109,42 @@ pub fn regex_prompt(
view.jumps.push((doc_id, snapshot.clone()));
}
- fun(view, doc, regex, event);
+ fun(cx.editor, regex, event);
+ let (view, doc) = current!(cx.editor);
view.ensure_cursor_in_view(doc, config.scrolloff);
}
- Err(_err) => (), // TODO: mark command line as error
+ Err(err) => {
+ let (view, doc) = current!(cx.editor);
+ doc.set_selection(view.id, snapshot.clone());
+ view.offset = offset_snapshot;
+
+ if event == PromptEvent::Validate {
+ let callback = async move {
+ let call: job::Callback = Box::new(
+ move |_editor: &mut Editor, compositor: &mut Compositor| {
+ let contents = Text::new(format!("{}", err));
+ let size = compositor.size();
+ let mut popup = Popup::new("invalid-regex", contents)
+ .position(Some(helix_core::Position::new(
+ size.height as usize - 2, // 2 = statusline + commandline
+ 0,
+ )))
+ .auto_close(true);
+ popup.required_size((size.width, size.height));
+
+ compositor.replace_or_push("invalid-regex", popup);
+ },
+ );
+ Ok(call)
+ };
+
+ cx.jobs.callback(callback);
+ } else {
+ // Update
+ // TODO: mark command line as error
+ }
+ }
}
}
}
diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs
index a56455d7..c7149c61 100644
--- a/helix-term/src/ui/picker.rs
+++ b/helix-term/src/ui/picker.rs
@@ -1,7 +1,7 @@
use crate::{
compositor::{Component, Compositor, Context, Event, EventResult},
ctrl, key, shift,
- ui::{self, EditorView},
+ ui::{self, fuzzy_match::FuzzyQuery, EditorView},
};
use tui::{
buffer::Buffer as Surface,
@@ -9,7 +9,6 @@ use tui::{
};
use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
-use fuzzy_matcher::FuzzyMatcher;
use tui::widgets::Widget;
use std::time::Instant;
@@ -161,6 +160,27 @@ impl<T: Item> FilePicker<T> {
self.preview_cache.insert(path.to_owned(), preview);
Preview::Cached(&self.preview_cache[path])
}
+
+ fn handle_idle_timeout(&mut self, cx: &mut Context) -> EventResult {
+ // Try to find a document in the cache
+ let doc = self
+ .current_file(cx.editor)
+ .and_then(|(path, _range)| self.preview_cache.get_mut(&path))
+ .and_then(|cache| match cache {
+ CachedPreview::Document(doc) => Some(doc),
+ _ => None,
+ });
+
+ // Then attempt to highlight it if it has no language set
+ if let Some(doc) = doc {
+ if doc.language_config().is_none() {
+ let loader = cx.editor.syn_loader.clone();
+ doc.detect_language(loader);
+ }
+ }
+
+ EventResult::Consumed(None)
+ }
}
impl<T: Item + 'static> Component for FilePicker<T> {
@@ -261,6 +281,9 @@ impl<T: Item + 'static> Component for FilePicker<T> {
}
fn handle_event(&mut self, event: &Event, ctx: &mut Context) -> EventResult {
+ if let Event::IdleTimeout = event {
+ return self.handle_idle_timeout(ctx);
+ }
// TODO: keybinds for scrolling preview
self.picker.handle_event(event, ctx)
}
@@ -287,8 +310,6 @@ pub struct Picker<T: Item> {
matcher: Box<Matcher>,
/// (index, score)
matches: Vec<(usize, i64)>,
- /// Filter over original options.
- filters: Vec<usize>, // could be optimized into bit but not worth it now
/// Current height of the completions box
completion_height: u16,
@@ -323,7 +344,6 @@ impl<T: Item> Picker<T> {
editor_data,
matcher: Box::new(Matcher::default()),
matches: Vec::new(),
- filters: Vec::new(),
cursor: 0,
prompt,
previous_pattern: String::new(),
@@ -365,13 +385,14 @@ impl<T: Item> Picker<T> {
.map(|(index, _option)| (index, 0)),
);
} else if pattern.starts_with(&self.previous_pattern) {
+ let query = FuzzyQuery::new(pattern);
// optimization: if the pattern is a more specific version of the previous one
// then we can score the filtered set.
self.matches.retain_mut(|(index, score)| {
let option = &self.options[*index];
let text = option.sort_text(&self.editor_data);
- match self.matcher.fuzzy_match(&text, pattern) {
+ match query.fuzzy_match(&text, &self.matcher) {
Some(s) => {
// Update the score
*score = s;
@@ -384,23 +405,17 @@ impl<T: Item> Picker<T> {
self.matches
.sort_unstable_by_key(|(_, score)| Reverse(*score));
} else {
+ let query = FuzzyQuery::new(pattern);
self.matches.clear();
self.matches.extend(
self.options
.iter()
.enumerate()
.filter_map(|(index, option)| {
- // filter options first before matching
- if !self.filters.is_empty() {
- // TODO: this filters functionality seems inefficient,
- // instead store and operate on filters if any
- self.filters.binary_search(&index).ok()?;
- }
-
let text = option.filter_text(&self.editor_data);
- self.matcher
- .fuzzy_match(&text, pattern)
+ query
+ .fuzzy_match(&text, &self.matcher)
.map(|score| (index, score))
}),
);
@@ -460,14 +475,6 @@ impl<T: Item> Picker<T> {
.map(|(index, _score)| &self.options[*index])
}
- pub fn save_filter(&mut self, cx: &Context) {
- self.filters.clear();
- self.filters
- .extend(self.matches.iter().map(|(index, _)| *index));
- self.filters.sort_unstable(); // used for binary search later
- self.prompt.clear(cx.editor);
- }
-
pub fn toggle_preview(&mut self) {
self.show_preview = !self.show_preview;
}
@@ -505,6 +512,9 @@ impl<T: Item + 'static> Component for Picker<T> {
compositor.last_picker = compositor.pop();
})));
+ // So that idle timeout retriggers
+ cx.editor.reset_idle_timer();
+
match key_event {
shift!(Tab) | key!(Up) | ctrl!('p') => {
self.move_by(1, Direction::Backward);
@@ -545,9 +555,6 @@ impl<T: Item + 'static> Component for Picker<T> {
}
return close_fn;
}
- ctrl!(' ') => {
- self.save_filter(cx);
- }
ctrl!('t') => {
self.toggle_preview();
}
@@ -630,9 +637,8 @@ impl<T: Item + 'static> Component for Picker<T> {
}
let spans = option.label(&self.editor_data);
- let (_score, highlights) = self
- .matcher
- .fuzzy_indices(&String::from(&spans), self.prompt.line())
+ let (_score, highlights) = FuzzyQuery::new(self.prompt.line())
+ .fuzzy_indicies(&String::from(&spans), &self.matcher)
.unwrap_or_default();
spans.0.into_iter().fold(inner, |pos, span| {
diff --git a/helix-term/src/ui/statusline.rs b/helix-term/src/ui/statusline.rs
index 365e1ca9..b0e8ec5d 100644
--- a/helix-term/src/ui/statusline.rs
+++ b/helix-term/src/ui/statusline.rs
@@ -144,6 +144,7 @@ where
helix_view::editor::StatusLineElement::Selections => render_selections,
helix_view::editor::StatusLineElement::Position => render_position,
helix_view::editor::StatusLineElement::PositionPercentage => render_position_percentage,
+ helix_view::editor::StatusLineElement::TotalLineNumbers => render_total_line_numbers,
helix_view::editor::StatusLineElement::Separator => render_separator,
helix_view::editor::StatusLineElement::Spacer => render_spacer,
}
@@ -154,16 +155,16 @@ where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
let visible = context.focused;
-
+ let modenames = &context.editor.config().statusline.mode;
write(
context,
format!(
" {} ",
if visible {
match context.editor.mode() {
- Mode::Insert => "INS",
- Mode::Select => "SEL",
- Mode::Normal => "NOR",
+ Mode::Insert => &modenames.insert,
+ Mode::Select => &modenames.select,
+ Mode::Normal => &modenames.normal,
}
} else {
// If not focused, explicitly leave an empty space instead of returning None.
@@ -276,6 +277,15 @@ where
);
}
+fn render_total_line_numbers<F>(context: &mut RenderContext, write: F)
+where
+ F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
+{
+ let total_line_numbers = context.doc.text().len_lines();
+
+ write(context, format!(" {} ", total_line_numbers), None);
+}
+
fn render_position_percentage<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,