mirror of
https://github.com/atom/atom.git
synced 2026-04-28 03:01:47 -04:00
Base cursor x position on char widths stored in DisplayBuffer
Whenever new lines are added to the screen, we measure and store any unseen scope/character combinations in the DisplayBuffer.
This commit is contained in:
@@ -2,22 +2,25 @@
|
||||
EditorComponent = require '../src/editor-component'
|
||||
|
||||
describe "EditorComponent", ->
|
||||
[editor, component, node, lineHeight, charWidth] = []
|
||||
[editor, component, node, lineHeightInPixels, charWidth] = []
|
||||
|
||||
beforeEach ->
|
||||
spyOn(window, 'requestAnimationFrame').andCallFake (fn) -> fn()
|
||||
waitsForPromise ->
|
||||
atom.packages.activatePackage('language-javascript')
|
||||
|
||||
editor = atom.project.openSync('sample.js')
|
||||
container = document.querySelector('#jasmine-content')
|
||||
component = React.renderComponent(EditorComponent({editor}), container)
|
||||
node = component.getDOMNode()
|
||||
runs ->
|
||||
spyOn(window, 'requestAnimationFrame').andCallFake (fn) -> fn()
|
||||
|
||||
node.style.lineHeight = 1.3
|
||||
node.style.fontSize = '20px'
|
||||
{lineHeight, charWidth} = component.measureLineDimensions()
|
||||
editor = atom.project.openSync('sample.js')
|
||||
container = document.querySelector('#jasmine-content')
|
||||
component = React.renderComponent(EditorComponent({editor}), container)
|
||||
component.setLineHeight(1.3)
|
||||
component.setFontSize(20)
|
||||
{lineHeightInPixels, charWidth} = component.measureLineDimensions()
|
||||
node = component.getDOMNode()
|
||||
|
||||
it "renders only the currently-visible lines", ->
|
||||
node.style.height = 4.5 * lineHeight + 'px'
|
||||
node.style.height = 4.5 * lineHeightInPixels + 'px'
|
||||
component.updateAllDimensions()
|
||||
|
||||
lines = node.querySelectorAll('.line')
|
||||
@@ -25,10 +28,10 @@ describe "EditorComponent", ->
|
||||
expect(lines[0].textContent).toBe editor.lineForScreenRow(0).text
|
||||
expect(lines[4].textContent).toBe editor.lineForScreenRow(4).text
|
||||
|
||||
node.querySelector('.vertical-scrollbar').scrollTop = 2.5 * lineHeight
|
||||
node.querySelector('.vertical-scrollbar').scrollTop = 2.5 * lineHeightInPixels
|
||||
component.onVerticalScroll()
|
||||
|
||||
expect(node.querySelector('.scrollable-content').style['-webkit-transform']).toBe "translateY(#{-2.5 * lineHeight}px)"
|
||||
expect(node.querySelector('.scrollable-content').style['-webkit-transform']).toBe "translateY(#{-2.5 * lineHeightInPixels}px)"
|
||||
|
||||
lines = node.querySelectorAll('.line')
|
||||
expect(lines.length).toBe 5
|
||||
@@ -36,19 +39,19 @@ describe "EditorComponent", ->
|
||||
expect(lines[4].textContent).toBe editor.lineForScreenRow(6).text
|
||||
|
||||
spacers = node.querySelectorAll('.spacer')
|
||||
expect(spacers[0].offsetHeight).toBe 2 * lineHeight
|
||||
expect(spacers[1].offsetHeight).toBe (editor.getScreenLineCount() - 7) * lineHeight
|
||||
expect(spacers[0].offsetHeight).toBe 2 * lineHeightInPixels
|
||||
expect(spacers[1].offsetHeight).toBe (editor.getScreenLineCount() - 7) * lineHeightInPixels
|
||||
|
||||
it "renders the currently visible cursors", ->
|
||||
cursor1 = editor.getCursor()
|
||||
cursor1.setScreenPosition([0, 5])
|
||||
|
||||
node.style.height = 4.5 * lineHeight + 'px'
|
||||
node.style.height = 4.5 * lineHeightInPixels + 'px'
|
||||
component.updateAllDimensions()
|
||||
|
||||
cursorNodes = node.querySelectorAll('.cursor')
|
||||
expect(cursorNodes.length).toBe 1
|
||||
expect(cursorNodes[0].offsetHeight).toBe lineHeight
|
||||
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
|
||||
@@ -60,25 +63,41 @@ describe "EditorComponent", ->
|
||||
expect(cursorNodes.length).toBe 2
|
||||
expect(cursorNodes[0].offsetTop).toBe 0
|
||||
expect(cursorNodes[0].offsetLeft).toBe 5 * charWidth
|
||||
expect(cursorNodes[1].offsetTop).toBe 4 * lineHeight
|
||||
expect(cursorNodes[1].offsetTop).toBe 4 * lineHeightInPixels
|
||||
expect(cursorNodes[1].offsetLeft).toBe 10 * charWidth
|
||||
|
||||
node.querySelector('.vertical-scrollbar').scrollTop = 2.5 * lineHeight
|
||||
node.querySelector('.vertical-scrollbar').scrollTop = 2.5 * lineHeightInPixels
|
||||
component.onVerticalScroll()
|
||||
|
||||
cursorNodes = node.querySelectorAll('.cursor')
|
||||
expect(cursorNodes.length).toBe 2
|
||||
expect(cursorNodes[0].offsetTop).toBe 6 * lineHeight
|
||||
expect(cursorNodes[0].offsetTop).toBe 6 * lineHeightInPixels
|
||||
expect(cursorNodes[0].offsetLeft).toBe 11 * charWidth
|
||||
expect(cursorNodes[1].offsetTop).toBe 4 * lineHeight
|
||||
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 * lineHeight
|
||||
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 "transfers focus to the hidden input", ->
|
||||
expect(document.activeElement).toBe document.body
|
||||
node.focus()
|
||||
|
||||
@@ -3,12 +3,5 @@
|
||||
module.exports =
|
||||
CursorComponent = React.createClass
|
||||
render: ->
|
||||
{cursor, lineHeight, charWidth} = @props
|
||||
{row, column} = cursor.getScreenPosition()
|
||||
|
||||
div className: 'cursor', style: {
|
||||
height: lineHeight,
|
||||
width: charWidth
|
||||
top: row * lineHeight
|
||||
left: column * charWidth
|
||||
}
|
||||
{top, left, height, width} = @props.cursor.getPixelRect()
|
||||
div className: 'cursor', style: {top, left, height, width}
|
||||
|
||||
@@ -54,6 +54,14 @@ class Cursor
|
||||
unless fn()
|
||||
@emit 'autoscrolled' if @needsAutoscroll
|
||||
|
||||
getPixelRect: ->
|
||||
screenPosition = @getScreenPosition()
|
||||
{top, left} = @editor.pixelPositionForScreenPosition(screenPosition, false)
|
||||
right = @editor.pixelPositionForScreenPosition(screenPosition.add([0, 1])).left
|
||||
width = right - left
|
||||
height = @editor.getLineHeight()
|
||||
{top, left, width, height}
|
||||
|
||||
# Public: Moves a cursor to a given screen position.
|
||||
#
|
||||
# screenPosition - An {Array} of two numbers: the screen row, and the screen
|
||||
|
||||
@@ -420,6 +420,9 @@ class DisplayBuffer extends Model
|
||||
setScopedCharWidths: (scopeNames, charWidths) ->
|
||||
_.extend(@getScopedCharWidths(scopeNames), charWidths)
|
||||
|
||||
clearScopedCharWidths: ->
|
||||
@charWidthsByScope = {}
|
||||
|
||||
# Get the grammar for this buffer.
|
||||
#
|
||||
# Returns the current {Grammar} or the {NullGrammar}.
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
{React, div, span} = require 'reactionary'
|
||||
{$$} = require 'space-pencil'
|
||||
{$$} = require 'space-pen'
|
||||
|
||||
SelectionComponent = require './selection-component'
|
||||
InputComponent = require './input-component'
|
||||
CustomEventMixin = require './custom-event-mixin'
|
||||
SubscriberMixin = require './subscriber-mixin'
|
||||
|
||||
DummyLineNode = $$ ->
|
||||
@div className: 'line', style: 'position: absolute; visibility: hidden;', -> @span 'x'
|
||||
DummyLineNode = $$(-> @div className: 'line', style: 'position: absolute; visibility: hidden;', => @span 'x')[0]
|
||||
MeasureRange = document.createRange()
|
||||
TextNodeFilter = { acceptNode: -> NodeFilter.FILTER_ACCEPT }
|
||||
|
||||
module.exports =
|
||||
EditorCompont = React.createClass
|
||||
@@ -18,7 +19,9 @@ EditorCompont = React.createClass
|
||||
mixins: [CustomEventMixin, SubscriberMixin]
|
||||
|
||||
render: ->
|
||||
div className: 'editor', tabIndex: -1,
|
||||
{fontSize, lineHeight, fontFamily} = @state
|
||||
|
||||
div className: 'editor', tabIndex: -1, style: {fontSize, lineHeight, fontFamily},
|
||||
div className: 'scroll-view', ref: 'scrollView',
|
||||
InputComponent ref: 'hiddenInput', className: 'hidden-input', onInput: @onInput
|
||||
@renderScrollableContent()
|
||||
@@ -26,7 +29,7 @@ EditorCompont = React.createClass
|
||||
div outlet: 'verticalScrollbarContent', style: {height: @getScrollHeight()}
|
||||
|
||||
renderScrollableContent: ->
|
||||
height = @props.editor.getScreenLineCount() * @state.lineHeight
|
||||
height = @props.editor.getScreenLineCount() * @state.lineHeightInPixels
|
||||
WebkitTransform = "translateY(#{-@state.scrollTop}px)"
|
||||
|
||||
div className: 'scrollable-content', style: {height, WebkitTransform},
|
||||
@@ -34,16 +37,16 @@ EditorCompont = React.createClass
|
||||
@renderVisibleLines()
|
||||
|
||||
renderOverlayer: ->
|
||||
{lineHeight, charWidth} = @state
|
||||
{lineHeightInPixels, charWidth} = @state
|
||||
|
||||
div className: 'overlayer',
|
||||
for selection in @props.editor.getSelections() when @selectionIntersectsVisibleRowRange(selection)
|
||||
SelectionComponent({selection, lineHeight, charWidth})
|
||||
SelectionComponent({selection})
|
||||
|
||||
renderVisibleLines: ->
|
||||
[startRow, endRow] = @getVisibleRowRange()
|
||||
precedingHeight = startRow * @state.lineHeight
|
||||
followingHeight = (@props.editor.getScreenLineCount() - endRow) * @state.lineHeight
|
||||
precedingHeight = startRow * @state.lineHeightInPixels
|
||||
followingHeight = (@props.editor.getScreenLineCount() - endRow) * @state.lineHeightInPixels
|
||||
|
||||
div className: 'lines', ref: 'lines', [
|
||||
div className: 'spacer', key: 'top-spacer', style: {height: precedingHeight}
|
||||
@@ -55,11 +58,12 @@ EditorCompont = React.createClass
|
||||
getInitialState: ->
|
||||
height: 0
|
||||
width: 0
|
||||
lineHeight: 0
|
||||
lineHeightInPixels: 0
|
||||
scrollTop: 0
|
||||
|
||||
componentDidMount: ->
|
||||
@listenForCustomEvents()
|
||||
@measuredLines = new WeakSet
|
||||
|
||||
@refs.scrollView.getDOMNode().addEventListener 'mousewheel', @onMousewheel
|
||||
@getDOMNode().addEventListener 'focus', @onFocus
|
||||
|
||||
@@ -68,12 +72,18 @@ EditorCompont = React.createClass
|
||||
@subscribe editor, 'selection-added', @onSelectionAdded
|
||||
@subscribe editor, 'selection-removed', @onSelectionAdded
|
||||
|
||||
@listenForCustomEvents()
|
||||
@observeConfig()
|
||||
|
||||
@updateAllDimensions()
|
||||
@props.editor.setVisible(true)
|
||||
|
||||
componentWillUnmount: ->
|
||||
@getDOMNode().removeEventListener 'mousewheel', @onMousewheel
|
||||
|
||||
componentDidUpdate: ->
|
||||
@measureNewLines()
|
||||
|
||||
listenForCustomEvents: ->
|
||||
{editor, mini} = @props
|
||||
|
||||
@@ -171,6 +181,23 @@ EditorCompont = React.createClass
|
||||
# 'core:page-up': => @pageUp()
|
||||
# 'editor:scroll-to-cursor': => @scrollToCursorPosition()
|
||||
|
||||
observeConfig: ->
|
||||
@subscribe atom.config.observe 'editor.fontFamily', @setFontFamily
|
||||
|
||||
setFontSize: (fontSize) ->
|
||||
@clearScopedCharWidths()
|
||||
@setState({fontSize})
|
||||
@updateLineDimensions()
|
||||
|
||||
setLineHeight: (lineHeight) ->
|
||||
@updateLineDimensions()
|
||||
@setState({lineHeight})
|
||||
|
||||
setFontFamily: (fontFamily) ->
|
||||
@clearScopedCharWidths()
|
||||
@setState({fontFamily})
|
||||
@updateLineDimensions()
|
||||
|
||||
onFocus: ->
|
||||
@refs.hiddenInput.focus()
|
||||
|
||||
@@ -200,10 +227,11 @@ EditorCompont = React.createClass
|
||||
@forceUpdate() if @selectionIntersectsVisibleRowRange(selection)
|
||||
|
||||
getVisibleRowRange: ->
|
||||
return [0, 0] unless @state.lineHeight > 0
|
||||
return [0, 0] unless @state.lineHeightInPixels > 0
|
||||
return [0, @props.editor.getScreenLineCount()] if @state.height is 0
|
||||
|
||||
heightInLines = @state.height / @state.lineHeight
|
||||
startRow = Math.floor(@state.scrollTop / @state.lineHeight)
|
||||
heightInLines = @state.height / @state.lineHeightInPixels
|
||||
startRow = Math.floor(@state.scrollTop / @state.lineHeightInPixels)
|
||||
endRow = Math.ceil(startRow + heightInLines)
|
||||
[startRow, endRow]
|
||||
|
||||
@@ -216,12 +244,20 @@ EditorCompont = React.createClass
|
||||
@intersectsVisibleRowRange(start.row, end.row + 1)
|
||||
|
||||
getScrollHeight: ->
|
||||
@props.editor.getLineCount() * @state.lineHeight
|
||||
@props.editor.getLineCount() * @state.lineHeightInPixels
|
||||
|
||||
updateAllDimensions: ->
|
||||
{height, width} = @measureScrollViewDimensions()
|
||||
{lineHeight, charWidth} = @measureLineDimensions()
|
||||
@setState({height, width, lineHeight, charWidth})
|
||||
{lineHeightInPixels, charWidth} = @measureLineDimensions()
|
||||
@props.editor.setLineHeight(lineHeightInPixels)
|
||||
@props.editor.setDefaultCharWidth(charWidth)
|
||||
@setState({height, width, lineHeightInPixels, charWidth})
|
||||
|
||||
updateLineDimensions: ->
|
||||
{lineHeightInPixels, charWidth} = @measureLineDimensions()
|
||||
@props.editor.setLineHeight(lineHeightInPixels)
|
||||
@props.editor.setDefaultCharWidth(charWidth)
|
||||
@setState({lineHeightInPixels, charWidth})
|
||||
|
||||
measureScrollViewDimensions: ->
|
||||
scrollViewNode = @refs.scrollView.getDOMNode()
|
||||
@@ -230,10 +266,39 @@ EditorCompont = React.createClass
|
||||
measureLineDimensions: ->
|
||||
linesNode = @refs.lines.getDOMNode()
|
||||
linesNode.appendChild(DummyLineNode)
|
||||
lineHeight = DummyLineNode.getBoundingClientRect().height
|
||||
lineHeightInPixels = DummyLineNode.getBoundingClientRect().height
|
||||
charWidth = DummyLineNode.firstChild.getBoundingClientRect().width
|
||||
linesNode.removeChild(DummyLineNode)
|
||||
{lineHeight, charWidth}
|
||||
{lineHeightInPixels, charWidth}
|
||||
|
||||
measureNewLines: ->
|
||||
[visibleStartRow, visibleEndRow] = @getVisibleRowRange()
|
||||
linesNode = @refs.lines.getDOMNode()
|
||||
|
||||
for tokenizedLine, i in @props.editor.linesForScreenRows(visibleStartRow, visibleEndRow - 1)
|
||||
unless @measuredLines.has(tokenizedLine)
|
||||
lineNode = linesNode.children[i + 1]
|
||||
@measureCharactersInLine(tokenizedLine, lineNode)
|
||||
|
||||
measureCharactersInLine: (tokenizedLine, lineNode) ->
|
||||
{editor} = @props
|
||||
iterator = document.createNodeIterator(lineNode, NodeFilter.SHOW_TEXT, TextNodeFilter)
|
||||
|
||||
for {value, scopes} in tokenizedLine.tokens
|
||||
textNode = iterator.nextNode()
|
||||
charWidths = editor.getScopedCharWidths(scopes)
|
||||
for char, i in value
|
||||
unless charWidths[char]?
|
||||
MeasureRange.setStart(textNode, i)
|
||||
MeasureRange.setEnd(textNode, i + 1)
|
||||
charWidth = MeasureRange.getBoundingClientRect().width
|
||||
@props.editor.setScopedCharWidth(scopes, char, charWidth)
|
||||
|
||||
@measuredLines.add(tokenizedLine)
|
||||
|
||||
clearScopedCharWidths: ->
|
||||
@measuredLines.clear()
|
||||
@props.editor.clearScopedCharWidths()
|
||||
|
||||
LineComponent = React.createClass
|
||||
render: ->
|
||||
@@ -251,6 +316,6 @@ LineComponent = React.createClass
|
||||
html += @buildScopeTreeHTML(child) for child in scopeTree.children
|
||||
html
|
||||
else
|
||||
"<span>#{scopeTree.value}</span>"
|
||||
"<span>#{scopeTree.getValueAsHtml({})}</span>"
|
||||
|
||||
shouldComponentUpdate: -> false
|
||||
|
||||
@@ -1472,6 +1472,7 @@ class EditorView extends View
|
||||
|
||||
# Copies the current file path to the native clipboard.
|
||||
copyPathToClipboard: ->
|
||||
@editor.copyPathToClipboard()
|
||||
path = @editor.getPath()
|
||||
atom.clipboard.write(path) if path?
|
||||
|
||||
|
||||
@@ -514,6 +514,8 @@ class Editor extends Model
|
||||
# this editor.
|
||||
shouldPromptToSave: -> @isModified() and not @buffer.hasMultipleEditors()
|
||||
|
||||
pixelPositionForScreenPosition: (screenPosition) -> @displayBuffer.pixelPositionForScreenPosition(screenPosition)
|
||||
|
||||
# Public: Convert a position in buffer-coordinates to screen-coordinates.
|
||||
#
|
||||
# The position is clipped via {::clipBufferPosition} prior to the conversion.
|
||||
@@ -1790,6 +1792,20 @@ class Editor extends Model
|
||||
getSelectionMarkerAttributes: ->
|
||||
type: 'selection', editorId: @id, invalidate: 'never'
|
||||
|
||||
getLineHeight: -> @displayBuffer.getLineHeight()
|
||||
|
||||
setLineHeight: (lineHeight) -> @displayBuffer.setLineHeight(lineHeight)
|
||||
|
||||
setDefaultCharWidth: (defaultCharWidth) -> @displayBuffer.setDefaultCharWidth(defaultCharWidth)
|
||||
|
||||
getScopedCharWidth: (args...) -> @displayBuffer.getScopedCharWidth(args...)
|
||||
|
||||
getScopedCharWidths: (args...) -> @displayBuffer.getScopedCharWidths(args...)
|
||||
|
||||
setScopedCharWidth: (args...) -> @displayBuffer.setScopedCharWidth(args...)
|
||||
|
||||
clearScopedCharWidths: -> @displayBuffer.clearScopedCharWidths()
|
||||
|
||||
# Deprecated: Call {::joinLines} instead.
|
||||
joinLine: ->
|
||||
deprecate("Use Editor::joinLines() instead")
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{View} = require 'space-pen'
|
||||
{$$} = require 'space-pencil'
|
||||
{React} = require 'reactionary'
|
||||
EditorComponent = require './editor-component'
|
||||
|
||||
|
||||
@@ -7,10 +7,8 @@ SelectionComponent = React.createClass
|
||||
mixins: [SubscriberMixin]
|
||||
|
||||
render: ->
|
||||
{selection, lineHeight, charWidth} = @props
|
||||
{cursor} = selection
|
||||
div className: 'selection',
|
||||
CursorComponent({cursor, lineHeight, charWidth})
|
||||
CursorComponent(cursor: @props.selection.cursor)
|
||||
|
||||
componentDidMount: ->
|
||||
@subscribe @props.selection, 'screen-range-changed', => @forceUpdate()
|
||||
|
||||
Reference in New Issue
Block a user