summaryrefslogtreecommitdiff
path: root/support/texlab/src/completion/latex
diff options
context:
space:
mode:
Diffstat (limited to 'support/texlab/src/completion/latex')
-rw-r--r--support/texlab/src/completion/latex/argument.rs117
-rw-r--r--support/texlab/src/completion/latex/begin_command.rs30
-rw-r--r--support/texlab/src/completion/latex/citation.rs144
-rw-r--r--support/texlab/src/completion/latex/color.rs74
-rw-r--r--support/texlab/src/completion/latex/color_model.rs94
-rw-r--r--support/texlab/src/completion/latex/combinators.rs152
-rw-r--r--support/texlab/src/completion/latex/component.rs278
-rw-r--r--support/texlab/src/completion/latex/glossary.rs53
-rw-r--r--support/texlab/src/completion/latex/import.rs110
-rw-r--r--support/texlab/src/completion/latex/include.rs129
-rw-r--r--support/texlab/src/completion/latex/label.rs156
-rw-r--r--support/texlab/src/completion/latex/mod.rs14
-rw-r--r--support/texlab/src/completion/latex/theorem.rs70
-rw-r--r--support/texlab/src/completion/latex/tikz.rs90
-rw-r--r--support/texlab/src/completion/latex/user.rs156
15 files changed, 1667 insertions, 0 deletions
diff --git a/support/texlab/src/completion/latex/argument.rs b/support/texlab/src/completion/latex/argument.rs
new file mode 100644
index 0000000000..db05bca824
--- /dev/null
+++ b/support/texlab/src/completion/latex/argument.rs
@@ -0,0 +1,117 @@
+use super::combinators::{self, Parameter};
+use crate::completion::factory;
+use crate::completion::DATABASE;
+use crate::workspace::*;
+use futures_boxed::boxed;
+use lsp_types::*;
+use std::iter;
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+pub struct LatexArgumentCompletionProvider;
+
+impl FeatureProvider for LatexArgumentCompletionProvider {
+ type Params = CompletionParams;
+ type Output = Vec<CompletionItem>;
+
+ #[boxed]
+ async fn execute<'a>(&'a self, request: &'a FeatureRequest<Self::Params>) -> Self::Output {
+ let mut all_items = Vec::new();
+ for component in DATABASE.related_components(request.related_documents()) {
+ for command in &component.commands {
+ let name = format!("\\{}", command.name);
+ for (i, parameter) in command.parameters.iter().enumerate() {
+ let mut items = combinators::argument(
+ request,
+ iter::once(Parameter::new(&name, i)),
+ |context| {
+ async move {
+ let mut items = Vec::new();
+ for argument in &parameter.0 {
+ let text_edit =
+ TextEdit::new(context.range, (&argument.name).into());
+ let item = factory::argument(
+ request,
+ &argument.name,
+ text_edit,
+ argument.image.as_ref().map(AsRef::as_ref),
+ );
+ items.push(item);
+ }
+ items
+ }
+ },
+ )
+ .await;
+ all_items.append(&mut items);
+ }
+ }
+ }
+ all_items
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::range::RangeExt;
+ use lsp_types::{Position, Range};
+
+ #[test]
+ fn test_inside_mathbb_empty() {
+ let items = test_feature(
+ LatexArgumentCompletionProvider,
+ FeatureSpec {
+ files: vec![FeatureSpec::file(
+ "foo.tex",
+ "\\usepackage{amsfonts}\n\\mathbb{}",
+ )],
+ main_file: "foo.tex",
+ position: Position::new(1, 8),
+ ..FeatureSpec::default()
+ },
+ );
+ assert!(!items.is_empty());
+ assert_eq!(
+ items[0].text_edit.as_ref().map(|edit| edit.range),
+ Some(Range::new_simple(1, 8, 1, 8))
+ );
+ }
+
+ #[test]
+ fn test_inside_mathbb_non_empty() {
+ let items = test_feature(
+ LatexArgumentCompletionProvider,
+ FeatureSpec {
+ files: vec![FeatureSpec::file(
+ "foo.tex",
+ "\\usepackage{amsfonts}\n\\mathbb{foo}",
+ )],
+ main_file: "foo.tex",
+ position: Position::new(1, 8),
+ ..FeatureSpec::default()
+ },
+ );
+ assert!(!items.is_empty());
+ assert_eq!(
+ items[0].text_edit.as_ref().map(|edit| edit.range),
+ Some(Range::new_simple(1, 8, 1, 11))
+ );
+ }
+
+ #[test]
+ fn test_outside_mathbb() {
+ let items = test_feature(
+ LatexArgumentCompletionProvider,
+ FeatureSpec {
+ files: vec![FeatureSpec::file(
+ "foo.tex",
+ "\\usepackage{amsfonts}\n\\mathbb{}",
+ )],
+ main_file: "foo.tex",
+ position: Position::new(1, 9),
+ ..FeatureSpec::default()
+ },
+ );
+ assert!(items.is_empty());
+ }
+}
diff --git a/support/texlab/src/completion/latex/begin_command.rs b/support/texlab/src/completion/latex/begin_command.rs
new file mode 100644
index 0000000000..ce6ba17afa
--- /dev/null
+++ b/support/texlab/src/completion/latex/begin_command.rs
@@ -0,0 +1,30 @@
+use super::combinators;
+use crate::completion::factory::{self, LatexComponentId};
+use crate::workspace::*;
+use futures_boxed::boxed;
+use lsp_types::{CompletionItem, CompletionParams};
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+pub struct LatexBeginCommandCompletionProvider;
+
+impl FeatureProvider for LatexBeginCommandCompletionProvider {
+ type Params = CompletionParams;
+ type Output = Vec<CompletionItem>;
+
+ #[boxed]
+ async fn execute<'a>(&'a self, request: &'a FeatureRequest<Self::Params>) -> Self::Output {
+ combinators::command(request, |_| {
+ async move {
+ let snippet = factory::command_snippet(
+ request,
+ "begin",
+ None,
+ "begin{$1}\n\t$0\n\\end{$1}",
+ &LatexComponentId::kernel(),
+ );
+ vec![snippet]
+ }
+ })
+ .await
+ }
+}
diff --git a/support/texlab/src/completion/latex/citation.rs b/support/texlab/src/completion/latex/citation.rs
new file mode 100644
index 0000000000..a217c24bff
--- /dev/null
+++ b/support/texlab/src/completion/latex/citation.rs
@@ -0,0 +1,144 @@
+use super::combinators::{self, Parameter};
+use crate::completion::factory;
+use crate::syntax::*;
+use crate::workspace::*;
+use futures_boxed::boxed;
+use lsp_types::{CompletionItem, CompletionParams, TextEdit};
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+pub struct LatexCitationCompletionProvider;
+
+impl FeatureProvider for LatexCitationCompletionProvider {
+ type Params = CompletionParams;
+ type Output = Vec<CompletionItem>;
+
+ #[boxed]
+ async fn execute<'a>(&'a self, request: &'a FeatureRequest<Self::Params>) -> Self::Output {
+ let parameters = LANGUAGE_DATA
+ .citation_commands
+ .iter()
+ .map(|cmd| Parameter::new(&cmd.name, cmd.index));
+
+ combinators::argument(request, parameters, |context| {
+ async move {
+ let mut items = Vec::new();
+ for document in request.related_documents() {
+ if let SyntaxTree::Bibtex(tree) = &document.tree {
+ for entry in &tree.entries() {
+ if !entry.is_comment() {
+ if let Some(key) = &entry.key {
+ let key = key.text().to_owned();
+ let text_edit = TextEdit::new(context.range, key.clone());
+ let item = factory::citation(
+ request,
+ document.uri.clone(),
+ entry,
+ key,
+ text_edit,
+ );
+ items.push(item);
+ }
+ }
+ }
+ }
+ }
+ items
+ }
+ })
+ .await
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::range::RangeExt;
+ use lsp_types::{Position, Range};
+
+ #[test]
+ fn test_empty() {
+ let items = test_feature(
+ LatexCitationCompletionProvider,
+ FeatureSpec {
+ files: vec![
+ FeatureSpec::file("foo.tex", "\\addbibresource{bar.bib}\n\\cite{}"),
+ FeatureSpec::file("bar.bib", "@article{foo,}"),
+ FeatureSpec::file("baz.bib", "@article{bar,}"),
+ ],
+ main_file: "foo.tex",
+ position: Position::new(1, 6),
+ ..FeatureSpec::default()
+ },
+ );
+ assert_eq!(items.len(), 1);
+ assert_eq!(items[0].label, "foo");
+ assert_eq!(
+ items[0].text_edit.as_ref().map(|edit| edit.range),
+ Some(Range::new_simple(1, 6, 1, 6))
+ );
+ }
+
+ #[test]
+ fn test_single_key() {
+ let items = test_feature(
+ LatexCitationCompletionProvider,
+ FeatureSpec {
+ files: vec![
+ FeatureSpec::file("foo.tex", "\\addbibresource{bar.bib}\n\\cite{foo}"),
+ FeatureSpec::file("bar.bib", "@article{foo,}"),
+ FeatureSpec::file("baz.bib", "@article{bar,}"),
+ ],
+ main_file: "foo.tex",
+ position: Position::new(1, 6),
+ ..FeatureSpec::default()
+ },
+ );
+ assert_eq!(items.len(), 1);
+ assert_eq!(items[0].label, "foo");
+ assert_eq!(
+ items[0].text_edit.as_ref().map(|edit| edit.range),
+ Some(Range::new_simple(1, 6, 1, 9))
+ );
+ }
+
+ #[test]
+ fn test_second_key() {
+ let items = test_feature(
+ LatexCitationCompletionProvider,
+ FeatureSpec {
+ files: vec![
+ FeatureSpec::file("foo.tex", "\\addbibresource{bar.bib}\n\\cite{foo,}"),
+ FeatureSpec::file("bar.bib", "@article{foo,}"),
+ FeatureSpec::file("baz.bib", "@article{bar,}"),
+ ],
+ main_file: "foo.tex",
+ position: Position::new(1, 10),
+ ..FeatureSpec::default()
+ },
+ );
+ assert_eq!(items.len(), 1);
+ assert_eq!(items[0].label, "foo");
+ assert_eq!(
+ items[0].text_edit.as_ref().map(|edit| edit.range),
+ Some(Range::new_simple(1, 10, 1, 10))
+ );
+ }
+
+ #[test]
+ fn test_outside_cite() {
+ let items = test_feature(
+ LatexCitationCompletionProvider,
+ FeatureSpec {
+ files: vec![
+ FeatureSpec::file("foo.tex", "\\addbibresource{bar.bib}\n\\cite{}"),
+ FeatureSpec::file("bar.bib", "@article{foo,}"),
+ FeatureSpec::file("baz.bib", "@article{bar,}"),
+ ],
+ main_file: "foo.tex",
+ position: Position::new(1, 7),
+ ..FeatureSpec::default()
+ },
+ );
+ assert!(items.is_empty());
+ }
+}
diff --git a/support/texlab/src/completion/latex/color.rs b/support/texlab/src/completion/latex/color.rs
new file mode 100644
index 0000000000..b2d390b7d8
--- /dev/null
+++ b/support/texlab/src/completion/latex/color.rs
@@ -0,0 +1,74 @@
+use super::combinators::{self, Parameter};
+use crate::completion::factory;
+use crate::syntax::*;
+use crate::workspace::*;
+use futures_boxed::boxed;
+use lsp_types::{CompletionItem, CompletionParams, TextEdit};
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+pub struct LatexColorCompletionProvider;
+
+impl FeatureProvider for LatexColorCompletionProvider {
+ type Params = CompletionParams;
+ type Output = Vec<CompletionItem>;
+
+ #[boxed]
+ async fn execute<'a>(&'a self, request: &'a FeatureRequest<Self::Params>) -> Self::Output {
+ let parameters = LANGUAGE_DATA
+ .color_commands
+ .iter()
+ .map(|cmd| Parameter::new(&cmd.name, cmd.index));
+
+ combinators::argument(request, parameters, |context| {
+ async move {
+ let mut items = Vec::new();
+ for name in &LANGUAGE_DATA.colors {
+ let text_edit = TextEdit::new(context.range, name.into());
+ let item = factory::color(request, name, text_edit);
+ items.push(item);
+ }
+ items
+ }
+ })
+ .await
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::range::RangeExt;
+ use lsp_types::{Position, Range};
+
+ #[test]
+ fn test_inside_color() {
+ let items = test_feature(
+ LatexColorCompletionProvider,
+ FeatureSpec {
+ files: vec![FeatureSpec::file("foo.tex", "\\color{}")],
+ main_file: "foo.tex",
+ position: Position::new(0, 7),
+ ..FeatureSpec::default()
+ },
+ );
+ assert!(!items.is_empty());
+ assert_eq!(
+ items[0].text_edit.as_ref().map(|edit| edit.range),
+ Some(Range::new_simple(0, 7, 0, 7))
+ );
+ }
+
+ #[test]
+ fn test_outside_color() {
+ let items = test_feature(
+ LatexColorCompletionProvider,
+ FeatureSpec {
+ files: vec![FeatureSpec::file("foo.tex", "\\color{}")],
+ main_file: "foo.tex",
+ position: Position::new(0, 8),
+ ..FeatureSpec::default()
+ },
+ );
+ assert!(items.is_empty());
+ }
+}
diff --git a/support/texlab/src/completion/latex/color_model.rs b/support/texlab/src/completion/latex/color_model.rs
new file mode 100644
index 0000000000..5b6e85fa31
--- /dev/null
+++ b/support/texlab/src/completion/latex/color_model.rs
@@ -0,0 +1,94 @@
+use super::combinators::{self, Parameter};
+use crate::completion::factory;
+use crate::syntax::LANGUAGE_DATA;
+use crate::workspace::*;
+use futures_boxed::boxed;
+use lsp_types::{CompletionItem, CompletionParams, TextEdit};
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+pub struct LatexColorModelCompletionProvider;
+
+impl FeatureProvider for LatexColorModelCompletionProvider {
+ type Params = CompletionParams;
+ type Output = Vec<CompletionItem>;
+
+ #[boxed]
+ async fn execute<'a>(&'a self, request: &'a FeatureRequest<Self::Params>) -> Self::Output {
+ let parameters = LANGUAGE_DATA
+ .color_model_commands
+ .iter()
+ .map(|cmd| Parameter::new(&cmd.name, cmd.index));
+
+ combinators::argument(&request, parameters, |context| {
+ async move {
+ let mut items = Vec::new();
+ for name in MODEL_NAMES {
+ let text_edit = TextEdit::new(context.range, (*name).into());
+ let item = factory::color_model(request, name, text_edit);
+ items.push(item);
+ }
+ items
+ }
+ })
+ .await
+ }
+}
+
+const MODEL_NAMES: &[&str] = &["gray", "rgb", "RGB", "HTML", "cmyk"];
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::range::RangeExt;
+ use lsp_types::{Position, Range};
+
+ #[test]
+ fn test_inside_define_color() {
+ let items = test_feature(
+ LatexColorModelCompletionProvider,
+ FeatureSpec {
+ files: vec![FeatureSpec::file("foo.tex", "\\definecolor{name}{}")],
+ main_file: "foo.tex",
+ position: Position::new(0, 19),
+ ..FeatureSpec::default()
+ },
+ );
+ assert!(!items.is_empty());
+ assert_eq!(
+ items[0].text_edit.as_ref().map(|edit| edit.range),
+ Some(Range::new_simple(0, 19, 0, 19))
+ );
+ }
+
+ #[test]
+ fn test_outside_define_color() {
+ let items = test_feature(
+ LatexColorModelCompletionProvider,
+ FeatureSpec {
+ files: vec![FeatureSpec::file("foo.tex", "\\definecolor{name}{}")],
+ main_file: "foo.tex",
+ position: Position::new(0, 18),
+ ..FeatureSpec::default()
+ },
+ );
+ assert!(items.is_empty());
+ }
+
+ #[test]
+ fn tet_inside_define_color_set() {
+ let items = test_feature(
+ LatexColorModelCompletionProvider,
+ FeatureSpec {
+ files: vec![FeatureSpec::file("foo.tex", "\\definecolorset{}")],
+ main_file: "foo.tex",
+ position: Position::new(0, 16),
+ ..FeatureSpec::default()
+ },
+ );
+ assert!(!items.is_empty());
+ assert_eq!(
+ items[0].text_edit.as_ref().map(|edit| edit.range),
+ Some(Range::new_simple(0, 16, 0, 16))
+ );
+ }
+}
diff --git a/support/texlab/src/completion/latex/combinators.rs b/support/texlab/src/completion/latex/combinators.rs
new file mode 100644
index 0000000000..74337f85b6
--- /dev/null
+++ b/support/texlab/src/completion/latex/combinators.rs
@@ -0,0 +1,152 @@
+use crate::range::RangeExt;
+use crate::syntax::*;
+use crate::workspace::*;
+use lsp_types::*;
+use std::future::Future;
+use std::sync::Arc;
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+pub struct Parameter<'a> {
+ pub name: &'a str,
+ pub index: usize,
+}
+
+impl<'a> Parameter<'a> {
+ pub fn new(name: &'a str, index: usize) -> Self {
+ Self { name, index }
+ }
+}
+
+pub async fn command<E, F>(
+ request: &FeatureRequest<CompletionParams>,
+ execute: E,
+) -> Vec<CompletionItem>
+where
+ E: FnOnce(Arc<LatexCommand>) -> F,
+ F: Future<Output = Vec<CompletionItem>>,
+{
+ if let SyntaxTree::Latex(tree) = &request.document().tree {
+ if let Some(command) =
+ tree.find_command_by_name(request.params.text_document_position.position)
+ {
+ return execute(command).await;
+ }
+ }
+ Vec::new()
+}
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub struct ArgumentContext<'a> {
+ pub parameter: Parameter<'a>,
+ pub command: Arc<LatexCommand>,
+ pub range: Range,
+}
+
+pub async fn argument<'a, I, E, F>(
+ request: &'a FeatureRequest<CompletionParams>,
+ mut parameters: I,
+ execute: E,
+) -> Vec<CompletionItem>
+where
+ I: Iterator<Item = Parameter<'a>>,
+ E: FnOnce(ArgumentContext<'a>) -> F,
+ F: Future<Output = Vec<CompletionItem>>,
+{
+ if let SyntaxTree::Latex(tree) = &request.document().tree {
+ let position = request.params.text_document_position.position;
+ if let Some(command) = find_command(tree, position) {
+ for parameter in parameters.by_ref() {
+ if command.name.text() != parameter.name {
+ continue;
+ }
+
+ if let Some(args) = command.args.get(parameter.index) {
+ if args.right.is_some() && !args.range().contains_exclusive(position) {
+ continue;
+ }
+
+ let mut range = None;
+ for child in &args.children {
+ if let LatexContent::Text(text) = &child {
+ for word in &text.words {
+ if word.range().contains(position) {
+ range = Some(word.range());
+ break;
+ }
+ }
+ }
+ }
+ let text_range = range.unwrap_or_else(|| Range::new(position, position));
+ let context = ArgumentContext {
+ parameter,
+ command: Arc::clone(&command),
+ range: text_range,
+ };
+ return execute(context).await;
+ }
+ }
+ }
+ }
+ Vec::new()
+}
+
+pub async fn argument_word<'a, I, E, F>(
+ request: &'a FeatureRequest<CompletionParams>,
+ mut parameters: I,
+ execute: E,
+) -> Vec<CompletionItem>
+where
+ I: Iterator<Item = Parameter<'a>>,
+ E: FnOnce(Arc<LatexCommand>, usize) -> F,
+ F: Future<Output = Vec<CompletionItem>>,
+{
+ if let SyntaxTree::Latex(tree) = &request.document().tree {
+ let position = request.params.text_document_position.position;
+ if let Some(command) = find_command(tree, position) {
+ for parameter in parameters.by_ref() {
+ if command.name.text() != parameter.name {
+ continue;
+ }
+
+ if let Some(args) = command.args.get(parameter.index) {
+ if args.right.is_some() && !args.range().contains_exclusive(position) {
+ continue;
+ }
+
+ if !args.children.is_empty() && !command.has_word(parameter.index) {
+ continue;
+ }
+
+ return execute(Arc::clone(&command), parameter.index).await;
+ }
+ }
+ }
+ }
+ Vec::new()
+}
+
+pub async fn environment<'a, E, F>(
+ request: &'a FeatureRequest<CompletionParams>,
+ execute: E,
+) -> Vec<CompletionItem>
+where
+ E: FnOnce(ArgumentContext<'a>) -> F,
+ F: Future<Output = Vec<CompletionItem>>,
+{
+ let parameters = LANGUAGE_DATA
+ .environment_commands
+ .iter()
+ .map(|cmd| Parameter::new(&cmd.name, cmd.index));
+ argument(request, parameters, execute).await
+}
+
+fn find_command(tree: &LatexSyntaxTree, position: Position) -> Option<Arc<LatexCommand>> {
+ let mut nodes = tree.find(position);
+ nodes.reverse();
+ for node in nodes {
+ if let LatexNode::Command(command) = node {
+ return Some(command);
+ }
+ }
+ None
+}
diff --git a/support/texlab/src/completion/latex/component.rs b/support/texlab/src/completion/latex/component.rs
new file mode 100644
index 0000000000..3699a4959e
--- /dev/null
+++ b/support/texlab/src/completion/latex/component.rs
@@ -0,0 +1,278 @@
+use super::combinators;
+use crate::completion::factory::{self, LatexComponentId};
+use crate::completion::DATABASE;
+use crate::workspace::*;
+use futures_boxed::boxed;
+use lsp_types::*;
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+pub struct LatexComponentCommandCompletionProvider;
+
+impl FeatureProvider for LatexComponentCommandCompletionProvider {
+ type Params = CompletionParams;
+ type Output = Vec<CompletionItem>;
+
+ #[boxed]
+ async fn execute<'a>(&'a self, request: &'a FeatureRequest<Self::Params>) -> Self::Output {
+ combinators::command(request, |command| {
+ async move {
+ let range = command.short_name_range();
+ let mut items = Vec::new();
+ for component in DATABASE.related_components(request.related_documents()) {
+ let file_names = component.file_names.iter().map(AsRef::as_ref).collect();
+ let id = LatexComponentId::Component(file_names);
+ for command in &component.commands {
+ let text_edit = TextEdit::new(range, (&command.name).into());
+ let item = factory::command(
+ request,
+ (&command.name).into(),
+ command.image.as_ref().map(AsRef::as_ref),
+ command.glyph.as_ref().map(AsRef::as_ref),
+ text_edit,
+ &id,
+ );
+ items.push(item);
+ }
+ }
+ items
+ }
+ })
+ .await
+ }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+pub struct LatexComponentEnvironmentCompletionProvider;
+
+impl FeatureProvider for LatexComponentEnvironmentCompletionProvider {
+ type Params = CompletionParams;
+ type Output = Vec<CompletionItem>;
+
+ #[boxed]
+ async fn execute<'a>(&'a self, request: &'a FeatureRequest<Self::Params>) -> Self::Output {
+ combinators::environment(request, |context| {
+ async move {
+ let mut items = Vec::new();
+ for component in DATABASE.related_components(request.related_documents()) {
+ let file_names = component.file_names.iter().map(AsRef::as_ref).collect();
+ let id = LatexComponentId::Component(file_names);
+ for environment in &component.environments {
+ let text_edit = TextEdit::new(context.range, environment.into());
+ let item =
+ factory::environment(request, environment.into(), text_edit, &id);
+ items.push(item);
+ }
+ }
+ items
+ }
+ })
+ .await
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::range::RangeExt;
+ use lsp_types::{Position, Range};
+
+ #[test]
+ fn test_command_start() {
+ let items = test_feature(
+ LatexComponentCommandCompletionProvider,
+ FeatureSpec {
+ files: vec![FeatureSpec::file("foo.tex", "\\use")],
+ main_file: "foo.tex",
+ position: Position::new(0, 0),
+ ..FeatureSpec::default()
+ },
+ );
+ assert!(items.is_empty());
+ }
+
+ #[test]
+ fn test_command_end() {
+ let items = test_feature(
+ LatexComponentCommandCompletionProvider,
+ FeatureSpec {
+ files: vec![FeatureSpec::file("foo.tex", "\\use")],
+ main_file: "foo.tex",
+ position: Position::new(0, 4),
+ ..FeatureSpec::default()
+ },
+ );
+ assert!(!items.is_empty());
+ assert_eq!(
+ items[0].text_edit.as_ref().map(|edit| edit.range),
+ Some(Range::new_simple(0, 1, 0, 4))
+ );
+ }
+
+ #[test]
+ fn test_command_word() {
+ let items = test_feature(
+ LatexComponentCommandCompletionProvider,
+ FeatureSpec {
+ files: vec![FeatureSpec::file("foo.tex", "use")],
+ main_file: "foo.tex",
+ position: Position::new(0, 2),
+ ..FeatureSpec::default()
+ },
+ );
+ assert!(items.is_empty());
+ }
+
+ #[test]
+ fn test_command_package() {
+ let items = test_feature(
+ LatexComponentCommandCompletionProvider,
+ FeatureSpec {
+ files: vec![FeatureSpec::file("foo.tex", "\\usepackage{lipsum}\n\\lips")],
+ main_file: "foo.tex",
+ position: Position::new(1, 2),
+ ..FeatureSpec::default()
+ },
+ );
+ assert!(items.iter().any(|item| item.label == "lipsum"));
+ }
+
+ #[test]
+ fn test_command_package_comma_separated() {
+ let items = test_feature(
+ LatexComponentCommandCompletionProvider,
+ FeatureSpec {
+ files: vec![FeatureSpec::file(
+ "foo.tex",
+ "\\usepackage{geometry, lipsum}\n\\lips",
+ )],
+ main_file: "foo.tex",
+ position: Position::new(1, 2),
+ ..FeatureSpec::default()
+ },
+ );
+ assert!(items.iter().any(|item| item.label == "lipsum"));
+ }
+
+ #[test]
+ fn test_command_class() {
+ let items = test_feature(
+ LatexComponentCommandCompletionProvider,
+ FeatureSpec {
+ files: vec![FeatureSpec::file(
+ "foo.tex",
+ "\\documentclass{book}\n\\chap",
+ )],
+ main_file: "foo.tex",
+ position: Position::new(1, 2),
+ ..FeatureSpec::default()
+ },
+ );
+ assert!(items.iter().any(|item| item.label == "chapter"));
+ }
+
+ #[test]
+ fn test_environment_inside_of_empty_begin() {
+ let items = test_feature(
+ LatexComponentEnvironmentCompletionProvider,
+ FeatureSpec {
+ files: vec![FeatureSpec::file("foo.tex", "\\begin{}")],
+ main_file: "foo.tex",
+ position: Position::new(0, 7),
+ ..FeatureSpec::default()
+ },
+ );
+ assert!(!items.is_empty());
+ assert_eq!(
+ items[0].text_edit.as_ref().map(|edit| edit.range),
+ Some(Range::new_simple(0, 7, 0, 7))
+ );
+ }
+
+ #[test]
+ fn test_environment_inside_of_non_empty_end() {
+ let items = test_feature(
+ LatexComponentEnvironmentCompletionProvider,
+ FeatureSpec {
+ files: vec![FeatureSpec::file("foo.tex", "\\end{foo}")],
+ main_file: "foo.tex",
+ position: Position::new(0, 6),
+ ..FeatureSpec::default()
+ },
+ );
+ assert!(!items.is_empty());
+ assert_eq!(
+ items[0].text_edit.as_ref().map(|edit| edit.range),
+ Some(Range::new_simple(0, 5, 0, 8))
+ );
+ }
+
+ #[test]
+ fn test_environment_outside_of_empty_begin() {
+ let items = test_feature(
+ LatexComponentEnvironmentCompletionProvider,
+ FeatureSpec {
+ files: vec![FeatureSpec::file("foo.tex", "\\begin{}")],
+ main_file: "foo.tex",
+ position: Position::new(0, 6),
+ ..FeatureSpec::default()
+ },
+ );
+ assert!(items.is_empty());
+ }
+
+ #[test]
+ fn test_environment_outside_of_empty_end() {
+ let items = test_feature(
+ LatexComponentEnvironmentCompletionProvider,
+ FeatureSpec {
+ files: vec![FeatureSpec::file("foo.tex", "\\end{}")],
+ main_file: "foo.tex",
+ position: Position::new(0, 6),
+ ..FeatureSpec::default()
+ },
+ );
+ assert!(items.is_empty());
+ }
+
+ #[test]
+ fn test_environment_inside_of_other_command() {
+ let items = test_feature(
+ LatexComponentEnvironmentCompletionProvider,
+ FeatureSpec {
+ files: vec![FeatureSpec::file("foo.tex", "\\foo{bar}")],
+ main_file: "foo.tex",
+ position: Position::new(0, 6),
+ ..FeatureSpec::default()
+ },
+ );
+ assert!(items.is_empty());
+ }
+
+ #[test]
+ fn test_environment_inside_second_argument() {
+ let items = test_feature(
+ LatexComponentEnvironmentCompletionProvider,
+ FeatureSpec {
+ files: vec![FeatureSpec::file("foo.tex", "\\begin{foo}{bar}")],
+ main_file: "foo.tex",
+ position: Position::new(0, 14),
+ ..FeatureSpec::default()
+ },
+ );
+ assert!(items.is_empty());
+ }
+
+ #[test]
+ fn test_environment_unterminated() {
+ let items = test_feature(
+ LatexComponentEnvironmentCompletionProvider,
+ FeatureSpec {
+ files: vec![FeatureSpec::file("foo.tex", "\\begin{ foo")],
+ main_file: "foo.tex",
+ position: Position::new(0, 7),
+ ..FeatureSpec::default()
+ },
+ );
+ assert!(!items.is_empty());
+ }
+}
diff --git a/support/texlab/src/completion/latex/glossary.rs b/support/texlab/src/completion/latex/glossary.rs
new file mode 100644
index 0000000000..ca6600b690
--- /dev/null
+++ b/support/texlab/src/completion/latex/glossary.rs
@@ -0,0 +1,53 @@
+use super::combinators::{self, Parameter};
+use crate::completion::factory;
+use crate::syntax::LatexGlossaryEntryKind::*;
+use crate::syntax::*;
+use crate::workspace::*;
+use futures_boxed::boxed;
+use lsp_types::{CompletionItem, CompletionParams, TextEdit};
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+pub struct LatexGlossaryCompletionProvider;
+
+impl FeatureProvider for LatexGlossaryCompletionProvider {
+ type Params = CompletionParams;
+ type Output = Vec<CompletionItem>;
+
+ #[boxed]
+ async fn execute<'a>(&'a self, request: &'a FeatureRequest<Self::Params>) -> Self::Output {
+ let parameters = LANGUAGE_DATA
+ .glossary_entry_reference_commands
+ .iter()
+ .map(|cmd| Parameter::new(&cmd.name, cmd.index));
+
+ combinators::argument(request, parameters, |context| {
+ async move {
+ let cmd_kind = LANGUAGE_DATA
+ .glossary_entry_reference_commands
+ .iter()
+ .find(|cmd| cmd.name == context.parameter.name)
+ .unwrap()
+ .kind;
+
+ let mut items = Vec::new();
+ for document in request.related_documents() {
+ if let SyntaxTree::Latex(tree) = &document.tree {
+ for entry in &tree.glossary.entries {
+ match (cmd_kind, entry.kind) {
+ (Acronym, Acronym) | (General, General) | (General, Acronym) => {
+ let label = entry.label().text().to_owned();
+ let text_edit = TextEdit::new(context.range, label.clone());
+ let item = factory::glossary_entry(request, label, text_edit);
+ items.push(item);
+ }
+ (Acronym, General) => {}
+ }
+ }
+ }
+ }
+ items
+ }
+ })
+ .await
+ }
+}
diff --git a/support/texlab/src/completion/latex/import.rs b/support/texlab/src/completion/latex/import.rs
new file mode 100644
index 0000000000..7f333f17ae
--- /dev/null
+++ b/support/texlab/src/completion/latex/import.rs
@@ -0,0 +1,110 @@
+use super::combinators::{self, Parameter};
+use crate::completion::factory;
+use crate::completion::DATABASE;
+use crate::syntax::*;
+use crate::workspace::*;
+use futures_boxed::boxed;
+use lsp_types::{CompletionItem, CompletionParams, TextEdit};
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub struct LatexClassImportProvider;
+
+impl FeatureProvider for LatexClassImportProvider {
+ type Params = CompletionParams;
+ type Output = Vec<CompletionItem>;
+
+ #[boxed]
+ async fn execute<'a>(&'a self, request: &'a FeatureRequest<Self::Params>) -> Self::Output {
+ import(request, LatexIncludeKind::Class, factory::class).await
+ }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub struct LatexPackageImportProvider;
+
+impl FeatureProvider for LatexPackageImportProvider {
+ type Params = CompletionParams;
+ type Output = Vec<CompletionItem>;
+
+ #[boxed]
+ async fn execute<'a>(&'a self, request: &'a FeatureRequest<Self::Params>) -> Self::Output {
+ import(request, LatexIncludeKind::Package, factory::package).await
+ }
+}
+
+async fn import<F>(
+ request: &FeatureRequest<CompletionParams>,
+ kind: LatexIncludeKind,
+ factory: F,
+) -> Vec<CompletionItem>
+where
+ F: Fn(&FeatureRequest<CompletionParams>, &'static str, TextEdit) -> CompletionItem,
+{
+ let extension = if kind == LatexIncludeKind::Package {
+ "sty"
+ } else {
+ "cls"
+ };
+
+ let parameters = LANGUAGE_DATA
+ .include_commands
+ .iter()
+ .filter(|cmd| cmd.kind == kind)
+ .map(|cmd| Parameter::new(&cmd.name, cmd.index));
+
+ combinators::argument(request, parameters, |context| {
+ async move {
+ let mut items = Vec::new();
+ for component in &DATABASE.components {
+ for file_name in &component.file_names {
+ if file_name.ends_with(extension) {
+ let stem = &file_name[0..file_name.len() - 4];
+ let text_edit = TextEdit::new(context.range, stem.into());
+ let item = factory(request, stem, text_edit);
+ items.push(item);
+ }
+ }
+ }
+ items
+ }
+ })
+ .await
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use lsp_types::Position;
+
+ #[test]
+ fn test_class() {
+ let items = test_feature(
+ LatexClassImportProvider,
+ FeatureSpec {
+ files: vec![FeatureSpec::file("foo.tex", "\\documentclass{}")],
+ main_file: "foo.tex",
+ position: Position::new(0, 15),
+ ..FeatureSpec::default()
+ },
+ );
+
+ assert!(items.iter().any(|item| item.label == "beamer"));
+ assert!(items.iter().all(|item| item.label != "amsmath"));
+ }
+
+ #[test]
+ fn test_package() {
+ let items = test_feature(
+ LatexPackageImportProvider,
+ FeatureSpec {
+ files: vec![FeatureSpec::file("foo.tex", "\\usepackage{}")],
+ main_file: "foo.tex",
+ position: Position::new(0, 12),
+ ..FeatureSpec::default()
+ },
+ );
+
+ assert!(items.iter().all(|item| item.label != "beamer"));
+ assert!(items.iter().any(|item| item.label == "amsmath"));
+ }
+}
diff --git a/support/texlab/src/completion/latex/include.rs b/support/texlab/src/completion/latex/include.rs
new file mode 100644
index 0000000000..2f4090b3f3
--- /dev/null
+++ b/support/texlab/src/completion/latex/include.rs
@@ -0,0 +1,129 @@
+use super::combinators::{self, Parameter};
+use crate::completion::factory;
+use crate::range::RangeExt;
+use crate::syntax::*;
+use crate::workspace::*;
+use futures_boxed::boxed;
+use lsp_types::{CompletionItem, CompletionParams, Range, TextEdit};
+use std::path::{Path, PathBuf};
+use walkdir::WalkDir;
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+pub struct LatexIncludeCompletionProvider;
+
+impl FeatureProvider for LatexIncludeCompletionProvider {
+ type Params = CompletionParams;
+ type Output = Vec<CompletionItem>;
+
+ #[boxed]
+ async fn execute<'a>(&'a self, request: &'a FeatureRequest<Self::Params>) -> Self::Output {
+ let parameters = LANGUAGE_DATA
+ .include_commands
+ .iter()
+ .map(|cmd| Parameter::new(&cmd.name, cmd.index));
+
+ combinators::argument_word(request, parameters, |command, index| {
+ async move {
+ if !request.document().is_file() {
+ return Vec::new();
+ }
+
+ let position = request.params.text_document_position.position;
+ let mut items = Vec::new();
+ let path_word = command.extract_word(index);
+ let name_range = match path_word {
+ Some(path_word) => Range::new_simple(
+ path_word.start().line,
+ path_word.end().character
+ - path_word.text().split('/').last().unwrap().chars().count() as u64,
+ path_word.end().line,
+ path_word.end().character,
+ ),
+ None => Range::new(position, position),
+ };
+ let directory = current_directory(&request, &command);
+
+ for entry in WalkDir::new(directory)
+ .min_depth(1)
+ .max_depth(1)
+ .follow_links(false)
+ .into_iter()
+ .filter_map(std::result::Result::ok)
+ {
+ if entry.file_type().is_file() && is_included(&command, &entry.path()) {
+ let mut path = entry.into_path();
+ let include_extension = LANGUAGE_DATA
+ .include_commands
+ .iter()
+ .find(|cmd| command.name.text() == cmd.name)
+ .unwrap()
+ .include_extension;
+
+ if !include_extension {
+ remove_extension(&mut path);
+ }
+ let text_edit = make_text_edit(name_range, &path);
+ items.push(factory::file(request, &path, text_edit));
+ } else if entry.file_type().is_dir() {
+ let path = entry.into_path();
+ let text_edit = make_text_edit(name_range, &path);
+ items.push(factory::folder(request, &path, text_edit));
+ }
+ }
+ items
+ }
+ })
+ .await
+ }
+}
+
+fn current_directory(
+ request: &FeatureRequest<CompletionParams>,
+ command: &LatexCommand,
+) -> PathBuf {
+ let mut path = request.document().uri.to_file_path().unwrap();
+ path = PathBuf::from(path.to_string_lossy().into_owned().replace('\\', "/"));
+
+ path.pop();
+ if let Some(include) = command.extract_word(0) {
+ path.push(include.text());
+ if !include.text().ends_with('/') {
+ path.pop();
+ }
+ }
+ path
+}
+
+fn is_included(command: &LatexCommand, file: &Path) -> bool {
+ if let Some(allowed_extensions) = LANGUAGE_DATA
+ .include_commands
+ .iter()
+ .find(|cmd| command.name.text() == cmd.name)
+ .unwrap()
+ .kind
+ .extensions()
+ {
+ file.extension()
+ .map(|extension| extension.to_string_lossy().to_lowercase())
+ .map(|extension| allowed_extensions.contains(&extension.as_str()))
+ .unwrap_or(false)
+ } else {
+ true
+ }
+}
+
+fn remove_extension(path: &mut PathBuf) {
+ let stem = path
+ .file_stem()
+ .map(|stem| stem.to_string_lossy().into_owned());
+
+ if let Some(stem) = stem {
+ path.pop();
+ path.push(PathBuf::from(stem));
+ }
+}
+
+fn make_text_edit(range: Range, path: &Path) -> TextEdit {
+ let text = path.file_name().unwrap().to_string_lossy().into_owned();
+ TextEdit::new(range, text)
+}
diff --git a/support/texlab/src/completion/latex/label.rs b/support/texlab/src/completion/latex/label.rs
new file mode 100644
index 0000000000..f8d74f4a22
--- /dev/null
+++ b/support/texlab/src/completion/latex/label.rs
@@ -0,0 +1,156 @@
+use super::combinators::{self, ArgumentContext, Parameter};
+use crate::completion::factory;
+use crate::range::RangeExt;
+use crate::syntax::*;
+use crate::workspace::*;
+use futures_boxed::boxed;
+use lsp_types::*;
+use std::sync::Arc;
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+pub struct LatexLabelCompletionProvider;
+
+impl FeatureProvider for LatexLabelCompletionProvider {
+ type Params = CompletionParams;
+ type Output = Vec<CompletionItem>;
+
+ #[boxed]
+ async fn execute<'a>(&'a self, request: &'a FeatureRequest<Self::Params>) -> Self::Output {
+ let parameters = LANGUAGE_DATA
+ .label_commands
+ .iter()
+ .filter(|cmd| cmd.kind.is_reference())
+ .map(|cmd| Parameter::new(&cmd.name, cmd.index));
+
+ combinators::argument(request, parameters, |context| {
+ async move {
+ let source = Self::find_source(&context);
+ let mut items = Vec::new();
+ for document in request.related_documents() {
+ let workspace = Arc::clone(&request.view.workspace);
+ let view = DocumentView::new(workspace, Arc::clone(&document));
+ let outline = Outline::from(&view);
+
+ if let SyntaxTree::Latex(tree) = &document.tree {
+ for label in tree
+ .structure
+ .labels
+ .iter()
+ .filter(|label| label.kind == LatexLabelKind::Definition)
+ .filter(|label| Self::is_included(tree, label, source))
+ {
+ let outline_context = OutlineContext::parse(&view, &label, &outline);
+ for name in label.names() {
+ let text = name.text().to_owned();
+ let text_edit = TextEdit::new(context.range, text.clone());
+ let item = factory::label(
+ request,
+ text,
+ text_edit,
+ outline_context.as_ref(),
+ );
+ items.push(item);
+ }
+ }
+ }
+ }
+ items
+ }
+ })
+ .await
+ }
+}
+
+impl LatexLabelCompletionProvider {
+ fn find_source(context: &ArgumentContext) -> LatexLabelReferenceSource {
+ match LANGUAGE_DATA
+ .label_commands
+ .iter()
+ .find(|cmd| cmd.name == context.parameter.name && cmd.index == context.parameter.index)
+ .map(|cmd| cmd.kind)
+ .unwrap()
+ {
+ LatexLabelKind::Definition => unreachable!(),
+ LatexLabelKind::Reference(source) => source,
+ }
+ }
+
+ fn is_included(
+ tree: &LatexSyntaxTree,
+ label: &LatexLabel,
+ source: LatexLabelReferenceSource,
+ ) -> bool {
+ match source {
+ LatexLabelReferenceSource::Everything => true,
+ LatexLabelReferenceSource::Math => tree
+ .env
+ .environments
+ .iter()
+ .filter(|env| env.left.is_math())
+ .any(|env| env.range().contains_exclusive(label.start())),
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use lsp_types::Position;
+
+ #[test]
+ fn test_inside_of_ref() {
+ let items = test_feature(
+ LatexLabelCompletionProvider,
+ FeatureSpec {
+ files: vec![
+ FeatureSpec::file(
+ "foo.tex",
+ "\\addbibresource{bar.bib}\\include{baz}\n\\ref{}",
+ ),
+ FeatureSpec::file("bar.bib", ""),
+ FeatureSpec::file("baz.tex", "\\label{foo}\\label{bar}\\ref{baz}"),
+ ],
+ main_file: "foo.tex",
+ position: Position::new(1, 5),
+ ..FeatureSpec::default()
+ },
+ );
+ let labels: Vec<&str> = items.iter().map(|item| item.label.as_ref()).collect();
+ assert_eq!(labels, vec!["foo", "bar"]);
+ }
+
+ #[test]
+ fn test_outside_of_ref() {
+ let items = test_feature(
+ LatexLabelCompletionProvider,
+ FeatureSpec {
+ files: vec![
+ FeatureSpec::file("foo.tex", "\\include{bar}\\ref{}"),
+ FeatureSpec::file("bar.tex", "\\label{foo}\\label{bar}"),
+ ],
+ main_file: "foo.tex",
+ position: Position::new(1, 6),
+ ..FeatureSpec::default()
+ },
+ );
+ assert!(items.is_empty());
+ }
+
+ #[test]
+ fn test_eqref() {
+ let items = test_feature(
+ LatexLabelCompletionProvider,
+ FeatureSpec {
+ files: vec![FeatureSpec::file(
+ "foo.tex",
+ "\\begin{align}\\label{foo}\\end{align}\\label{bar}\n\\eqref{}",
+ )],
+ main_file: "foo.tex",
+ position: Position::new(1, 7),
+ ..FeatureSpec::default()
+ },
+ );
+ let labels: Vec<&str> = items.iter().map(|item| item.label.as_ref()).collect();
+ assert_eq!(labels, vec!["foo"]);
+ }
+}
diff --git a/support/texlab/src/completion/latex/mod.rs b/support/texlab/src/completion/latex/mod.rs
new file mode 100644
index 0000000000..2902678c11
--- /dev/null
+++ b/support/texlab/src/completion/latex/mod.rs
@@ -0,0 +1,14 @@
+pub mod argument;
+pub mod begin_command;
+pub mod citation;
+pub mod color;
+pub mod color_model;
+pub mod combinators;
+pub mod component;
+pub mod glossary;
+pub mod import;
+pub mod include;
+pub mod label;
+pub mod theorem;
+pub mod tikz;
+pub mod user;
diff --git a/support/texlab/src/completion/latex/theorem.rs b/support/texlab/src/completion/latex/theorem.rs
new file mode 100644
index 0000000000..993ef5c7e8
--- /dev/null
+++ b/support/texlab/src/completion/latex/theorem.rs
@@ -0,0 +1,70 @@
+use super::combinators;
+use crate::completion::factory::{self, LatexComponentId};
+use crate::syntax::*;
+use crate::workspace::*;
+use futures_boxed::boxed;
+use lsp_types::{CompletionItem, CompletionParams, TextEdit};
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+pub struct LatexTheoremEnvironmentCompletionProvider;
+
+impl FeatureProvider for LatexTheoremEnvironmentCompletionProvider {
+ type Params = CompletionParams;
+ type Output = Vec<CompletionItem>;
+
+ #[boxed]
+ async fn execute<'a>(&'a self, request: &'a FeatureRequest<Self::Params>) -> Self::Output {
+ combinators::environment(request, |context| {
+ async move {
+ let mut items = Vec::new();
+ for document in request.related_documents() {
+ if let SyntaxTree::Latex(tree) = &document.tree {
+ for theorem in &tree.math.theorem_definitions {
+ let name = theorem.name().text().to_owned();
+ let text_edit = TextEdit::new(context.range, name.clone());
+ let item = factory::environment(
+ request,
+ name,
+ text_edit,
+ &LatexComponentId::User,
+ );
+ items.push(item);
+ }
+ }
+ }
+ items
+ }
+ })
+ .await
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::range::RangeExt;
+ use lsp_types::{Position, Range};
+ use std::borrow::Cow;
+
+ #[test]
+ fn test() {
+ let items = test_feature(
+ LatexTheoremEnvironmentCompletionProvider,
+ FeatureSpec {
+ files: vec![FeatureSpec::file(
+ "foo.tex",
+ "\\newtheorem{theorem}{Theorem}\n\\begin{th}",
+ )],
+ main_file: "foo.tex",
+ position: Position::new(1, 8),
+ ..FeatureSpec::default()
+ },
+ );
+ assert_eq!(items.len(), 1);
+ assert_eq!(items[0].label, Cow::from("theorem"));
+ assert_eq!(
+ items[0].text_edit.as_ref().map(|edit| edit.range),
+ Some(Range::new_simple(1, 7, 1, 9))
+ );
+ }
+}
diff --git a/support/texlab/src/completion/latex/tikz.rs b/support/texlab/src/completion/latex/tikz.rs
new file mode 100644
index 0000000000..2d6740e1a8
--- /dev/null
+++ b/support/texlab/src/completion/latex/tikz.rs
@@ -0,0 +1,90 @@
+use super::combinators::{self, Parameter};
+use crate::completion::factory;
+use crate::syntax::LANGUAGE_DATA;
+use crate::workspace::*;
+use futures_boxed::boxed;
+use lsp_types::{CompletionItem, CompletionParams, TextEdit};
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+pub struct LatexPgfLibraryCompletionProvider;
+
+impl FeatureProvider for LatexPgfLibraryCompletionProvider {
+ type Params = CompletionParams;
+ type Output = Vec<CompletionItem>;
+
+ #[boxed]
+ async fn execute<'a>(&'a self, request: &'a FeatureRequest<Self::Params>) -> Self::Output {
+ let parameter = Parameter::new("\\usepgflibrary", 0);
+ combinators::argument(request, std::iter::once(parameter), |context| {
+ async move {
+ let mut items = Vec::new();
+ for name in &LANGUAGE_DATA.pgf_libraries {
+ let text_edit = TextEdit::new(context.range, name.into());
+ let item = factory::pgf_library(request, name, text_edit);
+ items.push(item);
+ }
+ items
+ }
+ })
+ .await
+ }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+pub struct LatexTikzLibraryCompletionProvider;
+
+impl FeatureProvider for LatexTikzLibraryCompletionProvider {
+ type Params = CompletionParams;
+ type Output = Vec<CompletionItem>;
+
+ #[boxed]
+ async fn execute<'a>(&'a self, request: &'a FeatureRequest<Self::Params>) -> Self::Output {
+ let parameter = Parameter::new("\\usetikzlibrary", 0);
+ combinators::argument(request, std::iter::once(parameter), |context| {
+ async move {
+ let mut items = Vec::new();
+ for name in &LANGUAGE_DATA.tikz_libraries {
+ let text_edit = TextEdit::new(context.range, name.into());
+ let item = factory::tikz_library(request, name, text_edit);
+ items.push(item);
+ }
+ items
+ }
+ })
+ .await
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use lsp_types::Position;
+
+ #[test]
+ fn test_pgf_library() {
+ let items = test_feature(
+ LatexPgfLibraryCompletionProvider,
+ FeatureSpec {
+ files: vec![FeatureSpec::file("foo.tex", "\\usepgflibrary{}")],
+ main_file: "foo.tex",
+ position: Position::new(0, 15),
+ ..FeatureSpec::default()
+ },
+ );
+ assert!(!items.is_empty());
+ }
+
+ #[test]
+ fn test_tikz_library() {
+ let items = test_feature(
+ LatexTikzLibraryCompletionProvider,
+ FeatureSpec {
+ files: vec![FeatureSpec::file("foo.tex", "\\usetikzlibrary{}")],
+ main_file: "foo.tex",
+ position: Position::new(0, 16),
+ ..FeatureSpec::default()
+ },
+ );
+ assert!(!items.is_empty());
+ }
+}
diff --git a/support/texlab/src/completion/latex/user.rs b/support/texlab/src/completion/latex/user.rs
new file mode 100644
index 0000000000..890a75882b
--- /dev/null
+++ b/support/texlab/src/completion/latex/user.rs
@@ -0,0 +1,156 @@
+use super::combinators;
+use crate::completion::factory::{self, LatexComponentId};
+use crate::syntax::*;
+use crate::workspace::*;
+use futures_boxed::boxed;
+use itertools::Itertools;
+use lsp_types::*;
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+pub struct LatexUserCommandCompletionProvider;
+
+impl FeatureProvider for LatexUserCommandCompletionProvider {
+ type Params = CompletionParams;
+ type Output = Vec<CompletionItem>;
+
+ #[boxed]
+ async fn execute<'a>(&'a self, request: &'a FeatureRequest<Self::Params>) -> Self::Output {
+ combinators::command(request, |current_command| {
+ async move {
+ let mut items = Vec::new();
+ for document in request.related_documents() {
+ if let SyntaxTree::Latex(tree) = &document.tree {
+ tree.commands
+ .iter()
+ .filter(|command| command.range() != current_command.range())
+ .map(|command| &command.name.text()[1..])
+ .unique()
+ .map(|command| {
+ let text_edit = TextEdit::new(
+ current_command.short_name_range(),
+ command.to_owned(),
+ );
+ factory::command(
+ request,
+ command.to_owned(),
+ None,
+ None,
+ text_edit,
+ &LatexComponentId::User,
+ )
+ })
+ .for_each(|item| items.push(item));
+ }
+ }
+ items
+ }
+ })
+ .await
+ }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+pub struct LatexUserEnvironmentCompletionProvider;
+
+impl FeatureProvider for LatexUserEnvironmentCompletionProvider {
+ type Params = CompletionParams;
+ type Output = Vec<CompletionItem>;
+
+ #[boxed]
+ async fn execute<'a>(&'a self, request: &'a FeatureRequest<Self::Params>) -> Self::Output {
+ combinators::environment(request, |context| {
+ async move {
+ let mut items = Vec::new();
+ for document in request.related_documents() {
+ if let SyntaxTree::Latex(tree) = &document.tree {
+ for environment in &tree.env.environments {
+ if environment.left.command == context.command
+ || environment.right.command == context.command
+ {
+ continue;
+ }
+
+ if let Some(item) =
+ Self::make_item(request, &environment.left, context.range)
+ {
+ items.push(item);
+ }
+
+ if let Some(item) =
+ Self::make_item(request, &environment.right, context.range)
+ {
+ items.push(item);
+ }
+ }
+ }
+ }
+ items
+ }
+ })
+ .await
+ }
+}
+
+impl LatexUserEnvironmentCompletionProvider {
+ fn make_item(
+ request: &FeatureRequest<CompletionParams>,
+ delimiter: &LatexEnvironmentDelimiter,
+ name_range: Range,
+ ) -> Option<CompletionItem> {
+ if let Some(name) = delimiter.name() {
+ let text = name.text().to_owned();
+ let text_edit = TextEdit::new(name_range, text.clone());
+ let item = factory::environment(request, text, text_edit, &LatexComponentId::User);
+ return Some(item);
+ }
+ None
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use lsp_types::Position;
+
+ #[test]
+ fn test_command() {
+ let items = test_feature(
+ LatexUserCommandCompletionProvider,
+ FeatureSpec {
+ files: vec![
+ FeatureSpec::file("foo.tex", "\\include{bar.tex}\n\\foo"),
+ FeatureSpec::file("bar.tex", "\\bar"),
+ FeatureSpec::file("baz.tex", "\\baz"),
+ ],
+ main_file: "foo.tex",
+ position: Position::new(1, 2),
+ ..FeatureSpec::default()
+ },
+ );
+ let labels: Vec<&str> = items.iter().map(|item| item.label.as_ref()).collect();
+ assert_eq!(labels, vec!["include", "bar"]);
+ }
+
+ #[test]
+ fn test_environment() {
+ let items = test_feature(
+ LatexUserEnvironmentCompletionProvider,
+ FeatureSpec {
+ files: vec![
+ FeatureSpec::file("foo.tex", "\\include{bar.tex}\n\\begin{foo}"),
+ FeatureSpec::file("bar.tex", "\\begin{bar}\\end{bar}"),
+ FeatureSpec::file("baz.tex", "\\begin{baz}\\end{baz}"),
+ ],
+ main_file: "foo.tex",
+ position: Position::new(1, 9),
+ ..FeatureSpec::default()
+ },
+ );
+ let labels: Vec<&str> = items
+ .iter()
+ .map(|item| item.label.as_ref())
+ .unique()
+ .collect();
+ assert_eq!(labels, vec!["bar"]);
+ }
+}