Skip to content

Commit a833f7d

Browse files
angelozerrdatho7561
authored andcommitted
Wrap selection in XML element
Fixes #794 Signed-off-by: azerr <[email protected]>
1 parent 26e6c90 commit a833f7d

12 files changed

+374
-177
lines changed

Diff for: README.md

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ This VS Code extension provides support for creating and editing XML documents,
2020
| enabled by default | requires additional configuration to enable |
2121

2222
* [RelaxNG (experimental) support](https://github.com/redhat-developer/vscode-xml/blob/main/docs/Features/RelaxNGFeatures.md#relaxng-features) (since v0.22.0)
23+
* [Surround with Tags, Comments, CDATA](https://github.com/redhat-developer/vscode-xml/blob/main/docs/Refactor.md#refactor) (since v0.23.0)
2324
* Syntax error reporting
2425
* General code completion
2526
* [Auto-close tags](https://github.com/redhat-developer/vscode-xml/blob/main/docs/Features/XMLFeatures.md#xml-tag-auto-close)

Diff for: docs/Refactor.md

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Refactor
2+
3+
## Surround with Tags (Wrap)
4+
5+
This refactor command gives the capability to select an XML content and surround it with a given tag. To execute this command you can:
6+
7+
* use command palette (`Ctrl+P`) and type `Surround`
8+
9+
![Surround with Tags](images/Refactor/SurroundWithTags.gif)
10+
11+
* use contextual menu
12+
13+
If you prefer using keyboard to process `Surround with Tags (Wrap)`,you need to associate this command with a keybinding. See [Keyboard Shortcuts editor](https://code.visualstudio.com/docs/getstarted/keybindings#_keyboard-shortcuts-editor) for more informations.
14+
15+
## Surround with Comments
16+
17+
Similar to `Surround with Tags (Wrap)`, you can comment out the selected XML content:
18+
19+
![Surround with Tags](images/Refactor/SurroundWithComments.gif)
20+
21+
## Surround with CDATA
22+
23+
Similar to `Surround with Tags (Wrap)`, you can surround the selected XML content with CDATA:
24+
25+
![Surround with Tags](images/Refactor/SurroundWithCDATA.gif)

Diff for: docs/images/Refactor/SurroundWithCDATA.gif

129 KB
Loading

Diff for: docs/images/Refactor/SurroundWithComments.gif

123 KB
Loading

Diff for: docs/images/Refactor/SurroundWithTags.gif

177 KB
Loading

Diff for: package-lock.json

+155-155
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: package.json

+55-3
Original file line numberDiff line numberDiff line change
@@ -684,17 +684,69 @@
684684
"command": "xml.restart.language.server",
685685
"title": "Restart XML Language Server",
686686
"category": "XML"
687+
},
688+
{
689+
"command": "xml.refactor.surround.with.tags",
690+
"title": "Surround with Tags (Wrap)",
691+
"category": "XML"
692+
},
693+
{
694+
"command": "xml.refactor.surround.with.comments",
695+
"title": "Surround with Comments",
696+
"category": "XML"
697+
},
698+
{
699+
"command": "xml.refactor.surround.with.cdata",
700+
"title": "Surround with CDATA",
701+
"category": "XML"
687702
}
688703
],
689704
"menus": {
690705
"commandPalette": [
691706
{
692707
"command": "xml.validation.current.file",
693-
"when": "editorLangId == xml"
708+
"when": "editorLangId in xml.supportedLanguageIds && XMLLSReady"
709+
},
710+
{
711+
"command": "xml.validation.all.files",
712+
"when": "XMLLSReady"
694713
},
695714
{
696715
"command": "xml.command.bind.grammar",
697-
"when": "resourceFilename =~ /xml/ && editorIsOpen"
716+
"when": "resourceFilename =~ /xml/ && editorIsOpen && XMLLSReady"
717+
},
718+
{
719+
"command": "xml.restart.language.server",
720+
"when": "XMLLSReady"
721+
},
722+
{
723+
"command": "xml.refactor.surround.with.tags",
724+
"when": "editorLangId in xml.supportedLanguageIds && XMLLSReady"
725+
},
726+
{
727+
"command": "xml.refactor.surround.with.comments",
728+
"when": "editorLangId in xml.supportedLanguageIds && XMLLSReady"
729+
},
730+
{
731+
"command": "xml.refactor.surround.with.cdata",
732+
"when": "editorLangId in xml.supportedLanguageIds && XMLLSReady"
733+
}
734+
],
735+
"editor/context": [
736+
{
737+
"command": "xml.refactor.surround.with.tags",
738+
"when": "editorLangId in xml.supportedLanguageIds && XMLLSReady",
739+
"group": "1_modification"
740+
},
741+
{
742+
"command": "xml.refactor.surround.with.comments",
743+
"when": "editorLangId in xml.supportedLanguageIds && XMLLSReady",
744+
"group": "1_modification"
745+
},
746+
{
747+
"command": "xml.refactor.surround.with.cdata",
748+
"when": "editorLangId in xml.supportedLanguageIds && XMLLSReady",
749+
"group": "1_modification"
698750
}
699751
]
700752
},
@@ -705,4 +757,4 @@
705757
}
706758
]
707759
}
708-
}
760+
}

Diff for: src/client/xmlClient.ts

+18-11
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { TelemetryEvent } from '@redhat-developer/vscode-redhat-telemetry/lib';
22
import { commands, ExtensionContext, extensions, Position, TextDocument, TextEditor, Uri, window, workspace } from 'vscode';
3-
import { Command, ConfigurationParams, ConfigurationRequest, DidChangeConfigurationNotification, ExecuteCommandParams, LanguageClientOptions, MessageType, NotificationType, RequestType, RevealOutputChannelOn, TextDocumentPositionParams } from "vscode-languageclient";
3+
import { Command, ConfigurationParams, ConfigurationRequest, DidChangeConfigurationNotification, DocumentSelector, ExecuteCommandParams, LanguageClientOptions, MessageType, NotificationType, RequestType, RevealOutputChannelOn, State, TextDocumentPositionParams } from "vscode-languageclient";
44
import { Executable, LanguageClient } from 'vscode-languageclient/node';
55
import { XMLFileAssociation } from '../api/xmlExtensionApi';
66
import { registerClientServerCommands } from '../commands/registerCommands';
@@ -14,6 +14,17 @@ import * as Telemetry from '../telemetry';
1414
import { ClientErrorHandler } from './clientErrorHandler';
1515
import { activateTagClosing, AutoCloseResult } from './tagClosing';
1616

17+
export const XML_SUPPORTED_LANGUAGE_IDS = ['xml', 'xsl', 'dtd', 'svg'];
18+
19+
const XML_DOCUMENT_SELECTOR: DocumentSelector =
20+
XML_SUPPORTED_LANGUAGE_IDS
21+
.map(langId => ({ scheme: 'file', language: langId }))
22+
.concat(
23+
XML_SUPPORTED_LANGUAGE_IDS
24+
.map(langId => ({ scheme: 'untitled', language: langId }))
25+
);
26+
27+
1728
const ExecuteClientCommandRequest: RequestType<ExecuteCommandParams, any, void> = new RequestType('xml/executeClientCommand');
1829

1930
const TagCloseRequest: RequestType<TextDocumentPositionParams, AutoCloseResult, any> = new RequestType('xml/closeTag');
@@ -34,6 +45,11 @@ export async function startLanguageClient(context: ExtensionContext, executable:
3445
const languageClientOptions: LanguageClientOptions = getLanguageClientOptions(logfile, externalXmlSettings, requirementsData, context);
3546
languageClient = new LanguageClient('xml', 'XML Support', executable, languageClientOptions);
3647

48+
languageClient.onDidChangeState(e => {
49+
// Notify that XML language client is started / stoped
50+
commands.executeCommand('setContext', 'XMLLSReady', e.newState == State.Running);
51+
});
52+
3753
languageClient.onTelemetry(async (e: TelemetryEvent) => {
3854
if (e.name === Telemetry.SERVER_INITIALIZED_EVT) {
3955
e.properties[Telemetry.SETTINGS_EVT] = {
@@ -120,16 +136,7 @@ function getLanguageClientOptions(
120136
context: ExtensionContext): LanguageClientOptions {
121137
return {
122138
// Register the server for xml, xsl, dtd, svg
123-
documentSelector: [
124-
{ scheme: 'file', language: 'xml' },
125-
{ scheme: 'file', language: 'xsl' },
126-
{ scheme: 'file', language: 'dtd' },
127-
{ scheme: 'file', language: 'svg' },
128-
{ scheme: 'untitled', language: 'xml' },
129-
{ scheme: 'untitled', language: 'xsl' },
130-
{ scheme: 'untitled', language: 'dtd' },
131-
{ scheme: 'untitled', language: 'svg' }
132-
],
139+
documentSelector: XML_DOCUMENT_SELECTOR,
133140
revealOutputChannelOn: RevealOutputChannelOn.Never,
134141
//wrap with key 'settings' so it can be handled same a DidChangeConfiguration
135142
initializationOptions: {

Diff for: src/commands/clientCommandConstants.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,13 @@ export const EXECUTE_WORKSPACE_COMMAND = 'xml.workspace.executeCommand';
6565
/**
6666
* Command to restart connection to language server.
6767
*/
68-
export const RESTART_LANGUAGE_SERVER = 'xml.restart.language.server';
68+
export const RESTART_LANGUAGE_SERVER = 'xml.restart.language.server';
69+
70+
/**
71+
* Command to wrap element.
72+
*/
73+
export const REFACTOR_SURROUND_WITH_TAGS = 'xml.refactor.surround.with.tags';
74+
75+
export const REFACTOR_SURROUND_WITH_COMMENTS = 'xml.refactor.surround.with.comments';
76+
77+
export const REFACTOR_SURROUND_WITH_CDATA = 'xml.refactor.surround.with.cdata';

Diff for: src/commands/registerCommands.ts

+99-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as path from 'path';
2-
import { commands, ConfigurationTarget, env, ExtensionContext, OpenDialogOptions, Position, QuickPickItem, TextDocument, Uri, window, workspace, WorkspaceEdit } from "vscode";
3-
import { CancellationToken, ExecuteCommandParams, ExecuteCommandRequest, ReferencesRequest, TextDocumentEdit, TextDocumentIdentifier } from "vscode-languageclient";
2+
import * as vscode from 'vscode';
3+
import { commands, ConfigurationTarget, env, ExtensionContext, OpenDialogOptions, Position, QuickPickItem, SnippetString, TextDocument, Uri, window, workspace, WorkspaceEdit, Selection } from "vscode";
4+
import { CancellationToken, ExecuteCommandParams, ExecuteCommandRequest, ReferencesRequest, TextDocumentEdit, TextDocumentIdentifier, TextEdit } from "vscode-languageclient";
45
import { LanguageClient } from 'vscode-languageclient/node';
56
import { markdownPreviewProvider } from "../markdownPreviewProvider";
67
import { DEBUG } from '../server/java/javaServerStarter';
@@ -29,6 +30,7 @@ export async function registerClientServerCommands(context: ExtensionContext, la
2930

3031
registerCodeLensReferencesCommands(context, languageClient);
3132
registerValidationCommands(context);
33+
registerRefactorCommands(context, languageClient);
3234
registerAssociationCommands(context, languageClient);
3335
registerRestartLanguageServerCommand(context, languageClient);
3436

@@ -182,7 +184,7 @@ async function grammarAssociationCommand(documentURI: Uri, languageClient: Langu
182184
if (!predefinedUrl || !predefinedUrl.startsWith('http')) {
183185
predefinedUrl = '';
184186
}
185-
grammarURI = await window.showInputBox({title:'Fill with schema / grammar URL' , value:predefinedUrl});
187+
grammarURI = await window.showInputBox({ title: 'Fill with schema / grammar URL', value: predefinedUrl });
186188
} else {
187189
// step 2.1: Open a dialog to select the XSD, DTD, RelaxNG file to bind.
188190
const options: OpenDialogOptions = {
@@ -366,3 +368,97 @@ function registerRestartLanguageServerCommand(context: ExtensionContext, languag
366368

367369
}));
368370
}
371+
372+
interface SurroundWithResponse {
373+
start: TextEdit;
374+
end: TextEdit;
375+
}
376+
377+
class SurroundWithKind {
378+
379+
static readonly tags = 'tags';
380+
static readonly comments = 'comments';
381+
static readonly cdata = 'cdata';
382+
383+
}
384+
385+
/**
386+
* Register commands used for refactoring XML files
387+
*
388+
* @param context the extension context
389+
*/
390+
function registerRefactorCommands(context: ExtensionContext, languageClient: LanguageClient) {
391+
392+
// Surround with Tags (Wrap)
393+
context.subscriptions.push(commands.registerCommand(ClientCommandConstants.REFACTOR_SURROUND_WITH_TAGS, async () => {
394+
await surroundWith(SurroundWithKind.tags, languageClient);
395+
}));
396+
397+
// Surround with Comments
398+
context.subscriptions.push(commands.registerCommand(ClientCommandConstants.REFACTOR_SURROUND_WITH_COMMENTS, async () => {
399+
await surroundWith(SurroundWithKind.comments, languageClient);
400+
}));
401+
402+
403+
// Surround with CDATA
404+
context.subscriptions.push(commands.registerCommand(ClientCommandConstants.REFACTOR_SURROUND_WITH_CDATA, async () => {
405+
await surroundWith(SurroundWithKind.cdata, languageClient);
406+
}));
407+
}
408+
409+
async function surroundWith(surroundWithType: SurroundWithKind, languageClient: LanguageClient) {
410+
const activeEditor = window.activeTextEditor;
411+
if (!activeEditor) {
412+
return;
413+
}
414+
const selection = activeEditor.selections[0];
415+
if (!selection) {
416+
return;
417+
}
418+
419+
const uri = window.activeTextEditor.document.uri;
420+
const identifier = TextDocumentIdentifier.create(uri.toString());
421+
const range = languageClient.code2ProtocolConverter.asRange(selection);
422+
const supportedSnippet: boolean = vscode.SnippetTextEdit ? true : false;
423+
424+
let result: SurroundWithResponse;
425+
try {
426+
result = await commands.executeCommand(ServerCommandConstants.REFACTOR_SURROUND_WITH, identifier, range, surroundWithType, supportedSnippet);
427+
} catch (error) {
428+
console.log(`Error while surround with : ${error}`);
429+
}
430+
431+
if (!result) {
432+
return;
433+
}
434+
435+
const startTag = result.start.newText;
436+
const endTag = result.end.newText;
437+
438+
if (supportedSnippet) {
439+
// SnippetTextEdit is supported, uses snippet (with choice) to manage cursor.
440+
const startRange = languageClient.protocol2CodeConverter.asRange(result.start.range);
441+
const endRange = languageClient.protocol2CodeConverter.asRange(result.end.range);
442+
const snippetEdits = [new vscode.SnippetTextEdit(startRange, new SnippetString(startTag)), new vscode.SnippetTextEdit(endRange, new SnippetString(endTag))];
443+
const edit = new WorkspaceEdit();
444+
edit.set(activeEditor.document.uri, snippetEdits);
445+
await workspace.applyEdit(edit);
446+
} else {
447+
// SnippetTextEdit is not supported, update start / end tag
448+
const startPos = languageClient.protocol2CodeConverter.asPosition(result.start.range.start);
449+
const endPos = languageClient.protocol2CodeConverter.asPosition(result.end.range.start);
450+
activeEditor.edit((selectedText) => {
451+
selectedText.insert(startPos, startTag);
452+
selectedText.insert(endPos, endTag);
453+
})
454+
455+
if (surroundWithType === SurroundWithKind.tags) {
456+
// Force the show of completion
457+
const pos = languageClient.protocol2CodeConverter.asPosition(result.start.range.start);
458+
const posAfterStartBracket = new Position(pos.line, pos.character + 1);
459+
activeEditor.selections = [new Selection(posAfterStartBracket, posAfterStartBracket)];
460+
commands.executeCommand("editor.action.triggerSuggest");
461+
}
462+
}
463+
464+
}

Diff for: src/commands/serverCommandConstants.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,14 @@ export const ASSOCIATE_GRAMMAR_INSERT = "xml.associate.grammar.insert";
2424
/**
2525
* Command to check if the current XML document is bound to a grammar
2626
*/
27-
export const CHECK_BOUND_GRAMMAR = "xml.check.bound.grammar"
27+
export const CHECK_BOUND_GRAMMAR = "xml.check.bound.grammar";
2828

2929
/**
3030
* Command to check if a given file pattern matches any file on the workspace
3131
*/
32-
export const CHECK_FILE_PATTERN = "xml.check.file.pattern"
32+
export const CHECK_FILE_PATTERN = "xml.check.file.pattern";
33+
34+
/**
35+
* Command to surround with tags, comments, cdata
36+
*/
37+
export const REFACTOR_SURROUND_WITH = "xml.refactor.surround.with";

Diff for: src/extension.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,13 @@
1313
import * as fs from 'fs-extra';
1414
import * as os from 'os';
1515
import * as path from 'path';
16-
import { ExtensionContext, Uri, extensions, languages } from "vscode";
16+
import { ExtensionContext, Uri, extensions, languages, commands } from "vscode";
1717
import { Executable, LanguageClient } from 'vscode-languageclient/node';
1818
import { XMLExtensionApi } from './api/xmlExtensionApi';
1919
import { getXmlExtensionApiImplementation } from './api/xmlExtensionApiImplementation';
2020
import { cleanUpHeapDumps } from './client/clientErrorHandler';
2121
import { getIndentationRules } from './client/indentation';
22-
import { startLanguageClient } from './client/xmlClient';
22+
import { startLanguageClient, XML_SUPPORTED_LANGUAGE_IDS } from './client/xmlClient';
2323
import { registerClientOnlyCommands } from './commands/registerCommands';
2424
import { collectXmlJavaExtensions } from './plugin';
2525
import * as requirements from './server/requirements';
@@ -38,6 +38,8 @@ export async function activate(context: ExtensionContext): Promise<XMLExtensionA
3838

3939
languages.setLanguageConfiguration('xml', getIndentationRules());
4040
languages.setLanguageConfiguration('xsl', getIndentationRules());
41+
// Register in the context 'xml.supportedLanguageIds' to use it in command when condition in package.json
42+
commands.executeCommand('setContext', 'xml.supportedLanguageIds', XML_SUPPORTED_LANGUAGE_IDS);
4143

4244
let requirementsData: requirements.RequirementsData;
4345
try {

0 commit comments

Comments
 (0)