mirror of
https://github.com/Myriad-Dreamin/tinymist.git
synced 2025-07-24 13:13:43 +00:00
feat: add basic color providers (#171)
* feat: provide document color api * feat: provide color presentation api. * dev: update snapshot * dev: update snapshot
This commit is contained in:
parent
8a3fdbc642
commit
4afe4b30d9
18 changed files with 390 additions and 10 deletions
|
@ -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;
|
||||
|
|
128
crates/tinymist-query/src/analysis/color_exprs.rs
Normal file
128
crates/tinymist-query/src/analysis/color_exprs.rs
Normal file
|
@ -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<Vec<ColorInformation>> {
|
||||
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<ColorInformation>,
|
||||
}
|
||||
|
||||
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::<ast::FuncCall>()?;
|
||||
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<usize>, 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(())
|
||||
}
|
||||
}
|
|
@ -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<Value> {
|
||||
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.
|
||||
|
|
45
crates/tinymist-query/src/color_presentation.rs
Normal file
45
crates/tinymist-query/src/color_presentation.rs
Normal file
|
@ -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<Vec<ColorPresentation>> {
|
||||
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()
|
||||
}
|
||||
}
|
33
crates/tinymist-query/src/document_color.rs
Normal file
33
crates/tinymist-query/src/document_color.rs
Normal file
|
@ -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<ColorInformation>;
|
||||
|
||||
fn request(self, ctx: &mut AnalysisContext) -> Option<Self::Response> {
|
||||
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));
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
#let t = luma(240);
|
||||
#let t = luma(70%);
|
||||
#let t = color.hsl(1deg, 1, 1);
|
|
@ -0,0 +1 @@
|
|||
#let t = rgb("#777");
|
|
@ -0,0 +1,4 @@
|
|||
#let t = rgb("#ccc");
|
||||
#let t = rgb("#badbadbad");
|
||||
#let t = rgb("#caffee");
|
||||
#let t = rgb("#deadbeef");
|
|
@ -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"
|
||||
}
|
||||
]
|
|
@ -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"
|
||||
}
|
||||
]
|
|
@ -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"
|
||||
}
|
||||
]
|
|
@ -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<GotoDeclarationResponse>),
|
||||
References(Option<Vec<LspLocation>>),
|
||||
InlayHint(Option<Vec<InlayHint>>),
|
||||
DocumentColor(Option<Vec<ColorInformation>>),
|
||||
ColorPresentation(Option<Vec<ColorPresentation>>),
|
||||
CodeLens(Option<Vec<CodeLens>>),
|
||||
Completion(Option<CompletionResponse>),
|
||||
SignatureHelp(Option<SignatureHelp>),
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue