mirror of
https://github.com/atom/atom.git
synced 2026-01-22 21:38:10 -05:00
Merge pull request #12696 from atom/tj-upgrade-electron
Upgrade Electron to v1.6.x
This commit is contained in:
@@ -26,7 +26,7 @@ export default async function ({test}) {
|
||||
|
||||
let t0 = window.performance.now()
|
||||
const buffer = new TextBuffer({text})
|
||||
const editor = new TextEditor({buffer, largeFileMode: true})
|
||||
const editor = new TextEditor({buffer, autoHeight: false, largeFileMode: true})
|
||||
atom.workspace.getActivePane().activateItem(editor)
|
||||
let t1 = window.performance.now()
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ export default async function ({test}) {
|
||||
|
||||
let t0 = window.performance.now()
|
||||
const buffer = new TextBuffer({text})
|
||||
const editor = new TextEditor({buffer, largeFileMode: true})
|
||||
const editor = new TextEditor({buffer, autoHeight: false, largeFileMode: true})
|
||||
editor.setGrammar(atom.grammars.grammarForScopeName('source.js'))
|
||||
atom.workspace.getActivePane().activateItem(editor)
|
||||
let t1 = window.performance.now()
|
||||
|
||||
24
package.json
24
package.json
@@ -12,7 +12,7 @@
|
||||
"url": "https://github.com/atom/atom/issues"
|
||||
},
|
||||
"license": "MIT",
|
||||
"electronVersion": "1.3.15",
|
||||
"electronVersion": "1.6.9",
|
||||
"dependencies": {
|
||||
"async": "0.2.6",
|
||||
"atom-keymap": "8.1.2",
|
||||
@@ -27,9 +27,10 @@
|
||||
"color": "^0.7.3",
|
||||
"dedent": "^0.6.0",
|
||||
"devtron": "1.3.0",
|
||||
"etch": "^0.12.4",
|
||||
"event-kit": "^2.3.0",
|
||||
"find-parent-dir": "^0.3.0",
|
||||
"first-mate": "7.0.4",
|
||||
"first-mate": "7.0.6",
|
||||
"fs-plus": "^3.0.0",
|
||||
"fstream": "0.1.24",
|
||||
"fuzzaldrin": "^2.1",
|
||||
@@ -40,7 +41,7 @@
|
||||
"jasmine-tagged": "^1.1.4",
|
||||
"key-path-helpers": "^0.4.0",
|
||||
"less-cache": "1.1.0",
|
||||
"line-top-index": "0.2.0",
|
||||
"line-top-index": "0.3.1",
|
||||
"marked": "^0.3.6",
|
||||
"minimatch": "^3.0.3",
|
||||
"mocha": "2.5.1",
|
||||
@@ -64,7 +65,7 @@
|
||||
"sinon": "1.17.4",
|
||||
"@atom/source-map-support": "^0.3.4",
|
||||
"temp": "^0.8.3",
|
||||
"text-buffer": "11.4.1",
|
||||
"text-buffer": "12.1.4",
|
||||
"typescript-simple": "1.0.0",
|
||||
"underscore-plus": "^1.6.6",
|
||||
"winreg": "^1.2.1",
|
||||
@@ -94,7 +95,7 @@
|
||||
"autosave": "0.24.3",
|
||||
"background-tips": "0.27.0",
|
||||
"bookmarks": "0.44.4",
|
||||
"bracket-matcher": "0.85.5",
|
||||
"bracket-matcher": "0.85.6",
|
||||
"command-palette": "0.40.4",
|
||||
"dalek": "0.2.1",
|
||||
"deprecation-cop": "0.56.7",
|
||||
@@ -102,7 +103,7 @@
|
||||
"encoding-selector": "0.23.3",
|
||||
"exception-reporting": "0.41.4",
|
||||
"find-and-replace": "0.208.1",
|
||||
"fuzzy-finder": "1.5.6",
|
||||
"fuzzy-finder": "1.5.7",
|
||||
"github": "0.1.1",
|
||||
"git-diff": "1.3.6",
|
||||
"go-to-line": "0.32.1",
|
||||
@@ -113,7 +114,7 @@
|
||||
"line-ending-selector": "0.6.3",
|
||||
"link": "0.31.3",
|
||||
"markdown-preview": "0.159.12",
|
||||
"metrics": "1.2.3",
|
||||
"metrics": "1.2.4",
|
||||
"notifications": "0.67.1",
|
||||
"open-on-github": "1.2.1",
|
||||
"package-generator": "1.1.1",
|
||||
@@ -125,7 +126,7 @@
|
||||
"symbols-view": "0.116.0",
|
||||
"tabs": "0.106.0",
|
||||
"timecop": "0.36.0",
|
||||
"tree-view": "0.217.0-7",
|
||||
"tree-view": "0.217.0-8",
|
||||
"update-package-dependencies": "0.12.0",
|
||||
"welcome": "0.36.3",
|
||||
"whitespace": "0.36.2",
|
||||
@@ -185,7 +186,12 @@
|
||||
"spyOn",
|
||||
"waitsFor",
|
||||
"waitsForPromise",
|
||||
"indexedDB"
|
||||
"indexedDB",
|
||||
"IntersectionObserver",
|
||||
"FocusEvent",
|
||||
"requestAnimationFrame",
|
||||
"HTMLElement",
|
||||
"snapshotResult"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
"coffeelint": "1.15.7",
|
||||
"colors": "1.1.2",
|
||||
"csslint": "1.0.2",
|
||||
"donna": "1.0.13",
|
||||
"electron-chromedriver": "~1.3",
|
||||
"donna": "1.0.16",
|
||||
"electron-chromedriver": "~1.6",
|
||||
"electron-link": "0.0.24",
|
||||
"electron-mksnapshot": "~1.3",
|
||||
"electron-mksnapshot": "~1.6",
|
||||
"electron-packager": "7.3.0",
|
||||
"electron-winstaller": "2.5.2",
|
||||
"fs-extra": "0.30.0",
|
||||
|
||||
@@ -29,10 +29,12 @@ describe "AtomEnvironment", ->
|
||||
atom.setSize(originalSize.width, originalSize.height)
|
||||
|
||||
it 'sets the size of the window, and can retrieve the size just set', ->
|
||||
newWidth = originalSize.width + 12
|
||||
newHeight = originalSize.height + 23
|
||||
atom.setSize(newWidth, newHeight)
|
||||
expect(atom.getSize()).toEqual width: newWidth, height: newHeight
|
||||
newWidth = originalSize.width - 12
|
||||
newHeight = originalSize.height - 23
|
||||
waitsForPromise ->
|
||||
atom.setSize(newWidth, newHeight)
|
||||
runs ->
|
||||
expect(atom.getSize()).toEqual width: newWidth, height: newHeight
|
||||
|
||||
describe ".isReleasedVersion()", ->
|
||||
it "returns false if the version is a SHA and true otherwise", ->
|
||||
@@ -221,44 +223,70 @@ describe "AtomEnvironment", ->
|
||||
atom.loadState().then (state) -> expect(state).toEqual(serializedState)
|
||||
|
||||
it "saves state when the CPU is idle after a keydown or mousedown event", ->
|
||||
spyOn(atom, 'saveState')
|
||||
atomEnv = new AtomEnvironment({
|
||||
applicationDelegate: global.atom.applicationDelegate,
|
||||
})
|
||||
idleCallbacks = []
|
||||
spyOn(window, 'requestIdleCallback').andCallFake (callback) -> idleCallbacks.push(callback)
|
||||
atomEnv.initialize({
|
||||
window: {
|
||||
requestIdleCallback: (callback) -> idleCallbacks.push(callback),
|
||||
addEventListener: ->
|
||||
removeEventListener: ->
|
||||
},
|
||||
document: document.implementation.createHTMLDocument()
|
||||
})
|
||||
|
||||
spyOn(atomEnv, 'saveState')
|
||||
|
||||
keydown = new KeyboardEvent('keydown')
|
||||
atom.document.dispatchEvent(keydown)
|
||||
advanceClock atom.saveStateDebounceInterval
|
||||
atomEnv.document.dispatchEvent(keydown)
|
||||
advanceClock atomEnv.saveStateDebounceInterval
|
||||
idleCallbacks.shift()()
|
||||
expect(atom.saveState).toHaveBeenCalledWith({isUnloading: false})
|
||||
expect(atom.saveState).not.toHaveBeenCalledWith({isUnloading: true})
|
||||
expect(atomEnv.saveState).toHaveBeenCalledWith({isUnloading: false})
|
||||
expect(atomEnv.saveState).not.toHaveBeenCalledWith({isUnloading: true})
|
||||
|
||||
atom.saveState.reset()
|
||||
atomEnv.saveState.reset()
|
||||
mousedown = new MouseEvent('mousedown')
|
||||
atom.document.dispatchEvent(mousedown)
|
||||
advanceClock atom.saveStateDebounceInterval
|
||||
atomEnv.document.dispatchEvent(mousedown)
|
||||
advanceClock atomEnv.saveStateDebounceInterval
|
||||
idleCallbacks.shift()()
|
||||
expect(atom.saveState).toHaveBeenCalledWith({isUnloading: false})
|
||||
expect(atom.saveState).not.toHaveBeenCalledWith({isUnloading: true})
|
||||
expect(atomEnv.saveState).toHaveBeenCalledWith({isUnloading: false})
|
||||
expect(atomEnv.saveState).not.toHaveBeenCalledWith({isUnloading: true})
|
||||
|
||||
atomEnv.destroy()
|
||||
|
||||
it "ignores mousedown/keydown events happening after calling unloadEditorWindow", ->
|
||||
spyOn(atom, 'saveState')
|
||||
atomEnv = new AtomEnvironment({
|
||||
applicationDelegate: global.atom.applicationDelegate,
|
||||
})
|
||||
idleCallbacks = []
|
||||
spyOn(window, 'requestIdleCallback').andCallFake (callback) -> idleCallbacks.push(callback)
|
||||
atomEnv.initialize({
|
||||
window: {
|
||||
requestIdleCallback: (callback) -> idleCallbacks.push(callback),
|
||||
addEventListener: ->
|
||||
removeEventListener: ->
|
||||
},
|
||||
document: document.implementation.createHTMLDocument()
|
||||
})
|
||||
|
||||
spyOn(atomEnv, 'saveState')
|
||||
|
||||
mousedown = new MouseEvent('mousedown')
|
||||
atom.document.dispatchEvent(mousedown)
|
||||
atom.unloadEditorWindow()
|
||||
expect(atom.saveState).not.toHaveBeenCalled()
|
||||
atomEnv.document.dispatchEvent(mousedown)
|
||||
atomEnv.unloadEditorWindow()
|
||||
expect(atomEnv.saveState).not.toHaveBeenCalled()
|
||||
|
||||
advanceClock atom.saveStateDebounceInterval
|
||||
advanceClock atomEnv.saveStateDebounceInterval
|
||||
idleCallbacks.shift()()
|
||||
expect(atom.saveState).not.toHaveBeenCalled()
|
||||
expect(atomEnv.saveState).not.toHaveBeenCalled()
|
||||
|
||||
mousedown = new MouseEvent('mousedown')
|
||||
atom.document.dispatchEvent(mousedown)
|
||||
advanceClock atom.saveStateDebounceInterval
|
||||
atomEnv.document.dispatchEvent(mousedown)
|
||||
advanceClock atomEnv.saveStateDebounceInterval
|
||||
idleCallbacks.shift()()
|
||||
expect(atom.saveState).not.toHaveBeenCalled()
|
||||
expect(atomEnv.saveState).not.toHaveBeenCalled()
|
||||
|
||||
atomEnv.destroy()
|
||||
|
||||
it "serializes the project state with all the options supplied in saveState", ->
|
||||
spyOn(atom.project, 'serialize').andReturn({foo: 42})
|
||||
@@ -288,10 +316,13 @@ describe "AtomEnvironment", ->
|
||||
}
|
||||
)
|
||||
})
|
||||
atom2.initialize({document, window})
|
||||
atom2.deserialize(atom.serialize())
|
||||
|
||||
expect(atom2.textEditors.getGrammarOverride(editor)).toBe('text.plain')
|
||||
|
||||
atom2.destroy()
|
||||
|
||||
describe "openInitialEmptyEditorIfNecessary", ->
|
||||
describe "when there are no paths set", ->
|
||||
beforeEach ->
|
||||
@@ -452,11 +483,14 @@ describe "AtomEnvironment", ->
|
||||
}
|
||||
atomEnvironment = new AtomEnvironment({applicationDelegate: atom.applicationDelegate})
|
||||
atomEnvironment.initialize({window, document: fakeDocument})
|
||||
spyOn(atomEnvironment.packages, 'getAvailablePackagePaths').andReturn []
|
||||
spyOn(atomEnvironment, 'displayWindow').andReturn Promise.resolve()
|
||||
atomEnvironment.startEditorWindow()
|
||||
atomEnvironment.unloadEditorWindow()
|
||||
atomEnvironment.destroy()
|
||||
spyOn(atomEnvironment.packages, 'loadPackages').andReturn(Promise.resolve())
|
||||
spyOn(atomEnvironment.packages, 'activate').andReturn(Promise.resolve())
|
||||
spyOn(atomEnvironment, 'displayWindow').andReturn(Promise.resolve())
|
||||
waitsForPromise ->
|
||||
atomEnvironment.startEditorWindow()
|
||||
runs ->
|
||||
atomEnvironment.unloadEditorWindow()
|
||||
atomEnvironment.destroy()
|
||||
|
||||
describe "::whenShellEnvironmentLoaded()", ->
|
||||
[atomEnvironment, envLoaded, spy] = []
|
||||
@@ -471,21 +505,19 @@ describe "AtomEnvironment", ->
|
||||
applicationDelegate: atom.applicationDelegate
|
||||
updateProcessEnv: -> promise
|
||||
atomEnvironment.initialize({window, document})
|
||||
spyOn(atomEnvironment.packages, 'getAvailablePackagePaths').andReturn []
|
||||
spyOn(atomEnvironment, 'displayWindow').andReturn Promise.resolve()
|
||||
spy = jasmine.createSpy()
|
||||
atomEnvironment.startEditorWindow()
|
||||
|
||||
afterEach ->
|
||||
atomEnvironment.unloadEditorWindow()
|
||||
atomEnvironment.destroy()
|
||||
|
||||
it "is triggered once the shell environment is loaded", ->
|
||||
atomEnvironment.whenShellEnvironmentLoaded spy
|
||||
atomEnvironment.updateProcessEnvAndTriggerHooks()
|
||||
envLoaded()
|
||||
runs -> expect(spy).toHaveBeenCalled()
|
||||
|
||||
it "triggers the callback immediately if the shell environment is already loaded", ->
|
||||
atomEnvironment.updateProcessEnvAndTriggerHooks()
|
||||
envLoaded()
|
||||
runs ->
|
||||
atomEnvironment.whenShellEnvironmentLoaded spy
|
||||
|
||||
@@ -21,8 +21,8 @@ formatStackTrace = (spec, message='', stackTrace) ->
|
||||
lines.shift() if message.trim() is errorMatch?[1]?.trim()
|
||||
|
||||
for line, index in lines
|
||||
# Remove prefix of lines matching: at .<anonymous> (path:1:2)
|
||||
prefixMatch = line.match(/at \.<anonymous> \(([^)]+)\)/)
|
||||
# Remove prefix of lines matching: at jasmine.Spec.<anonymous> (path:1:2)
|
||||
prefixMatch = line.match(/at jasmine\.Spec\.<anonymous> \(([^)]+)\)/)
|
||||
line = "at #{prefixMatch[1]}" if prefixMatch
|
||||
|
||||
# Relativize locations to spec directory
|
||||
|
||||
@@ -43,6 +43,7 @@ describe "Babel transpiler support", ->
|
||||
|
||||
describe "when a .js file does not start with 'use babel';", ->
|
||||
it "does not transpile it using babel", ->
|
||||
spyOn(console, 'error')
|
||||
expect(-> require('./fixtures/babel/invalid.js')).toThrow()
|
||||
|
||||
it "does not try to log to stdout or stderr while parsing the file", ->
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
CustomGutterComponent = require '../src/custom-gutter-component'
|
||||
Gutter = require '../src/gutter'
|
||||
|
||||
describe "CustomGutterComponent", ->
|
||||
[gutterComponent, gutter] = []
|
||||
|
||||
beforeEach ->
|
||||
mockGutterContainer = {}
|
||||
gutter = new Gutter(mockGutterContainer, {name: 'test-gutter'})
|
||||
gutterComponent = new CustomGutterComponent({gutter, views: atom.views})
|
||||
|
||||
it "creates a gutter DOM node with only an empty 'custom-decorations' child node when it is initialized", ->
|
||||
expect(gutterComponent.getDomNode().classList.contains('gutter')).toBe true
|
||||
expect(gutterComponent.getDomNode().getAttribute('gutter-name')).toBe 'test-gutter'
|
||||
expect(gutterComponent.getDomNode().children.length).toBe 1
|
||||
decorationsWrapperNode = gutterComponent.getDomNode().children.item(0)
|
||||
expect(decorationsWrapperNode.classList.contains('custom-decorations')).toBe true
|
||||
|
||||
it "makes its view accessible from the view registry", ->
|
||||
expect(gutterComponent.getDomNode()).toBe gutter.getElement()
|
||||
|
||||
it "hides its DOM node when ::hideNode is called, and shows its DOM node when ::showNode is called", ->
|
||||
gutterComponent.hideNode()
|
||||
expect(gutterComponent.getDomNode().style.display).toBe 'none'
|
||||
gutterComponent.showNode()
|
||||
expect(gutterComponent.getDomNode().style.display).toBe ''
|
||||
|
||||
describe "::updateSync", ->
|
||||
decorationItem1 = document.createElement('div')
|
||||
|
||||
buildTestState = (customDecorations) ->
|
||||
mockTestState =
|
||||
content: if customDecorations then customDecorations else {}
|
||||
styles:
|
||||
scrollHeight: 100
|
||||
scrollTop: 10
|
||||
backgroundColor: 'black'
|
||||
|
||||
mockTestState
|
||||
|
||||
it "sets the custom-decoration wrapper's scrollHeight, scrollTop, and background color", ->
|
||||
decorationsWrapperNode = gutterComponent.getDomNode().children.item(0)
|
||||
expect(decorationsWrapperNode.style.height).toBe ''
|
||||
expect(decorationsWrapperNode.style['-webkit-transform']).toBe ''
|
||||
expect(decorationsWrapperNode.style.backgroundColor).toBe ''
|
||||
|
||||
gutterComponent.updateSync(buildTestState({}))
|
||||
expect(decorationsWrapperNode.style.height).not.toBe ''
|
||||
expect(decorationsWrapperNode.style['-webkit-transform']).not.toBe ''
|
||||
expect(decorationsWrapperNode.style.backgroundColor).not.toBe ''
|
||||
|
||||
it "creates a new DOM node for a new decoration and adds it to the gutter at the right place", ->
|
||||
customDecorations =
|
||||
'decoration-id-1':
|
||||
top: 0
|
||||
height: 10
|
||||
item: decorationItem1
|
||||
class: 'test-class-1'
|
||||
|
||||
gutterComponent.updateSync(buildTestState(customDecorations))
|
||||
decorationsWrapperNode = gutterComponent.getDomNode().children.item(0)
|
||||
expect(decorationsWrapperNode.children.length).toBe 1
|
||||
|
||||
decorationNode = decorationsWrapperNode.children.item(0)
|
||||
expect(decorationNode.style.top).toBe '0px'
|
||||
expect(decorationNode.style.height).toBe '10px'
|
||||
expect(decorationNode.classList.contains('test-class-1')).toBe true
|
||||
expect(decorationNode.classList.contains('decoration')).toBe true
|
||||
expect(decorationNode.children.length).toBe 1
|
||||
|
||||
decorationItem = decorationNode.children.item(0)
|
||||
expect(decorationItem).toBe decorationItem1
|
||||
|
||||
it "updates the existing DOM node for a decoration that existed but has new properties", ->
|
||||
initialCustomDecorations =
|
||||
'decoration-id-1':
|
||||
top: 0
|
||||
height: 10
|
||||
item: decorationItem1
|
||||
class: 'test-class-1'
|
||||
gutterComponent.updateSync(buildTestState(initialCustomDecorations))
|
||||
initialDecorationNode = gutterComponent.getDomNode().children.item(0).children.item(0)
|
||||
|
||||
# Change the dimensions and item, remove the class.
|
||||
decorationItem2 = document.createElement('div')
|
||||
changedCustomDecorations =
|
||||
'decoration-id-1':
|
||||
top: 10
|
||||
height: 20
|
||||
item: decorationItem2
|
||||
gutterComponent.updateSync(buildTestState(changedCustomDecorations))
|
||||
changedDecorationNode = gutterComponent.getDomNode().children.item(0).children.item(0)
|
||||
expect(changedDecorationNode).toBe initialDecorationNode
|
||||
expect(changedDecorationNode.style.top).toBe '10px'
|
||||
expect(changedDecorationNode.style.height).toBe '20px'
|
||||
expect(changedDecorationNode.classList.contains('test-class-1')).toBe false
|
||||
expect(changedDecorationNode.classList.contains('decoration')).toBe true
|
||||
expect(changedDecorationNode.children.length).toBe 1
|
||||
decorationItem = changedDecorationNode.children.item(0)
|
||||
expect(decorationItem).toBe decorationItem2
|
||||
|
||||
# Remove the item, add a class.
|
||||
changedCustomDecorations =
|
||||
'decoration-id-1':
|
||||
top: 10
|
||||
height: 20
|
||||
class: 'test-class-2'
|
||||
gutterComponent.updateSync(buildTestState(changedCustomDecorations))
|
||||
changedDecorationNode = gutterComponent.getDomNode().children.item(0).children.item(0)
|
||||
expect(changedDecorationNode).toBe initialDecorationNode
|
||||
expect(changedDecorationNode.style.top).toBe '10px'
|
||||
expect(changedDecorationNode.style.height).toBe '20px'
|
||||
expect(changedDecorationNode.classList.contains('test-class-2')).toBe true
|
||||
expect(changedDecorationNode.classList.contains('decoration')).toBe true
|
||||
expect(changedDecorationNode.children.length).toBe 0
|
||||
|
||||
it "removes any decorations that existed previously but aren't in the latest update", ->
|
||||
customDecorations =
|
||||
'decoration-id-1':
|
||||
top: 0
|
||||
height: 10
|
||||
class: 'test-class-1'
|
||||
gutterComponent.updateSync(buildTestState(customDecorations))
|
||||
decorationsWrapperNode = gutterComponent.getDomNode().children.item(0)
|
||||
expect(decorationsWrapperNode.children.length).toBe 1
|
||||
|
||||
emptyCustomDecorations = {}
|
||||
gutterComponent.updateSync(buildTestState(emptyCustomDecorations))
|
||||
expect(decorationsWrapperNode.children.length).toBe 0
|
||||
@@ -1,21 +1,21 @@
|
||||
DecorationManager = require '../src/decoration-manager'
|
||||
TextEditor = require '../src/text-editor'
|
||||
|
||||
describe "DecorationManager", ->
|
||||
[decorationManager, buffer, displayLayer, markerLayer1, markerLayer2] = []
|
||||
[decorationManager, buffer, editor, markerLayer1, markerLayer2] = []
|
||||
|
||||
beforeEach ->
|
||||
buffer = atom.project.bufferForPathSync('sample.js')
|
||||
displayLayer = buffer.addDisplayLayer()
|
||||
markerLayer1 = displayLayer.addMarkerLayer()
|
||||
markerLayer2 = displayLayer.addMarkerLayer()
|
||||
decorationManager = new DecorationManager(displayLayer)
|
||||
editor = new TextEditor({buffer})
|
||||
markerLayer1 = editor.addMarkerLayer()
|
||||
markerLayer2 = editor.addMarkerLayer()
|
||||
decorationManager = new DecorationManager(editor)
|
||||
|
||||
waitsForPromise ->
|
||||
atom.packages.activatePackage('language-javascript')
|
||||
|
||||
afterEach ->
|
||||
decorationManager.destroy()
|
||||
buffer.release()
|
||||
buffer.destroy()
|
||||
|
||||
describe "decorations", ->
|
||||
[layer1Marker, layer2Marker, layer1MarkerDecoration, layer2MarkerDecoration, decorationProperties] = []
|
||||
@@ -29,7 +29,6 @@ describe "DecorationManager", ->
|
||||
it "can add decorations associated with markers and remove them", ->
|
||||
expect(layer1MarkerDecoration).toBeDefined()
|
||||
expect(layer1MarkerDecoration.getProperties()).toBe decorationProperties
|
||||
expect(decorationManager.decorationForId(layer1MarkerDecoration.id)).toBe layer1MarkerDecoration
|
||||
expect(decorationManager.decorationsForScreenRowRange(2, 3)).toEqual {
|
||||
"#{layer1Marker.id}": [layer1MarkerDecoration],
|
||||
"#{layer2Marker.id}": [layer2MarkerDecoration]
|
||||
@@ -37,15 +36,12 @@ describe "DecorationManager", ->
|
||||
|
||||
layer1MarkerDecoration.destroy()
|
||||
expect(decorationManager.decorationsForScreenRowRange(2, 3)[layer1Marker.id]).not.toBeDefined()
|
||||
expect(decorationManager.decorationForId(layer1MarkerDecoration.id)).not.toBeDefined()
|
||||
layer2MarkerDecoration.destroy()
|
||||
expect(decorationManager.decorationsForScreenRowRange(2, 3)[layer2Marker.id]).not.toBeDefined()
|
||||
expect(decorationManager.decorationForId(layer2MarkerDecoration.id)).not.toBeDefined()
|
||||
|
||||
it "will not fail if the decoration is removed twice", ->
|
||||
layer1MarkerDecoration.destroy()
|
||||
layer1MarkerDecoration.destroy()
|
||||
expect(decorationManager.decorationForId(layer1MarkerDecoration.id)).not.toBeDefined()
|
||||
|
||||
it "does not allow destroyed markers to be decorated", ->
|
||||
layer1Marker.destroy()
|
||||
@@ -55,7 +51,7 @@ describe "DecorationManager", ->
|
||||
expect(decorationManager.getOverlayDecorations()).toEqual []
|
||||
|
||||
it "does not allow destroyed marker layers to be decorated", ->
|
||||
layer = displayLayer.addMarkerLayer()
|
||||
layer = editor.addMarkerLayer()
|
||||
layer.destroy()
|
||||
expect(->
|
||||
decorationManager.decorateMarkerLayer(layer, {type: 'highlight'})
|
||||
|
||||
@@ -1,115 +0,0 @@
|
||||
const DOMElementPool = require ('../src/dom-element-pool')
|
||||
|
||||
describe('DOMElementPool', function () {
|
||||
let domElementPool
|
||||
|
||||
beforeEach(() => {
|
||||
domElementPool = new DOMElementPool()
|
||||
spyOn(atom, 'isReleasedVersion').andReturn(true)
|
||||
})
|
||||
|
||||
it('builds DOM nodes, recycling them when they are freed', function () {
|
||||
let elements
|
||||
const [div, span1, span2, span3, span4, span5, textNode] = Array.from(elements = [
|
||||
domElementPool.buildElement('div', 'foo'),
|
||||
domElementPool.buildElement('span'),
|
||||
domElementPool.buildElement('span'),
|
||||
domElementPool.buildElement('span'),
|
||||
domElementPool.buildElement('span'),
|
||||
domElementPool.buildElement('span'),
|
||||
domElementPool.buildText('Hello world!')
|
||||
])
|
||||
|
||||
expect(div.className).toBe('foo')
|
||||
div.textContent = 'testing'
|
||||
div.style.backgroundColor = 'red'
|
||||
div.dataset.foo = 'bar'
|
||||
|
||||
expect(textNode.textContent).toBe('Hello world!')
|
||||
|
||||
div.appendChild(span1)
|
||||
span1.appendChild(span2)
|
||||
div.appendChild(span3)
|
||||
span3.appendChild(span4)
|
||||
span4.appendChild(textNode)
|
||||
|
||||
domElementPool.freeElementAndDescendants(div)
|
||||
domElementPool.freeElementAndDescendants(span5)
|
||||
|
||||
expect(elements.includes(domElementPool.buildElement('div'))).toBe(true)
|
||||
expect(elements.includes(domElementPool.buildElement('span'))).toBe(true)
|
||||
expect(elements.includes(domElementPool.buildElement('span'))).toBe(true)
|
||||
expect(elements.includes(domElementPool.buildElement('span'))).toBe(true)
|
||||
expect(elements.includes(domElementPool.buildElement('span'))).toBe(true)
|
||||
expect(elements.includes(domElementPool.buildElement('span'))).toBe(true)
|
||||
expect(elements.includes(domElementPool.buildText('another text'))).toBe(true)
|
||||
|
||||
expect(elements.includes(domElementPool.buildElement('div'))).toBe(false)
|
||||
expect(elements.includes(domElementPool.buildElement('span'))).toBe(false)
|
||||
expect(elements.includes(domElementPool.buildText('unexisting'))).toBe(false)
|
||||
|
||||
expect(div.className).toBe('')
|
||||
expect(div.textContent).toBe('')
|
||||
expect(div.style.backgroundColor).toBe('')
|
||||
expect(div.dataset.foo).toBeUndefined()
|
||||
|
||||
expect(textNode.textContent).toBe('another text')
|
||||
})
|
||||
|
||||
it('forgets free nodes after being cleared', function () {
|
||||
const span = domElementPool.buildElement('span')
|
||||
const div = domElementPool.buildElement('div')
|
||||
domElementPool.freeElementAndDescendants(span)
|
||||
domElementPool.freeElementAndDescendants(div)
|
||||
|
||||
domElementPool.clear()
|
||||
|
||||
expect(domElementPool.buildElement('span')).not.toBe(span)
|
||||
expect(domElementPool.buildElement('div')).not.toBe(div)
|
||||
})
|
||||
|
||||
it('does not attempt to free nodes that were not created by the pool', () => {
|
||||
let assertionFailure
|
||||
atom.onDidFailAssertion((error) => assertionFailure = error)
|
||||
|
||||
const foreignDiv = document.createElement('div')
|
||||
const div = domElementPool.buildElement('div')
|
||||
div.appendChild(foreignDiv)
|
||||
domElementPool.freeElementAndDescendants(div)
|
||||
const span = domElementPool.buildElement('span')
|
||||
span.appendChild(foreignDiv)
|
||||
domElementPool.freeElementAndDescendants(span)
|
||||
|
||||
expect(assertionFailure).toBeUndefined()
|
||||
})
|
||||
|
||||
it('fails an assertion when freeing the same element twice', function () {
|
||||
let assertionFailure
|
||||
atom.onDidFailAssertion((error) => assertionFailure = error)
|
||||
|
||||
const div = domElementPool.buildElement('div')
|
||||
div.textContent = 'testing'
|
||||
domElementPool.freeElementAndDescendants(div)
|
||||
expect(assertionFailure).toBeUndefined()
|
||||
domElementPool.freeElementAndDescendants(div)
|
||||
expect(assertionFailure.message).toBe('Assertion failed: The element has already been freed!')
|
||||
expect(assertionFailure.metadata.content).toBe('<div>testing</div>')
|
||||
})
|
||||
|
||||
it('fails an assertion when freeing the same text node twice', function () {
|
||||
let assertionFailure
|
||||
atom.onDidFailAssertion((error) => assertionFailure = error)
|
||||
|
||||
const node = domElementPool.buildText('testing')
|
||||
domElementPool.freeElementAndDescendants(node)
|
||||
expect(assertionFailure).toBeUndefined()
|
||||
domElementPool.freeElementAndDescendants(node)
|
||||
expect(assertionFailure.message).toBe('Assertion failed: The element has already been freed!')
|
||||
expect(assertionFailure.metadata.content).toBe('testing')
|
||||
})
|
||||
|
||||
it('throws an error when trying to free an invalid element', function () {
|
||||
expect(() => domElementPool.freeElementAndDescendants(null)).toThrow()
|
||||
expect(() => domElementPool.freeElementAndDescendants(undefined)).toThrow()
|
||||
})
|
||||
})
|
||||
@@ -1,63 +0,0 @@
|
||||
{Point} = require 'text-buffer'
|
||||
{isPairedCharacter} = require '../src/text-utils'
|
||||
|
||||
module.exports =
|
||||
class FakeLinesYardstick
|
||||
constructor: (@model, @lineTopIndex) ->
|
||||
{@displayLayer} = @model
|
||||
@characterWidthsByScope = {}
|
||||
|
||||
getScopedCharacterWidth: (scopeNames, char) ->
|
||||
@getScopedCharacterWidths(scopeNames)[char]
|
||||
|
||||
getScopedCharacterWidths: (scopeNames) ->
|
||||
scope = @characterWidthsByScope
|
||||
for scopeName in scopeNames
|
||||
scope[scopeName] ?= {}
|
||||
scope = scope[scopeName]
|
||||
scope.characterWidths ?= {}
|
||||
scope.characterWidths
|
||||
|
||||
setScopedCharacterWidth: (scopeNames, character, width) ->
|
||||
@getScopedCharacterWidths(scopeNames)[character] = width
|
||||
|
||||
pixelPositionForScreenPosition: (screenPosition) ->
|
||||
screenPosition = Point.fromObject(screenPosition)
|
||||
|
||||
targetRow = screenPosition.row
|
||||
targetColumn = screenPosition.column
|
||||
|
||||
top = @lineTopIndex.pixelPositionAfterBlocksForRow(targetRow)
|
||||
left = 0
|
||||
column = 0
|
||||
|
||||
scopes = []
|
||||
startIndex = 0
|
||||
{tagCodes, lineText} = @model.screenLineForScreenRow(targetRow)
|
||||
for tagCode in tagCodes
|
||||
if @displayLayer.isOpenTagCode(tagCode)
|
||||
scopes.push(@displayLayer.tagForCode(tagCode))
|
||||
else if @displayLayer.isCloseTagCode(tagCode)
|
||||
scopes.splice(scopes.lastIndexOf(@displayLayer.tagForCode(tagCode)), 1)
|
||||
else
|
||||
text = lineText.substr(startIndex, tagCode)
|
||||
startIndex += tagCode
|
||||
characterWidths = @getScopedCharacterWidths(scopes)
|
||||
|
||||
valueIndex = 0
|
||||
while valueIndex < text.length
|
||||
if isPairedCharacter(text, valueIndex)
|
||||
char = text[valueIndex...valueIndex + 2]
|
||||
charLength = 2
|
||||
valueIndex += 2
|
||||
else
|
||||
char = text[valueIndex]
|
||||
charLength = 1
|
||||
valueIndex++
|
||||
|
||||
break if column is targetColumn
|
||||
|
||||
left += characterWidths[char] ? @model.getDefaultCharWidth() unless char is '\0'
|
||||
column += charLength
|
||||
|
||||
{top, left}
|
||||
2
spec/fixtures/babel/invalid.js
vendored
2
spec/fixtures/babel/invalid.js
vendored
@@ -1,3 +1,3 @@
|
||||
'use 6to6';
|
||||
|
||||
module.exports = async function hello() {}
|
||||
module.exports = async function* hello() {}
|
||||
|
||||
@@ -1,160 +0,0 @@
|
||||
Gutter = require '../src/gutter'
|
||||
GutterContainerComponent = require '../src/gutter-container-component'
|
||||
DOMElementPool = require '../src/dom-element-pool'
|
||||
|
||||
describe "GutterContainerComponent", ->
|
||||
[gutterContainerComponent] = []
|
||||
mockGutterContainer = {}
|
||||
|
||||
buildTestState = (gutters) ->
|
||||
styles =
|
||||
scrollHeight: 100
|
||||
scrollTop: 10
|
||||
backgroundColor: 'black'
|
||||
|
||||
mockTestState = {gutters: []}
|
||||
for gutter in gutters
|
||||
if gutter.name is 'line-number'
|
||||
content = {maxLineNumberDigits: 10, lineNumbers: {}}
|
||||
else
|
||||
content = {}
|
||||
mockTestState.gutters.push({gutter, styles, content, visible: gutter.visible})
|
||||
|
||||
mockTestState
|
||||
|
||||
beforeEach ->
|
||||
domElementPool = new DOMElementPool
|
||||
mockEditor = {}
|
||||
mockMouseDown = ->
|
||||
gutterContainerComponent = new GutterContainerComponent({editor: mockEditor, onMouseDown: mockMouseDown, domElementPool, views: atom.views})
|
||||
|
||||
it "creates a DOM node with no child gutter nodes when it is initialized", ->
|
||||
expect(gutterContainerComponent.getDomNode() instanceof HTMLElement).toBe true
|
||||
expect(gutterContainerComponent.getDomNode().children.length).toBe 0
|
||||
|
||||
describe "when updated with state that contains a new line-number gutter", ->
|
||||
it "adds a LineNumberGutterComponent to its children", ->
|
||||
lineNumberGutter = new Gutter(mockGutterContainer, {name: 'line-number'})
|
||||
testState = buildTestState([lineNumberGutter])
|
||||
|
||||
expect(gutterContainerComponent.getDomNode().children.length).toBe 0
|
||||
gutterContainerComponent.updateSync(testState)
|
||||
expect(gutterContainerComponent.getDomNode().children.length).toBe 1
|
||||
expectedGutterNode = gutterContainerComponent.getDomNode().children.item(0)
|
||||
expect(expectedGutterNode.classList.contains('gutter')).toBe true
|
||||
expectedLineNumbersNode = expectedGutterNode.children.item(0)
|
||||
expect(expectedLineNumbersNode.classList.contains('line-numbers')).toBe true
|
||||
|
||||
expect(gutterContainerComponent.getLineNumberGutterComponent().getDomNode()).toBe expectedGutterNode
|
||||
|
||||
describe "when updated with state that contains a new custom gutter", ->
|
||||
it "adds a CustomGutterComponent to its children", ->
|
||||
customGutter = new Gutter(mockGutterContainer, {name: 'custom'})
|
||||
testState = buildTestState([customGutter])
|
||||
|
||||
expect(gutterContainerComponent.getDomNode().children.length).toBe 0
|
||||
gutterContainerComponent.updateSync(testState)
|
||||
expect(gutterContainerComponent.getDomNode().children.length).toBe 1
|
||||
expectedGutterNode = gutterContainerComponent.getDomNode().children.item(0)
|
||||
expect(expectedGutterNode.classList.contains('gutter')).toBe true
|
||||
expectedCustomDecorationsNode = expectedGutterNode.children.item(0)
|
||||
expect(expectedCustomDecorationsNode.classList.contains('custom-decorations')).toBe true
|
||||
|
||||
describe "when updated with state that contains a new gutter that is not visible", ->
|
||||
it "creates the gutter view but hides it, and unhides it when it is later updated to be visible", ->
|
||||
customGutter = new Gutter(mockGutterContainer, {name: 'custom', visible: false})
|
||||
testState = buildTestState([customGutter])
|
||||
|
||||
gutterContainerComponent.updateSync(testState)
|
||||
expect(gutterContainerComponent.getDomNode().children.length).toBe 1
|
||||
expectedCustomGutterNode = gutterContainerComponent.getDomNode().children.item(0)
|
||||
expect(expectedCustomGutterNode.style.display).toBe 'none'
|
||||
|
||||
customGutter.show()
|
||||
testState = buildTestState([customGutter])
|
||||
gutterContainerComponent.updateSync(testState)
|
||||
expect(gutterContainerComponent.getDomNode().children.length).toBe 1
|
||||
expectedCustomGutterNode = gutterContainerComponent.getDomNode().children.item(0)
|
||||
expect(expectedCustomGutterNode.style.display).toBe ''
|
||||
|
||||
describe "when updated with a gutter that already exists", ->
|
||||
it "reuses the existing gutter view, instead of recreating it", ->
|
||||
customGutter = new Gutter(mockGutterContainer, {name: 'custom'})
|
||||
testState = buildTestState([customGutter])
|
||||
|
||||
gutterContainerComponent.updateSync(testState)
|
||||
expect(gutterContainerComponent.getDomNode().children.length).toBe 1
|
||||
expectedCustomGutterNode = gutterContainerComponent.getDomNode().children.item(0)
|
||||
|
||||
testState = buildTestState([customGutter])
|
||||
gutterContainerComponent.updateSync(testState)
|
||||
expect(gutterContainerComponent.getDomNode().children.length).toBe 1
|
||||
expect(gutterContainerComponent.getDomNode().children.item(0)).toBe expectedCustomGutterNode
|
||||
|
||||
it "removes a gutter from the DOM if it does not appear in the latest state update", ->
|
||||
lineNumberGutter = new Gutter(mockGutterContainer, {name: 'line-number'})
|
||||
testState = buildTestState([lineNumberGutter])
|
||||
gutterContainerComponent.updateSync(testState)
|
||||
|
||||
expect(gutterContainerComponent.getDomNode().children.length).toBe 1
|
||||
testState = buildTestState([])
|
||||
gutterContainerComponent.updateSync(testState)
|
||||
expect(gutterContainerComponent.getDomNode().children.length).toBe 0
|
||||
|
||||
describe "when updated with multiple gutters", ->
|
||||
it "positions (and repositions) the gutters to match the order they appear in each state update", ->
|
||||
lineNumberGutter = new Gutter(mockGutterContainer, {name: 'line-number'})
|
||||
customGutter1 = new Gutter(mockGutterContainer, {name: 'custom', priority: -100})
|
||||
testState = buildTestState([customGutter1, lineNumberGutter])
|
||||
|
||||
gutterContainerComponent.updateSync(testState)
|
||||
expect(gutterContainerComponent.getDomNode().children.length).toBe 2
|
||||
expectedCustomGutterNode = gutterContainerComponent.getDomNode().children.item(0)
|
||||
expect(expectedCustomGutterNode).toBe customGutter1.getElement()
|
||||
expectedLineNumbersNode = gutterContainerComponent.getDomNode().children.item(1)
|
||||
expect(expectedLineNumbersNode).toBe lineNumberGutter.getElement()
|
||||
|
||||
# Add a gutter.
|
||||
customGutter2 = new Gutter(mockGutterContainer, {name: 'custom2', priority: -10})
|
||||
testState = buildTestState([customGutter1, customGutter2, lineNumberGutter])
|
||||
gutterContainerComponent.updateSync(testState)
|
||||
expect(gutterContainerComponent.getDomNode().children.length).toBe 3
|
||||
expectedCustomGutterNode1 = gutterContainerComponent.getDomNode().children.item(0)
|
||||
expect(expectedCustomGutterNode1).toBe customGutter1.getElement()
|
||||
expectedCustomGutterNode2 = gutterContainerComponent.getDomNode().children.item(1)
|
||||
expect(expectedCustomGutterNode2).toBe customGutter2.getElement()
|
||||
expectedLineNumbersNode = gutterContainerComponent.getDomNode().children.item(2)
|
||||
expect(expectedLineNumbersNode).toBe lineNumberGutter.getElement()
|
||||
|
||||
# Hide one gutter, reposition one gutter, remove one gutter; and add a new gutter.
|
||||
customGutter2.hide()
|
||||
customGutter3 = new Gutter(mockGutterContainer, {name: 'custom3', priority: 100})
|
||||
testState = buildTestState([customGutter2, customGutter1, customGutter3])
|
||||
gutterContainerComponent.updateSync(testState)
|
||||
expect(gutterContainerComponent.getDomNode().children.length).toBe 3
|
||||
expectedCustomGutterNode2 = gutterContainerComponent.getDomNode().children.item(0)
|
||||
expect(expectedCustomGutterNode2).toBe customGutter2.getElement()
|
||||
expect(expectedCustomGutterNode2.style.display).toBe 'none'
|
||||
expectedCustomGutterNode1 = gutterContainerComponent.getDomNode().children.item(1)
|
||||
expect(expectedCustomGutterNode1).toBe customGutter1.getElement()
|
||||
expectedCustomGutterNode3 = gutterContainerComponent.getDomNode().children.item(2)
|
||||
expect(expectedCustomGutterNode3).toBe customGutter3.getElement()
|
||||
|
||||
it "reorders correctly when prepending multiple gutters at once", ->
|
||||
lineNumberGutter = new Gutter(mockGutterContainer, {name: 'line-number'})
|
||||
testState = buildTestState([lineNumberGutter])
|
||||
gutterContainerComponent.updateSync(testState)
|
||||
expect(gutterContainerComponent.getDomNode().children.length).toBe 1
|
||||
expectedCustomGutterNode = gutterContainerComponent.getDomNode().children.item(0)
|
||||
expect(expectedCustomGutterNode).toBe lineNumberGutter.getElement()
|
||||
|
||||
# Prepend two gutters at once
|
||||
customGutter1 = new Gutter(mockGutterContainer, {name: 'first', priority: -200})
|
||||
customGutter2 = new Gutter(mockGutterContainer, {name: 'second', priority: -100})
|
||||
testState = buildTestState([customGutter1, customGutter2, lineNumberGutter])
|
||||
gutterContainerComponent.updateSync(testState)
|
||||
expect(gutterContainerComponent.getDomNode().children.length).toBe 3
|
||||
expectedCustomGutterNode1 = gutterContainerComponent.getDomNode().children.item(0)
|
||||
expect(expectedCustomGutterNode1).toBe customGutter1.getElement()
|
||||
expectedCustomGutterNode2 = gutterContainerComponent.getDomNode().children.item(1)
|
||||
expect(expectedCustomGutterNode2).toBe customGutter2.getElement()
|
||||
@@ -3,7 +3,9 @@ GutterContainer = require '../src/gutter-container'
|
||||
|
||||
describe 'GutterContainer', ->
|
||||
gutterContainer = null
|
||||
fakeTextEditor = {}
|
||||
fakeTextEditor = {
|
||||
scheduleComponentUpdate: ->
|
||||
}
|
||||
|
||||
beforeEach ->
|
||||
gutterContainer = new GutterContainer fakeTextEditor
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
Gutter = require '../src/gutter'
|
||||
|
||||
describe 'Gutter', ->
|
||||
fakeGutterContainer = {}
|
||||
fakeGutterContainer = {
|
||||
scheduleComponentUpdate: ->
|
||||
}
|
||||
name = 'name'
|
||||
|
||||
describe '::hide', ->
|
||||
|
||||
@@ -31,7 +31,6 @@ describe("HistoryManager", () => {
|
||||
})
|
||||
|
||||
historyManager = new HistoryManager({stateStore, project, commands: commandRegistry})
|
||||
historyManager.initialize(window.localStorage)
|
||||
await historyManager.loadState()
|
||||
})
|
||||
|
||||
@@ -76,7 +75,7 @@ describe("HistoryManager", () => {
|
||||
|
||||
it("saves the state", async () => {
|
||||
await historyManager.clearProjects()
|
||||
const historyManager2 = new HistoryManager({stateStore, localStorage: window.localStorage, project, commands: commandRegistry})
|
||||
const historyManager2 = new HistoryManager({stateStore, project, commands: commandRegistry})
|
||||
await historyManager2.loadState()
|
||||
expect(historyManager.getProjects().length).toBe(0)
|
||||
})
|
||||
@@ -187,7 +186,7 @@ describe("HistoryManager", () => {
|
||||
it("saves the state", async () => {
|
||||
await historyManager.addProject(["/save/state"])
|
||||
await historyManager.saveState()
|
||||
const historyManager2 = new HistoryManager({stateStore, localStorage: window.localStorage, project, commands: commandRegistry})
|
||||
const historyManager2 = new HistoryManager({stateStore, project, commands: commandRegistry})
|
||||
await historyManager2.loadState()
|
||||
expect(historyManager2.getProjects()[0].paths).toEqual(['/save/state'])
|
||||
})
|
||||
|
||||
@@ -6,7 +6,7 @@ runAtom = require './helpers/start-atom'
|
||||
|
||||
describe "Smoke Test", ->
|
||||
return unless process.platform is 'darwin' # Fails on win32
|
||||
|
||||
|
||||
atomHome = temp.mkdirSync('atom-home')
|
||||
|
||||
beforeEach ->
|
||||
@@ -14,7 +14,10 @@ describe "Smoke Test", ->
|
||||
season.writeFileSync(path.join(atomHome, 'config.cson'), {
|
||||
'*': {
|
||||
welcome: {showOnStartup: false},
|
||||
core: {telemetryConsent: 'no'}
|
||||
core: {
|
||||
telemetryConsent: 'no',
|
||||
disabledPackages: ['github']
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -28,6 +31,7 @@ describe "Smoke Test", ->
|
||||
.then (exists) -> expect(exists).toBe true
|
||||
.waitForPaneItemCount(1, 1000)
|
||||
.click("atom-text-editor")
|
||||
.waitUntil((-> @execute(-> document.activeElement.closest('atom-text-editor'))), 5000)
|
||||
.keys("Hello!")
|
||||
.execute -> atom.workspace.getActiveTextEditor().getText()
|
||||
.then ({value}) -> expect(value).toBe "Hello!"
|
||||
|
||||
@@ -1,248 +0,0 @@
|
||||
LinesYardstick = require '../src/lines-yardstick'
|
||||
LineTopIndex = require 'line-top-index'
|
||||
{Point} = require 'text-buffer'
|
||||
|
||||
describe "LinesYardstick", ->
|
||||
[editor, mockLineNodesProvider, createdLineNodes, linesYardstick, buildLineNode] = []
|
||||
|
||||
beforeEach ->
|
||||
waitsForPromise ->
|
||||
atom.packages.activatePackage('language-javascript')
|
||||
|
||||
waitsForPromise ->
|
||||
atom.workspace.open('sample.js').then (o) -> editor = o
|
||||
|
||||
runs ->
|
||||
createdLineNodes = []
|
||||
|
||||
buildLineNode = (screenRow) ->
|
||||
startIndex = 0
|
||||
scopes = []
|
||||
screenLine = editor.screenLineForScreenRow(screenRow)
|
||||
lineNode = document.createElement("div")
|
||||
lineNode.style.whiteSpace = "pre"
|
||||
for tagCode in screenLine.tagCodes when tagCode isnt 0
|
||||
if editor.displayLayer.isCloseTagCode(tagCode)
|
||||
scopes.pop()
|
||||
else if editor.displayLayer.isOpenTagCode(tagCode)
|
||||
scopes.push(editor.displayLayer.tagForCode(tagCode))
|
||||
else
|
||||
text = screenLine.lineText.substr(startIndex, tagCode)
|
||||
startIndex += tagCode
|
||||
|
||||
span = document.createElement("span")
|
||||
span.className = scopes.join(' ').replace(/\.+/g, ' ')
|
||||
span.textContent = text
|
||||
lineNode.appendChild(span)
|
||||
jasmine.attachToDOM(lineNode)
|
||||
createdLineNodes.push(lineNode)
|
||||
lineNode
|
||||
|
||||
mockLineNodesProvider =
|
||||
lineNodesById: {}
|
||||
|
||||
lineIdForScreenRow: (screenRow) ->
|
||||
editor.screenLineForScreenRow(screenRow)?.id
|
||||
|
||||
lineNodeForScreenRow: (screenRow) ->
|
||||
if id = @lineIdForScreenRow(screenRow)
|
||||
@lineNodesById[id] ?= buildLineNode(screenRow)
|
||||
|
||||
textNodesForScreenRow: (screenRow) ->
|
||||
lineNode = @lineNodeForScreenRow(screenRow)
|
||||
iterator = document.createNodeIterator(lineNode, NodeFilter.SHOW_TEXT)
|
||||
textNodes = []
|
||||
textNodes.push(textNode) while textNode = iterator.nextNode()
|
||||
textNodes
|
||||
|
||||
editor.setLineHeightInPixels(14)
|
||||
lineTopIndex = new LineTopIndex({defaultLineHeight: editor.getLineHeightInPixels()})
|
||||
linesYardstick = new LinesYardstick(editor, mockLineNodesProvider, lineTopIndex, atom.grammars)
|
||||
|
||||
afterEach ->
|
||||
lineNode.remove() for lineNode in createdLineNodes
|
||||
atom.themes.removeStylesheet('test')
|
||||
|
||||
describe "::pixelPositionForScreenPosition(screenPosition)", ->
|
||||
it "converts screen positions to pixel positions", ->
|
||||
atom.styles.addStyleSheet """
|
||||
* {
|
||||
font-size: 12px;
|
||||
font-family: monospace;
|
||||
}
|
||||
.syntax--function {
|
||||
font-size: 16px
|
||||
}
|
||||
"""
|
||||
|
||||
expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 0))).toEqual({left: 0, top: 0})
|
||||
expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 1))).toEqual({left: 7, top: 0})
|
||||
expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 5))).toEqual({left: 38, top: 0})
|
||||
|
||||
switch process.platform
|
||||
when 'darwin'
|
||||
expect(linesYardstick.pixelPositionForScreenPosition(Point(1, 6))).toEqual({left: 43, top: 14})
|
||||
expect(linesYardstick.pixelPositionForScreenPosition(Point(1, 9))).toEqual({left: 72, top: 14})
|
||||
expect(linesYardstick.pixelPositionForScreenPosition(Point(2, Infinity))).toEqual({left: 287.875, top: 28})
|
||||
when 'win32'
|
||||
expect(linesYardstick.pixelPositionForScreenPosition(Point(1, 6))).toEqual({left: 42, top: 14})
|
||||
expect(linesYardstick.pixelPositionForScreenPosition(Point(1, 9))).toEqual({left: 71, top: 14})
|
||||
expect(linesYardstick.pixelPositionForScreenPosition(Point(2, Infinity))).toEqual({left: 280, top: 28})
|
||||
|
||||
it "reuses already computed pixel positions unless it is invalidated", ->
|
||||
atom.styles.addStyleSheet """
|
||||
* {
|
||||
font-size: 16px;
|
||||
font-family: monospace;
|
||||
}
|
||||
"""
|
||||
|
||||
expect(linesYardstick.pixelPositionForScreenPosition(Point(1, 2))).toEqual({left: 19, top: 14})
|
||||
expect(linesYardstick.pixelPositionForScreenPosition(Point(2, 6))).toEqual({left: 57.609375, top: 28})
|
||||
expect(linesYardstick.pixelPositionForScreenPosition(Point(5, 10))).toEqual({left: 96, top: 70})
|
||||
|
||||
atom.styles.addStyleSheet """
|
||||
* {
|
||||
font-size: 20px;
|
||||
}
|
||||
"""
|
||||
|
||||
expect(linesYardstick.pixelPositionForScreenPosition(Point(1, 2))).toEqual({left: 19, top: 14})
|
||||
expect(linesYardstick.pixelPositionForScreenPosition(Point(2, 6))).toEqual({left: 57.609375, top: 28})
|
||||
expect(linesYardstick.pixelPositionForScreenPosition(Point(5, 10))).toEqual({left: 96, top: 70})
|
||||
|
||||
linesYardstick.invalidateCache()
|
||||
|
||||
expect(linesYardstick.pixelPositionForScreenPosition(Point(1, 2))).toEqual({left: 24, top: 14})
|
||||
expect(linesYardstick.pixelPositionForScreenPosition(Point(2, 6))).toEqual({left: 72, top: 28})
|
||||
expect(linesYardstick.pixelPositionForScreenPosition(Point(5, 10))).toEqual({left: 120, top: 70})
|
||||
|
||||
it "doesn't report a width greater than 0 when the character to measure is at the beginning of a text node", ->
|
||||
# This spec documents what seems to be a bug in Chromium, because we'd
|
||||
# expect that Range(0, 0).getBoundingClientRect().width to always be zero.
|
||||
atom.styles.addStyleSheet """
|
||||
* {
|
||||
font-size: 11px;
|
||||
font-family: monospace;
|
||||
}
|
||||
"""
|
||||
|
||||
text = " \\vec{w}_j^r(\\text{new}) &= \\vec{w}_j^r(\\text{old}) + \\Delta\\vec{w}_j^r, \\\\"
|
||||
buildLineNode = (screenRow) ->
|
||||
lineNode = document.createElement("div")
|
||||
lineNode.style.whiteSpace = "pre"
|
||||
# We couldn't reproduce the problem with a simple string, so we're
|
||||
# attaching the full one that comes from a bug report.
|
||||
lineNode.innerHTML = '<span><span> </span><span> </span><span><span>\\</span>vec</span><span><span>{</span>w<span>}</span></span>_j^r(<span><span>\\</span>text</span><span><span>{</span>new<span>}</span></span>) &= <span><span>\\</span>vec</span><span><span>{</span>w<span>}</span></span>_j^r(<span><span>\\</span>text</span><span><span>{</span>old<span>}</span></span>) + <span><span>\\</span>Delta</span><span><span>\\</span>vec</span><span><span>{</span>w<span>}</span></span>_j^r, <span>\\\\</span></span>'
|
||||
jasmine.attachToDOM(lineNode)
|
||||
createdLineNodes.push(lineNode)
|
||||
lineNode
|
||||
|
||||
editor.setText(text)
|
||||
|
||||
switch process.platform
|
||||
when 'darwin'
|
||||
expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 35)).left).toBe 230.90625
|
||||
expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 36)).left).toBe 237.5
|
||||
expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 37)).left).toBe 244.09375
|
||||
when 'win32'
|
||||
expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 35)).left).toBe 245
|
||||
expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 36)).left).toBe 252
|
||||
expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 37)).left).toBe 259
|
||||
|
||||
it "handles lines containing a mix of left-to-right and right-to-left characters", ->
|
||||
editor.setText('Persian, locally known as Parsi or Farsi (زبان فارسی), the predominant modern descendant of Old Persian.\n')
|
||||
|
||||
atom.styles.addStyleSheet """
|
||||
* {
|
||||
font-size: 14px;
|
||||
font-family: monospace;
|
||||
}
|
||||
"""
|
||||
|
||||
lineTopIndex = new LineTopIndex({defaultLineHeight: editor.getLineHeightInPixels()})
|
||||
linesYardstick = new LinesYardstick(editor, mockLineNodesProvider, lineTopIndex, atom.grammars)
|
||||
|
||||
switch process.platform
|
||||
when 'darwin'
|
||||
expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 15))).toEqual({left: 126, top: 0})
|
||||
expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 62))).toEqual({left: 521, top: 0})
|
||||
expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 58))).toEqual({left: 487, top: 0})
|
||||
expect(linesYardstick.pixelPositionForScreenPosition(Point(0, Infinity))).toEqual({left: 873.625, top: 0})
|
||||
when 'win32'
|
||||
expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 15))).toEqual({left: 120, top: 0})
|
||||
expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 62))).toEqual({left: 496, top: 0})
|
||||
expect(linesYardstick.pixelPositionForScreenPosition(Point(0, 58))).toEqual({left: 464, top: 0})
|
||||
expect(linesYardstick.pixelPositionForScreenPosition(Point(0, Infinity))).toEqual({left: 832, top: 0})
|
||||
|
||||
describe "::screenPositionForPixelPosition(pixelPosition)", ->
|
||||
it "converts pixel positions to screen positions", ->
|
||||
atom.styles.addStyleSheet """
|
||||
* {
|
||||
font-size: 12px;
|
||||
font-family: monospace;
|
||||
}
|
||||
.syntax--function {
|
||||
font-size: 16px
|
||||
}
|
||||
"""
|
||||
|
||||
expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 12.5})).toEqual([0, 2])
|
||||
expect(linesYardstick.screenPositionForPixelPosition({top: 14, left: 18.8})).toEqual([1, 3])
|
||||
expect(linesYardstick.screenPositionForPixelPosition({top: 28, left: 100})).toEqual([2, 14])
|
||||
expect(linesYardstick.screenPositionForPixelPosition({top: 32, left: 24.3})).toEqual([2, 3])
|
||||
expect(linesYardstick.screenPositionForPixelPosition({top: 46, left: 66.5})).toEqual([3, 9])
|
||||
expect(linesYardstick.screenPositionForPixelPosition({top: 70, left: 99.9})).toEqual([5, 14])
|
||||
expect(linesYardstick.screenPositionForPixelPosition({top: 70, left: 225})).toEqual([5, 30])
|
||||
|
||||
switch process.platform
|
||||
when 'darwin'
|
||||
expect(linesYardstick.screenPositionForPixelPosition({top: 70, left: 224.2365234375})).toEqual([5, 29])
|
||||
expect(linesYardstick.screenPositionForPixelPosition({top: 84, left: 247.1})).toEqual([6, 33])
|
||||
when 'win32'
|
||||
expect(linesYardstick.screenPositionForPixelPosition({top: 70, left: 224.2365234375})).toEqual([5, 30])
|
||||
expect(linesYardstick.screenPositionForPixelPosition({top: 84, left: 247.1})).toEqual([6, 34])
|
||||
|
||||
it "overshoots to the nearest character when text nodes are not spatially contiguous", ->
|
||||
atom.styles.addStyleSheet """
|
||||
* {
|
||||
font-size: 12px;
|
||||
font-family: monospace;
|
||||
}
|
||||
"""
|
||||
|
||||
buildLineNode = (screenRow) ->
|
||||
lineNode = document.createElement("div")
|
||||
lineNode.style.whiteSpace = "pre"
|
||||
lineNode.innerHTML = '<span>foo</span><span style="margin-left: 40px">bar</span>'
|
||||
jasmine.attachToDOM(lineNode)
|
||||
createdLineNodes.push(lineNode)
|
||||
lineNode
|
||||
editor.setText("foobar")
|
||||
|
||||
expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 7})).toEqual([0, 1])
|
||||
expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 14})).toEqual([0, 2])
|
||||
expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 21})).toEqual([0, 3])
|
||||
expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 30})).toEqual([0, 3])
|
||||
expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 50})).toEqual([0, 3])
|
||||
expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 62})).toEqual([0, 3])
|
||||
expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 69})).toEqual([0, 4])
|
||||
expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 76})).toEqual([0, 5])
|
||||
expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 100})).toEqual([0, 6])
|
||||
expect(linesYardstick.screenPositionForPixelPosition({top: 0, left: 200})).toEqual([0, 6])
|
||||
|
||||
it "clips pixel positions above buffer start", ->
|
||||
expect(linesYardstick.screenPositionForPixelPosition(top: -Infinity, left: -Infinity)).toEqual [0, 0]
|
||||
expect(linesYardstick.screenPositionForPixelPosition(top: -Infinity, left: Infinity)).toEqual [0, 0]
|
||||
expect(linesYardstick.screenPositionForPixelPosition(top: -1, left: Infinity)).toEqual [0, 0]
|
||||
expect(linesYardstick.screenPositionForPixelPosition(top: 0, left: Infinity)).toEqual [0, 29]
|
||||
|
||||
it "clips pixel positions below buffer end", ->
|
||||
expect(linesYardstick.screenPositionForPixelPosition(top: Infinity, left: -Infinity)).toEqual [12, 2]
|
||||
expect(linesYardstick.screenPositionForPixelPosition(top: Infinity, left: Infinity)).toEqual [12, 2]
|
||||
expect(linesYardstick.screenPositionForPixelPosition(top: (editor.getLastScreenRow() + 1) * 14, left: 0)).toEqual [12, 2]
|
||||
expect(linesYardstick.screenPositionForPixelPosition(top: editor.getLastScreenRow() * 14, left: 0)).toEqual [12, 0]
|
||||
|
||||
it "clips negative horizontal pixel positions", ->
|
||||
expect(linesYardstick.screenPositionForPixelPosition(top: 0, left: -10)).toEqual [0, 0]
|
||||
expect(linesYardstick.screenPositionForPixelPosition(top: 1 * 14, left: -10)).toEqual [1, 0]
|
||||
@@ -64,6 +64,7 @@ beforeEach ->
|
||||
atom.project.setPaths([specProjectPath])
|
||||
|
||||
window.resetTimeouts()
|
||||
spyOn(Date, 'now').andCallFake -> window.now
|
||||
spyOn(_._, "now").andCallFake -> window.now
|
||||
spyOn(window, "setTimeout").andCallFake window.fakeSetTimeout
|
||||
spyOn(window, "clearTimeout").andCallFake window.fakeClearTimeout
|
||||
@@ -179,6 +180,7 @@ jasmine.useRealClock = ->
|
||||
jasmine.unspy(window, 'setTimeout')
|
||||
jasmine.unspy(window, 'clearTimeout')
|
||||
jasmine.unspy(_._, 'now')
|
||||
jasmine.unspy(Date, 'now')
|
||||
|
||||
# The clock is halfway mocked now in a sad and terrible way... only setTimeout
|
||||
# and clearTimeout are included. This method will also include setInterval. We
|
||||
@@ -186,6 +188,8 @@ jasmine.useRealClock = ->
|
||||
jasmine.useMockClock = ->
|
||||
spyOn(window, 'setInterval').andCallFake(fakeSetInterval)
|
||||
spyOn(window, 'clearInterval').andCallFake(fakeClearInterval)
|
||||
spyOn(Date, 'now').andCallFake(-> window.now)
|
||||
|
||||
|
||||
addCustomMatchers = (spec) ->
|
||||
spec.addMatchers
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,320 +0,0 @@
|
||||
TextEditor = require '../src/text-editor'
|
||||
TextEditorElement = require '../src/text-editor-element'
|
||||
{Disposable} = require 'event-kit'
|
||||
|
||||
describe "TextEditorElement", ->
|
||||
jasmineContent = null
|
||||
|
||||
beforeEach ->
|
||||
jasmineContent = document.body.querySelector('#jasmine-content')
|
||||
|
||||
describe "instantiation", ->
|
||||
it "honors the 'mini' attribute", ->
|
||||
jasmineContent.innerHTML = "<atom-text-editor mini>"
|
||||
element = jasmineContent.firstChild
|
||||
expect(element.getModel().isMini()).toBe true
|
||||
|
||||
it "honors the 'placeholder-text' attribute", ->
|
||||
jasmineContent.innerHTML = "<atom-text-editor placeholder-text='testing'>"
|
||||
element = jasmineContent.firstChild
|
||||
expect(element.getModel().getPlaceholderText()).toBe 'testing'
|
||||
|
||||
it "honors the 'gutter-hidden' attribute", ->
|
||||
jasmineContent.innerHTML = "<atom-text-editor gutter-hidden>"
|
||||
element = jasmineContent.firstChild
|
||||
expect(element.getModel().isLineNumberGutterVisible()).toBe false
|
||||
|
||||
it "honors the text content", ->
|
||||
jasmineContent.innerHTML = "<atom-text-editor>testing</atom-text-editor>"
|
||||
element = jasmineContent.firstChild
|
||||
expect(element.getModel().getText()).toBe 'testing'
|
||||
|
||||
describe "when the model is assigned", ->
|
||||
it "adds the 'mini' attribute if .isMini() returns true on the model", ->
|
||||
element = new TextEditorElement
|
||||
model = new TextEditor({mini: true})
|
||||
element.setModel(model)
|
||||
expect(element.hasAttribute('mini')).toBe true
|
||||
|
||||
describe "when the editor is attached to the DOM", ->
|
||||
it "mounts the component and unmounts when removed from the dom", ->
|
||||
element = new TextEditorElement
|
||||
jasmine.attachToDOM(element)
|
||||
|
||||
component = element.component
|
||||
expect(component.mounted).toBe true
|
||||
element.remove()
|
||||
expect(component.mounted).toBe false
|
||||
|
||||
jasmine.attachToDOM(element)
|
||||
expect(element.component.mounted).toBe true
|
||||
|
||||
describe "when the editor is detached from the DOM and then reattached", ->
|
||||
it "does not render duplicate line numbers", ->
|
||||
editor = new TextEditor
|
||||
editor.setText('1\n2\n3')
|
||||
element = editor.getElement()
|
||||
|
||||
jasmine.attachToDOM(element)
|
||||
|
||||
initialCount = element.querySelectorAll('.line-number').length
|
||||
|
||||
element.remove()
|
||||
jasmine.attachToDOM(element)
|
||||
expect(element.querySelectorAll('.line-number').length).toBe initialCount
|
||||
|
||||
it "does not render duplicate decorations in custom gutters", ->
|
||||
editor = new TextEditor
|
||||
editor.setText('1\n2\n3')
|
||||
editor.addGutter({name: 'test-gutter'})
|
||||
marker = editor.markBufferRange([[0, 0], [2, 0]])
|
||||
editor.decorateMarker(marker, {type: 'gutter', gutterName: 'test-gutter'})
|
||||
element = editor.getElement()
|
||||
|
||||
jasmine.attachToDOM(element)
|
||||
initialDecorationCount = element.querySelectorAll('.decoration').length
|
||||
|
||||
element.remove()
|
||||
jasmine.attachToDOM(element)
|
||||
expect(element.querySelectorAll('.decoration').length).toBe initialDecorationCount
|
||||
|
||||
it "can be re-focused using the previous `document.activeElement`", ->
|
||||
editorElement = document.createElement('atom-text-editor')
|
||||
jasmine.attachToDOM(editorElement)
|
||||
editorElement.focus()
|
||||
|
||||
activeElement = document.activeElement
|
||||
|
||||
editorElement.remove()
|
||||
jasmine.attachToDOM(editorElement)
|
||||
activeElement.focus()
|
||||
|
||||
expect(editorElement.hasFocus()).toBe(true)
|
||||
|
||||
describe "focus and blur handling", ->
|
||||
it "proxies focus/blur events to/from the hidden input", ->
|
||||
element = new TextEditorElement
|
||||
jasmineContent.appendChild(element)
|
||||
|
||||
blurCalled = false
|
||||
element.addEventListener 'blur', -> blurCalled = true
|
||||
|
||||
element.focus()
|
||||
expect(blurCalled).toBe false
|
||||
expect(element.hasFocus()).toBe true
|
||||
expect(document.activeElement).toBe element.querySelector('input')
|
||||
|
||||
document.body.focus()
|
||||
expect(blurCalled).toBe true
|
||||
|
||||
it "doesn't trigger a blur event on the editor element when focusing an already focused editor element", ->
|
||||
blurCalled = false
|
||||
element = new TextEditorElement
|
||||
element.addEventListener 'blur', -> blurCalled = true
|
||||
|
||||
jasmineContent.appendChild(element)
|
||||
expect(document.activeElement).toBe(document.body)
|
||||
expect(blurCalled).toBe(false)
|
||||
|
||||
element.focus()
|
||||
expect(document.activeElement).toBe(element.querySelector('input'))
|
||||
expect(blurCalled).toBe(false)
|
||||
|
||||
element.focus()
|
||||
expect(document.activeElement).toBe(element.querySelector('input'))
|
||||
expect(blurCalled).toBe(false)
|
||||
|
||||
describe "when focused while a parent node is being attached to the DOM", ->
|
||||
class ElementThatFocusesChild extends HTMLDivElement
|
||||
attachedCallback: ->
|
||||
@firstChild.focus()
|
||||
|
||||
document.registerElement("element-that-focuses-child",
|
||||
prototype: ElementThatFocusesChild.prototype
|
||||
)
|
||||
|
||||
it "proxies the focus event to the hidden input", ->
|
||||
element = new TextEditorElement
|
||||
parentElement = document.createElement("element-that-focuses-child")
|
||||
parentElement.appendChild(element)
|
||||
jasmineContent.appendChild(parentElement)
|
||||
expect(document.activeElement).toBe element.querySelector('input')
|
||||
|
||||
describe "when the themes finish loading", ->
|
||||
[themeReloadCallback, initialThemeLoadComplete, element] = []
|
||||
|
||||
beforeEach ->
|
||||
themeReloadCallback = null
|
||||
initialThemeLoadComplete = false
|
||||
|
||||
spyOn(atom.themes, 'isInitialLoadComplete').andCallFake ->
|
||||
initialThemeLoadComplete
|
||||
spyOn(atom.themes, 'onDidChangeActiveThemes').andCallFake (fn) ->
|
||||
themeReloadCallback = fn
|
||||
new Disposable
|
||||
|
||||
element = new TextEditorElement()
|
||||
element.style.height = '200px'
|
||||
element.getModel().update({autoHeight: false})
|
||||
element.getModel().setText [0..20].join("\n")
|
||||
|
||||
it "re-renders the scrollbar", ->
|
||||
jasmineContent.appendChild(element)
|
||||
|
||||
atom.styles.addStyleSheet("""
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
""", context: 'atom-text-editor')
|
||||
|
||||
initialThemeLoadComplete = true
|
||||
themeReloadCallback()
|
||||
|
||||
verticalScrollbarNode = element.querySelector(".vertical-scrollbar")
|
||||
scrollbarWidth = verticalScrollbarNode.offsetWidth - verticalScrollbarNode.clientWidth
|
||||
expect(scrollbarWidth).toEqual(8)
|
||||
|
||||
describe "::onDidAttach and ::onDidDetach", ->
|
||||
it "invokes callbacks when the element is attached and detached", ->
|
||||
element = new TextEditorElement
|
||||
|
||||
attachedCallback = jasmine.createSpy("attachedCallback")
|
||||
detachedCallback = jasmine.createSpy("detachedCallback")
|
||||
|
||||
element.onDidAttach(attachedCallback)
|
||||
element.onDidDetach(detachedCallback)
|
||||
|
||||
jasmine.attachToDOM(element)
|
||||
|
||||
expect(attachedCallback).toHaveBeenCalled()
|
||||
expect(detachedCallback).not.toHaveBeenCalled()
|
||||
|
||||
attachedCallback.reset()
|
||||
element.remove()
|
||||
|
||||
expect(attachedCallback).not.toHaveBeenCalled()
|
||||
expect(detachedCallback).toHaveBeenCalled()
|
||||
|
||||
describe "::setUpdatedSynchronously", ->
|
||||
it "controls whether the text editor is updated synchronously", ->
|
||||
spyOn(window, 'requestAnimationFrame').andCallFake (fn) -> fn()
|
||||
|
||||
element = new TextEditorElement
|
||||
jasmine.attachToDOM(element)
|
||||
|
||||
element.setUpdatedSynchronously(false)
|
||||
expect(element.isUpdatedSynchronously()).toBe false
|
||||
|
||||
element.getModel().setText("hello")
|
||||
expect(window.requestAnimationFrame).toHaveBeenCalled()
|
||||
|
||||
expect(element.textContent).toContain "hello"
|
||||
|
||||
window.requestAnimationFrame.reset()
|
||||
element.setUpdatedSynchronously(true)
|
||||
element.getModel().setText("goodbye")
|
||||
expect(window.requestAnimationFrame).not.toHaveBeenCalled()
|
||||
expect(element.textContent).toContain "goodbye"
|
||||
|
||||
describe "::getDefaultCharacterWidth", ->
|
||||
it "returns null before the element is attached", ->
|
||||
element = new TextEditorElement
|
||||
expect(element.getDefaultCharacterWidth()).toBeNull()
|
||||
|
||||
it "returns the width of a character in the root scope", ->
|
||||
element = new TextEditorElement
|
||||
jasmine.attachToDOM(element)
|
||||
expect(element.getDefaultCharacterWidth()).toBeGreaterThan(0)
|
||||
|
||||
describe "::getMaxScrollTop", ->
|
||||
it "returns the maximum scroll top that can be applied to the element", ->
|
||||
editor = new TextEditor
|
||||
editor.setText('1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16')
|
||||
element = editor.getElement()
|
||||
element.style.lineHeight = "10px"
|
||||
element.style.width = "200px"
|
||||
jasmine.attachToDOM(element)
|
||||
|
||||
expect(element.getMaxScrollTop()).toBe(0)
|
||||
|
||||
element.style.height = '100px'
|
||||
editor.update({autoHeight: false})
|
||||
element.component.measureDimensions()
|
||||
expect(element.getMaxScrollTop()).toBe(60)
|
||||
|
||||
element.style.height = '120px'
|
||||
element.component.measureDimensions()
|
||||
expect(element.getMaxScrollTop()).toBe(40)
|
||||
|
||||
element.style.height = '200px'
|
||||
element.component.measureDimensions()
|
||||
expect(element.getMaxScrollTop()).toBe(0)
|
||||
|
||||
describe "on TextEditor::setMini", ->
|
||||
it "changes the element's 'mini' attribute", ->
|
||||
element = new TextEditorElement
|
||||
jasmine.attachToDOM(element)
|
||||
expect(element.hasAttribute('mini')).toBe false
|
||||
element.getModel().setMini(true)
|
||||
expect(element.hasAttribute('mini')).toBe true
|
||||
element.getModel().setMini(false)
|
||||
expect(element.hasAttribute('mini')).toBe false
|
||||
|
||||
describe "events", ->
|
||||
element = null
|
||||
|
||||
beforeEach ->
|
||||
element = new TextEditorElement
|
||||
element.getModel().setText("lorem\nipsum\ndolor\nsit\namet")
|
||||
element.setUpdatedSynchronously(true)
|
||||
element.setHeight(20)
|
||||
element.setWidth(20)
|
||||
element.getModel().update({autoHeight: false})
|
||||
|
||||
describe "::onDidChangeScrollTop(callback)", ->
|
||||
it "triggers even when subscribing before attaching the element", ->
|
||||
positions = []
|
||||
subscription1 = element.onDidChangeScrollTop (p) -> positions.push(p)
|
||||
jasmine.attachToDOM(element)
|
||||
subscription2 = element.onDidChangeScrollTop (p) -> positions.push(p)
|
||||
|
||||
positions.length = 0
|
||||
element.setScrollTop(10)
|
||||
expect(positions).toEqual([10, 10])
|
||||
|
||||
element.remove()
|
||||
jasmine.attachToDOM(element)
|
||||
|
||||
positions.length = 0
|
||||
element.setScrollTop(20)
|
||||
expect(positions).toEqual([20, 20])
|
||||
|
||||
subscription1.dispose()
|
||||
|
||||
positions.length = 0
|
||||
element.setScrollTop(30)
|
||||
expect(positions).toEqual([30])
|
||||
|
||||
describe "::onDidChangeScrollLeft(callback)", ->
|
||||
it "triggers even when subscribing before attaching the element", ->
|
||||
positions = []
|
||||
subscription1 = element.onDidChangeScrollLeft (p) -> positions.push(p)
|
||||
jasmine.attachToDOM(element)
|
||||
subscription2 = element.onDidChangeScrollLeft (p) -> positions.push(p)
|
||||
|
||||
positions.length = 0
|
||||
element.setScrollLeft(10)
|
||||
expect(positions).toEqual([10, 10])
|
||||
|
||||
element.remove()
|
||||
jasmine.attachToDOM(element)
|
||||
|
||||
positions.length = 0
|
||||
element.setScrollLeft(20)
|
||||
expect(positions).toEqual([20, 20])
|
||||
|
||||
subscription1.dispose()
|
||||
|
||||
positions.length = 0
|
||||
element.setScrollLeft(30)
|
||||
expect(positions).toEqual([30])
|
||||
428
spec/text-editor-element-spec.js
Normal file
428
spec/text-editor-element-spec.js
Normal file
@@ -0,0 +1,428 @@
|
||||
/* global HTMLDivElement */
|
||||
|
||||
const {it, fit, ffit, fffit, beforeEach, afterEach, conditionPromise, timeoutPromise} = require('./async-spec-helpers')
|
||||
const TextEditor = require('../src/text-editor')
|
||||
const TextEditorElement = require('../src/text-editor-element')
|
||||
|
||||
describe('TextEditorElement', () => {
|
||||
let jasmineContent
|
||||
|
||||
beforeEach(() => {
|
||||
jasmineContent = document.body.querySelector('#jasmine-content')
|
||||
})
|
||||
|
||||
function buildTextEditorElement (options = {}) {
|
||||
const element = new TextEditorElement()
|
||||
element.setUpdatedSynchronously(false)
|
||||
if (options.attach !== false) jasmine.attachToDOM(element)
|
||||
return element
|
||||
}
|
||||
|
||||
it("honors the 'mini' attribute", () => {
|
||||
jasmineContent.innerHTML = '<atom-text-editor mini>'
|
||||
const element = jasmineContent.firstChild
|
||||
expect(element.getModel().isMini()).toBe(true)
|
||||
|
||||
element.removeAttribute('mini')
|
||||
expect(element.getModel().isMini()).toBe(false)
|
||||
expect(element.getComponent().getGutterContainerWidth()).toBe(0)
|
||||
|
||||
element.setAttribute('mini', '')
|
||||
expect(element.getModel().isMini()).toBe(true)
|
||||
})
|
||||
|
||||
it('sets the editor to mini if the model is accessed prior to attaching the element', () => {
|
||||
const parent = document.createElement('div')
|
||||
parent.innerHTML = '<atom-text-editor mini>'
|
||||
const element = parent.firstChild
|
||||
expect(element.getModel().isMini()).toBe(true)
|
||||
})
|
||||
|
||||
it("honors the 'placeholder-text' attribute", () => {
|
||||
jasmineContent.innerHTML = "<atom-text-editor placeholder-text='testing'>"
|
||||
const element = jasmineContent.firstChild
|
||||
expect(element.getModel().getPlaceholderText()).toBe('testing')
|
||||
|
||||
element.setAttribute('placeholder-text', 'placeholder')
|
||||
expect(element.getModel().getPlaceholderText()).toBe('placeholder')
|
||||
|
||||
element.removeAttribute('placeholder-text')
|
||||
expect(element.getModel().getPlaceholderText()).toBeNull()
|
||||
})
|
||||
|
||||
it("only assigns 'placeholder-text' on the model if the attribute is present", () => {
|
||||
const editor = new TextEditor({placeholderText: 'placeholder'})
|
||||
editor.getElement()
|
||||
expect(editor.getPlaceholderText()).toBe('placeholder')
|
||||
})
|
||||
|
||||
it("honors the 'gutter-hidden' attribute", () => {
|
||||
jasmineContent.innerHTML = '<atom-text-editor gutter-hidden>'
|
||||
const element = jasmineContent.firstChild
|
||||
expect(element.getModel().isLineNumberGutterVisible()).toBe(false)
|
||||
|
||||
element.removeAttribute('gutter-hidden')
|
||||
expect(element.getModel().isLineNumberGutterVisible()).toBe(true)
|
||||
|
||||
element.setAttribute('gutter-hidden', '')
|
||||
expect(element.getModel().isLineNumberGutterVisible()).toBe(false)
|
||||
})
|
||||
|
||||
it('honors the text content', () => {
|
||||
jasmineContent.innerHTML = '<atom-text-editor>testing</atom-text-editor>'
|
||||
const element = jasmineContent.firstChild
|
||||
expect(element.getModel().getText()).toBe('testing')
|
||||
})
|
||||
|
||||
describe('when the model is assigned', () =>
|
||||
it("adds the 'mini' attribute if .isMini() returns true on the model", function (done) {
|
||||
const element = buildTextEditorElement()
|
||||
element.getModel().update({mini: true})
|
||||
atom.views.getNextUpdatePromise().then(() => {
|
||||
expect(element.hasAttribute('mini')).toBe(true)
|
||||
done()
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
describe('when the editor is attached to the DOM', () =>
|
||||
it('mounts the component and unmounts when removed from the dom', () => {
|
||||
const element = buildTextEditorElement()
|
||||
|
||||
const { component } = element
|
||||
expect(component.attached).toBe(true)
|
||||
element.remove()
|
||||
expect(component.attached).toBe(false)
|
||||
|
||||
jasmine.attachToDOM(element)
|
||||
expect(element.component.attached).toBe(true)
|
||||
})
|
||||
)
|
||||
|
||||
describe('when the editor is detached from the DOM and then reattached', () => {
|
||||
it('does not render duplicate line numbers', () => {
|
||||
const editor = new TextEditor()
|
||||
editor.setText('1\n2\n3')
|
||||
const element = editor.getElement()
|
||||
jasmine.attachToDOM(element)
|
||||
|
||||
const initialCount = element.querySelectorAll('.line-number').length
|
||||
|
||||
element.remove()
|
||||
jasmine.attachToDOM(element)
|
||||
expect(element.querySelectorAll('.line-number').length).toBe(initialCount)
|
||||
})
|
||||
|
||||
it('does not render duplicate decorations in custom gutters', () => {
|
||||
const editor = new TextEditor()
|
||||
editor.setText('1\n2\n3')
|
||||
editor.addGutter({name: 'test-gutter'})
|
||||
const marker = editor.markBufferRange([[0, 0], [2, 0]])
|
||||
editor.decorateMarker(marker, {type: 'gutter', gutterName: 'test-gutter'})
|
||||
const element = editor.getElement()
|
||||
|
||||
jasmine.attachToDOM(element)
|
||||
const initialDecorationCount = element.querySelectorAll('.decoration').length
|
||||
|
||||
element.remove()
|
||||
jasmine.attachToDOM(element)
|
||||
expect(element.querySelectorAll('.decoration').length).toBe(initialDecorationCount)
|
||||
})
|
||||
|
||||
it('can be re-focused using the previous `document.activeElement`', () => {
|
||||
const editorElement = buildTextEditorElement()
|
||||
editorElement.focus()
|
||||
|
||||
const { activeElement } = document
|
||||
|
||||
editorElement.remove()
|
||||
jasmine.attachToDOM(editorElement)
|
||||
activeElement.focus()
|
||||
|
||||
expect(editorElement.hasFocus()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('focus and blur handling', () => {
|
||||
it('proxies focus/blur events to/from the hidden input', () => {
|
||||
const element = buildTextEditorElement()
|
||||
jasmineContent.appendChild(element)
|
||||
|
||||
let blurCalled = false
|
||||
element.addEventListener('blur', () => {
|
||||
blurCalled = true
|
||||
})
|
||||
|
||||
element.focus()
|
||||
expect(blurCalled).toBe(false)
|
||||
expect(element.hasFocus()).toBe(true)
|
||||
expect(document.activeElement).toBe(element.querySelector('input'))
|
||||
|
||||
document.body.focus()
|
||||
expect(blurCalled).toBe(true)
|
||||
})
|
||||
|
||||
it("doesn't trigger a blur event on the editor element when focusing an already focused editor element", () => {
|
||||
let blurCalled = false
|
||||
const element = buildTextEditorElement()
|
||||
element.addEventListener('blur', () => { blurCalled = true })
|
||||
|
||||
jasmineContent.appendChild(element)
|
||||
expect(document.activeElement).toBe(document.body)
|
||||
expect(blurCalled).toBe(false)
|
||||
|
||||
element.focus()
|
||||
expect(document.activeElement).toBe(element.querySelector('input'))
|
||||
expect(blurCalled).toBe(false)
|
||||
|
||||
element.focus()
|
||||
expect(document.activeElement).toBe(element.querySelector('input'))
|
||||
expect(blurCalled).toBe(false)
|
||||
})
|
||||
|
||||
describe('when focused while a parent node is being attached to the DOM', () => {
|
||||
class ElementThatFocusesChild extends HTMLDivElement {
|
||||
attachedCallback () {
|
||||
this.firstChild.focus()
|
||||
}
|
||||
}
|
||||
|
||||
document.registerElement('element-that-focuses-child',
|
||||
{prototype: ElementThatFocusesChild.prototype}
|
||||
)
|
||||
|
||||
it('proxies the focus event to the hidden input', () => {
|
||||
const element = buildTextEditorElement()
|
||||
const parentElement = document.createElement('element-that-focuses-child')
|
||||
parentElement.appendChild(element)
|
||||
jasmineContent.appendChild(parentElement)
|
||||
expect(document.activeElement).toBe(element.querySelector('input'))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('::onDidAttach and ::onDidDetach', () =>
|
||||
it('invokes callbacks when the element is attached and detached', () => {
|
||||
const element = buildTextEditorElement({attach: false})
|
||||
|
||||
const attachedCallback = jasmine.createSpy('attachedCallback')
|
||||
const detachedCallback = jasmine.createSpy('detachedCallback')
|
||||
|
||||
element.onDidAttach(attachedCallback)
|
||||
element.onDidDetach(detachedCallback)
|
||||
|
||||
jasmine.attachToDOM(element)
|
||||
expect(attachedCallback).toHaveBeenCalled()
|
||||
expect(detachedCallback).not.toHaveBeenCalled()
|
||||
|
||||
attachedCallback.reset()
|
||||
element.remove()
|
||||
|
||||
expect(attachedCallback).not.toHaveBeenCalled()
|
||||
expect(detachedCallback).toHaveBeenCalled()
|
||||
})
|
||||
)
|
||||
|
||||
describe('::setUpdatedSynchronously', () =>
|
||||
it('controls whether the text editor is updated synchronously', () => {
|
||||
spyOn(window, 'requestAnimationFrame').andCallFake(fn => fn())
|
||||
|
||||
const element = buildTextEditorElement()
|
||||
jasmine.attachToDOM(element)
|
||||
|
||||
expect(element.isUpdatedSynchronously()).toBe(false)
|
||||
|
||||
element.getModel().setText('hello')
|
||||
expect(window.requestAnimationFrame).toHaveBeenCalled()
|
||||
|
||||
expect(element.textContent).toContain('hello')
|
||||
|
||||
window.requestAnimationFrame.reset()
|
||||
element.setUpdatedSynchronously(true)
|
||||
element.getModel().setText('goodbye')
|
||||
expect(window.requestAnimationFrame).not.toHaveBeenCalled()
|
||||
expect(element.textContent).toContain('goodbye')
|
||||
})
|
||||
)
|
||||
|
||||
describe('::getDefaultCharacterWidth', () => {
|
||||
it('returns 0 before the element is attached', () => {
|
||||
const element = buildTextEditorElement({attach: false})
|
||||
expect(element.getDefaultCharacterWidth()).toBe(0)
|
||||
})
|
||||
|
||||
it('returns the width of a character in the root scope', () => {
|
||||
const element = buildTextEditorElement()
|
||||
jasmine.attachToDOM(element)
|
||||
expect(element.getDefaultCharacterWidth()).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('::getMaxScrollTop', () =>
|
||||
it('returns the maximum scroll top that can be applied to the element', async () => {
|
||||
const editor = new TextEditor()
|
||||
editor.setText('1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16')
|
||||
const element = editor.getElement()
|
||||
element.style.lineHeight = '10px'
|
||||
element.style.width = '200px'
|
||||
jasmine.attachToDOM(element)
|
||||
|
||||
expect(element.getMaxScrollTop()).toBe(0)
|
||||
await editor.update({autoHeight: false})
|
||||
|
||||
element.style.height = '100px'
|
||||
await element.getNextUpdatePromise()
|
||||
expect(element.getMaxScrollTop()).toBe(60)
|
||||
|
||||
element.style.height = '120px'
|
||||
await element.getNextUpdatePromise()
|
||||
expect(element.getMaxScrollTop()).toBe(40)
|
||||
|
||||
element.style.height = '200px'
|
||||
await element.getNextUpdatePromise()
|
||||
expect(element.getMaxScrollTop()).toBe(0)
|
||||
})
|
||||
)
|
||||
|
||||
describe('::setScrollTop and ::setScrollLeft', () => {
|
||||
it('changes the scroll position', async () => {
|
||||
element = buildTextEditorElement()
|
||||
element.getModel().update({autoHeight: false})
|
||||
element.getModel().setText('lorem\nipsum\ndolor\nsit\namet')
|
||||
element.setHeight(20)
|
||||
await element.getNextUpdatePromise()
|
||||
element.setWidth(20)
|
||||
await element.getNextUpdatePromise()
|
||||
|
||||
element.setScrollTop(22)
|
||||
await element.getNextUpdatePromise()
|
||||
expect(element.getScrollTop()).toBe(22)
|
||||
|
||||
element.setScrollLeft(32)
|
||||
await element.getNextUpdatePromise()
|
||||
expect(element.getScrollLeft()).toBe(32)
|
||||
})
|
||||
})
|
||||
|
||||
describe('on TextEditor::setMini', () =>
|
||||
it("changes the element's 'mini' attribute", async () => {
|
||||
const element = buildTextEditorElement()
|
||||
expect(element.hasAttribute('mini')).toBe(false)
|
||||
element.getModel().setMini(true)
|
||||
await element.getNextUpdatePromise()
|
||||
expect(element.hasAttribute('mini')).toBe(true)
|
||||
element.getModel().setMini(false)
|
||||
await element.getNextUpdatePromise()
|
||||
expect(element.hasAttribute('mini')).toBe(false)
|
||||
})
|
||||
)
|
||||
|
||||
describe('::intersectsVisibleRowRange(start, end)', () => {
|
||||
it('returns true if the given row range intersects the visible row range', async () => {
|
||||
const element = buildTextEditorElement()
|
||||
const editor = element.getModel()
|
||||
editor.update({autoHeight: false})
|
||||
element.getModel().setText('x\n'.repeat(20))
|
||||
element.style.height = '120px'
|
||||
await element.getNextUpdatePromise()
|
||||
element.setScrollTop(80)
|
||||
await element.getNextUpdatePromise()
|
||||
expect(element.getVisibleRowRange()).toEqual([4, 11])
|
||||
|
||||
expect(element.intersectsVisibleRowRange(0, 4)).toBe(false)
|
||||
expect(element.intersectsVisibleRowRange(0, 5)).toBe(true)
|
||||
expect(element.intersectsVisibleRowRange(5, 8)).toBe(true)
|
||||
expect(element.intersectsVisibleRowRange(11, 12)).toBe(false)
|
||||
expect(element.intersectsVisibleRowRange(12, 13)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('::pixelRectForScreenRange(range)', () => {
|
||||
it('returns a {top/left/width/height} object describing the rectangle between two screen positions, even if they are not on screen', async () => {
|
||||
const element = buildTextEditorElement()
|
||||
const editor = element.getModel()
|
||||
editor.update({autoHeight: false})
|
||||
element.getModel().setText('xxxxxxxxxxxxxxxxxxxxxx\n'.repeat(20))
|
||||
element.style.height = '120px'
|
||||
await element.getNextUpdatePromise()
|
||||
element.setScrollTop(80)
|
||||
await element.getNextUpdatePromise()
|
||||
expect(element.getVisibleRowRange()).toEqual([4, 11])
|
||||
|
||||
const top = 2 * editor.getLineHeightInPixels()
|
||||
const bottom = 13 * editor.getLineHeightInPixels()
|
||||
const left = Math.round(3 * editor.getDefaultCharWidth())
|
||||
const right = Math.round(11 * editor.getDefaultCharWidth())
|
||||
expect(element.pixelRectForScreenRange([[2, 3], [13, 11]])).toEqual({
|
||||
top,
|
||||
left,
|
||||
height: bottom + editor.getLineHeightInPixels() - top,
|
||||
width: right - left
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('events', () => {
|
||||
let element = null
|
||||
|
||||
beforeEach(async () => {
|
||||
element = buildTextEditorElement()
|
||||
element.getModel().update({autoHeight: false})
|
||||
element.getModel().setText('lorem\nipsum\ndolor\nsit\namet')
|
||||
element.setHeight(20)
|
||||
await element.getNextUpdatePromise()
|
||||
element.setWidth(20)
|
||||
await element.getNextUpdatePromise()
|
||||
})
|
||||
|
||||
describe('::onDidChangeScrollTop(callback)', () =>
|
||||
it('triggers even when subscribing before attaching the element', () => {
|
||||
const positions = []
|
||||
const subscription1 = element.onDidChangeScrollTop(p => positions.push(p))
|
||||
element.onDidChangeScrollTop(p => positions.push(p))
|
||||
|
||||
positions.length = 0
|
||||
element.setScrollTop(10)
|
||||
expect(positions).toEqual([10, 10])
|
||||
|
||||
element.remove()
|
||||
jasmine.attachToDOM(element)
|
||||
|
||||
positions.length = 0
|
||||
element.setScrollTop(20)
|
||||
expect(positions).toEqual([20, 20])
|
||||
|
||||
subscription1.dispose()
|
||||
|
||||
positions.length = 0
|
||||
element.setScrollTop(30)
|
||||
expect(positions).toEqual([30])
|
||||
})
|
||||
)
|
||||
|
||||
describe('::onDidChangeScrollLeft(callback)', () =>
|
||||
it('triggers even when subscribing before attaching the element', () => {
|
||||
const positions = []
|
||||
const subscription1 = element.onDidChangeScrollLeft(p => positions.push(p))
|
||||
element.onDidChangeScrollLeft(p => positions.push(p))
|
||||
|
||||
positions.length = 0
|
||||
element.setScrollLeft(10)
|
||||
expect(positions).toEqual([10, 10])
|
||||
|
||||
element.remove()
|
||||
jasmine.attachToDOM(element)
|
||||
|
||||
positions.length = 0
|
||||
element.setScrollLeft(20)
|
||||
expect(positions).toEqual([20, 20])
|
||||
|
||||
subscription1.dispose()
|
||||
|
||||
positions.length = 0
|
||||
element.setScrollLeft(30)
|
||||
expect(positions).toEqual([30])
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
File diff suppressed because it is too large
Load Diff
@@ -19,7 +19,7 @@ describe('TextEditorRegistry', function () {
|
||||
packageManager: {deferredActivationHooks: null}
|
||||
})
|
||||
|
||||
editor = new TextEditor()
|
||||
editor = new TextEditor({autoHeight: false})
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
|
||||
@@ -99,23 +99,34 @@ describe "TextEditor", ->
|
||||
expect(editor.getAutoWidth()).toBeFalsy()
|
||||
expect(editor.getShowCursorOnSelection()).toBeTruthy()
|
||||
|
||||
editor.update({autoHeight: true, autoWidth: true, showCursorOnSelection: false})
|
||||
element = editor.getElement()
|
||||
element.setHeight(100)
|
||||
element.setWidth(100)
|
||||
jasmine.attachToDOM(element)
|
||||
|
||||
editor.update({showCursorOnSelection: false})
|
||||
editor.setSelectedBufferRange([[1, 2], [3, 4]])
|
||||
editor.addSelectionForBufferRange([[5, 6], [7, 8]], reversed: true)
|
||||
editor.firstVisibleScreenRow = 5
|
||||
editor.firstVisibleScreenColumn = 5
|
||||
editor.setScrollTopRow(3)
|
||||
expect(editor.getScrollTopRow()).toBe(3)
|
||||
editor.setScrollLeftColumn(4)
|
||||
expect(editor.getScrollLeftColumn()).toBe(4)
|
||||
editor.foldBufferRow(4)
|
||||
expect(editor.isFoldedAtBufferRow(4)).toBeTruthy()
|
||||
|
||||
editor2 = editor.copy()
|
||||
element2 = editor2.getElement()
|
||||
element2.setHeight(100)
|
||||
element2.setWidth(100)
|
||||
jasmine.attachToDOM(element2)
|
||||
expect(editor2.id).not.toBe editor.id
|
||||
expect(editor2.getSelectedBufferRanges()).toEqual editor.getSelectedBufferRanges()
|
||||
expect(editor2.getSelections()[1].isReversed()).toBeTruthy()
|
||||
expect(editor2.getFirstVisibleScreenRow()).toBe 5
|
||||
expect(editor2.getFirstVisibleScreenColumn()).toBe 5
|
||||
expect(editor2.getScrollTopRow()).toBe(3)
|
||||
expect(editor2.getScrollLeftColumn()).toBe(4)
|
||||
expect(editor2.isFoldedAtBufferRow(4)).toBeTruthy()
|
||||
expect(editor2.getAutoWidth()).toBeTruthy()
|
||||
expect(editor2.getAutoHeight()).toBeTruthy()
|
||||
expect(editor2.getAutoWidth()).toBe(false)
|
||||
expect(editor2.getAutoHeight()).toBe(false)
|
||||
expect(editor2.getShowCursorOnSelection()).toBeFalsy()
|
||||
|
||||
# editor2 can now diverge from its origin edit session
|
||||
@@ -137,7 +148,7 @@ describe "TextEditor", ->
|
||||
autoHeight: false
|
||||
})
|
||||
|
||||
expect(returnedPromise).toBe(atom.views.getNextUpdatePromise())
|
||||
expect(returnedPromise).toBe(element.component.getNextUpdatePromise())
|
||||
expect(changeSpy.callCount).toBe(1)
|
||||
expect(editor.getTabLength()).toBe(6)
|
||||
expect(editor.getSoftTabs()).toBe(false)
|
||||
@@ -1877,8 +1888,6 @@ describe "TextEditor", ->
|
||||
[[4, 16], [4, 21]]
|
||||
[[4, 25], [4, 29]]
|
||||
]
|
||||
for cursor in editor.getCursors()
|
||||
expect(cursor.isVisible()).toBeTruthy()
|
||||
|
||||
it "skips lines that are too short to create a non-empty selection", ->
|
||||
editor.setSelectedBufferRange([[3, 31], [3, 38]])
|
||||
@@ -2010,8 +2019,6 @@ describe "TextEditor", ->
|
||||
[[2, 16], [2, 21]]
|
||||
[[2, 37], [2, 40]]
|
||||
]
|
||||
for cursor in editor.getCursors()
|
||||
expect(cursor.isVisible()).toBeTruthy()
|
||||
|
||||
it "skips lines that are too short to create a non-empty selection", ->
|
||||
editor.setSelectedBufferRange([[6, 31], [6, 38]])
|
||||
@@ -2181,54 +2188,6 @@ describe "TextEditor", ->
|
||||
editor.setCursorScreenPosition([3, 3])
|
||||
expect(selection.isEmpty()).toBeTruthy()
|
||||
|
||||
describe "cursor visibility while there is a selection", ->
|
||||
describe "when showCursorOnSelection is true", ->
|
||||
it "is visible while there is no selection", ->
|
||||
expect(selection.isEmpty()).toBeTruthy()
|
||||
expect(editor.getShowCursorOnSelection()).toBeTruthy()
|
||||
expect(editor.getCursors().length).toBe 1
|
||||
expect(editor.getCursors()[0].isVisible()).toBeTruthy()
|
||||
|
||||
it "is visible while there is a selection", ->
|
||||
expect(selection.isEmpty()).toBeTruthy()
|
||||
editor.setSelectedBufferRange([[1, 2], [1, 5]])
|
||||
expect(selection.isEmpty()).toBeFalsy()
|
||||
expect(editor.getCursors().length).toBe 1
|
||||
expect(editor.getCursors()[0].isVisible()).toBeTruthy()
|
||||
|
||||
it "is visible while there are multiple selections", ->
|
||||
expect(editor.getSelections().length).toBe 1
|
||||
editor.setSelectedBufferRanges([[[1, 2], [1, 5]], [[2, 2], [2, 5]]])
|
||||
expect(editor.getSelections().length).toBe 2
|
||||
expect(editor.getCursors().length).toBe 2
|
||||
expect(editor.getCursors()[0].isVisible()).toBeTruthy()
|
||||
expect(editor.getCursors()[1].isVisible()).toBeTruthy()
|
||||
|
||||
describe "when showCursorOnSelection is false", ->
|
||||
it "is visible while there is no selection", ->
|
||||
editor.update({showCursorOnSelection: false})
|
||||
expect(selection.isEmpty()).toBeTruthy()
|
||||
expect(editor.getShowCursorOnSelection()).toBeFalsy()
|
||||
expect(editor.getCursors().length).toBe 1
|
||||
expect(editor.getCursors()[0].isVisible()).toBeTruthy()
|
||||
|
||||
it "is not visible while there is a selection", ->
|
||||
editor.update({showCursorOnSelection: false})
|
||||
expect(selection.isEmpty()).toBeTruthy()
|
||||
editor.setSelectedBufferRange([[1, 2], [1, 5]])
|
||||
expect(selection.isEmpty()).toBeFalsy()
|
||||
expect(editor.getCursors().length).toBe 1
|
||||
expect(editor.getCursors()[0].isVisible()).toBeFalsy()
|
||||
|
||||
it "is not visible while there are multiple selections", ->
|
||||
editor.update({showCursorOnSelection: false})
|
||||
expect(editor.getSelections().length).toBe 1
|
||||
editor.setSelectedBufferRanges([[[1, 2], [1, 5]], [[2, 2], [2, 5]]])
|
||||
expect(editor.getSelections().length).toBe 2
|
||||
expect(editor.getCursors().length).toBe 2
|
||||
expect(editor.getCursors()[0].isVisible()).toBeFalsy()
|
||||
expect(editor.getCursors()[1].isVisible()).toBeFalsy()
|
||||
|
||||
it "does not share selections between different edit sessions for the same buffer", ->
|
||||
editor2 = null
|
||||
waitsForPromise ->
|
||||
@@ -3295,7 +3254,6 @@ describe "TextEditor", ->
|
||||
expect(line).toBe " var ort = function(items) {"
|
||||
expect(editor.getCursorScreenPosition()).toEqual {row: 1, column: 6}
|
||||
expect(changeScreenRangeHandler).toHaveBeenCalled()
|
||||
expect(editor.getLastCursor().isVisible()).toBeTruthy()
|
||||
|
||||
describe "when the cursor is at the beginning of a line", ->
|
||||
it "joins it with the line above", ->
|
||||
@@ -4863,10 +4821,7 @@ describe "TextEditor", ->
|
||||
expect(buffer.getLineCount()).toBe(count - 1)
|
||||
|
||||
describe "when the line being deleted preceeds a fold, and the command is undone", ->
|
||||
# TODO: This seemed to have only been passing due to an accident in the text
|
||||
# buffer implementation. Once we moved selections to a different layer it
|
||||
# broke. We need to revisit our representation of folds and then reenable it.
|
||||
xit "restores the line and preserves the fold", ->
|
||||
it "restores the line and preserves the fold", ->
|
||||
editor.setCursorBufferPosition([4])
|
||||
editor.foldCurrentRow()
|
||||
expect(editor.isFoldedAtScreenRow(4)).toBeTruthy()
|
||||
@@ -5476,8 +5431,8 @@ describe "TextEditor", ->
|
||||
|
||||
tokens = editor.tokensForScreenRow(0)
|
||||
expect(tokens).toEqual [
|
||||
{text: '//', scopes: ['syntax--source.syntax--js', 'syntax--comment.syntax--line.syntax--double-slash.syntax--js', 'syntax--punctuation.syntax--definition.syntax--comment.syntax--js']},
|
||||
{text: ' http://github.com', scopes: ['syntax--source.syntax--js', 'syntax--comment.syntax--line.syntax--double-slash.syntax--js']}
|
||||
{text: '//', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--punctuation syntax--definition syntax--comment syntax--js']},
|
||||
{text: ' http://github.com', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']}
|
||||
]
|
||||
|
||||
waitsForPromise ->
|
||||
@@ -5486,9 +5441,9 @@ describe "TextEditor", ->
|
||||
runs ->
|
||||
tokens = editor.tokensForScreenRow(0)
|
||||
expect(tokens).toEqual [
|
||||
{text: '//', scopes: ['syntax--source.syntax--js', 'syntax--comment.syntax--line.syntax--double-slash.syntax--js', 'syntax--punctuation.syntax--definition.syntax--comment.syntax--js']},
|
||||
{text: ' ', scopes: ['syntax--source.syntax--js', 'syntax--comment.syntax--line.syntax--double-slash.syntax--js']}
|
||||
{text: 'http://github.com', scopes: ['syntax--source.syntax--js', 'syntax--comment.syntax--line.syntax--double-slash.syntax--js', 'syntax--markup.syntax--underline.syntax--link.syntax--http.syntax--hyperlink']}
|
||||
{text: '//', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--punctuation syntax--definition syntax--comment syntax--js']},
|
||||
{text: ' ', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']}
|
||||
{text: 'http://github.com', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--markup syntax--underline syntax--link syntax--http syntax--hyperlink']}
|
||||
]
|
||||
|
||||
describe "when the grammar is updated", ->
|
||||
@@ -5501,8 +5456,8 @@ describe "TextEditor", ->
|
||||
|
||||
tokens = editor.tokensForScreenRow(0)
|
||||
expect(tokens).toEqual [
|
||||
{text: '//', scopes: ['syntax--source.syntax--js', 'syntax--comment.syntax--line.syntax--double-slash.syntax--js', 'syntax--punctuation.syntax--definition.syntax--comment.syntax--js']},
|
||||
{text: ' SELECT * FROM OCTOCATS', scopes: ['syntax--source.syntax--js', 'syntax--comment.syntax--line.syntax--double-slash.syntax--js']}
|
||||
{text: '//', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--punctuation syntax--definition syntax--comment syntax--js']},
|
||||
{text: ' SELECT * FROM OCTOCATS', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']}
|
||||
]
|
||||
|
||||
waitsForPromise ->
|
||||
@@ -5511,8 +5466,8 @@ describe "TextEditor", ->
|
||||
runs ->
|
||||
tokens = editor.tokensForScreenRow(0)
|
||||
expect(tokens).toEqual [
|
||||
{text: '//', scopes: ['syntax--source.syntax--js', 'syntax--comment.syntax--line.syntax--double-slash.syntax--js', 'syntax--punctuation.syntax--definition.syntax--comment.syntax--js']},
|
||||
{text: ' SELECT * FROM OCTOCATS', scopes: ['syntax--source.syntax--js', 'syntax--comment.syntax--line.syntax--double-slash.syntax--js']}
|
||||
{text: '//', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--punctuation syntax--definition syntax--comment syntax--js']},
|
||||
{text: ' SELECT * FROM OCTOCATS', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']}
|
||||
]
|
||||
|
||||
waitsForPromise ->
|
||||
@@ -5521,14 +5476,14 @@ describe "TextEditor", ->
|
||||
runs ->
|
||||
tokens = editor.tokensForScreenRow(0)
|
||||
expect(tokens).toEqual [
|
||||
{text: '//', scopes: ['syntax--source.syntax--js', 'syntax--comment.syntax--line.syntax--double-slash.syntax--js', 'syntax--punctuation.syntax--definition.syntax--comment.syntax--js']},
|
||||
{text: ' ', scopes: ['syntax--source.syntax--js', 'syntax--comment.syntax--line.syntax--double-slash.syntax--js']},
|
||||
{text: 'SELECT', scopes: ['syntax--source.syntax--js', 'syntax--comment.syntax--line.syntax--double-slash.syntax--js', 'syntax--keyword.syntax--other.syntax--DML.syntax--sql']},
|
||||
{text: ' ', scopes: ['syntax--source.syntax--js', 'syntax--comment.syntax--line.syntax--double-slash.syntax--js']},
|
||||
{text: '*', scopes: ['syntax--source.syntax--js', 'syntax--comment.syntax--line.syntax--double-slash.syntax--js', 'syntax--keyword.syntax--operator.syntax--star.syntax--sql']},
|
||||
{text: ' ', scopes: ['syntax--source.syntax--js', 'syntax--comment.syntax--line.syntax--double-slash.syntax--js']},
|
||||
{text: 'FROM', scopes: ['syntax--source.syntax--js', 'syntax--comment.syntax--line.syntax--double-slash.syntax--js', 'syntax--keyword.syntax--other.syntax--DML.syntax--sql']},
|
||||
{text: ' OCTOCATS', scopes: ['syntax--source.syntax--js', 'syntax--comment.syntax--line.syntax--double-slash.syntax--js']}
|
||||
{text: '//', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--punctuation syntax--definition syntax--comment syntax--js']},
|
||||
{text: ' ', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']},
|
||||
{text: 'SELECT', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--keyword syntax--other syntax--DML syntax--sql']},
|
||||
{text: ' ', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']},
|
||||
{text: '*', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--keyword syntax--operator syntax--star syntax--sql']},
|
||||
{text: ' ', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']},
|
||||
{text: 'FROM', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js', 'syntax--keyword syntax--other syntax--DML syntax--sql']},
|
||||
{text: ' OCTOCATS', scopes: ['syntax--source syntax--js', 'syntax--comment syntax--line syntax--double-slash syntax--js']}
|
||||
]
|
||||
|
||||
describe ".normalizeTabsInBufferRange()", ->
|
||||
@@ -5549,7 +5504,11 @@ describe "TextEditor", ->
|
||||
|
||||
describe ".pageUp/Down()", ->
|
||||
it "moves the cursor down one page length", ->
|
||||
editor.setRowsPerPage(5)
|
||||
editor.update(autoHeight: false)
|
||||
element = editor.getElement()
|
||||
jasmine.attachToDOM(element)
|
||||
element.style.height = element.component.getLineHeight() * 5 + 'px'
|
||||
element.measureDimensions()
|
||||
|
||||
expect(editor.getCursorBufferPosition().row).toBe 0
|
||||
|
||||
@@ -5567,7 +5526,11 @@ describe "TextEditor", ->
|
||||
|
||||
describe ".selectPageUp/Down()", ->
|
||||
it "selects one screen height of text up or down", ->
|
||||
editor.setRowsPerPage(5)
|
||||
editor.update(autoHeight: false)
|
||||
element = editor.getElement()
|
||||
jasmine.attachToDOM(element)
|
||||
element.style.height = element.component.getLineHeight() * 5 + 'px'
|
||||
element.measureDimensions()
|
||||
|
||||
expect(editor.getCursorBufferPosition().row).toBe 0
|
||||
|
||||
@@ -5590,72 +5553,6 @@ describe "TextEditor", ->
|
||||
editor.selectPageUp()
|
||||
expect(editor.getSelectedBufferRanges()).toEqual [[[0, 0], [12, 2]]]
|
||||
|
||||
describe "::setFirstVisibleScreenRow() and ::getFirstVisibleScreenRow()", ->
|
||||
beforeEach ->
|
||||
line = Array(9).join('0123456789')
|
||||
editor.setText([1..100].map(-> line).join('\n'))
|
||||
expect(editor.getLineCount()).toBe 100
|
||||
expect(editor.lineTextForBufferRow(0).length).toBe 80
|
||||
|
||||
describe "when the editor doesn't have a height and lineHeightInPixels", ->
|
||||
it "does not affect the editor's visible row range", ->
|
||||
expect(editor.getVisibleRowRange()).toBeNull()
|
||||
|
||||
editor.setFirstVisibleScreenRow(1)
|
||||
expect(editor.getFirstVisibleScreenRow()).toEqual 1
|
||||
|
||||
editor.setFirstVisibleScreenRow(3)
|
||||
expect(editor.getFirstVisibleScreenRow()).toEqual 3
|
||||
|
||||
expect(editor.getVisibleRowRange()).toBeNull()
|
||||
expect(editor.getLastVisibleScreenRow()).toBeNull()
|
||||
|
||||
describe "when the editor has a height and lineHeightInPixels", ->
|
||||
beforeEach ->
|
||||
editor.update({scrollPastEnd: true})
|
||||
editor.setHeight(100, true)
|
||||
editor.setLineHeightInPixels(10)
|
||||
|
||||
it "updates the editor's visible row range", ->
|
||||
editor.setFirstVisibleScreenRow(2)
|
||||
expect(editor.getFirstVisibleScreenRow()).toEqual 2
|
||||
expect(editor.getLastVisibleScreenRow()).toBe 12
|
||||
expect(editor.getVisibleRowRange()).toEqual [2, 12]
|
||||
|
||||
it "notifies ::onDidChangeFirstVisibleScreenRow observers", ->
|
||||
changeCount = 0
|
||||
editor.onDidChangeFirstVisibleScreenRow -> changeCount++
|
||||
|
||||
editor.setFirstVisibleScreenRow(2)
|
||||
expect(changeCount).toBe 1
|
||||
|
||||
editor.setFirstVisibleScreenRow(2)
|
||||
expect(changeCount).toBe 1
|
||||
|
||||
editor.setFirstVisibleScreenRow(3)
|
||||
expect(changeCount).toBe 2
|
||||
|
||||
it "ensures that the top row is less than the buffer's line count", ->
|
||||
editor.setFirstVisibleScreenRow(102)
|
||||
expect(editor.getFirstVisibleScreenRow()).toEqual 99
|
||||
expect(editor.getVisibleRowRange()).toEqual [99, 99]
|
||||
|
||||
it "ensures that the left column is less than the length of the longest screen line", ->
|
||||
editor.setFirstVisibleScreenRow(10)
|
||||
expect(editor.getFirstVisibleScreenRow()).toEqual 10
|
||||
|
||||
editor.setText("\n\n\n")
|
||||
|
||||
editor.setFirstVisibleScreenRow(10)
|
||||
expect(editor.getFirstVisibleScreenRow()).toEqual 3
|
||||
|
||||
describe "when the 'editor.scrollPastEnd' option is set to false", ->
|
||||
it "ensures that the bottom row is less than the buffer's line count", ->
|
||||
editor.update({scrollPastEnd: false})
|
||||
editor.setFirstVisibleScreenRow(95)
|
||||
expect(editor.getFirstVisibleScreenRow()).toEqual 89
|
||||
expect(editor.getVisibleRowRange()).toEqual [89, 99]
|
||||
|
||||
describe "::scrollToScreenPosition(position, [options])", ->
|
||||
it "triggers ::onDidRequestAutoscroll with the logical coordinates along with the options", ->
|
||||
scrollSpy = jasmine.createSpy("::onDidRequestAutoscroll")
|
||||
@@ -5677,6 +5574,12 @@ describe "TextEditor", ->
|
||||
editor.update({scrollPastEnd: false})
|
||||
expect(editor.getScrollPastEnd()).toBe(false)
|
||||
|
||||
it "always returns false when autoHeight is on", ->
|
||||
editor.update({autoHeight: true, scrollPastEnd: true})
|
||||
expect(editor.getScrollPastEnd()).toBe(false)
|
||||
editor.update({autoHeight: false})
|
||||
expect(editor.getScrollPastEnd()).toBe(true)
|
||||
|
||||
describe "auto height", ->
|
||||
it "returns true by default but can be customized", ->
|
||||
editor = new TextEditor
|
||||
@@ -5963,20 +5866,20 @@ describe "TextEditor", ->
|
||||
|
||||
editor.update({showIndentGuide: false})
|
||||
expect(editor.tokensForScreenRow(0)).toEqual [
|
||||
{text: ' ', scopes: ['syntax--source.syntax--js', 'leading-whitespace']},
|
||||
{text: 'foo', scopes: ['syntax--source.syntax--js']}
|
||||
{text: ' ', scopes: ['syntax--source syntax--js', 'leading-whitespace']},
|
||||
{text: 'foo', scopes: ['syntax--source syntax--js']}
|
||||
]
|
||||
|
||||
editor.update({showIndentGuide: true})
|
||||
expect(editor.tokensForScreenRow(0)).toEqual [
|
||||
{text: ' ', scopes: ['syntax--source.syntax--js', 'leading-whitespace indent-guide']},
|
||||
{text: 'foo', scopes: ['syntax--source.syntax--js']}
|
||||
{text: ' ', scopes: ['syntax--source syntax--js', 'leading-whitespace indent-guide']},
|
||||
{text: 'foo', scopes: ['syntax--source syntax--js']}
|
||||
]
|
||||
|
||||
editor.setMini(true)
|
||||
expect(editor.tokensForScreenRow(0)).toEqual [
|
||||
{text: ' ', scopes: ['syntax--source.syntax--js', 'leading-whitespace']},
|
||||
{text: 'foo', scopes: ['syntax--source.syntax--js']}
|
||||
{text: ' ', scopes: ['syntax--source syntax--js', 'leading-whitespace']},
|
||||
{text: 'foo', scopes: ['syntax--source syntax--js']}
|
||||
]
|
||||
|
||||
describe "when the editor is constructed with the grammar option set", ->
|
||||
|
||||
@@ -17,16 +17,6 @@ describe('TokenizedBufferIterator', () => {
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
},
|
||||
|
||||
grammar: {
|
||||
scopeForId (id) {
|
||||
return {
|
||||
'-1': 'foo', '-2': 'foo',
|
||||
'-3': 'bar', '-4': 'bar',
|
||||
'-5': 'baz', '-6': 'baz'
|
||||
}[id]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,57 +24,57 @@ describe('TokenizedBufferIterator', () => {
|
||||
|
||||
expect(iterator.seek(Point(0, 0))).toEqual([])
|
||||
expect(iterator.getPosition()).toEqual(Point(0, 0))
|
||||
expect(iterator.getCloseTags()).toEqual([])
|
||||
expect(iterator.getOpenTags()).toEqual(['syntax--foo'])
|
||||
expect(iterator.getCloseScopeIds()).toEqual([])
|
||||
expect(iterator.getOpenScopeIds()).toEqual([257])
|
||||
|
||||
iterator.moveToSuccessor()
|
||||
expect(iterator.getCloseTags()).toEqual(['syntax--foo'])
|
||||
expect(iterator.getOpenTags()).toEqual(['syntax--bar'])
|
||||
expect(iterator.getCloseScopeIds()).toEqual([257])
|
||||
expect(iterator.getOpenScopeIds()).toEqual([259])
|
||||
|
||||
expect(iterator.seek(Point(0, 1))).toEqual(['syntax--baz'])
|
||||
expect(iterator.seek(Point(0, 1))).toEqual([261])
|
||||
expect(iterator.getPosition()).toEqual(Point(0, 3))
|
||||
expect(iterator.getCloseTags()).toEqual([])
|
||||
expect(iterator.getOpenTags()).toEqual(['syntax--bar'])
|
||||
expect(iterator.getCloseScopeIds()).toEqual([])
|
||||
expect(iterator.getOpenScopeIds()).toEqual([259])
|
||||
|
||||
iterator.moveToSuccessor()
|
||||
expect(iterator.getPosition()).toEqual(Point(0, 3))
|
||||
expect(iterator.getCloseTags()).toEqual(['syntax--bar', 'syntax--baz'])
|
||||
expect(iterator.getOpenTags()).toEqual(['syntax--baz'])
|
||||
expect(iterator.getCloseScopeIds()).toEqual([259, 261])
|
||||
expect(iterator.getOpenScopeIds()).toEqual([261])
|
||||
|
||||
expect(iterator.seek(Point(0, 3))).toEqual(['syntax--baz'])
|
||||
expect(iterator.seek(Point(0, 3))).toEqual([261])
|
||||
expect(iterator.getPosition()).toEqual(Point(0, 3))
|
||||
expect(iterator.getCloseTags()).toEqual([])
|
||||
expect(iterator.getOpenTags()).toEqual(['syntax--bar'])
|
||||
expect(iterator.getCloseScopeIds()).toEqual([])
|
||||
expect(iterator.getOpenScopeIds()).toEqual([259])
|
||||
|
||||
iterator.moveToSuccessor()
|
||||
expect(iterator.getPosition()).toEqual(Point(0, 3))
|
||||
expect(iterator.getCloseTags()).toEqual(['syntax--bar', 'syntax--baz'])
|
||||
expect(iterator.getOpenTags()).toEqual(['syntax--baz'])
|
||||
expect(iterator.getCloseScopeIds()).toEqual([259, 261])
|
||||
expect(iterator.getOpenScopeIds()).toEqual([261])
|
||||
|
||||
iterator.moveToSuccessor()
|
||||
expect(iterator.getPosition()).toEqual(Point(0, 7))
|
||||
expect(iterator.getCloseTags()).toEqual(['syntax--baz'])
|
||||
expect(iterator.getOpenTags()).toEqual(['syntax--bar'])
|
||||
expect(iterator.getCloseScopeIds()).toEqual([261])
|
||||
expect(iterator.getOpenScopeIds()).toEqual([259])
|
||||
|
||||
iterator.moveToSuccessor()
|
||||
expect(iterator.getPosition()).toEqual(Point(0, 7))
|
||||
expect(iterator.getCloseTags()).toEqual(['syntax--bar'])
|
||||
expect(iterator.getOpenTags()).toEqual([])
|
||||
expect(iterator.getCloseScopeIds()).toEqual([259])
|
||||
expect(iterator.getOpenScopeIds()).toEqual([])
|
||||
|
||||
iterator.moveToSuccessor()
|
||||
expect(iterator.getPosition()).toEqual(Point(1, 0))
|
||||
expect(iterator.getCloseTags()).toEqual([])
|
||||
expect(iterator.getOpenTags()).toEqual([])
|
||||
expect(iterator.getCloseScopeIds()).toEqual([])
|
||||
expect(iterator.getOpenScopeIds()).toEqual([])
|
||||
|
||||
expect(iterator.seek(Point(0, 5))).toEqual(['syntax--baz'])
|
||||
expect(iterator.seek(Point(0, 5))).toEqual([261])
|
||||
expect(iterator.getPosition()).toEqual(Point(0, 7))
|
||||
expect(iterator.getCloseTags()).toEqual(['syntax--baz'])
|
||||
expect(iterator.getOpenTags()).toEqual(['syntax--bar'])
|
||||
expect(iterator.getCloseScopeIds()).toEqual([261])
|
||||
expect(iterator.getOpenScopeIds()).toEqual([259])
|
||||
|
||||
iterator.moveToSuccessor()
|
||||
expect(iterator.getPosition()).toEqual(Point(0, 7))
|
||||
expect(iterator.getCloseTags()).toEqual(['syntax--bar'])
|
||||
expect(iterator.getOpenTags()).toEqual([])
|
||||
expect(iterator.getCloseScopeIds()).toEqual([259])
|
||||
expect(iterator.getOpenScopeIds()).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -97,12 +87,6 @@ describe('TokenizedBufferIterator', () => {
|
||||
text: '',
|
||||
openScopes: []
|
||||
}
|
||||
},
|
||||
|
||||
grammar: {
|
||||
scopeForId () {
|
||||
return 'foo'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,80 +94,17 @@ describe('TokenizedBufferIterator', () => {
|
||||
|
||||
iterator.seek(Point(0, 0))
|
||||
expect(iterator.getPosition()).toEqual(Point(0, 0))
|
||||
expect(iterator.getCloseTags()).toEqual([])
|
||||
expect(iterator.getOpenTags()).toEqual(['syntax--foo'])
|
||||
expect(iterator.getCloseScopeIds()).toEqual([])
|
||||
expect(iterator.getOpenScopeIds()).toEqual([257])
|
||||
|
||||
iterator.moveToSuccessor()
|
||||
expect(iterator.getPosition()).toEqual(Point(0, 0))
|
||||
expect(iterator.getCloseTags()).toEqual(['syntax--foo'])
|
||||
expect(iterator.getOpenTags()).toEqual(['syntax--foo'])
|
||||
expect(iterator.getCloseScopeIds()).toEqual([257])
|
||||
expect(iterator.getOpenScopeIds()).toEqual([257])
|
||||
|
||||
iterator.moveToSuccessor()
|
||||
expect(iterator.getCloseTags()).toEqual(['syntax--foo'])
|
||||
expect(iterator.getOpenTags()).toEqual([])
|
||||
})
|
||||
|
||||
it("reports a boundary at line end if the next line's open scopes don't match the containing tags for the current line", () => {
|
||||
const tokenizedBuffer = {
|
||||
tokenizedLineForRow (row) {
|
||||
if (row === 0) {
|
||||
return {
|
||||
tags: [-1, 3, -2, -3],
|
||||
text: 'bar',
|
||||
openScopes: []
|
||||
}
|
||||
} else if (row === 1) {
|
||||
return {
|
||||
tags: [3],
|
||||
text: 'baz',
|
||||
openScopes: [-1]
|
||||
}
|
||||
} else if (row === 2) {
|
||||
return {
|
||||
tags: [-2],
|
||||
text: '',
|
||||
openScopes: [-1]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
grammar: {
|
||||
scopeForId (id) {
|
||||
if (id === -2 || id === -1) {
|
||||
return 'foo'
|
||||
} else if (id === -3) {
|
||||
return 'qux'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const iterator = new TokenizedBufferIterator(tokenizedBuffer)
|
||||
|
||||
iterator.seek(Point(0, 0))
|
||||
expect(iterator.getPosition()).toEqual(Point(0, 0))
|
||||
expect(iterator.getCloseTags()).toEqual([])
|
||||
expect(iterator.getOpenTags()).toEqual(['syntax--foo'])
|
||||
|
||||
iterator.moveToSuccessor()
|
||||
expect(iterator.getPosition()).toEqual(Point(0, 3))
|
||||
expect(iterator.getCloseTags()).toEqual(['syntax--foo'])
|
||||
expect(iterator.getOpenTags()).toEqual(['syntax--qux'])
|
||||
|
||||
iterator.moveToSuccessor()
|
||||
expect(iterator.getPosition()).toEqual(Point(0, 3))
|
||||
expect(iterator.getCloseTags()).toEqual(['syntax--qux'])
|
||||
expect(iterator.getOpenTags()).toEqual([])
|
||||
|
||||
iterator.moveToSuccessor()
|
||||
expect(iterator.getPosition()).toEqual(Point(1, 0))
|
||||
expect(iterator.getCloseTags()).toEqual([])
|
||||
expect(iterator.getOpenTags()).toEqual(['syntax--foo'])
|
||||
|
||||
iterator.moveToSuccessor()
|
||||
expect(iterator.getPosition()).toEqual(Point(2, 0))
|
||||
expect(iterator.getCloseTags()).toEqual(['syntax--foo'])
|
||||
expect(iterator.getOpenTags()).toEqual([])
|
||||
expect(iterator.getCloseScopeIds()).toEqual([257])
|
||||
expect(iterator.getOpenScopeIds()).toEqual([])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -590,43 +590,56 @@ describe "TokenizedBuffer", ->
|
||||
iterator.seek(Point(0, 0))
|
||||
|
||||
expectedBoundaries = [
|
||||
{position: Point(0, 0), closeTags: [], openTags: ["syntax--source.syntax--js", "syntax--storage.syntax--type.syntax--var.syntax--js"]}
|
||||
{position: Point(0, 3), closeTags: ["syntax--storage.syntax--type.syntax--var.syntax--js"], openTags: []}
|
||||
{position: Point(0, 8), closeTags: [], openTags: ["syntax--keyword.syntax--operator.syntax--assignment.syntax--js"]}
|
||||
{position: Point(0, 9), closeTags: ["syntax--keyword.syntax--operator.syntax--assignment.syntax--js"], openTags: []}
|
||||
{position: Point(0, 10), closeTags: [], openTags: ["syntax--constant.syntax--numeric.syntax--decimal.syntax--js"]}
|
||||
{position: Point(0, 11), closeTags: ["syntax--constant.syntax--numeric.syntax--decimal.syntax--js"], openTags: []}
|
||||
{position: Point(0, 12), closeTags: [], openTags: ["syntax--comment.syntax--block.syntax--js", "syntax--punctuation.syntax--definition.syntax--comment.syntax--js"]}
|
||||
{position: Point(0, 14), closeTags: ["syntax--punctuation.syntax--definition.syntax--comment.syntax--js"], openTags: []}
|
||||
{position: Point(1, 5), closeTags: [], openTags: ["syntax--punctuation.syntax--definition.syntax--comment.syntax--js"]}
|
||||
{position: Point(1, 7), closeTags: ["syntax--punctuation.syntax--definition.syntax--comment.syntax--js", "syntax--comment.syntax--block.syntax--js"], openTags: ["syntax--storage.syntax--type.syntax--var.syntax--js"]}
|
||||
{position: Point(1, 10), closeTags: ["syntax--storage.syntax--type.syntax--var.syntax--js"], openTags: []}
|
||||
{position: Point(1, 15), closeTags: [], openTags: ["syntax--keyword.syntax--operator.syntax--assignment.syntax--js"]}
|
||||
{position: Point(1, 16), closeTags: ["syntax--keyword.syntax--operator.syntax--assignment.syntax--js"], openTags: []}
|
||||
{position: Point(1, 17), closeTags: [], openTags: ["syntax--constant.syntax--numeric.syntax--decimal.syntax--js"]}
|
||||
{position: Point(1, 18), closeTags: ["syntax--constant.syntax--numeric.syntax--decimal.syntax--js"], openTags: []}
|
||||
{position: Point(0, 0), closeTags: [], openTags: ["syntax--source syntax--js", "syntax--storage syntax--type syntax--var syntax--js"]}
|
||||
{position: Point(0, 3), closeTags: ["syntax--storage syntax--type syntax--var syntax--js"], openTags: []}
|
||||
{position: Point(0, 8), closeTags: [], openTags: ["syntax--keyword syntax--operator syntax--assignment syntax--js"]}
|
||||
{position: Point(0, 9), closeTags: ["syntax--keyword syntax--operator syntax--assignment syntax--js"], openTags: []}
|
||||
{position: Point(0, 10), closeTags: [], openTags: ["syntax--constant syntax--numeric syntax--decimal syntax--js"]}
|
||||
{position: Point(0, 11), closeTags: ["syntax--constant syntax--numeric syntax--decimal syntax--js"], openTags: []}
|
||||
{position: Point(0, 12), closeTags: [], openTags: ["syntax--comment syntax--block syntax--js", "syntax--punctuation syntax--definition syntax--comment syntax--js"]}
|
||||
{position: Point(0, 14), closeTags: ["syntax--punctuation syntax--definition syntax--comment syntax--js"], openTags: []}
|
||||
{position: Point(1, 5), closeTags: [], openTags: ["syntax--punctuation syntax--definition syntax--comment syntax--js"]}
|
||||
{position: Point(1, 7), closeTags: ["syntax--punctuation syntax--definition syntax--comment syntax--js", "syntax--comment syntax--block syntax--js"], openTags: ["syntax--storage syntax--type syntax--var syntax--js"]}
|
||||
{position: Point(1, 10), closeTags: ["syntax--storage syntax--type syntax--var syntax--js"], openTags: []}
|
||||
{position: Point(1, 15), closeTags: [], openTags: ["syntax--keyword syntax--operator syntax--assignment syntax--js"]}
|
||||
{position: Point(1, 16), closeTags: ["syntax--keyword syntax--operator syntax--assignment syntax--js"], openTags: []}
|
||||
{position: Point(1, 17), closeTags: [], openTags: ["syntax--constant syntax--numeric syntax--decimal syntax--js"]}
|
||||
{position: Point(1, 18), closeTags: ["syntax--constant syntax--numeric syntax--decimal syntax--js"], openTags: []}
|
||||
]
|
||||
|
||||
loop
|
||||
boundary = {
|
||||
position: iterator.getPosition(),
|
||||
closeTags: iterator.getCloseTags(),
|
||||
openTags: iterator.getOpenTags()
|
||||
closeTags: iterator.getCloseScopeIds().map((scopeId) -> tokenizedBuffer.classNameForScopeId(scopeId)),
|
||||
openTags: iterator.getOpenScopeIds().map((scopeId) -> tokenizedBuffer.classNameForScopeId(scopeId))
|
||||
}
|
||||
|
||||
expect(boundary).toEqual(expectedBoundaries.shift())
|
||||
break unless iterator.moveToSuccessor()
|
||||
|
||||
expect(iterator.seek(Point(0, 1))).toEqual(["syntax--source.syntax--js", "syntax--storage.syntax--type.syntax--var.syntax--js"])
|
||||
expect(iterator.seek(Point(0, 1)).map((scopeId) -> tokenizedBuffer.classNameForScopeId(scopeId))).toEqual([
|
||||
"syntax--source syntax--js",
|
||||
"syntax--storage syntax--type syntax--var syntax--js"
|
||||
])
|
||||
expect(iterator.getPosition()).toEqual(Point(0, 3))
|
||||
expect(iterator.seek(Point(0, 8))).toEqual(["syntax--source.syntax--js"])
|
||||
expect(iterator.seek(Point(0, 8)).map((scopeId) -> tokenizedBuffer.classNameForScopeId(scopeId))).toEqual([
|
||||
"syntax--source syntax--js"
|
||||
])
|
||||
expect(iterator.getPosition()).toEqual(Point(0, 8))
|
||||
expect(iterator.seek(Point(1, 0))).toEqual(["syntax--source.syntax--js", "syntax--comment.syntax--block.syntax--js"])
|
||||
expect(iterator.seek(Point(1, 0)).map((scopeId) -> tokenizedBuffer.classNameForScopeId(scopeId))).toEqual([
|
||||
"syntax--source syntax--js",
|
||||
"syntax--comment syntax--block syntax--js"
|
||||
])
|
||||
expect(iterator.getPosition()).toEqual(Point(1, 0))
|
||||
expect(iterator.seek(Point(1, 18))).toEqual(["syntax--source.syntax--js", "syntax--constant.syntax--numeric.syntax--decimal.syntax--js"])
|
||||
expect(iterator.seek(Point(1, 18)).map((scopeId) -> tokenizedBuffer.classNameForScopeId(scopeId))).toEqual([
|
||||
"syntax--source syntax--js",
|
||||
"syntax--constant syntax--numeric syntax--decimal syntax--js"
|
||||
])
|
||||
expect(iterator.getPosition()).toEqual(Point(1, 18))
|
||||
|
||||
expect(iterator.seek(Point(2, 0))).toEqual(["syntax--source.syntax--js"])
|
||||
expect(iterator.seek(Point(2, 0)).map((scopeId) -> tokenizedBuffer.classNameForScopeId(scopeId))).toEqual([
|
||||
"syntax--source syntax--js"
|
||||
])
|
||||
iterator.moveToSuccessor() # ensure we don't infinitely loop (regression test)
|
||||
|
||||
it "does not report columns beyond the length of the line", ->
|
||||
@@ -671,5 +684,5 @@ describe "TokenizedBuffer", ->
|
||||
iterator.seek(Point(1, 0))
|
||||
|
||||
expect(iterator.getPosition()).toEqual([1, 0])
|
||||
expect(iterator.getCloseTags()).toEqual ['syntax--blue.syntax--broken']
|
||||
expect(iterator.getOpenTags()).toEqual ['syntax--yellow.syntax--broken']
|
||||
expect(iterator.getCloseScopeIds().map((scopeId) -> tokenizedBuffer.classNameForScopeId(scopeId))).toEqual ['syntax--blue syntax--broken']
|
||||
expect(iterator.getOpenScopeIds().map((scopeId) -> tokenizedBuffer.classNameForScopeId(scopeId))).toEqual ['syntax--yellow syntax--broken']
|
||||
|
||||
@@ -5,7 +5,6 @@ describe "ViewRegistry", ->
|
||||
|
||||
beforeEach ->
|
||||
registry = new ViewRegistry
|
||||
registry.initialize()
|
||||
|
||||
afterEach ->
|
||||
registry.clearDocumentRequests()
|
||||
@@ -127,8 +126,6 @@ describe "ViewRegistry", ->
|
||||
spyOn(window, 'clearInterval').andCallFake(fakeClearInterval)
|
||||
events = []
|
||||
|
||||
registry.pollDocument -> events.push('poll')
|
||||
registry.pollAfterNextUpdate()
|
||||
registry.updateDocument -> events.push('write 1')
|
||||
registry.readDocument ->
|
||||
registry.updateDocument -> events.push('write from read 1')
|
||||
@@ -147,108 +144,20 @@ describe "ViewRegistry", ->
|
||||
'write 2'
|
||||
'read 1'
|
||||
'read 2'
|
||||
'poll'
|
||||
'write from read 1'
|
||||
'write from read 2'
|
||||
]
|
||||
|
||||
it "pauses DOM polling when reads or writes are pending", ->
|
||||
spyOn(window, 'setInterval').andCallFake(fakeSetInterval)
|
||||
spyOn(window, 'clearInterval').andCallFake(fakeClearInterval)
|
||||
events = []
|
||||
|
||||
registry.pollDocument -> events.push('poll')
|
||||
registry.updateDocument -> events.push('write')
|
||||
registry.readDocument -> events.push('read')
|
||||
|
||||
window.dispatchEvent(new UIEvent('resize'))
|
||||
expect(events).toEqual []
|
||||
|
||||
frameRequests[0]()
|
||||
expect(events).toEqual ['write', 'read', 'poll']
|
||||
|
||||
window.dispatchEvent(new UIEvent('resize'))
|
||||
expect(events).toEqual ['write', 'read', 'poll', 'poll']
|
||||
|
||||
it "polls the document after updating when ::pollAfterNextUpdate() has been called", ->
|
||||
events = []
|
||||
registry.pollDocument -> events.push('poll')
|
||||
registry.updateDocument -> events.push('write')
|
||||
registry.readDocument -> events.push('read')
|
||||
frameRequests.shift()()
|
||||
expect(events).toEqual ['write', 'read']
|
||||
|
||||
events = []
|
||||
registry.pollAfterNextUpdate()
|
||||
registry.updateDocument -> events.push('write')
|
||||
registry.readDocument -> events.push('read')
|
||||
frameRequests.shift()()
|
||||
expect(events).toEqual ['write', 'read', 'poll']
|
||||
|
||||
describe "::pollDocument(fn)", ->
|
||||
[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 []
|
||||
|
||||
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']
|
||||
|
||||
describe "::getNextUpdatePromise()", ->
|
||||
it "returns a promise that resolves at the end of the next update cycle", ->
|
||||
updateCalled = false
|
||||
readCalled = false
|
||||
pollCalled = false
|
||||
|
||||
waitsFor 'getNextUpdatePromise to resolve', (done) ->
|
||||
registry.getNextUpdatePromise().then ->
|
||||
expect(updateCalled).toBe true
|
||||
expect(readCalled).toBe true
|
||||
expect(pollCalled).toBe true
|
||||
done()
|
||||
|
||||
registry.updateDocument -> updateCalled = true
|
||||
registry.readDocument -> readCalled = true
|
||||
registry.pollDocument -> pollCalled = true
|
||||
registry.pollAfterNextUpdate()
|
||||
|
||||
@@ -230,28 +230,32 @@ describe('WorkspaceElement', () => {
|
||||
editorElement = editor.getElement()
|
||||
})
|
||||
|
||||
it("updates the font-size based on the 'editor.fontSize' config value", () => {
|
||||
it("updates the font-size based on the 'editor.fontSize' config value", async () => {
|
||||
const initialCharWidth = editor.getDefaultCharWidth()
|
||||
expect(getComputedStyle(editorElement).fontSize).toBe(atom.config.get('editor.fontSize') + 'px')
|
||||
|
||||
atom.config.set('editor.fontSize', atom.config.get('editor.fontSize') + 5)
|
||||
await editorElement.component.getNextUpdatePromise()
|
||||
expect(getComputedStyle(editorElement).fontSize).toBe(atom.config.get('editor.fontSize') + 'px')
|
||||
expect(editor.getDefaultCharWidth()).toBeGreaterThan(initialCharWidth)
|
||||
})
|
||||
|
||||
it("updates the font-family based on the 'editor.fontFamily' config value", () => {
|
||||
it("updates the font-family based on the 'editor.fontFamily' config value", async () => {
|
||||
const initialCharWidth = editor.getDefaultCharWidth()
|
||||
let fontFamily = atom.config.get('editor.fontFamily')
|
||||
expect(getComputedStyle(editorElement).fontFamily).toBe(fontFamily)
|
||||
|
||||
atom.config.set('editor.fontFamily', 'sans-serif')
|
||||
fontFamily = atom.config.get('editor.fontFamily')
|
||||
await editorElement.component.getNextUpdatePromise()
|
||||
expect(getComputedStyle(editorElement).fontFamily).toBe(fontFamily)
|
||||
expect(editor.getDefaultCharWidth()).not.toBe(initialCharWidth)
|
||||
})
|
||||
|
||||
it("updates the line-height based on the 'editor.lineHeight' config value", () => {
|
||||
it("updates the line-height based on the 'editor.lineHeight' config value", async () => {
|
||||
const initialLineHeight = editor.getLineHeightInPixels()
|
||||
atom.config.set('editor.lineHeight', '30px')
|
||||
await editorElement.component.getNextUpdatePromise()
|
||||
expect(getComputedStyle(editorElement).lineHeight).toBe(atom.config.get('editor.lineHeight'))
|
||||
expect(editor.getLineHeightInPixels()).not.toBe(initialLineHeight)
|
||||
})
|
||||
|
||||
@@ -135,6 +135,7 @@ class AtomEnvironment extends Model
|
||||
@deserializers = new DeserializerManager(this)
|
||||
@deserializeTimings = {}
|
||||
@views = new ViewRegistry(this)
|
||||
TextEditor.setScheduler(@views)
|
||||
@notifications = new NotificationManager
|
||||
@updateProcessEnv ?= updateProcessEnv # For testing
|
||||
|
||||
@@ -208,8 +209,6 @@ class AtomEnvironment extends Model
|
||||
@getStorageFolder().clear()
|
||||
@stateStore.clear()
|
||||
|
||||
@views.initialize()
|
||||
|
||||
ConfigSchema.projectHome = {
|
||||
type: 'string',
|
||||
default: path.join(fs.getHomeDirectory(), 'github'),
|
||||
@@ -251,9 +250,13 @@ class AtomEnvironment extends Model
|
||||
@attachSaveStateListeners()
|
||||
@windowEventHandler.initialize(@window, @document)
|
||||
|
||||
didChangeStyles = @didChangeStyles.bind(this)
|
||||
@disposables.add(@styles.onDidAddStyleElement(didChangeStyles))
|
||||
@disposables.add(@styles.onDidUpdateStyleElement(didChangeStyles))
|
||||
@disposables.add(@styles.onDidRemoveStyleElement(didChangeStyles))
|
||||
|
||||
@observeAutoHideMenuBar()
|
||||
|
||||
@history.initialize(@window.localStorage)
|
||||
@disposables.add @applicationDelegate.onDidChangeHistoryManager(=> @history.loadState())
|
||||
|
||||
preloadPackages: ->
|
||||
@@ -261,7 +264,7 @@ class AtomEnvironment extends Model
|
||||
|
||||
attachSaveStateListeners: ->
|
||||
saveState = _.debounce((=>
|
||||
window.requestIdleCallback => @saveState({isUnloading: false}) unless @unloaded
|
||||
@window.requestIdleCallback => @saveState({isUnloading: false}) unless @unloaded
|
||||
), @saveStateDebounceInterval)
|
||||
@document.addEventListener('mousedown', saveState, true)
|
||||
@document.addEventListener('keydown', saveState, true)
|
||||
@@ -677,11 +680,8 @@ class AtomEnvironment extends Model
|
||||
# Call this method when establishing a real application window.
|
||||
startEditorWindow: ->
|
||||
@unloaded = false
|
||||
updateProcessEnvPromise = @updateProcessEnv(@getLoadSettings().env)
|
||||
updateProcessEnvPromise.then =>
|
||||
@shellEnvironmentLoaded = true
|
||||
@emitter.emit('loaded-shell-environment')
|
||||
@packages.triggerActivationHook('core:loaded-shell-environment')
|
||||
|
||||
updateProcessEnvPromise = @updateProcessEnvAndTriggerHooks()
|
||||
|
||||
loadStatePromise = @loadState().then (state) =>
|
||||
@windowDimensions = state?.windowDimensions
|
||||
@@ -800,6 +800,17 @@ class AtomEnvironment extends Model
|
||||
@windowEventHandler?.unsubscribe()
|
||||
@windowEventHandler = null
|
||||
|
||||
didChangeStyles: (styleElement) ->
|
||||
TextEditor.didUpdateStyles()
|
||||
if styleElement.textContent.indexOf('scrollbar') >= 0
|
||||
TextEditor.didUpdateScrollbarStyles()
|
||||
|
||||
updateProcessEnvAndTriggerHooks: ->
|
||||
@updateProcessEnv(@getLoadSettings().env).then =>
|
||||
@shellEnvironmentLoaded = true
|
||||
@emitter.emit('loaded-shell-environment')
|
||||
@packages.triggerActivationHook('core:loaded-shell-environment')
|
||||
|
||||
###
|
||||
Section: Messaging the User
|
||||
###
|
||||
|
||||
@@ -114,16 +114,19 @@ function writeCachedJavascript (relativeCachePath, code) {
|
||||
|
||||
var INLINE_SOURCE_MAP_REGEXP = /\/\/[#@]\s*sourceMappingURL=([^'"\n]+)\s*$/mg
|
||||
|
||||
let snapshotSourceMapConsumer
|
||||
if (global.isGeneratingSnapshot) {
|
||||
// Warm up the source map consumer to efficiently translate positions when
|
||||
// generating stack traces containing a file that was snapshotted.
|
||||
const {SourceMapConsumer} = require('source-map')
|
||||
snapshotSourceMapConsumer = new SourceMapConsumer(snapshotAuxiliaryData.sourceMap) // eslint-disable-line no-undef
|
||||
snapshotSourceMapConsumer.originalPositionFor({line: 42, column: 0})
|
||||
}
|
||||
|
||||
exports.install = function (resourcesPath, nodeRequire) {
|
||||
const snapshotSourceMapConsumer = {
|
||||
originalPositionFor ({line, column}) {
|
||||
const {relativePath, row} = snapshotResult.translateSnapshotRow(line)
|
||||
return {
|
||||
column,
|
||||
line: row,
|
||||
source: path.join(resourcesPath, 'app', 'static', relativePath),
|
||||
name: null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sourceMapSupport.install({
|
||||
handleUncaughtExceptions: false,
|
||||
|
||||
@@ -132,10 +135,7 @@ exports.install = function (resourcesPath, nodeRequire) {
|
||||
// code from our cache directory.
|
||||
retrieveSourceMap: function (filePath) {
|
||||
if (filePath === '<embedded>') {
|
||||
return {
|
||||
map: snapshotSourceMapConsumer,
|
||||
url: path.join(resourcesPath, 'app', 'static', 'index.js')
|
||||
}
|
||||
return {map: snapshotSourceMapConsumer}
|
||||
}
|
||||
|
||||
if (!cacheDirectory || !fs.isFileSync(filePath)) {
|
||||
|
||||
@@ -4,7 +4,7 @@ module.exports = function (extra) {
|
||||
productName: 'Atom',
|
||||
companyName: 'GitHub',
|
||||
submitURL: 'https://crashreporter.atom.io',
|
||||
autoSubmit: false,
|
||||
uploadToServer: false,
|
||||
extra: extra
|
||||
})
|
||||
}
|
||||
|
||||
@@ -12,20 +12,14 @@ EmptyLineRegExp = /(\r\n[\t ]*\r\n)|(\n[\t ]*\n)/g
|
||||
# of a {DisplayMarker}.
|
||||
module.exports =
|
||||
class Cursor extends Model
|
||||
showCursorOnSelection: null
|
||||
screenPosition: null
|
||||
bufferPosition: null
|
||||
goalColumn: null
|
||||
visible: true
|
||||
|
||||
# Instantiated by a {TextEditor}
|
||||
constructor: ({@editor, @marker, @showCursorOnSelection, id}) ->
|
||||
constructor: ({@editor, @marker, id}) ->
|
||||
@emitter = new Emitter
|
||||
|
||||
@showCursorOnSelection ?= true
|
||||
|
||||
@assignId(id)
|
||||
@updateVisibility()
|
||||
|
||||
destroy: ->
|
||||
@marker.destroy()
|
||||
@@ -57,15 +51,6 @@ class Cursor extends Model
|
||||
onDidDestroy: (callback) ->
|
||||
@emitter.on 'did-destroy', callback
|
||||
|
||||
# Public: Calls your `callback` when the cursor's visibility has changed
|
||||
#
|
||||
# * `callback` {Function}
|
||||
# * `visibility` {Boolean}
|
||||
#
|
||||
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
||||
onDidChangeVisibility: (callback) ->
|
||||
@emitter.on 'did-change-visibility', callback
|
||||
|
||||
###
|
||||
Section: Managing Cursor Position
|
||||
###
|
||||
@@ -568,21 +553,6 @@ class Cursor extends Model
|
||||
Section: Visibility
|
||||
###
|
||||
|
||||
# Public: Sets whether the cursor is visible.
|
||||
setVisible: (visible) ->
|
||||
if @visible isnt visible
|
||||
@visible = visible
|
||||
@emitter.emit 'did-change-visibility', @visible
|
||||
|
||||
# Public: Returns the visibility of the cursor.
|
||||
isVisible: -> @visible
|
||||
|
||||
updateVisibility: ->
|
||||
if @showCursorOnSelection
|
||||
@setVisible(true)
|
||||
else
|
||||
@setVisible(@marker.getBufferRange().isEmpty())
|
||||
|
||||
###
|
||||
Section: Comparing to another cursor
|
||||
###
|
||||
@@ -599,9 +569,6 @@ class Cursor extends Model
|
||||
Section: Utilities
|
||||
###
|
||||
|
||||
# Public: Prevents this cursor from causing scrolling.
|
||||
clearAutoscroll: ->
|
||||
|
||||
# Public: Deselects the current selection.
|
||||
clearSelection: (options) ->
|
||||
@selection?.clear(options)
|
||||
@@ -651,11 +618,6 @@ class Cursor extends Model
|
||||
Section: Private
|
||||
###
|
||||
|
||||
setShowCursorOnSelection: (value) ->
|
||||
if value isnt @showCursorOnSelection
|
||||
@showCursorOnSelection = value
|
||||
@updateVisibility()
|
||||
|
||||
getNonWordCharacters: ->
|
||||
@editor.getNonWordCharacters(@getScopeDescriptor().getScopesArray())
|
||||
|
||||
@@ -668,7 +630,8 @@ class Cursor extends Model
|
||||
{row, column} = @getScreenPosition()
|
||||
new Range(new Point(row, column), new Point(row, column + 1))
|
||||
|
||||
autoscroll: (options) ->
|
||||
autoscroll: (options = {}) ->
|
||||
options.clip = false
|
||||
@editor.scrollToScreenRange(@getScreenRange(), options)
|
||||
|
||||
getBeginningOfNextParagraphBufferPosition: ->
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
module.exports =
|
||||
class CursorsComponent
|
||||
oldState: null
|
||||
|
||||
constructor: ->
|
||||
@cursorNodesById = {}
|
||||
@domNode = document.createElement('div')
|
||||
@domNode.classList.add('cursors')
|
||||
|
||||
getDomNode: ->
|
||||
@domNode
|
||||
|
||||
updateSync: (state) ->
|
||||
newState = state.content
|
||||
@oldState ?= {cursors: {}}
|
||||
|
||||
# update blink class
|
||||
if newState.cursorsVisible isnt @oldState.cursorsVisible
|
||||
if newState.cursorsVisible
|
||||
@domNode.classList.remove 'blink-off'
|
||||
else
|
||||
@domNode.classList.add 'blink-off'
|
||||
@oldState.cursorsVisible = newState.cursorsVisible
|
||||
|
||||
# remove cursors
|
||||
for id of @oldState.cursors
|
||||
unless newState.cursors[id]?
|
||||
@cursorNodesById[id].remove()
|
||||
delete @cursorNodesById[id]
|
||||
delete @oldState.cursors[id]
|
||||
|
||||
# add or update cursors
|
||||
for id, cursorState of newState.cursors
|
||||
unless @oldState.cursors[id]?
|
||||
cursorNode = document.createElement('div')
|
||||
cursorNode.classList.add('cursor')
|
||||
@cursorNodesById[id] = cursorNode
|
||||
@domNode.appendChild(cursorNode)
|
||||
@updateCursorNode(id, cursorState)
|
||||
|
||||
return
|
||||
|
||||
updateCursorNode: (id, newCursorState) ->
|
||||
cursorNode = @cursorNodesById[id]
|
||||
oldCursorState = (@oldState.cursors[id] ?= {})
|
||||
|
||||
if newCursorState.top isnt oldCursorState.top or newCursorState.left isnt oldCursorState.left
|
||||
cursorNode.style['-webkit-transform'] = "translate(#{newCursorState.left}px, #{newCursorState.top}px)"
|
||||
oldCursorState.top = newCursorState.top
|
||||
oldCursorState.left = newCursorState.left
|
||||
|
||||
if newCursorState.height isnt oldCursorState.height
|
||||
cursorNode.style.height = newCursorState.height + 'px'
|
||||
oldCursorState.height = newCursorState.height
|
||||
|
||||
if newCursorState.width isnt oldCursorState.width
|
||||
cursorNode.style.width = newCursorState.width + 'px'
|
||||
oldCursorState.width = newCursorState.width
|
||||
@@ -1,119 +0,0 @@
|
||||
# This class represents a gutter other than the 'line-numbers' gutter.
|
||||
# The contents of this gutter may be specified by Decorations.
|
||||
|
||||
module.exports =
|
||||
class CustomGutterComponent
|
||||
constructor: ({@gutter, @views}) ->
|
||||
@decorationNodesById = {}
|
||||
@decorationItemsById = {}
|
||||
@visible = true
|
||||
|
||||
@domNode = @gutter.getElement()
|
||||
@decorationsNode = @domNode.firstChild
|
||||
# Clear the contents in case the domNode is being reused.
|
||||
@decorationsNode.innerHTML = ''
|
||||
|
||||
getDomNode: ->
|
||||
@domNode
|
||||
|
||||
hideNode: ->
|
||||
if @visible
|
||||
@domNode.style.display = 'none'
|
||||
@visible = false
|
||||
|
||||
showNode: ->
|
||||
if not @visible
|
||||
@domNode.style.removeProperty('display')
|
||||
@visible = true
|
||||
|
||||
# `state` is a subset of the TextEditorPresenter state that is specific
|
||||
# to this line number gutter.
|
||||
updateSync: (state) ->
|
||||
@oldDimensionsAndBackgroundState ?= {}
|
||||
setDimensionsAndBackground(@oldDimensionsAndBackgroundState, state.styles, @decorationsNode)
|
||||
|
||||
@oldDecorationPositionState ?= {}
|
||||
decorationState = state.content
|
||||
|
||||
updatedDecorationIds = new Set
|
||||
for decorationId, decorationInfo of decorationState
|
||||
updatedDecorationIds.add(decorationId)
|
||||
existingDecoration = @decorationNodesById[decorationId]
|
||||
if existingDecoration
|
||||
@updateDecorationNode(existingDecoration, decorationId, decorationInfo)
|
||||
else
|
||||
newNode = @buildDecorationNode(decorationId, decorationInfo)
|
||||
@decorationNodesById[decorationId] = newNode
|
||||
@decorationsNode.appendChild(newNode)
|
||||
|
||||
for decorationId, decorationNode of @decorationNodesById
|
||||
if not updatedDecorationIds.has(decorationId)
|
||||
decorationNode.remove()
|
||||
delete @decorationNodesById[decorationId]
|
||||
delete @decorationItemsById[decorationId]
|
||||
delete @oldDecorationPositionState[decorationId]
|
||||
|
||||
###
|
||||
Section: Private Methods
|
||||
###
|
||||
|
||||
# Builds and returns an HTMLElement to represent the specified decoration.
|
||||
buildDecorationNode: (decorationId, decorationInfo) ->
|
||||
@oldDecorationPositionState[decorationId] = {}
|
||||
newNode = document.createElement('div')
|
||||
newNode.style.position = 'absolute'
|
||||
@updateDecorationNode(newNode, decorationId, decorationInfo)
|
||||
newNode
|
||||
|
||||
# Updates the existing HTMLNode with the new decoration info. Attempts to
|
||||
# minimize changes to the DOM.
|
||||
updateDecorationNode: (node, decorationId, newDecorationInfo) ->
|
||||
oldPositionState = @oldDecorationPositionState[decorationId]
|
||||
|
||||
if oldPositionState.top isnt newDecorationInfo.top + 'px'
|
||||
node.style.top = newDecorationInfo.top + 'px'
|
||||
oldPositionState.top = newDecorationInfo.top + 'px'
|
||||
|
||||
if oldPositionState.height isnt newDecorationInfo.height + 'px'
|
||||
node.style.height = newDecorationInfo.height + 'px'
|
||||
oldPositionState.height = newDecorationInfo.height + 'px'
|
||||
|
||||
if newDecorationInfo.class and not node.classList.contains(newDecorationInfo.class)
|
||||
node.className = 'decoration'
|
||||
node.classList.add(newDecorationInfo.class)
|
||||
else if not newDecorationInfo.class
|
||||
node.className = 'decoration'
|
||||
|
||||
@setDecorationItem(newDecorationInfo.item, newDecorationInfo.height, decorationId, node)
|
||||
|
||||
# Sets the decorationItem on the decorationNode.
|
||||
# If `decorationItem` is undefined, the decorationNode's child item will be cleared.
|
||||
setDecorationItem: (newItem, decorationHeight, decorationId, decorationNode) ->
|
||||
if newItem isnt @decorationItemsById[decorationId]
|
||||
while decorationNode.firstChild
|
||||
decorationNode.removeChild(decorationNode.firstChild)
|
||||
delete @decorationItemsById[decorationId]
|
||||
|
||||
if newItem
|
||||
newItemNode = null
|
||||
if newItem instanceof HTMLElement
|
||||
newItemNode = newItem
|
||||
else
|
||||
newItemNode = newItem.element
|
||||
|
||||
newItemNode.style.height = decorationHeight + 'px'
|
||||
decorationNode.appendChild(newItemNode)
|
||||
@decorationItemsById[decorationId] = newItem
|
||||
|
||||
setDimensionsAndBackground = (oldState, newState, domNode) ->
|
||||
if newState.scrollHeight isnt oldState.scrollHeight
|
||||
domNode.style.height = newState.scrollHeight + 'px'
|
||||
oldState.scrollHeight = newState.scrollHeight
|
||||
|
||||
if newState.scrollTop isnt oldState.scrollTop
|
||||
domNode.style['-webkit-transform'] = "translate3d(0px, #{-newState.scrollTop}px, 0px)"
|
||||
oldState.scrollTop = newState.scrollTop
|
||||
|
||||
if newState.backgroundColor isnt oldState.backgroundColor
|
||||
domNode.style.backgroundColor = newState.backgroundColor
|
||||
oldState.backgroundColor = newState.backgroundColor
|
||||
@@ -1,191 +0,0 @@
|
||||
{Emitter} = require 'event-kit'
|
||||
Model = require './model'
|
||||
Decoration = require './decoration'
|
||||
LayerDecoration = require './layer-decoration'
|
||||
|
||||
module.exports =
|
||||
class DecorationManager extends Model
|
||||
didUpdateDecorationsEventScheduled: false
|
||||
updatedSynchronously: false
|
||||
|
||||
constructor: (@displayLayer) ->
|
||||
super
|
||||
|
||||
@emitter = new Emitter
|
||||
@decorationsById = {}
|
||||
@decorationsByMarkerId = {}
|
||||
@overlayDecorationsById = {}
|
||||
@layerDecorationsByMarkerLayerId = {}
|
||||
@decorationCountsByLayerId = {}
|
||||
@layerUpdateDisposablesByLayerId = {}
|
||||
|
||||
observeDecorations: (callback) ->
|
||||
callback(decoration) for decoration in @getDecorations()
|
||||
@onDidAddDecoration(callback)
|
||||
|
||||
onDidAddDecoration: (callback) ->
|
||||
@emitter.on 'did-add-decoration', callback
|
||||
|
||||
onDidRemoveDecoration: (callback) ->
|
||||
@emitter.on 'did-remove-decoration', callback
|
||||
|
||||
onDidUpdateDecorations: (callback) ->
|
||||
@emitter.on 'did-update-decorations', callback
|
||||
|
||||
setUpdatedSynchronously: (@updatedSynchronously) ->
|
||||
|
||||
decorationForId: (id) ->
|
||||
@decorationsById[id]
|
||||
|
||||
getDecorations: (propertyFilter) ->
|
||||
allDecorations = []
|
||||
for markerId, decorations of @decorationsByMarkerId
|
||||
allDecorations.push(decorations...) if decorations?
|
||||
if propertyFilter?
|
||||
allDecorations = allDecorations.filter (decoration) ->
|
||||
for key, value of propertyFilter
|
||||
return false unless decoration.properties[key] is value
|
||||
true
|
||||
allDecorations
|
||||
|
||||
getLineDecorations: (propertyFilter) ->
|
||||
@getDecorations(propertyFilter).filter (decoration) -> decoration.isType('line')
|
||||
|
||||
getLineNumberDecorations: (propertyFilter) ->
|
||||
@getDecorations(propertyFilter).filter (decoration) -> decoration.isType('line-number')
|
||||
|
||||
getHighlightDecorations: (propertyFilter) ->
|
||||
@getDecorations(propertyFilter).filter (decoration) -> decoration.isType('highlight')
|
||||
|
||||
getOverlayDecorations: (propertyFilter) ->
|
||||
result = []
|
||||
for id, decoration of @overlayDecorationsById
|
||||
result.push(decoration)
|
||||
if propertyFilter?
|
||||
result.filter (decoration) ->
|
||||
for key, value of propertyFilter
|
||||
return false unless decoration.properties[key] is value
|
||||
true
|
||||
else
|
||||
result
|
||||
|
||||
decorationsForScreenRowRange: (startScreenRow, endScreenRow) ->
|
||||
decorationsByMarkerId = {}
|
||||
for layerId of @decorationCountsByLayerId
|
||||
layer = @displayLayer.getMarkerLayer(layerId)
|
||||
for marker in layer.findMarkers(intersectsScreenRowRange: [startScreenRow, endScreenRow])
|
||||
if decorations = @decorationsByMarkerId[marker.id]
|
||||
decorationsByMarkerId[marker.id] = decorations
|
||||
decorationsByMarkerId
|
||||
|
||||
decorationsStateForScreenRowRange: (startScreenRow, endScreenRow) ->
|
||||
decorationsState = {}
|
||||
|
||||
for layerId of @decorationCountsByLayerId
|
||||
layer = @displayLayer.getMarkerLayer(layerId)
|
||||
|
||||
for marker in layer.findMarkers(intersectsScreenRowRange: [startScreenRow, endScreenRow]) when marker.isValid()
|
||||
screenRange = marker.getScreenRange()
|
||||
bufferRange = marker.getBufferRange()
|
||||
rangeIsReversed = marker.isReversed()
|
||||
|
||||
if decorations = @decorationsByMarkerId[marker.id]
|
||||
for decoration in decorations
|
||||
decorationsState[decoration.id] = {
|
||||
properties: decoration.properties
|
||||
screenRange, bufferRange, rangeIsReversed
|
||||
}
|
||||
|
||||
if layerDecorations = @layerDecorationsByMarkerLayerId[layerId]
|
||||
for layerDecoration in layerDecorations
|
||||
decorationsState["#{layerDecoration.id}-#{marker.id}"] = {
|
||||
properties: layerDecoration.overridePropertiesByMarkerId[marker.id] ? layerDecoration.properties
|
||||
screenRange, bufferRange, rangeIsReversed
|
||||
}
|
||||
|
||||
decorationsState
|
||||
|
||||
decorateMarker: (marker, decorationParams) ->
|
||||
if marker.isDestroyed()
|
||||
error = new Error("Cannot decorate a destroyed marker")
|
||||
error.metadata = {markerLayerIsDestroyed: marker.layer.isDestroyed()}
|
||||
if marker.destroyStackTrace?
|
||||
error.metadata.destroyStackTrace = marker.destroyStackTrace
|
||||
if marker.bufferMarker?.destroyStackTrace?
|
||||
error.metadata.destroyStackTrace = marker.bufferMarker?.destroyStackTrace
|
||||
throw error
|
||||
marker = @displayLayer.getMarkerLayer(marker.layer.id).getMarker(marker.id)
|
||||
decoration = new Decoration(marker, this, decorationParams)
|
||||
@decorationsByMarkerId[marker.id] ?= []
|
||||
@decorationsByMarkerId[marker.id].push(decoration)
|
||||
@overlayDecorationsById[decoration.id] = decoration if decoration.isType('overlay')
|
||||
@decorationsById[decoration.id] = decoration
|
||||
@observeDecoratedLayer(marker.layer)
|
||||
@scheduleUpdateDecorationsEvent()
|
||||
@emitter.emit 'did-add-decoration', decoration
|
||||
decoration
|
||||
|
||||
decorateMarkerLayer: (markerLayer, decorationParams) ->
|
||||
throw new Error("Cannot decorate a destroyed marker layer") if markerLayer.isDestroyed()
|
||||
decoration = new LayerDecoration(markerLayer, this, decorationParams)
|
||||
@layerDecorationsByMarkerLayerId[markerLayer.id] ?= []
|
||||
@layerDecorationsByMarkerLayerId[markerLayer.id].push(decoration)
|
||||
@observeDecoratedLayer(markerLayer)
|
||||
@scheduleUpdateDecorationsEvent()
|
||||
decoration
|
||||
|
||||
decorationsForMarkerId: (markerId) ->
|
||||
@decorationsByMarkerId[markerId]
|
||||
|
||||
scheduleUpdateDecorationsEvent: ->
|
||||
if @updatedSynchronously
|
||||
@emitter.emit 'did-update-decorations'
|
||||
return
|
||||
|
||||
unless @didUpdateDecorationsEventScheduled
|
||||
@didUpdateDecorationsEventScheduled = true
|
||||
process.nextTick =>
|
||||
@didUpdateDecorationsEventScheduled = false
|
||||
@emitter.emit 'did-update-decorations'
|
||||
|
||||
decorationDidChangeType: (decoration) ->
|
||||
if decoration.isType('overlay')
|
||||
@overlayDecorationsById[decoration.id] = decoration
|
||||
else
|
||||
delete @overlayDecorationsById[decoration.id]
|
||||
|
||||
didDestroyMarkerDecoration: (decoration) ->
|
||||
{marker} = decoration
|
||||
return unless decorations = @decorationsByMarkerId[marker.id]
|
||||
index = decorations.indexOf(decoration)
|
||||
|
||||
if index > -1
|
||||
decorations.splice(index, 1)
|
||||
delete @decorationsById[decoration.id]
|
||||
@emitter.emit 'did-remove-decoration', decoration
|
||||
delete @decorationsByMarkerId[marker.id] if decorations.length is 0
|
||||
delete @overlayDecorationsById[decoration.id]
|
||||
@unobserveDecoratedLayer(marker.layer)
|
||||
@scheduleUpdateDecorationsEvent()
|
||||
|
||||
didDestroyLayerDecoration: (decoration) ->
|
||||
{markerLayer} = decoration
|
||||
return unless decorations = @layerDecorationsByMarkerLayerId[markerLayer.id]
|
||||
index = decorations.indexOf(decoration)
|
||||
|
||||
if index > -1
|
||||
decorations.splice(index, 1)
|
||||
delete @layerDecorationsByMarkerLayerId[markerLayer.id] if decorations.length is 0
|
||||
@unobserveDecoratedLayer(markerLayer)
|
||||
@scheduleUpdateDecorationsEvent()
|
||||
|
||||
observeDecoratedLayer: (layer) ->
|
||||
@decorationCountsByLayerId[layer.id] ?= 0
|
||||
if ++@decorationCountsByLayerId[layer.id] is 1
|
||||
@layerUpdateDisposablesByLayerId[layer.id] = layer.onDidUpdate(@scheduleUpdateDecorationsEvent.bind(this))
|
||||
|
||||
unobserveDecoratedLayer: (layer) ->
|
||||
if --@decorationCountsByLayerId[layer.id] is 0
|
||||
@layerUpdateDisposablesByLayerId[layer.id].dispose()
|
||||
delete @decorationCountsByLayerId[layer.id]
|
||||
delete @layerUpdateDisposablesByLayerId[layer.id]
|
||||
289
src/decoration-manager.js
Normal file
289
src/decoration-manager.js
Normal file
@@ -0,0 +1,289 @@
|
||||
const {Emitter} = require('event-kit')
|
||||
const Decoration = require('./decoration')
|
||||
const LayerDecoration = require('./layer-decoration')
|
||||
|
||||
module.exports =
|
||||
class DecorationManager {
|
||||
constructor (editor) {
|
||||
this.editor = editor
|
||||
this.displayLayer = this.editor.displayLayer
|
||||
|
||||
this.emitter = new Emitter()
|
||||
this.decorationCountsByLayer = new Map()
|
||||
this.markerDecorationCountsByLayer = new Map()
|
||||
this.decorationsByMarker = new Map()
|
||||
this.layerDecorationsByMarkerLayer = new Map()
|
||||
this.overlayDecorations = new Set()
|
||||
this.layerUpdateDisposablesByLayer = new WeakMap()
|
||||
}
|
||||
|
||||
observeDecorations (callback) {
|
||||
const decorations = this.getDecorations()
|
||||
for (let i = 0; i < decorations.length; i++) {
|
||||
callback(decorations[i])
|
||||
}
|
||||
return this.onDidAddDecoration(callback)
|
||||
}
|
||||
|
||||
onDidAddDecoration (callback) {
|
||||
return this.emitter.on('did-add-decoration', callback)
|
||||
}
|
||||
|
||||
onDidRemoveDecoration (callback) {
|
||||
return this.emitter.on('did-remove-decoration', callback)
|
||||
}
|
||||
|
||||
onDidUpdateDecorations (callback) {
|
||||
return this.emitter.on('did-update-decorations', callback)
|
||||
}
|
||||
|
||||
getDecorations (propertyFilter) {
|
||||
let allDecorations = []
|
||||
|
||||
this.decorationsByMarker.forEach((decorations) => {
|
||||
decorations.forEach((decoration) => allDecorations.push(decoration))
|
||||
})
|
||||
if (propertyFilter != null) {
|
||||
allDecorations = allDecorations.filter(function (decoration) {
|
||||
for (let key in propertyFilter) {
|
||||
const value = propertyFilter[key]
|
||||
if (decoration.properties[key] !== value) return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
return allDecorations
|
||||
}
|
||||
|
||||
getLineDecorations (propertyFilter) {
|
||||
return this.getDecorations(propertyFilter).filter(decoration => decoration.isType('line'))
|
||||
}
|
||||
|
||||
getLineNumberDecorations (propertyFilter) {
|
||||
return this.getDecorations(propertyFilter).filter(decoration => decoration.isType('line-number'))
|
||||
}
|
||||
|
||||
getHighlightDecorations (propertyFilter) {
|
||||
return this.getDecorations(propertyFilter).filter(decoration => decoration.isType('highlight'))
|
||||
}
|
||||
|
||||
getOverlayDecorations (propertyFilter) {
|
||||
const result = []
|
||||
result.push(...Array.from(this.overlayDecorations))
|
||||
if (propertyFilter != null) {
|
||||
return result.filter(function (decoration) {
|
||||
for (let key in propertyFilter) {
|
||||
const value = propertyFilter[key]
|
||||
if (decoration.properties[key] !== value) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
} else {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
decorationPropertiesByMarkerForScreenRowRange (startScreenRow, endScreenRow) {
|
||||
const decorationPropertiesByMarker = new Map()
|
||||
|
||||
this.decorationCountsByLayer.forEach((count, markerLayer) => {
|
||||
const markers = markerLayer.findMarkers({intersectsScreenRowRange: [startScreenRow, endScreenRow - 1]})
|
||||
const layerDecorations = this.layerDecorationsByMarkerLayer.get(markerLayer)
|
||||
const hasMarkerDecorations = this.markerDecorationCountsByLayer.get(markerLayer) > 0
|
||||
|
||||
for (let i = 0; i < markers.length; i++) {
|
||||
const marker = markers[i]
|
||||
if (!marker.isValid()) continue
|
||||
|
||||
let decorationPropertiesForMarker = decorationPropertiesByMarker.get(marker)
|
||||
if (decorationPropertiesForMarker == null) {
|
||||
decorationPropertiesForMarker = []
|
||||
decorationPropertiesByMarker.set(marker, decorationPropertiesForMarker)
|
||||
}
|
||||
|
||||
if (layerDecorations) {
|
||||
layerDecorations.forEach((layerDecoration) => {
|
||||
const properties = layerDecoration.getPropertiesForMarker(marker) || layerDecoration.getProperties()
|
||||
decorationPropertiesForMarker.push(properties)
|
||||
})
|
||||
}
|
||||
|
||||
if (hasMarkerDecorations) {
|
||||
const decorationsForMarker = this.decorationsByMarker.get(marker)
|
||||
if (decorationsForMarker) {
|
||||
decorationsForMarker.forEach((decoration) => {
|
||||
decorationPropertiesForMarker.push(decoration.getProperties())
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return decorationPropertiesByMarker
|
||||
}
|
||||
|
||||
decorationsForScreenRowRange (startScreenRow, endScreenRow) {
|
||||
const decorationsByMarkerId = {}
|
||||
for (const layer of this.decorationCountsByLayer.keys()) {
|
||||
for (const marker of layer.findMarkers({intersectsScreenRowRange: [startScreenRow, endScreenRow]})) {
|
||||
const decorations = this.decorationsByMarker.get(marker)
|
||||
if (decorations) {
|
||||
decorationsByMarkerId[marker.id] = Array.from(decorations)
|
||||
}
|
||||
}
|
||||
}
|
||||
return decorationsByMarkerId
|
||||
}
|
||||
|
||||
decorationsStateForScreenRowRange (startScreenRow, endScreenRow) {
|
||||
const decorationsState = {}
|
||||
|
||||
for (const layer of this.decorationCountsByLayer.keys()) {
|
||||
for (const marker of layer.findMarkers({intersectsScreenRowRange: [startScreenRow, endScreenRow]})) {
|
||||
if (marker.isValid()) {
|
||||
const screenRange = marker.getScreenRange()
|
||||
const bufferRange = marker.getBufferRange()
|
||||
const rangeIsReversed = marker.isReversed()
|
||||
|
||||
const decorations = this.decorationsByMarker.get(marker)
|
||||
if (decorations) {
|
||||
decorations.forEach((decoration) => {
|
||||
decorationsState[decoration.id] = {
|
||||
properties: decoration.properties,
|
||||
screenRange,
|
||||
bufferRange,
|
||||
rangeIsReversed
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const layerDecorations = this.layerDecorationsByMarkerLayer.get(layer)
|
||||
if (layerDecorations) {
|
||||
layerDecorations.forEach((layerDecoration) => {
|
||||
const properties = layerDecoration.getPropertiesForMarker(marker) || layerDecoration.getProperties()
|
||||
decorationsState[`${layerDecoration.id}-${marker.id}`] = {
|
||||
properties,
|
||||
screenRange,
|
||||
bufferRange,
|
||||
rangeIsReversed
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return decorationsState
|
||||
}
|
||||
|
||||
decorateMarker (marker, decorationParams) {
|
||||
if (marker.isDestroyed()) {
|
||||
const error = new Error('Cannot decorate a destroyed marker')
|
||||
error.metadata = {markerLayerIsDestroyed: marker.layer.isDestroyed()}
|
||||
if (marker.destroyStackTrace != null) {
|
||||
error.metadata.destroyStackTrace = marker.destroyStackTrace
|
||||
}
|
||||
if (marker.bufferMarker != null && marker.bufferMarker.destroyStackTrace != null) {
|
||||
error.metadata.destroyStackTrace = marker.bufferMarker.destroyStackTrace
|
||||
}
|
||||
throw error
|
||||
}
|
||||
marker = this.displayLayer.getMarkerLayer(marker.layer.id).getMarker(marker.id)
|
||||
const decoration = new Decoration(marker, this, decorationParams)
|
||||
let decorationsForMarker = this.decorationsByMarker.get(marker)
|
||||
if (!decorationsForMarker) {
|
||||
decorationsForMarker = new Set()
|
||||
this.decorationsByMarker.set(marker, decorationsForMarker)
|
||||
}
|
||||
decorationsForMarker.add(decoration)
|
||||
if (decoration.isType('overlay')) this.overlayDecorations.add(decoration)
|
||||
this.observeDecoratedLayer(marker.layer, true)
|
||||
this.editor.didAddDecoration(decoration)
|
||||
this.emitDidUpdateDecorations()
|
||||
this.emitter.emit('did-add-decoration', decoration)
|
||||
return decoration
|
||||
}
|
||||
|
||||
decorateMarkerLayer (markerLayer, decorationParams) {
|
||||
if (markerLayer.isDestroyed()) {
|
||||
throw new Error('Cannot decorate a destroyed marker layer')
|
||||
}
|
||||
markerLayer = this.displayLayer.getMarkerLayer(markerLayer.id)
|
||||
const decoration = new LayerDecoration(markerLayer, this, decorationParams)
|
||||
let layerDecorations = this.layerDecorationsByMarkerLayer.get(markerLayer)
|
||||
if (layerDecorations == null) {
|
||||
layerDecorations = new Set()
|
||||
this.layerDecorationsByMarkerLayer.set(markerLayer, layerDecorations)
|
||||
}
|
||||
layerDecorations.add(decoration)
|
||||
this.observeDecoratedLayer(markerLayer, false)
|
||||
this.emitDidUpdateDecorations()
|
||||
return decoration
|
||||
}
|
||||
|
||||
emitDidUpdateDecorations () {
|
||||
this.editor.scheduleComponentUpdate()
|
||||
this.emitter.emit('did-update-decorations')
|
||||
}
|
||||
|
||||
decorationDidChangeType (decoration) {
|
||||
if (decoration.isType('overlay')) {
|
||||
this.overlayDecorations.add(decoration)
|
||||
} else {
|
||||
this.overlayDecorations.delete(decoration)
|
||||
}
|
||||
}
|
||||
|
||||
didDestroyMarkerDecoration (decoration) {
|
||||
const {marker} = decoration
|
||||
const decorations = this.decorationsByMarker.get(marker)
|
||||
if (decorations && decorations.has(decoration)) {
|
||||
decorations.delete(decoration)
|
||||
if (decorations.size === 0) this.decorationsByMarker.delete(marker)
|
||||
this.overlayDecorations.delete(decoration)
|
||||
this.unobserveDecoratedLayer(marker.layer, true)
|
||||
this.emitter.emit('did-remove-decoration', decoration)
|
||||
this.emitDidUpdateDecorations()
|
||||
}
|
||||
}
|
||||
|
||||
didDestroyLayerDecoration (decoration) {
|
||||
const {markerLayer} = decoration
|
||||
const decorations = this.layerDecorationsByMarkerLayer.get(markerLayer)
|
||||
|
||||
if (decorations && decorations.has(decoration)) {
|
||||
decorations.delete(decoration)
|
||||
if (decorations.size === 0) {
|
||||
this.layerDecorationsByMarkerLayer.delete(markerLayer)
|
||||
}
|
||||
this.unobserveDecoratedLayer(markerLayer, true)
|
||||
this.emitDidUpdateDecorations()
|
||||
}
|
||||
}
|
||||
|
||||
observeDecoratedLayer (layer, isMarkerDecoration) {
|
||||
const newCount = (this.decorationCountsByLayer.get(layer) || 0) + 1
|
||||
this.decorationCountsByLayer.set(layer, newCount)
|
||||
if (newCount === 1) {
|
||||
this.layerUpdateDisposablesByLayer.set(layer, layer.onDidUpdate(this.emitDidUpdateDecorations.bind(this)))
|
||||
}
|
||||
if (isMarkerDecoration) {
|
||||
this.markerDecorationCountsByLayer.set(layer, (this.markerDecorationCountsByLayer.get(layer) || 0) + 1)
|
||||
}
|
||||
}
|
||||
|
||||
unobserveDecoratedLayer (layer, isMarkerDecoration) {
|
||||
const newCount = this.decorationCountsByLayer.get(layer) - 1
|
||||
if (newCount === 0) {
|
||||
this.layerUpdateDisposablesByLayer.get(layer).dispose()
|
||||
this.decorationCountsByLayer.delete(layer)
|
||||
} else {
|
||||
this.decorationCountsByLayer.set(layer, newCount)
|
||||
}
|
||||
if (isMarkerDecoration) {
|
||||
this.markerDecorationCountsByLayer.set(this.markerDecorationCountsByLayer.get(layer) - 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -150,7 +150,7 @@ class Decoration
|
||||
@properties = translateDecorationParamsOldToNew(newProperties)
|
||||
if newProperties.type?
|
||||
@decorationManager.decorationDidChangeType(this)
|
||||
@decorationManager.scheduleUpdateDecorationsEvent()
|
||||
@decorationManager.emitDidUpdateDecorations()
|
||||
@emitter.emit 'did-change-properties', {oldProperties, newProperties}
|
||||
|
||||
###
|
||||
@@ -171,9 +171,8 @@ class Decoration
|
||||
true
|
||||
|
||||
flash: (klass, duration=500) ->
|
||||
@properties.flashCount ?= 0
|
||||
@properties.flashCount++
|
||||
@properties.flashRequested = true
|
||||
@properties.flashClass = klass
|
||||
@properties.flashDuration = duration
|
||||
@decorationManager.scheduleUpdateDecorationsEvent()
|
||||
@decorationManager.emitDidUpdateDecorations()
|
||||
@emitter.emit 'did-flash'
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
module.exports =
|
||||
class DOMElementPool {
|
||||
constructor () {
|
||||
this.managedElements = new Set()
|
||||
this.freeElementsByTagName = new Map()
|
||||
this.freedElements = new Set()
|
||||
}
|
||||
|
||||
clear () {
|
||||
this.managedElements.clear()
|
||||
this.freedElements.clear()
|
||||
this.freeElementsByTagName.clear()
|
||||
}
|
||||
|
||||
buildElement (tagName, className) {
|
||||
const elements = this.freeElementsByTagName.get(tagName)
|
||||
let element = elements ? elements.pop() : null
|
||||
if (element) {
|
||||
for (let dataId in element.dataset) { delete element.dataset[dataId] }
|
||||
element.removeAttribute('style')
|
||||
if (className) {
|
||||
element.className = className
|
||||
} else {
|
||||
element.removeAttribute('class')
|
||||
}
|
||||
while (element.firstChild) {
|
||||
element.removeChild(element.firstChild)
|
||||
}
|
||||
this.freedElements.delete(element)
|
||||
} else {
|
||||
element = document.createElement(tagName)
|
||||
if (className) {
|
||||
element.className = className
|
||||
}
|
||||
this.managedElements.add(element)
|
||||
}
|
||||
return element
|
||||
}
|
||||
|
||||
buildText (textContent) {
|
||||
const elements = this.freeElementsByTagName.get('#text')
|
||||
let element = elements ? elements.pop() : null
|
||||
if (element) {
|
||||
element.textContent = textContent
|
||||
this.freedElements.delete(element)
|
||||
} else {
|
||||
element = document.createTextNode(textContent)
|
||||
this.managedElements.add(element)
|
||||
}
|
||||
return element
|
||||
}
|
||||
|
||||
freeElementAndDescendants (element) {
|
||||
this.free(element)
|
||||
element.remove()
|
||||
}
|
||||
|
||||
freeDescendants (element) {
|
||||
while (element.firstChild) {
|
||||
this.free(element.firstChild)
|
||||
element.removeChild(element.firstChild)
|
||||
}
|
||||
}
|
||||
|
||||
free (element) {
|
||||
if (element == null) { throw new Error('The element cannot be null or undefined.') }
|
||||
if (!this.managedElements.has(element)) return
|
||||
if (this.freedElements.has(element)) {
|
||||
atom.assert(false, 'The element has already been freed!', {
|
||||
content: element instanceof window.Text ? element.textContent : element.outerHTML
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const tagName = element.nodeName.toLowerCase()
|
||||
let elements = this.freeElementsByTagName.get(tagName)
|
||||
if (!elements) {
|
||||
elements = []
|
||||
this.freeElementsByTagName.set(tagName, elements)
|
||||
}
|
||||
elements.push(element)
|
||||
this.freedElements.add(element)
|
||||
|
||||
for (let i = element.childNodes.length - 1; i >= 0; i--) {
|
||||
const descendant = element.childNodes[i]
|
||||
this.free(descendant)
|
||||
}
|
||||
}
|
||||
}
|
||||
11
src/first-mate-helpers.js
Normal file
11
src/first-mate-helpers.js
Normal file
@@ -0,0 +1,11 @@
|
||||
module.exports = {
|
||||
fromFirstMateScopeId (firstMateScopeId) {
|
||||
let atomScopeId = -firstMateScopeId
|
||||
if ((atomScopeId & 1) === 0) atomScopeId--
|
||||
return atomScopeId + 256
|
||||
},
|
||||
|
||||
toFirstMateScopeId (atomScopeId) {
|
||||
return -(atomScopeId - 256)
|
||||
}
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
_ = require 'underscore-plus'
|
||||
CustomGutterComponent = require './custom-gutter-component'
|
||||
LineNumberGutterComponent = require './line-number-gutter-component'
|
||||
|
||||
# The GutterContainerComponent manages the GutterComponents of a particular
|
||||
# TextEditorComponent.
|
||||
|
||||
module.exports =
|
||||
class GutterContainerComponent
|
||||
constructor: ({@onLineNumberGutterMouseDown, @editor, @domElementPool, @views}) ->
|
||||
# An array of objects of the form: {name: {String}, component: {Object}}
|
||||
@gutterComponents = []
|
||||
@gutterComponentsByGutterName = {}
|
||||
@lineNumberGutterComponent = null
|
||||
|
||||
@domNode = document.createElement('div')
|
||||
@domNode.classList.add('gutter-container')
|
||||
@domNode.style.display = 'flex'
|
||||
|
||||
destroy: ->
|
||||
for {component} in @gutterComponents
|
||||
component.destroy?()
|
||||
return
|
||||
|
||||
getDomNode: ->
|
||||
@domNode
|
||||
|
||||
getLineNumberGutterComponent: ->
|
||||
@lineNumberGutterComponent
|
||||
|
||||
updateSync: (state) ->
|
||||
# The GutterContainerComponent expects the gutters to be sorted in the order
|
||||
# they should appear.
|
||||
newState = state.gutters
|
||||
|
||||
newGutterComponents = []
|
||||
newGutterComponentsByGutterName = {}
|
||||
for {gutter, visible, styles, content} in newState
|
||||
gutterComponent = @gutterComponentsByGutterName[gutter.name]
|
||||
if not gutterComponent
|
||||
if gutter.name is 'line-number'
|
||||
gutterComponent = new LineNumberGutterComponent({onMouseDown: @onLineNumberGutterMouseDown, @editor, gutter, @domElementPool, @views})
|
||||
@lineNumberGutterComponent = gutterComponent
|
||||
else
|
||||
gutterComponent = new CustomGutterComponent({gutter, @views})
|
||||
|
||||
if visible then gutterComponent.showNode() else gutterComponent.hideNode()
|
||||
# Pass the gutter only the state that it needs.
|
||||
if gutter.name is 'line-number'
|
||||
# For ease of use in the line number gutter component, set the shared
|
||||
# 'styles' as a field under the 'content'.
|
||||
gutterSubstate = _.clone(content)
|
||||
gutterSubstate.styles = styles
|
||||
else
|
||||
# Custom gutter 'content' is keyed on gutter name, so we cannot set
|
||||
# 'styles' as a subfield directly under it.
|
||||
gutterSubstate = {content, styles}
|
||||
gutterComponent.updateSync(gutterSubstate)
|
||||
|
||||
newGutterComponents.push({
|
||||
name: gutter.name,
|
||||
component: gutterComponent,
|
||||
})
|
||||
newGutterComponentsByGutterName[gutter.name] = gutterComponent
|
||||
|
||||
@reorderGutters(newGutterComponents, newGutterComponentsByGutterName)
|
||||
|
||||
@gutterComponents = newGutterComponents
|
||||
@gutterComponentsByGutterName = newGutterComponentsByGutterName
|
||||
|
||||
###
|
||||
Section: Private Methods
|
||||
###
|
||||
|
||||
reorderGutters: (newGutterComponents, newGutterComponentsByGutterName) ->
|
||||
# First, insert new gutters into the DOM.
|
||||
indexInOldGutters = 0
|
||||
oldGuttersLength = @gutterComponents.length
|
||||
|
||||
for gutterComponentDescription in newGutterComponents
|
||||
gutterComponent = gutterComponentDescription.component
|
||||
gutterName = gutterComponentDescription.name
|
||||
|
||||
if @gutterComponentsByGutterName[gutterName]
|
||||
# If the gutter existed previously, we first try to move the cursor to
|
||||
# the point at which it occurs in the previous gutters.
|
||||
matchingGutterFound = false
|
||||
while indexInOldGutters < oldGuttersLength
|
||||
existingGutterComponentDescription = @gutterComponents[indexInOldGutters]
|
||||
existingGutterComponent = existingGutterComponentDescription.component
|
||||
indexInOldGutters++
|
||||
if existingGutterComponent is gutterComponent
|
||||
matchingGutterFound = true
|
||||
break
|
||||
if not matchingGutterFound
|
||||
# If we've reached this point, the gutter previously existed, but its
|
||||
# position has moved. Remove it from the DOM and re-insert it.
|
||||
gutterComponent.getDomNode().remove()
|
||||
@domNode.appendChild(gutterComponent.getDomNode())
|
||||
|
||||
else
|
||||
if indexInOldGutters is oldGuttersLength
|
||||
@domNode.appendChild(gutterComponent.getDomNode())
|
||||
else
|
||||
@domNode.insertBefore(gutterComponent.getDomNode(), @domNode.children[indexInOldGutters])
|
||||
indexInOldGutters += 1
|
||||
|
||||
# Remove any gutters that were not present in the new gutters state.
|
||||
for gutterComponentDescription in @gutterComponents
|
||||
if not newGutterComponentsByGutterName[gutterComponentDescription.name]
|
||||
gutterComponent = gutterComponentDescription.component
|
||||
gutterComponent.getDomNode().remove()
|
||||
@@ -8,6 +8,9 @@ class GutterContainer
|
||||
@textEditor = textEditor
|
||||
@emitter = new Emitter
|
||||
|
||||
scheduleComponentUpdate: ->
|
||||
@textEditor.scheduleComponentUpdate()
|
||||
|
||||
destroy: ->
|
||||
# Create a copy, because `Gutter::destroy` removes the gutter from
|
||||
# GutterContainer's @gutters.
|
||||
@@ -36,6 +39,7 @@ class GutterContainer
|
||||
break
|
||||
if not inserted
|
||||
@gutters.push newGutter
|
||||
@scheduleComponentUpdate()
|
||||
@emitter.emit 'did-add-gutter', newGutter
|
||||
return newGutter
|
||||
|
||||
@@ -67,6 +71,7 @@ class GutterContainer
|
||||
index = @gutters.indexOf(gutter)
|
||||
if index > -1
|
||||
@gutters.splice(index, 1)
|
||||
@scheduleComponentUpdate()
|
||||
@emitter.emit 'did-remove-gutter', gutter.name
|
||||
else
|
||||
throw new Error 'The given gutter cannot be removed because it is not ' +
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{Emitter} = require 'event-kit'
|
||||
CustomGutterComponent = null
|
||||
|
||||
DefaultPriority = -100
|
||||
|
||||
@@ -28,19 +29,6 @@ class Gutter
|
||||
@emitter.emit 'did-destroy'
|
||||
@emitter.dispose()
|
||||
|
||||
getElement: ->
|
||||
unless @element?
|
||||
@element = document.createElement('div')
|
||||
@element.classList.add('gutter')
|
||||
@element.setAttribute('gutter-name', @name)
|
||||
childNode = document.createElement('div')
|
||||
if @name is 'line-number'
|
||||
childNode.classList.add('line-numbers')
|
||||
else
|
||||
childNode.classList.add('custom-decorations')
|
||||
@element.appendChild(childNode)
|
||||
@element
|
||||
|
||||
###
|
||||
Section: Event Subscription
|
||||
###
|
||||
@@ -70,12 +58,14 @@ class Gutter
|
||||
hide: ->
|
||||
if @visible
|
||||
@visible = false
|
||||
@gutterContainer.scheduleComponentUpdate()
|
||||
@emitter.emit 'did-change-visible', this
|
||||
|
||||
# Essential: Show the gutter.
|
||||
show: ->
|
||||
if not @visible
|
||||
@visible = true
|
||||
@gutterContainer.scheduleComponentUpdate()
|
||||
@emitter.emit 'did-change-visible', this
|
||||
|
||||
# Essential: Determine whether the gutter is visible.
|
||||
@@ -100,3 +90,6 @@ class Gutter
|
||||
# Returns a {Decoration} object
|
||||
decorateMarker: (marker, options) ->
|
||||
@gutterContainer.addGutterDecoration(this, marker, options)
|
||||
|
||||
getElement: ->
|
||||
@element ?= document.createElement('div')
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
RegionStyleProperties = ['top', 'left', 'right', 'width', 'height']
|
||||
SpaceRegex = /\s+/
|
||||
|
||||
module.exports =
|
||||
class HighlightsComponent
|
||||
oldState: null
|
||||
|
||||
constructor: (@domElementPool) ->
|
||||
@highlightNodesById = {}
|
||||
@regionNodesByHighlightId = {}
|
||||
|
||||
@domNode = @domElementPool.buildElement("div", "highlights")
|
||||
|
||||
getDomNode: ->
|
||||
@domNode
|
||||
|
||||
updateSync: (state) ->
|
||||
newState = state.highlights
|
||||
@oldState ?= {}
|
||||
|
||||
# remove highlights
|
||||
for id of @oldState
|
||||
unless newState[id]?
|
||||
@domElementPool.freeElementAndDescendants(@highlightNodesById[id])
|
||||
delete @highlightNodesById[id]
|
||||
delete @regionNodesByHighlightId[id]
|
||||
delete @oldState[id]
|
||||
|
||||
# add or update highlights
|
||||
for id, highlightState of newState
|
||||
unless @oldState[id]?
|
||||
highlightNode = @domElementPool.buildElement("div", "highlight")
|
||||
@highlightNodesById[id] = highlightNode
|
||||
@regionNodesByHighlightId[id] = {}
|
||||
@domNode.appendChild(highlightNode)
|
||||
@updateHighlightNode(id, highlightState)
|
||||
|
||||
return
|
||||
|
||||
updateHighlightNode: (id, newHighlightState) ->
|
||||
highlightNode = @highlightNodesById[id]
|
||||
oldHighlightState = (@oldState[id] ?= {regions: [], flashCount: 0})
|
||||
|
||||
# update class
|
||||
if newHighlightState.class isnt oldHighlightState.class
|
||||
if oldHighlightState.class?
|
||||
if SpaceRegex.test(oldHighlightState.class)
|
||||
highlightNode.classList.remove(oldHighlightState.class.split(SpaceRegex)...)
|
||||
else
|
||||
highlightNode.classList.remove(oldHighlightState.class)
|
||||
|
||||
if SpaceRegex.test(newHighlightState.class)
|
||||
highlightNode.classList.add(newHighlightState.class.split(SpaceRegex)...)
|
||||
else
|
||||
highlightNode.classList.add(newHighlightState.class)
|
||||
|
||||
oldHighlightState.class = newHighlightState.class
|
||||
|
||||
@updateHighlightRegions(id, newHighlightState)
|
||||
@flashHighlightNodeIfRequested(id, newHighlightState)
|
||||
|
||||
updateHighlightRegions: (id, newHighlightState) ->
|
||||
oldHighlightState = @oldState[id]
|
||||
highlightNode = @highlightNodesById[id]
|
||||
|
||||
# remove regions
|
||||
while oldHighlightState.regions.length > newHighlightState.regions.length
|
||||
oldHighlightState.regions.pop()
|
||||
@domElementPool.freeElementAndDescendants(@regionNodesByHighlightId[id][oldHighlightState.regions.length])
|
||||
delete @regionNodesByHighlightId[id][oldHighlightState.regions.length]
|
||||
|
||||
# add or update regions
|
||||
for newRegionState, i in newHighlightState.regions
|
||||
unless oldHighlightState.regions[i]?
|
||||
oldHighlightState.regions[i] = {}
|
||||
regionNode = @domElementPool.buildElement("div", "region")
|
||||
# This prevents highlights at the tiles boundaries to be hidden by the
|
||||
# subsequent tile. When this happens, subpixel anti-aliasing gets
|
||||
# disabled.
|
||||
regionNode.style.boxSizing = "border-box"
|
||||
regionNode.classList.add(newHighlightState.deprecatedRegionClass) if newHighlightState.deprecatedRegionClass?
|
||||
@regionNodesByHighlightId[id][i] = regionNode
|
||||
highlightNode.appendChild(regionNode)
|
||||
|
||||
oldRegionState = oldHighlightState.regions[i]
|
||||
regionNode = @regionNodesByHighlightId[id][i]
|
||||
|
||||
for property in RegionStyleProperties
|
||||
if newRegionState[property] isnt oldRegionState[property]
|
||||
oldRegionState[property] = newRegionState[property]
|
||||
if newRegionState[property]?
|
||||
regionNode.style[property] = newRegionState[property] + 'px'
|
||||
else
|
||||
regionNode.style[property] = ''
|
||||
|
||||
return
|
||||
|
||||
flashHighlightNodeIfRequested: (id, newHighlightState) ->
|
||||
oldHighlightState = @oldState[id]
|
||||
if newHighlightState.needsFlash and oldHighlightState.flashCount isnt newHighlightState.flashCount
|
||||
highlightNode = @highlightNodesById[id]
|
||||
|
||||
addFlashClass = =>
|
||||
highlightNode.classList.add(newHighlightState.flashClass)
|
||||
oldHighlightState.flashClass = newHighlightState.flashClass
|
||||
@flashTimeoutId = setTimeout(removeFlashClass, newHighlightState.flashDuration)
|
||||
|
||||
removeFlashClass = =>
|
||||
highlightNode.classList.remove(oldHighlightState.flashClass)
|
||||
oldHighlightState.flashClass = null
|
||||
clearTimeout(@flashTimeoutId)
|
||||
|
||||
if oldHighlightState.flashClass?
|
||||
removeFlashClass()
|
||||
requestAnimationFrame(addFlashClass)
|
||||
else
|
||||
addFlashClass()
|
||||
|
||||
oldHighlightState.flashCount = newHighlightState.flashCount
|
||||
@@ -17,10 +17,6 @@ export class HistoryManager {
|
||||
this.disposables.add(project.onDidChangePaths((projectPaths) => this.addProject(projectPaths)))
|
||||
}
|
||||
|
||||
initialize (localStorage) {
|
||||
this.localStorage = localStorage
|
||||
}
|
||||
|
||||
destroy () {
|
||||
this.disposables.dispose()
|
||||
}
|
||||
@@ -98,10 +94,6 @@ export class HistoryManager {
|
||||
|
||||
async loadState () {
|
||||
let history = await this.stateStore.load('history-manager')
|
||||
if (!history) {
|
||||
history = JSON.parse(this.localStorage.getItem('history'))
|
||||
}
|
||||
|
||||
if (history && history.projects) {
|
||||
this.projects = history.projects.filter(p => Array.isArray(p.paths) && p.paths.length > 0).map(p => new HistoryProject(p.paths, new Date(p.lastOpened)))
|
||||
this.didChangeProjects({reloaded: true})
|
||||
|
||||
@@ -59,6 +59,7 @@ if global.isGeneratingSnapshot
|
||||
|
||||
clipboard = new Clipboard
|
||||
TextEditor.setClipboard(clipboard)
|
||||
TextEditor.viewForItem = (item) -> atom.views.getView(item)
|
||||
|
||||
global.atom = new AtomEnvironment({
|
||||
clipboard,
|
||||
|
||||
@@ -54,6 +54,7 @@ export default async function () {
|
||||
|
||||
const clipboard = new Clipboard()
|
||||
TextEditor.setClipboard(clipboard)
|
||||
TextEditor.viewForItem = (item) => atom.views.getView(item)
|
||||
|
||||
const applicationDelegate = new ApplicationDelegate()
|
||||
const environmentParams = {
|
||||
|
||||
@@ -70,6 +70,7 @@ module.exports = ({blobStore}) ->
|
||||
|
||||
clipboard = new Clipboard
|
||||
TextEditor.setClipboard(clipboard)
|
||||
TextEditor.viewForItem = (item) -> atom.views.getView(item)
|
||||
|
||||
testRunner = require(testRunnerPath)
|
||||
legacyTestRunner = require(legacyTestRunnerPath)
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
module.exports =
|
||||
class InputComponent
|
||||
constructor: (@domNode) ->
|
||||
|
||||
updateSync: (state) ->
|
||||
@oldState ?= {}
|
||||
newState = state.hiddenInput
|
||||
|
||||
if newState.top isnt @oldState.top
|
||||
@domNode.style.top = newState.top + 'px'
|
||||
@oldState.top = newState.top
|
||||
|
||||
if newState.left isnt @oldState.left
|
||||
@domNode.style.left = newState.left + 'px'
|
||||
@oldState.left = newState.left
|
||||
|
||||
if newState.width isnt @oldState.width
|
||||
@domNode.style.width = newState.width + 'px'
|
||||
@oldState.width = newState.width
|
||||
|
||||
if newState.height isnt @oldState.height
|
||||
@domNode.style.height = newState.height + 'px'
|
||||
@oldState.height = newState.height
|
||||
@@ -189,7 +189,7 @@ class LanguageMode
|
||||
# row is a comment.
|
||||
isLineCommentedAtBufferRow: (bufferRow) ->
|
||||
return false unless 0 <= bufferRow <= @editor.getLastBufferRow()
|
||||
@editor.tokenizedBuffer.tokenizedLines[bufferRow]?.isComment()
|
||||
@editor.tokenizedBuffer.tokenizedLines[bufferRow]?.isComment() ? false
|
||||
|
||||
# Find a row range for a 'paragraph' around specified bufferRow. A paragraph
|
||||
# is a block of text bounded by and empty line or a block of text that is not
|
||||
|
||||
@@ -9,7 +9,7 @@ class LayerDecoration
|
||||
@id = nextId()
|
||||
@destroyed = false
|
||||
@markerLayerDestroyedDisposable = @markerLayer.onDidDestroy => @destroy()
|
||||
@overridePropertiesByMarkerId = {}
|
||||
@overridePropertiesByMarker = null
|
||||
|
||||
# Essential: Destroys the decoration.
|
||||
destroy: ->
|
||||
@@ -42,7 +42,7 @@ class LayerDecoration
|
||||
setProperties: (newProperties) ->
|
||||
return if @destroyed
|
||||
@properties = newProperties
|
||||
@decorationManager.scheduleUpdateDecorationsEvent()
|
||||
@decorationManager.emitDidUpdateDecorations()
|
||||
|
||||
# Essential: Override the decoration properties for a specific marker.
|
||||
#
|
||||
@@ -52,8 +52,13 @@ class LayerDecoration
|
||||
# Pass `null` to clear the override.
|
||||
setPropertiesForMarker: (marker, properties) ->
|
||||
return if @destroyed
|
||||
@overridePropertiesByMarker ?= new Map()
|
||||
marker = @markerLayer.getMarker(marker.id)
|
||||
if properties?
|
||||
@overridePropertiesByMarkerId[marker.id] = properties
|
||||
@overridePropertiesByMarker.set(marker, properties)
|
||||
else
|
||||
delete @overridePropertiesByMarkerId[marker.id]
|
||||
@decorationManager.scheduleUpdateDecorationsEvent()
|
||||
@overridePropertiesByMarker.delete(marker)
|
||||
@decorationManager.emitDidUpdateDecorations()
|
||||
|
||||
getPropertiesForMarker: (marker) ->
|
||||
@overridePropertiesByMarker?.get(marker)
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
TiledComponent = require './tiled-component'
|
||||
LineNumbersTileComponent = require './line-numbers-tile-component'
|
||||
|
||||
module.exports =
|
||||
class LineNumberGutterComponent extends TiledComponent
|
||||
dummyLineNumberNode: null
|
||||
|
||||
constructor: ({@onMouseDown, @editor, @gutter, @domElementPool, @views}) ->
|
||||
@visible = true
|
||||
|
||||
@dummyLineNumberComponent = LineNumbersTileComponent.createDummy(@domElementPool)
|
||||
|
||||
@domNode = @gutter.getElement()
|
||||
@lineNumbersNode = @domNode.firstChild
|
||||
@lineNumbersNode.innerHTML = ''
|
||||
|
||||
@domNode.addEventListener 'click', @onClick
|
||||
@domNode.addEventListener 'mousedown', @onMouseDown
|
||||
|
||||
destroy: ->
|
||||
@domNode.removeEventListener 'click', @onClick
|
||||
@domNode.removeEventListener 'mousedown', @onMouseDown
|
||||
|
||||
getDomNode: ->
|
||||
@domNode
|
||||
|
||||
hideNode: ->
|
||||
if @visible
|
||||
@domNode.style.display = 'none'
|
||||
@visible = false
|
||||
|
||||
showNode: ->
|
||||
if not @visible
|
||||
@domNode.style.removeProperty('display')
|
||||
@visible = true
|
||||
|
||||
buildEmptyState: ->
|
||||
{
|
||||
tiles: {}
|
||||
styles: {}
|
||||
}
|
||||
|
||||
getNewState: (state) -> state
|
||||
|
||||
getTilesNode: -> @lineNumbersNode
|
||||
|
||||
beforeUpdateSync: (state) ->
|
||||
@appendDummyLineNumber() unless @dummyLineNumberNode?
|
||||
|
||||
if @newState.styles.maxHeight isnt @oldState.styles.maxHeight
|
||||
@lineNumbersNode.style.height = @newState.styles.maxHeight + 'px'
|
||||
@oldState.maxHeight = @newState.maxHeight
|
||||
|
||||
if @newState.styles.backgroundColor isnt @oldState.styles.backgroundColor
|
||||
@lineNumbersNode.style.backgroundColor = @newState.styles.backgroundColor
|
||||
@oldState.styles.backgroundColor = @newState.styles.backgroundColor
|
||||
|
||||
if @newState.maxLineNumberDigits isnt @oldState.maxLineNumberDigits
|
||||
@updateDummyLineNumber()
|
||||
@oldState.styles = {}
|
||||
@oldState.maxLineNumberDigits = @newState.maxLineNumberDigits
|
||||
|
||||
buildComponentForTile: (id) -> new LineNumbersTileComponent({id, @domElementPool})
|
||||
|
||||
shouldRecreateAllTilesOnUpdate: ->
|
||||
@newState.continuousReflow
|
||||
|
||||
###
|
||||
Section: Private Methods
|
||||
###
|
||||
|
||||
# This dummy line number element holds the gutter to the appropriate width,
|
||||
# since the real line numbers are absolutely positioned for performance reasons.
|
||||
appendDummyLineNumber: ->
|
||||
@dummyLineNumberComponent.newState = @newState
|
||||
@dummyLineNumberNode = @dummyLineNumberComponent.buildLineNumberNode({bufferRow: -1})
|
||||
@lineNumbersNode.appendChild(@dummyLineNumberNode)
|
||||
|
||||
updateDummyLineNumber: ->
|
||||
@dummyLineNumberComponent.newState = @newState
|
||||
@dummyLineNumberComponent.setLineNumberInnerNodes(0, false, @dummyLineNumberNode)
|
||||
|
||||
onMouseDown: (event) =>
|
||||
{target} = event
|
||||
lineNumber = target.parentNode
|
||||
|
||||
unless target.classList.contains('icon-right') and lineNumber.classList.contains('foldable')
|
||||
@onMouseDown(event)
|
||||
|
||||
onClick: (event) =>
|
||||
{target} = event
|
||||
lineNumber = target.parentNode
|
||||
|
||||
if target.classList.contains('icon-right')
|
||||
bufferRow = parseInt(lineNumber.getAttribute('data-buffer-row'))
|
||||
if lineNumber.classList.contains('folded')
|
||||
@editor.unfoldBufferRow(bufferRow)
|
||||
else if lineNumber.classList.contains('foldable')
|
||||
@editor.foldBufferRow(bufferRow)
|
||||
@@ -1,157 +0,0 @@
|
||||
_ = require 'underscore-plus'
|
||||
|
||||
module.exports =
|
||||
class LineNumbersTileComponent
|
||||
@createDummy: (domElementPool) ->
|
||||
new LineNumbersTileComponent({id: -1, domElementPool})
|
||||
|
||||
constructor: ({@id, @domElementPool}) ->
|
||||
@lineNumberNodesById = {}
|
||||
@domNode = @domElementPool.buildElement("div")
|
||||
@domNode.style.position = "absolute"
|
||||
@domNode.style.display = "block"
|
||||
@domNode.style.top = 0 # Cover the space occupied by a dummy lineNumber
|
||||
|
||||
destroy: ->
|
||||
@domElementPool.freeElementAndDescendants(@domNode)
|
||||
|
||||
getDomNode: ->
|
||||
@domNode
|
||||
|
||||
updateSync: (state) ->
|
||||
@newState = state
|
||||
unless @oldState
|
||||
@oldState = {tiles: {}, styles: {}}
|
||||
@oldState.tiles[@id] = {lineNumbers: {}}
|
||||
|
||||
@newTileState = @newState.tiles[@id]
|
||||
@oldTileState = @oldState.tiles[@id]
|
||||
|
||||
if @newTileState.display isnt @oldTileState.display
|
||||
@domNode.style.display = @newTileState.display
|
||||
@oldTileState.display = @newTileState.display
|
||||
|
||||
if @newState.styles.backgroundColor isnt @oldState.styles.backgroundColor
|
||||
@domNode.style.backgroundColor = @newState.styles.backgroundColor
|
||||
@oldState.styles.backgroundColor = @newState.styles.backgroundColor
|
||||
|
||||
if @newTileState.height isnt @oldTileState.height
|
||||
@domNode.style.height = @newTileState.height + 'px'
|
||||
@oldTileState.height = @newTileState.height
|
||||
|
||||
if @newTileState.top isnt @oldTileState.top
|
||||
@domNode.style['-webkit-transform'] = "translate3d(0, #{@newTileState.top}px, 0px)"
|
||||
@oldTileState.top = @newTileState.top
|
||||
|
||||
if @newTileState.zIndex isnt @oldTileState.zIndex
|
||||
@domNode.style.zIndex = @newTileState.zIndex
|
||||
@oldTileState.zIndex = @newTileState.zIndex
|
||||
|
||||
if @newState.maxLineNumberDigits isnt @oldState.maxLineNumberDigits
|
||||
for id, node of @lineNumberNodesById
|
||||
@domElementPool.freeElementAndDescendants(node)
|
||||
|
||||
@oldState.tiles[@id] = {lineNumbers: {}}
|
||||
@oldTileState = @oldState.tiles[@id]
|
||||
@lineNumberNodesById = {}
|
||||
@oldState.maxLineNumberDigits = @newState.maxLineNumberDigits
|
||||
|
||||
@updateLineNumbers()
|
||||
|
||||
updateLineNumbers: ->
|
||||
newLineNumberIds = null
|
||||
newLineNumberNodes = null
|
||||
|
||||
for id, lineNumberState of @oldTileState.lineNumbers
|
||||
unless @newTileState.lineNumbers.hasOwnProperty(id)
|
||||
@domElementPool.freeElementAndDescendants(@lineNumberNodesById[id])
|
||||
delete @lineNumberNodesById[id]
|
||||
delete @oldTileState.lineNumbers[id]
|
||||
|
||||
for id, lineNumberState of @newTileState.lineNumbers
|
||||
if @oldTileState.lineNumbers.hasOwnProperty(id)
|
||||
@updateLineNumberNode(id, lineNumberState)
|
||||
else
|
||||
newLineNumberIds ?= []
|
||||
newLineNumberNodes ?= []
|
||||
newLineNumberIds.push(id)
|
||||
newLineNumberNodes.push(@buildLineNumberNode(lineNumberState))
|
||||
@oldTileState.lineNumbers[id] = _.clone(lineNumberState)
|
||||
|
||||
return unless newLineNumberIds?
|
||||
|
||||
for id, i in newLineNumberIds
|
||||
lineNumberNode = newLineNumberNodes[i]
|
||||
@lineNumberNodesById[id] = lineNumberNode
|
||||
if nextNode = @findNodeNextTo(lineNumberNode)
|
||||
@domNode.insertBefore(lineNumberNode, nextNode)
|
||||
else
|
||||
@domNode.appendChild(lineNumberNode)
|
||||
|
||||
findNodeNextTo: (node) ->
|
||||
for nextNode in @domNode.children
|
||||
return nextNode if @screenRowForNode(node) < @screenRowForNode(nextNode)
|
||||
return
|
||||
|
||||
screenRowForNode: (node) -> parseInt(node.dataset.screenRow)
|
||||
|
||||
buildLineNumberNode: (lineNumberState) ->
|
||||
{screenRow, bufferRow, softWrapped, blockDecorationsHeight} = lineNumberState
|
||||
|
||||
className = @buildLineNumberClassName(lineNumberState)
|
||||
lineNumberNode = @domElementPool.buildElement("div", className)
|
||||
lineNumberNode.dataset.screenRow = screenRow
|
||||
lineNumberNode.dataset.bufferRow = bufferRow
|
||||
lineNumberNode.style.marginTop = blockDecorationsHeight + "px"
|
||||
|
||||
@setLineNumberInnerNodes(bufferRow, softWrapped, lineNumberNode)
|
||||
lineNumberNode
|
||||
|
||||
setLineNumberInnerNodes: (bufferRow, softWrapped, lineNumberNode) ->
|
||||
@domElementPool.freeDescendants(lineNumberNode)
|
||||
|
||||
{maxLineNumberDigits} = @newState
|
||||
|
||||
if softWrapped
|
||||
lineNumber = "•"
|
||||
else
|
||||
lineNumber = (bufferRow + 1).toString()
|
||||
padding = _.multiplyString("\u00a0", maxLineNumberDigits - lineNumber.length)
|
||||
|
||||
textNode = @domElementPool.buildText(padding + lineNumber)
|
||||
iconRight = @domElementPool.buildElement("div", "icon-right")
|
||||
|
||||
lineNumberNode.appendChild(textNode)
|
||||
lineNumberNode.appendChild(iconRight)
|
||||
|
||||
updateLineNumberNode: (lineNumberId, newLineNumberState) ->
|
||||
oldLineNumberState = @oldTileState.lineNumbers[lineNumberId]
|
||||
node = @lineNumberNodesById[lineNumberId]
|
||||
|
||||
unless oldLineNumberState.foldable is newLineNumberState.foldable and _.isEqual(oldLineNumberState.decorationClasses, newLineNumberState.decorationClasses)
|
||||
node.className = @buildLineNumberClassName(newLineNumberState)
|
||||
oldLineNumberState.foldable = newLineNumberState.foldable
|
||||
oldLineNumberState.decorationClasses = _.clone(newLineNumberState.decorationClasses)
|
||||
|
||||
unless oldLineNumberState.screenRow is newLineNumberState.screenRow and oldLineNumberState.bufferRow is newLineNumberState.bufferRow
|
||||
@setLineNumberInnerNodes(newLineNumberState.bufferRow, newLineNumberState.softWrapped, node)
|
||||
node.dataset.screenRow = newLineNumberState.screenRow
|
||||
node.dataset.bufferRow = newLineNumberState.bufferRow
|
||||
oldLineNumberState.screenRow = newLineNumberState.screenRow
|
||||
oldLineNumberState.bufferRow = newLineNumberState.bufferRow
|
||||
|
||||
unless oldLineNumberState.blockDecorationsHeight is newLineNumberState.blockDecorationsHeight
|
||||
node.style.marginTop = newLineNumberState.blockDecorationsHeight + "px"
|
||||
oldLineNumberState.blockDecorationsHeight = newLineNumberState.blockDecorationsHeight
|
||||
|
||||
buildLineNumberClassName: ({bufferRow, foldable, decorationClasses, softWrapped}) ->
|
||||
className = "line-number"
|
||||
className += " " + decorationClasses.join(' ') if decorationClasses?
|
||||
className += " foldable" if foldable and not softWrapped
|
||||
className
|
||||
|
||||
lineNumberNodeForScreenRow: (screenRow) ->
|
||||
for id, lineNumberState of @oldTileState.lineNumbers
|
||||
if lineNumberState.screenRow is screenRow
|
||||
return @lineNumberNodesById[id]
|
||||
null
|
||||
@@ -1,109 +0,0 @@
|
||||
CursorsComponent = require './cursors-component'
|
||||
LinesTileComponent = require './lines-tile-component'
|
||||
TiledComponent = require './tiled-component'
|
||||
|
||||
module.exports =
|
||||
class LinesComponent extends TiledComponent
|
||||
placeholderTextDiv: null
|
||||
|
||||
constructor: ({@views, @presenter, @domElementPool, @assert}) ->
|
||||
@DummyLineNode = document.createElement('div')
|
||||
@DummyLineNode.className = 'line'
|
||||
@DummyLineNode.style.position = 'absolute'
|
||||
@DummyLineNode.style.visibility = 'hidden'
|
||||
@DummyLineNode.appendChild(document.createElement('span'))
|
||||
@DummyLineNode.appendChild(document.createElement('span'))
|
||||
@DummyLineNode.appendChild(document.createElement('span'))
|
||||
@DummyLineNode.appendChild(document.createElement('span'))
|
||||
@DummyLineNode.children[0].textContent = 'x'
|
||||
@DummyLineNode.children[1].textContent = '我'
|
||||
@DummyLineNode.children[2].textContent = 'ハ'
|
||||
@DummyLineNode.children[3].textContent = '세'
|
||||
|
||||
@domNode = document.createElement('div')
|
||||
@domNode.classList.add('lines')
|
||||
@tilesNode = document.createElement("div")
|
||||
# Create a new stacking context, so that tiles z-index does not interfere
|
||||
# with other visual elements.
|
||||
@tilesNode.style.isolation = "isolate"
|
||||
@tilesNode.style.zIndex = 0
|
||||
@domNode.appendChild(@tilesNode)
|
||||
|
||||
@cursorsComponent = new CursorsComponent
|
||||
@domNode.appendChild(@cursorsComponent.getDomNode())
|
||||
|
||||
getDomNode: ->
|
||||
@domNode
|
||||
|
||||
shouldRecreateAllTilesOnUpdate: ->
|
||||
@newState.continuousReflow
|
||||
|
||||
beforeUpdateSync: (state) ->
|
||||
if @newState.maxHeight isnt @oldState.maxHeight
|
||||
@domNode.style.height = @newState.maxHeight + 'px'
|
||||
@oldState.maxHeight = @newState.maxHeight
|
||||
|
||||
if @newState.backgroundColor isnt @oldState.backgroundColor
|
||||
@domNode.style.backgroundColor = @newState.backgroundColor
|
||||
@oldState.backgroundColor = @newState.backgroundColor
|
||||
|
||||
afterUpdateSync: (state) ->
|
||||
if @newState.placeholderText isnt @oldState.placeholderText
|
||||
@placeholderTextDiv?.remove()
|
||||
if @newState.placeholderText?
|
||||
@placeholderTextDiv = document.createElement('div')
|
||||
@placeholderTextDiv.classList.add('placeholder-text')
|
||||
@placeholderTextDiv.textContent = @newState.placeholderText
|
||||
@domNode.appendChild(@placeholderTextDiv)
|
||||
@oldState.placeholderText = @newState.placeholderText
|
||||
|
||||
# Removing and updating block decorations needs to be done in two different
|
||||
# steps, so that the same decoration node can be moved from one tile to
|
||||
# another in the same animation frame.
|
||||
for component in @getComponents()
|
||||
component.removeDeletedBlockDecorations()
|
||||
for component in @getComponents()
|
||||
component.updateBlockDecorations()
|
||||
|
||||
@cursorsComponent.updateSync(state)
|
||||
|
||||
buildComponentForTile: (id) -> new LinesTileComponent({id, @presenter, @domElementPool, @assert, @views})
|
||||
|
||||
buildEmptyState: ->
|
||||
{tiles: {}}
|
||||
|
||||
getNewState: (state) ->
|
||||
state.content
|
||||
|
||||
getTilesNode: -> @tilesNode
|
||||
|
||||
measureLineHeightAndDefaultCharWidth: ->
|
||||
@domNode.appendChild(@DummyLineNode)
|
||||
|
||||
lineHeightInPixels = @DummyLineNode.getBoundingClientRect().height
|
||||
defaultCharWidth = @DummyLineNode.children[0].getBoundingClientRect().width
|
||||
doubleWidthCharWidth = @DummyLineNode.children[1].getBoundingClientRect().width
|
||||
halfWidthCharWidth = @DummyLineNode.children[2].getBoundingClientRect().width
|
||||
koreanCharWidth = @DummyLineNode.children[3].getBoundingClientRect().width
|
||||
|
||||
@domNode.removeChild(@DummyLineNode)
|
||||
|
||||
@presenter.setLineHeight(lineHeightInPixels)
|
||||
@presenter.setBaseCharacterWidth(defaultCharWidth, doubleWidthCharWidth, halfWidthCharWidth, koreanCharWidth)
|
||||
|
||||
measureBlockDecorations: ->
|
||||
for component in @getComponents()
|
||||
component.measureBlockDecorations()
|
||||
return
|
||||
|
||||
lineIdForScreenRow: (screenRow) ->
|
||||
tile = @presenter.tileForRow(screenRow)
|
||||
@getComponentForTile(tile)?.lineIdForScreenRow(screenRow)
|
||||
|
||||
lineNodeForScreenRow: (screenRow) ->
|
||||
tile = @presenter.tileForRow(screenRow)
|
||||
@getComponentForTile(tile)?.lineNodeForScreenRow(screenRow)
|
||||
|
||||
textNodesForScreenRow: (screenRow) ->
|
||||
tile = @presenter.tileForRow(screenRow)
|
||||
@getComponentForTile(tile)?.textNodesForScreenRow(screenRow)
|
||||
@@ -1,401 +0,0 @@
|
||||
const HighlightsComponent = require('./highlights-component')
|
||||
const ZERO_WIDTH_NBSP = '\ufeff'
|
||||
|
||||
module.exports = class LinesTileComponent {
|
||||
constructor ({presenter, id, domElementPool, assert, views}) {
|
||||
this.id = id
|
||||
this.presenter = presenter
|
||||
this.views = views
|
||||
this.domElementPool = domElementPool
|
||||
this.assert = assert
|
||||
this.lineNodesByLineId = {}
|
||||
this.screenRowsByLineId = {}
|
||||
this.lineIdsByScreenRow = {}
|
||||
this.textNodesByLineId = {}
|
||||
this.blockDecorationNodesByLineIdAndDecorationId = {}
|
||||
this.domNode = this.domElementPool.buildElement('div')
|
||||
this.domNode.style.position = 'absolute'
|
||||
this.domNode.style.display = 'block'
|
||||
this.highlightsComponent = new HighlightsComponent(this.domElementPool)
|
||||
this.domNode.appendChild(this.highlightsComponent.getDomNode())
|
||||
}
|
||||
|
||||
destroy () {
|
||||
this.removeLineNodes()
|
||||
this.domElementPool.freeElementAndDescendants(this.domNode)
|
||||
}
|
||||
|
||||
getDomNode () {
|
||||
return this.domNode
|
||||
}
|
||||
|
||||
updateSync (state) {
|
||||
this.newState = state
|
||||
if (this.oldState == null) {
|
||||
this.oldState = {tiles: {}}
|
||||
this.oldState.tiles[this.id] = {lines: {}}
|
||||
}
|
||||
|
||||
this.newTileState = this.newState.tiles[this.id]
|
||||
this.oldTileState = this.oldState.tiles[this.id]
|
||||
|
||||
if (this.newState.backgroundColor !== this.oldState.backgroundColor) {
|
||||
this.domNode.style.backgroundColor = this.newState.backgroundColor
|
||||
this.oldState.backgroundColor = this.newState.backgroundColor
|
||||
}
|
||||
|
||||
if (this.newTileState.zIndex !== this.oldTileState.zIndex) {
|
||||
this.domNode.style.zIndex = this.newTileState.zIndex
|
||||
this.oldTileState.zIndex = this.newTileState.zIndex
|
||||
}
|
||||
|
||||
if (this.newTileState.display !== this.oldTileState.display) {
|
||||
this.domNode.style.display = this.newTileState.display
|
||||
this.oldTileState.display = this.newTileState.display
|
||||
}
|
||||
|
||||
if (this.newTileState.height !== this.oldTileState.height) {
|
||||
this.domNode.style.height = this.newTileState.height + 'px'
|
||||
this.oldTileState.height = this.newTileState.height
|
||||
}
|
||||
|
||||
if (this.newState.width !== this.oldState.width) {
|
||||
this.domNode.style.width = this.newState.width + 'px'
|
||||
this.oldState.width = this.newState.width
|
||||
}
|
||||
|
||||
if (this.newTileState.top !== this.oldTileState.top || this.newTileState.left !== this.oldTileState.left) {
|
||||
this.domNode.style.transform = `translate3d(${this.newTileState.left}px, ${this.newTileState.top}px, 0px)`
|
||||
this.oldTileState.top = this.newTileState.top
|
||||
this.oldTileState.left = this.newTileState.left
|
||||
}
|
||||
|
||||
this.updateLineNodes()
|
||||
this.highlightsComponent.updateSync(this.newTileState)
|
||||
}
|
||||
|
||||
removeLineNodes () {
|
||||
for (const id of Object.keys(this.oldTileState.lines)) {
|
||||
this.removeLineNode(id)
|
||||
}
|
||||
}
|
||||
|
||||
removeLineNode (lineId) {
|
||||
this.domElementPool.freeElementAndDescendants(this.lineNodesByLineId[lineId])
|
||||
for (const decorationId of Object.keys(this.oldTileState.lines[lineId].precedingBlockDecorations)) {
|
||||
const {topRulerNode, blockDecorationNode, bottomRulerNode} =
|
||||
this.blockDecorationNodesByLineIdAndDecorationId[lineId][decorationId]
|
||||
topRulerNode.remove()
|
||||
blockDecorationNode.remove()
|
||||
bottomRulerNode.remove()
|
||||
}
|
||||
for (const decorationId of Object.keys(this.oldTileState.lines[lineId].followingBlockDecorations)) {
|
||||
const {topRulerNode, blockDecorationNode, bottomRulerNode} =
|
||||
this.blockDecorationNodesByLineIdAndDecorationId[lineId][decorationId]
|
||||
topRulerNode.remove()
|
||||
blockDecorationNode.remove()
|
||||
bottomRulerNode.remove()
|
||||
}
|
||||
|
||||
delete this.blockDecorationNodesByLineIdAndDecorationId[lineId]
|
||||
delete this.lineNodesByLineId[lineId]
|
||||
delete this.textNodesByLineId[lineId]
|
||||
delete this.lineIdsByScreenRow[this.screenRowsByLineId[lineId]]
|
||||
delete this.screenRowsByLineId[lineId]
|
||||
delete this.oldTileState.lines[lineId]
|
||||
}
|
||||
|
||||
updateLineNodes () {
|
||||
for (const id of Object.keys(this.oldTileState.lines)) {
|
||||
if (!this.newTileState.lines.hasOwnProperty(id)) {
|
||||
this.removeLineNode(id)
|
||||
}
|
||||
}
|
||||
|
||||
const newLineIds = []
|
||||
const newLineNodes = []
|
||||
for (const id of Object.keys(this.newTileState.lines)) {
|
||||
const lineState = this.newTileState.lines[id]
|
||||
if (this.oldTileState.lines.hasOwnProperty(id)) {
|
||||
this.updateLineNode(id)
|
||||
} else {
|
||||
newLineIds.push(id)
|
||||
newLineNodes.push(this.buildLineNode(id))
|
||||
this.screenRowsByLineId[id] = lineState.screenRow
|
||||
this.lineIdsByScreenRow[lineState.screenRow] = id
|
||||
this.oldTileState.lines[id] = Object.assign({}, lineState)
|
||||
// Avoid assigning state for block decorations, because we need to
|
||||
// process it later when updating the DOM.
|
||||
this.oldTileState.lines[id].precedingBlockDecorations = {}
|
||||
this.oldTileState.lines[id].followingBlockDecorations = {}
|
||||
}
|
||||
}
|
||||
|
||||
while (newLineIds.length > 0) {
|
||||
const id = newLineIds.shift()
|
||||
const lineNode = newLineNodes.shift()
|
||||
this.lineNodesByLineId[id] = lineNode
|
||||
const nextNode = this.findNodeNextTo(lineNode)
|
||||
if (nextNode == null) {
|
||||
this.domNode.appendChild(lineNode)
|
||||
} else {
|
||||
this.domNode.insertBefore(lineNode, nextNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
findNodeNextTo (node) {
|
||||
let i = 1 // skip highlights node
|
||||
while (i < this.domNode.children.length) {
|
||||
const nextNode = this.domNode.children[i]
|
||||
if (this.screenRowForNode(node) < this.screenRowForNode(nextNode)) {
|
||||
return nextNode
|
||||
}
|
||||
i++
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
screenRowForNode (node) {
|
||||
return parseInt(node.dataset.screenRow)
|
||||
}
|
||||
|
||||
buildLineNode (id) {
|
||||
const {lineText, tagCodes, screenRow, decorationClasses} = this.newTileState.lines[id]
|
||||
|
||||
const lineNode = this.domElementPool.buildElement('div', 'line')
|
||||
lineNode.dataset.screenRow = screenRow
|
||||
if (decorationClasses != null) {
|
||||
for (const decorationClass of decorationClasses) {
|
||||
lineNode.classList.add(decorationClass)
|
||||
}
|
||||
}
|
||||
|
||||
const textNodes = []
|
||||
let startIndex = 0
|
||||
let openScopeNode = lineNode
|
||||
for (const tagCode of tagCodes) {
|
||||
if (tagCode !== 0) {
|
||||
if (this.presenter.isCloseTagCode(tagCode)) {
|
||||
openScopeNode = openScopeNode.parentElement
|
||||
} else if (this.presenter.isOpenTagCode(tagCode)) {
|
||||
const scope = this.presenter.tagForCode(tagCode)
|
||||
const newScopeNode = this.domElementPool.buildElement('span', scope.replace(/\.+/g, ' '))
|
||||
openScopeNode.appendChild(newScopeNode)
|
||||
openScopeNode = newScopeNode
|
||||
} else {
|
||||
const textNode = this.domElementPool.buildText(lineText.substr(startIndex, tagCode))
|
||||
startIndex += tagCode
|
||||
openScopeNode.appendChild(textNode)
|
||||
textNodes.push(textNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (startIndex === 0) {
|
||||
const textNode = this.domElementPool.buildText(' ')
|
||||
lineNode.appendChild(textNode)
|
||||
textNodes.push(textNode)
|
||||
}
|
||||
|
||||
if (lineText.endsWith(this.presenter.displayLayer.foldCharacter)) {
|
||||
// Insert a zero-width non-breaking whitespace, so that LinesYardstick can
|
||||
// take the fold-marker::after pseudo-element into account during
|
||||
// measurements when such marker is the last character on the line.
|
||||
const textNode = this.domElementPool.buildText(ZERO_WIDTH_NBSP)
|
||||
lineNode.appendChild(textNode)
|
||||
textNodes.push(textNode)
|
||||
}
|
||||
|
||||
this.textNodesByLineId[id] = textNodes
|
||||
return lineNode
|
||||
}
|
||||
|
||||
updateLineNode (id) {
|
||||
const oldLineState = this.oldTileState.lines[id]
|
||||
const newLineState = this.newTileState.lines[id]
|
||||
const lineNode = this.lineNodesByLineId[id]
|
||||
const newDecorationClasses = newLineState.decorationClasses
|
||||
const oldDecorationClasses = oldLineState.decorationClasses
|
||||
|
||||
if (oldDecorationClasses != null) {
|
||||
for (const decorationClass of oldDecorationClasses) {
|
||||
if (newDecorationClasses == null || !newDecorationClasses.includes(decorationClass)) {
|
||||
lineNode.classList.remove(decorationClass)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (newDecorationClasses != null) {
|
||||
for (const decorationClass of newDecorationClasses) {
|
||||
if (oldDecorationClasses == null || !oldDecorationClasses.includes(decorationClass)) {
|
||||
lineNode.classList.add(decorationClass)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
oldLineState.decorationClasses = newLineState.decorationClasses
|
||||
|
||||
if (newLineState.screenRow !== oldLineState.screenRow) {
|
||||
lineNode.dataset.screenRow = newLineState.screenRow
|
||||
this.lineIdsByScreenRow[newLineState.screenRow] = id
|
||||
this.screenRowsByLineId[id] = newLineState.screenRow
|
||||
}
|
||||
|
||||
oldLineState.screenRow = newLineState.screenRow
|
||||
}
|
||||
|
||||
removeDeletedBlockDecorations () {
|
||||
for (const lineId of Object.keys(this.newTileState.lines)) {
|
||||
const oldLineState = this.oldTileState.lines[lineId]
|
||||
const newLineState = this.newTileState.lines[lineId]
|
||||
for (const decorationId of Object.keys(oldLineState.precedingBlockDecorations)) {
|
||||
if (!newLineState.precedingBlockDecorations.hasOwnProperty(decorationId)) {
|
||||
const {topRulerNode, blockDecorationNode, bottomRulerNode} =
|
||||
this.blockDecorationNodesByLineIdAndDecorationId[lineId][decorationId]
|
||||
topRulerNode.remove()
|
||||
blockDecorationNode.remove()
|
||||
bottomRulerNode.remove()
|
||||
delete this.blockDecorationNodesByLineIdAndDecorationId[lineId][decorationId]
|
||||
delete oldLineState.precedingBlockDecorations[decorationId]
|
||||
}
|
||||
}
|
||||
for (const decorationId of Object.keys(oldLineState.followingBlockDecorations)) {
|
||||
if (!newLineState.followingBlockDecorations.hasOwnProperty(decorationId)) {
|
||||
const {topRulerNode, blockDecorationNode, bottomRulerNode} =
|
||||
this.blockDecorationNodesByLineIdAndDecorationId[lineId][decorationId]
|
||||
topRulerNode.remove()
|
||||
blockDecorationNode.remove()
|
||||
bottomRulerNode.remove()
|
||||
delete this.blockDecorationNodesByLineIdAndDecorationId[lineId][decorationId]
|
||||
delete oldLineState.followingBlockDecorations[decorationId]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateBlockDecorations () {
|
||||
for (const lineId of Object.keys(this.newTileState.lines)) {
|
||||
const oldLineState = this.oldTileState.lines[lineId]
|
||||
const newLineState = this.newTileState.lines[lineId]
|
||||
const lineNode = this.lineNodesByLineId[lineId]
|
||||
if (!this.blockDecorationNodesByLineIdAndDecorationId.hasOwnProperty(lineId)) {
|
||||
this.blockDecorationNodesByLineIdAndDecorationId[lineId] = {}
|
||||
}
|
||||
for (const decorationId of Object.keys(newLineState.precedingBlockDecorations)) {
|
||||
const oldBlockDecorationState = oldLineState.precedingBlockDecorations[decorationId]
|
||||
const newBlockDecorationState = newLineState.precedingBlockDecorations[decorationId]
|
||||
if (oldBlockDecorationState != null) {
|
||||
const {topRulerNode, blockDecorationNode, bottomRulerNode} =
|
||||
this.blockDecorationNodesByLineIdAndDecorationId[lineId][decorationId]
|
||||
if (oldBlockDecorationState.screenRow !== newBlockDecorationState.screenRow) {
|
||||
topRulerNode.remove()
|
||||
blockDecorationNode.remove()
|
||||
bottomRulerNode.remove()
|
||||
topRulerNode.dataset.screenRow = newBlockDecorationState.screenRow
|
||||
this.domNode.insertBefore(topRulerNode, lineNode)
|
||||
blockDecorationNode.dataset.screenRow = newBlockDecorationState.screenRow
|
||||
this.domNode.insertBefore(blockDecorationNode, lineNode)
|
||||
bottomRulerNode.dataset.screenRow = newBlockDecorationState.screenRow
|
||||
this.domNode.insertBefore(bottomRulerNode, lineNode)
|
||||
}
|
||||
} else {
|
||||
const topRulerNode = document.createElement('div')
|
||||
topRulerNode.dataset.screenRow = newBlockDecorationState.screenRow
|
||||
this.domNode.insertBefore(topRulerNode, lineNode)
|
||||
const blockDecorationNode = this.views.getView(newBlockDecorationState.decoration.getProperties().item)
|
||||
blockDecorationNode.dataset.screenRow = newBlockDecorationState.screenRow
|
||||
this.domNode.insertBefore(blockDecorationNode, lineNode)
|
||||
const bottomRulerNode = document.createElement('div')
|
||||
bottomRulerNode.dataset.screenRow = newBlockDecorationState.screenRow
|
||||
this.domNode.insertBefore(bottomRulerNode, lineNode)
|
||||
|
||||
this.blockDecorationNodesByLineIdAndDecorationId[lineId][decorationId] =
|
||||
{topRulerNode, blockDecorationNode, bottomRulerNode}
|
||||
}
|
||||
oldLineState.precedingBlockDecorations[decorationId] = Object.assign({}, newBlockDecorationState)
|
||||
}
|
||||
for (const decorationId of Object.keys(newLineState.followingBlockDecorations)) {
|
||||
const oldBlockDecorationState = oldLineState.followingBlockDecorations[decorationId]
|
||||
const newBlockDecorationState = newLineState.followingBlockDecorations[decorationId]
|
||||
if (oldBlockDecorationState != null) {
|
||||
const {topRulerNode, blockDecorationNode, bottomRulerNode} =
|
||||
this.blockDecorationNodesByLineIdAndDecorationId[lineId][decorationId]
|
||||
if (oldBlockDecorationState.screenRow !== newBlockDecorationState.screenRow) {
|
||||
topRulerNode.remove()
|
||||
blockDecorationNode.remove()
|
||||
bottomRulerNode.remove()
|
||||
bottomRulerNode.dataset.screenRow = newBlockDecorationState.screenRow
|
||||
this.domNode.insertBefore(bottomRulerNode, lineNode.nextSibling)
|
||||
blockDecorationNode.dataset.screenRow = newBlockDecorationState.screenRow
|
||||
this.domNode.insertBefore(blockDecorationNode, lineNode.nextSibling)
|
||||
topRulerNode.dataset.screenRow = newBlockDecorationState.screenRow
|
||||
this.domNode.insertBefore(topRulerNode, lineNode.nextSibling)
|
||||
}
|
||||
} else {
|
||||
const bottomRulerNode = document.createElement('div')
|
||||
bottomRulerNode.dataset.screenRow = newBlockDecorationState.screenRow
|
||||
this.domNode.insertBefore(bottomRulerNode, lineNode.nextSibling)
|
||||
const blockDecorationNode = this.views.getView(newBlockDecorationState.decoration.getProperties().item)
|
||||
blockDecorationNode.dataset.screenRow = newBlockDecorationState.screenRow
|
||||
this.domNode.insertBefore(blockDecorationNode, lineNode.nextSibling)
|
||||
const topRulerNode = document.createElement('div')
|
||||
topRulerNode.dataset.screenRow = newBlockDecorationState.screenRow
|
||||
this.domNode.insertBefore(topRulerNode, lineNode.nextSibling)
|
||||
|
||||
this.blockDecorationNodesByLineIdAndDecorationId[lineId][decorationId] =
|
||||
{topRulerNode, blockDecorationNode, bottomRulerNode}
|
||||
}
|
||||
oldLineState.followingBlockDecorations[decorationId] = Object.assign({}, newBlockDecorationState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
measureBlockDecorations () {
|
||||
for (const lineId of Object.keys(this.newTileState.lines)) {
|
||||
const newLineState = this.newTileState.lines[lineId]
|
||||
|
||||
for (const decorationId of Object.keys(newLineState.precedingBlockDecorations)) {
|
||||
const {topRulerNode, blockDecorationNode, bottomRulerNode} =
|
||||
this.blockDecorationNodesByLineIdAndDecorationId[lineId][decorationId]
|
||||
const width = blockDecorationNode.offsetWidth
|
||||
const height = bottomRulerNode.offsetTop - topRulerNode.offsetTop
|
||||
const {decoration} = newLineState.precedingBlockDecorations[decorationId]
|
||||
this.presenter.setBlockDecorationDimensions(decoration, width, height)
|
||||
}
|
||||
for (const decorationId of Object.keys(newLineState.followingBlockDecorations)) {
|
||||
const {topRulerNode, blockDecorationNode, bottomRulerNode} =
|
||||
this.blockDecorationNodesByLineIdAndDecorationId[lineId][decorationId]
|
||||
const width = blockDecorationNode.offsetWidth
|
||||
const height = bottomRulerNode.offsetTop - topRulerNode.offsetTop
|
||||
const {decoration} = newLineState.followingBlockDecorations[decorationId]
|
||||
this.presenter.setBlockDecorationDimensions(decoration, width, height)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lineNodeForScreenRow (screenRow) {
|
||||
return this.lineNodesByLineId[this.lineIdsByScreenRow[screenRow]]
|
||||
}
|
||||
|
||||
lineNodeForLineId (lineId) {
|
||||
return this.lineNodesByLineId[lineId]
|
||||
}
|
||||
|
||||
textNodesForLineId (lineId) {
|
||||
return this.textNodesByLineId[lineId].slice()
|
||||
}
|
||||
|
||||
lineIdForScreenRow (screenRow) {
|
||||
return this.lineIdsByScreenRow[screenRow]
|
||||
}
|
||||
|
||||
textNodesForScreenRow (screenRow) {
|
||||
const textNodes = this.textNodesByLineId[this.lineIdsByScreenRow[screenRow]]
|
||||
if (textNodes == null) {
|
||||
return null
|
||||
} else {
|
||||
return textNodes.slice()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
{Point} = require 'text-buffer'
|
||||
{isPairedCharacter} = require './text-utils'
|
||||
|
||||
module.exports =
|
||||
class LinesYardstick
|
||||
constructor: (@model, @lineNodesProvider, @lineTopIndex) ->
|
||||
@rangeForMeasurement = document.createRange()
|
||||
@invalidateCache()
|
||||
|
||||
invalidateCache: ->
|
||||
@leftPixelPositionCache = {}
|
||||
|
||||
measuredRowForPixelPosition: (pixelPosition) ->
|
||||
targetTop = pixelPosition.top
|
||||
row = Math.floor(targetTop / @model.getLineHeightInPixels())
|
||||
row if 0 <= row
|
||||
|
||||
screenPositionForPixelPosition: (pixelPosition) ->
|
||||
targetTop = pixelPosition.top
|
||||
row = Math.max(0, @lineTopIndex.rowForPixelPosition(targetTop))
|
||||
lineNode = @lineNodesProvider.lineNodeForScreenRow(row)
|
||||
unless lineNode
|
||||
lastScreenRow = @model.getLastScreenRow()
|
||||
if row > lastScreenRow
|
||||
return Point(lastScreenRow, @model.lineLengthForScreenRow(lastScreenRow))
|
||||
else
|
||||
return Point(row, 0)
|
||||
|
||||
targetLeft = pixelPosition.left
|
||||
targetLeft = 0 if targetTop < 0 or targetLeft < 0
|
||||
|
||||
textNodes = @lineNodesProvider.textNodesForScreenRow(row)
|
||||
lineOffset = lineNode.getBoundingClientRect().left
|
||||
targetLeft += lineOffset
|
||||
|
||||
textNodeIndex = 0
|
||||
low = 0
|
||||
high = textNodes.length - 1
|
||||
while low <= high
|
||||
mid = low + (high - low >> 1)
|
||||
textNode = textNodes[mid]
|
||||
rangeRect = @clientRectForRange(textNode, 0, textNode.length)
|
||||
if targetLeft < rangeRect.left
|
||||
high = mid - 1
|
||||
textNodeIndex = Math.max(0, mid - 1)
|
||||
else if targetLeft > rangeRect.right
|
||||
low = mid + 1
|
||||
textNodeIndex = Math.min(textNodes.length - 1, mid + 1)
|
||||
else
|
||||
textNodeIndex = mid
|
||||
break
|
||||
|
||||
textNode = textNodes[textNodeIndex]
|
||||
characterIndex = 0
|
||||
low = 0
|
||||
high = textNode.textContent.length - 1
|
||||
while low <= high
|
||||
charIndex = low + (high - low >> 1)
|
||||
if isPairedCharacter(textNode.textContent, charIndex)
|
||||
nextCharIndex = charIndex + 2
|
||||
else
|
||||
nextCharIndex = charIndex + 1
|
||||
|
||||
rangeRect = @clientRectForRange(textNode, charIndex, nextCharIndex)
|
||||
if targetLeft < rangeRect.left
|
||||
high = charIndex - 1
|
||||
characterIndex = Math.max(0, charIndex - 1)
|
||||
else if targetLeft > rangeRect.right
|
||||
low = nextCharIndex
|
||||
characterIndex = Math.min(textNode.textContent.length, nextCharIndex)
|
||||
else
|
||||
if targetLeft <= ((rangeRect.left + rangeRect.right) / 2)
|
||||
characterIndex = charIndex
|
||||
else
|
||||
characterIndex = nextCharIndex
|
||||
break
|
||||
|
||||
textNodeStartColumn = 0
|
||||
textNodeStartColumn += textNodes[i].length for i in [0...textNodeIndex] by 1
|
||||
Point(row, textNodeStartColumn + characterIndex)
|
||||
|
||||
pixelPositionForScreenPosition: (screenPosition) ->
|
||||
targetRow = screenPosition.row
|
||||
targetColumn = screenPosition.column
|
||||
|
||||
top = @lineTopIndex.pixelPositionAfterBlocksForRow(targetRow)
|
||||
left = @leftPixelPositionForScreenPosition(targetRow, targetColumn)
|
||||
|
||||
{top, left}
|
||||
|
||||
leftPixelPositionForScreenPosition: (row, column) ->
|
||||
lineNode = @lineNodesProvider.lineNodeForScreenRow(row)
|
||||
lineId = @lineNodesProvider.lineIdForScreenRow(row)
|
||||
|
||||
if lineNode?
|
||||
if @leftPixelPositionCache[lineId]?[column]?
|
||||
@leftPixelPositionCache[lineId][column]
|
||||
else
|
||||
textNodes = @lineNodesProvider.textNodesForScreenRow(row)
|
||||
textNodeStartColumn = 0
|
||||
for textNode in textNodes
|
||||
textNodeEndColumn = textNodeStartColumn + textNode.textContent.length
|
||||
if textNodeEndColumn > column
|
||||
indexInTextNode = column - textNodeStartColumn
|
||||
break
|
||||
else
|
||||
textNodeStartColumn = textNodeEndColumn
|
||||
|
||||
if textNode?
|
||||
indexInTextNode ?= textNode.textContent.length
|
||||
lineOffset = lineNode.getBoundingClientRect().left
|
||||
if indexInTextNode is 0
|
||||
leftPixelPosition = @clientRectForRange(textNode, 0, 1).left
|
||||
else
|
||||
leftPixelPosition = @clientRectForRange(textNode, 0, indexInTextNode).right
|
||||
leftPixelPosition -= lineOffset
|
||||
|
||||
@leftPixelPositionCache[lineId] ?= {}
|
||||
@leftPixelPositionCache[lineId][column] = leftPixelPosition
|
||||
leftPixelPosition
|
||||
else
|
||||
0
|
||||
else
|
||||
0
|
||||
|
||||
clientRectForRange: (textNode, startIndex, endIndex) ->
|
||||
@rangeForMeasurement.setStart(textNode, startIndex)
|
||||
@rangeForMeasurement.setEnd(textNode, endIndex)
|
||||
clientRects = @rangeForMeasurement.getClientRects()
|
||||
if clientRects.length is 1
|
||||
clientRects[0]
|
||||
else
|
||||
@rangeForMeasurement.getBoundingClientRect()
|
||||
@@ -26,17 +26,16 @@ class AtomWindow
|
||||
options =
|
||||
show: false
|
||||
title: 'Atom'
|
||||
# Add an opaque backgroundColor (instead of keeping the default
|
||||
# transparent one) to prevent subpixel anti-aliasing from being disabled.
|
||||
# We believe this is a regression introduced with Electron 0.37.3, and
|
||||
# thus we should remove this as soon as a fix gets released.
|
||||
backgroundColor: "#fff"
|
||||
webPreferences:
|
||||
# Prevent specs from throttling when the window is in the background:
|
||||
# this should result in faster CI builds, and an improvement in the
|
||||
# local development experience when running specs through the UI (which
|
||||
# now won't pause when e.g. minimizing the window).
|
||||
backgroundThrottling: not @isSpec
|
||||
# Disable the `auxclick` feature so that `click` events are triggered in
|
||||
# response to a middle-click.
|
||||
# (Ref: https://github.com/atom/atom/pull/12696#issuecomment-290496960)
|
||||
disableBlinkFeatures: 'Auxclick'
|
||||
|
||||
# Don't set icon on Windows so the exe's ico will be used as window and
|
||||
# taskbar's icon. See https://github.com/atom/atom/issues/4811 for more.
|
||||
@@ -315,4 +314,4 @@ class AtomWindow
|
||||
copy: -> @browserWindow.copy()
|
||||
|
||||
disableZoom: ->
|
||||
@browserWindow.webContents.setZoomLevelLimits(1, 1)
|
||||
@browserWindow.webContents.setVisualZoomLevelLimits(1, 1)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
if (typeof snapshotResult !== 'undefined') {
|
||||
snapshotResult.setGlobals(global, process, global, {}, console, require) // eslint-disable-line no-undef
|
||||
snapshotResult.setGlobals(global, process, global, {}, console, require)
|
||||
}
|
||||
|
||||
const startTime = Date.now()
|
||||
|
||||
@@ -22,6 +22,8 @@ module.exports = function start (resourcePath, startTime) {
|
||||
const previousConsoleLog = console.log
|
||||
console.log = nslog
|
||||
|
||||
app.commandLine.appendSwitch('enable-experimental-web-platform-features')
|
||||
|
||||
const args = parseCommandLine(process.argv.slice(1))
|
||||
atomPaths.setAtomHome(app.getPath('home'))
|
||||
atomPaths.setUserData(app)
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
module.exports =
|
||||
class MarkerObservationWindow
|
||||
constructor: (@decorationManager, @bufferWindow) ->
|
||||
|
||||
setScreenRange: (range) ->
|
||||
@bufferWindow.setRange(@decorationManager.bufferRangeForScreenRange(range))
|
||||
|
||||
setBufferRange: (range) ->
|
||||
@bufferWindow.setRange(range)
|
||||
|
||||
destroy: ->
|
||||
@bufferWindow.destroy()
|
||||
@@ -83,7 +83,7 @@ class NativeCompileCache {
|
||||
compiledWrapper = compilationResult.result
|
||||
}
|
||||
|
||||
let args = [moduleSelf.exports, require, moduleSelf, filename, dirname, process, global]
|
||||
let args = [moduleSelf.exports, require, moduleSelf, filename, dirname, process, global, Buffer]
|
||||
return compiledWrapper.apply(moduleSelf.exports, args)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
module.exports = class OffScreenBlockDecorationsComponent {
|
||||
constructor ({presenter, views}) {
|
||||
this.presenter = presenter
|
||||
this.views = views
|
||||
this.newState = {offScreenBlockDecorations: {}, width: 0}
|
||||
this.oldState = {offScreenBlockDecorations: {}, width: 0}
|
||||
this.domNode = document.createElement('div')
|
||||
this.domNode.style.visibility = 'hidden'
|
||||
this.domNode.style.position = 'absolute'
|
||||
this.blockDecorationNodesById = {}
|
||||
}
|
||||
|
||||
getDomNode () {
|
||||
return this.domNode
|
||||
}
|
||||
|
||||
updateSync (state) {
|
||||
this.newState = state.content
|
||||
|
||||
if (this.newState.width !== this.oldState.width) {
|
||||
this.domNode.style.width = `${this.newState.width}px`
|
||||
this.oldState.width = this.newState.width
|
||||
}
|
||||
|
||||
for (const id of Object.keys(this.oldState.offScreenBlockDecorations)) {
|
||||
if (!this.newState.offScreenBlockDecorations.hasOwnProperty(id)) {
|
||||
const {topRuler, blockDecoration, bottomRuler} = this.blockDecorationNodesById[id]
|
||||
topRuler.remove()
|
||||
blockDecoration.remove()
|
||||
bottomRuler.remove()
|
||||
delete this.blockDecorationNodesById[id]
|
||||
delete this.oldState.offScreenBlockDecorations[id]
|
||||
}
|
||||
}
|
||||
|
||||
for (const id of Object.keys(this.newState.offScreenBlockDecorations)) {
|
||||
const decoration = this.newState.offScreenBlockDecorations[id]
|
||||
if (!this.oldState.offScreenBlockDecorations.hasOwnProperty(id)) {
|
||||
const topRuler = document.createElement('div')
|
||||
this.domNode.appendChild(topRuler)
|
||||
const blockDecoration = this.views.getView(decoration.getProperties().item)
|
||||
this.domNode.appendChild(blockDecoration)
|
||||
const bottomRuler = document.createElement('div')
|
||||
this.domNode.appendChild(bottomRuler)
|
||||
|
||||
this.blockDecorationNodesById[id] = {topRuler, blockDecoration, bottomRuler}
|
||||
}
|
||||
|
||||
this.oldState.offScreenBlockDecorations[id] = decoration
|
||||
}
|
||||
}
|
||||
|
||||
measureBlockDecorations () {
|
||||
for (const id of Object.keys(this.blockDecorationNodesById)) {
|
||||
const {topRuler, blockDecoration, bottomRuler} = this.blockDecorationNodesById[id]
|
||||
const width = blockDecoration.offsetWidth
|
||||
const height = bottomRuler.offsetTop - topRuler.offsetTop
|
||||
const decoration = this.newState.offScreenBlockDecorations[id]
|
||||
this.presenter.setBlockDecorationDimensions(decoration, width, height)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
ElementResizeDetector = require('element-resize-detector')
|
||||
elementResizeDetector = null
|
||||
|
||||
module.exports =
|
||||
class OverlayManager
|
||||
constructor: (@presenter, @container, @views) ->
|
||||
@@ -12,6 +15,7 @@ class OverlayManager
|
||||
unless state.content.overlays.hasOwnProperty(id)
|
||||
delete @overlaysById[id]
|
||||
overlayNode.remove()
|
||||
elementResizeDetector.uninstall(overlayNode)
|
||||
|
||||
shouldUpdateOverlay: (decorationId, overlay) ->
|
||||
cachedOverlay = @overlaysById[decorationId]
|
||||
@@ -19,10 +23,6 @@ class OverlayManager
|
||||
cachedOverlay.pixelPosition?.top isnt overlay.pixelPosition?.top or
|
||||
cachedOverlay.pixelPosition?.left isnt overlay.pixelPosition?.left
|
||||
|
||||
measureOverlays: ->
|
||||
for decorationId, {itemView} of @overlaysById
|
||||
@measureOverlay(decorationId, itemView)
|
||||
|
||||
measureOverlay: (decorationId, itemView) ->
|
||||
contentMargin = parseInt(getComputedStyle(itemView)['margin-left']) ? 0
|
||||
@presenter.setOverlayDimensions(decorationId, itemView.offsetWidth, itemView.offsetHeight, contentMargin)
|
||||
@@ -33,13 +33,20 @@ class OverlayManager
|
||||
unless overlayNode = cachedOverlay?.overlayNode
|
||||
overlayNode = document.createElement('atom-overlay')
|
||||
overlayNode.classList.add(klass) if klass?
|
||||
elementResizeDetector ?= ElementResizeDetector({strategy: 'scroll'})
|
||||
elementResizeDetector.listenTo(overlayNode, =>
|
||||
if overlayNode.parentElement?
|
||||
@measureOverlay(decorationId, itemView)
|
||||
)
|
||||
@container.appendChild(overlayNode)
|
||||
@overlaysById[decorationId] = cachedOverlay = {overlayNode, itemView}
|
||||
|
||||
# The same node may be used in more than one overlay. This steals the node
|
||||
# back if it has been displayed in another overlay.
|
||||
overlayNode.appendChild(itemView) if overlayNode.childNodes.length is 0
|
||||
overlayNode.appendChild(itemView) unless overlayNode.contains(itemView)
|
||||
|
||||
cachedOverlay.pixelPosition = pixelPosition
|
||||
overlayNode.style.top = pixelPosition.top + 'px'
|
||||
overlayNode.style.left = pixelPosition.left + 'px'
|
||||
|
||||
@measureOverlay(decorationId, itemView)
|
||||
|
||||
@@ -751,7 +751,7 @@ class Package
|
||||
"installed-packages:#{@name}:#{@metadata.version}:build-error"
|
||||
|
||||
getIncompatibleNativeModulesStorageKey: ->
|
||||
electronVersion = process.versions['electron'] ? process.versions['atom-shell']
|
||||
electronVersion = process.versions.electron
|
||||
"installed-packages:#{@name}:#{@metadata.version}:electron-#{electronVersion}:incompatible-native-modules"
|
||||
|
||||
getCanDeferMainModuleRequireStorageKey: ->
|
||||
|
||||
@@ -27,7 +27,7 @@ class PaneElement extends HTMLElement
|
||||
|
||||
subscribeToDOMEvents: ->
|
||||
handleFocus = (event) =>
|
||||
@model.focus() unless @isActivating or @contains(event.relatedTarget)
|
||||
@model.focus() unless @isActivating or @model.isDestroyed() or @contains(event.relatedTarget)
|
||||
if event.target is this and view = @getActiveView()
|
||||
view.focus()
|
||||
event.stopPropagation()
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
module.exports =
|
||||
class ScrollbarComponent
|
||||
constructor: ({@orientation, @onScroll}) ->
|
||||
@domNode = document.createElement('div')
|
||||
@domNode.classList.add "#{@orientation}-scrollbar"
|
||||
@domNode.style['-webkit-transform'] = 'translateZ(0)' # See atom/atom#3559
|
||||
@domNode.style.left = 0 if @orientation is 'horizontal'
|
||||
|
||||
@contentNode = document.createElement('div')
|
||||
@contentNode.classList.add "scrollbar-content"
|
||||
@domNode.appendChild(@contentNode)
|
||||
|
||||
@domNode.addEventListener 'scroll', @onScrollCallback
|
||||
|
||||
destroy: ->
|
||||
@domNode.removeEventListener 'scroll', @onScrollCallback
|
||||
@onScroll = null
|
||||
|
||||
getDomNode: ->
|
||||
@domNode
|
||||
|
||||
updateSync: (state) ->
|
||||
@oldState ?= {}
|
||||
switch @orientation
|
||||
when 'vertical'
|
||||
@newState = state.verticalScrollbar
|
||||
@updateVertical()
|
||||
when 'horizontal'
|
||||
@newState = state.horizontalScrollbar
|
||||
@updateHorizontal()
|
||||
|
||||
if @newState.visible isnt @oldState.visible
|
||||
if @newState.visible
|
||||
@domNode.style.display = ''
|
||||
else
|
||||
@domNode.style.display = 'none'
|
||||
@oldState.visible = @newState.visible
|
||||
|
||||
updateVertical: ->
|
||||
if @newState.width isnt @oldState.width
|
||||
@domNode.style.width = @newState.width + 'px'
|
||||
@oldState.width = @newState.width
|
||||
|
||||
if @newState.bottom isnt @oldState.bottom
|
||||
@domNode.style.bottom = @newState.bottom + 'px'
|
||||
@oldState.bottom = @newState.bottom
|
||||
|
||||
if @newState.scrollHeight isnt @oldState.scrollHeight
|
||||
@contentNode.style.height = @newState.scrollHeight + 'px'
|
||||
@oldState.scrollHeight = @newState.scrollHeight
|
||||
|
||||
if @newState.scrollTop isnt @oldState.scrollTop
|
||||
@domNode.scrollTop = @newState.scrollTop
|
||||
@oldState.scrollTop = @newState.scrollTop
|
||||
|
||||
updateHorizontal: ->
|
||||
if @newState.height isnt @oldState.height
|
||||
@domNode.style.height = @newState.height + 'px'
|
||||
@oldState.height = @newState.height
|
||||
|
||||
if @newState.right isnt @oldState.right
|
||||
@domNode.style.right = @newState.right + 'px'
|
||||
@oldState.right = @newState.right
|
||||
|
||||
if @newState.scrollWidth isnt @oldState.scrollWidth
|
||||
@contentNode.style.width = @newState.scrollWidth + 'px'
|
||||
@oldState.scrollWidth = @newState.scrollWidth
|
||||
|
||||
if @newState.scrollLeft isnt @oldState.scrollLeft
|
||||
@domNode.scrollLeft = @newState.scrollLeft
|
||||
@oldState.scrollLeft = @newState.scrollLeft
|
||||
|
||||
|
||||
onScrollCallback: =>
|
||||
switch @orientation
|
||||
when 'vertical'
|
||||
@onScroll(@domNode.scrollTop)
|
||||
when 'horizontal'
|
||||
@onScroll(@domNode.scrollLeft)
|
||||
@@ -1,38 +0,0 @@
|
||||
module.exports =
|
||||
class ScrollbarCornerComponent
|
||||
constructor: ->
|
||||
@domNode = document.createElement('div')
|
||||
@domNode.classList.add('scrollbar-corner')
|
||||
|
||||
@contentNode = document.createElement('div')
|
||||
@domNode.appendChild(@contentNode)
|
||||
|
||||
getDomNode: ->
|
||||
@domNode
|
||||
|
||||
updateSync: (state) ->
|
||||
@oldState ?= {}
|
||||
@newState ?= {}
|
||||
|
||||
newHorizontalState = state.horizontalScrollbar
|
||||
newVerticalState = state.verticalScrollbar
|
||||
@newState.visible = newHorizontalState.visible and newVerticalState.visible
|
||||
@newState.height = newHorizontalState.height
|
||||
@newState.width = newVerticalState.width
|
||||
|
||||
if @newState.visible isnt @oldState.visible
|
||||
if @newState.visible
|
||||
@domNode.style.display = ''
|
||||
else
|
||||
@domNode.style.display = 'none'
|
||||
@oldState.visible = @newState.visible
|
||||
|
||||
if @newState.height isnt @oldState.height
|
||||
@domNode.style.height = @newState.height + 'px'
|
||||
@contentNode.style.height = @newState.height + 1 + 'px'
|
||||
@oldState.height = @newState.height
|
||||
|
||||
if @newState.width isnt @oldState.width
|
||||
@domNode.style.width = @newState.width + 'px'
|
||||
@contentNode.style.width = @newState.width + 1 + 'px'
|
||||
@oldState.width = @newState.width
|
||||
@@ -769,8 +769,6 @@ class Selection extends Model
|
||||
{oldHeadScreenPosition, oldTailScreenPosition, newHeadScreenPosition} = e
|
||||
{textChanged} = e
|
||||
|
||||
@cursor.updateVisibility()
|
||||
|
||||
unless oldHeadScreenPosition.isEqual(newHeadScreenPosition)
|
||||
@cursor.goalColumn = null
|
||||
cursorMovedEvent = {
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
{userAgent, taskPath} = process.env
|
||||
handler = null
|
||||
|
||||
setupGlobals = ->
|
||||
global.attachEvent = ->
|
||||
console =
|
||||
warn: -> emit 'task:warn', arguments...
|
||||
log: -> emit 'task:log', arguments...
|
||||
error: -> emit 'task:error', arguments...
|
||||
trace: ->
|
||||
global.__defineGetter__ 'console', -> console
|
||||
|
||||
global.document =
|
||||
createElement: ->
|
||||
setAttribute: ->
|
||||
getElementsByTagName: -> []
|
||||
appendChild: ->
|
||||
documentElement:
|
||||
insertBefore: ->
|
||||
removeChild: ->
|
||||
getElementById: -> {}
|
||||
createComment: -> {}
|
||||
createDocumentFragment: -> {}
|
||||
|
||||
global.emit = (event, args...) ->
|
||||
process.send({event, args})
|
||||
global.navigator = {userAgent}
|
||||
global.window = global
|
||||
|
||||
handleEvents = ->
|
||||
process.on 'uncaughtException', (error) ->
|
||||
console.error(error.message, error.stack)
|
||||
process.on 'message', ({event, args}={}) ->
|
||||
return unless event is 'start'
|
||||
|
||||
isAsync = false
|
||||
async = ->
|
||||
isAsync = true
|
||||
(result) ->
|
||||
emit('task:completed', result)
|
||||
result = handler.bind({async})(args...)
|
||||
emit('task:completed', result) unless isAsync
|
||||
|
||||
setupDeprecations = ->
|
||||
Grim = require 'grim'
|
||||
Grim.on 'updated', ->
|
||||
deprecations = Grim.getDeprecations().map (deprecation) -> deprecation.serialize()
|
||||
Grim.clearDeprecations()
|
||||
emit('task:deprecations', deprecations)
|
||||
|
||||
setupGlobals()
|
||||
handleEvents()
|
||||
setupDeprecations()
|
||||
handler = require(taskPath)
|
||||
68
src/task-bootstrap.js
Normal file
68
src/task-bootstrap.js
Normal file
@@ -0,0 +1,68 @@
|
||||
const {userAgent} = process.env
|
||||
const [compileCachePath, taskPath] = process.argv.slice(2)
|
||||
|
||||
const CompileCache = require('./compile-cache')
|
||||
CompileCache.setCacheDirectory(compileCachePath)
|
||||
CompileCache.install(`${process.resourcesPath}`, require)
|
||||
|
||||
const setupGlobals = function () {
|
||||
global.attachEvent = function () {}
|
||||
const console = {
|
||||
warn () { return global.emit('task:warn', ...arguments) },
|
||||
log () { return global.emit('task:log', ...arguments) },
|
||||
error () { return global.emit('task:error', ...arguments) },
|
||||
trace () {}
|
||||
}
|
||||
global.__defineGetter__('console', () => console)
|
||||
|
||||
global.document = {
|
||||
createElement () {
|
||||
return {
|
||||
setAttribute () {},
|
||||
getElementsByTagName () { return [] },
|
||||
appendChild () {}
|
||||
}
|
||||
},
|
||||
documentElement: {
|
||||
insertBefore () {},
|
||||
removeChild () {}
|
||||
},
|
||||
getElementById () { return {} },
|
||||
createComment () { return {} },
|
||||
createDocumentFragment () { return {} }
|
||||
}
|
||||
|
||||
global.emit = (event, ...args) => process.send({event, args})
|
||||
global.navigator = {userAgent}
|
||||
return (global.window = global)
|
||||
}
|
||||
|
||||
const handleEvents = function () {
|
||||
process.on('uncaughtException', error => console.error(error.message, error.stack))
|
||||
|
||||
return process.on('message', function ({event, args} = {}) {
|
||||
if (event !== 'start') { return }
|
||||
|
||||
let isAsync = false
|
||||
const async = function () {
|
||||
isAsync = true
|
||||
return result => global.emit('task:completed', result)
|
||||
}
|
||||
const result = handler.bind({async})(...args)
|
||||
if (!isAsync) { return global.emit('task:completed', result) }
|
||||
})
|
||||
}
|
||||
|
||||
const setupDeprecations = function () {
|
||||
const Grim = require('grim')
|
||||
return Grim.on('updated', function () {
|
||||
const deprecations = Grim.getDeprecations().map(deprecation => deprecation.serialize())
|
||||
Grim.clearDeprecations()
|
||||
return global.emit('task:deprecations', deprecations)
|
||||
})
|
||||
}
|
||||
|
||||
setupGlobals()
|
||||
handleEvents()
|
||||
setupDeprecations()
|
||||
const handler = require(taskPath)
|
||||
@@ -66,22 +66,11 @@ class Task
|
||||
constructor: (taskPath) ->
|
||||
@emitter = new Emitter
|
||||
|
||||
compileCacheRequire = "require('#{require.resolve('./compile-cache')}')"
|
||||
compileCachePath = require('./compile-cache').getCacheDirectory()
|
||||
taskBootstrapRequire = "require('#{require.resolve('./task-bootstrap')}');"
|
||||
bootstrap = """
|
||||
CompileCache = #{compileCacheRequire}
|
||||
CompileCache.setCacheDirectory('#{compileCachePath}');
|
||||
CompileCache.install("#{process.resourcesPath}", require)
|
||||
#{taskBootstrapRequire}
|
||||
"""
|
||||
bootstrap = bootstrap.replace(/\\/g, "\\\\")
|
||||
|
||||
taskPath = require.resolve(taskPath)
|
||||
taskPath = taskPath.replace(/\\/g, "\\\\")
|
||||
|
||||
env = _.extend({}, process.env, {taskPath, userAgent: navigator.userAgent})
|
||||
@childProcess = ChildProcess.fork '--eval', [bootstrap], {env, silent: true}
|
||||
env = Object.assign({}, process.env, {userAgent: navigator.userAgent})
|
||||
@childProcess = ChildProcess.fork require.resolve('./task-bootstrap'), [compileCachePath, taskPath], {env, silent: true}
|
||||
|
||||
@on "task:log", -> console.log(arguments...)
|
||||
@on "task:warn", -> console.warn(arguments...)
|
||||
|
||||
@@ -1,976 +0,0 @@
|
||||
scrollbarStyle = require 'scrollbar-style'
|
||||
{Range, Point} = require 'text-buffer'
|
||||
{CompositeDisposable} = require 'event-kit'
|
||||
{ipcRenderer} = require 'electron'
|
||||
Grim = require 'grim'
|
||||
|
||||
TextEditorPresenter = require './text-editor-presenter'
|
||||
GutterContainerComponent = require './gutter-container-component'
|
||||
InputComponent = require './input-component'
|
||||
LinesComponent = require './lines-component'
|
||||
OffScreenBlockDecorationsComponent = require './off-screen-block-decorations-component'
|
||||
ScrollbarComponent = require './scrollbar-component'
|
||||
ScrollbarCornerComponent = require './scrollbar-corner-component'
|
||||
OverlayManager = require './overlay-manager'
|
||||
DOMElementPool = require './dom-element-pool'
|
||||
LinesYardstick = require './lines-yardstick'
|
||||
LineTopIndex = require 'line-top-index'
|
||||
|
||||
module.exports =
|
||||
class TextEditorComponent
|
||||
cursorBlinkPeriod: 800
|
||||
cursorBlinkResumeDelay: 100
|
||||
tileSize: 12
|
||||
|
||||
pendingScrollTop: null
|
||||
pendingScrollLeft: null
|
||||
updateRequested: false
|
||||
updatesPaused: false
|
||||
updateRequestedWhilePaused: false
|
||||
heightAndWidthMeasurementRequested: false
|
||||
inputEnabled: true
|
||||
measureScrollbarsWhenShown: true
|
||||
measureLineHeightAndDefaultCharWidthWhenShown: true
|
||||
stylingChangeAnimationFrameRequested: false
|
||||
gutterComponent: null
|
||||
mounted: true
|
||||
initialized: false
|
||||
|
||||
Object.defineProperty @prototype, "domNode",
|
||||
get: -> @domNodeValue
|
||||
set: (domNode) ->
|
||||
@assert domNode?, "TextEditorComponent::domNode was set to null."
|
||||
@domNodeValue = domNode
|
||||
|
||||
constructor: ({@editor, @hostElement, tileSize, @views, @themes, @styles, @assert, hiddenInputElement}) ->
|
||||
@tileSize = tileSize if tileSize?
|
||||
@disposables = new CompositeDisposable
|
||||
|
||||
lineTopIndex = new LineTopIndex({
|
||||
defaultLineHeight: @editor.getLineHeightInPixels()
|
||||
})
|
||||
@presenter = new TextEditorPresenter
|
||||
model: @editor
|
||||
tileSize: tileSize
|
||||
cursorBlinkPeriod: @cursorBlinkPeriod
|
||||
cursorBlinkResumeDelay: @cursorBlinkResumeDelay
|
||||
stoppedScrollingDelay: 200
|
||||
lineTopIndex: lineTopIndex
|
||||
autoHeight: @editor.getAutoHeight()
|
||||
|
||||
@presenter.onDidUpdateState(@requestUpdate)
|
||||
|
||||
@domElementPool = new DOMElementPool
|
||||
@domNode = document.createElement('div')
|
||||
@domNode.classList.add('editor-contents--private')
|
||||
|
||||
@overlayManager = new OverlayManager(@presenter, @domNode, @views)
|
||||
|
||||
@scrollViewNode = document.createElement('div')
|
||||
@scrollViewNode.classList.add('scroll-view')
|
||||
@domNode.appendChild(@scrollViewNode)
|
||||
|
||||
@hiddenInputComponent = new InputComponent(hiddenInputElement)
|
||||
@scrollViewNode.appendChild(hiddenInputElement)
|
||||
# Add a getModel method to the hidden input component to make it easy to
|
||||
# access the editor in response to DOM events or when using
|
||||
# document.activeElement.
|
||||
hiddenInputElement.getModel = => @editor
|
||||
|
||||
@linesComponent = new LinesComponent({@presenter, @domElementPool, @assert, @grammars, @views})
|
||||
@scrollViewNode.appendChild(@linesComponent.getDomNode())
|
||||
|
||||
@offScreenBlockDecorationsComponent = new OffScreenBlockDecorationsComponent({@presenter, @views})
|
||||
@scrollViewNode.appendChild(@offScreenBlockDecorationsComponent.getDomNode())
|
||||
|
||||
@linesYardstick = new LinesYardstick(@editor, @linesComponent, lineTopIndex)
|
||||
@presenter.setLinesYardstick(@linesYardstick)
|
||||
|
||||
@horizontalScrollbarComponent = new ScrollbarComponent({orientation: 'horizontal', onScroll: @onHorizontalScroll})
|
||||
@scrollViewNode.appendChild(@horizontalScrollbarComponent.getDomNode())
|
||||
|
||||
@verticalScrollbarComponent = new ScrollbarComponent({orientation: 'vertical', onScroll: @onVerticalScroll})
|
||||
@domNode.appendChild(@verticalScrollbarComponent.getDomNode())
|
||||
|
||||
@scrollbarCornerComponent = new ScrollbarCornerComponent
|
||||
@domNode.appendChild(@scrollbarCornerComponent.getDomNode())
|
||||
|
||||
@observeEditor()
|
||||
@listenForDOMEvents()
|
||||
|
||||
@disposables.add @styles.onDidAddStyleElement @onStylesheetsChanged
|
||||
@disposables.add @styles.onDidUpdateStyleElement @onStylesheetsChanged
|
||||
@disposables.add @styles.onDidRemoveStyleElement @onStylesheetsChanged
|
||||
unless @themes.isInitialLoadComplete()
|
||||
@disposables.add @themes.onDidChangeActiveThemes @onAllThemesLoaded
|
||||
@disposables.add scrollbarStyle.onDidChangePreferredScrollbarStyle @refreshScrollbars
|
||||
|
||||
@disposables.add @views.pollDocument(@pollDOM)
|
||||
|
||||
@updateSync()
|
||||
@checkForVisibilityChange()
|
||||
@initialized = true
|
||||
|
||||
destroy: ->
|
||||
@mounted = false
|
||||
@disposables.dispose()
|
||||
@presenter.destroy()
|
||||
@gutterContainerComponent?.destroy()
|
||||
@domElementPool.clear()
|
||||
|
||||
@verticalScrollbarComponent.destroy()
|
||||
@horizontalScrollbarComponent.destroy()
|
||||
|
||||
@onVerticalScroll = null
|
||||
@onHorizontalScroll = null
|
||||
|
||||
getDomNode: ->
|
||||
@domNode
|
||||
|
||||
updateSync: ->
|
||||
@updateSyncPreMeasurement()
|
||||
|
||||
@oldState ?= {width: null}
|
||||
@newState = @presenter.getPostMeasurementState()
|
||||
|
||||
if @editor.getLastSelection()? and not @editor.getLastSelection().isEmpty()
|
||||
@domNode.classList.add('has-selection')
|
||||
else
|
||||
@domNode.classList.remove('has-selection')
|
||||
|
||||
if @newState.focused isnt @oldState.focused
|
||||
@domNode.classList.toggle('is-focused', @newState.focused)
|
||||
|
||||
@performedInitialMeasurement = false if @editor.isDestroyed()
|
||||
|
||||
if @performedInitialMeasurement
|
||||
if @newState.height isnt @oldState.height
|
||||
if @newState.height?
|
||||
@domNode.style.height = @newState.height + 'px'
|
||||
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)
|
||||
else
|
||||
@gutterContainerComponent?.getDomNode()?.remove()
|
||||
@gutterContainerComponent = null
|
||||
|
||||
@hiddenInputComponent.updateSync(@newState)
|
||||
@offScreenBlockDecorationsComponent.updateSync(@newState)
|
||||
@linesComponent.updateSync(@newState)
|
||||
@horizontalScrollbarComponent.updateSync(@newState)
|
||||
@verticalScrollbarComponent.updateSync(@newState)
|
||||
@scrollbarCornerComponent.updateSync(@newState)
|
||||
|
||||
@overlayManager?.render(@newState)
|
||||
|
||||
if @clearPoolAfterUpdate
|
||||
@domElementPool.clear()
|
||||
@clearPoolAfterUpdate = false
|
||||
|
||||
if @editor.isAlive()
|
||||
@updateParentViewFocusedClassIfNeeded()
|
||||
@updateParentViewMiniClass()
|
||||
|
||||
updateSyncPreMeasurement: ->
|
||||
@linesComponent.updateSync(@presenter.getPreMeasurementState())
|
||||
|
||||
readAfterUpdateSync: =>
|
||||
@overlayManager?.measureOverlays()
|
||||
@linesComponent.measureBlockDecorations()
|
||||
@offScreenBlockDecorationsComponent.measureBlockDecorations()
|
||||
|
||||
mountGutterContainerComponent: ->
|
||||
@gutterContainerComponent = new GutterContainerComponent({@editor, @onLineNumberGutterMouseDown, @domElementPool, @views})
|
||||
@domNode.insertBefore(@gutterContainerComponent.getDomNode(), @domNode.firstChild)
|
||||
|
||||
becameVisible: ->
|
||||
@updatesPaused = true
|
||||
# Always invalidate LinesYardstick measurements when the editor becomes
|
||||
# visible again, because content might have been reflowed and measurements
|
||||
# could be outdated.
|
||||
@invalidateMeasurements()
|
||||
@measureScrollbars() if @measureScrollbarsWhenShown
|
||||
@sampleFontStyling()
|
||||
@sampleBackgroundColors()
|
||||
@measureWindowSize()
|
||||
@measureDimensions()
|
||||
@measureLineHeightAndDefaultCharWidth() if @measureLineHeightAndDefaultCharWidthWhenShown
|
||||
@editor.setVisible(true)
|
||||
@performedInitialMeasurement = true
|
||||
@updatesPaused = false
|
||||
@updateSync() if @canUpdate()
|
||||
|
||||
requestUpdate: =>
|
||||
return unless @canUpdate()
|
||||
|
||||
if @updatesPaused
|
||||
@updateRequestedWhilePaused = true
|
||||
return
|
||||
|
||||
if @hostElement.isUpdatedSynchronously()
|
||||
@updateSync()
|
||||
else unless @updateRequested
|
||||
@updateRequested = true
|
||||
@views.updateDocument =>
|
||||
@updateRequested = false
|
||||
@updateSync() if @canUpdate()
|
||||
@views.readDocument(@readAfterUpdateSync)
|
||||
|
||||
canUpdate: ->
|
||||
@mounted and @editor.isAlive()
|
||||
|
||||
requestAnimationFrame: (fn) ->
|
||||
@updatesPaused = true
|
||||
requestAnimationFrame =>
|
||||
fn()
|
||||
@updatesPaused = false
|
||||
if @updateRequestedWhilePaused and @canUpdate()
|
||||
@updateRequestedWhilePaused = false
|
||||
@requestUpdate()
|
||||
|
||||
getTopmostDOMNode: ->
|
||||
@hostElement
|
||||
|
||||
observeEditor: ->
|
||||
@disposables.add @editor.observeGrammar(@onGrammarChanged)
|
||||
|
||||
listenForDOMEvents: ->
|
||||
@domNode.addEventListener 'mousewheel', @onMouseWheel
|
||||
@domNode.addEventListener 'textInput', @onTextInput
|
||||
@scrollViewNode.addEventListener 'mousedown', @onMouseDown
|
||||
@scrollViewNode.addEventListener 'scroll', @onScrollViewScroll
|
||||
|
||||
@detectAccentedCharacterMenu()
|
||||
@listenForIMEEvents()
|
||||
@trackSelectionClipboard() if process.platform is 'linux'
|
||||
|
||||
detectAccentedCharacterMenu: ->
|
||||
# We need to get clever to detect when the accented character menu is
|
||||
# opened on macOS. Usually, every keydown event that could cause input is
|
||||
# followed by a corresponding keypress. However, pressing and holding
|
||||
# long enough to open the accented character menu causes additional keydown
|
||||
# events to fire that aren't followed by their own keypress and textInput
|
||||
# events.
|
||||
#
|
||||
# Therefore, we assume the accented character menu has been deployed if,
|
||||
# before observing any keyup event, we observe events in the following
|
||||
# sequence:
|
||||
#
|
||||
# keydown(keyCode: X), keypress, keydown(keyCode: X)
|
||||
#
|
||||
# The keyCode X must be the same in the keydown events that bracket the
|
||||
# keypress, meaning we're *holding* the _same_ key we intially pressed.
|
||||
# Got that?
|
||||
lastKeydown = null
|
||||
lastKeydownBeforeKeypress = null
|
||||
|
||||
@domNode.addEventListener 'keydown', (event) =>
|
||||
if lastKeydownBeforeKeypress
|
||||
if lastKeydownBeforeKeypress.keyCode is event.keyCode
|
||||
@openedAccentedCharacterMenu = true
|
||||
lastKeydownBeforeKeypress = null
|
||||
else
|
||||
lastKeydown = event
|
||||
|
||||
@domNode.addEventListener 'keypress', =>
|
||||
lastKeydownBeforeKeypress = lastKeydown
|
||||
lastKeydown = null
|
||||
|
||||
# This cancels the accented character behavior if we type a key normally
|
||||
# with the menu open.
|
||||
@openedAccentedCharacterMenu = false
|
||||
|
||||
@domNode.addEventListener 'keyup', ->
|
||||
lastKeydownBeforeKeypress = null
|
||||
lastKeydown = null
|
||||
|
||||
listenForIMEEvents: ->
|
||||
# The IME composition events work like this:
|
||||
#
|
||||
# User types 's', chromium pops up the completion helper
|
||||
# 1. compositionstart fired
|
||||
# 2. compositionupdate fired; event.data == 's'
|
||||
# User hits arrow keys to move around in completion helper
|
||||
# 3. compositionupdate fired; event.data == 's' for each arry key press
|
||||
# User escape to cancel
|
||||
# 4. compositionend fired
|
||||
# OR User chooses a completion
|
||||
# 4. compositionend fired
|
||||
# 5. textInput fired; event.data == the completion string
|
||||
|
||||
checkpoint = null
|
||||
@domNode.addEventListener 'compositionstart', =>
|
||||
if @openedAccentedCharacterMenu
|
||||
@editor.selectLeft()
|
||||
@openedAccentedCharacterMenu = false
|
||||
checkpoint = @editor.createCheckpoint()
|
||||
@domNode.addEventListener 'compositionupdate', (event) =>
|
||||
@editor.insertText(event.data, select: true)
|
||||
@domNode.addEventListener 'compositionend', (event) =>
|
||||
@editor.revertToCheckpoint(checkpoint)
|
||||
event.target.value = ''
|
||||
|
||||
# Listen for selection changes and store the currently selected text
|
||||
# in the selection clipboard. This is only applicable on Linux.
|
||||
trackSelectionClipboard: ->
|
||||
timeoutId = null
|
||||
writeSelectedTextToSelectionClipboard = =>
|
||||
return if @editor.isDestroyed()
|
||||
if selectedText = @editor.getSelectedText()
|
||||
# This uses ipcRenderer.send instead of clipboard.writeText because
|
||||
# clipboard.writeText is a sync ipcRenderer call on Linux and that
|
||||
# will slow down selections.
|
||||
ipcRenderer.send('write-text-to-selection-clipboard', selectedText)
|
||||
@disposables.add @editor.onDidChangeSelectionRange ->
|
||||
clearTimeout(timeoutId)
|
||||
timeoutId = setTimeout(writeSelectedTextToSelectionClipboard)
|
||||
|
||||
onGrammarChanged: =>
|
||||
if @scopedConfigDisposables?
|
||||
@scopedConfigDisposables.dispose()
|
||||
@disposables.remove(@scopedConfigDisposables)
|
||||
|
||||
@scopedConfigDisposables = new CompositeDisposable
|
||||
@disposables.add(@scopedConfigDisposables)
|
||||
|
||||
focused: ->
|
||||
if @mounted
|
||||
@presenter.setFocused(true)
|
||||
|
||||
blurred: ->
|
||||
if @mounted
|
||||
@presenter.setFocused(false)
|
||||
|
||||
onTextInput: (event) =>
|
||||
event.stopPropagation()
|
||||
|
||||
# WARNING: If we call preventDefault on the input of a space character,
|
||||
# then the browser interprets the spacebar keypress as a page-down command,
|
||||
# causing spaces to scroll elements containing editors. This is impossible
|
||||
# to test.
|
||||
event.preventDefault() if event.data isnt ' '
|
||||
|
||||
return unless @isInputEnabled()
|
||||
|
||||
# Workaround of the accented character suggestion feature in macOS.
|
||||
# This will only occur when the user is not composing in IME mode.
|
||||
# When the user selects a modified character from the macOS menu, `textInput`
|
||||
# will occur twice, once for the initial character, and once for the
|
||||
# modified character. However, only a single keypress will have fired. If
|
||||
# this is the case, select backward to replace the original character.
|
||||
if @openedAccentedCharacterMenu
|
||||
@editor.selectLeft()
|
||||
@openedAccentedCharacterMenu = false
|
||||
|
||||
@editor.insertText(event.data, groupUndo: true)
|
||||
|
||||
onVerticalScroll: (scrollTop) =>
|
||||
return if @updateRequested or scrollTop is @presenter.getScrollTop()
|
||||
|
||||
animationFramePending = @pendingScrollTop?
|
||||
@pendingScrollTop = scrollTop
|
||||
unless animationFramePending
|
||||
@requestAnimationFrame =>
|
||||
pendingScrollTop = @pendingScrollTop
|
||||
@pendingScrollTop = null
|
||||
@presenter.setScrollTop(pendingScrollTop)
|
||||
@presenter.commitPendingScrollTopPosition()
|
||||
|
||||
onHorizontalScroll: (scrollLeft) =>
|
||||
return if @updateRequested or scrollLeft is @presenter.getScrollLeft()
|
||||
|
||||
animationFramePending = @pendingScrollLeft?
|
||||
@pendingScrollLeft = scrollLeft
|
||||
unless animationFramePending
|
||||
@requestAnimationFrame =>
|
||||
@presenter.setScrollLeft(@pendingScrollLeft)
|
||||
@presenter.commitPendingScrollLeftPosition()
|
||||
@pendingScrollLeft = null
|
||||
|
||||
onMouseWheel: (event) =>
|
||||
# Only scroll in one direction at a time
|
||||
{wheelDeltaX, wheelDeltaY} = event
|
||||
|
||||
if Math.abs(wheelDeltaX) > Math.abs(wheelDeltaY)
|
||||
# Scrolling horizontally
|
||||
previousScrollLeft = @presenter.getScrollLeft()
|
||||
updatedScrollLeft = previousScrollLeft - Math.round(wheelDeltaX * @editor.getScrollSensitivity() / 100)
|
||||
|
||||
event.preventDefault() if @presenter.canScrollLeftTo(updatedScrollLeft)
|
||||
@presenter.setScrollLeft(updatedScrollLeft)
|
||||
else
|
||||
# Scrolling vertically
|
||||
@presenter.setMouseWheelScreenRow(@screenRowForNode(event.target))
|
||||
previousScrollTop = @presenter.getScrollTop()
|
||||
updatedScrollTop = previousScrollTop - Math.round(wheelDeltaY * @editor.getScrollSensitivity() / 100)
|
||||
|
||||
event.preventDefault() if @presenter.canScrollTopTo(updatedScrollTop)
|
||||
@presenter.setScrollTop(updatedScrollTop)
|
||||
|
||||
onScrollViewScroll: =>
|
||||
if @mounted
|
||||
@scrollViewNode.scrollTop = 0
|
||||
@scrollViewNode.scrollLeft = 0
|
||||
|
||||
onDidChangeScrollTop: (callback) ->
|
||||
@presenter.onDidChangeScrollTop(callback)
|
||||
|
||||
onDidChangeScrollLeft: (callback) ->
|
||||
@presenter.onDidChangeScrollLeft(callback)
|
||||
|
||||
setScrollLeft: (scrollLeft) ->
|
||||
@presenter.setScrollLeft(scrollLeft)
|
||||
|
||||
setScrollRight: (scrollRight) ->
|
||||
@presenter.setScrollRight(scrollRight)
|
||||
|
||||
setScrollTop: (scrollTop) ->
|
||||
@presenter.setScrollTop(scrollTop)
|
||||
|
||||
setScrollBottom: (scrollBottom) ->
|
||||
@presenter.setScrollBottom(scrollBottom)
|
||||
|
||||
getScrollTop: ->
|
||||
@presenter.getScrollTop()
|
||||
|
||||
getScrollLeft: ->
|
||||
@presenter.getScrollLeft()
|
||||
|
||||
getScrollRight: ->
|
||||
@presenter.getScrollRight()
|
||||
|
||||
getScrollBottom: ->
|
||||
@presenter.getScrollBottom()
|
||||
|
||||
getScrollHeight: ->
|
||||
@presenter.getScrollHeight()
|
||||
|
||||
getScrollWidth: ->
|
||||
@presenter.getScrollWidth()
|
||||
|
||||
getMaxScrollTop: ->
|
||||
@presenter.getMaxScrollTop()
|
||||
|
||||
getVerticalScrollbarWidth: ->
|
||||
@presenter.getVerticalScrollbarWidth()
|
||||
|
||||
getHorizontalScrollbarHeight: ->
|
||||
@presenter.getHorizontalScrollbarHeight()
|
||||
|
||||
getVisibleRowRange: ->
|
||||
@presenter.getVisibleRowRange()
|
||||
|
||||
pixelPositionForScreenPosition: (screenPosition, clip=true) ->
|
||||
screenPosition = Point.fromObject(screenPosition)
|
||||
screenPosition = @editor.clipScreenPosition(screenPosition) if clip
|
||||
|
||||
unless @presenter.isRowRendered(screenPosition.row)
|
||||
@presenter.setScreenRowsToMeasure([screenPosition.row])
|
||||
|
||||
unless @linesComponent.lineNodeForScreenRow(screenPosition.row)?
|
||||
@updateSyncPreMeasurement()
|
||||
|
||||
pixelPosition = @linesYardstick.pixelPositionForScreenPosition(screenPosition)
|
||||
@presenter.clearScreenRowsToMeasure()
|
||||
pixelPosition
|
||||
|
||||
screenPositionForPixelPosition: (pixelPosition) ->
|
||||
row = @linesYardstick.measuredRowForPixelPosition(pixelPosition)
|
||||
if row? and not @presenter.isRowRendered(row)
|
||||
@presenter.setScreenRowsToMeasure([row])
|
||||
@updateSyncPreMeasurement()
|
||||
|
||||
position = @linesYardstick.screenPositionForPixelPosition(pixelPosition)
|
||||
@presenter.clearScreenRowsToMeasure()
|
||||
position
|
||||
|
||||
pixelRectForScreenRange: (screenRange) ->
|
||||
rowsToMeasure = []
|
||||
unless @presenter.isRowRendered(screenRange.start.row)
|
||||
rowsToMeasure.push(screenRange.start.row)
|
||||
unless @presenter.isRowRendered(screenRange.end.row)
|
||||
rowsToMeasure.push(screenRange.end.row)
|
||||
|
||||
if rowsToMeasure.length > 0
|
||||
@presenter.setScreenRowsToMeasure(rowsToMeasure)
|
||||
@updateSyncPreMeasurement()
|
||||
|
||||
rect = @presenter.absolutePixelRectForScreenRange(screenRange)
|
||||
|
||||
if rowsToMeasure.length > 0
|
||||
@presenter.clearScreenRowsToMeasure()
|
||||
|
||||
rect
|
||||
|
||||
pixelRangeForScreenRange: (screenRange, clip=true) ->
|
||||
{start, end} = Range.fromObject(screenRange)
|
||||
{start: @pixelPositionForScreenPosition(start, clip), end: @pixelPositionForScreenPosition(end, clip)}
|
||||
|
||||
pixelPositionForBufferPosition: (bufferPosition) ->
|
||||
@pixelPositionForScreenPosition(
|
||||
@editor.screenPositionForBufferPosition(bufferPosition)
|
||||
)
|
||||
|
||||
invalidateBlockDecorationDimensions: ->
|
||||
@presenter.invalidateBlockDecorationDimensions(arguments...)
|
||||
|
||||
onMouseDown: (event) =>
|
||||
# Handle middle mouse button on linux platform only (paste clipboard)
|
||||
if event.button is 1 and process.platform is 'linux'
|
||||
if selection = require('./safe-clipboard').readText('selection')
|
||||
screenPosition = @screenPositionForMouseEvent(event)
|
||||
@editor.setCursorScreenPosition(screenPosition, autoscroll: false)
|
||||
@editor.insertText(selection)
|
||||
return
|
||||
|
||||
# Handle mouse down events for left mouse button only
|
||||
# (except middle mouse button on linux platform, see above)
|
||||
unless event.button is 0
|
||||
return
|
||||
|
||||
return if event.target?.classList.contains('horizontal-scrollbar')
|
||||
|
||||
{detail, shiftKey, metaKey, ctrlKey} = event
|
||||
|
||||
# CTRL+click brings up the context menu on macOS, so don't handle those either
|
||||
return if ctrlKey and process.platform is 'darwin'
|
||||
|
||||
# Prevent focusout event on hidden input if editor is already focused
|
||||
event.preventDefault() if @oldState.focused
|
||||
|
||||
screenPosition = @screenPositionForMouseEvent(event)
|
||||
|
||||
if event.target?.classList.contains('fold-marker')
|
||||
bufferPosition = @editor.bufferPositionForScreenPosition(screenPosition)
|
||||
@editor.destroyFoldsIntersectingBufferRange([bufferPosition, bufferPosition])
|
||||
return
|
||||
|
||||
switch detail
|
||||
when 1
|
||||
if shiftKey
|
||||
@editor.selectToScreenPosition(screenPosition)
|
||||
else if metaKey or (ctrlKey and process.platform isnt 'darwin')
|
||||
cursorAtScreenPosition = @editor.getCursorAtScreenPosition(screenPosition)
|
||||
if cursorAtScreenPosition and @editor.hasMultipleCursors()
|
||||
cursorAtScreenPosition.destroy()
|
||||
else
|
||||
@editor.addCursorAtScreenPosition(screenPosition, autoscroll: false)
|
||||
else
|
||||
@editor.setCursorScreenPosition(screenPosition, autoscroll: false)
|
||||
when 2
|
||||
@editor.getLastSelection().selectWord(autoscroll: false)
|
||||
when 3
|
||||
@editor.getLastSelection().selectLine(null, autoscroll: false)
|
||||
|
||||
@handleDragUntilMouseUp (screenPosition) =>
|
||||
@editor.selectToScreenPosition(screenPosition, suppressSelectionMerge: true, autoscroll: false)
|
||||
|
||||
onLineNumberGutterMouseDown: (event) =>
|
||||
return unless event.button is 0 # only handle the left mouse button
|
||||
|
||||
{shiftKey, metaKey, ctrlKey} = event
|
||||
|
||||
if shiftKey
|
||||
@onGutterShiftClick(event)
|
||||
else if metaKey or (ctrlKey and process.platform isnt 'darwin')
|
||||
@onGutterMetaClick(event)
|
||||
else
|
||||
@onGutterClick(event)
|
||||
|
||||
onGutterClick: (event) =>
|
||||
clickedScreenRow = @screenPositionForMouseEvent(event).row
|
||||
clickedBufferRow = @editor.bufferRowForScreenRow(clickedScreenRow)
|
||||
initialScreenRange = @editor.screenRangeForBufferRange([[clickedBufferRow, 0], [clickedBufferRow + 1, 0]])
|
||||
@editor.setSelectedScreenRange(initialScreenRange, preserveFolds: true, autoscroll: false)
|
||||
@handleGutterDrag(initialScreenRange)
|
||||
|
||||
onGutterMetaClick: (event) =>
|
||||
clickedScreenRow = @screenPositionForMouseEvent(event).row
|
||||
clickedBufferRow = @editor.bufferRowForScreenRow(clickedScreenRow)
|
||||
initialScreenRange = @editor.screenRangeForBufferRange([[clickedBufferRow, 0], [clickedBufferRow + 1, 0]])
|
||||
@editor.addSelectionForScreenRange(initialScreenRange, autoscroll: false)
|
||||
@handleGutterDrag(initialScreenRange)
|
||||
|
||||
onGutterShiftClick: (event) =>
|
||||
tailScreenPosition = @editor.getLastSelection().getTailScreenPosition()
|
||||
clickedScreenRow = @screenPositionForMouseEvent(event).row
|
||||
clickedBufferRow = @editor.bufferRowForScreenRow(clickedScreenRow)
|
||||
clickedLineScreenRange = @editor.screenRangeForBufferRange([[clickedBufferRow, 0], [clickedBufferRow + 1, 0]])
|
||||
|
||||
if clickedScreenRow < tailScreenPosition.row
|
||||
@editor.selectToScreenPosition(clickedLineScreenRange.start, suppressSelectionMerge: true, autoscroll: false)
|
||||
else
|
||||
@editor.selectToScreenPosition(clickedLineScreenRange.end, suppressSelectionMerge: true, autoscroll: false)
|
||||
|
||||
@handleGutterDrag(new Range(tailScreenPosition, tailScreenPosition))
|
||||
|
||||
handleGutterDrag: (initialRange) ->
|
||||
@handleDragUntilMouseUp (screenPosition) =>
|
||||
dragRow = screenPosition.row
|
||||
if dragRow < initialRange.start.row
|
||||
startPosition = @editor.clipScreenPosition([dragRow, 0], skipSoftWrapIndentation: true)
|
||||
screenRange = new Range(startPosition, startPosition).union(initialRange)
|
||||
@editor.getLastSelection().setScreenRange(screenRange, reversed: true, autoscroll: false, preserveFolds: true)
|
||||
else
|
||||
endPosition = @editor.clipScreenPosition([dragRow + 1, 0], clipDirection: 'backward')
|
||||
screenRange = new Range(endPosition, endPosition).union(initialRange)
|
||||
@editor.getLastSelection().setScreenRange(screenRange, reversed: false, autoscroll: false, preserveFolds: true)
|
||||
|
||||
onStylesheetsChanged: (styleElement) =>
|
||||
return unless @performedInitialMeasurement
|
||||
return unless @themes.isInitialLoadComplete()
|
||||
|
||||
# This delay prevents the styling from going haywire when stylesheets are
|
||||
# reloaded in dev mode. It seems like a workaround for a browser bug, but
|
||||
# not totally sure.
|
||||
|
||||
unless @stylingChangeAnimationFrameRequested
|
||||
@stylingChangeAnimationFrameRequested = true
|
||||
requestAnimationFrame =>
|
||||
@stylingChangeAnimationFrameRequested = false
|
||||
if @mounted
|
||||
@refreshScrollbars() if not styleElement.sheet? or @containsScrollbarSelector(styleElement.sheet)
|
||||
@handleStylingChange()
|
||||
|
||||
onAllThemesLoaded: =>
|
||||
@refreshScrollbars()
|
||||
@handleStylingChange()
|
||||
|
||||
handleStylingChange: =>
|
||||
@sampleFontStyling()
|
||||
@sampleBackgroundColors()
|
||||
@invalidateMeasurements()
|
||||
|
||||
handleDragUntilMouseUp: (dragHandler) ->
|
||||
dragging = false
|
||||
lastMousePosition = {}
|
||||
animationLoop = =>
|
||||
@requestAnimationFrame =>
|
||||
if dragging and @mounted
|
||||
linesClientRect = @linesComponent.getDomNode().getBoundingClientRect()
|
||||
autoscroll(lastMousePosition, linesClientRect)
|
||||
screenPosition = @screenPositionForMouseEvent(lastMousePosition, linesClientRect)
|
||||
dragHandler(screenPosition)
|
||||
animationLoop()
|
||||
else if not @mounted
|
||||
stopDragging()
|
||||
|
||||
onMouseMove = (event) ->
|
||||
lastMousePosition.clientX = event.clientX
|
||||
lastMousePosition.clientY = event.clientY
|
||||
|
||||
# Start the animation loop when the mouse moves prior to a mouseup event
|
||||
unless dragging
|
||||
dragging = true
|
||||
animationLoop()
|
||||
|
||||
# Stop dragging when cursor enters dev tools because we can't detect mouseup
|
||||
onMouseUp() if event.which is 0
|
||||
|
||||
onMouseUp = (event) =>
|
||||
if dragging
|
||||
stopDragging()
|
||||
@editor.finalizeSelections()
|
||||
@editor.mergeIntersectingSelections()
|
||||
|
||||
stopDragging = ->
|
||||
dragging = false
|
||||
window.removeEventListener('mousemove', onMouseMove)
|
||||
window.removeEventListener('mouseup', onMouseUp)
|
||||
disposables.dispose()
|
||||
|
||||
autoscroll = (mouseClientPosition) =>
|
||||
{top, bottom, left, right} = @scrollViewNode.getBoundingClientRect()
|
||||
top += 30
|
||||
bottom -= 30
|
||||
left += 30
|
||||
right -= 30
|
||||
|
||||
if mouseClientPosition.clientY < top
|
||||
mouseYDelta = top - mouseClientPosition.clientY
|
||||
yDirection = -1
|
||||
else if mouseClientPosition.clientY > bottom
|
||||
mouseYDelta = mouseClientPosition.clientY - bottom
|
||||
yDirection = 1
|
||||
|
||||
if mouseClientPosition.clientX < left
|
||||
mouseXDelta = left - mouseClientPosition.clientX
|
||||
xDirection = -1
|
||||
else if mouseClientPosition.clientX > right
|
||||
mouseXDelta = mouseClientPosition.clientX - right
|
||||
xDirection = 1
|
||||
|
||||
if mouseYDelta?
|
||||
@presenter.setScrollTop(@presenter.getScrollTop() + yDirection * scaleScrollDelta(mouseYDelta))
|
||||
@presenter.commitPendingScrollTopPosition()
|
||||
|
||||
if mouseXDelta?
|
||||
@presenter.setScrollLeft(@presenter.getScrollLeft() + xDirection * scaleScrollDelta(mouseXDelta))
|
||||
@presenter.commitPendingScrollLeftPosition()
|
||||
|
||||
scaleScrollDelta = (scrollDelta) ->
|
||||
Math.pow(scrollDelta / 2, 3) / 280
|
||||
|
||||
window.addEventListener('mousemove', onMouseMove)
|
||||
window.addEventListener('mouseup', onMouseUp)
|
||||
disposables = new CompositeDisposable
|
||||
disposables.add(@editor.getBuffer().onWillChange(onMouseUp))
|
||||
disposables.add(@editor.onDidDestroy(stopDragging))
|
||||
|
||||
isVisible: ->
|
||||
# Investigating an exception that occurs here due to ::domNode being null.
|
||||
@assert @domNode?, "TextEditorComponent::domNode was null.", (error) =>
|
||||
error.metadata = {@initialized}
|
||||
|
||||
@domNode? and (@domNode.offsetHeight > 0 or @domNode.offsetWidth > 0)
|
||||
|
||||
pollDOM: =>
|
||||
unless @checkForVisibilityChange()
|
||||
@sampleBackgroundColors()
|
||||
@measureWindowSize()
|
||||
@measureDimensions()
|
||||
@sampleFontStyling()
|
||||
@overlayManager?.measureOverlays()
|
||||
|
||||
checkForVisibilityChange: ->
|
||||
if @isVisible()
|
||||
if @wasVisible
|
||||
false
|
||||
else
|
||||
@becameVisible()
|
||||
@wasVisible = true
|
||||
else
|
||||
@wasVisible = false
|
||||
|
||||
# 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.
|
||||
measureDimensions: ->
|
||||
# If we don't assign autoHeight explicitly, we try to automatically disable
|
||||
# auto-height in certain circumstances. This is legacy behavior that we
|
||||
# would rather not implement, but we can't remove it without risking
|
||||
# breakage currently.
|
||||
unless @editor.autoHeight?
|
||||
{position, top, bottom} = getComputedStyle(@hostElement)
|
||||
hasExplicitTopAndBottom = (position is 'absolute' and top isnt 'auto' and bottom isnt 'auto')
|
||||
hasInlineHeight = @hostElement.style.height.length > 0
|
||||
|
||||
if hasInlineHeight or hasExplicitTopAndBottom
|
||||
if @presenter.autoHeight
|
||||
@presenter.setAutoHeight(false)
|
||||
if hasExplicitTopAndBottom
|
||||
Grim.deprecate("""
|
||||
Assigning editor #{@editor.id}'s height explicitly via `position: 'absolute'` and an assigned `top` and `bottom` implicitly assigns the `autoHeight` property to false on the editor.
|
||||
This behavior is deprecated and will not be supported in the future. Please explicitly assign `autoHeight` on this editor.
|
||||
""")
|
||||
else if hasInlineHeight
|
||||
Grim.deprecate("""
|
||||
Assigning editor #{@editor.id}'s height explicitly via an inline style implicitly assigns the `autoHeight` property to false on the editor.
|
||||
This behavior is deprecated and will not be supported in the future. Please explicitly assign `autoHeight` on this editor.
|
||||
""")
|
||||
else
|
||||
@presenter.setAutoHeight(true)
|
||||
|
||||
if @presenter.autoHeight
|
||||
@presenter.setExplicitHeight(null)
|
||||
else if @hostElement.offsetHeight > 0
|
||||
@presenter.setExplicitHeight(@hostElement.offsetHeight)
|
||||
|
||||
clientWidth = @scrollViewNode.clientWidth
|
||||
paddingLeft = parseInt(getComputedStyle(@scrollViewNode).paddingLeft)
|
||||
clientWidth -= paddingLeft
|
||||
if clientWidth > 0
|
||||
@presenter.setContentFrameWidth(clientWidth)
|
||||
|
||||
@presenter.setGutterWidth(@gutterContainerComponent?.getDomNode().offsetWidth ? 0)
|
||||
@presenter.setBoundingClientRect(@hostElement.getBoundingClientRect())
|
||||
|
||||
measureWindowSize: ->
|
||||
return unless @mounted
|
||||
|
||||
# FIXME: on Ubuntu (via xvfb) `window.innerWidth` reports an incorrect value
|
||||
# when window gets resized through `atom.setWindowDimensions({width:
|
||||
# windowWidth, height: windowHeight})`.
|
||||
@presenter.setWindowSize(window.innerWidth, window.innerHeight)
|
||||
|
||||
sampleFontStyling: =>
|
||||
oldFontSize = @fontSize
|
||||
oldFontFamily = @fontFamily
|
||||
oldLineHeight = @lineHeight
|
||||
|
||||
{@fontSize, @fontFamily, @lineHeight} = getComputedStyle(@getTopmostDOMNode())
|
||||
|
||||
if @fontSize isnt oldFontSize or @fontFamily isnt oldFontFamily or @lineHeight isnt oldLineHeight
|
||||
@clearPoolAfterUpdate = true
|
||||
@measureLineHeightAndDefaultCharWidth()
|
||||
@invalidateMeasurements()
|
||||
|
||||
sampleBackgroundColors: (suppressUpdate) ->
|
||||
{backgroundColor} = getComputedStyle(@hostElement)
|
||||
@presenter.setBackgroundColor(backgroundColor)
|
||||
|
||||
lineNumberGutter = @gutterContainerComponent?.getLineNumberGutterComponent()
|
||||
if lineNumberGutter
|
||||
gutterBackgroundColor = getComputedStyle(lineNumberGutter.getDomNode()).backgroundColor
|
||||
@presenter.setGutterBackgroundColor(gutterBackgroundColor)
|
||||
|
||||
measureLineHeightAndDefaultCharWidth: ->
|
||||
if @isVisible()
|
||||
@measureLineHeightAndDefaultCharWidthWhenShown = false
|
||||
@linesComponent.measureLineHeightAndDefaultCharWidth()
|
||||
else
|
||||
@measureLineHeightAndDefaultCharWidthWhenShown = true
|
||||
|
||||
measureScrollbars: ->
|
||||
@measureScrollbarsWhenShown = false
|
||||
|
||||
cornerNode = @scrollbarCornerComponent.getDomNode()
|
||||
originalDisplayValue = cornerNode.style.display
|
||||
|
||||
cornerNode.style.display = 'block'
|
||||
|
||||
width = (cornerNode.offsetWidth - cornerNode.clientWidth) or 15
|
||||
height = (cornerNode.offsetHeight - cornerNode.clientHeight) or 15
|
||||
|
||||
@presenter.setVerticalScrollbarWidth(width)
|
||||
@presenter.setHorizontalScrollbarHeight(height)
|
||||
|
||||
cornerNode.style.display = originalDisplayValue
|
||||
|
||||
containsScrollbarSelector: (stylesheet) ->
|
||||
for rule in stylesheet.cssRules
|
||||
if rule.selectorText?.indexOf('scrollbar') > -1
|
||||
return true
|
||||
false
|
||||
|
||||
refreshScrollbars: =>
|
||||
if @isVisible()
|
||||
@measureScrollbarsWhenShown = false
|
||||
else
|
||||
@measureScrollbarsWhenShown = true
|
||||
return
|
||||
|
||||
verticalNode = @verticalScrollbarComponent.getDomNode()
|
||||
horizontalNode = @horizontalScrollbarComponent.getDomNode()
|
||||
cornerNode = @scrollbarCornerComponent.getDomNode()
|
||||
|
||||
originalVerticalDisplayValue = verticalNode.style.display
|
||||
originalHorizontalDisplayValue = horizontalNode.style.display
|
||||
originalCornerDisplayValue = cornerNode.style.display
|
||||
|
||||
# First, hide all scrollbars in case they are visible so they take on new
|
||||
# styles when they are shown again.
|
||||
verticalNode.style.display = 'none'
|
||||
horizontalNode.style.display = 'none'
|
||||
cornerNode.style.display = 'none'
|
||||
|
||||
# Force a reflow
|
||||
cornerNode.offsetWidth
|
||||
|
||||
# Now measure the new scrollbar dimensions
|
||||
@measureScrollbars()
|
||||
|
||||
# Now restore the display value for all scrollbars, since they were
|
||||
# previously hidden
|
||||
verticalNode.style.display = originalVerticalDisplayValue
|
||||
horizontalNode.style.display = originalHorizontalDisplayValue
|
||||
cornerNode.style.display = originalCornerDisplayValue
|
||||
|
||||
consolidateSelections: (e) ->
|
||||
e.abortKeyBinding() unless @editor.consolidateSelections()
|
||||
|
||||
lineNodeForScreenRow: (screenRow) ->
|
||||
@linesComponent.lineNodeForScreenRow(screenRow)
|
||||
|
||||
lineNumberNodeForScreenRow: (screenRow) ->
|
||||
tileRow = @presenter.tileForRow(screenRow)
|
||||
gutterComponent = @gutterContainerComponent.getLineNumberGutterComponent()
|
||||
tileComponent = gutterComponent.getComponentForTile(tileRow)
|
||||
|
||||
tileComponent?.lineNumberNodeForScreenRow(screenRow)
|
||||
|
||||
tileNodesForLines: ->
|
||||
@linesComponent.getTiles()
|
||||
|
||||
tileNodesForLineNumbers: ->
|
||||
gutterComponent = @gutterContainerComponent.getLineNumberGutterComponent()
|
||||
gutterComponent.getTiles()
|
||||
|
||||
screenRowForNode: (node) ->
|
||||
while node?
|
||||
if screenRow = node.dataset?.screenRow
|
||||
return parseInt(screenRow)
|
||||
node = node.parentElement
|
||||
null
|
||||
|
||||
getFontSize: ->
|
||||
parseInt(getComputedStyle(@getTopmostDOMNode()).fontSize)
|
||||
|
||||
setFontSize: (fontSize) ->
|
||||
@getTopmostDOMNode().style.fontSize = fontSize + 'px'
|
||||
@sampleFontStyling()
|
||||
@invalidateMeasurements()
|
||||
|
||||
getFontFamily: ->
|
||||
getComputedStyle(@getTopmostDOMNode()).fontFamily
|
||||
|
||||
setFontFamily: (fontFamily) ->
|
||||
@getTopmostDOMNode().style.fontFamily = fontFamily
|
||||
@sampleFontStyling()
|
||||
@invalidateMeasurements()
|
||||
|
||||
setLineHeight: (lineHeight) ->
|
||||
@getTopmostDOMNode().style.lineHeight = lineHeight
|
||||
@sampleFontStyling()
|
||||
@invalidateMeasurements()
|
||||
|
||||
invalidateMeasurements: ->
|
||||
@linesYardstick.invalidateCache()
|
||||
@presenter.measurementsChanged()
|
||||
|
||||
screenPositionForMouseEvent: (event, linesClientRect) ->
|
||||
pixelPosition = @pixelPositionForMouseEvent(event, linesClientRect)
|
||||
@screenPositionForPixelPosition(pixelPosition)
|
||||
|
||||
pixelPositionForMouseEvent: (event, linesClientRect) ->
|
||||
{clientX, clientY} = event
|
||||
|
||||
linesClientRect ?= @linesComponent.getDomNode().getBoundingClientRect()
|
||||
top = clientY - linesClientRect.top + @presenter.getRealScrollTop()
|
||||
left = clientX - linesClientRect.left + @presenter.getRealScrollLeft()
|
||||
bottom = linesClientRect.top + @presenter.getRealScrollTop() + linesClientRect.height - clientY
|
||||
right = linesClientRect.left + @presenter.getRealScrollLeft() + linesClientRect.width - clientX
|
||||
|
||||
{top, left, bottom, right}
|
||||
|
||||
getGutterWidth: ->
|
||||
@presenter.getGutterWidth()
|
||||
|
||||
getModel: ->
|
||||
@editor
|
||||
|
||||
isInputEnabled: -> @inputEnabled
|
||||
|
||||
setInputEnabled: (@inputEnabled) -> @inputEnabled
|
||||
|
||||
setContinuousReflow: (continuousReflow) ->
|
||||
@presenter.setContinuousReflow(continuousReflow)
|
||||
|
||||
updateParentViewFocusedClassIfNeeded: ->
|
||||
if @oldState.focused isnt @newState.focused
|
||||
@hostElement.classList.toggle('is-focused', @newState.focused)
|
||||
@oldState.focused = @newState.focused
|
||||
|
||||
updateParentViewMiniClass: ->
|
||||
@hostElement.classList.toggle('mini', @editor.isMini())
|
||||
3968
src/text-editor-component.js
Normal file
3968
src/text-editor-component.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,331 +0,0 @@
|
||||
Grim = require 'grim'
|
||||
{Emitter, CompositeDisposable} = require 'event-kit'
|
||||
TextBuffer = require 'text-buffer'
|
||||
TextEditorComponent = require './text-editor-component'
|
||||
|
||||
class TextEditorElement extends HTMLElement
|
||||
model: null
|
||||
componentDescriptor: null
|
||||
component: null
|
||||
attached: false
|
||||
tileSize: null
|
||||
focusOnAttach: false
|
||||
hasTiledRendering: true
|
||||
logicalDisplayBuffer: true
|
||||
lightDOM: true
|
||||
|
||||
createdCallback: ->
|
||||
# Use globals when the following instance variables aren't set.
|
||||
@themes = atom.themes
|
||||
@workspace = atom.workspace
|
||||
@assert = atom.assert
|
||||
@views = atom.views
|
||||
@styles = atom.styles
|
||||
|
||||
@emitter = new Emitter
|
||||
@subscriptions = new CompositeDisposable
|
||||
|
||||
@hiddenInputElement = document.createElement('input')
|
||||
@hiddenInputElement.classList.add('hidden-input')
|
||||
@hiddenInputElement.setAttribute('tabindex', -1)
|
||||
@hiddenInputElement.setAttribute('data-react-skip-selection-restoration', true)
|
||||
@hiddenInputElement.style['-webkit-transform'] = 'translateZ(0)'
|
||||
@hiddenInputElement.addEventListener 'paste', (event) -> event.preventDefault()
|
||||
|
||||
@addEventListener 'focus', @focused.bind(this)
|
||||
@addEventListener 'blur', @blurred.bind(this)
|
||||
@hiddenInputElement.addEventListener 'focus', @focused.bind(this)
|
||||
@hiddenInputElement.addEventListener 'blur', @inputNodeBlurred.bind(this)
|
||||
|
||||
@classList.add('editor')
|
||||
@setAttribute('tabindex', -1)
|
||||
|
||||
initializeContent: (attributes) ->
|
||||
Object.defineProperty(this, 'shadowRoot', {
|
||||
get: =>
|
||||
Grim.deprecate("""
|
||||
The contents of `atom-text-editor` elements are no longer encapsulated
|
||||
within a shadow DOM boundary. Please, stop using `shadowRoot` and access
|
||||
the editor contents directly instead.
|
||||
""")
|
||||
this
|
||||
})
|
||||
@rootElement = document.createElement('div')
|
||||
@rootElement.classList.add('editor--private')
|
||||
@appendChild(@rootElement)
|
||||
|
||||
attachedCallback: ->
|
||||
@buildModel() unless @getModel()?
|
||||
@assert(@model.isAlive(), "Attaching a view for a destroyed editor")
|
||||
@mountComponent() unless @component?
|
||||
@listenForComponentEvents()
|
||||
@component.checkForVisibilityChange()
|
||||
if @hasFocus()
|
||||
@focused()
|
||||
@emitter.emit("did-attach")
|
||||
|
||||
detachedCallback: ->
|
||||
@unmountComponent()
|
||||
@subscriptions.dispose()
|
||||
@subscriptions = new CompositeDisposable
|
||||
@emitter.emit("did-detach")
|
||||
|
||||
listenForComponentEvents: ->
|
||||
@subscriptions.add @component.onDidChangeScrollTop =>
|
||||
@emitter.emit("did-change-scroll-top", arguments...)
|
||||
@subscriptions.add @component.onDidChangeScrollLeft =>
|
||||
@emitter.emit("did-change-scroll-left", arguments...)
|
||||
|
||||
initialize: (model, {@views, @themes, @workspace, @assert, @styles}) ->
|
||||
throw new Error("Must pass a views parameter when initializing TextEditorElements") unless @views?
|
||||
throw new Error("Must pass a themes parameter when initializing TextEditorElements") unless @themes?
|
||||
throw new Error("Must pass a workspace parameter when initializing TextEditorElements") unless @workspace?
|
||||
throw new Error("Must pass an assert parameter when initializing TextEditorElements") unless @assert?
|
||||
throw new Error("Must pass a styles parameter when initializing TextEditorElements") unless @styles?
|
||||
|
||||
@setModel(model)
|
||||
this
|
||||
|
||||
setModel: (model) ->
|
||||
throw new Error("Model already assigned on TextEditorElement") if @model?
|
||||
return if model.isDestroyed()
|
||||
|
||||
@model = model
|
||||
@model.setUpdatedSynchronously(@isUpdatedSynchronously())
|
||||
@initializeContent()
|
||||
@mountComponent()
|
||||
@addGrammarScopeAttribute()
|
||||
@addMiniAttribute() if @model.isMini()
|
||||
@addEncodingAttribute()
|
||||
@model.onDidChangeGrammar => @addGrammarScopeAttribute()
|
||||
@model.onDidChangeEncoding => @addEncodingAttribute()
|
||||
@model.onDidDestroy => @unmountComponent()
|
||||
@model.onDidChangeMini (mini) => if mini then @addMiniAttribute() else @removeMiniAttribute()
|
||||
@model
|
||||
|
||||
getModel: ->
|
||||
@model ? @buildModel()
|
||||
|
||||
buildModel: ->
|
||||
@setModel(@workspace.buildTextEditor(
|
||||
buffer: new TextBuffer({
|
||||
text: @textContent
|
||||
shouldDestroyOnFileDelete:
|
||||
-> atom.config.get('core.closeDeletedFileTabs')})
|
||||
softWrapped: false
|
||||
tabLength: 2
|
||||
softTabs: true
|
||||
mini: @hasAttribute('mini')
|
||||
lineNumberGutterVisible: not @hasAttribute('gutter-hidden')
|
||||
placeholderText: @getAttribute('placeholder-text')
|
||||
))
|
||||
|
||||
mountComponent: ->
|
||||
@component = new TextEditorComponent(
|
||||
hostElement: this
|
||||
editor: @model
|
||||
tileSize: @tileSize
|
||||
views: @views
|
||||
themes: @themes
|
||||
styles: @styles
|
||||
workspace: @workspace
|
||||
assert: @assert,
|
||||
hiddenInputElement: @hiddenInputElement
|
||||
)
|
||||
@rootElement.appendChild(@component.getDomNode())
|
||||
|
||||
unmountComponent: ->
|
||||
if @component?
|
||||
@component.destroy()
|
||||
@component.getDomNode().remove()
|
||||
@component = null
|
||||
|
||||
focused: (event) ->
|
||||
@component?.focused()
|
||||
@hiddenInputElement.focus()
|
||||
|
||||
blurred: (event) ->
|
||||
if event.relatedTarget is @hiddenInputElement
|
||||
event.stopImmediatePropagation()
|
||||
return
|
||||
@component?.blurred()
|
||||
|
||||
inputNodeBlurred: (event) ->
|
||||
if event.relatedTarget isnt this
|
||||
@dispatchEvent(new FocusEvent('blur', relatedTarget: event.relatedTarget, bubbles: false))
|
||||
|
||||
addGrammarScopeAttribute: ->
|
||||
@dataset.grammar = @model.getGrammar()?.scopeName?.replace(/\./g, ' ')
|
||||
|
||||
addMiniAttribute: ->
|
||||
@setAttributeNode(document.createAttribute("mini"))
|
||||
|
||||
removeMiniAttribute: ->
|
||||
@removeAttribute("mini")
|
||||
|
||||
addEncodingAttribute: ->
|
||||
@dataset.encoding = @model.getEncoding()
|
||||
|
||||
hasFocus: ->
|
||||
this is document.activeElement or @contains(document.activeElement)
|
||||
|
||||
setUpdatedSynchronously: (@updatedSynchronously) ->
|
||||
@model?.setUpdatedSynchronously(@updatedSynchronously)
|
||||
@updatedSynchronously
|
||||
|
||||
isUpdatedSynchronously: -> @updatedSynchronously
|
||||
|
||||
# Extended: Continuously reflows lines and line numbers. (Has performance overhead)
|
||||
#
|
||||
# * `continuousReflow` A {Boolean} indicating whether to keep reflowing or not.
|
||||
setContinuousReflow: (continuousReflow) ->
|
||||
@component?.setContinuousReflow(continuousReflow)
|
||||
|
||||
# Extended: get the width of a character of text displayed in this element.
|
||||
#
|
||||
# Returns a {Number} of pixels.
|
||||
getDefaultCharacterWidth: ->
|
||||
@getModel().getDefaultCharWidth()
|
||||
|
||||
# Extended: Get the maximum scroll top that can be applied to this element.
|
||||
#
|
||||
# Returns a {Number} of pixels.
|
||||
getMaxScrollTop: ->
|
||||
@component?.getMaxScrollTop()
|
||||
|
||||
# Extended: Converts a buffer position to a pixel position.
|
||||
#
|
||||
# * `bufferPosition` An object that represents a buffer position. It can be either
|
||||
# an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point}
|
||||
#
|
||||
# Returns an {Object} with two values: `top` and `left`, representing the pixel position.
|
||||
pixelPositionForBufferPosition: (bufferPosition) ->
|
||||
@component.pixelPositionForBufferPosition(bufferPosition)
|
||||
|
||||
# Extended: Converts a screen position to a pixel position.
|
||||
#
|
||||
# * `screenPosition` An object that represents a screen position. It can be either
|
||||
# an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point}
|
||||
#
|
||||
# Returns an {Object} with two values: `top` and `left`, representing the pixel positions.
|
||||
pixelPositionForScreenPosition: (screenPosition) ->
|
||||
@component.pixelPositionForScreenPosition(screenPosition)
|
||||
|
||||
# Extended: Retrieves the number of the row that is visible and currently at the
|
||||
# top of the editor.
|
||||
#
|
||||
# Returns a {Number}.
|
||||
getFirstVisibleScreenRow: ->
|
||||
@getVisibleRowRange()[0]
|
||||
|
||||
# Extended: Retrieves the number of the row that is visible and currently at the
|
||||
# bottom of the editor.
|
||||
#
|
||||
# Returns a {Number}.
|
||||
getLastVisibleScreenRow: ->
|
||||
@getVisibleRowRange()[1]
|
||||
|
||||
# Extended: call the given `callback` when the editor is attached to the DOM.
|
||||
#
|
||||
# * `callback` {Function}
|
||||
onDidAttach: (callback) ->
|
||||
@emitter.on("did-attach", callback)
|
||||
|
||||
# Extended: call the given `callback` when the editor is detached from the DOM.
|
||||
#
|
||||
# * `callback` {Function}
|
||||
onDidDetach: (callback) ->
|
||||
@emitter.on("did-detach", callback)
|
||||
|
||||
onDidChangeScrollTop: (callback) ->
|
||||
@emitter.on("did-change-scroll-top", callback)
|
||||
|
||||
onDidChangeScrollLeft: (callback) ->
|
||||
@emitter.on("did-change-scroll-left", callback)
|
||||
|
||||
setScrollLeft: (scrollLeft) ->
|
||||
@component.setScrollLeft(scrollLeft)
|
||||
|
||||
setScrollRight: (scrollRight) ->
|
||||
@component.setScrollRight(scrollRight)
|
||||
|
||||
setScrollTop: (scrollTop) ->
|
||||
@component.setScrollTop(scrollTop)
|
||||
|
||||
setScrollBottom: (scrollBottom) ->
|
||||
@component.setScrollBottom(scrollBottom)
|
||||
|
||||
# Essential: Scrolls the editor to the top
|
||||
scrollToTop: ->
|
||||
@setScrollTop(0)
|
||||
|
||||
# Essential: Scrolls the editor to the bottom
|
||||
scrollToBottom: ->
|
||||
@setScrollBottom(Infinity)
|
||||
|
||||
getScrollTop: ->
|
||||
@component?.getScrollTop() or 0
|
||||
|
||||
getScrollLeft: ->
|
||||
@component?.getScrollLeft() or 0
|
||||
|
||||
getScrollRight: ->
|
||||
@component?.getScrollRight() or 0
|
||||
|
||||
getScrollBottom: ->
|
||||
@component?.getScrollBottom() or 0
|
||||
|
||||
getScrollHeight: ->
|
||||
@component?.getScrollHeight() or 0
|
||||
|
||||
getScrollWidth: ->
|
||||
@component?.getScrollWidth() or 0
|
||||
|
||||
getVerticalScrollbarWidth: ->
|
||||
@component?.getVerticalScrollbarWidth() or 0
|
||||
|
||||
getHorizontalScrollbarHeight: ->
|
||||
@component?.getHorizontalScrollbarHeight() or 0
|
||||
|
||||
getVisibleRowRange: ->
|
||||
@component?.getVisibleRowRange() or [0, 0]
|
||||
|
||||
intersectsVisibleRowRange: (startRow, endRow) ->
|
||||
[visibleStart, visibleEnd] = @getVisibleRowRange()
|
||||
not (endRow <= visibleStart or visibleEnd <= startRow)
|
||||
|
||||
selectionIntersectsVisibleRowRange: (selection) ->
|
||||
{start, end} = selection.getScreenRange()
|
||||
@intersectsVisibleRowRange(start.row, end.row + 1)
|
||||
|
||||
screenPositionForPixelPosition: (pixelPosition) ->
|
||||
@component.screenPositionForPixelPosition(pixelPosition)
|
||||
|
||||
pixelRectForScreenRange: (screenRange) ->
|
||||
@component.pixelRectForScreenRange(screenRange)
|
||||
|
||||
pixelRangeForScreenRange: (screenRange) ->
|
||||
@component.pixelRangeForScreenRange(screenRange)
|
||||
|
||||
setWidth: (width) ->
|
||||
@style.width = (@component.getGutterWidth() + width) + "px"
|
||||
|
||||
getWidth: ->
|
||||
@offsetWidth - @component.getGutterWidth()
|
||||
|
||||
setHeight: (height) ->
|
||||
@style.height = height + "px"
|
||||
|
||||
getHeight: ->
|
||||
@offsetHeight
|
||||
|
||||
# Experimental: Invalidate the passed block {Decoration} dimensions, forcing
|
||||
# them to be recalculated and the surrounding content to be adjusted on the
|
||||
# next animation frame.
|
||||
#
|
||||
# * {blockDecoration} A {Decoration} representing the block decoration you
|
||||
# want to update the dimensions of.
|
||||
invalidateBlockDecorationDimensions: ->
|
||||
@component.invalidateBlockDecorationDimensions(arguments...)
|
||||
|
||||
module.exports = TextEditorElement = document.registerElement 'atom-text-editor', prototype: TextEditorElement.prototype
|
||||
346
src/text-editor-element.js
Normal file
346
src/text-editor-element.js
Normal file
@@ -0,0 +1,346 @@
|
||||
const {Emitter, Range} = require('atom')
|
||||
const Grim = require('grim')
|
||||
const TextEditorComponent = require('./text-editor-component')
|
||||
const dedent = require('dedent')
|
||||
|
||||
class TextEditorElement extends HTMLElement {
|
||||
initialize (component) {
|
||||
this.component = component
|
||||
return this
|
||||
}
|
||||
|
||||
get shadowRoot () {
|
||||
Grim.deprecate(dedent`
|
||||
The contents of \`atom-text-editor\` elements are no longer encapsulated
|
||||
within a shadow DOM boundary. Please, stop using \`shadowRoot\` and access
|
||||
the editor contents directly instead.
|
||||
`)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
get rootElement () {
|
||||
Grim.deprecate(dedent`
|
||||
The contents of \`atom-text-editor\` elements are no longer encapsulated
|
||||
within a shadow DOM boundary. Please, stop using \`rootElement\` and access
|
||||
the editor contents directly instead.
|
||||
`)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
createdCallback () {
|
||||
this.emitter = new Emitter()
|
||||
this.initialText = this.textContent
|
||||
this.tabIndex = -1
|
||||
this.addEventListener('focus', (event) => this.getComponent().didFocus(event))
|
||||
this.addEventListener('blur', (event) => this.getComponent().didBlur(event))
|
||||
}
|
||||
|
||||
attachedCallback () {
|
||||
this.getComponent().didAttach()
|
||||
this.emitter.emit('did-attach')
|
||||
}
|
||||
|
||||
detachedCallback () {
|
||||
this.emitter.emit('did-detach')
|
||||
this.getComponent().didDetach()
|
||||
}
|
||||
|
||||
attributeChangedCallback (name, oldValue, newValue) {
|
||||
if (this.component) {
|
||||
switch (name) {
|
||||
case 'mini':
|
||||
this.getModel().update({mini: newValue != null})
|
||||
break
|
||||
case 'placeholder-text':
|
||||
this.getModel().update({placeholderText: newValue})
|
||||
break
|
||||
case 'gutter-hidden':
|
||||
this.getModel().update({lineNumberGutterVisible: newValue == null})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extended: Get a promise that resolves the next time the element's DOM
|
||||
// is updated in any way.
|
||||
//
|
||||
// This can be useful when you've made a change to the model and need to
|
||||
// be sure this change has been flushed to the DOM.
|
||||
//
|
||||
// Returns a {Promise}.
|
||||
getNextUpdatePromise () {
|
||||
return this.getComponent().getNextUpdatePromise()
|
||||
}
|
||||
|
||||
getModel () {
|
||||
return this.getComponent().props.model
|
||||
}
|
||||
|
||||
setModel (model) {
|
||||
this.getComponent().update({model})
|
||||
this.updateModelFromAttributes()
|
||||
}
|
||||
|
||||
updateModelFromAttributes () {
|
||||
const props = {mini: this.hasAttribute('mini')}
|
||||
if (this.hasAttribute('placeholder-text')) props.placeholderText = this.getAttribute('placeholder-text')
|
||||
if (this.hasAttribute('gutter-hidden')) props.lineNumberGutterVisible = false
|
||||
|
||||
this.getModel().update(props)
|
||||
if (this.initialText) this.getModel().setText(this.initialText)
|
||||
}
|
||||
|
||||
onDidAttach (callback) {
|
||||
return this.emitter.on('did-attach', callback)
|
||||
}
|
||||
|
||||
onDidDetach (callback) {
|
||||
return this.emitter.on('did-detach', callback)
|
||||
}
|
||||
|
||||
measureDimensions () {
|
||||
this.getComponent().measureDimensions()
|
||||
}
|
||||
|
||||
setWidth (width) {
|
||||
this.style.width = this.getComponent().getGutterContainerWidth() + width + 'px'
|
||||
}
|
||||
|
||||
getWidth () {
|
||||
return this.getComponent().getScrollContainerWidth()
|
||||
}
|
||||
|
||||
setHeight (height) {
|
||||
this.style.height = height + 'px'
|
||||
}
|
||||
|
||||
getHeight () {
|
||||
return this.getComponent().getScrollContainerHeight()
|
||||
}
|
||||
|
||||
onDidChangeScrollLeft (callback) {
|
||||
return this.emitter.on('did-change-scroll-left', callback)
|
||||
}
|
||||
|
||||
onDidChangeScrollTop (callback) {
|
||||
return this.emitter.on('did-change-scroll-top', callback)
|
||||
}
|
||||
|
||||
// Deprecated: get the width of an `x` character displayed in this element.
|
||||
//
|
||||
// Returns a {Number} of pixels.
|
||||
getDefaultCharacterWidth () {
|
||||
return this.getComponent().getBaseCharacterWidth()
|
||||
}
|
||||
|
||||
// Extended: get the width of an `x` character displayed in this element.
|
||||
//
|
||||
// Returns a {Number} of pixels.
|
||||
getBaseCharacterWidth () {
|
||||
return this.getComponent().getBaseCharacterWidth()
|
||||
}
|
||||
|
||||
getMaxScrollTop () {
|
||||
return this.getComponent().getMaxScrollTop()
|
||||
}
|
||||
|
||||
getScrollHeight () {
|
||||
return this.getComponent().getScrollHeight()
|
||||
}
|
||||
|
||||
getScrollWidth () {
|
||||
return this.getComponent().getScrollWidth()
|
||||
}
|
||||
|
||||
getVerticalScrollbarWidth () {
|
||||
return this.getComponent().getVerticalScrollbarWidth()
|
||||
}
|
||||
|
||||
getHorizontalScrollbarHeight () {
|
||||
return this.getComponent().getHorizontalScrollbarHeight()
|
||||
}
|
||||
|
||||
getScrollTop () {
|
||||
return this.getComponent().getScrollTop()
|
||||
}
|
||||
|
||||
setScrollTop (scrollTop) {
|
||||
const component = this.getComponent()
|
||||
component.setScrollTop(scrollTop)
|
||||
component.scheduleUpdate()
|
||||
}
|
||||
|
||||
getScrollBottom () {
|
||||
return this.getComponent().getScrollBottom()
|
||||
}
|
||||
|
||||
setScrollBottom (scrollBottom) {
|
||||
return this.getComponent().setScrollBottom(scrollBottom)
|
||||
}
|
||||
|
||||
getScrollLeft () {
|
||||
return this.getComponent().getScrollLeft()
|
||||
}
|
||||
|
||||
setScrollLeft (scrollLeft) {
|
||||
const component = this.getComponent()
|
||||
component.setScrollLeft(scrollLeft)
|
||||
component.scheduleUpdate()
|
||||
}
|
||||
|
||||
getScrollRight () {
|
||||
return this.getComponent().getScrollRight()
|
||||
}
|
||||
|
||||
setScrollRight (scrollRight) {
|
||||
return this.getComponent().setScrollRight(scrollRight)
|
||||
}
|
||||
|
||||
// Essential: Scrolls the editor to the top.
|
||||
scrollToTop () {
|
||||
this.setScrollTop(0)
|
||||
}
|
||||
|
||||
// Essential: Scrolls the editor to the bottom.
|
||||
scrollToBottom () {
|
||||
this.setScrollTop(Infinity)
|
||||
}
|
||||
|
||||
hasFocus () {
|
||||
return this.getComponent().focused
|
||||
}
|
||||
|
||||
// Extended: Converts a buffer position to a pixel position.
|
||||
//
|
||||
// * `bufferPosition` A {Point}-like object that represents a buffer position.
|
||||
//
|
||||
// Be aware that calling this method with a column that does not translate
|
||||
// to column 0 on screen could cause a synchronous DOM update in order to
|
||||
// measure the requested horizontal pixel position if it isn't already
|
||||
// cached.
|
||||
//
|
||||
// Returns an {Object} with two values: `top` and `left`, representing the
|
||||
// pixel position.
|
||||
pixelPositionForBufferPosition (bufferPosition) {
|
||||
const screenPosition = this.getModel().screenPositionForBufferPosition(bufferPosition)
|
||||
return this.getComponent().pixelPositionForScreenPosition(screenPosition)
|
||||
}
|
||||
|
||||
// Extended: Converts a screen position to a pixel position.
|
||||
//
|
||||
// * `screenPosition` A {Point}-like object that represents a buffer position.
|
||||
//
|
||||
// Be aware that calling this method with a non-zero column value could
|
||||
// cause a synchronous DOM update in order to measure the requested
|
||||
// horizontal pixel position if it isn't already cached.
|
||||
//
|
||||
// Returns an {Object} with two values: `top` and `left`, representing the
|
||||
// pixel position.
|
||||
pixelPositionForScreenPosition (screenPosition) {
|
||||
screenPosition = this.getModel().clipScreenPosition(screenPosition)
|
||||
return this.getComponent().pixelPositionForScreenPosition(screenPosition)
|
||||
}
|
||||
|
||||
screenPositionForPixelPosition (pixelPosition) {
|
||||
return this.getComponent().screenPositionForPixelPosition(pixelPosition)
|
||||
}
|
||||
|
||||
pixelRectForScreenRange (range) {
|
||||
range = Range.fromObject(range)
|
||||
|
||||
const start = this.pixelPositionForScreenPosition(range.start)
|
||||
const end = this.pixelPositionForScreenPosition(range.end)
|
||||
const lineHeight = this.getComponent().getLineHeight()
|
||||
|
||||
return {
|
||||
top: start.top,
|
||||
left: start.left,
|
||||
height: end.top + lineHeight - start.top,
|
||||
width: end.left - start.left
|
||||
}
|
||||
}
|
||||
|
||||
pixelRangeForScreenRange (range) {
|
||||
range = Range.fromObject(range)
|
||||
return {
|
||||
start: this.pixelPositionForScreenPosition(range.start),
|
||||
end: this.pixelPositionForScreenPosition(range.end)
|
||||
}
|
||||
}
|
||||
|
||||
getComponent () {
|
||||
if (!this.component) {
|
||||
this.component = new TextEditorComponent({
|
||||
element: this,
|
||||
mini: this.hasAttribute('mini'),
|
||||
updatedSynchronously: this.updatedSynchronously
|
||||
})
|
||||
this.updateModelFromAttributes()
|
||||
}
|
||||
|
||||
return this.component
|
||||
}
|
||||
|
||||
setUpdatedSynchronously (updatedSynchronously) {
|
||||
this.updatedSynchronously = updatedSynchronously
|
||||
if (this.component) this.component.updatedSynchronously = updatedSynchronously
|
||||
return updatedSynchronously
|
||||
}
|
||||
|
||||
isUpdatedSynchronously () {
|
||||
return this.component ? this.component.updatedSynchronously : this.updatedSynchronously
|
||||
}
|
||||
|
||||
// Experimental: Invalidate the passed block {Decoration}'s dimensions,
|
||||
// forcing them to be recalculated and the surrounding content to be adjusted
|
||||
// on the next animation frame.
|
||||
//
|
||||
// * {blockDecoration} A {Decoration} representing the block decoration you
|
||||
// want to update the dimensions of.
|
||||
invalidateBlockDecorationDimensions () {
|
||||
this.getComponent().invalidateBlockDecorationDimensions(...arguments)
|
||||
}
|
||||
|
||||
setFirstVisibleScreenRow (row) {
|
||||
this.getModel().setFirstVisibleScreenRow(row)
|
||||
}
|
||||
|
||||
getFirstVisibleScreenRow () {
|
||||
return this.getModel().getFirstVisibleScreenRow()
|
||||
}
|
||||
|
||||
getLastVisibleScreenRow () {
|
||||
return this.getModel().getLastVisibleScreenRow()
|
||||
}
|
||||
|
||||
getVisibleRowRange () {
|
||||
return this.getModel().getVisibleRowRange()
|
||||
}
|
||||
|
||||
intersectsVisibleRowRange (startRow, endRow) {
|
||||
return !(
|
||||
endRow <= this.getFirstVisibleScreenRow() ||
|
||||
this.getLastVisibleScreenRow() <= startRow
|
||||
)
|
||||
}
|
||||
|
||||
selectionIntersectsVisibleRowRange (selection) {
|
||||
const {start, end} = selection.getScreenRange()
|
||||
return this.intersectsVisibleRowRange(start.row, end.row + 1)
|
||||
}
|
||||
|
||||
setFirstVisibleScreenColumn (column) {
|
||||
return this.getModel().setFirstVisibleScreenColumn(column)
|
||||
}
|
||||
|
||||
getFirstVisibleScreenColumn () {
|
||||
return this.getModel().getFirstVisibleScreenColumn()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports =
|
||||
document.registerElement('atom-text-editor', {
|
||||
prototype: TextEditorElement.prototype
|
||||
})
|
||||
File diff suppressed because it is too large
Load Diff
@@ -12,7 +12,8 @@ Model = require './model'
|
||||
Selection = require './selection'
|
||||
TextMateScopeSelector = require('first-mate').ScopeSelector
|
||||
GutterContainer = require './gutter-container'
|
||||
TextEditorElement = require './text-editor-element'
|
||||
TextEditorComponent = null
|
||||
TextEditorElement = null
|
||||
{isDoubleWidthCharacter, isHalfWidthCharacter, isKoreanCharacter, isWrapBoundary} = require './text-utils'
|
||||
|
||||
ZERO_WIDTH_NBSP = '\ufeff'
|
||||
@@ -61,6 +62,20 @@ class TextEditor extends Model
|
||||
@setClipboard: (clipboard) ->
|
||||
@clipboard = clipboard
|
||||
|
||||
@setScheduler: (scheduler) ->
|
||||
TextEditorComponent ?= require './text-editor-component'
|
||||
TextEditorComponent.setScheduler(scheduler)
|
||||
|
||||
@didUpdateStyles: ->
|
||||
TextEditorComponent ?= require './text-editor-component'
|
||||
TextEditorComponent.didUpdateStyles()
|
||||
|
||||
@didUpdateScrollbarStyles: ->
|
||||
TextEditorComponent ?= require './text-editor-component'
|
||||
TextEditorComponent.didUpdateScrollbarStyles()
|
||||
|
||||
@viewForItem: (item) -> item.element ? item
|
||||
|
||||
serializationVersion: 1
|
||||
|
||||
buffer: null
|
||||
@@ -89,6 +104,17 @@ class TextEditor extends Model
|
||||
Object.defineProperty @prototype, "element",
|
||||
get: -> @getElement()
|
||||
|
||||
Object.defineProperty @prototype, "editorElement",
|
||||
get: ->
|
||||
Grim.deprecate("""
|
||||
`TextEditor.prototype.editorElement` has always been private, but now
|
||||
it is gone. Reading the `editorElement` property still returns a
|
||||
reference to the editor element but this field will be removed in a
|
||||
later version of Atom, so we recommend using the `element` property instead.
|
||||
""")
|
||||
|
||||
@getElement()
|
||||
|
||||
Object.defineProperty(@prototype, 'displayBuffer', get: ->
|
||||
Grim.deprecate("""
|
||||
`TextEditor.prototype.displayBuffer` has always been private, but now
|
||||
@@ -128,7 +154,7 @@ class TextEditor extends Model
|
||||
super
|
||||
|
||||
{
|
||||
@softTabs, @firstVisibleScreenRow, @firstVisibleScreenColumn, initialLine, initialColumn, tabLength,
|
||||
@softTabs, @initialScrollTopRow, @initialScrollLeftColumn, initialLine, initialColumn, tabLength,
|
||||
@softWrapped, @decorationManager, @selectionsMarkerLayer, @buffer, suppressCursorCreation,
|
||||
@mini, @placeholderText, lineNumberGutterVisible, @largeFileMode,
|
||||
@assert, grammar, @showInvisibles, @autoHeight, @autoWidth, @scrollPastEnd, @editorWidthInChars,
|
||||
@@ -138,8 +164,6 @@ class TextEditor extends Model
|
||||
} = params
|
||||
|
||||
@assert ?= (condition) -> condition
|
||||
@firstVisibleScreenRow ?= 0
|
||||
@firstVisibleScreenColumn ?= 0
|
||||
@emitter = new Emitter
|
||||
@disposables = new CompositeDisposable
|
||||
@cursors = []
|
||||
@@ -198,7 +222,10 @@ class TextEditor extends Model
|
||||
@selectionsMarkerLayer ?= @addMarkerLayer(maintainHistory: true, persistent: true)
|
||||
@selectionsMarkerLayer.trackDestructionInOnDidCreateMarkerCallbacks = true
|
||||
|
||||
@decorationManager = new DecorationManager(@displayLayer)
|
||||
@decorationManager = new DecorationManager(this)
|
||||
@decorateMarkerLayer(@selectionsMarkerLayer, type: 'cursor')
|
||||
@decorateCursorLine() unless @isMini()
|
||||
|
||||
@decorateMarkerLayer(@displayLayer.foldsMarkerLayer, {type: 'line-number', class: 'folded'})
|
||||
|
||||
for marker in @selectionsMarkerLayer.getMarkers()
|
||||
@@ -220,13 +247,23 @@ class TextEditor extends Model
|
||||
priority: 0
|
||||
visible: lineNumberGutterVisible
|
||||
|
||||
decorateCursorLine: ->
|
||||
@cursorLineDecorations = [
|
||||
@decorateMarkerLayer(@selectionsMarkerLayer, type: 'line', class: 'cursor-line', onlyEmpty: true),
|
||||
@decorateMarkerLayer(@selectionsMarkerLayer, type: 'line-number', class: 'cursor-line'),
|
||||
@decorateMarkerLayer(@selectionsMarkerLayer, type: 'line-number', class: 'cursor-line-no-selection', onlyHead: true, onlyEmpty: true)
|
||||
]
|
||||
|
||||
doBackgroundWork: (deadline) =>
|
||||
previousLongestRow = @getApproximateLongestScreenRow()
|
||||
if @displayLayer.doBackgroundWork(deadline)
|
||||
@presenter?.updateVerticalDimensions()
|
||||
@backgroundWorkHandle = requestIdleCallback(@doBackgroundWork)
|
||||
else
|
||||
@backgroundWorkHandle = null
|
||||
|
||||
if @getApproximateLongestScreenRow() isnt previousLongestRow
|
||||
@component?.scheduleUpdate()
|
||||
|
||||
update: (params) ->
|
||||
displayLayerParams = {}
|
||||
|
||||
@@ -292,6 +329,12 @@ class TextEditor extends Model
|
||||
displayLayerParams.invisibles = @getInvisibles()
|
||||
displayLayerParams.softWrapColumn = @getSoftWrapColumn()
|
||||
displayLayerParams.showIndentGuides = @doesShowIndentGuide()
|
||||
if @mini
|
||||
decoration.destroy() for decoration in @cursorLineDecorations
|
||||
@cursorLineDecorations = null
|
||||
else
|
||||
@decorateCursorLine()
|
||||
@component?.scheduleUpdate()
|
||||
|
||||
when 'placeholderText'
|
||||
if value isnt @placeholderText
|
||||
@@ -314,7 +357,6 @@ class TextEditor extends Model
|
||||
when 'showLineNumbers'
|
||||
if value isnt @showLineNumbers
|
||||
@showLineNumbers = value
|
||||
@presenter?.didChangeShowLineNumbers()
|
||||
|
||||
when 'showInvisibles'
|
||||
if value isnt @showInvisibles
|
||||
@@ -339,22 +381,20 @@ class TextEditor extends Model
|
||||
when 'scrollPastEnd'
|
||||
if value isnt @scrollPastEnd
|
||||
@scrollPastEnd = value
|
||||
@presenter?.didChangeScrollPastEnd()
|
||||
@component?.scheduleUpdate()
|
||||
|
||||
when 'autoHeight'
|
||||
if value isnt @autoHeight
|
||||
@autoHeight = value
|
||||
@presenter?.setAutoHeight(@autoHeight)
|
||||
|
||||
when 'autoWidth'
|
||||
if value isnt @autoWidth
|
||||
@autoWidth = value
|
||||
@presenter?.didChangeAutoWidth()
|
||||
|
||||
when 'showCursorOnSelection'
|
||||
if value isnt @showCursorOnSelection
|
||||
@showCursorOnSelection = value
|
||||
cursor.setShowCursorOnSelection(value) for cursor in @getCursors()
|
||||
@component?.scheduleUpdate()
|
||||
|
||||
else
|
||||
if param isnt 'ref' and param isnt 'key'
|
||||
@@ -362,11 +402,14 @@ class TextEditor extends Model
|
||||
|
||||
@displayLayer.reset(displayLayerParams)
|
||||
|
||||
if @editorElement?
|
||||
@editorElement.views.getNextUpdatePromise()
|
||||
if @component?
|
||||
@component.getNextUpdatePromise()
|
||||
else
|
||||
Promise.resolve()
|
||||
|
||||
scheduleComponentUpdate: ->
|
||||
@component?.scheduleUpdate()
|
||||
|
||||
serialize: ->
|
||||
tokenizedBufferState = @tokenizedBuffer.serialize()
|
||||
|
||||
@@ -381,8 +424,8 @@ class TextEditor extends Model
|
||||
displayLayerId: @displayLayer.id
|
||||
selectionsMarkerLayerId: @selectionsMarkerLayer.id
|
||||
|
||||
firstVisibleScreenRow: @getFirstVisibleScreenRow()
|
||||
firstVisibleScreenColumn: @getFirstVisibleScreenColumn()
|
||||
initialScrollTopRow: @getScrollTopRow()
|
||||
initialScrollLeftColumn: @getScrollLeftColumn()
|
||||
|
||||
atomicSoftTabs: @displayLayer.atomicSoftTabs
|
||||
softWrapHangingIndentLength: @displayLayer.softWrapHangingIndent
|
||||
@@ -413,14 +456,17 @@ class TextEditor extends Model
|
||||
@emitter.on 'did-terminate-pending-state', callback
|
||||
|
||||
subscribeToDisplayLayer: ->
|
||||
@disposables.add @selectionsMarkerLayer.onDidCreateMarker @addSelection.bind(this)
|
||||
@disposables.add @tokenizedBuffer.onDidChangeGrammar @handleGrammarChange.bind(this)
|
||||
@disposables.add @displayLayer.onDidChangeSync (e) =>
|
||||
@mergeIntersectingSelections()
|
||||
@component?.didChangeDisplayLayer(e)
|
||||
@emitter.emit 'did-change', e
|
||||
@disposables.add @displayLayer.onDidReset =>
|
||||
@mergeIntersectingSelections()
|
||||
@component?.didResetDisplayLayer()
|
||||
@emitter.emit 'did-change', {}
|
||||
@disposables.add @selectionsMarkerLayer.onDidCreateMarker @addSelection.bind(this)
|
||||
@disposables.add @selectionsMarkerLayer.onDidUpdate => @component?.didUpdateSelections()
|
||||
|
||||
destroyed: ->
|
||||
@disposables.dispose()
|
||||
@@ -432,8 +478,9 @@ class TextEditor extends Model
|
||||
@gutterContainer.destroy()
|
||||
@emitter.emit 'did-destroy'
|
||||
@emitter.clear()
|
||||
@editorElement = null
|
||||
@presenter = null
|
||||
@component?.element.component = null
|
||||
@component = null
|
||||
@lineNumberGutter.element = null
|
||||
|
||||
###
|
||||
Section: Event Subscription
|
||||
@@ -688,6 +735,11 @@ class TextEditor extends Model
|
||||
onDidRemoveDecoration: (callback) ->
|
||||
@decorationManager.onDidRemoveDecoration(callback)
|
||||
|
||||
# Called by DecorationManager when a decoration is added.
|
||||
didAddDecoration: (decoration) ->
|
||||
if decoration.isType('block')
|
||||
@component?.didAddBlockDecoration(decoration)
|
||||
|
||||
# Extended: Calls your `callback` when the placeholder text is changed.
|
||||
#
|
||||
# * `callback` {Function}
|
||||
@@ -697,9 +749,6 @@ class TextEditor extends Model
|
||||
onDidChangePlaceholderText: (callback) ->
|
||||
@emitter.on 'did-change-placeholder-text', callback
|
||||
|
||||
onDidChangeFirstVisibleScreenRow: (callback, fromView) ->
|
||||
@emitter.on 'did-change-first-visible-screen-row', callback
|
||||
|
||||
onDidChangeScrollTop: (callback) ->
|
||||
Grim.deprecate("This is now a view method. Call TextEditorElement::onDidChangeScrollTop instead.")
|
||||
|
||||
@@ -735,7 +784,8 @@ class TextEditor extends Model
|
||||
@buffer, selectionsMarkerLayer, softTabs,
|
||||
suppressCursorCreation: true,
|
||||
tabLength: @tokenizedBuffer.getTabLength(),
|
||||
@firstVisibleScreenRow, @firstVisibleScreenColumn,
|
||||
initialScrollTopRow: @getScrollTopRow(),
|
||||
initialScrollLeftColumn: @getScrollLeftColumn(),
|
||||
@assert, displayLayer, grammar: @getGrammar(),
|
||||
@autoWidth, @autoHeight, @showCursorOnSelection
|
||||
})
|
||||
@@ -749,9 +799,6 @@ class TextEditor extends Model
|
||||
|
||||
isMini: -> @mini
|
||||
|
||||
setUpdatedSynchronously: (updatedSynchronously) ->
|
||||
@decorationManager.setUpdatedSynchronously(updatedSynchronously)
|
||||
|
||||
onDidChangeMini: (callback) ->
|
||||
@emitter.on 'did-change-mini', callback
|
||||
|
||||
@@ -971,29 +1018,28 @@ class TextEditor extends Model
|
||||
tokens = []
|
||||
lineTextIndex = 0
|
||||
currentTokenScopes = []
|
||||
{lineText, tagCodes} = @screenLineForScreenRow(screenRow)
|
||||
for tagCode in tagCodes
|
||||
if @displayLayer.isOpenTagCode(tagCode)
|
||||
currentTokenScopes.push(@displayLayer.tagForCode(tagCode))
|
||||
else if @displayLayer.isCloseTagCode(tagCode)
|
||||
{lineText, tags} = @screenLineForScreenRow(screenRow)
|
||||
for tag in tags
|
||||
if @displayLayer.isOpenTag(tag)
|
||||
currentTokenScopes.push(@displayLayer.classNameForTag(tag))
|
||||
else if @displayLayer.isCloseTag(tag)
|
||||
currentTokenScopes.pop()
|
||||
else
|
||||
tokens.push({
|
||||
text: lineText.substr(lineTextIndex, tagCode)
|
||||
text: lineText.substr(lineTextIndex, tag)
|
||||
scopes: currentTokenScopes.slice()
|
||||
})
|
||||
lineTextIndex += tagCode
|
||||
lineTextIndex += tag
|
||||
tokens
|
||||
|
||||
screenLineForScreenRow: (screenRow) ->
|
||||
@displayLayer.getScreenLines(screenRow, screenRow + 1)[0]
|
||||
@displayLayer.getScreenLine(screenRow)
|
||||
|
||||
bufferRowForScreenRow: (screenRow) ->
|
||||
@displayLayer.translateScreenPosition(Point(screenRow, 0)).row
|
||||
|
||||
bufferRowsForScreenRows: (startScreenRow, endScreenRow) ->
|
||||
for screenRow in [startScreenRow..endScreenRow]
|
||||
@bufferRowForScreenRow(screenRow)
|
||||
@displayLayer.bufferRowsForScreenRows(startScreenRow, endScreenRow + 1)
|
||||
|
||||
screenRowForBufferRow: (row) ->
|
||||
@displayLayer.translateBufferPosition(Point(row, 0)).row
|
||||
@@ -1751,20 +1797,32 @@ class TextEditor extends Model
|
||||
# * `block` Positions the view associated with the given item before or
|
||||
# after the row of the given `TextEditorMarker`, depending on the `position`
|
||||
# property.
|
||||
# * `cursor` Renders a cursor at the head of the given marker. If multiple
|
||||
# decorations are created for the same marker, their class strings and
|
||||
# style objects are combined into a single cursor. You can use this
|
||||
# decoration type to style existing cursors by passing in their markers
|
||||
# or render artificial cursors that don't actually exist in the model
|
||||
# by passing a marker that isn't actually associated with a cursor.
|
||||
# * `class` This CSS class will be applied to the decorated line number,
|
||||
# line, highlight, or overlay.
|
||||
# * `style` An {Object} containing CSS style properties to apply to the
|
||||
# relevant DOM node. Currently this only works with a `type` of `cursor`.
|
||||
# * `item` (optional) An {HTMLElement} or a model {Object} with a
|
||||
# corresponding view registered. Only applicable to the `gutter`,
|
||||
# `overlay` and `block` types.
|
||||
# `overlay` and `block` decoration types.
|
||||
# * `onlyHead` (optional) If `true`, the decoration will only be applied to
|
||||
# the head of the `DisplayMarker`. Only applicable to the `line` and
|
||||
# `line-number` types.
|
||||
# `line-number` decoration types.
|
||||
# * `onlyEmpty` (optional) If `true`, the decoration will only be applied if
|
||||
# the associated `DisplayMarker` is empty. Only applicable to the `gutter`,
|
||||
# `line`, and `line-number` types.
|
||||
# `line`, and `line-number` decoration types.
|
||||
# * `onlyNonEmpty` (optional) If `true`, the decoration will only be applied
|
||||
# if the associated `DisplayMarker` is non-empty. Only applicable to the
|
||||
# `gutter`, `line`, and `line-number` types.
|
||||
# `gutter`, `line`, and `line-number` decoration types.
|
||||
# * `omitEmptyLastRow` (optional) If `false`, the decoration will be applied
|
||||
# to the last row of a non-empty range, even if it ends at column 0.
|
||||
# Defaults to `true`. Only applicable to the `gutter`, `line`, and
|
||||
# `line-number` decoration types.
|
||||
# * `position` (optional) Only applicable to decorations of type `overlay` and `block`.
|
||||
# Controls where the view is positioned relative to the `TextEditorMarker`.
|
||||
# Values can be `'head'` (the default) or `'tail'` for overlay decorations, and
|
||||
@@ -1852,12 +1910,6 @@ class TextEditor extends Model
|
||||
getOverlayDecorations: (propertyFilter) ->
|
||||
@decorationManager.getOverlayDecorations(propertyFilter)
|
||||
|
||||
decorationForId: (id) ->
|
||||
@decorationManager.decorationForId(id)
|
||||
|
||||
decorationsForMarkerId: (id) ->
|
||||
@decorationManager.decorationsForMarkerId(id)
|
||||
|
||||
###
|
||||
Section: Markers
|
||||
###
|
||||
@@ -2096,9 +2148,9 @@ class TextEditor extends Model
|
||||
#
|
||||
# Returns the first matched {Cursor} or undefined
|
||||
getCursorAtScreenPosition: (position) ->
|
||||
for cursor in @cursors
|
||||
return cursor if cursor.getScreenPosition().isEqual(position)
|
||||
undefined
|
||||
if selection = @getSelectionAtScreenPosition(position)
|
||||
if selection.getHeadScreenPosition().isEqual(position)
|
||||
selection.cursor
|
||||
|
||||
# Essential: Get the position of the most recently added cursor in screen
|
||||
# coordinates.
|
||||
@@ -2140,7 +2192,7 @@ class TextEditor extends Model
|
||||
#
|
||||
# Returns a {Cursor}.
|
||||
addCursorAtBufferPosition: (bufferPosition, options) ->
|
||||
@selectionsMarkerLayer.markBufferPosition(bufferPosition, {invalidate: 'never'})
|
||||
@selectionsMarkerLayer.markBufferPosition(bufferPosition, Object.assign({invalidate: 'never'}, options))
|
||||
@getLastSelection().cursor.autoscroll() unless options?.autoscroll is false
|
||||
@getLastSelection().cursor
|
||||
|
||||
@@ -2287,14 +2339,12 @@ class TextEditor extends Model
|
||||
cursor = new Cursor(editor: this, marker: marker, showCursorOnSelection: @showCursorOnSelection)
|
||||
@cursors.push(cursor)
|
||||
@cursorsByMarkerId.set(marker.id, cursor)
|
||||
@decorateMarker(marker, type: 'line-number', class: 'cursor-line')
|
||||
@decorateMarker(marker, type: 'line-number', class: 'cursor-line-no-selection', onlyHead: true, onlyEmpty: true)
|
||||
@decorateMarker(marker, type: 'line', class: 'cursor-line', onlyEmpty: true)
|
||||
cursor
|
||||
|
||||
moveCursors: (fn) ->
|
||||
fn(cursor) for cursor in @getCursors()
|
||||
@mergeCursors()
|
||||
@transact =>
|
||||
fn(cursor) for cursor in @getCursors()
|
||||
@mergeCursors()
|
||||
|
||||
cursorMoved: (event) ->
|
||||
@emitter.emit 'did-change-cursor-position', event
|
||||
@@ -2650,6 +2700,11 @@ class TextEditor extends Model
|
||||
@createLastSelectionIfNeeded()
|
||||
_.last(@selections)
|
||||
|
||||
getSelectionAtScreenPosition: (position) ->
|
||||
markers = @selectionsMarkerLayer.findMarkers(containsScreenPosition: position)
|
||||
if markers.length > 0
|
||||
@cursorsByMarkerId.get(markers[0].id).selection
|
||||
|
||||
# Extended: Get current {Selection}s.
|
||||
#
|
||||
# Returns: An {Array} of {Selection}s.
|
||||
@@ -2808,6 +2863,7 @@ class TextEditor extends Model
|
||||
|
||||
# Called by the selection
|
||||
selectionRangeChanged: (event) ->
|
||||
@component?.didChangeSelectionRange()
|
||||
@emitter.emit 'did-change-selection-range', event
|
||||
|
||||
createLastSelectionIfNeeded: ->
|
||||
@@ -3321,7 +3377,7 @@ class TextEditor extends Model
|
||||
#
|
||||
# Returns a {Boolean}.
|
||||
isFoldedAtCursorRow: ->
|
||||
@isFoldedAtScreenRow(@getCursorScreenPosition().row)
|
||||
@isFoldedAtBufferRow(@getCursorBufferPosition().row)
|
||||
|
||||
# Extended: Determine whether the given row in buffer coordinates is folded.
|
||||
#
|
||||
@@ -3329,7 +3385,11 @@ class TextEditor extends Model
|
||||
#
|
||||
# Returns a {Boolean}.
|
||||
isFoldedAtBufferRow: (bufferRow) ->
|
||||
@displayLayer.foldsIntersectingBufferRange(Range(Point(bufferRow, 0), Point(bufferRow, Infinity))).length > 0
|
||||
range = Range(
|
||||
Point(bufferRow, 0),
|
||||
Point(bufferRow, @buffer.lineLengthForRow(bufferRow))
|
||||
)
|
||||
@displayLayer.foldsIntersectingBufferRange(range).length > 0
|
||||
|
||||
# Extended: Determine whether the given row in screen coordinates is folded.
|
||||
#
|
||||
@@ -3379,6 +3439,9 @@ class TextEditor extends Model
|
||||
getGutters: ->
|
||||
@gutterContainer.getGutters()
|
||||
|
||||
getLineNumberGutter: ->
|
||||
@lineNumberGutter
|
||||
|
||||
# Essential: Get the gutter with the given name.
|
||||
#
|
||||
# Returns a {Gutter}, or `null` if no gutter exists for the given name.
|
||||
@@ -3426,7 +3489,9 @@ class TextEditor extends Model
|
||||
@getElement().scrollToBottom()
|
||||
|
||||
scrollToScreenRange: (screenRange, options = {}) ->
|
||||
screenRange = @clipScreenRange(screenRange) if options.clip isnt false
|
||||
scrollEvent = {screenRange, options}
|
||||
@component?.didRequestAutoscroll(scrollEvent)
|
||||
@emitter.emit "did-request-autoscroll", scrollEvent
|
||||
|
||||
getHorizontalScrollbarHeight: ->
|
||||
@@ -3453,9 +3518,12 @@ class TextEditor extends Model
|
||||
|
||||
# Returns the number of rows per page
|
||||
getRowsPerPage: ->
|
||||
Math.max(@rowsPerPage ? 1, 1)
|
||||
|
||||
setRowsPerPage: (@rowsPerPage) ->
|
||||
if @component?
|
||||
clientHeight = @component.getScrollContainerClientHeight()
|
||||
lineHeight = @component.getLineHeight()
|
||||
Math.max(1, Math.ceil(clientHeight / lineHeight))
|
||||
else
|
||||
1
|
||||
|
||||
###
|
||||
Section: Config
|
||||
@@ -3483,7 +3551,11 @@ class TextEditor extends Model
|
||||
# Experimental: Does this editor allow scrolling past the last line?
|
||||
#
|
||||
# Returns a {Boolean}.
|
||||
getScrollPastEnd: -> @scrollPastEnd
|
||||
getScrollPastEnd: ->
|
||||
if @getAutoHeight()
|
||||
false
|
||||
else
|
||||
@scrollPastEnd
|
||||
|
||||
# Experimental: How fast does the editor scroll in response to mouse wheel
|
||||
# movements?
|
||||
@@ -3543,7 +3615,17 @@ class TextEditor extends Model
|
||||
|
||||
# Get the Element for the editor.
|
||||
getElement: ->
|
||||
@editorElement ?= new TextEditorElement().initialize(this, atom)
|
||||
if @component?
|
||||
@component.element
|
||||
else
|
||||
TextEditorComponent ?= require('./text-editor-component')
|
||||
TextEditorElement ?= require('./text-editor-element')
|
||||
new TextEditorComponent({
|
||||
model: this,
|
||||
updatedSynchronously: TextEditorElement.prototype.updatedSynchronously,
|
||||
@initialScrollTopRow, @initialScrollLeftColumn
|
||||
})
|
||||
@component.element
|
||||
|
||||
# Essential: Retrieves the greyed out placeholder of a mini editor.
|
||||
#
|
||||
@@ -3600,66 +3682,51 @@ class TextEditor extends Model
|
||||
@doubleWidthCharWidth = doubleWidthCharWidth
|
||||
@halfWidthCharWidth = halfWidthCharWidth
|
||||
@koreanCharWidth = koreanCharWidth
|
||||
@displayLayer.reset({}) if @isSoftWrapped() and @getEditorWidthInChars()?
|
||||
if @isSoftWrapped()
|
||||
@displayLayer.reset({
|
||||
softWrapColumn: @getSoftWrapColumn()
|
||||
})
|
||||
defaultCharWidth
|
||||
|
||||
setHeight: (height, reentrant=false) ->
|
||||
if reentrant
|
||||
@height = height
|
||||
else
|
||||
Grim.deprecate("This is now a view method. Call TextEditorElement::setHeight instead.")
|
||||
@getElement().setHeight(height)
|
||||
setHeight: (height) ->
|
||||
Grim.deprecate("This is now a view method. Call TextEditorElement::setHeight instead.")
|
||||
@getElement().setHeight(height)
|
||||
|
||||
getHeight: ->
|
||||
Grim.deprecate("This is now a view method. Call TextEditorElement::getHeight instead.")
|
||||
@height
|
||||
@getElement().getHeight()
|
||||
|
||||
getAutoHeight: -> @autoHeight ? true
|
||||
|
||||
getAutoWidth: -> @autoWidth ? false
|
||||
|
||||
setWidth: (width, reentrant=false) ->
|
||||
if reentrant
|
||||
@update({width})
|
||||
@width
|
||||
else
|
||||
Grim.deprecate("This is now a view method. Call TextEditorElement::setWidth instead.")
|
||||
@getElement().setWidth(width)
|
||||
setWidth: (width) ->
|
||||
Grim.deprecate("This is now a view method. Call TextEditorElement::setWidth instead.")
|
||||
@getElement().setWidth(width)
|
||||
|
||||
getWidth: ->
|
||||
Grim.deprecate("This is now a view method. Call TextEditorElement::getWidth instead.")
|
||||
@width
|
||||
@getElement().getWidth()
|
||||
|
||||
# Experimental: Scroll the editor such that the given screen row is at the
|
||||
# top of the visible area.
|
||||
setFirstVisibleScreenRow: (screenRow, fromView) ->
|
||||
unless fromView
|
||||
maxScreenRow = @getScreenLineCount() - 1
|
||||
unless @scrollPastEnd
|
||||
if @height? and @lineHeightInPixels?
|
||||
maxScreenRow -= Math.floor(@height / @lineHeightInPixels)
|
||||
screenRow = Math.max(Math.min(screenRow, maxScreenRow), 0)
|
||||
# Use setScrollTopRow instead of this method
|
||||
setFirstVisibleScreenRow: (screenRow) ->
|
||||
@setScrollTopRow(screenRow)
|
||||
|
||||
unless screenRow is @firstVisibleScreenRow
|
||||
@firstVisibleScreenRow = screenRow
|
||||
@emitter.emit 'did-change-first-visible-screen-row', screenRow unless fromView
|
||||
|
||||
getFirstVisibleScreenRow: -> @firstVisibleScreenRow
|
||||
getFirstVisibleScreenRow: ->
|
||||
@getElement().component.getFirstVisibleRow()
|
||||
|
||||
getLastVisibleScreenRow: ->
|
||||
if @height? and @lineHeightInPixels?
|
||||
Math.min(@firstVisibleScreenRow + Math.floor(@height / @lineHeightInPixels), @getScreenLineCount() - 1)
|
||||
else
|
||||
null
|
||||
@getElement().component.getLastVisibleRow()
|
||||
|
||||
getVisibleRowRange: ->
|
||||
if lastVisibleScreenRow = @getLastVisibleScreenRow()
|
||||
[@firstVisibleScreenRow, lastVisibleScreenRow]
|
||||
else
|
||||
null
|
||||
[@getFirstVisibleScreenRow(), @getLastVisibleScreenRow()]
|
||||
|
||||
setFirstVisibleScreenColumn: (@firstVisibleScreenColumn) ->
|
||||
getFirstVisibleScreenColumn: -> @firstVisibleScreenColumn
|
||||
# Use setScrollLeftColumn instead of this method
|
||||
setFirstVisibleScreenColumn: (column) ->
|
||||
@setScrollLeftColumn(column)
|
||||
|
||||
getFirstVisibleScreenColumn: ->
|
||||
@getElement().component.getFirstVisibleColumn()
|
||||
|
||||
getScrollTop: ->
|
||||
Grim.deprecate("This is now a view method. Call TextEditorElement::getScrollTop instead.")
|
||||
@@ -3716,6 +3783,18 @@ class TextEditor extends Model
|
||||
|
||||
@getElement().getMaxScrollTop()
|
||||
|
||||
getScrollTopRow: ->
|
||||
@getElement().component.getScrollTopRow()
|
||||
|
||||
setScrollTopRow: (scrollTopRow) ->
|
||||
@getElement().component.setScrollTopRow(scrollTopRow)
|
||||
|
||||
getScrollLeftColumn: ->
|
||||
@getElement().component.getScrollLeftColumn()
|
||||
|
||||
setScrollLeftColumn: (scrollLeftColumn) ->
|
||||
@getElement().component.setScrollLeftColumn(scrollLeftColumn)
|
||||
|
||||
intersectsVisibleRowRange: (startRow, endRow) ->
|
||||
Grim.deprecate("This is now a view method. Call TextEditorElement::intersectsVisibleRowRange instead.")
|
||||
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
module.exports =
|
||||
class TiledComponent
|
||||
updateSync: (state) ->
|
||||
@newState = @getNewState(state)
|
||||
@oldState ?= @buildEmptyState()
|
||||
|
||||
@beforeUpdateSync?(state)
|
||||
|
||||
@removeTileNodes() if @shouldRecreateAllTilesOnUpdate?()
|
||||
@updateTileNodes()
|
||||
|
||||
@afterUpdateSync?(state)
|
||||
|
||||
removeTileNodes: ->
|
||||
@removeTileNode(tileRow) for tileRow of @oldState.tiles
|
||||
return
|
||||
|
||||
removeTileNode: (tileRow) ->
|
||||
@componentsByTileId[tileRow].destroy()
|
||||
delete @componentsByTileId[tileRow]
|
||||
delete @oldState.tiles[tileRow]
|
||||
|
||||
updateTileNodes: ->
|
||||
@componentsByTileId ?= {}
|
||||
|
||||
for tileRow of @oldState.tiles
|
||||
unless @newState.tiles.hasOwnProperty(tileRow)
|
||||
@removeTileNode(tileRow)
|
||||
|
||||
for tileRow, tileState of @newState.tiles
|
||||
if @oldState.tiles.hasOwnProperty(tileRow)
|
||||
component = @componentsByTileId[tileRow]
|
||||
else
|
||||
component = @componentsByTileId[tileRow] = @buildComponentForTile(tileRow)
|
||||
|
||||
@getTilesNode().appendChild(component.getDomNode())
|
||||
@oldState.tiles[tileRow] = Object.assign({}, tileState)
|
||||
|
||||
component.updateSync(@newState)
|
||||
|
||||
return
|
||||
|
||||
getComponentForTile: (tileRow) ->
|
||||
@componentsByTileId[tileRow]
|
||||
|
||||
getComponents: ->
|
||||
for _, component of @componentsByTileId
|
||||
component
|
||||
|
||||
getTiles: ->
|
||||
@getComponents().map((component) -> component.getDomNode())
|
||||
@@ -1,118 +1,104 @@
|
||||
const {Point} = require('text-buffer')
|
||||
const {fromFirstMateScopeId} = require('./first-mate-helpers')
|
||||
|
||||
module.exports = class TokenizedBufferIterator {
|
||||
constructor (tokenizedBuffer) {
|
||||
this.tokenizedBuffer = tokenizedBuffer
|
||||
this.openTags = null
|
||||
this.closeTags = null
|
||||
this.containingTags = null
|
||||
this.openScopeIds = null
|
||||
this.closeScopeIds = null
|
||||
}
|
||||
|
||||
seek (position) {
|
||||
this.openTags = []
|
||||
this.closeTags = []
|
||||
this.openScopeIds = []
|
||||
this.closeScopeIds = []
|
||||
this.tagIndex = null
|
||||
|
||||
const currentLine = this.tokenizedBuffer.tokenizedLineForRow(position.row)
|
||||
this.currentTags = currentLine.tags
|
||||
this.currentLineOpenTags = currentLine.openScopes
|
||||
this.currentLineTags = currentLine.tags
|
||||
this.currentLineLength = currentLine.text.length
|
||||
this.containingTags = this.currentLineOpenTags.map((id) => this.scopeForId(id))
|
||||
const containingScopeIds = currentLine.openScopes.map((id) => fromFirstMateScopeId(id))
|
||||
|
||||
let currentColumn = 0
|
||||
for (let [index, tag] of this.currentTags.entries()) {
|
||||
for (let index = 0; index < this.currentLineTags.length; index++) {
|
||||
const tag = this.currentLineTags[index]
|
||||
if (tag >= 0) {
|
||||
if (currentColumn >= position.column) {
|
||||
this.tagIndex = index
|
||||
break
|
||||
} else {
|
||||
currentColumn += tag
|
||||
while (this.closeTags.length > 0) {
|
||||
this.closeTags.shift()
|
||||
this.containingTags.pop()
|
||||
while (this.closeScopeIds.length > 0) {
|
||||
this.closeScopeIds.shift()
|
||||
containingScopeIds.pop()
|
||||
}
|
||||
while (this.openTags.length > 0) {
|
||||
const openTag = this.openTags.shift()
|
||||
this.containingTags.push(openTag)
|
||||
while (this.openScopeIds.length > 0) {
|
||||
const openTag = this.openScopeIds.shift()
|
||||
containingScopeIds.push(openTag)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const scopeName = this.scopeForId(tag)
|
||||
if (tag % 2 === 0) {
|
||||
if (this.openTags.length > 0) {
|
||||
const scopeId = fromFirstMateScopeId(tag)
|
||||
if ((tag & 1) === 0) {
|
||||
if (this.openScopeIds.length > 0) {
|
||||
if (currentColumn >= position.column) {
|
||||
this.tagIndex = index
|
||||
break
|
||||
} else {
|
||||
while (this.closeTags.length > 0) {
|
||||
this.closeTags.shift()
|
||||
this.containingTags.pop()
|
||||
while (this.closeScopeIds.length > 0) {
|
||||
this.closeScopeIds.shift()
|
||||
containingScopeIds.pop()
|
||||
}
|
||||
while (this.openTags.length > 0) {
|
||||
const openTag = this.openTags.shift()
|
||||
this.containingTags.push(openTag)
|
||||
while (this.openScopeIds.length > 0) {
|
||||
const openTag = this.openScopeIds.shift()
|
||||
containingScopeIds.push(openTag)
|
||||
}
|
||||
}
|
||||
}
|
||||
this.closeTags.push(scopeName)
|
||||
this.closeScopeIds.push(scopeId)
|
||||
} else {
|
||||
this.openTags.push(scopeName)
|
||||
this.openScopeIds.push(scopeId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.tagIndex == null) {
|
||||
this.tagIndex = this.currentTags.length
|
||||
this.tagIndex = this.currentLineTags.length
|
||||
}
|
||||
this.position = Point(position.row, Math.min(this.currentLineLength, currentColumn))
|
||||
return this.containingTags.slice()
|
||||
return containingScopeIds
|
||||
}
|
||||
|
||||
moveToSuccessor () {
|
||||
for (let tag of this.closeTags) { // eslint-disable-line no-unused-vars
|
||||
this.containingTags.pop()
|
||||
}
|
||||
for (let tag of this.openTags) {
|
||||
this.containingTags.push(tag)
|
||||
}
|
||||
this.openTags = []
|
||||
this.closeTags = []
|
||||
this.openScopeIds = []
|
||||
this.closeScopeIds = []
|
||||
while (true) {
|
||||
if (this.tagIndex === this.currentTags.length) {
|
||||
if (this.tagIndex === this.currentLineTags.length) {
|
||||
if (this.isAtTagBoundary()) {
|
||||
break
|
||||
} else if (this.shouldMoveToNextLine) {
|
||||
this.moveToNextLine()
|
||||
this.openTags = this.currentLineOpenTags.map((id) => this.scopeForId(id))
|
||||
this.shouldMoveToNextLine = false
|
||||
} else if (this.nextLineHasMismatchedContainingTags()) {
|
||||
this.closeTags = this.containingTags.slice().reverse()
|
||||
this.containingTags = []
|
||||
this.shouldMoveToNextLine = true
|
||||
} else if (!this.moveToNextLine()) {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
const tag = this.currentTags[this.tagIndex]
|
||||
const tag = this.currentLineTags[this.tagIndex]
|
||||
if (tag >= 0) {
|
||||
if (this.isAtTagBoundary()) {
|
||||
break
|
||||
} else {
|
||||
this.position = Point(this.position.row, Math.min(
|
||||
this.currentLineLength,
|
||||
this.position.column + this.currentTags[this.tagIndex]
|
||||
this.position.column + this.currentLineTags[this.tagIndex]
|
||||
))
|
||||
}
|
||||
} else {
|
||||
const scopeName = this.scopeForId(tag)
|
||||
if (tag % 2 === 0) {
|
||||
if (this.openTags.length > 0) {
|
||||
const scopeId = fromFirstMateScopeId(tag)
|
||||
if ((tag & 1) === 0) {
|
||||
if (this.openScopeIds.length > 0) {
|
||||
break
|
||||
} else {
|
||||
this.closeTags.push(scopeName)
|
||||
this.closeScopeIds.push(scopeId)
|
||||
}
|
||||
} else {
|
||||
this.openTags.push(scopeName)
|
||||
this.openScopeIds.push(scopeId)
|
||||
}
|
||||
}
|
||||
this.tagIndex++
|
||||
@@ -125,24 +111,12 @@ module.exports = class TokenizedBufferIterator {
|
||||
return this.position
|
||||
}
|
||||
|
||||
getCloseTags () {
|
||||
return this.closeTags.slice()
|
||||
getCloseScopeIds () {
|
||||
return this.closeScopeIds.slice()
|
||||
}
|
||||
|
||||
getOpenTags () {
|
||||
return this.openTags.slice()
|
||||
}
|
||||
|
||||
nextLineHasMismatchedContainingTags () {
|
||||
const line = this.tokenizedBuffer.tokenizedLineForRow(this.position.row + 1)
|
||||
if (line == null) {
|
||||
return false
|
||||
} else {
|
||||
return (
|
||||
this.containingTags.length !== line.openScopes.length ||
|
||||
this.containingTags.some((tag, i) => tag !== this.scopeForId(line.openScopes[i]))
|
||||
)
|
||||
}
|
||||
getOpenScopeIds () {
|
||||
return this.openScopeIds.slice()
|
||||
}
|
||||
|
||||
moveToNextLine () {
|
||||
@@ -151,24 +125,14 @@ module.exports = class TokenizedBufferIterator {
|
||||
if (tokenizedLine == null) {
|
||||
return false
|
||||
} else {
|
||||
this.currentTags = tokenizedLine.tags
|
||||
this.currentLineTags = tokenizedLine.tags
|
||||
this.currentLineLength = tokenizedLine.text.length
|
||||
this.currentLineOpenTags = tokenizedLine.openScopes
|
||||
this.tagIndex = 0
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
isAtTagBoundary () {
|
||||
return this.closeTags.length > 0 || this.openTags.length > 0
|
||||
}
|
||||
|
||||
scopeForId (id) {
|
||||
const scope = this.tokenizedBuffer.grammar.scopeForId(id)
|
||||
if (scope) {
|
||||
return `syntax--${scope.replace(/\./g, '.syntax--')}`
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
return this.closeScopeIds.length > 0 || this.openScopeIds.length > 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,9 @@ TokenIterator = require './token-iterator'
|
||||
ScopeDescriptor = require './scope-descriptor'
|
||||
TokenizedBufferIterator = require './tokenized-buffer-iterator'
|
||||
NullGrammar = require './null-grammar'
|
||||
{toFirstMateScopeId} = require './first-mate-helpers'
|
||||
|
||||
prefixedScopes = new Map()
|
||||
|
||||
module.exports =
|
||||
class TokenizedBuffer extends Model
|
||||
@@ -46,6 +49,19 @@ class TokenizedBuffer extends Model
|
||||
buildIterator: ->
|
||||
new TokenizedBufferIterator(this)
|
||||
|
||||
classNameForScopeId: (id) ->
|
||||
scope = @grammar.scopeForId(toFirstMateScopeId(id))
|
||||
if scope
|
||||
prefixedScope = prefixedScopes.get(scope)
|
||||
if prefixedScope
|
||||
prefixedScope
|
||||
else
|
||||
prefixedScope = "syntax--#{scope.replace(/\./g, ' syntax--')}"
|
||||
prefixedScopes.set(scope, prefixedScope)
|
||||
prefixedScope
|
||||
else
|
||||
null
|
||||
|
||||
getInvalidatedRanges: ->
|
||||
[]
|
||||
|
||||
@@ -252,7 +268,7 @@ class TokenizedBuffer extends Model
|
||||
buildTokenizedLineForRowWithText: (row, text, ruleStack = @stackForRow(row - 1), openScopes = @openScopesForRow(row)) ->
|
||||
lineEnding = @buffer.lineEndingForRow(row)
|
||||
{tags, ruleStack} = @grammar.tokenizeLine(text, ruleStack, row is 0, false)
|
||||
new TokenizedLine({openScopes, text, tags, ruleStack, lineEnding, @tokenIterator})
|
||||
new TokenizedLine({openScopes, text, tags, ruleStack, lineEnding, @tokenIterator, @grammar})
|
||||
|
||||
tokenizedLineForRow: (bufferRow) ->
|
||||
if 0 <= bufferRow <= @buffer.getLastRow()
|
||||
@@ -262,7 +278,7 @@ class TokenizedBuffer extends Model
|
||||
text = @buffer.lineForRow(bufferRow)
|
||||
lineEnding = @buffer.lineEndingForRow(bufferRow)
|
||||
tags = [@grammar.startIdForScope(@grammar.scopeName), text.length, @grammar.endIdForScope(@grammar.scopeName)]
|
||||
@tokenizedLines[bufferRow] = new TokenizedLine({openScopes: [], text, tags, lineEnding, @tokenIterator})
|
||||
@tokenizedLines[bufferRow] = new TokenizedLine({openScopes: [], text, tags, lineEnding, @tokenIterator, @grammar})
|
||||
|
||||
tokenizedLinesForRows: (startRow, endRow) ->
|
||||
for row in [startRow..endRow] by 1
|
||||
@@ -328,17 +344,16 @@ class TokenizedBuffer extends Model
|
||||
@indentLevelForLine(line)
|
||||
|
||||
indentLevelForLine: (line) ->
|
||||
if match = line.match(/^[\t ]+/)
|
||||
indentLength = 0
|
||||
for character in match[0]
|
||||
if character is '\t'
|
||||
indentLength += @getTabLength() - (indentLength % @getTabLength())
|
||||
else
|
||||
indentLength++
|
||||
indentLength = 0
|
||||
for char in line
|
||||
if char is '\t'
|
||||
indentLength += @getTabLength() - (indentLength % @getTabLength())
|
||||
else if char is ' '
|
||||
indentLength++
|
||||
else
|
||||
break
|
||||
|
||||
indentLength / @getTabLength()
|
||||
else
|
||||
0
|
||||
indentLength / @getTabLength()
|
||||
|
||||
scopeDescriptorForPosition: (position) ->
|
||||
{row, column} = @buffer.clipPosition(Point.fromObject(position))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Token = require './token'
|
||||
CommentScopeRegex = /(\b|\.)comment/
|
||||
CommentScopeRegex = /(\b|\.)comment/
|
||||
|
||||
idCounter = 1
|
||||
|
||||
@@ -10,9 +10,9 @@ class TokenizedLine
|
||||
|
||||
return unless properties?
|
||||
|
||||
{@openScopes, @text, @tags, @ruleStack, @tokenIterator} = properties
|
||||
{@openScopes, @text, @tags, @ruleStack, @tokenIterator, @grammar} = properties
|
||||
|
||||
getTokenIterator: -> @tokenIterator.reset(this, arguments...)
|
||||
getTokenIterator: -> @tokenIterator.reset(this)
|
||||
|
||||
Object.defineProperty @prototype, 'tokens', get: ->
|
||||
iterator = @getTokenIterator()
|
||||
@@ -48,17 +48,26 @@ class TokenizedLine
|
||||
return @isCommentLine if @isCommentLine?
|
||||
|
||||
@isCommentLine = false
|
||||
iterator = @getTokenIterator()
|
||||
while iterator.next()
|
||||
scopes = iterator.getScopes()
|
||||
continue if scopes.length is 1
|
||||
for scope in scopes
|
||||
if CommentScopeRegex.test(scope)
|
||||
@isCommentLine = true
|
||||
break
|
||||
break
|
||||
|
||||
for tag in @openScopes
|
||||
if @isCommentOpenTag(tag)
|
||||
@isCommentLine = true
|
||||
return @isCommentLine
|
||||
|
||||
for tag in @tags
|
||||
if @isCommentOpenTag(tag)
|
||||
@isCommentLine = true
|
||||
return @isCommentLine
|
||||
|
||||
@isCommentLine
|
||||
|
||||
isCommentOpenTag: (tag) ->
|
||||
if tag < 0 and (tag & 1) is 1
|
||||
scope = @grammar.scopeForId(tag)
|
||||
if CommentScopeRegex.test(scope)
|
||||
return true
|
||||
false
|
||||
|
||||
tokenAtIndex: (index) ->
|
||||
@tokens[index]
|
||||
|
||||
|
||||
@@ -27,21 +27,13 @@ module.exports =
|
||||
class ViewRegistry
|
||||
animationFrameRequest: null
|
||||
documentReadInProgress: false
|
||||
performDocumentPollAfterUpdate: false
|
||||
debouncedPerformDocumentPoll: null
|
||||
minimumPollInterval: 200
|
||||
|
||||
constructor: (@atomEnvironment) ->
|
||||
@polling = false
|
||||
@clear()
|
||||
|
||||
initialize: ->
|
||||
@observer = new MutationObserver(@requestDocumentPoll)
|
||||
|
||||
clear: ->
|
||||
@views = new WeakMap
|
||||
@providers = []
|
||||
@debouncedPerformDocumentPoll = _.throttle(@performDocumentPoll, @minimumPollInterval).bind(this)
|
||||
@clearDocumentRequests()
|
||||
|
||||
# Essential: Add a provider that will be used to construct views in the
|
||||
@@ -175,16 +167,6 @@ class ViewRegistry
|
||||
new Disposable =>
|
||||
@documentReaders = @documentReaders.filter (reader) -> reader isnt fn
|
||||
|
||||
pollDocument: (fn) ->
|
||||
@startPollingDocument() if @documentPollers.length is 0
|
||||
@documentPollers.push(fn)
|
||||
new Disposable =>
|
||||
@documentPollers = @documentPollers.filter (poller) -> poller isnt fn
|
||||
@stopPollingDocument() if @documentPollers.length is 0
|
||||
|
||||
pollAfterNextUpdate: ->
|
||||
@performDocumentPollAfterUpdate = true
|
||||
|
||||
getNextUpdatePromise: ->
|
||||
@nextUpdatePromise ?= new Promise (resolve) =>
|
||||
@resolveNextUpdatePromise = resolve
|
||||
@@ -192,13 +174,11 @@ class ViewRegistry
|
||||
clearDocumentRequests: ->
|
||||
@documentReaders = []
|
||||
@documentWriters = []
|
||||
@documentPollers = []
|
||||
@nextUpdatePromise = null
|
||||
@resolveNextUpdatePromise = null
|
||||
if @animationFrameRequest?
|
||||
cancelAnimationFrame(@animationFrameRequest)
|
||||
@animationFrameRequest = null
|
||||
@stopPollingDocument()
|
||||
|
||||
requestDocumentUpdate: ->
|
||||
@animationFrameRequest ?= requestAnimationFrame(@performDocumentUpdate)
|
||||
@@ -213,32 +193,9 @@ class ViewRegistry
|
||||
|
||||
@documentReadInProgress = true
|
||||
reader() while reader = @documentReaders.shift()
|
||||
@performDocumentPoll() if @performDocumentPollAfterUpdate
|
||||
@performDocumentPollAfterUpdate = false
|
||||
@documentReadInProgress = false
|
||||
|
||||
# process updates requested as a result of reads
|
||||
writer() while writer = @documentWriters.shift()
|
||||
|
||||
resolveNextUpdatePromise?()
|
||||
|
||||
startPollingDocument: ->
|
||||
window.addEventListener('resize', @requestDocumentPoll)
|
||||
@observer.observe(document, {subtree: true, childList: true, attributes: true})
|
||||
@polling = true
|
||||
|
||||
stopPollingDocument: ->
|
||||
if @polling
|
||||
window.removeEventListener('resize', @requestDocumentPoll)
|
||||
@observer.disconnect()
|
||||
@polling = false
|
||||
|
||||
requestDocumentPoll: =>
|
||||
if @animationFrameRequest?
|
||||
@performDocumentPollAfterUpdate = true
|
||||
else
|
||||
@debouncedPerformDocumentPoll()
|
||||
|
||||
performDocumentPoll: ->
|
||||
poller() for poller in @documentPollers
|
||||
return
|
||||
|
||||
@@ -64,7 +64,6 @@ class WorkspaceElement extends HTMLElement {
|
||||
line-height: ${this.config.get('editor.lineHeight')};
|
||||
}`
|
||||
this.styleManager.addStyleSheet(styleSheetSource, {sourcePath: 'global-text-editor-styles', priority: -1})
|
||||
this.viewRegistry.performDocumentPoll()
|
||||
}
|
||||
|
||||
initialize (model, {config, project, styleManager, viewRegistry}) {
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
@import "docks";
|
||||
@import "panes";
|
||||
@import "syntax";
|
||||
@import "text-editor-light";
|
||||
@import "text-editor";
|
||||
@import "title-bar";
|
||||
@import "workspace-view";
|
||||
|
||||
|
||||
@@ -13,14 +13,14 @@
|
||||
|
||||
// Editors
|
||||
& when ( lightness(@syntax-background-color) < 50% ) {
|
||||
.platform-darwin atom-text-editor:not([mini]) .editor-contents--private {
|
||||
.platform-darwin atom-text-editor:not([mini]) {
|
||||
.cursor-white();
|
||||
}
|
||||
}
|
||||
|
||||
// Mini Editors
|
||||
& when ( lightness(@input-background-color) < 50% ) {
|
||||
.platform-darwin atom-text-editor[mini] .editor-contents--private {
|
||||
.platform-darwin atom-text-editor[mini] {
|
||||
.cursor-white();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src *; img-src blob: data: *; script-src 'self'; style-src 'self' 'unsafe-inline'; media-src blob: data: mediastream: *;">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src * atom://*; img-src blob: data: * atom://*; script-src 'self'; style-src 'self' 'unsafe-inline'; media-src blob: data: mediastream: * atom://*;">
|
||||
<script src="index.js"></script>
|
||||
</head>
|
||||
<body tabindex="-1">
|
||||
|
||||
@@ -42,21 +42,21 @@
|
||||
if (process.platform === 'win32') {
|
||||
relativeFilePath = relativeFilePath.replace(/\\/g, '/')
|
||||
}
|
||||
let cachedModule = snapshotResult.customRequire.cache[relativeFilePath] // eslint-disable-line no-undef
|
||||
let cachedModule = snapshotResult.customRequire.cache[relativeFilePath]
|
||||
if (!cachedModule) {
|
||||
cachedModule = {exports: Module._load(module, this, false)}
|
||||
snapshotResult.customRequire.cache[relativeFilePath] = cachedModule // eslint-disable-line no-undef
|
||||
snapshotResult.customRequire.cache[relativeFilePath] = cachedModule
|
||||
}
|
||||
return cachedModule.exports
|
||||
}
|
||||
|
||||
snapshotResult.setGlobals(global, process, window, document, console, require) // eslint-disable-line no-undef
|
||||
snapshotResult.setGlobals(global, process, window, document, console, require)
|
||||
}
|
||||
|
||||
const FileSystemBlobStore = useSnapshot ? snapshotResult.customRequire('../src/file-system-blob-store.js') : require('../src/file-system-blob-store') // eslint-disable-line no-undef
|
||||
const FileSystemBlobStore = useSnapshot ? snapshotResult.customRequire('../src/file-system-blob-store.js') : require('../src/file-system-blob-store')
|
||||
blobStore = FileSystemBlobStore.load(path.join(process.env.ATOM_HOME, 'blob-store'))
|
||||
|
||||
const NativeCompileCache = useSnapshot ? snapshotResult.customRequire('../src/native-compile-cache.js') : require('../src/native-compile-cache') // eslint-disable-line no-undef
|
||||
const NativeCompileCache = useSnapshot ? snapshotResult.customRequire('../src/native-compile-cache.js') : require('../src/native-compile-cache')
|
||||
NativeCompileCache.setCacheStore(blobStore)
|
||||
NativeCompileCache.setV8Version(process.versions.v8)
|
||||
NativeCompileCache.install()
|
||||
@@ -88,21 +88,21 @@
|
||||
}
|
||||
|
||||
function setupWindow () {
|
||||
const CompileCache = useSnapshot ? snapshotResult.customRequire('../src/compile-cache.js') : require('../src/compile-cache') // eslint-disable-line no-undef
|
||||
const CompileCache = useSnapshot ? snapshotResult.customRequire('../src/compile-cache.js') : require('../src/compile-cache')
|
||||
CompileCache.setAtomHomeDirectory(process.env.ATOM_HOME)
|
||||
CompileCache.install(process.resourcesPath, require)
|
||||
|
||||
const ModuleCache = useSnapshot ? snapshotResult.customRequire('../src/module-cache.js') : require('../src/module-cache') // eslint-disable-line no-undef
|
||||
const ModuleCache = useSnapshot ? snapshotResult.customRequire('../src/module-cache.js') : require('../src/module-cache')
|
||||
ModuleCache.register(getWindowLoadSettings())
|
||||
|
||||
const startCrashReporter = useSnapshot ? snapshotResult.customRequire('../src/crash-reporter-start.js') : require('../src/crash-reporter-start') // eslint-disable-line no-undef
|
||||
const startCrashReporter = useSnapshot ? snapshotResult.customRequire('../src/crash-reporter-start.js') : require('../src/crash-reporter-start')
|
||||
startCrashReporter({_version: getWindowLoadSettings().appVersion})
|
||||
|
||||
const CSON = useSnapshot ? snapshotResult.customRequire('../node_modules/season/lib/cson.js') : require('season') // eslint-disable-line no-undef
|
||||
const CSON = useSnapshot ? snapshotResult.customRequire('../node_modules/season/lib/cson.js') : require('season')
|
||||
CSON.setCacheDir(path.join(CompileCache.getCacheDirectory(), 'cson'))
|
||||
|
||||
const initScriptPath = path.relative(entryPointDirPath, getWindowLoadSettings().windowInitializationScript)
|
||||
const initialize = useSnapshot ? snapshotResult.customRequire(initScriptPath) : require(initScriptPath) // eslint-disable-line no-undef
|
||||
const initialize = useSnapshot ? snapshotResult.customRequire(initScriptPath) : require(initScriptPath)
|
||||
return initialize({blobStore: blobStore}).then(function () {
|
||||
electron.ipcRenderer.send('window-command', 'window:loaded')
|
||||
})
|
||||
|
||||
@@ -5,54 +5,21 @@
|
||||
atom-text-editor {
|
||||
display: flex;
|
||||
font-family: Menlo, Consolas, 'DejaVu Sans Mono', monospace;
|
||||
cursor: text;
|
||||
|
||||
.editor--private, .editor-contents--private {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.editor-contents--private {
|
||||
width: 100%;
|
||||
cursor: text;
|
||||
display: flex;
|
||||
-webkit-user-select: none;
|
||||
position: relative;
|
||||
.gutter-container {
|
||||
width: min-content;
|
||||
background-color: inherit;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.gutter {
|
||||
overflow: hidden;
|
||||
z-index: 0;
|
||||
text-align: right;
|
||||
cursor: default;
|
||||
min-width: 1em;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.line-numbers {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.line-number {
|
||||
position: relative;
|
||||
white-space: nowrap;
|
||||
padding-left: .5em;
|
||||
opacity: 0.6;
|
||||
|
||||
&.cursor-line {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.icon-right {
|
||||
.octicon(chevron-down, 0.8em);
|
||||
display: inline-block;
|
||||
visibility: hidden;
|
||||
opacity: .6;
|
||||
padding: 0 .4em;
|
||||
|
||||
&::before {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
.gutter:hover {
|
||||
@@ -78,14 +45,33 @@ atom-text-editor {
|
||||
}
|
||||
}
|
||||
|
||||
.scroll-view {
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
.line-numbers {
|
||||
width: max-content;
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
.line-number {
|
||||
width: min-content;
|
||||
padding-left: .5em;
|
||||
white-space: nowrap;
|
||||
opacity: 0.6;
|
||||
position: relative;
|
||||
|
||||
.icon-right {
|
||||
.octicon(chevron-down, 0.8em);
|
||||
display: inline-block;
|
||||
visibility: hidden;
|
||||
opacity: .6;
|
||||
padding: 0 .4em;
|
||||
|
||||
&::before {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lines {
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
@@ -94,19 +80,14 @@ atom-text-editor {
|
||||
}
|
||||
|
||||
.highlight .region {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.lines {
|
||||
min-width: 100%;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.line {
|
||||
white-space: pre;
|
||||
overflow: hidden;
|
||||
contain: layout;
|
||||
|
||||
&.cursor-line .fold-marker::after {
|
||||
opacity: 1;
|
||||
@@ -139,17 +120,6 @@ atom-text-editor {
|
||||
box-shadow: inset 1px 0;
|
||||
}
|
||||
|
||||
.hidden-input {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
top: 0;
|
||||
left: 0;
|
||||
opacity: 0;
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
.cursor {
|
||||
z-index: 4;
|
||||
pointer-events: none;
|
||||
@@ -166,43 +136,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] {
|
||||
Reference in New Issue
Block a user