diff --git a/spec/lines-yardstick-spec.coffee b/spec/lines-yardstick-spec.coffee new file mode 100644 index 000000000..6ec5ddd39 --- /dev/null +++ b/spec/lines-yardstick-spec.coffee @@ -0,0 +1,58 @@ +LinesYardstick = require '../src/lines-yardstick' +MockLineNodesProvider = require './mock-line-nodes-provider' + +describe "LinesYardstick", -> + [editor, mockLineNodesProvider, builtLineNodes, linesYardstick] = [] + + beforeEach -> + waitsForPromise -> + atom.packages.activatePackage('language-javascript') + + waitsForPromise -> + atom.project.open('sample.js').then (o) -> editor = o + + runs -> + mockLineNodesProvider = new MockLineNodesProvider(editor) + linesYardstick = new LinesYardstick(editor, mockLineNodesProvider) + + afterEach -> + mockLineNodesProvider.dispose() + + it "converts screen positions to pixel positions", -> + mockLineNodesProvider.setDefaultFont("14px monospace") + + conversionTable = [ + [[0, 0], {left: 0, top: editor.getLineHeightInPixels() * 0}] + [[0, 3], {left: 24, top: editor.getLineHeightInPixels() * 0}] + [[0, 4], {left: 32, top: editor.getLineHeightInPixels() * 0}] + [[0, 5], {left: 40, top: editor.getLineHeightInPixels() * 0}] + [[1, 0], {left: 0, top: editor.getLineHeightInPixels() * 1}] + [[1, 1], {left: 0, top: editor.getLineHeightInPixels() * 1}] + [[1, 6], {left: 48, top: editor.getLineHeightInPixels() * 1}] + [[1, Infinity], {left: 240, top: editor.getLineHeightInPixels() * 1}] + ] + + for [point, position] in conversionTable + expect( + linesYardstick.pixelPositionForScreenPosition(point) + ).toEqual(position) + + mockLineNodesProvider.setFontForScopes( + ["source.js", "storage.modifier.js"], "16px monospace" + ) + + conversionTable = [ + [[0, 0], {left: 0, top: editor.getLineHeightInPixels() * 0}] + [[0, 3], {left: 30, top: editor.getLineHeightInPixels() * 0}] + [[0, 4], {left: 38, top: editor.getLineHeightInPixels() * 0}] + [[0, 5], {left: 46, top: editor.getLineHeightInPixels() * 0}] + [[1, 0], {left: 0, top: editor.getLineHeightInPixels() * 1}] + [[1, 1], {left: 0, top: editor.getLineHeightInPixels() * 1}] + [[1, 6], {left: 54, top: editor.getLineHeightInPixels() * 1}] + [[1, Infinity], {left: 246, top: editor.getLineHeightInPixels() * 1}] + ] + + for [point, position] in conversionTable + expect( + linesYardstick.pixelPositionForScreenPosition(point) + ).toEqual(position) diff --git a/spec/mock-line-nodes-provider.coffee b/spec/mock-line-nodes-provider.coffee new file mode 100644 index 000000000..81ed5a154 --- /dev/null +++ b/spec/mock-line-nodes-provider.coffee @@ -0,0 +1,34 @@ +TokenIterator = require '../src/token-iterator' + +module.exports = +class MockLineNodesProvider + constructor: (@editor) -> + @defaultFont = "" + @fontsByScopes = {} + @tokenIterator = new TokenIterator + @builtLineNodes = [] + + dispose: -> + node.remove() for node in @builtLineNodes + + setFontForScopes: (scopes, font) -> @fontsByScopes[scopes] = font + + setDefaultFont: (font) -> @defaultFont = font + + lineNodeForScreenRow: (screenRow) -> + lineNode = document.createElement("div") + lineNode.style.whiteSpace = "pre" + lineState = @editor.tokenizedLineForScreenRow(screenRow) + + @tokenIterator.reset(lineState) + while @tokenIterator.next() + font = @fontsByScopes[@tokenIterator.getScopes()] or @defaultFont + span = document.createElement("span") + span.style.font = font + span.textContent = @tokenIterator.getText() + lineNode.innerHTML += span.outerHTML + + @builtLineNodes.push(lineNode) + document.body.appendChild(lineNode) + + lineNode diff --git a/src/lines-yardstick.coffee b/src/lines-yardstick.coffee new file mode 100644 index 000000000..f361f403a --- /dev/null +++ b/src/lines-yardstick.coffee @@ -0,0 +1,82 @@ +TokenIterator = require './token-iterator' +AcceptFilter = {acceptNode: -> NodeFilter.FILTER_ACCEPT} +{Point} = require 'text-buffer' + +module.exports = +class LinesYardstick + constructor: (@model, @lineNodesProvider) -> + @tokenIterator = new TokenIterator + @rangeForMeasurement = document.createRange() + + pixelPositionForScreenPosition: (screenPosition, clip=true) -> + screenPosition = Point.fromObject(screenPosition) + screenPosition = @model.clipScreenPosition(screenPosition) if clip + + targetRow = screenPosition.row + targetColumn = screenPosition.column + baseCharacterWidth = @baseCharacterWidth + + top = targetRow * @model.getLineHeightInPixels() + left = @leftPixelPositionForScreenPosition(targetRow, targetColumn) + + {top, left} + + leftPixelPositionForScreenPosition: (row, column) -> + lineNode = @lineNodesProvider.lineNodeForScreenRow(row) + + tokenizedLine = @model.tokenizedLineForScreenRow(row) + iterator = document.createNodeIterator(lineNode, NodeFilter.SHOW_TEXT, AcceptFilter) + charIndex = 0 + + @tokenIterator.reset(tokenizedLine) + while @tokenIterator.next() + text = @tokenIterator.getText() + + textIndex = 0 + while textIndex < text.length + if @tokenIterator.isPairedCharacter() + char = text + charLength = 2 + textIndex += 2 + else + char = text[textIndex] + charLength = 1 + textIndex++ + + continue if char is '\0' + + unless textNode? + textNode = iterator.nextNode() + textNodeLength = textNode.textContent.length + textNodeIndex = 0 + nextTextNodeIndex = textNodeLength + + while nextTextNodeIndex <= charIndex + textNode = iterator.nextNode() + textNodeLength = textNode.textContent.length + textNodeIndex = nextTextNodeIndex + nextTextNodeIndex = textNodeIndex + textNodeLength + + if charIndex is column + indexWithinToken = charIndex - textNodeIndex + return @leftPixelPositionForCharInTextNode(textNode, indexWithinToken) + + charIndex += charLength + + if textNode? + @leftPixelPositionForCharInTextNode(textNode, textNode.textContent.length) + else + 0 + + leftPixelPositionForCharInTextNode: (textNode, charIndex) -> + @rangeForMeasurement.setEnd(textNode, textNode.textContent.length) + + if charIndex is 0 + @rangeForMeasurement.setStart(textNode, 0) + @rangeForMeasurement.getBoundingClientRect().left + else if charIndex is textNode.textContent.length + @rangeForMeasurement.setStart(textNode, 0) + @rangeForMeasurement.getBoundingClientRect().right + else + @rangeForMeasurement.setStart(textNode, charIndex) + @rangeForMeasurement.getBoundingClientRect().left