diff --git a/package.json b/package.json index 5165c61eb..74a07659d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "atom", "productName": "Atom", - "version": "0.205.0", + "version": "0.206.0", "description": "A hackable text editor for the 21st Century.", "main": "./src/browser/main.js", "repository": { @@ -32,7 +32,7 @@ "delegato": "^1", "emissary": "^1.3.3", "event-kit": "^1.2.0", - "first-mate": "^4.1.5", + "first-mate": "^4.1.6", "fs-plus": "^2.8.0", "fstream": "0.1.24", "fuzzaldrin": "^2.1", @@ -77,10 +77,10 @@ "atom-light-ui": "0.41.0", "base16-tomorrow-dark-theme": "0.26.0", "base16-tomorrow-light-theme": "0.9.0", - "one-dark-ui": "0.8.2", - "one-dark-syntax": "0.5.0", - "one-light-syntax": "0.6.0", - "one-light-ui": "0.8.2", + "one-dark-ui": "0.9.0", + "one-dark-syntax": "0.7.0", + "one-light-syntax": "0.7.0", + "one-light-ui": "0.9.0", "solarized-dark-syntax": "0.35.0", "solarized-light-syntax": "0.21.0", "archive-view": "0.57.0", @@ -88,12 +88,12 @@ "autocomplete-css": "0.7.2", "autocomplete-html": "0.7.2", "autocomplete-plus": "2.17.3", - "autocomplete-snippets": "1.6.3", + "autocomplete-snippets": "1.7.0", "autoflow": "0.24.0", "autosave": "0.20.0", "background-tips": "0.25.0", "bookmarks": "0.35.0", - "bracket-matcher": "0.74.0", + "bracket-matcher": "0.75.0", "command-palette": "0.36.0", "deprecation-cop": "0.52.0", "dev-live-reload": "0.46.0", @@ -115,7 +115,7 @@ "package-generator": "0.39.0", "release-notes": "0.52.0", "settings-view": "0.206.0", - "snippets": "0.91.0", + "snippets": "0.93.0", "spell-check": "0.58.0", "status-bar": "0.74.0", "styleguide": "0.44.0", @@ -139,7 +139,7 @@ "language-hyperlink": "0.13.0", "language-java": "0.15.0", "language-javascript": "0.78.0", - "language-json": "0.14.0", + "language-json": "0.15.0", "language-less": "0.27.0", "language-make": "0.14.0", "language-mustache": "0.11.0", @@ -153,7 +153,7 @@ "language-sass": "0.38.0", "language-shellscript": "0.15.0", "language-source": "0.9.0", - "language-sql": "0.15.0", + "language-sql": "0.16.0", "language-text": "0.6.0", "language-todo": "0.23.0", "language-toml": "0.16.0", diff --git a/spec/config-spec.coffee b/spec/config-spec.coffee index 2c5ba2fc2..b75a121a1 100644 --- a/spec/config-spec.coffee +++ b/spec/config-spec.coffee @@ -805,7 +805,6 @@ describe "Config", -> atom.config.loadUserConfig() expect(atom.config.get("foo.bar")).toBe "baz" - describe ".observeUserConfig()", -> updatedHandler = null @@ -1381,6 +1380,16 @@ describe "Config", -> expect(atom.config.set('foo.bar.aString', nope: 'nope')).toBe false expect(atom.config.get('foo.bar.aString')).toBe 'ok' + describe 'when the schema has a "maximumLength" key', -> + it "trims the string to be no longer than the specified maximum", -> + schema = + type: 'string' + default: 'ok' + maximumLength: 3 + atom.config.setSchema('foo.bar.aString', schema) + atom.config.set('foo.bar.aString', 'abcdefg') + expect(atom.config.get('foo.bar.aString')).toBe 'abc' + describe 'when the value has an "object" type', -> beforeEach -> schema = diff --git a/spec/default-directory-provider-spec.coffee b/spec/default-directory-provider-spec.coffee index 69370a77b..357348c4a 100644 --- a/spec/default-directory-provider-spec.coffee +++ b/spec/default-directory-provider-spec.coffee @@ -31,6 +31,12 @@ describe "DefaultDirectoryProvider", -> directory = provider.directoryForURISync(file) expect(directory.getPath()).toEqual tmp + it "creates a Directory with a path as a uri when passed a uri", -> + provider = new DefaultDirectoryProvider() + uri = 'remote://server:6792/path/to/a/dir' + directory = provider.directoryForURISync(uri) + expect(directory.getPath()).toEqual uri + describe ".directoryForURI(uri)", -> it "returns a Promise that resolves to a Directory with a path that matches the uri", -> provider = new DefaultDirectoryProvider() diff --git a/spec/integration/startup-spec.coffee b/spec/integration/startup-spec.coffee index 54817fedc..d2583ed27 100644 --- a/spec/integration/startup-spec.coffee +++ b/spec/integration/startup-spec.coffee @@ -227,3 +227,13 @@ describe "Starting Atom", -> [tempDirPath] [otherTempDirPath] ].sort() + + describe "opening a remote directory", -> + it "opens the parent directory and creates an empty text editor", -> + remoteDirectory = 'remote://server:3437/some/directory/path' + runAtom [remoteDirectory], {ATOM_HOME: atomHome}, (client) -> + client + .waitForWindowCount(1, 1000) + .waitForExist("atom-workspace", 5000) + .treeViewRootDirectories() + .then ({value}) -> expect(value).toEqual([remoteDirectory]) diff --git a/spec/task-spec.coffee b/spec/task-spec.coffee index 81a8713ad..bddb59d86 100644 --- a/spec/task-spec.coffee +++ b/spec/task-spec.coffee @@ -57,3 +57,16 @@ describe "Task", -> expect(deprecations.length).toBe 1 expect(deprecations[0].getStacks()[0][1].fileName).toBe handlerPath jasmine.restoreDeprecationsSnapshot() + + it "adds data listeners to standard out and error to report output", -> + task = new Task(require.resolve('./fixtures/task-spec-handler')) + {stdout, stderr} = task.childProcess + + task.start() + task.start() + expect(stdout.listeners('data').length).toBe 1 + expect(stderr.listeners('data').length).toBe 1 + + task.terminate() + expect(stdout.listeners('data').length).toBe 0 + expect(stderr.listeners('data').length).toBe 0 diff --git a/spec/text-editor-component-spec.coffee b/spec/text-editor-component-spec.coffee index 12d189035..b2bb09acd 100644 --- a/spec/text-editor-component-spec.coffee +++ b/spec/text-editor-component-spec.coffee @@ -7,10 +7,10 @@ nbsp = String.fromCharCode(160) describe "TextEditorComponent", -> [contentNode, editor, wrapperView, wrapperNode, component, componentNode, verticalScrollbarNode, horizontalScrollbarNode] = [] - [lineHeightInPixels, charWidth, nextAnimationFrame, noAnimationFrame, lineOverdrawMargin] = [] + [lineHeightInPixels, charWidth, nextAnimationFrame, noAnimationFrame, tileSize] = [] beforeEach -> - lineOverdrawMargin = 2 + tileSize = 3 waitsForPromise -> atom.packages.activatePackage('language-javascript') @@ -34,7 +34,7 @@ describe "TextEditorComponent", -> contentNode = document.querySelector('#jasmine-content') contentNode.style.width = '1000px' - wrapperView = new TextEditorView(editor, {lineOverdrawMargin}) + wrapperView = new TextEditorView(editor, {tileSize}) wrapperView.attachToDom() wrapperNode = wrapperView.element wrapperNode.setUpdatedSynchronously(false) @@ -68,48 +68,111 @@ describe "TextEditorComponent", -> expect(nextAnimationFrame).not.toThrow() describe "line rendering", -> - it "renders the currently-visible lines plus the overdraw margin", -> - wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' + expectTileContainsRow = (tileNode, screenRow, {top}) -> + lineNode = tileNode.querySelector("[data-screen-row='#{screenRow}']") + tokenizedLine = editor.tokenizedLineForScreenRow(screenRow) + + expect(lineNode.offsetTop).toBe(top) + if tokenizedLine.text is "" + expect(lineNode.innerHTML).toBe(" ") + else + expect(lineNode.textContent).toBe(tokenizedLine.text) + + it "renders the currently-visible lines in a tiled fashion", -> + wrapperNode.style.height = 6.5 * lineHeightInPixels + 'px' + tileHeight = tileSize * lineHeightInPixels component.measureDimensions() nextAnimationFrame() - linesNode = componentNode.querySelector('.lines') - expect(linesNode.style['-webkit-transform']).toBe "translate3d(0px, 0px, 0px)" - expect(componentNode.querySelectorAll('.line').length).toBe 6 + 2 # no margin above - expect(component.lineNodeForScreenRow(0).textContent).toBe editor.tokenizedLineForScreenRow(0).text - expect(component.lineNodeForScreenRow(0).offsetTop).toBe 0 - expect(component.lineNodeForScreenRow(5).textContent).toBe editor.tokenizedLineForScreenRow(5).text - expect(component.lineNodeForScreenRow(5).offsetTop).toBe 5 * lineHeightInPixels + tilesNodes = componentNode.querySelectorAll(".tile") - verticalScrollbarNode.scrollTop = 4.5 * lineHeightInPixels + expect(tilesNodes.length).toBe(3) + + expect(tilesNodes[0].style['-webkit-transform']).toBe "translate3d(0px, 0px, 0px)" + expect(tilesNodes[0].children.length).toBe(tileSize) + expectTileContainsRow(tilesNodes[0], 0, top: 0 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[0], 1, top: 1 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[0], 2, top: 2 * lineHeightInPixels) + + expect(tilesNodes[1].style['-webkit-transform']).toBe "translate3d(0px, #{1 * tileHeight}px, 0px)" + expect(tilesNodes[1].children.length).toBe(tileSize) + expectTileContainsRow(tilesNodes[1], 3, top: 0 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[1], 4, top: 1 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[1], 5, top: 2 * lineHeightInPixels) + + expect(tilesNodes[2].style['-webkit-transform']).toBe "translate3d(0px, #{2 * tileHeight}px, 0px)" + expect(tilesNodes[2].children.length).toBe(tileSize) + expectTileContainsRow(tilesNodes[2], 6, top: 0 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[2], 7, top: 1 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[2], 8, top: 2 * lineHeightInPixels) + + expect(component.lineNodeForScreenRow(9)).toBeUndefined() + + verticalScrollbarNode.scrollTop = tileSize * lineHeightInPixels + 5 verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) nextAnimationFrame() - expect(linesNode.style['-webkit-transform']).toBe "translate3d(0px, #{-4.5 * lineHeightInPixels}px, 0px)" - expect(componentNode.querySelectorAll('.line').length).toBe 6 + 4 # margin above and below - expect(component.lineNodeForScreenRow(2).offsetTop).toBe 2 * lineHeightInPixels - expect(component.lineNodeForScreenRow(2).textContent).toBe editor.tokenizedLineForScreenRow(2).text - expect(component.lineNodeForScreenRow(9).offsetTop).toBe 9 * lineHeightInPixels - expect(component.lineNodeForScreenRow(9).textContent).toBe editor.tokenizedLineForScreenRow(9).text + tilesNodes = componentNode.querySelectorAll(".tile") - it "updates the top position of subsequent lines when lines are inserted or removed", -> + expect(component.lineNodeForScreenRow(2)).toBeUndefined() + expect(tilesNodes.length).toBe(3) + + expect(tilesNodes[0].style['-webkit-transform']).toBe "translate3d(0px, -5px, 0px)" + expect(tilesNodes[0].children.length).toBe(tileSize) + expectTileContainsRow(tilesNodes[0], 3, top: 0 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[0], 4, top: 1 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[0], 5, top: 2 * lineHeightInPixels) + + expect(tilesNodes[1].style['-webkit-transform']).toBe "translate3d(0px, #{1 * tileHeight - 5}px, 0px)" + expect(tilesNodes[1].children.length).toBe(tileSize) + expectTileContainsRow(tilesNodes[1], 6, top: 0 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[1], 7, top: 1 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[1], 8, top: 2 * lineHeightInPixels) + + expect(tilesNodes[2].style['-webkit-transform']).toBe "translate3d(0px, #{2 * tileHeight - 5}px, 0px)" + expect(tilesNodes[2].children.length).toBe(tileSize) + expectTileContainsRow(tilesNodes[2], 9, top: 0 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[2], 10, top: 1 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[2], 11, top: 2 * lineHeightInPixels) + + it "updates the top position of subsequent tiles when lines are inserted or removed", -> + wrapperNode.style.height = 6.5 * lineHeightInPixels + 'px' + tileHeight = tileSize * lineHeightInPixels + component.measureDimensions() editor.getBuffer().deleteRows(0, 1) nextAnimationFrame() - lineNodes = componentNode.querySelectorAll('.line') - expect(component.lineNodeForScreenRow(0).offsetTop).toBe 0 - expect(component.lineNodeForScreenRow(1).offsetTop).toBe 1 * lineHeightInPixels - expect(component.lineNodeForScreenRow(2).offsetTop).toBe 2 * lineHeightInPixels + tilesNodes = componentNode.querySelectorAll(".tile") + + expect(tilesNodes[0].style['-webkit-transform']).toBe "translate3d(0px, 0px, 0px)" + expectTileContainsRow(tilesNodes[0], 0, top: 0 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[0], 1, top: 1 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[0], 2, top: 2 * lineHeightInPixels) + + expect(tilesNodes[1].style['-webkit-transform']).toBe "translate3d(0px, #{1 * tileHeight}px, 0px)" + expectTileContainsRow(tilesNodes[1], 3, top: 0 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[1], 4, top: 1 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[1], 5, top: 2 * lineHeightInPixels) editor.getBuffer().insert([0, 0], '\n\n') nextAnimationFrame() - lineNodes = componentNode.querySelectorAll('.line') - expect(component.lineNodeForScreenRow(0).offsetTop).toBe 0 * lineHeightInPixels - expect(component.lineNodeForScreenRow(1).offsetTop).toBe 1 * lineHeightInPixels - expect(component.lineNodeForScreenRow(2).offsetTop).toBe 2 * lineHeightInPixels - expect(component.lineNodeForScreenRow(3).offsetTop).toBe 3 * lineHeightInPixels - expect(component.lineNodeForScreenRow(4).offsetTop).toBe 4 * lineHeightInPixels + tilesNodes = componentNode.querySelectorAll(".tile") + + expect(tilesNodes[0].style['-webkit-transform']).toBe "translate3d(0px, 0px, 0px)" + expectTileContainsRow(tilesNodes[0], 0, top: 0 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[0], 1, top: 1 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[0], 2, top: 2 * lineHeightInPixels) + + expect(tilesNodes[1].style['-webkit-transform']).toBe "translate3d(0px, #{1 * tileHeight}px, 0px)" + expectTileContainsRow(tilesNodes[1], 3, top: 0 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[1], 4, top: 1 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[1], 5, top: 2 * lineHeightInPixels) + + expect(tilesNodes[2].style['-webkit-transform']).toBe "translate3d(0px, #{2 * tileHeight}px, 0px)" + expectTileContainsRow(tilesNodes[2], 6, top: 0 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[2], 7, top: 1 * lineHeightInPixels) + expectTileContainsRow(tilesNodes[2], 8, top: 2 * lineHeightInPixels) it "updates the lines when lines are inserted or removed above the rendered row range", -> wrapperNode.style.height = 4.5 * lineHeightInPixels + 'px' @@ -483,7 +546,7 @@ describe "TextEditorComponent", -> component.measureDimensions() nextAnimationFrame() - expect(componentNode.querySelectorAll('.line-number').length).toBe 6 + 2 + 1 # line overdraw margin below + dummy line number + expect(componentNode.querySelectorAll('.line-number').length).toBe 6 + 1 # visible line-numbers + dummy line number expect(component.lineNumberNodeForScreenRow(0).textContent).toBe "#{nbsp}1" expect(component.lineNumberNodeForScreenRow(5).textContent).toBe "#{nbsp}6" @@ -491,7 +554,7 @@ describe "TextEditorComponent", -> verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) nextAnimationFrame() - expect(componentNode.querySelectorAll('.line-number').length).toBe 6 + 4 + 1 # line overdraw margin above/below + dummy line number + expect(componentNode.querySelectorAll('.line-number').length).toBe 6 + 1 # visible line-numbers + dummy line number expect(component.lineNumberNodeForScreenRow(2).textContent).toBe "#{nbsp}3" expect(component.lineNumberNodeForScreenRow(2).offsetTop).toBe 2 * lineHeightInPixels @@ -527,7 +590,7 @@ describe "TextEditorComponent", -> component.measureDimensions() nextAnimationFrame() - expect(componentNode.querySelectorAll('.line-number').length).toBe 6 + lineOverdrawMargin + 1 # 1 dummy line componentNode + expect(componentNode.querySelectorAll('.line-number').length).toBe 6 + 1 # 1 dummy line expect(component.lineNumberNodeForScreenRow(0).textContent).toBe "#{nbsp}1" expect(component.lineNumberNodeForScreenRow(1).textContent).toBe "#{nbsp}•" expect(component.lineNumberNodeForScreenRow(2).textContent).toBe "#{nbsp}2" @@ -725,13 +788,13 @@ describe "TextEditorComponent", -> cursorNodes = componentNode.querySelectorAll('.cursor') expect(cursorNodes.length).toBe 2 - expect(cursorNodes[0].style['-webkit-transform']).toBe "translate(#{10 * charWidth}px, #{4 * lineHeightInPixels}px)" - expect(cursorNodes[1].style['-webkit-transform']).toBe "translate(#{11 * charWidth}px, #{8 * lineHeightInPixels}px)" + expect(cursorNodes[0].style['-webkit-transform']).toBe "translate(#{10 * charWidth - horizontalScrollbarNode.scrollLeft}px, #{4 * lineHeightInPixels - verticalScrollbarNode.scrollTop}px)" + expect(cursorNodes[1].style['-webkit-transform']).toBe "translate(#{11 * charWidth - horizontalScrollbarNode.scrollLeft}px, #{8 * lineHeightInPixels - verticalScrollbarNode.scrollTop}px)" editor.onDidChangeCursorPosition cursorMovedListener = jasmine.createSpy('cursorMovedListener') cursor3.setScreenPosition([4, 11], autoscroll: false) nextAnimationFrame() - expect(cursorNodes[0].style['-webkit-transform']).toBe "translate(#{11 * charWidth}px, #{4 * lineHeightInPixels}px)" + expect(cursorNodes[0].style['-webkit-transform']).toBe "translate(#{11 * charWidth - horizontalScrollbarNode.scrollLeft}px, #{4 * lineHeightInPixels - verticalScrollbarNode.scrollTop}px)" expect(cursorMovedListener).toHaveBeenCalled() cursor3.destroy() @@ -739,7 +802,7 @@ describe "TextEditorComponent", -> cursorNodes = componentNode.querySelectorAll('.cursor') expect(cursorNodes.length).toBe 1 - expect(cursorNodes[0].style['-webkit-transform']).toBe "translate(#{11 * charWidth}px, #{8 * lineHeightInPixels}px)" + expect(cursorNodes[0].style['-webkit-transform']).toBe "translate(#{11 * charWidth - horizontalScrollbarNode.scrollLeft}px, #{8 * lineHeightInPixels - verticalScrollbarNode.scrollTop}px)" it "accounts for character widths when positioning cursors", -> atom.config.set('editor.fontFamily', 'sans-serif') @@ -1001,7 +1064,7 @@ describe "TextEditorComponent", -> nextAnimationFrame() # Scroll decorations into view - verticalScrollbarNode.scrollTop = 2.5 * lineHeightInPixels + verticalScrollbarNode.scrollTop = 4.5 * lineHeightInPixels verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) nextAnimationFrame() expect(lineAndLineNumberHaveClass(9, 'b')).toBe true @@ -1147,15 +1210,16 @@ describe "TextEditorComponent", -> # Nothing when outside the rendered row range expect(regions.length).toBe 0 - verticalScrollbarNode.scrollTop = 4.5 * lineHeightInPixels + verticalScrollbarNode.scrollTop = 6 * lineHeightInPixels verticalScrollbarNode.dispatchEvent(new UIEvent('scroll')) nextAnimationFrame() + expect(component.presenter.endRow).toBeGreaterThan(8) regions = componentNode.querySelectorAll('.some-highlight .region') expect(regions.length).toBe 1 regionRect = regions[0].style - expect(regionRect.top).toBe 9 * lineHeightInPixels + 'px' + expect(regionRect.top).toBe (9 * lineHeightInPixels - verticalScrollbarNode.scrollTop) + 'px' expect(regionRect.height).toBe 1 * lineHeightInPixels + 'px' expect(regionRect.left).toBe 2 * charWidth + 'px' expect(regionRect.width).toBe 2 * charWidth + 'px' @@ -1920,13 +1984,23 @@ describe "TextEditorComponent", -> component.measureDimensions() nextAnimationFrame() - linesNode = componentNode.querySelector('.lines') - expect(linesNode.style['-webkit-transform']).toBe "translate3d(0px, 0px, 0px)" + tilesNodes = componentNode.querySelectorAll(".tile") + + top = 0 + for tileNode in tilesNodes + expect(tileNode.style['-webkit-transform']).toBe "translate3d(0px, #{top}px, 0px)" + top += tileNode.offsetHeight + expect(horizontalScrollbarNode.scrollLeft).toBe 0 editor.setScrollLeft(100) nextAnimationFrame() - expect(linesNode.style['-webkit-transform']).toBe "translate3d(-100px, 0px, 0px)" + + top = 0 + for tileNode in tilesNodes + expect(tileNode.style['-webkit-transform']).toBe "translate3d(-100px, #{top}px, 0px)" + top += tileNode.offsetHeight + expect(horizontalScrollbarNode.scrollLeft).toBe 100 it "updates the scrollLeft of the model when the scrollLeft of the horizontal scrollbar changes", -> @@ -2387,7 +2461,7 @@ describe "TextEditorComponent", -> hiddenParent.style.display = 'none' contentNode.appendChild(hiddenParent) - wrapperView = new TextEditorView(editor, {lineOverdrawMargin}) + wrapperView = new TextEditorView(editor, {tileSize}) wrapperNode = wrapperView.element wrapperView.appendTo(hiddenParent) @@ -2508,7 +2582,7 @@ describe "TextEditorComponent", -> advanceClock(atom.views.documentPollingInterval) nextAnimationFrame() - expect(componentNode.querySelectorAll('.line')).toHaveLength(4 + lineOverdrawMargin + 1) + expect(componentNode.querySelectorAll('.line')).toHaveLength(6) gutterWidth = componentNode.querySelector('.gutter').offsetWidth componentNode.style.width = gutterWidth + 14 * charWidth + editor.getVerticalScrollbarWidth() + 'px' diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index 1ad18da5a..142026913 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -39,7 +39,6 @@ describe "TextEditorPresenter", -> verticalScrollbarWidth: 10 scrollTop: 0 scrollLeft: 0 - lineOverdrawMargin: 0 new TextEditorPresenter(params) @@ -657,160 +656,125 @@ describe "TextEditorPresenter", -> expectStateUpdate presenter, -> editor.setPlaceholderText("new-placeholder-text") expect(presenter.getState().content.placeholderText).toBe "new-placeholder-text" - describe ".lines", -> - lineStateForScreenRow = (presenter, screenRow) -> - presenter.getState().content.lines[presenter.model.tokenizedLineForScreenRow(screenRow).id] + describe ".tiles", -> + lineStateForScreenRow = (presenter, row) -> + lineId = presenter.model.tokenizedLineForScreenRow(row).id + tileRow = presenter.tileForRow(row) + presenter.getState().content.tiles[tileRow]?.lines[lineId] - it "contains states for lines that are visible on screen, plus and minus the overdraw margin", -> - presenter = buildPresenter(explicitHeight: 15, scrollTop: 50, lineHeight: 10, lineOverdrawMargin: 1) + it "contains states for tiles that are visible on screen", -> + presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileSize: 2) - expect(lineStateForScreenRow(presenter, 3)).toBeUndefined() - - line4 = editor.tokenizedLineForScreenRow(4) - expectValues lineStateForScreenRow(presenter, 4), { - screenRow: 4 - text: line4.text - tags: line4.tags - specialTokens: line4.specialTokens - firstNonWhitespaceIndex: line4.firstNonWhitespaceIndex - firstTrailingWhitespaceIndex: line4.firstTrailingWhitespaceIndex - invisibles: line4.invisibles - top: 10 * 4 + expectValues presenter.getState().content.tiles[0], { + top: 0 + } + expectValues presenter.getState().content.tiles[2], { + top: 2 + } + expectValues presenter.getState().content.tiles[4], { + top: 4 + } + expectValues presenter.getState().content.tiles[6], { + top: 6 } - line5 = editor.tokenizedLineForScreenRow(5) - expectValues lineStateForScreenRow(presenter, 5), { - screenRow: 5 - text: line5.text - tags: line5.tags - specialTokens: line5.specialTokens - firstNonWhitespaceIndex: line5.firstNonWhitespaceIndex - firstTrailingWhitespaceIndex: line5.firstTrailingWhitespaceIndex - invisibles: line5.invisibles - top: 10 * 5 + expect(presenter.getState().content.tiles[8]).toBeUndefined() + + expectStateUpdate presenter, -> presenter.setScrollTop(3) + + expect(presenter.getState().content.tiles[0]).toBeUndefined() + + expectValues presenter.getState().content.tiles[2], { + top: -1 + } + expectValues presenter.getState().content.tiles[4], { + top: 1 + } + expectValues presenter.getState().content.tiles[6], { + top: 3 + } + expectValues presenter.getState().content.tiles[8], { + top: 5 + } + expectValues presenter.getState().content.tiles[10], { + top: 7 } - line6 = editor.tokenizedLineForScreenRow(6) - expectValues lineStateForScreenRow(presenter, 6), { - screenRow: 6 - text: line6.text - tags: line6.tags - specialTokens: line6.specialTokens - firstNonWhitespaceIndex: line6.firstNonWhitespaceIndex - firstTrailingWhitespaceIndex: line6.firstTrailingWhitespaceIndex - invisibles: line6.invisibles - top: 10 * 6 - } + expect(presenter.getState().content.tiles[12]).toBeUndefined() - line7 = editor.tokenizedLineForScreenRow(7) - expectValues lineStateForScreenRow(presenter, 7), { - screenRow: 7 - text: line7.text - tags: line7.tags - specialTokens: line7.specialTokens - firstNonWhitespaceIndex: line7.firstNonWhitespaceIndex - firstTrailingWhitespaceIndex: line7.firstTrailingWhitespaceIndex - invisibles: line7.invisibles - top: 10 * 7 - } - - line8 = editor.tokenizedLineForScreenRow(8) - expectValues lineStateForScreenRow(presenter, 8), { - screenRow: 8 - text: line8.text - tags: line8.tags - specialTokens: line8.specialTokens - firstNonWhitespaceIndex: line8.firstNonWhitespaceIndex - firstTrailingWhitespaceIndex: line8.firstTrailingWhitespaceIndex - invisibles: line8.invisibles - top: 10 * 8 - } - - expect(lineStateForScreenRow(presenter, 9)).toBeUndefined() - - it "does not overdraw above the first row", -> - presenter = buildPresenter(explicitHeight: 15, scrollTop: 10, lineHeight: 10, lineOverdrawMargin: 2) - expect(lineStateForScreenRow(presenter, 0)).toBeDefined() - expect(lineStateForScreenRow(presenter, 1)).toBeDefined() - expect(lineStateForScreenRow(presenter, 2)).toBeDefined() - expect(lineStateForScreenRow(presenter, 3)).toBeDefined() - expect(lineStateForScreenRow(presenter, 4)).toBeDefined() - expect(lineStateForScreenRow(presenter, 5)).toBeDefined() - expect(lineStateForScreenRow(presenter, 6)).toBeUndefined() - - it "does not overdraw below the last row", -> - presenter = buildPresenter(explicitHeight: 25, scrollTop: 105, lineHeight: 10, lineOverdrawMargin: 2) - expect(lineStateForScreenRow(presenter, 7)).toBeUndefined() - expect(lineStateForScreenRow(presenter, 8)).toBeDefined() - expect(lineStateForScreenRow(presenter, 9)).toBeDefined() - expect(lineStateForScreenRow(presenter, 10)).toBeDefined() - expect(lineStateForScreenRow(presenter, 11)).toBeDefined() - expect(lineStateForScreenRow(presenter, 12)).toBeDefined() - - it "includes state for all lines if no external ::explicitHeight is assigned", -> - presenter = buildPresenter(explicitHeight: null) - expect(lineStateForScreenRow(presenter, 0)).toBeDefined() - expect(lineStateForScreenRow(presenter, 12)).toBeDefined() + it "includes state for all tiles if no external ::explicitHeight is assigned", -> + presenter = buildPresenter(explicitHeight: null, tileSize: 2) + expect(presenter.getState().content.tiles[0]).toBeDefined() + expect(presenter.getState().content.tiles[12]).toBeDefined() it "is empty until all of the required measurements are assigned", -> presenter = buildPresenter(explicitHeight: null, lineHeight: null, scrollTop: null) - expect(presenter.getState().content.lines).toEqual({}) + expect(presenter.getState().content.tiles).toEqual({}) presenter.setExplicitHeight(25) - expect(presenter.getState().content.lines).toEqual({}) + expect(presenter.getState().content.tiles).toEqual({}) presenter.setLineHeight(10) - expect(presenter.getState().content.lines).toEqual({}) + expect(presenter.getState().content.tiles).toEqual({}) presenter.setScrollTop(0) - expect(presenter.getState().content.lines).not.toEqual({}) + expect(presenter.getState().content.tiles).not.toEqual({}) it "updates when ::scrollTop changes", -> - presenter = buildPresenter(explicitHeight: 25, scrollTop: 0, lineHeight: 10, lineOverdrawMargin: 1) + presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileSize: 2) - expect(lineStateForScreenRow(presenter, 0)).toBeDefined() - expect(lineStateForScreenRow(presenter, 4)).toBeDefined() - expect(lineStateForScreenRow(presenter, 5)).toBeUndefined() + expect(presenter.getState().content.tiles[0]).toBeDefined() + expect(presenter.getState().content.tiles[2]).toBeDefined() + expect(presenter.getState().content.tiles[4]).toBeDefined() + expect(presenter.getState().content.tiles[6]).toBeDefined() + expect(presenter.getState().content.tiles[8]).toBeUndefined() - expectStateUpdate presenter, -> presenter.setScrollTop(25) + expectStateUpdate presenter, -> presenter.setScrollTop(2) - expect(lineStateForScreenRow(presenter, 0)).toBeUndefined() - expect(lineStateForScreenRow(presenter, 1)).toBeDefined() - expect(lineStateForScreenRow(presenter, 6)).toBeDefined() - expect(lineStateForScreenRow(presenter, 7)).toBeUndefined() + expect(presenter.getState().content.tiles[0]).toBeUndefined() + expect(presenter.getState().content.tiles[2]).toBeDefined() + expect(presenter.getState().content.tiles[4]).toBeDefined() + expect(presenter.getState().content.tiles[6]).toBeDefined() + expect(presenter.getState().content.tiles[8]).toBeDefined() + expect(presenter.getState().content.tiles[10]).toBeUndefined() it "updates when ::explicitHeight changes", -> - presenter = buildPresenter(explicitHeight: 15, scrollTop: 15, lineHeight: 10, lineOverdrawMargin: 1) + presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileSize: 2) - line5 = editor.tokenizedLineForScreenRow(5) + expect(presenter.getState().content.tiles[0]).toBeDefined() + expect(presenter.getState().content.tiles[2]).toBeDefined() + expect(presenter.getState().content.tiles[4]).toBeDefined() + expect(presenter.getState().content.tiles[6]).toBeDefined() + expect(presenter.getState().content.tiles[8]).toBeUndefined() - expect(lineStateForScreenRow(presenter, 4)).toBeDefined() - expect(lineStateForScreenRow(presenter, 5)).toBeUndefined() + expectStateUpdate presenter, -> presenter.setExplicitHeight(8) - expectStateUpdate presenter, -> presenter.setExplicitHeight(35) + expect(presenter.getState().content.tiles[0]).toBeDefined() + expect(presenter.getState().content.tiles[2]).toBeDefined() + expect(presenter.getState().content.tiles[4]).toBeDefined() + expect(presenter.getState().content.tiles[6]).toBeDefined() + expect(presenter.getState().content.tiles[8]).toBeDefined() + expect(presenter.getState().content.tiles[10]).toBeUndefined() - expect(lineStateForScreenRow(presenter, 5)).toBeDefined() - expect(lineStateForScreenRow(presenter, 6)).toBeDefined() - expect(lineStateForScreenRow(presenter, 7)).toBeUndefined() it "updates when ::lineHeight changes", -> - presenter = buildPresenter(explicitHeight: 15, scrollTop: 10, lineHeight: 10, lineOverdrawMargin: 0) + presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileSize: 2) - expect(lineStateForScreenRow(presenter, 0)).toBeUndefined() - expect(lineStateForScreenRow(presenter, 1)).toBeDefined() - expect(lineStateForScreenRow(presenter, 2)).toBeDefined() - expect(lineStateForScreenRow(presenter, 4)).toBeUndefined() + expect(presenter.getState().content.tiles[0]).toBeDefined() + expect(presenter.getState().content.tiles[2]).toBeDefined() + expect(presenter.getState().content.tiles[4]).toBeDefined() + expect(presenter.getState().content.tiles[6]).toBeDefined() + expect(presenter.getState().content.tiles[8]).toBeUndefined() - expectStateUpdate presenter, -> presenter.setLineHeight(5) + expectStateUpdate presenter, -> presenter.setLineHeight(2) - expect(lineStateForScreenRow(presenter, 0)).toBeUndefined() - expect(lineStateForScreenRow(presenter, 1)).toBeUndefined() - expect(lineStateForScreenRow(presenter, 2)).toBeDefined() - expect(lineStateForScreenRow(presenter, 5)).toBeDefined() - expect(lineStateForScreenRow(presenter, 6)).toBeUndefined() + expect(presenter.getState().content.tiles[0]).toBeDefined() + expect(presenter.getState().content.tiles[2]).toBeDefined() + expect(presenter.getState().content.tiles[4]).toBeDefined() + expect(presenter.getState().content.tiles[6]).toBeUndefined() it "updates when the editor's content changes", -> - presenter = buildPresenter(explicitHeight: 25, scrollTop: 10, lineHeight: 10) + presenter = buildPresenter(explicitHeight: 25, scrollTop: 10, lineHeight: 10, tileSize: 2) expectStateUpdate presenter, -> buffer.insert([2, 0], "hello\nworld\n") @@ -832,53 +796,125 @@ describe "TextEditorPresenter", -> tags: line3.tags } - it "does not remove out-of-view lines corresponding to ::mouseWheelScreenRow until ::stoppedScrollingDelay elapses", -> - presenter = buildPresenter(explicitHeight: 25, scrollTop: 0, lineHeight: 10, lineOverdrawMargin: 1, stoppedScrollingDelay: 200) + it "does not remove out-of-view tiles corresponding to ::mouseWheelScreenRow until ::stoppedScrollingDelay elapses", -> + presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileSize: 2, stoppedScrollingDelay: 200) - expect(lineStateForScreenRow(presenter, 0)).toBeDefined() - expect(lineStateForScreenRow(presenter, 4)).toBeDefined() - expect(lineStateForScreenRow(presenter, 5)).toBeUndefined() + expect(presenter.getState().content.tiles[0]).toBeDefined() + expect(presenter.getState().content.tiles[6]).toBeDefined() + expect(presenter.getState().content.tiles[8]).toBeUndefined() presenter.setMouseWheelScreenRow(0) - expectStateUpdate presenter, -> presenter.setScrollTop(35) + expectStateUpdate presenter, -> presenter.setScrollTop(4) - expect(lineStateForScreenRow(presenter, 0)).toBeDefined() - expect(lineStateForScreenRow(presenter, 1)).toBeUndefined() - expect(lineStateForScreenRow(presenter, 7)).toBeDefined() - expect(lineStateForScreenRow(presenter, 8)).toBeUndefined() + expect(presenter.getState().content.tiles[0]).toBeDefined() + expect(presenter.getState().content.tiles[2]).toBeUndefined() + expect(presenter.getState().content.tiles[4]).toBeDefined() + expect(presenter.getState().content.tiles[12]).toBeUndefined() expectStateUpdate presenter, -> advanceClock(200) - expect(lineStateForScreenRow(presenter, 0)).toBeUndefined() - expect(lineStateForScreenRow(presenter, 1)).toBeUndefined() - expect(lineStateForScreenRow(presenter, 2)).toBeDefined() - expect(lineStateForScreenRow(presenter, 7)).toBeDefined() - expect(lineStateForScreenRow(presenter, 8)).toBeUndefined() + expect(presenter.getState().content.tiles[0]).toBeUndefined() + expect(presenter.getState().content.tiles[2]).toBeUndefined() + expect(presenter.getState().content.tiles[4]).toBeDefined() + expect(presenter.getState().content.tiles[12]).toBeUndefined() + # should clear ::mouseWheelScreenRow after stoppedScrollingDelay elapses even if we don't scroll first - presenter.setMouseWheelScreenRow(2) + presenter.setMouseWheelScreenRow(4) advanceClock(200) - expectStateUpdate presenter, -> presenter.setScrollTop(45) - expect(lineStateForScreenRow(presenter, 2)).toBeUndefined() + expectStateUpdate presenter, -> presenter.setScrollTop(6) + expect(presenter.getState().content.tiles[4]).toBeUndefined() - it "does not preserve on-screen lines even if they correspond to ::mouseWheelScreenRow", -> - presenter = buildPresenter(explicitHeight: 25, scrollTop: 0, lineHeight: 10, lineOverdrawMargin: 1, stoppedScrollingDelay: 200) - oldLine3 = editor.tokenizedLineForScreenRow(6) + it "does not preserve deleted on-screen tiles even if they correspond to ::mouseWheelScreenRow", -> + presenter = buildPresenter(explicitHeight: 6, scrollTop: 0, lineHeight: 1, tileSize: 2, stoppedScrollingDelay: 200) - presenter.setMouseWheelScreenRow(3) + presenter.setMouseWheelScreenRow(2) - expectStateUpdate presenter, -> editor.getBuffer().insert([3, Infinity], 'xyz') - newLine3 = editor.tokenizedLineForScreenRow(3) + expectStateUpdate presenter, -> editor.setText("") - expect(presenter.getState().content.lines[oldLine3.id]).toBeUndefined() - expect(presenter.getState().content.lines[newLine3.id]).toBeDefined() + expect(presenter.getState().content.tiles[2]).toBeUndefined() + expect(presenter.getState().content.tiles[0]).toBeDefined() - it "does not attempt to preserve lines corresponding to ::mouseWheelScreenRow if they have been deleted", -> - presenter = buildPresenter(explicitHeight: 25, scrollTop: 0, lineHeight: 10, lineOverdrawMargin: 1, stoppedScrollingDelay: 200) - presenter.setMouseWheelScreenRow(10) - editor.setText('') + describe "[tileId].lines[lineId]", -> # line state objects + it "includes the state for visible lines in a tile", -> + presenter = buildPresenter(explicitHeight: 3, scrollTop: 4, lineHeight: 1, tileSize: 3, stoppedScrollingDelay: 200) + + expect(lineStateForScreenRow(presenter, 2)).toBeUndefined() + + line3 = editor.tokenizedLineForScreenRow(3) + expectValues lineStateForScreenRow(presenter, 3), { + screenRow: 3 + text: line3.text + tags: line3.tags + specialTokens: line3.specialTokens + firstNonWhitespaceIndex: line3.firstNonWhitespaceIndex + firstTrailingWhitespaceIndex: line3.firstTrailingWhitespaceIndex + invisibles: line3.invisibles + top: 0 + } + + line4 = editor.tokenizedLineForScreenRow(4) + expectValues lineStateForScreenRow(presenter, 4), { + screenRow: 4 + text: line4.text + tags: line4.tags + specialTokens: line4.specialTokens + firstNonWhitespaceIndex: line4.firstNonWhitespaceIndex + firstTrailingWhitespaceIndex: line4.firstTrailingWhitespaceIndex + invisibles: line4.invisibles + top: 1 + } + + line5 = editor.tokenizedLineForScreenRow(5) + expectValues lineStateForScreenRow(presenter, 5), { + screenRow: 5 + text: line5.text + tags: line5.tags + specialTokens: line5.specialTokens + firstNonWhitespaceIndex: line5.firstNonWhitespaceIndex + firstTrailingWhitespaceIndex: line5.firstTrailingWhitespaceIndex + invisibles: line5.invisibles + top: 2 + } + + line6 = editor.tokenizedLineForScreenRow(6) + expectValues lineStateForScreenRow(presenter, 6), { + screenRow: 6 + text: line6.text + tags: line6.tags + specialTokens: line6.specialTokens + firstNonWhitespaceIndex: line6.firstNonWhitespaceIndex + firstTrailingWhitespaceIndex: line6.firstTrailingWhitespaceIndex + invisibles: line6.invisibles + top: 0 + } + + line7 = editor.tokenizedLineForScreenRow(7) + expectValues lineStateForScreenRow(presenter, 7), { + screenRow: 7 + text: line7.text + tags: line7.tags + specialTokens: line7.specialTokens + firstNonWhitespaceIndex: line7.firstNonWhitespaceIndex + firstTrailingWhitespaceIndex: line7.firstTrailingWhitespaceIndex + invisibles: line7.invisibles + top: 1 + } + + line8 = editor.tokenizedLineForScreenRow(8) + expectValues lineStateForScreenRow(presenter, 8), { + screenRow: 8 + text: line8.text + tags: line8.tags + specialTokens: line8.specialTokens + firstNonWhitespaceIndex: line8.firstNonWhitespaceIndex + firstTrailingWhitespaceIndex: line8.firstTrailingWhitespaceIndex + invisibles: line8.invisibles + top: 2 + } + + expect(lineStateForScreenRow(presenter, 9)).toBeUndefined() - describe "[lineId]", -> # line state objects it "includes the .endOfLineInvisibles if the editor.showInvisibles config option is true", -> editor.setText("hello\nworld\r\n") presenter = buildPresenter(explicitHeight: 25, scrollTop: 0, lineHeight: 10) @@ -1031,9 +1067,9 @@ describe "TextEditorPresenter", -> presenter = buildPresenter(explicitHeight: 30, scrollTop: 20) expect(stateForCursor(presenter, 0)).toBeUndefined() - expect(stateForCursor(presenter, 1)).toEqual {top: 2 * 10, left: 4 * 10, width: 10, height: 10} + expect(stateForCursor(presenter, 1)).toEqual {top: 0, left: 4 * 10, width: 10, height: 10} expect(stateForCursor(presenter, 2)).toBeUndefined() - expect(stateForCursor(presenter, 3)).toEqual {top: 5 * 10, left: 12 * 10, width: 10, height: 10} + expect(stateForCursor(presenter, 3)).toEqual {top: 5 * 10 - 20, left: 12 * 10, width: 10, height: 10} expect(stateForCursor(presenter, 4)).toBeUndefined() it "is empty until all of the required measurements are assigned", -> @@ -1069,8 +1105,21 @@ describe "TextEditorPresenter", -> expect(stateForCursor(presenter, 0)).toBeUndefined() expect(stateForCursor(presenter, 1)).toBeUndefined() expect(stateForCursor(presenter, 2)).toBeUndefined() - expect(stateForCursor(presenter, 3)).toEqual {top: 5 * 10, left: 12 * 10, width: 10, height: 10} - expect(stateForCursor(presenter, 4)).toEqual {top: 8 * 10, left: 4 * 10, width: 10, height: 10} + expect(stateForCursor(presenter, 3)).toEqual {top: 0, left: 12 * 10, width: 10, height: 10} + expect(stateForCursor(presenter, 4)).toEqual {top: 8 * 10 - 50, left: 4 * 10, width: 10, height: 10} + + it "updates when ::scrollTop changes after the model was changed", -> + editor.setCursorBufferPosition([8, 22]) + presenter = buildPresenter(explicitHeight: 50, scrollTop: 10 * 8) + + expect(stateForCursor(presenter, 0)).toEqual {top: 0, left: 10 * 22, width: 10, height: 10} + + expectStateUpdate presenter, -> + editor.getBuffer().deleteRow(12) + editor.getBuffer().deleteRow(11) + editor.getBuffer().deleteRow(10) + + expect(stateForCursor(presenter, 0)).toEqual {top: 20, left: 10 * 22, width: 10, height: 10} it "updates when ::explicitHeight changes", -> editor.setSelectedBufferRanges([ @@ -1084,9 +1133,9 @@ describe "TextEditorPresenter", -> expectStateUpdate presenter, -> presenter.setExplicitHeight(30) expect(stateForCursor(presenter, 0)).toBeUndefined() - expect(stateForCursor(presenter, 1)).toEqual {top: 2 * 10, left: 4 * 10, width: 10, height: 10} + expect(stateForCursor(presenter, 1)).toEqual {top: 0, left: 4 * 10, width: 10, height: 10} expect(stateForCursor(presenter, 2)).toBeUndefined() - expect(stateForCursor(presenter, 3)).toEqual {top: 5 * 10, left: 12 * 10, width: 10, height: 10} + expect(stateForCursor(presenter, 3)).toEqual {top: 5 * 10 - 20, left: 12 * 10, width: 10, height: 10} expect(stateForCursor(presenter, 4)).toBeUndefined() it "updates when ::lineHeight changes", -> @@ -1103,15 +1152,15 @@ describe "TextEditorPresenter", -> expect(stateForCursor(presenter, 0)).toBeUndefined() expect(stateForCursor(presenter, 1)).toBeUndefined() expect(stateForCursor(presenter, 2)).toBeUndefined() - expect(stateForCursor(presenter, 3)).toEqual {top: 5 * 5, left: 12 * 10, width: 10, height: 5} - expect(stateForCursor(presenter, 4)).toEqual {top: 8 * 5, left: 4 * 10, width: 10, height: 5} + expect(stateForCursor(presenter, 3)).toEqual {top: 5, left: 12 * 10, width: 10, height: 5} + expect(stateForCursor(presenter, 4)).toEqual {top: 8 * 5 - 20, left: 4 * 10, width: 10, height: 5} it "updates when ::baseCharacterWidth changes", -> editor.setCursorBufferPosition([2, 4]) presenter = buildPresenter(explicitHeight: 20, scrollTop: 20) expectStateUpdate presenter, -> presenter.setBaseCharacterWidth(20) - expect(stateForCursor(presenter, 0)).toEqual {top: 2 * 10, left: 4 * 20, width: 20, height: 10} + expect(stateForCursor(presenter, 0)).toEqual {top: 0, left: 4 * 20, width: 20, height: 10} it "updates when scoped character widths change", -> waitsForPromise -> @@ -1137,11 +1186,11 @@ describe "TextEditorPresenter", -> # moving into view expect(stateForCursor(presenter, 0)).toBeUndefined() editor.getCursors()[0].setBufferPosition([2, 4]) - expect(stateForCursor(presenter, 0)).toEqual {top: 2 * 10, left: 4 * 10, width: 10, height: 10} + expect(stateForCursor(presenter, 0)).toEqual {top: 0, left: 4 * 10, width: 10, height: 10} # showing expectStateUpdate presenter, -> editor.getSelections()[1].clear() - expect(stateForCursor(presenter, 1)).toEqual {top: 3 * 10, left: 5 * 10, width: 10, height: 10} + expect(stateForCursor(presenter, 1)).toEqual {top: 5, left: 5 * 10, width: 10, height: 10} # hiding expectStateUpdate presenter, -> editor.getSelections()[1].setBufferRange([[3, 4], [3, 5]]) @@ -1153,11 +1202,11 @@ describe "TextEditorPresenter", -> # adding expectStateUpdate presenter, -> editor.addCursorAtBufferPosition([4, 4]) - expect(stateForCursor(presenter, 2)).toEqual {top: 4 * 10, left: 4 * 10, width: 10, height: 10} + expect(stateForCursor(presenter, 2)).toEqual {top: 5, left: 4 * 10, width: 10, height: 10} # moving added cursor expectStateUpdate presenter, -> editor.getCursors()[2].setBufferPosition([4, 6]) - expect(stateForCursor(presenter, 2)).toEqual {top: 4 * 10, left: 6 * 10, width: 10, height: 10} + expect(stateForCursor(presenter, 2)).toEqual {top: 5, left: 6 * 10, width: 10, height: 10} # destroying destroyedCursor = editor.getCursors()[2] @@ -1275,39 +1324,39 @@ describe "TextEditorPresenter", -> expectValues stateForHighlight(presenter, highlight2), { class: 'b' regions: [ - {top: 2 * 10, left: 0 * 10, width: 6 * 10, height: 1 * 10} + {top: 2 * 10 - 20, left: 0 * 10, width: 6 * 10, height: 1 * 10} ] } expectValues stateForHighlight(presenter, highlight3), { class: 'c' regions: [ - {top: 2 * 10, left: 0 * 10, right: 0, height: 1 * 10} - {top: 3 * 10, left: 0 * 10, width: 6 * 10, height: 1 * 10} + {top: 2 * 10 - 20, left: 0 * 10, right: 0, height: 1 * 10} + {top: 3 * 10 - 20, left: 0 * 10, width: 6 * 10, height: 1 * 10} ] } expectValues stateForHighlight(presenter, highlight4), { class: 'd' regions: [ - {top: 2 * 10, left: 6 * 10, right: 0, height: 1 * 10} - {top: 3 * 10, left: 0, right: 0, height: 1 * 10} - {top: 4 * 10, left: 0, width: 6 * 10, height: 1 * 10} + {top: 2 * 10 - 20, left: 6 * 10, right: 0, height: 1 * 10} + {top: 3 * 10 - 20, left: 0, right: 0, height: 1 * 10} + {top: 4 * 10 - 20, left: 0, width: 6 * 10, height: 1 * 10} ] } expectValues stateForHighlight(presenter, highlight5), { class: 'e' regions: [ - {top: 3 * 10, left: 6 * 10, right: 0, height: 1 * 10} - {top: 4 * 10, left: 0 * 10, right: 0, height: 2 * 10} + {top: 3 * 10 - 20, left: 6 * 10, right: 0, height: 1 * 10} + {top: 4 * 10 - 20, left: 0 * 10, right: 0, height: 2 * 10} ] } expectValues stateForHighlight(presenter, highlight6), { class: 'f' regions: [ - {top: 5 * 10, left: 6 * 10, right: 0, height: 1 * 10} + {top: 5 * 10 - 20, left: 6 * 10, right: 0, height: 1 * 10} ] } @@ -1913,20 +1962,18 @@ describe "TextEditorPresenter", -> getLineNumberGutterState(presenter).content.lineNumbers[key] - it "contains states for line numbers that are visible on screen, plus and minus the overdraw margin", -> + it "contains states for line numbers that are visible on screen", -> editor.foldBufferRow(4) editor.setSoftWrapped(true) editor.setEditorWidthInChars(50) - presenter = buildPresenter(explicitHeight: 25, scrollTop: 30, lineHeight: 10, lineOverdrawMargin: 1) + presenter = buildPresenter(explicitHeight: 25, scrollTop: 30, lineHeight: 10) - expect(lineNumberStateForScreenRow(presenter, 1)).toBeUndefined() - expectValues lineNumberStateForScreenRow(presenter, 2), {screenRow: 2, bufferRow: 2, softWrapped: false, top: 2 * 10} + expect(lineNumberStateForScreenRow(presenter, 2)).toBeUndefined() expectValues lineNumberStateForScreenRow(presenter, 3), {screenRow: 3, bufferRow: 3, softWrapped: false, top: 3 * 10} expectValues lineNumberStateForScreenRow(presenter, 4), {screenRow: 4, bufferRow: 3, softWrapped: true, top: 4 * 10} expectValues lineNumberStateForScreenRow(presenter, 5), {screenRow: 5, bufferRow: 4, softWrapped: false, top: 5 * 10} expectValues lineNumberStateForScreenRow(presenter, 6), {screenRow: 6, bufferRow: 7, softWrapped: false, top: 6 * 10} - expectValues lineNumberStateForScreenRow(presenter, 7), {screenRow: 7, bufferRow: 8, softWrapped: false, top: 7 * 10} - expect(lineNumberStateForScreenRow(presenter, 8)).toBeUndefined() + expect(lineNumberStateForScreenRow(presenter, 7)).toBeUndefined() it "includes states for all line numbers if no ::explicitHeight is assigned", -> presenter = buildPresenter(explicitHeight: null) @@ -1937,43 +1984,43 @@ describe "TextEditorPresenter", -> editor.foldBufferRow(4) editor.setSoftWrapped(true) editor.setEditorWidthInChars(50) - presenter = buildPresenter(explicitHeight: 25, scrollTop: 30, lineOverdrawMargin: 1) + presenter = buildPresenter(explicitHeight: 25, scrollTop: 30) - expect(lineNumberStateForScreenRow(presenter, 1)).toBeUndefined() - expectValues lineNumberStateForScreenRow(presenter, 2), {bufferRow: 2} - expectValues lineNumberStateForScreenRow(presenter, 7), {bufferRow: 8} - expect(lineNumberStateForScreenRow(presenter, 8)).toBeUndefined() + expect(lineNumberStateForScreenRow(presenter, 2)).toBeUndefined() + expectValues lineNumberStateForScreenRow(presenter, 3), {bufferRow: 3} + expectValues lineNumberStateForScreenRow(presenter, 6), {bufferRow: 7} + expect(lineNumberStateForScreenRow(presenter, 7)).toBeUndefined() expectStateUpdate presenter, -> presenter.setScrollTop(20) - expect(lineNumberStateForScreenRow(presenter, 0)).toBeUndefined() - expectValues lineNumberStateForScreenRow(presenter, 1), {bufferRow: 1} - expectValues lineNumberStateForScreenRow(presenter, 6), {bufferRow: 7} - expect(lineNumberStateForScreenRow(presenter, 7)).toBeUndefined() + expect(lineNumberStateForScreenRow(presenter, 1)).toBeUndefined() + expectValues lineNumberStateForScreenRow(presenter, 2), {bufferRow: 2} + expectValues lineNumberStateForScreenRow(presenter, 5), {bufferRow: 4} + expect(lineNumberStateForScreenRow(presenter, 6)).toBeUndefined() it "updates when ::explicitHeight changes", -> editor.foldBufferRow(4) editor.setSoftWrapped(true) editor.setEditorWidthInChars(50) - presenter = buildPresenter(explicitHeight: 25, scrollTop: 30, lineOverdrawMargin: 1) + presenter = buildPresenter(explicitHeight: 25, scrollTop: 30) expect(lineNumberStateForScreenRow(presenter, 1)).toBeUndefined() - expectValues lineNumberStateForScreenRow(presenter, 2), {bufferRow: 2} - expectValues lineNumberStateForScreenRow(presenter, 7), {bufferRow: 8} - expect(lineNumberStateForScreenRow(presenter, 8)).toBeUndefined() + expectValues lineNumberStateForScreenRow(presenter, 3), {bufferRow: 3} + expectValues lineNumberStateForScreenRow(presenter, 6), {bufferRow: 7} + expect(lineNumberStateForScreenRow(presenter, 7)).toBeUndefined() expectStateUpdate presenter, -> presenter.setExplicitHeight(35) expect(lineNumberStateForScreenRow(presenter, 0)).toBeUndefined() - expectValues lineNumberStateForScreenRow(presenter, 2), {bufferRow: 2} - expectValues lineNumberStateForScreenRow(presenter, 8), {bufferRow: 8} - expect(lineNumberStateForScreenRow(presenter, 9)).toBeUndefined() + expectValues lineNumberStateForScreenRow(presenter, 3), {bufferRow: 3} + expectValues lineNumberStateForScreenRow(presenter, 7), {bufferRow: 8} + expect(lineNumberStateForScreenRow(presenter, 8)).toBeUndefined() it "updates when ::lineHeight changes", -> editor.foldBufferRow(4) editor.setSoftWrapped(true) editor.setEditorWidthInChars(50) - presenter = buildPresenter(explicitHeight: 25, scrollTop: 0, lineOverdrawMargin: 0) + presenter = buildPresenter(explicitHeight: 25, scrollTop: 0) expectValues lineNumberStateForScreenRow(presenter, 0), {bufferRow: 0} expectValues lineNumberStateForScreenRow(presenter, 3), {bufferRow: 3} @@ -1989,7 +2036,7 @@ describe "TextEditorPresenter", -> editor.foldBufferRow(4) editor.setSoftWrapped(true) editor.setEditorWidthInChars(50) - presenter = buildPresenter(explicitHeight: 35, scrollTop: 30, lineOverdrawMargin: 0) + presenter = buildPresenter(explicitHeight: 35, scrollTop: 30) expect(lineNumberStateForScreenRow(presenter, 2)).toBeUndefined() expectValues lineNumberStateForScreenRow(presenter, 3), {bufferRow: 3} @@ -2011,26 +2058,26 @@ describe "TextEditorPresenter", -> expect(lineNumberStateForScreenRow(presenter, 8)).toBeUndefined() it "does not remove out-of-view line numbers corresponding to ::mouseWheelScreenRow until ::stoppedScrollingDelay elapses", -> - presenter = buildPresenter(explicitHeight: 25, lineOverdrawMargin: 1, stoppedScrollingDelay: 200) + presenter = buildPresenter(explicitHeight: 25, stoppedScrollingDelay: 200) expect(lineNumberStateForScreenRow(presenter, 0)).toBeDefined() - expect(lineNumberStateForScreenRow(presenter, 4)).toBeDefined() - expect(lineNumberStateForScreenRow(presenter, 5)).toBeUndefined() + expect(lineNumberStateForScreenRow(presenter, 3)).toBeDefined() + expect(lineNumberStateForScreenRow(presenter, 4)).toBeUndefined() presenter.setMouseWheelScreenRow(0) expectStateUpdate presenter, -> presenter.setScrollTop(35) expect(lineNumberStateForScreenRow(presenter, 0)).toBeDefined() expect(lineNumberStateForScreenRow(presenter, 1)).toBeUndefined() - expect(lineNumberStateForScreenRow(presenter, 7)).toBeDefined() - expect(lineNumberStateForScreenRow(presenter, 8)).toBeUndefined() + expect(lineNumberStateForScreenRow(presenter, 6)).toBeDefined() + expect(lineNumberStateForScreenRow(presenter, 7)).toBeUndefined() expectStateUpdate presenter, -> advanceClock(200) expect(lineNumberStateForScreenRow(presenter, 0)).toBeUndefined() expect(lineNumberStateForScreenRow(presenter, 1)).toBeUndefined() - expect(lineNumberStateForScreenRow(presenter, 7)).toBeDefined() - expect(lineNumberStateForScreenRow(presenter, 8)).toBeUndefined() + expect(lineNumberStateForScreenRow(presenter, 6)).toBeDefined() + expect(lineNumberStateForScreenRow(presenter, 7)).toBeUndefined() it "correctly handles the first screen line being soft-wrapped", -> editor.setSoftWrapped(true) @@ -2213,12 +2260,11 @@ describe "TextEditorPresenter", -> scrollTop = 0 lineHeight = 10 explicitHeight = lineHeight * 10 - lineOverdrawMargin = 1 beforeEach -> # At the beginning of each test, decoration1 and decoration2 are in visible range, # but not decoration3. - presenter = buildPresenter({explicitHeight, scrollTop, lineHeight, lineOverdrawMargin}) + presenter = buildPresenter({explicitHeight, scrollTop, lineHeight}) gutter = editor.addGutter({name: 'test-gutter', visible: true}) decorationItem = document.createElement('div') decorationItem.class = 'decoration-item' @@ -2589,7 +2635,7 @@ describe "TextEditorPresenter", -> editor.setEditorWidthInChars(80) presenterParams = model: editor - lineOverdrawMargin: 1 + presenter = new TextEditorPresenter(presenterParams) statements = [] diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index 28dbaaff8..8ad1ce390 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -1,3 +1,6 @@ +fs = require 'fs-plus' +path = require 'path' +temp = require 'temp' clipboard = require '../src/safe-clipboard' TextEditor = require '../src/text-editor' @@ -19,6 +22,17 @@ describe "TextEditor", -> atom.packages.activatePackage('language-javascript') describe "when the editor is deserialized", -> + it "returns undefined when the path cannot be read", -> + pathToOpen = path.join(temp.mkdirSync(), 'file.txt') + editor1 = null + + waitsForPromise -> + atom.project.open(pathToOpen).then (o) -> editor1 = o + + runs -> + fs.mkdirSync(pathToOpen) + expect(editor1.testSerialization()).toBeUndefined() + it "restores selections and folds based on markers in the buffer", -> editor.setSelectedBufferRange([[1, 2], [3, 4]]) editor.addSelectionForBufferRange([[5, 6], [7, 5]], reversed: true) diff --git a/spec/window-spec.coffee b/spec/window-spec.coffee index 376a71ab3..94e605a36 100644 --- a/spec/window-spec.coffee +++ b/spec/window-spec.coffee @@ -293,3 +293,14 @@ describe "Window", -> pathToOpen = __dirname atom.getCurrentWindow().send 'message', 'open-locations', [{pathToOpen}] expect(atom.workspace.open.callCount).toBe 0 + + describe "when the opened path is a uri", -> + it "adds it to the project's paths as is", -> + pathToOpen = 'remote://server:7644/some/dir/path' + atom.getCurrentWindow().send 'message', 'open-locations', [{pathToOpen}] + + waitsFor -> + atom.project.getPaths().length is 1 + + runs -> + expect(atom.project.getPaths()[0]).toBe pathToOpen diff --git a/src/browser/atom-application.coffee b/src/browser/atom-application.coffee index ae56fb1d4..fb038c07c 100644 --- a/src/browser/atom-application.coffee +++ b/src/browser/atom-application.coffee @@ -370,7 +370,12 @@ class AtomApplication # :windowDimensions - Object with height and width keys. # :window - {AtomWindow} to open file paths in. openPaths: ({pathsToOpen, pidToKillWhenClosed, newWindow, devMode, safeMode, apiPreviewMode, windowDimensions, profileStartup, window}={}) -> - pathsToOpen = (fs.normalize(pathToOpen) for pathToOpen in pathsToOpen) + pathsToOpen = pathsToOpen.map (pathToOpen) -> + if fs.existsSync(pathToOpen) + fs.normalize(pathToOpen) + else + pathToOpen + locationsToOpen = (@locationForPathToOpen(pathToOpen) for pathToOpen in pathsToOpen) unless pidToKillWhenClosed or newWindow @@ -522,6 +527,7 @@ class AtomApplication locationForPathToOpen: (pathToOpen) -> return {pathToOpen} unless pathToOpen + return {pathToOpen} if url.parse(pathToOpen).protocol? return {pathToOpen} if fs.existsSync(pathToOpen) pathToOpen = pathToOpen.replace(/[:\s]+$/, '') diff --git a/src/browser/main.coffee b/src/browser/main.coffee index 1af0526dd..371dd71ea 100644 --- a/src/browser/main.coffee +++ b/src/browser/main.coffee @@ -5,6 +5,7 @@ app = require 'app' fs = require 'fs-plus' path = require 'path' yargs = require 'yargs' +url = require 'url' nslog = require 'nslog' console.log = nslog @@ -45,9 +46,11 @@ start = -> cwd = args.executedFrom?.toString() or process.cwd() args.pathsToOpen = args.pathsToOpen.map (pathToOpen) -> - pathToOpen = fs.normalize(pathToOpen) - if cwd - path.resolve(cwd, pathToOpen) + normalizedPath = fs.normalize(pathToOpen) + if url.parse(pathToOpen).protocol? + pathToOpen + else if cwd + path.resolve(cwd, normalizedPath) else path.resolve(pathToOpen) diff --git a/src/command-registry.coffee b/src/command-registry.coffee index 3969dc283..72098eda0 100644 --- a/src/command-registry.coffee +++ b/src/command-registry.coffee @@ -18,6 +18,12 @@ SequenceCount = 0 # command event listeners globally on `atom.commands` and constrain them to # specific kinds of elements with CSS selectors. # +# Command names must follow the `namespace:action` pattern, where `namespace` +# will typically be the name of your package, and `action` describes the +# behavior of your command. If either part consists of multiple words, these +# must be separated by hyphens. E.g. `awesome-package:turn-it-up-to-eleven`. +# All words should be lowercased. +# # As the event bubbles upward through the DOM, all registered event listeners # with matching selectors are invoked in order of specificity. In the event of a # specificity tie, the most recently registered listener is invoked first. This diff --git a/src/config-schema.coffee b/src/config-schema.coffee index b814e21de..fd35d2b07 100644 --- a/src/config-schema.coffee +++ b/src/config-schema.coffee @@ -179,15 +179,19 @@ module.exports = eol: type: ['boolean', 'string'] default: '\u00ac' + maximumLength: 1 space: type: ['boolean', 'string'] default: '\u00b7' + maximumLength: 1 tab: type: ['boolean', 'string'] default: '\u00bb' + maximumLength: 1 cr: type: ['boolean', 'string'] default: '\u00a4' + maximumLength: 1 zoomFontWhenCtrlScrolling: type: 'boolean' default: process.platform isnt 'darwin' diff --git a/src/config.coffee b/src/config.coffee index b97ce99ec..f781e02e9 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -1060,6 +1060,12 @@ Config.addSchemaEnforcers throw new Error("Validation failed at #{keyPath}, #{JSON.stringify(value)} must be a string") value + validateMaximumLength: (keyPath, value, schema) -> + if typeof schema.maximumLength is 'number' and value.length > schema.maximumLength + value.slice(0, schema.maximumLength) + else + value + 'null': # null sort of isnt supported. It will just unset in this case coerce: (keyPath, value, schema) -> @@ -1152,6 +1158,10 @@ withoutEmptyObjects = (object) -> resultObject = object resultObject +# TODO remove in 1.0 API +Config::unobserve = (keyPath) -> + Grim.deprecate 'Config::unobserve no longer does anything. Call `.dispose()` on the object returned by Config::observe instead.' + if Grim.includeDeprecatedAPIs EmitterMixin = require('emissary').Emitter EmitterMixin.includeInto(Config) @@ -1201,9 +1211,6 @@ if Grim.includeDeprecatedAPIs Grim.deprecate 'Config::toggle is no longer supported. Please remove from your code.' @set(keyPath, not @get(keyPath)) - Config::unobserve = (keyPath) -> - Grim.deprecate 'Config::unobserve no longer does anything. Call `.dispose()` on the object returned by Config::observe instead.' - Config::addScopedSettings = (source, selector, value, options) -> Grim.deprecate("Use ::set instead") settingsBySelector = {} diff --git a/src/context-menu-manager.coffee b/src/context-menu-manager.coffee index 0fdcf4ab6..73bcbf440 100644 --- a/src/context-menu-manager.coffee +++ b/src/context-menu-manager.coffee @@ -100,6 +100,9 @@ class ContextMenuManager # whether to display this item on a given context menu deployment. Called # with the following argument: # * `event` The click event that deployed the context menu. + # + # Returns a {Disposable} on which `.dispose()` can be called to remove the + # added menu items. add: (itemsBySelector) -> if Grim.includeDeprecatedAPIs # Detect deprecated file path as first argument diff --git a/src/default-directory-provider.coffee b/src/default-directory-provider.coffee index 9e25e097b..da2d17593 100644 --- a/src/default-directory-provider.coffee +++ b/src/default-directory-provider.coffee @@ -1,6 +1,7 @@ {Directory} = require 'pathwatcher' fs = require 'fs-plus' path = require 'path' +url = require 'url' module.exports = class DefaultDirectoryProvider @@ -14,14 +15,22 @@ class DefaultDirectoryProvider # * {Directory} if the given URI is compatible with this provider. # * `null` if the given URI is not compatibile with this provider. directoryForURISync: (uri) -> - projectPath = path.normalize(uri) - - directoryPath = if not fs.isDirectorySync(projectPath) and fs.isDirectorySync(path.dirname(projectPath)) - path.dirname(projectPath) + normalizedPath = path.normalize(uri) + {protocol} = url.parse(uri) + directoryPath = if protocol? + uri + else if not fs.isDirectorySync(normalizedPath) and fs.isDirectorySync(path.dirname(normalizedPath)) + path.dirname(normalizedPath) else - projectPath + normalizedPath - new Directory(directoryPath) + # TODO: Stop normalizing the path in pathwatcher's Directory. + directory = new Directory(directoryPath) + if protocol? + directory.path = directoryPath + if fs.isCaseInsensitive() + directory.lowerCasePath = directoryPath.toLowerCase() + directory # Public: Create a Directory that corresponds to the specified URI. # diff --git a/src/lines-component.coffee b/src/lines-component.coffee index 17c904e99..23f1c0015 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -1,16 +1,10 @@ -_ = require 'underscore-plus' -{toArray} = require 'underscore-plus' {$$} = require 'space-pen' CursorsComponent = require './cursors-component' HighlightsComponent = require './highlights-component' -TokenIterator = require './token-iterator' +TileComponent = require './tile-component' DummyLineNode = $$(-> @div className: 'line', style: 'position: absolute; visibility: hidden;', => @span 'x')[0] -AcceptFilter = {acceptNode: -> NodeFilter.FILTER_ACCEPT} -WrapperDiv = document.createElement('div') -TokenTextEscapeRegex = /[&"'<>]/g -MaxTokenLength = 20000 cloneObject = (object) -> clone = {} @@ -22,12 +16,7 @@ class LinesComponent placeholderTextDiv: null constructor: ({@presenter, @hostElement, @useShadowDOM, visible}) -> - @tokenIterator = new TokenIterator - @measuredLines = new Set - @lineNodesByLineId = {} - @screenRowsByLineId = {} - @lineIdsByScreenRow = {} - @renderedDecorationsByLineId = {} + @tileComponentsByTileId = {} @domNode = document.createElement('div') @domNode.classList.add('lines') @@ -48,17 +37,12 @@ class LinesComponent updateSync: (state) -> @newState = state.content - @oldState ?= {lines: {}} + @oldState ?= {tiles: {}} if @newState.scrollHeight isnt @oldState.scrollHeight @domNode.style.height = @newState.scrollHeight + 'px' @oldState.scrollHeight = @newState.scrollHeight - if @newState.scrollTop isnt @oldState.scrollTop or @newState.scrollLeft isnt @oldState.scrollLeft - @domNode.style['-webkit-transform'] = "translate3d(#{-@newState.scrollLeft}px, #{-@newState.scrollTop}px, 0px)" - @oldState.scrollTop = @newState.scrollTop - @oldState.scrollLeft = @newState.scrollLeft - if @newState.backgroundColor isnt @oldState.backgroundColor @domNode.style.backgroundColor = @newState.backgroundColor @oldState.backgroundColor = @newState.backgroundColor @@ -71,8 +55,8 @@ class LinesComponent @placeholderTextDiv.textContent = @newState.placeholderText @domNode.appendChild(@placeholderTextDiv) - @removeLineNodes() unless @oldState.indentGuidesVisible is @newState.indentGuidesVisible - @updateLineNodes() + @removeTileNodes() unless @oldState.indentGuidesVisible is @newState.indentGuidesVisible + @updateTileNodes() if @newState.scrollWidth isnt @oldState.scrollWidth @domNode.style.width = @newState.scrollWidth + 'px' @@ -84,248 +68,35 @@ class LinesComponent @oldState.indentGuidesVisible = @newState.indentGuidesVisible @oldState.scrollWidth = @newState.scrollWidth - removeLineNodes: -> - @removeLineNode(id) for id of @oldState.lines + removeTileNodes: -> + @removeTileNode(id) for id of @oldState.tiles return - removeLineNode: (id) -> - @lineNodesByLineId[id].remove() - delete @lineNodesByLineId[id] - delete @lineIdsByScreenRow[@screenRowsByLineId[id]] - delete @screenRowsByLineId[id] - delete @oldState.lines[id] + removeTileNode: (id) -> + node = @tileComponentsByTileId[id].getDomNode() - updateLineNodes: -> - for id of @oldState.lines - unless @newState.lines.hasOwnProperty(id) - @removeLineNode(id) + node.remove() + delete @tileComponentsByTileId[id] + delete @oldState.tiles[id] - newLineIds = null - newLinesHTML = null + updateTileNodes: -> + for id of @oldState.tiles + unless @newState.tiles.hasOwnProperty(id) + @removeTileNode(id) - for id, lineState of @newState.lines - if @oldState.lines.hasOwnProperty(id) - @updateLineNode(id) + for id, tileState of @newState.tiles + if @oldState.tiles.hasOwnProperty(id) + tileComponent = @tileComponentsByTileId[id] else - newLineIds ?= [] - newLinesHTML ?= "" - newLineIds.push(id) - newLinesHTML += @buildLineHTML(id) - @screenRowsByLineId[id] = lineState.screenRow - @lineIdsByScreenRow[lineState.screenRow] = id - @oldState.lines[id] = cloneObject(lineState) + tileComponent = @tileComponentsByTileId[id] = new TileComponent({id, @presenter}) - return unless newLineIds? + @domNode.appendChild(tileComponent.getDomNode()) + @oldState.tiles[id] = cloneObject(tileState) - WrapperDiv.innerHTML = newLinesHTML - newLineNodes = _.toArray(WrapperDiv.children) - for id, i in newLineIds - lineNode = newLineNodes[i] - @lineNodesByLineId[id] = lineNode - @domNode.appendChild(lineNode) + tileComponent.updateSync(@newState) return - buildLineHTML: (id) -> - {scrollWidth} = @newState - {screenRow, tokens, text, top, lineEnding, fold, isSoftWrapped, indentLevel, decorationClasses} = @newState.lines[id] - - classes = '' - if decorationClasses? - for decorationClass in decorationClasses - classes += decorationClass + ' ' - classes += 'line' - - lineHTML = "
" - - if text is "" - lineHTML += @buildEmptyLineInnerHTML(id) - else - lineHTML += @buildLineInnerHTML(id) - - lineHTML += '' if fold - lineHTML += "
" - lineHTML - - buildEmptyLineInnerHTML: (id) -> - {indentGuidesVisible} = @newState - {indentLevel, tabLength, endOfLineInvisibles} = @newState.lines[id] - - if indentGuidesVisible and indentLevel > 0 - invisibleIndex = 0 - lineHTML = '' - for i in [0...indentLevel] - lineHTML += "" - for j in [0...tabLength] - if invisible = endOfLineInvisibles?[invisibleIndex++] - lineHTML += "#{invisible}" - else - lineHTML += ' ' - lineHTML += "" - - while invisibleIndex < endOfLineInvisibles?.length - lineHTML += "#{endOfLineInvisibles[invisibleIndex++]}" - - lineHTML - else - @buildEndOfLineHTML(id) or ' ' - - buildLineInnerHTML: (id) -> - lineState = @newState.lines[id] - {firstNonWhitespaceIndex, firstTrailingWhitespaceIndex, invisibles} = lineState - lineIsWhitespaceOnly = firstTrailingWhitespaceIndex is 0 - - innerHTML = "" - @tokenIterator.reset(lineState) - - while @tokenIterator.next() - for scope in @tokenIterator.getScopeEnds() - innerHTML += "" - - for scope in @tokenIterator.getScopeStarts() - innerHTML += "" - - tokenStart = @tokenIterator.getScreenStart() - tokenEnd = @tokenIterator.getScreenEnd() - tokenText = @tokenIterator.getText() - isHardTab = @tokenIterator.isHardTab() - - if hasLeadingWhitespace = tokenStart < firstNonWhitespaceIndex - tokenFirstNonWhitespaceIndex = firstNonWhitespaceIndex - tokenStart - else - tokenFirstNonWhitespaceIndex = null - - if hasTrailingWhitespace = tokenEnd > firstTrailingWhitespaceIndex - tokenFirstTrailingWhitespaceIndex = Math.max(0, firstTrailingWhitespaceIndex - tokenStart) - else - tokenFirstTrailingWhitespaceIndex = null - - hasIndentGuide = - @newState.indentGuidesVisible and - (hasLeadingWhitespace or lineIsWhitespaceOnly) - - hasInvisibleCharacters = - (invisibles?.tab and isHardTab) or - (invisibles?.space and (hasLeadingWhitespace or hasTrailingWhitespace)) - - innerHTML += @buildTokenHTML(tokenText, isHardTab, tokenFirstNonWhitespaceIndex, tokenFirstTrailingWhitespaceIndex, hasIndentGuide, hasInvisibleCharacters) - - for scope in @tokenIterator.getScopeEnds() - innerHTML += "" - - for scope in @tokenIterator.getScopes() - innerHTML += "" - - innerHTML += @buildEndOfLineHTML(id) - innerHTML - - buildTokenHTML: (tokenText, isHardTab, firstNonWhitespaceIndex, firstTrailingWhitespaceIndex, hasIndentGuide, hasInvisibleCharacters) -> - if isHardTab - classes = 'hard-tab' - classes += ' leading-whitespace' if firstNonWhitespaceIndex? - classes += ' trailing-whitespace' if firstTrailingWhitespaceIndex? - classes += ' indent-guide' if hasIndentGuide - classes += ' invisible-character' if hasInvisibleCharacters - return "#{@escapeTokenText(tokenText)}" - else - startIndex = 0 - endIndex = tokenText.length - - leadingHtml = '' - trailingHtml = '' - - if firstNonWhitespaceIndex? - leadingWhitespace = tokenText.substring(0, firstNonWhitespaceIndex) - - classes = 'leading-whitespace' - classes += ' indent-guide' if hasIndentGuide - classes += ' invisible-character' if hasInvisibleCharacters - - leadingHtml = "#{leadingWhitespace}" - startIndex = firstNonWhitespaceIndex - - if firstTrailingWhitespaceIndex? - tokenIsOnlyWhitespace = firstTrailingWhitespaceIndex is 0 - trailingWhitespace = tokenText.substring(firstTrailingWhitespaceIndex) - - classes = 'trailing-whitespace' - classes += ' indent-guide' if hasIndentGuide and not firstNonWhitespaceIndex? and tokenIsOnlyWhitespace - classes += ' invisible-character' if hasInvisibleCharacters - - trailingHtml = "#{trailingWhitespace}" - - endIndex = firstTrailingWhitespaceIndex - - html = leadingHtml - if tokenText.length > MaxTokenLength - while startIndex < endIndex - html += "" + @escapeTokenText(tokenText, startIndex, startIndex + MaxTokenLength) + "" - startIndex += MaxTokenLength - else - html += @escapeTokenText(tokenText, startIndex, endIndex) - - html += trailingHtml - html - - escapeTokenText: (tokenText, startIndex, endIndex) -> - if startIndex? and endIndex? and startIndex > 0 or endIndex < tokenText.length - tokenText = tokenText.slice(startIndex, endIndex) - tokenText.replace(TokenTextEscapeRegex, @escapeTokenTextReplace) - - escapeTokenTextReplace: (match) -> - switch match - when '&' then '&' - when '"' then '"' - when "'" then ''' - when '<' then '<' - when '>' then '>' - else match - - buildEndOfLineHTML: (id) -> - {endOfLineInvisibles} = @newState.lines[id] - - html = '' - if endOfLineInvisibles? - for invisible in endOfLineInvisibles - html += "#{invisible}" - html - - updateLineNode: (id) -> - oldLineState = @oldState.lines[id] - newLineState = @newState.lines[id] - - lineNode = @lineNodesByLineId[id] - - if @newState.scrollWidth isnt @oldState.scrollWidth - lineNode.style.width = @newState.scrollWidth + 'px' - - newDecorationClasses = newLineState.decorationClasses - oldDecorationClasses = oldLineState.decorationClasses - - if oldDecorationClasses? - for decorationClass in oldDecorationClasses - unless newDecorationClasses? and decorationClass in newDecorationClasses - lineNode.classList.remove(decorationClass) - - if newDecorationClasses? - for decorationClass in newDecorationClasses - unless oldDecorationClasses? and decorationClass in oldDecorationClasses - lineNode.classList.add(decorationClass) - - oldLineState.decorationClasses = newLineState.decorationClasses - - if newLineState.top isnt oldLineState.top - lineNode.style.top = newLineState.top + 'px' - oldLineState.top = newLineState.cop - - if newLineState.screenRow isnt oldLineState.screenRow - lineNode.dataset.screenRow = newLineState.screenRow - oldLineState.screenRow = newLineState.screenRow - @lineIdsByScreenRow[newLineState.screenRow] = id - - lineNodeForScreenRow: (screenRow) -> - @lineNodesByLineId[@lineIdsByScreenRow[screenRow]] - measureLineHeightAndDefaultCharWidth: -> @domNode.appendChild(DummyLineNode) lineHeightInPixels = DummyLineNode.getBoundingClientRect().height @@ -343,59 +114,18 @@ class LinesComponent measureCharactersInNewLines: -> @presenter.batchCharacterMeasurement => - for id, lineState of @oldState.lines - unless @measuredLines.has(id) - lineNode = @lineNodesByLineId[id] - @measureCharactersInLine(id, lineState, lineNode) + for id, component of @tileComponentsByTileId + component.measureCharactersInNewLines() + return - measureCharactersInLine: (lineId, tokenizedLine, lineNode) -> - rangeForMeasurement = null - iterator = null - charIndex = 0 - - @tokenIterator.reset(tokenizedLine) - while @tokenIterator.next() - scopes = @tokenIterator.getScopes() - text = @tokenIterator.getText() - charWidths = @presenter.getScopedCharacterWidths(scopes) - - textIndex = 0 - while textIndex < text.length - if @tokenIterator.isPairedCharacter() - char = text - charLength = 2 - textIndex += 2 - else - char = text[textIndex] - charLength = 1 - textIndex++ - - continue if char is '\0' - - unless charWidths[char]? - unless textNode? - rangeForMeasurement ?= document.createRange() - iterator = document.createNodeIterator(lineNode, NodeFilter.SHOW_TEXT, AcceptFilter) - textNode = iterator.nextNode() - textNodeIndex = 0 - nextTextNodeIndex = textNode.textContent.length - - while nextTextNodeIndex <= charIndex - textNode = iterator.nextNode() - textNodeIndex = nextTextNodeIndex - nextTextNodeIndex = textNodeIndex + textNode.textContent.length - - i = charIndex - textNodeIndex - rangeForMeasurement.setStart(textNode, i) - rangeForMeasurement.setEnd(textNode, i + charLength) - charWidth = rangeForMeasurement.getBoundingClientRect().width - @presenter.setScopedCharacterWidth(scopes, char, charWidth) - - charIndex += charLength - - @measuredLines.add(lineId) - clearScopedCharWidths: -> - @measuredLines.clear() + for id, component of @tileComponentsByTileId + component.clearMeasurements() + @presenter.clearScopedCharacterWidths() + + lineNodeForScreenRow: (screenRow) -> + tile = @presenter.tileForRow(screenRow) + + @tileComponentsByTileId[tile]?.lineNodeForScreenRow(screenRow) diff --git a/src/package.coffee b/src/package.coffee index 65c7af822..b404f99e8 100644 --- a/src/package.coffee +++ b/src/package.coffee @@ -170,7 +170,7 @@ class Package if @mainModule? if @mainModule.config? and typeof @mainModule.config is 'object' atom.config.setSchema @name, {type: 'object', properties: @mainModule.config} - else if includeDeprecatedAPIs and @mainModule.configDefaults? and typeof @mainModule.configDefaults is 'object' + else if @mainModule.configDefaults? and typeof @mainModule.configDefaults is 'object' deprecate("""Use a config schema instead. See the configuration section of https://atom.io/docs/latest/hacking-atom-package-word-count and https://atom.io/docs/api/latest/Config for more details""", {packageName: @name}) @@ -203,16 +203,15 @@ class Package try itemsBySelector = map['context-menu'] - if includeDeprecatedAPIs - # Detect deprecated format for items object - for key, value of itemsBySelector - unless _.isArray(value) - deprecate(""" - The context menu CSON format has changed. Please see - https://atom.io/docs/api/latest/ContextMenuManager#context-menu-cson-format - for more info. - """, {packageName: @name}) - itemsBySelector = atom.contextMenu.convertLegacyItemsBySelector(itemsBySelector) + # Detect deprecated format for items object + for key, value of itemsBySelector + unless _.isArray(value) + deprecate(""" + The context menu CSON format has changed. Please see + https://atom.io/docs/api/latest/ContextMenuManager#context-menu-cson-format + for more info. + """, {packageName: @name}) + itemsBySelector = atom.contextMenu.convertLegacyItemsBySelector(itemsBySelector) @activationDisposables.add(atom.contextMenu.add(itemsBySelector)) catch error @@ -277,7 +276,7 @@ class Package [stylesheetPath, atom.themes.loadStylesheet(stylesheetPath, true)] getStylesheetsPath: -> - if includeDeprecatedAPIs and fs.isDirectorySync(path.join(@path, 'stylesheets')) + if fs.isDirectorySync(path.join(@path, 'stylesheets')) deprecate("Store package style sheets in the `styles/` directory instead of `stylesheets/` in the `#{@name}` package", packageName: @name) path.join(@path, 'stylesheets') else @@ -353,7 +352,7 @@ class Package deferred = Q.defer() - if includeDeprecatedAPIs and fs.isDirectorySync(path.join(@path, 'scoped-properties')) + if fs.isDirectorySync(path.join(@path, 'scoped-properties')) settingsDirPath = path.join(@path, 'scoped-properties') deprecate("Store package settings files in the `settings/` directory instead of `scoped-properties/`", packageName: @name) else @@ -489,7 +488,7 @@ class Package else if _.isArray(commands) @activationCommands[selector].push(commands...) - if includeDeprecatedAPIs and @metadata.activationEvents? + if @metadata.activationEvents? deprecate(""" Use `activationCommands` instead of `activationEvents` in your package.json Commands should be grouped by selector as follows: diff --git a/src/task.coffee b/src/task.coffee index d752ea11d..939b71635 100644 --- a/src/task.coffee +++ b/src/task.coffee @@ -100,11 +100,12 @@ class Task @childProcess.removeAllListeners() @childProcess.on 'message', ({event, args}) => @emit(event, args...) if @childProcess? + # Catch the errors that happened before task-bootstrap. - @childProcess.stdout.on 'data', (data) -> - console.log data.toString() - @childProcess.stderr.on 'data', (data) -> - console.error data.toString() + @childProcess.stdout.removeAllListeners() + @childProcess.stdout.on 'data', (data) -> console.log data.toString() + @childProcess.stderr.removeAllListeners() + @childProcess.stderr.on 'data', (data) -> console.error data.toString() # Public: Starts the task. # @@ -152,6 +153,8 @@ class Task return unless @childProcess? @childProcess.removeAllListeners() + @childProcess.stdout.removeAllListeners() + @childProcess.stderr.removeAllListeners() @childProcess.kill() @childProcess = null diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index 45a8a9720..ac71da748 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -18,7 +18,7 @@ class TextEditorComponent scrollSensitivity: 0.4 cursorBlinkPeriod: 800 cursorBlinkResumeDelay: 100 - lineOverdrawMargin: 15 + tileSize: 12 pendingScrollTop: null pendingScrollLeft: null @@ -36,8 +36,8 @@ class TextEditorComponent gutterComponent: null mounted: true - constructor: ({@editor, @hostElement, @rootElement, @stylesElement, @useShadowDOM, lineOverdrawMargin}) -> - @lineOverdrawMargin = lineOverdrawMargin if lineOverdrawMargin? + constructor: ({@editor, @hostElement, @rootElement, @stylesElement, @useShadowDOM, tileSize}) -> + @tileSize = tileSize if tileSize? @disposables = new CompositeDisposable @observeConfig() @@ -47,7 +47,7 @@ class TextEditorComponent model: @editor scrollTop: @editor.getScrollTop() scrollLeft: @editor.getScrollLeft() - lineOverdrawMargin: @lineOverdrawMargin + tileSize: tileSize cursorBlinkPeriod: @cursorBlinkPeriod cursorBlinkResumeDelay: @cursorBlinkResumeDelay stoppedScrollingDelay: 200 @@ -768,8 +768,8 @@ class TextEditorComponent {clientX, clientY} = event linesClientRect = @linesComponent.getDomNode().getBoundingClientRect() - top = clientY - linesClientRect.top - left = clientX - linesClientRect.left + top = clientY - linesClientRect.top + @presenter.scrollTop + left = clientX - linesClientRect.left + @presenter.scrollLeft {top, left} getModel: -> diff --git a/src/text-editor-element.coffee b/src/text-editor-element.coffee index beeae6cc6..e3eaaeb2a 100644 --- a/src/text-editor-element.coffee +++ b/src/text-editor-element.coffee @@ -15,8 +15,9 @@ class TextEditorElement extends HTMLElement componentDescriptor: null component: null attached: false - lineOverdrawMargin: null + tileSize: null focusOnAttach: false + hasTiledRendering: true createdCallback: -> @emitter = new Emitter @@ -110,7 +111,7 @@ class TextEditorElement extends HTMLElement rootElement: @rootElement stylesElement: @stylesElement editor: @model - lineOverdrawMargin: @lineOverdrawMargin + tileSize: @tileSize useShadowDOM: @useShadowDOM ) @rootElement.appendChild(@component.getDomNode()) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index e3e3ad5a7..c2a1d88a5 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -15,11 +15,12 @@ class TextEditorPresenter constructor: (params) -> {@model, @autoHeight, @explicitHeight, @contentFrameWidth, @scrollTop, @scrollLeft, @boundingClientRect, @windowWidth, @windowHeight, @gutterWidth} = params {horizontalScrollbarHeight, verticalScrollbarWidth} = params - {@lineHeight, @baseCharacterWidth, @lineOverdrawMargin, @backgroundColor, @gutterBackgroundColor} = params + {@lineHeight, @baseCharacterWidth, @backgroundColor, @gutterBackgroundColor, @tileSize} = params {@cursorBlinkPeriod, @cursorBlinkResumeDelay, @stoppedScrollingDelay, @focused} = params @measuredHorizontalScrollbarHeight = horizontalScrollbarHeight @measuredVerticalScrollbarWidth = verticalScrollbarWidth @gutterWidth ?= 0 + @tileSize ?= 12 @disposables = new CompositeDisposable @emitter = new Emitter @@ -79,7 +80,7 @@ class TextEditorPresenter @updateHiddenInputState() if @shouldUpdateHiddenInputState @updateContentState() if @shouldUpdateContentState @updateDecorations() if @shouldUpdateDecorations - @updateLinesState() if @shouldUpdateLinesState + @updateTilesState() if @shouldUpdateTilesState @updateCursorsState() if @shouldUpdateCursorsState @updateOverlaysState() if @shouldUpdateOverlaysState @updateLineNumberGutterState() if @shouldUpdateLineNumberGutterState @@ -101,7 +102,7 @@ class TextEditorPresenter @shouldUpdateHiddenInputState = false @shouldUpdateContentState = false @shouldUpdateDecorations = false - @shouldUpdateLinesState = false + @shouldUpdateTilesState = false @shouldUpdateCursorsState = false @shouldUpdateOverlaysState = false @shouldUpdateLineNumberGutterState = false @@ -119,7 +120,8 @@ class TextEditorPresenter @shouldUpdateScrollbarsState = true @shouldUpdateContentState = true @shouldUpdateDecorations = true - @shouldUpdateLinesState = true + @shouldUpdateCursorsState = true + @shouldUpdateTilesState = true @shouldUpdateLineNumberGutterState = true @shouldUpdateLineNumbersState = true @shouldUpdateGutterOrderState = true @@ -139,7 +141,7 @@ class TextEditorPresenter @shouldUpdateScrollbarsState = true @shouldUpdateContentState = true @shouldUpdateDecorations = true - @shouldUpdateLinesState = true + @shouldUpdateTilesState = true @shouldUpdateLineNumberGutterState = true @shouldUpdateLineNumbersState = true @shouldUpdateGutterOrderState = true @@ -214,7 +216,7 @@ class TextEditorPresenter content: scrollingVertically: false cursorsVisible: false - lines: {} + tiles: {} highlights: {} overlays: {} gutters: [] @@ -240,7 +242,7 @@ class TextEditorPresenter @updateHiddenInputState() @updateContentState() @updateDecorations() - @updateLinesState() + @updateTilesState() @updateCursorsState() @updateOverlaysState() @updateLineNumberGutterState() @@ -291,8 +293,6 @@ class TextEditorPresenter {top, left, height, width} = @pixelRectForScreenRange(lastCursor.getScreenRange()) if @focused - top -= @scrollTop - left -= @scrollLeft @state.hiddenInput.top = Math.max(Math.min(top, @clientHeight - height), 0) @state.hiddenInput.left = Math.max(Math.min(left, @clientWidth - width), 0) else @@ -309,56 +309,84 @@ class TextEditorPresenter @state.content.backgroundColor = if @model.isMini() then null else @backgroundColor @state.content.placeholderText = if @model.isEmpty() then @model.getPlaceholderText() else null - updateLinesState: -> + tileForRow: (row) -> + row - (row % @tileSize) + + getStartTileRow: -> + Math.max(0, @tileForRow(@startRow)) + + getEndTileRow: -> + Math.min( + @tileForRow(@model.getScreenLineCount()), @tileForRow(@endRow) + ) + + updateTilesState: -> return unless @startRow? and @endRow? and @lineHeight? + visibleTiles = {} + for startRow in [@getStartTileRow()..@getEndTileRow()] by @tileSize + endRow = Math.min(@model.getScreenLineCount(), startRow + @tileSize) + + tile = @state.content.tiles[startRow] ?= {} + tile.top = startRow * @lineHeight - @scrollTop + tile.left = -@scrollLeft + tile.height = @tileSize * @lineHeight + tile.display = "block" + + @updateLinesState(tile, startRow, endRow) + + visibleTiles[startRow] = true + + if @mouseWheelScreenRow? and @model.tokenizedLineForScreenRow(@mouseWheelScreenRow)? + mouseWheelTile = @tileForRow(@mouseWheelScreenRow) + + unless visibleTiles[mouseWheelTile]? + @state.content.tiles[mouseWheelTile].display = "none" + visibleTiles[mouseWheelTile] = true + + for id, tile of @state.content.tiles + continue if visibleTiles.hasOwnProperty(id) + + delete @state.content.tiles[id] + + updateLinesState: (tileState, startRow, endRow) -> + tileState.lines ?= {} visibleLineIds = {} - row = @startRow - while row < @endRow + row = startRow + while row < endRow line = @model.tokenizedLineForScreenRow(row) unless line? throw new Error("No line exists for row #{row}. Last screen row: #{@model.getLastScreenRow()}") visibleLineIds[line.id] = true - if @state.content.lines.hasOwnProperty(line.id) - @updateLineState(row, line) + if tileState.lines.hasOwnProperty(line.id) + lineState = tileState.lines[line.id] + lineState.screenRow = row + lineState.top = (row - startRow) * @lineHeight + lineState.decorationClasses = @lineDecorationClassesForRow(row) else - @buildLineState(row, line) + tileState.lines[line.id] = + screenRow: row + text: line.text + openScopes: line.openScopes + tags: line.tags + specialTokens: line.specialTokens + firstNonWhitespaceIndex: line.firstNonWhitespaceIndex + firstTrailingWhitespaceIndex: line.firstTrailingWhitespaceIndex + invisibles: line.invisibles + endOfLineInvisibles: line.endOfLineInvisibles + isOnlyWhitespace: line.isOnlyWhitespace() + indentLevel: line.indentLevel + tabLength: line.tabLength + fold: line.fold + top: (row - startRow) * @lineHeight + decorationClasses: @lineDecorationClassesForRow(row) row++ - if @mouseWheelScreenRow? - if preservedLine = @model.tokenizedLineForScreenRow(@mouseWheelScreenRow) - visibleLineIds[preservedLine.id] = true - - for id, line of @state.content.lines - unless visibleLineIds.hasOwnProperty(id) - delete @state.content.lines[id] + for id, line of tileState.lines + delete tileState.lines[id] unless visibleLineIds.hasOwnProperty(id) return - updateLineState: (row, line) -> - lineState = @state.content.lines[line.id] - lineState.screenRow = row - lineState.top = row * @lineHeight - lineState.decorationClasses = @lineDecorationClassesForRow(row) - - buildLineState: (row, line) -> - @state.content.lines[line.id] = - screenRow: row - text: line.text - openScopes: line.openScopes - tags: line.tags - specialTokens: line.specialTokens - firstNonWhitespaceIndex: line.firstNonWhitespaceIndex - firstTrailingWhitespaceIndex: line.firstTrailingWhitespaceIndex - invisibles: line.invisibles - endOfLineInvisibles: line.endOfLineInvisibles - isOnlyWhitespace: line.isOnlyWhitespace() - indentLevel: line.indentLevel - tabLength: line.tabLength - fold: line.fold - top: row * @lineHeight - decorationClasses: @lineDecorationClassesForRow(row) - updateCursorsState: -> @state.content.cursors = {} @updateCursorState(cursor) for cursor in @model.cursors # using property directly to avoid allocation @@ -391,12 +419,10 @@ class TextEditorPresenter else screenPosition = decoration.getMarker().getHeadScreenPosition() - pixelPosition = @pixelPositionForScreenPosition(screenPosition) + pixelPosition = @pixelPositionForScreenPosition(screenPosition, true) - {scrollTop, scrollLeft} = @state.content - - top = pixelPosition.top + @lineHeight - scrollTop - left = pixelPosition.left + @gutterWidth - scrollLeft + top = pixelPosition.top + @lineHeight + left = pixelPosition.left + @gutterWidth if overlayDimensions = @overlayDimensions[decoration.id] {itemWidth, itemHeight, contentMargin} = overlayDimensions @@ -578,7 +604,7 @@ class TextEditorPresenter updateStartRow: -> return unless @scrollTop? and @lineHeight? - startRow = Math.floor(@scrollTop / @lineHeight) - @lineOverdrawMargin + startRow = Math.floor(@scrollTop / @lineHeight) @startRow = Math.max(0, startRow) updateEndRow: -> @@ -586,7 +612,7 @@ class TextEditorPresenter startRow = Math.max(0, Math.floor(@scrollTop / @lineHeight)) visibleLinesCount = Math.ceil(@height / @lineHeight) + 1 - endRow = startRow + visibleLinesCount + @lineOverdrawMargin + endRow = startRow + visibleLinesCount @endRow = Math.min(@model.getScreenLineCount(), endRow) @markerObservationWindow.setScreenRange(Range(Point(@startRow, 0), Point(@endRow, 0))) @@ -620,6 +646,7 @@ class TextEditorPresenter oldContentWidth = @contentWidth clip = @model.tokenizedLineForScreenRow(@model.getLongestScreenRow())?.isSoftWrapped() @contentWidth = @pixelPositionForScreenPosition([@model.getLongestScreenRow(), @model.getMaxScreenLineLength()], clip).left + @contentWidth += @scrollLeft @contentWidth += 1 unless @model.isSoftWrapped() # account for cursor width if @contentHeight isnt oldContentHeight @@ -765,7 +792,7 @@ class TextEditorPresenter @shouldUpdateVerticalScrollState = true @shouldUpdateHiddenInputState = true @shouldUpdateDecorations = true - @shouldUpdateLinesState = true + @shouldUpdateTilesState = true @shouldUpdateCursorsState = true @shouldUpdateLineNumbersState = true @shouldUpdateCustomGutterDecorationState = true @@ -788,7 +815,7 @@ class TextEditorPresenter @state.content.scrollingVertically = false if @mouseWheelScreenRow? @mouseWheelScreenRow = null - @shouldUpdateLinesState = true + @shouldUpdateTilesState = true @shouldUpdateLineNumbersState = true @shouldUpdateCustomGutterDecorationState = true @@ -802,8 +829,10 @@ class TextEditorPresenter @model.setScrollLeft(scrollLeft) @shouldUpdateHorizontalScrollState = true @shouldUpdateHiddenInputState = true - @shouldUpdateCursorsState = true unless oldScrollLeft? + @shouldUpdateCursorsState = true @shouldUpdateOverlaysState = true + @shouldUpdateDecorations = true + @shouldUpdateTilesState = true @emitDidUpdateState() @@ -851,7 +880,7 @@ class TextEditorPresenter @shouldUpdateVerticalScrollState = true @shouldUpdateScrollbarsState = true @shouldUpdateDecorations = true - @shouldUpdateLinesState = true + @shouldUpdateTilesState = true @shouldUpdateCursorsState = true @shouldUpdateLineNumbersState = true @shouldUpdateCustomGutterDecorationState = true @@ -879,7 +908,7 @@ class TextEditorPresenter @shouldUpdateScrollbarsState = true @shouldUpdateContentState = true @shouldUpdateDecorations = true - @shouldUpdateLinesState = true + @shouldUpdateTilesState = true @shouldUpdateCursorsState = true unless oldContentFrameWidth? @emitDidUpdateState() @@ -945,7 +974,7 @@ class TextEditorPresenter @shouldUpdateScrollbarsState = true @shouldUpdateHiddenInputState = true @shouldUpdateDecorations = true - @shouldUpdateLinesState = true + @shouldUpdateTilesState = true @shouldUpdateCursorsState = true @shouldUpdateLineNumbersState = true @shouldUpdateCustomGutterDecorationState = true @@ -953,9 +982,9 @@ class TextEditorPresenter @emitDidUpdateState() - setMouseWheelScreenRow: (mouseWheelScreenRow) -> - unless @mouseWheelScreenRow is mouseWheelScreenRow - @mouseWheelScreenRow = mouseWheelScreenRow + setMouseWheelScreenRow: (screenRow) -> + if @mouseWheelScreenRow isnt screenRow + @mouseWheelScreenRow = screenRow @didStartScrolling() setBaseCharacterWidth: (baseCharacterWidth) -> @@ -997,7 +1026,7 @@ class TextEditorPresenter @shouldUpdateHiddenInputState = true @shouldUpdateContentState = true @shouldUpdateDecorations = true - @shouldUpdateLinesState = true + @shouldUpdateTilesState = true @shouldUpdateCursorsState = true @shouldUpdateOverlaysState = true @@ -1038,10 +1067,13 @@ class TextEditorPresenter charLength = 1 valueIndex++ - return {top, left} if column is targetColumn + break if column is targetColumn left += characterWidths[char] ? baseCharacterWidth unless char is '\0' column += charLength + + top -= @scrollTop + left -= @scrollLeft {top, left} hasPixelRectRequirements: -> @@ -1104,7 +1136,7 @@ class TextEditorPresenter if decoration.isType('overlay') @shouldUpdateOverlaysState = true if decoration.isType('line') - @shouldUpdateLinesState = true + @shouldUpdateTilesState = true if decoration.isType('line-number') @shouldUpdateLineNumbersState = true else if decoration.isType('gutter') @@ -1119,7 +1151,7 @@ class TextEditorPresenter decoration.getMarker().getScreenRange()) @addToLineDecorationCaches(decoration, decoration.getMarker().getScreenRange()) if decoration.isType('line') or Decoration.isType(oldProperties, 'line') - @shouldUpdateLinesState = true + @shouldUpdateTilesState = true if decoration.isType('line-number') or Decoration.isType(oldProperties, 'line-number') @shouldUpdateLineNumbersState = true if (decoration.isType('gutter') and not decoration.isType('line-number')) or @@ -1135,7 +1167,7 @@ class TextEditorPresenter didDestroyDecoration: (decoration) -> if decoration.isType('line') or decoration.isType('gutter') @removeFromLineDecorationCaches(decoration, decoration.getMarker().getScreenRange()) - @shouldUpdateLinesState = true if decoration.isType('line') + @shouldUpdateTilesState = true if decoration.isType('line') if decoration.isType('line-number') @shouldUpdateLineNumbersState = true else if decoration.isType('gutter') @@ -1160,7 +1192,7 @@ class TextEditorPresenter if decoration.isType('line') or decoration.isType('gutter') @addToLineDecorationCaches(decoration, decoration.getMarker().getScreenRange()) - @shouldUpdateLinesState = true if decoration.isType('line') + @shouldUpdateTilesState = true if decoration.isType('line') if decoration.isType('line-number') @shouldUpdateLineNumbersState = true else if decoration.isType('gutter') diff --git a/src/text-editor-view.coffee b/src/text-editor-view.coffee index 7f17ddaad..4d513c6f8 100644 --- a/src/text-editor-view.coffee +++ b/src/text-editor-view.coffee @@ -60,7 +60,7 @@ class TextEditorView extends View placeholderText: placeholderText element = new TextEditorElement - element.lineOverdrawMargin = props?.lineOverdrawMargin + element.tileSize = props?.tileSize element.setAttribute(name, value) for name, value of attributes if attributes? element.setModel(model) return element.__spacePenView diff --git a/src/text-editor.coffee b/src/text-editor.coffee index edfc17e0d..f2d8445aa 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -128,7 +128,15 @@ class TextEditor extends Model displayBuffer: @displayBuffer.serialize() deserializeParams: (params) -> - params.displayBuffer = DisplayBuffer.deserialize(params.displayBuffer) + try + displayBuffer = DisplayBuffer.deserialize(params.displayBuffer) + catch error + if error.syscall is 'read' + return # Error reading the file, don't deserialize an editor for it + else + throw error + + params.displayBuffer = displayBuffer params.registerEditor = true params diff --git a/src/tile-component.coffee b/src/tile-component.coffee new file mode 100644 index 000000000..88e06afd2 --- /dev/null +++ b/src/tile-component.coffee @@ -0,0 +1,361 @@ +_ = require 'underscore-plus' + +TokenIterator = require './token-iterator' +AcceptFilter = {acceptNode: -> NodeFilter.FILTER_ACCEPT} +WrapperDiv = document.createElement('div') +TokenTextEscapeRegex = /[&"'<>]/g +MaxTokenLength = 20000 + +cloneObject = (object) -> + clone = {} + clone[key] = value for key, value of object + clone + +module.exports = +class TileComponent + placeholderTextDiv: null + + constructor: ({@presenter, @id}) -> + @tokenIterator = new TokenIterator + @measuredLines = new Set + @lineNodesByLineId = {} + @screenRowsByLineId = {} + @lineIdsByScreenRow = {} + @domNode = document.createElement("div") + @domNode.classList.add("tile") + @domNode.style.position = "absolute" + @domNode.style.display = "block" + + getDomNode: -> + @domNode + + updateSync: (state) -> + @newState = state + unless @oldState + @oldState = {tiles: {}} + @oldState.tiles[@id] = {lines: {}} + + @newTileState = @newState.tiles[@id] + @oldTileState = @oldState.tiles[@id] + + if @newTileState.display isnt @oldTileState.display + @domNode.style.display = @newTileState.display + @oldTileState.display = @newTileState.display + + if @newTileState.height isnt @oldTileState.height + @domNode.style.height = @newTileState.height + 'px' + @oldTileState.height = @newTileState.height + + if @newTileState.top isnt @oldTileState.top or @newTileState.left isnt @oldTileState.left + @domNode.style['-webkit-transform'] = "translate3d(#{@newTileState.left}px, #{@newTileState.top}px, 0px)" + @oldTileState.top = @newTileState.top + @oldTileState.left = @newTileState.left + + @removeLineNodes() unless @oldState.indentGuidesVisible is @newState.indentGuidesVisible + @updateLineNodes() + + if @newState.scrollWidth isnt @oldState.scrollWidth + @domNode.style.width = @newState.scrollWidth + 'px' + @oldState.scrollWidth = @newState.scrollWidth + + @oldState.indentGuidesVisible = @newState.indentGuidesVisible + @oldState.scrollWidth = @newState.scrollWidth + + removeLineNodes: -> + @removeLineNode(id) for id of @oldTileState.lines + return + + removeLineNode: (id) -> + @lineNodesByLineId[id].remove() + delete @lineNodesByLineId[id] + delete @lineIdsByScreenRow[@screenRowsByLineId[id]] + delete @screenRowsByLineId[id] + delete @oldTileState.lines[id] + + updateLineNodes: -> + for id of @oldTileState.lines + unless @newTileState.lines.hasOwnProperty(id) + @removeLineNode(id) + + newLineIds = null + newLinesHTML = null + + for id, lineState of @newTileState.lines + if @oldTileState.lines.hasOwnProperty(id) + @updateLineNode(id) + else + newLineIds ?= [] + newLinesHTML ?= "" + newLineIds.push(id) + newLinesHTML += @buildLineHTML(id) + @screenRowsByLineId[id] = lineState.screenRow + @lineIdsByScreenRow[lineState.screenRow] = id + @oldTileState.lines[id] = cloneObject(lineState) + + return unless newLineIds? + + WrapperDiv.innerHTML = newLinesHTML + newLineNodes = _.toArray(WrapperDiv.children) + for id, i in newLineIds + lineNode = newLineNodes[i] + @lineNodesByLineId[id] = lineNode + @domNode.appendChild(lineNode) + + return + + buildLineHTML: (id) -> + {scrollWidth} = @newState + {screenRow, tokens, text, top, lineEnding, fold, isSoftWrapped, indentLevel, decorationClasses} = @newTileState.lines[id] + + classes = '' + if decorationClasses? + for decorationClass in decorationClasses + classes += decorationClass + ' ' + classes += 'line' + + lineHTML = "
" + + if text is "" + lineHTML += @buildEmptyLineInnerHTML(id) + else + lineHTML += @buildLineInnerHTML(id) + + lineHTML += '' if fold + lineHTML += "
" + lineHTML + + buildEmptyLineInnerHTML: (id) -> + {indentGuidesVisible} = @newState + {indentLevel, tabLength, endOfLineInvisibles} = @newTileState.lines[id] + + if indentGuidesVisible and indentLevel > 0 + invisibleIndex = 0 + lineHTML = '' + for i in [0...indentLevel] + lineHTML += "" + for j in [0...tabLength] + if invisible = endOfLineInvisibles?[invisibleIndex++] + lineHTML += "#{invisible}" + else + lineHTML += ' ' + lineHTML += "" + + while invisibleIndex < endOfLineInvisibles?.length + lineHTML += "#{endOfLineInvisibles[invisibleIndex++]}" + + lineHTML + else + @buildEndOfLineHTML(id) or ' ' + + buildLineInnerHTML: (id) -> + lineState = @newTileState.lines[id] + {firstNonWhitespaceIndex, firstTrailingWhitespaceIndex, invisibles} = lineState + lineIsWhitespaceOnly = firstTrailingWhitespaceIndex is 0 + + innerHTML = "" + @tokenIterator.reset(lineState) + + while @tokenIterator.next() + for scope in @tokenIterator.getScopeEnds() + innerHTML += "" + + for scope in @tokenIterator.getScopeStarts() + innerHTML += "" + + tokenStart = @tokenIterator.getScreenStart() + tokenEnd = @tokenIterator.getScreenEnd() + tokenText = @tokenIterator.getText() + isHardTab = @tokenIterator.isHardTab() + + if hasLeadingWhitespace = tokenStart < firstNonWhitespaceIndex + tokenFirstNonWhitespaceIndex = firstNonWhitespaceIndex - tokenStart + else + tokenFirstNonWhitespaceIndex = null + + if hasTrailingWhitespace = tokenEnd > firstTrailingWhitespaceIndex + tokenFirstTrailingWhitespaceIndex = Math.max(0, firstTrailingWhitespaceIndex - tokenStart) + else + tokenFirstTrailingWhitespaceIndex = null + + hasIndentGuide = + @newState.indentGuidesVisible and + (hasLeadingWhitespace or lineIsWhitespaceOnly) + + hasInvisibleCharacters = + (invisibles?.tab and isHardTab) or + (invisibles?.space and (hasLeadingWhitespace or hasTrailingWhitespace)) + + innerHTML += @buildTokenHTML(tokenText, isHardTab, tokenFirstNonWhitespaceIndex, tokenFirstTrailingWhitespaceIndex, hasIndentGuide, hasInvisibleCharacters) + + for scope in @tokenIterator.getScopeEnds() + innerHTML += "" + + for scope in @tokenIterator.getScopes() + innerHTML += "" + + innerHTML += @buildEndOfLineHTML(id) + innerHTML + + buildTokenHTML: (tokenText, isHardTab, firstNonWhitespaceIndex, firstTrailingWhitespaceIndex, hasIndentGuide, hasInvisibleCharacters) -> + if isHardTab + classes = 'hard-tab' + classes += ' leading-whitespace' if firstNonWhitespaceIndex? + classes += ' trailing-whitespace' if firstTrailingWhitespaceIndex? + classes += ' indent-guide' if hasIndentGuide + classes += ' invisible-character' if hasInvisibleCharacters + return "#{@escapeTokenText(tokenText)}" + else + startIndex = 0 + endIndex = tokenText.length + + leadingHtml = '' + trailingHtml = '' + + if firstNonWhitespaceIndex? + leadingWhitespace = tokenText.substring(0, firstNonWhitespaceIndex) + + classes = 'leading-whitespace' + classes += ' indent-guide' if hasIndentGuide + classes += ' invisible-character' if hasInvisibleCharacters + + leadingHtml = "#{leadingWhitespace}" + startIndex = firstNonWhitespaceIndex + + if firstTrailingWhitespaceIndex? + tokenIsOnlyWhitespace = firstTrailingWhitespaceIndex is 0 + trailingWhitespace = tokenText.substring(firstTrailingWhitespaceIndex) + + classes = 'trailing-whitespace' + classes += ' indent-guide' if hasIndentGuide and not firstNonWhitespaceIndex? and tokenIsOnlyWhitespace + classes += ' invisible-character' if hasInvisibleCharacters + + trailingHtml = "#{trailingWhitespace}" + + endIndex = firstTrailingWhitespaceIndex + + html = leadingHtml + if tokenText.length > MaxTokenLength + while startIndex < endIndex + html += "" + @escapeTokenText(tokenText, startIndex, startIndex + MaxTokenLength) + "" + startIndex += MaxTokenLength + else + html += @escapeTokenText(tokenText, startIndex, endIndex) + + html += trailingHtml + html + + escapeTokenText: (tokenText, startIndex, endIndex) -> + if startIndex? and endIndex? and startIndex > 0 or endIndex < tokenText.length + tokenText = tokenText.slice(startIndex, endIndex) + tokenText.replace(TokenTextEscapeRegex, @escapeTokenTextReplace) + + escapeTokenTextReplace: (match) -> + switch match + when '&' then '&' + when '"' then '"' + when "'" then ''' + when '<' then '<' + when '>' then '>' + else match + + buildEndOfLineHTML: (id) -> + {endOfLineInvisibles} = @newTileState.lines[id] + + html = '' + if endOfLineInvisibles? + for invisible in endOfLineInvisibles + html += "#{invisible}" + html + + updateLineNode: (id) -> + oldLineState = @oldTileState.lines[id] + newLineState = @newTileState.lines[id] + + lineNode = @lineNodesByLineId[id] + + if @newState.scrollWidth isnt @oldState.scrollWidth + lineNode.style.width = @newState.scrollWidth + 'px' + + newDecorationClasses = newLineState.decorationClasses + oldDecorationClasses = oldLineState.decorationClasses + + if oldDecorationClasses? + for decorationClass in oldDecorationClasses + unless newDecorationClasses? and decorationClass in newDecorationClasses + lineNode.classList.remove(decorationClass) + + if newDecorationClasses? + for decorationClass in newDecorationClasses + unless oldDecorationClasses? and decorationClass in oldDecorationClasses + lineNode.classList.add(decorationClass) + + oldLineState.decorationClasses = newLineState.decorationClasses + + if newLineState.top isnt oldLineState.top + lineNode.style.top = newLineState.top + 'px' + oldLineState.top = newLineState.top + + if newLineState.screenRow isnt oldLineState.screenRow + lineNode.dataset.screenRow = newLineState.screenRow + oldLineState.screenRow = newLineState.screenRow + @lineIdsByScreenRow[newLineState.screenRow] = id + + lineNodeForScreenRow: (screenRow) -> + @lineNodesByLineId[@lineIdsByScreenRow[screenRow]] + + measureCharactersInNewLines: -> + for id, lineState of @oldTileState.lines + unless @measuredLines.has(id) + lineNode = @lineNodesByLineId[id] + @measureCharactersInLine(id, lineState, lineNode) + return + + measureCharactersInLine: (lineId, tokenizedLine, lineNode) -> + rangeForMeasurement = null + iterator = null + charIndex = 0 + + @tokenIterator.reset(tokenizedLine) + while @tokenIterator.next() + scopes = @tokenIterator.getScopes() + text = @tokenIterator.getText() + charWidths = @presenter.getScopedCharacterWidths(scopes) + + textIndex = 0 + while textIndex < text.length + if @tokenIterator.isPairedCharacter() + char = text + charLength = 2 + textIndex += 2 + else + char = text[textIndex] + charLength = 1 + textIndex++ + + continue if char is '\0' + + unless charWidths[char]? + unless textNode? + rangeForMeasurement ?= document.createRange() + iterator = document.createNodeIterator(lineNode, NodeFilter.SHOW_TEXT, AcceptFilter) + textNode = iterator.nextNode() + textNodeIndex = 0 + nextTextNodeIndex = textNode.textContent.length + + while nextTextNodeIndex <= charIndex + textNode = iterator.nextNode() + textNodeIndex = nextTextNodeIndex + nextTextNodeIndex = textNodeIndex + textNode.textContent.length + + i = charIndex - textNodeIndex + rangeForMeasurement.setStart(textNode, i) + rangeForMeasurement.setEnd(textNode, i + charLength) + charWidth = rangeForMeasurement.getBoundingClientRect().width + @presenter.setScopedCharacterWidth(scopes, char, charWidth) + + charIndex += charLength + + @measuredLines.add(lineId) + + clearMeasurements: -> + @measuredLines.clear() diff --git a/src/tokenized-line.coffee b/src/tokenized-line.coffee index 6761eecad..b8f7226c8 100644 --- a/src/tokenized-line.coffee +++ b/src/tokenized-line.coffee @@ -220,17 +220,20 @@ class TokenizedLine copy: -> copy = new TokenizedLine copy.tokenIterator = @tokenIterator - copy.indentLevel = @indentLevel copy.openScopes = @openScopes copy.text = @text copy.tags = @tags copy.specialTokens = @specialTokens + copy.startBufferColumn = @startBufferColumn + copy.bufferDelta = @bufferDelta + copy.ruleStack = @ruleStack + copy.lineEnding = @lineEnding + copy.invisibles = @invisibles + copy.endOfLineInvisibles = @endOfLineInvisibles + copy.indentLevel = @indentLevel + copy.tabLength = @tabLength copy.firstNonWhitespaceIndex = @firstNonWhitespaceIndex copy.firstTrailingWhitespaceIndex = @firstTrailingWhitespaceIndex - copy.lineEnding = @lineEnding - copy.endOfLineInvisibles = @endOfLineInvisibles - copy.ruleStack = @ruleStack - copy.startBufferColumn = @startBufferColumn copy.fold = @fold copy diff --git a/src/window-event-handler.coffee b/src/window-event-handler.coffee index 7d67e87ab..3b9d54b96 100644 --- a/src/window-event-handler.coffee +++ b/src/window-event-handler.coffee @@ -23,10 +23,10 @@ class WindowEventHandler if pathToOpen? and needsProjectPaths if fs.existsSync(pathToOpen) atom.project.addPath(pathToOpen) + else if fs.existsSync(path.dirname(pathToOpen)) + atom.project.addPath(path.dirname(pathToOpen)) else - dirToOpen = path.dirname(pathToOpen) - if fs.existsSync(dirToOpen) - atom.project.addPath(dirToOpen) + atom.project.addPath(pathToOpen) unless fs.isDirectorySync(pathToOpen) atom.workspace?.open(pathToOpen, {initialLine, initialColumn}) diff --git a/static/atom.less b/static/atom.less index b45d7b29a..1acb0f54b 100644 --- a/static/atom.less +++ b/static/atom.less @@ -25,5 +25,6 @@ @import "text-editor-light"; @import "select-list"; @import "syntax"; +@import "text"; @import "utilities"; @import "octicons"; diff --git a/static/index.html b/static/index.html index 84e8d57d4..5fcb30ad2 100644 --- a/static/index.html +++ b/static/index.html @@ -2,7 +2,6 @@ - diff --git a/static/text.less b/static/text.less new file mode 100644 index 000000000..a189fbf9a --- /dev/null +++ b/static/text.less @@ -0,0 +1,39 @@ +@import "ui-variables"; + +.text-bits (@type) { + @text-color-name: "text-color-@{type}"; + @bg-color-name: "background-color-@{type}"; + + @text-color: @@text-color-name; + @bg-color: @@bg-color-name; + + code { + color: @text-color; + background: fadeout(@bg-color, 80%); + } + + a, a code { + text-decoration: underline; + color: darken(@text-color, 10%); + + &:hover { + color: darken(@text-color, 15%); + } + } +} + +.text-info { + .text-bits(info); +} + +.text-success { + .text-bits(success); +} + +.text-warning { + .text-bits(warning); +} + +.text-error { + .text-bits(error); +}