diff --git a/spec/app/buffer-spec.coffee b/spec/app/buffer-spec.coffee index dc599741d..cec94ad0c 100644 --- a/spec/app/buffer-spec.coffee +++ b/spec/app/buffer-spec.coffee @@ -666,78 +666,106 @@ describe 'Buffer', -> expect(buffer.positionForCharacterIndex(61)).toEqual [2, 0] expect(buffer.positionForCharacterIndex(408)).toEqual [12, 2] - fdescribe "anchor points", -> - [anchor1Id, anchor2Id, anchor3Id] = [] + fdescribe "markers", -> + [marker1, marker2, marker3] = [] beforeEach -> - anchor1Id = buffer.createAnchorPoint([4, 23]) - anchor2Id = buffer.createAnchorPoint([4, 23], ignoreSameLocationInserts: true) - anchor3Id = buffer.createAnchorPoint([4, 23], surviveSurroundingChanges: true) + marker1 = buffer.createMarker([[4, 20], [4, 23]]) + marker2 = buffer.createMarker([[4, 20], [4, 23]], stayValid: true) describe "when the buffer changes due to a new operation", -> - describe "when the change precedes the anchor point", -> - it "moves the anchor", -> + describe "when the change precedes the marker range", -> + it "moves the marker", -> buffer.insert([4, 5], '...') - expect(buffer.getAnchorPoint(anchor1Id)).toEqual [4, 26] + expect(buffer.getMarkerRange(marker1)).toEqual [[4, 23], [4, 26]] buffer.delete([[4, 5], [4, 8]]) - expect(buffer.getAnchorPoint(anchor1Id)).toEqual [4, 23] + expect(buffer.getMarkerRange(marker1)).toEqual [[4, 20], [4, 23]] buffer.insert([0, 0], '\nhi\n') - expect(buffer.getAnchorPoint(anchor1Id)).toEqual [6, 23] + expect(buffer.getMarkerRange(marker1)).toEqual [[6, 20], [6, 23]] # undo works buffer.undo() - expect(buffer.getAnchorPoint(anchor1Id)).toEqual [4, 23] + expect(buffer.getMarkerRange(marker1)).toEqual [[4, 20], [4, 23]] buffer.undo() - expect(buffer.getAnchorPoint(anchor1Id)).toEqual [4, 26] + expect(buffer.getMarkerRange(marker1)).toEqual [[4, 23], [4, 26]] - describe "when the change follows the anchor point", -> - it "does not move the anchor", -> + describe "when the change follows the marker range", -> + it "does not move the marker", -> buffer.insert([6, 5], '...') - expect(buffer.getAnchorPoint(anchor1Id)).toEqual [4, 23] + expect(buffer.getMarkerRange(marker1)).toEqual [[4, 20], [4, 23]] buffer.delete([[6, 5], [6, 8]]) - expect(buffer.getAnchorPoint(anchor1Id)).toEqual [4, 23] + expect(buffer.getMarkerRange(marker1)).toEqual [[4, 20], [4, 23]] buffer.insert([10, 0], '\nhi\n') - expect(buffer.getAnchorPoint(anchor1Id)).toEqual [4, 23] + expect(buffer.getMarkerRange(marker1)).toEqual [[4, 20], [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 "when the change is an insertion at the start of the marker range", -> + it "does not move the start point, but does move the end point", -> + buffer.insert([4, 20], '...') + expect(buffer.getMarkerRange(marker1)).toEqual [[4, 20], [4, 26]] - 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 is an insertion at the end of the marker range", -> + it "moves the end point", -> + buffer.insert([4, 23], '...') + expect(buffer.getMarkerRange(marker1)).toEqual [[4, 20], [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] + describe "when the change surrounds the marker range", -> + describe "when the marker was created with stayValid: false (the default)", -> + it "invalidates the marker", -> + buffer.delete([[4, 15], [4, 25]]) + expect(buffer.getMarkerRange(marker1)).toBeUndefined() buffer.undo() - expect(buffer.getAnchorPoint(anchor3Id)).toEqual [4, 23] + expect(buffer.getMarkerRange(marker1)).toEqual [[4, 20], [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() + describe "when the marker was created with stayValid: true", -> + it "does not invalidate the marker, but sets it to an empty range at the end of the change", -> + buffer.change([[4, 15], [4, 25]], "...") + expect(buffer.getMarkerRange(marker2)).toEqual [[4, 18], [4, 18]] buffer.undo() - expect(buffer.getAnchorPoint(anchor1Id)).toEqual [4, 23] + expect(buffer.getMarkerRange(marker2)).toEqual [[4, 20], [4, 23]] + + describe "when the change straddles the start of the marker range", -> + describe "when the marker was created with stayValid: false (the default)", -> + it "invalidates the marker", -> + buffer.delete([[4, 15], [4, 22]]) + expect(buffer.getMarkerRange(marker1)).toBeUndefined() + buffer.undo() + expect(buffer.getMarkerRange(marker1)).toEqual [[4, 20], [4, 23]] + + describe "when the marker was created with stayValid: true", -> + it "moves the start of the marker range to the end of the change", -> + buffer.delete([[4, 15], [4, 22]]) + expect(buffer.getMarkerRange(marker2)).toEqual [[4, 15], [4, 16]] + buffer.undo() + expect(buffer.getMarkerRange(marker1)).toEqual [[4, 20], [4, 23]] + + describe "when the change straddles the end of the marker range", -> + describe "when the marker was created with stayValid: false (the default)", -> + it "invalidates the marker", -> + buffer.delete([[4, 22], [4, 25]]) + expect(buffer.getMarkerRange(marker1)).toBeUndefined() + buffer.undo() + expect(buffer.getMarkerRange(marker1)).toEqual [[4, 20], [4, 23]] + + describe "when the marker was created with stayValid: true", -> + it "moves the end of the marker range to the start of the change", -> + buffer.delete([[4, 22], [4, 25]]) + expect(buffer.getMarkerRange(marker2)).toEqual [[4, 20], [4, 22]] + buffer.undo() + expect(buffer.getMarkerRange(marker1)).toEqual [[4, 20], [4, 23]] describe "when the buffer changes due to the undo or redo of a previous operation", -> - it "restores invalidated anchor points when undoing/redoing in the other direction", -> + it "restores invalidated markers when undoing/redoing in the other direction", -> buffer.change([[4, 21], [4, 24]], "foo") - expect(buffer.getAnchorPoint(anchor1Id)).toBeUndefined() - anchor4Id = buffer.createAnchorPoint([4, 23]) + expect(buffer.getMarkerRange(marker1)).toBeUndefined() + marker3 = buffer.createMarker([[4, 20], [4, 23]]) buffer.undo() - expect(buffer.getAnchorPoint(anchor1Id)).toEqual [4, 23] - expect(buffer.getAnchorPoint(anchor4Id)).toBeUndefined() - anchor5Id = buffer.createAnchorPoint([4, 23]) + expect(buffer.getMarkerRange(marker1)).toEqual [[4, 20], [4, 23]] + expect(buffer.getMarkerRange(marker3)).toBeUndefined() + marker4 = buffer.createMarker([[4, 20], [4, 23]]) buffer.redo() - expect(buffer.getAnchorPoint(anchor4Id)).toEqual [4, 23] - expect(buffer.getAnchorPoint(anchor5Id)).toBeUndefined() + expect(buffer.getMarkerRange(marker3)).toEqual [[4, 20], [4, 23]] + expect(buffer.getMarkerRange(marker4)).toBeUndefined() buffer.undo() - expect(buffer.getAnchorPoint(anchor5Id)).toEqual [4, 23] + expect(buffer.getMarkerRange(marker4)).toEqual [[4, 20], [4, 23]] describe "anchors", -> [anchor, destroyHandler] = [] @@ -873,7 +901,6 @@ describe 'Buffer', -> expect(contentsModifiedHandler).toHaveBeenCalledWith(differsFromDisk:true) bufferToDelete.destroy() - describe "when the buffer text has been changed", -> it "triggers the contents-modified event 'stoppedChangingDelay' ms after the last buffer change", -> delay = buffer.stoppedChangingDelay diff --git a/src/app/anchor-point.coffee b/src/app/anchor-point.coffee deleted file mode 100644 index ff22b9323..000000000 --- a/src/app/anchor-point.coffee +++ /dev/null @@ -1,50 +0,0 @@ -_ = require 'underscore' -Point = require 'point' - -module.exports = -class AnchorPoint - position: null - ignoreSameLocationInserts: false - surviveSurroundingChanges: false - - 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 = @getPosition() - - return if oldRange.containsPoint(position, exclusive: true) - return if @ignoreSameLocationInserts and position.isEqual(oldRange.start) - return if position.isLessThan(oldRange.end) - - newRow = newRange.end.row - newColumn = newRange.end.column - if position.row == oldRange.end.row - newColumn += position.column - oldRange.end.column - else - newColumn = position.column - newRow += position.row - oldRange.end.row - - @setPosition([newRow, newColumn]) - - setPosition: (position, options={}) -> - @position = Point.fromObject(position) - clip = options.clip ? true - @position = @buffer.clipPosition(@position) if clip - - getPosition: -> - @position - - 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 dd5eda780..bebcf384b 100644 --- a/src/app/buffer-change-operation.coffee +++ b/src/app/buffer-change-operation.coffee @@ -8,8 +8,8 @@ class BufferChangeOperation oldText: null newRange: null newText: null - anchorPointsToRestoreOnUndo: null - anchorPointsToRestoreOnRedo: null + markersToRestoreOnUndo: null + markersToRestoreOnRedo: null constructor: ({@buffer, @oldRange, @newText, @options}) -> @options ?= {} @@ -17,7 +17,7 @@ class BufferChangeOperation do: -> @oldText = @buffer.getTextInRange(@oldRange) @newRange = @calculateNewRange(@oldRange, @newText) - @anchorPointsToRestoreOnUndo = @invalidateAnchorPoints(@oldRange) + @markersToRestoreOnUndo = @invalidateMarkers(@oldRange) @changeBuffer oldRange: @oldRange newRange: @newRange @@ -25,16 +25,16 @@ class BufferChangeOperation newText: @newText redo: -> - @restoreAnchorPoints(@anchorPointsToRestoreOnRedo) + @restoreMarkers(@markersToRestoreOnRedo) undo: -> - @anchorPointsToRestoreOnRedo = @invalidateAnchorPoints(@newRange) + @markersToRestoreOnRedo = @invalidateMarkers(@newRange) @changeBuffer oldRange: @newRange newRange: @oldRange oldText: @newText newText: @oldText - @restoreAnchorPoints(@anchorPointsToRestoreOnUndo) + @restoreMarkers(@markersToRestoreOnUndo) splitLines: (text) -> lines = text.split('\n') @@ -47,16 +47,6 @@ 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) @@ -83,7 +73,7 @@ class BufferChangeOperation @buffer.trigger 'changed', event @buffer.scheduleStoppedChangingEvent() @buffer.updateAnchors(event) - @buffer.updateAnchorPoints(event) + @updateMarkers(event) newRange calculateNewRange: (oldRange, newText) -> @@ -96,3 +86,17 @@ class BufferChangeOperation newRange.end.row += lastLineIndex newRange.end.column = lines[lastLineIndex].length newRange + + invalidateMarkers: (oldRange) -> + _.compact(@buffer.getMarkers().map (marker) -> marker.tryToInvalidate(oldRange)) + + updateMarkers: (bufferChange) -> + marker.handleBufferChange(bufferChange) for marker in @buffer.getMarkers() + + restoreMarkers: (markersToRestore) -> + for [id, previousRange] in markersToRestore + if existingMarker = @buffer.validMarkers[id] + existingMarker.setRange(previousRange) + else + @buffer.validMarkers[id] = @buffer.invalidMarkers[id] + diff --git a/src/app/buffer-marker.coffee b/src/app/buffer-marker.coffee new file mode 100644 index 000000000..32e362b33 --- /dev/null +++ b/src/app/buffer-marker.coffee @@ -0,0 +1,81 @@ +_ = require 'underscore' +Point = require 'point' +Range = require 'range' + +module.exports = +class BufferMarker + headPosition: null + tailPosition: null + stayValid: false + + constructor: ({@id, @buffer, range, @stayValid}) -> + @setRange(range) + + setRange: (range) -> + range = @buffer.clipRange(range) + @tailPosition = range.start + @headPosition = range.end + + getRange: -> + new Range(@tailPosition, @headPosition) + + getStartPosition: -> + @getRange().start + + getEndPosition: -> + @getRange().end + + tryToInvalidate: (oldRange) -> + containsStart = oldRange.containsPoint(@getStartPosition(), exclusive: true) + containsEnd = oldRange.containsPoint(@getEndPosition(), exclusive: true) + return unless containsEnd or containsStart + + if @stayValid + previousRange = @getRange() + if containsStart and containsEnd + @setRange([oldRange.end, oldRange.end]) + else if containsStart + @setRange([oldRange.end, @getEndPosition()]) + else + @setRange([@getStartPosition(), oldRange.start]) + [@id, previousRange] + else + @invalidate() + [@id] + + handleBufferChange: (bufferChange) -> + @setTailPosition(@updatePosition(@tailPosition, bufferChange, true), clip: false) + @setHeadPosition(@updatePosition(@headPosition, bufferChange, false), clip: false) + + updatePosition: (position, bufferChange, isFirstPoint) -> + { oldRange, newRange } = bufferChange + + return position if oldRange.containsPoint(position, exclusive: true) + return position if isFirstPoint and oldRange.start.isEqual(position) + return position if position.isLessThan(oldRange.end) + + newRow = newRange.end.row + newColumn = newRange.end.column + + if position.row == oldRange.end.row + newColumn += position.column - oldRange.end.column + else + newColumn = position.column + newRow += position.row - oldRange.end.row + + [newRow, newColumn] + + setTailPosition: (tailPosition, options={}) -> + @tailPosition = Point.fromObject(tailPosition) + @tailPosition = @buffer.clipPosition(@tailPosition) if options.clip ? true + + setHeadPosition: (headPosition, options={}) -> + @headPosition = Point.fromObject(headPosition) + @headPosition = @buffer.clipPosition(@headPosition) if options.clip ? true + + getPosition: -> + @position + + invalidate: (preserve) -> + delete @buffer.validMarkers[@id] + @buffer.invalidMarkers[@id] = this diff --git a/src/app/buffer.coffee b/src/app/buffer.coffee index dc59c2ae7..ad02591a5 100644 --- a/src/app/buffer.coffee +++ b/src/app/buffer.coffee @@ -8,7 +8,7 @@ UndoManager = require 'undo-manager' BufferChangeOperation = require 'buffer-change-operation' Anchor = require 'anchor' AnchorRange = require 'anchor-range' -AnchorPoint = require 'anchor-point' +BufferMarker = require 'buffer-marker' module.exports = class Buffer @@ -22,17 +22,17 @@ class Buffer lines: null lineEndings: null file: null - validAnchorPointsById: null - invalidAnchorPointsById: null + validMarkers: null + invalidMarkers: null anchors: null anchorRanges: null refcount: 0 constructor: (path, @project) -> @id = @constructor.idCounter++ - @nextAnchorPointId = 1 - @validAnchorPointsById = {} - @invalidAnchorPointsById = {} + @nextMarkerId = 1 + @validMarkers = {} + @invalidMarkers = {} @anchors = [] @anchorRanges = [] @lines = [''] @@ -268,21 +268,23 @@ 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() + getMarkers: -> + _.values(@validMarkers) - getAnchorPoints: -> - _.values(@validAnchorPointsById) + createMarker: (range, options) -> + marker = new BufferMarker(_.extend({ + id: @nextMarkerId++ + buffer: this + range: range + }, options)) + @validMarkers[marker.id] = marker + marker.id - createAnchorPoint: (position, options) -> - id = @nextAnchorPointId++ - params = _.extend({buffer: this, id, position}, options) - @validAnchorPointsById[id] = new AnchorPoint(params) - id + getMarkerPosition: (id) -> + @validMarkers[id]?.getPosition() - getAnchorPoint: (id) -> - @validAnchorPointsById[id]?.getPosition() + getMarkerRange: (id) -> + @validMarkers[id]?.getRange() getAnchors: -> new Array(@anchors...) diff --git a/src/app/display-buffer-anchor-point.coffee b/src/app/display-buffer-anchor-point.coffee new file mode 100644 index 000000000..4eaa045c9 --- /dev/null +++ b/src/app/display-buffer-anchor-point.coffee @@ -0,0 +1,15 @@ +module.exports = +class DisplayBufferAnchorPoint + bufferPosition: null + screenPosition: null + + constructor: ({@displayBuffer, bufferPosition, screenPosition}) -> + {@buffer} = @displayBuffer + if screenPosition + bufferPosition = @displayBuffer.bufferPositionForScreenPosition(screenPosition) + + @id = @buffer.createAnchorPoint(bufferPosition) + + getBufferPosition: -> + @buffer.getAnchorPoint(@id) + diff --git a/src/app/edit-session.coffee b/src/app/edit-session.coffee index 7f006cda9..e7f3165b7 100644 --- a/src/app/edit-session.coffee +++ b/src/app/edit-session.coffee @@ -9,7 +9,6 @@ EventEmitter = require 'event-emitter' Subscriber = require 'subscriber' Range = require 'range' AnchorRange = require 'anchor-range' -AnchorPoint = require 'anchor-point' _ = require 'underscore' fs = require 'fs'