diff --git a/cli/lsp/analysis.rs b/cli/lsp/analysis.rs index b79a9ec654..8600301a5c 100644 --- a/cli/lsp/analysis.rs +++ b/cli/lsp/analysis.rs @@ -15,6 +15,7 @@ use deno_core::error::custom_error; use deno_core::error::AnyError; use deno_core::serde::Deserialize; use deno_core::serde::Serialize; +use deno_core::serde_json; use deno_core::serde_json::json; use deno_core::ModuleResolutionError; use deno_core::ModuleSpecifier; @@ -152,6 +153,23 @@ pub enum ResolvedDependencyErr { Missing, } +impl ResolvedDependencyErr { + pub fn as_code(&self) -> lsp::NumberOrString { + match self { + Self::InvalidDowngrade => { + lsp::NumberOrString::String("invalid-downgrade".to_string()) + } + Self::InvalidLocalImport => { + lsp::NumberOrString::String("invalid-local-import".to_string()) + } + Self::InvalidSpecifier(_) => { + lsp::NumberOrString::String("invalid-specifier".to_string()) + } + Self::Missing => lsp::NumberOrString::String("missing".to_string()), + } + } +} + impl fmt::Display for ResolvedDependencyErr { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { @@ -351,30 +369,34 @@ fn is_equivalent_code( /// action for a given set of actions. fn is_preferred( action: &tsc::CodeFixAction, - actions: &[(lsp::CodeAction, tsc::CodeFixAction)], + actions: &[CodeActionKind], fix_priority: u32, only_one: bool, ) -> bool { - actions.iter().all(|(_, a)| { - if action == a { - return true; - } - if a.fix_id.is_some() { - return true; - } - if let Some((other_fix_priority, _)) = - PREFERRED_FIXES.get(a.fix_name.as_str()) - { - match other_fix_priority.cmp(&fix_priority) { - Ordering::Less => return true, - Ordering::Greater => return false, - Ordering::Equal => (), + actions.iter().all(|i| { + if let CodeActionKind::Tsc(_, a) = i { + if action == a { + return true; } - if only_one && action.fix_name == a.fix_name { - return false; + if a.fix_id.is_some() { + return true; } + if let Some((other_fix_priority, _)) = + PREFERRED_FIXES.get(a.fix_name.as_str()) + { + match other_fix_priority.cmp(&fix_priority) { + Ordering::Less => return true, + Ordering::Greater => return false, + Ordering::Equal => (), + } + if only_one && action.fix_name == a.fix_name { + return false; + } + } + true + } else { + true } - true }) } @@ -404,13 +426,58 @@ pub struct CodeActionData { pub fix_id: String, } +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DenoFixData { + pub specifier: ModuleSpecifier, +} + +#[derive(Debug, Clone)] +enum CodeActionKind { + Deno(lsp::CodeAction), + Tsc(lsp::CodeAction, tsc::CodeFixAction), +} + +#[derive(Debug, Hash, PartialEq, Eq)] +enum FixAllKind { + Tsc(String), +} + #[derive(Debug, Default)] pub struct CodeActionCollection { - actions: Vec<(lsp::CodeAction, tsc::CodeFixAction)>, - fix_all_actions: HashMap, + actions: Vec, + fix_all_actions: HashMap, } impl CodeActionCollection { + pub(crate) fn add_deno_fix_action( + &mut self, + diagnostic: &lsp::Diagnostic, + ) -> Result<(), AnyError> { + if let Some(data) = diagnostic.data.clone() { + let fix_data: DenoFixData = serde_json::from_value(data)?; + let code_action = lsp::CodeAction { + title: format!( + "Cache \"{}\" and its dependencies.", + fix_data.specifier + ), + kind: Some(lsp::CodeActionKind::QUICKFIX), + diagnostics: Some(vec![diagnostic.clone()]), + edit: None, + command: Some(lsp::Command { + title: "".to_string(), + command: "deno.cache".to_string(), + arguments: Some(vec![json!([fix_data.specifier])]), + }), + is_preferred: None, + disabled: None, + data: None, + }; + self.actions.push(CodeActionKind::Deno(code_action)); + } + Ok(()) + } + /// Add a TypeScript code fix action to the code actions collection. pub(crate) async fn add_ts_fix_action( &mut self, @@ -442,19 +509,28 @@ impl CodeActionCollection { disabled: None, data: None, }; - self.actions.retain(|(c, a)| { - !(action.fix_name == a.fix_name && code_action.edit == c.edit) + self.actions.retain(|i| match i { + CodeActionKind::Tsc(c, a) => { + !(action.fix_name == a.fix_name && code_action.edit == c.edit) + } + _ => true, }); - self.actions.push((code_action, action.clone())); + self + .actions + .push(CodeActionKind::Tsc(code_action, action.clone())); if let Some(fix_id) = &action.fix_id { - if let Some((existing_fix_all, existing_action)) = - self.fix_all_actions.get(fix_id) + if let Some(CodeActionKind::Tsc(existing_fix_all, existing_action)) = + self.fix_all_actions.get(&FixAllKind::Tsc(fix_id.clone())) { - self.actions.retain(|(c, _)| c != existing_fix_all); - self - .actions - .push((existing_fix_all.clone(), existing_action.clone())); + self.actions.retain(|i| match i { + CodeActionKind::Tsc(c, _) => c != existing_fix_all, + _ => true, + }); + self.actions.push(CodeActionKind::Tsc( + existing_fix_all.clone(), + existing_action.clone(), + )); } } Ok(()) @@ -488,15 +564,21 @@ impl CodeActionCollection { disabled: None, data, }; - if let Some((existing, _)) = - self.fix_all_actions.get(&action.fix_id.clone().unwrap()) + if let Some(CodeActionKind::Tsc(existing, _)) = self + .fix_all_actions + .get(&FixAllKind::Tsc(action.fix_id.clone().unwrap())) { - self.actions.retain(|(c, _)| c != existing); + self.actions.retain(|i| match i { + CodeActionKind::Tsc(c, _) => c != existing, + _ => true, + }); } - self.actions.push((code_action.clone(), action.clone())); + self + .actions + .push(CodeActionKind::Tsc(code_action.clone(), action.clone())); self.fix_all_actions.insert( - action.fix_id.clone().unwrap(), - (code_action, action.clone()), + FixAllKind::Tsc(action.fix_id.clone().unwrap()), + CodeActionKind::Tsc(code_action, action.clone()), ); } @@ -505,7 +587,10 @@ impl CodeActionCollection { self .actions .into_iter() - .map(|(c, _)| lsp::CodeActionOrCommand::CodeAction(c)) + .map(|i| match i { + CodeActionKind::Tsc(c, _) => lsp::CodeActionOrCommand::CodeAction(c), + CodeActionKind::Deno(c) => lsp::CodeActionOrCommand::CodeAction(c), + }) .collect() } @@ -521,7 +606,7 @@ impl CodeActionCollection { if action.fix_id.is_none() || self .fix_all_actions - .contains_key(&action.fix_id.clone().unwrap()) + .contains_key(&FixAllKind::Tsc(action.fix_id.clone().unwrap())) { false } else { @@ -543,15 +628,17 @@ impl CodeActionCollection { /// when all actions are added to the collection. pub fn set_preferred_fixes(&mut self) { let actions = self.actions.clone(); - for (code_action, action) in self.actions.iter_mut() { - if action.fix_id.is_some() { - continue; - } - if let Some((fix_priority, only_one)) = - PREFERRED_FIXES.get(action.fix_name.as_str()) - { - code_action.is_preferred = - Some(is_preferred(action, &actions, *fix_priority, *only_one)); + for entry in self.actions.iter_mut() { + if let CodeActionKind::Tsc(code_action, action) = entry { + if action.fix_id.is_some() { + continue; + } + if let Some((fix_priority, only_one)) = + PREFERRED_FIXES.get(action.fix_name.as_str()) + { + code_action.is_preferred = + Some(is_preferred(action, &actions, *fix_priority, *only_one)); + } } } } diff --git a/cli/lsp/diagnostics.rs b/cli/lsp/diagnostics.rs index 437a965432..1ea2cce146 100644 --- a/cli/lsp/diagnostics.rs +++ b/cli/lsp/diagnostics.rs @@ -11,6 +11,7 @@ use crate::media_type::MediaType; use deno_core::error::AnyError; use deno_core::serde_json; +use deno_core::serde_json::json; use deno_core::ModuleSpecifier; use lspower::lsp; use std::collections::HashMap; @@ -279,14 +280,14 @@ pub async fn generate_dependency_diagnostics( &dependency.maybe_code_specifier_range, ) { match code.clone() { - ResolvedDependency::Err(err) => { + ResolvedDependency::Err(dependency_err) => { diagnostic_list.push(lsp::Diagnostic { range: *range, severity: Some(lsp::DiagnosticSeverity::Error), - code: None, + code: Some(dependency_err.as_code()), code_description: None, source: Some("deno".to_string()), - message: format!("{}", err), + message: format!("{}", dependency_err), related_information: None, tags: None, data: None, @@ -295,20 +296,23 @@ pub async fn generate_dependency_diagnostics( ResolvedDependency::Resolved(specifier) => { if !(state_snapshot.documents.contains(&specifier) || sources.contains(&specifier)) { let is_local = specifier.as_url().scheme() == "file"; + let (code, message) = if is_local { + (Some(lsp::NumberOrString::String("no-local".to_string())), format!("Unable to load a local module: \"{}\".\n Please check the file path.", specifier)) + } else { + (Some(lsp::NumberOrString::String("no-cache".to_string())), format!("Unable to load the remote module: \"{}\".", specifier)) + }; diagnostic_list.push(lsp::Diagnostic { range: *range, severity: Some(lsp::DiagnosticSeverity::Error), - code: None, + code, code_description: None, source: Some("deno".to_string()), - message: if is_local { - format!("Unable to load a local module: \"{}\".\n Please check the file path.", specifier) - } else { - format!("Unable to load the module: \"{}\".\n If the module exists, running `deno cache {}` should resolve this error.", specifier, specifier) - }, + message, related_information: None, tags: None, - data: None, + data: Some(json!({ + "specifier": specifier + })), }) } }, diff --git a/cli/lsp/language_server.rs b/cli/lsp/language_server.rs index 3f1d5f6c2d..7ae4de9786 100644 --- a/cli/lsp/language_server.rs +++ b/cli/lsp/language_server.rs @@ -65,6 +65,7 @@ pub struct LanguageServer(Arc>); pub struct StateSnapshot { pub assets: HashMap>, pub documents: DocumentCache, + pub performance: Performance, pub sources: Sources, } @@ -190,7 +191,7 @@ impl Inner { /// Only searches already cached assets and documents for a line index. If /// the line index cannot be found, `None` is returned. fn get_line_index_sync( - &mut self, + &self, specifier: &ModuleSpecifier, ) -> Option { let mark = self.performance.mark("get_line_index_sync"); @@ -389,6 +390,7 @@ impl Inner { StateSnapshot { assets: self.assets.clone(), documents: self.documents.clone(), + performance: self.performance.clone(), sources: self.sources.clone(), } } @@ -514,7 +516,7 @@ impl Inner { } pub(crate) fn document_version( - &mut self, + &self, specifier: ModuleSpecifier, ) -> Option { self.documents.version(&specifier) @@ -851,7 +853,7 @@ impl Inner { } } - async fn hover(&mut self, params: HoverParams) -> LspResult> { + async fn hover(&self, params: HoverParams) -> LspResult> { if !self.enabled() { return Ok(None); } @@ -912,7 +914,10 @@ impl Inner { } _ => false, }, - // currently only processing `deno-ts` quick fixes + "deno" => match &d.code { + Some(NumberOrString::String(code)) => code == "no-cache", + _ => false, + }, _ => false, }, None => false, @@ -923,53 +928,65 @@ impl Inner { return Ok(None); } let line_index = self.get_line_index_sync(&specifier).unwrap(); + let mut code_actions = CodeActionCollection::default(); let file_diagnostics: Vec = self .diagnostics .diagnostics_for(&specifier, &DiagnosticSource::TypeScript) .cloned() .collect(); - let mut code_actions = CodeActionCollection::default(); for diagnostic in &fixable_diagnostics { - let code = match &diagnostic.code.clone().unwrap() { - NumberOrString::String(code) => code.to_string(), - NumberOrString::Number(code) => code.to_string(), - }; - let codes = vec![code]; - let req = tsc::RequestMethod::GetCodeFixes(( - specifier.clone(), - line_index.offset_tsc(diagnostic.range.start)?, - line_index.offset_tsc(diagnostic.range.end)?, - codes, - )); - let res = - self - .ts_server - .request(self.snapshot(), req) - .await - .map_err(|err| { - error!("Error getting actions from TypeScript: {}", err); - LspError::internal_error() - })?; - let actions: Vec = - from_value(res).map_err(|err| { - error!("Cannot decode actions from TypeScript: {}", err); - LspError::internal_error() - })?; - for action in actions { - code_actions - .add_ts_fix_action(&action, diagnostic, self) - .await - .map_err(|err| { - error!("Unable to convert fix: {}", err); - LspError::internal_error() - })?; - if code_actions.is_fix_all_action( - &action, - diagnostic, - &file_diagnostics, - ) { - code_actions.add_ts_fix_all_action(&action, &specifier, diagnostic); + match diagnostic.source.as_deref() { + Some("deno-ts") => { + let code = match diagnostic.code.as_ref().unwrap() { + NumberOrString::String(code) => code.to_string(), + NumberOrString::Number(code) => code.to_string(), + }; + let codes = vec![code]; + let req = tsc::RequestMethod::GetCodeFixes(( + specifier.clone(), + line_index.offset_tsc(diagnostic.range.start)?, + line_index.offset_tsc(diagnostic.range.end)?, + codes, + )); + let res = + self.ts_server.request(self.snapshot(), req).await.map_err( + |err| { + error!("Error getting actions from TypeScript: {}", err); + LspError::internal_error() + }, + )?; + let actions: Vec = + from_value(res).map_err(|err| { + error!("Cannot decode actions from TypeScript: {}", err); + LspError::internal_error() + })?; + for action in actions { + code_actions + .add_ts_fix_action(&action, diagnostic, self) + .await + .map_err(|err| { + error!("Unable to convert fix: {}", err); + LspError::internal_error() + })?; + if code_actions.is_fix_all_action( + &action, + diagnostic, + &file_diagnostics, + ) { + code_actions + .add_ts_fix_all_action(&action, &specifier, diagnostic); + } + } } + Some("deno") => { + code_actions + .add_deno_fix_action(diagnostic) + .map_err(|err| { + error!("{}", err); + LspError::internal_error() + })? + } + _ => (), } } code_actions.set_preferred_fixes(); @@ -1020,9 +1037,8 @@ impl Inner { Ok(code_action) } } else { - Err(LspError::invalid_params( - "The CodeAction's data is missing.", - )) + // The code action doesn't need to be resolved + Ok(params) }; self.performance.measure(mark); result @@ -1343,7 +1359,7 @@ impl Inner { } async fn document_highlight( - &mut self, + &self, params: DocumentHighlightParams, ) -> LspResult>> { if !self.enabled() { @@ -1481,7 +1497,7 @@ impl Inner { } async fn completion( - &mut self, + &self, params: CompletionParams, ) -> LspResult> { if !self.enabled() { @@ -1830,7 +1846,12 @@ impl lspower::LanguageServer for LanguageServer { #[derive(Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] struct CacheParams { - text_document: TextDocumentIdentifier, + /// The document currently open in the editor. If there are no `uris` + /// supplied, the referrer will be cached. + referrer: TextDocumentIdentifier, + /// Any documents that have been specifically asked to be cached via the + /// command. + uris: Vec, } #[derive(Debug, Deserialize, Serialize)] @@ -1841,19 +1862,38 @@ struct VirtualTextDocumentParams { // These are implementations of custom commands supported by the LSP impl Inner { + /// Similar to `deno cache` on the command line, where modules will be cached + /// in the Deno cache, including any of their dependencies. async fn cache(&mut self, params: CacheParams) -> LspResult { let mark = self.performance.mark("cache"); - let specifier = utils::normalize_url(params.text_document.uri); - let maybe_import_map = self.maybe_import_map.clone(); - sources::cache(specifier.clone(), maybe_import_map) - .await - .map_err(|err| { - error!("{}", err); - LspError::internal_error() - })?; - if self.documents.contains(&specifier) { - self.diagnostics.invalidate(&specifier); + let referrer = utils::normalize_url(params.referrer.uri); + if !params.uris.is_empty() { + for identifier in ¶ms.uris { + let specifier = utils::normalize_url(identifier.uri.clone()); + sources::cache(&specifier, &self.maybe_import_map) + .await + .map_err(|err| { + error!("{}", err); + LspError::internal_error() + })?; + } + } else { + sources::cache(&referrer, &self.maybe_import_map) + .await + .map_err(|err| { + error!("{}", err); + LspError::internal_error() + })?; } + // now that we have dependencies loaded, we need to re-analyze them and + // invalidate some diagnostics + if self.documents.contains(&referrer) { + if let Some(source) = self.documents.content(&referrer).unwrap() { + self.analyze_dependencies(&referrer, &source); + } + self.diagnostics.invalidate(&referrer); + } + self.prepare_diagnostics().await.map_err(|err| { error!("{}", err); LspError::internal_error() @@ -2685,6 +2725,28 @@ mod tests { harness.run().await; } + #[tokio::test] + async fn test_code_actions_deno_cache() { + let mut harness = LspTestHarness::new(vec![ + ("initialize_request.json", LspResponse::RequestAny), + ("initialized_notification.json", LspResponse::None), + ("did_open_notification_cache.json", LspResponse::None), + ( + "code_action_request_cache.json", + LspResponse::RequestFixture( + 2, + "code_action_response_cache.json".to_string(), + ), + ), + ( + "shutdown_request.json", + LspResponse::Request(3, json!(null)), + ), + ("exit_notification.json", LspResponse::None), + ]); + harness.run().await; + } + #[derive(Deserialize)] struct PerformanceAverages { averages: Vec, @@ -2730,7 +2792,7 @@ mod tests { LspResponse::RequestAssert(|value| { let resp: PerformanceResponse = serde_json::from_value(value).unwrap(); - assert_eq!(resp.result.averages.len(), 10); + assert_eq!(resp.result.averages.len(), 12); }), ), ( diff --git a/cli/lsp/performance.rs b/cli/lsp/performance.rs index 8668519c8d..5375831418 100644 --- a/cli/lsp/performance.rs +++ b/cli/lsp/performance.rs @@ -49,7 +49,7 @@ impl From for PerformanceMeasure { /// /// The structure will limit the size of measurements to the most recent 1000, /// and will roll off when that limit is reached. -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Performance { counts: Arc>>, max_size: usize, @@ -127,13 +127,15 @@ impl Performance { /// A function which accepts a previously created performance mark which will /// be used to finalize the duration of the span being measured, and add the /// measurement to the internal buffer. - pub fn measure(&self, mark: PerformanceMark) { + pub fn measure(&self, mark: PerformanceMark) -> Duration { let measure = PerformanceMeasure::from(mark); + let duration = measure.duration; let mut measures = self.measures.lock().unwrap(); measures.push_back(measure); while measures.len() > self.max_size { measures.pop_front(); } + duration } } diff --git a/cli/lsp/sources.rs b/cli/lsp/sources.rs index c2ef27010b..3298b847bb 100644 --- a/cli/lsp/sources.rs +++ b/cli/lsp/sources.rs @@ -28,16 +28,16 @@ use std::sync::Mutex; use std::time::SystemTime; pub async fn cache( - specifier: ModuleSpecifier, - maybe_import_map: Option, + specifier: &ModuleSpecifier, + maybe_import_map: &Option, ) -> Result<(), AnyError> { let program_state = Arc::new(ProgramState::new(Default::default())?); let handler = Arc::new(Mutex::new(FetchHandler::new( &program_state, Permissions::allow_all(), )?)); - let mut builder = GraphBuilder::new(handler, maybe_import_map, None); - builder.add(&specifier, false).await + let mut builder = GraphBuilder::new(handler, maybe_import_map.clone(), None); + builder.add(specifier, false).await } #[derive(Debug, Clone, Default)] @@ -51,7 +51,7 @@ struct Metadata { } #[derive(Debug, Clone, Default)] -pub struct Sources { +struct Inner { http_cache: HttpCache, maybe_import_map: Option, metadata: HashMap, @@ -59,15 +59,80 @@ pub struct Sources { remotes: HashMap, } +#[derive(Debug, Clone, Default)] +pub struct Sources(Arc>); + impl Sources { pub fn new(location: &Path) -> Self { + Self(Arc::new(Mutex::new(Inner::new(location)))) + } + + pub fn contains(&self, specifier: &ModuleSpecifier) -> bool { + self.0.lock().unwrap().contains(specifier) + } + + pub fn get_length_utf16(&self, specifier: &ModuleSpecifier) -> Option { + self.0.lock().unwrap().get_length_utf16(specifier) + } + + pub fn get_line_index( + &self, + specifier: &ModuleSpecifier, + ) -> Option { + self.0.lock().unwrap().get_line_index(specifier) + } + + pub fn get_maybe_types( + &self, + specifier: &ModuleSpecifier, + ) -> Option { + self.0.lock().unwrap().get_maybe_types(specifier) + } + + pub fn get_media_type( + &self, + specifier: &ModuleSpecifier, + ) -> Option { + self.0.lock().unwrap().get_media_type(specifier) + } + + pub fn get_script_version( + &self, + specifier: &ModuleSpecifier, + ) -> Option { + self.0.lock().unwrap().get_script_version(specifier) + } + + pub fn get_text(&self, specifier: &ModuleSpecifier) -> Option { + self.0.lock().unwrap().get_text(specifier) + } + + pub fn resolve_import( + &self, + specifier: &str, + referrer: &ModuleSpecifier, + ) -> Option<(ModuleSpecifier, MediaType)> { + self.0.lock().unwrap().resolve_import(specifier, referrer) + } + + #[cfg(test)] + fn resolve_specifier( + &self, + specifier: &ModuleSpecifier, + ) -> Option { + self.0.lock().unwrap().resolve_specifier(specifier) + } +} + +impl Inner { + fn new(location: &Path) -> Self { Self { http_cache: HttpCache::new(location), ..Default::default() } } - pub fn contains(&mut self, specifier: &ModuleSpecifier) -> bool { + fn contains(&mut self, specifier: &ModuleSpecifier) -> bool { if let Some(specifier) = self.resolve_specifier(specifier) { if self.get_metadata(&specifier).is_some() { return true; @@ -80,16 +145,13 @@ impl Sources { /// match the behavior of JavaScript, where strings are stored effectively as /// `&[u16]` and when counting "chars" we need to represent the string as a /// UTF-16 string in Rust. - pub fn get_length_utf16( - &mut self, - specifier: &ModuleSpecifier, - ) -> Option { + fn get_length_utf16(&mut self, specifier: &ModuleSpecifier) -> Option { let specifier = self.resolve_specifier(specifier)?; let metadata = self.get_metadata(&specifier)?; Some(metadata.source.encode_utf16().count()) } - pub fn get_line_index( + fn get_line_index( &mut self, specifier: &ModuleSpecifier, ) -> Option { @@ -98,7 +160,7 @@ impl Sources { Some(metadata.line_index) } - pub fn get_maybe_types( + fn get_maybe_types( &mut self, specifier: &ModuleSpecifier, ) -> Option { @@ -106,7 +168,7 @@ impl Sources { metadata.maybe_types } - pub fn get_media_type( + fn get_media_type( &mut self, specifier: &ModuleSpecifier, ) -> Option { @@ -117,12 +179,11 @@ impl Sources { fn get_metadata(&mut self, specifier: &ModuleSpecifier) -> Option { if let Some(metadata) = self.metadata.get(specifier).cloned() { - if let Some(current_version) = self.get_script_version(specifier) { - if metadata.version == current_version { - return Some(metadata); - } + if metadata.version == self.get_script_version(specifier)? { + return Some(metadata); } } + // TODO(@kitsonk) this needs to be refactored, lots of duplicate logic and // is really difficult to follow. let version = self.get_script_version(specifier)?; @@ -248,28 +309,24 @@ impl Sources { None } - pub fn get_script_version( + fn get_script_version( &mut self, specifier: &ModuleSpecifier, ) -> Option { - if let Some(path) = self.get_path(specifier) { - if let Ok(metadata) = fs::metadata(path) { - if let Ok(modified) = metadata.modified() { - return if let Ok(n) = modified.duration_since(SystemTime::UNIX_EPOCH) - { - Some(format!("{}", n.as_millis())) - } else { - Some("1".to_string()) - }; - } else { - return Some("1".to_string()); - } + let path = self.get_path(specifier)?; + let metadata = fs::metadata(path).ok()?; + if let Ok(modified) = metadata.modified() { + if let Ok(n) = modified.duration_since(SystemTime::UNIX_EPOCH) { + Some(format!("{}", n.as_millis())) + } else { + Some("1".to_string()) } + } else { + Some("1".to_string()) } - None } - pub fn get_text(&mut self, specifier: &ModuleSpecifier) -> Option { + fn get_text(&mut self, specifier: &ModuleSpecifier) -> Option { let specifier = self.resolve_specifier(specifier)?; let metadata = self.get_metadata(&specifier)?; Some(metadata.source) @@ -289,7 +346,7 @@ impl Sources { Some((resolved_specifier, media_type)) } - pub fn resolve_import( + fn resolve_import( &mut self, specifier: &str, referrer: &ModuleSpecifier, @@ -385,7 +442,7 @@ mod tests { #[test] fn test_sources_get_script_version() { - let (mut sources, _) = setup(); + let (sources, _) = setup(); let c = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()); let tests = c.join("tests"); let specifier = ModuleSpecifier::resolve_path( @@ -398,7 +455,7 @@ mod tests { #[test] fn test_sources_get_text() { - let (mut sources, _) = setup(); + let (sources, _) = setup(); let c = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()); let tests = c.join("tests"); let specifier = ModuleSpecifier::resolve_path( @@ -413,7 +470,7 @@ mod tests { #[test] fn test_sources_get_length_utf16() { - let (mut sources, _) = setup(); + let (sources, _) = setup(); let c = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()); let tests = c.join("tests"); let specifier = ModuleSpecifier::resolve_path( @@ -428,7 +485,7 @@ mod tests { #[test] fn test_sources_resolve_specifier_non_supported_schema() { - let (mut sources, _) = setup(); + let (sources, _) = setup(); let specifier = ModuleSpecifier::resolve_url("foo://a/b/c.ts") .expect("could not create specifier"); let actual = sources.resolve_specifier(&specifier); diff --git a/cli/lsp/tsc.rs b/cli/lsp/tsc.rs index 588487517a..a66541dd26 100644 --- a/cli/lsp/tsc.rs +++ b/cli/lsp/tsc.rs @@ -978,10 +978,12 @@ struct SourceSnapshotArgs { /// The language service is dropping a reference to a source file snapshot, and /// we can drop our version of that document. fn dispose(state: &mut State, args: Value) -> Result { + let mark = state.state_snapshot.performance.mark("op_dispose"); let v: SourceSnapshotArgs = serde_json::from_value(args)?; state .snapshots .remove(&(v.specifier.into(), v.version.into())); + state.state_snapshot.performance.measure(mark); Ok(json!(true)) } @@ -997,6 +999,7 @@ struct GetChangeRangeArgs { /// The language service wants to compare an old snapshot with a new snapshot to /// determine what source hash changed. fn get_change_range(state: &mut State, args: Value) -> Result { + let mark = state.state_snapshot.performance.mark("op_get_change_range"); let v: GetChangeRangeArgs = serde_json::from_value(args.clone())?; cache_snapshot(state, v.specifier.clone(), v.version.clone())?; if let Some(current) = state @@ -1007,8 +1010,11 @@ fn get_change_range(state: &mut State, args: Value) -> Result { .snapshots .get(&(v.specifier.clone().into(), v.old_version.clone().into())) { + state.state_snapshot.performance.measure(mark); Ok(text::get_range_change(prev, current)) } else { + let new_length = current.encode_utf16().count(); + state.state_snapshot.performance.measure(mark); // when a local file is opened up in the editor, the compiler might // already have a snapshot of it in memory, and will request it, but we // now are working off in memory versions of the document, and so need @@ -1018,10 +1024,11 @@ fn get_change_range(state: &mut State, args: Value) -> Result { "start": 0, "length": v.old_length, }, - "newLength": current.encode_utf16().count(), + "newLength": new_length, })) } } else { + state.state_snapshot.performance.measure(mark); Err(custom_error( "MissingSnapshot", format!( @@ -1033,6 +1040,7 @@ fn get_change_range(state: &mut State, args: Value) -> Result { } fn get_length(state: &mut State, args: Value) -> Result { + let mark = state.state_snapshot.performance.mark("op_get_length"); let v: SourceSnapshotArgs = serde_json::from_value(args)?; let specifier = ModuleSpecifier::resolve_url(&v.specifier)?; if state.state_snapshot.documents.contains(&specifier) { @@ -1041,9 +1049,11 @@ fn get_length(state: &mut State, args: Value) -> Result { .snapshots .get(&(v.specifier.into(), v.version.into())) .unwrap(); + state.state_snapshot.performance.measure(mark); Ok(json!(content.encode_utf16().count())) } else { let sources = &mut state.state_snapshot.sources; + state.state_snapshot.performance.measure(mark); Ok(json!(sources.get_length_utf16(&specifier).unwrap())) } } @@ -1058,6 +1068,7 @@ struct GetTextArgs { } fn get_text(state: &mut State, args: Value) -> Result { + let mark = state.state_snapshot.performance.mark("op_get_text"); let v: GetTextArgs = serde_json::from_value(args)?; let specifier = ModuleSpecifier::resolve_url(&v.specifier)?; let content = if state.state_snapshot.documents.contains(&specifier) { @@ -1071,10 +1082,12 @@ fn get_text(state: &mut State, args: Value) -> Result { let sources = &mut state.state_snapshot.sources; sources.get_text(&specifier).unwrap() }; + state.state_snapshot.performance.measure(mark); Ok(json!(text::slice(&content, v.start..v.end))) } fn resolve(state: &mut State, args: Value) -> Result { + let mark = state.state_snapshot.performance.mark("op_resolve"); let v: ResolveArgs = serde_json::from_value(args)?; let mut resolved = Vec::>::new(); let referrer = ModuleSpecifier::resolve_url(&v.base)?; @@ -1102,9 +1115,13 @@ fn resolve(state: &mut State, args: Value) -> Result { if let ResolvedDependency::Resolved(resolved_specifier) = resolved_import { - if state.state_snapshot.documents.contains(&resolved_specifier) - || sources.contains(&resolved_specifier) - { + if state.state_snapshot.documents.contains(&resolved_specifier) { + let media_type = MediaType::from(&resolved_specifier); + resolved.push(Some(( + resolved_specifier.to_string(), + media_type.as_ts_extension(), + ))); + } else if sources.contains(&resolved_specifier) { let media_type = if let Some(media_type) = sources.get_media_type(&resolved_specifier) { @@ -1139,6 +1156,7 @@ fn resolve(state: &mut State, args: Value) -> Result { } } } else { + state.state_snapshot.performance.measure(mark); return Err(custom_error( "NotFound", format!( @@ -1148,6 +1166,7 @@ fn resolve(state: &mut State, args: Value) -> Result { )); } + state.state_snapshot.performance.measure(mark); Ok(json!(resolved)) } @@ -1167,6 +1186,7 @@ struct ScriptVersionArgs { } fn script_version(state: &mut State, args: Value) -> Result { + let mark = state.state_snapshot.performance.mark("op_script_version"); let v: ScriptVersionArgs = serde_json::from_value(args)?; let specifier = ModuleSpecifier::resolve_url(&v.specifier)?; if let Some(version) = state.state_snapshot.documents.version(&specifier) { @@ -1178,6 +1198,7 @@ fn script_version(state: &mut State, args: Value) -> Result { } } + state.state_snapshot.performance.measure(mark); Ok(json!(None::)) } @@ -1480,9 +1501,8 @@ mod tests { documents.open(specifier, version, content); } StateSnapshot { - assets: Default::default(), documents, - sources: Default::default(), + ..Default::default() } } diff --git a/cli/tests/lsp/code_action_request_cache.json b/cli/tests/lsp/code_action_request_cache.json new file mode 100644 index 0000000000..8e296be327 --- /dev/null +++ b/cli/tests/lsp/code_action_request_cache.json @@ -0,0 +1,46 @@ +{ + "jsonrpc": "2.0", + "id": 2, + "method": "textDocument/codeAction", + "params": { + "textDocument": { + "uri": "file:///a/file.ts" + }, + "range": { + "start": { + "line": 0, + "character": 19 + }, + "end": { + "line": 0, + "character": 49 + } + }, + "context": { + "diagnostics": [ + { + "range": { + "start": { + "line": 0, + "character": 19 + }, + "end": { + "line": 0, + "character": 49 + } + }, + "severity": 1, + "code": "no-cache", + "source": "deno", + "message": "Unable to load the remote module: \"https://deno.land/x/a/mod.ts\".", + "data": { + "specifier": "https://deno.land/x/a/mod.ts" + } + } + ], + "only": [ + "quickfix" + ] + } + } +} diff --git a/cli/tests/lsp/code_action_response_cache.json b/cli/tests/lsp/code_action_response_cache.json new file mode 100644 index 0000000000..c56b350233 --- /dev/null +++ b/cli/tests/lsp/code_action_response_cache.json @@ -0,0 +1,36 @@ +[ + { + "title": "Cache \"https://deno.land/x/a/mod.ts\" and its dependencies.", + "kind": "quickfix", + "diagnostics": [ + { + "range": { + "start": { + "line": 0, + "character": 19 + }, + "end": { + "line": 0, + "character": 49 + } + }, + "severity": 1, + "code": "no-cache", + "source": "deno", + "message": "Unable to load the remote module: \"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" + ] + ] + } + } +] diff --git a/cli/tests/lsp/did_open_notification_cache.json b/cli/tests/lsp/did_open_notification_cache.json new file mode 100644 index 0000000000..6f21ee5cd9 --- /dev/null +++ b/cli/tests/lsp/did_open_notification_cache.json @@ -0,0 +1,12 @@ +{ + "jsonrpc": "2.0", + "method": "textDocument/didOpen", + "params": { + "textDocument": { + "uri": "file:///a/file.ts", + "languageId": "typescript", + "version": 1, + "text": "import * as a from \"https://deno.land/x/a/mod.ts\";\n\nconsole.log(a);\n" + } + } +}