From 3255a3b9947fdaaf9f77e1597f71295e97888c04 Mon Sep 17 00:00:00 2001 From: fengqi <362254883@qq.com> Date: Tue, 9 Jan 2024 15:39:18 +0800 Subject: [PATCH 1/2] Enhancements: Find packages --- src/crates/api.rs | 4 + src/crates/mod.rs | 47 ++++++++++ src/crates/sparse.rs | 4 + src/main.rs | 174 ++++++++++++++++++++++------------ src/parse.rs | 217 ++++++++++++++++++++++++++++--------------- 5 files changed, 309 insertions(+), 137 deletions(-) diff --git a/src/crates/api.rs b/src/crates/api.rs index 0828200..ee4b579 100644 --- a/src/crates/api.rs +++ b/src/crates/api.rs @@ -15,6 +15,10 @@ pub struct CrateApi { #[async_trait] impl CrateLookup for CrateApi { + fn client(&self) -> &crate::crates::HyperClient { + &self.client + } + async fn get_latest_version(self, crate_name: String) -> Result { let response = self .client diff --git a/src/crates/mod.rs b/src/crates/mod.rs index c2edc40..ed07536 100644 --- a/src/crates/mod.rs +++ b/src/crates/mod.rs @@ -5,12 +5,18 @@ pub mod sparse; use std::collections::HashMap; use async_trait::async_trait; +use hyper::client::HttpConnector; +use hyper::{Body, Request}; +use hyper_rustls::HttpsConnector; use semver::Version; +use serde::Deserialize; use time::OffsetDateTime; use tokio::sync::mpsc; use self::cache::{CachedVersion, CrateCache}; +type HyperClient = hyper::Client>; + #[derive(Debug)] pub enum CrateError { NoVersionsFound, @@ -25,8 +31,49 @@ impl CrateError { } } +#[derive(Deserialize)] +pub struct Crate { + pub name: String, +} + +#[derive(Deserialize)] +struct Crates { + pub crates: Vec, +} + #[async_trait] pub trait CrateLookup: Clone + Send + 'static { + fn client(&self) -> &HyperClient; + async fn search_crates(&self, crate_name: &String) -> Result, CrateError> { + let response = self + .client() + .request( + Request::builder() + .uri(&format!( + "https://crates.io/api/v1/crates?q={}&per_page=5", + crate_name + )) + .header( + "User-Agent", + "crates-lsp (github.com/MathiasPius/crates-lsp)", + ) + .header("Accept", "application/json") + .body(Body::empty()) + .map_err(CrateError::transport)?, + ) + .await + .map_err(CrateError::transport)?; + let body = hyper::body::to_bytes(response.into_body()) + .await + .map_err(CrateError::transport)?; + + let stringified = String::from_utf8_lossy(&body); + let details: Crates = + serde_json::from_str(&stringified).map_err(CrateError::Deserialization)?; + + Ok(details.crates) + } + async fn get_latest_version(self, crate_name: String) -> Result; // How long to cache a result for. diff --git a/src/crates/sparse.rs b/src/crates/sparse.rs index 5469513..d119a05 100644 --- a/src/crates/sparse.rs +++ b/src/crates/sparse.rs @@ -15,6 +15,10 @@ pub struct CrateIndex { #[async_trait] impl CrateLookup for CrateIndex { + fn client(&self) -> &crate::crates::HyperClient { + &self.client + } + async fn get_latest_version(self, crate_name: String) -> Result { let crate_index_path = match crate_name.len() { 0 => return Err(CrateError::InvalidCrateName(crate_name)), diff --git a/src/main.rs b/src/main.rs index 028ec07..78b9d9e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +use crate::parse::{Dependency, DependencyWithVersion}; use crates::api::CrateApi; use crates::cache::CrateCache; use crates::sparse::CrateIndex; @@ -28,61 +29,69 @@ impl Backend { // Retrieve just the package names, so we can fetch the latest // versions via the crate registry. - let dependency_names: Vec<&str> = packages + let dependency_with_versions: Vec<&DependencyWithVersion> = packages .iter() - .map(|dependency| dependency.name.as_str()) + .filter_map(|dependency| match dependency { + Dependency::Partial { .. } => None, + Dependency::WithVersion(dep) => Some(dep), + Dependency::Other { .. } => None, + }) .collect(); + if dependency_with_versions.len() == 0 { + return Vec::new(); + } + + let crate_names: Vec<&str> = dependency_with_versions + .clone() + .into_iter() + .map(|x| x.name.as_str()) + .collect(); // Get the newest version of each crate that appears in the manifest. let newest_packages = if self.settings.use_api().await { self.api - .fetch_versions(self.cache.clone(), &dependency_names) + .fetch_versions(self.cache.clone(), &crate_names) .await } else { self.sparse - .fetch_versions(self.cache.clone(), &dependency_names) + .fetch_versions(self.cache.clone(), &crate_names) .await }; // Produce diagnostic hints for each crate where we might be helpful. - let diagnostics: Vec<_> = packages + let diagnostics: Vec<_> = dependency_with_versions .into_iter() .filter_map(|dependency| { - if let Some(version) = dependency.version { - if let Some(Some(newest_version)) = newest_packages.get(&dependency.name) { - match version { - DependencyVersion::Complete { range, version } => { - if !version.matches(newest_version) { - return Some(Diagnostic::new_simple( - range, - format!("{}: {newest_version}", &dependency.name), - )); - } else { - let range = Range { - start: Position::new(range.start.line, 0), - end: Position::new(range.start.line, 0), - }; - - return Some(Diagnostic::new_simple(range, "✓".to_string())); - } - } - - DependencyVersion::Partial { range, .. } => { + if let Some(Some(newest_version)) = newest_packages.get(&dependency.name) { + match &dependency.version { + DependencyVersion::Complete { range, version } => { + if !version.matches(newest_version) { return Some(Diagnostic::new_simple( - range, + range.clone(), format!("{}: {newest_version}", &dependency.name), )); + } else { + let range = Range { + start: Position::new(range.start.line, 0), + end: Position::new(range.start.line, 0), + }; + + return Some(Diagnostic::new_simple(range, "✓".to_string())); } } - } else { - return Some(Diagnostic::new_simple( - version.range(), - format!("{}: Unknown crate", &dependency.name), - )); + DependencyVersion::Partial { range, .. } => { + return Some(Diagnostic::new_simple( + range.clone(), + format!("{}: {newest_version}", &dependency.name), + )); + } } + } else { + return Some(Diagnostic::new_simple( + dependency.version.range(), + format!("{}: Unknown crate", &dependency.name), + )); } - - None }) .collect(); @@ -182,43 +191,86 @@ impl LanguageServer for Backend { return Ok(None); }; - let Some(dependency) = dependencies.into_iter().find(|dependency| { - dependency.version.as_ref().is_some_and(|version| { - version.range().start.line == cursor.line - && version.range().start.character <= cursor.character - && version.range().end.character >= cursor.character + let Some(dependency) = dependencies + .into_iter() + .find(|dependency| match dependency { + Dependency::Partial { line, .. } => { + if *line == cursor.line { + true + } else { + false + } + } + Dependency::WithVersion(dep) => { + if dep.version.range().start.line == cursor.line + && dep.version.range().start.character <= cursor.character + && dep.version.range().end.character >= cursor.character + { + true + } else { + false + } + } + Dependency::Other { .. } => false, }) - }) else { + else { return Ok(None); }; - let packages = self - .sparse - .fetch_versions(self.cache.clone(), &[&dependency.name]) - .await; + match dependency { + Dependency::Partial { name, .. } => { + let Ok(crates) = self.sparse.search_crates(&name).await else { + return Ok(None); + }; + let range = Range::new(Position::new(cursor.line, 0), cursor); + Ok(Some(CompletionResponse::Array( + crates + .into_iter() + .map(|x| CompletionItem { + text_edit: Some(CompletionTextEdit::Edit(TextEdit::new( + range, + x.name.clone(), + ))), + label: x.name, + ..CompletionItem::default() + }) + .collect(), + ))) + } + Dependency::WithVersion(dependency) => { + let packages = self + .sparse + .fetch_versions(self.cache.clone(), &[&dependency.name]) + .await; - if let Some(Some(newest_version)) = packages.get(&dependency.name) { - let specified_version = dependency.version.as_ref().unwrap().to_string(); - let specified_version = &specified_version[0..specified_version.len() - 1]; + if let Some(Some(newest_version)) = packages.get(&dependency.name) { + let specified_version = dependency.version.to_string(); + let specified_version = &specified_version[0..specified_version.len() - 1]; - let newest_version = newest_version.to_string(); + let newest_version = newest_version.to_string(); - let truncated_version = newest_version - .as_str() - .strip_prefix( - specified_version.trim_start_matches(&['<', '>', '=', '^', '~'] as &[_]), - ) - .unwrap_or(&newest_version) - .to_string(); + let truncated_version = newest_version + .as_str() + .strip_prefix( + specified_version + .trim_start_matches(&['<', '>', '=', '^', '~'] as &[_]), + ) + .unwrap_or(&newest_version) + .to_string(); - Ok(Some(CompletionResponse::Array(vec![CompletionItem { - insert_text: Some(truncated_version), - label: newest_version, + Ok(Some(CompletionResponse::Array(vec![CompletionItem { + insert_text: Some(truncated_version.clone()), + label: newest_version.clone(), - ..CompletionItem::default() - }]))) - } else { - Ok(None) + ..CompletionItem::default() + }]))) + } else { + Ok(None) + } + } + Dependency::Other { .. } => { + return Ok(None); + } } } } diff --git a/src/parse.rs b/src/parse.rs index 0f4b3d8..108d96f 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -5,17 +5,66 @@ use tokio::sync::RwLock; use tower_lsp::lsp_types::{Position, Range, Url}; #[derive(Debug, Clone, PartialEq, Eq)] -pub struct Dependency { +pub enum Dependency { + /// e.g: anyho + Partial { + name: String, + line: u32, + }, + WithVersion(DependencyWithVersion), + /// e.g: anyhow = { git = ".."} + Other { + name: String, + }, +} +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DependencyWithVersion { pub name: String, - pub version: Option, + pub version: DependencyVersion, +} +// pub struct Dependency { +// pub name: String, +// pub version: Option, +// } + +impl Dependency { + pub fn name(&self) -> Option<&String> { + match self { + Dependency::Partial { .. } => None, + Dependency::WithVersion(dep) => Some(&dep.name), + Dependency::Other { name } => Some(name), + } + } + + pub fn name_mut(&mut self) -> Option<&mut String> { + match self { + Dependency::Partial { .. } => None, + Dependency::WithVersion(dep) => Some(&mut dep.name), + Dependency::Other { name } => Some(name), + } + } + + pub fn version_mut(&mut self) -> Option<&mut DependencyVersion> { + match self { + Dependency::Partial { .. } => None, + Dependency::WithVersion(dep) => Some(&mut dep.version), + Dependency::Other { .. } => None, + } + } } impl Display for Dependency { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if let Some(version) = &self.version { - write!(f, "{} = \"{}\"", self.name, version) - } else { - write!(f, "{} = \"?\"", self.name) + match self { + Dependency::Partial { name, .. } => { + write!(f, "{} = \"?\"", name) + } + Dependency::WithVersion(dep) => { + write!(f, "{} = \"{}\"", dep.name, dep.version) + } + Dependency::Other { name } => { + write!(f, "{} = \"?\"", name) + } } } } @@ -79,17 +128,17 @@ enum Line<'a> { name: &'a str, start: usize, end: usize, - version: Option<&'a str>, + version: &'a str, }, Partial { name: &'a str, start: usize, - version: Option<&'a str>, + version: &'a str, }, } impl<'a> Line<'a> { - pub fn parse(line: &'a str) -> Option { + pub fn parse(line: &'a str, line_no: usize) -> Option { use Line::*; let mut state = Start; @@ -147,14 +196,14 @@ impl<'a> Line<'a> { name, start, end: i, - version: Some(&line[start..i]), + version: &line[start..i], }, 'a'..='z' | 'A'..='Z' => { if first { Partial { name, start, - version: Some(&line[start..i]), + version: &line[start..i], } } else { VersionSelector { @@ -177,7 +226,7 @@ impl<'a> Line<'a> { _ => Partial { name, start, - version: Some(&line[start..i]), + version: &line[start..i], }, }, }; @@ -189,61 +238,67 @@ impl<'a> Line<'a> { version, start, end, - } => Some(Dependency { - name: name.to_string(), - version: if let Some(version) = version { - let version = version[1..].trim(); - - if let Ok(version) = VersionReq::parse(version) { - Some(DependencyVersion::Complete { - version, - range: Range::new( - Position::new(0, start as u32), - Position::new(0, end as u32), - ), - }) - } else { - Some(DependencyVersion::Partial { - version: version.to_string(), - range: Range::new( - Position::new(0, start as u32), - Position::new(0, end as u32), - ), - }) + } => { + let version = version[1..].trim(); + let version = if let Ok(version) = VersionReq::parse(version) { + DependencyVersion::Complete { + version, + range: Range::new( + Position::new(0, start as u32), + Position::new(0, end as u32), + ), } } else { - None - }, - }), + DependencyVersion::Partial { + version: version.to_string(), + range: Range::new( + Position::new(0, start as u32), + Position::new(0, end as u32), + ), + } + }; + Some(Dependency::WithVersion(DependencyWithVersion { + name: name.to_string(), + version, + })) + } Partial { name, version, start, - } => Some(Dependency { - name: name.to_string(), - version: version.map(|version| DependencyVersion::Partial { + } => { + let version = DependencyVersion::Partial { version: version[1..].trim().trim_matches(',').to_string(), range: Range::new( Position::new(0, start as u32), Position::new(0, line.len() as u32), ), - }), - }), - Name { name, .. } | Struct { name, .. } => Some(Dependency { + }; + Some(Dependency::WithVersion(DependencyWithVersion { + name: name.to_string(), + version, + })) + } + Name { name, .. } | Struct { name, .. } => Some(Dependency::Other { name: name.to_string(), - version: None, }), - VersionSelector { name, start, .. } => Some(Dependency { - name: name.to_string(), - version: Some(DependencyVersion::Partial { - version: line[start + 1..].trim().to_string(), - range: Range::new( - Position::new(0, start as u32), - Position::new(0, line.len() as u32), - ), - }), + VersionSelector { name, start, .. } => { + Some(Dependency::WithVersion(DependencyWithVersion { + name: name.to_string(), + version: DependencyVersion::Partial { + version: line[start + 1..].trim().to_string(), + range: Range::new( + Position::new(0, start as u32), + Position::new(0, line.len() as u32), + ), + }, + })) + } + PartialName { start } => Some(Dependency::Partial { + name: line[start..].to_string(), + line: line_no as u32, }), - _ => None, + Start => None, } } } @@ -309,9 +364,9 @@ impl ManifestTracker { Dependencies => { // If we're in a generic dependency section, and find a line // which can be parsed as a versioned dependency, push it as a package. - if let Some(mut dependency) = Line::parse(line) { + if let Some(mut dependency) = Line::parse(line, i) { // Line::parse assumes line 0, modify so we have to fix this manually. - if let Some(version) = dependency.version.as_mut() { + if let Some(version) = dependency.version_mut() { version.range_mut().start.line = i as u32; version.range_mut().end.line = i as u32; } @@ -326,17 +381,21 @@ impl ManifestTracker { // [dependencies.serde] // version = "1" // ``` - if let Some(mut dependency) = Line::parse(line) { - if dependency.name != "version" { + if let Some(mut dependency) = Line::parse(line, i) { + if dependency + .name() + .and_then(|x| Some(x != "version")) + .unwrap_or_default() + { continue; } else { // Rename to the package section, since the dependency is currently // named "version" because of the Line::parse logic assuming this is // a regular dependencies section. - dependency.name = name.clone(); + dependency.name_mut().and_then(|x| Some(*x = name.clone())); } // Line::parse assumes line 0, modify so we have to fix this manually. - if let Some(version) = dependency.version.as_mut() { + if let Some(version) = dependency.version_mut() { version.range_mut().start.line = i as u32; version.range_mut().end.line = i as u32; } @@ -373,10 +432,10 @@ mod tests { use tower_lsp::lsp_types::Range; use tower_lsp::lsp_types::Url; - use crate::parse::Dependency; use crate::parse::DependencyVersion; use crate::parse::Line; use crate::parse::ManifestTracker; + use crate::parse::{Dependency, DependencyWithVersion}; #[tokio::test] async fn detect_plain_version() { @@ -407,12 +466,15 @@ mod tests { } fn matches_complete(line: &str, name: &str, version: &str) { - let line = Line::parse(line).unwrap(); + let line = Line::parse(line, 0).unwrap(); + let Dependency::WithVersion(line) = line else { + panic!("expected complete version selector") + }; let expected_version = VersionReq::parse(version).unwrap(); assert_eq!(line.name, name); - match line.version.unwrap() { + match line.version { DependencyVersion::Partial { .. } => panic!("expected complete version selector"), DependencyVersion::Complete { version, .. } => { assert_eq!(version, expected_version) @@ -421,10 +483,13 @@ mod tests { } fn matches_partial(line: &str, name: &str, expected_version: &str) { - let line = Line::parse(line).unwrap(); + let line = Line::parse(line, 0).unwrap(); + let Dependency::WithVersion(line) = line else { + panic!("expected complete version selector") + }; assert_eq!(line.name, name); - match line.version.unwrap() { + match line.version { DependencyVersion::Complete { .. } => panic!("expected partial version selector"), DependencyVersion::Partial { version, .. } => { assert_eq!(version.as_str(), expected_version) @@ -488,36 +553,36 @@ mod tests { assert_eq!( manifests.get(&url).await.unwrap(), vec![ - Dependency { + Dependency::WithVersion(DependencyWithVersion { name: "log".to_string(), - version: Some(DependencyVersion::Complete { + version: DependencyVersion::Complete { range: Range { start: Position::new(1, 6), end: Position::new(1, 8) }, version: VersionReq::parse("1").unwrap() - }) - }, - Dependency { + } + }), + Dependency::WithVersion(DependencyWithVersion { name: "serde".to_string(), - version: Some(DependencyVersion::Complete { + version: DependencyVersion::Complete { range: Range { start: Position::new(4, 10), end: Position::new(4, 12) }, version: VersionReq::parse("1").unwrap() - }) - }, - Dependency { + } + }), + Dependency::WithVersion(DependencyWithVersion { name: "tokio".to_string(), - version: Some(DependencyVersion::Complete { + version: DependencyVersion::Complete { range: Range { start: Position::new(7, 10), end: Position::new(7, 12) }, version: VersionReq::parse("1").unwrap() - }) - } + } + }) ] ); } From 07151273629b54b9b2b20a7af7cd3ea9138b8734 Mon Sep 17 00:00:00 2001 From: fengqi <362254883@qq.com> Date: Wed, 10 Jan 2024 09:53:32 +0800 Subject: [PATCH 2/2] Optimized the code according to Clippy's suggestions. --- src/main.rs | 42 ++++++++++++++---------------------------- src/parse.rs | 6 ++++-- 2 files changed, 18 insertions(+), 30 deletions(-) diff --git a/src/main.rs b/src/main.rs index 78b9d9e..822da2c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -38,7 +38,7 @@ impl Backend { }) .collect(); - if dependency_with_versions.len() == 0 { + if dependency_with_versions.is_empty() { return Vec::new(); } @@ -61,36 +61,33 @@ impl Backend { // Produce diagnostic hints for each crate where we might be helpful. let diagnostics: Vec<_> = dependency_with_versions .into_iter() - .filter_map(|dependency| { + .map(|dependency| { if let Some(Some(newest_version)) = newest_packages.get(&dependency.name) { match &dependency.version { DependencyVersion::Complete { range, version } => { if !version.matches(newest_version) { - return Some(Diagnostic::new_simple( - range.clone(), + Diagnostic::new_simple( + *range, format!("{}: {newest_version}", &dependency.name), - )); + ) } else { let range = Range { start: Position::new(range.start.line, 0), end: Position::new(range.start.line, 0), }; - - return Some(Diagnostic::new_simple(range, "✓".to_string())); + Diagnostic::new_simple(range, "✓".to_string()) } } - DependencyVersion::Partial { range, .. } => { - return Some(Diagnostic::new_simple( - range.clone(), - format!("{}: {newest_version}", &dependency.name), - )); - } + DependencyVersion::Partial { range, .. } => Diagnostic::new_simple( + *range, + format!("{}: {newest_version}", &dependency.name), + ), } } else { - return Some(Diagnostic::new_simple( + Diagnostic::new_simple( dependency.version.range(), format!("{}: Unknown crate", &dependency.name), - )); + ) } }) .collect(); @@ -194,22 +191,11 @@ impl LanguageServer for Backend { let Some(dependency) = dependencies .into_iter() .find(|dependency| match dependency { - Dependency::Partial { line, .. } => { - if *line == cursor.line { - true - } else { - false - } - } + Dependency::Partial { line, .. } => *line == cursor.line, Dependency::WithVersion(dep) => { - if dep.version.range().start.line == cursor.line + dep.version.range().start.line == cursor.line && dep.version.range().start.character <= cursor.character && dep.version.range().end.character >= cursor.character - { - true - } else { - false - } } Dependency::Other { .. } => false, }) diff --git a/src/parse.rs b/src/parse.rs index 108d96f..438bb5b 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -384,7 +384,7 @@ impl ManifestTracker { if let Some(mut dependency) = Line::parse(line, i) { if dependency .name() - .and_then(|x| Some(x != "version")) + .map(|x| x != "version") .unwrap_or_default() { continue; @@ -392,7 +392,9 @@ impl ManifestTracker { // Rename to the package section, since the dependency is currently // named "version" because of the Line::parse logic assuming this is // a regular dependencies section. - dependency.name_mut().and_then(|x| Some(*x = name.clone())); + if let Some(x) = dependency.name_mut() { + *x = name.clone() + } } // Line::parse assumes line 0, modify so we have to fix this manually. if let Some(version) = dependency.version_mut() {