diff --git a/Cargo.lock b/Cargo.lock index 59566d3b4..3423156d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -93,6 +93,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5dd14596c0e5b954530d0e6f1fd99b89c03e313aa2086e8da4303701a09e1cf" + [[package]] name = "block-buffer" version = "0.10.4" @@ -179,7 +185,7 @@ version = "3.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5" dependencies = [ - "bitflags", + "bitflags 1.3.2", "clap_lex 0.2.4", "indexmap", "textwrap", @@ -191,7 +197,7 @@ version = "4.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d7ae14b20b94cb02149ed21a86c423859cbe18dc7ed69845cace50e52b40a5" dependencies = [ - "bitflags", + "bitflags 1.3.2", "clap_derive", "clap_lex 0.3.2", "is-terminal", @@ -724,7 +730,7 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" dependencies = [ - "bitflags", + "bitflags 1.3.2", "inotify-sys", "libc", ] @@ -843,7 +849,7 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8367585489f01bc55dd27404dcf56b95e6da061a256a666ab23be9ba96a2e587" dependencies = [ - "bitflags", + "bitflags 1.3.2", "libc", ] @@ -931,7 +937,7 @@ version = "0.94.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b63735a13a1f9cd4f4835223d828ed9c2e35c8c5e61837774399f558b6a1237" dependencies = [ - "bitflags", + "bitflags 1.3.2", "serde", "serde_json", "serde_repr", @@ -998,7 +1004,7 @@ version = "5.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "58ea850aa68a06e48fdb069c0ec44d0d64c8dbffa49bf3b6f7f0a901fdea1ba9" dependencies = [ - "bitflags", + "bitflags 1.3.2", "crossbeam-channel", "filetime", "fsevent-sys", @@ -1301,7 +1307,7 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] @@ -1357,7 +1363,7 @@ version = "0.36.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd5c6ff11fecd55b40746d1995a02f2eb375bf8c00d192d521ee09f42bef37bc" dependencies = [ - "bitflags", + "bitflags 1.3.2", "errno", "io-lifetimes", "libc", @@ -1597,6 +1603,7 @@ version = "5.4.0" dependencies = [ "anyhow", "assert_unordered", + "bitflags 2.0.1", "chrono", "clap 4.1.8", "criterion", diff --git a/Cargo.toml b/Cargo.toml index 00d1cc4e2..bd9806f02 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,7 +36,7 @@ doctest = false [dependencies] anyhow = "1.0.69" chrono = { version = "0.4.23", default-features = false, features = ["std"] } -clap = { version = "4.1.6", features = ["derive"] } +clap = { version = "4.1.8", features = ["derive"] } crossbeam-channel = "0.5.6" dashmap = "5.4.0" dirs = "4.0.0" @@ -69,6 +69,7 @@ thiserror = "1.0.38" threadpool = "1.8.1" titlecase = "2.2.1" unicode-normalization = "0.1.22" +bitflags = "2.0.1" [dependencies.salsa] git = "https://github.com/salsa-rs/salsa" diff --git a/src/db/analysis.rs b/src/db/analysis.rs index 1f1bd508a..4101b2f34 100644 --- a/src/db/analysis.rs +++ b/src/db/analysis.rs @@ -104,6 +104,12 @@ impl TheoremEnvironment { } } +#[derive(Debug, PartialEq, Eq, Clone, Hash)] +pub struct TexCitation { + pub key: String, + pub range: TextRange, +} + #[salsa::tracked] pub struct GraphicsPath { #[return_ref] @@ -130,6 +136,9 @@ pub struct TexAnalysis { #[return_ref] pub links: Vec, + #[return_ref] + pub citations: Vec, + #[return_ref] pub labels: Vec, @@ -162,6 +171,7 @@ impl TexAnalysis { impl TexAnalysis { pub(super) fn analyze(db: &dyn Db, root: &latex::SyntaxNode) -> Self { let mut links = Vec::new(); + let mut citations = Vec::new(); let mut labels = Vec::new(); let mut label_numbers = Vec::new(); let mut theorem_environments = Vec::new(); @@ -178,6 +188,17 @@ impl TexAnalysis { NodeOrToken::Node(node) => { TexLink::of_include(db, node.clone(), &mut links) .or_else(|| TexLink::of_import(db, node.clone(), &mut links)) + .or_else(|| { + let citation = latex::Citation::cast(node.clone())?; + for key in citation.key_list()?.keys() { + citations.push(TexCitation { + key: key.to_string(), + range: latex::small_range(&key), + }); + } + + Some(()) + }) .or_else(|| label::Name::of_definition(db, node.clone(), &mut labels)) .or_else(|| label::Name::of_reference(db, node.clone(), &mut labels)) .or_else(|| label::Name::of_reference_range(db, node.clone(), &mut labels)) @@ -210,6 +231,7 @@ impl TexAnalysis { Self::new( db, links, + citations, labels, label_numbers, theorem_environments, diff --git a/src/features.rs b/src/features.rs index 0d82bcb4f..37e159da9 100644 --- a/src/features.rs +++ b/src/features.rs @@ -10,5 +10,6 @@ pub mod inlay_hint; pub mod link; pub mod reference; pub mod rename; +pub mod semantic_tokens; pub mod symbol; pub mod workspace_command; diff --git a/src/features/semantic_tokens.rs b/src/features/semantic_tokens.rs new file mode 100644 index 000000000..9cab23420 --- /dev/null +++ b/src/features/semantic_tokens.rs @@ -0,0 +1,163 @@ +mod citations; +mod label; +mod math_delimiter; + +use bitflags::bitflags; +use lsp_types::{ + Position, Range, SemanticToken, SemanticTokenModifier, SemanticTokenType, SemanticTokens, + SemanticTokensLegend, Url, +}; +use rowan::{TextLen, TextRange}; + +use crate::{ + db::{Document, Workspace}, + util::{line_index::LineIndex, line_index_ext::LineIndexExt}, + Db, +}; + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash)] +#[repr(u32)] +enum TokenKind { + GenericLabel = 0, + SectionLabel, + FloatLabel, + TheoremLabel, + EquationLabel, + EnumItemLabel, + + GenericCitation, + ArticleCitation, + BookCitation, + CollectionCitation, + PartCitation, + ThesisCitation, + + MathDelimiter, +} + +bitflags! { + #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash)] + struct TokenModifiers: u32 { + const NONE = 0; + const UNDEFINED = 1; + const UNUSED = 2; + const DEPRECATED = 4; + } +} + +pub fn legend() -> SemanticTokensLegend { + SemanticTokensLegend { + token_types: vec![ + SemanticTokenType::new("genericLabel"), + SemanticTokenType::new("sectionLabel"), + SemanticTokenType::new("floatLabel"), + SemanticTokenType::new("theoremLabel"), + SemanticTokenType::new("equationLabel"), + SemanticTokenType::new("enumItemLabel"), + SemanticTokenType::new("genericCitation"), + SemanticTokenType::new("articleCitation"), + SemanticTokenType::new("bookCitation"), + SemanticTokenType::new("collectionCitation"), + SemanticTokenType::new("partCitation"), + SemanticTokenType::new("thesisCitation"), + SemanticTokenType::new("mathDelimiter"), + ], + token_modifiers: vec![ + SemanticTokenModifier::new("undefined"), + SemanticTokenModifier::new("unused"), + SemanticTokenModifier::new("deprecated"), + ], + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] +struct Token { + range: TextRange, + kind: TokenKind, + modifiers: TokenModifiers, +} + +#[derive(Debug, Default)] +struct TokenBuilder { + tokens: Vec, +} + +impl TokenBuilder { + pub fn push(&mut self, token: Token) { + log::info!("Adding sem token {token:#?}"); + self.tokens.push(token); + } + + pub fn finish(mut self, line_index: &LineIndex) -> SemanticTokens { + let mut data = Vec::new(); + + self.tokens.sort_by_key(|token| token.range.start()); + + let mut last_pos = Position::new(0, 0); + for token in self.tokens { + let range = line_index.line_col_lsp_range(token.range); + let length = range.end.character - range.start.character; + let token_type = token.kind as u32; + let token_modifiers_bitset = token.modifiers.bits(); + + if range.start.line > last_pos.line { + let delta_line = range.start.line - last_pos.line; + let delta_start = range.start.character; + data.push(SemanticToken { + delta_line, + delta_start, + length, + token_type, + token_modifiers_bitset, + }); + } else { + let delta_line = 0; + let delta_start = range.start.character - last_pos.character; + data.push(SemanticToken { + delta_line, + delta_start, + length, + token_type, + token_modifiers_bitset, + }); + } + + last_pos = range.start; + } + + SemanticTokens { + result_id: None, + data, + } + } +} + +#[derive(Clone, Copy)] +struct Context<'db> { + db: &'db dyn Db, + document: Document, + viewport: TextRange, +} + +pub fn find_all(db: &dyn Db, uri: &Url, viewport: Option) -> Option { + let workspace = Workspace::get(db); + let document = workspace.lookup_uri(db, uri)?; + let viewport = viewport.map_or_else( + || TextRange::new(0.into(), document.text(db).text_len()), + |range| document.line_index(db).offset_lsp_range(range), + ); + + let context = Context { + db, + document, + viewport, + }; + + let mut builder = TokenBuilder::default(); + + label::find(context, &mut builder); + citations::find(context, &mut builder); + math_delimiter::find(context, &mut builder); + + Some(builder.finish(document.line_index(db))) +} diff --git a/src/features/semantic_tokens/citations.rs b/src/features/semantic_tokens/citations.rs new file mode 100644 index 000000000..b1c71e448 --- /dev/null +++ b/src/features/semantic_tokens/citations.rs @@ -0,0 +1,60 @@ +use rowan::ast::AstNode; + +use crate::{ + db::Workspace, + syntax::bibtex::{self, HasName, HasType}, + util::lang_data::{BibtexEntryTypeCategory, LANGUAGE_DATA}, +}; + +use super::{Context, Token, TokenBuilder, TokenKind, TokenModifiers}; + +pub(super) fn find(context: Context, builder: &mut TokenBuilder) -> Option<()> { + let db = context.db; + let analysis = context.document.parse(db).as_tex()?.analyze(db); + for citation in analysis + .citations(db) + .iter() + .filter(|citation| context.viewport.intersect(citation.range).is_some()) + { + let entry = Workspace::get(db) + .related(db, context.document) + .iter() + .filter_map(|document| document.parse(db).as_bib()) + .flat_map(|data| data.root(db).children()) + .filter_map(bibtex::Entry::cast) + .find(|entry| { + entry + .name_token() + .map_or(false, |name| name.text() == &citation.key) + }); + + let modifiers = if entry.is_some() { + TokenModifiers::NONE + } else { + TokenModifiers::UNDEFINED + }; + + let kind = match entry + .and_then(|entry| entry.type_token()) + .and_then(|token| LANGUAGE_DATA.find_entry_type(&token.text()[1..])) + .map(|doc| doc.category) + { + Some(BibtexEntryTypeCategory::String) => unreachable!(), + Some(BibtexEntryTypeCategory::Misc) => TokenKind::GenericCitation, + Some(BibtexEntryTypeCategory::Article) => TokenKind::ArticleCitation, + Some(BibtexEntryTypeCategory::Book) => TokenKind::BookCitation, + Some(BibtexEntryTypeCategory::Part) => TokenKind::PartCitation, + Some(BibtexEntryTypeCategory::Thesis) => TokenKind::ThesisCitation, + Some(BibtexEntryTypeCategory::Collection) => TokenKind::CollectionCitation, + None => TokenKind::GenericCitation, + }; + + builder.push(Token { + range: citation.range, + kind, + modifiers, + }); + } + + Some(()) +} diff --git a/src/features/semantic_tokens/label.rs b/src/features/semantic_tokens/label.rs new file mode 100644 index 000000000..7e14d77f8 --- /dev/null +++ b/src/features/semantic_tokens/label.rs @@ -0,0 +1,95 @@ +use crate::{ + db::{analysis::label, Document, Workspace}, + util, Db, +}; + +use super::{Context, Token, TokenBuilder, TokenKind, TokenModifiers}; + +pub(super) fn find(context: Context, builder: &mut TokenBuilder) -> Option<()> { + let db = context.db; + let labels = context.document.parse(db).as_tex()?.analyze(db).labels(db); + for label in labels + .iter() + .filter(|label| context.viewport.intersect(label.range(db)).is_some()) + .copied() + { + let kind = token_type(context, label); + let modifiers = token_modifiers(context, label); + let range = label.range(db); + builder.push(Token { + range, + kind, + modifiers, + }); + } + + Some(()) +} + +fn token_type(context: Context, label: label::Name) -> TokenKind { + let db = context.db; + let definition = match label.origin(db) { + label::Origin::Definition(_) => Some((context.document, label)), + label::Origin::Reference(_) | label::Origin::ReferenceRange(_) => { + util::label::find_label_definition(db, context.document, label.name(db)) + } + }; + + match definition + .and_then(|(doc, label)| util::label::render(db, doc, label)) + .map(|label| label.object) + { + Some(util::label::LabeledObject::Section { .. }) => TokenKind::SectionLabel, + Some(util::label::LabeledObject::Float { .. }) => TokenKind::FloatLabel, + Some(util::label::LabeledObject::EnumItem { .. }) => TokenKind::EnumItemLabel, + Some(util::label::LabeledObject::Equation { .. }) => TokenKind::EquationLabel, + Some(util::label::LabeledObject::Theorem { .. }) => TokenKind::TheoremLabel, + None => TokenKind::GenericLabel, + } +} + +fn token_modifiers(context: Context, label: label::Name) -> TokenModifiers { + let db = context.db; + let name = label.name(db).text(db); + match label.origin(db) { + label::Origin::Definition(_) => { + if !is_label_referenced(db, context.document, name) { + TokenModifiers::UNUSED + } else { + TokenModifiers::NONE + } + } + label::Origin::Reference(_) | label::Origin::ReferenceRange(_) => { + if !is_label_defined(db, context.document, name) { + TokenModifiers::UNDEFINED + } else { + TokenModifiers::NONE + } + } + } +} + +fn is_label_defined(db: &dyn Db, child: Document, name: &str) -> bool { + Workspace::get(db) + .related(db, child) + .iter() + .filter_map(|document| document.parse(db).as_tex()) + .flat_map(|data| data.analyze(db).labels(db)) + .filter(|label| matches!(label.origin(db), label::Origin::Definition(_))) + .any(|label| label.name(db).text(db) == name) +} + +fn is_label_referenced(db: &dyn Db, child: Document, name: &str) -> bool { + Workspace::get(db) + .related(db, child) + .iter() + .filter_map(|document| document.parse(db).as_tex()) + .flat_map(|data| data.analyze(db).labels(db)) + .filter(|label| { + matches!( + label.origin(db), + label::Origin::Reference(_) | label::Origin::ReferenceRange(_) + ) + }) + .any(|label| label.name(db).text(db) == name) +} diff --git a/src/features/semantic_tokens/math_delimiter.rs b/src/features/semantic_tokens/math_delimiter.rs new file mode 100644 index 000000000..109822d0b --- /dev/null +++ b/src/features/semantic_tokens/math_delimiter.rs @@ -0,0 +1,24 @@ +use crate::syntax::latex; + +use super::{Context, Token, TokenBuilder, TokenKind, TokenModifiers}; + +pub(super) fn find(context: Context, builder: &mut TokenBuilder) -> Option<()> { + let db = context.db; + let root = context.document.parse(db).as_tex()?.root(db); + + for token in root + .covering_element(context.viewport) + .as_node()? + .descendants_with_tokens() + .filter_map(|elem| elem.into_token()) + .filter(|token| token.kind() == latex::DOLLAR && token.text() == "$$") + { + builder.push(Token { + range: token.text_range(), + kind: TokenKind::MathDelimiter, + modifiers: TokenModifiers::DEPRECATED, + }); + } + + Some(()) +} diff --git a/src/server.rs b/src/server.rs index bf0ef6426..65b58f71f 100644 --- a/src/server.rs +++ b/src/server.rs @@ -28,7 +28,7 @@ use crate::{ build::{self, BuildParams, BuildResult, BuildStatus}, completion::{self, builder::CompletionItemData}, definition, folding, formatting, forward_search, highlight, hover, inlay_hint, link, - reference, rename, symbol, + reference, rename, semantic_tokens, symbol, workspace_command::{change_environment, clean, dep_graph}, }, normalize_uri, @@ -179,6 +179,14 @@ impl Server { ..Default::default() }), inlay_hint_provider: Some(OneOf::Left(true)), + semantic_tokens_provider: Some( + SemanticTokensServerCapabilities::SemanticTokensOptions(SemanticTokensOptions { + work_done_progress_options: Default::default(), + legend: semantic_tokens::legend(), + range: Some(true), + full: Some(SemanticTokensFullOptions::Bool(true)), + }), + ), ..ServerCapabilities::default() } } @@ -707,14 +715,6 @@ impl Server { Ok(()) } - fn semantic_tokens_range( - &self, - _id: RequestId, - _params: SemanticTokensRangeParams, - ) -> Result<()> { - Ok(()) - } - fn build(&mut self, id: RequestId, params: BuildParams) -> Result<()> { let mut uri = params.text_document.uri; normalize_uri(&mut uri); @@ -814,6 +814,26 @@ impl Server { Ok(()) } + fn semantic_tokens_full(&mut self, id: RequestId, params: SemanticTokensParams) -> Result<()> { + self.run_with_db(id, move |db| { + semantic_tokens::find_all(db, ¶ms.text_document.uri, None) + }); + + Ok(()) + } + + fn semantic_tokens_range( + &mut self, + id: RequestId, + params: SemanticTokensRangeParams, + ) -> Result<()> { + self.run_with_db(id, move |db| { + semantic_tokens::find_all(db, ¶ms.text_document.uri, Some(params.range)) + }); + + Ok(()) + } + fn handle_file_event(&mut self, event: notify::Event) { let mut changed = false; @@ -905,6 +925,9 @@ impl Server { .on::(|id, params| { self.semantic_tokens_range(id, params) })? + .on::(|id, params| { + self.semantic_tokens_full(id, params) + })? .on::(|id,params| { self.inlay_hints(id, params) })?