Skip to content

Commit

Permalink
Merge pull request #6 from jm-observer/main
Browse files Browse the repository at this point in the history
Enhancements: Find packages
  • Loading branch information
MathiasPius authored Jan 10, 2024
2 parents fae98c3 + 0715127 commit 5a3926a
Show file tree
Hide file tree
Showing 5 changed files with 300 additions and 140 deletions.
4 changes: 4 additions & 0 deletions src/crates/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Version, CrateError> {
let response = self
.client
Expand Down
47 changes: 47 additions & 0 deletions src/crates/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<HttpsConnector<HttpConnector>>;

#[derive(Debug)]
pub enum CrateError {
NoVersionsFound,
Expand All @@ -25,8 +31,49 @@ impl CrateError {
}
}

#[derive(Deserialize)]
pub struct Crate {
pub name: String,
}

#[derive(Deserialize)]
struct Crates {
pub crates: Vec<Crate>,
}

#[async_trait]
pub trait CrateLookup: Clone + Send + 'static {
fn client(&self) -> &HyperClient;
async fn search_crates(&self, crate_name: &String) -> Result<Vec<Crate>, 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<Version, CrateError>;

// How long to cache a result for.
Expand Down
4 changes: 4 additions & 0 deletions src/crates/sparse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Version, CrateError> {
let crate_index_path = match crate_name.len() {
0 => return Err(CrateError::InvalidCrateName(crate_name)),
Expand Down
166 changes: 102 additions & 64 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::parse::{Dependency, DependencyWithVersion};
use crates::api::CrateApi;
use crates::cache::CrateCache;
use crates::sparse::CrateIndex;
Expand Down Expand Up @@ -28,61 +29,66 @@ 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.is_empty() {
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, .. } => {
return Some(Diagnostic::new_simple(
range,
.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) {
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),
};
Diagnostic::new_simple(range, "✓".to_string())
}
}
} else {
return Some(Diagnostic::new_simple(
version.range(),
format!("{}: Unknown crate", &dependency.name),
));
DependencyVersion::Partial { range, .. } => Diagnostic::new_simple(
*range,
format!("{}: {newest_version}", &dependency.name),
),
}
} else {
Diagnostic::new_simple(
dependency.version.range(),
format!("{}: Unknown crate", &dependency.name),
)
}

None
})
.collect();

Expand Down Expand Up @@ -182,43 +188,75 @@ 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, .. } => *line == cursor.line,
Dependency::WithVersion(dep) => {
dep.version.range().start.line == cursor.line
&& dep.version.range().start.character <= cursor.character
&& dep.version.range().end.character >= cursor.character
}
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);
}
}
}
}
Expand Down
Loading

0 comments on commit 5a3926a

Please sign in to comment.