mirror of
https://github.com/helix-editor/helix.git
synced 2024-11-22 01:16:18 +04:00
Add an Amp-like jump command
This commit is contained in:
parent
553f8706ab
commit
1b6ef04857
@ -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
|
||||||
|
|
||||||
|
@ -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 |
|
||||||
|
@ -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));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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) {
|
||||||
|
@ -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(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
Loading…
Reference in New Issue
Block a user