/* -------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. * ------------------------------------------------------------------------------------------ */ 'use strict'; class FullTextDocument { constructor(uri, languageId, version, content) { this._uri = uri; this._languageId = languageId; this._version = version; this._content = content; this._lineOffsets = undefined; } get uri() { return this._uri; } get languageId() { return this._languageId; } get version() { return this._version; } getText(range) { if (range) { const start = this.offsetAt(range.start); const end = this.offsetAt(range.end); return this._content.substring(start, end); } return this._content; } update(changes, version) { for (let change of changes) { if (FullTextDocument.isIncremental(change)) { // makes sure start is before end const range = getWellformedRange(change.range); // update content const startOffset = this.offsetAt(range.start); const endOffset = this.offsetAt(range.end); this._content = this._content.substring(0, startOffset) + change.text + this._content.substring(endOffset, this._content.length); // update the offsets const startLine = Math.max(range.start.line, 0); const endLine = Math.max(range.end.line, 0); let lineOffsets = this._lineOffsets; const addedLineOffsets = computeLineOffsets(change.text, false, startOffset); if (endLine - startLine === addedLineOffsets.length) { for (let i = 0, len = addedLineOffsets.length; i < len; i++) { lineOffsets[i + startLine + 1] = addedLineOffsets[i]; } } else { if (addedLineOffsets.length < 10000) { lineOffsets.splice(startLine + 1, endLine - startLine, ...addedLineOffsets); } else { // avoid too many arguments for splice this._lineOffsets = lineOffsets = lineOffsets.slice(0, startLine + 1).concat(addedLineOffsets, lineOffsets.slice(endLine + 1)); } } const diff = change.text.length - (endOffset - startOffset); if (diff !== 0) { for (let i = startLine + 1 + addedLineOffsets.length, len = lineOffsets.length; i < len; i++) { lineOffsets[i] = lineOffsets[i] + diff; } } } else if (FullTextDocument.isFull(change)) { this._content = change.text; this._lineOffsets = undefined; } else { throw new Error('Unknown change event received'); } } this._version = version; } getLineOffsets() { if (this._lineOffsets === undefined) { this._lineOffsets = computeLineOffsets(this._content, true); } return this._lineOffsets; } positionAt(offset) { offset = Math.max(Math.min(offset, this._content.length), 0); let lineOffsets = this.getLineOffsets(); let low = 0, high = lineOffsets.length; if (high === 0) { return { line: 0, character: offset }; } while (low < high) { let mid = Math.floor((low + high) / 2); if (lineOffsets[mid] > offset) { high = mid; } else { low = mid + 1; } } // 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] }; } offsetAt(position) { let lineOffsets = this.getLineOffsets(); if (position.line >= lineOffsets.length) { return this._content.length; } else if (position.line < 0) { return 0; } let lineOffset = lineOffsets[position.line]; let nextLineOffset = (position.line + 1 < lineOffsets.length) ? lineOffsets[position.line + 1] : this._content.length; return Math.max(Math.min(lineOffset + position.character, nextLineOffset), lineOffset); } get lineCount() { return this.getLineOffsets().length; } static isIncremental(event) { let candidate = event; return candidate !== undefined && candidate !== null && typeof candidate.text === 'string' && candidate.range !== undefined && (candidate.rangeLength === undefined || typeof candidate.rangeLength === 'number'); } static isFull(event) { let candidate = event; return candidate !== undefined && candidate !== null && typeof candidate.text === 'string' && candidate.range === undefined && candidate.rangeLength === undefined; } } export var TextDocument; (function (TextDocument) { /** * Creates a new text document. * * @param uri The document's uri. * @param languageId The document's language Id. * @param version The document's initial version number. * @param content The document's content. */ function create(uri, languageId, version, content) { return new FullTextDocument(uri, languageId, version, content); } TextDocument.create = create; /** * Updates a TextDocument by modifying its content. * * @param document the document to update. Only documents created by TextDocument.create are valid inputs. * @param changes the changes to apply to the document. * @param version the changes version for the document. * @returns The updated TextDocument. Note: That's the same document instance passed in as first parameter. * */ function update(document, changes, version) { if (document instanceof FullTextDocument) { document.update(changes, version); return document; } else { throw new Error('TextDocument.update: document must be created by TextDocument.create'); } } TextDocument.update = update; function applyEdits(document, edits) { let text = document.getText(); let sortedEdits = mergeSort(edits.map(getWellformedEdit), (a, b) => { let diff = a.range.start.line - b.range.start.line; if (diff === 0) { return a.range.start.character - b.range.start.character; } return diff; }); let lastModifiedOffset = 0; const spans = []; for (const e of sortedEdits) { let startOffset = document.offsetAt(e.range.start); if (startOffset < lastModifiedOffset) { throw new Error('Overlapping edit'); } else if (startOffset > lastModifiedOffset) { spans.push(text.substring(lastModifiedOffset, startOffset)); } if (e.newText.length) { spans.push(e.newText); } lastModifiedOffset = document.offsetAt(e.range.end); } spans.push(text.substr(lastModifiedOffset)); return spans.join(''); } TextDocument.applyEdits = applyEdits; })(TextDocument || (TextDocument = {})); function mergeSort(data, compare) { if (data.length <= 1) { // sorted return data; } const p = (data.length / 2) | 0; const left = data.slice(0, p); const right = data.slice(p); mergeSort(left, compare); mergeSort(right, compare); let leftIdx = 0; let rightIdx = 0; let i = 0; while (leftIdx < left.length && rightIdx < right.length) { let ret = compare(left[leftIdx], right[rightIdx]); if (ret <= 0) { // smaller_equal -> take left to preserve order data[i++] = left[leftIdx++]; } else { // greater -> take right data[i++] = right[rightIdx++]; } } while (leftIdx < left.length) { data[i++] = left[leftIdx++]; } while (rightIdx < right.length) { data[i++] = right[rightIdx++]; } return data; } function computeLineOffsets(text, isAtLineStart, textOffset = 0) { const result = isAtLineStart ? [textOffset] : []; for (let i = 0; i < text.length; i++) { let ch = text.charCodeAt(i); if (ch === 13 /* CharCode.CarriageReturn */ || ch === 10 /* CharCode.LineFeed */) { if (ch === 13 /* CharCode.CarriageReturn */ && i + 1 < text.length && text.charCodeAt(i + 1) === 10 /* CharCode.LineFeed */) { i++; } result.push(textOffset + i + 1); } } return result; } function getWellformedRange(range) { const start = range.start; const end = range.end; if (start.line > end.line || (start.line === end.line && start.character > end.character)) { return { start: end, end: start }; } return range; } function getWellformedEdit(textEdit) { const range = getWellformedRange(textEdit.range); if (range !== textEdit.range) { return { newText: textEdit.newText, range }; } return textEdit; }