summaryrefslogtreecommitdiff
path: root/cli/lsp/tsc.rs
diff options
context:
space:
mode:
authorhaturau <135221985+haturatu@users.noreply.github.com>2024-11-20 01:20:47 +0900
committerGitHub <noreply@github.com>2024-11-20 01:20:47 +0900
commit85719a67e59c7aa45bead26e4942d7df8b1b42d4 (patch)
treeface0aecaac53e93ce2f23b53c48859bcf1a36ec /cli/lsp/tsc.rs
parent67697bc2e4a62a9670699fd18ad0dd8efc5bd955 (diff)
parent186b52731c6bb326c4d32905c5e732d082e83465 (diff)
Merge branch 'denoland:main' into main
Diffstat (limited to 'cli/lsp/tsc.rs')
-rw-r--r--cli/lsp/tsc.rs261
1 files changed, 211 insertions, 50 deletions
diff --git a/cli/lsp/tsc.rs b/cli/lsp/tsc.rs
index c8b5c47f8..fc7ff5794 100644
--- a/cli/lsp/tsc.rs
+++ b/cli/lsp/tsc.rs
@@ -34,6 +34,7 @@ use crate::util::path::relative_specifier;
use crate::util::path::to_percent_decoded_str;
use crate::util::result::InfallibleResultExt;
use crate::util::v8::convert;
+use crate::worker::create_isolate_create_params;
use deno_core::convert::Smi;
use deno_core::convert::ToV8;
use deno_core::error::StdAnyError;
@@ -69,6 +70,7 @@ use indexmap::IndexMap;
use indexmap::IndexSet;
use lazy_regex::lazy_regex;
use log::error;
+use node_resolver::NodeModuleKind;
use once_cell::sync::Lazy;
use regex::Captures;
use regex::Regex;
@@ -236,7 +238,7 @@ pub struct TsServer {
performance: Arc<Performance>,
sender: mpsc::UnboundedSender<Request>,
receiver: Mutex<Option<mpsc::UnboundedReceiver<Request>>>,
- specifier_map: Arc<TscSpecifierMap>,
+ pub specifier_map: Arc<TscSpecifierMap>,
inspector_server: Mutex<Option<Arc<InspectorServer>>>,
pending_change: Mutex<Option<PendingChange>>,
}
@@ -882,20 +884,22 @@ impl TsServer {
options: GetCompletionsAtPositionOptions,
format_code_settings: FormatCodeSettings,
scope: Option<ModuleSpecifier>,
- ) -> Option<CompletionInfo> {
+ ) -> Result<Option<CompletionInfo>, AnyError> {
let req = TscRequest::GetCompletionsAtPosition(Box::new((
self.specifier_map.denormalize(&specifier),
position,
options,
format_code_settings,
)));
- match self.request(snapshot, req, scope).await {
- Ok(maybe_info) => maybe_info,
- Err(err) => {
- log::error!("Unable to get completion info from TypeScript: {:#}", err);
- None
- }
- }
+ self
+ .request::<Option<CompletionInfo>>(snapshot, req, scope)
+ .await
+ .map(|mut info| {
+ if let Some(info) = &mut info {
+ info.normalize(&self.specifier_map);
+ }
+ info
+ })
}
pub async fn get_completion_details(
@@ -2183,6 +2187,50 @@ impl NavigateToItem {
}
#[derive(Debug, Clone, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct InlayHintDisplayPart {
+ pub text: String,
+ pub span: Option<TextSpan>,
+ pub file: Option<String>,
+}
+
+impl InlayHintDisplayPart {
+ pub fn to_lsp(
+ &self,
+ language_server: &language_server::Inner,
+ ) -> lsp::InlayHintLabelPart {
+ let location = self.file.as_ref().map(|f| {
+ let specifier =
+ resolve_url(f).unwrap_or_else(|_| INVALID_SPECIFIER.clone());
+ let file_referrer =
+ language_server.documents.get_file_referrer(&specifier);
+ let uri = language_server
+ .url_map
+ .specifier_to_uri(&specifier, file_referrer.as_deref())
+ .unwrap_or_else(|_| INVALID_URI.clone());
+ let range = self
+ .span
+ .as_ref()
+ .and_then(|s| {
+ let asset_or_doc =
+ language_server.get_asset_or_document(&specifier).ok()?;
+ Some(s.to_range(asset_or_doc.line_index()))
+ })
+ .unwrap_or_else(|| {
+ lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0))
+ });
+ lsp::Location { uri, range }
+ });
+ lsp::InlayHintLabelPart {
+ value: self.text.clone(),
+ tooltip: None,
+ location,
+ command: None,
+ }
+ }
+}
+
+#[derive(Debug, Clone, Deserialize)]
pub enum InlayHintKind {
Type,
Parameter,
@@ -2203,6 +2251,7 @@ impl InlayHintKind {
#[serde(rename_all = "camelCase")]
pub struct InlayHint {
pub text: String,
+ pub display_parts: Option<Vec<InlayHintDisplayPart>>,
pub position: u32,
pub kind: InlayHintKind,
pub whitespace_before: Option<bool>,
@@ -2210,10 +2259,23 @@ pub struct InlayHint {
}
impl InlayHint {
- pub fn to_lsp(&self, line_index: Arc<LineIndex>) -> lsp::InlayHint {
+ pub fn to_lsp(
+ &self,
+ line_index: Arc<LineIndex>,
+ language_server: &language_server::Inner,
+ ) -> lsp::InlayHint {
lsp::InlayHint {
position: line_index.position_tsc(self.position.into()),
- label: lsp::InlayHintLabel::String(self.text.clone()),
+ label: if let Some(display_parts) = &self.display_parts {
+ lsp::InlayHintLabel::LabelParts(
+ display_parts
+ .iter()
+ .map(|p| p.to_lsp(language_server))
+ .collect(),
+ )
+ } else {
+ lsp::InlayHintLabel::String(self.text.clone())
+ },
kind: self.kind.to_lsp(),
padding_left: self.whitespace_before,
padding_right: self.whitespace_after,
@@ -3355,9 +3417,18 @@ fn parse_code_actions(
additional_text_edits.extend(change.text_changes.iter().map(|tc| {
let mut text_edit = tc.as_text_edit(asset_or_doc.line_index());
if let Some(specifier_rewrite) = &data.specifier_rewrite {
- text_edit.new_text = text_edit
- .new_text
- .replace(&specifier_rewrite.0, &specifier_rewrite.1);
+ text_edit.new_text = text_edit.new_text.replace(
+ &specifier_rewrite.old_specifier,
+ &specifier_rewrite.new_specifier,
+ );
+ if let Some(deno_types_specifier) =
+ &specifier_rewrite.new_deno_types_specifier
+ {
+ text_edit.new_text = format!(
+ "// @deno-types=\"{}\"\n{}",
+ deno_types_specifier, &text_edit.new_text
+ );
+ }
}
text_edit
}));
@@ -3516,17 +3587,23 @@ impl CompletionEntryDetails {
let mut text_edit = original_item.text_edit.clone();
if let Some(specifier_rewrite) = &data.specifier_rewrite {
if let Some(text_edit) = &mut text_edit {
- match text_edit {
- lsp::CompletionTextEdit::Edit(text_edit) => {
- text_edit.new_text = text_edit
- .new_text
- .replace(&specifier_rewrite.0, &specifier_rewrite.1);
- }
+ let new_text = match text_edit {
+ lsp::CompletionTextEdit::Edit(text_edit) => &mut text_edit.new_text,
lsp::CompletionTextEdit::InsertAndReplace(insert_replace_edit) => {
- insert_replace_edit.new_text = insert_replace_edit
- .new_text
- .replace(&specifier_rewrite.0, &specifier_rewrite.1);
+ &mut insert_replace_edit.new_text
}
+ };
+ *new_text = new_text.replace(
+ &specifier_rewrite.old_specifier,
+ &specifier_rewrite.new_specifier,
+ );
+ if let Some(deno_types_specifier) =
+ &specifier_rewrite.new_deno_types_specifier
+ {
+ *new_text = format!(
+ "// @deno-types=\"{}\"\n{}",
+ deno_types_specifier, new_text
+ );
}
}
}
@@ -3584,6 +3661,12 @@ pub struct CompletionInfo {
}
impl CompletionInfo {
+ fn normalize(&mut self, specifier_map: &TscSpecifierMap) {
+ for entry in &mut self.entries {
+ entry.normalize(specifier_map);
+ }
+ }
+
pub fn as_completion_response(
&self,
line_index: Arc<LineIndex>,
@@ -3625,6 +3708,13 @@ impl CompletionInfo {
}
}
+#[derive(Debug, Clone, Deserialize, Serialize)]
+pub struct CompletionSpecifierRewrite {
+ old_specifier: String,
+ new_specifier: String,
+ new_deno_types_specifier: Option<String>,
+}
+
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CompletionItemData {
@@ -3637,7 +3727,7 @@ pub struct CompletionItemData {
/// be rewritten by replacing the first string with the second. Intended for
/// auto-import specifiers to be reverse-import-mapped.
#[serde(skip_serializing_if = "Option::is_none")]
- pub specifier_rewrite: Option<(String, String)>,
+ pub specifier_rewrite: Option<CompletionSpecifierRewrite>,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<Value>,
pub use_code_snippet: bool,
@@ -3645,11 +3735,17 @@ pub struct CompletionItemData {
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
-struct CompletionEntryDataImport {
+struct CompletionEntryDataAutoImport {
module_specifier: String,
file_name: String,
}
+#[derive(Debug)]
+pub struct CompletionNormalizedAutoImportData {
+ raw: CompletionEntryDataAutoImport,
+ normalized: ModuleSpecifier,
+}
+
#[derive(Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CompletionEntry {
@@ -3682,9 +3778,28 @@ pub struct CompletionEntry {
is_import_statement_completion: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
data: Option<Value>,
+ /// This is not from tsc, we add it for convenience during normalization.
+ /// Represents `self.data.file_name`, but normalized.
+ #[serde(skip)]
+ auto_import_data: Option<CompletionNormalizedAutoImportData>,
}
impl CompletionEntry {
+ fn normalize(&mut self, specifier_map: &TscSpecifierMap) {
+ let Some(data) = &self.data else {
+ return;
+ };
+ let Ok(raw) =
+ serde_json::from_value::<CompletionEntryDataAutoImport>(data.clone())
+ else {
+ return;
+ };
+ if let Ok(normalized) = specifier_map.normalize(&raw.file_name) {
+ self.auto_import_data =
+ Some(CompletionNormalizedAutoImportData { raw, normalized });
+ }
+ }
+
fn get_commit_characters(
&self,
info: &CompletionInfo,
@@ -3833,25 +3948,44 @@ impl CompletionEntry {
if let Some(source) = &self.source {
let mut display_source = source.clone();
- if let Some(data) = &self.data {
- if let Ok(import_data) =
- serde_json::from_value::<CompletionEntryDataImport>(data.clone())
+ if let Some(import_data) = &self.auto_import_data {
+ let import_mapper =
+ language_server.get_ts_response_import_mapper(specifier);
+ if let Some(mut new_specifier) = import_mapper
+ .check_specifier(&import_data.normalized, specifier)
+ .or_else(|| relative_specifier(specifier, &import_data.normalized))
{
- if let Ok(import_specifier) = resolve_url(&import_data.file_name) {
- if let Some(new_module_specifier) = language_server
- .get_ts_response_import_mapper(specifier)
- .check_specifier(&import_specifier, specifier)
- .or_else(|| relative_specifier(specifier, &import_specifier))
- {
- display_source.clone_from(&new_module_specifier);
- if new_module_specifier != import_data.module_specifier {
- specifier_rewrite =
- Some((import_data.module_specifier, new_module_specifier));
- }
- } else if source.starts_with(jsr_url().as_str()) {
- return None;
- }
+ if new_specifier.contains("/node_modules/") {
+ return None;
+ }
+ let mut new_deno_types_specifier = None;
+ if let Some(code_specifier) = language_server
+ .resolver
+ .deno_types_to_code_resolution(
+ &import_data.normalized,
+ Some(specifier),
+ )
+ .and_then(|s| {
+ import_mapper
+ .check_specifier(&s, specifier)
+ .or_else(|| relative_specifier(specifier, &s))
+ })
+ {
+ new_deno_types_specifier =
+ Some(std::mem::replace(&mut new_specifier, code_specifier));
+ }
+ display_source.clone_from(&new_specifier);
+ if new_specifier != import_data.raw.module_specifier
+ || new_deno_types_specifier.is_some()
+ {
+ specifier_rewrite = Some(CompletionSpecifierRewrite {
+ old_specifier: import_data.raw.module_specifier.clone(),
+ new_specifier,
+ new_deno_types_specifier,
+ });
}
+ } else if source.starts_with(jsr_url().as_str()) {
+ return None;
}
}
// We want relative or bare (import-mapped or otherwise) specifiers to
@@ -3939,7 +4073,7 @@ pub struct OutliningSpan {
kind: OutliningSpanKind,
}
-const FOLD_END_PAIR_CHARACTERS: &[u8] = &[b'}', b']', b')', b'`'];
+const FOLD_END_PAIR_CHARACTERS: &[u8] = b"}])`";
impl OutliningSpan {
pub fn to_folding_range(
@@ -4154,6 +4288,11 @@ impl TscSpecifierMap {
return specifier.to_string();
}
let mut specifier = original.to_string();
+ if !specifier.contains("/node_modules/@types/node/") {
+ // The ts server doesn't give completions from files in
+ // `node_modules/.deno/`. We work around it like this.
+ specifier = specifier.replace("/node_modules/", "/$node_modules/");
+ }
let media_type = MediaType::from_specifier(original);
// If the URL-inferred media type doesn't correspond to tsc's path-inferred
// media type, force it to be the same by appending an extension.
@@ -4271,7 +4410,7 @@ fn op_is_cancelled(state: &mut OpState) -> bool {
fn op_is_node_file(state: &mut OpState, #[string] path: String) -> bool {
let state = state.borrow::<State>();
let mark = state.performance.mark("tsc.op.op_is_node_file");
- let r = match ModuleSpecifier::parse(&path) {
+ let r = match state.specifier_map.normalize(path) {
Ok(specifier) => state.state_snapshot.resolver.in_node_modules(&specifier),
Err(_) => false,
};
@@ -4308,13 +4447,16 @@ fn op_load<'s>(
data: doc.text(),
script_kind: crate::tsc::as_ts_script_kind(doc.media_type()),
version: state.script_version(&specifier),
- is_cjs: matches!(
- doc.media_type(),
- MediaType::Cjs | MediaType::Cts | MediaType::Dcts
- ),
+ is_cjs: doc
+ .document()
+ .map(|d| state.state_snapshot.is_cjs_resolver.get_doc_module_kind(d))
+ .unwrap_or(NodeModuleKind::Esm)
+ == NodeModuleKind::Cjs,
})
};
+ lsp_warn!("op_load {} {}", &specifier, maybe_load_response.is_some());
+
let serialized = serde_v8::to_v8(scope, maybe_load_response)?;
state.performance.measure(mark);
@@ -4540,7 +4682,10 @@ fn op_script_names(state: &mut OpState) -> ScriptNames {
for doc in &docs {
let specifier = doc.specifier();
let is_open = doc.is_open();
- if is_open || specifier.scheme() == "file" {
+ if is_open
+ || (specifier.scheme() == "file"
+ && !state.state_snapshot.resolver.in_node_modules(specifier))
+ {
let script_names = doc
.scope()
.and_then(|s| result.by_scope.get_mut(s))
@@ -4551,6 +4696,10 @@ fn op_script_names(state: &mut OpState) -> ScriptNames {
let (types, _) = documents.resolve_dependency(
types,
specifier,
+ state
+ .state_snapshot
+ .is_cjs_resolver
+ .get_doc_module_kind(doc),
doc.file_referrer(),
)?;
let types_doc = documents.get_or_load(&types, doc.file_referrer())?;
@@ -4654,6 +4803,7 @@ fn run_tsc_thread(
specifier_map,
request_rx,
)],
+ create_params: create_isolate_create_params(),
startup_snapshot: Some(tsc::compiler_snapshot()),
inspector: has_inspector_server,
..Default::default()
@@ -4892,6 +5042,10 @@ pub struct UserPreferences {
pub allow_rename_of_import_path: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub auto_import_file_exclude_patterns: Option<Vec<String>>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub interactive_inlay_hints: Option<bool>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub prefer_type_only_auto_imports: Option<bool>,
}
impl UserPreferences {
@@ -4909,6 +5063,7 @@ impl UserPreferences {
include_completions_with_snippet_text: Some(
config.snippet_support_capable(),
),
+ interactive_inlay_hints: Some(true),
provide_refactor_not_applicable_reason: Some(true),
quote_preference: Some(fmt_config.into()),
use_label_details_in_completion_entries: Some(true),
@@ -5013,6 +5168,9 @@ impl UserPreferences {
} else {
Some(language_settings.preferences.quote_style)
},
+ prefer_type_only_auto_imports: Some(
+ language_settings.preferences.prefer_type_only_auto_imports,
+ ),
..base_preferences
}
}
@@ -5425,6 +5583,7 @@ mod tests {
documents: Arc::new(documents),
assets: Default::default(),
config: Arc::new(config),
+ is_cjs_resolver: Default::default(),
resolver,
});
let performance = Arc::new(Performance::default());
@@ -5958,6 +6117,7 @@ mod tests {
Some(temp_dir.url()),
)
.await
+ .unwrap()
.unwrap();
assert_eq!(info.entries.len(), 22);
let details = ts_server
@@ -6117,6 +6277,7 @@ mod tests {
Some(temp_dir.url()),
)
.await
+ .unwrap()
.unwrap();
let entry = info
.entries
@@ -6154,7 +6315,7 @@ mod tests {
let change = changes.text_changes.first().unwrap();
assert_eq!(
change.new_text,
- "import type { someLongVariable } from './b.ts'\n"
+ "import { someLongVariable } from './b.ts'\n"
);
}