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

Changed spell-checking to be plugin-based. #120

Merged
merged 6 commits into from
Aug 19, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
*~
npm-debug.log
node_modules
.DS_Store
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/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.
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
47 changes: 38 additions & 9 deletions lib/corrections-view.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -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: ->
Expand All @@ -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
Expand Down
64 changes: 64 additions & 0 deletions lib/known-words-checker.coffee
Original file line number Diff line number Diff line change
@@ -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
116 changes: 111 additions & 5 deletions lib/main.coffee
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Loading