diff --git a/spec/app/buffer-spec.coffee b/spec/app/buffer-spec.coffee index ea10907cf..84fea60db 100644 --- a/spec/app/buffer-spec.coffee +++ b/spec/app/buffer-spec.coffee @@ -658,6 +658,64 @@ describe 'Buffer', -> expect(buffer.positionForCharacterIndex(61)).toEqual [2, 0] expect(buffer.positionForCharacterIndex(408)).toEqual [12, 2] + fdescribe "anchor points", -> + [anchor1Id, anchor2Id, anchor3Id] = [] + beforeEach -> + anchor1Id = buffer.addAnchorPoint([4, 23]) + anchor2Id = buffer.addAnchorPoint([4, 23], ignoreSameLocationInserts: true) + anchor3Id = buffer.addAnchorPoint([4, 23], surviveSurroundingChanges: true) + + describe "when the buffer changes", -> + describe "when the change precedes the anchor point", -> + it "moves the anchor", -> + buffer.insert([4, 5], '...') + expect(buffer.getAnchorPoint(anchor1Id)).toEqual [4, 26] + buffer.delete([[4, 5], [4, 8]]) + expect(buffer.getAnchorPoint(anchor1Id)).toEqual [4, 23] + buffer.insert([0, 0], '\nhi\n') + expect(buffer.getAnchorPoint(anchor1Id)).toEqual [6, 23] + + # undo works + buffer.undo() + expect(buffer.getAnchorPoint(anchor1Id)).toEqual [4, 23] + buffer.undo() + expect(buffer.getAnchorPoint(anchor1Id)).toEqual [4, 26] + + describe "when the change follows the anchor point", -> + it "does not move the anchor", -> + buffer.insert([6, 5], '...') + expect(buffer.getAnchorPoint(anchor1Id)).toEqual [4, 23] + buffer.delete([[6, 5], [6, 8]]) + expect(buffer.getAnchorPoint(anchor1Id)).toEqual [4, 23] + buffer.insert([10, 0], '\nhi\n') + expect(buffer.getAnchorPoint(anchor1Id)).toEqual [4, 23] + + describe "when the change is an insertion at the same location as the anchor point", -> + describe "if the anchor ignores same location inserts", -> + it "treats the insertion as being to the right of the anchor and does not move it", -> + buffer.insert([4, 23], '...') + expect(buffer.getAnchorPoint(anchor2Id)).toEqual [4, 23] + + describe "if the anchor observes same location inserts", -> + it "treats the insertion as being to the left of the anchor and moves it accordingly", -> + buffer.insert([4, 23], '...') + expect(buffer.getAnchorPoint(anchor1Id)).toEqual [4, 26] + + describe "when the change surrounds the anchor point", -> + describe "when the anchor survives surrounding changes", -> + it "preserves the anchor", -> + buffer.delete([[4, 20], [4, 26]]) + expect(buffer.getAnchorPoint(anchor3Id)).toEqual [4, 20] + buffer.undo() + expect(buffer.getAnchorPoint(anchor3Id)).toEqual [4, 23] + + describe "when the anchor does not survive surrounding changes", -> + it "invalidates the anchor but re-validates it on undo", -> + buffer.delete([[4, 20], [4, 26]]) + expect(buffer.getAnchorPoint(anchor1Id)).toBeUndefined() + buffer.undo() + expect(buffer.getAnchorPoint(anchor1Id)).toEqual [4, 23] + describe "anchors", -> [anchor, destroyHandler] = [] diff --git a/spec/app/edit-session-spec.coffee b/spec/app/edit-session-spec.coffee index 329797c43..01ab8648e 100644 --- a/spec/app/edit-session-spec.coffee +++ b/spec/app/edit-session-spec.coffee @@ -1775,70 +1775,6 @@ describe "EditSession", -> describe "anchors", -> [anchor, destroyHandler] = [] - fdescribe "anchor points", -> - [anchor1Id, anchor2Id, anchor3Id] = [] - beforeEach -> - anchor1Id = editSession.addAnchorPointAtBufferPosition([4, 23]) - anchor2Id = editSession.addAnchorPointAtBufferPosition([4, 23], ignoreSameLocationInserts: true) - anchor3Id = editSession.addAnchorPointAtBufferPosition([4, 23], surviveSurroundingChanges: true) - - describe "when the buffer changes", -> - describe "when the change precedes the anchor point", -> - it "moves the anchor", -> - buffer.insert([4, 5], '...') - expect(editSession.getAnchorPointBufferPosition(anchor1Id)).toEqual [4, 26] - buffer.delete([[4, 5], [4, 8]]) - expect(editSession.getAnchorPointBufferPosition(anchor1Id)).toEqual [4, 23] - buffer.insert([0, 0], '\nhi\n') - expect(editSession.getAnchorPointBufferPosition(anchor1Id)).toEqual [6, 23] - - # undo works - editSession.undo() - expect(editSession.getAnchorPointBufferPosition(anchor1Id)).toEqual [4, 23] - editSession.undo() - expect(editSession.getAnchorPointBufferPosition(anchor1Id)).toEqual [4, 26] - - describe "when the change follows the anchor point", -> - it "does not move the anchor", -> - buffer.insert([6, 5], '...') - expect(editSession.getAnchorPointBufferPosition(anchor1Id)).toEqual [4, 23] - buffer.delete([[6, 5], [6, 8]]) - expect(editSession.getAnchorPointBufferPosition(anchor1Id)).toEqual [4, 23] - buffer.insert([10, 0], '\nhi\n') - expect(editSession.getAnchorPointBufferPosition(anchor1Id)).toEqual [4, 23] - - describe "when the change is an insertion at the same location as the anchor point", -> - describe "if the anchor ignores same location inserts", -> - it "treats the insertion as being to the right of the anchor and does not move it", -> - buffer.insert([4, 23], '...') - expect(editSession.getAnchorPointBufferPosition(anchor2Id)).toEqual [4, 23] - - describe "if the anchor observes same location inserts", -> - it "treats the insertion as being to the left of the anchor and moves it accordingly", -> - buffer.insert([4, 23], '...') - expect(editSession.getAnchorPointBufferPosition(anchor1Id)).toEqual [4, 26] - - describe "when the change surrounds the anchor point", -> - describe "when the anchor survives surrounding changes", -> - it "preserves the anchor, moving it to the start of the change, but restores its location on undo", -> - buffer.delete([[4, 20], [4, 26]]) - expect(editSession.getAnchorPointBufferPosition(anchor3Id)).toEqual [4, 20] - editSession.undo() - expect(editSession.getAnchorPointBufferPosition(anchor3Id)).toEqual [4, 23] - - describe "when the anchor does not survive surrounding changes", -> - it "invalidates the anchor but re-validates it on undo", -> - buffer.delete([[4, 20], [4, 26]]) - expect(editSession.getAnchorPointBufferPosition(anchor1Id)).toBeUndefined() - editSession.undo() - expect(editSession.getAnchorPointBufferPosition(anchor1Id)).toEqual [4, 23] - - describe ".clipBufferPosition(bufferPosition)", -> - it "clips the given position to a valid position", -> - expect(editSession.clipBufferPosition([-1, -1])).toEqual [0,0] - expect(editSession.clipBufferPosition([Infinity, Infinity])).toEqual [12,2] - expect(editSession.clipBufferPosition([8, 57])).toEqual [8, 56] - describe ".deleteLine()", -> it "deletes the first line when the cursor is there", -> editSession.getCursor().moveToTop() diff --git a/src/app/anchor-point.coffee b/src/app/anchor-point.coffee index 6bbfc66fc..ff22b9323 100644 --- a/src/app/anchor-point.coffee +++ b/src/app/anchor-point.coffee @@ -3,24 +3,27 @@ Point = require 'point' module.exports = class AnchorPoint - bufferPosition: null - screenPosition: null + position: null ignoreSameLocationInserts: false surviveSurroundingChanges: false - constructor: ({@id, @editSession, bufferPosition, @ignoreSameLocationInserts, @surviveSurroundingChanges}) -> - @setBufferPosition(bufferPosition) + constructor: ({@id, @buffer, position, @ignoreSameLocationInserts, @surviveSurroundingChanges}) -> + @setPosition(position) + + tryToInvalidate: (oldRange) -> + if oldRange.containsPoint(@getPosition(), exclusive: true) + position = @getPosition() + if @surviveSurroundingChanges + @setPosition(oldRange.start) + else + @invalidate() + [@id, position] handleBufferChange: (e) -> { oldRange, newRange } = e - position = @getBufferPosition() + position = @getPosition() - if oldRange.containsPoint(position, exclusive: true) - if @surviveSurroundingChanges - @setBufferPosition(oldRange.start) - else - @invalidate() - return + return if oldRange.containsPoint(position, exclusive: true) return if @ignoreSameLocationInserts and position.isEqual(oldRange.start) return if position.isLessThan(oldRange.end) @@ -32,48 +35,16 @@ class AnchorPoint newColumn = position.column newRow += position.row - oldRange.end.row - @setBufferPosition([newRow, newColumn]) + @setPosition([newRow, newColumn]) - setBufferPosition: (position, options={}) -> - @bufferPosition = Point.fromObject(position) + setPosition: (position, options={}) -> + @position = Point.fromObject(position) clip = options.clip ? true - @bufferPosition = @editSession.clipBufferPosition(@bufferPosition) if clip - @refreshScreenPosition(options) + @position = @buffer.clipPosition(@position) if clip - getBufferPosition: -> - @bufferPosition + getPosition: -> + @position - setScreenPosition: (position, options={}) -> - oldScreenPosition = @screenPosition - oldBufferPosition = @bufferPosition - @screenPosition = Point.fromObject(position) - clip = options.clip ? true - assignBufferPosition = options.assignBufferPosition ? true - - @screenPosition = @editSession.clipScreenPosition(@screenPosition, options) if clip - @bufferPosition = @editSession.bufferPositionForScreenPosition(@screenPosition, options) if assignBufferPosition - - Object.freeze @screenPosition - Object.freeze @bufferPosition - -# unless @screenPosition.isEqual(oldScreenPosition) -# @trigger 'moved', -# oldScreenPosition: oldScreenPosition -# newScreenPosition: @screenPosition -# oldBufferPosition: oldBufferPosition -# newBufferPosition: @bufferPosition -# bufferChange: options.bufferChange - - getScreenPosition: -> - @screenPosition - - getScreenRow: -> - @screenPosition.row - - refreshScreenPosition: (options={}) -> - return unless @editSession - screenPosition = @editSession.screenPositionForBufferPosition(@bufferPosition, options) - @setScreenPosition(screenPosition, bufferChange: options.bufferChange, clip: false, assignBufferPosition: false) - - invalidate: -> - @editSession.removeAnchorPoint(@id) \ No newline at end of file + invalidate: (preserve) -> + delete @buffer.validAnchorPointsById[@id] + @buffer.invalidAnchorPointsById[@id] = this diff --git a/src/app/buffer-change-operation.coffee b/src/app/buffer-change-operation.coffee index 06b241c75..f6e865861 100644 --- a/src/app/buffer-change-operation.coffee +++ b/src/app/buffer-change-operation.coffee @@ -8,25 +8,35 @@ class BufferChangeOperation oldText: null newRange: null newText: null + anchorPointsToRestoreOnUndo: null + anchorPointsToRestoreOnRedo: null constructor: ({@buffer, @oldRange, @newText, @options}) -> @options ?= {} + + do: -> @oldText = @buffer.getTextInRange(@oldRange) @newRange = @calculateNewRange(@oldRange, @newText) + @anchorPointsToRestoreOnUndo = @invalidateAnchorPoints(@oldRange) @changeBuffer oldRange: @oldRange newRange: @newRange oldText: @oldText newText: @newText + redo: -> + @restoreAnchorPoints(@anchorPointsToRestoreOnRedo) + undo: -> + @anchorPointsToRestoreOnRedo = @invalidateAnchorPoints(@newRange) @changeBuffer oldRange: @newRange newRange: @oldRange oldText: @newText newText: @oldText + @restoreAnchorPoints(@anchorPointsToRestoreOnUndo) splitLines: (text) -> lines = text.split('\n') @@ -39,9 +49,18 @@ class BufferChangeOperation lineEndings[index] = '\n' {lines, lineEndings} + invalidateAnchorPoints: (oldRange) -> + _.compact(@buffer.getAnchorPoints().map (pt) -> pt.tryToInvalidate(oldRange)) + + restoreAnchorPoints: (anchorPoints) -> + for [id, position] in anchorPoints + if existingAnchorPoint = @buffer.validAnchorPointsById[id] + existingAnchorPoint.setPosition(position) + else + @buffer.validAnchorPointsById[id] = @buffer.invalidAnchorPointsById[id] + changeBuffer: ({ oldRange, newRange, newText, oldText }) -> { prefix, suffix } = @buffer.prefixAndSuffixForRange(oldRange) - {lines, lineEndings} = @splitLines(newText) lastLineIndex = lines.length - 1 @@ -66,6 +85,7 @@ class BufferChangeOperation @buffer.trigger 'changed', event @buffer.scheduleStoppedChangingEvent() @buffer.updateAnchors(event) + @buffer.updateAnchorPoints(event) newRange calculateNewRange: (oldRange, newText) -> diff --git a/src/app/buffer.coffee b/src/app/buffer.coffee index 266b30ba2..07f4abc77 100644 --- a/src/app/buffer.coffee +++ b/src/app/buffer.coffee @@ -8,6 +8,7 @@ UndoManager = require 'undo-manager' BufferChangeOperation = require 'buffer-change-operation' Anchor = require 'anchor' AnchorRange = require 'anchor-range' +AnchorPoint = require 'anchor-point' module.exports = class Buffer @@ -21,12 +22,17 @@ class Buffer lines: null lineEndings: null file: null + validAnchorPointsById: null + invalidAnchorPointsById: null anchors: null anchorRanges: null refcount: 0 constructor: (path, @project) -> @id = @constructor.idCounter++ + @nextAnchorPointId = 1 + @validAnchorPointsById = {} + @invalidAnchorPointsById = {} @anchors = [] @anchorRanges = [] @lines = [''] @@ -258,6 +264,22 @@ class Buffer isEmpty: -> @lines.length is 1 and @lines[0].length is 0 + updateAnchorPoints: (bufferChange) -> + return unless bufferChange + anchorPoint.handleBufferChange(bufferChange) for anchorPoint in @getAnchorPoints() + + getAnchorPoints: -> + _.values(@validAnchorPointsById) + + addAnchorPoint: (position, options) -> + id = @nextAnchorPointId++ + params = _.extend({buffer: this, id, position}, options) + @validAnchorPointsById[id] = new AnchorPoint(params) + id + + getAnchorPoint: (id) -> + @validAnchorPointsById[id]?.getPosition() + getAnchors: -> new Array(@anchors...) addAnchor: (options) -> diff --git a/src/app/edit-session.coffee b/src/app/edit-session.coffee index 8856dba0f..7d77ac731 100644 --- a/src/app/edit-session.coffee +++ b/src/app/edit-session.coffee @@ -355,25 +355,6 @@ class EditSession pushOperation: (operation) -> @buffer.pushOperation(operation, this) - updateAnchorPoints: (bufferChange) -> - return unless bufferChange - anchorPoint.handleBufferChange(bufferChange) for anchorPoint in @getAnchorPoints() - - getAnchorPoints: -> - _.values(@anchorPointsById) - - addAnchorPointAtBufferPosition: (bufferPosition, options) -> - id = @nextAnchorPointId++ - params = _.extend({editSession: this, id, bufferPosition}, options) - @anchorPointsById[id] = new AnchorPoint(params) - id - - getAnchorPointBufferPosition: (id) -> - @anchorPointsById[id]?.getBufferPosition() - - removeAnchorPoint: (id) -> - delete @anchorPointsById[id] - getAnchors: -> new Array(@anchors...)