summaryrefslogtreecommitdiff
path: root/support/texlab/src/workspace
diff options
context:
space:
mode:
Diffstat (limited to 'support/texlab/src/workspace')
-rw-r--r--support/texlab/src/workspace/feature.rs246
-rw-r--r--support/texlab/src/workspace/mod.rs417
-rw-r--r--support/texlab/src/workspace/outline.rs438
-rw-r--r--support/texlab/src/workspace/uri.rs73
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(&current) {
+ continue;
+ }
+ visited.push(Arc::clone(&current));
+
+ results.push(Arc::clone(&current));
+ 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 = &section.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()
+ }
+}