mirror of
https://github.com/denoland/deno.git
synced 2024-11-21 15:04:11 -05:00
feat(lsp): cache all dependencies quick fix (#20665)
This commit is contained in:
parent
b2abae4771
commit
bb4b00df92
4 changed files with 267 additions and 68 deletions
|
@ -917,6 +917,24 @@ impl CodeActionCollection {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_cache_all_action(
|
||||
&mut self,
|
||||
specifier: &ModuleSpecifier,
|
||||
diagnostics: Vec<lsp::Diagnostic>,
|
||||
) {
|
||||
self.actions.push(CodeActionKind::Deno(lsp::CodeAction {
|
||||
title: "Cache all dependencies of this module.".to_string(),
|
||||
kind: Some(lsp::CodeActionKind::QUICKFIX),
|
||||
diagnostics: Some(diagnostics),
|
||||
command: Some(lsp::Command {
|
||||
title: "".to_string(),
|
||||
command: "deno.cache".to_string(),
|
||||
arguments: Some(vec![json!([]), json!(&specifier)]),
|
||||
}),
|
||||
..Default::default()
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
/// Prepend the whitespace characters found at the start of line_content to content.
|
||||
|
|
|
@ -24,6 +24,7 @@ use crate::tools::lint::get_configured_rules;
|
|||
use deno_ast::MediaType;
|
||||
use deno_core::anyhow::anyhow;
|
||||
use deno_core::error::AnyError;
|
||||
use deno_core::parking_lot::RwLock;
|
||||
use deno_core::resolve_url;
|
||||
use deno_core::serde::Deserialize;
|
||||
use deno_core::serde_json;
|
||||
|
@ -48,7 +49,6 @@ use std::sync::Arc;
|
|||
use std::thread;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::sync::RwLock;
|
||||
use tokio::time::Duration;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tower_lsp::lsp_types as lsp;
|
||||
|
@ -96,13 +96,13 @@ type DiagnosticsBySource = HashMap<DiagnosticSource, VersionedDiagnostics>;
|
|||
#[derive(Debug)]
|
||||
struct DiagnosticsPublisher {
|
||||
client: Client,
|
||||
state: Arc<RwLock<DiagnosticsState>>,
|
||||
state: Arc<DiagnosticsState>,
|
||||
diagnostics_by_specifier:
|
||||
Mutex<HashMap<ModuleSpecifier, DiagnosticsBySource>>,
|
||||
}
|
||||
|
||||
impl DiagnosticsPublisher {
|
||||
pub fn new(client: Client, state: Arc<RwLock<DiagnosticsState>>) -> Self {
|
||||
pub fn new(client: Client, state: Arc<DiagnosticsState>) -> Self {
|
||||
Self {
|
||||
client,
|
||||
state,
|
||||
|
@ -119,7 +119,6 @@ impl DiagnosticsPublisher {
|
|||
) -> usize {
|
||||
let mut diagnostics_by_specifier =
|
||||
self.diagnostics_by_specifier.lock().await;
|
||||
let mut state = self.state.write().await;
|
||||
let mut seen_specifiers = HashSet::with_capacity(diagnostics.len());
|
||||
let mut messages_sent = 0;
|
||||
|
||||
|
@ -146,7 +145,9 @@ impl DiagnosticsPublisher {
|
|||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
state.update(&record.specifier, version, &all_specifier_diagnostics);
|
||||
self
|
||||
.state
|
||||
.update(&record.specifier, version, &all_specifier_diagnostics);
|
||||
self
|
||||
.client
|
||||
.when_outside_lsp_lock()
|
||||
|
@ -177,7 +178,7 @@ impl DiagnosticsPublisher {
|
|||
specifiers_to_remove.push(specifier.clone());
|
||||
if let Some(removed_value) = maybe_removed_value {
|
||||
// clear out any diagnostics for this specifier
|
||||
state.update(specifier, removed_value.version, &[]);
|
||||
self.state.update(specifier, removed_value.version, &[]);
|
||||
self
|
||||
.client
|
||||
.when_outside_lsp_lock()
|
||||
|
@ -296,57 +297,72 @@ struct ChannelUpdateMessage {
|
|||
batch_index: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Debug)]
|
||||
struct SpecifierState {
|
||||
has_no_cache_diagnostic: bool,
|
||||
version: Option<i32>,
|
||||
no_cache_diagnostics: Vec<lsp::Diagnostic>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
#[derive(Debug, Default)]
|
||||
pub struct DiagnosticsState {
|
||||
specifiers: HashMap<ModuleSpecifier, (Option<i32>, SpecifierState)>,
|
||||
specifiers: RwLock<HashMap<ModuleSpecifier, SpecifierState>>,
|
||||
}
|
||||
|
||||
impl DiagnosticsState {
|
||||
fn update(
|
||||
&mut self,
|
||||
&self,
|
||||
specifier: &ModuleSpecifier,
|
||||
version: Option<i32>,
|
||||
diagnostics: &[lsp::Diagnostic],
|
||||
) {
|
||||
let current_version = self.specifiers.get(specifier).and_then(|s| s.0);
|
||||
let is_new = match (version, current_version) {
|
||||
(Some(arg), Some(existing)) => arg >= existing,
|
||||
_ => true,
|
||||
};
|
||||
if is_new {
|
||||
self.specifiers.insert(
|
||||
specifier.clone(),
|
||||
(
|
||||
version,
|
||||
SpecifierState {
|
||||
has_no_cache_diagnostic: diagnostics.iter().any(|d| {
|
||||
d.code
|
||||
== Some(lsp::NumberOrString::String("no-cache".to_string()))
|
||||
|| d.code
|
||||
== Some(lsp::NumberOrString::String(
|
||||
"no-cache-npm".to_string(),
|
||||
))
|
||||
}),
|
||||
},
|
||||
),
|
||||
);
|
||||
let mut specifiers = self.specifiers.write();
|
||||
let current_version = specifiers.get(specifier).and_then(|s| s.version);
|
||||
match (version, current_version) {
|
||||
(Some(arg), Some(existing)) if arg < existing => return,
|
||||
_ => {}
|
||||
}
|
||||
let mut no_cache_diagnostics = vec![];
|
||||
for diagnostic in diagnostics {
|
||||
if diagnostic.code
|
||||
== Some(lsp::NumberOrString::String("no-cache".to_string()))
|
||||
|| diagnostic.code
|
||||
== Some(lsp::NumberOrString::String("no-cache-npm".to_string()))
|
||||
{
|
||||
no_cache_diagnostics.push(diagnostic.clone());
|
||||
}
|
||||
}
|
||||
specifiers.insert(
|
||||
specifier.clone(),
|
||||
SpecifierState {
|
||||
version,
|
||||
no_cache_diagnostics,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
pub fn clear(&mut self, specifier: &ModuleSpecifier) {
|
||||
self.specifiers.remove(specifier);
|
||||
pub fn clear(&self, specifier: &ModuleSpecifier) {
|
||||
self.specifiers.write().remove(specifier);
|
||||
}
|
||||
|
||||
pub fn has_no_cache_diagnostic(&self, specifier: &ModuleSpecifier) -> bool {
|
||||
pub fn has_no_cache_diagnostics(&self, specifier: &ModuleSpecifier) -> bool {
|
||||
self
|
||||
.specifiers
|
||||
.read()
|
||||
.get(specifier)
|
||||
.map_or(false, |s| s.1.has_no_cache_diagnostic)
|
||||
.map(|s| !s.no_cache_diagnostics.is_empty())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn no_cache_diagnostics(
|
||||
&self,
|
||||
specifier: &ModuleSpecifier,
|
||||
) -> Vec<lsp::Diagnostic> {
|
||||
self
|
||||
.specifiers
|
||||
.read()
|
||||
.get(specifier)
|
||||
.map(|s| s.no_cache_diagnostics.clone())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -358,7 +374,7 @@ pub struct DiagnosticsServer {
|
|||
performance: Arc<Performance>,
|
||||
ts_server: Arc<TsServer>,
|
||||
batch_counter: DiagnosticBatchCounter,
|
||||
state: Arc<RwLock<DiagnosticsState>>,
|
||||
state: Arc<DiagnosticsState>,
|
||||
}
|
||||
|
||||
impl DiagnosticsServer {
|
||||
|
@ -366,6 +382,7 @@ impl DiagnosticsServer {
|
|||
client: Client,
|
||||
performance: Arc<Performance>,
|
||||
ts_server: Arc<TsServer>,
|
||||
state: Arc<DiagnosticsState>,
|
||||
) -> Self {
|
||||
DiagnosticsServer {
|
||||
channel: Default::default(),
|
||||
|
@ -374,14 +391,10 @@ impl DiagnosticsServer {
|
|||
performance,
|
||||
ts_server,
|
||||
batch_counter: Default::default(),
|
||||
state: Default::default(),
|
||||
state,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn state(&self) -> Arc<RwLock<DiagnosticsState>> {
|
||||
self.state.clone()
|
||||
}
|
||||
|
||||
pub fn get_ts_diagnostics(
|
||||
&self,
|
||||
specifier: &ModuleSpecifier,
|
||||
|
@ -906,7 +919,7 @@ async fn generate_ts_diagnostics(
|
|||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct DiagnosticDataSpecifier {
|
||||
pub struct DiagnosticDataSpecifier {
|
||||
pub specifier: ModuleSpecifier,
|
||||
}
|
||||
|
||||
|
@ -1050,14 +1063,11 @@ impl DenoDiagnostic {
|
|||
.clone()
|
||||
.ok_or_else(|| anyhow!("Diagnostic is missing data"))?;
|
||||
let data: DiagnosticDataSpecifier = serde_json::from_value(data)?;
|
||||
let title = match code.as_str() {
|
||||
"no-cache" | "no-cache-npm" => {
|
||||
format!("Cache \"{}\" and its dependencies.", data.specifier)
|
||||
}
|
||||
_ => "Cache the data URL and its dependencies.".to_string(),
|
||||
};
|
||||
lsp::CodeAction {
|
||||
title,
|
||||
title: format!(
|
||||
"Cache \"{}\" and its dependencies.",
|
||||
data.specifier
|
||||
),
|
||||
kind: Some(lsp::CodeActionKind::QUICKFIX),
|
||||
diagnostics: Some(vec![diagnostic.clone()]),
|
||||
command: Some(lsp::Command {
|
||||
|
|
|
@ -48,8 +48,10 @@ use super::config::ConfigSnapshot;
|
|||
use super::config::WorkspaceSettings;
|
||||
use super::config::SETTINGS_SECTION;
|
||||
use super::diagnostics;
|
||||
use super::diagnostics::DiagnosticDataSpecifier;
|
||||
use super::diagnostics::DiagnosticServerUpdateMessage;
|
||||
use super::diagnostics::DiagnosticsServer;
|
||||
use super::diagnostics::DiagnosticsState;
|
||||
use super::documents::to_hover_text;
|
||||
use super::documents::to_lsp_range;
|
||||
use super::documents::AssetOrDocument;
|
||||
|
@ -180,6 +182,7 @@ pub struct Inner {
|
|||
/// Configuration information.
|
||||
pub config: Config,
|
||||
deps_http_cache: Arc<dyn HttpCache>,
|
||||
diagnostics_state: Arc<diagnostics::DiagnosticsState>,
|
||||
diagnostics_server: diagnostics::DiagnosticsServer,
|
||||
/// The collection of documents that the server is currently handling, either
|
||||
/// on disk or "open" within the client.
|
||||
|
@ -557,10 +560,12 @@ impl Inner {
|
|||
let ts_server =
|
||||
Arc::new(TsServer::new(performance.clone(), deps_http_cache.clone()));
|
||||
let config = Config::new();
|
||||
let diagnostics_state = Arc::new(DiagnosticsState::default());
|
||||
let diagnostics_server = DiagnosticsServer::new(
|
||||
client.clone(),
|
||||
performance.clone(),
|
||||
ts_server.clone(),
|
||||
diagnostics_state.clone(),
|
||||
);
|
||||
let assets = Assets::new(ts_server.clone());
|
||||
let registry_url = CliNpmRegistryApi::default_url();
|
||||
|
@ -587,6 +592,7 @@ impl Inner {
|
|||
client,
|
||||
config,
|
||||
deps_http_cache,
|
||||
diagnostics_state,
|
||||
diagnostics_server,
|
||||
documents,
|
||||
http_client,
|
||||
|
@ -1442,12 +1448,7 @@ impl Inner {
|
|||
|
||||
async fn did_close(&mut self, params: DidCloseTextDocumentParams) {
|
||||
let mark = self.performance.mark("did_close", Some(¶ms));
|
||||
self
|
||||
.diagnostics_server
|
||||
.state()
|
||||
.write()
|
||||
.await
|
||||
.clear(¶ms.text_document.uri);
|
||||
self.diagnostics_state.clear(¶ms.text_document.uri);
|
||||
if params.text_document.uri.scheme() == "deno" {
|
||||
// we can ignore virtual text documents closing, as they don't need to
|
||||
// be tracked in memory, as they are static assets that won't change
|
||||
|
@ -1914,6 +1915,7 @@ impl Inner {
|
|||
let file_diagnostics = self
|
||||
.diagnostics_server
|
||||
.get_ts_diagnostics(&specifier, asset_or_doc.document_lsp_version());
|
||||
let mut includes_no_cache = false;
|
||||
for diagnostic in &fixable_diagnostics {
|
||||
match diagnostic.source.as_deref() {
|
||||
Some("deno-ts") => {
|
||||
|
@ -1957,12 +1959,21 @@ impl Inner {
|
|||
}
|
||||
}
|
||||
}
|
||||
Some("deno") => code_actions
|
||||
.add_deno_fix_action(&specifier, diagnostic)
|
||||
.map_err(|err| {
|
||||
error!("{}", err);
|
||||
LspError::internal_error()
|
||||
})?,
|
||||
Some("deno") => {
|
||||
if diagnostic.code
|
||||
== Some(NumberOrString::String("no-cache".to_string()))
|
||||
|| diagnostic.code
|
||||
== Some(NumberOrString::String("no-cache-npm".to_string()))
|
||||
{
|
||||
includes_no_cache = true;
|
||||
}
|
||||
code_actions
|
||||
.add_deno_fix_action(&specifier, diagnostic)
|
||||
.map_err(|err| {
|
||||
error!("{}", err);
|
||||
LspError::internal_error()
|
||||
})?
|
||||
}
|
||||
Some("deno-lint") => code_actions
|
||||
.add_deno_lint_ignore_action(
|
||||
&specifier,
|
||||
|
@ -1977,6 +1988,24 @@ impl Inner {
|
|||
_ => (),
|
||||
}
|
||||
}
|
||||
if includes_no_cache {
|
||||
let no_cache_diagnostics =
|
||||
self.diagnostics_state.no_cache_diagnostics(&specifier);
|
||||
let uncached_deps = no_cache_diagnostics
|
||||
.iter()
|
||||
.filter_map(|d| {
|
||||
let data = serde_json::from_value::<DiagnosticDataSpecifier>(
|
||||
d.data.clone().into(),
|
||||
)
|
||||
.ok()?;
|
||||
Some(data.specifier)
|
||||
})
|
||||
.collect::<HashSet<_>>();
|
||||
if uncached_deps.len() > 1 {
|
||||
code_actions
|
||||
.add_cache_all_action(&specifier, no_cache_diagnostics.to_owned());
|
||||
}
|
||||
}
|
||||
code_actions.set_preferred_fixes();
|
||||
all_actions.extend(code_actions.get_response());
|
||||
}
|
||||
|
@ -3277,17 +3306,14 @@ impl tower_lsp::LanguageServer for LanguageServer {
|
|||
if !is_importable_ext(&path) {
|
||||
return;
|
||||
}
|
||||
let diagnostics_state = {
|
||||
{
|
||||
let inner = self.0.read().await;
|
||||
if !inner.config.workspace_settings().cache_on_save
|
||||
|| !inner.config.specifier_enabled(uri)
|
||||
|| !inner.diagnostics_state.has_no_cache_diagnostics(uri)
|
||||
{
|
||||
return;
|
||||
}
|
||||
inner.diagnostics_server.state()
|
||||
};
|
||||
if !diagnostics_state.read().await.has_no_cache_diagnostic(uri) {
|
||||
return;
|
||||
}
|
||||
if let Err(err) = self
|
||||
.cache_request(Some(
|
||||
|
|
|
@ -4503,6 +4503,151 @@ fn lsp_code_actions_deno_cache_npm() {
|
|||
client.shutdown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lsp_code_actions_deno_cache_all() {
|
||||
let context = TestContextBuilder::new().use_temp_cwd().build();
|
||||
let mut client = context.new_lsp_command().build();
|
||||
client.initialize_default();
|
||||
let diagnostics = client.did_open(json!({
|
||||
"textDocument": {
|
||||
"uri": "file:///a/file.ts",
|
||||
"languageId": "typescript",
|
||||
"version": 1,
|
||||
"text": r#"
|
||||
import * as a from "https://deno.land/x/a/mod.ts";
|
||||
import chalk from "npm:chalk";
|
||||
console.log(a);
|
||||
console.log(chalk);
|
||||
"#,
|
||||
}
|
||||
}));
|
||||
assert_eq!(
|
||||
diagnostics.messages_with_source("deno"),
|
||||
serde_json::from_value(json!({
|
||||
"uri": "file:///a/file.ts",
|
||||
"diagnostics": [
|
||||
{
|
||||
"range": {
|
||||
"start": { "line": 1, "character": 27 },
|
||||
"end": { "line": 1, "character": 57 },
|
||||
},
|
||||
"severity": 1,
|
||||
"code": "no-cache",
|
||||
"source": "deno",
|
||||
"message": "Uncached or missing remote URL: https://deno.land/x/a/mod.ts",
|
||||
"data": { "specifier": "https://deno.land/x/a/mod.ts" },
|
||||
},
|
||||
{
|
||||
"range": {
|
||||
"start": { "line": 2, "character": 26 },
|
||||
"end": { "line": 2, "character": 37 },
|
||||
},
|
||||
"severity": 1,
|
||||
"code": "no-cache-npm",
|
||||
"source": "deno",
|
||||
"message": "Uncached or missing npm package: chalk",
|
||||
"data": { "specifier": "npm:chalk" },
|
||||
},
|
||||
],
|
||||
"version": 1,
|
||||
})).unwrap()
|
||||
);
|
||||
|
||||
let res =
|
||||
client
|
||||
.write_request( "textDocument/codeAction",
|
||||
json!({
|
||||
"textDocument": {
|
||||
"uri": "file:///a/file.ts",
|
||||
},
|
||||
"range": {
|
||||
"start": { "line": 1, "character": 27 },
|
||||
"end": { "line": 1, "character": 57 },
|
||||
},
|
||||
"context": {
|
||||
"diagnostics": [{
|
||||
"range": {
|
||||
"start": { "line": 1, "character": 27 },
|
||||
"end": { "line": 1, "character": 57 },
|
||||
},
|
||||
"severity": 1,
|
||||
"code": "no-cache",
|
||||
"source": "deno",
|
||||
"message": "Uncached or missing remote URL: https://deno.land/x/a/mod.ts",
|
||||
"data": {
|
||||
"specifier": "https://deno.land/x/a/mod.ts",
|
||||
},
|
||||
}],
|
||||
"only": ["quickfix"],
|
||||
}
|
||||
}),
|
||||
)
|
||||
;
|
||||
assert_eq!(
|
||||
res,
|
||||
json!([
|
||||
{
|
||||
"title": "Cache \"https://deno.land/x/a/mod.ts\" and its dependencies.",
|
||||
"kind": "quickfix",
|
||||
"diagnostics": [{
|
||||
"range": {
|
||||
"start": { "line": 1, "character": 27 },
|
||||
"end": { "line": 1, "character": 57 },
|
||||
},
|
||||
"severity": 1,
|
||||
"code": "no-cache",
|
||||
"source": "deno",
|
||||
"message": "Uncached or missing remote URL: https://deno.land/x/a/mod.ts",
|
||||
"data": {
|
||||
"specifier": "https://deno.land/x/a/mod.ts",
|
||||
},
|
||||
}],
|
||||
"command": {
|
||||
"title": "",
|
||||
"command": "deno.cache",
|
||||
"arguments": [["https://deno.land/x/a/mod.ts"], "file:///a/file.ts"],
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Cache all dependencies of this module.",
|
||||
"kind": "quickfix",
|
||||
"diagnostics": [
|
||||
{
|
||||
"range": {
|
||||
"start": { "line": 1, "character": 27 },
|
||||
"end": { "line": 1, "character": 57 },
|
||||
},
|
||||
"severity": 1,
|
||||
"code": "no-cache",
|
||||
"source": "deno",
|
||||
"message": "Uncached or missing remote URL: https://deno.land/x/a/mod.ts",
|
||||
"data": {
|
||||
"specifier": "https://deno.land/x/a/mod.ts",
|
||||
},
|
||||
},
|
||||
{
|
||||
"range": {
|
||||
"start": { "line": 2, "character": 26 },
|
||||
"end": { "line": 2, "character": 37 },
|
||||
},
|
||||
"severity": 1,
|
||||
"code": "no-cache-npm",
|
||||
"source": "deno",
|
||||
"message": "Uncached or missing npm package: chalk",
|
||||
"data": { "specifier": "npm:chalk" },
|
||||
},
|
||||
],
|
||||
"command": {
|
||||
"title": "",
|
||||
"command": "deno.cache",
|
||||
"arguments": [[], "file:///a/file.ts"],
|
||||
}
|
||||
},
|
||||
])
|
||||
);
|
||||
client.shutdown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lsp_cache_on_save() {
|
||||
let context = TestContextBuilder::new()
|
||||
|
|
Loading…
Reference in a new issue