From 8c534be76fb3c113962464ba03e849fe35986502 Mon Sep 17 00:00:00 2001 From: Weirong Xu Date: Sat, 7 May 2022 12:30:17 +0800 Subject: [PATCH] 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 137e1bd896d..10a733f496a 100644 --- a/src/window.ts +++ b/src/window.ts @@ -2,12 +2,13 @@ import { Neovim } from '@chemzqm/neovim' import fs from 'fs' import path from 'path' -import { CancellationToken, Disposable, Emitter, Event, Position, Range } from 'vscode-languageserver-protocol' +import { CancellationToken, Disposable, 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 { Position } from './edit/position' import events from './events' import Dialog, { DialogConfig, DialogPreferences } from './model/dialog' import Menu, { isMenuItem, MenuItem } from './model/menu' @@ -452,8 +453,9 @@ 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) } /** @@ -461,7 +463,7 @@ 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) }