Skip to content

Commit

Permalink
Punch to create (#7)
Browse files Browse the repository at this point in the history
  • Loading branch information
GladeDiviney committed Sep 29, 2020
1 parent a792e19 commit fdd696c
Show file tree
Hide file tree
Showing 8 changed files with 209 additions and 62 deletions.
5 changes: 3 additions & 2 deletions CHANGELOG.md
Expand Up @@ -2,8 +2,9 @@

## 1.4.10

* Ctrl+Space replacement of links improved

* Ctrl+Space replacement of links improved.
* Show link warnings (#6)
* Link command automatically creates new note if no heading yet (#7).

## 1.4.9

Expand Down
13 changes: 9 additions & 4 deletions README.md
Expand Up @@ -5,6 +5,8 @@

A [Visual Studio Code extension](https://marketplace.visualstudio.com/items?itemName=gladed.linkist) to create persistent links between markdown documents in your workspace.

It's a great way to implement a private "Zettelkasten" note taking system as described in [How to Take Smart Notes by Sönke Ahrens](https://amzn.to/2vi6Sm9).

## Install

1. Open [VSCode](https://code.visualstudio.com/) or [VSCodium](https://github.com/VSCodium/vscodium).
Expand Down Expand Up @@ -34,12 +36,15 @@ A [Visual Studio Code extension](https://marketplace.visualstudio.com/items?item

3. Put your cursor on that link and type `Ctrl+Alt+L` again to go back to the heading.

4. Type `Ctrl+Alt+L` yet again to visit/list other places the link ID appears.
4. Type `Ctrl+Alt+L` yet again to visit or list other places the link ID appears.

## Advanced Topics

* Select multiple lines (starting with a heading) and `Ctrl+Alt+L` will extract that section into a new file, replacing it with a link and backlink.
* Understand why some links create [warnings](https://github.com/gladed/linkist/wiki/Warnings).
* Create new note files automatically:
* Select multiple lines (starting with a heading) and `Ctrl+Alt+L` to extract that section into a new note
* Use `Ctrl+Alt+L` on a link with no associated heading yet
* Type `[` to auto-complete from existing links.
* Implement a "Zettelkasten" note taking system as described in [How to Take Smart Notes by Sönke Ahrens](https://amzn.to/2vi6Sm9).
* Read the [Change Log](CHANGELOG.md).
* Read [Frequently Asked Questions](https://github.com/gladed/linkist/wiki/FAQ).

See [Frequently Asked Questions](https://github.com/gladed/linkist/wiki/FAQ) for more.
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -4,7 +4,7 @@
"displayName": "linkist",
"icon": "img/icon_100.png",
"description": "Create and follow permanent links in Markdown files",
"version": "1.4.9",
"version": "1.4.10",
"repository": {
"type": "git",
"url": "https://github.com/gladed/linkist"
Expand Down
21 changes: 21 additions & 0 deletions src/editorLinkHandler.ts
Expand Up @@ -7,6 +7,7 @@ import {
Uri,
workspace,
commands,
window
} from 'vscode';
import { camelize } from './util/text';
import { TextEncoder } from 'util';
Expand Down Expand Up @@ -207,6 +208,26 @@ export class EditorLinkHandler {
return titleRange;
}

public async createNote(editor: TextEditor): Promise<Boolean> {
const link = await this.linker.linkAt(editor.document.uri, editor.selection.start);
if (!link || !link.label || link.isHead) {
return false;
}
const title = link.label!!.replace(/\b\w/g, c => c.toUpperCase());
const fileName = camelize(title);
let text = '# [' + title + '](^' + link.linkId.text + '^)\n';
if (link?.parent) {
text = text + '\nFrom: ' + link.parent.toMarkdown() + '\n';
}
const destUri = await this.createTargetFile(editor.document.uri, fileName, text);
if (!destUri) {
window.showWarningMessage("Could not create " + fileName);
return false;
}
await window.showTextDocument(destUri);
return true;
}

private async createTargetFile(near: Uri, name: string, text: string): Promise<Uri | undefined> {
let start = 0;
while (start < 10) {
Expand Down
103 changes: 54 additions & 49 deletions src/extension.ts
Expand Up @@ -43,6 +43,16 @@ export async function activate(context: ExtensionContext) {

disposer.register(new MarkdownDiagnosticHandler(languages.createDiagnosticCollection("Link diagnostics"), linker));

// Auto startup
if (window.activeTextEditor) {
linker.linksIn(window.activeTextEditor.document.uri);
} else {
disposer.register(window.onDidChangeActiveTextEditor(_ => {
if (window.activeTextEditor) {
linker.linksIn(window.activeTextEditor.document.uri);
}}));
}

// Things we don't do:

// Don't do symbol searches, markdown plugin already handles these for # lines
Expand All @@ -58,63 +68,60 @@ export async function activate(context: ExtensionContext) {
// the link tree will do a better job contextualizing the current link.
// disposer.register(languages.registerHoverProvider(markdownSelector, new MarkdownHoverProvider(linker)));

// Not ready:
function setupLinkExplorer() {
const linkExplorer = new LinkTree();

// Provide data to the explorer pane
disposer.register(window.registerTreeDataProvider('links', linkExplorer));

// When a link is selected (e.g. from LinkTree) specify the target selection range
disposer.register(commands.registerCommand('extension.openLinkSelection', (location: Location) => {
window.showTextDocument(location.uri, { selection: location.range });
}));

async function refreshLinks() {
if (window.activeTextEditor &&
window.activeTextEditor.document.uri.scheme === 'file' &&
window.activeTextEditor.document.languageId === 'markdown') {
linkExplorer.links = await linker.linksIn(window.activeTextEditor.document.uri);
}
}

// Enable/disable the explorer pane when the editor changes.
async function updateLinkExplorerVisibility() {
if (window.activeTextEditor) {
if (window.activeTextEditor.document.uri.scheme === 'file') {
const enabled = window.activeTextEditor.document.languageId === 'markdown';
if (enabled) {
refreshLinks();
}
commands.executeCommand('setContext', 'markdownLinksEnabled', enabled);
}
} else {
commands.executeCommand('setContext', 'markdownLinksEnabled', false);
}
}
disposer.register(window.onDidChangeActiveTextEditor(updateLinkExplorerVisibility));
disposer.register(workspace.onDidChangeTextDocument(e => {
if (e.document.uri.fsPath === window.activeTextEditor?.document.uri.fsPath) {
refreshLinks();
}
}));
updateLinkExplorerVisibility();
}
// Also not ready:
// function setupLinkExplorer() {
// const linkExplorer = new LinkTree();

// // Provide data to the explorer pane
// disposer.register(window.registerTreeDataProvider('links', linkExplorer));

// // When a link is selected (e.g. from LinkTree) specify the target selection range
// disposer.register(commands.registerCommand('extension.openLinkSelection', (location: Location) => {
// window.showTextDocument(location.uri, { selection: location.range });
// }));

// async function refreshLinks() {
// if (window.activeTextEditor &&
// window.activeTextEditor.document.uri.scheme === 'file' &&
// window.activeTextEditor.document.languageId === 'markdown') {
// }
// }

// // Enable/disable the explorer pane when the editor changes.
// async function updateLinkExplorerVisibility() {
// if (window.activeTextEditor) {
// if (window.activeTextEditor.document.uri.scheme === 'file') {
// const enabled = window.activeTextEditor.document.languageId === 'markdown';
// if (enabled) {
// refreshLinks();
// }
// commands.executeCommand('setContext', 'markdownLinksEnabled', enabled);
// }
// } else {
// commands.executeCommand('setContext', 'markdownLinksEnabled', false);
// }
// }
// disposer.register(window.onDidChangeActiveTextEditor(updateLinkExplorerVisibility));
// disposer.register(workspace.onDidChangeTextDocument(e => {
// if (e.document.uri.fsPath === window.activeTextEditor?.document.uri.fsPath) {
// refreshLinks();
// }
// }));
// updateLinkExplorerVisibility();
// }

async function handleLinkCommand() {
const editor = window.activeTextEditor;
if (!editor) {
return;
}

// Finish link population
await linker.linksIn(editor.document.uri);

const linkId = editorHandler.linkIdAt(editor.document, editor.selection.active);
if (linkId) {
let links = await linker.lookupLinks(linkId);

if (links.length === 2) {
if (!links.find(l => l.isHead) && await editorHandler.createNote(editor)) {
// Done already
} else if (links.length === 2) {
let jumpTo = links[0];
if (jumpTo.location.range.start.line === editor.selection.start.line) {
jumpTo = links[1];
Expand All @@ -132,8 +139,6 @@ export async function activate(context: ExtensionContext) {
} else {
commands.executeCommand("editor.action.referenceSearch.trigger");
}
} else {
window.showWarningMessage("'" + linkId + "' does not link to anything");
}
} else if (editorHandler.visitUri(editor, editor.selection)) {
// If true, request was launched so do nothing
Expand Down
19 changes: 13 additions & 6 deletions src/linker.ts
Expand Up @@ -37,18 +37,22 @@ export default class Linker extends Disposable {
});
this.refreshLinks();

// Watch everywhere for changes
// When document deleted, trash its links and refresh all affected docs
this.register(this.scanner.onDidDeleteDocument(deleted => {
const lostLinks = cache.get(deleted.fsPath)?.value;
this.invalidateLinks(lostLinks);
cache.delete(deleted.fsPath);
this.invalidateLinks(lostLinks);
this.refreshLinks();
}));
this.register(this.scanner.onDidUpdateDocument(updated => {

// When document updated, refresh its links
this.register(this.scanner.onDidUpdateDocument(async updated => {
const lostLinks = cache.get(updated.uri.fsPath)?.value;
const newLinks = this.scan(updated);
cache.set(updated.uri.fsPath, newLinks);
this.invalidateLinks(lostLinks);
cache.set(updated.uri.fsPath, this.scan(updated));
this.refreshLinks();
this.invalidateLinks(newLinks.value);
this.refreshLinks(updated.uri);
}));
return cache;
});
Expand All @@ -69,8 +73,11 @@ export default class Linker extends Disposable {
}
}

private async refreshLinks() {
private async refreshLinks(updated: Uri | undefined = undefined) {
const urisUpdated: Uri[] = [];
if (updated) {
urisUpdated.push(updated);
}
// Review all links in all files
const toAdd = new Map<string, Link[]>();
for (let links of (await this.fileMap).values()) {
Expand Down
60 changes: 60 additions & 0 deletions src/markdownDiagnosticHandler.ts
@@ -0,0 +1,60 @@
import {
Diagnostic,
DiagnosticCollection,
DiagnosticSeverity,
} from 'vscode';
import Linker from "./linker";
import { Disposable } from "./util/disposable";
import { Link } from './util/link';

class Issue extends Diagnostic {
constructor(public link: Link, message: string, severity: DiagnosticSeverity) {
super(link.range, message, severity);
}
}

export class MarkdownDiagnosticHandler extends Disposable {

constructor(diagnostics: DiagnosticCollection, linker: Linker) {
super();
this.register(linker.onUpdatedLinks(async uri => {
// First: trash ALL diagnostics for this link text.
const issues: Issue[] = [];
for (let link of (await linker.linksIn(uri)) ?? []) {
const links = linker.linksFor(link.linkId.text) ?? [];
this.checkMultihead(issues, links, link) &&
this.checkHeadless(issues, links, link) &&
this.checkWeak(issues, links, link);
}
diagnostics.set(uri, issues);
}));
}

private checkMultihead(issues: Issue[], links: Link[], link: Link): Boolean {
if (links?.filter(l => l.isHead).length > 1) {
issues.push(new Issue(link, "Multiple # heads for this link", DiagnosticSeverity.Error));
return false;
}
return true;
}

private checkHeadless(issues: Issue[], links: Link[], link: Link): Boolean {
if (!link.isHead && !links?.find(l => l.isHead)) {
issues.push(new Issue(link, "No # head for this link", DiagnosticSeverity.Warning));
return false;
}
return true;
}

private checkWeak(issues: Issue[], links: Link[], link: Link): Boolean {
if (links.length < 2) {
issues.push(new Issue(link, "No links to this; make more connections", DiagnosticSeverity.Hint));
return false;
}
if (links.length < 3) {
issues.push(new Issue(link, "Only one other link to this; make more connections", DiagnosticSeverity.Hint));
return false;
}
return true;
}
}
48 changes: 48 additions & 0 deletions src/test/suite/extension.test.ts
@@ -0,0 +1,48 @@
import * as assert from 'assert';
import * as vscode from 'vscode';
import * as path from 'path';
import {
TextEditor,
Position,
Range,
Selection
} from 'vscode';

suite('link command', async () => {
const testDirectory = '../../../src/test/res/';
const uri = vscode.Uri.file(path.join(__dirname + testDirectory + 'test.md'));

async function setText(editor: TextEditor, text: string) {
await editor.edit(builder => {
builder.delete(new Range(new Position(0, 0),
editor.document.lineAt(editor.document.lineCount - 1).range.end));
builder.insert(new Position(0, 0), text);
});
}
test('link a heading', async () => {
// Open the document
const document = await vscode.workspace.openTextDocument(uri);
const editor = await vscode.window.showTextDocument(document);
await setText(editor, '# Hello\n\nSome text');
editor.selection = new Selection(new Position(0, 0), new Position(0, 0));

// Insert a link on the top line
await vscode.commands.executeCommand('linkist.link');

// Validate there's a link there
assert.ok(editor.document.lineAt(0).text.match(/\# \[Hello\]\(\^[A-Za-z0-9]{4,7}\^\)/));
});
test('link a bullet', async () => {
// Open the document
const document = await vscode.workspace.openTextDocument(uri);
const editor = await vscode.window.showTextDocument(document);
await setText(editor, '* Hello\n\nSome text');
editor.selection = new Selection(new Position(0, 0), new Position(0, 0));

// Link the line
await vscode.commands.executeCommand('linkist.link');

// Validate there's a link there
assert.ok(editor.document.lineAt(0).text.match(/\* \[Hello\]\(\^[A-Za-z0-9]{4,7}\^\)/));
});
});

0 comments on commit fdd696c

Please sign in to comment.