From 1e2d333a15dcb4cf855e4eedc92895f80ed9dbae Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 10 May 2012 09:27:04 -0600 Subject: [PATCH] WIP: working on only rendering on-screen lines. still half-baked. --- benchmark/benchmark-suite.coffee | 2 +- spec/app/editor-spec.coffee | 246 ++++++++++++++++++------------- src/app/editor.coffee | 34 ++++- 3 files changed, 177 insertions(+), 105 deletions(-) diff --git a/benchmark/benchmark-suite.coffee b/benchmark/benchmark-suite.coffee index deabe62ae..53af263e9 100644 --- a/benchmark/benchmark-suite.coffee +++ b/benchmark/benchmark-suite.coffee @@ -42,7 +42,7 @@ describe "editor.", -> editor.backspace() describe "9000-line-file.", -> - benchmark "opening", 1, -> + fbenchmark "opening", 1, -> editor.setBuffer new Buffer(require.resolve('fixtures/huge.js')) describe "at-end.", -> diff --git a/spec/app/editor-spec.coffee b/spec/app/editor-spec.coffee index 3e98726ba..5055025d9 100644 --- a/spec/app/editor-spec.coffee +++ b/spec/app/editor-spec.coffee @@ -9,7 +9,15 @@ _ = require 'underscore' fs = require 'fs' describe "Editor", -> - [rootView, buffer, editor] = [] + [rootView, buffer, editor, cachedLineHeight] = [] + + getLineHeight = -> + return cachedLineHeight if cachedLineHeight + editorForMeasurement = new Editor() + editorForMeasurement.attachToDom() + cachedLineHeight = editorForMeasurement.lineHeight + editorForMeasurement.remove() + cachedLineHeight beforeEach -> rootView = new RootView(pathToOpen: require.resolve('fixtures/sample.js')) @@ -17,6 +25,11 @@ describe "Editor", -> editor = rootView.activeEditor() buffer = editor.buffer + editor.attachToDom = (options={}) -> + heightInLines = options.heightInLines ? this.buffer.numLines() + this.height(getLineHeight() * heightInLines) + $('#jasmine-content').append(this) + editor.autoIndent = false editor.enableKeymap() editor.isFocused = true @@ -62,125 +75,152 @@ describe "Editor", -> expect(openHandler).not.toHaveBeenCalled() describe "text rendering", -> - beforeEach -> - editor.attachToDom() - - it "creates a line element for each line in the buffer with the html-escaped text of the line", -> - expect(editor.lines.find('.line').length).toEqual(buffer.numLines()) - expect(buffer.lineForRow(2)).toContain('<') - expect(editor.lines.find('.line:eq(2)').html()).toContain '<' - - # renders empty lines with a non breaking space - expect(buffer.lineForRow(10)).toBe '' - expect(editor.lines.find('.line:eq(10)').html()).toBe ' ' - - it "syntax highlights code based on the file type", -> - line1 = editor.lines.find('.line:first') - expect(line1.find('span:eq(0)')).toMatchSelector '.keyword.definition' - expect(line1.find('span:eq(0)').text()).toBe 'var' - expect(line1.find('span:eq(1)')).toMatchSelector '.text' - expect(line1.find('span:eq(1)').text()).toBe ' ' - expect(line1.find('span:eq(2)')).toMatchSelector '.identifier' - expect(line1.find('span:eq(2)').text()).toBe 'quicksort' - expect(line1.find('span:eq(4)')).toMatchSelector '.operator' - expect(line1.find('span:eq(4)').text()).toBe '=' - - line12 = editor.lines.find('.line:eq(11)') - expect(line12.find('span:eq(1)')).toMatchSelector '.keyword' - - describe "when lines are updated in the buffer", -> - it "syntax highlights the updated lines", -> - expect(editor.lines.find('.line:eq(0) span:eq(0)')).toMatchSelector '.keyword.definition' - buffer.insert([0, 4], "g") - expect(editor.lines.find('.line:eq(0) span:eq(0)')).toMatchSelector '.keyword.definition' - - # verify that re-highlighting can occur below the changed line - buffer.insert([5,0], "/* */") - buffer.insert([1,0], "/*") - expect(editor.lines.find('.line:eq(2) span:eq(0)')).toMatchSelector '.comment' - - describe "when soft-wrap is enabled", -> + describe "when all lines in the buffer are visible on screen", -> beforeEach -> - setEditorWidthInChars(editor, 50) - editor.setSoftWrap(true) - expect(editor.renderer.maxLineLength).toBe 50 + editor.attachToDom() + expect(editor.height()).toBe buffer.numLines() * editor.lineHeight - it "wraps lines that are too long to fit within the editor's width, adjusting cursor positioning accordingly", -> - expect(editor.lines.find('.line').length).toBe 16 - expect(editor.lines.find('.line:eq(3)').text()).toBe " var pivot = items.shift(), current, left = [], " - expect(editor.lines.find('.line:eq(4)').text()).toBe "right = [];" + it "creates a line element for each line in the buffer with the html-escaped text of the line", -> + expect(editor.lines.find('.line').length).toEqual(buffer.numLines()) + expect(buffer.lineForRow(2)).toContain('<') + expect(editor.lines.find('.line:eq(2)').html()).toContain '<' - editor.setCursorBufferPosition([3, 51]) - expect(editor.find('.cursor').offset()).toEqual(editor.lines.find('.line:eq(4)').offset()) + # renders empty lines with a non breaking space + expect(buffer.lineForRow(10)).toBe '' + expect(editor.lines.find('.line:eq(10)').html()).toBe ' ' - editor.setCursorBufferPosition([4, 0]) - expect(editor.find('.cursor').offset()).toEqual(editor.lines.find('.line:eq(5)').offset()) + it "syntax highlights code based on the file type", -> + line1 = editor.lines.find('.line:first') + expect(line1.find('span:eq(0)')).toMatchSelector '.keyword.definition' + expect(line1.find('span:eq(0)').text()).toBe 'var' + expect(line1.find('span:eq(1)')).toMatchSelector '.text' + expect(line1.find('span:eq(1)').text()).toBe ' ' + expect(line1.find('span:eq(2)')).toMatchSelector '.identifier' + expect(line1.find('span:eq(2)').text()).toBe 'quicksort' + expect(line1.find('span:eq(4)')).toMatchSelector '.operator' + expect(line1.find('span:eq(4)').text()).toBe '=' - editor.getSelection().setBufferRange(new Range([6, 30], [6, 55])) - [region1, region2] = editor.getSelection().regions - expect(region1.offset().top).toBe(editor.lines.find('.line:eq(7)').offset().top) - expect(region2.offset().top).toBe(editor.lines.find('.line:eq(8)').offset().top) + line12 = editor.lines.find('.line:eq(11)') + expect(line12.find('span:eq(1)')).toMatchSelector '.keyword' - it "handles changes to wrapped lines correctly", -> - buffer.insert([6, 28], '1234567') - expect(editor.lines.find('.line:eq(7)').text()).toBe ' current < pivot ? left1234567.push(current) ' - expect(editor.lines.find('.line:eq(8)').text()).toBe ': right.push(current);' - expect(editor.lines.find('.line:eq(9)').text()).toBe ' }' + describe "when lines are updated in the buffer", -> + it "syntax highlights the updated lines", -> + expect(editor.lines.find('.line:eq(0) span:eq(0)')).toMatchSelector '.keyword.definition' + buffer.insert([0, 4], "g") + expect(editor.lines.find('.line:eq(0) span:eq(0)')).toMatchSelector '.keyword.definition' - it "changes the max line length and repositions the cursor when the window size changes", -> - editor.setCursorBufferPosition([3, 60]) - setEditorWidthInChars(editor, 40) - $(window).trigger 'resize' - expect(editor.lines.find('.line').length).toBe 19 - expect(editor.lines.find('.line:eq(4)').text()).toBe "left = [], right = [];" - expect(editor.lines.find('.line:eq(5)').text()).toBe " while(items.length > 0) {" - expect(editor.bufferPositionForScreenPosition(editor.getCursorScreenPosition())).toEqual [3, 60] + # verify that re-highlighting can occur below the changed line + buffer.insert([5,0], "/* */") + buffer.insert([1,0], "/*") + expect(editor.lines.find('.line:eq(2) span:eq(0)')).toMatchSelector '.comment' - it "wraps the lines of any newly assigned buffers", -> - otherBuffer = new Buffer - otherBuffer.setText([1..100].join('')) - editor.setBuffer(otherBuffer) - expect(editor.lines.find('.line').length).toBeGreaterThan(1) + describe "when soft-wrap is enabled", -> + beforeEach -> + setEditorWidthInChars(editor, 50) + editor.setSoftWrap(true) + expect(editor.renderer.maxLineLength).toBe 50 - it "unwraps lines and cancels window resize listener when softwrap is disabled", -> - editor.toggleSoftWrap() - expect(editor.lines.find('.line:eq(3)').text()).toBe ' var pivot = items.shift(), current, left = [], right = [];' + it "wraps lines that are too long to fit within the editor's width, adjusting cursor positioning accordingly", -> + expect(editor.lines.find('.line').length).toBe 16 + expect(editor.lines.find('.line:eq(3)').text()).toBe " var pivot = items.shift(), current, left = [], " + expect(editor.lines.find('.line:eq(4)').text()).toBe "right = [];" - spyOn(editor, 'setMaxLineLength') - $(window).trigger 'resize' - expect(editor.setMaxLineLength).not.toHaveBeenCalled() + editor.setCursorBufferPosition([3, 51]) + expect(editor.find('.cursor').offset()).toEqual(editor.lines.find('.line:eq(4)').offset()) - it "allows the cursor to move down to the last line", -> - _.times editor.getLastScreenRow(), -> editor.moveCursorDown() - expect(editor.getCursorScreenPosition()).toEqual [editor.getLastScreenRow(), 0] - editor.moveCursorDown() - expect(editor.getCursorScreenPosition()).toEqual [editor.getLastScreenRow(), 2] + editor.setCursorBufferPosition([4, 0]) + expect(editor.find('.cursor').offset()).toEqual(editor.lines.find('.line:eq(5)').offset()) - it "allows the cursor to move up to a shorter soft wrapped line", -> - editor.setCursorScreenPosition([11, 15]) - editor.moveCursorUp() - expect(editor.getCursorScreenPosition()).toEqual [10, 10] - editor.moveCursorUp() - editor.moveCursorUp() - expect(editor.getCursorScreenPosition()).toEqual [8, 15] + editor.getSelection().setBufferRange(new Range([6, 30], [6, 55])) + [region1, region2] = editor.getSelection().regions + expect(region1.offset().top).toBe(editor.lines.find('.line:eq(7)').offset().top) + expect(region2.offset().top).toBe(editor.lines.find('.line:eq(8)').offset().top) - it "it allows the cursor to wrap when moving horizontally past the beginning / end of a wrapped line", -> - editor.setCursorScreenPosition([11, 0]) - editor.moveCursorLeft() - expect(editor.getCursorScreenPosition()).toEqual [10, 10] + it "handles changes to wrapped lines correctly", -> + buffer.insert([6, 28], '1234567') + expect(editor.lines.find('.line:eq(7)').text()).toBe ' current < pivot ? left1234567.push(current) ' + expect(editor.lines.find('.line:eq(8)').text()).toBe ': right.push(current);' + expect(editor.lines.find('.line:eq(9)').text()).toBe ' }' - editor.moveCursorRight() - expect(editor.getCursorScreenPosition()).toEqual [11, 0] + it "changes the max line length and repositions the cursor when the window size changes", -> + editor.setCursorBufferPosition([3, 60]) + setEditorWidthInChars(editor, 40) + $(window).trigger 'resize' + expect(editor.lines.find('.line').length).toBe 19 + expect(editor.lines.find('.line:eq(4)').text()).toBe "left = [], right = [];" + expect(editor.lines.find('.line:eq(5)').text()).toBe " while(items.length > 0) {" + expect(editor.bufferPositionForScreenPosition(editor.getCursorScreenPosition())).toEqual [3, 60] - it "calls .setMaxLineLength() when the editor is attached because now its dimensions are available to calculate it", -> - otherEditor = new Editor() - spyOn(otherEditor, 'setMaxLineLength') + it "wraps the lines of any newly assigned buffers", -> + otherBuffer = new Buffer + otherBuffer.setText([1..100].join('')) + editor.setBuffer(otherBuffer) + expect(editor.lines.find('.line').length).toBeGreaterThan(1) - otherEditor.setSoftWrap(true) - expect(otherEditor.setMaxLineLength).not.toHaveBeenCalled() + it "unwraps lines and cancels window resize listener when softwrap is disabled", -> + editor.toggleSoftWrap() + expect(editor.lines.find('.line:eq(3)').text()).toBe ' var pivot = items.shift(), current, left = [], right = [];' - otherEditor.simulateDomAttachment() - expect(otherEditor.setMaxLineLength).toHaveBeenCalled() + spyOn(editor, 'setMaxLineLength') + $(window).trigger 'resize' + expect(editor.setMaxLineLength).not.toHaveBeenCalled() + + it "allows the cursor to move down to the last line", -> + _.times editor.getLastScreenRow(), -> editor.moveCursorDown() + expect(editor.getCursorScreenPosition()).toEqual [editor.getLastScreenRow(), 0] + editor.moveCursorDown() + expect(editor.getCursorScreenPosition()).toEqual [editor.getLastScreenRow(), 2] + + it "allows the cursor to move up to a shorter soft wrapped line", -> + editor.setCursorScreenPosition([11, 15]) + editor.moveCursorUp() + expect(editor.getCursorScreenPosition()).toEqual [10, 10] + editor.moveCursorUp() + editor.moveCursorUp() + expect(editor.getCursorScreenPosition()).toEqual [8, 15] + + it "it allows the cursor to wrap when moving horizontally past the beginning / end of a wrapped line", -> + editor.setCursorScreenPosition([11, 0]) + editor.moveCursorLeft() + expect(editor.getCursorScreenPosition()).toEqual [10, 10] + + editor.moveCursorRight() + expect(editor.getCursorScreenPosition()).toEqual [11, 0] + + it "calls .setMaxLineLength() when the editor is attached because now its dimensions are available to calculate it", -> + otherEditor = new Editor() + spyOn(otherEditor, 'setMaxLineLength') + + otherEditor.setSoftWrap(true) + expect(otherEditor.setMaxLineLength).not.toHaveBeenCalled() + + otherEditor.simulateDomAttachment() + expect(otherEditor.setMaxLineLength).toHaveBeenCalled() + + fdescribe "when some lines at the end of the buffer are not visible on screen", -> + beforeEach -> + editor.attachToDom(heightInLines: 5.5) + + it "only renders the visible lines, giving the final line a margin-bottom to account for the missing lines", -> + expect(editor.lines.find('.line').length).toBe 6 + expectedMarginBottom = (buffer.numLines() - 6) * editor.lineHeight + expect(editor.lines.find('.line:last').css('margin-bottom')).toBe "#{expectedMarginBottom}px" + + it "renders additional lines when the editor is scrolled down, adjusting margins appropriately", -> + editor.scroller.scrollTop(editor.lineHeight * 2.5) + editor.scroller.trigger 'scroll' + + expect(editor.lines.find('.line').length).toBe 6 + expect(editor.lines.find('.line:first')).toHaveText buffer.lineForRow(2) + expect(editor.lines.find('.line:last')).toHaveText buffer.lineForRow(7) + + for line, index in editor.lines.find('.line') + marginBottom = $(line).css('margin-bottom') + if index == 5 + expectedMarginBottom = (buffer.numLines() - 8) * editor.lineHeight + expect(marginBottom).toBe "#{expectedMarginBottom}px" + else + expect(marginBottom).toBe '0px' describe "gutter rendering", -> it "creates a line number element for each line in the buffer", -> diff --git a/src/app/editor.coffee b/src/app/editor.coffee index aeb7f3ce8..a01ec0942 100644 --- a/src/app/editor.coffee +++ b/src/app/editor.coffee @@ -203,6 +203,7 @@ class Editor extends View false @scroller.on 'scroll', => + @updateLines() @gutter.scrollTop(@scroller.scrollTop()) if @scroller.scrollLeft() == 0 @gutter.removeClass('drop-shadow') @@ -235,7 +236,36 @@ class Editor extends View renderLines: -> @lineCache = [] @lines.find('.line').remove() - @insertLineElements(0, @buildLineElements(0, @getLastScreenRow())) + + lastVisibleRow = @getLastVisibleRow() + @firstRenderedScreenRow = 0 + @lastRenderedScreenRow = lastVisibleRow + + lineElements = @buildLineElements(0, lastVisibleRow) + lineElements.last().css('margin-bottom', (@getLastScreenRow() - lastVisibleRow) * @lineHeight) + @insertLineElements(0, lineElements) + + updateLines: -> + firstVisibleRow = @getFirstVisibleRow() + lastVisibleRow = @getLastVisibleRow() + + # if @firstRenderedScreenRow < firstVisibleRow + # @lines.find('.line:first').remove() for row in [@firstRenderedScreenRow...firstVisibleRow] + # @firstRenderedScreenRow = firstVisibleRow + + if @lastRenderedScreenRow < lastVisibleRow + @lines.find('.line:last').css('margin-bottom', 'inherit') + console.log "building line elements for", @lastRenderedScreenRow + 1, lastVisibleRow + lineElements = @buildLineElements(@lastRenderedScreenRow + 1, lastVisibleRow) + lineElements.last().css('margin-bottom', (@getLastScreenRow() - lastVisibleRow) * @lineHeight) + @insertLineElements(@lastRenderedScreenRow + 1, lineElements) + @lastRenderedScreenRow = lastVisibleRow + + getFirstVisibleRow: -> + Math.floor(@scroller.scrollTop() / @lineHeight) + + getLastVisibleRow: -> + Math.ceil((@scroller.scrollTop() + @scroller.height()) / @lineHeight) - 1 getScreenLines: -> @renderer.getLines() @@ -345,6 +375,7 @@ class Editor extends View @spliceLineElements(startRow, endRow - startRow + 1, lineElements) spliceLineElements: (startRow, rowCount, lineElements) -> + console.log "splice", startRow, rowCount, lineElements.length endRow = startRow + rowCount elementToInsertBefore = @lineCache[startRow] elementsToReplace = @lineCache[startRow...endRow] @@ -357,6 +388,7 @@ class Editor extends View if elementToInsertBefore lines.insertBefore(fragment, elementToInsertBefore) else + console.log "appending child at start row", startRow, lineElements.text() lines.appendChild(fragment) elementsToReplace.forEach (element) =>