refactor: prepare for linting on syntaxes (#1640)

* refactor: for query

* refactor: for diag

* feat: lazy hash expr info

* feat: hash resolves

* fix: update snapshot
This commit is contained in:
Myriad-Dreamin 2025-04-09 04:10:47 +08:00 committed by GitHub
parent 01afa463f4
commit 054d3aecc0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 261 additions and 241 deletions

View file

@ -4,7 +4,7 @@ use tinymist_project::LspWorld;
use tinymist_world::vfs::WorkspaceResolver;
use typst::{diag::SourceDiagnostic, syntax::Span};
use crate::{prelude::*, LspWorldExt};
use crate::{analysis::Analysis, prelude::*, LspWorldExt};
use regex::RegexSet;
@ -16,52 +16,44 @@ type TypstSeverity = typst::diag::Severity;
/// Converts a list of Typst diagnostics to LSP diagnostics,
/// with potential refinements on the error messages.
pub fn check_doc<'a>(
pub fn convert_diagnostics<'a>(
world: &LspWorld,
errors: impl IntoIterator<Item = &'a TypstDiagnostic>,
position_encoding: PositionEncoding,
) -> DiagnosticsMap {
CheckDocWorker::new(world, position_encoding)
.check()
.convert_all(errors)
let analysis = Analysis {
position_encoding,
..Analysis::default()
};
let mut ctx = analysis.enter(world.clone());
DiagWorker::new(&mut ctx).convert_all(errors)
}
/// Context for converting Typst diagnostics to LSP diagnostics.
pub(crate) struct CheckDocWorker<'a> {
/// The worker for collecting diagnostics.
pub(crate) struct DiagWorker<'a> {
/// The world surface for Typst compiler.
pub world: &'a LspWorld,
/// The position encoding for the source.
pub position_encoding: PositionEncoding,
pub ctx: &'a mut LocalContext,
/// Results
pub results: DiagnosticsMap,
}
impl std::ops::Deref for CheckDocWorker<'_> {
type Target = LspWorld;
fn deref(&self) -> &Self::Target {
self.world
}
}
impl<'w> CheckDocWorker<'w> {
impl<'w> DiagWorker<'w> {
/// Creates a new `CheckDocWorker` instance.
pub fn new(world: &'w LspWorld, position_encoding: PositionEncoding) -> Self {
pub fn new(ctx: &'w mut LocalContext) -> Self {
Self {
world,
position_encoding,
ctx,
results: DiagnosticsMap::default(),
}
}
/// Runs code check on the document.
pub fn check(mut self) -> Self {
for dep in self.world.depended_files() {
for dep in self.ctx.world.depended_files() {
if WorkspaceResolver::is_package_file(dep) {
continue;
}
let Ok(source) = self.world.source(dep) else {
let Ok(source) = self.ctx.world.source(dep) else {
continue;
};
let res = lint_source(&source);
@ -89,7 +81,7 @@ impl<'w> CheckDocWorker<'w> {
/// Converts a list of Typst diagnostics to LSP diagnostics.
pub fn handle(&mut self, diag: &TypstDiagnostic) {
match convert_diagnostic(self, diag) {
match self.convert_diagnostic(diag) {
Ok((uri, diagnostic)) => {
self.results.entry(uri).or_default().push(diagnostic);
}
@ -98,121 +90,92 @@ impl<'w> CheckDocWorker<'w> {
}
}
}
}
fn convert_diagnostic(
ctx: &CheckDocWorker,
typst_diagnostic: &TypstDiagnostic,
) -> anyhow::Result<(Url, Diagnostic)> {
let typst_diagnostic = {
let mut diag = Cow::Borrowed(typst_diagnostic);
fn convert_diagnostic(
&self,
typst_diagnostic: &TypstDiagnostic,
) -> anyhow::Result<(Url, Diagnostic)> {
let typst_diagnostic = {
let mut diag = Cow::Borrowed(typst_diagnostic);
// Extend more refiners here by adding their instances.
let refiners: &[&dyn DiagnosticRefiner] =
&[&DeprecationRefiner::<13> {}, &OutOfRootHintRefiner {}];
// Extend more refiners here by adding their instances.
let refiners: &[&dyn DiagnosticRefiner] =
&[&DeprecationRefiner::<13> {}, &OutOfRootHintRefiner {}];
// NOTE: It would be nice to have caching here.
for refiner in refiners {
if refiner.matches(&diag) {
diag = Cow::Owned(refiner.refine(diag.into_owned()));
// NOTE: It would be nice to have caching here.
for refiner in refiners {
if refiner.matches(&diag) {
diag = Cow::Owned(refiner.refine(diag.into_owned()));
}
}
}
diag
};
diag
};
let (id, span) = diagnostic_span_id(ctx, &typst_diagnostic);
let uri = ctx.uri_for_id(id)?;
let source = ctx.source(id)?;
let lsp_range = diagnostic_range(&source, span, ctx.position_encoding);
let (id, span) = self.diagnostic_span_id(&typst_diagnostic);
let uri = self.ctx.uri_for_id(id)?;
let source = self.ctx.source_by_id(id)?;
let lsp_range = self.diagnostic_range(&source, span);
let lsp_severity = diagnostic_severity(typst_diagnostic.severity);
let lsp_message = diagnostic_message(&typst_diagnostic);
let lsp_severity = diagnostic_severity(typst_diagnostic.severity);
let lsp_message = diagnostic_message(&typst_diagnostic);
let tracepoints =
diagnostic_related_information(ctx, &typst_diagnostic, ctx.position_encoding)?;
let diagnostic = Diagnostic {
range: lsp_range,
severity: Some(lsp_severity),
message: lsp_message,
source: Some("typst".to_owned()),
related_information: (!typst_diagnostic.trace.is_empty()).then(|| {
typst_diagnostic
.trace
.iter()
.flat_map(|tracepoint| self.to_related_info(tracepoint))
.collect()
}),
..Default::default()
};
let diagnostic = Diagnostic {
range: lsp_range,
severity: Some(lsp_severity),
message: lsp_message,
source: Some("typst".to_owned()),
related_information: Some(tracepoints),
..Default::default()
};
Ok((uri, diagnostic))
}
fn tracepoint_to_relatedinformation(
ctx: &CheckDocWorker,
tracepoint: &Spanned<Tracepoint>,
position_encoding: PositionEncoding,
) -> anyhow::Result<Option<DiagnosticRelatedInformation>> {
if let Some(id) = tracepoint.span.id() {
let uri = ctx.uri_for_id(id)?;
let source = ctx.source(id)?;
if let Some(typst_range) = source.range(tracepoint.span) {
let lsp_range = to_lsp_range(typst_range, &source, position_encoding);
return Ok(Some(DiagnosticRelatedInformation {
location: LspLocation {
uri,
range: lsp_range,
},
message: tracepoint.v.to_string(),
}));
}
Ok((uri, diagnostic))
}
Ok(None)
}
fn to_related_info(
&self,
tracepoint: &Spanned<Tracepoint>,
) -> Option<DiagnosticRelatedInformation> {
let id = tracepoint.span.id()?;
// todo: expensive uri_for_id
let uri = self.ctx.uri_for_id(id).ok()?;
let source = self.ctx.source_by_id(id).ok()?;
fn diagnostic_related_information(
project: &CheckDocWorker,
typst_diagnostic: &TypstDiagnostic,
position_encoding: PositionEncoding,
) -> anyhow::Result<Vec<DiagnosticRelatedInformation>> {
let mut tracepoints = vec![];
let typst_range = source.range(tracepoint.span)?;
let lsp_range = self.ctx.to_lsp_range(typst_range, &source);
for tracepoint in &typst_diagnostic.trace {
if let Some(info) =
tracepoint_to_relatedinformation(project, tracepoint, position_encoding)?
{
tracepoints.push(info);
}
Some(DiagnosticRelatedInformation {
location: LspLocation {
uri,
range: lsp_range,
},
message: tracepoint.v.to_string(),
})
}
Ok(tracepoints)
}
fn diagnostic_span_id(&self, typst_diagnostic: &TypstDiagnostic) -> (TypstFileId, Span) {
iter::once(typst_diagnostic.span)
.chain(typst_diagnostic.trace.iter().map(|trace| trace.span))
.find_map(|span| Some((span.id()?, span)))
.unwrap_or_else(|| (self.ctx.world.main(), Span::detached()))
}
fn diagnostic_span_id(
ctx: &CheckDocWorker,
typst_diagnostic: &TypstDiagnostic,
) -> (TypstFileId, Span) {
iter::once(typst_diagnostic.span)
.chain(typst_diagnostic.trace.iter().map(|trace| trace.span))
.find_map(|span| Some((span.id()?, span)))
.unwrap_or_else(|| (ctx.main(), Span::detached()))
}
fn diagnostic_range(
source: &Source,
typst_span: Span,
position_encoding: PositionEncoding,
) -> LspRange {
// Due to nvaner/typst-lsp#241 and maybe typst/typst#2035, we sometimes fail to
// find the span. In that case, we use a default span as a better
// alternative to panicking.
//
// This may have been fixed after Typst 0.7.0, but it's still nice to avoid
// panics in case something similar reappears.
match source.find(typst_span) {
Some(node) => {
let typst_range = node.range();
to_lsp_range(typst_range, source, position_encoding)
fn diagnostic_range(&self, source: &Source, typst_span: Span) -> LspRange {
// Due to nvaner/typst-lsp#241 and maybe typst/typst#2035, we sometimes fail to
// find the span. In that case, we use a default span as a better
// alternative to panicking.
//
// This may have been fixed after Typst 0.7.0, but it's still nice to avoid
// panics in case something similar reappears.
match source.find(typst_span) {
Some(node) => self.ctx.to_lsp_range(node.range(), source),
None => LspRange::new(LspPosition::new(0, 0), LspPosition::new(0, 0)),
}
None => LspRange::new(LspPosition::new(0, 0), LspPosition::new(0, 0)),
}
}