From bde23fb906c97cec1949ca705b299a47a73b6f11 Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Tue, 6 Mar 2018 11:33:20 +0100 Subject: [PATCH] [HTML] server contains invalid implementation of applyEdit. Fixes #45014 --- extensions/html/server/src/test/utils.test.ts | 37 ++++++++++++++++ extensions/html/server/src/utils/arrays.ts | 44 ++++++++++++++++++- extensions/html/server/src/utils/edits.ts | 34 +++++++------- 3 files changed, 97 insertions(+), 18 deletions(-) create mode 100644 extensions/html/server/src/test/utils.test.ts diff --git a/extensions/html/server/src/test/utils.test.ts b/extensions/html/server/src/test/utils.test.ts new file mode 100644 index 00000000000..ac14e03ea45 --- /dev/null +++ b/extensions/html/server/src/test/utils.test.ts @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * 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 * as assert from 'assert'; +import { applyEdits } from '../utils/edits'; +import { TextDocument, TextEdit, Position, Range } from 'vscode-languageserver-types'; + +suite('Edits', () => { + + test('inserts', function (): any { + let input = TextDocument.create('foo://bar/f', 'html', 0, '012345678901234567890123456789'); + assert.equal(applyEdits(input, [TextEdit.insert(Position.create(0, 0), 'Hello')]), 'Hello012345678901234567890123456789'); + assert.equal(applyEdits(input, [TextEdit.insert(Position.create(0, 1), 'Hello')]), '0Hello12345678901234567890123456789'); + assert.equal(applyEdits(input, [TextEdit.insert(Position.create(0, 1), 'Hello'), TextEdit.insert(Position.create(0, 1), 'World')]), '0HelloWorld12345678901234567890123456789'); + assert.equal(applyEdits(input, [TextEdit.insert(Position.create(0, 2), 'One'), TextEdit.insert(Position.create(0, 1), 'Hello'), TextEdit.insert(Position.create(0, 1), 'World'), TextEdit.insert(Position.create(0, 2), 'Two'), TextEdit.insert(Position.create(0, 2), 'Three')]), '0HelloWorld1OneTwoThree2345678901234567890123456789'); + }); + + test('replace', function (): any { + let input = TextDocument.create('foo://bar/f', 'html', 0, '012345678901234567890123456789'); + assert.equal(applyEdits(input, [TextEdit.replace(Range.create(Position.create(0, 3), Position.create(0, 6)), 'Hello')]), '012Hello678901234567890123456789'); + assert.equal(applyEdits(input, [TextEdit.replace(Range.create(Position.create(0, 3), Position.create(0, 6)), 'Hello'), TextEdit.replace(Range.create(Position.create(0, 6), Position.create(0, 9)), 'World')]), '012HelloWorld901234567890123456789'); + assert.equal(applyEdits(input, [TextEdit.replace(Range.create(Position.create(0, 3), Position.create(0, 6)), 'Hello'), TextEdit.insert(Position.create(0, 6), 'World')]), '012HelloWorld678901234567890123456789'); + assert.equal(applyEdits(input, [TextEdit.insert(Position.create(0, 6), 'World'), TextEdit.replace(Range.create(Position.create(0, 3), Position.create(0, 6)), 'Hello')]), '012HelloWorld678901234567890123456789'); + assert.equal(applyEdits(input, [TextEdit.insert(Position.create(0, 3), 'World'), TextEdit.replace(Range.create(Position.create(0, 3), Position.create(0, 6)), 'Hello')]), '012WorldHello678901234567890123456789'); + + }); + + test('overlap', function (): any { + let input = TextDocument.create('foo://bar/f', 'html', 0, '012345678901234567890123456789'); + assert.throws(_ => applyEdits(input, [TextEdit.replace(Range.create(Position.create(0, 3), Position.create(0, 6)), 'Hello'), TextEdit.insert(Position.create(0, 3), 'World')])); + assert.throws(_ => applyEdits(input, [TextEdit.replace(Range.create(Position.create(0, 3), Position.create(0, 6)), 'Hello'), TextEdit.insert(Position.create(0, 4), 'World')])); + }); + +}); \ No newline at end of file diff --git a/extensions/html/server/src/utils/arrays.ts b/extensions/html/server/src/utils/arrays.ts index 50c33e519c7..a33ef18597c 100644 --- a/extensions/html/server/src/utils/arrays.ts +++ b/extensions/html/server/src/utils/arrays.ts @@ -14,4 +14,46 @@ export function pushAll(to: T[], from: T[]) { export function contains(arr: T[], val: T) { return arr.indexOf(val) !== -1; -} \ No newline at end of file +} + +/** + * Like `Array#sort` but always stable. Usually runs a little slower `than Array#sort` + * so only use this when actually needing stable sort. + */ +export function mergeSort(data: T[], compare: (a: T, b: T) => number): T[] { + _divideAndMerge(data, compare); + return data; +} + +function _divideAndMerge(data: T[], compare: (a: T, b: T) => number): void { + if (data.length <= 1) { + // sorted + return; + } + const p = (data.length / 2) | 0; + const left = data.slice(0, p); + const right = data.slice(p); + + _divideAndMerge(left, compare); + _divideAndMerge(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++]; + } +} diff --git a/extensions/html/server/src/utils/edits.ts b/extensions/html/server/src/utils/edits.ts index 1b7b0185fe5..66c74be030f 100644 --- a/extensions/html/server/src/utils/edits.ts +++ b/extensions/html/server/src/utils/edits.ts @@ -4,29 +4,29 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import { TextDocument, TextEdit, Position } from 'vscode-languageserver-types'; +import { TextDocument, TextEdit } from 'vscode-languageserver-types'; +import { mergeSort } from './arrays'; export function applyEdits(document: TextDocument, edits: TextEdit[]): string { let text = document.getText(); - let sortedEdits = edits.sort((a, b) => { - let startDiff = comparePositions(a.range.start, b.range.start); - if (startDiff === 0) { - return comparePositions(a.range.end, b.range.end); + let sortedEdits = mergeSort(edits, (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 startDiff; + return 0; }); - sortedEdits.forEach(e => { + let lastModifiedOffset = text.length; + for (let i = sortedEdits.length - 1; i >= 0; i--) { + let e = sortedEdits[i]; let startOffset = document.offsetAt(e.range.start); let endOffset = document.offsetAt(e.range.end); - text = text.substring(0, startOffset) + e.newText + text.substring(endOffset, text.length); - }); - return text; -} - -function comparePositions(p1: Position, p2: Position) { - let diff = p2.line - p1.line; - if (diff === 0) { - return p2.character - p1.character; + if (endOffset <= lastModifiedOffset) { + text = text.substring(0, startOffset) + e.newText + text.substring(endOffset, text.length); + } else { + throw new Error('Ovelapping edit'); + } + lastModifiedOffset = startOffset; } - return diff; + return text; } \ No newline at end of file -- GitLab