diff options
Diffstat (limited to 'support/texlab/src/workspace')
-rw-r--r-- | support/texlab/src/workspace/feature.rs | 246 | ||||
-rw-r--r-- | support/texlab/src/workspace/mod.rs | 417 | ||||
-rw-r--r-- | support/texlab/src/workspace/outline.rs | 438 | ||||
-rw-r--r-- | support/texlab/src/workspace/uri.rs | 73 |
4 files changed, 1174 insertions, 0 deletions
diff --git a/support/texlab/src/workspace/feature.rs b/support/texlab/src/workspace/feature.rs new file mode 100644 index 0000000000..815f5e1724 --- /dev/null +++ b/support/texlab/src/workspace/feature.rs @@ -0,0 +1,246 @@ +use super::{Document, DocumentView, Workspace, WorkspaceBuilder}; +use futures_boxed::boxed; +use lsp_types::*; +use std::sync::Arc; + +pub struct FeatureRequest<P> { + pub params: P, + pub view: DocumentView, + pub client_capabilities: Arc<ClientCapabilities>, + pub distribution: Arc<Box<dyn tex::Distribution>>, +} + +impl<P> FeatureRequest<P> { + pub fn workspace(&self) -> &Workspace { + &self.view.workspace + } + + pub fn document(&self) -> &Document { + &self.view.document + } + + pub fn related_documents(&self) -> &[Arc<Document>] { + &self.view.related_documents + } +} + +pub trait FeatureProvider { + type Params; + type Output; + + #[boxed] + async fn execute<'a>(&'a self, request: &'a FeatureRequest<Self::Params>) -> Self::Output; +} + +type ListProvider<P, O> = Box<dyn FeatureProvider<Params = P, Output = Vec<O>> + Send + Sync>; + +#[derive(Default)] +pub struct ConcatProvider<P, O> { + providers: Vec<ListProvider<P, O>>, +} + +impl<P, O> ConcatProvider<P, O> { + pub fn new(providers: Vec<ListProvider<P, O>>) -> Self { + Self { providers } + } +} + +impl<P, O> FeatureProvider for ConcatProvider<P, O> +where + P: Send + Sync, + O: Send + Sync, +{ + type Params = P; + type Output = Vec<O>; + + #[boxed] + async fn execute<'a>(&'a self, request: &'a FeatureRequest<P>) -> Vec<O> { + let mut items = Vec::new(); + for provider in &self.providers { + items.append(&mut provider.execute(request).await); + } + items + } +} + +type OptionProvider<P, O> = Box<dyn FeatureProvider<Params = P, Output = Option<O>> + Send + Sync>; + +#[derive(Default)] +pub struct ChoiceProvider<P, O> { + providers: Vec<OptionProvider<P, O>>, +} + +impl<P, O> ChoiceProvider<P, O> { + pub fn new(providers: Vec<OptionProvider<P, O>>) -> Self { + Self { providers } + } +} + +impl<P, O> FeatureProvider for ChoiceProvider<P, O> +where + P: Send + Sync, + O: Send + Sync, +{ + type Params = P; + type Output = Option<O>; + + #[boxed] + async fn execute<'a>(&'a self, request: &'a FeatureRequest<P>) -> Option<O> { + for provider in &self.providers { + let item = provider.execute(request).await; + if item.is_some() { + return item; + } + } + None + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct FeatureSpecFile { + name: &'static str, + text: &'static str, +} + +pub struct FeatureSpec { + pub files: Vec<FeatureSpecFile>, + pub main_file: &'static str, + pub position: Position, + pub new_name: &'static str, + pub include_declaration: bool, + pub client_capabilities: ClientCapabilities, + pub distribution: Box<dyn tex::Distribution>, +} + +impl Default for FeatureSpec { + fn default() -> Self { + Self { + files: Vec::new(), + main_file: "", + position: Position::new(0, 0), + new_name: "", + include_declaration: false, + client_capabilities: ClientCapabilities::default(), + distribution: Box::new(tex::Unknown::default()), + } + } +} + +impl FeatureSpec { + pub fn file(name: &'static str, text: &'static str) -> FeatureSpecFile { + FeatureSpecFile { name, text } + } + + pub fn uri(name: &str) -> Url { + let path = std::env::temp_dir().join(name); + Url::from_file_path(path).unwrap() + } + + fn identifier(&self) -> TextDocumentIdentifier { + let uri = Self::uri(self.main_file); + TextDocumentIdentifier::new(uri) + } + + fn view(&self) -> DocumentView { + let mut builder = WorkspaceBuilder::new(); + for file in &self.files { + builder.document(file.name, file.text); + } + let workspace = builder.workspace; + let main_uri = Self::uri(self.main_file); + let main_document = workspace.find(&main_uri.into()).unwrap(); + DocumentView::new(Arc::new(workspace), main_document) + } + + fn request<T>(self, params: T) -> FeatureRequest<T> { + FeatureRequest { + params, + view: self.view(), + client_capabilities: Arc::new(self.client_capabilities), + distribution: Arc::new(self.distribution), + } + } +} + +impl Into<FeatureRequest<TextDocumentPositionParams>> for FeatureSpec { + fn into(self) -> FeatureRequest<TextDocumentPositionParams> { + let params = TextDocumentPositionParams::new(self.identifier(), self.position); + self.request(params) + } +} + +impl Into<FeatureRequest<CompletionParams>> for FeatureSpec { + fn into(self) -> FeatureRequest<CompletionParams> { + let params = CompletionParams { + text_document_position: TextDocumentPositionParams::new( + self.identifier(), + self.position, + ), + context: None, + }; + self.request(params) + } +} + +impl Into<FeatureRequest<FoldingRangeParams>> for FeatureSpec { + fn into(self) -> FeatureRequest<FoldingRangeParams> { + let params = FoldingRangeParams { + text_document: self.identifier(), + }; + self.request(params) + } +} + +impl Into<FeatureRequest<DocumentLinkParams>> for FeatureSpec { + fn into(self) -> FeatureRequest<DocumentLinkParams> { + let params = DocumentLinkParams { + text_document: self.identifier(), + }; + self.request(params) + } +} + +impl Into<FeatureRequest<ReferenceParams>> for FeatureSpec { + fn into(self) -> FeatureRequest<ReferenceParams> { + let params = ReferenceParams { + text_document_position: TextDocumentPositionParams::new( + self.identifier(), + self.position, + ), + context: ReferenceContext { + include_declaration: self.include_declaration, + }, + }; + self.request(params) + } +} + +impl Into<FeatureRequest<RenameParams>> for FeatureSpec { + fn into(self) -> FeatureRequest<RenameParams> { + let params = RenameParams { + text_document_position: TextDocumentPositionParams::new( + self.identifier(), + self.position, + ), + new_name: self.new_name.to_owned(), + }; + self.request(params) + } +} + +impl Into<FeatureRequest<DocumentSymbolParams>> for FeatureSpec { + fn into(self) -> FeatureRequest<DocumentSymbolParams> { + let params = DocumentSymbolParams { + text_document: self.identifier(), + }; + self.request(params) + } +} + +pub fn test_feature<F, P, O, S>(provider: F, spec: S) -> O +where + F: FeatureProvider<Params = P, Output = O>, + S: Into<FeatureRequest<P>>, +{ + futures::executor::block_on(provider.execute(&spec.into())) +} diff --git a/support/texlab/src/workspace/mod.rs b/support/texlab/src/workspace/mod.rs new file mode 100644 index 0000000000..5ad2c21bf2 --- /dev/null +++ b/support/texlab/src/workspace/mod.rs @@ -0,0 +1,417 @@ +mod feature; +mod outline; +mod uri; + +pub use self::feature::*; +pub use self::outline::*; +pub use self::uri::*; +use crate::completion::DATABASE; +use crate::syntax::*; +use log::*; +use lsp_types::*; +use std::ffi::OsStr; +use std::fs; +use std::path::{Path, PathBuf}; +use std::sync::{Arc, Mutex}; +use std::time::SystemTime; +use tex::Language; + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct Document { + pub uri: Uri, + pub text: String, + pub tree: SyntaxTree, + pub modified: SystemTime, +} + +impl Document { + pub fn new(uri: Uri, text: String, tree: SyntaxTree) -> Self { + Self { + uri, + text, + tree, + modified: SystemTime::now(), + } + } + + pub fn parse(uri: Uri, text: String, language: Language) -> Self { + let tree = SyntaxTree::parse(&uri, &text, language); + Self::new(uri, text, tree) + } + + pub fn is_file(&self) -> bool { + self.uri.scheme() == "file" + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Default)] +pub struct Workspace { + pub documents: Vec<Arc<Document>>, +} + +impl Workspace { + pub fn new() -> Self { + Workspace { + documents: Vec::new(), + } + } + + pub fn find(&self, uri: &Uri) -> Option<Arc<Document>> { + self.documents + .iter() + .find(|document| &document.uri == uri) + .map(|document| Arc::clone(&document)) + } + + pub fn related_documents(&self, uri: &Uri) -> Vec<Arc<Document>> { + let edges = self.build_dependency_graph(); + let mut results = Vec::new(); + if let Some(start) = self.find(uri) { + let mut visited: Vec<Arc<Document>> = Vec::new(); + let mut stack = vec![start]; + while let Some(current) = stack.pop() { + if visited.contains(¤t) { + continue; + } + visited.push(Arc::clone(¤t)); + + results.push(Arc::clone(¤t)); + for edge in &edges { + if edge.0 == current { + stack.push(Arc::clone(&edge.1)); + } + } + } + } + results + } + + fn build_dependency_graph(&self) -> Vec<(Arc<Document>, Arc<Document>)> { + let mut edges: Vec<(Arc<Document>, Arc<Document>)> = Vec::new(); + for parent in self.documents.iter().filter(|document| document.is_file()) { + if let SyntaxTree::Latex(tree) = &parent.tree { + for include in &tree.includes { + for targets in &include.all_targets { + for target in targets { + if let Some(ref child) = self.find(target) { + edges.push((Arc::clone(&parent), Arc::clone(&child))); + edges.push((Arc::clone(&child), Arc::clone(&parent))); + } + } + } + } + + let tex_path = parent.uri.to_file_path().unwrap(); + let aux_path = tex_path.with_extension("aux"); + if let Some(child) = self.find(&Uri::from_file_path(aux_path).unwrap()) { + edges.push((Arc::clone(&parent), Arc::clone(&child))); + edges.push((Arc::clone(&child), Arc::clone(&parent))); + } + } + } + edges + } + + pub fn find_parent(&self, uri: &Uri) -> Option<Arc<Document>> { + for document in self.related_documents(uri) { + if let SyntaxTree::Latex(tree) = &document.tree { + if tree.env.is_standalone { + return Some(document); + } + } + } + None + } + + pub fn unresolved_includes(&self) -> Vec<PathBuf> { + let mut includes = Vec::new(); + for document in &self.documents { + if let SyntaxTree::Latex(tree) = &document.tree { + for include in &tree.includes { + match include.kind { + LatexIncludeKind::Bibliography | LatexIncludeKind::Latex => (), + LatexIncludeKind::Everything + | LatexIncludeKind::Image + | LatexIncludeKind::Pdf + | LatexIncludeKind::Svg => continue, + LatexIncludeKind::Package | LatexIncludeKind::Class => { + if include + .paths() + .iter() + .all(|name| DATABASE.contains(name.text())) + { + continue; + } + } + } + + for targets in &include.all_targets { + if targets.iter().any(|target| self.find(target).is_some()) { + continue; + } + + for target in targets { + let path = target.to_file_path().unwrap(); + if path.exists() { + includes.push(path); + } + } + } + } + + let aux_path = document.uri.to_file_path().unwrap().with_extension("aux"); + if self + .find(&Uri::from_file_path(&aux_path).unwrap()) + .is_none() + { + includes.push(aux_path); + } + } + } + includes + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct DocumentView { + pub workspace: Arc<Workspace>, + pub document: Arc<Document>, + pub related_documents: Vec<Arc<Document>>, +} + +impl DocumentView { + pub fn new(workspace: Arc<Workspace>, document: Arc<Document>) -> Self { + let related_documents = workspace.related_documents(&document.uri); + Self { + workspace, + document, + related_documents, + } + } +} + +#[derive(Debug)] +pub enum LoadError { + UnknownLanguage, + InvalidPath, + IO(std::io::Error), +} + +#[derive(Debug, Default)] +pub struct WorkspaceManager { + workspace: Mutex<Arc<Workspace>>, +} + +impl WorkspaceManager { + pub fn get(&self) -> Arc<Workspace> { + let workspace = self.workspace.lock().unwrap(); + Arc::clone(&workspace) + } + + pub fn add(&self, document: TextDocumentItem) { + let language = match Language::by_language_id(&document.language_id) { + Some(language) => language, + None => { + error!("Invalid language id: {}", &document.language_id); + return; + } + }; + + let mut workspace = self.workspace.lock().unwrap(); + *workspace = Self::add_or_update(&workspace, document.uri.into(), document.text, language); + } + + pub fn load(&self, path: &Path) -> Result<(), LoadError> { + let language = match path + .extension() + .and_then(OsStr::to_str) + .and_then(Language::by_extension) + { + Some(language) => language, + None => { + warn!("Could not determine language: {}", path.to_string_lossy()); + return Err(LoadError::UnknownLanguage); + } + }; + + let uri = match Uri::from_file_path(path) { + Ok(uri) => uri, + Err(_) => { + error!("Invalid path: {}", path.to_string_lossy()); + return Err(LoadError::InvalidPath); + } + }; + + let text = match fs::read_to_string(path) { + Ok(text) => text, + Err(why) => { + warn!("Could not open file: {}", path.to_string_lossy()); + return Err(LoadError::IO(why)); + } + }; + + let mut workspace = self.workspace.lock().unwrap(); + *workspace = Self::add_or_update(&workspace, uri, text, language); + Ok(()) + } + + pub fn update(&self, uri: Uri, text: String) { + let mut workspace = self.workspace.lock().unwrap(); + + let old_document = match workspace.documents.iter().find(|x| x.uri == uri) { + Some(document) => document, + None => { + warn!("Document not found: {}", uri); + return; + } + }; + + let language = match old_document.tree { + SyntaxTree::Latex(_) => Language::Latex, + SyntaxTree::Bibtex(_) => Language::Bibtex, + }; + + *workspace = Self::add_or_update(&workspace, uri, text, language); + } + + fn add_or_update( + workspace: &Workspace, + uri: Uri, + text: String, + language: Language, + ) -> Arc<Workspace> { + let document = Document::parse(uri, text, language); + let mut documents: Vec<Arc<Document>> = workspace + .documents + .iter() + .filter(|x| x.uri != document.uri) + .cloned() + .collect(); + + documents.push(Arc::new(document)); + Arc::new(Workspace { documents }) + } +} + +#[derive(Debug, Default)] +pub struct WorkspaceBuilder { + pub workspace: Workspace, +} + +impl WorkspaceBuilder { + pub fn new() -> Self { + Self::default() + } + + pub fn document(&mut self, name: &str, text: &str) -> Uri { + let path = std::env::temp_dir().join(name); + let language = Language::by_extension(path.extension().unwrap().to_str().unwrap()).unwrap(); + let uri = Uri::from_file_path(path).unwrap(); + let document = Document::parse(uri.clone(), text.to_owned(), language); + self.workspace.documents.push(Arc::new(document)); + uri + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn verify_documents(expected: Vec<Uri>, actual: Vec<Arc<Document>>) { + assert_eq!(expected.len(), actual.len()); + for i in 0..expected.len() { + assert_eq!(expected[i], actual[i].uri); + } + } + + #[test] + fn test_related_documents_append_extensions() { + let mut builder = WorkspaceBuilder::new(); + let uri1 = builder.document("foo.tex", "\\include{bar/baz}"); + let uri2 = builder.document("bar/baz.tex", ""); + let documents = builder.workspace.related_documents(&uri1); + verify_documents(vec![uri1, uri2], documents); + } + + #[test] + fn test_related_documents_relative_path() { + let mut builder = WorkspaceBuilder::new(); + let uri1 = builder.document("foo.tex", ""); + let uri2 = builder.document("bar/baz.tex", "\\input{../foo.tex}"); + let documents = builder.workspace.related_documents(&uri1); + verify_documents(vec![uri1, uri2], documents); + } + + #[test] + fn test_related_documents_invalid_includes() { + let mut builder = WorkspaceBuilder::new(); + let uri = builder.document("foo.tex", "\\include{<foo>?|bar|:}\n\\include{}"); + let documents = builder.workspace.related_documents(&uri); + verify_documents(vec![uri], documents); + } + + #[test] + fn test_related_documents_bibliographies() { + let mut builder = WorkspaceBuilder::new(); + let uri1 = builder.document("foo.tex", "\\addbibresource{bar.bib}"); + let uri2 = builder.document("bar.bib", ""); + let documents = builder.workspace.related_documents(&uri2); + verify_documents(vec![uri2, uri1], documents); + } + + #[test] + fn test_related_documents_unresolvable_include() { + let mut builder = WorkspaceBuilder::new(); + let uri = builder.document("foo.tex", "\\include{bar.tex}"); + builder.document("baz.tex", ""); + let documents = builder.workspace.related_documents(&uri); + verify_documents(vec![uri], documents); + } + + #[test] + fn test_related_documents_include_cycles() { + let mut builder = WorkspaceBuilder::new(); + let uri1 = builder.document("foo.tex", "\\input{bar.tex}"); + let uri2 = builder.document("bar.tex", "\\input{foo.tex}"); + let documents = builder.workspace.related_documents(&uri1); + verify_documents(vec![uri1, uri2], documents); + } + + #[test] + fn test_related_documents_same_parent() { + let mut builder = WorkspaceBuilder::new(); + let uri1 = builder.document("test.tex", "\\include{test1}\\include{test2}"); + let uri2 = builder.document("test1.tex", "\\label{foo}"); + let uri3 = builder.document("test2.tex", "\\ref{foo}"); + let documents = builder.workspace.related_documents(&uri3); + verify_documents(vec![uri3, uri1, uri2], documents); + } + + #[test] + fn test_related_documents_aux_file() { + let mut builder = WorkspaceBuilder::new(); + let uri1 = builder.document("foo.tex", "\\include{bar}"); + let uri2 = builder.document("bar.tex", ""); + let uri3 = builder.document("foo.aux", ""); + let documents = builder.workspace.related_documents(&uri2); + verify_documents(vec![uri2, uri1, uri3], documents); + } + + #[test] + fn test_find_parent() { + let mut builder = WorkspaceBuilder::new(); + let uri1 = builder.document("foo.tex", ""); + let uri2 = builder.document("bar.tex", "\\begin{document}\\include{foo}\\end{document}"); + let document = builder.workspace.find_parent(&uri1).unwrap(); + assert_eq!(uri2, document.uri); + } + + #[test] + fn test_find_parent_no_parent() { + let mut builder = WorkspaceBuilder::new(); + let uri = builder.document("foo.tex", ""); + builder.document("bar.tex", "\\begin{document}\\end{document}"); + let document = builder.workspace.find_parent(&uri); + assert_eq!(None, document); + } +} diff --git a/support/texlab/src/workspace/outline.rs b/support/texlab/src/workspace/outline.rs new file mode 100644 index 0000000000..818eed1a53 --- /dev/null +++ b/support/texlab/src/workspace/outline.rs @@ -0,0 +1,438 @@ +use super::{Document, DocumentView}; +use crate::range::RangeExt; +use crate::syntax::*; +use crate::workspace::Uri; +use lsp_types::*; +use std::collections::HashSet; + +#[derive(Debug, PartialEq, Eq, Clone, Default)] +pub struct Outline<'a> { + sections: Vec<OutlineSection<'a>>, +} + +impl<'a> Outline<'a> { + fn new(sections: Vec<OutlineSection<'a>>) -> Self { + Self { sections } + } + + pub fn find(&self, uri: &Uri, position: Position) -> Option<&'a LatexSection> { + self.sections + .iter() + .filter(|sec| sec.document.uri == *uri) + .rev() + .find(|sec| sec.item.end() <= position) + .map(|sec| sec.item) + } +} + +impl<'a> From<&'a DocumentView> for Outline<'a> { + fn from(view: &'a DocumentView) -> Self { + let mut finder = OutlineSectionFinder::default(); + let document = if let Some(parent) = view.workspace.find_parent(&view.document.uri) { + view.related_documents + .iter() + .find(|doc| doc.uri == parent.uri) + .unwrap() + } else { + &view.document + }; + finder.analyze(view, &document); + Outline::new(finder.sections) + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +struct OutlineSection<'a> { + pub document: &'a Document, + pub item: &'a LatexSection, +} + +impl<'a> OutlineSection<'a> { + fn new(document: &'a Document, item: &'a LatexSection) -> Self { + Self { document, item } + } +} + +#[derive(Debug, Default)] +struct OutlineSectionFinder<'a> { + visited: HashSet<&'a Uri>, + sections: Vec<OutlineSection<'a>>, +} + +impl<'a> OutlineSectionFinder<'a> { + fn analyze(&mut self, view: &'a DocumentView, document: &'a Document) { + if !self.visited.insert(&document.uri) { + return; + } + + if let SyntaxTree::Latex(tree) = &document.tree { + let mut items = Vec::new(); + for section in &tree.structure.sections { + items.push(OutlineItem::Section(section)); + } + for include in &tree.includes { + items.push(OutlineItem::Include(include)); + } + items.sort_by_key(SyntaxNode::start); + + for item in items { + match item { + OutlineItem::Section(item) => { + let section = OutlineSection::new(document, item); + self.sections.push(section); + } + OutlineItem::Include(item) => { + for document in &view.related_documents { + for targets in &item.all_targets { + if targets.contains(&document.uri) { + self.analyze(view, document); + break; + } + } + } + } + } + } + } + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +enum OutlineItem<'a> { + Section(&'a LatexSection), + Include(&'a LatexInclude), +} + +impl<'a> SyntaxNode for OutlineItem<'a> { + fn range(&self) -> Range { + match self { + OutlineItem::Section(section) => section.range(), + OutlineItem::Include(include) => include.range(), + } + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum OutlineCaptionKind { + Figure, + Table, + Listing, + Algorithm, +} + +impl OutlineCaptionKind { + pub fn as_str(self) -> &'static str { + match self { + Self::Figure => "Figure", + Self::Table => "Table", + Self::Listing => "Listing", + Self::Algorithm => "Algorithm", + } + } + + pub fn parse(environment_name: &str) -> Option<Self> { + match environment_name { + "figure" | "subfigure" => Some(Self::Figure), + "table" | "subtable" => Some(Self::Table), + "listing" | "lstlisting" => Some(Self::Listing), + "algorithm" => Some(Self::Algorithm), + _ => None, + } + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum OutlineContextItem { + Section { + prefix: &'static str, + text: String, + }, + Caption { + kind: Option<OutlineCaptionKind>, + text: String, + }, + Theorem { + kind: String, + description: Option<String>, + }, + Equation, + Item, +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct OutlineContext { + pub range: Range, + pub number: Option<String>, + pub item: OutlineContextItem, +} + +impl OutlineContext { + pub fn reference(&self) -> String { + match (&self.number, &self.item) { + (Some(number), OutlineContextItem::Section { prefix, text }) => { + format!("{} {} ({})", prefix, number, text) + } + (Some(number), OutlineContextItem::Caption { kind: None, text }) => { + format!("{} {}", number, text) + } + ( + Some(number), + OutlineContextItem::Caption { + kind: Some(kind), + text, + }, + ) => format!("{} {}: {}", kind.as_str(), number, text), + ( + Some(number), + OutlineContextItem::Theorem { + kind, + description: None, + }, + ) => format!("{} {}", kind, number), + ( + Some(number), + OutlineContextItem::Theorem { + kind, + description: Some(description), + }, + ) => format!("{} {} ({})", kind, number, description), + (Some(number), OutlineContextItem::Equation) => format!("Equation ({})", number), + (Some(number), OutlineContextItem::Item) => format!("Item {}", number), + (None, OutlineContextItem::Section { prefix, text }) => { + format!("{} ({})", prefix, text) + } + (None, OutlineContextItem::Caption { kind: None, text }) => text.clone(), + ( + None, + OutlineContextItem::Caption { + kind: Some(kind), + text, + }, + ) => format!("{}: {}", kind.as_str(), text), + ( + None, + OutlineContextItem::Theorem { + kind, + description: None, + }, + ) => kind.to_owned(), + ( + None, + OutlineContextItem::Theorem { + kind, + description: Some(description), + }, + ) => format!("{} ({})", kind, description), + (None, OutlineContextItem::Equation) => "Equation".to_owned(), + (None, OutlineContextItem::Item) => "Item".to_owned(), + } + } + + pub fn detail(&self) -> Option<String> { + match &self.item { + OutlineContextItem::Section { .. } + | OutlineContextItem::Theorem { .. } + | OutlineContextItem::Equation + | OutlineContextItem::Item => Some(self.reference()), + OutlineContextItem::Caption { + kind: Some(kind), .. + } => Some(match &self.number { + Some(number) => format!("{} {}", kind.as_str(), number), + None => kind.as_str().to_owned(), + }), + OutlineContextItem::Caption { .. } => None, + } + } + + pub fn documentation(&self) -> MarkupContent { + MarkupContent { + kind: MarkupKind::PlainText, + value: self.reference(), + } + } + + pub fn parse(view: &DocumentView, label: &LatexLabel, outline: &Outline) -> Option<Self> { + if let SyntaxTree::Latex(tree) = &view.document.tree { + Self::find_caption(view, label, tree) + .or_else(|| Self::find_theorem(view, label, tree)) + .or_else(|| Self::find_equation(view, label, tree)) + .or_else(|| Self::find_item(view, label, tree)) + .or_else(|| Self::find_section(view, label, outline)) + } else { + None + } + } + + fn find_caption( + view: &DocumentView, + label: &LatexLabel, + tree: &LatexSyntaxTree, + ) -> Option<Self> { + let caption_env = tree + .env + .environments + .iter() + .filter(|env| env.left.name().map(LatexToken::text) != Some("document")) + .find(|env| env.range().contains(label.start()))?; + + let caption = tree + .structure + .captions + .iter() + .find(|cap| tree.is_direct_child(caption_env, cap.start()))?; + + let caption_content = &caption.command.args[caption.index]; + let caption_text = extract_group(caption_content); + let caption_kind = caption_env + .left + .name() + .map(LatexToken::text) + .and_then(OutlineCaptionKind::parse); + + Some(Self { + range: caption_env.range(), + number: Self::find_number(view, label), + item: OutlineContextItem::Caption { + kind: caption_kind, + text: caption_text, + }, + }) + } + + fn find_theorem( + view: &DocumentView, + label: &LatexLabel, + tree: &LatexSyntaxTree, + ) -> Option<Self> { + let env = tree + .env + .environments + .iter() + .find(|env| env.range().contains(label.start()))?; + + let env_name = env.left.name().map(LatexToken::text)?; + + for document in &view.related_documents { + if let SyntaxTree::Latex(tree) = &document.tree { + for definition in &tree.math.theorem_definitions { + if env_name == definition.name().text() { + let kind = definition + .command + .args + .get(definition.index + 1) + .map(|content| extract_group(&content)) + .unwrap_or_else(|| titlelize(env_name)); + + let description = env + .left + .command + .options + .get(0) + .map(|content| extract_group(&content)); + + return Some(Self { + range: env.range(), + number: Self::find_number(view, label), + item: OutlineContextItem::Theorem { kind, description }, + }); + } + } + } + } + None + } + + fn find_equation( + view: &DocumentView, + label: &LatexLabel, + tree: &LatexSyntaxTree, + ) -> Option<Self> { + tree.env + .environments + .iter() + .filter(|env| env.left.is_math()) + .map(|env| env.range()) + .find(|range| range.contains(label.start())) + .map(|range| Self { + range, + number: Self::find_number(view, label), + item: OutlineContextItem::Equation, + }) + } + + fn find_item(view: &DocumentView, label: &LatexLabel, tree: &LatexSyntaxTree) -> Option<Self> { + struct LatexItemNode<'a> { + item: &'a LatexItem, + range: Range, + } + + let enumeration = tree + .env + .environments + .iter() + .find(|env| env.left.is_enum() && env.range().contains(label.start()))?; + + let mut item_nodes: Vec<_> = tree + .structure + .items + .iter() + .filter(|item| tree.is_enumeration_item(enumeration, item)) + .map(|item| LatexItemNode { + item, + range: Range::default(), + }) + .collect(); + + for i in 0..item_nodes.len() { + let start = item_nodes[i].item.start(); + let end = item_nodes + .get(i + 1) + .map(|node| node.item.start()) + .unwrap_or_else(|| enumeration.right.start()); + item_nodes[i].range = Range::new(start, end); + } + + let node = item_nodes + .iter() + .find(|node| node.range.contains(label.start()))?; + + let number = node.item.name().or_else(|| Self::find_number(view, label)); + + Some(Self { + range: enumeration.range(), + number, + item: OutlineContextItem::Item, + }) + } + + fn find_section(view: &DocumentView, label: &LatexLabel, outline: &Outline) -> Option<Self> { + let section = outline.find(&view.document.uri, label.start())?; + let content = §ion.command.args[section.index]; + Some(Self { + range: section.range(), + number: Self::find_number(view, label), + item: OutlineContextItem::Section { + prefix: section.prefix, + text: extract_group(content), + }, + }) + } + + pub fn find_number(view: &DocumentView, label: &LatexLabel) -> Option<String> { + let label_names = label.names(); + if label_names.len() != 1 { + return None; + } + + for document in &view.related_documents { + if let SyntaxTree::Latex(tree) = &document.tree { + for numbering in &tree.structure.label_numberings { + if numbering.name().text() == label_names[0].text() { + return Some(numbering.number.clone()); + } + } + } + } + None + } +} diff --git a/support/texlab/src/workspace/uri.rs b/support/texlab/src/workspace/uri.rs new file mode 100644 index 0000000000..7fc24d238f --- /dev/null +++ b/support/texlab/src/workspace/uri.rs @@ -0,0 +1,73 @@ +use lsp_types::*; +use serde::{Deserialize, Serialize}; +use std::fmt; +use std::hash::{Hash, Hasher}; +use std::ops::Deref; +use std::path::Path; + +#[derive(Debug, Eq, Clone, Serialize, Deserialize)] +pub struct Uri(Url); + +impl Uri { + pub fn from_file_path<P: AsRef<Path>>(path: P) -> Result<Self, ()> { + Url::from_file_path(path).map(|url| url.into()) + } +} + +impl PartialEq for Uri { + fn eq(&self, other: &Self) -> bool { + if cfg!(windows) { + self.as_str().to_lowercase() == other.as_str().to_lowercase() + } else { + self.as_str() == other.as_str() + } + } +} + +impl Hash for Uri { + fn hash<H: Hasher>(&self, state: &mut H) { + self.as_str().to_lowercase().hash(state); + } +} + +impl Deref for Uri { + type Target = Url; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From<Url> for Uri { + fn from(url: Url) -> Self { + Uri(url) + } +} + +impl Into<Url> for Uri { + fn into(self) -> Url { + self.0 + } +} + +impl fmt::Display for Uri { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +pub trait AsUri { + fn as_uri(&self) -> Uri; +} + +impl AsUri for TextDocumentIdentifier { + fn as_uri(&self) -> Uri { + self.uri.clone().into() + } +} + +impl AsUri for TextDocumentPositionParams { + fn as_uri(&self) -> Uri { + self.text_document.as_uri() + } +} |