Skip to content

Commit

Permalink
feat: refactor renameVariableParts (#34)
Browse files Browse the repository at this point in the history
- Now pressing esc in renaming part doesn't discard whole operation
- When entering part renaming, whole text is selected
- More precisely selecting range for variable to rename, ignoring user selection
- Now the part that was under cursor is preselected
- Add command for renaming file name parts with using the same quickpick
  • Loading branch information
zardoy committed Nov 15, 2022
1 parent 6743d09 commit ff9a1b0
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 72 deletions.
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,10 @@
{
"command": "generateGitlabPushAccept",
"title": "Accept created push sequence"
},
{
"command": "renameFileParts",
"title": "Rename File Parts"
}
],
"configuration": {
Expand Down
2 changes: 2 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { registerProductIconReference } from './features/productIconReference'
import { registerSelectLineContents } from './features/selectLineContents'
import { registerCutLineContents } from './features/cutLineContents'
import { registerCutLineContentsPreserve } from './features/cutLineContentsPreserve'
import { registerRenameFileParts } from './features/renameFileParts'
import typeDecorations from './features/typeDecorations'
import autoRemoveSemicolon from './features/autoRemoveSemicolon'
import printDocumentUri from './features/printDocumentUri'
Expand Down Expand Up @@ -131,6 +132,7 @@ export const activate = () => {
openReferencesInView()
statusbarOccurrencesCount()
generateGitlabPush()
registerRenameFileParts()

registerExtensionCommand('fixedTerminalMaximize', async () => {
await vscode.commands.executeCommand('workbench.action.toggleMaximizedPanel')
Expand Down
9 changes: 9 additions & 0 deletions src/features/renameFileParts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import * as vscode from 'vscode'
import { getActiveRegularEditor } from '@zardoy/vscode-utils'
import { getExtensionCommandId, registerExtensionCommand } from 'vscode-framework'

export const registerRenameFileParts = () => {
registerExtensionCommand('renameFileParts', async () => {
void vscode.commands.executeCommand(getExtensionCommandId('renameVariableParts'), 'fileName')
})
}
218 changes: 146 additions & 72 deletions src/features/renameVariableParts.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,102 @@
import * as vscode from 'vscode'
import { getActiveRegularEditor } from '@zardoy/vscode-utils'
import { noCase, camelCase } from 'change-case'
import { getExtensionContributionsPrefix, registerExtensionCommand, RegularCommands } from 'vscode-framework'
import { getExtensionCommandId, getExtensionContributionsPrefix, registerExtensionCommand, RegularCommands } from 'vscode-framework'
import { proxy, subscribe } from 'valtio/vanilla'
import { lowerCaseFirst } from 'lower-case-first'
import { Utils as UriUtils } from 'vscode-uri'
import { extname } from 'path-browserify'

// doesn't support multicursor
export const registerRenameVariableParts = () => {
// small task: add back button when editingIndex (like in GitLens menus)
registerExtensionCommand('renameVariableParts', async () => {
registerExtensionCommand('renameVariableParts', async (_data, renamingEntity: 'fileName' | 'variable' = 'variable') => {
const activeEditor = getActiveRegularEditor()
if (!activeEditor) return

const quickPick = vscode.window.createQuickPick()
const { document, selection } = activeEditor
const editRange = selection.isEmpty ? document.getWordRangeAtPosition(selection.end) : selection
if (!editRange || editRange.isEmpty) {
await vscode.window.showWarningMessage('No word range at position')
return
const { uri } = document

let editRange = document.getWordRangeAtPosition(selection.end)
let prepareRenameResult = false
let inputTextOverride: string | undefined
if (renamingEntity === 'variable') {
try {
type PrepareRenameResult = {
range: vscode.Range
placeholder: string
}
const { placeholder, range } = await vscode.commands.executeCommand<PrepareRenameResult>('vscode.prepareRename', uri, selection.end)
editRange = range
inputTextOverride = placeholder
prepareRenameResult = true
} catch {}

if (!editRange || editRange.isEmpty) {
void vscode.window.showWarningMessage('You cannot rename this element')
return
}
}

const parts = proxy([] as string[])
let isDisposeEnabled = true
/** expected: dash or underscore */
let seperatorChar = ''
let separatorChar = ''

let preselectedPartIndex = -1

const parts = proxy([] as string[])

const getFilenameAndExtension = () => {
const fullFileName = UriUtils.basename(uri)
let fileName = fullFileName
let fullExt = ''
// cut extension twice e.g. file.test.ts -> file & .test.ts
for (const _i of Array.from({ length: 2 })) {
const ext = extname(fileName)
if (ext.length === 0) continue

fileName = fileName.slice(0, -ext.length)
fullExt = `${ext}${fullExt}`
}

return {
fileName,
fullExt,
}
}

const setActiveItem = (existingIndex: number) => {
// do it in next loop after items update (most probably)
setTimeout(() => {
quickPick.activeItems = [quickPick.items[existingIndex]!]
})
}

const preselectItemUnderCursor = (splittedParts: string[]) => {
const cursorPosOffset = document.offsetAt(selection.active)
const wordStartOffset = document.offsetAt(editRange!.start)
const selectedWordCursorOffset = Math.abs(cursorPosOffset - wordStartOffset)

let accumulatedLength = 0
for (const [i, part] of splittedParts.entries()) {
accumulatedLength += part.length
if (selectedWordCursorOffset <= accumulatedLength) {
preselectedPartIndex = i
setActiveItem(preselectedPartIndex)
break
}
}
}

const { fileName, fullExt } = getFilenameAndExtension()

// init parts
const inputText = document.getText(editRange)
const inputText = inputTextOverride ?? renamingEntity === 'variable' ? document.getText(editRange) : fileName
noCase(inputText, {
transform(part, index) {
if (index === 1) seperatorChar = inputText[parts[0]!.length]!
transform(part, index, wordParts) {
if (preselectedPartIndex === -1 && renamingEntity === 'variable') preselectItemUnderCursor(wordParts)
if (index === 1) separatorChar = inputText[parts[0]!.length]!
parts.push(part)
return ''
},
Expand All @@ -36,20 +105,18 @@ export const registerRenameVariableParts = () => {
// if transformation results no differences - we're already in camel case
// we don't support mixed casing e.g. SomeVariable_meta
const isCamelCase = [inputText, lowerCaseFirst(inputText)].includes(camelCase(inputText))
if (isCamelCase) seperatorChar = ''
if (isCamelCase) separatorChar = ''
let isPascalCase = parts[0]![0]!.toUpperCase() === parts[0]![0]

let editingIndex: number | undefined

const getResultingName = () => parts.join(seperatorChar)
const getQuickPickTitle = () => {
const getResultingName = () => parts.join(separatorChar)
const updateTitle = (state: 'input' | 'quickPick') => {
const partsTemp = [...parts]
const mainIndex = editingIndex === undefined ? getActiveItemIndex() ?? -1 : editingIndex
const mainIndex = getActiveItemIndex() ?? -1
const mainPart = partsTemp[mainIndex]
// handle -1 case
if (!mainPart) return ''
partsTemp[mainIndex] = editingIndex === undefined ? `|${mainPart}` : `[${mainPart}]`
return partsTemp.join(seperatorChar)
partsTemp[mainIndex] = state === 'input' ? `[${mainPart}]` : `|${mainPart}`
return partsTemp.join(separatorChar)
}

const getActiveItemIndex = () => {
Expand All @@ -58,24 +125,14 @@ export const registerRenameVariableParts = () => {
return quickPick.items.indexOf(activeItem)
}

const setActiveItem = (existingIndex: number) => {
// do it in next loop after items update (most probably)
setTimeout(() => {
quickPick.activeItems = [quickPick.items[existingIndex]!]
})
}

const upperCaseFirstLetter = (part: string) => `${part[0]!.toUpperCase()}${part.slice(1)}`

const updateMainTitle = () => {
quickPick.title = `Rename variable parts: ${getQuickPickTitle()}`
}
const updateMainTitle = (state: 'input' | 'quickPick') =>
`Rename ${renamingEntity === 'variable' ? 'variable' : 'filename'} parts: ${updateTitle(state)}`

const resetItems = () => {
const updateQuickPick = () => {
quickPick.items = parts.map(part => ({ label: part }))
if (editingIndex !== undefined) setActiveItem(editingIndex)
updateMainTitle()
editingIndex = undefined
quickPick.title = updateMainTitle('quickPick')
}

// watch parts
Expand All @@ -99,18 +156,16 @@ export const registerRenameVariableParts = () => {
}
}

if (editingIndex === undefined) resetItems()
updateQuickPick()
}

updateParts()
resetItems()
updateQuickPick()
subscribe(parts, updateParts)

quickPick.onDidChangeActive(() => {
if (editingIndex !== undefined) return
updateMainTitle()
quickPick.title = updateMainTitle('quickPick')
})

const moveVariableParts = (direction: 'up' | 'down') => {
const currentActiveItemIndex = getActiveItemIndex() ?? -1
const currentPart = parts[currentActiveItemIndex]
Expand All @@ -133,20 +188,20 @@ export const registerRenameVariableParts = () => {
quickPick,
registerCommand('renameVariablePartsDeletePart', () => {
if (parts.length === 0) return
parts.splice(editingIndex ?? getActiveItemIndex()!, 1)
resetItems()
parts.splice(getActiveItemIndex()!, 1)
updateQuickPick()
}),
registerCommand('renameVariablePartsExtractPart', () => {
if (parts.length === 0) return
const onlyPart = parts[editingIndex ?? getActiveItemIndex()!]!
const onlyPart = parts[getActiveItemIndex()!]!
parts.splice(0, parts.length)
parts.push(onlyPart)
resetItems()
updateQuickPick()
}),
registerCommand('renameVariablePartsLowercasePart', () => {
if (parts.length === 0) return
const index = editingIndex ?? getActiveItemIndex()!
const content = editingIndex ? quickPick.value : parts[index]!
const index = getActiveItemIndex()!
const content = parts[index]!

if (index === 0) {
isPascalCase = false
Expand All @@ -157,8 +212,7 @@ export const registerRenameVariableParts = () => {
const prevIndex = index - 1
parts.splice(index, 1)
parts[prevIndex] = `${parts[prevIndex]!}${content.toLowerCase()}`
if (editingIndex) quickPick.value = ''
resetItems()
updateQuickPick()
}),
registerCommand('renameVariablePartsPartMoveUp', () => {
moveVariableParts('up')
Expand All @@ -167,47 +221,67 @@ export const registerRenameVariableParts = () => {
moveVariableParts('down')
}),
registerCommand('renameVariablePartsAccept', async () => {
quickPick.hide()
const edit: vscode.WorkspaceEdit = await vscode.commands.executeCommand(
'vscode.executeDocumentRenameProvider',
document.uri,
selection.end,
getResultingName(),
if (renamingEntity === 'variable' && !prepareRenameResult) {
void vscode.window.showWarningMessage("Renaming current element isn't supported. You can only replace text.")
return
}

mainDisposable.dispose()
await vscode.window.withProgress(
{ location: vscode.ProgressLocation.Notification, title: `Renaming ${renamingEntity === 'fileName' ? 'file' : 'variable'}` },
async () => {
let edit = new vscode.WorkspaceEdit()

if (renamingEntity === 'fileName') edit.renameFile(uri, UriUtils.joinPath(uri, '..', `${getResultingName()}${fullExt}`))

if (renamingEntity === 'variable')
edit = await vscode.commands.executeCommand('vscode.executeDocumentRenameProvider', uri, selection.end, getResultingName())

await vscode.workspace.applyEdit(edit)
},
)
await vscode.workspace.applyEdit(edit)
}),
registerCommand('renameVariablePartsAcceptReplace', () => {
void activeEditor.edit(builder => {
builder.replace(editRange, getResultingName())
})
quickPick.hide()
if (renamingEntity === 'variable')
void activeEditor.edit(builder => {
builder.replace(editRange!, getResultingName())
})

if (renamingEntity === 'variable') void vscode.commands.executeCommand(getExtensionCommandId('renameVariablePartsAccept'))

mainDisposable.dispose()
}),
{
dispose() {
void vscode.commands.executeCommand('setContext', 'zardoyExperiments.renameVariablePartsOpened', false)
},
},
)
quickPick.onDidAccept(() => {
quickPick.onDidAccept(async () => {
const activeItem = quickPick.activeItems[0]!
if (editingIndex === undefined) {
if (!activeItem) return
editingIndex = quickPick.items.indexOf(activeItem)
const { label } = activeItem
quickPick.items = []
quickPick.title = `Editing part: ${getQuickPickTitle()}`
quickPick.value = label
return
if (!activeItem) return
const updatingPartIndex = quickPick.items.indexOf(activeItem)
const { label } = activeItem
isDisposeEnabled = false
const renamedPart = await vscode.window.showInputBox({ value: label, title: updateMainTitle('input') })

if (renamedPart === '') {
await vscode.commands.executeCommand(getExtensionCommandId('renameVariablePartsDeletePart'))
} else {
if (renamedPart) parts.splice(updatingPartIndex, 1, renamedPart)
setActiveItem(updatingPartIndex)
}

parts.splice(editingIndex, 1, quickPick.value)
resetItems()
quickPick.value = ''
})
quickPick.onDidHide(() => {
mainDisposable.dispose()
updateQuickPick()

quickPick.show()
isDisposeEnabled = true
})
await vscode.commands.executeCommand('setContext', 'zardoyExperiments.renameVariablePartsOpened', true)

quickPick.show()
quickPick.onDidHide(() => {
if (isDisposeEnabled) mainDisposable.dispose()
})
})
}

0 comments on commit ff9a1b0

Please sign in to comment.