diff --git a/Cargo.lock b/Cargo.lock index 42e8d9c8..4ceebc44 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -352,6 +352,15 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fuzzy-matcher" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" +dependencies = [ + "thread_local", +] + [[package]] name = "getrandom" version = "0.2.15" @@ -518,6 +527,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + [[package]] name = "option-ext" version = "0.2.0" @@ -870,6 +885,16 @@ dependencies = [ "syn", ] +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -1006,6 +1031,7 @@ dependencies = [ "clap", "env_logger", "fnv", + "fuzzy-matcher", "log", "lsp-server", "lsp-types", diff --git a/vhdl_ls/Cargo.toml b/vhdl_ls/Cargo.toml index 9b1967b3..a464e5da 100644 --- a/vhdl_ls/Cargo.toml +++ b/vhdl_ls/Cargo.toml @@ -24,6 +24,7 @@ log = "0" env_logger = "0" clap = { version = "4", features = ["derive"] } lsp-server = "0" +fuzzy-matcher = "0.3.7" [dev-dependencies] tempfile = "3" diff --git a/vhdl_ls/src/vhdl_server.rs b/vhdl_ls/src/vhdl_server.rs index f8922f12..6b6655bc 100644 --- a/vhdl_ls/src/vhdl_server.rs +++ b/vhdl_ls/src/vhdl_server.rs @@ -17,6 +17,7 @@ use std::collections::hash_map::Entry; use vhdl_lang::ast::ObjectClass; use crate::rpc_channel::SharedRpcChannel; +use fuzzy_matcher::skim::SkimMatcherV2; use std::io; use std::io::ErrorKind; use std::path::{Path, PathBuf}; @@ -64,6 +65,7 @@ pub struct VHDLServer { init_params: Option, config_file: Option, severity_map: SeverityMap, + string_matcher: SkimMatcherV2, } impl VHDLServer { @@ -77,6 +79,7 @@ impl VHDLServer { init_params: None, config_file: None, severity_map: SeverityMap::default(), + string_matcher: SkimMatcherV2::default().use_cache(true).ignore_case(), } } @@ -91,6 +94,7 @@ impl VHDLServer { init_params: None, config_file: None, severity_map: SeverityMap::default(), + string_matcher: SkimMatcherV2::default(), } } diff --git a/vhdl_ls/src/vhdl_server/workspace.rs b/vhdl_ls/src/vhdl_server/workspace.rs index 88989cec..b17da42e 100644 --- a/vhdl_ls/src/vhdl_server/workspace.rs +++ b/vhdl_ls/src/vhdl_server/workspace.rs @@ -1,10 +1,13 @@ use crate::vhdl_server::{srcpos_to_location, to_symbol_kind, uri_to_file_name, VHDLServer}; +use fuzzy_matcher::FuzzyMatcher; use lsp_types::{ DidChangeWatchedFilesParams, OneOf, WorkspaceSymbol, WorkspaceSymbolParams, WorkspaceSymbolResponse, }; +use std::cmp::Ordering; +use std::collections::BinaryHeap; use vhdl_lang::ast::Designator; -use vhdl_lang::Message; +use vhdl_lang::{EntRef, Message}; impl VHDLServer { pub fn workspace_did_change_watched_files(&mut self, params: &DidChangeWatchedFilesParams) { @@ -32,39 +35,77 @@ impl VHDLServer { params: &WorkspaceSymbolParams, ) -> Option { let trunc_limit = 200; - let query = params.query.to_ascii_lowercase(); - let mut symbols: Vec<_> = self + let query = params.query.clone(); + let symbols = self .project .public_symbols() .filter_map(|ent| match ent.designator() { Designator::Identifier(_) | Designator::Character(_) => { - Some((ent, ent.designator().to_string().to_ascii_lowercase())) + Some((ent, ent.designator().to_string())) } - Designator::OperatorSymbol(op) => Some((ent, op.to_string().to_ascii_lowercase())), + Designator::OperatorSymbol(op) => Some((ent, op.to_string())), Designator::Anonymous(_) => None, - }) - .collect(); - symbols.sort_by(|(_, n1), (_, n2)| n1.cmp(n2)); + }); + Some(WorkspaceSymbolResponse::Nested( - symbols - .into_iter() - .filter_map(|(ent, name)| { - let decl_pos = ent.decl_pos()?; - if name.starts_with(&query) { - Some(WorkspaceSymbol { + self.filter_workspace_symbols(symbols.into_iter(), &query, trunc_limit), + )) + } + + /// Filters found workspace symbols according to a given query. + /// This uses a fuzzy matcher internally to improve the results. + /// Queries 'close' to the target string will score high and be included in the + /// returned vec, while queries 'not close' to the target string will be omitted. + /// The returned vec is sorted according to the score of the fuzzy matcher. + fn filter_workspace_symbols<'a>( + &self, + symbols: impl Iterator, String)>, + query: &str, + trunc_limit: usize, + ) -> Vec { + #[derive(Eq, PartialEq)] + struct WorkspaceSymbolWithScore { + symbol: WorkspaceSymbol, + score: i64, + } + + impl PartialOrd for WorkspaceSymbolWithScore { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } + } + + impl Ord for WorkspaceSymbolWithScore { + fn cmp(&self, other: &Self) -> Ordering { + self.score.cmp(&other.score) + } + } + + let symbols_with_scores: BinaryHeap<_> = symbols + .into_iter() + .filter_map(|(ent, name)| { + let decl_pos = ent.decl_pos()?; + self.string_matcher.fuzzy_match(&name, query).map(|score| { + WorkspaceSymbolWithScore { + symbol: WorkspaceSymbol { name: ent.describe(), kind: to_symbol_kind(ent.kind()), tags: None, container_name: ent.parent.map(|ent| ent.path_name()), location: OneOf::Left(srcpos_to_location(decl_pos)), data: None, - }) - } else { - None + }, + score, } }) - .take(trunc_limit) - .collect(), - )) + }) + .take(trunc_limit) + .collect(); + symbols_with_scores + .into_sorted_vec() + .into_iter() + .rev() + .map(|wsws| wsws.symbol) + .collect() } }