diff --git a/spec/text-editor-component-spec.coffee b/spec/text-editor-component-spec.coffee index 826affb42..74cdfd7b2 100644 --- a/spec/text-editor-component-spec.coffee +++ b/spec/text-editor-component-spec.coffee @@ -280,6 +280,7 @@ describe "TextEditorComponent", -> expect(tileNode.style.backgroundColor).toBe(backgroundColor) wrapperNode.style.backgroundColor = 'rgb(255, 0, 0)' + atom.views.performDocumentPoll() advanceClock(atom.views.documentPollingInterval) nextAnimationFrame() @@ -701,7 +702,8 @@ describe "TextEditorComponent", -> # favor gutter color if it's assigned gutterNode.style.backgroundColor = 'rgb(255, 0, 0)' - advanceClock(atom.views.documentPollingInterval) + atom.views.performDocumentPoll() + nextAnimationFrame() expect(lineNumbersNode.style.backgroundColor).toBe 'rgb(255, 0, 0)' for tileNode in lineNumbersNode.querySelectorAll(".tile") @@ -800,6 +802,7 @@ describe "TextEditorComponent", -> describe "when the component is destroyed", -> it "stops listening for folding events", -> + nextAnimationFrame() component.destroy() lineNumber = component.lineNumberNodeForScreenRow(1) @@ -824,6 +827,8 @@ describe "TextEditorComponent", -> expect(lineNumberHasClass(1, 'folded')).toBe false it "does not fold when the line number componentNode is clicked", -> + nextAnimationFrame() # clear pending frame request + lineNumber = component.lineNumberNodeForScreenRow(1) lineNumber.dispatchEvent(buildClickEvent(lineNumber)) expect(nextAnimationFrame).toBe noAnimationFrame @@ -2600,7 +2605,7 @@ describe "TextEditorComponent", -> expect(componentNode.querySelectorAll('.line').length).toBe 0 hiddenParent.style.display = 'block' - advanceClock(atom.views.documentPollingInterval) + atom.views.performDocumentPoll() expect(componentNode.querySelectorAll('.line').length).toBeGreaterThan 0 @@ -2710,13 +2715,13 @@ describe "TextEditorComponent", -> expect(parseInt(newHeight)).toBeLessThan wrapperNode.offsetHeight wrapperNode.style.height = newHeight - advanceClock(atom.views.documentPollingInterval) + atom.views.performDocumentPoll() nextAnimationFrame() expect(componentNode.querySelectorAll('.line')).toHaveLength(6) gutterWidth = componentNode.querySelector('.gutter').offsetWidth componentNode.style.width = gutterWidth + 14 * charWidth + editor.getVerticalScrollbarWidth() + 'px' - advanceClock(atom.views.documentPollingInterval) + atom.views.performDocumentPoll() nextAnimationFrame() expect(componentNode.querySelector('.line').textContent).toBe "var quicksort " @@ -2725,7 +2730,7 @@ describe "TextEditorComponent", -> scrollViewNode.style.paddingLeft = 20 + 'px' componentNode.style.width = 30 * charWidth + 'px' - advanceClock(atom.views.documentPollingInterval) + atom.views.performDocumentPoll() nextAnimationFrame() expect(component.lineNodeForScreenRow(0).textContent).toBe "var quicksort = " diff --git a/spec/view-registry-spec.coffee b/spec/view-registry-spec.coffee index df822309e..a12d46dde 100644 --- a/spec/view-registry-spec.coffee +++ b/spec/view-registry-spec.coffee @@ -7,6 +7,9 @@ describe "ViewRegistry", -> beforeEach -> registry = new ViewRegistry + afterEach -> + registry.clearDocumentRequests() + describe "::getView(object)", -> describe "when passed a DOM node", -> it "returns the given DOM node", -> @@ -158,13 +161,13 @@ describe "ViewRegistry", -> registry.updateDocument -> events.push('write') registry.readDocument -> events.push('read') - advanceClock(registry.documentPollingInterval) + window.dispatchEvent(new UIEvent('resize')) expect(events).toEqual [] frameRequests[0]() expect(events).toEqual ['write', 'read', 'poll'] - advanceClock(registry.documentPollingInterval) + window.dispatchEvent(new UIEvent('resize')) expect(events).toEqual ['write', 'read', 'poll', 'poll'] it "polls the document after updating when ::pollAfterNextUpdate() has been called", -> @@ -183,25 +186,51 @@ describe "ViewRegistry", -> expect(events).toEqual ['write', 'read', 'poll'] describe "::pollDocument(fn)", -> - it "calls all registered reader functions on an interval until they are disabled via a returned disposable", -> - spyOn(window, 'setInterval').andCallFake(fakeSetInterval) + [testElement, testStyleSheet, disposable1, disposable2, events] = [] + + beforeEach -> + testElement = document.createElement('div') + testStyleSheet = document.createElement('style') + testStyleSheet.textContent = 'body {}' + jasmineContent = document.getElementById('jasmine-content') + jasmineContent.appendChild(testElement) + jasmineContent.appendChild(testStyleSheet) events = [] disposable1 = registry.pollDocument -> events.push('poll 1') disposable2 = registry.pollDocument -> events.push('poll 2') + it "calls all registered polling functions after document or stylesheet changes until they are disabled via a returned disposable", -> + jasmine.useRealClock() expect(events).toEqual [] - advanceClock(registry.documentPollingInterval) + testElement.style.width = '400px' + + waitsFor "events to occur in response to DOM mutation", -> events.length > 0 + + runs -> + expect(events).toEqual ['poll 1', 'poll 2'] + events.length = 0 + + testStyleSheet.textContent = 'body {color: #333;}' + + waitsFor "events to occur in reponse to style sheet mutation", -> events.length > 0 + + runs -> + expect(events).toEqual ['poll 1', 'poll 2'] + events.length = 0 + + disposable1.dispose() + testElement.style.color = '#fff' + + waitsFor "more events to occur in response to DOM mutation", -> events.length > 0 + + runs -> + expect(events).toEqual ['poll 2'] + + it "calls all registered polling functions when the window resizes", -> + expect(events).toEqual [] + + window.dispatchEvent(new UIEvent('resize')) + expect(events).toEqual ['poll 1', 'poll 2'] - - advanceClock(registry.documentPollingInterval) - expect(events).toEqual ['poll 1', 'poll 2', 'poll 1', 'poll 2'] - - disposable1.dispose() - advanceClock(registry.documentPollingInterval) - expect(events).toEqual ['poll 1', 'poll 2', 'poll 1', 'poll 2', 'poll 2'] - - disposable2.dispose() - advanceClock(registry.documentPollingInterval) - expect(events).toEqual ['poll 1', 'poll 2', 'poll 1', 'poll 2', 'poll 2'] diff --git a/src/text-editor-component.coffee b/src/text-editor-component.coffee index 45591c195..8a1469bb9 100644 --- a/src/text-editor-component.coffee +++ b/src/text-editor-component.coffee @@ -107,7 +107,6 @@ class TextEditorComponent @disposables.dispose() @presenter.destroy() @gutterContainerComponent?.destroy() - window.removeEventListener 'resize', @requestHeightAndWidthMeasurement getDomNode: -> @domNode @@ -224,7 +223,6 @@ class TextEditorComponent @domNode.addEventListener 'textInput', @onTextInput @scrollViewNode.addEventListener 'mousedown', @onMouseDown @scrollViewNode.addEventListener 'scroll', @onScrollViewScroll - window.addEventListener 'resize', @requestHeightAndWidthMeasurement @listenForIMEEvents() @trackSelectionClipboard() if process.platform is 'linux' @@ -589,15 +587,6 @@ class TextEditorComponent else @wasVisible = false - requestHeightAndWidthMeasurement: => - return if @heightAndWidthMeasurementRequested - - @heightAndWidthMeasurementRequested = true - requestAnimationFrame => - @heightAndWidthMeasurementRequested = false - @measureDimensions() - @measureWindowSize() - # 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 diff --git a/src/text-editor-presenter.coffee b/src/text-editor-presenter.coffee index 8eb80275a..7bb84bd1a 100644 --- a/src/text-editor-presenter.coffee +++ b/src/text-editor-presenter.coffee @@ -1386,12 +1386,15 @@ class TextEditorPresenter @emitDidUpdateState() startBlinkingCursors: -> - unless @toggleCursorBlinkHandle + unless @isCursorBlinking() @state.content.cursorsVisible = true @toggleCursorBlinkHandle = setInterval(@toggleCursorBlink.bind(this), @getCursorBlinkPeriod() / 2) + isCursorBlinking: -> + @toggleCursorBlinkHandle? + stopBlinkingCursors: (visible) -> - if @toggleCursorBlinkHandle + if @isCursorBlinking() @state.content.cursorsVisible = visible clearInterval(@toggleCursorBlinkHandle) @toggleCursorBlinkHandle = null @@ -1401,7 +1404,8 @@ class TextEditorPresenter @emitDidUpdateState() pauseCursorBlinking: -> - @stopBlinkingCursors(true) - @startBlinkingCursorsAfterDelay ?= _.debounce(@startBlinkingCursors, @getCursorBlinkResumeDelay()) - @startBlinkingCursorsAfterDelay() - @emitDidUpdateState() + if @isCursorBlinking() + @stopBlinkingCursors(true) + @startBlinkingCursorsAfterDelay ?= _.debounce(@startBlinkingCursors, @getCursorBlinkResumeDelay()) + @startBlinkingCursorsAfterDelay() + @emitDidUpdateState() diff --git a/src/view-registry.coffee b/src/view-registry.coffee index 24c53bd57..58b2bf7c0 100644 --- a/src/view-registry.coffee +++ b/src/view-registry.coffee @@ -1,6 +1,7 @@ {find} = require 'underscore-plus' Grim = require 'grim' {Disposable} = require 'event-kit' +_ = require 'underscore-plus' # Essential: `ViewRegistry` handles the association between model and view # types in Atom. We call this association a View Provider. As in, for a given @@ -42,11 +43,11 @@ Grim = require 'grim' # ``` module.exports = class ViewRegistry - documentPollingInterval: 200 documentUpdateRequested: false documentReadInProgress: false performDocumentPollAfterUpdate: false - pollIntervalHandle: null + debouncedPerformDocumentPoll: null + minimumPollInterval: 200 constructor: -> @views = new WeakMap @@ -55,6 +56,9 @@ class ViewRegistry @documentReaders = [] @documentPollers = [] + @observer = new MutationObserver(@requestDocumentPoll) + @debouncedPerformDocumentPoll = _.throttle(@performDocumentPoll, @minimumPollInterval).bind(this) + # Essential: Add a provider that will be used to construct views in the # workspace's view layer based on model objects in its model layer. # @@ -208,14 +212,19 @@ class ViewRegistry writer() while writer = @documentWriters.shift() startPollingDocument: -> - @pollIntervalHandle = window.setInterval(@performDocumentPoll, @documentPollingInterval) + window.addEventListener('resize', @requestDocumentPoll) + @observer.observe(document, {subtree: true, childList: true, attributes: true}) stopPollingDocument: -> - window.clearInterval(@pollIntervalHandle) + window.removeEventListener('resize', @requestDocumentPoll) + @observer.disconnect() - performDocumentPoll: => + requestDocumentPoll: => if @documentUpdateRequested @performDocumentPollAfterUpdate = true else - poller() for poller in @documentPollers - return + @debouncedPerformDocumentPoll() + + performDocumentPoll: -> + poller() for poller in @documentPollers + return diff --git a/src/workspace-element.coffee b/src/workspace-element.coffee index 028a6e561..e07d9c90f 100644 --- a/src/workspace-element.coffee +++ b/src/workspace-element.coffee @@ -12,7 +12,6 @@ class WorkspaceElement extends HTMLElement createdCallback: -> @subscriptions = new CompositeDisposable - @initializeGlobalTextEditorStyleSheet() @initializeContent() @observeScrollbarStyle() @observeTextEditorFontConfig() @@ -26,10 +25,6 @@ class WorkspaceElement extends HTMLElement @subscriptions.dispose() @model.destroy() - initializeGlobalTextEditorStyleSheet: -> - atom.styles.addStyleSheet('atom-text-editor {}', sourcePath: 'global-text-editor-styles') - @globalTextEditorStyleSheet = document.head.querySelector('style[source-path="global-text-editor-styles"]').sheet - initializeContent: -> @classList.add 'workspace' @setAttribute 'tabindex', -1 @@ -54,9 +49,20 @@ class WorkspaceElement extends HTMLElement @classList.add("scrollbars-visible-when-scrolling") observeTextEditorFontConfig: -> - @subscriptions.add atom.config.observe 'editor.fontSize', @setTextEditorFontSize.bind(this) - @subscriptions.add atom.config.observe 'editor.fontFamily', @setTextEditorFontFamily.bind(this) - @subscriptions.add atom.config.observe 'editor.lineHeight', @setTextEditorLineHeight.bind(this) + @updateGlobalTextEditorStyleSheet() + @subscriptions.add atom.config.onDidChange 'editor.fontSize', @updateGlobalTextEditorStyleSheet.bind(this) + @subscriptions.add atom.config.onDidChange 'editor.fontFamily', @updateGlobalTextEditorStyleSheet.bind(this) + @subscriptions.add atom.config.onDidChange 'editor.lineHeight', @updateGlobalTextEditorStyleSheet.bind(this) + + updateGlobalTextEditorStyleSheet: -> + styleSheetSource = """ + atom-text-editor { + font-size: #{atom.config.get('editor.fontSize')}px; + font-family: #{atom.config.get('editor.fontFamily')}; + line-height: #{atom.config.get('editor.lineHeight')}; + } + """ + atom.styles.addStyleSheet(styleSheetSource, sourcePath: 'global-text-editor-styles') createSpacePenShim: -> WorkspaceView ?= require './workspace-view' @@ -87,20 +93,6 @@ class WorkspaceElement extends HTMLElement getModel: -> @model - setTextEditorFontSize: (fontSize) -> - @updateGlobalEditorStyle('font-size', fontSize + 'px') - - setTextEditorFontFamily: (fontFamily) -> - @updateGlobalEditorStyle('font-family', fontFamily) - - setTextEditorLineHeight: (lineHeight) -> - @updateGlobalEditorStyle('line-height', lineHeight) - - updateGlobalEditorStyle: (property, value) -> - editorRule = @globalTextEditorStyleSheet.cssRules[0] - editorRule.style[property] = value - atom.themes.emitter.emit 'did-update-stylesheet', @globalTextEditorStyleSheet - handleFocus: (event) -> @model.getActivePane().activate()