Skip to content

Commit 20ee5fe

Browse files
committed
preview: add move to existing file code action (refactoring)
also refactor two step refactorings to general interfrace
1 parent db09aca commit 20ee5fe

File tree

8 files changed

+274
-75
lines changed

8 files changed

+274
-75
lines changed

src/specialCommands.ts

Lines changed: 128 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@ import * as vscode from 'vscode'
22
import { getActiveRegularEditor } from '@zardoy/vscode-utils'
33
import { getExtensionCommandId, getExtensionSetting, registerExtensionCommand, VSCodeQuickPickItem } from 'vscode-framework'
44
import { showQuickPick } from '@zardoy/vscode-utils/build/quickPick'
5-
import _ from 'lodash'
5+
import _, { partition } from 'lodash'
66
import { compact } from '@zardoy/utils'
77
import { defaultJsSupersetLangsWithVue } from '@zardoy/vscode-utils/build/langs'
88
import { offsetPosition } from '@zardoy/vscode-utils/build/position'
9+
import { relative, join } from 'path-browserify'
910
import { RequestOptionsTypes, RequestResponseTypes } from '../typescript/src/ipcTypes'
1011
import { sendCommand } from './sendCommand'
11-
import { tsRangeToVscode, tsRangeToVscodeSelection, tsTextChangesToVcodeTextEdits } from './util'
12+
import { getTsLikePath, pickFileWithQuickPick, tsRangeToVscode, tsRangeToVscodeSelection, tsTextChangesToVcodeTextEdits } from './util'
1213

1314
export default () => {
1415
registerExtensionCommand('removeFunctionArgumentsTypesInSelection', async () => {
@@ -221,49 +222,29 @@ export default () => {
221222
await vscode.commands.executeCommand(getExtensionCommandId('goToNodeBySyntaxKind'), { filterWithSelection: true })
222223
})
223224

224-
async function sendTurnIntoArrayRequest<T = RequestResponseTypes['turnArrayIntoObject']>(
225-
range: vscode.Range,
226-
selectedKeyName?: string,
227-
document = vscode.window.activeTextEditor!.document,
228-
) {
229-
return sendCommand<T, RequestOptionsTypes['turnArrayIntoObject']>('turnArrayIntoObject', {
225+
async function getPossibleTwoStepRefactorings(range: vscode.Range, document = vscode.window.activeTextEditor!.document) {
226+
return sendCommand<RequestResponseTypes['getTwoStepCodeActions'], RequestOptionsTypes['getTwoStepCodeActions']>('getTwoStepCodeActions', {
230227
document,
231228
position: range.start,
232229
inputOptions: {
233230
range: [document.offsetAt(range.start), document.offsetAt(range.end)] as [number, number],
234-
selectedKeyName,
235231
},
236232
})
237233
}
238234

239-
registerExtensionCommand('turnArrayIntoObjectRefactoring' as any, async (_, arg?: RequestResponseTypes['turnArrayIntoObject']) => {
240-
if (!arg) return
241-
const { keysCount, totalCount, totalObjectCount } = arg
242-
const selectedKey: string | false | undefined =
243-
// eslint-disable-next-line @typescript-eslint/dot-notation
244-
arg['key'] ||
245-
(await showQuickPick(
246-
Object.entries(keysCount).map(([key, count]) => {
247-
const isAllowed = count === totalObjectCount
248-
return { label: `${isAllowed ? '$(check)' : '$(close)'}${key}`, value: isAllowed ? key : false, description: `${count} hits` }
249-
}),
250-
{
251-
title: `Selected available key from ${totalObjectCount} objects (${totalCount} elements)`,
235+
async function getSecondStepRefactoringData(range: vscode.Range, secondStepData?: any, document = vscode.window.activeTextEditor!.document) {
236+
return sendCommand<RequestResponseTypes['twoStepCodeActionSecondStep'], RequestOptionsTypes['twoStepCodeActionSecondStep']>(
237+
'twoStepCodeActionSecondStep',
238+
{
239+
document,
240+
position: range.start,
241+
inputOptions: {
242+
range: [document.offsetAt(range.start), document.offsetAt(range.end)] as [number, number],
243+
data: secondStepData,
252244
},
253-
))
254-
if (selectedKey === undefined || selectedKey === '') return
255-
if (selectedKey === false) {
256-
void vscode.window.showWarningMessage("Can't use selected key as its not used in every object")
257-
return
258-
}
259-
260-
const editor = vscode.window.activeTextEditor!
261-
const edits = await sendTurnIntoArrayRequest<RequestResponseTypes['turnArrayIntoObjectEdit']>(editor.selection, selectedKey)
262-
if (!edits) throw new Error('Unknown error. Try debug.')
263-
const edit = new vscode.WorkspaceEdit()
264-
edit.set(editor.document.uri, tsTextChangesToVcodeTextEdits(editor.document, edits))
265-
await vscode.workspace.applyEdit(edit)
266-
})
245+
},
246+
)
247+
}
267248

268249
registerExtensionCommand('acceptRenameWithParams' as any, async (_, { preview = false, comments = null, strings = null, alias = null } = {}) => {
269250
const editor = vscode.window.activeTextEditor
@@ -284,7 +265,90 @@ export default () => {
284265
await vscode.commands.executeCommand(preview ? 'acceptRenameInputWithPreview' : 'acceptRenameInput')
285266
})
286267

287-
// its actually a code action, but will be removed from there soon
268+
// #region two-steps code actions
269+
registerExtensionCommand('applyRefactor' as any, async (_, arg?: RequestResponseTypes['getTwoStepCodeActions']) => {
270+
if (!arg) return
271+
let sendNextData: RequestOptionsTypes['twoStepCodeActionSecondStep']['data'] | undefined
272+
const { turnArrayIntoObject, moveToExistingFile } = arg
273+
if (turnArrayIntoObject) {
274+
const { keysCount, totalCount, totalObjectCount } = turnArrayIntoObject
275+
const selectedKey = await showQuickPick(
276+
Object.entries(keysCount).map(([key, count]) => {
277+
const isAllowed = count === totalObjectCount
278+
return { label: `${isAllowed ? '$(check)' : '$(close)'}${key}`, value: isAllowed ? key : false, description: `${count} hits` }
279+
}),
280+
{
281+
title: `Selected available key from ${totalObjectCount} objects (${totalCount} elements)`,
282+
},
283+
)
284+
if (selectedKey === undefined || selectedKey === '') return
285+
if (selectedKey === false) {
286+
void vscode.window.showWarningMessage("Can't use selected key as its not used in object of every element")
287+
return
288+
}
289+
290+
sendNextData = {
291+
name: 'turnArrayIntoObject',
292+
selectedKeyName: selectedKey as string,
293+
}
294+
}
295+
296+
if (moveToExistingFile) {
297+
sendNextData = {
298+
name: 'moveToExistingFile',
299+
}
300+
}
301+
302+
if (!sendNextData) return
303+
const editor = vscode.window.activeTextEditor!
304+
const nextResponse = await getSecondStepRefactoringData(editor.selection, sendNextData)
305+
if (!nextResponse) throw new Error('No code action data. Try debug.')
306+
const edit = new vscode.WorkspaceEdit()
307+
let mainChanges = 'edits' in nextResponse && nextResponse.edits
308+
if (moveToExistingFile && 'fileNames' in nextResponse) {
309+
const { fileNames, fileEdits } = nextResponse
310+
const selectedFilePath = await pickFileWithQuickPick(fileNames)
311+
if (!selectedFilePath) return
312+
const document = await vscode.workspace.openTextDocument(vscode.Uri.file(selectedFilePath))
313+
const outline = await vscode.commands.executeCommand('vscode.executeDocumentSymbolProvider', document.uri)
314+
const currentEditorPath = getTsLikePath(vscode.window.activeTextEditor!.document.uri)
315+
// currently ignoring other files due to https://github.com/microsoft/TypeScript/issues/32344
316+
// TODO-high it ignores any updates in https://github.com/microsoft/TypeScript/blob/20182cf8485ca5cf360d9396ad25d939b848a0ec/src/services/refactors/moveToNewFile.ts#L290
317+
const currentFileEdits = [...fileEdits.find(fileEdit => fileEdit.fileName === currentEditorPath)!.textChanges]
318+
const textChangeIndexToPatch = currentFileEdits.findIndex(currentFileEdit => currentFileEdit.newText.trim())
319+
const { newText: updateImportText } = currentFileEdits[textChangeIndexToPatch]!
320+
// TODO-mid use native path resolver (ext, index, alias)
321+
let newRelativePath = relative(join(currentEditorPath, '..'), selectedFilePath)
322+
if (!newRelativePath.startsWith('./') && !newRelativePath.startsWith('../')) newRelativePath = `./${newRelativePath}`
323+
currentFileEdits[textChangeIndexToPatch]!.newText = updateImportText.replace(/(['"]).+(['"])/, (_m, g1) => `${g1}${newRelativePath}${g1}`)
324+
mainChanges = currentFileEdits
325+
const newFileText = fileEdits.find(fileEdit => fileEdit.isNewFile)!.textChanges[0]!.newText
326+
const [importLines, otherLines] = partition(newFileText.split('\n'), line => line.startsWith('import '))
327+
const startPos = new vscode.Position(0, 0)
328+
const newFileNodes = await sendCommand<RequestResponseTypes['filterBySyntaxKind']>('filterBySyntaxKind', {
329+
position: startPos,
330+
document,
331+
})
332+
const lastImportDeclaration = newFileNodes?.nodesByKind.ImportDeclaration?.at(-1)
333+
const lastImportEnd = lastImportDeclaration ? tsRangeToVscode(document, lastImportDeclaration.range).end : startPos
334+
edit.set(vscode.Uri.file(selectedFilePath), [
335+
{
336+
range: new vscode.Range(startPos, startPos),
337+
newText: [...importLines, '\n'].join('\n'),
338+
},
339+
{
340+
range: new vscode.Range(lastImportEnd, lastImportEnd),
341+
newText: ['\n', ...otherLines].join('\n'),
342+
},
343+
])
344+
}
345+
346+
if (!mainChanges) return
347+
edit.set(editor.document.uri, tsTextChangesToVcodeTextEdits(editor.document, mainChanges))
348+
await vscode.workspace.applyEdit(edit)
349+
})
350+
351+
// most probably will be moved to ts-code-actions extension
288352
vscode.languages.registerCodeActionsProvider(defaultJsSupersetLangsWithVue, {
289353
async provideCodeActions(document, range, context, token) {
290354
if (document !== vscode.window.activeTextEditor?.document || !getExtensionSetting('enablePlugin')) {
@@ -301,22 +365,34 @@ export default () => {
301365
}
302366

303367
if (context.triggerKind !== vscode.CodeActionTriggerKind.Invoke) return
304-
const result = await sendTurnIntoArrayRequest(range)
368+
const result = await getPossibleTwoStepRefactorings(range)
305369
if (!result) return
306-
const { keysCount, totalCount, totalObjectCount } = result
307-
return [
308-
{
309-
title: `Turn Array Into Object (${totalCount} elements)`,
310-
command: getExtensionCommandId('turnArrayIntoObjectRefactoring' as any),
311-
arguments: [
312-
{
313-
keysCount,
314-
totalCount,
315-
totalObjectCount,
316-
} satisfies RequestResponseTypes['turnArrayIntoObject'],
317-
],
318-
},
319-
]
370+
const { turnArrayIntoObject, moveToExistingFile } = result
371+
const codeActions: vscode.CodeAction[] = []
372+
const getCommand = (arg): vscode.Command | undefined => ({
373+
title: '',
374+
command: getExtensionCommandId('applyRefactor' as any),
375+
arguments: [arg],
376+
})
377+
378+
if (turnArrayIntoObject) {
379+
codeActions.push({
380+
title: `Turn array into object (${turnArrayIntoObject.totalCount} elements)`,
381+
command: getCommand({ turnArrayIntoObject }),
382+
kind: vscode.CodeActionKind.RefactorRewrite,
383+
})
384+
}
385+
386+
if (moveToExistingFile) {
387+
codeActions.push({
388+
title: `Move to existing file`,
389+
command: getCommand({ moveToExistingFile }),
390+
kind: vscode.CodeActionKind.Refactor.append('move'),
391+
})
392+
}
393+
394+
return codeActions
320395
},
321396
})
397+
// #endregion
322398
}

src/util.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import * as vscode from 'vscode'
22
import { offsetPosition } from '@zardoy/vscode-utils/build/position'
3+
import { showQuickPick } from '@zardoy/vscode-utils/build/quickPick'
4+
import { Utils } from 'vscode-uri'
5+
import { relative, join } from 'path-browserify'
6+
7+
const normalizeWindowPath = (path: string | undefined) => path?.replace(/\\/g, '/')
38

49
export const tsRangeToVscode = (document: vscode.TextDocument, [start, end]: [number, number]) =>
510
new vscode.Range(document.positionAt(start), document.positionAt(end))
@@ -15,3 +20,48 @@ export const tsTextChangesToVcodeTextEdits = (document: vscode.TextDocument, edi
1520
newText,
1621
}
1722
})
23+
24+
export const getTsLikePath = <T extends vscode.Uri | undefined>(uri: T): T extends undefined ? undefined : string =>
25+
uri && (normalizeWindowPath(uri.fsPath) as any)
26+
27+
// pick other file
28+
export const pickFileWithQuickPick = async (fileNames: string[], optionsOverride?) => {
29+
const editorUri = vscode.window.activeTextEditor?.document.uri
30+
const editorFilePath = editorUri?.fsPath && getTsLikePath(editorUri)
31+
if (editorFilePath) fileNames = fileNames.filter(fileName => fileName !== editorFilePath)
32+
const currentWorkspacePath = editorUri && getTsLikePath(vscode.workspace.getWorkspaceFolder(editorUri)?.uri)
33+
const getItems = (filter?: string) => {
34+
const filterFilePath = filter && editorUri ? getTsLikePath(Utils.joinPath(editorUri, '..', filter)) : undefined
35+
const filtered = fileNames.filter(fileName => (filterFilePath ? fileName.startsWith(filterFilePath) : true))
36+
const relativePath = filterFilePath ? join(filterFilePath, '..') : currentWorkspacePath
37+
return filtered.map(fileName => {
38+
let label = relativePath ? relative(relativePath, fileName) : fileName
39+
if (filterFilePath && !label.startsWith('./') && !label.startsWith('../')) label = `./${label}`
40+
return {
41+
label,
42+
value: fileName,
43+
alwaysShow: !!filterFilePath,
44+
buttons: [
45+
{
46+
iconPath: new vscode.ThemeIcon('go-to-file'),
47+
tooltip: 'Open file',
48+
},
49+
],
50+
}
51+
})
52+
}
53+
54+
const selectedFile = await showQuickPick(getItems(), {
55+
title: 'Select file',
56+
onDidChangeValue(text) {
57+
this.items = ['../', './'].some(p => text.startsWith(p)) ? getItems(text) : getItems()
58+
},
59+
onDidTriggerItemButton(button) {
60+
if (button.button.tooltip === 'Open file') {
61+
void vscode.window.showTextDocument(vscode.Uri.file(button.item.value))
62+
}
63+
},
64+
...optionsOverride,
65+
})
66+
return selectedFile
67+
}

typescript/src/codeActions/decorateProxy.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,22 @@
1+
import { compact } from '@zardoy/utils'
2+
import { previousGetCodeActionsResult } from '../specialCommands/handle'
13
import { GetConfig } from '../types'
24
import { handleFunctionRefactorEdits, processApplicableRefactors } from './functionExtractors'
3-
import getCodeActions, { REFACTORS_CATEGORY } from './getCodeActions'
5+
import getCustomCodeActions, { REFACTORS_CATEGORY } from './getCodeActions'
46
import improveBuiltin from './improveBuiltin'
57

68
export default (proxy: ts.LanguageService, languageService: ts.LanguageService, languageServiceHost: ts.LanguageServiceHost, c: GetConfig) => {
79
proxy.getApplicableRefactors = (fileName, positionOrRange, preferences) => {
810
let prior = languageService.getApplicableRefactors(fileName, positionOrRange, preferences)
911

12+
previousGetCodeActionsResult.value = compact(
13+
prior.flatMap(refactor => {
14+
const actions = refactor.actions.filter(action => !action.notApplicableReason).map(action => action.description)
15+
if (!actions.length) return
16+
return actions.map(action => ({ description: refactor.description, name: action }))
17+
}),
18+
)
19+
1020
const program = languageService.getProgram()!
1121
const sourceFile = program.getSourceFile(fileName)!
1222
processApplicableRefactors(
@@ -18,7 +28,7 @@ export default (proxy: ts.LanguageService, languageService: ts.LanguageService,
1828

1929
if (c('markTsCodeActions.enable')) prior = prior.map(item => ({ ...item, description: `🔵 ${item.description}` }))
2030

21-
const { info: refactorInfo } = getCodeActions(sourceFile, positionOrRange, languageService, languageServiceHost)
31+
const { info: refactorInfo } = getCustomCodeActions(sourceFile, positionOrRange, languageService, languageServiceHost)
2232
if (refactorInfo) prior = [...prior, refactorInfo]
2333

2434
return prior
@@ -29,7 +39,7 @@ export default (proxy: ts.LanguageService, languageService: ts.LanguageService,
2939
if (category === REFACTORS_CATEGORY) {
3040
const program = languageService.getProgram()
3141
const sourceFile = program!.getSourceFile(fileName)!
32-
const { edit } = getCodeActions(sourceFile, positionOrRange, languageService, languageServiceHost, formatOptions, actionName)
42+
const { edit } = getCustomCodeActions(sourceFile, positionOrRange, languageService, languageServiceHost, formatOptions, actionName)
3343
return edit
3444
}
3545
if (refactorName === 'Extract Symbol' && actionName.startsWith('function_scope')) {

typescript/src/codeActions/getCodeActions.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { compact } from '@zardoy/utils'
22
import { findChildContainingPosition } from '../utils'
33
import objectSwapKeysAndValues from './custom/objectSwapKeysAndValues'
44
import changeStringReplaceToRegex from './custom/changeStringReplaceToRegex'
5-
import splitDeclarationAndInitialization from './custom/splitDeclarationAndInitialization'
65

76
type SimplifiedRefactorInfo =
87
| {
@@ -31,7 +30,7 @@ export type CodeAction = {
3130
tryToApply: ApplyCodeAction
3231
}
3332

34-
const codeActions: CodeAction[] = [/* toggleBraces */ objectSwapKeysAndValues, changeStringReplaceToRegex, splitDeclarationAndInitialization]
33+
const codeActions: CodeAction[] = [/* toggleBraces */ objectSwapKeysAndValues, changeStringReplaceToRegex]
3534

3635
export const REFACTORS_CATEGORY = 'essential-refactors'
3736

typescript/src/completions/keywordsSpace.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export default (entries: ts.CompletionEntry[], scriptSnapshot: ts.IScriptSnapsho
4040
})
4141
}
4242

43-
const isTypeNode = (node: ts.Node) => {
43+
export const isTypeNode = (node: ts.Node) => {
4444
if (ts.isTypeNode(node)) {
4545
// built-in types
4646
return true

0 commit comments

Comments
 (0)