diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index 2c7238198..f2e8c8bee 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -38,7 +38,7 @@ describe "EditorComponent", -> describe "line rendering", -> it "renders only the currently-visible lines", -> node.style.height = 4.5 * lineHeightInPixels + 'px' - component.updateModelDimensions() + component.measureHeightAndWidth() lines = node.querySelectorAll('.line') expect(lines.length).toBe 6 @@ -121,7 +121,7 @@ describe "EditorComponent", -> describe "gutter rendering", -> it "renders the currently-visible line numbers", -> node.style.height = 4.5 * lineHeightInPixels + 'px' - component.updateModelDimensions() + component.measureHeightAndWidth() lines = node.querySelectorAll('.line-number') expect(lines.length).toBe 6 @@ -146,7 +146,7 @@ describe "EditorComponent", -> editor.setSoftWrap(true) node.style.height = 4.5 * lineHeightInPixels + 'px' node.style.width = 30 * charWidth + 'px' - component.updateModelDimensions() + component.measureHeightAndWidth() lines = node.querySelectorAll('.line-number') expect(lines.length).toBe 6 @@ -163,7 +163,7 @@ describe "EditorComponent", -> cursor1.setScreenPosition([0, 5]) node.style.height = 4.5 * lineHeightInPixels + 'px' - component.updateModelDimensions() + component.measureHeightAndWidth() cursorNodes = node.querySelectorAll('.cursor') expect(cursorNodes.length).toBe 1 @@ -258,7 +258,7 @@ describe "EditorComponent", -> inputNode = node.querySelector('.hidden-input') node.style.height = 5 * lineHeightInPixels + 'px' node.style.width = 10 * charWidth + 'px' - component.updateModelDimensions() + component.measureHeightAndWidth() expect(editor.getCursorScreenPosition()).toEqual [0, 0] editor.setScrollTop(3 * lineHeightInPixels) @@ -292,6 +292,7 @@ describe "EditorComponent", -> # 1-line selection editor.setSelectedScreenRange([[1, 6], [1, 10]]) regions = node.querySelectorAll('.selection .region') + expect(regions.length).toBe 1 regionRect = regions[0].getBoundingClientRect() expect(regionRect.top).toBe 1 * lineHeightInPixels @@ -355,7 +356,7 @@ describe "EditorComponent", -> it "moves the cursor to the nearest screen position", -> node.style.height = 4.5 * lineHeightInPixels + 'px' node.style.width = 10 * charWidth + 'px' - component.updateModelDimensions() + component.measureHeightAndWidth() editor.setScrollTop(3.5 * lineHeightInPixels) editor.setScrollLeft(2 * charWidth) @@ -468,7 +469,7 @@ describe "EditorComponent", -> describe "scrolling", -> it "updates the vertical scrollbar when the scrollTop is changed in the model", -> node.style.height = 4.5 * lineHeightInPixels + 'px' - component.updateModelDimensions() + component.measureHeightAndWidth() expect(verticalScrollbarNode.scrollTop).toBe 0 @@ -477,7 +478,7 @@ describe "EditorComponent", -> it "updates the horizontal scrollbar and scroll view content x transform based on the scrollLeft of the model", -> node.style.width = 30 * charWidth + 'px' - component.updateModelDimensions() + component.measureHeightAndWidth() scrollViewContentNode = node.querySelector('.scroll-view-content') expect(scrollViewContentNode.style['-webkit-transform']).toBe "translate(0px, 0px)" @@ -489,7 +490,7 @@ describe "EditorComponent", -> it "updates the scrollLeft of the model when the scrollLeft of the horizontal scrollbar changes", -> node.style.width = 30 * charWidth + 'px' - component.updateModelDimensions() + component.measureHeightAndWidth() expect(editor.getScrollLeft()).toBe 0 horizontalScrollbarNode.scrollLeft = 100 @@ -501,7 +502,7 @@ describe "EditorComponent", -> it "updates the horizontal or vertical scrollbar depending on which delta is greater (x or y)", -> node.style.height = 4.5 * lineHeightInPixels + 'px' node.style.width = 20 * charWidth + 'px' - component.updateModelDimensions() + component.measureHeightAndWidth() expect(verticalScrollbarNode.scrollTop).toBe 0 expect(horizontalScrollbarNode.scrollLeft).toBe 0 @@ -517,7 +518,7 @@ describe "EditorComponent", -> it "preserves the target of the mousewheel event when scrolling vertically", -> node.style.height = 4.5 * lineHeightInPixels + 'px' node.style.width = 20 * charWidth + 'px' - component.updateModelDimensions() + component.measureHeightAndWidth() lineNodes = node.querySelectorAll('.line') expect(lineNodes.length).toBe 6 diff --git a/src/cursors-component.coffee b/src/cursors-component.coffee index 675cd2f9d..e3143f1f1 100644 --- a/src/cursors-component.coffee +++ b/src/cursors-component.coffee @@ -17,10 +17,11 @@ CursorsComponent = React.createClass blinkOff = @state.blinkCursorsOff div className: 'cursors', - for selection in editor.getSelections() - if selection.isEmpty() and editor.selectionIntersectsVisibleRowRange(selection) - {cursor} = selection - CursorComponent({key: cursor.id, cursor, blinkOff}) + if @isMounted() + for selection in editor.getSelections() + if selection.isEmpty() and editor.selectionIntersectsVisibleRowRange(selection) + {cursor} = selection + CursorComponent({key: cursor.id, cursor, blinkOff}) getInitialState: -> blinkCursorsOff: false diff --git a/src/display-buffer.coffee b/src/display-buffer.coffee index 24e098425..c2379ae87 100644 --- a/src/display-buffer.coffee +++ b/src/display-buffer.coffee @@ -111,10 +111,10 @@ class DisplayBuffer extends Model getHorizontalScrollMargin: -> @horizontalScrollMargin setHorizontalScrollMargin: (@horizontalScrollMargin) -> @horizontalScrollMargin - getHeight: -> @height + getHeight: -> @height ? @getScrollHeight() setHeight: (@height) -> @height - getWidth: -> @width + getWidth: -> @width ? @getScrollWidth() setWidth: (newWidth) -> oldWidth = @width @width = newWidth @@ -172,18 +172,21 @@ class DisplayBuffer extends Model @charWidthsByScope = {} getScrollHeight: -> + unless @getLineHeight() > 0 + throw new Error("You must assign lineHeight before calling ::getScrollHeight()") + @getLineCount() * @getLineHeight() getScrollWidth: -> @getMaxLineLength() * @getDefaultCharWidth() getVisibleRowRange: -> - return [0, 0] unless @getLineHeight() > 0 - return [0, @getLineCount()] if @getHeight() is 0 + unless @getLineHeight() > 0 + throw new Error("You must assign a non-zero lineHeight before calling ::getVisibleRowRange()") heightInLines = Math.ceil(@getHeight() / @getLineHeight()) + 1 startRow = Math.floor(@getScrollTop() / @getLineHeight()) - endRow = Math.ceil(startRow + heightInLines) + endRow = Math.min(@getLineCount(), Math.ceil(startRow + heightInLines)) [startRow, endRow] intersectsVisibleRowRange: (startRow, endRow) -> diff --git a/src/editor-component.coffee b/src/editor-component.coffee index 1859c903d..32db47ccc 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -23,8 +23,12 @@ EditorComponent = React.createClass render: -> {focused, fontSize, lineHeight, fontFamily, showIndentGuide} = @state {editor, cursorBlinkPeriod, cursorBlinkResumeDelay} = @props - visibleRowRange = editor.getVisibleRowRange() - scrollTop = editor.getScrollTop() + if @isMounted() + visibleRowRange = editor.getVisibleRowRange() + scrollHeight = editor.getScrollHeight() + scrollWidth = editor.getScrollWidth() + scrollTop = editor.getScrollTop() + scrollLeft = editor.getScrollLeft() className = 'editor editor-colors react' className += ' is-focused' if focused @@ -44,16 +48,16 @@ EditorComponent = React.createClass className: 'vertical-scrollbar' orientation: 'vertical' onScroll: @onVerticalScroll - scrollTop: editor.getScrollTop() - scrollHeight: editor.getScrollHeight() + scrollTop: scrollTop + scrollHeight: scrollHeight ScrollbarComponent ref: 'horizontalScrollbar' className: 'horizontal-scrollbar' orientation: 'horizontal' onScroll: @onHorizontalScroll - scrollLeft: editor.getScrollLeft() - scrollWidth: editor.getScrollWidth() + scrollLeft: scrollLeft + scrollWidth: scrollWidth getInitialState: -> {} @@ -61,16 +65,17 @@ EditorComponent = React.createClass cursorBlinkPeriod: 800 cursorBlinkResumeDelay: 200 - componentDidMount: -> + componentWillMount: -> @pendingChanges = [] @props.editor.manageScrollPosition = true - - @listenForDOMEvents() - @listenForCommands() - @observeEditor() @observeConfig() + componentDidMount: -> + @observeEditor() + @listenForDOMEvents() + @listenForCommands() @props.editor.setVisible(true) + @requestUpdate() componentWillUnmount: -> @getDOMNode().removeEventListener 'mousewheel', @onMouseWheel @@ -317,8 +322,8 @@ EditorComponent = React.createClass else @forceUpdate() - updateModelDimensions: -> - @refs.scrollView.updateModelDimensions() + measureHeightAndWidth: -> + @refs.scrollView.measureHeightAndWidth() consolidateSelections: (e) -> e.abortKeyBinding() unless @props.editor.consolidateSelections() diff --git a/src/editor-scroll-view-component.coffee b/src/editor-scroll-view-component.coffee index 269bba241..1ee1a6f92 100644 --- a/src/editor-scroll-view-component.coffee +++ b/src/editor-scroll-view-component.coffee @@ -13,11 +13,13 @@ EditorScrollViewComponent = React.createClass render: -> {editor, fontSize, fontFamily, lineHeight, showIndentGuide, cursorBlinkPeriod, cursorBlinkResumeDelay} = @props {visibleRowRange, preservedScreenRow, pendingChanges, cursorsMoved, onInputFocused, onInputBlurred} = @props - contentStyle = - height: editor.getScrollHeight() - WebkitTransform: "translate(#{-editor.getScrollLeft()}px, #{-editor.getScrollTop()}px)" - div className: 'scroll-view', ref: 'scrollView', + if @isMounted() + contentStyle = + height: editor.getScrollHeight() + WebkitTransform: "translate(#{-editor.getScrollLeft()}px, #{-editor.getScrollTop()}px)" + + div className: 'scroll-view', InputComponent ref: 'input' className: 'hidden-input' @@ -36,8 +38,8 @@ EditorScrollViewComponent = React.createClass SelectionsComponent({editor}) componentDidMount: -> - @getDOMNode().addEventListener 'overflowchanged', @updateModelDimensions - @updateModelDimensions() + @getDOMNode().addEventListener 'overflowchanged', @measureHeightAndWidth + @measureHeightAndWidth() onInput: (char, replaceLastCharacter) -> {editor} = @props @@ -109,12 +111,14 @@ EditorScrollViewComponent = React.createClass {editor} = @props {clientX, clientY} = event - editorClientRect = @refs.scrollView.getDOMNode().getBoundingClientRect() + editorClientRect = @getDOMNode().getBoundingClientRect() top = clientY - editorClientRect.top + editor.getScrollTop() left = clientX - editorClientRect.left + editor.getScrollLeft() {top, left} getHiddenInputPosition: -> + return {top: 0, left: 0} unless @isMounted() + {editor} = @props if cursor = editor.getCursor() @@ -129,11 +133,20 @@ EditorScrollViewComponent = React.createClass {top, left} - updateModelDimensions: -> + # Measure explicitly-styled height and width and relay them to the model. If + # these values aren't explicitly styled, we assume the editor is unconstrained + # and use the scrollHeight / scrollWidth as its height and width in + # calculations. + measureHeightAndWidth: -> {editor} = @props node = @getDOMNode() - editor.setHeight(node.clientHeight) - editor.setWidth(node.clientWidth) + computedStyle = getComputedStyle(node) + + unless computedStyle.height is '0px' + editor.setHeight(node.clientHeight) + + unless computedStyle.width is '0px' + editor.setWidth(node.clientWidth) focus: -> @refs.input.focus() diff --git a/src/gutter-component.coffee b/src/gutter-component.coffee index b8dc9874d..05b122cea 100644 --- a/src/gutter-component.coffee +++ b/src/gutter-component.coffee @@ -9,6 +9,10 @@ GutterComponent = React.createClass mixins: [SubscriberMixin] render: -> + div className: 'gutter', + @renderLineNumbers() if @isMounted() + + renderLineNumbers: -> {editor, visibleRowRange, preservedScreenRow, scrollTop} = @props [startRow, endRow] = visibleRowRange lineHeightInPixels = editor.getLineHeight() @@ -21,6 +25,7 @@ GutterComponent = React.createClass lineNumbers = [] tokenizedLines = editor.linesForScreenRows(startRow, endRow - 1) + tokenizedLines.push({id: 0}) if tokenizedLines.length is 0 for bufferRow, i in editor.bufferRowsForScreenRows(startRow, endRow - 1) if bufferRow is lastBufferRow lineNumber = '•' @@ -28,7 +33,7 @@ GutterComponent = React.createClass lastBufferRow = bufferRow lineNumber = (bufferRow + 1).toString() - key = tokenizedLines[i]?.id ? 0 + key = tokenizedLines[i]?.id screenRow = startRow + i lineNumbers.push(LineNumberComponent({key, lineNumber, maxDigits, bufferRow, screenRow})) lastBufferRow = bufferRow @@ -36,9 +41,8 @@ GutterComponent = React.createClass if preservedScreenRow? and (preservedScreenRow < startRow or endRow <= preservedScreenRow) lineNumbers.push(LineNumberComponent({key: editor.lineForScreenRow(preservedScreenRow).id, preserved: true})) - div className: 'gutter', - div className: 'line-numbers', style: style, - lineNumbers + div className: 'line-numbers', style: style, + lineNumbers componentWillUnmount: -> @unsubscribe() diff --git a/src/lines-component.coffee b/src/lines-component.coffee index 7061572ae..9c63bdbd6 100644 --- a/src/lines-component.coffee +++ b/src/lines-component.coffee @@ -11,26 +11,28 @@ LinesComponent = React.createClass displayName: 'LinesComponent' render: -> - {editor, visibleRowRange, preservedScreenRow, showIndentGuide} = @props + if @isMounted() + {editor, visibleRowRange, preservedScreenRow, showIndentGuide} = @props + [startRow, endRow] = visibleRowRange - [startRow, endRow] = visibleRowRange - lineHeightInPixels = editor.getLineHeight() - paddingTop = startRow * lineHeightInPixels - paddingBottom = (editor.getScreenLineCount() - endRow) * lineHeightInPixels + style = + paddingTop: startRow * editor.getLineHeight() + paddingBottom: (editor.getScreenLineCount() - endRow) * editor.getLineHeight() - lines = - for tokenizedLine, i in editor.linesForScreenRows(startRow, endRow - 1) - LineComponent({key: tokenizedLine.id, tokenizedLine, showIndentGuide, screenRow: startRow + i}) + lines = + for tokenizedLine, i in editor.linesForScreenRows(startRow, endRow - 1) + LineComponent({key: tokenizedLine.id, tokenizedLine, showIndentGuide, screenRow: startRow + i}) - if preservedScreenRow? and (preservedScreenRow < startRow or endRow <= preservedScreenRow) - lines.push(LineComponent({key: editor.lineForScreenRow(preservedScreenRow).id, preserved: true})) + if preservedScreenRow? and (preservedScreenRow < startRow or endRow <= preservedScreenRow) + lines.push(LineComponent({key: editor.lineForScreenRow(preservedScreenRow).id, preserved: true})) - div className: 'lines', ref: 'lines', style: {paddingTop, paddingBottom}, - lines + div {className: 'lines', style}, lines + + componentWillMount: -> + @measuredLines = new WeakSet componentDidMount: -> - @measuredLines = new WeakSet - @updateModelDimensions() + @measureLineHeightAndCharWidth() shouldComponentUpdate: (newProps) -> return true unless isEqualForProperties(newProps, @props, 'visibleRowRange', 'preservedScreenRow', 'fontSize', 'fontFamily', 'lineHeight', 'showIndentGuide') @@ -42,31 +44,28 @@ LinesComponent = React.createClass false componentDidUpdate: (prevProps) -> - @updateModelDimensions() unless isEqualForProperties(prevProps, @props, 'fontSize', 'fontFamily', 'lineHeight') + @measureLineHeightAndCharWidth() unless isEqualForProperties(prevProps, @props, 'fontSize', 'fontFamily', 'lineHeight') @clearScopedCharWidths() unless isEqualForProperties(prevProps, @props, 'fontSize', 'fontFamily') @measureCharactersInNewLines() unless @props.preservedScreenRow? - updateModelDimensions: -> - {editor} = @props - {lineHeightInPixels, charWidth} = @measureLineDimensions() - editor.setLineHeight(lineHeightInPixels) - editor.setDefaultCharWidth(charWidth) - - measureLineDimensions: -> - linesNode = @refs.lines.getDOMNode() - linesNode.appendChild(DummyLineNode) - lineHeightInPixels = DummyLineNode.getBoundingClientRect().height + measureLineHeightAndCharWidth: -> + node = @getDOMNode() + node.appendChild(DummyLineNode) + lineHeight = DummyLineNode.getBoundingClientRect().height charWidth = DummyLineNode.firstChild.getBoundingClientRect().width - linesNode.removeChild(DummyLineNode) - {lineHeightInPixels, charWidth} + node.removeChild(DummyLineNode) + + {editor} = @props + editor.setLineHeight(lineHeight) + editor.setDefaultCharWidth(charWidth) measureCharactersInNewLines: -> [visibleStartRow, visibleEndRow] = @props.visibleRowRange - linesNode = @refs.lines.getDOMNode() + node = @getDOMNode() for tokenizedLine, i in @props.editor.linesForScreenRows(visibleStartRow, visibleEndRow - 1) unless @measuredLines.has(tokenizedLine) - lineNode = linesNode.children[i] + lineNode = node.children[i] @measureCharactersInLine(tokenizedLine, lineNode) measureCharactersInLine: (tokenizedLine, lineNode) -> diff --git a/src/selections-component.coffee b/src/selections-component.coffee index 7f94d99fb..616fc62dd 100644 --- a/src/selections-component.coffee +++ b/src/selections-component.coffee @@ -10,6 +10,7 @@ SelectionsComponent = React.createClass {editor} = @props div className: 'selections', - for selection in editor.getSelections() - if not selection.isEmpty() and editor.selectionIntersectsVisibleRowRange(selection) - SelectionComponent({key: selection.id, selection}) + if @isMounted() + for selection in editor.getSelections() + if not selection.isEmpty() and editor.selectionIntersectsVisibleRowRange(selection) + SelectionComponent({key: selection.id, selection})