diff --git a/docs/SPELLING.md b/docs/SPELLING.md index ee8725e5..359444a2 100644 --- a/docs/SPELLING.md +++ b/docs/SPELLING.md @@ -1,6 +1,6 @@ # Spelling -Mark Text can automatically check your text for misspelled words as you type and suggest corrections. You just need to enable spell checking in settings under *spelling* to never miss a misspelled word. We're using Hunspell for Linux and Windows and on macOS you can choose between Hunspell or the system spell checker (default). You can control the default proofing language via settings but can change the language at runtime via right-click menu `Change Language` entry under `Spelling` without changing the default language. By default Mark Text only support American English for Hunspell and the local available languages for macOS spell checker. You can download 42 languages for Hunspell and many more for macOS. +Mark Text can automatically check your text for misspelled words as you type and suggest corrections. You just need to enable spell checking in settings under *spelling* to never miss a misspelled word. We're using Hunspell for Linux and older Windows versions and on macOS and Window 10 you can choose between Hunspell or the system spell checker (default). You can control the default proofing language via settings but can change the language at runtime via right-click menu `Change Language` entry under `Spelling` without changing the default language. By default Mark Text only support American English for Hunspell and the local available languages for the system spell checker. You can download 42 languages for Hunspell and many more for macOS and Windows 10 via system settings. ![](assets/marktext-spellchecker-menu.png) @@ -22,7 +22,11 @@ You can add words to the selected dictionary by right-clicking on a misspelled w ### macOS spell checker -You need to add the needed language dictionaries via *Language & Region* in your system preferences pane. +You need to add the additional language dictionaries via *"Language & Region"* in your system preferences pane. + +### Windows spell checker + +On Windows 10, you need to add additional language dictionaries via *"Language"* in your *"Time & language"* settings. Add the additional language(s) and download the *"Basic typing"* language option for each language. ### Hunspell diff --git a/docs/assets/marktext-spelling-settings.png b/docs/assets/marktext-spelling-settings.png index 81c05aac..96052d66 100644 Binary files a/docs/assets/marktext-spelling-settings.png and b/docs/assets/marktext-spelling-settings.png differ diff --git a/docs/dev/BUILD.md b/docs/dev/BUILD.md index 26ca89e1..bf35bb20 100644 --- a/docs/dev/BUILD.md +++ b/docs/dev/BUILD.md @@ -10,9 +10,10 @@ git clone https://github.com/marktext/marktext.git Before you can get started developing, you need set up your build environment: -- Node.js `>=v12.0.0`, npm and yarn +- Node.js `>=v12.0.0` and yarn - Python `v2.7.x` for node-gyp - C++ compiler and development tools +- Build is supported on Linux, macOS and Windows 10 **Additional development dependencies on Linux:** diff --git a/package.json b/package.json index fd2bb5b1..badc2650 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ }, "dependencies": { "@hfelix/electron-localshortcut": "^3.1.1", - "@hfelix/electron-spellchecker": "^1.0.0-rc.3", + "@hfelix/electron-spellchecker": "1.0.0-rc.5", "@octokit/rest": "^16.33.1", "arg": "^4.1.1", "axios": "^0.19.0", diff --git a/src/index.ejs b/src/index.ejs index e2b05c46..e7dcbffe 100644 --- a/src/index.ejs +++ b/src/index.ejs @@ -81,14 +81,19 @@ diff --git a/src/main/app/index.js b/src/main/app/index.js index 84baf3c2..9891a94e 100644 --- a/src/main/app/index.js +++ b/src/main/app/index.js @@ -412,7 +412,7 @@ class App { } _openSettingsWindow () { - const settingWins = this._windowManager.getWindowsByType(WindowType.SETTING) + const settingWins = this._windowManager.getWindowsByType(WindowType.SETTINGS) if (settingWins.length >= 1) { // A setting window is already created const browserSettingWindow = settingWins[0].win.browserWindow diff --git a/src/main/app/windowManager.js b/src/main/app/windowManager.js index 74f2fe26..1447cca7 100644 --- a/src/main/app/windowManager.js +++ b/src/main/app/windowManager.js @@ -199,7 +199,7 @@ class WindowManager extends EventEmitter { /** * - * @param {WindowType} type the WindowType one of ['base', 'editor', 'setting'] + * @param {WindowType} type the WindowType one of ['base', 'editor', 'settings'] * @returns {{id: number, win: BaseWindow}[]} Return the windows of the given {type} */ getWindowsByType (type) { diff --git a/src/main/windows/base.js b/src/main/windows/base.js index 2e878f55..135b92ac 100644 --- a/src/main/windows/base.js +++ b/src/main/windows/base.js @@ -14,7 +14,7 @@ import { isLinux } from '../config' export const WindowType = { BASE: 'base', // You shold never create a `BASE` window. EDITOR: 'editor', - SETTING: 'setting' + SETTINGS: 'settings' } export const WindowLifecycle = { diff --git a/src/main/windows/setting.js b/src/main/windows/setting.js index a721987e..a704653e 100644 --- a/src/main/windows/setting.js +++ b/src/main/windows/setting.js @@ -11,7 +11,7 @@ class SettingWindow extends BaseWindow { */ constructor (accessor) { super(accessor) - this.type = WindowType.SETTING + this.type = WindowType.SETTINGS } /** diff --git a/src/renderer/bootstrap.js b/src/renderer/bootstrap.js index e2934c42..32a4aab7 100644 --- a/src/renderer/bootstrap.js +++ b/src/renderer/bootstrap.js @@ -99,7 +99,10 @@ const bootstrapRenderer = () => { global.marktext = marktext // Set option to always use Hunspell instead OS spell checker. - if (spellcheckerIsHunspell) { + if (spellcheckerIsHunspell && type !== 'settings') { + // HACK: This code doesn't do anything because `node-spellchecker` is loaded by + // `internal/modules/cjs/loader.js` before we can set the envoriment variable here. + // The code is additionally added to `index.ejs` to workaound the problem. process.env['SPELLCHECKER_PREFER_HUNSPELL'] = 1 // eslint-disable-line dot-notation } diff --git a/src/renderer/components/editorWithTabs/editor.vue b/src/renderer/components/editorWithTabs/editor.vue index daf9534b..eab916cf 100644 --- a/src/renderer/components/editorWithTabs/editor.vue +++ b/src/renderer/components/editorWithTabs/editor.vue @@ -97,8 +97,8 @@ import { DEFAULT_EDITOR_FONT_FAMILY } from '@/config' import { showContextMenu } from '@/contextMenu/editor' import notice from '@/services/notification' import Printer from '@/services/printService' -import { offsetToWordCursor, validateLineCursor, SpellChecker } from '@/spellchecker' -import { isOsx, animatedScrollTo } from '@/util' +import { isOsSpellcheckerSupported, offsetToWordCursor, validateLineCursor, SpellChecker } from '@/spellchecker' +import { delay, isOsx, animatedScrollTo } from '@/util' import { moveImageToFolder, uploadImage } from '@/util/fileSystem' import { guessClipboardFilePath } from '@/util/clipboard' import { getCssForOptions } from '@/util/pdf' @@ -373,19 +373,21 @@ export default { // Special case when the OS supports multiple spell checker because the // language may be invalid (provider 1 may support language xyz // but provider 2 not). Otherwise ignore this event. - const multiProviderSupported = isOsx - if (multiProviderSupported && value !== oldValue) { + if (isOsSpellcheckerSupported() && value !== oldValue) { const { spellchecker } = this const { isHunspell } = spellchecker if (value === isHunspell) { this.spellcheckerIgnorChanges = false - // Apply language from settings that may have changed. - const { spellcheckerLanguage } = this - const { isEnabled, lang } = spellchecker - if (isEnabled && spellcheckerLanguage !== lang) { - this.switchSpellcheckLanguage(spellcheckerLanguage) - } + // NOTE: Set timout because the language may be changed if it's not supported. + delay(500).then(() => { + // Apply language from settings that may have changed. + const { spellcheckerLanguage } = this + const { isEnabled, isHunspell, lang } = spellchecker + if (value === isHunspell && isEnabled && spellcheckerLanguage !== lang) { + this.switchSpellcheckLanguage(spellcheckerLanguage) + } + }) } else { // Ignore all settings language changes that occur when another // spell check provider is selected. @@ -641,20 +643,27 @@ export default { // Translate offsets into a cursor with the given line. const wordRange = offsetToWordCursor(selection, left, right) - this.spellchecker.getWordSuggestion(word) - .then(wordSuggestions => { - const replaceCallback = replacement => { - // wordRange := replace this range with the replacement - this.editor.replaceWordInline(selection, wordRange, replacement, true) - } - showContextMenu(event, selection, this.spellchecker, word, wordSuggestions, replaceCallback) - }) + + // NOTE: Need to check whether the word is misspelled because + // suggestions may be empty even if word is misspelled. + if (this.spellchecker.isMisspelled(word)) { + this.spellchecker.getWordSuggestion(word) + .then(wordSuggestions => { + const replaceCallback = replacement => { + // wordRange := replace this range with the replacement + this.editor.replaceWordInline(selection, wordRange, replacement, true) + } + showContextMenu(event, selection, this.spellchecker, word, wordSuggestions, replaceCallback) + }) + } else { + showContextMenu(event, selection, this.spellchecker, word, null, null) + } return } } // No word selected or fallback - showContextMenu(event, selection, isEnabled ? this.spellchecker : null, '', [], null) + showContextMenu(event, selection, isEnabled ? this.spellchecker : null, '', null, null) }) document.addEventListener('keyup', this.keyup) diff --git a/src/renderer/components/sideBar/index.vue b/src/renderer/components/sideBar/index.vue index ef3582e5..9c603b46 100644 --- a/src/renderer/components/sideBar/index.vue +++ b/src/renderer/components/sideBar/index.vue @@ -126,7 +126,7 @@ export default { } }, handleLeftBottomClick (name) { - if (name === 'setting') { + if (name === 'settings') { this.$store.dispatch('OPEN_SETTING_WINDOW') } } diff --git a/src/renderer/contextMenu/editor/spellcheck.js b/src/renderer/contextMenu/editor/spellcheck.js index 7be59cd0..b480993d 100644 --- a/src/renderer/contextMenu/editor/spellcheck.js +++ b/src/renderer/contextMenu/editor/spellcheck.js @@ -40,8 +40,8 @@ export default (spellchecker, selectedWord, wordSuggestions, replaceCallback) => spellingSubmenu.push(SEPARATOR) - // Word suggestions - if (selectedWord && wordSuggestions && wordSuggestions.length > 0) { + // Handle misspelled word if wordSuggestions is set, otherwise word is correct. + if (selectedWord && wordSuggestions) { spellingSubmenu.push({ label: 'Add to Dictionary', click (menuItem, targetWindow) { @@ -63,16 +63,19 @@ export default (spellchecker, selectedWord, wordSuggestions, replaceCallback) => spellchecker.ignoreWord(selectedWord) } }) - spellingSubmenu.push(SEPARATOR) - for (const word of wordSuggestions) { - spellingSubmenu.push({ - label: word, - click () { - // Notify Muya to replace the word. We cannot just use Chromium to - // replace the word because the change is not forwarded to Muya. - replaceCallback(word) - } - }) + + if (wordSuggestions.length > 0) { + spellingSubmenu.push(SEPARATOR) + for (const word of wordSuggestions) { + spellingSubmenu.push({ + label: word, + click () { + // Notify Muya to replace the word. We cannot just use Chromium to + // replace the word because the change is not forwarded to Muya. + replaceCallback(word) + } + }) + } } } else { spellingSubmenu.push({ diff --git a/src/renderer/prefComponents/spellchecker/index.vue b/src/renderer/prefComponents/spellchecker/index.vue index 5de8b4c3..7e0fc510 100644 --- a/src/renderer/prefComponents/spellchecker/index.vue +++ b/src/renderer/prefComponents/spellchecker/index.vue @@ -4,13 +4,13 @@ +
- Please add the needed language dictionaries via Language & Region in your system preferences pane. + Please add needed language dictionaries via "Language & Region" in your system preferences pane. +
+
+ Please add needed language dictionaries via "Language" in your "Time & language" settings. Add the additional language and download the "Basic typing" language option.
-
List of available Hunspell dictionaries. Please add additional language dictionaries via drop-down menu below.
state.preferences.spellcheckerAutoDetectLanguage, spellcheckerLanguage: state => state.preferences.spellcheckerLanguage, isHunspellSelected: state => { - return !isOsx || state.preferences.spellcheckerIsHunspell + return !isOsSpellcheckerSupported() || state.preferences.spellcheckerIsHunspell } }) }, watch: { spellcheckerIsHunspell: function (value, oldValue) { - if (isOsx && value !== oldValue && value) { - const { spellcheckerLanguage } = this - const index = HUNSPELL_DICTIONARY_LANGUAGE_MAP.findIndex(d => d.value === spellcheckerLanguage) - if (index === -1) { - // Language is not supported by Hunspell. - this.onSelectChange('spellcheckerLanguage', 'en-US') - } + if (this.isOsSpellcheckerSupported && value !== oldValue) { + this.ensureDictLanguage(value) this.refreshDictionaryList() } } @@ -141,8 +145,8 @@ export default { }) }, beforeDestroy () { - if (isOsx && this.spellChecker) { - this.spellChecker.provider.unsubscribe() + if (!isLinux && this.spellchecker) { + this.spellchecker.provider.unsubscribe() } }, methods: { @@ -152,14 +156,14 @@ export default { // Search hunspell dictionaries on disk. dictionaries = getAvailableHunspellDictionaries() } else { - // On macOS we only receive the dictionaries when the spell checker is active. - if (!this.spellChecker) { + // We only receive the dictionaries from OS spell checker via the instance. + if (!this.spellchecker) { // Create a new spell checker provider without attach it. - this.spellChecker = new SpellChecker() + this.spellchecker = new SpellChecker() } // Receive available dictionaries from OS. - dictionaries = this.spellChecker.getAvailableDictionaries() + dictionaries = this.spellchecker.getAvailableDictionaries() } return dictionaries.map(item => { @@ -172,6 +176,47 @@ export default { refreshDictionaryList () { this.availableDictionaries = this.getAvailableDictionaries() }, + ensureDictLanguage (isHunspell) { + const { isOsSpellcheckerSupported, spellcheckerLanguage } = this + if (isHunspell || !isOsSpellcheckerSupported) { + // Validate language for Hunspell. + const index = HUNSPELL_DICTIONARY_LANGUAGE_MAP.findIndex(d => d.value === spellcheckerLanguage) + if (index === -1) { + // Use fallback because language is not supported by Hunspell. + this.onSelectChange('spellcheckerLanguage', 'en-US') + } + } else { + // Validate language for OS spellchecker. We only receive the dictionaries from + // OS spell checker via the instance. + if (!this.spellchecker) { + // Create a new spell checker provider without attach it. + this.spellchecker = new SpellChecker() + } + + const dicts = this.spellchecker.getAvailableDictionaries() + const index = dicts.findIndex(d => d === spellcheckerLanguage) + if (index === -1 && dicts.length >= 1) { + // Language is not supported, prefer OS language. + var lang = process.env.LANG + lang = lang ? lang.split('.')[0] : null + if (lang) { + lang = lang.replace(/_/g, '-') + if (dicts.findIndex(d => d === lang) === -1) { + lang = null + } + } + this.onSelectChange('spellcheckerLanguage', lang || dicts[0]) + } + } + }, + + handleSpellcheckerEnabled (value) { + if (value) { + const { spellcheckerIsHunspell } = this + this.ensureDictLanguage(spellcheckerIsHunspell) + } + this.onSelectChange('spellcheckerEnabled', value) + }, onSelectChange (type, value) { this.$store.dispatch('SET_SINGLE_PREFERENCE', { type, value }) }, diff --git a/src/renderer/spellchecker/index.js b/src/renderer/spellchecker/index.js index 7e9eba2a..29bb9f3b 100644 --- a/src/renderer/spellchecker/index.js +++ b/src/renderer/spellchecker/index.js @@ -1,8 +1,9 @@ import fs from 'fs-extra' import path from 'path' +import os from 'os' import { remote } from 'electron' import { SpellCheckHandler, fallbackLocales, normalizeLanguageCode } from '@hfelix/electron-spellchecker' -import { isOsx, cloneObj } from '../util' +import { cloneObj, isOsx, isLinux, isWindows } from '@/util' // NOTE: Hardcoded in "@hfelix/electron-spellchecker/src/spell-check-handler.js" export const dictionaryPath = path.join(remote.app.getPath('userData'), 'dictionaries') @@ -83,6 +84,25 @@ export const getAvailableHunspellDictionaries = () => { return dict } +export const isOsSpellcheckerSupported = () => { + let envOverwrite = !!process.env['SPELLCHECKER_PREFER_HUNSPELL'] // eslint-disable-line dot-notation + if (isLinux || envOverwrite) { + return false + } else if (isOsx) { + return true + } else if (isWindows) { + // NOTE: Normally we need to initialize the spellchecker and check the result. + const windowsVersion = os.release().match(/^(\d+)\./) + if (windowsVersion && windowsVersion[1]) { + const windowsMajor = Number(windowsVersion[1]) + if (windowsMajor >= 10) { + return true + } + } + } + return false +} + /** * High level spell checker API. * @@ -98,7 +118,7 @@ export class SpellChecker { */ constructor (enabled = true) { // Hunspell is used on Linux and Windows but macOS can use Hunspell if prefered. - this.isHunspell = !isOsx || !!process.env['SPELLCHECKER_PREFER_HUNSPELL'] // eslint-disable-line dot-notation + this.isHunspell = !isOsSpellcheckerSupported() || !!process.env['SPELLCHECKER_PREFER_HUNSPELL'] // eslint-disable-line dot-notation // Initialize spell check provider. If spell check is not enabled don't // initialize the handler to not load the native module. @@ -118,6 +138,7 @@ export class SpellChecker { } this.provider = new SpellCheckHandler() + this.isHunspell = this.provider.isHunspell // The spell checker is now initialized but not yet enabled. You need to call `init`. this.isEnabled = false diff --git a/src/renderer/store/preferences.js b/src/renderer/store/preferences.js index 4c2e5c4b..7bfeedeb 100644 --- a/src/renderer/store/preferences.js +++ b/src/renderer/store/preferences.js @@ -50,7 +50,7 @@ const state = { autoSwitchTheme: 2, spellcheckerEnabled: false, - spellcheckerIsHunspell: false, // macOS only + spellcheckerIsHunspell: false, // macOS/Windows 10 only spellcheckerNoUnderline: false, spellcheckerAutoDetectLanguage: false, spellcheckerLanguage: 'en-US', diff --git a/src/renderer/util/index.js b/src/renderer/util/index.js index b40e31ba..233d4892 100644 --- a/src/renderer/util/index.js +++ b/src/renderer/util/index.js @@ -5,7 +5,11 @@ export const delay = time => { let rejectFn const p = new Promise((resolve, reject) => { rejectFn = reject - timerId = setTimeout(resolve, time) + timerId = setTimeout(() => { + p.cancel = () => {} + rejectFn = null + resolve() + }, time) }) p.cancel = () => { diff --git a/yarn.lock b/yarn.lock index 5e4853af..1889d9ed 100644 --- a/yarn.lock +++ b/yarn.lock @@ -805,12 +805,12 @@ electron-is-accelerator "^0.1.0" keyboardevents-areequal "^0.2.1" -"@hfelix/electron-spellchecker@^1.0.0-rc.3": - version "1.0.0-rc.3" - resolved "https://registry.npmjs.org/@hfelix/electron-spellchecker/-/electron-spellchecker-1.0.0-rc.3.tgz#6a76eaf16a4c15987cdda00d42bd9896a0590e70" - integrity sha512-qG2YxRtoOLycA7vRJENds4POxV7VC8yVmDXi7dIz3czRUZgNJNCQDR5YoRQ9xPXVLWT3oYB/RLwOS3RJRP3AnA== +"@hfelix/electron-spellchecker@1.0.0-rc.5": + version "1.0.0-rc.5" + resolved "https://registry.yarnpkg.com/@hfelix/electron-spellchecker/-/electron-spellchecker-1.0.0-rc.5.tgz#2e7c81857bbdd167aaea45b223a017bd89b19f7a" + integrity sha512-ma5osTva2F4/mF0h6Sbq0BTbMhZI0lBovjd/Ki2WesS9O5tIf75vdFeaZh0NVA7yynNsj5RmhfDTvrKOOg79MQ== dependencies: - "@hfelix/spellchecker" "^4.0.11" + "@hfelix/spellchecker" "^4.0.12-rc.2" bcp47 "^1.1.2" cld "^2.5.1" debug "^4.1.1" @@ -824,12 +824,12 @@ resolved "https://registry.npmjs.org/@hfelix/keyboardevent-from-electron-accelerator/-/keyboardevent-from-electron-accelerator-1.1.1.tgz#7e1d4fd913759c381b7919cc7faf4c0c641d457c" integrity sha512-1eVkDSqoRQkF2FrPPia2EZ3310c0TvFKYvSuJbaxHpRKbI6eVHcVGKpmOSDli6Qdn3Bu0h7ozfgMZbAEBD+BLQ== -"@hfelix/spellchecker@^4.0.11": - version "4.0.11" - resolved "https://registry.npmjs.org/@hfelix/spellchecker/-/spellchecker-4.0.11.tgz#bc86881e419c7e12c88ab996a5bbbe78f044385c" - integrity sha512-xBVSHB6OPnqD+KBL5cQO0o20/TIezigD0U1a7V+e+5OvAmCnqdWo23IOdZrUDA7bBhNvK2Pml9jF3U7HRo272A== +"@hfelix/spellchecker@^4.0.12-rc.2": + version "4.0.12-rc.2" + resolved "https://registry.yarnpkg.com/@hfelix/spellchecker/-/spellchecker-4.0.12-rc.2.tgz#3a3630c222e567eca68476a1bce1c919dc452378" + integrity sha512-FrO+96Di+EiJiib9AiLMXn3PBNg8gayn1r8JBPBM8ME2UwTs8f7GJF6yYCW99a0N8Lqwo13rplLqY6UVG5gEtQ== dependencies: - nan "^2.13.2" + nan "^2.14.0" "@markedjs/html-differ@^3.0.0": version "3.0.0" @@ -8175,7 +8175,7 @@ mute-stream@0.0.8: resolved "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== -nan@2.14.0, nan@^2.12.1, nan@^2.13.2, nan@^2.9.2: +nan@2.14.0, nan@^2.12.1, nan@^2.13.2, nan@^2.14.0, nan@^2.9.2: version "2.14.0" resolved "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==