mirror of
https://github.com/latex-lsp/texlab.git
synced 2025-08-04 18:58:31 +00:00
Move workspace module to a separate crate
This commit is contained in:
parent
24abeaf8d5
commit
864a109ab8
79 changed files with 203 additions and 162 deletions
19
crates/texlab_workspace/Cargo.toml
Normal file
19
crates/texlab_workspace/Cargo.toml
Normal file
|
@ -0,0 +1,19 @@
|
|||
[package]
|
||||
name = "texlab-workspace"
|
||||
version = "0.1.0"
|
||||
authors = [
|
||||
"Eric Förster <efoerster@users.noreply.github.com>",
|
||||
"Patrick Förster <pfoerster@users.noreply.github.com>"]
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
futures = "0.3"
|
||||
futures-boxed = { path = "../futures_boxed" }
|
||||
itertools = "0.8.2"
|
||||
log = "0.4.6"
|
||||
once_cell = "1.2.0"
|
||||
serde = { version = "1.0.103", features = ["derive", "rc"] }
|
||||
serde_json = "1.0.44"
|
||||
texlab-distro = { path = "../texlab_distro" }
|
||||
texlab-protocol = { path = "../texlab_protocol" }
|
||||
texlab-syntax = { path = "../texlab_syntax" }
|
1
crates/texlab_workspace/src/completion.json
Normal file
1
crates/texlab_workspace/src/completion.json
Normal file
File diff suppressed because one or more lines are too long
125
crates/texlab_workspace/src/completion.rs
Normal file
125
crates/texlab_workspace/src/completion.rs
Normal file
|
@ -0,0 +1,125 @@
|
|||
use super::document::Document;
|
||||
use itertools::Itertools;
|
||||
use once_cell::sync::Lazy;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use texlab_protocol::{MarkupContent, MarkupKind};
|
||||
use texlab_syntax::*;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Database {
|
||||
pub components: Vec<Component>,
|
||||
pub metadata: Vec<Metadata>,
|
||||
}
|
||||
|
||||
impl Database {
|
||||
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 contains(&self, short_name: &str) -> bool {
|
||||
let sty = format!("{}.sty", short_name);
|
||||
let cls = format!("{}.cls", short_name);
|
||||
self.find(&sty).is_some() || self.find(&cls).is_some()
|
||||
}
|
||||
|
||||
pub fn kernel(&self) -> &Component {
|
||||
self.components
|
||||
.iter()
|
||||
.find(|component| component.file_names.is_empty())
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn related_components(&self, documents: &[Arc<Document>]) -> Vec<&Component> {
|
||||
let mut start_components = vec![self.kernel()];
|
||||
for document in documents {
|
||||
if let SyntaxTree::Latex(tree) = &document.tree {
|
||||
tree.components
|
||||
.iter()
|
||||
.flat_map(|file| self.find(file))
|
||||
.for_each(|component| start_components.push(component))
|
||||
}
|
||||
}
|
||||
|
||||
let mut all_components = Vec::new();
|
||||
for component in start_components {
|
||||
all_components.push(component);
|
||||
component
|
||||
.references
|
||||
.iter()
|
||||
.flat_map(|file| self.find(&file))
|
||||
.for_each(|component| all_components.push(component))
|
||||
}
|
||||
|
||||
all_components
|
||||
.into_iter()
|
||||
.unique_by(|component| &component.file_names)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn exists(&self, file_name: &str) -> bool {
|
||||
self.components
|
||||
.iter()
|
||||
.any(|component| component.file_names.iter().any(|f| f == file_name))
|
||||
}
|
||||
|
||||
pub fn documentation(&self, name: &str) -> Option<MarkupContent> {
|
||||
let metadata = self
|
||||
.metadata
|
||||
.iter()
|
||||
.find(|metadata| metadata.name == name)?;
|
||||
|
||||
let desc = metadata.description.to_owned()?;
|
||||
Some(MarkupContent {
|
||||
kind: MarkupKind::PlainText,
|
||||
value: desc,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Component {
|
||||
pub file_names: Vec<String>,
|
||||
pub references: Vec<String>,
|
||||
pub commands: Vec<Command>,
|
||||
pub environments: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Command {
|
||||
pub name: String,
|
||||
pub image: Option<String>,
|
||||
pub glyph: Option<String>,
|
||||
pub parameters: Vec<Parameter>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Parameter(pub Vec<Argument>);
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Argument {
|
||||
pub name: String,
|
||||
pub image: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Metadata {
|
||||
pub name: String,
|
||||
pub caption: Option<String>,
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
const JSON: &str = include_str!("completion.json");
|
||||
|
||||
pub static COMPLETION_DATABASE: Lazy<Database> = Lazy::new(|| serde_json::from_str(JSON).unwrap());
|
36
crates/texlab_workspace/src/document.rs
Normal file
36
crates/texlab_workspace/src/document.rs
Normal file
|
@ -0,0 +1,36 @@
|
|||
use std::time::SystemTime;
|
||||
use texlab_distro::{Language, Resolver};
|
||||
use texlab_protocol::*;
|
||||
use texlab_syntax::{SyntaxTree, SyntaxTreeContext};
|
||||
|
||||
#[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(resolver: &Resolver, uri: Uri, text: String, language: Language) -> Self {
|
||||
let context = SyntaxTreeContext {
|
||||
resolver,
|
||||
uri: &uri,
|
||||
};
|
||||
let tree = SyntaxTree::parse(context, &text, language);
|
||||
Self::new(uri, text, tree)
|
||||
}
|
||||
|
||||
pub fn is_file(&self) -> bool {
|
||||
self.uri.scheme() == "file"
|
||||
}
|
||||
}
|
266
crates/texlab_workspace/src/feature.rs
Normal file
266
crates/texlab_workspace/src/feature.rs
Normal file
|
@ -0,0 +1,266 @@
|
|||
use super::document::Document;
|
||||
use super::workspace::{Workspace, WorkspaceBuilder};
|
||||
use futures_boxed::boxed;
|
||||
use std::sync::Arc;
|
||||
use texlab_distro::{Distribution, UnknownDistribution};
|
||||
use texlab_protocol::*;
|
||||
|
||||
#[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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FeatureRequest<P> {
|
||||
pub params: P,
|
||||
pub view: DocumentView,
|
||||
pub client_capabilities: Arc<ClientCapabilities>,
|
||||
pub distribution: Arc<Box<dyn 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 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(UnknownDistribution::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()))
|
||||
}
|
11
crates/texlab_workspace/src/lib.rs
Normal file
11
crates/texlab_workspace/src/lib.rs
Normal file
|
@ -0,0 +1,11 @@
|
|||
mod completion;
|
||||
mod document;
|
||||
mod feature;
|
||||
mod outline;
|
||||
mod workspace;
|
||||
|
||||
pub use self::completion::*;
|
||||
pub use self::document::Document;
|
||||
pub use self::feature::*;
|
||||
pub use self::outline::*;
|
||||
pub use self::workspace::*;
|
437
crates/texlab_workspace/src/outline.rs
Normal file
437
crates/texlab_workspace/src/outline.rs
Normal file
|
@ -0,0 +1,437 @@
|
|||
use super::document::Document;
|
||||
use super::feature::DocumentView;
|
||||
use std::collections::HashSet;
|
||||
use texlab_protocol::*;
|
||||
use texlab_syntax::*;
|
||||
|
||||
#[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
|
||||
}
|
||||
}
|
382
crates/texlab_workspace/src/workspace.rs
Normal file
382
crates/texlab_workspace/src/workspace.rs
Normal file
|
@ -0,0 +1,382 @@
|
|||
use super::completion::COMPLETION_DATABASE;
|
||||
use super::document::Document;
|
||||
use futures::executor::block_on;
|
||||
use log::*;
|
||||
use std::env;
|
||||
use std::ffi::OsStr;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use texlab_distro::{Distribution, Language, Resolver};
|
||||
use texlab_protocol::*;
|
||||
use texlab_syntax::*;
|
||||
|
||||
#[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| COMPLETION_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 {
|
||||
if let Ok(path) = target.to_file_path() {
|
||||
if path.exists() {
|
||||
includes.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(aux_path) = document
|
||||
.uri
|
||||
.to_file_path()
|
||||
.map(|path| path.with_extension("aux"))
|
||||
{
|
||||
if self
|
||||
.find(&Uri::from_file_path(&aux_path).unwrap())
|
||||
.is_none()
|
||||
{
|
||||
includes.push(aux_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
includes
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum LoadError {
|
||||
UnknownLanguage,
|
||||
InvalidPath,
|
||||
IO(std::io::Error),
|
||||
}
|
||||
|
||||
pub struct WorkspaceManager {
|
||||
distribution: Arc<Box<dyn Distribution>>,
|
||||
workspace: Mutex<Arc<Workspace>>,
|
||||
}
|
||||
|
||||
impl WorkspaceManager {
|
||||
pub fn new(distribution: Arc<Box<dyn Distribution>>) -> Self {
|
||||
Self {
|
||||
distribution,
|
||||
workspace: Mutex::default(),
|
||||
}
|
||||
}
|
||||
|
||||
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(
|
||||
&self,
|
||||
workspace: &Workspace,
|
||||
uri: Uri,
|
||||
text: String,
|
||||
language: Language,
|
||||
) -> Arc<Workspace> {
|
||||
let resolver = block_on(self.distribution.resolver());
|
||||
let document = Document::parse(&resolver, 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 resolver = Resolver::default();
|
||||
let path = 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(&resolver, 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);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue