Skip to content

Commit

Permalink
feat(lsp): support import maps (#8683)
Browse files Browse the repository at this point in the history
  • Loading branch information
kitsonk authored Dec 9, 2020
1 parent b6dd850 commit 95a6698
Show file tree
Hide file tree
Showing 6 changed files with 202 additions and 25 deletions.
13 changes: 8 additions & 5 deletions cli/lsp/analysis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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());
Expand Down
2 changes: 2 additions & 0 deletions cli/lsp/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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,
}

Expand Down
66 changes: 61 additions & 5 deletions cli/lsp/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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!({
Expand All @@ -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,
Expand Down Expand Up @@ -259,7 +261,7 @@ impl ServerState {
specifier.clone(),
params.text_document.version,
&params.text_document.text,
None,
state.maybe_import_map.clone(),
),
)
.is_some()
Expand All @@ -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(())
Expand Down Expand Up @@ -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) => {
Expand All @@ -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(())
Expand Down Expand Up @@ -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) {
Expand Down
10 changes: 9 additions & 1 deletion cli/lsp/sources.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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)]
Expand All @@ -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>,
Expand Down Expand Up @@ -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
};
Expand Down
109 changes: 104 additions & 5 deletions cli/lsp/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,25 +18,66 @@ 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;
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;

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),
Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand Down Expand Up @@ -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>>,
Expand All @@ -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)),
Expand Down Expand Up @@ -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")
)
);
}
}
Loading

0 comments on commit 95a6698

Please sign in to comment.