1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2025-01-21 21:50:00 -05:00

feat(lsp): add deno cache code actions (#9471)

This commit is contained in:
Kitson Kelly 2021-02-12 15:17:48 +11:00 committed by GitHub
parent 46da7c6aff
commit d6c05b09dd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 488 additions and 162 deletions

View file

@ -15,6 +15,7 @@ use deno_core::error::custom_error;
use deno_core::error::AnyError; use deno_core::error::AnyError;
use deno_core::serde::Deserialize; use deno_core::serde::Deserialize;
use deno_core::serde::Serialize; use deno_core::serde::Serialize;
use deno_core::serde_json;
use deno_core::serde_json::json; use deno_core::serde_json::json;
use deno_core::ModuleResolutionError; use deno_core::ModuleResolutionError;
use deno_core::ModuleSpecifier; use deno_core::ModuleSpecifier;
@ -152,6 +153,23 @@ pub enum ResolvedDependencyErr {
Missing, Missing,
} }
impl ResolvedDependencyErr {
pub fn as_code(&self) -> lsp::NumberOrString {
match self {
Self::InvalidDowngrade => {
lsp::NumberOrString::String("invalid-downgrade".to_string())
}
Self::InvalidLocalImport => {
lsp::NumberOrString::String("invalid-local-import".to_string())
}
Self::InvalidSpecifier(_) => {
lsp::NumberOrString::String("invalid-specifier".to_string())
}
Self::Missing => lsp::NumberOrString::String("missing".to_string()),
}
}
}
impl fmt::Display for ResolvedDependencyErr { impl fmt::Display for ResolvedDependencyErr {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self { match self {
@ -351,30 +369,34 @@ fn is_equivalent_code(
/// action for a given set of actions. /// action for a given set of actions.
fn is_preferred( fn is_preferred(
action: &tsc::CodeFixAction, action: &tsc::CodeFixAction,
actions: &[(lsp::CodeAction, tsc::CodeFixAction)], actions: &[CodeActionKind],
fix_priority: u32, fix_priority: u32,
only_one: bool, only_one: bool,
) -> bool { ) -> bool {
actions.iter().all(|(_, a)| { actions.iter().all(|i| {
if action == a { if let CodeActionKind::Tsc(_, a) = i {
return true; if action == a {
} return true;
if a.fix_id.is_some() {
return true;
}
if let Some((other_fix_priority, _)) =
PREFERRED_FIXES.get(a.fix_name.as_str())
{
match other_fix_priority.cmp(&fix_priority) {
Ordering::Less => return true,
Ordering::Greater => return false,
Ordering::Equal => (),
} }
if only_one && action.fix_name == a.fix_name { if a.fix_id.is_some() {
return false; return true;
} }
if let Some((other_fix_priority, _)) =
PREFERRED_FIXES.get(a.fix_name.as_str())
{
match other_fix_priority.cmp(&fix_priority) {
Ordering::Less => return true,
Ordering::Greater => return false,
Ordering::Equal => (),
}
if only_one && action.fix_name == a.fix_name {
return false;
}
}
true
} else {
true
} }
true
}) })
} }
@ -404,13 +426,58 @@ pub struct CodeActionData {
pub fix_id: String, pub fix_id: String,
} }
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DenoFixData {
pub specifier: ModuleSpecifier,
}
#[derive(Debug, Clone)]
enum CodeActionKind {
Deno(lsp::CodeAction),
Tsc(lsp::CodeAction, tsc::CodeFixAction),
}
#[derive(Debug, Hash, PartialEq, Eq)]
enum FixAllKind {
Tsc(String),
}
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct CodeActionCollection { pub struct CodeActionCollection {
actions: Vec<(lsp::CodeAction, tsc::CodeFixAction)>, actions: Vec<CodeActionKind>,
fix_all_actions: HashMap<String, (lsp::CodeAction, tsc::CodeFixAction)>, fix_all_actions: HashMap<FixAllKind, CodeActionKind>,
} }
impl CodeActionCollection { impl CodeActionCollection {
pub(crate) fn add_deno_fix_action(
&mut self,
diagnostic: &lsp::Diagnostic,
) -> Result<(), AnyError> {
if let Some(data) = diagnostic.data.clone() {
let fix_data: DenoFixData = serde_json::from_value(data)?;
let code_action = lsp::CodeAction {
title: format!(
"Cache \"{}\" and its dependencies.",
fix_data.specifier
),
kind: Some(lsp::CodeActionKind::QUICKFIX),
diagnostics: Some(vec![diagnostic.clone()]),
edit: None,
command: Some(lsp::Command {
title: "".to_string(),
command: "deno.cache".to_string(),
arguments: Some(vec![json!([fix_data.specifier])]),
}),
is_preferred: None,
disabled: None,
data: None,
};
self.actions.push(CodeActionKind::Deno(code_action));
}
Ok(())
}
/// Add a TypeScript code fix action to the code actions collection. /// Add a TypeScript code fix action to the code actions collection.
pub(crate) async fn add_ts_fix_action( pub(crate) async fn add_ts_fix_action(
&mut self, &mut self,
@ -442,19 +509,28 @@ impl CodeActionCollection {
disabled: None, disabled: None,
data: None, data: None,
}; };
self.actions.retain(|(c, a)| { self.actions.retain(|i| match i {
!(action.fix_name == a.fix_name && code_action.edit == c.edit) CodeActionKind::Tsc(c, a) => {
!(action.fix_name == a.fix_name && code_action.edit == c.edit)
}
_ => true,
}); });
self.actions.push((code_action, action.clone())); self
.actions
.push(CodeActionKind::Tsc(code_action, action.clone()));
if let Some(fix_id) = &action.fix_id { if let Some(fix_id) = &action.fix_id {
if let Some((existing_fix_all, existing_action)) = if let Some(CodeActionKind::Tsc(existing_fix_all, existing_action)) =
self.fix_all_actions.get(fix_id) self.fix_all_actions.get(&FixAllKind::Tsc(fix_id.clone()))
{ {
self.actions.retain(|(c, _)| c != existing_fix_all); self.actions.retain(|i| match i {
self CodeActionKind::Tsc(c, _) => c != existing_fix_all,
.actions _ => true,
.push((existing_fix_all.clone(), existing_action.clone())); });
self.actions.push(CodeActionKind::Tsc(
existing_fix_all.clone(),
existing_action.clone(),
));
} }
} }
Ok(()) Ok(())
@ -488,15 +564,21 @@ impl CodeActionCollection {
disabled: None, disabled: None,
data, data,
}; };
if let Some((existing, _)) = if let Some(CodeActionKind::Tsc(existing, _)) = self
self.fix_all_actions.get(&action.fix_id.clone().unwrap()) .fix_all_actions
.get(&FixAllKind::Tsc(action.fix_id.clone().unwrap()))
{ {
self.actions.retain(|(c, _)| c != existing); self.actions.retain(|i| match i {
CodeActionKind::Tsc(c, _) => c != existing,
_ => true,
});
} }
self.actions.push((code_action.clone(), action.clone())); self
.actions
.push(CodeActionKind::Tsc(code_action.clone(), action.clone()));
self.fix_all_actions.insert( self.fix_all_actions.insert(
action.fix_id.clone().unwrap(), FixAllKind::Tsc(action.fix_id.clone().unwrap()),
(code_action, action.clone()), CodeActionKind::Tsc(code_action, action.clone()),
); );
} }
@ -505,7 +587,10 @@ impl CodeActionCollection {
self self
.actions .actions
.into_iter() .into_iter()
.map(|(c, _)| lsp::CodeActionOrCommand::CodeAction(c)) .map(|i| match i {
CodeActionKind::Tsc(c, _) => lsp::CodeActionOrCommand::CodeAction(c),
CodeActionKind::Deno(c) => lsp::CodeActionOrCommand::CodeAction(c),
})
.collect() .collect()
} }
@ -521,7 +606,7 @@ impl CodeActionCollection {
if action.fix_id.is_none() if action.fix_id.is_none()
|| self || self
.fix_all_actions .fix_all_actions
.contains_key(&action.fix_id.clone().unwrap()) .contains_key(&FixAllKind::Tsc(action.fix_id.clone().unwrap()))
{ {
false false
} else { } else {
@ -543,15 +628,17 @@ impl CodeActionCollection {
/// when all actions are added to the collection. /// when all actions are added to the collection.
pub fn set_preferred_fixes(&mut self) { pub fn set_preferred_fixes(&mut self) {
let actions = self.actions.clone(); let actions = self.actions.clone();
for (code_action, action) in self.actions.iter_mut() { for entry in self.actions.iter_mut() {
if action.fix_id.is_some() { if let CodeActionKind::Tsc(code_action, action) = entry {
continue; if action.fix_id.is_some() {
} continue;
if let Some((fix_priority, only_one)) = }
PREFERRED_FIXES.get(action.fix_name.as_str()) if let Some((fix_priority, only_one)) =
{ PREFERRED_FIXES.get(action.fix_name.as_str())
code_action.is_preferred = {
Some(is_preferred(action, &actions, *fix_priority, *only_one)); code_action.is_preferred =
Some(is_preferred(action, &actions, *fix_priority, *only_one));
}
} }
} }
} }

View file

@ -11,6 +11,7 @@ use crate::media_type::MediaType;
use deno_core::error::AnyError; use deno_core::error::AnyError;
use deno_core::serde_json; use deno_core::serde_json;
use deno_core::serde_json::json;
use deno_core::ModuleSpecifier; use deno_core::ModuleSpecifier;
use lspower::lsp; use lspower::lsp;
use std::collections::HashMap; use std::collections::HashMap;
@ -279,14 +280,14 @@ pub async fn generate_dependency_diagnostics(
&dependency.maybe_code_specifier_range, &dependency.maybe_code_specifier_range,
) { ) {
match code.clone() { match code.clone() {
ResolvedDependency::Err(err) => { ResolvedDependency::Err(dependency_err) => {
diagnostic_list.push(lsp::Diagnostic { diagnostic_list.push(lsp::Diagnostic {
range: *range, range: *range,
severity: Some(lsp::DiagnosticSeverity::Error), severity: Some(lsp::DiagnosticSeverity::Error),
code: None, code: Some(dependency_err.as_code()),
code_description: None, code_description: None,
source: Some("deno".to_string()), source: Some("deno".to_string()),
message: format!("{}", err), message: format!("{}", dependency_err),
related_information: None, related_information: None,
tags: None, tags: None,
data: None, data: None,
@ -295,20 +296,23 @@ pub async fn generate_dependency_diagnostics(
ResolvedDependency::Resolved(specifier) => { ResolvedDependency::Resolved(specifier) => {
if !(state_snapshot.documents.contains(&specifier) || sources.contains(&specifier)) { if !(state_snapshot.documents.contains(&specifier) || sources.contains(&specifier)) {
let is_local = specifier.as_url().scheme() == "file"; let is_local = specifier.as_url().scheme() == "file";
let (code, message) = if is_local {
(Some(lsp::NumberOrString::String("no-local".to_string())), format!("Unable to load a local module: \"{}\".\n Please check the file path.", specifier))
} else {
(Some(lsp::NumberOrString::String("no-cache".to_string())), format!("Unable to load the remote module: \"{}\".", specifier))
};
diagnostic_list.push(lsp::Diagnostic { diagnostic_list.push(lsp::Diagnostic {
range: *range, range: *range,
severity: Some(lsp::DiagnosticSeverity::Error), severity: Some(lsp::DiagnosticSeverity::Error),
code: None, code,
code_description: None, code_description: None,
source: Some("deno".to_string()), source: Some("deno".to_string()),
message: if is_local { message,
format!("Unable to load a local module: \"{}\".\n Please check the file path.", specifier)
} else {
format!("Unable to load the module: \"{}\".\n If the module exists, running `deno cache {}` should resolve this error.", specifier, specifier)
},
related_information: None, related_information: None,
tags: None, tags: None,
data: None, data: Some(json!({
"specifier": specifier
})),
}) })
} }
}, },

View file

@ -65,6 +65,7 @@ pub struct LanguageServer(Arc<tokio::sync::Mutex<Inner>>);
pub struct StateSnapshot { pub struct StateSnapshot {
pub assets: HashMap<ModuleSpecifier, Option<AssetDocument>>, pub assets: HashMap<ModuleSpecifier, Option<AssetDocument>>,
pub documents: DocumentCache, pub documents: DocumentCache,
pub performance: Performance,
pub sources: Sources, pub sources: Sources,
} }
@ -190,7 +191,7 @@ impl Inner {
/// Only searches already cached assets and documents for a line index. If /// Only searches already cached assets and documents for a line index. If
/// the line index cannot be found, `None` is returned. /// the line index cannot be found, `None` is returned.
fn get_line_index_sync( fn get_line_index_sync(
&mut self, &self,
specifier: &ModuleSpecifier, specifier: &ModuleSpecifier,
) -> Option<LineIndex> { ) -> Option<LineIndex> {
let mark = self.performance.mark("get_line_index_sync"); let mark = self.performance.mark("get_line_index_sync");
@ -389,6 +390,7 @@ impl Inner {
StateSnapshot { StateSnapshot {
assets: self.assets.clone(), assets: self.assets.clone(),
documents: self.documents.clone(), documents: self.documents.clone(),
performance: self.performance.clone(),
sources: self.sources.clone(), sources: self.sources.clone(),
} }
} }
@ -514,7 +516,7 @@ impl Inner {
} }
pub(crate) fn document_version( pub(crate) fn document_version(
&mut self, &self,
specifier: ModuleSpecifier, specifier: ModuleSpecifier,
) -> Option<i32> { ) -> Option<i32> {
self.documents.version(&specifier) self.documents.version(&specifier)
@ -851,7 +853,7 @@ impl Inner {
} }
} }
async fn hover(&mut self, params: HoverParams) -> LspResult<Option<Hover>> { async fn hover(&self, params: HoverParams) -> LspResult<Option<Hover>> {
if !self.enabled() { if !self.enabled() {
return Ok(None); return Ok(None);
} }
@ -912,7 +914,10 @@ impl Inner {
} }
_ => false, _ => false,
}, },
// currently only processing `deno-ts` quick fixes "deno" => match &d.code {
Some(NumberOrString::String(code)) => code == "no-cache",
_ => false,
},
_ => false, _ => false,
}, },
None => false, None => false,
@ -923,53 +928,65 @@ impl Inner {
return Ok(None); return Ok(None);
} }
let line_index = self.get_line_index_sync(&specifier).unwrap(); let line_index = self.get_line_index_sync(&specifier).unwrap();
let mut code_actions = CodeActionCollection::default();
let file_diagnostics: Vec<Diagnostic> = self let file_diagnostics: Vec<Diagnostic> = self
.diagnostics .diagnostics
.diagnostics_for(&specifier, &DiagnosticSource::TypeScript) .diagnostics_for(&specifier, &DiagnosticSource::TypeScript)
.cloned() .cloned()
.collect(); .collect();
let mut code_actions = CodeActionCollection::default();
for diagnostic in &fixable_diagnostics { for diagnostic in &fixable_diagnostics {
let code = match &diagnostic.code.clone().unwrap() { match diagnostic.source.as_deref() {
NumberOrString::String(code) => code.to_string(), Some("deno-ts") => {
NumberOrString::Number(code) => code.to_string(), let code = match diagnostic.code.as_ref().unwrap() {
}; NumberOrString::String(code) => code.to_string(),
let codes = vec![code]; NumberOrString::Number(code) => code.to_string(),
let req = tsc::RequestMethod::GetCodeFixes(( };
specifier.clone(), let codes = vec![code];
line_index.offset_tsc(diagnostic.range.start)?, let req = tsc::RequestMethod::GetCodeFixes((
line_index.offset_tsc(diagnostic.range.end)?, specifier.clone(),
codes, line_index.offset_tsc(diagnostic.range.start)?,
)); line_index.offset_tsc(diagnostic.range.end)?,
let res = codes,
self ));
.ts_server let res =
.request(self.snapshot(), req) self.ts_server.request(self.snapshot(), req).await.map_err(
.await |err| {
.map_err(|err| { error!("Error getting actions from TypeScript: {}", err);
error!("Error getting actions from TypeScript: {}", err); LspError::internal_error()
LspError::internal_error() },
})?; )?;
let actions: Vec<tsc::CodeFixAction> = let actions: Vec<tsc::CodeFixAction> =
from_value(res).map_err(|err| { from_value(res).map_err(|err| {
error!("Cannot decode actions from TypeScript: {}", err); error!("Cannot decode actions from TypeScript: {}", err);
LspError::internal_error() LspError::internal_error()
})?; })?;
for action in actions { for action in actions {
code_actions code_actions
.add_ts_fix_action(&action, diagnostic, self) .add_ts_fix_action(&action, diagnostic, self)
.await .await
.map_err(|err| { .map_err(|err| {
error!("Unable to convert fix: {}", err); error!("Unable to convert fix: {}", err);
LspError::internal_error() LspError::internal_error()
})?; })?;
if code_actions.is_fix_all_action( if code_actions.is_fix_all_action(
&action, &action,
diagnostic, diagnostic,
&file_diagnostics, &file_diagnostics,
) { ) {
code_actions.add_ts_fix_all_action(&action, &specifier, diagnostic); code_actions
.add_ts_fix_all_action(&action, &specifier, diagnostic);
}
}
} }
Some("deno") => {
code_actions
.add_deno_fix_action(diagnostic)
.map_err(|err| {
error!("{}", err);
LspError::internal_error()
})?
}
_ => (),
} }
} }
code_actions.set_preferred_fixes(); code_actions.set_preferred_fixes();
@ -1020,9 +1037,8 @@ impl Inner {
Ok(code_action) Ok(code_action)
} }
} else { } else {
Err(LspError::invalid_params( // The code action doesn't need to be resolved
"The CodeAction's data is missing.", Ok(params)
))
}; };
self.performance.measure(mark); self.performance.measure(mark);
result result
@ -1343,7 +1359,7 @@ impl Inner {
} }
async fn document_highlight( async fn document_highlight(
&mut self, &self,
params: DocumentHighlightParams, params: DocumentHighlightParams,
) -> LspResult<Option<Vec<DocumentHighlight>>> { ) -> LspResult<Option<Vec<DocumentHighlight>>> {
if !self.enabled() { if !self.enabled() {
@ -1481,7 +1497,7 @@ impl Inner {
} }
async fn completion( async fn completion(
&mut self, &self,
params: CompletionParams, params: CompletionParams,
) -> LspResult<Option<CompletionResponse>> { ) -> LspResult<Option<CompletionResponse>> {
if !self.enabled() { if !self.enabled() {
@ -1830,7 +1846,12 @@ impl lspower::LanguageServer for LanguageServer {
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct CacheParams { struct CacheParams {
text_document: TextDocumentIdentifier, /// The document currently open in the editor. If there are no `uris`
/// supplied, the referrer will be cached.
referrer: TextDocumentIdentifier,
/// Any documents that have been specifically asked to be cached via the
/// command.
uris: Vec<TextDocumentIdentifier>,
} }
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
@ -1841,19 +1862,38 @@ struct VirtualTextDocumentParams {
// These are implementations of custom commands supported by the LSP // These are implementations of custom commands supported by the LSP
impl Inner { impl Inner {
/// Similar to `deno cache` on the command line, where modules will be cached
/// in the Deno cache, including any of their dependencies.
async fn cache(&mut self, params: CacheParams) -> LspResult<bool> { async fn cache(&mut self, params: CacheParams) -> LspResult<bool> {
let mark = self.performance.mark("cache"); let mark = self.performance.mark("cache");
let specifier = utils::normalize_url(params.text_document.uri); let referrer = utils::normalize_url(params.referrer.uri);
let maybe_import_map = self.maybe_import_map.clone(); if !params.uris.is_empty() {
sources::cache(specifier.clone(), maybe_import_map) for identifier in &params.uris {
.await let specifier = utils::normalize_url(identifier.uri.clone());
.map_err(|err| { sources::cache(&specifier, &self.maybe_import_map)
error!("{}", err); .await
LspError::internal_error() .map_err(|err| {
})?; error!("{}", err);
if self.documents.contains(&specifier) { LspError::internal_error()
self.diagnostics.invalidate(&specifier); })?;
}
} else {
sources::cache(&referrer, &self.maybe_import_map)
.await
.map_err(|err| {
error!("{}", err);
LspError::internal_error()
})?;
} }
// now that we have dependencies loaded, we need to re-analyze them and
// invalidate some diagnostics
if self.documents.contains(&referrer) {
if let Some(source) = self.documents.content(&referrer).unwrap() {
self.analyze_dependencies(&referrer, &source);
}
self.diagnostics.invalidate(&referrer);
}
self.prepare_diagnostics().await.map_err(|err| { self.prepare_diagnostics().await.map_err(|err| {
error!("{}", err); error!("{}", err);
LspError::internal_error() LspError::internal_error()
@ -2685,6 +2725,28 @@ mod tests {
harness.run().await; harness.run().await;
} }
#[tokio::test]
async fn test_code_actions_deno_cache() {
let mut harness = LspTestHarness::new(vec![
("initialize_request.json", LspResponse::RequestAny),
("initialized_notification.json", LspResponse::None),
("did_open_notification_cache.json", LspResponse::None),
(
"code_action_request_cache.json",
LspResponse::RequestFixture(
2,
"code_action_response_cache.json".to_string(),
),
),
(
"shutdown_request.json",
LspResponse::Request(3, json!(null)),
),
("exit_notification.json", LspResponse::None),
]);
harness.run().await;
}
#[derive(Deserialize)] #[derive(Deserialize)]
struct PerformanceAverages { struct PerformanceAverages {
averages: Vec<PerformanceAverage>, averages: Vec<PerformanceAverage>,
@ -2730,7 +2792,7 @@ mod tests {
LspResponse::RequestAssert(|value| { LspResponse::RequestAssert(|value| {
let resp: PerformanceResponse = let resp: PerformanceResponse =
serde_json::from_value(value).unwrap(); serde_json::from_value(value).unwrap();
assert_eq!(resp.result.averages.len(), 10); assert_eq!(resp.result.averages.len(), 12);
}), }),
), ),
( (

View file

@ -49,7 +49,7 @@ impl From<PerformanceMark> for PerformanceMeasure {
/// ///
/// The structure will limit the size of measurements to the most recent 1000, /// The structure will limit the size of measurements to the most recent 1000,
/// and will roll off when that limit is reached. /// and will roll off when that limit is reached.
#[derive(Debug)] #[derive(Debug, Clone)]
pub struct Performance { pub struct Performance {
counts: Arc<Mutex<HashMap<String, u32>>>, counts: Arc<Mutex<HashMap<String, u32>>>,
max_size: usize, max_size: usize,
@ -127,13 +127,15 @@ impl Performance {
/// A function which accepts a previously created performance mark which will /// A function which accepts a previously created performance mark which will
/// be used to finalize the duration of the span being measured, and add the /// be used to finalize the duration of the span being measured, and add the
/// measurement to the internal buffer. /// measurement to the internal buffer.
pub fn measure(&self, mark: PerformanceMark) { pub fn measure(&self, mark: PerformanceMark) -> Duration {
let measure = PerformanceMeasure::from(mark); let measure = PerformanceMeasure::from(mark);
let duration = measure.duration;
let mut measures = self.measures.lock().unwrap(); let mut measures = self.measures.lock().unwrap();
measures.push_back(measure); measures.push_back(measure);
while measures.len() > self.max_size { while measures.len() > self.max_size {
measures.pop_front(); measures.pop_front();
} }
duration
} }
} }

View file

@ -28,16 +28,16 @@ use std::sync::Mutex;
use std::time::SystemTime; use std::time::SystemTime;
pub async fn cache( pub async fn cache(
specifier: ModuleSpecifier, specifier: &ModuleSpecifier,
maybe_import_map: Option<ImportMap>, maybe_import_map: &Option<ImportMap>,
) -> Result<(), AnyError> { ) -> Result<(), AnyError> {
let program_state = Arc::new(ProgramState::new(Default::default())?); let program_state = Arc::new(ProgramState::new(Default::default())?);
let handler = Arc::new(Mutex::new(FetchHandler::new( let handler = Arc::new(Mutex::new(FetchHandler::new(
&program_state, &program_state,
Permissions::allow_all(), Permissions::allow_all(),
)?)); )?));
let mut builder = GraphBuilder::new(handler, maybe_import_map, None); let mut builder = GraphBuilder::new(handler, maybe_import_map.clone(), None);
builder.add(&specifier, false).await builder.add(specifier, false).await
} }
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
@ -51,7 +51,7 @@ struct Metadata {
} }
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct Sources { struct Inner {
http_cache: HttpCache, http_cache: HttpCache,
maybe_import_map: Option<ImportMap>, maybe_import_map: Option<ImportMap>,
metadata: HashMap<ModuleSpecifier, Metadata>, metadata: HashMap<ModuleSpecifier, Metadata>,
@ -59,15 +59,80 @@ pub struct Sources {
remotes: HashMap<ModuleSpecifier, PathBuf>, remotes: HashMap<ModuleSpecifier, PathBuf>,
} }
#[derive(Debug, Clone, Default)]
pub struct Sources(Arc<Mutex<Inner>>);
impl Sources { impl Sources {
pub fn new(location: &Path) -> Self { pub fn new(location: &Path) -> Self {
Self(Arc::new(Mutex::new(Inner::new(location))))
}
pub fn contains(&self, specifier: &ModuleSpecifier) -> bool {
self.0.lock().unwrap().contains(specifier)
}
pub fn get_length_utf16(&self, specifier: &ModuleSpecifier) -> Option<usize> {
self.0.lock().unwrap().get_length_utf16(specifier)
}
pub fn get_line_index(
&self,
specifier: &ModuleSpecifier,
) -> Option<LineIndex> {
self.0.lock().unwrap().get_line_index(specifier)
}
pub fn get_maybe_types(
&self,
specifier: &ModuleSpecifier,
) -> Option<analysis::ResolvedDependency> {
self.0.lock().unwrap().get_maybe_types(specifier)
}
pub fn get_media_type(
&self,
specifier: &ModuleSpecifier,
) -> Option<MediaType> {
self.0.lock().unwrap().get_media_type(specifier)
}
pub fn get_script_version(
&self,
specifier: &ModuleSpecifier,
) -> Option<String> {
self.0.lock().unwrap().get_script_version(specifier)
}
pub fn get_text(&self, specifier: &ModuleSpecifier) -> Option<String> {
self.0.lock().unwrap().get_text(specifier)
}
pub fn resolve_import(
&self,
specifier: &str,
referrer: &ModuleSpecifier,
) -> Option<(ModuleSpecifier, MediaType)> {
self.0.lock().unwrap().resolve_import(specifier, referrer)
}
#[cfg(test)]
fn resolve_specifier(
&self,
specifier: &ModuleSpecifier,
) -> Option<ModuleSpecifier> {
self.0.lock().unwrap().resolve_specifier(specifier)
}
}
impl Inner {
fn new(location: &Path) -> Self {
Self { Self {
http_cache: HttpCache::new(location), http_cache: HttpCache::new(location),
..Default::default() ..Default::default()
} }
} }
pub fn contains(&mut self, specifier: &ModuleSpecifier) -> bool { fn contains(&mut self, specifier: &ModuleSpecifier) -> bool {
if let Some(specifier) = self.resolve_specifier(specifier) { if let Some(specifier) = self.resolve_specifier(specifier) {
if self.get_metadata(&specifier).is_some() { if self.get_metadata(&specifier).is_some() {
return true; return true;
@ -80,16 +145,13 @@ impl Sources {
/// match the behavior of JavaScript, where strings are stored effectively as /// match the behavior of JavaScript, where strings are stored effectively as
/// `&[u16]` and when counting "chars" we need to represent the string as a /// `&[u16]` and when counting "chars" we need to represent the string as a
/// UTF-16 string in Rust. /// UTF-16 string in Rust.
pub fn get_length_utf16( fn get_length_utf16(&mut self, specifier: &ModuleSpecifier) -> Option<usize> {
&mut self,
specifier: &ModuleSpecifier,
) -> Option<usize> {
let specifier = self.resolve_specifier(specifier)?; let specifier = self.resolve_specifier(specifier)?;
let metadata = self.get_metadata(&specifier)?; let metadata = self.get_metadata(&specifier)?;
Some(metadata.source.encode_utf16().count()) Some(metadata.source.encode_utf16().count())
} }
pub fn get_line_index( fn get_line_index(
&mut self, &mut self,
specifier: &ModuleSpecifier, specifier: &ModuleSpecifier,
) -> Option<LineIndex> { ) -> Option<LineIndex> {
@ -98,7 +160,7 @@ impl Sources {
Some(metadata.line_index) Some(metadata.line_index)
} }
pub fn get_maybe_types( fn get_maybe_types(
&mut self, &mut self,
specifier: &ModuleSpecifier, specifier: &ModuleSpecifier,
) -> Option<analysis::ResolvedDependency> { ) -> Option<analysis::ResolvedDependency> {
@ -106,7 +168,7 @@ impl Sources {
metadata.maybe_types metadata.maybe_types
} }
pub fn get_media_type( fn get_media_type(
&mut self, &mut self,
specifier: &ModuleSpecifier, specifier: &ModuleSpecifier,
) -> Option<MediaType> { ) -> Option<MediaType> {
@ -117,12 +179,11 @@ impl Sources {
fn get_metadata(&mut self, specifier: &ModuleSpecifier) -> Option<Metadata> { fn get_metadata(&mut self, specifier: &ModuleSpecifier) -> Option<Metadata> {
if let Some(metadata) = self.metadata.get(specifier).cloned() { if let Some(metadata) = self.metadata.get(specifier).cloned() {
if let Some(current_version) = self.get_script_version(specifier) { if metadata.version == self.get_script_version(specifier)? {
if metadata.version == current_version { return Some(metadata);
return Some(metadata);
}
} }
} }
// TODO(@kitsonk) this needs to be refactored, lots of duplicate logic and // TODO(@kitsonk) this needs to be refactored, lots of duplicate logic and
// is really difficult to follow. // is really difficult to follow.
let version = self.get_script_version(specifier)?; let version = self.get_script_version(specifier)?;
@ -248,28 +309,24 @@ impl Sources {
None None
} }
pub fn get_script_version( fn get_script_version(
&mut self, &mut self,
specifier: &ModuleSpecifier, specifier: &ModuleSpecifier,
) -> Option<String> { ) -> Option<String> {
if let Some(path) = self.get_path(specifier) { let path = self.get_path(specifier)?;
if let Ok(metadata) = fs::metadata(path) { let metadata = fs::metadata(path).ok()?;
if let Ok(modified) = metadata.modified() { if let Ok(modified) = metadata.modified() {
return if let Ok(n) = modified.duration_since(SystemTime::UNIX_EPOCH) if let Ok(n) = modified.duration_since(SystemTime::UNIX_EPOCH) {
{ Some(format!("{}", n.as_millis()))
Some(format!("{}", n.as_millis())) } else {
} else { Some("1".to_string())
Some("1".to_string())
};
} else {
return Some("1".to_string());
}
} }
} else {
Some("1".to_string())
} }
None
} }
pub fn get_text(&mut self, specifier: &ModuleSpecifier) -> Option<String> { fn get_text(&mut self, specifier: &ModuleSpecifier) -> Option<String> {
let specifier = self.resolve_specifier(specifier)?; let specifier = self.resolve_specifier(specifier)?;
let metadata = self.get_metadata(&specifier)?; let metadata = self.get_metadata(&specifier)?;
Some(metadata.source) Some(metadata.source)
@ -289,7 +346,7 @@ impl Sources {
Some((resolved_specifier, media_type)) Some((resolved_specifier, media_type))
} }
pub fn resolve_import( fn resolve_import(
&mut self, &mut self,
specifier: &str, specifier: &str,
referrer: &ModuleSpecifier, referrer: &ModuleSpecifier,
@ -385,7 +442,7 @@ mod tests {
#[test] #[test]
fn test_sources_get_script_version() { fn test_sources_get_script_version() {
let (mut sources, _) = setup(); let (sources, _) = setup();
let c = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()); let c = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
let tests = c.join("tests"); let tests = c.join("tests");
let specifier = ModuleSpecifier::resolve_path( let specifier = ModuleSpecifier::resolve_path(
@ -398,7 +455,7 @@ mod tests {
#[test] #[test]
fn test_sources_get_text() { fn test_sources_get_text() {
let (mut sources, _) = setup(); let (sources, _) = setup();
let c = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()); let c = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
let tests = c.join("tests"); let tests = c.join("tests");
let specifier = ModuleSpecifier::resolve_path( let specifier = ModuleSpecifier::resolve_path(
@ -413,7 +470,7 @@ mod tests {
#[test] #[test]
fn test_sources_get_length_utf16() { fn test_sources_get_length_utf16() {
let (mut sources, _) = setup(); let (sources, _) = setup();
let c = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()); let c = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
let tests = c.join("tests"); let tests = c.join("tests");
let specifier = ModuleSpecifier::resolve_path( let specifier = ModuleSpecifier::resolve_path(
@ -428,7 +485,7 @@ mod tests {
#[test] #[test]
fn test_sources_resolve_specifier_non_supported_schema() { fn test_sources_resolve_specifier_non_supported_schema() {
let (mut sources, _) = setup(); let (sources, _) = setup();
let specifier = ModuleSpecifier::resolve_url("foo://a/b/c.ts") let specifier = ModuleSpecifier::resolve_url("foo://a/b/c.ts")
.expect("could not create specifier"); .expect("could not create specifier");
let actual = sources.resolve_specifier(&specifier); let actual = sources.resolve_specifier(&specifier);

View file

@ -978,10 +978,12 @@ struct SourceSnapshotArgs {
/// The language service is dropping a reference to a source file snapshot, and /// The language service is dropping a reference to a source file snapshot, and
/// we can drop our version of that document. /// we can drop our version of that document.
fn dispose(state: &mut State, args: Value) -> Result<Value, AnyError> { fn dispose(state: &mut State, args: Value) -> Result<Value, AnyError> {
let mark = state.state_snapshot.performance.mark("op_dispose");
let v: SourceSnapshotArgs = serde_json::from_value(args)?; let v: SourceSnapshotArgs = serde_json::from_value(args)?;
state state
.snapshots .snapshots
.remove(&(v.specifier.into(), v.version.into())); .remove(&(v.specifier.into(), v.version.into()));
state.state_snapshot.performance.measure(mark);
Ok(json!(true)) Ok(json!(true))
} }
@ -997,6 +999,7 @@ struct GetChangeRangeArgs {
/// The language service wants to compare an old snapshot with a new snapshot to /// The language service wants to compare an old snapshot with a new snapshot to
/// determine what source hash changed. /// determine what source hash changed.
fn get_change_range(state: &mut State, args: Value) -> Result<Value, AnyError> { fn get_change_range(state: &mut State, args: Value) -> Result<Value, AnyError> {
let mark = state.state_snapshot.performance.mark("op_get_change_range");
let v: GetChangeRangeArgs = serde_json::from_value(args.clone())?; let v: GetChangeRangeArgs = serde_json::from_value(args.clone())?;
cache_snapshot(state, v.specifier.clone(), v.version.clone())?; cache_snapshot(state, v.specifier.clone(), v.version.clone())?;
if let Some(current) = state if let Some(current) = state
@ -1007,8 +1010,11 @@ fn get_change_range(state: &mut State, args: Value) -> Result<Value, AnyError> {
.snapshots .snapshots
.get(&(v.specifier.clone().into(), v.old_version.clone().into())) .get(&(v.specifier.clone().into(), v.old_version.clone().into()))
{ {
state.state_snapshot.performance.measure(mark);
Ok(text::get_range_change(prev, current)) Ok(text::get_range_change(prev, current))
} else { } else {
let new_length = current.encode_utf16().count();
state.state_snapshot.performance.measure(mark);
// when a local file is opened up in the editor, the compiler might // when a local file is opened up in the editor, the compiler might
// already have a snapshot of it in memory, and will request it, but we // already have a snapshot of it in memory, and will request it, but we
// now are working off in memory versions of the document, and so need // now are working off in memory versions of the document, and so need
@ -1018,10 +1024,11 @@ fn get_change_range(state: &mut State, args: Value) -> Result<Value, AnyError> {
"start": 0, "start": 0,
"length": v.old_length, "length": v.old_length,
}, },
"newLength": current.encode_utf16().count(), "newLength": new_length,
})) }))
} }
} else { } else {
state.state_snapshot.performance.measure(mark);
Err(custom_error( Err(custom_error(
"MissingSnapshot", "MissingSnapshot",
format!( format!(
@ -1033,6 +1040,7 @@ fn get_change_range(state: &mut State, args: Value) -> Result<Value, AnyError> {
} }
fn get_length(state: &mut State, args: Value) -> Result<Value, AnyError> { fn get_length(state: &mut State, args: Value) -> Result<Value, AnyError> {
let mark = state.state_snapshot.performance.mark("op_get_length");
let v: SourceSnapshotArgs = serde_json::from_value(args)?; let v: SourceSnapshotArgs = serde_json::from_value(args)?;
let specifier = ModuleSpecifier::resolve_url(&v.specifier)?; let specifier = ModuleSpecifier::resolve_url(&v.specifier)?;
if state.state_snapshot.documents.contains(&specifier) { if state.state_snapshot.documents.contains(&specifier) {
@ -1041,9 +1049,11 @@ fn get_length(state: &mut State, args: Value) -> Result<Value, AnyError> {
.snapshots .snapshots
.get(&(v.specifier.into(), v.version.into())) .get(&(v.specifier.into(), v.version.into()))
.unwrap(); .unwrap();
state.state_snapshot.performance.measure(mark);
Ok(json!(content.encode_utf16().count())) Ok(json!(content.encode_utf16().count()))
} else { } else {
let sources = &mut state.state_snapshot.sources; let sources = &mut state.state_snapshot.sources;
state.state_snapshot.performance.measure(mark);
Ok(json!(sources.get_length_utf16(&specifier).unwrap())) Ok(json!(sources.get_length_utf16(&specifier).unwrap()))
} }
} }
@ -1058,6 +1068,7 @@ struct GetTextArgs {
} }
fn get_text(state: &mut State, args: Value) -> Result<Value, AnyError> { fn get_text(state: &mut State, args: Value) -> Result<Value, AnyError> {
let mark = state.state_snapshot.performance.mark("op_get_text");
let v: GetTextArgs = serde_json::from_value(args)?; let v: GetTextArgs = serde_json::from_value(args)?;
let specifier = ModuleSpecifier::resolve_url(&v.specifier)?; let specifier = ModuleSpecifier::resolve_url(&v.specifier)?;
let content = if state.state_snapshot.documents.contains(&specifier) { let content = if state.state_snapshot.documents.contains(&specifier) {
@ -1071,10 +1082,12 @@ fn get_text(state: &mut State, args: Value) -> Result<Value, AnyError> {
let sources = &mut state.state_snapshot.sources; let sources = &mut state.state_snapshot.sources;
sources.get_text(&specifier).unwrap() sources.get_text(&specifier).unwrap()
}; };
state.state_snapshot.performance.measure(mark);
Ok(json!(text::slice(&content, v.start..v.end))) Ok(json!(text::slice(&content, v.start..v.end)))
} }
fn resolve(state: &mut State, args: Value) -> Result<Value, AnyError> { fn resolve(state: &mut State, args: Value) -> Result<Value, AnyError> {
let mark = state.state_snapshot.performance.mark("op_resolve");
let v: ResolveArgs = serde_json::from_value(args)?; let v: ResolveArgs = serde_json::from_value(args)?;
let mut resolved = Vec::<Option<(String, String)>>::new(); let mut resolved = Vec::<Option<(String, String)>>::new();
let referrer = ModuleSpecifier::resolve_url(&v.base)?; let referrer = ModuleSpecifier::resolve_url(&v.base)?;
@ -1102,9 +1115,13 @@ fn resolve(state: &mut State, args: Value) -> Result<Value, AnyError> {
if let ResolvedDependency::Resolved(resolved_specifier) = if let ResolvedDependency::Resolved(resolved_specifier) =
resolved_import resolved_import
{ {
if state.state_snapshot.documents.contains(&resolved_specifier) if state.state_snapshot.documents.contains(&resolved_specifier) {
|| sources.contains(&resolved_specifier) let media_type = MediaType::from(&resolved_specifier);
{ resolved.push(Some((
resolved_specifier.to_string(),
media_type.as_ts_extension(),
)));
} else if sources.contains(&resolved_specifier) {
let media_type = if let Some(media_type) = let media_type = if let Some(media_type) =
sources.get_media_type(&resolved_specifier) sources.get_media_type(&resolved_specifier)
{ {
@ -1139,6 +1156,7 @@ fn resolve(state: &mut State, args: Value) -> Result<Value, AnyError> {
} }
} }
} else { } else {
state.state_snapshot.performance.measure(mark);
return Err(custom_error( return Err(custom_error(
"NotFound", "NotFound",
format!( format!(
@ -1148,6 +1166,7 @@ fn resolve(state: &mut State, args: Value) -> Result<Value, AnyError> {
)); ));
} }
state.state_snapshot.performance.measure(mark);
Ok(json!(resolved)) Ok(json!(resolved))
} }
@ -1167,6 +1186,7 @@ struct ScriptVersionArgs {
} }
fn script_version(state: &mut State, args: Value) -> Result<Value, AnyError> { fn script_version(state: &mut State, args: Value) -> Result<Value, AnyError> {
let mark = state.state_snapshot.performance.mark("op_script_version");
let v: ScriptVersionArgs = serde_json::from_value(args)?; let v: ScriptVersionArgs = serde_json::from_value(args)?;
let specifier = ModuleSpecifier::resolve_url(&v.specifier)?; let specifier = ModuleSpecifier::resolve_url(&v.specifier)?;
if let Some(version) = state.state_snapshot.documents.version(&specifier) { if let Some(version) = state.state_snapshot.documents.version(&specifier) {
@ -1178,6 +1198,7 @@ fn script_version(state: &mut State, args: Value) -> Result<Value, AnyError> {
} }
} }
state.state_snapshot.performance.measure(mark);
Ok(json!(None::<String>)) Ok(json!(None::<String>))
} }
@ -1480,9 +1501,8 @@ mod tests {
documents.open(specifier, version, content); documents.open(specifier, version, content);
} }
StateSnapshot { StateSnapshot {
assets: Default::default(),
documents, documents,
sources: Default::default(), ..Default::default()
} }
} }

View file

@ -0,0 +1,46 @@
{
"jsonrpc": "2.0",
"id": 2,
"method": "textDocument/codeAction",
"params": {
"textDocument": {
"uri": "file:///a/file.ts"
},
"range": {
"start": {
"line": 0,
"character": 19
},
"end": {
"line": 0,
"character": 49
}
},
"context": {
"diagnostics": [
{
"range": {
"start": {
"line": 0,
"character": 19
},
"end": {
"line": 0,
"character": 49
}
},
"severity": 1,
"code": "no-cache",
"source": "deno",
"message": "Unable to load the remote module: \"https://deno.land/x/a/mod.ts\".",
"data": {
"specifier": "https://deno.land/x/a/mod.ts"
}
}
],
"only": [
"quickfix"
]
}
}
}

View file

@ -0,0 +1,36 @@
[
{
"title": "Cache \"https://deno.land/x/a/mod.ts\" and its dependencies.",
"kind": "quickfix",
"diagnostics": [
{
"range": {
"start": {
"line": 0,
"character": 19
},
"end": {
"line": 0,
"character": 49
}
},
"severity": 1,
"code": "no-cache",
"source": "deno",
"message": "Unable to load the remote module: \"https://deno.land/x/a/mod.ts\".",
"data": {
"specifier": "https://deno.land/x/a/mod.ts"
}
}
],
"command": {
"title": "",
"command": "deno.cache",
"arguments": [
[
"https://deno.land/x/a/mod.ts"
]
]
}
}
]

View file

@ -0,0 +1,12 @@
{
"jsonrpc": "2.0",
"method": "textDocument/didOpen",
"params": {
"textDocument": {
"uri": "file:///a/file.ts",
"languageId": "typescript",
"version": 1,
"text": "import * as a from \"https://deno.land/x/a/mod.ts\";\n\nconsole.log(a);\n"
}
}
}