Decoration and command
This commit is contained in:
parent
d3452351e4
commit
55c1c6e1ff
7 changed files with 246 additions and 57 deletions
16
CHANGELOG.md
Normal file
16
CHANGELOG.md
Normal 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
|
49
package.json
49
package.json
|
@ -18,20 +18,59 @@
|
||||||
"Other"
|
"Other"
|
||||||
],
|
],
|
||||||
"activationEvents": [
|
"activationEvents": [
|
||||||
"onLanguage:markdown"
|
"onLanguage:markdown",
|
||||||
|
"onCommand:zoterolens.showInZotero",
|
||||||
|
"onCommand:zoterolens.openZoteroPDF"
|
||||||
],
|
],
|
||||||
"main": "./out/extension.js",
|
"main": "./out/extension.js",
|
||||||
"contributes": {
|
"contributes": {
|
||||||
"commands": [
|
"commands": [
|
||||||
{
|
{
|
||||||
"command": "zoterolens.showInZotero",
|
"command": "zoterolens.showInZotero",
|
||||||
"title": "Select item in Zotero by citeref"
|
"title": "Open Zotero item by citeref"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"command": "zoterolens.openZoteroPDF",
|
"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": {
|
"scripts": {
|
||||||
"vscode:prepublish": "npm run compile",
|
"vscode:prepublish": "npm run compile",
|
||||||
|
@ -54,4 +93,4 @@
|
||||||
"mocha": "^9.1.3",
|
"mocha": "^9.1.3",
|
||||||
"typescript": "^4.5.4"
|
"typescript": "^4.5.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,13 +1,40 @@
|
||||||
import { exec } from "child_process";
|
import { exec } from "child_process";
|
||||||
import { IncomingMessage, request } from "http";
|
import { IncomingMessage, request } from "http";
|
||||||
import { Reference } from "./zoteroCodeLensProvider";
|
import { window } from "vscode";
|
||||||
|
import { findReferences, Reference } from "./reference";
|
||||||
|
|
||||||
|
// when called as command using keyboard, reference argument is undefined
|
||||||
async function showInZotero(reference: Reference) {
|
// in that case, try to find one under the current cursor.
|
||||||
openUrl("zotero://select/items/@" + reference.citekey);
|
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", {
|
const req = request("http://127.0.0.1:23119/better-bibtex/json-rpc", {
|
||||||
'method': 'POST',
|
'method': 'POST',
|
||||||
'headers': {
|
'headers': {
|
||||||
|
@ -30,7 +57,7 @@ async function openZoteroPDF(reference: Reference) {
|
||||||
if (result['path'].toLowerCase().endsWith('.pdf')) {
|
if (result['path'].toLowerCase().endsWith('.pdf')) {
|
||||||
// TODO add ?page=nr to open given page
|
// 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);
|
openUrl(result['open'] + pagenr);
|
||||||
break; // only open a single attachement
|
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) {
|
function openUrl(url: string) {
|
||||||
|
|
85
src/decorations.ts
Normal file
85
src/decorations.ts
Normal 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);
|
||||||
|
|
||||||
|
}
|
|
@ -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 { ZoteroCodeLensProvider } from "./zoteroCodeLensProvider";
|
||||||
import { openZoteroPDF, showInZotero } from "./commands";
|
import { openZoteroPDF, showInZotero } from "./commands";
|
||||||
|
import { activateDecorations } from "./decorations";
|
||||||
|
|
||||||
export function activate(context: ExtensionContext) {
|
export function activate(context: ExtensionContext) {
|
||||||
// Register the command
|
// Register the command
|
||||||
|
@ -21,17 +22,27 @@ export function activate(context: ExtensionContext) {
|
||||||
scheme: "file"
|
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
|
const workspaceConfig: WorkspaceConfiguration = workspace.getConfiguration('zoterolens');
|
||||||
context.subscriptions.push(commandDisposable2);
|
const enabledLens: any = workspaceConfig.get('lens.enabled');
|
||||||
context.subscriptions.push(commandDisposable3);
|
const enabledDecoration: any = workspaceConfig.get('decoration.enabled');
|
||||||
context.subscriptions.push(codeLensProviderDisposable);
|
|
||||||
|
// 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
41
src/reference.ts
Normal 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;
|
||||||
|
}
|
|
@ -6,21 +6,16 @@ import {
|
||||||
Range,
|
Range,
|
||||||
Command
|
Command
|
||||||
} from "vscode";
|
} from "vscode";
|
||||||
|
import { findReferences } from "./reference";
|
||||||
export interface Reference {
|
|
||||||
document: TextDocument;
|
|
||||||
citekey: string;
|
|
||||||
pagenr: number | null;
|
|
||||||
range: Range;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ZoteroCodeLensProvider implements CodeLensProvider {
|
export class ZoteroCodeLensProvider implements CodeLensProvider {
|
||||||
// Each provider requires a provideCodeLenses function which will give the various documents
|
// Each provider requires a provideCodeLenses function which will give the various documents
|
||||||
// the code lenses
|
// the code lenses
|
||||||
async provideCodeLenses(document: TextDocument): Promise<CodeLens[]> {
|
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
|
return references
|
||||||
// sort by line, citekey, pagenr
|
// sort by line, citekey, pagenr
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
|
@ -96,31 +91,6 @@ export class ZoteroCodeLensProvider implements CodeLensProvider {
|
||||||
// return [codeLens];
|
// 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue