Skip to content
This repository has been archived by the owner on Dec 15, 2022. It is now read-only.

Commit

Permalink
Updated plugin-based implementation from provided feedback.
Browse files Browse the repository at this point in the history
- Switched to a task-based system.
    - Reworked the plugins so they return full paths to a `require`able instance.
    - Modified the activation/deactivation to create a single background task for all checking.
    - Initial request for corrections will have a delay while an in-process manager is created.
    - Added flow control via `send` and `emit` to communicate configuration changes with the task process.
- Changed to a word-based searching implementation.
    - Added a simplified tokenizer that includes some accented characters.
    - An internal cache is used for a single round of check to reduce overhead.
    - Removed the use of integer ranges and simplified processing logic.
- Removed a number of `console.log` calls used for debugging.
- Updated documentation.
    - Split out plugin creation into a separate document with a reference in the `README.md`.
  • Loading branch information
dmoonfire committed May 2, 2016
1 parent 8b7ab9c commit dff766e
Show file tree
Hide file tree
Showing 11 changed files with 409 additions and 239 deletions.
65 changes: 65 additions & 0 deletions PLUGINS.md
Original file line number Diff line number Diff line change
@@ -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/projet/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.
* checkArray(checkArgs, words: string[]): boolean?[]
* This takes an array of words in a given line. This will be called once for every line inside the buffer. It also also not include words already requested earlier in the buffer.
* The output is an array of the same length as words which has three values, one for each word given:
* `null`: The checker provides no opinion on correctness.
* `false`: The word is specifically false.
* `true`: The word is correctly spelled.
* True always takes precedence, then false. If every checker provides `null`, then the word is considered spelled correctly.
* 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.
44 changes: 4 additions & 40 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,46 +26,10 @@ for the _Spell Check_ package. Here are some examples: `source.coffee`,

## 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.

## Writing Providers
For Windows 8 and 10, you must install the language using the regional settings before the language can be chosen inside Atom.

The `spell-check` allows for additional dictionaries to be used at the same time using Atom's `providedServices` element in the `package.json` file.
## Plugins

"providedServices": {
"spell-check": {
"versions": {
"1.0.0": "nameOfFunctionToProvideSpellCheck"
}
}
}

The `nameOfFunctionToProvideSpellCheck` function may return either a single object describing the spell-check plugin or an array of them. Each spell-check plugin must implement the following:

* 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.
* getName(): string
* Returns the human-readable name for the plugin. This is used on the status screen and in various dialogs/popups.
* 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.
* getStatus(): string
* Returns a string that describes the current status or state of the plugin. This is to allow a plugin to identify why it is disabled or to indicate version numbers. This can be formatted for Markdown, including links, and will be displayed on a status screen (eventually).
* providesSpelling(buffer): boolean
* If this returns true, then the plugin will be included when looking for incorrect and correct words via the `check` function.
* check(buffer, text: string): { correct: [range], incorrect: [range] }
* `correct` and `incorrect` are both optional. If they are skipped, then it means the plugin does not contribute to the correctness or incorrectness of any word. If they are present but empty, it means there are no correct or incorrect words respectively.
* The `range` objects have a signature of `{ start: X, end: Y }`.
* providesSuggestions(buffer): boolean
* If this returns true, then the plugin will be included when querying for suggested words via the `suggest` function.
* suggest(buffer, 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(buffer): boolean
* If this returns true, then the dictionary allows a word to be added to the dictionary.
* getAddingTargets(buffer): [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.
_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.
2 changes: 1 addition & 1 deletion lib/corrections-view.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class CorrectionsView extends SelectListView
@editor.transact =>
if item.isSuggestion
# Update the buffer with the correction.
@editor.setSelectedBufferRange(@marker.getRange())
@editor.setSelectedBufferRange(@marker.getBufferRange())
@editor.insertText(item.suggestion)
else
# Build up the arguments object for this buffer and text.
Expand Down
13 changes: 12 additions & 1 deletion lib/known-words-checker.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ class KnownWordsChecker
@setKnownWords knownWords

deactivate: ->
console.log(@getid() + "deactivating")
#console.log(@getid() + "deactivating")
return

getId: -> "spell-check:known-words"
getName: -> "Known Words"
Expand All @@ -32,6 +33,16 @@ class KnownWordsChecker
ranges.push {start: token.start, end: token.end}
{correct: ranges}

checkArray: (args, words) ->
results = []
for word, index in words
result = @check args, word
if result.correct.length is 0
results.push null
else
results.push true
results

suggest: (args, word) ->
@spelling.suggest word

Expand Down
120 changes: 83 additions & 37 deletions lib/main.coffee
Original file line number Diff line number Diff line change
@@ -1,74 +1,104 @@
{Task} = require 'atom'

SpellCheckView = null
spellCheckViews = {}

module.exports =
instance: null

activate: ->
# Create the unified handler for all spellchecking.
SpellCheckerManager = require './spell-check-manager.coffee'
@instance = SpellCheckerManager
# 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) ->
console.log("updating views because of change", that)
that.updateViews()

# Initialize the spelling manager so it can perform deferred loading.
@instance.locales = atom.config.get('spell-check.locales')
@instance.localePaths = atom.config.get('spell-check.localePaths')
@instance.useLocales = atom.config.get('spell-check.useLocales')
# 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.instance.locales = atom.config.get('spell-check.locales')
that.instance.reloadLocales()
that.updateViews()
that.globalArgs.locales = atom.config.get('spell-check.locales')
that.sendGlobalArgs()
atom.config.onDidChange 'spell-check.localePaths', ({newValue, oldValue}) ->
that.instance.localePaths = atom.config.get('spell-check.localePaths')
that.instance.reloadLocales()
that.updateViews()
that.globalArgs.localePaths = atom.config.get('spell-check.localePaths')
that.sendGlobalArgs()
atom.config.onDidChange 'spell-check.useLocales', ({newValue, oldValue}) ->
that.instance.useLocales = atom.config.get('spell-check.useLocales')
that.instance.reloadLocales()
that.updateViews()

# Add in the settings for known words checker.
@instance.knownWords = atom.config.get('spell-check.knownWords')
@instance.addKnownWords = atom.config.get('spell-check.addKnownWords')

that.globalArgs.useLocales = atom.config.get('spell-check.useLocales')
that.sendGlobalArgs()
atom.config.onDidChange 'spell-check.knownWords', ({newValue, oldValue}) ->
that.instance.knownWords = atom.config.get('spell-check.knownWords')
that.instance.reloadKnownWords()
that.updateViews()
that.globalArgs.knownWords = atom.config.get('spell-check.knownWords')
that.sendGlobalArgs()
atom.config.onDidChange 'spell-check.addKnownWords', ({newValue, oldValue}) ->
that.instance.addKnownWords = atom.config.get('spell-check.addKnownWords')
that.instance.reloadKnownWords()
that.updateViews()
that.globalArgs.addKnownWords = atom.config.get('spell-check.addKnownWords')
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, @instance)

# 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, (ignore) => @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)
@viewsByEditor.set editor, spellCheckView

deactivate: ->
@instance.deactivate()
console.log "spell-check: deactiving"
@instance?.deactivate()
@instance = null
@task?.terminate()
@task = null
@commandSubscription.dispose()
@commandSubscription = null

# 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()

consumeSpellCheckers: (plugins) ->
unless plugins instanceof Array
plugins = [ plugins ]
# 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 ]

for plugin in plugins
@instance.addPluginChecker plugin
# 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()
Expand All @@ -79,6 +109,22 @@ module.exports =
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.coffee'
@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
Expand Down
45 changes: 31 additions & 14 deletions lib/spell-check-handler.coffee
Original file line number Diff line number Diff line change
@@ -1,17 +1,34 @@
# Background task for checking the text of a buffer and returning the
# spelling. Since this can be an expensive operation, it is intended to be run
# in the background with the results returned asynchronously.
backgroundCheck = (data) ->
# Load a manager in memory and let it initialize.
SpellCheckerManager = require './spell-check-manager.coffee'
instance = SpellCheckerManager
instance.locales = data.args.locales
instance.localePaths = data.args.localePaths
instance.useLocales = data.args.useLocales
instance.knownWords = data.args.knownWords
instance.addKnownWords = data.args.addKnownWords
# 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.coffee'
instance = SpellCheckerManager
instance.isTask = true

# 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.

misspellings = instance.check data.args, data.text
{id: data.args.id, misspellings}
# This handles updating the global configuration settings for
# `spell-check` along with the built-in checkers (locale and knownWords).
loadGlobalSettings = (data) ->
instance.setGlobalArgs data

# 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}

module.exports = backgroundCheck
Loading

0 comments on commit dff766e

Please sign in to comment.