diff --git a/Cargo.lock b/Cargo.lock index 74fc34f9..cdf920ae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -433,6 +433,17 @@ dependencies = [ "slab", ] +[[package]] +name = "fuzzydate" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7265f35cc1f40c655aad829323a1daef5f21fd38904f6ed9bd5ec3df3cbd851c" +dependencies = [ + "chrono", + "lazy_static", + "thiserror", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -630,6 +641,7 @@ dependencies = [ "anyhow", "chrono", "config", + "fuzzydate", "indexmap", "itertools", "nanoid", diff --git a/Cargo.toml b/Cargo.toml index 0f8dfe0d..70851c5c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ edition = "2021" anyhow = "1.0.80" chrono = "0.4.35" config = "0.14.0" +fuzzydate = "0.2.2" indexmap = "2.2.6" itertools = "0.10.5" nanoid = "0.4.0" diff --git a/README.md b/README.md index 603f41f8..aaa756ea 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,30 @@ Markdown Oxide's features are implemented in the form of a language server aimin + -
+ (optional) Enable opening daily notes with natural langauge + + Modify your lsp `on_attach` function to support opening daily notes with, for example, `:Daily two days ago` or `:Daily next monday`. The specification can be found [here](https://docs.rs/fuzzydate/latest/fuzzydate/) + + ```lua + -- setup Markdown Oxide daily note commands + if client.name == "markdown_oxide" then + + vim.api.nvim_create_user_command( + "Daily", + function(args) + local input = args.args + + vim.lsp.buf.execute_command({command="jump", arguments={input}}) + + end, + {desc = 'Open daily note', nargs = "*"} + ) + end + ``` + +
+ ### VSCode diff --git a/src/commands.rs b/src/commands.rs new file mode 100644 index 00000000..8172b63d --- /dev/null +++ b/src/commands.rs @@ -0,0 +1,88 @@ +use std::fs::File; +use std::path::Path; + +use crate::config::Settings; +use chrono::offset::Local; +use chrono::NaiveDateTime; +use fuzzydate::parse; +use serde_json::Value; +use tower_lsp::jsonrpc::{Error, Result}; +use tower_lsp::lsp_types::{MessageType, ShowDocumentParams, Url}; + +fn datetime_to_file(datetime: NaiveDateTime, dailynote_format: &str, root_dir: &Path) -> Option { + let filename = datetime.format(dailynote_format).to_string(); + let path = root_dir.join(&filename); + + println!("path: {:?}", path); + + Url::from_file_path(path.with_extension("md")).ok() +} + +pub async fn jump( + client: &tower_lsp::Client, + root_dir: &Path, + settings: &Settings, + jump_to: Option<&str>, +) -> Result> { + // if jump_to is None, use the current time. + + let daily_note_format = &settings.dailynote; + let note_file = match jump_to { + Some(jmp_str) => parse(jmp_str) + .ok() + .and_then(|dt| datetime_to_file(dt, &daily_note_format, root_dir)), + None => datetime_to_file(Local::now().naive_local(), &daily_note_format, root_dir), + }; + + if let Some(uri) = note_file { + // file creation can fail and return an Err, ignore this and try + // to open the file on the off chance the client knows what to do + // TODO: log failure to create file + let _ = uri.to_file_path().map(|path| { + path.parent().map(|parent| std::fs::create_dir_all(parent)); + if !path.exists() { + let _ = File::create(path.as_path().to_owned()); + } + }); + + client + .show_document(ShowDocumentParams { + uri, + external: Some(false), + take_focus: Some(true), + selection: None, + }) + .await + .map(|success| Some(success.into())) + } else { + client + .log_message( + MessageType::ERROR, + format!("could not parse {jump_to:?}: {:?}", jump_to.map(parse)), + ) + .await; + Err(Error::invalid_params( + format!("Could not parse journal format ({jump_to:?}) as a valid uri: {:?}.", jump_to.map(parse)), + )) + } +} + +// tests +#[cfg(test)] +mod tests { + use fuzzydate::parse; + + use super::datetime_to_file; + + + #[test] + fn test_string_to_file() { + + let input = "today"; + + let parsed_datetime = parse(input).unwrap(); + + let _ = datetime_to_file(parsed_datetime, "%Y-%m-%d", &std::fs::canonicalize("./").unwrap()).unwrap(); + + } +} diff --git a/src/main.rs b/src/main.rs index d73567c6..d2a97d28 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,6 @@ use std::collections::HashSet; use std::ops::{Deref, DerefMut}; -use std::path::{Path, PathBuf}; -use std::str::FromStr; +use std::path::PathBuf; use std::sync::Arc; use completion::get_completions; @@ -23,6 +22,7 @@ use vault::Vault; mod codeactions; mod codelens; +mod commands; mod completion; mod config; mod diagnostics; @@ -291,9 +291,10 @@ impl Backend { #[tower_lsp::async_trait] impl LanguageServer for Backend { async fn initialize(&self, i: InitializeParams) -> Result { - let root_dir = match i.root_uri { - Some(uri) => uri.to_file_path().or(Err(Error::new(ErrorCode::InvalidParams)))?, + Some(uri) => uri + .to_file_path() + .or(Err(Error::new(ErrorCode::InvalidParams)))?, None => std::env::current_dir().or(Err(Error::new(ErrorCode::InvalidParams)))?, }; @@ -371,7 +372,7 @@ impl LanguageServer for Backend { resolve_provider: None, }), execute_command_provider: Some(ExecuteCommandOptions { - commands: vec!["apply_edits".into()], + commands: vec!["apply_edits".into(), "jump".into()], ..Default::default() }), semantic_tokens_provider: Some( @@ -585,6 +586,14 @@ impl LanguageServer for Backend { Ok(None) } + ExecuteCommandParams { command, .. } if *command == *"jump" => { + let jump_to = params.arguments.first().and_then(|val| val.as_str()); + let settings = self + .bind_settings(|settings| Ok(settings.to_owned())) + .await?; + let root_dir = self.bind_vault(|vault| Ok(vault.root_dir().to_owned())).await?; + commands::jump(&self.client, &root_dir, &settings, jump_to).await + } _ => Ok(None), } }