Skip to content

Commit eb23517

Browse files
authored
(feat) html rename tags pair (#798)
#791 Support for renaming a html element start/end tag pair. Doesn't apply to components, that is handled by the typescript plugin.
1 parent 6705514 commit eb23517

File tree

5 files changed

+152
-10
lines changed

5 files changed

+152
-10
lines changed

packages/language-server/src/ls-config.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ const defaultLSConfig: LSConfig = {
3636
hover: { enable: true },
3737
completions: { enable: true, emmet: true },
3838
tagComplete: { enable: true },
39-
documentSymbols: { enable: true }
39+
documentSymbols: { enable: true },
40+
renameTags: { enable: true }
4041
},
4142
svelte: {
4243
enable: true,
@@ -152,6 +153,9 @@ export interface LSHTMLConfig {
152153
documentSymbols: {
153154
enable: boolean;
154155
};
156+
renameTags: {
157+
enable: boolean;
158+
};
155159
}
156160

157161
export type CompilerWarningsSettings = Record<string, 'ignore' | 'error'>;

packages/language-server/src/plugins/html/HTMLPlugin.ts

Lines changed: 76 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import { getEmmetCompletionParticipants } from 'vscode-emmet-helper';
22
import {
33
getLanguageService,
44
HTMLDocument,
5-
CompletionItem as HtmlCompletionItem
5+
CompletionItem as HtmlCompletionItem,
6+
Node
67
} from 'vscode-html-languageservice';
78
import {
89
CompletionList,
@@ -11,7 +12,9 @@ import {
1112
SymbolInformation,
1213
CompletionItem,
1314
CompletionItemKind,
14-
TextEdit
15+
TextEdit,
16+
Range,
17+
WorkspaceEdit
1518
} from 'vscode-languageserver';
1619
import {
1720
DocumentManager,
@@ -21,10 +24,10 @@ import {
2124
} from '../../lib/documents';
2225
import { LSConfigManager, LSHTMLConfig } from '../../ls-config';
2326
import { svelteHtmlDataProvider } from './dataProvider';
24-
import { HoverProvider, CompletionsProvider } from '../interfaces';
25-
import { isInsideMoustacheTag } from '../../lib/documents/utils';
27+
import { HoverProvider, CompletionsProvider, RenameProvider } from '../interfaces';
28+
import { isInsideMoustacheTag, toRange } from '../../lib/documents/utils';
2629

27-
export class HTMLPlugin implements HoverProvider, CompletionsProvider {
30+
export class HTMLPlugin implements HoverProvider, CompletionsProvider, RenameProvider {
2831
private configManager: LSConfigManager;
2932
private lang = getLanguageService({
3033
customDataProviders: [svelteHtmlDataProvider],
@@ -51,10 +54,7 @@ export class HTMLPlugin implements HoverProvider, CompletionsProvider {
5154
}
5255

5356
const node = html.findNodeAt(document.offsetAt(position));
54-
if (!node || node.tag?.[0].match(/[A-Z]/)) {
55-
// The language service is case insensitive, and would provide
56-
// hover info for Svelte components like `Option` which have
57-
// the same name like a html tag.
57+
if (!node || this.possiblyComponent(node)) {
5858
return null;
5959
}
6060

@@ -206,6 +206,73 @@ export class HTMLPlugin implements HoverProvider, CompletionsProvider {
206206
return this.lang.findDocumentSymbols(document, html);
207207
}
208208

209+
rename(document: Document, position: Position, newName: string): WorkspaceEdit | null {
210+
if (!this.featureEnabled('renameTags')) {
211+
return null;
212+
}
213+
const html = this.documents.get(document);
214+
if (!html) {
215+
return null;
216+
}
217+
218+
const node = html.findNodeAt(document.offsetAt(position));
219+
if (!node || this.possiblyComponent(node)) {
220+
return null;
221+
}
222+
223+
return this.lang.doRename(document, position, newName, html);
224+
}
225+
226+
prepareRename(document: Document, position: Position): Range | null {
227+
if (!this.featureEnabled('renameTags')) {
228+
return null;
229+
}
230+
231+
const html = this.documents.get(document);
232+
if (!html) {
233+
return null;
234+
}
235+
236+
const offset = document.offsetAt(position);
237+
const node = html.findNodeAt(offset);
238+
if (
239+
!node ||
240+
this.possiblyComponent(node) ||
241+
!node.tag ||
242+
!this.isRenameAtTag(node, offset)
243+
) {
244+
return null;
245+
}
246+
const tagNameStart = node.start + '<'.length;
247+
248+
return toRange(document.getText(), tagNameStart, tagNameStart + node.tag.length);
249+
}
250+
251+
/**
252+
*
253+
* The language service is case insensitive, and would provide
254+
* hover info for Svelte components like `Option` which have
255+
* the same name like a html tag.
256+
*/
257+
private possiblyComponent(node: Node): boolean {
258+
return !!node.tag?.[0].match(/[A-Z]/);
259+
}
260+
261+
/**
262+
* Returns true if rename happens at the tag name, not anywhere inbetween.
263+
*/
264+
private isRenameAtTag(node: Node, offset: number): boolean {
265+
if (!node.tag) {
266+
return false;
267+
}
268+
269+
const startTagNameEnd = node.start + `<${node.tag}`.length;
270+
const endTagNameStart = node.end - `${node.tag}>`.length;
271+
const isAtStartTag = offset > node.start && offset <= startTagNameEnd;
272+
const isAtEndTag = offset >= endTagNameStart && offset < node.end;
273+
return isAtStartTag || isAtEndTag;
274+
}
275+
209276
private featureEnabled(feature: keyof LSHTMLConfig) {
210277
return (
211278
this.configManager.enabled('html.enable') &&

packages/language-server/test/plugins/html/HTMLPlugin.test.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,4 +116,65 @@ describe('HTML Plugin', () => {
116116
undefined
117117
);
118118
});
119+
120+
it('does not provide rename for element being uppercase', async () => {
121+
const { plugin, document } = setup('<Div></Div>');
122+
123+
assert.deepStrictEqual(plugin.prepareRename(document, Position.create(0, 2)), null);
124+
assert.deepStrictEqual(plugin.rename(document, Position.create(0, 2), 'p'), null);
125+
});
126+
127+
it('does not provide rename for valid element but incorrect position', () => {
128+
const { plugin, document } = setup('<div on:click={ab => ab}>asd</div>');
129+
const newName = 'p';
130+
131+
assert.deepStrictEqual(plugin.prepareRename(document, Position.create(0, 16)), null);
132+
assert.deepStrictEqual(plugin.prepareRename(document, Position.create(0, 5)), null);
133+
assert.deepStrictEqual(plugin.prepareRename(document, Position.create(0, 26)), null);
134+
135+
assert.deepStrictEqual(plugin.rename(document, Position.create(0, 16), newName), null);
136+
assert.deepStrictEqual(plugin.rename(document, Position.create(0, 5), newName), null);
137+
assert.deepStrictEqual(plugin.rename(document, Position.create(0, 26), newName), null);
138+
});
139+
140+
it('provides rename for element', () => {
141+
const { plugin, document } = setup('<div on:click={() => {}}></div>');
142+
const newName = 'p';
143+
144+
const pepareRenameInfo = Range.create(Position.create(0, 1), Position.create(0, 4));
145+
assert.deepStrictEqual(
146+
plugin.prepareRename(document, Position.create(0, 2)),
147+
pepareRenameInfo
148+
);
149+
assert.deepStrictEqual(
150+
plugin.prepareRename(document, Position.create(0, 28)),
151+
pepareRenameInfo
152+
);
153+
154+
const renameInfo = {
155+
changes: {
156+
[document.uri]: [
157+
{
158+
newText: 'p',
159+
range: {
160+
start: { line: 0, character: 1 },
161+
end: { line: 0, character: 4 }
162+
}
163+
},
164+
{
165+
newText: 'p',
166+
range: {
167+
start: { line: 0, character: 27 },
168+
end: { line: 0, character: 30 }
169+
}
170+
}
171+
]
172+
}
173+
};
174+
assert.deepStrictEqual(plugin.rename(document, Position.create(0, 2), newName), renameInfo);
175+
assert.deepStrictEqual(
176+
plugin.rename(document, Position.create(0, 28), newName),
177+
renameInfo
178+
);
179+
});
119180
});

packages/svelte-vscode/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,10 @@ Enable HTML tag auto closing. _Default_: `true`
190190

191191
Enable document symbols for HTML. _Default_: `true`
192192

193+
##### `svelte.plugin.html.renameTags.enable`
194+
195+
Enable rename tags for the opening/closing tag pairs in HTML. _Default_: `true`
196+
193197
##### `svelte.plugin.svelte.enable`
194198

195199
Enable the Svelte plugin. _Default_: `true`

packages/svelte-vscode/package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,12 @@
232232
"title": "HTML: Symbols in Outline",
233233
"description": "Enable document symbols for HTML"
234234
},
235+
"svelte.plugin.html.renameTags.enable": {
236+
"type": "boolean",
237+
"default": true,
238+
"title": "HTML: Rename tags",
239+
"description": "Enable rename for the opening/closing tag pairs in HTML"
240+
},
235241
"svelte.plugin.svelte.enable": {
236242
"type": "boolean",
237243
"default": true,

0 commit comments

Comments
 (0)