Implement `CodeLensProvider`.
This commit is contained in:
parent
f64b72d22b
commit
e3d5498c75
14
package.json
14
package.json
|
@ -82,6 +82,10 @@
|
||||||
"command": "texinfo.preview.show",
|
"command": "texinfo.preview.show",
|
||||||
"title": "Show preview",
|
"title": "Show preview",
|
||||||
"icon": "$(open-preview)"
|
"icon": "$(open-preview)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "texinfo.preview.goto",
|
||||||
|
"title": "Goto node in preview"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"menus": {
|
"menus": {
|
||||||
|
@ -90,6 +94,11 @@
|
||||||
"command": "texinfo.preview.show",
|
"command": "texinfo.preview.show",
|
||||||
"when": "editorLangId == texinfo",
|
"when": "editorLangId == texinfo",
|
||||||
"group": "navigation"
|
"group": "navigation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "texinfo.preview.goto",
|
||||||
|
"when": "false",
|
||||||
|
"group": "navigation"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"editor/title": [
|
"editor/title": [
|
||||||
|
@ -116,6 +125,11 @@
|
||||||
"default": "makeinfo",
|
"default": "makeinfo",
|
||||||
"markdownDescription": "Path to the `makeinfo` (or `texi2any`) command. If not located in `$PATH`, an absolute path should be specified.\n\nThe value should not contain any command-line arguments, just the filename."
|
"markdownDescription": "Path to the `makeinfo` (or `texi2any`) command. If not located in `$PATH`, an absolute path should be specified.\n\nThe value should not contain any command-line arguments, just the filename."
|
||||||
},
|
},
|
||||||
|
"texinfo.enableCodeLens": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true,
|
||||||
|
"markdownDescription": "Enable code lens on node identifiers which jumps to the corresponding nodes in preview."
|
||||||
|
},
|
||||||
"texinfo.completion.enableSnippets": {
|
"texinfo.completion.enableSnippets": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"default": true,
|
"default": true,
|
||||||
|
|
|
@ -6,17 +6,21 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
|
import { lineNumToRange } from '../utils/misc';
|
||||||
import { FoldingRange, Range, NamedLine } from '../utils/types';
|
import { FoldingRange, Range, NamedLine } from '../utils/types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores information about folding ranges for a document.
|
* Stores information about folding ranges for a document.
|
||||||
|
*
|
||||||
|
* Actually, more than folding ranges (e.g. code lens) is handled within this context, so I believe
|
||||||
|
* we should use another name...
|
||||||
*/
|
*/
|
||||||
export default class FoldingRangeContext {
|
export default class FoldingRangeContext {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Regex for matching subsection/section/chapter (-like) commands.
|
* Regex for matching subsection/section/chapter (-like) commands.
|
||||||
*/
|
*/
|
||||||
private static nodeMatcher = new RegExp('^@(?:(subsection|unnumberedsubsec|appendixsubsec|subheading)|' +
|
private static readonly nodeFormat = RegExp('^@(?:(node)|(subsection|unnumberedsubsec|appendixsubsec|subheading)|' +
|
||||||
'(section|unnumberedsec|appendixsec|heading)|(chapter|unnumbered|appendix|majorheading|chapheading)) (.*)$');
|
'(section|unnumberedsec|appendixsec|heading)|(chapter|unnumbered|appendix|majorheading|chapheading)) (.*)$');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -26,8 +30,18 @@ export default class FoldingRangeContext {
|
||||||
return this.foldingRanges ?? this.calculateFoldingRanges();
|
return this.foldingRanges ?? this.calculateFoldingRanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get node values of document as VSCode code lenses.
|
||||||
|
*/
|
||||||
|
get nodeValues() {
|
||||||
|
this.foldingRanges ?? this.calculateFoldingRanges();
|
||||||
|
return this.nodes;
|
||||||
|
}
|
||||||
|
|
||||||
private foldingRanges?: FoldingRange[];
|
private foldingRanges?: FoldingRange[];
|
||||||
|
|
||||||
|
private nodes = <vscode.CodeLens[]>[];
|
||||||
|
|
||||||
private commentRange?: Range;
|
private commentRange?: Range;
|
||||||
|
|
||||||
private headerStart?: number;
|
private headerStart?: number;
|
||||||
|
@ -50,6 +64,7 @@ export default class FoldingRangeContext {
|
||||||
// Clear cached folding range when line count changes.
|
// Clear cached folding range when line count changes.
|
||||||
if (updatedLines !== 1 || event.range.start.line !== event.range.end.line) {
|
if (updatedLines !== 1 || event.range.start.line !== event.range.end.line) {
|
||||||
this.foldingRanges = undefined;
|
this.foldingRanges = undefined;
|
||||||
|
this.nodes = [];
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -134,23 +149,32 @@ export default class FoldingRangeContext {
|
||||||
constructor(private readonly document: vscode.TextDocument) {}
|
constructor(private readonly document: vscode.TextDocument) {}
|
||||||
|
|
||||||
private processNode(lineText: string, lineNum: number, lastLineNum: number) {
|
private processNode(lineText: string, lineNum: number, lastLineNum: number) {
|
||||||
const result = lineText.match(FoldingRangeContext.nodeMatcher);
|
const result = lineText.match(FoldingRangeContext.nodeFormat);
|
||||||
if (result === null) return false;
|
if (result === null) return false;
|
||||||
// Subsection level node.
|
// Node identifier.
|
||||||
if (result[1] !== undefined) {
|
if (result[1] !== undefined) {
|
||||||
this.addRange(lineNum, this.closingSubsection ?? lastLineNum, { name: result[1], detail: result[4] });
|
this.nodes.push(new vscode.CodeLens(lineNumToRange(lineNum), {
|
||||||
|
title: '$(search-goto-file) Goto node in preview',
|
||||||
|
command: 'texinfo.preview.goto',
|
||||||
|
arguments: [this.document, result[5]],
|
||||||
|
}));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Subsection level node.
|
||||||
|
if (result[2] !== undefined) {
|
||||||
|
this.addRange(lineNum, this.closingSubsection ?? lastLineNum, { name: result[2], detail: result[5] });
|
||||||
this.closingSubsection = this.getLastTextLine(lineNum - 1);
|
this.closingSubsection = this.getLastTextLine(lineNum - 1);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Section level node.
|
// Section level node.
|
||||||
if (result[2] !== undefined) {
|
if (result[3] !== undefined) {
|
||||||
this.addRange(lineNum, this.closingSection ?? lastLineNum, { name: result[2], detail: result[4] });
|
this.addRange(lineNum, this.closingSection ?? lastLineNum, { name: result[3], detail: result[5] });
|
||||||
this.closingSubsection = this.closingSection = this.getLastTextLine(lineNum - 1);
|
this.closingSubsection = this.closingSection = this.getLastTextLine(lineNum - 1);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Chapter level node.
|
// Chapter level node.
|
||||||
if (result[3] !== undefined) {
|
if (result[4] !== undefined) {
|
||||||
this.addRange(lineNum, this.closingChapter ?? lastLineNum, { name: result[3], detail: result[4] });
|
this.addRange(lineNum, this.closingChapter ?? lastLineNum, { name: result[4], detail: result[5] });
|
||||||
this.closingSubsection = this.closingSection = this.closingChapter = this.getLastTextLine(lineNum - 1);
|
this.closingSubsection = this.closingSection = this.closingChapter = this.getLastTextLine(lineNum - 1);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -178,6 +202,7 @@ export default class FoldingRangeContext {
|
||||||
private clearTemporaries() {
|
private clearTemporaries() {
|
||||||
this.commentRange = undefined;
|
this.commentRange = undefined;
|
||||||
this.headerStart = undefined;
|
this.headerStart = undefined;
|
||||||
|
this.nodes = [];
|
||||||
this.closingSubsection = this.closingSection = this.closingChapter = undefined;
|
this.closingSubsection = this.closingSection = this.closingChapter = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ import Diagnosis from '../diagnosis';
|
||||||
import Logger from '../logger';
|
import Logger from '../logger';
|
||||||
import Options from '../options';
|
import Options from '../options';
|
||||||
import Converter from '../utils/converter';
|
import Converter from '../utils/converter';
|
||||||
import { prompt } from '../utils/misc';
|
import { getNodeHtmlRef, prompt } from '../utils/misc';
|
||||||
import { Operator, Optional } from '../utils/types';
|
import { Operator, Optional } from '../utils/types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -37,6 +37,17 @@ export default class PreviewContext {
|
||||||
ContextMapping.getDocumentContext(document).initPreview().panel.reveal();
|
ContextMapping.getDocumentContext(document).initPreview().panel.reveal();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Jump to the corresponding section of document preview by node name.
|
||||||
|
*
|
||||||
|
* @param document
|
||||||
|
* @param nodeName
|
||||||
|
*/
|
||||||
|
static gotoPreview(document: vscode.TextDocument, nodeName: string) {
|
||||||
|
ContextMapping.getDocumentContext(document).initPreview().panel.webview
|
||||||
|
.postMessage({ command: 'goto', value: getNodeHtmlRef(nodeName) });
|
||||||
|
}
|
||||||
|
|
||||||
private readonly document = this.documentContext.document;
|
private readonly document = this.documentContext.document;
|
||||||
|
|
||||||
private readonly panel: vscode.WebviewPanel;
|
private readonly panel: vscode.WebviewPanel;
|
||||||
|
@ -70,7 +81,8 @@ export default class PreviewContext {
|
||||||
this.pendingUpdate = false;
|
this.pendingUpdate = false;
|
||||||
// Inform the user that the preview is updating if `makeinfo` takes too long.
|
// Inform the user that the preview is updating if `makeinfo` takes too long.
|
||||||
setTimeout(() => this.updating && this.updateTitle(), 500);
|
setTimeout(() => this.updating && this.updateTitle(), 500);
|
||||||
const { data, error } = await new Converter(this.document.fileName, this.imageTransformer).convert();
|
const { data, error } = await new Converter(this.document.fileName, this.imageTransformer, this.script)
|
||||||
|
.convert();
|
||||||
if (error) {
|
if (error) {
|
||||||
Logger.log(error);
|
Logger.log(error);
|
||||||
Diagnosis.update(this.document, error);
|
Diagnosis.update(this.document, error);
|
||||||
|
@ -97,9 +109,7 @@ export default class PreviewContext {
|
||||||
}
|
}
|
||||||
|
|
||||||
private get imageTransformer(): Optional<Operator<string>> {
|
private get imageTransformer(): Optional<Operator<string>> {
|
||||||
if (!Options.displayImage) {
|
if (!Options.displayImage) return undefined;
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
const pathName = path.dirname(this.document.fileName);
|
const pathName = path.dirname(this.document.fileName);
|
||||||
return src => {
|
return src => {
|
||||||
const srcUri = vscode.Uri.file(pathName + '/' + src);
|
const srcUri = vscode.Uri.file(pathName + '/' + src);
|
||||||
|
@ -108,6 +118,18 @@ export default class PreviewContext {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private get script() {
|
||||||
|
if (!Options.enableCodeLens) return undefined;
|
||||||
|
return "window.addEventListener('message', event => {" +
|
||||||
|
"const message = event.data;" +
|
||||||
|
"switch (message.command) {" +
|
||||||
|
"case 'goto':" +
|
||||||
|
"window.location.hash = message.value;" +
|
||||||
|
"break;" +
|
||||||
|
"}" +
|
||||||
|
"})";
|
||||||
|
}
|
||||||
|
|
||||||
private updateTitle() {
|
private updateTitle() {
|
||||||
const updating = this.updating ? '(Updating) ' : '';
|
const updating = this.updating ? '(Updating) ' : '';
|
||||||
const fileName = path.basename(this.document.fileName);
|
const fileName = path.basename(this.document.fileName);
|
||||||
|
|
|
@ -11,6 +11,7 @@ import Diagnosis from './diagnosis';
|
||||||
import Logger from './logger';
|
import Logger from './logger';
|
||||||
import Options from './options';
|
import Options from './options';
|
||||||
import PreviewContext from './contexts/preview';
|
import PreviewContext from './contexts/preview';
|
||||||
|
import CodeLensProvider from './providers/code_lens';
|
||||||
import CompletionItemProvider from './providers/completion_item';
|
import CompletionItemProvider from './providers/completion_item';
|
||||||
import DocumentSymbolProvider from './providers/document_symbol';
|
import DocumentSymbolProvider from './providers/document_symbol';
|
||||||
import FoldingRangeProvider from './providers/folding_range';
|
import FoldingRangeProvider from './providers/folding_range';
|
||||||
|
@ -23,6 +24,8 @@ export function activate(context: vscode.ExtensionContext) {
|
||||||
vscode.workspace.onDidCloseTextDocument(ContextMapping.onDocumentClose),
|
vscode.workspace.onDidCloseTextDocument(ContextMapping.onDocumentClose),
|
||||||
vscode.workspace.onDidChangeConfiguration(Options.clear),
|
vscode.workspace.onDidChangeConfiguration(Options.clear),
|
||||||
vscode.commands.registerTextEditorCommand('texinfo.preview.show', PreviewContext.showPreview),
|
vscode.commands.registerTextEditorCommand('texinfo.preview.show', PreviewContext.showPreview),
|
||||||
|
vscode.commands.registerCommand('texinfo.preview.goto', PreviewContext.gotoPreview),
|
||||||
|
vscode.languages.registerCodeLensProvider('texinfo', new CodeLensProvider()),
|
||||||
vscode.languages.registerCompletionItemProvider('texinfo', new CompletionItemProvider(), '@'),
|
vscode.languages.registerCompletionItemProvider('texinfo', new CompletionItemProvider(), '@'),
|
||||||
vscode.languages.registerDocumentSymbolProvider('texinfo', new DocumentSymbolProvider()),
|
vscode.languages.registerDocumentSymbolProvider('texinfo', new DocumentSymbolProvider()),
|
||||||
vscode.languages.registerFoldingRangeProvider('texinfo', new FoldingRangeProvider()),
|
vscode.languages.registerFoldingRangeProvider('texinfo', new FoldingRangeProvider()),
|
||||||
|
|
|
@ -24,6 +24,10 @@ export default class Options implements vscode.Disposable {
|
||||||
return Options.instance.getString('makeinfo');
|
return Options.instance.getString('makeinfo');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static get enableCodeLens() {
|
||||||
|
return Options.instance.getBoolean('enableCodeLens');
|
||||||
|
}
|
||||||
|
|
||||||
static get enableSnippets() {
|
static get enableSnippets() {
|
||||||
return Options.instance.getBoolean('completion.enableSnippets');
|
return Options.instance.getBoolean('completion.enableSnippets');
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
/**
|
||||||
|
* providers/code_lens.ts
|
||||||
|
*
|
||||||
|
* @author CismonX <admin@cismon.net>
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as vscode from 'vscode';
|
||||||
|
import ContextMapping from '../context_mapping';
|
||||||
|
import Options from '../options';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provide code lenses for Texinfo document.
|
||||||
|
*/
|
||||||
|
export default class CodeLensProvider implements vscode.CodeLensProvider {
|
||||||
|
|
||||||
|
provideCodeLenses(document: vscode.TextDocument) {
|
||||||
|
if (!Options.enableCodeLens) return undefined;
|
||||||
|
return ContextMapping.getDocumentContext(document).foldingRange.nodeValues;
|
||||||
|
}
|
||||||
|
}
|
|
@ -53,3 +53,44 @@ export function lineNumToRange(startLine: number, endLine = startLine) {
|
||||||
const endPosition = new vscode.Position(endLine, Number.MAX_SAFE_INTEGER);
|
const endPosition = new vscode.Position(endLine, Number.MAX_SAFE_INTEGER);
|
||||||
return new vscode.Range(startPosition, endPosition);
|
return new vscode.Range(startPosition, endPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether character is an alphabet.
|
||||||
|
*
|
||||||
|
* @param charCode ASCII code of character.
|
||||||
|
*/
|
||||||
|
export function isAlpha(charCode: number) {
|
||||||
|
return charCode >= 97 && charCode <= 122 || charCode >= 65 && charCode <= 90;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether character is alphanumeric.
|
||||||
|
*
|
||||||
|
* @param charCode ASCII code of character.
|
||||||
|
*/
|
||||||
|
export function isAlnum(charCode: number) {
|
||||||
|
return isAlpha(charCode) || charCode >= 48 && charCode <= 57;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get corresponding HTML cross-reference name by node name.
|
||||||
|
*
|
||||||
|
* See section *HTML Cross-reference Node Name Expansion* in the Texinfo manual.
|
||||||
|
*
|
||||||
|
* TODO: Node name is not displayed verbatim, leading to wrong HTML xref when containing commands.
|
||||||
|
* Fix this when migrating to LSP.
|
||||||
|
*
|
||||||
|
* @param nodeName
|
||||||
|
*/
|
||||||
|
export function getNodeHtmlRef(nodeName: string) {
|
||||||
|
const result = nodeName.trim().split(/\s+/)
|
||||||
|
.map(word => word.split('')
|
||||||
|
.map(ch => {
|
||||||
|
const charCode = ch.charCodeAt(0);
|
||||||
|
return isAlnum(charCode) ? ch : '_00' + charCode.toString(16);
|
||||||
|
})
|
||||||
|
.join(''))
|
||||||
|
.join('-');
|
||||||
|
const firstCharCode = result.charCodeAt(0);
|
||||||
|
return isAlpha(firstCharCode) ? result : 'g_t_00' + firstCharCode.toString(16) + result.substring(1);
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue