diff --git a/CHANGELOG.md b/CHANGELOG.md index 81ea5fa..f1d5339 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,14 +17,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - ### Changed - ### Removed --> +## 1.0.8 + +### Added + +- Supports scrolling conversion via shortcut keys `Ctrl + Alt + [` and `Ctrl + Alt + ]` (simultaneously supports multi-line selection conversion) 支持通过快捷键滚动转换 (同时支持多行选区转换) ## 1.0.7 diff --git a/package-comment.jsonc b/package-comment.jsonc index 706d0d7..1a744cf 100644 --- a/package-comment.jsonc +++ b/package-comment.jsonc @@ -4,7 +4,7 @@ "displayName": "Variable Conversion", "description": "一个强大的变量名转换插件,支持右键菜单、快捷键、底栏等多种方式使用,支持小驼峰、大驼峰(帕斯卡)、下划线(蛇形)、中划线(连字符/脊柱式)、空格分隔、全小写、全大写等常用命名方式(及组合)转换。 \nA powerful variable naming conversion extension. You can use it through the editer menu, shortcut keys and bottom bar. Support camel, pascal, snake, kebab(spinal), space, lower, upper case, and more.", // 版本号 - "version": "1.0.7", + "version": "1.0.8", // logo "icon": "image/logo.png", "publisher": "coder-xiaomo", @@ -37,7 +37,45 @@ "onTextSelected" ], "contributes": { + // docs: https://code.visualstudio.com/docs/getstarted/keybindings#_accepted-keys + "keybindings": [ + // 绑定快捷键 + { + "command": "variable-conversion.convertCase", + "key": "shift+alt+t", + "when": "editorTextFocus" + }, + // 滚动转换 上一个 + { + "command": "variable-conversion.cyclicConvertCase.previous", + "key": "ctrl+alt+[", + "args": { + "arrowKey": "[" + }, + "when": "editorTextFocus" + }, + // 滚动转换 下一个 + { + "command": "variable-conversion.cyclicConvertCase.next", + "key": "ctrl+alt+]", + "args": { + "arrowKey": "]" + }, + "when": "editorTextFocus" + } + ], "commands": [ + /** + * 滚动转换 可以不添加 + */ + // { + // "command": "variable-conversion.cyclicConvertCase.previous", + // "title": "字符串转换(上一个)" + // }, + // { + // "command": "variable-conversion.cyclicConvertCase.next", + // "title": "字符串转换(下一个)" + // }, /** * 右键菜单 */ @@ -149,15 +187,6 @@ // "enablement": "false" // } ], - // docs: https://code.visualstudio.com/docs/getstarted/keybindings#_accepted-keys - "keybindings": [ - // 绑定快捷键 - { - "command": "variable-conversion.convertCase", - "key": "shift+alt+t", - "when": "editorTextFocus" - } - ], // docs: https://code.visualstudio.com/api/references/contribution-points#contributes.menus "menus": { // 编辑器右键菜单 diff --git a/package.json b/package.json index bde3d7c..9097db5 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "variable-conversion", "displayName": "Variable Conversion", "description": "一个强大的变量名转换插件,支持右键菜单、快捷键、底栏等多种方式使用,支持小驼峰、大驼峰(帕斯卡)、下划线(蛇形)、中划线(连字符/脊柱式)、空格分隔、全小写、全大写等常用命名方式(及组合)转换。 \nA powerful variable naming conversion extension. You can use it through the editer menu, shortcut keys and bottom bar. Support camel, pascal, snake, kebab(spinal), space, lower, upper case, and more.", - "version": "1.0.7", + "version": "1.0.8", "icon": "image/logo.png", "publisher": "coder-xiaomo", "engines": { @@ -31,6 +31,29 @@ "onTextSelected" ], "contributes": { + "keybindings": [ + { + "command": "variable-conversion.convertCase", + "key": "shift+alt+t", + "when": "editorTextFocus" + }, + { + "command": "variable-conversion.cyclicConvertCase.previous", + "key": "ctrl+alt+[", + "args": { + "arrowKey": "[" + }, + "when": "editorTextFocus" + }, + { + "command": "variable-conversion.cyclicConvertCase.next", + "key": "ctrl+alt+]", + "args": { + "arrowKey": "]" + }, + "when": "editorTextFocus" + } + ], "commands": [ { "command": "variable-conversion.convertCase", @@ -101,13 +124,6 @@ "title": "全大写 (Upper Case) [ FOOBAR ]" } ], - "keybindings": [ - { - "command": "variable-conversion.convertCase", - "key": "shift+alt+t", - "when": "editorTextFocus" - } - ], "menus": { "editor/context": [ { diff --git a/src/extension-handler/status-bar-handler.ts b/src/extension-handler/status-bar-handler.ts index 45c4405..2e02cec 100644 --- a/src/extension-handler/status-bar-handler.ts +++ b/src/extension-handler/status-bar-handler.ts @@ -5,6 +5,8 @@ import * as vscode from 'vscode'; let statusBar: vscode.StatusBarItem; /** + * 创建状态栏按钮 + * * @since 2024-04-07 */ export function createStatusBarItem() { @@ -16,6 +18,8 @@ export function createStatusBarItem() { } /** + * 判断是否展示状态栏按钮 + * * @since 2024-04-07 */ export function updateStatusBarItemVisable(selectTextLength: number) { diff --git a/src/extension.ts b/src/extension.ts index 1914b56..dca5648 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -5,56 +5,80 @@ import handleEditorReplace from './extension-handler/editor-submenu-handler'; import { handleQuickPick } from './extension-handler/quick-pick-handler'; import { commands } from './type-definition/SupportCaseType'; import { createStatusBarItem, updateStatusBarItemVisable } from './extension-handler/status-bar-handler'; +import * as CyclicConversion from './main-code/cyclic-conversion'; +import { EOL } from './type-definition/EOLType'; // This method is called when your extension is activated // Your extension is activated the very first time the command is executed export function activate(context: vscode.ExtensionContext) { + try { + // 获取当前插件的扩展对象 + const currentExtension = vscode.extensions.getExtension('coder-xiaomo.variable-conversion'); + if (currentExtension) { + // 获取版本号 + const version = currentExtension.packageJSON.version; + console.log('[Variable Conversion] current version:', version); + } + } catch (err) { + console.log('get current extension failed', err); + } - // // Use the console to output diagnostic information (console.log) and errors (console.error) - // // This line of code will only be executed once when your extension is activated - // console.log('Congratulations, your extension "variable-conversion" is now active!'); - - // // The command has been defined in the package.json file - // // Now provide the implementation of the command with registerCommand - // // The commandId parameter must match the command field in package.json - // let disposable = vscode.commands.registerCommand('variable-conversion.helloWorld', () => { - // // The code you place here will be executed every time your command is executed - // // Display a message box to the user - // vscode.window.showInformationMessage('Hello World from variable-conversion!'); - // }); - + // 用于记录当前选中的文本的长度 let selectTextLength = 0; + + // 选中文本改变时触发 + const onTextEditorSelectionChangeCallback = (textEditor: vscode.TextEditor, selections: readonly vscode.Selection[]) => { + // 获取选中的文本 + const text: string = textEditor.document.getText(selections[0]); + selectTextLength = text.length; + + // 获取选中的文本块 + const textList: string[] = []; + for (const selection of selections) { + const text = textEditor.document.getText(selection); + textList.push(text); + } + + // 更新 _textSelectionLength (用于判断是否展示右键菜单) + vscode.commands.executeCommand('setContext', '_textSelectionLength', selectTextLength); + + // 判断是否展示状态栏按钮 + updateStatusBarItemVisable(selectTextLength); + + // 滚动转换:记录当前选中内容,并且进行转换 + let eol: EOL = textEditor.document.eol === vscode.EndOfLine.CRLF ? '\r\n' : '\n'; + CyclicConversion.onUserSelectionUpdated(selections, textList, eol); + }; + + // 创建状态栏按钮 createStatusBarItem(); + + /** + * 切换编辑器 Tab 时触发 + */ vscode.window.onDidChangeActiveTextEditor(event => { + console.log('onDidChangeActiveTextEditor', event); + // 判断是否展示状态栏按钮 updateStatusBarItemVisable(selectTextLength); }); - // 用于判断是否展示右键菜单 + /** + * 编辑器中光标选中位置改变触发 + */ vscode.window.onDidChangeTextEditorSelection(event => { - const text = event.textEditor.document.getText(event.selections[0]); - // console.log('text.length', text.length); - vscode.commands.executeCommand('setContext', '_textSelectionLength', text.length); - - selectTextLength = text.length; - updateStatusBarItemVisable(selectTextLength); + console.log('光标选中位置改变 onDidChangeTextEditorSelection', event); + // 执行 Callback + onTextEditorSelectionChangeCallback(event.textEditor, event.selections); }); // 初始(VSCode 插件初始化)时也判断一次 (考虑上次关闭 VSCode 有选区,重新打开后 VSCode 回复选区但用户未重新切换选区的场景) let editor = vscode.window.activeTextEditor; if (editor) { - let document = editor.document; - let selection = editor.selection; - // 获取选中的文本 - let text = document.getText(selection); - vscode.commands.executeCommand('setContext', '_textSelectionLength', text.length); - - selectTextLength = text.length; - updateStatusBarItemVisable(selectTextLength); - } else { - // vscode.window.showInformationMessage('editor is undefined'); - console.log('editor is undefined'); + // VSCode 启动时打开的 Tab 不是编辑器 Tab, 执行 Callback (如果不是则跳过) + onTextEditorSelectionChangeCallback(editor, editor.selections); } + // 逐一注册右键菜单-子菜单项 command for (const { command, targetCase } of commands) { let disposable = vscode.commands.registerCommand(command, () => { handleEditorReplace(targetCase); @@ -62,8 +86,21 @@ export function activate(context: vscode.ExtensionContext) { context.subscriptions.push(disposable); } + // 注册字符串转换 command 状态栏/快捷键/右键[字符串转换]菜单均有用到 let convertCaseDisposable = vscode.commands.registerCommand('variable-conversion.convertCase', handleQuickPick); context.subscriptions.push(convertCaseDisposable); + + // 注册滚动转换 command + let disposableLoopConversionPrev = vscode.commands.registerCommand('variable-conversion.cyclicConvertCase.previous', ({ arrowKey }) => { + console.log('variable-conversion.convertCase', arrowKey); + CyclicConversion.previousOne(); + }); + context.subscriptions.push(disposableLoopConversionPrev); + let disposableLoopConversionNext = vscode.commands.registerCommand('variable-conversion.cyclicConvertCase.next', ({ arrowKey }) => { + console.log('variable-conversion.convertCase', arrowKey); + CyclicConversion.nextOne(); + }); + context.subscriptions.push(disposableLoopConversionNext); } // This method is called when your extension is deactivated diff --git a/src/main-code/cyclic-conversion.ts b/src/main-code/cyclic-conversion.ts new file mode 100644 index 0000000..8f052e1 --- /dev/null +++ b/src/main-code/cyclic-conversion.ts @@ -0,0 +1,104 @@ +import * as vscode from 'vscode'; +import { EOL } from "../type-definition/EOLType"; +import { cyclicConvertCaseOrder } from "../type-definition/SupportCaseType"; +import { caseConversion } from "./conversion"; +import { isStringArrayEqual, stringListArrayDuplicateRemoval } from './utils'; + +interface UserSelection { + currentEol: EOL + currentSelections?: readonly vscode.Selection[] + currentSelectionsText: string[] + currentIndex: number + isConverted: boolean + conversionsTarget: Array + lastConvertedSelectionsText: string[] // 按快捷键后转换的值(如果下次触发 onUserSelectionUpdated 后传入值是这个,那么跳过,避免丢失当前滚动转换记录) +} + +const userSelection: UserSelection = { + currentEol: '\n', + // currentSelections: undefined, + currentSelectionsText: [], + currentIndex: 0, + isConverted: false, + conversionsTarget: [], // 转换后去重 剩余转换目标 + lastConvertedSelectionsText: [], +}; + +export function onUserSelectionUpdated(selections: readonly vscode.Selection[], textList: string[], eol: EOL): void { + if (textList.length !== 0 && isStringArrayEqual(textList, userSelection.lastConvertedSelectionsText)) { + console.log('skip onUserSelectionUpdated'); + return; + } + console.log('onUserSelectionUpdated', textList, userSelection.lastConvertedSelectionsText); + userSelection.currentEol = eol; + userSelection.currentSelections = selections; + userSelection.currentSelectionsText = textList; + userSelection.currentIndex = 0; + userSelection.isConverted = false; + userSelection.conversionsTarget = []; + userSelection.lastConvertedSelectionsText = textList; +} + +export function previousOne() { + lazyConvert(); + const length = userSelection.conversionsTarget.length; + const oldIndex = userSelection.currentIndex; + const newIndex = oldIndex === 0 ? (length - 1) : (oldIndex - 1); + userSelection.currentIndex = newIndex; + console.log('previousOne oldIndex', oldIndex, 'newIndex', newIndex); + replaceTextEditorSelectedText(); +} + +export function nextOne() { + lazyConvert(); + const length = userSelection.conversionsTarget.length; + const oldIndex = userSelection.currentIndex; + const newIndex = oldIndex >= length - 1 ? 0 : (oldIndex + 1); + userSelection.currentIndex = newIndex; + console.log('nextOne oldIndex', oldIndex, 'newIndex', newIndex); + replaceTextEditorSelectedText(); +} + +function lazyConvert() { + if (userSelection.isConverted) { + return; + } + + const textList = userSelection.currentSelectionsText; + const eol = userSelection.currentEol; + const conversionsTarget: Array = []; + for (const cyclicConvertCase of cyclicConvertCaseOrder) { + // 每一个类型 + const conversionsTargetItem: string[] = []; + for (const line of textList) { + // 选中区块的每一行 + const conversionResult: string = caseConversion(cyclicConvertCase, line, eol); + conversionsTargetItem.push(conversionResult); + } + conversionsTarget.push(conversionsTargetItem); + } + + // 按数组去重 + const noDuplicate = stringListArrayDuplicateRemoval(conversionsTarget); + console.log('noDuplicate', noDuplicate); + + userSelection.conversionsTarget = conversionsTarget; + userSelection.isConverted = true; +} + +function replaceTextEditorSelectedText() { + let editor = vscode.window.activeTextEditor; + if (editor) { + const selections = userSelection.currentSelections || []; + const textList = userSelection.conversionsTarget[userSelection.currentIndex]; + console.log('selections', selections, 'textList', textList); + editor.edit(editBuilder => { + for (let i = 0; i < selections.length; i++) { + const selection = selections[i]; + const converted = textList[i]; + editBuilder.replace(selection, converted); + } + }); + userSelection.lastConvertedSelectionsText = textList; + } +} \ No newline at end of file diff --git a/src/main-code/utils.ts b/src/main-code/utils.ts new file mode 100644 index 0000000..7a29c9f --- /dev/null +++ b/src/main-code/utils.ts @@ -0,0 +1,27 @@ +export function isStringArrayEqual(array1: string[], array2: string[]) { + if (array1.length !== array2.length) { + return false; + } + for (let index = 0; index < array1.length; index++) { + const element1 = array1[index]; + const element2 = array2[index]; + if (element1 !== element2) { + return false; + } + } + return true; +} + +export function stringListArrayDuplicateRemoval(stringArr: Array): Array { + const tempArr: Array = []; + const newArr: Array = []; + for (let index = 0; index < stringArr.length; index++) { + const element = stringArr[index]; + const elementStr = JSON.stringify(element); + if (!tempArr.includes(elementStr)) { + newArr.push(element); + tempArr.push(elementStr); + } + } + return newArr; +} \ No newline at end of file diff --git a/src/type-definition/SupportCaseType.ts b/src/type-definition/SupportCaseType.ts index ccd0122..6f81b71 100644 --- a/src/type-definition/SupportCaseType.ts +++ b/src/type-definition/SupportCaseType.ts @@ -359,3 +359,31 @@ export const quickPickSupportCases = [ keyword: keyword.upper, }, ]; + +/** + * 通过快捷键滚动转换的顺序 + * @since 2024-04-08 + */ +export const cyclicConvertCaseOrder = [ + SupportCase.CAMEL_CASE, + SupportCase.PASCAL_CASE, + + SupportCase.SNAKE_CASE, + SupportCase.KEBAB_CASE, + SupportCase.SPACE_CASE, + + SupportCase.SNAKE_UPPER_CASE, + SupportCase.KEBAB_UPPER_CASE, + SupportCase.SPACE_UPPER_CASE, + + SupportCase.SNAKE_PASCAL_CASE, + SupportCase.KEBAB_PASCAL_CASE, + SupportCase.SPACE_PASCAL_CASE, + + SupportCase.SNAKE_CAMEL_CASE, + SupportCase.KEBAB_CAMEL_CASE, + SupportCase.SPACE_CAMEL_CASE, + + SupportCase.LOWER_CASE, + SupportCase.UPPER_CASE, +];