Skip to content

Commit

Permalink
feat: ✨ add configuration to support overwrite word only when cursor …
Browse files Browse the repository at this point in the history
…is in middle of word

Signed-off-by: Krysl <krysl@qq.com>
  • Loading branch information
Krysl committed Apr 29, 2023
1 parent 44ccbd9 commit 1d7e615
Show file tree
Hide file tree
Showing 7 changed files with 260 additions and 15 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "copy-word",
"displayName": "Copy Word in Cursor",
"description": "Copy/Cut the current word when there is no selection",
"version": "3.10.0",
"version": "3.11.0",
"publisher": "alefragnani",
"galleryBanner": {
"color": "#0000FF",
Expand Down Expand Up @@ -62,6 +62,11 @@
"type": "boolean",
"default": false,
"description": "%copy-word.configuration.useOriginalCopyBehavior.description%"
},
"copyWord.overwriteWordBehavior": {
"type": "boolean",
"default": false,
"description": "%copy-word.configuration.overwriteWordBehavior.description%"
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
"copy-word.commands.cut.title": "Copy Word: Cut",
"copy-word.commands.paste.title": "Copy Word: Paste",
"copy-word.configuration.title": "Copy Word in Cursor",
"copy-word.configuration.useOriginalCopyBehavior.description": "Use original Cut/Copy behavior when no text is selected and no current word is defined."
"copy-word.configuration.useOriginalCopyBehavior.description": "Use original Cut/Copy behavior when no text is selected and no current word is defined.",
"copy-word.configuration.overwriteWordBehavior.description": "Overwrite word only when cursor is in middle of word."
}
8 changes: 8 additions & 0 deletions package.nls.zh-cn.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"copy-word.commands.copy.title": "复制单词: 复制",
"copy-word.commands.cut.title": "复制单词: 剪切",
"copy-word.commands.paste.title": "复制单词: 粘贴",
"copy-word.configuration.title": "复制光标处的单词",
"copy-word.configuration.useOriginalCopyBehavior.description": "当没有文本选中且没有定义当前单词时,使用原始的剪切复制行为。",
"copy-word.configuration.overwriteWordBehavior.description": "仅当光标在单词中间时覆盖单词。"
}
14 changes: 12 additions & 2 deletions src/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export function registerCommands() {

return true;
}

commands.registerCommand("copy-word.copy", async () => {
if (!canExecuteOperation(Operations.Copy)) { return; }

Expand Down Expand Up @@ -61,11 +61,21 @@ export function registerCommands() {

const editor = window.activeTextEditor!;
if (editor.selection.isEmpty) {
selectWordAtCursorPosition(editor);
if (overwriteWordOnlyWhenMiddle()) {
const cursorPosition = editor.selection.active;
const cursorWordRange = editor.document.getWordRangeAtPosition(cursorPosition);

if (cursorWordRange?.start.isBefore(cursorPosition) && cursorPosition.isBefore(cursorWordRange.end)) {
selectWordAtCursorPosition(editor);
}
} else {
selectWordAtCursorPosition(editor);
}
}
commands.executeCommand("editor.action.clipboardPasteAction");
});

const configuredToCopyLine = () => workspace.getConfiguration('copyWord').get('useOriginalCopyBehavior');
const overwriteWordOnlyWhenMiddle = () => workspace.getConfiguration('copyWord').get('overwriteWordBehavior');

}
7 changes: 6 additions & 1 deletion src/test/setupTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,13 @@ export async function setupTestSuite(originalValues) {
originalValues.useOriginalCopyBehavior = vscode.workspace.getConfiguration("copyWord").get<boolean>("useOriginalCopyBehavior", false);

await vscode.workspace.getConfiguration('copyWord').update('useOriginalCopyBehavior', false);

originalValues.overwriteWordBehavior = vscode.workspace.getConfiguration("copyWord").get<boolean>("overwriteWordBehavior", false);

await vscode.workspace.getConfiguration('copyWord').update('overwriteWordBehavior', false);
}

export async function teardownTestSuite(originalValues) {
await vscode.workspace.getConfiguration('copyWord').update('useOriginalCopyBehavior', originalValues.useOriginalCopyBehavior);
}
await vscode.workspace.getConfiguration('copyWord').update('overwriteWordBehavior', originalValues.overwriteWordBehavior);
}
232 changes: 224 additions & 8 deletions src/test/suite/commands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,176 @@ import * as vscode from 'vscode';
import * as assert from 'assert';
import * as sinon from 'sinon';
import { setupTestSuite, teardownTestSuite } from '../setupTests';
import { Operations } from '../../constants';

type PACKAGE_NAME = 'copy-word';
type CommandsType = `${Operations}`;
type Commands = `${PACKAGE_NAME}.${CommandsType}`;

type Public<T> = { [K in keyof T]: T[K] };

class CommandTestHelper {
doc!: vscode.TextDocument;
static fileCopys = new Set<string>();
async open(name = 'test2.md') {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-unnecessary-type-assertion
const workspaceUri = vscode.workspace.workspaceFolders![0]!.uri;
const filename = vscode.Uri.joinPath(workspaceUri, 'test.md');
const filename2 = vscode.Uri.joinPath(workspaceUri, name);
CommandTestHelper.fileCopys.add(filename2.toString());
await vscode.workspace.fs.copy(filename, filename2, { overwrite: true });
const doc = await vscode.workspace.openTextDocument(filename2);
this.doc = doc;
const originalDoc = await vscode.workspace.openTextDocument(filename);
const editor = await vscode.window.showTextDocument(this.doc);
const originalText = originalDoc.getText();
if (originalText !== doc.getText()) {
// sometimes not same, fix it
await editor.edit((builder) => {
builder.replace(new vscode.Range(0, 0, 1e4, 1e4), originalText);
});
}
assert.strictEqual(originalText, doc.getText(), 'file copy content should same to original');
return this.api();
}
static async clean() {
await vscode.commands.executeCommand('workbench.action.closeAllGroups');
const autoSave = vscode.workspace.getConfiguration('files').get<string>('autoSave');
if (autoSave !== 'off') {
// avoid deleted file appear again
await vscode.workspace.getConfiguration('files').update('autoSave', 'off');
}
for (const file of this.fileCopys) {
await vscode.workspace.fs.delete(vscode.Uri.parse(file));
}
this.fileCopys.clear();
if (autoSave !== 'off') {
await vscode.workspace.getConfiguration('files').update('autoSave', autoSave);
}
}

rangeOfWord(word: string) {
for (let index = 0; index < this.doc.lineCount; index++) {
const startColumn = this.doc.lineAt(index).text.indexOf(word);
if (startColumn > -1) {
return new vscode.Range(new vscode.Position(index, startColumn), new vscode.Position(index, startColumn + word.length));
}
}
throw new Error(`can not find word ${word}\n${this.doc.getText()}`);
}
putCursorAtWord(word: string | vscode.Range, offset: 'start' | 'end' | 'middle' | number) {
const range = typeof word === 'string' ? this.rangeOfWord(word) : word;
const positionWithOffset = (range: vscode.Range) => {
if (typeof offset === 'number') {

return range.start.with({ character: (offset >= 0) ? range.start.character + offset : range.end.character + offset });
} else {
switch (offset) {
case 'start':
return range.start;
case 'end':
return range.end;
case 'middle':
return range.start.with({ character: Math.floor((range.start.character + range.end.character) / 2) });
}
}
};
const position: vscode.Position = positionWithOffset(range);
const sel = new vscode.Selection(position, position);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-unnecessary-type-assertion
vscode.window.activeTextEditor!.selection = sel;

}
async copyWord(word: string) {
// put the cursor at the word
this.putCursorAtWord(word, 'start');

// runs the command, copy `thank` word
await this.runCommand('copy-word.copy');
}

private config = vscode.workspace.getConfiguration('copyWord');
get useOriginalCopyBehavior() {
return this.config.get<boolean>('useOriginalCopyBehavior');
}
async setUseOriginalCopyBehavior(value: boolean) {
await this.config.update('useOriginalCopyBehavior', value);
}
get overwriteWordBehavior() {
return this.config.get<boolean>('overwriteWordBehavior');
}
async setOverwriteWordBehavior(value: boolean) {
await this.config.update('overwriteWordBehavior', value);
}
async runCommand(cmd: Commands, settings?: {
useOriginalCopyBehavior?: boolean,
overwriteWordBehavior?: boolean,
}) {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (settings?.useOriginalCopyBehavior) {
await this.setUseOriginalCopyBehavior(settings.useOriginalCopyBehavior);
}
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (settings?.overwriteWordBehavior) {
await this.setOverwriteWordBehavior(settings.overwriteWordBehavior);
}
await vscode.commands.executeCommand(cmd);
}
async getWordAtPosition(position: vscode.Position, timeout = 500) {
// wait paste finish
await new Promise((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error(`Promise timed out after ${timeout} ms`));
}, timeout);
const dispose = vscode.workspace.onDidChangeTextDocument((event) => {
clearTimeout(timer);
resolve(event);
dispose.dispose();
});
});
const textRange = this.doc.getWordRangeAtPosition(position);
return this.doc.getText(textRange);
}
getLineAtPosition(position: vscode.Position) {
return this.doc.lineAt(position.line).text;
}
async setClipboard(value: string) {
await vscode.env.clipboard.writeText(value);
}

private api(): Omit<Public<CommandTestHelper>, 'open'> {
const that = this as CommandTestHelper;
return {
/* eslint-disable @typescript-eslint/no-unsafe-assignment*/
doc: that.doc,
rangeOfWord: that.rangeOfWord.bind(that),
putCursorAtWord: that.putCursorAtWord.bind(that),
get useOriginalCopyBehavior() { return that.useOriginalCopyBehavior; },
async setUseOriginalCopyBehavior(value: boolean) {
await that.setUseOriginalCopyBehavior(value);
},
get overwriteWordBehavior() { return that.overwriteWordBehavior; },
async setOverwriteWordBehavior(value: boolean) {
await that.setUseOriginalCopyBehavior(value);
},
runCommand: that.runCommand.bind(that),
copyWord: that.copyWord.bind(that),
getWordAtPosition: that.getWordAtPosition.bind(that),
getLineAtPosition: that.getLineAtPosition.bind(that),
setClipboard: that.setClipboard.bind(that)
/* eslint-enable @typescript-eslint/no-unsafe-assignment*/
};
}
}

suite('Copy Command Test Suite', () => {

const originalValue = {};
suiteSetup(async () => await setupTestSuite(originalValue));
suiteTeardown(async () => await teardownTestSuite(originalValue));
suiteTeardown(async () => {
await teardownTestSuite(originalValue);
await CommandTestHelper.clean();
});

test('can copy word', async () => {
// opens a file
Expand All @@ -23,7 +187,7 @@ suite('Copy Command Test Suite', () => {
// put the cursor at the `thank` word
const sel = new vscode.Selection(new vscode.Position(2, 16), new vscode.Position(2, 16));
vscode.window.activeTextEditor.selection = sel;

// runs the command
await vscode.commands.executeCommand('copy-word.copy');

Expand All @@ -45,7 +209,7 @@ suite('Copy Command Test Suite', () => {
// put the cursor at the `thank` word
const sel = new vscode.Selection(new vscode.Position(2, 16), new vscode.Position(2, 18));
vscode.window.activeTextEditor.selection = sel;

// runs the command
await vscode.commands.executeCommand('copy-word.copy');

Expand Down Expand Up @@ -80,7 +244,7 @@ suite('Copy Command Test Suite', () => {
await vscode.workspace.getConfiguration('copyWord').update('useOriginalCopyBehavior', true);
await vscode.commands.executeCommand('copy-word.copy');
await vscode.workspace.getConfiguration('copyWord').update('useOriginalCopyBehavior', false);

// get the newly selected text
const textInClipboard = await vscode.env.clipboard.readText();
const lineText = vscode.window.activeTextEditor.document.lineAt(2).text;
Expand All @@ -98,7 +262,7 @@ suite('Copy Command Test Suite', () => {
// put the cursor at an empty line
const sel = new vscode.Selection(new vscode.Position(3, 0), new vscode.Position(3, 0));
vscode.window.activeTextEditor.selection = sel;

// runs the command
await vscode.commands.executeCommand('copy-word.copy');

Expand All @@ -117,12 +281,64 @@ suite('Copy Command Test Suite', () => {

const mock = sinon.mock(vscode.window);
const expectation = mock.expects("showInformationMessage");

// runs the command
await vscode.commands.executeCommand('copy-word.copy');

mock.restore();

assert(expectation.calledOnce);
});

test('will override text when cursor in word sides (when "overwriteWordBehavior" is false)', async () => {
const helper = new CommandTestHelper();
await helper.setClipboard('thank');
const tests: [Parameters<CommandTestHelper['putCursorAtWord']>[1], string][] = [
['start', 'thank'],
['end', 'thank'],
['middle', 'thank']
];
for (const [position, result] of tests) {
// opens a file
const { putCursorAtWord, runCommand, rangeOfWord, getWordAtPosition, getLineAtPosition } = await helper.open();
const you = rangeOfWord('you');
// put the cursor at the left/right/middle side of `you` word
putCursorAtWord(you, position);

// runs the command, paste `thank` word, (with the required setting)
await runCommand('copy-word.paste', { overwriteWordBehavior: false });

// get the text at the position
const text = await getWordAtPosition(you.start);

// should all be `thank`
assert.strictEqual(text, result, `paste at ${position} side\n\t${getLineAtPosition(you.start)}`);
}
});

test('will not override text when cursor in word sides (when "overwriteWordBehavior" is true)', async () => {
const helper = new CommandTestHelper();
await helper.setClipboard('thank');
const tests: [Parameters<CommandTestHelper['putCursorAtWord']>[1], string][] = [
['start', 'thankyou'],
['end', 'youthank'],
['middle', 'thank']
];
for (const [position, result] of tests) {
// opens a file
const { putCursorAtWord, runCommand, rangeOfWord, getWordAtPosition, getLineAtPosition } = await helper.open();
const you = rangeOfWord('you');
// put the cursor at the left/right/middle side of `you` word
putCursorAtWord(you, position);

// runs the command, paste `thank` word, (with the required setting)
await runCommand('copy-word.paste', { overwriteWordBehavior: true });

// get the text at the position
const text = await getWordAtPosition(you.start);

// should all be `thank` only when middle
assert.strictEqual(text, result, `paste at ${position} side\n\t${getLineAtPosition(you.start)}`);
}
});
});

0 comments on commit 1d7e615

Please sign in to comment.