diff options
author | Norbert Preining <norbert@preining.info> | 2022-12-30 03:01:26 +0000 |
---|---|---|
committer | Norbert Preining <norbert@preining.info> | 2022-12-30 03:01:26 +0000 |
commit | 844019377157163b461e0fd4a66592e61963a530 (patch) | |
tree | 32f61991c6a5acc3a3359ffc2cdefdd2aa004268 /support/texlab/src/util | |
parent | 55c69feeef908f49007708be194b7bb1c607f302 (diff) |
CTAN sync 202212300301
Diffstat (limited to 'support/texlab/src/util')
-rw-r--r-- | support/texlab/src/util/capabilities.rs | 179 | ||||
-rw-r--r-- | support/texlab/src/util/chktex.rs | 112 | ||||
-rw-r--r-- | support/texlab/src/util/components.rs | 120 | ||||
-rw-r--r-- | support/texlab/src/util/cursor.rs | 300 | ||||
-rw-r--r-- | support/texlab/src/util/label.rs | 269 | ||||
-rw-r--r-- | support/texlab/src/util/lang_data.rs | 70 | ||||
-rw-r--r-- | support/texlab/src/util/line_index.rs | 217 | ||||
-rw-r--r-- | support/texlab/src/util/line_index_ext.rs | 42 | ||||
-rw-r--r-- | support/texlab/src/util/lsp_enums.rs | 94 |
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(§ion), + 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!(), + } + } +} |