diff --git a/Cargo.lock b/Cargo.lock index 657d611c..5a3f2002 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -335,6 +335,12 @@ dependencies = [ "const-random", ] +[[package]] +name = "do-notation" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e16a80c1dda2cf52fa07106427d3d798b6331dca8155fcb8c39f7fc78f6dd2" + [[package]] name = "either" version = "1.8.1" @@ -641,6 +647,7 @@ dependencies = [ "anyhow", "chrono", "config", + "do-notation", "fuzzydate", "indexmap", "itertools", diff --git a/Cargo.toml b/Cargo.toml index 1837d6ce..fa87143f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ repository = "https://github.com/Feel-ix-343/markdown-oxide" anyhow = "1.0.80" chrono = "0.4.35" config = "0.14.0" +do-notation = "0.1.3" fuzzydate = "0.2.2" indexmap = "2.2.6" itertools = "0.10.5" diff --git a/src/completion/adapters/mod.rs b/src/completion/adapters/mod.rs new file mode 100644 index 00000000..04fecabd --- /dev/null +++ b/src/completion/adapters/mod.rs @@ -0,0 +1,4 @@ + +mod parser; +mod query; + diff --git a/src/completion/adapters/parser.rs b/src/completion/adapters/parser.rs new file mode 100644 index 00000000..0e21b64d --- /dev/null +++ b/src/completion/adapters/parser.rs @@ -0,0 +1,2 @@ + +// impl Parser for ... diff --git a/src/completion/adapters/query.rs b/src/completion/adapters/query.rs new file mode 100644 index 00000000..f36ddca5 --- /dev/null +++ b/src/completion/adapters/query.rs @@ -0,0 +1,4 @@ + + + +// impl Query for ... diff --git a/src/completion/callout_completer.rs b/src/completion/callout_completer.rs deleted file mode 100644 index a8f846e0..00000000 --- a/src/completion/callout_completer.rs +++ /dev/null @@ -1,192 +0,0 @@ -use once_cell::sync::Lazy; -use regex::Regex; -use tower_lsp::lsp_types::{ - CompletionItem, CompletionItemKind, CompletionItemLabelDetails, CompletionTextEdit, - InsertTextFormat, Position, Range, TextEdit, -}; - -use super::{Completable, Completer}; - -pub struct CalloutCompleter { - nested_level: usize, - line: u32, - character: u32, - preceding_text: String, -} - -impl<'a> Completer<'a> for CalloutCompleter { - fn construct(context: super::Context<'a>, line: usize, character: usize) -> Option - where - Self: Sized + Completer<'a>, - { - let line_chars = context.vault.select_line(context.path, line as isize)?; - - static PARTIAL_CALLOUT: Lazy = - Lazy::new(|| Regex::new(r"^(?(> *)+)").unwrap()); // [display](relativePath) - - let binding = String::from_iter(line_chars); - let captures = PARTIAL_CALLOUT.captures(&binding)?; - - let (_full, preceding) = (captures.get(0)?, captures.name("preceding")?); - - let nested_level = preceding.as_str().matches('>').count(); - - return Some(Self { - nested_level, - preceding_text: preceding.as_str().to_string(), - line: line as u32, - character: character as u32, - }); - } - - fn completions(&self) -> Vec> - where - Self: Sized, - { - vec![ - CalloutCompletion::Note, - CalloutCompletion::Abstract, - CalloutCompletion::Summary, - CalloutCompletion::Tldr, - CalloutCompletion::Info, - CalloutCompletion::Todo, - CalloutCompletion::Tip, - CalloutCompletion::Hint, - CalloutCompletion::Important, - CalloutCompletion::Success, - CalloutCompletion::Check, - CalloutCompletion::Done, - CalloutCompletion::Question, - CalloutCompletion::Help, - CalloutCompletion::Faq, - CalloutCompletion::Warning, - CalloutCompletion::Caution, - CalloutCompletion::Attention, - CalloutCompletion::Failure, - CalloutCompletion::Fail, - CalloutCompletion::Missing, - CalloutCompletion::Danger, - CalloutCompletion::Error, - CalloutCompletion::Bug, - CalloutCompletion::Example, - CalloutCompletion::Quote, - CalloutCompletion::Cite, - ] - } - - // TODO: get rid of this in the API - type FilterParams = &'static str; - fn completion_filter_text(&self, params: Self::FilterParams) -> String { - format!("{}{}", self.preceding_text, params) - } -} - -enum CalloutCompletion { - Note, - Abstract, - Summary, - Tldr, - Info, - Todo, - Tip, - Hint, - Important, - Success, - Check, - Done, - Question, - Help, - Faq, - Warning, - Caution, - Attention, - Failure, - Fail, - Missing, - Danger, - Error, - Bug, - Example, - Quote, - Cite, -} - -impl Completable<'_, CalloutCompleter> for CalloutCompletion { - fn completions(&self, completer: &CalloutCompleter) -> Option { - let name = match self { - Self::Note => "note", - Self::Abstract => "abstract", - Self::Summary => "summary", - Self::Tldr => "tldr", - Self::Info => "info", - Self::Todo => "todo", - Self::Tip => "tip", - Self::Hint => "hint", - Self::Important => "important", - Self::Success => "success", - Self::Check => "check", - Self::Done => "done", - Self::Question => "question", - Self::Help => "help", - Self::Faq => "faq", - Self::Warning => "warning", - Self::Caution => "caution", - Self::Attention => "attention", - Self::Failure => "failure", - Self::Fail => "fail", - Self::Missing => "missing", - Self::Danger => "danger", - Self::Error => "error", - Self::Bug => "bug", - Self::Example => "example", - Self::Quote => "quote", - Self::Cite => "cite", - }; - - let label_detail = match self { - Self::Summary | Self::Tldr => Some("alias of Abstract"), - Self::Hint | Self::Important => Some("alias of Tip"), - Self::Check | Self::Done => Some("alias of Success"), - Self::Help | Self::Faq => Some("alias of Question"), - Self::Caution | Self::Attention => Some("alias of Warning"), - Self::Fail | Self::Missing => Some("alias of Failure"), - Self::Error => Some("alias of Danger"), - Self::Cite => Some("alias of Quote"), - _ => None, - }; - - let snippet = format!( - "{prefix}[!{name}] ${{1:Title}}\n{prefix}${{2:Description}}", - prefix = "> ".repeat(completer.nested_level) - ); - - let filter_text = completer.completion_filter_text(name); - - let completion_item = CompletionItem { - label: name.to_string(), - label_details: label_detail.map(|detail| CompletionItemLabelDetails { - detail: Some(detail.to_string()), - description: None, - }), - insert_text_format: Some(InsertTextFormat::SNIPPET), - kind: Some(CompletionItemKind::SNIPPET), - text_edit: Some(CompletionTextEdit::Edit(TextEdit { - range: Range { - start: Position { - line: completer.line, - character: 0, - }, - end: Position { - line: completer.line, - character: completer.character, - }, - }, - new_text: snippet, - })), - filter_text: Some(filter_text), - ..Default::default() - }; - - Some(completion_item) - } -} diff --git a/src/completion/completers/actions.rs b/src/completion/completers/actions.rs new file mode 100644 index 00000000..e69de29b diff --git a/src/completion/completers/referencer.rs b/src/completion/completers/referencer.rs new file mode 100644 index 00000000..e69de29b diff --git a/src/completion/completers/syntax.rs b/src/completion/completers/syntax.rs new file mode 100644 index 00000000..e69de29b diff --git a/src/completion/completions.rs b/src/completion/completions.rs new file mode 100644 index 00000000..87bd258e --- /dev/null +++ b/src/completion/completions.rs @@ -0,0 +1,98 @@ +use rayon::prelude::*; +use do_notation::m; +use tower_lsp::lsp_types::{CompletionItem, CompletionTextEdit, Documentation, MarkupContent, MarkupKind}; + + + +pub(super) trait Completer{ + fn completions(&self, location: &Location) -> Option>>; +} + +pub(super) trait Completion{ + fn label(&self) -> String; + fn label_detail(&self) -> Option; + fn kind(&self) -> tower_lsp::lsp_types::CompletionItemKind; + fn detail(&self) -> Option; + fn documentation(&self) -> Option; + fn deprecated(&self) -> Option; + fn preselect(&self) -> Option; + fn filter_text(&self) -> Option; + fn text_edit(&self) -> tower_lsp::lsp_types::TextEdit; + fn additional_text_edits(&self) -> Option>; + fn command(&self) -> Option; + fn commit_characters(&self) -> Option>; + + // TODO: possibly an ID to handle completion resolve +} + +pub (super) struct Location; + +pub (super) trait Context { + fn max_query_completion_items(&self) -> usize; +} + +pub (super) fn completions( + location: &Location, + context: &impl 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(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())) + ) + }?; + + + Some( + tower_lsp::lsp_types::CompletionResponse::List( + tower_lsp::lsp_types::CompletionList { + is_incomplete: true, + items: completion_items(all_completions) + } + ) + ) +} + +fn completion_items(completion_items: impl IndexedParallelIterator>) + -> Vec { + + completion_items.into_par_iter() + .enumerate() + .map(|(idx, completion)| CompletionItem { + label: completion.label(), + kind: Some(completion.kind()), + detail: completion.detail(), + documentation: m! { + documentation <- completion.documentation(); + + Some(Documentation::MarkupContent(MarkupContent { + kind: MarkupKind::Markdown, + value: documentation + })) + }, + deprecated: completion.deprecated(), + preselect: completion.preselect(), + filter_text: completion.filter_text(), + text_edit: Some(CompletionTextEdit::Edit(completion.text_edit())), + additional_text_edits: completion.additional_text_edits(), + command: completion.command(), + commit_characters: completion.commit_characters(), + sort_text: Some(idx.to_string()), + ..Default::default() + }); + + todo!() +} diff --git a/src/completion/completions_service.rs b/src/completion/completions_service.rs new file mode 100644 index 00000000..e69de29b diff --git a/src/completion/footnote_completer.rs b/src/completion/footnote_completer.rs deleted file mode 100644 index 1e64d15f..00000000 --- a/src/completion/footnote_completer.rs +++ /dev/null @@ -1,99 +0,0 @@ -use std::path::Path; - -use tower_lsp::lsp_types::{CompletionItem, CompletionItemKind, Documentation}; - -use crate::{ - ui::preview_referenceable, - vault::{MDFootnote, Preview, Referenceable, Vault}, -}; - -use super::{Completable, Completer}; - -use rayon::prelude::*; - -pub struct FootnoteCompleter<'a> { - vault: &'a Vault, - path: &'a Path, -} - -impl<'a> Completer<'a> for FootnoteCompleter<'a> { - fn construct(context: super::Context<'a>, line: usize, character: usize) -> Option - where - Self: Sized + Completer<'a>, - { - let selected_line = context.vault.select_line(context.path, line as isize)?; - - if character - .checked_sub(1) - .and_then(|start| selected_line.get(start..character)) - == Some(&['[']) - { - Some(FootnoteCompleter { - path: context.path, - vault: context.vault, - }) - } else { - None - } - } - - fn completions(&self) -> Vec> - where - Self: Sized, - { - let path_footnotes = self - .vault - .select_referenceable_nodes(Some(self.path)) - .into_par_iter() - .flat_map(|referenceable| FootnoteCompletion::from_referenceable(referenceable)) - .collect::>(); - - path_footnotes - } - - type FilterParams = (&'a str, Referenceable<'a>); - fn completion_filter_text(&self, params: Self::FilterParams) -> String { - self.vault - .select_referenceable_preview(¶ms.1) - .and_then(|preview| match preview { - Preview::Text(string) => Some(string), - Preview::Empty => None, - }) - .map(|preview_string| format!("{}{}", params.0, &preview_string)) - .unwrap_or("".to_owned()) - } -} - -struct FootnoteCompletion<'a> { - footnote: (&'a Path, &'a MDFootnote), -} - -impl FootnoteCompletion<'_> { - fn from_referenceable(referenceable: Referenceable<'_>) -> Option> { - match referenceable { - Referenceable::Footnote(path, footnote) => Some(FootnoteCompletion { - footnote: (path, footnote), - }), - _ => None, - } - } -} - -impl<'a> Completable<'a, FootnoteCompleter<'a>> for FootnoteCompletion<'a> { - fn completions(&self, completer: &FootnoteCompleter<'a>) -> Option { - let refname = &self.footnote.1.index; - - let path = self.footnote.0; - let path_buf = path.to_path_buf(); - let self_referenceable = Referenceable::Footnote(&path_buf, self.footnote.1); - - Some(CompletionItem { - label: refname.to_string(), - kind: Some(CompletionItemKind::REFERENCE), - documentation: preview_referenceable(completer.vault, &self_referenceable) - .map(Documentation::MarkupContent), - filter_text: Some(completer.completion_filter_text((refname, self_referenceable))), - ..Default::default() - }) - } -} diff --git a/src/completion/link_completer.rs b/src/completion/link_completer.rs deleted file mode 100644 index a8dac3ac..00000000 --- a/src/completion/link_completer.rs +++ /dev/null @@ -1,895 +0,0 @@ -use std::{ - collections::HashSet, - iter::once, - path::{Path, PathBuf}, - time::SystemTime, -}; - -use chrono::{Duration, NaiveDate}; -use itertools::Itertools; -use once_cell::sync::Lazy; -use rayon::prelude::*; -use regex::Regex; -use tower_lsp::lsp_types::{ - CompletionItem, CompletionItemKind, CompletionItemLabelDetails, CompletionTextEdit, - Documentation, InsertTextFormat, Position, Range, TextEdit, -}; - -use crate::{ - completion::util::check_in_code_block, - config::Settings, - ui::preview_referenceable, - vault::{MDFile, MDHeading, Reference, Referenceable, Vault}, -}; - -use super::{ - matcher::{fuzzy_match_completions, Matchable, OrderedCompletion}, - Completable, Completer, Context, -}; - -/// Range on a single line; assumes that the line number is known. -type LineRange = std::ops::Range; - -pub struct MarkdownLinkCompleter<'a> { - /// The display text of a link to be completed - pub display: (String, LineRange), - /// the filepath of the markdown link to be completed - pub path: (String, LineRange), - /// the infile ref; the range is the whole span of the infile ref. (including the ^ for Block refs) - pub infile_ref: Option<(PartialInfileRef, LineRange)>, - - pub full_range: LineRange, - pub line_nr: usize, - pub position: Position, - pub vault: &'a Vault, - pub context_path: &'a Path, - pub settings: &'a Settings, -} - -pub trait LinkCompleter<'a>: Completer<'a> { - fn settings(&self) -> &'a Settings; - fn completion_text_edit(&self, display: Option<&str>, refname: &str) -> CompletionTextEdit; - fn entered_refname(&self) -> String; - fn vault(&self) -> &'a Vault; - fn position(&self) -> Position; - fn path(&self) -> &'a Path; - fn link_completions(&self) -> Vec> - where - Self: Sync, - { - let referenceables = self.vault().select_referenceable_nodes(None); - - let position = self.position(); - - let unresolved_under_cursor = self - .vault() - .select_reference_at_position(self.path(), position) - .map(|reference| { - self.vault() - .select_referenceables_for_reference(reference, self.path()) - }) - .into_iter() - .flatten() - .find(|referenceable| referenceable.is_unresolved()); - - let single_unresolved_under_cursor = unresolved_under_cursor.and_then(|referenceable| { - let ref_count = self - .vault() - .select_references_for_referenceable(&referenceable)? - .len(); - - if ref_count == 1 { - Some(referenceable) - } else { - None - } - }); - - let heading_completions = self.settings().heading_completions; - - // Get and filter referenceables - let completions = referenceables - .into_par_iter() - .filter(|referenceable| Some(referenceable) != single_unresolved_under_cursor.as_ref()) - .filter(|referenceable| { - heading_completions - || !matches!( - referenceable, - Referenceable::Heading(..) | Referenceable::UnresolvedHeading(..) - ) - }) - .flat_map(|referenceable| { - LinkCompletion::new(referenceable.clone(), self) - .into_iter() - .par_bridge() - }) - .flatten() - .collect::>(); - - // TODO: This could be slow - let refnames = completions - .par_iter() - .map(|completion| completion.refname()) - .collect::>(); - - // Get daily notes for convienience - let today = chrono::Local::now().date_naive(); - let days = (-7..=7) - .flat_map(|i| Some(today + Duration::try_days(i)?)) - .flat_map(|date| MDDailyNote::from_date(date, self)) - .filter(|date| !refnames.contains(&date.ref_name)) - .map(LinkCompletion::DailyNote); - - completions.into_iter().chain(days).collect::>() - } -} - -impl<'a> LinkCompleter<'a> for MarkdownLinkCompleter<'a> { - fn settings(&self) -> &'a Settings { - self.settings - } - - fn path(&self) -> &'a Path { - self.context_path - } - fn position(&self) -> Position { - self.position - } - - fn vault(&self) -> &'a Vault { - self.vault - } - - fn entered_refname(&self) -> String { - format!( - "{}{}", - self.path.0, - self.infile_ref - .as_ref() - .map(|infile| infile.0.to_string()) - .unwrap_or("".to_string()) - ) - } - - /// Will add <$1> to the refname if it contains spaces - fn completion_text_edit(&self, display: Option<&str>, refname: &str) -> CompletionTextEdit { - let link_ref_text = match refname.contains(' ') { - true => format!("<{}>", refname), - false => refname.to_owned(), - }; - - CompletionTextEdit::Edit(TextEdit { - range: Range { - start: Position { - line: self.line_nr as u32, - character: self.full_range.start as u32, - }, - end: Position { - line: self.line_nr as u32, - character: self.full_range.end as u32, - }, - }, - new_text: format!("[{}]({})", display.unwrap_or(""), link_ref_text), - }) - } -} - -impl<'a> Completer<'a> for MarkdownLinkCompleter<'a> { - fn construct(context: Context<'a>, line: usize, character: usize) -> Option - where - Self: Sized, - { - if context.settings.references_in_codeblocks == false - && check_in_code_block(&context, line, character) - { - return None; - } - - let Context { - vault, - opened_files: _, - path, - .. - } = context; - - let line_chars = vault.select_line(path, line as isize)?; - let line_to_cursor = line_chars.get(0..character)?; - - static PARTIAL_MDLINK_REGEX: Lazy = Lazy::new(|| { - Regex::new(r"\[(?[^\[\]\(\)]*)\]\((?[^\[\]\(\)\#]*)(\#(?[^\[\]\(\)]*))?$").unwrap() - }); // [display](relativePath) - - let line_string_to_cursor = String::from_iter(line_to_cursor); - - let captures = PARTIAL_MDLINK_REGEX.captures(&line_string_to_cursor)?; - - let (full, display, reftext, infileref) = ( - captures.get(0)?, - captures.name("display")?, - captures.name("path")?, - captures.name("infileref"), - ); - - let line_string = String::from_iter(&line_chars); - - let reference_under_cursor = Reference::new(&line_string).into_iter().find(|reference| { - reference.range.start.character <= character as u32 - && reference.range.end.character >= character as u32 - }); - - let full_range = match reference_under_cursor { - Some( - reference @ (Reference::MDFileLink(..) - | Reference::MDHeadingLink(..) - | Reference::MDIndexedBlockLink(..)), - ) => reference.range.start.character as usize..reference.range.end.character as usize, - None if line_chars.get(character) == Some(&')') => { - full.range().start..full.range().end + 1 - } - _ => full.range(), - }; - - let partial_infileref = infileref.map(|infileref| { - let chars = infileref.as_str().chars().collect::>(); - - let range = infileref.range(); - - match chars.as_slice() { - ['^', rest @ ..] => (PartialInfileRef::BlockRef(String::from_iter(rest)), range), - rest => (PartialInfileRef::HeadingRef(String::from_iter(rest)), range), - } - }); - - let partial = Some(MarkdownLinkCompleter { - path: (reftext.as_str().to_string(), reftext.range()), - display: (display.as_str().to_string(), display.range()), - infile_ref: partial_infileref, - full_range, - line_nr: line, - position: Position { - line: line as u32, - character: character as u32, - }, - vault, - context_path: context.path, - settings: context.settings, - }); - - partial - } - - fn completions(&self) -> Vec>> { - let filter_text = format!( - "{}{}", - self.path.0, - self.infile_ref - .clone() - .map(|(infile, _)| format!("#{}", infile.completion_string())) - .unwrap_or("".to_string()) - ); - - let link_completions = self.link_completions(); - - let matches = fuzzy_match_completions(&filter_text, link_completions); - - matches - } - - /// The completions refname - type FilterParams = &'a str; - - fn completion_filter_text(&self, params: Self::FilterParams) -> String { - let filter_text = format!("[{}]({}", self.display.0, params); - - filter_text - } -} - -#[derive(Debug, Clone)] -pub enum PartialInfileRef { - HeadingRef(String), - /// The partial reference to a block, not including the ^ index - BlockRef(String), -} - -impl ToString for PartialInfileRef { - fn to_string(&self) -> String { - match self { - Self::HeadingRef(string) => string.to_owned(), - Self::BlockRef(string) => format!("^{}", string), - } - } -} - -impl PartialInfileRef { - fn completion_string(&self) -> String { - match self { - PartialInfileRef::HeadingRef(s) => s.to_string(), - PartialInfileRef::BlockRef(s) => format!("^{}", s), - } - } -} - -pub struct WikiLinkCompleter<'a> { - vault: &'a Vault, - cmp_text: Vec, - files: &'a [PathBuf], - index: u32, - character: u32, - line: u32, - context_path: &'a Path, - settings: &'a Settings, - chars_in_line: u32, -} - -impl<'a> LinkCompleter<'a> for WikiLinkCompleter<'a> { - fn settings(&self) -> &'a Settings { - self.settings - } - - fn path(&self) -> &'a Path { - self.context_path - } - - fn position(&self) -> Position { - Position { - line: self.line, - character: self.character, - } - } - - fn vault(&self) -> &'a Vault { - self.vault - } - - fn entered_refname(&self) -> String { - String::from_iter(&self.cmp_text) - } - - fn completion_text_edit(&self, display: Option<&str>, refname: &str) -> CompletionTextEdit { - CompletionTextEdit::Edit(TextEdit { - range: Range { - start: Position { - line: self.line, - character: self.index + 1_u32, // index is right at the '[' in [[link]]; we want one more than that - }, - end: Position { - line: self.line, - character: (self.chars_in_line).min(self.character + 2_u32), - }, - }, - new_text: format!( - "{}{}]]${{2:}}", - refname, - display - .map(|display| format!("|{}", display)) - .unwrap_or("".to_string()) - ), - }) - } -} - -impl<'a> Completer<'a> for WikiLinkCompleter<'a> { - fn construct(context: Context<'a>, line: usize, character: usize) -> Option - where - Self: Sized, - { - if context.settings.references_in_codeblocks == false - && check_in_code_block(&context, line, character) - { - return None; - } - - let Context { - vault, - opened_files, - path, - .. - } = context; - - let line_chars = vault.select_line(path, line as isize)?; - - let index = line_chars - .get(0..=character)? // select only the characters up to the cursor - .iter() - .enumerate() // attach indexes - .tuple_windows() // window into pairs of characters - .collect::>() - .into_iter() - .rev() // search from the cursor back - .find(|((_, &c1), (_, &c2))| c1 == '[' && c2 == '[') - .map(|(_, (i, _))| i); // only take the index; using map because find returns an option - - let index = index.and_then(|index| { - if line_chars.get(index..character)?.iter().contains(&']') { - None - } else { - Some(index) - } - }); - - index.and_then(|index| { - let cmp_text = line_chars.get(index + 1..character)?; - - Some(WikiLinkCompleter { - vault, - cmp_text: cmp_text.to_vec(), - files: opened_files, - index: index as u32, - character: character as u32, - line: line as u32, - context_path: context.path, - settings: context.settings, - chars_in_line: line_chars.len() as u32, - }) - }) - } - - fn completions(&self) -> Vec> - where - Self: Sized, - { - let WikiLinkCompleter { vault, .. } = self; - - match *self.cmp_text { - // Give recent referenceables; TODO: improve this; - [] => self - .files - .iter() - .map( - |path| match std::fs::metadata(path).and_then(|meta| meta.modified()) { - Ok(modified) => (path, modified), - Err(_) => (path, SystemTime::UNIX_EPOCH), - }, - ) - .sorted_by_key(|(_, modified)| *modified) - .flat_map(|(path, modified)| { - let referenceables = vault - .select_referenceable_nodes(Some(path)) - .into_iter() - .filter(|referenceable| { - self.settings().heading_completions - || !matches!( - referenceable, - Referenceable::Heading(..) - | Referenceable::UnresolvedHeading(..) - ) - }) - .collect::>(); - - let modified_string = modified - .duration_since(SystemTime::UNIX_EPOCH) - .ok()? - .as_secs() - .to_string(); - - Some( - referenceables - .into_iter() - .flat_map(move |referenceable| LinkCompletion::new(referenceable, self)) - .flatten() - .flat_map(move |completion| { - Some(OrderedCompletion::::new( - completion, - modified_string.clone(), - )) - }), - ) - }) - .flatten() - .collect_vec(), - ref filter_text @ [..] if !filter_text.contains(&']') => { - let filter_text = &self.cmp_text; - - let link_completions = self.link_completions(); - - let matches = - fuzzy_match_completions(&String::from_iter(filter_text), link_completions); - - matches - } - _ => vec![], - } - } - - type FilterParams = &'a str; - fn completion_filter_text(&self, params: Self::FilterParams) -> String { - params.to_string() - } -} - -#[derive(Debug, Clone)] -pub enum LinkCompletion<'a> { - File { - mdfile: &'a MDFile, - match_string: String, - referenceable: Referenceable<'a>, - }, - Alias { - filename: &'a str, - match_string: &'a str, - referenceable: Referenceable<'a>, - }, - Heading { - heading: &'a MDHeading, - match_string: String, - referenceable: Referenceable<'a>, - }, - Block { - match_string: String, - referenceable: Referenceable<'a>, - }, - Unresolved { - match_string: String, - /// Infile ref includes all after #, including ^ - infile_ref: Option, - referenceable: Referenceable<'a>, - }, - DailyNote(MDDailyNote<'a>), -} - -use LinkCompletion::*; - -impl LinkCompletion<'_> { - fn new<'a>( - referenceable: Referenceable<'a>, - completer: &impl LinkCompleter<'a>, - ) -> Option>> { - if let Some(daily) = MDDailyNote::from_referenceable(referenceable.clone(), completer) { - Some(vec![DailyNote(daily)]) - } else { - match referenceable { - Referenceable::File(_, mdfile) => { - Some( - once(File { - mdfile, - match_string: mdfile.file_name()?.to_string(), - referenceable: referenceable.clone(), - }) - .chain(mdfile.metadata.iter().flat_map(|it| it.aliases()).flat_map( - |alias| { - Some(Alias { - filename: mdfile.file_name()?, - match_string: alias, - referenceable: referenceable.clone(), - }) - }, - )) - .collect(), - ) - } - Referenceable::Heading(path, mdheading) => Some( - once(Heading { - heading: mdheading, - match_string: format!( - "{}#{}", - path.file_stem()?.to_str()?, - mdheading.heading_text - ), - referenceable, - }) - .collect(), - ), - Referenceable::IndexedBlock(path, indexed) => Some( - once(Block { - match_string: format!("{}#^{}", path.file_stem()?.to_str()?, indexed.index), - referenceable, - }) - .collect(), - ), - Referenceable::UnresovledFile(_, file) => Some( - once(Unresolved { - match_string: file.clone(), - infile_ref: None, - referenceable, - }) - .collect(), - ), - Referenceable::UnresolvedHeading(_, s1, s2) => Some( - once(Unresolved { - match_string: format!("{}#{}", s1, s2), - infile_ref: Some(s2.clone()), - referenceable, - }) - .collect(), - ), - Referenceable::UnresovledIndexedBlock(_, s1, s2) => Some( - once(Unresolved { - match_string: format!("{}#^{}", s1, s2), - infile_ref: Some(format!("^{}", s2)), - referenceable, - }) - .collect(), - ), - _ => None, - } - } - } - - fn default_completion<'a>( - &self, - text_edit: CompletionTextEdit, - filter_text: &str, - completer: &impl LinkCompleter<'a>, - ) -> CompletionItem { - let vault = completer.vault(); - let referenceable = match self { - Self::File { referenceable, .. } - | Self::Heading { referenceable, .. } - | Self::Block { referenceable, .. } - | Self::Unresolved { referenceable, .. } - | Self::Alias { referenceable, .. } => referenceable.to_owned(), - Self::DailyNote(daily) => daily.referenceable(completer), - }; - - let label = self.match_string(); - - CompletionItem { - label: label.to_string(), - kind: Some(match self { - Self::File { .. } => CompletionItemKind::FILE, - Self::Heading { .. } | Self::Block { .. } => CompletionItemKind::REFERENCE, - Self::Unresolved { - match_string: _, - infile_ref: _, - .. - } => CompletionItemKind::KEYWORD, - Self::Alias { .. } => CompletionItemKind::ENUM, - Self::DailyNote { .. } => CompletionItemKind::EVENT, - }), - label_details: match self { - Self::Unresolved { - match_string: _, - infile_ref: _, - .. - } => Some(CompletionItemLabelDetails { - detail: Some("Unresolved".into()), - description: None, - }), - Alias { filename, .. } => Some(CompletionItemLabelDetails { - detail: Some(format!("Alias: {}.md", filename)), - description: None, - }), - File { .. } => None, - Heading { .. } => None, - Block { .. } => None, - DailyNote(_) => None, - }, - text_edit: Some(text_edit), - preselect: Some(match self { - Self::DailyNote(daily) => { - daily.relative_name(completer) == Some(completer.entered_refname()) - } - link_completion => link_completion.refname() == completer.entered_refname(), - }), - filter_text: Some(filter_text.to_string()), - documentation: preview_referenceable(vault, &referenceable) - .map(Documentation::MarkupContent), - ..Default::default() - } - } - - /// Refname to be inserted into the document - fn refname(&self) -> String { - match self { - Self::DailyNote(MDDailyNote { ref_name, .. }) => ref_name.to_string(), - File { match_string, .. } - | Heading { match_string, .. } - | Block { match_string, .. } - | Unresolved { match_string, .. } => match_string.to_string(), - Alias { filename, .. } => filename.to_string(), - } - } -} - -impl<'a> Completable<'a, MarkdownLinkCompleter<'a>> for LinkCompletion<'a> { - fn completions( - &self, - markdown_link_completer: &MarkdownLinkCompleter<'a>, - ) -> Option { - let refname = self.refname(); - let match_string = self.match_string(); - - let display = &markdown_link_completer.display; - - let link_display_text = match self { - File { - mdfile: _, - match_string: _, - .. - } - | Self::Block { - match_string: _, .. - } => None, - Self::Alias { match_string, .. } => Some(match_string.to_string()), - Self::DailyNote(daily) => daily.relative_name(markdown_link_completer), - Self::Heading { - heading, - match_string: _, - .. - } => Some(heading.heading_text.to_string()), - Self::Unresolved { - match_string: _, - infile_ref, - .. - } => infile_ref.clone(), - }; - - let binding = (display.0.as_str(), link_display_text); - let link_display_text = match binding { - ("", Some(ref infile)) => infile, - // Get the first heading of the file, if possible. - ("", None) if markdown_link_completer.settings().title_headings => match self { - Self::File { mdfile, .. } => mdfile - .headings - .first() - .map(|heading| heading.heading_text.as_str()) - .unwrap_or(""), - Self::Alias { - match_string: alias, - .. - } => alias, - _ => "", - }, - (display, _) => display, - }; - - let link_display_text = format!("${{1:{}}}", link_display_text,); - - let text_edit = - markdown_link_completer.completion_text_edit(Some(&link_display_text), &refname); - - let filter_text = markdown_link_completer.completion_filter_text(match_string); // TODO: abstract into default_completion - - Some(CompletionItem { - insert_text_format: Some(InsertTextFormat::SNIPPET), - ..self.default_completion(text_edit, &filter_text, markdown_link_completer) - }) - } -} - -impl<'a> Completable<'a, WikiLinkCompleter<'a>> for LinkCompletion<'a> { - fn completions(&self, completer: &WikiLinkCompleter<'a>) -> Option { - let refname = self.refname(); - let match_text = self.match_string(); - - let wikilink_display_text = match self { - File { .. } => None, - Alias { match_string, .. } => Some(format!("${{1:{}}}", match_string)), - Heading { .. } => None, - Block { .. } => None, - Unresolved { .. } => None, - DailyNote(_) => None, - }; - - let text_edit = completer.completion_text_edit(wikilink_display_text.as_deref(), &refname); - - let filter_text = completer.completion_filter_text(match_text); - - Some(CompletionItem { - insert_text_format: Some(InsertTextFormat::SNIPPET), - ..self.default_completion(text_edit, &filter_text, completer) - }) - } -} - -impl Matchable for LinkCompletion<'_> { - /// The string used for fuzzy matching - fn match_string(&self) -> &str { - match self { - File { - mdfile: _, - match_string, - .. - } - | Heading { - heading: _, - match_string, - .. - } - | Block { match_string, .. } - | Unresolved { match_string, .. } - | DailyNote(MDDailyNote { match_string, .. }) => match_string, - Alias { match_string, .. } => match_string, - } - } -} - -#[derive(Clone, Debug)] -pub struct MDDailyNote<'a> { - match_string: String, - ref_name: String, - real_referenceaable: Option>, -} - -impl MDDailyNote<'_> { - pub fn relative_name<'a>(&self, completer: &impl LinkCompleter<'a>) -> Option { - let self_date = self.get_self_date(completer)?; - - Self::relative_date_string(self_date) - } - - pub fn get_self_date<'a>(&self, completer: &impl LinkCompleter<'a>) -> Option { - let dailynote_format = &completer.settings().dailynote; - - chrono::NaiveDate::parse_from_str(&self.ref_name, dailynote_format).ok() - } - - fn relative_date_string(date: NaiveDate) -> Option { - let today = chrono::Local::now().date_naive(); - - if today == date { - Some("today".to_string()) - } else { - match (date - today).num_days() { - 1 => Some("tomorrow".to_string()), - 2..=7 => Some(format!("next {}", date.format("%A"))), - -1 => Some("yesterday".to_string()), - -7..=-1 => Some(format!("last {}", date.format("%A"))), - _ => None, - } - } - } - - /// The refname used for fuzzy matching a completion - not the actual inserted text - fn from_referenceable<'a>( - referenceable: Referenceable<'a>, - completer: &impl LinkCompleter<'a>, - ) -> Option> { - let Some((filerefname, filter_refname)) = (match referenceable { - Referenceable::File(&ref path, _) | Referenceable::UnresovledFile(ref path, _) => { - let filename = path.file_name(); - let dailynote_format = &completer.settings().dailynote; - let (date, filename) = filename.and_then(|filename| { - let filename = filename.to_str()?; - let filename = filename.replace(".md", ""); - Some(( - chrono::NaiveDate::parse_from_str(&filename, dailynote_format).ok(), - filename, - )) - })?; - - date.and_then(Self::relative_date_string) - .map(|thing| (filename.clone(), format!("{}: {}", thing, filename))) - } - _ => None, - }) else { - return None; - }; - - Some(MDDailyNote { - match_string: filter_refname, - ref_name: filerefname, - real_referenceaable: Some(referenceable), - }) - } - - fn from_date<'a>( - date: NaiveDate, - completer: &impl LinkCompleter<'a>, - ) -> Option> { - let filerefname = date.format(&completer.settings().dailynote).to_string(); - let match_string = format!("{}: {}", Self::relative_date_string(date)?, filerefname); - - // path on unresolved file is useless - Some(MDDailyNote { - match_string, - ref_name: filerefname.clone(), - real_referenceaable: None, - }) - } - - /// mock referenceable for kicks - fn referenceable<'a, 'b>(&'b self, completer: &impl LinkCompleter<'a>) -> Referenceable<'b> { - if let Some(referencaable) = &self.real_referenceaable { - return referencaable.clone(); - } - - let mut path = completer.vault().root_dir().to_path_buf(); - path.push(format!("{}.md", self.ref_name)); - - let unresolved_file = Referenceable::UnresovledFile(path.to_path_buf(), &self.ref_name); - - unresolved_file - } -} diff --git a/src/completion/matcher.rs b/src/completion/matcher.rs deleted file mode 100644 index d9c23513..00000000 --- a/src/completion/matcher.rs +++ /dev/null @@ -1,94 +0,0 @@ -use std::ops::Deref; - -use nucleo_matcher::{ - pattern::{self, Normalization}, - Matcher, -}; -use tower_lsp::lsp_types::CompletionItem; - -use super::{Completable, Completer}; - -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() - } -} - -pub struct OrderedCompletion<'a, C, T> -where - C: Completer<'a>, - T: Completable<'a, C>, -{ - completable: T, - rank: String, - __phantom: std::marker::PhantomData<&'a T>, - __phantom2: std::marker::PhantomData, -} - -impl<'a, C: Completer<'a>, T: Completable<'a, C>> OrderedCompletion<'a, C, T> { - pub fn new(completable: T, rank: String) -> Self { - Self { - completable, - rank, - __phantom: std::marker::PhantomData, - __phantom2: std::marker::PhantomData, - } - } -} - -impl<'a, C: Completer<'a>, T: Completable<'a, C>> Completable<'a, C> - for OrderedCompletion<'a, C, T> -{ - fn completions(&self, completer: &C) -> Option { - let completion = self.completable.completions(completer); - - completion.map(|completion| CompletionItem { - sort_text: Some(self.rank.to_string()), - ..completion - }) - } -} - -pub fn fuzzy_match_completions<'a, 'b, C: Completer<'a>, T: Matchable + Completable<'a, C>>( - filter_text: &'b str, - items: impl IntoIterator, -) -> Vec> { - let normal_fuzzy_match = fuzzy_match(filter_text, items); - - normal_fuzzy_match - .into_iter() - .map(|(item, score)| OrderedCompletion::new(item, score.to_string())) - .collect::>() -} - -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() -} diff --git a/src/completion/mod.rs b/src/completion/mod.rs index 8cf26c8e..bc7b9a1b 100644 --- a/src/completion/mod.rs +++ b/src/completion/mod.rs @@ -1,333 +1,4 @@ -use std::path::{Path, PathBuf}; +mod completions; +mod shared_traits; +mod adapters; -use tower_lsp::lsp_types::{CompletionItem, CompletionList, CompletionParams, CompletionResponse}; - -use crate::{config::Settings, vault::Vault}; - -use self::callout_completer::CalloutCompleter; -use self::link_completer::WikiLinkCompleter; -use self::{ - footnote_completer::FootnoteCompleter, link_completer::MarkdownLinkCompleter, - tag_completer::TagCompleter, unindexed_block_completer::UnindexedBlockCompleter, -}; - -mod callout_completer; -mod footnote_completer; -mod link_completer; -mod matcher; -mod tag_completer; -mod unindexed_block_completer; -mod util; - -#[derive(Clone, Copy)] -pub struct Context<'a> { - vault: &'a Vault, - opened_files: &'a [PathBuf], - path: &'a Path, - settings: &'a Settings, -} - -pub trait Completer<'a>: Sized { - fn construct(context: Context<'a>, line: usize, character: usize) -> Option - where - Self: Sized + Completer<'a>; - - fn completions(&self) -> Vec> - where - Self: Sized; - - type FilterParams; - /// Completere like nvim-cmp are odd so manually define the filter text as a situational workaround - fn completion_filter_text(&self, params: Self::FilterParams) -> String; - - // fn compeltion_resolve(&self, vault: &Vault, resolve_item: CompletionItem) -> Option; -} - -pub trait Completable<'a, T: Completer<'a>>: Sized { - fn completions(&self, completer: &T) -> Option; -} - -/// Range indexes for one line of the file; NOT THE WHOLE FILE -type LineRange = std::ops::Range; - -pub fn get_completions( - vault: &Vault, - initial_completion_files: &[PathBuf], - params: &CompletionParams, - path: &Path, - config: &Settings, -) -> Option { - let completion_context = Context { - vault, - opened_files: initial_completion_files, - path, - settings: config, - }; - - // I would refactor this if I could figure out generic closures - run_completer::>( - completion_context, - params.text_document_position.position.line, - params.text_document_position.position.character, - ) - .or_else(|| { - run_completer::>( - completion_context, - params.text_document_position.position.line, - params.text_document_position.position.character, - ) - }) - .or_else(|| { - run_completer::( - completion_context, - params.text_document_position.position.line, - params.text_document_position.position.character, - ) - }) - .or_else(|| { - run_completer::( - completion_context, - params.text_document_position.position.line, - params.text_document_position.position.character, - ) - }) - .or_else(|| { - run_completer::( - completion_context, - params.text_document_position.position.line, - params.text_document_position.position.character, - ) - }) - .or_else(|| { - run_completer::( - completion_context, - params.text_document_position.position.line, - params.text_document_position.position.character, - ) - }) - .or_else(|| { - run_completer::( - completion_context, - params.text_document_position.position.line, - params.text_document_position.position.character, - ) - }) -} - -// #[cfg(test)] -// mod tests { -// use itertools::Itertools; -// -// use super::{get_wikilink_index, CompletableMDLink, CompletableTag, get_completable_tag}; -// -// #[test] -// fn test_index() { -// let s = "test [[linjfkdfjds]]"; -// -// let expected = 6; -// -// let actual = get_wikilink_index(&s.chars().collect(), 10); -// -// assert_eq!(Some(expected), actual); -// -// assert_eq!(Some("lin"), s.get(expected + 1..10)); -// } -// -// #[test] -// fn test_partial_mdlink() { -// let line = "This is line [display](partialpa"; // (th) -// -// let expected = Some(CompletableMDLink { -// partial: ("[display](partialpa".to_string(), 13..32), -// display: ("display".to_string(), 14..21), -// path: ("partialpa".to_string(), 23..32), -// infile_ref: None, -// full_range: 13..32, -// }); -// -// let actual = super::get_completable_mdlink(&line.chars().collect(), 32); -// -// assert_eq!(actual, expected); -// -// let line = "This is line [display](partialpath)"; // (th) -// -// let expected = Some(CompletableMDLink { -// partial: ("[display](partialpa".to_string(), 13..32), -// display: ("display".to_string(), 14..21), -// path: ("partialpa".to_string(), 23..32), -// infile_ref: None, -// full_range: 13..35, -// }); -// -// let actual = super::get_completable_mdlink(&line.chars().collect(), 32); -// -// assert_eq!(actual, expected); -// -// let line = "[disp](pp) This is line [display](partialpath)"; // (th) -// -// let expected = Some(CompletableMDLink { -// partial: ("[display](partialpa".to_string(), 24..43), -// display: ("display".to_string(), 25..32), -// path: ("partialpa".to_string(), 34..43), -// infile_ref: None, -// full_range: 24..46, -// }); -// -// let actual = super::get_completable_mdlink(&line.chars().collect(), 43); -// -// assert_eq!(actual, expected); -// -// let line = "[disp](pp) This is line [display](partialpath)"; // (th) -// -// let expected = Some(CompletableMDLink { -// partial: ("[display](partialpath".to_string(), 24..45), -// display: ("display".to_string(), 25..32), -// path: ("partialpath".to_string(), 34..45), -// infile_ref: None, -// full_range: 24..46, -// }); -// -// let actual = super::get_completable_mdlink(&line.chars().collect(), 45); -// -// assert_eq!(actual, expected); -// } -// -// #[test] -// fn test_partial_mdlink_infile_refs() { -// let line = "This is line [display](partialpa#"; // (th) -// -// let expected = Some(CompletableMDLink { -// partial: ("[display](partialpa#".to_string(), 13..33), -// display: ("display".to_string(), 14..21), -// path: ("partialpa".to_string(), 23..32), -// infile_ref: Some(("".to_string(), 33..33)), -// full_range: 13..33, -// }); -// -// let actual = super::get_completable_mdlink(&line.chars().collect(), 33); -// -// assert_eq!(actual, expected); -// -// let line = "[disp](pp) This is line [display](partialpath#Display)"; // (th) -// -// let expected = Some(CompletableMDLink { -// partial: ("[display](partialpath#Display".to_string(), 24..53), -// display: ("display".to_string(), 25..32), -// path: ("partialpath".to_string(), 34..45), -// infile_ref: Some(("Display".to_string(), 46..53)), -// full_range: 24..54, -// }); -// -// let actual = super::get_completable_mdlink(&line.chars().collect(), 53); -// -// assert_eq!(actual, expected); -// -// let line = "[disp](pp) This is line [display](partialpath#Display)"; // (th) -// -// let expected = Some(CompletableMDLink { -// partial: ("[display](partialpath#Disp".to_string(), 24..50), -// display: ("display".to_string(), 25..32), -// path: ("partialpath".to_string(), 34..45), -// infile_ref: Some(("Disp".to_string(), 46..50)), -// full_range: 24..54, -// }); -// -// let actual = super::get_completable_mdlink(&line.chars().collect(), 50); -// -// assert_eq!(actual, expected); -// } -// -// #[test] -// fn test_completable_tag_parsing() { -// // 0 1 2 -// // 01234567890123456789012345678 -// let text = "text over here #tag more text"; -// -// let insert_position = 19; -// -// let expected = CompletableTag { -// full_range: 15..19, -// inputted_tag: ("tag".to_string(), 16..19) // not inclusive -// }; -// -// let actual = get_completable_tag(&text.chars().collect_vec(), insert_position); -// -// -// assert_eq!(Some(expected), actual); -// -// -// -// // 0 1 2 -// // 01234567890123456789012345678 -// let text = "text over here #tag more text"; -// -// let insert_position = 20; -// -// let actual = get_completable_tag(&text.chars().collect_vec(), insert_position); -// -// -// assert_eq!(None, actual); -// -// -// // 0 1 2 -// // 01234567890123456789012345678 -// let text = "text over here # more text"; -// -// let insert_position = 16; -// -// let actual = get_completable_tag(&text.chars().collect_vec(), insert_position); -// -// let expected = Some(CompletableTag { -// full_range: 15..16, -// inputted_tag: ("".to_string(), 16..16) -// }); -// -// -// assert_eq!(expected, actual); -// -// -// // 0 1 2 -// // 01234567890123456789012345678 -// let text = "text over here #tag mor #tag "; -// -// let insert_position = 28; -// -// let expected = CompletableTag { -// full_range: 24..28, -// inputted_tag: ("tag".to_string(), 25..28) // not inclusive -// }; -// -// let actual = get_completable_tag(&text.chars().collect_vec(), insert_position); -// -// -// assert_eq!(Some(expected), actual); -// -// -// } -// } - -fn run_completer<'a, T: Completer<'a>>( - context: Context<'a>, - line: u32, - character: u32, -) -> Option { - let completer = T::construct(context, line as usize, character as usize)?; - let completions = completer.completions(); - - let completions = completions - .into_iter() - .take(20) - .flat_map(|completable| { - completable - .completions(&completer) - .into_iter() - .collect::>() - .into_iter() - }) - .collect::>(); - - Some(CompletionResponse::List(CompletionList { - is_incomplete: true, - items: completions, - })) -} diff --git a/src/completion/shared_traits.rs b/src/completion/shared_traits.rs new file mode 100644 index 00000000..a7cf09a6 --- /dev/null +++ b/src/completion/shared_traits.rs @@ -0,0 +1,9 @@ + + +pub (super) trait Parser { + +} + +pub (super) trait Querier { + +} diff --git a/src/completion/tag_completer.rs b/src/completion/tag_completer.rs deleted file mode 100644 index 2be03572..00000000 --- a/src/completion/tag_completer.rs +++ /dev/null @@ -1,175 +0,0 @@ -use std::path::Path; - -use itertools::Itertools; -use once_cell::sync::Lazy; -use regex::Regex; -use tower_lsp::lsp_types::{ - CompletionItem, CompletionItemKind, CompletionItemLabelDetails, CompletionTextEdit, - Documentation, Position, Range, TextEdit, -}; - -use crate::{ - completion::util::check_in_code_block, - ui, - vault::{MDTag, Referenceable, Vault}, -}; - -use super::{ - matcher::{fuzzy_match_completions, Matchable}, - Completable, Completer, LineRange, -}; - -use rayon::prelude::*; - -pub struct TagCompleter<'a> { - full_range: LineRange, - /// Tag name and range not including the '#' - inputted_tag: (String, LineRange), - vault: &'a Vault, - line: usize, - character: usize, -} - -impl<'a> Completer<'a> for TagCompleter<'a> { - fn construct(context: super::Context<'a>, line: usize, character: usize) -> Option - where - Self: Sized + Completer<'a>, - { - if context.settings.tags_in_codeblocks == false - && check_in_code_block(&context, line, character) - { - return None; - } - - static PARTIAL_TAG_REGEX: Lazy = - Lazy::new(|| Regex::new(r"\#(?[a-zA-Z0-9\/]*)").unwrap()); - - let line_chars = context.vault.select_line(context.path, line as isize)?; - let line_string = String::from_iter(line_chars); - - let captures_iter = PARTIAL_TAG_REGEX.captures_iter(&line_string); - - captures_iter - .flat_map(|captures| { - let (full, tag_text) = (captures.get(0)?, captures.name("text")?); - - // check if the cursor is in the tag - let preceding_character = character - 1; // User is inserting into the position after the character they are looking at; "#tag|" cursor is a position 4; I want pos 3; the end of the tag - if preceding_character >= full.range().start - && preceding_character < full.range().end - { - // end is exclusive - Some(TagCompleter { - full_range: full.range(), - inputted_tag: (tag_text.as_str().to_string(), tag_text.range()), - vault: context.vault, - line, - character, - }) - } else { - None - } - }) - .next() - } - - fn completions(&self) -> Vec> - where - Self: Sized, - { - let tag_referenceables = self - .vault - .select_referenceable_nodes(None) - .into_par_iter() - .flat_map(TagCompletable::from_referenceable) - .filter(|tag| { - !(tag.tag.1.range.start.line <= self.line as u32 - && tag.tag.1.range.start.character <= self.character as u32 - && tag.tag.1.range.end.line >= self.line as u32 - && tag.tag.1.range.end.character >= self.character as u32) - }) - .collect::>(); - - // uniqued - let tag_referenceables = tag_referenceables - .into_iter() - .unique_by(|tag| tag.match_string().to_owned()) - .collect::>(); - - let filter_text = &self.inputted_tag.0; - - let filtered = fuzzy_match_completions(filter_text, tag_referenceables); - - filtered - } - - type FilterParams = &'a str; - - fn completion_filter_text(&self, params: Self::FilterParams) -> String { - format!("#{}", params) - } -} - -struct TagCompletable<'a> { - tag: (&'a Path, &'a MDTag), -} - -impl TagCompletable<'_> { - fn from_referenceable(referenceable: Referenceable<'_>) -> Option> { - match referenceable { - Referenceable::Tag(path, tag) => Some(TagCompletable { tag: (path, tag) }), - _ => None, - } - } -} - -impl Matchable for TagCompletable<'_> { - fn match_string(&self) -> &str { - &self.tag.1.tag_ref - } -} - -impl<'a> Completable<'a, TagCompleter<'a>> for TagCompletable<'a> { - fn completions(&self, completer: &TagCompleter<'a>) -> Option { - let text_edit = CompletionTextEdit::Edit(TextEdit { - new_text: format!("#{}", self.tag.1.tag_ref), - range: Range { - start: Position { - line: completer.line as u32, - character: completer.full_range.start as u32, - }, - end: Position { - line: completer.line as u32, - character: completer.full_range.end as u32, - }, - }, - }); - - let path = self.tag.0; - let path_buf = path.to_path_buf(); - let self_as_referenceable = Referenceable::Tag(&path_buf, self.tag.1); - - let num_references = completer - .vault - .select_references_for_referenceable(&self_as_referenceable) - .map(|references| references.len()) - .unwrap_or(0); - - Some(CompletionItem { - label: self.tag.1.tag_ref.clone(), - kind: Some(CompletionItemKind::KEYWORD), - filter_text: Some(completer.completion_filter_text(&self.tag.1.tag_ref.clone())), - documentation: ui::preview_referenceable(completer.vault, &self_as_referenceable) - .map(Documentation::MarkupContent), - label_details: Some(CompletionItemLabelDetails { - detail: Some(match num_references { - 1 => "1 reference".to_string(), - n => format!("{} references", n), - }), - description: None, - }), - text_edit: Some(text_edit), - ..Default::default() - }) - } -} diff --git a/src/completion/unindexed_block_completer.rs b/src/completion/unindexed_block_completer.rs deleted file mode 100644 index dd606f24..00000000 --- a/src/completion/unindexed_block_completer.rs +++ /dev/null @@ -1,306 +0,0 @@ -use itertools::Itertools; -use rayon::prelude::*; -use tower_lsp::lsp_types::{ - Command, CompletionItem, CompletionItemKind, CompletionItemLabelDetails, Documentation, - InsertTextFormat, MarkupContent, MarkupKind, Position, Range, TextEdit, Url, -}; - -use crate::{ - ui::preview_referenceable, - vault::{get_obsidian_ref_path, Block, Referenceable}, -}; -use nanoid::nanoid; - -use super::{ - link_completer::{LinkCompleter, MarkdownLinkCompleter, WikiLinkCompleter}, - matcher::{fuzzy_match_completions, Matchable}, - Completable, Completer, -}; - -pub struct UnindexedBlockCompleter<'a, T: LinkCompleter<'a>> { - link_completer: T, - new_id: String, - __phantom: std::marker::PhantomData<&'a T>, -} - -impl<'a, C: LinkCompleter<'a>> UnindexedBlockCompleter<'a, C> { - fn from_link_completer(link_completer: C) -> Option> { - if link_completer.entered_refname().starts_with(' ') { - Some(UnindexedBlockCompleter::new(link_completer)) - } else { - None - } - } - - fn new(completer: C) -> Self { - let rand_id = nanoid!( - 5, - &['a', 'b', 'c', 'd', 'e', 'f', 'g', '1', '2', '3', '4', '5', '6', '7', '8', '9'] - ); - - Self { - link_completer: completer, - new_id: rand_id, - __phantom: std::marker::PhantomData, - } - } - - fn completables(&self) -> Vec> { - let blocks = self.link_completer.vault().select_blocks(); - let position = self.link_completer.position(); - - blocks - .into_par_iter() - .filter(|block| { - !(block.range.start.line <= position.line - && block.range.start.character <= position.character - && block.range.end.line >= position.line - && block.range.end.character >= position.character) - }) - .map(UnindexedBlock) - .collect::>() - } - - fn grep_match_text(&self) -> String { - self.link_completer.entered_refname() - } -} - -impl<'a> Completer<'a> for UnindexedBlockCompleter<'a, MarkdownLinkCompleter<'a>> { - fn construct(context: super::Context<'a>, line: usize, character: usize) -> Option - where - Self: Sized, - { - let markdown_link_completer = MarkdownLinkCompleter::construct(context, line, character)?; - - Self::from_link_completer(markdown_link_completer) - } - - fn completions( - &self, - ) -> Vec>>> - where - Self: Sized, - { - let completables = self.completables(); - - let grep_match_text = self.grep_match_text(); - - let matches = fuzzy_match_completions(&grep_match_text, completables); - - matches - } - - type FilterParams = as Completer<'a>>::FilterParams; - fn completion_filter_text(&self, params: Self::FilterParams) -> String { - self.link_completer.completion_filter_text(params) - } -} - -impl<'a> Completer<'a> for UnindexedBlockCompleter<'a, WikiLinkCompleter<'a>> { - fn construct(context: super::Context<'a>, line: usize, character: usize) -> Option - where - Self: Sized, - { - let wiki_link_completer = WikiLinkCompleter::construct(context, line, character)?; - - UnindexedBlockCompleter::from_link_completer(wiki_link_completer) - } - - fn completions(&self) -> Vec> - where - Self: Sized, - { - let completables = self.completables(); - let filter_text = self.grep_match_text(); - let matches = fuzzy_match_completions(&filter_text, completables); - - matches - } - - type FilterParams = as Completer<'a>>::FilterParams; - fn completion_filter_text(&self, params: Self::FilterParams) -> String { - self.link_completer.completion_filter_text(params) - } -} - -struct UnindexedBlock<'a>(Block<'a>); - -impl<'a> UnindexedBlock<'a> { - /// Return the refname and completion item - fn partial_completion>( - &self, - completer: &'a UnindexedBlockCompleter<'a, T>, - ) -> Option<(String, CompletionItem)> { - let rand_id = &completer.new_id; - - let path_ref = - get_obsidian_ref_path(completer.link_completer.vault().root_dir(), self.0.file)?; - let url = Url::from_file_path(self.0.file).ok()?; - - let block = self.0; - - // check if the block is already indexed - let (documentation, command, kind, label_detail, refname): ( - Option, - Option, - CompletionItemKind, - Option, - String, - ) = match completer - .link_completer - .vault() - .select_referenceable_nodes(Some(block.file)) - .into_par_iter() - .find_any(|referenceable| match referenceable { - Referenceable::IndexedBlock(_path, indexed_block) => { - indexed_block.range.start.line == block.range.start.line - } - _ => false, - }) { - Some(ref referenceable @ Referenceable::IndexedBlock(_, indexed_block)) => ( - preview_referenceable(completer.link_completer.vault(), referenceable) - .map(Documentation::MarkupContent), - None, - CompletionItemKind::REFERENCE, - Some(CompletionItemLabelDetails { - detail: Some("Indexed Block".to_string()), - description: None, - }), - format!("{}#^{}", path_ref, indexed_block.index), - ), - _ => ( - Some(Documentation::MarkupContent(MarkupContent { - kind: MarkupKind::Markdown, - value: (block.range.start.line as isize - 1 - ..=block.range.start.line as isize + 1) - .flat_map(|i| { - Some(( - completer - .link_completer - .vault() - .select_line(block.file, i)?, - i, - )) - }) - .map(|(iter, ln)| { - if ln == block.range.start.line as isize { - format!("**{}**\n", String::from_iter(iter).trim()) - // highlight the block to be references - } else { - String::from_iter(iter) - } - }) - .join(""), - })), - Some(Command { - title: "Insert Block Reference Into File".into(), - command: "apply_edits".into(), - arguments: Some(vec![serde_json::to_value( - tower_lsp::lsp_types::WorkspaceEdit { - changes: Some( - vec![( - url, - vec![TextEdit { - range: Range { - start: Position { - line: block.range.end.line, - character: block.range.end.character - 1, - }, - end: Position { - line: block.range.end.line, - character: block.range.end.character - 1, - }, - }, - new_text: format!(" ^{}", rand_id), - }], - )] - .into_iter() - .collect(), - ), - change_annotations: None, - document_changes: None, - }, - ) - .ok()?]), - }), - CompletionItemKind::TEXT, - None, - format!("{}#^{}", path_ref, rand_id), - ), - }; - - Some(( - refname, - CompletionItem { - label: block.text.to_string(), - documentation, - // Insert the index for the block - command, - kind: Some(kind), - label_details: label_detail, - ..Default::default() - }, - )) - } -} - -impl<'a> Completable<'a, UnindexedBlockCompleter<'a, MarkdownLinkCompleter<'a>>> - for UnindexedBlock<'a> -{ - fn completions( - &self, - completer: &UnindexedBlockCompleter<'a, MarkdownLinkCompleter<'a>>, - ) -> Option { - let (refname, partial_completion) = self.partial_completion(completer)?; - - let binding = completer.link_completer.entered_refname(); - let display = &binding.trim(); - - Some(CompletionItem { - text_edit: Some( - completer - .link_completer - .completion_text_edit(Some(&format!("${{1:{}}}", display)), &refname), - ), - filter_text: Some( - completer.completion_filter_text(&completer.link_completer.entered_refname()), - ), - insert_text_format: Some(InsertTextFormat::SNIPPET), - ..partial_completion - }) - } -} - -impl<'a> Completable<'a, UnindexedBlockCompleter<'a, WikiLinkCompleter<'a>>> - for UnindexedBlock<'a> -{ - fn completions( - &self, - completer: &UnindexedBlockCompleter<'a, WikiLinkCompleter<'a>>, - ) -> Option { - let (refname, partial_completion) = self.partial_completion(completer)?; - - let binding = completer.link_completer.entered_refname(); - let display = &binding.trim(); - - Some(CompletionItem { - text_edit: Some( - completer - .link_completer - .completion_text_edit(Some(&format!("${{1:{}}}", display)), &refname), - ), - filter_text: Some( - completer.completion_filter_text(&completer.link_completer.entered_refname()), - ), - insert_text_format: Some(InsertTextFormat::SNIPPET), - ..partial_completion - }) - } -} - -impl Matchable for UnindexedBlock<'_> { - fn match_string(&self) -> &str { - self.0.text - } -} diff --git a/src/completion/util.rs b/src/completion/util.rs deleted file mode 100644 index 9114fba0..00000000 --- a/src/completion/util.rs +++ /dev/null @@ -1,18 +0,0 @@ -use tower_lsp::lsp_types::Position; - -use crate::vault::Rangeable as _; - -use super::Context; - -pub fn check_in_code_block(context: &Context, line: usize, character: usize) -> bool { - let in_code_block = context.vault.md_files.get(context.path).is_some_and(|it| { - it.codeblocks.iter().any(|block| { - block.includes_position(Position { - line: line as u32, - character: character as u32, - }) - }) - }); - - in_code_block -}