diff --git a/CHANGELOG.md b/CHANGELOG.md index d859724e8..7e33aeeaf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,8 +9,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Add `texlab.inlayHints.labelDefinitions` and `texlab.inlayHints.labelReferences` options ([#753](https://github.com/latex-lsp/texlab/issues/753)) -- Display inlay hints for label references by default ([#753](https://github.com/latex-lsp/texlab/issues/753)) +- Add inlay hints for label references and citations ([#753](https://github.com/latex-lsp/texlab/issues/753)) +- Add new options to configure inlay hints ([#753](https://github.com/latex-lsp/texlab/issues/753)): + - `texlab.inlayHints.labelDefinitions` + - `texlab.inlayHints.labelReferences` + - `texlab.inlayHints.citations` ## [5.10.1] - 2023-10-10 diff --git a/Cargo.lock b/Cargo.lock index b565eaf54..f427f9ba2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -805,6 +805,7 @@ name = "inlay-hints" version = "0.0.0" dependencies = [ "base-db", + "citeproc", "rowan", "rustc-hash", "syntax", diff --git a/crates/base-db/src/config.rs b/crates/base-db/src/config.rs index 7c4187f7d..0ee9b4400 100644 --- a/crates/base-db/src/config.rs +++ b/crates/base-db/src/config.rs @@ -82,6 +82,7 @@ pub struct SymbolConfig { pub struct InlayHintConfig { pub label_definitions: bool, pub label_references: bool, + pub citations: bool, } #[derive(Debug)] @@ -187,6 +188,7 @@ impl Default for InlayHintConfig { Self { label_definitions: true, label_references: true, + citations: false, } } } diff --git a/crates/citeproc/src/driver.rs b/crates/citeproc/src/driver.rs index 778e2539b..e6c5f9b0f 100644 --- a/crates/citeproc/src/driver.rs +++ b/crates/citeproc/src/driver.rs @@ -10,17 +10,27 @@ use syntax::bibtex; use titlecase::titlecase; use url::Url; +use crate::{Mode, Options}; + use super::{ entry::{EntryData, EntryKind}, output::{Inline, InlineBuilder, Punct}, }; -#[derive(Debug, Default)] -pub struct Driver { +#[derive(Debug)] +pub struct Driver<'a> { builder: InlineBuilder, + options: &'a Options, } -impl Driver { +impl<'a> Driver<'a> { + pub fn new(options: &'a Options) -> Self { + Self { + builder: InlineBuilder::default(), + options, + } + } + pub fn process(&mut self, entry: &bibtex::Entry) { let entry = EntryData::from(entry); match entry.kind { @@ -531,6 +541,8 @@ impl Driver { } fn introduction(&mut self, entry: &mut EntryData) -> Option<()> { + self.check_detailed_mode()?; + let author = entry.author.remove(&AuthorField::Introduction)?; self.builder.push( Inline::Regular(format!("With an intro. by {}", author)), @@ -542,6 +554,8 @@ impl Driver { } fn foreword(&mut self, entry: &mut EntryData) -> Option<()> { + self.check_detailed_mode()?; + let author = entry.author.remove(&AuthorField::Commentator)?; self.builder.push( Inline::Regular(format!("With a forew. by {}", author)), @@ -553,6 +567,8 @@ impl Driver { } fn afterword(&mut self, entry: &mut EntryData) -> Option<()> { + self.check_detailed_mode()?; + let author = entry.author.remove(&AuthorField::Commentator)?; self.builder.push( Inline::Regular(format!("With an afterw. by {}", author)), @@ -564,6 +580,8 @@ impl Driver { } fn note(&mut self, entry: &mut EntryData) -> Option<()> { + self.check_detailed_mode()?; + let note = entry.text.remove(&TextField::Note)?; self.builder .push(Inline::Regular(note.text), Punct::Dot, Punct::Comma); @@ -614,6 +632,8 @@ impl Driver { } fn eid(&mut self, entry: &mut EntryData) -> Option<()> { + self.check_detailed_mode()?; + let eid = entry.text.remove(&TextField::Eid)?; self.builder .push(Inline::Regular(eid.text), Punct::Comma, Punct::Space); @@ -622,6 +642,8 @@ impl Driver { } fn isbn(&mut self, entry: &mut EntryData) -> Option<()> { + self.check_detailed_mode()?; + let isbn = entry.text.remove(&TextField::Isbn)?; self.builder.push( Inline::Regular("ISBN".to_string()), @@ -636,6 +658,8 @@ impl Driver { } fn issn(&mut self, entry: &mut EntryData) -> Option<()> { + self.check_detailed_mode()?; + let issn = entry.text.remove(&TextField::Issn)?; self.builder.push( Inline::Regular("ISSN".to_string()), @@ -650,6 +674,8 @@ impl Driver { } fn url(&mut self, entry: &mut EntryData) -> Option<()> { + self.check_detailed_mode()?; + let url = entry.text.remove(&TextField::Url)?; self.builder @@ -672,6 +698,8 @@ impl Driver { } fn doi(&mut self, entry: &mut EntryData) -> Option<()> { + self.check_detailed_mode()?; + let doi = entry.text.remove(&TextField::Doi)?; self.builder .push(Inline::Regular("DOI".to_string()), Punct::Dot, Punct::Colon); @@ -690,6 +718,8 @@ impl Driver { } fn eprint(&mut self, entry: &mut EntryData) -> Option<()> { + self.check_detailed_mode()?; + let eprint = entry.text.remove(&TextField::Eprint)?; let eprint_type = entry .text @@ -722,6 +752,13 @@ impl Driver { Some(()) } + fn check_detailed_mode(&self) -> Option<()> { + match self.options.mode { + Mode::Detailed => Some(()), + Mode::Overview => None, + } + } + pub fn finish(self) -> impl Iterator { self.builder.finish() } diff --git a/crates/citeproc/src/lib.rs b/crates/citeproc/src/lib.rs index de2430658..7b48208c5 100644 --- a/crates/citeproc/src/lib.rs +++ b/crates/citeproc/src/lib.rs @@ -7,10 +7,27 @@ use unicode_normalization::UnicodeNormalization; use self::{driver::Driver, output::Inline}; +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash)] +pub enum Mode { + Detailed, + Overview, +} + +impl Default for Mode { + fn default() -> Self { + Self::Detailed + } +} + +#[derive(Debug, Default)] +pub struct Options { + pub mode: Mode, +} + #[must_use] -pub fn render(entry: &bibtex::Entry) -> Option { +pub fn render(entry: &bibtex::Entry, options: &Options) -> Option { let mut output = String::new(); - let mut driver = Driver::default(); + let mut driver = Driver::new(options); driver.process(entry); driver.finish().for_each(|(inline, punct)| { let text = match inline { diff --git a/crates/citeproc/src/tests.rs b/crates/citeproc/src/tests.rs index 1c91580f3..3d4b0f4cf 100644 --- a/crates/citeproc/src/tests.rs +++ b/crates/citeproc/src/tests.rs @@ -3,11 +3,13 @@ use parser::parse_bibtex; use rowan::ast::AstNode; use syntax::bibtex; +use crate::Options; + fn check(input: &str, expect: Expect) { let green = parse_bibtex(input); let root = bibtex::Root::cast(bibtex::SyntaxNode::new_root(green)).unwrap(); let entry = root.entries().next().unwrap(); - let output = super::render(&entry).unwrap(); + let output = super::render(&entry, &Options::default()).unwrap(); expect.assert_eq(&output); } diff --git a/crates/hover/src/citation.rs b/crates/hover/src/citation.rs index 49e5359bb..4711b8905 100644 --- a/crates/hover/src/citation.rs +++ b/crates/hover/src/citation.rs @@ -31,7 +31,7 @@ pub(super) fn find_hover<'db>(params: &HoverParams<'db>) -> Option> { let data = document.data.as_bib()?; let root = bibtex::Root::cast(data.root_node())?; let entry = root.find_entry(name)?; - citeproc::render(&entry) + citeproc::render(&entry, &citeproc::Options::default()) })?; let data = HoverData::Citation(text); diff --git a/crates/inlay-hints/Cargo.toml b/crates/inlay-hints/Cargo.toml index 82b1c729b..34537bd22 100644 --- a/crates/inlay-hints/Cargo.toml +++ b/crates/inlay-hints/Cargo.toml @@ -8,6 +8,7 @@ rust-version.workspace = true [dependencies] base-db = { path = "../base-db" } +citeproc = { path = "../citeproc" } rowan = "0.15.11" rustc-hash = "1.1.0" syntax = { path = "../syntax" } diff --git a/crates/inlay-hints/src/citations.rs b/crates/inlay-hints/src/citations.rs new file mode 100644 index 000000000..e143b2e61 --- /dev/null +++ b/crates/inlay-hints/src/citations.rs @@ -0,0 +1,56 @@ +use base_db::{ + semantics::{bib::Entry, tex::Citation}, + util::queries::Object, + Document, +}; +use rowan::ast::AstNode; +use rustc_hash::FxHashMap; +use syntax::bibtex; + +use crate::{InlayHint, InlayHintBuilder, InlayHintData}; + +pub(super) fn find_hints(builder: &mut InlayHintBuilder) -> Option<()> { + let params = &builder.params.feature; + let data = params.document.data.as_tex()?; + let range = builder.params.range; + + let entries = Entry::find_all(¶ms.project) + .map(|(document, entry)| (entry.name_text(), (document, entry))) + .collect::>(); + + for citation in data + .semantics + .citations + .iter() + .filter(|citation| citation.name.range.intersect(range).is_some()) + { + if let Some(hint) = process_citation(&entries, citation) { + builder.hints.push(hint); + } + } + + Some(()) +} + +fn process_citation<'a>( + entries: &FxHashMap<&str, (&'a Document, &'a Entry)>, + citation: &'a Citation, +) -> Option> { + let offset = citation.name.range.end(); + let (document, entry) = entries.get(citation.name.text.as_str())?; + + let data = document.data.as_bib()?; + let root = &data.root_node(); + let name = root + .token_at_offset(entry.name.range.start()) + .right_biased()?; + + let entry = name.parent_ancestors().find_map(bibtex::Entry::cast)?; + let options = citeproc::Options { + mode: citeproc::Mode::Overview, + }; + + let text = citeproc::render(&entry, &options)?; + let data = InlayHintData::Citation(text); + Some(InlayHint { offset, data }) +} diff --git a/crates/inlay-hints/src/lib.rs b/crates/inlay-hints/src/lib.rs index 48373d305..4fc3a2ed7 100644 --- a/crates/inlay-hints/src/lib.rs +++ b/crates/inlay-hints/src/lib.rs @@ -1,3 +1,4 @@ +mod citations; mod label; use base_db::{util::RenderedLabel, FeatureParams}; @@ -18,6 +19,7 @@ pub struct InlayHint<'a> { pub enum InlayHintData<'a> { LabelDefinition(RenderedLabel<'a>), LabelReference(RenderedLabel<'a>), + Citation(String), } pub fn find_all<'a>(params: InlayHintParams<'a>) -> Option> { @@ -27,6 +29,7 @@ pub fn find_all<'a>(params: InlayHintParams<'a>) -> Option> { }; label::find_hints(&mut builder); + citations::find_hints(&mut builder); Some(builder.hints) } diff --git a/crates/texlab/src/features/inlay_hint.rs b/crates/texlab/src/features/inlay_hint.rs index cb4056f92..6c89affdf 100644 --- a/crates/texlab/src/features/inlay_hint.rs +++ b/crates/texlab/src/features/inlay_hint.rs @@ -60,6 +60,16 @@ pub fn find_all( data: None, } } + InlayHintData::Citation(text) => lsp_types::InlayHint { + position, + label: lsp_types::InlayHintLabel::String(format!(" {text} ")), + kind: None, + text_edits: None, + tooltip: None, + padding_left: Some(true), + padding_right: None, + data: None, + }, }) }); diff --git a/crates/texlab/src/server.rs b/crates/texlab/src/server.rs index 5a0c48960..dc2230d54 100644 --- a/crates/texlab/src/server.rs +++ b/crates/texlab/src/server.rs @@ -576,7 +576,9 @@ impl Server { { item.documentation = bibtex::Root::cast(data.root_node()) .and_then(|root| root.find_entry(&key)) - .and_then(|entry| citeproc::render(&entry)) + .and_then(|entry| { + citeproc::render(&entry, &citeproc::Options::default()) + }) .map(|value| { Documentation::MarkupContent(MarkupContent { kind: MarkupKind::Markdown, diff --git a/crates/texlab/src/server/options.rs b/crates/texlab/src/server/options.rs index 382874525..c50cea51c 100644 --- a/crates/texlab/src/server/options.rs +++ b/crates/texlab/src/server/options.rs @@ -107,6 +107,7 @@ pub struct DiagnosticsOptions { pub struct InlayHintOptions { pub label_definitions: Option, pub label_references: Option, + pub citations: Option, } #[derive(Debug, Clone, Default, Serialize, Deserialize)] @@ -254,6 +255,7 @@ impl From for Config { config.inlay_hints.label_definitions = value.inlay_hints.label_definitions.unwrap_or(true); config.inlay_hints.label_references = value.inlay_hints.label_references.unwrap_or(true); + config.inlay_hints.citations = value.inlay_hints.citations.unwrap_or(false); config.completion.matcher = match value.completion.matcher { CompletionMatcher::Fuzzy => base_db::MatchingAlgo::Skim,