From 149119c8bf6bef08f0f8c440e6779d129afe183e Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Fri, 3 May 2024 16:54:34 +0300 Subject: [PATCH 1/8] fix: correctly pass all unknown params for new TS protocol features fixes #209 --- typescript/src/codeFixes.ts | 6 ++--- typescript/src/completionEntryDetails.ts | 3 ++- typescript/src/completionsAtPosition.ts | 3 +++ typescript/src/decorateEditsForFileRename.ts | 4 +-- typescript/src/decorateLinkedEditing.ts | 4 +-- typescript/src/decorateProxy.ts | 25 +++++++++++++------ typescript/src/decorateSignatureHelp.ts | 5 ++-- ...ch.ts => decorateWorkspaceSymbolSearch.ts} | 5 ++-- typescript/src/definitions.ts | 4 +-- typescript/src/documentHighlights.ts | 4 +-- typescript/src/references.ts | 4 +-- typescript/src/semanticDiagnostics.ts | 4 +-- 12 files changed, 44 insertions(+), 27 deletions(-) rename typescript/src/{workspaceSymbolSearch.ts => decorateWorkspaceSymbolSearch.ts} (92%) diff --git a/typescript/src/codeFixes.ts b/typescript/src/codeFixes.ts index eff070c4..d4093838 100644 --- a/typescript/src/codeFixes.ts +++ b/typescript/src/codeFixes.ts @@ -5,7 +5,7 @@ import { findChildContainingPosition, getCancellationToken, getIndentFromPos, is import namespaceAutoImports from './namespaceAutoImports' export default (proxy: ts.LanguageService, languageService: ts.LanguageService, languageServiceHost: ts.LanguageServiceHost, c: GetConfig) => { - proxy.getCodeFixesAtPosition = (fileName, start, end, errorCodes, formatOptions, preferences) => { + proxy.getCodeFixesAtPosition = (fileName, start, end, errorCodes, formatOptions, preferences, ...args) => { const sourceFile = languageService.getProgram()!.getSourceFile(fileName)! const node = findChildContainingPosition(ts, sourceFile, start) @@ -72,7 +72,7 @@ export default (proxy: ts.LanguageService, languageService: ts.LanguageService, }, ) toUnpatch.push(unpatch) - prior = languageService.getCodeFixesAtPosition(fileName, start, end, errorCodes, formatOptions, preferences) + prior = languageService.getCodeFixesAtPosition(fileName, start, end, errorCodes, formatOptions, preferences, ...args) prior = [...addNamespaceImports, ...prior] prior = _.sortBy(prior, ({ fixName }) => { if (fixName.startsWith(importFixName)) { @@ -82,7 +82,7 @@ export default (proxy: ts.LanguageService, languageService: ts.LanguageService, }) prior = prior.filter(x => x.fixName !== 'IGNORE') } catch (err) { - prior = languageService.getCodeFixesAtPosition(fileName, start, end, errorCodes, formatOptions, preferences) + prior = languageService.getCodeFixesAtPosition(fileName, start, end, errorCodes, formatOptions, preferences, ...args) setTimeout(() => { // make sure we still get code fixes, but error is still getting reported console.error(err) diff --git a/typescript/src/completionEntryDetails.ts b/typescript/src/completionEntryDetails.ts index 10f6c5b5..7126f8a3 100644 --- a/typescript/src/completionEntryDetails.ts +++ b/typescript/src/completionEntryDetails.ts @@ -16,7 +16,7 @@ export default function completionEntryDetails( c: GetConfig, { enableMethodCompletion, completionsSymbolMap }: PrevCompletionsAdditionalData, ): ts.CompletionEntryDetails | undefined { - const [fileName, position, entryName, formatOptions, source, preferences, data] = inputArgs + const [fileName, position, entryName, formatOptions, source, preferences, data, ...args] = inputArgs lastResolvedCompletion.value = { name: entryName, range: prevCompletionsMap[entryName]?.range } const program = languageService.getProgram() const sourceFile = program?.getSourceFile(fileName) @@ -54,6 +54,7 @@ export default function completionEntryDetails( source, preferences, data, + ...args, ) if (detailPrepend) { prior ??= { diff --git a/typescript/src/completionsAtPosition.ts b/typescript/src/completionsAtPosition.ts index 4beb54bb..1b2cbd13 100644 --- a/typescript/src/completionsAtPosition.ts +++ b/typescript/src/completionsAtPosition.ts @@ -68,6 +68,7 @@ export const getCompletionsAtPosition = ( scriptSnapshot: ts.IScriptSnapshot, formatOptions: ts.FormatCodeSettings | undefined, additionalData: { scriptKind: ts.ScriptKind; compilerOptions: ts.CompilerOptions }, + ...args: any[] ): GetCompletionAtPositionReturnType | undefined => { const prevCompletionsMap: PrevCompletionMap = {} const program = languageService.getProgram() @@ -94,6 +95,8 @@ export const getCompletionsAtPosition = ( includeSymbol: true, }, formatOptions, + //@ts-expect-error + ...args, ) } finally { unpatch?.() diff --git a/typescript/src/decorateEditsForFileRename.ts b/typescript/src/decorateEditsForFileRename.ts index 00c03f5c..69458b20 100644 --- a/typescript/src/decorateEditsForFileRename.ts +++ b/typescript/src/decorateEditsForFileRename.ts @@ -4,8 +4,8 @@ import { GetConfig } from './types' import { approveCast, findChildContainingExactPosition } from './utils' export default (proxy: ts.LanguageService, languageService: ts.LanguageService, c: GetConfig) => { - proxy.getEditsForFileRename = (oldFilePath, newFilePath, formatOptions, preferences) => { - let edits = languageService.getEditsForFileRename(oldFilePath, newFilePath, formatOptions, preferences) + proxy.getEditsForFileRename = (oldFilePath, newFilePath, formatOptions, preferences, ...args) => { + let edits = languageService.getEditsForFileRename(oldFilePath, newFilePath, formatOptions, preferences, ...args) if (c('renameImportNameOfFileRename')) { const predictedNameFromPath = (p: string) => { const input = p.split(/[/\\]/g).pop()!.replace(/\..+/, '') diff --git a/typescript/src/decorateLinkedEditing.ts b/typescript/src/decorateLinkedEditing.ts index 244d8d7c..e0bf4c1e 100644 --- a/typescript/src/decorateLinkedEditing.ts +++ b/typescript/src/decorateLinkedEditing.ts @@ -10,7 +10,7 @@ export default (proxy: ts.LanguageService, languageService: ts.LanguageService, result: ts.LinkedEditingInfo } | undefined - proxy.getLinkedEditingRangeAtPosition = (fileName, position) => { + proxy.getLinkedEditingRangeAtPosition = (fileName, position, ...props) => { const scriptSnapshot = languageServiceHost.getScriptSnapshot(fileName)! const fileContent = scriptSnapshot.getText(0, scriptSnapshot.getLength()) const lastChar = fileContent[position - 1] @@ -37,7 +37,7 @@ export default (proxy: ts.LanguageService, languageService: ts.LanguageService, } lastLinkedEditingRangeRequest = undefined - const prior = languageService.getLinkedEditingRangeAtPosition(fileName, position) + const prior = languageService.getLinkedEditingRangeAtPosition(fileName, position, ...props) if (!prior) return lastLinkedEditingRangeRequest = { pos: position, diff --git a/typescript/src/decorateProxy.ts b/typescript/src/decorateProxy.ts index 0eebc464..079b5bb4 100644 --- a/typescript/src/decorateProxy.ts +++ b/typescript/src/decorateProxy.ts @@ -12,7 +12,7 @@ import decorateDefinitions from './definitions' import decorateDocumentHighlights from './documentHighlights' import completionEntryDetails from './completionEntryDetails' import { GetConfig, PluginCreateArg } from './types' -import decorateWorkspaceSymbolSearch from './workspaceSymbolSearch' +import decorateWorkspaceSymbolSearch from './decorateWorkspaceSymbolSearch' import decorateFormatFeatures from './decorateFormatFeatures' import libDomPatching from './libDomPatching' import decorateSignatureHelp from './decorateSignatureHelp' @@ -51,9 +51,9 @@ export const decorateLanguageService = ( let prevCompletionsMap: PrevCompletionMap let prevCompletionsAdditionalData: PrevCompletionsAdditionalData - proxy.getCompletionsAtPosition = (fileName, position, options, formatOptions) => { + proxy.getCompletionsAtPosition = (fileName, position, options, formatOptions, ...args) => { if (options?.triggerCharacter && typeof options.triggerCharacter !== 'string') { - return languageService.getCompletionsAtPosition(fileName, position, options) + return languageService.getCompletionsAtPosition(fileName, position, options, formatOptions, ...args) } const updateConfigCommand = 'updateConfig' if (options?.triggerCharacter?.startsWith(updateConfigCommand)) { @@ -87,10 +87,21 @@ export const decorateLanguageService = ( if (!scriptSnapshot) return const compilerOptions = languageServiceHost.getCompilationSettings() try { - const result = getCompletionsAtPosition(fileName, position, options, c, languageService, languageServiceHost, scriptSnapshot, formatOptions, { - scriptKind, - compilerOptions, - }) + const result = getCompletionsAtPosition( + fileName, + position, + options, + c, + languageService, + languageServiceHost, + scriptSnapshot, + formatOptions, + { + scriptKind, + compilerOptions, + }, + ...args, + ) if (!result) return prevCompletionsMap = result.prevCompletionsMap prevCompletionsAdditionalData = result.prevCompletionsAdditionalData diff --git a/typescript/src/decorateSignatureHelp.ts b/typescript/src/decorateSignatureHelp.ts index 9f83d933..475242ba 100644 --- a/typescript/src/decorateSignatureHelp.ts +++ b/typescript/src/decorateSignatureHelp.ts @@ -3,7 +3,7 @@ import { GetConfig } from './types' import { findChildContainingExactPosition } from './utils' export default (proxy: ts.LanguageService, languageService: ts.LanguageService, languageServiceHost: ts.LanguageServiceHost, c: GetConfig) => { - proxy.getSignatureHelpItems = (fileName, position, options) => { + proxy.getSignatureHelpItems = (fileName, position, options, ...props) => { const program = languageService.getProgram()! const sourceFile = program.getSourceFile(fileName)! let node: ts.Node | undefined @@ -40,7 +40,7 @@ export default (proxy: ts.LanguageService, languageService: ts.LanguageService, } if (!c('signatureHelp.excludeBlockScope') || options?.triggerReason?.kind !== 'invoked') { - return languageService.getSignatureHelpItems(fileName, position, options) + return languageService.getSignatureHelpItems(fileName, position, options, ...props) } node ??= findChildContainingExactPosition(sourceFile, position) @@ -59,6 +59,7 @@ export default (proxy: ts.LanguageService, languageService: ts.LanguageService, kind: 'retrigger', }, }, + ...props, ) } } diff --git a/typescript/src/workspaceSymbolSearch.ts b/typescript/src/decorateWorkspaceSymbolSearch.ts similarity index 92% rename from typescript/src/workspaceSymbolSearch.ts rename to typescript/src/decorateWorkspaceSymbolSearch.ts index 329ffce0..bca04958 100644 --- a/typescript/src/workspaceSymbolSearch.ts +++ b/typescript/src/decorateWorkspaceSymbolSearch.ts @@ -2,10 +2,10 @@ import { GetConfig } from './types' import { getCancellationToken } from './utils' export default (proxy: ts.LanguageService, languageService: ts.LanguageService, c: GetConfig, languageServiceHost: ts.LanguageServiceHost) => { - proxy.getNavigateToItems = (searchValue, maxResultCount, fileName, excludeDtsFiles) => { + proxy.getNavigateToItems = (searchValue, maxResultCount, fileName, excludeDtsFiles, ...args) => { const workspaceSymbolSearchExcludePatterns = c('workspaceSymbolSearchExcludePatterns') if (workspaceSymbolSearchExcludePatterns.length === 0) { - return languageService.getNavigateToItems(searchValue, maxResultCount, fileName, excludeDtsFiles) + return languageService.getNavigateToItems(searchValue, maxResultCount, fileName, excludeDtsFiles, ...args) } const program = languageService.getProgram()! @@ -25,6 +25,7 @@ export default (proxy: ts.LanguageService, languageService: ts.LanguageService, searchValue, maxResultCount, excludeDtsFiles ?? false, + ...args ) } } diff --git a/typescript/src/definitions.ts b/typescript/src/definitions.ts index 77bd7a4c..ef749b4d 100644 --- a/typescript/src/definitions.ts +++ b/typescript/src/definitions.ts @@ -3,8 +3,8 @@ import { GetConfig } from './types' import { findChildContainingExactPosition } from './utils' export default (proxy: ts.LanguageService, languageService: ts.LanguageService, languageServiceHost: ts.LanguageServiceHost, c: GetConfig) => { - proxy.getDefinitionAndBoundSpan = (fileName, position) => { - const prior = languageService.getDefinitionAndBoundSpan(fileName, position) + proxy.getDefinitionAndBoundSpan = (fileName, position, ...props) => { + const prior = languageService.getDefinitionAndBoundSpan(fileName, position, ...props) if (c('removeModuleFileDefinitions') && prior) { prior.definitions = prior.definitions?.filter(def => { diff --git a/typescript/src/documentHighlights.ts b/typescript/src/documentHighlights.ts index 33a91586..23e62d9f 100644 --- a/typescript/src/documentHighlights.ts +++ b/typescript/src/documentHighlights.ts @@ -2,8 +2,8 @@ import { GetConfig } from './types' import { findChildContainingPosition } from './utils' export default (proxy: ts.LanguageService, languageService: ts.LanguageService, c: GetConfig) => { - proxy.getDocumentHighlights = (fileName, position, filesToSearch) => { - const prior = languageService.getDocumentHighlights(fileName, position, filesToSearch) + proxy.getDocumentHighlights = (fileName, position, filesToSearch, ...props) => { + const prior = languageService.getDocumentHighlights(fileName, position, filesToSearch, ...props) if (!prior) return if (prior.length !== 1 || c('disableUselessHighlighting') === 'disable') return prior const node = findChildContainingPosition(ts, languageService.getProgram()!.getSourceFile(fileName)!, position) diff --git a/typescript/src/references.ts b/typescript/src/references.ts index 40210083..33245fc0 100644 --- a/typescript/src/references.ts +++ b/typescript/src/references.ts @@ -2,8 +2,8 @@ import { GetConfig } from './types' import { findChildContainingPositionMaxDepth, approveCast, findChildContainingExactPosition, matchParents } from './utils' export default (proxy: ts.LanguageService, languageService: ts.LanguageService, c: GetConfig) => { - proxy.findReferences = (fileName, position) => { - let prior = languageService.findReferences(fileName, position) + proxy.findReferences = (fileName, position, ...props) => { + let prior = languageService.findReferences(fileName, position, ...props) if (!prior) return const program = languageService.getProgram()! if (c('removeDefinitionFromReferences')) { diff --git a/typescript/src/semanticDiagnostics.ts b/typescript/src/semanticDiagnostics.ts index cbdddccc..fe4f283e 100644 --- a/typescript/src/semanticDiagnostics.ts +++ b/typescript/src/semanticDiagnostics.ts @@ -1,8 +1,8 @@ import { GetConfig } from './types' export default (proxy: ts.LanguageService, languageService: ts.LanguageService, languageServiceHost: ts.LanguageServiceHost, c: GetConfig) => { - proxy.getSemanticDiagnostics = fileName => { - let prior = languageService.getSemanticDiagnostics(fileName) + proxy.getSemanticDiagnostics = (fileName, ...props) => { + let prior = languageService.getSemanticDiagnostics(fileName, ...props) if (c('supportTsDiagnosticDisableComment')) { const scriptSnapshot = languageServiceHost.getScriptSnapshot(fileName)! const firstLine = scriptSnapshot.getText(0, scriptSnapshot.getLength()).split(/\r?\n/)[0]! From 25521acafc28e31d22d7016efedc61e5fb32d749 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Fri, 24 May 2024 07:24:40 +0300 Subject: [PATCH 2/8] feat: Add inlay hints for missing JSX attributes. Disabled by default, enable with `tsEssentialPlugins.inlayHints.missingJsxAttributes.enabled` #208 --- src/configurationType.ts | 9 +++++++ src/extension.ts | 2 ++ src/inlayHints.ts | 57 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+) create mode 100644 src/inlayHints.ts diff --git a/src/configurationType.ts b/src/configurationType.ts index 93ae2d61..8e1e06bf 100644 --- a/src/configurationType.ts +++ b/src/configurationType.ts @@ -685,6 +685,11 @@ export type Configuration = { */ declareMissingPropertyQuickfixOtherFiles: boolean /** + * @recommended {".svg": { + * "importPath": "$path?react", + * "prefix": "Svg", + * "nameCasing": "pascal" + * }, * @default {} */ filesAutoImport: { @@ -708,6 +713,10 @@ export type Configuration = { iconPost?: string } } + /** + * @default false + */ + 'inlayHints.missingJsxAttributes.enabled': boolean } // scrapped using search editor. config: caseInsensitive, context lines: 0, regex: const fix\w+ = "[^ ]+" diff --git a/src/extension.ts b/src/extension.ts index a60488d6..e66e4374 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -17,6 +17,7 @@ import moreCompletions from './moreCompletions' import { mergeSettingsFromScopes } from './mergeSettings' import codeActionProvider from './codeActionProvider' import nonTsCommands from './nonTsCommands' +import inlayHints from './inlayHints' let isActivated = false // let erroredStatusBarItem: vscode.StatusBarItem | undefined @@ -96,6 +97,7 @@ export const activateTsPlugin = (tsApi: { configurePlugin; onCompletionAccepted figIntegration() vueVolarSupport() + inlayHints() if (process.env.PLATFORM === 'node' && process.env.NODE_ENV === 'development') { require('./autoPluginReload').default() diff --git a/src/inlayHints.ts b/src/inlayHints.ts new file mode 100644 index 00000000..570a8e9f --- /dev/null +++ b/src/inlayHints.ts @@ -0,0 +1,57 @@ +import * as vscode from 'vscode' +import { watchExtensionSetting } from '@zardoy/vscode-utils/build/settings' +import { getExtensionSetting, registerActiveDevelopmentCommand } from 'vscode-framework' + +// todo respect enabled setting, deactivate +export default () => { + const provider = new (class implements vscode.InlayHintsProvider { + eventEmitter = new vscode.EventEmitter() + onDidChangeInlayHints = this.eventEmitter.event + provideInlayHints(document: vscode.TextDocument, range: vscode.Range, token: vscode.CancellationToken): vscode.ProviderResult { + const diagnostics = vscode.languages.getDiagnostics(document.uri) + const jsxMissingAttributesErrors = diagnostics.filter(({ code, source }) => (code === 2740 || code === 2739) && source === 'ts') + return jsxMissingAttributesErrors + .flatMap(({ range, message }) => { + const regex = /: (?[\w, ]+)(?:, and (?\d+) more)?\.?$/ + const match = regex.exec(message) + if (!match) return null as never + const props = match.groups!.prop!.split(', ') + const { more } = match.groups! + let text = ` ${props.map(prop => `${prop}!`).join(', ')}` + if (more) text += `, and ${more} more` + return { + kind: vscode.InlayHintKind.Type, + label: text, + tooltip: `Inlay hint: Missing attributes`, + position: range.end, + paddingLeft: true, + } satisfies vscode.InlayHint + // return [...props, ...(more ? [more] : [])].map((prop) => ({ + // kind: vscode.InlayHintKind.Type, + // label: prop, + // tooltip: 'Missing attribute', + // position: + // })) + }) + .filter(Boolean) + } + })() + let disposables = [] as vscode.Disposable[] + + const manageEnablement = () => { + if (getExtensionSetting('inlayHints.missingJsxAttributes.enabled')) { + vscode.languages.registerInlayHintsProvider('typescriptreact,javascript,javascriptreact'.split(','), provider) + vscode.languages.onDidChangeDiagnostics(e => { + for (const uri of e.uris) { + if (uri === vscode.window.activeTextEditor?.document.uri) provider.eventEmitter.fire() + } + }) + } else { + for (const d of disposables) d.dispose() + disposables = [] + } + } + + manageEnablement() + watchExtensionSetting('inlayHints.missingJsxAttributes.enabled', manageEnablement) +} From 8a11e715e2d6433ca7b0764931961713e9d4962a Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Fri, 24 May 2024 07:52:24 +0300 Subject: [PATCH 3/8] feat: add Declare missing attributes snippet codefix --- package.json | 1 + .../extended/declareMissingAttributes.ts | 44 +++++++++++++++++++ typescript/src/codeActions/getCodeActions.ts | 3 +- typescript/src/codeFixes.ts | 4 +- 4 files changed, 49 insertions(+), 3 deletions(-) create mode 100644 typescript/src/codeActions/extended/declareMissingAttributes.ts diff --git a/package.json b/package.json index 1c173d02..ee9e536b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "name": "ts-essential-plugins", "displayName": "TypeScript Essential Plugins", + "description": "50+ features: TS extension for professionals", "version": "0.0.0-dev", "license": "MIT", "web": true, diff --git a/typescript/src/codeActions/extended/declareMissingAttributes.ts b/typescript/src/codeActions/extended/declareMissingAttributes.ts new file mode 100644 index 00000000..82ecbb4b --- /dev/null +++ b/typescript/src/codeActions/extended/declareMissingAttributes.ts @@ -0,0 +1,44 @@ +import { ExtendedCodeAction } from '../getCodeActions' + +const errorCodes = [ + // ts.Diagnostics.Property_0_does_not_exist_on_type_1.code, + // ts.Diagnostics.Property_0_does_not_exist_on_type_1_Did_you_mean_2.code, + // ts.Diagnostics.Property_0_is_missing_in_type_1_but_required_in_type_2.code, + // ts.Diagnostics.Type_0_is_missing_the_following_properties_from_type_1_Colon_2.code, + // ts.Diagnostics.Type_0_is_missing_the_following_properties_from_type_1_Colon_2_and_3_more.code, + // // ts.Diagnostics.Argument_of_type_0_is_not_assignable_to_parameter_of_type_1.code, + // // ts.Diagnostics.Cannot_find_name_0.code, + 2339, 2551, 2741, 2739, 2740 /* 2345, 2304, */, +] + +export default { + codes: errorCodes, + kind: 'quickfix', + title: 'Declare missing attributes', + tryToApply({ sourceFile, node, c, languageService, position, formatOptions, range }) { + // todo maybe cache from prev request? + if (!node) return + const codeFixes = languageService.getCodeFixesAtPosition( + sourceFile.fileName, + node.getStart(), + range?.end ?? node.getStart(), + errorCodes, + formatOptions ?? {}, + {}, + ) + const fix = codeFixes.find(codeFix => codeFix.fixName === 'fixMissingAttributes') + if (fix && fix.changes[0]?.textChanges.length === 1) { + const changes = fix.changes[0]!.textChanges + let i = 1 + return { + snippetEdits: [ + { + newText: changes[0]!.newText.replaceAll('$', '\\$').replaceAll('={undefined}', () => `={$${i++}}`), + span: fix.changes[0]!.textChanges[0]!.span, + }, + ], + } + } + return + }, +} as ExtendedCodeAction diff --git a/typescript/src/codeActions/getCodeActions.ts b/typescript/src/codeActions/getCodeActions.ts index e69509f1..468f8a26 100644 --- a/typescript/src/codeActions/getCodeActions.ts +++ b/typescript/src/codeActions/getCodeActions.ts @@ -11,6 +11,7 @@ import { renameParameterToNameFromType, renameAllParametersToNameFromType } from import addDestructure_1 from './custom/addDestructure/addDestructure' import fromDestructure_1 from './custom/fromDestructure/fromDestructure' import fixClosingTagName from './custom/fixClosingTagName' +import declareMissingAttributes from './extended/declareMissingAttributes' const codeActions: CodeAction[] = [ addDestructure_1, @@ -22,7 +23,7 @@ const codeActions: CodeAction[] = [ renameAllParametersToNameFromType, fixClosingTagName, ] -const extendedCodeActions: ExtendedCodeAction[] = [declareMissingProperties] +const extendedCodeActions: ExtendedCodeAction[] = [declareMissingProperties, declareMissingAttributes] type SimplifiedRefactorInfo = | { diff --git a/typescript/src/codeFixes.ts b/typescript/src/codeFixes.ts index d4093838..e89e7d94 100644 --- a/typescript/src/codeFixes.ts +++ b/typescript/src/codeFixes.ts @@ -103,14 +103,14 @@ export default (proxy: ts.LanguageService, languageService: ts.LanguageService, // #endregion const semanticDiagnostics = languageService.getSemanticDiagnostics(fileName) - const syntacicDiagnostics = languageService.getSyntacticDiagnostics(fileName) + const syntacticDiagnostics = languageService.getSyntacticDiagnostics(fileName) // https://github.com/Microsoft/TypeScript/blob/v4.5.5/src/compiler/diagnosticMessages.json#L458 const findDiagnosticByCode = (codes: number[]) => { const errorCode = codes.find(code => errorCodes.includes(code)) if (!errorCode) return const diagnosticPredicate = ({ code, start: localStart }) => code === errorCode && localStart === start - return syntacicDiagnostics.find(diagnosticPredicate) || semanticDiagnostics.find(diagnosticPredicate) + return syntacticDiagnostics.find(diagnosticPredicate) || semanticDiagnostics.find(diagnosticPredicate) } const wrapBlockDiagnostics = findDiagnosticByCode([1156, 1157]) From 54b4acdb5db57edfb5aab94244164b9ea731eb42 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Fri, 24 May 2024 08:01:41 +0300 Subject: [PATCH 4/8] feat: Add auto-completion trigger for JSX attributes --- src/autoCompletionsTrigger.ts | 23 +++++++++++++++++++++++ src/configurationType.ts | 4 ++++ src/extension.ts | 2 ++ 3 files changed, 29 insertions(+) create mode 100644 src/autoCompletionsTrigger.ts diff --git a/src/autoCompletionsTrigger.ts b/src/autoCompletionsTrigger.ts new file mode 100644 index 00000000..a8e81390 --- /dev/null +++ b/src/autoCompletionsTrigger.ts @@ -0,0 +1,23 @@ +import * as vscode from 'vscode' +import { defaultLanguageSupersets } from '@zardoy/vscode-utils/build/langs' +import { getExtensionSetting } from 'vscode-framework' +import { sendCommand } from './sendCommand' + +const jsxAttributesAutoTrigger = () => { + vscode.workspace.onDidChangeTextDocument(async ({ contentChanges, document, reason }) => { + const editor = vscode.window.activeTextEditor + if (document !== editor?.document || contentChanges.length === 0) return + if (contentChanges[0]!.text !== ' ') return + if (![...defaultLanguageSupersets.react, 'javascript'].includes(document.languageId)) return + if (!getExtensionSetting('completionsAutoTrigger.jsx')) return + const path = await sendCommand('getNodePath', { document, position: editor.selection.active }) + if (!path) return + if (['JsxSelfClosingElement', 'JsxOpeningElement'].includes(path.at(-1)?.kindName ?? '')) { + await vscode.commands.executeCommand('editor.action.triggerSuggest') + } + }) +} + +export default () => { + jsxAttributesAutoTrigger() +} diff --git a/src/configurationType.ts b/src/configurationType.ts index 8e1e06bf..3511f83e 100644 --- a/src/configurationType.ts +++ b/src/configurationType.ts @@ -713,6 +713,10 @@ export type Configuration = { iconPost?: string } } + /** + * @default true + */ + 'completionsAutoTrigger.jsx': boolean /** * @default false */ diff --git a/src/extension.ts b/src/extension.ts index e66e4374..0bacb075 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -18,6 +18,7 @@ import { mergeSettingsFromScopes } from './mergeSettings' import codeActionProvider from './codeActionProvider' import nonTsCommands from './nonTsCommands' import inlayHints from './inlayHints' +import autoCompletionsTrigger from './autoCompletionsTrigger' let isActivated = false // let erroredStatusBarItem: vscode.StatusBarItem | undefined @@ -98,6 +99,7 @@ export const activateTsPlugin = (tsApi: { configurePlugin; onCompletionAccepted figIntegration() vueVolarSupport() inlayHints() + autoCompletionsTrigger() if (process.env.PLATFORM === 'node' && process.env.NODE_ENV === 'development') { require('./autoPluginReload').default() From 01dfb8aa3d24efd31f912cd0e3ed7e78a7b55981 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Fri, 24 May 2024 08:19:22 +0300 Subject: [PATCH 5/8] fix: a lot of fixes to Extract into JSX component code action! --- .../src/codeActions/functionExtractors.ts | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/typescript/src/codeActions/functionExtractors.ts b/typescript/src/codeActions/functionExtractors.ts index b5c63bed..5d94ae87 100644 --- a/typescript/src/codeActions/functionExtractors.ts +++ b/typescript/src/codeActions/functionExtractors.ts @@ -100,14 +100,17 @@ export const handleFunctionRefactorEdits = ( const oldFunctionText = functionChange.newText const sourceFile = languageService.getProgram()!.getSourceFile(fileName)! if (actionName.endsWith('_jsx')) { + // refactor.extract.jsx implementation const lines = oldFunctionText.trimStart().split('\n') const oldFunctionSignature = lines[0]! const componentName = tsFull.getUniqueName('ExtractedComponent', sourceFile as unknown as FullSourceFile) - const newFunctionSignature = changeArgumentsToDestructured(oldFunctionSignature, formatOptions, sourceFile, componentName) - const insertChange = textChanges.at(-2)! - let args = insertChange.newText.slice(1, -2) - args = args.slice(args.indexOf('(') + 1) + const args = insertChange.newText.slice(insertChange.newText.indexOf('(') + 1, insertChange.newText.lastIndexOf(')')) + + const newFunctionSignature = changeArgumentsToDestructured(oldFunctionSignature, formatOptions, sourceFile, componentName).replace('{}: {}', '') + + const oldSpan = sourceFile.text.slice(0, functionChange.span.start).length + const fileEdits = [ { fileName, @@ -130,11 +133,17 @@ export const handleFunctionRefactorEdits = ( ], }, ] + const diff = fileEdits[0]!.textChanges.slice(0, -1).reduce((diff, { newText, span }) => { + const oldText = sourceFile.text.slice(span.start, span.start + span.length) + const newSpan = newText.length + const oldSpan = oldText.length + diff += newSpan - oldSpan + return diff + }, 0) return { edits: fileEdits, renameFilename, - renameLocation: insertChange.span.start + 1, - // renameLocation: tsFull.getRenameLocation(fileEdits, fileName, componentName, /*preferLastLocation*/ false), + renameLocation: functionChange.span.start + diff, } } From 9f532d33e3a31ba9d13e77ffc9b90dd193a6c1a3 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Fri, 24 May 2024 08:28:33 +0300 Subject: [PATCH 6/8] fix: fix outline crash for upcoming ts 5.5 --- typescript/src/getPatchedNavTree.ts | 7 ++++--- typescript/src/utils.ts | 1 + 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/typescript/src/getPatchedNavTree.ts b/typescript/src/getPatchedNavTree.ts index 158d1b87..1c6ceac9 100644 --- a/typescript/src/getPatchedNavTree.ts +++ b/typescript/src/getPatchedNavTree.ts @@ -1,5 +1,5 @@ import { ensureArray } from '@zardoy/utils' -import { getCancellationToken, isTs5, nodeModules } from './utils' +import { getCancellationToken, isTs5, isTs5And5, nodeModules } from './utils' import { createLanguageService } from './dummyLanguageService' import { getCannotFindCodes } from './utils/cannotFindCodes' @@ -11,8 +11,9 @@ type AdditionalFeatures = Record<'arraysTuplesNumberedItems', boolean> const getPatchedNavModule = (additionalFeatures: AdditionalFeatures): { getNavigationTree(...args) } => { // what is happening here: grabbing & patching NavigationBar module contents from actual running JS const tsServerPath = typeof __TS_SEVER_PATH__ === 'undefined' ? require.main!.filename : __TS_SEVER_PATH__ + const typescriptFilePath = `${nodeModules!.path.dirname(tsServerPath)}/typescript.js` // current lib/tsserver.js - const mainScript = nodeModules!.fs.readFileSync(tsServerPath, 'utf8') + const mainScript = nodeModules!.fs.readFileSync(isTs5And5() ? typescriptFilePath : tsServerPath, 'utf8') type PatchData = { markerModuleStart: string skipStartMarker?: boolean @@ -173,7 +174,7 @@ export const getNavTreeItems = ( fileName: string, additionalFeatures: AdditionalFeatures, ) => { - if (!navModule) navModule = getPatchedNavModule(additionalFeatures) + navModule = getPatchedNavModule(additionalFeatures) const sourceFile = (languageService as unknown as import('typescript-full').LanguageService).getNonBoundSourceFile?.(fileName) ?? languageService.getProgram()!.getSourceFile(fileName) diff --git a/typescript/src/utils.ts b/typescript/src/utils.ts index 1aea5e65..bb3f9360 100644 --- a/typescript/src/utils.ts +++ b/typescript/src/utils.ts @@ -131,6 +131,7 @@ export const buildStringCompletion = (node: ts.StringLiteralLike, completion: Ex // semver: can't use compare as it incorrectly works with build postfix export const isTs5 = () => semver.major(ts.version) >= 5 +export const isTs5And5 = () => (semver.major(ts.version) === 5 && semver.minor(ts.version) >= 5) || semver.major(ts.version) > 5 export const isTsPatched = () => { try { From 5aa78d093608953aa34205bb40bfbf0ece8c0a74 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Fri, 24 May 2024 08:35:13 +0300 Subject: [PATCH 7/8] feat: Improve relative file path handling in filesAutoImport --- typescript/src/completions/filesAutoImport.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/typescript/src/completions/filesAutoImport.ts b/typescript/src/completions/filesAutoImport.ts index 13b9d066..207e16a0 100644 --- a/typescript/src/completions/filesAutoImport.ts +++ b/typescript/src/completions/filesAutoImport.ts @@ -60,7 +60,8 @@ export default () => { const files = collected.filter(f => f.endsWith(ext)) for (const file of files) { const fullPath = nodeModules.path.join(root, file) - const relativeToFile = nodeModules.path.relative(nodeModules.path.dirname(sourceFile.fileName), fullPath).replaceAll('\\', '/') + let relativeToFile = nodeModules.path.relative(nodeModules.path.dirname(sourceFile.fileName), fullPath).replaceAll('\\', '/') + if (!relativeToFile.startsWith('.')) relativeToFile = `./${relativeToFile}` const lastModified = nodeModules.fs.statSync(fullPath).mtime const lastModifiedFormatted = timeDifference(Date.now(), lastModified.getTime()) const importPath = (item.importPath ?? '$path').replaceAll('$path', relativeToFile) From 16743dd29ceb29274cb893a0588788ff885f9dd1 Mon Sep 17 00:00:00 2001 From: Vitaly Turovsky Date: Fri, 24 May 2024 08:35:32 +0300 Subject: [PATCH 8/8] fix prettier --- typescript/src/decorateWorkspaceSymbolSearch.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typescript/src/decorateWorkspaceSymbolSearch.ts b/typescript/src/decorateWorkspaceSymbolSearch.ts index bca04958..d74373b1 100644 --- a/typescript/src/decorateWorkspaceSymbolSearch.ts +++ b/typescript/src/decorateWorkspaceSymbolSearch.ts @@ -25,7 +25,7 @@ export default (proxy: ts.LanguageService, languageService: ts.LanguageService, searchValue, maxResultCount, excludeDtsFiles ?? false, - ...args + ...args, ) } }