mirror of
https://github.com/microsoft/vscode.git
synced 2026-08-30 10:38:28 +01:00
- add `editor.useTabStops` for tab/backspace behaviour - add `editor.trimAutoWhitespace` for auto inserted whitespace cleanup
853 lines
25 KiB
TypeScript
853 lines
25 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
'use strict';
|
|
|
|
import {OrderGuaranteeEventEmitter} from 'vs/base/common/eventEmitter';
|
|
import * as strings from 'vs/base/common/strings';
|
|
import {Position} from 'vs/editor/common/core/position';
|
|
import {Range} from 'vs/editor/common/core/range';
|
|
import * as editorCommon from 'vs/editor/common/editorCommon';
|
|
import {ModelLine} from 'vs/editor/common/model/modelLine';
|
|
import {guessIndentation} from 'vs/editor/common/model/indentationGuesser';
|
|
import {DEFAULT_INDENTATION, DefaultConfig} from 'vs/editor/common/config/defaultConfig';
|
|
|
|
var LIMIT_FIND_COUNT = 999;
|
|
|
|
export class TextModel extends OrderGuaranteeEventEmitter implements editorCommon.ITextModel {
|
|
|
|
public static DEFAULT_CREATION_OPTIONS: editorCommon.ITextModelCreationOptions = {
|
|
tabSize: DEFAULT_INDENTATION.tabSize,
|
|
insertSpaces: DEFAULT_INDENTATION.insertSpaces,
|
|
detectIndentation: false,
|
|
trimAutoWhitespace: DefaultConfig.editor.trimAutoWhitespace,
|
|
defaultEOL: editorCommon.DefaultEndOfLine.LF
|
|
};
|
|
|
|
_lines:ModelLine[];
|
|
_EOL:string;
|
|
_isDisposed:boolean;
|
|
_isDisposing:boolean;
|
|
protected _options: editorCommon.ITextModelResolvedOptions;
|
|
|
|
private _versionId:number;
|
|
/**
|
|
* Unlike, versionId, this can go down (via undo) or go to previous values (via redo)
|
|
*/
|
|
private _alternativeVersionId: number;
|
|
private _BOM:string;
|
|
|
|
constructor(allowedEventTypes:string[], rawText:editorCommon.IRawText) {
|
|
allowedEventTypes.push(editorCommon.EventType.ModelContentChanged, editorCommon.EventType.ModelOptionsChanged);
|
|
super(allowedEventTypes);
|
|
|
|
this._options = rawText.options;
|
|
this._constructLines(rawText);
|
|
this._setVersionId(1);
|
|
this._isDisposed = false;
|
|
this._isDisposing = false;
|
|
}
|
|
|
|
public getOptions(): editorCommon.ITextModelResolvedOptions {
|
|
return this._options;
|
|
}
|
|
|
|
public updateOptions(newOpts:editorCommon.ITextModelUpdateOptions): void {
|
|
let somethingChanged = false;
|
|
let changed:editorCommon.IModelOptionsChangedEvent = {
|
|
tabSize: false,
|
|
insertSpaces: false,
|
|
trimAutoWhitespace: false
|
|
};
|
|
|
|
if (typeof newOpts.insertSpaces !== 'undefined') {
|
|
if (this._options.insertSpaces !== newOpts.insertSpaces) {
|
|
somethingChanged = true;
|
|
changed.insertSpaces = true;
|
|
this._options.insertSpaces = newOpts.insertSpaces;
|
|
}
|
|
}
|
|
if (typeof newOpts.tabSize !== 'undefined') {
|
|
if (this._options.tabSize !== newOpts.tabSize) {
|
|
somethingChanged = true;
|
|
changed.tabSize = true;
|
|
this._options.tabSize = newOpts.tabSize;
|
|
}
|
|
}
|
|
if (typeof newOpts.trimAutoWhitespace !== 'undefined') {
|
|
if (this._options.trimAutoWhitespace !== newOpts.trimAutoWhitespace) {
|
|
somethingChanged = true;
|
|
changed.trimAutoWhitespace = true;
|
|
this._options.trimAutoWhitespace = newOpts.trimAutoWhitespace;
|
|
}
|
|
}
|
|
|
|
if (somethingChanged) {
|
|
this.emit(editorCommon.EventType.ModelOptionsChanged, changed);
|
|
}
|
|
}
|
|
|
|
public detectIndentation(defaultInsertSpaces:boolean, defaultTabSize:number): void {
|
|
let lines = this._lines.map(line => line.text);
|
|
let guessedIndentation = guessIndentation(lines, defaultTabSize, defaultInsertSpaces);
|
|
this.updateOptions({
|
|
insertSpaces: guessedIndentation.insertSpaces,
|
|
tabSize: guessedIndentation.tabSize
|
|
});
|
|
}
|
|
|
|
private _normalizeIndentationFromWhitespace(str:string): string {
|
|
let tabSize = this._options.tabSize;
|
|
let insertSpaces = this._options.insertSpaces;
|
|
|
|
let spacesCnt = 0;
|
|
for (let i = 0; i < str.length; i++) {
|
|
if (str.charAt(i) === '\t') {
|
|
spacesCnt += tabSize;
|
|
} else {
|
|
spacesCnt++;
|
|
}
|
|
}
|
|
|
|
let result = '';
|
|
if (!insertSpaces) {
|
|
let tabsCnt = Math.floor(spacesCnt / tabSize);
|
|
spacesCnt = spacesCnt % tabSize;
|
|
for (let i = 0; i < tabsCnt; i++) {
|
|
result += '\t';
|
|
}
|
|
}
|
|
|
|
for (let i = 0; i < spacesCnt; i++) {
|
|
result += ' ';
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
public normalizeIndentation(str:string): string {
|
|
let firstNonWhitespaceIndex = strings.firstNonWhitespaceIndex(str);
|
|
if (firstNonWhitespaceIndex === -1) {
|
|
firstNonWhitespaceIndex = str.length;
|
|
}
|
|
return this._normalizeIndentationFromWhitespace(str.substring(0, firstNonWhitespaceIndex)) + str.substring(firstNonWhitespaceIndex);
|
|
}
|
|
|
|
public getOneIndent(): string {
|
|
let tabSize = this._options.tabSize;
|
|
let insertSpaces = this._options.insertSpaces;
|
|
|
|
if (insertSpaces) {
|
|
let result = '';
|
|
for (let i = 0; i < tabSize; i++) {
|
|
result += ' ';
|
|
}
|
|
return result;
|
|
} else {
|
|
return '\t';
|
|
}
|
|
}
|
|
|
|
public getVersionId(): number {
|
|
return this._versionId;
|
|
}
|
|
|
|
public getAlternativeVersionId(): number {
|
|
return this._alternativeVersionId;
|
|
}
|
|
|
|
_increaseVersionId(): void {
|
|
this._setVersionId(this._versionId + 1);
|
|
}
|
|
|
|
_setVersionId(newVersionId:number): void {
|
|
this._versionId = newVersionId;
|
|
this._alternativeVersionId = this._versionId;
|
|
}
|
|
|
|
_overwriteAlternativeVersionId(newAlternativeVersionId:number): void {
|
|
this._alternativeVersionId = newAlternativeVersionId;
|
|
}
|
|
|
|
public isDisposed(): boolean {
|
|
return this._isDisposed;
|
|
}
|
|
|
|
public dispose(): void {
|
|
this._isDisposed = true;
|
|
// Null out members, such that any use of a disposed model will throw exceptions sooner rather than later
|
|
this._lines = null;
|
|
this._EOL = null;
|
|
this._BOM = null;
|
|
|
|
super.dispose();
|
|
}
|
|
|
|
_createContentChangedFlushEvent(): editorCommon.IModelContentChangedFlushEvent {
|
|
return {
|
|
changeType: editorCommon.EventType.ModelContentChangedFlush,
|
|
detail: null,
|
|
// TODO@Alex -> remove these fields from here
|
|
versionId: -1,
|
|
isUndoing: false,
|
|
isRedoing: false
|
|
};
|
|
}
|
|
|
|
protected _emitContentChanged2(startLineNumber:number, startColumn:number, endLineNumber:number, endColumn:number, rangeLength:number, text:string, isUndoing:boolean, isRedoing:boolean): void {
|
|
var e:editorCommon.IModelContentChangedEvent2 = {
|
|
range: new Range(startLineNumber, startColumn, endLineNumber, endColumn),
|
|
rangeLength: rangeLength,
|
|
text: text,
|
|
eol: this._EOL,
|
|
versionId: this.getVersionId(),
|
|
isUndoing: isUndoing,
|
|
isRedoing: isRedoing
|
|
};
|
|
if (!this._isDisposing) {
|
|
this.emit(editorCommon.EventType.ModelContentChanged2, e);
|
|
}
|
|
}
|
|
|
|
_resetValue(e:editorCommon.IModelContentChangedFlushEvent, newValue:editorCommon.IRawText): void {
|
|
this._constructLines(newValue);
|
|
|
|
this._increaseVersionId();
|
|
|
|
e.detail = this.toRawText();
|
|
e.versionId = this._versionId;
|
|
}
|
|
|
|
public toRawText(): editorCommon.IRawText {
|
|
return {
|
|
BOM: this._BOM,
|
|
EOL: this._EOL,
|
|
lines: this.getLinesContent(),
|
|
length: this.getValueLength(),
|
|
options: this._options
|
|
};
|
|
}
|
|
|
|
public equals(other: editorCommon.IRawText): boolean {
|
|
if (this._BOM !== other.BOM) {
|
|
return false;
|
|
}
|
|
if (this._EOL !== other.EOL) {
|
|
return false;
|
|
}
|
|
if (this._lines.length !== other.lines.length) {
|
|
return false;
|
|
}
|
|
for (let i = 0, len = this._lines.length; i < len; i++) {
|
|
if (this._lines[i].text !== other.lines[i]) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
public setValue(value:string): void {
|
|
let rawText: editorCommon.IRawText = null;
|
|
if (value !== null) {
|
|
rawText = TextModel.toRawText(value, {
|
|
tabSize: this._options.tabSize,
|
|
insertSpaces: this._options.insertSpaces,
|
|
trimAutoWhitespace: this._options.trimAutoWhitespace,
|
|
detectIndentation: false,
|
|
defaultEOL: this._options.defaultEOL
|
|
});
|
|
}
|
|
this.setValueFromRawText(rawText);
|
|
}
|
|
|
|
public setValueFromRawText(newValue:editorCommon.IRawText): void {
|
|
if (newValue === null) {
|
|
// There's nothing to do
|
|
return;
|
|
}
|
|
var oldFullModelRange = this.getFullModelRange();
|
|
var oldModelValueLength = this.getValueLengthInRange(oldFullModelRange);
|
|
var endLineNumber = this.getLineCount();
|
|
var endColumn = this.getLineMaxColumn(endLineNumber);
|
|
var e = this._createContentChangedFlushEvent();
|
|
|
|
this._resetValue(e, newValue);
|
|
this._emitModelContentChangedFlushEvent(e);
|
|
this._emitContentChanged2(1, 1, endLineNumber, endColumn, oldModelValueLength, this.getValue(), false, false);
|
|
}
|
|
|
|
public getValue(eol?:editorCommon.EndOfLinePreference, preserveBOM:boolean=false): string {
|
|
var fullModelRange = this.getFullModelRange();
|
|
var fullModelValue = this.getValueInRange(fullModelRange, eol);
|
|
|
|
if (preserveBOM) {
|
|
return this._BOM + fullModelValue;
|
|
}
|
|
|
|
return fullModelValue;
|
|
}
|
|
|
|
public getValueLength(eol?: editorCommon.EndOfLinePreference, preserveBOM: boolean = false): number {
|
|
var fullModelRange = this.getFullModelRange();
|
|
var fullModelValue = this.getValueLengthInRange(fullModelRange, eol);
|
|
|
|
if (preserveBOM) {
|
|
return this._BOM.length + fullModelValue;
|
|
}
|
|
|
|
return fullModelValue;
|
|
}
|
|
|
|
public getEmptiedValueInRange(rawRange:editorCommon.IRange, fillCharacter: string = '', eol:editorCommon.EndOfLinePreference=editorCommon.EndOfLinePreference.TextDefined): string {
|
|
var range = this.validateRange(rawRange);
|
|
|
|
if (range.isEmpty()) {
|
|
return '';
|
|
}
|
|
|
|
if (range.startLineNumber === range.endLineNumber) {
|
|
return this._repeatCharacter(fillCharacter, range.endColumn - range.startColumn);
|
|
}
|
|
|
|
var lineEnding = this._getEndOfLine(eol),
|
|
startLineIndex = range.startLineNumber - 1,
|
|
endLineIndex = range.endLineNumber - 1,
|
|
resultLines:string[] = [];
|
|
|
|
resultLines.push(this._repeatCharacter(fillCharacter, this._lines[startLineIndex].text.length - range.startColumn + 1));
|
|
for (var i = startLineIndex + 1; i < endLineIndex; i++) {
|
|
resultLines.push(this._repeatCharacter(fillCharacter, this._lines[i].text.length));
|
|
}
|
|
resultLines.push(this._repeatCharacter(fillCharacter, range.endColumn - 1));
|
|
|
|
return resultLines.join(lineEnding);
|
|
}
|
|
|
|
private _repeatCharacter(fillCharacter:string, count:number): string {
|
|
var r = '';
|
|
for (var i = 0; i < count; i++) {
|
|
r += fillCharacter;
|
|
}
|
|
return r;
|
|
}
|
|
|
|
public getValueInRange(rawRange:editorCommon.IRange, eol:editorCommon.EndOfLinePreference=editorCommon.EndOfLinePreference.TextDefined): string {
|
|
var range = this.validateRange(rawRange);
|
|
|
|
if (range.isEmpty()) {
|
|
return '';
|
|
}
|
|
|
|
if (range.startLineNumber === range.endLineNumber) {
|
|
return this._lines[range.startLineNumber - 1].text.substring(range.startColumn - 1, range.endColumn - 1);
|
|
}
|
|
|
|
var lineEnding = this._getEndOfLine(eol),
|
|
startLineIndex = range.startLineNumber - 1,
|
|
endLineIndex = range.endLineNumber - 1,
|
|
resultLines:string[] = [];
|
|
|
|
resultLines.push(this._lines[startLineIndex].text.substring(range.startColumn - 1));
|
|
for (var i = startLineIndex + 1; i < endLineIndex; i++) {
|
|
resultLines.push(this._lines[i].text);
|
|
}
|
|
resultLines.push(this._lines[endLineIndex].text.substring(0, range.endColumn - 1));
|
|
|
|
return resultLines.join(lineEnding);
|
|
}
|
|
|
|
public getValueLengthInRange(rawRange:editorCommon.IRange, eol:editorCommon.EndOfLinePreference=editorCommon.EndOfLinePreference.TextDefined): number {
|
|
var range = this.validateRange(rawRange);
|
|
|
|
if (range.isEmpty()) {
|
|
return 0;
|
|
}
|
|
|
|
if (range.startLineNumber === range.endLineNumber) {
|
|
return (range.endColumn - range.startColumn);
|
|
}
|
|
|
|
var lineEndingLength = this._getEndOfLine(eol).length,
|
|
startLineIndex = range.startLineNumber - 1,
|
|
endLineIndex = range.endLineNumber - 1,
|
|
result = 0;
|
|
|
|
result += (this._lines[startLineIndex].text.length - range.startColumn + 1);
|
|
for (var i = startLineIndex + 1; i < endLineIndex; i++) {
|
|
result += lineEndingLength + this._lines[i].text.length;
|
|
}
|
|
result += lineEndingLength + (range.endColumn - 1);
|
|
|
|
return result;
|
|
}
|
|
|
|
public isDominatedByLongLines(longLineBoundary:number): boolean {
|
|
var smallLineCharCount = 0,
|
|
longLineCharCount = 0,
|
|
i: number,
|
|
len: number,
|
|
lines = this._lines,
|
|
lineLength: number;
|
|
|
|
for (i = 0, len = this._lines.length; i < len; i++) {
|
|
lineLength = lines[i].text.length;
|
|
if (lineLength >= longLineBoundary) {
|
|
longLineCharCount += lineLength;
|
|
} else {
|
|
smallLineCharCount += lineLength;
|
|
}
|
|
}
|
|
|
|
return (longLineCharCount > smallLineCharCount);
|
|
}
|
|
|
|
public getLineCount(): number {
|
|
return this._lines.length;
|
|
}
|
|
|
|
public getLineContent(lineNumber:number): string {
|
|
if (lineNumber < 1 || lineNumber > this.getLineCount()) {
|
|
throw new Error('Illegal value ' + lineNumber + ' for `lineNumber`');
|
|
}
|
|
|
|
return this._lines[lineNumber - 1].text;
|
|
}
|
|
|
|
public getLinesContent(): string[] {
|
|
var r: string[] = [];
|
|
for (var i = 0, len = this._lines.length; i < len; i++) {
|
|
r[i] = this._lines[i].text;
|
|
}
|
|
return r;
|
|
}
|
|
|
|
public getEOL(): string {
|
|
return this._EOL;
|
|
}
|
|
|
|
public setEOL(eol: editorCommon.EndOfLineSequence): void {
|
|
var newEOL = (eol === editorCommon.EndOfLineSequence.CRLF ? '\r\n' : '\n');
|
|
if (this._EOL === newEOL) {
|
|
// Nothing to do
|
|
return;
|
|
}
|
|
|
|
var oldFullModelRange = this.getFullModelRange();
|
|
var oldModelValueLength = this.getValueLengthInRange(oldFullModelRange);
|
|
var endLineNumber = this.getLineCount();
|
|
var endColumn = this.getLineMaxColumn(endLineNumber);
|
|
|
|
this._EOL = newEOL;
|
|
this._increaseVersionId();
|
|
|
|
var e = this._createContentChangedFlushEvent();
|
|
e.detail = this.toRawText();
|
|
e.versionId = this._versionId;
|
|
|
|
this._emitModelContentChangedFlushEvent(e);
|
|
this._emitContentChanged2(1, 1, endLineNumber, endColumn, oldModelValueLength, this.getValue(), false, false);
|
|
}
|
|
|
|
public getLineMinColumn(lineNumber:number): number {
|
|
return 1;
|
|
}
|
|
|
|
public getLineMaxColumn(lineNumber:number): number {
|
|
if (lineNumber < 1 || lineNumber > this.getLineCount()) {
|
|
throw new Error('Illegal value ' + lineNumber + ' for `lineNumber`');
|
|
}
|
|
|
|
return this._lines[lineNumber - 1].text.length + 1;
|
|
}
|
|
|
|
public getLineFirstNonWhitespaceColumn(lineNumber: number): number {
|
|
if (lineNumber < 1 || lineNumber > this.getLineCount()) {
|
|
throw new Error('Illegal value ' + lineNumber + ' for `lineNumber`');
|
|
}
|
|
|
|
var result = strings.firstNonWhitespaceIndex(this._lines[lineNumber - 1].text);
|
|
if (result === -1) {
|
|
return 0;
|
|
}
|
|
return result + 1;
|
|
}
|
|
|
|
public getLineLastNonWhitespaceColumn(lineNumber: number): number {
|
|
if (lineNumber < 1 || lineNumber > this.getLineCount()) {
|
|
throw new Error('Illegal value ' + lineNumber + ' for `lineNumber`');
|
|
}
|
|
|
|
var result = strings.lastNonWhitespaceIndex(this._lines[lineNumber - 1].text);
|
|
if (result === -1) {
|
|
return 0;
|
|
}
|
|
return result + 2;
|
|
}
|
|
|
|
public validateLineNumber(lineNumber:number): number {
|
|
if (lineNumber < 1) {
|
|
lineNumber = 1;
|
|
}
|
|
if (lineNumber > this._lines.length) {
|
|
lineNumber = this._lines.length;
|
|
}
|
|
return lineNumber;
|
|
}
|
|
|
|
public validatePosition(position:editorCommon.IPosition): editorCommon.IEditorPosition {
|
|
var lineNumber = position.lineNumber ? position.lineNumber : 1;
|
|
var column = position.column ? position.column : 1;
|
|
|
|
if (lineNumber < 1) {
|
|
lineNumber = 1;
|
|
}
|
|
if (lineNumber > this._lines.length) {
|
|
lineNumber = this._lines.length;
|
|
}
|
|
|
|
if (column < 1) {
|
|
column = 1;
|
|
}
|
|
var maxColumn = this.getLineMaxColumn(lineNumber);
|
|
if (column > maxColumn) {
|
|
column = maxColumn;
|
|
}
|
|
|
|
return new Position(lineNumber, column);
|
|
}
|
|
|
|
public validateRange(range:editorCommon.IRange): editorCommon.IEditorRange {
|
|
var start = this.validatePosition(new Position(range.startLineNumber, range.startColumn));
|
|
var end = this.validatePosition(new Position(range.endLineNumber, range.endColumn));
|
|
return new Range(start.lineNumber, start.column, end.lineNumber, end.column);
|
|
}
|
|
|
|
public modifyPosition(rawPosition: editorCommon.IPosition, offset: number) : editorCommon.IEditorPosition {
|
|
var position = this.validatePosition(rawPosition);
|
|
|
|
// Handle positive offsets, one line at a time
|
|
while (offset > 0) {
|
|
var maxColumn = this.getLineMaxColumn(position.lineNumber);
|
|
|
|
// Get to end of line
|
|
if (position.column < maxColumn) {
|
|
var subtract = Math.min(offset, maxColumn - position.column);
|
|
offset -= subtract;
|
|
position.column += subtract;
|
|
}
|
|
|
|
if (offset === 0) {
|
|
break;
|
|
}
|
|
|
|
// Go to next line
|
|
offset -= this._EOL.length;
|
|
if (offset < 0) {
|
|
throw new Error('TextModel.modifyPosition: Breaking line terminators');
|
|
}
|
|
|
|
++position.lineNumber;
|
|
if (position.lineNumber > this._lines.length) {
|
|
throw new Error('TextModel.modifyPosition: Offset goes beyond the end of the model');
|
|
}
|
|
|
|
position.column = 1;
|
|
}
|
|
|
|
// Handle negative offsets, one line at a time
|
|
while (offset < 0) {
|
|
|
|
// Get to the start of the line
|
|
if (position.column > 1) {
|
|
var add = Math.min(-offset, position.column - 1);
|
|
offset += add;
|
|
position.column -= add;
|
|
}
|
|
|
|
if (offset === 0) {
|
|
break;
|
|
}
|
|
|
|
// Go to the previous line
|
|
offset += this._EOL.length;
|
|
if (offset > 0) {
|
|
throw new Error('TextModel.modifyPosition: Breaking line terminators');
|
|
}
|
|
|
|
--position.lineNumber;
|
|
if (position.lineNumber < 1) {
|
|
throw new Error('TextModel.modifyPosition: Offset goes beyond the beginning of the model');
|
|
}
|
|
|
|
position.column = this.getLineMaxColumn(position.lineNumber);
|
|
}
|
|
|
|
return position;
|
|
}
|
|
|
|
public getFullModelRange(): editorCommon.IEditorRange {
|
|
var lineCount = this.getLineCount();
|
|
return new Range(1, 1, lineCount, this.getLineMaxColumn(lineCount));
|
|
}
|
|
|
|
_emitModelContentChangedFlushEvent(e:editorCommon.IModelContentChangedFlushEvent): void {
|
|
if (!this._isDisposing) {
|
|
this.emit(editorCommon.EventType.ModelContentChanged, e);
|
|
}
|
|
}
|
|
|
|
public static toRawText(rawText:string, opts:editorCommon.ITextModelCreationOptions): editorCommon.IRawText {
|
|
// Count the number of lines that end with \r\n
|
|
var carriageReturnCnt = 0,
|
|
lastCarriageReturnIndex = -1;
|
|
while ((lastCarriageReturnIndex = rawText.indexOf('\r', lastCarriageReturnIndex + 1)) !== -1) {
|
|
carriageReturnCnt++;
|
|
}
|
|
|
|
// Split the text into liens
|
|
var lines = rawText.split(/\r\n|\r|\n/);
|
|
|
|
// Remove the BOM (if present)
|
|
var BOM = '';
|
|
if (strings.startsWithUTF8BOM(lines[0])) {
|
|
BOM = strings.UTF8_BOM_CHARACTER;
|
|
lines[0] = lines[0].substr(1);
|
|
}
|
|
|
|
var lineFeedCnt = lines.length - 1;
|
|
var EOL = '';
|
|
if (lineFeedCnt === 0) {
|
|
// This is an empty file or a file with precisely one line
|
|
EOL = (opts.defaultEOL === editorCommon.DefaultEndOfLine.LF ? '\n' : '\r\n');
|
|
} else if (carriageReturnCnt > lineFeedCnt / 2) {
|
|
// More than half of the file contains \r\n ending lines
|
|
EOL = '\r\n';
|
|
} else {
|
|
// At least one line more ends in \n
|
|
EOL = '\n';
|
|
}
|
|
|
|
let resolvedOpts: editorCommon.ITextModelResolvedOptions;
|
|
if (opts.detectIndentation) {
|
|
let guessedIndentation = guessIndentation(lines, opts.tabSize, opts.insertSpaces);
|
|
resolvedOpts = {
|
|
tabSize: guessedIndentation.tabSize,
|
|
insertSpaces: guessedIndentation.insertSpaces,
|
|
trimAutoWhitespace: opts.trimAutoWhitespace,
|
|
defaultEOL: opts.defaultEOL
|
|
};
|
|
} else {
|
|
resolvedOpts = {
|
|
tabSize: opts.tabSize,
|
|
insertSpaces: opts.insertSpaces,
|
|
trimAutoWhitespace: opts.trimAutoWhitespace,
|
|
defaultEOL: opts.defaultEOL
|
|
};
|
|
}
|
|
|
|
return {
|
|
BOM: BOM,
|
|
EOL: EOL,
|
|
lines: lines,
|
|
length: rawText.length,
|
|
options: resolvedOpts
|
|
};
|
|
}
|
|
|
|
_constructLines(rawText:editorCommon.IRawText): void {
|
|
var rawLines = rawText.lines,
|
|
modelLines: ModelLine[] = [],
|
|
i: number,
|
|
len: number;
|
|
|
|
for (i = 0, len = rawLines.length; i < len; i++) {
|
|
modelLines.push(new ModelLine(i + 1, rawLines[i]));
|
|
}
|
|
this._BOM = rawText.BOM;
|
|
this._EOL = rawText.EOL;
|
|
this._lines = modelLines;
|
|
}
|
|
|
|
private _getEndOfLine(eol:editorCommon.EndOfLinePreference): string {
|
|
switch (eol) {
|
|
case editorCommon.EndOfLinePreference.LF:
|
|
return '\n';
|
|
case editorCommon.EndOfLinePreference.CRLF:
|
|
return '\r\n';
|
|
case editorCommon.EndOfLinePreference.TextDefined:
|
|
return this.getEOL();
|
|
}
|
|
throw new Error('Unknown EOL preference');
|
|
}
|
|
|
|
public findMatches(searchString:string, rawSearchScope:any, isRegex:boolean, matchCase:boolean, wholeWord:boolean, limitResultCount:number = LIMIT_FIND_COUNT): editorCommon.IEditorRange[] {
|
|
var regex = strings.createSafeRegExp(searchString, isRegex, matchCase, wholeWord);
|
|
if (!regex) {
|
|
return [];
|
|
}
|
|
|
|
var searchRange:editorCommon.IEditorRange;
|
|
if (Range.isIRange(rawSearchScope)) {
|
|
searchRange = rawSearchScope;
|
|
} else {
|
|
searchRange = this.getFullModelRange();
|
|
}
|
|
|
|
return this._doFindMatches(searchRange, regex, limitResultCount);
|
|
}
|
|
|
|
public findNextMatch(searchString:string, rawSearchStart:editorCommon.IPosition, isRegex:boolean, matchCase:boolean, wholeWord:boolean): editorCommon.IEditorRange {
|
|
var regex = strings.createSafeRegExp(searchString, isRegex, matchCase, wholeWord);
|
|
if (!regex) {
|
|
return null;
|
|
}
|
|
|
|
var searchStart = this.validatePosition(rawSearchStart),
|
|
lineCount = this.getLineCount(),
|
|
startLineNumber = searchStart.lineNumber,
|
|
text: string,
|
|
r: editorCommon.IEditorRange;
|
|
|
|
// Look in first line
|
|
text = this._lines[startLineNumber - 1].text.substring(searchStart.column - 1);
|
|
r = this._findMatchInLine(regex, text, startLineNumber, searchStart.column - 1);
|
|
if (r) {
|
|
return r;
|
|
}
|
|
|
|
for (var i = 1; i <= lineCount; i++) {
|
|
var lineIndex = (startLineNumber + i - 1) % lineCount;
|
|
text = this._lines[lineIndex].text;
|
|
r = this._findMatchInLine(regex, text, lineIndex + 1, 0);
|
|
if (r) {
|
|
return r;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public findPreviousMatch(searchString:string, rawSearchStart:editorCommon.IPosition, isRegex:boolean, matchCase:boolean, wholeWord:boolean): editorCommon.IEditorRange {
|
|
var regex = strings.createSafeRegExp(searchString, isRegex, matchCase, wholeWord);
|
|
if (!regex) {
|
|
return null;
|
|
}
|
|
|
|
var searchStart = this.validatePosition(rawSearchStart),
|
|
lineCount = this.getLineCount(),
|
|
startLineNumber = searchStart.lineNumber,
|
|
text: string,
|
|
r: editorCommon.IEditorRange;
|
|
|
|
// Look in first line
|
|
text = this._lines[startLineNumber - 1].text.substring(0, searchStart.column - 1);
|
|
r = this._findLastMatchInLine(regex, text, startLineNumber);
|
|
if (r) {
|
|
return r;
|
|
}
|
|
|
|
for (var i = 1; i <= lineCount; i++) {
|
|
var lineIndex = (lineCount + startLineNumber - i - 1) % lineCount;
|
|
text = this._lines[lineIndex].text;
|
|
r = this._findLastMatchInLine(regex, text, lineIndex + 1);
|
|
if (r) {
|
|
return r;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private _doFindMatches(searchRange:editorCommon.IEditorRange, searchRegex:RegExp, limitResultCount:number): editorCommon.IEditorRange[] {
|
|
var result:editorCommon.IEditorRange[] = [],
|
|
text: string,
|
|
counter = 0;
|
|
|
|
// Early case for a search range that starts & stops on the same line number
|
|
if (searchRange.startLineNumber === searchRange.endLineNumber) {
|
|
text = this._lines[searchRange.startLineNumber - 1].text.substring(searchRange.startColumn - 1, searchRange.endColumn - 1);
|
|
counter = this._findMatchesInLine(searchRegex, text, searchRange.startLineNumber, searchRange.startColumn - 1, counter, result, limitResultCount);
|
|
return result;
|
|
}
|
|
|
|
// Collect results from first line
|
|
text = this._lines[searchRange.startLineNumber - 1].text.substring(searchRange.startColumn - 1);
|
|
counter = this._findMatchesInLine(searchRegex, text, searchRange.startLineNumber, searchRange.startColumn - 1, counter, result, limitResultCount);
|
|
|
|
// Collect results from middle lines
|
|
for (var lineNumber = searchRange.startLineNumber + 1; lineNumber < searchRange.endLineNumber && counter < limitResultCount; lineNumber++) {
|
|
counter = this._findMatchesInLine(searchRegex, this._lines[lineNumber - 1].text, lineNumber, 0, counter, result, limitResultCount);
|
|
}
|
|
|
|
// Collect results from last line
|
|
if (counter < limitResultCount) {
|
|
text = this._lines[searchRange.endLineNumber - 1].text.substring(0, searchRange.endColumn - 1);
|
|
counter = this._findMatchesInLine(searchRegex, text, searchRange.endLineNumber, 0, counter, result, limitResultCount);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private _findMatchInLine(searchRegex:RegExp, text:string, lineNumber:number, deltaOffset:number): editorCommon.IEditorRange {
|
|
var m = searchRegex.exec(text);
|
|
if (!m) {
|
|
return null;
|
|
}
|
|
return new Range(lineNumber, m.index + 1 + deltaOffset, lineNumber, m.index + 1 + m[0].length + deltaOffset);
|
|
}
|
|
|
|
private _findLastMatchInLine(searchRegex:RegExp, text:string, lineNumber:number): editorCommon.IEditorRange {
|
|
let bestResult: editorCommon.IEditorRange = null;
|
|
let m:RegExpExecArray;
|
|
while ((m = searchRegex.exec(text))) {
|
|
let result = new Range(lineNumber, m.index + 1, lineNumber, m.index + 1 + m[0].length);
|
|
if (result.equalsRange(bestResult)) {
|
|
break;
|
|
}
|
|
bestResult = result;
|
|
}
|
|
return bestResult;
|
|
}
|
|
|
|
private _findMatchesInLine(searchRegex:RegExp, text:string, lineNumber:number, deltaOffset:number, counter:number, result:editorCommon.IEditorRange[], limitResultCount:number): number {
|
|
var m:RegExpExecArray;
|
|
// Reset regex to search from the beginning
|
|
searchRegex.lastIndex = 0;
|
|
do {
|
|
m = searchRegex.exec(text);
|
|
if (m) {
|
|
var range = new Range(lineNumber, m.index + 1 + deltaOffset, lineNumber, m.index + 1 + m[0].length + deltaOffset);
|
|
// Exit early if the regex matches the same range
|
|
if (range.equalsRange(result[result.length - 1])) {
|
|
return counter;
|
|
}
|
|
result.push(range);
|
|
counter++;
|
|
if (counter >= limitResultCount) {
|
|
return counter;
|
|
}
|
|
}
|
|
} while(m);
|
|
return counter;
|
|
}
|
|
}
|
|
|
|
export class RawText {
|
|
|
|
public static fromString(rawText:string, opts:editorCommon.ITextModelCreationOptions): editorCommon.IRawText {
|
|
return TextModel.toRawText(rawText, opts);
|
|
}
|
|
|
|
public static fromStringWithModelOptions(rawText:string, model:editorCommon.IModel): editorCommon.IRawText {
|
|
let opts = model.getOptions();
|
|
return TextModel.toRawText(rawText, {
|
|
tabSize: opts.tabSize,
|
|
insertSpaces: opts.insertSpaces,
|
|
trimAutoWhitespace: opts.trimAutoWhitespace,
|
|
detectIndentation: false,
|
|
defaultEOL: opts.defaultEOL
|
|
});
|
|
}
|
|
|
|
} |