From eda104aa074596c2ce08f1da3d4d8f37bdc76179 Mon Sep 17 00:00:00 2001 From: Shi Chen Date: Mon, 17 May 2021 15:34:50 +0800 Subject: [PATCH 01/12] able to modify the profile --- package-lock.json | 50 ++- package.json | 4 +- src/formatter-settings/FormatterConstants.ts | 3 + src/formatter-settings/FormatterConverter.ts | 138 +++++++ .../FormatterSettingView.tsx | 2 +- src/formatter-settings/index.ts | 343 +++++++++++++++--- src/formatter-settings/types.ts | 6 + src/formatter-settings/utils.ts | 61 ++++ 8 files changed, 543 insertions(+), 64 deletions(-) create mode 100644 src/formatter-settings/FormatterConverter.ts create mode 100644 src/formatter-settings/utils.ts diff --git a/package-lock.json b/package-lock.json index 1db8eb09..8a01db7a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -272,6 +272,12 @@ "integrity": "sha1-kdZxDlNtNFucmwF8V0z2qNpkxRg=", "dev": true }, + "@types/xmldom": { + "version": "0.1.30", + "resolved": "https://registry.npmjs.org/@types/xmldom/-/xmldom-0.1.30.tgz", + "integrity": "sha512-edqgAFXMEtVvaBZ3YnhamvmrHjoYpuxETmnb0lbTZmf/dXpAsO9ZKotUO4K2rn2SIZBDFCMOuA7fOe0H6dRZcA==", + "dev": true + }, "@webassemblyjs/ast": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz", @@ -960,7 +966,7 @@ }, "browserify-aes": { "version": "1.2.0", - "resolved": "http://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", "dev": true, "requires": { @@ -1207,7 +1213,7 @@ }, "camelcase-keys": { "version": "2.1.0", - "resolved": "http://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", "dev": true, "requires": { @@ -1651,7 +1657,7 @@ }, "create-hash": { "version": "1.2.0", - "resolved": "http://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", "dev": true, "requires": { @@ -1664,7 +1670,7 @@ }, "create-hmac": { "version": "1.1.7", - "resolved": "http://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", "dev": true, "requires": { @@ -2034,7 +2040,7 @@ }, "diffie-hellman": { "version": "5.0.3", - "resolved": "http://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", "dev": true, "requires": { @@ -2082,7 +2088,7 @@ "dependencies": { "domelementtype": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz", + "resolved": "http://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz", "integrity": "sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs=", "dev": true } @@ -3227,7 +3233,7 @@ }, "html-webpack-plugin": { "version": "3.2.0", - "resolved": "http://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz", + "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz", "integrity": "sha1-sBq71yOsqqeze2r0SS69oD2d03s=", "dev": true, "requires": { @@ -3753,7 +3759,7 @@ }, "json5": { "version": "0.5.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", + "resolved": "http://registry.npmjs.org/json5/-/json5-0.5.1.tgz", "integrity": "sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE=", "dev": true }, @@ -3786,7 +3792,7 @@ }, "load-json-file": { "version": "1.1.0", - "resolved": "http://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", "dev": true, "requires": { @@ -3939,7 +3945,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, "requires": { @@ -3965,7 +3971,7 @@ }, "meow": { "version": "3.7.0", - "resolved": "http://registry.npmjs.org/meow/-/meow-3.7.0.tgz", + "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", "dev": true, "requires": { @@ -4264,7 +4270,7 @@ "dependencies": { "string_decoder": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "requires": { @@ -4325,7 +4331,7 @@ }, "chalk": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "dev": true, "requires": { @@ -4504,13 +4510,13 @@ }, "os-homedir": { "version": "1.0.2", - "resolved": "http://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", "dev": true }, "os-tmpdir": { "version": "1.0.2", - "resolved": "http://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "dev": true }, @@ -4661,7 +4667,7 @@ }, "path-is-absolute": { "version": "1.0.1", - "resolved": "http://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "dev": true }, @@ -5328,7 +5334,7 @@ }, "readable-stream": { "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", "dev": true, "requires": { @@ -5616,7 +5622,7 @@ }, "safe-regex": { "version": "1.1.0", - "resolved": "http://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", "dev": true, "requires": { @@ -5760,7 +5766,7 @@ }, "sha.js": { "version": "2.4.11", - "resolved": "http://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", "dev": true, "requires": { @@ -7536,6 +7542,12 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true }, + "xmldom": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.6.0.tgz", + "integrity": "sha512-iAcin401y58LckRZ0TkI4k0VSM1Qg0KGSc3i8rU+xrxe19A/BN1zHyVSJY7uoutVlaTSzYyk/v5AmkewAP7jtg==", + "dev": true + }, "xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index bc4f80e9..ea067abc 100644 --- a/package.json +++ b/package.json @@ -289,6 +289,7 @@ "@types/semver": "^5.5.0", "@types/vscode": "1.52.0", "@types/winreg": "^1.2.30", + "@types/xmldom": "^0.1.30", "arch": "^2.1.2", "autoprefixer": "^8.5.1", "bootstrap": "^4.5.2", @@ -321,7 +322,8 @@ "url-loader": "^4.1.1", "vscode-tas-client": "^0.1.22", "webpack": "^4.44.1", - "webpack-cli": "^3.3.12" + "webpack-cli": "^3.3.12", + "xmldom": "^0.6.0" }, "extensionPack": [ "redhat.java", diff --git a/src/formatter-settings/FormatterConstants.ts b/src/formatter-settings/FormatterConstants.ts index 73891cf2..42ad97c6 100644 --- a/src/formatter-settings/FormatterConstants.ts +++ b/src/formatter-settings/FormatterConstants.ts @@ -6,6 +6,9 @@ import { Category, ExampleKind, JavaFormatterSetting, ValueKind } from "./types" export namespace JavaConstants { export const JAVA_CORE_FORMATTER_ID = "org.eclipse.jdt.core.formatter"; export const CURRENT_FORMATTER_SETTINGS_VERSION = "21"; + export const SETTINGS_URL_KEY = "format.settings.url"; + export const SETTINGS_PROFILE_KEY = "format.settings.profile"; + export const MINIMUM_JAVA_EXTENSION_VERSION: string = "0.77.0"; } export namespace VSCodeSettings { diff --git a/src/formatter-settings/FormatterConverter.ts b/src/formatter-settings/FormatterConverter.ts new file mode 100644 index 00000000..1f3b75c0 --- /dev/null +++ b/src/formatter-settings/FormatterConverter.ts @@ -0,0 +1,138 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +import { SupportedSettings } from "./FormatterConstants"; + +export namespace FormatterConverter { + export function webView2ProfileConvert(id: string, value: string | undefined): string | undefined { + switch (id) { + case SupportedSettings.INSERT_SPACE_BEFORE_FIRST_INITIALIZER: + case SupportedSettings.INSERT_SPACE_AFTER_CLOSING_ANGLE_BRACKET_IN_TYPE_ARGUMENTS: + case SupportedSettings.INSERT_SPACE_AFTER_CLOSING_PAREN_IN_CAST: + case SupportedSettings.INSERT_SPACE_AFTER_OPENING_BRACE_IN_ARRAY_INITIALIZER: + case SupportedSettings.INSERT_SPACE_BEFORE_CLOSING_BRACE_IN_ARRAY_INITIALIZER: + case SupportedSettings.INSERT_NEW_LINE_IN_CONTROL_STATEMENTS: + case SupportedSettings.INSERT_NEW_LINE_AFTER_ANNOTATION: + case SupportedSettings.INSERT_NEW_LINE_IN_EMPTY_TYPE_DECLARATION: + case SupportedSettings.INSERT_NEW_LINE_IN_EMPTY_METHOD_BODY: + case SupportedSettings.INSERT_NEW_LINE_IN_EMPTY_ENUM_DECLARATION: + case SupportedSettings.INSERT_NEW_LINE_IN_EMPTY_ENUM_CONSTANT: + case SupportedSettings.INSERT_NEW_LINE_IN_EMPTY_ANONYMOUS_TYPE_DECLARATION: + case SupportedSettings.INSERT_NEW_LINE_IN_EMPTY_ANNOTATION_DECLARATION: + case SupportedSettings.INSERT_NEW_LINE_AFTER_ANNOTATION_ON_ENUM_CONSTANT: + case SupportedSettings.INSERT_NEW_LINE_AFTER_ANNOTATION_ON_PACKAGE: + case SupportedSettings.INSERT_NEW_LINE_AFTER_ANNOTATION_ON_PARAMETER: + case SupportedSettings.INSERT_NEW_LINE_AFTER_OPENING_BRACE_IN_ARRAY_INITIALIZER: + case SupportedSettings.INSERT_NEW_LINE_BEFORE_CATCH_IN_TRY_STATEMENT: + case SupportedSettings.INSERT_NEW_LINE_BEFORE_CLOSING_BRACE_IN_ARRAY_INITIALIZER: + case SupportedSettings.INSERT_NEW_LINE_BEFORE_ELSE_IN_IF_STATEMENT: + case SupportedSettings.INSERT_NEW_LINE_BEFORE_FINALLY_IN_TRY_STATEMENT: + case SupportedSettings.INSERT_NEW_LINE_BEFORE_WHILE_IN_DO_STATEMENT: + switch (value) { + case "true": + return "insert"; + case "false": + return "do not insert"; + default: + return undefined; + } + case SupportedSettings.KEEP_TYPE_DECLARATION_ON_ONE_LINE: + case SupportedSettings.KEEP_RECORD_DECLARATION_ON_ONE_LINE: + case SupportedSettings.KEEP_RECORD_CONSTRUCTOR_ON_ONE_LINE: + case SupportedSettings.KEEP_METHOD_BODY_ON_ONE_LINE: + case SupportedSettings.KEEP_ENUM_DECLARATION_ON_ONE_LINE: + case SupportedSettings.KEEP_ENUM_CONSTANT_DECLARATION_ON_ONE_LINE: + case SupportedSettings.KEEP_ANONYMOUS_TYPE_DECLARATION_ON_ONE_LINE: + case SupportedSettings.KEEP_ANNOTATION_DECLARATION_ON_ONE_LINE: + switch (value) { + case "never": + return "one_line_never"; + case "if empty": + return "one_line_if_empty"; + case "if at most one item": + return "one_line_if_single_item"; + default: + return undefined; + } + } + return value; + } + + export function profile2WebViewConvert(id: string, value: string | undefined): string | undefined { + switch (id) { + case SupportedSettings.INSERT_SPACE_BEFORE_FIRST_INITIALIZER: + case SupportedSettings.INSERT_SPACE_AFTER_CLOSING_ANGLE_BRACKET_IN_TYPE_ARGUMENTS: + case SupportedSettings.INSERT_SPACE_AFTER_CLOSING_PAREN_IN_CAST: + case SupportedSettings.INSERT_SPACE_AFTER_OPENING_BRACE_IN_ARRAY_INITIALIZER: + case SupportedSettings.INSERT_SPACE_BEFORE_CLOSING_BRACE_IN_ARRAY_INITIALIZER: + case SupportedSettings.INSERT_NEW_LINE_IN_CONTROL_STATEMENTS: + case SupportedSettings.INSERT_NEW_LINE_AFTER_ANNOTATION: + case SupportedSettings.INSERT_NEW_LINE_IN_EMPTY_TYPE_DECLARATION: + case SupportedSettings.INSERT_NEW_LINE_IN_EMPTY_METHOD_BODY: + case SupportedSettings.INSERT_NEW_LINE_IN_EMPTY_ENUM_DECLARATION: + case SupportedSettings.INSERT_NEW_LINE_IN_EMPTY_ENUM_CONSTANT: + case SupportedSettings.INSERT_NEW_LINE_IN_EMPTY_ANONYMOUS_TYPE_DECLARATION: + case SupportedSettings.INSERT_NEW_LINE_IN_EMPTY_ANNOTATION_DECLARATION: + case SupportedSettings.INSERT_NEW_LINE_AFTER_ANNOTATION_ON_ENUM_CONSTANT: + case SupportedSettings.INSERT_NEW_LINE_AFTER_ANNOTATION_ON_PACKAGE: + case SupportedSettings.INSERT_NEW_LINE_AFTER_ANNOTATION_ON_PARAMETER: + case SupportedSettings.INSERT_NEW_LINE_AFTER_OPENING_BRACE_IN_ARRAY_INITIALIZER: + case SupportedSettings.INSERT_NEW_LINE_BEFORE_CATCH_IN_TRY_STATEMENT: + case SupportedSettings.INSERT_NEW_LINE_BEFORE_CLOSING_BRACE_IN_ARRAY_INITIALIZER: + case SupportedSettings.INSERT_NEW_LINE_BEFORE_ELSE_IN_IF_STATEMENT: + case SupportedSettings.INSERT_NEW_LINE_BEFORE_FINALLY_IN_TRY_STATEMENT: + case SupportedSettings.INSERT_NEW_LINE_BEFORE_WHILE_IN_DO_STATEMENT: + switch (value) { + case "insert": + return "true"; + case "do not insert": + return "false"; + default: + return undefined; + } + case SupportedSettings.KEEP_TYPE_DECLARATION_ON_ONE_LINE: + case SupportedSettings.KEEP_RECORD_DECLARATION_ON_ONE_LINE: + case SupportedSettings.KEEP_RECORD_CONSTRUCTOR_ON_ONE_LINE: + case SupportedSettings.KEEP_METHOD_BODY_ON_ONE_LINE: + case SupportedSettings.KEEP_ENUM_DECLARATION_ON_ONE_LINE: + case SupportedSettings.KEEP_ENUM_CONSTANT_DECLARATION_ON_ONE_LINE: + case SupportedSettings.KEEP_ANONYMOUS_TYPE_DECLARATION_ON_ONE_LINE: + case SupportedSettings.KEEP_ANNOTATION_DECLARATION_ON_ONE_LINE: + switch (value) { + case "one_line_never": + return "never"; + case "one_line_if_empty": + return "if empty"; + case "one_line_if_single_item": + return "if at most one item"; + default: + return undefined; + } + case SupportedSettings.PUT_EMPTY_STATEMENT_ON_NEW_LINE: + case SupportedSettings.COMMENT_INDENTPARAMETERDESCRIPTION: + case SupportedSettings.COMMENT_INDENT_PARAMETER_DESCRIPTION: + case SupportedSettings.COMMENT_FORMATHEADER: + case SupportedSettings.COMMENT_FORMAT_HEADER: + case SupportedSettings.COMMENT_FORMATTER_COMMENT: + case SupportedSettings.COMMENT_FORMATTER_COMMENT_CORE: + case SupportedSettings.COMMENT_FORMAT_BLOCK_COMMENTS: + case SupportedSettings.FORMAT_LINE_COMMENTS: + case SupportedSettings.COMMENT_COUNT_LINE_LENGTH_FROM_STARTING_POSITION: + case SupportedSettings.COMMENT_CLEARBLANKLINES: + case SupportedSettings.COMMENT_CLEAR_BLANK_LINES: + case SupportedSettings.COMMENT_CLEAR_BLANK_LINES_IN_JAVADOC_COMMENT: + case SupportedSettings.COMMENT_CLEAR_BLANK_LINES_IN_BLOCK_COMMENT: + case SupportedSettings.COMMENT_ON_OFF_TAGS: + case SupportedSettings.INSERT_SPACE_BEFORE_CLOSING_BRACE_IN_ARRAY_INITIALIZER: + case SupportedSettings.INSERT_SPACE_BEFORE_FIRST_INITIALIZER: + case SupportedSettings.INSERT_SPACE_AFTER_OPENING_BRACE_IN_ARRAY_INITIALIZER: + case SupportedSettings.INSERT_SPACE_AFTER_CLOSING_PAREN_IN_CAST: + case SupportedSettings.INSERT_SPACE_AFTER_CLOSING_ANGLE_BRACKET_IN_TYPE_ARGUMENTS: + if (value === "true" || value === "false") { + return value; + } + return undefined; + } + return value; + } +} diff --git a/src/formatter-settings/assets/features/formatterSettings/FormatterSettingView.tsx b/src/formatter-settings/assets/features/formatterSettings/FormatterSettingView.tsx index 15843ecc..c83954dd 100644 --- a/src/formatter-settings/assets/features/formatterSettings/FormatterSettingView.tsx +++ b/src/formatter-settings/assets/features/formatterSettings/FormatterSettingView.tsx @@ -18,8 +18,8 @@ const FormatterSettingsView = (): JSX.Element => { const dispatch: Dispatch = useDispatch(); const onClickNaviBar = (element: any) => { const activeCategory: Category = Number(element); - let exampleKind: ExampleKind = ExampleKind.INDENTATION_EXAMPLE; dispatch(changeActiveCategory(activeCategory)); + let exampleKind: ExampleKind = ExampleKind.INDENTATION_EXAMPLE; switch (activeCategory) { case Category.BlankLine: exampleKind = ExampleKind.BLANKLINE_EXAMPLE; diff --git a/src/formatter-settings/index.ts b/src/formatter-settings/index.ts index 08cb560d..c97e9ac9 100644 --- a/src/formatter-settings/index.ts +++ b/src/formatter-settings/index.ts @@ -1,29 +1,52 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. -import * as vscode from "vscode"; +import compareVersions from "compare-versions"; +import * as fse from "fs-extra"; import * as path from "path"; +import * as vscode from "vscode"; +import { instrumentOperation, sendError, sendInfo, setUserError } from "vscode-extension-telemetry-wrapper"; +import { DOMParser, XMLSerializer } from "xmldom"; import { loadTextFromFile } from "../utils"; -import { Example, getSupportedProfileSettings, getSupportedVSCodeSettings, JavaConstants } from "./FormatterConstants"; -import { ExampleKind, JavaFormatterSetting } from "./types"; +import { Example, getDefaultValue, getSupportedProfileSettings, getSupportedVSCodeSettings, JavaConstants, SupportedSettings, VSCodeSettings } from "./FormatterConstants"; +import { FormatterConverter } from "./FormatterConverter"; +import { DOMElement, ExampleKind, JavaFormatterSetting } from "./types"; +import { getProfilePath, getVSCodeSetting, isRemote, getTargetProfilePath } from "./utils"; export class JavaFormatterSettingsEditorProvider implements vscode.CustomTextEditorProvider { public static readonly viewType = "java.formatterSettingsEditor"; private exampleKind: ExampleKind = ExampleKind.INDENTATION_EXAMPLE; private supportedProfileSettings: Map = getSupportedProfileSettings(20); private supportedVSCodeSettings: Map = getSupportedVSCodeSettings(); + private profileElements: Map = new Map(); + private profileSettings: Map = new Map(); + private lastElement: DOMElement | undefined; + private settingsVersion: string = JavaConstants.CURRENT_FORMATTER_SETTINGS_VERSION; + private checkedRequirement: boolean = false; + private checkedProfile: boolean = false; + private diagnosticCollection: vscode.DiagnosticCollection = vscode.languages.createDiagnosticCollection(); constructor(private readonly context: vscode.ExtensionContext) { + vscode.workspace.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(`java.${JavaConstants.SETTINGS_URL_KEY}`) || e.affectsConfiguration(`java.${JavaConstants.SETTINGS_PROFILE_KEY}`)) { + this.checkedProfile = false; + } + }); } - public async showFormatterSettingsEditor() { - // Use a fake profile - const defaultProfile: string = path.join(this.context.extensionPath, "webview-resources", "java-formatter.xml"); - const resource = vscode.Uri.file(defaultProfile); - vscode.commands.executeCommand("vscode.openWith", resource, "java.formatterSettingsEditor"); + public async showFormatterSettingsEditor(): Promise { + if (!await this.checkProfile()) { + return; + } + const settingsUrl = vscode.workspace.getConfiguration("java").get(JavaConstants.SETTINGS_URL_KEY); + if (!settingsUrl) { + return; + } + const filePath = vscode.Uri.file(await getProfilePath(settingsUrl)); + vscode.commands.executeCommand("vscode.openWith", filePath, "java.formatterSettingsEditor"); } - public async resolveCustomTextEditor(_document: vscode.TextDocument, webviewPanel: vscode.WebviewPanel, _token: vscode.CancellationToken): Promise { + public async resolveCustomTextEditor(document: vscode.TextDocument, webviewPanel: vscode.WebviewPanel, _token: vscode.CancellationToken): Promise { webviewPanel.webview.options = { enableScripts: true, @@ -31,57 +54,216 @@ export class JavaFormatterSettingsEditorProvider implements vscode.CustomTextEdi }; const resourceUri = this.context.asAbsolutePath("./out/assets/formatter-settings/index.html"); webviewPanel.webview.html = await loadTextFromFile(resourceUri); - this.exampleKind = ExampleKind.INDENTATION_EXAMPLE; + const changeDocumentSubscription = vscode.workspace.onDidChangeTextDocument(async (e: vscode.TextDocumentChangeEvent) => { + if (e.document.uri.toString() !== document.uri.toString()) { + return; + } + const diagnostics = this.updateProfileSettings(e.document, webviewPanel); + this.diagnosticCollection.set(e.document.uri, diagnostics); + await this.updateVSCodeSettings(webviewPanel); + this.format(webviewPanel); + }); + webviewPanel.onDidDispose(() => { + changeDocumentSubscription.dispose(); + }); webviewPanel.webview.onDidReceiveMessage(async (e) => { switch (e.command) { case "onWillInitialize": - // use default values temporarily - webviewPanel.webview.postMessage({ - command: "loadProfileSetting", - setting: Array.from(this.supportedProfileSettings.values()), - }); - webviewPanel.webview.postMessage({ - command: "loadVSCodeSetting", - setting: Array.from(this.supportedVSCodeSettings.values()), - }); - this.formatWithProfileSettings(webviewPanel); + if (!await this.initialize(document, webviewPanel)) { + webviewPanel.dispose(); + } break; case "onWillChangeExampleKind": this.exampleKind = e.exampleKind; - this.formatWithProfileSettings(webviewPanel); + this.format(webviewPanel); break; case "onWillChangeSetting": - // modify the settings in memory temporarily - for (const entry of this.supportedProfileSettings.entries()) { - if (entry[0] === e.id) { - entry[1].value = e.value; - break; - } + const settingValue: string | undefined = FormatterConverter.webView2ProfileConvert(e.id, e.value.toString()); + if (!settingValue) { + return; } - for (const entry of this.supportedVSCodeSettings.entries()) { - if (entry[0] === e.id) { - entry[1].value = e.value; - break; + if (SupportedSettings.indentationSettings.includes(e.id)) { + const config = vscode.workspace.getConfiguration(undefined, { languageId: "java" }); + if (e.id === SupportedSettings.TABULATION_CHAR) { + const targetValue = (settingValue === "tab") ? false : true; + await config.update(VSCodeSettings.INSERT_SPACES, targetValue, undefined, true); + } else if (e.id === SupportedSettings.TABULATION_SIZE) { + await config.update(VSCodeSettings.TAB_SIZE, Number(settingValue), undefined, true); } + this.profileSettings.set(e.id, settingValue); + } else if (e.id === VSCodeSettings.DETECT_INDENTATION) { + const config = vscode.workspace.getConfiguration(undefined, { languageId: "java" }); + await config.update(VSCodeSettings.DETECT_INDENTATION, (settingValue === "true"), undefined, true); + } else { + await this.modifyProfile(e.id, settingValue, document); } - webviewPanel.webview.postMessage({ - command: "loadProfileSetting", - setting: Array.from(this.supportedProfileSettings.values()), - }); - webviewPanel.webview.postMessage({ - command: "loadVSCodeSetting", - setting: Array.from(this.supportedVSCodeSettings.values()), - }); break; default: break; } }); + await this.checkRequirement(); + } + + private async checkRequirement(): Promise { + if (this.checkedRequirement) { + return true; + } + const javaExt = vscode.extensions.getExtension("redhat.java"); + if (!javaExt) { + vscode.window.showErrorMessage("The extension 'redhat.java' is not installed. Please install it and run this command again."); + const err: Error = new Error("The extension 'redhat.java' is not installed."); + setUserError(err); + sendError(err); + return false; + } + const javaExtVersion: string = javaExt.packageJSON.version; + if (compareVersions(javaExtVersion, JavaConstants.MINIMUM_JAVA_EXTENSION_VERSION) < 0) { + vscode.window.showErrorMessage(`The extension version of 'redhat.java' is too stale. Please install at least ${JavaConstants.MINIMUM_JAVA_EXTENSION_VERSION} and run this command again.`); + const err: Error = new Error(`The extension version of 'redhat.java' (${javaExtVersion}) is too stale.`); + setUserError(err); + sendError(err); + return false; + } + await javaExt.activate(); + this.checkedRequirement = true; + return true; + } + + private async initialize(document: vscode.TextDocument, webviewPanel: vscode.WebviewPanel): Promise { + if (!await this.checkRequirement()) { + return false; + } + if (!await this.checkProfile()) { + return false; + } + this.exampleKind = ExampleKind.INDENTATION_EXAMPLE; + const onDidChangeSetting: vscode.Disposable = vscode.workspace.onDidChangeConfiguration(async (e) => { + if (e.affectsConfiguration(`java.${JavaConstants.SETTINGS_URL_KEY}`) || e.affectsConfiguration(`java.${JavaConstants.SETTINGS_PROFILE_KEY}`)) { + this.onChangeProfile(webviewPanel); + } else if (e.affectsConfiguration(VSCodeSettings.TAB_SIZE) || e.affectsConfiguration(VSCodeSettings.INSERT_SPACES) || e.affectsConfiguration(VSCodeSettings.DETECT_INDENTATION)) { + await this.updateVSCodeSettings(webviewPanel); + this.format(webviewPanel); + } + }); + webviewPanel.onDidDispose(() => { + onDidChangeSetting.dispose(); + }); + const diagnostics = this.updateProfileSettings(document, webviewPanel); + this.diagnosticCollection.set(document.uri, diagnostics); + await this.updateVSCodeSettings(webviewPanel); + this.format(webviewPanel); + return true; + } + + private onChangeProfile(webviewPanel: vscode.WebviewPanel): void { + if (webviewPanel.visible) { + vscode.window.showInformationMessage(`Formatter Profile settings have been changed, do you want to reload this editor?`, + "Yes", "No").then(async (result) => { + if (result === "Yes") { + vscode.commands.executeCommand("workbench.action.webview.reloadWebviewAction"); + } + }); + } + } + + private updateProfileSettings(document: vscode.TextDocument, webviewPanel: vscode.WebviewPanel): vscode.Diagnostic[] { + this.profileElements.clear(); + this.profileSettings.clear(); + this.lastElement = undefined; + this.diagnosticCollection.clear(); + const diagnostics: vscode.Diagnostic[] = []; + const documentDOM = new DOMParser().parseFromString(document.getText()); + this.settingsVersion = documentDOM.documentElement.getAttribute("version") || this.settingsVersion; + const profiles = documentDOM.documentElement.getElementsByTagName("profile"); + if (!profiles || profiles.length === 0) { + diagnostics.push(new vscode.Diagnostic(new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 0)), "No valid profiles found.")); + return diagnostics; + } + const settingsProfileName: string | undefined = vscode.workspace.getConfiguration("java").get(JavaConstants.SETTINGS_PROFILE_KEY); + for (let i = 0; i < profiles.length; i++) { + if (!settingsProfileName || settingsProfileName === profiles[i].getAttribute("name")) { + if (profiles[i].getAttribute("kind") !== "CodeFormatterProfile") { + continue; + } + this.settingsVersion = profiles[i].getAttribute("version") || this.settingsVersion; + const settings = profiles[i].getElementsByTagName("setting"); + for (let j = 0; j < settings.length; j++) { + const setting: DOMElement = settings[j] as DOMElement; + const settingContent: string = new XMLSerializer().serializeToString(setting); + const id = setting.getAttribute("id"); + if (!id) { + diagnostics.push(new vscode.Diagnostic(new vscode.Range(new vscode.Position(setting.lineNumber - 1, setting.columnNumber - 1), new vscode.Position(setting.lineNumber - 1, setting.columnNumber - 1 + settingContent.length)), "The setting has no 'id' property.", vscode.DiagnosticSeverity.Error)); + continue; + } + const value = settings[j].getAttribute("value"); + if (!value) { + diagnostics.push(new vscode.Diagnostic(new vscode.Range(new vscode.Position(setting.lineNumber - 1, setting.columnNumber - 1), new vscode.Position(setting.lineNumber - 1, setting.columnNumber - 1 + settingContent.length)), "The setting has no 'value' property.", vscode.DiagnosticSeverity.Error)); + continue; + } + this.profileElements.set(id, setting); + this.profileSettings.set(id, value); + this.lastElement = setting; + } + break; + } + } + if (!this.profileElements.size) { + diagnostics.push(new vscode.Diagnostic(new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 0)), "No valid settings found in the profile.")); + return diagnostics; + } + this.supportedProfileSettings = getSupportedProfileSettings(Number(this.settingsVersion)); + for (const setting of this.supportedProfileSettings.values()) { + const element = this.profileElements.get(setting.id); + const value = this.profileSettings.get(setting.id); + if (!element || !value) { + continue; + } + const webViewValue: string | undefined = FormatterConverter.profile2WebViewConvert(setting.id, value); + if (!webViewValue) { + const elementContent = new XMLSerializer().serializeToString(element); + const elementRange = new vscode.Range(new vscode.Position(element.lineNumber - 1, element.columnNumber - 1), new vscode.Position(element.lineNumber - 1, element.columnNumber - 1 + elementContent.length)); + diagnostics.push(new vscode.Diagnostic(elementRange, `Invalid value in id: "${setting.id}", "${value}" is not supported.`, vscode.DiagnosticSeverity.Error)); + this.profileSettings.delete(setting.id); + setting.value = FormatterConverter.profile2WebViewConvert(setting.id, getDefaultValue(setting.id))!; + continue; + } + setting.value = webViewValue; + } + webviewPanel.webview.postMessage({ + command: "loadProfileSetting", + setting: Array.from(this.supportedProfileSettings.values()), + }); + return diagnostics; + } + + private async updateVSCodeSettings(webviewPanel: vscode.WebviewPanel): Promise { + this.supportedVSCodeSettings = getSupportedVSCodeSettings(); + for (const setting of this.supportedVSCodeSettings.values()) { + switch (setting.id) { + case SupportedSettings.TABULATION_CHAR: + setting.value = (await getVSCodeSetting(VSCodeSettings.INSERT_SPACES, true) === false) ? "tab" : "space"; + this.profileSettings.set(setting.id, setting.value); + break; + case SupportedSettings.TABULATION_SIZE: + setting.value = String(await getVSCodeSetting(VSCodeSettings.TAB_SIZE, 4)); + this.profileSettings.set(setting.id, setting.value); + break; + case VSCodeSettings.DETECT_INDENTATION: + setting.value = String(await getVSCodeSetting(VSCodeSettings.DETECT_INDENTATION, true)); + break; + default: + return; + } + } + webviewPanel.webview.postMessage({ + command: "loadVSCodeSetting", + setting: Array.from(this.supportedVSCodeSettings.values()), + }); } - private async formatWithProfileSettings(webviewPanel: vscode.WebviewPanel) { - // use default settings temporarily - const content = await vscode.commands.executeCommand("java.execute.workspaceCommand", "java.edit.stringFormatting", Example.getExample(this.exampleKind), JSON.stringify([]), JavaConstants.CURRENT_FORMATTER_SETTINGS_VERSION); + private async format(webviewPanel: vscode.WebviewPanel): Promise { + const content = await vscode.commands.executeCommand("java.execute.workspaceCommand", "java.edit.stringFormatting", Example.getExample(this.exampleKind), JSON.stringify([...this.profileSettings]), this.settingsVersion); if (webviewPanel && webviewPanel.webview) { webviewPanel.webview.postMessage({ command: "formattedContent", @@ -89,11 +271,86 @@ export class JavaFormatterSettingsEditorProvider implements vscode.CustomTextEdi }); } } + + private async modifyProfile(id: string, value: string, document: vscode.TextDocument): Promise { + const profileElement = this.profileElements.get(id); + if (!profileElement) { + // add a new setting not exist in the profile + if (!this.lastElement) { + return; + } + const cloneElement = this.lastElement.cloneNode() as DOMElement; + const originalString: string = new XMLSerializer().serializeToString(cloneElement); + cloneElement.setAttribute("id", id); + cloneElement.setAttribute("value", value); + const edit: vscode.WorkspaceEdit = new vscode.WorkspaceEdit(); + edit.insert(document.uri, new vscode.Position(cloneElement.lineNumber - 1, cloneElement.columnNumber - 1 + originalString.length), ((document.eol === vscode.EndOfLine.LF) ? "\n" : "\r\n") + " ".repeat(cloneElement.columnNumber - 1) + new XMLSerializer().serializeToString(cloneElement)); + vscode.workspace.applyEdit(edit); + } else { + // edit a current setting in the profile + const originalSetting: string = new XMLSerializer().serializeToString(profileElement); + profileElement.setAttribute("value", value); + const edit: vscode.WorkspaceEdit = new vscode.WorkspaceEdit(); + edit.replace(document.uri, new vscode.Range(new vscode.Position(profileElement.lineNumber - 1, profileElement.columnNumber - 1), new vscode.Position(profileElement.lineNumber - 1, profileElement.columnNumber - 1 + originalSetting.length)), new XMLSerializer().serializeToString(profileElement)); + vscode.workspace.applyEdit(edit); + } + } + + private async checkProfile(): Promise { + if (this.checkedProfile) { + return true; + } + const settingsUrl = vscode.workspace.getConfiguration("java").get(JavaConstants.SETTINGS_URL_KEY); + this.checkedProfile = await instrumentOperation("formatter.checkProfileSetting", async (operationId: string) => { + if (!settingsUrl) { + sendInfo(operationId, { formatterProfile: "undefined" }); + await vscode.window.showInformationMessage("No active Formatter Profile found, do you want to create a default one?", + "Yes", "No").then((result) => { + if (result === "Yes") { + this.addDefaultProfile(); + } + }); + return false; + } else { + if (isRemote(settingsUrl)) { + // Will handle remote profile in the next PR + sendInfo(operationId, { formatterProfile: "remote" }); + return false; + } + const profilePath = await getProfilePath(settingsUrl); + if (!(await fse.pathExists(profilePath))) { + sendInfo(operationId, { formatterProfile: "notExist" }); + await vscode.window.showInformationMessage("The active formatter profile does not exist, please check it in the Settings and try again.", + "Open Settings", "Generate a default profile").then((result) => { + if (result === "Open Settings") { + vscode.commands.executeCommand("workbench.action.openSettings", JavaConstants.SETTINGS_URL_KEY); + if (vscode.workspace.workspaceFolders?.length) { + vscode.commands.executeCommand("workbench.action.openWorkspaceSettings"); + } + } else if (result === "Generate a default profile") { + this.addDefaultProfile(); + } + }); + return false; + } + sendInfo(operationId, { formatterProfile: "valid" }); + return true; + } + })(); + return this.checkedProfile; + } + + private async addDefaultProfile(): Promise { + const defaultProfile: string = path.join(this.context.extensionPath, "webview-resources", "java-formatter.xml"); + const profilePath = await getTargetProfilePath(this.context); + await fse.copy(defaultProfile, profilePath); + vscode.commands.executeCommand("vscode.openWith", vscode.Uri.file(profilePath), "java.formatterSettingsEditor"); + } } export let javaFormatterSettingsEditorProvider: JavaFormatterSettingsEditorProvider; export function initFormatterSettingsEditorProvider(context: vscode.ExtensionContext) { javaFormatterSettingsEditorProvider = new JavaFormatterSettingsEditorProvider(context); - context.subscriptions.push(vscode.window.registerCustomEditorProvider(JavaFormatterSettingsEditorProvider.viewType, javaFormatterSettingsEditorProvider, { webviewOptions: {enableFindWidget: true, retainContextWhenHidden: true}})); + context.subscriptions.push(vscode.window.registerCustomEditorProvider(JavaFormatterSettingsEditorProvider.viewType, javaFormatterSettingsEditorProvider, { webviewOptions: { enableFindWidget: true, retainContextWhenHidden: true } })); } diff --git a/src/formatter-settings/types.ts b/src/formatter-settings/types.ts index c6f6c0b3..8592079b 100644 --- a/src/formatter-settings/types.ts +++ b/src/formatter-settings/types.ts @@ -42,3 +42,9 @@ export enum ExampleKind { WHITESPACE_EXAMPLE, WRAPPING_EXAMPLE, } + +// two extra properties from xmldom package, see https://www.npmjs.com/package/xmldom +export interface DOMElement extends Element { + lineNumber: number; + columnNumber: number; +} diff --git a/src/formatter-settings/utils.ts b/src/formatter-settings/utils.ts new file mode 100644 index 00000000..c371d734 --- /dev/null +++ b/src/formatter-settings/utils.ts @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +import * as fse from "fs-extra"; +import * as path from "path"; +import * as vscode from "vscode"; +import { instrumentOperation, sendInfo } from "vscode-extension-telemetry-wrapper"; + +export async function getProfilePath(formatterUrl: string): Promise { + const workspaceFolders = vscode.workspace.workspaceFolders; + if (workspaceFolders?.length && !path.isAbsolute(formatterUrl)) { + for (const workspaceFolder of workspaceFolders) { + const filePath = path.resolve(workspaceFolder.uri.fsPath, formatterUrl); + if (await fse.pathExists(filePath)) { + return filePath; + } + } + } + return path.resolve(formatterUrl); +} + +export async function getVSCodeSetting(setting: string, defaultValue: any): Promise { + return await instrumentOperation("formatter.getSetting", async (operationId: string) => { + const config = vscode.workspace.getConfiguration(undefined, { languageId: "java" }); + let result = config.get(setting); + if (result === undefined) { + result = vscode.workspace.getConfiguration().get(setting); + } + if (result === undefined) { + sendInfo(operationId, { notFoundSetting: setting }); + return defaultValue; + } + return result; + })(); +} + +export function isRemote(path: string): boolean { + return path !== null && path.startsWith("http:/") || path.startsWith("https:/"); +} + +export async function getTargetProfilePath(context: vscode.ExtensionContext, fileName?: string): Promise { + const targetFileName = fileName || "java-formatter.xml"; + let profilePath: string; + const workspaceFolder = vscode.workspace.workspaceFolders; + if (workspaceFolder?.length) { + profilePath = path.posix.join(workspaceFolder[0].uri.fsPath, ".vscode", targetFileName); + } else { + const folder: string = context.globalStorageUri.fsPath; + await fse.ensureDir(folder); + profilePath = path.posix.join(folder, targetFileName); + } + // bug: https://github.com/redhat-developer/vscode-java/issues/1944, only the profiles in posix path can be monitored when changes, so we use posix path for default profile creation temporarily. + const relativePath = toPosixPath(path.join(".vscode", targetFileName)); + profilePath = toPosixPath(profilePath); + await vscode.workspace.getConfiguration("java").update("format.settings.url", (workspaceFolder?.length ? relativePath : profilePath), !(workspaceFolder?.length)); + return profilePath; +} + +function toPosixPath(inputPath: string): string { + return inputPath.split(path.win32.sep).join(path.posix.sep); +} From 2379f3173c59c61b106fb3b72125d8ab6ba13a23 Mon Sep 17 00:00:00 2001 From: Shi Chen Date: Thu, 20 May 2021 10:22:06 +0800 Subject: [PATCH 02/12] isolate parse logics and address comments --- .../formatterSettings/components/Setting.tsx | 1 + src/formatter-settings/index.ts | 200 +++++++----------- src/formatter-settings/types.ts | 10 + src/formatter-settings/utils.ts | 96 ++++++++- 4 files changed, 178 insertions(+), 129 deletions(-) diff --git a/src/formatter-settings/assets/features/formatterSettings/components/Setting.tsx b/src/formatter-settings/assets/features/formatterSettings/components/Setting.tsx index 9f3f2fb0..8d0348d7 100644 --- a/src/formatter-settings/assets/features/formatterSettings/components/Setting.tsx +++ b/src/formatter-settings/assets/features/formatterSettings/components/Setting.tsx @@ -30,6 +30,7 @@ const Setting = (): JSX.Element => { if (!value || value === "") { value = "0"; } + value = Number(value); onWillChangeSetting(id, value); }; diff --git a/src/formatter-settings/index.ts b/src/formatter-settings/index.ts index c97e9ac9..8389029a 100644 --- a/src/formatter-settings/index.ts +++ b/src/formatter-settings/index.ts @@ -6,18 +6,16 @@ import * as fse from "fs-extra"; import * as path from "path"; import * as vscode from "vscode"; import { instrumentOperation, sendError, sendInfo, setUserError } from "vscode-extension-telemetry-wrapper"; -import { DOMParser, XMLSerializer } from "xmldom"; +import { XMLSerializer } from "xmldom"; import { loadTextFromFile } from "../utils"; -import { Example, getDefaultValue, getSupportedProfileSettings, getSupportedVSCodeSettings, JavaConstants, SupportedSettings, VSCodeSettings } from "./FormatterConstants"; +import { Example, getSupportedVSCodeSettings, JavaConstants, SupportedSettings, VSCodeSettings } from "./FormatterConstants"; import { FormatterConverter } from "./FormatterConverter"; -import { DOMElement, ExampleKind, JavaFormatterSetting } from "./types"; -import { getProfilePath, getVSCodeSetting, isRemote, getTargetProfilePath } from "./utils"; +import { DOMElement, ExampleKind, ProfileContent } from "./types"; +import { getProfilePath, getVSCodeSetting, isRemote, getTargetProfilePath, parseProfile } from "./utils"; export class JavaFormatterSettingsEditorProvider implements vscode.CustomTextEditorProvider { public static readonly viewType = "java.formatterSettingsEditor"; private exampleKind: ExampleKind = ExampleKind.INDENTATION_EXAMPLE; - private supportedProfileSettings: Map = getSupportedProfileSettings(20); - private supportedVSCodeSettings: Map = getSupportedVSCodeSettings(); private profileElements: Map = new Map(); private profileSettings: Map = new Map(); private lastElement: DOMElement | undefined; @@ -25,57 +23,64 @@ export class JavaFormatterSettingsEditorProvider implements vscode.CustomTextEdi private checkedRequirement: boolean = false; private checkedProfile: boolean = false; private diagnosticCollection: vscode.DiagnosticCollection = vscode.languages.createDiagnosticCollection(); + private settingsUrl: string | undefined = vscode.workspace.getConfiguration("java").get(JavaConstants.SETTINGS_URL_KEY); + private webviewPanel: vscode.WebviewPanel | undefined; constructor(private readonly context: vscode.ExtensionContext) { vscode.workspace.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(`java.${JavaConstants.SETTINGS_URL_KEY}`) || e.affectsConfiguration(`java.${JavaConstants.SETTINGS_PROFILE_KEY}`)) { + if (e.affectsConfiguration(`java.${JavaConstants.SETTINGS_URL_KEY}`)) { + this.checkedProfile = false; + this.settingsUrl = vscode.workspace.getConfiguration("java").get(JavaConstants.SETTINGS_URL_KEY); + } else if (e.affectsConfiguration(`java.${JavaConstants.SETTINGS_PROFILE_KEY}`)) { this.checkedProfile = false; } }); + vscode.workspace.onDidChangeTextDocument(async (e: vscode.TextDocumentChangeEvent) => { + if (!this.settingsUrl || e.document.uri.toString() !== vscode.Uri.file(await getProfilePath(this.settingsUrl)).toString()) { + return; + } + await this.parseProfileAndUpdate(e.document); + }); } public async showFormatterSettingsEditor(): Promise { - if (!await this.checkProfile()) { - return; - } - const settingsUrl = vscode.workspace.getConfiguration("java").get(JavaConstants.SETTINGS_URL_KEY); - if (!settingsUrl) { + if (!await this.checkProfile() || !this.settingsUrl) { return; } - const filePath = vscode.Uri.file(await getProfilePath(settingsUrl)); + const filePath = vscode.Uri.file(await getProfilePath(this.settingsUrl)); vscode.commands.executeCommand("vscode.openWith", filePath, "java.formatterSettingsEditor"); } public async resolveCustomTextEditor(document: vscode.TextDocument, webviewPanel: vscode.WebviewPanel, _token: vscode.CancellationToken): Promise { + // restrict one webviewpanel only + if (this.webviewPanel) { + vscode.commands.executeCommand("vscode.open", document.uri); + webviewPanel.dispose(); + return; + } else { + this.webviewPanel = webviewPanel; + } + webviewPanel.webview.options = { enableScripts: true, enableCommandUris: true, }; + this.webviewPanel.onDidDispose(() => { + this.webviewPanel = undefined; + }) const resourceUri = this.context.asAbsolutePath("./out/assets/formatter-settings/index.html"); - webviewPanel.webview.html = await loadTextFromFile(resourceUri); - const changeDocumentSubscription = vscode.workspace.onDidChangeTextDocument(async (e: vscode.TextDocumentChangeEvent) => { - if (e.document.uri.toString() !== document.uri.toString()) { - return; - } - const diagnostics = this.updateProfileSettings(e.document, webviewPanel); - this.diagnosticCollection.set(e.document.uri, diagnostics); - await this.updateVSCodeSettings(webviewPanel); - this.format(webviewPanel); - }); - webviewPanel.onDidDispose(() => { - changeDocumentSubscription.dispose(); - }); - webviewPanel.webview.onDidReceiveMessage(async (e) => { + this.webviewPanel.webview.html = await loadTextFromFile(resourceUri); + this.webviewPanel.webview.onDidReceiveMessage(async (e) => { switch (e.command) { case "onWillInitialize": - if (!await this.initialize(document, webviewPanel)) { - webviewPanel.dispose(); + if (!await this.initialize(document)) { + this.webviewPanel?.dispose(); } break; case "onWillChangeExampleKind": this.exampleKind = e.exampleKind; - this.format(webviewPanel); + this.format(); break; case "onWillChangeSetting": const settingValue: string | undefined = FormatterConverter.webView2ProfileConvert(e.id, e.value.toString()); @@ -130,7 +135,7 @@ export class JavaFormatterSettingsEditorProvider implements vscode.CustomTextEdi return true; } - private async initialize(document: vscode.TextDocument, webviewPanel: vscode.WebviewPanel): Promise { + private async initialize(document: vscode.TextDocument): Promise { if (!await this.checkRequirement()) { return false; } @@ -140,24 +145,40 @@ export class JavaFormatterSettingsEditorProvider implements vscode.CustomTextEdi this.exampleKind = ExampleKind.INDENTATION_EXAMPLE; const onDidChangeSetting: vscode.Disposable = vscode.workspace.onDidChangeConfiguration(async (e) => { if (e.affectsConfiguration(`java.${JavaConstants.SETTINGS_URL_KEY}`) || e.affectsConfiguration(`java.${JavaConstants.SETTINGS_PROFILE_KEY}`)) { - this.onChangeProfile(webviewPanel); + this.onChangeProfile(); } else if (e.affectsConfiguration(VSCodeSettings.TAB_SIZE) || e.affectsConfiguration(VSCodeSettings.INSERT_SPACES) || e.affectsConfiguration(VSCodeSettings.DETECT_INDENTATION)) { - await this.updateVSCodeSettings(webviewPanel); - this.format(webviewPanel); + await this.updateVSCodeSettings(); + this.format(); } }); - webviewPanel.onDidDispose(() => { + this.webviewPanel?.onDidDispose(() => { onDidChangeSetting.dispose(); }); - const diagnostics = this.updateProfileSettings(document, webviewPanel); - this.diagnosticCollection.set(document.uri, diagnostics); - await this.updateVSCodeSettings(webviewPanel); - this.format(webviewPanel); + await this.parseProfileAndUpdate(document); return true; } - private onChangeProfile(webviewPanel: vscode.WebviewPanel): void { - if (webviewPanel.visible) { + private async parseProfileAndUpdate(document: vscode.TextDocument): Promise { + const content: ProfileContent = parseProfile(document); + this.diagnosticCollection.set(document.uri, content.diagnostics); + if (this.webviewPanel) { + this.profileElements = content.profileElements || this.profileElements; + this.profileSettings = content.profileSettings || this.profileSettings; + this.lastElement = content.lastElement || this.lastElement; + this.settingsVersion = content.settingsVersion; + if (content.supportedProfileSettings) { + this.webviewPanel.webview.postMessage({ + command: "loadProfileSetting", + setting: Array.from(content.supportedProfileSettings.values()), + }); + } + await this.updateVSCodeSettings(); + this.format(); + } + } + + private onChangeProfile(): void { + if (this.webviewPanel?.visible) { vscode.window.showInformationMessage(`Formatter Profile settings have been changed, do you want to reload this editor?`, "Yes", "No").then(async (result) => { if (result === "Yes") { @@ -167,79 +188,9 @@ export class JavaFormatterSettingsEditorProvider implements vscode.CustomTextEdi } } - private updateProfileSettings(document: vscode.TextDocument, webviewPanel: vscode.WebviewPanel): vscode.Diagnostic[] { - this.profileElements.clear(); - this.profileSettings.clear(); - this.lastElement = undefined; - this.diagnosticCollection.clear(); - const diagnostics: vscode.Diagnostic[] = []; - const documentDOM = new DOMParser().parseFromString(document.getText()); - this.settingsVersion = documentDOM.documentElement.getAttribute("version") || this.settingsVersion; - const profiles = documentDOM.documentElement.getElementsByTagName("profile"); - if (!profiles || profiles.length === 0) { - diagnostics.push(new vscode.Diagnostic(new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 0)), "No valid profiles found.")); - return diagnostics; - } - const settingsProfileName: string | undefined = vscode.workspace.getConfiguration("java").get(JavaConstants.SETTINGS_PROFILE_KEY); - for (let i = 0; i < profiles.length; i++) { - if (!settingsProfileName || settingsProfileName === profiles[i].getAttribute("name")) { - if (profiles[i].getAttribute("kind") !== "CodeFormatterProfile") { - continue; - } - this.settingsVersion = profiles[i].getAttribute("version") || this.settingsVersion; - const settings = profiles[i].getElementsByTagName("setting"); - for (let j = 0; j < settings.length; j++) { - const setting: DOMElement = settings[j] as DOMElement; - const settingContent: string = new XMLSerializer().serializeToString(setting); - const id = setting.getAttribute("id"); - if (!id) { - diagnostics.push(new vscode.Diagnostic(new vscode.Range(new vscode.Position(setting.lineNumber - 1, setting.columnNumber - 1), new vscode.Position(setting.lineNumber - 1, setting.columnNumber - 1 + settingContent.length)), "The setting has no 'id' property.", vscode.DiagnosticSeverity.Error)); - continue; - } - const value = settings[j].getAttribute("value"); - if (!value) { - diagnostics.push(new vscode.Diagnostic(new vscode.Range(new vscode.Position(setting.lineNumber - 1, setting.columnNumber - 1), new vscode.Position(setting.lineNumber - 1, setting.columnNumber - 1 + settingContent.length)), "The setting has no 'value' property.", vscode.DiagnosticSeverity.Error)); - continue; - } - this.profileElements.set(id, setting); - this.profileSettings.set(id, value); - this.lastElement = setting; - } - break; - } - } - if (!this.profileElements.size) { - diagnostics.push(new vscode.Diagnostic(new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 0)), "No valid settings found in the profile.")); - return diagnostics; - } - this.supportedProfileSettings = getSupportedProfileSettings(Number(this.settingsVersion)); - for (const setting of this.supportedProfileSettings.values()) { - const element = this.profileElements.get(setting.id); - const value = this.profileSettings.get(setting.id); - if (!element || !value) { - continue; - } - const webViewValue: string | undefined = FormatterConverter.profile2WebViewConvert(setting.id, value); - if (!webViewValue) { - const elementContent = new XMLSerializer().serializeToString(element); - const elementRange = new vscode.Range(new vscode.Position(element.lineNumber - 1, element.columnNumber - 1), new vscode.Position(element.lineNumber - 1, element.columnNumber - 1 + elementContent.length)); - diagnostics.push(new vscode.Diagnostic(elementRange, `Invalid value in id: "${setting.id}", "${value}" is not supported.`, vscode.DiagnosticSeverity.Error)); - this.profileSettings.delete(setting.id); - setting.value = FormatterConverter.profile2WebViewConvert(setting.id, getDefaultValue(setting.id))!; - continue; - } - setting.value = webViewValue; - } - webviewPanel.webview.postMessage({ - command: "loadProfileSetting", - setting: Array.from(this.supportedProfileSettings.values()), - }); - return diagnostics; - } - - private async updateVSCodeSettings(webviewPanel: vscode.WebviewPanel): Promise { - this.supportedVSCodeSettings = getSupportedVSCodeSettings(); - for (const setting of this.supportedVSCodeSettings.values()) { + private async updateVSCodeSettings(): Promise { + const supportedVSCodeSettings = getSupportedVSCodeSettings(); + for (const setting of supportedVSCodeSettings.values()) { switch (setting.id) { case SupportedSettings.TABULATION_CHAR: setting.value = (await getVSCodeSetting(VSCodeSettings.INSERT_SPACES, true) === false) ? "tab" : "space"; @@ -256,16 +207,16 @@ export class JavaFormatterSettingsEditorProvider implements vscode.CustomTextEdi return; } } - webviewPanel.webview.postMessage({ + this.webviewPanel?.webview.postMessage({ command: "loadVSCodeSetting", - setting: Array.from(this.supportedVSCodeSettings.values()), + setting: Array.from(supportedVSCodeSettings.values()), }); } - private async format(webviewPanel: vscode.WebviewPanel): Promise { + private async format(): Promise { const content = await vscode.commands.executeCommand("java.execute.workspaceCommand", "java.edit.stringFormatting", Example.getExample(this.exampleKind), JSON.stringify([...this.profileSettings]), this.settingsVersion); - if (webviewPanel && webviewPanel.webview) { - webviewPanel.webview.postMessage({ + if (this.webviewPanel?.webview) { + this.webviewPanel.webview.postMessage({ command: "formattedContent", content: content, }); @@ -285,14 +236,14 @@ export class JavaFormatterSettingsEditorProvider implements vscode.CustomTextEdi cloneElement.setAttribute("value", value); const edit: vscode.WorkspaceEdit = new vscode.WorkspaceEdit(); edit.insert(document.uri, new vscode.Position(cloneElement.lineNumber - 1, cloneElement.columnNumber - 1 + originalString.length), ((document.eol === vscode.EndOfLine.LF) ? "\n" : "\r\n") + " ".repeat(cloneElement.columnNumber - 1) + new XMLSerializer().serializeToString(cloneElement)); - vscode.workspace.applyEdit(edit); + await vscode.workspace.applyEdit(edit); } else { // edit a current setting in the profile const originalSetting: string = new XMLSerializer().serializeToString(profileElement); profileElement.setAttribute("value", value); const edit: vscode.WorkspaceEdit = new vscode.WorkspaceEdit(); edit.replace(document.uri, new vscode.Range(new vscode.Position(profileElement.lineNumber - 1, profileElement.columnNumber - 1), new vscode.Position(profileElement.lineNumber - 1, profileElement.columnNumber - 1 + originalSetting.length)), new XMLSerializer().serializeToString(profileElement)); - vscode.workspace.applyEdit(edit); + await vscode.workspace.applyEdit(edit); } } @@ -300,9 +251,8 @@ export class JavaFormatterSettingsEditorProvider implements vscode.CustomTextEdi if (this.checkedProfile) { return true; } - const settingsUrl = vscode.workspace.getConfiguration("java").get(JavaConstants.SETTINGS_URL_KEY); this.checkedProfile = await instrumentOperation("formatter.checkProfileSetting", async (operationId: string) => { - if (!settingsUrl) { + if (!this.settingsUrl) { sendInfo(operationId, { formatterProfile: "undefined" }); await vscode.window.showInformationMessage("No active Formatter Profile found, do you want to create a default one?", "Yes", "No").then((result) => { @@ -312,12 +262,12 @@ export class JavaFormatterSettingsEditorProvider implements vscode.CustomTextEdi }); return false; } else { - if (isRemote(settingsUrl)) { + if (isRemote(this.settingsUrl)) { // Will handle remote profile in the next PR sendInfo(operationId, { formatterProfile: "remote" }); return false; } - const profilePath = await getProfilePath(settingsUrl); + const profilePath = await getProfilePath(this.settingsUrl); if (!(await fse.pathExists(profilePath))) { sendInfo(operationId, { formatterProfile: "notExist" }); await vscode.window.showInformationMessage("The active formatter profile does not exist, please check it in the Settings and try again.", diff --git a/src/formatter-settings/types.ts b/src/formatter-settings/types.ts index 8592079b..64fa946c 100644 --- a/src/formatter-settings/types.ts +++ b/src/formatter-settings/types.ts @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. +import * as vscode from "vscode"; export interface JavaFormatterSetting { id: string; name: string; @@ -48,3 +49,12 @@ export interface DOMElement extends Element { lineNumber: number; columnNumber: number; } + +export interface ProfileContent { + profileElements?: Map, + profileSettings?: Map, + lastElement?: DOMElement, + supportedProfileSettings?: Map + settingsVersion: string, + diagnostics: vscode.Diagnostic[], +} diff --git a/src/formatter-settings/utils.ts b/src/formatter-settings/utils.ts index c371d734..c21c93d0 100644 --- a/src/formatter-settings/utils.ts +++ b/src/formatter-settings/utils.ts @@ -5,6 +5,10 @@ import * as fse from "fs-extra"; import * as path from "path"; import * as vscode from "vscode"; import { instrumentOperation, sendInfo } from "vscode-extension-telemetry-wrapper"; +import { DOMElement, ProfileContent } from "./types"; +import { DOMParser, XMLSerializer } from "xmldom"; +import { getDefaultValue, getSupportedProfileSettings, JavaConstants } from "./FormatterConstants"; +import { FormatterConverter } from "./FormatterConverter"; export async function getProfilePath(formatterUrl: string): Promise { const workspaceFolders = vscode.workspace.workspaceFolders; @@ -22,10 +26,7 @@ export async function getProfilePath(formatterUrl: string): Promise { export async function getVSCodeSetting(setting: string, defaultValue: any): Promise { return await instrumentOperation("formatter.getSetting", async (operationId: string) => { const config = vscode.workspace.getConfiguration(undefined, { languageId: "java" }); - let result = config.get(setting); - if (result === undefined) { - result = vscode.workspace.getConfiguration().get(setting); - } + let result = config.get(setting) ?? vscode.workspace.getConfiguration().get(setting); if (result === undefined) { sendInfo(operationId, { notFoundSetting: setting }); return defaultValue; @@ -59,3 +60,90 @@ export async function getTargetProfilePath(context: vscode.ExtensionContext, fil function toPosixPath(inputPath: string): string { return inputPath.split(path.win32.sep).join(path.posix.sep); } + +export function parseProfile(document: vscode.TextDocument): ProfileContent { + const profileElements = new Map(); + const profileSettings = new Map(); + let lastElement = undefined; + const diagnostics: vscode.Diagnostic[] = []; + const documentDOM = new DOMParser({ + locator: {}, errorHandler: (_level, msg) => { + const bracketExp: RegExp = new RegExp("\\[(.*?)\\]", "g"); + const result: RegExpMatchArray = msg.match(bracketExp); + if (result && result.length) { + const lineExp: RegExp = new RegExp("line:(\\d*)"); + const colExp: RegExp = new RegExp("col:(\\d*)"); + const line = result[result.length - 1].match(lineExp); + const column = result[result.length - 1].match(colExp); + if (line && line.length === 2 && column && column.length === 2) { + diagnostics.push(new vscode.Diagnostic(new vscode.Range(new vscode.Position(Number(line[1]) - 1, Number(column[1]) - 1), new vscode.Position(Number(line[1]) - 1, Number(column[1]))), msg)); + } + } + } + }).parseFromString(document.getText()); + let settingsVersion = documentDOM.documentElement.getAttribute("version") || JavaConstants.CURRENT_FORMATTER_SETTINGS_VERSION; + const profiles = documentDOM.documentElement.getElementsByTagName("profile"); + if (!profiles || profiles.length === 0) { + diagnostics.push(new vscode.Diagnostic(new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 0)), "No valid profiles found.")); + return { settingsVersion: settingsVersion, diagnostics: diagnostics }; + } + const settingsProfileName: string | undefined = vscode.workspace.getConfiguration("java").get(JavaConstants.SETTINGS_PROFILE_KEY); + for (let i = 0; i < profiles.length; i++) { + if (!settingsProfileName || settingsProfileName === profiles[i].getAttribute("name")) { + if (profiles[i].getAttribute("kind") !== "CodeFormatterProfile") { + continue; + } + settingsVersion = profiles[i].getAttribute("version") || settingsVersion; + const settings = profiles[i].getElementsByTagName("setting"); + for (let j = 0; j < settings.length; j++) { + const setting: DOMElement = settings[j] as DOMElement; + const settingContent: string = new XMLSerializer().serializeToString(setting); + const id = setting.getAttribute("id"); + if (!id) { + diagnostics.push(new vscode.Diagnostic(new vscode.Range(new vscode.Position(setting.lineNumber - 1, setting.columnNumber - 1), new vscode.Position(setting.lineNumber - 1, setting.columnNumber - 1 + settingContent.length)), "The setting has no 'id' property.", vscode.DiagnosticSeverity.Error)); + continue; + } + const value = settings[j].getAttribute("value"); + if (!value) { + diagnostics.push(new vscode.Diagnostic(new vscode.Range(new vscode.Position(setting.lineNumber - 1, setting.columnNumber - 1), new vscode.Position(setting.lineNumber - 1, setting.columnNumber - 1 + settingContent.length)), "The setting has no 'value' property.", vscode.DiagnosticSeverity.Error)); + continue; + } + profileElements.set(id, setting); + profileSettings.set(id, value); + lastElement = setting; + } + break; + } + } + if (!profileElements.size) { + diagnostics.push(new vscode.Diagnostic(new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 0)), "No valid settings found in the profile.")); + return { settingsVersion: settingsVersion, diagnostics: diagnostics }; + } + const supportedProfileSettings = getSupportedProfileSettings(Number(settingsVersion)); + for (const setting of supportedProfileSettings.values()) { + const element = profileElements.get(setting.id); + const value = profileSettings.get(setting.id); + if (!element || !value) { + setting.value = FormatterConverter.profile2WebViewConvert(setting.id, getDefaultValue(setting.id))!; + continue; + } + const webViewValue: string | undefined = FormatterConverter.profile2WebViewConvert(setting.id, value); + if (!webViewValue) { + const elementContent = new XMLSerializer().serializeToString(element); + const elementRange = new vscode.Range(new vscode.Position(element.lineNumber - 1, element.columnNumber - 1), new vscode.Position(element.lineNumber - 1, element.columnNumber - 1 + elementContent.length)); + diagnostics.push(new vscode.Diagnostic(elementRange, `Invalid value in id: "${setting.id}", "${value}" is not supported.`, vscode.DiagnosticSeverity.Error)); + profileSettings.delete(setting.id); + setting.value = FormatterConverter.profile2WebViewConvert(setting.id, getDefaultValue(setting.id))!; + continue; + } + setting.value = webViewValue; + } + return { + profileElements: profileElements, + profileSettings: profileSettings, + lastElement: lastElement, + supportedProfileSettings: supportedProfileSettings, + settingsVersion: settingsVersion, + diagnostics: diagnostics + } +} From de6d96786af2b07762b78c746a117724ca63f172 Mon Sep 17 00:00:00 2001 From: Shi Chen Date: Thu, 20 May 2021 17:11:48 +0800 Subject: [PATCH 03/12] address comments --- .../formatterSettings/components/Setting.tsx | 4 - src/formatter-settings/index.ts | 118 +++++++++--------- src/formatter-settings/types.ts | 9 +- src/formatter-settings/utils.ts | 61 +++++---- 4 files changed, 94 insertions(+), 98 deletions(-) diff --git a/src/formatter-settings/assets/features/formatterSettings/components/Setting.tsx b/src/formatter-settings/assets/features/formatterSettings/components/Setting.tsx index 8d0348d7..ecae9d77 100644 --- a/src/formatter-settings/assets/features/formatterSettings/components/Setting.tsx +++ b/src/formatter-settings/assets/features/formatterSettings/components/Setting.tsx @@ -27,10 +27,6 @@ const Setting = (): JSX.Element => { const handleChangeInput = (e: any) => { const id = e.target.id; let value = e.target.value; - if (!value || value === "") { - value = "0"; - } - value = Number(value); onWillChangeSetting(id, value); }; diff --git a/src/formatter-settings/index.ts b/src/formatter-settings/index.ts index 8389029a..b1796a7f 100644 --- a/src/formatter-settings/index.ts +++ b/src/formatter-settings/index.ts @@ -8,7 +8,7 @@ import * as vscode from "vscode"; import { instrumentOperation, sendError, sendInfo, setUserError } from "vscode-extension-telemetry-wrapper"; import { XMLSerializer } from "xmldom"; import { loadTextFromFile } from "../utils"; -import { Example, getSupportedVSCodeSettings, JavaConstants, SupportedSettings, VSCodeSettings } from "./FormatterConstants"; +import { Example, getDefaultValue, getSupportedVSCodeSettings, JavaConstants, SupportedSettings, VSCodeSettings } from "./FormatterConstants"; import { FormatterConverter } from "./FormatterConverter"; import { DOMElement, ExampleKind, ProfileContent } from "./types"; import { getProfilePath, getVSCodeSetting, isRemote, getTargetProfilePath, parseProfile } from "./utils"; @@ -21,22 +21,30 @@ export class JavaFormatterSettingsEditorProvider implements vscode.CustomTextEdi private lastElement: DOMElement | undefined; private settingsVersion: string = JavaConstants.CURRENT_FORMATTER_SETTINGS_VERSION; private checkedRequirement: boolean = false; - private checkedProfile: boolean = false; + private checkedProfileSettings: boolean = false; private diagnosticCollection: vscode.DiagnosticCollection = vscode.languages.createDiagnosticCollection(); private settingsUrl: string | undefined = vscode.workspace.getConfiguration("java").get(JavaConstants.SETTINGS_URL_KEY); private webviewPanel: vscode.WebviewPanel | undefined; + private profilePath: string = ""; constructor(private readonly context: vscode.ExtensionContext) { - vscode.workspace.onDidChangeConfiguration(e => { + vscode.workspace.onDidChangeConfiguration(async (e) => { + if (e.affectsConfiguration(`java.${JavaConstants.SETTINGS_URL_KEY}`) || e.affectsConfiguration(`java.${JavaConstants.SETTINGS_PROFILE_KEY}`)) { + this.checkedProfileSettings = false; + this.onChangeProfileSettings(); + } else if (this.webviewPanel && (e.affectsConfiguration(VSCodeSettings.TAB_SIZE) || e.affectsConfiguration(VSCodeSettings.INSERT_SPACES) || e.affectsConfiguration(VSCodeSettings.DETECT_INDENTATION))) { + await this.updateVSCodeSettings(); + this.format(); + } if (e.affectsConfiguration(`java.${JavaConstants.SETTINGS_URL_KEY}`)) { - this.checkedProfile = false; this.settingsUrl = vscode.workspace.getConfiguration("java").get(JavaConstants.SETTINGS_URL_KEY); - } else if (e.affectsConfiguration(`java.${JavaConstants.SETTINGS_PROFILE_KEY}`)) { - this.checkedProfile = false; + if (this.settingsUrl) { + this.profilePath = await getProfilePath(this.settingsUrl); + } } }); vscode.workspace.onDidChangeTextDocument(async (e: vscode.TextDocumentChangeEvent) => { - if (!this.settingsUrl || e.document.uri.toString() !== vscode.Uri.file(await getProfilePath(this.settingsUrl)).toString()) { + if (!this.settingsUrl || e.document.uri.toString() !== vscode.Uri.file(this.profilePath).toString()) { return; } await this.parseProfileAndUpdate(e.document); @@ -44,10 +52,10 @@ export class JavaFormatterSettingsEditorProvider implements vscode.CustomTextEdi } public async showFormatterSettingsEditor(): Promise { - if (!await this.checkProfile() || !this.settingsUrl) { + if (!await this.checkProfileSettings() || !this.settingsUrl) { return; } - const filePath = vscode.Uri.file(await getProfilePath(this.settingsUrl)); + const filePath = vscode.Uri.file(this.profilePath); vscode.commands.executeCommand("vscode.openWith", filePath, "java.formatterSettingsEditor"); } @@ -139,21 +147,10 @@ export class JavaFormatterSettingsEditorProvider implements vscode.CustomTextEdi if (!await this.checkRequirement()) { return false; } - if (!await this.checkProfile()) { + if (!await this.checkProfileSettings()) { return false; } this.exampleKind = ExampleKind.INDENTATION_EXAMPLE; - const onDidChangeSetting: vscode.Disposable = vscode.workspace.onDidChangeConfiguration(async (e) => { - if (e.affectsConfiguration(`java.${JavaConstants.SETTINGS_URL_KEY}`) || e.affectsConfiguration(`java.${JavaConstants.SETTINGS_PROFILE_KEY}`)) { - this.onChangeProfile(); - } else if (e.affectsConfiguration(VSCodeSettings.TAB_SIZE) || e.affectsConfiguration(VSCodeSettings.INSERT_SPACES) || e.affectsConfiguration(VSCodeSettings.DETECT_INDENTATION)) { - await this.updateVSCodeSettings(); - this.format(); - } - }); - this.webviewPanel?.onDidDispose(() => { - onDidChangeSetting.dispose(); - }); await this.parseProfileAndUpdate(document); return true; } @@ -177,7 +174,7 @@ export class JavaFormatterSettingsEditorProvider implements vscode.CustomTextEdi } } - private onChangeProfile(): void { + private onChangeProfileSettings(): void { if (this.webviewPanel?.visible) { vscode.window.showInformationMessage(`Formatter Profile settings have been changed, do you want to reload this editor?`, "Yes", "No").then(async (result) => { @@ -225,6 +222,10 @@ export class JavaFormatterSettingsEditorProvider implements vscode.CustomTextEdi private async modifyProfile(id: string, value: string, document: vscode.TextDocument): Promise { const profileElement = this.profileElements.get(id); + const fixedValue = value || getDefaultValue(id); + if (!fixedValue) { + return; + } if (!profileElement) { // add a new setting not exist in the profile if (!this.lastElement) { @@ -233,62 +234,61 @@ export class JavaFormatterSettingsEditorProvider implements vscode.CustomTextEdi const cloneElement = this.lastElement.cloneNode() as DOMElement; const originalString: string = new XMLSerializer().serializeToString(cloneElement); cloneElement.setAttribute("id", id); - cloneElement.setAttribute("value", value); + cloneElement.setAttribute("value", fixedValue); const edit: vscode.WorkspaceEdit = new vscode.WorkspaceEdit(); edit.insert(document.uri, new vscode.Position(cloneElement.lineNumber - 1, cloneElement.columnNumber - 1 + originalString.length), ((document.eol === vscode.EndOfLine.LF) ? "\n" : "\r\n") + " ".repeat(cloneElement.columnNumber - 1) + new XMLSerializer().serializeToString(cloneElement)); await vscode.workspace.applyEdit(edit); } else { // edit a current setting in the profile const originalSetting: string = new XMLSerializer().serializeToString(profileElement); - profileElement.setAttribute("value", value); + profileElement.setAttribute("value", fixedValue); const edit: vscode.WorkspaceEdit = new vscode.WorkspaceEdit(); edit.replace(document.uri, new vscode.Range(new vscode.Position(profileElement.lineNumber - 1, profileElement.columnNumber - 1), new vscode.Position(profileElement.lineNumber - 1, profileElement.columnNumber - 1 + originalSetting.length)), new XMLSerializer().serializeToString(profileElement)); await vscode.workspace.applyEdit(edit); } } - private async checkProfile(): Promise { - if (this.checkedProfile) { + private checkProfileSettings = instrumentOperation("formatter.checkProfileSetting", async (operationId: string) => { + if (this.checkedProfileSettings) { return true; } - this.checkedProfile = await instrumentOperation("formatter.checkProfileSetting", async (operationId: string) => { - if (!this.settingsUrl) { - sendInfo(operationId, { formatterProfile: "undefined" }); - await vscode.window.showInformationMessage("No active Formatter Profile found, do you want to create a default one?", - "Yes", "No").then((result) => { - if (result === "Yes") { + if (!this.settingsUrl) { + sendInfo(operationId, { formatterProfile: "undefined" }); + await vscode.window.showInformationMessage("No active Formatter Profile found, do you want to create a default one?", + "Yes", "No").then((result) => { + if (result === "Yes") { + this.addDefaultProfile(); + } + }); + } else { + if (isRemote(this.settingsUrl)) { + // Will handle remote profile in the next PR + sendInfo(operationId, { formatterProfile: "remote" }); + return false; + } + if (!this.profilePath) { + this.profilePath = await getProfilePath(this.settingsUrl); + } + if (!(await fse.pathExists(this.profilePath))) { + sendInfo(operationId, { formatterProfile: "notExist" }); + await vscode.window.showInformationMessage("The active formatter profile does not exist, please check it in the Settings and try again.", + "Open Settings", "Generate a default profile").then((result) => { + if (result === "Open Settings") { + vscode.commands.executeCommand("workbench.action.openSettings", JavaConstants.SETTINGS_URL_KEY); + if (vscode.workspace.workspaceFolders?.length) { + vscode.commands.executeCommand("workbench.action.openWorkspaceSettings"); + } + } else if (result === "Generate a default profile") { this.addDefaultProfile(); } }); return false; - } else { - if (isRemote(this.settingsUrl)) { - // Will handle remote profile in the next PR - sendInfo(operationId, { formatterProfile: "remote" }); - return false; - } - const profilePath = await getProfilePath(this.settingsUrl); - if (!(await fse.pathExists(profilePath))) { - sendInfo(operationId, { formatterProfile: "notExist" }); - await vscode.window.showInformationMessage("The active formatter profile does not exist, please check it in the Settings and try again.", - "Open Settings", "Generate a default profile").then((result) => { - if (result === "Open Settings") { - vscode.commands.executeCommand("workbench.action.openSettings", JavaConstants.SETTINGS_URL_KEY); - if (vscode.workspace.workspaceFolders?.length) { - vscode.commands.executeCommand("workbench.action.openWorkspaceSettings"); - } - } else if (result === "Generate a default profile") { - this.addDefaultProfile(); - } - }); - return false; - } - sendInfo(operationId, { formatterProfile: "valid" }); - return true; } - })(); - return this.checkedProfile; - } + sendInfo(operationId, { formatterProfile: "valid" }); + this.checkedProfileSettings = true; + } + return this.checkedProfileSettings; + }); private async addDefaultProfile(): Promise { const defaultProfile: string = path.join(this.context.extensionPath, "webview-resources", "java-formatter.xml"); diff --git a/src/formatter-settings/types.ts b/src/formatter-settings/types.ts index 64fa946c..ce5af37f 100644 --- a/src/formatter-settings/types.ts +++ b/src/formatter-settings/types.ts @@ -50,11 +50,16 @@ export interface DOMElement extends Element { columnNumber: number; } +export interface DOMAttr extends Attr { + lineNumber: number; + columnNumber: number; +} + export interface ProfileContent { + settingsVersion: string, + diagnostics: vscode.Diagnostic[], profileElements?: Map, profileSettings?: Map, lastElement?: DOMElement, supportedProfileSettings?: Map - settingsVersion: string, - diagnostics: vscode.Diagnostic[], } diff --git a/src/formatter-settings/utils.ts b/src/formatter-settings/utils.ts index c21c93d0..78d856ef 100644 --- a/src/formatter-settings/utils.ts +++ b/src/formatter-settings/utils.ts @@ -5,7 +5,7 @@ import * as fse from "fs-extra"; import * as path from "path"; import * as vscode from "vscode"; import { instrumentOperation, sendInfo } from "vscode-extension-telemetry-wrapper"; -import { DOMElement, ProfileContent } from "./types"; +import { DOMAttr, DOMElement, ProfileContent } from "./types"; import { DOMParser, XMLSerializer } from "xmldom"; import { getDefaultValue, getSupportedProfileSettings, JavaConstants } from "./FormatterConstants"; import { FormatterConverter } from "./FormatterConverter"; @@ -23,17 +23,15 @@ export async function getProfilePath(formatterUrl: string): Promise { return path.resolve(formatterUrl); } -export async function getVSCodeSetting(setting: string, defaultValue: any): Promise { - return await instrumentOperation("formatter.getSetting", async (operationId: string) => { - const config = vscode.workspace.getConfiguration(undefined, { languageId: "java" }); - let result = config.get(setting) ?? vscode.workspace.getConfiguration().get(setting); - if (result === undefined) { - sendInfo(operationId, { notFoundSetting: setting }); - return defaultValue; - } - return result; - })(); -} +export const getVSCodeSetting = instrumentOperation("formatter.getSetting", async (operationId: string, setting: string, defaultValue: any) => { + const config = vscode.workspace.getConfiguration(undefined, { languageId: "java" }); + let result = config.get(setting) ?? vscode.workspace.getConfiguration().get(setting); + if (result === undefined) { + sendInfo(operationId, { notFoundSetting: setting }); + return defaultValue; + } + return result; +}); export function isRemote(path: string): boolean { return path !== null && path.startsWith("http:/") || path.startsWith("https:/"); @@ -68,16 +66,10 @@ export function parseProfile(document: vscode.TextDocument): ProfileContent { const diagnostics: vscode.Diagnostic[] = []; const documentDOM = new DOMParser({ locator: {}, errorHandler: (_level, msg) => { - const bracketExp: RegExp = new RegExp("\\[(.*?)\\]", "g"); - const result: RegExpMatchArray = msg.match(bracketExp); - if (result && result.length) { - const lineExp: RegExp = new RegExp("line:(\\d*)"); - const colExp: RegExp = new RegExp("col:(\\d*)"); - const line = result[result.length - 1].match(lineExp); - const column = result[result.length - 1].match(colExp); - if (line && line.length === 2 && column && column.length === 2) { - diagnostics.push(new vscode.Diagnostic(new vscode.Range(new vscode.Position(Number(line[1]) - 1, Number(column[1]) - 1), new vscode.Position(Number(line[1]) - 1, Number(column[1]))), msg)); - } + const bracketExp: RegExp = new RegExp("\\[line:(\\d*),col:(\\d*)\\]", "g"); + const result = bracketExp.exec(msg); + if (result && result.length === 3) { + diagnostics.push(new vscode.Diagnostic(new vscode.Range(new vscode.Position(Number(result[1]) - 1, Number(result[2]) - 1), new vscode.Position(Number(result[1]) - 1, Number(result[2]))), msg)); } } }).parseFromString(document.getText()); @@ -85,7 +77,7 @@ export function parseProfile(document: vscode.TextDocument): ProfileContent { const profiles = documentDOM.documentElement.getElementsByTagName("profile"); if (!profiles || profiles.length === 0) { diagnostics.push(new vscode.Diagnostic(new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 0)), "No valid profiles found.")); - return { settingsVersion: settingsVersion, diagnostics: diagnostics }; + return { settingsVersion, diagnostics }; } const settingsProfileName: string | undefined = vscode.workspace.getConfiguration("java").get(JavaConstants.SETTINGS_PROFILE_KEY); for (let i = 0; i < profiles.length; i++) { @@ -117,7 +109,7 @@ export function parseProfile(document: vscode.TextDocument): ProfileContent { } if (!profileElements.size) { diagnostics.push(new vscode.Diagnostic(new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 0)), "No valid settings found in the profile.")); - return { settingsVersion: settingsVersion, diagnostics: diagnostics }; + return { settingsVersion, diagnostics }; } const supportedProfileSettings = getSupportedProfileSettings(Number(settingsVersion)); for (const setting of supportedProfileSettings.values()) { @@ -129,9 +121,12 @@ export function parseProfile(document: vscode.TextDocument): ProfileContent { } const webViewValue: string | undefined = FormatterConverter.profile2WebViewConvert(setting.id, value); if (!webViewValue) { - const elementContent = new XMLSerializer().serializeToString(element); - const elementRange = new vscode.Range(new vscode.Position(element.lineNumber - 1, element.columnNumber - 1), new vscode.Position(element.lineNumber - 1, element.columnNumber - 1 + elementContent.length)); - diagnostics.push(new vscode.Diagnostic(elementRange, `Invalid value in id: "${setting.id}", "${value}" is not supported.`, vscode.DiagnosticSeverity.Error)); + const valueNode = element.getAttributeNode("value") as DOMAttr; + if (!valueNode || !valueNode.nodeValue) { + continue; + } + const elementRange = new vscode.Range(new vscode.Position(valueNode.lineNumber - 1, valueNode.columnNumber), new vscode.Position(valueNode.lineNumber - 1, valueNode.columnNumber + valueNode.nodeValue.length)); + diagnostics.push(new vscode.Diagnostic(elementRange, `"${value}" is not supported in id: "${setting.id}".`, vscode.DiagnosticSeverity.Error)); profileSettings.delete(setting.id); setting.value = FormatterConverter.profile2WebViewConvert(setting.id, getDefaultValue(setting.id))!; continue; @@ -139,11 +134,11 @@ export function parseProfile(document: vscode.TextDocument): ProfileContent { setting.value = webViewValue; } return { - profileElements: profileElements, - profileSettings: profileSettings, - lastElement: lastElement, - supportedProfileSettings: supportedProfileSettings, - settingsVersion: settingsVersion, - diagnostics: diagnostics + settingsVersion, + diagnostics, + profileElements, + profileSettings, + lastElement, + supportedProfileSettings, } } From 39865b2110f76e5c79cf874230c49733a7637830 Mon Sep 17 00:00:00 2001 From: Shi Chen Date: Thu, 20 May 2021 17:20:29 +0800 Subject: [PATCH 04/12] remove unused variables --- .../assets/features/formatterSettings/components/Setting.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/formatter-settings/assets/features/formatterSettings/components/Setting.tsx b/src/formatter-settings/assets/features/formatterSettings/components/Setting.tsx index ecae9d77..d6498811 100644 --- a/src/formatter-settings/assets/features/formatterSettings/components/Setting.tsx +++ b/src/formatter-settings/assets/features/formatterSettings/components/Setting.tsx @@ -25,9 +25,7 @@ const Setting = (): JSX.Element => { }; const handleChangeInput = (e: any) => { - const id = e.target.id; - let value = e.target.value; - onWillChangeSetting(id, value); + onWillChangeSetting(e.target.id, e.target.value); }; const handleSelect = (setting: JavaFormatterSetting, entry: string) => { From 9db2a84d2b70aa789e3351c5feefccdbd7b4fe81 Mon Sep 17 00:00:00 2001 From: Shi Chen Date: Thu, 20 May 2021 18:18:04 +0800 Subject: [PATCH 05/12] fix error when generating default profile --- src/formatter-settings/index.ts | 20 +++++++------------- src/formatter-settings/utils.ts | 8 +++++--- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/src/formatter-settings/index.ts b/src/formatter-settings/index.ts index b1796a7f..4b11bcfd 100644 --- a/src/formatter-settings/index.ts +++ b/src/formatter-settings/index.ts @@ -3,7 +3,6 @@ import compareVersions from "compare-versions"; import * as fse from "fs-extra"; -import * as path from "path"; import * as vscode from "vscode"; import { instrumentOperation, sendError, sendInfo, setUserError } from "vscode-extension-telemetry-wrapper"; import { XMLSerializer } from "xmldom"; @@ -11,7 +10,7 @@ import { loadTextFromFile } from "../utils"; import { Example, getDefaultValue, getSupportedVSCodeSettings, JavaConstants, SupportedSettings, VSCodeSettings } from "./FormatterConstants"; import { FormatterConverter } from "./FormatterConverter"; import { DOMElement, ExampleKind, ProfileContent } from "./types"; -import { getProfilePath, getVSCodeSetting, isRemote, getTargetProfilePath, parseProfile } from "./utils"; +import { getProfilePath, getVSCodeSetting, isRemote, parseProfile, addDefaultProfile } from "./utils"; export class JavaFormatterSettingsEditorProvider implements vscode.CustomTextEditorProvider { public static readonly viewType = "java.formatterSettingsEditor"; @@ -39,7 +38,8 @@ export class JavaFormatterSettingsEditorProvider implements vscode.CustomTextEdi if (e.affectsConfiguration(`java.${JavaConstants.SETTINGS_URL_KEY}`)) { this.settingsUrl = vscode.workspace.getConfiguration("java").get(JavaConstants.SETTINGS_URL_KEY); if (this.settingsUrl) { - this.profilePath = await getProfilePath(this.settingsUrl); + const res = await getProfilePath(this.settingsUrl); + this.profilePath = res; } } }); @@ -257,7 +257,7 @@ export class JavaFormatterSettingsEditorProvider implements vscode.CustomTextEdi await vscode.window.showInformationMessage("No active Formatter Profile found, do you want to create a default one?", "Yes", "No").then((result) => { if (result === "Yes") { - this.addDefaultProfile(); + addDefaultProfile(this.context); } }); } else { @@ -267,7 +267,8 @@ export class JavaFormatterSettingsEditorProvider implements vscode.CustomTextEdi return false; } if (!this.profilePath) { - this.profilePath = await getProfilePath(this.settingsUrl); + const res = await getProfilePath(this.settingsUrl); + this.profilePath = res; } if (!(await fse.pathExists(this.profilePath))) { sendInfo(operationId, { formatterProfile: "notExist" }); @@ -279,7 +280,7 @@ export class JavaFormatterSettingsEditorProvider implements vscode.CustomTextEdi vscode.commands.executeCommand("workbench.action.openWorkspaceSettings"); } } else if (result === "Generate a default profile") { - this.addDefaultProfile(); + addDefaultProfile(this.context); } }); return false; @@ -289,13 +290,6 @@ export class JavaFormatterSettingsEditorProvider implements vscode.CustomTextEdi } return this.checkedProfileSettings; }); - - private async addDefaultProfile(): Promise { - const defaultProfile: string = path.join(this.context.extensionPath, "webview-resources", "java-formatter.xml"); - const profilePath = await getTargetProfilePath(this.context); - await fse.copy(defaultProfile, profilePath); - vscode.commands.executeCommand("vscode.openWith", vscode.Uri.file(profilePath), "java.formatterSettingsEditor"); - } } export let javaFormatterSettingsEditorProvider: JavaFormatterSettingsEditorProvider; diff --git a/src/formatter-settings/utils.ts b/src/formatter-settings/utils.ts index 78d856ef..69f04995 100644 --- a/src/formatter-settings/utils.ts +++ b/src/formatter-settings/utils.ts @@ -37,8 +37,9 @@ export function isRemote(path: string): boolean { return path !== null && path.startsWith("http:/") || path.startsWith("https:/"); } -export async function getTargetProfilePath(context: vscode.ExtensionContext, fileName?: string): Promise { - const targetFileName = fileName || "java-formatter.xml"; +export async function addDefaultProfile(context: vscode.ExtensionContext): Promise { + const defaultProfile: string = path.join(context.extensionPath, "webview-resources", "java-formatter.xml"); + const targetFileName = "java-formatter.xml"; let profilePath: string; const workspaceFolder = vscode.workspace.workspaceFolders; if (workspaceFolder?.length) { @@ -51,8 +52,9 @@ export async function getTargetProfilePath(context: vscode.ExtensionContext, fil // bug: https://github.com/redhat-developer/vscode-java/issues/1944, only the profiles in posix path can be monitored when changes, so we use posix path for default profile creation temporarily. const relativePath = toPosixPath(path.join(".vscode", targetFileName)); profilePath = toPosixPath(profilePath); + await fse.copy(defaultProfile, profilePath); await vscode.workspace.getConfiguration("java").update("format.settings.url", (workspaceFolder?.length ? relativePath : profilePath), !(workspaceFolder?.length)); - return profilePath; + vscode.commands.executeCommand("vscode.openWith", vscode.Uri.file(profilePath), "java.formatterSettingsEditor"); } function toPosixPath(inputPath: string): string { From c89466a77dee42a1e1586d39d866b1d30e88394b Mon Sep 17 00:00:00 2001 From: Shi Chen Date: Fri, 21 May 2021 10:31:18 +0800 Subject: [PATCH 06/12] address comments --- src/formatter-settings/index.ts | 3 +-- src/formatter-settings/utils.ts | 8 ++++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/formatter-settings/index.ts b/src/formatter-settings/index.ts index 4b11bcfd..21f99850 100644 --- a/src/formatter-settings/index.ts +++ b/src/formatter-settings/index.ts @@ -38,8 +38,7 @@ export class JavaFormatterSettingsEditorProvider implements vscode.CustomTextEdi if (e.affectsConfiguration(`java.${JavaConstants.SETTINGS_URL_KEY}`)) { this.settingsUrl = vscode.workspace.getConfiguration("java").get(JavaConstants.SETTINGS_URL_KEY); if (this.settingsUrl) { - const res = await getProfilePath(this.settingsUrl); - this.profilePath = res; + this.profilePath = await getProfilePath(this.settingsUrl); } } }); diff --git a/src/formatter-settings/utils.ts b/src/formatter-settings/utils.ts index 69f04995..7bbb210c 100644 --- a/src/formatter-settings/utils.ts +++ b/src/formatter-settings/utils.ts @@ -41,9 +41,9 @@ export async function addDefaultProfile(context: vscode.ExtensionContext): Promi const defaultProfile: string = path.join(context.extensionPath, "webview-resources", "java-formatter.xml"); const targetFileName = "java-formatter.xml"; let profilePath: string; - const workspaceFolder = vscode.workspace.workspaceFolders; - if (workspaceFolder?.length) { - profilePath = path.posix.join(workspaceFolder[0].uri.fsPath, ".vscode", targetFileName); + const workspaceFolders = vscode.workspace.workspaceFolders; + if (workspaceFolders?.length) { + profilePath = path.posix.join(workspaceFolders[0].uri.fsPath, ".vscode", targetFileName); } else { const folder: string = context.globalStorageUri.fsPath; await fse.ensureDir(folder); @@ -53,7 +53,7 @@ export async function addDefaultProfile(context: vscode.ExtensionContext): Promi const relativePath = toPosixPath(path.join(".vscode", targetFileName)); profilePath = toPosixPath(profilePath); await fse.copy(defaultProfile, profilePath); - await vscode.workspace.getConfiguration("java").update("format.settings.url", (workspaceFolder?.length ? relativePath : profilePath), !(workspaceFolder?.length)); + await vscode.workspace.getConfiguration("java").update("format.settings.url", (workspaceFolders?.length ? relativePath : profilePath), !(workspaceFolders?.length)); vscode.commands.executeCommand("vscode.openWith", vscode.Uri.file(profilePath), "java.formatterSettingsEditor"); } From 50becd049959f032b75f7806e2f1cdf7e7796c5c Mon Sep 17 00:00:00 2001 From: Shi Chen Date: Fri, 21 May 2021 13:33:41 +0800 Subject: [PATCH 07/12] allow empty value for input box --- src/formatter-settings/FormatterConverter.ts | 10 +++++++++- .../formatterSettings/components/Setting.tsx | 2 +- src/formatter-settings/index.ts | 14 +++++--------- src/formatter-settings/utils.ts | 12 +++++++----- 4 files changed, 22 insertions(+), 16 deletions(-) diff --git a/src/formatter-settings/FormatterConverter.ts b/src/formatter-settings/FormatterConverter.ts index 1f3b75c0..0e15dc10 100644 --- a/src/formatter-settings/FormatterConverter.ts +++ b/src/formatter-settings/FormatterConverter.ts @@ -33,6 +33,8 @@ export namespace FormatterConverter { return "insert"; case "false": return "do not insert"; + case "": + return ""; default: return undefined; } @@ -51,6 +53,8 @@ export namespace FormatterConverter { return "one_line_if_empty"; case "if at most one item": return "one_line_if_single_item"; + case "": + return ""; default: return undefined; } @@ -87,6 +91,8 @@ export namespace FormatterConverter { return "true"; case "do not insert": return "false"; + case "": + return ""; default: return undefined; } @@ -105,6 +111,8 @@ export namespace FormatterConverter { return "if empty"; case "one_line_if_single_item": return "if at most one item"; + case "": + return ""; default: return undefined; } @@ -128,7 +136,7 @@ export namespace FormatterConverter { case SupportedSettings.INSERT_SPACE_AFTER_OPENING_BRACE_IN_ARRAY_INITIALIZER: case SupportedSettings.INSERT_SPACE_AFTER_CLOSING_PAREN_IN_CAST: case SupportedSettings.INSERT_SPACE_AFTER_CLOSING_ANGLE_BRACKET_IN_TYPE_ARGUMENTS: - if (value === "true" || value === "false") { + if (value === "true" || value === "false" || value === "") { return value; } return undefined; diff --git a/src/formatter-settings/assets/features/formatterSettings/components/Setting.tsx b/src/formatter-settings/assets/features/formatterSettings/components/Setting.tsx index d6498811..ca811d29 100644 --- a/src/formatter-settings/assets/features/formatterSettings/components/Setting.tsx +++ b/src/formatter-settings/assets/features/formatterSettings/components/Setting.tsx @@ -37,7 +37,7 @@ const Setting = (): JSX.Element => { }; const generateSetting = (setting: JavaFormatterSetting) => { - if (!setting.name || !setting.id || !setting.value) { + if (!setting.name || !setting.id) { return null; } const candidates = []; diff --git a/src/formatter-settings/index.ts b/src/formatter-settings/index.ts index 21f99850..4e5b7830 100644 --- a/src/formatter-settings/index.ts +++ b/src/formatter-settings/index.ts @@ -7,7 +7,7 @@ import * as vscode from "vscode"; import { instrumentOperation, sendError, sendInfo, setUserError } from "vscode-extension-telemetry-wrapper"; import { XMLSerializer } from "xmldom"; import { loadTextFromFile } from "../utils"; -import { Example, getDefaultValue, getSupportedVSCodeSettings, JavaConstants, SupportedSettings, VSCodeSettings } from "./FormatterConstants"; +import { Example, getSupportedVSCodeSettings, JavaConstants, SupportedSettings, VSCodeSettings } from "./FormatterConstants"; import { FormatterConverter } from "./FormatterConverter"; import { DOMElement, ExampleKind, ProfileContent } from "./types"; import { getProfilePath, getVSCodeSetting, isRemote, parseProfile, addDefaultProfile } from "./utils"; @@ -91,7 +91,7 @@ export class JavaFormatterSettingsEditorProvider implements vscode.CustomTextEdi break; case "onWillChangeSetting": const settingValue: string | undefined = FormatterConverter.webView2ProfileConvert(e.id, e.value.toString()); - if (!settingValue) { + if (settingValue === undefined) { return; } if (SupportedSettings.indentationSettings.includes(e.id)) { @@ -100,7 +100,7 @@ export class JavaFormatterSettingsEditorProvider implements vscode.CustomTextEdi const targetValue = (settingValue === "tab") ? false : true; await config.update(VSCodeSettings.INSERT_SPACES, targetValue, undefined, true); } else if (e.id === SupportedSettings.TABULATION_SIZE) { - await config.update(VSCodeSettings.TAB_SIZE, Number(settingValue), undefined, true); + await config.update(VSCodeSettings.TAB_SIZE, (settingValue === "") ? "" : Number(settingValue), undefined, true); } this.profileSettings.set(e.id, settingValue); } else if (e.id === VSCodeSettings.DETECT_INDENTATION) { @@ -221,10 +221,6 @@ export class JavaFormatterSettingsEditorProvider implements vscode.CustomTextEdi private async modifyProfile(id: string, value: string, document: vscode.TextDocument): Promise { const profileElement = this.profileElements.get(id); - const fixedValue = value || getDefaultValue(id); - if (!fixedValue) { - return; - } if (!profileElement) { // add a new setting not exist in the profile if (!this.lastElement) { @@ -233,14 +229,14 @@ export class JavaFormatterSettingsEditorProvider implements vscode.CustomTextEdi const cloneElement = this.lastElement.cloneNode() as DOMElement; const originalString: string = new XMLSerializer().serializeToString(cloneElement); cloneElement.setAttribute("id", id); - cloneElement.setAttribute("value", fixedValue); + cloneElement.setAttribute("value", value); const edit: vscode.WorkspaceEdit = new vscode.WorkspaceEdit(); edit.insert(document.uri, new vscode.Position(cloneElement.lineNumber - 1, cloneElement.columnNumber - 1 + originalString.length), ((document.eol === vscode.EndOfLine.LF) ? "\n" : "\r\n") + " ".repeat(cloneElement.columnNumber - 1) + new XMLSerializer().serializeToString(cloneElement)); await vscode.workspace.applyEdit(edit); } else { // edit a current setting in the profile const originalSetting: string = new XMLSerializer().serializeToString(profileElement); - profileElement.setAttribute("value", fixedValue); + profileElement.setAttribute("value", value); const edit: vscode.WorkspaceEdit = new vscode.WorkspaceEdit(); edit.replace(document.uri, new vscode.Range(new vscode.Position(profileElement.lineNumber - 1, profileElement.columnNumber - 1), new vscode.Position(profileElement.lineNumber - 1, profileElement.columnNumber - 1 + originalSetting.length)), new XMLSerializer().serializeToString(profileElement)); await vscode.workspace.applyEdit(edit); diff --git a/src/formatter-settings/utils.ts b/src/formatter-settings/utils.ts index 7bbb210c..b73f96d2 100644 --- a/src/formatter-settings/utils.ts +++ b/src/formatter-settings/utils.ts @@ -94,13 +94,15 @@ export function parseProfile(document: vscode.TextDocument): ProfileContent { const settingContent: string = new XMLSerializer().serializeToString(setting); const id = setting.getAttribute("id"); if (!id) { - diagnostics.push(new vscode.Diagnostic(new vscode.Range(new vscode.Position(setting.lineNumber - 1, setting.columnNumber - 1), new vscode.Position(setting.lineNumber - 1, setting.columnNumber - 1 + settingContent.length)), "The setting has no 'id' property.", vscode.DiagnosticSeverity.Error)); + diagnostics.push(new vscode.Diagnostic(new vscode.Range(new vscode.Position(setting.lineNumber - 1, setting.columnNumber - 1), new vscode.Position(setting.lineNumber - 1, setting.columnNumber - 1 + settingContent.length)), "The setting has no valid 'id' property.", vscode.DiagnosticSeverity.Error)); continue; } const value = settings[j].getAttribute("value"); if (!value) { - diagnostics.push(new vscode.Diagnostic(new vscode.Range(new vscode.Position(setting.lineNumber - 1, setting.columnNumber - 1), new vscode.Position(setting.lineNumber - 1, setting.columnNumber - 1 + settingContent.length)), "The setting has no 'value' property.", vscode.DiagnosticSeverity.Error)); - continue; + diagnostics.push(new vscode.Diagnostic(new vscode.Range(new vscode.Position(setting.lineNumber - 1, setting.columnNumber - 1), new vscode.Position(setting.lineNumber - 1, setting.columnNumber - 1 + settingContent.length)), "The setting has no valid 'value' property.", vscode.DiagnosticSeverity.Error)); + if (value === null) { + continue; + } } profileElements.set(id, setting); profileSettings.set(id, value); @@ -117,12 +119,12 @@ export function parseProfile(document: vscode.TextDocument): ProfileContent { for (const setting of supportedProfileSettings.values()) { const element = profileElements.get(setting.id); const value = profileSettings.get(setting.id); - if (!element || !value) { + if (!element || value === undefined) { setting.value = FormatterConverter.profile2WebViewConvert(setting.id, getDefaultValue(setting.id))!; continue; } const webViewValue: string | undefined = FormatterConverter.profile2WebViewConvert(setting.id, value); - if (!webViewValue) { + if (webViewValue === undefined) { const valueNode = element.getAttributeNode("value") as DOMAttr; if (!valueNode || !valueNode.nodeValue) { continue; From 0de06ada2ced04f3c25bf7f554eddd16521abc62 Mon Sep 17 00:00:00 2001 From: Shi Chen Date: Fri, 21 May 2021 13:56:08 +0800 Subject: [PATCH 08/12] add comments --- src/formatter-settings/FormatterConverter.ts | 5 +++++ src/formatter-settings/index.ts | 1 + src/formatter-settings/utils.ts | 2 ++ 3 files changed, 8 insertions(+) diff --git a/src/formatter-settings/FormatterConverter.ts b/src/formatter-settings/FormatterConverter.ts index 0e15dc10..08a6b014 100644 --- a/src/formatter-settings/FormatterConverter.ts +++ b/src/formatter-settings/FormatterConverter.ts @@ -33,6 +33,7 @@ export namespace FormatterConverter { return "insert"; case "false": return "do not insert"; + // We regard an empty string as a valid value and may write it to the profile case "": return ""; default: @@ -53,6 +54,7 @@ export namespace FormatterConverter { return "one_line_if_empty"; case "if at most one item": return "one_line_if_single_item"; + // We regard an empty string as a valid value and may write it to the profile case "": return ""; default: @@ -91,6 +93,7 @@ export namespace FormatterConverter { return "true"; case "do not insert": return "false"; + // We regard an empty string as a valid value and show it in the webview case "": return ""; default: @@ -111,6 +114,7 @@ export namespace FormatterConverter { return "if empty"; case "one_line_if_single_item": return "if at most one item"; + // We regard an empty string as a valid value and show it in the webview case "": return ""; default: @@ -136,6 +140,7 @@ export namespace FormatterConverter { case SupportedSettings.INSERT_SPACE_AFTER_OPENING_BRACE_IN_ARRAY_INITIALIZER: case SupportedSettings.INSERT_SPACE_AFTER_CLOSING_PAREN_IN_CAST: case SupportedSettings.INSERT_SPACE_AFTER_CLOSING_ANGLE_BRACKET_IN_TYPE_ARGUMENTS: + // We regard an empty string as a valid value and show it in the webview if (value === "true" || value === "false" || value === "") { return value; } diff --git a/src/formatter-settings/index.ts b/src/formatter-settings/index.ts index 4e5b7830..f12b48b0 100644 --- a/src/formatter-settings/index.ts +++ b/src/formatter-settings/index.ts @@ -91,6 +91,7 @@ export class JavaFormatterSettingsEditorProvider implements vscode.CustomTextEdi break; case "onWillChangeSetting": const settingValue: string | undefined = FormatterConverter.webView2ProfileConvert(e.id, e.value.toString()); + // "" represents an empty inputbox, we regard it as a valid value. if (settingValue === undefined) { return; } diff --git a/src/formatter-settings/utils.ts b/src/formatter-settings/utils.ts index b73f96d2..d48e03e6 100644 --- a/src/formatter-settings/utils.ts +++ b/src/formatter-settings/utils.ts @@ -99,6 +99,7 @@ export function parseProfile(document: vscode.TextDocument): ProfileContent { } const value = settings[j].getAttribute("value"); if (!value) { + // value maybe "" or null, "" is an valid value comes from deleting the values in the inpux box of the editor, and we will still push diagnostic here. diagnostics.push(new vscode.Diagnostic(new vscode.Range(new vscode.Position(setting.lineNumber - 1, setting.columnNumber - 1), new vscode.Position(setting.lineNumber - 1, setting.columnNumber - 1 + settingContent.length)), "The setting has no valid 'value' property.", vscode.DiagnosticSeverity.Error)); if (value === null) { continue; @@ -119,6 +120,7 @@ export function parseProfile(document: vscode.TextDocument): ProfileContent { for (const setting of supportedProfileSettings.values()) { const element = profileElements.get(setting.id); const value = profileSettings.get(setting.id); + // "" is a valid value, so we distinguish it and undefined here. if (!element || value === undefined) { setting.value = FormatterConverter.profile2WebViewConvert(setting.id, getDefaultValue(setting.id))!; continue; From d9f20f1bb76089e96b0a9bc340181544431e246d Mon Sep 17 00:00:00 2001 From: Shi Chen Date: Fri, 21 May 2021 15:26:40 +0800 Subject: [PATCH 09/12] support preview remote profile in readonly mode --- src/extension.ts | 2 + .../RemoteProfileProvider.ts | 25 ++++++ .../FormatterSettingView.tsx | 16 ++-- .../formatterSettings/components/Setting.tsx | 7 +- .../formatterSettingViewSlice.ts | 5 ++ src/formatter-settings/assets/style.scss | 3 + src/formatter-settings/assets/utils.ts | 6 ++ src/formatter-settings/index.ts | 65 ++++++++++---- src/formatter-settings/utils.ts | 89 +++++++++++++++++-- 9 files changed, 185 insertions(+), 33 deletions(-) create mode 100644 src/formatter-settings/RemoteProfileProvider.ts diff --git a/src/extension.ts b/src/extension.ts index fe2bf998..23238a35 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -18,6 +18,7 @@ import { ClassPathConfigurationViewSerializer } from "./classpath/classpathConfi import { TreatmentVariables } from "./exp/TreatmentVariables"; import { MarkdownPreviewSerializer } from "./classpath/markdownPreviewProvider"; import { initFormatterSettingsEditorProvider } from "./formatter-settings"; +import { initRemoteProfileProvider } from "./formatter-settings/RemoteProfileProvider"; export async function activate(context: vscode.ExtensionContext) { syncState(context); @@ -29,6 +30,7 @@ export async function activate(context: vscode.ExtensionContext) { async function initializeExtension(_operationId: string, context: vscode.ExtensionContext) { initFormatterSettingsEditorProvider(context); + initRemoteProfileProvider(context); initUtils(context); initCommands(context); initRecommendations(context); diff --git a/src/formatter-settings/RemoteProfileProvider.ts b/src/formatter-settings/RemoteProfileProvider.ts new file mode 100644 index 00000000..94a5ff7e --- /dev/null +++ b/src/formatter-settings/RemoteProfileProvider.ts @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +import * as vscode from "vscode"; +import { downloadFile, getVersion } from "./utils"; + +class RemoteProfileProvider implements vscode.TextDocumentContentProvider { + + public static scheme = "formatter"; + + constructor (private readonly context: vscode.ExtensionContext) { + } + + async provideTextDocumentContent(uri: vscode.Uri): Promise { + const originalUri: vscode.Uri = uri.with({ scheme: "https" }); + return await downloadFile(originalUri.toString(), await getVersion(this.context)); + } +} + +export let remoteProfileProvider: RemoteProfileProvider; + +export function initRemoteProfileProvider(context: vscode.ExtensionContext) { + remoteProfileProvider = new RemoteProfileProvider(context); + context.subscriptions.push(vscode.workspace.registerTextDocumentContentProvider(RemoteProfileProvider.scheme, remoteProfileProvider)); +} diff --git a/src/formatter-settings/assets/features/formatterSettings/FormatterSettingView.tsx b/src/formatter-settings/assets/features/formatterSettings/FormatterSettingView.tsx index c83954dd..9426df29 100644 --- a/src/formatter-settings/assets/features/formatterSettings/FormatterSettingView.tsx +++ b/src/formatter-settings/assets/features/formatterSettings/FormatterSettingView.tsx @@ -5,16 +5,19 @@ import React, { useEffect } from "react"; import { Col, Container, Nav, Row } from "react-bootstrap"; import { useSelector, useDispatch } from "react-redux"; import { Dispatch } from "@reduxjs/toolkit"; -import { applyFormatResult, changeActiveCategory, loadProfileSetting, loadVSCodeSetting } from "./formatterSettingViewSlice"; +import { applyFormatResult, changeActiveCategory, changeReadOnlyState, loadProfileSetting, loadVSCodeSetting } from "./formatterSettingViewSlice"; import { highlight } from "./components/Highlight"; import { Category, ExampleKind } from "../../../types"; import Setting from "./components/Setting"; import { renderWhitespace } from "../../whitespace"; -import { onWillChangeExampleKind, onWillInitialize } from "../../utils"; +import { onWillChangeExampleKind, onWillDownloadAndUse, onWillInitialize } from "../../utils"; const FormatterSettingsView = (): JSX.Element => { const activeCategory: Category = useSelector((state: any) => state.formatterSettings.activeCategory); const contentText: string = useSelector((state: any) => state.formatterSettings.formattedContent); + const readOnly: boolean = useSelector((state: any) => state.formatterSettings.readOnly); + const title: string = "Java Formatter Settings" + (readOnly ? " (Read Only)" : ""); + const dispatch: Dispatch = useDispatch(); const onClickNaviBar = (element: any) => { const activeCategory: Category = Number(element); @@ -75,6 +78,8 @@ const FormatterSettingsView = (): JSX.Element => { dispatch(loadProfileSetting(event.data)); } else if (event.data.command === "loadVSCodeSetting") { dispatch(loadVSCodeSetting(event.data)); + } else if (event.data.command === "changeReadOnlyState") { + dispatch(changeReadOnlyState(event.data)); } }; @@ -90,10 +95,9 @@ const FormatterSettingsView = (): JSX.Element => { return ( - - -

Java Formatter Settings

- + +

{title}

+ {readOnly && ()}
{naviBar} diff --git a/src/formatter-settings/assets/features/formatterSettings/components/Setting.tsx b/src/formatter-settings/assets/features/formatterSettings/components/Setting.tsx index ca811d29..54505c9c 100644 --- a/src/formatter-settings/assets/features/formatterSettings/components/Setting.tsx +++ b/src/formatter-settings/assets/features/formatterSettings/components/Setting.tsx @@ -17,6 +17,7 @@ const Setting = (): JSX.Element => { const vscodeSettings: JavaFormatterSetting[] = useSelector((state: any) => state.formatterSettings.vscodeSettings); const activeCategory: Category = useSelector((state: any) => state.formatterSettings.activeCategory); const detectIndentation: boolean = useSelector((state: any) => state.formatterSettings.detectIndentation); + const readOnly: boolean = useSelector((state: any) => state.formatterSettings.readOnly); const handleChangeCheckbox = (e: any) => { const id = e.target.id; @@ -47,7 +48,7 @@ const Setting = (): JSX.Element => { return (
handleClick(setting.exampleKind)}> - +
{setting.name}.
@@ -74,7 +75,7 @@ const Setting = (): JSX.Element => {
handleClick(setting.exampleKind)}> {setting.name}. - + {setting.value} @@ -88,7 +89,7 @@ const Setting = (): JSX.Element => { return (
handleClick(setting.exampleKind)}> {setting.name}. - +
); default: diff --git a/src/formatter-settings/assets/features/formatterSettings/formatterSettingViewSlice.ts b/src/formatter-settings/assets/features/formatterSettings/formatterSettingViewSlice.ts index e241df2f..ce0577da 100644 --- a/src/formatter-settings/assets/features/formatterSettings/formatterSettingViewSlice.ts +++ b/src/formatter-settings/assets/features/formatterSettings/formatterSettingViewSlice.ts @@ -13,6 +13,7 @@ export const formatterSettingsViewSlice = createSlice({ vscodeSettings: [] as JavaFormatterSetting[], detectIndentation: false, formattedContent: "", + readOnly: false, }, reducers: { changeActiveCategory: (state, action) => { @@ -34,6 +35,9 @@ export const formatterSettingsViewSlice = createSlice({ applyFormatResult: (state, action) => { state.formattedContent = action.payload.content; }, + changeReadOnlyState: (state, action) => { + state.readOnly = Boolean(action.payload.value); + }, }, }); @@ -42,6 +46,7 @@ export const { loadProfileSetting, loadVSCodeSetting, applyFormatResult, + changeReadOnlyState, } = formatterSettingsViewSlice.actions; export default formatterSettingsViewSlice.reducer; diff --git a/src/formatter-settings/assets/style.scss b/src/formatter-settings/assets/style.scss index 7f4065b9..b360d46b 100644 --- a/src/formatter-settings/assets/style.scss +++ b/src/formatter-settings/assets/style.scss @@ -153,6 +153,9 @@ $enable-rounded: false; box-shadow: none !important; border: 1px solid var(--vscode-focusBorder) !important; } + &:disabled { + opacity: 0.67 !important; + } } .setting-nav .nav-link { diff --git a/src/formatter-settings/assets/utils.ts b/src/formatter-settings/assets/utils.ts index 66a0500d..97d215e1 100644 --- a/src/formatter-settings/assets/utils.ts +++ b/src/formatter-settings/assets/utils.ts @@ -26,3 +26,9 @@ export function onWillChangeSetting(id: string, value: any) { value: value, }); } + +export function onWillDownloadAndUse() { + vscode.postMessage({ + command: "onWillDownloadAndUse" + }); +} diff --git a/src/formatter-settings/index.ts b/src/formatter-settings/index.ts index f12b48b0..0613c107 100644 --- a/src/formatter-settings/index.ts +++ b/src/formatter-settings/index.ts @@ -3,6 +3,7 @@ import compareVersions from "compare-versions"; import * as fse from "fs-extra"; +import * as path from "path"; import * as vscode from "vscode"; import { instrumentOperation, sendError, sendInfo, setUserError } from "vscode-extension-telemetry-wrapper"; import { XMLSerializer } from "xmldom"; @@ -10,7 +11,7 @@ import { loadTextFromFile } from "../utils"; import { Example, getSupportedVSCodeSettings, JavaConstants, SupportedSettings, VSCodeSettings } from "./FormatterConstants"; import { FormatterConverter } from "./FormatterConverter"; import { DOMElement, ExampleKind, ProfileContent } from "./types"; -import { getProfilePath, getVSCodeSetting, isRemote, parseProfile, addDefaultProfile } from "./utils"; +import { addDefaultProfile, downloadFile, getProfilePath, getTargetPath, getVersion, getVSCodeSetting, isRemote, openFormatterSettings, parseProfile } from "./utils"; export class JavaFormatterSettingsEditorProvider implements vscode.CustomTextEditorProvider { public static readonly viewType = "java.formatterSettingsEditor"; @@ -25,6 +26,7 @@ export class JavaFormatterSettingsEditorProvider implements vscode.CustomTextEdi private settingsUrl: string | undefined = vscode.workspace.getConfiguration("java").get(JavaConstants.SETTINGS_URL_KEY); private webviewPanel: vscode.WebviewPanel | undefined; private profilePath: string = ""; + private readOnly: boolean = false; constructor(private readonly context: vscode.ExtensionContext) { vscode.workspace.onDidChangeConfiguration(async (e) => { @@ -37,7 +39,7 @@ export class JavaFormatterSettingsEditorProvider implements vscode.CustomTextEdi } if (e.affectsConfiguration(`java.${JavaConstants.SETTINGS_URL_KEY}`)) { this.settingsUrl = vscode.workspace.getConfiguration("java").get(JavaConstants.SETTINGS_URL_KEY); - if (this.settingsUrl) { + if (this.settingsUrl && !isRemote(this.settingsUrl)) { this.profilePath = await getProfilePath(this.settingsUrl); } } @@ -54,7 +56,7 @@ export class JavaFormatterSettingsEditorProvider implements vscode.CustomTextEdi if (!await this.checkProfileSettings() || !this.settingsUrl) { return; } - const filePath = vscode.Uri.file(this.profilePath); + const filePath = this.readOnly ? vscode.Uri.parse(this.settingsUrl).with({ scheme: "formatter" }) : vscode.Uri.file(this.profilePath); vscode.commands.executeCommand("vscode.openWith", filePath, "java.formatterSettingsEditor"); } @@ -68,7 +70,7 @@ export class JavaFormatterSettingsEditorProvider implements vscode.CustomTextEdi } else { this.webviewPanel = webviewPanel; } - + webviewPanel.webview.options = { enableScripts: true, enableCommandUris: true, @@ -111,6 +113,21 @@ export class JavaFormatterSettingsEditorProvider implements vscode.CustomTextEdi await this.modifyProfile(e.id, settingValue, document); } break; + case "onWillDownloadAndUse": { + const settingsUrl = vscode.workspace.getConfiguration("java").get(JavaConstants.SETTINGS_URL_KEY); + if (!settingsUrl || !isRemote(settingsUrl)) { + vscode.window.showErrorMessage("The active formatter profile does not exist or is not remote, please check it in the Settings and try again.", + "Open Settings").then((result) => { + if (result === "Open Settings") { + openFormatterSettings(); + } + }); + return; + } + webviewPanel.dispose(); + await this.downloadAndUse(settingsUrl); + break; + } default: break; } @@ -152,6 +169,10 @@ export class JavaFormatterSettingsEditorProvider implements vscode.CustomTextEdi } this.exampleKind = ExampleKind.INDENTATION_EXAMPLE; await this.parseProfileAndUpdate(document); + this.webviewPanel?.webview.postMessage({ + command: "changeReadOnlyState", + value: this.readOnly, + }); return true; } @@ -244,10 +265,19 @@ export class JavaFormatterSettingsEditorProvider implements vscode.CustomTextEdi } } + private async downloadAndUse(settingsUrl: string): Promise { + const targetPath = await getTargetPath(this.context, path.basename(settingsUrl)); + await downloadFile(settingsUrl, await getVersion(this.context), targetPath.profilePath); + const workspaceFolders = vscode.workspace.workspaceFolders; + await vscode.workspace.getConfiguration("java").update("format.settings.url", (workspaceFolders?.length ? targetPath.relativePath : targetPath.profilePath), !(workspaceFolders?.length)); + this.showFormatterSettingsEditor(); + } + private checkProfileSettings = instrumentOperation("formatter.checkProfileSetting", async (operationId: string) => { if (this.checkedProfileSettings) { return true; } + this.readOnly = false; if (!this.settingsUrl) { sendInfo(operationId, { formatterProfile: "undefined" }); await vscode.window.showInformationMessage("No active Formatter Profile found, do you want to create a default one?", @@ -256,25 +286,30 @@ export class JavaFormatterSettingsEditorProvider implements vscode.CustomTextEdi addDefaultProfile(this.context); } }); + } else if (isRemote(this.settingsUrl)) { + sendInfo(operationId, { formatterProfile: "remote" }); + this.checkedProfileSettings = await vscode.window.showInformationMessage("The active formatter profile is remote, do you want to open it in read-only mode or download and use it locally?", + "Open in read-only mode", "Download and use it locally").then(async (result) => { + if (result === "Open in read-only mode") { + this.readOnly = true; + return true; + } else if (result === "Download and use it locally") { + await this.downloadAndUse(this.settingsUrl!); + return true; + } else { + return false; + } + }); } else { - if (isRemote(this.settingsUrl)) { - // Will handle remote profile in the next PR - sendInfo(operationId, { formatterProfile: "remote" }); - return false; - } if (!this.profilePath) { - const res = await getProfilePath(this.settingsUrl); - this.profilePath = res; + this.profilePath = await getProfilePath(this.settingsUrl); } if (!(await fse.pathExists(this.profilePath))) { sendInfo(operationId, { formatterProfile: "notExist" }); await vscode.window.showInformationMessage("The active formatter profile does not exist, please check it in the Settings and try again.", "Open Settings", "Generate a default profile").then((result) => { if (result === "Open Settings") { - vscode.commands.executeCommand("workbench.action.openSettings", JavaConstants.SETTINGS_URL_KEY); - if (vscode.workspace.workspaceFolders?.length) { - vscode.commands.executeCommand("workbench.action.openWorkspaceSettings"); - } + openFormatterSettings(); } else if (result === "Generate a default profile") { addDefaultProfile(this.context); } diff --git a/src/formatter-settings/utils.ts b/src/formatter-settings/utils.ts index d48e03e6..5c9312af 100644 --- a/src/formatter-settings/utils.ts +++ b/src/formatter-settings/utils.ts @@ -4,6 +4,9 @@ import * as fse from "fs-extra"; import * as path from "path"; import * as vscode from "vscode"; +import * as http from "http"; +import * as https from "https"; +import * as url from "url"; import { instrumentOperation, sendInfo } from "vscode-extension-telemetry-wrapper"; import { DOMAttr, DOMElement, ProfileContent } from "./types"; import { DOMParser, XMLSerializer } from "xmldom"; @@ -39,22 +42,28 @@ export function isRemote(path: string): boolean { export async function addDefaultProfile(context: vscode.ExtensionContext): Promise { const defaultProfile: string = path.join(context.extensionPath, "webview-resources", "java-formatter.xml"); - const targetFileName = "java-formatter.xml"; - let profilePath: string; + const targetPath = await getTargetPath(context, "java-formatter.xml"); + const profilePath = targetPath.profilePath; + await fse.copy(defaultProfile, profilePath); + const workspaceFolders = vscode.workspace.workspaceFolders; + await vscode.workspace.getConfiguration("java").update("format.settings.url", (workspaceFolders?.length ? targetPath.relativePath : profilePath), !(workspaceFolders?.length)); + vscode.commands.executeCommand("vscode.openWith", vscode.Uri.file(profilePath), "java.formatterSettingsEditor"); +} + +export async function getTargetPath(context: vscode.ExtensionContext, fileName: string): Promise<{relativePath: string, profilePath: string}> { const workspaceFolders = vscode.workspace.workspaceFolders; + let profilePath: string; if (workspaceFolders?.length) { - profilePath = path.posix.join(workspaceFolders[0].uri.fsPath, ".vscode", targetFileName); + profilePath = path.join(workspaceFolders[0].uri.fsPath, ".vscode", fileName); } else { const folder: string = context.globalStorageUri.fsPath; await fse.ensureDir(folder); - profilePath = path.posix.join(folder, targetFileName); + profilePath = path.join(folder, fileName); } - // bug: https://github.com/redhat-developer/vscode-java/issues/1944, only the profiles in posix path can be monitored when changes, so we use posix path for default profile creation temporarily. - const relativePath = toPosixPath(path.join(".vscode", targetFileName)); + // bug: https://github.com/redhat-developer/vscode-java/issues/1944, only the profiles in posix path can be monitored when changes, so we use posix path for default profile creation temporarily. + const relativePath = toPosixPath(path.join(".vscode", fileName)); profilePath = toPosixPath(profilePath); - await fse.copy(defaultProfile, profilePath); - await vscode.workspace.getConfiguration("java").update("format.settings.url", (workspaceFolders?.length ? relativePath : profilePath), !(workspaceFolders?.length)); - vscode.commands.executeCommand("vscode.openWith", vscode.Uri.file(profilePath), "java.formatterSettingsEditor"); + return {relativePath, profilePath}; } function toPosixPath(inputPath: string): string { @@ -148,3 +157,65 @@ export function parseProfile(document: vscode.TextDocument): ProfileContent { supportedProfileSettings, } } + +export async function downloadFile(settingsUrl: string, extensionVersion: string, targetPath?: string): Promise { + if (targetPath) { + await fse.ensureDir(path.dirname(targetPath)); + if (await fse.pathExists(targetPath)) { + await fse.remove(targetPath); + } + } + return await new Promise((resolve: (res: string) => void, reject: (e: Error) => void): void => { + const urlObj: url.Url = url.parse(settingsUrl); + const options = Object.assign({ headers: Object.assign({}, { "User-Agent": `vscode/${extensionVersion}` }) }, urlObj); + let client: any; + if (urlObj.protocol === "https:") { + client = https; + // tslint:disable-next-line:no-http-string + } else if (urlObj.protocol === "http:") { + client = http; + } else { + return reject(new Error("Unsupported protocol.")); + } + client.get(options, (res: http.IncomingMessage) => { + let ws: fse.WriteStream; + let rawData: string; + if (targetPath) { + ws = fse.createWriteStream(targetPath); + } else { + rawData = ""; + } + res.on("data", (chunk: string | Buffer) => { + if (targetPath) { + ws.write(chunk); + } else { + rawData += chunk; + } + }); + res.on("end", () => { + if (targetPath) { + ws.end(); + ws.on("close", () => { + resolve(""); + }); + } else { + resolve(rawData); + } + }); + }).on("error", (err: Error) => { + reject(err); + }); + }); +} + +export async function getVersion(context: vscode.ExtensionContext): Promise { + const { version } = await fse.readJSON(context.asAbsolutePath("./package.json")); + return version; +} + +export function openFormatterSettings(): void { + vscode.commands.executeCommand("workbench.action.openSettings", JavaConstants.SETTINGS_URL_KEY); + if (vscode.workspace.workspaceFolders?.length) { + vscode.commands.executeCommand("workbench.action.openWorkspaceSettings"); + } +} From 1fa11185864d01960a4461629fa07329d5f881f3 Mon Sep 17 00:00:00 2001 From: Shi Chen Date: Fri, 21 May 2021 15:33:21 +0800 Subject: [PATCH 10/12] use constants --- src/formatter-settings/RemoteProfileProvider.ts | 6 ++---- src/formatter-settings/index.ts | 3 ++- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/formatter-settings/RemoteProfileProvider.ts b/src/formatter-settings/RemoteProfileProvider.ts index 94a5ff7e..2572f70d 100644 --- a/src/formatter-settings/RemoteProfileProvider.ts +++ b/src/formatter-settings/RemoteProfileProvider.ts @@ -4,7 +4,7 @@ import * as vscode from "vscode"; import { downloadFile, getVersion } from "./utils"; -class RemoteProfileProvider implements vscode.TextDocumentContentProvider { +export class RemoteProfileProvider implements vscode.TextDocumentContentProvider { public static scheme = "formatter"; @@ -17,9 +17,7 @@ class RemoteProfileProvider implements vscode.TextDocumentContentProvider { } } -export let remoteProfileProvider: RemoteProfileProvider; - export function initRemoteProfileProvider(context: vscode.ExtensionContext) { - remoteProfileProvider = new RemoteProfileProvider(context); + const remoteProfileProvider = new RemoteProfileProvider(context); context.subscriptions.push(vscode.workspace.registerTextDocumentContentProvider(RemoteProfileProvider.scheme, remoteProfileProvider)); } diff --git a/src/formatter-settings/index.ts b/src/formatter-settings/index.ts index 0613c107..4bd6717b 100644 --- a/src/formatter-settings/index.ts +++ b/src/formatter-settings/index.ts @@ -10,6 +10,7 @@ import { XMLSerializer } from "xmldom"; import { loadTextFromFile } from "../utils"; import { Example, getSupportedVSCodeSettings, JavaConstants, SupportedSettings, VSCodeSettings } from "./FormatterConstants"; import { FormatterConverter } from "./FormatterConverter"; +import { RemoteProfileProvider } from "./RemoteProfileProvider"; import { DOMElement, ExampleKind, ProfileContent } from "./types"; import { addDefaultProfile, downloadFile, getProfilePath, getTargetPath, getVersion, getVSCodeSetting, isRemote, openFormatterSettings, parseProfile } from "./utils"; export class JavaFormatterSettingsEditorProvider implements vscode.CustomTextEditorProvider { @@ -56,7 +57,7 @@ export class JavaFormatterSettingsEditorProvider implements vscode.CustomTextEdi if (!await this.checkProfileSettings() || !this.settingsUrl) { return; } - const filePath = this.readOnly ? vscode.Uri.parse(this.settingsUrl).with({ scheme: "formatter" }) : vscode.Uri.file(this.profilePath); + const filePath = this.readOnly ? vscode.Uri.parse(this.settingsUrl).with({ scheme: RemoteProfileProvider.scheme }) : vscode.Uri.file(this.profilePath); vscode.commands.executeCommand("vscode.openWith", filePath, "java.formatterSettingsEditor"); } From fe4586955c57efc02ff81f6a2fdabf2add2ac492 Mon Sep 17 00:00:00 2001 From: Shi Chen Date: Fri, 21 May 2021 16:43:42 +0800 Subject: [PATCH 11/12] address comments --- package.json | 1 + .../RemoteProfileProvider.ts | 9 +-- src/formatter-settings/index.ts | 19 ++--- src/formatter-settings/utils.ts | 71 +++---------------- 4 files changed, 24 insertions(+), 76 deletions(-) diff --git a/package.json b/package.json index ea067abc..a59b5291 100644 --- a/package.json +++ b/package.json @@ -290,6 +290,7 @@ "@types/vscode": "1.52.0", "@types/winreg": "^1.2.30", "@types/xmldom": "^0.1.30", + "axios": "^0.21.1", "arch": "^2.1.2", "autoprefixer": "^8.5.1", "bootstrap": "^4.5.2", diff --git a/src/formatter-settings/RemoteProfileProvider.ts b/src/formatter-settings/RemoteProfileProvider.ts index 2572f70d..27192c61 100644 --- a/src/formatter-settings/RemoteProfileProvider.ts +++ b/src/formatter-settings/RemoteProfileProvider.ts @@ -2,22 +2,19 @@ // Licensed under the MIT license. import * as vscode from "vscode"; -import { downloadFile, getVersion } from "./utils"; +import { downloadFile } from "./utils"; export class RemoteProfileProvider implements vscode.TextDocumentContentProvider { public static scheme = "formatter"; - constructor (private readonly context: vscode.ExtensionContext) { - } - async provideTextDocumentContent(uri: vscode.Uri): Promise { const originalUri: vscode.Uri = uri.with({ scheme: "https" }); - return await downloadFile(originalUri.toString(), await getVersion(this.context)); + return await downloadFile(originalUri.toString()); } } export function initRemoteProfileProvider(context: vscode.ExtensionContext) { - const remoteProfileProvider = new RemoteProfileProvider(context); + const remoteProfileProvider = new RemoteProfileProvider(); context.subscriptions.push(vscode.workspace.registerTextDocumentContentProvider(RemoteProfileProvider.scheme, remoteProfileProvider)); } diff --git a/src/formatter-settings/index.ts b/src/formatter-settings/index.ts index 4bd6717b..e6912cda 100644 --- a/src/formatter-settings/index.ts +++ b/src/formatter-settings/index.ts @@ -12,7 +12,7 @@ import { Example, getSupportedVSCodeSettings, JavaConstants, SupportedSettings, import { FormatterConverter } from "./FormatterConverter"; import { RemoteProfileProvider } from "./RemoteProfileProvider"; import { DOMElement, ExampleKind, ProfileContent } from "./types"; -import { addDefaultProfile, downloadFile, getProfilePath, getTargetPath, getVersion, getVSCodeSetting, isRemote, openFormatterSettings, parseProfile } from "./utils"; +import { addDefaultProfile, downloadFile, getProfilePath, getAbsoluteTargetPath, getVSCodeSetting, isRemote, openFormatterSettings, parseProfile } from "./utils"; export class JavaFormatterSettingsEditorProvider implements vscode.CustomTextEditorProvider { public static readonly viewType = "java.formatterSettingsEditor"; @@ -54,7 +54,7 @@ export class JavaFormatterSettingsEditorProvider implements vscode.CustomTextEdi } public async showFormatterSettingsEditor(): Promise { - if (!await this.checkProfileSettings() || !this.settingsUrl) { + if (this.webviewPanel || !await this.checkProfileSettings() || !this.settingsUrl) { return; } const filePath = this.readOnly ? vscode.Uri.parse(this.settingsUrl).with({ scheme: RemoteProfileProvider.scheme }) : vscode.Uri.file(this.profilePath); @@ -72,7 +72,7 @@ export class JavaFormatterSettingsEditorProvider implements vscode.CustomTextEdi this.webviewPanel = webviewPanel; } - webviewPanel.webview.options = { + this.webviewPanel.webview.options = { enableScripts: true, enableCommandUris: true, }; @@ -125,7 +125,7 @@ export class JavaFormatterSettingsEditorProvider implements vscode.CustomTextEdi }); return; } - webviewPanel.dispose(); + this.webviewPanel?.dispose(); await this.downloadAndUse(settingsUrl); break; } @@ -267,10 +267,11 @@ export class JavaFormatterSettingsEditorProvider implements vscode.CustomTextEdi } private async downloadAndUse(settingsUrl: string): Promise { - const targetPath = await getTargetPath(this.context, path.basename(settingsUrl)); - await downloadFile(settingsUrl, await getVersion(this.context), targetPath.profilePath); + const profilePath = await getAbsoluteTargetPath(this.context, path.basename(settingsUrl)); + const data = await downloadFile(settingsUrl); + await fse.outputFile(profilePath, data); const workspaceFolders = vscode.workspace.workspaceFolders; - await vscode.workspace.getConfiguration("java").update("format.settings.url", (workspaceFolders?.length ? targetPath.relativePath : targetPath.profilePath), !(workspaceFolders?.length)); + await vscode.workspace.getConfiguration("java").update("format.settings.url", (workspaceFolders?.length ? vscode.workspace.asRelativePath(profilePath) : profilePath), !(workspaceFolders?.length)); this.showFormatterSettingsEditor(); } @@ -295,8 +296,8 @@ export class JavaFormatterSettingsEditorProvider implements vscode.CustomTextEdi this.readOnly = true; return true; } else if (result === "Download and use it locally") { - await this.downloadAndUse(this.settingsUrl!); - return true; + this.downloadAndUse(this.settingsUrl!); + return false; } else { return false; } diff --git a/src/formatter-settings/utils.ts b/src/formatter-settings/utils.ts index 5c9312af..deede8c6 100644 --- a/src/formatter-settings/utils.ts +++ b/src/formatter-settings/utils.ts @@ -7,6 +7,7 @@ import * as vscode from "vscode"; import * as http from "http"; import * as https from "https"; import * as url from "url"; +import axios from 'axios'; import { instrumentOperation, sendInfo } from "vscode-extension-telemetry-wrapper"; import { DOMAttr, DOMElement, ProfileContent } from "./types"; import { DOMParser, XMLSerializer } from "xmldom"; @@ -22,8 +23,10 @@ export async function getProfilePath(formatterUrl: string): Promise { return filePath; } } + } else { + return path.resolve(formatterUrl); } - return path.resolve(formatterUrl); + return ""; } export const getVSCodeSetting = instrumentOperation("formatter.getSetting", async (operationId: string, setting: string, defaultValue: any) => { @@ -42,15 +45,14 @@ export function isRemote(path: string): boolean { export async function addDefaultProfile(context: vscode.ExtensionContext): Promise { const defaultProfile: string = path.join(context.extensionPath, "webview-resources", "java-formatter.xml"); - const targetPath = await getTargetPath(context, "java-formatter.xml"); - const profilePath = targetPath.profilePath; + const profilePath = await getAbsoluteTargetPath(context, "java-formatter.xml"); await fse.copy(defaultProfile, profilePath); const workspaceFolders = vscode.workspace.workspaceFolders; - await vscode.workspace.getConfiguration("java").update("format.settings.url", (workspaceFolders?.length ? targetPath.relativePath : profilePath), !(workspaceFolders?.length)); + await vscode.workspace.getConfiguration("java").update("format.settings.url", (workspaceFolders?.length ? vscode.workspace.asRelativePath(profilePath) : profilePath), !(workspaceFolders?.length)); vscode.commands.executeCommand("vscode.openWith", vscode.Uri.file(profilePath), "java.formatterSettingsEditor"); } -export async function getTargetPath(context: vscode.ExtensionContext, fileName: string): Promise<{relativePath: string, profilePath: string}> { +export async function getAbsoluteTargetPath(context: vscode.ExtensionContext, fileName: string): Promise { const workspaceFolders = vscode.workspace.workspaceFolders; let profilePath: string; if (workspaceFolders?.length) { @@ -61,9 +63,7 @@ export async function getTargetPath(context: vscode.ExtensionContext, fileName: profilePath = path.join(folder, fileName); } // bug: https://github.com/redhat-developer/vscode-java/issues/1944, only the profiles in posix path can be monitored when changes, so we use posix path for default profile creation temporarily. - const relativePath = toPosixPath(path.join(".vscode", fileName)); - profilePath = toPosixPath(profilePath); - return {relativePath, profilePath}; + return toPosixPath(profilePath); } function toPosixPath(inputPath: string): string { @@ -158,59 +158,8 @@ export function parseProfile(document: vscode.TextDocument): ProfileContent { } } -export async function downloadFile(settingsUrl: string, extensionVersion: string, targetPath?: string): Promise { - if (targetPath) { - await fse.ensureDir(path.dirname(targetPath)); - if (await fse.pathExists(targetPath)) { - await fse.remove(targetPath); - } - } - return await new Promise((resolve: (res: string) => void, reject: (e: Error) => void): void => { - const urlObj: url.Url = url.parse(settingsUrl); - const options = Object.assign({ headers: Object.assign({}, { "User-Agent": `vscode/${extensionVersion}` }) }, urlObj); - let client: any; - if (urlObj.protocol === "https:") { - client = https; - // tslint:disable-next-line:no-http-string - } else if (urlObj.protocol === "http:") { - client = http; - } else { - return reject(new Error("Unsupported protocol.")); - } - client.get(options, (res: http.IncomingMessage) => { - let ws: fse.WriteStream; - let rawData: string; - if (targetPath) { - ws = fse.createWriteStream(targetPath); - } else { - rawData = ""; - } - res.on("data", (chunk: string | Buffer) => { - if (targetPath) { - ws.write(chunk); - } else { - rawData += chunk; - } - }); - res.on("end", () => { - if (targetPath) { - ws.end(); - ws.on("close", () => { - resolve(""); - }); - } else { - resolve(rawData); - } - }); - }).on("error", (err: Error) => { - reject(err); - }); - }); -} - -export async function getVersion(context: vscode.ExtensionContext): Promise { - const { version } = await fse.readJSON(context.asAbsolutePath("./package.json")); - return version; +export async function downloadFile(settingsUrl: string): Promise { + return (await axios.get(settingsUrl)).data; } export function openFormatterSettings(): void { From dce7ea39295ed70d759d468c6a2b9da3db20e56b Mon Sep 17 00:00:00 2001 From: Shi Chen Date: Fri, 21 May 2021 16:49:53 +0800 Subject: [PATCH 12/12] remove unused imports --- src/formatter-settings/utils.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/formatter-settings/utils.ts b/src/formatter-settings/utils.ts index deede8c6..24cf095b 100644 --- a/src/formatter-settings/utils.ts +++ b/src/formatter-settings/utils.ts @@ -4,9 +4,6 @@ import * as fse from "fs-extra"; import * as path from "path"; import * as vscode from "vscode"; -import * as http from "http"; -import * as https from "https"; -import * as url from "url"; import axios from 'axios'; import { instrumentOperation, sendInfo } from "vscode-extension-telemetry-wrapper"; import { DOMAttr, DOMElement, ProfileContent } from "./types";