Skip to content

Commit 3aca612

Browse files
committed
feat: rewrite emmet support. Replace jsxPseudoEmmet.enable setting with jsxEmmet.type
you can opt into disabling or using real emmet completions (they are broken) +perf
1 parent 6a33538 commit 3aca612

File tree

5 files changed

+148
-39
lines changed

5 files changed

+148
-39
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
},
5050
"dependencies": {
5151
"@types/lodash": "^4.14.182",
52+
"@vscode/emmet-helper": "^2.8.4",
5253
"@zardoy/vscode-utils": "^0.0.9",
5354
"chokidar": "^3.5.3",
5455
"eslint": "^8.7.0",

pnpm-lock.yaml

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

src/configurationType.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,11 +94,10 @@ export type Configuration = {
9494
* */
9595
'removeCodeFixes.codefixes': ('fixMissingMember' | 'fixMissingProperties' | 'fixMissingAttributes' | 'fixMissingFunctionDeclaration')[]
9696
/**
97-
* @experimental
9897
* Only tag support
99-
* @default true
98+
* @default fakeEmmet
10099
* */
101-
'jsxPseudoEmmet.enable': boolean
100+
'jsxEmmet.type': 'realEmmet' | 'fakeEmmet' | 'disabled'
102101
/**
103102
* Sorting matters
104103
*/

src/configurationTypeCache.jsonc

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// GENERATED. DON'T EDIT MANUALLY
2-
// md5hash: 9aa6b84ad1d774daa0a3a7d9bb9f723b
2+
// md5hash: ba4b89b4b50869a61ab291839b74492d
33
{
44
"type": "object",
55
"properties": {
@@ -60,9 +60,15 @@
6060
"type": "string"
6161
}
6262
},
63-
"jsxPseudoEmmet.enable": {
64-
"default": true,
65-
"type": "boolean"
63+
"jsxEmmet.type": {
64+
"description": "Only tag support",
65+
"default": "fakeEmmet",
66+
"enum": [
67+
"disabled",
68+
"fakeEmmet",
69+
"realEmmet"
70+
],
71+
"type": "string"
6672
},
6773
"jsxPseudoEmmet.tags": {
6874
"description": "Sorting matters",
@@ -222,8 +228,8 @@
222228
"required": [
223229
"correctSorting.enable",
224230
"highlightNonFunctionMethods.enable",
231+
"jsxEmmet.type",
225232
"jsxImproveElementsSuggestions.enabled",
226-
"jsxPseudoEmmet.enable",
227233
"jsxPseudoEmmet.tags",
228234
"markTsCodeActions.enable",
229235
"markTsCodeFixes.character",

typescript/src/index.ts

Lines changed: 78 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import get from 'lodash.get'
22
import type tslib from 'typescript/lib/tsserverlibrary'
3+
import * as emmet from '@vscode/emmet-helper'
34

45
//@ts-ignore
56
import type { Configuration } from '../../src/configurationType'
67

78
export = function ({ typescript }: { typescript: typeof import('typescript/lib/tsserverlibrary') }) {
9+
const ts = typescript
810
let _configuration: Configuration
911
const c = <T extends keyof Configuration>(key: T): Configuration[T] => get(_configuration, key)
1012

@@ -22,7 +24,7 @@ export = function ({ typescript }: { typescript: typeof import('typescript/lib/t
2224
proxy[k] = (...args: Array<Record<string, unknown>>) => x.apply(info.languageService, args)
2325
}
2426

25-
let prevCompletionsMap: any
27+
let prevCompletionsMap: Record<string, { originalName: string }>
2628
proxy.getCompletionsAtPosition = (fileName, position, options) => {
2729
prevCompletionsMap = {}
2830
if (!_configuration) {
@@ -40,24 +42,38 @@ export = function ({ typescript }: { typescript: typeof import('typescript/lib/t
4042
// 'raw prior',
4143
// prior?.entries.map(entry => entry.name),
4244
// )
43-
const node = findChildContainingPosition(typescript, sourceFile, position)
44-
if (node) {
45-
if (c('jsxPseudoEmmet.enable') && (typescript.isJsxElement(node) || (node.parent && typescript.isJsxElement(node.parent)))) {
46-
if (typescript.isJsxOpeningElement(node)) {
47-
const nodeText = node.getText().slice(0, position - node.pos)
48-
if (c('jsxImproveElementsSuggestions.enabled') && !nodeText.includes(' ') && prior) {
49-
let lastPart = nodeText.split('.').at(-1)!
50-
if (lastPart.startsWith('<')) lastPart = lastPart.slice(1)
51-
const isStartingWithUpperCase = (str: string) => str[0] === str[0]?.toUpperCase()
52-
// check if starts with lowercase
53-
if (isStartingWithUpperCase(lastPart)) {
54-
// TODO! compare with suggestions from lib.dom
55-
prior.entries = prior.entries.filter(
56-
entry => isStartingWithUpperCase(entry.name) && ![typescript.ScriptElementKind.enumElement].includes(entry.kind),
57-
)
58-
}
45+
if (['.jsx', '.tsx'].some(ext => fileName.endsWith(ext))) {
46+
// JSX Features
47+
const node = findChildContainingPosition(typescript, sourceFile, position)
48+
if (node) {
49+
const { SyntaxKind } = ts
50+
const emmetSyntaxKinds = [SyntaxKind.JsxFragment, SyntaxKind.JsxElement, SyntaxKind.JsxText]
51+
const emmetClosingSyntaxKinds = [SyntaxKind.JsxClosingElement, SyntaxKind.JsxClosingFragment]
52+
// TODO maybe allow fragment?
53+
const correntComponentSuggestionsKinds = [SyntaxKind.JsxOpeningElement, SyntaxKind.JsxSelfClosingElement]
54+
const nodeText = node.getFullText().slice(0, position - node.pos)
55+
if (
56+
correntComponentSuggestionsKinds.includes(node.kind) &&
57+
c('jsxImproveElementsSuggestions.enabled') &&
58+
!nodeText.includes(' ') &&
59+
prior
60+
) {
61+
let lastPart = nodeText.split('.').at(-1)!
62+
if (lastPart.startsWith('<')) lastPart = lastPart.slice(1)
63+
const isStartingWithUpperCase = (str: string) => str[0] === str[0]?.toUpperCase()
64+
// check if starts with lowercase
65+
if (isStartingWithUpperCase(lastPart)) {
66+
// TODO! compare with suggestions from lib.dom
67+
prior.entries = prior.entries.filter(
68+
entry => isStartingWithUpperCase(entry.name) && ![typescript.ScriptElementKind.enumElement].includes(entry.kind),
69+
)
5970
}
60-
} else if (!typescript.isJsxClosingElement(node) /* TODO! scriptSnapshot.getText(position - 1, position).match(/(\s|\w|>)/) */) {
71+
}
72+
if (
73+
c('jsxEmmet.type') !== 'disabled' &&
74+
(emmetSyntaxKinds.includes(node.kind) ||
75+
/* Just before closing tag */ (emmetClosingSyntaxKinds.includes(node.kind) && nodeText.length === 0))
76+
) {
6177
// const { textSpan } = proxy.getSmartSelectionRange(fileName, position)
6278
// let existing = scriptSnapshot.getText(textSpan.start, textSpan.start + textSpan.length)
6379
// if (existing.includes('\n')) existing = ''
@@ -72,18 +88,47 @@ export = function ({ typescript }: { typescript: typeof import('typescript/lib/t
7288
// isSnippet: true,
7389
// })
7490
// } else if (!existing[0] || existing[0].match(/\w/)) {
75-
const tags = c('jsxPseudoEmmet.tags')
76-
for (let [tag, value] of Object.entries(tags)) {
77-
if (value === true) value = `<${tag}>$1</${tag}>`
78-
prior.entries.push({
79-
kind: typescript.ScriptElementKind.label,
80-
name: tag,
81-
sortText: '!5',
82-
insertText: value,
83-
isSnippet: true,
84-
})
91+
if (c('jsxEmmet.type') === 'realEmmet') {
92+
const sendToEmmet = nodeText.split(' ').at(-1)!
93+
const emmetCompletions = emmet.doComplete(
94+
{
95+
getText: () => sendToEmmet,
96+
languageId: 'html',
97+
lineCount: 1,
98+
offsetAt: position => position.character,
99+
positionAt: offset => ({ line: 0, character: offset }),
100+
uri: '/',
101+
version: 1,
102+
},
103+
{ line: 0, character: sendToEmmet.length },
104+
'html',
105+
{},
106+
) ?? { items: [] }
107+
for (const completion of emmetCompletions.items) {
108+
prior.entries.push({
109+
kind: typescript.ScriptElementKind.label,
110+
name: completion.label.slice(1),
111+
sortText: '!5',
112+
// insertText: `${completion.label.slice(1)} ${completion.textEdit?.newText}`,
113+
insertText: completion.textEdit?.newText,
114+
isSnippet: true,
115+
sourceDisplay: completion.detail !== undefined ? [{ kind: 'text', text: completion.detail }] : undefined,
116+
// replacementSpan: { start: position - 5, length: 5 },
117+
})
118+
}
119+
} else {
120+
const tags = c('jsxPseudoEmmet.tags')
121+
for (let [tag, value] of Object.entries(tags)) {
122+
if (value === true) value = `<${tag}>$1</${tag}>`
123+
prior.entries.push({
124+
kind: typescript.ScriptElementKind.label,
125+
name: tag,
126+
sortText: '!5',
127+
insertText: value,
128+
isSnippet: true,
129+
})
130+
}
85131
}
86-
// }
87132
}
88133
}
89134
}
@@ -107,7 +152,9 @@ export = function ({ typescript }: { typescript: typeof import('typescript/lib/t
107152
prior.entries = prior.entries.map(entry => {
108153
if (!standardProps.includes(entry.name)) {
109154
const newName = `☆${entry.name}`
110-
prevCompletionsMap[newName] = entry.name
155+
prevCompletionsMap[newName] = {
156+
originalName: entry.name,
157+
}
111158
return {
112159
...entry,
113160
insertText: entry.insertText ?? entry.name,
@@ -176,7 +223,7 @@ export = function ({ typescript }: { typescript: typeof import('typescript/lib/t
176223
const prior = info.languageService.getCompletionEntryDetails(
177224
fileName,
178225
position,
179-
prevCompletionsMap[entryName] || entryName,
226+
prevCompletionsMap[entryName]?.originalName || entryName,
180227
formatOptions,
181228
source,
182229
preferences,

0 commit comments

Comments
 (0)