Skip to content

Commit 0aa9607

Browse files
authored
Merge pull request #210 from zardoy/develop
2 parents 0ae5b20 + 16743dd commit 0aa9607

23 files changed

+212
-40
lines changed

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"name": "ts-essential-plugins",
33
"displayName": "TypeScript Essential Plugins",
4+
"description": "50+ features: TS extension for professionals",
45
"version": "0.0.0-dev",
56
"license": "MIT",
67
"web": true,

src/autoCompletionsTrigger.ts

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import * as vscode from 'vscode'
2+
import { defaultLanguageSupersets } from '@zardoy/vscode-utils/build/langs'
3+
import { getExtensionSetting } from 'vscode-framework'
4+
import { sendCommand } from './sendCommand'
5+
6+
const jsxAttributesAutoTrigger = () => {
7+
vscode.workspace.onDidChangeTextDocument(async ({ contentChanges, document, reason }) => {
8+
const editor = vscode.window.activeTextEditor
9+
if (document !== editor?.document || contentChanges.length === 0) return
10+
if (contentChanges[0]!.text !== ' ') return
11+
if (![...defaultLanguageSupersets.react, 'javascript'].includes(document.languageId)) return
12+
if (!getExtensionSetting('completionsAutoTrigger.jsx')) return
13+
const path = await sendCommand('getNodePath', { document, position: editor.selection.active })
14+
if (!path) return
15+
if (['JsxSelfClosingElement', 'JsxOpeningElement'].includes(path.at(-1)?.kindName ?? '')) {
16+
await vscode.commands.executeCommand('editor.action.triggerSuggest')
17+
}
18+
})
19+
}
20+
21+
export default () => {
22+
jsxAttributesAutoTrigger()
23+
}

src/configurationType.ts

+13
Original file line numberDiff line numberDiff line change
@@ -685,6 +685,11 @@ export type Configuration = {
685685
*/
686686
declareMissingPropertyQuickfixOtherFiles: boolean
687687
/**
688+
* @recommended {".svg": {
689+
* "importPath": "$path?react",
690+
* "prefix": "Svg",
691+
* "nameCasing": "pascal"
692+
* },
688693
* @default {}
689694
*/
690695
filesAutoImport: {
@@ -708,6 +713,14 @@ export type Configuration = {
708713
iconPost?: string
709714
}
710715
}
716+
/**
717+
* @default true
718+
*/
719+
'completionsAutoTrigger.jsx': boolean
720+
/**
721+
* @default false
722+
*/
723+
'inlayHints.missingJsxAttributes.enabled': boolean
711724
}
712725

713726
// scrapped using search editor. config: caseInsensitive, context lines: 0, regex: const fix\w+ = "[^ ]+"

src/extension.ts

+4
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import moreCompletions from './moreCompletions'
1717
import { mergeSettingsFromScopes } from './mergeSettings'
1818
import codeActionProvider from './codeActionProvider'
1919
import nonTsCommands from './nonTsCommands'
20+
import inlayHints from './inlayHints'
21+
import autoCompletionsTrigger from './autoCompletionsTrigger'
2022

2123
let isActivated = false
2224
// let erroredStatusBarItem: vscode.StatusBarItem | undefined
@@ -96,6 +98,8 @@ export const activateTsPlugin = (tsApi: { configurePlugin; onCompletionAccepted
9698

9799
figIntegration()
98100
vueVolarSupport()
101+
inlayHints()
102+
autoCompletionsTrigger()
99103

100104
if (process.env.PLATFORM === 'node' && process.env.NODE_ENV === 'development') {
101105
require('./autoPluginReload').default()

src/inlayHints.ts

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import * as vscode from 'vscode'
2+
import { watchExtensionSetting } from '@zardoy/vscode-utils/build/settings'
3+
import { getExtensionSetting, registerActiveDevelopmentCommand } from 'vscode-framework'
4+
5+
// todo respect enabled setting, deactivate
6+
export default () => {
7+
const provider = new (class implements vscode.InlayHintsProvider {
8+
eventEmitter = new vscode.EventEmitter<void>()
9+
onDidChangeInlayHints = this.eventEmitter.event
10+
provideInlayHints(document: vscode.TextDocument, range: vscode.Range, token: vscode.CancellationToken): vscode.ProviderResult<vscode.InlayHint[]> {
11+
const diagnostics = vscode.languages.getDiagnostics(document.uri)
12+
const jsxMissingAttributesErrors = diagnostics.filter(({ code, source }) => (code === 2740 || code === 2739) && source === 'ts')
13+
return jsxMissingAttributesErrors
14+
.flatMap(({ range, message }) => {
15+
const regex = /: (?<prop>[\w, ]+)(?:, and (?<more>\d+) more)?\.?$/
16+
const match = regex.exec(message)
17+
if (!match) return null as never
18+
const props = match.groups!.prop!.split(', ')
19+
const { more } = match.groups!
20+
let text = ` ${props.map(prop => `${prop}!`).join(', ')}`
21+
if (more) text += `, and ${more} more`
22+
return {
23+
kind: vscode.InlayHintKind.Type,
24+
label: text,
25+
tooltip: `Inlay hint: Missing attributes`,
26+
position: range.end,
27+
paddingLeft: true,
28+
} satisfies vscode.InlayHint
29+
// return [...props, ...(more ? [more] : [])].map((prop) => ({
30+
// kind: vscode.InlayHintKind.Type,
31+
// label: prop,
32+
// tooltip: 'Missing attribute',
33+
// position:
34+
// }))
35+
})
36+
.filter(Boolean)
37+
}
38+
})()
39+
let disposables = [] as vscode.Disposable[]
40+
41+
const manageEnablement = () => {
42+
if (getExtensionSetting('inlayHints.missingJsxAttributes.enabled')) {
43+
vscode.languages.registerInlayHintsProvider('typescriptreact,javascript,javascriptreact'.split(','), provider)
44+
vscode.languages.onDidChangeDiagnostics(e => {
45+
for (const uri of e.uris) {
46+
if (uri === vscode.window.activeTextEditor?.document.uri) provider.eventEmitter.fire()
47+
}
48+
})
49+
} else {
50+
for (const d of disposables) d.dispose()
51+
disposables = []
52+
}
53+
}
54+
55+
manageEnablement()
56+
watchExtensionSetting('inlayHints.missingJsxAttributes.enabled', manageEnablement)
57+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { ExtendedCodeAction } from '../getCodeActions'
2+
3+
const errorCodes = [
4+
// ts.Diagnostics.Property_0_does_not_exist_on_type_1.code,
5+
// ts.Diagnostics.Property_0_does_not_exist_on_type_1_Did_you_mean_2.code,
6+
// ts.Diagnostics.Property_0_is_missing_in_type_1_but_required_in_type_2.code,
7+
// ts.Diagnostics.Type_0_is_missing_the_following_properties_from_type_1_Colon_2.code,
8+
// ts.Diagnostics.Type_0_is_missing_the_following_properties_from_type_1_Colon_2_and_3_more.code,
9+
// // ts.Diagnostics.Argument_of_type_0_is_not_assignable_to_parameter_of_type_1.code,
10+
// // ts.Diagnostics.Cannot_find_name_0.code,
11+
2339, 2551, 2741, 2739, 2740 /* 2345, 2304, */,
12+
]
13+
14+
export default {
15+
codes: errorCodes,
16+
kind: 'quickfix',
17+
title: 'Declare missing attributes',
18+
tryToApply({ sourceFile, node, c, languageService, position, formatOptions, range }) {
19+
// todo maybe cache from prev request?
20+
if (!node) return
21+
const codeFixes = languageService.getCodeFixesAtPosition(
22+
sourceFile.fileName,
23+
node.getStart(),
24+
range?.end ?? node.getStart(),
25+
errorCodes,
26+
formatOptions ?? {},
27+
{},
28+
)
29+
const fix = codeFixes.find(codeFix => codeFix.fixName === 'fixMissingAttributes')
30+
if (fix && fix.changes[0]?.textChanges.length === 1) {
31+
const changes = fix.changes[0]!.textChanges
32+
let i = 1
33+
return {
34+
snippetEdits: [
35+
{
36+
newText: changes[0]!.newText.replaceAll('$', '\\$').replaceAll('={undefined}', () => `={$${i++}}`),
37+
span: fix.changes[0]!.textChanges[0]!.span,
38+
},
39+
],
40+
}
41+
}
42+
return
43+
},
44+
} as ExtendedCodeAction

typescript/src/codeActions/functionExtractors.ts

+15-6
Original file line numberDiff line numberDiff line change
@@ -100,14 +100,17 @@ export const handleFunctionRefactorEdits = (
100100
const oldFunctionText = functionChange.newText
101101
const sourceFile = languageService.getProgram()!.getSourceFile(fileName)!
102102
if (actionName.endsWith('_jsx')) {
103+
// refactor.extract.jsx implementation
103104
const lines = oldFunctionText.trimStart().split('\n')
104105
const oldFunctionSignature = lines[0]!
105106
const componentName = tsFull.getUniqueName('ExtractedComponent', sourceFile as unknown as FullSourceFile)
106-
const newFunctionSignature = changeArgumentsToDestructured(oldFunctionSignature, formatOptions, sourceFile, componentName)
107-
108107
const insertChange = textChanges.at(-2)!
109-
let args = insertChange.newText.slice(1, -2)
110-
args = args.slice(args.indexOf('(') + 1)
108+
const args = insertChange.newText.slice(insertChange.newText.indexOf('(') + 1, insertChange.newText.lastIndexOf(')'))
109+
110+
const newFunctionSignature = changeArgumentsToDestructured(oldFunctionSignature, formatOptions, sourceFile, componentName).replace('{}: {}', '')
111+
112+
const oldSpan = sourceFile.text.slice(0, functionChange.span.start).length
113+
111114
const fileEdits = [
112115
{
113116
fileName,
@@ -130,11 +133,17 @@ export const handleFunctionRefactorEdits = (
130133
],
131134
},
132135
]
136+
const diff = fileEdits[0]!.textChanges.slice(0, -1).reduce((diff, { newText, span }) => {
137+
const oldText = sourceFile.text.slice(span.start, span.start + span.length)
138+
const newSpan = newText.length
139+
const oldSpan = oldText.length
140+
diff += newSpan - oldSpan
141+
return diff
142+
}, 0)
133143
return {
134144
edits: fileEdits,
135145
renameFilename,
136-
renameLocation: insertChange.span.start + 1,
137-
// renameLocation: tsFull.getRenameLocation(fileEdits, fileName, componentName, /*preferLastLocation*/ false),
146+
renameLocation: functionChange.span.start + diff,
138147
}
139148
}
140149

typescript/src/codeActions/getCodeActions.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { renameParameterToNameFromType, renameAllParametersToNameFromType } from
1111
import addDestructure_1 from './custom/addDestructure/addDestructure'
1212
import fromDestructure_1 from './custom/fromDestructure/fromDestructure'
1313
import fixClosingTagName from './custom/fixClosingTagName'
14+
import declareMissingAttributes from './extended/declareMissingAttributes'
1415

1516
const codeActions: CodeAction[] = [
1617
addDestructure_1,
@@ -22,7 +23,7 @@ const codeActions: CodeAction[] = [
2223
renameAllParametersToNameFromType,
2324
fixClosingTagName,
2425
]
25-
const extendedCodeActions: ExtendedCodeAction[] = [declareMissingProperties]
26+
const extendedCodeActions: ExtendedCodeAction[] = [declareMissingProperties, declareMissingAttributes]
2627

2728
type SimplifiedRefactorInfo =
2829
| {

typescript/src/codeFixes.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { findChildContainingPosition, getCancellationToken, getIndentFromPos, is
55
import namespaceAutoImports from './namespaceAutoImports'
66

77
export default (proxy: ts.LanguageService, languageService: ts.LanguageService, languageServiceHost: ts.LanguageServiceHost, c: GetConfig) => {
8-
proxy.getCodeFixesAtPosition = (fileName, start, end, errorCodes, formatOptions, preferences) => {
8+
proxy.getCodeFixesAtPosition = (fileName, start, end, errorCodes, formatOptions, preferences, ...args) => {
99
const sourceFile = languageService.getProgram()!.getSourceFile(fileName)!
1010
const node = findChildContainingPosition(ts, sourceFile, start)
1111

@@ -72,7 +72,7 @@ export default (proxy: ts.LanguageService, languageService: ts.LanguageService,
7272
},
7373
)
7474
toUnpatch.push(unpatch)
75-
prior = languageService.getCodeFixesAtPosition(fileName, start, end, errorCodes, formatOptions, preferences)
75+
prior = languageService.getCodeFixesAtPosition(fileName, start, end, errorCodes, formatOptions, preferences, ...args)
7676
prior = [...addNamespaceImports, ...prior]
7777
prior = _.sortBy(prior, ({ fixName }) => {
7878
if (fixName.startsWith(importFixName)) {
@@ -82,7 +82,7 @@ export default (proxy: ts.LanguageService, languageService: ts.LanguageService,
8282
})
8383
prior = prior.filter(x => x.fixName !== 'IGNORE')
8484
} catch (err) {
85-
prior = languageService.getCodeFixesAtPosition(fileName, start, end, errorCodes, formatOptions, preferences)
85+
prior = languageService.getCodeFixesAtPosition(fileName, start, end, errorCodes, formatOptions, preferences, ...args)
8686
setTimeout(() => {
8787
// make sure we still get code fixes, but error is still getting reported
8888
console.error(err)
@@ -103,14 +103,14 @@ export default (proxy: ts.LanguageService, languageService: ts.LanguageService,
103103
// #endregion
104104

105105
const semanticDiagnostics = languageService.getSemanticDiagnostics(fileName)
106-
const syntacicDiagnostics = languageService.getSyntacticDiagnostics(fileName)
106+
const syntacticDiagnostics = languageService.getSyntacticDiagnostics(fileName)
107107

108108
// https://github.com/Microsoft/TypeScript/blob/v4.5.5/src/compiler/diagnosticMessages.json#L458
109109
const findDiagnosticByCode = (codes: number[]) => {
110110
const errorCode = codes.find(code => errorCodes.includes(code))
111111
if (!errorCode) return
112112
const diagnosticPredicate = ({ code, start: localStart }) => code === errorCode && localStart === start
113-
return syntacicDiagnostics.find(diagnosticPredicate) || semanticDiagnostics.find(diagnosticPredicate)
113+
return syntacticDiagnostics.find(diagnosticPredicate) || semanticDiagnostics.find(diagnosticPredicate)
114114
}
115115

116116
const wrapBlockDiagnostics = findDiagnosticByCode([1156, 1157])

typescript/src/completionEntryDetails.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export default function completionEntryDetails(
1616
c: GetConfig,
1717
{ enableMethodCompletion, completionsSymbolMap }: PrevCompletionsAdditionalData,
1818
): ts.CompletionEntryDetails | undefined {
19-
const [fileName, position, entryName, formatOptions, source, preferences, data] = inputArgs
19+
const [fileName, position, entryName, formatOptions, source, preferences, data, ...args] = inputArgs
2020
lastResolvedCompletion.value = { name: entryName, range: prevCompletionsMap[entryName]?.range }
2121
const program = languageService.getProgram()
2222
const sourceFile = program?.getSourceFile(fileName)
@@ -54,6 +54,7 @@ export default function completionEntryDetails(
5454
source,
5555
preferences,
5656
data,
57+
...args,
5758
)
5859
if (detailPrepend) {
5960
prior ??= {

typescript/src/completions/filesAutoImport.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ export default () => {
6060
const files = collected.filter(f => f.endsWith(ext))
6161
for (const file of files) {
6262
const fullPath = nodeModules.path.join(root, file)
63-
const relativeToFile = nodeModules.path.relative(nodeModules.path.dirname(sourceFile.fileName), fullPath).replaceAll('\\', '/')
63+
let relativeToFile = nodeModules.path.relative(nodeModules.path.dirname(sourceFile.fileName), fullPath).replaceAll('\\', '/')
64+
if (!relativeToFile.startsWith('.')) relativeToFile = `./${relativeToFile}`
6465
const lastModified = nodeModules.fs.statSync(fullPath).mtime
6566
const lastModifiedFormatted = timeDifference(Date.now(), lastModified.getTime())
6667
const importPath = (item.importPath ?? '$path').replaceAll('$path', relativeToFile)

typescript/src/completionsAtPosition.ts

+3
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ export const getCompletionsAtPosition = (
6868
scriptSnapshot: ts.IScriptSnapshot,
6969
formatOptions: ts.FormatCodeSettings | undefined,
7070
additionalData: { scriptKind: ts.ScriptKind; compilerOptions: ts.CompilerOptions },
71+
...args: any[]
7172
): GetCompletionAtPositionReturnType | undefined => {
7273
const prevCompletionsMap: PrevCompletionMap = {}
7374
const program = languageService.getProgram()
@@ -94,6 +95,8 @@ export const getCompletionsAtPosition = (
9495
includeSymbol: true,
9596
},
9697
formatOptions,
98+
//@ts-expect-error
99+
...args,
97100
)
98101
} finally {
99102
unpatch?.()

typescript/src/decorateEditsForFileRename.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import { GetConfig } from './types'
44
import { approveCast, findChildContainingExactPosition } from './utils'
55

66
export default (proxy: ts.LanguageService, languageService: ts.LanguageService, c: GetConfig) => {
7-
proxy.getEditsForFileRename = (oldFilePath, newFilePath, formatOptions, preferences) => {
8-
let edits = languageService.getEditsForFileRename(oldFilePath, newFilePath, formatOptions, preferences)
7+
proxy.getEditsForFileRename = (oldFilePath, newFilePath, formatOptions, preferences, ...args) => {
8+
let edits = languageService.getEditsForFileRename(oldFilePath, newFilePath, formatOptions, preferences, ...args)
99
if (c('renameImportNameOfFileRename')) {
1010
const predictedNameFromPath = (p: string) => {
1111
const input = p.split(/[/\\]/g).pop()!.replace(/\..+/, '')

typescript/src/decorateLinkedEditing.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export default (proxy: ts.LanguageService, languageService: ts.LanguageService,
1010
result: ts.LinkedEditingInfo
1111
}
1212
| undefined
13-
proxy.getLinkedEditingRangeAtPosition = (fileName, position) => {
13+
proxy.getLinkedEditingRangeAtPosition = (fileName, position, ...props) => {
1414
const scriptSnapshot = languageServiceHost.getScriptSnapshot(fileName)!
1515
const fileContent = scriptSnapshot.getText(0, scriptSnapshot.getLength())
1616
const lastChar = fileContent[position - 1]
@@ -37,7 +37,7 @@ export default (proxy: ts.LanguageService, languageService: ts.LanguageService,
3737
}
3838
lastLinkedEditingRangeRequest = undefined
3939

40-
const prior = languageService.getLinkedEditingRangeAtPosition(fileName, position)
40+
const prior = languageService.getLinkedEditingRangeAtPosition(fileName, position, ...props)
4141
if (!prior) return
4242
lastLinkedEditingRangeRequest = {
4343
pos: position,

0 commit comments

Comments
 (0)