From 3b9928131312bd4f8bc65b19e9fc0739f9f2febb Mon Sep 17 00:00:00 2001 From: Felix Zeller Date: Thu, 7 Dec 2023 13:14:32 -0500 Subject: [PATCH] Refactoring and adding support for tags --- TestFiles/Another Test.md | 3 + TestFiles/Test.md | 4 + TestFiles/folder/File.md | 2 + src/gotodef.rs | 9 +- src/main.rs | 2 +- src/references.rs | 7 +- src/vault.rs | 218 ++++++++++++++++++++++++++------------ 7 files changed, 169 insertions(+), 76 deletions(-) diff --git a/TestFiles/Another Test.md b/TestFiles/Another Test.md index ae921ada..d4ee6803 100644 --- a/TestFiles/Another Test.md +++ b/TestFiles/Another Test.md @@ -18,3 +18,6 @@ This file is popppinnng, 2 links to it. [[folder/File#Heading]] [[Test#Heading 1 2]] [[Test#Heading 1 2]] + +#tag + diff --git a/TestFiles/Test.md b/TestFiles/Test.md index 4c2da8a3..ac7288b5 100644 --- a/TestFiles/Test.md +++ b/TestFiles/Test.md @@ -17,3 +17,7 @@ # Heading 1 2 [[Another Test]] + + +#tag + diff --git a/TestFiles/folder/File.md b/TestFiles/folder/File.md index c002b308..d8574229 100644 --- a/TestFiles/folder/File.md +++ b/TestFiles/folder/File.md @@ -1,3 +1,5 @@ [[Another Test]] # Heading + +#tag diff --git a/src/gotodef.rs b/src/gotodef.rs index d6895329..1f96aa78 100644 --- a/src/gotodef.rs +++ b/src/gotodef.rs @@ -4,9 +4,9 @@ use tower_lsp::lsp_types::{Position, Url, Location}; use crate::vault::Vault; -pub fn goto_definition(vault: &Vault, cursor_position: Position, path: &Path) -> Option { +pub fn goto_definition(vault: &Vault, cursor_position: Position, path: &Path) -> Option> { // First, find the link that the cursor is in. Get a links for the file and match the cursor position up to one of them - let links = vault.select_links(Some(&path))?; + let links = vault.select_references(Some(&path))?; let cursors_link = links.iter().find(|&l| l.1.range.start.line <= cursor_position.line && l.1.range.end.line >= cursor_position.line && @@ -19,8 +19,7 @@ pub fn goto_definition(vault: &Vault, cursor_position: Position, path: &Path) -> // Lets get all of the linkable nodes let positions = vault.select_linkable_nodes(); - let referenced_linkable = positions.iter().find(|i| i.get_refname(&vault.root_dir()).as_ref() == Some(&reference_text))?; - let path_as_str = referenced_linkable.get_path().to_str()?; + let referenced_linkables = positions.iter().filter(|i| i.get_refname(&vault.root_dir()).as_ref() == Some(&reference_text)); - return Some(Location { uri: Url::from_file_path(path_as_str).unwrap(), range: referenced_linkable.get_range() }) + return Some(referenced_linkables.filter_map(|linkable| Some(Location{uri: Url::from_file_path(linkable.get_path().to_str()?).unwrap(), range: linkable.get_range()})).collect()); } diff --git a/src/main.rs b/src/main.rs index b5727b26..15bf4a6a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -94,7 +94,7 @@ impl LanguageServer for Backend { let result = goto_definition(&vault, position, &path); - return Ok(result.map(|l| GotoDefinitionResponse::Scalar(l))) + return Ok(result.map(|l| GotoDefinitionResponse::Array(l))) } diff --git a/src/references.rs b/src/references.rs index 661edcf4..3ae0217b 100644 --- a/src/references.rs +++ b/src/references.rs @@ -3,7 +3,7 @@ use std::path::Path; use itertools::Itertools; use tower_lsp::lsp_types::{Position, Location, Url}; -use crate::vault::{Vault, Linkable}; +use crate::vault::{Vault, Referenceable}; pub fn references(vault: &Vault, cursor_position: Position, path: &Path) -> Option> { // First we need to get the linkable node under the cursor @@ -19,7 +19,7 @@ pub fn references(vault: &Vault, cursor_position: Position, path: &Path) -> Opti )?; - let references = vault.select_links(None)?; + let references = vault.select_references(None)?; let locations = |reference_text| references.iter() .filter(move |r| r.1.reference_text == reference_text) .map(|link| Url::from_file_path(link.0).map(|good| Location {uri: good, range: link.1.range})) @@ -30,8 +30,9 @@ pub fn references(vault: &Vault, cursor_position: Position, path: &Path) -> Opti .flatten(); return match linkable { - Linkable::MDFile(path, md) => { + Referenceable::File(path, md) => { return Some(linkable_nodes.iter() + .filter(|&referenceable| !matches!(referenceable, &Referenceable::Tag(_, _))) .filter_map(|linkable| linkable.get_refname(vault.root_dir())) .map(|refname| locations(refname)) .flatten() diff --git a/src/vault.rs b/src/vault.rs index 63808720..129e3f07 100644 --- a/src/vault.rs +++ b/src/vault.rs @@ -43,32 +43,36 @@ pub fn reconstruct_vault(old: &mut Vault, new_file: (&Path, &str)) { fn parse_obsidian_md(text: &str) -> MDFile { - let links = parse_obsidian_links(text); + let links = parse_obsidian_references(text); let headings = parse_obsidian_headings(text); let indexed_blocks = parse_obsidian_indexed_blocks(text); + let tags = parse_obsidian_tags(text); - return MDFile { links, headings, indexed_blocks } + return MDFile { references: links, headings, indexed_blocks, tags } } -fn parse_obsidian_links(text: &str) -> Vec { +/// Parse out the references to linkables in each file. This will have links to files and tags +fn parse_obsidian_references(text: &str) -> Vec { static LINK_RE: Lazy = Lazy::new(|| Regex::new(r"\[\[(?[.[^\[\]\|]]+)(\|(?[.[^\[\]|]]+))?\]\]").unwrap() ); // A [[link]] that does not have any [ or ] in it - let links: Vec = LINK_RE.captures_iter(text) + let links: Vec = LINK_RE.captures_iter(text) .flat_map(|capture| match (capture.get(0), capture.name("referencetext"), capture.name("display")) { (Some(full), Some(reference_text), display) => Some((full, reference_text, display)), _ => None }) .map(|(outer, re_match, display)| { - Link { + Reference { reference_text: re_match.as_str().into(), range: range_to_position(&Rope::from_str(text), outer.range()), display_text: display.map(|d| d.as_str().into()) }}) .collect_vec(); - return links + let tags: Vec = parse_obsidian_tags(text).iter().map(|tag| Reference {display_text: None, range: tag.range, reference_text: format!("#{}", tag.tag_ref)}).collect(); + + return links.into_iter().chain(tags.into_iter()).collect_vec() } fn parse_obsidian_headings(text: &str) -> Vec { @@ -112,6 +116,26 @@ fn parse_obsidian_indexed_blocks(text: &str) -> Vec { return indexed_blocks } // Make this better identify the full blocks +fn parse_obsidian_tags(text: &str) -> Vec { + static TAG_RE: Lazy = Lazy::new(|| Regex::new(r"(\n|\A| )(?#(?[.[^ \n\#]]+))(\n|\z| )").unwrap()); + + + let tagged_blocks = TAG_RE.captures_iter(&text.to_string()) + .flat_map(|c| match (c.name("full"), c.name("tag")) { + (Some(full), Some(index)) => Some((full, index)), + _ => None + }) + .map(|(full, index)| { + MDTag { + tag_ref: index.as_str().into(), + range: range_to_position(&Rope::from_str(text), full.range()) + } + }) + .collect_vec(); + + return tagged_blocks +} + fn range_to_position(rope: &Rope, range: Range) -> tower_lsp::lsp_types::Range { // convert from byte offset to char offset let char_start = rope.byte_to_char(range.start); @@ -143,93 +167,104 @@ pub struct Vault { #[derive(Debug)] /// Linkable algebreic type that easily allows for new linkable nodes to be added if necessary and everything in it should live the same amount because it is all from vault /// These will also use the current obsidian syntax to come up with reference names for the linkables. These are the things that are using in links ([[Refname]]) -pub enum Linkable<'a> { - MDFile(&'a PathBuf, &'a MDFile), +pub enum Referenceable<'a> { + File(&'a PathBuf, &'a MDFile), Heading(&'a PathBuf, &'a MDHeading), - IndexedBlock(&'a PathBuf, &'a MDIndexedBlock) + IndexedBlock(&'a PathBuf, &'a MDIndexedBlock), + Tag(&'a PathBuf, &'a MDTag) } fn get_obsidian_ref_path(root_dir: &Path, path: &Path) -> Option { diff_paths(path, root_dir).and_then(|diff| diff.with_extension("").to_str().map(|refname| String::from(refname))) } -impl Linkable<'_> { +impl Referenceable<'_> { pub fn get_refname(&self, root_dir: &Path) -> Option { match self { - &Linkable::MDFile(path, _) => get_obsidian_ref_path(root_dir, path), - &Linkable::Heading(path, heading) => get_obsidian_ref_path(root_dir, path).and_then(|refpath| Some(format!("{}#{}", refpath, heading.heading_text))), - &Linkable::IndexedBlock(path, heading) => get_obsidian_ref_path(root_dir, path).and_then(|refpath| Some(format!("{}#^{}", refpath, heading.index))) + &Referenceable::File(path, _) => get_obsidian_ref_path(root_dir, path), + &Referenceable::Heading(path, heading) => get_obsidian_ref_path(root_dir, path).and_then(|refpath| Some(format!("{}#{}", refpath, heading.heading_text))), + &Referenceable::IndexedBlock(path, heading) => get_obsidian_ref_path(root_dir, path).and_then(|refpath| Some(format!("{}#^{}", refpath, heading.index))), + &Referenceable::Tag(_, tag) => Some(format!("#{}", tag.tag_ref)) } } pub fn get_path(&self) -> &PathBuf { match self { - &Linkable::MDFile(path, _) => path, - &Linkable::Heading(path, _) => path, - &Linkable::IndexedBlock(path, _) => path + &Referenceable::File(path, _) => path, + &Referenceable::Heading(path, _) => path, + &Referenceable::IndexedBlock(path, _) => path, + &Referenceable::Tag(path, _) => path } } pub fn get_range(&self) -> tower_lsp::lsp_types::Range { match self { - &Linkable::MDFile(_, _) => tower_lsp::lsp_types::Range { start: Position { line: 0, character: 0 }, end: Position { line: 0, character: u32::MAX} }, - &Linkable::Heading(_, heading) => heading.range, - &Linkable::IndexedBlock(_, indexed_block) => indexed_block.range + &Referenceable::File(_, _) => tower_lsp::lsp_types::Range { start: Position { line: 0, character: 0 }, end: Position { line: 0, character: u32::MAX} }, + &Referenceable::Heading(_, heading) => heading.range, + &Referenceable::IndexedBlock(_, indexed_block) => indexed_block.range, + &Referenceable::Tag(_, tag) => tag.range } } } impl Vault { - /// Select all links ([[Link]]) in a file - pub fn select_links<'a>(&'a self, path: Option<&'a Path>) -> Option> { + /// Select all references ([[link]] or #tag) in a file if path is some, else select all references in the vault. + pub fn select_references<'a>(&'a self, path: Option<&'a Path>) -> Option> { match path { - Some(path) => self.files.get(path).map(|md| &md.links).map(|vec| vec.iter().map(|i| (path, i)).collect()), - None => Some(self.files.iter().map(|(path, md)| md.links.iter().map(|link| (path.as_path(), link))).flatten().collect()) + Some(path) => self.files.get(path).map(|md| &md.references).map(|vec| vec.iter().map(|i| (path, i)).collect()), + None => Some(self.files.iter().map(|(path, md)| md.references.iter().map(|link| (path.as_path(), link))).flatten().collect()) } } // TODO: less cloning? /// Select all linkable positions in the vault - pub fn select_linkable_nodes<'a>(&'a self) -> Vec> { + pub fn select_linkable_nodes<'a>(&'a self) -> Vec> { let files = self.files.iter() - .map(|(path, md)| Linkable::MDFile(path, md)); + .map(|(path, md)| Referenceable::File(path, md)); let headings = self.files.iter() .flat_map(|(path, md)| md.headings.iter().map(move |h| (path, h))) - .map(|(path, h)| Linkable::Heading(path, h)); + .map(|(path, h)| Referenceable::Heading(path, h)); let indexed_blocks = self.files.iter() .flat_map(|(path, md)| md.indexed_blocks.iter().map(move |ib| (path, ib))) - .map(|(path, ib)| Linkable::IndexedBlock(path, ib)); + .map(|(path, ib)| Referenceable::IndexedBlock(path, ib)); - return files.into_iter().chain(headings).into_iter().chain(indexed_blocks).collect() + let tags = self.files.iter() + .flat_map(|(path, md)| md.tags.iter().map(move |tag| (path, tag))) + .map(|(path, tag)| Referenceable::Tag(path, tag)); + + return files.into_iter().chain(headings).chain(indexed_blocks).chain(tags).collect() } - pub fn select_linkable_nodes_for_path<'a>(&'a self, path: &'a PathBuf) -> Option>> { + pub fn select_linkable_nodes_for_path<'a>(&'a self, path: &'a PathBuf) -> Option>> { let file = self.files.get(path)?; - let file_linkable = Linkable::MDFile(path, file); + let file_linkable = Referenceable::File(path, file); - let headings = file.headings.iter().map(|h| Linkable::Heading(path, h)); + let headings = file.headings.iter().map(|h| Referenceable::Heading(path, h)); - let indexed_blocks = file.indexed_blocks.iter().map(|ib| Linkable::IndexedBlock(path, ib)); + let indexed_blocks = file.indexed_blocks.iter().map(|ib| Referenceable::IndexedBlock(path, ib)); - return Some(vec![file_linkable].into_iter().chain(headings).into_iter().chain(indexed_blocks).collect()) - } + let tags = file.tags.iter().map(|tag| Referenceable::Tag(path, tag)); + + return Some(vec![file_linkable].into_iter().chain(headings).chain(indexed_blocks).chain(tags).collect()) + } // TODO: Fix this design. Duplication is bad and I want to require that all references for all types of Referenceable are searched for. pub fn root_dir(&self) -> &PathBuf { &self.root_dir } } -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, Default)] pub struct MDFile { - links: Vec, + references: Vec, headings: Vec, - indexed_blocks: Vec + indexed_blocks: Vec, + tags: Vec } #[derive(Debug, PartialEq, Eq, Default, Clone)] -pub struct Link { +pub struct Reference { pub reference_text: String, pub display_text: Option, pub range: tower_lsp::lsp_types::Range @@ -247,38 +282,44 @@ pub struct MDIndexedBlock { range: tower_lsp::lsp_types::Range } + +#[derive(Debug, PartialEq, Eq)] +pub struct MDTag { + tag_ref: String, + range: tower_lsp::lsp_types::Range +} + // tests #[cfg(test)] mod vault_tests { use std::path::{Path, PathBuf}; - use ropey::Rope; use tower_lsp::lsp_types::{Position, Range, Location, Url}; - use crate::{vault::parse_obsidian_headings, gotodef::goto_definition}; + use crate::{vault::{parse_obsidian_headings, parse_obsidian_tags}, gotodef::goto_definition}; - use super::{Link, parse_obsidian_links, MDHeading, parse_obsidian_indexed_blocks, MDIndexedBlock, Linkable, MDFile, construct_vault}; + use super::{Reference, parse_obsidian_references, MDHeading, parse_obsidian_indexed_blocks, MDIndexedBlock, Referenceable, MDFile, construct_vault, MDTag}; #[test] fn link_parsing() { let text = "This is a [[link]] [[link 2]]\n[[link 3]]"; - let parsed = parse_obsidian_links(text); + let parsed = parse_obsidian_references(text); let expected = vec![ - Link { + Reference { reference_text: "link".into(), range: tower_lsp::lsp_types::Range { start: tower_lsp::lsp_types::Position { line: 0, character: 10 }, end: tower_lsp::lsp_types::Position { line: 0, character: 18 } }, - ..Link::default() + ..Reference::default() }, - Link { + Reference { reference_text: "link 2".into(), range: tower_lsp::lsp_types::Range { start: tower_lsp::lsp_types::Position { line: 0, character: 19 }, end: tower_lsp::lsp_types::Position { line: 0, character: 29} }, - ..Link::default() + ..Reference::default() }, - Link { + Reference { reference_text: "link 3".into(), range: tower_lsp::lsp_types::Range { start: tower_lsp::lsp_types::Position { line: 1, character: 0 }, end: tower_lsp::lsp_types::Position { line: 1, character: 10 } }, - ..Link::default() + ..Reference::default() } ]; @@ -288,20 +329,20 @@ mod vault_tests { #[test] fn link_parsin_with_display_text() { let text = "This is a [[link|but called different]] [[link 2|222]]\n[[link 3|333]]"; - let parsed = parse_obsidian_links(text); + let parsed = parse_obsidian_references(text); let expected = vec![ - Link { + Reference { reference_text: "link".into(), range: tower_lsp::lsp_types::Range { start: tower_lsp::lsp_types::Position { line: 0, character: 10 }, end: tower_lsp::lsp_types::Position { line: 0, character: 39 } }, display_text: Some("but called different".into()), }, - Link { + Reference { reference_text: "link 2".into(), range: tower_lsp::lsp_types::Range { start: tower_lsp::lsp_types::Position { line: 0, character: 40 }, end: tower_lsp::lsp_types::Position { line: 0, character: 54} }, display_text: Some("222".into()), }, - Link { + Reference { reference_text: "link 3".into(), range: tower_lsp::lsp_types::Range { start: tower_lsp::lsp_types::Position { line: 1, character: 0 }, end: tower_lsp::lsp_types::Position { line: 1, character: 14 } }, display_text: Some("333".into()), @@ -369,8 +410,8 @@ more text fn test_linkable_reference() { let path = Path::new("/home/vault/test.md"); let path_buf = path.to_path_buf(); - let md_file = MDFile{indexed_blocks: vec![], headings: vec![], links: vec![]}; - let linkable: Linkable = Linkable::MDFile(&path_buf, &md_file); + let md_file = MDFile::default(); + let linkable: Referenceable = Referenceable::File(&path_buf, &md_file); let root_dir = Path::new("/home/vault"); let refname = linkable.get_refname(root_dir); @@ -383,7 +424,7 @@ more text let path = Path::new("/home/vault/test.md"); let path_buf = path.to_path_buf(); let md_heading = MDHeading{heading_text: "Test Heading".into(), range: tower_lsp::lsp_types::Range::default()}; - let linkable: Linkable = Linkable::Heading(&path_buf, &md_heading); + let linkable: Referenceable = Referenceable::Heading(&path_buf, &md_heading); let root_dir = Path::new("/home/vault"); let refname = linkable.get_refname(root_dir); @@ -398,7 +439,7 @@ more text let path = Path::new("/home/vault/test.md"); let path_buf = path.to_path_buf(); let md_indexed_block = MDIndexedBlock{index: "12345".into(), range: tower_lsp::lsp_types::Range::default()}; - let linkable: Linkable = Linkable::IndexedBlock(&path_buf, &md_indexed_block); + let linkable: Referenceable = Referenceable::IndexedBlock(&path_buf, &md_indexed_block); let root_dir = Path::new("/home/vault"); let refname = linkable.get_refname(root_dir); @@ -410,23 +451,23 @@ more text #[test] fn parsing_special_text() { let text = "’’’󰌶 is a [[link]] [[link 2]]\n[[link 3]]"; - let parsed = parse_obsidian_links(text); + let parsed = parse_obsidian_references(text); let expected = vec![ - Link { + Reference { reference_text: "link".into(), range: tower_lsp::lsp_types::Range { start: tower_lsp::lsp_types::Position { line: 0, character: 10 }, end: tower_lsp::lsp_types::Position { line: 0, character: 18 } }, - ..Link::default() + ..Reference::default() }, - Link { + Reference { reference_text: "link 2".into(), range: tower_lsp::lsp_types::Range { start: tower_lsp::lsp_types::Position { line: 0, character: 19 }, end: tower_lsp::lsp_types::Position { line: 0, character: 29} }, - ..Link::default() + ..Reference::default() }, - Link { + Reference { reference_text: "link 3".into(), range: tower_lsp::lsp_types::Range { start: tower_lsp::lsp_types::Position { line: 1, character: 0 }, end: tower_lsp::lsp_types::Position { line: 1, character: 10 } }, - ..Link::default() + ..Reference::default() } ]; @@ -461,13 +502,13 @@ more text let mut result_path = root_dir.clone(); result_path.push("Another Test.md"); - let proper = Some(Location{ + let proper = Some(vec![Location{ uri: Url::from_file_path(result_path.to_str().unwrap()).unwrap(), range: Range { start: Position { line: 0, character: 0 }, end: Position { line: 0, character: u32::MAX } } - }); + }]); assert_eq!(result, proper); let position = Position {line: 6, character: 27}; @@ -477,13 +518,56 @@ more text let mut result_path = root_dir.clone(); result_path.push("Another Test.md"); - let proper = Some(Location{ + let proper = Some(vec![Location{ uri: Url::from_file_path(result_path.to_str().unwrap()).unwrap(), range: Range { start: Position { line: 2, character: 0 }, end: Position { line: 2, character: 24 } } - }); + }]); assert_eq!(result, proper); } + + #[test] + fn test_obsidian_tag() { + let text = r"# This is a heading + +This is a #tag + +and another #tag/ttagg + +and a third tag#notatag [[link#not a tag]] + +#MapOfContext/apworld +"; + let expected: Vec = vec![ + MDTag { + tag_ref: "tag".into(), + range: Range { + start: Position { line: 2, character: 10 }, + end: Position { line: 2, character: 14 } + } + }, + MDTag { + tag_ref: "tag/ttagg".into(), + range: Range { + start: Position { line: 4, character: 12 }, + end: Position { line: 4, character: 22 } + } + }, + MDTag { + tag_ref: "MapOfContext/apworld".into(), + range: Range { + start: Position { line: 8, character: 0 }, + end: Position { line: 8, character: 21 } + } + } + + ]; + + let parsed = parse_obsidian_tags(&text); + + assert_eq!(parsed, expected) + } } +