diff --git a/Cargo.lock b/Cargo.lock index ac5bc96..f5b7414 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -88,6 +88,21 @@ version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "annotate-snippets" version = "0.11.4" @@ -409,6 +424,7 @@ dependencies = [ "anyhow", "cargo-audit", "cargo-lock", + "chrono", "clap", "executable_path_finder", "futures", @@ -577,6 +593,20 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets 0.52.6", +] + [[package]] name = "clap" version = "4.5.20" @@ -2706,6 +2736,29 @@ dependencies = [ "tracing", ] +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -5168,6 +5221,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-registry" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index 1c7fefd..224ca2d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,7 @@ cargo-audit = { git = "https://github.com/washanhanzi/rustsec", branch = "main", cargo-lock = { git = "https://github.com/washanhanzi/rustsec", branch = "main", version = "10.0.1", features = [ "dependency-tree", ] } +chrono = "0.4.38" [features] default = [] diff --git a/src/config.rs b/src/config.rs index ac54ccb..2c13ca4 100644 --- a/src/config.rs +++ b/src/config.rs @@ -2,10 +2,15 @@ use once_cell::sync::Lazy; use serde::Deserialize; use std::sync::RwLock; -use crate::decoration::DecorationFormatter; +use crate::decoration::{CompiledFormatter, DecorationFormatter}; -#[derive(Default, Debug, Deserialize, Clone)] +#[derive(Default, Debug, Clone)] pub struct Config { + pub decoration_formatter: CompiledFormatter, +} + +#[derive(Default, Debug, Deserialize, Clone)] +pub struct UserConfig { #[serde(flatten)] pub renderer: RendererConfig, } @@ -19,15 +24,9 @@ pub struct RendererConfig { pub static GLOBAL_CONFIG: Lazy> = Lazy::new(|| RwLock::new(Config::default())); -pub fn initialize_config(mut config: Config) { - let mut global_config = GLOBAL_CONFIG.write().unwrap(); - *global_config = config; -} - -pub fn update_config(update_fn: F) -where - F: FnOnce(&mut Config), -{ +pub fn initialize_config(config: UserConfig) { let mut global_config = GLOBAL_CONFIG.write().unwrap(); - update_fn(&mut global_config); + *global_config = Config { + decoration_formatter: config.renderer.decoration_formatter.compile(), + }; } diff --git a/src/controller/appraiser.rs b/src/controller/appraiser.rs index a9c3621..21f1204 100644 --- a/src/controller/appraiser.rs +++ b/src/controller/appraiser.rs @@ -70,6 +70,7 @@ pub enum CargoDocumentEvent { Completion(Uri, Position, oneshot::Sender>), CargoDiagnostic(Uri, CargoError), Audited(AuditReports), + Parse(CargoTomlPayload), } pub struct CargoTomlPayload { @@ -123,7 +124,7 @@ impl Appraiser { }); //timer task - let mut debouncer = Debouncer::new(tx.clone(), 1000, 10000); + let mut debouncer = Debouncer::new(tx.clone(), 1000, 5000); debouncer.spawn(); //audit task @@ -289,6 +290,9 @@ impl Appraiser { CargoDocumentEvent::Closed(uri) => { if let Some(doc) = state.document_mut(&uri) { doc.mark_dirty(); + if let Err(e) = render_tx.send(DecorationEvent::Reset(uri)).await { + error!("render tx send reset error: {}", e); + } } } CargoDocumentEvent::CargoLockChanged => { @@ -362,11 +366,11 @@ impl Appraiser { error!("debounder send interactive error: {}", e); } } - CargoDocumentEvent::Opened(msg) | CargoDocumentEvent::Saved(msg) => { + CargoDocumentEvent::Parse(msg) => { if let Err(e) = audit_controller.send(&msg.uri).await { error!("audit controller send error: {}", e); }; - let doc = match state.reconsile(&msg.uri, &msg.text) { + match state.reconsile(&msg.uri, &msg.text) { Ok((doc, diff)) => { if diff.is_empty() && !doc.is_dirty() { continue; @@ -386,45 +390,31 @@ impl Appraiser { continue; } }; - - if let Some(uri) = doc.root_manifest.as_ref() { - if uri != &msg.uri { - if client_capabilities.can_read_file() { - let param = ReadFileParam { uri: uri.clone() }; - match client.send_request::(param).await { - Ok(content) => { - if let Err(e) = inner_tx - .send(CargoDocumentEvent::Opened( - CargoTomlPayload { - uri: uri.clone(), - text: content.content, - }, - )) - .await - { - error!("inner tx send error: {}", e); - } - } - Err(e) => { - error!("read file error: {}", e); - } - } + } + CargoDocumentEvent::Opened(msg) | CargoDocumentEvent::Saved(msg) => { + if let Err(e) = audit_controller.send(&msg.uri).await { + error!("audit controller send error: {}", e); + }; + let doc = match state.reconsile(&msg.uri, &msg.text) { + Ok((doc, diff)) => { + if diff.is_empty() && !doc.is_dirty() { + continue; } else { - //read file with os - let content = - std::fs::read_to_string(uri.path().as_str()).unwrap(); - if let Err(e) = inner_tx - .send(CargoDocumentEvent::Opened(CargoTomlPayload { - uri: uri.clone(), - text: content, - })) - .await - { - error!("inner tx send error: {}", e); - } + doc } } - } + Err(err) => { + for e in err { + let Some((id, diag)) = e.diagnostic() else { + continue; + }; + diagnostic_controller + .add_parse_diagnostic(&msg.uri, &id, diag) + .await; + } + continue; + } + }; if let Err(e) = debouncer .send_interactive(Ctx { @@ -437,8 +427,21 @@ impl Appraiser { } } CargoDocumentEvent::ReadyToResolve(ctx) => { + //delay audit + if let Err(e) = audit_controller.send(&ctx.uri).await { + error!("audit controller send error: {}", e); + }; if state.check_rev(&ctx.uri, ctx.rev) { - start_resolve(&ctx.uri, &mut state, &render_tx, &cargo_tx).await; + start_resolve( + &ctx.uri, + &mut state, + &render_tx, + &cargo_tx, + &inner_tx, + &client, + &client_capabilities, + ) + .await; } } CargoDocumentEvent::CargoResolved(mut output) => { @@ -454,96 +457,62 @@ impl Appraiser { for dep in doc.dependencies.values_mut() { let key = dep.toml_key(); - if doc.dirty_nodes.contains_key(&dep.id) { + if let Some(rev) = doc.dirty_nodes.get(&dep.id) { + if *rev > output.ctx.rev { + continue; + } // Take resolved out of the output.dependencies hashmap let maybe_resolved = output.dependencies.remove(&key); dep.resolved = maybe_resolved; let package_name = dep.package_name(); - let Some(summaries) = output.summaries.remove(package_name) else { + let Some(mut summaries) = output.summaries.remove(package_name) + else { continue; }; - dep.summaries = Some(summaries.clone()); - - if let Some(resolved) = dep.resolved.as_ref() { + if let (Some(resolved), Some(unresolved)) = + (dep.resolved.as_ref(), dep.unresolved.as_ref()) + { let installed = resolved.version().clone(); - let req_version = - dep.unresolved.as_ref().unwrap().version_req(); + let req_version = unresolved.version_req(); - let mut latest: Option<&Version> = None; - let mut latest_matched: Option<&Version> = None; + //order summaries by version + summaries.sort_by(|a, b| b.version().cmp(a.version())); for summary in &summaries { + if dep.matched_summary.is_some() + && dep.latest_matched_summary.is_some() + && dep.latest_summary.is_some() + { + break; + } if &installed == summary.version() { dep.matched_summary = Some(summary.clone()); } - match latest { - Some(cur) - if summary.version() > cur - && summary.version().is_prerelease() - == installed.is_prerelease() => - { - latest = Some(summary.version()); - dep.latest_summary = Some(summary.clone()); - } - None if summary.version().is_prerelease() - == installed.is_prerelease() => - { - latest = Some(summary.version()); - dep.latest_summary = Some(summary.clone()); - } - _ => {} + if dep.latest_summary.is_none() + && summary.version().is_prerelease() + == installed.is_prerelease() + { + dep.latest_summary = Some(summary.clone()); } - match (latest_matched.as_ref(), installed.is_prerelease()) { - (Some(cur), true) - if req_version - .matches_prerelease(summary.version()) - && summary.version() > cur => - { - latest_matched = Some(summary.version()); - dep.latest_matched_summary = Some(summary.clone()); - } - (Some(cur), false) - if req_version.matches(summary.version()) - && summary.version() > cur => - { - latest_matched = Some(summary.version()); - dep.latest_matched_summary = Some(summary.clone()); - } - (None, true) - if req_version - .matches_prerelease(summary.version()) => - { - latest_matched = Some(summary.version()); - dep.latest_matched_summary = Some(summary.clone()); - } - (None, false) - if req_version.matches(summary.version()) => - { - latest_matched = Some(summary.version()); - dep.latest_matched_summary = Some(summary.clone()); - } - _ => {} + if dep.latest_matched_summary.is_none() + && req_version.matches(summary.version()) + { + dep.latest_matched_summary = Some(summary.clone()); } } + dep.summaries = Some(summaries.clone()); }; - - //send to render - if let Some(rev) = doc.dirty_nodes.get(&dep.id) { - if *rev > output.ctx.rev { - continue; - } - //send to render task - render_tx - .send(DecorationEvent::Dependency( - output.ctx.uri.clone(), - dep.id.clone(), - dep.range, - dep.clone(), - )) - .await - .unwrap(); - doc.dirty_nodes.remove(&dep.id); - } + //send to render task + render_tx + .send(DecorationEvent::Dependency( + output.ctx.uri.clone(), + dep.id.clone(), + dep.range, + dep.clone(), + )) + .await + .unwrap(); + doc.dirty_nodes.remove(&dep.id); } } if doc.is_dirty() { @@ -571,11 +540,50 @@ async fn start_resolve( state: &mut Workspace, render_tx: &Sender, cargo_tx: &Sender, + inner_tx: &Sender, + client: &Client, + client_capabilities: &ClientCapabilities, ) { let Some(doc) = state.document_mut(uri) else { return; }; - // doc.populate_dependencies(); + doc.populate_dependencies(); + + if let Some(root_uri) = doc.root_manifest.as_ref() { + if root_uri != uri { + if client_capabilities.can_read_file() { + let param = ReadFileParam { uri: uri.clone() }; + match client.send_request::(param).await { + Ok(content) => { + if let Err(e) = inner_tx + .send(CargoDocumentEvent::Parse(CargoTomlPayload { + uri: uri.clone(), + text: content.content, + })) + .await + { + error!("inner tx send error: {}", e); + } + } + Err(e) => { + error!("read file error: {}", e); + } + } + } else { + //read file with os + let content = std::fs::read_to_string(uri.path().as_str()).unwrap(); + if let Err(e) = inner_tx + .send(CargoDocumentEvent::Parse(CargoTomlPayload { + uri: uri.clone(), + text: content, + })) + .await + { + error!("inner tx send error: {}", e); + } + } + } + } //virtual workspace doesn't need to resolve if doc.is_virtual() { diff --git a/src/controller/audit.rs b/src/controller/audit.rs index 5b15770..7a4b4bd 100644 --- a/src/controller/audit.rs +++ b/src/controller/audit.rs @@ -144,7 +144,7 @@ impl AuditController { received_uri = Some(uri); } } - timer = Some(Box::pin(tokio::time::sleep(Duration::from_secs(20)))); + timer = Some(Box::pin(tokio::time::sleep(Duration::from_secs(60)))); } () = async { if let Some(ref mut t) = timer { @@ -178,6 +178,7 @@ pub fn audit_workspace( uri: &Uri, audited: &mut Option, ) -> Result { + info!("audit workspace: {}", uri.path()); let gctx = cargo::util::context::GlobalContext::default()?; let path = Path::new(uri.path().as_str()); let workspace = cargo::core::Workspace::new(path, &gctx)?; diff --git a/src/controller/code_action.rs b/src/controller/code_action.rs index ecf3203..03ce61c 100644 --- a/src/controller/code_action.rs +++ b/src/controller/code_action.rs @@ -7,10 +7,9 @@ use tower_lsp::lsp_types::{ CodeAction, CodeActionKind, CodeActionOrCommand, CodeActionResponse, Command, Range, TextEdit, Uri, WorkspaceEdit, }; -use tracing::info; use crate::{ - decoration::{version_decoration, VersionDecoration}, + decoration::{version_decoration, VersionDecorationKind}, entity::{strip_quotes, Dependency, DependencyEntryKind, EntryKind, NodeKind, TomlNode, CARGO}, }; @@ -31,46 +30,52 @@ pub fn code_action_dependency( ) -> Option { match key { DependencyEntryKind::SimpleDependency | DependencyEntryKind::TableDependencyVersion => { - let version_deco = version_decoration(dep); - let latest = dep.latest_summary.as_ref().map(|s| s.version()); - let latest_matched = dep.latest_matched_summary.as_ref().map(|s| s.version()); + let version = version_decoration(dep); let mut actions = VersionCodeAction::new(uri, node); actions.check_unresolved(dep); - match version_deco { - VersionDecoration::Latest => { - if let Some(v) = latest { + match version.kind { + VersionDecorationKind::Latest => { + if let Some(v) = version.latest.as_ref() { actions.add_refactor(v); } } - VersionDecoration::Local => return None, - VersionDecoration::NotInstalled => return None, - VersionDecoration::MixedUpgradeable => { - if let Some(v) = latest_matched { + VersionDecorationKind::Local => return None, + VersionDecorationKind::NotInstalled => return None, + VersionDecorationKind::MixedUpgradeable => { + if let Some(v) = version.latest_matched.as_ref() { actions.add_quickfix(v); + // actions.add_precies_update_command(dep.package_name(), v); } - if let Some(v) = latest { + if let Some(v) = version.latest.as_ref() { actions.add_quickfix(v); } + actions.add_update_command(dep.package_name()); } - VersionDecoration::CompatibleLatest => { - let v = latest?; + VersionDecorationKind::CompatibleLatest => { + let v = version.latest.as_ref()?; actions.add_refactor(v); actions.add_quickfix(v); - actions.add_command(dep.package_name(), v); + // actions.add_precies_update_command(dep.package_name(), v); + actions.add_update_command(dep.package_name()); } - VersionDecoration::NoncompatibleLatest => { - let v = latest?; + VersionDecorationKind::NonCompatibleLatest => { + let v = version.latest.as_ref()?; actions.add_quickfix(v); + actions.add_update_command(dep.package_name()); } - VersionDecoration::Yanked => { - if let Some(v) = latest { + VersionDecorationKind::Yanked => { + if let Some(v) = version.latest.as_ref() { actions.add_quickfix(v); } - if let Some(v) = latest_matched { + if let Some(v) = version.latest_matched.as_ref() { actions.add_quickfix(v); } + actions.add_update_command(dep.package_name()); } - VersionDecoration::NotParsed => return None, + VersionDecorationKind::Git => { + actions.add_update_command(dep.package_name()); + } + VersionDecorationKind::NotParsed => return None, }; actions.add_eq_refactor(); if let Some(p) = dep.resolved.as_ref() { @@ -230,57 +235,46 @@ impl<'a> VersionCodeAction<'a> { title: Option, ) { self.actions - .push(MyCodeAction::new(self.uri.clone(), v, kind, range, title).into()); + .push(new_code_action(self.uri.clone(), v, kind, range, title)); } - fn add_command(&mut self, package_name: &str, v: &Version) { - self.actions.push(new_command(package_name, v).into()); + fn add_precies_update_command(&mut self, package_name: &str, v: &Version) { + self.actions + .push(new_precise_update_command(package_name, v).into()); } -} - -struct MyCodeAction(CodeAction); -impl From for CodeActionOrCommand { - fn from(value: MyCodeAction) -> Self { - value.0.into() + fn add_update_command(&mut self, package_name: &str) { + self.actions.push(new_update_command(package_name).into()); } } -impl MyCodeAction { - fn new(uri: Uri, v: String, kind: CodeActionKind, range: Range, title: Option) -> Self { - Self(CodeAction { - title: title.unwrap_or(v.to_string()), - kind: Some(kind), - diagnostics: None, - edit: Some(WorkspaceEdit { - changes: Some(HashMap::from([( - uri, - vec![TextEdit { new_text: v, range }], - )])), - document_changes: None, - change_annotations: None, - }), - ..Default::default() - }) - } - - fn with_command(&mut self, package_name: &str, v: &Version) { - self.0.command = Some(Command::new( - format!("update {} to {}", package_name, v), - CARGO.to_string(), - Some(vec![ - Value::String("update".to_string()), - Value::String(package_name.to_string()), - Value::String("--precise".to_string()), - Value::String(v.to_string()), - ]), - )); +fn new_code_action( + uri: Uri, + v: String, + kind: CodeActionKind, + range: Range, + title: Option, +) -> CodeActionOrCommand { + CodeAction { + title: title.unwrap_or(v.to_string()), + kind: Some(kind), + diagnostics: None, + edit: Some(WorkspaceEdit { + changes: Some(HashMap::from([( + uri, + vec![TextEdit { new_text: v, range }], + )])), + document_changes: None, + change_annotations: None, + }), + ..Default::default() } + .into() } -fn new_command(package_name: &str, v: &Version) -> Command { +fn new_precise_update_command(package_name: &str, v: &Version) -> Command { Command::new( - format!("update {} to {} use cargo", package_name, v), + format!("cargo update {} --precise {}", package_name, v), CARGO.to_string(), Some(vec![ Value::String("update".to_string()), @@ -290,3 +284,14 @@ fn new_command(package_name: &str, v: &Version) -> Command { ]), ) } + +fn new_update_command(package_name: &str) -> Command { + Command::new( + format!("cargo update {}", package_name), + CARGO.to_string(), + Some(vec![ + Value::String("update".to_string()), + Value::String(package_name.to_string()), + ]), + ) +} diff --git a/src/controller/debouncer.rs b/src/controller/debouncer.rs index db70a7a..84a190d 100644 --- a/src/controller/debouncer.rs +++ b/src/controller/debouncer.rs @@ -9,7 +9,7 @@ use std::{ use tokio::sync::mpsc::{self, error::SendError, Sender}; use tokio_util::time::{delay_queue, DelayQueue}; use tower_lsp::lsp_types::Uri; -use tracing::error; +use tracing::{error, info}; // Change Timer pub struct Debouncer { @@ -68,16 +68,21 @@ impl Stream for Queue { fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { let this = self.get_mut(); - match this.expirations.poll_expired(cx) { - Poll::Ready(Some(expired)) => match this.entries.remove(&expired.get_ref().clone()) { - Some((rev, _)) => Poll::Ready(Some(Ctx { + + while let Poll::Ready(Some(expired)) = this.expirations.poll_expired(cx) { + if let Some((rev, _)) = this.entries.remove(expired.get_ref()) { + return Poll::Ready(Some(Ctx { uri: expired.get_ref().clone(), rev, - })), - None => Poll::Ready(None), - }, - Poll::Ready(None) => Poll::Ready(None), - Poll::Pending => Poll::Pending, + })); + } // If not found in entries, just loop to the next expired item. + } + + // Only return None when the DelayQueue itself is empty. + if this.expirations.is_empty() { + Poll::Ready(None) + } else { + Poll::Pending } } } @@ -146,11 +151,11 @@ impl Debouncer { fn calculate_backoff_timeout(base_timeout: u64, count: u32) -> u64 { let factor = match count { - 0..=5 => 1, - 6..=10 => 2, - 11..=15 => 3, - 16..=20 => 4, - _ => 5, + 0..=2 => 1, + 3..=5 => 2, + 6..=10 => 3, + 11..=15 => 6, + _ => 7, }; - (base_timeout * factor).min(30_000) // Cap at 15 seconds + (base_timeout * factor).min(30_000) // Cap at 30 seconds } diff --git a/src/controller/hover.rs b/src/controller/hover.rs index 0f7e5e0..fc64357 100644 --- a/src/controller/hover.rs +++ b/src/controller/hover.rs @@ -50,14 +50,19 @@ pub fn hover( .collect(); let mut feature_list = features.keys().collect::>(); feature_list.sort(); - let feature_list = feature_list - .iter() - .map(|key| format!("- {}", key)) - .collect::>() - .join("\n"); + let mut s = String::new(); + for key in feature_list { + s.push_str(&format!("- {}", key)); + if !features[key].is_empty() { + s.push_str(": ["); + s.push_str(&features[key].join(", ")); + s.push(']'); + } + s.push('\n'); + } Some(Hover { - contents: HoverContents::Scalar(MarkedString::String(feature_list)), + contents: HoverContents::Scalar(MarkedString::String(s)), range: Some(node.range), }) } diff --git a/src/decoration.rs b/src/decoration.rs index 851a21e..abe563b 100644 --- a/src/decoration.rs +++ b/src/decoration.rs @@ -1,10 +1,12 @@ use cargo::core::SourceKind; +use semver::Version; use serde::{Deserialize, Serialize}; use tokio::sync::mpsc::Sender; use tower_lsp::{ lsp_types::{InlayHint, Range, Uri}, Client, }; +use tracing::info; use crate::entity::{commit_str_short, git_ref_str, Dependency}; @@ -55,15 +57,17 @@ impl DecorationRenderer { #[derive(Clone)] pub enum DecorationEvent { - Reset, + Reset(Uri), DependencyRangeUpdate(Uri, String, Range), DependencyRemove(Uri, String), DependencyWaiting(Uri, String, Range), Dependency(Uri, String, Range, Dependency), } -#[derive(Debug, PartialEq, Eq, Clone)] -pub enum VersionDecoration { +#[derive(Debug, Default, PartialEq, Eq, Clone, Serialize)] +pub enum VersionDecorationKind { + #[default] + NotParsed, //installed == latest_matched == latest Latest, Local, @@ -73,155 +77,118 @@ pub enum VersionDecoration { //installed -> latest_matched == latest CompatibleLatest, //installed !-> latest_matched == latest - NoncompatibleLatest, + NonCompatibleLatest, Yanked, - NotParsed, + Git, } #[derive(Debug, Default, Serialize, Clone)] #[serde(rename_all = "camelCase")] pub struct DecorationPayload { - installed: String, - latest_matched: String, - latest: String, + pub kind: VersionDecorationKind, + #[serde(skip_serializing_if = "Option::is_none")] + pub installed: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub latest_matched: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub latest: Option, //(ref,commit) - git: Option<(String, String)>, + #[serde(skip_serializing_if = "Option::is_none")] + pub git: Option<(String, String)>, } -pub fn decoration_payload(dep: &Dependency) -> DecorationPayload { - let mut git = None; - let installed = match dep.resolved.as_ref() { - Some(resolved) => { - if resolved.package_id().source_id().is_git() { - git = Some(( - git_ref_str(&resolved.package_id().source_id()).unwrap_or_default(), - commit_str_short(&resolved.package_id().source_id()) - .map_or(String::new(), |c| c.to_string()), - )); - String::new() - } else { - resolved.version().to_string() - } - } - None => "".to_string(), - }; - let latest_matched = match dep.latest_matched_summary.as_ref() { - Some(matched) => matched.version().to_string(), - None => "".to_string(), - }; - let latest = match dep.latest_summary.as_ref() { - Some(latest) => latest.version().to_string(), - None => "".to_string(), +pub fn formatted_string(dep: &Dependency, formatter: &CompiledFormatter) -> Option { + let version = version_decoration(dep); + + let template = match version.kind { + VersionDecorationKind::Git => &formatter.git, + VersionDecorationKind::Latest => &formatter.latest, + VersionDecorationKind::Local => &formatter.local, + VersionDecorationKind::NotInstalled => &formatter.not_installed, + VersionDecorationKind::MixedUpgradeable => &formatter.mixed_upgradeable, + VersionDecorationKind::CompatibleLatest => &formatter.compatible_latest, + VersionDecorationKind::NonCompatibleLatest => &formatter.noncompatible_latest, + VersionDecorationKind::Yanked => &formatter.yanked, + VersionDecorationKind::NotParsed => return None, }; - DecorationPayload { - installed, - latest_matched, - latest, - git, - } -} -pub fn formatted_string(dep: &Dependency, formatter: &DecorationFormatter) -> Option { - let version = version_decoration(dep); - let payload = decoration_payload(dep); - if let Some((r, commit)) = payload.git { - return Some( - formatter - .git - .replace("{{ref}}", &r) - .replace("{{commit}}", &commit), - ); - } - match version { - VersionDecoration::Latest => Some( - formatter - .latest - .replace("{{installed}}", &payload.installed) - .replace("{{latest_matched}}", &payload.latest_matched) - .replace("{{latest}}", &payload.latest), - ), - VersionDecoration::Local => Some( - formatter - .local - .replace("{{installed}}", &payload.installed) - .replace("{{latest_matched}}", &payload.latest_matched) - .replace("{{latest}}", &payload.latest), - ), - VersionDecoration::NotInstalled => Some( - formatter - .not_installed - .replace("{{installed}}", &payload.installed) - .replace("{{latest_matched}}", &payload.latest_matched) - .replace("{{latest}}", &payload.latest), - ), - VersionDecoration::MixedUpgradeable => Some( - formatter - .mixed_upgradeable - .replace("{{installed}}", &payload.installed) - .replace("{{latest_matched}}", &payload.latest_matched) - .replace("{{latest}}", &payload.latest), - ), - VersionDecoration::CompatibleLatest => Some( - formatter - .compatible_latest - .replace("{{installed}}", &payload.installed) - .replace("{{latest_matched}}", &payload.latest_matched) - .replace("{{latest}}", &payload.latest), - ), - VersionDecoration::NoncompatibleLatest => Some( - formatter - .noncompatible_latest - .replace("{{installed}}", &payload.installed) - .replace("{{latest_matched}}", &payload.latest_matched) - .replace("{{latest}}", &payload.latest), - ), - VersionDecoration::Yanked => Some( - formatter - .yanked - .replace("{{installed}}", &payload.installed) - .replace("{{latest_matched}}", &payload.latest_matched) - .replace("{{latest}}", &payload.latest), - ), - _ => None, - } + Some(template.format(&version)) } -pub fn version_decoration(dep: &Dependency) -> VersionDecoration { +pub fn version_decoration(dep: &Dependency) -> DecorationPayload { let Some(unresolved) = dep.unresolved.as_ref() else { - return VersionDecoration::NotParsed; + return DecorationPayload { + kind: VersionDecorationKind::NotParsed, + ..Default::default() + }; + }; + let Some(resolved) = dep.resolved.as_ref() else { + return DecorationPayload { + kind: VersionDecorationKind::NotInstalled, + ..Default::default() + }; }; match unresolved.source_id().kind() { - SourceKind::Path => VersionDecoration::Local, + SourceKind::Path => DecorationPayload { + kind: VersionDecorationKind::Local, + ..Default::default() + }, //TODO idk what's this - SourceKind::Directory => VersionDecoration::Local, + SourceKind::Directory => DecorationPayload { + kind: VersionDecorationKind::Local, + ..Default::default() + }, + SourceKind::Git(_) => { + let mut git = None; + if resolved.package_id().source_id().is_git() { + git = Some(( + git_ref_str(&resolved.package_id().source_id()).unwrap_or_default(), + commit_str_short(&resolved.package_id().source_id()) + .map_or(String::new(), |c| c.to_string()), + )); + }; + DecorationPayload { + kind: VersionDecorationKind::Git, + git, + ..Default::default() + } + } _ => { match ( - dep.resolved.as_ref(), dep.matched_summary.as_ref(), dep.latest_matched_summary.as_ref(), dep.latest_summary.as_ref(), ) { - (Some(_), Some(matched), Some(latest_matched), Some(latest)) => { + (Some(matched), Some(latest_matched), Some(latest)) => { //latest + let mut p = DecorationPayload::default(); if matched.version() == latest_matched.version() && latest_matched.version() == latest.version() { - VersionDecoration::Latest + p.kind = VersionDecorationKind::Latest; } else if matched.version() != latest_matched.version() && latest_matched.version() == latest.version() { - VersionDecoration::CompatibleLatest + p.kind = VersionDecorationKind::CompatibleLatest; } else if matched.version() == latest_matched.version() && latest_matched.version() != latest.version() { - VersionDecoration::NoncompatibleLatest + p.kind = VersionDecorationKind::NonCompatibleLatest; } else { - VersionDecoration::MixedUpgradeable + p.kind = VersionDecorationKind::MixedUpgradeable; } + p.installed = Some(matched.version().clone()); + p.latest = Some(latest.version().clone()); + p.latest_matched = Some(latest_matched.version().clone()); + p } - (Some(_), None, Some(_), Some(_)) => VersionDecoration::Yanked, - (None, _, _, _) => VersionDecoration::NotInstalled, - //TODO get latest version for not installed + (None, Some(latest_matched), Some(latest)) => DecorationPayload { + kind: VersionDecorationKind::Yanked, + installed: Some(resolved.version().clone()), + latest_matched: Some(latest_matched.version().clone()), + latest: Some(latest.version().clone()), + ..Default::default() + }, //TODO any other match arm? _ => unreachable!(), } @@ -269,6 +236,22 @@ pub struct DecorationFormatter { pub git: String, } +impl DecorationFormatter { + pub fn compile(&self) -> CompiledFormatter { + CompiledFormatter { + waiting: CompiledTemplate::new(self.waiting.clone()), + latest: CompiledTemplate::new(self.latest.clone()), + local: CompiledTemplate::new(self.local.clone()), + not_installed: CompiledTemplate::new(self.not_installed.clone()), + mixed_upgradeable: CompiledTemplate::new(self.mixed_upgradeable.clone()), + compatible_latest: CompiledTemplate::new(self.compatible_latest.clone()), + noncompatible_latest: CompiledTemplate::new(self.noncompatible_latest.clone()), + yanked: CompiledTemplate::new(self.yanked.clone()), + git: CompiledTemplate::new(self.git.clone()), + } + } +} + impl Default for DecorationFormatter { fn default() -> Self { Self { @@ -285,6 +268,76 @@ impl Default for DecorationFormatter { } } +#[derive(Debug, Clone, Default)] +pub struct CompiledFormatter { + waiting: CompiledTemplate, + latest: CompiledTemplate, + local: CompiledTemplate, + not_installed: CompiledTemplate, + mixed_upgradeable: CompiledTemplate, + compatible_latest: CompiledTemplate, + noncompatible_latest: CompiledTemplate, + yanked: CompiledTemplate, + git: CompiledTemplate, +} + +#[derive(Debug, Clone, Default)] +struct CompiledTemplate { + template: String, + needs_installed: bool, + needs_latest_matched: bool, + needs_latest: bool, + needs_git_ref: bool, + needs_git_commit: bool, +} + +impl CompiledTemplate { + fn new(template: String) -> Self { + Self { + needs_installed: template.contains("{{installed}}"), + needs_latest_matched: template.contains("{{latest_matched}}"), + needs_latest: template.contains("{{latest}}"), + needs_git_ref: template.contains("{{ref}}"), + needs_git_commit: template.contains("{{commit}}"), + template, + } + } + + fn template(&self) -> &str { + &self.template + } + + fn format(&self, version: &DecorationPayload) -> String { + let mut result = self.template.clone(); + + if self.needs_installed && version.installed.is_some() { + result = result.replace( + "{{installed}}", + &version.installed.as_ref().unwrap().to_string(), + ); + } + if self.needs_latest_matched && version.latest_matched.is_some() { + result = result.replace( + "{{latest_matched}}", + &version.latest_matched.as_ref().unwrap().to_string(), + ); + } + if self.needs_latest && version.latest.is_some() { + result = result.replace("{{latest}}", &version.latest.as_ref().unwrap().to_string()); + } + if let Some((ref_str, commit)) = version.git.as_ref() { + if self.needs_git_ref { + result = result.replace("{{ref}}", ref_str); + } + if self.needs_git_commit { + result = result.replace("{{commit}}", commit); + } + } + + result + } +} + fn default_latest() -> String { "✅ {{installed}}".to_string() } diff --git a/src/decoration/inlay_hint.rs b/src/decoration/inlay_hint.rs index f9e7391..afef908 100644 --- a/src/decoration/inlay_hint.rs +++ b/src/decoration/inlay_hint.rs @@ -47,9 +47,9 @@ mod inlay_hint_decoration_state { } } - pub fn reset(state: &RwLock) { + pub fn reset(state: &RwLock, uri: &Uri) { let mut state = state.write(); - state.clear(); + state.remove(uri); } pub fn list(state: &RwLock, uri: &Uri) -> Vec { @@ -100,9 +100,9 @@ impl InlayHintDecoration { value: GLOBAL_CONFIG .read() .unwrap() - .renderer .decoration_formatter .waiting + .template() .to_string(), tooltip: None, location: None, @@ -120,13 +120,12 @@ impl InlayHintDecoration { DecorationEvent::DependencyRemove(path, id) => { inlay_hint_decoration_state::remove(&state, &path, &id); } - DecorationEvent::Reset => { - inlay_hint_decoration_state::reset(&state); + DecorationEvent::Reset(uri) => { + inlay_hint_decoration_state::reset(&state, &uri); } DecorationEvent::Dependency(path, id, range, p) => { let config = GLOBAL_CONFIG.read().unwrap(); - let Some(decoration) = - formatted_string(&p, &config.renderer.decoration_formatter) + let Some(decoration) = formatted_string(&p, &config.decoration_formatter) else { continue; }; @@ -157,8 +156,8 @@ impl InlayHintDecoration { inlay_hint_decoration_state::remove(&self.hints, uri, id); } - pub fn reset(&mut self) { - inlay_hint_decoration_state::reset(&self.hints); + pub fn reset(&mut self, uri: &Uri) { + inlay_hint_decoration_state::reset(&self.hints, uri); } } diff --git a/src/main.rs b/src/main.rs index e389ae7..3656f12 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,5 @@ use clap::{arg, command, Parser}; -use config::{initialize_config, Config}; +use config::{initialize_config, UserConfig}; use controller::{Appraiser, CargoDocumentEvent, CargoTomlPayload, ClientCapability}; use decoration::{DecorationRenderer, Renderer}; use entity::{supported_commands, CARGO}; @@ -28,7 +28,7 @@ struct CargoAppraiser { impl LanguageServer for CargoAppraiser { async fn initialize(&self, params: InitializeParams) -> Result { //init config - let config: Config = params + let config: UserConfig = params .initialization_options .map(serde_json::from_value) .and_then(|v| v.ok()) diff --git a/src/usecase/workspace.rs b/src/usecase/workspace.rs index 213e020..4747685 100644 --- a/src/usecase/workspace.rs +++ b/src/usecase/workspace.rs @@ -66,14 +66,14 @@ impl Workspace { let doc = entry.into_mut(); if !diff.is_empty() { doc.reconsile(new_doc, &diff); - doc.populate_dependencies(); + // doc.populate_dependencies(); } Ok((doc, diff)) } Entry::Vacant(entry) => { let diff = Document::diff(None, &new_doc); new_doc.self_reconsile(&diff); - new_doc.populate_dependencies(); + // new_doc.populate_dependencies(); let doc = entry.insert(new_doc); Ok((doc, diff)) }