diff options
Diffstat (limited to 'support/texlab/crates/completion/src/providers')
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(¶ms.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 ¶m.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 ¶ms.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 ¶ms.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 = ¶ms.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 = ¶ms.feature.document.text[std::ops::Range::from(segment_range)]; + + let mut dirs = vec![current_dir(¶ms.feature, &cursor.text, None)]; + if include.kind() == latex::GRAPHICS_INCLUDE { + for document in ¶ms.feature.project.documents { + let DocumentData::Tex(data) = &document.data else { + continue; + }; + + for graphics_path in &data.semantics.graphics_paths { + dirs.push(current_dir( + ¶ms.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 = ¶ms.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 ¶ms.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, ¶ms.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", +]; |