Browse Source

feat: enable builtin spellchecker (#20897)

* feat: enable builtin spellchecker (#20692)

* chore: add code required to use chromes spellchecker

* chore: fix linting

* chore: manifests needs buildflags now

* chore: add dictionarySuggestions to the context menu event when the spellchecker is active

* chore: enable by default for windows builds

* chore: add patch to remove incognito usage in the spellchecker

* chore: add dependencies on spellcheck common and flags

* chore: conditionally include spell check panel impl

* chore: fix deps for spellcheck feature flags

* chore: add patch for electron resources

* chore: add dependency on //components/language/core/browser

* chore: patches to make hunspell work on windows

* build: collect hunspell dictionaries into a zip file and publish

* chore: clean up patches

* chore: add docs and set spell checker url method

* chore: fix error handling

* chore: fix hash logic

* build: update hunspell filename generator

* fix: default spellchecker list to the current system locale if we can

* docs: document the language getter

* chore: patch IDS_ resources for linux builds

* feat: add spellcheck webpref flag to disable the builtin spellchecker

* chore: fix docs typo

* chore: clean up spellchecker impl as per feedback

* remove unneeded deps

* chore: disable spellcheck by default in web prefs
Samuel Attard 5 years ago
parent
commit
40e0e8e499

+ 28 - 1
.circleci/config.yml

@@ -319,6 +319,8 @@ step-gn-check: &step-gn-check
       gn check out/Default //electron:electron_app
       gn check out/Default //electron:manifests
       gn check out/Default //electron/shell/common/api:mojo
+      # Check the hunspell filenames
+      node electron/script/gen-hunspell-filenames.js --check
 
 step-electron-build: &step-electron-build
   run:
@@ -534,6 +536,20 @@ step-mksnapshot-store: &step-mksnapshot-store
     path: src/out/Default/mksnapshot.zip
     destination: mksnapshot.zip
 
+step-hunspell-build: &step-hunspell-build
+  run:
+    name: hunspell build
+    command: |
+      cd src
+      if [ "$SKIP_DIST_ZIP" != "1" ]; then
+        ninja -C out/Default electron:hunspell_dictionaries_zip -j $NUMBER_OF_NINJA_PROCESSES
+      fi
+
+step-hunspell-store: &step-hunspell-store
+  store_artifacts:
+    path: src/out/Default/hunspell_dictionaries.zip
+    destination: hunspell_dictionaries.zip
+
 step-maybe-generate-breakpad-symbols: &step-maybe-generate-breakpad-symbols
   run:
     name: Generate breakpad symbols
@@ -693,7 +709,6 @@ step-minimize-workspace-size-from-checkout: &step-minimize-workspace-size-from-c
       rm -rf src/ios
       rm -rf src/third_party/blink/web_tests
       rm -rf src/third_party/blink/perf_tests
-      rm -rf src/third_party/hunspell_dictionaries
       rm -rf src/third_party/WebKit/LayoutTests
 
 # Save the src cache based on the deps hash
@@ -912,6 +927,10 @@ steps-electron-build: &steps-electron-build
     - *step-ffmpeg-build
     - *step-ffmpeg-store
 
+    # hunspell
+    - *step-hunspell-build
+    - *step-hunspell-store
+
     # Save all data needed for a further tests run.
     - *step-persist-data-for-tests
 
@@ -990,6 +1009,10 @@ steps-electron-build-with-inline-checkout-for-tests: &steps-electron-build-with-
     - *step-ffmpeg-build
     - *step-ffmpeg-store
 
+    # hunspell
+    - *step-hunspell-build
+    - *step-hunspell-store
+
     # Save all data needed for a further tests run.
     - *step-persist-data-for-tests
 
@@ -1079,6 +1102,10 @@ steps-electron-build-for-publish: &steps-electron-build-for-publish
     - *step-ffmpeg-build
     - *step-ffmpeg-store
 
+    # hunspell
+    - *step-hunspell-build
+    - *step-hunspell-store
+
     # typescript defs
     - *step-maybe-generate-typescript-defs
 

+ 30 - 1
BUILD.gn

@@ -1,6 +1,7 @@
 import("//build/config/locales.gni")
 import("//build/config/ui.gni")
 import("//build/config/win/manifest.gni")
+import("//components/spellcheck/spellcheck_build_features.gni")
 import("//content/public/app/mac_helpers.gni")
 import("//pdf/features.gni")
 import("//printing/buildflags/buildflags.gni")
@@ -21,6 +22,7 @@ import("buildflags/buildflags.gni")
 import("electron_paks.gni")
 import("filenames.auto.gni")
 import("filenames.gni")
+import("filenames.hunspell.gni")
 
 if (is_mac) {
   import("//build/config/mac/rules.gni")
@@ -358,12 +360,12 @@ source_set("electron_lib") {
     "//chrome/app/resources:platform_locale_settings",
     "//chrome/services/printing/public/mojom",
     "//components/certificate_transparency",
+    "//components/language/core/browser",
     "//components/net_log",
     "//components/network_hints/common",
     "//components/network_hints/renderer",
     "//components/network_session_configurator/common",
     "//components/prefs",
-    "//components/spellcheck/renderer",
     "//components/viz/host",
     "//components/viz/service",
     "//content/public/browser",
@@ -478,6 +480,10 @@ source_set("electron_lib") {
     ]
   }
 
+  if (enable_builtin_spellchecker) {
+    deps += [ "chromium_src:chrome_spellchecker" ]
+  }
+
   if (is_mac) {
     deps += [
       "//components/remote_cocoa/app_shim",
@@ -1265,9 +1271,14 @@ template("dist_zip") {
                              "outputs",
                              "testonly",
                            ])
+    flatten = false
+    if (defined(invoker.flatten)) {
+      flatten = invoker.flatten
+    }
     args = rebase_path(outputs + [ _runtime_deps_file ], root_build_dir) + [
              target_cpu,
              target_os,
+             "$flatten",
            ]
   }
 }
@@ -1353,6 +1364,24 @@ dist_zip("electron_mksnapshot_zip") {
   ]
 }
 
+copy("hunspell_dictionaries") {
+  sources = hunspell_dictionaries + hunspell_licenses
+  outputs = [
+    "$target_gen_dir/electron_hunspell/{{source_file_part}}",
+  ]
+}
+
+dist_zip("hunspell_dictionaries_zip") {
+  data_deps = [
+    ":hunspell_dictionaries",
+  ]
+  flatten = true
+
+  outputs = [
+    "$root_build_dir/hunspell_dictionaries.zip",
+  ]
+}
+
 group("electron") {
   public_deps = [
     ":electron_app",

+ 2 - 0
appveyor.yml

@@ -91,6 +91,7 @@ build_script:
   - ninja -C out/Default electron:electron_dist_zip
   - ninja -C out/Default shell_browser_ui_unittests
   - ninja -C out/Default electron:electron_mksnapshot_zip
+  - ninja -C out/Default electron:hunspell_dictionaries_zip
   - ninja -C out/Default electron:electron_chromedriver_zip
   - ninja -C out/Default third_party/electron_node:headers
   - appveyor PushArtifact out/Default/dist.zip
@@ -100,6 +101,7 @@ build_script:
   - 7z a node_headers.zip out\Default\gen\node_headers
   - appveyor PushArtifact node_headers.zip
   - appveyor PushArtifact out/Default/mksnapshot.zip
+  - appveyor PushArtifact out/Default/hunspell_dictionaries.zip
   - appveyor PushArtifact out/Default/electron.lib
   - ps: >-
       if ($env:GN_CONFIG -eq 'release') {

+ 4 - 3
build/zip.py

@@ -46,13 +46,14 @@ def execute(argv):
     raise e
 
 def main(argv):
-  dist_zip, runtime_deps, target_cpu, target_os = argv
+  dist_zip, runtime_deps, target_cpu, target_os, flatten_val = argv
+  should_flatten = flatten_val == "true"
   dist_files = set()
   with open(runtime_deps) as f:
     for dep in f.readlines():
       dep = dep.strip()
       dist_files.add(dep)
-  if sys.platform == 'darwin':
+  if sys.platform == 'darwin' and not should_flatten:
     execute(['zip', '-r', '-y', dist_zip] + list(dist_files))
   else:
     with zipfile.ZipFile(dist_zip, 'w', zipfile.ZIP_DEFLATED, allowZip64=True) as z:
@@ -67,7 +68,7 @@ def main(argv):
           basename = os.path.basename(dep)
           dirname = os.path.dirname(dep)
           arcname = os.path.join(dirname, 'chrome-sandbox') if basename == 'chrome_sandbox' else dep
-          z.write(dep, arcname)
+          z.write(dep, os.path.basename(arcname) if should_flatten else arcname)
 
 if __name__ == '__main__':
   sys.exit(main(sys.argv[1:]))

+ 1 - 0
buildflags/BUILD.gn

@@ -19,6 +19,7 @@ buildflag_header("buildflags") {
     "ENABLE_TTS=$enable_tts",
     "ENABLE_COLOR_CHOOSER=$enable_color_chooser",
     "ENABLE_ELECTRON_EXTENSIONS=$enable_electron_extensions",
+    "ENABLE_BUILTIN_SPELLCHECKER=$enable_builtin_spellchecker",
     "ENABLE_PICTURE_IN_PICTURE=$enable_picture_in_picture",
     "OVERRIDE_LOCATION_PROVIDER=$enable_fake_location_provider",
   ]

+ 3 - 0
buildflags/buildflags.gni

@@ -33,4 +33,7 @@ declare_args() {
 
   # Enable Chrome extensions support.
   enable_electron_extensions = false
+
+  # Enable Spellchecker support
+  enable_builtin_spellchecker = true
 }

+ 50 - 0
chromium_src/BUILD.gn

@@ -3,6 +3,7 @@
 # found in the LICENSE file.
 
 import("//build/config/ui.gni")
+import("//components/spellcheck/spellcheck_build_features.gni")
 import("//electron/buildflags/buildflags.gni")
 import("//printing/buildflags/buildflags.gni")
 import("//third_party/widevine/cdm/widevine.gni")
@@ -225,3 +226,52 @@ static_library("chrome") {
     ]
   }
 }
+
+# This source set is just so we don't have to depend on all of //chrome/browser
+# You may have to add new files here during the upgrade if //chrome/browser/spellchecker
+# gets more files
+source_set("chrome_spellchecker") {
+  sources = [
+    "//chrome/browser/spellchecker/spell_check_host_chrome_impl.cc",
+    "//chrome/browser/spellchecker/spell_check_host_chrome_impl.h",
+    "//chrome/browser/spellchecker/spellcheck_custom_dictionary.cc",
+    "//chrome/browser/spellchecker/spellcheck_custom_dictionary.h",
+    "//chrome/browser/spellchecker/spellcheck_factory.cc",
+    "//chrome/browser/spellchecker/spellcheck_factory.h",
+    "//chrome/browser/spellchecker/spellcheck_hunspell_dictionary.cc",
+    "//chrome/browser/spellchecker/spellcheck_hunspell_dictionary.h",
+    "//chrome/browser/spellchecker/spellcheck_language_blacklist_policy_handler.cc",
+    "//chrome/browser/spellchecker/spellcheck_language_blacklist_policy_handler.h",
+    "//chrome/browser/spellchecker/spellcheck_language_policy_handler.cc",
+    "//chrome/browser/spellchecker/spellcheck_language_policy_handler.h",
+    "//chrome/browser/spellchecker/spellcheck_service.cc",
+    "//chrome/browser/spellchecker/spellcheck_service.h",
+  ]
+
+  if (has_spellcheck_panel) {
+    sources += [
+      "//chrome/browser/spellchecker/spell_check_panel_host_impl.cc",
+      "//chrome/browser/spellchecker/spell_check_panel_host_impl.h",
+    ]
+  }
+
+  if (use_browser_spellchecker) {
+    sources += [
+      "//chrome/browser/spellchecker/spelling_request.cc",
+      "//chrome/browser/spellchecker/spelling_request.h",
+    ]
+  }
+
+  deps = [
+    "//base:base_static",
+    "//components/language/core/browser",
+    "//components/spellcheck:buildflags",
+    "//components/sync",
+  ]
+
+  public_deps = [
+    "//components/spellcheck/browser",
+    "//components/spellcheck/common",
+    "//components/spellcheck/renderer",
+  ]
+}

+ 2 - 0
docs/api/browser-window.md

@@ -385,6 +385,8 @@ It creates a new `BrowserWindow` with native properties as set by the `options`.
     * `accessibleTitle` String (optional) - An alternative title string provided only
       to accessibility tools such as screen readers. This string is not directly
       visible to users.
+    * `spellcheck` Boolean (optional) - Whether to enable the builtin spellchecker.
+      Default is `false`.
 
 When setting minimum or maximum window size with `minWidth`/`maxWidth`/
 `minHeight`/`maxHeight`, it only constrains the users. It won't prevent you from

+ 28 - 0
docs/api/session.md

@@ -456,10 +456,38 @@ this session just before normal `preload` scripts run.
 Returns `String[]` an array of paths to preload scripts that have been
 registered.
 
+#### `ses.setSpellCheckerLanguages(languages)`
+
+* `languages` String[] - An array of language codes to enable the spellchecker for.
+
+The built in spellchecker does not automatically detect what language a user is typing in.  In order for the
+spell checker to correctly check their words you must call this API with an array of language codes.  You can
+get the list of supported language codes with the `ses.availableSpellCheckerLanguages` property.
+
+#### `ses.getSpellCheckerLanguages()`
+
+Returns `String[]` - An array of language codes the spellchecker is enabled for.  If this list is empty the spellchecker
+will fallback to using `en-US`.  By default on launch if this setting is an empty list Electron will try to populate this
+setting with the current OS locale.  This setting is persisted across restarts.
+
+#### `ses.setSpellCheckerDictionaryDownloadURL(url)`
+
+* `url` String - A base URL for Electron to download hunspell dictionaries from.
+
+By default Electron will download hunspell dictionaries from the Chromium CDN.  If you want to override this
+behavior you can use this API to point the dictionary downloader at your own hosted version of the hunspell
+dictionaries.  We publish a `hunspell_dictionaries.zip` file with each release which contains the files you need
+to host here.
+
 ### Instance Properties
 
 The following properties are available on instances of `Session`:
 
+#### `ses.availableSpellCheckerLanguages` _Readonly_
+
+A `String[]` array which consists of all the known available spell checker languages.  Providing a language
+code to the `setSpellCheckerLanaguages` API that isn't in this array will result in an error.
+
 #### `ses.cookies` _Readonly_
 
 A [`Cookies`](cookies.md) object for this session.

+ 3 - 0
docs/api/web-contents.md

@@ -570,6 +570,9 @@ Returns:
   * `titleText` String - Title or alt text of the selection that the context
     was invoked on.
   * `misspelledWord` String - The misspelled word under the cursor, if any.
+  * `dictionarySuggestions` String[] - An array of suggested words to show the
+    user to replace the `misspelledWord`.  Only available if there is a misspelled
+    word and spellchecker is enabled.
   * `frameCharset` String - The character encoding of the frame on which the
     menu was invoked.
   * `inputFieldType` String - If the context menu was invoked on an input

+ 11 - 0
docs/api/web-frame.md

@@ -74,6 +74,17 @@ Sets the maximum and minimum layout-based (i.e. non-visual) zoom level.
 
 Sets a provider for spell checking in input fields and text areas.
 
+If you want to use this method you must disable the builtin spellchecker when you
+construct the window.
+
+```js
+const mainWindow = new BrowserWindow({
+  webPreferences: {
+    spellcheck: false
+  }
+})
+```
+
 The `provider` must be an object that has a `spellCheck` method that accepts
 an array of individual words for spellchecking.
 The `spellCheck` function runs asynchronously and calls the `callback` function

+ 6 - 0
electron_strings.grdp

@@ -69,4 +69,10 @@
   <message name="IDS_PICTURE_IN_PICTURE_PREVIOUS_TRACK_CONTROL_ACCESSIBLE_TEXT" desc="Accessible text label used for the controls button in the Picture-in-Picture window. The button invokes previous track action.">
     Previous track
   </message>
+  <message name="IDS_SPELLCHECK_DICTIONARY" use_name_for_id="true">
+    en-US
+  </message>
+  <message name="IDS_ACCEPT_LANGUAGES" use_name_for_id="true">
+    en-US,en
+  </message>
 </grit-part>

+ 60 - 0
filenames.hunspell.gni

@@ -0,0 +1,60 @@
+hunspell_dictionaries = [
+  "//third_party/hunspell_dictionaries/af-ZA-3-0.bdic",
+  "//third_party/hunspell_dictionaries/bg-BG-3-0.bdic",
+  "//third_party/hunspell_dictionaries/ca-ES-3-0.bdic",
+  "//third_party/hunspell_dictionaries/cs-CZ-3-0.bdic",
+  "//third_party/hunspell_dictionaries/cy-GB-1-0.bdic",
+  "//third_party/hunspell_dictionaries/da-DK-3-0.bdic",
+  "//third_party/hunspell_dictionaries/de-DE-3-0.bdic",
+  "//third_party/hunspell_dictionaries/el-GR-3-0.bdic",
+  "//third_party/hunspell_dictionaries/en-AU-8-0.bdic",
+  "//third_party/hunspell_dictionaries/en-CA-8-0.bdic",
+  "//third_party/hunspell_dictionaries/en-GB-8-0.bdic",
+  "//third_party/hunspell_dictionaries/en-US-8-0.bdic",
+  "//third_party/hunspell_dictionaries/es-ES-3-0.bdic",
+  "//third_party/hunspell_dictionaries/et-EE-3-0.bdic",
+  "//third_party/hunspell_dictionaries/fa-IR-8-0.bdic",
+  "//third_party/hunspell_dictionaries/fo-FO-3-0.bdic",
+  "//third_party/hunspell_dictionaries/fr-FR-3-0.bdic",
+  "//third_party/hunspell_dictionaries/he-IL-3-0.bdic",
+  "//third_party/hunspell_dictionaries/hi-IN-3-0.bdic",
+  "//third_party/hunspell_dictionaries/hr-HR-3-0.bdic",
+  "//third_party/hunspell_dictionaries/hu-HU-3-0.bdic",
+  "//third_party/hunspell_dictionaries/hy-1-0.bdic",
+  "//third_party/hunspell_dictionaries/id-ID-3-0.bdic",
+  "//third_party/hunspell_dictionaries/it-IT-3-0.bdic",
+  "//third_party/hunspell_dictionaries/ko-3-0.bdic",
+  "//third_party/hunspell_dictionaries/lt-LT-3-0.bdic",
+  "//third_party/hunspell_dictionaries/lv-LV-3-0.bdic",
+  "//third_party/hunspell_dictionaries/nb-NO-3-0.bdic",
+  "//third_party/hunspell_dictionaries/nl-NL-3-0.bdic",
+  "//third_party/hunspell_dictionaries/pl-PL-3-0.bdic",
+  "//third_party/hunspell_dictionaries/pt-BR-3-0.bdic",
+  "//third_party/hunspell_dictionaries/pt-PT-3-0.bdic",
+  "//third_party/hunspell_dictionaries/ro-RO-3-0.bdic",
+  "//third_party/hunspell_dictionaries/ru-RU-3-0.bdic",
+  "//third_party/hunspell_dictionaries/sh-3-0.bdic",
+  "//third_party/hunspell_dictionaries/sh-4-0.bdic",
+  "//third_party/hunspell_dictionaries/sk-SK-3-0.bdic",
+  "//third_party/hunspell_dictionaries/sl-SI-3-0.bdic",
+  "//third_party/hunspell_dictionaries/sq-3-0.bdic",
+  "//third_party/hunspell_dictionaries/sr-3-0.bdic",
+  "//third_party/hunspell_dictionaries/sr-4-0.bdic",
+  "//third_party/hunspell_dictionaries/sv-SE-3-0.bdic",
+  "//third_party/hunspell_dictionaries/ta-IN-3-0.bdic",
+  "//third_party/hunspell_dictionaries/tg-TG-5-0.bdic",
+  "//third_party/hunspell_dictionaries/tr-TR-4-0.bdic",
+  "//third_party/hunspell_dictionaries/uk-UA-3-0.bdic",
+  "//third_party/hunspell_dictionaries/vi-VN-3-0.bdic",
+  "//third_party/hunspell_dictionaries/xx-XX-3-0.bdic",
+]
+
+hunspell_licenses = [
+  "//third_party/hunspell_dictionaries/COPYING",
+  "//third_party/hunspell_dictionaries/COPYING.Apache",
+  "//third_party/hunspell_dictionaries/COPYING.LESSER",
+  "//third_party/hunspell_dictionaries/COPYING.LGPL",
+  "//third_party/hunspell_dictionaries/COPYING.MIT",
+  "//third_party/hunspell_dictionaries/COPYING.MPL",
+  "//third_party/hunspell_dictionaries/LICENSE",
+]

+ 3 - 0
package.json

@@ -129,6 +129,9 @@
       "node script/gen-filenames.js",
       "python script/check-trailing-whitespace.py --fix",
       "git add filenames.auto.gni"
+    ],
+    "DEPS": [
+      "node script/gen-hunspell-filenames.js"
     ]
   },
   "dependencies": {

+ 2 - 0
patches/chromium/.patches

@@ -81,3 +81,5 @@ ui_views_fix_jumbo_build.patch
 export_fetchapi_mojo_traits_to_fix_component_build.patch
 build_fix_when_building_with_enable_plugins_false.patch
 add_zoom_limit_setters_to_webcontents.patch
+remove_usage_of_incognito_apis_in_the_spellchecker.patch
+chore_use_electron_resources_not_chrome_for_spellchecker.patch

+ 34 - 0
patches/chromium/chore_use_electron_resources_not_chrome_for_spellchecker.patch

@@ -0,0 +1,34 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Samuel Attard <[email protected]>
+Date: Wed, 23 Oct 2019 14:17:18 -0700
+Subject: chore: use electron resources not chrome for spellchecker
+
+spellchecker uses a few IDS_ resources.  We need to load these from
+Electrons grit header instead of Chromes
+
+diff --git a/chrome/browser/spellchecker/spellcheck_factory.cc b/chrome/browser/spellchecker/spellcheck_factory.cc
+index 48ac0a24efde0cb7d3ba71c8b8bdf5178f606e80..e2beefc276098fdc8f1cdab2e0edb8fae4ee67ca 100644
+--- a/chrome/browser/spellchecker/spellcheck_factory.cc
++++ b/chrome/browser/spellchecker/spellcheck_factory.cc
+@@ -6,7 +6,7 @@
+ 
+ #include "chrome/browser/profiles/incognito_helpers.h"
+ #include "chrome/browser/spellchecker/spellcheck_service.h"
+-#include "chrome/grit/locale_settings.h"
++#include "electron/grit/electron_resources.h"
+ #include "components/keyed_service/content/browser_context_dependency_manager.h"
+ #include "components/pref_registry/pref_registry_syncable.h"
+ #include "components/prefs/pref_service.h"
+diff --git a/components/language/core/browser/language_prefs.cc b/components/language/core/browser/language_prefs.cc
+index d5e4c09e1722232df44b112ce39cdacea03a4710..c6caf7eacd9eed439ab5167e51b9fcce5d6af664 100644
+--- a/components/language/core/browser/language_prefs.cc
++++ b/components/language/core/browser/language_prefs.cc
+@@ -21,7 +21,7 @@
+ #include "components/pref_registry/pref_registry_syncable.h"
+ #include "components/prefs/pref_service.h"
+ #include "components/prefs/scoped_user_pref_update.h"
+-#include "components/strings/grit/components_locale_settings.h"
++#include "electron/grit/electron_resources.h"
+ #include "ui/base/l10n/l10n_util.h"
+ 
+ namespace language {

+ 25 - 0
patches/chromium/remove_usage_of_incognito_apis_in_the_spellchecker.patch

@@ -0,0 +1,25 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: Samuel Attard <[email protected]>
+Date: Wed, 23 Oct 2019 11:43:58 -0700
+Subject: remove usage of incognito APIs in the spellchecker
+
+chrome::GetBrowserContextRedirectedInIncognito is not available in
+Electron nor do we want it to be.  We could potentially upstream a
+change to move more of //chrome spellchecker logic into //components so
+that we can further separate our dependency from //chrome.
+
+diff --git a/chrome/browser/spellchecker/spellcheck_factory.cc b/chrome/browser/spellchecker/spellcheck_factory.cc
+index 5253d1f48e188b0339834c876378677b459e718e..48ac0a24efde0cb7d3ba71c8b8bdf5178f606e80 100644
+--- a/chrome/browser/spellchecker/spellcheck_factory.cc
++++ b/chrome/browser/spellchecker/spellcheck_factory.cc
+@@ -78,7 +78,10 @@ void SpellcheckServiceFactory::RegisterProfilePrefs(
+ 
+ content::BrowserContext* SpellcheckServiceFactory::GetBrowserContextToUse(
+     content::BrowserContext* context) const {
++  return context;
++#if 0
+   return chrome::GetBrowserContextRedirectedInIncognito(context);
++#endif
+ }
+ 
+ bool SpellcheckServiceFactory::ServiceIsNULLWhileTesting() const {

+ 35 - 0
script/gen-hunspell-filenames.js

@@ -0,0 +1,35 @@
+const fs = require('fs')
+const path = require('path')
+
+const check = process.argv.includes('--check')
+
+const dictsPath = path.resolve(__dirname, '..', '..', 'third_party', 'hunspell_dictionaries')
+const gclientPath = 'third_party/hunspell_dictionaries'
+
+const allFiles = fs.readdirSync(dictsPath)
+
+const dictionaries = allFiles
+  .filter(file => path.extname(file) === '.bdic')
+
+const licenses = allFiles
+  .filter(file => file.startsWith('LICENSE') || file.startsWith('COPYING'))
+
+const content = `hunspell_dictionaries = [
+  ${dictionaries.map(f => `"//${path.posix.join(gclientPath, f)}"`).join(',\n  ')},
+]
+
+hunspell_licenses = [
+  ${licenses.map(f => `"//${path.posix.join(gclientPath, f)}"`).join(',\n  ')},
+]
+`
+
+const filenamesPath = path.resolve(__dirname, '..', 'filenames.hunspell.gni')
+
+if (check) {
+  const currentContent = fs.readFileSync(filenamesPath, 'utf8')
+  if (currentContent !== content) {
+    throw new Error('hunspell filenames need to be regenerated, latest generation does not match current file.  Please run node gen-hunspell-filenames.js')
+  }
+} else {
+  fs.writeFileSync(filenamesPath, content)
+}

+ 1 - 0
script/generate-deps-hash.js

@@ -27,6 +27,7 @@ addAllFiles(path.resolve(__dirname, '../patches'))
 
 // Create Hash
 const hasher = crypto.createHash('SHA256')
+hasher.update(`HASH_VERSION:${HASH_VERSION}`)
 for (const file of filesToHash) {
   hasher.update(fs.readFileSync(file))
 }

+ 1 - 0
script/release/release.js

@@ -130,6 +130,7 @@ function assetsForVersion (version, validatingRelease) {
     `electron-${version}-win32-arm64.zip`,
     `electron-api.json`,
     `electron.d.ts`,
+    `hunspell_dictionaries.zip`,
     `ffmpeg-${version}-darwin-x64.zip`,
     `ffmpeg-${version}-linux-arm64.zip`,
     `ffmpeg-${version}-linux-armv7l.zip`,

+ 5 - 0
script/release/uploaders/upload.py

@@ -106,6 +106,11 @@ def main():
   shutil.copy2(os.path.join(OUT_DIR, 'mksnapshot.zip'), mksnapshot_zip)
   upload_electron(release, mksnapshot_zip, args)
 
+  if PLATFORM == 'linux' and get_target_arch() == 'x64':
+    # Upload the hunspell dictionaries only from the linux x64 build
+    hunspell_dictionaries_zip = os.path.join(OUT_DIR, 'hunspell_dictionaries.zip')
+    upload_electron(release, hunspell_dictionaries_zip, args)
+
   if not tag_exists and not args.upload_to_s3:
     # Upload symbols to symbol server.
     run_python_upload_script('upload-symbols.py')

+ 6 - 1
shell/browser/api/atom_api_app.cc

@@ -901,9 +901,14 @@ void App::SetPath(gin_helper::ErrorThrower thrower,
 
   bool succeed = false;
   int key = GetPathConstant(name);
-  if (key >= 0)
+  if (key >= 0) {
     succeed =
         base::PathService::OverrideAndCreateIfNeeded(key, path, true, false);
+    if (key == DIR_USER_DATA) {
+      succeed |= base::PathService::OverrideAndCreateIfNeeded(
+          chrome::DIR_USER_DATA, path, true, false);
+    }
+  }
   if (!succeed)
     thrower.ThrowError("Failed to set path");
 }

+ 50 - 0
shell/browser/api/atom_api_session.cc

@@ -69,6 +69,12 @@
 #include "shell/browser/extensions/atom_extension_system.h"
 #endif
 
+#if BUILDFLAG(ENABLE_BUILTIN_SPELLCHECKER)
+#include "chrome/browser/spellchecker/spellcheck_hunspell_dictionary.h"
+#include "components/spellcheck/browser/pref_names.h"
+#include "components/spellcheck/common/spellcheck_common.h"
+#endif
+
 using content::BrowserThread;
 using content::StoragePartition;
 
@@ -646,6 +652,42 @@ void Session::Preconnect(const mate::Dictionary& options,
                      url, num_sockets_to_preconnect));
 }
 
+#if BUILDFLAG(ENABLE_BUILTIN_SPELLCHECKER)
+base::Value Session::GetSpellCheckerLanguages() {
+  return browser_context_->prefs()
+      ->Get(spellcheck::prefs::kSpellCheckDictionaries)
+      ->Clone();
+}
+
+void Session::SetSpellCheckerLanguages(
+    gin_helper::ErrorThrower thrower,
+    const std::vector<std::string>& languages) {
+  base::ListValue language_codes;
+  for (const std::string& lang : languages) {
+    std::string code = spellcheck::GetCorrespondingSpellCheckLanguage(lang);
+    if (code.empty()) {
+      thrower.ThrowError("Invalid language code provided: \"" + lang +
+                         "\" is not a valid language code");
+      return;
+    }
+    language_codes.AppendString(code);
+  }
+  browser_context_->prefs()->Set(spellcheck::prefs::kSpellCheckDictionaries,
+                                 language_codes);
+}
+
+void SetSpellCheckerDictionaryDownloadURL(gin_helper::ErrorThrower thrower,
+                                          const GURL& url) {
+  if (!url.is_valid()) {
+    thrower.ThrowError(
+        "The URL you provided to setSpellCheckerDictionaryDownloadURL is not a "
+        "valid URL");
+    return;
+  }
+  SpellcheckHunspellDictionary::SetDownloadURLForTesting(url);
+}
+#endif
+
 // static
 mate::Handle<Session> Session::CreateFrom(v8::Isolate* isolate,
                                           AtomBrowserContext* browser_context) {
@@ -716,6 +758,14 @@ void Session::BuildPrototype(v8::Isolate* isolate,
       .SetMethod("getPreloads", &Session::GetPreloads)
 #if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
       .SetMethod("loadChromeExtension", &Session::LoadChromeExtension)
+#endif
+#if BUILDFLAG(ENABLE_BUILTIN_SPELLCHECKER)
+      .SetMethod("getSpellCheckerLanguages", &Session::GetSpellCheckerLanguages)
+      .SetMethod("setSpellCheckerLanguages", &Session::SetSpellCheckerLanguages)
+      .SetProperty("availableSpellCheckerLanguages",
+                   &spellcheck::SpellCheckLanguages)
+      .SetMethod("setSpellCheckerDictionaryDownloadURL",
+                 &SetSpellCheckerDictionaryDownloadURL)
 #endif
       .SetMethod("preconnect", &Session::Preconnect)
       .SetProperty("cookies", &Session::Cookies)

+ 5 - 0
shell/browser/api/atom_api_session.h

@@ -87,6 +87,11 @@ class Session : public mate::TrackableObject<Session>,
   v8::Local<v8::Value> WebRequest(v8::Isolate* isolate);
   v8::Local<v8::Value> NetLog(v8::Isolate* isolate);
   void Preconnect(const mate::Dictionary& options, mate::Arguments* args);
+#if BUILDFLAG(ENABLE_BUILTIN_SPELLCHECKER)
+  base::Value GetSpellCheckerLanguages();
+  void SetSpellCheckerLanguages(gin_helper::ErrorThrower thrower,
+                                const std::vector<std::string>& languages);
+#endif
 
 #if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
   void LoadChromeExtension(const base::FilePath extension_path);

+ 17 - 0
shell/browser/atom_browser_client.cc

@@ -100,6 +100,11 @@
 #include "net/ssl/client_cert_store.h"
 #endif
 
+#if BUILDFLAG(ENABLE_BUILTIN_SPELLCHECKER)
+#include "chrome/browser/spellchecker/spell_check_host_chrome_impl.h"  // nogncheck
+#include "components/spellcheck/common/spellcheck.mojom.h"  // nogncheck
+#endif
+
 #if BUILDFLAG(ENABLE_PEPPER_FLASH)
 #include "chrome/browser/renderer_host/pepper/chrome_browser_pepper_host_factory.h"
 #endif  // BUILDFLAG(ENABLE_PEPPER_FLASH)
@@ -768,6 +773,18 @@ network::mojom::NetworkContext* AtomBrowserClient::GetSystemNetworkContext() {
   return g_browser_process->system_network_context_manager()->GetContext();
 }
 
+#if BUILDFLAG(ENABLE_BUILTIN_SPELLCHECKER)
+void AtomBrowserClient::BindHostReceiverForRenderer(
+    content::RenderProcessHost* render_process_host,
+    mojo::GenericPendingReceiver receiver) {
+  if (auto host_receiver = receiver.As<spellcheck::mojom::SpellCheckHost>()) {
+    SpellCheckHostChromeImpl::Create(render_process_host->GetID(),
+                                     std::move(host_receiver));
+    return;
+  }
+}
+#endif
+
 base::Optional<service_manager::Manifest>
 AtomBrowserClient::GetServiceManifestOverlay(base::StringPiece name) {
   if (name == content::mojom::kBrowserServiceName)

+ 5 - 0
shell/browser/atom_browser_client.h

@@ -148,6 +148,11 @@ class AtomBrowserClient : public content::ContentBrowserClient,
       bool in_memory,
       const base::FilePath& relative_partition_path) override;
   network::mojom::NetworkContext* GetSystemNetworkContext() override;
+#if BUILDFLAG(ENABLE_BUILTIN_SPELLCHECKER)
+  void BindHostReceiverForRenderer(
+      content::RenderProcessHost* render_process_host,
+      mojo::GenericPendingReceiver receiver) override;
+#endif
   base::Optional<service_manager::Manifest> GetServiceManifestOverlay(
       base::StringPiece name) override;
   content::MediaObserver* GetMediaObserver() override;

+ 41 - 3
shell/browser/atom_browser_context.cc

@@ -47,8 +47,6 @@
 #include "shell/common/options_switches.h"
 
 #if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
-#include "components/pref_registry/pref_registry_syncable.h"
-#include "components/user_prefs/user_prefs.h"
 #include "extensions/browser/browser_context_keyed_service_factories.h"
 #include "extensions/browser/extension_pref_store.h"
 #include "extensions/browser/extension_pref_value_map_factory.h"
@@ -62,6 +60,19 @@
 #include "shell/common/extensions/atom_extensions_client.h"
 #endif  // BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
 
+#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS) || \
+    BUILDFLAG(ENABLE_BUILTIN_SPELLCHECKER)
+#include "components/pref_registry/pref_registry_syncable.h"
+#include "components/user_prefs/user_prefs.h"
+#endif
+
+#if BUILDFLAG(ENABLE_BUILTIN_SPELLCHECKER)
+#include "base/i18n/rtl.h"
+#include "components/language/core/browser/language_prefs.h"
+#include "components/spellcheck/browser/pref_names.h"
+#include "components/spellcheck/common/spellcheck_common.h"
+#endif
+
 using content::BrowserThread;
 
 namespace electron {
@@ -101,6 +112,7 @@ AtomBrowserContext::AtomBrowserContext(const std::string& partition,
     base::PathService::Get(DIR_APP_DATA, &path_);
     path_ = path_.Append(base::FilePath::FromUTF8Unsafe(GetApplicationName()));
     base::PathService::Override(DIR_USER_DATA, path_);
+    base::PathService::Override(chrome::DIR_USER_DATA, path_);
   }
 
   if (!in_memory && !partition.empty())
@@ -155,7 +167,10 @@ void AtomBrowserContext::InitPrefs() {
       ExtensionPrefValueMapFactory::GetForBrowserContext(this),
       IsOffTheRecord());
   prefs_factory.set_extension_prefs(ext_pref_store);
+#endif
 
+#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS) || \
+    BUILDFLAG(ENABLE_BUILTIN_SPELLCHECKER)
   auto registry = WrapRefCounted(new user_prefs::PrefRegistrySyncable);
 #else
   auto registry = WrapRefCounted(new PrefRegistrySimple);
@@ -176,13 +191,36 @@ void AtomBrowserContext::InitPrefs() {
   extensions::ExtensionPrefs::RegisterProfilePrefs(registry.get());
 #endif
 
+#if BUILDFLAG(ENABLE_BUILTIN_SPELLCHECKER)
+  BrowserContextDependencyManager::GetInstance()
+      ->RegisterProfilePrefsForServices(registry.get());
+
+  language::LanguagePrefs::RegisterProfilePrefs(registry.get());
+#endif
+
   prefs_ = prefs_factory.Create(
       registry.get(),
       std::make_unique<PrefStoreDelegate>(weak_factory_.GetWeakPtr()));
   prefs_->UpdateCommandLinePrefStore(new ValueMapPrefStore);
-#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
+#if BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS) || \
+    BUILDFLAG(ENABLE_BUILTIN_SPELLCHECKER)
   user_prefs::UserPrefs::Set(this, prefs_.get());
 #endif
+
+#if BUILDFLAG(ENABLE_BUILTIN_SPELLCHECKER)
+  auto* current_dictionaries =
+      prefs()->Get(spellcheck::prefs::kSpellCheckDictionaries);
+  // No configured dictionaries, the default will be en-US
+  if (current_dictionaries->GetList().size() == 0) {
+    std::string default_code = spellcheck::GetCorrespondingSpellCheckLanguage(
+        base::i18n::GetConfiguredLocale());
+    if (!default_code.empty()) {
+      base::ListValue language_codes;
+      language_codes.AppendString(default_code);
+      prefs()->Set(spellcheck::prefs::kSpellCheckDictionaries, language_codes);
+    }
+  }
+#endif
 }
 
 void AtomBrowserContext::SetUserAgent(const std::string& user_agent) {

+ 8 - 0
shell/browser/atom_browser_main_parts.cc

@@ -103,6 +103,10 @@
 #include "shell/common/extensions/atom_extensions_client.h"
 #endif  // BUILDFLAG(ENABLE_ELECTRON_EXTENSIONS)
 
+#if BUILDFLAG(ENABLE_BUILTIN_SPELLCHECKER)
+#include "chrome/browser/spellchecker/spellcheck_factory.h"  // nogncheck
+#endif
+
 namespace electron {
 
 namespace {
@@ -442,6 +446,10 @@ void AtomBrowserMainParts::PreMainMessageLoopRun() {
   extensions::electron::EnsureBrowserContextKeyedServiceFactoriesBuilt();
 #endif
 
+#if BUILDFLAG(ENABLE_BUILTIN_SPELLCHECKER)
+  SpellcheckServiceFactory::GetInstance();
+#endif
+
   // url::Add*Scheme are not threadsafe, this helps prevent data races.
   url::LockSchemeRegistries();
 

+ 9 - 0
shell/browser/web_contents_preferences.cc

@@ -144,6 +144,9 @@ WebContentsPreferences::WebContentsPreferences(
   SetDefaultBoolIfUndefined(options::kScrollBounce, false);
 #endif
   SetDefaultBoolIfUndefined(options::kOffscreen, false);
+#if BUILDFLAG(ENABLE_BUILTIN_SPELLCHECKER)
+  SetDefaultBoolIfUndefined(options::kSpellcheck, false);
+#endif
 
   // If this is a <webview> tag, and the embedder is offscreen-rendered, then
   // this WebContents is also offscreen-rendered.
@@ -414,6 +417,12 @@ void WebContentsPreferences::AppendCommandLineSwitches(
   if (IsEnabled(options::kNodeIntegrationInSubFrames))
     command_line->AppendSwitch(switches::kNodeIntegrationInSubFrames);
 
+#if BUILDFLAG(ENABLE_BUILTIN_SPELLCHECKER)
+  if (IsEnabled(options::kSpellcheck)) {
+    command_line->AppendSwitch(switches::kEnableSpellcheck);
+  }
+#endif
+
   // We are appending args to a webContents so let's save the current state
   // of our preferences object so that during the lifetime of the WebContents
   // we can fetch the options used to initally configure the WebContents

+ 3 - 0
shell/common/native_mate_converters/content_converter.cc

@@ -119,6 +119,9 @@ v8::Local<v8::Value> Converter<ContextMenuParamsWithWebContents>::ToV8(
   dict.Set("selectionText", params.selection_text);
   dict.Set("titleText", params.title_text);
   dict.Set("misspelledWord", params.misspelled_word);
+#if BUILDFLAG(ENABLE_BUILTIN_SPELLCHECKER)
+  dict.Set("dictionarySuggestions", params.dictionary_suggestions);
+#endif
   dict.Set("frameCharset", params.frame_charset);
   dict.Set("inputFieldType", params.input_field_type);
   dict.Set("menuSourceType", params.source_type);

+ 8 - 0
shell/common/options_switches.cc

@@ -173,6 +173,10 @@ const char kWebGL[] = "webgl";
 // navigation.
 const char kNavigateOnDragDrop[] = "navigateOnDragDrop";
 
+#if BUILDFLAG(ENABLE_BUILTIN_SPELLCHECKER)
+const char kSpellcheck[] = "spellcheck";
+#endif
+
 #if BUILDFLAG(ENABLE_REMOTE_MODULE)
 const char kEnableRemoteModule[] = "enableRemoteModule";
 #endif
@@ -267,6 +271,10 @@ const char kAuthNegotiateDelegateWhitelist[] =
 // If set, include the port in generated Kerberos SPNs.
 const char kEnableAuthNegotiatePort[] = "enable-auth-negotiate-port";
 
+#if BUILDFLAG(ENABLE_BUILTIN_SPELLCHECKER)
+const char kEnableSpellcheck[] = "enable-spellcheck";
+#endif
+
 #if BUILDFLAG(ENABLE_REMOTE_MODULE)
 const char kEnableRemoteModule[] = "enable-remote-module";
 #endif

+ 8 - 0
shell/common/options_switches.h

@@ -84,6 +84,10 @@ extern const char kTextAreasAreResizable[];
 extern const char kWebGL[];
 extern const char kNavigateOnDragDrop[];
 
+#if BUILDFLAG(ENABLE_BUILTIN_SPELLCHECKER)
+extern const char kSpellcheck[];
+#endif
+
 #if BUILDFLAG(ENABLE_REMOTE_MODULE)
 extern const char kEnableRemoteModule[];
 #endif
@@ -134,6 +138,10 @@ extern const char kAuthServerWhitelist[];
 extern const char kAuthNegotiateDelegateWhitelist[];
 extern const char kEnableAuthNegotiatePort[];
 
+#if BUILDFLAG(ENABLE_BUILTIN_SPELLCHECKER)
+extern const char kEnableSpellcheck[];
+#endif
+
 #if BUILDFLAG(ENABLE_REMOTE_MODULE)
 extern const char kEnableRemoteModule[];
 #endif

+ 40 - 0
shell/renderer/renderer_client_base.cc

@@ -6,6 +6,7 @@
 
 #include <memory>
 #include <string>
+#include <utility>
 #include <vector>
 
 #include "base/command_line.h"
@@ -46,6 +47,11 @@
 #include <shlobj.h>
 #endif
 
+#if BUILDFLAG(ENABLE_BUILTIN_SPELLCHECKER)
+#include "components/spellcheck/renderer/spellcheck.h"
+#include "components/spellcheck/renderer/spellcheck_provider.h"
+#endif
+
 #if BUILDFLAG(ENABLE_PDF_VIEWER)
 #include "shell/common/atom_constants.h"
 #endif  // BUILDFLAG(ENABLE_PDF_VIEWER)
@@ -149,6 +155,11 @@ void RendererClientBase::RenderThreadStarted() {
   thread->AddObserver(extensions_renderer_client_->GetDispatcher());
 #endif
 
+#if BUILDFLAG(ENABLE_BUILTIN_SPELLCHECKER)
+  if (command_line->HasSwitch(switches::kEnableSpellcheck))
+    spellcheck_ = std::make_unique<SpellCheck>(&registry_, this);
+#endif
+
   blink::WebCustomElement::AddEmbedderCustomElementName("webview");
   blink::WebCustomElement::AddEmbedderCustomElementName("browserplugin");
 
@@ -263,8 +274,37 @@ void RendererClientBase::RenderFrameCreated(
 
   dispatcher->OnRenderFrameCreated(render_frame);
 #endif
+
+#if BUILDFLAG(ENABLE_BUILTIN_SPELLCHECKER)
+  auto* command_line = base::CommandLine::ForCurrentProcess();
+  if (command_line->HasSwitch(switches::kEnableSpellcheck))
+    new SpellCheckProvider(render_frame, spellcheck_.get(), this);
+#endif
+}
+
+#if BUILDFLAG(ENABLE_BUILTIN_SPELLCHECKER)
+void RendererClientBase::BindReceiverOnMainThread(
+    mojo::GenericPendingReceiver receiver) {
+  // TODO(crbug.com/977637): Get rid of the use of BinderRegistry here. This is
+  // only used to bind a spellcheck interface.
+  std::string interface_name = *receiver.interface_name();
+  auto pipe = receiver.PassPipe();
+  registry_.TryBindInterface(interface_name, &pipe);
 }
 
+void RendererClientBase::GetInterface(
+    const std::string& interface_name,
+    mojo::ScopedMessagePipeHandle interface_pipe) {
+  // TODO(crbug.com/977637): Get rid of the use of this implementation of
+  // |service_manager::LocalInterfaceProvider|. This was done only to avoid
+  // churning spellcheck code while eliminating the "chrome" and
+  // "chrome_renderer" services. Spellcheck is (and should remain) the only
+  // consumer of this implementation.
+  content::RenderThread::Get()->BindHostReceiver(
+      mojo::GenericPendingReceiver(interface_name, std::move(interface_pipe)));
+}
+#endif
+
 void RendererClientBase::DidClearWindowObject(
     content::RenderFrame* render_frame) {
   // Make sure every page will get a script context created.

+ 26 - 1
shell/renderer/renderer_client_base.h

@@ -19,6 +19,13 @@
 #include "chrome/renderer/media/chrome_key_systems_provider.h"  // nogncheck
 #endif
 
+#if BUILDFLAG(ENABLE_BUILTIN_SPELLCHECKER)
+#include "services/service_manager/public/cpp/binder_registry.h"
+#include "services/service_manager/public/cpp/local_interface_provider.h"
+
+class SpellCheck;
+#endif
+
 namespace network_hints {
 class PrescientNetworkingDispatcher;
 }
@@ -35,11 +42,24 @@ namespace electron {
 class AtomExtensionsRendererClient;
 #endif
 
-class RendererClientBase : public content::ContentRendererClient {
+class RendererClientBase : public content::ContentRendererClient
+#if BUILDFLAG(ENABLE_BUILTIN_SPELLCHECKER)
+    ,
+                           public service_manager::LocalInterfaceProvider
+#endif
+{
  public:
   RendererClientBase();
   ~RendererClientBase() override;
 
+#if BUILDFLAG(ENABLE_BUILTIN_SPELLCHECKER)
+  // service_manager::LocalInterfaceProvider implementation.
+  void GetInterface(const std::string& name,
+                    mojo::ScopedMessagePipeHandle request_handle) override;
+
+  void BindReceiverOnMainThread(mojo::GenericPendingReceiver receiver) override;
+#endif
+
   virtual void DidCreateScriptContext(v8::Handle<v8::Context> context,
                                       content::RenderFrame* render_frame);
   virtual void WillReleaseScriptContext(v8::Handle<v8::Context> context,
@@ -108,6 +128,11 @@ class RendererClientBase : public content::ContentRendererClient {
   std::string renderer_client_id_;
   // An increasing ID used for indentifying an V8 context in this process.
   int64_t next_context_id_ = 0;
+
+#if BUILDFLAG(ENABLE_BUILTIN_SPELLCHECKER)
+  std::unique_ptr<SpellCheck> spellcheck_;
+  service_manager::BinderRegistry registry_;
+#endif
 };
 
 }  // namespace electron