UNPKG

monaco-editor-core

Version:

A browser based code editor

171 lines (170 loc) 9.15 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { BugIndicatingError } from '../../../base/common/errors.js'; import { LineRange } from '../core/lineRange.js'; import { Position } from '../core/position.js'; import { Range } from '../core/range.js'; import { SingleTextEdit } from '../core/textEdit.js'; /** * Maps a line range in the original text model to a line range in the modified text model. */ export class LineRangeMapping { static inverse(mapping, originalLineCount, modifiedLineCount) { const result = []; let lastOriginalEndLineNumber = 1; let lastModifiedEndLineNumber = 1; for (const m of mapping) { const r = new LineRangeMapping(new LineRange(lastOriginalEndLineNumber, m.original.startLineNumber), new LineRange(lastModifiedEndLineNumber, m.modified.startLineNumber)); if (!r.modified.isEmpty) { result.push(r); } lastOriginalEndLineNumber = m.original.endLineNumberExclusive; lastModifiedEndLineNumber = m.modified.endLineNumberExclusive; } const r = new LineRangeMapping(new LineRange(lastOriginalEndLineNumber, originalLineCount + 1), new LineRange(lastModifiedEndLineNumber, modifiedLineCount + 1)); if (!r.modified.isEmpty) { result.push(r); } return result; } static clip(mapping, originalRange, modifiedRange) { const result = []; for (const m of mapping) { const original = m.original.intersect(originalRange); const modified = m.modified.intersect(modifiedRange); if (original && !original.isEmpty && modified && !modified.isEmpty) { result.push(new LineRangeMapping(original, modified)); } } return result; } constructor(originalRange, modifiedRange) { this.original = originalRange; this.modified = modifiedRange; } toString() { return `{${this.original.toString()}->${this.modified.toString()}}`; } flip() { return new LineRangeMapping(this.modified, this.original); } join(other) { return new LineRangeMapping(this.original.join(other.original), this.modified.join(other.modified)); } /** * This method assumes that the LineRangeMapping describes a valid diff! * I.e. if one range is empty, the other range cannot be the entire document. * It avoids various problems when the line range points to non-existing line-numbers. */ toRangeMapping() { const origInclusiveRange = this.original.toInclusiveRange(); const modInclusiveRange = this.modified.toInclusiveRange(); if (origInclusiveRange && modInclusiveRange) { return new RangeMapping(origInclusiveRange, modInclusiveRange); } else if (this.original.startLineNumber === 1 || this.modified.startLineNumber === 1) { if (!(this.modified.startLineNumber === 1 && this.original.startLineNumber === 1)) { // If one line range starts at 1, the other one must start at 1 as well. throw new BugIndicatingError('not a valid diff'); } // Because one range is empty and both ranges start at line 1, none of the ranges can cover all lines. // Thus, `endLineNumberExclusive` is a valid line number. return new RangeMapping(new Range(this.original.startLineNumber, 1, this.original.endLineNumberExclusive, 1), new Range(this.modified.startLineNumber, 1, this.modified.endLineNumberExclusive, 1)); } else { // We can assume here that both startLineNumbers are greater than 1. return new RangeMapping(new Range(this.original.startLineNumber - 1, Number.MAX_SAFE_INTEGER, this.original.endLineNumberExclusive - 1, Number.MAX_SAFE_INTEGER), new Range(this.modified.startLineNumber - 1, Number.MAX_SAFE_INTEGER, this.modified.endLineNumberExclusive - 1, Number.MAX_SAFE_INTEGER)); } } /** * This method assumes that the LineRangeMapping describes a valid diff! * I.e. if one range is empty, the other range cannot be the entire document. * It avoids various problems when the line range points to non-existing line-numbers. */ toRangeMapping2(original, modified) { if (isValidLineNumber(this.original.endLineNumberExclusive, original) && isValidLineNumber(this.modified.endLineNumberExclusive, modified)) { return new RangeMapping(new Range(this.original.startLineNumber, 1, this.original.endLineNumberExclusive, 1), new Range(this.modified.startLineNumber, 1, this.modified.endLineNumberExclusive, 1)); } if (!this.original.isEmpty && !this.modified.isEmpty) { return new RangeMapping(Range.fromPositions(new Position(this.original.startLineNumber, 1), normalizePosition(new Position(this.original.endLineNumberExclusive - 1, Number.MAX_SAFE_INTEGER), original)), Range.fromPositions(new Position(this.modified.startLineNumber, 1), normalizePosition(new Position(this.modified.endLineNumberExclusive - 1, Number.MAX_SAFE_INTEGER), modified))); } if (this.original.startLineNumber > 1 && this.modified.startLineNumber > 1) { return new RangeMapping(Range.fromPositions(normalizePosition(new Position(this.original.startLineNumber - 1, Number.MAX_SAFE_INTEGER), original), normalizePosition(new Position(this.original.endLineNumberExclusive - 1, Number.MAX_SAFE_INTEGER), original)), Range.fromPositions(normalizePosition(new Position(this.modified.startLineNumber - 1, Number.MAX_SAFE_INTEGER), modified), normalizePosition(new Position(this.modified.endLineNumberExclusive - 1, Number.MAX_SAFE_INTEGER), modified))); } // Situation now: one range is empty and one range touches the last line and one range starts at line 1. // I don't think this can happen. throw new BugIndicatingError(); } } function normalizePosition(position, content) { if (position.lineNumber < 1) { return new Position(1, 1); } if (position.lineNumber > content.length) { return new Position(content.length, content[content.length - 1].length + 1); } const line = content[position.lineNumber - 1]; if (position.column > line.length + 1) { return new Position(position.lineNumber, line.length + 1); } return position; } function isValidLineNumber(lineNumber, lines) { return lineNumber >= 1 && lineNumber <= lines.length; } /** * Maps a line range in the original text model to a line range in the modified text model. * Also contains inner range mappings. */ export class DetailedLineRangeMapping extends LineRangeMapping { static fromRangeMappings(rangeMappings) { const originalRange = LineRange.join(rangeMappings.map(r => LineRange.fromRangeInclusive(r.originalRange))); const modifiedRange = LineRange.join(rangeMappings.map(r => LineRange.fromRangeInclusive(r.modifiedRange))); return new DetailedLineRangeMapping(originalRange, modifiedRange, rangeMappings); } constructor(originalRange, modifiedRange, innerChanges) { super(originalRange, modifiedRange); this.innerChanges = innerChanges; } flip() { return new DetailedLineRangeMapping(this.modified, this.original, this.innerChanges?.map(c => c.flip())); } withInnerChangesFromLineRanges() { return new DetailedLineRangeMapping(this.original, this.modified, [this.toRangeMapping()]); } } /** * Maps a range in the original text model to a range in the modified text model. */ export class RangeMapping { static assertSorted(rangeMappings) { for (let i = 1; i < rangeMappings.length; i++) { const previous = rangeMappings[i - 1]; const current = rangeMappings[i]; if (!(previous.originalRange.getEndPosition().isBeforeOrEqual(current.originalRange.getStartPosition()) && previous.modifiedRange.getEndPosition().isBeforeOrEqual(current.modifiedRange.getStartPosition()))) { throw new BugIndicatingError('Range mappings must be sorted'); } } } constructor(originalRange, modifiedRange) { this.originalRange = originalRange; this.modifiedRange = modifiedRange; } toString() { return `{${this.originalRange.toString()}->${this.modifiedRange.toString()}}`; } flip() { return new RangeMapping(this.modifiedRange, this.originalRange); } /** * Creates a single text edit that describes the change from the original to the modified text. */ toTextEdit(modified) { const newText = modified.getValueOfRange(this.modifiedRange); return new SingleTextEdit(this.originalRange, newText); } }