Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add configuration to support overwrite word only when cursor is in middle of word #64

Merged
merged 1 commit into from Aug 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
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
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
@@ -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
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
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
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)}`);
}
});
});