From 6de35e4b2e47117deb87ba95acd271ee851fdbd9 Mon Sep 17 00:00:00 2001 From: Nayeem Rahman Date: Thu, 17 Aug 2023 15:46:11 +0100 Subject: [PATCH] fix(lsp): pass fmt options to completion requests (#20184) Fixes https://github.com/denoland/vscode_deno/issues/856. --- cli/lsp/language_server.rs | 16 ++- cli/lsp/tsc.rs | 198 ++++++++++++++++++++++++++++++++++-- cli/tsc/99_main_compiler.js | 13 +-- cli/tsc/compiler.d.ts | 5 + 4 files changed, 213 insertions(+), 19 deletions(-) diff --git a/cli/lsp/language_server.rs b/cli/lsp/language_server.rs index f0da80bab8..f532430c88 100644 --- a/cli/lsp/language_server.rs +++ b/cli/lsp/language_server.rs @@ -69,6 +69,7 @@ use super::text; use super::tsc; use super::tsc::Assets; use super::tsc::AssetsSnapshot; +use super::tsc::GetCompletionDetailsArgs; use super::tsc::TsServer; use super::urls; use super::urls::LspClientUrl; @@ -1899,6 +1900,7 @@ impl Inner { line_index.offset_tsc(diagnostic.range.start)? ..line_index.offset_tsc(diagnostic.range.end)?, codes, + (&self.fmt_options.options).into(), ) .await; for action in actions { @@ -2007,7 +2009,11 @@ impl Inner { })?; let combined_code_actions = self .ts_server - .get_combined_code_fix(self.snapshot(), &code_action_data) + .get_combined_code_fix( + self.snapshot(), + &code_action_data, + (&self.fmt_options.options).into(), + ) .await?; if combined_code_actions.commands.is_some() { error!("Deno does not support code actions with commands."); @@ -2047,6 +2053,7 @@ impl Inner { .get_edits_for_refactor( self.snapshot(), action_data.specifier, + (&self.fmt_options.options).into(), line_index.offset_tsc(action_data.range.start)? ..line_index.offset_tsc(action_data.range.end)?, action_data.refactor_name, @@ -2402,6 +2409,7 @@ impl Inner { trigger_character, trigger_kind, }, + (&self.fmt_options.options).into(), ) .await; @@ -2436,9 +2444,13 @@ impl Inner { })?; if let Some(data) = &data.tsc { let specifier = &data.specifier; + let args = GetCompletionDetailsArgs { + format_code_settings: Some((&self.fmt_options.options).into()), + ..data.into() + }; let result = self .ts_server - .get_completion_details(self.snapshot(), data.into()) + .get_completion_details(self.snapshot(), args) .await; match result { Ok(maybe_completion_info) => { diff --git a/cli/lsp/tsc.rs b/cli/lsp/tsc.rs index 75ed8ebe38..99dd92193c 100644 --- a/cli/lsp/tsc.rs +++ b/cli/lsp/tsc.rs @@ -21,6 +21,7 @@ use super::urls::LspClientUrl; use super::urls::LspUrlMap; use super::urls::INVALID_SPECIFIER; +use crate::args::FmtOptionsConfig; use crate::args::TsConfig; use crate::cache::HttpCache; use crate::lsp::cache::CacheMetadata; @@ -95,6 +96,35 @@ type Request = ( CancellationToken, ); +/// Relevant subset of https://github.com/denoland/deno/blob/80331d1fe5b85b829ac009fdc201c128b3427e11/cli/tsc/dts/typescript.d.ts#L6658. +#[derive(Clone, Debug, Default, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct FormatCodeSettings { + convert_tabs_to_spaces: Option, + indent_size: Option, + semicolons: Option, +} + +impl From<&FmtOptionsConfig> for FormatCodeSettings { + fn from(config: &FmtOptionsConfig) -> Self { + FormatCodeSettings { + convert_tabs_to_spaces: Some(!config.use_tabs.unwrap_or(false)), + indent_size: Some(config.indent_width.unwrap_or(2)), + semicolons: match config.semi_colons { + Some(false) => Some(SemicolonPreference::Remove), + _ => Some(SemicolonPreference::Insert), + }, + } + } +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub enum SemicolonPreference { + Insert, + Remove, +} + #[derive(Clone, Debug)] pub struct TsServer(mpsc::UnboundedSender); @@ -202,9 +232,15 @@ impl TsServer { specifier: ModuleSpecifier, range: Range, codes: Vec, + format_code_settings: FormatCodeSettings, ) -> Vec { - let req = - RequestMethod::GetCodeFixes((specifier, range.start, range.end, codes)); + let req = RequestMethod::GetCodeFixes(( + specifier, + range.start, + range.end, + codes, + format_code_settings, + )); match self.request(snapshot, req).await { Ok(items) => items, Err(err) => { @@ -243,10 +279,12 @@ impl TsServer { &self, snapshot: Arc, code_action_data: &CodeActionData, + format_code_settings: FormatCodeSettings, ) -> Result { let req = RequestMethod::GetCombinedCodeFix(( code_action_data.specifier.clone(), json!(code_action_data.fix_id.clone()), + format_code_settings, )); self.request(snapshot, req).await.map_err(|err| { log::error!("Unable to get combined fix from TypeScript: {}", err); @@ -258,12 +296,14 @@ impl TsServer { &self, snapshot: Arc, specifier: ModuleSpecifier, + format_code_settings: FormatCodeSettings, range: Range, refactor_name: String, action_name: String, ) -> Result { let req = RequestMethod::GetEditsForRefactor(( specifier, + format_code_settings, TextSpan { start: range.start, length: range.end - range.start, @@ -330,8 +370,14 @@ impl TsServer { specifier: ModuleSpecifier, position: u32, options: GetCompletionsAtPositionOptions, + format_code_settings: FormatCodeSettings, ) -> Option { - let req = RequestMethod::GetCompletions((specifier, position, options)); + let req = RequestMethod::GetCompletions(( + specifier, + position, + options, + format_code_settings, + )); match self.request(snapshot, req).await { Ok(maybe_info) => maybe_info, Err(err) => { @@ -3542,6 +3588,8 @@ pub struct GetCompletionDetailsArgs { pub position: u32, pub name: String, #[serde(skip_serializing_if = "Option::is_none")] + pub format_code_settings: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub source: Option, #[serde(skip_serializing_if = "Option::is_none")] pub preferences: Option, @@ -3557,6 +3605,7 @@ impl From<&CompletionItemData> for GetCompletionDetailsArgs { name: item_data.name.clone(), source: item_data.source.clone(), preferences: None, + format_code_settings: None, data: item_data.data.clone(), } } @@ -3586,15 +3635,30 @@ enum RequestMethod { /// Retrieve the possible refactor info for a range of a file. GetApplicableRefactors((ModuleSpecifier, TextSpan, String)), /// Retrieve the refactor edit info for a range. - GetEditsForRefactor((ModuleSpecifier, TextSpan, String, String)), + GetEditsForRefactor( + ( + ModuleSpecifier, + FormatCodeSettings, + TextSpan, + String, + String, + ), + ), /// Retrieve code fixes for a range of a file with the provided error codes. - GetCodeFixes((ModuleSpecifier, u32, u32, Vec)), + GetCodeFixes((ModuleSpecifier, u32, u32, Vec, FormatCodeSettings)), /// Get completion information at a given position (IntelliSense). - GetCompletions((ModuleSpecifier, u32, GetCompletionsAtPositionOptions)), + GetCompletions( + ( + ModuleSpecifier, + u32, + GetCompletionsAtPositionOptions, + FormatCodeSettings, + ), + ), /// Get details about a specific completion entry. GetCompletionDetails(GetCompletionDetailsArgs), /// Retrieve the combined code fixes for a fix id for a module. - GetCombinedCodeFix((ModuleSpecifier, Value)), + GetCombinedCodeFix((ModuleSpecifier, Value, FormatCodeSettings)), /// Get declaration information for a specific position. GetDefinition((ModuleSpecifier, u32)), /// Return diagnostics for given file. @@ -3680,6 +3744,7 @@ impl RequestMethod { }), RequestMethod::GetEditsForRefactor(( specifier, + format_code_settings, span, refactor_name, action_name, @@ -3687,6 +3752,7 @@ impl RequestMethod { "id": id, "method": "getEditsForRefactor", "specifier": state.denormalize_specifier(specifier), + "formatCodeSettings": format_code_settings, "range": { "pos": span.start, "end": span.start + span.length}, "refactorName": refactor_name, "actionName": action_name, @@ -3696,6 +3762,7 @@ impl RequestMethod { start_pos, end_pos, error_codes, + format_code_settings, )) => json!({ "id": id, "method": "getCodeFixes", @@ -3703,25 +3770,37 @@ impl RequestMethod { "startPosition": start_pos, "endPosition": end_pos, "errorCodes": error_codes, + "formatCodeSettings": format_code_settings, }), - RequestMethod::GetCombinedCodeFix((specifier, fix_id)) => json!({ + RequestMethod::GetCombinedCodeFix(( + specifier, + fix_id, + format_code_settings, + )) => json!({ "id": id, "method": "getCombinedCodeFix", "specifier": state.denormalize_specifier(specifier), "fixId": fix_id, + "formatCodeSettings": format_code_settings, }), RequestMethod::GetCompletionDetails(args) => json!({ "id": id, "method": "getCompletionDetails", "args": args }), - RequestMethod::GetCompletions((specifier, position, preferences)) => { + RequestMethod::GetCompletions(( + specifier, + position, + preferences, + format_code_settings, + )) => { json!({ "id": id, "method": "getCompletions", "specifier": state.denormalize_specifier(specifier), "position": position, "preferences": preferences, + "formatCodeSettings": format_code_settings, }) } RequestMethod::GetDefinition((specifier, position)) => json!({ @@ -4589,6 +4668,7 @@ mod tests { trigger_character: Some(".".to_string()), trigger_kind: None, }, + Default::default(), )), Default::default(), ); @@ -4605,6 +4685,7 @@ mod tests { name: "log".to_string(), source: None, preferences: None, + format_code_settings: None, data: None, }), Default::default(), @@ -4700,6 +4781,105 @@ mod tests { ); } + #[test] + fn test_completions_fmt() { + let fixture_a = r#" + console.log(someLongVaria) + "#; + let fixture_b = r#" + export const someLongVariable = 1 + "#; + let line_index = LineIndex::new(fixture_a); + let position = line_index + .offset_tsc(lsp::Position { + line: 1, + character: 33, + }) + .unwrap(); + let temp_dir = TempDir::new(); + let (mut runtime, state_snapshot, _) = setup( + &temp_dir, + false, + json!({ + "target": "esnext", + "module": "esnext", + "lib": ["deno.ns", "deno.window"], + "noEmit": true, + }), + &[ + ("file:///a.ts", fixture_a, 1, LanguageId::TypeScript), + ("file:///b.ts", fixture_b, 1, LanguageId::TypeScript), + ], + ); + let specifier = resolve_url("file:///a.ts").expect("could not resolve url"); + let result = request( + &mut runtime, + state_snapshot.clone(), + RequestMethod::GetDiagnostics(vec![specifier.clone()]), + Default::default(), + ); + assert!(result.is_ok()); + let fmt_options_config = FmtOptionsConfig { + semi_colons: Some(false), + ..Default::default() + }; + let result = request( + &mut runtime, + state_snapshot.clone(), + RequestMethod::GetCompletions(( + specifier.clone(), + position, + GetCompletionsAtPositionOptions { + user_preferences: UserPreferences { + allow_incomplete_completions: Some(true), + allow_text_changes_in_new_files: Some(true), + include_completions_for_module_exports: Some(true), + include_completions_with_insert_text: Some(true), + ..Default::default() + }, + ..Default::default() + }, + (&fmt_options_config).into(), + )), + Default::default(), + ) + .unwrap(); + let info: CompletionInfo = serde_json::from_value(result).unwrap(); + let entry = info + .entries + .iter() + .find(|e| &e.name == "someLongVariable") + .unwrap(); + let result = request( + &mut runtime, + state_snapshot, + RequestMethod::GetCompletionDetails(GetCompletionDetailsArgs { + specifier, + position, + name: entry.name.clone(), + source: entry.source.clone(), + preferences: None, + format_code_settings: Some((&fmt_options_config).into()), + data: entry.data.clone(), + }), + Default::default(), + ) + .unwrap(); + let details: CompletionEntryDetails = + serde_json::from_value(result).unwrap(); + let actions = details.code_actions.unwrap(); + let action = actions + .iter() + .find(|a| &a.description == r#"Add import from "./b.ts""#) + .unwrap(); + let changes = action.changes.first().unwrap(); + let change = changes.text_changes.first().unwrap(); + assert_eq!( + change.new_text, + "import { someLongVariable } from \"./b.ts\"\n" + ); + } + #[test] fn test_update_import_statement() { let fixtures = vec![ diff --git a/cli/tsc/99_main_compiler.js b/cli/tsc/99_main_compiler.js index 43a3c3bcf6..f2ccec466f 100644 --- a/cli/tsc/99_main_compiler.js +++ b/cli/tsc/99_main_compiler.js @@ -1035,10 +1035,8 @@ delete Object.prototype.__proto__; languageService.getEditsForRefactor( request.specifier, { - indentSize: 2, + ...request.formatCodeSettings, indentStyle: ts.IndentStyle.Smart, - semicolons: ts.SemicolonPreference.Insert, - convertTabsToSpaces: true, insertSpaceBeforeAndAfterBinaryOperators: true, insertSpaceAfterCommaDelimiter: true, }, @@ -1060,9 +1058,8 @@ delete Object.prototype.__proto__; request.endPosition, request.errorCodes.map((v) => Number(v)), { - indentSize: 2, + ...request.formatCodeSettings, indentStyle: ts.IndentStyle.Block, - semicolons: ts.SemicolonPreference.Insert, }, { quotePreference: "double", @@ -1080,9 +1077,8 @@ delete Object.prototype.__proto__; }, request.fixId, { - indentSize: 2, + ...request.formatCodeSettings, indentStyle: ts.IndentStyle.Block, - semicolons: ts.SemicolonPreference.Insert, }, { quotePreference: "double", @@ -1100,7 +1096,7 @@ delete Object.prototype.__proto__; request.args.specifier, request.args.position, request.args.name, - {}, + request.args.formatCodeSettings ?? {}, request.args.source, request.args.preferences, request.args.data, @@ -1114,6 +1110,7 @@ delete Object.prototype.__proto__; request.specifier, request.position, request.preferences, + request.formatCodeSettings, ), ); } diff --git a/cli/tsc/compiler.d.ts b/cli/tsc/compiler.d.ts index 66c0946972..da713a1bd5 100644 --- a/cli/tsc/compiler.d.ts +++ b/cli/tsc/compiler.d.ts @@ -121,6 +121,7 @@ declare global { interface GetEditsForRefactor extends BaseLanguageServerRequest { method: "getEditsForRefactor"; specifier: string; + formatCodeSettings: ts.FormatCodeSettings; range: ts.TextRange; refactorName: string; actionName: string; @@ -132,6 +133,7 @@ declare global { startPosition: number; endPosition: number; errorCodes: string[]; + formatCodeSettings: ts.FormatCodeSettings; } interface GetCombinedCodeFix extends BaseLanguageServerRequest { @@ -139,6 +141,7 @@ declare global { specifier: string; // deno-lint-ignore ban-types fixId: {}; + formatCodeSettings: ts.FormatCodeSettings; } interface GetCompletionDetails extends BaseLanguageServerRequest { @@ -147,6 +150,7 @@ declare global { specifier: string; position: number; name: string; + formatCodeSettings: ts.FormatCodeSettings; source?: string; preferences?: ts.UserPreferences; data?: ts.CompletionEntryData; @@ -158,6 +162,7 @@ declare global { specifier: string; position: number; preferences: ts.GetCompletionsAtPositionOptions; + formatCodeSettings: ts.FormatCodeSettings; } interface GetDiagnosticsRequest extends BaseLanguageServerRequest {