diff options
-rw-r--r-- | cli/lsp/analysis.rs | 13 | ||||
-rw-r--r-- | cli/lsp/config.rs | 2 | ||||
-rw-r--r-- | cli/lsp/mod.rs | 66 | ||||
-rw-r--r-- | cli/lsp/sources.rs | 10 | ||||
-rw-r--r-- | cli/lsp/state.rs | 109 | ||||
-rw-r--r-- | cli/lsp/tsc.rs | 27 |
6 files changed, 202 insertions, 25 deletions
diff --git a/cli/lsp/analysis.rs b/cli/lsp/analysis.rs index 370b41c45..95e21ed9a 100644 --- a/cli/lsp/analysis.rs +++ b/cli/lsp/analysis.rs @@ -13,9 +13,10 @@ use deno_core::ModuleSpecifier; use deno_lint::rules; use lsp_types::Position; use lsp_types::Range; -use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; +use std::sync::Arc; +use std::sync::RwLock; /// Category of self-generated diagnostic messages (those not coming from) /// TypeScript. @@ -113,11 +114,13 @@ pub enum ResolvedImport { pub fn resolve_import( specifier: &str, referrer: &ModuleSpecifier, - maybe_import_map: Option<Rc<RefCell<ImportMap>>>, + maybe_import_map: Option<Arc<RwLock<ImportMap>>>, ) -> ResolvedImport { let maybe_mapped = if let Some(import_map) = maybe_import_map { - if let Ok(maybe_specifier) = - import_map.borrow().resolve(specifier, referrer.as_str()) + if let Ok(maybe_specifier) = import_map + .read() + .unwrap() + .resolve(specifier, referrer.as_str()) { maybe_specifier } else { @@ -159,7 +162,7 @@ pub fn analyze_dependencies( specifier: &ModuleSpecifier, source: &str, media_type: &MediaType, - maybe_import_map: Option<Rc<RefCell<ImportMap>>>, + maybe_import_map: Option<Arc<RwLock<ImportMap>>>, ) -> Option<(HashMap<String, Dependency>, Option<ResolvedImport>)> { let specifier_str = specifier.to_string(); let source_map = Rc::new(swc_common::SourceMap::default()); diff --git a/cli/lsp/config.rs b/cli/lsp/config.rs index ebc145708..fc3f030c9 100644 --- a/cli/lsp/config.rs +++ b/cli/lsp/config.rs @@ -4,6 +4,7 @@ use deno_core::error::AnyError; use deno_core::serde::Deserialize; use deno_core::serde_json; use deno_core::serde_json::Value; +use deno_core::url::Url; #[derive(Debug, Clone, Default)] pub struct ClientCapabilities { @@ -23,6 +24,7 @@ pub struct WorkspaceSettings { #[derive(Debug, Clone, Default)] pub struct Config { pub client_capabilities: ClientCapabilities, + pub root_uri: Option<Url>, pub settings: WorkspaceSettings, } diff --git a/cli/lsp/mod.rs b/cli/lsp/mod.rs index e3092d815..784f3503d 100644 --- a/cli/lsp/mod.rs +++ b/cli/lsp/mod.rs @@ -18,6 +18,7 @@ use config::Config; use diagnostics::DiagnosticSource; use dispatch::NotificationDispatcher; use dispatch::RequestDispatcher; +use state::update_import_map; use state::DocumentData; use state::Event; use state::ServerState; @@ -87,13 +88,13 @@ pub fn start() -> Result<(), AnyError> { } let mut config = Config::default(); + config.root_uri = initialize_params.root_uri.clone(); if let Some(value) = initialize_params.initialization_options { config.update(value)?; } config.update_capabilities(&initialize_params.capabilities); let mut server_state = state::ServerState::new(connection.sender, config); - let state = server_state.snapshot(); // TODO(@kitsonk) need to make this configurable, respect unstable let ts_config = TsConfig::new(json!({ @@ -106,6 +107,7 @@ pub fn start() -> Result<(), AnyError> { "strict": true, "target": "esnext", })); + let state = server_state.snapshot(); tsc::request( &mut server_state.ts_runtime, &state, @@ -259,7 +261,7 @@ impl ServerState { specifier.clone(), params.text_document.version, ¶ms.text_document.text, - None, + state.maybe_import_map.clone(), ), ) .is_some() @@ -281,7 +283,11 @@ impl ServerState { let mut content = file_cache.get_contents(file_id)?; apply_content_changes(&mut content, params.content_changes); let doc_data = state.doc_data.get_mut(&specifier).unwrap(); - doc_data.update(params.text_document.version, &content, None); + doc_data.update( + params.text_document.version, + &content, + state.maybe_import_map.clone(), + ); file_cache.set_contents(specifier, Some(content.into_bytes())); Ok(()) @@ -326,6 +332,15 @@ impl ServerState { if let Err(err) = state.config.update(config.clone()) { error!("failed to update settings: {}", err); } + if let Err(err) = update_import_map(state) { + state + .send_notification::<lsp_types::notification::ShowMessage>( + lsp_types::ShowMessageParams { + typ: lsp_types::MessageType::Warning, + message: err.to_string(), + }, + ); + } } } (None, None) => { @@ -337,6 +352,15 @@ impl ServerState { Ok(()) })? + .on::<lsp_types::notification::DidChangeWatchedFiles>(|state, params| { + // if the current import map has changed, we need to reload it + if let Some(import_map_uri) = &state.maybe_import_map_uri { + if params.changes.iter().any(|fe| import_map_uri == &fe.uri) { + update_import_map(state)?; + } + } + Ok(()) + })? .finish(); Ok(()) @@ -395,8 +419,40 @@ impl ServerState { /// Start consuming events from the provided receiver channel. pub fn run(mut self, inbox: Receiver<Message>) -> Result<(), AnyError> { - // currently we don't need to do any other loading or tasks, so as soon as - // we run we are "ready" + // Check to see if we need to setup the import map + if let Err(err) = update_import_map(&mut self) { + self.send_notification::<lsp_types::notification::ShowMessage>( + lsp_types::ShowMessageParams { + typ: lsp_types::MessageType::Warning, + message: err.to_string(), + }, + ); + } + + // we are going to watch all the JSON files in the workspace, and the + // notification handler will pick up any of the changes of those files we + // are interested in. + let watch_registration_options = + lsp_types::DidChangeWatchedFilesRegistrationOptions { + watchers: vec![lsp_types::FileSystemWatcher { + glob_pattern: "**/*.json".to_string(), + kind: Some(lsp_types::WatchKind::Change), + }], + }; + let registration = lsp_types::Registration { + id: "workspace/didChangeWatchedFiles".to_string(), + method: "workspace/didChangeWatchedFiles".to_string(), + register_options: Some( + serde_json::to_value(watch_registration_options).unwrap(), + ), + }; + self.send_request::<lsp_types::request::RegisterCapability>( + lsp_types::RegistrationParams { + registrations: vec![registration], + }, + |_, _| (), + ); + self.transition(Status::Ready); while let Some(event) = self.next_event(&inbox) { diff --git a/cli/lsp/sources.rs b/cli/lsp/sources.rs index 4f80044a2..c6a15461f 100644 --- a/cli/lsp/sources.rs +++ b/cli/lsp/sources.rs @@ -7,6 +7,7 @@ use crate::file_fetcher::get_source_from_bytes; use crate::file_fetcher::map_content_type; use crate::http_cache; use crate::http_cache::HttpCache; +use crate::import_map::ImportMap; use crate::media_type::MediaType; use crate::text_encoding; @@ -16,6 +17,8 @@ use std::collections::HashMap; use std::fs; use std::path::Path; use std::path::PathBuf; +use std::sync::Arc; +use std::sync::RwLock; use std::time::SystemTime; #[derive(Debug, Clone, Default)] @@ -30,6 +33,7 @@ struct Metadata { #[derive(Debug, Clone, Default)] pub struct Sources { http_cache: HttpCache, + maybe_import_map: Option<Arc<RwLock<ImportMap>>>, metadata: HashMap<ModuleSpecifier, Metadata>, redirects: HashMap<ModuleSpecifier, ModuleSpecifier>, remotes: HashMap<ModuleSpecifier, PathBuf>, @@ -124,7 +128,11 @@ impl Sources { if let Ok(source) = get_source_from_bytes(bytes, maybe_charset) { let mut maybe_types = if let Some(types) = headers.get("x-typescript-types") { - Some(analysis::resolve_import(types, &specifier, None)) + Some(analysis::resolve_import( + types, + &specifier, + self.maybe_import_map.clone(), + )) } else { None }; diff --git a/cli/lsp/state.rs b/cli/lsp/state.rs index 18a1e4023..579a749f6 100644 --- a/cli/lsp/state.rs +++ b/cli/lsp/state.rs @@ -18,6 +18,9 @@ use crossbeam_channel::select; use crossbeam_channel::unbounded; use crossbeam_channel::Receiver; use crossbeam_channel::Sender; +use deno_core::error::anyhow; +use deno_core::error::AnyError; +use deno_core::url::Url; use deno_core::JsRuntime; use deno_core::ModuleSpecifier; use lsp_server::Message; @@ -25,11 +28,10 @@ use lsp_server::Notification; use lsp_server::Request; use lsp_server::RequestId; use lsp_server::Response; -use std::cell::RefCell; use std::collections::HashMap; use std::env; use std::fmt; -use std::rc::Rc; +use std::fs; use std::sync::Arc; use std::sync::RwLock; use std::time::Instant; @@ -37,6 +39,45 @@ use std::time::Instant; type ReqHandler = fn(&mut ServerState, Response); type ReqQueue = lsp_server::ReqQueue<(String, Instant), ReqHandler>; +pub fn update_import_map(state: &mut ServerState) -> Result<(), AnyError> { + if let Some(import_map_str) = &state.config.settings.import_map { + let import_map_url = if let Ok(url) = Url::from_file_path(import_map_str) { + Ok(url) + } else if let Some(root_uri) = &state.config.root_uri { + let root_path = root_uri + .to_file_path() + .map_err(|_| anyhow!("Bad root_uri: {}", root_uri))?; + let import_map_path = root_path.join(import_map_str); + Url::from_file_path(import_map_path).map_err(|_| { + anyhow!("Bad file path for import map: {:?}", import_map_str) + }) + } else { + Err(anyhow!( + "The path to the import map (\"{}\") is not resolvable.", + import_map_str + )) + }?; + let import_map_path = import_map_url + .to_file_path() + .map_err(|_| anyhow!("Bad file path."))?; + let import_map_json = + fs::read_to_string(import_map_path).map_err(|err| { + anyhow!( + "Failed to load the import map at: {}. [{}]", + import_map_url, + err + ) + })?; + let import_map = + ImportMap::from_json(&import_map_url.to_string(), &import_map_json)?; + state.maybe_import_map_uri = Some(import_map_url); + state.maybe_import_map = Some(Arc::new(RwLock::new(import_map))); + } else { + state.maybe_import_map = None; + } + Ok(()) +} + pub enum Event { Message(Message), Task(Task), @@ -107,7 +148,7 @@ impl DocumentData { specifier: ModuleSpecifier, version: i32, source: &str, - maybe_import_map: Option<Rc<RefCell<ImportMap>>>, + maybe_import_map: Option<Arc<RwLock<ImportMap>>>, ) -> Self { let dependencies = if let Some((dependencies, _)) = analysis::analyze_dependencies( @@ -131,7 +172,7 @@ impl DocumentData { &mut self, version: i32, source: &str, - maybe_import_map: Option<Rc<RefCell<ImportMap>>>, + maybe_import_map: Option<Arc<RwLock<ImportMap>>>, ) { self.dependencies = if let Some((dependencies, _)) = analysis::analyze_dependencies( @@ -163,6 +204,8 @@ pub struct ServerState { pub diagnostics: DiagnosticCollection, pub doc_data: HashMap<ModuleSpecifier, DocumentData>, pub file_cache: Arc<RwLock<MemoryCache>>, + pub maybe_import_map: Option<Arc<RwLock<ImportMap>>>, + pub maybe_import_map_uri: Option<Url>, req_queue: ReqQueue, sender: Sender<Message>, pub sources: Arc<RwLock<Sources>>, @@ -189,8 +232,10 @@ impl ServerState { Self { config, diagnostics: Default::default(), - doc_data: HashMap::new(), + doc_data: Default::default(), file_cache: Arc::new(RwLock::new(Default::default())), + maybe_import_map: None, + maybe_import_map_uri: None, req_queue: Default::default(), sender, sources: Arc::new(RwLock::new(sources)), @@ -290,3 +335,57 @@ impl ServerState { self.status = new_status; } } + +#[cfg(test)] +mod tests { + use super::*; + use deno_core::serde_json::json; + use deno_core::serde_json::Value; + use lsp_server::Connection; + use tempfile::TempDir; + + #[test] + fn test_update_import_map() { + let temp_dir = TempDir::new().expect("could not create temp dir"); + let import_map_path = temp_dir.path().join("import_map.json"); + let import_map_str = &import_map_path.to_string_lossy(); + fs::write( + import_map_path.clone(), + r#"{ + "imports": { + "denoland/": "https://deno.land/x/" + } + }"#, + ) + .expect("could not write file"); + let mut config = Config::default(); + config + .update(json!({ + "enable": false, + "config": Value::Null, + "lint": false, + "importMap": import_map_str, + "unstable": true, + })) + .expect("could not update config"); + let (connection, _) = Connection::memory(); + let mut state = ServerState::new(connection.sender, config); + let result = update_import_map(&mut state); + assert!(result.is_ok()); + assert!(state.maybe_import_map.is_some()); + let expected = + Url::from_file_path(import_map_path).expect("could not parse url"); + assert_eq!(state.maybe_import_map_uri, Some(expected)); + let import_map = state.maybe_import_map.unwrap(); + let import_map = import_map.read().unwrap(); + assert_eq!( + import_map + .resolve("denoland/mod.ts", "https://example.com/index.js") + .expect("bad response"), + Some( + ModuleSpecifier::resolve_url("https://deno.land/x/mod.ts") + .expect("could not create URL") + ) + ); + } +} diff --git a/cli/lsp/tsc.rs b/cli/lsp/tsc.rs index 86c9a2980..649dd1bb5 100644 --- a/cli/lsp/tsc.rs +++ b/cli/lsp/tsc.rs @@ -844,17 +844,26 @@ fn resolve(state: &mut State, args: Value) -> Result<Value, AnyError> { }; if let ResolvedImport::Resolved(resolved_specifier) = resolved_import { - let media_type = if let Some(media_type) = - sources.get_media_type(&resolved_specifier) + if state + .server_state + .doc_data + .contains_key(&resolved_specifier) + || sources.contains(&resolved_specifier) { - media_type + let media_type = if let Some(media_type) = + sources.get_media_type(&resolved_specifier) + { + media_type + } else { + MediaType::from(&resolved_specifier) + }; + resolved.push(Some(( + resolved_specifier.to_string(), + media_type.as_ts_extension(), + ))); } else { - MediaType::from(&resolved_specifier) - }; - resolved.push(Some(( - resolved_specifier.to_string(), - media_type.as_ts_extension(), - ))); + resolved.push(None); + } } else { resolved.push(None); } |