diff --git a/lib/corrections-view.coffee b/lib/corrections-view.coffee index 8e5a30cb..5f7c29b7 100644 --- a/lib/corrections-view.coffee +++ b/lib/corrections-view.coffee @@ -24,7 +24,7 @@ class CorrectionsView extends SelectListView @cancel() return unless correction @editor.transact => - @editor.selectMarker(@marker) + @editor.setSelectedBufferRange(@marker.getRange()) @editor.insertText(correction) cancelled: -> diff --git a/lib/main.coffee b/lib/main.coffee index 29c6f302..a8d484a1 100644 --- a/lib/main.coffee +++ b/lib/main.coffee @@ -14,11 +14,13 @@ module.exports = description: 'List of scopes for languages which will be checked for misspellings. See [the README](https://github.com/atom/spell-check#spell-check-package-) for more information on finding the correct scope for a specific language.' activate: -> - @disposable = atom.workspace.observeTextEditors(addViewToEditor) + @viewsByEditor = new WeakMap + @disposable = atom.workspace.observeTextEditors (editor) => + SpellCheckView ?= require './spell-check-view' + @viewsByEditor.set(editor, new SpellCheckView(editor)) + + misspellingMarkersForEditor: (editor) -> + @viewsByEditor.get(editor).markerLayer.getMarkers() deactivate: -> @disposable.dispose() - -addViewToEditor = (editor) -> - SpellCheckView ?= require './spell-check-view' - new SpellCheckView(editor) diff --git a/lib/misspelling-view.coffee b/lib/misspelling-view.coffee deleted file mode 100644 index 9d354c2b..00000000 --- a/lib/misspelling-view.coffee +++ /dev/null @@ -1,44 +0,0 @@ -CorrectionsView = null - -module.exports = -class MisspellingView - constructor: (bufferRange, @editor) -> - @createMarker(bufferRange) - @correctMispellingCommand = atom.commands.add atom.views.getView(@editor), 'spell-check:correct-misspelling', => - if @containsCursor() - CorrectionsView ?= require './corrections-view' - @correctionsView?.destroy() - @correctionsView = new CorrectionsView(@editor, @getCorrections(), @marker) - - createMarker: (bufferRange) -> - @marker = @editor.markBufferRange(bufferRange, - invalidate: 'touch', - replicate: false, - persistent: false, - maintainHistory: false - ) - @editor.decorateMarker(@marker, - type: 'highlight', - class: 'spell-check-misspelling', - deprecatedRegionClass: 'misspelling' - ) - - getCorrections: -> - screenRange = @marker.getScreenRange() - misspelling = @editor.getTextInRange(@editor.bufferRangeForScreenRange(screenRange)) - SpellChecker = require 'spellchecker' - corrections = SpellChecker.getCorrectionsForMisspelling(misspelling) - - containsCursor: -> - cursor = @editor.getCursorScreenPosition() - @marker.getScreenRange().containsPoint(cursor, false) - - destroy: -> - @correctMispellingCommand?.dispose() - @correctMispellingCommand = null - - @correctionsView?.remove() - @correctionsView = null - - @marker?.destroy() - @marker = null diff --git a/lib/spell-check-handler.coffee b/lib/spell-check-handler.coffee index 2d64aa3c..b44054d1 100644 --- a/lib/spell-check-handler.coffee +++ b/lib/spell-check-handler.coffee @@ -1,18 +1,32 @@ SpellChecker = require 'spellchecker' -wordRegex = /(?:^|[\s\[\]"'])([a-zA-Z]+([a-zA-Z']+[a-zA-Z])?)(?=[\s\.\[\]:,"']|$)/g - module.exports = ({id, text}) -> + SpellChecker.add("GitHub") + SpellChecker.add("github") + + misspelledCharacterRanges = SpellChecker.checkSpelling(text) + row = 0 + rangeIndex = 0 + characterIndex = 0 misspellings = [] - for line in text.split('\n') - while matches = wordRegex.exec(line) - word = matches[1] - continue if word in ['GitHub', 'github'] - continue unless SpellChecker.isMisspelled(word) + while characterIndex < text.length and rangeIndex < misspelledCharacterRanges.length + lineBreakIndex = text.indexOf('\n', characterIndex) + if lineBreakIndex is -1 + lineBreakIndex = Infinity - startColumn = matches.index + matches[0].length - word.length - endColumn = startColumn + word.length - misspellings.push([[row, startColumn], [row, endColumn]]) + loop + range = misspelledCharacterRanges[rangeIndex] + if range and range.start < lineBreakIndex + misspellings.push([ + [row, range.start - characterIndex], + [row, range.end - characterIndex] + ]) + rangeIndex++ + else + break + + characterIndex = lineBreakIndex + 1 row++ + {id, misspellings} diff --git a/lib/spell-check-view.coffee b/lib/spell-check-view.coffee index b6a882e6..9663db3d 100644 --- a/lib/spell-check-view.coffee +++ b/lib/spell-check-view.coffee @@ -1,8 +1,10 @@ _ = require 'underscore-plus' {CompositeDisposable} = require 'atom' -MisspellingView = require './misspelling-view' SpellCheckTask = require './spell-check-task' +CorrectionsView = null +SpellChecker = null + module.exports = class SpellCheckView @content: -> @@ -10,12 +12,18 @@ class SpellCheckView constructor: (@editor) -> @disposables = new CompositeDisposable - @views = [] @task = new SpellCheckTask() + @initializeMarkerLayer() + + @correctMisspellingCommand = atom.commands.add atom.views.getView(@editor), 'spell-check:correct-misspelling', => + if marker = @markerLayer.findMarkers({containsPoint: @editor.getCursorBufferPosition()})[0] + CorrectionsView ?= require './corrections-view' + @correctionsView?.destroy() + @correctionsView = new CorrectionsView(@editor, @getCorrections(marker), marker) @task.onDidSpellCheck (misspellings) => - @destroyViews() - @addViews(misspellings) if @buffer? + @detroyMarkers() + @addMarkers(misspellings) if @buffer? @disposables.add @editor.onDidChangePath => @subscribeToBuffer() @@ -33,13 +41,25 @@ class SpellCheckView @disposables.add @editor.onDidDestroy(@destroy.bind(this)) + initializeMarkerLayer: -> + @markerLayer = @editor.getBuffer().addMarkerLayer() + @markerLayerDecoration = @editor.decorateMarkerLayer(@markerLayer, { + type: 'highlight', + class: 'spell-check-misspelling', + deprecatedRegionClass: 'misspelling' + }) + destroy: -> @unsubscribeFromBuffer() @disposables.dispose() @task.terminate() + @markerLayer.destroy() + @markerLayerDecoration.destroy() + @correctMisspellingCommand.dispose() + @correctionsView?.remove() unsubscribeFromBuffer: -> - @destroyViews() + @detroyMarkers() if @buffer? @bufferDisposable.dispose() @@ -57,14 +77,19 @@ class SpellCheckView grammar = @editor.getGrammar().scopeName _.contains(atom.config.get('spell-check.grammars'), grammar) - destroyViews: -> - while view = @views.shift() - view.destroy() + detroyMarkers: -> + @markerLayer.destroy() + @markerLayerDecoration.destroy() + @initializeMarkerLayer() - addViews: (misspellings) -> + addMarkers: (misspellings) -> for misspelling in misspellings - view = new MisspellingView(misspelling, @editor) - @views.push(view) + @markerLayer.markRange(misspelling, + invalidate: 'touch', + replicate: 'false', + persistent: false, + maintainHistory: false, + ) updateMisspellings: -> # Task::start can throw errors atom/atom#3326 @@ -72,3 +97,8 @@ class SpellCheckView @task.start(@buffer.getText()) catch error console.warn('Error starting spell check task', error.stack ? error) + + getCorrections: (marker) -> + SpellChecker ?= require 'spellchecker' + misspelling = @editor.getTextInBufferRange(marker.getRange()) + corrections = SpellChecker.getCorrectionsForMisspelling(misspelling) diff --git a/package.json b/package.json index 9a94f7bb..b064cbdf 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "description": "Highlights misspelled words and shows possible corrections.", "dependencies": { "atom-space-pen-views": "^2.0.0", - "spellchecker": "^3.1.2", + "spellchecker": "3.2.0", "underscore-plus": "^1" }, "repository": "https://github.com/atom/spell-check", diff --git a/script/benchmark.coffee b/script/benchmark.coffee new file mode 100755 index 00000000..f16fe537 --- /dev/null +++ b/script/benchmark.coffee @@ -0,0 +1,15 @@ +#!/usr/bin/env coffee + +handler = require '../lib/spell-check-handler' +fs = require 'fs' + +pathToCheck = process.argv[2] +console.log("Spellchecking %s...", pathToCheck) + +text = fs.readFileSync(pathToCheck, 'utf8') + +t0 = Date.now() +result = handler({id: 1, text}) +t1 = Date.now() + +console.log("Found %d misspellings in %d milliseconds", result.misspellings.length, t1 - t0) diff --git a/spec/spell-check-spec.coffee b/spec/spell-check-spec.coffee index ad367385..0ce215e7 100644 --- a/spec/spell-check-spec.coffee +++ b/spec/spell-check-spec.coffee @@ -1,5 +1,11 @@ describe "Spell check", -> - [workspaceElement, editor, editorElement] = [] + [workspaceElement, editor, editorElement, spellCheckModule] = [] + + textForMarker = (marker) -> + editor.getTextInBufferRange(marker.getRange()) + + getMisspellingMarkers = -> + spellCheckModule.misspellingMarkersForEditor(editor) beforeEach -> workspaceElement = atom.views.getView(atom.workspace) @@ -17,7 +23,8 @@ describe "Spell check", -> atom.workspace.open('sample.js') waitsForPromise -> - atom.packages.activatePackage('spell-check') + atom.packages.activatePackage('spell-check').then ({mainModule}) -> + spellCheckModule = mainModule runs -> jasmine.attachToDOM(workspaceElement) @@ -25,40 +32,54 @@ describe "Spell check", -> editorElement = atom.views.getView(editor) it "decorates all misspelled words", -> - editor.setText("This middle of thiss sentencts has issues and the \"edn\" 'dsoe' too") + editor.setText("This middle of thiss\nsentencts\n\nhas issues and the \"edn\" 'dsoe' too") atom.config.set('spell-check.grammars', ['source.js']) - decorations = null + misspellingMarkers = null + waitsFor -> + misspellingMarkers = getMisspellingMarkers() + misspellingMarkers.length > 0 + + runs -> + expect(misspellingMarkers.length).toBe 4 + expect(textForMarker(misspellingMarkers[0])).toEqual "thiss" + expect(textForMarker(misspellingMarkers[1])).toEqual "sentencts" + expect(textForMarker(misspellingMarkers[2])).toEqual "edn" + expect(textForMarker(misspellingMarkers[3])).toEqual "dsoe" + + it "doesn't consider our company's name to be a spelling error", -> + editor.setText("GitHub (aka github): Where codez are built.") + atom.config.set('spell-check.grammars', ['source.js']) + misspellingMarkers = null waitsFor -> - decorations = editor.getHighlightDecorations(class: 'spell-check-misspelling') - decorations.length > 0 + misspellingMarkers = getMisspellingMarkers() + misspellingMarkers.length > 0 runs -> - expect(decorations.length).toBe 4 - expect(decorations[0].marker.getBufferRange()).toEqual [[0, 15], [0, 20]] - expect(decorations[1].marker.getBufferRange()).toEqual [[0, 21], [0, 30]] - expect(decorations[2].marker.getBufferRange()).toEqual [[0, 51], [0, 54]] - expect(decorations[3].marker.getBufferRange()).toEqual [[0, 57], [0, 61]] + expect(misspellingMarkers.length).toBe 1 + expect(textForMarker(misspellingMarkers[0])).toBe "codez" it "hides decorations when a misspelled word is edited", -> editor.setText('notaword') advanceClock(editor.getBuffer().getStoppedChangingDelay()) atom.config.set('spell-check.grammars', ['source.js']) - decorations = null + misspellingMarkers = null waitsFor -> - decorations = editor.getHighlightDecorations(class: 'spell-check-misspelling') - decorations.length > 0 + misspellingMarkers = getMisspellingMarkers() + misspellingMarkers.length > 0 runs -> - expect(decorations.length).toBe 1 + expect(misspellingMarkers.length).toBe 1 editor.moveToEndOfLine() editor.insertText('a') advanceClock(editor.getBuffer().getStoppedChangingDelay()) - decorations = editor.getHighlightDecorations(class: 'spell-check-misspelling') - expect(decorations.length).toBe 1 - expect(decorations[0].marker.isValid()).toBe false + + misspellingMarkers = getMisspellingMarkers() + + expect(misspellingMarkers.length).toBe 1 + expect(misspellingMarkers[0].isValid()).toBe false describe "when spell checking for a grammar is removed", -> it "removes all the misspellings", -> @@ -66,14 +87,15 @@ describe "Spell check", -> advanceClock(editor.getBuffer().getStoppedChangingDelay()) atom.config.set('spell-check.grammars', ['source.js']) - decorations = null + misspellingMarkers = null waitsFor -> - editor.getHighlightDecorations(class: 'spell-check-misspelling').length > 0 + misspellingMarkers = getMisspellingMarkers() + misspellingMarkers.length > 0 runs -> - expect(editor.getHighlightDecorations(class: 'spell-check-misspelling').length).toBe 1 + expect(getMisspellingMarkers().length).toBe 1 atom.config.set('spell-check.grammars', []) - expect(editor.getHighlightDecorations(class: 'spell-check-misspelling').length).toBe 0 + expect(getMisspellingMarkers().length).toBe 0 describe "when the editor's grammar changes to one that does not have spell check enabled", -> it "removes all the misspellings", -> @@ -82,12 +104,12 @@ describe "Spell check", -> atom.config.set('spell-check.grammars', ['source.js']) waitsFor -> - editor.getHighlightDecorations(class: 'spell-check-misspelling').length > 0 + getMisspellingMarkers().length > 0 runs -> - expect(editor.getHighlightDecorations(class: 'spell-check-misspelling').length).toBe 1 + expect(getMisspellingMarkers().length).toBe 1 editor.setGrammar(atom.grammars.selectGrammar('.txt')) - expect(editor.getHighlightDecorations(class: 'spell-check-misspelling').length).toBe 0 + expect(getMisspellingMarkers().length).toBe 0 describe "when 'spell-check:correct-misspelling' is triggered on the editor", -> describe "when the cursor touches a misspelling that has corrections", -> @@ -97,9 +119,11 @@ describe "Spell check", -> atom.config.set('spell-check.grammars', ['source.js']) waitsFor -> - editor.getHighlightDecorations(class: 'spell-check-misspelling').length > 0 + getMisspellingMarkers().length is 1 runs -> + expect(getMisspellingMarkers()[0].isValid()).toBe true + atom.commands.dispatch editorElement, 'spell-check:correct-misspelling' correctionsElement = editorElement.querySelector('.corrections') @@ -111,8 +135,8 @@ describe "Spell check", -> expect(editor.getText()).toBe 'together' expect(editor.getCursorBufferPosition()).toEqual [0, 8] - advanceClock(editor.getBuffer().getStoppedChangingDelay()) - expect(editorElement.querySelectorAll('.spell-check-misspelling').length).toBe 0 + + expect(getMisspellingMarkers()[0].isValid()).toBe false expect(editorElement.querySelector('.corrections')).toBeNull() describe "when the cursor touches a misspelling that has no corrections", -> @@ -122,7 +146,7 @@ describe "Spell check", -> atom.config.set('spell-check.grammars', ['source.js']) waitsFor -> - editor.getHighlightDecorations(class: 'spell-check-misspelling').length > 0 + getMisspellingMarkers().length > 0 runs -> atom.commands.dispatch editorElement, 'spell-check:correct-misspelling' @@ -136,8 +160,8 @@ describe "Spell check", -> atom.config.set('spell-check.grammars', ['source.js']) waitsFor -> - editor.getHighlightDecorations(class: 'spell-check-misspelling').length > 0 + getMisspellingMarkers().length > 0 runs -> editor.destroy() - expect(editor.getMarkers().length).toBe 0 + expect(getMisspellingMarkers().length).toBe 0