Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add basic support for LSP snippets #5864

Merged
merged 12 commits into from
Mar 8, 2023
5 changes: 5 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ members = [
"helix-dap",
"helix-loader",
"helix-vcs",
"helix-parsec",
Urgau marked this conversation as resolved.
Show resolved Hide resolved
"xtask",
]

Expand Down
10 changes: 10 additions & 0 deletions helix-core/src/selection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,16 @@ impl Selection {
self.normalize()
}

/// Takes a closure and maps each `Range` over the closure to multiple `Range`s.
pub fn transform_iter<F, I>(mut self, f: F) -> Self
where
F: FnMut(Range) -> I,
I: Iterator<Item = Range>,
{
self.ranges = self.ranges.into_iter().flat_map(f).collect();
self.normalize()
}

// Ensures the selection adheres to the following invariants:
// 1. All ranges are grapheme aligned.
// 2. All ranges are at least 1 character wide, unless at the
Expand Down
1 change: 1 addition & 0 deletions helix-lsp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ homepage = "https://helix-editor.com"
[dependencies]
helix-core = { version = "0.6", path = "../helix-core" }
helix-loader = { version = "0.6", path = "../helix-loader" }
helix-parsec = { version = "0.6", path = "../helix-parsec" }

anyhow = "1.0"
futures-executor = "0.3"
Expand Down
2 changes: 1 addition & 1 deletion helix-lsp/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ impl Client {
text_document: Some(lsp::TextDocumentClientCapabilities {
completion: Some(lsp::CompletionClientCapabilities {
completion_item: Some(lsp::CompletionItemCapability {
snippet_support: Some(false),
snippet_support: Some(true),
resolve_support: Some(lsp::CompletionItemCapabilityResolveSupport {
properties: vec![
String::from("documentation"),
Expand Down
89 changes: 75 additions & 14 deletions helix-lsp/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
mod client;
pub mod jsonrpc;
pub mod snippet;
mod transport;

pub use client::Client;
Expand Down Expand Up @@ -59,6 +60,7 @@ pub mod util {
use super::*;
use helix_core::line_ending::{line_end_byte_index, line_end_char_index};
use helix_core::{diagnostic::NumberOrString, Range, Rope, Selection, Tendril, Transaction};
use helix_core::{smallvec, SmallVec};

/// Converts a diagnostic in the document to [`lsp::Diagnostic`].
///
Expand Down Expand Up @@ -250,26 +252,17 @@ pub mod util {
pub fn generate_transaction_from_completion_edit(
doc: &Rope,
selection: &Selection,
edit: lsp::TextEdit,
offset_encoding: OffsetEncoding,
start_offset: i128,
end_offset: i128,
new_text: String,
) -> Transaction {
let replacement: Option<Tendril> = if edit.new_text.is_empty() {
let replacement: Option<Tendril> = if new_text.is_empty() {
None
} else {
Some(edit.new_text.into())
Some(new_text.into())
};

let text = doc.slice(..);
let primary_cursor = selection.primary().cursor(text);

let start_offset = match lsp_pos_to_pos(doc, edit.range.start, offset_encoding) {
Some(start) => start as i128 - primary_cursor as i128,
None => return Transaction::new(doc),
};
let end_offset = match lsp_pos_to_pos(doc, edit.range.end, offset_encoding) {
Some(end) => end as i128 - primary_cursor as i128,
None => return Transaction::new(doc),
};

Transaction::change_by_selection(doc, selection, |range| {
let cursor = range.cursor(text);
Expand All @@ -281,6 +274,74 @@ pub mod util {
})
}

/// Creates a [Transaction] from the [snippet::Snippet] in a completion response.
/// The transaction applies the edit to all cursors.
pub fn generate_transaction_from_snippet(
doc: &Rope,
selection: &Selection,
start_offset: i128,
end_offset: i128,
snippet: snippet::Snippet,
line_ending: &str,
include_placeholder: bool,
) -> Transaction {
let text = doc.slice(..);

// For each cursor store offsets for the first tabstop
let mut cursor_tabstop_offsets = Vec::<SmallVec<[(i128, i128); 1]>>::new();
let transaction = Transaction::change_by_selection(doc, selection, |range| {
let cursor = range.cursor(text);
let replacement_start = (cursor as i128 + start_offset) as usize;
let replacement_end = (cursor as i128 + end_offset) as usize;
let newline_with_offset = format!(
"{line_ending}{blank:width$}",
line_ending = line_ending,
width = replacement_start - doc.line_to_char(doc.char_to_line(replacement_start)),
blank = ""
);

let (replacement, tabstops) =
snippet::render(&snippet, newline_with_offset, include_placeholder);

let replacement_len = replacement.chars().count();
cursor_tabstop_offsets.push(
tabstops
.first()
.unwrap_or(&smallvec![(replacement_len, replacement_len)])
.iter()
.map(|(from, to)| -> (i128, i128) {
(
*from as i128 - replacement_len as i128,
*to as i128 - replacement_len as i128,
)
})
.collect(),
);

(replacement_start, replacement_end, Some(replacement.into()))
});

// Create new selection based on the cursor tabstop from above
let mut cursor_tabstop_offsets_iter = cursor_tabstop_offsets.iter();
let selection = selection
.clone()
.map(transaction.changes())
.transform_iter(|range| {
cursor_tabstop_offsets_iter
.next()
.unwrap()
.iter()
.map(move |(from, to)| {
Range::new(
(range.anchor as i128 + *from) as usize,
(range.anchor as i128 + *to) as usize,
)
})
});

transaction.with_selection(selection)
}

pub fn generate_transaction_from_edits(
doc: &Rope,
mut edits: Vec<lsp::TextEdit>,
Expand Down
Loading