Skip to content

Commit

Permalink
feat: Add "Extract range to a new note" command
Browse files Browse the repository at this point in the history
  • Loading branch information
svsool committed Dec 18, 2021
1 parent 501b48b commit da3074a
Show file tree
Hide file tree
Showing 13 changed files with 312 additions and 0 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ If you want to try out Memo just install it via marketplace using [this link](ht

- "Rename Symbol" command support for renaming links right in the editor

- "Extract range to a new note" command to ease notes refactoring

## FAQ

- [Memo vs Foam](https:/svsool/vscode-memo/issues/9#issuecomment-658346216)
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions help/Features/Commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,7 @@
### `Rename Symbol` command which allows you to rename links right in the editor

![[Automatic link synchronization 2.gif]]

### `Extract range to a new note` command to ease notes refactoring

![[Extracting range to a new note.gif]]
1 change: 1 addition & 0 deletions help/How to/How to.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@

- [[Pasting images from clipboard]]
- [[Pasting HTML as Markdown]]
- [[Notes refactoring]]

Continue to [[Features]].
7 changes: 7 additions & 0 deletions help/How to/Notes refactoring.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Notes refactoring

## Extract range to a new note

Select the following fragment, hit `cmd/ctrl + .` and select `Extract range to a new note` [code action](https://code.visualstudio.com/docs/editor/refactoring#_code-actions-quick-fixes-and-refactorings).

![[Extracting range to a new note.gif]]
10 changes: 10 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"onCommand:memo.openReferenceInDefaultApp",
"onCommand:memo.openReferenceBeside",
"onCommand:memo.pasteHtmlAsMarkdown",
"onCommand:memo.extractRangeToNewNote",
"onCommand:_memo.openReference",
"onCommand:_memo.cacheWorkspace",
"onCommand:_memo.cleanWorkspaceCache",
Expand Down Expand Up @@ -76,6 +77,11 @@
"command": "memo.pasteHtmlAsMarkdown",
"title": "Paste HTML as Markdown",
"category": "Memo"
},
{
"command": "memo.extractRangeToNewNote",
"title": "Extract range to a new note",
"category": "Memo"
}
],
"configuration": {
Expand Down Expand Up @@ -198,6 +204,10 @@
{
"command": "memo.openReferenceBeside",
"when": "editorLangId == markdown && memo:refFocusedOrHovered"
},
{
"command": "memo.extractRangeToNewNote",
"when": "editorHasSelection && editorLangId == markdown"
}
]
},
Expand Down
2 changes: 2 additions & 0 deletions src/commands/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import openReferenceInDefaultApp from './openReferenceInDefaultApp';
import openReferenceBeside from './openReferenceBeside';
import openDailyNote from './openDailyNote';
import pasteHtmlAsMarkdown from './pasteHtmlAsMarkdown';
import extractRangeToNewNote from './extractRangeToNewNote';
import { cacheWorkspace, cleanWorkspaceCache, getWorkspaceCache } from '../utils';

const commands = [
Expand All @@ -17,6 +18,7 @@ const commands = [
vscode.commands.registerCommand('memo.openDailyNote', openDailyNote),
vscode.commands.registerCommand('memo.openReferenceInDefaultApp', openReferenceInDefaultApp),
vscode.commands.registerCommand('memo.openReferenceBeside', openReferenceBeside),
vscode.commands.registerCommand('memo.extractRangeToNewNote', extractRangeToNewNote),
vscode.commands.registerCommand('memo.pasteHtmlAsMarkdown', pasteHtmlAsMarkdown),
];

Expand Down
163 changes: 163 additions & 0 deletions src/commands/extractRangeToNewNote.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import vscode, { window } from 'vscode';
import path from 'path';

import extractRangeToNewNote from './extractRangeToNewNote';
import { getWorkspaceFolder } from '../utils';
import {
closeEditorsAndCleanWorkspace,
rndName,
createFile,
openTextDocument,
} from '../test/testUtils';

describe('extractRangeToNewNote command', () => {
beforeEach(closeEditorsAndCleanWorkspace);

afterEach(closeEditorsAndCleanWorkspace);

it('should extract range to a new note', async () => {
const name0 = rndName();
const name1 = rndName();

await createFile(`${name0}.md`, 'Hello world.');

const doc = await openTextDocument(`${name0}.md`);

const targetPathInputBoxSpy = jest.spyOn(vscode.window, 'showInputBox');

targetPathInputBoxSpy.mockReturnValue(
Promise.resolve(path.join(getWorkspaceFolder()!, `${name1}.md`)),
);

await extractRangeToNewNote(doc, new vscode.Range(0, 0, 0, 12));

expect(await doc.getText()).toBe('');

const newDoc = await openTextDocument(`${name1}.md`);

expect(await newDoc.getText()).toBe('Hello world.');

targetPathInputBoxSpy.mockRestore();
});

it('should extract a multiline range to a new note', async () => {
const name0 = rndName();
const name1 = rndName();

await createFile(
`${name0}.md`,
`Multiline
Hello world.`,
);

const doc = await openTextDocument(`${name0}.md`);

const targetPathInputBoxSpy = jest.spyOn(vscode.window, 'showInputBox');

targetPathInputBoxSpy.mockReturnValue(
Promise.resolve(path.join(getWorkspaceFolder()!, `${name1}.md`)),
);

await extractRangeToNewNote(doc, new vscode.Range(0, 0, 1, 16));

expect(await doc.getText()).toBe('');

const newDoc = await openTextDocument(`${name1}.md`);

expect(await newDoc.getText()).toMatchInlineSnapshot(`
"Multiline
Hello world."
`);

targetPathInputBoxSpy.mockRestore();
});

it('should extract range from active markdown file', async () => {
const name0 = rndName();
const name1 = rndName();

await createFile(`${name0}.md`, 'Hello world.');

const doc = await openTextDocument(`${name0}.md`);
const editor = await window.showTextDocument(doc);

editor.selection = new vscode.Selection(0, 0, 0, 12);

const targetPathInputBoxSpy = jest.spyOn(vscode.window, 'showInputBox');

targetPathInputBoxSpy.mockReturnValue(
Promise.resolve(path.join(getWorkspaceFolder()!, `${name1}.md`)),
);

await extractRangeToNewNote();

expect(await doc.getText()).toBe('');

const newDoc = await openTextDocument(`${name1}.md`);

expect(await newDoc.getText()).toBe('Hello world.');

targetPathInputBoxSpy.mockRestore();
});

it('should not extract anything from unknown file format', async () => {
const name0 = rndName();

await createFile(`${name0}.txt`, 'Hello world.');

const doc = await openTextDocument(`${name0}.txt`);
const editor = await window.showTextDocument(doc);

editor.selection = new vscode.Selection(0, 0, 0, 12);

const targetPathInputBoxSpy = jest.spyOn(vscode.window, 'showInputBox');

await extractRangeToNewNote();

expect(await doc.getText()).toBe('Hello world.');

expect(targetPathInputBoxSpy).not.toBeCalled();

targetPathInputBoxSpy.mockRestore();
});

it('should fail when target path is outside of the workspace', async () => {
const name0 = rndName();

await createFile(`${name0}.md`, 'Hello world.');

const doc = await openTextDocument(`${name0}.md`);

const targetPathInputBoxSpy = jest.spyOn(vscode.window, 'showInputBox');

targetPathInputBoxSpy.mockReturnValue(Promise.resolve('/random-path/file.md'));

expect(extractRangeToNewNote(doc, new vscode.Range(0, 0, 0, 12))).rejects.toThrowError(
'should be within the current workspace',
);

targetPathInputBoxSpy.mockRestore();
});

it('should fail when entered file already exists', async () => {
const name0 = rndName();
const name1 = rndName();

await createFile(`${name0}.md`, 'Hello world.');
await createFile(`${name1}.md`);

const doc = await openTextDocument(`${name0}.md`);

const targetPathInputBoxSpy = jest.spyOn(vscode.window, 'showInputBox');

targetPathInputBoxSpy.mockReturnValue(
Promise.resolve(path.join(getWorkspaceFolder()!, `${name1}.md`)),
);

expect(extractRangeToNewNote(doc, new vscode.Range(0, 0, 0, 12))).rejects.toThrowError(
'Such file or directory already exists. Please use unique filename instead.',
);

targetPathInputBoxSpy.mockRestore();
});
});
76 changes: 76 additions & 0 deletions src/commands/extractRangeToNewNote.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import vscode, { Uri, window } from 'vscode';
import fs from 'fs-extra';
import path from 'path';

const filename = 'New File.md';

const prompt = 'New location within workspace';

const createFile = async (uri: vscode.Uri, content: string) => {
const workspaceEdit = new vscode.WorkspaceEdit();
workspaceEdit.createFile(uri);
workspaceEdit.set(uri, [new vscode.TextEdit(new vscode.Range(0, 0, 0, 0), content)]);

await vscode.workspace.applyEdit(workspaceEdit);
};

const showFile = async (uri: vscode.Uri) =>
await window.showTextDocument(await vscode.workspace.openTextDocument(uri));

const deleteRange = async (document: vscode.TextDocument, range: vscode.Range) => {
const editor = await window.showTextDocument(document);
await editor.edit((edit) => edit.delete(range));
};

const extractRangeToNewNote = async (
documentParam?: vscode.TextDocument,
rangeParam?: vscode.Range,
) => {
const document = documentParam ? documentParam : window.activeTextEditor?.document;

if (!document || (document && document.languageId !== 'markdown')) {
return;
}

const range = rangeParam ? rangeParam : window.activeTextEditor?.selection;

if (!range || (range && range.isEmpty)) {
return;
}

const filepath = path.join(path.dirname(document.uri.fsPath), filename);
const targetPath = await window.showInputBox({
prompt,
value: filepath,
valueSelection: [filepath.lastIndexOf(filename), filepath.lastIndexOf('.md')],
});

const targetUri = Uri.file(targetPath || '');

const workspaceFolder = vscode.workspace.getWorkspaceFolder(document.uri);

if (!targetPath) {
return;
}

if (!vscode.workspace.getWorkspaceFolder(targetUri)) {
throw new Error(
`New location "${targetUri.fsPath}" should be within the current workspace.${
workspaceFolder ? ` Example: ${path.join(workspaceFolder.uri.fsPath, filename)}` : ''
}`,
);
}

if (await fs.pathExists(targetUri.fsPath)) {
throw new Error('Such file or directory already exists. Please use unique filename instead.');
}

// Order matters
await createFile(targetUri, document.getText(range).trim());

await deleteRange(document, range);

await showFile(targetUri);
};

export default extractRangeToNewNote;
2 changes: 2 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
BacklinksTreeDataProvider,
extendMarkdownIt,
newVersionNotifier,
codeActionProvider,
} from './features';
import commands from './commands';
import { cacheWorkspace, getMemoConfigProperty, MemoBoolConfigProp, isDefined } from './utils';
Expand Down Expand Up @@ -40,6 +41,7 @@ export const activate = async (

context.subscriptions.push(
...commands,
vscode.languages.registerCodeActionsProvider(mdLangSelector, codeActionProvider),
vscode.workspace.onDidChangeConfiguration(async (configChangeEvent) => {
if (configChangeEvent.affectsConfiguration('search.exclude')) {
await cacheWorkspace();
Expand Down
25 changes: 25 additions & 0 deletions src/features/codeActionProvider.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import vscode from 'vscode';

import codeActionProvider from './codeActionProvider';
import { rndName, createFile, openTextDocument } from '../test/testUtils';

describe('codeActionProvider', () => {
it('should provide code actions', async () => {
const name0 = rndName();

await createFile(`${name0}.md`, 'Hello world!');

const doc = await openTextDocument(`${name0}.md`);
const range = new vscode.Range(0, 0, 0, 12);

expect(
codeActionProvider.provideCodeActions(doc, range, undefined as any, undefined as any),
).toEqual([
{
title: 'Extract range to a new note',
command: 'memo.extractRangeToNewNote',
arguments: [doc, range],
},
]);
});
});
19 changes: 19 additions & 0 deletions src/features/codeActionProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { CodeActionProvider } from 'vscode';

const codeActionProvider: CodeActionProvider = {
provideCodeActions(document, range) {
if (range.isEmpty) {
return [];
}

return [
{
title: 'Extract range to a new note',
command: 'memo.extractRangeToNewNote',
arguments: [document, range],
},
];
},
};

export default codeActionProvider;
1 change: 1 addition & 0 deletions src/features/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export { default as ReferenceProvider } from './ReferenceProvider';
export { default as ReferenceRenameProvider } from './ReferenceRenameProvider';
export { default as BacklinksTreeDataProvider } from './BacklinksTreeDataProvider';
export { default as extendMarkdownIt } from './extendMarkdownIt';
export { default as codeActionProvider } from './codeActionProvider';
export * as fsWatcher from './fsWatcher';
export * as referenceContextWatcher from './referenceContextWatcher';
export * as syntaxDecorations from './syntaxDecorations';
Expand Down

0 comments on commit da3074a

Please sign in to comment.