diff --git a/cli/lsp/capabilities.rs b/cli/lsp/capabilities.rs index 0e9c93e638..03192f5979 100644 --- a/cli/lsp/capabilities.rs +++ b/cli/lsp/capabilities.rs @@ -141,6 +141,6 @@ pub fn server_capabilities( "denoConfigTasks": true, "testingApi":true, })), - inlay_hint_provider: None, + inlay_hint_provider: Some(OneOf::Left(true)), } } diff --git a/cli/lsp/config.rs b/cli/lsp/config.rs index 98ba5afb59..3c44ebe054 100644 --- a/cli/lsp/config.rs +++ b/cli/lsp/config.rs @@ -106,6 +106,101 @@ impl Default for CompletionSettings { } } +#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct InlayHintsSettings { + #[serde(default)] + pub parameter_names: InlayHintsParamNamesOptions, + #[serde(default)] + pub parameter_types: InlayHintsParamTypesOptions, + #[serde(default)] + pub variable_types: InlayHintsVarTypesOptions, + #[serde(default)] + pub property_declaration_types: InlayHintsPropDeclTypesOptions, + #[serde(default)] + pub function_like_return_types: InlayHintsFuncLikeReturnTypesOptions, + #[serde(default)] + pub enum_member_values: InlayHintsEnumMemberValuesOptions, +} + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct InlayHintsParamNamesOptions { + #[serde(default)] + pub enabled: InlayHintsParamNamesEnabled, + #[serde(default = "is_true")] + pub suppress_when_argument_matches_name: bool, +} + +impl Default for InlayHintsParamNamesOptions { + fn default() -> Self { + Self { + enabled: InlayHintsParamNamesEnabled::None, + suppress_when_argument_matches_name: true, + } + } +} + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub enum InlayHintsParamNamesEnabled { + None, + Literals, + All, +} + +impl Default for InlayHintsParamNamesEnabled { + fn default() -> Self { + Self::None + } +} + +#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct InlayHintsParamTypesOptions { + #[serde(default)] + pub enabled: bool, +} + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct InlayHintsVarTypesOptions { + #[serde(default)] + pub enabled: bool, + #[serde(default = "is_true")] + pub suppress_when_argument_matches_name: bool, +} + +impl Default for InlayHintsVarTypesOptions { + fn default() -> Self { + Self { + enabled: false, + suppress_when_argument_matches_name: true, + } + } +} + +#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct InlayHintsPropDeclTypesOptions { + #[serde(default)] + pub enabled: bool, +} + +#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct InlayHintsFuncLikeReturnTypesOptions { + #[serde(default)] + pub enabled: bool, +} + +#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct InlayHintsEnumMemberValuesOptions { + #[serde(default)] + pub enabled: bool, +} + #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct ImportCompletionSettings { @@ -202,6 +297,9 @@ pub struct WorkspaceSettings { #[serde(default)] pub code_lens: CodeLensSettings, + #[serde(default)] + pub inlay_hints: InlayHintsSettings, + /// A flag that indicates if internal debug logging should be made available. #[serde(default)] pub internal_debug: bool, @@ -238,6 +336,19 @@ impl WorkspaceSettings { pub fn enabled_code_lens(&self) -> bool { self.code_lens.implementations || self.code_lens.references } + + /// Determine if any inlay hints are enabled. This allows short circuiting + /// when there are no inlay hints enabled. + pub fn enabled_inlay_hints(&self) -> bool { + !matches!( + self.inlay_hints.parameter_names.enabled, + InlayHintsParamNamesEnabled::None + ) || self.inlay_hints.parameter_types.enabled + || self.inlay_hints.variable_types.enabled + || self.inlay_hints.property_declaration_types.enabled + || self.inlay_hints.function_like_return_types.enabled + || self.inlay_hints.enum_member_values.enabled + } } #[derive(Debug, Clone, Default)] @@ -566,6 +677,26 @@ mod tests { references_all_functions: false, test: true, }, + inlay_hints: InlayHintsSettings { + parameter_names: InlayHintsParamNamesOptions { + enabled: InlayHintsParamNamesEnabled::None, + suppress_when_argument_matches_name: true + }, + parameter_types: InlayHintsParamTypesOptions { enabled: false }, + variable_types: InlayHintsVarTypesOptions { + enabled: false, + suppress_when_argument_matches_name: true + }, + property_declaration_types: InlayHintsPropDeclTypesOptions { + enabled: false + }, + function_like_return_types: InlayHintsFuncLikeReturnTypesOptions { + enabled: false + }, + enum_member_values: InlayHintsEnumMemberValuesOptions { + enabled: false + }, + }, internal_debug: false, lint: true, suggest: CompletionSettings { diff --git a/cli/lsp/language_server.rs b/cli/lsp/language_server.rs index 64c7adeb6d..c4617df9f1 100644 --- a/cli/lsp/language_server.rs +++ b/cli/lsp/language_server.rs @@ -196,6 +196,13 @@ impl LanguageServer { } } + pub async fn inlay_hint( + &self, + params: InlayHintParams, + ) -> LspResult>> { + self.0.lock().await.inlay_hint(params).await + } + pub async fn virtual_text_document( &self, params: Option, @@ -2896,6 +2903,50 @@ impl Inner { ) } + async fn inlay_hint( + &self, + params: InlayHintParams, + ) -> LspResult>> { + let specifier = self.url_map.normalize_url(¶ms.text_document.uri); + let workspace_settings = self.config.get_workspace_settings(); + if !self.is_diagnosable(&specifier) + || !self.config.specifier_enabled(&specifier) + || !workspace_settings.enabled_inlay_hints() + { + return Ok(None); + } + + let mark = self.performance.mark("inlay_hint", Some(¶ms)); + let asset_or_doc = self.get_asset_or_document(&specifier)?; + let line_index = asset_or_doc.line_index(); + let range = tsc::TextSpan::from_range(¶ms.range, line_index.clone()) + .map_err(|err| { + error!("Failed to convert range to text_span: {}", err); + LspError::internal_error() + })?; + let req = tsc::RequestMethod::ProvideInlayHints(( + specifier.clone(), + range, + (&workspace_settings).into(), + )); + let maybe_inlay_hints: Option> = self + .ts_server + .request(self.snapshot(), req) + .await + .map_err(|err| { + error!("Unable to get inlay hints: {}", err); + LspError::internal_error() + })?; + let maybe_inlay_hints = maybe_inlay_hints.map(|hints| { + hints + .iter() + .map(|hint| hint.to_lsp(line_index.clone())) + .collect() + }); + self.performance.measure(mark); + Ok(maybe_inlay_hints) + } + async fn reload_import_registries(&mut self) -> LspResult> { fs_util::remove_dir_all_if_exists(&self.module_registries_location) .await diff --git a/cli/lsp/lsp_custom.rs b/cli/lsp/lsp_custom.rs index 49c06e15c4..b154234c79 100644 --- a/cli/lsp/lsp_custom.rs +++ b/cli/lsp/lsp_custom.rs @@ -11,6 +11,9 @@ pub const RELOAD_IMPORT_REGISTRIES_REQUEST: &str = "deno/reloadImportRegistries"; pub const VIRTUAL_TEXT_DOCUMENT: &str = "deno/virtualTextDocument"; +// While lsp_types supports inlay hints currently, tower_lsp does not. +pub const INLAY_HINT: &str = "textDocument/inlayHint"; + #[derive(Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct CacheParams { diff --git a/cli/lsp/mod.rs b/cli/lsp/mod.rs index 2ee22510fb..7161c52091 100644 --- a/cli/lsp/mod.rs +++ b/cli/lsp/mod.rs @@ -58,6 +58,7 @@ pub async fn start() -> Result<(), AnyError> { lsp_custom::VIRTUAL_TEXT_DOCUMENT, LanguageServer::virtual_text_document, ) + .custom_method(lsp_custom::INLAY_HINT, LanguageServer::inlay_hint) .finish(); Server::new(stdin, stdout, socket).serve(service).await; diff --git a/cli/lsp/repl.rs b/cli/lsp/repl.rs index b6329205a8..60354f2d0f 100644 --- a/cli/lsp/repl.rs +++ b/cli/lsp/repl.rs @@ -288,6 +288,7 @@ pub fn get_repl_workspace_settings() -> WorkspaceSettings { cache: None, import_map: None, code_lens: Default::default(), + inlay_hints: Default::default(), internal_debug: false, lint: false, tls_certificate: None, diff --git a/cli/lsp/tsc.rs b/cli/lsp/tsc.rs index 51dd74240c..6c21369903 100644 --- a/cli/lsp/tsc.rs +++ b/cli/lsp/tsc.rs @@ -618,6 +618,15 @@ pub struct TextSpan { } impl TextSpan { + pub fn from_range( + range: &lsp::Range, + line_index: Arc, + ) -> Result { + let start = line_index.offset_tsc(range.start)?; + let length = line_index.offset_tsc(range.end)? - start; + Ok(Self { start, length }) + } + pub fn to_range(&self, line_index: Arc) -> lsp::Range { lsp::Range { start: line_index.position_tsc(self.start.into()), @@ -932,6 +941,48 @@ impl NavigateToItem { } } +#[derive(Debug, Clone, Deserialize)] +pub enum InlayHintKind { + Type, + Parameter, + Enum, +} + +impl InlayHintKind { + pub fn to_lsp(&self) -> Option { + match self { + Self::Enum => None, + Self::Parameter => Some(lsp::InlayHintKind::PARAMETER), + Self::Type => Some(lsp::InlayHintKind::TYPE), + } + } +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct InlayHint { + pub text: String, + pub position: u32, + pub kind: InlayHintKind, + pub whitespace_before: Option, + pub whitespace_after: Option, +} + +impl InlayHint { + pub fn to_lsp(&self, line_index: Arc) -> lsp::InlayHint { + lsp::InlayHint { + position: line_index.position_tsc(self.position.into()), + label: lsp::InlayHintLabel::String(self.text.clone()), + kind: self.kind.to_lsp(), + padding_left: self.whitespace_before, + padding_right: self.whitespace_after, + text_edits: None, + tooltip: None, + data: None, + } + } +} + #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct NavigationTree { @@ -2830,6 +2881,18 @@ pub enum IncludeInlayParameterNameHints { All, } +impl From<&config::InlayHintsParamNamesEnabled> + for IncludeInlayParameterNameHints +{ + fn from(setting: &config::InlayHintsParamNamesEnabled) -> Self { + match setting { + config::InlayHintsParamNamesEnabled::All => Self::All, + config::InlayHintsParamNamesEnabled::Literals => Self::Literals, + config::InlayHintsParamNamesEnabled::None => Self::None, + } + } +} + #[derive(Debug, Serialize)] #[serde(rename_all = "kebab-case")] #[allow(dead_code)] @@ -2910,6 +2973,8 @@ pub struct UserPreferences { #[serde(skip_serializing_if = "Option::is_none")] pub include_inlay_variable_type_hints: Option, #[serde(skip_serializing_if = "Option::is_none")] + pub include_inlay_variable_type_hints_when_type_matches_name: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub include_inlay_property_declaration_type_hints: Option, #[serde(skip_serializing_if = "Option::is_none")] pub include_inlay_function_like_return_type_hints: Option, @@ -2921,6 +2986,43 @@ pub struct UserPreferences { pub auto_import_file_exclude_patterns: Option>, } +impl From<&config::WorkspaceSettings> for UserPreferences { + fn from(workspace_settings: &config::WorkspaceSettings) -> Self { + let inlay_hints = &workspace_settings.inlay_hints; + Self { + include_inlay_parameter_name_hints: Some( + (&inlay_hints.parameter_names.enabled).into(), + ), + include_inlay_parameter_name_hints_when_argument_matches_name: Some( + inlay_hints + .parameter_names + .suppress_when_argument_matches_name, + ), + include_inlay_function_parameter_type_hints: Some( + inlay_hints.parameter_types.enabled, + ), + include_inlay_variable_type_hints: Some( + inlay_hints.variable_types.enabled, + ), + include_inlay_variable_type_hints_when_type_matches_name: Some( + inlay_hints + .variable_types + .suppress_when_argument_matches_name, + ), + include_inlay_property_declaration_type_hints: Some( + inlay_hints.property_declaration_types.enabled, + ), + include_inlay_function_like_return_type_hints: Some( + inlay_hints.function_like_return_types.enabled, + ), + include_inlay_enum_member_value_hints: Some( + inlay_hints.enum_member_values.enabled, + ), + ..Default::default() + } + } +} + #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct SignatureHelpItemsOptions { @@ -3053,6 +3155,8 @@ pub enum RequestMethod { ProvideCallHierarchyIncomingCalls((ModuleSpecifier, u32)), /// Resolve outgoing call hierarchy items for a specific position. ProvideCallHierarchyOutgoingCalls((ModuleSpecifier, u32)), + /// Resolve inlay hints for a specific text span + ProvideInlayHints((ModuleSpecifier, TextSpan, UserPreferences)), // Special request, used only internally by the LSP Restart, @@ -3269,6 +3373,15 @@ impl RequestMethod { "position": position }) } + RequestMethod::ProvideInlayHints((specifier, span, preferences)) => { + json!({ + "id": id, + "method": "provideInlayHints", + "specifier": state.denormalize_specifier(specifier), + "span": span, + "preferences": preferences, + }) + } RequestMethod::Restart => json!({ "id": id, "method": "restart", diff --git a/cli/tests/integration/lsp_tests.rs b/cli/tests/integration/lsp_tests.rs index cc8625476e..7dc5ff02da 100644 --- a/cli/tests/integration/lsp_tests.rs +++ b/cli/tests/integration/lsp_tests.rs @@ -1071,6 +1071,213 @@ fn lsp_hover_disabled() { shutdown(&mut client); } +#[test] +fn lsp_inlay_hints() { + let mut client = init("initialize_params_hints.json"); + did_open( + &mut client, + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "languageId": "typescript", + "version": 1, + "text": r#"function a(b: string) { + return b; + } + + a("foo"); + + enum C { + A, + } + + parseInt("123", 8); + + const d = Date.now(); + + class E { + f = Date.now(); + } + + ["a"].map((v) => v + v); + "# + } + }), + ); + let (maybe_res, maybe_err) = client + .write_request::<_, _, Value>( + "textDocument/inlayHint", + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + }, + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 19, + "character": 0, + } + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + json!(maybe_res), + json!([ + { + "position": { + "line": 0, + "character": 21 + }, + "label": ": string", + "kind": 1, + "paddingLeft": true + }, + { + "position": { + "line": 4, + "character": 10 + }, + "label": "b:", + "kind": 2, + "paddingRight": true + }, + { + "position": { + "line": 7, + "character": 11 + }, + "label": "= 0", + "paddingLeft": true + }, + { + "position": { + "line": 10, + "character": 17 + }, + "label": "string:", + "kind": 2, + "paddingRight": true + }, + { + "position": { + "line": 10, + "character": 24 + }, + "label": "radix:", + "kind": 2, + "paddingRight": true + }, + { + "position": { + "line": 12, + "character": 15 + }, + "label": ": number", + "kind": 1, + "paddingLeft": true + }, + { + "position": { + "line": 15, + "character": 11 + }, + "label": ": number", + "kind": 1, + "paddingLeft": true + }, + { + "position": { + "line": 18, + "character": 18 + }, + "label": "callbackfn:", + "kind": 2, + "paddingRight": true + }, + { + "position": { + "line": 18, + "character": 20 + }, + "label": ": string", + "kind": 1, + "paddingLeft": true + }, + { + "position": { + "line": 18, + "character": 21 + }, + "label": ": string", + "kind": 1, + "paddingLeft": true + } + ]) + ); +} + +#[test] +fn lsp_inlay_hints_not_enabled() { + let mut client = init("initialize_params.json"); + did_open( + &mut client, + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + "languageId": "typescript", + "version": 1, + "text": r#"function a(b: string) { + return b; + } + + a("foo"); + + enum C { + A, + } + + parseInt("123", 8); + + const d = Date.now(); + + class E { + f = Date.now(); + } + + ["a"].map((v) => v + v); + "# + } + }), + ); + let (maybe_res, maybe_err) = client + .write_request::<_, _, Value>( + "textDocument/inlayHint", + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + }, + "range": { + "start": { + "line": 0, + "character": 0 + }, + "end": { + "line": 19, + "character": 0, + } + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!(json!(maybe_res), json!(null)); +} + #[test] fn lsp_workspace_enable_paths() { let mut params: lsp::InitializeParams = serde_json::from_value(load_fixture( diff --git a/cli/tests/testdata/lsp/initialize_params_hints.json b/cli/tests/testdata/lsp/initialize_params_hints.json new file mode 100644 index 0000000000..1bab6e86d8 --- /dev/null +++ b/cli/tests/testdata/lsp/initialize_params_hints.json @@ -0,0 +1,102 @@ +{ + "processId": 0, + "clientInfo": { + "name": "test-harness", + "version": "1.0.0" + }, + "rootUri": null, + "initializationOptions": { + "enable": true, + "cache": null, + "certificateStores": null, + "codeLens": { + "implementations": true, + "references": true, + "test": true + }, + "config": null, + "importMap": null, + "inlayHints": { + "parameterNames": { + "enabled": "all" + }, + "parameterTypes": { + "enabled": true + }, + "variableTypes": { + "enabled": true + }, + "propertyDeclarationTypes": { + "enabled": true + }, + "functionLikeReturnTypes": { + "enabled": true + }, + "enumMemberValues": { + "enabled": true + } + }, + "lint": true, + "suggest": { + "autoImports": true, + "completeFunctionCalls": false, + "names": true, + "paths": true, + "imports": { + "hosts": {} + } + }, + "testing": { + "args": [ + "--allow-all" + ], + "enable": true + }, + "tlsCertificate": null, + "unsafelyIgnoreCertificateErrors": null, + "unstable": false + }, + "capabilities": { + "textDocument": { + "codeAction": { + "codeActionLiteralSupport": { + "codeActionKind": { + "valueSet": [ + "quickfix", + "refactor" + ] + } + }, + "isPreferredSupport": true, + "dataSupport": true, + "disabledSupport": true, + "resolveSupport": { + "properties": [ + "edit" + ] + } + }, + "completion": { + "completionItem": { + "snippetSupport": true + } + }, + "foldingRange": { + "lineFoldingOnly": true + }, + "synchronization": { + "dynamicRegistration": true, + "willSave": true, + "willSaveWaitUntil": true, + "didSave": true + } + }, + "workspace": { + "configuration": true, + "workspaceFolders": true + }, + "experimental": { + "testingApi": true + } + } +} diff --git a/cli/tsc/99_main_compiler.js b/cli/tsc/99_main_compiler.js index ab43af38db..b39f56cf6a 100644 --- a/cli/tsc/99_main_compiler.js +++ b/cli/tsc/99_main_compiler.js @@ -898,6 +898,15 @@ delete Object.prototype.__proto__; ), ); } + case "provideInlayHints": + return respond( + id, + languageService.provideInlayHints( + request.specifier, + request.span, + request.preferences, + ), + ); default: throw new TypeError( // @ts-ignore exhausted case statement sets type to never diff --git a/cli/tsc/compiler.d.ts b/cli/tsc/compiler.d.ts index 31c8b8c649..ffd4695aec 100644 --- a/cli/tsc/compiler.d.ts +++ b/cli/tsc/compiler.d.ts @@ -77,7 +77,8 @@ declare global { | GetTypeDefinitionRequest | PrepareCallHierarchy | ProvideCallHierarchyIncomingCalls - | ProvideCallHierarchyOutgoingCalls; + | ProvideCallHierarchyOutgoingCalls + | ProvideInlayHints; interface BaseLanguageServerRequest { id: number; @@ -255,6 +256,13 @@ declare global { position: number; } + interface ProvideInlayHints extends BaseLanguageServerRequest { + method: "provideInlayHints"; + specifier: string; + span: ts.TextSpan; + preferences?: ts.UserPreferences; + } + interface Restart extends BaseLanguageServerRequest { method: "restart"; }