diff --git a/src/editor/model.js b/src/editor/model.js index 2f1e5218d8..c5b80247f6 100644 --- a/src/editor/model.js +++ b/src/editor/model.js @@ -16,6 +16,8 @@ limitations under the License. */ import {diffAtCaret, diffDeletion} from "./diff"; +import DocumentPosition from "./position"; +import Range from "./range"; export default class EditorModel { constructor(parts, partCreator, updateCallback = null) { @@ -197,7 +199,7 @@ export default class EditorModel { this._updateCallback(pos); } - _mergeAdjacentParts(docPos) { + _mergeAdjacentParts() { let prevPart; for (let i = 0; i < this._parts.length; ++i) { let part = this._parts[i]; @@ -339,19 +341,32 @@ export default class EditorModel { return new DocumentPosition(index, totalOffset - currentOffset); } -} -class DocumentPosition { - constructor(index, offset) { - this._index = index; - this._offset = offset; + startRange(position) { + return new Range(this, position); } - get index() { - return this._index; - } - - get offset() { - return this._offset; + // called from Range.replace + replaceRange(startPosition, endPosition, parts) { + const newStartPartIndex = this._splitAt(startPosition); + const idxDiff = newStartPartIndex - startPosition.index; + // if both position are in the same part, and we split it at start position, + // the offset of the end position needs to be decreased by the offset of the start position + const removedOffset = startPosition.index === endPosition.index ? startPosition.offset : 0; + const adjustedEndPosition = new DocumentPosition( + endPosition.index + idxDiff, + endPosition.offset - removedOffset, + ); + const newEndPartIndex = this._splitAt(adjustedEndPosition); + for (let i = newEndPartIndex - 1; i >= newStartPartIndex; --i) { + this._removePart(i); + } + let insertIdx = newStartPartIndex; + for (const part of parts) { + this._insertPart(insertIdx, part); + insertIdx += 1; + } + this._mergeAdjacentParts(); + this._updateCallback(); } } diff --git a/src/editor/position.js b/src/editor/position.js new file mode 100644 index 0000000000..c771482012 --- /dev/null +++ b/src/editor/position.js @@ -0,0 +1,38 @@ +/* +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +export default class DocumentPosition { + constructor(index, offset) { + this._index = index; + this._offset = offset; + } + + get index() { + return this._index; + } + + get offset() { + return this._offset; + } + + compare(otherPos) { + if (this._index === otherPos._index) { + return this._offset - otherPos._offset; + } else { + return this._index - otherPos._index; + } + } +} diff --git a/src/editor/range.js b/src/editor/range.js new file mode 100644 index 0000000000..e2ecc5d12b --- /dev/null +++ b/src/editor/range.js @@ -0,0 +1,53 @@ +/* +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +export default class Range { + constructor(model, startPosition, endPosition = startPosition) { + this._model = model; + this._start = startPosition; + this._end = endPosition; + } + + moveStart(delta) { + this._start = this._start.forwardsWhile(this._model, () => { + delta -= 1; + return delta >= 0; + }); + } + + expandBackwardsWhile(predicate) { + this._start = this._start.backwardsWhile(this._model, predicate); + } + + get text() { + let text = ""; + this._start.iteratePartsBetween(this._end, this._model, (part, startIdx, endIdx) => { + const t = part.text.substring(startIdx, endIdx); + text = text + t; + }); + return text; + } + + replace(parts) { + const newLength = parts.reduce((sum, part) => sum + part.text.length, 0); + let oldLength = 0; + this._start.iteratePartsBetween(this._end, this._model, (part, startIdx, endIdx) => { + oldLength += endIdx - startIdx; + }); + this._model.replaceRange(this._start, this._end, parts); + return newLength - oldLength; + } +} diff --git a/test/editor/range-test.js b/test/editor/range-test.js new file mode 100644 index 0000000000..5a95da952d --- /dev/null +++ b/test/editor/range-test.js @@ -0,0 +1,88 @@ +/* +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import expect from 'expect'; +import EditorModel from "../../src/editor/model"; +import {createPartCreator} from "./mock"; + +function createRenderer() { + const render = (c) => { + render.caret = c; + render.count += 1; + }; + render.count = 0; + render.caret = null; + return render; +} + +const pillChannel = "#riot-dev:matrix.org"; + +describe('editor/range', function() { + it('range on empty model', function() { + const renderer = createRenderer(); + const pc = createPartCreator(); + const model = new EditorModel([], pc, renderer); + const range = model.startRange(model.positionForOffset(0, true)); // after "world" + let called = false; + range.expandBackwardsWhile(chr => { + called = true; + return true; + }); + expect(called).toBe(false); + expect(range.text).toBe(""); + }); + it('range replace within a part', function() { + const renderer = createRenderer(); + const pc = createPartCreator(); + const model = new EditorModel([pc.plain("hello world!!!!")], pc, renderer); + const range = model.startRange(model.positionForOffset(11)); // after "world" + range.expandBackwardsWhile((index, offset) => model.parts[index].text[offset] !== " "); + expect(range.text).toBe("world"); + range.replace([pc.roomPill(pillChannel)]); + console.log({parts: JSON.stringify(model.serializeParts())}); + expect(model.parts[0].type).toBe("plain"); + expect(model.parts[0].text).toBe("hello "); + expect(model.parts[1].type).toBe("room-pill"); + expect(model.parts[1].text).toBe(pillChannel); + expect(model.parts[2].type).toBe("plain"); + expect(model.parts[2].text).toBe("!!!!"); + expect(model.parts.length).toBe(3); + expect(renderer.count).toBe(1); + }); + it('range replace across parts', function() { + const renderer = createRenderer(); + const pc = createPartCreator(); + const model = new EditorModel([ + pc.plain("try to re"), + pc.plain("pla"), + pc.plain("ce "), + pc.plain("me"), + ], pc, renderer); + const range = model.startRange(model.positionForOffset(14)); // after "replace" + range.expandBackwardsWhile((index, offset) => model.parts[index].text[offset] !== " "); + expect(range.text).toBe("replace"); + console.log("range.text", {text: range.text}); + range.replace([pc.roomPill(pillChannel)]); + expect(model.parts[0].type).toBe("plain"); + expect(model.parts[0].text).toBe("try to "); + expect(model.parts[1].type).toBe("room-pill"); + expect(model.parts[1].text).toBe(pillChannel); + expect(model.parts[2].type).toBe("plain"); + expect(model.parts[2].text).toBe(" me"); + expect(model.parts.length).toBe(3); + expect(renderer.count).toBe(1); + }); +});