diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..27461a0 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,16 @@ +# Change Log + + + +## [0.1.1] +### Added + +- Added a decorator which shows the links to Zotero on hovering the citerefs. +- Added commands which can be accessed using the command palette and bound with a keybinding. +- Configuration options to enable & disable CodeLens or Decorator + +## [0.1.1] +### Added +- Initial release +- CodeLens to open PDF's based on citekey +- CodeLens to select item in Zotero based on citekey \ No newline at end of file diff --git a/package.json b/package.json index 1228e09..41bef44 100644 --- a/package.json +++ b/package.json @@ -18,20 +18,59 @@ "Other" ], "activationEvents": [ - "onLanguage:markdown" + "onLanguage:markdown", + "onCommand:zoterolens.showInZotero", + "onCommand:zoterolens.openZoteroPDF" ], "main": "./out/extension.js", "contributes": { "commands": [ { "command": "zoterolens.showInZotero", - "title": "Select item in Zotero by citeref" + "title": "Open Zotero item by citeref" }, { "command": "zoterolens.openZoteroPDF", - "title": "Open PDF attached in Zotero by citeref." + "title": "Open Zotero PDF attachment by citeref." } - ] + ], + "menus": { + "commandPalette": [ + { + "command": "zoterolens.showInZotero", + "when": "editorLangId == markdown" + }, + { + "command": "zoterolens.openZoteroPDF", + "when": "editorLangId == markdown" + } + ] + }, + "configuration": { + "title": "ZoteroLens", + "properties": { + "zoterolens.lens.enabled": { + "type": "boolean", + "scope": "resource", + "default": true, + "markdownDescription": "Show references in a 'lens', an additional line above the paragraph.", + "examples": [ + true, + false + ] + }, + "zoterolens.decoration.enabled": { + "type": "boolean", + "scope": "resource", + "default": true, + "markdownDescription": "Show links to @cite-key references in a hover over the @cite-key.", + "examples": [ + true, + false + ] + } + } + } }, "scripts": { "vscode:prepublish": "npm run compile", @@ -54,4 +93,4 @@ "mocha": "^9.1.3", "typescript": "^4.5.4" } -} +} \ No newline at end of file diff --git a/src/commands.ts b/src/commands.ts index 34c494f..7b37957 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -1,13 +1,40 @@ import { exec } from "child_process"; import { IncomingMessage, request } from "http"; -import { Reference } from "./zoteroCodeLensProvider"; +import { window } from "vscode"; +import { findReferences, Reference } from "./reference"; - -async function showInZotero(reference: Reference) { - openUrl("zotero://select/items/@" + reference.citekey); +// when called as command using keyboard, reference argument is undefined +// in that case, try to find one under the current cursor. +function resolveReferenceArgument(reference: Reference | undefined): Reference|undefined { + if (reference === undefined) { + const editor = window.activeTextEditor; + if (!editor) { + console.log("No active editor to run command"); + return; + } + const position = editor.selection.active; + const referencesAtCursor = findReferences(editor.document).filter(ref => ref.range.contains(position)); + if (referencesAtCursor.length < 1) { + window.showWarningMessage("No reference at cursor position."); + return; + } + return referencesAtCursor[0]; + } + return reference; } -async function openZoteroPDF(reference: Reference) { +async function showInZotero(reference?: Reference) { + const ref = resolveReferenceArgument(reference); + if (!ref) { return; } + openUrl("zotero://select/items/@" + ref.citekey); +} + +async function openZoteroPDF(reference?: Reference) { + const ref = resolveReferenceArgument(reference); + if (!ref) { + return; + } + const req = request("http://127.0.0.1:23119/better-bibtex/json-rpc", { 'method': 'POST', 'headers': { @@ -30,7 +57,7 @@ async function openZoteroPDF(reference: Reference) { if (result['path'].toLowerCase().endsWith('.pdf')) { // TODO add ?page=nr to open given page - const pagenr = reference.pagenr !== null ? "?page=" + reference.pagenr : ""; + const pagenr = ref.pagenr !== null ? "?page=" + ref.pagenr : ""; openUrl(result['open'] + pagenr); break; // only open a single attachement } @@ -39,7 +66,7 @@ async function openZoteroPDF(reference: Reference) { } }); - req.end('{"jsonrpc": "2.0", "method": "item.attachments", "params": ["' + reference.citekey + '"]}', 'binary'); + req.end('{"jsonrpc": "2.0", "method": "item.attachments", "params": ["' + ref.citekey + '"]}', 'binary'); } function openUrl(url: string) { diff --git a/src/decorations.ts b/src/decorations.ts new file mode 100644 index 0000000..33e2d02 --- /dev/null +++ b/src/decorations.ts @@ -0,0 +1,85 @@ +import { window, OverviewRulerLane, DecorationOptions, Uri, MarkdownString, Range, workspace, ExtensionContext } from "vscode"; +import { findReferences } from "./reference"; + +let timeout: NodeJS.Timer | undefined = undefined; + +// create a decorator type that we use to decorate small numbers +const referenceDecorationType = window.createTextEditorDecorationType({ + borderWidth: '2px', + borderStyle: 'none none solid none', + overviewRulerColor: 'blue', + overviewRulerLane: OverviewRulerLane.Right, + light: { + // this color will be used in light color themes + borderColor: 'darkblue' + }, + dark: { + // this color will be used in dark color themes + borderColor: 'lightblue' + } +}); + + +let activeEditor = window.activeTextEditor; + +function updateDecorations() { + if (!activeEditor) { + return; + } + const regEx = /\d+/g; + const text = activeEditor.document.getText(); + const decorations: DecorationOptions[] = []; + + const references = findReferences(activeEditor.document); + + for(let reference of references) { + const uriArguments = `?${encodeURIComponent(JSON.stringify([reference]))}`; + const pdfCommandUri = Uri.parse(`command:zoterolens.openZoteroPDF`+uriArguments); + const viewCommandUri = Uri.parse(`command:zoterolens.showInZotero`+uriArguments); + const contents = new MarkdownString(`${reference.citekey}\n\n[View in Zotero](${viewCommandUri}) | [Open PDF](${pdfCommandUri})`); + + // To enable command URIs in Markdown content, you must set the `isTrusted` flag. + // When creating trusted Markdown string, make sure to properly sanitize all the + // input content so that only expected command URIs can be executed + contents.isTrusted = true; + + const decoration = { range: reference.range, hoverMessage: contents }; + decorations.push(decoration); + } + + activeEditor.setDecorations(referenceDecorationType, decorations); +} + +function triggerUpdateDecorations(throttle = false) { + if (timeout) { + clearTimeout(timeout); + timeout = undefined; + } + if (throttle) { + timeout = setTimeout(updateDecorations, 500); + } else { + updateDecorations(); + } +} + +export function activateDecorations(context: ExtensionContext) { + + if (activeEditor) { + triggerUpdateDecorations(); + } + + window.onDidChangeActiveTextEditor(editor => { + activeEditor = editor; + if (editor) { + triggerUpdateDecorations(); + } + }, null, context.subscriptions); + + workspace.onDidChangeTextDocument(event => { + console.log(event.contentChanges); + if (activeEditor && event.document === activeEditor.document) { + triggerUpdateDecorations(true); + } + }, null, context.subscriptions); + +} \ No newline at end of file diff --git a/src/extension.ts b/src/extension.ts index c21930e..a84d3b6 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,6 +1,7 @@ -import { commands, ExtensionContext, languages } from "vscode"; +import { commands, ExtensionContext, languages, HoverProvider, TextDocument, CancellationToken, Hover, MarkdownString, Position, ProviderResult, Uri, DecorationOptions, OverviewRulerLane, Range, window, workspace, WorkspaceConfiguration } from "vscode"; import { ZoteroCodeLensProvider } from "./zoteroCodeLensProvider"; import { openZoteroPDF, showInZotero } from "./commands"; +import { activateDecorations } from "./decorations"; export function activate(context: ExtensionContext) { // Register the command @@ -21,17 +22,27 @@ export function activate(context: ExtensionContext) { scheme: "file" }; - // Register our CodeLens provider - let codeLensProviderDisposable = languages.registerCodeLensProvider( - docSelector, - new ZoteroCodeLensProvider() - ); - // Push the command and CodeLens provider to the context so it can be disposed of later - context.subscriptions.push(commandDisposable2); - context.subscriptions.push(commandDisposable3); - context.subscriptions.push(codeLensProviderDisposable); + const workspaceConfig: WorkspaceConfiguration = workspace.getConfiguration('zoterolens'); + const enabledLens: any = workspaceConfig.get('lens.enabled'); + const enabledDecoration: any = workspaceConfig.get('decoration.enabled'); + + // Register our CodeLens provider + if (enabledLens) { + let codeLensProviderDisposable = languages.registerCodeLensProvider( + docSelector, + new ZoteroCodeLensProvider() + ); + + // Push the command and CodeLens provider to the context so it can be disposed of later + context.subscriptions.push(commandDisposable2); + context.subscriptions.push(commandDisposable3); + context.subscriptions.push(codeLensProviderDisposable); + } + if (enabledDecoration) { + activateDecorations(context); + } } -export function deactivate() {} \ No newline at end of file +export function deactivate() { } \ No newline at end of file diff --git a/src/reference.ts b/src/reference.ts new file mode 100644 index 0000000..128ef71 --- /dev/null +++ b/src/reference.ts @@ -0,0 +1,41 @@ +import { + CodeLensProvider, + TextDocument, + CodeLens, + Range, + Command +} from "vscode"; + +export interface Reference { + document: TextDocument; + citekey: string; + pagenr: number | null; + range: Range; +} + +// TODO match citations in brackets [@citation] inline @citatinon, +// but also page number in [see @citation p.23] and +// inlince @citation [p.23] (so brackets after inline) +// TODO possibly use https://github.com/martinring/markdown-it-citations/blob/ba82a511de047a2438b4ac060c4c71b5a5c82da9/src/index.ts#L43 +export function findReferences(document: TextDocument): Reference[] { + const matches: Reference[] = []; + for (let lineNr = 0; lineNr < document.lineCount; lineNr++) { + const line = document.lineAt(lineNr); + let match: RegExpExecArray | null; + let regex = /(?<=@)([\w\.]+)[, ]*\[?(?:[p]{0,2}\.)?(\d+)?(?:-+\d+)?\]?/g; + regex.lastIndex = 0; + const text = line.text;//.substring(0, 1000); + while ((match = regex.exec(text))) { + const result = { + document: document, + citekey: match[1], + pagenr: match[2] ? parseInt(match[2]) : null, + range: new Range(lineNr, match.index, lineNr, match.index + match[0].length) + } as Reference; + // if (result) { + matches.push(result); + // } + } + } + return matches; +} \ No newline at end of file diff --git a/src/zoteroCodeLensProvider.ts b/src/zoteroCodeLensProvider.ts index 0b63773..971e2a6 100644 --- a/src/zoteroCodeLensProvider.ts +++ b/src/zoteroCodeLensProvider.ts @@ -6,21 +6,16 @@ import { Range, Command } from "vscode"; - -export interface Reference { - document: TextDocument; - citekey: string; - pagenr: number | null; - range: Range; -} +import { findReferences } from "./reference"; export class ZoteroCodeLensProvider implements CodeLensProvider { // Each provider requires a provideCodeLenses function which will give the various documents // the code lenses async provideCodeLenses(document: TextDocument): Promise { - const references = this.findReferences(document); + const references = findReferences(document); + // some sorting and filtering to have a nicer lens return references // sort by line, citekey, pagenr .sort((a, b) => { @@ -96,31 +91,6 @@ export class ZoteroCodeLensProvider implements CodeLensProvider { // return [codeLens]; } - // TODO match citations in brackets [@citation] inline @citatinon, - // but also page number in [see @citation p.23] and - // inlince @citation [p.23] (so brackets after inline) - // TODO possibly use https://github.com/martinring/markdown-it-citations/blob/ba82a511de047a2438b4ac060c4c71b5a5c82da9/src/index.ts#L43 - findReferences(document: TextDocument): Reference[] { - const matches: Reference[] = []; - for (let lineNr = 0; lineNr < document.lineCount; lineNr++) { - const line = document.lineAt(lineNr); - let match: RegExpExecArray | null; - let regex = /(?<=@)([\w\.]+)[, ]*\[?(?:[p]{0,2}\.)?(\d+)?(?:-+\d+)?\]?/g; - regex.lastIndex = 0; - const text = line.text;//.substring(0, 1000); - while ((match = regex.exec(text))) { - const result = { - document: document, - citekey: match[1], - pagenr: match[2] ? parseInt(match[2]) : null, - range: new Range(lineNr, match.index, lineNr, match.index + match[0].length) - } as Reference; - // if (result) { - matches.push(result); - // } - } - } - return matches; - } + }