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

Commit dff766e

Browse files
committed
Updated plugin-based implementation from provided feedback.
- 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`.
1 parent 8b7ab9c commit dff766e

11 files changed

+409
-239
lines changed

PLUGINS.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# Plugins
2+
3+
The `spell-check` allows for additional dictionaries to be used at the same time using Atom's `providedServices` element in the `package.json` file.
4+
5+
"providedServices": {
6+
"spell-check": {
7+
"versions": {
8+
"1.0.0": "nameOfFunctionToProvideSpellCheck"
9+
}
10+
}
11+
}
12+
13+
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).
14+
15+
provideSpellCheck: ->
16+
require.resolve './project-checker.coffee'
17+
18+
The path given must resolve to a singleton instance of a class.
19+
20+
class ProjectChecker
21+
# Magical code
22+
checker = new ProjectChecker()
23+
module.exports = checker
24+
25+
See the `spell-check-project` for an example implementation.
26+
27+
# Checker
28+
29+
A common parameter type is `checkArgs`, this is a hash with the following signature.
30+
31+
args = {
32+
projectPath: "/absolute/path/to/project/root,
33+
relativePath: "relative/path/from/projet/root"
34+
}
35+
36+
Below the required methods for the checker instance.
37+
38+
* getId(): string
39+
* 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.
40+
* This will also used to pass information from the Atom process into the background task once that is implemented.
41+
* getPriority(): number
42+
* 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).
43+
* isEnabled(): boolean
44+
* If this returns true, then the plugin will considered for processing.
45+
* providesSpelling(checkArgs): boolean
46+
* If this returns true, then the plugin will be included when looking for incorrect and correct words via the `check` function.
47+
* checkArray(checkArgs, words: string[]): boolean?[]
48+
* 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.
49+
* The output is an array of the same length as words which has three values, one for each word given:
50+
* `null`: The checker provides no opinion on correctness.
51+
* `false`: The word is specifically false.
52+
* `true`: The word is correctly spelled.
53+
* True always takes precedence, then false. If every checker provides `null`, then the word is considered spelled correctly.
54+
* providesSuggestions(checkArgs): boolean
55+
* If this returns true, then the plugin will be included when querying for suggested words via the `suggest` function.
56+
* suggest(checkArgs, word: string): [suggestion: string]
57+
* Returns a list of suggestions for a given word ordered so the most important is at the beginning of the list.
58+
* providesAdding(checkArgs): boolean
59+
* If this returns true, then the dictionary allows a word to be added to the dictionary.
60+
* getAddingTargets(checkArgs): [target]
61+
* Gets a list of targets to show to the user.
62+
* The `target` object has a minimum signature of `{ label: stringToShowTheUser }`. For example, `{ label: "Ignore word (case-sensitive)" }`.
63+
* 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.
64+
* add(buffer, target, word)
65+
* Adds a word to the dictionary, using the target for identifying which one is used.

README.md

Lines changed: 4 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -26,46 +26,10 @@ for the _Spell Check_ package. Here are some examples: `source.coffee`,
2626

2727
## Changing the dictionary
2828

29-
Currently, only the English (US) dictionary is supported. Follow [this issue](https://github.com/atom/spell-check/issues/11) for updates.
29+
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.
3030

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

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

35-
"providedServices": {
36-
"spell-check": {
37-
"versions": {
38-
"1.0.0": "nameOfFunctionToProvideSpellCheck"
39-
}
40-
}
41-
}
42-
43-
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:
44-
45-
* getId(): string
46-
* 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.
47-
* getName(): string
48-
* Returns the human-readable name for the plugin. This is used on the status screen and in various dialogs/popups.
49-
* getPriority(): number
50-
* 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).
51-
* isEnabled(): boolean
52-
* If this returns true, then the plugin will considered for processing.
53-
* getStatus(): string
54-
* 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).
55-
* providesSpelling(buffer): boolean
56-
* If this returns true, then the plugin will be included when looking for incorrect and correct words via the `check` function.
57-
* check(buffer, text: string): { correct: [range], incorrect: [range] }
58-
* `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.
59-
* The `range` objects have a signature of `{ start: X, end: Y }`.
60-
* providesSuggestions(buffer): boolean
61-
* If this returns true, then the plugin will be included when querying for suggested words via the `suggest` function.
62-
* suggest(buffer, word: string): [suggestion: string]
63-
* Returns a list of suggestions for a given word ordered so the most important is at the beginning of the list.
64-
* providesAdding(buffer): boolean
65-
* If this returns true, then the dictionary allows a word to be added to the dictionary.
66-
* getAddingTargets(buffer): [target]
67-
* Gets a list of targets to show to the user.
68-
* The `target` object has a minimum signature of `{ label: stringToShowTheUser }`. For example, `{ label: "Ignore word (case-sensitive)" }`.
69-
* 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.
70-
* add(buffer, target, word)
71-
* Adds a word to the dictionary, using the target for identifying which one is used.
35+
_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.

lib/corrections-view.coffee

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ class CorrectionsView extends SelectListView
2626
@editor.transact =>
2727
if item.isSuggestion
2828
# Update the buffer with the correction.
29-
@editor.setSelectedBufferRange(@marker.getRange())
29+
@editor.setSelectedBufferRange(@marker.getBufferRange())
3030
@editor.insertText(item.suggestion)
3131
else
3232
# Build up the arguments object for this buffer and text.

lib/known-words-checker.coffee

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ class KnownWordsChecker
1313
@setKnownWords knownWords
1414

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

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

36+
checkArray: (args, words) ->
37+
results = []
38+
for word, index in words
39+
result = @check args, word
40+
if result.correct.length is 0
41+
results.push null
42+
else
43+
results.push true
44+
results
45+
3546
suggest: (args, word) ->
3647
@spelling.suggest word
3748

lib/main.coffee

Lines changed: 83 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,74 +1,104 @@
1+
{Task} = require 'atom'
2+
13
SpellCheckView = null
24
spellCheckViews = {}
35

46
module.exports =
5-
instance: null
6-
77
activate: ->
8-
# Create the unified handler for all spellchecking.
9-
SpellCheckerManager = require './spell-check-manager.coffee'
10-
@instance = SpellCheckerManager
8+
# Set up the task for handling spell-checking in the background. This is
9+
# what is actually in the background.
10+
handlerFilename = require.resolve './spell-check-handler'
11+
@task ?= new Task handlerFilename
12+
13+
# Set up our callback to track when settings changed.
1114
that = this
15+
@task.on "spell-check:settings-changed", (ignore) ->
16+
console.log("updating views because of change", that)
17+
that.updateViews()
1218

13-
# Initialize the spelling manager so it can perform deferred loading.
14-
@instance.locales = atom.config.get('spell-check.locales')
15-
@instance.localePaths = atom.config.get('spell-check.localePaths')
16-
@instance.useLocales = atom.config.get('spell-check.useLocales')
19+
# Since the spell-checking is done on another process, we gather up all the
20+
# arguments and pass them into the task. Whenever these change, we'll update
21+
# the object with the parameters and resend it to the task.
22+
@globalArgs = {
23+
locales: atom.config.get('spell-check.locales'),
24+
localePaths: atom.config.get('spell-check.localePaths'),
25+
useLocales: atom.config.get('spell-check.useLocales'),
26+
knownWords: atom.config.get('spell-check.knownWords'),
27+
addKnownWords: atom.config.get('spell-check.addKnownWords'),
28+
checkerPaths: []
29+
}
30+
@sendGlobalArgs()
1731

1832
atom.config.onDidChange 'spell-check.locales', ({newValue, oldValue}) ->
19-
that.instance.locales = atom.config.get('spell-check.locales')
20-
that.instance.reloadLocales()
21-
that.updateViews()
33+
that.globalArgs.locales = atom.config.get('spell-check.locales')
34+
that.sendGlobalArgs()
2235
atom.config.onDidChange 'spell-check.localePaths', ({newValue, oldValue}) ->
23-
that.instance.localePaths = atom.config.get('spell-check.localePaths')
24-
that.instance.reloadLocales()
25-
that.updateViews()
36+
that.globalArgs.localePaths = atom.config.get('spell-check.localePaths')
37+
that.sendGlobalArgs()
2638
atom.config.onDidChange 'spell-check.useLocales', ({newValue, oldValue}) ->
27-
that.instance.useLocales = atom.config.get('spell-check.useLocales')
28-
that.instance.reloadLocales()
29-
that.updateViews()
30-
31-
# Add in the settings for known words checker.
32-
@instance.knownWords = atom.config.get('spell-check.knownWords')
33-
@instance.addKnownWords = atom.config.get('spell-check.addKnownWords')
34-
39+
that.globalArgs.useLocales = atom.config.get('spell-check.useLocales')
40+
that.sendGlobalArgs()
3541
atom.config.onDidChange 'spell-check.knownWords', ({newValue, oldValue}) ->
36-
that.instance.knownWords = atom.config.get('spell-check.knownWords')
37-
that.instance.reloadKnownWords()
38-
that.updateViews()
42+
that.globalArgs.knownWords = atom.config.get('spell-check.knownWords')
43+
that.sendGlobalArgs()
3944
atom.config.onDidChange 'spell-check.addKnownWords', ({newValue, oldValue}) ->
40-
that.instance.addKnownWords = atom.config.get('spell-check.addKnownWords')
41-
that.instance.reloadKnownWords()
42-
that.updateViews()
45+
that.globalArgs.addKnownWords = atom.config.get('spell-check.addKnownWords')
46+
that.sendGlobalArgs()
4347

4448
# Hook up the UI and processing.
4549
@commandSubscription = atom.commands.add 'atom-workspace',
4650
'spell-check:toggle': => @toggle()
4751
@viewsByEditor = new WeakMap
4852
@disposable = atom.workspace.observeTextEditors (editor) =>
4953
SpellCheckView ?= require './spell-check-view'
50-
spellCheckView = new SpellCheckView(editor, @instance)
54+
55+
# The SpellCheckView needs both a handle for the task to handle the
56+
# background checking and a cached view of the in-process manager for
57+
# getting corrections. We used a function to a function because scope
58+
# wasn't working properly.
59+
spellCheckView = new SpellCheckView(editor, @task, (ignore) => @getInstance @globalArgs)
5160

5261
# save the {editor} into a map
5362
editorId = editor.id
5463
spellCheckViews[editorId] = {}
5564
spellCheckViews[editorId]['view'] = spellCheckView
5665
spellCheckViews[editorId]['active'] = true
57-
@viewsByEditor.set(editor, spellCheckView)
66+
@viewsByEditor.set editor, spellCheckView
5867

5968
deactivate: ->
60-
@instance.deactivate()
69+
console.log "spell-check: deactiving"
70+
@instance?.deactivate()
6171
@instance = null
72+
@task?.terminate()
73+
@task = null
6274
@commandSubscription.dispose()
6375
@commandSubscription = null
76+
77+
# While we have WeakMap.clear, it isn't a function available in ES6. So, we
78+
# just replace the WeakMap entirely and let the system release the objects.
79+
@viewsByEditor = new WeakMap
80+
81+
# Finish up by disposing everything else associated with the plugin.
6482
@disposable.dispose()
6583

66-
consumeSpellCheckers: (plugins) ->
67-
unless plugins instanceof Array
68-
plugins = [ plugins ]
84+
# Registers any Atom packages that provide our service. Because we use a Task,
85+
# we have to load the plugin's checker in both that service and in the Atom
86+
# process (for coming up with corrections). Since everything passed to the
87+
# task must be JSON serialized, we pass the full path to the task and let it
88+
# require it on that end.
89+
consumeSpellCheckers: (checkerPaths) ->
90+
# Normalize it so we always have an array.
91+
unless checkerPaths instanceof Array
92+
checkerPaths = [ checkerPaths ]
6993

70-
for plugin in plugins
71-
@instance.addPluginChecker plugin
94+
# Go through and add any new plugins to the list.
95+
changed = false
96+
for checkerPath in checkerPaths
97+
if checkerPath not in @globalArgs.checkerPaths
98+
@task?.send {type: "checker", checkerPath: checkerPath}
99+
@instance?.addCheckerPath checkerPath
100+
@globalArgs.checkerPaths.push checkerPath
101+
changed = true
72102

73103
misspellingMarkersForEditor: (editor) ->
74104
@viewsByEditor.get(editor).markerLayer.getMarkers()
@@ -79,6 +109,22 @@ module.exports =
79109
if view['active']
80110
view['view'].updateMisspellings()
81111

112+
sendGlobalArgs: ->
113+
@task.send {type: "global", global: @globalArgs}
114+
115+
# Retrieves, creating if required, a spelling manager for use with synchronous
116+
# operations such as retrieving corrections.
117+
getInstance: (globalArgs) ->
118+
if not @instance
119+
SpellCheckerManager = require './spell-check-manager.coffee'
120+
@instance = SpellCheckerManager
121+
@instance.setGlobalArgs globalArgs
122+
123+
for checkerPath in globalArgs.checkerPaths
124+
@instance.addCheckerPath checkerPath
125+
126+
return @instance
127+
82128
# Internal: Toggles the spell-check activation state.
83129
toggle: ->
84130
editorId = atom.workspace.getActiveTextEditor().id

lib/spell-check-handler.coffee

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,34 @@
1-
# Background task for checking the text of a buffer and returning the
2-
# spelling. Since this can be an expensive operation, it is intended to be run
3-
# in the background with the results returned asynchronously.
4-
backgroundCheck = (data) ->
5-
# Load a manager in memory and let it initialize.
6-
SpellCheckerManager = require './spell-check-manager.coffee'
7-
instance = SpellCheckerManager
8-
instance.locales = data.args.locales
9-
instance.localePaths = data.args.localePaths
10-
instance.useLocales = data.args.useLocales
11-
instance.knownWords = data.args.knownWords
12-
instance.addKnownWords = data.args.addKnownWords
1+
# This is the task local handler for the manager so we can reuse the manager
2+
# throughout the life of the task.
3+
SpellCheckerManager = require './spell-check-manager.coffee'
4+
instance = SpellCheckerManager
5+
instance.isTask = true
6+
7+
# Because of the heavy use of configuration options for the packages and our
8+
# inability to listen/access config settings from this process, we need to get
9+
# the settings in a roundabout manner via sending messages through the process.
10+
# This has an additional complexity because other packages may need to send
11+
# messages through the main `spell-check` task so they can update *their*
12+
# checkers inside the task process.
13+
#
14+
# Below the dispatcher for all messages from the server. The type argument is
15+
# require, how it is handled is based on the type.
16+
process.on "message", (message) ->
17+
switch
18+
when message.type is "global" then loadGlobalSettings message.global
19+
when message.type is "checker" then instance.addCheckerPath message.checkerPath
20+
# Quietly ignore unknown message types.
1321

14-
misspellings = instance.check data.args, data.text
15-
{id: data.args.id, misspellings}
22+
# This handles updating the global configuration settings for
23+
# `spell-check` along with the built-in checkers (locale and knownWords).
24+
loadGlobalSettings = (data) ->
25+
instance.setGlobalArgs data
26+
27+
# This is the function that is called by the views whenever data changes. It
28+
# returns with the misspellings along with an identifier that will let the task
29+
# wrapper route it to the appropriate view.
30+
backgroundCheck = (data) ->
31+
misspellings = instance.check data, data.text
32+
{id: data.id, misspellings: misspellings.misspellings}
1633

1734
module.exports = backgroundCheck

0 commit comments

Comments
 (0)