Add an Amp-like jump command

This commit is contained in:
Michael Davis 2023-06-21 14:14:34 -05:00
parent 553f8706ab
commit 1b6ef04857
No known key found for this signature in database
7 changed files with 160 additions and 1 deletions

View File

@ -64,6 +64,7 @@ ### `[editor]` Section
| `text-width` | Maximum line length. Used for the `:reflow` command and soft-wrapping if `soft-wrap.wrap-at-text-width` is set | `80` | | `text-width` | Maximum line length. Used for the `:reflow` command and soft-wrapping if `soft-wrap.wrap-at-text-width` is set | `80` |
| `workspace-lsp-roots` | Directories relative to the workspace root that are treated as LSP roots. Should only be set in `.helix/config.toml` | `[]` | | `workspace-lsp-roots` | Directories relative to the workspace root that are treated as LSP roots. Should only be set in `.helix/config.toml` | `[]` |
| `default-line-ending` | The line ending to use for new documents. Can be `native`, `lf`, `crlf`, `ff`, `cr` or `nel`. `native` uses the platform's native line ending (`crlf` on Windows, otherwise `lf`). | `native` | | `default-line-ending` | The line ending to use for new documents. Can be `native`, `lf`, `crlf`, `ff`, `cr` or `nel`. `native` uses the platform's native line ending (`crlf` on Windows, otherwise `lf`). | `native` |
| `jump-label-alphabet` | The set of characters to use when creating jump labels. | `abcdefghijklmnopqrstuvwxyz` |
### `[editor.statusline]` Section ### `[editor.statusline]` Section

View File

@ -307,6 +307,7 @@ #### Interface
| `ui.virtual.inlay-hint.parameter` | Style for inlay hints of kind `parameter` (LSPs are not required to set a kind) | | `ui.virtual.inlay-hint.parameter` | Style for inlay hints of kind `parameter` (LSPs are not required to set a kind) |
| `ui.virtual.inlay-hint.type` | Style for inlay hints of kind `type` (LSPs are not required to set a kind) | | `ui.virtual.inlay-hint.type` | Style for inlay hints of kind `type` (LSPs are not required to set a kind) |
| `ui.virtual.wrap` | Soft-wrap indicator (see the [`editor.soft-wrap` config][editor-section]) | | `ui.virtual.wrap` | Soft-wrap indicator (see the [`editor.soft-wrap` config][editor-section]) |
| `ui.virtual.jump-label` | Style for virtual jump labels |
| `ui.menu` | Code and command completion menus | | `ui.menu` | Code and command completion menus |
| `ui.menu.selected` | Selected autocomplete item | | `ui.menu.selected` | Selected autocomplete item |
| `ui.menu.scroll` | `fg` sets thumb color, `bg` sets track color of scrollbar | | `ui.menu.scroll` | `fg` sets thumb color, `bg` sets track color of scrollbar |

View File

@ -490,6 +490,7 @@ pub fn doc(&self) -> &str {
record_macro, "Record macro", record_macro, "Record macro",
replay_macro, "Replay macro", replay_macro, "Replay macro",
command_palette, "Open command palette", command_palette, "Open command palette",
jump_to_label, "Jump to a two-character label",
); );
} }
@ -5688,3 +5689,107 @@ fn replay_macro(cx: &mut Context) {
cx.editor.macro_replaying.pop(); cx.editor.macro_replaying.pop();
})); }));
} }
fn jump_to_label(cx: &mut Context) {
use helix_core::text_annotations::Overlay;
fn clear_overlays(editor: &mut Editor) {
let (view, doc) = current!(editor);
doc.jump_label_overlays.remove(&view.id);
}
let alphabet = &cx.editor.config().jump_label_alphabet;
let alphabet_chars: Vec<char> = alphabet.chars().collect();
let alphabet_len = alphabet_chars.len();
// Each jump uses a distinct two-character pair from the alphabet.
// For example for the default alphabet labels will be:
// aa, ab, ac, ..., ba, bb, bc, ..., zx, zy, zz
let jump_limit = alphabet_len * alphabet_len;
// Calculate the jump candidates: ranges for any visible words with two or
// more characters.
let mut candidates = Vec::with_capacity(jump_limit);
let (view, doc) = current!(cx.editor);
let text = doc.text().slice(..);
let start = text.line_to_char(text.char_to_line(view.offset.anchor));
// This is not necessarily exact if there is virtual text like soft wrap.
// It's ok though because the extra jump labels will not be rendered.
let end = text.line_to_char(view.estimate_last_doc_line(doc));
let mut word = Range::point(start);
loop {
word = movement::move_next_word_start(text, word, 1);
// The word is on a word longer than 2 characters.
if text
.chars_at(word.anchor)
.take_while(|ch| ch.is_alphabetic())
.take(2)
.count()
== 2
{
candidates.push(word);
}
if word.anchor >= end || candidates.len() == jump_limit {
break;
}
}
// Add label for each jump candidate to the View as virtual text.
let overlays: Vec<_> = candidates
.iter()
.enumerate()
.flat_map(|(i, range)| {
[
Overlay::new(range.anchor, String::from(alphabet_chars[i / alphabet_len])),
Overlay::new(
range.anchor + 1,
String::from(alphabet_chars[i % alphabet_len]),
),
]
})
.collect();
doc.jump_label_overlays.insert(view.id, overlays);
// Accept two characters matching a visible label. Jump to the candidate
// for that label if it exists.
cx.on_next_key(move |cx, event| {
let alphabet = &cx.editor.config().jump_label_alphabet;
let Some(first_index) = event.char().and_then(|ch| alphabet.find(ch)) else {
clear_overlays(cx.editor);
return;
};
// Bail if the given character cannot be a jump label.
if first_index * alphabet_len >= candidates.len() {
clear_overlays(cx.editor);
return;
}
cx.on_next_key(move |cx, event| {
clear_overlays(cx.editor);
let alphabet = &cx.editor.config().jump_label_alphabet;
let Some(second_index) = event.char().and_then(|ch| alphabet.find(ch)) else {
clear_overlays(cx.editor);
return;
};
let index = first_index * alphabet_len + second_index;
if let Some(range) = candidates.get(index) {
let (view, doc) = current!(cx.editor);
// Trim any trailing whitespace left by 'move_next_word_start'.
let head = movement::backwards_skip_while(doc.text().slice(..), range.head, |ch| {
ch.is_whitespace()
})
.unwrap_or(range.head);
doc.set_selection(view.id, Selection::single(range.anchor, head));
}
});
});
}

View File

@ -277,6 +277,9 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
"r" => rename_symbol, "r" => rename_symbol,
"h" => select_references_to_symbol_under_cursor, "h" => select_references_to_symbol_under_cursor,
"?" => command_palette, "?" => command_palette,
// TODO: find a home for this.
"l" => jump_to_label,
}, },
"z" => { "View" "z" => { "View"
"z" | "c" => align_view_center, "z" | "c" => align_view_center,

View File

@ -7,7 +7,7 @@
use helix_core::doc_formatter::TextFormat; use helix_core::doc_formatter::TextFormat;
use helix_core::encoding::Encoding; use helix_core::encoding::Encoding;
use helix_core::syntax::{Highlight, LanguageServerFeature}; use helix_core::syntax::{Highlight, LanguageServerFeature};
use helix_core::text_annotations::InlineAnnotation; use helix_core::text_annotations::{InlineAnnotation, Overlay};
use helix_vcs::{DiffHandle, DiffProviderRegistry}; use helix_vcs::{DiffHandle, DiffProviderRegistry};
use ::parking_lot::Mutex; use ::parking_lot::Mutex;
@ -140,6 +140,9 @@ pub struct Document {
/// update from the LSP /// update from the LSP
pub inlay_hints_oudated: bool, pub inlay_hints_oudated: bool,
/// Overlay labels for jump points in the document, by view.
pub jump_label_overlays: HashMap<ViewId, Vec<Overlay>>,
path: Option<PathBuf>, path: Option<PathBuf>,
encoding: &'static encoding::Encoding, encoding: &'static encoding::Encoding,
has_bom: bool, has_bom: bool,
@ -656,6 +659,7 @@ pub fn from(
selections: HashMap::default(), selections: HashMap::default(),
inlay_hints: HashMap::default(), inlay_hints: HashMap::default(),
inlay_hints_oudated: false, inlay_hints_oudated: false,
jump_label_overlays: HashMap::default(),
indent_style: DEFAULT_INDENT, indent_style: DEFAULT_INDENT,
line_ending, line_ending,
restore_cursor: false, restore_cursor: false,
@ -1231,6 +1235,16 @@ fn apply_impl(
self.diagnostics self.diagnostics
.sort_unstable_by_key(|diagnostic| diagnostic.range); .sort_unstable_by_key(|diagnostic| diagnostic.range);
// Update jump label positions. This should be a no-op most of the time unless
// a document is updated asynchronously while the jump labels are present.
for overlays in self.jump_label_overlays.values_mut() {
changes.update_positions(
overlays
.iter_mut()
.map(|overlay| (&mut overlay.char_idx, Assoc::After)),
);
}
// Update the inlay hint annotations' positions, helping ensure they are displayed in the proper place // Update the inlay hint annotations' positions, helping ensure they are displayed in the proper place
let apply_inlay_hint_changes = |annotations: &mut Rc<[InlineAnnotation]>| { let apply_inlay_hint_changes = |annotations: &mut Rc<[InlineAnnotation]>| {
if let Some(data) = Rc::get_mut(annotations) { if let Some(data) = Rc::get_mut(annotations) {

View File

@ -289,6 +289,9 @@ pub struct Config {
pub default_line_ending: LineEndingConfig, pub default_line_ending: LineEndingConfig,
/// Enables smart tab /// Enables smart tab
pub smart_tab: Option<SmartTabConfig>, pub smart_tab: Option<SmartTabConfig>,
/// Characters used in jump labels.
#[serde(deserialize_with = "deserialize_jump_label_alphabet")]
pub jump_label_alphabet: String,
} }
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Eq, PartialOrd, Ord)] #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Eq, PartialOrd, Ord)]
@ -307,6 +310,30 @@ fn default() -> Self {
} }
} }
fn deserialize_jump_label_alphabet<'de, D>(deserializer: D) -> Result<String, D::Error>
where
D: serde::Deserializer<'de>,
{
use std::collections::HashSet;
let alphabet = String::deserialize(deserializer)?;
if alphabet.is_empty() {
return Err(serde::de::Error::custom(
"the jump list alphabet cannot be empty",
));
}
let mut charset: HashSet<char> = alphabet.chars().collect();
for ch in alphabet.chars() {
if charset.remove(&ch) {
return Err(serde::de::Error::custom(format!(
"duplicate character '{ch}' found in jump label alphabet"
)));
}
}
Ok(alphabet)
}
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)] #[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
pub struct TerminalConfig { pub struct TerminalConfig {
@ -843,6 +870,7 @@ fn default() -> Self {
workspace_lsp_roots: Vec::new(), workspace_lsp_roots: Vec::new(),
default_line_ending: LineEndingConfig::default(), default_line_ending: LineEndingConfig::default(),
smart_tab: Some(SmartTabConfig::default()), smart_tab: Some(SmartTabConfig::default()),
jump_label_alphabet: "abcdefghijklmnopqrstuvwxyz".to_owned(),
} }
} }
} }

View File

@ -415,6 +415,13 @@ pub fn text_annotations<'a>(
) -> TextAnnotations<'a> { ) -> TextAnnotations<'a> {
let mut text_annotations = TextAnnotations::default(); let mut text_annotations = TextAnnotations::default();
if let Some(overlays) = doc.jump_label_overlays.get(&self.id) {
let style = theme
.and_then(|t| t.find_scope_index("ui.virtual.jump-label"))
.map(Highlight);
text_annotations.add_overlay(overlays, style);
}
let DocumentInlayHints { let DocumentInlayHints {
id: _, id: _,
type_inlay_hints, type_inlay_hints,