mirror of
https://github.com/pulsar-edit/pulsar.git
synced 2024-11-10 10:17:11 +03:00
Merge pull request #1883 from atom/ns-react-editor-view
React Editor View, Take 2
This commit is contained in:
commit
104aa5efc7
@ -40,6 +40,8 @@
|
|||||||
"property-accessors": "1.x",
|
"property-accessors": "1.x",
|
||||||
"q": "^1.0.1",
|
"q": "^1.0.1",
|
||||||
"random-words": "0.0.1",
|
"random-words": "0.0.1",
|
||||||
|
"react": "^0.10.0",
|
||||||
|
"reactionary": "^0.8.0",
|
||||||
"runas": "0.5.x",
|
"runas": "0.5.x",
|
||||||
"scandal": "0.15.2",
|
"scandal": "0.15.2",
|
||||||
"scoped-property-store": "^0.8.0",
|
"scoped-property-store": "^0.8.0",
|
||||||
@ -51,7 +53,7 @@
|
|||||||
"temp": "0.5.0",
|
"temp": "0.5.0",
|
||||||
"text-buffer": "^2.1.0",
|
"text-buffer": "^2.1.0",
|
||||||
"theorist": "1.x",
|
"theorist": "1.x",
|
||||||
"underscore-plus": "^1.1.2",
|
"underscore-plus": "^1.2.1",
|
||||||
"vm-compatibility-layer": "0.1.0"
|
"vm-compatibility-layer": "0.1.0"
|
||||||
},
|
},
|
||||||
"packageDependencies": {
|
"packageDependencies": {
|
||||||
|
@ -943,3 +943,69 @@ describe "DisplayBuffer", ->
|
|||||||
expect(displayBuffer.getMarkerCount()).toBe initialMarkerCount + 2
|
expect(displayBuffer.getMarkerCount()).toBe initialMarkerCount + 2
|
||||||
expect(marker1.getAttributes()).toEqual a: 1, b: 2
|
expect(marker1.getAttributes()).toEqual a: 1, b: 2
|
||||||
expect(marker2.getAttributes()).toEqual a: 1, b: 3
|
expect(marker2.getAttributes()).toEqual a: 1, b: 3
|
||||||
|
|
||||||
|
describe "DisplayBufferMarker::getPixelRange()", ->
|
||||||
|
it "returns the start and end positions of the marker based on the line height and character widths assigned to the DisplayBuffer", ->
|
||||||
|
marker = displayBuffer.markScreenRange([[5, 10], [6, 4]])
|
||||||
|
|
||||||
|
displayBuffer.setLineHeight(20)
|
||||||
|
displayBuffer.setDefaultCharWidth(10)
|
||||||
|
displayBuffer.setScopedCharWidths(["source.js", "keyword.control.js"], r: 11, e: 11, t: 11, u: 11, n: 11)
|
||||||
|
|
||||||
|
{start, end} = marker.getPixelRange()
|
||||||
|
expect(start.top).toBe 5 * 20
|
||||||
|
expect(start.left).toBe (4 * 10) + (6 * 11)
|
||||||
|
|
||||||
|
describe "::setScrollTop", ->
|
||||||
|
beforeEach ->
|
||||||
|
displayBuffer.manageScrollPosition = true
|
||||||
|
displayBuffer.setLineHeight(10)
|
||||||
|
|
||||||
|
it "disallows negative values", ->
|
||||||
|
displayBuffer.setHeight(displayBuffer.getScrollHeight() + 100)
|
||||||
|
expect(displayBuffer.setScrollTop(-10)).toBe 0
|
||||||
|
expect(displayBuffer.getScrollTop()).toBe 0
|
||||||
|
|
||||||
|
it "disallows values that would make ::getScrollBottom() exceed ::getScrollHeight()", ->
|
||||||
|
displayBuffer.setHeight(50)
|
||||||
|
maxScrollTop = displayBuffer.getScrollHeight() - displayBuffer.getHeight()
|
||||||
|
|
||||||
|
expect(displayBuffer.setScrollTop(maxScrollTop)).toBe maxScrollTop
|
||||||
|
expect(displayBuffer.getScrollTop()).toBe maxScrollTop
|
||||||
|
|
||||||
|
expect(displayBuffer.setScrollTop(maxScrollTop + 50)).toBe maxScrollTop
|
||||||
|
expect(displayBuffer.getScrollTop()).toBe maxScrollTop
|
||||||
|
|
||||||
|
describe "::setScrollLeft", ->
|
||||||
|
beforeEach ->
|
||||||
|
displayBuffer.manageScrollPosition = true
|
||||||
|
displayBuffer.setDefaultCharWidth(10)
|
||||||
|
|
||||||
|
it "disallows negative values", ->
|
||||||
|
displayBuffer.setWidth(displayBuffer.getScrollWidth() + 100)
|
||||||
|
expect(displayBuffer.setScrollLeft(-10)).toBe 0
|
||||||
|
expect(displayBuffer.getScrollLeft()).toBe 0
|
||||||
|
|
||||||
|
it "disallows values that would make ::getScrollRight() exceed ::getScrollWidth()", ->
|
||||||
|
displayBuffer.setWidth(50)
|
||||||
|
maxScrollLeft = displayBuffer.getScrollWidth() - displayBuffer.getWidth()
|
||||||
|
|
||||||
|
expect(displayBuffer.setScrollLeft(maxScrollLeft)).toBe maxScrollLeft
|
||||||
|
expect(displayBuffer.getScrollLeft()).toBe maxScrollLeft
|
||||||
|
|
||||||
|
expect(displayBuffer.setScrollLeft(maxScrollLeft + 50)).toBe maxScrollLeft
|
||||||
|
expect(displayBuffer.getScrollLeft()).toBe maxScrollLeft
|
||||||
|
|
||||||
|
describe "::scrollToScreenPosition(position)", ->
|
||||||
|
it "sets the scroll top and scroll left so the given screen position is in view", ->
|
||||||
|
displayBuffer.manageScrollPosition = true
|
||||||
|
displayBuffer.setLineHeight(10)
|
||||||
|
displayBuffer.setDefaultCharWidth(10)
|
||||||
|
|
||||||
|
displayBuffer.setHeight(50)
|
||||||
|
displayBuffer.setWidth(50)
|
||||||
|
maxScrollTop = displayBuffer.getScrollHeight() - displayBuffer.getHeight()
|
||||||
|
|
||||||
|
displayBuffer.scrollToScreenPosition([8, 20])
|
||||||
|
expect(displayBuffer.getScrollBottom()).toBe (9 + displayBuffer.getVerticalScrollMargin()) * 10
|
||||||
|
expect(displayBuffer.getScrollRight()).toBe (20 + displayBuffer.getHorizontalScrollMargin()) * 10
|
||||||
|
580
spec/editor-component-spec.coffee
Normal file
580
spec/editor-component-spec.coffee
Normal file
@ -0,0 +1,580 @@
|
|||||||
|
{extend, flatten, toArray} = require 'underscore-plus'
|
||||||
|
ReactEditorView = require '../src/react-editor-view'
|
||||||
|
nbsp = String.fromCharCode(160)
|
||||||
|
|
||||||
|
describe "EditorComponent", ->
|
||||||
|
[editor, wrapperView, component, node, verticalScrollbarNode, horizontalScrollbarNode] = []
|
||||||
|
[lineHeightInPixels, charWidth, delayAnimationFrames, nextAnimationFrame] = []
|
||||||
|
|
||||||
|
beforeEach ->
|
||||||
|
waitsForPromise ->
|
||||||
|
atom.packages.activatePackage('language-javascript')
|
||||||
|
|
||||||
|
runs ->
|
||||||
|
spyOn(window, "setInterval").andCallFake window.fakeSetInterval
|
||||||
|
spyOn(window, "clearInterval").andCallFake window.fakeClearInterval
|
||||||
|
|
||||||
|
delayAnimationFrames = false
|
||||||
|
nextAnimationFrame = null
|
||||||
|
spyOn(window, 'requestAnimationFrame').andCallFake (fn) ->
|
||||||
|
if delayAnimationFrames
|
||||||
|
nextAnimationFrame = fn
|
||||||
|
else
|
||||||
|
fn()
|
||||||
|
|
||||||
|
editor = atom.project.openSync('sample.js')
|
||||||
|
wrapperView = new ReactEditorView(editor)
|
||||||
|
wrapperView.attachToDom()
|
||||||
|
{component} = wrapperView
|
||||||
|
component.setLineHeight(1.3)
|
||||||
|
component.setFontSize(20)
|
||||||
|
|
||||||
|
lineHeightInPixels = editor.getLineHeight()
|
||||||
|
charWidth = editor.getDefaultCharWidth()
|
||||||
|
node = component.getDOMNode()
|
||||||
|
verticalScrollbarNode = node.querySelector('.vertical-scrollbar')
|
||||||
|
horizontalScrollbarNode = node.querySelector('.horizontal-scrollbar')
|
||||||
|
|
||||||
|
describe "line rendering", ->
|
||||||
|
it "renders only the currently-visible lines", ->
|
||||||
|
node.style.height = 4.5 * lineHeightInPixels + 'px'
|
||||||
|
component.measureHeightAndWidth()
|
||||||
|
|
||||||
|
lines = node.querySelectorAll('.line')
|
||||||
|
expect(lines.length).toBe 6
|
||||||
|
expect(lines[0].textContent).toBe editor.lineForScreenRow(0).text
|
||||||
|
expect(lines[5].textContent).toBe editor.lineForScreenRow(5).text
|
||||||
|
|
||||||
|
verticalScrollbarNode.scrollTop = 2.5 * lineHeightInPixels
|
||||||
|
verticalScrollbarNode.dispatchEvent(new UIEvent('scroll'))
|
||||||
|
|
||||||
|
expect(node.querySelector('.scroll-view-content').style['-webkit-transform']).toBe "translate3d(0px, #{-2.5 * lineHeightInPixels}px, 0)"
|
||||||
|
|
||||||
|
lineNodes = node.querySelectorAll('.line')
|
||||||
|
expect(lineNodes.length).toBe 6
|
||||||
|
expect(lineNodes[0].offsetTop).toBe 2 * lineHeightInPixels
|
||||||
|
expect(lineNodes[0].textContent).toBe editor.lineForScreenRow(2).text
|
||||||
|
expect(lineNodes[5].textContent).toBe editor.lineForScreenRow(7).text
|
||||||
|
|
||||||
|
it "updates absolute positions of subsequent lines when lines are inserted or removed", ->
|
||||||
|
editor.getBuffer().deleteRows(0, 1)
|
||||||
|
lineNodes = node.querySelectorAll('.line')
|
||||||
|
expect(lineNodes[0].offsetTop).toBe 0
|
||||||
|
expect(lineNodes[1].offsetTop).toBe 1 * lineHeightInPixels
|
||||||
|
expect(lineNodes[2].offsetTop).toBe 2 * lineHeightInPixels
|
||||||
|
|
||||||
|
editor.getBuffer().insert([0, 0], '\n\n')
|
||||||
|
lineNodes = node.querySelectorAll('.line')
|
||||||
|
expect(lineNodes[0].offsetTop).toBe 0
|
||||||
|
expect(lineNodes[1].offsetTop).toBe 1 * lineHeightInPixels
|
||||||
|
expect(lineNodes[2].offsetTop).toBe 2 * lineHeightInPixels
|
||||||
|
expect(lineNodes[3].offsetTop).toBe 3 * lineHeightInPixels
|
||||||
|
expect(lineNodes[4].offsetTop).toBe 4 * lineHeightInPixels
|
||||||
|
|
||||||
|
describe "when indent guides are enabled", ->
|
||||||
|
beforeEach ->
|
||||||
|
component.setShowIndentGuide(true)
|
||||||
|
|
||||||
|
it "adds an 'indent-guide' class to spans comprising the leading whitespace", ->
|
||||||
|
lines = node.querySelectorAll('.line')
|
||||||
|
line1LeafNodes = getLeafNodes(lines[1])
|
||||||
|
expect(line1LeafNodes[0].textContent).toBe ' '
|
||||||
|
expect(line1LeafNodes[0].classList.contains('indent-guide')).toBe true
|
||||||
|
expect(line1LeafNodes[1].classList.contains('indent-guide')).toBe false
|
||||||
|
|
||||||
|
line2LeafNodes = getLeafNodes(lines[2])
|
||||||
|
expect(line2LeafNodes[0].textContent).toBe ' '
|
||||||
|
expect(line2LeafNodes[0].classList.contains('indent-guide')).toBe true
|
||||||
|
expect(line2LeafNodes[1].textContent).toBe ' '
|
||||||
|
expect(line2LeafNodes[1].classList.contains('indent-guide')).toBe true
|
||||||
|
expect(line2LeafNodes[2].classList.contains('indent-guide')).toBe false
|
||||||
|
|
||||||
|
it "renders leading whitespace spans with the 'indent-guide' class for empty lines", ->
|
||||||
|
editor.getBuffer().insert([1, Infinity], '\n')
|
||||||
|
|
||||||
|
lines = node.querySelectorAll('.line')
|
||||||
|
line2LeafNodes = getLeafNodes(lines[2])
|
||||||
|
|
||||||
|
expect(line2LeafNodes.length).toBe 3
|
||||||
|
expect(line2LeafNodes[0].textContent).toBe ' '
|
||||||
|
expect(line2LeafNodes[0].classList.contains('indent-guide')).toBe true
|
||||||
|
expect(line2LeafNodes[1].textContent).toBe ' '
|
||||||
|
expect(line2LeafNodes[1].classList.contains('indent-guide')).toBe true
|
||||||
|
expect(line2LeafNodes[2].textContent).toBe ' '
|
||||||
|
expect(line2LeafNodes[2].classList.contains('indent-guide')).toBe true
|
||||||
|
|
||||||
|
it "renders indent guides correctly on lines containing only whitespace", ->
|
||||||
|
editor.getBuffer().insert([1, Infinity], '\n ')
|
||||||
|
lines = node.querySelectorAll('.line')
|
||||||
|
line2LeafNodes = getLeafNodes(lines[2])
|
||||||
|
expect(line2LeafNodes.length).toBe 3
|
||||||
|
expect(line2LeafNodes[0].textContent).toBe ' '
|
||||||
|
expect(line2LeafNodes[0].classList.contains('indent-guide')).toBe true
|
||||||
|
expect(line2LeafNodes[1].textContent).toBe ' '
|
||||||
|
expect(line2LeafNodes[1].classList.contains('indent-guide')).toBe true
|
||||||
|
expect(line2LeafNodes[2].textContent).toBe ' '
|
||||||
|
expect(line2LeafNodes[2].classList.contains('indent-guide')).toBe true
|
||||||
|
|
||||||
|
it "does not render indent guides in trailing whitespace for lines containing non whitespace characters", ->
|
||||||
|
editor.getBuffer().setText (" hi ")
|
||||||
|
lines = node.querySelectorAll('.line')
|
||||||
|
line0LeafNodes = getLeafNodes(lines[0])
|
||||||
|
expect(line0LeafNodes[0].textContent).toBe ' '
|
||||||
|
expect(line0LeafNodes[0].classList.contains('indent-guide')).toBe true
|
||||||
|
expect(line0LeafNodes[1].textContent).toBe ' '
|
||||||
|
expect(line0LeafNodes[1].classList.contains('indent-guide')).toBe false
|
||||||
|
|
||||||
|
getLeafNodes = (node) ->
|
||||||
|
if node.children.length > 0
|
||||||
|
flatten(toArray(node.children).map(getLeafNodes))
|
||||||
|
else
|
||||||
|
[node]
|
||||||
|
|
||||||
|
describe "gutter rendering", ->
|
||||||
|
it "renders the currently-visible line numbers", ->
|
||||||
|
node.style.height = 4.5 * lineHeightInPixels + 'px'
|
||||||
|
component.measureHeightAndWidth()
|
||||||
|
|
||||||
|
lines = node.querySelectorAll('.line-number')
|
||||||
|
expect(lines.length).toBe 6
|
||||||
|
expect(lines[0].textContent).toBe "#{nbsp}1"
|
||||||
|
expect(lines[5].textContent).toBe "#{nbsp}6"
|
||||||
|
|
||||||
|
verticalScrollbarNode.scrollTop = 2.5 * lineHeightInPixels
|
||||||
|
verticalScrollbarNode.dispatchEvent(new UIEvent('scroll'))
|
||||||
|
|
||||||
|
expect(node.querySelector('.line-numbers').style['-webkit-transform']).toBe "translate3d(0, #{-2.5 * lineHeightInPixels}px, 0)"
|
||||||
|
|
||||||
|
lineNumberNodes = node.querySelectorAll('.line-number')
|
||||||
|
expect(lineNumberNodes.length).toBe 6
|
||||||
|
expect(lineNumberNodes[0].offsetTop).toBe 2 * lineHeightInPixels
|
||||||
|
expect(lineNumberNodes[5].offsetTop).toBe 7 * lineHeightInPixels
|
||||||
|
expect(lineNumberNodes[0].textContent).toBe "#{nbsp}3"
|
||||||
|
expect(lineNumberNodes[5].textContent).toBe "#{nbsp}8"
|
||||||
|
|
||||||
|
it "updates absolute positions of subsequent line numbers when lines are inserted or removed", ->
|
||||||
|
editor.getBuffer().insert([0, 0], '\n\n')
|
||||||
|
|
||||||
|
lineNumberNodes = node.querySelectorAll('.line-number')
|
||||||
|
expect(lineNumberNodes[0].offsetTop).toBe 0
|
||||||
|
expect(lineNumberNodes[1].offsetTop).toBe 1 * lineHeightInPixels
|
||||||
|
expect(lineNumberNodes[2].offsetTop).toBe 2 * lineHeightInPixels
|
||||||
|
expect(lineNumberNodes[3].offsetTop).toBe 3 * lineHeightInPixels
|
||||||
|
expect(lineNumberNodes[4].offsetTop).toBe 4 * lineHeightInPixels
|
||||||
|
|
||||||
|
editor.getBuffer().insert([0, 0], '\n\n')
|
||||||
|
lineNumberNodes = node.querySelectorAll('.line-number')
|
||||||
|
expect(lineNumberNodes[0].offsetTop).toBe 0
|
||||||
|
expect(lineNumberNodes[1].offsetTop).toBe 1 * lineHeightInPixels
|
||||||
|
expect(lineNumberNodes[2].offsetTop).toBe 2 * lineHeightInPixels
|
||||||
|
expect(lineNumberNodes[3].offsetTop).toBe 3 * lineHeightInPixels
|
||||||
|
expect(lineNumberNodes[4].offsetTop).toBe 4 * lineHeightInPixels
|
||||||
|
|
||||||
|
it "renders • characters for soft-wrapped lines", ->
|
||||||
|
editor.setSoftWrap(true)
|
||||||
|
node.style.height = 4.5 * lineHeightInPixels + 'px'
|
||||||
|
node.style.width = 30 * charWidth + 'px'
|
||||||
|
component.measureHeightAndWidth()
|
||||||
|
|
||||||
|
lines = node.querySelectorAll('.line-number')
|
||||||
|
expect(lines.length).toBe 6
|
||||||
|
expect(lines[0].textContent).toBe "#{nbsp}1"
|
||||||
|
expect(lines[1].textContent).toBe "#{nbsp}•"
|
||||||
|
expect(lines[2].textContent).toBe "#{nbsp}2"
|
||||||
|
expect(lines[3].textContent).toBe "#{nbsp}•"
|
||||||
|
expect(lines[4].textContent).toBe "#{nbsp}3"
|
||||||
|
expect(lines[5].textContent).toBe "#{nbsp}•"
|
||||||
|
|
||||||
|
describe "cursor rendering", ->
|
||||||
|
it "renders the currently visible cursors", ->
|
||||||
|
cursor1 = editor.getCursor()
|
||||||
|
cursor1.setScreenPosition([0, 5])
|
||||||
|
|
||||||
|
node.style.height = 4.5 * lineHeightInPixels + 'px'
|
||||||
|
component.measureHeightAndWidth()
|
||||||
|
|
||||||
|
cursorNodes = node.querySelectorAll('.cursor')
|
||||||
|
expect(cursorNodes.length).toBe 1
|
||||||
|
expect(cursorNodes[0].offsetHeight).toBe lineHeightInPixels
|
||||||
|
expect(cursorNodes[0].offsetWidth).toBe charWidth
|
||||||
|
expect(cursorNodes[0].offsetTop).toBe 0
|
||||||
|
expect(cursorNodes[0].offsetLeft).toBe 5 * charWidth
|
||||||
|
|
||||||
|
cursor2 = editor.addCursorAtScreenPosition([6, 11])
|
||||||
|
cursor3 = editor.addCursorAtScreenPosition([4, 10])
|
||||||
|
|
||||||
|
cursorNodes = node.querySelectorAll('.cursor')
|
||||||
|
expect(cursorNodes.length).toBe 2
|
||||||
|
expect(cursorNodes[0].offsetTop).toBe 0
|
||||||
|
expect(cursorNodes[0].offsetLeft).toBe 5 * charWidth
|
||||||
|
expect(cursorNodes[1].offsetTop).toBe 4 * lineHeightInPixels
|
||||||
|
expect(cursorNodes[1].offsetLeft).toBe 10 * charWidth
|
||||||
|
|
||||||
|
verticalScrollbarNode.scrollTop = 2.5 * lineHeightInPixels
|
||||||
|
verticalScrollbarNode.dispatchEvent(new UIEvent('scroll'))
|
||||||
|
|
||||||
|
cursorNodes = node.querySelectorAll('.cursor')
|
||||||
|
expect(cursorNodes.length).toBe 2
|
||||||
|
expect(cursorNodes[0].offsetTop).toBe 6 * lineHeightInPixels
|
||||||
|
expect(cursorNodes[0].offsetLeft).toBe 11 * charWidth
|
||||||
|
expect(cursorNodes[1].offsetTop).toBe 4 * lineHeightInPixels
|
||||||
|
expect(cursorNodes[1].offsetLeft).toBe 10 * charWidth
|
||||||
|
|
||||||
|
cursor3.destroy()
|
||||||
|
cursorNodes = node.querySelectorAll('.cursor')
|
||||||
|
expect(cursorNodes.length).toBe 1
|
||||||
|
expect(cursorNodes[0].offsetTop).toBe 6 * lineHeightInPixels
|
||||||
|
expect(cursorNodes[0].offsetLeft).toBe 11 * charWidth
|
||||||
|
|
||||||
|
it "accounts for character widths when positioning cursors", ->
|
||||||
|
atom.config.set('editor.fontFamily', 'sans-serif')
|
||||||
|
editor.setCursorScreenPosition([0, 16])
|
||||||
|
|
||||||
|
cursor = node.querySelector('.cursor')
|
||||||
|
cursorRect = cursor.getBoundingClientRect()
|
||||||
|
|
||||||
|
cursorLocationTextNode = node.querySelector('.storage.type.function.js').firstChild.firstChild
|
||||||
|
range = document.createRange()
|
||||||
|
range.setStart(cursorLocationTextNode, 0)
|
||||||
|
range.setEnd(cursorLocationTextNode, 1)
|
||||||
|
rangeRect = range.getBoundingClientRect()
|
||||||
|
|
||||||
|
expect(cursorRect.left).toBe rangeRect.left
|
||||||
|
expect(cursorRect.width).toBe rangeRect.width
|
||||||
|
|
||||||
|
it "blinks cursors when they aren't moving", ->
|
||||||
|
editor.addCursorAtScreenPosition([1, 0])
|
||||||
|
[cursorNode1, cursorNode2] = node.querySelectorAll('.cursor')
|
||||||
|
expect(cursorNode1.classList.contains('blink-off')).toBe false
|
||||||
|
expect(cursorNode2.classList.contains('blink-off')).toBe false
|
||||||
|
|
||||||
|
advanceClock(component.props.cursorBlinkPeriod / 2)
|
||||||
|
expect(cursorNode1.classList.contains('blink-off')).toBe true
|
||||||
|
expect(cursorNode2.classList.contains('blink-off')).toBe true
|
||||||
|
|
||||||
|
advanceClock(component.props.cursorBlinkPeriod / 2)
|
||||||
|
expect(cursorNode1.classList.contains('blink-off')).toBe false
|
||||||
|
expect(cursorNode2.classList.contains('blink-off')).toBe false
|
||||||
|
|
||||||
|
advanceClock(component.props.cursorBlinkPeriod / 2)
|
||||||
|
expect(cursorNode1.classList.contains('blink-off')).toBe true
|
||||||
|
expect(cursorNode2.classList.contains('blink-off')).toBe true
|
||||||
|
|
||||||
|
# Stop blinking immediately when cursors move
|
||||||
|
advanceClock(component.props.cursorBlinkPeriod / 4)
|
||||||
|
expect(cursorNode1.classList.contains('blink-off')).toBe true
|
||||||
|
expect(cursorNode2.classList.contains('blink-off')).toBe true
|
||||||
|
|
||||||
|
# Stop blinking for one full period after moving the cursor
|
||||||
|
editor.moveCursorRight()
|
||||||
|
expect(cursorNode1.classList.contains('blink-off')).toBe false
|
||||||
|
expect(cursorNode2.classList.contains('blink-off')).toBe false
|
||||||
|
|
||||||
|
advanceClock(component.props.cursorBlinkResumeDelay / 2)
|
||||||
|
expect(cursorNode1.classList.contains('blink-off')).toBe false
|
||||||
|
expect(cursorNode2.classList.contains('blink-off')).toBe false
|
||||||
|
|
||||||
|
advanceClock(component.props.cursorBlinkResumeDelay / 2)
|
||||||
|
expect(cursorNode1.classList.contains('blink-off')).toBe true
|
||||||
|
expect(cursorNode2.classList.contains('blink-off')).toBe true
|
||||||
|
|
||||||
|
advanceClock(component.props.cursorBlinkPeriod / 2)
|
||||||
|
expect(cursorNode1.classList.contains('blink-off')).toBe false
|
||||||
|
expect(cursorNode2.classList.contains('blink-off')).toBe false
|
||||||
|
|
||||||
|
it "renders the hidden input field at the position of the last cursor if it is on screen", ->
|
||||||
|
inputNode = node.querySelector('.hidden-input')
|
||||||
|
node.style.height = 5 * lineHeightInPixels + 'px'
|
||||||
|
node.style.width = 10 * charWidth + 'px'
|
||||||
|
component.measureHeightAndWidth()
|
||||||
|
|
||||||
|
expect(editor.getCursorScreenPosition()).toEqual [0, 0]
|
||||||
|
editor.setScrollTop(3 * lineHeightInPixels)
|
||||||
|
editor.setScrollLeft(3 * charWidth)
|
||||||
|
expect(inputNode.offsetTop).toBe 0
|
||||||
|
expect(inputNode.offsetLeft).toBe 0
|
||||||
|
|
||||||
|
editor.setCursorBufferPosition([5, 5])
|
||||||
|
cursorRect = editor.getCursor().getPixelRect()
|
||||||
|
cursorTop = cursorRect.top
|
||||||
|
cursorLeft = cursorRect.left
|
||||||
|
expect(inputNode.offsetTop).toBe cursorTop - editor.getScrollTop()
|
||||||
|
expect(inputNode.offsetLeft).toBe cursorLeft - editor.getScrollLeft()
|
||||||
|
|
||||||
|
it "does not render cursors that are associated with non-empty selections", ->
|
||||||
|
editor.setSelectedScreenRange([[0, 4], [4, 6]])
|
||||||
|
editor.addCursorAtScreenPosition([6, 8])
|
||||||
|
|
||||||
|
cursorNodes = node.querySelectorAll('.cursor')
|
||||||
|
expect(cursorNodes.length).toBe 1
|
||||||
|
expect(cursorNodes[0].offsetTop).toBe 6 * lineHeightInPixels
|
||||||
|
expect(cursorNodes[0].offsetLeft).toBe 8 * charWidth
|
||||||
|
|
||||||
|
describe "selection rendering", ->
|
||||||
|
scrollViewClientLeft = null
|
||||||
|
|
||||||
|
beforeEach ->
|
||||||
|
scrollViewClientLeft = node.querySelector('.scroll-view').getBoundingClientRect().left
|
||||||
|
|
||||||
|
it "renders 1 region for 1-line selections", ->
|
||||||
|
# 1-line selection
|
||||||
|
editor.setSelectedScreenRange([[1, 6], [1, 10]])
|
||||||
|
regions = node.querySelectorAll('.selection .region')
|
||||||
|
|
||||||
|
expect(regions.length).toBe 1
|
||||||
|
regionRect = regions[0].getBoundingClientRect()
|
||||||
|
expect(regionRect.top).toBe 1 * lineHeightInPixels
|
||||||
|
expect(regionRect.height).toBe 1 * lineHeightInPixels
|
||||||
|
expect(regionRect.left).toBe scrollViewClientLeft + 6 * charWidth
|
||||||
|
expect(regionRect.width).toBe 4 * charWidth
|
||||||
|
|
||||||
|
it "renders 2 regions for 2-line selections", ->
|
||||||
|
editor.setSelectedScreenRange([[1, 6], [2, 10]])
|
||||||
|
regions = node.querySelectorAll('.selection .region')
|
||||||
|
expect(regions.length).toBe 2
|
||||||
|
|
||||||
|
region1Rect = regions[0].getBoundingClientRect()
|
||||||
|
expect(region1Rect.top).toBe 1 * lineHeightInPixels
|
||||||
|
expect(region1Rect.height).toBe 1 * lineHeightInPixels
|
||||||
|
expect(region1Rect.left).toBe scrollViewClientLeft + 6 * charWidth
|
||||||
|
expect(Math.ceil(region1Rect.right)).toBe node.clientWidth # TODO: Remove ceiling when react-wrapper is removed
|
||||||
|
|
||||||
|
region2Rect = regions[1].getBoundingClientRect()
|
||||||
|
expect(region2Rect.top).toBe 2 * lineHeightInPixels
|
||||||
|
expect(region2Rect.height).toBe 1 * lineHeightInPixels
|
||||||
|
expect(region2Rect.left).toBe scrollViewClientLeft + 0
|
||||||
|
expect(region2Rect.width).toBe 10 * charWidth
|
||||||
|
|
||||||
|
it "renders 3 regions for selections with more than 2 lines", ->
|
||||||
|
editor.setSelectedScreenRange([[1, 6], [5, 10]])
|
||||||
|
regions = node.querySelectorAll('.selection .region')
|
||||||
|
expect(regions.length).toBe 3
|
||||||
|
|
||||||
|
region1Rect = regions[0].getBoundingClientRect()
|
||||||
|
expect(region1Rect.top).toBe 1 * lineHeightInPixels
|
||||||
|
expect(region1Rect.height).toBe 1 * lineHeightInPixels
|
||||||
|
expect(region1Rect.left).toBe scrollViewClientLeft + 6 * charWidth
|
||||||
|
expect(Math.ceil(region1Rect.right)).toBe node.clientWidth # TODO: Remove ceiling when react-wrapper is removed
|
||||||
|
|
||||||
|
region2Rect = regions[1].getBoundingClientRect()
|
||||||
|
expect(region2Rect.top).toBe 2 * lineHeightInPixels
|
||||||
|
expect(region2Rect.height).toBe 3 * lineHeightInPixels
|
||||||
|
expect(region2Rect.left).toBe scrollViewClientLeft + 0
|
||||||
|
expect(Math.ceil(region2Rect.right)).toBe node.clientWidth # TODO: Remove ceiling when react-wrapper is removed
|
||||||
|
|
||||||
|
region3Rect = regions[2].getBoundingClientRect()
|
||||||
|
expect(region3Rect.top).toBe 5 * lineHeightInPixels
|
||||||
|
expect(region3Rect.height).toBe 1 * lineHeightInPixels
|
||||||
|
expect(region3Rect.left).toBe scrollViewClientLeft + 0
|
||||||
|
expect(region3Rect.width).toBe 10 * charWidth
|
||||||
|
|
||||||
|
it "does not render empty selections", ->
|
||||||
|
expect(editor.getSelection().isEmpty()).toBe true
|
||||||
|
expect(node.querySelectorAll('.selection').length).toBe 0
|
||||||
|
|
||||||
|
describe "mouse interactions", ->
|
||||||
|
linesNode = null
|
||||||
|
|
||||||
|
beforeEach ->
|
||||||
|
delayAnimationFrames = true
|
||||||
|
linesNode = node.querySelector('.lines')
|
||||||
|
|
||||||
|
describe "when a non-folded line is single-clicked", ->
|
||||||
|
describe "when no modifier keys are held down", ->
|
||||||
|
it "moves the cursor to the nearest screen position", ->
|
||||||
|
node.style.height = 4.5 * lineHeightInPixels + 'px'
|
||||||
|
node.style.width = 10 * charWidth + 'px'
|
||||||
|
component.measureHeightAndWidth()
|
||||||
|
editor.setScrollTop(3.5 * lineHeightInPixels)
|
||||||
|
editor.setScrollLeft(2 * charWidth)
|
||||||
|
|
||||||
|
linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([4, 8])))
|
||||||
|
expect(editor.getCursorScreenPosition()).toEqual [4, 8]
|
||||||
|
|
||||||
|
describe "when the shift key is held down", ->
|
||||||
|
it "selects to the nearest screen position", ->
|
||||||
|
editor.setCursorScreenPosition([3, 4])
|
||||||
|
linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 6]), shiftKey: true))
|
||||||
|
expect(editor.getSelectedScreenRange()).toEqual [[3, 4], [5, 6]]
|
||||||
|
|
||||||
|
describe "when the command key is held down", ->
|
||||||
|
it "adds a cursor at the nearest screen position", ->
|
||||||
|
editor.setCursorScreenPosition([3, 4])
|
||||||
|
linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 6]), metaKey: true))
|
||||||
|
expect(editor.getSelectedScreenRanges()).toEqual [[[3, 4], [3, 4]], [[5, 6], [5, 6]]]
|
||||||
|
|
||||||
|
describe "when a non-folded line is double-clicked", ->
|
||||||
|
it "selects the word containing the nearest screen position", ->
|
||||||
|
linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), detail: 2))
|
||||||
|
linesNode.dispatchEvent(buildMouseEvent('mouseup'))
|
||||||
|
expect(editor.getSelectedScreenRange()).toEqual [[5, 6], [5, 13]]
|
||||||
|
|
||||||
|
linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([6, 6]), detail: 1))
|
||||||
|
linesNode.dispatchEvent(buildMouseEvent('mouseup'))
|
||||||
|
expect(editor.getSelectedScreenRange()).toEqual [[6, 6], [6, 6]]
|
||||||
|
|
||||||
|
linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([8, 8]), detail: 1, shiftKey: true))
|
||||||
|
linesNode.dispatchEvent(buildMouseEvent('mouseup'))
|
||||||
|
expect(editor.getSelectedScreenRange()).toEqual [[6, 6], [8, 8]]
|
||||||
|
|
||||||
|
describe "when a non-folded line is triple-clicked", ->
|
||||||
|
it "selects the line containing the nearest screen position", ->
|
||||||
|
linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([5, 10]), detail: 3))
|
||||||
|
linesNode.dispatchEvent(buildMouseEvent('mouseup'))
|
||||||
|
expect(editor.getSelectedScreenRange()).toEqual [[5, 0], [6, 0]]
|
||||||
|
|
||||||
|
linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([6, 6]), detail: 1, shiftKey: true))
|
||||||
|
linesNode.dispatchEvent(buildMouseEvent('mouseup'))
|
||||||
|
expect(editor.getSelectedScreenRange()).toEqual [[5, 0], [7, 0]]
|
||||||
|
|
||||||
|
linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([7, 5]), detail: 1))
|
||||||
|
linesNode.dispatchEvent(buildMouseEvent('mouseup'))
|
||||||
|
linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([8, 8]), detail: 1, shiftKey: true))
|
||||||
|
linesNode.dispatchEvent(buildMouseEvent('mouseup'))
|
||||||
|
expect(editor.getSelectedScreenRange()).toEqual [[7, 5], [8, 8]]
|
||||||
|
|
||||||
|
describe "when the mouse is clicked and dragged", ->
|
||||||
|
it "selects to the nearest screen position until the mouse button is released", ->
|
||||||
|
linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([2, 4]), which: 1))
|
||||||
|
linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([6, 8]), which: 1))
|
||||||
|
nextAnimationFrame()
|
||||||
|
expect(editor.getSelectedScreenRange()).toEqual [[2, 4], [6, 8]]
|
||||||
|
|
||||||
|
linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([10, 0]), which: 1))
|
||||||
|
nextAnimationFrame()
|
||||||
|
expect(editor.getSelectedScreenRange()).toEqual [[2, 4], [10, 0]]
|
||||||
|
|
||||||
|
linesNode.dispatchEvent(buildMouseEvent('mouseup'))
|
||||||
|
linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([12, 0]), which: 1))
|
||||||
|
nextAnimationFrame()
|
||||||
|
expect(editor.getSelectedScreenRange()).toEqual [[2, 4], [10, 0]]
|
||||||
|
|
||||||
|
it "stops selecting if the mouse is dragged into the dev tools", ->
|
||||||
|
linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([2, 4]), which: 1))
|
||||||
|
linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([6, 8]), which: 1))
|
||||||
|
nextAnimationFrame()
|
||||||
|
expect(editor.getSelectedScreenRange()).toEqual [[2, 4], [6, 8]]
|
||||||
|
|
||||||
|
linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([10, 0]), which: 0))
|
||||||
|
nextAnimationFrame()
|
||||||
|
expect(editor.getSelectedScreenRange()).toEqual [[2, 4], [6, 8]]
|
||||||
|
|
||||||
|
linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([8, 0]), which: 1))
|
||||||
|
nextAnimationFrame()
|
||||||
|
expect(editor.getSelectedScreenRange()).toEqual [[2, 4], [6, 8]]
|
||||||
|
|
||||||
|
clientCoordinatesForScreenPosition = (screenPosition) ->
|
||||||
|
positionOffset = editor.pixelPositionForScreenPosition(screenPosition)
|
||||||
|
scrollViewClientRect = node.querySelector('.scroll-view').getBoundingClientRect()
|
||||||
|
clientX = scrollViewClientRect.left + positionOffset.left - editor.getScrollLeft()
|
||||||
|
clientY = scrollViewClientRect.top + positionOffset.top - editor.getScrollTop()
|
||||||
|
{clientX, clientY}
|
||||||
|
|
||||||
|
buildMouseEvent = (type, properties...) ->
|
||||||
|
properties = extend({bubbles: true, cancelable: true}, properties...)
|
||||||
|
event = new MouseEvent(type, properties)
|
||||||
|
Object.defineProperty(event, 'which', get: -> properties.which) if properties.which?
|
||||||
|
event
|
||||||
|
|
||||||
|
describe "focus handling", ->
|
||||||
|
inputNode = null
|
||||||
|
|
||||||
|
beforeEach ->
|
||||||
|
inputNode = node.querySelector('.hidden-input')
|
||||||
|
|
||||||
|
it "transfers focus to the hidden input", ->
|
||||||
|
expect(document.activeElement).toBe document.body
|
||||||
|
node.focus()
|
||||||
|
expect(document.activeElement).toBe inputNode
|
||||||
|
|
||||||
|
it "adds the 'is-focused' class to the editor when the hidden input is focused", ->
|
||||||
|
expect(document.activeElement).toBe document.body
|
||||||
|
inputNode.focus()
|
||||||
|
expect(node.classList.contains('is-focused')).toBe true
|
||||||
|
inputNode.blur()
|
||||||
|
expect(node.classList.contains('is-focused')).toBe false
|
||||||
|
|
||||||
|
describe "scrolling", ->
|
||||||
|
it "updates the vertical scrollbar when the scrollTop is changed in the model", ->
|
||||||
|
node.style.height = 4.5 * lineHeightInPixels + 'px'
|
||||||
|
component.measureHeightAndWidth()
|
||||||
|
|
||||||
|
expect(verticalScrollbarNode.scrollTop).toBe 0
|
||||||
|
|
||||||
|
editor.setScrollTop(10)
|
||||||
|
expect(verticalScrollbarNode.scrollTop).toBe 10
|
||||||
|
|
||||||
|
it "updates the horizontal scrollbar and scroll view content x transform based on the scrollLeft of the model", ->
|
||||||
|
node.style.width = 30 * charWidth + 'px'
|
||||||
|
component.measureHeightAndWidth()
|
||||||
|
|
||||||
|
scrollViewContentNode = node.querySelector('.scroll-view-content')
|
||||||
|
expect(scrollViewContentNode.style['-webkit-transform']).toBe "translate3d(0px, 0px, 0)"
|
||||||
|
expect(horizontalScrollbarNode.scrollLeft).toBe 0
|
||||||
|
|
||||||
|
editor.setScrollLeft(100)
|
||||||
|
expect(scrollViewContentNode.style['-webkit-transform']).toBe "translate3d(-100px, 0px, 0)"
|
||||||
|
expect(horizontalScrollbarNode.scrollLeft).toBe 100
|
||||||
|
|
||||||
|
it "updates the scrollLeft of the model when the scrollLeft of the horizontal scrollbar changes", ->
|
||||||
|
node.style.width = 30 * charWidth + 'px'
|
||||||
|
component.measureHeightAndWidth()
|
||||||
|
|
||||||
|
expect(editor.getScrollLeft()).toBe 0
|
||||||
|
horizontalScrollbarNode.scrollLeft = 100
|
||||||
|
horizontalScrollbarNode.dispatchEvent(new UIEvent('scroll'))
|
||||||
|
|
||||||
|
expect(editor.getScrollLeft()).toBe 100
|
||||||
|
|
||||||
|
describe "when a mousewheel event occurs on the editor", ->
|
||||||
|
it "updates the horizontal or vertical scrollbar depending on which delta is greater (x or y)", ->
|
||||||
|
node.style.height = 4.5 * lineHeightInPixels + 'px'
|
||||||
|
node.style.width = 20 * charWidth + 'px'
|
||||||
|
component.measureHeightAndWidth()
|
||||||
|
|
||||||
|
expect(verticalScrollbarNode.scrollTop).toBe 0
|
||||||
|
expect(horizontalScrollbarNode.scrollLeft).toBe 0
|
||||||
|
|
||||||
|
node.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: -5, wheelDeltaY: -10))
|
||||||
|
expect(verticalScrollbarNode.scrollTop).toBe 10
|
||||||
|
expect(horizontalScrollbarNode.scrollLeft).toBe 0
|
||||||
|
|
||||||
|
node.dispatchEvent(new WheelEvent('mousewheel', wheelDeltaX: -15, wheelDeltaY: -5))
|
||||||
|
expect(verticalScrollbarNode.scrollTop).toBe 10
|
||||||
|
expect(horizontalScrollbarNode.scrollLeft).toBe 15
|
||||||
|
|
||||||
|
describe "input events", ->
|
||||||
|
inputNode = null
|
||||||
|
|
||||||
|
beforeEach ->
|
||||||
|
inputNode = node.querySelector('.hidden-input')
|
||||||
|
|
||||||
|
it "inserts the newest character in the input's value into the buffer", ->
|
||||||
|
inputNode.value = 'x'
|
||||||
|
inputNode.dispatchEvent(new Event('input'))
|
||||||
|
expect(editor.lineForBufferRow(0)).toBe 'xvar quicksort = function () {'
|
||||||
|
|
||||||
|
inputNode.value = 'xy'
|
||||||
|
inputNode.dispatchEvent(new Event('input'))
|
||||||
|
expect(editor.lineForBufferRow(0)).toBe 'xyvar quicksort = function () {'
|
||||||
|
|
||||||
|
it "replaces the last character if the length of the input's value doesn't increase, as occurs with the accented character menu", ->
|
||||||
|
inputNode.value = 'u'
|
||||||
|
inputNode.dispatchEvent(new Event('input'))
|
||||||
|
expect(editor.lineForBufferRow(0)).toBe 'uvar quicksort = function () {'
|
||||||
|
|
||||||
|
inputNode.value = 'ü'
|
||||||
|
inputNode.dispatchEvent(new Event('input'))
|
||||||
|
expect(editor.lineForBufferRow(0)).toBe 'üvar quicksort = function () {'
|
||||||
|
|
||||||
|
describe "commands", ->
|
||||||
|
describe "editor:consolidate-selections", ->
|
||||||
|
it "consolidates selections on the editor model, aborting the key binding if there is only one selection", ->
|
||||||
|
spyOn(editor, 'consolidateSelections').andCallThrough()
|
||||||
|
|
||||||
|
event = new CustomEvent('editor:consolidate-selections', bubbles: true, cancelable: true)
|
||||||
|
event.abortKeyBinding = jasmine.createSpy("event.abortKeyBinding")
|
||||||
|
node.dispatchEvent(event)
|
||||||
|
|
||||||
|
expect(editor.consolidateSelections).toHaveBeenCalled()
|
||||||
|
expect(event.abortKeyBinding).toHaveBeenCalled()
|
@ -111,6 +111,21 @@ describe "Editor", ->
|
|||||||
editor.moveCursorDown()
|
editor.moveCursorDown()
|
||||||
expect(editor.getCursorBufferPosition()).toEqual [1, 1]
|
expect(editor.getCursorBufferPosition()).toEqual [1, 1]
|
||||||
|
|
||||||
|
it "emits a single 'cursors-moved' event for all moved cursors", ->
|
||||||
|
editor.on 'cursors-moved', cursorsMovedHandler = jasmine.createSpy("cursorsMovedHandler")
|
||||||
|
|
||||||
|
editor.moveCursorDown()
|
||||||
|
expect(cursorsMovedHandler.callCount).toBe 1
|
||||||
|
|
||||||
|
cursorsMovedHandler.reset()
|
||||||
|
editor.addCursorAtScreenPosition([3, 0])
|
||||||
|
editor.moveCursorDown()
|
||||||
|
expect(cursorsMovedHandler.callCount).toBe 1
|
||||||
|
|
||||||
|
cursorsMovedHandler.reset()
|
||||||
|
editor.getCursor().moveDown()
|
||||||
|
expect(cursorsMovedHandler.callCount).toBe 1
|
||||||
|
|
||||||
describe ".setCursorScreenPosition(screenPosition)", ->
|
describe ".setCursorScreenPosition(screenPosition)", ->
|
||||||
it "clears a goal column established by vertical movement", ->
|
it "clears a goal column established by vertical movement", ->
|
||||||
# set a goal column by moving down
|
# set a goal column by moving down
|
||||||
@ -645,6 +660,67 @@ describe "Editor", ->
|
|||||||
cursor2 = editor.addCursorAtBufferPosition([1,4])
|
cursor2 = editor.addCursorAtBufferPosition([1,4])
|
||||||
expect(cursor2.marker).toBe cursor1.marker
|
expect(cursor2.marker).toBe cursor1.marker
|
||||||
|
|
||||||
|
describe "autoscroll", ->
|
||||||
|
beforeEach ->
|
||||||
|
editor.manageScrollPosition = true
|
||||||
|
editor.setVerticalScrollMargin(2)
|
||||||
|
editor.setHorizontalScrollMargin(2)
|
||||||
|
editor.setLineHeight(10)
|
||||||
|
editor.setDefaultCharWidth(10)
|
||||||
|
editor.setHeight(5.5 * 10)
|
||||||
|
editor.setWidth(5.5 * 10)
|
||||||
|
|
||||||
|
it "scrolls down when the last cursor gets closer than ::verticalScrollMargin to the bottom of the editor", ->
|
||||||
|
expect(editor.getScrollTop()).toBe 0
|
||||||
|
expect(editor.getScrollBottom()).toBe 5.5 * 10
|
||||||
|
|
||||||
|
editor.setCursorScreenPosition([2, 0])
|
||||||
|
expect(editor.getScrollBottom()).toBe 5.5 * 10
|
||||||
|
|
||||||
|
editor.moveCursorDown()
|
||||||
|
expect(editor.getScrollBottom()).toBe 6 * 10
|
||||||
|
|
||||||
|
editor.moveCursorDown()
|
||||||
|
expect(editor.getScrollBottom()).toBe 7 * 10
|
||||||
|
|
||||||
|
it "scrolls up when the last cursor gets closer than ::verticalScrollMargin to the top of the editor", ->
|
||||||
|
editor.setCursorScreenPosition([11, 0])
|
||||||
|
editor.setScrollBottom(editor.getScrollHeight())
|
||||||
|
|
||||||
|
editor.moveCursorUp()
|
||||||
|
expect(editor.getScrollBottom()).toBe editor.getScrollHeight()
|
||||||
|
|
||||||
|
editor.moveCursorUp()
|
||||||
|
expect(editor.getScrollTop()).toBe 7 * 10
|
||||||
|
|
||||||
|
editor.moveCursorUp()
|
||||||
|
expect(editor.getScrollTop()).toBe 6 * 10
|
||||||
|
|
||||||
|
it "scrolls right when the last cursor gets closer than ::horizontalScrollMargin to the right of the editor", ->
|
||||||
|
expect(editor.getScrollLeft()).toBe 0
|
||||||
|
expect(editor.getScrollRight()).toBe 5.5 * 10
|
||||||
|
|
||||||
|
editor.setCursorScreenPosition([0, 2])
|
||||||
|
expect(editor.getScrollRight()).toBe 5.5 * 10
|
||||||
|
|
||||||
|
editor.moveCursorRight()
|
||||||
|
expect(editor.getScrollRight()).toBe 6 * 10
|
||||||
|
|
||||||
|
editor.moveCursorRight()
|
||||||
|
expect(editor.getScrollRight()).toBe 7 * 10
|
||||||
|
|
||||||
|
it "scrolls left when the last cursor gets closer than ::horizontalScrollMargin to the left of the editor", ->
|
||||||
|
editor.setScrollRight(editor.getScrollWidth())
|
||||||
|
editor.setCursorScreenPosition([6, 62])
|
||||||
|
|
||||||
|
expect(editor.getScrollRight()).toBe editor.getScrollWidth()
|
||||||
|
|
||||||
|
editor.moveCursorLeft()
|
||||||
|
expect(editor.getScrollLeft()).toBe 59 * 10
|
||||||
|
|
||||||
|
editor.moveCursorLeft()
|
||||||
|
expect(editor.getScrollLeft()).toBe 58 * 10
|
||||||
|
|
||||||
describe "selection", ->
|
describe "selection", ->
|
||||||
selection = null
|
selection = null
|
||||||
|
|
||||||
@ -1000,7 +1076,7 @@ describe "Editor", ->
|
|||||||
expect(selection1).toBe selection
|
expect(selection1).toBe selection
|
||||||
expect(selection1.getBufferRange()).toEqual [[2, 2], [3, 3]]
|
expect(selection1.getBufferRange()).toEqual [[2, 2], [3, 3]]
|
||||||
|
|
||||||
describe "when the preserveFolds option is false (the default)", ->
|
describe "when the 'preserveFolds' option is false (the default)", ->
|
||||||
it "removes folds that contain the selections", ->
|
it "removes folds that contain the selections", ->
|
||||||
editor.setSelectedBufferRange([[0,0], [0,0]])
|
editor.setSelectedBufferRange([[0,0], [0,0]])
|
||||||
editor.createFold(1, 4)
|
editor.createFold(1, 4)
|
||||||
@ -1014,7 +1090,7 @@ describe "Editor", ->
|
|||||||
expect(editor.lineForScreenRow(6).fold).toBeUndefined()
|
expect(editor.lineForScreenRow(6).fold).toBeUndefined()
|
||||||
expect(editor.lineForScreenRow(10).fold).toBeDefined()
|
expect(editor.lineForScreenRow(10).fold).toBeDefined()
|
||||||
|
|
||||||
describe "when the preserve folds option is true", ->
|
describe "when the 'preserveFolds' option is true", ->
|
||||||
it "does not remove folds that contain the selections", ->
|
it "does not remove folds that contain the selections", ->
|
||||||
editor.setSelectedBufferRange([[0,0], [0,0]])
|
editor.setSelectedBufferRange([[0,0], [0,0]])
|
||||||
editor.createFold(1, 4)
|
editor.createFold(1, 4)
|
||||||
@ -1023,6 +1099,24 @@ describe "Editor", ->
|
|||||||
expect(editor.isFoldedAtBufferRow(1)).toBeTruthy()
|
expect(editor.isFoldedAtBufferRow(1)).toBeTruthy()
|
||||||
expect(editor.isFoldedAtBufferRow(6)).toBeTruthy()
|
expect(editor.isFoldedAtBufferRow(6)).toBeTruthy()
|
||||||
|
|
||||||
|
describe ".setSelectedBufferRange(range)", ->
|
||||||
|
describe "when the 'autoscroll' option is true", ->
|
||||||
|
it "autoscrolls to the selection", ->
|
||||||
|
editor.manageScrollPosition = true
|
||||||
|
editor.setLineHeight(10)
|
||||||
|
editor.setDefaultCharWidth(10)
|
||||||
|
editor.setHeight(50)
|
||||||
|
editor.setWidth(50)
|
||||||
|
expect(editor.getScrollTop()).toBe 0
|
||||||
|
|
||||||
|
editor.setSelectedBufferRange([[5, 6], [6, 8]], autoscroll: true)
|
||||||
|
expect(editor.getScrollBottom()).toBe (7 + editor.getVerticalScrollMargin()) * 10
|
||||||
|
expect(editor.getScrollRight()).toBe 50
|
||||||
|
|
||||||
|
editor.setSelectedBufferRange([[6, 6], [6, 8]], autoscroll: true)
|
||||||
|
expect(editor.getScrollBottom()).toBe (7 + editor.getVerticalScrollMargin()) * 10
|
||||||
|
expect(editor.getScrollRight()).toBe (8 + editor.getHorizontalScrollMargin()) * 10
|
||||||
|
|
||||||
describe ".selectMarker(marker)", ->
|
describe ".selectMarker(marker)", ->
|
||||||
describe "if the marker is valid", ->
|
describe "if the marker is valid", ->
|
||||||
it "selects the marker's range and returns the selected range", ->
|
it "selects the marker's range and returns the selected range", ->
|
||||||
@ -2934,3 +3028,37 @@ describe "Editor", ->
|
|||||||
editor.setSoftTabs(false)
|
editor.setSoftTabs(false)
|
||||||
editor.normalizeTabsInBufferRange([[0, 0], [Infinity, Infinity]])
|
editor.normalizeTabsInBufferRange([[0, 0], [Infinity, Infinity]])
|
||||||
expect(editor.getText()).toBe ' '
|
expect(editor.getText()).toBe ' '
|
||||||
|
|
||||||
|
describe ".scrollToCursorPosition()", ->
|
||||||
|
it "scrolls the last cursor into view", ->
|
||||||
|
editor.setCursorScreenPosition([8, 8])
|
||||||
|
editor.setLineHeight(10)
|
||||||
|
editor.setDefaultCharWidth(10)
|
||||||
|
editor.setHeight(50)
|
||||||
|
editor.setWidth(50)
|
||||||
|
expect(editor.getScrollTop()).toBe 0
|
||||||
|
expect(editor.getScrollLeft()).toBe 0
|
||||||
|
|
||||||
|
editor.scrollToCursorPosition()
|
||||||
|
expect(editor.getScrollBottom()).toBe (9 + editor.getVerticalScrollMargin()) * 10
|
||||||
|
expect(editor.getScrollRight()).toBe (9 + editor.getHorizontalScrollMargin()) * 10
|
||||||
|
|
||||||
|
describe ".pageUp/Down()", ->
|
||||||
|
it "scrolls one screen height up or down", ->
|
||||||
|
editor.manageScrollPosition = true
|
||||||
|
|
||||||
|
editor.setLineHeight(10)
|
||||||
|
editor.setHeight(50)
|
||||||
|
expect(editor.getScrollHeight()).toBe 130
|
||||||
|
|
||||||
|
editor.pageDown()
|
||||||
|
expect(editor.getScrollTop()).toBe 50
|
||||||
|
|
||||||
|
editor.pageDown()
|
||||||
|
expect(editor.getScrollTop()).toBe 80
|
||||||
|
|
||||||
|
editor.pageUp()
|
||||||
|
expect(editor.getScrollTop()).toBe 30
|
||||||
|
|
||||||
|
editor.pageUp()
|
||||||
|
expect(editor.getScrollTop()).toBe 0
|
||||||
|
@ -1596,7 +1596,7 @@ describe "EditorView", ->
|
|||||||
editor.setSoftWrap(true)
|
editor.setSoftWrap(true)
|
||||||
|
|
||||||
it "doesn't show the end of line invisible at the end of lines broken due to wrapping", ->
|
it "doesn't show the end of line invisible at the end of lines broken due to wrapping", ->
|
||||||
editor.setText "a line that wraps"
|
editor.setText "a line that wraps "
|
||||||
editorView.attachToDom()
|
editorView.attachToDom()
|
||||||
editorView.setWidthInChars(6)
|
editorView.setWidthInChars(6)
|
||||||
atom.config.set "editor.showInvisibles", true
|
atom.config.set "editor.showInvisibles", true
|
||||||
@ -1604,11 +1604,11 @@ describe "EditorView", ->
|
|||||||
expect(space).toBeTruthy()
|
expect(space).toBeTruthy()
|
||||||
eol = editorView.invisibles?.eol
|
eol = editorView.invisibles?.eol
|
||||||
expect(eol).toBeTruthy()
|
expect(eol).toBeTruthy()
|
||||||
expect(editorView.renderedLines.find('.line:first').text()).toBe "a line#{space}"
|
expect(editorView.renderedLines.find('.line:first').text()).toBe "a line "
|
||||||
expect(editorView.renderedLines.find('.line:last').text()).toBe "wraps#{eol}"
|
expect(editorView.renderedLines.find('.line:last').text()).toBe "wraps#{space}#{eol}"
|
||||||
|
|
||||||
it "displays trailing carriage return using a visible non-empty value", ->
|
it "displays trailing carriage return using a visible non-empty value", ->
|
||||||
editor.setText "a line that\r\n"
|
editor.setText "a line that \r\n"
|
||||||
editorView.attachToDom()
|
editorView.attachToDom()
|
||||||
editorView.setWidthInChars(6)
|
editorView.setWidthInChars(6)
|
||||||
atom.config.set "editor.showInvisibles", true
|
atom.config.set "editor.showInvisibles", true
|
||||||
@ -1618,8 +1618,8 @@ describe "EditorView", ->
|
|||||||
expect(cr).toBeTruthy()
|
expect(cr).toBeTruthy()
|
||||||
eol = editorView.invisibles?.eol
|
eol = editorView.invisibles?.eol
|
||||||
expect(eol).toBeTruthy()
|
expect(eol).toBeTruthy()
|
||||||
expect(editorView.renderedLines.find('.line:first').text()).toBe "a line#{space}"
|
expect(editorView.renderedLines.find('.line:first').text()).toBe "a line "
|
||||||
expect(editorView.renderedLines.find('.line:eq(1)').text()).toBe "that#{cr}#{eol}"
|
expect(editorView.renderedLines.find('.line:eq(1)').text()).toBe "that#{space}#{cr}#{eol}"
|
||||||
expect(editorView.renderedLines.find('.line:last').text()).toBe "#{eol}"
|
expect(editorView.renderedLines.find('.line:last').text()).toBe "#{eol}"
|
||||||
|
|
||||||
describe "when editor.showIndentGuide is set to true", ->
|
describe "when editor.showIndentGuide is set to true", ->
|
||||||
|
@ -84,6 +84,7 @@ beforeEach ->
|
|||||||
config.set "editor.autoIndent", false
|
config.set "editor.autoIndent", false
|
||||||
config.set "core.disabledPackages", ["package-that-throws-an-exception",
|
config.set "core.disabledPackages", ["package-that-throws-an-exception",
|
||||||
"package-with-broken-package-json", "package-with-broken-keymap"]
|
"package-with-broken-package-json", "package-with-broken-keymap"]
|
||||||
|
config.set "core.useReactEditor", false
|
||||||
config.save.reset()
|
config.save.reset()
|
||||||
atom.config = config
|
atom.config = config
|
||||||
|
|
||||||
@ -243,6 +244,15 @@ window.fakeSetTimeout = (callback, ms) ->
|
|||||||
window.fakeClearTimeout = (idToClear) ->
|
window.fakeClearTimeout = (idToClear) ->
|
||||||
window.timeouts = window.timeouts.filter ([id]) -> id != idToClear
|
window.timeouts = window.timeouts.filter ([id]) -> id != idToClear
|
||||||
|
|
||||||
|
window.fakeSetInterval = (callback, ms) ->
|
||||||
|
action = ->
|
||||||
|
callback()
|
||||||
|
window.fakeSetTimeout(action, ms)
|
||||||
|
window.fakeSetTimeout(action, ms)
|
||||||
|
|
||||||
|
window.fakeClearInterval = (idToClear) ->
|
||||||
|
window.fakeClearTimeout(idToClear)
|
||||||
|
|
||||||
window.advanceClock = (delta=1) ->
|
window.advanceClock = (delta=1) ->
|
||||||
window.now += delta
|
window.now += delta
|
||||||
callbacks = []
|
callbacks = []
|
||||||
|
@ -463,3 +463,77 @@ describe "TokenizedBuffer", ->
|
|||||||
expect(tokenizedBuffer.tokenForPosition([0,0]).value).toBe ' '
|
expect(tokenizedBuffer.tokenForPosition([0,0]).value).toBe ' '
|
||||||
atom.config.set('editor.tabLength', 0)
|
atom.config.set('editor.tabLength', 0)
|
||||||
expect(tokenizedBuffer.tokenForPosition([0,0]).value).toBe ' '
|
expect(tokenizedBuffer.tokenForPosition([0,0]).value).toBe ' '
|
||||||
|
|
||||||
|
describe "leading and trailing whitespace", ->
|
||||||
|
beforeEach ->
|
||||||
|
buffer = atom.project.bufferForPathSync('sample.js')
|
||||||
|
tokenizedBuffer = new TokenizedBuffer({buffer})
|
||||||
|
fullyTokenize(tokenizedBuffer)
|
||||||
|
|
||||||
|
it "sets ::hasLeadingWhitespace to true on tokens that have leading whitespace", ->
|
||||||
|
expect(tokenizedBuffer.lineForScreenRow(0).tokens[0].hasLeadingWhitespace).toBe false
|
||||||
|
expect(tokenizedBuffer.lineForScreenRow(1).tokens[0].hasLeadingWhitespace).toBe true
|
||||||
|
expect(tokenizedBuffer.lineForScreenRow(1).tokens[1].hasLeadingWhitespace).toBe false
|
||||||
|
expect(tokenizedBuffer.lineForScreenRow(2).tokens[0].hasLeadingWhitespace).toBe true
|
||||||
|
expect(tokenizedBuffer.lineForScreenRow(2).tokens[1].hasLeadingWhitespace).toBe true
|
||||||
|
expect(tokenizedBuffer.lineForScreenRow(2).tokens[2].hasLeadingWhitespace).toBe false
|
||||||
|
|
||||||
|
# The 4th token *has* leading whitespace, but isn't entirely whitespace
|
||||||
|
buffer.insert([5, 0], ' ')
|
||||||
|
expect(tokenizedBuffer.lineForScreenRow(5).tokens[3].hasLeadingWhitespace).toBe true
|
||||||
|
expect(tokenizedBuffer.lineForScreenRow(5).tokens[4].hasLeadingWhitespace).toBe false
|
||||||
|
|
||||||
|
# Lines that are *only* whitespace are not considered to have leading whitespace
|
||||||
|
buffer.insert([10, 0], ' ')
|
||||||
|
expect(tokenizedBuffer.lineForScreenRow(10).tokens[0].hasLeadingWhitespace).toBe false
|
||||||
|
|
||||||
|
it "sets ::hasTrailingWhitespace to true on tokens that have trailing whitespace", ->
|
||||||
|
buffer.insert([0, Infinity], ' ')
|
||||||
|
expect(tokenizedBuffer.lineForScreenRow(0).tokens[11].hasTrailingWhitespace).toBe false
|
||||||
|
expect(tokenizedBuffer.lineForScreenRow(0).tokens[12].hasTrailingWhitespace).toBe true
|
||||||
|
|
||||||
|
# The last token *has* trailing whitespace, but isn't entirely whitespace
|
||||||
|
buffer.setTextInRange([[2, 39], [2, 40]], ' ')
|
||||||
|
expect(tokenizedBuffer.lineForScreenRow(2).tokens[14].hasTrailingWhitespace).toBe false
|
||||||
|
expect(tokenizedBuffer.lineForScreenRow(2).tokens[15].hasTrailingWhitespace).toBe true
|
||||||
|
|
||||||
|
# Lines that are *only* whitespace are considered to have trailing whitespace
|
||||||
|
buffer.insert([10, 0], ' ')
|
||||||
|
expect(tokenizedBuffer.lineForScreenRow(10).tokens[0].hasTrailingWhitespace).toBe true
|
||||||
|
|
||||||
|
it "only marks trailing whitespace on the last segment of a soft-wrapped line", ->
|
||||||
|
buffer.insert([0, Infinity], ' ')
|
||||||
|
tokenizedLine = tokenizedBuffer.lineForScreenRow(0)
|
||||||
|
[segment1, segment2] = tokenizedLine.softWrapAt(16)
|
||||||
|
expect(segment1.tokens[5].value).toBe ' '
|
||||||
|
expect(segment1.tokens[5].hasTrailingWhitespace).toBe false
|
||||||
|
expect(segment2.tokens[6].value).toBe ' '
|
||||||
|
expect(segment2.tokens[6].hasTrailingWhitespace).toBe true
|
||||||
|
|
||||||
|
describe "indent level", ->
|
||||||
|
beforeEach ->
|
||||||
|
buffer = atom.project.bufferForPathSync('sample.js')
|
||||||
|
tokenizedBuffer = new TokenizedBuffer({buffer})
|
||||||
|
fullyTokenize(tokenizedBuffer)
|
||||||
|
|
||||||
|
describe "when the line is non-empty", ->
|
||||||
|
it "has an indent level based on the leading whitespace on the line", ->
|
||||||
|
expect(tokenizedBuffer.lineForScreenRow(0).indentLevel).toBe 0
|
||||||
|
expect(tokenizedBuffer.lineForScreenRow(1).indentLevel).toBe 1
|
||||||
|
expect(tokenizedBuffer.lineForScreenRow(2).indentLevel).toBe 2
|
||||||
|
buffer.insert([2, 0], ' ')
|
||||||
|
expect(tokenizedBuffer.lineForScreenRow(2).indentLevel).toBe 2.5
|
||||||
|
|
||||||
|
describe "when the line is empty", ->
|
||||||
|
it "assumes the indentation level of the first non-empty line below or above if one exists", ->
|
||||||
|
buffer.insert([12, 0], ' ')
|
||||||
|
buffer.insert([12, Infinity], '\n\n')
|
||||||
|
expect(tokenizedBuffer.lineForScreenRow(13).indentLevel).toBe 2
|
||||||
|
expect(tokenizedBuffer.lineForScreenRow(14).indentLevel).toBe 2
|
||||||
|
|
||||||
|
buffer.insert([1, Infinity], '\n\n')
|
||||||
|
expect(tokenizedBuffer.lineForScreenRow(2).indentLevel).toBe 2
|
||||||
|
expect(tokenizedBuffer.lineForScreenRow(3).indentLevel).toBe 2
|
||||||
|
|
||||||
|
buffer.setText('\n\n\n')
|
||||||
|
expect(tokenizedBuffer.lineForScreenRow(1).indentLevel).toBe 0
|
||||||
|
22
spec/tokenized-line-spec.coffee
Normal file
22
spec/tokenized-line-spec.coffee
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
describe "TokenizedLine", ->
|
||||||
|
editor = null
|
||||||
|
|
||||||
|
beforeEach ->
|
||||||
|
waitsForPromise -> atom.packages.activatePackage('language-coffee-script')
|
||||||
|
|
||||||
|
describe "::getScopeTree()", ->
|
||||||
|
it "returns a tree whose inner nodes are scopes and whose leaf nodes are tokens in those scopes", ->
|
||||||
|
editor = atom.project.openSync('coffee.coffee')
|
||||||
|
|
||||||
|
ensureValidScopeTree = (scopeTree, scopes=[]) ->
|
||||||
|
if scopeTree.children?
|
||||||
|
for child in scopeTree.children
|
||||||
|
ensureValidScopeTree(child, scopes.concat([scopeTree.scope]))
|
||||||
|
else
|
||||||
|
expect(scopeTree).toBe tokens[tokenIndex++]
|
||||||
|
expect(scopes).toEqual scopeTree.scopes
|
||||||
|
|
||||||
|
tokenIndex = 0
|
||||||
|
tokens = editor.lineForScreenRow(1).tokens
|
||||||
|
scopeTree = editor.lineForScreenRow(1).getScopeTree()
|
||||||
|
ensureValidScopeTree(scopeTree)
|
@ -157,6 +157,9 @@ class Atom extends Model
|
|||||||
# Still set NODE_PATH since tasks may need it.
|
# Still set NODE_PATH since tasks may need it.
|
||||||
process.env.NODE_PATH = exportsPath
|
process.env.NODE_PATH = exportsPath
|
||||||
|
|
||||||
|
# Make react.js faster
|
||||||
|
process.env.NODE_ENV ?= 'production'
|
||||||
|
|
||||||
@config = new Config({configDirPath, resourcePath})
|
@config = new Config({configDirPath, resourcePath})
|
||||||
@keymaps = new KeymapManager({configDirPath, resourcePath})
|
@keymaps = new KeymapManager({configDirPath, resourcePath})
|
||||||
@keymap = @keymaps # Deprecated
|
@keymap = @keymaps # Deprecated
|
||||||
|
13
src/cursor-component.coffee
Normal file
13
src/cursor-component.coffee
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
React = require 'react'
|
||||||
|
{div} = require 'reactionary'
|
||||||
|
|
||||||
|
module.exports =
|
||||||
|
CursorComponent = React.createClass
|
||||||
|
displayName: 'CursorComponent'
|
||||||
|
|
||||||
|
render: ->
|
||||||
|
{top, left, height, width} = @props.cursor.getPixelRect()
|
||||||
|
className = 'cursor'
|
||||||
|
className += ' blink-off' if @props.blinkOff
|
||||||
|
|
||||||
|
div className: className, style: {top, left, height, width}
|
@ -1,5 +1,5 @@
|
|||||||
{Point, Range} = require 'text-buffer'
|
{Point, Range} = require 'text-buffer'
|
||||||
{Emitter} = require 'emissary'
|
{Model} = require 'theorist'
|
||||||
_ = require 'underscore-plus'
|
_ = require 'underscore-plus'
|
||||||
|
|
||||||
# Public: The `Cursor` class represents the little blinking line identifying
|
# Public: The `Cursor` class represents the little blinking line identifying
|
||||||
@ -8,9 +8,7 @@ _ = require 'underscore-plus'
|
|||||||
# Cursors belong to {Editor}s and have some metadata attached in the form
|
# Cursors belong to {Editor}s and have some metadata attached in the form
|
||||||
# of a {Marker}.
|
# of a {Marker}.
|
||||||
module.exports =
|
module.exports =
|
||||||
class Cursor
|
class Cursor extends Model
|
||||||
Emitter.includeInto(this)
|
|
||||||
|
|
||||||
screenPosition: null
|
screenPosition: null
|
||||||
bufferPosition: null
|
bufferPosition: null
|
||||||
goalColumn: null
|
goalColumn: null
|
||||||
@ -18,7 +16,8 @@ class Cursor
|
|||||||
needsAutoscroll: null
|
needsAutoscroll: null
|
||||||
|
|
||||||
# Instantiated by an {Editor}
|
# Instantiated by an {Editor}
|
||||||
constructor: ({@editor, @marker}) ->
|
constructor: ({@editor, @marker, id}) ->
|
||||||
|
@assignId(id)
|
||||||
@updateVisibility()
|
@updateVisibility()
|
||||||
@marker.on 'changed', (e) =>
|
@marker.on 'changed', (e) =>
|
||||||
@updateVisibility()
|
@updateVisibility()
|
||||||
@ -27,7 +26,12 @@ class Cursor
|
|||||||
{textChanged} = e
|
{textChanged} = e
|
||||||
return if oldHeadScreenPosition.isEqual(newHeadScreenPosition)
|
return if oldHeadScreenPosition.isEqual(newHeadScreenPosition)
|
||||||
|
|
||||||
|
# Supports old editor view
|
||||||
@needsAutoscroll ?= @isLastCursor() and !textChanged
|
@needsAutoscroll ?= @isLastCursor() and !textChanged
|
||||||
|
|
||||||
|
# Supports react editor view
|
||||||
|
@autoscroll() if @needsAutoscroll and @editor.manageScrollPosition
|
||||||
|
|
||||||
@goalColumn = null
|
@goalColumn = null
|
||||||
|
|
||||||
movedEvent =
|
movedEvent =
|
||||||
@ -38,7 +42,7 @@ class Cursor
|
|||||||
textChanged: textChanged
|
textChanged: textChanged
|
||||||
|
|
||||||
@emit 'moved', movedEvent
|
@emit 'moved', movedEvent
|
||||||
@editor.emit 'cursor-moved', movedEvent
|
@editor.cursorMoved(movedEvent)
|
||||||
@marker.on 'destroyed', =>
|
@marker.on 'destroyed', =>
|
||||||
@destroyed = true
|
@destroyed = true
|
||||||
@editor.removeCursor(this)
|
@editor.removeCursor(this)
|
||||||
@ -54,6 +58,9 @@ class Cursor
|
|||||||
unless fn()
|
unless fn()
|
||||||
@emit 'autoscrolled' if @needsAutoscroll
|
@emit 'autoscrolled' if @needsAutoscroll
|
||||||
|
|
||||||
|
getPixelRect: ->
|
||||||
|
@editor.pixelRectForScreenRange(@getScreenRange())
|
||||||
|
|
||||||
# Public: Moves a cursor to a given screen position.
|
# Public: Moves a cursor to a given screen position.
|
||||||
#
|
#
|
||||||
# screenPosition - An {Array} of two numbers: the screen row, and the screen
|
# screenPosition - An {Array} of two numbers: the screen row, and the screen
|
||||||
@ -69,6 +76,10 @@ class Cursor
|
|||||||
getScreenPosition: ->
|
getScreenPosition: ->
|
||||||
@marker.getHeadScreenPosition()
|
@marker.getHeadScreenPosition()
|
||||||
|
|
||||||
|
getScreenRange: ->
|
||||||
|
{row, column} = @getScreenPosition()
|
||||||
|
new Range(new Point(row, column), new Point(row, column + 1))
|
||||||
|
|
||||||
# Public: Moves a cursor to a given buffer position.
|
# Public: Moves a cursor to a given buffer position.
|
||||||
#
|
#
|
||||||
# bufferPosition - An {Array} of two numbers: the buffer row, and the buffer
|
# bufferPosition - An {Array} of two numbers: the buffer row, and the buffer
|
||||||
@ -84,6 +95,9 @@ class Cursor
|
|||||||
getBufferPosition: ->
|
getBufferPosition: ->
|
||||||
@marker.getHeadBufferPosition()
|
@marker.getHeadBufferPosition()
|
||||||
|
|
||||||
|
autoscroll: ->
|
||||||
|
@editor.scrollToScreenRange(@getScreenRange())
|
||||||
|
|
||||||
# Public: If the marker range is empty, the cursor is marked as being visible.
|
# Public: If the marker range is empty, the cursor is marked as being visible.
|
||||||
updateVisibility: ->
|
updateVisibility: ->
|
||||||
@setVisible(@marker.getBufferRange().isEmpty())
|
@setVisible(@marker.getBufferRange().isEmpty())
|
||||||
|
50
src/cursors-component.coffee
Normal file
50
src/cursors-component.coffee
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
React = require 'react'
|
||||||
|
{div} = require 'reactionary'
|
||||||
|
{debounce} = require 'underscore-plus'
|
||||||
|
SubscriberMixin = require './subscriber-mixin'
|
||||||
|
CursorComponent = require './cursor-component'
|
||||||
|
|
||||||
|
|
||||||
|
module.exports =
|
||||||
|
CursorsComponent = React.createClass
|
||||||
|
displayName: 'CursorsComponent'
|
||||||
|
mixins: [SubscriberMixin]
|
||||||
|
|
||||||
|
cursorBlinkIntervalHandle: null
|
||||||
|
|
||||||
|
render: ->
|
||||||
|
{editor} = @props
|
||||||
|
blinkOff = @state.blinkCursorsOff
|
||||||
|
|
||||||
|
div className: 'cursors',
|
||||||
|
if @isMounted()
|
||||||
|
for selection in editor.getSelections()
|
||||||
|
if selection.isEmpty() and editor.selectionIntersectsVisibleRowRange(selection)
|
||||||
|
{cursor} = selection
|
||||||
|
CursorComponent({key: cursor.id, cursor, blinkOff})
|
||||||
|
|
||||||
|
getInitialState: ->
|
||||||
|
blinkCursorsOff: false
|
||||||
|
|
||||||
|
componentDidMount: ->
|
||||||
|
{editor} = @props
|
||||||
|
@startBlinkingCursors()
|
||||||
|
|
||||||
|
componentWillUnmount: ->
|
||||||
|
clearInterval(@cursorBlinkIntervalHandle)
|
||||||
|
|
||||||
|
componentWillUpdate: ({cursorsMoved}) ->
|
||||||
|
@pauseCursorBlinking() if cursorsMoved
|
||||||
|
|
||||||
|
startBlinkingCursors: ->
|
||||||
|
@cursorBlinkIntervalHandle = setInterval(@toggleCursorBlink, @props.cursorBlinkPeriod / 2)
|
||||||
|
|
||||||
|
startBlinkingCursorsAfterDelay: null # Created lazily
|
||||||
|
|
||||||
|
toggleCursorBlink: -> @setState(blinkCursorsOff: not @state.blinkCursorsOff)
|
||||||
|
|
||||||
|
pauseCursorBlinking: ->
|
||||||
|
@state.blinkCursorsOff = false
|
||||||
|
clearInterval(@cursorBlinkIntervalHandle)
|
||||||
|
@startBlinkingCursorsAfterDelay ?= debounce(@startBlinkingCursors, @props.cursorBlinkResumeDelay)
|
||||||
|
@startBlinkingCursorsAfterDelay()
|
15
src/custom-event-mixin.coffee
Normal file
15
src/custom-event-mixin.coffee
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
module.exports =
|
||||||
|
CustomEventMixin =
|
||||||
|
componentWillMount: ->
|
||||||
|
@customEventListeners = {}
|
||||||
|
|
||||||
|
componentWillUnmount: ->
|
||||||
|
for name, listeners in @customEventListeners
|
||||||
|
for listener in listeners
|
||||||
|
@getDOMNode().removeEventListener(name, listener)
|
||||||
|
|
||||||
|
addCustomEventListeners: (customEventListeners) ->
|
||||||
|
for name, listener of customEventListeners
|
||||||
|
@customEventListeners[name] ?= []
|
||||||
|
@customEventListeners[name].push(listener)
|
||||||
|
@getDOMNode().addEventListener(name, listener)
|
@ -54,6 +54,9 @@ class DisplayBufferMarker
|
|||||||
setBufferRange: (bufferRange, options) ->
|
setBufferRange: (bufferRange, options) ->
|
||||||
@bufferMarker.setRange(bufferRange, options)
|
@bufferMarker.setRange(bufferRange, options)
|
||||||
|
|
||||||
|
getPixelRange: ->
|
||||||
|
@displayBuffer.pixelRangeForScreenRange(@getScreenRange(), false)
|
||||||
|
|
||||||
# Retrieves the screen position of the marker's head.
|
# Retrieves the screen position of the marker's head.
|
||||||
#
|
#
|
||||||
# Returns a {Point}.
|
# Returns a {Point}.
|
||||||
|
@ -20,14 +20,25 @@ class DisplayBuffer extends Model
|
|||||||
Serializable.includeInto(this)
|
Serializable.includeInto(this)
|
||||||
|
|
||||||
@properties
|
@properties
|
||||||
|
manageScrollPosition: false
|
||||||
softWrap: null
|
softWrap: null
|
||||||
editorWidthInChars: null
|
editorWidthInChars: null
|
||||||
|
lineHeight: null
|
||||||
|
defaultCharWidth: null
|
||||||
|
height: null
|
||||||
|
width: null
|
||||||
|
scrollTop: 0
|
||||||
|
scrollLeft: 0
|
||||||
|
|
||||||
|
verticalScrollMargin: 2
|
||||||
|
horizontalScrollMargin: 6
|
||||||
|
|
||||||
constructor: ({tabLength, @editorWidthInChars, @tokenizedBuffer, buffer}={}) ->
|
constructor: ({tabLength, @editorWidthInChars, @tokenizedBuffer, buffer}={}) ->
|
||||||
super
|
super
|
||||||
@softWrap ?= atom.config.get('editor.softWrap') ? false
|
@softWrap ?= atom.config.get('editor.softWrap') ? false
|
||||||
@tokenizedBuffer ?= new TokenizedBuffer({tabLength, buffer})
|
@tokenizedBuffer ?= new TokenizedBuffer({tabLength, buffer})
|
||||||
@buffer = @tokenizedBuffer.buffer
|
@buffer = @tokenizedBuffer.buffer
|
||||||
|
@charWidthsByScope = {}
|
||||||
@markers = {}
|
@markers = {}
|
||||||
@foldsByMarkerId = {}
|
@foldsByMarkerId = {}
|
||||||
@updateAllScreenLines()
|
@updateAllScreenLines()
|
||||||
@ -51,6 +62,8 @@ class DisplayBuffer extends Model
|
|||||||
id: @id
|
id: @id
|
||||||
softWrap: @softWrap
|
softWrap: @softWrap
|
||||||
editorWidthInChars: @editorWidthInChars
|
editorWidthInChars: @editorWidthInChars
|
||||||
|
scrollTop: @scrollTop
|
||||||
|
scrollLeft: @scrollLeft
|
||||||
tokenizedBuffer: @tokenizedBuffer.serialize()
|
tokenizedBuffer: @tokenizedBuffer.serialize()
|
||||||
|
|
||||||
deserializeParams: (params) ->
|
deserializeParams: (params) ->
|
||||||
@ -59,6 +72,9 @@ class DisplayBuffer extends Model
|
|||||||
|
|
||||||
copy: ->
|
copy: ->
|
||||||
newDisplayBuffer = new DisplayBuffer({@buffer, tabLength: @getTabLength()})
|
newDisplayBuffer = new DisplayBuffer({@buffer, tabLength: @getTabLength()})
|
||||||
|
newDisplayBuffer.setScrollTop(@getScrollTop())
|
||||||
|
newDisplayBuffer.setScrollLeft(@getScrollLeft())
|
||||||
|
|
||||||
for marker in @findMarkers(displayBufferId: @id)
|
for marker in @findMarkers(displayBufferId: @id)
|
||||||
marker.copy(displayBufferId: newDisplayBuffer.id)
|
marker.copy(displayBufferId: newDisplayBuffer.id)
|
||||||
newDisplayBuffer
|
newDisplayBuffer
|
||||||
@ -89,6 +105,151 @@ class DisplayBuffer extends Model
|
|||||||
# visible - A {Boolean} indicating of the tokenized buffer is shown
|
# visible - A {Boolean} indicating of the tokenized buffer is shown
|
||||||
setVisible: (visible) -> @tokenizedBuffer.setVisible(visible)
|
setVisible: (visible) -> @tokenizedBuffer.setVisible(visible)
|
||||||
|
|
||||||
|
getVerticalScrollMargin: -> @verticalScrollMargin
|
||||||
|
setVerticalScrollMargin: (@verticalScrollMargin) -> @verticalScrollMargin
|
||||||
|
|
||||||
|
getHorizontalScrollMargin: -> @horizontalScrollMargin
|
||||||
|
setHorizontalScrollMargin: (@horizontalScrollMargin) -> @horizontalScrollMargin
|
||||||
|
|
||||||
|
getHeight: -> @height ? @getScrollHeight()
|
||||||
|
setHeight: (@height) -> @height
|
||||||
|
|
||||||
|
getWidth: -> @width ? @getScrollWidth()
|
||||||
|
setWidth: (newWidth) ->
|
||||||
|
oldWidth = @width
|
||||||
|
@width = newWidth
|
||||||
|
@updateWrappedScreenLines() if newWidth isnt oldWidth and @softWrap
|
||||||
|
@width
|
||||||
|
|
||||||
|
getScrollTop: -> @scrollTop
|
||||||
|
setScrollTop: (scrollTop) ->
|
||||||
|
if @manageScrollPosition
|
||||||
|
@scrollTop = Math.max(0, Math.min(@getScrollHeight() - @getHeight(), scrollTop))
|
||||||
|
else
|
||||||
|
@scrollTop = scrollTop
|
||||||
|
|
||||||
|
getScrollBottom: -> @scrollTop + @height
|
||||||
|
setScrollBottom: (scrollBottom) ->
|
||||||
|
@setScrollTop(scrollBottom - @height)
|
||||||
|
@getScrollBottom()
|
||||||
|
|
||||||
|
getScrollLeft: -> @scrollLeft
|
||||||
|
setScrollLeft: (scrollLeft) ->
|
||||||
|
if @manageScrollPosition
|
||||||
|
@scrollLeft = Math.max(0, Math.min(@getScrollWidth() - @getWidth(), scrollLeft))
|
||||||
|
else
|
||||||
|
@scrollLeft = scrollLeft
|
||||||
|
|
||||||
|
getScrollRight: -> @scrollLeft + @width
|
||||||
|
setScrollRight: (scrollRight) ->
|
||||||
|
@setScrollLeft(scrollRight - @width)
|
||||||
|
@getScrollRight()
|
||||||
|
|
||||||
|
getLineHeight: -> @lineHeight
|
||||||
|
setLineHeight: (@lineHeight) -> @lineHeight
|
||||||
|
|
||||||
|
getDefaultCharWidth: -> @defaultCharWidth
|
||||||
|
setDefaultCharWidth: (@defaultCharWidth) -> @defaultCharWidth
|
||||||
|
|
||||||
|
getScopedCharWidth: (scopeNames, char) ->
|
||||||
|
@getScopedCharWidths(scopeNames)[char]
|
||||||
|
|
||||||
|
getScopedCharWidths: (scopeNames) ->
|
||||||
|
scope = @charWidthsByScope
|
||||||
|
for scopeName in scopeNames
|
||||||
|
scope[scopeName] ?= {}
|
||||||
|
scope = scope[scopeName]
|
||||||
|
scope.charWidths ?= {}
|
||||||
|
scope.charWidths
|
||||||
|
|
||||||
|
setScopedCharWidth: (scopeNames, char, width) ->
|
||||||
|
@getScopedCharWidths(scopeNames)[char] = width
|
||||||
|
|
||||||
|
setScopedCharWidths: (scopeNames, charWidths) ->
|
||||||
|
_.extend(@getScopedCharWidths(scopeNames), charWidths)
|
||||||
|
|
||||||
|
clearScopedCharWidths: ->
|
||||||
|
@charWidthsByScope = {}
|
||||||
|
|
||||||
|
getScrollHeight: ->
|
||||||
|
unless @getLineHeight() > 0
|
||||||
|
throw new Error("You must assign lineHeight before calling ::getScrollHeight()")
|
||||||
|
|
||||||
|
@getLineCount() * @getLineHeight()
|
||||||
|
|
||||||
|
getScrollWidth: ->
|
||||||
|
@getMaxLineLength() * @getDefaultCharWidth()
|
||||||
|
|
||||||
|
getVisibleRowRange: ->
|
||||||
|
unless @getLineHeight() > 0
|
||||||
|
throw new Error("You must assign a non-zero lineHeight before calling ::getVisibleRowRange()")
|
||||||
|
|
||||||
|
heightInLines = Math.ceil(@getHeight() / @getLineHeight()) + 1
|
||||||
|
startRow = Math.floor(@getScrollTop() / @getLineHeight())
|
||||||
|
endRow = Math.min(@getLineCount(), Math.ceil(startRow + heightInLines))
|
||||||
|
[startRow, endRow]
|
||||||
|
|
||||||
|
intersectsVisibleRowRange: (startRow, endRow) ->
|
||||||
|
[visibleStart, visibleEnd] = @getVisibleRowRange()
|
||||||
|
not (endRow <= visibleStart or visibleEnd <= startRow)
|
||||||
|
|
||||||
|
selectionIntersectsVisibleRowRange: (selection) ->
|
||||||
|
{start, end} = selection.getScreenRange()
|
||||||
|
@intersectsVisibleRowRange(start.row, end.row + 1)
|
||||||
|
|
||||||
|
scrollToScreenRange: (screenRange) ->
|
||||||
|
verticalScrollMarginInPixels = @getVerticalScrollMargin() * @getLineHeight()
|
||||||
|
horizontalScrollMarginInPixels = @getHorizontalScrollMargin() * @getDefaultCharWidth()
|
||||||
|
|
||||||
|
{top, left, height, width} = @pixelRectForScreenRange(screenRange)
|
||||||
|
bottom = top + height
|
||||||
|
right = left + width
|
||||||
|
desiredScrollTop = top - verticalScrollMarginInPixels
|
||||||
|
desiredScrollBottom = bottom + verticalScrollMarginInPixels
|
||||||
|
desiredScrollLeft = left - horizontalScrollMarginInPixels
|
||||||
|
desiredScrollRight = right + horizontalScrollMarginInPixels
|
||||||
|
|
||||||
|
if desiredScrollTop < @getScrollTop()
|
||||||
|
@setScrollTop(desiredScrollTop)
|
||||||
|
else if desiredScrollBottom > @getScrollBottom()
|
||||||
|
@setScrollBottom(desiredScrollBottom)
|
||||||
|
|
||||||
|
if desiredScrollLeft < @getScrollLeft()
|
||||||
|
@setScrollLeft(desiredScrollLeft)
|
||||||
|
else if desiredScrollRight > @getScrollRight()
|
||||||
|
@setScrollRight(desiredScrollRight)
|
||||||
|
|
||||||
|
scrollToScreenPosition: (screenPosition) ->
|
||||||
|
@scrollToScreenRange(new Range(screenPosition, screenPosition))
|
||||||
|
|
||||||
|
scrollToBufferPosition: (bufferPosition) ->
|
||||||
|
@scrollToScreenPosition(@screenPositionForBufferPosition(bufferPosition))
|
||||||
|
|
||||||
|
pixelRectForScreenRange: (screenRange) ->
|
||||||
|
if screenRange.end.row > screenRange.start.row
|
||||||
|
top = @pixelPositionForScreenPosition(screenRange.start).top
|
||||||
|
left = 0
|
||||||
|
height = (screenRange.end.row - screenRange.start.row + 1) * @getLineHeight()
|
||||||
|
width = @getScrollWidth()
|
||||||
|
else
|
||||||
|
{top, left} = @pixelPositionForScreenPosition(screenRange.start)
|
||||||
|
height = @getLineHeight()
|
||||||
|
width = @pixelPositionForScreenPosition(screenRange.end).left - left
|
||||||
|
|
||||||
|
{top, left, width, height}
|
||||||
|
|
||||||
|
# Retrieves the current tab length.
|
||||||
|
#
|
||||||
|
# Returns a {Number}.
|
||||||
|
getTabLength: ->
|
||||||
|
@tokenizedBuffer.getTabLength()
|
||||||
|
|
||||||
|
# Specifies the tab length.
|
||||||
|
#
|
||||||
|
# tabLength - A {Number} that defines the new tab length.
|
||||||
|
setTabLength: (tabLength) ->
|
||||||
|
@tokenizedBuffer.setTabLength(tabLength)
|
||||||
|
|
||||||
# Deprecated: Use the softWrap property directly
|
# Deprecated: Use the softWrap property directly
|
||||||
setSoftWrap: (@softWrap) -> @softWrap
|
setSoftWrap: (@softWrap) -> @softWrap
|
||||||
|
|
||||||
@ -105,12 +266,19 @@ class DisplayBuffer extends Model
|
|||||||
if editorWidthInChars isnt previousWidthInChars and @softWrap
|
if editorWidthInChars isnt previousWidthInChars and @softWrap
|
||||||
@updateWrappedScreenLines()
|
@updateWrappedScreenLines()
|
||||||
|
|
||||||
getSoftWrapColumn: ->
|
getEditorWidthInChars: ->
|
||||||
if atom.config.get('editor.softWrapAtPreferredLineLength')
|
width = @getWidth()
|
||||||
Math.min(@editorWidthInChars, atom.config.getPositiveInt('editor.preferredLineLength', @editorWidthInChars))
|
if width? and @defaultCharWidth > 0
|
||||||
|
Math.floor(width / @defaultCharWidth)
|
||||||
else
|
else
|
||||||
@editorWidthInChars
|
@editorWidthInChars
|
||||||
|
|
||||||
|
getSoftWrapColumn: ->
|
||||||
|
if atom.config.get('editor.softWrapAtPreferredLineLength')
|
||||||
|
Math.min(@getEditorWidthInChars(), atom.config.getPositiveInt('editor.preferredLineLength', @getEditorWidthInChars()))
|
||||||
|
else
|
||||||
|
@getEditorWidthInChars()
|
||||||
|
|
||||||
# Gets the screen line for the given screen row.
|
# Gets the screen line for the given screen row.
|
||||||
#
|
#
|
||||||
# screenRow - A {Number} indicating the screen row.
|
# screenRow - A {Number} indicating the screen row.
|
||||||
@ -134,6 +302,9 @@ class DisplayBuffer extends Model
|
|||||||
getLines: ->
|
getLines: ->
|
||||||
new Array(@screenLines...)
|
new Array(@screenLines...)
|
||||||
|
|
||||||
|
indentLevelForLine: (line) ->
|
||||||
|
@tokenizedBuffer.indentLevelForLine(line)
|
||||||
|
|
||||||
# Given starting and ending screen rows, this returns an array of the
|
# Given starting and ending screen rows, this returns an array of the
|
||||||
# buffer rows corresponding to every screen row in the range
|
# buffer rows corresponding to every screen row in the range
|
||||||
#
|
#
|
||||||
@ -273,6 +444,52 @@ class DisplayBuffer extends Model
|
|||||||
end = @bufferPositionForScreenPosition(screenRange.end)
|
end = @bufferPositionForScreenPosition(screenRange.end)
|
||||||
new Range(start, end)
|
new Range(start, end)
|
||||||
|
|
||||||
|
pixelRangeForScreenRange: (screenRange, clip=true) ->
|
||||||
|
{start, end} = Range.fromObject(screenRange)
|
||||||
|
{start: @pixelPositionForScreenPosition(start, clip), end: @pixelPositionForScreenPosition(end, clip)}
|
||||||
|
|
||||||
|
pixelPositionForScreenPosition: (screenPosition, clip=true) ->
|
||||||
|
screenPosition = Point.fromObject(screenPosition)
|
||||||
|
screenPosition = @clipScreenPosition(screenPosition) if clip
|
||||||
|
|
||||||
|
targetRow = screenPosition.row
|
||||||
|
targetColumn = screenPosition.column
|
||||||
|
defaultCharWidth = @defaultCharWidth
|
||||||
|
|
||||||
|
top = targetRow * @lineHeight
|
||||||
|
left = 0
|
||||||
|
column = 0
|
||||||
|
for token in @lineForRow(targetRow).tokens
|
||||||
|
charWidths = @getScopedCharWidths(token.scopes)
|
||||||
|
for char in token.value
|
||||||
|
return {top, left} if column is targetColumn
|
||||||
|
left += charWidths[char] ? defaultCharWidth
|
||||||
|
column++
|
||||||
|
{top, left}
|
||||||
|
|
||||||
|
screenPositionForPixelPosition: (pixelPosition) ->
|
||||||
|
targetTop = pixelPosition.top
|
||||||
|
targetLeft = pixelPosition.left
|
||||||
|
defaultCharWidth = @defaultCharWidth
|
||||||
|
row = Math.floor(targetTop / @getLineHeight())
|
||||||
|
row = Math.min(row, @getLastRow())
|
||||||
|
row = Math.max(0, row)
|
||||||
|
|
||||||
|
left = 0
|
||||||
|
column = 0
|
||||||
|
for token in @lineForRow(row).tokens
|
||||||
|
charWidths = @getScopedCharWidths(token.scopes)
|
||||||
|
for char in token.value
|
||||||
|
charWidth = charWidths[char] ? defaultCharWidth
|
||||||
|
break if targetLeft <= left + (charWidth / 2)
|
||||||
|
left += charWidth
|
||||||
|
column++
|
||||||
|
|
||||||
|
new Point(row, column)
|
||||||
|
|
||||||
|
pixelPositionForBufferPosition: (bufferPosition) ->
|
||||||
|
@pixelPositionForScreenPosition(@screenPositionForBufferPosition(bufferPosition))
|
||||||
|
|
||||||
# Gets the number of screen lines.
|
# Gets the number of screen lines.
|
||||||
#
|
#
|
||||||
# Returns a {Number}.
|
# Returns a {Number}.
|
||||||
@ -358,18 +575,6 @@ class DisplayBuffer extends Model
|
|||||||
tokenForBufferPosition: (bufferPosition) ->
|
tokenForBufferPosition: (bufferPosition) ->
|
||||||
@tokenizedBuffer.tokenForPosition(bufferPosition)
|
@tokenizedBuffer.tokenForPosition(bufferPosition)
|
||||||
|
|
||||||
# Retrieves the current tab length.
|
|
||||||
#
|
|
||||||
# Returns a {Number}.
|
|
||||||
getTabLength: ->
|
|
||||||
@tokenizedBuffer.getTabLength()
|
|
||||||
|
|
||||||
# Specifies the tab length.
|
|
||||||
#
|
|
||||||
# tabLength - A {Number} that defines the new tab length.
|
|
||||||
setTabLength: (tabLength) ->
|
|
||||||
@tokenizedBuffer.setTabLength(tabLength)
|
|
||||||
|
|
||||||
# Get the grammar for this buffer.
|
# Get the grammar for this buffer.
|
||||||
#
|
#
|
||||||
# Returns the current {Grammar} or the {NullGrammar}.
|
# Returns the current {Grammar} or the {NullGrammar}.
|
||||||
|
338
src/editor-component.coffee
Normal file
338
src/editor-component.coffee
Normal file
@ -0,0 +1,338 @@
|
|||||||
|
React = require 'react'
|
||||||
|
{div, span} = require 'reactionary'
|
||||||
|
{debounce} = require 'underscore-plus'
|
||||||
|
|
||||||
|
GutterComponent = require './gutter-component'
|
||||||
|
EditorScrollViewComponent = require './editor-scroll-view-component'
|
||||||
|
ScrollbarComponent = require './scrollbar-component'
|
||||||
|
SubscriberMixin = require './subscriber-mixin'
|
||||||
|
|
||||||
|
module.exports =
|
||||||
|
EditorComponent = React.createClass
|
||||||
|
displayName: 'EditorComponent'
|
||||||
|
mixins: [SubscriberMixin]
|
||||||
|
|
||||||
|
pendingScrollTop: null
|
||||||
|
pendingScrollLeft: null
|
||||||
|
selectOnMouseMove: false
|
||||||
|
batchingUpdates: false
|
||||||
|
updateRequested: false
|
||||||
|
cursorsMoved: false
|
||||||
|
preservedRowRange: null
|
||||||
|
scrollingVertically: false
|
||||||
|
|
||||||
|
render: ->
|
||||||
|
{focused, fontSize, lineHeight, fontFamily, showIndentGuide} = @state
|
||||||
|
{editor, cursorBlinkPeriod, cursorBlinkResumeDelay} = @props
|
||||||
|
if @isMounted()
|
||||||
|
renderedRowRange = @getRenderedRowRange()
|
||||||
|
scrollHeight = editor.getScrollHeight()
|
||||||
|
scrollWidth = editor.getScrollWidth()
|
||||||
|
scrollTop = editor.getScrollTop()
|
||||||
|
scrollLeft = editor.getScrollLeft()
|
||||||
|
lineHeightInPixels = editor.getLineHeight()
|
||||||
|
|
||||||
|
className = 'editor editor-colors react'
|
||||||
|
className += ' is-focused' if focused
|
||||||
|
|
||||||
|
div className: className, style: {fontSize, lineHeight, fontFamily}, tabIndex: -1,
|
||||||
|
GutterComponent {
|
||||||
|
editor, renderedRowRange, scrollTop, scrollHeight,
|
||||||
|
lineHeight: lineHeightInPixels, @pendingChanges
|
||||||
|
}
|
||||||
|
|
||||||
|
EditorScrollViewComponent {
|
||||||
|
ref: 'scrollView', editor, fontSize, fontFamily, showIndentGuide
|
||||||
|
scrollHeight, scrollWidth, lineHeight: lineHeightInPixels,
|
||||||
|
renderedRowRange, @pendingChanges, @scrollingVertically, @cursorsMoved,
|
||||||
|
cursorBlinkPeriod, cursorBlinkResumeDelay, @onInputFocused, @onInputBlurred
|
||||||
|
}
|
||||||
|
|
||||||
|
ScrollbarComponent
|
||||||
|
ref: 'verticalScrollbar'
|
||||||
|
className: 'vertical-scrollbar'
|
||||||
|
orientation: 'vertical'
|
||||||
|
onScroll: @onVerticalScroll
|
||||||
|
scrollTop: scrollTop
|
||||||
|
scrollHeight: scrollHeight
|
||||||
|
|
||||||
|
ScrollbarComponent
|
||||||
|
ref: 'horizontalScrollbar'
|
||||||
|
className: 'horizontal-scrollbar'
|
||||||
|
orientation: 'horizontal'
|
||||||
|
onScroll: @onHorizontalScroll
|
||||||
|
scrollLeft: scrollLeft
|
||||||
|
scrollWidth: scrollWidth
|
||||||
|
|
||||||
|
getRenderedRowRange: ->
|
||||||
|
renderedRowRange = @props.editor.getVisibleRowRange()
|
||||||
|
if @preservedRowRange?
|
||||||
|
renderedRowRange[0] = Math.min(@preservedRowRange[0], renderedRowRange[0])
|
||||||
|
renderedRowRange[1] = Math.max(@preservedRowRange[1], renderedRowRange[1])
|
||||||
|
renderedRowRange
|
||||||
|
|
||||||
|
getInitialState: -> {}
|
||||||
|
|
||||||
|
getDefaultProps: ->
|
||||||
|
cursorBlinkPeriod: 800
|
||||||
|
cursorBlinkResumeDelay: 200
|
||||||
|
|
||||||
|
componentWillMount: ->
|
||||||
|
@pendingChanges = []
|
||||||
|
@props.editor.manageScrollPosition = true
|
||||||
|
@observeConfig()
|
||||||
|
|
||||||
|
componentDidMount: ->
|
||||||
|
@observeEditor()
|
||||||
|
@listenForDOMEvents()
|
||||||
|
@listenForCommands()
|
||||||
|
@props.editor.setVisible(true)
|
||||||
|
@requestUpdate()
|
||||||
|
|
||||||
|
componentWillUnmount: ->
|
||||||
|
@unsubscribe()
|
||||||
|
@getDOMNode().removeEventListener 'mousewheel', @onMouseWheel
|
||||||
|
|
||||||
|
componentWillUpdate: ->
|
||||||
|
@props.parentView.trigger 'cursor:moved' if @cursorsMoved
|
||||||
|
|
||||||
|
componentDidUpdate: ->
|
||||||
|
@pendingChanges.length = 0
|
||||||
|
@cursorsMoved = false
|
||||||
|
@props.parentView.trigger 'editor:display-updated'
|
||||||
|
|
||||||
|
observeEditor: ->
|
||||||
|
{editor} = @props
|
||||||
|
@subscribe editor, 'batched-updates-started', @onBatchedUpdatesStarted
|
||||||
|
@subscribe editor, 'batched-updates-ended', @onBatchedUpdatesEnded
|
||||||
|
@subscribe editor, 'screen-lines-changed', @onScreenLinesChanged
|
||||||
|
@subscribe editor, 'cursors-moved', @onCursorsMoved
|
||||||
|
@subscribe editor, 'selection-screen-range-changed', @requestUpdate
|
||||||
|
@subscribe editor, 'selection-added', @onSelectionAdded
|
||||||
|
@subscribe editor, 'selection-removed', @onSelectionAdded
|
||||||
|
@subscribe editor.$scrollTop.changes, @onScrollTopChanged
|
||||||
|
@subscribe editor.$scrollLeft.changes, @requestUpdate
|
||||||
|
@subscribe editor.$height.changes, @requestUpdate
|
||||||
|
@subscribe editor.$width.changes, @requestUpdate
|
||||||
|
@subscribe editor.$defaultCharWidth.changes, @requestUpdate
|
||||||
|
@subscribe editor.$lineHeight.changes, @requestUpdate
|
||||||
|
|
||||||
|
listenForDOMEvents: ->
|
||||||
|
node = @getDOMNode()
|
||||||
|
node.addEventListener 'mousewheel', @onMouseWheel
|
||||||
|
node.addEventListener 'focus', @onFocus # For some reason, React's built in focus events seem to bubble
|
||||||
|
|
||||||
|
listenForCommands: ->
|
||||||
|
{parentView, editor, mini} = @props
|
||||||
|
|
||||||
|
@addCommandListeners
|
||||||
|
'core:move-left': => editor.moveCursorLeft()
|
||||||
|
'core:move-right': => editor.moveCursorRight()
|
||||||
|
'core:select-left': => editor.selectLeft()
|
||||||
|
'core:select-right': => editor.selectRight()
|
||||||
|
'core:select-all': => editor.selectAll()
|
||||||
|
'core:backspace': => editor.backspace()
|
||||||
|
'core:delete': => editor.delete()
|
||||||
|
'core:undo': => editor.undo()
|
||||||
|
'core:redo': => editor.redo()
|
||||||
|
'core:cut': => editor.cutSelectedText()
|
||||||
|
'core:copy': => editor.copySelectedText()
|
||||||
|
'core:paste': => editor.pasteText()
|
||||||
|
'editor:move-to-previous-word': => editor.moveCursorToPreviousWord()
|
||||||
|
'editor:select-word': => editor.selectWord()
|
||||||
|
'editor:consolidate-selections': @consolidateSelections
|
||||||
|
'editor:backspace-to-beginning-of-word': => editor.backspaceToBeginningOfWord()
|
||||||
|
'editor:backspace-to-beginning-of-line': => editor.backspaceToBeginningOfLine()
|
||||||
|
'editor:delete-to-end-of-word': => editor.deleteToEndOfWord()
|
||||||
|
'editor:delete-line': => editor.deleteLine()
|
||||||
|
'editor:cut-to-end-of-line': => editor.cutToEndOfLine()
|
||||||
|
'editor:move-to-beginning-of-screen-line': => editor.moveCursorToBeginningOfScreenLine()
|
||||||
|
'editor:move-to-beginning-of-line': => editor.moveCursorToBeginningOfLine()
|
||||||
|
'editor:move-to-end-of-screen-line': => editor.moveCursorToEndOfScreenLine()
|
||||||
|
'editor:move-to-end-of-line': => editor.moveCursorToEndOfLine()
|
||||||
|
'editor:move-to-first-character-of-line': => editor.moveCursorToFirstCharacterOfLine()
|
||||||
|
'editor:move-to-beginning-of-word': => editor.moveCursorToBeginningOfWord()
|
||||||
|
'editor:move-to-end-of-word': => editor.moveCursorToEndOfWord()
|
||||||
|
'editor:move-to-beginning-of-next-word': => editor.moveCursorToBeginningOfNextWord()
|
||||||
|
'editor:move-to-previous-word-boundary': => editor.moveCursorToPreviousWordBoundary()
|
||||||
|
'editor:move-to-next-word-boundary': => editor.moveCursorToNextWordBoundary()
|
||||||
|
'editor:select-to-end-of-line': => editor.selectToEndOfLine()
|
||||||
|
'editor:select-to-beginning-of-line': => editor.selectToBeginningOfLine()
|
||||||
|
'editor:select-to-end-of-word': => editor.selectToEndOfWord()
|
||||||
|
'editor:select-to-beginning-of-word': => editor.selectToBeginningOfWord()
|
||||||
|
'editor:select-to-beginning-of-next-word': => editor.selectToBeginningOfNextWord()
|
||||||
|
'editor:select-to-next-word-boundary': => editor.selectToNextWordBoundary()
|
||||||
|
'editor:select-to-previous-word-boundary': => editor.selectToPreviousWordBoundary()
|
||||||
|
'editor:select-to-first-character-of-line': => editor.selectToFirstCharacterOfLine()
|
||||||
|
'editor:select-line': => editor.selectLine()
|
||||||
|
'editor:transpose': => editor.transpose()
|
||||||
|
'editor:upper-case': => editor.upperCase()
|
||||||
|
'editor:lower-case': => editor.lowerCase()
|
||||||
|
|
||||||
|
unless mini
|
||||||
|
@addCommandListeners
|
||||||
|
'core:move-up': => editor.moveCursorUp()
|
||||||
|
'core:move-down': => editor.moveCursorDown()
|
||||||
|
'core:move-to-top': => editor.moveCursorToTop()
|
||||||
|
'core:move-to-bottom': => editor.moveCursorToBottom()
|
||||||
|
'core:select-up': => editor.selectUp()
|
||||||
|
'core:select-down': => editor.selectDown()
|
||||||
|
'core:select-to-top': => editor.selectToTop()
|
||||||
|
'core:select-to-bottom': => editor.selectToBottom()
|
||||||
|
'editor:indent': => editor.indent()
|
||||||
|
'editor:auto-indent': => editor.autoIndentSelectedRows()
|
||||||
|
'editor:indent-selected-rows': => editor.indentSelectedRows()
|
||||||
|
'editor:outdent-selected-rows': => editor.outdentSelectedRows()
|
||||||
|
'editor:newline': => editor.insertNewline()
|
||||||
|
'editor:newline-below': => editor.insertNewlineBelow()
|
||||||
|
'editor:newline-above': => editor.insertNewlineAbove()
|
||||||
|
'editor:add-selection-below': => editor.addSelectionBelow()
|
||||||
|
'editor:add-selection-above': => editor.addSelectionAbove()
|
||||||
|
'editor:split-selections-into-lines': => editor.splitSelectionsIntoLines()
|
||||||
|
'editor:toggle-soft-tabs': => editor.toggleSoftTabs()
|
||||||
|
'editor:toggle-soft-wrap': => editor.toggleSoftWrap()
|
||||||
|
'editor:fold-all': => editor.foldAll()
|
||||||
|
'editor:unfold-all': => editor.unfoldAll()
|
||||||
|
'editor:fold-current-row': => editor.foldCurrentRow()
|
||||||
|
'editor:unfold-current-row': => editor.unfoldCurrentRow()
|
||||||
|
'editor:fold-selection': => neditor.foldSelectedLines()
|
||||||
|
'editor:fold-at-indent-level-1': => editor.foldAllAtIndentLevel(0)
|
||||||
|
'editor:fold-at-indent-level-2': => editor.foldAllAtIndentLevel(1)
|
||||||
|
'editor:fold-at-indent-level-3': => editor.foldAllAtIndentLevel(2)
|
||||||
|
'editor:fold-at-indent-level-4': => editor.foldAllAtIndentLevel(3)
|
||||||
|
'editor:fold-at-indent-level-5': => editor.foldAllAtIndentLevel(4)
|
||||||
|
'editor:fold-at-indent-level-6': => editor.foldAllAtIndentLevel(5)
|
||||||
|
'editor:fold-at-indent-level-7': => editor.foldAllAtIndentLevel(6)
|
||||||
|
'editor:fold-at-indent-level-8': => editor.foldAllAtIndentLevel(7)
|
||||||
|
'editor:fold-at-indent-level-9': => editor.foldAllAtIndentLevel(8)
|
||||||
|
'editor:toggle-line-comments': => editor.toggleLineCommentsInSelection()
|
||||||
|
'editor:log-cursor-scope': => editor.logCursorScope()
|
||||||
|
'editor:checkout-head-revision': => editor.checkoutHead()
|
||||||
|
'editor:copy-path': => editor.copyPathToClipboard()
|
||||||
|
'editor:move-line-up': => editor.moveLineUp()
|
||||||
|
'editor:move-line-down': => editor.moveLineDown()
|
||||||
|
'editor:duplicate-lines': => editor.duplicateLines()
|
||||||
|
'editor:join-lines': => editor.joinLines()
|
||||||
|
'editor:toggle-indent-guide': => atom.config.toggle('editor.showIndentGuide')
|
||||||
|
'editor:toggle-line-numbers': => atom.config.toggle('editor.showLineNumbers')
|
||||||
|
'editor:scroll-to-cursor': => editor.scrollToCursorPosition()
|
||||||
|
'core:page-up': => editor.pageUp()
|
||||||
|
'core:page-down': => editor.pageDown()
|
||||||
|
|
||||||
|
addCommandListeners: (listenersByCommandName) ->
|
||||||
|
{parentView} = @props
|
||||||
|
|
||||||
|
for command, listener of listenersByCommandName
|
||||||
|
parentView.command command, listener
|
||||||
|
|
||||||
|
observeConfig: ->
|
||||||
|
@subscribe atom.config.observe 'editor.fontFamily', @setFontFamily
|
||||||
|
@subscribe atom.config.observe 'editor.fontSize', @setFontSize
|
||||||
|
@subscribe atom.config.observe 'editor.showIndentGuide', @setShowIndentGuide
|
||||||
|
|
||||||
|
setFontSize: (fontSize) ->
|
||||||
|
@setState({fontSize})
|
||||||
|
|
||||||
|
setLineHeight: (lineHeight) ->
|
||||||
|
@setState({lineHeight})
|
||||||
|
|
||||||
|
setFontFamily: (fontFamily) ->
|
||||||
|
@setState({fontFamily})
|
||||||
|
|
||||||
|
setShowIndentGuide: (showIndentGuide) ->
|
||||||
|
@setState({showIndentGuide})
|
||||||
|
|
||||||
|
onFocus: ->
|
||||||
|
@refs.scrollView.focus()
|
||||||
|
|
||||||
|
onInputFocused: ->
|
||||||
|
@setState(focused: true)
|
||||||
|
|
||||||
|
onInputBlurred: ->
|
||||||
|
@setState(focused: false)
|
||||||
|
|
||||||
|
onVerticalScroll: (scrollTop) ->
|
||||||
|
{editor} = @props
|
||||||
|
|
||||||
|
return if scrollTop is editor.getScrollTop()
|
||||||
|
|
||||||
|
animationFramePending = @pendingScrollTop?
|
||||||
|
@pendingScrollTop = scrollTop
|
||||||
|
unless animationFramePending
|
||||||
|
requestAnimationFrame =>
|
||||||
|
@props.editor.setScrollTop(@pendingScrollTop)
|
||||||
|
@pendingScrollTop = null
|
||||||
|
|
||||||
|
onHorizontalScroll: (scrollLeft) ->
|
||||||
|
{editor} = @props
|
||||||
|
|
||||||
|
return if scrollLeft is editor.getScrollLeft()
|
||||||
|
|
||||||
|
animationFramePending = @pendingScrollLeft?
|
||||||
|
@pendingScrollLeft = scrollLeft
|
||||||
|
unless animationFramePending
|
||||||
|
requestAnimationFrame =>
|
||||||
|
@props.editor.setScrollLeft(@pendingScrollLeft)
|
||||||
|
@pendingScrollLeft = null
|
||||||
|
|
||||||
|
onMouseWheel: (event) ->
|
||||||
|
# Only scroll in one direction at a time
|
||||||
|
{wheelDeltaX, wheelDeltaY} = event
|
||||||
|
if Math.abs(wheelDeltaX) > Math.abs(wheelDeltaY)
|
||||||
|
@refs.horizontalScrollbar.getDOMNode().scrollLeft -= wheelDeltaX
|
||||||
|
else
|
||||||
|
@refs.verticalScrollbar.getDOMNode().scrollTop -= wheelDeltaY
|
||||||
|
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
clearPreservedRowRange: ->
|
||||||
|
@preservedRowRange = null
|
||||||
|
@scrollingVertically = false
|
||||||
|
@requestUpdate()
|
||||||
|
|
||||||
|
clearPreservedRowRangeAfterDelay: null # Created lazily
|
||||||
|
|
||||||
|
onBatchedUpdatesStarted: ->
|
||||||
|
@batchingUpdates = true
|
||||||
|
|
||||||
|
onBatchedUpdatesEnded: ->
|
||||||
|
updateRequested = @updateRequested
|
||||||
|
@updateRequested = false
|
||||||
|
@batchingUpdates = false
|
||||||
|
if updateRequested
|
||||||
|
@requestUpdate()
|
||||||
|
|
||||||
|
onScreenLinesChanged: (change) ->
|
||||||
|
{editor} = @props
|
||||||
|
@pendingChanges.push(change)
|
||||||
|
@requestUpdate() if editor.intersectsVisibleRowRange(change.start, change.end + 1) # TODO: Use closed-open intervals for change events
|
||||||
|
|
||||||
|
onSelectionAdded: (selection) ->
|
||||||
|
{editor} = @props
|
||||||
|
@requestUpdate() if editor.selectionIntersectsVisibleRowRange(selection)
|
||||||
|
|
||||||
|
onScrollTopChanged: ->
|
||||||
|
@preservedRowRange = @getRenderedRowRange()
|
||||||
|
@scrollingVertically = true
|
||||||
|
@clearPreservedRowRangeAfterDelay ?= debounce(@clearPreservedRowRange, 200)
|
||||||
|
@clearPreservedRowRangeAfterDelay()
|
||||||
|
@requestUpdate()
|
||||||
|
|
||||||
|
onSelectionRemoved: (selection) ->
|
||||||
|
{editor} = @props
|
||||||
|
@requestUpdate() if editor.selectionIntersectsVisibleRowRange(selection)
|
||||||
|
|
||||||
|
onCursorsMoved: ->
|
||||||
|
@cursorsMoved = true
|
||||||
|
|
||||||
|
requestUpdate: ->
|
||||||
|
if @batchingUpdates
|
||||||
|
@updateRequested = true
|
||||||
|
else
|
||||||
|
@forceUpdate()
|
||||||
|
|
||||||
|
measureHeightAndWidth: ->
|
||||||
|
@refs.scrollView.measureHeightAndWidth()
|
||||||
|
|
||||||
|
consolidateSelections: (e) ->
|
||||||
|
e.abortKeyBinding() unless @props.editor.consolidateSelections()
|
198
src/editor-scroll-view-component.coffee
Normal file
198
src/editor-scroll-view-component.coffee
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
React = require 'react'
|
||||||
|
{div} = require 'reactionary'
|
||||||
|
{debounce} = require 'underscore-plus'
|
||||||
|
|
||||||
|
InputComponent = require './input-component'
|
||||||
|
LinesComponent = require './lines-component'
|
||||||
|
CursorsComponent = require './cursors-component'
|
||||||
|
SelectionsComponent = require './selections-component'
|
||||||
|
|
||||||
|
module.exports =
|
||||||
|
EditorScrollViewComponent = React.createClass
|
||||||
|
displayName: 'EditorScrollViewComponent'
|
||||||
|
|
||||||
|
measurementPending: false
|
||||||
|
overflowChangedEventsPaused: false
|
||||||
|
overflowChangedWhilePaused: false
|
||||||
|
|
||||||
|
render: ->
|
||||||
|
{editor, fontSize, fontFamily, lineHeight, showIndentGuide} = @props
|
||||||
|
{scrollHeight, scrollWidth, renderedRowRange, pendingChanges, scrollingVertically} = @props
|
||||||
|
{cursorBlinkPeriod, cursorBlinkResumeDelay, cursorsMoved, onInputFocused, onInputBlurred} = @props
|
||||||
|
|
||||||
|
if @isMounted()
|
||||||
|
inputStyle = @getHiddenInputPosition()
|
||||||
|
inputStyle.WebkitTransform = 'translateZ(0)'
|
||||||
|
|
||||||
|
contentStyle =
|
||||||
|
height: scrollHeight
|
||||||
|
minWidth: scrollWidth
|
||||||
|
WebkitTransform: "translate3d(#{-editor.getScrollLeft()}px, #{-editor.getScrollTop()}px, 0)"
|
||||||
|
|
||||||
|
div className: 'scroll-view',
|
||||||
|
InputComponent
|
||||||
|
ref: 'input'
|
||||||
|
className: 'hidden-input'
|
||||||
|
style: inputStyle
|
||||||
|
onInput: @onInput
|
||||||
|
onFocus: onInputFocused
|
||||||
|
onBlur: onInputBlurred
|
||||||
|
|
||||||
|
div className: 'scroll-view-content', style: contentStyle, onMouseDown: @onMouseDown,
|
||||||
|
CursorsComponent({editor, cursorsMoved, cursorBlinkPeriod, cursorBlinkResumeDelay})
|
||||||
|
LinesComponent {
|
||||||
|
ref: 'lines', editor, fontSize, fontFamily, lineHeight, showIndentGuide,
|
||||||
|
renderedRowRange, pendingChanges, scrollingVertically
|
||||||
|
}
|
||||||
|
div className: 'underlayer',
|
||||||
|
SelectionsComponent({editor})
|
||||||
|
|
||||||
|
componentDidMount: ->
|
||||||
|
@getDOMNode().addEventListener 'overflowchanged', @onOverflowChanged
|
||||||
|
window.addEventListener('resize', @onWindowResize)
|
||||||
|
|
||||||
|
@measureHeightAndWidth()
|
||||||
|
|
||||||
|
componentDidUnmount: ->
|
||||||
|
window.removeEventListener('resize', @onWindowResize)
|
||||||
|
|
||||||
|
componentDidUpdate: ->
|
||||||
|
@pauseOverflowChangedEvents()
|
||||||
|
|
||||||
|
onOverflowChanged: ->
|
||||||
|
if @overflowChangedEventsPaused
|
||||||
|
@overflowChangedWhilePaused = true
|
||||||
|
else
|
||||||
|
@requestMeasurement()
|
||||||
|
|
||||||
|
onWindowResize: ->
|
||||||
|
@requestMeasurement()
|
||||||
|
|
||||||
|
pauseOverflowChangedEvents: ->
|
||||||
|
@overflowChangedEventsPaused = true
|
||||||
|
@resumeOverflowChangedEventsAfterDelay ?= debounce(@resumeOverflowChangedEvents, 500)
|
||||||
|
@resumeOverflowChangedEventsAfterDelay()
|
||||||
|
|
||||||
|
resumeOverflowChangedEvents: ->
|
||||||
|
if @overflowChangedWhilePaused
|
||||||
|
@overflowChangedWhilePaused = false
|
||||||
|
@requestMeasurement()
|
||||||
|
|
||||||
|
resumeOverflowChangedEventsAfterDelay: null
|
||||||
|
|
||||||
|
requestMeasurement: ->
|
||||||
|
return if @measurementPending
|
||||||
|
|
||||||
|
@measurementPending = true
|
||||||
|
requestAnimationFrame =>
|
||||||
|
@measurementPending = false
|
||||||
|
@measureHeightAndWidth()
|
||||||
|
|
||||||
|
onInput: (char, replaceLastCharacter) ->
|
||||||
|
{editor} = @props
|
||||||
|
|
||||||
|
if replaceLastCharacter
|
||||||
|
editor.transact ->
|
||||||
|
editor.selectLeft()
|
||||||
|
editor.insertText(char)
|
||||||
|
else
|
||||||
|
editor.insertText(char)
|
||||||
|
|
||||||
|
onMouseDown: (event) ->
|
||||||
|
{editor} = @props
|
||||||
|
{detail, shiftKey, metaKey} = event
|
||||||
|
screenPosition = @screenPositionForMouseEvent(event)
|
||||||
|
|
||||||
|
if shiftKey
|
||||||
|
editor.selectToScreenPosition(screenPosition)
|
||||||
|
else if metaKey
|
||||||
|
editor.addCursorAtScreenPosition(screenPosition)
|
||||||
|
else
|
||||||
|
editor.setCursorScreenPosition(screenPosition)
|
||||||
|
switch detail
|
||||||
|
when 2 then editor.selectWord()
|
||||||
|
when 3 then editor.selectLine()
|
||||||
|
|
||||||
|
@selectToMousePositionUntilMouseUp(event)
|
||||||
|
|
||||||
|
selectToMousePositionUntilMouseUp: (event) ->
|
||||||
|
{editor} = @props
|
||||||
|
dragging = false
|
||||||
|
lastMousePosition = {}
|
||||||
|
|
||||||
|
animationLoop = =>
|
||||||
|
requestAnimationFrame =>
|
||||||
|
if dragging
|
||||||
|
@selectToMousePosition(lastMousePosition)
|
||||||
|
animationLoop()
|
||||||
|
|
||||||
|
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 = ->
|
||||||
|
dragging = false
|
||||||
|
window.removeEventListener('mousemove', onMouseMove)
|
||||||
|
window.removeEventListener('mouseup', onMouseUp)
|
||||||
|
editor.finalizeSelections()
|
||||||
|
|
||||||
|
window.addEventListener('mousemove', onMouseMove)
|
||||||
|
window.addEventListener('mouseup', onMouseUp)
|
||||||
|
|
||||||
|
selectToMousePosition: (event) ->
|
||||||
|
@props.editor.selectToScreenPosition(@screenPositionForMouseEvent(event))
|
||||||
|
|
||||||
|
screenPositionForMouseEvent: (event) ->
|
||||||
|
pixelPosition = @pixelPositionForMouseEvent(event)
|
||||||
|
@props.editor.screenPositionForPixelPosition(pixelPosition)
|
||||||
|
|
||||||
|
pixelPositionForMouseEvent: (event) ->
|
||||||
|
{editor} = @props
|
||||||
|
{clientX, clientY} = event
|
||||||
|
|
||||||
|
editorClientRect = @getDOMNode().getBoundingClientRect()
|
||||||
|
top = clientY - editorClientRect.top + editor.getScrollTop()
|
||||||
|
left = clientX - editorClientRect.left + editor.getScrollLeft()
|
||||||
|
{top, left}
|
||||||
|
|
||||||
|
getHiddenInputPosition: ->
|
||||||
|
{editor} = @props
|
||||||
|
return {top: 0, left: 0} unless @isMounted() and editor.getCursor()?
|
||||||
|
|
||||||
|
{top, left, height, width} = editor.getCursor().getPixelRect()
|
||||||
|
top = top - editor.getScrollTop()
|
||||||
|
top = Math.max(0, Math.min(editor.getHeight() - height, top))
|
||||||
|
left = left - editor.getScrollLeft()
|
||||||
|
left = Math.max(0, Math.min(editor.getWidth() - width, left))
|
||||||
|
|
||||||
|
{top, left}
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
measureHeightAndWidth: ->
|
||||||
|
return unless @isMounted()
|
||||||
|
|
||||||
|
node = @getDOMNode()
|
||||||
|
computedStyle = getComputedStyle(node)
|
||||||
|
{editor} = @props
|
||||||
|
|
||||||
|
unless computedStyle.height is '0px'
|
||||||
|
clientHeight = node.clientHeight
|
||||||
|
editor.setHeight(clientHeight) if clientHeight > 0
|
||||||
|
|
||||||
|
unless computedStyle.width is '0px'
|
||||||
|
clientWidth = node.clientWidth
|
||||||
|
editor.setWidth(clientWidth) if clientHeight > 0
|
||||||
|
|
||||||
|
focus: ->
|
||||||
|
@refs.input.focus()
|
@ -1482,16 +1482,11 @@ class EditorView extends View
|
|||||||
html = @buildEmptyLineHtml(showIndentGuide, eolInvisibles, htmlEolInvisibles, indentation, editor, mini)
|
html = @buildEmptyLineHtml(showIndentGuide, eolInvisibles, htmlEolInvisibles, indentation, editor, mini)
|
||||||
line.push(html) if html
|
line.push(html) if html
|
||||||
else
|
else
|
||||||
firstNonWhitespacePosition = text.search(/\S/)
|
|
||||||
firstTrailingWhitespacePosition = text.search(/\s*$/)
|
|
||||||
lineIsWhitespaceOnly = firstTrailingWhitespacePosition is 0
|
|
||||||
position = 0
|
position = 0
|
||||||
for token in tokens
|
for token in tokens
|
||||||
@updateScopeStack(line, scopeStack, token.scopes)
|
@updateScopeStack(line, scopeStack, token.scopes)
|
||||||
hasLeadingWhitespace = position < firstNonWhitespacePosition
|
hasIndentGuide = not mini and showIndentGuide
|
||||||
hasTrailingWhitespace = position + token.value.length > firstTrailingWhitespacePosition
|
line.push(token.getValueAsHtml({invisibles, hasIndentGuide}))
|
||||||
hasIndentGuide = not mini and showIndentGuide and (hasLeadingWhitespace or lineIsWhitespaceOnly)
|
|
||||||
line.push(token.getValueAsHtml({invisibles, hasLeadingWhitespace, hasTrailingWhitespace, hasIndentGuide}))
|
|
||||||
position += token.value.length
|
position += token.value.length
|
||||||
|
|
||||||
@popScope(line, scopeStack) while scopeStack.length > 0
|
@popScope(line, scopeStack) while scopeStack.length > 0
|
||||||
|
@ -136,10 +136,6 @@ class Editor extends Model
|
|||||||
atom.deserializers.add(this)
|
atom.deserializers.add(this)
|
||||||
Delegator.includeInto(this)
|
Delegator.includeInto(this)
|
||||||
|
|
||||||
@properties
|
|
||||||
scrollTop: 0
|
|
||||||
scrollLeft: 0
|
|
||||||
|
|
||||||
deserializing: false
|
deserializing: false
|
||||||
callDisplayBufferCreatedHook: false
|
callDisplayBufferCreatedHook: false
|
||||||
registerEditor: false
|
registerEditor: false
|
||||||
@ -153,6 +149,9 @@ class Editor extends Model
|
|||||||
'autoDecreaseIndentForBufferRow', 'toggleLineCommentForBufferRow', 'toggleLineCommentsForBufferRows',
|
'autoDecreaseIndentForBufferRow', 'toggleLineCommentForBufferRow', 'toggleLineCommentsForBufferRows',
|
||||||
toProperty: 'languageMode'
|
toProperty: 'languageMode'
|
||||||
|
|
||||||
|
@delegatesProperties '$lineHeight', '$defaultCharWidth', '$height', '$width',
|
||||||
|
'$scrollTop', '$scrollLeft', 'manageScrollPosition', toProperty: 'displayBuffer'
|
||||||
|
|
||||||
constructor: ({@softTabs, initialLine, tabLength, softWrap, @displayBuffer, buffer, registerEditor, suppressCursorCreation}) ->
|
constructor: ({@softTabs, initialLine, tabLength, softWrap, @displayBuffer, buffer, registerEditor, suppressCursorCreation}) ->
|
||||||
super
|
super
|
||||||
|
|
||||||
@ -217,7 +216,10 @@ class Editor extends Model
|
|||||||
@subscribe @displayBuffer, 'soft-wrap-changed', (args...) => @emit 'soft-wrap-changed', args...
|
@subscribe @displayBuffer, 'soft-wrap-changed', (args...) => @emit 'soft-wrap-changed', args...
|
||||||
|
|
||||||
getViewClass: ->
|
getViewClass: ->
|
||||||
require './editor-view'
|
if atom.config.get('core.useReactEditor')
|
||||||
|
require './react-editor-view'
|
||||||
|
else
|
||||||
|
require './editor-view'
|
||||||
|
|
||||||
destroyed: ->
|
destroyed: ->
|
||||||
@unsubscribe()
|
@unsubscribe()
|
||||||
@ -232,8 +234,6 @@ class Editor extends Model
|
|||||||
displayBuffer = @displayBuffer.copy()
|
displayBuffer = @displayBuffer.copy()
|
||||||
softTabs = @getSoftTabs()
|
softTabs = @getSoftTabs()
|
||||||
newEditor = new Editor({@buffer, displayBuffer, tabLength, softTabs, suppressCursorCreation: true, registerEditor: true})
|
newEditor = new Editor({@buffer, displayBuffer, tabLength, softTabs, suppressCursorCreation: true, registerEditor: true})
|
||||||
newEditor.setScrollTop(@getScrollTop())
|
|
||||||
newEditor.setScrollLeft(@getScrollLeft())
|
|
||||||
for marker in @findMarkers(editorId: @id)
|
for marker in @findMarkers(editorId: @id)
|
||||||
marker.copy(editorId: newEditor.id, preserveFolds: true)
|
marker.copy(editorId: newEditor.id, preserveFolds: true)
|
||||||
newEditor
|
newEditor
|
||||||
@ -269,18 +269,6 @@ class Editor extends Model
|
|||||||
# Controls visiblity based on the given {Boolean}.
|
# Controls visiblity based on the given {Boolean}.
|
||||||
setVisible: (visible) -> @displayBuffer.setVisible(visible)
|
setVisible: (visible) -> @displayBuffer.setVisible(visible)
|
||||||
|
|
||||||
# Called by {EditorView} when the scroll position changes so it can be
|
|
||||||
# persisted across reloads.
|
|
||||||
setScrollTop: (@scrollTop) -> @scrollTop
|
|
||||||
|
|
||||||
getScrollTop: -> @scrollTop
|
|
||||||
|
|
||||||
# Called by {EditorView} when the scroll position changes so it can be
|
|
||||||
# persisted across reloads.
|
|
||||||
setScrollLeft: (@scrollLeft) -> @scrollLeft
|
|
||||||
|
|
||||||
getScrollLeft: -> @scrollLeft
|
|
||||||
|
|
||||||
# Set the number of characters that can be displayed horizontally in the
|
# Set the number of characters that can be displayed horizontally in the
|
||||||
# editor.
|
# editor.
|
||||||
#
|
#
|
||||||
@ -301,6 +289,9 @@ class Editor extends Model
|
|||||||
# softTabs - A {Boolean}
|
# softTabs - A {Boolean}
|
||||||
setSoftTabs: (@softTabs) -> @softTabs
|
setSoftTabs: (@softTabs) -> @softTabs
|
||||||
|
|
||||||
|
# Public: Toggle soft tabs for this editor
|
||||||
|
toggleSoftTabs: -> @setSoftTabs(not @getSoftTabs())
|
||||||
|
|
||||||
# Public: Get whether soft wrap is enabled for this editor.
|
# Public: Get whether soft wrap is enabled for this editor.
|
||||||
getSoftWrap: -> @displayBuffer.getSoftWrap()
|
getSoftWrap: -> @displayBuffer.getSoftWrap()
|
||||||
|
|
||||||
@ -309,6 +300,9 @@ class Editor extends Model
|
|||||||
# softWrap - A {Boolean}
|
# softWrap - A {Boolean}
|
||||||
setSoftWrap: (softWrap) -> @displayBuffer.setSoftWrap(softWrap)
|
setSoftWrap: (softWrap) -> @displayBuffer.setSoftWrap(softWrap)
|
||||||
|
|
||||||
|
# Public: Toggle soft wrap for this editor
|
||||||
|
toggleSoftWrap: -> @setSoftWrap(not @getSoftWrap())
|
||||||
|
|
||||||
# Public: Get the text representing a single level of indent.
|
# Public: Get the text representing a single level of indent.
|
||||||
#
|
#
|
||||||
# If soft tabs are enabled, the text is composed of N spaces, where N is the
|
# If soft tabs are enabled, the text is composed of N spaces, where N is the
|
||||||
@ -394,13 +388,7 @@ class Editor extends Model
|
|||||||
#
|
#
|
||||||
# Returns a {Number}.
|
# Returns a {Number}.
|
||||||
indentLevelForLine: (line) ->
|
indentLevelForLine: (line) ->
|
||||||
if match = line.match(/^[\t ]+/)
|
@displayBuffer.indentLevelForLine(line)
|
||||||
leadingWhitespace = match[0]
|
|
||||||
tabCount = leadingWhitespace.match(/\t/g)?.length ? 0
|
|
||||||
spaceCount = leadingWhitespace.match(/[ ]/g)?.length ? 0
|
|
||||||
tabCount + (spaceCount / @getTabLength())
|
|
||||||
else
|
|
||||||
0
|
|
||||||
|
|
||||||
# Constructs the string used for tabs.
|
# Constructs the string used for tabs.
|
||||||
buildIndentString: (number) ->
|
buildIndentString: (number) ->
|
||||||
@ -421,6 +409,15 @@ class Editor extends Model
|
|||||||
# filePath - A {String} path.
|
# filePath - A {String} path.
|
||||||
saveAs: (filePath) -> @buffer.saveAs(filePath)
|
saveAs: (filePath) -> @buffer.saveAs(filePath)
|
||||||
|
|
||||||
|
checkoutHead: ->
|
||||||
|
if path = @getPath()
|
||||||
|
atom.project.getRepo()?.checkoutHead(path)
|
||||||
|
|
||||||
|
# Copies the current file path to the native clipboard.
|
||||||
|
copyPathToClipboard: ->
|
||||||
|
path = @getPath()
|
||||||
|
atom.clipboard.write(path) if path?
|
||||||
|
|
||||||
# Public: Returns the {String} path of this editor's text buffer.
|
# Public: Returns the {String} path of this editor's text buffer.
|
||||||
getPath: -> @buffer.getPath()
|
getPath: -> @buffer.getPath()
|
||||||
|
|
||||||
@ -599,6 +596,9 @@ class Editor extends Model
|
|||||||
# Returns an {Array} of {String}s.
|
# Returns an {Array} of {String}s.
|
||||||
getCursorScopes: -> @getCursor().getScopes()
|
getCursorScopes: -> @getCursor().getScopes()
|
||||||
|
|
||||||
|
logCursorScope: ->
|
||||||
|
console.log @getCursorScopes()
|
||||||
|
|
||||||
# Public: For each selection, replace the selected text with the given text.
|
# Public: For each selection, replace the selected text with the given text.
|
||||||
#
|
#
|
||||||
# text - A {String} representing the text to insert.
|
# text - A {String} representing the text to insert.
|
||||||
@ -1178,6 +1178,16 @@ class Editor extends Model
|
|||||||
setSelectedBufferRange: (bufferRange, options) ->
|
setSelectedBufferRange: (bufferRange, options) ->
|
||||||
@setSelectedBufferRanges([bufferRange], options)
|
@setSelectedBufferRanges([bufferRange], options)
|
||||||
|
|
||||||
|
# Public: Set the selected range in screen coordinates. If there are multiple
|
||||||
|
# selections, they are reduced to a single selection with the given range.
|
||||||
|
#
|
||||||
|
# screenRange - A {Range} or range-compatible {Array}.
|
||||||
|
# options - An options {Object}:
|
||||||
|
# :reversed - A {Boolean} indicating whether to create the selection in a
|
||||||
|
# reversed orientation.
|
||||||
|
setSelectedScreenRange: (screenRange, options) ->
|
||||||
|
@setSelectedBufferRange(@bufferRangeForScreenRange(screenRange, options), options)
|
||||||
|
|
||||||
# Public: Set the selected ranges in buffer coordinates. If there are multiple
|
# Public: Set the selected ranges in buffer coordinates. If there are multiple
|
||||||
# selections, they are replaced by new selections with the given ranges.
|
# selections, they are replaced by new selections with the given ranges.
|
||||||
#
|
#
|
||||||
@ -1202,6 +1212,7 @@ class Editor extends Model
|
|||||||
# Remove the given selection.
|
# Remove the given selection.
|
||||||
removeSelection: (selection) ->
|
removeSelection: (selection) ->
|
||||||
_.remove(@selections, selection)
|
_.remove(@selections, selection)
|
||||||
|
@emit 'selection-removed', selection
|
||||||
|
|
||||||
# Reduce one or more selections to a single empty selection based on the most
|
# Reduce one or more selections to a single empty selection based on the most
|
||||||
# recently added cursor.
|
# recently added cursor.
|
||||||
@ -1218,6 +1229,9 @@ class Editor extends Model
|
|||||||
else
|
else
|
||||||
false
|
false
|
||||||
|
|
||||||
|
selectionScreenRangeChanged: (selection) ->
|
||||||
|
@emit 'selection-screen-range-changed', selection
|
||||||
|
|
||||||
# Public: Get current {Selection}s.
|
# Public: Get current {Selection}s.
|
||||||
#
|
#
|
||||||
# Returns: An {Array} of {Selection}s.
|
# Returns: An {Array} of {Selection}s.
|
||||||
@ -1328,6 +1342,14 @@ class Editor extends Model
|
|||||||
getSelectedBufferRanges: ->
|
getSelectedBufferRanges: ->
|
||||||
selection.getBufferRange() for selection in @getSelectionsOrderedByBufferPosition()
|
selection.getBufferRange() for selection in @getSelectionsOrderedByBufferPosition()
|
||||||
|
|
||||||
|
# Public: Get the {Range}s of all selections in screen coordinates.
|
||||||
|
#
|
||||||
|
# The ranges are sorted by their position in the buffer.
|
||||||
|
#
|
||||||
|
# Returns an {Array} of {Range}s.
|
||||||
|
getSelectedScreenRanges: ->
|
||||||
|
selection.getScreenRange() for selection in @getSelectionsOrderedByBufferPosition()
|
||||||
|
|
||||||
# Public: Get the selected text of the most recently added selection.
|
# Public: Get the selected text of the most recently added selection.
|
||||||
#
|
#
|
||||||
# Returns a {String}.
|
# Returns a {String}.
|
||||||
@ -1431,9 +1453,26 @@ class Editor extends Model
|
|||||||
moveCursorToNextWordBoundary: ->
|
moveCursorToNextWordBoundary: ->
|
||||||
@moveCursors (cursor) -> cursor.moveToNextWordBoundary()
|
@moveCursors (cursor) -> cursor.moveToNextWordBoundary()
|
||||||
|
|
||||||
|
scrollToCursorPosition: ->
|
||||||
|
@getCursor().autoscroll()
|
||||||
|
|
||||||
|
pageUp: ->
|
||||||
|
@setScrollTop(@getScrollTop() - @getHeight())
|
||||||
|
|
||||||
|
pageDown: ->
|
||||||
|
@setScrollTop(@getScrollTop() + @getHeight())
|
||||||
|
|
||||||
moveCursors: (fn) ->
|
moveCursors: (fn) ->
|
||||||
fn(cursor) for cursor in @getCursors()
|
@movingCursors = true
|
||||||
@mergeCursors()
|
@batchUpdates =>
|
||||||
|
fn(cursor) for cursor in @getCursors()
|
||||||
|
@mergeCursors()
|
||||||
|
@movingCursors = false
|
||||||
|
@emit 'cursors-moved'
|
||||||
|
|
||||||
|
cursorMoved: (event) ->
|
||||||
|
@emit 'cursor-moved', event
|
||||||
|
@emit 'cursors-moved' unless @movingCursors
|
||||||
|
|
||||||
# Public: Select from the current cursor position to the given position in
|
# Public: Select from the current cursor position to the given position in
|
||||||
# screen coordinates.
|
# screen coordinates.
|
||||||
@ -1735,7 +1774,9 @@ class Editor extends Model
|
|||||||
# execution and revert any changes performed up to the abortion.
|
# execution and revert any changes performed up to the abortion.
|
||||||
#
|
#
|
||||||
# fn - A {Function} to call inside the transaction.
|
# fn - A {Function} to call inside the transaction.
|
||||||
transact: (fn) -> @buffer.transact(fn)
|
transact: (fn) ->
|
||||||
|
@batchUpdates =>
|
||||||
|
@buffer.transact(fn)
|
||||||
|
|
||||||
# Public: Start an open-ended transaction.
|
# Public: Start an open-ended transaction.
|
||||||
#
|
#
|
||||||
@ -1755,6 +1796,12 @@ class Editor extends Model
|
|||||||
# within the transaction.
|
# within the transaction.
|
||||||
abortTransaction: -> @buffer.abortTransaction()
|
abortTransaction: -> @buffer.abortTransaction()
|
||||||
|
|
||||||
|
batchUpdates: (fn) ->
|
||||||
|
@emit 'batched-updates-started'
|
||||||
|
result = fn()
|
||||||
|
@emit 'batched-updates-ended'
|
||||||
|
result
|
||||||
|
|
||||||
inspect: ->
|
inspect: ->
|
||||||
"<Editor #{@id}>"
|
"<Editor #{@id}>"
|
||||||
|
|
||||||
@ -1771,6 +1818,66 @@ class Editor extends Model
|
|||||||
getSelectionMarkerAttributes: ->
|
getSelectionMarkerAttributes: ->
|
||||||
type: 'selection', editorId: @id, invalidate: 'never'
|
type: 'selection', editorId: @id, invalidate: 'never'
|
||||||
|
|
||||||
|
getVerticalScrollMargin: -> @displayBuffer.getVerticalScrollMargin()
|
||||||
|
setVerticalScrollMargin: (verticalScrollMargin) -> @displayBuffer.setVerticalScrollMargin(verticalScrollMargin)
|
||||||
|
|
||||||
|
getHorizontalScrollMargin: -> @displayBuffer.getHorizontalScrollMargin()
|
||||||
|
setHorizontalScrollMargin: (horizontalScrollMargin) -> @displayBuffer.setHorizontalScrollMargin(horizontalScrollMargin)
|
||||||
|
|
||||||
|
getLineHeight: -> @displayBuffer.getLineHeight()
|
||||||
|
setLineHeight: (lineHeight) -> @displayBuffer.setLineHeight(lineHeight)
|
||||||
|
|
||||||
|
getScopedCharWidth: (scopeNames, char) -> @displayBuffer.getScopedCharWidth(scopeNames, char)
|
||||||
|
setScopedCharWidth: (scopeNames, char, width) -> @displayBuffer.setScopedCharWidth(scopeNames, char, width)
|
||||||
|
|
||||||
|
getScopedCharWidths: (scopeNames) -> @displayBuffer.getScopedCharWidths(scopeNames)
|
||||||
|
|
||||||
|
clearScopedCharWidths: -> @displayBuffer.clearScopedCharWidths()
|
||||||
|
|
||||||
|
getDefaultCharWidth: -> @displayBuffer.getDefaultCharWidth()
|
||||||
|
setDefaultCharWidth: (defaultCharWidth) -> @displayBuffer.setDefaultCharWidth(defaultCharWidth)
|
||||||
|
|
||||||
|
setHeight: (height) -> @displayBuffer.setHeight(height)
|
||||||
|
getHeight: -> @displayBuffer.getHeight()
|
||||||
|
|
||||||
|
setWidth: (width) -> @displayBuffer.setWidth(width)
|
||||||
|
getWidth: -> @displayBuffer.getWidth()
|
||||||
|
|
||||||
|
getScrollTop: -> @displayBuffer.getScrollTop()
|
||||||
|
setScrollTop: (scrollTop) -> @displayBuffer.setScrollTop(scrollTop)
|
||||||
|
|
||||||
|
getScrollBottom: -> @displayBuffer.getScrollBottom()
|
||||||
|
setScrollBottom: (scrollBottom) -> @displayBuffer.setScrollBottom(scrollBottom)
|
||||||
|
|
||||||
|
getScrollLeft: -> @displayBuffer.getScrollLeft()
|
||||||
|
setScrollLeft: (scrollLeft) -> @displayBuffer.setScrollLeft(scrollLeft)
|
||||||
|
|
||||||
|
getScrollRight: -> @displayBuffer.getScrollRight()
|
||||||
|
setScrollRight: (scrollRight) -> @displayBuffer.setScrollRight(scrollRight)
|
||||||
|
|
||||||
|
getScrollHeight: -> @displayBuffer.getScrollHeight()
|
||||||
|
getScrollWidth: (scrollWidth) -> @displayBuffer.getScrollWidth(scrollWidth)
|
||||||
|
|
||||||
|
getVisibleRowRange: -> @displayBuffer.getVisibleRowRange()
|
||||||
|
|
||||||
|
intersectsVisibleRowRange: (startRow, endRow) -> @displayBuffer.intersectsVisibleRowRange(startRow, endRow)
|
||||||
|
|
||||||
|
selectionIntersectsVisibleRowRange: (selection) -> @displayBuffer.selectionIntersectsVisibleRowRange(selection)
|
||||||
|
|
||||||
|
pixelPositionForScreenPosition: (screenPosition) -> @displayBuffer.pixelPositionForScreenPosition(screenPosition)
|
||||||
|
|
||||||
|
pixelPositionForBufferPosition: (bufferPosition) -> @displayBuffer.pixelPositionForBufferPosition(bufferPosition)
|
||||||
|
|
||||||
|
screenPositionForPixelPosition: (pixelPosition) -> @displayBuffer.screenPositionForPixelPosition(pixelPosition)
|
||||||
|
|
||||||
|
pixelRectForScreenRange: (screenRange) -> @displayBuffer.pixelRectForScreenRange(screenRange)
|
||||||
|
|
||||||
|
scrollToScreenRange: (screenRange) -> @displayBuffer.scrollToScreenRange(screenRange)
|
||||||
|
|
||||||
|
scrollToScreenPosition: (screenPosition) -> @displayBuffer.scrollToScreenPosition(screenPosition)
|
||||||
|
|
||||||
|
scrollToBufferPosition: (bufferPosition) -> @displayBuffer.scrollToBufferPosition(bufferPosition)
|
||||||
|
|
||||||
# Deprecated: Call {::joinLines} instead.
|
# Deprecated: Call {::joinLines} instead.
|
||||||
joinLine: ->
|
joinLine: ->
|
||||||
deprecate("Use Editor::joinLines() instead")
|
deprecate("Use Editor::joinLines() instead")
|
||||||
|
83
src/gutter-component.coffee
Normal file
83
src/gutter-component.coffee
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
React = require 'react'
|
||||||
|
{div} = require 'reactionary'
|
||||||
|
{isEqual, isEqualForProperties, multiplyString} = require 'underscore-plus'
|
||||||
|
SubscriberMixin = require './subscriber-mixin'
|
||||||
|
|
||||||
|
module.exports =
|
||||||
|
GutterComponent = React.createClass
|
||||||
|
displayName: 'GutterComponent'
|
||||||
|
mixins: [SubscriberMixin]
|
||||||
|
|
||||||
|
render: ->
|
||||||
|
div className: 'gutter',
|
||||||
|
@renderLineNumbers() if @isMounted()
|
||||||
|
|
||||||
|
renderLineNumbers: ->
|
||||||
|
{editor, renderedRowRange, scrollTop, scrollHeight} = @props
|
||||||
|
[startRow, endRow] = renderedRowRange
|
||||||
|
charWidth = editor.getDefaultCharWidth()
|
||||||
|
lineHeight = editor.getLineHeight()
|
||||||
|
maxDigits = editor.getLastBufferRow().toString().length
|
||||||
|
style =
|
||||||
|
width: charWidth * (maxDigits + 1.5)
|
||||||
|
height: scrollHeight
|
||||||
|
WebkitTransform: "translate3d(0, #{-scrollTop}px, 0)"
|
||||||
|
|
||||||
|
lineNumbers = []
|
||||||
|
tokenizedLines = editor.linesForScreenRows(startRow, endRow - 1)
|
||||||
|
tokenizedLines.push({id: 0}) if tokenizedLines.length is 0
|
||||||
|
for bufferRow, i in editor.bufferRowsForScreenRows(startRow, endRow - 1)
|
||||||
|
if bufferRow is lastBufferRow
|
||||||
|
lineNumber = '•'
|
||||||
|
else
|
||||||
|
lastBufferRow = bufferRow
|
||||||
|
lineNumber = (bufferRow + 1).toString()
|
||||||
|
|
||||||
|
key = tokenizedLines[i]?.id
|
||||||
|
screenRow = startRow + i
|
||||||
|
lineNumbers.push(LineNumberComponent({key, lineNumber, maxDigits, bufferRow, screenRow, lineHeight}))
|
||||||
|
lastBufferRow = bufferRow
|
||||||
|
|
||||||
|
div className: 'line-numbers', style: style,
|
||||||
|
lineNumbers
|
||||||
|
|
||||||
|
componentWillUnmount: ->
|
||||||
|
@unsubscribe()
|
||||||
|
|
||||||
|
# Only update the gutter if the visible row range has changed or if a
|
||||||
|
# non-zero-delta change to the screen lines has occurred within the current
|
||||||
|
# visible row range.
|
||||||
|
shouldComponentUpdate: (newProps) ->
|
||||||
|
{renderedRowRange, pendingChanges, scrollTop} = @props
|
||||||
|
|
||||||
|
return true unless isEqualForProperties(newProps, @props, 'renderedRowRange', 'scrollTop', 'lineHeight')
|
||||||
|
|
||||||
|
for change in pendingChanges when change.screenDelta > 0 or change.bufferDelta > 0
|
||||||
|
return true unless change.end <= renderedRowRange.start or renderedRowRange.end <= change.start
|
||||||
|
|
||||||
|
false
|
||||||
|
|
||||||
|
LineNumberComponent = React.createClass
|
||||||
|
displayName: 'LineNumberComponent'
|
||||||
|
|
||||||
|
render: ->
|
||||||
|
{bufferRow, screenRow, lineHeight} = @props
|
||||||
|
div
|
||||||
|
className: "line-number line-number-#{bufferRow}"
|
||||||
|
style: {top: screenRow * lineHeight}
|
||||||
|
'data-buffer-row': bufferRow
|
||||||
|
'data-screen-row': screenRow
|
||||||
|
dangerouslySetInnerHTML: {__html: @buildInnerHTML()}
|
||||||
|
|
||||||
|
buildInnerHTML: ->
|
||||||
|
{lineNumber, maxDigits} = @props
|
||||||
|
if lineNumber.length < maxDigits
|
||||||
|
padding = multiplyString(' ', maxDigits - lineNumber.length)
|
||||||
|
padding + lineNumber + @iconDivHTML
|
||||||
|
else
|
||||||
|
lineNumber + @iconDivHTML
|
||||||
|
|
||||||
|
iconDivHTML: '<div class="icon-right"></div>'
|
||||||
|
|
||||||
|
shouldComponentUpdate: (newProps) ->
|
||||||
|
not isEqualForProperties(newProps, @props, 'lineHeight', 'screenRow')
|
51
src/input-component.coffee
Normal file
51
src/input-component.coffee
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
punycode = require 'punycode'
|
||||||
|
{last, isEqual} = require 'underscore-plus'
|
||||||
|
React = require 'react'
|
||||||
|
{input} = require 'reactionary'
|
||||||
|
|
||||||
|
module.exports =
|
||||||
|
InputComponent = React.createClass
|
||||||
|
displayName: 'InputComponent'
|
||||||
|
|
||||||
|
render: ->
|
||||||
|
{className, style, onFocus, onBlur} = @props
|
||||||
|
|
||||||
|
input {className, style, onFocus, onBlur}
|
||||||
|
|
||||||
|
getInitialState: ->
|
||||||
|
{lastChar: ''}
|
||||||
|
|
||||||
|
componentDidMount: ->
|
||||||
|
@getDOMNode().addEventListener 'input', @onInput
|
||||||
|
@getDOMNode().addEventListener 'compositionupdate', @onCompositionUpdate
|
||||||
|
|
||||||
|
# Don't let text accumulate in the input forever, but avoid excessive reflows
|
||||||
|
componentDidUpdate: ->
|
||||||
|
if @lastValueLength > 500 and not @isPressAndHoldCharacter(@state.lastChar)
|
||||||
|
@getDOMNode().value = ''
|
||||||
|
@lastValueLength = 0
|
||||||
|
|
||||||
|
# This should actually consult the property lists in /System/Library/Input Methods/PressAndHold.app
|
||||||
|
isPressAndHoldCharacter: (char) ->
|
||||||
|
@state.lastChar.match /[aeiouAEIOU]/
|
||||||
|
|
||||||
|
shouldComponentUpdate: (newProps) ->
|
||||||
|
not isEqual(newProps.style, @props.style)
|
||||||
|
|
||||||
|
onInput: (e) ->
|
||||||
|
e.stopPropagation()
|
||||||
|
valueCharCodes = punycode.ucs2.decode(@getDOMNode().value)
|
||||||
|
valueLength = valueCharCodes.length
|
||||||
|
replaceLastChar = valueLength is @lastValueLength
|
||||||
|
@lastValueLength = valueLength
|
||||||
|
lastChar = String.fromCharCode(last(valueCharCodes))
|
||||||
|
@props.onInput?(lastChar, replaceLastChar)
|
||||||
|
|
||||||
|
onFocus: ->
|
||||||
|
@props.onFocus?()
|
||||||
|
|
||||||
|
onBlur: ->
|
||||||
|
@props.onBlur?()
|
||||||
|
|
||||||
|
focus: ->
|
||||||
|
@getDOMNode().focus()
|
140
src/lines-component.coffee
Normal file
140
src/lines-component.coffee
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
React = require 'react'
|
||||||
|
{div, span} = require 'reactionary'
|
||||||
|
{debounce, isEqual, isEqualForProperties, multiplyString} = require 'underscore-plus'
|
||||||
|
{$$} = require 'space-pen'
|
||||||
|
|
||||||
|
DummyLineNode = $$(-> @div className: 'line', style: 'position: absolute; visibility: hidden;', => @span 'x')[0]
|
||||||
|
AcceptFilter = {acceptNode: -> NodeFilter.FILTER_ACCEPT}
|
||||||
|
|
||||||
|
module.exports =
|
||||||
|
LinesComponent = React.createClass
|
||||||
|
displayName: 'LinesComponent'
|
||||||
|
|
||||||
|
render: ->
|
||||||
|
if @isMounted()
|
||||||
|
{editor, renderedRowRange, lineHeight, showIndentGuide} = @props
|
||||||
|
[startRow, endRow] = renderedRowRange
|
||||||
|
|
||||||
|
lines =
|
||||||
|
for tokenizedLine, i in editor.linesForScreenRows(startRow, endRow - 1)
|
||||||
|
LineComponent({key: tokenizedLine.id, tokenizedLine, showIndentGuide, lineHeight, screenRow: startRow + i})
|
||||||
|
|
||||||
|
div {className: 'lines'}, lines
|
||||||
|
|
||||||
|
componentWillMount: ->
|
||||||
|
@measuredLines = new WeakSet
|
||||||
|
|
||||||
|
componentDidMount: ->
|
||||||
|
@measureLineHeightAndCharWidth()
|
||||||
|
|
||||||
|
shouldComponentUpdate: (newProps) ->
|
||||||
|
return true unless isEqualForProperties(newProps, @props, 'renderedRowRange', 'fontSize', 'fontFamily', 'lineHeight', 'showIndentGuide')
|
||||||
|
|
||||||
|
{renderedRowRange, pendingChanges} = newProps
|
||||||
|
for change in pendingChanges
|
||||||
|
return true unless change.end <= renderedRowRange.start or renderedRowRange.end <= change.start
|
||||||
|
|
||||||
|
false
|
||||||
|
|
||||||
|
componentDidUpdate: (prevProps) ->
|
||||||
|
@measureLineHeightAndCharWidth() unless isEqualForProperties(prevProps, @props, 'fontSize', 'fontFamily', 'lineHeight')
|
||||||
|
@clearScopedCharWidths() unless isEqualForProperties(prevProps, @props, 'fontSize', 'fontFamily')
|
||||||
|
@measureCharactersInNewLines() unless @props.scrollingVertically
|
||||||
|
|
||||||
|
measureLineHeightAndCharWidth: ->
|
||||||
|
node = @getDOMNode()
|
||||||
|
node.appendChild(DummyLineNode)
|
||||||
|
lineHeight = DummyLineNode.getBoundingClientRect().height
|
||||||
|
charWidth = DummyLineNode.firstChild.getBoundingClientRect().width
|
||||||
|
node.removeChild(DummyLineNode)
|
||||||
|
|
||||||
|
{editor} = @props
|
||||||
|
editor.setLineHeight(lineHeight)
|
||||||
|
editor.setDefaultCharWidth(charWidth)
|
||||||
|
|
||||||
|
measureCharactersInNewLines: ->
|
||||||
|
[visibleStartRow, visibleEndRow] = @props.renderedRowRange
|
||||||
|
node = @getDOMNode()
|
||||||
|
|
||||||
|
for tokenizedLine, i in @props.editor.linesForScreenRows(visibleStartRow, visibleEndRow - 1)
|
||||||
|
unless @measuredLines.has(tokenizedLine)
|
||||||
|
lineNode = node.children[i]
|
||||||
|
@measureCharactersInLine(tokenizedLine, lineNode)
|
||||||
|
|
||||||
|
measureCharactersInLine: (tokenizedLine, lineNode) ->
|
||||||
|
{editor} = @props
|
||||||
|
rangeForMeasurement = null
|
||||||
|
iterator = null
|
||||||
|
charIndex = 0
|
||||||
|
|
||||||
|
for {value, scopes}, tokenIndex in tokenizedLine.tokens
|
||||||
|
charWidths = editor.getScopedCharWidths(scopes)
|
||||||
|
|
||||||
|
for char in value
|
||||||
|
unless charWidths[char]?
|
||||||
|
unless textNode?
|
||||||
|
rangeForMeasurement ?= document.createRange()
|
||||||
|
iterator = document.createNodeIterator(lineNode, NodeFilter.SHOW_TEXT, AcceptFilter)
|
||||||
|
textNode = iterator.nextNode()
|
||||||
|
textNodeIndex = 0
|
||||||
|
nextTextNodeIndex = textNode.textContent.length
|
||||||
|
|
||||||
|
while nextTextNodeIndex <= charIndex
|
||||||
|
textNode = iterator.nextNode()
|
||||||
|
textNodeIndex = nextTextNodeIndex
|
||||||
|
nextTextNodeIndex = textNodeIndex + textNode.textContent.length
|
||||||
|
|
||||||
|
i = charIndex - textNodeIndex
|
||||||
|
rangeForMeasurement.setStart(textNode, i)
|
||||||
|
rangeForMeasurement.setEnd(textNode, i + 1)
|
||||||
|
charWidth = rangeForMeasurement.getBoundingClientRect().width
|
||||||
|
editor.setScopedCharWidth(scopes, char, charWidth)
|
||||||
|
|
||||||
|
charIndex++
|
||||||
|
|
||||||
|
@measuredLines.add(tokenizedLine)
|
||||||
|
|
||||||
|
clearScopedCharWidths: ->
|
||||||
|
@measuredLines.clear()
|
||||||
|
@props.editor.clearScopedCharWidths()
|
||||||
|
|
||||||
|
|
||||||
|
LineComponent = React.createClass
|
||||||
|
displayName: 'LineComponent'
|
||||||
|
|
||||||
|
render: ->
|
||||||
|
{screenRow, lineHeight} = @props
|
||||||
|
|
||||||
|
style =
|
||||||
|
top: screenRow * lineHeight
|
||||||
|
position: 'absolute'
|
||||||
|
|
||||||
|
div className: 'line', style: style, 'data-screen-row': screenRow, dangerouslySetInnerHTML: {__html: @buildInnerHTML()}
|
||||||
|
|
||||||
|
buildInnerHTML: ->
|
||||||
|
if @props.tokenizedLine.text.length is 0
|
||||||
|
@buildEmptyLineHTML()
|
||||||
|
else
|
||||||
|
@buildScopeTreeHTML(@props.tokenizedLine.getScopeTree())
|
||||||
|
|
||||||
|
buildEmptyLineHTML: ->
|
||||||
|
{showIndentGuide, tokenizedLine} = @props
|
||||||
|
{indentLevel, tabLength} = tokenizedLine
|
||||||
|
|
||||||
|
if showIndentGuide and indentLevel > 0
|
||||||
|
indentSpan = "<span class='indent-guide'>#{multiplyString(' ', tabLength)}</span>"
|
||||||
|
multiplyString(indentSpan, indentLevel + 1)
|
||||||
|
else
|
||||||
|
" "
|
||||||
|
|
||||||
|
buildScopeTreeHTML: (scopeTree) ->
|
||||||
|
if scopeTree.children?
|
||||||
|
html = "<span class='#{scopeTree.scope.replace(/\./g, ' ')}'>"
|
||||||
|
html += @buildScopeTreeHTML(child) for child in scopeTree.children
|
||||||
|
html += "</span>"
|
||||||
|
html
|
||||||
|
else
|
||||||
|
"<span>#{scopeTree.getValueAsHtml({hasIndentGuide: @props.showIndentGuide})}</span>"
|
||||||
|
|
||||||
|
shouldComponentUpdate: (newProps) ->
|
||||||
|
not isEqualForProperties(newProps, @props, 'showIndentGuide', 'lineHeight', 'screenRow')
|
@ -290,6 +290,7 @@ class Package
|
|||||||
$(event.target).trigger(event)
|
$(event.target).trigger(event)
|
||||||
@restoreEventHandlersOnBubblePath(bubblePathEventHandlers)
|
@restoreEventHandlersOnBubblePath(bubblePathEventHandlers)
|
||||||
@unsubscribeFromActivationEvents()
|
@unsubscribeFromActivationEvents()
|
||||||
|
false
|
||||||
|
|
||||||
unsubscribeFromActivationEvents: ->
|
unsubscribeFromActivationEvents: ->
|
||||||
return unless atom.workspaceView?
|
return unless atom.workspaceView?
|
||||||
|
81
src/react-editor-view.coffee
Normal file
81
src/react-editor-view.coffee
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
{View, $} = require 'space-pen'
|
||||||
|
React = require 'react'
|
||||||
|
EditorComponent = require './editor-component'
|
||||||
|
|
||||||
|
module.exports =
|
||||||
|
class ReactEditorView extends View
|
||||||
|
@content: -> @div class: 'editor react-wrapper'
|
||||||
|
|
||||||
|
focusOnAttach: false
|
||||||
|
|
||||||
|
constructor: (@editor) ->
|
||||||
|
super
|
||||||
|
|
||||||
|
getEditor: -> @editor
|
||||||
|
|
||||||
|
Object.defineProperty @::, 'lineHeight', get: -> @editor.getLineHeight()
|
||||||
|
Object.defineProperty @::, 'charWidth', get: -> @editor.getDefaultCharWidth()
|
||||||
|
|
||||||
|
scrollTop: (scrollTop) ->
|
||||||
|
if scrollTop?
|
||||||
|
@editor.setScrollTop(scrollTop)
|
||||||
|
else
|
||||||
|
@editor.getScrollTop()
|
||||||
|
|
||||||
|
scrollLeft: (scrollLeft) ->
|
||||||
|
if scrollLeft?
|
||||||
|
@editor.setScrollLeft(scrollLeft)
|
||||||
|
else
|
||||||
|
@editor.getScrollLeft()
|
||||||
|
|
||||||
|
scrollToScreenPosition: (screenPosition) ->
|
||||||
|
@editor.scrollToScreenPosition(screenPosition)
|
||||||
|
|
||||||
|
scrollToBufferPosition: (bufferPosition) ->
|
||||||
|
@editor.scrollToBufferPosition(bufferPosition)
|
||||||
|
|
||||||
|
afterAttach: (onDom) ->
|
||||||
|
return unless onDom
|
||||||
|
@attached = true
|
||||||
|
@component = React.renderComponent(EditorComponent({@editor, parentView: this}), @element)
|
||||||
|
|
||||||
|
node = @component.getDOMNode()
|
||||||
|
|
||||||
|
@underlayer = $(node).find('.underlayer')
|
||||||
|
|
||||||
|
@gutter = $(node).find('.gutter')
|
||||||
|
@gutter.removeClassFromAllLines = (klass) =>
|
||||||
|
@gutter.find('.line-number').removeClass(klass)
|
||||||
|
|
||||||
|
@gutter.addClassToLine = (bufferRow, klass) =>
|
||||||
|
lines = @gutter.find(".line-number-#{bufferRow}")
|
||||||
|
lines.addClass(klass)
|
||||||
|
lines.length > 0
|
||||||
|
|
||||||
|
@focus() if @focusOnAttach
|
||||||
|
|
||||||
|
@trigger 'editor:attached', [this]
|
||||||
|
|
||||||
|
pixelPositionForBufferPosition: (bufferPosition) ->
|
||||||
|
@editor.pixelPositionForBufferPosition(bufferPosition)
|
||||||
|
|
||||||
|
pixelPositionForScreenPosition: (screenPosition) ->
|
||||||
|
@editor.pixelPositionForScreenPosition(screenPosition)
|
||||||
|
|
||||||
|
appendToLinesView: (view) ->
|
||||||
|
view.css('position', 'absolute')
|
||||||
|
@find('.scroll-view-content').prepend(view)
|
||||||
|
|
||||||
|
beforeRemove: ->
|
||||||
|
React.unmountComponentAtNode(@element)
|
||||||
|
@attached = false
|
||||||
|
@trigger 'editor:detached', this
|
||||||
|
|
||||||
|
getPane: ->
|
||||||
|
@closest('.pane').view()
|
||||||
|
|
||||||
|
focus: ->
|
||||||
|
if @component?
|
||||||
|
@component.onFocus()
|
||||||
|
else
|
||||||
|
@focusOnAttach = true
|
54
src/scrollbar-component.coffee
Normal file
54
src/scrollbar-component.coffee
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
React = require 'react'
|
||||||
|
{div} = require 'reactionary'
|
||||||
|
{isEqualForProperties} = require 'underscore-plus'
|
||||||
|
|
||||||
|
module.exports =
|
||||||
|
ScrollbarComponent = React.createClass
|
||||||
|
render: ->
|
||||||
|
{orientation, className, scrollHeight, scrollWidth} = @props
|
||||||
|
|
||||||
|
div {className, @onScroll},
|
||||||
|
switch orientation
|
||||||
|
when 'vertical'
|
||||||
|
div className: 'scrollbar-content', style: {height: scrollHeight}
|
||||||
|
when 'horizontal'
|
||||||
|
div className: 'scrollbar-content', style: {width: scrollWidth}
|
||||||
|
|
||||||
|
componentDidMount: ->
|
||||||
|
{orientation} = @props
|
||||||
|
|
||||||
|
unless orientation is 'vertical' or orientation is 'horizontal'
|
||||||
|
throw new Error("Must specify an orientation property of 'vertical' or 'horizontal'")
|
||||||
|
|
||||||
|
shouldComponentUpdate: (newProps) ->
|
||||||
|
switch @props.orientation
|
||||||
|
when 'vertical'
|
||||||
|
not isEqualForProperties(newProps, @props, 'scrollHeight', 'scrollTop')
|
||||||
|
when 'horizontal'
|
||||||
|
not isEqualForProperties(newProps, @props, 'scrollWidth', 'scrollLeft')
|
||||||
|
|
||||||
|
componentDidUpdate: ->
|
||||||
|
{orientation, scrollTop, scrollLeft} = @props
|
||||||
|
node = @getDOMNode()
|
||||||
|
|
||||||
|
switch orientation
|
||||||
|
when 'vertical'
|
||||||
|
node.scrollTop = scrollTop
|
||||||
|
@props.scrollTop = node.scrollTop # Ensure scrollTop reflects actual DOM without triggering another update
|
||||||
|
when 'horizontal'
|
||||||
|
node.scrollLeft = scrollLeft
|
||||||
|
@props.scrollLeft = node.scrollLeft # Ensure scrollLeft reflects actual DOM without triggering another update
|
||||||
|
|
||||||
|
onScroll: ->
|
||||||
|
{orientation, onScroll} = @props
|
||||||
|
node = @getDOMNode()
|
||||||
|
|
||||||
|
switch orientation
|
||||||
|
when 'vertical'
|
||||||
|
scrollTop = node.scrollTop
|
||||||
|
@props.scrollTop = scrollTop # Ensure scrollTop reflects actual DOM without triggering another update
|
||||||
|
onScroll(scrollTop)
|
||||||
|
when 'horizontal'
|
||||||
|
scrollLeft = node.scrollLeft
|
||||||
|
@props.scrollLeft = scrollLeft # Ensure scrollLeft reflects actual DOM without triggering another update
|
||||||
|
onScroll(scrollLeft)
|
11
src/selection-component.coffee
Normal file
11
src/selection-component.coffee
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
React = require 'react'
|
||||||
|
{div} = require 'reactionary'
|
||||||
|
|
||||||
|
module.exports =
|
||||||
|
SelectionComponent = React.createClass
|
||||||
|
displayName: 'SelectionComponent'
|
||||||
|
|
||||||
|
render: ->
|
||||||
|
div className: 'selection',
|
||||||
|
for regionRect, i in @props.selection.getRegionRects()
|
||||||
|
div className: 'region', key: i, style: regionRect
|
@ -1,12 +1,10 @@
|
|||||||
{Point, Range} = require 'text-buffer'
|
{Point, Range} = require 'text-buffer'
|
||||||
{Emitter} = require 'emissary'
|
{Model} = require 'theorist'
|
||||||
{pick} = require 'underscore-plus'
|
{pick} = require 'underscore-plus'
|
||||||
|
|
||||||
# Public: Represents a selection in the {Editor}.
|
# Public: Represents a selection in the {Editor}.
|
||||||
module.exports =
|
module.exports =
|
||||||
class Selection
|
class Selection extends Model
|
||||||
Emitter.includeInto(this)
|
|
||||||
|
|
||||||
cursor: null
|
cursor: null
|
||||||
marker: null
|
marker: null
|
||||||
editor: null
|
editor: null
|
||||||
@ -14,7 +12,8 @@ class Selection
|
|||||||
wordwise: false
|
wordwise: false
|
||||||
needsAutoscroll: null
|
needsAutoscroll: null
|
||||||
|
|
||||||
constructor: ({@cursor, @marker, @editor}) ->
|
constructor: ({@cursor, @marker, @editor, id}) ->
|
||||||
|
@assignId(id)
|
||||||
@cursor.selection = this
|
@cursor.selection = this
|
||||||
@marker.on 'changed', => @screenRangeChanged()
|
@marker.on 'changed', => @screenRangeChanged()
|
||||||
@marker.on 'destroyed', =>
|
@marker.on 'destroyed', =>
|
||||||
@ -77,8 +76,9 @@ class Selection
|
|||||||
options.reversed ?= @isReversed()
|
options.reversed ?= @isReversed()
|
||||||
@editor.destroyFoldsIntersectingBufferRange(bufferRange) unless options.preserveFolds
|
@editor.destroyFoldsIntersectingBufferRange(bufferRange) unless options.preserveFolds
|
||||||
@modifySelection =>
|
@modifySelection =>
|
||||||
@cursor.needsAutoscroll = false if options.autoscroll?
|
@cursor.needsAutoscroll = false if @needsAutoscroll?
|
||||||
@marker.setBufferRange(bufferRange, options)
|
@marker.setBufferRange(bufferRange, options)
|
||||||
|
@autoscroll() if @needsAutoscroll
|
||||||
|
|
||||||
# Public: Returns the starting and ending buffer rows the selection is
|
# Public: Returns the starting and ending buffer rows the selection is
|
||||||
# highlighting.
|
# highlighting.
|
||||||
@ -91,6 +91,9 @@ class Selection
|
|||||||
end = Math.max(start, end - 1) if range.end.column == 0
|
end = Math.max(start, end - 1) if range.end.column == 0
|
||||||
[start, end]
|
[start, end]
|
||||||
|
|
||||||
|
autoscroll: ->
|
||||||
|
@editor.scrollToScreenRange(@getScreenRange())
|
||||||
|
|
||||||
# Public: Returns the text in the selection.
|
# Public: Returns the text in the selection.
|
||||||
getText: ->
|
getText: ->
|
||||||
@editor.buffer.getTextInRange(@getBufferRange())
|
@editor.buffer.getTextInRange(@getBufferRange())
|
||||||
@ -570,6 +573,48 @@ class Selection
|
|||||||
compare: (otherSelection) ->
|
compare: (otherSelection) ->
|
||||||
@getBufferRange().compare(otherSelection.getBufferRange())
|
@getBufferRange().compare(otherSelection.getBufferRange())
|
||||||
|
|
||||||
|
# Get the pixel dimensions of rectangular regions that cover selection's area
|
||||||
|
# on the screen. Used by SelectionComponent for rendering.
|
||||||
|
getRegionRects: ->
|
||||||
|
lineHeight = @editor.getLineHeight()
|
||||||
|
{start, end} = @getScreenRange()
|
||||||
|
rowCount = end.row - start.row + 1
|
||||||
|
startPixelPosition = @editor.pixelPositionForScreenPosition(start)
|
||||||
|
endPixelPosition = @editor.pixelPositionForScreenPosition(end)
|
||||||
|
|
||||||
|
if rowCount is 1
|
||||||
|
# Single line selection
|
||||||
|
rects = [{
|
||||||
|
top: startPixelPosition.top
|
||||||
|
height: lineHeight
|
||||||
|
left: startPixelPosition.left
|
||||||
|
width: endPixelPosition.left - startPixelPosition.left
|
||||||
|
}]
|
||||||
|
else
|
||||||
|
# Multi-line selection
|
||||||
|
rects = []
|
||||||
|
|
||||||
|
# First row, extending from selection start to the right side of screen
|
||||||
|
rects.push {
|
||||||
|
top: startPixelPosition.top
|
||||||
|
left: startPixelPosition.left
|
||||||
|
height: lineHeight
|
||||||
|
right: 0
|
||||||
|
}
|
||||||
|
if rowCount > 2
|
||||||
|
# Middle rows, extending from left side to right side of screen
|
||||||
|
rects.push {
|
||||||
|
top: startPixelPosition.top + lineHeight
|
||||||
|
height: (rowCount - 2) * lineHeight
|
||||||
|
left: 0
|
||||||
|
right: 0
|
||||||
|
}
|
||||||
|
# Last row, extending from left side of screen to selection end
|
||||||
|
rects.push {top: endPixelPosition.top, height: lineHeight, left: 0, width: endPixelPosition.left }
|
||||||
|
|
||||||
|
rects
|
||||||
|
|
||||||
screenRangeChanged: ->
|
screenRangeChanged: ->
|
||||||
screenRange = @getScreenRange()
|
screenRange = @getScreenRange()
|
||||||
@emit 'screen-range-changed', screenRange
|
@emit 'screen-range-changed', screenRange
|
||||||
|
@editor.selectionScreenRangeChanged(this)
|
||||||
|
16
src/selections-component.coffee
Normal file
16
src/selections-component.coffee
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
React = require 'react'
|
||||||
|
{div} = require 'reactionary'
|
||||||
|
SelectionComponent = require './selection-component'
|
||||||
|
|
||||||
|
module.exports =
|
||||||
|
SelectionsComponent = React.createClass
|
||||||
|
displayName: 'SelectionsComponent'
|
||||||
|
|
||||||
|
render: ->
|
||||||
|
{editor} = @props
|
||||||
|
|
||||||
|
div className: 'selections',
|
||||||
|
if @isMounted()
|
||||||
|
for selection in editor.getSelections()
|
||||||
|
if not selection.isEmpty() and editor.selectionIntersectsVisibleRowRange(selection)
|
||||||
|
SelectionComponent({key: selection.id, selection})
|
@ -69,4 +69,6 @@ jQuery(document.body).on 'show.bs.tooltip', ({target}) ->
|
|||||||
jQuery.fn.setTooltip.getKeystroke = getKeystroke
|
jQuery.fn.setTooltip.getKeystroke = getKeystroke
|
||||||
jQuery.fn.setTooltip.humanizeKeystrokes = humanizeKeystrokes
|
jQuery.fn.setTooltip.humanizeKeystrokes = humanizeKeystrokes
|
||||||
|
|
||||||
|
Object.defineProperty jQuery.fn, 'element', get: -> @[0]
|
||||||
|
|
||||||
module.exports = spacePen
|
module.exports = spacePen
|
||||||
|
4
src/subscriber-mixin.coffee
Normal file
4
src/subscriber-mixin.coffee
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{Subscriber} = require 'emissary'
|
||||||
|
SubscriberMixin = componentDidUnmount: -> @unsubscribe()
|
||||||
|
Subscriber.extend(SubscriberMixin)
|
||||||
|
module.exports = SubscriberMixin
|
@ -20,6 +20,8 @@ class Token
|
|||||||
scopes: null
|
scopes: null
|
||||||
isAtomic: null
|
isAtomic: null
|
||||||
isHardTab: null
|
isHardTab: null
|
||||||
|
hasLeadingWhitespace: false
|
||||||
|
hasTrailingWhitespace: false
|
||||||
|
|
||||||
constructor: ({@value, @scopes, @isAtomic, @bufferDelta, @isHardTab}) ->
|
constructor: ({@value, @scopes, @isAtomic, @bufferDelta, @isHardTab}) ->
|
||||||
@screenDelta = @value.length
|
@screenDelta = @value.length
|
||||||
@ -40,7 +42,7 @@ class Token
|
|||||||
whitespaceRegexForTabLength: (tabLength) ->
|
whitespaceRegexForTabLength: (tabLength) ->
|
||||||
WhitespaceRegexesByTabLength[tabLength] ?= new RegExp("([ ]{#{tabLength}})|(\t)|([^\t]+)", "g")
|
WhitespaceRegexesByTabLength[tabLength] ?= new RegExp("([ ]{#{tabLength}})|(\t)|([^\t]+)", "g")
|
||||||
|
|
||||||
breakOutAtomicTokens: (tabLength, breakOutLeadingWhitespace) ->
|
breakOutAtomicTokens: (tabLength, breakOutLeadingSoftTabs) ->
|
||||||
if @hasSurrogatePair
|
if @hasSurrogatePair
|
||||||
outputTokens = []
|
outputTokens = []
|
||||||
|
|
||||||
@ -48,14 +50,14 @@ class Token
|
|||||||
if token.isAtomic
|
if token.isAtomic
|
||||||
outputTokens.push(token)
|
outputTokens.push(token)
|
||||||
else
|
else
|
||||||
outputTokens.push(token.breakOutAtomicTokens(tabLength, breakOutLeadingWhitespace)...)
|
outputTokens.push(token.breakOutAtomicTokens(tabLength, breakOutLeadingSoftTabs)...)
|
||||||
breakOutLeadingWhitespace = token.isOnlyWhitespace() if breakOutLeadingWhitespace
|
breakOutLeadingSoftTabs = token.isOnlyWhitespace() if breakOutLeadingSoftTabs
|
||||||
|
|
||||||
outputTokens
|
outputTokens
|
||||||
else
|
else
|
||||||
return [this] if @isAtomic
|
return [this] if @isAtomic
|
||||||
|
|
||||||
if breakOutLeadingWhitespace
|
if breakOutLeadingSoftTabs
|
||||||
return [this] unless /^[ ]|\t/.test(@value)
|
return [this] unless /^[ ]|\t/.test(@value)
|
||||||
else
|
else
|
||||||
return [this] unless /\t/.test(@value)
|
return [this] unless /\t/.test(@value)
|
||||||
@ -64,13 +66,13 @@ class Token
|
|||||||
regex = @whitespaceRegexForTabLength(tabLength)
|
regex = @whitespaceRegexForTabLength(tabLength)
|
||||||
while match = regex.exec(@value)
|
while match = regex.exec(@value)
|
||||||
[fullMatch, softTab, hardTab] = match
|
[fullMatch, softTab, hardTab] = match
|
||||||
if softTab and breakOutLeadingWhitespace
|
if softTab and breakOutLeadingSoftTabs
|
||||||
outputTokens.push(@buildSoftTabToken(tabLength, false))
|
outputTokens.push(@buildSoftTabToken(tabLength))
|
||||||
else if hardTab
|
else if hardTab
|
||||||
breakOutLeadingWhitespace = false
|
breakOutLeadingSoftTabs = false
|
||||||
outputTokens.push(@buildHardTabToken(tabLength, true))
|
outputTokens.push(@buildHardTabToken(tabLength))
|
||||||
else
|
else
|
||||||
breakOutLeadingWhitespace = false
|
breakOutLeadingSoftTabs = false
|
||||||
value = match[0]
|
value = match[0]
|
||||||
outputTokens.push(new Token({value, @scopes}))
|
outputTokens.push(new Token({value, @scopes}))
|
||||||
|
|
||||||
@ -127,7 +129,7 @@ class Token
|
|||||||
scopeClasses = scope.split('.')
|
scopeClasses = scope.split('.')
|
||||||
_.isSubset(targetClasses, scopeClasses)
|
_.isSubset(targetClasses, scopeClasses)
|
||||||
|
|
||||||
getValueAsHtml: ({invisibles, hasLeadingWhitespace, hasTrailingWhitespace, hasIndentGuide})->
|
getValueAsHtml: ({invisibles, hasIndentGuide})->
|
||||||
invisibles ?= {}
|
invisibles ?= {}
|
||||||
if @isHardTab
|
if @isHardTab
|
||||||
classes = 'hard-tab'
|
classes = 'hard-tab'
|
||||||
@ -142,7 +144,7 @@ class Token
|
|||||||
leadingHtml = ''
|
leadingHtml = ''
|
||||||
trailingHtml = ''
|
trailingHtml = ''
|
||||||
|
|
||||||
if hasLeadingWhitespace and match = LeadingWhitespaceRegex.exec(@value)
|
if @hasLeadingWhitespace and match = LeadingWhitespaceRegex.exec(@value)
|
||||||
classes = 'leading-whitespace'
|
classes = 'leading-whitespace'
|
||||||
classes += ' indent-guide' if hasIndentGuide
|
classes += ' indent-guide' if hasIndentGuide
|
||||||
classes += ' invisible-character' if invisibles.space
|
classes += ' invisible-character' if invisibles.space
|
||||||
@ -152,9 +154,10 @@ class Token
|
|||||||
|
|
||||||
startIndex = match[0].length
|
startIndex = match[0].length
|
||||||
|
|
||||||
if hasTrailingWhitespace and match = TrailingWhitespaceRegex.exec(@value)
|
if @hasTrailingWhitespace and match = TrailingWhitespaceRegex.exec(@value)
|
||||||
|
tokenIsOnlyWhitespace = match[0].length is @value.length
|
||||||
classes = 'trailing-whitespace'
|
classes = 'trailing-whitespace'
|
||||||
classes += ' indent-guide' if hasIndentGuide and not hasLeadingWhitespace
|
classes += ' indent-guide' if hasIndentGuide and not @hasLeadingWhitespace and tokenIsOnlyWhitespace
|
||||||
classes += ' invisible-character' if invisibles.space
|
classes += ' invisible-character' if invisibles.space
|
||||||
|
|
||||||
match[0] = match[0].replace(CharacterRegex, invisibles.space) if invisibles.space
|
match[0] = match[0].replace(CharacterRegex, invisibles.space) if invisibles.space
|
||||||
|
@ -185,14 +185,16 @@ class TokenizedBuffer extends Model
|
|||||||
line = @buffer.lineForRow(row)
|
line = @buffer.lineForRow(row)
|
||||||
tokens = [new Token(value: line, scopes: [@grammar.scopeName])]
|
tokens = [new Token(value: line, scopes: [@grammar.scopeName])]
|
||||||
tabLength = @getTabLength()
|
tabLength = @getTabLength()
|
||||||
new TokenizedLine({tokens, tabLength})
|
indentLevel = @indentLevelForRow(row)
|
||||||
|
new TokenizedLine({tokens, tabLength, indentLevel})
|
||||||
|
|
||||||
buildTokenizedTokenizedLineForRow: (row, ruleStack) ->
|
buildTokenizedTokenizedLineForRow: (row, ruleStack) ->
|
||||||
line = @buffer.lineForRow(row)
|
line = @buffer.lineForRow(row)
|
||||||
lineEnding = @buffer.lineEndingForRow(row)
|
lineEnding = @buffer.lineEndingForRow(row)
|
||||||
tabLength = @getTabLength()
|
tabLength = @getTabLength()
|
||||||
|
indentLevel = @indentLevelForRow(row)
|
||||||
{ tokens, ruleStack } = @grammar.tokenizeLine(line, ruleStack, row is 0)
|
{ tokens, ruleStack } = @grammar.tokenizeLine(line, ruleStack, row is 0)
|
||||||
new TokenizedLine({tokens, ruleStack, tabLength, lineEnding})
|
new TokenizedLine({tokens, ruleStack, tabLength, lineEnding, indentLevel})
|
||||||
|
|
||||||
# FIXME: benogle says: These are actually buffer rows as all buffer rows are
|
# FIXME: benogle says: These are actually buffer rows as all buffer rows are
|
||||||
# accounted for in @tokenizedLines
|
# accounted for in @tokenizedLines
|
||||||
@ -207,6 +209,36 @@ class TokenizedBuffer extends Model
|
|||||||
stackForRow: (row) ->
|
stackForRow: (row) ->
|
||||||
@tokenizedLines[row]?.ruleStack
|
@tokenizedLines[row]?.ruleStack
|
||||||
|
|
||||||
|
indentLevelForRow: (row) ->
|
||||||
|
line = @buffer.lineForRow(row)
|
||||||
|
|
||||||
|
if line is ''
|
||||||
|
nextRow = row + 1
|
||||||
|
lineCount = @getLineCount()
|
||||||
|
while nextRow < lineCount
|
||||||
|
nextLine = @buffer.lineForRow(nextRow)
|
||||||
|
return @indentLevelForLine(nextLine) unless nextLine is ''
|
||||||
|
nextRow++
|
||||||
|
|
||||||
|
previousRow = row - 1
|
||||||
|
while previousRow >= 0
|
||||||
|
previousLine = @buffer.lineForRow(previousRow)
|
||||||
|
return @indentLevelForLine(previousLine) unless previousLine is ''
|
||||||
|
previousRow--
|
||||||
|
|
||||||
|
0
|
||||||
|
else
|
||||||
|
@indentLevelForLine(line)
|
||||||
|
|
||||||
|
indentLevelForLine: (line) ->
|
||||||
|
if match = line.match(/^[\t ]+/)
|
||||||
|
leadingWhitespace = match[0]
|
||||||
|
tabCount = leadingWhitespace.match(/\t/g)?.length ? 0
|
||||||
|
spaceCount = leadingWhitespace.match(/[ ]/g)?.length ? 0
|
||||||
|
tabCount + (spaceCount / @getTabLength())
|
||||||
|
else
|
||||||
|
0
|
||||||
|
|
||||||
scopesForPosition: (position) ->
|
scopesForPosition: (position) ->
|
||||||
@tokenForPosition(position).scopes
|
@tokenForPosition(position).scopes
|
||||||
|
|
||||||
@ -306,6 +338,9 @@ class TokenizedBuffer extends Model
|
|||||||
getLastRow: ->
|
getLastRow: ->
|
||||||
@buffer.getLastRow()
|
@buffer.getLastRow()
|
||||||
|
|
||||||
|
getLineCount: ->
|
||||||
|
@buffer.getLineCount()
|
||||||
|
|
||||||
logLines: (start=0, end=@buffer.getLastRow()) ->
|
logLines: (start=0, end=@buffer.getLastRow()) ->
|
||||||
for row in [start..end]
|
for row in [start..end]
|
||||||
line = @lineForScreenRow(row).text
|
line = @lineForScreenRow(row).text
|
||||||
|
@ -1,12 +1,16 @@
|
|||||||
_ = require 'underscore-plus'
|
_ = require 'underscore-plus'
|
||||||
|
|
||||||
|
idCounter = 1
|
||||||
|
|
||||||
module.exports =
|
module.exports =
|
||||||
class TokenizedLine
|
class TokenizedLine
|
||||||
constructor: ({tokens, @lineEnding, @ruleStack, @startBufferColumn, @fold, tabLength}) ->
|
constructor: ({tokens, @lineEnding, @ruleStack, @startBufferColumn, @fold, @tabLength, @indentLevel}) ->
|
||||||
@tokens = @breakOutAtomicTokens(tokens, tabLength)
|
@tokens = @breakOutAtomicTokens(tokens)
|
||||||
@startBufferColumn ?= 0
|
@startBufferColumn ?= 0
|
||||||
@text = _.pluck(@tokens, 'value').join('')
|
@text = _.pluck(@tokens, 'value').join('')
|
||||||
@bufferDelta = _.sum(_.pluck(@tokens, 'bufferDelta'))
|
@bufferDelta = _.sum(_.pluck(@tokens, 'bufferDelta'))
|
||||||
|
@id = idCounter++
|
||||||
|
@markLeadingAndTrailingWhitespaceTokens()
|
||||||
|
|
||||||
copy: ->
|
copy: ->
|
||||||
new TokenizedLine({@tokens, @lineEnding, @ruleStack, @startBufferColumn, @fold})
|
new TokenizedLine({@tokens, @lineEnding, @ruleStack, @startBufferColumn, @fold})
|
||||||
@ -106,14 +110,25 @@ class TokenizedLine
|
|||||||
delta = nextDelta
|
delta = nextDelta
|
||||||
delta
|
delta
|
||||||
|
|
||||||
breakOutAtomicTokens: (inputTokens, tabLength) ->
|
breakOutAtomicTokens: (inputTokens) ->
|
||||||
outputTokens = []
|
outputTokens = []
|
||||||
breakOutLeadingWhitespace = true
|
breakOutLeadingSoftTabs = true
|
||||||
for token in inputTokens
|
for token in inputTokens
|
||||||
outputTokens.push(token.breakOutAtomicTokens(tabLength, breakOutLeadingWhitespace)...)
|
outputTokens.push(token.breakOutAtomicTokens(@tabLength, breakOutLeadingSoftTabs)...)
|
||||||
breakOutLeadingWhitespace = token.isOnlyWhitespace() if breakOutLeadingWhitespace
|
breakOutLeadingSoftTabs = token.isOnlyWhitespace() if breakOutLeadingSoftTabs
|
||||||
outputTokens
|
outputTokens
|
||||||
|
|
||||||
|
markLeadingAndTrailingWhitespaceTokens: ->
|
||||||
|
firstNonWhitespacePosition = @text.search(/\S/)
|
||||||
|
firstTrailingWhitespacePosition = @text.search(/\s*$/)
|
||||||
|
lineIsWhitespaceOnly = firstTrailingWhitespacePosition is 0
|
||||||
|
position = 0
|
||||||
|
for token, i in @tokens
|
||||||
|
token.hasLeadingWhitespace = position < firstNonWhitespacePosition
|
||||||
|
# Only the *last* segment of a soft-wrapped line can have trailing whitespace
|
||||||
|
token.hasTrailingWhitespace = @lineEnding? and (position + token.value.length > firstTrailingWhitespacePosition)
|
||||||
|
position += token.value.length
|
||||||
|
|
||||||
isComment: ->
|
isComment: ->
|
||||||
for token in @tokens
|
for token in @tokens
|
||||||
continue if token.scopes.length is 1
|
continue if token.scopes.length is 1
|
||||||
@ -134,3 +149,33 @@ class TokenizedLine
|
|||||||
for token in @tokens
|
for token in @tokens
|
||||||
return column if token is targetToken
|
return column if token is targetToken
|
||||||
column += token.bufferDelta
|
column += token.bufferDelta
|
||||||
|
|
||||||
|
getScopeTree: ->
|
||||||
|
return @scopeTree if @scopeTree?
|
||||||
|
|
||||||
|
scopeStack = []
|
||||||
|
for token in @tokens
|
||||||
|
@updateScopeStack(scopeStack, token.scopes)
|
||||||
|
_.last(scopeStack).children.push(token)
|
||||||
|
|
||||||
|
@scopeTree = scopeStack[0]
|
||||||
|
@updateScopeStack(scopeStack, [])
|
||||||
|
@scopeTree
|
||||||
|
|
||||||
|
updateScopeStack: (scopeStack, desiredScopes) ->
|
||||||
|
# Find a common prefix
|
||||||
|
for scope, i in desiredScopes
|
||||||
|
break unless scopeStack[i]?.scope is desiredScopes[i]
|
||||||
|
|
||||||
|
# Pop scopes until we're at the common prefx
|
||||||
|
until scopeStack.length is i
|
||||||
|
poppedScope = scopeStack.pop()
|
||||||
|
_.last(scopeStack)?.children.push(poppedScope)
|
||||||
|
|
||||||
|
# Push onto common prefix until scopeStack equals desiredScopes
|
||||||
|
for j in [i...desiredScopes.length]
|
||||||
|
scopeStack.push(new Scope(desiredScopes[j]))
|
||||||
|
|
||||||
|
class Scope
|
||||||
|
constructor: (@scope) ->
|
||||||
|
@children = []
|
||||||
|
@ -71,6 +71,7 @@ class WorkspaceView extends View
|
|||||||
projectHome: path.join(fs.getHomeDirectory(), 'github')
|
projectHome: path.join(fs.getHomeDirectory(), 'github')
|
||||||
audioBeep: true
|
audioBeep: true
|
||||||
destroyEmptyPanes: true
|
destroyEmptyPanes: true
|
||||||
|
useReactEditor: false
|
||||||
|
|
||||||
@content: ->
|
@content: ->
|
||||||
@div class: 'workspace', tabindex: -1, =>
|
@div class: 'workspace', tabindex: -1, =>
|
||||||
|
@ -2,6 +2,64 @@
|
|||||||
@import "octicon-utf-codes";
|
@import "octicon-utf-codes";
|
||||||
@import "octicon-mixins";
|
@import "octicon-mixins";
|
||||||
|
|
||||||
|
.editor.react {
|
||||||
|
.underlayer {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: -2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lines {
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.horizontal-scrollbar {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
|
||||||
|
height: 15px;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
z-index: 3;
|
||||||
|
|
||||||
|
.scrollbar-content {
|
||||||
|
height: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-view {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-view-content {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gutter {
|
||||||
|
padding-left: 0.5em;
|
||||||
|
padding-right: 0.5em;
|
||||||
|
|
||||||
|
.line-number {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
.icon-right {
|
||||||
|
padding: 0;
|
||||||
|
padding-left: .1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.editor {
|
.editor {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
cursor: text;
|
cursor: text;
|
||||||
@ -28,7 +86,6 @@
|
|||||||
.editor .gutter .line-number {
|
.editor .gutter .line-number {
|
||||||
padding-left: .5em;
|
padding-left: .5em;
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
position: relative;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor .gutter .line-numbers {
|
.editor .gutter .line-numbers {
|
||||||
|
@ -38,7 +38,7 @@
|
|||||||
background-color: @pane-item-background-color;
|
background-color: @pane-item-background-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
> * {
|
> *, > .react-wrapper > * {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
|
Loading…
Reference in New Issue
Block a user