Simplify toc structure

This commit is contained in:
Matt Bierner
2022-01-19 12:49:14 -08:00
parent 7756c6d7c7
commit a2d7dfaf35
7 changed files with 75 additions and 91 deletions

View File

@@ -5,7 +5,7 @@
import * as vscode from 'vscode';
import { MarkdownEngine } from '../markdownEngine';
import { SkinnyTextDocument, TableOfContentsProvider, TocEntry } from '../tableOfContentsProvider';
import { SkinnyTextDocument, TableOfContents, TocEntry } from '../tableOfContentsProvider';
interface MarkdownSymbol {
readonly level: number;
@@ -20,22 +20,22 @@ export default class MDDocumentSymbolProvider implements vscode.DocumentSymbolPr
) { }
public async provideDocumentSymbolInformation(document: SkinnyTextDocument): Promise<vscode.SymbolInformation[]> {
const toc = await new TableOfContentsProvider(this.engine, document).getToc();
return toc.map(entry => this.toSymbolInformation(entry));
const toc = await TableOfContents.create(this.engine, document);
return toc.entries.map(entry => this.toSymbolInformation(entry));
}
public async provideDocumentSymbols(document: SkinnyTextDocument): Promise<vscode.DocumentSymbol[]> {
const toc = await new TableOfContentsProvider(this.engine, document).getToc();
const toc = await TableOfContents.create(this.engine, document);
const root: MarkdownSymbol = {
level: -Infinity,
children: [],
parent: undefined
};
this.buildTree(root, toc);
this.buildTree(root, toc.entries);
return root.children;
}
private buildTree(parent: MarkdownSymbol, entries: TocEntry[]) {
private buildTree(parent: MarkdownSymbol, entries: readonly TocEntry[]) {
if (!entries.length) {
return;
}

View File

@@ -6,7 +6,7 @@
import Token = require('markdown-it/lib/token');
import * as vscode from 'vscode';
import { MarkdownEngine } from '../markdownEngine';
import { TableOfContentsProvider } from '../tableOfContentsProvider';
import { TableOfContents } from '../tableOfContentsProvider';
const rangeLimit = 5000;
@@ -54,9 +54,8 @@ export default class MarkdownFoldingProvider implements vscode.FoldingRangeProvi
}
private async getHeaderFoldingRanges(document: vscode.TextDocument) {
const tocProvider = new TableOfContentsProvider(this.engine, document);
const toc = await tocProvider.getToc();
return toc.map(entry => {
const toc = await TableOfContents.create(this.engine, document);
return toc.entries.map(entry => {
let endLine = entry.location.range.end.line;
if (document.lineAt(endLine).isEmptyOrWhitespace && endLine >= entry.line + 1) {
endLine = endLine - 1;

View File

@@ -6,7 +6,7 @@
import { dirname, resolve } from 'path';
import * as vscode from 'vscode';
import { MarkdownEngine } from '../markdownEngine';
import { TableOfContentsProvider, TocEntry } from '../tableOfContentsProvider';
import { TableOfContents, TocEntry } from '../tableOfContentsProvider';
import { isMarkdownFile } from '../util/file';
import { resolveUriToMarkdownFile } from '../util/openDocumentLink';
import LinkProvider from './documentLinkProvider';
@@ -257,8 +257,7 @@ export class PathCompletionProvider implements vscode.CompletionItemProvider {
for (const cell of notebook.getCells()) {
if (cell.kind === vscode.NotebookCellKind.Markup && isMarkdownFile(cell.document)) {
const tocProvider = new TableOfContentsProvider(this.engine, cell.document);
toc.push(...(await tocProvider.getToc()));
toc.push(...(await TableOfContents.create(this.engine, cell.document)).entries);
}
}
@@ -266,9 +265,7 @@ export class PathCompletionProvider implements vscode.CompletionItemProvider {
}
}
const tocProvider = new TableOfContentsProvider(this.engine, document);
const toc = await tocProvider.getToc();
return toc;
return (await TableOfContents.create(this.engine, document)).entries;
}
private async *providePathSuggestions(document: vscode.TextDocument, position: vscode.Position, context: CompletionContext): AsyncIterable<vscode.CompletionItem> {

View File

@@ -5,7 +5,7 @@
import Token = require('markdown-it/lib/token');
import * as vscode from 'vscode';
import { MarkdownEngine } from '../markdownEngine';
import { TableOfContentsProvider, TocEntry } from '../tableOfContentsProvider';
import { TableOfContents, TocEntry } from '../tableOfContentsProvider';
interface MarkdownItTokenWithMap extends Token {
map: [number, number];
@@ -53,24 +53,22 @@ export default class MarkdownSmartSelect implements vscode.SelectionRangeProvide
}
private async getHeaderSelectionRange(document: vscode.TextDocument, position: vscode.Position): Promise<vscode.SelectionRange | undefined> {
const toc = await TableOfContents.create(this.engine, document);
const tocProvider = new TableOfContentsProvider(this.engine, document);
const toc = await tocProvider.getToc();
const headerInfo = getHeadersForPosition(toc, position);
const headerInfo = getHeadersForPosition(toc.entries, position);
const headers = headerInfo.headers;
let currentRange: vscode.SelectionRange | undefined;
for (let i = 0; i < headers.length; i++) {
currentRange = createHeaderRange(headers[i], i === headers.length - 1, headerInfo.headerOnThisLine, currentRange, getFirstChildHeader(document, headers[i], toc));
currentRange = createHeaderRange(headers[i], i === headers.length - 1, headerInfo.headerOnThisLine, currentRange, getFirstChildHeader(document, headers[i], toc.entries));
}
return currentRange;
}
}
function getHeadersForPosition(toc: TocEntry[], position: vscode.Position): { headers: TocEntry[], headerOnThisLine: boolean } {
function getHeadersForPosition(toc: readonly TocEntry[], position: vscode.Position): { headers: TocEntry[], headerOnThisLine: boolean } {
const enclosingHeaders = toc.filter(header => header.location.range.start.line <= position.line && header.location.range.end.line >= position.line);
const sortedHeaders = enclosingHeaders.sort((header1, header2) => (header1.line - position.line) - (header2.line - position.line));
const onThisLine = toc.find(header => header.line === position.line) !== undefined;
@@ -238,7 +236,7 @@ function isBlockElement(token: Token): boolean {
return !['list_item_close', 'paragraph_close', 'bullet_list_close', 'inline', 'heading_close', 'heading_open'].includes(token.type);
}
function getFirstChildHeader(document: vscode.TextDocument, header?: TocEntry, toc?: TocEntry[]): vscode.Position | undefined {
function getFirstChildHeader(document: vscode.TextDocument, header?: TocEntry, toc?: readonly TocEntry[]): vscode.Position | undefined {
let childRange: vscode.Position | undefined;
if (header && toc) {
let children = toc.filter(t => header.location.range.contains(t.location.range) && t.location.range.start.line > header.location.range.start.line).sort((t1, t2) => t1.line - t2.line);

View File

@@ -28,34 +28,24 @@ export interface SkinnyTextDocument {
getText(): string;
}
export class TableOfContentsProvider {
private toc?: TocEntry[];
export class TableOfContents {
public static async create(engine: MarkdownEngine, document: SkinnyTextDocument): Promise<TableOfContents> {
const entries = await this.buildToc(engine, document);
return new TableOfContents(entries);
}
public constructor(
private engine: MarkdownEngine,
private document: SkinnyTextDocument
private constructor(
public readonly entries: readonly TocEntry[],
) { }
public async getToc(): Promise<TocEntry[]> {
if (!this.toc) {
try {
this.toc = await this.buildToc(this.document);
} catch (e) {
this.toc = [];
}
}
return this.toc;
}
public async lookup(fragment: string): Promise<TocEntry | undefined> {
const toc = await this.getToc();
public lookup(fragment: string): TocEntry | undefined {
const slug = githubSlugifier.fromHeading(fragment);
return toc.find(entry => entry.slug.equals(slug));
return this.entries.find(entry => entry.slug.equals(slug));
}
private async buildToc(document: SkinnyTextDocument): Promise<TocEntry[]> {
private static async buildToc(engine: MarkdownEngine, document: SkinnyTextDocument): Promise<TocEntry[]> {
const toc: TocEntry[] = [];
const tokens = await this.engine.parse(document);
const tokens = await engine.parse(document);
const existingSlugEntries = new Map<string, { count: number }>();
@@ -78,8 +68,8 @@ export class TableOfContentsProvider {
toc.push({
slug,
text: TableOfContentsProvider.getHeaderText(line.text),
level: TableOfContentsProvider.getHeaderLevel(heading.markup),
text: TableOfContents.getHeaderText(line.text),
level: TableOfContents.getHeaderLevel(heading.markup),
line: lineNumber,
location: new vscode.Location(document.uri,
new vscode.Range(lineNumber, 0, lineNumber, line.text.length))

View File

@@ -6,7 +6,7 @@
import * as assert from 'assert';
import 'mocha';
import * as vscode from 'vscode';
import { TableOfContentsProvider } from '../tableOfContentsProvider';
import { TableOfContents } from '../tableOfContentsProvider';
import { createNewMarkdownEngine } from './engine';
import { InMemoryDocument } from './inMemoryDocument';
@@ -16,36 +16,36 @@ const testFileName = vscode.Uri.file('test.md');
suite('markdown.TableOfContentsProvider', () => {
test('Lookup should not return anything for empty document', async () => {
const doc = new InMemoryDocument(testFileName, '');
const provider = new TableOfContentsProvider(createNewMarkdownEngine(), doc);
const provider = await TableOfContents.create(createNewMarkdownEngine(), doc);
assert.strictEqual(await provider.lookup(''), undefined);
assert.strictEqual(await provider.lookup('foo'), undefined);
assert.strictEqual(provider.lookup(''), undefined);
assert.strictEqual(provider.lookup('foo'), undefined);
});
test('Lookup should not return anything for document with no headers', async () => {
const doc = new InMemoryDocument(testFileName, 'a *b*\nc');
const provider = new TableOfContentsProvider(createNewMarkdownEngine(), doc);
const provider = await TableOfContents.create(createNewMarkdownEngine(), doc);
assert.strictEqual(await provider.lookup(''), undefined);
assert.strictEqual(await provider.lookup('foo'), undefined);
assert.strictEqual(await provider.lookup('a'), undefined);
assert.strictEqual(await provider.lookup('b'), undefined);
assert.strictEqual(provider.lookup(''), undefined);
assert.strictEqual(provider.lookup('foo'), undefined);
assert.strictEqual(provider.lookup('a'), undefined);
assert.strictEqual(provider.lookup('b'), undefined);
});
test('Lookup should return basic #header', async () => {
const doc = new InMemoryDocument(testFileName, `# a\nx\n# c`);
const provider = new TableOfContentsProvider(createNewMarkdownEngine(), doc);
const provider = await TableOfContents.create(createNewMarkdownEngine(), doc);
{
const entry = await provider.lookup('a');
const entry = provider.lookup('a');
assert.ok(entry);
assert.strictEqual(entry!.line, 0);
}
{
assert.strictEqual(await provider.lookup('x'), undefined);
assert.strictEqual(provider.lookup('x'), undefined);
}
{
const entry = await provider.lookup('c');
const entry = provider.lookup('c');
assert.ok(entry);
assert.strictEqual(entry!.line, 2);
}
@@ -53,40 +53,40 @@ suite('markdown.TableOfContentsProvider', () => {
test('Lookups should be case in-sensitive', async () => {
const doc = new InMemoryDocument(testFileName, `# fOo\n`);
const provider = new TableOfContentsProvider(createNewMarkdownEngine(), doc);
const provider = await TableOfContents.create(createNewMarkdownEngine(), doc);
assert.strictEqual((await provider.lookup('fOo'))!.line, 0);
assert.strictEqual((await provider.lookup('foo'))!.line, 0);
assert.strictEqual((await provider.lookup('FOO'))!.line, 0);
assert.strictEqual((provider.lookup('fOo'))!.line, 0);
assert.strictEqual((provider.lookup('foo'))!.line, 0);
assert.strictEqual((provider.lookup('FOO'))!.line, 0);
});
test('Lookups should ignore leading and trailing white-space, and collapse internal whitespace', async () => {
const doc = new InMemoryDocument(testFileName, `# f o o \n`);
const provider = new TableOfContentsProvider(createNewMarkdownEngine(), doc);
const provider = await TableOfContents.create(createNewMarkdownEngine(), doc);
assert.strictEqual((await provider.lookup('f o o'))!.line, 0);
assert.strictEqual((await provider.lookup(' f o o'))!.line, 0);
assert.strictEqual((await provider.lookup(' f o o '))!.line, 0);
assert.strictEqual((await provider.lookup('f o o'))!.line, 0);
assert.strictEqual((await provider.lookup('f o o'))!.line, 0);
assert.strictEqual((provider.lookup('f o o'))!.line, 0);
assert.strictEqual((provider.lookup(' f o o'))!.line, 0);
assert.strictEqual((provider.lookup(' f o o '))!.line, 0);
assert.strictEqual((provider.lookup('f o o'))!.line, 0);
assert.strictEqual((provider.lookup('f o o'))!.line, 0);
assert.strictEqual(await provider.lookup('f'), undefined);
assert.strictEqual(await provider.lookup('foo'), undefined);
assert.strictEqual(await provider.lookup('fo o'), undefined);
assert.strictEqual(provider.lookup('f'), undefined);
assert.strictEqual(provider.lookup('foo'), undefined);
assert.strictEqual(provider.lookup('fo o'), undefined);
});
test('should handle special characters #44779', async () => {
const doc = new InMemoryDocument(testFileName, `# Indentação\n`);
const provider = new TableOfContentsProvider(createNewMarkdownEngine(), doc);
const provider = await TableOfContents.create(createNewMarkdownEngine(), doc);
assert.strictEqual((await provider.lookup('indentação'))!.line, 0);
assert.strictEqual((provider.lookup('indentação'))!.line, 0);
});
test('should handle special characters 2, #48482', async () => {
const doc = new InMemoryDocument(testFileName, `# Инструкция - Делай Раз, Делай Два\n`);
const provider = new TableOfContentsProvider(createNewMarkdownEngine(), doc);
const provider = await TableOfContents.create(createNewMarkdownEngine(), doc);
assert.strictEqual((await provider.lookup('инструкция---делай-раз-делай-два'))!.line, 0);
assert.strictEqual((provider.lookup('инструкция---делай-раз-делай-два'))!.line, 0);
});
test('should handle special characters 3, #37079', async () => {
@@ -97,32 +97,32 @@ suite('markdown.TableOfContentsProvider', () => {
### Заголовок Header 3
## Заголовок`);
const provider = new TableOfContentsProvider(createNewMarkdownEngine(), doc);
const provider = await TableOfContents.create(createNewMarkdownEngine(), doc);
assert.strictEqual((await provider.lookup('header-2'))!.line, 0);
assert.strictEqual((await provider.lookup('header-3'))!.line, 1);
assert.strictEqual((await provider.lookup('Заголовок-2'))!.line, 2);
assert.strictEqual((await provider.lookup('Заголовок-3'))!.line, 3);
assert.strictEqual((await provider.lookup('Заголовок-header-3'))!.line, 4);
assert.strictEqual((await provider.lookup('Заголовок'))!.line, 5);
assert.strictEqual((provider.lookup('header-2'))!.line, 0);
assert.strictEqual((provider.lookup('header-3'))!.line, 1);
assert.strictEqual((provider.lookup('Заголовок-2'))!.line, 2);
assert.strictEqual((provider.lookup('Заголовок-3'))!.line, 3);
assert.strictEqual((provider.lookup('Заголовок-header-3'))!.line, 4);
assert.strictEqual((provider.lookup('Заголовок'))!.line, 5);
});
test('Lookup should support suffixes for repeated headers', async () => {
const doc = new InMemoryDocument(testFileName, `# a\n# a\n## a`);
const provider = new TableOfContentsProvider(createNewMarkdownEngine(), doc);
const provider = await TableOfContents.create(createNewMarkdownEngine(), doc);
{
const entry = await provider.lookup('a');
const entry = provider.lookup('a');
assert.ok(entry);
assert.strictEqual(entry!.line, 0);
}
{
const entry = await provider.lookup('a-1');
const entry = provider.lookup('a-1');
assert.ok(entry);
assert.strictEqual(entry!.line, 1);
}
{
const entry = await provider.lookup('a-2');
const entry = provider.lookup('a-2');
assert.ok(entry);
assert.strictEqual(entry!.line, 2);
}

View File

@@ -6,7 +6,7 @@
import * as path from 'path';
import * as vscode from 'vscode';
import { MarkdownEngine } from '../markdownEngine';
import { TableOfContentsProvider } from '../tableOfContentsProvider';
import { TableOfContents } from '../tableOfContentsProvider';
import { isMarkdownFile } from './file';
import { extname } from './path';
@@ -104,8 +104,8 @@ function getViewColumn(resource: vscode.Uri): vscode.ViewColumn {
}
async function tryRevealLineUsingTocFragment(engine: MarkdownEngine, editor: vscode.TextEditor, fragment: string): Promise<boolean> {
const toc = new TableOfContentsProvider(engine, editor.document);
const entry = await toc.lookup(fragment);
const toc = await TableOfContents.create(engine, editor.document);
const entry = toc.lookup(fragment);
if (entry) {
const lineStart = new vscode.Range(entry.line, 0, entry.line, 0);
editor.selection = new vscode.Selection(lineStart.start, lineStart.end);