diff --git a/package.json b/package.json index b27ccb3..7cdd430 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,16 @@ "plugin:@typescript-eslint/recommended" ], "rules": { - "comma-dangle": ["warn", "always-multiline"], + "comma-dangle": [ + "warn", + "always-multiline" + ], + "max-len": [ + "warn", + { + "code": 120 + } + ], "@typescript-eslint/explicit-module-boundary-types": "off", "@typescript-eslint/semi": "warn" } diff --git a/src/extension.ts b/src/extension.ts index 1b7969d..fa03879 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -9,17 +9,25 @@ import * as vscode from 'vscode'; import { Options } from './options'; import { Preview } from './preview'; import { CompletionItemProvider } from './completion'; +import { FoldingRangeProvider, FoldingRangeContext } from './folding'; export function activate(context: vscode.ExtensionContext) { context.subscriptions.push( vscode.workspace.onDidSaveTextDocument(Preview.update), - vscode.workspace.onDidCloseTextDocument(Preview.close), + vscode.workspace.onDidCloseTextDocument((document) => { + Preview.close(document); + FoldingRangeContext.close(document); + }), + vscode.workspace.onDidOpenTextDocument(FoldingRangeContext.open), + vscode.workspace.onDidChangeTextDocument(FoldingRangeContext.update), vscode.commands.registerTextEditorCommand('texinfo.showPreview', Preview.show), vscode.languages.registerCompletionItemProvider('texinfo', new CompletionItemProvider(), '@'), + vscode.languages.registerFoldingRangeProvider('texinfo', new FoldingRangeProvider()), ); } export function deactivate() { Preview.destroyAll(); Options.clear(); + FoldingRangeContext.clear(); } diff --git a/src/folding.ts b/src/folding.ts new file mode 100644 index 0000000..6284f28 --- /dev/null +++ b/src/folding.ts @@ -0,0 +1,144 @@ +/** + * folding.ts + * + * @author CismonX + * @license MIT + */ + +import * as vscode from 'vscode'; + +/** + * Provide folding range info for Texinfo source code. + */ +export class FoldingRangeProvider implements vscode.FoldingRangeProvider { + + provideFoldingRanges(document: vscode.TextDocument) { + return FoldingRangeContext.get(document).foldingRanges; + } +} + +export class FoldingRangeContext { + + private static readonly map = new Map(); + + static open(document: vscode.TextDocument) { + if (document.languageId === 'texinfo') { + new FoldingRangeContext(document); + } + } + + static get(document: vscode.TextDocument) { + return FoldingRangeContext.map.get(document) ?? new FoldingRangeContext(document); + } + + static update(event: vscode.TextDocumentChangeEvent) { + if (event.document.languageId !== 'texinfo') { + return; + } + FoldingRangeContext.get(event.document)?.update(event.contentChanges); + } + + static close(document: vscode.TextDocument) { + FoldingRangeContext.map.delete(document); + } + + static clear() { + FoldingRangeContext.map.clear(); + } + + foldingRanges = []; + + private commentRange?: vscode.FoldingRange; + + private headerStart?: number; + + private closingBlocks = []; + + private constructor(private readonly document: vscode.TextDocument) { + FoldingRangeContext.map.set(document, this); + this.calculateFoldingRanges(); + } + + private calculateFoldingRanges() { + for (let idx = this.document.lineCount - 1; idx >= 0; --idx) { + const line = this.document.lineAt(idx); + const lineText = line.text; + const lineNum = line.lineNumber; + if (!lineText.startsWith('@')) { + continue; + } + if (this.processComment(lineText, lineNum)) { + continue; + } + this.processBlock(lineText, lineNum); + } + if (this.commentRange !== undefined) { + if (this.commentRange.end - this.commentRange.start > 1) { + this.foldingRanges.push(this.commentRange); + } + } + } + + private processComment(lineText: string, lineNum: number) { + if (lineText.startsWith('c', 1)) { + if (!lineText.startsWith(' ', 2) && !lineText.startsWith('omment ', 2)) { + return false; + } + // Check for opening/closing header. + if (lineText.startsWith('%**', lineText[2] === ' ' ? 3 : 9)) { + if (this.headerStart === undefined) { + this.headerStart = lineNum; + } else { + this.foldingRanges.push(new vscode.FoldingRange(lineNum, this.headerStart)); + this.headerStart = undefined; + } + } + if (this.commentRange === undefined) { + this.commentRange = new vscode.FoldingRange(lineNum, lineNum, vscode.FoldingRangeKind.Comment); + } else if (this.commentRange.start - 1 === lineNum) { + this.commentRange.start = lineNum; + } else { + this.foldingRanges.push(this.commentRange); + this.commentRange = new vscode.FoldingRange(lineNum, lineNum, vscode.FoldingRangeKind.Comment); + } + return true; + } + return false; + } + + private processBlock(lineText: string, lineNum: number) { + if (lineText.startsWith('end ', 1)) { + this.closingBlocks.push({ name: lineText.substring(5), line: lineNum }); + } else { + const closingBlock = this.closingBlocks.pop(); + if (closingBlock === undefined) { + return; + } + if (lineText.substring(1, closingBlock.name.length + 2).trim() === closingBlock.name) { + this.foldingRanges.push(new vscode.FoldingRange(lineNum, closingBlock.line)); + } else { + this.closingBlocks.push(closingBlock); + } + } + } + + private update(events: readonly vscode.TextDocumentContentChangeEvent[]) { + // console.log(events); + } +} + +/** + * Represents a Texinfo block marked "closing" by `@end` command. + */ +interface ClosingBlock { + + /** + * The name of the block. + */ + name: string; + + /** + * The terminating line number of the block. + */ + line: number; +}