Skip to content

Commit e87c9fe

Browse files
authored
Merge pull request #455 from anteprimorac/feature/jsx-support
jsx support
2 parents 495a689 + e66879a commit e87c9fe

File tree

8 files changed

+653
-106
lines changed

8 files changed

+653
-106
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
.DS_Store
12
out
23
node_modules
34
.vscode-test/

.nvmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
12

package-lock.json

Lines changed: 340 additions & 64 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@
3030
"onLanguage:vue-html",
3131
"onLanguage:svelte",
3232
"onLanguage:erb",
33-
"onLanguage:nunjucks"
33+
"onLanguage:nunjucks",
34+
"onLanguage:javascript",
35+
"onLanguage:javascriptreact",
36+
"onLanguage:typescriptreact"
3437
],
3538
"repository": {
3639
"type": "git",
@@ -58,19 +61,23 @@
5861
"test": "node ./out/test/runTest.js"
5962
},
6063
"devDependencies": {
61-
"@types/glob": "^7.2.0",
64+
"@types/glob": "^8.0.1",
6265
"@types/mocha": "^9.1.1",
63-
"@types/node": "^12.20.52",
66+
"@types/node": "^12.20.55",
6467
"@types/vscode": "~1.42.0",
6568
"all-contributors-cli": "^6.24.0",
66-
"glob": "^8.0.2",
69+
"glob": "^8.1.0",
6770
"mocha": "^9.2.2",
68-
"prettier": "^2.8.0",
71+
"prettier": "^2.8.3",
6972
"tslint": "^6.1.3",
70-
"typescript": "^4.9.3",
73+
"typescript": "^4.9.5",
7174
"vscode-test": "^1.6.1"
7275
},
7376
"dependencies": {
74-
"vscode-html-languageservice": "^5.0.3"
77+
"@babel/parser": "^7.20.13",
78+
"@babel/traverse": "^7.20.13",
79+
"@babel/types": "^7.20.7",
80+
"@types/babel__traverse": "^7.18.3",
81+
"vscode-html-languageservice": "^5.0.4"
7582
}
7683
}

src/closing-labels-decorations.ts

Lines changed: 139 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,54 @@
11
import * as vscode from 'vscode';
2-
import { getLanguageService, LanguageService, SymbolKind } from 'vscode-html-languageservice';
2+
import {
3+
getLanguageService as getHTMLLanguageService,
4+
LanguageService as HTMLLanguageService,
5+
SymbolKind as HTMLSymbolKind,
6+
} from 'vscode-html-languageservice';
7+
import * as babelParser from '@babel/parser';
8+
import type { ParserPlugin } from '@babel/parser';
9+
import babelTraverse from '@babel/traverse';
10+
import type { JSXAttribute } from '@babel/types';
311

412
type HTMLEndTagDecoration = vscode.DecorationOptions & {
513
renderOptions: { after: { contentText: string } };
614
};
715

16+
function getJSXAttributeStringValue(attr?: JSXAttribute): string | undefined {
17+
if (attr?.value) {
18+
if (attr.value.type === 'StringLiteral' && typeof attr.value.value === 'string') {
19+
return attr.value.value;
20+
}
21+
22+
if (
23+
attr.value.type === 'JSXExpressionContainer' &&
24+
attr.value.expression.type === 'StringLiteral' &&
25+
typeof attr.value.expression.value === 'string'
26+
) {
27+
return attr.value.expression.value;
28+
}
29+
}
30+
31+
return undefined;
32+
}
33+
834
export default class ClosingLabelsDecorations implements vscode.Disposable {
935
private activeEditor?: vscode.TextEditor;
1036
private subscriptions: vscode.Disposable[] = [];
11-
private languageService?: LanguageService;
37+
private htmlLanguageService?: HTMLLanguageService;
1238
private updateTimeout?: NodeJS.Timeout;
1339

1440
private decorationType = this.createTextEditorDecoration();
1541

42+
private getHTMLLanguageService() {
43+
if (!this.htmlLanguageService) {
44+
this.htmlLanguageService = getHTMLLanguageService();
45+
}
46+
47+
return this.htmlLanguageService;
48+
}
49+
1650
constructor() {
1751
this.update = this.update.bind(this);
18-
this.languageService = getLanguageService();
1952

2053
this.subscriptions.push(
2154
vscode.workspace.onDidChangeConfiguration((event) => {
@@ -79,22 +112,17 @@ export default class ClosingLabelsDecorations implements vscode.Disposable {
79112
this.updateTimeout = setTimeout(this.update, 500);
80113
}
81114

82-
getDocumentDecorations(input: vscode.TextDocument) {
83-
if (!this.languageService) {
84-
return [];
85-
}
115+
getHTMLDocumentDecorations(input: vscode.TextDocument) {
116+
const htmlLanguageService = this.getHTMLLanguageService();
86117

87118
const document = { ...input, uri: input.uri.toString() };
88-
const symbols = this.languageService.findDocumentSymbols(
89-
document,
90-
this.languageService.parseHTMLDocument(document)
91-
);
119+
const symbols = htmlLanguageService.findDocumentSymbols(document, htmlLanguageService.parseHTMLDocument(document));
92120

93121
const decorations: HTMLEndTagDecoration[] = symbols
94122
.filter((symbol) => {
95123
// field symbol
96124
return (
97-
symbol.kind === SymbolKind.Field &&
125+
symbol.kind === HTMLSymbolKind.Field &&
98126
// isn't html document
99127
!symbol.name.startsWith('html') &&
100128
// isn't child of html
@@ -171,12 +199,109 @@ export default class ClosingLabelsDecorations implements vscode.Disposable {
171199
return decorations;
172200
}
173201

202+
getJSXDocumentDecorations(input: vscode.TextDocument, options?: { typescript?: boolean }) {
203+
const decorations: HTMLEndTagDecoration[] = [];
204+
205+
const plugins: ParserPlugin[] = ['jsx'];
206+
207+
if (options?.typescript) {
208+
plugins.push('typescript');
209+
}
210+
211+
const ast = babelParser.parse(input.getText(), {
212+
allowAwaitOutsideFunction: true,
213+
allowImportExportEverywhere: true,
214+
allowReturnOutsideFunction: true,
215+
allowSuperOutsideMethod: true,
216+
allowUndeclaredExports: true,
217+
attachComment: false,
218+
createParenthesizedExpressions: false,
219+
errorRecovery: true,
220+
ranges: true,
221+
strictMode: false,
222+
tokens: true,
223+
plugins,
224+
});
225+
226+
babelTraverse(ast, {
227+
JSXElement({ node }) {
228+
if (
229+
!node.selfClosing &&
230+
node.closingElement &&
231+
node.closingElement.loc &&
232+
node.openingElement.loc &&
233+
node.openingElement.name.type === 'JSXIdentifier' &&
234+
node.openingElement.name.name.toLowerCase() === node.openingElement.name.name &&
235+
node.openingElement.loc.end.line !== node.closingElement.loc.start.line
236+
) {
237+
let id: string | undefined;
238+
let className: string[] = [];
239+
const idAttr = node.openingElement.attributes.find(
240+
(attribute): attribute is JSXAttribute => attribute.type === 'JSXAttribute' && attribute.name.name === 'id'
241+
);
242+
const classNameAttr = node.openingElement.attributes.find(
243+
(attribute): attribute is JSXAttribute =>
244+
attribute.type === 'JSXAttribute' && attribute.name.name === 'className'
245+
);
246+
const idAttrVal = getJSXAttributeStringValue(idAttr);
247+
const classNameAttrVal = getJSXAttributeStringValue(classNameAttr);
248+
249+
if (idAttrVal) {
250+
id = idAttrVal.trim();
251+
252+
if (id.length < 1) {
253+
id = undefined;
254+
}
255+
}
256+
257+
if (classNameAttrVal) {
258+
className = classNameAttrVal
259+
.trim()
260+
.split(' ')
261+
.map((item) => item.trim())
262+
.filter((item) => item.length);
263+
}
264+
265+
if (id || className.length) {
266+
decorations.push({
267+
range: new vscode.Range(
268+
new vscode.Position(node.closingElement.loc.start.line - 1, node.closingElement.loc.start.column),
269+
new vscode.Position(node.closingElement.loc.end.line - 1, node.closingElement.loc.end.column)
270+
),
271+
renderOptions: {
272+
after: {
273+
contentText: '/' + (id ? `#${id}` : '') + (className.length > 0 ? `.${className.join('.')}` : ''),
274+
},
275+
},
276+
});
277+
}
278+
}
279+
},
280+
});
281+
282+
return decorations;
283+
}
284+
174285
update() {
175-
if (!this.languageService || !this.activeEditor) {
286+
if (!this.activeEditor) {
176287
return;
177288
}
178289

179-
this.activeEditor.setDecorations(this.decorationType, this.getDocumentDecorations(this.activeEditor.document));
290+
const languageId = this.activeEditor.document.languageId.toLowerCase();
291+
292+
if (['javascript', 'javascriptreact'].includes(languageId)) {
293+
this.activeEditor.setDecorations(this.decorationType, this.getJSXDocumentDecorations(this.activeEditor.document));
294+
} else if (languageId === 'typescriptreact') {
295+
this.activeEditor.setDecorations(
296+
this.decorationType,
297+
this.getJSXDocumentDecorations(this.activeEditor.document, { typescript: true })
298+
);
299+
} else {
300+
this.activeEditor.setDecorations(
301+
this.decorationType,
302+
this.getHTMLDocumentDecorations(this.activeEditor.document)
303+
);
304+
}
180305
}
181306

182307
public dispose() {

0 commit comments

Comments
 (0)