summaryrefslogtreecommitdiff
path: root/support/texlab/crates/completion/src/providers
diff options
context:
space:
mode:
Diffstat (limited to 'support/texlab/crates/completion/src/providers')
-rw-r--r--support/texlab/crates/completion/src/providers/argument.rs76
-rw-r--r--support/texlab/crates/completion/src/providers/citations.rs71
-rw-r--r--support/texlab/crates/completion/src/providers/color.rs116
-rw-r--r--support/texlab/crates/completion/src/providers/color_model.rs50
-rw-r--r--support/texlab/crates/completion/src/providers/command.rs134
-rw-r--r--support/texlab/crates/completion/src/providers/entry_type.rs40
-rw-r--r--support/texlab/crates/completion/src/providers/environment.rs135
-rw-r--r--support/texlab/crates/completion/src/providers/field.rs66
-rw-r--r--support/texlab/crates/completion/src/providers/glossary.rs88
-rw-r--r--support/texlab/crates/completion/src/providers/import.rs69
-rw-r--r--support/texlab/crates/completion/src/providers/include.rs157
-rw-r--r--support/texlab/crates/completion/src/providers/label.rs101
-rw-r--r--support/texlab/crates/completion/src/providers/tikz_library.rs149
13 files changed, 1252 insertions, 0 deletions
diff --git a/support/texlab/crates/completion/src/providers/argument.rs b/support/texlab/crates/completion/src/providers/argument.rs
new file mode 100644
index 0000000000..6637cecdd6
--- /dev/null
+++ b/support/texlab/crates/completion/src/providers/argument.rs
@@ -0,0 +1,76 @@
+use base_db::semantics::Span;
+use rowan::{ast::AstNode, TokenAtOffset};
+use syntax::latex;
+
+use crate::{
+ util::{included_packages, is_inside_latex_curly, CompletionBuilder},
+ ArgumentData, CompletionItem, CompletionItemData, CompletionParams,
+};
+
+pub fn complete_arguments<'a>(
+ params: &'a CompletionParams<'a>,
+ builder: &mut CompletionBuilder<'a>,
+) -> Option<()> {
+ let (cursor, group) = find_argument(params)?;
+
+ let command = latex::GenericCommand::cast(group.syntax().parent()?)?;
+
+ let index = command
+ .syntax()
+ .children()
+ .filter_map(latex::CurlyGroup::cast)
+ .position(|g| g.syntax().text_range() == group.syntax().text_range())?;
+
+ let command_name = command.name()?;
+ let command_name = &command_name.text()[1..];
+
+ for package in included_packages(&params.feature) {
+ for package_command in package
+ .commands
+ .iter()
+ .filter(|command| command.name == command_name)
+ {
+ for (_, param) in package_command
+ .parameters
+ .iter()
+ .enumerate()
+ .filter(|(i, _)| *i == index)
+ {
+ for arg in &param.0 {
+ if let Some(score) = builder.matcher.score(&arg.name, &cursor.text) {
+ let data = CompletionItemData::Argument(ArgumentData(arg));
+ builder
+ .items
+ .push(CompletionItem::new_simple(score, cursor.range, data));
+ }
+ }
+ }
+ }
+ }
+
+ Some(())
+}
+
+fn find_argument(params: &CompletionParams) -> Option<(Span, latex::CurlyGroup)> {
+ let data = params.feature.document.data.as_tex()?;
+ let tokens = data.root_node().token_at_offset(params.offset);
+
+ let (span, token) = match tokens.clone().find(|token| token.kind() == latex::WORD) {
+ Some(token) => (Span::from(&token), token),
+ None if matches!(tokens, TokenAtOffset::Between(_, _)) => {
+ (Span::empty(params.offset), tokens.left_biased()?)
+ }
+ None => return None,
+ };
+
+ let group = latex::CurlyGroup::cast(token.parent()?)
+ .or_else(|| {
+ token
+ .parent()
+ .and_then(|node| node.parent())
+ .and_then(latex::CurlyGroup::cast)
+ })
+ .filter(|group| is_inside_latex_curly(group, params.offset))?;
+
+ Some((span, group))
+}
diff --git a/support/texlab/crates/completion/src/providers/citations.rs b/support/texlab/crates/completion/src/providers/citations.rs
new file mode 100644
index 0000000000..631a899203
--- /dev/null
+++ b/support/texlab/crates/completion/src/providers/citations.rs
@@ -0,0 +1,71 @@
+use base_db::semantics::Span;
+use rayon::prelude::{IntoParallelRefIterator, ParallelExtend, ParallelIterator};
+use rowan::ast::AstNode;
+use syntax::latex;
+
+use crate::{
+ util::{find_curly_group_word_list, CompletionBuilder},
+ CitationData, CompletionItem, CompletionItemData, CompletionParams,
+};
+
+pub fn complete_citations<'a>(
+ params: &'a CompletionParams<'a>,
+ builder: &mut CompletionBuilder<'a>,
+) -> Option<()> {
+ let cursor = find_citation(params)?;
+
+ for document in &params.feature.project.documents {
+ if let Some(data) = document.data.as_bib() {
+ let items = data.semantics.entries.par_iter().filter_map(|entry| {
+ let score = builder.matcher.score(&entry.name.text, &cursor.text)?;
+ let data = CompletionItemData::Citation(CitationData { document, entry });
+ Some(CompletionItem::new_simple(score, cursor.range, data))
+ });
+
+ builder.items.par_extend(items);
+ }
+ }
+
+ Some(())
+}
+
+fn find_citation(params: &CompletionParams) -> Option<Span> {
+ find_citation_command(params).or_else(|| find_citation_acronym(params))
+}
+
+fn find_citation_command(params: &CompletionParams) -> Option<Span> {
+ let (span, group) = find_curly_group_word_list(params)?;
+ latex::Citation::cast(group.syntax().parent()?)?;
+ Some(span)
+}
+
+fn find_citation_acronym(params: &CompletionParams) -> Option<Span> {
+ let offset = params.offset;
+ let data = params.feature.document.data.as_tex()?;
+ let root = data.root_node();
+ let tokens = root.token_at_offset(offset);
+ let token = tokens
+ .clone()
+ .find(|token| token.kind() == latex::WORD)
+ .or_else(|| tokens.left_biased())?;
+
+ let span = if token.kind() == latex::WORD {
+ let name = latex::Text::cast(token.parent()?)?;
+ Span::new(token.text().into(), latex::small_range(&name))
+ } else {
+ Span::empty(offset)
+ };
+
+ let pair = token
+ .parent_ancestors()
+ .find_map(latex::KeyValuePair::cast)?;
+
+ if pair.key()?.to_string() == "cite" {
+ let body = pair.syntax().parent()?;
+ let group = body.parent()?;
+ latex::AcronymDeclaration::cast(group.parent()?)?;
+ Some(span)
+ } else {
+ None
+ }
+}
diff --git a/support/texlab/crates/completion/src/providers/color.rs b/support/texlab/crates/completion/src/providers/color.rs
new file mode 100644
index 0000000000..cbdd38dc32
--- /dev/null
+++ b/support/texlab/crates/completion/src/providers/color.rs
@@ -0,0 +1,116 @@
+use rowan::ast::AstNode;
+use syntax::latex;
+
+use crate::{
+ util::{find_curly_group_word, CompletionBuilder},
+ CompletionItem, CompletionItemData, CompletionParams,
+};
+
+pub fn complete_colors<'a>(
+ params: &'a CompletionParams<'a>,
+ builder: &mut CompletionBuilder<'a>,
+) -> Option<()> {
+ let (cursor, group) = find_curly_group_word(params)?;
+ latex::ColorReference::cast(group.syntax().parent()?)?;
+
+ for name in COLORS {
+ if let Some(score) = builder.matcher.score(&name, &cursor.text) {
+ let data = CompletionItemData::Color(name);
+ builder
+ .items
+ .push(CompletionItem::new_simple(score, cursor.range, data));
+ }
+ }
+
+ Some(())
+}
+
+const COLORS: &[&str] = &[
+ "black",
+ "blue",
+ "brown",
+ "cyan",
+ "darkgray",
+ "gray",
+ "green",
+ "lightgray",
+ "lime",
+ "magenta",
+ "olive",
+ "orange",
+ "pink",
+ "purple",
+ "red",
+ "teal",
+ "violet",
+ "white",
+ "yellow",
+ "Apricot",
+ "Bittersweet",
+ "Blue",
+ "BlueViolet",
+ "Brown",
+ "CadetBlue",
+ "Cerulean",
+ "Cyan",
+ "DarkOrchid",
+ "ForestGreen",
+ "Goldenrod",
+ "Green",
+ "JungleGreen",
+ "LimeGreen",
+ "Mahogany",
+ "Melon",
+ "Mulberry",
+ "OliveGreen",
+ "OrangeRed",
+ "Peach",
+ "PineGreen",
+ "ProcessBlue",
+ "RawSienna",
+ "RedOrange",
+ "Rhodamine",
+ "RoyalPurple",
+ "Salmon",
+ "Sepia",
+ "SpringGreen",
+ "TealBlue",
+ "Turquoise",
+ "VioletRed",
+ "WildStrawberry",
+ "YellowGreen",
+ "Aquamarine",
+ "Black",
+ "BlueGreen",
+ "BrickRed",
+ "BurntOrange",
+ "CarnationPink",
+ "CornflowerBlue",
+ "Dandelion",
+ "Emerald",
+ "Fuchsia",
+ "Gray",
+ "GreenYellow",
+ "Lavender",
+ "Magenta",
+ "Maroon",
+ "MidnightBlue",
+ "NavyBlue",
+ "Orange",
+ "Orchid",
+ "Periwinkle",
+ "Plum",
+ "Purple",
+ "Red",
+ "RedViolet",
+ "RoyalBlue",
+ "RubineRed",
+ "SeaGreen",
+ "SkyBlue",
+ "Tan",
+ "Thistle",
+ "Violet",
+ "White",
+ "Yellow",
+ "YellowOrange",
+];
diff --git a/support/texlab/crates/completion/src/providers/color_model.rs b/support/texlab/crates/completion/src/providers/color_model.rs
new file mode 100644
index 0000000000..4b991c4807
--- /dev/null
+++ b/support/texlab/crates/completion/src/providers/color_model.rs
@@ -0,0 +1,50 @@
+use base_db::semantics::Span;
+use rowan::ast::AstNode;
+use syntax::latex;
+
+use crate::{
+ util::{find_curly_group_word, find_curly_group_word_list, CompletionBuilder},
+ CompletionItem, CompletionItemData, CompletionParams,
+};
+
+pub fn complete_color_models<'a>(
+ params: &'a CompletionParams<'a>,
+ builder: &mut CompletionBuilder<'a>,
+) -> Option<()> {
+ let cursor = check_color_definition(params).or_else(|| check_color_definition_set(params))?;
+
+ for name in MODEL_NAMES {
+ if let Some(score) = builder.matcher.score(&name, &cursor.text) {
+ let data = CompletionItemData::ColorModel(name);
+ builder
+ .items
+ .push(CompletionItem::new_simple(score, cursor.range, data));
+ }
+ }
+
+ Some(())
+}
+
+fn check_color_definition(params: &CompletionParams) -> Option<Span> {
+ let (span, group) = find_curly_group_word(params)?;
+
+ let definition = latex::ColorDefinition::cast(group.syntax().parent()?)?;
+ definition
+ .model()
+ .filter(|model| model.syntax().text_range() == group.syntax().text_range())?;
+
+ Some(span)
+}
+
+fn check_color_definition_set(params: &CompletionParams) -> Option<Span> {
+ let (span, group) = find_curly_group_word_list(params)?;
+
+ let definition = latex::ColorSetDefinition::cast(group.syntax().parent()?)?;
+ definition
+ .model_list()
+ .filter(|model| model.syntax().text_range() == group.syntax().text_range())?;
+
+ Some(span)
+}
+
+const MODEL_NAMES: &[&str] = &["gray", "rgb", "RGB", "HTML", "cmyk"];
diff --git a/support/texlab/crates/completion/src/providers/command.rs b/support/texlab/crates/completion/src/providers/command.rs
new file mode 100644
index 0000000000..ddbf8bc206
--- /dev/null
+++ b/support/texlab/crates/completion/src/providers/command.rs
@@ -0,0 +1,134 @@
+use base_db::{semantics::Span, DocumentData};
+use rowan::{TextRange, TextSize};
+use syntax::{bibtex, latex};
+
+use crate::{
+ util::{included_packages, CompletionBuilder, ProviderContext},
+ CommandData, CompletionItem, CompletionItemData, CompletionParams,
+};
+
+pub fn complete_commands<'a>(
+ params: &'a CompletionParams<'a>,
+ builder: &mut CompletionBuilder<'a>,
+) -> Option<()> {
+ let cursor = find_command_name(params)?;
+
+ let mut proc = Processor(ProviderContext {
+ builder,
+ params,
+ cursor,
+ });
+
+ proc.add_begin_snippet();
+ proc.add_library();
+ proc.add_user();
+ Some(())
+}
+
+struct Processor<'a, 'b>(ProviderContext<'a, 'b>);
+
+impl<'a, 'b> Processor<'a, 'b> {
+ pub fn add_begin_snippet(&mut self) -> Option<()> {
+ let score = self.0.builder.matcher.score("begin", &self.0.cursor.text)?;
+ let data = CompletionItemData::BeginEnvironment;
+ self.0
+ .builder
+ .items
+ .push(CompletionItem::new_simple(score, self.0.cursor.range, data));
+
+ Some(())
+ }
+
+ pub fn add_library(&mut self) -> Option<()> {
+ for package in included_packages(&self.0.params.feature) {
+ let commands_with_score = package.commands.iter().filter_map(|command| {
+ let matcher = &self.0.builder.matcher;
+ let score = matcher.score(&command.name, &self.0.cursor.text)?;
+ Some((command, score))
+ });
+
+ for (command, score) in commands_with_score {
+ let data = CompletionItemData::Command(CommandData {
+ name: &command.name,
+ glyph: command.glyph.as_deref(),
+ image: command.image.as_deref(),
+ package: Some(package),
+ });
+
+ self.0.builder.items.push(CompletionItem::new_simple(
+ score,
+ self.0.cursor.range,
+ data,
+ ));
+ }
+ }
+
+ Some(())
+ }
+
+ fn add_user(&mut self) {
+ let documents = self.0.params.feature.project.documents.iter();
+ for data in documents.filter_map(|document| document.data.as_tex()) {
+ let commands = data
+ .semantics
+ .commands
+ .iter()
+ .filter(|name| name.range != self.0.cursor.range);
+
+ let commands_with_score = commands.filter_map(|command| {
+ let matcher = &self.0.builder.matcher;
+ let score = matcher.score(&command.text, &self.0.cursor.text)?;
+ Some((command, score))
+ });
+
+ for (command, score) in commands_with_score {
+ let data = CompletionItemData::Command(CommandData {
+ name: &command.text,
+ glyph: None,
+ image: None,
+ package: None,
+ });
+
+ self.0.builder.items.push(CompletionItem::new_simple(
+ score,
+ self.0.cursor.range,
+ data,
+ ));
+ }
+ }
+ }
+}
+
+fn find_command_name(params: &CompletionParams) -> Option<Span> {
+ let offset = params.offset;
+ match &params.feature.document.data {
+ DocumentData::Tex(data) => {
+ let root = data.root_node();
+ find_command_name_ast(&root, latex::COMMAND_NAME, offset)
+ }
+ DocumentData::Bib(data) => {
+ let root = data.root_node();
+ find_command_name_ast(&root, bibtex::COMMAND_NAME, offset)
+ .or_else(|| find_command_name_ast(&root, bibtex::ACCENT_NAME, offset))
+ }
+ _ => None,
+ }
+}
+
+fn find_command_name_ast<L: rowan::Language>(
+ root: &rowan::SyntaxNode<L>,
+ kind: L::Kind,
+ offset: TextSize,
+) -> Option<Span> {
+ let token = root
+ .token_at_offset(offset)
+ .filter(|token| token.text_range().start() != offset)
+ .find(|token| token.kind() == kind)?;
+
+ let full_range = token.text_range();
+ let text = String::from(&token.text()[1..]);
+ Some(Span::new(
+ text,
+ TextRange::new(full_range.start() + TextSize::of('\\'), full_range.end()),
+ ))
+}
diff --git a/support/texlab/crates/completion/src/providers/entry_type.rs b/support/texlab/crates/completion/src/providers/entry_type.rs
new file mode 100644
index 0000000000..ce8bffa51e
--- /dev/null
+++ b/support/texlab/crates/completion/src/providers/entry_type.rs
@@ -0,0 +1,40 @@
+use base_db::semantics::Span;
+use syntax::bibtex;
+
+use crate::{
+ util::CompletionBuilder, CompletionItem, CompletionItemData, CompletionParams, EntryTypeData,
+};
+
+pub fn complete_entry_types<'a>(
+ params: &'a CompletionParams<'a>,
+ builder: &mut CompletionBuilder<'a>,
+) -> Option<()> {
+ let cursor = find_entry_type(params)?;
+
+ for entry_type in base_db::data::BIBTEX_ENTRY_TYPES {
+ if let Some(score) = builder.matcher.score(entry_type.name, &cursor.text) {
+ let data = CompletionItemData::EntryType(EntryTypeData(*entry_type));
+ builder
+ .items
+ .push(CompletionItem::new_simple(score, cursor.range, data));
+ }
+ }
+
+ Some(())
+}
+
+fn find_entry_type(params: &CompletionParams) -> Option<Span> {
+ let data = params.feature.document.data.as_bib()?;
+
+ let token = data
+ .root_node()
+ .token_at_offset(params.offset)
+ .find(|token| token.kind() == bibtex::TYPE)?;
+
+ let range = token.text_range();
+ if range.start() == params.offset {
+ None
+ } else {
+ Some(Span::from(&token))
+ }
+}
diff --git a/support/texlab/crates/completion/src/providers/environment.rs b/support/texlab/crates/completion/src/providers/environment.rs
new file mode 100644
index 0000000000..393dd100ba
--- /dev/null
+++ b/support/texlab/crates/completion/src/providers/environment.rs
@@ -0,0 +1,135 @@
+use base_db::semantics::Span;
+use rowan::ast::AstNode;
+use syntax::latex;
+
+use crate::{
+ util::{find_curly_group_word, included_packages, CompletionBuilder, ProviderContext},
+ CompletionItem, CompletionItemData, CompletionParams, EnvironmentData,
+};
+
+pub fn complete_environments<'a>(
+ params: &'a CompletionParams<'a>,
+ builder: &mut CompletionBuilder<'a>,
+) -> Option<()> {
+ let (cursor, group) = find_environment(params)?;
+
+ let begin = group
+ .syntax()
+ .parent()
+ .and_then(|node| node.parent())
+ .and_then(latex::Environment::cast)
+ .and_then(|env| env.begin())
+ .and_then(|begin| begin.name())
+ .and_then(|name| name.key())
+ .map_or_else(|| String::new(), |name| name.to_string());
+
+ let mut proc = Processor {
+ inner: ProviderContext {
+ builder,
+ params,
+ cursor,
+ },
+ begin,
+ };
+
+ proc.add_library();
+ proc.add_theorem();
+ proc.add_user();
+ Some(())
+}
+
+struct Processor<'a, 'b> {
+ inner: ProviderContext<'a, 'b>,
+ begin: String,
+}
+
+impl<'a, 'b> Processor<'a, 'b> {
+ fn add_library(&mut self) {
+ for package in included_packages(&self.inner.params.feature) {
+ let envs_with_score = package.environments.iter().filter_map(|env| {
+ let matcher = &self.inner.builder.matcher;
+ let score = matcher.score(&env, &self.inner.cursor.text)?;
+ Some((*env, score))
+ });
+
+ for (name, score) in envs_with_score {
+ let data = CompletionItemData::Environment(EnvironmentData {
+ name,
+ package: Some(package),
+ });
+
+ self.inner.builder.items.push(CompletionItem {
+ score,
+ data,
+ range: self.inner.cursor.range,
+ preselect: name == self.begin,
+ });
+ }
+ }
+ }
+
+ fn add_theorem(&mut self) {
+ let documents = self.inner.params.feature.project.documents.iter();
+ for theorem in documents
+ .filter_map(|document| document.data.as_tex())
+ .flat_map(|data| data.semantics.theorem_definitions.iter())
+ {
+ let matcher = &self.inner.builder.matcher;
+ let name = theorem.name.text.as_str();
+ if let Some(score) = matcher.score(&name, &self.inner.cursor.text) {
+ let data = CompletionItemData::Environment(EnvironmentData {
+ name,
+ package: None,
+ });
+
+ self.inner.builder.items.push(CompletionItem {
+ score,
+ data,
+ range: self.inner.cursor.range,
+ preselect: name == self.begin,
+ });
+ }
+ }
+ }
+
+ fn add_user(&mut self) {
+ let documents = self.inner.params.feature.project.documents.iter();
+ for data in documents.filter_map(|document| document.data.as_tex()) {
+ let envs = data
+ .semantics
+ .environments
+ .iter()
+ .filter(|name| name.range != self.inner.cursor.range);
+
+ let envs_with_score = envs.filter_map(|env| {
+ let matcher = &self.inner.builder.matcher;
+ let score = matcher.score(&env.text, &self.inner.cursor.text)?;
+ Some((&env.text, score))
+ });
+
+ for (name, score) in envs_with_score {
+ let data = CompletionItemData::Environment(EnvironmentData {
+ name,
+ package: None,
+ });
+
+ self.inner.builder.items.push(CompletionItem {
+ score,
+ data,
+ range: self.inner.cursor.range,
+ preselect: name == &self.begin,
+ });
+ }
+ }
+ }
+}
+
+fn find_environment(params: &CompletionParams) -> Option<(Span, latex::CurlyGroupWord)> {
+ let (span, group) = find_curly_group_word(params)?;
+ let parent = group.syntax().parent()?;
+ if matches!(parent.kind(), latex::BEGIN | latex::END) {
+ Some((span, group))
+ } else {
+ None
+ }
+}
diff --git a/support/texlab/crates/completion/src/providers/field.rs b/support/texlab/crates/completion/src/providers/field.rs
new file mode 100644
index 0000000000..2dcd7fd26b
--- /dev/null
+++ b/support/texlab/crates/completion/src/providers/field.rs
@@ -0,0 +1,66 @@
+use base_db::semantics::Span;
+use rowan::{ast::AstNode, TokenAtOffset};
+use syntax::bibtex::{self, HasName};
+
+use crate::{
+ util::CompletionBuilder, CompletionItem, CompletionItemData, CompletionParams, FieldTypeData,
+};
+
+pub fn complete_fields<'a>(
+ params: &'a CompletionParams<'a>,
+ builder: &mut CompletionBuilder<'a>,
+) -> Option<()> {
+ let cursor = find_field(params)?;
+
+ for field in base_db::data::BIBTEX_FIELD_TYPES {
+ if let Some(score) = builder.matcher.score(field.name, &cursor.text) {
+ let data = CompletionItemData::Field(FieldTypeData(*field));
+ builder
+ .items
+ .push(CompletionItem::new_simple(score, cursor.range, data));
+ }
+ }
+
+ Some(())
+}
+
+fn find_field(params: &CompletionParams) -> Option<Span> {
+ let token = select_token(params)?;
+ if token.kind() == bibtex::TYPE {
+ return None;
+ }
+
+ let parent = token.parent()?;
+ if let Some(entry) = bibtex::Entry::cast(parent.clone()) {
+ if entry.name_token()?.text_range() == token.text_range() {
+ return None;
+ }
+ } else {
+ bibtex::Field::cast(parent)?;
+ }
+
+ Some(if token.kind() == bibtex::NAME {
+ Span::from(&token)
+ } else {
+ Span::empty(params.offset)
+ })
+}
+
+fn select_token(params: &CompletionParams) -> Option<bibtex::SyntaxToken> {
+ let data = params.feature.document.data.as_bib()?;
+ Some(match data.root_node().token_at_offset(params.offset) {
+ TokenAtOffset::Between(_, r) if r.kind() == bibtex::TYPE => r,
+ TokenAtOffset::Between(l, _) if l.kind() == bibtex::TYPE => l,
+ TokenAtOffset::Between(l, _) if l.kind() == bibtex::COMMAND_NAME => l,
+ TokenAtOffset::Between(l, _) if l.kind() == bibtex::ACCENT_NAME => l,
+ TokenAtOffset::Between(_, r) if r.kind() == bibtex::WORD => r,
+ TokenAtOffset::Between(_, r) if r.kind() == bibtex::NAME => r,
+ TokenAtOffset::Between(l, _) if l.kind() == bibtex::WORD => l,
+ TokenAtOffset::Between(l, _) if l.kind() == bibtex::NAME => l,
+ TokenAtOffset::Between(_, r) if r.kind() == bibtex::COMMAND_NAME => r,
+ TokenAtOffset::Between(_, r) if r.kind() == bibtex::ACCENT_NAME => r,
+ TokenAtOffset::Between(_, r) => r,
+ TokenAtOffset::Single(t) => t,
+ TokenAtOffset::None => return None,
+ })
+}
diff --git a/support/texlab/crates/completion/src/providers/glossary.rs b/support/texlab/crates/completion/src/providers/glossary.rs
new file mode 100644
index 0000000000..c22f5b5888
--- /dev/null
+++ b/support/texlab/crates/completion/src/providers/glossary.rs
@@ -0,0 +1,88 @@
+use base_db::semantics::Span;
+use rowan::ast::AstNode;
+use syntax::latex;
+
+use crate::{
+ util::{find_curly_group_word, CompletionBuilder, ProviderContext},
+ CompletionItem, CompletionItemData, CompletionParams, GlossaryEntryData,
+};
+
+pub fn complete_acronyms<'a>(
+ params: &'a CompletionParams<'a>,
+ builder: &mut CompletionBuilder<'a>,
+) -> Option<()> {
+ let cursor = find_acronym(params)?;
+ let mut proc = Processor(ProviderContext {
+ builder,
+ params,
+ cursor,
+ });
+
+ proc.add_acronyms();
+ Some(())
+}
+
+pub fn complete_glossaries<'a>(
+ params: &'a CompletionParams<'a>,
+ builder: &mut CompletionBuilder<'a>,
+) -> Option<()> {
+ let cursor = find_glossary(params)?;
+ let mut proc = Processor(ProviderContext {
+ builder,
+ params,
+ cursor,
+ });
+
+ proc.add_acronyms();
+ proc.add_glossaries();
+ Some(())
+}
+
+struct Processor<'a, 'b>(ProviderContext<'a, 'b>);
+
+impl<'a, 'b> Processor<'a, 'b> {
+ pub fn add_acronyms(&mut self) {
+ self.add_generic(|node| latex::AcronymDefinition::cast(node)?.name());
+ }
+
+ pub fn add_glossaries(&mut self) {
+ self.add_generic(|node| latex::GlossaryEntryDefinition::cast(node)?.name());
+ }
+
+ fn add_generic<F>(&mut self, extract: F)
+ where
+ F: Fn(latex::SyntaxNode) -> Option<latex::CurlyGroupWord>,
+ {
+ let documents = self.0.params.feature.project.documents.iter();
+ for data in documents.filter_map(|document| document.data.as_tex()) {
+ for name in data
+ .root_node()
+ .descendants()
+ .filter_map(|node| extract(node))
+ .filter_map(|name| name.key())
+ .map(|name| name.to_string())
+ {
+ if let Some(score) = self.0.builder.matcher.score(&name, &self.0.cursor.text) {
+ let data = CompletionItemData::GlossaryEntry(GlossaryEntryData { name });
+ self.0.builder.items.push(CompletionItem::new_simple(
+ score,
+ self.0.cursor.range,
+ data,
+ ));
+ }
+ }
+ }
+ }
+}
+
+fn find_acronym(params: &CompletionParams) -> Option<Span> {
+ let (cursor, group) = find_curly_group_word(params)?;
+ latex::AcronymReference::cast(group.syntax().parent()?)?;
+ Some(cursor)
+}
+
+fn find_glossary(params: &CompletionParams) -> Option<Span> {
+ let (cursor, group) = find_curly_group_word(params)?;
+ latex::GlossaryEntryReference::cast(group.syntax().parent()?)?;
+ Some(cursor)
+}
diff --git a/support/texlab/crates/completion/src/providers/import.rs b/support/texlab/crates/completion/src/providers/import.rs
new file mode 100644
index 0000000000..c506026a99
--- /dev/null
+++ b/support/texlab/crates/completion/src/providers/import.rs
@@ -0,0 +1,69 @@
+use rowan::ast::AstNode;
+use rustc_hash::FxHashSet;
+use syntax::latex;
+
+use crate::{
+ util::{find_curly_group_word_list, CompletionBuilder},
+ CompletionItem, CompletionItemData, CompletionParams,
+};
+
+pub fn complete_imports<'a>(
+ params: &'a CompletionParams<'a>,
+ builder: &mut CompletionBuilder<'a>,
+) -> Option<()> {
+ let (cursor, group) = find_curly_group_word_list(params)?;
+
+ let kind = group.syntax().parent()?.kind();
+ let extension = match kind {
+ latex::PACKAGE_INCLUDE => "sty",
+ latex::CLASS_INCLUDE => "cls",
+ _ => return Some(()),
+ };
+
+ let mut file_names = FxHashSet::default();
+ for file_name in completion_data::DATABASE
+ .iter()
+ .flat_map(|package| package.file_names.iter())
+ .filter(|file_name| file_name.ends_with(extension))
+ {
+ file_names.insert(file_name);
+ let stem = &file_name[0..file_name.len() - 4];
+ if kind == latex::PACKAGE_INCLUDE {
+ if let Some(score) = builder.matcher.score(&stem, &cursor.text) {
+ let data = CompletionItemData::Package(stem);
+ builder
+ .items
+ .push(CompletionItem::new_simple(score, cursor.range, data));
+ }
+ } else if let Some(score) = builder.matcher.score(&stem, &cursor.text) {
+ let data = CompletionItemData::DocumentClass(stem);
+ builder
+ .items
+ .push(CompletionItem::new_simple(score, cursor.range, data));
+ }
+ }
+
+ let file_name_db = &params.feature.workspace.distro().file_name_db;
+ for file_name in file_name_db
+ .iter()
+ .map(|(file_name, _)| file_name)
+ .filter(|file_name| file_name.ends_with(extension) && !file_names.contains(file_name))
+ {
+ let stem = &file_name[0..file_name.len() - 4];
+ if kind == latex::PACKAGE_INCLUDE {
+ if let Some(score) = builder.matcher.score(&stem, &cursor.text) {
+ let data = CompletionItemData::Package(stem);
+ builder
+ .items
+ .push(CompletionItem::new_simple(score, cursor.range, data));
+ }
+ } else if let Some(score) = builder.matcher.score(&stem, &cursor.text) {
+ let data = CompletionItemData::DocumentClass(stem);
+ builder
+ .items
+ .push(CompletionItem::new_simple(score, cursor.range, data));
+ }
+ }
+
+ Some(())
+}
diff --git a/support/texlab/crates/completion/src/providers/include.rs b/support/texlab/crates/completion/src/providers/include.rs
new file mode 100644
index 0000000000..2dd204cfdc
--- /dev/null
+++ b/support/texlab/crates/completion/src/providers/include.rs
@@ -0,0 +1,157 @@
+use std::{
+ fs,
+ path::{Path, PathBuf},
+};
+
+use base_db::{DocumentData, FeatureParams};
+use rowan::{ast::AstNode, TextLen, TextRange};
+use syntax::latex;
+
+use crate::{
+ util::{find_curly_group_word_list, CompletionBuilder},
+ CompletionItem, CompletionItemData, CompletionParams,
+};
+
+pub fn complete_includes<'a>(
+ params: &'a CompletionParams<'a>,
+ builder: &mut CompletionBuilder<'a>,
+) -> Option<()> {
+ if params.feature.document.path.is_none() {
+ return None;
+ }
+
+ let (cursor, group) = find_curly_group_word_list(params)?;
+
+ let include = group.syntax().parent()?;
+ let (include_extension, extensions): (bool, &[&str]) = match include.kind() {
+ latex::PACKAGE_INCLUDE => (false, &["sty"]),
+ latex::CLASS_INCLUDE => (false, &["cls"]),
+ latex::LATEX_INCLUDE => {
+ let include = latex::Include::cast(include.clone())?;
+ (
+ matches!(include.command()?.text(), "\\input" | "\\subfile"),
+ &["tex"],
+ )
+ }
+ latex::BIBLATEX_INCLUDE => (true, &["bib"]),
+ latex::BIBTEX_INCLUDE => (false, &["bib"]),
+ latex::GRAPHICS_INCLUDE => (true, &["pdf", "png", "jpg", "jpeg", "bmp"]),
+ latex::SVG_INCLUDE => (true, &["svg"]),
+ latex::INKSCAPE_INCLUDE => (true, &["pdf", "eps", "ps", "png"]),
+ latex::VERBATIM_INCLUDE => (true, &[]),
+ _ => return None,
+ };
+
+ let segment_range = if cursor.text.is_empty() {
+ cursor.range
+ } else {
+ let start = cursor.range.end() - cursor.text.split('/').last()?.text_len();
+ TextRange::new(start, cursor.range.end())
+ };
+
+ let segment_text = &params.feature.document.text[std::ops::Range::from(segment_range)];
+
+ let mut dirs = vec![current_dir(&params.feature, &cursor.text, None)];
+ if include.kind() == latex::GRAPHICS_INCLUDE {
+ for document in &params.feature.project.documents {
+ let DocumentData::Tex(data) = &document.data else {
+ continue;
+ };
+
+ for graphics_path in &data.semantics.graphics_paths {
+ dirs.push(current_dir(
+ &params.feature,
+ &cursor.text,
+ Some(graphics_path),
+ ));
+ }
+ }
+ }
+
+ for entry in dirs
+ .into_iter()
+ .flatten()
+ .filter_map(|dir| fs::read_dir(dir).ok())
+ .flatten()
+ .flatten()
+ {
+ let mut path = entry.path();
+
+ let file_type = entry.file_type().ok()?;
+ if file_type.is_file() && is_included(&path, extensions) {
+ if !include_extension {
+ remove_extension(&mut path);
+ }
+
+ let name = String::from(path.file_name()?.to_str()?);
+ if let Some(score) = builder.matcher.score(&name, segment_text) {
+ builder.items.push(CompletionItem::new_simple(
+ score,
+ cursor.range,
+ CompletionItemData::File(name),
+ ));
+ }
+ } else if file_type.is_dir() {
+ let name = String::from(path.file_name()?.to_str()?);
+ if let Some(score) = builder.matcher.score(&name, segment_text) {
+ builder.items.push(CompletionItem::new_simple(
+ score,
+ cursor.range,
+ CompletionItemData::Directory(name),
+ ));
+ }
+ }
+ }
+
+ Some(())
+}
+
+fn current_dir(
+ params: &FeatureParams,
+ path_text: &str,
+ graphics_path: Option<&str>,
+) -> Option<PathBuf> {
+ let workspace = &params.workspace;
+ let parent = workspace
+ .parents(params.document)
+ .iter()
+ .next()
+ .map_or(params.document, Clone::clone);
+
+ let path = workspace.current_dir(&parent.dir).to_file_path().ok()?;
+
+ let mut path = PathBuf::from(path.to_str()?.replace('\\', "/"));
+ if !path_text.is_empty() {
+ if let Some(graphics_path) = graphics_path {
+ path.push(graphics_path);
+ }
+
+ path.push(path_text);
+ if !path_text.ends_with('/') {
+ path.pop();
+ }
+ }
+
+ Some(path)
+}
+
+fn is_included(file: &Path, allowed_extensions: &[&str]) -> bool {
+ allowed_extensions.is_empty()
+ || file
+ .extension()
+ .and_then(std::ffi::OsStr::to_str)
+ .map(str::to_lowercase)
+ .map(|ext| allowed_extensions.contains(&ext.as_str()))
+ .unwrap_or_default()
+}
+
+fn remove_extension(path: &mut PathBuf) {
+ if let Some(stem) = path
+ .file_stem()
+ .and_then(std::ffi::OsStr::to_str)
+ .map(ToOwned::to_owned)
+ {
+ path.pop();
+ path.push(stem);
+ }
+}
diff --git a/support/texlab/crates/completion/src/providers/label.rs b/support/texlab/crates/completion/src/providers/label.rs
new file mode 100644
index 0000000000..8af2873457
--- /dev/null
+++ b/support/texlab/crates/completion/src/providers/label.rs
@@ -0,0 +1,101 @@
+use base_db::{
+ semantics::{tex::LabelKind, Span},
+ util::{render_label, RenderedObject},
+ DocumentData,
+};
+use rowan::ast::AstNode;
+use syntax::latex;
+
+use crate::{
+ util::{find_curly_group_word, find_curly_group_word_list, CompletionBuilder},
+ CompletionItem, CompletionItemData, CompletionParams,
+};
+
+pub fn complete_labels<'a>(
+ params: &'a CompletionParams<'a>,
+ builder: &mut CompletionBuilder<'a>,
+) -> Option<()> {
+ let FindResult { cursor, is_math } =
+ find_reference(params).or_else(|| find_reference_range(params))?;
+
+ for document in &params.feature.project.documents {
+ let DocumentData::Tex(data) = &document.data else {
+ continue;
+ };
+
+ for label in data
+ .semantics
+ .labels
+ .iter()
+ .filter(|label| label.kind == LabelKind::Definition)
+ {
+ match render_label(params.feature.workspace, &params.feature.project, label) {
+ Some(rendered_label) => {
+ if is_math && !matches!(rendered_label.object, RenderedObject::Equation) {
+ continue;
+ }
+
+ let header = rendered_label.detail();
+ let footer = match &rendered_label.object {
+ RenderedObject::Float { caption, .. } => Some(*caption),
+ _ => None,
+ };
+
+ let keywords = format!("{} {}", label.name.text, rendered_label.reference());
+
+ if let Some(score) = builder.matcher.score(&keywords, &cursor.text) {
+ let data = CompletionItemData::Label(crate::LabelData {
+ name: &label.name.text,
+ header,
+ footer,
+ object: Some(rendered_label.object),
+ keywords,
+ });
+
+ builder
+ .items
+ .push(CompletionItem::new_simple(score, cursor.range, data));
+ }
+ }
+ None => {
+ if let Some(score) = builder.matcher.score(&label.name.text, &cursor.text) {
+ let data = CompletionItemData::Label(crate::LabelData {
+ name: &label.name.text,
+ header: None,
+ footer: None,
+ object: None,
+ keywords: label.name.text.clone(),
+ });
+
+ builder
+ .items
+ .push(CompletionItem::new_simple(score, cursor.range, data));
+ }
+ }
+ }
+ }
+ }
+
+ Some(())
+}
+
+struct FindResult {
+ cursor: Span,
+ is_math: bool,
+}
+
+fn find_reference(params: &CompletionParams) -> Option<FindResult> {
+ let (cursor, group) = find_curly_group_word_list(params)?;
+ let reference = latex::LabelReference::cast(group.syntax().parent()?)?;
+ let is_math = reference.command()?.text() == "\\eqref";
+ Some(FindResult { cursor, is_math })
+}
+
+fn find_reference_range(params: &CompletionParams) -> Option<FindResult> {
+ let (cursor, group) = find_curly_group_word(params)?;
+ latex::LabelReferenceRange::cast(group.syntax().parent()?)?;
+ Some(FindResult {
+ cursor,
+ is_math: false,
+ })
+}
diff --git a/support/texlab/crates/completion/src/providers/tikz_library.rs b/support/texlab/crates/completion/src/providers/tikz_library.rs
new file mode 100644
index 0000000000..10a0aa44ba
--- /dev/null
+++ b/support/texlab/crates/completion/src/providers/tikz_library.rs
@@ -0,0 +1,149 @@
+use rowan::ast::AstNode;
+use syntax::latex;
+
+use crate::{
+ util::{find_curly_group_word_list, CompletionBuilder},
+ CompletionItem, CompletionItemData, CompletionParams,
+};
+
+pub fn complete_tikz_libraries<'a>(
+ params: &'a CompletionParams,
+ builder: &mut CompletionBuilder<'a>,
+) -> Option<()> {
+ let (cursor, group) = find_curly_group_word_list(params)?;
+
+ let import = latex::TikzLibraryImport::cast(group.syntax().parent()?)?;
+
+ let libraries = if import.command()?.text() == "\\usepgflibrary" {
+ PGF_LIBRARIES
+ } else {
+ TIKZ_LIBRARIES
+ };
+
+ for name in libraries {
+ if let Some(score) = builder.matcher.score(&name, &cursor.text) {
+ let data = CompletionItemData::TikzLibrary(name);
+ builder
+ .items
+ .push(CompletionItem::new_simple(score, cursor.range, data));
+ }
+ }
+
+ Some(())
+}
+
+static PGF_LIBRARIES: &[&str] = &[
+ "arrows",
+ "arrows.meta",
+ "arrows.spaced",
+ "curvilinear",
+ "datavisualization.barcharts",
+ "datavisualization.formats.functions",
+ "datavisualization.polar",
+ "decorations.footprints",
+ "decorations.fractals",
+ "decorations.markings",
+ "decorations.pathmorphing",
+ "decorations.pathreplacing",
+ "decorations.shapes",
+ "decorations.text",
+ "fadings",
+ "fixedpointarithmetic",
+ "fpu",
+ "intersections",
+ "lindenmayersystems",
+ "luamath",
+ "patterns",
+ "patterns.meta",
+ "plothandlers",
+ "plotmarks",
+ "profiler",
+ "shadings",
+ "shapes.arrows",
+ "shapes.callouts",
+ "shapes",
+ "shapes.gates.ee",
+ "shapes.gates.ee.IEC",
+ "shapes.gates.logic",
+ "shapes.gates.logic.IEC",
+ "shapes.gates.logic.US",
+ "shapes.geometric",
+ "shapes.misc",
+ "shapes.multipart",
+ "shapes.symbols",
+ "snakes",
+ "svg.path",
+];
+
+static TIKZ_LIBRARIES: &[&str] = &[
+ "3d",
+ "angles",
+ "arrows",
+ "automata",
+ "babel",
+ "backgrounds",
+ "bending",
+ "calc",
+ "calendar",
+ "chains",
+ "circuits",
+ "circuits.ee",
+ "circuits.ee.IEC",
+ "circuits.logic.CDH",
+ "circuits.logic",
+ "circuits.logic.IEC",
+ "circuits.logic.US",
+ "datavisualization.3d",
+ "datavisualization.barcharts",
+ "datavisualization",
+ "datavisualization.formats.functions",
+ "datavisualization.polar",
+ "datavisualization.sparklines",
+ "decorations",
+ "decorations.footprints",
+ "decorations.fractals",
+ "decorations.markings",
+ "decorations.pathmorphing",
+ "decorations.pathreplacing",
+ "decorations.shapes",
+ "decorations.text",
+ "er",
+ "fadings",
+ "fit",
+ "fixedpointarithmetic",
+ "folding",
+ "fpu",
+ "graphs",
+ "graphs.standard",
+ "intersections",
+ "lindenmayersystems",
+ "math",
+ "matrix",
+ "mindmap",
+ "patterns",
+ "patterns.meta",
+ "petri",
+ "plothandlers",
+ "plotmarks",
+ "positioning",
+ "quotes",
+ "scopes",
+ "shadings",
+ "shadows",
+ "shapes.arrows",
+ "shapes.callouts",
+ "shapes",
+ "shapes.gates.logic.IEC",
+ "shapes.gates.logic.US",
+ "shapes.geometric",
+ "shapes.misc",
+ "shapes.multipart",
+ "shapes.symbols",
+ "snakes",
+ "spy",
+ "svg.path",
+ "through",
+ "topaths",
+ "trees",
+ "turtle",
+];