Path: blob/main/src/vs/workbench/api/common/extHostDocumentData.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { ok } from '../../../base/common/assert.js';6import { Schemas } from '../../../base/common/network.js';7import { regExpLeadsToEndlessLoop } from '../../../base/common/strings.js';8import { URI, UriComponents } from '../../../base/common/uri.js';9import { MirrorTextModel } from '../../../editor/common/model/mirrorTextModel.js';10import { ensureValidWordDefinition, getWordAtText } from '../../../editor/common/core/wordHelper.js';11import type * as vscode from 'vscode';12import { equals } from '../../../base/common/arrays.js';13import { EndOfLine } from './extHostTypes/textEdit.js';14import { Position } from './extHostTypes/position.js';15import { Range } from './extHostTypes/range.js';1617const _languageId2WordDefinition = new Map<string, RegExp>();18export function setWordDefinitionFor(languageId: string, wordDefinition: RegExp | undefined): void {19if (!wordDefinition) {20_languageId2WordDefinition.delete(languageId);21} else {22_languageId2WordDefinition.set(languageId, wordDefinition);23}24}2526function getWordDefinitionFor(languageId: string): RegExp | undefined {27return _languageId2WordDefinition.get(languageId);28}2930export interface IExtHostDocumentSaveDelegate {31$trySaveDocument(uri: UriComponents): Promise<boolean>;32}3334export class ExtHostDocumentData extends MirrorTextModel {3536private _document?: vscode.TextDocument;37private _isDisposed: boolean = false;3839constructor(40private readonly _proxy: IExtHostDocumentSaveDelegate,41uri: URI, lines: string[], eol: string, versionId: number,42private _languageId: string,43private _isDirty: boolean,44private _encoding: string,45private readonly _strictInstanceofChecks = true // used for code reuse46) {47super(uri, lines, eol, versionId);48}4950// eslint-disable-next-line local/code-must-use-super-dispose51override dispose(): void {52// we don't really dispose documents but let53// extensions still read from them. some54// operations, live saving, will now error tho55ok(!this._isDisposed);56this._isDisposed = true;57this._isDirty = false;58}5960equalLines(lines: readonly string[]): boolean {61return equals(this._lines, lines);62}6364get document(): vscode.TextDocument {65if (!this._document) {66const that = this;67this._document = {68get uri() { return that._uri; },69get fileName() { return that._uri.fsPath; },70get isUntitled() { return that._uri.scheme === Schemas.untitled; },71get languageId() { return that._languageId; },72get version() { return that._versionId; },73get isClosed() { return that._isDisposed; },74get isDirty() { return that._isDirty; },75get encoding() { return that._encoding; },76save() { return that._save(); },77getText(range?) { return range ? that._getTextInRange(range) : that.getText(); },78get eol() { return that._eol === '\n' ? EndOfLine.LF : EndOfLine.CRLF; },79get lineCount() { return that._lines.length; },80lineAt(lineOrPos: number | vscode.Position) { return that._lineAt(lineOrPos); },81offsetAt(pos) { return that._offsetAt(pos); },82positionAt(offset) { return that._positionAt(offset); },83validateRange(ran) { return that._validateRange(ran); },84validatePosition(pos) { return that._validatePosition(pos); },85getWordRangeAtPosition(pos, regexp?) { return that._getWordRangeAtPosition(pos, regexp); },86[Symbol.for('debug.description')]() {87return `TextDocument(${that._uri.toString()})`;88}89};90}91return Object.freeze(this._document);92}9394_acceptLanguageId(newLanguageId: string): void {95ok(!this._isDisposed);96this._languageId = newLanguageId;97}9899_acceptIsDirty(isDirty: boolean): void {100ok(!this._isDisposed);101this._isDirty = isDirty;102}103104_acceptEncoding(encoding: string): void {105ok(!this._isDisposed);106this._encoding = encoding;107}108109private _save(): Promise<boolean> {110if (this._isDisposed) {111return Promise.reject(new Error('Document has been closed'));112}113return this._proxy.$trySaveDocument(this._uri);114}115116private _getTextInRange(_range: vscode.Range): string {117const range = this._validateRange(_range);118119if (range.isEmpty) {120return '';121}122123if (range.isSingleLine) {124return this._lines[range.start.line].substring(range.start.character, range.end.character);125}126127const lineEnding = this._eol,128startLineIndex = range.start.line,129endLineIndex = range.end.line,130resultLines: string[] = [];131132resultLines.push(this._lines[startLineIndex].substring(range.start.character));133for (let i = startLineIndex + 1; i < endLineIndex; i++) {134resultLines.push(this._lines[i]);135}136resultLines.push(this._lines[endLineIndex].substring(0, range.end.character));137138return resultLines.join(lineEnding);139}140141private _lineAt(lineOrPosition: number | vscode.Position): vscode.TextLine {142143let line: number | undefined;144if (lineOrPosition instanceof Position) {145line = lineOrPosition.line;146} else if (typeof lineOrPosition === 'number') {147line = lineOrPosition;148} else if (!this._strictInstanceofChecks && Position.isPosition(lineOrPosition)) {149line = lineOrPosition.line;150}151152if (typeof line !== 'number' || line < 0 || line >= this._lines.length || Math.floor(line) !== line) {153throw new Error('Illegal value for `line`');154}155156return new ExtHostDocumentLine(line, this._lines[line], line === this._lines.length - 1);157}158159private _offsetAt(position: vscode.Position): number {160position = this._validatePosition(position);161this._ensureLineStarts();162return this._lineStarts!.getPrefixSum(position.line - 1) + position.character;163}164165private _positionAt(offset: number): vscode.Position {166offset = Math.floor(offset);167offset = Math.max(0, offset);168169this._ensureLineStarts();170const out = this._lineStarts!.getIndexOf(offset);171172const lineLength = this._lines[out.index].length;173174// Ensure we return a valid position175return new Position(out.index, Math.min(out.remainder, lineLength));176}177178// ---- range math179180private _validateRange(range: vscode.Range): vscode.Range {181if (this._strictInstanceofChecks) {182if (!(range instanceof Range)) {183throw new Error('Invalid argument');184}185} else {186if (!Range.isRange(range)) {187throw new Error('Invalid argument');188}189}190191const start = this._validatePosition(range.start);192const end = this._validatePosition(range.end);193194if (start === range.start && end === range.end) {195return range;196}197return new Range(start.line, start.character, end.line, end.character);198}199200private _validatePosition(position: vscode.Position): vscode.Position {201if (this._strictInstanceofChecks) {202if (!(position instanceof Position)) {203throw new Error('Invalid argument');204}205} else {206if (!Position.isPosition(position)) {207throw new Error('Invalid argument');208}209}210211if (this._lines.length === 0) {212return position.with(0, 0);213}214215let { line, character } = position;216let hasChanged = false;217218if (line < 0) {219line = 0;220character = 0;221hasChanged = true;222}223else if (line >= this._lines.length) {224line = this._lines.length - 1;225character = this._lines[line].length;226hasChanged = true;227}228else {229const maxCharacter = this._lines[line].length;230if (character < 0) {231character = 0;232hasChanged = true;233}234else if (character > maxCharacter) {235character = maxCharacter;236hasChanged = true;237}238}239240if (!hasChanged) {241return position;242}243return new Position(line, character);244}245246private _getWordRangeAtPosition(_position: vscode.Position, regexp?: RegExp): vscode.Range | undefined {247const position = this._validatePosition(_position);248249if (!regexp) {250// use default when custom-regexp isn't provided251regexp = getWordDefinitionFor(this._languageId);252253} else if (regExpLeadsToEndlessLoop(regexp)) {254// use default when custom-regexp is bad255throw new Error(`[getWordRangeAtPosition]: ignoring custom regexp '${regexp.source}' because it matches the empty string.`);256}257258const wordAtText = getWordAtText(259position.character + 1,260ensureValidWordDefinition(regexp),261this._lines[position.line],2620263);264265if (wordAtText) {266return new Range(position.line, wordAtText.startColumn - 1, position.line, wordAtText.endColumn - 1);267}268return undefined;269}270}271272export class ExtHostDocumentLine implements vscode.TextLine {273274private readonly _line: number;275private readonly _text: string;276private readonly _isLastLine: boolean;277278constructor(line: number, text: string, isLastLine: boolean) {279this._line = line;280this._text = text;281this._isLastLine = isLastLine;282}283284public get lineNumber(): number {285return this._line;286}287288public get text(): string {289return this._text;290}291292public get range(): Range {293return new Range(this._line, 0, this._line, this._text.length);294}295296public get rangeIncludingLineBreak(): Range {297if (this._isLastLine) {298return this.range;299}300return new Range(this._line, 0, this._line + 1, 0);301}302303public get firstNonWhitespaceCharacterIndex(): number {304//TODO@api, rename to 'leadingWhitespaceLength'305return /^(\s*)/.exec(this._text)![1].length;306}307308public get isEmptyOrWhitespace(): boolean {309return this.firstNonWhitespaceCharacterIndex === this._text.length;310}311}312313314