From 20b54505f354791b387829807ef3d4c425764fb2 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 15 Jul 2016 11:46:00 -0700 Subject: [PATCH] Move logic for assigning grammars to editors into TextEditorRegistry --- spec/text-editor-registry-spec.js | 11 ++++-- src/atom-environment.coffee | 4 +- src/grammar-registry.coffee | 5 ++- src/text-editor-registry.js | 62 ++++++++++++++++++++++++++++++- src/tokenized-buffer.coffee | 28 +++----------- src/workspace.coffee | 1 + 6 files changed, 81 insertions(+), 30 deletions(-) diff --git a/spec/text-editor-registry-spec.js b/spec/text-editor-registry-spec.js index 9b93919f8..a6ec420db 100644 --- a/spec/text-editor-registry-spec.js +++ b/spec/text-editor-registry-spec.js @@ -10,7 +10,8 @@ describe('TextEditorRegistry', function () { beforeEach(function () { registry = new TextEditorRegistry({ - config: atom.config + config: atom.config, + grammarRegistry: atom.grammars }) editor = new TextEditor({ @@ -65,7 +66,9 @@ describe('TextEditorRegistry', function () { await atom.packages.activatePackage('language-javascript') await atom.packages.activatePackage('language-c') + editor.getBuffer().setPath('test.js') registry.maintainGrammar(editor) + expect(editor.getGrammar().name).toBe('JavaScript') editor.getBuffer().setPath('test.c') @@ -73,7 +76,9 @@ describe('TextEditorRegistry', function () { }) it('updates the editor\'s grammar when a more appropriate grammar is added for its path', async function () { - expect(editor.getGrammar().name).toBe('Null Grammar') + expect(editor.getGrammar()).toBe(null) + + editor.getBuffer().setPath('test.js') registry.maintainGrammar(editor) await atom.packages.activatePackage('language-javascript') expect(editor.getGrammar().name).toBe('JavaScript') @@ -95,7 +100,7 @@ describe('TextEditorRegistry', function () { registry.maintainConfig(editor) registry.maintainConfig(editor2) - expect(editor.getRootScopeDescriptor().getScopesArray()).toEqual(['text.plain.null-grammar']) + expect(editor.getRootScopeDescriptor().getScopesArray()).toEqual(['text.plain']) expect(editor2.getRootScopeDescriptor().getScopesArray()).toEqual(['source.js']) expect(editor.getEncoding()).toBe('utf8') diff --git a/src/atom-environment.coffee b/src/atom-environment.coffee index ad6b17f4d..b41eafd7d 100644 --- a/src/atom-environment.coffee +++ b/src/atom-environment.coffee @@ -189,7 +189,7 @@ class AtomEnvironment extends Model @commandInstaller = new CommandInstaller(@getVersion(), @applicationDelegate) - @textEditors = new TextEditorRegistry({@config}) + @textEditors = new TextEditorRegistry({@config, grammarRegistry: @grammars}) @workspace = new Workspace({ @config, @project, packageManager: @packages, grammarRegistry: @grammars, deserializerManager: @deserializers, @@ -330,6 +330,8 @@ class AtomEnvironment extends Model @grammars.clear() + @textEditors.clear() + @views.clear() @registerDefaultViewProviders() diff --git a/src/grammar-registry.coffee b/src/grammar-registry.coffee index 383d4d776..87fa9b9a3 100644 --- a/src/grammar-registry.coffee +++ b/src/grammar-registry.coffee @@ -29,6 +29,9 @@ class GrammarRegistry extends FirstMate.GrammarRegistry # # Returns a {Grammar}, never null. selectGrammar: (filePath, fileContents) -> + @selectGrammarWithScore(filePath, fileContents).grammar + + selectGrammarWithScore: (filePath, fileContents) -> bestMatch = null highestScore = -Infinity for grammar in @grammars @@ -36,7 +39,7 @@ class GrammarRegistry extends FirstMate.GrammarRegistry if score > highestScore or not bestMatch? bestMatch = grammar highestScore = score - bestMatch + {grammar: bestMatch, score: highestScore} # Extended: Returns a {Number} representing how well the grammar matches the # `filePath` and `contents`. diff --git a/src/text-editor-registry.js b/src/text-editor-registry.js index fd9332fcc..467b36d8c 100644 --- a/src/text-editor-registry.js +++ b/src/text-editor-registry.js @@ -22,6 +22,8 @@ const EDITOR_SETTER_NAMES_BY_SETTING_KEY = [ ['editor.nonWordCharacters', 'setNonWordCharacters'] ] +const GRAMMAR_SELECTION_RANGE = Range(Point.ZERO, Point(10, 0)).freeze() + // Experimental: This global registry tracks registered `TextEditors`. // // If you want to add functionality to a wider set of text editors than just @@ -34,14 +36,30 @@ const EDITOR_SETTER_NAMES_BY_SETTING_KEY = [ // done using your editor, be sure to call `dispose` on the returned disposable // to avoid leaking editors. export default class TextEditorRegistry { - constructor ({config}) { + constructor ({config, grammarRegistry}) { this.config = config + this.grammarRegistry = grammarRegistry + this.scopedSettingsDelegate = new ScopedSettingsDelegate(config) + this.grammarAddedOrUpdated = this.grammarAddedOrUpdated.bind(this) + this.clear() + } + + clear () { + if (this.subscriptions) { + this.subscriptions.dispose() + } + this.subscriptions = new CompositeDisposable() this.editors = new Set() this.emitter = new Emitter() this.scopesWithConfigSubscriptions = new Set() this.editorsWithMaintainedConfig = new Set() - this.scopedSettingsDelegate = new ScopedSettingsDelegate(config) + this.editorsWithMaintainedGrammar = new Set() + this.editorGrammarScores = new WeakMap() + this.subscriptions.add( + this.grammarRegistry.onDidAddGrammar(this.grammarAddedOrUpdated), + this.grammarRegistry.onDidUpdateGrammar(this.grammarAddedOrUpdated) + ) } destroy () { @@ -87,7 +105,24 @@ export default class TextEditorRegistry { } maintainGrammar (editor) { + this.editorsWithMaintainedGrammar.add(editor) + const assignGrammar = () => { + const {grammar, score} = this.grammarRegistry.selectGrammarWithScore( + editor.getPath(), + editor.getTextInBufferRange(GRAMMAR_SELECTION_RANGE) + ) + + if (!grammar) { + throw new Error(`No grammar found for path: ${editor.getPath()}`) + } + + editor.setGrammar(grammar) + this.editorGrammarScores.set(editor, score) + } + + assignGrammar() + this.subscriptions.add(editor.onDidChangePath(assignGrammar)) } maintainConfig (editor) { @@ -112,6 +147,29 @@ export default class TextEditorRegistry { this.subscriptions.add(editor.onDidTokenize(updateTabTypes)) } + // Private + + grammarAddedOrUpdated (grammar) { + this.editorsWithMaintainedGrammar.forEach(editor => { + if (grammar.injectionSelector != null) { + if (editor.tokenizedBuffer.hasTokenForSelector(grammar.injectionSelector)) { + editor.tokenizedBuffer.retokenizeLines() + } + } else { + const newScore = this.grammarRegistry.getGrammarScore( + grammar, + editor.getPath(), + editor.getTextInBufferRange(GRAMMAR_SELECTION_RANGE) + ) + let currentScore = this.editorGrammarScores.get(editor) + if (currentScore == null || newScore > currentScore) { + editor.setGrammar(grammar, newScore) + this.subscribeToSettingsForEditorScope(editor) + } + } + }) + } + subscribeToSettingsForEditorScope (editor) { const scopeDescriptor = editor.getRootScopeDescriptor() const scopeChain = scopeDescriptor.getScopeChain() diff --git a/src/tokenized-buffer.coffee b/src/tokenized-buffer.coffee index 79083a853..64b78ca3c 100644 --- a/src/tokenized-buffer.coffee +++ b/src/tokenized-buffer.coffee @@ -41,16 +41,13 @@ class TokenizedBuffer extends Model @disposables = new CompositeDisposable @tokenIterator = new TokenIterator({@grammarRegistry}) - @disposables.add @grammarRegistry.onDidAddGrammar(@grammarAddedOrUpdated) - @disposables.add @grammarRegistry.onDidUpdateGrammar(@grammarAddedOrUpdated) - @disposables.add @buffer.preemptDidChange (e) => @handleBufferChange(e) - @disposables.add @buffer.onDidChangePath (@bufferPath) => @reloadGrammar() + @rootScopeDescriptor = new ScopeDescriptor(scopes: ['text.plain']) if grammar = @grammarRegistry.grammarForScopeName(grammarScopeName) @setGrammar(grammar) else - @reloadGrammar() + @retokenizeLines() @grammarToRestoreScopeName = grammarScopeName destroyed: -> @@ -92,15 +89,6 @@ class TokenizedBuffer extends Model onDidTokenize: (callback) -> @emitter.on 'did-tokenize', callback - grammarAddedOrUpdated: (grammar) => - if @grammarToRestoreScopeName is grammar.scopeName - @setGrammar(grammar) - else if grammar.injectionSelector? - @retokenizeLines() if @hasTokenForSelector(grammar.injectionSelector) - else - newScore = @grammarRegistry.getGrammarScore(grammar, @buffer.getPath(), @getGrammarSelectionContent()) - @setGrammar(grammar, newScore) if newScore > @currentGrammarScore - setGrammar: (grammar, score) -> return unless grammar? and grammar isnt @grammar @@ -121,12 +109,6 @@ class TokenizedBuffer extends Model getGrammarSelectionContent: -> @buffer.getTextInRange([[0, 0], [10, 0]]) - reloadGrammar: -> - if grammar = @grammarRegistry.selectGrammar(@buffer.getPath(), @getGrammarSelectionContent()) - @setGrammar(grammar) - else - throw new Error("No grammar found for path: #{path}") - hasTokenForSelector: (selector) -> for tokenizedLine in @tokenizedLines when tokenizedLine? for token in tokenizedLine.tokens @@ -159,7 +141,7 @@ class TokenizedBuffer extends Model tokenizeNextChunk: -> # Short circuit null grammar which can just use the placeholder tokens - if @grammar is @grammarRegistry.nullGrammar and @firstInvalidRow()? + if (not @grammar? or @grammar.name is 'Null grammar') and @firstInvalidRow()? @invalidRows = [] @markTokenizationComplete() return @@ -235,7 +217,7 @@ class TokenizedBuffer extends Model @updateInvalidRows(start, end, delta) previousEndStack = @stackForRow(end) # used in spill detection below - if @largeFileMode + if @largeFileMode or not @grammar? newTokenizedLines = @buildPlaceholderTokenizedLinesForRows(start, end + delta) else newTokenizedLines = @buildTokenizedLinesForRows(start, end + delta, @stackForRow(start - 1), @openScopesForRow(start)) @@ -311,7 +293,7 @@ class TokenizedBuffer extends Model @buildPlaceholderTokenizedLineForRow(row) for row in [startRow..endRow] by 1 buildPlaceholderTokenizedLineForRow: (row) -> - openScopes = [@grammar.startIdForScope(@grammar.scopeName)] + openScopes = [] text = @buffer.lineForRow(row) tags = [text.length] lineEnding = @buffer.lineEndingForRow(row) diff --git a/src/workspace.coffee b/src/workspace.coffee index 0e0e04b31..ed3da8a51 100644 --- a/src/workspace.coffee +++ b/src/workspace.coffee @@ -577,6 +577,7 @@ class Workspace extends Model }, params) editor = new TextEditor(params) @textEditorRegistry.maintainConfig(editor) + @textEditorRegistry.maintainGrammar(editor) editor # Public: Asynchronously reopens the last-closed item's URI if it hasn't already been