mirror of
https://github.com/atom/atom.git
synced 2026-01-23 05:48:10 -05:00
Merge pull request #13880 from atom/ns-editor-rendering
Rewrite editor rendering layout to use new browser features and virtual DOM
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()
|
||||
|
||||
12
package.json
12
package.json
@@ -27,7 +27,7 @@
|
||||
"color": "^0.7.3",
|
||||
"dedent": "^0.6.0",
|
||||
"devtron": "1.3.0",
|
||||
"element-resize-detector": "^1.1.10",
|
||||
"etch": "^0.12.2",
|
||||
"event-kit": "^2.3.0",
|
||||
"find-parent-dir": "^0.3.0",
|
||||
"first-mate": "7.0.4",
|
||||
@@ -42,7 +42,7 @@
|
||||
"jquery": "2.1.4",
|
||||
"key-path-helpers": "^0.4.0",
|
||||
"less-cache": "1.1.0",
|
||||
"line-top-index": "0.2.0",
|
||||
"line-top-index": "0.3.0",
|
||||
"marked": "^0.3.6",
|
||||
"minimatch": "^3.0.3",
|
||||
"mocha": "2.5.1",
|
||||
@@ -66,7 +66,7 @@
|
||||
"sinon": "1.17.4",
|
||||
"source-map-support": "^0.3.2",
|
||||
"temp": "^0.8.3",
|
||||
"text-buffer": "11.4.1",
|
||||
"text-buffer": "12.1.0",
|
||||
"typescript-simple": "1.0.0",
|
||||
"underscore-plus": "^1.6.6",
|
||||
"winreg": "^1.2.1",
|
||||
@@ -186,7 +186,11 @@
|
||||
"spyOn",
|
||||
"waitsFor",
|
||||
"waitsForPromise",
|
||||
"indexedDB"
|
||||
"indexedDB",
|
||||
"IntersectionObserver",
|
||||
"FocusEvent",
|
||||
"requestAnimationFrame",
|
||||
"HTMLElement"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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}
|
||||
@@ -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', ->
|
||||
|
||||
@@ -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 ->
|
||||
@@ -28,6 +28,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]
|
||||
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])
|
||||
419
spec/text-editor-element-spec.js
Normal file
419
spec/text-editor-element-spec.js
Normal file
@@ -0,0 +1,419 @@
|
||||
/* 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])
|
||||
|
||||
expect(element.pixelRectForScreenRange([[2, 3], [13, 11]])).toEqual({top: 34, left: 22, height: 204, width: 57})
|
||||
})
|
||||
})
|
||||
|
||||
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 ->
|
||||
@@ -3279,7 +3238,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", ->
|
||||
@@ -5460,8 +5418,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 ->
|
||||
@@ -5470,9 +5428,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", ->
|
||||
@@ -5485,8 +5443,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 ->
|
||||
@@ -5495,8 +5453,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 ->
|
||||
@@ -5505,14 +5463,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()", ->
|
||||
@@ -5533,7 +5491,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
|
||||
|
||||
@@ -5551,7 +5513,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
|
||||
|
||||
@@ -5574,72 +5540,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")
|
||||
@@ -5661,6 +5561,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
|
||||
@@ -5947,20 +5853,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,17 +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([])
|
||||
expect(iterator.getCloseScopeIds()).toEqual([257])
|
||||
expect(iterator.getOpenScopeIds()).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", () => {
|
||||
@@ -145,16 +129,6 @@ describe('TokenizedBufferIterator', () => {
|
||||
openScopes: [-1]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
grammar: {
|
||||
scopeForId (id) {
|
||||
if (id === -2 || id === -1) {
|
||||
return 'foo'
|
||||
} else if (id === -3) {
|
||||
return 'qux'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,28 +136,28 @@ 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, 3))
|
||||
expect(iterator.getCloseTags()).toEqual(['syntax--foo'])
|
||||
expect(iterator.getOpenTags()).toEqual(['syntax--qux'])
|
||||
expect(iterator.getCloseScopeIds()).toEqual([257])
|
||||
expect(iterator.getOpenScopeIds()).toEqual([259])
|
||||
|
||||
iterator.moveToSuccessor()
|
||||
expect(iterator.getPosition()).toEqual(Point(0, 3))
|
||||
expect(iterator.getCloseTags()).toEqual(['syntax--qux'])
|
||||
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(['syntax--foo'])
|
||||
expect(iterator.getCloseScopeIds()).toEqual([])
|
||||
expect(iterator.getOpenScopeIds()).toEqual([257])
|
||||
|
||||
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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -249,6 +250,11 @@ 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)
|
||||
@@ -798,6 +804,11 @@ class AtomEnvironment extends Model
|
||||
@windowEventHandler?.unsubscribe()
|
||||
@windowEventHandler = null
|
||||
|
||||
didChangeStyles: (styleElement) ->
|
||||
TextEditor.didUpdateStyles()
|
||||
if styleElement.textContent.indexOf('scrollbar') >= 0
|
||||
TextEditor.didUpdateScrollbarStyles()
|
||||
|
||||
###
|
||||
Section: Messaging the User
|
||||
###
|
||||
|
||||
@@ -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())
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
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
|
||||
@@ -58,6 +58,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,158 +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
|
||||
@domNode.style.backgroundColor = "inherit"
|
||||
|
||||
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,110 +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
|
||||
@tilesNode.style.backgroundColor = "inherit"
|
||||
@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,402 +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.domNode.style.backgroundColor = 'inherit'
|
||||
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()
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,3 +1,5 @@
|
||||
/* global snapshotResult */
|
||||
|
||||
if (typeof snapshotResult !== 'undefined') {
|
||||
snapshotResult.setGlobals(global, process, global, {}, console, require)
|
||||
}
|
||||
@@ -6,7 +8,7 @@ const {userAgent} = process.env
|
||||
const [compileCachePath, taskPath] = process.argv.slice(2)
|
||||
|
||||
const CompileCache = require('./compile-cache')
|
||||
CompileCache.setCacheDirectory(compileCachePath);
|
||||
CompileCache.setCacheDirectory(compileCachePath)
|
||||
CompileCache.install(`${process.resourcesPath}`, require)
|
||||
|
||||
const setupGlobals = function () {
|
||||
|
||||
@@ -1,967 +0,0 @@
|
||||
scrollbarStyle = require 'scrollbar-style'
|
||||
{Range, Point} = require 'text-buffer'
|
||||
{CompositeDisposable, Disposable} = require 'event-kit'
|
||||
{ipcRenderer} = require 'electron'
|
||||
Grim = require 'grim'
|
||||
ElementResizeDetector = require('element-resize-detector')
|
||||
elementResizeDetector = null
|
||||
|
||||
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
|
||||
|
||||
@updateSync()
|
||||
@initialized = true
|
||||
|
||||
destroy: ->
|
||||
@mounted = false
|
||||
@disposables.dispose()
|
||||
@presenter.destroy()
|
||||
@gutterContainerComponent?.destroy()
|
||||
@domElementPool.clear()
|
||||
|
||||
@verticalScrollbarComponent.destroy()
|
||||
@horizontalScrollbarComponent.destroy()
|
||||
|
||||
@onVerticalScroll = null
|
||||
@onHorizontalScroll = null
|
||||
|
||||
@intersectionObserver?.disconnect()
|
||||
|
||||
didAttach: ->
|
||||
@intersectionObserver = new IntersectionObserver((entries) =>
|
||||
{intersectionRect} = entries[entries.length - 1]
|
||||
if intersectionRect.width > 0 or intersectionRect.height > 0
|
||||
@becameVisible()
|
||||
)
|
||||
@intersectionObserver.observe(@domNode)
|
||||
@becameVisible() if @isVisible()
|
||||
|
||||
measureDimensions = @measureDimensions.bind(this)
|
||||
elementResizeDetector ?= ElementResizeDetector({strategy: 'scroll'})
|
||||
elementResizeDetector.listenTo(@domNode, measureDimensions)
|
||||
@disposables.add(new Disposable => elementResizeDetector.removeListener(@domNode, measureDimensions))
|
||||
|
||||
measureWindowSize = @measureWindowSize.bind(this)
|
||||
window.addEventListener('resize', measureWindowSize)
|
||||
@disposables.add(new Disposable -> window.removeEventListener('resize', measureWindowSize))
|
||||
|
||||
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: =>
|
||||
@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()
|
||||
@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()
|
||||
|
||||
# Handle styling change synchronously if a global editor property such as
|
||||
# font size might have changed. Otherwise coalesce multiple style sheet changes
|
||||
# into a measurement on the next animation frame to prevent excessive thrashing.
|
||||
if styleElement.getAttribute('source-path') is 'global-text-editor-styles'
|
||||
@handleStylingChange()
|
||||
else if not @stylingChangeAnimationFrameRequested
|
||||
@stylingChangeAnimationFrameRequested = true
|
||||
requestAnimationFrame =>
|
||||
@stylingChangeAnimationFrameRequested = false
|
||||
if @mounted
|
||||
@refreshScrollbars() if not styleElement.sheet? or @containsScrollbarSelector(styleElement.sheet)
|
||||
@handleStylingChange()
|
||||
|
||||
onAllThemesLoaded: =>
|
||||
@refreshScrollbars()
|
||||
@handleStylingChange()
|
||||
|
||||
handleStylingChange: =>
|
||||
if @isVisible()
|
||||
@sampleFontStyling()
|
||||
@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)
|
||||
|
||||
# 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()
|
||||
|
||||
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())
|
||||
3904
src/text-editor-component.js
Normal file
3904
src/text-editor-component.js
Normal file
File diff suppressed because it is too large
Load Diff
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()
|
||||
@@ -433,7 +479,6 @@ class TextEditor extends Model
|
||||
@emitter.emit 'did-destroy'
|
||||
@emitter.clear()
|
||||
@editorElement = null
|
||||
@presenter = null
|
||||
|
||||
###
|
||||
Section: Event Subscription
|
||||
@@ -688,6 +733,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 +747,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 +782,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 +797,6 @@ class TextEditor extends Model
|
||||
|
||||
isMini: -> @mini
|
||||
|
||||
setUpdatedSynchronously: (updatedSynchronously) ->
|
||||
@decorationManager.setUpdatedSynchronously(updatedSynchronously)
|
||||
|
||||
onDidChangeMini: (callback) ->
|
||||
@emitter.on 'did-change-mini', callback
|
||||
|
||||
@@ -971,22 +1016,22 @@ 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
|
||||
@@ -1751,20 +1796,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 +1909,6 @@ class TextEditor extends Model
|
||||
getOverlayDecorations: (propertyFilter) ->
|
||||
@decorationManager.getOverlayDecorations(propertyFilter)
|
||||
|
||||
decorationForId: (id) ->
|
||||
@decorationManager.decorationForId(id)
|
||||
|
||||
decorationsForMarkerId: (id) ->
|
||||
@decorationManager.decorationsForMarkerId(id)
|
||||
|
||||
###
|
||||
Section: Markers
|
||||
###
|
||||
@@ -2096,9 +2147,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 +2191,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 +2338,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 +2699,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 +2862,7 @@ class TextEditor extends Model
|
||||
|
||||
# Called by the selection
|
||||
selectionRangeChanged: (event) ->
|
||||
@component?.didChangeSelectionRange()
|
||||
@emitter.emit 'did-change-selection-range', event
|
||||
|
||||
createLastSelectionIfNeeded: ->
|
||||
@@ -3379,6 +3434,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 +3484,9 @@ class TextEditor extends Model
|
||||
@getElement().scrollToBottom()
|
||||
|
||||
scrollToScreenRange: (screenRange, options = {}) ->
|
||||
screenRange = @clipScreenRange(screenRange)
|
||||
scrollEvent = {screenRange, options}
|
||||
@component?.didRequestAutoscroll(scrollEvent)
|
||||
@emitter.emit "did-request-autoscroll", scrollEvent
|
||||
|
||||
getHorizontalScrollbarHeight: ->
|
||||
@@ -3453,9 +3513,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 +3546,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 +3610,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 +3677,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 +3778,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,118 +1,121 @@
|
||||
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
|
||||
this.containingScopeIds = 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.currentLineTags = currentLine.tags
|
||||
this.currentLineOpenTags = currentLine.openScopes
|
||||
this.currentLineLength = currentLine.text.length
|
||||
this.containingTags = this.currentLineOpenTags.map((id) => this.scopeForId(id))
|
||||
this.containingScopeIds = this.currentLineOpenTags.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()
|
||||
this.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()
|
||||
this.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()
|
||||
this.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()
|
||||
this.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 this.containingScopeIds.slice()
|
||||
}
|
||||
|
||||
moveToSuccessor () {
|
||||
for (let tag of this.closeTags) { // eslint-disable-line no-unused-vars
|
||||
this.containingTags.pop()
|
||||
for (let i = 0; i < this.closeScopeIds.length; i++) {
|
||||
this.containingScopeIds.pop()
|
||||
}
|
||||
for (let tag of this.openTags) {
|
||||
this.containingTags.push(tag)
|
||||
for (let i = 0; i < this.openScopeIds.length; i++) {
|
||||
const tag = this.openScopeIds[i]
|
||||
this.containingScopeIds.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.openScopeIds = this.currentLineOpenTags.map((id) => fromFirstMateScopeId(id))
|
||||
this.shouldMoveToNextLine = false
|
||||
} else if (this.nextLineHasMismatchedContainingTags()) {
|
||||
this.closeTags = this.containingTags.slice().reverse()
|
||||
this.containingTags = []
|
||||
this.closeScopeIds = this.containingScopeIds.slice().reverse()
|
||||
this.containingScopeIds = []
|
||||
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,12 +128,12 @@ module.exports = class TokenizedBufferIterator {
|
||||
return this.position
|
||||
}
|
||||
|
||||
getCloseTags () {
|
||||
return this.closeTags.slice()
|
||||
getCloseScopeIds () {
|
||||
return this.closeScopeIds.slice()
|
||||
}
|
||||
|
||||
getOpenTags () {
|
||||
return this.openTags.slice()
|
||||
getOpenScopeIds () {
|
||||
return this.openScopeIds.slice()
|
||||
}
|
||||
|
||||
nextLineHasMismatchedContainingTags () {
|
||||
@@ -139,8 +142,8 @@ module.exports = class TokenizedBufferIterator {
|
||||
return false
|
||||
} else {
|
||||
return (
|
||||
this.containingTags.length !== line.openScopes.length ||
|
||||
this.containingTags.some((tag, i) => tag !== this.scopeForId(line.openScopes[i]))
|
||||
this.containingScopeIds.length !== line.openScopes.length ||
|
||||
this.containingScopeIds.some((tag, i) => tag !== fromFirstMateScopeId(line.openScopes[i]))
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -151,7 +154,7 @@ 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
|
||||
@@ -160,15 +163,6 @@ module.exports = class TokenizedBufferIterator {
|
||||
}
|
||||
|
||||
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: ->
|
||||
[]
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,63 +5,23 @@
|
||||
atom-text-editor {
|
||||
display: flex;
|
||||
font-family: Menlo, Consolas, 'DejaVu Sans Mono', monospace;
|
||||
|
||||
.editor--private, .editor-contents--private {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
.editor-contents--private {
|
||||
width: 100%;
|
||||
cursor: text;
|
||||
display: flex;
|
||||
-webkit-user-select: none;
|
||||
position: relative;
|
||||
}
|
||||
cursor: text;
|
||||
|
||||
.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;
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
.line-numbers {
|
||||
position: relative;
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.gutter:hover {
|
||||
.line-number.foldable .icon-right {
|
||||
visibility: visible;
|
||||
@@ -85,14 +45,32 @@ 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;
|
||||
}
|
||||
|
||||
@@ -102,20 +80,14 @@ atom-text-editor {
|
||||
}
|
||||
|
||||
.highlight .region {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.lines {
|
||||
min-width: 100%;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
.line {
|
||||
white-space: pre;
|
||||
overflow: hidden;
|
||||
contain: layout;
|
||||
|
||||
&.cursor-line .fold-marker::after {
|
||||
opacity: 1;
|
||||
@@ -148,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;
|
||||
@@ -175,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