Decoration and command

This commit is contained in:
Ruben van de Ven 2022-09-27 14:06:30 +02:00
parent d3452351e4
commit 55c1c6e1ff
7 changed files with 246 additions and 57 deletions

16
CHANGELOG.md Normal file
View File

@ -0,0 +1,16 @@
# Change Log
<!-- Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. -->
## [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

View File

@ -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"
}
}
}

View File

@ -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) {

85
src/decorations.ts Normal file
View File

@ -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);
}

View File

@ -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() {}
export function deactivate() { }

41
src/reference.ts Normal file
View File

@ -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;
}

View File

@ -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<CodeLens[]> {
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;
}
}