Skip to content

Commit

Permalink
implement snippet tabstop support
Browse files Browse the repository at this point in the history
  • Loading branch information
pascalkuthe committed Apr 1, 2024
1 parent 0889a48 commit de9f66e
Show file tree
Hide file tree
Showing 10 changed files with 261 additions and 1,235 deletions.
132 changes: 15 additions & 117 deletions helix-lsp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ mod client;
pub mod file_event;
mod file_operations;
pub mod jsonrpc;
pub mod snippet;
mod transport;

use arc_swap::ArcSwap;
Expand Down Expand Up @@ -65,7 +64,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`].
Expand Down Expand Up @@ -354,25 +354,17 @@ pub mod util {
transaction.with_selection(selection)
}

/// Creates a [Transaction] from the [snippet::Snippet] in a completion response.
/// Creates a [Transaction] from the [Snippet](helix_core::snippets::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,
Expand All @@ -381,8 +373,7 @@ pub mod util {
)
.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| {
Expand All @@ -391,108 +382,15 @@ pub mod util {
.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 = ""
);

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)
},
cx,
);

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(
Expand Down

0 comments on commit de9f66e

Please sign in to comment.