From 989bb861992f4bf5383842239a46897588e3e195 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 29 Jul 2015 11:27:24 -0700 Subject: [PATCH] Start work on custom scope -> filetypes configuration --- spec/grammars-spec.coffee | 5 +++ src/config-schema.coffee | 3 ++ src/grammar-registry.coffee | 87 ++++++++++++++++++++++++++++++++++++- src/tokenized-buffer.coffee | 4 +- 4 files changed, 96 insertions(+), 3 deletions(-) diff --git a/spec/grammars-spec.coffee b/spec/grammars-spec.coffee index cc975468f..ef48ec15b 100644 --- a/spec/grammars-spec.coffee +++ b/spec/grammars-spec.coffee @@ -110,6 +110,11 @@ 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 filetypes", -> + it "considers the custom filetypes as well as those defined in the grammar", -> + atom.config.set('core.fileTypesByScope', 'source.ruby': ['Cheffile']) + expect(atom.grammars.selectGrammar('build/Cheffile', 'cookbook "postgres"').scopeName).toBe 'source.ruby' + describe ".removeGrammar(grammar)", -> it "removes the grammar, so it won't be returned by selectGrammar", -> grammar = atom.grammars.selectGrammar('foo.js') diff --git a/src/config-schema.coffee b/src/config-schema.coffee index d46cdf589..6ffcfa632 100644 --- a/src/config-schema.coffee +++ b/src/config-schema.coffee @@ -26,6 +26,9 @@ module.exports = default: [] items: type: 'string' + fileTypesByScope: + type: 'object' + default: {} themes: type: 'array' default: ['one-dark-ui', 'one-dark-syntax'] diff --git a/src/grammar-registry.coffee b/src/grammar-registry.coffee index 7b1ef823f..6a84b8ddd 100644 --- a/src/grammar-registry.coffee +++ b/src/grammar-registry.coffee @@ -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,87 @@ 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.fileTypesByScope')?[grammar.scopeName] + fileTypes = fileTypes.concat(customFileTypes) + + for fileType in fileTypes + fileTypeComponents = fileType.toLowerCase().split(PathSplitRegex) + pathSuffix = pathComponents[-fileTypeComponents.length..-1] + if _.isEqual(pathSuffix, fileTypeComponents) + pathScore = Math.max(pathScore, fileType.length) + 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 = !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 diff --git a/src/tokenized-buffer.coffee b/src/tokenized-buffer.coffee index b4b381c63..ca525c34c 100644 --- a/src/tokenized-buffer.coffee +++ b/src/tokenized-buffer.coffee @@ -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()