From a4dd36e3ff728aaff5af7216bcff94195783b82e Mon Sep 17 00:00:00 2001 From: Andrew Dupont Date: Wed, 23 Jul 2025 19:21:34 -0700 Subject: [PATCH] =?UTF-8?q?[spell-check]=20Rewrite/reformat=20for=20ease?= =?UTF-8?q?=20of=20understanding=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …with a couple of bug fixes along the way. --- packages/spell-check/.prettierrc | 10 +- packages/spell-check/lib/checker-env.js | 45 +- packages/spell-check/lib/corrections-view.js | 185 +-- .../spell-check/lib/known-words-checker.js | 154 +- packages/spell-check/lib/locale-checker.js | 385 ++--- packages/spell-check/lib/main.js | 455 +++-- packages/spell-check/lib/scope-helper.js | 172 +- .../spell-check/lib/spell-check-manager.js | 998 +++++------ packages/spell-check/lib/spell-check-task.js | 363 ++-- packages/spell-check/lib/spell-check-view.js | 728 ++++---- packages/spell-check/lib/system-checker.js | 120 +- packages/spell-check/spec/spell-check-spec.js | 1458 ++++++++--------- 12 files changed, 2522 insertions(+), 2551 deletions(-) diff --git a/packages/spell-check/.prettierrc b/packages/spell-check/.prettierrc index d0b55e5ee9..01f841432f 100644 --- a/packages/spell-check/.prettierrc +++ b/packages/spell-check/.prettierrc @@ -1,7 +1,7 @@ { - "endOfLine": "lf", - "semi": true, - "singleQuote": true, - "tabWidth": 4, - "trailingComma": "es5" + "endOfLine": "lf", + "semi": true, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "es5" } diff --git a/packages/spell-check/lib/checker-env.js b/packages/spell-check/lib/checker-env.js index 23a404b2ff..d4b19dcc3c 100644 --- a/packages/spell-check/lib/checker-env.js +++ b/packages/spell-check/lib/checker-env.js @@ -1,25 +1,24 @@ module.exports = { - isLinux() { - return /linux/.test(process.platform); - }, - isWindows() { - return /win32/.test(process.platform); - }, // TODO: Windows < 8 or >= 8 - isDarwin() { - return /darwin/.test(process.platform); - }, - preferHunspell() { - return !!process.env.SPELLCHECKER_PREFER_HUNSPELL; - }, - - isSystemSupported() { - return this.isWindows() || this.isDarwin(); - }, - isLocaleSupported() { - return true; - }, - - useLocales() { - return this.isLinux() || this.isWindows() || this.preferHunspell(); - }, + isLinux() { + return /linux/.test(process.platform); + }, + isWindows() { + // TODO: Windows < 8 or >= 8 + return /win32/.test(process.platform); + }, + isDarwin() { + return /darwin/.test(process.platform); + }, + preferHunspell() { + return !!process.env.SPELLCHECKER_PREFER_HUNSPELL; + }, + isSystemSupported() { + return this.isWindows() || this.isDarwin(); + }, + isLocaleSupported() { + return true; + }, + useLocales() { + return this.isLinux() || this.isWindows() || this.preferHunspell(); + }, }; diff --git a/packages/spell-check/lib/corrections-view.js b/packages/spell-check/lib/corrections-view.js index ba6a1348a1..bf6c56c3d3 100644 --- a/packages/spell-check/lib/corrections-view.js +++ b/packages/spell-check/lib/corrections-view.js @@ -1,102 +1,95 @@ -/** @babel */ +const SelectListView = require('atom-select-list'); -import SelectListView from 'atom-select-list'; +module.exports = class CorrectionsView { + constructor(editor, corrections, marker, updateTarget, updateCallback) { + this.editor = editor; + this.corrections = corrections; + this.marker = marker; + this.updateTarget = updateTarget; + this.updateCallback = updateCallback; -export default class CorrectionsView { - constructor(editor, corrections, marker, updateTarget, updateCallback) { - this.editor = editor; - this.corrections = corrections; - this.marker = marker; - this.updateTarget = updateTarget; - this.updateCallback = updateCallback; - this.selectListView = new SelectListView({ - emptyMessage: 'No corrections', - items: this.corrections, - filterKeyForItem: (item) => item.label, - elementForItem: (item) => { - const 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. - const em = document.createElement('em'); - em.textContent = item.label; - element.appendChild(em); - } - return element; - }, - didConfirmSelection: (item) => { - this.editor.transact(() => { - if (item.isSuggestion) { - // Update the buffer with the correction. - this.editor.setSelectedBufferRange( - this.marker.getBufferRange() - ); - this.editor.insertText(item.suggestion); - } else { - // Build up the arguments object for this buffer and text. - let projectPath = null; - let relativePath = null; - if ( - this.editor && - this.editor.buffer && - this.editor.buffer.file && - this.editor.buffer.file.path - ) { - [ - projectPath, - relativePath, - ] = atom.project.relativizePath( - this.editor.buffer.file.path - ); - } + this.selectListView = new SelectListView({ + emptyMessage: 'No corrections', + items: this.corrections, + filterKeyForItem: (item) => item.label, + elementForItem: (item) => { + const 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. + const em = document.createElement('em'); + em.textContent = item.label; + element.appendChild(em); + } + return element; + }, + didConfirmSelection: (item) => { + this.editor.transact(() => { + if (item.isSuggestion) { + // Update the buffer with the correction. + this.editor.setSelectedBufferRange(this.marker.getBufferRange()); + this.editor.insertText(item.suggestion); + } else { + // Build up the arguments object for this buffer and text. + let projectPath = null; + let relativePath = null; + if ( + this.editor && + this.editor.buffer && + this.editor.buffer.file && + this.editor.buffer.file.path + ) { + [projectPath, relativePath] = atom.project.relativizePath( + this.editor.buffer.file.path + ); + } - const args = { id: this.id, projectPath, relativePath }; - // Send the "add" request to the plugin. - item.plugin.add(args, item); - // Update the buffer to handle the corrections. - this.updateCallback.bind(this.updateTarget)(); - } - }); - this.destroy(); - }, - didConfirmEmptySelection: () => { - this.destroy(); - }, - didCancelSelection: () => { - this.destroy(); - }, + const args = { id: this.id, projectPath, relativePath }; + // Send the "add" request to the plugin. + item.plugin.add(args, item); + // Update the buffer to handle the corrections. + this.updateCallback.bind(this.updateTarget)(); + } }); - this.selectListView.element.classList.add( - 'spell-check-corrections', - 'corrections', - 'popover-list' - ); - } + this.destroy(); + }, + didConfirmEmptySelection: () => { + this.destroy(); + }, + didCancelSelection: () => { + this.destroy(); + }, + }); - attach() { - this.previouslyFocusedElement = document.activeElement; - this.overlayDecoration = this.editor.decorateMarker(this.marker, { - type: 'overlay', - item: this.selectListView, - }); - process.nextTick(() => { - atom.views.readDocument(() => { - this.selectListView.focus(); - }); - }); - } + this.selectListView.element.classList.add( + 'spell-check-corrections', + 'corrections', + 'popover-list' + ); + } - async destroy() { - if (!this.destroyed) { - this.destroyed = true; - this.overlayDecoration.destroy(); - await this.selectListView.destroy(); - if (this.previouslyFocusedElement) { - this.previouslyFocusedElement.focus(); - this.previouslyFocusedElement = null; - } - } - } -} + attach() { + this.previouslyFocusedElement = document.activeElement; + this.overlayDecoration = this.editor.decorateMarker(this.marker, { + type: 'overlay', + item: this.selectListView, + }); + + process.nextTick(() => { + atom.views.readDocument(() => this.selectListView.focus()); + }); + } + + async destroy() { + if (this.destroyed) return; + this.destroyed = true; + + this.overlayDecoration.destroy(); + await this.selectListView.destroy(); + + this.previouslyFocusedElement?.focus(); + this.previouslyFocusedElement &&= null; + } +}; diff --git a/packages/spell-check/lib/known-words-checker.js b/packages/spell-check/lib/known-words-checker.js index 757eaf0c4c..2fac346f0e 100644 --- a/packages/spell-check/lib/known-words-checker.js +++ b/packages/spell-check/lib/known-words-checker.js @@ -1,93 +1,99 @@ +let SpellingManager; + class KnownWordsChecker { - static initClass() { - this.prototype.enableAdd = false; - this.prototype.spelling = null; - this.prototype.checker = null; - } + enableAdd = false; + spelling = null; + checker = null; - constructor(knownWords) { - // Set up the spelling manager we'll be using. - const spellingManager = require('spelling-manager'); - this.spelling = new spellingManager.TokenSpellingManager(); - this.checker = new spellingManager.BufferSpellingChecker(this.spelling); + static initClass() { + this.prototype.enableAdd = false; + this.prototype.spelling = null; + this.prototype.checker = null; + } - // Set our known words. - this.setKnownWords(knownWords); - } + constructor(knownWords) { + // Set up the spelling manager we'll be using. + SpellingManager ??= require('spelling-manager'); + this.spelling = new SpellingManager.TokenSpellingManager(); + this.checker = new SpellingManager.BufferSpellingChecker(this.spelling); - deactivate() {} + // Set our known words. + this.setKnownWords(knownWords); + } - getId() { - return 'spell-check:known-words'; - } - getName() { - return 'Known Words'; - } - getPriority() { - return 10; - } - isEnabled() { - return this.spelling.sensitive || this.spelling.insensitive; - } + deactivate() {} - getStatus() { - return 'Working correctly.'; - } - providesSpelling(args) { - return true; - } - providesSuggestions(args) { - return true; - } - providesAdding(args) { - return this.enableAdd; - } + getId() { + return 'spell-check:known-words'; + } + getName() { + return 'Known Words'; + } + getPriority() { + return 10; + } + isEnabled() { + return this.spelling.sensitive || this.spelling.insensitive; + } - check(args, text) { - const ranges = []; - const checked = this.checker.check(text); - const id = this.getId(); - for (let token of checked) { - if (token.status === 1) { - ranges.push({ start: token.start, end: token.end }); - } - } - return { id, correct: ranges }; - } + getStatus() { + return 'Working correctly.'; + } + providesSpelling(_) { + return true; + } + providesSuggestions(_) { + return true; + } + providesAdding(_) { + return this.enableAdd; + } - suggest(args, word) { - return this.spelling.suggest(word); + check(_, text) { + const ranges = []; + const checked = this.checker.check(text); + const id = this.getId(); + for (let token of checked) { + if (token.status === 1) { + ranges.push({ start: token.start, end: token.end }); + } } - getAddingTargets(args) { - if (this.enableAdd) { - return [{ sensitive: false, label: 'Add to ' + this.getName() }]; - } else { - return []; - } - } + return { id, correct: ranges }; + } - add(args, target) { - const c = atom.config.get('spell-check.knownWords'); - c.push(target.word); - return atom.config.set('spell-check.knownWords', c); - } + suggest(_, word) { + return this.spelling.suggest(word); + } - setAddKnownWords(newValue) { - return (this.enableAdd = newValue); + getAddingTargets(_) { + if (this.enableAdd) { + return [{ sensitive: false, label: 'Add to ' + this.getName() }]; + } else { + return []; } + } + + add(_, target) { + const c = atom.config.get('spell-check.knownWords'); + c.push(target.word); + atom.config.set('spell-check.knownWords', c); + } + + setAddKnownWords(newValue) { + this.enableAdd = newValue; + } - setKnownWords(knownWords) { - // Clear out the old list. - this.spelling.sensitive = {}; - this.spelling.insensitive = {}; + setKnownWords(knownWords) { + // Clear out the old list. + this.spelling.sensitive = {}; + this.spelling.insensitive = {}; - // Add the new ones into the list. - if (knownWords) { - return knownWords.map((ignore) => this.spelling.add(ignore)); - } + // Add the new ones into the list. + if (knownWords) { + return knownWords.map((ignore) => this.spelling.add(ignore)); } + } } -KnownWordsChecker.initClass(); module.exports = KnownWordsChecker; diff --git a/packages/spell-check/lib/locale-checker.js b/packages/spell-check/lib/locale-checker.js index 75c66d4117..9452fb418f 100644 --- a/packages/spell-check/lib/locale-checker.js +++ b/packages/spell-check/lib/locale-checker.js @@ -2,227 +2,206 @@ const spellchecker = require('spellchecker'); const pathspec = require('atom-pathspec'); const env = require('./checker-env'); +let debug; + // The locale checker is a checker that takes a locale string (`en-US`) and // optionally a path and then checks it. class LocaleChecker { - static initClass() { - this.prototype.spellchecker = null; - this.prototype.locale = null; - this.prototype.enabled = true; - this.prototype.reason = null; - this.prototype.paths = null; - this.prototype.checkDictionaryPath = true; - this.prototype.checkDefaultPaths = true; + spellchecker = null; + locale = null; + enabled = true; + reason = null; + paths = null; + checkDictionaryPath = true; + checkDefaultPaths = true; + + constructor(locale, paths, hasSystemChecker, inferredLocale) { + this.locale = locale; + this.paths = paths; + this.enabled = true; + this.hasSystemChecker = hasSystemChecker; + this.inferredLocale = inferredLocale; + if (atom.config.get('spell-check.enableDebug')) { + debug = require('debug'); + this.log = debug('spell-check:locale-checker').extend(locale); + } else { + this.log = (_str) => {}; } - - constructor(locale, paths, hasSystemChecker, inferredLocale) { - this.locale = locale; - this.paths = paths; - this.enabled = true; - this.hasSystemChecker = hasSystemChecker; - this.inferredLocale = inferredLocale; - if (atom.config.get('spell-check.enableDebug')) { - debug = require('debug'); - this.log = debug('spell-check:locale-checker').extend(locale); - } else { - this.log = (str) => {}; - } - this.log( - 'enabled', - this.isEnabled(), - 'hasSystemChecker', - this.hasSystemChecker, - 'inferredLocale', - this.inferredLocale - ); + this.log( + 'enabled', + this.isEnabled(), + 'hasSystemChecker', + this.hasSystemChecker, + 'inferredLocale', + this.inferredLocale + ); + } + + deactivate() {} + + getId() { + let localeToken = this.locale.toLowerCase().replace('_', '-'); + return `spell-check:locale:${localeToken};`; + } + getName() { + return `Locale Dictionary (${this.locale})`; + } + getPriority() { + // Hard-coded system level data; has no user input. + return 100; + } + isEnabled() { + return this.enabled; + } + getStatus() { + return this.reason; + } + providesSpelling(_) { + return this.enabled; + } + providesSuggestions(_) { + return this.enabled; + } + providesAdding(_) { + return false; + } + + async check(_, text) { + this.deferredInit(); + const id = this.getId(); + if (!this.enabled) { + return { id, status: this.getStatus() }; } - deactivate() {} - - getId() { - return ( - 'spell-check:locale:' + this.locale.toLowerCase().replace('_', '-') - ); + let incorrect = await this.spellchecker.checkSpellingAsync(text); + if (this.log.enabled) { + this.log('check', incorrect); } - getName() { - return 'Locale Dictionary (' + this.locale + ')'; + return { id, invertIncorrectAsCorrect: true, incorrect }; + } + + suggest(_, word) { + this.deferredInit(); + return this.spellchecker.getCorrectionsForMisspelling(word); + } + + deferredInit() { + // If we already have a spellchecker, then we don't have to do anything. + if (this.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. + const checker = new spellchecker.Spellchecker(); + checker.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. + const searchPaths = []; + for (let searchPath of this.paths) { + searchPaths.push(pathspec.getPath(searchPath)); } - getPriority() { - return 100; - } // Hard-coded system level data, has no user input. - isEnabled() { - return this.enabled; - } - getStatus() { - return this.reason; + + // Add operating system specific paths to the search list. + if (this.checkDefaultPaths) { + if (env.isLinux()) { + searchPaths.push('/usr/share/hunspell'); + searchPaths.push('/usr/share/myspell'); + searchPaths.push('/usr/share/myspell/dicts'); + } + + if (env.isDarwin()) { + searchPaths.push('/'); // …? + searchPaths.push('/System/Library/Spelling'); + } + + if (env.isWindows()) { + searchPaths.push('C:\\'); + } } - providesSpelling(args) { - return this.enabled; + + // Attempt to load all the paths for the dictionary until we find one. + this.log('checking paths', searchPaths); + for (let searchPath of searchPaths) { + if (checker.setDictionary(this.locale, searchPath)) { + this.log('found checker', searchPath); + this.spellchecker = checker; + return; + } } - providesSuggestions(args) { - return this.enabled; + + // On Windows, if we can't find the dictionary using the paths, then we + // also try the spelling API. This uses system checker with the given + // locale, but doesn't provide a path. We do this at the end to let + // Hunspell be used if the user provides that. + if (env.isWindows()) { + const systemChecker = new spellchecker.Spellchecker(); + systemChecker.setSpellcheckerType(spellchecker.ALWAYS_USE_SYSTEM); + if (systemChecker.setDictionary(this.locale, '')) { + this.log('using Windows Spell API'); + this.spellchecker = systemChecker; + return; + } } - providesAdding(args) { - return false; + + // If all else fails, try the packaged en-US dictionary in the `spellcheck` + // library. + if (this.checkDictionaryPath) { + if ( + checker.setDictionary(this.locale, spellchecker.getDictionaryPath()) + ) { + this.log('using packaged locale', spellchecker.getDictionaryPath()); + this.spellchecker = checker; + return; + } } - check(args, text) { - this.deferredInit(); - const id = this.getId(); - if (this.enabled) { - return this.spellchecker - .checkSpellingAsync(text) - .then((incorrect) => { - if (this.log.enabled) { - this.log('check', incorrect); - } - return { id, invertIncorrectAsCorrect: true, incorrect }; - }); - } else { - return { id, status: this.getStatus() }; - } + // If we are using the system checker and we infered the locale, then we + // don't want to show an error. This is because the system checker may have + // handled it already. + if (this.hasSystemChecker && this.inferredLocale) { + this.log( + 'giving up quietly because of system checker and inferred locale' + ); + this.enabled = false; + this.reason = `Cannot load the locale dictionary for \`${this.locale}\`. No warning because system checker is in use and locale is inferred.`; + return; } - suggest(args, word) { - this.deferredInit(); - return this.spellchecker.getCorrectionsForMisspelling(word); + // If we fell through all the `if` blocks, then we couldn't load the + // dictionary. + this.enabled = false; + this.reason = `Cannot load the locale dictionary for \`${this.locale}\`.`; + const message = `The package \`spell-check\` cannot load the checker for \`${this.locale}\`. See the settings for ways of changing the languages used, resolving missing dictionaries, or hiding this warning.`; + + let searches = `\n\nThe plugin checked the following paths for dictionary files:\n* ${searchPaths.join( + '\n* ' + )}`; + + if (!env.useLocales()) { + searches = + '\n\nThe plugin tried to use the system dictionaries to find the locale.'; } - deferredInit() { - // If we already have a spellchecker, then we don't have to do anything. - let path; - if (this.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. - const checker = new spellchecker.Spellchecker(); - checker.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. - const searchPaths = []; - for (path of this.paths) { - searchPaths.push(pathspec.getPath(path)); - } - - // Add operating system specific paths to the search list. - if (this.checkDefaultPaths) { - if (env.isLinux()) { - searchPaths.push('/usr/share/hunspell'); - searchPaths.push('/usr/share/myspell'); - searchPaths.push('/usr/share/myspell/dicts'); - } - - if (env.isDarwin()) { - searchPaths.push('/'); - searchPaths.push('/System/Library/Spelling'); - } - - if (env.isWindows()) { - searchPaths.push('C:\\'); - } - } - - // Attempt to load all the paths for the dictionary until we find one. - this.log('checking paths', searchPaths); - for (path of searchPaths) { - if (checker.setDictionary(this.locale, path)) { - this.log('found checker', path); - this.spellchecker = checker; - return; - } - } - - // On Windows, if we can't find the dictionary using the paths, then we also - // try the spelling API. This uses system checker with the given locale, but - // doesn't provide a path. We do this at the end to let Hunspell be used if - // the user provides that. - if (env.isWindows()) { - const systemChecker = new spellchecker.Spellchecker(); - systemChecker.setSpellcheckerType(spellchecker.ALWAYS_USE_SYSTEM); - if (systemChecker.setDictionary(this.locale, '')) { - this.log('using Windows Spell API'); - this.spellchecker = systemChecker; - return; - } - } - - // If all else fails, try the packaged en-US dictionary in the `spellcheck` - // library. - if (this.checkDictionaryPath) { - if ( - checker.setDictionary( - this.locale, - spellchecker.getDictionaryPath() - ) - ) { - this.log('using packaged locale', path); - this.spellchecker = checker; - return; - } - } - - // If we are using the system checker and we infered the locale, then we - // don't want to show an error. This is because the system checker may have - // handled it already. - if (this.hasSystemChecker && this.inferredLocale) { - this.log( - 'giving up quietly because of system checker and inferred locale' - ); - this.enabled = false; - this.reason = - 'Cannot load the locale dictionary for `' + - this.locale + - '`. No warning because system checker is in use and locale is inferred.'; - return; - } - - // If we fell through all the if blocks, then we couldn't load the dictionary. - this.enabled = false; - this.reason = - 'Cannot load the locale dictionary for `' + this.locale + '`.'; - const message = - 'The package `spell-check` cannot load the ' + - 'checker for `' + - this.locale + - '`.' + - ' See the settings for ways of changing the languages used, ' + - ' resolving missing dictionaries, or hiding this warning.'; - - let searches = - '\n\nThe plugin checked the following paths for dictionary files:\n* ' + - searchPaths.join('\n* '); - - if (!env.useLocales()) { - searches = - '\n\nThe plugin tried to use the system dictionaries to find the locale.'; - } - - const noticesMode = atom.config.get('spell-check.noticesMode'); - - if (noticesMode === 'console' || noticesMode === 'both') { - console.log(this.getId(), message + searches); - } - if (noticesMode === 'popup' || noticesMode === 'both') { - return atom.notifications.addWarning(message, { - buttons: [ - { - className: 'btn', - onDidClick() { - return atom.workspace.open( - 'atom://config/packages/spell-check' - ); - }, - text: 'Settings', - }, - ], - }); - } + const noticesMode = atom.config.get('spell-check.noticesMode'); + + if (noticesMode === 'console' || noticesMode === 'both') { + console.log(this.getId(), message + searches); + } + if (noticesMode === 'popup' || noticesMode === 'both') { + atom.notifications.addWarning(message, { + buttons: [ + { + className: 'btn', + onDidClick() { + return atom.workspace.open('atom://config/packages/spell-check'); + }, + text: 'Settings', + }, + ], + }); } + } } -LocaleChecker.initClass(); module.exports = LocaleChecker; diff --git a/packages/spell-check/lib/main.js b/packages/spell-check/lib/main.js index 8803a23c25..de06cdde96 100644 --- a/packages/spell-check/lib/main.js +++ b/packages/spell-check/lib/main.js @@ -5,263 +5,214 @@ let spellCheckViews = {}; const LARGE_FILE_SIZE = 2 * 1024 * 1024; -let log = (str) => {}; +let debug; +let log = (_str) => {}; module.exports = { - activate() { - if (atom.config.get('spell-check.enableDebug')) { - debug = require('debug'); - log = debug('spell-check'); + activate() { + if (atom.config.get('spell-check.enableDebug')) { + debug ??= require('debug'); + log = debug('spell-check'); + } + + log('initializing'); + + this.subs = new CompositeDisposable(); + + // 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. + this.globalArgs = { + // 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'), + + // Collection of all the absolute paths to checkers which will be + // `require`d on the process side to load the checker. We have to do this + // because we can't pass the actual objects from the main Pulsar process + // to the background safely. + checkerPaths: [], + }; + + const manager = this.getInstance(this.globalArgs); + + // Hook up changes to the configuration settings. + this.excludedScopeRegexLists = []; + this.subs.add( + atom.config.onDidChange('spell-check.excludedScopes', () => { + return this.updateViews(); + }) + ); + + this.subs.add( + atom.config.onDidChange('spell-check.locales', ({ newValue }) => { + this.globalArgs.locales = newValue; + manager.setGlobalArgs(this.globalArgs); + }), + atom.config.onDidChange('spell-check.localePaths', ({ newValue }) => { + this.globalArgs.localePaths = newValue; + manager.setGlobalArgs(this.globalArgs); + }), + atom.config.onDidChange('spell-check.useSystem', ({ newValue }) => { + this.globalArgs.useSystem = newValue; + manager.setGlobalArgs(this.globalArgs); + }), + atom.config.onDidChange('spell-check.useLocales', ({ newValue }) => { + this.globalArgs.useLocales = newValue; + manager.setGlobalArgs(this.globalArgs); + }), + atom.config.onDidChange('spell-check.knownWords', ({ newValue }) => { + this.globalArgs.knownWords = newValue; + manager.setGlobalArgs(this.globalArgs); + }), + atom.config.onDidChange('spell-check.addKnownWords', ({ newValue }) => { + this.globalArgs.addKnownWords = newValue; + manager.setGlobalArgs(this.globalArgs); + }), + // Hook up the UI and processing. + atom.commands.add('atom-workspace', { + 'spell-check:toggle': () => this.toggle(), + }) + ); + + this.viewsByEditor = new WeakMap(); + this.contextMenuEntries = []; + + this.subs.add( + atom.workspace.observeTextEditors((editor) => { + if (this.viewsByEditor.has(editor)) { + return; } - log('initializing'); - - this.subs = new CompositeDisposable(); - - // 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. - this.globalArgs = { - // 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'), - - // Collection of all the absolute paths to checkers which will be - // `require` on the process side to load the checker. We have to do this - // because we can't pass the actual objects from the main Atom process to - // the background safely. - checkerPaths: [], - }; - - const manager = this.getInstance(this.globalArgs); - - // Hook up changes to the configuration settings. - this.excludedScopeRegexLists = []; - this.subs.add( - atom.config.onDidChange('spell-check.excludedScopes', () => { - return this.updateViews(); - }) - ); - - this.subs.add( - atom.config.onDidChange( - 'spell-check.locales', - ({ newValue, oldValue }) => { - this.globalArgs.locales = newValue; - return manager.setGlobalArgs(this.globalArgs); - } - ) - ); - this.subs.add( - atom.config.onDidChange( - 'spell-check.localePaths', - ({ newValue, oldValue }) => { - this.globalArgs.localePaths = newValue; - return manager.setGlobalArgs(this.globalArgs); - } - ) - ); - this.subs.add( - atom.config.onDidChange( - 'spell-check.useSystem', - ({ newValue, oldValue }) => { - this.globalArgs.useSystem = newValue; - return manager.setGlobalArgs(this.globalArgs); - } - ) - ); - this.subs.add( - atom.config.onDidChange( - 'spell-check.useLocales', - ({ newValue, oldValue }) => { - this.globalArgs.useLocales = newValue; - return manager.setGlobalArgs(this.globalArgs); - } - ) - ); - this.subs.add( - atom.config.onDidChange( - 'spell-check.knownWords', - ({ newValue, oldValue }) => { - this.globalArgs.knownWords = newValue; - return manager.setGlobalArgs(this.globalArgs); - } - ) - ); - this.subs.add( - atom.config.onDidChange( - 'spell-check.addKnownWords', - ({ newValue, oldValue }) => { - this.globalArgs.addKnownWords = newValue; - return manager.setGlobalArgs(this.globalArgs); - } - ) - ); - - // Hook up the UI and processing. - this.subs.add( - atom.commands.add('atom-workspace', { - 'spell-check:toggle': () => this.toggle(), - }) - ); - this.viewsByEditor = new WeakMap(); - this.contextMenuEntries = []; - return this.subs.add( - atom.workspace.observeTextEditors((editor) => { - if (this.viewsByEditor.has(editor)) { - return; - } - - // For now, just don't spell check large files. - if (editor.getBuffer().getLength() > LARGE_FILE_SIZE) { - return; - } - - // Defer loading the spell check view if we actually need it. This also - // avoids slowing down Atom's startup by getting it loaded on demand. - if (SpellCheckView == null) { - SpellCheckView = require('./spell-check-view'); - } - - // 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. - // Each view also needs the list of added context menu entries so that - // they can dispose old corrections which were not created by the current - // active editor. A reference to this entire module is passed right now - // because a direct reference to @contextMenuEntries wasn't updating - // properly between different SpellCheckView's. - const spellCheckView = new SpellCheckView( - editor, - this, - manager - ); - - // save the {editor} into a map - const editorId = editor.id; - spellCheckViews[editorId] = { - view: spellCheckView, - active: true, - editor, - }; - - // Make sure that the view is cleaned up on editor destruction. - var destroySub = editor.onDidDestroy(() => { - spellCheckView.destroy(); - delete spellCheckViews[editorId]; - return this.subs.remove(destroySub); - }); - this.subs.add(destroySub); - - return this.viewsByEditor.set(editor, spellCheckView); - }) - ); - }, - - deactivate() { - if (this.instance != null) { - this.instance.deactivate(); - } - this.instance = null; - - // Clear out the known views. - for (let editorId in spellCheckViews) { - const { view } = spellCheckViews[editorId]; - view.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. - this.viewsByEditor = new WeakMap(); - - // Finish up by disposing everything else associated with the plugin. - return this.subs.dispose(); - }, - - // Registers any Atom packages that provide our service. - consumeSpellCheckers(checkerPaths) { - // Normalize it so we always have an array. - if (!(checkerPaths instanceof Array)) { - checkerPaths = [checkerPaths]; + // For now, just don't spell check large files. + if (editor.getBuffer().getLength() > LARGE_FILE_SIZE) { + return; } - // Go through and add any new plugins to the list. - return (() => { - const result = []; - for (let checkerPath of checkerPaths) { - if (!this.globalArgs.checkerPaths.includes(checkerPath)) { - if (this.instance != null) { - this.instance.addCheckerPath(checkerPath); - } - result.push(this.globalArgs.checkerPaths.push(checkerPath)); - } else { - result.push(undefined); - } - } - return result; - })(); - }, - - misspellingMarkersForEditor(editor) { - return this.viewsByEditor.get(editor).markerLayer.getMarkers(); - }, - - updateViews() { - return (() => { - const result = []; - for (let editorId in spellCheckViews) { - const view = spellCheckViews[editorId]; - if (view['active']) { - result.push(view['view'].updateMisspellings()); - } else { - result.push(undefined); - } - } - return result; - })(); - }, - - // Retrieves, creating if required, the single SpellingManager instance. - getInstance(globalArgs) { - if (!this.instance) { - const SpellCheckerManager = require('./spell-check-manager'); - this.instance = SpellCheckerManager; - this.instance.setGlobalArgs(globalArgs); - - for (let checkerPath of globalArgs.checkerPaths) { - this.instance.addCheckerPath(checkerPath); - } - } - - return this.instance; - }, - - // Internal: Toggles the spell-check activation state. - toggle() { - if (!atom.workspace.getActiveTextEditor()) { - return; - } - const editorId = atom.workspace.getActiveTextEditor().id; - - if (!spellCheckViews.hasOwnProperty(editorId)) { - // The editor was never registered with a view, ignore it - return; - } + // Defer loading the spell check view until we actually need it. This + // also avoids slowing down Pulsar's startup by getting it loaded on + // demand. + SpellCheckView ??= require('./spell-check-view'); + + // 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. + // + // Each view also needs the list of added context menu entries so that + // they can dispose old corrections which were not created by the + // current active editor. A reference to this entire module is passed + // right now because a direct reference to `contextMenuEntries` wasn't + // updating properly between different `SpellCheckView`s. + const spellCheckView = new SpellCheckView(editor, this, manager); + + // Save the editor into a map. + const editorId = editor.id; + spellCheckViews[editorId] = { + view: spellCheckView, + active: true, + editor, + }; - if (spellCheckViews[editorId]['active']) { - // deactivate spell check for this {editor} - spellCheckViews[editorId]['active'] = false; - return spellCheckViews[editorId]['view'].unsubscribeFromBuffer(); - } else { - // activate spell check for this {editor} - spellCheckViews[editorId]['active'] = true; - return spellCheckViews[editorId]['view'].subscribeToBuffer(); - } - }, + // Make sure that the view is cleaned up on editor destruction. + let destroySub = editor.onDidDestroy(() => { + spellCheckView.destroy(); + delete spellCheckViews[editorId]; + this.subs.remove(destroySub); + }); + this.subs.add(destroySub); + + this.viewsByEditor.set(editor, spellCheckView); + }) + ); + }, + + deactivate() { + this.instance?.deactivate(); + this.instance &&= null; + + // Clear out the known views. + for (let editorId in spellCheckViews) { + const { view } = spellCheckViews[editorId]; + view.destroy(); + } + spellCheckViews = {}; + + this.viewsByEditor = new WeakMap(); + + // Finish up by disposing everything else associated with the plugin. + return this.subs.dispose(); + }, + + // Registers any Pulsar packages that provide our service. + consumeSpellCheckers(checkerPaths) { + // Normalize it so we always have an array. + if (!Array.isArray(checkerPaths)) { + checkerPaths = [checkerPaths]; + } + + for (let checkerPath of checkerPaths) { + if (!this.globalArgs.checkerPaths.includes(checkerPath)) { + this.instance?.addCheckerPath(checkerPath); + this.globalArgs.checkerPaths.push(checkerPath); + } + } + }, + + misspellingMarkersForEditor(editor) { + return this.viewsByEditor.get(editor).markerLayer.getMarkers(); + }, + + updateViews() { + let bundles = Object.values(spellCheckViews); + for (let bundle of bundles) { + if (bundle.active) { + bundle.view.updateMisspellings(); + } + } + return bundles; + }, + + // Retrieves, creating if required, the single SpellingManager instance. + getInstance(globalArgs) { + if (!this.instance) { + this.instance = require('./spell-check-manager'); + this.instance.setGlobalArgs(globalArgs); + + for (let checkerPath of globalArgs.checkerPaths) { + this.instance.addCheckerPath(checkerPath); + } + } + + return this.instance; + }, + + // Internal: Toggles the spell-check activation state. + toggle() { + let activeEditor = atom.workspace.getActiveTextEditor(); + if (!activeEditor) return; + const editorId = activeEditor.id; + + // eslint-disable-next-line no-prototype-builtins + if (!spellCheckViews.hasOwnProperty(editorId)) { + // The editor was never registered with a view, so ignore it. + return; + } + + let bundle = spellCheckViews[editorId]; + + if (bundle.active) { + // Deactivate spell check for this editor. + bundle.active = false; + bundle.view.unsubscribeFromBuffer(); + } else { + // Activate spell check for this editor. + bundle.active = true; + bundle.view.subscribeToBuffer(); + } + }, }; diff --git a/packages/spell-check/lib/scope-helper.js b/packages/spell-check/lib/scope-helper.js index 31c526ba7c..2ca71bfb9e 100644 --- a/packages/spell-check/lib/scope-helper.js +++ b/packages/spell-check/lib/scope-helper.js @@ -1,108 +1,104 @@ function normalizeSegment(segment) { - if (!segment.startsWith('.')) return segment; - return segment.substring(1); + if (!segment.startsWith('.')) return segment; + return segment.substring(1); } function segmentsMatch( - descriptorSegment, - selectorSegment, - { enforceSegmentOrder = false } = {} + descriptorSegment, + selectorSegment, + { enforceSegmentOrder = false } = {} ) { - let descriptorParts = normalizeSegment(descriptorSegment).split('.'); - let selectorParts = normalizeSegment(selectorSegment).split('.'); + let descriptorParts = normalizeSegment(descriptorSegment).split('.'); + let selectorParts = normalizeSegment(selectorSegment).split('.'); - if (selectorParts.length > descriptorParts.length) { - return false; - } + if (selectorParts.length > descriptorParts.length) { + return false; + } - // Remove all parts from the descriptor scope name that aren't present in the - // selector scope name. - for (let i = descriptorParts.length - 1; i >= 0; i--) { - let part = descriptorParts[i]; - if (!selectorParts.includes(part)) { - descriptorParts.splice(i, 1); - } + // Remove all parts from the descriptor scope name that aren't present in the + // selector scope name. + for (let i = descriptorParts.length - 1; i >= 0; i--) { + let part = descriptorParts[i]; + if (!selectorParts.includes(part)) { + descriptorParts.splice(i, 1); } - // Does order matter? It would if this were a TextMate scope, but Atom has - // broadly treated `.function.entity` as equivalent to `.entity.function`, - // even though it causes headaches in some places. - // - // We'll assume that order doesn't matter, but the user can opt into strict - // ordering if they want. - if (!enforceSegmentOrder) { - descriptorParts.sort(); - selectorParts.sort(); - } - return descriptorParts.join('.') === selectorParts.join('.'); + } + // Does order matter? It would if this were a TextMate scope, but Atom has + // broadly treated `.function.entity` as equivalent to `.entity.function`, + // even though it causes headaches in some places. + // + // We'll assume that order doesn't matter, but the user can opt into strict + // ordering if they want. + if (!enforceSegmentOrder) { + descriptorParts.sort(); + selectorParts.sort(); + } + return descriptorParts.join('.') === selectorParts.join('.'); } class ScopeSelector { - static create(stringOrScopeSelector) { - if (typeof stringOrScopeSelector === 'string') { - return new ScopeSelector(stringOrScopeSelector); - } else if (stringOrScopeSelector instanceof ScopeSelector) { - return stringOrScopeSelector; - } else { - throw new TypeError(`Invalid argument`); - } + static create(stringOrScopeSelector) { + if (typeof stringOrScopeSelector === 'string') { + return new ScopeSelector(stringOrScopeSelector); + } else if (stringOrScopeSelector instanceof ScopeSelector) { + return stringOrScopeSelector; + } else { + throw new TypeError(`Invalid argument`); } + } - constructor(selectorString) { - this.selectorString = selectorString; - this.variations = selectorString.split(/,\s*/); - } - - matches(scopeDescriptorOrArray, rawOptions = {}) { - let options = { - // Whether to treat (e.g.) `.function.entity` as distinct from - // `.entity.function`. Defaults to `false` to match prevailing Atom - // behavior. - enforceSegmentOrder: false, - ...rawOptions, - }; - let scopeList; - if (Array.isArray(scopeDescriptorOrArray)) { - scopeList = scopeDescriptorOrArray; - } else { - scopeList = scopeDescriptorOrArray.getScopesArray(); - } + constructor(selectorString) { + this.selectorString = selectorString; + this.variations = selectorString.split(/,\s*/); + } - return this.variations.some((variation) => - this.matchesVariation(scopeList, variation, options) - ); + matches(scopeDescriptorOrArray, rawOptions = {}) { + let options = { + // Whether to treat (e.g.) `.function.entity` as distinct from + // `.entity.function`. Defaults to `false` to match prevailing Atom + // behavior. + enforceSegmentOrder: false, + ...rawOptions, + }; + let scopeList; + if (Array.isArray(scopeDescriptorOrArray)) { + scopeList = scopeDescriptorOrArray; + } else { + scopeList = scopeDescriptorOrArray.getScopesArray(); } - matchesVariation(scopeList, selectorString, options) { - let parts = selectorString.split(/\s+/); - if (parts.length > scopeList.length) return false; + return this.variations.some((variation) => + this.matchesVariation(scopeList, variation, options) + ); + } + + matchesVariation(scopeList, selectorString, options) { + let parts = selectorString.split(/\s+/); + if (parts.length > scopeList.length) return false; - let lastIndex = -1; + let lastIndex = -1; - outer: for (let selectorPart of parts) { - // Find something in the descriptor that matches this selector part. - for (let [i, descriptorPart] of scopeList.entries()) { - // Ignore everything before our index cursor; this is what enforces the - // ordering of the scope selector. - if (i <= lastIndex) continue; - let doesMatch = segmentsMatch( - descriptorPart, - selectorPart, - options - ); - if (doesMatch) { - lastIndex = i; - continue outer; - } - } - // If we get this far, we searched the entire descriptor list for a - // selector part and failed to find it; hence this variation doesn't - // match. - return false; + outer: for (let selectorPart of parts) { + // Find something in the descriptor that matches this selector part. + for (let [i, descriptorPart] of scopeList.entries()) { + // Ignore everything before our index cursor; this is what enforces the + // ordering of the scope selector. + if (i <= lastIndex) continue; + let doesMatch = segmentsMatch(descriptorPart, selectorPart, options); + if (doesMatch) { + lastIndex = i; + continue outer; } - // If we get this far, we made it through the entire gauntlet without - // hitting the early return. This variation matches! - return true; + } + // If we get this far, we searched the entire descriptor list for a + // selector part and failed to find it; hence this variation doesn't + // match. + return false; } + // If we get this far, we made it through the entire gauntlet without + // hitting the early return. This variation matches! + return true; + } } // Private: A candidate for possible addition to the {ScopeDescriptor} API. @@ -145,10 +141,10 @@ class ScopeSelector { // - `scopeDescriptor` A {ScopeDescriptor} or a scope descriptor {Array}. // - `selector` A {String} representing a scope selector. function scopeDescriptorMatchesSelector(scopeDescriptor, selector) { - let scopeSelector = ScopeSelector.create(selector); - return scopeSelector.matches(scopeDescriptor); + let scopeSelector = ScopeSelector.create(selector); + return scopeSelector.matches(scopeDescriptor); } module.exports = { - scopeDescriptorMatchesSelector, + scopeDescriptorMatchesSelector, }; diff --git a/packages/spell-check/lib/spell-check-manager.js b/packages/spell-check/lib/spell-check-manager.js index 2c50c3e551..224e4b400b 100644 --- a/packages/spell-check/lib/spell-check-manager.js +++ b/packages/spell-check/lib/spell-check-manager.js @@ -1,544 +1,554 @@ const env = require('./checker-env'); +let _; +let KnownWordsChecker; +let LocaleChecker; +let SystemChecker; + class SpellCheckerManager { - static initClass() { - this.prototype.checkers = []; - this.prototype.checkerPaths = []; - this.prototype.locales = []; - this.prototype.localePaths = []; - this.prototype.useLocales = false; - this.prototype.systemChecker = null; - this.prototype.knownWordsChecker = null; - this.prototype.localeCheckers = null; - this.prototype.knownWords = []; - this.prototype.addKnownWords = false; + checkers = []; + checkerPaths = []; + locales = []; + localePaths = []; + useLocales = false; + + systemChecker = null; + knownWordsChecker = null; + localeCheckers = null; + + knownWords = []; + addKnownWords = false; + + setGlobalArgs(data) { + // We need underscore to do the array comparisons. + _ ??= require('underscore-plus'); + + // 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 bundled: + // + // - 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. + let removeKnownWordsChecker = false; + + if (!_.isEqual(this.knownWords, data.knownWords)) { + this.knownWords = data.knownWords; + removeKnownWordsChecker = true; } - - setGlobalArgs(data) { - // We need underscore to do the array comparisons. - const _ = require('underscore-plus'); - - // 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. - let removeKnownWordsChecker = false; - - if (!_.isEqual(this.knownWords, data.knownWords)) { - this.knownWords = data.knownWords; - removeKnownWordsChecker = true; - } - if (this.addKnownWords !== data.addKnownWords) { - this.addKnownWords = data.addKnownWords; - removeKnownWordsChecker = true; - } - - if (removeKnownWordsChecker && this.knownWordsChecker) { - this.removeSpellChecker(this.knownWordsChecker); - this.knownWordsChecker = null; - } - - // Handle system checker. We also will remove the locale checkers if we - // change the system checker because we show different messages if we cannot - // find a locale based on the use of the system checker. - let removeSystemChecker = false; - let removeLocaleCheckers = false; - - if (this.useSystem !== data.useSystem) { - this.useSystem = data.useSystem; - removeSystemChecker = true; - removeLocaleCheckers = true; - } - - if (removeSystemChecker && this.systemChecker) { - this.removeSpellChecker(this.systemChecker); - this.systemChecker = undefined; - } - - // Handle locale checkers. - if (!_.isEqual(this.locales, data.locales)) { - // If the locales is blank, then we always create a default one. However, - // any new data.locales will remain blank. - if ( - !this.localeCheckers || - (data.locales != null ? data.locales.length : undefined) !== 0 - ) { - this.locales = data.locales; - removeLocaleCheckers = true; - } - } - if (!_.isEqual(this.localePaths, data.localePaths)) { - this.localePaths = data.localePaths; - removeLocaleCheckers = true; - } - if (this.useLocales !== data.useLocales) { - this.useLocales = data.useLocales; - removeLocaleCheckers = true; - } - - if (removeLocaleCheckers && this.localeCheckers) { - const checkers = this.localeCheckers; - for (let checker of checkers) { - this.removeSpellChecker(checker); - } - return (this.localeCheckers = null); - } + if (this.addKnownWords !== data.addKnownWords) { + this.addKnownWords = data.addKnownWords; + removeKnownWordsChecker = true; } - addCheckerPath(checkerPath) { - // Load the given path via require. - let checker = require(checkerPath); + if (removeKnownWordsChecker && this.knownWordsChecker) { + this.removeSpellChecker(this.knownWordsChecker); + this.knownWordsChecker = null; + } - // If this a ES6 module, then we need to construct it. We require - // the coders to export it as `default` since we don't have another - // way of figuring out which object to instantiate. - if (checker.default) { - checker = new checker.default(); - } + // Handle system checker. We also will remove the locale checkers if we + // change the system checker because we show different messages if we + // cannot find a locale based on the use of the system checker. + let removeSystemChecker = false; + let removeLocaleCheckers = false; - // Add in the resulting checker. - return this.addPluginChecker(checker); + if (this.useSystem !== data.useSystem) { + this.useSystem = data.useSystem; + removeSystemChecker = true; + removeLocaleCheckers = true; } - addPluginChecker(checker) { - // Add the spell checker to the list. - return this.addSpellChecker(checker); + if (removeSystemChecker && this.systemChecker) { + this.removeSpellChecker(this.systemChecker); + this.systemChecker = undefined; } - addSpellChecker(checker) { - return this.checkers.push(checker); + // Handle locale checkers. + if (!_.isEqual(this.locales, data.locales)) { + // If the locales is blank, then we always create a default one. However, + // any new data.locales will remain blank. + if ( + !this.localeCheckers || + (data.locales != null ? data.locales.length : undefined) !== 0 + ) { + this.locales = data.locales; + removeLocaleCheckers = true; + } } - - removeSpellChecker(spellChecker) { - return (this.checkers = this.checkers.filter( - (plugin) => plugin !== spellChecker - )); + if (!_.isEqual(this.localePaths, data.localePaths)) { + this.localePaths = data.localePaths; + removeLocaleCheckers = true; + } + if (this.useLocales !== data.useLocales) { + this.useLocales = data.useLocales; + removeLocaleCheckers = true; } - check(args, text) { - // Make sure our deferred initialization is done. - this.init(); - - // We need a couple packages but we want to lazy load them to - // reduce load time. - const multirange = require('multi-integer-range'); - - // For every registered spellchecker, we need to find out the ranges in the - // text that the checker confirms are correct or indicates is a misspelling. - // We keep these as separate lists since the different checkers may indicate - // the same range for either and we need to be able to remove confirmed words - // from the misspelled ones. - const correct = new multirange.MultiRange([]); - const incorrects = []; - const promises = []; - - for (let checker of this.checkers) { - // We only care if this plugin contributes to checking spelling. - if (!checker.isEnabled() || !checker.providesSpelling(args)) { - continue; - } - - // Get the possibly asynchronous results which include positive - // (correct) and negative (incorrect) ranges. If we have an incorrect - // range but no correct, everything not in incorrect is considered correct. - promises.push(Promise.resolve(checker.check(args, text))); - } - - return Promise.all(promises).then((allResults) => { - let range; - if (this.log.enabled) { - this.log('check results', allResults, text); - } - - for (let results of allResults) { - if (results.invertIncorrectAsCorrect && results.incorrect) { - // We need to add the opposite of the incorrect as correct elements in - // the list. We do this by creating a subtraction. - const invertedCorrect = new multirange.MultiRange([ - [0, text.length], - ]); - const removeRange = new multirange.MultiRange([]); - for (range of results.incorrect) { - removeRange.appendRange(range.start, range.end); - } - invertedCorrect.subtract(removeRange); - - // Everything in `invertedCorrect` is correct, so add it directly to - // the list. - correct.append(invertedCorrect); - } else if (results.correct) { - for (range of results.correct) { - correct.appendRange(range.start, range.end); - } - } - - if (results.incorrect) { - const newIncorrect = new multirange.MultiRange([]); - incorrects.push(newIncorrect); - - for (range of results.incorrect) { - newIncorrect.appendRange(range.start, range.end); - } - } - } - - // If we don't have any incorrect spellings, then there is nothing to worry - // about, so just return and stop processing. - if (this.log.enabled) { - this.log('merged correct ranges', correct); - this.log('merged incorrect ranges', incorrects); - } - - if (incorrects.length === 0) { - this.log('no spelling errors'); - return { misspellings: [] }; - } - - // Build up an intersection of all the incorrect ranges. We only treat a word - // as being incorrect if *every* checker that provides negative values treats - // it as incorrect. We know there are at least one item in this list, so pull - // that out. If that is the only one, we don't have to do any additional work, - // otherwise we compare every other one against it, removing any elements - // that aren't an intersection which (hopefully) will produce a smaller list - // with each iteration. - let intersection = null; - - for (let incorrect of incorrects) { - if (intersection === null) { - intersection = incorrect; - } else { - intersection.append(incorrect); - } - } - - // If we have no intersection, then nothing to report as a problem. - if (intersection.length === 0) { - this.log('no spelling after intersections'); - return { misspellings: [] }; - } - - // Remove all of the confirmed correct words from the resulting incorrect - // list. This allows us to have correct-only providers as opposed to only - // incorrect providers. - if (correct.ranges.length > 0) { - intersection.subtract(correct); - } - - if (this.log.enabled) { - this.log('check intersections', intersection); - } + if (removeLocaleCheckers && this.localeCheckers) { + const checkers = this.localeCheckers; + for (let checker of checkers) { + this.removeSpellChecker(checker); + } + this.localeCheckers = null; + } + } - // Convert the text ranges (index into the string) into Atom buffer - // coordinates ( row and column). - let row = 0; - let rangeIndex = 0; - let lineBeginIndex = 0; - const misspellings = []; - while ( - lineBeginIndex < text.length && - rangeIndex < intersection.ranges.length - ) { - // Figure out where the next line break is. If we hit -1, then we make sure - // it is a higher number so our < comparisons work properly. - let lineEndIndex = text.indexOf('\n', lineBeginIndex); - if (lineEndIndex === -1) { - lineEndIndex = Infinity; - } - - // Loop through and get all the ranegs for this line. - while (true) { - range = intersection.ranges[rangeIndex]; - if (range && range[0] < lineEndIndex) { - // Figure out the character range of this line. We need this because - // @addMisspellings doesn't handle jumping across lines easily and the - // use of the number ranges is inclusive. - const lineRange = new multirange.MultiRange( - [] - ).appendRange(lineBeginIndex, lineEndIndex); - const rangeRange = new multirange.MultiRange( - [] - ).appendRange(range[0], range[1]); - lineRange.intersect(rangeRange); - - // The range we have here includes whitespace between two concurrent - // tokens ("zz zz zz" shows up as a single misspelling). The original - // version would split the example into three separate ones, so we - // do the same thing, but only for the ranges within the line. - this.addMisspellings( - misspellings, - row, - lineRange.ranges[0], - lineBeginIndex, - text - ); - - // If this line is beyond the limits of our current range, we move to - // the next one, otherwise we loop again to reuse this range against - // the next line. - if (lineEndIndex >= range[1]) { - rangeIndex++; - } else { - break; - } - } else { - break; - } - } - - lineBeginIndex = lineEndIndex + 1; - row++; - } + addCheckerPath(checkerPath) { + // Load the given path via require. + let checker = require(checkerPath); - // Return the resulting misspellings. - return { misspellings }; - }); + // If this a ES6 module, then we need to construct it. We require + // the coders to export it as `default` since we don't have another + // way of figuring out which object to instantiate. + if (checker.default) { + checker = new checker.default(); } - suggest(args, word) { - // Make sure our deferred initialization is done. - let checker, index, key, priority, suggestion; - this.init(); - - // Gather up a list of corrections and put them into a custom object that has - // the priority of the plugin, the index in the results, and the word itself. - // We use this to intersperse the results together to avoid having the - // preferred answer for the second plugin below the least preferred of the - // first. - const suggestions = []; - - for (checker of this.checkers) { - // We only care if this plugin contributes to checking to suggestions. - if (!checker.isEnabled() || !checker.providesSuggestions(args)) { - continue; - } + // Add in the resulting checker. + return this.addPluginChecker(checker); + } + + addPluginChecker(checker) { + // Add the spell checker to the list. + return this.addSpellChecker(checker); + } + + addSpellChecker(checker) { + return this.checkers.push(checker); + } + + removeSpellChecker(spellChecker) { + let index = this.checkers.findIndex((c) => c === spellChecker); + if (index === -1) return false; + this.checkers.splice(index, 1); + return true; + } + + async check(args, text) { + // Make sure our deferred initialization is done. + this.init(); + + // We need a couple packages but we want to lazy load them to + // reduce load time. + const multirange = require('multi-integer-range'); + + // For every registered spellchecker, we need to find out the ranges in the + // text that the checker confirms are correct or indicates is a misspelling. + // We keep these as separate lists since the different checkers may indicate + // the same range for either and we need to be able to remove confirmed words + // from the misspelled ones. + const correct = new multirange.MultiRange([]); + const incorrects = []; + const promises = []; + + for (let checker of this.checkers) { + // We only care if this plugin contributes to checking spelling. + if (!checker.isEnabled() || !checker.providesSpelling(args)) { + continue; + } + + // Get the possibly asynchronous results which include positive (correct) + // and negative (incorrect) ranges. If we have an incorrect range but no + // correct, everything not in incorrect is considered correct. + promises.push(Promise.resolve(checker.check(args, text))); + } - // Get the suggestions for this word. - index = 0; - priority = checker.getPriority(); - - for (suggestion of checker.suggest(args, word)) { - suggestions.push({ - isSuggestion: true, - priority, - index: index++, - suggestion, - label: suggestion, - }); - } + try { + let allResults = await Promise.all(promises); + if (this.log.enabled) { + this.log('check results', allResults, text); + } + + for (let results of allResults) { + if (results.invertIncorrectAsCorrect && results.incorrect) { + // We need to add the opposite of the incorrect as correct elements + // in the list. We do this by creating a subtraction. + const invertedCorrect = new multirange.MultiRange([[0, text.length]]); + const removeRange = new multirange.MultiRange([]); + for (let range of results.incorrect) { + removeRange.appendRange(range.start, range.end); + } + + invertedCorrect.subtract(removeRange); + + // Everything in `invertedCorrect` is correct, so add it directly to + // the list. + correct.append(invertedCorrect); + } else if (results.correct) { + for (let range of results.correct) { + correct.appendRange(range.start, range.end); + } } - // Once we have the suggestions, then sort them to intersperse the results. - let keys = Object.keys(suggestions).sort(function (key1, key2) { - const value1 = suggestions[key1]; - const value2 = suggestions[key2]; - const weight1 = value1.priority + value1.index; - const weight2 = value2.priority + value2.index; + if (results.incorrect) { + const newIncorrect = new multirange.MultiRange([]); + incorrects.push(newIncorrect); - if (weight1 !== weight2) { - return weight1 - weight2; - } - - return value1.suggestion.localeCompare(value2.suggestion); - }); - - // Go through the keys and build the final list of suggestions. As we go - // through, we also want to remove duplicates. - const results = []; - const seen = []; - for (key of keys) { - const s = suggestions[key]; - if (seen.hasOwnProperty(s.suggestion)) { - continue; - } - results.push(s); - seen[s.suggestion] = 1; + for (let range of results.incorrect) { + newIncorrect.appendRange(range.start, range.end); + } } - - // We also grab the "add to dictionary" listings. - const that = this; - keys = Object.keys(this.checkers).sort(function (key1, key2) { - const value1 = that.checkers[key1]; - const value2 = that.checkers[key2]; - return value1.getPriority() - value2.getPriority(); - }); - - for (key of keys) { - // We only care if this plugin contributes to checking to suggestions. - checker = this.checkers[key]; - if (!checker.isEnabled() || !checker.providesAdding(args)) { - continue; - } - - // Add all the targets to the list. - const targets = checker.getAddingTargets(args); - for (let target of targets) { - target.plugin = checker; - target.word = word; - target.isSuggestion = false; - results.push(target); - } + } + + // If we don't have any incorrect spellings, then there is nothing to worry + // about, so just return and stop processing. + if (this.log.enabled) { + this.log('merged correct ranges', correct); + this.log('merged incorrect ranges', incorrects); + } + + if (incorrects.length === 0) { + this.log('no spelling errors'); + return { misspellings: [] }; + } + + // Build up an intersection of all the incorrect ranges. We only treat a + // word as being incorrect if *every* checker that provides negative + // values treats it as incorrect. + // + // We know there is at least one item in this list, so pull that out. If + // that is the only one, we don't have to do any additional work. + // Otherwise we compare every other one against it, removing any elements + // that aren't an intersection — which, hopefully, will produce a smaller + // list with each iteration. + let intersection = null; + + for (let incorrect of incorrects) { + if (intersection === null) { + intersection = incorrect; + } else { + intersection.append(incorrect); + } + } + + // If we have no intersection, then we have no problems to report. + if (intersection.length === 0) { + this.log('no spelling after intersections'); + return { misspellings: [] }; + } + + // Remove all of the confirmed correct words from the resulting incorrect + // list. This allows us to have correct-only providers as opposed to only + // incorrect providers. + if (correct.ranges.length > 0) { + intersection.subtract(correct); + } + + if (this.log.enabled) { + this.log('check intersections', intersection); + } + + // Convert the text ranges (index into the string) into Pulsar buffer + // coordinates (row and column). + let row = 0; + let rangeIndex = 0; + let lineBeginIndex = 0; + const misspellings = []; + while ( + lineBeginIndex < text.length && + rangeIndex < intersection.ranges.length + ) { + // Figure out where the next line break is. If we hit -1, then we make sure + // it is a higher number so our < comparisons work properly. + let lineEndIndex = text.indexOf('\n', lineBeginIndex); + if (lineEndIndex === -1) { + lineEndIndex = Infinity; } - // Return the resulting list of options. - return results; - } - - addMisspellings(misspellings, row, range, lineBeginIndex, text) { - // Get the substring of text, if there is no space, then we can just return - // the entire result. - const substring = text.substring(range[0], range[1]); - - if (/\s+/.test(substring)) { - // We have a space, to break it into individual components and push each - // one to the misspelling list. - const parts = substring.split(/(\s+)/); - let substringIndex = 0; - for (let part of parts) { - if (!/\s+/.test(part)) { - const markBeginIndex = - range[0] - lineBeginIndex + substringIndex; - const markEndIndex = markBeginIndex + part.length; - misspellings.push([ - [row, markBeginIndex], - [row, markEndIndex], - ]); - } - - substringIndex += part.length; + // Loop through and get all the ranegs for this line. + while (true) { + let range = intersection.ranges[rangeIndex]; + if (range && range[0] < lineEndIndex) { + // Figure out the character range of this line. We need this + // because `addMisspellings` doesn't handle jumping across lines + // easily and the use of the number ranges is inclusive. + const lineRange = new multirange.MultiRange([]).appendRange( + lineBeginIndex, + lineEndIndex + ); + const rangeRange = new multirange.MultiRange([]).appendRange( + range[0], + range[1] + ); + lineRange.intersect(rangeRange); + + // The range we have here includes whitespace between two + // concurrent tokens ("zz zz zz" shows up as a single misspelling). + // The original version would split the example into three separate + // ones, so we do the same thing, but only for the ranges within + // the line. + this.addMisspellings( + misspellings, + row, + lineRange.ranges[0], + lineBeginIndex, + text + ); + + // If this line is beyond the limits of our current range, we move + // to the next one; otherwise we loop again to reuse this range + // against the next line. + if (lineEndIndex >= range[1]) { + rangeIndex++; + } else { + break; } - - return; + } else { + break; + } } - // There were no spaces, so just return the entire list. - return misspellings.push([ - [row, range[0] - lineBeginIndex], - [row, range[1] - lineBeginIndex], - ]); + lineBeginIndex = lineEndIndex + 1; + row++; + } + + // Return the resulting misspellings. + return { misspellings }; + } catch (err) { + console.error(`Error during spellcheck:`); + console.error(err); + } + } + + suggest(args, word) { + // Make sure our deferred initialization is done. + // let checker, index, key, priority, suggestion; + this.init(); + + // Gather up a list of corrections and put them into a custom object that has + // the priority of the plugin, the index in the results, and the word itself. + // We use this to intersperse the results together to avoid having the + // preferred answer for the second plugin below the least preferred of the + // first. + const suggestions = []; + + for (let checker of this.checkers) { + // We only care if this plugin contributes to checking to suggestions. + if (!checker.isEnabled() || !checker.providesSuggestions(args)) { + continue; + } + + // Get the suggestions for this word. + let index = 0; + let priority = checker.getPriority(); + + for (let suggestion of checker.suggest(args, word)) { + suggestions.push({ + isSuggestion: true, + priority, + index: index++, + suggestion, + label: suggestion, + }); + } } - init() { - // Set up logging. - if (atom.config.get('spell-check.enableDebug')) { - debug = require('debug'); - this.log = debug('spell-check:spell-check-manager'); - } else { - this.log = (str) => {}; - } + // Once we have the suggestions, then sort them to intersperse the results. + let keys = Object.keys(suggestions).sort(function (key1, key2) { + const value1 = suggestions[key1]; + const value2 = suggestions[key2]; + const weight1 = value1.priority + value1.index; + const weight2 = value2.priority + value2.index; + + if (weight1 !== weight2) { + return weight1 - weight2; + } + + return value1.suggestion.localeCompare(value2.suggestion); + }); + + // Go through the keys and build the final list of suggestions. As we go + // through, we also want to remove duplicates. + const results = []; + const seen = []; + for (let key of keys) { + const s = suggestions[key]; + // eslint-disable-next-line no-prototype-builtins + if (seen.hasOwnProperty(s.suggestion)) { + continue; + } + results.push(s); + seen[s.suggestion] = 1; + } - // Set up the system checker. - const hasSystemChecker = this.useSystem && env.isSystemSupported(); - if (this.useSystem && this.systemChecker === null) { - const SystemChecker = require('./system-checker'); - this.systemChecker = new SystemChecker(); - this.addSpellChecker(this.systemChecker); - } + // We also grab the "add to dictionary" listings. + const that = this; + keys = Object.keys(this.checkers).sort(function (key1, key2) { + const value1 = that.checkers[key1]; + const value2 = that.checkers[key2]; + return value1.getPriority() - value2.getPriority(); + }); + + for (let key of keys) { + // We only care if this plugin contributes to checking to suggestions. + let checker = this.checkers[key]; + if (!checker.isEnabled() || !checker.providesAdding(args)) { + continue; + } + + // Add all the targets to the list. + const targets = checker.getAddingTargets(args); + for (let target of targets) { + target.plugin = checker; + target.word = word; + target.isSuggestion = false; + results.push(target); + } + } - // Set up the known words. - if (this.knownWordsChecker === null) { - const KnownWordsChecker = require('./known-words-checker'); - this.knownWordsChecker = new KnownWordsChecker(this.knownWords); - this.knownWordsChecker.enableAdd = this.addKnownWords; - this.addSpellChecker(this.knownWordsChecker); + // Return the resulting list of options. + return results; + } + + addMisspellings(misspellings, row, range, lineBeginIndex, text) { + // Get the substring of text. If there is no space, we can return the + // entire result. + const substring = text.substring(range[0], range[1]); + + if (/\s+/.test(substring)) { + // We have a space, break it into individual components and add each one + // to the misspelling list. + const parts = substring.split(/(\s+)/); + let substringIndex = 0; + for (let part of parts) { + if (!/\s+/.test(part)) { + const markBeginIndex = range[0] - lineBeginIndex + substringIndex; + const markEndIndex = markBeginIndex + part.length; + misspellings.push([ + [row, markBeginIndex], + [row, markEndIndex], + ]); } - // See if we need to initialize the built-in checkers. - if (this.useLocales && this.localeCheckers === null) { - // Set up the locale checkers. - let defaultLocale; - this.localeCheckers = []; - - // If we have a blank location, use the default based on the process. If - // set, then it will be the best language. We keep track if we are using - // the default locale to control error messages. - let inferredLocale = false; - - if (!this.locales.length) { - defaultLocale = process.env.LANG; - if (defaultLocale) { - inferredLocale = true; - this.locales = [defaultLocale.split('.')[0]]; - } - } + substringIndex += part.length; + } - // 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 (!this.locales.length) { - defaultLocale = navigator.language; - if (defaultLocale && defaultLocale.length === 5) { - const separatorChar = defaultLocale.charAt(2); - if (separatorChar === '_' || separatorChar === '-') { - inferredLocale = true; - this.locales = [defaultLocale]; - } - } - } + return; + } - // 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 (!this.locales.length) { - inferredLocale = true; - this.locales = ['en_US']; - } + // There were no spaces, so just return the entire list. + return misspellings.push([ + [row, range[0] - lineBeginIndex], + [row, range[1] - lineBeginIndex], + ]); + } + + init() { + // Set up logging. + if (atom.config.get('spell-check.enableDebug')) { + let debug = require('debug'); + this.log = debug('spell-check:spell-check-manager'); + } else { + this.log = (_str) => {}; + } - // Go through the new list and create new locale checkers. - const LocaleChecker = require('./locale-checker'); - return (() => { - const result = []; - for (let locale of this.locales) { - const checker = new LocaleChecker( - locale, - this.localePaths, - hasSystemChecker, - inferredLocale - ); - this.addSpellChecker(checker); - result.push(this.localeCheckers.push(checker)); - } - return result; - })(); - } + // Set up the system checker. + const hasSystemChecker = this.useSystem && env.isSystemSupported(); + if (this.useSystem && this.systemChecker === null) { + SystemChecker ??= require('./system-checker'); + this.systemChecker = new SystemChecker(); + this.addSpellChecker(this.systemChecker); } - deactivate() { - this.checkers = []; - this.locales = []; - this.localePaths = []; - this.useSystem = false; - this.useLocales = false; - this.knownWords = []; - this.addKnownWords = false; - - this.systemChecker = null; - this.localeCheckers = null; - return (this.knownWordsChecker = null); + // Set up the known words. + if (this.knownWordsChecker === null) { + KnownWordsChecker ??= require('./known-words-checker'); + this.knownWordsChecker = new KnownWordsChecker(this.knownWords); + this.knownWordsChecker.enableAdd = this.addKnownWords; + this.addSpellChecker(this.knownWordsChecker); } - reloadLocales() { - if (this.localeCheckers) { - for (let localeChecker of this.localeCheckers) { - this.removeSpellChecker(localeChecker); - } - return (this.localeCheckers = null); + // See if we need to initialize the built-in checkers. + if (this.useLocales && this.localeCheckers === null) { + // Set up the locale checkers. + let defaultLocale; + this.localeCheckers = []; + + // If we have a blank location, use the default based on the process. If + // set, then it will be the best language. We keep track if we are using + // the default locale to control error messages. + let inferredLocale = false; + + if (!this.locales.length) { + defaultLocale = process.env.LANG; + if (defaultLocale) { + inferredLocale = true; + this.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 (!this.locales.length) { + defaultLocale = navigator.language; + if (defaultLocale && defaultLocale.length === 5) { + const separatorChar = defaultLocale.charAt(2); + if (separatorChar === '_' || separatorChar === '-') { + inferredLocale = true; + this.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 — plus it can be used with the + // fallback path of the `spellchecker` package. + if (!this.locales.length) { + inferredLocale = true; + this.locales = ['en_US']; + } + + // Go through the new list and create new locale checkers. + LocaleChecker ??= require('./locale-checker'); + + for (let locale of this.locales) { + let checker = new LocaleChecker( + locale, + this.localePaths, + hasSystemChecker, + inferredLocale + ); + this.addSpellChecker(checker); + } } + } + + deactivate() { + this.checkers = []; + this.locales = []; + this.localePaths = []; + this.useSystem = false; + this.useLocales = false; + this.knownWords = []; + this.addKnownWords = false; + + this.systemChecker = null; + this.localeCheckers = null; + this.knownWordsChecker = null; + } + + reloadLocales() { + if (this.localeCheckers) { + for (let localeChecker of this.localeCheckers) { + this.removeSpellChecker(localeChecker); + } + this.localeCheckers = null; + } + } - reloadKnownWords() { - if (this.knownWordsChecker) { - this.removeSpellChecker(this.knownWordsChecker); - return (this.knownWordsChecker = null); - } + reloadKnownWords() { + if (this.knownWordsChecker) { + this.removeSpellChecker(this.knownWordsChecker); + this.knownWordsChecker = null; } + } } -SpellCheckerManager.initClass(); const manager = new SpellCheckerManager(); module.exports = manager; diff --git a/packages/spell-check/lib/spell-check-task.js b/packages/spell-check/lib/spell-check-task.js index 226d7d16d5..444353e198 100644 --- a/packages/spell-check/lib/spell-check-task.js +++ b/packages/spell-check/lib/spell-check-task.js @@ -1,129 +1,242 @@ -let SpellCheckTask; +// let SpellCheckTask; let idCounter = 0; -module.exports = SpellCheckTask = (function () { - SpellCheckTask = class SpellCheckTask { - static initClass() { - this.handler = null; - this.jobs = []; - } - - constructor(manager) { - this.manager = manager; - this.id = idCounter++; - } - - terminate() { - return this.constructor.removeFromArray( - this.constructor.jobs, - (j) => j.args.id === this.id - ); - } - - start(editor, onDidSpellCheck) { - // Figure out the paths since we need that for checkers that are project-specific. - const buffer = editor.getBuffer(); - let projectPath = null; - let relativePath = null; - - if (buffer != null && buffer.file && buffer.file.path) { - [projectPath, relativePath] = atom.project.relativizePath( - buffer.file.path - ); - } - - // Remove old jobs for this SpellCheckTask from the shared jobs list. - this.constructor.removeFromArray( - this.constructor.jobs, - (j) => j.args.id === this.id - ); - - // Create an job that contains everything we'll need to do the work. - const job = { - manager: this.manager, - callbacks: [onDidSpellCheck], - editorId: editor.id, - args: { - id: this.id, - projectPath, - relativePath, - text: buffer.getText(), - }, - }; - - // If we already have a job for this work piggy-back on it with our callback. - if (this.constructor.piggybackExistingJob(job)) { - return; - } - - // Do the work now if not busy or queue it for later. - this.constructor.jobs.unshift(job); - if (this.constructor.jobs.length === 1) { - return this.constructor.startNextJob(); - } - } - - static piggybackExistingJob(newJob) { - if (this.jobs.length > 0) { - for (let job of this.jobs) { - if (this.isDuplicateRequest(job, newJob)) { - job.callbacks = job.callbacks.concat(newJob.callbacks); - return true; - } - } - } - return false; - } - - static isDuplicateRequest(a, b) { - return ( - a.args.projectPath === b.args.projectPath && - a.args.relativePath === b.args.relativePath - ); - } - - static removeFromArray(array, predicate) { - if (array.length > 0) { - for (let i = 0; i < array.length; i++) { - if (predicate(array[i])) { - const found = array[i]; - array.splice(i, 1); - return found; - } - } - } - } - - static startNextJob() { - const activeEditor = atom.workspace.getActiveTextEditor(); - if (!activeEditor) return; - - const activeEditorId = activeEditor.id; - const job = - this.jobs.find((j) => j.editorId === activeEditorId) || - this.jobs[0]; - - return job.manager - .check(job.args, job.args.text) - .then((results) => { - this.removeFromArray( - this.jobs, - (j) => j.args.id === job.args.id - ); - for (let callback of job.callbacks) { - callback(results.misspellings); - } - - if (this.jobs.length > 0) { - return this.startNextJob(); - } - }); - } - - static clear() { - return (this.jobs = []); +class SpellCheckTask { + static handler = null; + static jobs = []; + + static removeJobById(id) { + if (this.jobs.length === 0) return false; + let index = this.jobs.findIndex((j) => j.args.id === id); + if (index === -1) return false; + this.jobs.splice(index, 1); + return true; + } + + static findJobByEditorId(id) { + return this.jobs.find((j) => j.editorId === id) ?? this.jobs[0]; + } + + static addJob(job) { + this.jobs.unshift(job); + if (this.jobs.length === 1) { + this.startNextJob(); + } + } + + static clearJobs() { + this.jobs = []; + } + + static async startNextJob() { + if (this.jobs.length === 0) return; + let activeEditor = atom.workspace.getActiveTextEditor(); + if (!activeEditor) return; + + let job = this.findJobByEditorId(activeEditor.id); + + try { + let results = await job.manager.check(job.args, job.args.text); + // We check whether this resulted in a job removal. If not, it means + // that this job is no longer valid, and we should not proceed. + let result = SpellCheckTask.removeJobById(job.args.id); + if (result) { + for (let callback of job.callbacks) { + callback(results.misspellings); } + } + } catch (err) { + console.error('Error running job:', job); + console.error(err); + } finally { + SpellCheckTask.removeJobById(job.args.id); + if (this.jobs.length > 0) { + this.startNextJob(); + } + } + } + + static piggybackExistingJob(newJob) { + if (this.jobs.length === 0) return false; + for (let job of this.jobs) { + if (this.isDuplicateRequest(job, newJob)) { + job.callbacks.push(...newJob.callbacks); + return true; + } + } + } + + static isDuplicateRequest(a, b) { + return ( + a.args.projectPath === b.args.projectPath && + a.args.relativePath === b.args.relativePath + ); + } + + constructor(manager) { + this.manager = manager; + this.id = idCounter++; + } + + terminate() { + return SpellCheckTask.removeJobById(this.id); + } + + start(editor, onDidSpellCheck) { + // Figure out the paths since we need that for checkers that are project-specific. + const buffer = editor.getBuffer(); + let projectPath = null; + let relativePath = null; + + if (buffer != null && buffer.file && buffer.file.path) { + [projectPath, relativePath] = atom.project.relativizePath( + buffer.file.path + ); + } + // Remove old jobs for this SpellCheckTask from the shared jobs list. + SpellCheckTask.removeJobById(this.id); + + let job = { + manager: this.manager, + callbacks: [onDidSpellCheck], + editorId: editor.id, + args: { + id: this.id, + projectPath, + relativePath, + text: buffer.getText(), + }, }; - SpellCheckTask.initClass(); - return SpellCheckTask; -})(); + + // If we already have a job for this work, piggy-back on it with our + // callback. + if (SpellCheckTask.piggybackExistingJob(job)) { + return; + } + // Do the work now if not busy or queue it for later. + SpellCheckTask.addJob(job); + } +} + +module.exports = SpellCheckTask; +// +// module.exports = SpellCheckTask = (function () { +// SpellCheckTask = class SpellCheckTask { +// static initClass() { +// this.handler = null; +// this.jobs = []; +// } +// +// constructor(manager) { +// this.manager = manager; +// this.id = idCounter++; +// } +// +// terminate() { +// return this.constructor.removeFromArray( +// this.constructor.jobs, +// (j) => j.args.id === this.id +// ); +// } +// +// start(editor, onDidSpellCheck) { +// // Figure out the paths since we need that for checkers that are project-specific. +// const buffer = editor.getBuffer(); +// let projectPath = null; +// let relativePath = null; +// +// if (buffer != null && buffer.file && buffer.file.path) { +// [projectPath, relativePath] = atom.project.relativizePath( +// buffer.file.path +// ); +// } +// +// // Remove old jobs for this SpellCheckTask from the shared jobs list. +// this.constructor.removeFromArray( +// this.constructor.jobs, +// (j) => j.args.id === this.id +// ); +// +// // Create an job that contains everything we'll need to do the work. +// const job = { +// manager: this.manager, +// callbacks: [onDidSpellCheck], +// editorId: editor.id, +// args: { +// id: this.id, +// projectPath, +// relativePath, +// text: buffer.getText(), +// }, +// }; +// +// // If we already have a job for this work piggy-back on it with our callback. +// if (this.constructor.piggybackExistingJob(job)) { +// return; +// } +// +// // Do the work now if not busy or queue it for later. +// this.constructor.jobs.unshift(job); +// if (this.constructor.jobs.length === 1) { +// return this.constructor.startNextJob(); +// } +// } +// +// static piggybackExistingJob(newJob) { +// if (this.jobs.length > 0) { +// for (let job of this.jobs) { +// if (this.isDuplicateRequest(job, newJob)) { +// job.callbacks = job.callbacks.concat(newJob.callbacks); +// return true; +// } +// } +// } +// return false; +// } +// +// static isDuplicateRequest(a, b) { +// return ( +// a.args.projectPath === b.args.projectPath && +// a.args.relativePath === b.args.relativePath +// ); +// } +// +// static removeFromArray(array, predicate) { +// if (array.length > 0) { +// for (let i = 0; i < array.length; i++) { +// if (predicate(array[i])) { +// const found = array[i]; +// array.splice(i, 1); +// return found; +// } +// } +// } +// } +// +// static startNextJob() { +// const activeEditor = atom.workspace.getActiveTextEditor(); +// if (!activeEditor) return; +// +// const activeEditorId = activeEditor.id; +// const job = +// this.jobs.find((j) => j.editorId === activeEditorId) || this.jobs[0]; +// +// return job.manager.check(job.args, job.args.text).then((results) => { +// this.removeFromArray(this.jobs, (j) => j.args.id === job.args.id); +// for (let callback of job.callbacks) { +// callback(results.misspellings); +// } +// +// if (this.jobs.length > 0) { +// return this.startNextJob(); +// } +// }); +// } +// +// static clear() { +// return (this.jobs = []); +// } +// }; +// SpellCheckTask.initClass(); +// return SpellCheckTask; +// })(); diff --git a/packages/spell-check/lib/spell-check-view.js b/packages/spell-check/lib/spell-check-view.js index f995c5e8e5..d0d2d35826 100644 --- a/packages/spell-check/lib/spell-check-view.js +++ b/packages/spell-check/lib/spell-check-view.js @@ -1,5 +1,3 @@ -let SpellCheckView; -const _ = require('underscore-plus'); const { CompositeDisposable } = require('atom'); const SpellCheckTask = require('./spell-check-task'); const { scopeDescriptorMatchesSelector } = require('./scope-helper'); @@ -8,418 +6,364 @@ const { scopeDescriptorMatchesSelector } = require('./scope-helper'); // `grammars` setting. Allows for a more generic name in the setting (e.g., // `source` to match all `source.[x]` grammars). function topLevelScopeMatches(grammar, scope) { - if (scope === grammar) return true; - if (grammar.startsWith(`${scope}.`)) { - return true; - } - return false; + if (scope === grammar) return true; + if (grammar.startsWith(`${scope}.`)) { + return true; + } + return false; } let CorrectionsView = null; -module.exports = SpellCheckView = class SpellCheckView { - constructor(editor, spellCheckModule, manager) { - this.addContextMenuEntries = this.addContextMenuEntries.bind(this); - this.makeCorrection = this.makeCorrection.bind(this); - this.editor = editor; - this.spellCheckModule = spellCheckModule; - this.manager = manager; - this.disposables = new CompositeDisposable(); - this.initializeMarkerLayer(); - - this.taskWrapper = new SpellCheckTask(this.manager); - - this.correctMisspellingCommand = atom.commands.add( - atom.views.getView(this.editor), - 'spell-check:correct-misspelling', - () => { - let marker; - if ( - (marker = this.markerLayer.findMarkers({ - containsBufferPosition: this.editor.getCursorBufferPosition(), - })[0]) - ) { - if (CorrectionsView == null) { - CorrectionsView = require('./corrections-view'); - } - if (this.correctionsView != null) { - this.correctionsView.destroy(); - } - this.correctionsView = new CorrectionsView( - this.editor, - this.getCorrections(marker), - marker, - this, - this.updateMisspellings - ); - return this.correctionsView.attach(); - } - } - ); - - atom.views - .getView(this.editor) - .addEventListener('contextmenu', this.addContextMenuEntries); - - this.disposables.add( - this.editor.onDidChangePath(() => { - return this.subscribeToBuffer(); - }) - ); - - this.disposables.add( - this.editor.onDidChangeGrammar(() => { - return this.subscribeToBuffer(); - }) - ); - - this.disposables.add( - atom.config.onDidChange('editor.fontSize', () => { - return this.subscribeToBuffer(); - }) - ); - - this.disposables.add( - atom.config.onDidChange('spell-check.grammars', () => { - return this.subscribeToBuffer(); - }) - ); - - this.disposables.add( - atom.config.observe( - 'spell-check.excludedScopes', - (excludedScopes) => { - this.excludedScopes = excludedScopes; - } - ) - ); - - this.subscribeToBuffer(); - - this.disposables.add(this.editor.onDidDestroy(this.destroy.bind(this))); - } - - initializeMarkerLayer() { - this.markerLayer = this.editor.addMarkerLayer({ - maintainHistory: false, +class SpellCheckView { + constructor(editor, spellCheckModule, manager) { + this.addContextMenuEntries = this.addContextMenuEntries.bind(this); + this.makeCorrection = this.makeCorrection.bind(this); + + this.editor = editor; + this.spellCheckModule = spellCheckModule; + this.manager = manager; + this.disposables = new CompositeDisposable(); + this.initializeMarkerLayer(); + + this.taskWrapper = new SpellCheckTask(this.manager); + + this.correctMisspellingCommand = atom.commands.add( + atom.views.getView(this.editor), + 'spell-check:correct-misspelling', + () => { + let markers = this.markerLayer.findMarkers({ + containsBufferPosition: this.editor.getCursorBufferPosition(), }); - return (this.markerLayerDecoration = this.editor.decorateMarkerLayer( - this.markerLayer, - { - type: 'highlight', - class: 'spell-check-misspelling', - deprecatedRegionClass: 'misspelling', - } - )); - } - - destroy() { - this.unsubscribeFromBuffer(); - this.disposables.dispose(); - this.taskWrapper.terminate(); - this.markerLayer.destroy(); - this.markerLayerDecoration.destroy(); - this.correctMisspellingCommand.dispose(); - if (this.correctionsView != null) { - this.correctionsView.destroy(); + if (markers.length > 0) { + let [marker] = markers; + CorrectionsView ??= require('./corrections-view'); + this.correctionsView?.destroy(); + + this.correctionsView = new CorrectionsView( + this.editor, + this.getCorrections(marker), + marker, + this, + this.updateMisspellings + ); + + return this.correctionsView.attach(); } - return this.clearContextMenuEntries(); - } - - unsubscribeFromBuffer() { - this.destroyMarkers(); - - if (this.buffer != null) { - this.bufferDisposable.dispose(); - return (this.buffer = null); + } + ); + + atom.views + .getView(this.editor) + .addEventListener('contextmenu', this.addContextMenuEntries); + + const subscribe = this.subscribeToBuffer.bind(this); + + this.disposables.add( + this.editor.onDidChangePath(subscribe), + this.editor.onDidChangeGrammar(subscribe), + atom.config.onDidChange('editor.fontSize', subscribe), + + // When these settings are changed, it could affect each buffer's + // results, so we need to reset state. + atom.config.onDidChange('spell-check.grammars', () => subscribe), + atom.config.observe('spell-check.excludedScopes', (excludedScopes) => { + this.excludedScopes = excludedScopes; + subscribe(); + }) + ); + + this.subscribeToBuffer(); + + this.disposables.add(this.editor.onDidDestroy(() => this.destroy())); + } + + initializeMarkerLayer() { + this.markerLayer = this.editor.addMarkerLayer({ maintainHistory: false }); + this.markerLayerDecoration = this.editor.decorateMarkerLayer( + this.markerLayer, + { + type: 'highlight', + class: 'spell-check-misspelling', + deprecatedRegionClass: 'misspelling', + } + ); + } + + destroy() { + this.unsubscribeFromBuffer(); + this.disposables.dispose(); + this.taskWrapper.terminate(); + this.markerLayer.destroy(); + this.markerLayerDecoration.destroy(); + this.correctMisspellingCommand.dispose(); + this.correctionsView?.destroy(); + this.clearContextMenuEntries(); + } + + unsubscribeFromBuffer() { + this.destroyMarkers(); + this.bufferDisposable?.dispose(); + this.buffer &&= null; + } + + subscribeToBuffer() { + this.unsubscribeFromBuffer(); + if (!this.spellCheckCurrentGrammar()) return; + + this.scopesToSpellCheck = this.getSpellCheckScopesForCurrentGrammar(); + this.buffer = this.editor.getBuffer(); + this.bufferDisposable = new CompositeDisposable( + this.buffer.onDidStopChanging(() => this.updateMisspellings()), + this.editor.onDidTokenize(() => this.updateMisspellings()) + ); + + this.updateMisspellings(); + } + + spellCheckCurrentGrammar() { + let grammar = this.editor.getGrammar().scopeName; + let grammars = atom.config.get('spell-check.grammars'); + // We allow for complex scope descriptors here. But to do a naïve filtering + // by grammar, we'll extract the "top-level" scope from each one. + let topLevelScopes = grammars.map((rawScope) => { + if (!rawScope.includes(' ')) return rawScope; + return rawScope.substring(0, rawScope.indexOf(' ')); + }); + return topLevelScopes.some((scope) => { + return topLevelScopeMatches(grammar, scope); + }); + } + + // Behavior: + // + // * Returns `true` if the entire buffer should be checked. + // * Returns `false` if none of the buffer should be checked. + // * Otherwise returns an {Array} of scope names matching regions of the + // buffer that should be checked. + getSpellCheckScopesForCurrentGrammar() { + const grammar = this.editor.getGrammar().scopeName; + let grammars = atom.config.get('spell-check.grammars'); + let scopeList = []; + + // Despite the name of this setting, spell-checking is no longer all or + // nothing on a per-grammar basis; we now allow users to opt into + // checking subsections of a buffer by adding descendant scopes. Each + // segment must begin with all or part of a root scope name (e.g., + // `source.js`, `text.html`, but otherwise any valid scope selector is + // accepted here.) + // + // Examples: + // + // * `source.js comment.block` + // * `source comment, source string.quoted` + // * `text` + // + // The first example targets just JS block comments; the second targets + // all comments and quoted strings in _all_ source files; and the third + // targets any text format, whether HTML or Markdown or plaintext. + // + // This allows for more granular spell-checking than was possible + // before, even if the `excludeScopes` setting was utilized. + for (let rawScope of grammars) { + if (!rawScope.includes(' ')) { + // Any value that's just the bare root scope of the language + // (like `source.python`) means that we're spell-checking the + // entire buffer. This applies even if there's a later match + // for this grammar that's more restrictive. + if (topLevelScopeMatches(grammar, rawScope)) { + return true; } - } - - subscribeToBuffer() { - this.unsubscribeFromBuffer(); - - if (this.spellCheckCurrentGrammar()) { - this.scopesToSpellCheck = this.getSpellCheckScopesForCurrentGrammar(); - this.buffer = this.editor.getBuffer(); - this.bufferDisposable = new CompositeDisposable( - this.buffer.onDidStopChanging( - () => this.updateMisspellings(), - this.editor.onDidTokenize(() => this.updateMisspellings()) - ) - ); - return this.updateMisspellings(); + } else { + // If the value also includes a descendant scope, it means we're + // spell-checking some subset of the buffer. + let index = rawScope.indexOf(' '); + let rootScope = rawScope.substring(0, index); + if (topLevelScopeMatches(grammar, rootScope)) { + // There could be multiple of these — e.g., `source.python string, + // source.python comment` — so we won't return early. + scopeList.push(rawScope); } + } } - - spellCheckCurrentGrammar() { - const grammar = this.editor.getGrammar().scopeName; - let grammars = atom.config.get('spell-check.grammars'); - let topLevelScopes = grammars.map((rawScope) => { - if (!rawScope.includes(' ')) return rawScope; - return rawScope.substring(0, rawScope.indexOf(' ')); - }); - return topLevelScopes.some((scope) => { - return topLevelScopeMatches(grammar, scope); - }); - // return topLevelScopes.includes(grammar); - } - - // Returns: - // - // * `true` if the entire buffer should be checked; - // * `false` if none of the buffer should be checked; or, if only certain - // parts of the buffer should be checked, - // * an {Array} of scope names matching regions of the buffer that should - // be checked. - getSpellCheckScopesForCurrentGrammar() { - const grammar = this.editor.getGrammar().scopeName; - let grammars = atom.config.get('spell-check.grammars'); - let scopeList = []; - // Despite the name of this setting, spell-checking is no longer all or - // nothing on a per-grammar basis; we now allow users to opt into - // checking subsections of a buffer by adding descendant scopes. Each - // segment must begin with all or part of a root scope name (e.g., - // `source.js`, `text.html`, but otherwise any valid scope selector is - // accepted here.) - // - // Examples: - // - // * `source.js comment.block` - // * `source comment, source string.quoted` - // * `text` - // - // The first example targets just JS block comments; the second targets - // all comments and quoted strings in _all_ source files; and the third - // targets any text format, whether HTML or Markdown or plaintext. - // - // This allows for more granular spell-checking than was possible - // before, even if the `excludeScopes` setting was utilized. - for (let rawScope of grammars) { - if (!rawScope.includes(' ')) { - // Any value that's just the bare root scope of the language - // (like `source.python`) means that we're spell-checking the - // entire buffer. This applies even if there's a later match - // for this grammar that's more restrictive. - if (topLevelScopeMatches(grammar, rawScope)) { - return true; - } - } else { - // If the value also includes a descendant scope, it means we're - // spell-checking some subset of the buffer. - let index = rawScope.indexOf(' '); - let rootScope = rawScope.substring(0, index); - if (topLevelScopeMatches(grammar, rootScope)) { - // There could be multiple of these — e.g., `source.python string, - // source.python comment` — so we won't return early. - scopeList.push(rawScope); - } - } - } - return scopeList.length > 0 ? scopeList : false; + return scopeList.length > 0 ? scopeList : false; + } + + destroyMarkers() { + this.markerLayer.destroy(); + this.markerLayerDecoration.destroy(); + this.initializeMarkerLayer(); + } + + addMarkers(misspellings) { + let result = []; + for (let misspelling of misspellings) { + let scope = this.editor.scopeDescriptorForBufferPosition(misspelling[0]); + if (this.scopeIsExcluded(scope)) { + result.push(undefined); + } else { + result.push( + this.markerLayer.markBufferRange(misspelling, { invalidate: 'touch' }) + ); + } } - - destroyMarkers() { - this.markerLayer.destroy(); - this.markerLayerDecoration.destroy(); - return this.initializeMarkerLayer(); + return result; + } + + updateMisspellings() { + this.taskWrapper.start(this.editor, (misspellings) => { + this.destroyMarkers(); + if (this.buffer != null) { + this.addMarkers(misspellings); + } + }); + } + + getCorrections(marker) { + // Build up the arguments object for this buffer and text. + let projectPath = null; + let relativePath = null; + + if (this.buffer != null && this.buffer.file && this.buffer.file.path) { + [projectPath, relativePath] = atom.project.relativizePath( + this.buffer.file.path + ); } - addMarkers(misspellings) { - return (() => { - const result = []; - for (let misspelling of misspellings) { - const scope = this.editor.scopeDescriptorForBufferPosition( - misspelling[0] - ); - // Under the hood, we spell-check the entire document; but we - // might end up ignoring some of the misspellings based on - // their scope descriptor. - if (!this.scopeIsExcluded(scope)) { - result.push( - this.markerLayer.markBufferRange(misspelling, { - invalidate: 'touch', - }) - ); - } else { - result.push(undefined); - } - } - return result; - })(); - } + const args = { projectPath, relativePath }; + + // Get the misspelled word and then request corrections. + const misspelling = this.editor.getTextInBufferRange( + marker.getBufferRange() + ); + return this.manager.suggest(args, misspelling); + } + + addContextMenuEntries(mouseEvent) { + this.clearContextMenuEntries(); + + // Get buffer position of the right click event. If the click happens + // outside the boundaries of any text, the method defaults to the buffer + // position of the last character in the editor. + const currentScreenPosition = atom.views + .getView(this.editor) + .getComponent() + .screenPositionForMouseEvent(mouseEvent); + const currentBufferPosition = this.editor.bufferPositionForScreenPosition( + currentScreenPosition + ); + + // Check to see if the selected word is incorrect. + let markers = this.markerLayer.findMarkers({ + containsBufferPosition: currentBufferPosition, + }); + + if (markers.length > 0) { + let [marker] = markers; + let corrections = this.getCorrections(marker); + + if (corrections.length === 0) return; + + this.spellCheckModule.contextMenuEntries.push({ + menuItem: atom.contextMenu.add({ + 'atom-text-editor': [{ type: 'separator' }], + }), + }); + + let correctionIndex = 0; + for (let correction of corrections) { + let contextMenuEntry = {}; + // Register new command for correction. + var commandName = `spell-check:correct-misspelling-${correctionIndex}`; + + contextMenuEntry.command = atom.commands.add( + atom.views.getView(this.editor), + commandName, + () => { + this.makeCorrection(correction, marker); + this.clearContextMenuEntries(); + } + ); - updateMisspellings() { - return this.taskWrapper.start(this.editor, (misspellings) => { - this.destroyMarkers(); - if (this.buffer != null) { - return this.addMarkers(misspellings); - } + contextMenuEntry.menuItem = atom.contextMenu.add({ + 'atom-text-editor': [ + { label: correction.label, command: commandName }, + ], }); - } - - getCorrections(marker) { - // Build up the arguments object for this buffer and text. - let projectPath = null; - let relativePath = null; - - if (this.buffer != null && this.buffer.file && this.buffer.file.path) { - [projectPath, relativePath] = atom.project.relativizePath( - this.buffer.file.path - ); - } - const args = { - projectPath, - relativePath, - }; + this.spellCheckModule.contextMenuEntries.push(contextMenuEntry); + correctionIndex++; + } - // Get the misspelled word and then request corrections. - const misspelling = this.editor.getTextInBufferRange( - marker.getBufferRange() - ); - return this.manager.suggest(args, misspelling); + this.spellCheckModule.contextMenuEntries.push({ + menuItem: atom.contextMenu.add({ + 'atom-text-editor': [{ type: 'separator' }], + }), + }); } - - addContextMenuEntries(mouseEvent) { - let marker; - this.clearContextMenuEntries(); - // Get buffer position of the right click event. If the click happens outside - // the boundaries of any text, the method defaults to the buffer position of - // the last character in the editor. - const currentScreenPosition = atom.views - .getView(this.editor) - .component.screenPositionForMouseEvent(mouseEvent); - const currentBufferPosition = this.editor.bufferPositionForScreenPosition( - currentScreenPosition + } + + makeCorrection(correction, marker) { + if (correction.isSuggestion) { + // Update the buffer with the correction. + this.editor.setSelectedBufferRange(marker.getBufferRange()); + return this.editor.insertText(correction.suggestion); + } else { + // Build up the arguments object for this buffer and text. + let projectPath = null; + let relativePath = null; + + if ( + this.editor.buffer != null && + this.editor.buffer.file && + this.editor.buffer.file.path + ) { + [projectPath, relativePath] = atom.project.relativizePath( + this.editor.buffer.file.path ); + } - // Check to see if the selected word is incorrect. - if ( - (marker = this.markerLayer.findMarkers({ - containsBufferPosition: currentBufferPosition, - })[0]) - ) { - const corrections = this.getCorrections(marker); - if (corrections.length > 0) { - this.spellCheckModule.contextMenuEntries.push({ - menuItem: atom.contextMenu.add({ - 'atom-text-editor': [{ type: 'separator' }], - }), - }); - - let correctionIndex = 0; - for (let correction of corrections) { - const contextMenuEntry = {}; - // Register new command for correction. - var commandName = - 'spell-check:correct-misspelling-' + correctionIndex; - contextMenuEntry.command = (( - correction, - contextMenuEntry - ) => { - return atom.commands.add( - atom.views.getView(this.editor), - commandName, - () => { - this.makeCorrection(correction, marker); - return this.clearContextMenuEntries(); - } - ); - })(correction, contextMenuEntry); - - // Add new menu item for correction. - contextMenuEntry.menuItem = atom.contextMenu.add({ - 'atom-text-editor': [ - { label: correction.label, command: commandName }, - ], - }); - this.spellCheckModule.contextMenuEntries.push( - contextMenuEntry - ); - correctionIndex++; - } - - return this.spellCheckModule.contextMenuEntries.push({ - menuItem: atom.contextMenu.add({ - 'atom-text-editor': [{ type: 'separator' }], - }), - }); - } - } - } + const args = { id: this.id, projectPath, relativePath }; - makeCorrection(correction, marker) { - if (correction.isSuggestion) { - // Update the buffer with the correction. - this.editor.setSelectedBufferRange(marker.getBufferRange()); - return this.editor.insertText(correction.suggestion); - } else { - // Build up the arguments object for this buffer and text. - let projectPath = null; - let relativePath = null; - - if ( - this.editor.buffer != null && - this.editor.buffer.file && - this.editor.buffer.file.path - ) { - [projectPath, relativePath] = atom.project.relativizePath( - this.editor.buffer.file.path - ); - } - - const args = { - id: this.id, - projectPath, - relativePath, - }; - - // Send the "add" request to the plugin. - correction.plugin.add(args, correction); - - // Update the buffer to handle the corrections. - return this.updateMisspellings.bind(this)(); - } - } + // Send the "add" request to the plugin. + correction.plugin.add(args, correction); - clearContextMenuEntries() { - for (let entry of this.spellCheckModule.contextMenuEntries) { - if (entry.command != null) { - entry.command.dispose(); - } - if (entry.menuItem != null) { - entry.menuItem.dispose(); - } - } + // Update the buffer to handle the corrections. + this.updateMisspellings(); + } + } - return (this.spellCheckModule.contextMenuEntries = []); + clearContextMenuEntries() { + for (let entry of this.spellCheckModule.contextMenuEntries) { + entry.command?.dispose(); + entry.menuItem?.dispose(); } - scopeIsExcluded(scopeDescriptor) { - // Practically speaking, `this.scopesToSpellCheck` will either be `true` - // or an array of scope selectors. If it's the latter, then we should - // apply whitelisting and exclude anything that doesn't match. - if (Array.isArray(this.scopesToSpellCheck)) { - // If we know none of the subscopes match this region, we can - // exclude it even before we get to the `excludedScopes` setting. - let someMatch = this.scopesToSpellCheck.some( - (scopeToSpellCheck) => { - return scopeDescriptorMatchesSelector( - scopeDescriptor, - scopeToSpellCheck - ); - } - ); - if (!someMatch) return true; - } - // Whether or not we applied whitelisting above, excluded scopes take - // precedence; anything that doesn't make it through this gauntlet - // gets excluded. - return this.excludedScopes.some((excludedScope) => { - return scopeDescriptorMatchesSelector( - scopeDescriptor, - excludedScope - ); - }); + this.spellCheckModule.contextMenuEntries = []; + } + + scopeIsExcluded(scopeDescriptor) { + // Practically speaking, `this.scopesToSpellCheck` will either be `true` + // or an array of scope selectors. If it's the latter, then we should + // apply whitelisting and exclude anything that doesn't match. + if (Array.isArray(this.scopesToSpellCheck)) { + // If we know none of the subscopes match this region, we can + // exclude it even before we get to the `excludedScopes` setting. + let someMatch = this.scopesToSpellCheck.some((scopeToSpellCheck) => { + return scopeDescriptorMatchesSelector( + scopeDescriptor, + scopeToSpellCheck + ); + }); + if (!someMatch) return true; } -}; + // Whether or not we applied whitelisting above, excluded scopes take + // precedence; anything that doesn't make it through this gauntlet + // gets excluded. + return this.excludedScopes.some((excludedScope) => { + return scopeDescriptorMatchesSelector(scopeDescriptor, excludedScope); + }); + } +} + +module.exports = SpellCheckView; diff --git a/packages/spell-check/lib/system-checker.js b/packages/spell-check/lib/system-checker.js index fa4dbc3a01..fee943adee 100644 --- a/packages/spell-check/lib/system-checker.js +++ b/packages/spell-check/lib/system-checker.js @@ -1,19 +1,21 @@ let instance; const spellchecker = require('spellchecker'); -const pathspec = require('atom-pathspec'); +// const pathspec = require('atom-pathspec'); const env = require('./checker-env'); +let debug; + // Initialize the global spell checker which can take some time. We also force // the use of the system or operating system library instead of Hunspell. if (env.isSystemSupported()) { - instance = new spellchecker.Spellchecker(); - instance.setSpellcheckerType(spellchecker.ALWAYS_USE_SYSTEM); + instance = new spellchecker.Spellchecker(); + instance.setSpellcheckerType(spellchecker.ALWAYS_USE_SYSTEM); - if (!instance.setDictionary('', '')) { - instance = undefined; - } -} else { + if (!instance.setDictionary('', '')) { instance = undefined; + } +} else { + instance = undefined; } // The `SystemChecker` is a special case to use the built-in system spell-checking @@ -22,68 +24,68 @@ if (env.isSystemSupported()) { // starts to throw an occasional error if you use multiple locales at the same time // due to some memory bug. class SystemChecker { - constructor() { - if (atom.config.get('spell-check.enableDebug')) { - debug = require('debug'); - this.log = debug('spell-check:system-checker'); - } else { - this.log = (str) => {}; - } - this.log('enabled', this.isEnabled(), this.getStatus()); + constructor() { + if (atom.config.get('spell-check.enableDebug')) { + debug = require('debug'); + this.log = debug('spell-check:system-checker'); + } else { + this.log = (_str) => {}; } + this.log('enabled', this.isEnabled(), this.getStatus()); + } - deactivate() {} + deactivate() {} - getId() { - return 'spell-check:system'; - } - getName() { - return 'System Checker'; - } - getPriority() { - return 110; - } - isEnabled() { - return instance; - } - getStatus() { - if (instance) { - return 'working correctly'; - } else { - return 'not supported on platform'; - } + getId() { + return 'spell-check:system'; + } + getName() { + return 'System Checker'; + } + getPriority() { + return 110; + } + isEnabled() { + return instance; + } + getStatus() { + if (instance) { + return 'working correctly'; + } else { + return 'not supported on platform'; } + } - providesSpelling(args) { - return this.isEnabled(); - } - providesSuggestions(args) { - return this.isEnabled(); - } - providesAdding(args) { - return false; - } // Users can't add yet. + providesSpelling(_) { + return this.isEnabled(); + } + providesSuggestions(_) { + return this.isEnabled(); + } + providesAdding(_) { + return false; + } // Users can't add yet. - check(args, text) { - const id = this.getId(); + check(_, text) { + const id = this.getId(); - if (this.isEnabled()) { - // We use the default checker here and not the locale-specific one so it - // will check all languages at the same time. - return instance.checkSpellingAsync(text).then((incorrect) => { - if (this.log.enabled) { - this.log('check', incorrect); - } - return { id, invertIncorrectAsCorrect: true, incorrect }; - }); - } else { - return { id, status: this.getStatus() }; + if (this.isEnabled()) { + // We use the default checker here and not the locale-specific one so it + // will check all languages at the same time. + return instance.checkSpellingAsync(text).then((incorrect) => { + if (this.log.enabled) { + this.log('check', incorrect); } + return { id, invertIncorrectAsCorrect: true, incorrect }; + }); + } else { + return { id, status: this.getStatus() }; } + } - suggest(args, word) { - return instance.getCorrectionsForMisspelling(word); - } + suggest(_, word) { + return instance.getCorrectionsForMisspelling(word); + } } module.exports = SystemChecker; diff --git a/packages/spell-check/spec/spell-check-spec.js b/packages/spell-check/spec/spell-check-spec.js index 37147971d0..861df0a8d6 100644 --- a/packages/spell-check/spec/spell-check-spec.js +++ b/packages/spell-check/spec/spell-check-spec.js @@ -2,800 +2,778 @@ const SpellCheckTask = require('../lib/spell-check-task'); const env = require('../lib/checker-env'); const { sep } = require('path'); const { - it, - fit, - ffit, - beforeEach, - afterEach, - conditionPromise, - timeoutPromise, + it, + fit, + ffit, + beforeEach, + afterEach, + conditionPromise, } = require('./async-spec-helpers'); describe('Spell check', function () { - let workspaceElement, editor, editorElement, spellCheckModule, languageMode; - - const textForMarker = (marker) => - editor.getTextInBufferRange(marker.getBufferRange()); - - const getMisspellingMarkers = () => - spellCheckModule.misspellingMarkersForEditor(editor); - - beforeEach(async function () { - jasmine.useRealClock(); - - workspaceElement = atom.views.getView(atom.workspace); - await atom.packages.activatePackage('language-text'); - await atom.packages.activatePackage('language-javascript'); - await atom.workspace.open(`${__dirname}${sep}sample.js`); - const pack = await atom.packages.activatePackage('spell-check'); - spellCheckModule = pack.mainModule; - - // Disable the grammers so nothing is done until we turn it back on. - atom.config.set('spell-check.grammars', []); - - // Set the settings to a specific setting to avoid side effects. - atom.config.set('spell-check.useSystem', false); - atom.config.set('spell-check.useLocales', false); - atom.config.set('spell-check.locales', ['en-US']); - - // Attach everything and ready to test. - jasmine.attachToDOM(workspaceElement); - editor = atom.workspace.getActiveTextEditor(); - editorElement = atom.views.getView(editor); - languageMode = editor.getBuffer().getLanguageMode(); - languageMode.useAsyncParsing = false; - }); - - afterEach(async () => { - await languageMode.atTransactionEnd(); - SpellCheckTask.clear(); - }); - - it('decorates all misspelled words', async function () { - atom.config.set('spell-check.useLocales', true); - editor.insertText( - 'This middle of thiss\nsentencts\n\nhas issues and the "edn" \'dsoe\' too' - ); - atom.config.set('spell-check.grammars', ['source.js']); - - await conditionPromise(() => getMisspellingMarkers().length === 4); - const misspellingMarkers = getMisspellingMarkers(); - expect(textForMarker(misspellingMarkers[0])).toEqual('thiss'); - expect(textForMarker(misspellingMarkers[1])).toEqual('sentencts'); - expect(textForMarker(misspellingMarkers[2])).toEqual('edn'); - expect(textForMarker(misspellingMarkers[3])).toEqual('dsoe'); - }); - - it('decorates misspelled words with a leading space', async function () { - atom.config.set('spell-check.useLocales', true); - editor.setText('\nchok bok'); - atom.config.set('spell-check.grammars', ['source.js']); - - await conditionPromise(() => getMisspellingMarkers().length === 2); - const misspellingMarkers = getMisspellingMarkers(); - expect(textForMarker(misspellingMarkers[0])).toEqual('chok'); - expect(textForMarker(misspellingMarkers[1])).toEqual('bok'); - }); - - it('allows certain sub-scopes to be whitelisted into spell checking, implicitly excluding anything that does not match', async () => { - editor.setText( - `speledWrong = 5; + let workspaceElement, editor, editorElement, spellCheckModule, languageMode; + + const textForMarker = (marker) => + editor.getTextInBufferRange(marker.getBufferRange()); + + const getMisspellingMarkers = () => + spellCheckModule.misspellingMarkersForEditor(editor); + + beforeEach(async function () { + jasmine.useRealClock(); + + workspaceElement = atom.views.getView(atom.workspace); + await atom.packages.activatePackage('language-text'); + await atom.packages.activatePackage('language-javascript'); + await atom.workspace.open(`${__dirname}${sep}sample.js`); + const pack = await atom.packages.activatePackage('spell-check'); + spellCheckModule = pack.mainModule; + + // Disable the grammers so nothing is done until we turn it back on. + atom.config.set('spell-check.grammars', []); + + // Set the settings to a specific setting to avoid side effects. + atom.config.set('spell-check.useSystem', false); + atom.config.set('spell-check.useLocales', false); + atom.config.set('spell-check.locales', ['en-US']); + + // Attach everything and ready to test. + jasmine.attachToDOM(workspaceElement); + editor = atom.workspace.getActiveTextEditor(); + editorElement = atom.views.getView(editor); + languageMode = editor.getBuffer().getLanguageMode(); + languageMode.useAsyncParsing = false; + }); + + afterEach(async () => { + await languageMode.atTransactionEnd(); + SpellCheckTask.clearJobs(); + }); + + it('decorates all misspelled words', async function () { + // jasmine.useRealClock(); + console.log('use locales!'); + atom.config.set('spell-check.useLocales', true); + editor.insertText( + 'This middle of thiss\nsentencts\n\nhas issues and the "edn" \'dsoe\' too' + ); + atom.config.set('spell-check.grammars', ['source.js']); + + await conditionPromise(() => getMisspellingMarkers().length > 0); + const misspellingMarkers = getMisspellingMarkers(); + expect(textForMarker(misspellingMarkers[0])).toEqual('thiss'); + expect(textForMarker(misspellingMarkers[1])).toEqual('sentencts'); + expect(textForMarker(misspellingMarkers[2])).toEqual('edn'); + expect(textForMarker(misspellingMarkers[3])).toEqual('dsoe'); + }); + + it('decorates misspelled words with a leading space', async function () { + atom.config.set('spell-check.useLocales', true); + editor.setText('\nchok bok'); + atom.config.set('spell-check.grammars', ['source.js']); + + await conditionPromise(() => getMisspellingMarkers().length === 2); + const misspellingMarkers = getMisspellingMarkers(); + expect(textForMarker(misspellingMarkers[0])).toEqual('chok'); + expect(textForMarker(misspellingMarkers[1])).toEqual('bok'); + }); + + it('allows certain sub-scopes to be whitelisted into spell checking, implicitly excluding anything that does not match', async () => { + editor.setText( + `speledWrong = 5; function speledWrong() {} // We only care about mispelings in comments and strings! let foo = "this is speled wrong" class SpeledWrong {}` - ); - - atom.config.set('spell-check.useLocales', true); - atom.config.set('spell-check.grammars', [ - 'source.js comment', - 'source.js string', - 'text.plain.null-grammar', - ]); - - { - await conditionPromise(() => getMisspellingMarkers().length > 0); - const markers = getMisspellingMarkers(); - expect(markers.map((marker) => marker.getBufferRange())).toEqual([ - [ - [2, 22], - [2, 32], - ], - [ - [3, 19], - [3, 25], - ], - ]); - } - }); + ); + + atom.config.set('spell-check.useLocales', true); + atom.config.set('spell-check.grammars', [ + 'source.js comment', + 'source.js string', + 'text.plain.null-grammar', + ]); + + { + await conditionPromise(() => getMisspellingMarkers().length > 0); + const markers = getMisspellingMarkers(); + expect(markers.map((marker) => marker.getBufferRange())).toEqual([ + [ + [2, 22], + [2, 32], + ], + [ + [3, 19], + [3, 25], + ], + ]); + } + }); - it('interprets a bare root scope as opting out of scope whitelisting, even when other more specific segments are present', async () => { - editor.setText( - `speledWrong = 5; + it('interprets a bare root scope as opting out of scope whitelisting, even when other more specific segments are present', async () => { + editor.setText( + `speledWrong = 5; function speledWrong() {} // We only care about mispelings in comments and strings! let fxo = "this is speled wrong" class SpeledWrong {}` - ); - - atom.config.set('spell-check.useLocales', true); - atom.config.set('spell-check.grammars', [ - // Exactly as above, but with an extra `'source.js'` listing; this will - // supersede the more specific settings below. - 'source.js', - 'source.js comment', - 'source.js string', - 'text.plain.null-grammar', - ]); - - { - await conditionPromise(() => getMisspellingMarkers().length > 0); - const markers = getMisspellingMarkers(); - expect(markers.map((marker) => marker.getBufferRange())).toEqual([ - [ - [0, 0], - [0, 11], - ], - [ - [1, 9], - [1, 20], - ], - [ - [2, 22], - [2, 32], - ], - [ - [3, 4], - [3, 7], - ], - [ - [3, 19], - [3, 25], - ], - [ - [4, 6], - [4, 17], - ], - ]); - } - }); + ); + + atom.config.set('spell-check.useLocales', true); + atom.config.set('spell-check.grammars', [ + // Exactly as above, but with an extra `'source.js'` listing; this will + // supersede the more specific settings below. + 'source.js', + 'source.js comment', + 'source.js string', + 'text.plain.null-grammar', + ]); + + { + await conditionPromise(() => getMisspellingMarkers().length > 0); + const markers = getMisspellingMarkers(); + expect(markers.map((marker) => marker.getBufferRange())).toEqual([ + [ + [0, 0], + [0, 11], + ], + [ + [1, 9], + [1, 20], + ], + [ + [2, 22], + [2, 32], + ], + [ + [3, 4], + [3, 7], + ], + [ + [3, 19], + [3, 25], + ], + [ + [4, 6], + [4, 17], + ], + ]); + } + }); - it('allows a generic root scope like "source"', async () => { - editor.setText( - `speledWrong = 5; + it('allows a generic root scope like "source"', async () => { + editor.setText( + `speledWrong = 5; function speledWrong() {} // We only care about mispelings in comments and strings! let foo = "this is speled wrong" class SpeledWrong {}` - ); + ); + + atom.config.set('spell-check.useLocales', true); + atom.config.set('spell-check.grammars', [ + 'source comment', + 'source string', + 'text.plain.null-grammar', + ]); + + { + await conditionPromise(() => getMisspellingMarkers().length > 0); + const markers = getMisspellingMarkers(); + expect(markers.map((marker) => marker.getBufferRange())).toEqual([ + [ + [2, 22], + [2, 32], + ], + [ + [3, 19], + [3, 25], + ], + ]); + } + }); - atom.config.set('spell-check.useLocales', true); - atom.config.set('spell-check.grammars', [ - 'source comment', - 'source string', - 'text.plain.null-grammar', - ]); + it('allows certain scopes to be excluded from spell checking', async function () { + editor.setText( + `speledWrong = 5; +function speledWrong() {} +class SpeledWrong {}` + ); + atom.config.set('spell-check.useLocales', true); + atom.config.set('spell-check.grammars', [ + 'source.js', + 'text.plain.null-grammar', + ]); + atom.config.set('spell-check.excludedScopes', ['.function.entity']); + + { + await conditionPromise(() => getMisspellingMarkers().length > 0); + const markers = getMisspellingMarkers(); + expect(markers.map((marker) => marker.getBufferRange())).toEqual([ + [ + [0, 0], + [0, 11], + ], + [ + [2, 6], + [2, 17], + ], + ]); + } - { - await conditionPromise(() => getMisspellingMarkers().length > 0); - const markers = getMisspellingMarkers(); - expect(markers.map((marker) => marker.getBufferRange())).toEqual([ - [ - [2, 22], - [2, 32], - ], - [ - [3, 19], - [3, 25], - ], - ]); - } + { + atom.config.set('spell-check.excludedScopes', ['.functio.entity']); + await conditionPromise(() => getMisspellingMarkers().length === 3); + const markers = getMisspellingMarkers(); + expect(markers.map((marker) => marker.getBufferRange())).toEqual([ + [ + [0, 0], + [0, 11], + ], + [ + [1, 9], + [1, 20], + ], + [ + [2, 6], + [2, 17], + ], + ]); + } + + { + atom.config.set('spell-check.excludedScopes', [ + '.entity.name.type.class', + ]); + await conditionPromise(() => getMisspellingMarkers().length === 2); + const markers = getMisspellingMarkers(); + expect(markers.map((marker) => marker.getBufferRange())).toEqual([ + [ + [0, 0], + [0, 11], + ], + [ + [1, 9], + [1, 20], + ], + ]); + } + + { + atom.grammars.assignLanguageMode(editor, null); + await conditionPromise(() => getMisspellingMarkers().length === 3); + const markers = getMisspellingMarkers(); + expect(markers.map((marker) => marker.getBufferRange())).toEqual([ + [ + [0, 0], + [0, 11], + ], + [ + [1, 9], + [1, 20], + ], + [ + [2, 6], + [2, 17], + ], + ]); + } + }); + + it('allow entering of known words', async function () { + atom.config.set('spell-check.knownWords', ['GitHub', '!github', 'codez']); + atom.config.set('spell-check.useLocales', true); + editor.setText('GitHub (aka github): Where codez are builz.'); + atom.config.set('spell-check.grammars', ['source.js']); + + await conditionPromise(() => getMisspellingMarkers().length === 1); + const misspellingMarkers = getMisspellingMarkers(); + expect(textForMarker(misspellingMarkers[0])).toBe('builz'); + }); + + it('hides decorations when a misspelled word is edited', async function () { + editor.setText('notaword'); + advanceClock(editor.getBuffer().getStoppedChangingDelay()); + atom.config.set('spell-check.useLocales', true); + atom.config.set('spell-check.grammars', ['source.js']); + await conditionPromise(() => getMisspellingMarkers().length === 1); + + editor.moveToEndOfLine(); + editor.insertText('a'); + await conditionPromise(() => { + const misspellingMarkers = getMisspellingMarkers(); + return ( + misspellingMarkers.length === 1 && !misspellingMarkers[0].isValid() + ); }); + }); + + describe('when spell checking for a grammar is removed', () => + it('removes all the misspellings', async function () { + atom.config.set('spell-check.useLocales', true); + editor.setText('notaword'); + atom.config.set('spell-check.grammars', ['source.js']); + await conditionPromise(() => getMisspellingMarkers().length === 1); + + atom.config.set('spell-check.grammars', []); + expect(getMisspellingMarkers().length).toBe(0); + })); + + describe('when spell checking for a grammar is toggled off', () => + it('removes all the misspellings', async function () { + atom.config.set('spell-check.useLocales', true); + editor.setText('notaword'); + atom.config.set('spell-check.grammars', ['source.js']); + await conditionPromise(() => getMisspellingMarkers().length === 1); + + atom.commands.dispatch(workspaceElement, 'spell-check:toggle'); + expect(getMisspellingMarkers().length).toBe(0); + })); + + describe("when the editor's grammar changes to one that does not have spell check enabled", () => + it('removes all the misspellings', async function () { + atom.config.set('spell-check.useLocales', true); + editor.setText('notaword'); + atom.config.set('spell-check.grammars', ['source.js']); + + await conditionPromise(() => getMisspellingMarkers().length === 1); + const misspellingMarkers = getMisspellingMarkers(); + editor.setGrammar(atom.grammars.selectGrammar('.txt')); + expect(getMisspellingMarkers().length).toBe(0); + })); + + describe("when 'spell-check:correct-misspelling' is triggered on the editor", function () { + describe('when the cursor touches a misspelling that has corrections', () => + it('displays the corrections for the misspelling and replaces the misspelling when a correction is selected', async function () { + atom.config.set('spell-check.useLocales', true); + editor.setText('tofether'); + atom.config.set('spell-check.grammars', ['source.js']); + let correctionsElement = null; - it('allows certain scopes to be excluded from spell checking', async function () { - editor.setText( - `speledWrong = 5; -function speledWrong() {} -class SpeledWrong {}` + await conditionPromise(() => getMisspellingMarkers().length === 1); + expect(getMisspellingMarkers()[0].isValid()).toBe(true); + + atom.commands.dispatch( + editorElement, + 'spell-check:correct-misspelling' + ); + correctionsElement = editorElement.querySelector('.corrections'); + expect(correctionsElement).toBeDefined(); + expect( + correctionsElement.querySelectorAll('li').length + ).toBeGreaterThan(0); + expect(correctionsElement.querySelectorAll('li')[0].textContent).toBe( + 'together' ); + + atom.commands.dispatch(correctionsElement, 'core:confirm'); + expect(editor.getText()).toBe('together'); + expect(editor.getCursorBufferPosition()).toEqual([0, 8]); + + expect(getMisspellingMarkers()[0].isValid()).toBe(false); + expect(editorElement.querySelector('.corrections')).toBeNull(); + })); + + describe('when the cursor touches a misspelling that has no corrections', () => + it('displays a message saying no corrections found', async function () { atom.config.set('spell-check.useLocales', true); - atom.config.set('spell-check.grammars', [ - 'source.js', - 'text.plain.null-grammar', - ]); - atom.config.set('spell-check.excludedScopes', ['.function.entity']); + editor.setText('zxcasdfysyadfyasdyfasdfyasdfyasdfyasydfasdf'); + atom.config.set('spell-check.grammars', ['source.js']); + await conditionPromise(() => getMisspellingMarkers().length > 0); - { - await conditionPromise(() => getMisspellingMarkers().length > 0); - const markers = getMisspellingMarkers(); - expect(markers.map((marker) => marker.getBufferRange())).toEqual([ - [ - [0, 0], - [0, 11], - ], - [ - [2, 6], - [2, 17], - ], - ]); - } + atom.commands.dispatch( + editorElement, + 'spell-check:correct-misspelling' + ); + expect(editorElement.querySelectorAll('.corrections').length).toBe(1); + expect(editorElement.querySelectorAll('.corrections li').length).toBe( + 0 + ); + expect(editorElement.querySelector('.corrections').textContent).toMatch( + /No corrections/ + ); + })); + }); - { - atom.config.set('spell-check.excludedScopes', ['.functio.entity']); - await conditionPromise(() => getMisspellingMarkers().length === 3); - const markers = getMisspellingMarkers(); - expect(markers.map((marker) => marker.getBufferRange())).toEqual([ - [ - [0, 0], - [0, 11], - ], - [ - [1, 9], - [1, 20], - ], - [ - [2, 6], - [2, 17], - ], - ]); - } + describe('when a right mouse click is triggered on the editor', function () { + describe('when the cursor touches a misspelling that has corrections', () => + it('displays the context menu items for the misspelling and replaces the misspelling when a correction is selected', async function () { + atom.config.set('spell-check.useLocales', true); + editor.setText('tofether'); + advanceClock(editor.getBuffer().getStoppedChangingDelay()); + atom.config.set('spell-check.grammars', ['source.js']); + await conditionPromise(() => getMisspellingMarkers().length === 1); + + expect(getMisspellingMarkers()[0].isValid()).toBe(true); + editorElement.dispatchEvent(new MouseEvent('contextmenu')); + + // Check that the proper context menu entries are created for the misspelling. + // A misspelling will have atleast 2 context menu items for the lines separating + // the corrections. + expect(spellCheckModule.contextMenuEntries.length).toBeGreaterThan(2); + const commandName = 'spell-check:correct-misspelling-0'; + const menuItemLabel = 'together'; { - atom.config.set('spell-check.excludedScopes', [ - '.entity.name.type.class', - ]); - await conditionPromise(() => getMisspellingMarkers().length === 2); - const markers = getMisspellingMarkers(); - expect(markers.map((marker) => marker.getBufferRange())).toEqual([ - [ - [0, 0], - [0, 11], - ], - [ - [1, 9], - [1, 20], - ], - ]); + const editorCommands = atom.commands.findCommands({ + target: editorElement, + }); + const correctionCommand = editorCommands.filter( + (command) => command.name === commandName + )[0]; + const correctionMenuItem = atom.contextMenu.itemSets.filter( + (item) => item.items[0].label === menuItemLabel + )[0]; + expect(correctionCommand).toBeDefined(); + expect(correctionMenuItem).toBeDefined(); } + atom.commands.dispatch(editorElement, commandName); + // Check that the misspelling is corrected and the context menu entries are properly disposed. + expect(editor.getText()).toBe('together'); + expect(editor.getCursorBufferPosition()).toEqual([0, 8]); + expect(getMisspellingMarkers()[0].isValid()).toBe(false); + expect(spellCheckModule.contextMenuEntries.length).toBe(0); + { - atom.grammars.assignLanguageMode(editor, null); - await conditionPromise(() => getMisspellingMarkers().length === 3); - const markers = getMisspellingMarkers(); - expect(markers.map((marker) => marker.getBufferRange())).toEqual([ - [ - [0, 0], - [0, 11], - ], - [ - [1, 9], - [1, 20], - ], - [ - [2, 6], - [2, 17], - ], - ]); + const editorCommands = atom.commands.findCommands({ + target: editorElement, + }); + const correctionCommand = editorCommands.filter( + (command) => command.name === commandName + )[0]; + const correctionMenuItem = atom.contextMenu.itemSets.filter( + (item) => item.items[0].label === menuItemLabel + )[0]; + expect(correctionCommand).toBeUndefined(); + expect(correctionMenuItem).toBeUndefined(); } - }); + })); - it('allow entering of known words', async function () { - atom.config.set('spell-check.knownWords', [ - 'GitHub', - '!github', - 'codez', - ]); + describe('when the cursor touches a misspelling and adding known words is enabled', () => + it("displays the 'Add to Known Words' option and adds that word when the option is selected", async function () { atom.config.set('spell-check.useLocales', true); - editor.setText('GitHub (aka github): Where codez are builz.'); + editor.setText('zxcasdfysyadfyasdyfasdfyasdfyasdfyasydfasdf'); + advanceClock(editor.getBuffer().getStoppedChangingDelay()); atom.config.set('spell-check.grammars', ['source.js']); + atom.config.set('spell-check.addKnownWords', true); + + expect(atom.config.get('spell-check.knownWords').length).toBe(0); await conditionPromise(() => getMisspellingMarkers().length === 1); - const misspellingMarkers = getMisspellingMarkers(); - expect(textForMarker(misspellingMarkers[0])).toBe('builz'); + + expect(getMisspellingMarkers()[0].isValid()).toBe(true); + editorElement.dispatchEvent(new MouseEvent('contextmenu')); + + // Check that the 'Add to Known Words' entry is added to the context menu. + // There should be 1 entry for 'Add to Known Words' and 2 entries for the line separators. + expect(spellCheckModule.contextMenuEntries.length).toBe(3); + const commandName = 'spell-check:correct-misspelling-0'; + const menuItemLabel = 'together'; + + { + const editorCommands = atom.commands.findCommands({ + target: editorElement, + }); + const correctionCommand = editorCommands.filter( + (command) => command.name === commandName + )[0]; + + const correctionMenuItem = atom.contextMenu.itemSets.filter( + (item) => item.items[0].label === menuItemLabel + )[0]; + expect(correctionCommand).toBeDefined; + expect(correctionMenuItem).toBeDefined; + } + + atom.commands.dispatch(editorElement, commandName); + // Check that the misspelling is added as a known word, that there are no more misspelling + // markers in the editor, and that the context menu entries are properly disposed. + waitsFor(() => getMisspellingMarkers().length === 0); + expect(atom.config.get('spell-check.knownWords').length).toBe(1); + expect(spellCheckModule.contextMenuEntries.length).toBe(0); + + { + const editorCommands = atom.commands.findCommands({ + target: editorElement, + }); + const correctionCommand = editorCommands.filter( + (command) => command.name === commandName + )[0]; + const correctionMenuItem = atom.contextMenu.itemSets.filter( + (item) => item.items[0].label === menuItemLabel + )[0]; + expect(correctionCommand).toBeUndefined(); + expect(correctionMenuItem).toBeUndefined(); + } + })); + }); + + describe('when the editor is destroyed', () => + it('destroys all misspelling markers', async function () { + atom.config.set('spell-check.useLocales', true); + editor.setText('mispelling'); + atom.config.set('spell-check.grammars', ['source.js']); + + await conditionPromise(() => getMisspellingMarkers().length > 0); + editor.destroy(); + expect(getMisspellingMarkers().length).toBe(0); + // Check that all the views have been cleaned up. + expect(spellCheckModule.updateViews().length).toBe(0); + })); + + describe('when using checker plugins', function () { + it('no opinion on input means correctly spells', async function () { + spellCheckModule.consumeSpellCheckers( + require.resolve('./known-1-spec-checker') + ); + spellCheckModule.consumeSpellCheckers( + require.resolve('./known-2-spec-checker') + ); + spellCheckModule.consumeSpellCheckers( + require.resolve('./known-3-spec-checker') + ); + spellCheckModule.consumeSpellCheckers( + require.resolve('./known-4-spec-checker') + ); + spellCheckModule.consumeSpellCheckers( + require.resolve('./eot-spec-checker') + ); + editor.setText('eot'); + atom.config.set('spell-check.grammars', ['source.js']); + + await conditionPromise(() => getMisspellingMarkers().length === 1); + editor.destroy(); + expect(getMisspellingMarkers().length).toBe(0); }); - it('hides decorations when a misspelled word is edited', async function () { - editor.setText('notaword'); - advanceClock(editor.getBuffer().getStoppedChangingDelay()); - atom.config.set('spell-check.useLocales', true); - atom.config.set('spell-check.grammars', ['source.js']); - await conditionPromise(() => getMisspellingMarkers().length === 1); + it('correctly spelling k1a', async function () { + spellCheckModule.consumeSpellCheckers( + require.resolve('./known-1-spec-checker') + ); + spellCheckModule.consumeSpellCheckers( + require.resolve('./known-2-spec-checker') + ); + spellCheckModule.consumeSpellCheckers( + require.resolve('./known-3-spec-checker') + ); + spellCheckModule.consumeSpellCheckers( + require.resolve('./known-4-spec-checker') + ); + spellCheckModule.consumeSpellCheckers( + require.resolve('./eot-spec-checker') + ); + editor.setText('k1a eot'); + atom.config.set('spell-check.grammars', ['source.js']); + await conditionPromise(() => getMisspellingMarkers().length === 1); + editor.destroy(); + expect(getMisspellingMarkers().length).toBe(0); + }); - editor.moveToEndOfLine(); - editor.insertText('a'); - await conditionPromise(() => { - const misspellingMarkers = getMisspellingMarkers(); - return ( - misspellingMarkers.length === 1 && - !misspellingMarkers[0].isValid() - ); - }); + it('correctly mispelling k2a', async function () { + spellCheckModule.consumeSpellCheckers( + require.resolve('./known-1-spec-checker') + ); + spellCheckModule.consumeSpellCheckers( + require.resolve('./known-2-spec-checker') + ); + spellCheckModule.consumeSpellCheckers( + require.resolve('./known-3-spec-checker') + ); + spellCheckModule.consumeSpellCheckers( + require.resolve('./known-4-spec-checker') + ); + spellCheckModule.consumeSpellCheckers( + require.resolve('./eot-spec-checker') + ); + editor.setText('k2a eot'); + atom.config.set('spell-check.grammars', ['source.js']); + + await conditionPromise(() => getMisspellingMarkers().length === 2); + editor.destroy(); + expect(getMisspellingMarkers().length).toBe(0); }); - describe('when spell checking for a grammar is removed', () => - it('removes all the misspellings', async function () { - atom.config.set('spell-check.useLocales', true); - editor.setText('notaword'); - atom.config.set('spell-check.grammars', ['source.js']); - await conditionPromise(() => getMisspellingMarkers().length === 1); - - atom.config.set('spell-check.grammars', []); - expect(getMisspellingMarkers().length).toBe(0); - })); - - describe('when spell checking for a grammar is toggled off', () => - it('removes all the misspellings', async function () { - atom.config.set('spell-check.useLocales', true); - editor.setText('notaword'); - atom.config.set('spell-check.grammars', ['source.js']); - await conditionPromise(() => getMisspellingMarkers().length === 1); - - atom.commands.dispatch(workspaceElement, 'spell-check:toggle'); - expect(getMisspellingMarkers().length).toBe(0); - })); - - describe("when the editor's grammar changes to one that does not have spell check enabled", () => - it('removes all the misspellings', async function () { - atom.config.set('spell-check.useLocales', true); - editor.setText('notaword'); - atom.config.set('spell-check.grammars', ['source.js']); - - await conditionPromise(() => getMisspellingMarkers().length === 1); - const misspellingMarkers = getMisspellingMarkers(); - editor.setGrammar(atom.grammars.selectGrammar('.txt')); - expect(getMisspellingMarkers().length).toBe(0); - })); - - describe("when 'spell-check:correct-misspelling' is triggered on the editor", function () { - describe('when the cursor touches a misspelling that has corrections', () => - it('displays the corrections for the misspelling and replaces the misspelling when a correction is selected', async function () { - atom.config.set('spell-check.useLocales', true); - editor.setText('tofether'); - atom.config.set('spell-check.grammars', ['source.js']); - let correctionsElement = null; - - await conditionPromise( - () => getMisspellingMarkers().length === 1 - ); - expect(getMisspellingMarkers()[0].isValid()).toBe(true); - - atom.commands.dispatch( - editorElement, - 'spell-check:correct-misspelling' - ); - correctionsElement = editorElement.querySelector( - '.corrections' - ); - expect(correctionsElement).toBeDefined(); - expect( - correctionsElement.querySelectorAll('li').length - ).toBeGreaterThan(0); - expect( - correctionsElement.querySelectorAll('li')[0].textContent - ).toBe('together'); - - atom.commands.dispatch(correctionsElement, 'core:confirm'); - expect(editor.getText()).toBe('together'); - expect(editor.getCursorBufferPosition()).toEqual([0, 8]); - - expect(getMisspellingMarkers()[0].isValid()).toBe(false); - expect(editorElement.querySelector('.corrections')).toBeNull(); - })); - - describe('when the cursor touches a misspelling that has no corrections', () => - it('displays a message saying no corrections found', async function () { - atom.config.set('spell-check.useLocales', true); - editor.setText('zxcasdfysyadfyasdyfasdfyasdfyasdfyasydfasdf'); - atom.config.set('spell-check.grammars', ['source.js']); - await conditionPromise( - () => getMisspellingMarkers().length > 0 - ); - - atom.commands.dispatch( - editorElement, - 'spell-check:correct-misspelling' - ); - expect( - editorElement.querySelectorAll('.corrections').length - ).toBe(1); - expect( - editorElement.querySelectorAll('.corrections li').length - ).toBe(0); - expect( - editorElement.querySelector('.corrections').textContent - ).toMatch(/No corrections/); - })); + it('correctly mispelling k2a with text in middle', async function () { + spellCheckModule.consumeSpellCheckers( + require.resolve('./known-1-spec-checker') + ); + spellCheckModule.consumeSpellCheckers( + require.resolve('./known-2-spec-checker') + ); + spellCheckModule.consumeSpellCheckers( + require.resolve('./known-3-spec-checker') + ); + spellCheckModule.consumeSpellCheckers( + require.resolve('./known-4-spec-checker') + ); + spellCheckModule.consumeSpellCheckers( + require.resolve('./eot-spec-checker') + ); + editor.setText('k2a good eot'); + atom.config.set('spell-check.grammars', ['source.js']); + + await conditionPromise(() => getMisspellingMarkers().length === 2); + editor.destroy(); + expect(getMisspellingMarkers().length).toBe(0); }); - describe('when a right mouse click is triggered on the editor', function () { - describe('when the cursor touches a misspelling that has corrections', () => - it('displays the context menu items for the misspelling and replaces the misspelling when a correction is selected', async function () { - atom.config.set('spell-check.useLocales', true); - editor.setText('tofether'); - advanceClock(editor.getBuffer().getStoppedChangingDelay()); - atom.config.set('spell-check.grammars', ['source.js']); - await conditionPromise( - () => getMisspellingMarkers().length === 1 - ); - - expect(getMisspellingMarkers()[0].isValid()).toBe(true); - editorElement.dispatchEvent(new MouseEvent('contextmenu')); - - // Check that the proper context menu entries are created for the misspelling. - // A misspelling will have atleast 2 context menu items for the lines separating - // the corrections. - expect( - spellCheckModule.contextMenuEntries.length - ).toBeGreaterThan(2); - const commandName = 'spell-check:correct-misspelling-0'; - const menuItemLabel = 'together'; - - { - const editorCommands = atom.commands.findCommands({ - target: editorElement, - }); - const correctionCommand = editorCommands.filter( - (command) => command.name === commandName - )[0]; - const correctionMenuItem = atom.contextMenu.itemSets.filter( - (item) => item.items[0].label === menuItemLabel - )[0]; - expect(correctionCommand).toBeDefined(); - expect(correctionMenuItem).toBeDefined(); - } - - atom.commands.dispatch(editorElement, commandName); - // Check that the misspelling is corrected and the context menu entries are properly disposed. - expect(editor.getText()).toBe('together'); - expect(editor.getCursorBufferPosition()).toEqual([0, 8]); - expect(getMisspellingMarkers()[0].isValid()).toBe(false); - expect(spellCheckModule.contextMenuEntries.length).toBe(0); - - { - const editorCommands = atom.commands.findCommands({ - target: editorElement, - }); - const correctionCommand = editorCommands.filter( - (command) => command.name === commandName - )[0]; - const correctionMenuItem = atom.contextMenu.itemSets.filter( - (item) => item.items[0].label === menuItemLabel - )[0]; - expect(correctionCommand).toBeUndefined(); - expect(correctionMenuItem).toBeUndefined(); - } - })); - - describe('when the cursor touches a misspelling and adding known words is enabled', () => - it("displays the 'Add to Known Words' option and adds that word when the option is selected", async function () { - atom.config.set('spell-check.useLocales', true); - editor.setText('zxcasdfysyadfyasdyfasdfyasdfyasdfyasydfasdf'); - advanceClock(editor.getBuffer().getStoppedChangingDelay()); - atom.config.set('spell-check.grammars', ['source.js']); - atom.config.set('spell-check.addKnownWords', true); - - expect(atom.config.get('spell-check.knownWords').length).toBe( - 0 - ); - - await conditionPromise( - () => getMisspellingMarkers().length === 1 - ); - - expect(getMisspellingMarkers()[0].isValid()).toBe(true); - editorElement.dispatchEvent(new MouseEvent('contextmenu')); - - // Check that the 'Add to Known Words' entry is added to the context menu. - // There should be 1 entry for 'Add to Known Words' and 2 entries for the line separators. - expect(spellCheckModule.contextMenuEntries.length).toBe(3); - const commandName = 'spell-check:correct-misspelling-0'; - const menuItemLabel = 'together'; - - { - const editorCommands = atom.commands.findCommands({ - target: editorElement, - }); - const correctionCommand = editorCommands.filter( - (command) => command.name === commandName - )[0]; - - const correctionMenuItem = atom.contextMenu.itemSets.filter( - (item) => item.items[0].label === menuItemLabel - )[0]; - expect(correctionCommand).toBeDefined; - expect(correctionMenuItem).toBeDefined; - } - - atom.commands.dispatch(editorElement, commandName); - // Check that the misspelling is added as a known word, that there are no more misspelling - // markers in the editor, and that the context menu entries are properly disposed. - waitsFor(() => getMisspellingMarkers().length === 0); - expect(atom.config.get('spell-check.knownWords').length).toBe( - 1 - ); - expect(spellCheckModule.contextMenuEntries.length).toBe(0); - - { - const editorCommands = atom.commands.findCommands({ - target: editorElement, - }); - const correctionCommand = editorCommands.filter( - (command) => command.name === commandName - )[0]; - const correctionMenuItem = atom.contextMenu.itemSets.filter( - (item) => item.items[0].label === menuItemLabel - )[0]; - expect(correctionCommand).toBeUndefined(); - expect(correctionMenuItem).toBeUndefined(); - } - })); + it('word is both correct and incorrect is correct', async function () { + spellCheckModule.consumeSpellCheckers( + require.resolve('./known-1-spec-checker') + ); + spellCheckModule.consumeSpellCheckers( + require.resolve('./known-2-spec-checker') + ); + spellCheckModule.consumeSpellCheckers( + require.resolve('./known-3-spec-checker') + ); + spellCheckModule.consumeSpellCheckers( + require.resolve('./known-4-spec-checker') + ); + spellCheckModule.consumeSpellCheckers( + require.resolve('./eot-spec-checker') + ); + editor.setText('k0a eot'); + atom.config.set('spell-check.grammars', ['source.js']); + + await conditionPromise(() => getMisspellingMarkers().length === 1); + editor.destroy(); + expect(getMisspellingMarkers().length).toBe(0); }); - describe('when the editor is destroyed', () => - it('destroys all misspelling markers', async function () { - atom.config.set('spell-check.useLocales', true); - editor.setText('mispelling'); - atom.config.set('spell-check.grammars', ['source.js']); - - await conditionPromise(() => getMisspellingMarkers().length > 0); - editor.destroy(); - expect(getMisspellingMarkers().length).toBe(0); - // Check that all the views have been cleaned up. - expect(spellCheckModule.updateViews().length).toBe(0); - })); - - describe('when using checker plugins', function () { - it('no opinion on input means correctly spells', async function () { - spellCheckModule.consumeSpellCheckers( - require.resolve('./known-1-spec-checker') - ); - spellCheckModule.consumeSpellCheckers( - require.resolve('./known-2-spec-checker') - ); - spellCheckModule.consumeSpellCheckers( - require.resolve('./known-3-spec-checker') - ); - spellCheckModule.consumeSpellCheckers( - require.resolve('./known-4-spec-checker') - ); - spellCheckModule.consumeSpellCheckers( - require.resolve('./eot-spec-checker') - ); - editor.setText('eot'); - atom.config.set('spell-check.grammars', ['source.js']); - - await conditionPromise(() => getMisspellingMarkers().length === 1); - editor.destroy(); - expect(getMisspellingMarkers().length).toBe(0); - }); + it('word is correct twice is correct', async function () { + spellCheckModule.consumeSpellCheckers( + require.resolve('./known-1-spec-checker') + ); + spellCheckModule.consumeSpellCheckers( + require.resolve('./known-2-spec-checker') + ); + spellCheckModule.consumeSpellCheckers( + require.resolve('./known-3-spec-checker') + ); + spellCheckModule.consumeSpellCheckers( + require.resolve('./known-4-spec-checker') + ); + spellCheckModule.consumeSpellCheckers( + require.resolve('./eot-spec-checker') + ); + editor.setText('k0b eot'); + atom.config.set('spell-check.grammars', ['source.js']); + + await conditionPromise(() => getMisspellingMarkers().length === 1); + editor.destroy(); + expect(getMisspellingMarkers().length).toBe(0); + }); - it('correctly spelling k1a', async function () { - spellCheckModule.consumeSpellCheckers( - require.resolve('./known-1-spec-checker') - ); - spellCheckModule.consumeSpellCheckers( - require.resolve('./known-2-spec-checker') - ); - spellCheckModule.consumeSpellCheckers( - require.resolve('./known-3-spec-checker') - ); - spellCheckModule.consumeSpellCheckers( - require.resolve('./known-4-spec-checker') - ); - spellCheckModule.consumeSpellCheckers( - require.resolve('./eot-spec-checker') - ); - editor.setText('k1a eot'); - atom.config.set('spell-check.grammars', ['source.js']); - await conditionPromise(() => getMisspellingMarkers().length === 1); - editor.destroy(); - expect(getMisspellingMarkers().length).toBe(0); - }); + it('word is incorrect twice is incorrect', async function () { + spellCheckModule.consumeSpellCheckers( + require.resolve('./known-1-spec-checker') + ); + spellCheckModule.consumeSpellCheckers( + require.resolve('./known-2-spec-checker') + ); + spellCheckModule.consumeSpellCheckers( + require.resolve('./known-3-spec-checker') + ); + spellCheckModule.consumeSpellCheckers( + require.resolve('./known-4-spec-checker') + ); + spellCheckModule.consumeSpellCheckers( + require.resolve('./eot-spec-checker') + ); + editor.setText('k0c eot'); + atom.config.set('spell-check.grammars', ['source.js']); + + await conditionPromise(() => getMisspellingMarkers().length === 2); + editor.destroy(); + expect(getMisspellingMarkers().length).toBe(0); + }); - it('correctly mispelling k2a', async function () { - spellCheckModule.consumeSpellCheckers( - require.resolve('./known-1-spec-checker') - ); - spellCheckModule.consumeSpellCheckers( - require.resolve('./known-2-spec-checker') - ); - spellCheckModule.consumeSpellCheckers( - require.resolve('./known-3-spec-checker') - ); - spellCheckModule.consumeSpellCheckers( - require.resolve('./known-4-spec-checker') - ); - spellCheckModule.consumeSpellCheckers( - require.resolve('./eot-spec-checker') - ); - editor.setText('k2a eot'); - atom.config.set('spell-check.grammars', ['source.js']); - - await conditionPromise(() => getMisspellingMarkers().length === 2); - editor.destroy(); - expect(getMisspellingMarkers().length).toBe(0); - }); + it('treats unknown Unicode words as incorrect', async function () { + spellCheckModule.consumeSpellCheckers( + require.resolve('./eot-spec-checker') + ); + editor.setText('абырг eot'); + atom.config.set('spell-check.grammars', ['source.js']); + expect(atom.config.get('spell-check.knownWords').length).toBe(0); + + await conditionPromise(() => getMisspellingMarkers().length > 0); + const markers = getMisspellingMarkers(); + expect(markers[0].getBufferRange()).toEqual({ + start: { row: 0, column: 6 }, + end: { row: 0, column: 9 }, + }); + + editor.destroy(); + expect(getMisspellingMarkers().length).toBe(0); + }); - it('correctly mispelling k2a with text in middle', async function () { - spellCheckModule.consumeSpellCheckers( - require.resolve('./known-1-spec-checker') - ); - spellCheckModule.consumeSpellCheckers( - require.resolve('./known-2-spec-checker') - ); - spellCheckModule.consumeSpellCheckers( - require.resolve('./known-3-spec-checker') - ); - spellCheckModule.consumeSpellCheckers( - require.resolve('./known-4-spec-checker') - ); - spellCheckModule.consumeSpellCheckers( - require.resolve('./eot-spec-checker') - ); - editor.setText('k2a good eot'); - atom.config.set('spell-check.grammars', ['source.js']); - - await conditionPromise(() => getMisspellingMarkers().length === 2); - editor.destroy(); - expect(getMisspellingMarkers().length).toBe(0); - }); + it('treats known Unicode words as correct', async function () { + spellCheckModule.consumeSpellCheckers( + require.resolve('./known-unicode-spec-checker') + ); + spellCheckModule.consumeSpellCheckers( + require.resolve('./eot-spec-checker') + ); + editor.setText('абырг eot'); + atom.config.set('spell-check.grammars', ['source.js']); + expect(atom.config.get('spell-check.knownWords').length).toBe(0); + + await conditionPromise(() => getMisspellingMarkers().length === 1); + editor.destroy(); + expect(getMisspellingMarkers().length).toBe(0); + }); + }); + + // These tests are only run on Macs because the CI for Windows doesn't have + // spelling provided. + if (env.isSystemSupported() && env.isDarwin()) { + let markers; + describe('when using system checker plugin', function () { + it('marks chzz as not a valid word but cheese is', async function () { + atom.config.set('spell-check.useSystem', true); + editor.setText('cheese chzz'); + atom.config.set('spell-check.grammars', ['source.js']); - it('word is both correct and incorrect is correct', async function () { - spellCheckModule.consumeSpellCheckers( - require.resolve('./known-1-spec-checker') - ); - spellCheckModule.consumeSpellCheckers( - require.resolve('./known-2-spec-checker') - ); - spellCheckModule.consumeSpellCheckers( - require.resolve('./known-3-spec-checker') - ); - spellCheckModule.consumeSpellCheckers( - require.resolve('./known-4-spec-checker') - ); - spellCheckModule.consumeSpellCheckers( - require.resolve('./eot-spec-checker') - ); - editor.setText('k0a eot'); - atom.config.set('spell-check.grammars', ['source.js']); - - await conditionPromise(() => getMisspellingMarkers().length === 1); - editor.destroy(); - expect(getMisspellingMarkers().length).toBe(0); + await conditionPromise(() => { + markers = getMisspellingMarkers(); + return ( + markers.length === 1 && + markers[0].getBufferRange().start.column === 7 && + markers[0].getBufferRange().end.column === 11 + ); }); - it('word is correct twice is correct', async function () { - spellCheckModule.consumeSpellCheckers( - require.resolve('./known-1-spec-checker') - ); - spellCheckModule.consumeSpellCheckers( - require.resolve('./known-2-spec-checker') - ); - spellCheckModule.consumeSpellCheckers( - require.resolve('./known-3-spec-checker') - ); - spellCheckModule.consumeSpellCheckers( - require.resolve('./known-4-spec-checker') - ); - spellCheckModule.consumeSpellCheckers( - require.resolve('./eot-spec-checker') - ); - editor.setText('k0b eot'); - atom.config.set('spell-check.grammars', ['source.js']); - - await conditionPromise(() => getMisspellingMarkers().length === 1); - editor.destroy(); - expect(getMisspellingMarkers().length).toBe(0); - }); + editor.destroy(); + expect(getMisspellingMarkers().length).toBe(0); + }); - it('word is incorrect twice is incorrect', async function () { - spellCheckModule.consumeSpellCheckers( - require.resolve('./known-1-spec-checker') - ); - spellCheckModule.consumeSpellCheckers( - require.resolve('./known-2-spec-checker') - ); - spellCheckModule.consumeSpellCheckers( - require.resolve('./known-3-spec-checker') - ); - spellCheckModule.consumeSpellCheckers( - require.resolve('./known-4-spec-checker') - ); - spellCheckModule.consumeSpellCheckers( - require.resolve('./eot-spec-checker') - ); - editor.setText('k0c eot'); - atom.config.set('spell-check.grammars', ['source.js']); - - await conditionPromise(() => getMisspellingMarkers().length === 2); - editor.destroy(); - expect(getMisspellingMarkers().length).toBe(0); - }); + it('marks multiple words as wrong', async function () { + atom.config.set('spell-check.useSystem', true); + editor.setText('cheese chz chzz chzzz'); + atom.config.set('spell-check.grammars', ['source.js']); - it('treats unknown Unicode words as incorrect', async function () { - spellCheckModule.consumeSpellCheckers( - require.resolve('./eot-spec-checker') - ); - editor.setText('абырг eot'); - atom.config.set('spell-check.grammars', ['source.js']); - expect(atom.config.get('spell-check.knownWords').length).toBe(0); - - await conditionPromise(() => getMisspellingMarkers().length > 0); - const markers = getMisspellingMarkers(); - expect(markers[0].getBufferRange()).toEqual({ - start: { row: 0, column: 6 }, - end: { row: 0, column: 9 }, - }); - - editor.destroy(); - expect(getMisspellingMarkers().length).toBe(0); + await conditionPromise(() => { + markers = getMisspellingMarkers(); + return ( + markers.length === 3 && + markers[0].getBufferRange().start.column === 7 && + markers[0].getBufferRange().end.column === 10 && + markers[1].getBufferRange().start.column === 11 && + markers[1].getBufferRange().end.column === 15 && + markers[2].getBufferRange().start.column === 16 && + markers[2].getBufferRange().end.column === 21 + ); }); - it('treats known Unicode words as correct', async function () { - spellCheckModule.consumeSpellCheckers( - require.resolve('./known-unicode-spec-checker') - ); - spellCheckModule.consumeSpellCheckers( - require.resolve('./eot-spec-checker') - ); - editor.setText('абырг eot'); - atom.config.set('spell-check.grammars', ['source.js']); - expect(atom.config.get('spell-check.knownWords').length).toBe(0); - - await conditionPromise(() => getMisspellingMarkers().length === 1); - editor.destroy(); - expect(getMisspellingMarkers().length).toBe(0); - }); + editor.destroy(); + expect(getMisspellingMarkers().length).toBe(0); + }); }); - - // These tests are only run on Macs because the CI for Windows doesn't have - // spelling provided. - if (env.isSystemSupported() && env.isDarwin()) { - let markers; - describe('when using system checker plugin', function () { - it('marks chzz as not a valid word but cheese is', async function () { - atom.config.set('spell-check.useSystem', true); - editor.setText('cheese chzz'); - atom.config.set('spell-check.grammars', ['source.js']); - - await conditionPromise(() => { - markers = getMisspellingMarkers(); - return ( - markers.length === 1 && - markers[0].getBufferRange().start.column === 7 && - markers[0].getBufferRange().end.column === 11 - ); - }); - - editor.destroy(); - expect(getMisspellingMarkers().length).toBe(0); - }); - - it('marks multiple words as wrong', async function () { - atom.config.set('spell-check.useSystem', true); - editor.setText('cheese chz chzz chzzz'); - atom.config.set('spell-check.grammars', ['source.js']); - - await conditionPromise(() => { - markers = getMisspellingMarkers(); - return ( - markers.length === 3 && - markers[0].getBufferRange().start.column === 7 && - markers[0].getBufferRange().end.column === 10 && - markers[1].getBufferRange().start.column === 11 && - markers[1].getBufferRange().end.column === 15 && - markers[2].getBufferRange().start.column === 16 && - markers[2].getBufferRange().end.column === 21 - ); - }); - - editor.destroy(); - expect(getMisspellingMarkers().length).toBe(0); - }); - }); - } else { - console.log( - "Skipping system checker tests because they don't run on Windows CI or Linux" - ); - } + } else { + console.log( + "Skipping system checker tests because they don't run on Windows CI or Linux" + ); + } });