mirror of
https://github.com/atom/atom.git
synced 2026-01-23 05:48:10 -05:00
Merge pull request #8144 from atom/mb-custom-extension-grammar-map
Allow custom association between file names and grammars
This commit is contained in:
@@ -108,7 +108,7 @@
|
||||
"open-on-github": "0.38.0",
|
||||
"package-generator": "0.40.0",
|
||||
"release-notes": "0.53.0",
|
||||
"settings-view": "0.213.0",
|
||||
"settings-view": "0.213.1",
|
||||
"snippets": "0.95.0",
|
||||
"spell-check": "0.59.0",
|
||||
"status-bar": "0.77.0",
|
||||
|
||||
@@ -1142,8 +1142,8 @@ describe "Config", ->
|
||||
type: 'integer'
|
||||
default: 12
|
||||
|
||||
expect(atom.config.getSchema('foo.baz')).toBeUndefined()
|
||||
expect(atom.config.getSchema('foo.bar.anInt.baz')).toBeUndefined()
|
||||
expect(atom.config.getSchema('foo.baz')).toEqual {type: 'any'}
|
||||
expect(atom.config.getSchema('foo.bar.anInt.baz')).toBe(null)
|
||||
|
||||
it "respects the schema for scoped settings", ->
|
||||
schema =
|
||||
@@ -1380,6 +1380,10 @@ describe "Config", ->
|
||||
expect(atom.config.set('foo.bar.aString', nope: 'nope')).toBe false
|
||||
expect(atom.config.get('foo.bar.aString')).toBe 'ok'
|
||||
|
||||
it 'does not allow setting children of that key-path', ->
|
||||
expect(atom.config.set('foo.bar.aString.something', 123)).toBe false
|
||||
expect(atom.config.get('foo.bar.aString')).toBe 'ok'
|
||||
|
||||
describe 'when the schema has a "maximumLength" key', ->
|
||||
it "trims the string to be no longer than the specified maximum", ->
|
||||
schema =
|
||||
@@ -1425,6 +1429,47 @@ describe "Config", ->
|
||||
expect(atom.config.get('foo.bar.anInt')).toEqual 12
|
||||
expect(atom.config.get('foo.bar.nestedObject.nestedBool')).toEqual true
|
||||
|
||||
describe "when the value has additionalProperties set to false", ->
|
||||
it 'does not allow other properties to be set on the object', ->
|
||||
atom.config.setSchema('foo.bar',
|
||||
type: 'object'
|
||||
properties:
|
||||
anInt:
|
||||
type: 'integer'
|
||||
default: 12
|
||||
additionalProperties: false
|
||||
)
|
||||
|
||||
expect(atom.config.set('foo.bar', {anInt: 5, somethingElse: 'ok'})).toBe true
|
||||
expect(atom.config.get('foo.bar.anInt')).toBe 5
|
||||
expect(atom.config.get('foo.bar.somethingElse')).toBeUndefined()
|
||||
|
||||
expect(atom.config.set('foo.bar.somethingElse', {anInt: 5})).toBe false
|
||||
expect(atom.config.get('foo.bar.somethingElse')).toBeUndefined()
|
||||
|
||||
describe 'when the value has an additionalProperties schema', ->
|
||||
it 'validates properties of the object against that schema', ->
|
||||
atom.config.setSchema('foo.bar',
|
||||
type: 'object'
|
||||
properties:
|
||||
anInt:
|
||||
type: 'integer'
|
||||
default: 12
|
||||
additionalProperties:
|
||||
type: 'string'
|
||||
)
|
||||
|
||||
expect(atom.config.set('foo.bar', {anInt: 5, somethingElse: 'ok'})).toBe true
|
||||
expect(atom.config.get('foo.bar.anInt')).toBe 5
|
||||
expect(atom.config.get('foo.bar.somethingElse')).toBe 'ok'
|
||||
|
||||
expect(atom.config.set('foo.bar.somethingElse', 7)).toBe false
|
||||
expect(atom.config.get('foo.bar.somethingElse')).toBe 'ok'
|
||||
|
||||
expect(atom.config.set('foo.bar', {anInt: 6, somethingElse: 7})).toBe true
|
||||
expect(atom.config.get('foo.bar.anInt')).toBe 6
|
||||
expect(atom.config.get('foo.bar.somethingElse')).toBe undefined
|
||||
|
||||
describe 'when the value has an "array" type', ->
|
||||
beforeEach ->
|
||||
schema =
|
||||
@@ -1438,6 +1483,11 @@ describe "Config", ->
|
||||
atom.config.set 'foo.bar', ['2', '3', '4']
|
||||
expect(atom.config.get('foo.bar')).toEqual [2, 3, 4]
|
||||
|
||||
it 'does not allow setting children of that key-path', ->
|
||||
expect(atom.config.set('foo.bar.child', 123)).toBe false
|
||||
expect(atom.config.set('foo.bar.child.grandchild', 123)).toBe false
|
||||
expect(atom.config.get('foo.bar')).toEqual [1, 2, 3]
|
||||
|
||||
describe 'when the value has a "color" type', ->
|
||||
beforeEach ->
|
||||
schema =
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
path = require 'path'
|
||||
fs = require 'fs-plus'
|
||||
temp = require 'temp'
|
||||
GrammarRegistry = require '../src/grammar-registry'
|
||||
|
||||
describe "the `grammars` global", ->
|
||||
beforeEach ->
|
||||
@@ -16,6 +17,9 @@ describe "the `grammars` global", ->
|
||||
waitsForPromise ->
|
||||
atom.packages.activatePackage('language-ruby')
|
||||
|
||||
waitsForPromise ->
|
||||
atom.packages.activatePackage('language-git')
|
||||
|
||||
afterEach ->
|
||||
atom.packages.deactivatePackages()
|
||||
atom.packages.unloadPackages()
|
||||
@@ -30,6 +34,30 @@ describe "the `grammars` global", ->
|
||||
expect(grammars2.selectGrammar(filePath).name).toBe 'Ruby'
|
||||
|
||||
describe ".selectGrammar(filePath)", ->
|
||||
it "always returns a grammar", ->
|
||||
registry = new GrammarRegistry()
|
||||
expect(registry.selectGrammar().scopeName).toBe 'text.plain.null-grammar'
|
||||
|
||||
it "selects the text.plain grammar over the null grammar", ->
|
||||
expect(atom.grammars.selectGrammar('test.txt').scopeName).toBe 'text.plain'
|
||||
|
||||
it "selects a grammar based on the file path case insensitively", ->
|
||||
expect(atom.grammars.selectGrammar('/tmp/source.coffee').scopeName).toBe 'source.coffee'
|
||||
expect(atom.grammars.selectGrammar('/tmp/source.COFFEE').scopeName).toBe 'source.coffee'
|
||||
|
||||
describe "on Windows", ->
|
||||
originalPlatform = null
|
||||
|
||||
beforeEach ->
|
||||
originalPlatform = process.platform
|
||||
Object.defineProperty process, 'platform', value: 'win32'
|
||||
|
||||
afterEach ->
|
||||
Object.defineProperty process, 'platform', value: originalPlatform
|
||||
|
||||
it "normalizes back slashes to forward slashes when matching the fileTypes", ->
|
||||
expect(atom.grammars.selectGrammar('something\\.git\\config').scopeName).toBe 'source.git-config'
|
||||
|
||||
it "can use the filePath to load the correct grammar based on the grammar's filetype", ->
|
||||
waitsForPromise ->
|
||||
atom.packages.activatePackage('language-git')
|
||||
@@ -110,6 +138,23 @@ describe "the `grammars` global", ->
|
||||
expect(-> atom.grammars.selectGrammar(null, '')).not.toThrow()
|
||||
expect(-> atom.grammars.selectGrammar(null, null)).not.toThrow()
|
||||
|
||||
describe "when the user has custom grammar file types", ->
|
||||
it "considers the custom file types as well as those defined in the grammar", ->
|
||||
atom.config.set('core.customFileTypes', 'source.ruby': ['Cheffile'])
|
||||
expect(atom.grammars.selectGrammar('build/Cheffile', 'cookbook "postgres"').scopeName).toBe 'source.ruby'
|
||||
|
||||
it "favors user-defined file types over built-in ones of equal length", ->
|
||||
atom.config.set('core.customFileTypes',
|
||||
'source.coffee': ['Rakefile'],
|
||||
'source.ruby': ['Cakefile']
|
||||
)
|
||||
expect(atom.grammars.selectGrammar('Rakefile', '').scopeName).toBe 'source.coffee'
|
||||
expect(atom.grammars.selectGrammar('Cakefile', '').scopeName).toBe 'source.ruby'
|
||||
|
||||
it "favors grammars with matching first-line-regexps even if custom file types match the file", ->
|
||||
atom.config.set('core.customFileTypes', 'source.ruby': ['bootstrap'])
|
||||
expect(atom.grammars.selectGrammar('bootstrap', '#!/usr/bin/env node').scopeName).toBe 'source.js'
|
||||
|
||||
describe ".removeGrammar(grammar)", ->
|
||||
it "removes the grammar, so it won't be returned by selectGrammar", ->
|
||||
grammar = atom.grammars.selectGrammar('foo.js')
|
||||
|
||||
@@ -26,6 +26,14 @@ module.exports =
|
||||
default: []
|
||||
items:
|
||||
type: 'string'
|
||||
customFileTypes:
|
||||
type: 'object'
|
||||
default: {}
|
||||
description: 'Associates scope names (e.g. "source.js") with arrays of file extensions and file names (e.g. ["Somefile", ".js2"])'
|
||||
additionalProperties:
|
||||
type: 'array'
|
||||
items:
|
||||
type: 'string'
|
||||
themes:
|
||||
type: 'array'
|
||||
default: ['one-dark-ui', 'one-dark-syntax']
|
||||
|
||||
@@ -664,13 +664,24 @@ class Config
|
||||
# * `keyPath` The {String} name of the key.
|
||||
#
|
||||
# Returns an {Object} eg. `{type: 'integer', default: 23, minimum: 1}`.
|
||||
# Returns `null` when the keyPath has no schema specified.
|
||||
# Returns `null` when the keyPath has no schema specified, but is accessible
|
||||
# from the root schema.
|
||||
getSchema: (keyPath) ->
|
||||
keys = splitKeyPath(keyPath)
|
||||
schema = @schema
|
||||
for key in keys
|
||||
break unless schema?
|
||||
schema = schema.properties?[key]
|
||||
if schema.type is 'object'
|
||||
childSchema = schema.properties?[key]
|
||||
unless childSchema?
|
||||
if isPlainObject(schema.additionalProperties)
|
||||
childSchema = schema.additionalProperties
|
||||
else if schema.additionalProperties is false
|
||||
return null
|
||||
else
|
||||
return {type: 'any'}
|
||||
else
|
||||
return null
|
||||
schema = childSchema
|
||||
schema
|
||||
|
||||
# Extended: Get the {String} path to the config file being used.
|
||||
@@ -948,8 +959,9 @@ class Config
|
||||
catch e
|
||||
undefined
|
||||
else
|
||||
value = @constructor.executeSchemaEnforcers(keyPath, value, schema) if schema = @getSchema(keyPath)
|
||||
value
|
||||
unless (schema = @getSchema(keyPath))?
|
||||
throw new Error("Illegal key path #{keyPath}") if schema is false
|
||||
@constructor.executeSchemaEnforcers(keyPath, value, schema)
|
||||
|
||||
# When the schema is changed / added, there may be values set in the config
|
||||
# that do not conform to the schema. This will reset make them conform.
|
||||
@@ -1027,6 +1039,10 @@ class Config
|
||||
# order of specification. Then the `*` enforcers will be run, in order of
|
||||
# specification.
|
||||
Config.addSchemaEnforcers
|
||||
'any':
|
||||
coerce: (keyPath, value, schema) ->
|
||||
value
|
||||
|
||||
'integer':
|
||||
coerce: (keyPath, value, schema) ->
|
||||
value = parseInt(value)
|
||||
@@ -1077,17 +1093,26 @@ Config.addSchemaEnforcers
|
||||
throw new Error("Validation failed at #{keyPath}, #{JSON.stringify(value)} must be an object") unless isPlainObject(value)
|
||||
return value unless schema.properties?
|
||||
|
||||
defaultChildSchema = null
|
||||
allowsAdditionalProperties = true
|
||||
if isPlainObject(schema.additionalProperties)
|
||||
defaultChildSchema = schema.additionalProperties
|
||||
if schema.additionalProperties is false
|
||||
allowsAdditionalProperties = false
|
||||
|
||||
newValue = {}
|
||||
for prop, propValue of value
|
||||
childSchema = schema.properties[prop]
|
||||
childSchema = schema.properties[prop] ? defaultChildSchema
|
||||
if childSchema?
|
||||
try
|
||||
newValue[prop] = @executeSchemaEnforcers("#{keyPath}.#{prop}", propValue, childSchema)
|
||||
catch error
|
||||
console.warn "Error setting item in object: #{error.message}"
|
||||
else
|
||||
else if allowsAdditionalProperties
|
||||
# Just pass through un-schema'd values
|
||||
newValue[prop] = propValue
|
||||
else
|
||||
console.warn "Illegal object key: #{keyPath}.#{prop}"
|
||||
|
||||
newValue
|
||||
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
_ = require 'underscore-plus'
|
||||
{Emitter} = require 'event-kit'
|
||||
{includeDeprecatedAPIs, deprecate} = require 'grim'
|
||||
FirstMate = require 'first-mate'
|
||||
Token = require './token'
|
||||
fs = require 'fs-plus'
|
||||
|
||||
PathSplitRegex = new RegExp("[/.]")
|
||||
|
||||
# Extended: Syntax class holding the grammars used for tokenizing.
|
||||
#
|
||||
@@ -39,7 +43,7 @@ class GrammarRegistry extends FirstMate.GrammarRegistry
|
||||
bestMatch = null
|
||||
highestScore = -Infinity
|
||||
for grammar in @grammars
|
||||
score = grammar.getScore(filePath, fileContents)
|
||||
score = @getGrammarScore(grammar, filePath, fileContents)
|
||||
if score > highestScore or not bestMatch?
|
||||
bestMatch = grammar
|
||||
highestScore = score
|
||||
@@ -47,6 +51,90 @@ class GrammarRegistry extends FirstMate.GrammarRegistry
|
||||
bestMatch = grammar unless grammar.bundledPackage
|
||||
bestMatch
|
||||
|
||||
# Extended: Returns a {Number} representing how well the grammar matches the
|
||||
# `filePath` and `contents`.
|
||||
getGrammarScore: (grammar, filePath, contents) ->
|
||||
contents = fs.readFileSync(filePath, 'utf8') if not contents? and fs.isFileSync(filePath)
|
||||
|
||||
if @grammarOverrideForPath(filePath) is grammar.scopeName
|
||||
2 + (filePath?.length ? 0)
|
||||
else if @grammarMatchesContents(grammar, contents)
|
||||
1 + (filePath?.length ? 0)
|
||||
else
|
||||
@getGrammarPathScore(grammar, filePath)
|
||||
|
||||
getGrammarPathScore: (grammar, filePath) ->
|
||||
return -1 unless filePath
|
||||
filePath = filePath.replace(/\\/g, '/') if process.platform is 'win32'
|
||||
|
||||
pathComponents = filePath.toLowerCase().split(PathSplitRegex)
|
||||
pathScore = -1
|
||||
|
||||
fileTypes = grammar.fileTypes
|
||||
if customFileTypes = atom.config.get('core.customFileTypes')?[grammar.scopeName]
|
||||
fileTypes = fileTypes.concat(customFileTypes)
|
||||
|
||||
for fileType, i in fileTypes
|
||||
fileTypeComponents = fileType.toLowerCase().split(PathSplitRegex)
|
||||
pathSuffix = pathComponents[-fileTypeComponents.length..-1]
|
||||
if _.isEqual(pathSuffix, fileTypeComponents)
|
||||
pathScore = Math.max(pathScore, fileType.length)
|
||||
if i >= grammar.fileTypes.length
|
||||
pathScore += 0.5
|
||||
|
||||
pathScore
|
||||
|
||||
grammarMatchesContents: (grammar, contents) ->
|
||||
return false unless contents? and grammar.firstLineRegex?
|
||||
|
||||
escaped = false
|
||||
numberOfNewlinesInRegex = 0
|
||||
for character in grammar.firstLineRegex.source
|
||||
switch character
|
||||
when '\\'
|
||||
escaped = not escaped
|
||||
when 'n'
|
||||
numberOfNewlinesInRegex++ if escaped
|
||||
escaped = false
|
||||
else
|
||||
escaped = false
|
||||
lines = contents.split('\n')
|
||||
grammar.firstLineRegex.testSync(lines[0..numberOfNewlinesInRegex].join('\n'))
|
||||
|
||||
# Public: Get the grammar override for the given file path.
|
||||
#
|
||||
# * `filePath` A {String} file path.
|
||||
#
|
||||
# Returns a {Grammar} or undefined.
|
||||
grammarOverrideForPath: (filePath) ->
|
||||
@grammarOverridesByPath[filePath]
|
||||
|
||||
# Public: Set the grammar override for the given file path.
|
||||
#
|
||||
# * `filePath` A non-empty {String} file path.
|
||||
# * `scopeName` A {String} such as `"source.js"`.
|
||||
#
|
||||
# Returns a {Grammar} or undefined.
|
||||
setGrammarOverrideForPath: (filePath, scopeName) ->
|
||||
if filePath
|
||||
@grammarOverridesByPath[filePath] = scopeName
|
||||
|
||||
# Public: Remove the grammar override for the given file path.
|
||||
#
|
||||
# * `filePath` A {String} file path.
|
||||
#
|
||||
# Returns undefined.
|
||||
clearGrammarOverrideForPath: (filePath) ->
|
||||
delete @grammarOverridesByPath[filePath]
|
||||
undefined
|
||||
|
||||
# Public: Remove all grammar overrides.
|
||||
#
|
||||
# Returns undefined.
|
||||
clearGrammarOverrides: ->
|
||||
@grammarOverridesByPath = {}
|
||||
undefined
|
||||
|
||||
clearObservers: ->
|
||||
@off() if includeDeprecatedAPIs
|
||||
@emitter = new Emitter
|
||||
|
||||
@@ -68,7 +68,7 @@ class TokenizedBuffer extends Model
|
||||
if grammar.injectionSelector?
|
||||
@retokenizeLines() if @hasTokenForSelector(grammar.injectionSelector)
|
||||
else
|
||||
newScore = grammar.getScore(@buffer.getPath(), @getGrammarSelectionContent())
|
||||
newScore = atom.grammars.getGrammarScore(grammar, @buffer.getPath(), @getGrammarSelectionContent())
|
||||
@setGrammar(grammar, newScore) if newScore > @currentGrammarScore
|
||||
|
||||
setGrammar: (grammar, score) ->
|
||||
@@ -76,7 +76,7 @@ class TokenizedBuffer extends Model
|
||||
|
||||
@grammar = grammar
|
||||
@rootScopeDescriptor = new ScopeDescriptor(scopes: [@grammar.scopeName])
|
||||
@currentGrammarScore = score ? grammar.getScore(@buffer.getPath(), @getGrammarSelectionContent())
|
||||
@currentGrammarScore = score ? atom.grammars.getGrammarScore(grammar, @buffer.getPath(), @getGrammarSelectionContent())
|
||||
|
||||
@grammarUpdateDisposable?.dispose()
|
||||
@grammarUpdateDisposable = @grammar.onDidUpdate => @retokenizeLines()
|
||||
|
||||
Reference in New Issue
Block a user