Allow configuring completion matcher algorithm

See #872.
This commit is contained in:
Patrick Förster 2023-04-16 15:19:58 +02:00
parent 617c799dcd
commit dae46eff80
6 changed files with 129 additions and 23 deletions

View file

@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Add experimental `texlab.experimental.citationCommands` setting to allow extending the list of citation commands
([#832](https://github.com/latex-lsp/texlab/issues/832))
- Add support for escaping placeholders in build arguments similar to forward search
- Allow configuring completion matching algorithm ([#872](https://github.com/latex-lsp/texlab/issues/872))
### Fixed

View file

@ -12,6 +12,7 @@ pub struct Config {
pub synctex: Option<SynctexConfig>,
pub symbols: SymbolConfig,
pub syntax: SyntaxConfig,
pub completion: CompletionConfig,
}
#[derive(Debug)]
@ -71,6 +72,19 @@ pub struct SymbolConfig {
pub ignored_patterns: Vec<Regex>,
}
#[derive(Debug)]
pub struct CompletionConfig {
pub matcher: MatchingAlgo,
}
#[derive(Debug)]
pub enum MatchingAlgo {
Skim,
SkimIgnoreCase,
Prefix,
PrefixIgnoreCase,
}
impl Default for Config {
fn default() -> Self {
Self {
@ -81,6 +95,7 @@ impl Default for Config {
synctex: None,
symbols: SymbolConfig::default(),
syntax: SyntaxConfig::default(),
completion: CompletionConfig::default(),
}
}
}
@ -149,3 +164,11 @@ impl Default for SymbolConfig {
}
}
}
impl Default for CompletionConfig {
fn default() -> Self {
Self {
matcher: MatchingAlgo::SkimIgnoreCase,
}
}
}

View file

@ -13,6 +13,7 @@ mod glossary_ref;
mod import;
mod include;
mod label;
mod matcher;
mod theorem;
mod tikz_library;
mod user_command;

View file

@ -1,5 +1,5 @@
use base_db::Document;
use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher};
use base_db::{Document, MatchingAlgo};
use fuzzy_matcher::skim::SkimMatcherV2;
use itertools::Itertools;
use lsp_types::{
ClientCapabilities, ClientInfo, CompletionItem, CompletionItemKind, CompletionList,
@ -23,12 +23,15 @@ use crate::util::{
lsp_enums::Structure,
};
use super::COMPLETION_LIMIT;
use super::{
matcher::{self, Matcher},
COMPLETION_LIMIT,
};
pub struct CompletionBuilder<'a> {
context: &'a CursorContext<'a>,
items: Vec<Item<'a>>,
matcher: SkimMatcherV2,
matcher: Box<dyn Matcher>,
text_pattern: String,
file_pattern: String,
preselect: Option<String>,
@ -45,7 +48,13 @@ impl<'a> CompletionBuilder<'a> {
client_info: Option<&'a ClientInfo>,
) -> Self {
let items = Vec::new();
let matcher = SkimMatcherV2::default().ignore_case();
let matcher: Box<dyn Matcher> = match context.workspace.config().completion.matcher {
MatchingAlgo::Skim => Box::new(SkimMatcherV2::default()),
MatchingAlgo::SkimIgnoreCase => Box::new(SkimMatcherV2::default().ignore_case()),
MatchingAlgo::Prefix => Box::new(matcher::Prefix),
MatchingAlgo::PrefixIgnoreCase => Box::new(matcher::PrefixIgnoreCase),
};
let text_pattern = match &context.cursor {
Cursor::Tex(token) if token.kind() == latex::COMMAND_NAME => {
if token.text_range().start() + TextSize::from(1) == context.offset {
@ -124,7 +133,7 @@ impl<'a> CompletionBuilder<'a> {
}
pub fn glossary_entry(&mut self, range: TextRange, name: String) -> Option<()> {
let score = self.matcher.fuzzy_match(&name, &self.text_pattern)?;
let score = self.matcher.score(&name, &self.text_pattern)?;
self.items.push(Item {
range,
data: Data::GlossaryEntry { name },
@ -141,7 +150,7 @@ impl<'a> CompletionBuilder<'a> {
name: &'a str,
image: Option<&'a str>,
) -> Option<()> {
let score = self.matcher.fuzzy_match(name, &self.text_pattern)?;
let score = self.matcher.score(name, &self.text_pattern)?;
self.items.push(Item {
range,
data: Data::Argument { name, image },
@ -154,7 +163,7 @@ impl<'a> CompletionBuilder<'a> {
pub fn begin_snippet(&mut self, range: TextRange) -> Option<()> {
if self.snippets {
let score = self.matcher.fuzzy_match("begin", &self.text_pattern[1..])?;
let score = self.matcher.score("begin", &self.text_pattern[1..])?;
self.items.push(Item {
range,
data: Data::BeginSnippet,
@ -187,7 +196,7 @@ impl<'a> CompletionBuilder<'a> {
.trim(),
);
let score = self.matcher.fuzzy_match(&filter_text, &self.text_pattern)?;
let score = self.matcher.score(&filter_text, &self.text_pattern)?;
let data = Data::Citation {
document,
@ -207,7 +216,7 @@ impl<'a> CompletionBuilder<'a> {
}
pub fn color_model(&mut self, range: TextRange, name: &'a str) -> Option<()> {
let score = self.matcher.fuzzy_match(name, &self.text_pattern)?;
let score = self.matcher.score(name, &self.text_pattern)?;
self.items.push(Item {
range,
data: Data::ColorModel { name },
@ -219,7 +228,7 @@ impl<'a> CompletionBuilder<'a> {
}
pub fn color(&mut self, range: TextRange, name: &'a str) -> Option<()> {
let score = self.matcher.fuzzy_match(name, &self.text_pattern)?;
let score = self.matcher.score(name, &self.text_pattern)?;
self.items.push(Item {
range,
data: Data::Color { name },
@ -238,7 +247,7 @@ impl<'a> CompletionBuilder<'a> {
glyph: Option<&'a str>,
file_names: &'a [SmolStr],
) -> Option<()> {
let score = self.matcher.fuzzy_match(name, &self.text_pattern[1..])?;
let score = self.matcher.score(name, &self.text_pattern[1..])?;
let data = Data::ComponentCommand {
name,
image,
@ -262,7 +271,7 @@ impl<'a> CompletionBuilder<'a> {
name: &'a str,
file_names: &'a [SmolStr],
) -> Option<()> {
let score = self.matcher.fuzzy_match(name, &self.text_pattern)?;
let score = self.matcher.score(name, &self.text_pattern)?;
self.items.push(Item {
range,
data: Data::ComponentEnvironment { name, file_names },
@ -280,7 +289,7 @@ impl<'a> CompletionBuilder<'a> {
) -> Option<()> {
let score = self
.matcher
.fuzzy_match(&entry_type.name, &self.text_pattern[1..])?;
.score(&entry_type.name, &self.text_pattern[1..])?;
self.items.push(Item {
range,
@ -293,7 +302,7 @@ impl<'a> CompletionBuilder<'a> {
}
pub fn field(&mut self, range: TextRange, field: &'a BibtexFieldDoc) -> Option<()> {
let score = self.matcher.fuzzy_match(&field.name, &self.text_pattern)?;
let score = self.matcher.score(&field.name, &self.text_pattern)?;
self.items.push(Item {
range,
data: Data::Field { field },
@ -305,7 +314,7 @@ impl<'a> CompletionBuilder<'a> {
}
pub fn class(&mut self, range: TextRange, name: &'a str) -> Option<()> {
let score = self.matcher.fuzzy_match(name, &self.text_pattern)?;
let score = self.matcher.score(name, &self.text_pattern)?;
self.items.push(Item {
range,
data: Data::Class { name },
@ -317,7 +326,7 @@ impl<'a> CompletionBuilder<'a> {
}
pub fn package(&mut self, range: TextRange, name: &'a str) -> Option<()> {
let score = self.matcher.fuzzy_match(name, &self.text_pattern)?;
let score = self.matcher.score(name, &self.text_pattern)?;
self.items.push(Item {
range,
data: Data::Package { name },
@ -329,7 +338,7 @@ impl<'a> CompletionBuilder<'a> {
}
pub fn file(&mut self, range: TextRange, name: String) -> Option<()> {
let score = self.matcher.fuzzy_match(&name, &self.file_pattern)?;
let score = self.matcher.score(&name, &self.file_pattern)?;
self.items.push(Item {
range,
data: Data::File { name },
@ -341,7 +350,7 @@ impl<'a> CompletionBuilder<'a> {
}
pub fn directory(&mut self, range: TextRange, name: String) -> Option<()> {
let score = self.matcher.fuzzy_match(&name, &self.file_pattern)?;
let score = self.matcher.score(&name, &self.file_pattern)?;
self.items.push(Item {
range,
data: Data::Directory { name },
@ -361,7 +370,7 @@ impl<'a> CompletionBuilder<'a> {
footer: Option<&'a str>,
text: String,
) -> Option<()> {
let score = self.matcher.fuzzy_match(&text, &self.text_pattern)?;
let score = self.matcher.score(&text, &self.text_pattern)?;
self.items.push(Item {
range,
data: Data::Label {
@ -379,7 +388,7 @@ impl<'a> CompletionBuilder<'a> {
}
pub fn tikz_library(&mut self, range: TextRange, name: &'a str) -> Option<()> {
let score = self.matcher.fuzzy_match(name, &self.text_pattern)?;
let score = self.matcher.score(name, &self.text_pattern)?;
self.items.push(Item {
range,
data: Data::TikzLibrary { name },
@ -391,7 +400,7 @@ impl<'a> CompletionBuilder<'a> {
}
pub fn user_command(&mut self, range: TextRange, name: &'a str) -> Option<()> {
let score = self.matcher.fuzzy_match(name, &self.text_pattern[1..])?;
let score = self.matcher.score(name, &self.text_pattern[1..])?;
self.items.push(Item {
range,
data: Data::UserCommand { name },
@ -403,7 +412,7 @@ impl<'a> CompletionBuilder<'a> {
}
pub fn user_environment(&mut self, range: TextRange, name: &'a str) -> Option<()> {
let score = self.matcher.fuzzy_match(name, &self.text_pattern)?;
let score = self.matcher.score(name, &self.text_pattern)?;
self.items.push(Item {
range,
data: Data::UserEnvironment { name },

View file

@ -0,0 +1,42 @@
pub trait Matcher {
fn score(&mut self, choice: &str, pattern: &str) -> Option<i32>;
}
impl<T: fuzzy_matcher::FuzzyMatcher> Matcher for T {
fn score(&mut self, choice: &str, pattern: &str) -> Option<i32> {
fuzzy_matcher::FuzzyMatcher::fuzzy_match(self, choice, pattern)
}
}
#[derive(Debug)]
pub struct Prefix;
impl Matcher for Prefix {
fn score(&mut self, choice: &str, pattern: &str) -> Option<i32> {
if choice.starts_with(pattern) {
Some(-(choice.len() as i32))
} else {
None
}
}
}
#[derive(Debug)]
pub struct PrefixIgnoreCase;
impl Matcher for PrefixIgnoreCase {
fn score(&mut self, choice: &str, pattern: &str) -> Option<i32> {
if pattern.len() > choice.len() {
return None;
}
let mut cs = choice.chars();
for p in pattern.chars() {
if !cs.next().unwrap().eq_ignore_ascii_case(&p) {
return None;
}
}
return Some(-(choice.len() as i32));
}
}

View file

@ -20,6 +20,7 @@ pub struct Options {
pub symbols: SymbolOptions,
pub latexindent: LatexindentOptions,
pub forward_search: ForwardSearchOptions,
pub completion: CompletionOptions,
pub experimental: ExperimentalOptions,
}
@ -121,6 +122,28 @@ pub struct StartupOptions {
pub skip_distro: bool,
}
#[derive(Debug, PartialEq, Eq, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(default)]
pub struct CompletionOptions {
pub matcher: CompletionMatcher,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum CompletionMatcher {
Fuzzy,
FuzzyIgnoreCase,
Prefix,
PrefixIgnoreCase,
}
impl Default for CompletionMatcher {
fn default() -> Self {
Self::FuzzyIgnoreCase
}
}
impl From<Options> for Config {
fn from(value: Options) -> Self {
let mut config = Config::default();
@ -194,6 +217,13 @@ impl From<Options> for Config {
.map(|pattern| pattern.0)
.collect();
config.completion.matcher = match value.completion.matcher {
CompletionMatcher::Fuzzy => base_db::MatchingAlgo::Skim,
CompletionMatcher::FuzzyIgnoreCase => base_db::MatchingAlgo::SkimIgnoreCase,
CompletionMatcher::Prefix => base_db::MatchingAlgo::Prefix,
CompletionMatcher::PrefixIgnoreCase => base_db::MatchingAlgo::PrefixIgnoreCase,
};
config
.syntax
.math_environments