/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ 'use strict'; import { ok } from 'vs/base/common/assert'; import { regExpLeadsToEndlessLoop } from 'vs/base/common/strings'; import { MirrorModel } from 'vs/editor/common/model/mirrorModel'; import URI from 'vs/base/common/uri'; import { Range, Position, EndOfLine } from 'vs/workbench/api/node/extHostTypes'; import * as vscode from 'vscode'; import { getWordAtText, ensureValidWordDefinition } from 'vs/editor/common/model/wordHelper'; import { MainThreadDocumentsShape } from './extHost.protocol'; import { ITextSource } from 'vs/editor/common/model/textSource'; import { TPromise } from 'vs/base/common/winjs.base'; const _modeId2WordDefinition = new Map(); export function setWordDefinitionFor(modeId: string, wordDefinition: RegExp): void { _modeId2WordDefinition.set(modeId, wordDefinition); } export function getWordDefinitionFor(modeId: string): RegExp { return _modeId2WordDefinition.get(modeId); } export class ExtHostDocumentData extends MirrorModel { private _proxy: MainThreadDocumentsShape; private _languageId: string; private _isDirty: boolean; private _document: vscode.TextDocument; private _textLines: vscode.TextLine[] = []; private _isDisposed: boolean = false; constructor(proxy: MainThreadDocumentsShape, uri: URI, lines: string[], eol: string, languageId: string, versionId: number, isDirty: boolean ) { super(uri, lines, eol, versionId); this._proxy = proxy; this._languageId = languageId; this._isDirty = isDirty; } dispose(): void { // we don't really dispose documents but let // extensions still read from them. some // operations, live saving, will now error tho ok(!this._isDisposed); this._isDisposed = true; this._isDirty = false; } equalLines({ lines }: ITextSource): boolean { const len = lines.length; if (len !== this._lines.length) { return false; } for (let i = 0; i < len; i++) { if (lines[i] !== this._lines[i]) { return false; } } return true; } get document(): vscode.TextDocument { if (!this._document) { const data = this; this._document = { get uri() { return data._uri; }, get fileName() { return data._uri.fsPath; }, get isUntitled() { return data._uri.scheme !== 'file'; }, get languageId() { return data._languageId; }, get version() { return data._versionId; }, get isClosed() { return data._isDisposed; }, get isDirty() { return data._isDirty; }, save() { return data._save(); }, getText(range?) { return range ? data._getTextInRange(range) : data.getText(); }, get eol() { return data._eol === '\n' ? EndOfLine.LF : EndOfLine.CRLF; }, get lineCount() { return data._lines.length; }, lineAt(lineOrPos: number | vscode.Position) { return data._lineAt(lineOrPos); }, offsetAt(pos) { return data._offsetAt(pos); }, positionAt(offset) { return data._positionAt(offset); }, validateRange(ran) { return data._validateRange(ran); }, validatePosition(pos) { return data._validatePosition(pos); }, getWordRangeAtPosition(pos, regexp?) { return data._getWordRangeAtPosition(pos, regexp); } }; } return Object.freeze(this._document); } _acceptLanguageId(newLanguageId: string): void { ok(!this._isDisposed); this._languageId = newLanguageId; } _acceptIsDirty(isDirty: boolean): void { ok(!this._isDisposed); this._isDirty = isDirty; } private _save(): TPromise { if (this._isDisposed) { return TPromise.wrapError(new Error('Document has been closed')); } return this._proxy.$trySaveDocument(this._uri); } private _getTextInRange(_range: vscode.Range): string { let range = this._validateRange(_range); if (range.isEmpty) { return ''; } if (range.isSingleLine) { return this._lines[range.start.line].substring(range.start.character, range.end.character); } let lineEnding = this._eol, startLineIndex = range.start.line, endLineIndex = range.end.line, resultLines: string[] = []; resultLines.push(this._lines[startLineIndex].substring(range.start.character)); for (let i = startLineIndex + 1; i < endLineIndex; i++) { resultLines.push(this._lines[i]); } resultLines.push(this._lines[endLineIndex].substring(0, range.end.character)); return resultLines.join(lineEnding); } private _lineAt(lineOrPosition: number | vscode.Position): vscode.TextLine { let line: number; if (lineOrPosition instanceof Position) { line = lineOrPosition.line; } else if (typeof lineOrPosition === 'number') { line = lineOrPosition; } if (line < 0 || line >= this._lines.length) { throw new Error('Illegal value for `line`'); } let result = this._textLines[line]; if (!result || result.lineNumber !== line || result.text !== this._lines[line]) { const text = this._lines[line]; const firstNonWhitespaceCharacterIndex = /^(\s*)/.exec(text)[1].length; const range = new Range(line, 0, line, text.length); const rangeIncludingLineBreak = line < this._lines.length - 1 ? new Range(line, 0, line + 1, 0) : range; result = Object.freeze({ lineNumber: line, range, rangeIncludingLineBreak, text, firstNonWhitespaceCharacterIndex, //TODO@api, rename to 'leadingWhitespaceLength' isEmptyOrWhitespace: firstNonWhitespaceCharacterIndex === text.length }); this._textLines[line] = result; } return result; } private _offsetAt(position: vscode.Position): number { position = this._validatePosition(position); this._ensureLineStarts(); return this._lineStarts.getAccumulatedValue(position.line - 1) + position.character; } private _positionAt(offset: number): vscode.Position { offset = Math.floor(offset); offset = Math.max(0, offset); this._ensureLineStarts(); let out = this._lineStarts.getIndexOf(offset); let lineLength = this._lines[out.index].length; // Ensure we return a valid position return new Position(out.index, Math.min(out.remainder, lineLength)); } // ---- range math private _validateRange(range: vscode.Range): vscode.Range { if (!(range instanceof Range)) { throw new Error('Invalid argument'); } let start = this._validatePosition(range.start); let end = this._validatePosition(range.end); if (start === range.start && end === range.end) { return range; } return new Range(start.line, start.character, end.line, end.character); } private _validatePosition(position: vscode.Position): vscode.Position { if (!(position instanceof Position)) { throw new Error('Invalid argument'); } let { line, character } = position; let hasChanged = false; if (line < 0) { line = 0; character = 0; hasChanged = true; } else if (line >= this._lines.length) { line = this._lines.length - 1; character = this._lines[line].length; hasChanged = true; } else { let maxCharacter = this._lines[line].length; if (character < 0) { character = 0; hasChanged = true; } else if (character > maxCharacter) { character = maxCharacter; hasChanged = true; } } if (!hasChanged) { return position; } return new Position(line, character); } private _getWordRangeAtPosition(_position: vscode.Position, regexp?: RegExp): vscode.Range { let position = this._validatePosition(_position); if (!regexp) { // use default when custom-regexp isn't provided regexp = getWordDefinitionFor(this._languageId); } else if (regExpLeadsToEndlessLoop(regexp)) { // use default when custom-regexp is bad console.warn(`[getWordRangeAtPosition]: ignoring custom regexp '${regexp.source}' because it matches the empty string.`); regexp = getWordDefinitionFor(this._languageId); } let wordAtText = getWordAtText( position.character + 1, ensureValidWordDefinition(regexp), this._lines[position.line], 0 ); if (wordAtText) { return new Range(position.line, wordAtText.startColumn - 1, position.line, wordAtText.endColumn - 1); } return undefined; } }