diff --git a/extensions/markdown-language-features/src/languageFeatures/documentLinks.ts b/extensions/markdown-language-features/src/languageFeatures/documentLinks.ts index 4fe6320c6cc..154baf8abcb 100644 --- a/extensions/markdown-language-features/src/languageFeatures/documentLinks.ts +++ b/extensions/markdown-language-features/src/languageFeatures/documentLinks.ts @@ -9,7 +9,7 @@ import * as uri from 'vscode-uri'; import { OpenDocumentLinkCommand } from '../commands/openDocumentLink'; import { ILogger } from '../logging'; import { IMdParser } from '../markdownEngine'; -import { ITextDocument } from '../types/textDocument'; +import { getLine, ITextDocument } from '../types/textDocument'; import { coalesce } from '../util/arrays'; import { noopToken } from '../util/cancellation'; import { Disposable } from '../util/dispose'; @@ -422,9 +422,9 @@ export class MdLinkComputer { reference = match[5]; const offset = ((match.index ?? 0) + match[1].length) + 1; hrefStart = document.positionAt(offset); - const line = document.lineAt(hrefStart.line); + const line = getLine(document, hrefStart.line); // See if link looks like a checkbox - const checkboxMatch = line.text.match(/^\s*[\-\*]\s*\[x\]/i); + const checkboxMatch = line.match(/^\s*[\-\*]\s*\[x\]/i); if (checkboxMatch && hrefStart.character <= checkboxMatch[0].length) { continue; } diff --git a/extensions/markdown-language-features/src/languageFeatures/folding.ts b/extensions/markdown-language-features/src/languageFeatures/folding.ts index f79df449b04..2fef6f67958 100644 --- a/extensions/markdown-language-features/src/languageFeatures/folding.ts +++ b/extensions/markdown-language-features/src/languageFeatures/folding.ts @@ -7,7 +7,8 @@ import type Token = require('markdown-it/lib/token'); import * as vscode from 'vscode'; import { IMdParser } from '../markdownEngine'; import { MdTableOfContentsProvider } from '../tableOfContents'; -import { ITextDocument } from '../types/textDocument'; +import { getLine, ITextDocument } from '../types/textDocument'; +import { isEmptyOrWhitespace } from '../util/string'; const rangeLimit = 5000; @@ -59,7 +60,7 @@ export class MdFoldingProvider implements vscode.FoldingRangeProvider { const toc = await this.tocProvide.getForDocument(document); return toc.entries.map(entry => { let endLine = entry.sectionLocation.range.end.line; - if (document.lineAt(endLine).isEmptyOrWhitespace && endLine >= entry.line + 1) { + if (isEmptyOrWhitespace(getLine(document, endLine)) && endLine >= entry.line + 1) { endLine = endLine - 1; } return new vscode.FoldingRange(entry.line, endLine); @@ -72,7 +73,7 @@ export class MdFoldingProvider implements vscode.FoldingRangeProvider { return multiLineListItems.map(listItem => { const start = listItem.map[0]; let end = listItem.map[1] - 1; - if (document.lineAt(end).isEmptyOrWhitespace && end >= start + 1) { + if (isEmptyOrWhitespace(getLine(document, end)) && end >= start + 1) { end = end - 1; } return new vscode.FoldingRange(start, end, this.getFoldingRangeKind(listItem)); diff --git a/extensions/markdown-language-features/src/languageFeatures/pathCompletions.ts b/extensions/markdown-language-features/src/languageFeatures/pathCompletions.ts index 28f75b57444..65ad060c080 100644 --- a/extensions/markdown-language-features/src/languageFeatures/pathCompletions.ts +++ b/extensions/markdown-language-features/src/languageFeatures/pathCompletions.ts @@ -7,7 +7,7 @@ import { dirname, resolve } from 'path'; import * as vscode from 'vscode'; import { IMdParser } from '../markdownEngine'; import { TableOfContents } from '../tableOfContents'; -import { ITextDocument } from '../types/textDocument'; +import { getLine, ITextDocument } from '../types/textDocument'; import { resolveUriToMarkdownFile } from '../util/openDocumentLink'; import { Schemes } from '../util/schemes'; import { IMdWorkspace } from '../workspace'; @@ -167,7 +167,7 @@ export class MdVsCodePathCompletionProvider implements vscode.CompletionItemProv private readonly definitionPattern = /^\s*\[[\w\-]+\]:\s*([^\s]*)$/m; private getPathCompletionContext(document: ITextDocument, position: vscode.Position): CompletionContext | undefined { - const line = document.lineAt(position.line).text; + const line = getLine(document, position.line); const linePrefixText = line.slice(0, position.character); const lineSuffixText = line.slice(position.character); diff --git a/extensions/markdown-language-features/src/languageFeatures/smartSelect.ts b/extensions/markdown-language-features/src/languageFeatures/smartSelect.ts index 29f0e922559..61fa887073f 100644 --- a/extensions/markdown-language-features/src/languageFeatures/smartSelect.ts +++ b/extensions/markdown-language-features/src/languageFeatures/smartSelect.ts @@ -6,7 +6,8 @@ import Token = require('markdown-it/lib/token'); import * as vscode from 'vscode'; import { IMdParser } from '../markdownEngine'; import { MdTableOfContentsProvider, TocEntry } from '../tableOfContents'; -import { ITextDocument } from '../types/textDocument'; +import { getLine, ITextDocument } from '../types/textDocument'; +import { isEmptyOrWhitespace } from '../util/string'; interface MarkdownItTokenWithMap extends Token { map: [number, number]; @@ -111,14 +112,14 @@ function createBlockRange(block: MarkdownItTokenWithMap, document: ITextDocument if (block.type === 'fence') { return createFencedRange(block, cursorLine, document, parent); } else { - let startLine = document.lineAt(block.map[0]).isEmptyOrWhitespace ? block.map[0] + 1 : block.map[0]; + let startLine = isEmptyOrWhitespace(getLine(document, block.map[0])) ? block.map[0] + 1 : block.map[0]; let endLine = startLine === block.map[1] ? block.map[1] : block.map[1] - 1; if (block.type === 'paragraph_open' && block.map[1] - block.map[0] === 2) { startLine = endLine = cursorLine; - } else if (isList(block) && document.lineAt(endLine).isEmptyOrWhitespace) { + } else if (isList(block) && isEmptyOrWhitespace(getLine(document, endLine))) { endLine = endLine - 1; } - const range = new vscode.Range(startLine, 0, endLine, document.lineAt(endLine).text?.length ?? 0); + const range = new vscode.Range(startLine, 0, endLine, getLine(document, endLine).length); if (parent?.range.contains(range) && !parent.range.isEqual(range)) { return new vscode.SelectionRange(range, parent); } else if (parent?.range.isEqual(range)) { @@ -130,7 +131,7 @@ function createBlockRange(block: MarkdownItTokenWithMap, document: ITextDocument } function createInlineRange(document: ITextDocument, cursorPosition: vscode.Position, parent?: vscode.SelectionRange): vscode.SelectionRange | undefined { - const lineText = document.lineAt(cursorPosition.line).text; + const lineText = getLine(document, cursorPosition.line); const boldSelection = createBoldRange(lineText, cursorPosition.character, cursorPosition.line, parent); const italicSelection = createOtherInlineRange(lineText, cursorPosition.character, cursorPosition.line, true, parent); let comboSelection: vscode.SelectionRange | undefined; @@ -150,8 +151,8 @@ function createFencedRange(token: MarkdownItTokenWithMap, cursorLine: number, do const startLine = token.map[0]; const endLine = token.map[1] - 1; const onFenceLine = cursorLine === startLine || cursorLine === endLine; - const fenceRange = new vscode.Range(startLine, 0, endLine, document.lineAt(endLine).text.length); - const contentRange = endLine - startLine > 2 && !onFenceLine ? new vscode.Range(startLine + 1, 0, endLine - 1, document.lineAt(endLine - 1).text.length) : undefined; + const fenceRange = new vscode.Range(startLine, 0, endLine, getLine(document, endLine).length); + const contentRange = endLine - startLine > 2 && !onFenceLine ? new vscode.Range(startLine + 1, 0, endLine - 1, getLine(document, endLine - 1).length) : undefined; if (contentRange) { return new vscode.SelectionRange(contentRange, new vscode.SelectionRange(fenceRange, parent)); } else { @@ -242,7 +243,7 @@ function getFirstChildHeader(document: ITextDocument, header?: TocEntry, toc?: r const children = toc.filter(t => header.sectionLocation.range.contains(t.sectionLocation.range) && t.sectionLocation.range.start.line > header.sectionLocation.range.start.line).sort((t1, t2) => t1.line - t2.line); if (children.length > 0) { childRange = children[0].sectionLocation.range.start; - const lineText = document.lineAt(childRange.line - 1).text; + const lineText = getLine(document, childRange.line - 1); return childRange ? childRange.translate(-1, lineText.length) : undefined; } } diff --git a/extensions/markdown-language-features/src/tableOfContents.ts b/extensions/markdown-language-features/src/tableOfContents.ts index b1e3e25c8d3..5e0a0cc3684 100644 --- a/extensions/markdown-language-features/src/tableOfContents.ts +++ b/extensions/markdown-language-features/src/tableOfContents.ts @@ -7,7 +7,7 @@ import * as vscode from 'vscode'; import { ILogger } from './logging'; import { IMdParser } from './markdownEngine'; import { githubSlugifier, Slug, Slugifier } from './slugify'; -import { ITextDocument } from './types/textDocument'; +import { getLine, ITextDocument } from './types/textDocument'; import { Disposable } from './util/dispose'; import { isMarkdownFile } from './util/file'; import { Schemes } from './util/schemes'; @@ -108,9 +108,9 @@ export class TableOfContents { } const lineNumber = heading.map[0]; - const line = document.lineAt(lineNumber); + const line = getLine(document, lineNumber); - let slug = parser.slugifier.fromHeading(line.text); + let slug = parser.slugifier.fromHeading(line); const existingSlugEntry = existingSlugEntries.get(slug.value); if (existingSlugEntry) { ++existingSlugEntry.count; @@ -120,14 +120,14 @@ export class TableOfContents { } const headerLocation = new vscode.Location(document.uri, - new vscode.Range(lineNumber, 0, lineNumber, line.text.length)); + new vscode.Range(lineNumber, 0, lineNumber, line.length)); const headerTextLocation = new vscode.Location(document.uri, - new vscode.Range(lineNumber, line.text.match(/^#+\s*/)?.[0].length ?? 0, lineNumber, line.text.length - (line.text.match(/\s*#*$/)?.[0].length ?? 0))); + new vscode.Range(lineNumber, line.match(/^#+\s*/)?.[0].length ?? 0, lineNumber, line.length - (line.match(/\s*#*$/)?.[0].length ?? 0))); toc.push({ slug, - text: TableOfContents.getHeaderText(line.text), + text: TableOfContents.getHeaderText(line), level: TableOfContents.getHeaderLevel(heading.markup), line: lineNumber, sectionLocation: headerLocation, // Populated in next steps @@ -151,7 +151,7 @@ export class TableOfContents { sectionLocation: new vscode.Location(document.uri, new vscode.Range( entry.sectionLocation.range.start, - new vscode.Position(endLine, document.lineAt(endLine).text.length))) + new vscode.Position(endLine, getLine(document, endLine).length))) }; }); } diff --git a/extensions/markdown-language-features/src/test/documentLinkProvider.test.ts b/extensions/markdown-language-features/src/test/documentLinkProvider.test.ts index a96967bafd2..0111980e849 100644 --- a/extensions/markdown-language-features/src/test/documentLinkProvider.test.ts +++ b/extensions/markdown-language-features/src/test/documentLinkProvider.test.ts @@ -15,7 +15,7 @@ import { nulLogger } from './nulLogging'; import { assertRangeEqual, joinLines, workspacePath } from './util'; -suite.only('Markdown: MdLinkComputer', () => { +suite('Markdown: MdLinkComputer', () => { function getLinksForFile(fileContents: string): Promise { const doc = new InMemoryDocument(workspacePath('x.md'), fileContents); diff --git a/extensions/markdown-language-features/src/types/textDocument.ts b/extensions/markdown-language-features/src/types/textDocument.ts index 5c49e065d40..f45614cc0cc 100644 --- a/extensions/markdown-language-features/src/types/textDocument.ts +++ b/extensions/markdown-language-features/src/types/textDocument.ts @@ -3,15 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import type * as vscode from 'vscode'; - -/** - * Minimal version of {@link vscode.TextLine}. - */ -export interface ITextLine { - readonly text: string; - readonly isEmptyOrWhitespace: boolean; -} +import * as vscode from 'vscode'; /** * Minimal version of {@link vscode.TextDocument}. @@ -22,6 +14,9 @@ export interface ITextDocument { readonly lineCount: number; getText(range?: vscode.Range): string; - lineAt(line: number): ITextLine; positionAt(offset: number): vscode.Position; } + +export function getLine(doc: ITextDocument, line: number): string { + return doc.getText(new vscode.Range(line, 0, line, Number.MAX_VALUE)).replace(/\r?\n$/, ''); +} diff --git a/extensions/markdown-language-features/src/util/inMemoryDocument.ts b/extensions/markdown-language-features/src/util/inMemoryDocument.ts index de29de21355..f626878deff 100644 --- a/extensions/markdown-language-features/src/util/inMemoryDocument.ts +++ b/extensions/markdown-language-features/src/util/inMemoryDocument.ts @@ -5,14 +5,12 @@ import * as vscode from 'vscode'; import { TextDocument } from 'vscode-languageserver-textdocument'; -import { ITextDocument, ITextLine } from '../types/textDocument'; +import { ITextDocument } from '../types/textDocument'; export class InMemoryDocument implements ITextDocument { private readonly _doc: TextDocument; - private lines: ITextLine[] | undefined; - constructor( public readonly uri: vscode.Uri, contents: string, public readonly version = 0, @@ -25,16 +23,6 @@ export class InMemoryDocument implements ITextDocument { return this._doc.lineCount; } - lineAt(index: any): ITextLine { - if (!this.lines) { - this.lines = this._doc.getText().split(/\r?\n/).map(text => ({ - text, - get isEmptyOrWhitespace() { return /^\s*$/.test(text); } - })); - } - return this.lines[index]; - } - positionAt(offset: number): vscode.Position { const pos = this._doc.positionAt(offset); return new vscode.Position(pos.line, pos.character); diff --git a/extensions/markdown-language-features/src/util/string.ts b/extensions/markdown-language-features/src/util/string.ts new file mode 100644 index 00000000000..dd9733e9ffd --- /dev/null +++ b/extensions/markdown-language-features/src/util/string.ts @@ -0,0 +1,8 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export function isEmptyOrWhitespace(str: string): boolean { + return /^\s*$/.test(str); +}