Skip to content

Commit 8c70e19

Browse files
committed
[skip ci] feat(huge-feature!): add jsxCompletionsMap for incredible jsx attributes customization!
feat: add enable by default fix for jsx completions snippets
1 parent 252feb1 commit 8c70e19

File tree

6 files changed

+106
-12
lines changed

6 files changed

+106
-12
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
"chokidar": "^3.5.3",
7070
"chokidar-cli": "^3.0.0",
7171
"delay": "^5.0.0",
72+
"escape-string-regexp": "^5.0.0",
7273
"eslint": "^8.7.0",
7374
"eslint-config-zardoy": "^0.2.8",
7475
"glob": "^8.0.3",

pnpm-lock.yaml

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

src/configurationType.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,4 +175,33 @@ export type Configuration = {
175175
* @default false
176176
*/
177177
patchOutline: boolean
178+
/**
179+
* Improve JSX completions:
180+
* - enable fixes
181+
* - enable jsxCompletionsMap
182+
* @default true
183+
*/
184+
improveJsxCompletions: boolean
185+
/**
186+
* Replace JSX completions by map with `tagName#attribute` pattern as keys
187+
* `tagName` can be ommited, but not `attribute` for now
188+
* Example usages:
189+
* - `#className`: `insertText: "={classNames$1}"`
190+
* - `button#type`: `insertText: "='button'"`
191+
* - `#on*`: `insertText: "={${1:($2) => $3}}"`
192+
* - `Table#someProp`: `insertText: "="something"`
193+
* Remove attribute:
194+
* - `children`: `false`
195+
* @default {}
196+
*/
197+
jsxCompletionsMap: {
198+
[rule: string]:
199+
| {
200+
insertText: string
201+
// TODO make it accept 'above'?
202+
/** Make original suggestion keep below patched */
203+
duplicate?: boolean
204+
}
205+
| false
206+
}
178207
}

typescript/src/completionsAtPosition.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import isInBannedPosition from './isInBannedPosition'
55
import { GetConfig } from './types'
66
import { findChildContainingPosition } from './utils'
77
import { isGoodPositionBuiltinMethodCompletion } from './isGoodPositionMethodCompletion'
8+
import improveJsxCompletions from './jsxCompletions'
89

910
export type PrevCompletionMap = Record<string, { originalName?: string; documentationOverride?: string | tslib.SymbolDisplayPart[] }>
1011

@@ -26,7 +27,7 @@ export const getCompletionsAtPosition = (
2627
const program = languageService.getProgram()
2728
const sourceFile = program?.getSourceFile(fileName)
2829
if (!program || !sourceFile) return
29-
if (!scriptSnapshot || isInBannedPosition(position, fileName, scriptSnapshot, sourceFile, languageService)) return
30+
if (!scriptSnapshot || isInBannedPosition(position, scriptSnapshot, sourceFile)) return
3031
let prior = languageService.getCompletionsAtPosition(fileName, position, options)
3132
// console.log(
3233
// 'raw prior',
@@ -208,6 +209,8 @@ export const getCompletionsAtPosition = (
208209
})
209210
}
210211

212+
if (c('improveJsxCompletions') && node) prior.entries = improveJsxCompletions(ts, prior.entries, node, position, sourceFile, c('jsxCompletionsMap'))
213+
211214
for (const rule of c('replaceSuggestions')) {
212215
let foundIndex: number
213216
const suggestion = prior.entries.find(({ name, kind }, index) => {

typescript/src/isInBannedPosition.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,7 @@
11
import type tslib from 'typescript/lib/tsserverlibrary'
22

3-
export default (
4-
position: number,
5-
fileName: string,
6-
scriptSnapshot: tslib.IScriptSnapshot,
7-
sourceFile: tslib.SourceFile,
8-
languageService: tslib.LanguageService,
9-
): boolean => {
10-
const { character } = languageService.toLineColumnOffset!(fileName, position)
3+
export default (position: number, scriptSnapshot: tslib.IScriptSnapshot, sourceFile: tslib.SourceFile): boolean => {
4+
const { character } = sourceFile.getLineAndCharacterOfPosition(position)
115
const textBeforePositionLine = scriptSnapshot?.getText(position - character, position)
126
const textAfterPositionLine = scriptSnapshot?.getText(position, sourceFile.getLineEndOfPosition(position))
137
if (textBeforePositionLine.trimStart() === 'import ' && textAfterPositionLine.trimStart().startsWith('from')) return true

typescript/src/jsxCompletions.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { compact } from '@zardoy/utils'
2+
import type tslib from 'typescript/lib/tsserverlibrary'
3+
import { Configuration } from '../../src/configurationType'
4+
import escapeStringRegexp from 'escape-string-regexp'
5+
6+
export default (
7+
ts: typeof tslib,
8+
entries: tslib.CompletionEntry[],
9+
node: tslib.Node,
10+
position: number,
11+
sourceFile: tslib.SourceFile,
12+
jsxCompletionsMap: Configuration['jsxCompletionsMap'],
13+
): tslib.CompletionEntry[] => {
14+
// TODO refactor to findNodeAtPosition
15+
if (ts.isIdentifier(node)) node = node.parent
16+
if (ts.isJsxAttribute(node) && node.initializer) {
17+
entries = entries.map(entry => {
18+
return {
19+
...entry,
20+
insertText: entry.name,
21+
}
22+
})
23+
}
24+
if (ts.isJsxAttribute(node)) node = node.parent
25+
if (ts.isJsxAttributes(node)) node = node.parent
26+
if (Object.keys(jsxCompletionsMap).length && (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node))) {
27+
const tagName = node.tagName.getText()
28+
// TODO use the same perf optimization for replaceSuggestions
29+
const patchEntries: Record<number, Configuration['jsxCompletionsMap'][string]> = {}
30+
for (let [key, patchMethod] of Object.entries(jsxCompletionsMap)) {
31+
const splitTagNameIdx = key.indexOf('#')
32+
if (splitTagNameIdx === -1) continue
33+
const comparingTagName = key.slice(0, splitTagNameIdx)
34+
if (comparingTagName && comparingTagName !== tagName) continue
35+
const comparingName = key.slice(splitTagNameIdx + 1)
36+
if (comparingName.includes('*')) {
37+
const regexMatch = new RegExp(escapeStringRegexp(comparingName).replaceAll('\\*', '.*'))
38+
entries.forEach(({ name, kind }, index) => {
39+
if (kind === ts.ScriptElementKind.memberVariableElement && regexMatch.test(name)) {
40+
patchEntries[index] = patchMethod
41+
}
42+
})
43+
} else {
44+
// I think it needs some sort of optimization by using wordRange
45+
const indexToPatch = entries.findIndex(({ name, kind }) => kind === ts.ScriptElementKind.memberVariableElement && name === comparingName)
46+
if (indexToPatch === -1) continue
47+
patchEntries[indexToPatch] = patchMethod
48+
}
49+
}
50+
entries = compact(
51+
entries.flatMap((entry, i) => {
52+
const patchMethod = patchEntries[i]
53+
if (patchMethod === undefined) return entry
54+
if (patchMethod === false) return
55+
const patchedEntry: tslib.CompletionEntry = { ...entry, insertText: entry.name + patchMethod.insertText, isSnippet: true }
56+
return patchMethod.duplicate ? [patchedEntry, entry] : patchedEntry
57+
}),
58+
)
59+
}
60+
return entries
61+
}

0 commit comments

Comments
 (0)