From 46561fe89fdb12e30c52b9ee02c22789e908a858 Mon Sep 17 00:00:00 2001 From: Weirong Xu Date: Thu, 17 Mar 2022 13:55:54 +0800 Subject: [PATCH] feat(workspace): Compatible with the workspace utils class of Vscode --- src/edit/location.ts | 64 ++++++++++ src/edit/position.ts | 186 +++++++++++++++++++++++++++++ src/edit/range.ts | 182 ++++++++++++++++++++++++++++ src/edit/selection.ts | 70 +++++++++++ src/edit/textEdit.ts | 117 ++++++++++++++++++ src/markdown/baseMarkdownString.ts | 166 +++++++++++++++++++++++++ src/markdown/markdownString.ts | 75 ++++++++++++ src/util/string.ts | 7 ++ 8 files changed, 867 insertions(+) create mode 100644 src/edit/location.ts create mode 100644 src/edit/position.ts create mode 100644 src/edit/range.ts create mode 100644 src/edit/selection.ts create mode 100644 src/edit/textEdit.ts create mode 100644 src/markdown/baseMarkdownString.ts create mode 100644 src/markdown/markdownString.ts diff --git a/src/edit/location.ts b/src/edit/location.ts new file mode 100644 index 00000000000..f546fed4ab2 --- /dev/null +++ b/src/edit/location.ts @@ -0,0 +1,64 @@ +/* --------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { Location as ILocation } from 'vscode-languageserver-protocol' +import { URI } from 'vscode-uri' +import { Position } from './position' +import { Range } from './range' + +export class Location implements ILocation { + public static isLocation(thing: any): thing is ILocation { + if (thing instanceof Location) { + return true + } + if (!thing) { + return false + } + return Range.isRange((thing as Location).range) + && URI.isUri((thing as Location).uri) + } + + /** + * Creates a Location literal. + * + * @param uri The location's uri. + * @param range The location's range. + * @deprecated use `new Location(uri, range)` instead. + */ + public static create(uri: string, range: Range): Location { + return new Location(uri, range) + } + /** + * Checks whether the given literal conforms to the [Location](#Location) interface. + * + * @deprecated Use the `Location.isLocation` instead. + */ + public static is(value: any): value is ILocation { + return ILocation.is(value) + } + + public uri: string + public range!: Range + + constructor(uri: string, rangeOrPosition: Range | Position) { + this.uri = uri + + if (!rangeOrPosition) { + // that's OK + } else if (Range.isRange(rangeOrPosition)) { + this.range = Range.of(rangeOrPosition) + } else if (Position.isPosition(rangeOrPosition)) { + this.range = new Range(rangeOrPosition, rangeOrPosition) + } else { + throw new Error('Illegal argument') + } + } + + public toJSON(): any { + return { + uri: this.uri, + range: this.range + } + } +} diff --git a/src/edit/position.ts b/src/edit/position.ts new file mode 100644 index 00000000000..058d9375e4a --- /dev/null +++ b/src/edit/position.ts @@ -0,0 +1,186 @@ +/* --------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { Position as IPosition } from 'vscode-languageserver-protocol' +import { illegalArgument } from '../util/errors' + +export { IPosition } + +export class Position implements IPosition { + public static Min(...positions: Position[]): Position { + if (positions.length === 0) { + throw new TypeError() + } + let result = positions[0] + for (let i = 1; i < positions.length; i++) { + const p = positions[i] + if (p.isBefore(result!)) { + result = p + } + } + return result + } + + public static Max(...positions: Position[]): Position { + if (positions.length === 0) { + throw new TypeError() + } + let result = positions[0] + for (let i = 1; i < positions.length; i++) { + const p = positions[i] + if (p.isAfter(result!)) { + result = p + } + } + return result + } + + public static isPosition(other: any): other is Position { + if (!other) { + return false + } + if (other instanceof Position) { + return true + } + let { line, character } = other as Position + if (typeof line === 'number' && typeof character === 'number') { + return true + } + return false + } + + public static of(obj: IPosition): Position { + if (obj instanceof Position) { + return obj + } else if (this.isPosition(obj)) { + return new Position(obj.line, obj.character) + } + throw new Error('Invalid argument, is NOT a position-like object') + } + + private _line: number + private _character: number + + public get line(): number { + return this._line + } + + public get character(): number { + return this._character + } + + constructor(line: number, character: number) { + if (line < 0) { + throw illegalArgument('line must be non-negative') + } + if (character < 0) { + throw illegalArgument('character must be non-negative') + } + this._line = line + this._character = character + } + + public isBefore(other: Position): boolean { + if (this._line < other._line) { + return true + } + if (other._line < this._line) { + return false + } + return this._character < other._character + } + + public isBeforeOrEqual(other: Position): boolean { + if (this._line < other._line) { + return true + } + if (other._line < this._line) { + return false + } + return this._character <= other._character + } + + public isAfter(other: Position): boolean { + return !this.isBeforeOrEqual(other) + } + + public isAfterOrEqual(other: Position): boolean { + return !this.isBefore(other) + } + + public isEqual(other: Position): boolean { + return this._line === other._line && this._character === other._character + } + + public compareTo(other: Position): number { + if (this._line < other._line) { + return -1 + } else if (this._line > other.line) { + return 1 + } else { + // equal line + if (this._character < other._character) { + return -1 + } else if (this._character > other._character) { + return 1 + } else { + // equal line and character + return 0 + } + } + } + + public translate(change: { lineDelta?: number; characterDelta?: number }): Position + public translate(lineDelta?: number, characterDelta?: number): Position + public translate(lineDeltaOrChange: number | undefined | { lineDelta?: number; characterDelta?: number }, characterDelta = 0): Position { + + if (lineDeltaOrChange === null || characterDelta === null) { + throw illegalArgument() + } + + let lineDelta: number + if (typeof lineDeltaOrChange === 'undefined') { + lineDelta = 0 + } else if (typeof lineDeltaOrChange === 'number') { + lineDelta = lineDeltaOrChange + } else { + lineDelta = typeof lineDeltaOrChange.lineDelta === 'number' ? lineDeltaOrChange.lineDelta : 0 + characterDelta = typeof lineDeltaOrChange.characterDelta === 'number' ? lineDeltaOrChange.characterDelta : 0 + } + + if (lineDelta === 0 && characterDelta === 0) { + return this + } + return new Position(this.line + lineDelta, this.character + characterDelta) + } + + public with(change: { line?: number; character?: number }): Position + public with(line?: number, character?: number): Position + public with(lineOrChange: number | undefined | { line?: number; character?: number }, character: number = this.character): Position { + if (lineOrChange === null || character === null) { + throw illegalArgument() + } + + let line: number + if (typeof lineOrChange === 'undefined') { + line = this.line + + } else if (typeof lineOrChange === 'number') { + line = lineOrChange + + } else { + line = typeof lineOrChange.line === 'number' ? lineOrChange.line : this.line + character = typeof lineOrChange.character === 'number' ? lineOrChange.character : this.character + } + + if (line === this.line && character === this.character) { + return this + } + return new Position(line, character) + } + + public toJSON(): any { + return { line: this.line, character: this.character } + } +} diff --git a/src/edit/range.ts b/src/edit/range.ts new file mode 100644 index 00000000000..83a29e09c27 --- /dev/null +++ b/src/edit/range.ts @@ -0,0 +1,182 @@ +/* --------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { Range as IRange } from 'vscode-languageserver-protocol' +import { illegalArgument } from '../util/errors' +import { IPosition, Position } from './position' + +export class Range implements IRange { + public static isRange(thing: any): thing is IRange { + if (thing instanceof Range) { + return true + } + if (!thing) { + return false + } + return Position.isPosition((thing as Range).start) + && Position.isPosition(thing.end) + } + + public static of(obj: IRange): Range { + if (obj instanceof Range) { + return obj + } + if (this.isRange(obj)) { + return new Range(obj.start, obj.end) + } + throw new Error('Invalid argument, is NOT a range-like object') + } + + /** + * Create a new Range liternal. + * + * @param start The range's start position. + * @param end The range's end position. + * @deprecated use `new Range(start, end)` instead. + */ + public static create(start: Position, end: Position): Range + /** + * Create a new Range liternal. + * + * @param startLine The start line number. + * @param startCharacter The start character. + * @param endLine The end line number. + * @param endCharacter The end character. + * @deprecated use `new Range(startLine, startCharacter, endLine, endCharacter)` instead. + */ + public static create(startLine: number, startCharacter: number, endLine: number, endCharacter: number): Range + public static create(startLineOrStart: number | Position | IPosition, startColumnOrEnd: number | Position | IPosition, endLine?: number, endColumn?: number): Range { + return new Range(startLineOrStart as number, startColumnOrEnd as number, endLine, endColumn) + } + + /** + * Checks whether the given literal conforms to the [Range](#Range) interface. + * + * @deprecated Use the `Range.isRange` instead. + */ + public is(value: any): value is IRange { + return IRange.is(value) + } + + protected _start: Position + protected _end: Position + + public get start(): Position { + return this._start + } + + public get end(): Position { + return this._end + } + + constructor(start: IPosition, end: IPosition) + constructor(start: Position, end: Position) + constructor(startLine: number, startColumn: number, endLine: number, endColumn: number) + constructor(startLineOrStart: number | Position | IPosition, startColumnOrEnd: number | Position | IPosition, endLine?: number, endColumn?: number) { + let start: Position | undefined + let end: Position | undefined + + if (typeof startLineOrStart === 'number' && typeof startColumnOrEnd === 'number' && typeof endLine === 'number' && typeof endColumn === 'number') { + start = new Position(startLineOrStart, startColumnOrEnd) + end = new Position(endLine, endColumn) + } else if (Position.isPosition(startLineOrStart) && Position.isPosition(startColumnOrEnd)) { + start = Position.of(startLineOrStart) + end = Position.of(startColumnOrEnd) + } + + if (!start || !end) { + throw new Error('Invalid arguments') + } + + if (start.isBefore(end)) { + this._start = start + this._end = end + } else { + this._start = end + this._end = start + } + } + + public contains(positionOrRange: Position | Range): boolean { + if (Range.isRange(positionOrRange)) { + return this.contains(positionOrRange.start) + && this.contains(positionOrRange.end) + + } else if (Position.isPosition(positionOrRange)) { + if (Position.of(positionOrRange).isBefore(this._start)) { + return false + } + if (this._end.isBefore(positionOrRange)) { + return false + } + return true + } + return false + } + + public isEqual(other: Range): boolean { + return this._start.isEqual(other._start) && this._end.isEqual(other._end) + } + + public intersection(other: Range): Range | undefined { + const start = Position.Max(other.start, this._start) + const end = Position.Min(other.end, this._end) + if (start.isAfter(end)) { + // this happens when there is no overlap: + // |-----| + // |----| + return undefined + } + return new Range(start, end) + } + + public union(other: Range): Range { + if (this.contains(other)) { + return this + } else if (other.contains(this)) { + return other + } + const start = Position.Min(other.start, this._start) + const end = Position.Max(other.end, this.end) + return new Range(start, end) + } + + public get isEmpty(): boolean { + return this._start.isEqual(this._end) + } + + public get isSingleLine(): boolean { + return this._start.line === this._end.line + } + + public with(change: { start?: Position; end?: Position }): Range + public with(start?: Position, end?: Position): Range + public with(startOrChange: Position | undefined | { start?: Position; end?: Position }, end: Position = this.end): Range { + + if (startOrChange === null || end === null) { + throw illegalArgument() + } + + let start: Position + if (!startOrChange) { + start = this.start + + } else if (Position.isPosition(startOrChange)) { + start = startOrChange + + } else { + start = startOrChange.start || this.start + end = startOrChange.end || this.end + } + + if (start.isEqual(this._start) && end.isEqual(this.end)) { + return this + } + return new Range(start, end) + } + + public toJSON(): any { + return [this.start, this.end] + } +} diff --git a/src/edit/selection.ts b/src/edit/selection.ts new file mode 100644 index 00000000000..6176cad2a57 --- /dev/null +++ b/src/edit/selection.ts @@ -0,0 +1,70 @@ +/* --------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { Position } from "./position" +import { Range } from "./range" + +export class Selection extends Range { + public static isSelection(thing: any): thing is Selection { + if (thing instanceof Selection) { + return true + } + if (!thing) { + return false + } + return Range.isRange(thing) + && Position.isPosition((thing as Selection).anchor) + && Position.isPosition((thing as Selection).active) + && typeof (thing as Selection).isReversed === 'boolean' + } + + private _anchor: Position + + public get anchor(): Position { + return this._anchor + } + + private _active: Position + + public get active(): Position { + return this._active + } + + constructor(anchor: Position, active: Position) + constructor(anchorLine: number, anchorColumn: number, activeLine: number, activeColumn: number) + constructor(anchorLineOrAnchor: number | Position, anchorColumnOrActive: number | Position, activeLine?: number, activeColumn?: number) { + let anchor: Position | undefined + let active: Position | undefined + + if (typeof anchorLineOrAnchor === 'number' && typeof anchorColumnOrActive === 'number' && typeof activeLine === 'number' && typeof activeColumn === 'number') { + anchor = new Position(anchorLineOrAnchor, anchorColumnOrActive) + active = new Position(activeLine, activeColumn) + } else if (Position.isPosition(anchorLineOrAnchor) && Position.isPosition(anchorColumnOrActive)) { + anchor = Position.of(anchorLineOrAnchor) + active = Position.of(anchorColumnOrActive) + } + + if (!anchor || !active) { + throw new Error('Invalid arguments') + } + + super(anchor, active) + + this._anchor = anchor + this._active = active + } + + public get isReversed(): boolean { + return this._anchor === this._end + } + + public override toJSON() { + return { + start: this.start, + end: this.end, + active: this.active, + anchor: this.anchor + } + } +} diff --git a/src/edit/textEdit.ts b/src/edit/textEdit.ts new file mode 100644 index 00000000000..1ba2a37b262 --- /dev/null +++ b/src/edit/textEdit.ts @@ -0,0 +1,117 @@ +/* --------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { TextEdit as ITextEdit } from 'vscode-languageserver-protocol' +import { illegalArgument } from '../util/errors' +import { Position } from './position' +import { Range } from './range' + +export enum EndOfLine { + LF = 1, + CRLF = 2 +} + +export enum EnvironmentVariableMutatorType { + Replace = 1, + Append = 2, + Prepend = 3 +} + +export class TextEdit implements ITextEdit { + public static isTextEdit(thing: any): thing is TextEdit { + if (thing instanceof TextEdit) { + return true + } + if (!thing) { + return false + } + return Range.isRange((thing as TextEdit)) + && typeof (thing as TextEdit).newText === 'string' + } + + public static replace(range: Range, newText: string): TextEdit { + return new TextEdit(range, newText) + } + + public static insert(position: Position, newText: string): TextEdit { + return TextEdit.replace(new Range(position, position), newText) + } + + public static delete(range: Range): TextEdit { + return TextEdit.replace(range, '') + } + + /** + * Creates a delete text edit. + * + * @param range The range of text to be deleted. + * @deprecated use `TextEdit.delete(range)` instead. + */ + public static del(range: Range): ITextEdit { + return new TextEdit(range, null) + } + + /** + * @deprecated use `TextEdit.isTextEdit(value)` instead. + */ + public static is(value: any): value is ITextEdit { + return ITextEdit.is(value) + } + + public static setEndOfLine(eol: EndOfLine): TextEdit { + const ret = new TextEdit(new Range(new Position(0, 0), new Position(0, 0)), '') + ret.newEol = eol + return ret + } + + protected _range: Range + protected _newText: string | null + protected _newEol?: EndOfLine + + public get range(): Range { + return this._range + } + + public set range(value: Range) { + if (value && !Range.isRange(value)) { + throw illegalArgument('range') + } + this._range = value + } + + public get newText(): string { + return this._newText || '' + } + + public set newText(value: string) { + if (value && typeof value !== 'string') { + throw illegalArgument('newText') + } + this._newText = value + } + + public get newEol(): EndOfLine | undefined { + return this._newEol + } + + public set newEol(value: EndOfLine | undefined) { + if (value && typeof value !== 'number') { + throw illegalArgument('newEol') + } + this._newEol = value + } + + constructor(range: Range, newText: string | null) { + this._range = range + this._newText = newText + } + + public toJSON(): any { + return { + range: this.range, + newText: this.newText, + newEol: this._newEol + } + } +} diff --git a/src/markdown/baseMarkdownString.ts b/src/markdown/baseMarkdownString.ts new file mode 100644 index 00000000000..7190419d499 --- /dev/null +++ b/src/markdown/baseMarkdownString.ts @@ -0,0 +1,166 @@ +/* --------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { URI, UriComponents } from "vscode-uri" +import { illegalArgument } from '../util/errors' +import { escapeRegExpCharacters } from '../util/string' + +export interface IMarkdownString { + readonly value: string; + readonly isTrusted?: boolean; + readonly supportThemeIcons?: boolean; + readonly supportHtml?: boolean; + readonly baseUri?: UriComponents; + uris?: { [href: string]: UriComponents }; +} + +export const enum MarkdownStringTextNewlineStyle { + Paragraph = 0, + Break = 1, +} + +export class BaseMarkdownString implements IMarkdownString { + + public value: string + public isTrusted?: boolean + public supportThemeIcons?: boolean + public supportHtml?: boolean + public baseUri?: URI + + constructor( + value = '', + isTrustedOrOptions: boolean | { isTrusted?: boolean; supportThemeIcons?: boolean; supportHtml?: boolean } = false, + ) { + this.value = value + if (typeof this.value !== 'string') { + throw illegalArgument('value') + } + + if (typeof isTrustedOrOptions === 'boolean') { + this.isTrusted = isTrustedOrOptions + this.supportThemeIcons = false + this.supportHtml = false + } + else { + this.isTrusted = isTrustedOrOptions.isTrusted ?? undefined + this.supportThemeIcons = isTrustedOrOptions.supportThemeIcons ?? false + this.supportHtml = isTrustedOrOptions.supportHtml ?? false + } + } + + public appendText(value: string, newlineStyle: MarkdownStringTextNewlineStyle = MarkdownStringTextNewlineStyle.Paragraph): BaseMarkdownString { + this.value += escapeMarkdownSyntaxTokens(value) + .replace(/([ \t]+)/g, (_match, g1) => ' '.repeat(g1.length)) + .replace(/>/gm, '\\>') + .replace(/\n/g, newlineStyle === MarkdownStringTextNewlineStyle.Break ? '\\\n' : '\n\n') + + return this + } + + public appendMarkdown(value: string): BaseMarkdownString { + this.value += value + return this + } + + public appendCodeblock(langId: string, code: string): BaseMarkdownString { + this.value += '\n```' + this.value += langId + this.value += '\n' + this.value += code + this.value += '\n```\n' + return this + } + + public appendLink(target: URI | string, label: string, title?: string): BaseMarkdownString { + this.value += '[' + this.value += this._escape(label, ']') + this.value += '](' + this.value += this._escape(String(target), ')') + if (title) { + this.value += ` "${this._escape(this._escape(title, '"'), ')')}"` + } + this.value += ')' + return this + } + + private _escape(value: string, ch: string): string { + const r = new RegExp(escapeRegExpCharacters(ch), 'g') + return value.replace(r, (match, offset) => { + if (value.charAt(offset - 1) !== '\\') { + return `\\${match}` + } else { + return match + } + }) + } +} + +export function isEmptyMarkdownString(oneOrMany: IMarkdownString | IMarkdownString[] | null | undefined): boolean { + if (isMarkdownString(oneOrMany)) { + return !oneOrMany.value + } else if (Array.isArray(oneOrMany)) { + return oneOrMany.every(isEmptyMarkdownString) + } else { + return true + } +} + +export function isMarkdownString(thing: any): thing is IMarkdownString { + if (thing instanceof BaseMarkdownString) { + return true + } else if (thing && typeof thing === 'object') { + return typeof (thing as IMarkdownString).value === 'string' + && (typeof (thing as IMarkdownString).isTrusted === 'boolean' || (thing as IMarkdownString).isTrusted === undefined) + && (typeof (thing as IMarkdownString).supportThemeIcons === 'boolean' || (thing as IMarkdownString).supportThemeIcons === undefined) + } + return false +} + +export function markdownStringEqual(a: IMarkdownString, b: IMarkdownString): boolean { + if (a === b) { + return true + } else if (!a || !b) { + return false + } else { + return a.value === b.value + && a.isTrusted === b.isTrusted + && a.supportThemeIcons === b.supportThemeIcons + && a.supportHtml === b.supportHtml + && (a.baseUri === b.baseUri || !!a.baseUri && !!b.baseUri && URI.from(a.baseUri).fsPath === URI.from(b.baseUri).fsPath) + } +} + +export function escapeMarkdownSyntaxTokens(text: string): string { + // escape markdown syntax tokens: http://daringfireball.net/projects/markdown/syntax#backslash + return text.replace(/[\\`*_{}[\]()#+\-!]/g, '\\$&') +} + +export function removeMarkdownEscapes(text: string): string { + if (!text) { + return text + } + return text.replace(/\\([\\`*_{}[\]()#+\-.!])/g, '$1') +} + +export function parseHrefAndDimensions(href: string): { href: string; dimensions: string[] } { + const dimensions: string[] = [] + const splitted = href.split('|').map(s => s.trim()) + href = splitted[0] + const parameters = splitted[1] + if (parameters) { + const heightFromParams = /height=(\d+)/.exec(parameters) + const widthFromParams = /width=(\d+)/.exec(parameters) + const height = heightFromParams ? heightFromParams[1] : '' + const width = widthFromParams ? widthFromParams[1] : '' + const widthIsFinite = isFinite(parseInt(width, 10)) + const heightIsFinite = isFinite(parseInt(height, 10)) + if (widthIsFinite) { + dimensions.push(`width="${width}"`) + } + if (heightIsFinite) { + dimensions.push(`height="${height}"`) + } + } + return { href, dimensions } +} diff --git a/src/markdown/markdownString.ts b/src/markdown/markdownString.ts new file mode 100644 index 00000000000..5899abfae88 --- /dev/null +++ b/src/markdown/markdownString.ts @@ -0,0 +1,75 @@ +/* --------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { URI } from "vscode-uri" +import { BaseMarkdownString } from "./baseMarkdownString" + +export class MarkdownString { + readonly #delegate: BaseMarkdownString + + public static isMarkdownString(thing: any): thing is MarkdownString { + if (thing instanceof MarkdownString) { + return true + } + return thing && thing.appendCodeblock && thing.appendMarkdown && thing.appendText && (thing.value !== undefined) + } + + constructor(value?: string) { + this.#delegate = new BaseMarkdownString(value) + } + + public get value(): string { + return this.#delegate.value + } + public set value(value: string) { + this.#delegate.value = value + } + + public get isTrusted(): boolean | undefined { + return this.#delegate.isTrusted + } + + public set isTrusted(value: boolean | undefined) { + this.#delegate.isTrusted = value + } + + public get supportThemeIcons(): boolean | undefined { + return this.#delegate.supportThemeIcons + } + + public set supportThemeIcons(value: boolean | undefined) { + this.#delegate.supportThemeIcons = value + } + + public get supportHtml(): boolean | undefined { + return this.#delegate.supportHtml + } + + public set supportHtml(value: boolean | undefined) { + this.#delegate.supportHtml = value + } + + public get baseUri(): URI | undefined { + return this.#delegate.baseUri + } + + public set baseUri(value: URI | undefined) { + this.#delegate.baseUri = value + } + + public appendText(value: string): MarkdownString { + this.#delegate.appendText(value) + return this + } + + public appendMarkdown(value: string): MarkdownString { + this.#delegate.appendMarkdown(value) + return this + } + + public appendCodeblock(value: string, language?: string): MarkdownString { + this.#delegate.appendCodeblock(language ?? '', value) + return this + } +} diff --git a/src/util/string.ts b/src/util/string.ts index 3eb386c64ad..831cc72fa42 100644 --- a/src/util/string.ts +++ b/src/util/string.ts @@ -114,6 +114,13 @@ function doEqualsIgnoreCase(a: string, b: string, stopAt = a.length): boolean { return true } +/** + * Escapes regular expression characters in a given string + */ +export function escapeRegExpCharacters(value: string): string { + return value.replace(/[\\{}*+?|^$.[\]()]/g, '\\$&') +} + export function equalsIgnoreCase(a: string, b: string): boolean { const len1 = a ? a.length : 0 const len2 = b ? b.length : 0