Skip to content
This repository has been archived by the owner on Mar 3, 2023. It is now read-only.

Character Measurements (iframe) #7945

Closed
wants to merge 61 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
92e1388
:art: Extract `LineHtmlBuilder` out
as-cii Jul 7, 2015
f66fde5
Create a `LinesYardstick` to measure stuff
as-cii Jul 7, 2015
b396ef4
Use a `console.log` instead for realistic metrics
as-cii Jul 7, 2015
70d5f9f
Measure multiple lines
as-cii Jul 7, 2015
c9d04ef
:racehorse: No Spans
as-cii Jul 7, 2015
25e28e1
:racehorse: Measure lineOffset only once
as-cii Jul 7, 2015
eed3491
:art:
as-cii Jul 7, 2015
87df7df
:racehorse: We don't need to compute a `lineOffset`
as-cii Jul 8, 2015
fd7ced5
Remove `style` from line in measuring context
as-cii Jul 8, 2015
14b4764
Remove the DOM node before parsing HTML
as-cii Jul 8, 2015
2f3125d
:fire:
as-cii Jul 8, 2015
abad3e2
Use the same HTML on the measuring context
as-cii Jul 13, 2015
e2ae88d
Merge branch 'master' into as-iframe-measurements
as-cii Jul 13, 2015
d7f05b0
:green_heart: Wait for `linesYardstick` to be fully initialized
as-cii Jul 14, 2015
b4596f7
:art: Start crafting `pixelPositionForScreenPosition`
as-cii Jul 14, 2015
e454b3f
Use real font information in `LinesYardstick`
as-cii Jul 14, 2015
22663fc
:memo: Remember which lines to precompute
as-cii Jul 14, 2015
be48432
:green_heart: Adapt specs to the new tokens HTML
as-cii Jul 14, 2015
1050bab
Use `forEach` so that we can accept both `Set` and `Array`
as-cii Jul 15, 2015
a0a13d3
:racehorse: Make HTML insertion incremental
as-cii Jul 15, 2015
4b66b21
:bug: Fix `LinesYardstick`
as-cii Jul 15, 2015
2e3eb25
Identify lines by id
as-cii Jul 16, 2015
b1900f4
Rebuild `lineNodesByScreenRow`
as-cii Jul 16, 2015
fb4df84
:art:
as-cii Jul 16, 2015
e9a86d4
Begin to improve generated lines HTML
as-cii Jul 16, 2015
270fa1f
Split `updateContentDimensions`
as-cii Jul 16, 2015
e2ee49e
Guard against unconstructed line nodes
as-cii Jul 16, 2015
13608a7
Batch cursor updates
as-cii Jul 16, 2015
b063d7b
Replace `TextEditorPresenter#pixelPositionForScreenPosition`
as-cii Jul 16, 2015
ec9ffbd
:bug: Round down while finding token
as-cii Jul 16, 2015
5483cbc
Remove unnecessary level of nesting
as-cii Jul 17, 2015
e97cd3e
Add syntax theme to `LinesYardstick`
as-cii Jul 17, 2015
23794de
:fire:
as-cii Jul 17, 2015
28405e0
:racehorse: Introduce smaller measuring contexts
as-cii Jul 19, 2015
3a16156
:art:
as-cii Jul 19, 2015
ea2dd19
Use `line.id` to perform changes in `LinesYardstick`
as-cii Jul 21, 2015
71ad3eb
:bug: Store only new lines to avoid mismatches
as-cii Jul 21, 2015
2257688
:racehorse: Use a subset of `atom-styles`
as-cii Jul 21, 2015
eb8a1c0
Build only those scopes that have an impact on char widths
as-cii Jul 21, 2015
b4209b0
Merge branch 'master' into as-iframe-measurements
as-cii Jul 22, 2015
63ca3d5
:bug: Fix trailing whitespace end index calculation
as-cii Jul 23, 2015
377771c
:bug: Guard against empty trailing whitespace
as-cii Jul 23, 2015
44c34d8
Remove null bytes from generated HTML
as-cii Jul 23, 2015
c7b6d58
Correctly exclude scope tokens
as-cii Jul 23, 2015
b40fb11
Exclude extra spans for lines on the main document
as-cii Jul 23, 2015
49eb45d
:art: Revert changes to specs
as-cii Jul 23, 2015
c729d78
Merge branch 'master' into as-iframe-measurements
as-cii Jul 24, 2015
b7e8155
Merge branch 'master' into as-iframe-measurements
as-cii Jul 24, 2015
82910ec
Merge branch 'master' into as-iframe-measurements
as-cii Aug 13, 2015
57637cc
Merge branch 'master' into as-iframe-measurements
as-cii Aug 26, 2015
9ce3c07
Use TokenIterator and NodeIterator to measure stuff
as-cii Aug 26, 2015
5b79955
Some :art:
as-cii Aug 26, 2015
6d1b2e0
Refactor LinesYardstick to be ignorant about lines creation
as-cii Aug 26, 2015
1427e37
Start working on `TextEditorPresenter` specs
as-cii Aug 26, 2015
556f794
:racehorse: Remove double iteration of tokens
as-cii Aug 26, 2015
45da0e7
Merge branch 'master' into as-iframe-measurements
as-cii Aug 26, 2015
8764721
Reset fonts on yardstick initialization
as-cii Aug 26, 2015
89cad9f
Merge branch 'master' into as-iframe-measurements
as-cii Aug 27, 2015
7faca84
:racehorse: Build only needed rows
as-cii Aug 27, 2015
78287dd
Merge branch 'master' into as-iframe-measurements
as-cii Aug 27, 2015
4e96d4c
:green_heart: Fix `TextEditorPresenter` specs
as-cii Aug 27, 2015
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
101 changes: 101 additions & 0 deletions spec/lines-yardstick-spec.coffee
@@ -0,0 +1,101 @@
LinesYardstick = require '../src/lines-yardstick'
{Point} = require 'text-buffer'

describe "LinesYardstick", ->
[linesYardstick, editor, styleNodesToRemove] = []

styleSheetWithSelectorAndFont = (selector, font) ->
styleNode = document.createElement("style")
styleNode.innerHTML = """
#{selector} {
font: #{font};
}
"""
document.body.appendChild(styleNode)
styleNodesToRemove ?= []
styleNodesToRemove.push(styleNode)
styleNode

cleanupStyleSheets = ->
styleNode.remove() while styleNode = styleNodesToRemove?.pop()

beforeEach ->
waitsForPromise ->
atom.packages.activatePackage('language-javascript')

waitsForPromise ->
atom.project.open('sample.js').then (o) -> editor = o

runs ->
linesYardstick = new LinesYardstick(editor)
document.body.appendChild(linesYardstick.getDomNode())

waitsFor ->
linesYardstick.canMeasure()

afterEach ->
linesYardstick.getDomNode().remove()
cleanupStyleSheets()

describe "::buildDomNodesForScreenRows(screenRows)", ->
it "asks for a line HTML only once", ->
requestedLinesByScreenRow = {}
linesYardstick.setLineHtmlProvider (screenRow, line) ->
requestedLinesByScreenRow[screenRow] ?= 0
requestedLinesByScreenRow[screenRow]++

"<div></div>"

linesYardstick.buildDomNodesForScreenRows([0, 1, 2])
linesYardstick.buildDomNodesForScreenRows([1, 2, 3])
linesYardstick.buildDomNodesForScreenRows([3, 4, 5])

expect(Object.keys(requestedLinesByScreenRow).length).not.toBe(0)
for screenRow, requestsCount of requestedLinesByScreenRow
expect(requestsCount).toBe(1)

describe "::leftPixelPositionForScreenPosition(point)", ->
it "measure positions based on stylesheets and default font", ->
editor.setText("hello\nworld\n")
linesYardstick.setDefaultFont("monospace", "14px")
linesYardstick.setLineHtmlProvider (screenRow, line) ->
if screenRow is 0
"<div>he<span class='bigger'>l</span>lo</div>"
else if screenRow is 1
"<div>world</div>"
else
throw new Error("This screen row shouldn't have been requested.")

linesYardstick.buildDomNodesForScreenRows([0, 1])

conversionTable = [
[new Point(0, 0), {left: 0, top: editor.getLineHeightInPixels() * 0}]
[new Point(0, 1), {left: 8, top: editor.getLineHeightInPixels() * 0}]
[new Point(0, 3), {left: 24, top: editor.getLineHeightInPixels() * 0}]
[new Point(1, 0), {left: 0, top: editor.getLineHeightInPixels() * 1}]
[new Point(1, 1), {left: 8, top: editor.getLineHeightInPixels() * 1}]
[new Point(1, 4), {left: 32, top: editor.getLineHeightInPixels() * 1}]
]

for [point, position] in conversionTable
expect(
linesYardstick.pixelPositionForScreenPosition(point)
).toEqual(position)

linesYardstick.resetStyleSheets([
styleSheetWithSelectorAndFont(".bigger", "16px monospace")
])

conversionTable = [
[new Point(0, 0), {left: 0, top: 0 * editor.getLineHeightInPixels()}]
[new Point(0, 1), {left: 8, top: 0 * editor.getLineHeightInPixels()}]
[new Point(0, 3), {left: 26, top: 0 * editor.getLineHeightInPixels()}]
[new Point(1, 0), {left: 0, top: 1 * editor.getLineHeightInPixels()}]
[new Point(1, 1), {left: 8, top: 1 * editor.getLineHeightInPixels()}]
[new Point(1, 4), {left: 32, top: 1 * editor.getLineHeightInPixels()}]
]

for [point, position] in conversionTable
expect(
linesYardstick.pixelPositionForScreenPosition(point)
).toEqual(position)
38 changes: 37 additions & 1 deletion spec/text-editor-component-spec.coffee
Expand Up @@ -5,7 +5,7 @@ TextEditorView = require '../src/text-editor-view'
TextEditorComponent = require '../src/text-editor-component'
nbsp = String.fromCharCode(160)

describe "TextEditorComponent", ->
fdescribe "TextEditorComponent", ->
[contentNode, editor, wrapperView, wrapperNode, component, componentNode, verticalScrollbarNode, horizontalScrollbarNode] = []
[lineHeightInPixels, charWidth, nextAnimationFrame, noAnimationFrame, tileSize, tileHeightInPixels] = []

Expand Down Expand Up @@ -40,6 +40,11 @@ describe "TextEditorComponent", ->
wrapperNode.setUpdatedSynchronously(false)

{component} = wrapperView

waitsFor ->
component.linesYardstick.canMeasure()

runs ->
component.setFontFamily('monospace')
component.setLineHeight(1.3)
component.setFontSize(20)
Expand Down Expand Up @@ -77,6 +82,37 @@ describe "TextEditorComponent", ->

expect(component.lineNodeForScreenRow(0).textContent).not.toBe("You shouldn't see this update.")

describe "measurements", ->
it "is equivalent to TextEditorPresenter::pixelPositionForScreenPosition", ->
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this comparison test for now, but will we be able to drop the old method before merging this and test the new method directly?

screenRows = new Set([0...editor.getScreenLineCount()])
component.prepareScreenRowsForMeasurement(screenRows)

screenRows.forEach (screenRow) ->
length = editor.tokenizedLineForScreenRow(screenRow).getMaxScreenColumn()

for screenColumn in [0..length] by 1
point = [screenRow, screenColumn]
actual = component.pixelPositionForScreenPosition(point)
expected = component.presenter.pixelPositionForScreenPosition(point)

expect(expected).toEqual(actual)

component.setFontSize(14)
component.measureDimensions()
nextAnimationFrame()

component.prepareScreenRowsForMeasurement(screenRows)

screenRows.forEach (screenRow) ->
length = editor.tokenizedLineForScreenRow(screenRow).getMaxScreenColumn()

for screenColumn in [0..length] by 1
point = [screenRow, screenColumn]
actual = component.pixelPositionForScreenPosition(point)
expected = component.presenter.pixelPositionForScreenPosition(point)

expect(expected).toEqual(actual)

describe "line rendering", ->
expectTileContainsRow = (tileNode, screenRow, {top}) ->
lineNode = tileNode.querySelector("[data-screen-row='#{screenRow}']")
Expand Down
105 changes: 35 additions & 70 deletions spec/text-editor-presenter-spec.coffee
Expand Up @@ -4,12 +4,13 @@ TextBuffer = require 'text-buffer'
{Point, Range} = TextBuffer
TextEditor = require '../src/text-editor'
TextEditorPresenter = require '../src/text-editor-presenter'
LinesYardstick = require '../src/lines-yardstick'

describe "TextEditorPresenter", ->
# These `describe` and `it` blocks mirror the structure of the ::state object.
# Please maintain this structure when adding specs for new state fields.
describe "::getState()", ->
[buffer, editor] = []
[buffer, editor, linesYardstick] = []

beforeEach ->
# These *should* be mocked in the spec helper, but changing that now would break packages :-(
Expand All @@ -18,11 +19,20 @@ describe "TextEditorPresenter", ->

buffer = new TextBuffer(filePath: require.resolve('./fixtures/sample.js'))
editor = new TextEditor({buffer})
linesYardstick = new LinesYardstick(editor)
linesYardstick.setDefaultFont("monospace", "16px")
linesYardstick.setLineHtmlProvider (screenRow, line) ->
"<div>#{line.text}</div>"
document.body.appendChild(linesYardstick.getDomNode())

waitsForPromise -> buffer.load()

waitsFor -> linesYardstick.canMeasure()

afterEach ->
editor.destroy()
buffer.destroy()
linesYardstick.getDomNode().remove()

buildPresenter = (params={}) ->
_.defaults params,
Expand All @@ -39,6 +49,7 @@ describe "TextEditorPresenter", ->
verticalScrollbarWidth: 10
scrollTop: 0
scrollLeft: 0
linesYardstick: linesYardstick

new TextEditorPresenter(params)

Expand Down Expand Up @@ -289,25 +300,16 @@ describe "TextEditorPresenter", ->
expectStateUpdate presenter, -> presenter.setContentFrameWidth(10 * maxLineLength + 20)
expect(presenter.getState().horizontalScrollbar.scrollWidth).toBe 10 * maxLineLength + 20

it "updates when the ::baseCharacterWidth changes", ->
it "updates when character widths change", ->
maxLineLength = editor.getMaxScreenLineLength()
presenter = buildPresenter(contentFrameWidth: 50, baseCharacterWidth: 10)

expect(presenter.getState().horizontalScrollbar.scrollWidth).toBe 10 * maxLineLength + 1
expectStateUpdate presenter, -> presenter.setBaseCharacterWidth(15)
expectStateUpdate presenter, ->
linesYardstick.setDefaultFont("monospace", "25px")
presenter.characterWidthsChanged()
expect(presenter.getState().horizontalScrollbar.scrollWidth).toBe 15 * maxLineLength + 1

it "updates when the scoped character widths change", ->
waitsForPromise -> atom.packages.activatePackage('language-javascript')

runs ->
maxLineLength = editor.getMaxScreenLineLength()
presenter = buildPresenter(contentFrameWidth: 50, baseCharacterWidth: 10)

expect(presenter.getState().horizontalScrollbar.scrollWidth).toBe 10 * maxLineLength + 1
expectStateUpdate presenter, -> presenter.setScopedCharacterWidth(['source.js', 'support.function.js'], 'p', 20)
expect(presenter.getState().horizontalScrollbar.scrollWidth).toBe (10 * (maxLineLength - 2)) + (20 * 2) + 1 # 2 of the characters are 20px wide now instead of 10px wide

it "updates when ::softWrapped changes on the editor", ->
presenter = buildPresenter(contentFrameWidth: 470, baseCharacterWidth: 10)
expect(presenter.getState().horizontalScrollbar.scrollWidth).toBe 10 * editor.getMaxScreenLineLength() + 1
Expand Down Expand Up @@ -533,12 +535,11 @@ describe "TextEditorPresenter", ->
presenter = buildPresenter()
expect(presenter.getState().hiddenInput.width).toBe 10

expectStateUpdate presenter, -> presenter.setBaseCharacterWidth(15)
expectStateUpdate presenter, ->
linesYardstick.setDefaultFont("monospace", "25px")
presenter.characterWidthsChanged()
expect(presenter.getState().hiddenInput.width).toBe 15

expectStateUpdate presenter, -> presenter.setScopedCharacterWidth(['source.js', 'storage.modifier.js'], 'r', 20)
expect(presenter.getState().hiddenInput.width).toBe 20

it "is 2px at the end of lines", ->
presenter = buildPresenter()
editor.setCursorBufferPosition([3, Infinity])
Expand Down Expand Up @@ -623,25 +624,16 @@ describe "TextEditorPresenter", ->
expectStateUpdate presenter, -> presenter.setContentFrameWidth(10 * maxLineLength + 20)
expect(presenter.getState().content.scrollWidth).toBe 10 * maxLineLength + 20

it "updates when the ::baseCharacterWidth changes", ->
it "updates when character widths changes", ->
maxLineLength = editor.getMaxScreenLineLength()
presenter = buildPresenter(contentFrameWidth: 50, baseCharacterWidth: 10)

expect(presenter.getState().content.scrollWidth).toBe 10 * maxLineLength + 1
expectStateUpdate presenter, -> presenter.setBaseCharacterWidth(15)
expectStateUpdate presenter, ->
linesYardstick.setDefaultFont("monospace", "25px")
presenter.characterWidthsChanged()
expect(presenter.getState().content.scrollWidth).toBe 15 * maxLineLength + 1

it "updates when the scoped character widths change", ->
waitsForPromise -> atom.packages.activatePackage('language-javascript')

runs ->
maxLineLength = editor.getMaxScreenLineLength()
presenter = buildPresenter(contentFrameWidth: 50, baseCharacterWidth: 10)

expect(presenter.getState().content.scrollWidth).toBe 10 * maxLineLength + 1
expectStateUpdate presenter, -> presenter.setScopedCharacterWidth(['source.js', 'support.function.js'], 'p', 20)
expect(presenter.getState().content.scrollWidth).toBe (10 * (maxLineLength - 2)) + (20 * 2) + 1 # 2 of the characters are 20px wide now instead of 10px wide

it "updates when ::softWrapped changes on the editor", ->
presenter = buildPresenter(contentFrameWidth: 470, baseCharacterWidth: 10)
expect(presenter.getState().content.scrollWidth).toBe 10 * editor.getMaxScreenLineLength() + 1
Expand Down Expand Up @@ -1179,27 +1171,15 @@ describe "TextEditorPresenter", ->
expect(stateForCursor(presenter, 3)).toEqual {top: 5, left: 12 * 10, width: 10, height: 5}
expect(stateForCursor(presenter, 4)).toEqual {top: 8 * 5 - 20, left: 4 * 10, width: 10, height: 5}

it "updates when ::baseCharacterWidth changes", ->
it "updates when character widths change", ->
editor.setCursorBufferPosition([2, 4])
presenter = buildPresenter(explicitHeight: 20, scrollTop: 20)

expectStateUpdate presenter, -> presenter.setBaseCharacterWidth(20)
expectStateUpdate presenter, ->
linesYardstick.setDefaultFont("monospace", "33px")
presenter.characterWidthsChanged()
expect(stateForCursor(presenter, 0)).toEqual {top: 0, left: 4 * 20, width: 20, height: 10}

it "updates when scoped character widths change", ->
waitsForPromise ->
atom.packages.activatePackage('language-javascript')

runs ->
editor.setCursorBufferPosition([1, 4])
presenter = buildPresenter(explicitHeight: 20)

expectStateUpdate presenter, -> presenter.setScopedCharacterWidth(['source.js', 'storage.modifier.js'], 'v', 20)
expect(stateForCursor(presenter, 0)).toEqual {top: 1 * 10, left: (3 * 10) + 20, width: 10, height: 10}

expectStateUpdate presenter, -> presenter.setScopedCharacterWidth(['source.js', 'storage.modifier.js'], 'r', 20)
expect(stateForCursor(presenter, 0)).toEqual {top: 1 * 10, left: (3 * 10) + 20, width: 20, height: 10}

it "updates when cursors are added, moved, hidden, shown, or destroyed", ->
editor.setSelectedBufferRanges([
[[1, 2], [1, 2]],
Expand Down Expand Up @@ -1513,7 +1493,7 @@ describe "TextEditorPresenter", ->
]
}

it "updates when ::baseCharacterWidth changes", ->
it "updates when character widths change", ->
editor.setSelectedBufferRanges([
[[2, 2], [2, 4]],
])
Expand All @@ -1523,30 +1503,13 @@ describe "TextEditorPresenter", ->
expectValues stateForSelectionInTile(presenter, 0, 2), {
regions: [{top: 0, left: 2 * 10, width: 2 * 10, height: 10}]
}
expectStateUpdate presenter, -> presenter.setBaseCharacterWidth(20)
expectStateUpdate presenter, ->
linesYardstick.setDefaultFont("monospace", "33px")
presenter.characterWidthsChanged()
expectValues stateForSelectionInTile(presenter, 0, 2), {
regions: [{top: 0, left: 2 * 20, width: 2 * 20, height: 10}]
}

it "updates when scoped character widths change", ->
waitsForPromise ->
atom.packages.activatePackage('language-javascript')

runs ->
editor.setSelectedBufferRanges([
[[2, 4], [2, 6]],
])

presenter = buildPresenter(explicitHeight: 20, scrollTop: 0, tileSize: 2)

expectValues stateForSelectionInTile(presenter, 0, 2), {
regions: [{top: 0, left: 4 * 10, width: 2 * 10, height: 10}]
}
expectStateUpdate presenter, -> presenter.setScopedCharacterWidth(['source.js', 'keyword.control.js'], 'i', 20)
expectValues stateForSelectionInTile(presenter, 0, 2), {
regions: [{top: 0, left: 4 * 10, width: 20 + 10, height: 10}]
}

it "updates when highlight decorations are added, moved, hidden, shown, or destroyed", ->
editor.setSelectedBufferRanges([
[[1, 2], [1, 4]],
Expand Down Expand Up @@ -1695,7 +1658,7 @@ describe "TextEditorPresenter", ->
pixelPosition: {top: 3 * 10 - presenter.state.content.scrollTop, left: 13 * 10}
}

it "updates when ::baseCharacterWidth changes", ->
it "updates when character widths change", ->
scrollTop = 20
marker = editor.markBufferPosition([2, 13], invalidate: 'touch')
decoration = editor.decorateMarker(marker, {type: 'overlay', item})
Expand All @@ -1706,7 +1669,9 @@ describe "TextEditorPresenter", ->
pixelPosition: {top: 3 * 10 - scrollTop, left: 13 * 10}
}

expectStateUpdate presenter, -> presenter.setBaseCharacterWidth(5)
expectStateUpdate presenter, ->
linesYardstick.setDefaultFont("monospace", "8px")
presenter.characterWidthsChanged()

expectValues stateForOverlay(presenter, decoration), {
item: item
Expand Down