From a252ecd8c85af5cc16638a4752011e2e920fa652 Mon Sep 17 00:00:00 2001 From: Omnikar Date: Sun, 7 Nov 2021 19:54:39 -0500 Subject: Add WORD textobject (#991) * Add WORD textobject * Document WORD textobject--- helix-term/src/commands.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'helix-term/src/commands.rs') diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 80cbd6d2..5f091775 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -4672,7 +4672,8 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) { let selection = doc.selection(view.id).clone().transform(|range| { match ch { - 'w' => textobject::textobject_word(text, range, objtype, count), + 'w' => textobject::textobject_word(text, range, objtype, count, false), + 'W' => textobject::textobject_word(text, range, objtype, count, true), 'c' => textobject_treesitter("class", range), 'f' => textobject_treesitter("function", range), 'p' => textobject_treesitter("parameter", range), -- cgit v1.2.3-70-g09d2 From 29e684941389f949491efe4d9807a0edd1facee3 Mon Sep 17 00:00:00 2001 From: CossonLeo Date: Mon, 8 Nov 2021 23:17:54 +0800 Subject: Add LSP rename_symbol (space-r) (#1011) improve apply_workspace_edit--- helix-lsp/src/client.rs | 27 +++++++ helix-term/src/commands.rs | 188 ++++++++++++++++++++++++++++++++++++--------- helix-term/src/keymap.rs | 1 + helix-view/src/editor.rs | 6 ++ 4 files changed, 187 insertions(+), 35 deletions(-) (limited to 'helix-term/src/commands.rs') diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs index b810feef..271fd9d5 100644 --- a/helix-lsp/src/client.rs +++ b/helix-lsp/src/client.rs @@ -257,6 +257,12 @@ impl Client { content_format: Some(vec![lsp::MarkupKind::Markdown]), ..Default::default() }), + rename: Some(lsp::RenameClientCapabilities { + dynamic_registration: Some(false), + prepare_support: Some(false), + prepare_support_default_behavior: None, + honors_change_annotations: Some(false), + }), code_action: Some(lsp::CodeActionClientCapabilities { code_action_literal_support: Some(lsp::CodeActionLiteralSupport { code_action_kind: lsp::CodeActionKindLiteralSupport { @@ -773,4 +779,25 @@ impl Client { self.call::(params) } + + pub async fn rename_symbol( + &self, + text_document: lsp::TextDocumentIdentifier, + position: lsp::Position, + new_name: String, + ) -> anyhow::Result { + let params = lsp::RenameParams { + text_document_position: lsp::TextDocumentPositionParams { + text_document, + position, + }, + new_name, + work_done_progress_params: lsp::WorkDoneProgressParams { + work_done_token: None, + }, + }; + + let response = self.request::(params).await?; + Ok(response.unwrap_or_default()) + } } diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 5f091775..245fbe4e 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -23,7 +23,7 @@ use helix_view::{ use anyhow::{anyhow, bail, Context as _}; use helix_lsp::{ - lsp, + block_on, lsp, util::{lsp_pos_to_pos, lsp_range_to_range, pos_to_lsp_pos, range_to_lsp_range}, OffsetEncoding, }; @@ -339,6 +339,7 @@ impl Command { shell_append_output, "Append output of shell command after each selection", shell_keep_pipe, "Filter selections with shell predicate", suspend, "Suspend", + rename_symbol, "Rename symbol", ); } @@ -2749,14 +2750,104 @@ pub fn code_action(cx: &mut Context) { ) } +pub fn apply_document_resource_op(op: &lsp::ResourceOp) -> std::io::Result<()> { + use lsp::ResourceOp; + use std::fs; + match op { + ResourceOp::Create(op) => { + let path = op.uri.to_file_path().unwrap(); + let ignore_if_exists = if let Some(options) = &op.options { + !options.overwrite.unwrap_or(false) && options.ignore_if_exists.unwrap_or(false) + } else { + false + }; + if ignore_if_exists && path.exists() { + Ok(()) + } else { + fs::write(&path, []) + } + } + ResourceOp::Delete(op) => { + let path = op.uri.to_file_path().unwrap(); + if path.is_dir() { + let recursive = if let Some(options) = &op.options { + options.recursive.unwrap_or(false) + } else { + false + }; + if recursive { + fs::remove_dir_all(&path) + } else { + fs::remove_dir(&path) + } + } else if path.is_file() { + fs::remove_file(&path) + } else { + Ok(()) + } + } + ResourceOp::Rename(op) => { + let from = op.old_uri.to_file_path().unwrap(); + let to = op.new_uri.to_file_path().unwrap(); + let ignore_if_exists = if let Some(options) = &op.options { + !options.overwrite.unwrap_or(false) && options.ignore_if_exists.unwrap_or(false) + } else { + false + }; + if ignore_if_exists && to.exists() { + Ok(()) + } else { + fs::rename(&from, &to) + } + } + } +} + fn apply_workspace_edit( editor: &mut Editor, offset_encoding: OffsetEncoding, workspace_edit: &lsp::WorkspaceEdit, ) { + let mut apply_edits = |uri: &helix_lsp::Url, text_edits: Vec| { + let path = uri + .to_file_path() + .expect("unable to convert URI to filepath"); + + let current_view_id = view!(editor).id; + let doc_id = editor.open(path, Action::Load).unwrap(); + let doc = editor + .document_mut(doc_id) + .expect("Document for document_changes not found"); + + // Need to determine a view for apply/append_changes_to_history + let selections = doc.selections(); + let view_id = if selections.contains_key(¤t_view_id) { + // use current if possible + current_view_id + } else { + // Hack: we take the first available view_id + selections + .keys() + .next() + .copied() + .expect("No view_id available") + }; + + let transaction = helix_lsp::util::generate_transaction_from_edits( + doc.text(), + text_edits, + offset_encoding, + ); + doc.apply(&transaction, view_id); + doc.append_changes_to_history(view_id); + }; + if let Some(ref changes) = workspace_edit.changes { log::debug!("workspace changes: {:?}", changes); - editor.set_error(String::from("Handling workspace_edit.changes is not implemented yet, see https://github.com/helix-editor/helix/issues/183")); + for (uri, text_edits) in changes { + let text_edits = text_edits.to_vec(); + apply_edits(uri, text_edits); + } return; // Not sure if it works properly, it'll be safer to just panic here to avoid breaking some parts of code on which code actions will be used // TODO: find some example that uses workspace changes, and test it @@ -2774,30 +2865,6 @@ fn apply_workspace_edit( match document_changes { lsp::DocumentChanges::Edits(document_edits) => { for document_edit in document_edits { - let path = document_edit - .text_document - .uri - .to_file_path() - .expect("unable to convert URI to filepath"); - let current_view_id = view!(editor).id; - let doc = editor - .document_by_path_mut(path) - .expect("Document for document_changes not found"); - - // Need to determine a view for apply/append_changes_to_history - let selections = doc.selections(); - let view_id = if selections.contains_key(¤t_view_id) { - // use current if possible - current_view_id - } else { - // Hack: we take the first available view_id - selections - .keys() - .next() - .copied() - .expect("No view_id available") - }; - let edits = document_edit .edits .iter() @@ -2809,19 +2876,33 @@ fn apply_workspace_edit( }) .cloned() .collect(); - - let transaction = helix_lsp::util::generate_transaction_from_edits( - doc.text(), - edits, - offset_encoding, - ); - doc.apply(&transaction, view_id); - doc.append_changes_to_history(view_id); + apply_edits(&document_edit.text_document.uri, edits); } } lsp::DocumentChanges::Operations(operations) => { log::debug!("document changes - operations: {:?}", operations); - editor.set_error(String::from("Handling document operations is not implemented yet, see https://github.com/helix-editor/helix/issues/183")); + for operateion in operations { + match operateion { + lsp::DocumentChangeOperation::Op(op) => { + apply_document_resource_op(op).unwrap(); + } + + lsp::DocumentChangeOperation::Edit(document_edit) => { + let edits = document_edit + .edits + .iter() + .map(|edit| match edit { + lsp::OneOf::Left(text_edit) => text_edit, + lsp::OneOf::Right(annotated_text_edit) => { + &annotated_text_edit.text_edit + } + }) + .cloned() + .collect(); + apply_edits(&document_edit.text_document.uri, edits); + } + } + } } } } @@ -4982,3 +5063,40 @@ fn add_newline_impl(cx: &mut Context, open: Open) { doc.apply(&transaction, view.id); doc.append_changes_to_history(view.id); } + +fn rename_symbol(cx: &mut Context) { + let prompt = Prompt::new( + "Rename to: ".into(), + None, + |_input: &str| Vec::new(), + move |cx: &mut compositor::Context, input: &str, event: PromptEvent| { + if event != PromptEvent::Validate { + return; + } + + log::debug!("renaming to: {:?}", input); + + let (view, doc) = current!(cx.editor); + let language_server = match doc.language_server() { + Some(language_server) => language_server, + None => return, + }; + + let offset_encoding = language_server.offset_encoding(); + + let pos = pos_to_lsp_pos( + doc.text(), + doc.selection(view.id) + .primary() + .cursor(doc.text().slice(..)), + offset_encoding, + ); + + let task = language_server.rename_symbol(doc.identifier(), pos, input.to_string()); + let edits = block_on(task).unwrap_or_default(); + log::debug!("Edits from LSP: {:?}", edits); + apply_workspace_edit(&mut cx.editor, offset_encoding, &edits); + }, + ); + cx.push_layer(Box::new(prompt)); +} diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index ce50f0ab..d497401f 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -584,6 +584,7 @@ impl Default for Keymaps { "R" => replace_selections_with_clipboard, "/" => global_search, "k" => hover, + "r" => rename_symbol, }, "z" => { "View" "z" | "c" => align_view_center, diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 17cd3d7b..631dcf0c 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -287,6 +287,12 @@ impl Editor { return; } Action::Load => { + let view_id = view!(self).id; + if let Some(doc) = self.document_mut(id) { + if doc.selections().is_empty() { + doc.selections.insert(view_id, Selection::point(0)); + } + } return; } Action::HorizontalSplit => { -- cgit v1.2.3-70-g09d2 From a69caff450ff8201e16d0a0b4617114e03ed3c97 Mon Sep 17 00:00:00 2001 From: CossonLeo Date: Tue, 9 Nov 2021 10:11:45 +0800 Subject: search_impl will only align cursor center when it isn't in view (#959) --- helix-term/src/commands.rs | 22 +++++++++++++++++++--- helix-view/src/view.rs | 41 +++++++++++++++++++++++++++++++++-------- 2 files changed, 52 insertions(+), 11 deletions(-) (limited to 'helix-term/src/commands.rs') diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 245fbe4e..42163b8e 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1175,6 +1175,7 @@ fn search_impl( regex: &Regex, movement: Movement, direction: Direction, + scrolloff: usize, ) { let text = doc.text().slice(..); let selection = doc.selection(view.id); @@ -1233,7 +1234,11 @@ fn search_impl( }; doc.set_selection(view.id, selection); - align_view(doc, view, Align::Center); + if view.is_cursor_in_view(doc, 0) { + view.ensure_cursor_in_view(doc, scrolloff); + } else { + align_view(doc, view, Align::Center) + } }; } @@ -1257,6 +1262,8 @@ fn rsearch(cx: &mut Context) { // TODO: use one function for search vs extend fn searcher(cx: &mut Context, direction: Direction) { let reg = cx.register.unwrap_or('/'); + let scrolloff = cx.editor.config.scrolloff; + let (_, doc) = current!(cx.editor); // TODO: could probably share with select_on_matches? @@ -1281,7 +1288,15 @@ fn searcher(cx: &mut Context, direction: Direction) { if event != PromptEvent::Update { return; } - search_impl(doc, view, &contents, ®ex, Movement::Move, direction); + search_impl( + doc, + view, + &contents, + ®ex, + Movement::Move, + direction, + scrolloff, + ); }, ); @@ -1289,6 +1304,7 @@ fn searcher(cx: &mut Context, direction: Direction) { } fn search_next_or_prev_impl(cx: &mut Context, movement: Movement, direction: Direction) { + let scrolloff = cx.editor.config.scrolloff; let (view, doc) = current!(cx.editor); let registers = &cx.editor.registers; if let Some(query) = registers.read('/') { @@ -1303,7 +1319,7 @@ fn search_next_or_prev_impl(cx: &mut Context, movement: Movement, direction: Dir .case_insensitive(case_insensitive) .build() { - search_impl(doc, view, &contents, ®ex, movement, direction); + search_impl(doc, view, &contents, ®ex, movement, direction, scrolloff); } else { // get around warning `mutable_borrow_reservation_conflict` // which will be a hard error in the future diff --git a/helix-view/src/view.rs b/helix-view/src/view.rs index 11f30155..6a624ded 100644 --- a/helix-view/src/view.rs +++ b/helix-view/src/view.rs @@ -85,7 +85,12 @@ impl View { self.area.clip_left(OFFSET).clip_bottom(1) // -1 for statusline } - pub fn ensure_cursor_in_view(&mut self, doc: &Document, scrolloff: usize) { + // + pub fn offset_coords_to_in_view( + &self, + doc: &Document, + scrolloff: usize, + ) -> Option<(usize, usize)> { let cursor = doc .selection(self.id) .primary() @@ -104,23 +109,43 @@ impl View { let last_col = self.offset.col + inner_area.width.saturating_sub(1) as usize; - if line > last_line.saturating_sub(scrolloff) { + let row = if line > last_line.saturating_sub(scrolloff) { // scroll down - self.offset.row += line - (last_line.saturating_sub(scrolloff)); + self.offset.row + line - (last_line.saturating_sub(scrolloff)) } else if line < self.offset.row + scrolloff { // scroll up - self.offset.row = line.saturating_sub(scrolloff); - } + line.saturating_sub(scrolloff) + } else { + self.offset.row + }; - if col > last_col.saturating_sub(scrolloff) { + let col = if col > last_col.saturating_sub(scrolloff) { // scroll right - self.offset.col += col - (last_col.saturating_sub(scrolloff)); + self.offset.col + col - (last_col.saturating_sub(scrolloff)) } else if col < self.offset.col + scrolloff { // scroll left - self.offset.col = col.saturating_sub(scrolloff); + col.saturating_sub(scrolloff) + } else { + self.offset.col + }; + if row == self.offset.row && col == self.offset.col { + None + } else { + Some((row, col)) } } + pub fn ensure_cursor_in_view(&mut self, doc: &Document, scrolloff: usize) { + if let Some((row, col)) = self.offset_coords_to_in_view(doc, scrolloff) { + self.offset.row = row; + self.offset.col = col; + } + } + + pub fn is_cursor_in_view(&mut self, doc: &Document, scrolloff: usize) -> bool { + self.offset_coords_to_in_view(doc, scrolloff).is_none() + } + /// Calculates the last visible line on screen #[inline] pub fn last_line(&self, doc: &Document) -> usize { -- cgit v1.2.3-70-g09d2 From 68224232af1126daa5b043a096269fbd6cb53551 Mon Sep 17 00:00:00 2001 From: CossonLeo Date: Wed, 10 Nov 2021 09:52:39 +0800 Subject: buffer picker add is_modifier flag (#1020) --- helix-term/src/commands.rs | 68 +++++++++++++++++++++++++++++++++------------- 1 file changed, 49 insertions(+), 19 deletions(-) (limited to 'helix-term/src/commands.rs') diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 42163b8e..489308d8 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -2600,36 +2600,66 @@ fn file_picker(cx: &mut Context) { fn buffer_picker(cx: &mut Context) { let current = view!(cx.editor).doc; + struct BufferMeta { + id: DocumentId, + path: Option, + is_modified: bool, + is_current: bool, + } + + impl BufferMeta { + fn format(&self) -> Cow { + let path = self + .path + .as_deref() + .map(helix_core::path::get_relative_path); + let path = match path.as_deref().and_then(Path::to_str) { + Some(path) => path, + None => return Cow::Borrowed("[scratch buffer]"), + }; + + let mut flags = Vec::new(); + if self.is_modified { + flags.push("+"); + } + if self.is_current { + flags.push("*"); + } + + let flag = if flags.is_empty() { + "".into() + } else { + format!(" ({})", flags.join("")) + }; + Cow::Owned(format!("{}{}", path, flag)) + } + } + + let new_meta = |doc: &Document| BufferMeta { + id: doc.id(), + path: doc.path().cloned(), + is_modified: doc.is_modified(), + is_current: doc.id() == current, + }; + let picker = FilePicker::new( cx.editor .documents .iter() - .map(|(id, doc)| (*id, doc.path().cloned())) + .map(|(_, doc)| new_meta(doc)) .collect(), - move |(id, path): &(DocumentId, Option)| { - let path = path.as_deref().map(helix_core::path::get_relative_path); - match path.as_ref().and_then(|path| path.to_str()) { - Some(path) => { - if *id == current { - format!("{} (*)", &path).into() - } else { - path.to_owned().into() - } - } - None => "[scratch buffer]".into(), - } - }, - |editor: &mut Editor, (id, _path): &(DocumentId, Option), _action| { - editor.switch(*id, Action::Replace); + BufferMeta::format, + |editor: &mut Editor, meta, _action| { + editor.switch(meta.id, Action::Replace); }, - |editor, (id, path)| { - let doc = &editor.documents.get(id)?; + |editor, meta| { + let doc = &editor.documents.get(&meta.id)?; let &view_id = doc.selections().keys().next()?; let line = doc .selection(view_id) .primary() .cursor_line(doc.text().slice(..)); - Some((path.clone()?, Some((line, line)))) + Some((meta.path.clone()?, Some((line, line)))) }, ); cx.push_layer(Box::new(picker)); -- cgit v1.2.3-70-g09d2 From 4d22454386d52d14f626b209016a71f119cc1cbf Mon Sep 17 00:00:00 2001 From: Bob Date: Thu, 11 Nov 2021 10:32:23 +0800 Subject: add wonly -- window only (#1057) * add wonly * Update book/src/keymap.md Co-authored-by: Blaž Hrastnik * add `wonly` to space w mode too * remove the TODO Co-authored-by: Blaž Hrastnik --- book/src/keymap.md | 1 + helix-term/src/commands.rs | 15 +++++++++++++++ helix-term/src/keymap.rs | 2 ++ 3 files changed, 18 insertions(+) (limited to 'helix-term/src/commands.rs') diff --git a/book/src/keymap.md b/book/src/keymap.md index 212ed496..901c8471 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -194,6 +194,7 @@ This layer is similar to vim keybindings as kakoune does not support window. | `k`, `Ctrl-k`, `up` | Move to split above | `jump_view_up` | | `l`, `Ctrl-l`, `right` | Move to right split | `jump_view_right` | | `q`, `Ctrl-q` | Close current window | `wclose` | +| `o`, `Ctrl-o` | Only keep the current window, closing all the others | `wonly` | #### Space mode diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 489308d8..089f92f1 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -320,6 +320,7 @@ impl Command { hsplit, "Horizontal bottom split", vsplit, "Vertical right split", wclose, "Close window", + wonly, "Current window only", select_register, "Select register", align_view_middle, "Align view middle", align_view_top, "Align view top", @@ -4723,6 +4724,20 @@ fn wclose(cx: &mut Context) { cx.editor.close(view_id, /* close_buffer */ false); } +fn wonly(cx: &mut Context) { + let views = cx + .editor + .tree + .views() + .map(|(v, focus)| (v.id, focus)) + .collect::>(); + for (view_id, focus) in views { + if !focus { + cx.editor.close(view_id, /* close_buffer */ false); + } + } +} + fn select_register(cx: &mut Context) { cx.on_next_key(move |cx, event| { if let Some(ch) = event.char() { diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 7bed3ddb..d7040b88 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -583,6 +583,7 @@ impl Default for Keymaps { "C-s" | "s" => hsplit, "C-v" | "v" => vsplit, "C-q" | "q" => wclose, + "C-o" | "o" => wonly, "C-h" | "h" | "left" => jump_view_left, "C-j" | "j" | "down" => jump_view_down, "C-k" | "k" | "up" => jump_view_up, @@ -609,6 +610,7 @@ impl Default for Keymaps { "C-s" | "s" => hsplit, "C-v" | "v" => vsplit, "C-q" | "q" => wclose, + "C-o" | "o" => wonly, "C-h" | "h" | "left" => jump_view_left, "C-j" | "j" | "down" => jump_view_down, "C-k" | "k" | "up" => jump_view_up, -- cgit v1.2.3-70-g09d2 From ebc14d9d206a0960a97fd706bd8fbfbedf2fb203 Mon Sep 17 00:00:00 2001 From: Omnikar Date: Wed, 10 Nov 2021 21:33:31 -0500 Subject: Add `m` textobject for pair under cursor (#961) --- helix-term/src/commands.rs | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'helix-term/src/commands.rs') diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 089f92f1..24c38dd5 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -4819,6 +4819,14 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) { 'c' => textobject_treesitter("class", range), 'f' => textobject_treesitter("function", range), 'p' => textobject_treesitter("parameter", range), + 'm' => { + let ch = text.char(range.cursor(text)); + if !ch.is_ascii_alphanumeric() { + textobject::textobject_surround(text, range, objtype, ch, count) + } else { + range + } + } // TODO: cancel new ranges if inconsistent surround matches across lines ch if !ch.is_ascii_alphanumeric() => { textobject::textobject_surround(text, range, objtype, ch, count) -- cgit v1.2.3-70-g09d2 From bf70cfd050d961ce7b8d5a95fe663dff9eb1e66e Mon Sep 17 00:00:00 2001 From: Cole Helbling Date: Wed, 10 Nov 2021 19:22:15 -0800 Subject: helix-term/command: make scratch buffer name consistent (#1071) --- helix-term/src/commands.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'helix-term/src/commands.rs') diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 24c38dd5..23b385eb 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -53,6 +53,8 @@ use grep_searcher::{sinks, BinaryDetection, SearcherBuilder}; use ignore::{DirEntry, WalkBuilder, WalkState}; use tokio_stream::wrappers::UnboundedReceiverStream; +pub const SCRATCH_BUFFER_NAME: &str = "[scratch]"; + pub struct Context<'a> { pub register: Option, pub count: Option, @@ -1890,7 +1892,7 @@ mod cmd { .map(|doc| { doc.relative_path() .map(|path| path.to_string_lossy().to_string()) - .unwrap_or_else(|| "[scratch]".into()) + .unwrap_or_else(|| SCRATCH_BUFFER_NAME.into()) }) .collect(); if !modified.is_empty() { @@ -2616,7 +2618,7 @@ fn buffer_picker(cx: &mut Context) { .map(helix_core::path::get_relative_path); let path = match path.as_deref().and_then(Path::to_str) { Some(path) => path, - None => return Cow::Borrowed("[scratch buffer]"), + None => return Cow::Borrowed(SCRATCH_BUFFER_NAME), }; let mut flags = Vec::new(); -- cgit v1.2.3-70-g09d2 From 9d591427be900b7a43fc7e13dd86f31199e8c00e Mon Sep 17 00:00:00 2001 From: Ivan Tham Date: Thu, 11 Nov 2021 21:32:44 +0800 Subject: Fix earlier/later missing changeset update (#1069) Fix #1059--- helix-term/src/commands.rs | 10 ++++++++-- helix-view/src/document.rs | 24 ++++++++++++++++++++---- 2 files changed, 28 insertions(+), 6 deletions(-) (limited to 'helix-term/src/commands.rs') diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 23b385eb..738621b0 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1845,7 +1845,10 @@ mod cmd { .map_err(|s| anyhow!(s))?; let (view, doc) = current!(cx.editor); - doc.earlier(view.id, uk); + let success = doc.earlier(view.id, uk); + if !success { + cx.editor.set_status("Already at oldest change".to_owned()); + } Ok(()) } @@ -1860,7 +1863,10 @@ mod cmd { .parse::() .map_err(|s| anyhow!(s))?; let (view, doc) = current!(cx.editor); - doc.later(view.id, uk); + let success = doc.later(view.id, uk); + if !success { + cx.editor.set_status("Already at newest change".to_owned()); + } Ok(()) } diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 351ad05a..80f6a740 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -751,19 +751,35 @@ impl Document { } /// Undo modifications to the [`Document`] according to `uk`. - pub fn earlier(&mut self, view_id: ViewId, uk: helix_core::history::UndoKind) { + pub fn earlier(&mut self, view_id: ViewId, uk: helix_core::history::UndoKind) -> bool { let txns = self.history.get_mut().earlier(uk); + let mut success = false; for txn in txns { - self.apply_impl(&txn, view_id); + if self.apply_impl(&txn, view_id) { + success = true; + } + } + if success { + // reset changeset to fix len + self.changes = ChangeSet::new(self.text()); } + success } /// Redo modifications to the [`Document`] according to `uk`. - pub fn later(&mut self, view_id: ViewId, uk: helix_core::history::UndoKind) { + pub fn later(&mut self, view_id: ViewId, uk: helix_core::history::UndoKind) -> bool { let txns = self.history.get_mut().later(uk); + let mut success = false; for txn in txns { - self.apply_impl(&txn, view_id); + if self.apply_impl(&txn, view_id) { + success = true; + } + } + if success { + // reset changeset to fix len + self.changes = ChangeSet::new(self.text()); } + success } /// Commit pending changes to history -- cgit v1.2.3-70-g09d2 From bf95a9ed043242d95e431412e45e218d40a5695a Mon Sep 17 00:00:00 2001 From: Omnikar Date: Thu, 11 Nov 2021 19:34:08 -0500 Subject: Add `remove_selections` command (#1065) * Add `remove_selections` command * Document `remove_selections` * Update helix-term/src/keymap.rs Co-authored-by: Blaž Hrastnik --- book/src/keymap.md | 1 + helix-core/src/selection.rs | 5 +++-- helix-term/src/commands.rs | 19 +++++++++++++++---- helix-term/src/keymap.rs | 2 +- 4 files changed, 20 insertions(+), 7 deletions(-) (limited to 'helix-term/src/commands.rs') diff --git a/book/src/keymap.md b/book/src/keymap.md index 901c8471..7a9a4378 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -101,6 +101,7 @@ | | Expand selection to parent syntax node TODO: pick a key | `expand_selection` | | `J` | Join lines inside selection | `join_selections` | | `K` | Keep selections matching the regex | `keep_selections` | +| `Alt-K` | Remove selections matching the regex | `remove_selections` | | `$` | Pipe each selection into shell command, keep selections where command returned 0 | `shell_keep_pipe` | | `Ctrl-c` | Comment/uncomment the selections | `toggle_comments` | diff --git a/helix-core/src/selection.rs b/helix-core/src/selection.rs index f3b5d2c8..f7c7dbcb 100644 --- a/helix-core/src/selection.rs +++ b/helix-core/src/selection.rs @@ -528,14 +528,15 @@ impl<'a> IntoIterator for &'a Selection { // TODO: checkSelection -> check if valid for doc length && sorted -pub fn keep_matches( +pub fn keep_or_remove_matches( text: RopeSlice, selection: &Selection, regex: &crate::regex::Regex, + remove: bool, ) -> Option { let result: SmallVec<_> = selection .iter() - .filter(|range| regex.is_match(&range.fragment(text))) + .filter(|range| regex.is_match(&range.fragment(text)) ^ remove) .copied() .collect(); diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 738621b0..4352ee66 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -302,6 +302,7 @@ impl Command { format_selections, "Format selection", join_selections, "Join lines inside selection", keep_selections, "Keep selections matching regex", + remove_selections, "Remove selections matching regex", keep_primary_selection, "Keep primary selection", remove_primary_selection, "Remove primary selection", completion, "Invoke completion popup", @@ -4320,12 +4321,12 @@ fn join_selections(cx: &mut Context) { doc.append_changes_to_history(view.id); } -fn keep_selections(cx: &mut Context) { - // keep selections matching regex +fn keep_or_remove_selections_impl(cx: &mut Context, remove: bool) { + // keep or remove selections matching regex let reg = cx.register.unwrap_or('/'); let prompt = ui::regex_prompt( cx, - "keep:".into(), + if !remove { "keep:" } else { "remove:" }.into(), Some(reg), |_input: &str| Vec::new(), move |view, doc, regex, event| { @@ -4334,7 +4335,9 @@ fn keep_selections(cx: &mut Context) { } let text = doc.text().slice(..); - if let Some(selection) = selection::keep_matches(text, doc.selection(view.id), ®ex) { + if let Some(selection) = + selection::keep_or_remove_matches(text, doc.selection(view.id), ®ex, remove) + { doc.set_selection(view.id, selection); } }, @@ -4343,6 +4346,14 @@ fn keep_selections(cx: &mut Context) { cx.push_layer(Box::new(prompt)); } +fn keep_selections(cx: &mut Context) { + keep_or_remove_selections_impl(cx, false) +} + +fn remove_selections(cx: &mut Context) { + keep_or_remove_selections_impl(cx, true) +} + fn keep_primary_selection(cx: &mut Context) { let (view, doc) = current!(cx.editor); // TODO: handle count diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index b2b865e4..f79978fb 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -577,7 +577,7 @@ impl Default for Keymaps { "=" => format_selections, "J" => join_selections, "K" => keep_selections, - // TODO: and another method for inverse + "A-K" => remove_selections, "," => keep_primary_selection, "A-," => remove_primary_selection, -- cgit v1.2.3-70-g09d2 From 6d4409c00ffd35fbb3f92d627a21845b931c609b Mon Sep 17 00:00:00 2001 From: Omnikar Date: Fri, 12 Nov 2021 11:34:49 -0500 Subject: Make prompts consistent (#1080) --- helix-term/src/commands.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'helix-term/src/commands.rs') diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 4352ee66..48fd0ee0 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1367,7 +1367,7 @@ fn global_search(cx: &mut Context) { let completions = search_completions(cx, None); let prompt = ui::regex_prompt( cx, - "global search:".into(), + "global-search:".into(), None, move |input: &str| { completions @@ -5154,7 +5154,7 @@ fn add_newline_impl(cx: &mut Context, open: Open) { fn rename_symbol(cx: &mut Context) { let prompt = Prompt::new( - "Rename to: ".into(), + "rename-to:".into(), None, |_input: &str| Vec::new(), move |cx: &mut compositor::Context, input: &str, event: PromptEvent| { -- cgit v1.2.3-70-g09d2 From b824e091a948c076a428fb981cd5be2929378533 Mon Sep 17 00:00:00 2001 From: Cole Helbling Date: Fri, 12 Nov 2021 20:15:41 -0800 Subject: helix-term/commands: move SCRATCH_BUFFER_NAME to helix-view/document (#1091) This way, the name is accessible everywhere `Document` and related types are.--- helix-term/src/commands.rs | 4 +--- helix-view/src/document.rs | 2 ++ 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'helix-term/src/commands.rs') diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 48fd0ee0..d5a48c5f 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -13,7 +13,7 @@ use helix_core::{ use helix_view::{ clipboard::ClipboardType, - document::Mode, + document::{Mode, SCRATCH_BUFFER_NAME}, editor::{Action, Motion}, input::KeyEvent, keyboard::KeyCode, @@ -53,8 +53,6 @@ use grep_searcher::{sinks, BinaryDetection, SearcherBuilder}; use ignore::{DirEntry, WalkBuilder, WalkState}; use tokio_stream::wrappers::UnboundedReceiverStream; -pub const SCRATCH_BUFFER_NAME: &str = "[scratch]"; - pub struct Context<'a> { pub register: Option, pub count: Option, diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 80f6a740..6b429151 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -25,6 +25,8 @@ const BUF_SIZE: usize = 8192; const DEFAULT_INDENT: IndentStyle = IndentStyle::Spaces(4); +pub const SCRATCH_BUFFER_NAME: &str = "[scratch]"; + #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub enum Mode { Normal, -- cgit v1.2.3-70-g09d2 From 35c974c9c49f9127da3798c9a8e49795b3c4aadc Mon Sep 17 00:00:00 2001 From: ath3 Date: Sun, 14 Nov 2021 16:11:53 +0100 Subject: Implement "Goto last modification" command (#1067) --- book/src/keymap.md | 1 + helix-core/src/history.rs | 30 +++++++++++++++++++++++++++++- helix-term/src/commands.rs | 14 ++++++++++++++ helix-term/src/keymap.rs | 1 + helix-term/src/ui/editor.rs | 2 +- helix-view/src/document.rs | 2 +- 6 files changed, 47 insertions(+), 3 deletions(-) (limited to 'helix-term/src/commands.rs') diff --git a/book/src/keymap.md b/book/src/keymap.md index c544a472..6155e553 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -165,6 +165,7 @@ Jumps to various locations. | `a` | Go to the last accessed/alternate file | `goto_last_accessed_file` | | `n` | Go to next buffer | `goto_next_buffer` | | `p` | Go to previous buffer | `goto_previous_buffer` | +| `.` | Go to last modification in current file | `goto_last_modification` | #### Match mode diff --git a/helix-core/src/history.rs b/helix-core/src/history.rs index b53c01fe..bf2624e2 100644 --- a/helix-core/src/history.rs +++ b/helix-core/src/history.rs @@ -1,4 +1,4 @@ -use crate::{ChangeSet, Rope, State, Transaction}; +use crate::{Assoc, ChangeSet, Rope, State, Transaction}; use once_cell::sync::Lazy; use regex::Regex; use std::num::NonZeroUsize; @@ -133,6 +133,34 @@ impl History { Some(&self.revisions[last_child.get()].transaction) } + // Get the position of last change + pub fn last_edit_pos(&self) -> Option { + if self.current == 0 { + return None; + } + let current_revision = &self.revisions[self.current]; + let primary_selection = current_revision + .inversion + .selection() + .expect("inversion always contains a selection") + .primary(); + let (_from, to, _fragment) = current_revision + .transaction + .changes_iter() + // find a change that matches the primary selection + .find(|(from, to, _fragment)| { + crate::Range::new(*from, *to).overlaps(&primary_selection) + }) + // or use the first change + .or_else(|| current_revision.transaction.changes_iter().next()) + .unwrap(); + let pos = current_revision + .transaction + .changes() + .map_pos(to, Assoc::After); + Some(pos) + } + fn lowest_common_ancestor(&self, mut a: usize, mut b: usize) -> usize { use std::collections::HashSet; let mut a_path_set = HashSet::new(); diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index d5a48c5f..e37265a8 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -257,6 +257,7 @@ impl Command { goto_window_middle, "Goto window middle", goto_window_bottom, "Goto window bottom", goto_last_accessed_file, "Goto last accessed file", + goto_last_modification, "Goto last modification", goto_line, "Goto line", goto_last_line, "Goto last line", goto_first_diag, "Goto first diagnostic", @@ -3195,6 +3196,19 @@ fn goto_last_accessed_file(cx: &mut Context) { } } +fn goto_last_modification(cx: &mut Context) { + let (view, doc) = current!(cx.editor); + let pos = doc.history.get_mut().last_edit_pos(); + let text = doc.text().slice(..); + if let Some(pos) = pos { + let selection = doc + .selection(view.id) + .clone() + .transform(|range| range.put_cursor(text, pos, doc.mode == Mode::Select)); + doc.set_selection(view.id, selection); + } +} + fn select_mode(cx: &mut Context) { let (view, doc) = current!(cx.editor); let text = doc.text().slice(..); diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index e3e01995..b14b1a6f 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -525,6 +525,7 @@ impl Default for Keymaps { "a" => goto_last_accessed_file, "n" => goto_next_buffer, "p" => goto_previous_buffer, + "." => goto_last_modification, }, ":" => command_mode, diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index dcf87203..bcd9f8f0 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -743,7 +743,7 @@ impl EditorView { std::num::NonZeroUsize::new(cxt.editor.count.map_or(i, |c| c.get() * 10 + i)); } // special handling for repeat operator - key!('.') => { + key!('.') if self.keymaps.pending().is_empty() => { // first execute whatever put us into insert mode self.last_insert.0.execute(cxt); // then replay the inputs diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 6b429151..76b19a07 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -98,7 +98,7 @@ pub struct Document { // It can be used as a cell where we will take it out to get some parts of the history and put // it back as it separated from the edits. We could split out the parts manually but that will // be more troublesome. - history: Cell, + pub history: Cell, pub savepoint: Option, -- cgit v1.2.3-70-g09d2 From edc976b6bb36c6017bf59691abbde5c086267bfd Mon Sep 17 00:00:00 2001 From: Ebbe Steenhoudt Date: Sun, 14 Nov 2021 16:12:56 +0100 Subject: Added workspace_symbol_picker (#1041) * Added workspace_symbol_picker * Moved truncation of the symbol pickers to the end. * Fixed typo--- book/src/keymap.md | 1 + helix-term/src/commands.rs | 66 ++++++++++++++++++++++++++++++++++++++++++++- helix-term/src/keymap.rs | 1 + helix-term/src/ui/picker.rs | 8 +++++- 4 files changed, 74 insertions(+), 2 deletions(-) (limited to 'helix-term/src/commands.rs') diff --git a/book/src/keymap.md b/book/src/keymap.md index 6155e553..9f1714f6 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -211,6 +211,7 @@ This layer is a kludge of mappings, mostly pickers. | `b` | Open buffer picker | `buffer_picker` | | `k` | Show documentation for item under cursor (**LSP**) | `hover` | | `s` | Open document symbol picker (**LSP**) | `symbol_picker` | +| `S` | Open workspace symbol picker (**LSP**) | `workspace_symbol_picker` | | `r` | Rename symbol (**LSP**) | `rename_symbol` | | `a` | Apply code action (**LSP**) | `code_action` | | `'` | Open last fuzzy picker | `last_picker` | diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index e37265a8..ebacb377 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -237,6 +237,7 @@ impl Command { code_action, "Perform code action", buffer_picker, "Open buffer picker", symbol_picker, "Open symbol picker", + workspace_symbol_picker, "Open workspace symbol picker", last_picker, "Open last picker", prepend_to_line, "Insert at start of line", append_to_line, "Insert at end of line", @@ -2723,7 +2724,7 @@ fn symbol_picker(cx: &mut Context) { } }; - let picker = FilePicker::new( + let mut picker = FilePicker::new( symbols, |symbol| (&symbol.name).into(), move |editor: &mut Editor, symbol, _action| { @@ -2748,6 +2749,69 @@ fn symbol_picker(cx: &mut Context) { Some((path, line)) }, ); + picker.truncate_start = false; + compositor.push(Box::new(picker)) + } + }, + ) +} + +fn workspace_symbol_picker(cx: &mut Context) { + let (_, doc) = current!(cx.editor); + + let language_server = match doc.language_server() { + Some(language_server) => language_server, + None => return, + }; + let offset_encoding = language_server.offset_encoding(); + + let future = language_server.workspace_symbols("".to_string()); + + let current_path = doc_mut!(cx.editor).path().cloned(); + cx.callback( + future, + move |_editor: &mut Editor, + compositor: &mut Compositor, + response: Option>| { + if let Some(symbols) = response { + let mut picker = FilePicker::new( + symbols, + move |symbol| { + let path = symbol.location.uri.to_file_path().unwrap(); + if current_path.as_ref().map(|p| p == &path).unwrap_or(false) { + (&symbol.name).into() + } else { + let relative_path = helix_core::path::get_relative_path(path.as_path()) + .to_str() + .unwrap() + .to_owned(); + format!("{} ({})", &symbol.name, relative_path).into() + } + }, + move |editor: &mut Editor, symbol, action| { + let path = symbol.location.uri.to_file_path().unwrap(); + editor.open(path, action).expect("editor.open failed"); + let (view, doc) = current!(editor); + + if let Some(range) = + lsp_range_to_range(doc.text(), symbol.location.range, offset_encoding) + { + // we flip the range so that the cursor sits on the start of the symbol + // (for example start of the function). + doc.set_selection(view.id, Selection::single(range.head, range.anchor)); + align_view(doc, view, Align::Center); + } + }, + move |_editor, symbol| { + let path = symbol.location.uri.to_file_path().unwrap(); + let line = Some(( + symbol.location.range.start.line as usize, + symbol.location.range.end.line as usize, + )); + Some((path, line)) + }, + ); + picker.truncate_start = false; compositor.push(Box::new(picker)) } }, diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index b14b1a6f..3280f0f8 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -641,6 +641,7 @@ impl Default for Keymaps { "f" => file_picker, "b" => buffer_picker, "s" => symbol_picker, + "S" => workspace_symbol_picker, "a" => code_action, "'" => last_picker, "w" => { "Window" diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index c44b7625..6b1c5832 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -37,6 +37,7 @@ type FileLocation = (PathBuf, Option<(usize, usize)>); pub struct FilePicker { picker: Picker, + pub truncate_start: bool, /// Caches paths to documents preview_cache: HashMap, read_buffer: Vec, @@ -90,6 +91,7 @@ impl FilePicker { ) -> Self { Self { picker: Picker::new(false, options, format_fn, callback_fn), + truncate_start: true, preview_cache: HashMap::new(), read_buffer: Vec::with_capacity(1024), file_fn: Box::new(preview_fn), @@ -172,6 +174,7 @@ impl Component for FilePicker { }; let picker_area = area.with_width(picker_width); + self.picker.truncate_start = self.truncate_start; self.picker.render(picker_area, surface, cx); if !render_preview { @@ -277,6 +280,8 @@ pub struct Picker { prompt: Prompt, /// Whether to render in the middle of the area render_centered: bool, + /// Wheather to truncate the start (default true) + pub truncate_start: bool, format_fn: Box Cow>, callback_fn: Box, @@ -306,6 +311,7 @@ impl Picker { cursor: 0, prompt, render_centered, + truncate_start: true, format_fn: Box::new(format_fn), callback_fn: Box::new(callback_fn), }; @@ -521,7 +527,7 @@ impl Component for Picker { text_style }, true, - true, + self.truncate_start, ); } } -- cgit v1.2.3-70-g09d2 From 6fa76d9fe77d43ebc18cc78a6a1c1957d72cf59b Mon Sep 17 00:00:00 2001 From: ath3 Date: Sun, 14 Nov 2021 16:16:20 +0100 Subject: Add trim_selections command (#1092) --- book/src/keymap.md | 1 + helix-core/src/movement.rs | 2 +- helix-term/src/commands.rs | 37 +++++++++++++++++++++++++++++++++++++ helix-term/src/keymap.rs | 2 +- 4 files changed, 40 insertions(+), 2 deletions(-) (limited to 'helix-term/src/commands.rs') diff --git a/book/src/keymap.md b/book/src/keymap.md index 9f1714f6..69f5f02c 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -88,6 +88,7 @@ | `s` | Select all regex matches inside selections | `select_regex` | | `S` | Split selection into subselections on regex matches | `split_selection` | | `Alt-s` | Split selection on newlines | `split_selection_on_newline` | +| `_` | Trim whitespace from the selection | `trim_selections` | | `;` | Collapse selection onto a single cursor | `collapse_selection` | | `Alt-;` | Flip selection cursor and anchor | `flip_selections` | | `,` | Keep only the primary selection | `keep_primary_selection` | diff --git a/helix-core/src/movement.rs b/helix-core/src/movement.rs index 9e85bd21..01a8f890 100644 --- a/helix-core/src/movement.rs +++ b/helix-core/src/movement.rs @@ -168,7 +168,7 @@ pub fn backwards_skip_while(slice: RopeSlice, pos: usize, fun: F) -> Option bool, { - let mut chars_starting_from_next = slice.chars_at(pos + 1); + let mut chars_starting_from_next = slice.chars_at(pos); let mut backwards = iter::from_fn(|| chars_starting_from_next.prev()).enumerate(); backwards.find_map(|(i, c)| { if !fun(c) { diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index ebacb377..56cc02a2 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -272,6 +272,7 @@ impl Command { // TODO: different description ? goto_line_end_newline, "Goto line end", goto_first_nonwhitespace, "Goto first non-blank in line", + trim_selections, "Trim whitespace from selections", extend_to_line_start, "Extend to line start", extend_to_line_end, "Extend to line end", extend_to_line_end_newline, "Extend to line end", @@ -584,6 +585,42 @@ fn goto_first_nonwhitespace(cx: &mut Context) { doc.set_selection(view.id, selection); } +fn trim_selections(cx: &mut Context) { + let (view, doc) = current!(cx.editor); + let text = doc.text().slice(..); + + let ranges: SmallVec<[Range; 1]> = doc + .selection(view.id) + .iter() + .filter_map(|range| { + if range.is_empty() || range.fragment(text).chars().all(|ch| ch.is_whitespace()) { + return None; + } + let mut start = range.from(); + let mut end = range.to(); + start = movement::skip_while(text, start, |x| x.is_whitespace()).unwrap_or(start); + end = movement::backwards_skip_while(text, end, |x| x.is_whitespace()).unwrap_or(end); + if range.anchor < range.head { + Some(Range::new(start, end)) + } else { + Some(Range::new(end, start)) + } + }) + .collect(); + + if !ranges.is_empty() { + let primary = doc.selection(view.id).primary(); + let idx = ranges + .iter() + .position(|range| range.overlaps(&primary)) + .unwrap_or(ranges.len() - 1); + doc.set_selection(view.id, Selection::new(ranges, idx)); + } else { + collapse_selection(cx); + keep_primary_selection(cx); + }; +} + fn goto_window(cx: &mut Context, align: Align) { let (view, doc) = current!(cx.editor); diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 3280f0f8..fc9fd590 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -603,7 +603,7 @@ impl Default for Keymaps { // "Q" => replay_macro, // & align selections - // _ trim selections + "_" => trim_selections, "(" => rotate_selections_backward, ")" => rotate_selections_forward, -- cgit v1.2.3-70-g09d2 From b7c3877e947d95ee0fd9b1653dab6f65bb340439 Mon Sep 17 00:00:00 2001 From: Ivan Tham Date: Sun, 14 Nov 2021 23:16:47 +0800 Subject: Add movement shortcut for history (#1088) alt-u and alt-U--- book/src/keymap.md | 2 ++ helix-core/src/history.rs | 2 +- helix-term/src/commands.rs | 60 +++++++++++++++++++++++++++++++++------------- helix-term/src/keymap.rs | 2 ++ 4 files changed, 48 insertions(+), 18 deletions(-) (limited to 'helix-term/src/commands.rs') diff --git a/book/src/keymap.md b/book/src/keymap.md index 69f5f02c..88610a77 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -61,6 +61,8 @@ | `.` | Repeat last change | N/A | | `u` | Undo change | `undo` | | `U` | Redo change | `redo` | +| `Alt-u` | Move backward in history | `earlier` | +| `Alt-U` | Move forward in history | `later` | | `y` | Yank selection | `yank` | | `p` | Paste after selection | `paste_after` | | `P` | Paste before selection | `paste_before` | diff --git a/helix-core/src/history.rs b/helix-core/src/history.rs index 9fe7e530..4b1c8d3b 100644 --- a/helix-core/src/history.rs +++ b/helix-core/src/history.rs @@ -282,7 +282,7 @@ impl History { } /// Whether to undo by a number of edits or a duration of time. -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone, Copy)] pub enum UndoKind { Steps(usize), TimePeriod(std::time::Duration), diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 56cc02a2..115d1789 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1,5 +1,7 @@ use helix_core::{ - comment, coords_at_pos, find_first_non_whitespace_char, find_root, graphemes, indent, + comment, coords_at_pos, find_first_non_whitespace_char, find_root, graphemes, + history::UndoKind, + indent, indent::IndentStyle, line_ending::{get_line_ending_of_str, line_end_char_index, str_is_line_ending}, match_brackets, @@ -284,6 +286,8 @@ impl Command { delete_word_backward, "Delete previous word", undo, "Undo change", redo, "Redo change", + earlier, "Move backward in history", + later, "Move forward in history", yank, "Yank selection", yank_joined_to_clipboard, "Join and yank selections to clipboard", yank_main_selection_to_clipboard, "Yank main selection to clipboard", @@ -1877,10 +1881,7 @@ mod cmd { args: &[&str], _event: PromptEvent, ) -> anyhow::Result<()> { - let uk = args - .join(" ") - .parse::() - .map_err(|s| anyhow!(s))?; + let uk = args.join(" ").parse::().map_err(|s| anyhow!(s))?; let (view, doc) = current!(cx.editor); let success = doc.earlier(view.id, uk); @@ -1896,10 +1897,7 @@ mod cmd { args: &[&str], _event: PromptEvent, ) -> anyhow::Result<()> { - let uk = args - .join(" ") - .parse::() - .map_err(|s| anyhow!(s))?; + let uk = args.join(" ").parse::().map_err(|s| anyhow!(s))?; let (view, doc) = current!(cx.editor); let success = doc.later(view.id, uk); if !success { @@ -3963,20 +3961,48 @@ pub mod insert { // storing it? fn undo(cx: &mut Context) { + let count = cx.count(); let (view, doc) = current!(cx.editor); - let view_id = view.id; - let success = doc.undo(view_id); - if !success { - cx.editor.set_status("Already at oldest change".to_owned()); + for _ in 0..count { + if !doc.undo(view.id) { + cx.editor.set_status("Already at oldest change".to_owned()); + break; + } } } fn redo(cx: &mut Context) { + let count = cx.count(); + let (view, doc) = current!(cx.editor); + for _ in 0..count { + if !doc.redo(view.id) { + cx.editor.set_status("Already at newest change".to_owned()); + break; + } + } +} + +fn earlier(cx: &mut Context) { + let count = cx.count(); + 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)) { + cx.editor.set_status("Already at oldest change".to_owned()); + break; + } + } +} + +fn later(cx: &mut Context) { + let count = cx.count(); let (view, doc) = current!(cx.editor); - let view_id = view.id; - let success = doc.redo(view_id); - if !success { - cx.editor.set_status("Already at newest change".to_owned()); + for _ in 0..count { + // rather than doing in batch we do this so get error halfway + if !doc.later(view.id, UndoKind::Steps(1)) { + cx.editor.set_status("Already at newest change".to_owned()); + break; + } } } diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index fc9fd590..010714dc 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -582,6 +582,8 @@ impl Default for Keymaps { "u" => undo, "U" => redo, + "A-u" => earlier, + "A-U" => later, "y" => yank, // yank_all -- cgit v1.2.3-70-g09d2 From e128a8702eda3d449cfe355b853724d18b03a977 Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Mon, 15 Nov 2021 10:29:07 +0900 Subject: Implement MarkedString rendering Solves typescript and python documentation rendering --- helix-term/src/commands.rs | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) (limited to 'helix-term/src/commands.rs') diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 115d1789..cb1f470b 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -4660,18 +4660,28 @@ fn hover(cx: &mut Context) { move |editor: &mut Editor, compositor: &mut Compositor, response: Option| { if let Some(hover) = response { // hover.contents / .range <- used for visualizing - let contents = match hover.contents { - lsp::HoverContents::Scalar(contents) => { - // markedstring(string/languagestring to be highlighted) - // TODO - log::error!("hover contents {:?}", contents); - return; - } - lsp::HoverContents::Array(contents) => { - log::error!("hover contents {:?}", contents); - return; + + fn marked_string_to_markdown(contents: lsp::MarkedString) -> String { + match contents { + lsp::MarkedString::String(contents) => contents, + lsp::MarkedString::LanguageString(string) => { + log::error!("MarkedString {}: {}", string.language, string.value); + if string.language == "markdown" { + string.value + } else { + format!("```{}\n{}\n```", string.language, string.value) + } + } } - // TODO: render markdown + } + + let contents = match hover.contents { + lsp::HoverContents::Scalar(contents) => marked_string_to_markdown(contents), + lsp::HoverContents::Array(contents) => contents + .into_iter() + .map(marked_string_to_markdown) + .collect::>() + .join("\n\n"), lsp::HoverContents::Markup(contents) => contents.value, }; -- cgit v1.2.3-70-g09d2 From f5e070e808d2edaeae04497c01f5a0f813407956 Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Mon, 15 Nov 2021 10:33:14 +0900 Subject: minor: Remove leftover log line --- helix-term/src/commands.rs | 1 - 1 file changed, 1 deletion(-) (limited to 'helix-term/src/commands.rs') diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index cb1f470b..ab62c1aa 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -4665,7 +4665,6 @@ fn hover(cx: &mut Context) { match contents { lsp::MarkedString::String(contents) => contents, lsp::MarkedString::LanguageString(string) => { - log::error!("MarkedString {}: {}", string.language, string.value); if string.language == "markdown" { string.value } else { -- cgit v1.2.3-70-g09d2 From 87e61a0894d6af1838c5d288fae83279004026fb Mon Sep 17 00:00:00 2001 From: Cole Helbling Date: Sun, 14 Nov 2021 20:06:12 -0800 Subject: helix-term/commands: implement cquit (#1096) This allows you to exit helix with an exit code, e.g. `:cq 2`.--- helix-term/src/application.rs | 4 ++-- helix-term/src/commands.rs | 26 ++++++++++++++++++++++++++ helix-term/src/main.rs | 12 +++++++++--- helix-view/src/editor.rs | 3 +++ 4 files changed, 40 insertions(+), 5 deletions(-) (limited to 'helix-term/src/commands.rs') diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index b04eef0d..2969a9e5 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -599,7 +599,7 @@ impl Application { Ok(()) } - pub async fn run(&mut self) -> Result<(), Error> { + pub async fn run(&mut self) -> Result { self.claim_term().await?; // Exit the alternate screen and disable raw mode before panicking @@ -622,6 +622,6 @@ impl Application { self.restore_term()?; - Ok(()) + Ok(self.editor.exit_code) } } diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index ab62c1aa..8c0a005c 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -2042,6 +2042,25 @@ mod cmd { quit_all_impl(&mut cx.editor, args, event, true) } + fn cquit( + cx: &mut compositor::Context, + args: &[&str], + _event: PromptEvent, + ) -> anyhow::Result<()> { + let exit_code = args + .first() + .and_then(|code| code.parse::().ok()) + .unwrap_or(1); + cx.editor.exit_code = exit_code; + + let views: Vec<_> = cx.editor.tree.views().map(|(view, _)| view.id).collect(); + for view_id in views { + cx.editor.close(view_id, false); + } + + Ok(()) + } + fn theme( cx: &mut compositor::Context, args: &[&str], @@ -2411,6 +2430,13 @@ mod cmd { fun: force_quit_all, completer: None, }, + TypableCommand { + name: "cquit", + aliases: &["cq"], + doc: "Quit with exit code (default 1). Accepts an optional integer exit code (:cq 2).", + fun: cquit, + completer: None, + }, TypableCommand { name: "theme", aliases: &[], diff --git a/helix-term/src/main.rs b/helix-term/src/main.rs index 3ae4f7c9..6fa1ce67 100644 --- a/helix-term/src/main.rs +++ b/helix-term/src/main.rs @@ -38,8 +38,13 @@ fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> { Ok(()) } +fn main() -> Result<()> { + let exit_code = main_impl()?; + std::process::exit(exit_code); +} + #[tokio::main] -async fn main() -> Result<()> { +async fn main_impl() -> Result { let cache_dir = helix_core::cache_dir(); if !cache_dir.exists() { std::fs::create_dir_all(&cache_dir).ok(); @@ -109,7 +114,8 @@ FLAGS: // TODO: use the thread local executor to spawn the application task separately from the work pool let mut app = Application::new(args, config).context("unable to create new application")?; - app.run().await.unwrap(); - Ok(()) + let exit_code = app.run().await?; + + Ok(exit_code) } diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 7650d217..e4015707 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -129,6 +129,8 @@ pub struct Editor { pub idle_timer: Pin>, pub last_motion: Option, + + pub exit_code: i32, } #[derive(Debug, Copy, Clone)] @@ -167,6 +169,7 @@ impl Editor { idle_timer: Box::pin(sleep(config.idle_timeout)), last_motion: None, config, + exit_code: 0, } } -- cgit v1.2.3-70-g09d2 From c638b6b60e69697b7e7957ed1af1ac071c41974b Mon Sep 17 00:00:00 2001 From: Cole Helbling Date: Mon, 15 Nov 2021 07:30:45 -0800 Subject: helix-term/commands: implement buffer-close (bc, bclose) (#1035) * helix-view/view: impl method to remove document from jumps * helix-view/editor: impl close_document * helix-view/editor: remove close_buffer argument from `close` According to archseer, this was never implemented or used properly. Now that we have a proper "buffer close" function, we can get rid of this. * helix-term/commands: implement buffer-close (bc, bclose) This behaves the same as Kakoune's `delete-buffer` / `db` command: * With 3 files opened by the user with `:o ab`, `:o cd`, and `:o ef`: * `buffer-close` once closes `ef` and switches to `cd` * `buffer-close` again closes `cd` and switches to `ab` * `buffer-close` again closes `ab` and switches to a scratch buffer * With 3 files opened from the command line with `hx -- ab cd ef`: * `buffer-close` once closes `ab` and switches to `cd` * `buffer-close` again closes `cd` and switches to `ef` * `buffer-close` again closes `ef` and switches to a scratch buffer * With 1 file opened (`ab`): * `buffer-close` once closes `ab` and switches to a scratch buffer * `buffer-close` again closes the scratch buffer and switches to a new scratch buffer * helix-term/commands: implement buffer-close! (bclose!, bc!) Namely, if you have a document open in multiple splits, all the splits will be closed at the same time, leaving only splits without that document focused (or a scratch buffer if they were all focused on that buffer). * helix-view/tree: reset focus if Tree is empty --- helix-term/src/commands.rs | 50 ++++++++++++++++---- helix-view/src/editor.rs | 113 ++++++++++++++++++++++++++++++++++----------- helix-view/src/tree.rs | 3 ++ helix-view/src/view.rs | 4 ++ 4 files changed, 135 insertions(+), 35 deletions(-) (limited to 'helix-term/src/commands.rs') diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 8c0a005c..c7aab726 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1700,8 +1700,7 @@ mod cmd { buffers_remaining_impl(cx.editor)? } - cx.editor - .close(view!(cx.editor).id, /* close_buffer */ false); + cx.editor.close(view!(cx.editor).id); Ok(()) } @@ -1711,8 +1710,7 @@ mod cmd { _args: &[&str], _event: PromptEvent, ) -> anyhow::Result<()> { - cx.editor - .close(view!(cx.editor).id, /* close_buffer */ false); + cx.editor.close(view!(cx.editor).id); Ok(()) } @@ -1730,6 +1728,28 @@ mod cmd { Ok(()) } + fn buffer_close( + cx: &mut compositor::Context, + _args: &[&str], + _event: PromptEvent, + ) -> anyhow::Result<()> { + let view = view!(cx.editor); + let doc_id = view.doc; + cx.editor.close_document(doc_id, false)?; + Ok(()) + } + + fn force_buffer_close( + cx: &mut compositor::Context, + _args: &[&str], + _event: PromptEvent, + ) -> anyhow::Result<()> { + let view = view!(cx.editor); + let doc_id = view.doc; + cx.editor.close_document(doc_id, true)?; + Ok(()) + } + fn write_impl>( cx: &mut compositor::Context, path: Option

, @@ -1976,7 +1996,7 @@ mod cmd { // close all views let views: Vec<_> = cx.editor.tree.views().map(|(view, _)| view.id).collect(); for view_id in views { - cx.editor.close(view_id, false); + cx.editor.close(view_id); } } @@ -2020,7 +2040,7 @@ mod cmd { // close all views let views: Vec<_> = editor.tree.views().map(|(view, _)| view.id).collect(); for view_id in views { - editor.close(view_id, false); + editor.close(view_id); } Ok(()) @@ -2332,6 +2352,20 @@ mod cmd { fun: open, completer: Some(completers::filename), }, + TypableCommand { + name: "buffer-close", + aliases: &["bc", "bclose"], + doc: "Close the current buffer.", + fun: buffer_close, + completer: None, // FIXME: buffer completer + }, + TypableCommand { + name: "buffer-close!", + aliases: &["bc!", "bclose!"], + doc: "Close the current buffer forcefully (ignoring unsaved changes).", + fun: force_buffer_close, + completer: None, // FIXME: buffer completer + }, TypableCommand { name: "write", aliases: &["w"], @@ -4914,7 +4948,7 @@ fn wclose(cx: &mut Context) { } let view_id = view!(cx.editor).id; // close current split - cx.editor.close(view_id, /* close_buffer */ false); + cx.editor.close(view_id); } fn wonly(cx: &mut Context) { @@ -4926,7 +4960,7 @@ fn wonly(cx: &mut Context) { .collect::>(); for (view_id, focus) in views { if !focus { - cx.editor.close(view_id, /* close_buffer */ false); + cx.editor.close(view_id); } } } diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index e4015707..4712c52a 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -22,7 +22,7 @@ use anyhow::Error; pub use helix_core::diagnostic::Severity; pub use helix_core::register::Registers; use helix_core::syntax; -use helix_core::Position; +use helix_core::{Position, Selection}; use serde::Deserialize; @@ -235,9 +235,28 @@ impl Editor { } } + fn replace_document_in_view(&mut self, current_view: ViewId, doc_id: DocumentId) { + let view = self.tree.get_mut(current_view); + view.doc = doc_id; + view.offset = Position::default(); + + let doc = self.documents.get_mut(&doc_id).unwrap(); + + // initialize selection for view + doc.selections + .entry(view.id) + .or_insert_with(|| Selection::point(0)); + // TODO: reuse align_view + let pos = doc + .selection(view.id) + .primary() + .cursor(doc.text().slice(..)); + let line = doc.text().char_to_line(pos); + view.offset.row = line.saturating_sub(view.inner_area().height as usize / 2); + } + pub fn switch(&mut self, id: DocumentId, action: Action) { use crate::tree::Layout; - use helix_core::Selection; if !self.documents.contains_key(&id) { log::error!("cannot switch to document that does not exist (anymore)"); @@ -271,22 +290,9 @@ impl Editor { view.jumps.push(jump); view.last_accessed_doc = Some(view.doc); } - view.doc = id; - view.offset = Position::default(); - - let (view, doc) = current!(self); - // initialize selection for view - doc.selections - .entry(view.id) - .or_insert_with(|| Selection::point(0)); - // TODO: reuse align_view - let pos = doc - .selection(view.id) - .primary() - .cursor(doc.text().slice(..)); - let line = doc.text().char_to_line(pos); - view.offset.row = line.saturating_sub(view.inner_area().height as usize / 2); + let view_id = view.id; + self.replace_document_in_view(view_id, id); return; } @@ -318,11 +324,16 @@ impl Editor { self._refresh(); } - fn new_file_from_document(&mut self, action: Action, mut document: Document) -> DocumentId { + fn new_document(&mut self, mut document: Document) -> DocumentId { let id = DocumentId(self.next_document_id); self.next_document_id += 1; document.id = id; self.documents.insert(id, document); + id + } + + fn new_file_from_document(&mut self, action: Action, document: Document) -> DocumentId { + let id = self.new_document(document); self.switch(id, action); id } @@ -392,7 +403,7 @@ impl Editor { Ok(id) } - pub fn close(&mut self, id: ViewId, close_buffer: bool) { + pub fn close(&mut self, id: ViewId) { let view = self.tree.get(self.tree.focus); // remove selection self.documents @@ -401,18 +412,66 @@ impl Editor { .selections .remove(&id); - if close_buffer { - // get around borrowck issues - let doc = &self.documents[&view.doc]; + self.tree.remove(id); + self._refresh(); + } - if let Some(language_server) = doc.language_server() { - tokio::spawn(language_server.text_document_did_close(doc.identifier())); - } - self.documents.remove(&view.doc); + pub fn close_document(&mut self, doc_id: DocumentId, force: bool) -> anyhow::Result<()> { + let doc = match self.documents.get(&doc_id) { + Some(doc) => doc, + None => anyhow::bail!("document does not exist"), + }; + + if !force && doc.is_modified() { + anyhow::bail!( + "buffer {:?} is modified", + doc.relative_path() + .map(|path| path.to_string_lossy().to_string()) + .unwrap_or_else(|| "[scratch]".into()) + ); + } + + if let Some(language_server) = doc.language_server() { + tokio::spawn(language_server.text_document_did_close(doc.identifier())); + } + + let views_to_close = self + .tree + .views() + .filter_map(|(view, _focus)| { + if view.doc == doc_id { + Some(view.id) + } else { + None + } + }) + .collect::>(); + + for view_id in views_to_close { + self.close(view_id); + } + + self.documents.remove(&doc_id); + + // If the document we removed was visible in all views, we will have no more views. We don't + // want to close the editor just for a simple buffer close, so we need to create a new view + // containing either an existing document, or a brand new document. + if self.tree.views().peekable().peek().is_none() { + let doc_id = self + .documents + .iter() + .map(|(&doc_id, _)| doc_id) + .next() + .unwrap_or_else(|| self.new_document(Document::default())); + let view = View::new(doc_id); + let view_id = self.tree.insert(view); + let doc = self.documents.get_mut(&doc_id).unwrap(); + doc.selections.insert(view_id, Selection::point(0)); } - self.tree.remove(id); self._refresh(); + + Ok(()) } pub fn resize(&mut self, area: Rect) { diff --git a/helix-view/src/tree.rs b/helix-view/src/tree.rs index 064334b1..de5046ac 100644 --- a/helix-view/src/tree.rs +++ b/helix-view/src/tree.rs @@ -314,6 +314,9 @@ impl Tree { pub fn recalculate(&mut self) { if self.is_empty() { + // There are no more views, so the tree should focus itself again. + self.focus = self.root; + return; } diff --git a/helix-view/src/view.rs b/helix-view/src/view.rs index 6a624ded..a77f1562 100644 --- a/helix-view/src/view.rs +++ b/helix-view/src/view.rs @@ -54,6 +54,10 @@ impl JumpList { None } } + + pub fn remove(&mut self, doc_id: &DocumentId) { + self.jumps.retain(|(other_id, _)| other_id != doc_id); + } } #[derive(Debug)] -- cgit v1.2.3-70-g09d2 From 46d9ae2b62f5b8494c527e0f8475509ce5fad095 Mon Sep 17 00:00:00 2001 From: Bob Date: Mon, 15 Nov 2021 23:31:20 +0800 Subject: Readline style insert mode (#1039) * readline style insert mode * update keymap.md * don't save change history in insert mode * Revert "don't save change history in insert mode" This reverts commit cb47f946d7fb62ceda68e7d1692a3914d0be7762. * don't affect register and history in insert mode * add insert_register * don't call exit_select_mode in insert mode * avoid set_selection * avoid duplicated current!--- book/src/keymap.md | 30 +++++++++++++++----- helix-term/src/commands.rs | 67 +++++++++++++++++++++++++++++++++++++++++++-- helix-term/src/keymap.rs | 17 ++++++++++++ helix-term/src/ui/prompt.rs | 9 ++++++ 4 files changed, 114 insertions(+), 9 deletions(-) (limited to 'helix-term/src/commands.rs') diff --git a/book/src/keymap.md b/book/src/keymap.md index 88610a77..7a896035 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -243,11 +243,26 @@ Mappings in the style of [vim-unimpaired](https://github.com/tpope/vim-unimpaire ## Insert Mode -| Key | Description | Command | -| ----- | ----------- | ------- | -| `Escape` | Switch to normal mode | `normal_mode` | -| `Ctrl-x` | Autocomplete | `completion` | -| `Ctrl-w` | Delete previous word | `delete_word_backward` | +| Key | Description | Command | +| ----- | ----------- | ------- | +| `Escape` | Switch to normal mode | `normal_mode` | +| `Ctrl-x` | Autocomplete | `completion` | +| `Ctrl-r` | Insert a register content | `insert_register` | +| `Ctrl-w` | Delete previous word | `delete_word_backward` | +| `Alt-d` | Delete next word | `delete_word_forward` | +| `Alt-b`, `Alt-Left` | Backward a word | `move_prev_word_end` | +| `Ctrl-b`, `Left` | Backward a char | `move_char_left` | +| `Alt-f`, `Alt-Right` | Forward a word | `move_next_word_start` | +| `Ctrl-f`, `Right` | Forward a char | `move_char_right` | +| `Ctrl-e`, `End` | move to line end | `goto_line_end_newline` | +| `Ctrl-a`, `Home` | move to line start | `goto_line_start` | +| `Ctrl-w` | delete previous word | `delete_word_backwar` | +| `Ctrl-u` | delete to start of line | `kill_to_line_start` | +| `Ctrl-k` | delete to end of line | `kill_to_line_end` | +| `backspace`, `Ctrl-h` | delete previous char | `delete_char_backward` | +| `delete`, `Ctrl-d` | delete previous char | `delete_char_forward` | +| `Ctrl-p`, `Up` | move to previous line | `move_line_up` | +| `Ctrl-n`, `Down` | move to next line | `move_line_down` | ## Select / extend mode @@ -285,6 +300,7 @@ Keys to use within prompt, Remapping currently not supported. | `Ctrl-e`, `End` | Move prompt end | | `Ctrl-a`, `Home` | Move prompt start | | `Ctrl-w` | Delete previous word | +| `Alt-d` | Delete next word | | `Ctrl-u` | Delete to start of line | | `Ctrl-k` | Delete to end of line | | `backspace`, `Ctrl-h` | Delete previous char | @@ -292,7 +308,7 @@ Keys to use within prompt, Remapping currently not supported. | `Ctrl-s` | Insert a word under doc cursor, may be changed to Ctrl-r Ctrl-w later | | `Ctrl-p`, `Up` | Select previous history | | `Ctrl-n`, `Down` | Select next history | -| `Tab` | Select next completion item | -| `BackTab` | Select previous completion item | +| `Tab` | Select next completion item | +| `BackTab` | Select previous completion item | | `Enter` | Open selected | diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index c7aab726..74bc52fd 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -187,6 +187,7 @@ impl Command { copy_selection_on_prev_line, "Copy selection on previous line", move_next_word_start, "Move to beginning of next word", move_prev_word_start, "Move to beginning of previous word", + move_prev_word_end, "Move to end of previous word", move_next_word_end, "Move to end of next word", move_next_long_word_start, "Move to beginning of next long word", move_prev_long_word_start, "Move to beginning of previous long word", @@ -284,6 +285,9 @@ impl Command { delete_char_backward, "Delete previous char", delete_char_forward, "Delete next char", delete_word_backward, "Delete previous word", + delete_word_forward, "Delete next word", + kill_to_line_start, "Delete content till the start of the line", + kill_to_line_end, "Delete content till the end of the line", undo, "Undo change", redo, "Redo change", earlier, "Move backward in history", @@ -330,6 +334,7 @@ impl Command { wclose, "Close window", wonly, "Current window only", select_register, "Select register", + insert_register, "Insert register", align_view_middle, "Align view middle", align_view_top, "Align view top", align_view_center, "Align view center", @@ -572,6 +577,29 @@ fn extend_to_line_start(cx: &mut Context) { goto_line_start_impl(view, doc, Movement::Extend) } +fn kill_to_line_start(cx: &mut Context) { + let (view, doc) = current!(cx.editor); + let text = doc.text().slice(..); + + let selection = doc.selection(view.id).clone().transform(|range| { + let line = range.cursor_line(text); + range.put_cursor(text, text.line_to_char(line), true) + }); + delete_selection_insert_mode(doc, view, &selection); +} + +fn kill_to_line_end(cx: &mut Context) { + let (view, doc) = current!(cx.editor); + let text = doc.text().slice(..); + + let selection = doc.selection(view.id).clone().transform(|range| { + let line = range.cursor_line(text); + let pos = line_end_char_index(&text, line); + range.put_cursor(text, pos, true) + }); + delete_selection_insert_mode(doc, view, &selection); +} + fn goto_first_nonwhitespace(cx: &mut Context) { let (view, doc) = current!(cx.editor); let text = doc.text().slice(..); @@ -684,6 +712,10 @@ fn move_prev_word_start(cx: &mut Context) { move_word_impl(cx, movement::move_prev_word_start) } +fn move_prev_word_end(cx: &mut Context) { + move_word_impl(cx, movement::move_prev_word_end) +} + fn move_next_word_end(cx: &mut Context) { move_word_impl(cx, movement::move_next_word_end) } @@ -1586,6 +1618,17 @@ fn delete_selection_impl(reg: &mut Register, doc: &mut Document, view_id: ViewId doc.apply(&transaction, view_id); } +#[inline] +fn delete_selection_insert_mode(doc: &mut Document, view: &View, selection: &Selection) { + let view_id = view.id; + + // then delete + let transaction = Transaction::change_by_selection(doc.text(), selection, |range| { + (range.from(), range.to(), None) + }); + doc.apply(&transaction, view_id); +} + fn delete_selection(cx: &mut Context) { let reg_name = cx.register.unwrap_or('"'); let (view, doc) = current!(cx.editor); @@ -4010,8 +4053,19 @@ pub mod insert { .selection(view.id) .clone() .transform(|range| movement::move_prev_word_start(text, range, count)); - doc.set_selection(view.id, selection); - delete_selection(cx) + delete_selection_insert_mode(doc, view, &selection); + } + + pub fn delete_word_forward(cx: &mut Context) { + let count = cx.count(); + let (view, doc) = current!(cx.editor); + let text = doc.text().slice(..); + + let selection = doc + .selection(view.id) + .clone() + .transform(|range| movement::move_next_word_start(text, range, count)); + delete_selection_insert_mode(doc, view, &selection); } } @@ -4973,6 +5027,15 @@ fn select_register(cx: &mut Context) { }) } +fn insert_register(cx: &mut Context) { + cx.on_next_key(move |cx, event| { + if let Some(ch) = event.char() { + cx.editor.selected_register = Some(ch); + paste_before(cx); + } + }) +} + fn align_view_top(cx: &mut Context) { let (view, doc) = current!(cx.editor); align_view(doc, view, Align::Top); diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 010714dc..fdf43d87 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -731,21 +731,38 @@ impl Default for Keymaps { "esc" => normal_mode, "backspace" => delete_char_backward, + "C-h" => delete_char_backward, "del" => delete_char_forward, + "C-d" => delete_char_forward, "ret" => insert_newline, "tab" => insert_tab, "C-w" => delete_word_backward, + "A-d" => delete_word_forward, "left" => move_char_left, + "C-b" => move_char_left, "down" => move_line_down, + "C-n" => move_line_down, "up" => move_line_up, + "C-p" => move_line_up, "right" => move_char_right, + "C-f" => move_char_right, + "A-b" => move_prev_word_end, + "A-left" => move_prev_word_end, + "A-f" => move_next_word_start, + "A-right" => move_next_word_start, "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, + + "C-k" => kill_to_line_end, + "C-u" => kill_to_line_start, "C-x" => completion, + "C-r" => insert_register, }); Keymaps(hashmap!( Mode::Normal => Keymap::new(normal), diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs index deed1609..e90b0772 100644 --- a/helix-term/src/ui/prompt.rs +++ b/helix-term/src/ui/prompt.rs @@ -231,6 +231,14 @@ impl Prompt { self.completion = (self.completion_fn)(&self.line); } + pub fn delete_word_forwards(&mut self) { + let pos = self.eval_movement(Movement::ForwardWord(1)); + self.line.replace_range(self.cursor..pos, ""); + + self.exit_selection(); + self.completion = (self.completion_fn)(&self.line); + } + pub fn kill_to_start_of_line(&mut self) { let pos = self.eval_movement(Movement::StartOfLine); self.line.replace_range(pos..self.cursor, ""); @@ -435,6 +443,7 @@ impl Component for Prompt { ctrl!('e') | key!(End) => self.move_end(), ctrl!('a') | key!(Home) => self.move_start(), ctrl!('w') => self.delete_word_backwards(), + alt!('d') => self.delete_word_forwards(), ctrl!('k') => self.kill_to_end_of_line(), ctrl!('u') => self.kill_to_start_of_line(), ctrl!('h') | key!(Backspace) => { -- cgit v1.2.3-70-g09d2 From 6cb35d28a878470ef742b813e1e8d412d09e6b52 Mon Sep 17 00:00:00 2001 From: Jason Hansen Date: Mon, 15 Nov 2021 08:32:58 -0700 Subject: Add command to inc/dec number under cursor (#1027) * Add command to inc/dec number under cursor With the cursor over a number in normal mode, Ctrl + A will increment the number and Ctrl + X will decrement the number. It works with binary, octal, decimal, and hexidecimal numbers. Here are some examples. 0b01110100 0o1734 -24234 0x1F245 If the number isn't over a number it will try to find a number after the cursor on the same line. * Move several functions to helix-core * Change to work based on word under selection * It no longer finds the next number if the cursor isn't already over a number. * It only matches numbers that are part of words with other characters like "foo123bar". * It now works with multiple selections. * Add some unit tests * Fix for clippy * Simplify some things * Keep previous selection after incrementing * Use short word instead of long word This change requires us to manually handle minus sign. * Don't pad decimal numbers if no leading zeros * Handle numbers with `_` separators * Refactor and add tests * Move most of the code into core * Add tests for the incremented output * Use correct range * Formatting * Rename increment functions * Make docs more specific * This is easier to read * This is clearer * Type can be inferred--- book/src/keymap.md | 2 + helix-core/src/lib.rs | 1 + helix-core/src/numbers.rs | 499 +++++++++++++++++++++++++++++++++++++++++++++ helix-term/src/commands.rs | 38 ++++ helix-term/src/keymap.rs | 3 + 5 files changed, 543 insertions(+) create mode 100644 helix-core/src/numbers.rs (limited to 'helix-term/src/commands.rs') diff --git a/book/src/keymap.md b/book/src/keymap.md index 7a896035..335e393b 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -72,6 +72,8 @@ | `=` | Format selection (**LSP**) | `format_selections` | | `d` | Delete selection | `delete_selection` | | `c` | Change selection (delete and enter insert mode) | `change_selection` | +| `Ctrl-a` | Increment object (number) under cursor | `increment` | +| `Ctrl-x` | Decrement object (number) under cursor | `decrement` | #### Shell diff --git a/helix-core/src/lib.rs b/helix-core/src/lib.rs index d1720df0..de7e95c1 100644 --- a/helix-core/src/lib.rs +++ b/helix-core/src/lib.rs @@ -10,6 +10,7 @@ pub mod line_ending; pub mod macros; pub mod match_brackets; pub mod movement; +pub mod numbers; pub mod object; pub mod path; mod position; diff --git a/helix-core/src/numbers.rs b/helix-core/src/numbers.rs new file mode 100644 index 00000000..e9f3c898 --- /dev/null +++ b/helix-core/src/numbers.rs @@ -0,0 +1,499 @@ +use std::borrow::Cow; + +use ropey::RopeSlice; + +use crate::{ + textobject::{textobject_word, TextObject}, + Range, Tendril, +}; + +#[derive(Debug, PartialEq, Eq)] +pub struct NumberIncrementor<'a> { + pub range: Range, + pub value: i64, + pub radix: u32, + + text: RopeSlice<'a>, +} + +impl<'a> NumberIncrementor<'a> { + /// Return information about number under rang if there is one. + pub fn from_range(text: RopeSlice, range: Range) -> Option { + // If the cursor is on the minus sign of a number we want to get the word textobject to the + // right of it. + let range = if range.to() < text.len_chars() + && range.to() - range.from() <= 1 + && text.char(range.from()) == '-' + { + Range::new(range.from() + 1, range.to() + 1) + } else { + range + }; + + let range = textobject_word(text, range, TextObject::Inside, 1, false); + + // If there is a minus sign to the left of the word object, we want to include it in the range. + let range = if range.from() > 0 && text.char(range.from() - 1) == '-' { + range.extend(range.from() - 1, range.from()) + } else { + range + }; + + let word: String = text + .slice(range.from()..range.to()) + .chars() + .filter(|&c| c != '_') + .collect(); + let (radix, prefixed) = if word.starts_with("0x") { + (16, true) + } else if word.starts_with("0o") { + (8, true) + } else if word.starts_with("0b") { + (2, true) + } else { + (10, false) + }; + + let number = if prefixed { &word[2..] } else { &word }; + + let value = i128::from_str_radix(number, radix).ok()?; + if (value.is_positive() && value.leading_zeros() < 64) + || (value.is_negative() && value.leading_ones() < 64) + { + return None; + } + + let value = value as i64; + Some(NumberIncrementor { + range, + value, + radix, + text, + }) + } + + /// Add `amount` to the number and return the formatted text. + pub fn incremented_text(&self, amount: i64) -> Tendril { + let old_text: Cow = self.text.slice(self.range.from()..self.range.to()).into(); + let old_length = old_text.len(); + let new_value = self.value.wrapping_add(amount); + + // Get separator indexes from right to left. + let separator_rtl_indexes: Vec = old_text + .chars() + .rev() + .enumerate() + .filter_map(|(i, c)| if c == '_' { Some(i) } else { None }) + .collect(); + + let format_length = if self.radix == 10 { + match (self.value.is_negative(), new_value.is_negative()) { + (true, false) => old_length - 1, + (false, true) => old_length + 1, + _ => old_text.len(), + } + } else { + old_text.len() - 2 + } - separator_rtl_indexes.len(); + + let mut new_text = match self.radix { + 2 => format!("0b{:01$b}", new_value, format_length), + 8 => format!("0o{:01$o}", new_value, format_length), + 10 if old_text.starts_with('0') || old_text.starts_with("-0") => { + format!("{:01$}", new_value, format_length) + } + 10 => format!("{}", new_value), + 16 => { + let (lower_count, upper_count): (usize, usize) = + old_text.chars().skip(2).fold((0, 0), |(lower, upper), c| { + ( + lower + c.is_ascii_lowercase().then(|| 1).unwrap_or(0), + upper + c.is_ascii_uppercase().then(|| 1).unwrap_or(0), + ) + }); + if upper_count > lower_count { + format!("0x{:01$X}", new_value, format_length) + } else { + format!("0x{:01$x}", new_value, format_length) + } + } + _ => unimplemented!("radix not supported: {}", self.radix), + }; + + // Add separators from original number. + for &rtl_index in &separator_rtl_indexes { + if rtl_index < new_text.len() { + let new_index = new_text.len() - rtl_index; + new_text.insert(new_index, '_'); + } + } + + // Add in additional separators if necessary. + if new_text.len() > old_length && !separator_rtl_indexes.is_empty() { + let spacing = match separator_rtl_indexes.as_slice() { + [.., b, a] => a - b - 1, + _ => separator_rtl_indexes[0], + }; + + let prefix_length = if self.radix == 10 { 0 } else { 2 }; + if let Some(mut index) = new_text.find('_') { + while index - prefix_length > spacing { + index -= spacing; + new_text.insert(index, '_'); + } + } + } + + new_text.into() + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::Rope; + + #[test] + fn test_decimal_at_point() { + let rope = Rope::from_str("Test text 12345 more text."); + let range = Range::point(12); + assert_eq!( + NumberIncrementor::from_range(rope.slice(..), range), + Some(NumberIncrementor { + range: Range::new(10, 15), + value: 12345, + radix: 10, + text: rope.slice(..), + }) + ); + } + + #[test] + fn test_uppercase_hexadecimal_at_point() { + let rope = Rope::from_str("Test text 0x123ABCDEF more text."); + let range = Range::point(12); + assert_eq!( + NumberIncrementor::from_range(rope.slice(..), range), + Some(NumberIncrementor { + range: Range::new(10, 21), + value: 0x123ABCDEF, + radix: 16, + text: rope.slice(..), + }) + ); + } + + #[test] + fn test_lowercase_hexadecimal_at_point() { + let rope = Rope::from_str("Test text 0xfa3b4e more text."); + let range = Range::point(12); + assert_eq!( + NumberIncrementor::from_range(rope.slice(..), range), + Some(NumberIncrementor { + range: Range::new(10, 18), + value: 0xfa3b4e, + radix: 16, + text: rope.slice(..), + }) + ); + } + + #[test] + fn test_octal_at_point() { + let rope = Rope::from_str("Test text 0o1074312 more text."); + let range = Range::point(12); + assert_eq!( + NumberIncrementor::from_range(rope.slice(..), range), + Some(NumberIncrementor { + range: Range::new(10, 19), + value: 0o1074312, + radix: 8, + text: rope.slice(..), + }) + ); + } + + #[test] + fn test_binary_at_point() { + let rope = Rope::from_str("Test text 0b10111010010101 more text."); + let range = Range::point(12); + assert_eq!( + NumberIncrementor::from_range(rope.slice(..), range), + Some(NumberIncrementor { + range: Range::new(10, 26), + value: 0b10111010010101, + radix: 2, + text: rope.slice(..), + }) + ); + } + + #[test] + fn test_negative_decimal_at_point() { + let rope = Rope::from_str("Test text -54321 more text."); + let range = Range::point(12); + assert_eq!( + NumberIncrementor::from_range(rope.slice(..), range), + Some(NumberIncrementor { + range: Range::new(10, 16), + value: -54321, + radix: 10, + text: rope.slice(..), + }) + ); + } + + #[test] + fn test_decimal_with_leading_zeroes_at_point() { + let rope = Rope::from_str("Test text 000045326 more text."); + let range = Range::point(12); + assert_eq!( + NumberIncrementor::from_range(rope.slice(..), range), + Some(NumberIncrementor { + range: Range::new(10, 19), + value: 45326, + radix: 10, + text: rope.slice(..), + }) + ); + } + + #[test] + fn test_negative_decimal_cursor_on_minus_sign() { + let rope = Rope::from_str("Test text -54321 more text."); + let range = Range::point(10); + assert_eq!( + NumberIncrementor::from_range(rope.slice(..), range), + Some(NumberIncrementor { + range: Range::new(10, 16), + value: -54321, + radix: 10, + text: rope.slice(..), + }) + ); + } + + #[test] + fn test_number_under_range_start_of_rope() { + let rope = Rope::from_str("100"); + let range = Range::point(0); + assert_eq!( + NumberIncrementor::from_range(rope.slice(..), range), + Some(NumberIncrementor { + range: Range::new(0, 3), + value: 100, + radix: 10, + text: rope.slice(..), + }) + ); + } + + #[test] + fn test_number_under_range_end_of_rope() { + let rope = Rope::from_str("100"); + let range = Range::point(2); + assert_eq!( + NumberIncrementor::from_range(rope.slice(..), range), + Some(NumberIncrementor { + range: Range::new(0, 3), + value: 100, + radix: 10, + text: rope.slice(..), + }) + ); + } + + #[test] + fn test_number_surrounded_by_punctuation() { + let rope = Rope::from_str(",100;"); + let range = Range::point(1); + assert_eq!( + NumberIncrementor::from_range(rope.slice(..), range), + Some(NumberIncrementor { + range: Range::new(1, 4), + value: 100, + radix: 10, + text: rope.slice(..), + }) + ); + } + + #[test] + fn test_not_a_number_point() { + let rope = Rope::from_str("Test text 45326 more text."); + let range = Range::point(6); + assert_eq!(NumberIncrementor::from_range(rope.slice(..), range), None); + } + + #[test] + fn test_number_too_large_at_point() { + let rope = Rope::from_str("Test text 0xFFFFFFFFFFFFFFFFF more text."); + let range = Range::point(12); + assert_eq!(NumberIncrementor::from_range(rope.slice(..), range), None); + } + + #[test] + fn test_number_cursor_one_right_of_number() { + let rope = Rope::from_str("100 "); + let range = Range::point(3); + assert_eq!(NumberIncrementor::from_range(rope.slice(..), range), None); + } + + #[test] + fn test_number_cursor_one_left_of_number() { + let rope = Rope::from_str(" 100"); + let range = Range::point(0); + assert_eq!(NumberIncrementor::from_range(rope.slice(..), range), None); + } + + #[test] + fn test_increment_basic_decimal_numbers() { + let tests = [ + ("100", 1, "101"), + ("100", -1, "99"), + ("99", 1, "100"), + ("100", 1000, "1100"), + ("100", -1000, "-900"), + ("-1", 1, "0"), + ("-1", 2, "1"), + ("1", -1, "0"), + ("1", -2, "-1"), + ]; + + for (original, amount, expected) in tests { + let rope = Rope::from_str(original); + let range = Range::point(0); + assert_eq!( + NumberIncrementor::from_range(rope.slice(..), range) + .unwrap() + .incremented_text(amount), + expected.into() + ); + } + } + + #[test] + fn test_increment_basic_hexadedimal_numbers() { + let tests = [ + ("0x0100", 1, "0x0101"), + ("0x0100", -1, "0x00ff"), + ("0x0001", -1, "0x0000"), + ("0x0000", -1, "0xffffffffffffffff"), + ("0xffffffffffffffff", 1, "0x0000000000000000"), + ("0xffffffffffffffff", 2, "0x0000000000000001"), + ("0xffffffffffffffff", -1, "0xfffffffffffffffe"), + ("0xABCDEF1234567890", 1, "0xABCDEF1234567891"), + ("0xabcdef1234567890", 1, "0xabcdef1234567891"), + ]; + + for (original, amount, expected) in tests { + let rope = Rope::from_str(original); + let range = Range::point(0); + assert_eq!( + NumberIncrementor::from_range(rope.slice(..), range) + .unwrap() + .incremented_text(amount), + expected.into() + ); + } + } + + #[test] + fn test_increment_basic_octal_numbers() { + let tests = [ + ("0o0107", 1, "0o0110"), + ("0o0110", -1, "0o0107"), + ("0o0001", -1, "0o0000"), + ("0o7777", 1, "0o10000"), + ("0o1000", -1, "0o0777"), + ("0o0107", 10, "0o0121"), + ("0o0000", -1, "0o1777777777777777777777"), + ("0o1777777777777777777777", 1, "0o0000000000000000000000"), + ("0o1777777777777777777777", 2, "0o0000000000000000000001"), + ("0o1777777777777777777777", -1, "0o1777777777777777777776"), + ]; + + for (original, amount, expected) in tests { + let rope = Rope::from_str(original); + let range = Range::point(0); + assert_eq!( + NumberIncrementor::from_range(rope.slice(..), range) + .unwrap() + .incremented_text(amount), + expected.into() + ); + } + } + + #[test] + fn test_increment_basic_binary_numbers() { + let tests = [ + ("0b00000100", 1, "0b00000101"), + ("0b00000100", -1, "0b00000011"), + ("0b00000100", 2, "0b00000110"), + ("0b00000100", -2, "0b00000010"), + ("0b00000001", -1, "0b00000000"), + ("0b00111111", 10, "0b01001001"), + ("0b11111111", 1, "0b100000000"), + ("0b10000000", -1, "0b01111111"), + ( + "0b0000", + -1, + "0b1111111111111111111111111111111111111111111111111111111111111111", + ), + ( + "0b1111111111111111111111111111111111111111111111111111111111111111", + 1, + "0b0000000000000000000000000000000000000000000000000000000000000000", + ), + ( + "0b1111111111111111111111111111111111111111111111111111111111111111", + 2, + "0b0000000000000000000000000000000000000000000000000000000000000001", + ), + ( + "0b1111111111111111111111111111111111111111111111111111111111111111", + -1, + "0b1111111111111111111111111111111111111111111111111111111111111110", + ), + ]; + + for (original, amount, expected) in tests { + let rope = Rope::from_str(original); + let range = Range::point(0); + assert_eq!( + NumberIncrementor::from_range(rope.slice(..), range) + .unwrap() + .incremented_text(amount), + expected.into() + ); + } + } + + #[test] + fn test_increment_with_separators() { + let tests = [ + ("999_999", 1, "1_000_000"), + ("1_000_000", -1, "999_999"), + ("-999_999", -1, "-1_000_000"), + ("0x0000_0000_0001", 0x1_ffff_0000, "0x0001_ffff_0001"), + ("0x0000_0000_0001", 0x1_ffff_0000, "0x0001_ffff_0001"), + ("0x0000_0000_0001", 0x1_ffff_0000, "0x0001_ffff_0001"), + ("0x0000_0000", -1, "0xffff_ffff_ffff_ffff"), + ("0x0000_0000_0000", -1, "0xffff_ffff_ffff_ffff"), + ("0b01111111_11111111", 1, "0b10000000_00000000"), + ("0b11111111_11111111", 1, "0b1_00000000_00000000"), + ]; + + for (original, amount, expected) in tests { + let rope = Rope::from_str(original); + let range = Range::point(0); + assert_eq!( + NumberIncrementor::from_range(rope.slice(..), range) + .unwrap() + .incremented_text(amount), + expected.into() + ); + } + } +} diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 74bc52fd..7ef8f56c 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -6,6 +6,7 @@ use helix_core::{ line_ending::{get_line_ending_of_str, line_end_char_index, str_is_line_ending}, match_brackets, movement::{self, Direction}, + numbers::NumberIncrementor, object, pos_at_coords, regex::{self, Regex, RegexBuilder}, register::Register, @@ -354,6 +355,8 @@ impl Command { shell_keep_pipe, "Filter selections with shell predicate", suspend, "Suspend", rename_symbol, "Rename symbol", + increment, "Increment", + decrement, "Decrement", ); } @@ -5459,3 +5462,38 @@ fn rename_symbol(cx: &mut Context) { ); cx.push_layer(Box::new(prompt)); } + +/// Increment object under cursor by count. +fn increment(cx: &mut Context) { + increment_impl(cx, cx.count() as i64); +} + +/// Decrement object under cursor by count. +fn decrement(cx: &mut Context) { + increment_impl(cx, -(cx.count() as i64)); +} + +/// Decrement object under cursor by `amount`. +fn increment_impl(cx: &mut Context, amount: i64) { + let (view, doc) = current!(cx.editor); + let selection = doc.selection(view.id); + let text = doc.text(); + + let changes = selection.ranges().iter().filter_map(|range| { + let incrementor = NumberIncrementor::from_range(text.slice(..), *range)?; + let new_text = incrementor.incremented_text(amount); + Some(( + incrementor.range.from(), + incrementor.range.to(), + Some(new_text), + )) + }); + + if changes.clone().count() > 0 { + let transaction = Transaction::change(doc.text(), changes); + let transaction = transaction.with_selection(selection.clone()); + + doc.apply(&transaction, view.id); + doc.append_changes_to_history(view.id); + } +} diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index fdf43d87..bf3b594e 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -698,6 +698,9 @@ impl Default for Keymaps { "A-!" => shell_append_output, "$" => shell_keep_pipe, "C-z" => suspend, + + "C-a" => increment, + "C-x" => decrement, }); let mut select = normal.clone(); select.merge_nodes(keymap!({ "Select mode" -- cgit v1.2.3-70-g09d2 From dd98727bad2c727c0cf862dc95525a2fec16b10d Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Tue, 16 Nov 2021 00:37:30 +0900 Subject: fix: editor.close now takes only a single parameter --- helix-term/src/commands.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'helix-term/src/commands.rs') diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 7ef8f56c..e9bfdfdd 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -2121,7 +2121,7 @@ mod cmd { let views: Vec<_> = cx.editor.tree.views().map(|(view, _)| view.id).collect(); for view_id in views { - cx.editor.close(view_id, false); + cx.editor.close(view_id); } Ok(()) -- cgit v1.2.3-70-g09d2 From fa4c59df4623ac33c307c5637dcf74c83c71d763 Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Thu, 18 Nov 2021 11:08:47 +0900 Subject: Simplify compositor.find --- helix-term/src/application.rs | 18 +++--------------- helix-term/src/commands.rs | 22 +++++++++------------- helix-term/src/compositor.rs | 5 +++-- 3 files changed, 15 insertions(+), 30 deletions(-) (limited to 'helix-term/src/commands.rs') diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 2969a9e5..78b93cd9 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -270,12 +270,8 @@ impl Application { } let editor_view = self .compositor - .find(std::any::type_name::()) + .find::() .expect("expected at least one EditorView"); - let editor_view = editor_view - .as_any_mut() - .downcast_mut::() - .unwrap(); if editor_view.completion.is_some() { return; @@ -440,12 +436,8 @@ impl Application { { let editor_view = self .compositor - .find(std::any::type_name::()) + .find::() .expect("expected at least one EditorView"); - let editor_view = editor_view - .as_any_mut() - .downcast_mut::() - .unwrap(); let lsp::ProgressParams { token, value } = params; let lsp::ProgressParamsValue::WorkDone(work) = value; @@ -559,12 +551,8 @@ impl Application { let editor_view = self .compositor - .find(std::any::type_name::()) + .find::() .expect("expected at least one EditorView"); - let editor_view = editor_view - .as_any_mut() - .downcast_mut::() - .unwrap(); let spinner = editor_view.spinners_mut().get_or_create(server_id); if spinner.is_stopped() { spinner.start(); diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index e9bfdfdd..847e6f09 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -4735,19 +4735,15 @@ pub fn completion(cx: &mut Context) { return; } let size = compositor.size(); - let ui = compositor - .find(std::any::type_name::()) - .unwrap(); - if let Some(ui) = ui.as_any_mut().downcast_mut::() { - ui.set_completion( - editor, - items, - offset_encoding, - start_offset, - trigger_offset, - size, - ); - }; + let ui = compositor.find::().unwrap(); + ui.set_completion( + editor, + items, + offset_encoding, + start_offset, + trigger_offset, + size, + ); }, ); } diff --git a/helix-term/src/compositor.rs b/helix-term/src/compositor.rs index dc8b91d7..3a644750 100644 --- a/helix-term/src/compositor.rs +++ b/helix-term/src/compositor.rs @@ -177,11 +177,12 @@ impl Compositor { .any(|component| component.type_name() == type_name) } - pub fn find(&mut self, type_name: &str) -> Option<&mut dyn Component> { + pub fn find(&mut self) -> Option<&mut T> { + let type_name = std::any::type_name::(); self.layers .iter_mut() .find(|component| component.type_name() == type_name) - .map(|component| component.as_mut()) + .and_then(|component| component.as_any_mut().downcast_mut()) } } -- cgit v1.2.3-70-g09d2 From bd56dde6e28b22b661ad991d0f23b66e089a9700 Mon Sep 17 00:00:00 2001 From: Ivan Tham Date: Thu, 18 Nov 2021 17:46:27 +0800 Subject: Ensure cursor in view after pipe (#1123) Fix #1024--- helix-term/src/commands.rs | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'helix-term/src/commands.rs') diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 847e6f09..e1120ef1 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -5377,6 +5377,10 @@ fn shell(cx: &mut Context, prompt: Cow<'static, str>, behavior: ShellBehavior) { doc.apply(&transaction, view.id); doc.append_changes_to_history(view.id); } + + // after replace cursor may be out of bounds, do this to + // make sure cursor is in view and update scroll as well + view.ensure_cursor_in_view(doc, cx.editor.config.scrolloff); }, ); -- cgit v1.2.3-70-g09d2 From 5959356a2404a8c317d07934ee388d6637c2888a Mon Sep 17 00:00:00 2001 From: WindSoilder Date: Thu, 18 Nov 2021 23:19:40 +0800 Subject: Implement indent-aware delete (#1120) * delete character backward can make undent behavior * improve to handle mixed indentation--- helix-term/src/commands.rs | 70 ++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 61 insertions(+), 9 deletions(-) (limited to 'helix-term/src/commands.rs') diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index e1120ef1..e5cf7bb2 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -10,10 +10,11 @@ use helix_core::{ object, pos_at_coords, regex::{self, Regex, RegexBuilder}, register::Register, - search, selection, surround, textobject, LineEnding, Position, Range, Rope, RopeGraphemes, - RopeSlice, Selection, SmallVec, Tendril, Transaction, + search, selection, surround, textobject, + unicode::width::UnicodeWidthChar, + LineEnding, Position, Range, Rope, RopeGraphemes, RopeSlice, Selection, SmallVec, Tendril, + Transaction, }; - use helix_view::{ clipboard::ClipboardType, document::{Mode, SCRATCH_BUFFER_NAME}, @@ -4014,19 +4015,70 @@ pub mod insert { doc.apply(&transaction, view.id); } - // TODO: handle indent-aware delete pub fn delete_char_backward(cx: &mut Context) { let count = cx.count(); let (view, doc) = current!(cx.editor); let text = doc.text().slice(..); + let indent_unit = doc.indent_unit(); + let tab_size = doc.tab_width(); + let transaction = Transaction::change_by_selection(doc.text(), doc.selection(view.id), |range| { let pos = range.cursor(text); - ( - graphemes::nth_prev_grapheme_boundary(text, pos, count), - pos, - None, - ) + let line_start_pos = text.line_to_char(range.cursor_line(text)); + // considier to delete by indent level if all characters before `pos` are indent units. + let fragment = Cow::from(text.slice(line_start_pos..pos)); + if !fragment.is_empty() && fragment.chars().all(|ch| ch.is_whitespace()) { + if text.get_char(pos.saturating_sub(1)) == Some('\t') { + // fast path, delete one char + ( + graphemes::nth_prev_grapheme_boundary(text, pos, 1), + pos, + None, + ) + } else { + let unit_len = indent_unit.chars().count(); + // NOTE: indent_unit always contains 'only spaces' or 'only tab' according to `IndentStyle` definition. + let unit_size = if indent_unit.starts_with('\t') { + tab_size * unit_len + } else { + unit_len + }; + let width: usize = fragment + .chars() + .map(|ch| { + if ch == '\t' { + tab_size + } else { + // it can be none if it still meet control characters other than '\t' + // here just set the width to 1 (or some value better?). + ch.width().unwrap_or(1) + } + }) + .sum(); + let mut drop = width % unit_size; // round down to nearest unit + if drop == 0 { + drop = unit_size + }; // if it's already at a unit, consume a whole unit + let mut chars = fragment.chars().rev(); + let mut start = pos; + for _ in 0..drop { + // delete up to `drop` spaces + match chars.next() { + Some(' ') => start -= 1, + _ => break, + } + } + (start, pos, None) // delete! + } + } else { + // delete char + ( + graphemes::nth_prev_grapheme_boundary(text, pos, count), + pos, + None, + ) + } }); doc.apply(&transaction, view.id); } -- cgit v1.2.3-70-g09d2 From 2b7c0866538676de4e5738d82e450163ff733104 Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Fri, 19 Nov 2021 12:06:19 +0900 Subject: fix: Expand tilde first, then deal with relative paths Otherwise the ~ gets treated as a relative path. Fixes #1107 --- helix-core/src/path.rs | 4 ++-- helix-term/src/commands.rs | 5 +---- 2 files changed, 3 insertions(+), 6 deletions(-) (limited to 'helix-term/src/commands.rs') diff --git a/helix-core/src/path.rs b/helix-core/src/path.rs index 6c37cfa1..a6644465 100644 --- a/helix-core/src/path.rs +++ b/helix-core/src/path.rs @@ -40,7 +40,6 @@ pub fn expand_tilde(path: &Path) -> PathBuf { /// needs to improve on. /// Copied from cargo: pub fn get_normalized_path(path: &Path) -> PathBuf { - let path = expand_tilde(path); let mut components = path.components().peekable(); let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() { components.next(); @@ -72,10 +71,11 @@ pub fn get_normalized_path(path: &Path) -> PathBuf { /// This function is used instead of `std::fs::canonicalize` because we don't want to verify /// here if the path exists, just normalize it's components. pub fn get_canonicalized_path(path: &Path) -> std::io::Result { + let path = expand_tilde(path); let path = if path.is_relative() { std::env::current_dir().map(|current_dir| current_dir.join(path))? } else { - path.to_path_buf() + path }; Ok(get_normalized_path(path.as_path())) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index e5cf7bb2..431265cd 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1767,11 +1767,8 @@ mod cmd { args: &[&str], _event: PromptEvent, ) -> anyhow::Result<()> { - use helix_core::path::expand_tilde; let path = args.get(0).context("wrong argument count")?; - let _ = cx - .editor - .open(expand_tilde(Path::new(path)), Action::Replace)?; + let _ = cx.editor.open(path.into(), Action::Replace)?; Ok(()) } -- cgit v1.2.3-70-g09d2