diff --git a/spec/app/display-buffer-spec.coffee b/spec/app/display-buffer-spec.coffee index 8065cd1af..a95c35b8d 100644 --- a/spec/app/display-buffer-spec.coffee +++ b/spec/app/display-buffer-spec.coffee @@ -210,7 +210,6 @@ describe "DisplayBuffer", -> expect(line5.text).toMatch /9-+/ outerFold.destroy() - [line4, line5, line6, line7] = displayBuffer.linesForRows(4, 7) expect(line4.fold).toBeUndefined() expect(line4.text).toMatch /^4-+/ @@ -234,11 +233,11 @@ describe "DisplayBuffer", -> describe "when creating a fold where one already exists", -> it "returns existing fold and does't create new fold", -> fold = displayBuffer.createFold(0,10) - expect(displayBuffer.activeFolds[0].length).toBe 1 + expect(displayBuffer.findMarkers(class: 'fold').length).toBe 1 newFold = displayBuffer.createFold(0,10) expect(newFold).toBe fold - expect(displayBuffer.activeFolds[0].length).toBe 1 + expect(displayBuffer.findMarkers(class: 'fold').length).toBe 1 describe "when a fold is created inside an existing folded region", -> it "creates/destroys the fold, but does not trigger change event", -> @@ -294,8 +293,8 @@ describe "DisplayBuffer", -> buffer.delete([[1, 1], [2, 0]]) buffer.insert([0, 1], "\nnew") - expect(fold1.startRow).toBe 2 - expect(fold1.endRow).toBe 4 + expect(fold1.getStartRow()).toBe 2 + expect(fold1.getEndRow()).toBe 4 describe "when the old range precedes lines with a fold", -> describe "when the new range precedes lines with a fold", -> @@ -325,8 +324,8 @@ describe "DisplayBuffer", -> it "replaces lines in the portion of the range that precedes the fold and adjusts the end of the fold to encompass additional lines", -> buffer.change([[1, 1], [3, 0]], "a\nb\nc\nd\n") - expect(fold1.startRow).toBe 2 - expect(fold1.endRow).toBe 6 + expect(fold1.getStartRow()).toBe 2 + expect(fold1.getEndRow()).toBe 6 expect(displayBuffer.lineForRow(1).text).toBe '1a' expect(displayBuffer.lineForRow(2).text).toBe 'b' @@ -348,8 +347,8 @@ describe "DisplayBuffer", -> describe "when the end of the new range precedes the end of the fold", -> it "updates the fold and ensures the change is present when the fold is destroyed", -> buffer.insert([3, 0], '\n') - expect(fold1.startRow).toBe 2 - expect(fold1.endRow).toBe 5 + expect(fold1.getStartRow()).toBe 2 + expect(fold1.getEndRow()).toBe 5 expect(displayBuffer.lineForRow(1).text).toBe "1" expect(displayBuffer.lineForRow(2).text).toBe "2" @@ -364,8 +363,8 @@ describe "DisplayBuffer", -> describe "when the end of the new range exceeds the end of the fold", -> it "expands the fold to contain all the inserted lines", -> buffer.change([[3, 0], [4, 0]], 'a\nb\nc\nd\n') - expect(fold1.startRow).toBe 2 - expect(fold1.endRow).toBe 7 + expect(fold1.getStartRow()).toBe 2 + expect(fold1.getEndRow()).toBe 7 expect(displayBuffer.lineForRow(1).text).toBe "1" expect(displayBuffer.lineForRow(2).text).toBe "2" @@ -427,21 +426,21 @@ describe "DisplayBuffer", -> describe ".destroyFoldsContainingBufferRow(row)", -> it "destroys all folds containing the given row", -> - displayBuffer.createFold(2, 4) - displayBuffer.createFold(2, 6) - displayBuffer.createFold(7, 8) - displayBuffer.createFold(1, 9) - displayBuffer.createFold(11, 12) + displayBuffer.createFold(2, 4) + displayBuffer.createFold(2, 6) + displayBuffer.createFold(7, 8) + displayBuffer.createFold(1, 9) + displayBuffer.createFold(11, 12) - expect(displayBuffer.lineForRow(1).text).toBe '1' - expect(displayBuffer.lineForRow(2).text).toBe '10' + expect(displayBuffer.lineForRow(1).text).toBe '1' + expect(displayBuffer.lineForRow(2).text).toBe '10' - displayBuffer.destroyFoldsContainingBufferRow(2) - expect(displayBuffer.lineForRow(1).text).toBe '1' - expect(displayBuffer.lineForRow(2).text).toBe '2' - expect(displayBuffer.lineForRow(7).fold).toBeDefined() - expect(displayBuffer.lineForRow(8).text).toMatch /^9-+/ - expect(displayBuffer.lineForRow(10).fold).toBeDefined() + displayBuffer.destroyFoldsContainingBufferRow(2) + expect(displayBuffer.lineForRow(1).text).toBe '1' + expect(displayBuffer.lineForRow(2).text).toBe '2' + expect(displayBuffer.lineForRow(7).fold).toBeDefined() + expect(displayBuffer.lineForRow(8).text).toMatch /^9-+/ + expect(displayBuffer.lineForRow(10).fold).toBeDefined() describe ".clipScreenPosition(screenPosition, wrapBeyondNewlines: false, wrapAtSoftNewlines: false, skipAtomicTokens: false)", -> beforeEach -> diff --git a/src/app/buffer-marker.coffee b/src/app/buffer-marker.coffee index eb9c2935f..c481ee1bd 100644 --- a/src/app/buffer-marker.coffee +++ b/src/app/buffer-marker.coffee @@ -55,6 +55,10 @@ class BufferMarker return false unless @getRange().start.row == value when 'endRow' return false unless @getRange().end.row == value + when 'containsRange' + return false unless @getRange().containsRange(value, exclusive: true) + when 'containsRow' + return false unless @getRange().containsRow(value) else return false unless _.isEqual(@attributes[key], value) true diff --git a/src/app/display-buffer-marker.coffee b/src/app/display-buffer-marker.coffee index 24c85fcfc..ec0ff3f52 100644 --- a/src/app/display-buffer-marker.coffee +++ b/src/app/display-buffer-marker.coffee @@ -129,6 +129,9 @@ class DisplayBufferMarker isDestroyed: -> @bufferMarker.isDestroyed() + matchesAttributes: (attributes) -> + @bufferMarker.matchesAttributes(attributes) + # Destroys the marker destroy: -> @bufferMarker.destroy() diff --git a/src/app/display-buffer.coffee b/src/app/display-buffer.coffee index 3e0a65c02..a8a40b392 100644 --- a/src/app/display-buffer.coffee +++ b/src/app/display-buffer.coffee @@ -8,15 +8,15 @@ Fold = require 'fold' ScreenLine = require 'screen-line' Token = require 'token' DisplayBufferMarker = require 'display-buffer-marker' +Subscriber = require 'subscriber' module.exports = class DisplayBuffer @idCounter: 1 lineMap: null tokenizedBuffer: null - activeFolds: null - foldsById: null markers: null + foldsByMarkerId: null ### # Internal # @@ -26,14 +26,13 @@ class DisplayBuffer @id = @constructor.idCounter++ @tokenizedBuffer = new TokenizedBuffer(@buffer, options) @softWrapColumn = options.softWrapColumn ? Infinity - @activeFolds = {} - @foldsById = {} @markers = {} + @foldsByMarkerId = {} @buildLineMap() @tokenizedBuffer.on 'grammar-changed', (grammar) => @trigger 'grammar-changed', grammar @tokenizedBuffer.on 'changed', @handleTokenizedBufferChange @buffer.on 'markers-updated', @handleMarkersUpdated - @buffer.on 'marker-created', (marker) => @trigger 'marker-created', @getMarker(marker.id) + @buffer.on 'marker-created', @handleMarkerCreated buildLineMap: -> @lineMap = new LineMap @@ -104,43 +103,17 @@ class DisplayBuffer # # Returns the new {Fold}. createFold: (startRow, endRow) -> - return fold if fold = @foldFor(startRow, endRow, 0, refreshMarkers: true) - fold = new Fold(this, startRow, endRow) - @registerFold(fold) - unless @isFoldContainedByActiveFold(fold) - @updateScreenLines(startRow, endRow, 0, refreshMarkers: true) - fold - - # Public: Given a {Fold}, determines if it is contained within another fold. - # - # fold - The {Fold} to check - # - # Returns the contaiing {Fold} (if it exists), `null` otherwise. - isFoldContainedByActiveFold: (fold) -> - for row, folds of @activeFolds - for otherFold in folds - return otherFold if fold != otherFold and fold.isContainedByFold(otherFold) - - # Public: Given a starting and ending row, tries to find an existing fold. - # - # startRow - A {Number} representing a fold's starting row - # endRow - A {Number} representing a fold's ending row - # - # Returns a {Fold} (if it exists). - foldFor: (startRow, endRow) -> - _.find @activeFolds[startRow] ? [], (fold) -> - fold.startRow == startRow and fold.endRow == endRow + foldMarker = + @findMarker({class: 'fold', startRow, endRow}) ? + @markBufferRange([[startRow, 0], [endRow, Infinity]], class: 'fold') + @foldForMarker(foldMarker) # Public: Removes any folds found that contain the given buffer row. # # bufferRow - The buffer row {Number} to check against destroyFoldsContainingBufferRow: (bufferRow) -> - for row, folds of @activeFolds - for fold in new Array(folds...) - fold.destroy() if fold.getBufferRange().containsRow(bufferRow) - - foldsStartingAtBufferRow: (bufferRow) -> - new Array((@activeFolds[bufferRow] ? [])...) + for marker in @findMarkers(class: 'fold', containsBufferRow: bufferRow) + marker.destroy() # Public: Given a buffer row, this returns the largest fold that starts there. # @@ -151,8 +124,8 @@ class DisplayBuffer # # Returns a {Fold}. largestFoldStartingAtBufferRow: (bufferRow) -> - return unless folds = @activeFolds[bufferRow] - (folds.sort (a, b) -> b.endRow - a.endRow)[0] + if marker = @findMarker(class: 'fold', startBufferRow: bufferRow) + @foldForMarker(marker) # Public: Given a screen row, this returns the largest fold that starts there. # @@ -177,9 +150,13 @@ class DisplayBuffer largestFold = null for currentBufferRow in [bufferRow..0] if fold = @largestFoldStartingAtBufferRow(currentBufferRow) - largestFold = fold if fold.endRow >= bufferRow + largestFold = fold if fold.getEndRow() >= bufferRow largestFold + largestFoldStartingAtBufferRange: (bufferRange) -> + if marker = @findMarker(class: 'fold', containingBufferRange: bufferRange) + @foldForMarker(marker) + # Public: Given a buffer range, this converts it into a screen range. # # bufferRange - A {Range} consisting of buffer positions @@ -324,30 +301,8 @@ class DisplayBuffer # Internal # ### - registerFold: (fold) -> - @activeFolds[fold.startRow] ?= [] - @activeFolds[fold.startRow].push(fold) - @foldsById[fold.id] = fold - - unregisterFold: (bufferRow, fold) -> - folds = @activeFolds[bufferRow] - _.remove(folds, fold) - delete @foldsById[fold.id] - delete @activeFolds[bufferRow] if folds.length == 0 - - destroyFold: (fold) -> - @unregisterFold(fold.startRow, fold) - unless @isFoldContainedByActiveFold(fold) - @updateScreenLines(fold.startRow, fold.endRow, 0, refreshMarkers: true) - - handleBufferChange: (e) -> - allFolds = [] # Folds can modify @activeFolds, so first make sure we have a stable array of folds - allFolds.push(folds...) for row, folds of @activeFolds - fold.handleBufferChange(e) for fold in allFolds - handleTokenizedBufferChange: (tokenizedBufferChange) => {start, end, delta, bufferChange} = tokenizedBufferChange - @handleBufferChange(bufferChange) if bufferChange @updateScreenLines(start, end, delta, delayChangeEvent: bufferChange?) updateScreenLines: (startBufferRow, endBufferRow, bufferDelta, options={}) -> @@ -371,11 +326,6 @@ class DisplayBuffer else @triggerChanged(changeEvent, options.refreshMarkers) - handleMarkersUpdated: => - event = @pendingChangeEvent - @pendingChangeEvent = null - @triggerChanged(event, false) - buildLineForBufferRow: (bufferRow) -> @buildLinesForBufferRows(bufferRow, bufferRow) @@ -394,7 +344,7 @@ class DisplayBuffer screenLine.fold = fold screenLine.bufferRows = fold.getBufferRowCount() lineFragments.push(screenLine) - currentBufferRow = fold.endRow + 1 + currentBufferRow = fold.getEndRow() + 1 continue startBufferColumn ?= 0 @@ -412,6 +362,22 @@ class DisplayBuffer lineFragments + handleMarkersUpdated: => + event = @pendingChangeEvent + @pendingChangeEvent = null + @triggerChanged(event, false) + + handleMarkerCreated: (marker) => + marker = @getMarker(marker.id) + new Fold(this, marker) if marker.matchesAttributes(class: 'fold') + @trigger 'marker-created', marker + + buildFoldForMarker: (marker) -> + + + foldForMarker: (marker) -> + @foldsByMarkerId[marker.id] + ### # Public # ### @@ -540,15 +506,18 @@ class DisplayBuffer # # Returns an {Array} of {DisplayBufferMarker}s findMarkers: (attributes) -> - { startBufferRow, endBufferRow } = attributes + { startBufferRow, endBufferRow, containsBufferRange, containsBufferRow } = attributes attributes.startRow = startBufferRow if startBufferRow? attributes.endRow = endBufferRow if endBufferRow? - attributes = _.omit(attributes, ['startBufferRow', 'endBufferRow']) + attributes.containsRange = containsBufferRange if containsBufferRange? + attributes.containsRow = containsBufferRow if containsBufferRow? + attributes = _.omit(attributes, ['startBufferRow', 'endBufferRow', 'containsBufferRange', 'containsBufferRow']) @buffer.findMarkers(attributes).map ({id}) => @getMarker(id) ### # Internal # ### + pauseMarkerObservers: -> marker.pauseEvents() for marker in @getMarkers() @@ -573,3 +542,4 @@ class DisplayBuffer lines.join('\n') _.extend DisplayBuffer.prototype, EventEmitter +_.extend DisplayBuffer.prototype, Subscriber diff --git a/src/app/fold.coffee b/src/app/fold.coffee index e53824924..d37d1811f 100644 --- a/src/app/fold.coffee +++ b/src/app/fold.coffee @@ -1,86 +1,54 @@ Range = require 'range' Point = require 'point' -# Public: Represents a fold that's hiding text from the screen. +# Public: Represents a fold that's hiding text from the screen. # # Folds are the primary reason that screen ranges and buffer ranges vary. Their # creation is managed by the {DisplayBuffer}. module.exports = class Fold - @idCounter: 1 - displayBuffer: null - startRow: null - endRow: null + marker: null - ### - # Internal # - ### + constructor: (@displayBuffer, @marker) -> + @displayBuffer.foldsByMarkerId[@marker.id] = this + @updateDisplayBuffer() + @marker.on 'changed', (e) => + oldRange = new Range(e.oldHeadBufferPosition, e.oldTailBufferPosition) + newRange = new Range(e.newHeadBufferPosition, e.newTailBufferPosition) + @updateDisplayBuffer() unless newRange.isEqual(oldRange) + @marker.on 'destroyed', => @destroyed() - constructor: (@displayBuffer, @startRow, @endRow) -> - @id = @constructor.idCounter++ + updateDisplayBuffer: -> + unless @isInsideLargerFold() + @displayBuffer.updateScreenLines(@getStartRow(), @getEndRow(), 0, updateMarkers: true) + + isInsideLargerFold: -> + @displayBuffer.findMarker(class: 'fold', containsBufferRange: @getBufferRange())? destroy: -> - @displayBuffer.destroyFold(this) + @marker.destroy() + + getBufferRange: -> + @marker.getBufferRange() + + getStartRow: -> + @getBufferRange().start.row + + getEndRow: -> + @getBufferRange().end.row inspect: -> - "Fold(#{@startRow}, #{@endRow})" + "Fold(#{@getStartRow()}, #{@getEndRow()})" - # Public: Retrieves the buffer row range that a fold occupies. - # - # includeNewline - A {Boolean} which, if `true`, includes the trailing newline - # - # Returns a {Range}. - getBufferRange: ({includeNewline}={}) -> - if includeNewline - end = [@endRow + 1, 0] - else - end = [@endRow, Infinity] - - new Range([@startRow, 0], end) - - # Public: Retrieves the number of buffer rows a fold occupies. + # Retrieves the number of buffer rows spanned by the fold. # # Returns a {Number}. getBufferRowCount: -> - @endRow - @startRow + 1 + @getEndRow() - @getStartRow() + 1 - handleBufferChange: (event) -> - oldStartRow = @startRow + ## Internal ## - if @isContainedByRange(event.oldRange) - @displayBuffer.unregisterFold(@startRow, this) - return - - @startRow += @getRowDelta(event, @startRow) - @endRow += @getRowDelta(event, @endRow) - - if @startRow != oldStartRow - @displayBuffer.unregisterFold(oldStartRow, this) - @displayBuffer.registerFold(this) - - # Public: Identifies if a {Range} occurs within a fold. - # - # range - A {Range} to check - # - # Returns a {Boolean}. - isContainedByRange: (range) -> - range.start.row <= @startRow and @endRow <= range.end.row - - # Public: Identifies if a fold is nested within a fold. - # - # fold - A {Fold} to check - # - # Returns a {Boolean}. - isContainedByFold: (fold) -> - @isContainedByRange(fold.getBufferRange()) - - getRowDelta: (event, row) -> - { newRange, oldRange } = event - - if oldRange.end.row <= row - newRange.end.row - oldRange.end.row - else if newRange.end.row < row - newRange.end.row - row - else - 0 + destroyed: -> + delete @displayBuffer.foldsByMarkerId[@marker.id] + @updateDisplayBuffer()