index.ts 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. import { EditorView, ViewPlugin } from '@codemirror/view'
  2. import {
  3. EditorState,
  4. StateEffect,
  5. StateField,
  6. TransactionSpec,
  7. } from '@codemirror/state'
  8. import { misspelledWordsField } from './misspelled-words'
  9. import {
  10. addIgnoredWord,
  11. ignoredWordsField,
  12. removeIgnoredWord,
  13. resetSpellChecker,
  14. updateAfterAddingIgnoredWord,
  15. } from './ignored-words'
  16. import { addWordToCache, cacheField, removeWordFromCache } from './cache'
  17. import { hideSpellingMenu, spellingMenuField } from './context-menu'
  18. import { SpellChecker } from './spellchecker'
  19. import { parserWatcher } from '../wait-for-parser'
  20. import type { HunspellManager } from '@/features/source-editor/hunspell/HunspellManager'
  21. import { debugConsole } from '@/utils/debugging'
  22. type Options = {
  23. spellCheckLanguage?: string
  24. hunspellManager?: HunspellManager
  25. }
  26. /**
  27. * A custom extension that creates a spell checker for the current language (from the user settings).
  28. * The spell check runs on the server whenever a line changes.
  29. * The mis-spelled words, ignored words and spell-checked words are stored in a state field.
  30. * Mis-spelled words are decorated with a Mark decoration.
  31. * The suggestions menu is displayed in a tooltip, activated with a right-click on the decoration.
  32. */
  33. export const spelling = ({ spellCheckLanguage, hunspellManager }: Options) => {
  34. return [
  35. spellingTheme,
  36. parserWatcher,
  37. spellCheckLanguageField.init(() => spellCheckLanguage),
  38. spellCheckerField.init(() =>
  39. spellCheckLanguage
  40. ? new SpellChecker(spellCheckLanguage, hunspellManager)
  41. : null
  42. ),
  43. misspelledWordsField,
  44. ignoredWordsField,
  45. cacheField,
  46. spellingMenuField,
  47. ]
  48. }
  49. const spellingTheme = EditorView.baseTheme({
  50. '.ol-cm-spelling-error': {
  51. textDecorationColor: 'red',
  52. textDecorationLine: 'underline',
  53. textDecorationStyle: 'dotted',
  54. textDecorationThickness: '2px',
  55. textDecorationSkipInk: 'none',
  56. textUnderlineOffset: '0.2em',
  57. },
  58. '.cm-tooltip.ol-cm-spelling-context-menu-tooltip': {
  59. borderWidth: '0',
  60. },
  61. })
  62. export const getSpellChecker = (state: EditorState) =>
  63. state.field(spellCheckerField, false)
  64. const spellCheckerField = StateField.define<SpellChecker | null>({
  65. create() {
  66. return null
  67. },
  68. update(value, tr) {
  69. for (const effect of tr.effects) {
  70. if (effect.is(setSpellCheckLanguageEffect)) {
  71. value?.destroy()
  72. value = effect.value.spellCheckLanguage
  73. ? new SpellChecker(
  74. effect.value.spellCheckLanguage,
  75. effect.value.hunspellManager
  76. )
  77. : null
  78. } else if (effect.is(addIgnoredWord)) {
  79. value?.addWord(effect.value.text).catch(debugConsole.error)
  80. } else if (effect.is(removeIgnoredWord)) {
  81. value?.removeWord(effect.value.text).catch(debugConsole.error)
  82. }
  83. }
  84. return value
  85. },
  86. provide(field) {
  87. return [
  88. ViewPlugin.define(view => {
  89. return {
  90. destroy: () => {
  91. view.state.field(field)?.destroy()
  92. },
  93. }
  94. }),
  95. EditorView.domEventHandlers({
  96. focus: (_event, view) => {
  97. if (view.state.facet(EditorView.editable)) {
  98. view.state.field(field)?.scheduleSpellCheck(view)
  99. }
  100. },
  101. }),
  102. EditorView.updateListener.of(update => {
  103. if (update.state.facet(EditorView.editable)) {
  104. update.state.field(field)?.handleUpdate(update)
  105. }
  106. }),
  107. ]
  108. },
  109. })
  110. export const getSpellCheckLanguage = (state: EditorState) =>
  111. state.field(spellCheckLanguageField, false)
  112. const spellCheckLanguageField = StateField.define<string | undefined>({
  113. create() {
  114. return undefined
  115. },
  116. update(value, tr) {
  117. for (const effect of tr.effects) {
  118. if (effect.is(setSpellCheckLanguageEffect)) {
  119. value = effect.value.spellCheckLanguage
  120. }
  121. }
  122. return value
  123. },
  124. })
  125. export const setSpellCheckLanguageEffect = StateEffect.define<{
  126. spellCheckLanguage: string | undefined
  127. hunspellManager?: HunspellManager
  128. }>()
  129. export const setSpellCheckLanguage = ({
  130. spellCheckLanguage,
  131. hunspellManager,
  132. }: Options): TransactionSpec => {
  133. return {
  134. effects: [
  135. setSpellCheckLanguageEffect.of({
  136. spellCheckLanguage,
  137. hunspellManager,
  138. }),
  139. hideSpellingMenu.of(null),
  140. ],
  141. }
  142. }
  143. export const addLearnedWord = (
  144. spellCheckLanguage: string,
  145. word: string
  146. ): TransactionSpec => {
  147. return {
  148. effects: [
  149. addWordToCache.of({
  150. lang: spellCheckLanguage,
  151. wordText: word,
  152. value: true,
  153. }),
  154. updateAfterAddingIgnoredWord.of(word),
  155. ],
  156. }
  157. }
  158. export const removeLearnedWord = (
  159. spellCheckLanguage: string,
  160. word: string
  161. ): TransactionSpec => {
  162. return {
  163. effects: [
  164. removeWordFromCache.of({
  165. lang: spellCheckLanguage,
  166. wordText: word,
  167. }),
  168. removeIgnoredWord.of({ text: word }),
  169. ],
  170. }
  171. }
  172. export const resetLearnedWords = (): TransactionSpec => {
  173. return {
  174. effects: [resetSpellChecker.of(null)],
  175. }
  176. }