Merge pull request #97 from github/batch-screen-updates

Update the screen once per keystroke, and make typing faster
This commit is contained in:
Nathan Sobo
2012-11-14 20:03:02 -08:00
20 changed files with 406 additions and 337 deletions

View File

@@ -143,12 +143,6 @@ describe 'Buffer', ->
waitsFor "file to be removed", ->
not bufferToDelete.getPath()
describe ".isModified()", ->
beforeEach ->
buffer.destroy()
waitsFor "file to be removed", ->
not bufferToDelete.getPath()
describe ".isModified()", ->
it "returns true when user changes buffer", ->
expect(buffer.isModified()).toBeFalsy()
@@ -733,3 +727,23 @@ describe 'Buffer', ->
expect(buffer.isEmpty()).toBeFalsy()
buffer.setText('\n')
expect(buffer.isEmpty()).toBeFalsy()
describe "stopped-changing event", ->
it "fires 'stoppedChangingDelay' ms after the last buffer change", ->
delay = buffer.stoppedChangingDelay
stoppedChangingHandler = jasmine.createSpy("stoppedChangingHandler")
buffer.on 'stopped-changing', stoppedChangingHandler
buffer.insert([0, 0], 'a')
expect(stoppedChangingHandler).not.toHaveBeenCalled()
advanceClock(delay / 2)
buffer.insert([0, 0], 'b')
expect(stoppedChangingHandler).not.toHaveBeenCalled()
advanceClock(delay / 2)
expect(stoppedChangingHandler).not.toHaveBeenCalled()
advanceClock(delay / 2)
expect(stoppedChangingHandler).toHaveBeenCalled()

View File

@@ -238,6 +238,7 @@ describe "Editor", ->
editor.setSelectedBufferRange([[40, 0], [43, 1]])
expect(editor.getSelection().getScreenRange()).toEqual [[40, 0], [43, 1]]
previousScrollHeight = editor.verticalScrollbar.prop('scrollHeight')
editor.scrollTop(750)
expect(editor.scrollTop()).toBe 750
@@ -483,6 +484,8 @@ describe "Editor", ->
describe "when the font size changes on the view", ->
it "updates the font sizes of editors and recalculates dimensions critical to cursor positioning", ->
rootView.attachToDom()
rootView.height(200)
rootView.width(200)
rootView.setFontSize(10)
lineHeightBefore = editor.lineHeight
charWidthBefore = editor.charWidth
@@ -656,13 +659,11 @@ describe "Editor", ->
it "places an additional cursor", ->
editor.attachToDom()
setEditorHeightInLines(editor, 5)
editor.renderedLines.trigger mousedownEvent(editor: editor, point: [3, 0])
editor.setCursorBufferPosition([3, 0])
editor.scrollTop(editor.lineHeight * 6)
spyOn(editor, "scrollTo").andCallThrough()
editor.renderedLines.trigger mousedownEvent(editor: editor, point: [6, 0], metaKey: true)
expect(editor.scrollTo.callCount).toBe 1
expect(editor.scrollTop()).toBe editor.lineHeight * (6 - editor.vScrollMargin)
[cursor1, cursor2] = editor.getCursorViews()
expect(cursor1.position()).toEqual(top: 3 * editor.lineHeight, left: 0)
@@ -819,8 +820,8 @@ describe "Editor", ->
expect(region1.position().top).toBeCloseTo(2 * lineHeight)
expect(region1.position().left).toBeCloseTo(7 * charWidth)
expect(region1.height()).toBeCloseTo lineHeight
expect(region1.width()).toBeCloseTo(editor.renderedLines.outerWidth() - region1.position().left)
expect(region1.width()).toBeCloseTo(editor.renderedLines.outerWidth() - region1.position().left)
region2 = selectionView.regions[1]
expect(region2.position().top).toBeCloseTo(3 * lineHeight)
expect(region2.position().left).toBeCloseTo(0)
@@ -837,8 +838,8 @@ describe "Editor", ->
expect(region1.position().top).toBeCloseTo(2 * lineHeight)
expect(region1.position().left).toBeCloseTo(7 * charWidth)
expect(region1.height()).toBeCloseTo lineHeight
expect(region1.width()).toBeCloseTo(editor.renderedLines.outerWidth() - region1.position().left)
expect(region1.width()).toBeCloseTo(editor.renderedLines.outerWidth() - region1.position().left)
region2 = selectionView.regions[1]
expect(region2.position().top).toBeCloseTo(3 * lineHeight)
expect(region2.position().left).toBeCloseTo(0)
@@ -861,7 +862,7 @@ describe "Editor", ->
expect(selectionView.regions.length).toBe 3
expect(selectionView.find('.selection').length).toBe 3
selectionView.updateAppearance()
selectionView.updateDisplay()
expect(selectionView.regions.length).toBe 3
expect(selectionView.find('.selection').length).toBe 3
@@ -906,7 +907,7 @@ describe "Editor", ->
editor.setCursorScreenPosition(row: 2, column: 2)
expect(editor.getCursorView().position()).toEqual(top: 2 * editor.lineHeight, left: 2 * editor.charWidth)
it "removes the idle class while moving, then adds it back when it stops", ->
xit "removes the idle class while moving, then adds it back when it stops", ->
cursorView = editor.getCursorView()
advanceClock(200)
@@ -934,11 +935,11 @@ describe "Editor", ->
editor.setSelectedBufferRange([[0, 0], [3, 0]])
expect(editor.getSelection().isEmpty()).toBeFalsy()
expect(cursorView).not.toBeVisible()
expect(cursorView.css('visibility')).toBe 'hidden'
editor.setCursorBufferPosition([1, 3])
expect(editor.getSelection().isEmpty()).toBeTruthy()
expect(cursorView).toBeVisible()
expect(cursorView.css('visibility')).toBe 'visible'
describe "auto-scrolling", ->
it "only auto-scrolls when the last cursor is moved", ->
@@ -1339,7 +1340,7 @@ describe "Editor", ->
editor.attachToDom(heightInLines: 5)
spyOn(editor, "scrollTo")
describe "when the change the precedes the first rendered row", ->
describe "when the change precedes the first rendered row", ->
it "inserts and removes rendered lines to account for upstream change", ->
editor.scrollToBottom()
expect(editor.renderedLines.find(".line").length).toBe 7
@@ -1347,9 +1348,9 @@ describe "Editor", ->
expect(editor.renderedLines.find(".line:last").text()).toBe buffer.lineForRow(12)
buffer.change([[1,0], [3,0]], "1\n2\n3\n")
expect(editor.renderedLines.find(".line").length).toBe 8
expect(editor.renderedLines.find(".line").length).toBe 7
expect(editor.renderedLines.find(".line:first").text()).toBe buffer.lineForRow(6)
expect(editor.renderedLines.find(".line:last").text()).toBe buffer.lineForRow(13)
expect(editor.renderedLines.find(".line:last").text()).toBe buffer.lineForRow(12)
describe "when the change straddles the first rendered row", ->
it "doesn't render rows that were not previously rendered", ->
@@ -1360,9 +1361,9 @@ describe "Editor", ->
expect(editor.renderedLines.find(".line:last").text()).toBe buffer.lineForRow(12)
buffer.change([[2,0], [7,0]], "2\n3\n4\n5\n6\n7\n8\n9\n")
expect(editor.renderedLines.find(".line").length).toBe 9
expect(editor.renderedLines.find(".line").length).toBe 7
expect(editor.renderedLines.find(".line:first").text()).toBe buffer.lineForRow(6)
expect(editor.renderedLines.find(".line:last").text()).toBe buffer.lineForRow(14)
expect(editor.renderedLines.find(".line:last").text()).toBe buffer.lineForRow(12)
describe "when the change straddles the last rendered row", ->
it "doesn't render rows that were not previously rendered", ->
@@ -1382,7 +1383,7 @@ describe "Editor", ->
maxLineLength = editor.maxScreenLineLength()
setEditorWidthInChars(editor, maxLineLength)
widthBefore = editor.renderedLines.width()
expect(widthBefore).toBe editor.scrollView.width()
expect(widthBefore).toBe editor.scrollView.width() + 20
buffer.change([[12,0], [12,0]], [1..maxLineLength*2].join(''))
expect(editor.renderedLines.width()).toBeGreaterThan widthBefore
@@ -1398,7 +1399,7 @@ describe "Editor", ->
expect(editor.renderedLines.width()).toBeGreaterThan editor.scrollView.width()
widthBefore = editor.renderedLines.width()
buffer.delete([[12, 0], [12, Infinity]])
expect(editor.renderedLines.width()).toBe editor.scrollView.width()
expect(editor.renderedLines.width()).toBe editor.scrollView.width() + 20
describe "when the change the precedes the first rendered row", ->
it "removes rendered lines to account for upstream change", ->
@@ -1497,63 +1498,12 @@ describe "Editor", ->
buffer.insert([0, 0], "")
expect(editor.find('.line:eq(0)').outerHeight()).toBe editor.find('.line:eq(1)').outerHeight()
describe ".spliceLineElements(startRow, rowCount, lineElements)", ->
elements = null
beforeEach ->
editor.attachToDom()
elements = $$ ->
@div "A", class: 'line'
@div "B", class: 'line'
describe "when the start row is 0", ->
describe "when the row count is 0", ->
it "inserts the given elements before the first row", ->
editor.spliceLineElements 0, 0, elements
expect(editor.renderedLines.find('.line:eq(0)').text()).toBe 'A'
expect(editor.renderedLines.find('.line:eq(1)').text()).toBe 'B'
expect(editor.renderedLines.find('.line:eq(2)').text()).toBe 'var quicksort = function () {'
describe "when the row count is > 0", ->
it "replaces the initial rows with the given elements", ->
editor.spliceLineElements 0, 2, elements
expect(editor.renderedLines.find('.line:eq(0)').text()).toBe 'A'
expect(editor.renderedLines.find('.line:eq(1)').text()).toBe 'B'
expect(editor.renderedLines.find('.line:eq(2)').text()).toBe ' if (items.length <= 1) return items;'
describe "when the start row is less than the last row", ->
describe "when the row count is 0", ->
it "inserts the elements at the specified location", ->
editor.spliceLineElements 2, 0, elements
expect(editor.renderedLines.find('.line:eq(2)').text()).toBe 'A'
expect(editor.renderedLines.find('.line:eq(3)').text()).toBe 'B'
expect(editor.renderedLines.find('.line:eq(4)').text()).toBe ' if (items.length <= 1) return items;'
describe "when the row count is > 0", ->
it "replaces the elements at the specified location", ->
editor.spliceLineElements 2, 2, elements
expect(editor.renderedLines.find('.line:eq(2)').text()).toBe 'A'
expect(editor.renderedLines.find('.line:eq(3)').text()).toBe 'B'
expect(editor.renderedLines.find('.line:eq(4)').text()).toBe ' while(items.length > 0) {'
describe "when the start row is the last row", ->
it "appends the elements to the end of the lines", ->
editor.spliceLineElements 13, 0, elements
expect(editor.renderedLines.find('.line:eq(12)').text()).toBe '};'
expect(editor.renderedLines.find('.line:eq(13)').text()).toBe 'A'
expect(editor.renderedLines.find('.line:eq(14)').text()).toBe 'B'
expect(editor.renderedLines.find('.line:eq(15)')).not.toExist()
describe "when editor.setShowInvisibles is called", ->
it "displays spaces as •, tabs as ▸ and newlines as ¬ when true", ->
editor.attachToDom()
editor.setInvisibles(rootView.getInvisibles())
editor.setText " a line with tabs\tand spaces "
expect(editor.showInvisibles).toBeFalsy()
expect(editor.renderedLines.find('.line').text()).toBe " a line with tabs and spaces "
editor.setShowInvisibles(true)
@@ -1844,15 +1794,15 @@ describe "Editor", ->
editor.setCursorScreenPosition([2,0])
expect(editor.lineElementForScreenRow(2)).toMatchSelector('.fold.selected')
expect(editor.find('.cursor').css('display')).toBe 'none'
expect(editor.find('.cursor').css('visibility')).toBe 'hidden'
editor.setCursorScreenPosition([3,0])
expect(editor.find('.cursor').css('display')).toBe 'block'
expect(editor.find('.cursor').css('visibility')).toBe 'visible'
describe "when a selected fold is scrolled into view (and the fold line was not previously rendered)", ->
it "renders the fold's line element with the 'selected' class", ->
setEditorHeightInLines(editor, 5)
editor.renderLines() # re-render lines so certain lines are not rendered
editor.resetDisplay()
editor.createFold(2, 4)
editor.setSelectedBufferRange([[1, 0], [5, 0]], preserveFolds: true)

View File

@@ -15,10 +15,6 @@ describe "StatusBar", ->
statusBar = rootView.find('.status-bar').view()
buffer = editor.getBuffer()
# updating the status bar is asynchronous for performance reasons
# for testing purposes, make it synchronous
spyOn(_, 'delay').andCallFake (fn) -> fn()
afterEach ->
rootView.remove()
@@ -56,6 +52,7 @@ describe "StatusBar", ->
it "enables the buffer modified indicator", ->
expect(statusBar.bufferModified.text()).toBe ''
editor.insertText("\n")
advanceClock(buffer.stoppedChangingDelay)
expect(statusBar.bufferModified.text()).toBe '*'
editor.backspace()
@@ -66,6 +63,7 @@ describe "StatusBar", ->
rootView.open(path)
expect(statusBar.bufferModified.text()).toBe ''
editor.insertText("\n")
advanceClock(buffer.stoppedChangingDelay)
expect(statusBar.bufferModified.text()).toBe '*'
editor.save()
expect(statusBar.bufferModified.text()).toBe ''
@@ -73,15 +71,19 @@ describe "StatusBar", ->
it "disables the buffer modified indicator if the content matches again", ->
expect(statusBar.bufferModified.text()).toBe ''
editor.insertText("\n")
advanceClock(buffer.stoppedChangingDelay)
expect(statusBar.bufferModified.text()).toBe '*'
editor.backspace()
advanceClock(buffer.stoppedChangingDelay)
expect(statusBar.bufferModified.text()).toBe ''
it "disables the buffer modified indicator when the change is undone", ->
expect(statusBar.bufferModified.text()).toBe ''
editor.insertText("\n")
advanceClock(buffer.stoppedChangingDelay)
expect(statusBar.bufferModified.text()).toBe '*'
editor.undo()
advanceClock(buffer.stoppedChangingDelay)
expect(statusBar.bufferModified.text()).toBe ''
describe "when the buffer changes", ->
@@ -89,6 +91,7 @@ describe "StatusBar", ->
expect(statusBar.bufferModified.text()).toBe ''
rootView.open(require.resolve('fixtures/sample.txt'))
editor.insertText("\n")
advanceClock(buffer.stoppedChangingDelay)
expect(statusBar.bufferModified.text()).toBe '*'
it "doesn't update the buffer modified indicator for the old buffer", ->
@@ -96,6 +99,7 @@ describe "StatusBar", ->
expect(statusBar.bufferModified.text()).toBe ''
rootView.open(require.resolve('fixtures/sample.txt'))
oldBuffer.setText("new text")
advanceClock(buffer.stoppedChangingDelay)
expect(statusBar.bufferModified.text()).toBe ''
describe "when the associated editor's cursor position changes", ->

View File

@@ -7,6 +7,7 @@ Project = require 'project'
Directory = require 'directory'
File = require 'file'
RootView = require 'root-view'
Editor = require 'editor'
TextMateBundle = require 'text-mate-bundle'
TextMateTheme = require 'text-mate-theme'
fs = require 'fs'
@@ -22,6 +23,9 @@ beforeEach ->
window.resetTimeouts()
pathsWithSubscriptions = []
# make editor display updates synchronous
spyOn(Editor.prototype, 'requestDisplayUpdate').andCallFake -> @updateDisplay()
afterEach ->
delete window.rootView if window.rootView
$('#jasmine-content').empty()

View File

@@ -41,6 +41,8 @@ class BufferChangeOperation
event = { oldRange, newRange, oldText, newText }
@buffer.trigger 'change', event
@buffer.scheduleStoppedChangingEvent()
anchor.handleBufferChange(event) for anchor in @buffer.getAnchors()
@buffer.trigger 'update-anchors-after-change'
newRange

View File

@@ -13,6 +13,8 @@ Git = require 'git'
module.exports =
class Buffer
@idCounter = 1
stoppedChangingDelay: 300
stoppedChangingTimeout: null
undoManager: null
cachedDiskContents: null
cachedMemoryContents: null
@@ -378,4 +380,11 @@ class Buffer
if @git?.checkoutHead(path)
@trigger 'git-status-change'
scheduleStoppedChangingEvent: ->
clearTimeout(@stoppedChangingTimeout) if @stoppedChangingTimeout
stoppedChangingCallback = =>
@stoppedChangingTimeout = null
@trigger 'stopped-changing'
@stoppedChangingTimeout = setTimeout(stoppedChangingCallback, @stoppedChangingDelay)
_.extend(Buffer.prototype, EventEmitter)

View File

@@ -9,52 +9,79 @@ class CursorView extends View
@content: ->
@pre class: 'cursor idle', => @raw '&nbsp;'
blinkPeriod: 800
editor: null
visible: true
needsUpdate: true
needsAutoscroll: true
needsRemoval: false
shouldPauseBlinking: false
initialize: (@cursor, @editor) ->
@cursor.on 'change-screen-position.cursor-view', (screenPosition, { bufferChange, autoscroll }) =>
@updateAppearance({autoscroll})
@removeIdleClassTemporarily() unless bufferChange
@trigger 'cursor-move', {bufferChange}
@cursor.on 'change-screen-position.cursor-view', (screenPosition, { autoscroll }) =>
@needsUpdate = true
@shouldPauseBlinking = true
@needsAutoscroll = (autoscroll ? true) and @cursor?.isLastCursor()
@editor.requestDisplayUpdate()
@cursor.on 'change-visibility.cursor-view', (visible) => @setVisible(visible)
@cursor.on 'destroy.cursor-view', => @remove()
@cursor.on 'change-visibility.cursor-view', (visible) =>
@needsUpdate = true
@needsAutoscroll = visible and @cursor.isLastCursor()
@editor.requestDisplayUpdate()
afterAttach: (onDom) ->
return unless onDom
@updateAppearance()
@editor.syncCursorAnimations()
@cursor.on 'destroy.cursor-view', =>
@needsRemoval = true
@editor.requestDisplayUpdate()
remove: ->
@editor.removeCursorView(this)
@cursor.off('.cursor-view')
super
updateAppearance: (options={}) ->
autoscroll = options.autoscroll ? true
updateDisplay: ->
screenPosition = @getScreenPosition()
pixelPosition = @getPixelPosition()
@css(pixelPosition)
@autoscroll() if @cursor.isLastCursor() and autoscroll
unless _.isEqual(@lastPixelPosition, pixelPosition)
changedPosition = true
@css(pixelPosition)
@trigger 'cursor-move'
if @shouldPauseBlinking
@resetBlinking()
else if !@startBlinkingTimeout
@startBlinking()
@setVisible(@cursor.isVisible() and not @editor.isFoldedAtScreenRow(screenPosition.row))
getPixelPosition: ->
@editor.pixelPositionForScreenPosition(@getScreenPosition())
autoscroll: ->
pixelPosition =
@editor.scrollTo(@getPixelPosition())
setVisible: (visible) ->
return if visible == @visible
@visible = visible
unless @visible == visible
@visible = visible
if @visible
@css('visibility', '')
else
@css('visibility', 'hidden')
if @visible
@show()
@autoscroll()
else
@hide()
toggleVisible: ->
@setVisible(not @visible) if @cursor.isVisible
stopBlinking: ->
clearInterval(@blinkInterval) if @blinkInterval
@blinkInterval = null
@setVisible(true) if @cursor.isVisible
startBlinking: ->
return if @blinkInterval?
blink = => @toggleVisible()
@blinkInterval = setInterval(blink, @blinkPeriod / 2)
resetBlinking: ->
@stopBlinking()
@startBlinking()
getBufferPosition: ->
@cursor.getBufferPosition()

View File

@@ -207,7 +207,7 @@ class DisplayBuffer
@trigger 'change',
oldRange: oldScreenRange
newRange: newScreenRange
bufferChanged: true
bufferChange: e.bufferChange
lineNumbersChanged: !e.oldRange.coversSameRows(newRange) or !oldScreenRange.coversSameRows(newScreenRange)
buildLineForBufferRow: (bufferRow) ->

View File

@@ -60,9 +60,8 @@ class EditSession
@mergeCursors()
@displayBuffer.on "change.edit-session-#{@id}", (e) =>
@refreshAnchorScreenPositions() unless e.bufferChange
@trigger 'screen-lines-change', e
unless e.bufferChanged
anchor.refreshScreenPosition() for anchor in @getAnchors()
destroy: ->
throw new Error("Edit session already destroyed") if @destroyed
@@ -263,7 +262,7 @@ class EditSession
@setCursorBufferPosition([fold.startRow, 0])
isFoldedAtScreenRow: (screenRow) ->
@lineForScreenRow(screenRow).fold?
@lineForScreenRow(screenRow)?.fold?
largestFoldContainingBufferRow: (bufferRow) ->
@displayBuffer.largestFoldContainingBufferRow(bufferRow)
@@ -332,6 +331,9 @@ class EditSession
removeAnchor: (anchor) ->
_.remove(@anchors, anchor)
refreshAnchorScreenPositions: ->
anchor.refreshScreenPosition() for anchor in @getAnchors()
removeAnchorRange: (anchorRange) ->
_.remove(@anchorRanges, anchorRange)

View File

@@ -20,7 +20,9 @@ class Editor extends View
@subview 'gutter', new Gutter
@input class: 'hidden-input', outlet: 'hiddenInput'
@div class: 'scroll-view', outlet: 'scrollView', =>
@div class: 'lines', outlet: 'renderedLines', =>
@div class: 'overlayer', outlet: 'overlayer'
@div class: 'lines', outlet: 'renderedLines'
@div class: 'underlayer', outlet: 'underlayer'
@div class: 'vertical-scrollbar', outlet: 'verticalScrollbar', =>
@div outlet: 'verticalScrollbarContent'
@@ -42,6 +44,9 @@ class Editor extends View
editSessions: null
attached: false
lineOverdraw: 100
pendingChanges: null
newCursors: null
newSelections: null
@deserialize: (state, rootView) ->
editSessions = state.editSessions.map (state) -> EditSession.deserialize(state, rootView.project)
@@ -60,6 +65,9 @@ class Editor extends View
@cursorViews = []
@selectionViews = []
@editSessions = []
@pendingChanges = []
@newCursors = []
@newSelections = []
if editSession?
@editSessions.push editSession
@@ -269,10 +277,10 @@ class Editor extends View
setShowInvisibles: (showInvisibles) ->
return if showInvisibles == @showInvisibles
@showInvisibles = showInvisibles
@renderLines()
@resetDisplay()
setInvisibles: (@invisibles={}) ->
@renderLines()
@resetDisplay()
checkoutHead: -> @getBuffer().checkoutHead()
setText: (text) -> @getBuffer().setText(text)
@@ -349,8 +357,6 @@ class Editor extends View
else
@gutter.addClass('drop-shadow')
@on 'selection-change', => @highlightCursorLine()
selectOnMousemoveUntilMouseup: ->
moveHandler = (e) => @selectToScreenPosition(@screenPositionFromMouseEvent(e))
@on 'mousemove', moveHandler
@@ -364,17 +370,15 @@ class Editor extends View
afterAttach: (onDom) ->
return if @attached or not onDom
@attached = true
@clearRenderedLines()
@subscribeToFontSize()
@calculateDimensions()
@hiddenInput.width(@charWidth)
@setSoftWrapColumn() if @activeEditSession.getSoftWrap()
@invisibles = @rootView()?.getInvisibles()
$(window).on "resize.editor#{@id}", =>
@updateRenderedLines()
$(window).on "resize.editor#{@id}", => @requestDisplayUpdate()
@focus() if @isFocused
@renderWhenAttached()
@resetDisplay()
@trigger 'editor-open', [this]
@@ -425,11 +429,8 @@ class Editor extends View
@activeEditSession.on "buffer-path-change", =>
@trigger 'editor-path-change'
@activeEditSession.getSelection().on 'change-screen-range', =>
@trigger 'selection-change'
@trigger 'editor-path-change'
@renderWhenAttached()
@resetDisplay()
if @attached and @activeEditSession.buffer.isInConflict()
@showBufferConflictAlert(@activeEditSession)
@@ -452,17 +453,18 @@ class Editor extends View
getOpenBufferPaths: ->
editSession.buffer.getPath() for editSession in @editSessions when editSession.buffer.getPath()?
scrollTop: (scrollTop, options) ->
scrollTop: (scrollTop, options={}) ->
return @cachedScrollTop or 0 unless scrollTop?
maxScrollTop = @verticalScrollbar.prop('scrollHeight') - @verticalScrollbar.height()
scrollTop = Math.floor(Math.max(0, Math.min(maxScrollTop, scrollTop)))
return if scrollTop == @cachedScrollTop
@cachedScrollTop = scrollTop
@updateRenderedLines() if @attached
@updateDisplay() if @attached
@renderedLines.css('top', -scrollTop)
@underlayer.css('top', -scrollTop)
@overlayer.css('top', -scrollTop)
@gutter.lineNumbers.css('top', -scrollTop)
if options?.adjustVerticalScrollbar ? true
@verticalScrollbar.scrollTop(scrollTop)
@@ -573,10 +575,8 @@ class Editor extends View
@css('font-size', fontSize + 'px')
@calculateDimensions()
@updatePaddingOfRenderedLines()
@handleScrollHeightChange()
@updateCursorViews()
@updateSelectionViews()
@updateRenderedLines()
@updateLayerDimensions()
@requestDisplayUpdate()
newSplitEditor: ->
new Editor { editSession: @activeEditSession.copy(), @showInvisibles }
@@ -633,22 +633,6 @@ class Editor extends View
for session in @getEditSessions()
session.destroy()
renderWhenAttached: ->
return unless @attached
@removeAllCursorAndSelectionViews()
@addCursorView(cursor) for cursor in @activeEditSession.getCursors()
@addSelectionView(selection) for selection in @activeEditSession.getSelections()
@activeEditSession.on 'add-cursor', (cursor) => @addCursorView(cursor)
@activeEditSession.on 'add-selection', (selection) => @addSelectionView(selection)
@prepareForScrolling()
@setScrollPositionFromActiveEditSession()
@renderLines()
@highlightCursorLine()
@activeEditSession.on 'screen-lines-change', (e) => @handleDisplayBufferChange(e)
getCursorView: (index) ->
index ?= @cursorViews.length - 1
@cursorViews[index]
@@ -656,27 +640,15 @@ class Editor extends View
getCursorViews: ->
new Array(@cursorViews...)
addCursorView: (cursor) ->
cursorView = new CursorView(cursor, this)
addCursorView: (cursor, options) ->
cursorView = new CursorView(cursor, this, options)
@cursorViews.push(cursorView)
@appendToLinesView(cursorView)
@overlayer.append(cursorView)
cursorView
removeCursorView: (cursorView) ->
_.remove(@cursorViews, cursorView)
updateCursorViews: ->
for cursorView in @getCursorViews()
cursorView.updateAppearance()
updateSelectionViews: ->
for selectionView in @getSelectionViews()
selectionView.updateAppearance()
syncCursorAnimations: ->
for cursorView in @getCursorViews()
do (cursorView) -> cursorView.resetCursorAnimation()
getSelectionView: (index) ->
index ?= @selectionViews.length - 1
@selectionViews[index]
@@ -687,7 +659,7 @@ class Editor extends View
addSelectionView: (selection) ->
selectionView = new SelectionView({editor: this, selection})
@selectionViews.push(selectionView)
@appendToLinesView(selectionView)
@underlayer.append(selectionView)
selectionView
removeSelectionView: (selectionView) ->
@@ -698,11 +670,11 @@ class Editor extends View
selectionView.remove() for selectionView in @getSelectionViews()
appendToLinesView: (view) ->
@renderedLines.append(view)
@overlayer.append(view)
calculateDimensions: ->
fragment = $('<pre class="line" style="position: absolute; visibility: hidden;"><span>x</span></div>')
@appendToLinesView(fragment)
@renderedLines.append(fragment)
lineRect = fragment[0].getBoundingClientRect()
charRect = fragment.find('span')[0].getBoundingClientRect()
@@ -712,66 +684,203 @@ class Editor extends View
@height(@lineHeight) if @mini
fragment.remove()
updateLayerDimensions: ->
@gutter.calculateWidth()
prepareForScrolling: ->
@adjustHeightOfRenderedLines()
@adjustMinWidthOfRenderedLines()
height = @lineHeight * @screenLineCount()
unless @layerHeight == height
@renderedLines.height(height)
@underlayer.height(height)
@overlayer.height(height)
@layerHeight = height
adjustHeightOfRenderedLines: ->
heightOfRenderedLines = @lineHeight * @screenLineCount()
@verticalScrollbarContent.height(heightOfRenderedLines)
@renderedLines.css('padding-bottom', heightOfRenderedLines)
@verticalScrollbarContent.height(height)
@scrollBottom(height) if @scrollBottom() > height
adjustMinWidthOfRenderedLines: ->
minWidth = @charWidth * @maxScreenLineLength()
unless @renderedLines.cachedMinWidth == minWidth
minWidth = @charWidth * @maxScreenLineLength() + 20
unless @layerMinWidth == minWidth
@renderedLines.css('min-width', minWidth)
@renderedLines.cachedMinWidth = minWidth
handleScrollHeightChange: ->
scrollHeight = @lineHeight * @screenLineCount()
@verticalScrollbarContent.height(scrollHeight)
@scrollBottom(scrollHeight) if @scrollBottom() > scrollHeight
renderLines: ->
@clearRenderedLines()
@updateRenderedLines()
@underlayer.css('min-width', minWidth)
@overlayer.css('min-width', minWidth)
@layerMinWidth = minWidth
clearRenderedLines: ->
@lineCache = []
@renderedLines.find('.line').remove()
@renderedLines.empty()
@firstRenderedScreenRow = null
@lastRenderedScreenRow = null
@firstRenderedScreenRow = -1
@lastRenderedScreenRow = -1
resetDisplay: ->
return unless @attached
@clearRenderedLines()
@removeAllCursorAndSelectionViews()
@updateLayerDimensions()
@setScrollPositionFromActiveEditSession()
@activeEditSession.on 'add-selection', (selection) =>
@newCursors.push(selection.cursor)
@newSelections.push(selection)
@requestDisplayUpdate()
@activeEditSession.on 'screen-lines-change', (e) => @handleDisplayBufferChange(e)
@newCursors = @activeEditSession.getCursors()
@newSelections = @activeEditSession.getSelections()
@updateDisplay(suppressAutoScroll: true)
requestDisplayUpdate: ()->
return if @pendingDisplayUpdate
@pendingDisplayUpdate = true
_.nextTick =>
@updateDisplay()
@pendingDisplayUpdate = false
updateDisplay: (options={}) ->
return unless @attached
@updateRenderedLines()
@highlightCursorLine()
@updateCursorViews()
@updateSelectionViews()
@autoscroll(options)
updateCursorViews: ->
if @newCursors.length > 0
@addCursorView(cursor) for cursor in @newCursors
@syncCursorAnimations()
@newCursors = []
for cursorView in @getCursorViews()
if cursorView.needsRemoval
cursorView.remove()
else if cursorView.needsUpdate
cursorView.updateDisplay()
updateSelectionViews: ->
if @newSelections.length > 0
@addSelectionView(selection) for selection in @newSelections
@newSelections = []
for selectionView in @getSelectionViews()
if selectionView.destroyed
selectionView.remove()
else
selectionView.updateDisplay()
syncCursorAnimations: ->
for cursorView in @getCursorViews()
do (cursorView) -> cursorView.resetBlinking()
autoscroll: (options={}) ->
for cursorView in @getCursorViews() when cursorView.needsAutoscroll
@scrollTo(cursorView.getPixelPosition()) unless options.suppressAutoScroll
cursorView.needsAutoscroll = false
updateRenderedLines: ->
firstVisibleScreenRow = @getFirstVisibleScreenRow()
lastVisibleScreenRow = @getLastVisibleScreenRow()
renderFrom = Math.max(0, firstVisibleScreenRow - @lineOverdraw)
renderTo = Math.min(@getLastScreenRow(), lastVisibleScreenRow + @lineOverdraw)
if firstVisibleScreenRow < @firstRenderedScreenRow
@removeLineElements(Math.max(@firstRenderedScreenRow, renderTo + 1), @lastRenderedScreenRow)
@lastRenderedScreenRow = renderTo
newLines = @buildLineElements(renderFrom, Math.min(@firstRenderedScreenRow - 1, renderTo))
@insertLineElements(renderFrom, newLines)
@firstRenderedScreenRow = renderFrom
renderedLines = true
if @firstRenderedScreenRow? and firstVisibleScreenRow >= @firstRenderedScreenRow and lastVisibleScreenRow <= @lastRenderedScreenRow
renderFrom = @firstRenderedScreenRow
renderTo = Math.min(@getLastScreenRow(), @lastRenderedScreenRow)
else
renderFrom = Math.max(0, firstVisibleScreenRow - @lineOverdraw)
renderTo = Math.min(@getLastScreenRow(), lastVisibleScreenRow + @lineOverdraw)
if lastVisibleScreenRow > @lastRenderedScreenRow
if 0 <= @firstRenderedScreenRow < renderFrom
@removeLineElements(@firstRenderedScreenRow, Math.min(@lastRenderedScreenRow, renderFrom - 1))
@firstRenderedScreenRow = renderFrom
startRowOfNewLines = Math.max(@lastRenderedScreenRow + 1, renderFrom)
newLines = @buildLineElements(startRowOfNewLines, renderTo)
@insertLineElements(startRowOfNewLines, newLines)
@lastRenderedScreenRow = renderTo
renderedLines = true
if @pendingChanges.length == 0 and @firstRenderedScreenRow and @firstRenderedScreenRow <= renderFrom and renderTo <= @lastRenderedScreenRow
return
if renderedLines
@gutter.renderLineNumbers(renderFrom, renderTo)
@updatePaddingOfRenderedLines()
@gutter.updateLineNumbers(@pendingChanges, renderFrom, renderTo)
intactRanges = @computeIntactRanges()
@pendingChanges = []
@truncateIntactRanges(intactRanges, renderFrom, renderTo)
@clearDirtyRanges(intactRanges)
@fillDirtyRanges(intactRanges, renderFrom, renderTo)
@firstRenderedScreenRow = renderFrom
@lastRenderedScreenRow = renderTo
@updateLayerDimensions()
@updatePaddingOfRenderedLines()
computeIntactRanges: ->
return [] if !@firstRenderedScreenRow? and !@lastRenderedScreenRow?
intactRanges = [{from: @firstRenderedScreenRow, to: @lastRenderedScreenRow, domStart: 0}]
for change in @pendingChanges
newIntactRanges = []
delta = change.delta
for range in intactRanges
if change.to < range.from and change.delta != 0
newIntactRanges.push(
from: range.from + delta
to: range.to + delta
domStart: range.domStart
)
else if change.to < range.from or change.from > range.to
newIntactRanges.push(range)
else
if change.from > range.from
newIntactRanges.push(
from: range.from
to: change.from - 1
domStart: range.domStart)
if change.to < range.to
newIntactRanges.push(
from: change.to + delta + 1
to: range.to + delta
domStart: range.domStart + change.to + 1 - range.from
)
intactRanges = newIntactRanges
@pendingChanges = []
intactRanges
truncateIntactRanges: (intactRanges, renderFrom, renderTo) ->
i = 0
while i < intactRanges.length
range = intactRanges[i]
if range.from < renderFrom
range.domStart += renderFrom - range.from
range.from = renderFrom
if range.to > renderTo
range.to = renderTo
if range.from >= range.to
intactRanges.splice(i--, 1)
i++
intactRanges.sort (a, b) -> a.domStart - b.domStart
clearDirtyRanges: (intactRanges) ->
renderedLines = @renderedLines[0]
killLine = (line) ->
next = line.nextSibling
renderedLines.removeChild(line)
next
if intactRanges.length == 0
@renderedLines.empty()
else
domPosition = 0
currentLine = renderedLines.firstChild
for intactRange in intactRanges
while intactRange.domStart > domPosition
currentLine = killLine(currentLine)
domPosition++
for i in [intactRange.from..intactRange.to]
currentLine = currentLine.nextSibling
domPosition++
while currentLine
currentLine = killLine(currentLine)
fillDirtyRanges: (intactRanges, renderFrom, renderTo) ->
renderedLines = @renderedLines[0]
nextIntact = intactRanges.shift()
currentLine = renderedLines.firstChild
screenRow = renderFrom
for row in [renderFrom..renderTo]
if row == nextIntact?.to + 1
nextIntact = intactRanges.shift()
if !nextIntact or row < nextIntact.from
lineElement = @buildLineElementForScreenRow(row)
renderedLines.insertBefore(lineElement, currentLine)
else
currentLine = currentLine.nextSibling
updatePaddingOfRenderedLines: ->
paddingTop = @firstRenderedScreenRow * @lineHeight
@@ -786,66 +895,24 @@ class Editor extends View
Math.floor(@scrollTop() / @lineHeight)
getLastVisibleScreenRow: ->
Math.ceil((@scrollTop() + @scrollView.height()) / @lineHeight) - 1
Math.max(0, Math.ceil((@scrollTop() + @scrollView.height()) / @lineHeight) - 1)
handleDisplayBufferChange: (e) ->
oldScreenRange = e.oldRange
newScreenRange = e.newRange
{ oldRange, newRange } = e
from = oldRange.start.row
to = oldRange.end.row
delta = newRange.end.row - oldRange.end.row
if @attached
@handleScrollHeightChange() unless newScreenRange.coversSameRows(oldScreenRange)
@adjustMinWidthOfRenderedLines()
if bufferChange = e.bufferChange
bufferDelta = bufferChange.newRange.end.row - bufferChange.oldRange.end.row
return if oldScreenRange.start.row > @lastRenderedScreenRow
@pendingChanges.push({from, to, delta, bufferDelta})
@requestDisplayUpdate()
maxEndRow = Math.max(@getLastVisibleScreenRow() + @lineOverdraw, @lastRenderedScreenRow)
@gutter.renderLineNumbers(@firstRenderedScreenRow, maxEndRow) if e.lineNumbersChanged
newScreenRange = newScreenRange.copy()
oldScreenRange = oldScreenRange.copy()
endOfShortestRange = Math.min(oldScreenRange.end.row, newScreenRange.end.row)
delta = @firstRenderedScreenRow - endOfShortestRange
if delta > 0
newScreenRange.start.row += delta
newScreenRange.end.row += delta
oldScreenRange.start.row += delta
oldScreenRange.end.row += delta
oldScreenRange.start.row = Math.max(oldScreenRange.start.row, @firstRenderedScreenRow)
oldScreenRange.end.row = Math.min(oldScreenRange.end.row, @lastRenderedScreenRow)
newScreenRange.start.row = Math.max(newScreenRange.start.row, @firstRenderedScreenRow)
newScreenRange.end.row = Math.min(newScreenRange.end.row, maxEndRow)
lineElements = @buildLineElements(newScreenRange.start.row, newScreenRange.end.row)
@replaceLineElements(oldScreenRange.start.row, oldScreenRange.end.row, lineElements)
rowDelta = newScreenRange.end.row - oldScreenRange.end.row
@lastRenderedScreenRow += rowDelta
@updateRenderedLines() if rowDelta < 0
if @lastRenderedScreenRow > maxEndRow
@removeLineElements(maxEndRow + 1, @lastRenderedScreenRow)
@lastRenderedScreenRow = maxEndRow
@updatePaddingOfRenderedLines()
@highlightCursorLine()
buildLineElements: (startRow, endRow) ->
charWidth = @charWidth
charHeight = @charHeight
lines = @activeEditSession.linesForScreenRows(startRow, endRow)
activeEditSession = @activeEditSession
cursorScreenRow = @getCursorScreenPosition().row
mini = @mini
buildLineHtml = (line) => @buildLineHtml(line)
$$ ->
row = startRow
for line in lines
@raw(buildLineHtml(line))
row++
buildLineElementForScreenRow: (screenRow) ->
div = document.createElement('div')
div.innerHTML = @buildLineHtml(@activeEditSession.lineForScreenRow(screenRow))
div.firstChild
buildLineHtml: (screenLine) ->
scopeStack = []
@@ -908,44 +975,8 @@ class Editor extends View
line.push('</pre>')
line.join('')
insertLineElements: (row, lineElements) ->
@spliceLineElements(row, 0, lineElements)
replaceLineElements: (startRow, endRow, lineElements) ->
@spliceLineElements(startRow, endRow - startRow + 1, lineElements)
removeLineElements: (startRow, endRow) ->
@spliceLineElements(startRow, endRow - startRow + 1)
spliceLineElements: (startScreenRow, rowCount, lineElements) ->
throw new Error("Splicing at a negative start row: #{startScreenRow}") if startScreenRow < 0
if startScreenRow < @firstRenderedScreenRow
startRow = 0
else
startRow = startScreenRow - @firstRenderedScreenRow
endRow = startRow + rowCount
elementToInsertBefore = @lineCache[startRow]
elementsToReplace = @lineCache[startRow...endRow]
@lineCache[startRow...endRow] = lineElements?.toArray() or []
lines = @renderedLines[0]
if lineElements
fragment = document.createDocumentFragment()
lineElements.each -> fragment.appendChild(this)
if elementToInsertBefore
lines.insertBefore(fragment, elementToInsertBefore)
else
lines.appendChild(fragment)
elementsToReplace.forEach (element) =>
lines.removeChild(element)
lineElementForScreenRow: (screenRow) ->
element = @lineCache[screenRow - @firstRenderedScreenRow]
$(element)
@renderedLines.children(":eq(#{screenRow - @firstRenderedScreenRow})")
logScreenLines: (start, end) ->
@activeEditSession.logScreenLines(start, end)

View File

@@ -9,7 +9,8 @@ class Gutter extends View
@div class: 'gutter', =>
@div outlet: 'lineNumbers', class: 'line-numbers'
firstScreenRow: -1
firstScreenRow: Infinity
lastScreenRow: -1
highestNumberWidth: null
afterAttach: (onDom) ->
@@ -33,9 +34,18 @@ class Gutter extends View
widthTesterElement.remove()
lineNumberPadding
updateLineNumbers: (changes, renderFrom, renderTo) ->
if renderFrom < @firstScreenRow or renderTo > @lastScreenRow
performUpdate = true
else
for change in changes
if change.delta != 0 or (change.bufferDelta? and change.bufferDelta != 0)
performUpdate = true
break
@renderLineNumbers(renderFrom, renderTo) if performUpdate
renderLineNumbers: (startScreenRow, endScreenRow) ->
@firstScreenRow = startScreenRow
lastScreenRow = -1
rows = @editor().bufferRowsForScreenRows(startScreenRow, endScreenRow)
cursorScreenRow = @editor().getCursorScreenPosition().row
@@ -49,6 +59,8 @@ class Gutter extends View
lastScreenRow = row
@calculateWidth()
@firstScreenRow = startScreenRow
@lastScreenRow = endScreenRow
@highlightedRow = null
@highlightCursorLine()

View File

@@ -27,7 +27,7 @@ class LineMap
@maxScreenLineLength = Math.max(@maxScreenLineLength, screenLine.text.length)
lineForScreenRow: (row) ->
@linesForScreenRows(row, row)[0]
@screenLines[row]
linesForScreenRows: (startRow, endRow) ->
@screenLines[startRow..endRow]

View File

@@ -9,17 +9,20 @@ class SelectionView extends View
@div()
regions: null
destroyed: false
initialize: ({@editor, @selection} = {}) ->
@regions = []
@selection.on 'change-screen-range', => @updateAppearance()
@selection.on 'destroy', => @remove()
@updateAppearance()
@selection.on 'change-screen-range', => @editor.requestDisplayUpdate()
@selection.on 'destroy', =>
@destroyed = true
@editor.requestDisplayUpdate()
updateAppearance: ->
updateDisplay: ->
@clearRegions()
range = @getScreenRange()
@trigger 'selection-change'
@editor.highlightFoldsContainingBufferRange(@getBufferRange())
return if range.isEmpty()

View File

@@ -13,7 +13,7 @@ class Selection
@cursor.selection = this
@cursor.on 'change-screen-position.selection', (e) =>
@screenRangeChanged() unless e.bufferChanged
@screenRangeChanged() unless e.bufferChange
@cursor.on 'destroy.selection', =>
@cursor = null

View File

@@ -42,9 +42,9 @@ class StatusBar extends View
subscribeToBuffer: ->
@buffer?.off '.status-bar'
@buffer = @editor.getBuffer()
@buffer.on 'change.status-bar', => _.delay (=> @updateBufferModifiedText()), 50
@buffer.on 'after-save.status-bar', => _.delay (=> @updateStatusBar()), 50
@buffer.on 'git-status-change.status-bar', => _.delay (=> @updateStatusBar()), 50
@buffer.on 'stopped-changing.status-bar', => @updateBufferModifiedText()
@buffer.on 'after-save.status-bar', => @updateStatusBar()
@buffer.on 'git-status-change.status-bar', => @updateStatusBar()
@updateStatusBar()
updateStatusBar: ->

View File

@@ -360,7 +360,7 @@ describe "Autocomplete", ->
beforeEach ->
editor.attachToDom()
setEditorHeightInLines(editor, 13)
editor.renderLines() # Ensures the editor only has 13 lines visible
editor.resetDisplay() # Ensures the editor only has 13 lines visible
editor.setCursorBufferPosition [1, 1]

View File

@@ -17,10 +17,10 @@ describe "WrapGuide", ->
describe "@initialize", ->
it "appends a wrap guide to all existing and new editors", ->
expect(rootView.panes.find('.pane').length).toBe 1
expect(rootView.panes.find('.lines > .wrap-guide').length).toBe 1
expect(rootView.panes.find('.underlayer > .wrap-guide').length).toBe 1
editor.splitRight()
expect(rootView.find('.pane').length).toBe 2
expect(rootView.panes.find('.lines > .wrap-guide').length).toBe 2
expect(rootView.panes.find('.underlayer > .wrap-guide').length).toBe 2
describe "@updateGuide", ->
it "positions the guide at the configured column", ->

View File

@@ -13,8 +13,8 @@ class WrapGuide extends View
@appendToEditorPane(rootView, editor, config)
@appendToEditorPane: (rootView, editor, config) ->
if lines = editor.pane()?.find('.lines')
lines.append(new WrapGuide(rootView, editor, config))
if underlayer = editor.pane()?.find('.underlayer')
underlayer.append(new WrapGuide(rootView, editor, config))
@content: ->
@div class: 'wrap-guide'

View File

@@ -71,4 +71,14 @@ _.mixin
inverted
multiplyString: (string, n) ->
new Array(1 + n).join(string)
new Array(1 + n).join(string)
nextTick: (fn) ->
unless @messageChannel
@pendingNextTickFns = []
@messageChannel = new MessageChannel
@messageChannel.port1.onmessage = =>
fn() while fn = @pendingNextTickFns.shift()
@pendingNextTickFns.push(fn)
@messageChannel.port2.postMessage(0)

View File

@@ -73,36 +73,37 @@
overflow-x: hidden;
}
.editor .underlayer, .editor .lines, .editor .overlayer {
width: 100%;
height: 100%;
}
.editor .underlayer {
z-index: 0;
position: absolute;
}
.editor .lines {
position: relative;
display: table;
height: 100%;
width: 100%;
/*overflow: hidden; i'm worried this is causing rendering problems */
padding-right: 2em;
z-index: 1;
}
.editor .overlayer {
z-index: 2;
pointer-events: none;
position: absolute;
}
.editor .line span {
vertical-align: top;
}
@-webkit-keyframes blink {
0% { opacity: 1; }
60% { opacity: 1; }
61% { opacity: 0; }
100% { opacity: 0; }
}
.editor .cursor {
position: absolute;
border-left: 2px solid;
}
.editor.focused .cursor.idle {
-webkit-animation: blink 0.8s;
-webkit-animation-iteration-count: infinite;
}
.editor .hidden-input {
position: absolute;
z-index: -1;