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

Commit

Permalink
Split node-spellchecker-based checker into a locale and system version.
Browse files Browse the repository at this point in the history
- The locale version uses the locale paths but can be used without an
environment variable.
  - This is the same logic for Linux and when the environment variable
was used (which no longer is used).
  - Defaults to on.
- The system version only uses the operating system based checker and
always ignores Hunspell, even with the environment variable.
  - This does nothing on Linux but can be set.
  - Defaults to on.
- Refactored tests to be easier to clean.
- Added system-based checking tests but are only active on OS X because
of inconsistent availability on Windows.
  • Loading branch information
dmoonfire committed Jan 1, 2020
1 parent 7f84669 commit ffa893a
Show file tree
Hide file tree
Showing 9 changed files with 356 additions and 203 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ If your Windows user does not have Administration privileges, you'll need to do

Once the additional language is added, Atom will need to be restarted.

You can set the `SPELLCHECKER_PREFER_HUNSPELL` environment variable to request the use of the built-in hunspell spell checking library instead of the system dictionaries. If the environment variable is not set, then the `en-US` dictionaries found in the Atom's installation directory will not be used.
*Previously, setting `SPELLCHECKER_PREFER_HUNSPELL` environment variable would change how checking works. Now this is controlled by the system and locale checker to use the operating system version or Hunspell respectively.*

If locale is not set, Atom will attempt to use the current locale from the environment variable; if that is missing, `en-US` will be used. The dictionary for `en-US` is shipping with Atom but all other locale-based checkers will need to be downloaded from another source.

### Debian, Ubuntu, and Mint

Expand Down
10 changes: 10 additions & 0 deletions lib/checker-env.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module.exports =
isLinux: -> /linux/.test process.platform
isWindows: -> /win32/.test process.platform # TODO: Windows < 8 or >= 8
isDarwin: -> /darwin/.test process.platform
preferHunspell: -> !!process.env.SPELLCHECKER_PREFER_HUNSPELL

isSystemSupported: -> @isWindows() or @isDarwin()
isLocaleSupported: -> true

useLocales: -> @isLinux() or @isWindows() or @preferHunspell()
3 changes: 2 additions & 1 deletion lib/known-words-checker.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,11 @@ class KnownWordsChecker
check: (args, text) ->
ranges = []
checked = @checker.check text
id = @getId()
for token in checked
if token.status is 1
ranges.push {start: token.start, end: token.end}
{correct: ranges}
{id, correct: ranges}

suggest: (args, word) ->
@spelling.suggest word
Expand Down
129 changes: 129 additions & 0 deletions lib/locale-checker.coffee
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
spellchecker = require 'spellchecker'
pathspec = require 'atom-pathspec'
env = require './checker-env'

# The locale checker is a checker that takes a locale string (`en-US`) and
# optionally a path and then checks it.
class LocaleChecker
spellchecker: null
locale: null
enabled: true
reason: null
paths: null

constructor: (locale, paths) ->
@locale = locale
@paths = paths
@enabled = true
#console.log @getId(), "enabled", @isEnabled()

deactivate: ->
return

getId: -> "spell-check:locale:" + @locale.toLowerCase().replace("_", "-")
getName: -> "Locale Dictionary (" + @locale + ")"
getPriority: -> 100 # Hard-coded system level data, has no user input.
isEnabled: -> @enabled
getStatus: -> @reason
providesSpelling: (args) -> @enabled
providesSuggestions: (args) -> @enabled
providesAdding: (args) -> false

check: (args, text) ->
@deferredInit()
id = @getId()
if @enabled
@spellchecker.checkSpellingAsync(text).then (incorrect) ->
{id, invertIncorrectAsCorrect: true, incorrect}
else
{id, status: @getStatus()}

suggest: (args, word) ->
@deferredInit()
@spellchecker.getCorrectionsForMisspelling(word)

deferredInit: ->
# The system checker is not enabled for Linux platforms (no built-in checker).
if not @enabled
@reason = "Darwin does not use locale-based checking without SPELLCHECKER_PREFER_HUNSPELL set."
return

# 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. We also force
# the use of the Hunspell library even on Mac OS X. The "system checker"
# is the one that uses the built-in dictionaries from the operating system.
@spellchecker = new spellchecker.Spellchecker
@spellchecker.setSpellcheckerType spellchecker.ALWAYS_USE_HUNSPELL

# Build up a list of paths we are checking so we can report them fully
# to the user if we fail.
searchPaths = []

# Windows uses its own API and the paths are unimportant, only attempting
# to load it works.
if env.isWindows()
#if env.useWindowsSystemDictionary()
# return
searchPaths.push "C:\\"

# Check the paths supplied by the user.
for path in @paths
searchPaths.push pathspec.getPath(path)

# For Linux, we have to search the directory paths to find the dictionary.
if env.isLinux()
searchPaths.push "/usr/share/hunspell"
searchPaths.push "/usr/share/myspell"
searchPaths.push "/usr/share/myspell/dicts"

# OS X uses the following paths.
if env.isDarwin()
searchPaths.push "/"
searchPaths.push "/System/Library/Spelling"

# 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.
searchPaths.push spellchecker.getDictionaryPath()

# Attempt to load all the paths for the dictionary until we find one.
for path in searchPaths
if @spellchecker.setDictionary @locale, path
return

# If we fell through all the if blocks, then we couldn't load the dictionary.
@enabled = false
@reason = "Cannot load the system dictionary for `" + @locale + "`."
message = "The package `spell-check` cannot load the " \
+ "checker for `" \
+ @locale + "`." \
+ " See the settings for ways of changing the languages used, " \
+ " resolving missing dictionaries, or hiding this warning."

searches = "\n\nThe plugin checked the following paths for dictionary files:\n* " \
+ searchPaths.join("\n* ")

if not env.useLocales()
searches = "\n\nThe plugin tried to use the system dictionaries to find the locale."

noticesMode = atom.config.get('spell-check.noticesMode')

if noticesMode is "console" or noticesMode is "both"
console.log @getId(), (message + searches)
if noticesMode is "popup" or noticesMode is "both"
atom.notifications.addWarning(
message,
{
buttons: [
{
className: "btn",
onDidClick: -> atom.workspace.open("atom://config/packages/spell-check"),
text: "Settings"
}
]
}
)

module.exports = LocaleChecker
4 changes: 4 additions & 0 deletions lib/main.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ module.exports =
# These are the settings that are part of the main `spell-check` package.
locales: atom.config.get('spell-check.locales'),
localePaths: atom.config.get('spell-check.localePaths'),
useSystem: atom.config.get('spell-check.useSystem'),
useLocales: atom.config.get('spell-check.useLocales'),
knownWords: atom.config.get('spell-check.knownWords'),
addKnownWords: atom.config.get('spell-check.addKnownWords'),
Expand All @@ -42,6 +43,9 @@ module.exports =
@subs.add atom.config.onDidChange 'spell-check.localePaths', ({newValue, oldValue}) =>
@globalArgs.localePaths = newValue
manager.setGlobalArgs @globalArgs
@subs.add atom.config.onDidChange 'spell-check.useSystem', ({newValue, oldValue}) =>
@globalArgs.useSystem = newValue
manager.setGlobalArgs @globalArgs
@subs.add atom.config.onDidChange 'spell-check.useLocales', ({newValue, oldValue}) =>
@globalArgs.useLocales = newValue
manager.setGlobalArgs @globalArgs
Expand Down
144 changes: 86 additions & 58 deletions lib/spell-check-manager.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,51 @@ class SpellCheckerManager
locales: []
localePaths: []
useLocales: false
systemChecker: null
knownWordsChecker: null
localeCheckers: null
knownWords: []
addKnownWords: false
knownWordsChecker: null

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.
removeLocaleCheckers = false
# Check to see if any values have changed. When they have, then clear out
# the applicable checker which forces a reload. We have three basic
# checkers that are packaged in this:
# - system: Used for the built-in checkers for Windows and Mac
# - knownWords: For a configuration-based collection of known words
# - locale: For linux or when SPELLCHECKER_PREFER_HUNSPELL is set

# Handle known words checker.
removeKnownWordsChecker = false

if not _.isEqual(@knownWords, data.knownWords)
@knownWords = data.knownWords
removeKnownWordsChecker = true
if @addKnownWords isnt data.addKnownWords
@addKnownWords = data.addKnownWords
removeKnownWordsChecker = true

if removeKnownWordsChecker and @knownWordsChecker
@removeSpellChecker @knownWordsChecker
@knownWordsChecker = null

# Handle system checker.
removeSystemChecker = false

if @useSystem isnt data.useSystem
@useSystem = data.useSystem
removeSystemChecker = true

if removeSystemChecker and @systemChecker
@removeSpellChecker @systemChecker
@systemChecker = undefined

# Handle locale checkers.
removeLocaleCheckers = 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.
Expand All @@ -30,25 +61,13 @@ class SpellCheckerManager
if @useLocales isnt data.useLocales
@useLocales = data.useLocales
removeLocaleCheckers = true
if not _.isEqual(@knownWords, data.knownWords)
@knownWords = data.knownWords
removeKnownWordsChecker = true
if @addKnownWords isnt data.addKnownWords
@addKnownWords = data.addKnownWords
removeKnownWordsChecker = true

# 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

if removeKnownWordsChecker and @knownWordsChecker
@removeSpellChecker @knownWordsChecker
@knownWordsChecker = null

addCheckerPath: (checkerPath) ->
# Load the given path via require.
checker = require checkerPath
Expand Down Expand Up @@ -76,7 +95,8 @@ class SpellCheckerManager
# Make sure our deferred initialization is done.
@init()

# We need a couple packages.
# We need a couple packages but we want to lazy load them to
# reduce load time.
multirange = require 'multi-integer-range'

# For every registered spellchecker, we need to find out the ranges in the
Expand All @@ -86,8 +106,8 @@ class SpellCheckerManager
# from the misspelled ones.
correct = new multirange.MultiRange([])
incorrects = []

promises = []

for checker in @checkers
# We only care if this plugin contributes to checking spelling.
if not checker.isEnabled() or not checker.providesSpelling(args)
Expand Down Expand Up @@ -296,59 +316,67 @@ class SpellCheckerManager
])

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.
@localeCheckers = []
# Set up the system checker.
if @useSystem and @systemChecker is null
SystemChecker = require './system-checker'
@systemChecker = new SystemChecker
@addSpellChecker @systemChecker

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.
# Set up the known words.
if @knownWordsChecker is null
KnownWordsChecker = require './known-words-checker'
@knownWordsChecker = new KnownWordsChecker @knownWords
@knownWordsChecker.enableAdd = @addKnownWords
@addSpellChecker @knownWordsChecker

# See if we need to initialize the built-in checkers.
if @useLocales and @localeCheckers is null
# Set up the locale checkers.
@localeCheckers = []

# 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.
LocaleChecker = require "./locale-checker"
for locale in @locales
checker = new LocaleChecker locale, @localePaths
@addSpellChecker checker
@localeCheckers.push checker

deactivate: ->
@checkers = []
@locales = []
@localePaths = []
@useLocales= false
@localeCheckers = null
@useSystem = false
@useLocales = false
@knownWords = []
@addKnownWords = false

@systemChecker = null
@localeCheckers = null
@knownWordsChecker = null

reloadLocales: ->
Expand Down
Loading

0 comments on commit ffa893a

Please sign in to comment.