diff --git a/README.md b/README.md index 2b7dd399..4b7be605 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,9 @@ Language service (LSP) features: - Also known as "hovering tooltip". - [Inlay hints](https://www.jetbrains.com/help/idea/inlay-hints.html) - Inlay hints are special markers that appear in the editor and provide you with additional information about your code, like the names of the parameters that a called method expects. +- [Color Provider](https://code.visualstudio.com/api/language-extensions/programmatic-language-features#show-color-decorators) + - View all inlay colorful label for color literals in your document. + - Change the color literal's value by a color picker or its code presentation. - [Code Lens](https://code.visualstudio.com/blogs/2017/02/12/code-lens-roundup) - Should give contextual buttons along with code. For example, a button for exporting your document to various formats at the start of the document. - [Rename symbols](https://code.visualstudio.com/api/language-extensions/programmatic-language-features#rename-symbols) diff --git a/crates/tinymist-query/src/analysis.rs b/crates/tinymist-query/src/analysis.rs index 9f7bcbe7..36116fde 100644 --- a/crates/tinymist-query/src/analysis.rs +++ b/crates/tinymist-query/src/analysis.rs @@ -2,6 +2,8 @@ pub mod call; pub use call::*; +pub mod color_exprs; +pub use color_exprs::*; pub mod def_use; pub use def_use::*; pub mod track_values; diff --git a/crates/tinymist-query/src/analysis/color_exprs.rs b/crates/tinymist-query/src/analysis/color_exprs.rs new file mode 100644 index 00000000..117974b7 --- /dev/null +++ b/crates/tinymist-query/src/analysis/color_exprs.rs @@ -0,0 +1,128 @@ +//! Analyze color expressions in a source file. +use std::{ops::Range, str::FromStr}; + +use lsp_types::ColorInformation; +use typst::{ + syntax::{ + ast::{self, AstNode}, + LinkedNode, Source, SyntaxKind, + }, + visualize::Color, +}; + +use crate::AnalysisContext; + +/// Get color expressions from a source. +pub fn get_color_exprs(ctx: &mut AnalysisContext, src: &Source) -> Option> { + let mut worker = ColorExprWorker { + ctx, + source: src.clone(), + colors: vec![], + }; + let root = LinkedNode::new(src.root()); + worker.collect_colors(root)?; + Some(worker.colors) +} + +struct ColorExprWorker<'a, 'w> { + ctx: &'a mut AnalysisContext<'w>, + source: Source, + colors: Vec, +} + +impl<'a, 'w> ColorExprWorker<'a, 'w> { + fn collect_colors(&mut self, node: LinkedNode) -> Option<()> { + match node.kind() { + SyntaxKind::FuncCall => { + let fc = self.analyze_call(node.clone()); + if fc.is_some() { + return Some(()); + } + } + SyntaxKind::Named => {} + k if k.is_trivia() || k.is_keyword() || k.is_error() => return Some(()), + _ => {} + }; + + for child in node.children() { + self.collect_colors(child); + } + + Some(()) + } + + fn analyze_call(&mut self, node: LinkedNode) -> Option<()> { + let call = node.cast::()?; + let mut callee = call.callee(); + 'check_color_fn: loop { + match callee { + ast::Expr::FieldAccess(fa) => { + let target = fa.target(); + let ast::Expr::Ident(ident) = target else { + return None; + }; + if ident.get().as_str() != "color" { + return None; + } + callee = ast::Expr::Ident(fa.field()); + continue 'check_color_fn; + } + ast::Expr::Ident(ident) => { + // currently support rgb, luma + match ident.get().as_str() { + "rgb" => self.analyze_rgb(&node, call)?, + "luma" | "oklab" | "oklch" | "linear-rgb" | "cmyk" | "hsl" | "hsv" => { + self.analyze_general(&node, call)? + } + _ => return None, + } + } + _ => return None, + } + return None; + } + } + + fn analyze_rgb(&mut self, node: &LinkedNode, call: ast::FuncCall) -> Option<()> { + let mut args = call.args().items(); + let hex_or_color_or_r = args.next()?; + let g = args.next(); + match (g.is_some(), hex_or_color_or_r) { + (true, _) => self.analyze_general(node, call)?, + (false, ast::Arg::Pos(ast::Expr::Str(s))) => { + // parse hex + let color = typst::visualize::Color::from_str(s.get().as_str()).ok()?; + // todo: smarter + // let arg = node.find(hex_or_color_or_r.span())?; + let arg = node.find(call.span())?; + self.push_color(arg.range(), color); + } + (false, _) => {} + } + + Some(()) + } + + fn analyze_general(&mut self, node: &LinkedNode, call: ast::FuncCall) -> Option<()> { + let color = self.ctx.mini_eval(ast::Expr::FuncCall(call))?.cast().ok()?; + self.push_color(node.range(), color); + Some(()) + } + + fn push_color(&mut self, range: Range, color: Color) -> Option<()> { + let rng = self.ctx.to_lsp_range(range, &self.source); + let [r, g, b, a] = color.to_rgb().to_vec4(); + + self.colors.push(ColorInformation { + range: rng, + color: lsp_types::Color { + red: r, + green: g, + blue: b, + alpha: a, + }, + }); + + Some(()) + } +} diff --git a/crates/tinymist-query/src/analysis/global.rs b/crates/tinymist-query/src/analysis/global.rs index 57ac3bc0..e3042cac 100644 --- a/crates/tinymist-query/src/analysis/global.rs +++ b/crates/tinymist-query/src/analysis/global.rs @@ -8,12 +8,12 @@ use std::{ use ecow::EcoVec; use once_cell::sync::OnceCell; use reflexo::{cow_mut::CowMut, debug_loc::DataSource, ImmutPath}; -use typst::text::Font; use typst::{ diag::{eco_format, FileError, FileResult, PackageError}, - syntax::{package::PackageSpec, Source, VirtualPath}, + syntax::{package::PackageSpec, Source, Span, VirtualPath}, World, }; +use typst::{foundations::Value, syntax::ast, text::Font}; use typst::{layout::Position, syntax::FileId as TypstFileId}; use super::{get_def_use_inner, DefUseInfo}; @@ -360,6 +360,46 @@ impl<'w> AnalysisContext<'w> { crate::syntax::get_lexical_hierarchy(after, crate::syntax::LexicalScopeKind::DefUse) }) } + + pub(crate) fn mini_eval(&self, rr: ast::Expr<'_>) -> Option { + Some(match rr { + ast::Expr::None(_) => Value::None, + ast::Expr::Auto(_) => Value::Auto, + ast::Expr::Bool(v) => Value::Bool(v.get()), + ast::Expr::Int(v) => Value::Int(v.get()), + ast::Expr::Float(v) => Value::Float(v.get()), + ast::Expr::Numeric(v) => Value::numeric(v.get()), + ast::Expr::Str(v) => Value::Str(v.get().into()), + e => { + use comemo::Track; + use typst::engine::*; + use typst::eval::*; + use typst::foundations::*; + use typst::introspection::*; + + let mut locator = Locator::default(); + let introspector = Introspector::default(); + let mut tracer = Tracer::new(); + let engine = Engine { + world: self.world().track(), + route: Route::default(), + introspector: introspector.track(), + locator: &mut locator, + tracer: tracer.track_mut(), + }; + + let context = Context::none(); + let mut vm = Vm::new( + engine, + context.track(), + Scopes::new(Some(self.world().library())), + Span::detached(), + ); + + return e.eval(&mut vm).ok(); + } + }) + } } /// The context for searching in the workspace. diff --git a/crates/tinymist-query/src/color_presentation.rs b/crates/tinymist-query/src/color_presentation.rs new file mode 100644 index 00000000..dc07f116 --- /dev/null +++ b/crates/tinymist-query/src/color_presentation.rs @@ -0,0 +1,45 @@ +use typst::foundations::Repr; + +use crate::prelude::*; + +/// The +#[derive(Debug, Clone)] +pub struct ColorPresentationRequest { + /// The path of the document to request color presentations for. + pub path: PathBuf, + /// The color to request presentations for. + pub color: lsp_types::Color, + /// The range of the color to request presentations for. + pub range: LspRange, +} + +impl ColorPresentationRequest { + /// Serve the request. + pub fn request(self) -> Option> { + let color = typst::visualize::Color::Rgb(typst::visualize::Rgb::new( + self.color.red, + self.color.green, + self.color.blue, + self.color.alpha, + )); + Some(vec![ + simple(format!("{:?}", color.to_hex())), + simple(color.to_rgb().repr().to_string()), + simple(color.to_luma().repr().to_string()), + simple(color.to_oklab().repr().to_string()), + simple(color.to_oklch().repr().to_string()), + simple(color.to_rgb().repr().to_string()), + simple(color.to_linear_rgb().repr().to_string()), + simple(color.to_cmyk().repr().to_string()), + simple(color.to_hsl().repr().to_string()), + simple(color.to_hsv().repr().to_string()), + ]) + } +} + +fn simple(label: String) -> ColorPresentation { + ColorPresentation { + label, + ..ColorPresentation::default() + } +} diff --git a/crates/tinymist-query/src/document_color.rs b/crates/tinymist-query/src/document_color.rs new file mode 100644 index 00000000..a30bc93f --- /dev/null +++ b/crates/tinymist-query/src/document_color.rs @@ -0,0 +1,33 @@ +use crate::{analysis::get_color_exprs, prelude::*, SemanticRequest}; + +/// The +#[derive(Debug, Clone)] +pub struct DocumentColorRequest { + /// The. + pub path: PathBuf, +} + +impl SemanticRequest for DocumentColorRequest { + type Response = Vec; + + fn request(self, ctx: &mut AnalysisContext) -> Option { + let source = ctx.source_by_path(&self.path).ok()?; + get_color_exprs(ctx, &source) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tests::*; + + #[test] + fn test() { + snapshot_testing("document_color", &|ctx, path| { + let request = DocumentColorRequest { path: path.clone() }; + + let result = request.request(ctx); + assert_snapshot!(JsonRepr::new_redacted(result, &REDACT_LOC)); + }); + } +} diff --git a/crates/tinymist-query/src/fixtures/document_color/advanced.typ b/crates/tinymist-query/src/fixtures/document_color/advanced.typ new file mode 100644 index 00000000..7e14caf0 --- /dev/null +++ b/crates/tinymist-query/src/fixtures/document_color/advanced.typ @@ -0,0 +1,3 @@ +#let t = luma(240); +#let t = luma(70%); +#let t = color.hsl(1deg, 1, 1); \ No newline at end of file diff --git a/crates/tinymist-query/src/fixtures/document_color/base.typ b/crates/tinymist-query/src/fixtures/document_color/base.typ new file mode 100644 index 00000000..baf4712d --- /dev/null +++ b/crates/tinymist-query/src/fixtures/document_color/base.typ @@ -0,0 +1 @@ +#let t = rgb("#777"); \ No newline at end of file diff --git a/crates/tinymist-query/src/fixtures/document_color/rgb.typ b/crates/tinymist-query/src/fixtures/document_color/rgb.typ new file mode 100644 index 00000000..73834601 --- /dev/null +++ b/crates/tinymist-query/src/fixtures/document_color/rgb.typ @@ -0,0 +1,4 @@ +#let t = rgb("#ccc"); +#let t = rgb("#badbadbad"); +#let t = rgb("#caffee"); +#let t = rgb("#deadbeef"); diff --git a/crates/tinymist-query/src/fixtures/document_color/snaps/test@advanced.typ.snap b/crates/tinymist-query/src/fixtures/document_color/snaps/test@advanced.typ.snap new file mode 100644 index 00000000..a98da6f4 --- /dev/null +++ b/crates/tinymist-query/src/fixtures/document_color/snaps/test@advanced.typ.snap @@ -0,0 +1,34 @@ +--- +source: crates/tinymist-query/src/document_color.rs +expression: "JsonRepr::new_redacted(result, &REDACT_LOC)" +input_file: crates/tinymist-query/src/fixtures/document_color/advanced.typ +--- +[ + { + "color": { + "alpha": 1.0, + "blue": 0.9411764740943909, + "green": 0.9411764740943909, + "red": 0.9411764740943909 + }, + "range": "0:9:0:18" + }, + { + "color": { + "alpha": 1.0, + "blue": 0.699999988079071, + "green": 0.699999988079071, + "red": 0.699999988079071 + }, + "range": "1:9:1:18" + }, + { + "color": { + "alpha": 1.0, + "blue": 0.003906190162524581, + "green": 0.003906702622771263, + "red": 0.003936947789043188 + }, + "range": "2:9:2:30" + } +] diff --git a/crates/tinymist-query/src/fixtures/document_color/snaps/test@base.typ.snap b/crates/tinymist-query/src/fixtures/document_color/snaps/test@base.typ.snap new file mode 100644 index 00000000..b84f28eb --- /dev/null +++ b/crates/tinymist-query/src/fixtures/document_color/snaps/test@base.typ.snap @@ -0,0 +1,16 @@ +--- +source: crates/tinymist-query/src/document_color.rs +expression: "JsonRepr::new_redacted(result, &REDACT_LOC)" +input_file: crates/tinymist-query/src/fixtures/document_color/base.typ +--- +[ + { + "color": { + "alpha": 1.0, + "blue": 0.46666666865348816, + "green": 0.46666666865348816, + "red": 0.46666666865348816 + }, + "range": "0:9:0:20" + } +] diff --git a/crates/tinymist-query/src/fixtures/document_color/snaps/test@rgb.typ.snap b/crates/tinymist-query/src/fixtures/document_color/snaps/test@rgb.typ.snap new file mode 100644 index 00000000..6e8b7669 --- /dev/null +++ b/crates/tinymist-query/src/fixtures/document_color/snaps/test@rgb.typ.snap @@ -0,0 +1,34 @@ +--- +source: crates/tinymist-query/src/document_color.rs +expression: "JsonRepr::new_redacted(result, &REDACT_LOC)" +input_file: crates/tinymist-query/src/fixtures/document_color/rgb.typ +--- +[ + { + "color": { + "alpha": 1.0, + "blue": 0.800000011920929, + "green": 0.800000011920929, + "red": 0.800000011920929 + }, + "range": "0:9:0:20" + }, + { + "color": { + "alpha": 1.0, + "blue": 0.9333333373069763, + "green": 1.0, + "red": 0.7921568751335144 + }, + "range": "2:9:2:23" + }, + { + "color": { + "alpha": 0.9372549057006836, + "blue": 0.7450980544090271, + "green": 0.6784313917160034, + "red": 0.8705882430076599 + }, + "range": "3:9:3:25" + } +] diff --git a/crates/tinymist-query/src/lib.rs b/crates/tinymist-query/src/lib.rs index 137abfd2..cb36e9f1 100644 --- a/crates/tinymist-query/src/lib.rs +++ b/crates/tinymist-query/src/lib.rs @@ -24,6 +24,10 @@ pub(crate) mod code_lens; pub use code_lens::*; pub(crate) mod completion; pub use completion::*; +pub(crate) mod color_presentation; +pub use color_presentation::*; +pub(crate) mod document_color; +pub use document_color::*; pub(crate) mod document_symbol; pub use document_symbol::*; pub(crate) mod document_metrics; @@ -197,6 +201,8 @@ mod polymorphic { GotoDeclaration(GotoDeclarationRequest), References(ReferencesRequest), InlayHint(InlayHintRequest), + DocumentColor(DocumentColorRequest), + ColorPresentation(ColorPresentationRequest), CodeLens(CodeLensRequest), Completion(CompletionRequest), SignatureHelp(SignatureHelpRequest), @@ -225,6 +231,8 @@ mod polymorphic { CompilerQueryRequest::GotoDeclaration(..) => PinnedFirst, CompilerQueryRequest::References(..) => PinnedFirst, CompilerQueryRequest::InlayHint(..) => Unique, + CompilerQueryRequest::DocumentColor(..) => PinnedFirst, + CompilerQueryRequest::ColorPresentation(..) => ContextFreeUnique, CompilerQueryRequest::CodeLens(..) => Unique, CompilerQueryRequest::Completion(..) => Mergable, CompilerQueryRequest::SignatureHelp(..) => PinnedFirst, @@ -252,6 +260,8 @@ mod polymorphic { CompilerQueryRequest::GotoDeclaration(req) => &req.path, CompilerQueryRequest::References(req) => &req.path, CompilerQueryRequest::InlayHint(req) => &req.path, + CompilerQueryRequest::DocumentColor(req) => &req.path, + CompilerQueryRequest::ColorPresentation(req) => &req.path, CompilerQueryRequest::CodeLens(req) => &req.path, CompilerQueryRequest::Completion(req) => &req.path, CompilerQueryRequest::SignatureHelp(req) => &req.path, @@ -280,6 +290,8 @@ mod polymorphic { GotoDeclaration(Option), References(Option>), InlayHint(Option>), + DocumentColor(Option>), + ColorPresentation(Option>), CodeLens(Option>), Completion(Option), SignatureHelp(Option), diff --git a/crates/tinymist-query/src/prelude.rs b/crates/tinymist-query/src/prelude.rs index f38bcfa3..a022ffc7 100644 --- a/crates/tinymist-query/src/prelude.rs +++ b/crates/tinymist-query/src/prelude.rs @@ -9,12 +9,13 @@ pub use ecow::EcoVec; pub use itertools::{Format, Itertools}; pub use log::{error, trace}; pub use lsp_types::{ - request::GotoDeclarationResponse, CodeLens, CompletionResponse, DiagnosticRelatedInformation, - DocumentSymbol, DocumentSymbolResponse, Documentation, FoldingRange, GotoDefinitionResponse, - Hover, InlayHint, LanguageString, Location as LspLocation, LocationLink, MarkedString, - MarkupContent, MarkupKind, Position as LspPosition, PrepareRenameResponse, SelectionRange, - SemanticTokens, SemanticTokensDelta, SemanticTokensFullDeltaResult, SemanticTokensResult, - SignatureHelp, SignatureInformation, SymbolInformation, Url, WorkspaceEdit, + request::GotoDeclarationResponse, CodeLens, ColorInformation, ColorPresentation, + CompletionResponse, DiagnosticRelatedInformation, DocumentSymbol, DocumentSymbolResponse, + Documentation, FoldingRange, GotoDefinitionResponse, Hover, InlayHint, LanguageString, + Location as LspLocation, LocationLink, MarkedString, MarkupContent, MarkupKind, + Position as LspPosition, PrepareRenameResponse, SelectionRange, SemanticTokens, + SemanticTokensDelta, SemanticTokensFullDeltaResult, SemanticTokensResult, SignatureHelp, + SignatureInformation, SymbolInformation, Url, WorkspaceEdit, }; pub use reflexo::vector::ir::DefId; pub use serde_json::Value as JsonValue; diff --git a/crates/tinymist/src/server/lsp.rs b/crates/tinymist/src/server/lsp.rs index 72ec0242..2b35c2cb 100644 --- a/crates/tinymist/src/server/lsp.rs +++ b/crates/tinymist/src/server/lsp.rs @@ -303,6 +303,8 @@ impl TypstLanguageServer { request_fn!(SelectionRangeRequest, Self::selection_range), // latency insensitive request_fn!(InlayHintRequest, Self::inlay_hint), + request_fn!(DocumentColor, Self::document_color), + request_fn!(ColorPresentationRequest, Self::color_presentation), request_fn!(HoverRequest, Self::hover), request_fn!(CodeLensRequest, Self::code_lens), request_fn!(FoldingRangeRequest, Self::folding_range), @@ -1147,6 +1149,24 @@ impl TypstLanguageServer { run_query!(self.InlayHint(path, range)) } + fn document_color( + &self, + params: DocumentColorParams, + ) -> LspResult>> { + let path = as_path(params.text_document); + run_query!(self.DocumentColor(path)) + } + + fn color_presentation( + &self, + params: ColorPresentationParams, + ) -> LspResult>> { + let path = as_path(params.text_document); + let color = params.color; + let range = params.range; + run_query!(self.ColorPresentation(path, color, range)) + } + fn code_lens(&self, params: CodeLensParams) -> LspResult>> { let path = as_path(params.text_document); run_query!(self.CodeLens(path)) diff --git a/crates/tinymist/src/server/lsp_init.rs b/crates/tinymist/src/server/lsp_init.rs index 708e363f..0686f359 100644 --- a/crates/tinymist/src/server/lsp_init.rs +++ b/crates/tinymist/src/server/lsp_init.rs @@ -451,6 +451,7 @@ impl Init { work_done_progress: None, }, }), + color_provider: Some(ColorProviderCapability::Simple(true)), document_symbol_provider: Some(OneOf::Left(true)), workspace_symbol_provider: Some(OneOf::Left(true)), selection_range_provider: Some(SelectionRangeProviderCapability::Simple(true)), diff --git a/crates/tinymist/src/state.rs b/crates/tinymist/src/state.rs index a770b537..b968dcc2 100644 --- a/crates/tinymist/src/state.rs +++ b/crates/tinymist/src/state.rs @@ -227,6 +227,7 @@ impl TypstLanguageServer { FoldingRange(req) => query_source!(self, FoldingRange, req), SelectionRange(req) => query_source!(self, SelectionRange, req), DocumentSymbol(req) => query_source!(self, DocumentSymbol, req), + ColorPresentation(req) => Ok(CompilerQueryResponse::ColorPresentation(req.request())), _ => { let client = &self.primary; if !self.pinning && !self.config.compile.has_default_entry_path { @@ -260,6 +261,7 @@ impl TypstLanguageServer { GotoDeclaration(req) => query_world!(client, GotoDeclaration, req), References(req) => query_world!(client, References, req), InlayHint(req) => query_world!(client, InlayHint, req), + DocumentColor(req) => query_world!(client, DocumentColor, req), CodeLens(req) => query_world!(client, CodeLens, req), Completion(req) => query_state!(client, Completion, req), SignatureHelp(req) => query_world!(client, SignatureHelp, req), @@ -278,6 +280,7 @@ impl TypstLanguageServer { | SemanticTokensDelta(..) | Formatting(..) | DocumentSymbol(..) + | ColorPresentation(..) | SemanticTokensFull(..) => unreachable!(), } } diff --git a/tests/e2e/main.rs b/tests/e2e/main.rs index fba74b70..d5330293 100644 --- a/tests/e2e/main.rs +++ b/tests/e2e/main.rs @@ -375,7 +375,7 @@ fn e2e() { }); let hash = replay_log(&tinymist_binary, &root.join("neovim")); - insta::assert_snapshot!(hash, @"siphash128_13:7f6e1eb7ad3be8efe0cd45b956cce954"); + insta::assert_snapshot!(hash, @"siphash128_13:201f10c7c427e8d0975ad08a07130f0d"); } { @@ -386,7 +386,7 @@ fn e2e() { }); let hash = replay_log(&tinymist_binary, &root.join("vscode")); - insta::assert_snapshot!(hash, @"siphash128_13:f5c933fa12aa44edd674ef3193239d2a"); + insta::assert_snapshot!(hash, @"siphash128_13:b0df822751a28735300f974c06710ddb"); } }