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>? `` |
|
||||
| `goto_word` | Jump to a two-character label | normal: `` 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;
|
||||
mod file_operations;
|
||||
pub mod jsonrpc;
|
||||
pub mod snippet;
|
||||
mod transport;
|
||||
|
||||
use arc_swap::ArcSwap;
|
||||
@ -67,7 +66,8 @@ pub enum OffsetEncoding {
|
||||
pub mod util {
|
||||
use super::*;
|
||||
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};
|
||||
|
||||
/// Converts a diagnostic in the document to [`lsp::Diagnostic`].
|
||||
@ -355,25 +355,17 @@ pub fn generate_transaction_from_completion_edit(
|
||||
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.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn generate_transaction_from_snippet(
|
||||
doc: &Rope,
|
||||
selection: &Selection,
|
||||
edit_offset: Option<(i128, i128)>,
|
||||
replace_mode: bool,
|
||||
snippet: snippet::Snippet,
|
||||
line_ending: &str,
|
||||
include_placeholder: bool,
|
||||
tab_width: usize,
|
||||
indent_width: usize,
|
||||
) -> Transaction {
|
||||
snippet: Snippet,
|
||||
cx: &mut SnippetRenderCtx,
|
||||
) -> (Transaction, RenderedSnippet) {
|
||||
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(
|
||||
text,
|
||||
edit_offset,
|
||||
@ -382,8 +374,7 @@ pub fn generate_transaction_from_snippet(
|
||||
)
|
||||
.expect("transaction must be valid for primary selection");
|
||||
let removed_text = text.slice(removed_start..removed_end);
|
||||
|
||||
let (transaction, mut selection) = Transaction::change_by_selection_ignore_overlapping(
|
||||
let (transaction, mapped_selection, snippet) = snippet.render(
|
||||
doc,
|
||||
selection,
|
||||
|range| {
|
||||
@ -392,108 +383,15 @@ pub fn generate_transaction_from_snippet(
|
||||
.filter(|(start, end)| text.slice(start..end) == removed_text)
|
||||
.unwrap_or_else(|| find_completion_range(text, replace_mode, cursor))
|
||||
},
|
||||
|replacement_start, replacement_end| {
|
||||
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 = ""
|
||||
cx,
|
||||
);
|
||||
|
||||
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 changes = transaction.changes();
|
||||
if changes.is_empty() {
|
||||
return transaction;
|
||||
}
|
||||
|
||||
// Don't normalize to avoid merging/reording selections which would
|
||||
// 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))
|
||||
let transaction = transaction.with_selection(snippet.first_selection(
|
||||
// we keep the direction of the old primary selection in case it changed during mapping
|
||||
// but use the primary idx from the mapped selection in case ranges had to be merged
|
||||
selection.primary().direction(),
|
||||
mapped_selection.primary_index(),
|
||||
));
|
||||
(transaction, snippet)
|
||||
}
|
||||
|
||||
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",
|
||||
goto_word, "Jump 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 doc.active_snippet.is_some() {
|
||||
goto_next_tabstop(cx);
|
||||
} else {
|
||||
move_parent_node_end(cx);
|
||||
}
|
||||
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) {
|
||||
if let Some((reg, mut keys)) = cx.editor.macro_recording.take() {
|
||||
// Remove the keypress which ends the recording
|
||||
|
@ -16,6 +16,7 @@
|
||||
pub mod completion;
|
||||
mod diagnostics;
|
||||
mod signature_help;
|
||||
mod snippet;
|
||||
|
||||
pub fn setup(config: Arc<ArcSwap<Config>>) -> Handlers {
|
||||
events::register();
|
||||
@ -34,5 +35,6 @@ pub fn setup(config: Arc<ArcSwap<Config>>) -> Handlers {
|
||||
signature_help::register_hooks(&handlers);
|
||||
auto_save::register_hooks(&handlers);
|
||||
diagnostics::register_hooks(&handlers);
|
||||
snippet::register_hooks(&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 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 crate::ui::{menu, Markdown, Menu, Popup, PromptEvent};
|
||||
@ -133,101 +137,6 @@ pub fn new(
|
||||
|
||||
// Then create the menu
|
||||
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);
|
||||
|
||||
macro_rules! language_server {
|
||||
@ -272,18 +181,17 @@ macro_rules! language_server {
|
||||
let item = item.unwrap();
|
||||
|
||||
match item {
|
||||
CompletionItem::Lsp(item) => doc.apply_temporary(
|
||||
&lsp_item_to_transaction(
|
||||
CompletionItem::Lsp(item) => {
|
||||
let (transaction, _) = lsp_item_to_transaction(
|
||||
doc,
|
||||
view.id,
|
||||
&item.item,
|
||||
language_server!(item).offset_encoding(),
|
||||
trigger_offset,
|
||||
true,
|
||||
replace_mode,
|
||||
),
|
||||
view.id,
|
||||
),
|
||||
);
|
||||
doc.apply_temporary(&transaction, view.id)
|
||||
}
|
||||
CompletionItem::Other(core::CompletionItem { transaction, .. }) => {
|
||||
doc.apply_temporary(transaction, view.id)
|
||||
}
|
||||
@ -303,7 +211,7 @@ macro_rules! language_server {
|
||||
doc.append_changes_to_history(view);
|
||||
|
||||
// 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) => {
|
||||
let language_server = language_server!(item);
|
||||
|
||||
@ -318,29 +226,40 @@ macro_rules! language_server {
|
||||
};
|
||||
|
||||
let encoding = language_server.offset_encoding();
|
||||
let transaction = lsp_item_to_transaction(
|
||||
let (transaction, snippet) = lsp_item_to_transaction(
|
||||
doc,
|
||||
view.id,
|
||||
&item.item,
|
||||
encoding,
|
||||
trigger_offset,
|
||||
false,
|
||||
replace_mode,
|
||||
);
|
||||
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, .. }) => {
|
||||
(transaction, None)
|
||||
(transaction, None, None)
|
||||
}
|
||||
};
|
||||
|
||||
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 {
|
||||
trigger_offset,
|
||||
changes: completion_changes(&transaction, trigger_offset),
|
||||
placeholder,
|
||||
});
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
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 let Some(tabstops) = Self::tabstop_highlights(doc, theme) {
|
||||
overlay_highlights = Box::new(syntax::merge(overlay_highlights, tabstops));
|
||||
}
|
||||
let highlights = syntax::merge(
|
||||
overlay_highlights,
|
||||
Self::doc_selection_highlights(
|
||||
@ -592,6 +595,24 @@ pub fn highlight_focused_view_elements(
|
||||
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
|
||||
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
|
||||
@ -1055,24 +1076,38 @@ pub fn set_completion(
|
||||
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;
|
||||
let mut on_next_key: Option<OnKeyCallback> = None;
|
||||
if let Some(last_completion) = editor.last_completion.take() {
|
||||
match last_completion {
|
||||
CompleteAction::Triggered => (),
|
||||
CompleteAction::Applied {
|
||||
trigger_offset,
|
||||
changes,
|
||||
} => self.last_insert.1.push(InsertEvent::CompletionApply {
|
||||
placeholder,
|
||||
} => {
|
||||
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 } => {
|
||||
let (view, doc) = current!(editor);
|
||||
doc.restore(view, &savepoint, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
on_next_key
|
||||
}
|
||||
|
||||
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 callback.is_some() {
|
||||
// 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::doc_formatter::TextFormat;
|
||||
use helix_core::encoding::Encoding;
|
||||
use helix_core::snippets::{ActiveSnippet, SnippetRenderCtx};
|
||||
use helix_core::syntax::{Highlight, LanguageServerFeature};
|
||||
use helix_core::text_annotations::{InlineAnnotation, Overlay};
|
||||
use helix_lsp::util::lsp_pos_to_pos;
|
||||
@ -135,6 +136,7 @@ pub struct Document {
|
||||
text: Rope,
|
||||
selections: HashMap<ViewId, Selection>,
|
||||
view_data: HashMap<ViewId, ViewData>,
|
||||
pub active_snippet: Option<ActiveSnippet>,
|
||||
|
||||
/// Inlay hints annotations for the document, by view.
|
||||
///
|
||||
@ -655,6 +657,7 @@ pub fn from(
|
||||
|
||||
Self {
|
||||
id: DocumentId::default(),
|
||||
active_snippet: None,
|
||||
path: None,
|
||||
encoding,
|
||||
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 {
|
||||
let config = self.config.load();
|
||||
let text_width = self
|
||||
|
@ -1137,6 +1137,7 @@ pub enum CompleteAction {
|
||||
Applied {
|
||||
trigger_offset: usize,
|
||||
changes: Vec<Change>,
|
||||
placeholder: bool,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -27,6 +27,7 @@ string = "silver"
|
||||
"constant.character.escape" = "honey"
|
||||
# used for lifetimes
|
||||
label = "honey"
|
||||
tabstop = { modifiers = ["italic"], bg = "bossanova" }
|
||||
|
||||
"markup.heading" = "lilac"
|
||||
"markup.bold" = { modifiers = ["bold"] }
|
||||
|
Loading…
Reference in New Issue
Block a user