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:
Antonio Scandurra
2017-05-05 19:57:08 +02:00
committed by GitHub
62 changed files with 8916 additions and 14812 deletions

View File

@@ -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()

View File

@@ -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()

View File

@@ -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"
]
}
}

View File

@@ -1,129 +0,0 @@
CustomGutterComponent = require '../src/custom-gutter-component'
Gutter = require '../src/gutter'
describe "CustomGutterComponent", ->
[gutterComponent, gutter] = []
beforeEach ->
mockGutterContainer = {}
gutter = new Gutter(mockGutterContainer, {name: 'test-gutter'})
gutterComponent = new CustomGutterComponent({gutter, views: atom.views})
it "creates a gutter DOM node with only an empty 'custom-decorations' child node when it is initialized", ->
expect(gutterComponent.getDomNode().classList.contains('gutter')).toBe true
expect(gutterComponent.getDomNode().getAttribute('gutter-name')).toBe 'test-gutter'
expect(gutterComponent.getDomNode().children.length).toBe 1
decorationsWrapperNode = gutterComponent.getDomNode().children.item(0)
expect(decorationsWrapperNode.classList.contains('custom-decorations')).toBe true
it "makes its view accessible from the view registry", ->
expect(gutterComponent.getDomNode()).toBe gutter.getElement()
it "hides its DOM node when ::hideNode is called, and shows its DOM node when ::showNode is called", ->
gutterComponent.hideNode()
expect(gutterComponent.getDomNode().style.display).toBe 'none'
gutterComponent.showNode()
expect(gutterComponent.getDomNode().style.display).toBe ''
describe "::updateSync", ->
decorationItem1 = document.createElement('div')
buildTestState = (customDecorations) ->
mockTestState =
content: if customDecorations then customDecorations else {}
styles:
scrollHeight: 100
scrollTop: 10
backgroundColor: 'black'
mockTestState
it "sets the custom-decoration wrapper's scrollHeight, scrollTop, and background color", ->
decorationsWrapperNode = gutterComponent.getDomNode().children.item(0)
expect(decorationsWrapperNode.style.height).toBe ''
expect(decorationsWrapperNode.style['-webkit-transform']).toBe ''
expect(decorationsWrapperNode.style.backgroundColor).toBe ''
gutterComponent.updateSync(buildTestState({}))
expect(decorationsWrapperNode.style.height).not.toBe ''
expect(decorationsWrapperNode.style['-webkit-transform']).not.toBe ''
expect(decorationsWrapperNode.style.backgroundColor).not.toBe ''
it "creates a new DOM node for a new decoration and adds it to the gutter at the right place", ->
customDecorations =
'decoration-id-1':
top: 0
height: 10
item: decorationItem1
class: 'test-class-1'
gutterComponent.updateSync(buildTestState(customDecorations))
decorationsWrapperNode = gutterComponent.getDomNode().children.item(0)
expect(decorationsWrapperNode.children.length).toBe 1
decorationNode = decorationsWrapperNode.children.item(0)
expect(decorationNode.style.top).toBe '0px'
expect(decorationNode.style.height).toBe '10px'
expect(decorationNode.classList.contains('test-class-1')).toBe true
expect(decorationNode.classList.contains('decoration')).toBe true
expect(decorationNode.children.length).toBe 1
decorationItem = decorationNode.children.item(0)
expect(decorationItem).toBe decorationItem1
it "updates the existing DOM node for a decoration that existed but has new properties", ->
initialCustomDecorations =
'decoration-id-1':
top: 0
height: 10
item: decorationItem1
class: 'test-class-1'
gutterComponent.updateSync(buildTestState(initialCustomDecorations))
initialDecorationNode = gutterComponent.getDomNode().children.item(0).children.item(0)
# Change the dimensions and item, remove the class.
decorationItem2 = document.createElement('div')
changedCustomDecorations =
'decoration-id-1':
top: 10
height: 20
item: decorationItem2
gutterComponent.updateSync(buildTestState(changedCustomDecorations))
changedDecorationNode = gutterComponent.getDomNode().children.item(0).children.item(0)
expect(changedDecorationNode).toBe initialDecorationNode
expect(changedDecorationNode.style.top).toBe '10px'
expect(changedDecorationNode.style.height).toBe '20px'
expect(changedDecorationNode.classList.contains('test-class-1')).toBe false
expect(changedDecorationNode.classList.contains('decoration')).toBe true
expect(changedDecorationNode.children.length).toBe 1
decorationItem = changedDecorationNode.children.item(0)
expect(decorationItem).toBe decorationItem2
# Remove the item, add a class.
changedCustomDecorations =
'decoration-id-1':
top: 10
height: 20
class: 'test-class-2'
gutterComponent.updateSync(buildTestState(changedCustomDecorations))
changedDecorationNode = gutterComponent.getDomNode().children.item(0).children.item(0)
expect(changedDecorationNode).toBe initialDecorationNode
expect(changedDecorationNode.style.top).toBe '10px'
expect(changedDecorationNode.style.height).toBe '20px'
expect(changedDecorationNode.classList.contains('test-class-2')).toBe true
expect(changedDecorationNode.classList.contains('decoration')).toBe true
expect(changedDecorationNode.children.length).toBe 0
it "removes any decorations that existed previously but aren't in the latest update", ->
customDecorations =
'decoration-id-1':
top: 0
height: 10
class: 'test-class-1'
gutterComponent.updateSync(buildTestState(customDecorations))
decorationsWrapperNode = gutterComponent.getDomNode().children.item(0)
expect(decorationsWrapperNode.children.length).toBe 1
emptyCustomDecorations = {}
gutterComponent.updateSync(buildTestState(emptyCustomDecorations))
expect(decorationsWrapperNode.children.length).toBe 0

View File

@@ -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'})

View File

@@ -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}

View File

@@ -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()

View File

@@ -3,7 +3,9 @@ GutterContainer = require '../src/gutter-container'
describe 'GutterContainer', ->
gutterContainer = null
fakeTextEditor = {}
fakeTextEditor = {
scheduleComponentUpdate: ->
}
beforeEach ->
gutterContainer = new GutterContainer fakeTextEditor

View File

@@ -1,7 +1,9 @@
Gutter = require '../src/gutter'
describe 'Gutter', ->
fakeGutterContainer = {}
fakeGutterContainer = {
scheduleComponentUpdate: ->
}
name = 'name'
describe '::hide', ->

View File

@@ -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!"

View File

@@ -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>) &amp;= <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

View File

@@ -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])

View 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

View File

@@ -19,7 +19,7 @@ describe('TextEditorRegistry', function () {
packageManager: {deferredActivationHooks: null}
})
editor = new TextEditor()
editor = new TextEditor({autoHeight: false})
})
afterEach(function () {

View File

@@ -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", ->

View File

@@ -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([])
})
})
})

View File

@@ -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']

View File

@@ -5,7 +5,6 @@ describe "ViewRegistry", ->
beforeEach ->
registry = new ViewRegistry
registry.initialize()
afterEach ->
registry.clearDocumentRequests()

View File

@@ -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)
})

View File

@@ -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
###

View File

@@ -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())

View File

@@ -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

View File

@@ -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

View File

@@ -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
View 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)
}
}
}

View File

@@ -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
View 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)
}
}

View File

@@ -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()

View File

@@ -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 ' +

View File

@@ -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')

View File

@@ -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

View File

@@ -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,

View File

@@ -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 = {

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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()
}
}
}

View File

@@ -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()

View File

@@ -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()

View File

@@ -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)
}
}
}

View File

@@ -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()

View File

@@ -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)

View File

@@ -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

View File

@@ -769,8 +769,6 @@ class Selection extends Model
{oldHeadScreenPosition, oldTailScreenPosition, newHeadScreenPosition} = e
{textChanged} = e
@cursor.updateVisibility()
unless oldHeadScreenPosition.isEqual(newHeadScreenPosition)
@cursor.goalColumn = null
cursorMovedEvent = {

View File

@@ -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 () {

View File

@@ -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

File diff suppressed because it is too large Load Diff

346
src/text-editor-element.js Normal file
View 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

View File

@@ -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.")

View File

@@ -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
}
}

View File

@@ -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: ->
[]

View File

@@ -21,7 +21,7 @@
@import "docks";
@import "panes";
@import "syntax";
@import "text-editor-light";
@import "text-editor";
@import "title-bar";
@import "workspace-view";

View File

@@ -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();
}
}

View File

@@ -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] {