summaryrefslogtreecommitdiff
path: root/support/texlab/src/util
diff options
context:
space:
mode:
authorNorbert Preining <norbert@preining.info>2022-12-30 03:01:26 +0000
committerNorbert Preining <norbert@preining.info>2022-12-30 03:01:26 +0000
commit844019377157163b461e0fd4a66592e61963a530 (patch)
tree32f61991c6a5acc3a3359ffc2cdefdd2aa004268 /support/texlab/src/util
parent55c69feeef908f49007708be194b7bb1c607f302 (diff)
CTAN sync 202212300301
Diffstat (limited to 'support/texlab/src/util')
-rw-r--r--support/texlab/src/util/capabilities.rs179
-rw-r--r--support/texlab/src/util/chktex.rs112
-rw-r--r--support/texlab/src/util/components.rs120
-rw-r--r--support/texlab/src/util/cursor.rs300
-rw-r--r--support/texlab/src/util/label.rs269
-rw-r--r--support/texlab/src/util/lang_data.rs70
-rw-r--r--support/texlab/src/util/line_index.rs217
-rw-r--r--support/texlab/src/util/line_index_ext.rs42
-rw-r--r--support/texlab/src/util/lsp_enums.rs94
9 files changed, 1403 insertions, 0 deletions
diff --git a/support/texlab/src/util/capabilities.rs b/support/texlab/src/util/capabilities.rs
new file mode 100644
index 0000000000..987c0b48dd
--- /dev/null
+++ b/support/texlab/src/util/capabilities.rs
@@ -0,0 +1,179 @@
+use lsp_types::{ClientCapabilities, MarkupKind};
+
+pub trait ClientCapabilitiesExt {
+ fn has_definition_link_support(&self) -> bool;
+
+ fn has_hierarchical_document_symbol_support(&self) -> bool;
+
+ fn has_work_done_progress_support(&self) -> bool;
+
+ fn has_completion_markdown_support(&self) -> bool;
+
+ fn has_hover_markdown_support(&self) -> bool;
+
+ fn has_pull_configuration_support(&self) -> bool;
+
+ fn has_push_configuration_support(&self) -> bool;
+
+ fn has_file_watching_support(&self) -> bool;
+
+ fn has_snippet_support(&self) -> bool;
+}
+
+impl ClientCapabilitiesExt for ClientCapabilities {
+ fn has_definition_link_support(&self) -> bool {
+ self.text_document
+ .as_ref()
+ .and_then(|cap| cap.definition.as_ref())
+ .and_then(|cap| cap.link_support)
+ == Some(true)
+ }
+
+ fn has_hierarchical_document_symbol_support(&self) -> bool {
+ self.text_document
+ .as_ref()
+ .and_then(|cap| cap.document_symbol.as_ref())
+ .and_then(|cap| cap.hierarchical_document_symbol_support)
+ == Some(true)
+ }
+
+ fn has_work_done_progress_support(&self) -> bool {
+ self.window.as_ref().and_then(|cap| cap.work_done_progress) == Some(true)
+ }
+
+ fn has_completion_markdown_support(&self) -> bool {
+ self.text_document
+ .as_ref()
+ .and_then(|cap| cap.completion.as_ref())
+ .and_then(|cap| cap.completion_item.as_ref())
+ .and_then(|cap| cap.documentation_format.as_ref())
+ .map_or(false, |formats| formats.contains(&MarkupKind::Markdown))
+ }
+
+ fn has_hover_markdown_support(&self) -> bool {
+ self.text_document
+ .as_ref()
+ .and_then(|cap| cap.hover.as_ref())
+ .and_then(|cap| cap.content_format.as_ref())
+ .map_or(false, |formats| formats.contains(&MarkupKind::Markdown))
+ }
+
+ fn has_pull_configuration_support(&self) -> bool {
+ self.workspace.as_ref().and_then(|cap| cap.configuration) == Some(true)
+ }
+
+ fn has_push_configuration_support(&self) -> bool {
+ self.workspace
+ .as_ref()
+ .and_then(|cap| cap.did_change_configuration)
+ .and_then(|cap| cap.dynamic_registration)
+ == Some(true)
+ }
+
+ fn has_file_watching_support(&self) -> bool {
+ self.workspace
+ .as_ref()
+ .and_then(|cap| cap.did_change_watched_files)
+ .and_then(|cap| cap.dynamic_registration)
+ == Some(true)
+ }
+
+ fn has_snippet_support(&self) -> bool {
+ self.text_document
+ .as_ref()
+ .and_then(|cap| cap.completion.as_ref())
+ .and_then(|cap| cap.completion_item.as_ref())
+ .and_then(|cap| cap.snippet_support)
+ == Some(true)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use lsp_types::{
+ DocumentSymbolClientCapabilities, GotoCapability, HoverClientCapabilities,
+ TextDocumentClientCapabilities, WindowClientCapabilities,
+ };
+
+ #[test]
+ fn test_has_definition_link_support_true() {
+ let capabilities = ClientCapabilities {
+ text_document: Some(TextDocumentClientCapabilities {
+ definition: Some(GotoCapability {
+ link_support: Some(true),
+ ..GotoCapability::default()
+ }),
+ ..TextDocumentClientCapabilities::default()
+ }),
+ ..ClientCapabilities::default()
+ };
+ assert!(capabilities.has_definition_link_support());
+ }
+
+ #[test]
+ fn test_has_definition_link_support_false() {
+ let capabilities = ClientCapabilities::default();
+ assert!(!capabilities.has_definition_link_support());
+ }
+
+ #[test]
+ fn test_has_hierarchical_document_symbol_support_true() {
+ let capabilities = ClientCapabilities {
+ text_document: Some(TextDocumentClientCapabilities {
+ document_symbol: Some(DocumentSymbolClientCapabilities {
+ hierarchical_document_symbol_support: Some(true),
+ ..DocumentSymbolClientCapabilities::default()
+ }),
+ ..TextDocumentClientCapabilities::default()
+ }),
+ ..ClientCapabilities::default()
+ };
+ assert!(capabilities.has_hierarchical_document_symbol_support());
+ }
+
+ #[test]
+ fn test_has_hierarchical_document_symbol_support_false() {
+ let capabilities = ClientCapabilities::default();
+ assert!(!capabilities.has_hierarchical_document_symbol_support());
+ }
+
+ #[test]
+ fn test_has_work_done_progress_support_true() {
+ let capabilities = ClientCapabilities {
+ window: Some(WindowClientCapabilities {
+ work_done_progress: Some(true),
+ ..WindowClientCapabilities::default()
+ }),
+ ..ClientCapabilities::default()
+ };
+ assert!(capabilities.has_work_done_progress_support());
+ }
+
+ #[test]
+ fn test_has_work_done_progress_support_false() {
+ let capabilities = ClientCapabilities::default();
+ assert!(!capabilities.has_work_done_progress_support());
+ }
+
+ #[test]
+ fn test_has_hover_markdown_support_true() {
+ let capabilities = ClientCapabilities {
+ text_document: Some(TextDocumentClientCapabilities {
+ hover: Some(HoverClientCapabilities {
+ content_format: Some(vec![MarkupKind::PlainText, MarkupKind::Markdown]),
+ ..HoverClientCapabilities::default()
+ }),
+ ..TextDocumentClientCapabilities::default()
+ }),
+ ..ClientCapabilities::default()
+ };
+ assert!(capabilities.has_hover_markdown_support());
+ }
+
+ #[test]
+ fn test_has_hover_markdown_support_false() {
+ let capabilities = ClientCapabilities::default();
+ assert!(!capabilities.has_hover_markdown_support());
+ }
+}
diff --git a/support/texlab/src/util/chktex.rs b/support/texlab/src/util/chktex.rs
new file mode 100644
index 0000000000..29a7ac5630
--- /dev/null
+++ b/support/texlab/src/util/chktex.rs
@@ -0,0 +1,112 @@
+use std::{
+ io::{BufRead, BufReader, Write},
+ path::PathBuf,
+ process::Stdio,
+};
+
+use encoding_rs_io::DecodeReaderBytesBuilder;
+use lsp_types::{DiagnosticSeverity, Position, Range};
+use once_cell::sync::Lazy;
+use regex::Regex;
+
+use crate::{
+ db::{
+ diagnostics::{Diagnostic, DiagnosticCode},
+ Document, Workspace,
+ },
+ Db,
+};
+
+#[derive(Debug)]
+pub struct Command {
+ text: String,
+ working_dir: PathBuf,
+}
+
+impl Command {
+ pub fn new(db: &dyn Db, document: Document) -> Option<Self> {
+ document.parse(db).as_tex()?;
+
+ let workspace = Workspace::get(db);
+ let parent = workspace
+ .parents(db, document)
+ .iter()
+ .next()
+ .map_or(document, Clone::clone);
+
+ let working_dir = workspace
+ .working_dir(db, parent.directory(db))
+ .path(db)
+ .as_deref()?
+ .to_owned();
+
+ log::debug!("Calling ChkTeX from directory: {}", working_dir.display());
+
+ let text = document.contents(db).text(db).clone();
+
+ Some(Self { text, working_dir })
+ }
+
+ pub fn run(self) -> std::io::Result<Vec<Diagnostic>> {
+ let mut child = std::process::Command::new("chktex")
+ .args(&["-I0", "-f%l:%c:%d:%k:%n:%m\n"])
+ .stdin(Stdio::piped())
+ .stdout(Stdio::piped())
+ .stderr(Stdio::null())
+ .current_dir(self.working_dir)
+ .spawn()?;
+
+ let stdout = child.stdout.take().unwrap();
+ let reader = std::thread::spawn(move || {
+ let mut diagnostics = Vec::new();
+ let reader = BufReader::new(
+ DecodeReaderBytesBuilder::new()
+ .encoding(Some(encoding_rs::UTF_8))
+ .utf8_passthru(true)
+ .strip_bom(true)
+ .build(stdout),
+ );
+
+ for line in reader.lines().flatten() {
+ let captures = LINE_REGEX.captures(&line).unwrap();
+ let line = captures[1].parse::<u32>().unwrap() - 1;
+ let character = captures[2].parse::<u32>().unwrap() - 1;
+ let digit = captures[3].parse::<u32>().unwrap();
+ let kind = &captures[4];
+ let code = &captures[5];
+ let message = captures[6].into();
+ let range = Range::new(
+ Position::new(line, character),
+ Position::new(line, character + digit),
+ );
+
+ let severity = match kind {
+ "Message" => DiagnosticSeverity::INFORMATION,
+ "Warning" => DiagnosticSeverity::WARNING,
+ _ => DiagnosticSeverity::ERROR,
+ };
+
+ diagnostics.push(Diagnostic {
+ range,
+ severity,
+ code: DiagnosticCode::Chktex(code.into()),
+ message,
+ });
+ }
+
+ diagnostics
+ });
+
+ let mut stdin = child.stdin.take().unwrap();
+ let bytes = self.text.into_bytes();
+ let writer = std::thread::spawn(move || stdin.write_all(&bytes));
+
+ child.wait()?;
+ writer.join().unwrap()?;
+ let diagnostics = reader.join().unwrap();
+ Ok(diagnostics)
+ }
+}
+
+static LINE_REGEX: Lazy<Regex> =
+ Lazy::new(|| Regex::new("(\\d+):(\\d+):(\\d+):(\\w+):(\\w+):(.*)").unwrap());
diff --git a/support/texlab/src/util/components.rs b/support/texlab/src/util/components.rs
new file mode 100644
index 0000000000..022e7efbc8
--- /dev/null
+++ b/support/texlab/src/util/components.rs
@@ -0,0 +1,120 @@
+use std::io::Read;
+
+use flate2::read::GzDecoder;
+use itertools::Itertools;
+use lsp_types::{MarkupContent, MarkupKind};
+use once_cell::sync::Lazy;
+use serde::Deserialize;
+use smol_str::SmolStr;
+
+use crate::{
+ db::{analysis::TexLinkKind, Document, Workspace},
+ Db,
+};
+
+#[derive(Debug, PartialEq, Eq, Clone, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ComponentDatabase {
+ pub components: Vec<Component>,
+ pub metadata: Vec<ComponentMetadata>,
+}
+
+impl ComponentDatabase {
+ pub fn find(&self, name: &str) -> Option<&Component> {
+ self.components.iter().find(|component| {
+ component
+ .file_names
+ .iter()
+ .any(|file_name| file_name == name)
+ })
+ }
+
+ pub fn linked_components(&self, db: &dyn Db, child: Document) -> Vec<&Component> {
+ Workspace::get(db)
+ .related(db, child)
+ .iter()
+ .filter_map(|document| document.parse(db).as_tex())
+ .flat_map(|data| data.analyze(db).links(db))
+ .filter_map(|link| match link.kind(db) {
+ TexLinkKind::Sty => Some(format!("{}.sty", link.path(db).text(db))),
+ TexLinkKind::Cls => Some(format!("{}.cls", link.path(db).text(db))),
+ _ => None,
+ })
+ .filter_map(|name| self.find(&name))
+ .chain(std::iter::once(self.kernel()))
+ .flat_map(|comp| {
+ comp.references
+ .iter()
+ .filter_map(|name| self.find(name))
+ .chain(std::iter::once(comp))
+ })
+ .unique_by(|comp| &comp.file_names)
+ .collect()
+ }
+
+ pub fn kernel(&self) -> &Component {
+ self.components
+ .iter()
+ .find(|component| component.file_names.is_empty())
+ .unwrap()
+ }
+
+ pub fn documentation(&self, name: &str) -> Option<MarkupContent> {
+ let metadata = self
+ .metadata
+ .iter()
+ .find(|metadata| metadata.name == name)?;
+
+ let desc = metadata.description.clone()?;
+ Some(MarkupContent {
+ kind: MarkupKind::PlainText,
+ value: desc,
+ })
+ }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct Component {
+ pub file_names: Vec<SmolStr>,
+ pub references: Vec<SmolStr>,
+ pub commands: Vec<ComponentCommand>,
+ pub environments: Vec<SmolStr>,
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ComponentCommand {
+ pub name: SmolStr,
+ pub image: Option<String>,
+ pub glyph: Option<SmolStr>,
+ pub parameters: Vec<ComponentParameter>,
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ComponentParameter(pub Vec<ComponentArgument>);
+
+#[derive(Debug, PartialEq, Eq, Clone, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ComponentArgument {
+ pub name: SmolStr,
+ pub image: Option<String>,
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ComponentMetadata {
+ pub name: String,
+ pub caption: Option<String>,
+ pub description: Option<String>,
+}
+
+const JSON_GZ: &[u8] = include_bytes!("../../data/components.json.gz");
+
+pub static COMPONENT_DATABASE: Lazy<ComponentDatabase> = Lazy::new(|| {
+ let mut decoder = GzDecoder::new(JSON_GZ);
+ let mut buf = String::new();
+ decoder.read_to_string(&mut buf).unwrap();
+ serde_json::from_str(&buf).unwrap()
+});
diff --git a/support/texlab/src/util/cursor.rs b/support/texlab/src/util/cursor.rs
new file mode 100644
index 0000000000..5c3758ef46
--- /dev/null
+++ b/support/texlab/src/util/cursor.rs
@@ -0,0 +1,300 @@
+use lsp_types::{Position, Url};
+use rowan::{ast::AstNode, TextRange, TextSize};
+
+use crate::{
+ db::{parse::DocumentData, Document, Workspace},
+ syntax::{bibtex, latex},
+ Db,
+};
+
+use super::{line_index::LineIndex, line_index_ext::LineIndexExt};
+
+#[derive(Debug)]
+pub enum Cursor {
+ Tex(latex::SyntaxToken),
+ Bib(bibtex::SyntaxToken),
+ Nothing,
+}
+
+impl Cursor {
+ pub fn new_tex(
+ left: Option<latex::SyntaxToken>,
+ right: Option<latex::SyntaxToken>,
+ ) -> Option<Self> {
+ let left = left?;
+ let right = right?;
+
+ if left.kind().is_command_name() {
+ return Some(Self::Tex(left));
+ }
+
+ if right.kind() == latex::WORD {
+ return Some(Self::Tex(right));
+ }
+
+ if left.kind() == latex::WORD {
+ return Some(Self::Tex(left));
+ }
+
+ if right.kind().is_command_name() {
+ return Some(Self::Tex(right));
+ }
+
+ if left.kind() == latex::WHITESPACE && left.parent()?.kind() == latex::KEY {
+ return Some(Self::Tex(left));
+ }
+
+ if matches!(right.kind(), latex::WHITESPACE | latex::LINE_BREAK)
+ && right.parent()?.kind() == latex::KEY
+ {
+ return Some(Self::Tex(right));
+ }
+
+ Some(Self::Tex(right))
+ }
+
+ pub fn new_bib(
+ left: Option<bibtex::SyntaxToken>,
+ right: Option<bibtex::SyntaxToken>,
+ ) -> Option<Self> {
+ let left = left?;
+ let right = right?;
+
+ if right.kind() == bibtex::TYPE {
+ return Some(Self::Bib(right));
+ }
+
+ if left.kind() == bibtex::TYPE {
+ return Some(Self::Bib(left));
+ }
+
+ if matches!(left.kind(), bibtex::COMMAND_NAME | bibtex::ACCENT_NAME) {
+ return Some(Self::Bib(left));
+ }
+
+ if matches!(right.kind(), bibtex::WORD | bibtex::NAME) {
+ return Some(Self::Bib(right));
+ }
+
+ if matches!(left.kind(), bibtex::WORD | bibtex::NAME) {
+ return Some(Self::Bib(left));
+ }
+
+ if matches!(right.kind(), bibtex::COMMAND_NAME | bibtex::ACCENT_NAME) {
+ return Some(Self::Bib(right));
+ }
+
+ Some(Self::Bib(right))
+ }
+
+ pub fn as_tex(&self) -> Option<&latex::SyntaxToken> {
+ if let Self::Tex(v) = self {
+ Some(v)
+ } else {
+ None
+ }
+ }
+
+ pub fn as_bib(&self) -> Option<&bibtex::SyntaxToken> {
+ if let Self::Bib(v) = self {
+ Some(v)
+ } else {
+ None
+ }
+ }
+
+ pub fn command_range(&self, offset: TextSize) -> Option<TextRange> {
+ self.as_tex()
+ .filter(|token| token.kind().is_command_name())
+ .filter(|token| token.text_range().start() != offset)
+ .map(|token| token.text_range())
+ .map(|range| TextRange::new(range.start() + TextSize::from(1), range.end()))
+ .or_else(|| {
+ self.as_bib()
+ .filter(|token| {
+ matches!(token.kind(), bibtex::COMMAND_NAME | bibtex::ACCENT_NAME)
+ })
+ .filter(|token| token.text_range().start() != offset)
+ .map(|token| token.text_range())
+ .map(|range| TextRange::new(range.start() + TextSize::from(1), range.end()))
+ })
+ }
+}
+
+pub struct CursorContext<'db, T = ()> {
+ pub db: &'db dyn Db,
+ pub document: Document,
+ pub line_index: &'db LineIndex,
+ pub workspace: Workspace,
+ pub cursor: Cursor,
+ pub offset: TextSize,
+ pub params: T,
+}
+
+impl<'db, T> CursorContext<'db, T> {
+ pub fn new(db: &'db dyn Db, uri: &Url, position: Position, params: T) -> Option<Self> {
+ let workspace = Workspace::get(db);
+ let document = workspace.lookup_uri(db, uri)?;
+ let line_index = document.contents(db).line_index(db);
+ let offset = line_index.offset_lsp(position);
+
+ let cursor = match document.parse(db) {
+ DocumentData::Tex(data) => {
+ let root = data.root(db);
+ let left = root.token_at_offset(offset).left_biased();
+ let right = root.token_at_offset(offset).right_biased();
+ Cursor::new_tex(left, right)
+ }
+ DocumentData::Bib(data) => {
+ let root = data.root(db);
+ let left = root.token_at_offset(offset).left_biased();
+ let right = root.token_at_offset(offset).right_biased();
+ Cursor::new_bib(left, right)
+ }
+ DocumentData::Log(_) => None,
+ };
+
+ Some(Self {
+ db,
+ document,
+ line_index,
+ workspace,
+ cursor: cursor.unwrap_or(Cursor::Nothing),
+ offset,
+ params,
+ })
+ }
+
+ pub fn related(&self) -> impl Iterator<Item = Document> + '_ {
+ self.workspace
+ .related(self.db, self.document)
+ .iter()
+ .copied()
+ }
+
+ pub fn is_inside_latex_curly(&self, group: &impl latex::HasCurly) -> bool {
+ latex::small_range(group).contains(self.offset) || group.right_curly().is_none()
+ }
+
+ pub fn find_citation_key_word(&self) -> Option<(String, TextRange)> {
+ let word = self
+ .cursor
+ .as_tex()
+ .filter(|token| token.kind() == latex::WORD)?;
+
+ let key = latex::Key::cast(word.parent()?)?;
+
+ let group = latex::CurlyGroupWordList::cast(key.syntax().parent()?)?;
+ latex::Citation::cast(group.syntax().parent()?)?;
+ Some((key.to_string(), latex::small_range(&key)))
+ }
+
+ pub fn find_citation_key_command(&self) -> Option<(String, TextRange)> {
+ let command = self.cursor.as_tex()?;
+
+ let citation = latex::Citation::cast(command.parent()?)?;
+ let key = citation.key_list()?.keys().next()?;
+ Some((key.to_string(), latex::small_range(&key)))
+ }
+
+ pub fn find_entry_key(&self) -> Option<(String, TextRange)> {
+ let key = self
+ .cursor
+ .as_bib()
+ .filter(|token| token.kind() == bibtex::NAME)?;
+
+ bibtex::Entry::cast(key.parent()?)?;
+ Some((key.to_string(), key.text_range()))
+ }
+
+ pub fn find_label_name_key(&self) -> Option<(String, TextRange)> {
+ let name = self
+ .cursor
+ .as_tex()
+ .filter(|token| token.kind() == latex::WORD)?;
+
+ let key = latex::Key::cast(name.parent()?)?;
+
+ if matches!(
+ key.syntax().parent()?.parent()?.kind(),
+ latex::LABEL_DEFINITION | latex::LABEL_REFERENCE | latex::LABEL_REFERENCE_RANGE
+ ) {
+ Some((key.to_string(), latex::small_range(&key)))
+ } else {
+ None
+ }
+ }
+
+ pub fn find_label_name_command(&self) -> Option<(String, TextRange)> {
+ let node = self.cursor.as_tex()?.parent()?;
+ if let Some(label) = latex::LabelDefinition::cast(node.clone()) {
+ let name = label.name()?.key()?;
+ Some((name.to_string(), latex::small_range(&name)))
+ } else if let Some(label) = latex::LabelReference::cast(node.clone()) {
+ let name = label.name_list()?.keys().next()?;
+ Some((name.to_string(), latex::small_range(&name)))
+ } else if let Some(label) = latex::LabelReferenceRange::cast(node) {
+ let name = label.from()?.key()?;
+ Some((name.to_string(), latex::small_range(&name)))
+ } else {
+ None
+ }
+ }
+
+ pub fn find_environment_name(&self) -> Option<(String, TextRange)> {
+ let (name, range, group) = self.find_curly_group_word()?;
+
+ if !matches!(group.syntax().parent()?.kind(), latex::BEGIN | latex::END) {
+ return None;
+ }
+
+ Some((name, range))
+ }
+
+ pub fn find_curly_group_word(&self) -> Option<(String, TextRange, latex::CurlyGroupWord)> {
+ let token = self.cursor.as_tex()?;
+ let key = latex::Key::cast(token.parent()?);
+
+ let group = key
+ .as_ref()
+ .and_then(|key| key.syntax().parent())
+ .unwrap_or(token.parent()?);
+
+ let group =
+ latex::CurlyGroupWord::cast(group).filter(|group| self.is_inside_latex_curly(group))?;
+
+ key.map(|key| (key.to_string(), latex::small_range(&key), group.clone()))
+ .or_else(|| Some((String::new(), TextRange::empty(self.offset), group)))
+ }
+
+ pub fn find_curly_group_word_list(
+ &self,
+ ) -> Option<(String, TextRange, latex::CurlyGroupWordList)> {
+ let token = self.cursor.as_tex()?;
+ let key = latex::Key::cast(token.parent()?);
+
+ let group = key
+ .as_ref()
+ .and_then(|key| key.syntax().parent())
+ .unwrap_or(token.parent()?);
+
+ let group = latex::CurlyGroupWordList::cast(group)
+ .filter(|group| self.is_inside_latex_curly(group))?;
+
+ key.map(|key| {
+ let range = if group
+ .syntax()
+ .last_token()
+ .filter(|tok| tok.kind() == latex::MISSING)
+ .is_some()
+ {
+ TextRange::new(latex::small_range(&key).start(), token.text_range().end())
+ } else {
+ latex::small_range(&key)
+ };
+
+ (key.to_string(), range, group.clone())
+ })
+ .or_else(|| Some((String::new(), TextRange::empty(self.offset), group)))
+ }
+}
diff --git a/support/texlab/src/util/label.rs b/support/texlab/src/util/label.rs
new file mode 100644
index 0000000000..f0fae34337
--- /dev/null
+++ b/support/texlab/src/util/label.rs
@@ -0,0 +1,269 @@
+use std::str::FromStr;
+
+use rowan::{ast::AstNode, TextRange};
+
+use crate::{
+ db::{analysis::label, Document, Word, Workspace},
+ syntax::latex::{self, HasBrack, HasCurly},
+ Db,
+};
+
+use self::LabeledObject::*;
+
+use super::lang_data::LANGUAGE_DATA;
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+pub enum LabeledFloatKind {
+ Figure,
+ Table,
+ Listing,
+ Algorithm,
+}
+
+impl LabeledFloatKind {
+ pub fn as_str(self) -> &'static str {
+ match self {
+ Self::Figure => "Figure",
+ Self::Table => "Table",
+ Self::Listing => "Listing",
+ Self::Algorithm => "Algorithm",
+ }
+ }
+}
+
+impl FromStr for LabeledFloatKind {
+ type Err = ();
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ match s {
+ "figure" | "subfigure" => Ok(Self::Figure),
+ "table" | "subtable" => Ok(Self::Table),
+ "listing" | "lstlisting" => Ok(Self::Listing),
+ "algorithm" => Ok(Self::Algorithm),
+ _ => Err(()),
+ }
+ }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub enum LabeledObject {
+ Section {
+ prefix: &'static str,
+ text: String,
+ },
+ Float {
+ kind: LabeledFloatKind,
+ caption: String,
+ },
+ Theorem {
+ kind: Word,
+ description: Option<String>,
+ },
+ Equation,
+ EnumItem,
+}
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub struct RenderedLabel {
+ pub range: TextRange,
+ pub number: Option<Word>,
+ pub object: LabeledObject,
+}
+
+impl RenderedLabel {
+ pub fn reference(&self, db: &dyn Db) -> String {
+ match &self.number {
+ Some(number) => match &self.object {
+ Section { prefix, text } => format!("{} {} ({})", prefix, number.text(db), text),
+ Float { kind, caption } => {
+ format!("{} {}: {}", kind.as_str(), number.text(db), caption)
+ }
+ Theorem {
+ kind,
+ description: None,
+ } => format!("{} {}", kind.text(db), number.text(db)),
+ Theorem {
+ kind,
+ description: Some(description),
+ } => format!("{} {} ({})", kind.text(db), number.text(db), description),
+ Equation => format!("Equation ({})", number.text(db)),
+ EnumItem => format!("Item {}", number.text(db)),
+ },
+ None => match &self.object {
+ Section { prefix, text } => format!("{} ({})", prefix, text),
+ Float { kind, caption } => format!("{}: {}", kind.as_str(), caption),
+ Theorem {
+ kind,
+ description: None,
+ } => kind.text(db).into(),
+ Theorem {
+ kind,
+ description: Some(description),
+ } => format!("{} ({})", kind.text(db), description),
+ Equation => "Equation".into(),
+ EnumItem => "Item".into(),
+ },
+ }
+ }
+
+ pub fn detail(&self, db: &dyn Db) -> Option<String> {
+ match &self.object {
+ Section { .. } | Theorem { .. } | Equation | EnumItem => Some(self.reference(db)),
+ Float { kind, .. } => {
+ let result = match &self.number {
+ Some(number) => format!("{} {}", kind.as_str(), number.text(db)),
+ None => kind.as_str().to_owned(),
+ };
+ Some(result)
+ }
+ }
+ }
+}
+
+pub fn render(db: &dyn Db, document: Document, label_def: label::Name) -> Option<RenderedLabel> {
+ let workspace = Workspace::get(db);
+ let label_num = workspace.number_of_label(db, document, label_def.name(db));
+ let root = document.parse(db).as_tex()?.root(db);
+
+ label_def
+ .origin(db)
+ .as_definition()?
+ .to_node(&root)
+ .syntax()
+ .ancestors()
+ .find_map(|parent| {
+ render_label_float(parent.clone(), label_num)
+ .or_else(|| render_label_section(parent.clone(), label_num))
+ .or_else(|| render_label_enum_item(db, parent.clone(), label_num))
+ .or_else(|| render_label_equation(parent.clone(), label_num))
+ .or_else(|| render_label_theorem(db, document, parent, label_num))
+ })
+}
+
+pub fn find_label_definition(
+ db: &dyn Db,
+ child: Document,
+ name: Word,
+) -> Option<(Document, label::Name)> {
+ Workspace::get(db)
+ .related(db, child)
+ .iter()
+ .find_map(|document| {
+ let data = document.parse(db).as_tex()?;
+ let label = data
+ .analyze(db)
+ .labels(db)
+ .iter()
+ .filter(|label| label.origin(db).as_definition().is_some())
+ .find(|label| label.name(db) == name)?;
+
+ Some((*document, *label))
+ })
+}
+
+fn render_label_float(parent: latex::SyntaxNode, number: Option<Word>) -> Option<RenderedLabel> {
+ let environment = latex::Environment::cast(parent.clone())?;
+ let environment_name = environment.begin()?.name()?.key()?.to_string();
+ let kind = LabeledFloatKind::from_str(&environment_name).ok()?;
+ let caption = find_caption_by_parent(&parent)?;
+ Some(RenderedLabel {
+ range: latex::small_range(&environment),
+ number,
+ object: LabeledObject::Float { caption, kind },
+ })
+}
+
+fn render_label_section(parent: latex::SyntaxNode, number: Option<Word>) -> Option<RenderedLabel> {
+ let section = latex::Section::cast(parent)?;
+ let text_group = section.name()?;
+ let text = text_group.content_text()?;
+
+ Some(RenderedLabel {
+ range: latex::small_range(&section),
+ number,
+ object: LabeledObject::Section {
+ prefix: match section.syntax().kind() {
+ latex::PART => "Part",
+ latex::CHAPTER => "Chapter",
+ latex::SECTION => "Section",
+ latex::SUBSECTION => "Subsection",
+ latex::SUBSUBSECTION => "Subsubsection",
+ latex::PARAGRAPH => "Paragraph",
+ latex::SUBPARAGRAPH => "Subparagraph",
+ _ => unreachable!(),
+ },
+ text,
+ },
+ })
+}
+
+fn render_label_enum_item(
+ db: &dyn Db,
+ parent: latex::SyntaxNode,
+ number: Option<Word>,
+) -> Option<RenderedLabel> {
+ let enum_item = latex::EnumItem::cast(parent)?;
+ Some(RenderedLabel {
+ range: latex::small_range(&enum_item),
+ number: enum_item
+ .label()
+ .and_then(|label| label.content_text())
+ .map(|text| Word::new(db, text))
+ .or_else(|| number),
+ object: LabeledObject::EnumItem,
+ })
+}
+
+fn render_label_equation(parent: latex::SyntaxNode, number: Option<Word>) -> Option<RenderedLabel> {
+ let environment = latex::Environment::cast(parent)?;
+ let environment_name = environment.begin()?.name()?.key()?.to_string();
+
+ if !LANGUAGE_DATA
+ .math_environments
+ .iter()
+ .any(|name| name == &environment_name)
+ {
+ return None;
+ }
+
+ Some(RenderedLabel {
+ range: latex::small_range(&environment),
+ number,
+ object: LabeledObject::Equation,
+ })
+}
+
+fn render_label_theorem(
+ db: &dyn Db,
+ document: Document,
+ parent: latex::SyntaxNode,
+ number: Option<Word>,
+) -> Option<RenderedLabel> {
+ let environment = latex::Environment::cast(parent)?;
+ let begin = environment.begin()?;
+ let description = begin.options().and_then(|options| options.content_text());
+
+ let environment_name = begin.name()?.key()?.to_string();
+
+ let kind = Workspace::get(db)
+ .related(db, document)
+ .iter()
+ .filter_map(|document| document.parse(db).as_tex())
+ .flat_map(|data| data.analyze(db).theorem_environments(db))
+ .find(|env| env.name(db).text(db) == &environment_name)
+ .map(|env| env.description(db))?;
+
+ Some(RenderedLabel {
+ range: latex::small_range(&environment),
+ number,
+ object: LabeledObject::Theorem { kind, description },
+ })
+}
+
+pub fn find_caption_by_parent(parent: &latex::SyntaxNode) -> Option<String> {
+ parent
+ .children()
+ .filter_map(latex::Caption::cast)
+ .find_map(|node| node.long())
+ .and_then(|node| node.content_text())
+}
diff --git a/support/texlab/src/util/lang_data.rs b/support/texlab/src/util/lang_data.rs
new file mode 100644
index 0000000000..f270c3f65e
--- /dev/null
+++ b/support/texlab/src/util/lang_data.rs
@@ -0,0 +1,70 @@
+use once_cell::sync::Lazy;
+use serde::{Deserialize, Serialize};
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub enum BibtexEntryTypeCategory {
+ Misc,
+ String,
+ Article,
+ Book,
+ Collection,
+ Part,
+ Thesis,
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct BibtexEntryTypeDoc {
+ pub name: String,
+ pub category: BibtexEntryTypeCategory,
+ pub documentation: Option<String>,
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct BibtexFieldDoc {
+ pub name: String,
+ pub documentation: String,
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Default, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct LanguageData {
+ pub colors: Vec<String>,
+ pub entry_types: Vec<BibtexEntryTypeDoc>,
+ pub fields: Vec<BibtexFieldDoc>,
+ pub pgf_libraries: Vec<String>,
+ pub tikz_libraries: Vec<String>,
+ pub math_environments: Vec<String>,
+ pub enum_environments: Vec<String>,
+}
+
+impl LanguageData {
+ #[must_use]
+ pub fn find_entry_type(&self, name: &str) -> Option<&BibtexEntryTypeDoc> {
+ let name = name.to_lowercase();
+ self.entry_types
+ .iter()
+ .find(|ty| ty.name.to_lowercase() == name)
+ }
+
+ #[must_use]
+ pub fn entry_type_documentation(&self, name: &str) -> Option<&str> {
+ self.find_entry_type(name)
+ .and_then(|ty| ty.documentation.as_ref().map(AsRef::as_ref))
+ }
+
+ #[must_use]
+ pub fn field_documentation(&self, name: &str) -> Option<&str> {
+ self.fields
+ .iter()
+ .find(|field| field.name.to_lowercase() == name.to_lowercase())
+ .map(|field| field.documentation.as_ref())
+ }
+}
+
+pub static LANGUAGE_DATA: Lazy<LanguageData> = Lazy::new(|| {
+ const JSON: &str = include_str!("../../data/lang_data.json");
+ serde_json::from_str(JSON).expect("Failed to deserialize language.json")
+});
diff --git a/support/texlab/src/util/line_index.rs b/support/texlab/src/util/line_index.rs
new file mode 100644
index 0000000000..70e8f8128b
--- /dev/null
+++ b/support/texlab/src/util/line_index.rs
@@ -0,0 +1,217 @@
+// The following code has been copied from rust-analyzer.
+
+//! `LineIndex` maps flat `TextSize` offsets into `(Line, Column)`
+//! representation.
+use std::iter;
+
+use rowan::{TextRange, TextSize};
+use rustc_hash::FxHashMap;
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct LineIndex {
+ /// Offset the the beginning of each line, zero-based
+ pub(crate) newlines: Vec<TextSize>,
+ /// List of non-ASCII characters on each line
+ pub(crate) utf16_lines: FxHashMap<u32, Vec<Utf16Char>>,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
+pub struct LineColUtf16 {
+ /// Zero-based
+ pub line: u32,
+ /// Zero-based
+ pub col: u32,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
+pub struct LineCol {
+ /// Zero-based
+ pub line: u32,
+ /// Zero-based utf8 offset
+ pub col: u32,
+}
+
+#[derive(Clone, Debug, Hash, PartialEq, Eq)]
+pub(crate) struct Utf16Char {
+ /// Start offset of a character inside a line, zero-based
+ pub(crate) start: TextSize,
+ /// End offset of a character inside a line, zero-based
+ pub(crate) end: TextSize,
+}
+
+impl Utf16Char {
+ /// Returns the length in 8-bit UTF-8 code units.
+ fn len(&self) -> TextSize {
+ self.end - self.start
+ }
+
+ /// Returns the length in 16-bit UTF-16 code units.
+ fn len_utf16(&self) -> usize {
+ if self.len() == TextSize::from(4) {
+ 2
+ } else {
+ 1
+ }
+ }
+}
+
+impl LineIndex {
+ pub fn new(text: &str) -> LineIndex {
+ let mut utf16_lines = FxHashMap::default();
+ let mut utf16_chars = Vec::new();
+
+ let mut newlines = vec![0.into()];
+ let mut curr_row = 0.into();
+ let mut curr_col = 0.into();
+ let mut line = 0;
+ for c in text.chars() {
+ let c_len = TextSize::of(c);
+ curr_row += c_len;
+ if c == '\n' {
+ newlines.push(curr_row);
+
+ // Save any utf-16 characters seen in the previous line
+ if !utf16_chars.is_empty() {
+ utf16_lines.insert(line, utf16_chars);
+ utf16_chars = Vec::new();
+ }
+
+ // Prepare for processing the next line
+ curr_col = 0.into();
+ line += 1;
+ continue;
+ }
+
+ if !c.is_ascii() {
+ utf16_chars.push(Utf16Char {
+ start: curr_col,
+ end: curr_col + c_len,
+ });
+ }
+
+ curr_col += c_len;
+ }
+
+ // Save any utf-16 characters seen in the last line
+ if !utf16_chars.is_empty() {
+ utf16_lines.insert(line, utf16_chars);
+ }
+
+ LineIndex {
+ newlines,
+ utf16_lines,
+ }
+ }
+
+ pub fn line_col(&self, offset: TextSize) -> LineCol {
+ let line = partition_point(&self.newlines, |&it| it <= offset) - 1;
+ let line_start_offset = self.newlines[line];
+ let col = offset - line_start_offset;
+ LineCol {
+ line: line as u32,
+ col: col.into(),
+ }
+ }
+
+ pub fn offset(&self, line_col: LineCol) -> TextSize {
+ self.newlines[line_col.line as usize] + TextSize::from(line_col.col)
+ }
+
+ pub fn to_utf16(&self, line_col: LineCol) -> LineColUtf16 {
+ let col = self.utf8_to_utf16_col(line_col.line, line_col.col.into());
+ LineColUtf16 {
+ line: line_col.line,
+ col: col as u32,
+ }
+ }
+
+ pub fn to_utf8(&self, line_col: LineColUtf16) -> LineCol {
+ let col = self.utf16_to_utf8_col(line_col.line, line_col.col);
+ LineCol {
+ line: line_col.line,
+ col: col.into(),
+ }
+ }
+
+ pub fn lines(&self, range: TextRange) -> impl Iterator<Item = TextRange> + '_ {
+ let lo = partition_point(&self.newlines, |&it| it < range.start());
+ let hi = partition_point(&self.newlines, |&it| it <= range.end());
+ let all = iter::once(range.start())
+ .chain(self.newlines[lo..hi].iter().copied())
+ .chain(iter::once(range.end()));
+
+ all.clone()
+ .zip(all.skip(1))
+ .map(|(lo, hi)| TextRange::new(lo, hi))
+ .filter(|it| !it.is_empty())
+ }
+
+ fn utf8_to_utf16_col(&self, line: u32, col: TextSize) -> usize {
+ let mut res: usize = col.into();
+ if let Some(utf16_chars) = self.utf16_lines.get(&line) {
+ for c in utf16_chars {
+ if c.end <= col {
+ res -= usize::from(c.len()) - c.len_utf16();
+ } else {
+ // From here on, all utf16 characters come *after* the character we are mapping,
+ // so we don't need to take them into account
+ break;
+ }
+ }
+ }
+ res
+ }
+
+ fn utf16_to_utf8_col(&self, line: u32, mut col: u32) -> TextSize {
+ if let Some(utf16_chars) = self.utf16_lines.get(&line) {
+ for c in utf16_chars {
+ if col > u32::from(c.start) {
+ col += u32::from(c.len()) - c.len_utf16() as u32;
+ } else {
+ // From here on, all utf16 characters come *after* the character we are mapping,
+ // so we don't need to take them into account
+ break;
+ }
+ }
+ }
+
+ col.into()
+ }
+}
+
+/// Returns `idx` such that:
+///
+/// ```text
+/// ∀ x in slice[..idx]: pred(x)
+/// && ∀ x in slice[idx..]: !pred(x)
+/// ```
+///
+/// https://github.com/rust-lang/rust/issues/73831
+fn partition_point<T, P>(slice: &[T], mut pred: P) -> usize
+where
+ P: FnMut(&T) -> bool,
+{
+ let mut left = 0;
+ let mut right = slice.len();
+
+ while left != right {
+ let mid = left + (right - left) / 2;
+ // SAFETY:
+ // When left < right, left <= mid < right.
+ // Therefore left always increases and right always decreases,
+ // and either of them is selected.
+ // In both cases left <= right is satisfied.
+ // Therefore if left < right in a step,
+ // left <= right is satisfied in the next step.
+ // Therefore as long as left != right, 0 <= left < right <= len is satisfied
+ // and if this case 0 <= mid < len is satisfied too.
+ let value = unsafe { slice.get_unchecked(mid) };
+ if pred(value) {
+ left = mid + 1;
+ } else {
+ right = mid;
+ }
+ }
+
+ left
+}
diff --git a/support/texlab/src/util/line_index_ext.rs b/support/texlab/src/util/line_index_ext.rs
new file mode 100644
index 0000000000..becbb8bde7
--- /dev/null
+++ b/support/texlab/src/util/line_index_ext.rs
@@ -0,0 +1,42 @@
+use lsp_types::{Position, Range};
+use rowan::{TextRange, TextSize};
+
+use super::line_index::{LineColUtf16, LineIndex};
+
+pub trait LineIndexExt {
+ fn offset_lsp(&self, line_col: Position) -> TextSize;
+
+ fn offset_lsp_range(&self, line_col: Range) -> TextRange;
+
+ fn line_col_lsp(&self, offset: TextSize) -> Position;
+
+ fn line_col_lsp_range(&self, offset: TextRange) -> Range;
+}
+
+impl LineIndexExt for LineIndex {
+ fn offset_lsp(&self, line_col: Position) -> TextSize {
+ let line_col = LineColUtf16 {
+ line: line_col.line,
+ col: line_col.character,
+ };
+ self.offset(self.to_utf8(line_col))
+ }
+
+ fn offset_lsp_range(&self, line_col: Range) -> TextRange {
+ let start = self.offset_lsp(line_col.start);
+ let end = self.offset_lsp(line_col.end);
+ TextRange::new(start, end)
+ }
+
+ fn line_col_lsp(&self, offset: TextSize) -> Position {
+ let position = self.line_col(offset);
+ let LineColUtf16 { line, col } = self.to_utf16(position);
+ Position::new(line, col)
+ }
+
+ fn line_col_lsp_range(&self, offset: TextRange) -> Range {
+ let start = self.line_col_lsp(offset.start());
+ let end = self.line_col_lsp(offset.end());
+ Range::new(start, end)
+ }
+}
diff --git a/support/texlab/src/util/lsp_enums.rs b/support/texlab/src/util/lsp_enums.rs
new file mode 100644
index 0000000000..75b97097c5
--- /dev/null
+++ b/support/texlab/src/util/lsp_enums.rs
@@ -0,0 +1,94 @@
+use lsp_types::{CompletionItemKind, SymbolKind};
+
+use super::lang_data::BibtexEntryTypeCategory;
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+pub enum Structure {
+ Command,
+ Snippet,
+ Environment,
+ Section,
+ Float,
+ Theorem,
+ Equation,
+ Item,
+ Label,
+ Folder,
+ File,
+ PgfLibrary,
+ TikzLibrary,
+ Color,
+ ColorModel,
+ Package,
+ Class,
+ Entry(BibtexEntryTypeCategory),
+ Field,
+ Argument,
+ GlossaryEntry,
+}
+
+impl Structure {
+ pub fn completion_kind(self) -> CompletionItemKind {
+ match self {
+ Self::Command => CompletionItemKind::FUNCTION,
+ Self::Snippet => CompletionItemKind::SNIPPET,
+ Self::Environment => CompletionItemKind::ENUM,
+ Self::Section => CompletionItemKind::MODULE,
+ Self::Float => CompletionItemKind::METHOD,
+ Self::Theorem => CompletionItemKind::VARIABLE,
+ Self::Equation => CompletionItemKind::CONSTANT,
+ Self::Item => CompletionItemKind::ENUM_MEMBER,
+ Self::Label => CompletionItemKind::CONSTRUCTOR,
+ Self::Folder => CompletionItemKind::FOLDER,
+ Self::File => CompletionItemKind::FILE,
+ Self::PgfLibrary => CompletionItemKind::PROPERTY,
+ Self::TikzLibrary => CompletionItemKind::PROPERTY,
+ Self::Color => CompletionItemKind::COLOR,
+ Self::ColorModel => CompletionItemKind::COLOR,
+ Self::Package => CompletionItemKind::CLASS,
+ Self::Class => CompletionItemKind::CLASS,
+ Self::Entry(BibtexEntryTypeCategory::Misc) => CompletionItemKind::INTERFACE,
+ Self::Entry(BibtexEntryTypeCategory::String) => CompletionItemKind::TEXT,
+ Self::Entry(BibtexEntryTypeCategory::Article) => CompletionItemKind::EVENT,
+ Self::Entry(BibtexEntryTypeCategory::Book) => CompletionItemKind::STRUCT,
+ Self::Entry(BibtexEntryTypeCategory::Collection) => CompletionItemKind::TYPE_PARAMETER,
+ Self::Entry(BibtexEntryTypeCategory::Part) => CompletionItemKind::OPERATOR,
+ Self::Entry(BibtexEntryTypeCategory::Thesis) => CompletionItemKind::UNIT,
+ Self::Field => CompletionItemKind::FIELD,
+ Self::Argument => CompletionItemKind::VALUE,
+ Self::GlossaryEntry => CompletionItemKind::KEYWORD,
+ }
+ }
+
+ pub fn symbol_kind(self) -> SymbolKind {
+ match self {
+ Self::Command => SymbolKind::FUNCTION,
+ Self::Snippet => unimplemented!(),
+ Self::Environment => SymbolKind::ENUM,
+ Self::Section => SymbolKind::MODULE,
+ Self::Float => SymbolKind::METHOD,
+ Self::Theorem => SymbolKind::VARIABLE,
+ Self::Equation => SymbolKind::CONSTANT,
+ Self::Item => SymbolKind::ENUM_MEMBER,
+ Self::Label => SymbolKind::CONSTRUCTOR,
+ Self::Folder => SymbolKind::NAMESPACE,
+ Self::File => SymbolKind::FILE,
+ Self::PgfLibrary => SymbolKind::PROPERTY,
+ Self::TikzLibrary => SymbolKind::PROPERTY,
+ Self::Color => unimplemented!(),
+ Self::ColorModel => unimplemented!(),
+ Self::Package => SymbolKind::CLASS,
+ Self::Class => SymbolKind::CLASS,
+ Self::Entry(BibtexEntryTypeCategory::Misc) => SymbolKind::INTERFACE,
+ Self::Entry(BibtexEntryTypeCategory::String) => SymbolKind::STRING,
+ Self::Entry(BibtexEntryTypeCategory::Article) => SymbolKind::EVENT,
+ Self::Entry(BibtexEntryTypeCategory::Book) => SymbolKind::STRUCT,
+ Self::Entry(BibtexEntryTypeCategory::Collection) => SymbolKind::TYPE_PARAMETER,
+ Self::Entry(BibtexEntryTypeCategory::Part) => SymbolKind::OPERATOR,
+ Self::Entry(BibtexEntryTypeCategory::Thesis) => SymbolKind::OBJECT,
+ Self::Field => SymbolKind::FIELD,
+ Self::Argument => SymbolKind::NUMBER,
+ Self::GlossaryEntry => unimplemented!(),
+ }
+ }
+}