diff --git a/cli/lsp/config.rs b/cli/lsp/config.rs index 9af05484c5d1ea..6a005e83b4cf5b 100644 --- a/cli/lsp/config.rs +++ b/cli/lsp/config.rs @@ -6,6 +6,7 @@ use crate::lsp::logging::lsp_warn; use crate::util::fs::canonicalize_path_maybe_not_exists; use crate::util::path::specifier_to_file_path; use deno_ast::MediaType; +use deno_config::FmtOptionsConfig; use deno_core::parking_lot::Mutex; use deno_core::serde::de::DeserializeOwned; use deno_core::serde::Deserialize; @@ -356,6 +357,29 @@ impl Default for JsxAttributeCompletionStyle { } } +#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +pub enum QuoteStyle { + Auto, + Double, + Single, +} + +impl Default for QuoteStyle { + fn default() -> Self { + Self::Auto + } +} + +impl From<&FmtOptionsConfig> for QuoteStyle { + fn from(config: &FmtOptionsConfig) -> Self { + match config.single_quote { + Some(true) => QuoteStyle::Single, + _ => QuoteStyle::Double, + } + } +} + #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct LanguagePreferences { @@ -367,6 +391,8 @@ pub struct LanguagePreferences { pub auto_import_file_exclude_patterns: Vec, #[serde(default = "is_true")] pub use_aliases_for_renames: bool, + #[serde(default)] + pub quote_style: QuoteStyle, } impl Default for LanguagePreferences { @@ -376,6 +402,7 @@ impl Default for LanguagePreferences { jsx_attribute_completion_style: Default::default(), auto_import_file_exclude_patterns: vec![], use_aliases_for_renames: true, + quote_style: Default::default(), } } } @@ -1372,6 +1399,7 @@ mod tests { jsx_attribute_completion_style: JsxAttributeCompletionStyle::Auto, auto_import_file_exclude_patterns: vec![], use_aliases_for_renames: true, + quote_style: QuoteStyle::Auto, }, suggest: CompletionSettings { complete_function_calls: false, @@ -1416,6 +1444,7 @@ mod tests { jsx_attribute_completion_style: JsxAttributeCompletionStyle::Auto, auto_import_file_exclude_patterns: vec![], use_aliases_for_renames: true, + quote_style: QuoteStyle::Auto, }, suggest: CompletionSettings { complete_function_calls: false, diff --git a/cli/lsp/language_server.rs b/cli/lsp/language_server.rs index b19b00b4aa0103..6940bd633ed9e7 100644 --- a/cli/lsp/language_server.rs +++ b/cli/lsp/language_server.rs @@ -2953,7 +2953,6 @@ impl Inner { (&self.fmt_options.options).into(), tsc::UserPreferences { allow_text_changes_in_new_files: Some(true), - quote_preference: Some((&self.fmt_options.options).into()), ..Default::default() }, ) diff --git a/cli/lsp/tsc.rs b/cli/lsp/tsc.rs index bac3f837746fb4..4ba813625a3732 100644 --- a/cli/lsp/tsc.rs +++ b/cli/lsp/tsc.rs @@ -99,24 +99,90 @@ type Request = ( CancellationToken, ); -/// Relevant subset of https://github.com/denoland/deno/blob/80331d1fe5b85b829ac009fdc201c128b3427e11/cli/tsc/dts/typescript.d.ts#L6658. +#[derive(Debug, Clone, Copy, Serialize_repr)] +#[repr(u8)] +pub enum IndentStyle { + #[allow(dead_code)] + None = 0, + Block = 1, + #[allow(dead_code)] + Smart = 2, +} + +/// Relevant subset of https://github.com/denoland/deno/blob/v1.37.1/cli/tsc/dts/typescript.d.ts#L6658. #[derive(Clone, Debug, Default, Serialize)] #[serde(rename_all = "camelCase")] pub struct FormatCodeSettings { - convert_tabs_to_spaces: Option, + base_indent_size: Option, indent_size: Option, + tab_size: Option, + new_line_character: Option, + convert_tabs_to_spaces: Option, + indent_style: Option, + trim_trailing_whitespace: Option, + insert_space_after_comma_delimiter: Option, + insert_space_after_semicolon_in_for_statements: Option, + insert_space_before_and_after_binary_operators: Option, + insert_space_after_constructor: Option, + insert_space_after_keywords_in_control_flow_statements: Option, + insert_space_after_function_keyword_for_anonymous_functions: Option, + insert_space_after_opening_and_before_closing_nonempty_parenthesis: + Option, + insert_space_after_opening_and_before_closing_nonempty_brackets: Option, + insert_space_after_opening_and_before_closing_nonempty_braces: Option, + insert_space_after_opening_and_before_closing_template_string_braces: + Option, + insert_space_after_opening_and_before_closing_jsx_expression_braces: + Option, + insert_space_after_type_assertion: Option, + insert_space_before_function_parenthesis: Option, + place_open_brace_on_new_line_for_functions: Option, + place_open_brace_on_new_line_for_control_blocks: Option, + insert_space_before_type_annotation: Option, + indent_multi_line_object_literal_beginning_on_blank_line: Option, semicolons: Option, + indent_switch_case: Option, } impl From<&FmtOptionsConfig> for FormatCodeSettings { fn from(config: &FmtOptionsConfig) -> Self { FormatCodeSettings { - convert_tabs_to_spaces: Some(!config.use_tabs.unwrap_or(false)), + base_indent_size: Some(0), indent_size: Some(config.indent_width.unwrap_or(2)), + tab_size: Some(config.indent_width.unwrap_or(2)), + new_line_character: Some("\n".to_string()), + convert_tabs_to_spaces: Some(!config.use_tabs.unwrap_or(false)), + indent_style: Some(IndentStyle::Block), + trim_trailing_whitespace: Some(false), + insert_space_after_comma_delimiter: Some(true), + insert_space_after_semicolon_in_for_statements: Some(true), + insert_space_before_and_after_binary_operators: Some(true), + insert_space_after_constructor: Some(false), + insert_space_after_keywords_in_control_flow_statements: Some(true), + insert_space_after_function_keyword_for_anonymous_functions: Some(true), + insert_space_after_opening_and_before_closing_nonempty_parenthesis: Some( + false, + ), + insert_space_after_opening_and_before_closing_nonempty_brackets: Some( + false, + ), + insert_space_after_opening_and_before_closing_nonempty_braces: Some(true), + insert_space_after_opening_and_before_closing_template_string_braces: + Some(false), + insert_space_after_opening_and_before_closing_jsx_expression_braces: Some( + false, + ), + insert_space_after_type_assertion: Some(false), + insert_space_before_function_parenthesis: Some(false), + place_open_brace_on_new_line_for_functions: Some(false), + place_open_brace_on_new_line_for_control_blocks: Some(false), + insert_space_before_type_annotation: Some(false), + indent_multi_line_object_literal_beginning_on_blank_line: Some(false), semicolons: match config.semi_colons { Some(false) => Some(SemicolonPreference::Remove), _ => Some(SemicolonPreference::Insert), }, + indent_switch_case: Some(true), } } } @@ -294,9 +360,6 @@ impl TsServer { format_code_settings: FormatCodeSettings, preferences: UserPreferences, ) -> Vec { - let mut format_code_settings = json!(format_code_settings); - let format_object = format_code_settings.as_object_mut().unwrap(); - format_object.insert("indentStyle".to_string(), json!(1)); let req = TscRequest { method: "getCodeFixesAtPosition", // https://github.com/denoland/deno/blob/v1.37.1/cli/tsc/dts/typescript.d.ts#L6257 @@ -363,9 +426,6 @@ impl TsServer { format_code_settings: FormatCodeSettings, preferences: UserPreferences, ) -> Result { - let mut format_code_settings = json!(format_code_settings); - let format_object = format_code_settings.as_object_mut().unwrap(); - format_object.insert("indentStyle".to_string(), json!(1)); let req = TscRequest { method: "getCombinedCodeFix", // https://github.com/denoland/deno/blob/v1.37.1/cli/tsc/dts/typescript.d.ts#L6258 @@ -403,15 +463,6 @@ impl TsServer { action_name: String, preferences: Option, ) -> Result { - let mut format_code_settings = json!(format_code_settings); - let format_object = format_code_settings.as_object_mut().unwrap(); - format_object.insert("indentStyle".to_string(), json!(2)); - format_object.insert( - "insertSpaceBeforeAndAfterBinaryOperators".to_string(), - json!(true), - ); - format_object - .insert("insertSpaceAfterCommaDelimiter".to_string(), json!(true)); let req = TscRequest { method: "getEditsForRefactor", // https://github.com/denoland/deno/blob/v1.37.1/cli/tsc/dts/typescript.d.ts#L6275 @@ -4023,23 +4074,7 @@ impl From for CompletionTriggerKind { } } -#[derive(Debug, Serialize)] -#[serde(rename_all = "kebab-case")] -#[allow(dead_code)] -pub enum QuotePreference { - Auto, - Double, - Single, -} - -impl From<&FmtOptionsConfig> for QuotePreference { - fn from(config: &FmtOptionsConfig) -> Self { - match config.single_quote { - Some(true) => QuotePreference::Single, - _ => QuotePreference::Double, - } - } -} +pub type QuotePreference = config::QuoteStyle; pub type ImportModuleSpecifierPreference = config::ImportModuleSpecifier; @@ -4269,6 +4304,12 @@ impl UserPreferences { provide_prefix_and_suffix_text_for_rename: Some( language_settings.preferences.use_aliases_for_renames, ), + // Only use workspace settings for quote style if there's no `deno.json`. + quote_preference: if config.has_config_file() { + base_preferences.quote_preference + } else { + Some(language_settings.preferences.quote_style) + }, ..base_preferences } } diff --git a/cli/tests/integration/lsp_tests.rs b/cli/tests/integration/lsp_tests.rs index c13053db8935f2..5792c7facfb0bb 100644 --- a/cli/tests/integration/lsp_tests.rs +++ b/cli/tests/integration/lsp_tests.rs @@ -5436,6 +5436,151 @@ fn lsp_code_actions_imports_respects_fmt_config() { client.shutdown(); } +#[test] +fn lsp_quote_style_from_workspace_settings() { + let context = TestContextBuilder::new().use_temp_cwd().build(); + let temp_dir = context.temp_dir(); + temp_dir.write( + "file00.ts", + r#" + export interface MallardDuckConfigOptions extends DuckConfigOptions { + kind: "mallard"; + } + "#, + ); + temp_dir.write( + "file01.ts", + r#" + export interface DuckConfigOptions { + kind: string; + quacks: boolean; + } + "#, + ); + let mut client = context.new_lsp_command().build(); + client.initialize_default(); + client.write_notification( + "workspace/didChangeConfiguration", + json!({ + "settings": {} + }), + ); + let settings = json!({ + "typescript": { + "preferences": { + "quoteStyle": "single", + }, + }, + }); + // one for the workspace + client.handle_configuration_request(&settings); + // one for the specifier + client.handle_configuration_request(&settings); + + let code_action_params = json!({ + "textDocument": { + "uri": temp_dir.uri().join("file00.ts").unwrap(), + }, + "range": { + "start": { "line": 0, "character": 0 }, + "end": { "line": 4, "character": 0 }, + }, + "context": { + "diagnostics": [{ + "range": { + "start": { "line": 1, "character": 56 }, + "end": { "line": 1, "character": 73 }, + }, + "severity": 1, + "code": 2304, + "source": "deno-ts", + "message": "Cannot find name 'DuckConfigOptions'.", + }], + "only": ["quickfix"], + }, + }); + + let res = + client.write_request("textDocument/codeAction", code_action_params.clone()); + // Expect single quotes in the auto-import. + assert_eq!( + res, + json!([{ + "title": "Add import from \"./file01.ts\"", + "kind": "quickfix", + "diagnostics": [{ + "range": { + "start": { "line": 1, "character": 56 }, + "end": { "line": 1, "character": 73 }, + }, + "severity": 1, + "code": 2304, + "source": "deno-ts", + "message": "Cannot find name 'DuckConfigOptions'.", + }], + "edit": { + "documentChanges": [{ + "textDocument": { + "uri": temp_dir.uri().join("file00.ts").unwrap(), + "version": null, + }, + "edits": [{ + "range": { + "start": { "line": 0, "character": 0 }, + "end": { "line": 0, "character": 0 }, + }, + "newText": "import { DuckConfigOptions } from './file01.ts';\n", + }], + }], + }, + }]), + ); + + // It should ignore the workspace setting if a `deno.json` is present. + temp_dir.write("./deno.json", json!({}).to_string()); + client.did_change_watched_files(json!({ + "changes": [{ + "uri": temp_dir.uri().join("deno.json").unwrap(), + "type": 1, + }], + })); + + let res = client.write_request("textDocument/codeAction", code_action_params); + // Expect double quotes in the auto-import. + assert_eq!( + res, + json!([{ + "title": "Add import from \"./file01.ts\"", + "kind": "quickfix", + "diagnostics": [{ + "range": { + "start": { "line": 1, "character": 56 }, + "end": { "line": 1, "character": 73 }, + }, + "severity": 1, + "code": 2304, + "source": "deno-ts", + "message": "Cannot find name 'DuckConfigOptions'.", + }], + "edit": { + "documentChanges": [{ + "textDocument": { + "uri": temp_dir.uri().join("file00.ts").unwrap(), + "version": null, + }, + "edits": [{ + "range": { + "start": { "line": 0, "character": 0 }, + "end": { "line": 0, "character": 0 }, + }, + "newText": "import { DuckConfigOptions } from \"./file01.ts\";\n", + }], + }], + }, + }]), + ); +} + #[test] fn lsp_code_actions_refactor_no_disabled_support() { let context = TestContextBuilder::new().use_temp_cwd().build();