From 0e3b5add438a183d6371682eb893065b1118e3fc Mon Sep 17 00:00:00 2001 From: Weirong Xu Date: Thu, 17 Mar 2022 13:55:54 +0800 Subject: [PATCH 1/3] 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 22aed879057..bc51d99b901 100644 --- a/src/util/string.ts +++ b/src/util/string.ts @@ -134,6 +134,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 From 3542f9e67629fe1814ed323820320a996c5818da Mon Sep 17 00:00:00 2001 From: Weirong Xu Date: Fri, 18 Mar 2022 10:50:25 +0800 Subject: [PATCH 2/3] fix: markdownString must implements MarkupContent --- src/edit/textEdit.ts | 4 ++-- src/markdown/markdownString.ts | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/edit/textEdit.ts b/src/edit/textEdit.ts index 1ba2a37b262..cad9cc9446d 100644 --- a/src/edit/textEdit.ts +++ b/src/edit/textEdit.ts @@ -14,8 +14,8 @@ export enum EndOfLine { export enum EnvironmentVariableMutatorType { Replace = 1, - Append = 2, - Prepend = 3 + Append = 2, + Prepend = 3 } export class TextEdit implements ITextEdit { diff --git a/src/markdown/markdownString.ts b/src/markdown/markdownString.ts index 5899abfae88..30663402495 100644 --- a/src/markdown/markdownString.ts +++ b/src/markdown/markdownString.ts @@ -2,10 +2,13 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { MarkupContent, MarkupKind } from "vscode-languageserver-protocol" import { URI } from "vscode-uri" import { BaseMarkdownString } from "./baseMarkdownString" -export class MarkdownString { +export class MarkdownString implements MarkupContent { + public kind: MarkupKind = MarkupKind.Markdown + readonly #delegate: BaseMarkdownString public static isMarkdownString(thing: any): thing is MarkdownString { From 20c0216c5e2ed9c6f588d5c671bb4d7a04739c36 Mon Sep 17 00:00:00 2001 From: Weirong Xu Date: Sat, 7 May 2022 12:30:17 +0800 Subject: [PATCH 3/3] feat: using class Position instance instead of plain object --- src/edit/location.ts | 2 +- src/edit/position.ts | 17 +++ src/edit/range.ts | 4 +- src/edit/selection.ts | 6 +- src/edit/textEdit.ts | 2 +- src/edit/workspaceEdit.ts | 177 +++++++++++++++++++++++++++++ src/markdown/baseMarkdownString.ts | 1 - src/model/textdocument.ts | 13 ++- src/window.ts | 10 +- 9 files changed, 214 insertions(+), 18 deletions(-) create mode 100644 src/edit/workspaceEdit.ts diff --git a/src/edit/location.ts b/src/edit/location.ts index f546fed4ab2..c22be999d9f 100644 --- a/src/edit/location.ts +++ b/src/edit/location.ts @@ -16,7 +16,7 @@ export class Location implements ILocation { return false } return Range.isRange((thing as Location).range) - && URI.isUri((thing as Location).uri) + && URI.isUri((thing as Location).uri) } /** diff --git a/src/edit/position.ts b/src/edit/position.ts index 058d9375e4a..110623054fe 100644 --- a/src/edit/position.ts +++ b/src/edit/position.ts @@ -59,6 +59,23 @@ export class Position implements IPosition { throw new Error('Invalid argument, is NOT a position-like object') } + /** + * Creates a new Position literal from the given line and character. + * + * @param line The position's line. + * @param character The position's character. + */ + public static create(line: number, character: number): Position { + return new Position(line, character) + } + + /** + * Checks whether the given liternal conforms to the [Position](#Position) interface. + */ + public static is(value: any): value is IPosition { + return IPosition.is(value) + } + private _line: number private _character: number diff --git a/src/edit/range.ts b/src/edit/range.ts index 83a29e09c27..3ed4d1c9ce0 100644 --- a/src/edit/range.ts +++ b/src/edit/range.ts @@ -15,7 +15,7 @@ export class Range implements IRange { return false } return Position.isPosition((thing as Range).start) - && Position.isPosition(thing.end) + && Position.isPosition(thing.end) } public static of(obj: IRange): Range { @@ -101,7 +101,7 @@ export class Range implements IRange { public contains(positionOrRange: Position | Range): boolean { if (Range.isRange(positionOrRange)) { return this.contains(positionOrRange.start) - && this.contains(positionOrRange.end) + && this.contains(positionOrRange.end) } else if (Position.isPosition(positionOrRange)) { if (Position.of(positionOrRange).isBefore(this._start)) { diff --git a/src/edit/selection.ts b/src/edit/selection.ts index 6176cad2a57..41aa8572cf9 100644 --- a/src/edit/selection.ts +++ b/src/edit/selection.ts @@ -14,9 +14,9 @@ export class Selection extends Range { return false } return Range.isRange(thing) - && Position.isPosition((thing as Selection).anchor) - && Position.isPosition((thing as Selection).active) - && typeof (thing as Selection).isReversed === 'boolean' + && Position.isPosition((thing as Selection).anchor) + && Position.isPosition((thing as Selection).active) + && typeof (thing as Selection).isReversed === 'boolean' } private _anchor: Position diff --git a/src/edit/textEdit.ts b/src/edit/textEdit.ts index cad9cc9446d..b918d477ca6 100644 --- a/src/edit/textEdit.ts +++ b/src/edit/textEdit.ts @@ -27,7 +27,7 @@ export class TextEdit implements ITextEdit { return false } return Range.isRange((thing as TextEdit)) - && typeof (thing as TextEdit).newText === 'string' + && typeof (thing as TextEdit).newText === 'string' } public static replace(range: Range, newText: string): TextEdit { diff --git a/src/edit/workspaceEdit.ts b/src/edit/workspaceEdit.ts new file mode 100644 index 00000000000..f265784fe32 --- /dev/null +++ b/src/edit/workspaceEdit.ts @@ -0,0 +1,177 @@ +/* --------------------------------------------------------------------------------------------- + * 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 { Position } from "./position" +import { Range } from "./range" +import { TextEdit } from "./textEdit" + +/** + * Remove all falsy values from `array`. The original array IS modified. + */ +function coalesceInPlace(array: Array): void { + let to = 0 + for (let i = 0; i < array.length; i++) { + if (array[i]) { + array[to] = array[i] + to += 1 + } + } + array.length = to +} + +/** + * Additional data for entries of a workspace edit. Supports to label entries and marks entries + * as needing confirmation by the user. The editor groups edits with equal labels into tree nodes, + * for instance all edits labelled with "Changes in Strings" would be a tree node. + */ +export interface WorkspaceEditEntryMetadata { + + /** + * A flag which indicates that user confirmation is needed. + */ + needsConfirmation: boolean; + + /** + * A human-readable string which is rendered prominent. + */ + label: string; + + /** + * A human-readable string which is rendered less prominent on the same line. + */ + description?: string; + + /** + * The icon path or {@link ThemeIcon} for the edit. + */ + iconPath?: URI | { light: URI; dark: URI }; +} + +export interface IFileOperationOptions { + overwrite?: boolean; + ignoreIfExists?: boolean; + ignoreIfNotExists?: boolean; + recursive?: boolean; +} + +export const enum FileEditType { + File = 1, + Text = 2, + Cell = 3, + CellReplace = 5, +} + +export interface IFileOperation { + _type: FileEditType.File; + from?: URI; + to?: URI; + options?: IFileOperationOptions; + metadata?: WorkspaceEditEntryMetadata; +} + +export interface IFileTextEdit { + _type: FileEditType.Text; + uri: URI; + edit: TextEdit; + metadata?: WorkspaceEditEntryMetadata; +} + +type WorkspaceEditEntry = IFileOperation | IFileTextEdit + +export class WorkspaceEdit { + + private readonly _edits: WorkspaceEditEntry[] = [] + + public _allEntries(): ReadonlyArray { + return this._edits + } + + // --- file + + public renameFile(from: URI, to: URI, options?: { overwrite?: boolean; ignoreIfExists?: boolean }, metadata?: WorkspaceEditEntryMetadata): void { + this._edits.push({ _type: FileEditType.File, from, to, options, metadata }) + } + + public createFile(uri: URI, options?: { overwrite?: boolean; ignoreIfExists?: boolean }, metadata?: WorkspaceEditEntryMetadata): void { + this._edits.push({ _type: FileEditType.File, from: undefined, to: uri, options, metadata }) + } + + public deleteFile(uri: URI, options?: { recursive?: boolean; ignoreIfNotExists?: boolean }, metadata?: WorkspaceEditEntryMetadata): void { + this._edits.push({ _type: FileEditType.File, from: uri, to: undefined, options, metadata }) + } + + // --- text + + public replace(uri: URI, range: Range, newText: string, metadata?: WorkspaceEditEntryMetadata): void { + this._edits.push({ _type: FileEditType.Text, uri, edit: new TextEdit(range, newText), metadata }) + } + + public insert(resource: URI, position: Position, newText: string, metadata?: WorkspaceEditEntryMetadata): void { + this.replace(resource, new Range(position, position), newText, metadata) + } + + public delete(resource: URI, range: Range, metadata?: WorkspaceEditEntryMetadata): void { + this.replace(resource, range, '', metadata) + } + + // --- text (Maplike) + + public has(uri: URI): boolean { + return this._edits.some(edit => edit._type === FileEditType.Text && edit.uri.toString() === uri.toString()) + } + + public set(uri: URI, edits: TextEdit[]): void { + if (!edits) { + // remove all text edits for `uri` + for (let i = 0; i < this._edits.length; i++) { + const element = this._edits[i] + if (element._type === FileEditType.Text && element.uri.toString() === uri.toString()) { + this._edits[i] = undefined! // will be coalesced down below + } + } + coalesceInPlace(this._edits) + } else { + // append edit to the end + for (const edit of edits) { + if (edit) { + this._edits.push({ _type: FileEditType.Text, uri, edit }) + } + } + } + } + + public get(uri: URI): TextEdit[] { + const res: TextEdit[] = [] + for (let candidate of this._edits) { + if (candidate._type === FileEditType.Text && candidate.uri.toString() === uri.toString()) { + res.push(candidate.edit) + } + } + return res + } + + public entries(): [URI, TextEdit[]][] { + const textEdits = new ResourceMap<[URI, TextEdit[]]>() + for (let candidate of this._edits) { + if (candidate._type === FileEditType.Text) { + let textEdit = textEdits.get(candidate.uri) + if (!textEdit) { + textEdit = [candidate.uri, []] + textEdits.set(candidate.uri, textEdit) + } + textEdit[1].push(candidate.edit) + } + } + return [...textEdits.values()] + } + + public get size(): number { + return this.entries().length + } + + public toJSON(): any { + return this.entries() + } +} diff --git a/src/markdown/baseMarkdownString.ts b/src/markdown/baseMarkdownString.ts index 7190419d499..106d9c12fce 100644 --- a/src/markdown/baseMarkdownString.ts +++ b/src/markdown/baseMarkdownString.ts @@ -21,7 +21,6 @@ export const enum MarkdownStringTextNewlineStyle { } export class BaseMarkdownString implements IMarkdownString { - public value: string public isTrusted?: boolean public supportThemeIcons?: boolean diff --git a/src/model/textdocument.ts b/src/model/textdocument.ts index 19dae86f2ae..5b075136350 100644 --- a/src/model/textdocument.ts +++ b/src/model/textdocument.ts @@ -1,7 +1,8 @@ 'use strict' -import { Position, Range } from 'vscode-languageserver-protocol' +import { Position as IPosition, Range } from 'vscode-languageserver-protocol' import { TextDocument } from 'vscode-languageserver-textdocument' import { TextLine } from './textline' +import { Position } from '../edit/position' export function computeLinesOffsets(lines: ReadonlyArray, eol: boolean): number[] { const result: number[] = [] @@ -72,8 +73,8 @@ export class LinesTextDocument implements TextDocument { return this.content } - public lineAt(lineOrPos: number | Position): TextLine { - const line = Position.is(lineOrPos) ? lineOrPos.line : lineOrPos + public lineAt(lineOrPos: number | IPosition): TextLine { + const line = IPosition.is(lineOrPos) ? lineOrPos.line : lineOrPos if (typeof line !== 'number' || line < 0 || line >= this.lineCount || @@ -90,7 +91,7 @@ export class LinesTextDocument implements TextDocument { let low = 0 let high = lineOffsets.length if (high === 0) { - return { line: 0, character: offset } + return new Position(0, offset) } while (low < high) { let mid = Math.floor((low + high) / 2) @@ -103,10 +104,10 @@ export class LinesTextDocument implements TextDocument { // low is the least x for which the line offset is larger than the current offset // or array.length if no line offset is larger than the current offset let line = low - 1 - return { line, character: offset - lineOffsets[line] } + return new Position(line, offset - lineOffsets[line]) } - public offsetAt(position: Position) { + public offsetAt(position: IPosition) { let lineOffsets = this.getLineOffsets() if (position.line >= lineOffsets.length) { return this.content.length diff --git a/src/window.ts b/src/window.ts index 8be9ffd2061..f09bc1c5cfc 100644 --- a/src/window.ts +++ b/src/window.ts @@ -2,13 +2,14 @@ import { Neovim } from '@chemzqm/neovim' import fs from 'fs' import path from 'path' -import { CancellationToken, Emitter, Event, Position, Range } from 'vscode-languageserver-protocol' +import { CancellationToken, Emitter, Event, Position as IPosition, Range } from 'vscode-languageserver-protocol' import { URI } from 'vscode-uri' import channels from './core/channels' import { TextEditor } from './core/editors' import Terminals from './core/terminals' import * as ui from './core/ui' import Cursors from './cursors/index' +import { Position } from './edit/position' import events from './events' import languages from './languages' import Dialog, { DialogConfig, DialogPreferences } from './model/dialog' @@ -471,8 +472,9 @@ export class Window { * * @returns Cursor position. */ - public getCursorPosition(): Promise { - return ui.getCursorPosition(this.nvim) + public async getCursorPosition(): Promise { + const position = await ui.getCursorPosition(this.nvim) + return new Position(position.line, position.character) } /** @@ -480,7 +482,7 @@ export class Window { * * @param position LSP position. */ - public async moveTo(position: Position): Promise { + public async moveTo(position: IPosition): Promise { await ui.moveTo(this.nvim, position, workspace.env.isVim) }