From 80a9cebbeffd922b5539db86cf375c09d9c5d80d Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Fri, 4 Oct 2013 13:00:02 -0700 Subject: [PATCH 01/87] Don't parse empty strings as JSON windowState can be an empty string which should not be attempted to be parsed. --- src/atom.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/atom.coffee b/src/atom.coffee index 8b1ae12a5..07569171e 100644 --- a/src/atom.coffee +++ b/src/atom.coffee @@ -244,7 +244,7 @@ class Atom documentStateJson = @getLoadSettings().windowState try - documentState = JSON.parse(documentStateJson) if documentStateJson? + documentState = JSON.parse(documentStateJson) if documentStateJson catch error console.warn "Error parsing window state: #{windowStatePath}", error.stack, error From 668bb9ffc314cbf17ef1faa8878d6772905875bf Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Fri, 4 Oct 2013 13:04:56 -0700 Subject: [PATCH 02/87] Upgrade to collaboration@0.20.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 144bd0651..54549211e 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "autoflow": "0.3.0", "bookmarks": "0.4.0", "bracket-matcher": "0.5.0", - "collaboration": "0.19.0", + "collaboration": "0.20.0", "command-logger": "0.4.0", "command-palette": "0.4.0", "editor-stats": "0.3.0", From dcba2bcc643e887e85315737f4ff38e56d6c1700 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Fri, 4 Oct 2013 13:08:27 -0700 Subject: [PATCH 03/87] Upgrade to to-the-hubs@0.5.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 54549211e..045a10311 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ "tabs": "0.5.0", "terminal": "0.10.0", "timecop": "0.5.0", - "to-the-hubs": "0.4.0", + "to-the-hubs": "0.5.0", "toml": "0.3.0", "tree-view": "0.8.0", "ui-demo": "0.8.0", From 1930ad500308ae21323f1a64ce7fb0d87d5f3ece Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Fri, 4 Oct 2013 13:14:54 -0700 Subject: [PATCH 04/87] Prepare 30.0.0 release --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 045a10311..5634fb432 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "atom", - "version": "29.0.0", + "version": "30.0.0", "main": "./src/main.js", "repository": { "type": "git", From 501b6e08902297e97c651afd88153fda1d0f44be Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Fri, 4 Oct 2013 14:08:37 -0700 Subject: [PATCH 05/87] Upgrade to to-the-hubs@0.6.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5634fb432..8a77591af 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ "tabs": "0.5.0", "terminal": "0.10.0", "timecop": "0.5.0", - "to-the-hubs": "0.5.0", + "to-the-hubs": "0.6.0", "toml": "0.3.0", "tree-view": "0.8.0", "ui-demo": "0.8.0", From 2f46db29b52a530ff69ecbb514899cec8fecff51 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Fri, 4 Oct 2013 14:10:47 -0700 Subject: [PATCH 06/87] Prepare 31.0.0 release --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8a77591af..71c7d3f3d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "atom", - "version": "30.0.0", + "version": "31.0.0", "main": "./src/main.js", "repository": { "type": "git", From 8f68d6242039539f93dfe040169d4c672863169a Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Fri, 27 Sep 2013 17:06:11 -0700 Subject: [PATCH 07/87] Only re render the cursors/selections when in rendered range --- src/editor.coffee | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/editor.coffee b/src/editor.coffee index ee105b165..a41030f3b 100644 --- a/src/editor.coffee +++ b/src/editor.coffee @@ -1184,9 +1184,16 @@ class Editor extends View for cursorView in @getCursorViews() if cursorView.needsRemoval cursorView.remove() - else if cursorView.needsUpdate + else if @shouldUpdateCursor(cursorView) cursorView.updateDisplay() + shouldUpdateCursor: (cursorView) -> + return false unless cursorView.needsUpdate + + pos = cursorView.getScreenPosition() + should = pos.row >= @firstRenderedScreenRow and pos.row <= @lastRenderedScreenRow + should + updateSelectionViews: -> if @newSelections.length > 0 @addSelectionView(selection) for selection in @newSelections when not selection.destroyed @@ -1195,9 +1202,17 @@ class Editor extends View for selectionView in @getSelectionViews() if selectionView.needsRemoval selectionView.remove() - else + else if @shouldUpdateSelection(selectionView) selectionView.updateDisplay() + shouldUpdateSelection: (selectionView) -> + screenRange = selectionView.getScreenRange() + startRow = screenRange.start.row + endRow = screenRange.end.row + should = (startRow >= @firstRenderedScreenRow and startRow <= @lastRenderedScreenRow) or (endRow >= @firstRenderedScreenRow and endRow <= @lastRenderedScreenRow) + should + + syncCursorAnimations: -> for cursorView in @getCursorViews() do (cursorView) -> cursorView.resetBlinking() From 5c46bff4c16c5d97a350306b996dd861a0cbce9e Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Fri, 27 Sep 2013 17:16:38 -0700 Subject: [PATCH 08/87] Use isVisible() --- src/editor.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/editor.coffee b/src/editor.coffee index a41030f3b..23955355b 100644 --- a/src/editor.coffee +++ b/src/editor.coffee @@ -703,7 +703,7 @@ class Editor extends View @on 'cursor:moved', => return unless @isFocused cursorView = @getCursorView() - @hiddenInput.offset(cursorView.offset()) if cursorView.is(':visible') + @hiddenInput.offset(cursorView.offset()) if cursorView.isVisible() selectedText = null @hiddenInput.on 'compositionstart', => From aba5eb5b41d207e566354f35b27681a3443dda6a Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Fri, 27 Sep 2013 17:17:41 -0700 Subject: [PATCH 09/87] Add a cache to positionLeftForLineAndColumn() This is the slowest function of all. --- src/editor.coffee | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/editor.coffee b/src/editor.coffee index 23955355b..d3aa96537 100644 --- a/src/editor.coffee +++ b/src/editor.coffee @@ -94,6 +94,8 @@ class Editor extends View @pendingChanges = [] @newCursors = [] @newSelections = [] + @lineId = 0 + @pixelLeftCache = {} if editSession? @edit(editSession) @@ -1425,9 +1427,9 @@ class Editor extends View htmlForScreenLine: (screenLine, screenRow) -> { tokens, text, lineEnding, fold, isSoftWrapped } = screenLine if fold - attributes = { class: 'fold line', 'fold-id': fold.id } + attributes = { class: 'fold line', 'fold-id': fold.id, 'line-id': @lineId++ } else - attributes = { class: 'line' } + attributes = { class: 'line', 'line-id': @lineId++ } invisibles = @invisibles if @showInvisibles eolInvisibles = @getEndOfLineInvisibles(screenLine) @@ -1518,6 +1520,14 @@ class Editor extends View positionLeftForLineAndColumn: (lineElement, column) -> return 0 if column is 0 + + @pixelLeftCache ?= {} + cacheKey = lineElement.getAttribute('line-id')+':'+column + + if @pixelLeftCache[cacheKey]? + #console.log 'hit', cacheKey + return @pixelLeftCache[cacheKey] + delta = 0 iterator = document.createNodeIterator(lineElement, NodeFilter.SHOW_TEXT, acceptNode: -> NodeFilter.FILTER_ACCEPT) while textNode = iterator.nextNode() @@ -1532,6 +1542,9 @@ class Editor extends View range.collapse() leftPixels = range.getClientRects()[0].left - Math.floor(@scrollView.offset().left) + Math.floor(@scrollLeft()) range.detach() + + @pixelLeftCache[cacheKey] = leftPixels + leftPixels pixelOffsetForScreenPosition: (position) -> From 914288ab861f5c86694cdc425d8a71570158460e Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Mon, 30 Sep 2013 12:17:32 -0700 Subject: [PATCH 10/87] Wrap each char in a span; compute left position by measuring each span. This is is faster and simpler than the old method. Running each function 20,000 times yielded these results: Old: 3750ms This method: 1523ms By looking up each char's width in a dict: 29ms --- spec/editor-spec.coffee | 85 ++++++++++++++++++++++++++++++----------- src/editor.coffee | 37 ++++-------------- src/token.coffee | 56 +++++++++++++++++++-------- 3 files changed, 111 insertions(+), 67 deletions(-) diff --git a/spec/editor-spec.coffee b/spec/editor-spec.coffee index 820d56654..1ad1c527c 100644 --- a/spec/editor-spec.coffee +++ b/spec/editor-spec.coffee @@ -1090,25 +1090,39 @@ describe "Editor", -> expect(span0.children('span:eq(0)')).toMatchSelector '.storage.modifier.js' expect(span0.children('span:eq(0)').text()).toBe 'var' - span0_1 = span0.children('span:eq(1)') - expect(span0_1).toMatchSelector '.meta.function.js' - expect(span0_1.text()).toBe 'quicksort = function ()' - expect(span0_1.children('span:eq(0)')).toMatchSelector '.entity.name.function.js' - expect(span0_1.children('span:eq(0)').text()).toBe "quicksort" - expect(span0_1.children('span:eq(1)')).toMatchSelector '.keyword.operator.js' - expect(span0_1.children('span:eq(1)').text()).toBe "=" - expect(span0_1.children('span:eq(2)')).toMatchSelector '.storage.type.function.js' - expect(span0_1.children('span:eq(2)').text()).toBe "function" - expect(span0_1.children('span:eq(3)')).toMatchSelector '.punctuation.definition.parameters.begin.js' - expect(span0_1.children('span:eq(3)').text()).toBe "(" - expect(span0_1.children('span:eq(4)')).toMatchSelector '.punctuation.definition.parameters.end.js' - expect(span0_1.children('span:eq(4)').text()).toBe ")" + expect(span0.children('span:eq(1)')).toMatchSelector '.character' + expect(span0.children('span:eq(1)').text()).toBe " " - expect(span0.children('span:eq(2)')).toMatchSelector '.meta.brace.curly.js' - expect(span0.children('span:eq(2)').text()).toBe "{" + span0_2 = span0.children('span:eq(2)') + console.log span0 + console.log span0_2[0] + expect(span0_2).toMatchSelector '.meta.function.js' + expect(span0_2.text()).toBe 'quicksort = function ()' + expect(span0_2.children('span:eq(0)')).toMatchSelector '.entity.name.function.js' + expect(span0_2.children('span:eq(0)').text()).toBe "quicksort" + expect(span0_2.children('span:eq(1)')).toMatchSelector '.character' + expect(span0_2.children('span:eq(1)').text()).toBe " " + expect(span0_2.children('span:eq(2)')).toMatchSelector '.keyword.operator.js' + expect(span0_2.children('span:eq(2)').text()).toBe "=" + expect(span0_2.children('span:eq(3)')).toMatchSelector '.character' + expect(span0_2.children('span:eq(3)').text()).toBe " " + expect(span0_2.children('span:eq(4)')).toMatchSelector '.storage.type.function.js' + expect(span0_2.children('span:eq(4)').text()).toBe "function" + expect(span0_2.children('span:eq(5)')).toMatchSelector '.character' + expect(span0_2.children('span:eq(5)').text()).toBe " " + expect(span0_2.children('span:eq(6)')).toMatchSelector '.punctuation.definition.parameters.begin.js' + expect(span0_2.children('span:eq(6)').text()).toBe "(" + expect(span0_2.children('span:eq(7)')).toMatchSelector '.punctuation.definition.parameters.end.js' + expect(span0_2.children('span:eq(7)').text()).toBe ")" - line12 = editor.renderedLines.find('.line:eq(11)') - expect(line12.find('span:eq(2)')).toMatchSelector '.keyword' + expect(span0.children('span:eq(3)')).toMatchSelector '.character' + expect(span0.children('span:eq(3)').text()).toBe " " + + expect(span0.children('span:eq(4)')).toMatchSelector '.meta.brace.curly.js' + expect(span0.children('span:eq(4)').text()).toBe "{" + + line12 = editor.renderedLines.find('.line:eq(11)').children('span:eq(0)') + expect(line12.children('span:eq(1)')).toMatchSelector '.keyword' it "wraps hard tabs in a span", -> editor.setText('\t<- hard tab') @@ -1123,12 +1137,37 @@ describe "Editor", -> expect(span0_0).toMatchSelector '.leading-whitespace' expect(span0_0.text()).toBe ' ' - it "wraps trailing whitespace in a span", -> - editor.setText('trailing whitespace -> ') + it "wraps every character in a span", -> + text = ' leading and no trailing whitespace' + editor.setText(text) line0 = editor.renderedLines.find('.line:first') - span0_last = line0.children('span:eq(0)').children('span:last') - expect(span0_last).toMatchSelector '.trailing-whitespace' - expect(span0_last.text()).toBe ' ' + characters = line0.find('.character') + + renderedText = '' + renderedText += $(ch).text() for ch in characters + + expect(characters).toHaveLength text.length + expect(renderedText).toEqual text + + describe "when the line has trailing whitespace", -> + it "wraps trailing whitespace in a span", -> + editor.setText('trailing whitespace -> ') + line0 = editor.renderedLines.find('.line:first') + span0_last = line0.children('span:eq(0)').children('span:last') + expect(span0_last).toMatchSelector '.trailing-whitespace' + expect(span0_last.text()).toBe ' ' + + it "wraps every character in a span", -> + text = ' leading and trailing whitespace ' + editor.setText(text) + line0 = editor.renderedLines.find('.line:first') + characters = line0.find('.character') + + renderedText = '' + renderedText += $(ch).text() for ch in characters + + expect(characters).toHaveLength text.length + expect(renderedText).toEqual text describe "when lines are updated in the buffer", -> it "syntax highlights the updated lines", -> @@ -1476,7 +1515,7 @@ describe "Editor", -> editor.setShowInvisibles(true) editor.attachToDom() editor.setText "var" - expect(editor.find('.line').html()).toBe 'var¬' + expect(editor.find('.line').html()).toBe 'var¬' it "allows invisible glyphs to be customized via config.editor.invisibles", -> editor.setText(" \t ") diff --git a/src/editor.coffee b/src/editor.coffee index d3aa96537..a1791d1a6 100644 --- a/src/editor.coffee +++ b/src/editor.coffee @@ -1509,43 +1509,22 @@ class Editor extends View {row, column} = Point.fromObject(position) actualRow = Math.floor(row) - lineElement = existingLineElement = @lineElementForScreenRow(actualRow)[0] + line = @lineElementForScreenRow(actualRow) + lineElement = existingLineElement = line[0] unless existingLineElement lineElement = @buildLineElementForScreenRow(actualRow) @renderedLines.append(lineElement) - left = @positionLeftForLineAndColumn(lineElement, column) + left = @positionLeftForLineAndColumn(line, column) unless existingLineElement @renderedLines[0].removeChild(lineElement) { top: row * @lineHeight, left } positionLeftForLineAndColumn: (lineElement, column) -> - return 0 if column is 0 - - @pixelLeftCache ?= {} - cacheKey = lineElement.getAttribute('line-id')+':'+column - - if @pixelLeftCache[cacheKey]? - #console.log 'hit', cacheKey - return @pixelLeftCache[cacheKey] - - delta = 0 - iterator = document.createNodeIterator(lineElement, NodeFilter.SHOW_TEXT, acceptNode: -> NodeFilter.FILTER_ACCEPT) - while textNode = iterator.nextNode() - nextDelta = delta + textNode.textContent.length - if nextDelta >= column - offset = column - delta - break - delta = nextDelta - - range = document.createRange() - range.setEnd(textNode, offset) - range.collapse() - leftPixels = range.getClientRects()[0].left - Math.floor(@scrollView.offset().left) + Math.floor(@scrollLeft()) - range.detach() - - @pixelLeftCache[cacheKey] = leftPixels - - leftPixels + chars = lineElement.find('.character') + left = 0 + for i in [0...column] + left += chars[i].offsetWidth if chars[i] + left pixelOffsetForScreenPosition: (position) -> {top, left} = @pixelPositionForScreenPosition(position) diff --git a/src/token.coffee b/src/token.coffee index 141e6a756..17910d71b 100644 --- a/src/token.coffee +++ b/src/token.coffee @@ -123,11 +123,6 @@ class Token getValueAsHtml: ({invisibles, hasLeadingWhitespace, hasTrailingWhitespace, hasIndentGuide})-> invisibles ?= {} html = @value - .replace(/&/g, '&') - .replace(/"/g, '"') - .replace(/'/g, ''') - .replace(//g, '>') if @isHardTab classes = [] @@ -135,27 +130,58 @@ class Token classes.push('invisible-character') if invisibles.tab classes.push('hard-tab') classes = classes.join(' ') - html = html.replace /^./, (match) -> + html = html.replace /^./, (match) => match = invisibles.tab ? match - "#{match}" + "#{@wrapCharacters(match)}" else - if hasLeadingWhitespace + startIndex = 0 + endIndex = html.length + + leadingHtml = '' + trailingHtml = '' + + if hasLeadingWhitespace and match = /^[ ]+/.exec(html) classes = [] classes.push('indent-guide') if hasIndentGuide classes.push('invisible-character') if invisibles.space classes.push('leading-whitespace') classes = classes.join(' ') - html = html.replace /^[ ]+/, (match) -> - match = match.replace(/./g, invisibles.space) if invisibles.space - "#{match}" - if hasTrailingWhitespace + + match[0] = match[0].replace(/./g, invisibles.space) if invisibles.space + leadingHtml = "#{@wrapCharacters(match[0])}" + + startIndex = match[0].length + + if hasTrailingWhitespace and match = /[ ]+$/.exec(html) classes = [] classes.push('indent-guide') if hasIndentGuide and not hasLeadingWhitespace classes.push('invisible-character') if invisibles.space classes.push('trailing-whitespace') classes = classes.join(' ') - html = html.replace /[ ]+$/, (match) -> - match = match.replace(/./g, invisibles.space) if invisibles.space - "#{match}" + + match[0] = match[0].replace(/./g, invisibles.space) if invisibles.space + trailingHtml = "#{@wrapCharacters(match[0])}" + + endIndex = match.index + + html = leadingHtml + @wrapCharacters(html, startIndex, endIndex) + trailingHtml html + + wrapCharacters: (str, startIndex, endIndex) -> + startIndex ?= 0 + endIndex ?= str.length + + ret = '' + + for i in [startIndex...endIndex] + character = str[i] + .replace('&', '&') + .replace('"', '"') + .replace("'", ''') + .replace('<', '<') + .replace('>', '>') + ret += "#{character}" + + ret + From 2394f25b92edaa0fa64bed856ccd87a339d2438e Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Mon, 30 Sep 2013 14:33:06 -0700 Subject: [PATCH 11/87] Add the leftPixel cache back in --- spec/editor-spec.coffee | 26 +++++++++++++++++++++++++- src/editor.coffee | 33 +++++++++++++++++++++------------ 2 files changed, 46 insertions(+), 13 deletions(-) diff --git a/spec/editor-spec.coffee b/spec/editor-spec.coffee index 1ad1c527c..f6b6d8a59 100644 --- a/spec/editor-spec.coffee +++ b/spec/editor-spec.coffee @@ -2199,10 +2199,34 @@ describe "Editor", -> expect(editor.pixelPositionForBufferPosition([2,7])).toEqual top: 0, left: 0 describe "when the editor is attached and visible", -> - it "returns the top and left pixel positions", -> + beforeEach -> editor.attachToDom() + + it "returns the top and left pixel positions", -> expect(editor.pixelPositionForBufferPosition([2,7])).toEqual top: 40, left: 70 + it "caches the left position", -> + editor.renderedLines.css('font-size', '16px') + expect(editor.pixelPositionForBufferPosition([2,8])).toEqual top: 40, left: 80 + + # make characters smaller + editor.renderedLines.css('font-size', '15px') + + expect(editor.pixelPositionForBufferPosition([2,8])).toEqual top: 40, left: 80 + + it "breaks left position cache when line is changed", -> + editor.renderedLines.css('font-size', '16px') + expect(editor.pixelPositionForBufferPosition([2,8])).toEqual top: 40, left: 80 + + editor.setCursorBufferPosition([2, 8]) + editor.insertText("a") + + # make characters smaller + editor.renderedLines.css('font-size', '15px') + + expect(editor.pixelPositionForBufferPosition([2,8])).toEqual top: 40, left: 72 + + describe "when clicking in the gutter", -> beforeEach -> editor.attachToDom() diff --git a/src/editor.coffee b/src/editor.coffee index a1791d1a6..ba18118a9 100644 --- a/src/editor.coffee +++ b/src/editor.coffee @@ -94,8 +94,7 @@ class Editor extends View @pendingChanges = [] @newCursors = [] @newSelections = [] - @lineId = 0 - @pixelLeftCache = {} + @pixelLeftCache = new WeakMap() if editSession? @edit(editSession) @@ -1330,10 +1329,6 @@ class Editor extends View clearDirtyRanges: (intactRanges) -> renderedLines = @renderedLines[0] - killLine = (line) -> - next = line.nextSibling - renderedLines.removeChild(line) - next if intactRanges.length == 0 @renderedLines.empty() @@ -1341,13 +1336,19 @@ class Editor extends View domPosition = 0 for intactRange in intactRanges while intactRange.domStart > domPosition - currentLine = killLine(currentLine) + currentLine = @clearLine(currentLine) domPosition++ for i in [intactRange.start..intactRange.end] currentLine = currentLine.nextSibling domPosition++ while currentLine - currentLine = killLine(currentLine) + currentLine = @clearLine(currentLine) + + clearLine: (lineElement) -> + @pixelLeftCache.delete(lineElement) + next = lineElement.nextSibling + @renderedLines[0].removeChild(lineElement) + next fillDirtyRanges: (intactRanges, renderFrom, renderTo) -> renderedLines = @renderedLines[0] @@ -1427,9 +1428,9 @@ class Editor extends View htmlForScreenLine: (screenLine, screenRow) -> { tokens, text, lineEnding, fold, isSoftWrapped } = screenLine if fold - attributes = { class: 'fold line', 'fold-id': fold.id, 'line-id': @lineId++ } + attributes = { class: 'fold line', 'fold-id': fold.id } else - attributes = { class: 'line', 'line-id': @lineId++ } + attributes = { class: 'line' } invisibles = @invisibles if @showInvisibles eolInvisibles = @getEndOfLineInvisibles(screenLine) @@ -1519,11 +1520,19 @@ class Editor extends View @renderedLines[0].removeChild(lineElement) { top: row * @lineHeight, left } - positionLeftForLineAndColumn: (lineElement, column) -> - chars = lineElement.find('.character') + positionLeftForLineAndColumn: (line, column) -> + lineCache = @pixelLeftCache.get(line[0]) + @pixelLeftCache.set(line[0], lineCache = {}) unless lineCache? + + return lineCache[column] if lineCache[column]? + + chars = line.find('.character') left = 0 for i in [0...column] left += chars[i].offsetWidth if chars[i] + + lineCache[column] = left + left pixelOffsetForScreenPosition: (position) -> From 98a3bb475b9b2b7448b9ff68e79866ce99cc29a2 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Mon, 30 Sep 2013 14:54:40 -0700 Subject: [PATCH 12/87] Fix for non-extant lines --- src/editor.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/src/editor.coffee b/src/editor.coffee index ba18118a9..9603f7357 100644 --- a/src/editor.coffee +++ b/src/editor.coffee @@ -1514,6 +1514,7 @@ class Editor extends View lineElement = existingLineElement = line[0] unless existingLineElement lineElement = @buildLineElementForScreenRow(actualRow) + line = $(lineElement) @renderedLines.append(lineElement) left = @positionLeftForLineAndColumn(line, column) unless existingLineElement From 0ab382133b132b74ed04fad029d32f71d867cb86 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Mon, 30 Sep 2013 14:54:56 -0700 Subject: [PATCH 13/87] Back to display == none for $.fn.isHidden() --- src/jquery-extensions.coffee | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/src/jquery-extensions.coffee b/src/jquery-extensions.coffee index c749b1ce5..25cf799b9 100644 --- a/src/jquery-extensions.coffee +++ b/src/jquery-extensions.coffee @@ -38,18 +38,9 @@ $.fn.isVisible = -> !@isHidden() $.fn.isHidden = -> - # Implementation taken from jQuery's `:hidden` expression code: - # https://github.com/jquery/jquery/blob/master/src/css/hiddenVisibleSelectors.js - # - # We were using a pseudo selector: @is(':hidden'). But jQuery's pseudo - # selector code checks the element's webkitMatchesSelector, which is always - # false, and is really really really slow. - - elem = this[0] - - return null unless elem - - elem.offsetWidth <= 0 and elem.offsetHeight <= 0 + # We used to check @is(':hidden'). But this is much faster than the + # offsetWidth/offsetHeight check + all the pseudo selector mess in jquery. + @css('display') == 'none' $.fn.isDisabled = -> !!@attr('disabled') From 45d3fea3d033216300989de813a3e9efe7314a6b Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Mon, 30 Sep 2013 17:04:17 -0700 Subject: [PATCH 14/87] Update only the changed line numbers --- src/editor.coffee | 47 ++++++++++++++++++++++++++--------------- src/gutter.coffee | 54 ++++++++++++++++++++++++++++++----------------- 2 files changed, 65 insertions(+), 36 deletions(-) diff --git a/src/editor.coffee b/src/editor.coffee index 9603f7357..369fcf54f 100644 --- a/src/editor.coffee +++ b/src/editor.coffee @@ -1245,10 +1245,11 @@ class Editor extends View if @pendingChanges.length == 0 and @firstRenderedScreenRow and @firstRenderedScreenRow <= renderFrom and renderTo <= @lastRenderedScreenRow return - @gutter.updateLineNumbers(@pendingChanges, renderFrom, renderTo) - intactRanges = @computeIntactRanges() - @pendingChanges = [] - @truncateIntactRanges(intactRanges, renderFrom, renderTo) + changes = @pendingChanges + intactRanges = @computeIntactRanges(renderFrom, renderTo) + + @gutter.updateLineNumbers(changes, intactRanges, renderFrom, renderTo) + @clearDirtyRanges(intactRanges) @fillDirtyRanges(intactRanges, renderFrom, renderTo) @firstRenderedScreenRow = renderFrom @@ -1274,7 +1275,7 @@ class Editor extends View emptyLineChanges - computeIntactRanges: -> + computeIntactRanges: (renderFrom, renderTo) -> return [] if !@firstRenderedScreenRow? and !@lastRenderedScreenRow? intactRanges = [{start: @firstRenderedScreenRow, end: @lastRenderedScreenRow, domStart: 0}] @@ -1309,6 +1310,9 @@ class Editor extends View domStart: range.domStart + change.end + 1 - range.start ) intactRanges = newIntactRanges + + @truncateIntactRanges(intactRanges, renderFrom, renderTo) + @pendingChanges = [] intactRanges @@ -1327,45 +1331,54 @@ class Editor extends View i++ intactRanges.sort (a, b) -> a.domStart - b.domStart - clearDirtyRanges: (intactRanges) -> - renderedLines = @renderedLines[0] + # renderedLines - optional + # clearLine - optional + clearDirtyRanges: (intactRanges, renderedLines, clearLine) -> + renderedLines ?= @renderedLines[0] + clearLine ?= @clearLine if intactRanges.length == 0 - @renderedLines.empty() + renderedLines.innerHTML = '' else if currentLine = renderedLines.firstChild domPosition = 0 for intactRange in intactRanges while intactRange.domStart > domPosition - currentLine = @clearLine(currentLine) + currentLine = clearLine(currentLine) domPosition++ for i in [intactRange.start..intactRange.end] currentLine = currentLine.nextSibling domPosition++ while currentLine - currentLine = @clearLine(currentLine) + currentLine = clearLine(currentLine) - clearLine: (lineElement) -> + clearLine: (lineElement) => @pixelLeftCache.delete(lineElement) next = lineElement.nextSibling @renderedLines[0].removeChild(lineElement) next - fillDirtyRanges: (intactRanges, renderFrom, renderTo) -> - renderedLines = @renderedLines[0] - nextIntact = intactRanges.shift() + # renderedLines - optional + # buildLineElements - optional + fillDirtyRanges: (intactRanges, renderFrom, renderTo, renderedLines, buildLineElements) -> + renderedLines ?= @renderedLines[0] + buildLineElements ?= @buildLineElementsForScreenRows + + i = 0 + nextIntact = intactRanges[i] currentLine = renderedLines.firstChild row = renderFrom while row <= renderTo if row == nextIntact?.end + 1 - nextIntact = intactRanges.shift() + nextIntact = intactRanges[++i] + if !nextIntact or row < nextIntact.start if nextIntact dirtyRangeEnd = nextIntact.start - 1 else dirtyRangeEnd = renderTo - for lineElement in @buildLineElementsForScreenRows(row, dirtyRangeEnd) + for lineElement in buildLineElements(row, dirtyRangeEnd) renderedLines.insertBefore(lineElement, currentLine) row++ else @@ -1413,7 +1426,7 @@ class Editor extends View buildLineElementForScreenRow: (screenRow) -> @buildLineElementsForScreenRows(screenRow, screenRow)[0] - buildLineElementsForScreenRows: (startRow, endRow) -> + buildLineElementsForScreenRows: (startRow, endRow) => div = document.createElement('div') div.innerHTML = @htmlForScreenRows(startRow, endRow) new Array(div.children...) diff --git a/src/gutter.coffee b/src/gutter.coffee index d1c3ac8b1..9f56b19cf 100644 --- a/src/gutter.coffee +++ b/src/gutter.coffee @@ -64,7 +64,7 @@ class Gutter extends View ### Internal ### - updateLineNumbers: (changes, renderFrom, renderTo) -> + updateLineNumbers: (changes, intactRanges, renderFrom, renderTo) -> if renderFrom < @firstScreenRow or renderTo > @lastScreenRow performUpdate = true else if @getEditor().getLastScreenRow() < @lastScreenRow @@ -75,33 +75,49 @@ class Gutter extends View performUpdate = true break - @renderLineNumbers(renderFrom, renderTo) if performUpdate + @renderLineNumbers(intactRanges, renderFrom, renderTo) if performUpdate - renderLineNumbers: (startScreenRow, endScreenRow) -> + renderLineNumbers: (intactRanges, startScreenRow, endScreenRow) -> editor = @getEditor() - maxDigits = editor.getLineCount().toString().length - rows = editor.bufferRowsForScreenRows(startScreenRow, endScreenRow) - cursorScreenRow = editor.getCursorScreenPosition().row - @lineNumbers[0].innerHTML = $$$ -> - for row in rows - if row == lastScreenRow - rowValue = '•' - else - rowValue = (row + 1).toString() - classes = ['line-number'] - classes.push('fold') if editor.isFoldedAtBufferRow(row) - @div linenumber: row, class: classes.join(' '), => - rowValuePadding = _.multiplyString(' ', maxDigits - rowValue.length) - @raw("#{rowValuePadding}#{rowValue}") - - lastScreenRow = row + editor.clearDirtyRanges(intactRanges, @lineNumbers[0], @clearLine) + editor.fillDirtyRanges(intactRanges, startScreenRow, endScreenRow, @lineNumbers[0], @buildLineElements) @firstScreenRow = startScreenRow @lastScreenRow = endScreenRow @highlightedRows = null @highlightLines() + clearLine: (lineElement) => + next = lineElement.nextSibling + @lineNumbers[0].removeChild(lineElement) + next + + buildLineElements: (startScreenRow, endScreenRow) => + editor = @getEditor() + maxDigits = editor.getLineCount().toString().length + rows = editor.bufferRowsForScreenRows(startScreenRow, endScreenRow) + + html = '' + for row in rows + if row == lastScreenRow + rowValue = '•' + else + rowValue = (row + 1).toString() + + classes = 'line-number' + classes += ' fold' if editor.isFoldedAtBufferRow(row) + + rowValuePadding = _.multiplyString(' ', maxDigits - rowValue.length) + + html += """
#{rowValuePadding}#{rowValue}
""" + + lastScreenRow = row + + div = document.createElement('div') + div.innerHTML = html + new Array(div.children...) + removeLineHighlights: -> return unless @highlightedLineNumbers for line in @highlightedLineNumbers From aa404a316dcdfcf8a25c59c1fcd42a5ad343e94f Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Mon, 30 Sep 2013 17:21:28 -0700 Subject: [PATCH 15/87] Don't allow NaNs --- src/editor.coffee | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/editor.coffee b/src/editor.coffee index 369fcf54f..b07311cce 100644 --- a/src/editor.coffee +++ b/src/editor.coffee @@ -1400,14 +1400,18 @@ class Editor extends View # # Returns a {Number}. getFirstVisibleScreenRow: -> - Math.floor(@scrollTop() / @lineHeight) + screenRow = Math.floor(@scrollTop() / @lineHeight) + screenRow = 0 if isNaN(screenRow) + screenRow # Retrieves the number of the row that is visible and currently at the bottom of the editor. # # Returns a {Number}. getLastVisibleScreenRow: -> calculatedRow = Math.ceil((@scrollTop() + @scrollView.height()) / @lineHeight) - 1 - Math.max(0, Math.min(@getScreenLineCount() - 1, calculatedRow)) + screenRow = Math.max(0, Math.min(@getScreenLineCount() - 1, calculatedRow)) + screenRow = 0 if isNaN(screenRow) + screenRow # Given a row number, identifies if it is currently visible. # From 6a494f65a5dc3390a9f3a0a6d6ecd438e6427a4f Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Mon, 30 Sep 2013 17:25:15 -0700 Subject: [PATCH 16/87] :lipstick: --- src/editor.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/src/editor.coffee b/src/editor.coffee index b07311cce..20c8d64cb 100644 --- a/src/editor.coffee +++ b/src/editor.coffee @@ -1213,7 +1213,6 @@ class Editor extends View should = (startRow >= @firstRenderedScreenRow and startRow <= @lastRenderedScreenRow) or (endRow >= @firstRenderedScreenRow and endRow <= @lastRenderedScreenRow) should - syncCursorAnimations: -> for cursorView in @getCursorViews() do (cursorView) -> cursorView.resetBlinking() From 875f0ca1e53413377567e069270058dd9aba7fd9 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Tue, 1 Oct 2013 11:38:30 -0700 Subject: [PATCH 17/87] Get benchmarks running again: Fix imports. --- benchmark/benchmark-bootstrap.coffee | 6 ++++-- benchmark/benchmark-helper.coffee | 12 +++++------- benchmark/benchmark-suite.coffee | 6 ++---- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/benchmark/benchmark-bootstrap.coffee b/benchmark/benchmark-bootstrap.coffee index 5d74df7de..7bf2a1cbe 100644 --- a/benchmark/benchmark-bootstrap.coffee +++ b/benchmark/benchmark-bootstrap.coffee @@ -1,5 +1,7 @@ -Atom = require '../src/atom' -window.atom = new Atom() +require '../src/window' +require '../src/atom' +require 'atom' + {runSpecSuite} = require '../spec/jasmine-helper' atom.openDevTools() diff --git a/benchmark/benchmark-helper.coffee b/benchmark/benchmark-helper.coffee index aa91f76d7..5b94da991 100644 --- a/benchmark/benchmark-helper.coffee +++ b/benchmark/benchmark-helper.coffee @@ -1,11 +1,9 @@ require '../spec/spec-helper' -$ = require 'jquery' -_ = require 'underscore' -{Point} = require 'telepath' -Project = require 'project' -fsUtils = require 'fs-utils' -TokenizedBuffer = require 'tokenized-buffer' +{$, _, Point, fs} = require 'atom' +Project = require '../src/project' +fsUtils = require '../src/fs-utils' +TokenizedBuffer = require '../src/tokenized-buffer' defaultCount = 100 window.pbenchmark = (args...) -> window.benchmark(args..., profile: true) @@ -13,7 +11,7 @@ window.fbenchmark = (args...) -> window.benchmark(args..., focused: true) window.fpbenchmark = (args...) -> window.benchmark(args..., profile: true, focused: true) window.pfbenchmark = window.fpbenchmark -window.benchmarkFixturesProject = new Project(fsUtils.resolveOnLoadPath('benchmark/fixtures')) +window.benchmarkFixturesProject = new Project(fsUtils.resolveOnLoadPath('../benchmark/fixtures')) beforeEach -> window.project = window.benchmarkFixturesProject diff --git a/benchmark/benchmark-suite.coffee b/benchmark/benchmark-suite.coffee index 7a15c846c..d760ce736 100644 --- a/benchmark/benchmark-suite.coffee +++ b/benchmark/benchmark-suite.coffee @@ -1,8 +1,6 @@ require './benchmark-helper' -$ = require 'jquery' -_ = require 'underscore' -TokenizedBuffer = require 'tokenized-buffer' -RootView = require 'root-view' +{$, _, RootView} = require 'atom' +TokenizedBuffer = require '../src/tokenized-buffer' describe "editor.", -> editor = null From 303124f1dd51f25449849cf7da3f7c6c07bc159c Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Tue, 1 Oct 2013 16:43:59 -0700 Subject: [PATCH 18/87] Add benchmarks for text rendering --- benchmark/benchmark-suite.coffee | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/benchmark/benchmark-suite.coffee b/benchmark/benchmark-suite.coffee index d760ce736..043d0ed33 100644 --- a/benchmark/benchmark-suite.coffee +++ b/benchmark/benchmark-suite.coffee @@ -10,7 +10,6 @@ describe "editor.", -> window.rootView = new RootView window.rootView.attachToDom() - rootView.width(1024) rootView.height(768) rootView.open() # open blank editor @@ -60,6 +59,24 @@ describe "editor.", -> editor.insertText('"') editor.backspace() + describe "text-rendering.", -> + beforeEach -> + editor.scrollTop(200) + + benchmark "resetDisplay", 20, -> + editor.resetDisplay() + + benchmark "htmlForScreenRows", 50, -> + lastRow = editor.getLastScreenRow() + editor.htmlForScreenRows(0, lastRow) + + benchmark "htmlForScreenRows.htmlParsing", 20, -> + lastRow = editor.getLastScreenRow() + html = editor.htmlForScreenRows(0, lastRow) + + div = document.createElement('div') + div.innerHTML = html + describe "9000-line-file.", -> benchmark "opening.", 5, -> rootView.open('huge.js') From c2bb5a998bcb7bf08f0589c97942930d41e3b72f Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Tue, 1 Oct 2013 16:45:43 -0700 Subject: [PATCH 19/87] Override isHidden for the CursorView We know it's visible if it is on the dom and doesn't have display == none set. This is an order of magnitude faster than the default implementation which calls getComputedStyle() --- src/cursor-view.coffee | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/cursor-view.coffee b/src/cursor-view.coffee index 3c15c235d..5cef2af46 100644 --- a/src/cursor-view.coffee +++ b/src/cursor-view.coffee @@ -54,6 +54,14 @@ class CursorView extends View @setVisible(@cursor.isVisible() and not @editor.isFoldedAtScreenRow(screenPosition.row)) + # Override for speed. The base function checks the computedStyle + isHidden: -> + style = this[0].style + if style.display == 'none' or not @isOnDom() + true + else + false + needsAutoscroll: -> @cursor.needsAutoscroll From d351938702960b9668603be80723253363f0ca8d Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Tue, 1 Oct 2013 16:46:40 -0700 Subject: [PATCH 20/87] Speed up setting the hidden input at the same place as the cursor. Order of magnitude speedup --- src/editor.coffee | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/editor.coffee b/src/editor.coffee index 20c8d64cb..5c8ab8631 100644 --- a/src/editor.coffee +++ b/src/editor.coffee @@ -704,7 +704,12 @@ class Editor extends View @on 'cursor:moved', => return unless @isFocused cursorView = @getCursorView() - @hiddenInput.offset(cursorView.offset()) if cursorView.isVisible() + + if cursorView.isVisible() + # This is an order of magnitude faster than checking .offset(). + style = cursorView[0].style + @hiddenInput[0].style.top = style.top + @hiddenInput[0].style.left = style.left selectedText = null @hiddenInput.on 'compositionstart', => From 8cd04b51784f150d0f16bf465358dfc9f66caec0 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Tue, 1 Oct 2013 16:47:23 -0700 Subject: [PATCH 21/87] Override isHidden for the Editor We know it's visible if it is on the dom and doesn't have display == none set. This is an order of magnitude faster than the default implementation which calls getComputedStyle() --- src/editor.coffee | 69 +++++++++++++++++++++++++++-------------------- 1 file changed, 40 insertions(+), 29 deletions(-) diff --git a/src/editor.coffee b/src/editor.coffee index 5c8ab8631..f45d7d541 100644 --- a/src/editor.coffee +++ b/src/editor.coffee @@ -1139,6 +1139,14 @@ class Editor extends View @layerMinWidth = minWidth @trigger 'editor:min-width-changed' + # Override for speed. The base function checks computedStyle, unnecessary here. + isHidden: -> + style = this[0].style + if style.display == 'none' or not @isOnDom() + true + else + false + clearRenderedLines: -> @renderedLines.empty() @firstRenderedScreenRow = null @@ -1440,11 +1448,11 @@ class Editor extends View new Array(div.children...) htmlForScreenRows: (startRow, endRow) -> - htmlLines = [] + htmlLines = '' screenRow = startRow for line in @activeEditSession.linesForScreenRows(startRow, endRow) - htmlLines.push(@htmlForScreenLine(line, screenRow++)) - htmlLines.join('\n\n') + htmlLines += @htmlForScreenLine(line, screenRow++) + htmlLines htmlForScreenLine: (screenLine, screenRow) -> { tokens, text, lineEnding, fold, isSoftWrapped } = screenLine @@ -1629,30 +1637,9 @@ class Editor extends View scopeStack = [] line = [] - updateScopeStack = (desiredScopes) -> - excessScopes = scopeStack.length - desiredScopes.length - _.times(excessScopes, popScope) if excessScopes > 0 - - # pop until common prefix - for i in [scopeStack.length..0] - break if _.isEqual(scopeStack[0...i], desiredScopes[0...i]) - popScope() - - # push on top of common prefix until scopeStack == desiredScopes - for j in [i...desiredScopes.length] - pushScope(desiredScopes[j]) - - pushScope = (scope) -> - scopeStack.push(scope) - line.push("") - - popScope = -> - scopeStack.pop() - line.push("") - - attributePairs = [] - attributePairs.push "#{attributeName}=\"#{value}\"" for attributeName, value of attributes - line.push("
") + attributePairs = '' + attributePairs += " #{attributeName}=\"#{value}\"" for attributeName, value of attributes + line.push("
") if text == '' html = Editor.buildEmptyLineHtml(showIndentGuide, eolInvisibles, htmlEolInvisibles, indentation, activeEditSession, mini) @@ -1663,20 +1650,44 @@ class Editor extends View lineIsWhitespaceOnly = firstTrailingWhitespacePosition is 0 position = 0 for token in tokens - updateScopeStack(token.scopes) + @updateScopeStack(line, scopeStack, token.scopes) hasLeadingWhitespace = position < firstNonWhitespacePosition hasTrailingWhitespace = position + token.value.length > firstTrailingWhitespacePosition hasIndentGuide = not mini and showIndentGuide and (hasLeadingWhitespace or lineIsWhitespaceOnly) line.push(token.getValueAsHtml({invisibles, hasLeadingWhitespace, hasTrailingWhitespace, hasIndentGuide})) position += token.value.length - popScope() while scopeStack.length > 0 + @popScope(line, scopeStack) while scopeStack.length > 0 line.push(htmlEolInvisibles) unless text == '' line.push("") if fold line.push('
') line.join('') + @updateScopeStack: (line, scopeStack, desiredScopes) -> + excessScopes = scopeStack.length - desiredScopes.length + if excessScopes > 0 + @popScope(line, scopeStack) while excessScopes-- + + # pop until common prefix + for i in [scopeStack.length..0] + break if _.isEqual(scopeStack[0...i], desiredScopes[0...i]) + @popScope(line, scopeStack) + + # push on top of common prefix until scopeStack == desiredScopes + for j in [i...desiredScopes.length] + @pushScope(line, scopeStack, desiredScopes[j]) + + null + + @pushScope: (line, scopeStack, scope) -> + scopeStack.push(scope) + line.push("") + + @popScope: (line, scopeStack)-> + scopeStack.pop() + line.push("") + @buildEmptyLineHtml: (showIndentGuide, eolInvisibles, htmlEolInvisibles, indentation, activeEditSession, mini) -> if not mini and showIndentGuide if indentation > 0 From a57c86f4ea23828450beb3deccf928752ebe3ad9 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Tue, 1 Oct 2013 16:48:19 -0700 Subject: [PATCH 22/87] Shortcut out of isHidden when we can. Checking computed style is an order of magnitude more expensive than checking the style property. --- src/jquery-extensions.coffee | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/jquery-extensions.coffee b/src/jquery-extensions.coffee index 25cf799b9..76ee60178 100644 --- a/src/jquery-extensions.coffee +++ b/src/jquery-extensions.coffee @@ -40,7 +40,14 @@ $.fn.isVisible = -> $.fn.isHidden = -> # We used to check @is(':hidden'). But this is much faster than the # offsetWidth/offsetHeight check + all the pseudo selector mess in jquery. - @css('display') == 'none' + style = this[0].style + + if style.display == 'none' or not @isOnDom() + true + else if style.display + false + else + getComputedStyle(this[0]).display == 'none' $.fn.isDisabled = -> !!@attr('disabled') From 8a3f137519a7182424163fb20511a26fa1f1c1f4 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Tue, 1 Oct 2013 16:52:06 -0700 Subject: [PATCH 23/87] Speed up token html generation. 20ms -> 6ms Character replace was most of it. Removing regex creation, array creation and joins seems to reduce the GC pressure. Calling 500 times, GC spent 100ms less time. --- src/token.coffee | 45 +++++++++++++++++++++------------------------ 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/src/token.coffee b/src/token.coffee index 17910d71b..95422c5aa 100644 --- a/src/token.coffee +++ b/src/token.coffee @@ -2,6 +2,8 @@ _ = require './underscore-extensions' textUtils = require './text-utils' whitespaceRegexesByTabLength = {} +LEADING_WHITESPACE_REGEX = /^[ ]+/ +TRAILING_WHITESPACE_REGEX = /[ ]+$/ # Private: Represents a single unit of text as selected by a grammar. module.exports = @@ -125,11 +127,9 @@ class Token html = @value if @isHardTab - classes = [] - classes.push('indent-guide') if hasIndentGuide - classes.push('invisible-character') if invisibles.tab - classes.push('hard-tab') - classes = classes.join(' ') + classes = 'hard-tab' + classes += ' indent-guide' if hasIndentGuide + classes += ' invisible-character' if invisibles.tab html = html.replace /^./, (match) => match = invisibles.tab ? match "#{@wrapCharacters(match)}" @@ -140,24 +140,20 @@ class Token leadingHtml = '' trailingHtml = '' - if hasLeadingWhitespace and match = /^[ ]+/.exec(html) - classes = [] - classes.push('indent-guide') if hasIndentGuide - classes.push('invisible-character') if invisibles.space - classes.push('leading-whitespace') - classes = classes.join(' ') + if hasLeadingWhitespace and match = LEADING_WHITESPACE_REGEX.exec(html) + classes = 'leading-whitespace' + classes += ' indent-guide' if hasIndentGuide + classes += ' invisible-character' if invisibles.space match[0] = match[0].replace(/./g, invisibles.space) if invisibles.space leadingHtml = "#{@wrapCharacters(match[0])}" startIndex = match[0].length - if hasTrailingWhitespace and match = /[ ]+$/.exec(html) - classes = [] - classes.push('indent-guide') if hasIndentGuide and not hasLeadingWhitespace - classes.push('invisible-character') if invisibles.space - classes.push('trailing-whitespace') - classes = classes.join(' ') + if hasTrailingWhitespace and match = TRAILING_WHITESPACE_REGEX.exec(html) + classes = 'trailing-whitespace' + classes += ' indent-guide' if hasIndentGuide and not hasLeadingWhitespace + classes += ' invisible-character' if invisibles.space match[0] = match[0].replace(/./g, invisibles.space) if invisibles.space trailingHtml = "#{@wrapCharacters(match[0])}" @@ -175,13 +171,14 @@ class Token ret = '' for i in [startIndex...endIndex] - character = str[i] - .replace('&', '&') - .replace('"', '"') - .replace("'", ''') - .replace('<', '<') - .replace('>', '>') + character = switch str[i] + when '&' then '&' + when '"' then '"' + when "'" then ''' + when '<' then '<' + when '>' then '>' + else str[i] + ret += "#{character}" ret - From 6eb72ac2a1c6ec4319e3fdd0b9c298135b25f532 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Tue, 1 Oct 2013 18:09:55 -0700 Subject: [PATCH 24/87] Use getElementsByClassName rather than jQuery --- src/editor.coffee | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/editor.coffee b/src/editor.coffee index f45d7d541..f7940642f 100644 --- a/src/editor.coffee +++ b/src/editor.coffee @@ -1539,24 +1539,22 @@ class Editor extends View {row, column} = Point.fromObject(position) actualRow = Math.floor(row) - line = @lineElementForScreenRow(actualRow) - lineElement = existingLineElement = line[0] + lineElement = existingLineElement = @lineElementForScreenRow(actualRow)[0] unless existingLineElement lineElement = @buildLineElementForScreenRow(actualRow) - line = $(lineElement) @renderedLines.append(lineElement) - left = @positionLeftForLineAndColumn(line, column) + left = @positionLeftForLineAndColumn(lineElement, column) unless existingLineElement @renderedLines[0].removeChild(lineElement) { top: row * @lineHeight, left } - positionLeftForLineAndColumn: (line, column) -> - lineCache = @pixelLeftCache.get(line[0]) - @pixelLeftCache.set(line[0], lineCache = {}) unless lineCache? + positionLeftForLineAndColumn: (lineElement, column) -> + lineCache = @pixelLeftCache.get(lineElement) + @pixelLeftCache.set(lineElement, lineCache = {}) unless lineCache? return lineCache[column] if lineCache[column]? - chars = line.find('.character') + chars = lineElement.getElementsByClassName('character') left = 0 for i in [0...column] left += chars[i].offsetWidth if chars[i] From 8ab8201020361ddf6a2c3e47e4ae05a3220e1e48 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Tue, 1 Oct 2013 18:22:43 -0700 Subject: [PATCH 25/87] More benchmarks for text rendering --- benchmark/benchmark-suite.coffee | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/benchmark/benchmark-suite.coffee b/benchmark/benchmark-suite.coffee index 043d0ed33..1e833fc21 100644 --- a/benchmark/benchmark-suite.coffee +++ b/benchmark/benchmark-suite.coffee @@ -59,24 +59,49 @@ describe "editor.", -> editor.insertText('"') editor.backspace() + describe "calculating-pixel-position.", -> + line = null + beforeEach -> + editor.scrollTop(2000) + editor.resetDisplay() + line = editor.lineElementForScreenRow(106)[0] + + benchmark "positionLeftForLineAndColumn", 20000, -> + editor.positionLeftForLineAndColumn(line, 82) + editor.pixelLeftCache.delete(line) + + benchmark "positionLeftForLineAndColumn.cached", 20000, -> + editor.positionLeftForLineAndColumn(line, 82) + describe "text-rendering.", -> beforeEach -> - editor.scrollTop(200) + editor.scrollTop(2000) - benchmark "resetDisplay", 20, -> + benchmark "resetDisplay", 50, -> editor.resetDisplay() benchmark "htmlForScreenRows", 50, -> lastRow = editor.getLastScreenRow() editor.htmlForScreenRows(0, lastRow) - benchmark "htmlForScreenRows.htmlParsing", 20, -> + benchmark "htmlForScreenRows.htmlParsing", 50, -> lastRow = editor.getLastScreenRow() html = editor.htmlForScreenRows(0, lastRow) div = document.createElement('div') div.innerHTML = html + describe "line-htmlification.", -> + div = null + html = null + beforeEach -> + lastRow = editor.getLastScreenRow() + html = editor.htmlForScreenRows(0, lastRow) + div = document.createElement('div') + + benchmark "setInnerHTML", 1, -> + div.innerHTML = html + describe "9000-line-file.", -> benchmark "opening.", 5, -> rootView.open('huge.js') From 5a772d70782da553675e232612a31e213d9caf44 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Tue, 1 Oct 2013 18:24:02 -0700 Subject: [PATCH 26/87] Move back to the node walker and Range() This is slower than the span compute, but rendering lines without tons of spans is much faster than with --- src/editor.coffee | 19 ++++++++++++++----- src/token.coffee | 2 +- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/editor.coffee b/src/editor.coffee index f7940642f..7c23544b4 100644 --- a/src/editor.coffee +++ b/src/editor.coffee @@ -1554,13 +1554,22 @@ class Editor extends View return lineCache[column] if lineCache[column]? - chars = lineElement.getElementsByClassName('character') - left = 0 - for i in [0...column] - left += chars[i].offsetWidth if chars[i] + delta = 0 + iterator = document.createNodeIterator(lineElement, NodeFilter.SHOW_TEXT, acceptNode: -> NodeFilter.FILTER_ACCEPT) + while textNode = iterator.nextNode() + nextDelta = delta + textNode.textContent.length + if nextDelta >= column + offset = column - delta + break + delta = nextDelta + + range = document.createRange() + range.setEnd(textNode, offset) + range.collapse() + left = range.getClientRects()[0].left - Math.floor(@scrollView.offset().left) + Math.floor(@scrollLeft()) + range.detach() lineCache[column] = left - left pixelOffsetForScreenPosition: (position) -> diff --git a/src/token.coffee b/src/token.coffee index 95422c5aa..96d6b26c4 100644 --- a/src/token.coffee +++ b/src/token.coffee @@ -179,6 +179,6 @@ class Token when '>' then '>' else str[i] - ret += "#{character}" + ret += character ret From 8463c759b593145cd49ed97b23a2656328ede4ff Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Wed, 2 Oct 2013 15:09:34 -0700 Subject: [PATCH 27/87] Use global range. Faster! Less garbage! --- benchmark/benchmark-suite.coffee | 2 +- src/editor.coffee | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/benchmark/benchmark-suite.coffee b/benchmark/benchmark-suite.coffee index 1e833fc21..7857da039 100644 --- a/benchmark/benchmark-suite.coffee +++ b/benchmark/benchmark-suite.coffee @@ -59,7 +59,7 @@ describe "editor.", -> editor.insertText('"') editor.backspace() - describe "calculating-pixel-position.", -> + fdescribe "calculating-pixel-position.", -> line = null beforeEach -> editor.scrollTop(2000) diff --git a/src/editor.coffee b/src/editor.coffee index 7c23544b4..f5df9f6ef 100644 --- a/src/editor.coffee +++ b/src/editor.coffee @@ -9,6 +9,8 @@ fsUtils = require './fs-utils' $ = require './jquery-extensions' _ = require './underscore-extensions' +MEASURE_RANGE = document.createRange() + # Private: Represents the entire visual pane in Atom. # # The Editor manages the {EditSession}, which manages the file buffers. @@ -1563,11 +1565,9 @@ class Editor extends View break delta = nextDelta - range = document.createRange() - range.setEnd(textNode, offset) - range.collapse() - left = range.getClientRects()[0].left - Math.floor(@scrollView.offset().left) + Math.floor(@scrollLeft()) - range.detach() + MEASURE_RANGE.setEnd(textNode, offset) + MEASURE_RANGE.collapse() + left = MEASURE_RANGE.getClientRects()[0].left - Math.floor(@scrollView.offset().left) + Math.floor(@scrollLeft()) lineCache[column] = left left From d0be7fbf8eb39992c8e9bd26a8528985a58d8673 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Wed, 2 Oct 2013 18:34:03 -0700 Subject: [PATCH 28/87] Add a character width cache based on scopes. This is slower than the position cache in the best case, but faster in the worst and average case. With this, you can search for a space in find and replace, and still scroll the buffer. In editor.coffee, there are 10,500 spaces. To highlight all of them, the previous cache method took 7 seconds, this takes 2 when the cache is empty, and about 10ms when the entire file is cached. --- src/editor.coffee | 103 +++++++++++++++++++++++++++++++++++++--------- 1 file changed, 84 insertions(+), 19 deletions(-) diff --git a/src/editor.coffee b/src/editor.coffee index f5df9f6ef..b67474591 100644 --- a/src/editor.coffee +++ b/src/editor.coffee @@ -10,12 +10,15 @@ $ = require './jquery-extensions' _ = require './underscore-extensions' MEASURE_RANGE = document.createRange() +TEXT_NODE_FILTER = { acceptNode: -> NodeFilter.FILTER_ACCEPT } +NO_SCOPE = ['no-scope'] # Private: Represents the entire visual pane in Atom. # # The Editor manages the {EditSession}, which manages the file buffers. module.exports = class Editor extends View + @characterWidthCache: {} @configDefaults: fontSize: 20 showInvisibles: false @@ -96,7 +99,6 @@ class Editor extends View @pendingChanges = [] @newCursors = [] @newSelections = [] - @pixelLeftCache = new WeakMap() if editSession? @edit(editSession) @@ -969,6 +971,9 @@ class Editor extends View # fontSize - A {Number} indicating the font size in pixels. setFontSize: (fontSize) -> @css('font-size', "#{fontSize}px}") + + @clearCharacterWidthCache() + if @isOnDom() @redraw() else @@ -985,6 +990,9 @@ class Editor extends View # fontFamily - A {String} identifying the CSS `font-family`, setFontFamily: (fontFamily='') -> @css('font-family', fontFamily) + + @clearCharacterWidthCache() + @redraw() # Gets the font family for the editor. @@ -1366,7 +1374,6 @@ class Editor extends View currentLine = clearLine(currentLine) clearLine: (lineElement) => - @pixelLeftCache.delete(lineElement) next = lineElement.nextSibling @renderedLines[0].removeChild(lineElement) next @@ -1545,33 +1552,91 @@ class Editor extends View unless existingLineElement lineElement = @buildLineElementForScreenRow(actualRow) @renderedLines.append(lineElement) - left = @positionLeftForLineAndColumn(lineElement, column) + left = @positionLeftForLineAndColumn(lineElement, actualRow, column) unless existingLineElement @renderedLines[0].removeChild(lineElement) { top: row * @lineHeight, left } - positionLeftForLineAndColumn: (lineElement, column) -> - lineCache = @pixelLeftCache.get(lineElement) - @pixelLeftCache.set(lineElement, lineCache = {}) unless lineCache? + positionLeftForLineAndColumn: (lineElement, screenRow, column) -> + return 0 if column == 0 - return lineCache[column] if lineCache[column]? + bufferRow = @bufferRowsForScreenRows(screenRow)[0] ? screenRow + tokenizedLine = @activeEditSession.displayBuffer.tokenizedBuffer.tokenizedLines[bufferRow] - delta = 0 - iterator = document.createNodeIterator(lineElement, NodeFilter.SHOW_TEXT, acceptNode: -> NodeFilter.FILTER_ACCEPT) - while textNode = iterator.nextNode() - nextDelta = delta + textNode.textContent.length - if nextDelta >= column - offset = column - delta - break - delta = nextDelta + left = 0 + index = 0 + for token in tokenizedLine.tokens + for char in token.value + return left if index >= column - MEASURE_RANGE.setEnd(textNode, offset) - MEASURE_RANGE.collapse() - left = MEASURE_RANGE.getClientRects()[0].left - Math.floor(@scrollView.offset().left) + Math.floor(@scrollLeft()) + val = @checkCharacterWidthCache(token.scopes, char) + if val? + left += val + else + return @measureToColumn(lineElement, tokenizedLine, column) - lineCache[column] = left + index++ left + scopesForColumn: (tokenizedLine, column) -> + index = 0 + for token in tokenizedLine.tokens + for char in token.value + return token.scopes if index == column + index++ + null + + measureToColumn: (lineElement, tokenizedLine, column) -> + left = oldLeft = index = 0 + iterator = document.createNodeIterator(lineElement, NodeFilter.SHOW_TEXT, TEXT_NODE_FILTER) + + returnLeft = null + + while textNode = iterator.nextNode() + content = textNode.textContent + + for char, i in content + + # Dont return right away, finish caching the whole line + returnLeft = left if index == column + oldLeft = left + + scopes = @scopesForColumn(tokenizedLine, index) + cachedVal = @checkCharacterWidthCache(scopes, char) + + if cachedVal? + left = oldLeft + cachedVal + else + # i + 1 to measure to the end of the current character + MEASURE_RANGE.setEnd(textNode, i + 1) + MEASURE_RANGE.collapse() + left = MEASURE_RANGE.getClientRects()[0].left - Math.floor(@scrollView.offset().left) + Math.floor(@scrollLeft()) + + @setCharacterWidthCache(scopes, char, left - oldLeft) if scopes? + + index++ + + returnLeft ? left + + checkCharacterWidthCache: (scopes, char) -> + scopes ?= NO_SCOPE + obj = Editor.characterWidthCache + for scope in scopes + obj = obj[scope] + return null unless obj? + obj[char] + + setCharacterWidthCache: (scopes, char, val) -> + scopes ?= NO_SCOPE + obj = Editor.characterWidthCache + for scope in scopes + obj[scope] ?= {} + obj = obj[scope] + obj[char] = val + + clearCharacterWidthCache: -> + Editor.characterWidthCache = {} + pixelOffsetForScreenPosition: (position) -> {top, left} = @pixelPositionForScreenPosition(position) offset = @renderedLines.offset() From b4afc24ee89eaa64bd095d2c76a8229c79bd1d88 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Wed, 2 Oct 2013 18:34:12 -0700 Subject: [PATCH 29/87] Most of the tests work --- spec/editor-spec.coffee | 101 +++++++++++----------------------------- 1 file changed, 27 insertions(+), 74 deletions(-) diff --git a/spec/editor-spec.coffee b/spec/editor-spec.coffee index f6b6d8a59..1483a283b 100644 --- a/spec/editor-spec.coffee +++ b/spec/editor-spec.coffee @@ -1,7 +1,7 @@ {_, $, $$, fs, Editor, Range, RootView} = require 'atom' path = require 'path' -describe "Editor", -> +fdescribe "Editor", -> [buffer, editor, editSession, cachedLineHeight, cachedCharWidth] = [] beforeEach -> @@ -119,36 +119,37 @@ describe "Editor", -> it "updates the rendered lines, cursors, selections, scroll position, and event subscriptions to match the given edit session", -> editor.attachToDom(heightInLines: 5, widthInChars: 30) - editor.setCursorBufferPosition([3, 5]) + editor.setCursorBufferPosition([6, 13]) editor.scrollToBottom() editor.scrollLeft(150) previousScrollHeight = editor.verticalScrollbar.prop('scrollHeight') previousScrollTop = editor.scrollTop() previousScrollLeft = editor.scrollLeft() - newEditSession.setScrollTop(120) + newEditSession.setScrollTop(900) newEditSession.setSelectedBufferRange([[40, 0], [43, 1]]) editor.edit(newEditSession) { firstRenderedScreenRow, lastRenderedScreenRow } = editor expect(editor.lineElementForScreenRow(firstRenderedScreenRow).text()).toBe newBuffer.lineForRow(firstRenderedScreenRow) expect(editor.lineElementForScreenRow(lastRenderedScreenRow).text()).toBe newBuffer.lineForRow(editor.lastRenderedScreenRow) - expect(editor.scrollTop()).toBe 120 + expect(editor.scrollTop()).toBe 900 expect(editor.scrollLeft()).toBe 0 expect(editor.getSelectionView().regions[0].position().top).toBe 40 * editor.lineHeight editor.insertText("hello") expect(editor.lineElementForScreenRow(40).text()).toBe "hello3" editor.edit(editSession) + console.log editor.scrollTop(), editSession.getCursorScreenPosition() { firstRenderedScreenRow, lastRenderedScreenRow } = editor expect(editor.lineElementForScreenRow(firstRenderedScreenRow).text()).toBe buffer.lineForRow(firstRenderedScreenRow) expect(editor.lineElementForScreenRow(lastRenderedScreenRow).text()).toBe buffer.lineForRow(editor.lastRenderedScreenRow) expect(editor.verticalScrollbar.prop('scrollHeight')).toBe previousScrollHeight expect(editor.scrollTop()).toBe previousScrollTop expect(editor.scrollLeft()).toBe previousScrollLeft - expect(editor.getCursorView().position()).toEqual { top: 3 * editor.lineHeight, left: 5 * editor.charWidth } + expect(editor.getCursorView().position()).toEqual { top: 6 * editor.lineHeight, left: 13 * editor.charWidth } editor.insertText("goodbye") - expect(editor.lineElementForScreenRow(3).text()).toMatch /^ vgoodbyear/ + expect(editor.lineElementForScreenRow(6).text()).toMatch /^ currentgoodbye/ it "triggers alert if edit session's buffer goes into conflict with changes on disk", -> filePath = "/tmp/atom-changed-file.txt" @@ -904,7 +905,8 @@ describe "Editor", -> it "moves the hiddenInput to the same position with cursor's view", -> editor.setCursorScreenPosition(row: 2, column: 2) - expect(editor.getCursorView().offset()).toEqual(editor.hiddenInput.offset()) + expect(editor.getCursorView()[0].style.left).toEqual(editor.hiddenInput[0].style.left) + expect(editor.getCursorView()[0].style.top).toEqual(editor.hiddenInput[0].style.top) describe "when the editor is using a variable-width font", -> beforeEach -> @@ -915,6 +917,8 @@ describe "Editor", -> expect(editor.getCursorView().position()).toEqual {top: 3 * editor.lineHeight, left: 178} editor.setCursorBufferPosition([3, Infinity]) expect(editor.getCursorView().position()).toEqual {top: 3 * editor.lineHeight, left: 353} + console.log Editor.CHARACTER_WIDTH_CACHE + describe "autoscrolling", -> it "only autoscrolls when the last cursor is moved", -> @@ -1090,36 +1094,22 @@ describe "Editor", -> expect(span0.children('span:eq(0)')).toMatchSelector '.storage.modifier.js' expect(span0.children('span:eq(0)').text()).toBe 'var' - expect(span0.children('span:eq(1)')).toMatchSelector '.character' - expect(span0.children('span:eq(1)').text()).toBe " " + span0_1 = span0.children('span:eq(1)') + expect(span0_1).toMatchSelector '.meta.function.js' + expect(span0_1.text()).toBe 'quicksort = function ()' + expect(span0_1.children('span:eq(0)')).toMatchSelector '.entity.name.function.js' + expect(span0_1.children('span:eq(0)').text()).toBe "quicksort" + expect(span0_1.children('span:eq(1)')).toMatchSelector '.keyword.operator.js' + expect(span0_1.children('span:eq(1)').text()).toBe "=" + expect(span0_1.children('span:eq(2)')).toMatchSelector '.storage.type.function.js' + expect(span0_1.children('span:eq(2)').text()).toBe "function" + expect(span0_1.children('span:eq(3)')).toMatchSelector '.punctuation.definition.parameters.begin.js' + expect(span0_1.children('span:eq(3)').text()).toBe "(" + expect(span0_1.children('span:eq(4)')).toMatchSelector '.punctuation.definition.parameters.end.js' + expect(span0_1.children('span:eq(4)').text()).toBe ")" - span0_2 = span0.children('span:eq(2)') - console.log span0 - console.log span0_2[0] - expect(span0_2).toMatchSelector '.meta.function.js' - expect(span0_2.text()).toBe 'quicksort = function ()' - expect(span0_2.children('span:eq(0)')).toMatchSelector '.entity.name.function.js' - expect(span0_2.children('span:eq(0)').text()).toBe "quicksort" - expect(span0_2.children('span:eq(1)')).toMatchSelector '.character' - expect(span0_2.children('span:eq(1)').text()).toBe " " - expect(span0_2.children('span:eq(2)')).toMatchSelector '.keyword.operator.js' - expect(span0_2.children('span:eq(2)').text()).toBe "=" - expect(span0_2.children('span:eq(3)')).toMatchSelector '.character' - expect(span0_2.children('span:eq(3)').text()).toBe " " - expect(span0_2.children('span:eq(4)')).toMatchSelector '.storage.type.function.js' - expect(span0_2.children('span:eq(4)').text()).toBe "function" - expect(span0_2.children('span:eq(5)')).toMatchSelector '.character' - expect(span0_2.children('span:eq(5)').text()).toBe " " - expect(span0_2.children('span:eq(6)')).toMatchSelector '.punctuation.definition.parameters.begin.js' - expect(span0_2.children('span:eq(6)').text()).toBe "(" - expect(span0_2.children('span:eq(7)')).toMatchSelector '.punctuation.definition.parameters.end.js' - expect(span0_2.children('span:eq(7)').text()).toBe ")" - - expect(span0.children('span:eq(3)')).toMatchSelector '.character' - expect(span0.children('span:eq(3)').text()).toBe " " - - expect(span0.children('span:eq(4)')).toMatchSelector '.meta.brace.curly.js' - expect(span0.children('span:eq(4)').text()).toBe "{" + expect(span0.children('span:eq(2)')).toMatchSelector '.meta.brace.curly.js' + expect(span0.children('span:eq(2)').text()).toBe "{" line12 = editor.renderedLines.find('.line:eq(11)').children('span:eq(0)') expect(line12.children('span:eq(1)')).toMatchSelector '.keyword' @@ -1137,18 +1127,6 @@ describe "Editor", -> expect(span0_0).toMatchSelector '.leading-whitespace' expect(span0_0.text()).toBe ' ' - it "wraps every character in a span", -> - text = ' leading and no trailing whitespace' - editor.setText(text) - line0 = editor.renderedLines.find('.line:first') - characters = line0.find('.character') - - renderedText = '' - renderedText += $(ch).text() for ch in characters - - expect(characters).toHaveLength text.length - expect(renderedText).toEqual text - describe "when the line has trailing whitespace", -> it "wraps trailing whitespace in a span", -> editor.setText('trailing whitespace -> ') @@ -1157,18 +1135,6 @@ describe "Editor", -> expect(span0_last).toMatchSelector '.trailing-whitespace' expect(span0_last.text()).toBe ' ' - it "wraps every character in a span", -> - text = ' leading and trailing whitespace ' - editor.setText(text) - line0 = editor.renderedLines.find('.line:first') - characters = line0.find('.character') - - renderedText = '' - renderedText += $(ch).text() for ch in characters - - expect(characters).toHaveLength text.length - expect(renderedText).toEqual text - describe "when lines are updated in the buffer", -> it "syntax highlights the updated lines", -> expect(editor.renderedLines.find('.line:eq(0) > span:first > span:first')).toMatchSelector '.storage.modifier.js' @@ -1515,7 +1481,7 @@ describe "Editor", -> editor.setShowInvisibles(true) editor.attachToDom() editor.setText "var" - expect(editor.find('.line').html()).toBe 'var¬' + expect(editor.find('.line').html()).toBe 'var¬' it "allows invisible glyphs to be customized via config.editor.invisibles", -> editor.setText(" \t ") @@ -2214,19 +2180,6 @@ describe "Editor", -> expect(editor.pixelPositionForBufferPosition([2,8])).toEqual top: 40, left: 80 - it "breaks left position cache when line is changed", -> - editor.renderedLines.css('font-size', '16px') - expect(editor.pixelPositionForBufferPosition([2,8])).toEqual top: 40, left: 80 - - editor.setCursorBufferPosition([2, 8]) - editor.insertText("a") - - # make characters smaller - editor.renderedLines.css('font-size', '15px') - - expect(editor.pixelPositionForBufferPosition([2,8])).toEqual top: 40, left: 72 - - describe "when clicking in the gutter", -> beforeEach -> editor.attachToDom() From f1cf8496f81301a2132905412935362484c5c730 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Thu, 3 Oct 2013 11:53:34 -0700 Subject: [PATCH 30/87] Fix test, use bufferRowsFOrScreenRowsProperly --- spec/editor-spec.coffee | 2 +- src/editor.coffee | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/editor-spec.coffee b/spec/editor-spec.coffee index 1483a283b..f799f5b5f 100644 --- a/spec/editor-spec.coffee +++ b/spec/editor-spec.coffee @@ -1,7 +1,7 @@ {_, $, $$, fs, Editor, Range, RootView} = require 'atom' path = require 'path' -fdescribe "Editor", -> +describe "Editor", -> [buffer, editor, editSession, cachedLineHeight, cachedCharWidth] = [] beforeEach -> diff --git a/src/editor.coffee b/src/editor.coffee index b67474591..0455c1b7b 100644 --- a/src/editor.coffee +++ b/src/editor.coffee @@ -1560,7 +1560,7 @@ class Editor extends View positionLeftForLineAndColumn: (lineElement, screenRow, column) -> return 0 if column == 0 - bufferRow = @bufferRowsForScreenRows(screenRow)[0] ? screenRow + bufferRow = @bufferRowsForScreenRows(screenRow, screenRow)[0] ? screenRow tokenizedLine = @activeEditSession.displayBuffer.tokenizedBuffer.tokenizedLines[bufferRow] left = 0 From 5516dadffe9e0e38698e7910e017fd506512d869 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Thu, 3 Oct 2013 12:05:32 -0700 Subject: [PATCH 31/87] Remove selective gutter rendering. It is faster but incorrect in the case of newlines. It will have to be rethought. It can be a different unit of work --- src/gutter.coffee | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/src/gutter.coffee b/src/gutter.coffee index 9f56b19cf..6db451d25 100644 --- a/src/gutter.coffee +++ b/src/gutter.coffee @@ -78,22 +78,13 @@ class Gutter extends View @renderLineNumbers(intactRanges, renderFrom, renderTo) if performUpdate renderLineNumbers: (intactRanges, startScreenRow, endScreenRow) -> - editor = @getEditor() - - editor.clearDirtyRanges(intactRanges, @lineNumbers[0], @clearLine) - editor.fillDirtyRanges(intactRanges, startScreenRow, endScreenRow, @lineNumbers[0], @buildLineElements) - + @lineNumbers[0].innerHTML = @buildLineElementsHtml(startScreenRow, endScreenRow) @firstScreenRow = startScreenRow @lastScreenRow = endScreenRow @highlightedRows = null @highlightLines() - clearLine: (lineElement) => - next = lineElement.nextSibling - @lineNumbers[0].removeChild(lineElement) - next - - buildLineElements: (startScreenRow, endScreenRow) => + buildLineElementsHtml: (startScreenRow, endScreenRow) => editor = @getEditor() maxDigits = editor.getLineCount().toString().length rows = editor.bufferRowsForScreenRows(startScreenRow, endScreenRow) @@ -114,9 +105,7 @@ class Gutter extends View lastScreenRow = row - div = document.createElement('div') - div.innerHTML = html - new Array(div.children...) + html removeLineHighlights: -> return unless @highlightedLineNumbers From 407ccc4819e0e4dc1085a13c7adb7192703d2b0d Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Thu, 3 Oct 2013 12:22:14 -0700 Subject: [PATCH 32/87] Get a few benchmarks in there --- benchmark/benchmark-suite.coffee | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/benchmark/benchmark-suite.coffee b/benchmark/benchmark-suite.coffee index 7857da039..e96c88bad 100644 --- a/benchmark/benchmark-suite.coffee +++ b/benchmark/benchmark-suite.coffee @@ -59,19 +59,34 @@ describe "editor.", -> editor.insertText('"') editor.backspace() - fdescribe "calculating-pixel-position.", -> + describe "positionLeftForLineAndColumn.", -> line = null beforeEach -> editor.scrollTop(2000) editor.resetDisplay() line = editor.lineElementForScreenRow(106)[0] - benchmark "positionLeftForLineAndColumn", 20000, -> - editor.positionLeftForLineAndColumn(line, 82) - editor.pixelLeftCache.delete(line) + describe "one-line.", -> + beforeEach -> + editor.clearCharacterWidthCache() - benchmark "positionLeftForLineAndColumn.cached", 20000, -> - editor.positionLeftForLineAndColumn(line, 82) + benchmark "uncached", 5000, -> + editor.positionLeftForLineAndColumn(line, 106, 82) + editor.clearCharacterWidthCache() + + benchmark "cached", 5000, -> + editor.positionLeftForLineAndColumn(line, 106, 82) + + describe "multiple-lines.", -> + [firstRow, lastRow] = [] + beforeEach -> + firstRow = editor.getFirstVisibleScreenRow() + lastRow = editor.getLastVisibleScreenRow() + + benchmark "cache-entire-visible-area", 100, -> + for i in [firstRow..lastRow] + line = editor.lineElementForScreenRow(i)[0] + editor.positionLeftForLineAndColumn(line, i, Math.max(0, editor.lineLengthForBufferRow(i))) describe "text-rendering.", -> beforeEach -> From 156b6a9490cc5699915119e7d9b87e614958c197 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Thu, 3 Oct 2013 13:32:18 -0700 Subject: [PATCH 33/87] Make wrapCharacters -> escapeString. Use a regex. The `htmlForScreenRows` benchmark went from ~3.5 ms to ~2.9ms for a call to htmlForScreenRows over the entire screen range. :racehorse: --- benchmark/benchmark-suite.coffee | 2 +- src/token.coffee | 38 ++++++++++++++++---------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/benchmark/benchmark-suite.coffee b/benchmark/benchmark-suite.coffee index e96c88bad..e5cbeee29 100644 --- a/benchmark/benchmark-suite.coffee +++ b/benchmark/benchmark-suite.coffee @@ -95,7 +95,7 @@ describe "editor.", -> benchmark "resetDisplay", 50, -> editor.resetDisplay() - benchmark "htmlForScreenRows", 50, -> + benchmark "htmlForScreenRows", 1000, -> lastRow = editor.getLastScreenRow() editor.htmlForScreenRows(0, lastRow) diff --git a/src/token.coffee b/src/token.coffee index 96d6b26c4..c6e440db6 100644 --- a/src/token.coffee +++ b/src/token.coffee @@ -4,6 +4,7 @@ textUtils = require './text-utils' whitespaceRegexesByTabLength = {} LEADING_WHITESPACE_REGEX = /^[ ]+/ TRAILING_WHITESPACE_REGEX = /[ ]+$/ +EscapeRegex = /[&"'<>]/g # Private: Represents a single unit of text as selected by a grammar. module.exports = @@ -132,7 +133,7 @@ class Token classes += ' invisible-character' if invisibles.tab html = html.replace /^./, (match) => match = invisibles.tab ? match - "#{@wrapCharacters(match)}" + "#{@escapeString(match)}" else startIndex = 0 endIndex = html.length @@ -146,7 +147,7 @@ class Token classes += ' invisible-character' if invisibles.space match[0] = match[0].replace(/./g, invisibles.space) if invisibles.space - leadingHtml = "#{@wrapCharacters(match[0])}" + leadingHtml = "#{@escapeString(match[0])}" startIndex = match[0].length @@ -156,29 +157,28 @@ class Token classes += ' invisible-character' if invisibles.space match[0] = match[0].replace(/./g, invisibles.space) if invisibles.space - trailingHtml = "#{@wrapCharacters(match[0])}" + trailingHtml = "#{@escapeString(match[0])}" endIndex = match.index - html = leadingHtml + @wrapCharacters(html, startIndex, endIndex) + trailingHtml + html = leadingHtml + @escapeString(html, startIndex, endIndex) + trailingHtml html - wrapCharacters: (str, startIndex, endIndex) -> + escapeString: (str, startIndex, endIndex) -> + strLength = str.length + startIndex ?= 0 - endIndex ?= str.length + endIndex ?= strLength - ret = '' + str = str.slice(startIndex, endIndex) if startIndex > 0 or endIndex < strLength + str.replace(EscapeRegex, @escapeStringReplace) - for i in [startIndex...endIndex] - character = switch str[i] - when '&' then '&' - when '"' then '"' - when "'" then ''' - when '<' then '<' - when '>' then '>' - else str[i] - - ret += character - - ret + escapeStringReplace: (match) -> + switch match + when '&' then '&' + when '"' then '"' + when "'" then ''' + when '<' then '<' + when '>' then '>' + else match From 1a6884ff711bf9181014934fcba17b0fe652ac2a Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Thu, 3 Oct 2013 13:34:47 -0700 Subject: [PATCH 34/87] Remove the ALL_CAPS vars replace with CapCamelCase --- src/token.coffee | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/token.coffee b/src/token.coffee index c6e440db6..ad2ece720 100644 --- a/src/token.coffee +++ b/src/token.coffee @@ -1,9 +1,9 @@ _ = require './underscore-extensions' textUtils = require './text-utils' -whitespaceRegexesByTabLength = {} -LEADING_WHITESPACE_REGEX = /^[ ]+/ -TRAILING_WHITESPACE_REGEX = /[ ]+$/ +WhitespaceRegexesByTabLength = {} +LeadingWhitespaceRegex = /^[ ]+/ +TrailingWhitespaceRegex = /[ ]+$/ EscapeRegex = /[&"'<>]/g # Private: Represents a single unit of text as selected by a grammar. @@ -36,7 +36,7 @@ class Token [new Token(value: value1, scopes: @scopes), new Token(value: value2, scopes: @scopes)] whitespaceRegexForTabLength: (tabLength) -> - whitespaceRegexesByTabLength[tabLength] ?= new RegExp("([ ]{#{tabLength}})|(\t)|([^\t]+)", "g") + WhitespaceRegexesByTabLength[tabLength] ?= new RegExp("([ ]{#{tabLength}})|(\t)|([^\t]+)", "g") breakOutAtomicTokens: (tabLength, breakOutLeadingWhitespace) -> if @hasSurrogatePair @@ -141,7 +141,7 @@ class Token leadingHtml = '' trailingHtml = '' - if hasLeadingWhitespace and match = LEADING_WHITESPACE_REGEX.exec(html) + if hasLeadingWhitespace and match = LeadingWhitespaceRegex.exec(html) classes = 'leading-whitespace' classes += ' indent-guide' if hasIndentGuide classes += ' invisible-character' if invisibles.space @@ -151,7 +151,7 @@ class Token startIndex = match[0].length - if hasTrailingWhitespace and match = TRAILING_WHITESPACE_REGEX.exec(html) + if hasTrailingWhitespace and match = TrailingWhitespaceRegex.exec(html) classes = 'trailing-whitespace' classes += ' indent-guide' if hasIndentGuide and not hasLeadingWhitespace classes += ' invisible-character' if invisibles.space From bd8c14355c8495457eb213838eb0484c3c8f5a20 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Thu, 3 Oct 2013 13:40:43 -0700 Subject: [PATCH 35/87] Move regexes out into variables --- benchmark/benchmark-suite.coffee | 2 +- src/token.coffee | 18 +++++++++++------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/benchmark/benchmark-suite.coffee b/benchmark/benchmark-suite.coffee index e5cbeee29..2ef442241 100644 --- a/benchmark/benchmark-suite.coffee +++ b/benchmark/benchmark-suite.coffee @@ -95,7 +95,7 @@ describe "editor.", -> benchmark "resetDisplay", 50, -> editor.resetDisplay() - benchmark "htmlForScreenRows", 1000, -> + fbenchmark "htmlForScreenRows", 1000, -> lastRow = editor.getLastScreenRow() editor.htmlForScreenRows(0, lastRow) diff --git a/src/token.coffee b/src/token.coffee index ad2ece720..81b5b2556 100644 --- a/src/token.coffee +++ b/src/token.coffee @@ -5,6 +5,10 @@ WhitespaceRegexesByTabLength = {} LeadingWhitespaceRegex = /^[ ]+/ TrailingWhitespaceRegex = /[ ]+$/ EscapeRegex = /[&"'<>]/g +CharacterRegex = /./g +StartCharacterRegex = /^./ +StartDotRegex = /^\.?/ +WhitespaceRegex = /\S/ # Private: Represents a single unit of text as selected by a grammar. module.exports = @@ -115,10 +119,10 @@ class Token ) isOnlyWhitespace: -> - not /\S/.test(@value) + not WhitespaceRegex.test(@value) matchesScopeSelector: (selector) -> - targetClasses = selector.replace(/^\.?/, '').split('.') + targetClasses = selector.replace(StartDotRegex, '').split('.') _.any @scopes, (scope) -> scopeClasses = scope.split('.') _.isSubset(targetClasses, scopeClasses) @@ -131,7 +135,7 @@ class Token classes = 'hard-tab' classes += ' indent-guide' if hasIndentGuide classes += ' invisible-character' if invisibles.tab - html = html.replace /^./, (match) => + html = html.replace StartCharacterRegex, (match) => match = invisibles.tab ? match "#{@escapeString(match)}" else @@ -146,8 +150,8 @@ class Token classes += ' indent-guide' if hasIndentGuide classes += ' invisible-character' if invisibles.space - match[0] = match[0].replace(/./g, invisibles.space) if invisibles.space - leadingHtml = "#{@escapeString(match[0])}" + match[0] = match[0].replace(CharacterRegex, invisibles.space) if invisibles.space + leadingHtml = "#{match[0]}" startIndex = match[0].length @@ -156,8 +160,8 @@ class Token classes += ' indent-guide' if hasIndentGuide and not hasLeadingWhitespace classes += ' invisible-character' if invisibles.space - match[0] = match[0].replace(/./g, invisibles.space) if invisibles.space - trailingHtml = "#{@escapeString(match[0])}" + match[0] = match[0].replace(CharacterRegex, invisibles.space) if invisibles.space + trailingHtml = "#{match[0]}" endIndex = match.index From a50e948a90f2fbce063a39ee1838c8044e8cc2a8 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Thu, 3 Oct 2013 13:42:04 -0700 Subject: [PATCH 36/87] Remove log lines --- spec/editor-spec.coffee | 2 -- 1 file changed, 2 deletions(-) diff --git a/spec/editor-spec.coffee b/spec/editor-spec.coffee index f799f5b5f..9da9b1b3b 100644 --- a/spec/editor-spec.coffee +++ b/spec/editor-spec.coffee @@ -140,7 +140,6 @@ describe "Editor", -> expect(editor.lineElementForScreenRow(40).text()).toBe "hello3" editor.edit(editSession) - console.log editor.scrollTop(), editSession.getCursorScreenPosition() { firstRenderedScreenRow, lastRenderedScreenRow } = editor expect(editor.lineElementForScreenRow(firstRenderedScreenRow).text()).toBe buffer.lineForRow(firstRenderedScreenRow) expect(editor.lineElementForScreenRow(lastRenderedScreenRow).text()).toBe buffer.lineForRow(editor.lastRenderedScreenRow) @@ -917,7 +916,6 @@ describe "Editor", -> expect(editor.getCursorView().position()).toEqual {top: 3 * editor.lineHeight, left: 178} editor.setCursorBufferPosition([3, Infinity]) expect(editor.getCursorView().position()).toEqual {top: 3 * editor.lineHeight, left: 353} - console.log Editor.CHARACTER_WIDTH_CACHE describe "autoscrolling", -> From 26a3a77fedb88d3f2b2052f34b4587b70e696779 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Thu, 3 Oct 2013 13:44:38 -0700 Subject: [PATCH 37/87] Rename ALL_CAPS vars to CapCamelCase --- src/editor.coffee | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/editor.coffee b/src/editor.coffee index 0455c1b7b..4de449097 100644 --- a/src/editor.coffee +++ b/src/editor.coffee @@ -9,9 +9,9 @@ fsUtils = require './fs-utils' $ = require './jquery-extensions' _ = require './underscore-extensions' -MEASURE_RANGE = document.createRange() -TEXT_NODE_FILTER = { acceptNode: -> NodeFilter.FILTER_ACCEPT } -NO_SCOPE = ['no-scope'] +MeasureRange = document.createRange() +TextNodeFileter = { acceptNode: -> NodeFilter.FILTER_ACCEPT } +NoScope = ['no-scope'] # Private: Represents the entire visual pane in Atom. # @@ -1588,7 +1588,7 @@ class Editor extends View measureToColumn: (lineElement, tokenizedLine, column) -> left = oldLeft = index = 0 - iterator = document.createNodeIterator(lineElement, NodeFilter.SHOW_TEXT, TEXT_NODE_FILTER) + iterator = document.createNodeIterator(lineElement, NodeFilter.SHOW_TEXT, TextNodeFileter) returnLeft = null @@ -1608,9 +1608,9 @@ class Editor extends View left = oldLeft + cachedVal else # i + 1 to measure to the end of the current character - MEASURE_RANGE.setEnd(textNode, i + 1) - MEASURE_RANGE.collapse() - left = MEASURE_RANGE.getClientRects()[0].left - Math.floor(@scrollView.offset().left) + Math.floor(@scrollLeft()) + MeasureRange.setEnd(textNode, i + 1) + MeasureRange.collapse() + left = MeasureRange.getClientRects()[0].left - Math.floor(@scrollView.offset().left) + Math.floor(@scrollLeft()) @setCharacterWidthCache(scopes, char, left - oldLeft) if scopes? @@ -1619,7 +1619,7 @@ class Editor extends View returnLeft ? left checkCharacterWidthCache: (scopes, char) -> - scopes ?= NO_SCOPE + scopes ?= NoScope obj = Editor.characterWidthCache for scope in scopes obj = obj[scope] @@ -1627,7 +1627,7 @@ class Editor extends View obj[char] setCharacterWidthCache: (scopes, char, val) -> - scopes ?= NO_SCOPE + scopes ?= NoScope obj = Editor.characterWidthCache for scope in scopes obj[scope] ?= {} From 5942f2997ffb95170c173ad672c75e438bda019c Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Thu, 3 Oct 2013 13:53:53 -0700 Subject: [PATCH 38/87] Remove array joins in buildEmptyLineHtml --- src/editor.coffee | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/editor.coffee b/src/editor.coffee index 4de449097..da764135f 100644 --- a/src/editor.coffee +++ b/src/editor.coffee @@ -1764,21 +1764,21 @@ class Editor extends View if not mini and showIndentGuide if indentation > 0 tabLength = activeEditSession.getTabLength() - indentGuideHtml = [] + indentGuideHtml = '' for level in [0...indentation] - indentLevelHtml = [""] + indentLevelHtml = "" for characterPosition in [0...tabLength] if invisible = eolInvisibles.shift() - indentLevelHtml.push("#{invisible}") + indentLevelHtml += "#{invisible}" else - indentLevelHtml.push(' ') - indentLevelHtml.push("") - indentGuideHtml.push(indentLevelHtml.join('')) + indentLevelHtml += ' ' + indentLevelHtml += "" + indentGuideHtml += indentLevelHtml for invisible in eolInvisibles - indentGuideHtml.push("#{invisible}") + indentGuideHtml += "#{invisible}" - return indentGuideHtml.join('') + return indentGuideHtml if htmlEolInvisibles.length > 0 htmlEolInvisibles From 4f1bdee2d7a867e47cceaa5511a587a6669c9060 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Thu, 3 Oct 2013 13:57:08 -0700 Subject: [PATCH 39/87] shift() multiple little arrays. Avoid. --- src/editor.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/editor.coffee b/src/editor.coffee index da764135f..5d9c40cea 100644 --- a/src/editor.coffee +++ b/src/editor.coffee @@ -1761,6 +1761,7 @@ class Editor extends View line.push("") @buildEmptyLineHtml: (showIndentGuide, eolInvisibles, htmlEolInvisibles, indentation, activeEditSession, mini) -> + indentCharIndex = 0 if not mini and showIndentGuide if indentation > 0 tabLength = activeEditSession.getTabLength() @@ -1768,7 +1769,7 @@ class Editor extends View for level in [0...indentation] indentLevelHtml = "" for characterPosition in [0...tabLength] - if invisible = eolInvisibles.shift() + if invisible = eolInvisibles[indentCharIndex++] indentLevelHtml += "#{invisible}" else indentLevelHtml += ' ' From 87687036cb1922c005c69d966871fa30375a4fff Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Thu, 3 Oct 2013 14:12:41 -0700 Subject: [PATCH 40/87] Add benchmark for empty() vs innerHTML = '' --- benchmark/benchmark-suite.coffee | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/benchmark/benchmark-suite.coffee b/benchmark/benchmark-suite.coffee index 2ef442241..2b8bec8ba 100644 --- a/benchmark/benchmark-suite.coffee +++ b/benchmark/benchmark-suite.coffee @@ -59,6 +59,23 @@ describe "editor.", -> editor.insertText('"') editor.backspace() + describe "empty-vs-set-innerHTML.", -> + [firstRow, lastRow] = [] + beforeEach -> + firstRow = editor.getFirstVisibleScreenRow() + lastRow = editor.getLastVisibleScreenRow() + + benchmark "build-gutter-html.", 1000, -> + editor.gutter.renderLineNumbers(null, firstRow, lastRow) + + benchmark "set-innerHTML.", 1000, -> + editor.gutter.renderLineNumbers(null, firstRow, lastRow) + editor.gutter.lineNumbers[0].innerHtml = '' + + benchmark "empty.", 1000, -> + editor.gutter.renderLineNumbers(null, firstRow, lastRow) + editor.gutter.lineNumbers.empty() + describe "positionLeftForLineAndColumn.", -> line = null beforeEach -> @@ -95,7 +112,7 @@ describe "editor.", -> benchmark "resetDisplay", 50, -> editor.resetDisplay() - fbenchmark "htmlForScreenRows", 1000, -> + benchmark "htmlForScreenRows", 1000, -> lastRow = editor.getLastScreenRow() editor.htmlForScreenRows(0, lastRow) From 04cdad680b2916f517b80ab02c163aa597a290dd Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Thu, 3 Oct 2013 14:27:07 -0700 Subject: [PATCH 41/87] Remove ability to pass clearDirtyRanges and fillDirtyRanges extra vars We don't need them anymore in the gutter. --- src/editor.coffee | 30 ++++++++++-------------------- src/gutter.coffee | 6 +++--- 2 files changed, 13 insertions(+), 23 deletions(-) diff --git a/src/editor.coffee b/src/editor.coffee index 5d9c40cea..0fd1bb85e 100644 --- a/src/editor.coffee +++ b/src/editor.coffee @@ -1270,7 +1270,7 @@ class Editor extends View changes = @pendingChanges intactRanges = @computeIntactRanges(renderFrom, renderTo) - @gutter.updateLineNumbers(changes, intactRanges, renderFrom, renderTo) + @gutter.updateLineNumbers(changes, renderFrom, renderTo) @clearDirtyRanges(intactRanges) @fillDirtyRanges(intactRanges, renderFrom, renderTo) @@ -1353,40 +1353,30 @@ class Editor extends View i++ intactRanges.sort (a, b) -> a.domStart - b.domStart - # renderedLines - optional - # clearLine - optional - clearDirtyRanges: (intactRanges, renderedLines, clearLine) -> - renderedLines ?= @renderedLines[0] - clearLine ?= @clearLine - + clearDirtyRanges: (intactRanges) -> if intactRanges.length == 0 - renderedLines.innerHTML = '' - else if currentLine = renderedLines.firstChild + @renderedLines[0].innerHTML = '' + else if currentLine = @renderedLines[0].firstChild domPosition = 0 for intactRange in intactRanges while intactRange.domStart > domPosition - currentLine = clearLine(currentLine) + currentLine = @clearLine(currentLine) domPosition++ for i in [intactRange.start..intactRange.end] currentLine = currentLine.nextSibling domPosition++ while currentLine - currentLine = clearLine(currentLine) + currentLine = @clearLine(currentLine) clearLine: (lineElement) => next = lineElement.nextSibling @renderedLines[0].removeChild(lineElement) next - # renderedLines - optional - # buildLineElements - optional - fillDirtyRanges: (intactRanges, renderFrom, renderTo, renderedLines, buildLineElements) -> - renderedLines ?= @renderedLines[0] - buildLineElements ?= @buildLineElementsForScreenRows - + fillDirtyRanges: (intactRanges, renderFrom, renderTo) -> i = 0 nextIntact = intactRanges[i] - currentLine = renderedLines.firstChild + currentLine = @renderedLines[0].firstChild row = renderFrom while row <= renderTo @@ -1399,8 +1389,8 @@ class Editor extends View else dirtyRangeEnd = renderTo - for lineElement in buildLineElements(row, dirtyRangeEnd) - renderedLines.insertBefore(lineElement, currentLine) + for lineElement in @buildLineElementsForScreenRows(row, dirtyRangeEnd) + @renderedLines[0].insertBefore(lineElement, currentLine) row++ else currentLine = currentLine.nextSibling diff --git a/src/gutter.coffee b/src/gutter.coffee index 6db451d25..0fb2ae9a3 100644 --- a/src/gutter.coffee +++ b/src/gutter.coffee @@ -64,7 +64,7 @@ class Gutter extends View ### Internal ### - updateLineNumbers: (changes, intactRanges, renderFrom, renderTo) -> + updateLineNumbers: (changes, renderFrom, renderTo) -> if renderFrom < @firstScreenRow or renderTo > @lastScreenRow performUpdate = true else if @getEditor().getLastScreenRow() < @lastScreenRow @@ -75,9 +75,9 @@ class Gutter extends View performUpdate = true break - @renderLineNumbers(intactRanges, renderFrom, renderTo) if performUpdate + @renderLineNumbers(renderFrom, renderTo) if performUpdate - renderLineNumbers: (intactRanges, startScreenRow, endScreenRow) -> + renderLineNumbers: (startScreenRow, endScreenRow) -> @lineNumbers[0].innerHTML = @buildLineElementsHtml(startScreenRow, endScreenRow) @firstScreenRow = startScreenRow @lastScreenRow = endScreenRow From 4df546a19ee7be56cde5241156aadfc283d21b71 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Thu, 3 Oct 2013 14:30:21 -0700 Subject: [PATCH 42/87] Remove shoulds --- src/editor.coffee | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/editor.coffee b/src/editor.coffee index 0fd1bb85e..a8b5275cb 100644 --- a/src/editor.coffee +++ b/src/editor.coffee @@ -1215,8 +1215,7 @@ class Editor extends View return false unless cursorView.needsUpdate pos = cursorView.getScreenPosition() - should = pos.row >= @firstRenderedScreenRow and pos.row <= @lastRenderedScreenRow - should + pos.row >= @firstRenderedScreenRow and pos.row <= @lastRenderedScreenRow updateSelectionViews: -> if @newSelections.length > 0 @@ -1233,8 +1232,7 @@ class Editor extends View screenRange = selectionView.getScreenRange() startRow = screenRange.start.row endRow = screenRange.end.row - should = (startRow >= @firstRenderedScreenRow and startRow <= @lastRenderedScreenRow) or (endRow >= @firstRenderedScreenRow and endRow <= @lastRenderedScreenRow) - should + (startRow >= @firstRenderedScreenRow and startRow <= @lastRenderedScreenRow) or (endRow >= @firstRenderedScreenRow and endRow <= @lastRenderedScreenRow) syncCursorAnimations: -> for cursorView in @getCursorViews() From 98928dd99f7fed87988408be11381888a3a3a2be Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Thu, 3 Oct 2013 15:21:18 -0700 Subject: [PATCH 43/87] Upgrade to bracket-matcher@0.6.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 71c7d3f3d..14bf6f9dd 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "autocomplete": "0.6.0", "autoflow": "0.3.0", "bookmarks": "0.4.0", - "bracket-matcher": "0.5.0", + "bracket-matcher": "0.6.0", "collaboration": "0.20.0", "command-logger": "0.4.0", "command-palette": "0.4.0", From 89b0e4d15967cbbd0474def3c7ca373657828563 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Thu, 3 Oct 2013 16:28:21 -0700 Subject: [PATCH 44/87] Fix issue when there are no client rects --- src/editor.coffee | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/editor.coffee b/src/editor.coffee index a8b5275cb..eef1825b5 100644 --- a/src/editor.coffee +++ b/src/editor.coffee @@ -1598,7 +1598,9 @@ class Editor extends View # i + 1 to measure to the end of the current character MeasureRange.setEnd(textNode, i + 1) MeasureRange.collapse() - left = MeasureRange.getClientRects()[0].left - Math.floor(@scrollView.offset().left) + Math.floor(@scrollLeft()) + rects = MeasureRange.getClientRects() + return 0 if rects.length == 0 + left = rects[0].left - Math.floor(@scrollView.offset().left) + Math.floor(@scrollLeft()) @setCharacterWidthCache(scopes, char, left - oldLeft) if scopes? From 7f9ce094c766835b7fe79f44819c253f9dcaf629 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Thu, 3 Oct 2013 17:50:23 -0700 Subject: [PATCH 45/87] Add only window import so the benchmarks run --- benchmark/benchmark-bootstrap.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/benchmark/benchmark-bootstrap.coffee b/benchmark/benchmark-bootstrap.coffee index 7bf2a1cbe..04d450407 100644 --- a/benchmark/benchmark-bootstrap.coffee +++ b/benchmark/benchmark-bootstrap.coffee @@ -1,6 +1,6 @@ require '../src/window' -require '../src/atom' -require 'atom' +Atom = require '../src/atom' +window.atom = new Atom() {runSpecSuite} = require '../spec/jasmine-helper' From 785c71daf8874604e828cf5d5bcc61d63788e50c Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Thu, 3 Oct 2013 17:51:25 -0700 Subject: [PATCH 46/87] :lipstick: --- spec/editor-spec.coffee | 2 -- 1 file changed, 2 deletions(-) diff --git a/spec/editor-spec.coffee b/spec/editor-spec.coffee index 9da9b1b3b..e407048cb 100644 --- a/spec/editor-spec.coffee +++ b/spec/editor-spec.coffee @@ -917,7 +917,6 @@ describe "Editor", -> editor.setCursorBufferPosition([3, Infinity]) expect(editor.getCursorView().position()).toEqual {top: 3 * editor.lineHeight, left: 353} - describe "autoscrolling", -> it "only autoscrolls when the last cursor is moved", -> editor.setCursorBufferPosition([11,0]) @@ -1881,7 +1880,6 @@ describe "Editor", -> # doesn't allow regular editors to set grammars expect(-> editor.setGrammar()).toThrow() - describe "when config.editor.showLineNumbers is false", -> it "doesn't render any line numbers", -> expect(editor.gutter.lineNumbers).toBeVisible() From b3324b49b09ef961e02e14d0dfec830cf48e6fdc Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Thu, 3 Oct 2013 17:52:53 -0700 Subject: [PATCH 47/87] Skinny arrows :lipstick: --- src/editor.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/editor.coffee b/src/editor.coffee index eef1825b5..8865ff0d7 100644 --- a/src/editor.coffee +++ b/src/editor.coffee @@ -1366,7 +1366,7 @@ class Editor extends View while currentLine currentLine = @clearLine(currentLine) - clearLine: (lineElement) => + clearLine: (lineElement) -> next = lineElement.nextSibling @renderedLines[0].removeChild(lineElement) next @@ -1439,7 +1439,7 @@ class Editor extends View buildLineElementForScreenRow: (screenRow) -> @buildLineElementsForScreenRows(screenRow, screenRow)[0] - buildLineElementsForScreenRows: (startRow, endRow) => + buildLineElementsForScreenRows: (startRow, endRow) -> div = document.createElement('div') div.innerHTML = @htmlForScreenRows(startRow, endRow) new Array(div.children...) From 8825be937e02675c888c5f15f6131afd2cb62234 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Thu, 3 Oct 2013 17:54:30 -0700 Subject: [PATCH 48/87] TextNodeFielter -> TextNodeFilter --- src/editor.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/editor.coffee b/src/editor.coffee index 8865ff0d7..62e65f5fa 100644 --- a/src/editor.coffee +++ b/src/editor.coffee @@ -10,7 +10,7 @@ $ = require './jquery-extensions' _ = require './underscore-extensions' MeasureRange = document.createRange() -TextNodeFileter = { acceptNode: -> NodeFilter.FILTER_ACCEPT } +TextNodeFilter = { acceptNode: -> NodeFilter.FILTER_ACCEPT } NoScope = ['no-scope'] # Private: Represents the entire visual pane in Atom. @@ -1576,7 +1576,7 @@ class Editor extends View measureToColumn: (lineElement, tokenizedLine, column) -> left = oldLeft = index = 0 - iterator = document.createNodeIterator(lineElement, NodeFilter.SHOW_TEXT, TextNodeFileter) + iterator = document.createNodeIterator(lineElement, NodeFilter.SHOW_TEXT, TextNodeFilter) returnLeft = null From 3aae7bb77efbcdb10a7c5160525f17d857fc6d34 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Thu, 3 Oct 2013 17:55:58 -0700 Subject: [PATCH 49/87] :lipstick: --- src/editor.coffee | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/editor.coffee b/src/editor.coffee index 62e65f5fa..4ced66eea 100644 --- a/src/editor.coffee +++ b/src/editor.coffee @@ -1557,7 +1557,7 @@ class Editor extends View for char in token.value return left if index >= column - val = @checkCharacterWidthCache(token.scopes, char) + val = @getCharacterWidthCache(token.scopes, char) if val? left += val else @@ -1590,7 +1590,7 @@ class Editor extends View oldLeft = left scopes = @scopesForColumn(tokenizedLine, index) - cachedVal = @checkCharacterWidthCache(scopes, char) + cachedVal = @getCharacterWidthCache(scopes, char) if cachedVal? left = oldLeft + cachedVal @@ -1608,7 +1608,7 @@ class Editor extends View returnLeft ? left - checkCharacterWidthCache: (scopes, char) -> + getCharacterWidthCache: (scopes, char) -> scopes ?= NoScope obj = Editor.characterWidthCache for scope in scopes @@ -1746,7 +1746,7 @@ class Editor extends View scopeStack.push(scope) line.push("") - @popScope: (line, scopeStack)-> + @popScope: (line, scopeStack) -> scopeStack.pop() line.push("") From 26ab49306e83585995f75832eb47c4a5f22af7b9 Mon Sep 17 00:00:00 2001 From: Matt Colyer Date: Mon, 7 Oct 2013 11:48:20 -0700 Subject: [PATCH 50/87] Bump release notes version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 14bf6f9dd..3254a0d4a 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "markdown-preview": "0.6.0", "metrics": "0.8.0", "package-generator": "0.10.0", - "release-notes": "0.2.0", + "release-notes": "0.3.0", "settings-view": "0.27.0", "snippets": "0.6.0", "spell-check": "0.6.0", From 9f7b430c02b907d9b0fcdc0df9a6fc4eaff9bcf5 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 7 Oct 2013 13:30:07 -0700 Subject: [PATCH 51/87] Show window in benchmark bootstrap --- benchmark/benchmark-bootstrap.coffee | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/benchmark/benchmark-bootstrap.coffee b/benchmark/benchmark-bootstrap.coffee index 04d450407..8d9a0ba4e 100644 --- a/benchmark/benchmark-bootstrap.coffee +++ b/benchmark/benchmark-bootstrap.coffee @@ -1,6 +1,8 @@ require '../src/window' Atom = require '../src/atom' -window.atom = new Atom() +atom = new Atom() +atom.show() unless atom.getLoadSettings().exitWhenDone +window.atom = atom {runSpecSuite} = require '../spec/jasmine-helper' From 32329070c24f09183f325562caf2696ef1e60bbc Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Thu, 3 Oct 2013 17:40:38 -0700 Subject: [PATCH 52/87] Add a simple gutter api for adding/removing classes --- spec/editor-spec.coffee | 30 +++++++++++++++++++++++ src/gutter.coffee | 54 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 82 insertions(+), 2 deletions(-) diff --git a/spec/editor-spec.coffee b/spec/editor-spec.coffee index e407048cb..d063926d8 100644 --- a/spec/editor-spec.coffee +++ b/spec/editor-spec.coffee @@ -1886,6 +1886,36 @@ describe "Editor", -> config.set("editor.showLineNumbers", false) expect(editor.gutter.lineNumbers).not.toBeVisible() + describe "using gutter's api", -> + it "can get all the line number elements", -> + elements = editor.gutter.getLineNumberElements() + len = editor.gutter.lastScreenRow - editor.gutter.firstScreenRow + 1 + expect(elements).toHaveLength(len) + + it "can get a single line number element", -> + element = editor.gutter.getLineNumberElement(3) + + expect(element).toBeTruthy() + expect($(element)).toHaveClass('line-number') + expect($(element)).toHaveClass('line-number-3') + + it "returns falsy when there is no line element", -> + expect(editor.gutter.getLineNumberElement(42)).toBeFalsy() + + it "can add and remove classes to all the line numbers", -> + elements = editor.gutter.addClassToAllLines('heyok') + expect($(elements)).toHaveClass('heyok') + + elements = editor.gutter.removeClassFromAllLines('heyok') + expect($(elements)).not.toHaveClass('heyok') + + it "can add and remove classes from a single line number", -> + element = editor.gutter.addClassToLine(3, 'heyok') + expect($(element)).toHaveClass('heyok') + + element = editor.gutter.getLineNumberElement(2) + expect($(element)).not.toHaveClass('heyok') + describe "gutter line highlighting", -> beforeEach -> editor.attachToDom(heightInLines: 5.5) diff --git a/src/gutter.coffee b/src/gutter.coffee index 0fb2ae9a3..b8526548d 100644 --- a/src/gutter.coffee +++ b/src/gutter.coffee @@ -62,6 +62,56 @@ class Gutter extends View setShowLineNumbers: (showLineNumbers) -> if showLineNumbers then @lineNumbers.show() else @lineNumbers.hide() + # Get all the line-number divs. + # + # Returns a list of {HTMLElement}s. + getLineNumberElements: -> + @lineNumbers[0].getElementsByClassName('line-number') + + # Get a single line-number div. + # + # * lineNumber: 0 based line number + # + # Returns a {HTMLElement} + getLineNumberElement: (lineNumber) -> + @lineNumbers[0].getElementsByClassName("line-number-#{lineNumber}")[0] + + # Add a class to all line-number divs. + # + # * clas: string class name + # + # Returns a list of {HTMLElement}s. + addClassToAllLines: (clas)-> + $.fn.addClass.call(@getLineNumberElements(), clas) + + # Remove a class from all line-number divs. + # + # * clas: string class name + # + # Returns a list of {HTMLElement}s. + removeClassFromAllLines: (clas)-> + $.fn.removeClass.call(@getLineNumberElements(), clas) + + # Add a class to a single line-number div + # + # * lineNumber: 0 based line number + # * clas: string class name + # + # Returns the {HTMLElement} on which the class was set. undefined if the line was not found + addClassToLine: (lineNumber, clas)-> + line = @lineNumbers[0].getElementsByClassName("line-number-#{lineNumber}") + $.fn.addClass.call(line, clas) if line + + # Remove a class from a single line-number div + # + # * lineNumber: 0 based line number + # * clas: string class name + # + # Returns the {HTMLElement} on which the class was set. undefined if the line was not found + removeClassFromLine: (lineNumber, clas)-> + line = @lineNumbers[0].getElementsByClassName("line-number-#{lineNumber}") + $.fn.removeClass.call(line, clas) if line + ### Internal ### updateLineNumbers: (changes, renderFrom, renderTo) -> @@ -96,12 +146,12 @@ class Gutter extends View else rowValue = (row + 1).toString() - classes = 'line-number' + classes = "line-number line-number-#{row}" classes += ' fold' if editor.isFoldedAtBufferRow(row) rowValuePadding = _.multiplyString(' ', maxDigits - rowValue.length) - html += """
#{rowValuePadding}#{rowValue}
""" + html += """
#{rowValuePadding}#{rowValue}
""" lastScreenRow = row From 964e88f131f879120e2286e7cea7f44bb220ec3f Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Mon, 7 Oct 2013 13:55:04 -0700 Subject: [PATCH 53/87] Add benchmark for the gutter api fns --- benchmark/benchmark-suite.coffee | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/benchmark/benchmark-suite.coffee b/benchmark/benchmark-suite.coffee index 2b8bec8ba..3653e3e89 100644 --- a/benchmark/benchmark-suite.coffee +++ b/benchmark/benchmark-suite.coffee @@ -123,6 +123,19 @@ describe "editor.", -> div = document.createElement('div') div.innerHTML = html + describe "gutter-api.", -> + describe "getLineNumberElementsForClass.", -> + beforeEach -> + editor.gutter.addClassToLine(20, 'omgwow') + editor.gutter.addClassToLine(40, 'omgwow') + + benchmark "DOM", 20000, -> + editor.gutter.getLineNumberElementsForClass('omgwow') + + describe "getLineNumberElement.", -> + benchmark "DOM", 20000, -> + editor.gutter.getLineNumberElementDOM(12) + describe "line-htmlification.", -> div = null html = null From 1ff97fc21a8770e37e35a3c8c0d075a72bb9f4d0 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Mon, 7 Oct 2013 13:57:00 -0700 Subject: [PATCH 54/87] Add getLineNumbersForClass() Also clean up and use klass --- spec/editor-spec.coffee | 15 ++++++++++++- src/gutter.coffee | 48 +++++++++++++++++++++++------------------ 2 files changed, 41 insertions(+), 22 deletions(-) diff --git a/spec/editor-spec.coffee b/spec/editor-spec.coffee index d063926d8..700a88e16 100644 --- a/spec/editor-spec.coffee +++ b/spec/editor-spec.coffee @@ -1900,7 +1900,7 @@ describe "Editor", -> expect($(element)).toHaveClass('line-number-3') it "returns falsy when there is no line element", -> - expect(editor.gutter.getLineNumberElement(42)).toBeFalsy() + expect(editor.gutter.getLineNumberElement(42).length).toBeFalsy() it "can add and remove classes to all the line numbers", -> elements = editor.gutter.addClassToAllLines('heyok') @@ -1916,6 +1916,19 @@ describe "Editor", -> element = editor.gutter.getLineNumberElement(2) expect($(element)).not.toHaveClass('heyok') + it "can fetch line numbers by their class", -> + editor.gutter.addClassToLine(1, 'heyok') + editor.gutter.addClassToLine(3, 'heyok') + + elements = editor.gutter.getLineNumberElementsForClass('heyok') + expect(elements.length).toBe 2 + + expect($(elements[0])).toHaveClass 'line-number-1' + expect($(elements[0])).toHaveClass 'heyok' + + expect($(elements[1])).toHaveClass 'line-number-3' + expect($(elements[1])).toHaveClass 'heyok' + describe "gutter line highlighting", -> beforeEach -> editor.attachToDom(heightInLines: 5.5) diff --git a/src/gutter.coffee b/src/gutter.coffee index b8526548d..bca1db848 100644 --- a/src/gutter.coffee +++ b/src/gutter.coffee @@ -66,51 +66,57 @@ class Gutter extends View # # Returns a list of {HTMLElement}s. getLineNumberElements: -> - @lineNumbers[0].getElementsByClassName('line-number') + @lineNumbers[0].childNodes + + # Get all the line-number divs. + # + # Returns a list of {HTMLElement}s. + getLineNumberElementsForClass: (klass) -> + @lineNumbers[0].getElementsByClassName(klass) # Get a single line-number div. # - # * lineNumber: 0 based line number + # * bufferRow: 0 based line number # - # Returns a {HTMLElement} - getLineNumberElement: (lineNumber) -> - @lineNumbers[0].getElementsByClassName("line-number-#{lineNumber}")[0] + # Returns a list of {HTMLElement}s that correspond to the bufferRow + getLineNumberElement: (bufferRow) -> + @getLineNumberElementsForClass("line-number-#{bufferRow}") # Add a class to all line-number divs. # - # * clas: string class name + # * klass: string class name # # Returns a list of {HTMLElement}s. - addClassToAllLines: (clas)-> - $.fn.addClass.call(@getLineNumberElements(), clas) + addClassToAllLines: (klass)-> + $.fn.addClass.call(@getLineNumberElements(), klass) # Remove a class from all line-number divs. # - # * clas: string class name + # * klass: string class name # # Returns a list of {HTMLElement}s. - removeClassFromAllLines: (clas)-> - $.fn.removeClass.call(@getLineNumberElements(), clas) + removeClassFromAllLines: (klass)-> + $.fn.removeClass.call(@getLineNumberElements(), klass) # Add a class to a single line-number div # - # * lineNumber: 0 based line number - # * clas: string class name + # * bufferRow: 0 based line number + # * klass: string class name # # Returns the {HTMLElement} on which the class was set. undefined if the line was not found - addClassToLine: (lineNumber, clas)-> - line = @lineNumbers[0].getElementsByClassName("line-number-#{lineNumber}") - $.fn.addClass.call(line, clas) if line + addClassToLine: (bufferRow, klass)-> + line = @getLineNumberElement(bufferRow) + $.fn.addClass.call(line, klass) if line and line.length # Remove a class from a single line-number div # - # * lineNumber: 0 based line number - # * clas: string class name + # * bufferRow: 0 based line number + # * klass: string class name # # Returns the {HTMLElement} on which the class was set. undefined if the line was not found - removeClassFromLine: (lineNumber, clas)-> - line = @lineNumbers[0].getElementsByClassName("line-number-#{lineNumber}") - $.fn.removeClass.call(line, clas) if line + removeClassFromLine: (bufferRow, klass)-> + line = @getLineNumberElement(bufferRow) + $.fn.removeClass.call(line, klass) if line and line.length ### Internal ### From 64e8c978e4ef396d5cfd1faf7e66d293b026b016 Mon Sep 17 00:00:00 2001 From: Matt Colyer Date: Mon, 7 Oct 2013 14:28:13 -0700 Subject: [PATCH 55/87] Return proper return code from atom.sh Also improve the test failure message returned by apm. --- atom.sh | 1 + vendor/apm | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/atom.sh b/atom.sh index 53de6a493..6ee2b9990 100755 --- a/atom.sh +++ b/atom.sh @@ -32,6 +32,7 @@ done if [ $EXPECT_OUTPUT ]; then $ATOM_BINARY --executed-from="$(pwd)" --pid=$$ $@ + exit $? else open -a $ATOM_PATH -n --args --executed-from="$(pwd)" --pid=$$ $@ fi diff --git a/vendor/apm b/vendor/apm index 162824eb1..fcb19e296 160000 --- a/vendor/apm +++ b/vendor/apm @@ -1 +1 @@ -Subproject commit 162824eb1a73c154e3bf823c591fa42f8ebcaa37 +Subproject commit fcb19e296ca8979a28d2d503c2650f4ef381c8be From ba8707dfd9a6101282ec7455d45d35db525160a5 Mon Sep 17 00:00:00 2001 From: Matt Colyer Date: Mon, 7 Oct 2013 14:35:15 -0700 Subject: [PATCH 56/87] Bump exception-reporting to include the user agent --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3254a0d4a..6f7c8410e 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "command-logger": "0.4.0", "command-palette": "0.4.0", "editor-stats": "0.3.0", - "exception-reporting": "0.3.0", + "exception-reporting": "0.4.0", "find-and-replace": "0.24.0", "fuzzy-finder": "0.7.0", "gfm": "0.5.0", From 2cee400547b1588fc5035857eba559e4d156019e Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Mon, 7 Oct 2013 14:56:29 -0700 Subject: [PATCH 57/87] More benchmarks --- benchmark/benchmark-suite.coffee | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/benchmark/benchmark-suite.coffee b/benchmark/benchmark-suite.coffee index 3653e3e89..b5401d385 100644 --- a/benchmark/benchmark-suite.coffee +++ b/benchmark/benchmark-suite.coffee @@ -132,9 +132,31 @@ describe "editor.", -> benchmark "DOM", 20000, -> editor.gutter.getLineNumberElementsForClass('omgwow') - describe "getLineNumberElement.", -> - benchmark "DOM", 20000, -> - editor.gutter.getLineNumberElementDOM(12) + benchmark "getLineNumberElement.DOM", 20000, -> + editor.gutter.getLineNumberElement(12) + + benchmark "toggle-class", 2000, -> + editor.gutter.addClassToLine(40, 'omgwow') + editor.gutter.removeClassFromLine(40, 'omgwow') + + describe "find-then-unset.", -> + classes = ['one', 'two', 'three', 'four'] + + benchmark "single-class", 200, -> + editor.gutter.addClassToLine(30, 'omgwow') + editor.gutter.addClassToLine(40, 'omgwow') + editor.gutter.removeClassFromAllLines('omgwow') + + benchmark "multiple-class", 200, -> + editor.gutter.addClassToLine(30, 'one') + editor.gutter.addClassToLine(30, 'two') + + editor.gutter.addClassToLine(40, 'two') + editor.gutter.addClassToLine(40, 'three') + editor.gutter.addClassToLine(40, 'four') + + for klass in classes + editor.gutter.removeClassFromAllLines(klass) describe "line-htmlification.", -> div = null From 9c6353977fa64dcd972dbd4bb080fbbd7ec9ea46 Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Mon, 7 Oct 2013 14:57:45 -0700 Subject: [PATCH 58/87] Rework the api using native methods. This is :racehorse: editor.300-line-file.gutter-api.getLineNumberElementsForClass.DOM: 3 / 20000 = 0.00015ms editor.300-line-file.gutter-api.getLineNumberElement.DOM: 8 / 20000 = 0.0004ms editor.300-line-file.gutter-api.toggle-class: 17 / 2000 = 0.0085ms editor.300-line-file.gutter-api.find-then-unset.single-class: 3 / 200 = 0.015ms editor.300-line-file.gutter-api.find-then-unset.multiple-class: 9 / 200 = 0.045ms --- spec/editor-spec.coffee | 18 +++++++++--------- src/gutter.coffee | 38 ++++++++++++++++++++++++++------------ 2 files changed, 35 insertions(+), 21 deletions(-) diff --git a/spec/editor-spec.coffee b/spec/editor-spec.coffee index 700a88e16..48de79f12 100644 --- a/spec/editor-spec.coffee +++ b/spec/editor-spec.coffee @@ -1894,24 +1894,24 @@ describe "Editor", -> it "can get a single line number element", -> element = editor.gutter.getLineNumberElement(3) - expect(element).toBeTruthy() - expect($(element)).toHaveClass('line-number') - expect($(element)).toHaveClass('line-number-3') it "returns falsy when there is no line element", -> - expect(editor.gutter.getLineNumberElement(42).length).toBeFalsy() + expect(editor.gutter.getLineNumberElement(42)).toHaveLength 0 it "can add and remove classes to all the line numbers", -> - elements = editor.gutter.addClassToAllLines('heyok') + wasAdded = editor.gutter.addClassToAllLines('heyok') + expect(wasAdded).toBe true + + elements = editor.gutter.getLineNumberElementsForClass('heyok') expect($(elements)).toHaveClass('heyok') - elements = editor.gutter.removeClassFromAllLines('heyok') - expect($(elements)).not.toHaveClass('heyok') + editor.gutter.removeClassFromAllLines('heyok') + expect($(editor.gutter.getLineNumberElements())).not.toHaveClass('heyok') it "can add and remove classes from a single line number", -> - element = editor.gutter.addClassToLine(3, 'heyok') - expect($(element)).toHaveClass('heyok') + wasAdded = editor.gutter.addClassToLine(3, 'heyok') + expect(wasAdded).toBe true element = editor.gutter.getLineNumberElement(2) expect($(element)).not.toHaveClass('heyok') diff --git a/src/gutter.coffee b/src/gutter.coffee index bca1db848..1e38daa7a 100644 --- a/src/gutter.coffee +++ b/src/gutter.coffee @@ -78,7 +78,8 @@ class Gutter extends View # # * bufferRow: 0 based line number # - # Returns a list of {HTMLElement}s that correspond to the bufferRow + # Returns a list of {HTMLElement}s that correspond to the bufferRow. More than + # one in the list indicates a wrapped line. getLineNumberElement: (bufferRow) -> @getLineNumberElementsForClass("line-number-#{bufferRow}") @@ -86,37 +87,50 @@ class Gutter extends View # # * klass: string class name # - # Returns a list of {HTMLElement}s. + # Returns true if the class was added to any lines addClassToAllLines: (klass)-> - $.fn.addClass.call(@getLineNumberElements(), klass) + elements = @getLineNumberElements() + el.classList.add(klass) for el in elements + !!elements.length # Remove a class from all line-number divs. # - # * klass: string class name + # * klass: string class name. Can only be one class name. i.e. 'my-class' # - # Returns a list of {HTMLElement}s. + # Returns true if the class was removed from any lines removeClassFromAllLines: (klass)-> - $.fn.removeClass.call(@getLineNumberElements(), klass) + # This is faster than calling $.removeClass on all lines, and faster than + # making a new array and iterating through it. + elements = @getLineNumberElementsForClass(klass) + willRemoveClasses = !!elements.length + elements[0].classList.remove(klass) while elements.length > 0 + willRemoveClasses # Add a class to a single line-number div # # * bufferRow: 0 based line number # * klass: string class name # - # Returns the {HTMLElement} on which the class was set. undefined if the line was not found + # Returns true if there were lines the class was added to addClassToLine: (bufferRow, klass)-> - line = @getLineNumberElement(bufferRow) - $.fn.addClass.call(line, klass) if line and line.length + elements = @getLineNumberElement(bufferRow) + el.classList.add(klass) for el in elements + !!elements.length # Remove a class from a single line-number div # # * bufferRow: 0 based line number # * klass: string class name # - # Returns the {HTMLElement} on which the class was set. undefined if the line was not found + # Returns true if there were lines the class was removed from removeClassFromLine: (bufferRow, klass)-> - line = @getLineNumberElement(bufferRow) - $.fn.removeClass.call(line, klass) if line and line.length + classesRemoved = false + elements = @getLineNumberElement(bufferRow) + for el in elements + hasClass = el.classList.contains(klass) + classesRemoved |= hasClass + el.classList.remove(klass) if hasClass + classesRemoved ### Internal ### From 0c54f6254d0fac574267057971eef788ccbe9e9a Mon Sep 17 00:00:00 2001 From: Ben Ogle Date: Mon, 7 Oct 2013 15:15:30 -0700 Subject: [PATCH 59/87] Upgrade to new versions of git-diff and bookmarks --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 3254a0d4a..c6a656627 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "archive-view": "0.8.0", "autocomplete": "0.6.0", "autoflow": "0.3.0", - "bookmarks": "0.4.0", + "bookmarks": "0.5.0", "bracket-matcher": "0.6.0", "collaboration": "0.20.0", "command-logger": "0.4.0", @@ -56,7 +56,7 @@ "find-and-replace": "0.24.0", "fuzzy-finder": "0.7.0", "gfm": "0.5.0", - "git-diff": "0.4.0", + "git-diff": "0.5.0", "gists": "0.3.0", "github-sign-in": "0.7.0", "go-to-line": "0.4.0", From a3559d1289d5b76e9f8266bf7bb6fe255ae4a81c Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 7 Oct 2013 14:36:19 -0700 Subject: [PATCH 60/87] Call methods through this instead of atom global --- src/package-manager.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/package-manager.coffee b/src/package-manager.coffee index cc08c6f56..70eaec4aa 100644 --- a/src/package-manager.coffee +++ b/src/package-manager.coffee @@ -122,8 +122,8 @@ class PackageManager getAvailablePackageMetadata: -> packages = [] - for packagePath in atom.getAvailablePackagePaths() + for packagePath in @getAvailablePackagePaths() name = path.basename(packagePath) - metadata = atom.getLoadedPackage(name)?.metadata ? Package.loadMetadata(packagePath, true) + metadata = @getLoadedPackage(name)?.metadata ? Package.loadMetadata(packagePath, true) packages.push(metadata) packages From 722be2267d8e5b8284162f4250bdf44d3929b1c8 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 7 Oct 2013 14:36:58 -0700 Subject: [PATCH 61/87] Add getter for config directory path --- src/config.coffee | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/config.coffee b/src/config.coffee index 1b2466381..05bdc19a4 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -117,6 +117,9 @@ class Config _.extend hash, defaults @update() + # Public: Get the path to this config's directory. + getDirectoryPath: -> @configDirPath + # Public: Returns a new {Object} containing all of settings and defaults. getSettings: -> _.deepExtend(@settings, @defaultSettings) From 87bfcf56837486f25a1641bc0f8aa5ede9c8bbe1 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 7 Oct 2013 14:38:19 -0700 Subject: [PATCH 62/87] :syringe: dependencies into AtomPackage Removes use of resourcePath and config globals --- src/atom.coffee | 18 ++++++++++-------- src/package-manager.coffee | 14 +++++++++----- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/atom.coffee b/src/atom.coffee index 07569171e..7a2982101 100644 --- a/src/atom.coffee +++ b/src/atom.coffee @@ -27,6 +27,8 @@ class Atom initialize: -> @unsubscribe() + {devMode, resourcePath} = atom.getLoadSettings() + Config = require './config' Keymap = require './keymap' PackageManager = require './package-manager' @@ -35,20 +37,20 @@ class Atom ThemeManager = require './theme-manager' ContextMenuManager = require './context-menu-manager' - @packages = new PackageManager() - - #TODO Remove once packages have been updated to not touch atom.packageStates directly - @__defineGetter__ 'packageStates', => @packages.packageStates - @__defineSetter__ 'packageStates', (packageStates) => @packages.packageStates = packageStates - - @subscribe @packages, 'loaded', => @watchThemes() @themes = new ThemeManager() - @contextMenu = new ContextMenuManager(@getLoadSettings().devMode) + @contextMenu = new ContextMenuManager(devMode) @config = new Config() @pasteboard = new Pasteboard() @keymap = new Keymap() @syntax = deserialize(@getWindowState('syntax')) ? new Syntax() + @packages = new PackageManager({devMode, resourcePath, configDirPath: @config.getDirectoryPath()}) + @subscribe @packages, 'loaded', => @watchThemes() + + #TODO Remove once packages have been updated to not touch atom.packageStates directly + @__defineGetter__ 'packageStates', => @packages.packageStates + @__defineSetter__ 'packageStates', (packageStates) => @packages.packageStates = packageStates + getCurrentWindow: -> remote.getCurrentWindow() diff --git a/src/package-manager.coffee b/src/package-manager.coffee index 70eaec4aa..a2f026b2d 100644 --- a/src/package-manager.coffee +++ b/src/package-manager.coffee @@ -8,7 +8,11 @@ module.exports = class PackageManager _.extend @prototype, EventEmitter - constructor: -> + constructor: ({configDirPath, devMode, @resourcePath}) -> + @packageDirPaths = [path.join(configDirPath, "packages")] + if devMode + @packageDirPaths.unshift(path.join(configDirPath, "dev", "packages")) + @loadedPackages = {} @activePackages = {} @packageStates = {} @@ -83,10 +87,10 @@ class PackageManager resolvePackagePath: (name) -> return name if fsUtils.isDirectorySync(name) - packagePath = fsUtils.resolve(config.packageDirPaths..., name) + packagePath = fsUtils.resolve(@packageDirPaths..., name) return packagePath if fsUtils.isDirectorySync(packagePath) - packagePath = path.join(window.resourcePath, 'node_modules', name) + packagePath = path.join(@resourcePath, 'node_modules', name) return packagePath if @isInternalPackage(packagePath) isInternalPackage: (packagePath) -> @@ -108,11 +112,11 @@ class PackageManager getAvailablePackagePaths: -> packagePaths = [] - for packageDirPath in config.packageDirPaths + for packageDirPath in @packageDirPaths for packagePath in fsUtils.listSync(packageDirPath) packagePaths.push(packagePath) if fsUtils.isDirectorySync(packagePath) - for packagePath in fsUtils.listSync(path.join(window.resourcePath, 'node_modules')) + for packagePath in fsUtils.listSync(path.join(@resourcePath, 'node_modules')) packagePaths.push(packagePath) if @isInternalPackage(packagePath) _.uniq(packagePaths) From 19a8626c212a14f211c0e9860dfd4dd58aecf2cc Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 7 Oct 2013 14:40:05 -0700 Subject: [PATCH 63/87] Add extension point for opening urls If packages specify a urlMain in their package.json then that file will be used as the bootstrap script new windows when a URL is opened to that package. --- src/atom-application.coffee | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/src/atom-application.coffee b/src/atom-application.coffee index b54b99bfa..25c7c2b1e 100644 --- a/src/atom-application.coffee +++ b/src/atom-application.coffee @@ -51,7 +51,7 @@ class AtomApplication version: null constructor: (options) -> - {@resourcePath, @version} = options + {@resourcePath, @version, @devMode} = options global.atomApplication = this @pidsToOpenWindows = {} @@ -147,7 +147,7 @@ class AtomApplication app.on 'open-url', (event, urlToOpen) => event.preventDefault() - @openUrl(urlToOpen) + @openUrl({urlToOpen, @devMode}) autoUpdater.on 'ready-for-update-on-quit', (event, version, quitAndUpdateCallback) => event.preventDefault() @@ -253,8 +253,6 @@ class AtomApplication # Private: Handles an atom:// url. # - # Currently only supports atom://session/ urls. - # # * options # + urlToOpen: # The atom:// url to open. @@ -262,14 +260,27 @@ class AtomApplication # Boolean to control the opened window's dev mode. openUrl: ({urlToOpen, devMode}) -> parsedUrl = url.parse(urlToOpen) - if parsedUrl.host is 'session' - sessionId = parsedUrl.path.split('/')[1] - console.log "Joining session #{sessionId}" - if sessionId - bootstrapScript = 'collaboration/lib/bootstrap' - new AtomWindow({bootstrapScript, @resourcePath, sessionId, devMode}) + packageName = parsedUrl.host + unless @packages? + PackageManager = require './package-manager' + fsUtils = require './fs-utils' + @packages = new PackageManager({ + devMode: devMode + configDirPath: fsUtils.absolute('~/.atom') + resourcePath: @resourcePath + }) + + pack = _.find @packages.getAvailablePackageMetadata(), ({name}) -> name is packageName + if pack? + console.log(pack) + if pack.urlMain + packagePath = @packages.resolvePackagePath(packageName) + bootstrapScript = path.resolve(packagePath, pack.urlMain) + new AtomWindow({bootstrapScript, @resourcePath, devMode}) + else + console.log "Package '#{pack.name}' does not have a url main: #{urlToOpen}" else - console.log "Opening unknown url #{urlToOpen}" + console.log "Opening unknown url: #{urlToOpen}" # Private: Opens up a new {AtomWindow} to run specs within. # From ad577d6315ba6bd1f12ab068c529247c7cb4407d Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 7 Oct 2013 14:41:13 -0700 Subject: [PATCH 64/87] Add url to open to load settings --- src/atom-application.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/src/atom-application.coffee b/src/atom-application.coffee index 25c7c2b1e..0807209b3 100644 --- a/src/atom-application.coffee +++ b/src/atom-application.coffee @@ -268,6 +268,7 @@ class AtomApplication devMode: devMode configDirPath: fsUtils.absolute('~/.atom') resourcePath: @resourcePath + urlToOpen: urlToOpen }) pack = _.find @packages.getAvailablePackageMetadata(), ({name}) -> name is packageName From db47d02c3c59e5d79d9033278544f65850bea487 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 7 Oct 2013 14:52:30 -0700 Subject: [PATCH 65/87] Add fixture packages to atom.packages.packageDirPaths --- spec/spec-helper.coffee | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index a902fc2b4..5fb160e87 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -18,6 +18,7 @@ atom.themes.loadBaseStylesheets() atom.themes.requireStylesheet '../static/jasmine' fixturePackagesPath = path.resolve(__dirname, './fixtures/packages') +atom.packages.packageDirPaths.unshift(fixturePackagesPath) atom.keymap.loadBundledKeymaps() [bindingSetsToRestore, bindingSetsByFirstKeystrokeToRestore] = [] From 55b71405c9bac21133f79a5953330fc505c1c04d Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 7 Oct 2013 14:56:19 -0700 Subject: [PATCH 66/87] Remove uneeded curlies and parens --- src/atom-application.coffee | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/atom-application.coffee b/src/atom-application.coffee index 0807209b3..d95a27a4e 100644 --- a/src/atom-application.coffee +++ b/src/atom-application.coffee @@ -264,12 +264,11 @@ class AtomApplication unless @packages? PackageManager = require './package-manager' fsUtils = require './fs-utils' - @packages = new PackageManager({ + @packages = new PackageManager devMode: devMode configDirPath: fsUtils.absolute('~/.atom') resourcePath: @resourcePath urlToOpen: urlToOpen - }) pack = _.find @packages.getAvailablePackageMetadata(), ({name}) -> name is packageName if pack? From 9db3f9b5d9d4d8c9553217d9ffb1de4d4a4d46b4 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 7 Oct 2013 15:06:59 -0700 Subject: [PATCH 67/87] Remove logging of found package --- src/atom-application.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/src/atom-application.coffee b/src/atom-application.coffee index d95a27a4e..491b9650e 100644 --- a/src/atom-application.coffee +++ b/src/atom-application.coffee @@ -272,7 +272,6 @@ class AtomApplication pack = _.find @packages.getAvailablePackageMetadata(), ({name}) -> name is packageName if pack? - console.log(pack) if pack.urlMain packagePath = @packages.resolvePackagePath(packageName) bootstrapScript = path.resolve(packagePath, pack.urlMain) From 0033c9659f36f38db7179959e810cfac39434e83 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 7 Oct 2013 15:28:00 -0700 Subject: [PATCH 68/87] :syringe: directory dependencies into Config --- spec/spec-helper.coffee | 4 +++- src/atom.coffee | 24 +++++++++++++++--------- src/config.coffee | 18 ++++++------------ 3 files changed, 24 insertions(+), 22 deletions(-) diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index 5fb160e87..db907b685 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -51,7 +51,9 @@ beforeEach -> bindingSetsByFirstKeystrokeToRestore = _.clone(keymap.bindingSetsByFirstKeystroke) # reset config before each spec; don't load or save from/to `config.json` - config = new Config() + config = new Config + resourcePath: window.resourcePath + configDirPath: atom.getConfigDirPath() config.packageDirPaths.unshift(fixturePackagesPath) spyOn(config, 'load') spyOn(config, 'save') diff --git a/src/atom.coffee b/src/atom.coffee index 7a2982101..96501496b 100644 --- a/src/atom.coffee +++ b/src/atom.coffee @@ -28,6 +28,7 @@ class Atom @unsubscribe() {devMode, resourcePath} = atom.getLoadSettings() + configDirPath = @getConfigDirPath() Config = require './config' Keymap = require './keymap' @@ -37,20 +38,21 @@ class Atom ThemeManager = require './theme-manager' ContextMenuManager = require './context-menu-manager' - @themes = new ThemeManager() - @contextMenu = new ContextMenuManager(devMode) - @config = new Config() - @pasteboard = new Pasteboard() - @keymap = new Keymap() - @syntax = deserialize(@getWindowState('syntax')) ? new Syntax() - - @packages = new PackageManager({devMode, resourcePath, configDirPath: @config.getDirectoryPath()}) - @subscribe @packages, 'loaded', => @watchThemes() + @packages = new PackageManager({devMode, configDirPath, resourcePath}) #TODO Remove once packages have been updated to not touch atom.packageStates directly @__defineGetter__ 'packageStates', => @packages.packageStates @__defineSetter__ 'packageStates', (packageStates) => @packages.packageStates = packageStates + @subscribe @packages, 'loaded', => @watchThemes() + @themes = new ThemeManager() + @contextMenu = new ContextMenuManager(devMode) + @config = new Config({configDirPath, resourcePath}) + @pasteboard = new Pasteboard() + @keymap = new Keymap() + @syntax = deserialize(@getWindowState('syntax')) ? new Syntax() + + getCurrentWindow: -> remote.getCurrentWindow() @@ -215,6 +217,10 @@ class Atom getHomeDirPath: -> app.getHomeDir() + # Public: Get the directory path to Atom's configuration area. + getConfigDirPath: -> + @configDirPath ?= fsUtils.absolute('~/.atom') + getWindowStatePath: -> switch @windowMode when 'spec' diff --git a/src/config.coffee b/src/config.coffee index 05bdc19a4..c50384e18 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -7,8 +7,6 @@ path = require 'path' async = require 'async' pathWatcher = require 'pathwatcher' -configDirPath = fsUtils.absolute("~/.atom") - # Public: Used to access all of Atom's configuration details. # # A global instance of this class is available to all plugins which can be @@ -35,14 +33,13 @@ class Config configFileHasErrors: null # Private: Created during initialization, available as `global.config` - constructor: -> - @configDirPath = configDirPath - @bundledKeymapsDirPath = path.join(resourcePath, "keymaps") - @nodeModulesDirPath = path.join(resourcePath, "node_modules") + constructor: ({@configDirPath, @resourcePath}={}) -> + @bundledKeymapsDirPath = path.join(@resourcePath, "keymaps") + @nodeModulesDirPath = path.join(@resourcePath, "node_modules") @bundledPackageDirPaths = [@nodeModulesDirPath] @lessSearchPaths = [ - path.join(resourcePath, 'static', 'variables') - path.join(resourcePath, 'static') + path.join(@resourcePath, 'static', 'variables') + path.join(@resourcePath, 'static') ] @packageDirPaths = [path.join(configDirPath, "packages")] if atom.getLoadSettings().devMode @@ -67,7 +64,7 @@ class Config fsUtils.copy(sourcePath, destinationPath, callback) queue.drain = done - templateConfigDirPath = fsUtils.resolve(window.resourcePath, 'dot-atom') + templateConfigDirPath = fsUtils.resolve(@resourcePath, 'dot-atom') onConfigDirFile = (sourcePath) => relativePath = sourcePath.substring(templateConfigDirPath.length + 1) destinationPath = path.join(@configDirPath, relativePath) @@ -117,9 +114,6 @@ class Config _.extend hash, defaults @update() - # Public: Get the path to this config's directory. - getDirectoryPath: -> @configDirPath - # Public: Returns a new {Object} containing all of settings and defaults. getSettings: -> _.deepExtend(@settings, @defaultSettings) From e95e8a22c3f755a31771c57449560e6bcdd662e4 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 7 Oct 2013 15:29:44 -0700 Subject: [PATCH 69/87] :lipstick: Remove extra newline --- src/atom.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/src/atom.coffee b/src/atom.coffee index 96501496b..f4ce8e441 100644 --- a/src/atom.coffee +++ b/src/atom.coffee @@ -52,7 +52,6 @@ class Atom @keymap = new Keymap() @syntax = deserialize(@getWindowState('syntax')) ? new Syntax() - getCurrentWindow: -> remote.getCurrentWindow() From f8e61f5c4861b07f9ac24eda76ac0fab080f8a9f Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 7 Oct 2013 15:31:08 -0700 Subject: [PATCH 70/87] Add missing @ before configDirPath --- src/config.coffee | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/config.coffee b/src/config.coffee index c50384e18..f6104747f 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -41,18 +41,18 @@ class Config path.join(@resourcePath, 'static', 'variables') path.join(@resourcePath, 'static') ] - @packageDirPaths = [path.join(configDirPath, "packages")] + @packageDirPaths = [path.join(@configDirPath, "packages")] if atom.getLoadSettings().devMode - @packageDirPaths.unshift(path.join(configDirPath, "dev", "packages")) + @packageDirPaths.unshift(path.join(@configDirPath, "dev", "packages")) @userPackageDirPaths = _.clone(@packageDirPaths) - @userStoragePath = path.join(configDirPath, "storage") + @userStoragePath = path.join(@configDirPath, "storage") @defaultSettings = core: _.clone(require('./root-view').configDefaults) editor: _.clone(require('./editor').configDefaults) @settings = {} - @configFilePath = fsUtils.resolve(configDirPath, 'config', ['json', 'cson']) - @configFilePath ?= path.join(configDirPath, 'config.cson') + @configFilePath = fsUtils.resolve(@configDirPath, 'config', ['json', 'cson']) + @configFilePath ?= path.join(@configDirPath, 'config.cson') # Private: initializeConfigDirectory: (done) -> From d3e2d9b5f9fe874649fc46faf5ecca8d210cff3d Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 7 Oct 2013 15:42:23 -0700 Subject: [PATCH 71/87] :memo: Update AtomApplication.openUrl() comment --- src/atom-application.coffee | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/atom-application.coffee b/src/atom-application.coffee index 491b9650e..354e3d202 100644 --- a/src/atom-application.coffee +++ b/src/atom-application.coffee @@ -251,7 +251,11 @@ class AtomApplication console.log("Killing process #{pid} failed: #{error.code}") delete @pidsToOpenWindows[pid] - # Private: Handles an atom:// url. + # Private: Open an atom:// url. + # + # The host of the URL being opened is assumed to be the package name + # responsible for opening the URL. A new window will be created with + # that package's `urlMain` as the bootstrap script. # # * options # + urlToOpen: From 124b1ebd3398306bd714021d465442a78dd17318 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 7 Oct 2013 15:42:42 -0700 Subject: [PATCH 72/87] :lipstick: Reorganize option param --- src/atom-application.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/atom-application.coffee b/src/atom-application.coffee index 354e3d202..f6b95e06c 100644 --- a/src/atom-application.coffee +++ b/src/atom-application.coffee @@ -269,8 +269,8 @@ class AtomApplication PackageManager = require './package-manager' fsUtils = require './fs-utils' @packages = new PackageManager - devMode: devMode configDirPath: fsUtils.absolute('~/.atom') + devMode: devMode resourcePath: @resourcePath urlToOpen: urlToOpen From 7f0150c6b880d66c7bd6ebc91f858425b9ca85ba Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 7 Oct 2013 15:43:31 -0700 Subject: [PATCH 73/87] :lipstick: Inline parsed url --- src/atom-application.coffee | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/atom-application.coffee b/src/atom-application.coffee index f6b95e06c..7d86e68bf 100644 --- a/src/atom-application.coffee +++ b/src/atom-application.coffee @@ -263,8 +263,6 @@ class AtomApplication # + devMode: # Boolean to control the opened window's dev mode. openUrl: ({urlToOpen, devMode}) -> - parsedUrl = url.parse(urlToOpen) - packageName = parsedUrl.host unless @packages? PackageManager = require './package-manager' fsUtils = require './fs-utils' @@ -274,6 +272,7 @@ class AtomApplication resourcePath: @resourcePath urlToOpen: urlToOpen + packageName = url.parse(urlToOpen).host pack = _.find @packages.getAvailablePackageMetadata(), ({name}) -> name is packageName if pack? if pack.urlMain From 2029895f0bf6a0e948892a25476620c6865a28d4 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 7 Oct 2013 16:01:32 -0700 Subject: [PATCH 74/87] Pass urlToOpen to AtomWindow constructor --- src/atom-application.coffee | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/atom-application.coffee b/src/atom-application.coffee index 7d86e68bf..2d3bb244f 100644 --- a/src/atom-application.coffee +++ b/src/atom-application.coffee @@ -270,7 +270,6 @@ class AtomApplication configDirPath: fsUtils.absolute('~/.atom') devMode: devMode resourcePath: @resourcePath - urlToOpen: urlToOpen packageName = url.parse(urlToOpen).host pack = _.find @packages.getAvailablePackageMetadata(), ({name}) -> name is packageName @@ -278,7 +277,7 @@ class AtomApplication if pack.urlMain packagePath = @packages.resolvePackagePath(packageName) bootstrapScript = path.resolve(packagePath, pack.urlMain) - new AtomWindow({bootstrapScript, @resourcePath, devMode}) + new AtomWindow({bootstrapScript, @resourcePath, devMode, urlToOpen}) else console.log "Package '#{pack.name}' does not have a url main: #{urlToOpen}" else From d2d77012c53875fd30bd554b8eb28c67d31dac43 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 7 Oct 2013 16:21:21 -0700 Subject: [PATCH 75/87] Upgrade to collaboration@0.21.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 97e1e87a4..b30f70324 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "autoflow": "0.3.0", "bookmarks": "0.5.0", "bracket-matcher": "0.6.0", - "collaboration": "0.20.0", + "collaboration": "0.21.0", "command-logger": "0.4.0", "command-palette": "0.4.0", "editor-stats": "0.3.0", From 1ed6733ab5ebb941363791cd6ade88d0366563d1 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 7 Oct 2013 17:09:52 -0700 Subject: [PATCH 76/87] Only parse line number when path to open is set Previously if atom was launched with no path to open then '/' was being opened as the project and a file named 'undefined' was initially open. This was because path.basename() returns the string 'undefined' when called with an undefined path. This restores the previous behavior of launching a new untitled editor with no project. Closes #936 --- src/atom-application.coffee | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/atom-application.coffee b/src/atom-application.coffee index 2d3bb244f..f094dac4e 100644 --- a/src/atom-application.coffee +++ b/src/atom-application.coffee @@ -221,9 +221,10 @@ class AtomApplication # + devMode: # Boolean to control the opened window's dev mode. openPath: ({pathToOpen, pidToKillWhenClosed, newWindow, devMode}={}) -> - [basename, initialLine] = path.basename(pathToOpen).split(':') - pathToOpen = "#{path.dirname(pathToOpen)}/#{basename}" - initialLine -= 1 if initialLine # Convert line numbers to a base of 0 + if pathToOpen + [basename, initialLine] = path.basename(pathToOpen).split(':') + pathToOpen = "#{path.dirname(pathToOpen)}/#{basename}" + initialLine -= 1 if initialLine # Convert line numbers to a base of 0 unless devMode existingWindow = @windowForPath(pathToOpen) unless pidToKillWhenClosed or newWindow From 8caed2081bb2cf6bb6535c29c2956e6cb988d0ab Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 7 Oct 2013 17:21:02 -0700 Subject: [PATCH 77/87] Don't reuse window when opening a subfolder Windows will only be reused when they match the initial path or are paths to a new or existing file. Closes #934 --- src/atom-window.coffee | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/atom-window.coffee b/src/atom-window.coffee index 350aac6f5..a16c5ae3f 100644 --- a/src/atom-window.coffee +++ b/src/atom-window.coffee @@ -55,6 +55,8 @@ class AtomWindow false else if pathToCheck is initialPath true + else if fs.statSyncNoException(pathToCheck).isDirectory() + false else if pathToCheck.indexOf(path.join(initialPath, path.sep)) is 0 true else From 8e60db65a332d62a677509ebf06a6507c52e5c00 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 7 Oct 2013 17:55:41 -0700 Subject: [PATCH 78/87] :racehorse: Use fs.statSyncNoException() in AtomWindow --- src/atom-window.coffee | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/atom-window.coffee b/src/atom-window.coffee index a16c5ae3f..e275eef8c 100644 --- a/src/atom-window.coffee +++ b/src/atom-window.coffee @@ -30,9 +30,8 @@ class AtomWindow loadSettings = _.extend({}, settings) loadSettings.windowState ?= '' loadSettings.initialPath = pathToOpen - try - if fs.statSync(pathToOpen).isFile() - loadSettings.initialPath = path.dirname(pathToOpen) + if fs.statSyncNoException(pathToOpen).isFile() + loadSettings.initialPath = path.dirname(pathToOpen) @browserWindow.loadSettings = loadSettings @browserWindow.once 'window:loaded', => @loaded = true From 380bad212981cecfc4ced76f4f12a43b94f888d2 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 7 Oct 2013 18:09:51 -0700 Subject: [PATCH 79/87] Upgrade to tree-view@0.9.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b30f70324..bc8b60039 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,7 @@ "timecop": "0.5.0", "to-the-hubs": "0.6.0", "toml": "0.3.0", - "tree-view": "0.8.0", + "tree-view": "0.9.0", "ui-demo": "0.8.0", "whitespace": "0.5.0", "wrap-guide": "0.3.0", From 41cfa2892b415ed34b783f6cbbd864186a515710 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 7 Oct 2013 18:20:15 -0700 Subject: [PATCH 80/87] Guard against false result --- src/atom-window.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/atom-window.coffee b/src/atom-window.coffee index e275eef8c..4cd34b85e 100644 --- a/src/atom-window.coffee +++ b/src/atom-window.coffee @@ -30,7 +30,7 @@ class AtomWindow loadSettings = _.extend({}, settings) loadSettings.windowState ?= '' loadSettings.initialPath = pathToOpen - if fs.statSyncNoException(pathToOpen).isFile() + if fs.statSyncNoException(pathToOpen).isFile?() loadSettings.initialPath = path.dirname(pathToOpen) @browserWindow.loadSettings = loadSettings @@ -54,7 +54,7 @@ class AtomWindow false else if pathToCheck is initialPath true - else if fs.statSyncNoException(pathToCheck).isDirectory() + else if fs.statSyncNoException(pathToCheck).isDirectory?() false else if pathToCheck.indexOf(path.join(initialPath, path.sep)) is 0 true From 37ec1abc252100baffa88614df4492bf51c1aa7c Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Mon, 7 Oct 2013 19:42:29 -0700 Subject: [PATCH 81/87] Add Git::isProjectAtRoot helper --- src/git.coffee | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/git.coffee b/src/git.coffee index 14e7620fd..e6ae615e9 100644 --- a/src/git.coffee +++ b/src/git.coffee @@ -142,6 +142,12 @@ class Git # Public: Determine if the given path is new. isPathNew: (path) -> @isStatusNew(@getPathStatus(path)) + # Public: Is the project at the root of this repository? + # + # Returns true if at the root, false if in a subfolder of the repository. + isProjectAtRoot: -> + @projectAtRoot ?= project.relativize(@getWorkingDirectory()) is '' + # Public: Makes a path relative to the repository's working directory. relativize: (path) -> @getRepo().relativize(path) From 6d15fbb114a3912802cff1212047ea035ad03cdf Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Tue, 8 Oct 2013 09:00:05 -0700 Subject: [PATCH 82/87] Upgrade to tree-view@0.10.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bc8b60039..8b0e5a099 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,7 @@ "timecop": "0.5.0", "to-the-hubs": "0.6.0", "toml": "0.3.0", - "tree-view": "0.9.0", + "tree-view": "0.10.0", "ui-demo": "0.8.0", "whitespace": "0.5.0", "wrap-guide": "0.3.0", From 7365be7d88924720a50c2d8ee363c3c206fc9e86 Mon Sep 17 00:00:00 2001 From: Garen Torikian Date: Sat, 5 Oct 2013 16:41:14 -0700 Subject: [PATCH 83/87] Add new helper functions from git-utils@0.26.0 --- package.json | 2 +- src/git.coffee | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 8b0e5a099..c0ec3ea0e 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "coffee-script": "1.6.2", "coffeestack": "0.6.0", "first-mate": "0.2.0", - "git-utils": "0.25.0", + "git-utils": "0.26.0", "guid": "0.0.10", "jasmine-focused": "~0.14.0", "mkdirp": "0.3.5", diff --git a/src/git.coffee b/src/git.coffee index e6ae615e9..cfeb14720 100644 --- a/src/git.coffee +++ b/src/git.coffee @@ -177,6 +177,15 @@ class Git @getPathStatus(path) if headCheckedOut headCheckedOut + # Public: Checks out a branch in your repository. + # + # reference - The {String} reference to checkout + # create - A {Boolean} value which, if `true` creates the new reference if it doesn't exist. + # + # Returns a {Boolean} that's `true` if the method was successful. + checkoutReference: (reference, create) -> + @getRepo().checkoutReference(reference, create) + # Public: Retrieves the number of lines added and removed to a path. # # This compares the working directory contents of the path to the `HEAD` @@ -245,6 +254,13 @@ class Git # Public: ? getReferenceTarget: (reference) -> @getRepo().getReferenceTarget(reference) + # Public: Gets all the local and remote references. + # + # + # Returns an object with three keys: `heads`, `remotes`, and `tags`. Each key + # can be an array of strings containing the reference names. + getReferences: -> @getRepo().getReferences() + # Public: ? getAheadBehindCount: (reference) -> @getRepo().getAheadBehindCount(reference) From 250af9191af41c54ac1f1f3e035ffa5601c43c4e Mon Sep 17 00:00:00 2001 From: Garen Torikian Date: Sun, 6 Oct 2013 23:52:15 -0700 Subject: [PATCH 84/87] Trigger statuses-changed for branches, too There don't seem to be any tests for this --- src/git.coffee | 6 ++++-- src/repository-status-handler.coffee | 4 +++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/git.coffee b/src/git.coffee index cfeb14720..8d8d45559 100644 --- a/src/git.coffee +++ b/src/git.coffee @@ -44,6 +44,7 @@ class Git path: null statuses: null upstream: null + branch: null statusTask: null # Private: Creates a new `Git` object. @@ -269,8 +270,9 @@ class Git # Private: refreshStatus: -> - @statusTask = Task.once require.resolve('./repository-status-handler'), @getPath(), ({statuses, upstream}) => - statusesUnchanged = _.isEqual(statuses, @statuses) and _.isEqual(upstream, @upstream) + @statusTask = Task.once require.resolve('./repository-status-handler'), @getPath(), ({statuses, upstream, branch}) => + statusesUnchanged = _.isEqual(statuses, @statuses) and _.isEqual(upstream, @upstream)and _.isEqual(branch, @branch) @statuses = statuses @upstream = upstream + @branch = branch @trigger 'statuses-changed' unless statusesUnchanged diff --git a/src/repository-status-handler.coffee b/src/repository-status-handler.coffee index 9eda7e2f6..3d63c99f9 100644 --- a/src/repository-status-handler.coffee +++ b/src/repository-status-handler.coffee @@ -9,9 +9,11 @@ module.exports = (repoPath) -> for filePath, status of repo.getStatus() statuses[path.join(workingDirectoryPath, filePath)] = status upstream = repo.getAheadBehindCount() + branch = repo.getHead() repo.release() else upstream = {} statuses = {} + branch = {} - {statuses, upstream} + {statuses, upstream, branch} From 43d5c3f66d73b81c58bdcfbb8b7d0360c5edd2d2 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Tue, 8 Oct 2013 09:27:23 -0700 Subject: [PATCH 85/87] :memo: Remove extra newline --- src/git.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/src/git.coffee b/src/git.coffee index 8d8d45559..02afecec4 100644 --- a/src/git.coffee +++ b/src/git.coffee @@ -257,7 +257,6 @@ class Git # Public: Gets all the local and remote references. # - # # Returns an object with three keys: `heads`, `remotes`, and `tags`. Each key # can be an array of strings containing the reference names. getReferences: -> @getRepo().getReferences() From ff1ab93652927e7d4266e22a3ea9341e7e6be3d1 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Tue, 8 Oct 2013 09:30:05 -0700 Subject: [PATCH 86/87] :lipstick: Add space before and --- src/git.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/git.coffee b/src/git.coffee index 02afecec4..c10e50fb1 100644 --- a/src/git.coffee +++ b/src/git.coffee @@ -270,7 +270,7 @@ class Git # Private: refreshStatus: -> @statusTask = Task.once require.resolve('./repository-status-handler'), @getPath(), ({statuses, upstream, branch}) => - statusesUnchanged = _.isEqual(statuses, @statuses) and _.isEqual(upstream, @upstream)and _.isEqual(branch, @branch) + statusesUnchanged = _.isEqual(statuses, @statuses) and _.isEqual(upstream, @upstream) and _.isEqual(branch, @branch) @statuses = statuses @upstream = upstream @branch = branch From 4c9059d4454c96558021e465ee15801e13f1ef68 Mon Sep 17 00:00:00 2001 From: Kevin Sawicki Date: Tue, 8 Oct 2013 09:31:02 -0700 Subject: [PATCH 87/87] Default branch to null instead of empty object --- src/repository-status-handler.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/repository-status-handler.coffee b/src/repository-status-handler.coffee index 3d63c99f9..0a9c08f7c 100644 --- a/src/repository-status-handler.coffee +++ b/src/repository-status-handler.coffee @@ -14,6 +14,6 @@ module.exports = (repoPath) -> else upstream = {} statuses = {} - branch = {} + branch = null {statuses, upstream, branch}