Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhancements: Find packages #6

Merged
merged 2 commits into from
Jan 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading