Merge pull request #3910 from atom/bo-descriptor-object

Add a ScopeDescriptor object
This commit is contained in:
Ben Ogle
2014-10-21 13:27:17 -07:00
14 changed files with 156 additions and 120 deletions

View File

@@ -9,6 +9,7 @@ pathWatcher = require 'pathwatcher'
{deprecate} = require 'grim'
ScopedPropertyStore = require 'scoped-property-store'
ScopeDescriptor = require './scope-descriptor'
# Essential: Used to access all of Atom's configuration details.
#
@@ -335,7 +336,7 @@ class Config
# # do stuff with value
# ```
#
# * `scopeDescriptor` (optional) {Array} of {String}s describing a path from
# * `scopeDescriptor` (optional) {ScopeDescriptor} describing a path from
# the root of the syntax tree to a token. Get one by calling
# {editor.getLastCursor().getScopeDescriptor()}. See {::get} for examples.
# See [the scopes docs](https://atom.io/docs/latest/advanced/scopes-and-scope-descriptors)
@@ -351,7 +352,7 @@ class Config
if args.length is 2
# observe(keyPath, callback)
[keyPath, callback, scopeDescriptor, options] = args
else if args.length is 3 and Array.isArray(scopeDescriptor)
else if args.length is 3 and (Array.isArray(scopeDescriptor) or scopeDescriptor instanceof ScopeDescriptor)
# observe(scopeDescriptor, keyPath, callback)
[scopeDescriptor, keyPath, callback, options] = args
else if args.length is 3 and _.isString(scopeDescriptor) and _.isObject(keyPath)
@@ -373,7 +374,7 @@ class Config
# Essential: Add a listener for changes to a given key path. If `keyPath` is
# not specified, your callback will be called on changes to any key.
#
# * `scopeDescriptor` (optional) {Array} of {String}s describing a path from
# * `scopeDescriptor` (optional) {ScopeDescriptor} describing a path from
# the root of the syntax tree to a token. Get one by calling
# {editor.getLastCursor().getScopeDescriptor()}. See {::get} for examples.
# See [the scopes docs](https://atom.io/docs/latest/advanced/scopes-and-scope-descriptors)
@@ -444,7 +445,7 @@ class Config
# atom.config.get(scopeDescriptor, 'editor.tabLength') # => 2
# ```
#
# * `scopeDescriptor` (optional) {Array} of {String}s describing a path from
# * `scopeDescriptor` (optional) {ScopeDescriptor} describing a path from
# the root of the syntax tree to a token. Get one by calling
# {editor.getLastCursor().getScopeDescriptor()}
# See [the scopes docs](https://atom.io/docs/latest/advanced/scopes-and-scope-descriptors)
@@ -904,8 +905,8 @@ class Config
@emitter.emit 'did-change'
getRawScopedValue: (scopeDescriptor, keyPath) ->
scopeChain = @scopeChainForScopeDescriptor(scopeDescriptor)
@scopedSettingsStore.getPropertyValue(scopeChain, keyPath)
scopeDescriptor = ScopeDescriptor.fromObject(scopeDescriptor)
@scopedSettingsStore.getPropertyValue(scopeDescriptor.getScopeChain(), keyPath)
observeScopedKeyPath: (scopeDescriptor, keyPath, callback) ->
oldValue = @get(scopeDescriptor, keyPath)
@@ -932,19 +933,8 @@ class Config
# * language mode uses it for one thing.
# * autocomplete uses it for editor.completions
settingsForScopeDescriptor: (scopeDescriptor, keyPath) ->
scopeChain = scopeDescriptor
.map (scope) ->
scope = ".#{scope}" unless scope[0] is '.'
scope
.join(' ')
@scopedSettingsStore.getProperties(scopeChain, keyPath)
scopeChainForScopeDescriptor: (scopeDescriptor) ->
scopeDescriptor
.map (scope) ->
scope = ".#{scope}" unless scope[0] is '.'
scope
.join(' ')
scopeDescriptor = ScopeDescriptor.fromObject(scopeDescriptor)
@scopedSettingsStore.getProperties(scopeDescriptor.getScopeChain(), keyPath)
# Base schema enforcers. These will coerce raw input into the specified type,
# and will throw an error when the value cannot be coerced. Throwing the error

View File

@@ -216,12 +216,12 @@ class Cursor extends Model
# Public: Retrieves the scope descriptor for the cursor's current position.
#
# Returns an {Array} of {String}s.
# Returns a {ScopeDescriptor}
getScopeDescriptor: ->
@editor.scopeDescriptorForBufferPosition(@getBufferPosition())
getScopes: ->
Grim.deprecate 'Use Cursor::getScopeDescriptor() instead'
@getScopeDescriptor()
@getScopeDescriptor().getScopesArray()
# Public: Returns true if this cursor has no non-whitespace characters before
# its current position.

View File

@@ -650,7 +650,7 @@ class DisplayBuffer extends Model
left = 0
column = 0
for token in @tokenizedLineForScreenRow(targetRow).tokens
charWidths = @getScopedCharWidths(token.scopeDescriptor)
charWidths = @getScopedCharWidths(token.scopes)
for char in token.value
return {top, left} if column is targetColumn
left += charWidths[char] ? defaultCharWidth unless char is '\0'
@@ -669,7 +669,7 @@ class DisplayBuffer extends Model
left = 0
column = 0
for token in @tokenizedLineForScreenRow(row).tokens
charWidths = @getScopedCharWidths(token.scopeDescriptor)
charWidths = @getScopedCharWidths(token.scopes)
for char in token.value
charWidth = charWidths[char] ? defaultCharWidth
break if targetLeft <= left + (charWidth / 2)
@@ -757,7 +757,7 @@ class DisplayBuffer extends Model
#
# bufferPosition - A {Point} in the {TextBuffer}
#
# Returns an {Array} of {String}s.
# Returns a {ScopeDescriptor}.
scopeDescriptorForBufferPosition: (bufferPosition) ->
@tokenizedBuffer.scopeDescriptorForPosition(bufferPosition)

View File

@@ -200,7 +200,7 @@ LinesComponent = React.createClass
firstTrailingWhitespacePosition = text.search(/\s*$/)
lineIsWhitespaceOnly = firstTrailingWhitespacePosition is 0
for token in tokens
innerHTML += @updateScopeStack(scopeStack, token.scopeDescriptor)
innerHTML += @updateScopeStack(scopeStack, token.scopes)
hasIndentGuide = not mini and showIndentGuide and (token.hasLeadingWhitespace() or (token.hasTrailingWhitespace() and lineIsWhitespaceOnly))
innerHTML += token.getValueAsHtml({hasIndentGuide})
@@ -308,8 +308,8 @@ LinesComponent = React.createClass
iterator = null
charIndex = 0
for {value, scopeDescriptor}, tokenIndex in tokenizedLine.tokens
charWidths = editor.getScopedCharWidths(scopeDescriptor)
for {value, scopes}, tokenIndex in tokenizedLine.tokens
charWidths = editor.getScopedCharWidths(scopes)
for char in value
continue if char is '\0'
@@ -331,7 +331,7 @@ LinesComponent = React.createClass
rangeForMeasurement.setStart(textNode, i)
rangeForMeasurement.setEnd(textNode, i + 1)
charWidth = rangeForMeasurement.getBoundingClientRect().width
editor.setScopedCharWidth(scopeDescriptor, char, charWidth)
editor.setScopedCharWidth(scopes, char, charWidth)
charIndex++

View File

@@ -0,0 +1,46 @@
# Extended: Wraps an {Array} of `String`s. The Array describes a path from the
# root of the syntax tree to a token including _all_ scope names for the entire
# path.
#
# Methods that take a `ScopeDescriptor` will also accept an {Array} of {Strings}
# scope names e.g. `['.source.js']`.
#
# You can use `ScopeDescriptor`s to get language-specific config settings via
# {Config::get}.
#
# You should not need to create a `ScopeDescriptor` directly.
#
# * {Editor::getRootScopeDescriptor} to get the language's descriptor.
# * {Editor::scopeDescriptorForBufferPosition} to get the descriptor at a
# specific position in the buffer.
# * {Cursor::getScopeDescriptor} to get a cursor's descriptor based on position.
#
# See the [scopes and scope descriptor guide](https://atom.io/docs/v0.138.0/advanced/scopes-and-scope-descriptors)
# for more information.
module.exports =
class ScopeDescriptor
@fromObject: (scopes) ->
if scopes instanceof ScopeDescriptor
scopes
else
new ScopeDescriptor({scopes})
###
Section: Construction and Destruction
###
# Public: Create a {ScopeDescriptor} object.
#
# * `object` {Object}
# * `scopes` {Array} of {String}s
constructor: ({@scopes}) ->
# Public: Returns an {Array} of {String}s
getScopesArray: -> @scopes
getScopeChain: ->
@scopes
.map (scope) ->
scope = ".#{scope}" unless scope[0] is '.'
scope
.join(' ')

View File

@@ -32,7 +32,7 @@ class Syntax extends GrammarRegistry
serialize: ->
{deserializer: @constructor.name, @grammarOverridesByPath}
createToken: (value, scopeDescriptor) -> new Token({value, scopeDescriptor})
createToken: (value, scopes) -> new Token({value, scopes})
# Deprecated: Used by settings-view to display snippets for packages
@::accessor 'propertyStore', ->

View File

@@ -487,7 +487,7 @@ class TextEditor extends Model
when 'decoration-updated'
deprecate("Use Decoration::onDidChangeProperties instead. You will get the decoration back from `TextEditor::decorateMarker()`")
when 'decoration-changed'
deprecate("Use Marker::onDidChange instead. eg. `editor::decorateMarker(...).getMarker().onDidChange()`")
deprecate("Use Marker::onDidChange instead. e.g. `editor::decorateMarker(...).getMarker().onDidChange()`")
when 'screen-lines-changed'
deprecate("Use TextEditor::onDidChange instead")
@@ -1216,7 +1216,7 @@ class TextEditor extends Model
# ## Arguments
#
# * `marker` A {Marker} you want this decoration to follow.
# * `decorationParams` An {Object} representing the decoration eg. `{type: 'gutter', class: 'linter-error'}`
# * `decorationParams` An {Object} representing the decoration e.g. `{type: 'gutter', class: 'linter-error'}`
# * `type` There are a few supported decoration types: `gutter`, `line`, and `highlight`
# * `class` This CSS class will be applied to the decorated line number,
# line, or highlight.
@@ -2370,8 +2370,8 @@ class TextEditor extends Model
Section: Managing Syntax Scopes
###
# Essential: Returns the scope descriptor that includes the language. eg.
# `['.source.ruby']`, or `['.source.coffee']`. You can use this with
# Essential: Returns a {ScopeDescriptor} that includes this editor's language.
# e.g. `['.source.ruby']`, or `['.source.coffee']`. You can use this with
# {Config::get} to get language specific config values.
getRootScopeDescriptor: ->
@displayBuffer.getRootScopeDescriptor()
@@ -2385,11 +2385,12 @@ class TextEditor extends Model
#
# * `bufferPosition` A {Point} or {Array} of [row, column].
#
# Returns an {Array} of {String}s.
scopeDescriptorForBufferPosition: (bufferPosition) -> @displayBuffer.scopeDescriptorForBufferPosition(bufferPosition)
# Returns a {ScopeDescriptor}.
scopeDescriptorForBufferPosition: (bufferPosition) ->
@displayBuffer.scopeDescriptorForBufferPosition(bufferPosition)
scopesForBufferPosition: (bufferPosition) ->
deprecate 'Use ::scopeDescriptorForBufferPosition instead'
@scopeDescriptorForBufferPosition(bufferPosition)
deprecate 'Use ::scopeDescriptorForBufferPosition instead. The return value has changed! It now returns a `ScopeDescriptor`'
@scopeDescriptorForBufferPosition(bufferPosition).getScopesArray()
# Extended: Get the range in buffer coordinates of all tokens surrounding the
# cursor that match the given scope selector.
@@ -2397,14 +2398,16 @@ class TextEditor extends Model
# For example, if you wanted to find the string surrounding the cursor, you
# could call `editor.bufferRangeForScopeAtCursor(".string.quoted")`.
#
# * `scopeSelector` {String} selector. e.g. `'.source.ruby'`
#
# Returns a {Range}.
bufferRangeForScopeAtCursor: (selector) ->
@displayBuffer.bufferRangeForScopeAtPosition(selector, @getCursorBufferPosition())
bufferRangeForScopeAtCursor: (scopeSelector) ->
@displayBuffer.bufferRangeForScopeAtPosition(scopeSelector, @getCursorBufferPosition())
# Extended: Determine if the given row is entirely a comment
isBufferRowCommented: (bufferRow) ->
if match = @lineTextForBufferRow(bufferRow).match(/\S/)
scopeDescriptor = @tokenForBufferPosition([bufferRow, match.index]).scopeDescriptor
scopeDescriptor = @tokenForBufferPosition([bufferRow, match.index]).scopes
@commentScopeSelector ?= new TextMateScopeSelector('comment.*')
@commentScopeSelector.matches(scopeDescriptor)
@@ -2415,10 +2418,10 @@ class TextEditor extends Model
tokenForBufferPosition: (bufferPosition) -> @displayBuffer.tokenForBufferPosition(bufferPosition)
scopesAtCursor: ->
deprecate 'Use editor.getLastCursor().scopesAtCursor() instead'
@getLastCursor().getScopeDescriptor()
deprecate 'Use editor.getLastCursor().getScopeDescriptor() instead'
@getLastCursor().getScopeDescriptor().getScopesArray()
getCursorScopes: ->
deprecate 'Use editor.getLastCursor().scopesAtCursor() instead'
deprecate 'Use editor.getLastCursor().getScopeDescriptor() instead'
@scopesAtCursor()
###

View File

@@ -14,35 +14,28 @@ module.exports =
class Token
value: null
hasPairedCharacter: false
scopeDescriptor: null
scopes: null
isAtomic: null
isHardTab: null
firstNonWhitespaceIndex: null
firstTrailingWhitespaceIndex: null
hasInvisibleCharacters: false
Object.defineProperty @::, 'scopes', get: ->
deprecate 'Use ::scopeDescriptor instead'
@scopeDescriptor
constructor: ({@value, @scopeDescriptor, @isAtomic, @bufferDelta, @isHardTab}) ->
constructor: ({@value, @scopes, @isAtomic, @bufferDelta, @isHardTab}) ->
@screenDelta = @value.length
@bufferDelta ?= @screenDelta
@hasPairedCharacter = textUtils.hasPairedCharacter(@value)
isEqual: (other) ->
# TODO: scopes is deprecated. This is here for the sake of lang package tests
scopeDescriptor = other.scopeDescriptor ? other.scopes
deprecate 'Test the Token for `scopeDescriptor` rather than `scopes`' if other.scopes?
@value == other.value and _.isEqual(@scopeDescriptor, scopeDescriptor) and !!@isAtomic == !!other.isAtomic
@value == other.value and _.isEqual(@scopes, other.scopes) and !!@isAtomic == !!other.isAtomic
isBracket: ->
/^meta\.brace\b/.test(_.last(@scopeDescriptor))
/^meta\.brace\b/.test(_.last(@scopes))
splitAt: (splitIndex) ->
leftToken = new Token(value: @value.substring(0, splitIndex), scopeDescriptor: @scopeDescriptor)
rightToken = new Token(value: @value.substring(splitIndex), scopeDescriptor: @scopeDescriptor)
leftToken = new Token(value: @value.substring(0, splitIndex), scopes: @scopes)
rightToken = new Token(value: @value.substring(splitIndex), scopes: @scopes)
if @firstNonWhitespaceIndex?
leftToken.firstNonWhitespaceIndex = Math.min(splitIndex, @firstNonWhitespaceIndex)
@@ -101,7 +94,7 @@ class Token
else
breakOutLeadingSoftTabs = false
value = match[0]
token = new Token({value, @scopeDescriptor})
token = new Token({value, @scopes})
column += token.value.length
outputTokens.push(token)
@@ -115,7 +108,7 @@ class Token
while index < @value.length
if textUtils.isPairedCharacter(@value, index)
if nonPairStart isnt index
outputTokens.push(new Token({value: @value[nonPairStart...index], @scopeDescriptor}))
outputTokens.push(new Token({value: @value[nonPairStart...index], @scopes}))
outputTokens.push(@buildPairedCharacterToken(@value, index))
index += 2
nonPairStart = index
@@ -123,14 +116,14 @@ class Token
index++
if nonPairStart isnt index
outputTokens.push(new Token({value: @value[nonPairStart...index], @scopeDescriptor}))
outputTokens.push(new Token({value: @value[nonPairStart...index], @scopes}))
outputTokens
buildPairedCharacterToken: (value, index) ->
new Token(
value: value[index..index + 1]
scopeDescriptor: @scopeDescriptor
scopes: @scopes
isAtomic: true
)
@@ -144,7 +137,7 @@ class Token
tabStop = tabLength - (column % tabLength)
new Token(
value: _.multiplyString(" ", tabStop)
scopeDescriptor: @scopeDescriptor
scopes: @scopes
bufferDelta: if isHardTab then 1 else tabStop
isAtomic: true
isHardTab: isHardTab
@@ -155,7 +148,7 @@ class Token
matchesScopeSelector: (selector) ->
targetClasses = selector.replace(StartDotRegex, '').split('.')
_.any @scopeDescriptor, (scope) ->
_.any @scopes, (scope) ->
scopeClasses = scope.split('.')
_.isSubset(targetClasses, scopeClasses)

View File

@@ -6,6 +6,7 @@ EmitterMixin = require('emissary').Emitter
Serializable = require 'serializable'
TokenizedLine = require './tokenized-line'
Token = require './token'
ScopeDescriptor = require './scope-descriptor'
Grim = require 'grim'
module.exports =
@@ -79,7 +80,7 @@ class TokenizedBuffer extends Model
return if grammar is @grammar
@unsubscribe(@grammar) if @grammar
@grammar = grammar
@rootScopeDescriptor = [@grammar.scopeName]
@rootScopeDescriptor = new ScopeDescriptor(scopes: [@grammar.scopeName])
@currentGrammarScore = score ? grammar.getScore(@buffer.getPath(), @buffer.getText())
@subscribe @grammar.onDidUpdate => @retokenizeLines()
@@ -105,7 +106,7 @@ class TokenizedBuffer extends Model
hasTokenForSelector: (selector) ->
for {tokens} in @tokenizedLines
for token in tokens
return true if selector.matches(token.scopeDescriptor)
return true if selector.matches(token.scopes)
false
retokenizeLines: ->
@@ -247,7 +248,7 @@ class TokenizedBuffer extends Model
buildPlaceholderTokenizedLineForRow: (row) ->
line = @buffer.lineForRow(row)
tokens = [new Token(value: line, scopeDescriptor: [@grammar.scopeName])]
tokens = [new Token(value: line, scopes: [@grammar.scopeName])]
tabLength = @getTabLength()
indentLevel = @indentLevelForRow(row)
lineEnding = @buffer.lineEndingForRow(row)
@@ -303,7 +304,7 @@ class TokenizedBuffer extends Model
0
scopeDescriptorForPosition: (position) ->
@tokenForPosition(position).scopeDescriptor
new ScopeDescriptor(scopes: @tokenForPosition(position).scopes)
tokenForPosition: (position) ->
{row, column} = Point.fromObject(position)

View File

@@ -194,9 +194,9 @@ class TokenizedLine
isComment: ->
for token in @tokens
continue if token.scopeDescriptor.length is 1
continue if token.scopes.length is 1
continue if token.isOnlyWhitespace()
for scope in token.scopeDescriptor
for scope in token.scopes
return true if _.contains(scope.split('.'), 'comment')
break
false
@@ -226,7 +226,7 @@ class TokenizedLine
scopeStack = []
for token in @tokens
@updateScopeStack(scopeStack, token.scopeDescriptor)
@updateScopeStack(scopeStack, token.scopes)
_.last(scopeStack).children.push(token)
@scopeTree = scopeStack[0]