From 1b1cffb32d15370cd3d5b2e4048c6cc92cf52a3f Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 23 Mar 2017 07:28:33 -0600 Subject: [PATCH] :arrow_up: etch to allow arbitrary objects as keys --- package.json | 2 +- spec/text-editor-component-spec.js | 75 +++++++++++--- src/decoration-manager.js | 9 +- src/gutter.coffee | 4 + src/initialize-application-window.coffee | 2 +- src/initialize-benchmark-window.js | 2 +- src/initialize-test-window.coffee | 2 +- src/text-editor-component.js | 119 ++++++++++++++++++++--- src/text-editor.coffee | 2 +- static/text-editor-light.less | 71 ++------------ 10 files changed, 193 insertions(+), 95 deletions(-) diff --git a/package.json b/package.json index b5be71b30..131392122 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "dedent": "^0.6.0", "devtron": "1.3.0", "element-resize-detector": "^1.1.10", - "etch": "^0.10.0", + "etch": "^0.11.0", "event-kit": "^2.3.0", "find-parent-dir": "^0.3.0", "first-mate": "7.0.4", diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index 5e6fbf615..14f2c14c9 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -990,39 +990,92 @@ describe('TextEditorComponent', () => { ]) }) - it('allows the element of custom gutters to be retrieved', async () => { + it('allows the element of custom gutters to be retrieved before being rendered in the editor component', async () => { const {component, element, editor} = buildComponent() + const [lineNumberGutter] = editor.getGutters() const gutterA = editor.addGutter({name: 'a', priority: -1}) const gutterB = editor.addGutter({name: 'b', priority: 1}) + + const lineNumberGutterElement = lineNumberGutter.getElement() + const gutterAElement = gutterA.getElement() + const gutterBElement = gutterB.getElement() + await component.getNextUpdatePromise() - expect(element.contains(gutterA.element)).toBe(true) - expect(element.contains(gutterB.element)).toBe(true) + expect(element.contains(lineNumberGutterElement)).toBe(true) + expect(element.contains(gutterAElement)).toBe(true) + expect(element.contains(gutterBElement)).toBe(true) }) it('can show and hide custom gutters', async () => { const {component, element, editor} = buildComponent() const gutterA = editor.addGutter({name: 'a', priority: -1}) const gutterB = editor.addGutter({name: 'b', priority: 1}) + const gutterAElement = gutterA.getElement() + const gutterBElement = gutterB.getElement() await component.getNextUpdatePromise() - expect(gutterA.element.style.display).toBe('') - expect(gutterB.element.style.display).toBe('') + expect(gutterAElement.style.display).toBe('') + expect(gutterBElement.style.display).toBe('') gutterA.hide() await component.getNextUpdatePromise() - expect(gutterA.element.style.display).toBe('none') - expect(gutterB.element.style.display).toBe('') + expect(gutterAElement.style.display).toBe('none') + expect(gutterBElement.style.display).toBe('') gutterB.hide() await component.getNextUpdatePromise() - expect(gutterA.element.style.display).toBe('none') - expect(gutterB.element.style.display).toBe('none') + expect(gutterAElement.style.display).toBe('none') + expect(gutterBElement.style.display).toBe('none') gutterA.show() await component.getNextUpdatePromise() - expect(gutterA.element.style.display).toBe('') - expect(gutterB.element.style.display).toBe('none') + expect(gutterAElement.style.display).toBe('') + expect(gutterBElement.style.display).toBe('none') + }) + + it('renders decorations in custom gutters', async () => { + const {component, element, editor} = buildComponent() + const gutterA = editor.addGutter({name: 'a', priority: -1}) + const gutterB = editor.addGutter({name: 'b', priority: 1}) + const marker1 = editor.markScreenRange([[2, 0], [4, 0]]) + const marker2 = editor.markScreenRange([[6, 0], [7, 0]]) + const marker3 = editor.markScreenRange([[9, 0], [12, 0]]) + const decorationElement1 = document.createElement('div') + const decorationElement2 = document.createElement('div') + + const decoration1 = gutterA.decorateMarker(marker1, {class: 'a'}) + const decoration2 = gutterA.decorateMarker(marker2, {class: 'b', item: decorationElement1}) + const decoration3 = gutterB.decorateMarker(marker3, {item: decorationElement2}) + await component.getNextUpdatePromise() + + let [decorationNode1, decorationNode2] = gutterA.getElement().firstChild.children + const [decorationNode3] = gutterB.getElement().firstChild.children + + expect(decorationNode1.className).toBe('a') + expect(decorationNode1.getBoundingClientRect().top).toBe(clientTopForLine(component, 2)) + expect(decorationNode1.getBoundingClientRect().bottom).toBe(clientTopForLine(component, 5)) + expect(decorationNode1.firstChild).toBeNull() + + expect(decorationNode2.className).toBe('b') + expect(decorationNode2.getBoundingClientRect().top).toBe(clientTopForLine(component, 6)) + expect(decorationNode2.getBoundingClientRect().bottom).toBe(clientTopForLine(component, 8)) + expect(decorationNode2.firstChild).toBe(decorationElement1) + + expect(decorationNode3.className).toBe('') + expect(decorationNode3.getBoundingClientRect().top).toBe(clientTopForLine(component, 9)) + expect(decorationNode3.getBoundingClientRect().bottom).toBe(clientTopForLine(component, 12) + component.getLineHeight()) + expect(decorationNode3.firstChild).toBe(decorationElement2) + + decoration1.setProperties({type: 'gutter', gutterName: 'a', class: 'c', item: decorationElement1}) + decoration2.setProperties({type: 'gutter', gutterName: 'a', item: decorationElement2}) + decoration3.destroy() + await component.getNextUpdatePromise() + expect(decorationNode1.className).toBe('c') + expect(decorationNode1.firstChild).toBe(decorationElement1) + expect(decorationNode2.className).toBe('') + expect(decorationNode2.firstChild).toBe(decorationElement2) + expect(gutterB.getElement().firstChild.children.length).toBe(0) }) }) diff --git a/src/decoration-manager.js b/src/decoration-manager.js index ec8b8b684..fc3692bce 100644 --- a/src/decoration-manager.js +++ b/src/decoration-manager.js @@ -106,9 +106,12 @@ class DecorationManager { } if (hasMarkerDecorations) { - this.decorationsByMarker.get(marker).forEach((decoration) => { - decorationPropertiesForMarker.push(decoration.getProperties()) - }) + const decorationsForMarker = this.decorationsByMarker.get(marker) + if (decorationsForMarker) { + decorationsForMarker.forEach((decoration) => { + decorationPropertiesForMarker.push(decoration.getProperties()) + }) + } } } }) diff --git a/src/gutter.coffee b/src/gutter.coffee index 1001fdfa2..19792ff12 100644 --- a/src/gutter.coffee +++ b/src/gutter.coffee @@ -1,4 +1,5 @@ {Emitter} = require 'event-kit' +CustomGutterComponent = null DefaultPriority = -100 @@ -102,3 +103,6 @@ class Gutter # Returns a {Decoration} object decorateMarker: (marker, options) -> @gutterContainer.addGutterDecoration(this, marker, options) + + getElement: -> + @element ?= document.createElement('div') diff --git a/src/initialize-application-window.coffee b/src/initialize-application-window.coffee index ccf88cc9f..25b990110 100644 --- a/src/initialize-application-window.coffee +++ b/src/initialize-application-window.coffee @@ -58,7 +58,7 @@ if global.isGeneratingSnapshot clipboard = new Clipboard TextEditor.setClipboard(clipboard) -TextEditor.viewForOverlayItem = (item) -> atom.views.getView(item) +TextEditor.viewForItem = (item) -> atom.views.getView(item) global.atom = new AtomEnvironment({ clipboard, diff --git a/src/initialize-benchmark-window.js b/src/initialize-benchmark-window.js index 2d9e724b2..166319b94 100644 --- a/src/initialize-benchmark-window.js +++ b/src/initialize-benchmark-window.js @@ -54,7 +54,7 @@ export default async function () { const clipboard = new Clipboard() TextEditor.setClipboard(clipboard) - TextEditor.viewForOverlayItem = (item) -> atom.views.getView(item) + TextEditor.viewForItem = (item) -> atom.views.getView(item) const applicationDelegate = new ApplicationDelegate() const environmentParams = { diff --git a/src/initialize-test-window.coffee b/src/initialize-test-window.coffee index a5fdc43d6..5ad10670a 100644 --- a/src/initialize-test-window.coffee +++ b/src/initialize-test-window.coffee @@ -70,7 +70,7 @@ module.exports = ({blobStore}) -> clipboard = new Clipboard TextEditor.setClipboard(clipboard) - TextEditor.viewForOverlayItem = (item) -> atom.views.getView(item) + TextEditor.viewForItem = (item) -> atom.views.getView(item) testRunner = require(testRunnerPath) legacyTestRunner = require(legacyTestRunnerPath) diff --git a/src/text-editor-component.js b/src/text-editor-component.js index d5781e876..91889e7c1 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -240,12 +240,15 @@ class TextEditorComponent { innerStyle.transform = `translateY(${-this.getScrollTop()}px)` gutterNodes = this.guttersToRender.map((gutter) => { if (gutter.name === 'line-number') { - return this.renderLineNumberGutter() + return this.renderLineNumberGutter(gutter) } else { return $(CustomGutterComponent, { key: gutter, - gutter: gutter, - height: this.getScrollHeight() + element: gutter.getElement(), + name: gutter.name, + visible: gutter.isVisible(), + height: this.getScrollHeight(), + decorations: this.decorationsToRender.customGutter.get(gutter.name) }) } }) @@ -267,7 +270,7 @@ class TextEditorComponent { ) } - renderLineNumberGutter () { + renderLineNumberGutter (gutter) { const {model} = this.props if (!model.isLineNumberGutterVisible()) return null @@ -302,6 +305,7 @@ class TextEditorComponent { this.currentFrameLineNumberGutterProps = { ref: 'lineNumberGutter', + element: gutter.getElement(), parentComponent: this, height: this.getScrollHeight(), width: this.getLineNumberGutterWidth(), @@ -680,6 +684,9 @@ class TextEditorComponent { case 'overlay': this.addOverlayDecorationToRender(decoration, marker) break + case 'gutter': + this.addCustomGutterDecorationToRender(decoration, screenRange) + break } } } @@ -770,7 +777,7 @@ class TextEditorComponent { addOverlayDecorationToRender (decoration, marker) { const {class: className, item, position, avoidOverflow} = decoration - const element = TextEditor.viewForOverlayItem(item) + const element = TextEditor.viewForItem(item) const screenPosition = (position === 'tail') ? marker.getTailScreenPosition() : marker.getHeadScreenPosition() @@ -779,6 +786,22 @@ class TextEditorComponent { this.decorationsToRender.overlays.push({className, element, avoidOverflow, screenPosition}) } + addCustomGutterDecorationToRender (decoration, screenRange) { + let decorations = this.decorationsToRender.customGutter.get(decoration.gutterName) + if (!decorations) { + decorations = [] + this.decorationsToRender.customGutter.set(decoration.gutterName, decorations) + } + const top = this.pixelTopForRow(screenRange.start.row) + const height = this.pixelTopForRow(screenRange.end.row + 1) - top + + decorations.push({ + className: decoration.class, + element: TextEditor.viewForItem(decoration.item), + top, height + }) + } + updateAbsolutePositionedDecorations () { this.updateHighlightsToRender() this.updateCursorsToRender() @@ -1984,7 +2007,10 @@ class DummyScrollbarComponent { class LineNumberGutterComponent { constructor (props) { this.props = props - etch.initialize(this) + this.element = this.props.element + this.virtualNode = $.div(null) + this.virtualNode.domNode = this.element + etch.updateSync(this) } update (newProps) { @@ -2100,8 +2126,10 @@ class LineNumberGutterComponent { class CustomGutterComponent { constructor (props) { this.props = props - etch.initialize(this) - this.props.gutter.element = this.element + this.element = this.props.element + this.virtualNode = $.div(null) + this.virtualNode.domNode = this.element + etch.updateSync(this) } update (props) { @@ -2109,21 +2137,68 @@ class CustomGutterComponent { etch.updateSync(this) } + destroy () { + etch.destroy(this) + } + render () { return $.div( { className: 'gutter', - attributes: {'gutter-name': this.props.gutter.name}, + attributes: {'gutter-name': this.props.name}, style: { - display: this.props.gutter.isVisible() ? '' : 'none' + display: this.props.visible ? '' : 'none' } }, - $.div({ - className: 'custom-decorations', - style: {height: this.props.height + 'px'} - }) + $.div( + { + className: 'custom-decorations', + style: {height: this.props.height + 'px'} + }, + this.renderDecorations() + ) ) } + + renderDecorations () { + if (!this.props.decorations) return null + + return this.props.decorations.map(({className, element, top, height}) => { + return $(CustomGutterDecorationComponent, { + className, + element, + top, + height + }) + }) + } +} + +class CustomGutterDecorationComponent { + constructor (props) { + this.props = props + this.element = document.createElement('div') + const {top, height, className, element} = this.props + + this.element.style.position = 'absolute' + this.element.style.top = top + 'px' + this.element.style.height = height + 'px' + if (className != null) this.element.className = className + if (element != null) this.element.appendChild(element) + } + + update (newProps) { + const oldProps = this.props + this.props = newProps + + if (newProps.top != oldProps.top) this.element.style.top = newProps.top + 'px' + if (newProps.height != oldProps.height) this.element.style.height = newProps.height + 'px' + if (newProps.className != oldProps.className) this.element.className = newProps.className || '' + if (newProps.element != oldProps.element) { + if (this.element.firstChild) this.element.firstChild.remove() + this.element.appendChild(newProps.element) + } + } } class LinesTileComponent { @@ -2453,6 +2528,22 @@ class OverlayComponent { } } +class ComponentWrapper { + constructor (props) { + this.component = props.component + this.element = this.component.element + this.component.update(props) + } + + update (props) { + this.component.update(props) + } + + destroy () { + this.component.destroy() + } +} + const classNamesByScopeName = new Map() function classNameForScopeName (scopeName) { let classString = classNamesByScopeName.get(scopeName) diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 49fb920e2..2cd30944c 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -69,7 +69,7 @@ class TextEditor extends Model TextEditorComponent ?= require './text-editor-component' TextEditorComponent.didUpdateScrollbarStyles() - @viewForOverlayItem: (item) -> item + @viewForItem: (item) -> item.element ? item serializationVersion: 1 diff --git a/static/text-editor-light.less b/static/text-editor-light.less index d688db3c0..493696aca 100644 --- a/static/text-editor-light.less +++ b/static/text-editor-light.less @@ -24,15 +24,15 @@ atom-text-editor { // background-color: inherit; // } - // .gutter { - // overflow: hidden; - // z-index: 0; - // text-align: right; - // cursor: default; - // min-width: 1em; - // box-sizing: border-box; - // background-color: inherit; - // } + .gutter { + overflow: hidden; + z-index: 0; + text-align: right; + cursor: default; + min-width: 1em; + box-sizing: border-box; + background-color: inherit; + } // // .line-numbers { // position: relative; @@ -85,15 +85,6 @@ atom-text-editor { } } - // .scroll-view { - // position: relative; - // z-index: 0; - // overflow: hidden; - // flex: 1; - // min-width: 0; - // min-height: 0; - // } - .highlight { background: none; padding: 0; @@ -105,13 +96,6 @@ atom-text-editor { z-index: -1; } - // .lines { - // min-width: 100%; - // position: relative; - // z-index: 1; - // background-color: inherit; - // } - .line { white-space: pre; @@ -162,43 +146,6 @@ atom-text-editor { .cursors.blink-off .cursor { opacity: 0; } - - .horizontal-scrollbar { - position: absolute; - left: 0; - right: 0; - bottom: 0; - - height: 15px; - overflow-x: auto; - overflow-y: hidden; - z-index: 3; - cursor: default; - - .scrollbar-content { - height: 15px; - } - } - - .vertical-scrollbar { - position: absolute; - top: 0; - right: 0; - bottom: 0; - - width: 15px; - overflow-x: hidden; - overflow-y: auto; - z-index: 3; - cursor: default; - } - - .scrollbar-corner { - position: absolute; - overflow: auto; - bottom: 0; - right: 0; - } } atom-text-editor[mini] {