diff --git a/README.md b/README.md index b5d10f9b..54ae2133 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ This VS Code extension provides support for creating and editing XML documents, | enabled by default | requires additional configuration to enable | * [RelaxNG (experimental) support](https://github.com/redhat-developer/vscode-xml/blob/main/docs/Features/RelaxNGFeatures.md#relaxng-features) (since v0.22.0) + * [Surround with Tags,Comments, CDATA](https://github.com/redhat-developer/vscode-xml/blob/main/docs/Refactor.md#refactor) (since v0.23.0) * Syntax error reporting * General code completion * [Auto-close tags](https://github.com/redhat-developer/vscode-xml/blob/main/docs/Features/XMLFeatures.md#xml-tag-auto-close) diff --git a/docs/Refactor.md b/docs/Refactor.md new file mode 100644 index 00000000..42929db5 --- /dev/null +++ b/docs/Refactor.md @@ -0,0 +1,23 @@ +# Refactor + +## Surround with Tags (Wrap) + +This refactor command gives the capability to select an XML content and surround it with a given tag. To execute this command you can: + + * use command palette (`Ctrl+P`) and type `Surround` + +![Surround with Tags](images/Refactor/SurroundWithTags.gif) + + * use contextual menu + +## Surround with Comments + +As `Surround with Tags (Wrap)`, you can comments a selected XML content: + +![Surround with Tags](images/Refactor/SurroundWithComments.gif) + +## Surround with CDATA + +As `Surround with Tags (Wrap)`, you can surround with CDATA a selected XML content: + +![Surround with Tags](images/Refactor/SurroundWithCDATA.gif) \ No newline at end of file diff --git a/docs/images/Refactor/SurroundWithCDATA.gif b/docs/images/Refactor/SurroundWithCDATA.gif new file mode 100644 index 00000000..0a7364ed Binary files /dev/null and b/docs/images/Refactor/SurroundWithCDATA.gif differ diff --git a/docs/images/Refactor/SurroundWithComments.gif b/docs/images/Refactor/SurroundWithComments.gif new file mode 100644 index 00000000..9b310c67 Binary files /dev/null and b/docs/images/Refactor/SurroundWithComments.gif differ diff --git a/docs/images/Refactor/SurroundWithTags.gif b/docs/images/Refactor/SurroundWithTags.gif new file mode 100644 index 00000000..88bb48aa Binary files /dev/null and b/docs/images/Refactor/SurroundWithTags.gif differ diff --git a/package-lock.json b/package-lock.json index ad6cc06d..4bf8ee8c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -318,14 +318,14 @@ "dev": true }, "@types/vscode": { - "version": "1.69.0", - "resolved": "https://repository.engineering.redhat.com/nexus/repository/registry.npmjs.org/@types/vscode/-/vscode-1.69.0.tgz", - "integrity": "sha512-RlzDAnGqUoo9wS6d4tthNyAdZLxOIddLiX3djMoWk29jFfSA1yJbIwr0epBYqqYarWB6s2Z+4VaZCQ80Jaa3kA==", + "version": "1.73.1", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.73.1.tgz", + "integrity": "sha512-eArfOrAoZVV+Ao9zQOCaFNaeXj4kTCD+bGS2gyNgIFZH9xVMuLMlRrEkhb22NyxycFWKV1UyTh03vhaVHmqVMg==", "dev": true }, "@types/yauzl": { "version": "2.9.1", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.9.1.tgz", + "resolved": "https://repository.engineering.redhat.com/nexus/repository/registry.npmjs.org/@types/yauzl/-/yauzl-2.9.1.tgz", "integrity": "sha512-A1b8SU4D10uoPjwb0lnHmmu8wZhR9d+9o2PKBQT2jU5YPTKsxac6M2qGAdY7VcL+dHHhARVUDmeg0rOrcd9EjA==", "dev": true, "requires": { @@ -817,7 +817,7 @@ }, "ajv-keywords": { "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "resolved": "https://repository.engineering.redhat.com/nexus/repository/registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", "dev": true }, @@ -1204,7 +1204,7 @@ }, "bindings": { "version": "1.5.0", - "resolved": "https://repository.engineering.redhat.com/nexus/repository/registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", "dev": true, "optional": true, @@ -1285,7 +1285,7 @@ }, "buffer-crc32": { "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "resolved": "https://repository.engineering.redhat.com/nexus/repository/registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=" }, "buffer-equal": { @@ -1373,7 +1373,7 @@ "charenc": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", - "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==" + "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=" }, "chokidar": { "version": "2.1.8", @@ -1555,12 +1555,12 @@ "component-type": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/component-type/-/component-type-1.2.1.tgz", - "integrity": "sha512-Kgy+2+Uwr75vAi6ChWXgHuLvd+QLD7ssgpaRq2zCvt80ptvAfMc/hijcJxXkBa2wMlEZcJvC2H8Ubo+A9ATHIg==" + "integrity": "sha1-ikeQFwAjjk/DIml3EjAibyS0Fak=" }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, "concat-stream": { "version": "1.6.2", @@ -1641,7 +1641,7 @@ "crypt": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", - "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==" + "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=" }, "d": { "version": "1.0.1", @@ -2556,7 +2556,7 @@ }, "fast-deep-equal": { "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "resolved": "https://repository.engineering.redhat.com/nexus/repository/registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, @@ -2635,7 +2635,7 @@ }, "fast-json-stable-stringify": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "resolved": "https://repository.engineering.redhat.com/nexus/repository/registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true }, @@ -2662,7 +2662,7 @@ }, "fd-slicer": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "resolved": "https://repository.engineering.redhat.com/nexus/repository/registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", "requires": { "pend": "~1.2.0" @@ -2679,7 +2679,7 @@ }, "file-uri-to-path": { "version": "1.0.0", - "resolved": "https://repository.engineering.redhat.com/nexus/repository/registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", "dev": true, "optional": true @@ -2851,7 +2851,7 @@ }, "fsevents": { "version": "1.2.13", - "resolved": "https://repository.engineering.redhat.com/nexus/repository/registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", "dev": true, "optional": true, @@ -3607,7 +3607,7 @@ "join-component": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/join-component/-/join-component-1.1.0.tgz", - "integrity": "sha512-bF7vcQxbODoGK1imE2P9GS9aw4zD0Sd+Hni68IMZLj7zRnquH7dXUmMw9hDI5S/Jzt7q+IyTXN0rSg2GI0IKhQ==" + "integrity": "sha1-uEF7dQZho5K+4sJTfGiyqdSXfNU=" }, "json-parse-better-errors": { "version": "1.0.2", @@ -3630,7 +3630,7 @@ "jsonfile": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", "requires": { "graceful-fs": "^4.1.6" } @@ -3747,7 +3747,7 @@ "lodash.isstring": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" }, "lodash.merge": { "version": "4.6.2", @@ -3989,7 +3989,7 @@ }, "neo-async": { "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "resolved": "https://repository.engineering.redhat.com/nexus/repository/registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, @@ -4216,7 +4216,7 @@ "p-defer": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", - "integrity": "sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw==" + "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=" }, "p-is-promise": { "version": "2.1.0", @@ -4360,7 +4360,7 @@ }, "pend": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "resolved": "https://repository.engineering.redhat.com/nexus/repository/registry.npmjs.org/pend/-/pend-1.2.0.tgz", "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=" }, "picomatch": { @@ -5930,7 +5930,7 @@ }, "yauzl": { "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "resolved": "https://repository.engineering.redhat.com/nexus/repository/registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", "requires": { "buffer-crc32": "~0.2.3", @@ -5944,4 +5944,4 @@ "dev": true } } -} \ No newline at end of file +} diff --git a/package.json b/package.json index b484a098..099ac584 100644 --- a/package.json +++ b/package.json @@ -684,17 +684,61 @@ "command": "xml.restart.language.server", "title": "Restart XML Language Server", "category": "XML" + }, + { + "command": "xml.refactor.surround.with.tags", + "title": "Surround with Tags (Wrap)", + "category": "XML" + }, + { + "command": "xml.refactor.surround.with.comments", + "title": "Surround with Comments", + "category": "XML" + }, + { + "command": "xml.refactor.surround.with.cdata", + "title": "Surround with CDATA", + "category": "XML" } ], "menus": { "commandPalette": [ { "command": "xml.validation.current.file", - "when": "editorLangId == xml" + "when": "editorLangId == xml || editorLangId == dtd || editorLangId == xsl || editorLangId == svg" }, { "command": "xml.command.bind.grammar", "when": "resourceFilename =~ /xml/ && editorIsOpen" + }, + { + "command": "xml.refactor.surround.with.tags", + "when": "editorLangId == xml || editorLangId == xsl || editorLangId == svg" + }, + { + "command": "xml.refactor.surround.with.comments", + "when": "editorLangId == xml || editorLangId == xsl || editorLangId == svg" + }, + { + "command": "xml.refactor.surround.with.cdata", + "when": "editorLangId == xml || editorLangId == xsl || editorLangId == svg" + } + ], + "editor/context": [ + { + "command": "xml.refactor.surround.with.tags", + "when": "editorLangId == xml || editorLangId == xsl || editorLangId == svg", + "group": "1_modification" + }, + { + "command": "xml.refactor.surround.with.comments", + "when": "editorLangId == xml || editorLangId == xsl || editorLangId == svg", + "group": "1_modification" + }, + { + "command": "xml.refactor.surround.with.cdata", + "when": "editorLangId == xml || editorLangId == xsl || editorLangId == svg", + "group": "1_modification" } ] }, @@ -705,4 +749,4 @@ } ] } -} \ No newline at end of file +} diff --git a/src/commands/clientCommandConstants.ts b/src/commands/clientCommandConstants.ts index fc3b1bc6..7b021f85 100644 --- a/src/commands/clientCommandConstants.ts +++ b/src/commands/clientCommandConstants.ts @@ -65,4 +65,13 @@ export const EXECUTE_WORKSPACE_COMMAND = 'xml.workspace.executeCommand'; /** * Command to restart connection to language server. */ - export const RESTART_LANGUAGE_SERVER = 'xml.restart.language.server'; \ No newline at end of file + export const RESTART_LANGUAGE_SERVER = 'xml.restart.language.server'; + +/** + * Command to wrap element. + */ + export const REFACTOR_SURROUND_WITH_TAGS = 'xml.refactor.surround.with.tags'; + + export const REFACTOR_SURROUND_WITH_COMMENTS = 'xml.refactor.surround.with.comments'; + + export const REFACTOR_SURROUND_WITH_CDATA = 'xml.refactor.surround.with.cdata'; \ No newline at end of file diff --git a/src/commands/registerCommands.ts b/src/commands/registerCommands.ts index be0bf5f4..ab24d335 100644 --- a/src/commands/registerCommands.ts +++ b/src/commands/registerCommands.ts @@ -1,6 +1,7 @@ import * as path from 'path'; -import { commands, ConfigurationTarget, env, ExtensionContext, OpenDialogOptions, Position, QuickPickItem, TextDocument, Uri, window, workspace, WorkspaceEdit } from "vscode"; -import { CancellationToken, ExecuteCommandParams, ExecuteCommandRequest, ReferencesRequest, TextDocumentEdit, TextDocumentIdentifier } from "vscode-languageclient"; +import * as vscode from 'vscode'; +import { commands, ConfigurationTarget, env, ExtensionContext, OpenDialogOptions, Position, QuickPickItem, SnippetString, TextDocument, Uri, window, workspace, WorkspaceEdit, Selection } from "vscode"; +import { CancellationToken, ExecuteCommandParams, ExecuteCommandRequest, ReferencesRequest, TextDocumentEdit, TextDocumentIdentifier, TextEdit } from "vscode-languageclient"; import { LanguageClient } from 'vscode-languageclient/node'; import { markdownPreviewProvider } from "../markdownPreviewProvider"; import { DEBUG } from '../server/java/javaServerStarter'; @@ -29,6 +30,7 @@ export async function registerClientServerCommands(context: ExtensionContext, la registerCodeLensReferencesCommands(context, languageClient); registerValidationCommands(context); + registerRefactorCommands(context, languageClient); registerAssociationCommands(context, languageClient); registerRestartLanguageServerCommand(context, languageClient); @@ -182,7 +184,7 @@ async function grammarAssociationCommand(documentURI: Uri, languageClient: Langu if (!predefinedUrl || !predefinedUrl.startsWith('http')) { predefinedUrl = ''; } - grammarURI = await window.showInputBox({title:'Fill with schema / grammar URL' , value:predefinedUrl}); + grammarURI = await window.showInputBox({ title: 'Fill with schema / grammar URL', value: predefinedUrl }); } else { // step 2.1: Open a dialog to select the XSD, DTD, RelaxNG file to bind. const options: OpenDialogOptions = { @@ -366,3 +368,97 @@ function registerRestartLanguageServerCommand(context: ExtensionContext, languag })); } + +interface SurroundWithResponse { + start: TextEdit; + end: TextEdit; +} + +class SurroundWithKind { + + static readonly tags = 'tags'; + static readonly comments = 'comments'; + static readonly cdata = 'cdata'; + +} + +/** + * Register commands used for refactoring XML files + * + * @param context the extension context + */ +function registerRefactorCommands(context: ExtensionContext, languageClient: LanguageClient) { + + // Surround with Tags (Wrap) + context.subscriptions.push(commands.registerCommand(ClientCommandConstants.REFACTOR_SURROUND_WITH_TAGS, async () => { + await surroundWith(SurroundWithKind.tags, languageClient); + })); + + // Surround with Comments + context.subscriptions.push(commands.registerCommand(ClientCommandConstants.REFACTOR_SURROUND_WITH_COMMENTS, async () => { + await surroundWith(SurroundWithKind.comments, languageClient); + })); + + + // Surround with CDATA + context.subscriptions.push(commands.registerCommand(ClientCommandConstants.REFACTOR_SURROUND_WITH_CDATA, async () => { + await surroundWith(SurroundWithKind.cdata, languageClient); + })); +} + +async function surroundWith(surroundWithType: SurroundWithKind, languageClient: LanguageClient) { + const activeEditor = window.activeTextEditor; + if (!activeEditor) { + return; + } + const selection = activeEditor.selections[0]; + if (!selection) { + return; + } + + const uri = window.activeTextEditor.document.uri; + const identifier = TextDocumentIdentifier.create(uri.toString()); + const range = languageClient.code2ProtocolConverter.asRange(selection); + const supportedSnippet: boolean = vscode.SnippetTextEdit ? true : false; + + let result: SurroundWithResponse; + try { + result = await commands.executeCommand(ServerCommandConstants.REFACTOR_SURROUND_WITH, identifier, range, surroundWithType, supportedSnippet); + } catch (error) { + console.log(`Error while surround with : ${error}`); + } + + if (!result) { + return; + } + + const startTag = result.start.newText; + const endTag = result.end.newText; + + if (supportedSnippet) { + // SnippetTextEdit is supported, uses snippet (with choice) to manage cursor. + const startRange = languageClient.protocol2CodeConverter.asRange(result.start.range); + const endRange = languageClient.protocol2CodeConverter.asRange(result.end.range); + const snippetEdits = [new vscode.SnippetTextEdit(startRange, new SnippetString(startTag)), new vscode.SnippetTextEdit(endRange, new SnippetString(endTag))]; + const edit = new WorkspaceEdit(); + edit.set(activeEditor.document.uri, snippetEdits); + await workspace.applyEdit(edit); + } else { + // SnippetTextEdit is not supported, update start / end tag + const startPos = languageClient.protocol2CodeConverter.asPosition(result.start.range.start); + const endPos = languageClient.protocol2CodeConverter.asPosition(result.end.range.start); + activeEditor.edit((selectedText) => { + selectedText.insert(startPos, startTag); + selectedText.insert(endPos, endTag); + }) + + if (surroundWithType === SurroundWithKind.tags) { + // Force the show of completion + const pos = languageClient.protocol2CodeConverter.asPosition(result.start.range.start); + const posAfterStartBracket = new Position(pos.line, pos.character + 1); + activeEditor.selections = [new Selection(posAfterStartBracket, posAfterStartBracket)]; + commands.executeCommand("editor.action.triggerSuggest"); + } + } + +} diff --git a/src/commands/serverCommandConstants.ts b/src/commands/serverCommandConstants.ts index 7aab9dab..8d2eda44 100644 --- a/src/commands/serverCommandConstants.ts +++ b/src/commands/serverCommandConstants.ts @@ -24,9 +24,14 @@ export const ASSOCIATE_GRAMMAR_INSERT = "xml.associate.grammar.insert"; /** * Command to check if the current XML document is bound to a grammar */ -export const CHECK_BOUND_GRAMMAR = "xml.check.bound.grammar" +export const CHECK_BOUND_GRAMMAR = "xml.check.bound.grammar"; /** * Command to check if a given file pattern matches any file on the workspace */ -export const CHECK_FILE_PATTERN = "xml.check.file.pattern" \ No newline at end of file +export const CHECK_FILE_PATTERN = "xml.check.file.pattern"; + +/** + * Command to ... + */ + export const REFACTOR_SURROUND_WITH = "xml.refactor.surround.with"; \ No newline at end of file