diff --git a/Cargo.lock b/Cargo.lock index c6952590..33ca2bf4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -163,8 +163,12 @@ name = "completions" version = "0.1.0" dependencies = [ "do-notation", + "moxide_config", + "nucleo-matcher", "rayon", + "regex", "tower-lsp", + "vault", ] [[package]] @@ -638,6 +642,7 @@ dependencies = [ "fuzzydate", "indexmap", "itertools", + "moxide_config", "nanoid", "nucleo-matcher", "once_cell", @@ -651,6 +656,7 @@ dependencies = [ "shellexpand", "tokio", "tower-lsp", + "vault", "walkdir", ] @@ -686,6 +692,28 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "moxide_config" +version = "0.1.0" +dependencies = [ + "anyhow", + "config", + "do-notation", + "indexmap", + "itertools", + "once_cell", + "pathdiff", + "rayon", + "regex", + "ropey", + "serde", + "serde_json", + "serde_yaml", + "shellexpand", + "tower-lsp", + "walkdir", +] + [[package]] name = "nanoid" version = "0.4.0" @@ -1461,6 +1489,24 @@ dependencies = [ "serde", ] +[[package]] +name = "vault" +version = "0.1.0" +dependencies = [ + "do-notation", + "itertools", + "moxide_config", + "once_cell", + "pathdiff", + "rayon", + "regex", + "ropey", + "serde", + "serde_yaml", + "tower-lsp", + "walkdir", +] + [[package]] name = "version_check" version = "0.9.4" diff --git a/Cargo.toml b/Cargo.toml index 9a04f27d..a3e38509 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,3 +31,5 @@ walkdir = "2.4.0" # local crates completions.path = "crates/completions" +moxide_config.path = "crates/moxide_config" +vault.path = "crates/vault" diff --git a/TestFiles/Test.md b/TestFiles/Test.md index 245b1d23..a632e02f 100644 --- a/TestFiles/Test.md +++ b/TestFiles/Test.md @@ -1,4 +1,7 @@ + +[[]] + #tag # Heading 1 diff --git a/crates/completions/Cargo.toml b/crates/completions/Cargo.toml index baf63f67..03fe1504 100644 --- a/crates/completions/Cargo.toml +++ b/crates/completions/Cargo.toml @@ -9,3 +9,5 @@ do-notation.workspace = true tower-lsp.workspace = true vault.workspace = true moxide_config.workspace = true +regex.workspace = true +nucleo-matcher.workspace = true diff --git a/crates/completions/src/completers/mod.rs b/crates/completions/src/completers/mod.rs deleted file mode 100644 index 90a57c8c..00000000 --- a/crates/completions/src/completers/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ - -pub(crate) mod referencer; - -// mod actions -// mod syntax - diff --git a/crates/completions/src/completers/referencer/mod.rs b/crates/completions/src/completers/referencer/mod.rs deleted file mode 100644 index 027b94e4..00000000 --- a/crates/completions/src/completers/referencer/mod.rs +++ /dev/null @@ -1,44 +0,0 @@ -mod named_entity_completion; - -use rayon::prelude::*; - -use crate::{completions::{Completer, Completion}, context::{Context, Query}}; - - -pub(crate) struct Referencer; - -impl<'a> Completer for Referencer { - fn completions(&self, cx: &Context, location: &crate::completions::Location) -> Option>> { - - let completion_info = cx.parser().link_completion_info(location)?; - let query = completion_info.query(); - - match &query.query { - Query::Named(named) => { - - let items = cx.querier().named_grep_query(named); - let items = items - .map(|named_entity| Box::new(named_entity) as Box) - .take(cx.settings().max_query_completion_items()) - .collect::>(); - - - Some(items) - - // map to a Completion - - }, - Query::Block(..) => None - // Query::Block(block) => { - // - // self.querier.unnamed_grep_query(&block) - // .into_par_iter() - // .map(|block| Box::new(block) as Box) - // - // } - } - } -} - - - diff --git a/crates/completions/src/completers/referencer/named_entity_completion.rs b/crates/completions/src/completers/referencer/named_entity_completion.rs deleted file mode 100644 index 58c7ac32..00000000 --- a/crates/completions/src/completers/referencer/named_entity_completion.rs +++ /dev/null @@ -1,228 +0,0 @@ -use crate::{completions::Completion, context::{Context, EntityWithBacklinksPreview, FileEntityType, LinkingStyle, NamedEntity, NamedEntityInfo, UnresolvedFileEntityType}}; - - -/// UI implementation for named entities - -impl Completion for NamedEntity { - fn label(&self, cx: &Context) -> String { - - match self.info() { - NamedEntityInfo::File { path, entity_type } => { - - let file_ref = path.file_name().unwrap().to_str().expect("file name to be a valid string"); - - match entity_type { - FileEntityType::Normal => { - if cx.settings().file_as_first_heading() { - cx.querier().first_heading_of_file(&path).unwrap_or(file_ref).to_string() - } else { - file_ref.to_string() - } - } - FileEntityType::DailyNote { relative_label } => { - relative_label.to_string() - } - FileEntityType::Alias { alias } => { - alias.to_string() - } - } - - - } - NamedEntityInfo::Heading { heading, file } => { - - let filename = file.file_name().unwrap().to_str().expect("file name to be a valid string"); - - // TODO: implement the matched text for long headings - format!("{filename}#{heading}") - - } - NamedEntityInfo::Block { index, file } => { - - let file_name = file.file_name().unwrap().to_str().expect("file name to be a valid string"); - format!("{file_name}#{index}") - - } - - NamedEntityInfo::UnresovledFile { file_ref, entity_type } => { - match entity_type { - UnresolvedFileEntityType::Normal => { - file_ref.to_string() - } - UnresolvedFileEntityType::DailyNote { relative_label } => { - relative_label.to_string() - } - } - } - - NamedEntityInfo::UnresolvedHeading { file_ref, heading } => { - format!("{file_ref}#{heading}") - } - } - - // TODO: relative path and absolute path - - } - - fn label_detail(&self, cx: &Context) -> Option { - match self.info() { - NamedEntityInfo::UnresolvedHeading {..} | NamedEntityInfo::UnresovledFile {..} => Some("Unresolved".to_string()), - NamedEntityInfo::File {path, entity_type: FileEntityType::Alias {..} } => { - let file_name = path.file_name().unwrap().to_str().expect("file name to be a valid string"); - - Some(format!("Alias for {file_name}")) - } - _ => None - } - } - - fn kind(&self, cx: &Context) -> tower_lsp::lsp_types::CompletionItemKind { - match self.info() { - NamedEntityInfo::File { entity_type, .. } => { - match entity_type { - FileEntityType::Normal => tower_lsp::lsp_types::CompletionItemKind::FILE, - FileEntityType::DailyNote { .. } => tower_lsp::lsp_types::CompletionItemKind::ENUM_MEMBER, - FileEntityType::Alias { .. } => tower_lsp::lsp_types::CompletionItemKind::ENUM_MEMBER - } - } - NamedEntityInfo::Heading { .. } => tower_lsp::lsp_types::CompletionItemKind::REFERENCE, - NamedEntityInfo::Block { .. } => tower_lsp::lsp_types::CompletionItemKind::REFERENCE, - NamedEntityInfo::UnresovledFile { .. } => tower_lsp::lsp_types::CompletionItemKind::ENUM_MEMBER, - NamedEntityInfo::UnresolvedHeading { .. } => tower_lsp::lsp_types::CompletionItemKind::ENUM_MEMBER - } - } - - fn detail(&self, cx: &Context) -> Option { - None - } - - fn documentation(&self, cx: &Context) -> Option { - let EntityWithBacklinksPreview { entity_preview, backlinks } = cx.entity_view().preview_with_backlinks(self); - - let backlinks_string = backlinks.take(cx.settings().backlinks_to_preview()) - .map(|(_, text)| text) - .collect::>().join("\n"); - - let backlinks_string = if backlinks_string.is_empty() { - "".to_string() - } else { - format!("\n---\n{}", backlinks_string) - }; - - Some(format!("{entity_preview}{backlinks_string}")) - } - - fn deprecated(&self, cx: &Context) -> Option { - None - } - - fn preselect(&self, cx: &Context) -> Option { - - let entered_query = cx.parser().entered_query_string(); - - Some(match self.info() { - NamedEntityInfo::File { path, entity_type: FileEntityType::DailyNote { relative_label } } => { - entered_query == relative_label - } - NamedEntityInfo::File { path, entity_type: FileEntityType::Normal } if cx.settings().file_as_first_heading() => { - let first_heading = cx.querier().first_heading_of_file(&path)?; - - entered_query == first_heading - } - NamedEntityInfo::File { path, entity_type: FileEntityType::Normal } => { - - let name = path.file_name().unwrap().to_str().expect("file name to be a valid string"); - - entered_query == name - } - NamedEntityInfo::File { path, entity_type: FileEntityType::Alias { alias } } => { - entered_query == alias - } - NamedEntityInfo::UnresovledFile { file_ref, entity_type: UnresolvedFileEntityType::Normal } => { - file_ref == entered_query - } - NamedEntityInfo::UnresovledFile { file_ref, entity_type: UnresolvedFileEntityType::DailyNote { relative_label } } => { - relative_label == entered_query - } - NamedEntityInfo::Heading { heading, file } => false, - NamedEntityInfo::Block { index, file } => false, - NamedEntityInfo::UnresolvedHeading { file_ref: file_Ref, heading } => false, - }) - - } - - fn text_edit(&self, cx: &Context) -> tower_lsp::lsp_types::TextEdit { - let (display_text, file_ref, heading_ref): (Option, String, Option) = match self.info() { - - NamedEntityInfo::File { path, entity_type: FileEntityType::Normal } if cx.settings().file_as_first_heading() => { - (cx.querier().first_heading_of_file(&path).map(ToString::to_string), path.file_name().unwrap().to_str().expect("file name to be a valid string").to_string(), None) - } - NamedEntityInfo::File { path, entity_type: FileEntityType::Normal } => { - (None, path.file_name().unwrap().to_str().expect("file name to be a valid string").to_string(), None) - } - NamedEntityInfo::File { path, entity_type: FileEntityType::Alias { alias } } => { - (Some(alias.to_string()), path.file_name().unwrap().to_str().expect("file name to be a valid string").to_string(), None) - } - NamedEntityInfo::File { path, entity_type: FileEntityType::DailyNote { relative_label } } => { - (Some(relative_label.to_string()), path.file_name().unwrap().to_str().expect("file name to be a valid string").to_string(), None) - } - NamedEntityInfo::UnresovledFile { file_ref, entity_type } => { - (None, file_ref.to_string(), None) - } - NamedEntityInfo::Heading { heading, file } => { - (Some(heading.to_string()), file.file_name().unwrap().to_str().expect("file name to be a valid string").to_string(), Some(heading.to_string())) - } - NamedEntityInfo::Block { index, file } => { - (None, file.file_name().unwrap().to_str().expect("file name to be a valid string").to_string(), Some(format!("^{}", index))) - }, - NamedEntityInfo::UnresolvedHeading { file_ref, heading } => { - (Some(heading.to_string()), file_ref.to_string(), Some(heading.to_string())) - }, - - }; - - let new_text = match cx.parser().linking_style() { - LinkingStyle::Markdown => { - let ref_text = |file: String, heading| { - if file.contains(" ") || heading_ref.clone().is_some_and(|it| it.contains(" ")) { - format!("<{}{}>", file, heading) - } else { - format!("{}{}", file, heading) - } - }; - - let heading = heading_ref.as_ref().map(|it| format!("#{}", it)).unwrap_or("".to_string()); - let display_text = display_text.unwrap_or("".to_string()); - - format!("[{}]({})", display_text, ref_text(file_ref, heading)) - } - LinkingStyle::Wikilink => { - let display = |d| format!("|{}", d); - let heading = heading_ref.map(|it| format!("#{}", it)).unwrap_or("".to_string()); - let display_text = display_text.map(display).unwrap_or("".to_string()); - - format!("[[{file_ref}{heading}{display_text}]]") - } - - }; - - let range = cx.parser().link_range(); - - tower_lsp::lsp_types::TextEdit { - range, - new_text - } - } - - fn command(&self, cx: &Context) -> Option { - None - } - - -} - -// NOTE: This is here instead of in the struct functions becuaes it is this module that is responsible for what the *display -// text is, not the named entity struct. This is a UI responsibility. -fn display_text(named: &NamedEntity) -> String { - todo!() -} diff --git a/crates/completions/src/completions.rs b/crates/completions/src/completions.rs deleted file mode 100644 index 176a63dc..00000000 --- a/crates/completions/src/completions.rs +++ /dev/null @@ -1,95 +0,0 @@ -use rayon::prelude::*; -use do_notation::m; -use tower_lsp::lsp_types::{CompletionItem, CompletionTextEdit, Documentation, MarkupContent, MarkupKind}; - -use crate::context::Context; - -pub(super) trait Completer { - fn completions(&self, cx: &Context, location: &Location) -> Option>>; -} - -pub(super) trait Completion: Send + Sync { - fn label(&self, cx: &Context) -> String; - fn label_detail(&self, cx: &Context) -> Option; - fn kind(&self, cx: &Context) -> tower_lsp::lsp_types::CompletionItemKind; - fn detail(&self, cx: &Context) -> Option; - fn documentation(&self, cx: &Context) -> Option; - fn deprecated(&self, cx: &Context) -> Option; - fn preselect(&self, cx: &Context) -> Option; - fn text_edit(&self, cx: &Context) -> tower_lsp::lsp_types::TextEdit; - fn command(&self, cx: &Context) -> Option; - - // TODO: possibly an ID to handle completion resolve -} - -pub (super) struct Location { - pub(crate) line: u32, - pub(crate) character: u32, - pub(crate) file: String -} - - -pub (super) fn completions( - location: &Location, - cx: &Context, - referencer: &impl Completer, - // syntax_completer: &impl Completer, - // actions_completer: &impl Completer -) -> Option { - - // let actions_completions = actions_completer.completions(location); - // let syntax_completions = syntax_completer.completions(location); - let references_completions = referencer.completions(cx, location); - - let all_completions = m! { - // actions <- actions_completions; - // syntax_completions <- syntax_completions; - references_completions <- references_completions; - - Some( - // actions - // .chain(syntax_completions) - // .chain(references_completions.take(context.max_query_completion_items())) - references_completions - ) - }?; - - - Some( - tower_lsp::lsp_types::CompletionResponse::List( - tower_lsp::lsp_types::CompletionList { - is_incomplete: true, - items: completion_items(all_completions, cx) - } - ) - ) -} - -fn completion_items(completion_items: Vec>, cx: &Context) - -> Vec { - - completion_items.into_par_iter() - .enumerate() - .map(|(idx, completion)| CompletionItem { - label: completion.label(cx), - kind: Some(completion.kind(cx)), - detail: completion.detail(cx), - documentation: m! { - documentation <- completion.documentation(cx); - - Some(Documentation::MarkupContent(MarkupContent { - kind: MarkupKind::Markdown, - value: documentation - })) - }, - deprecated: completion.deprecated(cx), - preselect: completion.preselect(cx), - filter_text: Some(cx.parser().entered_completion_string().to_string()), - text_edit: Some(CompletionTextEdit::Edit(completion.text_edit(cx))), - command: completion.command(cx), - sort_text: Some(idx.to_string()), - ..Default::default() - }) - .collect() - -} diff --git a/crates/completions/src/context/mod.rs b/crates/completions/src/context/mod.rs deleted file mode 100644 index 60a28db9..00000000 --- a/crates/completions/src/context/mod.rs +++ /dev/null @@ -1,281 +0,0 @@ -use std::{ops::Range, path::{Path, PathBuf}}; - -use crate::completions::Location; -use moxide_config::Settings as MoxideSettings; -use rayon::prelude::*; -use vault::Vault; - -pub(crate) struct Context { -} - -impl Context { - pub fn new(vault: &Vault, settings: &MoxideSettings) -> Context { - todo!() - } - - pub fn parser(&self) -> &Parser { - todo!() - } - - pub fn querier(&self) -> &Querier { - todo!() - } - - pub fn entity_view(&self) -> &EntityView { - todo!() - } - - pub fn settings(&self) -> &Settings { - todo!() - } -} - - - - -pub (crate) struct Parser; - -impl Parser { - pub(crate) fn link_completion_info(&self, location: &Location) -> Option { - todo!() - } - - pub(crate) fn footnote_completion(&self, location: &Location) -> Option { - todo!() - } - pub(crate) fn link_ref_def_completion(&self, location: &Location) -> Option { - todo!() - } - pub(crate) fn tag(&self, location: &Location) -> Option> { - todo!() - } - - pub(crate) fn entered_query_string(&self) -> &str { - todo!() - } - - /// The string that the completion will be replacing. - pub(crate) fn entered_completion_string(&self) -> &str { - todo!() - } - - pub(crate) fn link_range(&self) -> tower_lsp::lsp_types::Range { - todo!() - } - - pub(crate) fn linking_style(&self) -> LinkingStyle { - todo!() - } -} - -pub(crate) enum LinkingStyle { - Markdown, - Wikilink -} - - -/// Querying the markdown entities in the vault: files, headings, blocks -pub (crate) struct Querier<'fs>(&'fs str); - -impl<'fs> Querier<'fs> { - pub(crate) fn named_grep_query<'a>(&'a self, query: &NamedQuery) -> impl IndexedParallelIterator + 'a { - vec![].into_par_iter() - } - // fn named_semantic_query - - // pub(crate) fn unnamed_grep_query(&self, query: &BlockQuery) -> impl IndexedParallelIterator { - // todo!() - // } - - // note; this lifetime might be wrong - pub(crate) fn first_heading_of_file(&self, path: &Path) -> Option<&str> { - todo!() - } -} - - -pub (crate) struct Settings; - -impl Settings { - pub(crate) fn max_query_completion_items(&self) -> usize { - todo!() - } - - pub(crate) fn file_as_first_heading(&self) -> bool { - todo!() - } - - pub(crate) fn backlinks_to_preview(&self) -> usize { - todo!() - } -} - -pub(crate) struct EntityView; - -impl EntityView { - // NOTE: the lifetime might be wrong - pub(crate) fn preview_with_backlinks(&self, entity: &NamedEntity) -> EntityWithBacklinksPreview { - todo!() - } -} - -pub(crate) struct EntityWithBacklinksPreview<'fs> { - pub(crate) entity_preview: String, - pub(crate) backlinks: Box> -} - -pub(crate) enum PathSettings { - Full, - Relative, - OnlyFileID -} - -pub(crate) struct LinkCompletionInfo; - -impl LinkCompletionInfo { - pub(crate) fn line_range(&self) -> Range { - todo!() - } - pub(crate) fn display_text(&self) -> Option<&str> { - todo!() - } - pub(crate) fn linking_mode(&self) -> LinkingMode { - todo!() - } - pub(crate) fn query(&self) -> &LinkQuery { - todo!() - } -} - -pub(crate) struct LinkingMode { - link_type: LinkType, - ref_type: RefType -} - -pub(crate) enum LinkType { - Markdown, - Wikilink -} - -pub(crate) enum RefType { - Infile, - Full -} - -pub(crate) struct LinkQuery<'fs> { - pub(crate) query: Query<'fs>, -} - -pub(crate) enum Query<'fs> { - Named(NamedQuery<'fs>), - Block(BlockQuery<'fs>) -} - - -pub (crate) enum Named { - File, -} - -pub (crate) struct NamedQuery<'fs>{ - query_string: NamedQueryString<'fs>, - // filters: Option, // TODO: implement -} - - -pub(crate) enum NamedQueryString<'fs> { - Path(PathBuf), - String(&'fs str), -} - -// TODO: implement -pub(crate) struct NamedQueryFilters; - - - -pub (crate) struct BlockQuery<'fs> { - query_string: &'fs str, - // filters: Option> -} - -// pub(crate) struct UnnamedQueryFilters<'fs> { -// references: Vec>, -// file: Vec>, -// } - -pub(crate) struct NamedEntitySpecifier<'fs> { - file: FileSpecifier<'fs>, - infile: Option>, -} - -pub(crate) enum FileSpecifier<'fs> { - Path(PathBuf), - Name(&'fs str), -} - -pub(crate) enum InfileSpecifier<'fs> { - Heading(&'fs str), - /// Block index without the `^` - BlockIndex(&'fs str), -} - - -pub(crate) struct NamedEntity { -} - -impl<'fs> NamedEntity { - pub(crate) fn info(&self) -> NamedEntityInfo<'fs> { - todo!() - } - pub(crate) fn documentation(&self) -> Option<&str> { - todo!() - } -} - - -pub(crate) enum NamedEntityInfo<'a> { - File { - path: PathBuf, - entity_type: FileEntityType<'a> - }, - Heading { - heading: &'a str, - file: PathBuf, - }, - Block { - index: &'a str, - file: PathBuf, - }, - UnresovledFile { - file_ref: &'a str, - entity_type: UnresolvedFileEntityType<'a> - }, - UnresolvedHeading { - file_ref: &'a str, - heading: &'a str - } -} - -impl NamedEntityInfo<'_> { -} - -pub(crate) enum FileEntityType<'a> { - Normal, - DailyNote { - relative_label: &'a str - }, - Alias { - alias: &'a str - }, - // Image, - // anything else? -} - - -pub(crate) enum UnresolvedFileEntityType<'a> { - Normal, - DailyNote { - relative_label: &'a str - }, - // anything else? -} - diff --git a/crates/completions/src/lib.rs b/crates/completions/src/lib.rs index d82da95a..d449ecac 100644 --- a/crates/completions/src/lib.rs +++ b/crates/completions/src/lib.rs @@ -1,41 +1,168 @@ -use std::path::{Path, PathBuf}; +#![feature(anonymous_lifetime_in_impl_trait)] -use completions::Location; -use tower_lsp::lsp_types::{CompletionParams, CompletionResponse}; +use std::{ops::{Deref, Range}, path::{Path, PathBuf}}; + +use rayon::prelude::*; +use regex::Regex; +use tower_lsp::lsp_types::{CompletionItem, CompletionList, CompletionParams, CompletionResponse}; use vault::Vault; use moxide_config::Settings; -use crate::{completers::referencer::Referencer, completions::completions, context::Context}; - -mod completions; -mod context; -mod completers; - pub fn get_completions( vault: &Vault, - files: &Box<[PathBuf]>, + _files: &Box<[PathBuf]>, params: &CompletionParams, path: &Path, - settings: &Settings + _settings: &Settings ) -> Option { + let parser = Parser::new(vault); + let querier = Querier::new(vault); + let location = Location { + path, + line: params.text_document_position.position.line as usize, + character: params.text_document_position.position.character as usize + }; + + dbg!(completions(&parser, &querier, location)) +} - // init context - let cx = Context::new(vault, settings); +fn completions(parser: &Parser, querier: &Querier, location: Location) -> Option { + let (file_ref, link_info) = parser.parse_link(location)?; + let files = querier.query(file_ref); + Some(to_completion_response(&link_info, files.into_par_iter())) +} - // init completers - let referencer = Referencer; - let location = Location { - line: params.text_document_position.position.line, - character: params.text_document_position.position.character, - file: path.to_string_lossy().to_string() - }; +struct Parser<'a> { + vault: &'a Vault +} + +impl<'a> Parser<'a> { + fn new(vault: &'a Vault) -> Self { + Self { vault } + } +} + +impl Parser<'_> { + fn parse_link(&self, location: Location) -> Option<(FileRef, LinkInfo)> { + + let chars = self.vault.select_line(location.path, location.line as isize)?; + let line_string = String::from_iter(chars); + + let re = Regex::new(r"\[\[(?.*?)\]\]").expect("Regex failed to compile"); + + let c = re.captures_iter(&line_string) + .next()?; + let file_ref = c.name("file_ref")?.as_str(); + let char_range = c.get(0)?.start()..c.get(0)?.end(); + + Some((file_ref.to_string(), LinkInfo {char_range, line: location.line})) - completions(&location, &cx, &referencer) + } +} + +type FileRef = String; +struct LinkInfo { + line: usize, + char_range: Range +} +struct Location<'fs> { + path: &'fs Path, + line: usize, + character: usize } +struct Querier<'a> { + vault: &'a Vault +} + +impl<'a> Querier<'a> { + fn new(vault: &'a Vault) -> Self { + Self { vault } + } +} +impl<'a> Querier<'a> { + fn query(&self, file_ref: FileRef) -> Vec<&'a Path> { + let paths = self.vault.md_files + .keys() + .map(|key| (key.file_name().unwrap().to_str().unwrap().to_string(), key)) + .collect::>(); + + let matched = fuzzy_match(&file_ref, paths); + matched.into_par_iter() + .map(|((_, path), _)| path as &Path) + .collect() + } +} + +impl<'a> Matchable for (String, &'a PathBuf) { + fn match_string(&self) -> &str { + self.0.as_str() + } +} + + +pub trait Matchable { + fn match_string(&self) -> &str; +} + + +struct NucleoMatchable(T); +impl Deref for NucleoMatchable { + type Target = T; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl AsRef for NucleoMatchable { + fn as_ref(&self) -> &str { + self.match_string() + } +} + + +use nucleo_matcher::{pattern::{self, Normalization}, Matcher}; +pub fn fuzzy_match<'a, T: Matchable>( + filter_text: &str, + items: impl IntoIterator, +) -> Vec<(T, u32)> { + let items = items.into_iter().map(NucleoMatchable); + + let mut matcher = Matcher::new(nucleo_matcher::Config::DEFAULT); + let matches = pattern::Pattern::parse( + filter_text, + pattern::CaseMatching::Smart, + Normalization::Smart, + ) + .match_list(items, &mut matcher); + + matches + .into_iter() + .map(|(item, score)| (item.0, score)) + .collect() +} + + + + +fn to_completion_response(info: &LinkInfo, files: impl IndexedParallelIterator) -> CompletionResponse { + let items = files.enumerate() + .flat_map(|(i, path)| Some((i, path.file_name()?.to_str()?))) + .flat_map(|(i, name)| Some(CompletionItem { + label: name.to_string(), + sort_text: Some(i.to_string()), + ..Default::default() + })) + .collect::>(); + + CompletionResponse::List(CompletionList { + is_incomplete: true, + items + }) +}