Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Last release #210

Merged
merged 8 commits into from
May 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
23 changes: 23 additions & 0 deletions src/autoCompletionsTrigger.ts
Original file line number Diff line number Diff line change
@@ -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()
}
13 changes: 13 additions & 0 deletions src/configurationType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -685,6 +685,11 @@ export type Configuration = {
*/
declareMissingPropertyQuickfixOtherFiles: boolean
/**
* @recommended {".svg": {
* "importPath": "$path?react",
* "prefix": "Svg",
* "nameCasing": "pascal"
* },
* @default {}
*/
filesAutoImport: {
Expand All @@ -708,6 +713,14 @@ export type Configuration = {
iconPost?: string
}
}
/**
* @default true
*/
'completionsAutoTrigger.jsx': boolean
/**
* @default false
*/
'inlayHints.missingJsxAttributes.enabled': boolean
}

// scrapped using search editor. config: caseInsensitive, context lines: 0, regex: const fix\w+ = "[^ ]+"
Expand Down
4 changes: 4 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import moreCompletions from './moreCompletions'
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
Expand Down Expand Up @@ -96,6 +98,8 @@ export const activateTsPlugin = (tsApi: { configurePlugin; onCompletionAccepted

figIntegration()
vueVolarSupport()
inlayHints()
autoCompletionsTrigger()

if (process.env.PLATFORM === 'node' && process.env.NODE_ENV === 'development') {
require('./autoPluginReload').default()
Expand Down
57 changes: 57 additions & 0 deletions src/inlayHints.ts
Original file line number Diff line number Diff line change
@@ -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<void>()
onDidChangeInlayHints = this.eventEmitter.event
provideInlayHints(document: vscode.TextDocument, range: vscode.Range, token: vscode.CancellationToken): vscode.ProviderResult<vscode.InlayHint[]> {
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 = /: (?<prop>[\w, ]+)(?:, and (?<more>\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)
}
44 changes: 44 additions & 0 deletions typescript/src/codeActions/extended/declareMissingAttributes.ts
Original file line number Diff line number Diff line change
@@ -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
21 changes: 15 additions & 6 deletions typescript/src/codeActions/functionExtractors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
}
}

Expand Down
3 changes: 2 additions & 1 deletion typescript/src/codeActions/getCodeActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -22,7 +23,7 @@ const codeActions: CodeAction[] = [
renameAllParametersToNameFromType,
fixClosingTagName,
]
const extendedCodeActions: ExtendedCodeAction[] = [declareMissingProperties]
const extendedCodeActions: ExtendedCodeAction[] = [declareMissingProperties, declareMissingAttributes]

type SimplifiedRefactorInfo =
| {
Expand Down
10 changes: 5 additions & 5 deletions typescript/src/codeFixes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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)) {
Expand All @@ -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)
Expand All @@ -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])
Expand Down
3 changes: 2 additions & 1 deletion typescript/src/completionEntryDetails.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -54,6 +54,7 @@ export default function completionEntryDetails(
source,
preferences,
data,
...args,
)
if (detailPrepend) {
prior ??= {
Expand Down
3 changes: 2 additions & 1 deletion typescript/src/completions/filesAutoImport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions typescript/src/completionsAtPosition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -94,6 +95,8 @@ export const getCompletionsAtPosition = (
includeSymbol: true,
},
formatOptions,
//@ts-expect-error
...args,
)
} finally {
unpatch?.()
Expand Down
4 changes: 2 additions & 2 deletions typescript/src/decorateEditsForFileRename.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(/\..+/, '')
Expand Down
4 changes: 2 additions & 2 deletions typescript/src/decorateLinkedEditing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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,
Expand Down
Loading
Loading