Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions crates/csslsrs/src/features/completions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
use biome_rowan::{AstNode, SyntaxNodeOptionExt};
use lsp_types::{CompletionList, Position, TextDocumentItem};

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

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, self.options.encoding)
}
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, self.options.encoding)
}
None => empty_completion_list(),
};

serde_wasm_bindgen::to_value(&completions).unwrap()
}
}
}
1 change: 1 addition & 0 deletions crates/csslsrs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
1 change: 1 addition & 0 deletions crates/csslsrs/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
"#;
Expand Down
194 changes: 194 additions & 0 deletions crates/csslsrs/tests/completions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
use std::str::FromStr;

use csslsrs::{
css_data::CssCustomData,
service::{LanguageService, LanguageServiceOptions},
};
use lsp_types::{
Command, CompletionItem, CompletionItemKind, CompletionList, CompletionTextEdit, Documentation,
InsertTextFormat, Position, TextDocumentItem, Uri,
};

#[derive(Debug, Default)]
pub struct ItemDescription {
label: String,
detail: Option<String>,
documentation: Option<Documentation>,
documentation_includes: Option<String>,
kind: Option<CompletionItemKind>,
insert_text_format: Option<InsertTextFormat>,
result_text: Option<String>,
not_available: Option<bool>,
command: Option<Command>,
sort_text: Option<String>,
}

pub fn assert_completion(
completions: &CompletionList,
expected: &ItemDescription,
document_content: &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::<Vec<&str>>()
.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_content, 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!(),
}
}

struct ExpectedCompletions {
count: Option<usize>,
items: Option<Vec<ItemDescription>>,
}

fn test_completion_for(
content: &str,
expected: ExpectedCompletions,
test_uri: &str,
custom_data: Vec<CssCustomData>,
) {
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 = 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);

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, &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![],
);
}