diff --git a/Cargo.lock b/Cargo.lock index e5edcaac..45eb9023 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -523,6 +523,7 @@ dependencies = [ "futures-util", "helix-core", "helix-dap", + "helix-loader", "helix-lsp", "helix-tui", "log", @@ -1024,9 +1025,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" [[package]] name = "smartstring" @@ -1111,18 +1112,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.36" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a99cb8c4b9a8ef0e7907cd3b617cc8dc04d571c4e73c8ae403d80ac160bb122" +checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.36" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a891860d3c8d66fec8e73ddb3765f90082374dbaaa833407b904a94f1a7eb43" +checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" dependencies = [ "proc-macro2", "quote", @@ -1164,9 +1165,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.21.1" +version = "1.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0020c875007ad96677dcc890298f4b942882c5d4eb7cc8f439fc3bf813dc9c95" +checksum = "a9e03c497dc955702ba729190dc4aac6f2a0ce97f913e5b1b5912fc5039d9099" dependencies = [ "autocfg", "bytes", @@ -1174,7 +1175,6 @@ dependencies = [ "memchr", "mio", "num_cpus", - "once_cell", "parking_lot", "pin-project-lite", "signal-hook-registry", diff --git a/Cargo.toml b/Cargo.toml index 780811f7..9e985ddc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,9 +14,6 @@ default-members = [ "helix-term" ] -[profile.dev] -split-debuginfo = "unpacked" - [profile.release] lto = "thin" # debug = true diff --git a/book/src/configuration.md b/book/src/configuration.md index 1ebfea78..cfa03430 100644 --- a/book/src/configuration.md +++ b/book/src/configuration.md @@ -28,6 +28,10 @@ hidden = false You may also specify a file to use for configuration with the `-c` or `--config` CLI argument: `hx -c path/to/custom-config.toml`. +It is also possible to trigger configuration file reloading by sending the `USR1` +signal to the helix process, e.g. via `pkill -USR1 hx`. This is only supported +on unix operating systems. + ## Editor ### `[editor]` Section @@ -68,17 +72,32 @@ left = ["mode", "spinner"] center = ["file-name"] right = ["diagnostics", "selections", "position", "file-encoding", "file-line-ending", "file-type"] separator = "│" +mode.normal = "NORMAL" +mode.insert = "INSERT" +mode.select = "SELECT" ``` +The `[editor.statusline]` key takes the following sub-keys: + +| Key | Description | Default | +| --- | --- | --- | +| `left` | A list of elements aligned to the left of the statusline | `["mode", "spinner", "file-name"]` | +| `center` | A list of elements aligned to the middle of the statusline | `[]` | +| `right` | A list of elements aligned to the right of the statusline | `["diagnostics", "selections", "position", "file-encoding"]` | +| `separator` | The character used to separate elements in the statusline | `"│"` | +| `mode.normal` | The text shown in the `mode` element for normal mode | `"NOR"` | +| `mode.insert` | The text shown in the `mode` element for insert mode | `"INS"` | +| `mode.select` | The text shown in the `mode` element for select mode | `"SEL"` | -The following elements can be configured: +The following statusline elements can be configured: | Key | Description | | ------ | ----------- | -| `mode` | The current editor mode (`NOR`/`INS`/`SEL`) | +| `mode` | The current editor mode (`mode.normal`/`mode.insert`/`mode.select`) | | `spinner` | A progress spinner indicating LSP activity | | `file-name` | The path/name of the opened file | | `file-encoding` | The encoding of the opened file if it differs from UTF-8 | | `file-line-ending` | The file line endings (CRLF or LF) | +| `total-line-numbers` | The total line numbers of the opened file | | `file-type` | The type of the opened file | | `diagnostics` | The number of warnings and/or errors | | `selections` | The number of active selections | @@ -232,11 +251,24 @@ Sets explorer side width and style. Options for rendering vertical indent guides. +<<<<<<< HEAD | Key | Description | Default | | --- | --- | --- | | `render` | Whether to render indent guides. | `false` | | `character` | Literal character to use for rendering the indent guide | `│` | | `rainbow` | Whether or not the indent guides shall have changing colors. | `false` | +||||||| 60aa7d36 +| Key | Description | Default | +| --- | --- | --- | +| `render` | Whether to render indent guides. | `false` | +| `character` | Literal character to use for rendering the indent guide | `│` | +======= +| Key | Description | Default | +| --- | --- | --- | +| `render` | Whether to render indent guides. | `false` | +| `character` | Literal character to use for rendering the indent guide | `│` | +| `skip-levels` | Number of indent levels to skip | `0` | +>>>>>>> seperate_code_action Example: @@ -244,7 +276,12 @@ Example: [editor.indent-guides] render = true character = "╎" +<<<<<<< HEAD rainbow = true +||||||| 60aa7d36 +======= +skip-levels = 1 +>>>>>>> seperate_code_action ``` ### `[editor.explorer]` Section diff --git a/book/src/generated/lang-support.md b/book/src/generated/lang-support.md index 86c26042..5c64d097 100644 --- a/book/src/generated/lang-support.md +++ b/book/src/generated/lang-support.md @@ -119,6 +119,8 @@ | vala | ✓ | | | `vala-language-server` | | verilog | ✓ | ✓ | | `svlangserver` | | vue | ✓ | | | `vls` | +| wast | ✓ | | | | +| wat | ✓ | | | | | wgsl | ✓ | | | `wgsl_analyzer` | | xit | ✓ | | | | | yaml | ✓ | | ✓ | `yaml-language-server` | diff --git a/book/src/keymap.md b/book/src/keymap.md index cdaa11c5..1373b065 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -68,8 +68,8 @@ | `` Alt-` `` | Set the selected text to upper case | `switch_to_uppercase` | | `i` | Insert before selection | `insert_mode` | | `a` | Insert after selection (append) | `append_mode` | -| `I` | Insert at the start of the line | `prepend_to_line` | -| `A` | Insert at the end of the line | `append_to_line` | +| `I` | Insert at the start of the line | `insert_at_line_start` | +| `A` | Insert at the end of the line | `insert_at_line_end` | | `o` | Open new line below selection | `open_below` | | `O` | Open new line above selection | `open_above` | | `.` | Repeat last insert | N/A | @@ -129,6 +129,7 @@ | `X` | Extend selection to line bounds (line-wise selection) | `extend_to_line_bounds` | | `Alt-x` | Shrink selection to line bounds (line-wise selection) | `shrink_to_line_bounds` | | `J` | Join lines inside selection | `join_selections` | +| `A-J` | Join lines inside selection and select space | `join_selections_space` | | `K` | Keep selections matching the regex | `keep_selections` | | `Alt-K` | Remove selections matching the regex | `remove_selections` | | `Ctrl-c` | Comment/uncomment the selections | `toggle_comments` | @@ -315,7 +316,7 @@ Mappings in the style of [vim-unimpaired](https://github.com/tpope/vim-unimpaire | `[space` | Add newline above | `add_newline_above` | | `]space` | Add newline below | `add_newline_below` | -## Insert Mode +## Insert mode Insert mode bindings are somewhat minimal by default. Helix is designed to be a modal editor, and this is reflected in the user experience and internal @@ -324,44 +325,47 @@ escaping from insert mode to normal mode. For this reason, new users are strongly encouraged to learn the modal editing paradigm to get the smoothest experience. -| Key | Description | Command | -| ----- | ----------- | ------- | -| `Escape` | Switch to normal mode | `normal_mode` | -| `Ctrl-x` | Autocomplete | `completion` | -| `Ctrl-r` | Insert a register content | `insert_register` | -| `Ctrl-w`, `Alt-Backspace`, `Ctrl-Backspace` | Delete previous word | `delete_word_backward` | -| `Alt-d`, `Alt-Delete`, `Ctrl-Delete` | Delete next word | `delete_word_forward` | -| `Ctrl-u` | Delete to start of line | `kill_to_line_start` | -| `Ctrl-k` | Delete to end of line | `kill_to_line_end` | -| `Ctrl-j`, `Enter` | Insert new line | `insert_newline` | -| `Backspace`, `Ctrl-h` | Delete previous char | `delete_char_backward` | -| `Delete`, `Ctrl-d` | Delete next char | `delete_char_forward` | - -However, if you really want navigation in insert mode, this is supported. An -example config that gives the ability to use arrow keys while still in insert -mode: +| Key | Description | Command | +| ----- | ----------- | ------- | +| `Escape` | Switch to normal mode | `normal_mode` | +| `Ctrl-s` | Commit undo checkpoint | `commit_undo_checkpoint` | +| `Ctrl-x` | Autocomplete | `completion` | +| `Ctrl-r` | Insert a register content | `insert_register` | +| `Ctrl-w`, `Alt-Backspace` | Delete previous word | `delete_word_backward` | +| `Alt-d`, `Alt-Delete` | Delete next word | `delete_word_forward` | +| `Ctrl-u` | Delete to start of line | `kill_to_line_start` | +| `Ctrl-k` | Delete to end of line | `kill_to_line_end` | +| `Ctrl-h`, `Backspace` | Delete previous char | `delete_char_backward` | +| `Ctrl-d`, `Delete` | Delete next char | `delete_char_forward` | +| `Ctrl-j`, `Enter` | Insert new line | `insert_newline` | + +These keys are not recommended, but are included for new users less familiar +with modal editors. + +| Key | Description | Command | +| ----- | ----------- | ------- | +| `Up` | Move to previous line | `move_line_up` | +| `Down` | Move to next line | `move_line_down` | +| `Left` | Backward a char | `move_char_left` | +| `Right` | Forward a char | `move_char_right` | +| `PageUp` | Move one page up | `page_up` | +| `PageDown` | Move one page down | `page_down` | +| `Home` | Move to line start | `goto_line_start` | +| `End` | Move to line end | `goto_line_end_newline` | + +If you want to disable them in insert mode as you become more comfortable with modal editing, you can use +the following in your `config.toml`: ```toml [keys.insert] -"up" = "move_line_up" -"down" = "move_line_down" -"left" = "move_char_left" -"right" = "move_char_right" -"C-b" = "move_char_left" -"C-f" = "move_char_right" -"A-b" = "move_prev_word_end" -"C-left" = "move_prev_word_end" -"A-f" = "move_next_word_start" -"C-right" = "move_next_word_start" -"A-<" = "goto_file_start" -"A->" = "goto_file_end" -"pageup" = "page_up" -"pagedown" = "page_down" -"home" = "goto_line_start" -"C-a" = "goto_line_start" -"end" = "goto_line_end_newline" -"C-e" = "goto_line_end_newline" -"A-left" = "goto_line_start" +up = "no_op" +down = "no_op" +left = "no_op" +right = "no_op" +pageup = "no_op" +pagedown = "no_op" +home = "no_op" +end = "no_op" ``` ## Select / extend mode diff --git a/book/src/themes.md b/book/src/themes.md index ea516959..650ffac3 100644 --- a/book/src/themes.md +++ b/book/src/themes.md @@ -89,6 +89,7 @@ Less common modifiers might not be supported by your terminal emulator. | `hidden` | | `crossed_out` | +<<<<<<< HEAD ### Rainbow The `rainbow` key is used for rainbow highlight for matching brackets. @@ -100,6 +101,24 @@ rainbow = ["#ff0000", "#ffa500", "#fff000", { fg = "#00ff00", modifiers = ["bold Colors from the palette and modifiers may be used. +||||||| 60aa7d36 +======= +### Inheritance + +Extend upon other themes by setting the `inherits` property to an existing theme. + +```toml +inherits = "boo_berry" + +# Override the theming for "keyword"s: +"keyword" = { fg = "gold" } + +# Override colors in the palette: +[palette] +berry = "#2A2A4D" +``` + +>>>>>>> seperate_code_action ### Scopes The following is a list of scopes available to use for styling. @@ -230,6 +249,8 @@ These scopes are used for theming the editor interface. | `ui.cursor.select` | | | `ui.cursor.match` | Matching bracket etc. | | `ui.cursor.primary` | Cursor with primary selection | +| `ui.gutter` | Gutter | +| `ui.gutter.selected` | Gutter for the line the cursor is on | | `ui.linenr` | Line numbers | | `ui.linenr.selected` | Line number for the line the cursor is on | | `ui.statusline` | Statusline | diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml index ba6901ba..d7ab89ba 100644 --- a/helix-core/Cargo.toml +++ b/helix-core/Cargo.toml @@ -18,7 +18,7 @@ integration = [] helix-loader = { version = "0.6", path = "../helix-loader" } ropey = { version = "1.5", default-features = false, features = ["simd"] } -smallvec = "1.9" +smallvec = "1.10" smartstring = "1.0.1" unicode-segmentation = "1.10" unicode-width = "0.1" diff --git a/helix-core/src/movement.rs b/helix-core/src/movement.rs index c232484c..278375e8 100644 --- a/helix-core/src/movement.rs +++ b/helix-core/src/movement.rs @@ -389,6 +389,8 @@ fn reached_target(target: WordMotionTarget, prev_ch: char, next_ch: char) -> boo } } +/// Finds the range of the next or previous textobject in the syntax sub-tree of `node`. +/// Returns the range in the forwards direction. pub fn goto_treesitter_object( slice: RopeSlice, range: Range, @@ -419,8 +421,8 @@ pub fn goto_treesitter_object( .filter(|n| n.start_byte() > byte_pos) .min_by_key(|n| n.start_byte())?, Direction::Backward => nodes - .filter(|n| n.start_byte() < byte_pos) - .max_by_key(|n| n.start_byte())?, + .filter(|n| n.end_byte() < byte_pos) + .max_by_key(|n| n.end_byte())?, }; let len = slice.len_bytes(); @@ -434,7 +436,7 @@ pub fn goto_treesitter_object( let end_char = slice.byte_to_char(end_byte); // head of range should be at beginning - Some(Range::new(end_char, start_char)) + Some(Range::new(start_char, end_char)) }; (0..count).fold(range, |range, _| get_range(range).unwrap_or(range)) } diff --git a/helix-core/src/selection.rs b/helix-core/src/selection.rs index 3463c1d3..1f28ecef 100644 --- a/helix-core/src/selection.rs +++ b/helix-core/src/selection.rs @@ -122,7 +122,7 @@ impl Range { } } - // flips the direction of the selection + /// Flips the direction of the selection pub fn flip(&self) -> Self { Self { anchor: self.head, @@ -131,6 +131,16 @@ impl Range { } } + /// Returns the selection if it goes in the direction of `direction`, + /// flipping the selection otherwise. + pub fn with_direction(self, direction: Direction) -> Self { + if self.direction() == direction { + self + } else { + self.flip() + } + } + /// Check two ranges for overlap. #[must_use] pub fn overlaps(&self, other: &Self) -> bool { diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs index ee3cbf90..af3c4b57 100644 --- a/helix-lsp/src/client.rs +++ b/helix-lsp/src/client.rs @@ -49,6 +49,7 @@ impl Client { root_markers: &[String], id: usize, req_timeout: u64, + doc_path: Option<&std::path::PathBuf>, ) -> Result<(Self, UnboundedReceiver<(usize, Call)>, Arc)> { // Resolve path to the binary let cmd = which::which(cmd).map_err(|err| anyhow::anyhow!(err))?; @@ -72,7 +73,10 @@ impl Client { let (server_rx, server_tx, initialize_notify) = Transport::start(reader, writer, stderr, id); - let root_path = find_root(None, root_markers); + let root_path = find_root( + doc_path.and_then(|x| x.parent().and_then(|x| x.to_str())), + root_markers, + ); let root_uri = lsp::Url::from_file_path(root_path.clone()).ok(); diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index a6e6be2f..5999feac 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -14,6 +14,7 @@ use tokio::sync::mpsc::UnboundedReceiver; use std::{ collections::{hash_map::Entry, HashMap}, + path::PathBuf, sync::{ atomic::{AtomicUsize, Ordering}, Arc, @@ -206,6 +207,20 @@ pub mod util { // in reverse order. edits.sort_unstable_by_key(|edit| edit.range.start); + // Generate a diff if the edit is a full document replacement. + #[allow(clippy::collapsible_if)] + if edits.len() == 1 { + let is_document_replacement = edits.first().and_then(|edit| { + let start = lsp_pos_to_pos(doc, edit.range.start, offset_encoding)?; + let end = lsp_pos_to_pos(doc, edit.range.end, offset_encoding)?; + Some(start..end) + }) == Some(0..doc.len_chars()); + if is_document_replacement { + let new_text = Rope::from(edits.pop().unwrap().new_text); + return helix_core::diff::compare_ropes(doc, &new_text); + } + } + Transaction::change( doc, edits.into_iter().map(|edit| { @@ -338,7 +353,11 @@ impl Registry { .map(|(_, client)| client.as_ref()) } - pub fn get(&mut self, language_config: &LanguageConfiguration) -> Result>> { + pub fn get( + &mut self, + language_config: &LanguageConfiguration, + doc_path: Option<&std::path::PathBuf>, + ) -> Result>> { let config = match &language_config.language_server { Some(config) => config, None => return Ok(None), @@ -350,7 +369,8 @@ impl Registry { // initialize a new client let id = self.counter.fetch_add(1, Ordering::Relaxed); - let NewClientResult(client, incoming) = start_client(id, language_config, config)?; + let NewClientResult(client, incoming) = + start_client(id, language_config, config, doc_path)?; self.incoming.push(UnboundedReceiverStream::new(incoming)); entry.insert((id, client.clone())); @@ -359,7 +379,11 @@ impl Registry { } } - pub fn restart(&mut self, language_config: &LanguageConfiguration) -> Result> { + pub fn restart( + &mut self, + language_config: &LanguageConfiguration, + path: Option<&PathBuf>, + ) -> Result> { let config = language_config .language_server .as_ref() @@ -369,7 +393,7 @@ impl Registry { .get(&language_config.scope) .ok_or(Error::LspNotDefined)? .0; - let new_client = self.initialize_client(language_config, config, id)?; + let new_client = self.initialize_client(language_config, config, id, path)?; let (_, client) = self .inner .get_mut(&language_config.scope) @@ -384,6 +408,7 @@ impl Registry { language_config: &LanguageConfiguration, config: &helix_core::syntax::LanguageServerConfiguration, id: usize, + path: Option<&PathBuf>, ) -> Result> { let (client, incoming, initialize_notify) = Client::start( &config.command, @@ -392,6 +417,7 @@ impl Registry { &language_config.roots, id, config.timeout, + path, )?; self.incoming.push(UnboundedReceiverStream::new(incoming)); let client = Arc::new(client); @@ -517,6 +543,7 @@ fn start_client( id: usize, config: &LanguageConfiguration, ls_config: &LanguageServerConfiguration, + doc_path: Option<&std::path::PathBuf>, ) -> Result { let (client, incoming, initialize_notify) = Client::start( &ls_config.command, @@ -525,6 +552,7 @@ fn start_client( &config.roots, id, ls_config.timeout, + doc_path, )?; let client = Arc::new(client); diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index ac50b610..30de5589 100644 --- a/helix-term/Cargo.toml +++ b/helix-term/Cargo.toml @@ -74,6 +74,6 @@ signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] } helix-loader = { version = "0.6", path = "../helix-loader" } [dev-dependencies] -smallvec = "1.9" +smallvec = "1.10" indoc = "1.0.6" tempfile = "3.3.0" diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 8dd4f1ed..d1cce281 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -236,8 +236,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, @@ -438,6 +438,10 @@ impl Application { self.compositor.load_cursor(); self.render(); } + signal::SIGUSR1 => { + self.refresh_config(); + self.render(); + } _ => unreachable!(), } } @@ -878,9 +882,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/commands.rs b/helix-term/src/commands.rs index a777c640..2031fd06 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -274,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", @@ -347,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", @@ -889,8 +890,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) { @@ -1515,7 +1520,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; } @@ -1536,7 +1542,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; } @@ -1560,15 +1567,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); @@ -1598,17 +1606,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); @@ -1684,19 +1704,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, ®ex, Movement::Move, direction, scrolloff, wrap_around, + false, ); }, ); @@ -1706,7 +1726,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(); @@ -1724,14 +1744,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, ®ex, movement, direction, scrolloff, wrap_around, + true, ); } } else { @@ -1829,7 +1849,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; } @@ -2165,10 +2185,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); } @@ -2451,11 +2468,11 @@ 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(), + Some(bindings) => format!("{} ({}) [{}]", doc, fmt_binding(bindings), name).into(), None => doc.as_str().into(), }, MappableCommand::Static { doc, name, .. } => match keymap.get(*name) { - Some(bindings) => format!("{} ({})", doc, fmt_binding(bindings)).into(), + Some(bindings) => format!("{} ({}) [{}]", doc, fmt_binding(bindings), name).into(), None => (*doc).into(), }, } @@ -2506,13 +2523,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); @@ -2545,18 +2562,23 @@ async fn make_format_callback( ) -> anyhow::Result { let format = format.await?; let call: job::Callback = Box::new(move |editor, _compositor| { - let view_id = view!(editor).id; - if let Some(doc) = editor.document_mut(doc_id) { - if doc.version() == doc_version { - doc.apply(&format, view_id); - doc.append_changes_to_history(view_id); - doc.detect_indent_and_line_ending(); - if let Modified::SetUnmodified = modified { - doc.reset_modified(); - } - } else { - log::info!("discarded formatting changes because the document changed"); + if !editor.documents.contains_key(&doc_id) { + return; + } + + let scrolloff = editor.config().scrolloff; + let doc = doc_mut!(editor, &doc_id); + let view = view_mut!(editor); + if doc.version() == doc_version { + doc.apply(&format, view.id); + doc.append_changes_to_history(view.id); + doc.detect_indent_and_line_ending(); + view.ensure_cursor_in_view(doc, scrolloff); + if let Modified::SetUnmodified = modified { + doc.reset_modified(); } + } else { + log::info!("discarded formatting changes because the document changed"); } }); Ok(call) @@ -3772,7 +3794,7 @@ fn format_selections(cx: &mut Context) { } } -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(); @@ -3807,9 +3829,21 @@ 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); } @@ -3822,7 +3856,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; } @@ -3837,6 +3872,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) } @@ -4345,7 +4388,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, @@ -4353,7 +4396,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); @@ -4418,7 +4473,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); @@ -4467,33 +4521,31 @@ 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 + .into_iter() + .map(|(col1, col2)| (col1.to_string(), col2.to_string())) + .collect(), + )); } fn surround_add(cx: &mut Context) { diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index 5a33e833..78fb53e4 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -452,43 +452,73 @@ pub fn code_action(cx: &mut Context) { cx.callback( future, move |editor, compositor, response: Option| { - let actions = match response { + let mut actions = match response { Some(a) => a, None => return, }; + if actions.is_empty() { editor.set_status("No code actions available"); return; } - let mut picker = ui::Menu::new(actions, (), move |editor, code_action, event| { - if event != PromptEvent::Validate { - return; + // sort by CodeActionKind + // this ensures that the most relevant codeactions (quickfix) show up first + // while more situational commands (like refactors) show up later + // this behaviour is modeled after the behaviour of vscode (https://github.com/microsoft/vscode/blob/eaec601dd69aeb4abb63b9601a6f44308c8d8c6e/src/vs/editor/contrib/codeAction/browser/codeActionWidget.ts) + + actions.sort_by_key(|action| match &action { + lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction { + kind: Some(kind), .. + }) => { + let mut components = kind.as_str().split('.'); + + match components.next() { + Some("quickfix") => 0, + Some("refactor") => match components.next() { + Some("extract") => 1, + Some("inline") => 2, + Some("rewrite") => 3, + Some("move") => 4, + Some("surround") => 5, + _ => 7, + }, + Some("source") => 6, + _ => 7, + } } + _ => 7, + }); - // always present here - let code_action = code_action.unwrap(); - - match code_action { - lsp::CodeActionOrCommand::Command(command) => { - log::debug!("code action command: {:?}", command); - execute_lsp_command(editor, command.clone()); + let mut picker = + ui::Menu::new(actions, false, (), move |editor, code_action, event| { + if event != PromptEvent::Validate { + return; } - lsp::CodeActionOrCommand::CodeAction(code_action) => { - log::debug!("code action: {:?}", code_action); - if let Some(ref workspace_edit) = code_action.edit { - log::debug!("edit: {:?}", workspace_edit); - apply_workspace_edit(editor, offset_encoding, workspace_edit); - } - // if code action provides both edit and command first the edit - // should be applied and then the command - if let Some(command) = &code_action.command { + // always present here + let code_action = code_action.unwrap(); + + match code_action { + lsp::CodeActionOrCommand::Command(command) => { + log::debug!("code action command: {:?}", command); execute_lsp_command(editor, command.clone()); } + lsp::CodeActionOrCommand::CodeAction(code_action) => { + log::debug!("code action: {:?}", code_action); + if let Some(ref workspace_edit) = code_action.edit { + log::debug!("edit: {:?}", workspace_edit); + apply_workspace_edit(editor, offset_encoding, workspace_edit); + } + + // if code action provides both edit and command first the edit + // should be applied and then the command + if let Some(command) = &code_action.command { + execute_lsp_command(editor, command.clone()); + } + } } - } - }); + }); picker.move_down(); // pre-select the first item let popup = Popup::new("code-action", picker); diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 91fa3cfc..576b1d0c 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -2,7 +2,7 @@ use std::ops::Deref; use super::*; -use helix_view::editor::{Action, ConfigEvent}; +use helix_view::editor::{Action, CloseError, ConfigEvent}; use ui::completers::{self, Completer}; #[derive(Clone)] @@ -71,8 +71,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(¤t.id()) { + editor.switch(*first, Action::Replace); + } + bail!( + "{} unsaved buffer(s) remaining: {:?}", + modified_names.len(), + modified_names + ); } Ok(()) @@ -538,23 +559,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(¤t.id()) { + editor.switch(*first, Action::Replace); + } bail!( "{} unsaved buffer(s) remaining: {:?}", - modified.len(), - modified + modified_names.len(), + modified_names ); } Ok(()) @@ -1025,7 +1049,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 = cx @@ -1221,18 +1245,41 @@ pub(super) fn goto_line_number( args: &[Cow], 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::()?; - goto_line_impl(cx.editor, NonZeroUsize::new(line)); - let (view, doc) = current!(cx.editor); - view.ensure_cursor_in_view(doc, line); Ok(()) } diff --git a/helix-term/src/keymap/default.rs b/helix-term/src/keymap/default.rs index 75fe238f..9d6699b9 100644 --- a/helix-term/src/keymap/default.rs +++ b/helix-term/src/keymap/default.rs @@ -61,9 +61,9 @@ pub fn default() -> HashMap { ":" => 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, @@ -146,6 +146,7 @@ pub fn default() -> HashMap { "<" => unindent, "=" => format_selections, "J" => join_selections, + "A-J" => join_selections_space, "K" => keep_selections, "A-K" => remove_selections, @@ -347,24 +348,27 @@ pub fn default() -> HashMap { 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 a2d71ca4..0203c129 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -97,7 +97,7 @@ impl Completion { start_offset: usize, trigger_offset: usize, ) -> Self { - let menu = Menu::new(items, (), move |editor: &mut Editor, item, event| { + let menu = Menu::new(items, true, (), move |editor: &mut Editor, item, event| { fn item_to_transaction( doc: &Document, item: &CompletionItem, diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 984378a2..296483ec 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -33,6 +33,7 @@ use super::statusline; pub struct EditorView { pub keymaps: Keymaps, on_next_key: Option>, + pseudo_pending: Vec, last_insert: (commands::MappableCommand, Vec), pub(crate) completion: Option, spinners: ProgressSpinners, @@ -57,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(), @@ -438,7 +440,8 @@ impl EditorView { return; } - let starting_indent = (offset.col / tab_width) as u16; + let starting_indent = + (offset.col / tab_width) as u16 + config.indent_guides.skip_levels; // 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. @@ -705,6 +708,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); @@ -717,6 +721,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 { @@ -840,6 +850,7 @@ impl EditorView { event: KeyEvent, ) -> Option { 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()); @@ -1317,6 +1328,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(); @@ -1432,8 +1448,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/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..ee11bfbc 100644 --- a/helix-term/src/ui/menu.rs +++ b/helix-term/src/ui/menu.rs @@ -74,6 +74,7 @@ impl Menu { // rendering) pub fn new( options: Vec, + sort: bool, editor_data: ::Data, callback_fn: impl Fn(&mut Editor, Option<&T>, MenuEvent) + 'static, ) -> Self { @@ -91,8 +92,12 @@ impl Menu { recalculate: true, }; - // TODO: scoring on empty input should just use a fastpath - menu.score(""); + if sort { + // TODO: scoring on empty input should just use a fastpath + menu.score(""); + } else { + menu.matches = (0..menu.options.len()).map(|i| (i, 0)).collect(); + } menu } @@ -112,10 +117,7 @@ impl Menu { .map(|score| (index, score)) }), ); - // matches.sort_unstable_by_key(|(_, score)| -score); - self.matches.sort_unstable_by_key(|(index, _score)| { - self.options[*index].sort_text(&self.editor_data) - }); + self.matches.sort_unstable_by_key(|(_, score)| -score); // reset cursor position self.cursor = None; diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index aa720c94..c70ea1d0 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -14,6 +14,8 @@ mod statusline; mod text; mod tree; +use crate::compositor::{Component, Compositor}; +use crate::job; pub use completion::Completion; pub use editor::EditorView; pub use explore::Explorer; @@ -28,7 +30,7 @@ pub use tree::{Tree, TreeItem, TreeOp}; use helix_core::regex::Regex; use helix_core::regex::RegexBuilder; -use helix_view::{Document, Editor, View}; +use helix_view::Editor; use std::path::PathBuf; @@ -63,7 +65,7 @@ pub fn regex_prompt( prompt: std::borrow::Cow<'static, str>, history_register: Option, completion_fn: impl FnMut(&Editor, &str) -> Vec + '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; @@ -110,11 +112,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/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