diff --git a/spec/text-editor-component-spec.js b/spec/text-editor-component-spec.js index e23ede439..5acc2cefc 100644 --- a/spec/text-editor-component-spec.js +++ b/spec/text-editor-component-spec.js @@ -4368,6 +4368,33 @@ describe('TextEditorComponent', function () { }) }) + describe('width', function () { + it('sizes the editor element according to the content width when auto width is true, or according to the container width otherwise', function () { + contentNode.style.width = '600px' + component.measureDimensions() + editor.setText("abcdefghi") + runAnimationFrames() + expect(wrapperNode.offsetWidth).toBe(contentNode.offsetWidth) + + editor.update({autoWidth: true}) + runAnimationFrames() + const editorWidth1 = wrapperNode.offsetWidth + expect(editorWidth1).toBeGreaterThan(0) + expect(editorWidth1).toBeLessThan(contentNode.offsetWidth) + + editor.setText("abcdefghijkl") + editor.update({autoWidth: true}) + runAnimationFrames() + const editorWidth2 = wrapperNode.offsetWidth + expect(editorWidth2).toBeGreaterThan(editorWidth1) + expect(editorWidth2).toBeLessThan(contentNode.offsetWidth) + + editor.update({autoWidth: false}) + runAnimationFrames() + expect(wrapperNode.offsetWidth).toBe(contentNode.offsetWidth) + }) + }) + describe('when the "mini" property is true', function () { beforeEach(function () { editor.setMini(true) diff --git a/spec/text-editor-presenter-spec.coffee b/spec/text-editor-presenter-spec.coffee index e04eadaa2..728768799 100644 --- a/spec/text-editor-presenter-spec.coffee +++ b/spec/text-editor-presenter-spec.coffee @@ -425,6 +425,13 @@ describe "TextEditorPresenter", -> editor.setMini(false) expect(getState(presenter).horizontalScrollbar.visible).toBe true + it "is false when `editor.autoWidth` is true", -> + editor.update({autoWidth: true}) + presenter = buildPresenter(explicitHeight: 10, contentFrameWidth: 30, verticalScrollbarWidth: 7, baseCharacterWidth: 10) + getState(presenter) # trigger a state update to store state in the presenter + editor.setText('abcdefghijklm') + expect(getState(presenter).horizontalScrollbar.visible).toBe(false) + describe ".height", -> it "tracks the value of ::horizontalScrollbarHeight", -> presenter = buildPresenter(horizontalScrollbarHeight: 10) @@ -538,6 +545,21 @@ describe "TextEditorPresenter", -> presenter.setScrollLeft(10) expect(getState(presenter).content.scrollLeft).toBe 0 + it "is always 0 when `editor.autoWidth` is true", -> + editor.update({autoWidth: true}) + editor.setText('abcdefghijklm') + presenter = buildPresenter(explicitHeight: 10, contentFrameWidth: 30, verticalScrollbarWidth: 15, baseCharacterWidth: 10) + getState(presenter) # trigger a state update to store state in the presenter + + editor.setCursorBufferPosition([0, Infinity]) + editor.insertText('n') + expect(getState(presenter).content.scrollLeft).toBe(0) + + editor.setText('abcdefghijklm\nnopqrstuvwxy') # make the vertical scrollbar appear + editor.setCursorBufferPosition([1, Infinity]) + editor.insertText('z') + expect(getState(presenter).content.scrollLeft).toBe(0) + describe ".verticalScrollbar", -> describe ".visible", -> it "is true if the scrollHeight exceeds the computed client height", -> @@ -755,6 +777,39 @@ describe "TextEditorPresenter", -> expect(getState(presenter).hiddenInput.width).toBe 2 describe ".content", -> + describe '.width', -> + describe "when `editor.autoWidth` is false (the default)", -> + it "equals to the max width between the content frame width and the content width + the vertical scrollbar width", -> + editor.setText('abc\ndef\nghi\njkl') + presenter = buildPresenter(explicitHeight: 10, contentFrameWidth: 33, verticalScrollbarWidth: 7, baseCharacterWidth: 10) + expect(getState(presenter).content.width).toBe(3 * 10 + 7 + 1) + presenter.setContentFrameWidth(50) + expect(getState(presenter).content.width).toBe(50) + presenter.setVerticalScrollbarWidth(27) + expect(getState(presenter).content.width).toBe(3 * 10 + 27 + 1) + + describe "when `editor.autoWidth` is true", -> + it "equals to the content width + the vertical scrollbar width", -> + editor.setText('abc\ndef\nghi\njkl') + presenter = buildPresenter(explicitHeight: 10, contentFrameWidth: 300, verticalScrollbarWidth: 7, baseCharacterWidth: 10) + expectStateUpdate presenter, -> editor.update({autoWidth: true}) + expect(getState(presenter).content.width).toBe(3 * 10 + 7 + 1) + editor.setText('abcdefghi\n') + expect(getState(presenter).content.width).toBe(9 * 10 + 7 + 1) + + it "ignores the vertical scrollbar width when it is unset", -> + editor.setText('abcdef\nghijkl') + presenter = buildPresenter(explicitHeight: 10, contentFrameWidth: 33, verticalScrollbarWidth: 7, baseCharacterWidth: 10) + presenter.setVerticalScrollbarWidth(null) + expect(getState(presenter).content.width).toBe(6 * 10 + 1) + + it "ignores the content frame width when it is unset", -> + editor.setText('abc\ndef\nghi\njkl') + presenter = buildPresenter(explicitHeight: 10, contentFrameWidth: 33, verticalScrollbarWidth: 7, baseCharacterWidth: 10) + getState(presenter) # trigger a state update, causing verticalScrollbarWidth to be stored in the presenter + presenter.setContentFrameWidth(null) + expect(getState(presenter).content.width).toBe(3 * 10 + 7 + 1) + describe ".maxHeight", -> it "changes based on boundingClientRect", -> presenter = buildPresenter(scrollTop: 0, lineHeight: 10) @@ -2664,6 +2719,17 @@ describe "TextEditorPresenter", -> pixelPosition: {top: 10, left: 0} } + describe ".width", -> + it "is null when `editor.autoWidth` is false (the default)", -> + presenter = buildPresenter(explicitHeight: 50, gutterWidth: 20, contentFrameWidth: 300, baseCharacterWidth: 10) + expect(getState(presenter).width).toBeNull() + + it "equals to sum of .content.width and the width of the gutter when `editor.autoWidth` is true", -> + editor.setText('abcdef') + editor.update({autoWidth: true}) + presenter = buildPresenter(explicitHeight: 50, gutterWidth: 20, contentFrameWidth: 300, baseCharacterWidth: 10) + expect(getState(presenter).width).toBe(20 + 6 * 10 + 1) + describe ".height", -> it "updates model's rows per page when it changes", -> presenter = buildPresenter(explicitHeight: 50, lineHeightInPixels: 10, horizontalScrollbarHeight: 10) diff --git a/spec/text-editor-spec.coffee b/spec/text-editor-spec.coffee index 605b6cf05..e352a6c88 100644 --- a/spec/text-editor-spec.coffee +++ b/spec/text-editor-spec.coffee @@ -5516,6 +5516,14 @@ describe "TextEditor", -> editor.update({autoHeight: true}) expect(editor.getAutoHeight()).toBe(true) + describe "auto width", -> + it "returns false by default but can be customized", -> + expect(editor.getAutoWidth()).toBe(false) + editor.update({autoWidth: true}) + expect(editor.getAutoWidth()).toBe(true) + editor.update({autoWidth: false}) + expect(editor.getAutoWidth()).toBe(false) + describe '.get/setPlaceholderText()', -> it 'can be created with placeholderText', -> newEditor = atom.workspace.buildTextEditor( diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index 262b62301..d40f69e38 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -129,7 +129,7 @@ class TextEditorComponent updateSync: -> @updateSyncPreMeasurement() - @oldState ?= {} + @oldState ?= {width: null} @newState = @presenter.getPostMeasurementState() if @editor.getLastSelection()? and not @editor.getLastSelection().isEmpty() @@ -149,6 +149,13 @@ class TextEditorComponent else @domNode.style.height = '' + if @newState.width isnt @oldState.width + if @newState.width? + @hostElement.style.width = @newState.width + 'px' + else + @hostElement.style.width = '' + @oldState.width = @newState.width + if @newState.gutters.length @mountGutterContainerComponent() unless @gutterContainerComponent? @gutterContainerComponent.updateSync(@newState) diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index cb2d70a67..49df8ab80 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -110,13 +110,14 @@ class TextEditorPresenter @updateLines() - @updateFocusedState() - @updateHeightState() @updateVerticalScrollState() @updateHorizontalScrollState() @updateScrollbarsState() @updateHiddenInputState() @updateContentState() + @updateFocusedState() + @updateHeightState() + @updateWidthState() @updateHighlightDecorations() if @shouldUpdateDecorations @updateTilesState() @updateCursorsState() @@ -224,6 +225,12 @@ class TextEditorPresenter else @state.height = null + updateWidthState: -> + if @model.getAutoWidth() + @state.width = @state.content.width + @gutterWidth + else + @state.width = null + updateVerticalScrollState: -> @state.content.scrollHeight = @scrollHeight @sharedGutterStyles.scrollHeight = @scrollHeight @@ -269,7 +276,13 @@ class TextEditorPresenter @sharedGutterStyles.maxHeight = @boundingClientRect.height @state.content.maxHeight = @boundingClientRect.height - @state.content.width = Math.max(@contentWidth + @verticalScrollbarWidth, @contentFrameWidth) + verticalScrollbarWidth = @verticalScrollbarWidth ? 0 + contentFrameWidth = @contentFrameWidth ? 0 + contentWidth = @contentWidth ? 0 + if @model.getAutoWidth() + @state.content.width = contentWidth + verticalScrollbarWidth + else + @state.content.width = Math.max(contentWidth + verticalScrollbarWidth, contentFrameWidth) @state.content.scrollWidth = @scrollWidth @state.content.scrollLeft = @scrollLeft @state.content.backgroundColor = if @model.isMini() then null else @backgroundColor @@ -662,6 +675,7 @@ class TextEditorPresenter if @contentWidth isnt oldContentWidth @updateScrollbarDimensions() + @updateClientWidth() @updateScrollWidth() updateClientHeight: -> @@ -678,7 +692,11 @@ class TextEditorPresenter updateClientWidth: -> return unless @contentFrameWidth? and @verticalScrollbarWidth? - clientWidth = @contentFrameWidth - @verticalScrollbarWidth + if @model.getAutoWidth() + clientWidth = @contentWidth + else + clientWidth = @contentFrameWidth - @verticalScrollbarWidth + @model.setWidth(clientWidth, true) unless @editorWidthInChars unless @clientWidth is clientWidth @@ -720,20 +738,23 @@ class TextEditorPresenter return unless @measuredVerticalScrollbarWidth? and @measuredHorizontalScrollbarHeight? return unless @contentWidth? and @contentHeight? - clientWidthWithoutVerticalScrollbar = @contentFrameWidth - clientWidthWithVerticalScrollbar = clientWidthWithoutVerticalScrollbar - @measuredVerticalScrollbarWidth - clientHeightWithoutHorizontalScrollbar = @height - clientHeightWithHorizontalScrollbar = clientHeightWithoutHorizontalScrollbar - @measuredHorizontalScrollbarHeight + if @model.getAutoWidth() + clientWidthWithVerticalScrollbar = @contentWidth + @measuredVerticalScrollbarWidth + else + clientWidthWithVerticalScrollbar = @contentFrameWidth + clientWidthWithoutVerticalScrollbar = clientWidthWithVerticalScrollbar - @measuredVerticalScrollbarWidth + clientHeightWithHorizontalScrollbar = @height + clientHeightWithoutHorizontalScrollbar = clientHeightWithHorizontalScrollbar - @measuredHorizontalScrollbarHeight horizontalScrollbarVisible = not @model.isMini() and - (@contentWidth > clientWidthWithoutVerticalScrollbar or - @contentWidth > clientWidthWithVerticalScrollbar and @contentHeight > clientHeightWithoutHorizontalScrollbar) + (@contentWidth > clientWidthWithVerticalScrollbar or + @contentWidth > clientWidthWithoutVerticalScrollbar and @contentHeight > clientHeightWithHorizontalScrollbar) verticalScrollbarVisible = not @model.isMini() and - (@contentHeight > clientHeightWithoutHorizontalScrollbar or - @contentHeight > clientHeightWithHorizontalScrollbar and @contentWidth > clientWidthWithoutVerticalScrollbar) + (@contentHeight > clientHeightWithHorizontalScrollbar or + @contentHeight > clientHeightWithoutHorizontalScrollbar and @contentWidth > clientWidthWithVerticalScrollbar) horizontalScrollbarHeight = if horizontalScrollbarVisible @@ -896,6 +917,9 @@ class TextEditorPresenter @updateScrollHeight() @updateEndRow() + didChangeAutoWidth: -> + @emitDidUpdateState() + setContentFrameWidth: (contentFrameWidth) -> if @contentFrameWidth isnt contentFrameWidth or @editorWidthInChars? oldContentFrameWidth = @contentFrameWidth diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 354ac8069..420bee702 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -127,7 +127,7 @@ class TextEditor extends Model @softTabs, @firstVisibleScreenRow, @firstVisibleScreenColumn, initialLine, initialColumn, tabLength, @softWrapped, @decorationManager, @selectionsMarkerLayer, @buffer, suppressCursorCreation, @mini, @placeholderText, lineNumberGutterVisible, @largeFileMode, @clipboard, - @assert, grammar, @showInvisibles, @autoHeight, @scrollPastEnd, @editorWidthInChars, + @assert, grammar, @showInvisibles, @autoHeight, @autoWidth, @scrollPastEnd, @editorWidthInChars, @tokenizedBuffer, @displayLayer, @invisibles, @showIndentGuide, @softWrapHangingIndentLength, @softWrapped, @softWrapAtPreferredLineLength, @preferredLineLength } = params @@ -144,6 +144,7 @@ class TextEditor extends Model @selections = [] @hasTerminatedPendingState = false + @autoWidth ?= false @autoHeight ?= true @mini ?= false @scrollPastEnd ?= true @@ -320,6 +321,10 @@ class TextEditor extends Model @autoHeight = value @editorElement?.didChangeAutoHeight() + when 'autoWidth' + if value isnt @autoWidth + @autoWidth = value + @presenter?.didChangeAutoWidth() else throw new TypeError("Invalid TextEditor parameter: '#{param}'") @@ -3552,6 +3557,9 @@ class TextEditor extends Model Grim.deprecate("This is now a view method. Call TextEditorElement::getWidth instead.") @width + getAutoWidth: -> + @autoWidth + # Experimental: Scroll the editor such that the given screen row is at the # top of the visible area. setFirstVisibleScreenRow: (screenRow, fromView) ->