mirror of
https://github.com/latex-lsp/texlab.git
synced 2025-08-04 10:49:55 +00:00
Move completion module to a separate crate
This commit is contained in:
parent
59a1fb69a7
commit
ee26f16101
29 changed files with 71 additions and 49 deletions
22
crates/texlab_completion/Cargo.toml
Normal file
22
crates/texlab_completion/Cargo.toml
Normal 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"
|
109
crates/texlab_completion/src/bibtex/command.rs
Normal file
109
crates/texlab_completion/src/bibtex/command.rs
Normal 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());
|
||||
}
|
||||
}
|
153
crates/texlab_completion/src/bibtex/entry_type.rs
Normal file
153
crates/texlab_completion/src/bibtex/entry_type.rs
Normal 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());
|
||||
}
|
||||
}
|
174
crates/texlab_completion/src/bibtex/field_name.rs
Normal file
174
crates/texlab_completion/src/bibtex/field_name.rs
Normal 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());
|
||||
}
|
||||
}
|
3
crates/texlab_completion/src/bibtex/mod.rs
Normal file
3
crates/texlab_completion/src/bibtex/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
pub mod command;
|
||||
pub mod entry_type;
|
||||
pub mod field_name;
|
437
crates/texlab_completion/src/factory.rs
Normal file
437
crates/texlab_completion/src/factory.rs
Normal 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, ¶ms);
|
||||
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!(
|
||||
"",
|
||||
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
|
||||
}
|
114
crates/texlab_completion/src/latex/argument.rs
Normal file
114
crates/texlab_completion/src/latex/argument.rs
Normal 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 ¶meter.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());
|
||||
}
|
||||
}
|
30
crates/texlab_completion/src/latex/begin_command.rs
Normal file
30
crates/texlab_completion/src/latex/begin_command.rs
Normal 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
|
||||
}
|
||||
}
|
142
crates/texlab_completion/src/latex/citation.rs
Normal file
142
crates/texlab_completion/src/latex/citation.rs
Normal 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());
|
||||
}
|
||||
}
|
72
crates/texlab_completion/src/latex/color.rs
Normal file
72
crates/texlab_completion/src/latex/color.rs
Normal 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());
|
||||
}
|
||||
}
|
92
crates/texlab_completion/src/latex/color_model.rs
Normal file
92
crates/texlab_completion/src/latex/color_model.rs
Normal 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))
|
||||
);
|
||||
}
|
||||
}
|
151
crates/texlab_completion/src/latex/combinators.rs
Normal file
151
crates/texlab_completion/src/latex/combinators.rs
Normal 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
|
||||
}
|
276
crates/texlab_completion/src/latex/component.rs
Normal file
276
crates/texlab_completion/src/latex/component.rs
Normal 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());
|
||||
}
|
||||
}
|
53
crates/texlab_completion/src/latex/glossary.rs
Normal file
53
crates/texlab_completion/src/latex/glossary.rs
Normal 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
|
||||
}
|
||||
}
|
109
crates/texlab_completion/src/latex/import.rs
Normal file
109
crates/texlab_completion/src/latex/import.rs
Normal 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"));
|
||||
}
|
||||
}
|
128
crates/texlab_completion/src/latex/include.rs
Normal file
128
crates/texlab_completion/src/latex/include.rs
Normal 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)
|
||||
}
|
154
crates/texlab_completion/src/latex/label.rs
Normal file
154
crates/texlab_completion/src/latex/label.rs
Normal 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"]);
|
||||
}
|
||||
}
|
14
crates/texlab_completion/src/latex/mod.rs
Normal file
14
crates/texlab_completion/src/latex/mod.rs
Normal 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;
|
67
crates/texlab_completion/src/latex/theorem.rs
Normal file
67
crates/texlab_completion/src/latex/theorem.rs
Normal 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))
|
||||
);
|
||||
}
|
||||
}
|
89
crates/texlab_completion/src/latex/tikz.rs
Normal file
89
crates/texlab_completion/src/latex/tikz.rs
Normal 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());
|
||||
}
|
||||
}
|
155
crates/texlab_completion/src/latex/user.rs
Normal file
155
crates/texlab_completion/src/latex/user.rs
Normal 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"]);
|
||||
}
|
||||
}
|
111
crates/texlab_completion/src/lib.rs
Normal file
111
crates/texlab_completion/src/lib.rs
Normal 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);
|
||||
}
|
||||
}
|
55
crates/texlab_completion/src/preselect.rs
Normal file
55
crates/texlab_completion/src/preselect.rs
Normal 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
|
||||
}
|
||||
}
|
125
crates/texlab_completion/src/quality.rs
Normal file
125
crates/texlab_completion/src/quality.rs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue