Move completion module to a separate crate

This commit is contained in:
Patrick Förster 2019-12-12 10:46:49 +01:00
parent 59a1fb69a7
commit ee26f16101
29 changed files with 71 additions and 49 deletions

View file

@ -0,0 +1,22 @@
[package]
name = "texlab-completion"
version = "0.1.0"
authors = [
"Eric Förster <efoerster@users.noreply.github.com>",
"Patrick Förster <pfoerster@users.noreply.github.com>"]
edition = "2018"
[dependencies]
futures = "0.3"
futures-boxed = { path = "../futures_boxed" }
itertools = "0.8.2"
log = "0.4.6"
once_cell = "1.2.0"
regex = "1.3.1"
serde = { version = "1.0.103", features = ["derive", "rc"] }
serde_json = "1.0.44"
texlab-distro = { path = "../texlab_distro" }
texlab-protocol = { path = "../texlab_protocol" }
texlab-syntax = { path = "../texlab_syntax" }
texlab-workspace = { path = "../texlab_workspace" }
walkdir = "2"

View file

@ -0,0 +1,109 @@
use crate::factory::{self, LatexComponentId};
use futures_boxed::boxed;
use texlab_protocol::*;
use texlab_syntax::*;
use texlab_workspace::*;
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub struct BibtexCommandCompletionProvider;
impl FeatureProvider for BibtexCommandCompletionProvider {
type Params = CompletionParams;
type Output = Vec<CompletionItem>;
#[boxed]
async fn execute<'a>(&'a self, request: &'a FeatureRequest<Self::Params>) -> Self::Output {
let mut items = Vec::new();
if let SyntaxTree::Bibtex(tree) = &request.document().tree {
let position = request.params.text_document_position.position;
if let Some(BibtexNode::Command(command)) = tree.find(position).last() {
if command.token.range().contains(position)
&& command.token.start().character != position.character
{
let mut range = command.range();
range.start.character += 1;
let component = LatexComponentId::kernel();
for command in &COMPLETION_DATABASE.kernel().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,
&component,
);
items.push(item);
}
}
}
}
items
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_inside_command() {
let items = test_feature(
BibtexCommandCompletionProvider,
FeatureSpec {
files: vec![FeatureSpec::file("foo.bib", "@article{foo, bar=\n\\}")],
main_file: "foo.bib",
position: Position::new(1, 1),
..FeatureSpec::default()
},
);
assert!(!items.is_empty());
assert_eq!(
items[0].text_edit.as_ref().map(|edit| edit.range),
Some(Range::new_simple(1, 1, 1, 2))
);
}
#[test]
fn test_start_of_command() {
let items = test_feature(
BibtexCommandCompletionProvider,
FeatureSpec {
files: vec![FeatureSpec::file("foo.bib", "@article{foo, bar=\n\\}")],
main_file: "foo.bib",
position: Position::new(1, 0),
..FeatureSpec::default()
},
);
assert!(items.is_empty());
}
#[test]
fn test_inside_text() {
let items = test_feature(
BibtexCommandCompletionProvider,
FeatureSpec {
files: vec![FeatureSpec::file("foo.bib", "@article{foo, bar=\n}")],
main_file: "foo.bib",
position: Position::new(1, 0),
..FeatureSpec::default()
},
);
assert!(items.is_empty());
}
#[test]
fn test_latex() {
let items = test_feature(
BibtexCommandCompletionProvider,
FeatureSpec {
files: vec![FeatureSpec::file("foo.tex", "\\")],
main_file: "foo.tex",
position: Position::new(0, 1),
..FeatureSpec::default()
},
);
assert!(items.is_empty());
}
}

View file

@ -0,0 +1,153 @@
use crate::factory;
use texlab_workspace::*;
use futures_boxed::boxed;
use texlab_protocol::*;
use texlab_syntax::*;
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub struct BibtexEntryTypeCompletionProvider;
impl FeatureProvider for BibtexEntryTypeCompletionProvider {
type Params = CompletionParams;
type Output = Vec<CompletionItem>;
#[boxed]
async fn execute<'a>(&'a self, request: &'a FeatureRequest<Self::Params>) -> Self::Output {
if let SyntaxTree::Bibtex(tree) = &request.document().tree {
let position = request.params.text_document_position.position;
for declaration in &tree.root.children {
match declaration {
BibtexDeclaration::Preamble(preamble) => {
if contains(&preamble.ty, position) {
return make_items(request, preamble.ty.range());
}
}
BibtexDeclaration::String(string) => {
if contains(&string.ty, position) {
return make_items(request, string.ty.range());
}
}
BibtexDeclaration::Entry(entry) => {
if contains(&entry.ty, position) {
return make_items(request, entry.ty.range());
}
}
BibtexDeclaration::Comment(_) => {}
}
}
}
Vec::new()
}
}
fn contains(ty: &BibtexToken, position: Position) -> bool {
ty.range().contains(position) && ty.start().character != position.character
}
fn make_items(request: &FeatureRequest<CompletionParams>, mut range: Range) -> Vec<CompletionItem> {
range.start.character += 1;
let mut items = Vec::new();
for ty in &LANGUAGE_DATA.entry_types {
let text_edit = TextEdit::new(range, (&ty.name).into());
let item = factory::entry_type(request, ty, text_edit);
items.push(item);
}
items
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_before_at_sign() {
let items = test_feature(
BibtexEntryTypeCompletionProvider,
FeatureSpec {
files: vec![FeatureSpec::file("foo.bib", "@")],
main_file: "foo.bib",
position: Position::new(0, 0),
..FeatureSpec::default()
},
);
assert!(items.is_empty());
}
#[test]
fn test_after_at_sign() {
let items = test_feature(
BibtexEntryTypeCompletionProvider,
FeatureSpec {
files: vec![FeatureSpec::file("foo.bib", "@")],
main_file: "foo.bib",
position: Position::new(0, 1),
..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, 1))
);
}
#[test]
fn test_inside_entry_type() {
let items = test_feature(
BibtexEntryTypeCompletionProvider,
FeatureSpec {
files: vec![FeatureSpec::file("foo.bib", "@foo")],
main_file: "foo.bib",
position: Position::new(0, 2),
..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_inside_entry_key() {
let items = test_feature(
BibtexEntryTypeCompletionProvider,
FeatureSpec {
files: vec![FeatureSpec::file("foo.bib", "@article{foo,}")],
main_file: "foo.bib",
position: Position::new(0, 11),
..FeatureSpec::default()
},
);
assert!(items.is_empty());
}
#[test]
fn test_inside_comments() {
let items = test_feature(
BibtexEntryTypeCompletionProvider,
FeatureSpec {
files: vec![FeatureSpec::file("foo.bib", "foo")],
main_file: "foo.bib",
position: Position::new(0, 2),
..FeatureSpec::default()
},
);
assert!(items.is_empty());
}
#[test]
fn test_latex() {
let items = test_feature(
BibtexEntryTypeCompletionProvider,
FeatureSpec {
files: vec![FeatureSpec::file("foo.tex", "@")],
main_file: "foo.tex",
position: Position::new(0, 1),
..FeatureSpec::default()
},
);
assert!(items.is_empty());
}
}

View file

@ -0,0 +1,174 @@
use crate::factory;
use futures_boxed::boxed;
use texlab_protocol::*;
use texlab_syntax::*;
use texlab_workspace::*;
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub struct BibtexFieldNameCompletionProvider;
impl FeatureProvider for BibtexFieldNameCompletionProvider {
type Params = CompletionParams;
type Output = Vec<CompletionItem>;
#[boxed]
async fn execute<'a>(&'a self, request: &'a FeatureRequest<Self::Params>) -> Self::Output {
if let SyntaxTree::Bibtex(tree) = &request.document().tree {
let position = request.params.text_document_position.position;
match tree.find(position).last() {
Some(BibtexNode::Field(field)) => {
if field.name.range().contains(position) {
return make_items(request, field.name.range());
}
}
Some(BibtexNode::Entry(entry)) => {
if !entry.is_comment() && !entry.ty.range().contains(position) {
let edit_range = Range::new(position, position);
if let Some(key) = &entry.key {
if !key.range().contains(position) {
return make_items(request, edit_range);
}
} else {
return make_items(request, edit_range);
}
}
}
_ => {}
}
}
Vec::new()
}
}
fn make_items(
request: &FeatureRequest<CompletionParams>,
edit_range: Range,
) -> Vec<CompletionItem> {
let mut items = Vec::new();
for field in &LANGUAGE_DATA.fields {
let text_edit = TextEdit::new(edit_range, (&field.name).into());
let item = factory::field_name(request, field, text_edit);
items.push(item);
}
items
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_inside_first_field() {
let items = test_feature(
BibtexFieldNameCompletionProvider,
FeatureSpec {
files: vec![FeatureSpec::file("foo.bib", "@article{foo,\nbar}")],
main_file: "foo.bib",
position: Position::new(1, 1),
..FeatureSpec::default()
},
);
assert!(!items.is_empty());
assert_eq!(
items[0].text_edit.as_ref().map(|edit| edit.range),
Some(Range::new_simple(1, 0, 1, 3))
);
}
#[test]
fn test_inside_second_field() {
let items = test_feature(
BibtexFieldNameCompletionProvider,
FeatureSpec {
files: vec![FeatureSpec::file(
"foo.bib",
"@article{foo, bar = {baz}, qux}",
)],
main_file: "foo.bib",
position: Position::new(0, 27),
..FeatureSpec::default()
},
);
assert!(!items.is_empty());
assert_eq!(
items[0].text_edit.as_ref().map(|edit| edit.range),
Some(Range::new_simple(0, 27, 0, 30))
);
}
#[test]
fn test_inside_entry() {
let items = test_feature(
BibtexFieldNameCompletionProvider,
FeatureSpec {
files: vec![FeatureSpec::file("foo.bib", "@article{foo, \n}")],
main_file: "foo.bib",
position: Position::new(1, 0),
..FeatureSpec::default()
},
);
assert!(!items.is_empty());
assert_eq!(
items[0].text_edit.as_ref().map(|edit| edit.range),
Some(Range::new_simple(1, 0, 1, 0))
);
}
#[test]
fn test_inside_content() {
let items = test_feature(
BibtexFieldNameCompletionProvider,
FeatureSpec {
files: vec![FeatureSpec::file("foo.bib", "@article{foo,\nbar = {baz}}")],
main_file: "foo.bib",
position: Position::new(1, 7),
..FeatureSpec::default()
},
);
assert!(items.is_empty());
}
#[test]
fn test_inside_entry_type() {
let items = test_feature(
BibtexFieldNameCompletionProvider,
FeatureSpec {
files: vec![FeatureSpec::file("foo.bib", "@article{foo,}")],
main_file: "foo.bib",
position: Position::new(0, 3),
..FeatureSpec::default()
},
);
assert!(items.is_empty());
}
// TODO: Improve behavior of this provider
//
// #[test]
// fn test_after_equals_sign() {
// let items = test_feature(
// BibtexFieldNameCompletionProvider,
// FeatureSpec {
// files: vec![FeatureSpec::file("foo.bib", "@article{foo, bar = \n}")],
// main_file: "foo.bib",
// position: Position::new(1, 0),
// ..FeatureSpec::default()
// },
// );
// assert!(items.is_empty());
// }
#[test]
fn test_inside_latex() {
let items = test_feature(
BibtexFieldNameCompletionProvider,
FeatureSpec {
files: vec![FeatureSpec::file("foo.tex", "@article{foo,}")],
main_file: "foo.tex",
position: Position::new(0, 3),
..FeatureSpec::default()
},
);
assert!(items.is_empty());
}
}

View file

@ -0,0 +1,3 @@
pub mod command;
pub mod entry_type;
pub mod field_name;

View file

@ -0,0 +1,437 @@
use once_cell::sync::Lazy;
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::path::Path;
use texlab_protocol::*;
use texlab_syntax::*;
use texlab_workspace::*;
static WHITESPACE_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new("\\s+").unwrap());
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum CompletionItemData {
Command,
CommandSnippet,
Environment,
Label,
Folder,
File,
PgfLibrary,
TikzLibrary,
Color,
ColorModel,
Package,
Class,
EntryType,
FieldName,
Citation { uri: Uri, key: String },
Argument,
GlossaryEntry,
}
impl Into<serde_json::Value> for CompletionItemData {
fn into(self) -> serde_json::Value {
serde_json::to_value(self).unwrap()
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum LatexComponentId<'a> {
User,
Component(Vec<&'a str>),
}
impl<'a> LatexComponentId<'a> {
pub fn kernel() -> Self {
LatexComponentId::Component(vec![])
}
pub fn detail(&self) -> String {
match self {
LatexComponentId::User => "user-defined".to_owned(),
LatexComponentId::Component(files) => {
if files.is_empty() {
"built-in".to_owned()
} else {
files.join(", ")
}
}
}
}
}
fn supports_images(request: &FeatureRequest<CompletionParams>) -> bool {
request
.client_capabilities
.text_document
.as_ref()
.and_then(|cap| cap.completion.as_ref())
.and_then(|cap| cap.completion_item.as_ref())
.and_then(|cap| cap.documentation_format.as_ref())
.map_or(true, |formats| formats.contains(&MarkupKind::Markdown))
}
pub fn command(
request: &FeatureRequest<CompletionParams>,
name: String,
image: Option<&str>,
glyph: Option<&str>,
text_edit: TextEdit,
component: &LatexComponentId,
) -> CompletionItem {
let detail = glyph.map_or_else(
|| component.detail(),
|glyph| format!("{}, {}", glyph, component.detail()),
);
CompletionItem {
kind: Some(adjust_kind(request, Structure::Command.completion_kind())),
data: Some(CompletionItemData::Command.into()),
documentation: image.and_then(|image| image_documentation(&request, &name, image)),
text_edit: Some(text_edit),
..CompletionItem::new_simple(name, detail)
}
}
pub fn command_snippet(
request: &FeatureRequest<CompletionParams>,
name: &'static str,
image: Option<&str>,
template: &'static str,
component: &LatexComponentId,
) -> CompletionItem {
CompletionItem {
kind: Some(adjust_kind(request, Structure::Snippet.completion_kind())),
data: Some(CompletionItemData::CommandSnippet.into()),
documentation: image.and_then(|image| image_documentation(&request, &name, image)),
insert_text: Some(template.into()),
insert_text_format: Some(InsertTextFormat::Snippet),
..CompletionItem::new_simple(name.into(), component.detail())
}
}
pub fn environment(
request: &FeatureRequest<CompletionParams>,
name: String,
text_edit: TextEdit,
component: &LatexComponentId,
) -> CompletionItem {
CompletionItem {
kind: Some(adjust_kind(
request,
Structure::Environment.completion_kind(),
)),
data: Some(CompletionItemData::Environment.into()),
text_edit: Some(text_edit),
..CompletionItem::new_simple(name, component.detail())
}
}
pub fn label(
request: &FeatureRequest<CompletionParams>,
name: String,
text_edit: TextEdit,
context: Option<&OutlineContext>,
) -> CompletionItem {
let kind = match context.as_ref().map(|ctx| &ctx.item) {
Some(OutlineContextItem::Section { .. }) => Structure::Section.completion_kind(),
Some(OutlineContextItem::Caption { .. }) => Structure::Float.completion_kind(),
Some(OutlineContextItem::Theorem { .. }) => Structure::Theorem.completion_kind(),
Some(OutlineContextItem::Equation) => Structure::Equation.completion_kind(),
Some(OutlineContextItem::Item) => Structure::Item.completion_kind(),
None => Structure::Label.completion_kind(),
};
let detail = context.as_ref().and_then(|ctx| ctx.detail());
let filter_text = context
.as_ref()
.map(|ctx| format!("{} {}", name, ctx.reference()));
let documentation = context
.and_then(|ctx| match &ctx.item {
OutlineContextItem::Caption { text, .. } => Some(text.clone()),
_ => None,
})
.map(Documentation::String);
CompletionItem {
label: name,
kind: Some(adjust_kind(request, kind)),
data: Some(CompletionItemData::Label.into()),
text_edit: Some(text_edit),
filter_text,
detail,
documentation,
..CompletionItem::default()
}
}
pub fn folder(
request: &FeatureRequest<CompletionParams>,
path: &Path,
text_edit: TextEdit,
) -> CompletionItem {
CompletionItem {
label: path.file_name().unwrap().to_string_lossy().into_owned(),
kind: Some(adjust_kind(request, Structure::Folder.completion_kind())),
data: Some(CompletionItemData::Folder.into()),
text_edit: Some(text_edit),
..CompletionItem::default()
}
}
pub fn file(
request: &FeatureRequest<CompletionParams>,
path: &Path,
text_edit: TextEdit,
) -> CompletionItem {
CompletionItem {
label: path.file_name().unwrap().to_string_lossy().into_owned(),
kind: Some(adjust_kind(request, Structure::File.completion_kind())),
data: Some(CompletionItemData::File.into()),
text_edit: Some(text_edit),
..CompletionItem::default()
}
}
pub fn pgf_library(
request: &FeatureRequest<CompletionParams>,
name: &'static str,
text_edit: TextEdit,
) -> CompletionItem {
CompletionItem {
label: name.into(),
kind: Some(adjust_kind(
request,
Structure::PgfLibrary.completion_kind(),
)),
data: Some(CompletionItemData::PgfLibrary.into()),
text_edit: Some(text_edit),
..CompletionItem::default()
}
}
pub fn tikz_library(
request: &FeatureRequest<CompletionParams>,
name: &'static str,
text_edit: TextEdit,
) -> CompletionItem {
CompletionItem {
label: name.into(),
kind: Some(adjust_kind(
request,
Structure::TikzLibrary.completion_kind(),
)),
data: Some(CompletionItemData::TikzLibrary.into()),
text_edit: Some(text_edit),
..CompletionItem::default()
}
}
pub fn color(
request: &FeatureRequest<CompletionParams>,
name: &'static str,
text_edit: TextEdit,
) -> CompletionItem {
CompletionItem {
label: name.into(),
kind: Some(adjust_kind(request, Structure::Color.completion_kind())),
data: Some(CompletionItemData::Color.into()),
text_edit: Some(text_edit),
..CompletionItem::default()
}
}
pub fn color_model(
request: &FeatureRequest<CompletionParams>,
name: &'static str,
text_edit: TextEdit,
) -> CompletionItem {
CompletionItem {
label: name.into(),
kind: Some(adjust_kind(
request,
Structure::ColorModel.completion_kind(),
)),
data: Some(CompletionItemData::ColorModel.into()),
text_edit: Some(text_edit),
..CompletionItem::default()
}
}
pub fn package(
request: &FeatureRequest<CompletionParams>,
name: String,
text_edit: TextEdit,
) -> CompletionItem {
CompletionItem {
label: name,
kind: Some(adjust_kind(request, Structure::Package.completion_kind())),
data: Some(CompletionItemData::Package.into()),
text_edit: Some(text_edit),
..CompletionItem::default()
}
}
pub fn class(
request: &FeatureRequest<CompletionParams>,
name: String,
text_edit: TextEdit,
) -> CompletionItem {
CompletionItem {
label: name,
kind: Some(adjust_kind(request, Structure::Class.completion_kind())),
data: Some(CompletionItemData::Class.into()),
text_edit: Some(text_edit),
..CompletionItem::default()
}
}
pub fn citation(
request: &FeatureRequest<CompletionParams>,
uri: Uri,
entry: &BibtexEntry,
key: String,
text_edit: TextEdit,
) -> CompletionItem {
let params = BibtexFormattingParams::default();
let entry_code = format_entry(&entry, &params);
let filter_text = format!(
"{} {}",
&key,
WHITESPACE_REGEX
.replace_all(
&entry_code
.replace('{', "")
.replace('}', "")
.replace(',', " ")
.replace('=', " "),
" ",
)
.trim()
);
let kind = LANGUAGE_DATA
.find_entry_type(&entry.ty.text()[1..])
.map(|ty| Structure::Entry(ty.category).completion_kind())
.unwrap_or_else(|| Structure::Entry(BibtexEntryTypeCategory::Misc).completion_kind());
CompletionItem {
label: key.to_owned(),
kind: Some(adjust_kind(request, kind)),
filter_text: Some(filter_text),
data: Some(CompletionItemData::Citation { uri, key }.into()),
text_edit: Some(text_edit),
..CompletionItem::default()
}
}
pub fn entry_type(
request: &FeatureRequest<CompletionParams>,
ty: &BibtexEntryTypeDoc,
text_edit: TextEdit,
) -> CompletionItem {
let kind = Structure::Entry(ty.category).completion_kind();
CompletionItem {
label: (&ty.name).into(),
kind: Some(adjust_kind(request, kind)),
data: Some(CompletionItemData::EntryType.into()),
text_edit: Some(text_edit),
documentation: ty.documentation.as_ref().map(|doc| {
Documentation::MarkupContent(MarkupContent {
kind: MarkupKind::Markdown,
value: doc.into(),
})
}),
..CompletionItem::default()
}
}
pub fn field_name(
request: &FeatureRequest<CompletionParams>,
field: &'static BibtexFieldDoc,
text_edit: TextEdit,
) -> CompletionItem {
CompletionItem {
label: (&field.name).into(),
kind: Some(adjust_kind(request, Structure::Field.completion_kind())),
data: Some(CompletionItemData::FieldName.into()),
text_edit: Some(text_edit),
documentation: Some(Documentation::MarkupContent(MarkupContent {
kind: MarkupKind::Markdown,
value: (&field.documentation).into(),
})),
..CompletionItem::default()
}
}
pub fn argument(
request: &FeatureRequest<CompletionParams>,
name: &'static str,
text_edit: TextEdit,
image: Option<&str>,
) -> CompletionItem {
CompletionItem {
label: name.into(),
kind: Some(adjust_kind(request, Structure::Argument.completion_kind())),
data: Some(CompletionItemData::Argument.into()),
text_edit: Some(text_edit),
documentation: image.and_then(|image| image_documentation(&request, &name, image)),
..CompletionItem::default()
}
}
pub fn glossary_entry(
request: &FeatureRequest<CompletionParams>,
label: String,
text_edit: TextEdit,
) -> CompletionItem {
CompletionItem {
label,
kind: Some(adjust_kind(
request,
Structure::GlossaryEntry.completion_kind(),
)),
data: Some(CompletionItemData::GlossaryEntry.into()),
text_edit: Some(text_edit),
..CompletionItem::default()
}
}
fn image_documentation(
request: &FeatureRequest<CompletionParams>,
name: &str,
image: &str,
) -> Option<Documentation> {
if supports_images(request) {
Some(Documentation::MarkupContent(MarkupContent {
kind: MarkupKind::Markdown,
value: format!(
"![{}](data:image/png;base64,{}|width=48,height=48)",
name, image
),
}))
} else {
None
}
}
fn adjust_kind(
request: &FeatureRequest<CompletionParams>,
kind: CompletionItemKind,
) -> CompletionItemKind {
if let Some(value_set) = request
.client_capabilities
.text_document
.as_ref()
.and_then(|cap| cap.completion.as_ref())
.and_then(|cap| cap.completion_item_kind.as_ref())
.and_then(|cap| cap.value_set.as_ref())
{
if value_set.contains(&kind) {
return kind;
}
}
CompletionItemKind::Text
}

View file

@ -0,0 +1,114 @@
use super::combinators::{self, Parameter};
use crate::factory;
use futures_boxed::boxed;
use std::iter;
use texlab_protocol::*;
use texlab_workspace::*;
#[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 COMPLETION_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::*;
#[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());
}
}

View file

@ -0,0 +1,30 @@
use super::combinators;
use crate::factory::{self, LatexComponentId};
use texlab_workspace::*;
use futures_boxed::boxed;
use texlab_protocol::{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
}
}

View file

@ -0,0 +1,142 @@
use super::combinators::{self, Parameter};
use crate::factory;
use texlab_workspace::*;
use futures_boxed::boxed;
use texlab_protocol::*;
use texlab_syntax::*;
#[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::*;
#[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());
}
}

View file

@ -0,0 +1,72 @@
use super::combinators::{self, Parameter};
use crate::factory;
use texlab_workspace::*;
use futures_boxed::boxed;
use texlab_protocol::*;
use texlab_syntax::*;
#[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::*;
#[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());
}
}

View file

@ -0,0 +1,92 @@
use super::combinators::{self, Parameter};
use crate::factory;
use texlab_workspace::*;
use futures_boxed::boxed;
use texlab_protocol::*;
use texlab_syntax::LANGUAGE_DATA;
#[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::*;
#[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))
);
}
}

View file

@ -0,0 +1,151 @@
use texlab_workspace::*;
use std::future::Future;
use std::sync::Arc;
use texlab_protocol::*;
use texlab_syntax::*;
#[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
}

View file

@ -0,0 +1,276 @@
use super::combinators;
use crate::factory::{self, LatexComponentId};
use futures_boxed::boxed;
use texlab_protocol::*;
use texlab_workspace::*;
#[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 COMPLETION_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 COMPLETION_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::*;
#[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());
}
}

View file

@ -0,0 +1,53 @@
use super::combinators::{self, Parameter};
use crate::factory;
use texlab_workspace::*;
use futures_boxed::boxed;
use texlab_protocol::*;
use texlab_syntax::LatexGlossaryEntryKind::*;
use texlab_syntax::*;
#[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
}
}

View file

@ -0,0 +1,109 @@
use super::combinators::{self, Parameter};
use crate::factory;
use texlab_workspace::*;
use futures_boxed::boxed;
use texlab_protocol::*;
use texlab_syntax::*;
#[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>, String, 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 resolver = request.distribution.resolver().await;
COMPLETION_DATABASE
.components
.iter()
.flat_map(|comp| comp.file_names.iter())
.chain(resolver.files_by_name.keys())
.filter(|file_name| file_name.ends_with(extension))
.map(|file_name| {
let stem = &file_name[0..file_name.len() - 4];
let text_edit = TextEdit::new(context.range, stem.to_owned());
factory(request, stem.into(), text_edit)
})
.collect()
}
})
.await
}
#[cfg(test)]
mod tests {
use super::*;
#[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"));
}
}

View file

@ -0,0 +1,128 @@
use super::combinators::{self, Parameter};
use crate::factory;
use texlab_workspace::*;
use futures_boxed::boxed;
use std::path::{Path, PathBuf};
use texlab_protocol::*;
use texlab_syntax::*;
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)
}

View file

@ -0,0 +1,154 @@
use super::combinators::{self, ArgumentContext, Parameter};
use crate::factory;
use texlab_workspace::*;
use futures_boxed::boxed;
use std::sync::Arc;
use texlab_protocol::*;
use texlab_syntax::*;
#[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::*;
#[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"]);
}
}

View file

@ -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;

View file

@ -0,0 +1,67 @@
use super::combinators;
use crate::factory::{self, LatexComponentId};
use texlab_workspace::*;
use futures_boxed::boxed;
use texlab_protocol::*;
use texlab_syntax::*;
#[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::*;
#[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, "theorem");
assert_eq!(
items[0].text_edit.as_ref().map(|edit| edit.range),
Some(Range::new_simple(1, 7, 1, 9))
);
}
}

View file

@ -0,0 +1,89 @@
use super::combinators::{self, Parameter};
use crate::factory;
use texlab_workspace::*;
use futures_boxed::boxed;
use texlab_protocol::*;
use texlab_syntax::LANGUAGE_DATA;
#[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::*;
#[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());
}
}

View file

@ -0,0 +1,155 @@
use super::combinators;
use crate::factory::{self, LatexComponentId};
use texlab_workspace::*;
use futures_boxed::boxed;
use itertools::Itertools;
use texlab_protocol::*;
use texlab_syntax::*;
#[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::*;
#[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"]);
}
}

View file

@ -0,0 +1,111 @@
mod bibtex;
mod factory;
mod latex;
mod preselect;
mod quality;
pub use self::factory::CompletionItemData;
use self::bibtex::command::BibtexCommandCompletionProvider;
use self::bibtex::entry_type::BibtexEntryTypeCompletionProvider;
use self::bibtex::field_name::BibtexFieldNameCompletionProvider;
use self::latex::argument::LatexArgumentCompletionProvider;
use self::latex::begin_command::LatexBeginCommandCompletionProvider;
use self::latex::citation::LatexCitationCompletionProvider;
use self::latex::color::LatexColorCompletionProvider;
use self::latex::color_model::LatexColorModelCompletionProvider;
use self::latex::component::*;
use self::latex::glossary::LatexGlossaryCompletionProvider;
use self::latex::import::{LatexClassImportProvider, LatexPackageImportProvider};
use self::latex::include::LatexIncludeCompletionProvider;
use self::latex::label::LatexLabelCompletionProvider;
use self::latex::theorem::LatexTheoremEnvironmentCompletionProvider;
use self::latex::tikz::*;
use self::latex::user::*;
use self::preselect::PreselectCompletionProvider;
use self::quality::OrderByQualityCompletionProvider;
use texlab_workspace::*;
use futures_boxed::boxed;
use itertools::Itertools;
use std::hash::{Hash, Hasher};
use texlab_protocol::{CompletionItem, CompletionParams};
pub const COMPLETION_LIMIT: usize = 50;
type MergeProvider = ConcatProvider<CompletionParams, CompletionItem>;
pub struct CompletionProvider {
provider: OrderByQualityCompletionProvider<PreselectCompletionProvider<MergeProvider>>,
}
impl CompletionProvider {
pub fn new() -> Self {
Self {
provider: OrderByQualityCompletionProvider::new(PreselectCompletionProvider::new(
ConcatProvider::new(vec![
Box::new(BibtexEntryTypeCompletionProvider),
Box::new(BibtexFieldNameCompletionProvider),
Box::new(BibtexCommandCompletionProvider),
Box::new(LatexPgfLibraryCompletionProvider),
Box::new(LatexTikzLibraryCompletionProvider),
Box::new(LatexColorCompletionProvider),
Box::new(LatexColorModelCompletionProvider),
Box::new(LatexArgumentCompletionProvider),
Box::new(LatexComponentEnvironmentCompletionProvider),
Box::new(LatexTheoremEnvironmentCompletionProvider),
Box::new(LatexLabelCompletionProvider),
Box::new(LatexCitationCompletionProvider),
Box::new(LatexGlossaryCompletionProvider),
Box::new(LatexIncludeCompletionProvider),
Box::new(LatexClassImportProvider),
Box::new(LatexPackageImportProvider),
Box::new(LatexBeginCommandCompletionProvider),
Box::new(LatexComponentCommandCompletionProvider),
Box::new(LatexUserCommandCompletionProvider),
Box::new(LatexUserEnvironmentCompletionProvider),
]),
)),
}
}
}
impl Default for CompletionProvider {
fn default() -> Self {
Self::new()
}
}
impl FeatureProvider for CompletionProvider {
type Params = CompletionParams;
type Output = Vec<CompletionItem>;
#[boxed]
async fn execute<'a>(&'a self, request: &'a FeatureRequest<Self::Params>) -> Self::Output {
self.provider
.execute(request)
.await
.into_iter()
.map(LabeledCompletionItem)
.unique()
.map(|item| item.0)
.take(COMPLETION_LIMIT)
.collect()
}
}
#[derive(Debug, Clone)]
struct LabeledCompletionItem(CompletionItem);
impl PartialEq for LabeledCompletionItem {
fn eq(&self, other: &Self) -> bool {
self.0.label == other.0.label
}
}
impl Eq for LabeledCompletionItem {}
impl Hash for LabeledCompletionItem {
fn hash<H: Hasher>(&self, state: &mut H) {
self.0.label.hash(state);
}
}

View file

@ -0,0 +1,55 @@
use texlab_workspace::*;
use futures_boxed::boxed;
use texlab_protocol::RangeExt;
use texlab_protocol::{CompletionItem, CompletionParams};
use texlab_syntax::*;
#[derive(Debug)]
pub struct PreselectCompletionProvider<F> {
provider: F,
}
impl<F> PreselectCompletionProvider<F> {
pub fn new(provider: F) -> Self {
Self { provider }
}
}
impl<F> FeatureProvider for PreselectCompletionProvider<F>
where
F: FeatureProvider<Params = CompletionParams, Output = Vec<CompletionItem>> + Send + Sync,
{
type Params = CompletionParams;
type Output = Vec<CompletionItem>;
#[boxed]
async fn execute<'a>(&'a self, request: &'a FeatureRequest<Self::Params>) -> Self::Output {
let mut items = self.provider.execute(request).await;
if let SyntaxTree::Latex(tree) = &request.document().tree {
for environment in &tree.env.environments {
if let Some(name) = environment.left.name() {
let right_args = &environment.right.command.args[0];
let cond1 = right_args
.range()
.contains_exclusive(request.params.text_document_position.position);
let cond2 = right_args.right.is_none()
&& right_args
.range()
.contains(request.params.text_document_position.position);
if cond1 || cond2 {
for item in &mut items {
item.preselect = Some(false);
if item.label == name.text() {
item.preselect = Some(true);
break;
}
}
}
}
}
}
items
}
}

View file

@ -0,0 +1,125 @@
use texlab_workspace::*;
use futures_boxed::boxed;
use std::borrow::Cow;
use texlab_protocol::RangeExt;
use texlab_protocol::{CompletionItem, CompletionParams, Position};
use texlab_syntax::*;
pub struct OrderByQualityCompletionProvider<F> {
pub provider: F,
}
impl<F> OrderByQualityCompletionProvider<F> {
pub fn new(provider: F) -> Self {
Self { provider }
}
}
impl<F> FeatureProvider for OrderByQualityCompletionProvider<F>
where
F: FeatureProvider<Params = CompletionParams, Output = Vec<CompletionItem>> + Send + Sync,
{
type Params = CompletionParams;
type Output = Vec<CompletionItem>;
#[boxed]
async fn execute<'a>(&'a self, request: &'a FeatureRequest<Self::Params>) -> Self::Output {
let query = Self::get_query(
request.document(),
request.params.text_document_position.position,
);
let mut items = self.provider.execute(&request).await;
items.sort_by_key(|item| -Self::get_quality(&query, &item));
items
}
}
impl<F> OrderByQualityCompletionProvider<F> {
fn get_query(document: &Document, position: Position) -> Option<Cow<str>> {
match &document.tree {
SyntaxTree::Latex(tree) => {
let node = tree
.find_command_by_name(position)
.map(LatexNode::Command)
.or_else(|| tree.find(position).into_iter().last())?;
match node {
LatexNode::Root(_) | LatexNode::Group(_) => Some("".into()),
LatexNode::Command(command) => Some(command.name.text()[1..].to_owned().into()),
LatexNode::Text(text) => text
.words
.iter()
.find(|word| word.range().contains(position))
.map(|word| word.text().to_owned().into()),
LatexNode::Comma(_) => Some(",".into()),
LatexNode::Math(math) => Some(math.token.text().to_owned().into()),
}
}
SyntaxTree::Bibtex(tree) => {
fn get_type_query(ty: &BibtexToken, position: Position) -> Option<Cow<str>> {
if ty.range().contains(position) {
Some((&ty.text()[1..]).into())
} else {
Some("".into())
}
}
match tree.find(position).pop()? {
BibtexNode::Root(_) => Some("".into()),
BibtexNode::Preamble(preamble) => get_type_query(&preamble.ty, position),
BibtexNode::String(string) => get_type_query(&string.ty, position),
BibtexNode::Entry(entry) => get_type_query(&entry.ty, position),
BibtexNode::Comment(comment) => Some(comment.token.text().into()),
BibtexNode::Field(field) => {
if field.name.range().contains(position) {
Some(field.name.text().into())
} else {
Some("".into())
}
}
BibtexNode::Word(word) => Some(word.token.text().into()),
BibtexNode::Command(command) => Some((&command.token.text()[1..]).into()),
BibtexNode::QuotedContent(_)
| BibtexNode::BracedContent(_)
| BibtexNode::Concat(_) => Some("".into()),
}
}
}
}
fn get_quality(query: &Option<Cow<str>>, item: &CompletionItem) -> i32 {
if item.preselect == Some(true) {
return 8;
}
let label = &item.label;
if let Some(query) = query {
if label == query {
return 7;
}
if label.to_lowercase() == query.to_lowercase() {
return 6;
}
if label.starts_with(query.as_ref()) {
return 5;
}
if label.to_lowercase().starts_with(&query.to_lowercase()) {
return 4;
}
if label.contains(query.as_ref()) {
return 3;
}
if label.to_lowercase().contains(&query.to_lowercase()) {
return 2;
}
1
} else {
0
}
}
}

View file

@ -250,6 +250,7 @@ pub fn format_content(content: &BibtexContent, params: &BibtexFormattingParams)
#[cfg(test)]
mod tests {
use super::*;
use crate::bibtex::BibtexSyntaxTree;
use indoc::indoc;
fn verify(source: &str, expected: &str, line_length: i32) {