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

Commit

Permalink
Changed spell-checking to be plugin-based.
Browse files Browse the repository at this point in the history
* Changed the package to allow for external packages to provide additional checking. (Closes #74)
  - Disabled the task-based handling because of passing plugins.
  - Two default plugins are included: system-based dictionaries and "known words".
  - Suggestions and "add to dictionary" are also provided via interfaces. (Closes #10)
  - Modified various calls so they are aware of the where the buffer is located.
* Modified system to allow for multiple plugins/checkers to identify correctness.
  - Incorrect words must be incorrect for all checkers.
  - Any checker that treats a word as valid is considered valid for the buffer.
* Extracted system-based dictionary support into separate checker.
  - System dictionaries can now check across multiple system locales.
  - Locale selection can be changed via package settings. (Closes #21)
  - Multiple locales can be selected. (Closes #11)
  - External search paths can be used for Linux and OS X.
  - Default language is based on the process environment, with a fallback to the browser, before finally using `en-US` as a fallback.
* Extracted hard-coded approved list into a separate checker.
  - User can add additional "known words" via settings.
  - Added an option to add more known words via the suggestion dialog.
* Updated ignore files and added EditorConfig settings for development.
* Various coffee-centric formatting.
  • Loading branch information
dmoonfire committed Mar 15, 2016
1 parent 2d03cf9 commit 833330f
Show file tree
Hide file tree
Showing 15 changed files with 783 additions and 82 deletions.
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
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,45 @@ 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.

## Writing Providers

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 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.
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.getRange())
@editor.insertText(correction)
if item.isSuggestion
# Update the buffer with the correction.
@editor.setSelectedBufferRange(@marker.getRange())
@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
62 changes: 62 additions & 0 deletions lib/known-words-checker.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
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")

getId: -> "spell-check:known-words"
getName: -> "Known Words"
getPriority: -> 10
isEnabled: -> true
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
62 changes: 58 additions & 4 deletions lib/main.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,52 @@ SpellCheckView = null
spellCheckViews = {}

module.exports =
instance: null

activate: ->
# Create the unified handler for all spellchecking.
SpellCheckerManager = require './spell-check-manager.coffee'
@instance = SpellCheckerManager
that = this

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

atom.config.onDidChange 'spell-check.locales', ({newValue, oldValue}) ->
that.instance.locales = atom.config.get('spell-check.locales')
that.instance.reloadLocales()
that.updateViews()
atom.config.onDidChange 'spell-check.localePaths', ({newValue, oldValue}) ->
that.instance.localePaths = atom.config.get('spell-check.localePaths')
that.instance.reloadLocales()
that.updateViews()
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')

atom.config.onDidChange 'spell-check.knownWords', ({newValue, oldValue}) ->
that.instance.knownWords = atom.config.get('spell-check.knownWords')
that.instance.reloadKnownWords()
that.updateViews()
atom.config.onDidChange 'spell-check.addKnownWords', ({newValue, oldValue}) ->
that.instance.addKnownWords = atom.config.get('spell-check.addKnownWords')
that.instance.reloadKnownWords()
that.updateViews()

# 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)
spellCheckView = new SpellCheckView(editor, @instance)

# save the {editor} into a map
editorId = editor.id
Expand All @@ -17,14 +56,29 @@ module.exports =
spellCheckViews[editorId]['active'] = true
@viewsByEditor.set(editor, spellCheckView)

misspellingMarkersForEditor: (editor) ->
@viewsByEditor.get(editor).markerLayer.getMarkers()

deactivate: ->
@instance.deactivate()
@instance = null
@commandSubscription.dispose()
@commandSubscription = null
@disposable.dispose()

consumeSpellCheckers: (plugins) ->
unless plugins instanceof Array
plugins = [ plugins ]

for plugin in plugins
@instance.addPluginChecker plugin

misspellingMarkersForEditor: (editor) ->
@viewsByEditor.get(editor).markerLayer.getMarkers()

updateViews: ->
for editorId of spellCheckViews
view = spellCheckViews[editorId]
if view['active']
view['view'].updateMisspellings()

# Internal: Toggles the spell-check activation state.
toggle: ->
editorId = atom.workspace.getActiveTextEditor().id
Expand Down
45 changes: 15 additions & 30 deletions lib/spell-check-handler.coffee
Original file line number Diff line number Diff line change
@@ -1,32 +1,17 @@
SpellChecker = require 'spellchecker'
# 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

module.exports = ({id, text}) ->
SpellChecker.add("GitHub")
SpellChecker.add("github")
misspellings = instance.check data.args, data.text
{id: data.args.id, misspellings}

misspelledCharacterRanges = SpellChecker.checkSpelling(text)

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

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
Loading

0 comments on commit 833330f

Please sign in to comment.