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:
Myriad-Dreamin 2024-04-06 14:10:48 +08:00 committed by GitHub
parent 8a3fdbc642
commit 4afe4b30d9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 390 additions and 10 deletions

View file

@ -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;

View 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(())
}
}

View file

@ -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.

View 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()
}
}

View 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));
});
}
}

View file

@ -0,0 +1,3 @@
#let t = luma(240);
#let t = luma(70%);
#let t = color.hsl(1deg, 1, 1);

View file

@ -0,0 +1 @@
#let t = rgb("#777");

View file

@ -0,0 +1,4 @@
#let t = rgb("#ccc");
#let t = rgb("#badbadbad");
#let t = rgb("#caffee");
#let t = rgb("#deadbeef");

View file

@ -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"
}
]

View file

@ -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"
}
]

View file

@ -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"
}
]

View file

@ -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>),

View file

@ -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;