More lsp endpoints we were missing (#6612)

* add prepare rename

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* add document color

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
This commit is contained in:
Jess Frazelle
2025-04-30 19:47:36 -07:00
committed by GitHub
parent 2d77aa0d36
commit b686c79b49
10 changed files with 529 additions and 76 deletions

30
flake.lock generated
View File

@ -5,11 +5,11 @@
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1743800763,
"narHash": "sha256-YFKV+fxEpMgP5VsUcM6Il28lI0NlpM7+oB1XxbBAYCw=",
"lastModified": 1745925850,
"narHash": "sha256-cyAAMal0aPrlb1NgzMxZqeN1mAJ2pJseDhm2m6Um8T0=",
"owner": "nix-community",
"repo": "naersk",
"rev": "ed0232117731a4c19d3ee93aa0c382a8fe754b01",
"rev": "38bc60bbc157ae266d4a0c96671c6c742ee17a5f",
"type": "github"
},
"original": {
@ -20,11 +20,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1744316434,
"narHash": "sha256-lzFCg/1C39pyY2hMB2gcuHV79ozpOz/Vu15hdjiFOfI=",
"lastModified": 1745998881,
"narHash": "sha256-vonyYAKJSlsX4n9GCsS0pHxR6yCrfqBIuGvANlkwG6U=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "d19cf9dfc633816a437204555afeb9e722386b76",
"rev": "423d2df5b04b4ee7688c3d71396e872afa236a89",
"type": "github"
},
"original": {
@ -36,11 +36,11 @@
},
"nixpkgs_2": {
"locked": {
"lastModified": 1744316434,
"narHash": "sha256-lzFCg/1C39pyY2hMB2gcuHV79ozpOz/Vu15hdjiFOfI=",
"lastModified": 1745998881,
"narHash": "sha256-vonyYAKJSlsX4n9GCsS0pHxR6yCrfqBIuGvANlkwG6U=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "d19cf9dfc633816a437204555afeb9e722386b76",
"rev": "423d2df5b04b4ee7688c3d71396e872afa236a89",
"type": "github"
},
"original": {
@ -52,11 +52,11 @@
},
"nixpkgs_3": {
"locked": {
"lastModified": 1736320768,
"narHash": "sha256-nIYdTAiKIGnFNugbomgBJR+Xv5F1ZQU+HfaBqJKroC0=",
"lastModified": 1744536153,
"narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "4bc9c909d9ac828a039f288cf872d16d38185db8",
"rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11",
"type": "github"
},
"original": {
@ -78,11 +78,11 @@
"nixpkgs": "nixpkgs_3"
},
"locked": {
"lastModified": 1744338850,
"narHash": "sha256-pwMIVmsb8fjjT92n5XFDqCsplcX70qVMMT7NulumPXs=",
"lastModified": 1745980514,
"narHash": "sha256-CITAeiuXGjDvT5iZBXr6vKVWQwsUQLJUMFO91bfJFC4=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "5e64aecc018e6f775572609e7d7485fdba6985a7",
"rev": "7fbdae44b0f40ea432e46fd152ad8be0f8f41ad6",
"type": "github"
},
"original": {

View File

@ -547,19 +547,10 @@ export class LanguageServerPlugin implements PluginValue {
try {
// First check if rename is possible at this position
const prepareResult = await this.client
.textDocumentPrepareRename({
textDocument: { uri: this.getDocUri() },
position: { line, character },
})
.catch(() => {
// In case prepareRename is not supported,
// we fallback to the default implementation
return this.prepareRenameFallback(view, {
let prepareResult = this.prepareRenameFallback(view, {
line,
character,
})
})
if (!prepareResult || 'defaultBehavior' in prepareResult) {
showErrorMessage(view, 'Cannot rename this symbol')

58
rust/Cargo.lock generated
View File

@ -713,6 +713,15 @@ dependencies = [
"typenum",
]
[[package]]
name = "csscolorparser"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46f9a16a848a7fb95dd47ce387ac1ee9a6df879ba784b815537fcd388a1a8288"
dependencies = [
"phf",
]
[[package]]
name = "darling"
version = "0.20.10"
@ -1899,6 +1908,7 @@ dependencies = [
"console_error_panic_hook",
"convert_case",
"criterion",
"csscolorparser",
"dashmap 6.1.0",
"dhat",
"expectorate",
@ -2630,6 +2640,48 @@ dependencies = [
"sha2",
]
[[package]]
name = "phf"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
dependencies = [
"phf_macros",
"phf_shared",
]
[[package]]
name = "phf_generator"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
dependencies = [
"phf_shared",
"rand 0.8.5",
]
[[package]]
name = "phf_macros"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216"
dependencies = [
"phf_generator",
"phf_shared",
"proc-macro2",
"quote",
"syn 2.0.100",
]
[[package]]
name = "phf_shared"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
dependencies = [
"siphasher",
]
[[package]]
name = "phonenumber"
version = "0.3.7+8.13.52"
@ -3575,6 +3627,12 @@ version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa"
[[package]]
name = "siphasher"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
[[package]]
name = "slab"
version = "0.4.9"

View File

@ -31,6 +31,7 @@ clap = { version = "4.5.36", default-features = false, optional = true, features
"derive",
] }
convert_case = "0.8.0"
csscolorparser = "0.7.0"
dashmap = { workspace = true }
dhat = { version = "0.3", optional = true }
fnv = "1.0.7"

View File

@ -8,12 +8,11 @@ use parse_display::{Display, FromStr};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::engine::{PlaneName, DEFAULT_PLANE_INFO};
use crate::errors::KclErrorDetails;
#[cfg(feature = "artifact-graph")]
use crate::execution::ArtifactId;
use crate::{
errors::KclError,
engine::{PlaneName, DEFAULT_PLANE_INFO},
errors::{KclError, KclErrorDetails},
execution::{types::NumericType, ExecState, ExecutorContext, Metadata, TagEngineInfo, TagIdentifier, UnitLen},
parsing::ast::types::{Node, NodeRef, TagDeclarator, TagNode},
std::{args::TyF64, sketch::PlaneData},

View File

@ -2,13 +2,14 @@ use anyhow::Result;
use crate::{
errors::Suggestion,
lint::rule::{def_finding, Discovered, Finding},
lint::{
checks::offset_plane::start_sketch_on_check_specific_plane,
rule::{def_finding, Discovered, Finding},
},
parsing::ast::types::{Node as AstNode, Program},
walk::Node,
};
use super::offset_plane::start_sketch_on_check_specific_plane;
def_finding!(
Z0002,
"default plane should be called versus explicitly defined",

View File

@ -18,23 +18,24 @@ use tower_lsp::{
jsonrpc::Result as RpcResult,
lsp_types::{
CodeAction, CodeActionKind, CodeActionOptions, CodeActionOrCommand, CodeActionParams,
CodeActionProviderCapability, CodeActionResponse, CompletionItem, CompletionItemKind, CompletionOptions,
CompletionParams, CompletionResponse, CreateFilesParams, DeleteFilesParams, Diagnostic, DiagnosticOptions,
CodeActionProviderCapability, CodeActionResponse, ColorInformation, ColorPresentation, ColorPresentationParams,
ColorProviderCapability, CompletionItem, CompletionItemKind, CompletionOptions, CompletionParams,
CompletionResponse, CreateFilesParams, DeleteFilesParams, Diagnostic, DiagnosticOptions,
DiagnosticServerCapabilities, DiagnosticSeverity, DidChangeConfigurationParams, DidChangeTextDocumentParams,
DidChangeWatchedFilesParams, DidChangeWorkspaceFoldersParams, DidCloseTextDocumentParams,
DidOpenTextDocumentParams, DidSaveTextDocumentParams, DocumentDiagnosticParams, DocumentDiagnosticReport,
DocumentDiagnosticReportResult, DocumentFilter, DocumentFormattingParams, DocumentSymbol, DocumentSymbolParams,
DocumentSymbolResponse, Documentation, FoldingRange, FoldingRangeParams, FoldingRangeProviderCapability,
FullDocumentDiagnosticReport, Hover as LspHover, HoverContents, HoverParams, HoverProviderCapability,
InitializeParams, InitializeResult, InitializedParams, InlayHint, InlayHintParams, InsertTextFormat,
MarkupContent, MarkupKind, MessageType, OneOf, Position, RelatedFullDocumentDiagnosticReport,
RenameFilesParams, RenameParams, SemanticToken, SemanticTokenModifier, SemanticTokenType, SemanticTokens,
SemanticTokensFullOptions, SemanticTokensLegend, SemanticTokensOptions, SemanticTokensParams,
SemanticTokensRegistrationOptions, SemanticTokensResult, SemanticTokensServerCapabilities, ServerCapabilities,
SignatureHelp, SignatureHelpOptions, SignatureHelpParams, StaticRegistrationOptions, TextDocumentItem,
TextDocumentRegistrationOptions, TextDocumentSyncCapability, TextDocumentSyncKind, TextDocumentSyncOptions,
TextEdit, WorkDoneProgressOptions, WorkspaceEdit, WorkspaceFolder, WorkspaceFoldersServerCapabilities,
WorkspaceServerCapabilities,
DidOpenTextDocumentParams, DidSaveTextDocumentParams, DocumentColorParams, DocumentDiagnosticParams,
DocumentDiagnosticReport, DocumentDiagnosticReportResult, DocumentFilter, DocumentFormattingParams,
DocumentSymbol, DocumentSymbolParams, DocumentSymbolResponse, Documentation, FoldingRange, FoldingRangeParams,
FoldingRangeProviderCapability, FullDocumentDiagnosticReport, Hover as LspHover, HoverContents, HoverParams,
HoverProviderCapability, InitializeParams, InitializeResult, InitializedParams, InlayHint, InlayHintParams,
InsertTextFormat, MarkupContent, MarkupKind, MessageType, OneOf, Position, PrepareRenameResponse,
RelatedFullDocumentDiagnosticReport, RenameFilesParams, RenameParams, SemanticToken, SemanticTokenModifier,
SemanticTokenType, SemanticTokens, SemanticTokensFullOptions, SemanticTokensLegend, SemanticTokensOptions,
SemanticTokensParams, SemanticTokensRegistrationOptions, SemanticTokensResult,
SemanticTokensServerCapabilities, ServerCapabilities, SignatureHelp, SignatureHelpOptions, SignatureHelpParams,
StaticRegistrationOptions, TextDocumentItem, TextDocumentPositionParams, TextDocumentRegistrationOptions,
TextDocumentSyncCapability, TextDocumentSyncKind, TextDocumentSyncOptions, TextEdit, WorkDoneProgressOptions,
WorkspaceEdit, WorkspaceFolder, WorkspaceFoldersServerCapabilities, WorkspaceServerCapabilities,
},
Client, LanguageServer,
};
@ -827,6 +828,39 @@ impl Backend {
Ok(custom_notifications::UpdateCanExecuteResponse {})
}
/// Returns the new string for the code after rename.
pub fn inner_prepare_rename(
&self,
params: &TextDocumentPositionParams,
new_name: &str,
) -> RpcResult<Option<(String, String)>> {
let filename = params.text_document.uri.to_string();
let Some(current_code) = self.code_map.get(&filename) else {
return Ok(None);
};
let Ok(current_code) = std::str::from_utf8(&current_code) else {
return Ok(None);
};
// Parse the ast.
// I don't know if we need to do this again since it should be updated in the context.
// But I figure better safe than sorry since this will write back out to the file.
let module_id = ModuleId::default();
let Ok(mut ast) = crate::parsing::parse_str(current_code, module_id).parse_errs_as_err() else {
return Ok(None);
};
// Let's convert the position to a character index.
let pos = position_to_char_index(params.position, current_code);
// Now let's perform the rename on the ast.
ast.rename_symbol(new_name, pos);
// Now recast it.
let recast = ast.recast(&Default::default(), 0);
Ok(Some((current_code.to_string(), recast)))
}
}
#[tower_lsp::async_trait]
@ -838,6 +872,7 @@ impl LanguageServer for Backend {
Ok(InitializeResult {
capabilities: ServerCapabilities {
color_provider: Some(ColorProviderCapability::Simple(true)),
code_action_provider: Some(CodeActionProviderCapability::Options(CodeActionOptions {
code_action_kinds: Some(vec![CodeActionKind::QUICKFIX]),
resolve_provider: Some(false),
@ -1459,36 +1494,19 @@ impl LanguageServer for Backend {
}
async fn rename(&self, params: RenameParams) -> RpcResult<Option<WorkspaceEdit>> {
let filename = params.text_document_position.text_document.uri.to_string();
let Some(current_code) = self.code_map.get(&filename) else {
return Ok(None);
};
let Ok(current_code) = std::str::from_utf8(&current_code) else {
let Some((current_code, new_code)) =
self.inner_prepare_rename(&params.text_document_position, &params.new_name)?
else {
return Ok(None);
};
// Parse the ast.
// I don't know if we need to do this again since it should be updated in the context.
// But I figure better safe than sorry since this will write back out to the file.
let module_id = ModuleId::default();
let Ok(mut ast) = crate::parsing::parse_str(current_code, module_id).parse_errs_as_err() else {
return Ok(None);
};
// Let's convert the position to a character index.
let pos = position_to_char_index(params.text_document_position.position, current_code);
// Now let's perform the rename on the ast.
ast.rename_symbol(&params.new_name, pos);
// Now recast it.
let recast = ast.recast(&Default::default(), 0);
let source_range = SourceRange::new(0, current_code.len(), module_id);
let range = source_range.to_lsp_range(current_code);
let source_range = SourceRange::new(0, current_code.len(), ModuleId::default());
let range = source_range.to_lsp_range(&current_code);
Ok(Some(WorkspaceEdit {
changes: Some(HashMap::from([(
params.text_document_position.text_document.uri,
vec![TextEdit {
new_text: recast,
new_text: new_code,
range,
}],
)])),
@ -1497,6 +1515,18 @@ impl LanguageServer for Backend {
}))
}
async fn prepare_rename(&self, params: TextDocumentPositionParams) -> RpcResult<Option<PrepareRenameResponse>> {
if self
.inner_prepare_rename(&params, "someNameNoOneInTheirRightMindWouldEverUseForTesting")?
.is_none()
{
return Ok(None);
}
// Return back to the client, that it is safe to use the rename behavior.
Ok(Some(PrepareRenameResponse::DefaultBehavior { default_behavior: true }))
}
async fn folding_range(&self, params: FoldingRangeParams) -> RpcResult<Option<Vec<FoldingRange>>> {
let filename = params.text_document.uri.to_string();
@ -1552,6 +1582,55 @@ impl LanguageServer for Backend {
Ok(Some(actions))
}
async fn document_color(&self, params: DocumentColorParams) -> RpcResult<Vec<ColorInformation>> {
let filename = params.text_document.uri.to_string();
let Some(current_code) = self.code_map.get(&filename) else {
return Ok(vec![]);
};
let Ok(current_code) = std::str::from_utf8(&current_code) else {
return Ok(vec![]);
};
// Get the ast from our map.
let Some(ast) = self.ast_map.get(&filename) else {
return Ok(vec![]);
};
// Get the colors from the ast.
let Ok(colors) = ast.ast.document_color(current_code) else {
return Ok(vec![]);
};
Ok(colors)
}
async fn color_presentation(&self, params: ColorPresentationParams) -> RpcResult<Vec<ColorPresentation>> {
let filename = params.text_document.uri.to_string();
let Some(current_code) = self.code_map.get(&filename) else {
return Ok(vec![]);
};
let Ok(current_code) = std::str::from_utf8(&current_code) else {
return Ok(vec![]);
};
// Get the ast from our map.
let Some(ast) = self.ast_map.get(&filename) else {
return Ok(vec![]);
};
let pos_start = position_to_char_index(params.range.start, current_code);
let pos_end = position_to_char_index(params.range.end, current_code);
// Get the colors from the ast.
let Ok(Some(presentation)) = ast.ast.color_presentation(&params.color, pos_start, pos_end) else {
return Ok(vec![]);
};
Ok(vec![presentation])
}
}
/// Get completions from our stdlib.
@ -1662,3 +1741,48 @@ async fn with_cached_var<T>(name: &str, f: impl Fn(&KclValue) -> T) -> Option<T>
Some(f(value))
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn test_position_to_char_index_first_line() {
let code = r#"def foo():
return 42"#;
let position = Position::new(0, 3);
let index = position_to_char_index(position, code);
assert_eq!(index, 3);
}
#[test]
fn test_position_to_char_index() {
let code = r#"def foo():
return 42"#;
let position = Position::new(1, 4);
let index = position_to_char_index(position, code);
assert_eq!(index, 15);
}
#[test]
fn test_position_to_char_index_with_newline() {
let code = r#"def foo():
return 42"#;
let position = Position::new(2, 0);
let index = position_to_char_index(position, code);
assert_eq!(index, 12);
}
#[test]
fn test_position_to_char_at_end() {
let code = r#"def foo():
return 42"#;
let position = Position::new(1, 8);
let index = position_to_char_index(position, code);
assert_eq!(index, 19);
}
}

View File

@ -3,8 +3,8 @@ use std::collections::{BTreeMap, HashMap};
use pretty_assertions::assert_eq;
use tower_lsp::{
lsp_types::{
CodeActionKind, CodeActionOrCommand, Diagnostic, SemanticTokenModifier, SemanticTokenType, TextEdit,
WorkspaceEdit,
CodeActionKind, CodeActionOrCommand, Diagnostic, PrepareRenameResponse, SemanticTokenModifier,
SemanticTokenType, TextEdit, WorkspaceEdit,
},
LanguageServer,
};
@ -4146,3 +4146,173 @@ async fn kcl_test_kcl_lsp_code_actions_lint_offset_planes() {
})
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_kcl_lsp_prepare_rename() {
let server = kcl_lsp_server(false).await.unwrap();
// Send open file.
server
.did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams {
text_document: tower_lsp::lsp_types::TextDocumentItem {
uri: "file:///test.kcl".try_into().unwrap(),
language_id: "kcl".to_string(),
version: 1,
text: r#"thing= 1"#.to_string(),
},
})
.await;
// Send rename request.
let result = server
.prepare_rename(tower_lsp::lsp_types::TextDocumentPositionParams {
text_document: tower_lsp::lsp_types::TextDocumentIdentifier {
uri: "file:///test.kcl".try_into().unwrap(),
},
position: tower_lsp::lsp_types::Position { line: 0, character: 2 },
})
.await
.unwrap()
.unwrap();
// Check the result.
assert_eq!(
result,
PrepareRenameResponse::DefaultBehavior { default_behavior: true }
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_kcl_lsp_document_color() {
let server = kcl_lsp_server(false).await.unwrap();
// Send open file.
server
.did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams {
text_document: tower_lsp::lsp_types::TextDocumentItem {
uri: "file:///test.kcl".try_into().unwrap(),
language_id: "kcl".to_string(),
version: 1,
text: r#"// Add color to a revolved solid.
sketch001 = startSketchOn(XY)
|> circle(center = [15, 0], radius = 5)
|> revolve(angle = 360, axis = Y)
|> appearance(color = '#ff0000', metalness = 90, roughness = 90)"#
.to_string(),
},
})
.await;
// Send document color request.
let result = server
.document_color(tower_lsp::lsp_types::DocumentColorParams {
text_document: tower_lsp::lsp_types::TextDocumentIdentifier {
uri: "file:///test.kcl".try_into().unwrap(),
},
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
})
.await
.unwrap();
// Check the result.
assert_eq!(
result,
vec![tower_lsp::lsp_types::ColorInformation {
range: tower_lsp::lsp_types::Range {
start: tower_lsp::lsp_types::Position { line: 4, character: 24 },
end: tower_lsp::lsp_types::Position { line: 4, character: 33 },
},
color: tower_lsp::lsp_types::Color {
red: 1.0,
green: 0.0,
blue: 0.0,
alpha: 1.0,
},
}]
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_kcl_lsp_color_presentation() {
let server = kcl_lsp_server(false).await.unwrap();
let text = r#"// Add color to a revolved solid.
sketch001 = startSketchOn(XY)
|> circle(center = [15, 0], radius = 5)
|> revolve(angle = 360, axis = Y)
|> appearance(color = '#ff0000', metalness = 90, roughness = 90)"#;
// Send open file.
server
.did_open(tower_lsp::lsp_types::DidOpenTextDocumentParams {
text_document: tower_lsp::lsp_types::TextDocumentItem {
uri: "file:///test.kcl".try_into().unwrap(),
language_id: "kcl".to_string(),
version: 1,
text: text.to_string(),
},
})
.await;
// Send document color request.
let result = server
.document_color(tower_lsp::lsp_types::DocumentColorParams {
text_document: tower_lsp::lsp_types::TextDocumentIdentifier {
uri: "file:///test.kcl".try_into().unwrap(),
},
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
})
.await
.unwrap();
// Check the result.
assert_eq!(
result,
vec![tower_lsp::lsp_types::ColorInformation {
range: tower_lsp::lsp_types::Range {
start: tower_lsp::lsp_types::Position { line: 4, character: 24 },
end: tower_lsp::lsp_types::Position { line: 4, character: 33 },
},
color: tower_lsp::lsp_types::Color {
red: 1.0,
green: 0.0,
blue: 0.0,
alpha: 1.0,
},
}]
);
// Send color presentation request.
let result = server
.color_presentation(tower_lsp::lsp_types::ColorPresentationParams {
text_document: tower_lsp::lsp_types::TextDocumentIdentifier {
uri: "file:///test.kcl".try_into().unwrap(),
},
range: tower_lsp::lsp_types::Range {
start: tower_lsp::lsp_types::Position { line: 4, character: 24 },
end: tower_lsp::lsp_types::Position { line: 4, character: 33 },
},
color: tower_lsp::lsp_types::Color {
red: 1.0,
green: 0.0,
blue: 1.0,
alpha: 1.0,
},
work_done_progress_params: Default::default(),
partial_result_params: Default::default(),
})
.await
.unwrap();
// Check the result.
assert_eq!(
result,
vec![tower_lsp::lsp_types::ColorPresentation {
label: "#ff00ff".to_string(),
text_edit: None,
additional_text_edits: None,
}]
);
}

View File

@ -1,4 +1,4 @@
use std::fmt;
use std::{fmt, str::FromStr};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@ -32,6 +32,21 @@ impl LiteralValue {
_ => None,
}
}
pub fn is_color(&self) -> Option<csscolorparser::Color> {
if let Self::String(s) = self {
// Check if the string is a color.
if s.starts_with('#') && s.len() == 7 {
let Ok(c) = csscolorparser::Color::from_str(s) else {
return None;
};
return Some(c);
}
}
None
}
}
impl fmt::Display for LiteralValue {

View File

@ -14,7 +14,8 @@ use parse_display::{Display, FromStr};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use tower_lsp::lsp_types::{
CompletionItem, CompletionItemKind, DocumentSymbol, FoldingRange, FoldingRangeKind, SymbolKind,
Color, ColorInformation, ColorPresentation, CompletionItem, CompletionItemKind, DocumentSymbol, FoldingRange,
FoldingRangeKind, SymbolKind,
};
pub use crate::parsing::ast::types::{
@ -389,6 +390,99 @@ impl Node<Program> {
true
}
/// Find all the color strings in the program.
/// For example `appearance(color = "#ff0000")`
/// This is to fulfill the `documentColor` request in LSP.
pub fn document_color<'a>(&'a self, code: &str) -> Result<Vec<ColorInformation>> {
let colors = Rc::new(RefCell::new(vec![]));
let add_color = |literal: &Node<Literal>| {
// Check if the string is a color.
if let Some(c) = literal.value.is_color() {
let color = ColorInformation {
range: literal.as_source_range().to_lsp_range(code),
color: tower_lsp::lsp_types::Color {
red: c.r,
green: c.g,
blue: c.b,
alpha: c.a,
},
};
if colors.borrow().iter().any(|c| *c == color) {
return;
}
colors.borrow_mut().push(color);
}
};
// The position must be within the variable declaration.
crate::walk::walk(self, |node: crate::walk::Node<'a>| {
match node {
crate::walk::Node::CallExpressionKw(call) => {
if call.inner.callee.inner.name.inner.name == "appearance" {
for arg in &call.arguments {
if arg.label.inner.name == "color" {
// Get the value of the argument.
if let Expr::Literal(literal) = &arg.arg {
add_color(literal);
}
}
}
}
}
crate::walk::Node::Literal(literal) => {
// Check if the literal is a color.
add_color(literal);
}
_ => {
// Do nothing.
}
}
Ok::<bool, anyhow::Error>(true)
})?;
let colors = colors.take();
Ok(colors)
}
/// This is to fulfill the `colorPresentation` request in LSP.
pub fn color_presentation<'a>(
&'a self,
color: &Color,
pos_start: usize,
pos_end: usize,
) -> Result<Option<ColorPresentation>> {
let found = Rc::new(RefCell::new(false));
// Find the literal with the same start and end.
crate::walk::walk(self, |node: crate::walk::Node<'a>| {
match node {
crate::walk::Node::Literal(literal) => {
if literal.start == pos_start && literal.end == pos_end && literal.value.is_color().is_some() {
found.replace(true);
return Ok(true);
}
}
_ => {
// Do nothing.
}
}
Ok::<bool, anyhow::Error>(true)
})?;
let found = found.take();
if !found {
return Ok(None);
}
let new_color = csscolorparser::Color::new(color.red, color.green, color.blue, color.alpha);
Ok(Some(ColorPresentation {
// The label will be what they replace the color with.
label: new_color.to_hex_string(),
text_edit: None,
additional_text_edits: None,
}))
}
}
impl Program {