diff --git a/cli/lsp/capabilities.rs b/cli/lsp/capabilities.rs index e253b2e22c..1efdce037c 100644 --- a/cli/lsp/capabilities.rs +++ b/cli/lsp/capabilities.rs @@ -200,6 +200,25 @@ pub fn server_capabilities( diagnostic_provider: None, inline_value_provider: None, inline_completion_provider: None, - notebook_document_sync: None, + notebook_document_sync: Some(OneOf::Left(NotebookDocumentSyncOptions { + notebook_selector: vec![NotebookSelector::ByCells { + notebook: None, + cells: vec![ + NotebookCellSelector { + language: "javascript".to_string(), + }, + NotebookCellSelector { + language: "javascriptreact".to_string(), + }, + NotebookCellSelector { + language: "typescript".to_string(), + }, + NotebookCellSelector { + language: "typescriptreact".to_string(), + }, + ], + }], + save: Some(true), + })), } } diff --git a/cli/lsp/completions.rs b/cli/lsp/completions.rs index 650f26b3fe..5372180cbd 100644 --- a/cli/lsp/completions.rs +++ b/cli/lsp/completions.rs @@ -851,6 +851,7 @@ mod tests { *version, *language_id, (*source).into(), + None, ); } for (specifier, source) in fs_sources { diff --git a/cli/lsp/diagnostics.rs b/cli/lsp/diagnostics.rs index bde760cd3e..20269119f9 100644 --- a/cli/lsp/diagnostics.rs +++ b/cli/lsp/diagnostics.rs @@ -4,6 +4,7 @@ use std::collections::BTreeMap; use std::collections::HashMap; use std::collections::HashSet; use std::path::PathBuf; +use std::str::FromStr; use std::sync::atomic::AtomicUsize; use std::sync::Arc; use std::thread; @@ -15,6 +16,7 @@ use deno_core::anyhow::anyhow; use deno_core::error::AnyError; use deno_core::parking_lot::Mutex; use deno_core::parking_lot::RwLock; +use deno_core::resolve_url; use deno_core::serde::Deserialize; use deno_core::serde_json; use deno_core::serde_json::json; @@ -49,13 +51,13 @@ use super::client::Client; use super::config::Config; use super::documents::Document; use super::documents::DocumentModule; +use super::documents::DocumentModules; use super::language_server; use super::language_server::StateSnapshot; use super::performance::Performance; use super::tsc; use super::tsc::MaybeAmbientModules; use super::tsc::TsServer; -use super::urls::uri_parse_unencoded; use crate::graph_util; use crate::graph_util::enhanced_resolution_error_message; use crate::lsp::logging::lsp_warn; @@ -867,6 +869,8 @@ fn to_lsp_range( fn to_lsp_related_information( related_information: &Option>, + module: &DocumentModule, + document_modules: &DocumentModules, ) -> Option> { related_information.as_ref().map(|related| { related @@ -875,7 +879,13 @@ fn to_lsp_related_information( if let (Some(file_name), Some(start), Some(end)) = (&ri.file_name, &ri.start, &ri.end) { - let uri = uri_parse_unencoded(file_name).unwrap(); + let uri = resolve_url(file_name) + .ok() + .and_then(|s| { + document_modules.module_for_specifier(&s, module.scope.as_deref()) + }) + .map(|m| m.uri.as_ref().clone()) + .unwrap_or_else(|| Uri::from_str("unknown:").unwrap()); Some(lsp::DiagnosticRelatedInformation { location: lsp::Location { uri, @@ -893,6 +903,8 @@ fn to_lsp_related_information( fn ts_json_to_diagnostics( diagnostics: Vec, + module: &DocumentModule, + document_modules: &DocumentModules, ) -> Vec { diagnostics .iter() @@ -907,6 +919,8 @@ fn ts_json_to_diagnostics( message: get_diagnostic_message(d), related_information: to_lsp_related_information( &d.related_information, + module, + document_modules, ), tags: match d.code { // These are codes that indicate the variable is unused. @@ -933,6 +947,12 @@ fn generate_lint_diagnostics( let config_data_by_scope = config.tree.data_by_scope(); let mut records = Vec::new(); for document in snapshot.document_modules.documents.open_docs() { + // TODO(nayeemrmn): Support linting notebooks cells. Will require stitching + // cells from the same notebook into one module, linting it and then + // splitting/relocating the diagnostics to each cell. + if document.notebook_uri.is_some() { + continue; + } let Some(module) = snapshot .document_modules .primary_module(&Document::Open(document.clone())) @@ -1054,7 +1074,7 @@ async fn generate_ts_diagnostics( { if config.specifier_enabled(&module.specifier) { enabled_modules_by_scope - .entry(module.scope.clone()) + .entry((module.scope.clone(), module.notebook_uri.clone())) .or_default() .push(module); continue; @@ -1075,18 +1095,21 @@ async fn generate_ts_diagnostics( }); } let mut enabled_modules_with_diagnostics = Vec::new(); - for (scope, enabled_modules) in enabled_modules_by_scope { + for ((scope, notebook_uri), enabled_modules) in enabled_modules_by_scope { let (diagnostics_list, ambient_modules) = ts_server .get_diagnostics( snapshot.clone(), enabled_modules.iter().map(|m| m.specifier.as_ref()), scope.as_ref(), + notebook_uri.as_ref(), &token, ) .await?; enabled_modules_with_diagnostics .extend(enabled_modules.into_iter().zip(diagnostics_list)); - ambient_modules_by_scope.insert(scope.clone(), ambient_modules); + if notebook_uri.is_none() { + ambient_modules_by_scope.insert(scope, ambient_modules); + } } for (module, mut diagnostics) in enabled_modules_with_diagnostics { let suggestion_actions_settings = snapshot @@ -1103,7 +1126,8 @@ async fn generate_ts_diagnostics( || d.reports_unnecessary == Some(true) }); } - let diagnostics = ts_json_to_diagnostics(diagnostics); + let diagnostics = + ts_json_to_diagnostics(diagnostics, &module, &snapshot.document_modules); records.push(DiagnosticRecord { uri: module.uri.clone(), versioned: VersionedDiagnostics { @@ -2032,6 +2056,7 @@ mod tests { *version, *language_id, (*source).into(), + None, ); } ( diff --git a/cli/lsp/documents.rs b/cli/lsp/documents.rs index 8efe241937..a4f06c65d0 100644 --- a/cli/lsp/documents.rs +++ b/cli/lsp/documents.rs @@ -58,6 +58,7 @@ use super::resolver::SingleReferrerGraphResolver; use super::testing::TestCollector; use super::testing::TestModule; use super::text::LineIndex; +use super::tsc::ChangeKind; use super::tsc::NavigationTree; use super::urls::uri_is_file_like; use super::urls::uri_to_file_path; @@ -73,6 +74,7 @@ pub struct OpenDocument { pub line_index: Arc, pub version: i32, pub language_id: LanguageId, + pub notebook_uri: Option>, pub fs_version_on_open: Option, } @@ -82,6 +84,7 @@ impl OpenDocument { version: i32, language_id: LanguageId, text: Arc, + notebook_uri: Option>, ) -> Self { let line_index = Arc::new(LineIndex::new(&text)); let fs_version_on_open = uri_to_file_path(&uri) @@ -93,6 +96,7 @@ impl OpenDocument { line_index, version, language_id, + notebook_uri, fs_version_on_open, } } @@ -130,6 +134,7 @@ impl OpenDocument { line_index, version, language_id: self.language_id, + notebook_uri: self.notebook_uri.clone(), fs_version_on_open: self.fs_version_on_open.clone(), }) } @@ -480,6 +485,7 @@ impl Document { pub struct Documents { open: IndexMap>, server: Arc>>, + cells_by_notebook_uri: BTreeMap, Vec>>, file_like_uris_by_url: Arc>>, /// These URLs can not be recovered from the URIs we assign them without these /// maps. We want to be able to discard old documents from here but keep these @@ -489,16 +495,22 @@ pub struct Documents { } impl Documents { - pub fn open( + fn open( &mut self, uri: Uri, version: i32, language_id: LanguageId, text: Arc, + notebook_uri: Option>, ) -> Arc { self.server.remove(&uri); - let doc = - Arc::new(OpenDocument::new(uri.clone(), version, language_id, text)); + let doc = Arc::new(OpenDocument::new( + uri.clone(), + version, + language_id, + text, + notebook_uri, + )); self.open.insert(uri, doc.clone()); if !doc.uri.scheme().is_some_and(|s| s.eq_lowercase("file")) { let url = uri_to_url(&doc.uri); @@ -509,7 +521,7 @@ impl Documents { doc } - pub fn change( + fn change( &mut self, uri: &Uri, version: i32, @@ -532,9 +544,9 @@ impl Documents { Ok(doc) } - pub fn close(&mut self, uri: &Uri) -> Result, AnyError> { + fn close(&mut self, uri: &Uri) -> Result, AnyError> { self.file_like_uris_by_url.retain(|_, u| u.as_ref() != uri); - self.open.shift_remove(uri).ok_or_else(|| { + let doc = self.open.shift_remove(uri).ok_or_else(|| { JsErrorBox::new( "NotFound", format!( @@ -542,8 +554,134 @@ impl Documents { uri.as_str() ), ) - .into() - }) + })?; + Ok(doc) + } + + fn open_notebook( + &mut self, + uri: Uri, + cells: Vec, + ) -> Vec> { + let uri = Arc::new(uri); + let mut documents = Vec::with_capacity(cells.len()); + for cell in cells { + let language_id = cell.language_id.parse().unwrap_or_else(|err| { + lsp_warn!("{:#}", err); + LanguageId::Unknown + }); + if language_id == LanguageId::Unknown { + lsp_warn!( + "Unsupported language id \"{}\" received for document \"{}\".", + cell.language_id, + cell.uri.as_str() + ); + } + let document = self.open( + cell.uri.clone(), + cell.version, + language_id, + cell.text.into(), + Some(uri.clone()), + ); + documents.push(document); + } + self + .cells_by_notebook_uri + .insert(uri, documents.iter().map(|d| d.uri.clone()).collect()); + documents + } + + pub fn change_notebook( + &mut self, + uri: &Uri, + structure: Option, + content: Option>, + ) -> Vec<(Arc, ChangeKind)> { + let uri = Arc::new(uri.clone()); + let mut documents_with_change_kinds = Vec::new(); + if let Some(structure) = structure { + if let Some(cells) = self.cells_by_notebook_uri.get_mut(&uri) { + cells.splice( + structure.array.start as usize + ..(structure.array.start + structure.array.delete_count) as usize, + structure + .array + .cells + .into_iter() + .flatten() + .map(|c| Arc::new(c.document)), + ); + } + for closed in structure.did_close.into_iter().flatten() { + let document = match self.close(&closed.uri) { + Ok(d) => d, + Err(err) => { + lsp_warn!("{:#}", err); + continue; + } + }; + documents_with_change_kinds.push((document, ChangeKind::Closed)); + } + for opened in structure.did_open.into_iter().flatten() { + let language_id = opened.language_id.parse().unwrap_or_else(|err| { + lsp_warn!("{:#}", err); + LanguageId::Unknown + }); + if language_id == LanguageId::Unknown { + lsp_warn!( + "Unsupported language id \"{}\" received for document \"{}\".", + opened.language_id, + opened.uri.as_str() + ); + } + let document = self.open( + opened.uri, + opened.version, + language_id, + opened.text.into(), + Some(uri.clone()), + ); + documents_with_change_kinds.push((document, ChangeKind::Opened)); + } + } + for changed in content.into_iter().flatten() { + let document = match self.change( + &changed.document.uri, + changed.document.version, + changed.changes, + ) { + Ok(d) => d, + Err(err) => { + lsp_warn!("{:#}", err); + continue; + } + }; + documents_with_change_kinds.push((document, ChangeKind::Modified)); + } + documents_with_change_kinds + } + + pub fn close_notebook(&mut self, uri: &Uri) -> Vec> { + let Some(cell_uris) = self.cells_by_notebook_uri.remove(uri) else { + lsp_warn!( + "The URI \"{}\" does not refer to an open notebook document.", + uri.as_str(), + ); + return Default::default(); + }; + let mut documents = Vec::with_capacity(cell_uris.len()); + for cell_uri in cell_uris { + let document = match self.close(&cell_uri) { + Ok(d) => d, + Err(err) => { + lsp_warn!("{:#}", err); + continue; + } + }; + documents.push(document); + } + documents } pub fn get(&self, uri: &Uri) -> Option { @@ -629,6 +767,10 @@ impl Documents { } } + pub fn cells_by_notebook_uri(&self) -> &BTreeMap, Vec>> { + &self.cells_by_notebook_uri + } + pub fn open_docs(&self) -> impl Iterator> { self.open.values() } @@ -684,6 +826,7 @@ pub struct DocumentModuleOpenData { pub struct DocumentModule { pub uri: Arc, pub open_data: Option, + pub notebook_uri: Option>, pub script_version: String, pub specifier: Arc, pub scope: Option>, @@ -718,10 +861,11 @@ impl DocumentModule { Some(cache_entry.metadata.headers) }) .flatten(); + let open_document = document.open(); let media_type = resolve_media_type( &specifier, headers.as_ref(), - document.open().map(|d| d.language_id), + open_document.map(|d| d.language_id), ); let (parsed_source, maybe_module, resolution_mode) = if media_type_is_diagnosable(media_type) { @@ -748,10 +892,11 @@ impl DocumentModule { get_maybe_test_module_fut(parsed_source.as_ref(), config); DocumentModule { uri: document.uri().clone(), - open_data: document.open().map(|d| DocumentModuleOpenData { + open_data: open_document.map(|d| DocumentModuleOpenData { version: d.version, parsed_source, }), + notebook_uri: open_document.and_then(|d| d.notebook_uri.clone()), script_version: document.script_version(), specifier, scope, @@ -935,9 +1080,12 @@ impl DocumentModules { version: i32, language_id: LanguageId, text: Arc, + notebook_uri: Option>, ) -> Arc { self.dep_info_by_scope = Default::default(); - self.documents.open(uri, version, language_id, text) + self + .documents + .open(uri, version, language_id, text, notebook_uri) } pub fn change_document( @@ -968,6 +1116,33 @@ impl DocumentModules { Ok(document) } + pub fn open_notebook_document( + &mut self, + uri: Uri, + cells: Vec, + ) -> Vec> { + self.dep_info_by_scope = Default::default(); + self.documents.open_notebook(uri, cells) + } + + pub fn change_notebook_document( + &mut self, + uri: &Uri, + structure: Option, + content: Option>, + ) -> Vec<(Arc, ChangeKind)> { + self.dep_info_by_scope = Default::default(); + self.documents.change_notebook(uri, structure, content) + } + + pub fn close_notebook_document( + &mut self, + uri: &Uri, + ) -> Vec> { + self.dep_info_by_scope = Default::default(); + self.documents.close_notebook(uri) + } + pub fn release(&self, specifier: &Url, scope: Option<&Url>) { let Some(module) = self.module_for_specifier(specifier, scope) else { return; @@ -975,6 +1150,28 @@ impl DocumentModules { self.documents.remove_server_doc(&module.uri); } + fn infer_specifier(&self, document: &Document) -> Option> { + if let Some(document) = document.server() { + match &document.kind { + ServerDocumentKind::Fs { .. } => {} + ServerDocumentKind::RemoteUrl { url, .. } => return Some(url.clone()), + ServerDocumentKind::DataUrl { url, .. } => return Some(url.clone()), + ServerDocumentKind::Asset { url, .. } => return Some(url.clone()), + } + } + let uri = document.uri(); + let url = uri_to_url(uri); + if url.scheme() != "file" { + return None; + } + if uri.scheme().is_some_and(|s| s.eq_lowercase("file")) { + if let Some(remote_specifier) = self.cache.unvendored_specifier(&url) { + return Some(Arc::new(remote_specifier)); + } + } + Some(Arc::new(url)) + } + fn module_inner( &self, document: &Document, @@ -987,35 +1184,7 @@ impl DocumentModules { } let specifier = specifier .cloned() - .or_else(|| { - if let Some(document) = document.server() { - match &document.kind { - ServerDocumentKind::Fs { .. } => {} - ServerDocumentKind::RemoteUrl { url, .. } => { - return Some(url.clone()) - } - ServerDocumentKind::DataUrl { url, .. } => { - return Some(url.clone()) - } - ServerDocumentKind::Asset { url, .. } => return Some(url.clone()), - } - } - None - }) - .or_else(|| { - let uri = document.uri(); - let url = uri_to_url(uri); - if url.scheme() != "file" { - return None; - } - if uri.scheme().is_some_and(|s| s.eq_lowercase("file")) { - if let Some(remote_specifier) = self.cache.unvendored_specifier(&url) - { - return Some(Arc::new(remote_specifier)); - } - } - Some(Arc::new(url)) - })?; + .or_else(|| self.infer_specifier(document))?; let module = Arc::new(DocumentModule::new( document, specifier, @@ -1110,8 +1279,12 @@ impl DocumentModules { if modules_with_scopes.contains_key(uri) { continue; } + let open_document = document.open(); + if open_document.is_some_and(|d| d.notebook_uri.is_some()) { + continue; + } let url = uri_to_url(uri); - if document.open().is_none() + if open_document.is_none() && (url.scheme() != "file" || !self.config.specifier_enabled(&url) || self.resolver.in_node_modules(&url) @@ -1169,23 +1342,6 @@ impl DocumentModules { self.modules_unscoped.get(document) } - /// This will not create any module entries, only retrieve existing entries. - pub fn inspect_modules_by_scope( - &self, - document: &Document, - ) -> BTreeMap>, Arc> { - let mut result = BTreeMap::new(); - for (scope, modules) in self.modules_by_scope.iter() { - if let Some(module) = modules.get(document) { - result.insert(Some(scope.clone()), module); - } - } - if let Some(module) = self.modules_unscoped.get(document) { - result.insert(None, module); - } - result - } - /// This will not store any module entries, only retrieve existing entries or /// create temporary entries for scopes where one doesn't exist. pub fn inspect_or_temp_modules_by_scope( @@ -1230,7 +1386,7 @@ impl DocumentModules { } } - fn primary_scope(&self, uri: &Uri) -> Option>> { + pub fn primary_scope(&self, uri: &Uri) -> Option>> { let url = uri_to_url(uri); if url.scheme() == "file" && !self.cache.in_global_cache_directory(&url) { let scope = self.config.tree.scope_for_specifier(&url); @@ -1239,6 +1395,13 @@ impl DocumentModules { None } + pub fn primary_specifier(&self, document: &Document) -> Option> { + self + .inspect_primary_module(document) + .map(|m| m.specifier.clone()) + .or_else(|| self.infer_specifier(document)) + } + pub fn remove_expired_modules(&self) { self.modules_unscoped.remove_expired(); for modules in self.modules_by_scope.values() { @@ -1945,6 +2108,7 @@ console.log(b); 1, "javascript".parse().unwrap(), content.into(), + None, ); let document = document_modules .documents @@ -1975,6 +2139,7 @@ console.log(b); 1, "javascript".parse().unwrap(), content.into(), + None, ); document_modules .change_document( @@ -2043,6 +2208,7 @@ console.log(b, "hello deno"); 1, LanguageId::TypeScript, "import {} from 'test';".into(), + None, ); // set the initial import map and point to file 2 diff --git a/cli/lsp/language_server.rs b/cli/lsp/language_server.rs index 3ed1eb21bb..a05e2dd4dd 100644 --- a/cli/lsp/language_server.rs +++ b/cli/lsp/language_server.rs @@ -184,6 +184,13 @@ pub struct StateSnapshot { pub resolver: Arc, } +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +enum ProjectScopesChange { + None, + OpenNotebooks, + Config, +} + type LanguageServerTaskFn = Box; /// Used to queue tasks from inside of the language server lock that must be @@ -363,9 +370,7 @@ impl LanguageServer { Ok(()) } - if !self.init_flag.is_raised() { - self.init_flag.wait_raised().await; - } + self.init_flag.wait_raised().await; // prepare the cache inside the lock let mark = self @@ -412,9 +417,7 @@ impl LanguageServer { &self, _token: CancellationToken, ) -> LspResult> { - if !self.init_flag.is_raised() { - self.init_flag.wait_raised().await; - } + self.init_flag.wait_raised().await; Ok( self .inner @@ -430,9 +433,7 @@ impl LanguageServer { &self, _token: CancellationToken, ) -> LspResult> { - if !self.init_flag.is_raised() { - self.init_flag.wait_raised().await; - } + self.init_flag.wait_raised().await; Ok(Some(self.inner.read().await.get_performance())) } @@ -440,9 +441,7 @@ impl LanguageServer { &self, _token: CancellationToken, ) -> LspResult> { - if !self.init_flag.is_raised() { - self.init_flag.wait_raised().await; - } + self.init_flag.wait_raised().await; self.inner.read().await.task_definitions() } @@ -451,9 +450,7 @@ impl LanguageServer { params: Option, _token: CancellationToken, ) -> LspResult> { - if !self.init_flag.is_raised() { - self.init_flag.wait_raised().await; - } + self.init_flag.wait_raised().await; self.inner.read().await.test_run_request(params).await } @@ -462,9 +459,7 @@ impl LanguageServer { params: Option, _token: CancellationToken, ) -> LspResult> { - if !self.init_flag.is_raised() { - self.init_flag.wait_raised().await; - } + self.init_flag.wait_raised().await; self.inner.read().await.test_run_cancel_request(params) } @@ -473,9 +468,7 @@ impl LanguageServer { params: Option, _token: CancellationToken, ) -> LspResult> { - if !self.init_flag.is_raised() { - self.init_flag.wait_raised().await; - } + self.init_flag.wait_raised().await; match params.map(serde_json::from_value) { Some(Ok(params)) => Ok(Some( serde_json::to_value( @@ -676,6 +669,7 @@ impl Inner { self.snapshot(), &module.specifier, module.scope.as_ref(), + module.notebook_uri.as_ref(), token, ) .await @@ -1224,18 +1218,20 @@ impl Inner { // a @types/node package and now's a good time to do that anyway self.refresh_dep_info().await; - self.project_changed([], true); + self.project_changed([], ProjectScopesChange::Config); } #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))] async fn did_open(&mut self, params: DidOpenTextDocumentParams) { let mark = self.performance.mark_with_args("lsp.did_open", ¶ms); - let Some(scheme) = params.text_document.uri.scheme() else { - return; - }; // `deno:` documents are read-only and should only be handled as server // documents. - if scheme.eq_lowercase("deno") { + if params + .text_document + .uri + .scheme() + .is_some_and(|s| s.eq_lowercase("deno")) + { return; } let language_id = @@ -1255,21 +1251,23 @@ impl Inner { ); } let document = self.document_modules.open_document( - params.text_document.uri.clone(), + params.text_document.uri, params.text_document.version, - params.text_document.language_id.parse().unwrap(), + language_id, params.text_document.text.into(), + None, ); if document.is_diagnosable() { self.check_semantic_tokens_capabilities(); self.refresh_dep_info().await; self.project_changed( - [(¶ms.text_document.uri, ChangeKind::Opened)], - false, + self + .document_modules + .primary_specifier(&Document::Open(document.clone())) + .map(|s| (s, ChangeKind::Opened)), + ProjectScopesChange::None, ); - self - .diagnostics_server - .invalidate(&[¶ms.text_document.uri]); + self.diagnostics_server.invalidate(&[document.uri.as_ref()]); self.send_diagnostics_update(); self.send_testing_update(); } @@ -1279,12 +1277,14 @@ impl Inner { #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))] async fn did_change(&mut self, params: DidChangeTextDocumentParams) { let mark = self.performance.mark_with_args("lsp.did_change", ¶ms); - let Some(scheme) = params.text_document.uri.scheme() else { - return; - }; // `deno:` documents are read-only and should only be handled as server // documents. - if scheme.eq_lowercase("deno") { + if params + .text_document + .uri + .scheme() + .is_some_and(|s| s.eq_lowercase("deno")) + { return; } let document = match self.document_modules.change_document( @@ -1311,8 +1311,15 @@ impl Inner { config_changed = true; } self.project_changed( - [(document.uri.as_ref(), ChangeKind::Modified)], - config_changed, + self + .document_modules + .primary_specifier(&Document::Open(document.clone())) + .map(|s| (s, ChangeKind::Modified)), + if config_changed { + ProjectScopesChange::Config + } else { + ProjectScopesChange::None + }, ); self.diagnostics_server.invalidate(&[&document.uri]); self.send_diagnostics_update(); @@ -1388,12 +1395,14 @@ impl Inner { #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))] async fn did_close(&mut self, params: DidCloseTextDocumentParams) { let mark = self.performance.mark_with_args("lsp.did_close", ¶ms); - let Some(scheme) = params.text_document.uri.scheme() else { - return; - }; // `deno:` documents are read-only and should only be handled as server // documents. - if scheme.eq_lowercase("deno") { + if params + .text_document + .uri + .scheme() + .is_some_and(|s| s.eq_lowercase("deno")) + { return; } self.diagnostics_state.clear(¶ms.text_document.uri); @@ -1409,11 +1418,14 @@ impl Inner { }; if document.is_diagnosable() { self.refresh_dep_info().await; + let changed_specifier = self + .document_modules + .primary_specifier(&Document::Open(document.clone())) + .map(|s| (s, ChangeKind::Closed)); + // Invalidate the weak references of `document` before calling + // `self.project_changed()` so its module entries will be dropped. drop(document); - self.project_changed( - [(¶ms.text_document.uri, ChangeKind::Closed)], - false, - ); + self.project_changed(changed_specifier, ProjectScopesChange::None); self .diagnostics_server .invalidate(&[¶ms.text_document.uri]); @@ -1423,6 +1435,157 @@ impl Inner { self.performance.measure(mark); } + async fn notebook_did_open(&mut self, params: DidOpenNotebookDocumentParams) { + let _mark = self.performance.measure_scope("lsp.notebook_did_open"); + let documents = self.document_modules.open_notebook_document( + params.notebook_document.uri, + params.cell_text_documents, + ); + let diagnosable_documents = documents + .iter() + .filter(|d| d.is_diagnosable()) + .collect::>(); + if !diagnosable_documents.is_empty() { + self.check_semantic_tokens_capabilities(); + self.refresh_dep_info().await; + self.project_changed( + diagnosable_documents + .iter() + .flat_map(|d| { + let specifier = self + .document_modules + .primary_specifier(&Document::Open((*d).clone()))?; + Some((specifier, ChangeKind::Closed)) + }) + .collect::>(), + ProjectScopesChange::OpenNotebooks, + ); + self.diagnostics_server.invalidate( + &diagnosable_documents + .iter() + .map(|d| d.uri.as_ref()) + .collect::>(), + ); + self.send_diagnostics_update(); + } + } + + async fn notebook_did_change( + &mut self, + params: DidChangeNotebookDocumentParams, + ) { + let _mark = self.performance.measure_scope("lsp.notebook_did_change"); + let Some(cells) = params.change.cells else { + return; + }; + let documents = self.document_modules.change_notebook_document( + ¶ms.notebook_document.uri, + cells.structure, + cells.text_content, + ); + let diagnosable_documents = documents + .iter() + .filter(|(d, _)| d.is_diagnosable()) + .collect::>(); + if !diagnosable_documents.is_empty() { + let old_scopes_with_node_specifier = + self.document_modules.scopes_with_node_specifier(); + self.refresh_dep_info().await; + let mut config_changed = false; + if !self + .document_modules + .scopes_with_node_specifier() + .equivalent(&old_scopes_with_node_specifier) + { + config_changed = true; + } + self.project_changed( + diagnosable_documents + .iter() + .flat_map(|(d, k)| { + let specifier = self + .document_modules + .primary_specifier(&Document::Open(d.clone()))?; + Some((specifier, *k)) + }) + .collect::>(), + if config_changed { + ProjectScopesChange::Config + } else { + ProjectScopesChange::None + }, + ); + self.diagnostics_server.invalidate( + &diagnosable_documents + .iter() + .map(|(d, _)| d.uri.as_ref()) + .collect::>(), + ); + self.send_diagnostics_update(); + } + } + + fn notebook_did_save(&mut self, params: DidSaveNotebookDocumentParams) { + let _mark = self.performance.measure_scope("lsp.notebook_did_save"); + let Some(cell_uris) = self + .document_modules + .documents + .cells_by_notebook_uri() + .get(¶ms.notebook_document.uri) + .cloned() + else { + lsp_warn!( + "The URI \"{}\" does not refer to an open notebook document.", + params.notebook_document.uri.as_str() + ); + return; + }; + for cell_uri in cell_uris { + self.did_save(DidSaveTextDocumentParams { + text_document: TextDocumentIdentifier { + uri: cell_uri.as_ref().clone(), + }, + text: None, + }); + } + } + + async fn notebook_did_close( + &mut self, + params: DidCloseNotebookDocumentParams, + ) { + let _mark = self.performance.measure_scope("lsp.notebook_did_close"); + let documents = self + .document_modules + .close_notebook_document(¶ms.notebook_document.uri); + let diagnosable_documents = documents + .iter() + .filter(|d| d.is_diagnosable()) + .collect::>(); + if !diagnosable_documents.is_empty() { + self.refresh_dep_info().await; + self.project_changed( + diagnosable_documents + .iter() + .flat_map(|d| { + let specifier = self + .document_modules + .primary_specifier(&Document::Open((*d).clone()))?; + Some((specifier, ChangeKind::Closed)) + }) + .collect::>(), + ProjectScopesChange::OpenNotebooks, + ); + self.diagnostics_server.invalidate( + &diagnosable_documents + .iter() + .map(|d| d.uri.as_ref()) + .collect::>(), + ); + self.send_diagnostics_update(); + } + } + #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))] async fn did_change_configuration( &mut self, @@ -1504,8 +1667,19 @@ impl Inner { self.refresh_resolver().await; self.refresh_documents_config().await; self.project_changed( - changes.iter().map(|(_, e)| (&e.uri, ChangeKind::Modified)), - false, + changes + .iter() + .filter_map(|(_, e)| { + let document = self.document_modules.documents.inspect(&e.uri)?; + if !document.is_diagnosable() { + return None; + } + let specifier = + self.document_modules.primary_specifier(&document)?; + Some((specifier, ChangeKind::Modified)) + }) + .collect::>(), + ProjectScopesChange::None, ); self.ts_server.cleanup_semantic_cache(self.snapshot()).await; self.diagnostics_server.invalidate_all(); @@ -1780,6 +1954,7 @@ impl Inner { &module.specifier, position, module.scope.as_ref(), + module.notebook_uri.as_ref(), token, ) .await @@ -1926,6 +2101,7 @@ impl Inner { &module.specifier, ), module.scope.as_ref(), + module.notebook_uri.as_ref(), token, ) .await @@ -2038,6 +2214,7 @@ impl Inner { params.context.trigger_kind, only, module.scope.as_ref(), + module.notebook_uri.as_ref(), token, ) .await @@ -2138,6 +2315,7 @@ impl Inner { &module.specifier, ), module.scope.as_ref(), + module.notebook_uri.as_ref(), token, ) .await @@ -2221,6 +2399,7 @@ impl Inner { &module.specifier, )), module.scope.as_ref(), + module.notebook_uri.as_ref(), token, ) .await @@ -2433,6 +2612,7 @@ impl Inner { .offset_tsc(params.text_document_position_params.position)?, vec![module.specifier.as_ref().clone()], module.scope.as_ref(), + module.notebook_uri.as_ref(), token, ) .await @@ -2504,6 +2684,8 @@ impl Inner { .line_index .offset_tsc(params.text_document_position.position)?, scope.as_ref(), + // TODO(nayeemrmn): Support notebook scopes here. + None, token, ) .await @@ -2570,6 +2752,7 @@ impl Inner { .line_index .offset_tsc(params.text_document_position_params.position)?, module.scope.as_ref(), + module.notebook_uri.as_ref(), token, ) .await @@ -2632,6 +2815,7 @@ impl Inner { .line_index .offset_tsc(params.text_document_position_params.position)?, module.scope.as_ref(), + module.notebook_uri.as_ref(), token, ) .await @@ -2755,6 +2939,7 @@ impl Inner { .options) .into(), module.scope.as_ref(), + module.notebook_uri.as_ref(), token, ) .await @@ -2848,6 +3033,7 @@ impl Inner { )), data.data.clone(), module.scope.as_ref(), + module.notebook_uri.as_ref(), token, ) .await; @@ -2931,6 +3117,7 @@ impl Inner { .line_index .offset_tsc(params.text_document_position_params.position)?, scope.as_ref(), + module.notebook_uri.as_ref(), token, ) .await @@ -2997,6 +3184,7 @@ impl Inner { self.snapshot(), &module.specifier, module.scope.as_ref(), + module.notebook_uri.as_ref(), token, ) .await @@ -3066,6 +3254,7 @@ impl Inner { .line_index .offset_tsc(params.item.selection_range.start)?, scope.as_ref(), + module.notebook_uri.as_ref(), token, ) .await @@ -3133,6 +3322,7 @@ impl Inner { .line_index .offset_tsc(params.item.selection_range.start)?, module.scope.as_ref(), + module.notebook_uri.as_ref(), token, ) .await @@ -3195,6 +3385,7 @@ impl Inner { .line_index .offset_tsc(params.text_document_position_params.position)?, module.scope.as_ref(), + module.notebook_uri.as_ref(), token, ) .await @@ -3283,6 +3474,8 @@ impl Inner { &module.specifier, ), scope.as_ref(), + // TODO(nayeemrmn): Support notebook scopes here. + None, token, ) .await @@ -3359,6 +3552,7 @@ impl Inner { &module.specifier, module.line_index.offset_tsc(position)?, module.scope.as_ref(), + module.notebook_uri.as_ref(), token, ) .await @@ -3412,6 +3606,7 @@ impl Inner { &module.specifier, 0..module.line_index.text_content_length_utf16().into(), module.scope.as_ref(), + module.notebook_uri.as_ref(), token, ) .await @@ -3480,6 +3675,7 @@ impl Inner { module.line_index.offset_tsc(params.range.start)? ..module.line_index.offset_tsc(params.range.end)?, module.scope.as_ref(), + module.notebook_uri.as_ref(), token, ) .await @@ -3548,6 +3744,7 @@ impl Inner { .offset_tsc(params.text_document_position_params.position)?, options, module.scope.as_ref(), + module.notebook_uri.as_ref(), token, ) .await @@ -3637,6 +3834,8 @@ impl Inner { ..Default::default() }, scope.as_ref(), + // TODO(nayeemrmn): Support notebook scopes here. + None, token, ) .await @@ -3684,6 +3883,8 @@ impl Inner { Some(256), None, scope.as_ref(), + // TODO(nayeemrmn): Support notebook scopes here. + None, token, ) .await @@ -3718,28 +3919,15 @@ impl Inner { #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))] fn project_changed<'a>( &mut self, - changed_docs: impl IntoIterator, - config_changed: bool, + changed_specifiers: impl IntoIterator, ChangeKind)>, + scopes_change: ProjectScopesChange, ) { self.project_version += 1; // increment before getting the snapshot - let modified_scripts = changed_docs - .into_iter() - .filter_map(|(u, k)| { - Some((self.document_modules.documents.inspect(u)?, k)) - }) - .flat_map(|(d, k)| { - self - .document_modules - .inspect_modules_by_scope(&d) - .values() - .map(|m| (m.specifier.clone(), k)) - .collect::>() - }) - .collect::>(); + let changed_specifiers = changed_specifiers.into_iter().collect::>(); self.ts_server.project_changed( self.snapshot(), - modified_scripts.iter().map(|(s, k)| (s.as_ref(), *k)), - config_changed.then(|| { + changed_specifiers.iter().map(|(u, k)| (u.as_ref(), *k)), + matches!(scopes_change, ProjectScopesChange::Config).then(|| { self .config .tree @@ -3748,6 +3936,24 @@ impl Inner { .map(|(s, d)| (s.clone(), d.ts_config.clone())) .collect() }), + matches!( + scopes_change, + ProjectScopesChange::OpenNotebooks | ProjectScopesChange::Config + ) + .then(|| { + self + .document_modules + .documents + .cells_by_notebook_uri() + .keys() + .map(|u| { + ( + u.clone(), + self.document_modules.primary_scope(u).flatten().cloned(), + ) + }) + .collect() + }), ); self.document_modules.remove_expired_modules(); } @@ -3780,9 +3986,7 @@ impl tower_lsp::LanguageServer for LanguageServer { params: ExecuteCommandParams, _token: CancellationToken, ) -> LspResult> { - if !self.init_flag.is_raised() { - self.init_flag.wait_raised().await; - } + self.init_flag.wait_raised().await; if params.command == "deno.cache" { #[derive(Default, Deserialize)] #[serde(rename_all = "camelCase")] @@ -3864,40 +4068,50 @@ impl tower_lsp::LanguageServer for LanguageServer { } async fn did_open(&self, params: DidOpenTextDocumentParams) { - if !self.init_flag.is_raised() { - self.init_flag.wait_raised().await; - } + self.init_flag.wait_raised().await; self.inner.write().await.did_open(params).await; } async fn did_change(&self, params: DidChangeTextDocumentParams) { - if !self.init_flag.is_raised() { - self.init_flag.wait_raised().await; - } + self.init_flag.wait_raised().await; self.inner.write().await.did_change(params).await; } async fn did_save(&self, params: DidSaveTextDocumentParams) { - if !self.init_flag.is_raised() { - self.init_flag.wait_raised().await; - } + self.init_flag.wait_raised().await; self.inner.write().await.did_save(params); } async fn did_close(&self, params: DidCloseTextDocumentParams) { - if !self.init_flag.is_raised() { - self.init_flag.wait_raised().await; - } + self.init_flag.wait_raised().await; self.inner.write().await.did_close(params).await; } + async fn notebook_did_open(&self, params: DidOpenNotebookDocumentParams) { + self.init_flag.wait_raised().await; + self.inner.write().await.notebook_did_open(params).await + } + + async fn notebook_did_change(&self, params: DidChangeNotebookDocumentParams) { + self.init_flag.wait_raised().await; + self.inner.write().await.notebook_did_change(params).await + } + + async fn notebook_did_save(&self, params: DidSaveNotebookDocumentParams) { + self.init_flag.wait_raised().await; + self.inner.write().await.notebook_did_save(params) + } + + async fn notebook_did_close(&self, params: DidCloseNotebookDocumentParams) { + self.init_flag.wait_raised().await; + self.inner.write().await.notebook_did_close(params).await + } + async fn did_change_configuration( &self, params: DidChangeConfigurationParams, ) { - if !self.init_flag.is_raised() { - self.init_flag.wait_raised().await; - } + self.init_flag.wait_raised().await; let mark = self .performance .mark_with_args("lsp.did_change_configuration", ¶ms); @@ -3915,9 +4129,7 @@ impl tower_lsp::LanguageServer for LanguageServer { &self, params: DidChangeWatchedFilesParams, ) { - if !self.init_flag.is_raised() { - self.init_flag.wait_raised().await; - } + self.init_flag.wait_raised().await; self .inner .write() @@ -3930,9 +4142,7 @@ impl tower_lsp::LanguageServer for LanguageServer { &self, params: DidChangeWorkspaceFoldersParams, ) { - if !self.init_flag.is_raised() { - self.init_flag.wait_raised().await; - } + self.init_flag.wait_raised().await; let mark = self .performance .mark_with_args("lsp.did_change_workspace_folders", ¶ms); @@ -3956,9 +4166,7 @@ impl tower_lsp::LanguageServer for LanguageServer { params: DocumentSymbolParams, token: CancellationToken, ) -> LspResult> { - if !self.init_flag.is_raised() { - self.init_flag.wait_raised().await; - } + self.init_flag.wait_raised().await; self .inner .read() @@ -3972,9 +4180,7 @@ impl tower_lsp::LanguageServer for LanguageServer { params: DocumentFormattingParams, token: CancellationToken, ) -> LspResult>> { - if !self.init_flag.is_raised() { - self.init_flag.wait_raised().await; - } + self.init_flag.wait_raised().await; self.inner.read().await.formatting(params, &token).await } @@ -3983,9 +4189,7 @@ impl tower_lsp::LanguageServer for LanguageServer { params: HoverParams, token: CancellationToken, ) -> LspResult> { - if !self.init_flag.is_raised() { - self.init_flag.wait_raised().await; - } + self.init_flag.wait_raised().await; self.inner.read().await.hover(params, &token).await } @@ -3994,9 +4198,7 @@ impl tower_lsp::LanguageServer for LanguageServer { params: InlayHintParams, token: CancellationToken, ) -> LspResult>> { - if !self.init_flag.is_raised() { - self.init_flag.wait_raised().await; - } + self.init_flag.wait_raised().await; self.inner.read().await.inlay_hint(params, &token).await } @@ -4005,9 +4207,7 @@ impl tower_lsp::LanguageServer for LanguageServer { params: CodeActionParams, token: CancellationToken, ) -> LspResult> { - if !self.init_flag.is_raised() { - self.init_flag.wait_raised().await; - } + self.init_flag.wait_raised().await; self.inner.read().await.code_action(params, &token).await } @@ -4016,9 +4216,7 @@ impl tower_lsp::LanguageServer for LanguageServer { params: CodeAction, token: CancellationToken, ) -> LspResult { - if !self.init_flag.is_raised() { - self.init_flag.wait_raised().await; - } + self.init_flag.wait_raised().await; self .inner .read() @@ -4032,9 +4230,7 @@ impl tower_lsp::LanguageServer for LanguageServer { params: CodeLensParams, token: CancellationToken, ) -> LspResult>> { - if !self.init_flag.is_raised() { - self.init_flag.wait_raised().await; - } + self.init_flag.wait_raised().await; self.inner.read().await.code_lens(params, &token).await } @@ -4043,9 +4239,7 @@ impl tower_lsp::LanguageServer for LanguageServer { params: CodeLens, token: CancellationToken, ) -> LspResult { - if !self.init_flag.is_raised() { - self.init_flag.wait_raised().await; - } + self.init_flag.wait_raised().await; self .inner .read() @@ -4059,9 +4253,7 @@ impl tower_lsp::LanguageServer for LanguageServer { params: DocumentHighlightParams, token: CancellationToken, ) -> LspResult>> { - if !self.init_flag.is_raised() { - self.init_flag.wait_raised().await; - } + self.init_flag.wait_raised().await; self .inner .read() @@ -4075,9 +4267,7 @@ impl tower_lsp::LanguageServer for LanguageServer { params: ReferenceParams, token: CancellationToken, ) -> LspResult>> { - if !self.init_flag.is_raised() { - self.init_flag.wait_raised().await; - } + self.init_flag.wait_raised().await; self.inner.read().await.references(params, &token).await } @@ -4086,9 +4276,7 @@ impl tower_lsp::LanguageServer for LanguageServer { params: GotoDefinitionParams, token: CancellationToken, ) -> LspResult> { - if !self.init_flag.is_raised() { - self.init_flag.wait_raised().await; - } + self.init_flag.wait_raised().await; self .inner .read() @@ -4102,9 +4290,7 @@ impl tower_lsp::LanguageServer for LanguageServer { params: GotoTypeDefinitionParams, token: CancellationToken, ) -> LspResult> { - if !self.init_flag.is_raised() { - self.init_flag.wait_raised().await; - } + self.init_flag.wait_raised().await; self .inner .read() @@ -4118,9 +4304,7 @@ impl tower_lsp::LanguageServer for LanguageServer { params: CompletionParams, token: CancellationToken, ) -> LspResult> { - if !self.init_flag.is_raised() { - self.init_flag.wait_raised().await; - } + self.init_flag.wait_raised().await; self.inner.read().await.completion(params, &token).await } @@ -4129,9 +4313,7 @@ impl tower_lsp::LanguageServer for LanguageServer { params: CompletionItem, token: CancellationToken, ) -> LspResult { - if !self.init_flag.is_raised() { - self.init_flag.wait_raised().await; - } + self.init_flag.wait_raised().await; self .inner .read() @@ -4145,9 +4327,7 @@ impl tower_lsp::LanguageServer for LanguageServer { params: GotoImplementationParams, token: CancellationToken, ) -> LspResult> { - if !self.init_flag.is_raised() { - self.init_flag.wait_raised().await; - } + self.init_flag.wait_raised().await; self .inner .read() @@ -4161,9 +4341,7 @@ impl tower_lsp::LanguageServer for LanguageServer { params: FoldingRangeParams, token: CancellationToken, ) -> LspResult>> { - if !self.init_flag.is_raised() { - self.init_flag.wait_raised().await; - } + self.init_flag.wait_raised().await; self.inner.read().await.folding_range(params, &token).await } @@ -4172,9 +4350,7 @@ impl tower_lsp::LanguageServer for LanguageServer { params: CallHierarchyIncomingCallsParams, token: CancellationToken, ) -> LspResult>> { - if !self.init_flag.is_raised() { - self.init_flag.wait_raised().await; - } + self.init_flag.wait_raised().await; self.inner.read().await.incoming_calls(params, &token).await } @@ -4183,9 +4359,7 @@ impl tower_lsp::LanguageServer for LanguageServer { params: CallHierarchyOutgoingCallsParams, token: CancellationToken, ) -> LspResult>> { - if !self.init_flag.is_raised() { - self.init_flag.wait_raised().await; - } + self.init_flag.wait_raised().await; self.inner.read().await.outgoing_calls(params, &token).await } @@ -4194,9 +4368,7 @@ impl tower_lsp::LanguageServer for LanguageServer { params: CallHierarchyPrepareParams, token: CancellationToken, ) -> LspResult>> { - if !self.init_flag.is_raised() { - self.init_flag.wait_raised().await; - } + self.init_flag.wait_raised().await; self .inner .read() @@ -4210,9 +4382,7 @@ impl tower_lsp::LanguageServer for LanguageServer { params: RenameParams, token: CancellationToken, ) -> LspResult> { - if !self.init_flag.is_raised() { - self.init_flag.wait_raised().await; - } + self.init_flag.wait_raised().await; self.inner.read().await.rename(params, &token).await } @@ -4221,9 +4391,7 @@ impl tower_lsp::LanguageServer for LanguageServer { params: SelectionRangeParams, token: CancellationToken, ) -> LspResult>> { - if !self.init_flag.is_raised() { - self.init_flag.wait_raised().await; - } + self.init_flag.wait_raised().await; self .inner .read() @@ -4237,9 +4405,7 @@ impl tower_lsp::LanguageServer for LanguageServer { params: SemanticTokensParams, token: CancellationToken, ) -> LspResult> { - if !self.init_flag.is_raised() { - self.init_flag.wait_raised().await; - } + self.init_flag.wait_raised().await; self .inner .read() @@ -4253,9 +4419,7 @@ impl tower_lsp::LanguageServer for LanguageServer { params: SemanticTokensRangeParams, token: CancellationToken, ) -> LspResult> { - if !self.init_flag.is_raised() { - self.init_flag.wait_raised().await; - } + self.init_flag.wait_raised().await; self .inner .read() @@ -4269,9 +4433,7 @@ impl tower_lsp::LanguageServer for LanguageServer { params: SignatureHelpParams, token: CancellationToken, ) -> LspResult> { - if !self.init_flag.is_raised() { - self.init_flag.wait_raised().await; - } + self.init_flag.wait_raised().await; self.inner.read().await.signature_help(params, &token).await } @@ -4280,9 +4442,7 @@ impl tower_lsp::LanguageServer for LanguageServer { params: RenameFilesParams, token: CancellationToken, ) -> LspResult> { - if !self.init_flag.is_raised() { - self.init_flag.wait_raised().await; - } + self.init_flag.wait_raised().await; self .inner .read() @@ -4296,9 +4456,7 @@ impl tower_lsp::LanguageServer for LanguageServer { params: WorkspaceSymbolParams, token: CancellationToken, ) -> LspResult>> { - if !self.init_flag.is_raised() { - self.init_flag.wait_raised().await; - } + self.init_flag.wait_raised().await; self.inner.read().await.symbol(params, &token).await } } @@ -4500,7 +4658,7 @@ impl Inner { self.resolver.did_cache(); self.refresh_dep_info().await; self.diagnostics_server.invalidate_all(); - self.project_changed([], true); + self.project_changed([], ProjectScopesChange::Config); self.ts_server.cleanup_semantic_cache(self.snapshot()).await; self.send_diagnostics_update(); self.send_testing_update(); @@ -4659,6 +4817,7 @@ impl Inner { &module.specifier, ), module.scope.as_ref(), + module.notebook_uri.as_ref(), token, ) .await diff --git a/cli/lsp/tsc.rs b/cli/lsp/tsc.rs index 74f513006e..23ac43b43e 100644 --- a/cli/lsp/tsc.rs +++ b/cli/lsp/tsc.rs @@ -118,6 +118,7 @@ const FILE_EXTENSION_KIND_MODIFIERS: &[&str] = type Request = ( TscRequest, Option>, + Option>, Arc, oneshot::Sender>, CancellationToken, @@ -294,6 +295,7 @@ pub struct PendingChange { pub modified_scripts: Vec<(String, ChangeKind)>, pub project_version: usize, pub new_configs_by_scope: Option, Arc>>, + pub new_notebook_scopes: Option, Option>>>, } impl<'a> ToV8<'a> for PendingChange { @@ -329,11 +331,29 @@ impl<'a> ToV8<'a> for PendingChange { } else { v8::null(scope).into() }; + let new_notebook_scopes = + if let Some(new_notebook_scopes) = self.new_notebook_scopes { + serde_v8::to_v8( + scope, + new_notebook_scopes.into_iter().collect::>(), + ) + .unwrap_or_else(|err| { + lsp_warn!("Couldn't serialize ts configs: {err}"); + v8::null(scope).into() + }) + } else { + v8::null(scope).into() + }; Ok( v8::Array::new_with_elements( scope, - &[modified_scripts, project_version, new_configs_by_scope], + &[ + modified_scripts, + project_version, + new_configs_by_scope, + new_notebook_scopes, + ], ) .into(), ) @@ -346,12 +366,16 @@ impl PendingChange { new_version: usize, modified_scripts: Vec<(String, ChangeKind)>, new_configs_by_scope: Option, Arc>>, + new_notebook_scopes: Option, Option>>>, ) { use ChangeKind::*; self.project_version = self.project_version.max(new_version); if let Some(new_configs_by_scope) = new_configs_by_scope { self.new_configs_by_scope = Some(new_configs_by_scope); } + if let Some(new_notebook_scopes) = new_notebook_scopes { + self.new_notebook_scopes = Some(new_notebook_scopes); + } for (spec, new) in modified_scripts { if let Some((_, current)) = self.modified_scripts.iter_mut().find(|(s, _)| s == &spec) @@ -468,6 +492,7 @@ impl TsServer { snapshot: Arc, modified_scripts: impl IntoIterator, new_configs_by_scope: Option, Arc>>, + new_notebook_scopes: Option, Option>>>, ) { let modified_scripts = modified_scripts .into_iter() @@ -479,6 +504,7 @@ impl TsServer { snapshot.project_version, modified_scripts, new_configs_by_scope, + new_notebook_scopes, ); } pending => { @@ -486,6 +512,7 @@ impl TsServer { modified_scripts, project_version: snapshot.project_version, new_configs_by_scope, + new_notebook_scopes, }; *pending = Some(pending_change); } @@ -498,6 +525,7 @@ impl TsServer { snapshot: Arc, specifiers: impl IntoIterator, scope: Option<&Arc>, + notebook_uri: Option<&Arc>, token: &CancellationToken, ) -> Result<(Vec>, MaybeAmbientModules), AnyError> { @@ -509,7 +537,11 @@ impl TsServer { TscRequest::GetDiagnostics((specifiers, snapshot.project_version)); self .request::<(Vec>, MaybeAmbientModules)>( - snapshot, req, scope, token, + snapshot, + req, + scope, + notebook_uri, + token, ) .await .and_then(|(mut diagnostics, ambient_modules)| { @@ -528,24 +560,15 @@ impl TsServer { if !self.is_started() { return; } - for scope in snapshot - .config - .tree - .data_by_scope() - .keys() - .map(Some) - .chain(std::iter::once(None)) - { - let req = TscRequest::CleanupSemanticCache; - self - .request::<()>(snapshot.clone(), req, scope, &Default::default()) - .await - .map_err(|err| { - log::error!("Failed to request to tsserver {}", err); - LspError::invalid_request() - }) - .ok(); - } + let req = TscRequest::CleanupSemanticCache; + self + .request::<()>(snapshot.clone(), req, None, None, &Default::default()) + .await + .map_err(|err| { + log::error!("Failed to request to tsserver {}", err); + LspError::invalid_request() + }) + .ok(); } #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))] @@ -555,6 +578,7 @@ impl TsServer { specifier: &Url, position: u32, scope: Option<&Arc>, + notebook_uri: Option<&Arc>, token: &CancellationToken, ) -> Result>, AnyError> { let req = TscRequest::FindReferences(( @@ -562,7 +586,13 @@ impl TsServer { position, )); self - .request::>>(snapshot, req, scope, token) + .request::>>( + snapshot, + req, + scope, + notebook_uri, + token, + ) .await .and_then(|mut symbols| { for symbol in symbols.iter_mut().flatten() { @@ -581,12 +611,15 @@ impl TsServer { snapshot: Arc, specifier: &Url, scope: Option<&Arc>, + notebook_uri: Option<&Arc>, token: &CancellationToken, ) -> Result { let req = TscRequest::GetNavigationTree((self .specifier_map .denormalize(specifier),)); - self.request(snapshot, req, scope, token).await + self + .request(snapshot, req, scope, notebook_uri, token) + .await } #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))] @@ -596,7 +629,7 @@ impl TsServer { ) -> Result, LspError> { let req = TscRequest::GetSupportedCodeFixes; self - .request(snapshot, req, None, &Default::default()) + .request(snapshot, req, None, None, &Default::default()) .await .map_err(|err| { log::error!("Unable to get fixable diagnostics: {}", err); @@ -611,13 +644,16 @@ impl TsServer { specifier: &Url, position: u32, scope: Option<&Arc>, + notebook_uri: Option<&Arc>, token: &CancellationToken, ) -> Result, AnyError> { let req = TscRequest::GetQuickInfoAtPosition(( self.specifier_map.denormalize(specifier), position, )); - self.request(snapshot, req, scope, token).await + self + .request(snapshot, req, scope, notebook_uri, token) + .await } #[allow(clippy::too_many_arguments)] @@ -631,6 +667,7 @@ impl TsServer { format_code_settings: FormatCodeSettings, preferences: UserPreferences, scope: Option<&Arc>, + notebook_uri: Option<&Arc>, token: &CancellationToken, ) -> Result, AnyError> { let req = TscRequest::GetCodeFixesAtPosition(Box::new(( @@ -642,7 +679,7 @@ impl TsServer { preferences, ))); self - .request::>(snapshot, req, scope, token) + .request::>(snapshot, req, scope, notebook_uri, token) .await .and_then(|mut actions| { for action in &mut actions { @@ -663,6 +700,7 @@ impl TsServer { trigger_kind: Option, only: String, scope: Option<&Arc>, + notebook_uri: Option<&Arc>, token: &CancellationToken, ) -> Result, LspError> { let trigger_kind = trigger_kind.map(|reason| match reason { @@ -678,7 +716,7 @@ impl TsServer { only, ))); self - .request(snapshot, req, scope, token) + .request(snapshot, req, scope, notebook_uri, token) .await .map_err(|err| { log::error!("Failed to request to tsserver {}", err); @@ -696,6 +734,7 @@ impl TsServer { format_code_settings: FormatCodeSettings, preferences: UserPreferences, scope: Option<&Arc>, + notebook_uri: Option<&Arc>, token: &CancellationToken, ) -> Result { let req = TscRequest::GetCombinedCodeFix(Box::new(( @@ -708,7 +747,7 @@ impl TsServer { preferences, ))); self - .request::(snapshot, req, scope, token) + .request::(snapshot, req, scope, notebook_uri, token) .await .and_then(|mut actions| { actions.normalize(&self.specifier_map)?; @@ -728,6 +767,7 @@ impl TsServer { action_name: String, preferences: Option, scope: Option<&Arc>, + notebook_uri: Option<&Arc>, token: &CancellationToken, ) -> Result { let req = TscRequest::GetEditsForRefactor(Box::new(( @@ -739,7 +779,7 @@ impl TsServer { preferences, ))); self - .request::(snapshot, req, scope, token) + .request::(snapshot, req, scope, notebook_uri, token) .await .and_then(|mut info| { info.normalize(&self.specifier_map)?; @@ -757,6 +797,7 @@ impl TsServer { format_code_settings: FormatCodeSettings, user_preferences: UserPreferences, scope: Option<&Arc>, + notebook_uri: Option<&Arc>, token: &CancellationToken, ) -> Result, AnyError> { let req = TscRequest::GetEditsForFileRename(Box::new(( @@ -766,7 +807,13 @@ impl TsServer { user_preferences, ))); self - .request::>(snapshot, req, scope, token) + .request::>( + snapshot, + req, + scope, + notebook_uri, + token, + ) .await .and_then(|mut changes| { for changes in &mut changes { @@ -783,6 +830,7 @@ impl TsServer { }) } + #[allow(clippy::too_many_arguments)] #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))] pub async fn get_document_highlights( &self, @@ -791,6 +839,7 @@ impl TsServer { position: u32, files_to_search: Vec, scope: Option<&Arc>, + notebook_uri: Option<&Arc>, token: &CancellationToken, ) -> Result>, AnyError> { let req = TscRequest::GetDocumentHighlights(Box::new(( @@ -801,7 +850,9 @@ impl TsServer { .map(|s| self.specifier_map.denormalize(&s)) .collect::>(), ))); - self.request(snapshot, req, scope, token).await + self + .request(snapshot, req, scope, notebook_uri, token) + .await } #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))] @@ -811,6 +862,7 @@ impl TsServer { specifier: &Url, position: u32, scope: Option<&Arc>, + notebook_uri: Option<&Arc>, token: &CancellationToken, ) -> Result, AnyError> { let req = TscRequest::GetDefinitionAndBoundSpan(( @@ -819,7 +871,11 @@ impl TsServer { )); self .request::>( - snapshot, req, scope, token, + snapshot, + req, + scope, + notebook_uri, + token, ) .await .and_then(|mut info| { @@ -837,6 +893,7 @@ impl TsServer { specifier: &Url, position: u32, scope: Option<&Arc>, + notebook_uri: Option<&Arc>, token: &CancellationToken, ) -> Result>, AnyError> { let req = TscRequest::GetTypeDefinitionAtPosition(( @@ -844,7 +901,13 @@ impl TsServer { position, )); self - .request::>>(snapshot, req, scope, token) + .request::>>( + snapshot, + req, + scope, + notebook_uri, + token, + ) .await .and_then(|mut infos| { for info in infos.iter_mut().flatten() { @@ -867,6 +930,7 @@ impl TsServer { options: GetCompletionsAtPositionOptions, format_code_settings: FormatCodeSettings, scope: Option<&Arc>, + notebook_uri: Option<&Arc>, token: &CancellationToken, ) -> Result, AnyError> { let req = TscRequest::GetCompletionsAtPosition(Box::new(( @@ -876,7 +940,13 @@ impl TsServer { format_code_settings, ))); self - .request::>(snapshot, req, scope, token) + .request::>( + snapshot, + req, + scope, + notebook_uri, + token, + ) .await .and_then(|mut info| { if let Some(info) = &mut info { @@ -899,6 +969,7 @@ impl TsServer { preferences: Option, data: Option, scope: Option<&Arc>, + notebook_uri: Option<&Arc>, token: &CancellationToken, ) -> Result, AnyError> { let req = TscRequest::GetCompletionEntryDetails(Box::new(( @@ -911,7 +982,13 @@ impl TsServer { data, ))); self - .request::>(snapshot, req, scope, token) + .request::>( + snapshot, + req, + scope, + notebook_uri, + token, + ) .await .and_then(|mut details| { if let Some(details) = &mut details { @@ -928,6 +1005,7 @@ impl TsServer { specifier: &Url, position: u32, scope: Option<&Arc>, + notebook_uri: Option<&Arc>, token: &CancellationToken, ) -> Result>, AnyError> { let req = TscRequest::GetImplementationAtPosition(( @@ -936,7 +1014,11 @@ impl TsServer { )); self .request::>>( - snapshot, req, scope, token, + snapshot, + req, + scope, + notebook_uri, + token, ) .await .and_then(|mut locations| { @@ -956,12 +1038,15 @@ impl TsServer { snapshot: Arc, specifier: &Url, scope: Option<&Arc>, + notebook_uri: Option<&Arc>, token: &CancellationToken, ) -> Result, AnyError> { let req = TscRequest::GetOutliningSpans((self .specifier_map .denormalize(specifier),)); - self.request(snapshot, req, scope, token).await + self + .request(snapshot, req, scope, notebook_uri, token) + .await } #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))] @@ -971,6 +1056,7 @@ impl TsServer { specifier: &Url, position: u32, scope: Option<&Arc>, + notebook_uri: Option<&Arc>, token: &CancellationToken, ) -> Result, AnyError> { let req = TscRequest::ProvideCallHierarchyIncomingCalls(( @@ -978,7 +1064,13 @@ impl TsServer { position, )); self - .request::>(snapshot, req, scope, token) + .request::>( + snapshot, + req, + scope, + notebook_uri, + token, + ) .await .and_then(|mut calls| { for call in &mut calls { @@ -995,6 +1087,7 @@ impl TsServer { specifier: &Url, position: u32, scope: Option<&Arc>, + notebook_uri: Option<&Arc>, token: &CancellationToken, ) -> Result, AnyError> { let req = TscRequest::ProvideCallHierarchyOutgoingCalls(( @@ -1002,7 +1095,13 @@ impl TsServer { position, )); self - .request::>(snapshot, req, scope, token) + .request::>( + snapshot, + req, + scope, + notebook_uri, + token, + ) .await .and_then(|mut calls| { for call in &mut calls { @@ -1022,6 +1121,7 @@ impl TsServer { specifier: &Url, position: u32, scope: Option<&Arc>, + notebook_uri: Option<&Arc>, token: &CancellationToken, ) -> Result>, AnyError> { let req = TscRequest::PrepareCallHierarchy(( @@ -1030,7 +1130,11 @@ impl TsServer { )); self .request::>>( - snapshot, req, scope, token, + snapshot, + req, + scope, + notebook_uri, + token, ) .await .and_then(|mut items| { @@ -1049,6 +1153,7 @@ impl TsServer { }) } + #[allow(clippy::too_many_arguments)] #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))] pub async fn find_rename_locations( &self, @@ -1057,6 +1162,7 @@ impl TsServer { position: u32, user_preferences: UserPreferences, scope: Option<&Arc>, + notebook_uri: Option<&Arc>, token: &CancellationToken, ) -> Result>, AnyError> { let req = TscRequest::FindRenameLocations(( @@ -1067,7 +1173,13 @@ impl TsServer { user_preferences, )); self - .request::>>(snapshot, req, scope, token) + .request::>>( + snapshot, + req, + scope, + notebook_uri, + token, + ) .await .and_then(|mut locations| { for location in locations.iter_mut().flatten() { @@ -1087,13 +1199,16 @@ impl TsServer { specifier: &Url, position: u32, scope: Option<&Arc>, + notebook_uri: Option<&Arc>, token: &CancellationToken, ) -> Result { let req = TscRequest::GetSmartSelectionRange(( self.specifier_map.denormalize(specifier), position, )); - self.request(snapshot, req, scope, token).await + self + .request(snapshot, req, scope, notebook_uri, token) + .await } #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))] @@ -1103,6 +1218,7 @@ impl TsServer { specifier: &Url, range: Range, scope: Option<&Arc>, + notebook_uri: Option<&Arc>, token: &CancellationToken, ) -> Result { let req = TscRequest::GetEncodedSemanticClassifications(( @@ -1113,9 +1229,12 @@ impl TsServer { }, "2020", )); - self.request(snapshot, req, scope, token).await + self + .request(snapshot, req, scope, notebook_uri, token) + .await } + #[allow(clippy::too_many_arguments)] #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))] pub async fn get_signature_help_items( &self, @@ -1124,6 +1243,7 @@ impl TsServer { position: u32, options: SignatureHelpItemsOptions, scope: Option<&Arc>, + notebook_uri: Option<&Arc>, token: &CancellationToken, ) -> Result, AnyError> { let req = TscRequest::GetSignatureHelpItems(( @@ -1131,9 +1251,12 @@ impl TsServer { position, options, )); - self.request(snapshot, req, scope, token).await + self + .request(snapshot, req, scope, notebook_uri, token) + .await } + #[allow(clippy::too_many_arguments)] #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))] pub async fn get_navigate_to_items( &self, @@ -1142,6 +1265,7 @@ impl TsServer { max_result_count: Option, file: Option, scope: Option<&Arc>, + notebook_uri: Option<&Arc>, token: &CancellationToken, ) -> Result, AnyError> { let req = TscRequest::GetNavigateToItems(( @@ -1153,7 +1277,7 @@ impl TsServer { }), )); self - .request::>(snapshot, req, scope, token) + .request::>(snapshot, req, scope, notebook_uri, token) .await .and_then(|mut items| { for item in &mut items { @@ -1166,6 +1290,7 @@ impl TsServer { }) } + #[allow(clippy::too_many_arguments)] #[cfg_attr(feature = "lsp-tracing", tracing::instrument(skip_all))] pub async fn provide_inlay_hints( &self, @@ -1174,6 +1299,7 @@ impl TsServer { text_span: TextSpan, user_preferences: UserPreferences, scope: Option<&Arc>, + notebook_uri: Option<&Arc>, token: &CancellationToken, ) -> Result>, AnyError> { let req = TscRequest::ProvideInlayHints(( @@ -1181,7 +1307,9 @@ impl TsServer { text_span, user_preferences, )); - self.request(snapshot, req, scope, token).await + self + .request(snapshot, req, scope, notebook_uri, token) + .await } async fn request( @@ -1189,6 +1317,7 @@ impl TsServer { snapshot: Arc, req: TscRequest, scope: Option<&Arc>, + notebook_uri: Option<&Arc>, token: &CancellationToken, ) -> Result where @@ -1208,6 +1337,7 @@ impl TsServer { .send(( req, scope.cloned(), + notebook_uri.cloned(), snapshot, tx, token.clone(), @@ -4298,6 +4428,7 @@ struct State { state_snapshot: Arc, specifier_map: Arc, last_scope: Option>, + last_notebook_uri: Option>, token: CancellationToken, pending_requests: Option>, mark: Option, @@ -4320,6 +4451,7 @@ impl State { state_snapshot, specifier_map, last_scope: None, + last_notebook_uri: None, token: Default::default(), mark: None, pending_requests: Some(pending_requests), @@ -4399,6 +4531,7 @@ struct LoadResponse { script_kind: i32, version: Option, is_cjs: bool, + is_classic_script: bool, } #[op2] @@ -4423,6 +4556,7 @@ fn op_load<'s>( script_kind: crate::tsc::as_ts_script_kind(m.media_type), version: state.script_version(&specifier), is_cjs: m.resolution_mode == ResolutionMode::Require, + is_classic_script: m.notebook_uri.is_some(), }); let serialized = serde_v8::to_v8(scope, maybe_load_response)?; state.performance.measure(mark); @@ -4463,6 +4597,7 @@ fn op_resolve( struct TscRequestArray { request: TscRequest, scope: Option>, + notebook_uri: Option>, id: Smi, change: convert::OptionNull, } @@ -4484,13 +4619,14 @@ impl<'a> ToV8<'a> for TscRequestArray { .into(); let args = args.unwrap_or_else(|| v8::Array::new(scope, 0).into()); let scope_url = serde_v8::to_v8(scope, self.scope)?; + let notebook_uri = serde_v8::to_v8(scope, self.notebook_uri)?; let change = self.change.to_v8(scope).unwrap_infallible(); Ok( v8::Array::new_with_elements( scope, - &[id, method_name, args, scope_url, change], + &[id, method_name, args, scope_url, notebook_uri, change], ) .into(), ) @@ -4511,8 +4647,16 @@ async fn op_poll_requests( // clear the resolution cache after each request NodeResolutionThreadLocalCache::clear(); - let Some((request, scope, snapshot, response_tx, token, change, context)) = - pending_requests.recv().await + let Some(( + request, + scope, + notebook_uri, + snapshot, + response_tx, + token, + change, + context, + )) = pending_requests.recv().await else { return None.into(); }; @@ -4526,6 +4670,7 @@ async fn op_poll_requests( let id = state.last_id; state.last_id += 1; state.last_scope.clone_from(&scope); + state.last_notebook_uri.clone_from(¬ebook_uri); let mark = state .performance .mark_with_args(format!("tsc.host.{}", request.method()), &request); @@ -4535,6 +4680,7 @@ async fn op_poll_requests( Some(TscRequestArray { request, scope, + notebook_uri, id: Smi(id), change: change.into(), }) @@ -4582,6 +4728,7 @@ fn op_respond( let state = state.borrow_mut::(); state.performance.measure(state.mark.take().unwrap()); state.last_scope = None; + state.last_notebook_uri = None; let response = if !error.is_empty() { Err(anyhow!("tsc error: {error}")) } else { @@ -4665,6 +4812,7 @@ fn op_exit_span(op_state: &mut OpState, span: *const c_void, root: bool) { struct ScriptNames { unscoped: IndexSet, by_scope: BTreeMap, IndexSet>, + by_notebook_uri: BTreeMap, IndexSet>, } #[op2] @@ -4684,6 +4832,7 @@ fn op_script_names(state: &mut OpState) -> ScriptNames { .into_iter() .filter_map(|s| Some((s?, IndexSet::new()))), ), + by_notebook_uri: Default::default(), }; let scopes_with_node_specifier = state @@ -4728,6 +4877,41 @@ fn op_script_names(state: &mut OpState) -> ScriptNames { } } + // roots for notebook scopes + for (notebook_uri, cell_uris) in state + .state_snapshot + .document_modules + .documents + .cells_by_notebook_uri() + { + let mut script_names = IndexSet::default(); + let scope = state + .state_snapshot + .document_modules + .primary_scope(notebook_uri) + .flatten(); + + // Copy over the globals from the containing regular scopes. + let global_script_names = scope + .and_then(|s| result.by_scope.get(s)) + .unwrap_or(&result.unscoped); + script_names.extend(global_script_names.iter().cloned()); + + // Add the cells as roots. + script_names.extend(cell_uris.iter().flat_map(|u| { + let document = state.state_snapshot.document_modules.documents.get(u)?; + let module = state + .state_snapshot + .document_modules + .module(&document, scope.map(|s| s.as_ref()))?; + Some(module.specifier.to_string()) + })); + + result + .by_notebook_uri + .insert(notebook_uri.clone(), script_names); + } + // finally include the documents for (scope, modules) in state .state_snapshot @@ -5516,7 +5700,7 @@ impl TscRequest { TscRequest::ProvideInlayHints(args) => { ("provideInlayHints", Some(serde_v8::to_v8(scope, args)?)) } - TscRequest::CleanupSemanticCache => ("cleanupSemanticCache", None), + TscRequest::CleanupSemanticCache => ("$cleanupSemanticCache", None), }; Ok(args) @@ -5525,7 +5709,7 @@ impl TscRequest { fn method(&self) -> &'static str { match self { TscRequest::GetDiagnostics(_) => "$getDiagnostics", - TscRequest::CleanupSemanticCache => "cleanupSemanticCache", + TscRequest::CleanupSemanticCache => "$cleanupSemanticCache", TscRequest::FindReferences(_) => "findReferences", TscRequest::GetNavigationTree(_) => "getNavigationTree", TscRequest::GetSupportedCodeFixes => "$getSupportedCodeFixes", @@ -5631,11 +5815,12 @@ mod tests { ); for (relative_specifier, source, version, language_id) in sources { let specifier = temp_dir.url().join(relative_specifier).unwrap(); - document_modules.documents.open( + document_modules.open_document( url_to_uri(&specifier).unwrap(), *version, *language_id, (*source).into(), + None, ); } let snapshot = Arc::new(StateSnapshot { @@ -5658,6 +5843,7 @@ mod tests { .map(|(s, d)| (s.clone(), d.ts_config.clone())) .collect(), ), + None, ); (temp_dir, ts_server, snapshot, cache) } @@ -5713,6 +5899,7 @@ mod tests { snapshot.clone(), [&specifier], snapshot.config.tree.scope_for_specifier(&specifier), + None, &Default::default(), ) .await @@ -5762,6 +5949,7 @@ mod tests { snapshot.clone(), [&specifier], snapshot.config.tree.scope_for_specifier(&specifier), + None, &Default::default(), ) .await @@ -5797,6 +5985,7 @@ mod tests { snapshot.clone(), [&specifier], snapshot.config.tree.scope_for_specifier(&specifier), + None, &Default::default(), ) .await @@ -5828,6 +6017,7 @@ mod tests { snapshot.clone(), [&specifier], snapshot.config.tree.scope_for_specifier(&specifier), + None, &Default::default(), ) .await @@ -5883,6 +6073,7 @@ mod tests { snapshot.clone(), [&specifier], snapshot.config.tree.scope_for_specifier(&specifier), + None, &Default::default(), ) .await @@ -5921,6 +6112,7 @@ mod tests { snapshot.clone(), [&specifier], snapshot.config.tree.scope_for_specifier(&specifier), + None, &Default::default(), ) .await @@ -5985,6 +6177,7 @@ mod tests { snapshot.clone(), [&specifier], snapshot.config.tree.scope_for_specifier(&specifier), + None, &Default::default(), ) .await @@ -6048,6 +6241,7 @@ mod tests { snapshot.clone(), [&specifier], snapshot.config.tree.scope_for_specifier(&specifier), + None, &Default::default(), ) .await @@ -6098,6 +6292,7 @@ mod tests { snapshot.clone(), [(&specifier_dep, ChangeKind::Opened)], None, + None, ); let specifier = temp_dir.url().join("a.ts").unwrap(); let (diagnostics, _) = ts_server @@ -6105,6 +6300,7 @@ mod tests { snapshot.clone(), [&specifier], snapshot.config.tree.scope_for_specifier(&specifier), + None, &Default::default(), ) .await @@ -6182,6 +6378,7 @@ mod tests { }, Default::default(), snapshot.config.tree.scope_for_specifier(&specifier), + None, &Default::default(), ) .await @@ -6199,6 +6396,7 @@ mod tests { None, None, snapshot.config.tree.scope_for_specifier(&specifier), + None, &Default::default(), ) .await @@ -6375,6 +6573,7 @@ mod tests { }, FormatCodeSettings::from(&fmt_options_config), snapshot.config.tree.scope_for_specifier(&specifier), + None, &Default::default(), ) .await @@ -6399,6 +6598,7 @@ mod tests { }), entry.data.clone(), snapshot.config.tree.scope_for_specifier(&specifier), + None, &Default::default(), ) .await @@ -6469,6 +6669,7 @@ mod tests { FormatCodeSettings::default(), UserPreferences::default(), Some(&Arc::new(temp_dir.url())), + None, &Default::default(), ) .await @@ -6563,6 +6764,7 @@ mod tests { .map(|(s, c)| (s.as_ref().into(), c)) .collect(), new_configs_by_scope, + new_notebook_scopes: None, } } let cases = [ @@ -6623,7 +6825,7 @@ mod tests { for (start, new, expected) in cases { let mut pending = start; - pending.coalesce(new.project_version, new.modified_scripts, None); + pending.coalesce(new.project_version, new.modified_scripts, None, None); assert_eq!(json!(pending), json!(expected)); } } diff --git a/cli/lsp/urls.rs b/cli/lsp/urls.rs index 30df9947ed..fbc37de7eb 100644 --- a/cli/lsp/urls.rs +++ b/cli/lsp/urls.rs @@ -132,8 +132,7 @@ pub fn uri_to_url(uri: &Uri) -> Url { } Url::parse(&format!( "file:///{}", - &uri.as_str()[uri.path_bounds.0 as usize..uri.path_bounds.1 as usize] - .trim_start_matches('/'), + &uri.as_str()[uri.path_bounds.0 as usize..].trim_start_matches('/'), )) .ok() .map(normalize_url) @@ -161,9 +160,15 @@ fn normalize_url(url: Url) -> Url { return url; }; let normalized_path = normalize_path(&path); - let Ok(normalized_url) = Url::from_file_path(&normalized_path) else { + let Ok(mut normalized_url) = Url::from_file_path(&normalized_path) else { return url; }; + if let Some(query) = url.query() { + normalized_url.set_query(Some(query)); + } + if let Some(fragment) = url.fragment() { + normalized_url.set_fragment(Some(fragment)); + } normalized_url } diff --git a/cli/tsc/97_ts_host.js b/cli/tsc/97_ts_host.js index 4501a36cc8..f76412c430 100644 --- a/cli/tsc/97_ts_host.js +++ b/cli/tsc/97_ts_host.js @@ -131,7 +131,7 @@ export function assert(cond, msg = "Assertion failed.") { /** @type {Map} */ export const SOURCE_FILE_CACHE = new Map(); -/** @type {Map} */ +/** @type {Map} */ export const SCRIPT_SNAPSHOT_CACHE = new Map(); /** @type {Map} */ @@ -174,6 +174,15 @@ export const LAST_REQUEST_SCOPE = { }, }; +/** @type {string | null} */ +let lastRequestNotebookUri = null; +export const LAST_REQUEST_NOTEBOOK_URI = { + get: () => lastRequestNotebookUri, + set: (notebookUri) => { + lastRequestNotebookUri = notebookUri; + }, +}; + /** @param sourceFile {ts.SourceFile} */ function isNodeSourceFile(sourceFile) { const fileName = sourceFile.fileName; @@ -379,14 +388,15 @@ class CancellationToken { * ls: ts.LanguageService & { [k:string]: any }, * compilerOptions: ts.CompilerOptions, * }} LanguageServiceEntry */ -/** @type {{ unscoped: LanguageServiceEntry, byScope: Map }} */ +/** @type {{ unscoped: LanguageServiceEntry, byScope: Map, byNotebookUri: Map }} */ export const LANGUAGE_SERVICE_ENTRIES = { // @ts-ignore Will be set later. unscoped: null, byScope: new Map(), + byNotebookUri: new Map(), }; -/** @type {{ unscoped: string[], byScope: Map } | null} */ +/** @type {{ unscoped: string[], byScope: Map, byNotebookUri: Map } | null} */ let SCRIPT_NAMES_CACHE = null; export function clearScriptNamesCache() { @@ -668,16 +678,22 @@ const hostImpl = { debug("host.getScriptFileNames()"); } if (!SCRIPT_NAMES_CACHE) { - const { unscoped, byScope } = ops.op_script_names(); + const { unscoped, byScope, byNotebookUri } = ops.op_script_names(); SCRIPT_NAMES_CACHE = { unscoped, byScope: new Map(Object.entries(byScope)), + byNotebookUri: new Map(Object.entries(byNotebookUri)), }; } const lastRequestScope = LAST_REQUEST_SCOPE.get(); - return (lastRequestScope - ? SCRIPT_NAMES_CACHE.byScope.get(lastRequestScope) - : null) ?? SCRIPT_NAMES_CACHE.unscoped; + const lastRequestNotebookUri = LAST_REQUEST_NOTEBOOK_URI.get(); + return (lastRequestNotebookUri + ? SCRIPT_NAMES_CACHE.byNotebookUri.get(lastRequestNotebookUri) + : null) ?? + (lastRequestScope + ? SCRIPT_NAMES_CACHE.byScope.get(lastRequestScope) + : null) ?? + SCRIPT_NAMES_CACHE.unscoped; }, getScriptVersion(specifier) { if (logDebug) { @@ -714,13 +730,14 @@ const hostImpl = { } let scriptSnapshot = SCRIPT_SNAPSHOT_CACHE.get(specifier); if (scriptSnapshot == undefined) { - /** @type {{ data: string, version: string, isCjs: boolean }} */ + /** @type {{ data: string, version: string, isCjs: boolean, isClassicScript: boolean }} */ const fileInfo = ops.op_load(specifier); if (!fileInfo) { return undefined; } scriptSnapshot = ts.ScriptSnapshot.fromString(fileInfo.data); scriptSnapshot.isCjs = fileInfo.isCjs; + scriptSnapshot.isClassicScript = fileInfo.isClassicScript; SCRIPT_SNAPSHOT_CACHE.set(specifier, scriptSnapshot); SCRIPT_VERSION_CACHE.set(specifier, fileInfo.version); } @@ -795,6 +812,13 @@ export function filterMapDiagnostic(diagnostic) { ) { return false; } + const isClassicScript = !diagnostic.file?.["externalModuleIndicator"]; + if (isClassicScript) { + // Top-level-await. + if (diagnostic.code == 1375) { + return false; + } + } // make the diagnostic for using an `export =` in an es module a warning if (diagnostic.code === 1203) { diagnostic.category = ts.DiagnosticCategory.Warning; diff --git a/cli/tsc/98_lsp.js b/cli/tsc/98_lsp.js index f6ef817173..1da40dfead 100644 --- a/cli/tsc/98_lsp.js +++ b/cli/tsc/98_lsp.js @@ -13,6 +13,7 @@ import { IS_NODE_SOURCE_FILE_CACHE, LANGUAGE_SERVICE_ENTRIES, LAST_REQUEST_METHOD, + LAST_REQUEST_NOTEBOOK_URI, LAST_REQUEST_SCOPE, OperationCanceledError, PROJECT_VERSION_CACHE, @@ -106,6 +107,9 @@ const documentRegistry = { true, scriptKind, ); + if (scriptSnapshot.isClassicScript) { + sourceFile.externalModuleIndicator = undefined; + } documentRegistrySourceFileCache.set(mapKey, sourceFile); } const sourceRefCount = SOURCE_REF_COUNTS.get(fileName) ?? 0; @@ -168,6 +172,9 @@ const documentRegistry = { /** @type {ts.IScriptSnapshot} */ (sourceFile.scriptSnapShot), ), ); + if (scriptSnapshot.isClassicScript) { + sourceFile.externalModuleIndicator = undefined; + } documentRegistrySourceFileCache.set(mapKey, sourceFile); } return sourceFile; @@ -272,7 +279,7 @@ function respond(_id, data = null, error = null) { } } -/** @typedef {[[string, number][], number, [string, any][]] } PendingChange */ +/** @typedef {[[string, number][], number, [string, any][], [string, string][]] } PendingChange */ /** * @template T * @typedef {T | null} Option */ @@ -357,6 +364,7 @@ export async function serverMainLoop(enableDebugLogging) { request[2], request[3], request[4], + request[5], ); } catch (err) { error(`Internal error occurred processing request: ${err}`); @@ -407,14 +415,16 @@ function arraysEqual(a, b) { * @param {string} method * @param {any[]} args * @param {string | null} scope + * @param {string | null} notebookUri * @param {PendingChange | null} maybeChange */ -function serverRequestInner(id, method, args, scope, maybeChange) { - debug(`serverRequest()`, id, method, args, scope, maybeChange); +function serverRequestInner(id, method, args, scope, notebookUri, maybeChange) { + debug(`serverRequest()`, id, method, args, scope, notebookUri, maybeChange); if (maybeChange !== null) { const changedScripts = maybeChange[0]; const newProjectVersion = maybeChange[1]; const newConfigsByScope = maybeChange[2]; + const newNotebookScopes = maybeChange[3]; if (newConfigsByScope) { IS_NODE_SOURCE_FILE_CACHE.clear(); ASSET_SCOPES.clear(); @@ -422,6 +432,7 @@ function serverRequestInner(id, method, args, scope, maybeChange) { const newByScope = new Map(); for (const [scope, config] of newConfigsByScope) { LAST_REQUEST_SCOPE.set(scope); + LAST_REQUEST_NOTEBOOK_URI.set(null); const oldEntry = LANGUAGE_SERVICE_ENTRIES.byScope.get(scope); const ls = oldEntry ? oldEntry.ls : createLs(); const compilerOptions = lspTsConfigToCompilerOptions(config); @@ -433,6 +444,27 @@ function serverRequestInner(id, method, args, scope, maybeChange) { } LANGUAGE_SERVICE_ENTRIES.byScope = newByScope; } + if (newNotebookScopes) { + /** @type { typeof LANGUAGE_SERVICE_ENTRIES.byNotebookUri } */ + const newByNotebookUri = new Map(); + for (const [notebookUri, scope] of newNotebookScopes) { + LAST_REQUEST_SCOPE.set(scope); + LAST_REQUEST_NOTEBOOK_URI.set(notebookUri); + const oldEntry = LANGUAGE_SERVICE_ENTRIES.byNotebookUri.get( + notebookUri, + ); + const ls = oldEntry ? oldEntry.ls : createLs(); + const compilerOptions = + LANGUAGE_SERVICE_ENTRIES.byScope.get(scope)?.compilerOptions ?? + LANGUAGE_SERVICE_ENTRIES.unscoped.compilerOptions; + newByNotebookUri.set(notebookUri, { ls, compilerOptions }); + LANGUAGE_SERVICE_ENTRIES.byNotebookUri.delete(notebookUri); + } + for (const oldEntry of LANGUAGE_SERVICE_ENTRIES.byNotebookUri.values()) { + oldEntry.ls.dispose(); + } + LANGUAGE_SERVICE_ENTRIES.byNotebookUri = newByNotebookUri; + } PROJECT_VERSION_CACHE.set(newProjectVersion); @@ -448,7 +480,7 @@ function serverRequestInner(id, method, args, scope, maybeChange) { SCRIPT_SNAPSHOT_CACHE.delete(script); } - if (newConfigsByScope || opened || closed) { + if (newConfigsByScope || newNotebookScopes || opened || closed) { clearScriptNamesCache(); } } @@ -461,9 +493,28 @@ function serverRequestInner(id, method, args, scope, maybeChange) { } LAST_REQUEST_METHOD.set(method); LAST_REQUEST_SCOPE.set(scope); - const ls = (scope ? LANGUAGE_SERVICE_ENTRIES.byScope.get(scope)?.ls : null) ?? - LANGUAGE_SERVICE_ENTRIES.unscoped.ls; + LAST_REQUEST_NOTEBOOK_URI.set(notebookUri); + const ls = + (notebookUri + ? LANGUAGE_SERVICE_ENTRIES.byNotebookUri.get(notebookUri)?.ls + : null) ?? + (scope ? LANGUAGE_SERVICE_ENTRIES.byScope.get(scope)?.ls : null) ?? + LANGUAGE_SERVICE_ENTRIES.unscoped.ls; switch (method) { + case "$cleanupSemanticCache": { + for ( + const ls of [ + LANGUAGE_SERVICE_ENTRIES.unscoped.ls, + ...[...LANGUAGE_SERVICE_ENTRIES.byScope.values()].map((e) => e.ls), + ...[...LANGUAGE_SERVICE_ENTRIES.byNotebookUri.values()].map((e) => + e.ls + ), + ] + ) { + ls.cleanupSemanticCache(); + } + return respond(id, null); + } case "$getSupportedCodeFixes": { return respond( id, @@ -510,7 +561,14 @@ function serverRequestInner(id, method, args, scope, maybeChange) { return respond( id, [[], null], - formatErrorWithArgs(e, [id, method, args, scope, maybeChange]), + formatErrorWithArgs(e, [ + id, + method, + args, + scope, + notebookUri, + maybeChange, + ]), ); } return respond(id, [[], null]); @@ -530,7 +588,14 @@ function serverRequestInner(id, method, args, scope, maybeChange) { return respond( id, null, - formatErrorWithArgs(e, [id, method, args, scope, maybeChange]), + formatErrorWithArgs(e, [ + id, + method, + args, + scope, + notebookUri, + maybeChange, + ]), ); } return respond(id); @@ -548,12 +613,13 @@ function serverRequestInner(id, method, args, scope, maybeChange) { * @param {string} method * @param {any[]} args * @param {string | null} scope + * @param {string | null} notebookUri * @param {PendingChange | null} maybeChange */ -function serverRequest(id, method, args, scope, maybeChange) { +function serverRequest(id, method, args, scope, notebookUri, maybeChange) { const span = ops.op_make_span(`serverRequest(${method})`, true); try { - serverRequestInner(id, method, args, scope, maybeChange); + serverRequestInner(id, method, args, scope, notebookUri, maybeChange); } finally { ops.op_exit_span(span, true); } diff --git a/cli/util/sync/async_flag.rs b/cli/util/sync/async_flag.rs index c0ec57fa6a..dbadc0d5ae 100644 --- a/cli/util/sync/async_flag.rs +++ b/cli/util/sync/async_flag.rs @@ -15,14 +15,10 @@ impl Default for AsyncFlag { impl AsyncFlag { pub fn raise(&self) { - self.0.close(); - } - - pub fn is_raised(&self) -> bool { - self.0.is_closed() + self.0.add_permits(1); } pub async fn wait_raised(&self) { - self.0.acquire().await.unwrap_err(); + drop(self.0.acquire().await); } } diff --git a/tests/integration/lsp_tests.rs b/tests/integration/lsp_tests.rs index 85ae69b8b2..f605df254e 100644 --- a/tests/integration/lsp_tests.rs +++ b/tests/integration/lsp_tests.rs @@ -14,6 +14,7 @@ use test_util::lsp::range_of; use test_util::lsp::source_file; use test_util::lsp::LspClient; use test_util::testdata_path; +use test_util::url_to_notebook_cell_uri; use test_util::url_to_uri; use test_util::TestContextBuilder; use tower_lsp::lsp_types as lsp; @@ -11417,45 +11418,260 @@ fn lsp_diagnostics_none_for_resolving_types() { #[test] #[timeout(300_000)] -fn lsp_jupyter_diagnostics() { +fn lsp_jupyter_import_map_and_diagnostics() { let context = TestContextBuilder::new().use_temp_cwd().build(); + let temp_dir = context.temp_dir(); + temp_dir.write( + "deno.json", + json!({ + "imports": { + "exports": "./exports.ts", + }, + }) + .to_string(), + ); + temp_dir.write("./exports.ts", "export const someExport = 1;\n"); let mut client = context.new_lsp_command().build(); client.initialize_default(); - let diagnostics = client.did_open(json!({ - "textDocument": { - "uri": "deno-notebook-cell:/a/file.ts#abc", - "languageId": "typescript", - "version": 1, - "text": "Deno.readTextFileSync(1234);", - }, - })); + + let diagnostics = client.notebook_did_open( + url_to_uri(&temp_dir.url().join("file.ipynb").unwrap()).unwrap(), + 1, + vec![ + json!({ + "uri": url_to_notebook_cell_uri(&temp_dir.url().join("file.ipynb#a").unwrap()), + "languageId": "typescript", + "version": 1, + "text": r#" + // This should be shared between cells. + import { someExport } from "exports"; + "#, + }), + json!({ + "uri": url_to_notebook_cell_uri(&temp_dir.url().join("file.ipynb#b").unwrap()), + "languageId": "typescript", + "version": 1, + "text": r#" + // This should produce a type checking error. + const someString: string = someExport; + console.log(someString); + + // Top-level-await should be allowed. + await new Promise((r) => r(null)); + + // Local variables conflicting with globals. + const name = "Hello"; + console.log(name); + + // This should transfer to the next cell. + const someNumber = 123; + "#, + }), + json!({ + "uri": url_to_notebook_cell_uri(&temp_dir.url().join("file.ipynb#c").unwrap()), + "languageId": "typescript", + "version": 1, + "text": r#" + console.log(someNumber); + "#, + }), + ], + ); assert_eq!( json!(diagnostics.all_messages()), json!([ { - "uri": "deno-notebook-cell:/a/file.ts#abc", + "uri": url_to_notebook_cell_uri(&temp_dir.url().join("file.ipynb#c").unwrap()), + "diagnostics": [], + "version": 1, + }, + { + "uri": url_to_notebook_cell_uri(&temp_dir.url().join("file.ipynb#b").unwrap()), "diagnostics": [ { "range": { - "start": { - "line": 0, - "character": 22, - }, - "end": { - "line": 0, - "character": 26, - }, + "start": { "line": 2, "character": 16 }, + "end": { "line": 2, "character": 26 }, }, "severity": 1, - "code": 2345, + "code": 2322, "source": "deno-ts", - "message": "Argument of type 'number' is not assignable to parameter of type 'string | URL'.", + "message": "Type 'number' is not assignable to type 'string'.", + }, + // TODO(nayeemrmn): This only errors for classic scripts which we use + // for notebook cells. Figure out a workaround. + { + "range": { + "start": { "line": 9, "character": 16 }, + "end": { "line": 9, "character": 20 }, + }, + "severity": 1, + "code": 2451, + "source": "deno-ts", + "message": "Cannot redeclare block-scoped variable 'name'.", + "relatedInformation": [ + { + "location": { + "uri": "deno:/asset/lib.deno.window.d.ts", + "range": { + "start": { "line": 466, "character": 12 }, + "end": { "line": 466, "character": 16 }, + }, + }, + "message": "'name' was also declared here.", + }, + ], }, ], "version": 1, }, - ]) + { + "uri": url_to_notebook_cell_uri(&temp_dir.url().join("file.ipynb#a").unwrap()), + "diagnostics": [], + "version": 1, + }, + ]), ); + + client.write_notification( + "notebookDocument/didChange", + json!({ + "notebookDocument": { + "version": 2, + "uri": url_to_uri(&temp_dir.url().join("file.ipynb").unwrap()).unwrap(), + }, + "change": { + "cells": { + "structure": { + "array": { "start": 1, "deleteCount": 1 }, + "didOpen": [], + "didClose": [ + { "uri": url_to_notebook_cell_uri(&temp_dir.url().join("file.ipynb#b").unwrap()) }, + ], + }, + }, + }, + }), + ); + let diagnostics = client.read_diagnostics(); + assert_eq!( + json!(diagnostics.all_messages()), + json!([ + { + "uri": url_to_notebook_cell_uri(&temp_dir.url().join("file.ipynb#b").unwrap()), + "diagnostics": [], + "version": 1, + }, + { + "uri": url_to_notebook_cell_uri(&temp_dir.url().join("file.ipynb#c").unwrap()), + "diagnostics": [ + { + "range": { + "start": { "line": 1, "character": 22 }, + "end": { "line": 1, "character": 32 }, + }, + "severity": 1, + "code": 2304, + "source": "deno-ts", + "message": "Cannot find name 'someNumber'.", + }, + ], + "version": 1, + }, + { + "uri": url_to_notebook_cell_uri(&temp_dir.url().join("file.ipynb#a").unwrap()), + "diagnostics": [], + "version": 1, + }, + ]), + ); + + client.shutdown(); +} + +#[test] +#[timeout(300_000)] +fn lsp_jupyter_completions() { + let context = TestContextBuilder::new().use_temp_cwd().build(); + let temp_dir = context.temp_dir(); + temp_dir.write("deno.json", json!({}).to_string()); + temp_dir.write("./other.ts", ""); + let mut client = context.new_lsp_command().build(); + client.initialize_default(); + client.notebook_did_open( + url_to_uri(&temp_dir.url().join("file.ipynb").unwrap()).unwrap(), + 1, + vec![ + json!({ + "uri": url_to_notebook_cell_uri(&temp_dir.url().join("file.ipynb#a").unwrap()), + "languageId": "typescript", + "version": 1, + "text": r#" + import "./othe"; + await Deno.readTextFil; + "#, + }), + ], + ); + + let list = client.get_completion_list( + url_to_notebook_cell_uri(&temp_dir.url().join("file.ipynb#a").unwrap()) + .as_str(), + (1, 24), + json!({ "triggerKind": 1 }), + ); + assert!(!list.is_incomplete); + let item = list + .items + .iter() + .find(|item| item.label == "other.ts") + .unwrap(); + assert_eq!( + json!(item), + json!({ + "label": "other.ts", + "kind": 17, + "detail": "(local)", + "sortText": "1", + "filterText": "./other.ts", + "textEdit": { + "range": { + "start": { "line": 1, "character": 18 }, + "end": { "line": 1, "character": 24 }, + }, + "newText": "./other.ts", + }, + "commitCharacters": ["\"", "'"], + }), + ); + + let list = client.get_completion_list( + url_to_notebook_cell_uri(&temp_dir.url().join("file.ipynb#a").unwrap()) + .as_str(), + (2, 32), + json!({ "triggerKind": 1 }), + ); + assert!(!list.is_incomplete); + let item = list + .items + .iter() + .find(|item| item.label == "readTextFile") + .unwrap(); + let item = client.write_request("completionItem/resolve", item); + assert_eq!( + json!(item), + json!({ + "label": "readTextFile", + "kind": 3, + "detail": "function Deno.readTextFile(path: string | URL, options?: Deno.ReadFileOptions): Promise", + "documentation": { + "kind": "markdown", + "value": "Asynchronously reads and returns the entire contents of a file as an UTF-8\ndecoded string. Reading a directory throws an error.\n\n```ts\nconst data = await Deno.readTextFile(\"hello.txt\");\nconsole.log(data);\n```\n\nRequires `allow-read` permission.\n\n*@tags* - allow-read \n\n*@category* - File System", + }, + "sortText": "11", + }), + ); + client.shutdown(); } @@ -16540,67 +16756,6 @@ fn lsp_import_unstable_bare_node_builtins_auto_discovered() { client.shutdown(); } -#[test] -#[timeout(300_000)] -fn lsp_jupyter_byonm_diagnostics() { - let context = TestContextBuilder::for_npm().use_temp_cwd().build(); - let temp_dir = context.temp_dir().path(); - temp_dir.join("package.json").write_json(&json!({ - "dependencies": { - "@denotest/esm-basic": "*" - } - })); - temp_dir.join("deno.json").write_json(&json!({ - "unstable": ["byonm"] - })); - context.run_npm("install"); - let mut client = context.new_lsp_command().build(); - client.initialize_default(); - let notebook_uri = temp_dir.join("notebook.ipynb").uri_file(); - let notebook_specifier = format!( - "{}#abc", - notebook_uri - .to_string() - .replace("file://", "deno-notebook-cell:") - ); - let diagnostics = client.did_open(json!({ - "textDocument": { - "uri": notebook_specifier, - "languageId": "typescript", - "version": 1, - "text": "import { getValue, nonExistent } from '@denotest/esm-basic';\n console.log(getValue, nonExistent);", - }, - })); - assert_eq!( - json!(diagnostics.all_messages()), - json!([ - { - "uri": notebook_specifier, - "diagnostics": [ - { - "range": { - "start": { - "line": 0, - "character": 19, - }, - "end": { - "line": 0, - "character": 30, - }, - }, - "severity": 1, - "code": 2305, - "source": "deno-ts", - "message": "Module '\"@denotest/esm-basic\"' has no exported member 'nonExistent'.", - }, - ], - "version": 1, - }, - ]) - ); - client.shutdown(); -} - #[test] #[timeout(300_000)] fn lsp_byonm() { diff --git a/tests/util/server/src/fs.rs b/tests/util/server/src/fs.rs index 01ea502df6..ebfea8daa2 100644 --- a/tests/util/server/src/fs.rs +++ b/tests/util/server/src/fs.rs @@ -95,6 +95,15 @@ pub fn url_to_uri(url: &Url) -> Result { }) } +pub fn url_to_notebook_cell_uri(url: &Url) -> Uri { + let uri = url_to_uri(url).unwrap(); + Uri::from_str(&format!( + "vscode-notebook-cell:{}", + uri.as_str().strip_prefix("file:").unwrap() + )) + .unwrap() +} + /// Represents a path on the file system, which can be used /// to perform specific actions. #[derive(Clone, Debug, Default, PartialEq, Eq, Hash)] diff --git a/tests/util/server/src/lib.rs b/tests/util/server/src/lib.rs index 6465e4cdd3..efd6831972 100644 --- a/tests/util/server/src/lib.rs +++ b/tests/util/server/src/lib.rs @@ -40,6 +40,7 @@ pub use builders::TestCommandBuilder; pub use builders::TestCommandOutput; pub use builders::TestContext; pub use builders::TestContextBuilder; +pub use fs::url_to_notebook_cell_uri; pub use fs::url_to_uri; pub use fs::PathRef; pub use fs::TempDir; diff --git a/tests/util/server/src/lsp.rs b/tests/util/server/src/lsp.rs index b788960dd7..ab186c4686 100644 --- a/tests/util/server/src/lsp.rs +++ b/tests/util/server/src/lsp.rs @@ -918,6 +918,42 @@ impl LspClient { self.write_notification("textDocument/didOpen", params); } + pub fn notebook_did_open( + &mut self, + uri: Uri, + version: i32, + cells: Vec, + ) -> CollectedDiagnostics { + let cells = cells + .into_iter() + .map(|c| serde_json::from_value::(c).unwrap()) + .collect::>(); + let params = lsp::DidOpenNotebookDocumentParams { + notebook_document: lsp::NotebookDocument { + uri, + notebook_type: "jupyter-notebook".to_string(), + version, + metadata: None, + cells: cells + .iter() + .map(|c| lsp::NotebookCell { + kind: if c.language_id == "markdown" { + lsp::NotebookCellKind::Markup + } else { + lsp::NotebookCellKind::Code + }, + document: c.uri.clone(), + metadata: None, + execution_summary: None, + }) + .collect(), + }, + cell_text_documents: cells, + }; + self.write_notification("notebookDocument/didOpen", json!(params)); + self.read_diagnostics() + } + pub fn change_configuration(&mut self, config: Value) { self.config = config; if self.supports_workspace_configuration {