diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..e98111f9 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +# EditorConfig is awesome: http://EditorConfig.org + +# top-most EditorConfig file +root = true + +[*] +indent_size = 2 +indent_style = space +insert_final_newline = true +max_line_length = 80 +tab_width = 2 +trim_trailing_whitespace = true + +[*.{js,ts,coffee}] +quote_type = single diff --git a/.gitignore b/.gitignore index fd4f2b06..84f486e3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ +*~ +npm-debug.log node_modules .DS_Store diff --git a/PLUGINS.md b/PLUGINS.md new file mode 100644 index 00000000..57480b26 --- /dev/null +++ b/PLUGINS.md @@ -0,0 +1,65 @@ +# Plugins + +The `spell-check` allows for additional dictionaries to be used at the same time using Atom's `providedServices` element in the `package.json` file. + + "providedServices": { + "spell-check": { + "versions": { + "1.0.0": "nameOfFunctionToProvideSpellCheck" + } + } + } + +The `nameOfFunctionToProvideSpellCheck` function may return either a single `require`able path or an array of them. This must be an absolute path to a class that provides a checker instance (below). + + provideSpellCheck: -> + require.resolve './project-checker.coffee' + +The path given must resolve to a singleton instance of a class. + + class ProjectChecker + # Magical code + checker = new ProjectChecker() + module.exports = checker + +See the `spell-check-project` for an example implementation. + +# Checker + +A common parameter type is `checkArgs`, this is a hash with the following signature. + + args = { + projectPath: "/absolute/path/to/project/root, + relativePath: "relative/path/from/project/root" + } + +Below the required methods for the checker instance. + +* getId(): string + * This returns the canonical identifier for this plugin. Typically, this will be the package name with an optional suffix for options, such as `spell-check-project` or `spell-check:en-US`. This identifier will be used for some control plugins (such as `spell-check-project`) to enable or disable the plugin. + * This will also used to pass information from the Atom process into the background task once that is implemented. +* getPriority(): number + * Determines how significant the plugin is for information with lower numbers being more important. Typically, user-entered data (such as the config `knownWords` configuration or a project's dictionary) will be lower than system data (priority 100). +* isEnabled(): boolean + * If this returns true, then the plugin will considered for processing. +* providesSpelling(checkArgs): boolean + * If this returns true, then the plugin will be included when looking for incorrect and correct words via the `check` function. +* check(checkArgs, text: string): [results] + * This takes the entire text buffer and will be called once per buffer. + * The output is an array with three parameters, all optional: `{ invertIncorrectAsCorrect: true, incorrect: [ranges], correct: [ranges] }` + * The ranges are a zero-based index of a start and stop character (`[1, 23]`). + * `invertIncorrectAsCorrect` means take the incorrect range and assume everything not in this list is correct. + * Correct words always take precedence, even if another checker indicates a word is incorrect. + * If a word or character is neither correct or incorrect, it is considered correct. +* providesSuggestions(checkArgs): boolean + * If this returns true, then the plugin will be included when querying for suggested words via the `suggest` function. +* suggest(checkArgs, word: string): [suggestion: string] + * Returns a list of suggestions for a given word ordered so the most important is at the beginning of the list. +* providesAdding(checkArgs): boolean + * If this returns true, then the dictionary allows a word to be added to the dictionary. +* getAddingTargets(checkArgs): [target] + * Gets a list of targets to show to the user. + * The `target` object has a minimum signature of `{ label: stringToShowTheUser }`. For example, `{ label: "Ignore word (case-sensitive)" }`. + * This is a list to allow plugins to have multiple options, such as adding it as a case-sensitive or insensitive, temporary verses configuration, etc. +* add(buffer, target, word) + * Adds a word to the dictionary, using the target for identifying which one is used. diff --git a/README.md b/README.md index ea59deac..ea554138 100644 --- a/README.md +++ b/README.md @@ -19,4 +19,10 @@ To enable _Spell Check_ for your current file type: put your cursor in the file, ## Changing the dictionary -Currently, only the English (US) dictionary is supported. Follow [this issue](https://github.com/atom/spell-check/issues/11) for updates. +To change the language of the dictionary, set the "Locales" configuration option to the IEFT tag (en-US, fr-FR, etc). More than one language can be used, simply separate them by commas. + +For Windows 8 and 10, you must install the language using the regional settings before the language can be chosen inside Atom. + +## Plugins + +_Spell Check_ allows for plugins to provide additional spell checking functionality. See the `PLUGINS.md` file in the repository on how to write a plugin. diff --git a/lib/corrections-view.coffee b/lib/corrections-view.coffee index 80d542a4..068a303f 100644 --- a/lib/corrections-view.coffee +++ b/lib/corrections-view.coffee @@ -3,9 +3,9 @@ module.exports = class CorrectionsView extends SelectListView - initialize: (@editor, @corrections, @marker) -> + initialize: (@editor, @corrections, @marker, @updateTarget, @updateCallback) -> super - @addClass('corrections popover-list') + @addClass('spell-check-corrections corrections popover-list') @attach() attach: -> @@ -20,22 +20,51 @@ class CorrectionsView extends SelectListView @cancel() @remove() - confirmed: (correction) -> + confirmed: (item) -> @cancel() - return unless correction + return unless item @editor.transact => - @editor.setSelectedBufferRange(@marker.getBufferRange()) - @editor.insertText(correction) + if item.isSuggestion + # Update the buffer with the correction. + @editor.setSelectedBufferRange(@marker.getBufferRange()) + @editor.insertText(item.suggestion) + else + # Build up the arguments object for this buffer and text. + projectPath = null + relativePath = null + if @editor.buffer?.file?.path + [projectPath, relativePath] = atom.project.relativizePath(@editor.buffer.file.path) + args = { + id: @id, + projectPath: projectPath, + relativePath: relativePath + } + + # Send the "add" request to the plugin. + item.plugin.add args, item + + # Update the buffer to handle the corrections. + @updateCallback.bind(@updateTarget)() cancelled: -> @overlayDecoration.destroy() @restoreFocus() - viewForItem: (word) -> - element = document.createElement('li') - element.textContent = word + viewForItem: (item) -> + element = document.createElement "li" + if item.isSuggestion + # This is a word replacement suggestion. + element.textContent = item.label + else + # This is an operation such as add word. + em = document.createElement "em" + em.textContent = item.label + element.appendChild em element + getFilterKey: -> + "label" + selectNextItemView: -> super false diff --git a/lib/known-words-checker.coffee b/lib/known-words-checker.coffee new file mode 100644 index 00000000..57f67d0c --- /dev/null +++ b/lib/known-words-checker.coffee @@ -0,0 +1,64 @@ +class KnownWordsChecker + enableAdd: false + spelling: null + checker: null + + constructor: (knownWords) -> + # Set up the spelling manager we'll be using. + spellingManager = require "spelling-manager" + @spelling = new spellingManager.TokenSpellingManager + @checker = new spellingManager.BufferSpellingChecker @spelling + + # Set our known words. + @setKnownWords knownWords + + deactivate: -> + #console.log(@getid() + "deactivating") + return + + getId: -> "spell-check:known-words" + getName: -> "Known Words" + getPriority: -> 10 + isEnabled: -> @spelling.sensitive or @spelling.insensitive + + getStatus: -> "Working correctly." + providesSpelling: (args) -> true + providesSuggestions: (args) -> true + providesAdding: (args) -> @enableAdd + + check: (args, text) -> + ranges = [] + checked = @checker.check text + for token in checked + if token.status is 1 + ranges.push {start: token.start, end: token.end} + {correct: ranges} + + suggest: (args, word) -> + @spelling.suggest word + + getAddingTargets: (args) -> + if @enableAdd + [{sensitive: false, label: "Add to " + @getName()}] + else + [] + + add: (args, target) -> + c = atom.config.get 'spell-check.knownWords' + c.push target.word + atom.config.set 'spell-check.knownWords', c + + setAddKnownWords: (newValue) -> + @enableAdd = newValue + + setKnownWords: (knownWords) -> + # Clear out the old list. + @spelling.sensitive = {} + @spelling.insensitive = {} + + # Add the new ones into the list. + if knownWords + for ignore in knownWords + @spelling.add ignore + +module.exports = KnownWordsChecker diff --git a/lib/main.coffee b/lib/main.coffee index e211914a..44f0b40d 100644 --- a/lib/main.coffee +++ b/lib/main.coffee @@ -1,30 +1,136 @@ +{Task} = require 'atom' + SpellCheckView = null spellCheckViews = {} module.exports = activate: -> + # Set up the task for handling spell-checking in the background. This is + # what is actually in the background. + handlerFilename = require.resolve './spell-check-handler' + @task ?= new Task handlerFilename + + # Set up our callback to track when settings changed. + that = this + @task.on "spell-check:settings-changed", (ignore) -> + that.updateViews() + + # Since the spell-checking is done on another process, we gather up all the + # arguments and pass them into the task. Whenever these change, we'll update + # the object with the parameters and resend it to the task. + @globalArgs = { + locales: atom.config.get('spell-check.locales'), + localePaths: atom.config.get('spell-check.localePaths'), + useLocales: atom.config.get('spell-check.useLocales'), + knownWords: atom.config.get('spell-check.knownWords'), + addKnownWords: atom.config.get('spell-check.addKnownWords'), + checkerPaths: [] + } + @sendGlobalArgs() + + atom.config.onDidChange 'spell-check.locales', ({newValue, oldValue}) -> + that.globalArgs.locales = newValue + that.sendGlobalArgs() + atom.config.onDidChange 'spell-check.localePaths', ({newValue, oldValue}) -> + that.globalArgs.localePaths = newValue + that.sendGlobalArgs() + atom.config.onDidChange 'spell-check.useLocales', ({newValue, oldValue}) -> + that.globalArgs.useLocales = newValue + that.sendGlobalArgs() + atom.config.onDidChange 'spell-check.knownWords', ({newValue, oldValue}) -> + that.globalArgs.knownWords = newValue + that.sendGlobalArgs() + atom.config.onDidChange 'spell-check.addKnownWords', ({newValue, oldValue}) -> + that.globalArgs.addKnownWords = newValue + that.sendGlobalArgs() + + # Hook up the UI and processing. @commandSubscription = atom.commands.add 'atom-workspace', 'spell-check:toggle': => @toggle() @viewsByEditor = new WeakMap @disposable = atom.workspace.observeTextEditors (editor) => SpellCheckView ?= require './spell-check-view' - spellCheckView = new SpellCheckView(editor) + + # The SpellCheckView needs both a handle for the task to handle the + # background checking and a cached view of the in-process manager for + # getting corrections. We used a function to a function because scope + # wasn't working properly. + spellCheckView = new SpellCheckView(editor, @task, => @getInstance @globalArgs) # save the {editor} into a map editorId = editor.id spellCheckViews[editorId] = {} spellCheckViews[editorId]['view'] = spellCheckView spellCheckViews[editorId]['active'] = true - @viewsByEditor.set(editor, spellCheckView) - - misspellingMarkersForEditor: (editor) -> - @viewsByEditor.get(editor).markerLayer.getMarkers() + spellCheckViews[editorId]['editor'] = editor + @viewsByEditor.set editor, spellCheckView deactivate: -> + console.log "spell-check: deactiving" + @instance?.deactivate() + @instance = null + @task?.terminate() + @task = null @commandSubscription.dispose() @commandSubscription = null + + # Clear out the known views. + for editorId of spellCheckViews + view = spellCheckViews[editorId] + view['editor'].destroy() + spellCheckViews = {} + + # While we have WeakMap.clear, it isn't a function available in ES6. So, we + # just replace the WeakMap entirely and let the system release the objects. + @viewsByEditor = new WeakMap + + # Finish up by disposing everything else associated with the plugin. @disposable.dispose() + # Registers any Atom packages that provide our service. Because we use a Task, + # we have to load the plugin's checker in both that service and in the Atom + # process (for coming up with corrections). Since everything passed to the + # task must be JSON serialized, we pass the full path to the task and let it + # require it on that end. + consumeSpellCheckers: (checkerPaths) -> + # Normalize it so we always have an array. + unless checkerPaths instanceof Array + checkerPaths = [ checkerPaths ] + + # Go through and add any new plugins to the list. + changed = false + for checkerPath in checkerPaths + if checkerPath not in @globalArgs.checkerPaths + @task?.send {type: "checker", checkerPath: checkerPath} + @instance?.addCheckerPath checkerPath + @globalArgs.checkerPaths.push checkerPath + changed = true + + misspellingMarkersForEditor: (editor) -> + @viewsByEditor.get(editor).markerLayer.getMarkers() + + updateViews: -> + for editorId of spellCheckViews + view = spellCheckViews[editorId] + if view['active'] + view['view'].updateMisspellings() + + sendGlobalArgs: -> + @task.send {type: "global", global: @globalArgs} + + # Retrieves, creating if required, a spelling manager for use with + # synchronous operations such as retrieving corrections. + getInstance: (globalArgs) -> + if not @instance + SpellCheckerManager = require './spell-check-manager' + @instance = SpellCheckerManager + @instance.setGlobalArgs globalArgs + + for checkerPath in globalArgs.checkerPaths + @instance.addCheckerPath checkerPath + + return @instance + # Internal: Toggles the spell-check activation state. toggle: -> editorId = atom.workspace.getActiveTextEditor().id diff --git a/lib/spell-check-handler.coffee b/lib/spell-check-handler.coffee index b44054d1..92d02d37 100644 --- a/lib/spell-check-handler.coffee +++ b/lib/spell-check-handler.coffee @@ -1,32 +1,34 @@ -SpellChecker = require 'spellchecker' +# This is the task local handler for the manager so we can reuse the manager +# throughout the life of the task. +SpellCheckerManager = require './spell-check-manager' +instance = SpellCheckerManager +instance.isTask = true -module.exports = ({id, text}) -> - SpellChecker.add("GitHub") - SpellChecker.add("github") +# Because of the heavy use of configuration options for the packages and our +# inability to listen/access config settings from this process, we need to get +# the settings in a roundabout manner via sending messages through the process. +# This has an additional complexity because other packages may need to send +# messages through the main `spell-check` task so they can update *their* +# checkers inside the task process. +# +# Below the dispatcher for all messages from the server. The type argument is +# require, how it is handled is based on the type. +process.on "message", (message) -> + switch + when message.type is "global" then loadGlobalSettings message.global + when message.type is "checker" then instance.addCheckerPath message.checkerPath + # Quietly ignore unknown message types. - misspelledCharacterRanges = SpellChecker.checkSpelling(text) +# This handles updating the global configuration settings for +# `spell-check` along with the built-in checkers (locale and knownWords). +loadGlobalSettings = (data) -> + instance.setGlobalArgs data - row = 0 - rangeIndex = 0 - characterIndex = 0 - misspellings = [] - while characterIndex < text.length and rangeIndex < misspelledCharacterRanges.length - lineBreakIndex = text.indexOf('\n', characterIndex) - if lineBreakIndex is -1 - lineBreakIndex = Infinity +# This is the function that is called by the views whenever data changes. It +# returns with the misspellings along with an identifier that will let the task +# wrapper route it to the appropriate view. +backgroundCheck = (data) -> + misspellings = instance.check data, data.text + {id: data.id, misspellings: misspellings.misspellings} - 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} +module.exports = backgroundCheck diff --git a/lib/spell-check-manager.coffee b/lib/spell-check-manager.coffee new file mode 100644 index 00000000..3fce31c6 --- /dev/null +++ b/lib/spell-check-manager.coffee @@ -0,0 +1,379 @@ +class SpellCheckerManager + checkers: [] + checkerPaths: [] + locales: [] + localePaths: [] + useLocales: false + localeCheckers: null + knownWords: [] + addKnownWords: false + knownWordsChecker: null + isTask: false + + setGlobalArgs: (data) -> + # We need underscore to do the array comparisons. + _ = require "underscore-plus" + + # Check to see if any values have changed. When they have, they clear out + # the applicable checker which forces a reload. + changed = false + removeLocaleCheckers = false + removeKnownWordsChecker = false + + if not _.isEqual(@locales, data.locales) + # If the locales is blank, then we always create a default one. However, + # any new data.locales will remain blank. + if not @localeCheckers or data.locales?.length isnt 0 + @locales = data.locales + removeLocaleCheckers = true + if not _.isEqual(@localePaths, data.localePaths) + @localePaths = data.localePaths + removeLocaleCheckers = true + if @useLocales isnt data.useLocales + @useLocales = data.useLocales + removeLocaleCheckers = true + if @knownWords isnt data.knownWords + @knownWords = data.knownWords + removeKnownWordsChecker = true + changed = true + if @addKnownWords isnt data.addKnownWords + @addKnownWords = data.addKnownWords + removeKnownWordsChecker = true + # We don't update `changed` since it doesn't affect the plugins. + + # If we made a change to the checkers, we need to remove them from the + # system so they can be reinitialized. + if removeLocaleCheckers and @localeCheckers + checkers = @localeCheckers + for checker in checkers + @removeSpellChecker checker + @localeCheckers = null + changed = true + + if removeKnownWordsChecker and @knownWordsChecker + @removeSpellChecker @knownWordsChecker + @knownWordsChecker = null + changed = true + + # If we had any change to the system, we need to send a message back to the + # main process so it can trigger a recheck which then calls `init` which + # then locales any changed locales or known words checker. + if changed + @emitSettingsChanged() + + emitSettingsChanged: -> + if @isTask + emit("spell-check:settings-changed") + + addCheckerPath: (checkerPath) -> + checker = require checkerPath + @addPluginChecker checker + + addPluginChecker: (checker) -> + # Add the spell checker to the list. + @addSpellChecker checker + + # We only emit a settings change for plugins since the core checkers are + # handled in a different manner. + @emitSettingsChanged() + + addSpellChecker: (checker) -> + @checkers.push checker + + removeSpellChecker: (spellChecker) -> + @checkers = @checkers.filter (plugin) -> plugin isnt spellChecker + + check: (args, text) -> + # Make sure our deferred initialization is done. + @init() + + # We need a couple packages. + multirange = require 'multi-integer-range' + + # For every registered spellchecker, we need to find out the ranges in the + # text that the checker confirms are correct or indicates is a misspelling. + # We keep these as separate lists since the different checkers may indicate + # the same range for either and we need to be able to remove confirmed words + # from the misspelled ones. + correct = new multirange.MultiRange([]) + incorrects = [] + + for checker in @checkers + # We only care if this plugin contributes to checking spelling. + if not checker.isEnabled() or not checker.providesSpelling(args) + continue + + # Get the results which includes positive (correct) and negative (incorrect) + # ranges. If we have an incorrect range but no correct, everything not + # in incorrect is considered correct. + results = checker.check(args, text) + + if results.invertIncorrectAsCorrect and results.incorrect + # We need to add the opposite of the incorrect as correct elements in + # the list. We do this by creating a subtraction. + invertedCorrect = new multirange.MultiRange([[0, text.length]]) + removeRange = new multirange.MultiRange([]) + for range in results.incorrect + removeRange.appendRange(range.start, range.end) + invertedCorrect.subtract(removeRange) + + # Everything in `invertedCorrect` is correct, so add it directly to + # the list. + correct.append invertedCorrect + else if results.correct + for range in results.correct + correct.appendRange(range.start, range.end) + + if results.incorrect + newIncorrect = new multirange.MultiRange([]) + incorrects.push(newIncorrect) + + for range in results.incorrect + newIncorrect.appendRange(range.start, range.end) + + # If we don't have any incorrect spellings, then there is nothing to worry + # about, so just return and stop processing. + if incorrects.length is 0 + return {id: args.id, misspellings: []} + + # Build up an intersection of all the incorrect ranges. We only treat a word + # as being incorrect if *every* checker that provides negative values treats + # it as incorrect. We know there are at least one item in this list, so pull + # that out. If that is the only one, we don't have to do any additional work, + # otherwise we compare every other one against it, removing any elements + # that aren't an intersection which (hopefully) will produce a smaller list + # with each iteration. + intersection = null + index = 1 + + for incorrect in incorrects + if intersection is null + intersection = incorrect + else + intersection.append(incorrect) + + # If we have no intersection, then nothing to report as a problem. + if intersection.length is 0 + return {id: args.id, misspellings: []} + + # Remove all of the confirmed correct words from the resulting incorrect + # list. This allows us to have correct-only providers as opposed to only + # incorrect providers. + if correct.ranges.length > 0 + intersection.subtract(correct) + + # Convert the text ranges (index into the string) into Atom buffer + # coordinates ( row and column). + row = 0 + rangeIndex = 0 + lineBeginIndex = 0 + misspellings = [] + while lineBeginIndex < text.length and rangeIndex < intersection.ranges.length + # Figure out where the next line break is. If we hit -1, then we make sure + # it is a higher number so our < comparisons work properly. + lineEndIndex = text.indexOf('\n', lineBeginIndex) + if lineEndIndex is -1 + lineEndIndex = Infinity + + # Loop through and get all the ranegs for this line. + loop + range = intersection.ranges[rangeIndex] + if range and range[0] < lineEndIndex + # Figure out the character range of this line. We need this because + # @addMisspellings doesn't handle jumping across lines easily and the + # use of the number ranges is inclusive. + lineRange = new multirange.MultiRange([]).appendRange(lineBeginIndex, lineEndIndex) + rangeRange = new multirange.MultiRange([]).appendRange(range[0], range[1]) + lineRange.intersect(rangeRange) + + # The range we have here includes whitespace between two concurrent + # tokens ("zz zz zz" shows up as a single misspelling). The original + # version would split the example into three separate ones, so we + # do the same thing, but only for the ranges within the line. + @addMisspellings(misspellings, row, lineRange.ranges[0], lineBeginIndex, text) + + # If this line is beyond the limits of our current range, we move to + # the next one, otherwise we loop again to reuse this range against + # the next line. + if lineEndIndex >= range[1] + rangeIndex++ + else + break + else + break + + lineBeginIndex = lineEndIndex + 1 + row++ + + # Return the resulting misspellings. + {id: args.id, misspellings: misspellings} + + suggest: (args, word) -> + # Make sure our deferred initialization is done. + @init() + + # Gather up a list of corrections and put them into a custom object that has + # the priority of the plugin, the index in the results, and the word itself. + # We use this to intersperse the results together to avoid having the + # preferred answer for the second plugin below the least preferred of the + # first. + suggestions = [] + + for checker in @checkers + # We only care if this plugin contributes to checking to suggestions. + if not checker.isEnabled() or not checker.providesSuggestions(args) + continue + + # Get the suggestions for this word. + index = 0 + priority = checker.getPriority() + + for suggestion in checker.suggest(args, word) + suggestions.push {isSuggestion: true, priority: priority, index: index++, suggestion: suggestion, label: suggestion} + + # Once we have the suggestions, then sort them to intersperse the results. + keys = Object.keys(suggestions).sort (key1, key2) -> + value1 = suggestions[key1] + value2 = suggestions[key2] + weight1 = value1.priority + value1.index + weight2 = value2.priority + value2.index + + if weight1 isnt weight2 + return weight1 - weight2 + + return value1.suggestion.localeCompare(value2.suggestion) + + # Go through the keys and build the final list of suggestions. As we go + # through, we also want to remove duplicates. + results = [] + seen = [] + for key in keys + s = suggestions[key] + if seen.hasOwnProperty s.suggestion + continue + results.push s + seen[s.suggestion] = 1 + + # We also grab the "add to dictionary" listings. + that = this + keys = Object.keys(@checkers).sort (key1, key2) -> + value1 = that.checkers[key1] + value2 = that.checkers[key2] + value1.getPriority() - value2.getPriority() + + for key in keys + # We only care if this plugin contributes to checking to suggestions. + checker = @checkers[key] + if not checker.isEnabled() or not checker.providesAdding(args) + continue + + # Add all the targets to the list. + targets = checker.getAddingTargets args + for target in targets + target.plugin = checker + target.word = word + target.isSuggestion = false + results.push target + + # Return the resulting list of options. + results + + addMisspellings: (misspellings, row, range, lineBeginIndex, text) -> + # Get the substring of text, if there is no space, then we can just return + # the entire result. + substring = text.substring(range[0], range[1]) + + if /\s+/.test substring + # We have a space, to break it into individual components and push each + # one to the misspelling list. + parts = substring.split /(\s+)/ + substringIndex = 0 + for part in parts + if not /\s+/.test part + markBeginIndex = range[0] - lineBeginIndex + substringIndex + markEndIndex = markBeginIndex + part.length + misspellings.push([[row, markBeginIndex], [row, markEndIndex]]) + + substringIndex += part.length + + return + + # There were no spaces, so just return the entire list. + misspellings.push([ + [row, range[0] - lineBeginIndex], + [row, range[1] - lineBeginIndex] + ]) + + init: -> + # See if we need to initialize the system checkers. + if @localeCheckers is null + # Initialize the collection. If we aren't using any, then stop doing anything. + console.log "spell-check: loading locales", @useLocales, @locales + @localeCheckers = [] + + if @useLocales + # If we have a blank location, use the default based on the process. If + # set, then it will be the best language. + if not @locales.length + defaultLocale = process.env.LANG + if defaultLocale + @locales = [defaultLocale.split('.')[0]] + + # If we can't figure out the language from the process, check the + # browser. After testing this, we found that this does not reliably + # produce a proper IEFT tag for languages; on OS X, it was providing + # "English" which doesn't work with the locale selection. To avoid using + # it, we use some tests to make sure it "looks like" an IEFT tag. + if not @locales.length + defaultLocale = navigator.language + if defaultLocale and defaultLocale.length is 5 + separatorChar = defaultLocale.charAt(2) + if separatorChar is '_' or separatorChar is '-' + @locales = [defaultLocale] + + # If we still can't figure it out, use US English. It isn't a great + # choice, but it is a reasonable default not to mention is can be used + # with the fallback path of the `spellchecker` package. + if not @locales.length + @locales = ['en_US'] + + # Go through the new list and create new locale checkers. + SystemChecker = require "./system-checker" + for locale in @locales + checker = new SystemChecker locale, @localePaths + @addSpellChecker checker + @localeCheckers.push checker + + # See if we need to reload the known words. + if @knownWordsChecker is null + console.log "spell-check: loading known words", @knownWords + KnownWordsChecker = require './known-words-checker' + @knownWordsChecker = new KnownWordsChecker @knownWords + @knownWordsChecker.enableAdd = @addKnownWords + @addSpellChecker @knownWordsChecker + + deactivate: -> + @checkers = [] + @locales = [] + @localePaths = [] + @useLocales= false + @localeCheckers = null + @knownWords = [] + @addKnownWords = false + @knownWordsChecker = null + + reloadLocales: -> + if @localeCheckers + console.log "spell-check: unloading locales" + for localeChecker in @localeCheckers + @removeSpellChecker localeChecker + @localeCheckers = null + + reloadKnownWords: -> + if @knownWordsChecker + console.log "spell-check: unloading known words" + @removeSpellChecker @knownWordsChecker + @knownWordsChecker = null + +manager = new SpellCheckerManager +module.exports = manager diff --git a/lib/spell-check-task.coffee b/lib/spell-check-task.coffee index 709c7810..d53d3280 100644 --- a/lib/spell-check-task.coffee +++ b/lib/spell-check-task.coffee @@ -1,28 +1,34 @@ -{Task} = require 'atom' idCounter = 0 -# Wraps a single {Task} so that multiple views reuse the same task but it is -# terminated once all views are removed. module.exports = class SpellCheckTask + @handler: null @callbacksById: {} - constructor: -> + constructor: (@task) -> @id = idCounter++ terminate: -> delete @constructor.callbacksById[@id] - if Object.keys(@constructor.callbacksById).length is 0 - @constructor.task?.terminate() - @constructor.task = null + start: (buffer) -> + # Figure out the paths since we need that for checkers that are project-specific. + projectPath = null + relativePath = null + if buffer?.file?.path + [projectPath, relativePath] = atom.project.relativizePath(buffer.file.path) - start: (text) -> - @constructor.task ?= new Task(require.resolve('./spell-check-handler')) - @constructor.task?.start {@id, text}, @constructor.dispatchMisspellings + # Submit the spell check request to the background task. + args = { + id: @id, + projectPath, + relativePath, + text: buffer.getText() + } + @task?.start args, @constructor.dispatchMisspellings onDidSpellCheck: (callback) -> @constructor.callbacksById[@id] = callback - @dispatchMisspellings: ({id, misspellings}) => - @callbacksById[id]?(misspellings) + @dispatchMisspellings: (data) => + @callbacksById[data.id]?(data.misspellings) diff --git a/lib/spell-check-view.coffee b/lib/spell-check-view.coffee index 966614bf..79d9bc75 100644 --- a/lib/spell-check-view.coffee +++ b/lib/spell-check-view.coffee @@ -10,19 +10,19 @@ class SpellCheckView @content: -> @div class: 'spell-check' - constructor: (@editor) -> + constructor: (@editor, @task, @getInstance) -> @disposables = new CompositeDisposable - @task = new SpellCheckTask() @initializeMarkerLayer() + @taskWrapper = new SpellCheckTask @task @correctMisspellingCommand = atom.commands.add atom.views.getView(@editor), 'spell-check:correct-misspelling', => if marker = @markerLayer.findMarkers({containsBufferPosition: @editor.getCursorBufferPosition()})[0] CorrectionsView ?= require './corrections-view' @correctionsView?.destroy() - @correctionsView = new CorrectionsView(@editor, @getCorrections(marker), marker) + @correctionsView = new CorrectionsView(@editor, @getCorrections(marker), marker, this, @updateMisspellings) - @task.onDidSpellCheck (misspellings) => - @detroyMarkers() + @taskWrapper.onDidSpellCheck (misspellings) => + @destroyMarkers() @addMarkers(misspellings) if @buffer? @disposables.add @editor.onDidChangePath => @@ -52,14 +52,14 @@ class SpellCheckView destroy: -> @unsubscribeFromBuffer() @disposables.dispose() - @task.terminate() + @taskWrapper.terminate() @markerLayer.destroy() @markerLayerDecoration.destroy() @correctMisspellingCommand.dispose() @correctionsView?.remove() unsubscribeFromBuffer: -> - @detroyMarkers() + @destroyMarkers() if @buffer? @bufferDisposable.dispose() @@ -77,7 +77,7 @@ class SpellCheckView grammar = @editor.getGrammar().scopeName _.contains(atom.config.get('spell-check.grammars'), grammar) - detroyMarkers: -> + destroyMarkers: -> @markerLayer.destroy() @markerLayerDecoration.destroy() @initializeMarkerLayer() @@ -89,11 +89,22 @@ class SpellCheckView updateMisspellings: -> # Task::start can throw errors atom/atom#3326 try - @task.start(@buffer.getText()) + @taskWrapper.start @editor.buffer catch error console.warn('Error starting spell check task', error.stack ? error) getCorrections: (marker) -> - SpellChecker ?= require 'spellchecker' - misspelling = @editor.getTextInBufferRange(marker.getBufferRange()) - corrections = SpellChecker.getCorrectionsForMisspelling(misspelling) + # Build up the arguments object for this buffer and text. + projectPath = null + relativePath = null + if @buffer?.file?.path + [projectPath, relativePath] = atom.project.relativizePath(@buffer.file.path) + args = { + projectPath: projectPath, + relativePath: relativePath + } + + # Get the misspelled word and then request corrections. + instance = @getInstance() + misspelling = @editor.getTextInBufferRange marker.getBufferRange() + corrections = instance.suggest args, misspelling diff --git a/lib/system-checker.coffee b/lib/system-checker.coffee new file mode 100644 index 00000000..025a22eb --- /dev/null +++ b/lib/system-checker.coffee @@ -0,0 +1,92 @@ +spellchecker = require 'spellchecker' + +class SystemChecker + spellchecker: null + locale: null + enabled: true + reason: null + paths: null + + constructor: (locale, paths) -> + @locale = locale + @paths = paths + + deactivate: -> + #console.log @getId(), "deactivating" + return + + getId: -> "spell-check:" + @locale.toLowerCase().replace("_", "-") + getName: -> "System Dictionary (" + @locale + ")" + getPriority: -> 100 # System level data, has no user input. + isEnabled: -> @enabled + getStatus: -> + if @enabled + "Working correctly." + else + @reason + + providesSpelling: (args) -> true + providesSuggestions: (args) -> true + providesAdding: (args) -> false # Users shouldn't be adding to the system dictionary. + + check: (args, text) -> + @deferredInit() + {invertIncorrectAsCorrect: true, incorrect: @spellchecker.checkSpelling(text)} + + suggest: (args, word) -> + @deferredInit() + @spellchecker.getCorrectionsForMisspelling(word) + + deferredInit: -> + # If we already have a spellchecker, then we don't have to do anything. + if @spellchecker + return + + # Initialize the spell checker which can take some time. + @spellchecker = new spellchecker.Spellchecker + + # Windows uses its own API and the paths are unimportant, only attempting + # to load it works. + if /win32/.test process.platform + if @spellchecker.setDictionary @locale, "C:\\" + #console.log @getId(), "Windows API" + return + + # Check the paths supplied by the user. + for path in @paths + if @spellchecker.setDictionary @locale, path + #console.log @getId(), path + return + + # For Linux, we have to search the directory paths to find the dictionary. + if /linux/.test process.platform + if @spellchecker.setDictionary @locale, "/usr/share/hunspell" + #console.log @getId(), "/usr/share/hunspell" + return + if @spellchecker.setDictionary @locale, "/usr/share/myspell/dicts" + #console.log @getId(), "/usr/share/myspell/dicts" + return + + # OS X uses the following paths. + if /darwin/.test process.platform + if @spellchecker.setDictionary @locale, "/" + #console.log @getId(), "OS X API" + return + if @spellchecker.setDictionary @locale, "/System/Library/Spelling" + #console.log @getId(), "/System/Library/Spelling" + return + + # Try the packaged library inside the node_modules. `getDictionaryPath` is + # not available, so we have to fake it. This will only work for en-US. + path = require 'path' + vendor = path.join __dirname, "..", "node_modules", "spellchecker", "vendor", "hunspell_dictionaries" + if @spellchecker.setDictionary @locale, vendor + #console.log @getId(), vendor + return + + # If we fell through all the if blocks, then we couldn't load the dictionary. + @enabled = false + @reason = "Cannot find dictionary for " + @locale + "." + console.log @getId(), "Can't load " + @locale + ": " + @reason + +module.exports = SystemChecker diff --git a/package.json b/package.json index 974dd63f..2cf05697 100644 --- a/package.json +++ b/package.json @@ -5,10 +5,13 @@ "description": "Highlights misspelled words and shows possible corrections.", "dependencies": { "atom-space-pen-views": "^2.0.0", + "multi-integer-range": "^2.0.0", + "natural": "^0.4.0", "spellchecker": "3.2.3", + "spelling-manager": "0.3.0", "underscore-plus": "^1" }, - "repository": "https://github.com/atom/spell-check", + "repository": "https://github.com/dmoonfire/spell-check", "license": "MIT", "engines": { "atom": "*" @@ -23,10 +26,54 @@ "text.plain", "text.plain.null-grammar" ], - "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." + "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.", + "order": "1" + }, + "useLocales": { + "type": "boolean", + "default": "true", + "description": "If unchecked, then the locales below will not be used for spell-checking and no spell-checking using system dictionaries will be provided.", + "order": "2" + }, + "locales": { + "type": "array", + "default": [], + "items": { + "type": "string" + }, + "description": "List of locales to use for the system spell-checking. Examples would be `en-US` or `de-DE`. For Windows, the appropriate language must be installed using *Region and language settings*. If this is blank, then the default language for the user will be used.", + "order": 3 + }, + "localePaths": { + "type": "array", + "default": [], + "items": { + "type": "string" + }, + "description": "List of additional paths to search for dictionary files. If a locale cannot be found in these, the internal code will attempt to find it using common search paths. This is used for Linux and OS X.", + "order": 4 + }, + "knownWords": { + "type": "array", + "default": [], + "description": "List words that are considered correct even if they do not appear in any other dictionary. Words with capitals or ones that start with `!` are case-sensitive.", + "order": 5 + }, + "addKnownWords": { + "type": "boolean", + "default": false, + "description": "If checked, then the suggestions will include options to add to the known words list above.", + "order": 6 } }, "devDependencies": { "coffeelint": "^1.9.7" + }, + "consumedServices": { + "spell-check": { + "versions": { + "^1.0.0": "consumeSpellCheckers" + } + } } } diff --git a/script/benchmark.coffee b/script/benchmark.coffee old mode 100755 new mode 100644 diff --git a/spec/eot-spec-checker.coffee b/spec/eot-spec-checker.coffee new file mode 100644 index 00000000..a0436d6e --- /dev/null +++ b/spec/eot-spec-checker.coffee @@ -0,0 +1,8 @@ +SpecChecker = require './spec-checker' + +class EndOfTestSpecChecker extends SpecChecker + constructor: -> + super("eot", true, ["eot"]) + +checker = new EndOfTestSpecChecker +module.exports = checker diff --git a/spec/known-1-spec-checker.coffee b/spec/known-1-spec-checker.coffee new file mode 100644 index 00000000..1418303c --- /dev/null +++ b/spec/known-1-spec-checker.coffee @@ -0,0 +1,8 @@ +SpecChecker = require './spec-checker' + +class Known1SpecChecker extends SpecChecker + constructor: -> + super("known-1", false, ["k1a", "k0b", "k0a"]) + +checker = new Known1SpecChecker +module.exports = checker diff --git a/spec/known-2-spec-checker.coffee b/spec/known-2-spec-checker.coffee new file mode 100644 index 00000000..8303fe83 --- /dev/null +++ b/spec/known-2-spec-checker.coffee @@ -0,0 +1,8 @@ +SpecChecker = require './spec-checker' + +class Known2SpecChecker extends SpecChecker + constructor: -> + super("known-2", true, ["k2a", "k0c", "k0a"]) + +checker = new Known2SpecChecker +module.exports = checker diff --git a/spec/known-3-spec-checker.coffee b/spec/known-3-spec-checker.coffee new file mode 100644 index 00000000..d96f274a --- /dev/null +++ b/spec/known-3-spec-checker.coffee @@ -0,0 +1,8 @@ +SpecChecker = require './spec-checker' + +class Known3SpecChecker extends SpecChecker + constructor: -> + super("known-3", false, ["k3a", "k0b", "k0a"]) + +checker = new Known3SpecChecker +module.exports = checker diff --git a/spec/known-4-spec-checker.coffee b/spec/known-4-spec-checker.coffee new file mode 100644 index 00000000..89664bc7 --- /dev/null +++ b/spec/known-4-spec-checker.coffee @@ -0,0 +1,8 @@ +SpecChecker = require './spec-checker' + +class Known4SpecChecker extends SpecChecker + constructor: -> + super("known-4", true, ["k4a", "k0c", "k0a"]) + +checker = new Known4SpecChecker +module.exports = checker diff --git a/spec/spec-checker.coffee b/spec/spec-checker.coffee new file mode 100644 index 00000000..729c9364 --- /dev/null +++ b/spec/spec-checker.coffee @@ -0,0 +1,50 @@ +class SpecChecker + spelling: null + checker: null + + constructor: (@id, @isNegative, knownWords) -> + # Set up the spelling manager we'll be using. + spellingManager = require "spelling-manager" + @spelling = new spellingManager.TokenSpellingManager + @checker = new spellingManager.BufferSpellingChecker @spelling + + # Set our known words. + @setKnownWords knownWords + + console.log "constructor", @getId() + + deactivate: -> + return + + getId: -> "spell-check:spec:" + @id + getName: -> "Spec Checker" + getPriority: -> 10 + isEnabled: -> true + getStatus: -> "Working correctly." + providesSpelling: (args) -> true + providesSuggestions: (args) -> false + providesAdding: (args) -> false + + check: (args, text) -> + ranges = [] + checked = @checker.check text + for token in checked + if token.status is 1 + ranges.push {start: token.start, end: token.end} + + if @isNegative + {incorrect: ranges} + else + {correct: ranges} + + setKnownWords: (knownWords) -> + # Clear out the old list. + @spelling.sensitive = {} + @spelling.insensitive = {} + + # Add the new ones into the list. + if knownWords + for ignore in knownWords + @spelling.add ignore + +module.exports = SpecChecker diff --git a/spec/spell-check-spec.coffee b/spec/spell-check-spec.coffee index 24b920f0..5af83f72 100644 --- a/spec/spell-check-spec.coffee +++ b/spec/spell-check-spec.coffee @@ -32,6 +32,7 @@ describe "Spell check", -> editorElement = atom.views.getView(editor) it "decorates all misspelled words", -> + atom.config.set('spell-check.locales', ['en-US']) editor.setText("This middle of thiss\nsentencts\n\nhas issues and the \"edn\" 'dsoe' too") atom.config.set('spell-check.grammars', ['source.js']) @@ -47,8 +48,25 @@ describe "Spell check", -> 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.") + it "decorates misspelled words with a leading space", -> + atom.config.set('spell-check.locales', ['en-US']) + editor.setText("\nchok bok") + atom.config.set('spell-check.grammars', ['source.js']) + + misspellingMarkers = null + waitsFor -> + misspellingMarkers = getMisspellingMarkers() + misspellingMarkers.length > 0 + + runs -> + expect(misspellingMarkers.length).toBe 2 + expect(textForMarker(misspellingMarkers[0])).toEqual "chok" + expect(textForMarker(misspellingMarkers[1])).toEqual "bok" + + it "allow entering of known words", -> + atom.config.set('spell-check.knownWords', ['GitHub', '!github', 'codez']) + atom.config.set('spell-check.locales', ['en-US']) + editor.setText("GitHub (aka github): Where codez are builz.") atom.config.set('spell-check.grammars', ['source.js']) misspellingMarkers = null @@ -58,7 +76,7 @@ describe "Spell check", -> runs -> expect(misspellingMarkers.length).toBe 1 - expect(textForMarker(misspellingMarkers[0])).toBe "codez" + expect(textForMarker(misspellingMarkers[0])).toBe "builz" it "hides decorations when a misspelled word is edited", -> editor.setText('notaword') @@ -83,14 +101,13 @@ describe "Spell check", -> describe "when spell checking for a grammar is removed", -> it "removes all the misspellings", -> + atom.config.set('spell-check.locales', ['en-US']) editor.setText('notaword') advanceClock(editor.getBuffer().getStoppedChangingDelay()) atom.config.set('spell-check.grammars', ['source.js']) - misspellingMarkers = null waitsFor -> - misspellingMarkers = getMisspellingMarkers() - misspellingMarkers.length > 0 + getMisspellingMarkers().length > 0 runs -> expect(getMisspellingMarkers().length).toBe 1 @@ -99,14 +116,13 @@ describe "Spell check", -> describe "when spell checking for a grammar is toggled off", -> it "removes all the misspellings", -> + atom.config.set('spell-check.locales', ['en-US']) editor.setText('notaword') advanceClock(editor.getBuffer().getStoppedChangingDelay()) atom.config.set('spell-check.grammars', ['source.js']) - misspellingMarkers = null waitsFor -> - misspellingMarkers = getMisspellingMarkers() - misspellingMarkers.length > 0 + getMisspellingMarkers().length > 0 runs -> expect(getMisspellingMarkers().length).toBe 1 @@ -115,6 +131,7 @@ describe "Spell check", -> describe "when the editor's grammar changes to one that does not have spell check enabled", -> it "removes all the misspellings", -> + atom.config.set('spell-check.locales', ['en-US']) editor.setText('notaword') advanceClock(editor.getBuffer().getStoppedChangingDelay()) atom.config.set('spell-check.grammars', ['source.js']) @@ -130,6 +147,7 @@ describe "Spell check", -> describe "when 'spell-check:correct-misspelling' is triggered on the editor", -> describe "when the cursor touches a misspelling that has corrections", -> it "displays the corrections for the misspelling and replaces the misspelling when a correction is selected", -> + atom.config.set('spell-check.locales', ['en-US']) editor.setText('tofether') advanceClock(editor.getBuffer().getStoppedChangingDelay()) atom.config.set('spell-check.grammars', ['source.js']) @@ -157,6 +175,7 @@ describe "Spell check", -> describe "when the cursor touches a misspelling that has no corrections", -> it "displays a message saying no corrections found", -> + atom.config.set('spell-check.locales', ['en-US']) editor.setText('zxcasdfysyadfyasdyfasdfyasdfyasdfyasydfasdf') advanceClock(editor.getBuffer().getStoppedChangingDelay()) atom.config.set('spell-check.grammars', ['source.js']) @@ -172,6 +191,7 @@ describe "Spell check", -> describe "when the editor is destroyed", -> it "destroys all misspelling markers", -> + atom.config.set('spell-check.locales', ['en-US']) editor.setText('mispelling') atom.config.set('spell-check.grammars', ['source.js']) @@ -181,3 +201,137 @@ describe "Spell check", -> runs -> editor.destroy() expect(getMisspellingMarkers().length).toBe 0 + + describe "when using checker plugins", -> + it "no opinion on input means correctly spells", -> + spellCheckModule.consumeSpellCheckers require.resolve('./known-1-spec-checker.coffee') + spellCheckModule.consumeSpellCheckers require.resolve('./known-2-spec-checker.coffee') + spellCheckModule.consumeSpellCheckers require.resolve('./known-3-spec-checker.coffee') + spellCheckModule.consumeSpellCheckers require.resolve('./known-4-spec-checker.coffee') + spellCheckModule.consumeSpellCheckers require.resolve('./eot-spec-checker.coffee') + atom.config.set('spell-check.locales', ['en-US']) + atom.config.set('spell-check.useLocales', false) + editor.setText('eot') + atom.config.set('spell-check.grammars', ['source.js']) + + waitsFor -> + getMisspellingMarkers().length > 0 + + runs -> + expect(getMisspellingMarkers().length).toBe 1 + editor.destroy() + expect(getMisspellingMarkers().length).toBe 0 + + it "correctly spelling k1a", -> + spellCheckModule.consumeSpellCheckers require.resolve('./known-1-spec-checker.coffee') + spellCheckModule.consumeSpellCheckers require.resolve('./known-2-spec-checker.coffee') + spellCheckModule.consumeSpellCheckers require.resolve('./known-3-spec-checker.coffee') + spellCheckModule.consumeSpellCheckers require.resolve('./known-4-spec-checker.coffee') + spellCheckModule.consumeSpellCheckers require.resolve('./eot-spec-checker.coffee') + atom.config.set('spell-check.locales', ['en-US']) + atom.config.set('spell-check.useLocales', false) + editor.setText('k1a eot') + atom.config.set('spell-check.grammars', ['source.js']) + + waitsFor -> + getMisspellingMarkers().length > 0 + + runs -> + expect(getMisspellingMarkers().length).toBe 1 + editor.destroy() + expect(getMisspellingMarkers().length).toBe 0 + + it "correctly mispelling k2a", -> + spellCheckModule.consumeSpellCheckers require.resolve('./known-1-spec-checker.coffee') + spellCheckModule.consumeSpellCheckers require.resolve('./known-2-spec-checker.coffee') + spellCheckModule.consumeSpellCheckers require.resolve('./known-3-spec-checker.coffee') + spellCheckModule.consumeSpellCheckers require.resolve('./known-4-spec-checker.coffee') + spellCheckModule.consumeSpellCheckers require.resolve('./eot-spec-checker.coffee') + atom.config.set('spell-check.locales', ['en-US']) + atom.config.set('spell-check.useLocales', false) + editor.setText('k2a eot') + atom.config.set('spell-check.grammars', ['source.js']) + + waitsFor -> + getMisspellingMarkers().length > 0 + + runs -> + expect(getMisspellingMarkers().length).toBe 2 + editor.destroy() + expect(getMisspellingMarkers().length).toBe 0 + + it "correctly mispelling k2a with text in middle", -> + spellCheckModule.consumeSpellCheckers require.resolve('./known-1-spec-checker.coffee') + spellCheckModule.consumeSpellCheckers require.resolve('./known-2-spec-checker.coffee') + spellCheckModule.consumeSpellCheckers require.resolve('./known-3-spec-checker.coffee') + spellCheckModule.consumeSpellCheckers require.resolve('./known-4-spec-checker.coffee') + spellCheckModule.consumeSpellCheckers require.resolve('./eot-spec-checker.coffee') + atom.config.set('spell-check.locales', ['en-US']) + atom.config.set('spell-check.useLocales', false) + editor.setText('k2a good eot') + atom.config.set('spell-check.grammars', ['source.js']) + + waitsFor -> + getMisspellingMarkers().length > 0 + + runs -> + expect(getMisspellingMarkers().length).toBe 2 + editor.destroy() + expect(getMisspellingMarkers().length).toBe 0 + + it "word is both correct and incorrect is correct", -> + spellCheckModule.consumeSpellCheckers require.resolve('./known-1-spec-checker.coffee') + spellCheckModule.consumeSpellCheckers require.resolve('./known-2-spec-checker.coffee') + spellCheckModule.consumeSpellCheckers require.resolve('./known-3-spec-checker.coffee') + spellCheckModule.consumeSpellCheckers require.resolve('./known-4-spec-checker.coffee') + spellCheckModule.consumeSpellCheckers require.resolve('./eot-spec-checker.coffee') + atom.config.set('spell-check.locales', ['en-US']) + atom.config.set('spell-check.useLocales', false) + editor.setText('k0a eot') + atom.config.set('spell-check.grammars', ['source.js']) + + waitsFor -> + getMisspellingMarkers().length > 0 + + runs -> + expect(getMisspellingMarkers().length).toBe 1 + editor.destroy() + expect(getMisspellingMarkers().length).toBe 0 + + it "word is correct twice is correct", -> + spellCheckModule.consumeSpellCheckers require.resolve('./known-1-spec-checker.coffee') + spellCheckModule.consumeSpellCheckers require.resolve('./known-2-spec-checker.coffee') + spellCheckModule.consumeSpellCheckers require.resolve('./known-3-spec-checker.coffee') + spellCheckModule.consumeSpellCheckers require.resolve('./known-4-spec-checker.coffee') + spellCheckModule.consumeSpellCheckers require.resolve('./eot-spec-checker.coffee') + atom.config.set('spell-check.locales', ['en-US']) + atom.config.set('spell-check.useLocales', false) + editor.setText('k0b eot') + atom.config.set('spell-check.grammars', ['source.js']) + + waitsFor -> + getMisspellingMarkers().length > 0 + + runs -> + expect(getMisspellingMarkers().length).toBe 1 + editor.destroy() + expect(getMisspellingMarkers().length).toBe 0 + + it "word is incorrect twice is incorrect", -> + spellCheckModule.consumeSpellCheckers require.resolve('./known-1-spec-checker.coffee') + spellCheckModule.consumeSpellCheckers require.resolve('./known-2-spec-checker.coffee') + spellCheckModule.consumeSpellCheckers require.resolve('./known-3-spec-checker.coffee') + spellCheckModule.consumeSpellCheckers require.resolve('./known-4-spec-checker.coffee') + spellCheckModule.consumeSpellCheckers require.resolve('./eot-spec-checker.coffee') + atom.config.set('spell-check.locales', ['en-US']) + atom.config.set('spell-check.useLocales', false) + editor.setText('k0c eot') + atom.config.set('spell-check.grammars', ['source.js']) + + waitsFor -> + getMisspellingMarkers().length > 0 + + runs -> + expect(getMisspellingMarkers().length).toBe 2 + editor.destroy() + expect(getMisspellingMarkers().length).toBe 0 diff --git a/styles/spell-check.atom-text-editor.less b/styles/spell-check.atom-text-editor.less index 6a89a4b1..ee7013b3 100644 --- a/styles/spell-check.atom-text-editor.less +++ b/styles/spell-check.atom-text-editor.less @@ -1,3 +1,7 @@ .spell-check-misspelling .region { border-bottom: 2px dotted hsla(0, 100%, 60%, 0.75); } + +.spell-check-corrections { + width: 25em !important; +}