From 51630b27f91f5e162ea2412cc1904d6d0029b29c Mon Sep 17 00:00:00 2001 From: Princesseuh <3019731+Princesseuh@users.noreply.github.com> Date: Sun, 15 Dec 2024 01:53:26 +0100 Subject: [PATCH 1/2] feat(completions): beginning --- crates/csslsrs/src/features/completions.rs | 67 ++++++++++ crates/csslsrs/src/lib.rs | 1 + crates/csslsrs/src/service.rs | 1 + crates/csslsrs/tests/completions.rs | 147 +++++++++++++++++++++ 4 files changed, 216 insertions(+) create mode 100644 crates/csslsrs/src/features/completions.rs create mode 100644 crates/csslsrs/tests/completions.rs diff --git a/crates/csslsrs/src/features/completions.rs b/crates/csslsrs/src/features/completions.rs new file mode 100644 index 0000000..e0ec347 --- /dev/null +++ b/crates/csslsrs/src/features/completions.rs @@ -0,0 +1,67 @@ +use lsp_types::{CompletionItem, CompletionList, Position, TextDocumentItem}; + +use crate::{service::LanguageService, store::StoreEntry}; + +fn compute_completions(store_entry: &StoreEntry, position: Position) -> CompletionList { + CompletionList { + is_incomplete: true, + items: vec![], + } +} + +impl LanguageService { + pub fn get_completions( + &self, + document: TextDocumentItem, + position: Position, + ) -> CompletionList { + let store_document = self.store.get(&document.uri); + + match store_document { + Some(store_document) => compute_completions(store_document, position), + None => empty_completion_list(), + } + } +} + +fn empty_completion_list() -> CompletionList { + CompletionList { + is_incomplete: true, + items: vec![], + } +} + +#[cfg(feature = "wasm")] +mod wasm_bindings { + use std::str::FromStr; + + use crate::{ + features::completions::{compute_completions, empty_completion_list}, + service::wasm_bindings::WASMLanguageService, + }; + use lsp_types::{Position, Uri}; + use wasm_bindgen::{prelude::wasm_bindgen, JsValue}; + + #[wasm_bindgen(typescript_custom_section)] + const TS_APPEND_CONTENT: &'static str = r#" + declare function get_completions(documentUri: string, position: import("vscode-languageserver-types").Position): import("vscode-languageserver-types").FoldingRange[]; + "#; + + #[wasm_bindgen] + impl WASMLanguageService { + #[wasm_bindgen(skip_typescript, js_name = getCompletions)] + pub fn get_completions(&self, document_uri: String, position: JsValue) -> JsValue { + let store_document = self.store.get(&Uri::from_str(&document_uri).unwrap()); + + let completions = match store_document { + Some(store_document) => { + let position: Position = serde_wasm_bindgen::from_value(position).unwrap(); + compute_completions(store_document, position) + } + None => empty_completion_list(), + }; + + serde_wasm_bindgen::to_value(&completions).unwrap() + } + } +} diff --git a/crates/csslsrs/src/lib.rs b/crates/csslsrs/src/lib.rs index 1e7d66b..e3f1e11 100644 --- a/crates/csslsrs/src/lib.rs +++ b/crates/csslsrs/src/lib.rs @@ -8,6 +8,7 @@ pub mod store; pub mod features { pub mod color_parser; pub mod colors; + pub mod completions; pub mod folding; pub mod hover; } diff --git a/crates/csslsrs/src/service.rs b/crates/csslsrs/src/service.rs index eef8ee3..1112f02 100644 --- a/crates/csslsrs/src/service.rs +++ b/crates/csslsrs/src/service.rs @@ -187,6 +187,7 @@ pub mod wasm_bindings { getDocumentColors: typeof get_document_colors; getColorPresentations: typeof get_color_presentations; getFoldingRanges: typeof get_folding_ranges; + getCompletions: typeof get_completions; free(): void; } "#; diff --git a/crates/csslsrs/tests/completions.rs b/crates/csslsrs/tests/completions.rs new file mode 100644 index 0000000..8ab1f42 --- /dev/null +++ b/crates/csslsrs/tests/completions.rs @@ -0,0 +1,147 @@ +use csslsrs::{ + css_data::CssCustomData, + service::{LanguageService, LanguageServiceOptions}, +}; +use lsp_types::{ + Command, CompletionItem, CompletionItemKind, CompletionList, CompletionTextEdit, Documentation, + InsertTextFormat, Position, +}; + +#[derive(Debug)] +pub struct ItemDescription { + label: String, + detail: Option, + documentation: Option, + documentation_includes: Option, + kind: Option, + insert_text_format: Option, + result_text: Option, + not_available: Option, + command: Option, + sort_text: Option, +} + +pub fn assert_completion(completions: &CompletionList, expected: &ItemDescription, document: &str) { + let matches: Vec<&CompletionItem> = completions + .items + .iter() + .filter(|completion| completion.label == expected.label) + .collect(); + + if expected.not_available.is_some() { + assert_eq!(matches.len(), 0, "{} should not be present", expected.label); + } else { + assert_eq!( + matches.len(), + 1, + "{} should only exist once: Actual: {}", + expected.label, + completions + .items + .iter() + .map(|c| c.label.as_str()) + .collect::>() + .join(", ") + ); + } + + let match_item = matches[0]; + if let Some(detail) = &expected.detail { + assert_eq!(match_item.detail, Some(detail.clone())); + } + if let Some(documentation) = &expected.documentation { + assert_eq!(match_item.documentation, Some(documentation.clone())); + } + if let Some(documentation_includes) = &expected.documentation_includes { + if let Some(doc) = &match_item.documentation { + match doc { + Documentation::String(doc) => { + assert!(doc.contains(documentation_includes)); + } + Documentation::MarkupContent(doc) => { + assert!(doc.value.contains(documentation_includes)); + } + } + } + } + if let Some(kind) = &expected.kind { + assert_eq!(match_item.kind, Some(*kind)); + } + if let Some(result_text) = &expected.result_text { + if let Some(text_edit) = &match_item.text_edit { + let edited_text = apply_text_edit(document, text_edit); + assert_eq!(edited_text, *result_text); + } + } + if let Some(insert_text_format) = &expected.insert_text_format { + assert_eq!(match_item.insert_text_format, Some(*insert_text_format)); + } + if let Some(command) = &expected.command { + assert_eq!(match_item.command, Some(command.clone())); + } + if let Some(sort_text) = &expected.sort_text { + assert_eq!(match_item.sort_text, Some(sort_text.clone())); + } +} + +fn apply_text_edit(document: &str, text_edit: &CompletionTextEdit) -> String { + let mut lines: Vec<&str> = document.lines().collect(); + + match text_edit { + CompletionTextEdit::Edit(edit) => { + let start_line = edit.range.start.line as usize; + let start_char = edit.range.start.character as usize; + let end_line = edit.range.end.line as usize; + let end_char = edit.range.end.character as usize; + + let start_line_content = &lines[start_line][..start_char]; + let end_line_content = &lines[end_line][end_char..]; + + let formatted_line = format!( + "{}{}{}", + start_line_content, edit.new_text, end_line_content + ); + lines[start_line] = formatted_line.as_str(); + lines[start_line + 1..=end_line] + .iter_mut() + .for_each(|line| *line = ""); + + lines.join("\n") + } + CompletionTextEdit::InsertAndReplace(_) => unreachable!(), + } +} + +pub async fn test_completion_for( + content: String, + expected: ExpectedCompetions, + test_uri: &str, + workspace_folder_uri: &str, + custom_data: Vec, +) { + let offset = content.find('|').expect("| missing in value"); + let value = content.replace("|", ""); + + let mut ls = LanguageService::new(LanguageServiceOptions { + include_base_css_custom_data: true, + ..Default::default() + }); + + for data in custom_data { + ls.add_css_custom_data(data); + } + + let document = TextDocument::create(test_uri, lang, 0, value); + let position = Position::new(0, offset as u32); + + let list = ls.get_completions(document, position); + + if let Some(count) = expected.count { + assert_eq!(list.items.len(), count); + } + if let Some(items) = expected.items { + for item in items { + assert_completion(&list, &item, &document); + } + } +} From 86ebd95c71b6b8c9cdcea581e215a3921e0f354f Mon Sep 17 00:00:00 2001 From: Princesseuh <3019731+Princesseuh@users.noreply.github.com> Date: Sun, 15 Dec 2024 22:02:13 +0100 Subject: [PATCH 2/2] fix: progress --- crates/csslsrs/src/features/completions.rs | 40 +++++++++++-- crates/csslsrs/tests/completions.rs | 67 ++++++++++++++++++---- 2 files changed, 92 insertions(+), 15 deletions(-) diff --git a/crates/csslsrs/src/features/completions.rs b/crates/csslsrs/src/features/completions.rs index e0ec347..56209be 100644 --- a/crates/csslsrs/src/features/completions.rs +++ b/crates/csslsrs/src/features/completions.rs @@ -1,8 +1,36 @@ -use lsp_types::{CompletionItem, CompletionList, Position, TextDocumentItem}; +use biome_rowan::{AstNode, SyntaxNodeOptionExt}; +use lsp_types::{CompletionList, Position, TextDocumentItem}; -use crate::{service::LanguageService, store::StoreEntry}; +use crate::{ + converters::{from_proto::offset, PositionEncoding}, + service::LanguageService, + store::StoreEntry, +}; + +fn compute_completions( + store_entry: &StoreEntry, + position: Position, + encoding: PositionEncoding, +) -> CompletionList { + let offset = offset(&store_entry.line_index, position, encoding).unwrap(); + let parent = store_entry + .css_tree + .tree() + .syntax() + .token_at_offset(offset) + .left_biased() + .unwrap() + .parent(); + + if let Some(kind) = parent.kind() { + match kind { + biome_css_syntax::CssSyntaxKind::CSS_ROOT => { + + } + _ => todo!(), + } + }; -fn compute_completions(store_entry: &StoreEntry, position: Position) -> CompletionList { CompletionList { is_incomplete: true, items: vec![], @@ -18,7 +46,9 @@ impl LanguageService { let store_document = self.store.get(&document.uri); match store_document { - Some(store_document) => compute_completions(store_document, position), + Some(store_document) => { + compute_completions(store_document, position, self.options.encoding) + } None => empty_completion_list(), } } @@ -56,7 +86,7 @@ mod wasm_bindings { let completions = match store_document { Some(store_document) => { let position: Position = serde_wasm_bindgen::from_value(position).unwrap(); - compute_completions(store_document, position) + compute_completions(store_document, position, self.options.encoding) } None => empty_completion_list(), }; diff --git a/crates/csslsrs/tests/completions.rs b/crates/csslsrs/tests/completions.rs index 8ab1f42..3222033 100644 --- a/crates/csslsrs/tests/completions.rs +++ b/crates/csslsrs/tests/completions.rs @@ -1,13 +1,15 @@ +use std::str::FromStr; + use csslsrs::{ css_data::CssCustomData, service::{LanguageService, LanguageServiceOptions}, }; use lsp_types::{ Command, CompletionItem, CompletionItemKind, CompletionList, CompletionTextEdit, Documentation, - InsertTextFormat, Position, + InsertTextFormat, Position, TextDocumentItem, Uri, }; -#[derive(Debug)] +#[derive(Debug, Default)] pub struct ItemDescription { label: String, detail: Option, @@ -21,7 +23,11 @@ pub struct ItemDescription { sort_text: Option, } -pub fn assert_completion(completions: &CompletionList, expected: &ItemDescription, document: &str) { +pub fn assert_completion( + completions: &CompletionList, + expected: &ItemDescription, + document_content: &str, +) { let matches: Vec<&CompletionItem> = completions .items .iter() @@ -69,7 +75,7 @@ pub fn assert_completion(completions: &CompletionList, expected: &ItemDescriptio } if let Some(result_text) = &expected.result_text { if let Some(text_edit) = &match_item.text_edit { - let edited_text = apply_text_edit(document, text_edit); + let edited_text = apply_text_edit(document_content, text_edit); assert_eq!(edited_text, *result_text); } } @@ -112,11 +118,15 @@ fn apply_text_edit(document: &str, text_edit: &CompletionTextEdit) -> String { } } -pub async fn test_completion_for( - content: String, - expected: ExpectedCompetions, +struct ExpectedCompletions { + count: Option, + items: Option>, +} + +fn test_completion_for( + content: &str, + expected: ExpectedCompletions, test_uri: &str, - workspace_folder_uri: &str, custom_data: Vec, ) { let offset = content.find('|').expect("| missing in value"); @@ -131,7 +141,15 @@ pub async fn test_completion_for( ls.add_css_custom_data(data); } - let document = TextDocument::create(test_uri, lang, 0, value); + let document = TextDocumentItem::new( + Uri::from_str(test_uri).unwrap(), + "css".to_string(), + 0, + value.clone(), + ); + + ls.upsert_document(document.clone()); + let position = Position::new(0, offset as u32); let list = ls.get_completions(document, position); @@ -141,7 +159,36 @@ pub async fn test_completion_for( } if let Some(items) = expected.items { for item in items { - assert_completion(&list, &item, &document); + assert_completion(&list, &item, &value); } } } + +#[test] +fn test_top_level_completions() { + test_completion_for( + "| ", + ExpectedCompletions { + count: None, + items: Some(vec![ + ItemDescription { + label: "@import".to_string(), + result_text: Some("@import body {".to_string()), + ..Default::default() + }, + ItemDescription { + label: "@keyframes".to_string(), + result_text: Some("@keyframes body {".to_string()), + ..Default::default() + }, + ItemDescription { + label: "html".to_string(), + result_text: Some("html body {".to_string()), + ..Default::default() + }, + ]), + }, + "file:///test", + vec![], + ); +}