Utilize LSP completion `itemDefaults` a bit

Julia created

Tailwind likes to throw a lot of completion data at us, this gets it to
send less. Previously it would respond to a completion with 2.5 MB JSON
blob, now it is more like 0.8 MB.

Relies on a local copy of lsp-types with the `itemDefaults` field added.
I don't have write perms to push to our fork of the crate atm, sorry :)

Change summary

Cargo.lock                        |  4 -
crates/lsp/Cargo.toml             |  2 
crates/lsp/src/lsp.rs             |  8 +++
crates/project/src/lsp_command.rs | 69 ++++++++++++++++++++++++--------
4 files changed, 62 insertions(+), 21 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -4185,9 +4185,7 @@ dependencies = [
 
 [[package]]
 name = "lsp-types"
-version = "0.94.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b63735a13a1f9cd4f4835223d828ed9c2e35c8c5e61837774399f558b6a1237"
+version = "0.94.1"
 dependencies = [
  "bitflags 1.3.2",
  "serde",

crates/lsp/Cargo.toml 🔗

@@ -20,7 +20,7 @@ anyhow.workspace = true
 async-pipe = { git = "https://github.com/zed-industries/async-pipe-rs", rev = "82d00a04211cf4e1236029aa03e6b6ce2a74c553", optional = true }
 futures.workspace = true
 log.workspace = true
-lsp-types = "0.94"
+lsp-types = { path = "../../../lsp-types" }
 parking_lot.workspace = true
 postage.workspace = true
 serde.workspace = true

crates/lsp/src/lsp.rs 🔗

@@ -423,6 +423,14 @@ impl LanguageServer {
                             }),
                             ..Default::default()
                         }),
+                        completion_list: Some(CompletionListCapability {
+                            item_defaults: Some(vec![
+                                "commitCharacters".to_owned(),
+                                "editRange".to_owned(),
+                                "insertTextMode".to_owned(),
+                                "data".to_owned(),
+                            ]),
+                        }),
                         ..Default::default()
                     }),
                     rename: Some(RenameClientCapabilities {

crates/project/src/lsp_command.rs 🔗

@@ -15,7 +15,10 @@ use language::{
     range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, CachedLspAdapter, CharKind, CodeAction,
     Completion, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Transaction, Unclipped,
 };
-use lsp::{DocumentHighlightKind, LanguageServer, LanguageServerId, ServerCapabilities};
+use lsp::{
+    CompletionListItemDefaultsEditRange, DocumentHighlightKind, LanguageServer, LanguageServerId,
+    ServerCapabilities,
+};
 use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc};
 
 pub fn lsp_formatting_options(tab_size: u32) -> lsp::FormattingOptions {
@@ -1341,10 +1344,16 @@ impl LspCommand for GetCompletions {
         server_id: LanguageServerId,
         cx: AsyncAppContext,
     ) -> Result<Vec<Completion>> {
+        let mut response_list = None;
         let completions = if let Some(completions) = completions {
             match completions {
                 lsp::CompletionResponse::Array(completions) => completions,
-                lsp::CompletionResponse::List(list) => list.items,
+
+                lsp::CompletionResponse::List(mut list) => {
+                    let items = std::mem::take(&mut list.items);
+                    response_list = Some(list);
+                    items
+                }
             }
         } else {
             Default::default()
@@ -1354,6 +1363,7 @@ impl LspCommand for GetCompletions {
             let language = buffer.language().cloned();
             let snapshot = buffer.snapshot();
             let clipped_position = buffer.clip_point_utf16(Unclipped(self.position), Bias::Left);
+
             let mut range_for_token = None;
             completions
                 .into_iter()
@@ -1374,6 +1384,7 @@ impl LspCommand for GetCompletions {
                                 edit.new_text.clone(),
                             )
                         }
+
                         // If the language server does not provide a range, then infer
                         // the range based on the syntax tree.
                         None => {
@@ -1381,27 +1392,51 @@ impl LspCommand for GetCompletions {
                                 log::info!("completion out of expected range");
                                 return None;
                             }
-                            let Range { start, end } = range_for_token
-                                .get_or_insert_with(|| {
-                                    let offset = self.position.to_offset(&snapshot);
-                                    let (range, kind) = snapshot.surrounding_word(offset);
-                                    if kind == Some(CharKind::Word) {
-                                        range
-                                    } else {
-                                        offset..offset
-                                    }
-                                })
-                                .clone();
+
+                            let default_edit_range = response_list
+                                .as_ref()
+                                .and_then(|list| list.item_defaults.as_ref())
+                                .and_then(|defaults| defaults.edit_range.as_ref())
+                                .and_then(|range| match range {
+                                    CompletionListItemDefaultsEditRange::Range(r) => Some(r),
+                                    _ => None,
+                                });
+
+                            let range = if let Some(range) = default_edit_range {
+                                let range = range_from_lsp(range.clone());
+                                let start = snapshot.clip_point_utf16(range.start, Bias::Left);
+                                let end = snapshot.clip_point_utf16(range.end, Bias::Left);
+                                if start != range.start.0 || end != range.end.0 {
+                                    log::info!("completion out of expected range");
+                                    return None;
+                                }
+
+                                snapshot.anchor_before(start)..snapshot.anchor_after(end)
+                            } else {
+                                range_for_token
+                                    .get_or_insert_with(|| {
+                                        let offset = self.position.to_offset(&snapshot);
+                                        let (range, kind) = snapshot.surrounding_word(offset);
+                                        let range = if kind == Some(CharKind::Word) {
+                                            range
+                                        } else {
+                                            offset..offset
+                                        };
+
+                                        snapshot.anchor_before(range.start)
+                                            ..snapshot.anchor_after(range.end)
+                                    })
+                                    .clone()
+                            };
+
                             let text = lsp_completion
                                 .insert_text
                                 .as_ref()
                                 .unwrap_or(&lsp_completion.label)
                                 .clone();
-                            (
-                                snapshot.anchor_before(start)..snapshot.anchor_after(end),
-                                text,
-                            )
+                            (range, text)
                         }
+
                         Some(lsp::CompletionTextEdit::InsertAndReplace(_)) => {
                             log::info!("unsupported insert/replace completion");
                             return None;