Skip to content

Commit 51f0340

Browse files
committed
implement extended code actions with snippet support
fix: bump minimal supported vscode version to 1.72.0 to support code actions with snippets
1 parent 238ff90 commit 51f0340

File tree

12 files changed

+246
-78
lines changed

12 files changed

+246
-78
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@
111111
"@types/fs-extra": "^9.0.13",
112112
"@types/node": "^16.11.21",
113113
"@types/semver": "^7.3.13",
114-
"@types/vscode": "^1.63.1",
114+
"@types/vscode": "1.72.0",
115115
"@zardoy/tsconfig": "^1.3.1",
116116
"esbuild": "^0.16.16",
117117
"fs-extra": "^10.1.0",

pnpm-lock.yaml

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

src/sendCommand.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ type SendCommandData<K> = {
88
document?: vscode.TextDocument
99
inputOptions?: K
1010
}
11-
export const sendCommand = async <T, K = any>(command: TriggerCharacterCommand, sendCommandDataArg?: SendCommandData<K>): Promise<T | undefined> => {
11+
export const sendCommand = async <Response, K = any>(
12+
command: TriggerCharacterCommand,
13+
sendCommandDataArg?: SendCommandData<K>,
14+
): Promise<Response | undefined> => {
1215
// plugin id disabled, languageService would not understand the special trigger character
1316
if (!getExtensionSetting('enablePlugin')) {
1417
console.warn('Ignoring request because plugin is disabled')

src/specialCommands.ts

Lines changed: 68 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable unicorn/consistent-destructuring */
12
import * as vscode from 'vscode'
23
import { getActiveRegularEditor } from '@zardoy/vscode-utils'
34
import { getExtensionCommandId, getExtensionSetting, registerExtensionCommand, VSCodeQuickPickItem } from 'vscode-framework'
@@ -9,7 +10,15 @@ import { offsetPosition } from '@zardoy/vscode-utils/build/position'
910
import { relative, join } from 'path-browserify'
1011
import { RequestOptionsTypes, RequestResponseTypes } from '../typescript/src/ipcTypes'
1112
import { sendCommand } from './sendCommand'
12-
import { getTsLikePath, pickFileWithQuickPick, tsRangeToVscode, tsRangeToVscodeSelection, tsTextChangesToVcodeTextEdits } from './util'
13+
import {
14+
getTsLikePath,
15+
pickFileWithQuickPick,
16+
tsRangeToVscode,
17+
tsRangeToVscodeSelection,
18+
tsTextChangesToVscodeSnippetTextEdits,
19+
tsTextChangesToVscodeTextEdits,
20+
vscodeRangeToTs,
21+
} from './util'
1322

1423
export default () => {
1524
registerExtensionCommand('removeFunctionArgumentsTypesInSelection', async () => {
@@ -227,7 +236,7 @@ export default () => {
227236
document,
228237
position: range.start,
229238
inputOptions: {
230-
range: [document.offsetAt(range.start), document.offsetAt(range.end)] as [number, number],
239+
range: vscodeRangeToTs(document, range),
231240
},
232241
})
233242
}
@@ -239,7 +248,7 @@ export default () => {
239248
document,
240249
position: range.start,
241250
inputOptions: {
242-
range: [document.offsetAt(range.start), document.offsetAt(range.end)] as [number, number],
251+
range: vscodeRangeToTs(document, range),
243252
data: secondStepData,
244253
},
245254
},
@@ -343,10 +352,12 @@ export default () => {
343352
}
344353

345354
if (!mainChanges) return
346-
edit.set(editor.document.uri, tsTextChangesToVcodeTextEdits(editor.document, mainChanges))
355+
edit.set(editor.document.uri, tsTextChangesToVscodeTextEdits(editor.document, mainChanges))
347356
await vscode.workspace.applyEdit(edit)
348357
})
349358

359+
type ExtendedCodeAction = vscode.CodeAction & { document: vscode.TextDocument; requestRange: vscode.Range }
360+
350361
// most probably will be moved to ts-code-actions extension
351362
vscode.languages.registerCodeActionsProvider(defaultJsSupersetLangsWithVue, {
352363
async provideCodeActions(document, range, context, token) {
@@ -355,18 +366,20 @@ export default () => {
355366
}
356367

357368
if (context.only?.contains(vscode.CodeActionKind.SourceFixAll)) {
358-
const fixAllEdits = await sendCommand<RequestResponseTypes['getFixAllEdits']>('getFixAllEdits')
359-
if (!fixAllEdits) return
369+
const fixAllEdits = await sendCommand<RequestResponseTypes['getFixAllEdits']>('getFixAllEdits', {
370+
document,
371+
})
372+
if (!fixAllEdits || token.isCancellationRequested) return
360373
const edit = new vscode.WorkspaceEdit()
361-
edit.set(document.uri, tsTextChangesToVcodeTextEdits(document, fixAllEdits))
374+
edit.set(document.uri, tsTextChangesToVscodeTextEdits(document, fixAllEdits))
362375
await vscode.workspace.applyEdit(edit)
363376
return
364377
}
365378

366379
if (context.triggerKind !== vscode.CodeActionTriggerKind.Invoke) return
367380
const result = await getPossibleTwoStepRefactorings(range)
368381
if (!result) return
369-
const { turnArrayIntoObject, moveToExistingFile } = result
382+
const { turnArrayIntoObject, moveToExistingFile, extendedCodeActions } = result
370383
const codeActions: vscode.CodeAction[] = []
371384
const getCommand = (arg): vscode.Command | undefined => ({
372385
title: '',
@@ -390,8 +403,55 @@ export default () => {
390403
// })
391404
}
392405

406+
codeActions.push(
407+
...compact(
408+
extendedCodeActions.map(({ title, kind, codes }): ExtendedCodeAction | undefined => {
409+
let diagnostics: vscode.Diagnostic[] | undefined
410+
if (codes) {
411+
diagnostics = context.diagnostics.filter(({ source, code }) => {
412+
if (source !== 'ts' || !code) return
413+
const codeNumber = +(typeof code === 'object' ? code.value : code)
414+
return codes.includes(codeNumber)
415+
})
416+
if (diagnostics.length === 0) return
417+
}
418+
419+
return {
420+
title,
421+
diagnostics,
422+
kind: vscode.CodeActionKind.Empty.append(kind),
423+
requestRange: range,
424+
document,
425+
}
426+
}),
427+
),
428+
)
429+
393430
return codeActions
394431
},
432+
async resolveCodeAction(codeAction: ExtendedCodeAction, token) {
433+
const { document } = codeAction
434+
if (!document) throw new Error('Unresolved code action without document')
435+
const result = await sendCommand<RequestResponseTypes['getExtendedCodeActionEdits'], RequestOptionsTypes['getExtendedCodeActionEdits']>(
436+
'getExtendedCodeActionEdits',
437+
{
438+
document,
439+
inputOptions: {
440+
applyCodeActionTitle: codeAction.title,
441+
range: vscodeRangeToTs(document, codeAction.diagnostics?.length ? codeAction.diagnostics[0]!.range : codeAction.requestRange),
442+
},
443+
},
444+
)
445+
if (!result) throw new Error('No code action edits. Try debug.')
446+
const { edits = [], snippetEdits = [] } = result
447+
const workspaceEdit = new vscode.WorkspaceEdit()
448+
workspaceEdit.set(document.uri, [
449+
...tsTextChangesToVscodeTextEdits(document, edits),
450+
...tsTextChangesToVscodeSnippetTextEdits(document, snippetEdits),
451+
])
452+
codeAction.edit = workspaceEdit
453+
return codeAction
454+
},
395455
})
396456
// #endregion
397457
}

src/util.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export const tsRangeToVscode = (document: vscode.TextDocument, [start, end]: [nu
1212
export const tsRangeToVscodeSelection = (document: vscode.TextDocument, [start, end]: [number, number]) =>
1313
new vscode.Selection(document.positionAt(start), document.positionAt(end))
1414

15-
export const tsTextChangesToVcodeTextEdits = (document: vscode.TextDocument, edits: Array<import('typescript').TextChange>): vscode.TextEdit[] =>
15+
export const tsTextChangesToVscodeTextEdits = (document: vscode.TextDocument, edits: Array<import('typescript').TextChange>): vscode.TextEdit[] =>
1616
edits.map(({ span, newText }) => {
1717
const start = document.positionAt(span.start)
1818
return {
@@ -21,6 +21,18 @@ export const tsTextChangesToVcodeTextEdits = (document: vscode.TextDocument, edi
2121
}
2222
})
2323

24+
export const tsTextChangesToVscodeSnippetTextEdits = (document: vscode.TextDocument, edits: Array<import('typescript').TextChange>): vscode.SnippetTextEdit[] =>
25+
edits.map(({ span, newText }) => {
26+
const start = document.positionAt(span.start)
27+
return {
28+
range: new vscode.Range(start, offsetPosition(document, start, span.length)),
29+
snippet: new vscode.SnippetString(newText),
30+
}
31+
})
32+
33+
export const vscodeRangeToTs = (document: vscode.TextDocument, range: vscode.Range) =>
34+
[document.offsetAt(range.start), document.offsetAt(range.end)] as [number, number]
35+
2436
export const getTsLikePath = <T extends vscode.Uri | undefined>(uri: T): T extends undefined ? undefined : string =>
2537
uri && (normalizeWindowPath(uri.fsPath) as any)
2638

typescript/src/codeActions/custom/splitDeclarationAndInitialization.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export default {
55
name: 'Split Declaration and Initialization',
66
id: 'splitDeclarationAndInitialization',
77
kind: 'refactor.rewrite.split-declaration-and-initialization',
8-
tryToApply(sourceFile, position, range, node, formatOptions, languageService, languageServiceHost) {
8+
tryToApply(sourceFile, position, range, node, formatOptions, languageService) {
99
if (range || !node) return
1010
if (!ts.isVariableDeclarationList(node) || node.declarations.length !== 1) return
1111
const declaration = node.declarations[0]!
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { ExtendedCodeAction } from '../getCodeActions'
2+
3+
export default {
4+
codes: [2339],
5+
kind: 'quickfix',
6+
title: 'Declare missing property',
7+
tryToApply({ sourceFile, node }) {
8+
if (node && ts.isIdentifier(node) && ts.isObjectBindingPattern(node.parent.parent) && ts.isParameter(node.parent.parent.parent)) {
9+
const param = node.parent.parent.parent
10+
// special react pattern
11+
if (ts.isArrowFunction(param.parent) && ts.isVariableDeclaration(param.parent.parent)) {
12+
const variableDecl = param.parent.parent
13+
if (variableDecl.type?.getText().match(/(React\.)?FC/)) {
14+
// handle interface
15+
}
16+
}
17+
// general patterns
18+
if (param.type && ts.isTypeLiteralNode(param.type) && param.type.members) {
19+
const hasMembers = param.type.members.length !== 0
20+
const insertPos = param.type.members.at(-1)?.end ?? param.type.end - 1
21+
const insertComma = hasMembers && sourceFile.getFullText().slice(insertPos - 1, insertPos) !== ','
22+
let insertText = node.text
23+
if (insertComma) insertText = `, ${insertText}`
24+
// alternatively only one snippetEdit could be used with tsFull.escapeSnippetText(insertText) + $0
25+
return {
26+
edits: [
27+
{
28+
newText: insertText,
29+
span: {
30+
length: 0,
31+
start: insertPos,
32+
},
33+
},
34+
],
35+
snippetEdits: [
36+
{
37+
newText: '$0',
38+
span: {
39+
length: 0,
40+
start: insertPos + insertText.length - 1,
41+
},
42+
},
43+
],
44+
}
45+
}
46+
}
47+
return
48+
},
49+
} as ExtendedCodeAction

0 commit comments

Comments
 (0)