mirror of
https://github.com/helix-editor/helix.git
synced 2024-12-18 14:01:55 +04:00
implement snippet tabstop support
This commit is contained in:
parent
66fb1e67c0
commit
1badd9e434
@ -292,3 +292,5 @@
|
|||||||
| `command_palette` | Open command palette | normal: `` <space>? ``, select: `` <space>? `` |
|
| `command_palette` | Open command palette | normal: `` <space>? ``, select: `` <space>? `` |
|
||||||
| `goto_word` | Jump to a two-character label | normal: `` gw `` |
|
| `goto_word` | Jump to a two-character label | normal: `` gw `` |
|
||||||
| `extend_to_word` | Extend to a two-character label | select: `` gw `` |
|
| `extend_to_word` | Extend to a two-character label | select: `` gw `` |
|
||||||
|
| `goto_next_tabstop` | goto next snippet placeholder | |
|
||||||
|
| `goto_prev_tabstop` | goto next snippet placeholder | |
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
pub mod file_event;
|
pub mod file_event;
|
||||||
mod file_operations;
|
mod file_operations;
|
||||||
pub mod jsonrpc;
|
pub mod jsonrpc;
|
||||||
pub mod snippet;
|
|
||||||
mod transport;
|
mod transport;
|
||||||
|
|
||||||
use arc_swap::ArcSwap;
|
use arc_swap::ArcSwap;
|
||||||
@ -67,7 +66,8 @@ pub enum OffsetEncoding {
|
|||||||
pub mod util {
|
pub mod util {
|
||||||
use super::*;
|
use super::*;
|
||||||
use helix_core::line_ending::{line_end_byte_index, line_end_char_index};
|
use helix_core::line_ending::{line_end_byte_index, line_end_char_index};
|
||||||
use helix_core::{chars, RopeSlice, SmallVec};
|
use helix_core::snippets::{RenderedSnippet, Snippet, SnippetRenderCtx};
|
||||||
|
use helix_core::{chars, RopeSlice};
|
||||||
use helix_core::{diagnostic::NumberOrString, Range, Rope, Selection, Tendril, Transaction};
|
use helix_core::{diagnostic::NumberOrString, Range, Rope, Selection, Tendril, Transaction};
|
||||||
|
|
||||||
/// Converts a diagnostic in the document to [`lsp::Diagnostic`].
|
/// Converts a diagnostic in the document to [`lsp::Diagnostic`].
|
||||||
@ -355,25 +355,17 @@ pub fn generate_transaction_from_completion_edit(
|
|||||||
transaction.with_selection(selection)
|
transaction.with_selection(selection)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a [Transaction] from the [snippet::Snippet] in a completion response.
|
/// Creates a [Transaction] from the [Snippet] in a completion response.
|
||||||
/// The transaction applies the edit to all cursors.
|
/// The transaction applies the edit to all cursors.
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
pub fn generate_transaction_from_snippet(
|
pub fn generate_transaction_from_snippet(
|
||||||
doc: &Rope,
|
doc: &Rope,
|
||||||
selection: &Selection,
|
selection: &Selection,
|
||||||
edit_offset: Option<(i128, i128)>,
|
edit_offset: Option<(i128, i128)>,
|
||||||
replace_mode: bool,
|
replace_mode: bool,
|
||||||
snippet: snippet::Snippet,
|
snippet: Snippet,
|
||||||
line_ending: &str,
|
cx: &mut SnippetRenderCtx,
|
||||||
include_placeholder: bool,
|
) -> (Transaction, RenderedSnippet) {
|
||||||
tab_width: usize,
|
|
||||||
indent_width: usize,
|
|
||||||
) -> Transaction {
|
|
||||||
let text = doc.slice(..);
|
let text = doc.slice(..);
|
||||||
|
|
||||||
let mut off = 0i128;
|
|
||||||
let mut mapped_doc = doc.clone();
|
|
||||||
let mut selection_tabstops: SmallVec<[_; 1]> = SmallVec::new();
|
|
||||||
let (removed_start, removed_end) = completion_range(
|
let (removed_start, removed_end) = completion_range(
|
||||||
text,
|
text,
|
||||||
edit_offset,
|
edit_offset,
|
||||||
@ -382,8 +374,7 @@ pub fn generate_transaction_from_snippet(
|
|||||||
)
|
)
|
||||||
.expect("transaction must be valid for primary selection");
|
.expect("transaction must be valid for primary selection");
|
||||||
let removed_text = text.slice(removed_start..removed_end);
|
let removed_text = text.slice(removed_start..removed_end);
|
||||||
|
let (transaction, mapped_selection, snippet) = snippet.render(
|
||||||
let (transaction, mut selection) = Transaction::change_by_selection_ignore_overlapping(
|
|
||||||
doc,
|
doc,
|
||||||
selection,
|
selection,
|
||||||
|range| {
|
|range| {
|
||||||
@ -392,108 +383,15 @@ pub fn generate_transaction_from_snippet(
|
|||||||
.filter(|(start, end)| text.slice(start..end) == removed_text)
|
.filter(|(start, end)| text.slice(start..end) == removed_text)
|
||||||
.unwrap_or_else(|| find_completion_range(text, replace_mode, cursor))
|
.unwrap_or_else(|| find_completion_range(text, replace_mode, cursor))
|
||||||
},
|
},
|
||||||
|replacement_start, replacement_end| {
|
cx,
|
||||||
let mapped_replacement_start = (replacement_start as i128 + off) as usize;
|
|
||||||
let mapped_replacement_end = (replacement_end as i128 + off) as usize;
|
|
||||||
|
|
||||||
let line_idx = mapped_doc.char_to_line(mapped_replacement_start);
|
|
||||||
let indent_level = helix_core::indent::indent_level_for_line(
|
|
||||||
mapped_doc.line(line_idx),
|
|
||||||
tab_width,
|
|
||||||
indent_width,
|
|
||||||
) * indent_width;
|
|
||||||
|
|
||||||
let newline_with_offset = format!(
|
|
||||||
"{line_ending}{blank:indent_level$}",
|
|
||||||
line_ending = line_ending,
|
|
||||||
blank = ""
|
|
||||||
);
|
|
||||||
|
|
||||||
let (replacement, tabstops) =
|
|
||||||
snippet::render(&snippet, &newline_with_offset, include_placeholder);
|
|
||||||
selection_tabstops.push((mapped_replacement_start, tabstops));
|
|
||||||
mapped_doc.remove(mapped_replacement_start..mapped_replacement_end);
|
|
||||||
mapped_doc.insert(mapped_replacement_start, &replacement);
|
|
||||||
off +=
|
|
||||||
replacement_start as i128 - replacement_end as i128 + replacement.len() as i128;
|
|
||||||
|
|
||||||
Some(replacement)
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
let transaction = transaction.with_selection(snippet.first_selection(
|
||||||
let changes = transaction.changes();
|
// we keep the direction of the old primary selection in case it changed during mapping
|
||||||
if changes.is_empty() {
|
// but use the primary idx from the mapped selection in case ranges had to be merged
|
||||||
return transaction;
|
selection.primary().direction(),
|
||||||
}
|
mapped_selection.primary_index(),
|
||||||
|
));
|
||||||
// Don't normalize to avoid merging/reording selections which would
|
(transaction, snippet)
|
||||||
// break the association between tabstops and selections. Most ranges
|
|
||||||
// will be replaced by tabstops anyways and the final selection will be
|
|
||||||
// normalized anyways
|
|
||||||
selection = selection.map_no_normalize(changes);
|
|
||||||
let mut mapped_selection = SmallVec::with_capacity(selection.len());
|
|
||||||
let mut mapped_primary_idx = 0;
|
|
||||||
let primary_range = selection.primary();
|
|
||||||
for (range, (tabstop_anchor, tabstops)) in selection.into_iter().zip(selection_tabstops) {
|
|
||||||
if range == primary_range {
|
|
||||||
mapped_primary_idx = mapped_selection.len()
|
|
||||||
}
|
|
||||||
|
|
||||||
let tabstops = tabstops.first().filter(|tabstops| !tabstops.is_empty());
|
|
||||||
let Some(tabstops) = tabstops else {
|
|
||||||
// no tabstop normal mapping
|
|
||||||
mapped_selection.push(range);
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
// expand the selection to cover the tabstop to retain the helix selection semantic
|
|
||||||
// the tabstop closest to the range simply replaces `head` while anchor remains in place
|
|
||||||
// the remaining tabstops receive their own single-width cursor
|
|
||||||
if range.head < range.anchor {
|
|
||||||
let last_idx = tabstops.len() - 1;
|
|
||||||
let last_tabstop = tabstop_anchor + tabstops[last_idx].0;
|
|
||||||
|
|
||||||
// if selection is forward but was moved to the right it is
|
|
||||||
// contained entirely in the replacement text, just do a point
|
|
||||||
// selection (fallback below)
|
|
||||||
if range.anchor > last_tabstop {
|
|
||||||
let range = Range::new(range.anchor, last_tabstop);
|
|
||||||
mapped_selection.push(range);
|
|
||||||
let rem_tabstops = tabstops[..last_idx]
|
|
||||||
.iter()
|
|
||||||
.map(|tabstop| Range::point(tabstop_anchor + tabstop.0));
|
|
||||||
mapped_selection.extend(rem_tabstops);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let first_tabstop = tabstop_anchor + tabstops[0].0;
|
|
||||||
|
|
||||||
// if selection is forward but was moved to the right it is
|
|
||||||
// contained entirely in the replacement text, just do a point
|
|
||||||
// selection (fallback below)
|
|
||||||
if range.anchor < first_tabstop {
|
|
||||||
// we can't properly compute the the next grapheme
|
|
||||||
// here because the transaction hasn't been applied yet
|
|
||||||
// that is not a problem because the range gets grapheme aligned anyway
|
|
||||||
// tough so just adding one will always cause head to be grapheme
|
|
||||||
// aligned correctly when applied to the document
|
|
||||||
let range = Range::new(range.anchor, first_tabstop + 1);
|
|
||||||
mapped_selection.push(range);
|
|
||||||
let rem_tabstops = tabstops[1..]
|
|
||||||
.iter()
|
|
||||||
.map(|tabstop| Range::point(tabstop_anchor + tabstop.0));
|
|
||||||
mapped_selection.extend(rem_tabstops);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let tabstops = tabstops
|
|
||||||
.iter()
|
|
||||||
.map(|tabstop| Range::point(tabstop_anchor + tabstop.0));
|
|
||||||
mapped_selection.extend(tabstops);
|
|
||||||
}
|
|
||||||
|
|
||||||
transaction.with_selection(Selection::new(mapped_selection, mapped_primary_idx))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate_transaction_from_edits(
|
pub fn generate_transaction_from_edits(
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -585,6 +585,8 @@ pub fn doc(&self) -> &str {
|
|||||||
command_palette, "Open command palette",
|
command_palette, "Open command palette",
|
||||||
goto_word, "Jump to a two-character label",
|
goto_word, "Jump to a two-character label",
|
||||||
extend_to_word, "Extend to a two-character label",
|
extend_to_word, "Extend to a two-character label",
|
||||||
|
goto_next_tabstop, "goto next snippet placeholder",
|
||||||
|
goto_prev_tabstop, "goto next snippet placeholder",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3948,7 +3950,11 @@ pub fn smart_tab(cx: &mut Context) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if !cursors_after_whitespace {
|
if !cursors_after_whitespace {
|
||||||
move_parent_node_end(cx);
|
if doc.active_snippet.is_some() {
|
||||||
|
goto_next_tabstop(cx);
|
||||||
|
} else {
|
||||||
|
move_parent_node_end(cx);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -6187,6 +6193,47 @@ fn increment_impl(cx: &mut Context, increment_direction: IncrementDirection) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn goto_next_tabstop(cx: &mut Context) {
|
||||||
|
goto_next_tabstop_impl(cx, Direction::Forward)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn goto_prev_tabstop(cx: &mut Context) {
|
||||||
|
goto_next_tabstop_impl(cx, Direction::Backward)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn goto_next_tabstop_impl(cx: &mut Context, direction: Direction) {
|
||||||
|
let (view, doc) = current!(cx.editor);
|
||||||
|
let view_id = view.id;
|
||||||
|
let Some(mut snippet) = doc.active_snippet.take() else {
|
||||||
|
cx.editor.set_error("no snippet is currently active");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let tabstop = match direction {
|
||||||
|
Direction::Forward => Some(snippet.next_tabstop(doc.selection(view_id))),
|
||||||
|
Direction::Backward => snippet
|
||||||
|
.prev_tabstop(doc.selection(view_id))
|
||||||
|
.map(|selection| (selection, false)),
|
||||||
|
};
|
||||||
|
let Some((selection, last_tabstop)) = tabstop else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
doc.set_selection(view_id, selection);
|
||||||
|
if !last_tabstop {
|
||||||
|
doc.active_snippet = Some(snippet)
|
||||||
|
}
|
||||||
|
if cx.editor.mode() == Mode::Insert {
|
||||||
|
cx.on_next_key_fallback(|cx, key| {
|
||||||
|
if let Some(c) = key.char() {
|
||||||
|
let (view, doc) = current!(cx.editor);
|
||||||
|
if let Some(snippet) = &doc.active_snippet {
|
||||||
|
doc.apply(&snippet.delete_placeholder(doc.text()), view.id);
|
||||||
|
}
|
||||||
|
insert_char(cx, c);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn record_macro(cx: &mut Context) {
|
fn record_macro(cx: &mut Context) {
|
||||||
if let Some((reg, mut keys)) = cx.editor.macro_recording.take() {
|
if let Some((reg, mut keys)) = cx.editor.macro_recording.take() {
|
||||||
// Remove the keypress which ends the recording
|
// Remove the keypress which ends the recording
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
pub mod completion;
|
pub mod completion;
|
||||||
mod diagnostics;
|
mod diagnostics;
|
||||||
mod signature_help;
|
mod signature_help;
|
||||||
|
mod snippet;
|
||||||
|
|
||||||
pub fn setup(config: Arc<ArcSwap<Config>>) -> Handlers {
|
pub fn setup(config: Arc<ArcSwap<Config>>) -> Handlers {
|
||||||
events::register();
|
events::register();
|
||||||
@ -34,5 +35,6 @@ pub fn setup(config: Arc<ArcSwap<Config>>) -> Handlers {
|
|||||||
signature_help::register_hooks(&handlers);
|
signature_help::register_hooks(&handlers);
|
||||||
auto_save::register_hooks(&handlers);
|
auto_save::register_hooks(&handlers);
|
||||||
diagnostics::register_hooks(&handlers);
|
diagnostics::register_hooks(&handlers);
|
||||||
|
snippet::register_hooks(&handlers);
|
||||||
handlers
|
handlers
|
||||||
}
|
}
|
||||||
|
28
helix-term/src/handlers/snippet.rs
Normal file
28
helix-term/src/handlers/snippet.rs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
use helix_event::register_hook;
|
||||||
|
use helix_view::events::{DocumentDidChange, DocumentFocusLost, SelectionDidChange};
|
||||||
|
use helix_view::handlers::Handlers;
|
||||||
|
|
||||||
|
pub(super) fn register_hooks(_handlers: &Handlers) {
|
||||||
|
register_hook!(move |event: &mut SelectionDidChange<'_>| {
|
||||||
|
if let Some(snippet) = &event.doc.active_snippet {
|
||||||
|
if !snippet.is_valid(event.doc.selection(event.view)) {
|
||||||
|
event.doc.active_snippet = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
register_hook!(move |event: &mut DocumentDidChange<'_>| {
|
||||||
|
if let Some(snippet) = &mut event.doc.active_snippet {
|
||||||
|
let invalid = snippet.map(event.changes);
|
||||||
|
if invalid {
|
||||||
|
event.doc.active_snippet = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
register_hook!(move |event: &mut DocumentFocusLost<'_>| {
|
||||||
|
let editor = &mut event.editor;
|
||||||
|
doc_mut!(editor).active_snippet = None;
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
}
|
@ -16,7 +16,11 @@
|
|||||||
|
|
||||||
use std::{borrow::Cow, sync::Arc};
|
use std::{borrow::Cow, sync::Arc};
|
||||||
|
|
||||||
use helix_core::{self as core, chars, Change, Transaction};
|
use helix_core::{
|
||||||
|
self as core, chars,
|
||||||
|
snippets::{ActiveSnippet, RenderedSnippet, Snippet},
|
||||||
|
Change, Transaction,
|
||||||
|
};
|
||||||
use helix_view::{graphics::Rect, Document, Editor};
|
use helix_view::{graphics::Rect, Document, Editor};
|
||||||
|
|
||||||
use crate::ui::{menu, Markdown, Menu, Popup, PromptEvent};
|
use crate::ui::{menu, Markdown, Menu, Popup, PromptEvent};
|
||||||
@ -133,101 +137,6 @@ pub fn new(
|
|||||||
|
|
||||||
// Then create the menu
|
// Then create the menu
|
||||||
let menu = Menu::new(items, (), move |editor: &mut Editor, item, event| {
|
let menu = Menu::new(items, (), move |editor: &mut Editor, item, event| {
|
||||||
fn lsp_item_to_transaction(
|
|
||||||
doc: &Document,
|
|
||||||
view_id: ViewId,
|
|
||||||
item: &lsp::CompletionItem,
|
|
||||||
offset_encoding: OffsetEncoding,
|
|
||||||
trigger_offset: usize,
|
|
||||||
include_placeholder: bool,
|
|
||||||
replace_mode: bool,
|
|
||||||
) -> Transaction {
|
|
||||||
use helix_lsp::snippet;
|
|
||||||
let selection = doc.selection(view_id);
|
|
||||||
let text = doc.text().slice(..);
|
|
||||||
let primary_cursor = selection.primary().cursor(text);
|
|
||||||
|
|
||||||
let (edit_offset, new_text) = if let Some(edit) = &item.text_edit {
|
|
||||||
let edit = match edit {
|
|
||||||
lsp::CompletionTextEdit::Edit(edit) => edit.clone(),
|
|
||||||
lsp::CompletionTextEdit::InsertAndReplace(item) => {
|
|
||||||
let range = if replace_mode {
|
|
||||||
item.replace
|
|
||||||
} else {
|
|
||||||
item.insert
|
|
||||||
};
|
|
||||||
lsp::TextEdit::new(range, item.new_text.clone())
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let Some(range) =
|
|
||||||
util::lsp_range_to_range(doc.text(), edit.range, offset_encoding)
|
|
||||||
else {
|
|
||||||
return Transaction::new(doc.text());
|
|
||||||
};
|
|
||||||
|
|
||||||
let start_offset = range.anchor as i128 - primary_cursor as i128;
|
|
||||||
let end_offset = range.head as i128 - primary_cursor as i128;
|
|
||||||
|
|
||||||
(Some((start_offset, end_offset)), edit.new_text)
|
|
||||||
} else {
|
|
||||||
let new_text = item
|
|
||||||
.insert_text
|
|
||||||
.clone()
|
|
||||||
.unwrap_or_else(|| item.label.clone());
|
|
||||||
// check that we are still at the correct savepoint
|
|
||||||
// we can still generate a transaction regardless but if the
|
|
||||||
// document changed (and not just the selection) then we will
|
|
||||||
// likely delete the wrong text (same if we applied an edit sent by the LS)
|
|
||||||
debug_assert!(primary_cursor == trigger_offset);
|
|
||||||
(None, new_text)
|
|
||||||
};
|
|
||||||
|
|
||||||
if matches!(item.kind, Some(lsp::CompletionItemKind::SNIPPET))
|
|
||||||
|| matches!(
|
|
||||||
item.insert_text_format,
|
|
||||||
Some(lsp::InsertTextFormat::SNIPPET)
|
|
||||||
)
|
|
||||||
{
|
|
||||||
match snippet::parse(&new_text) {
|
|
||||||
Ok(snippet) => util::generate_transaction_from_snippet(
|
|
||||||
doc.text(),
|
|
||||||
selection,
|
|
||||||
edit_offset,
|
|
||||||
replace_mode,
|
|
||||||
snippet,
|
|
||||||
doc.line_ending.as_str(),
|
|
||||||
include_placeholder,
|
|
||||||
doc.tab_width(),
|
|
||||||
doc.indent_width(),
|
|
||||||
),
|
|
||||||
Err(err) => {
|
|
||||||
log::error!(
|
|
||||||
"Failed to parse snippet: {:?}, remaining output: {}",
|
|
||||||
&new_text,
|
|
||||||
err
|
|
||||||
);
|
|
||||||
Transaction::new(doc.text())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
util::generate_transaction_from_completion_edit(
|
|
||||||
doc.text(),
|
|
||||||
selection,
|
|
||||||
edit_offset,
|
|
||||||
replace_mode,
|
|
||||||
new_text,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn completion_changes(transaction: &Transaction, trigger_offset: usize) -> Vec<Change> {
|
|
||||||
transaction
|
|
||||||
.changes_iter()
|
|
||||||
.filter(|(start, end, _)| (*start..=*end).contains(&trigger_offset))
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
let (view, doc) = current!(editor);
|
let (view, doc) = current!(editor);
|
||||||
|
|
||||||
macro_rules! language_server {
|
macro_rules! language_server {
|
||||||
@ -272,18 +181,17 @@ macro_rules! language_server {
|
|||||||
let item = item.unwrap();
|
let item = item.unwrap();
|
||||||
|
|
||||||
match item {
|
match item {
|
||||||
CompletionItem::Lsp(item) => doc.apply_temporary(
|
CompletionItem::Lsp(item) => {
|
||||||
&lsp_item_to_transaction(
|
let (transaction, _) = lsp_item_to_transaction(
|
||||||
doc,
|
doc,
|
||||||
view.id,
|
view.id,
|
||||||
&item.item,
|
&item.item,
|
||||||
language_server!(item).offset_encoding(),
|
language_server!(item).offset_encoding(),
|
||||||
trigger_offset,
|
trigger_offset,
|
||||||
true,
|
|
||||||
replace_mode,
|
replace_mode,
|
||||||
),
|
);
|
||||||
view.id,
|
doc.apply_temporary(&transaction, view.id)
|
||||||
),
|
}
|
||||||
CompletionItem::Other(core::CompletionItem { transaction, .. }) => {
|
CompletionItem::Other(core::CompletionItem { transaction, .. }) => {
|
||||||
doc.apply_temporary(transaction, view.id)
|
doc.apply_temporary(transaction, view.id)
|
||||||
}
|
}
|
||||||
@ -303,7 +211,7 @@ macro_rules! language_server {
|
|||||||
doc.append_changes_to_history(view);
|
doc.append_changes_to_history(view);
|
||||||
|
|
||||||
// item always present here
|
// item always present here
|
||||||
let (transaction, additional_edits) = match item.unwrap().clone() {
|
let (transaction, additional_edits, snippet) = match item.unwrap().clone() {
|
||||||
CompletionItem::Lsp(mut item) => {
|
CompletionItem::Lsp(mut item) => {
|
||||||
let language_server = language_server!(item);
|
let language_server = language_server!(item);
|
||||||
|
|
||||||
@ -318,29 +226,40 @@ macro_rules! language_server {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let encoding = language_server.offset_encoding();
|
let encoding = language_server.offset_encoding();
|
||||||
let transaction = lsp_item_to_transaction(
|
let (transaction, snippet) = lsp_item_to_transaction(
|
||||||
doc,
|
doc,
|
||||||
view.id,
|
view.id,
|
||||||
&item.item,
|
&item.item,
|
||||||
encoding,
|
encoding,
|
||||||
trigger_offset,
|
trigger_offset,
|
||||||
false,
|
|
||||||
replace_mode,
|
replace_mode,
|
||||||
);
|
);
|
||||||
let add_edits = item.item.additional_text_edits;
|
let add_edits = item.item.additional_text_edits;
|
||||||
|
|
||||||
(transaction, add_edits.map(|edits| (edits, encoding)))
|
(
|
||||||
|
transaction,
|
||||||
|
add_edits.map(|edits| (edits, encoding)),
|
||||||
|
snippet,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
CompletionItem::Other(core::CompletionItem { transaction, .. }) => {
|
CompletionItem::Other(core::CompletionItem { transaction, .. }) => {
|
||||||
(transaction, None)
|
(transaction, None, None)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
doc.apply(&transaction, view.id);
|
doc.apply(&transaction, view.id);
|
||||||
|
let placeholder = snippet.is_some();
|
||||||
|
if let Some(snippet) = snippet {
|
||||||
|
doc.active_snippet = match doc.active_snippet.take() {
|
||||||
|
Some(active) => active.insert_subsnippet(snippet),
|
||||||
|
None => ActiveSnippet::new(snippet),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
editor.last_completion = Some(CompleteAction::Applied {
|
editor.last_completion = Some(CompleteAction::Applied {
|
||||||
trigger_offset,
|
trigger_offset,
|
||||||
changes: completion_changes(&transaction, trigger_offset),
|
changes: completion_changes(&transaction, trigger_offset),
|
||||||
|
placeholder,
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: add additional _edits to completion_changes?
|
// TODO: add additional _edits to completion_changes?
|
||||||
@ -581,3 +500,86 @@ fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
|
|||||||
markdown_doc.render(doc_area, surface, cx);
|
markdown_doc.render(doc_area, surface, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fn lsp_item_to_transaction(
|
||||||
|
doc: &Document,
|
||||||
|
view_id: ViewId,
|
||||||
|
item: &lsp::CompletionItem,
|
||||||
|
offset_encoding: OffsetEncoding,
|
||||||
|
trigger_offset: usize,
|
||||||
|
replace_mode: bool,
|
||||||
|
) -> (Transaction, Option<RenderedSnippet>) {
|
||||||
|
let selection = doc.selection(view_id);
|
||||||
|
let text = doc.text().slice(..);
|
||||||
|
let primary_cursor = selection.primary().cursor(text);
|
||||||
|
|
||||||
|
let (edit_offset, new_text) = if let Some(edit) = &item.text_edit {
|
||||||
|
let edit = match edit {
|
||||||
|
lsp::CompletionTextEdit::Edit(edit) => edit.clone(),
|
||||||
|
lsp::CompletionTextEdit::InsertAndReplace(item) => {
|
||||||
|
let range = if replace_mode {
|
||||||
|
item.replace
|
||||||
|
} else {
|
||||||
|
item.insert
|
||||||
|
};
|
||||||
|
lsp::TextEdit::new(range, item.new_text.clone())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(range) = util::lsp_range_to_range(doc.text(), edit.range, offset_encoding) else {
|
||||||
|
return (Transaction::new(doc.text()), None);
|
||||||
|
};
|
||||||
|
|
||||||
|
let start_offset = range.anchor as i128 - primary_cursor as i128;
|
||||||
|
let end_offset = range.head as i128 - primary_cursor as i128;
|
||||||
|
|
||||||
|
(Some((start_offset, end_offset)), edit.new_text)
|
||||||
|
} else {
|
||||||
|
let new_text = item
|
||||||
|
.insert_text
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| item.label.clone());
|
||||||
|
// check that we are still at the correct savepoint
|
||||||
|
// we can still generate a transaction regardless but if the
|
||||||
|
// document changed (and not just the selection) then we will
|
||||||
|
// likely delete the wrong text (same if we applied an edit sent by the LS)
|
||||||
|
debug_assert!(primary_cursor == trigger_offset);
|
||||||
|
(None, new_text)
|
||||||
|
};
|
||||||
|
|
||||||
|
if matches!(item.kind, Some(lsp::CompletionItemKind::SNIPPET))
|
||||||
|
|| matches!(
|
||||||
|
item.insert_text_format,
|
||||||
|
Some(lsp::InsertTextFormat::SNIPPET)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
let Ok(snippet) = Snippet::parse(&new_text) else {
|
||||||
|
log::error!("Failed to parse snippet: {new_text:?}",);
|
||||||
|
return (Transaction::new(doc.text()), None);
|
||||||
|
};
|
||||||
|
let (transaction, snippet) = util::generate_transaction_from_snippet(
|
||||||
|
doc.text(),
|
||||||
|
selection,
|
||||||
|
edit_offset,
|
||||||
|
replace_mode,
|
||||||
|
snippet,
|
||||||
|
&mut doc.snippet_ctx(),
|
||||||
|
);
|
||||||
|
(transaction, Some(snippet))
|
||||||
|
} else {
|
||||||
|
let transaction = util::generate_transaction_from_completion_edit(
|
||||||
|
doc.text(),
|
||||||
|
selection,
|
||||||
|
edit_offset,
|
||||||
|
replace_mode,
|
||||||
|
new_text,
|
||||||
|
);
|
||||||
|
(transaction, None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn completion_changes(transaction: &Transaction, trigger_offset: usize) -> Vec<Change> {
|
||||||
|
transaction
|
||||||
|
.changes_iter()
|
||||||
|
.filter(|(start, end, _)| (*start..=*end).contains(&trigger_offset))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
@ -147,6 +147,9 @@ pub fn render_view(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if is_focused {
|
if is_focused {
|
||||||
|
if let Some(tabstops) = Self::tabstop_highlights(doc, theme) {
|
||||||
|
overlay_highlights = Box::new(syntax::merge(overlay_highlights, tabstops));
|
||||||
|
}
|
||||||
let highlights = syntax::merge(
|
let highlights = syntax::merge(
|
||||||
overlay_highlights,
|
overlay_highlights,
|
||||||
Self::doc_selection_highlights(
|
Self::doc_selection_highlights(
|
||||||
@ -592,6 +595,24 @@ pub fn highlight_focused_view_elements(
|
|||||||
Vec::new()
|
Vec::new()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn tabstop_highlights(
|
||||||
|
doc: &Document,
|
||||||
|
theme: &Theme,
|
||||||
|
) -> Option<Vec<(usize, std::ops::Range<usize>)>> {
|
||||||
|
let snippet = doc.active_snippet.as_ref()?;
|
||||||
|
let highlight = theme.find_scope_index_exact("tabstop")?;
|
||||||
|
let mut highlights = Vec::new();
|
||||||
|
for tabstop in snippet.tabstops() {
|
||||||
|
highlights.extend(
|
||||||
|
tabstop
|
||||||
|
.ranges
|
||||||
|
.iter()
|
||||||
|
.map(|range| (highlight, range.start..range.end)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
(!highlights.is_empty()).then_some(highlights)
|
||||||
|
}
|
||||||
|
|
||||||
/// Render bufferline at the top
|
/// Render bufferline at the top
|
||||||
pub fn render_bufferline(editor: &Editor, viewport: Rect, surface: &mut Surface) {
|
pub fn render_bufferline(editor: &Editor, viewport: Rect, surface: &mut Surface) {
|
||||||
let scratch = PathBuf::from(SCRATCH_BUFFER_NAME); // default filename to use for scratch buffer
|
let scratch = PathBuf::from(SCRATCH_BUFFER_NAME); // default filename to use for scratch buffer
|
||||||
@ -1055,24 +1076,38 @@ pub fn set_completion(
|
|||||||
Some(area)
|
Some(area)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clear_completion(&mut self, editor: &mut Editor) {
|
pub fn clear_completion(&mut self, editor: &mut Editor) -> Option<OnKeyCallback> {
|
||||||
self.completion = None;
|
self.completion = None;
|
||||||
|
let mut on_next_key: Option<OnKeyCallback> = None;
|
||||||
if let Some(last_completion) = editor.last_completion.take() {
|
if let Some(last_completion) = editor.last_completion.take() {
|
||||||
match last_completion {
|
match last_completion {
|
||||||
CompleteAction::Triggered => (),
|
CompleteAction::Triggered => (),
|
||||||
CompleteAction::Applied {
|
CompleteAction::Applied {
|
||||||
trigger_offset,
|
trigger_offset,
|
||||||
changes,
|
changes,
|
||||||
} => self.last_insert.1.push(InsertEvent::CompletionApply {
|
placeholder,
|
||||||
trigger_offset,
|
} => {
|
||||||
changes,
|
self.last_insert.1.push(InsertEvent::CompletionApply {
|
||||||
}),
|
trigger_offset,
|
||||||
|
changes,
|
||||||
|
});
|
||||||
|
on_next_key = placeholder.then_some(Box::new(|cx, key| {
|
||||||
|
if let Some(c) = key.char() {
|
||||||
|
let (view, doc) = current!(cx.editor);
|
||||||
|
if let Some(snippet) = &doc.active_snippet {
|
||||||
|
doc.apply(&snippet.delete_placeholder(doc.text()), view.id);
|
||||||
|
}
|
||||||
|
commands::insert::insert_char(cx, c);
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
CompleteAction::Selected { savepoint } => {
|
CompleteAction::Selected { savepoint } => {
|
||||||
let (view, doc) = current!(editor);
|
let (view, doc) = current!(editor);
|
||||||
doc.restore(view, &savepoint, false);
|
doc.restore(view, &savepoint, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
on_next_key
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_idle_timeout(&mut self, cx: &mut commands::Context) -> EventResult {
|
pub fn handle_idle_timeout(&mut self, cx: &mut commands::Context) -> EventResult {
|
||||||
@ -1419,7 +1454,15 @@ fn handle_event(
|
|||||||
if let Some(callback) = res {
|
if let Some(callback) = res {
|
||||||
if callback.is_some() {
|
if callback.is_some() {
|
||||||
// assume close_fn
|
// assume close_fn
|
||||||
self.clear_completion(cx.editor);
|
if let Some(cb) = self.clear_completion(cx.editor) {
|
||||||
|
if consumed {
|
||||||
|
cx.on_next_key_callback =
|
||||||
|
Some((cb, OnKeyCallbackKind::Fallback))
|
||||||
|
} else {
|
||||||
|
self.on_next_key =
|
||||||
|
Some((cb, OnKeyCallbackKind::Fallback));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
use helix_core::chars::char_is_word;
|
use helix_core::chars::char_is_word;
|
||||||
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::snippets::{ActiveSnippet, SnippetRenderCtx};
|
||||||
use helix_core::syntax::{Highlight, LanguageServerFeature};
|
use helix_core::syntax::{Highlight, LanguageServerFeature};
|
||||||
use helix_core::text_annotations::{InlineAnnotation, Overlay};
|
use helix_core::text_annotations::{InlineAnnotation, Overlay};
|
||||||
use helix_lsp::util::lsp_pos_to_pos;
|
use helix_lsp::util::lsp_pos_to_pos;
|
||||||
@ -135,6 +136,7 @@ pub struct Document {
|
|||||||
text: Rope,
|
text: Rope,
|
||||||
selections: HashMap<ViewId, Selection>,
|
selections: HashMap<ViewId, Selection>,
|
||||||
view_data: HashMap<ViewId, ViewData>,
|
view_data: HashMap<ViewId, ViewData>,
|
||||||
|
pub active_snippet: Option<ActiveSnippet>,
|
||||||
|
|
||||||
/// Inlay hints annotations for the document, by view.
|
/// Inlay hints annotations for the document, by view.
|
||||||
///
|
///
|
||||||
@ -655,6 +657,7 @@ pub fn from(
|
|||||||
|
|
||||||
Self {
|
Self {
|
||||||
id: DocumentId::default(),
|
id: DocumentId::default(),
|
||||||
|
active_snippet: None,
|
||||||
path: None,
|
path: None,
|
||||||
encoding,
|
encoding,
|
||||||
has_bom,
|
has_bom,
|
||||||
@ -2053,6 +2056,16 @@ pub fn auto_pairs<'a>(&'a self, editor: &'a Editor) -> Option<&'a AutoPairs> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn snippet_ctx(&self) -> SnippetRenderCtx {
|
||||||
|
SnippetRenderCtx {
|
||||||
|
// TODO snippet variable resolution
|
||||||
|
resolve_var: Box::new(|_| None),
|
||||||
|
tab_width: self.tab_width(),
|
||||||
|
indent_style: self.indent_style,
|
||||||
|
line_ending: self.line_ending.as_str(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn text_format(&self, mut viewport_width: u16, theme: Option<&Theme>) -> TextFormat {
|
pub fn text_format(&self, mut viewport_width: u16, theme: Option<&Theme>) -> TextFormat {
|
||||||
let config = self.config.load();
|
let config = self.config.load();
|
||||||
let text_width = self
|
let text_width = self
|
||||||
|
@ -1137,6 +1137,7 @@ pub enum CompleteAction {
|
|||||||
Applied {
|
Applied {
|
||||||
trigger_offset: usize,
|
trigger_offset: usize,
|
||||||
changes: Vec<Change>,
|
changes: Vec<Change>,
|
||||||
|
placeholder: bool,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,6 +27,7 @@ string = "silver"
|
|||||||
"constant.character.escape" = "honey"
|
"constant.character.escape" = "honey"
|
||||||
# used for lifetimes
|
# used for lifetimes
|
||||||
label = "honey"
|
label = "honey"
|
||||||
|
tabstop = { modifiers = ["italic"], bg = "bossanova" }
|
||||||
|
|
||||||
"markup.heading" = "lilac"
|
"markup.heading" = "lilac"
|
||||||
"markup.bold" = { modifiers = ["bold"] }
|
"markup.bold" = { modifiers = ["bold"] }
|
||||||
|
Loading…
Reference in New Issue
Block a user