mirror of
https://github.com/denoland/deno.git
synced 2025-01-21 21:50:00 -05:00
feat(lsp): support import maps (#8683)
This commit is contained in:
parent
b6dd850f71
commit
95a6698cac
6 changed files with 202 additions and 25 deletions
|
@ -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());
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
|
109
cli/lsp/state.rs
109
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")
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -843,6 +843,12 @@ fn resolve(state: &mut State, args: Value) -> Result<Value, AnyError> {
|
|||
ResolvedImport::Err("missing dependency".to_string())
|
||||
};
|
||||
if let ResolvedImport::Resolved(resolved_specifier) = resolved_import
|
||||
{
|
||||
if state
|
||||
.server_state
|
||||
.doc_data
|
||||
.contains_key(&resolved_specifier)
|
||||
|| sources.contains(&resolved_specifier)
|
||||
{
|
||||
let media_type = if let Some(media_type) =
|
||||
sources.get_media_type(&resolved_specifier)
|
||||
|
@ -858,6 +864,9 @@ fn resolve(state: &mut State, args: Value) -> Result<Value, AnyError> {
|
|||
} else {
|
||||
resolved.push(None);
|
||||
}
|
||||
} else {
|
||||
resolved.push(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue