index.ts 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. import { EditorView, ViewPlugin } from '@codemirror/view'
  2. import {
  3. Compartment,
  4. Facet,
  5. StateEffect,
  6. StateField,
  7. TransactionSpec,
  8. } from '@codemirror/state'
  9. import { misspelledWordsField, resetMisspelledWords } from './misspelled-words'
  10. import {
  11. ignoredWordsField,
  12. resetSpellChecker,
  13. updateAfterAddingIgnoredWord,
  14. } from './ignored-words'
  15. import { addWordToCache, cacheField, removeWordFromCache } from './cache'
  16. import { spellingMenuField } from './context-menu'
  17. import { SpellChecker } from './spellchecker'
  18. import { parserWatcher } from '../wait-for-parser'
  19. const spellCheckLanguageConf = new Compartment()
  20. const spellCheckLanguageFacet = Facet.define<string | undefined>()
  21. type Options = { spellCheckLanguage?: string }
  22. /**
  23. * A custom extension that creates a spell checker for the current language (from the user settings).
  24. * The spell check runs on the server whenever a line changes.
  25. * The mis-spelled words, ignored words and spell-checked words are stored in a state field.
  26. * Mis-spelled words are decorated with a Mark decoration.
  27. * The suggestions menu is displayed in a tooltip, activated with a right-click on the decoration.
  28. */
  29. export const spelling = ({ spellCheckLanguage }: Options) => {
  30. return [
  31. spellingTheme,
  32. parserWatcher,
  33. spellCheckLanguageConf.of(spellCheckLanguageFacet.of(spellCheckLanguage)),
  34. spellCheckField,
  35. misspelledWordsField,
  36. ignoredWordsField,
  37. cacheField,
  38. spellingMenuField,
  39. ]
  40. }
  41. const spellingTheme = EditorView.baseTheme({
  42. '.ol-cm-spelling-error': {
  43. textDecorationColor: 'red',
  44. textDecorationLine: 'underline',
  45. textDecorationStyle: 'dotted',
  46. textDecorationThickness: '2px',
  47. textDecorationSkipInk: 'none',
  48. textUnderlineOffset: '0.2em',
  49. },
  50. '.cm-tooltip.ol-cm-spelling-context-menu-tooltip': {
  51. borderWidth: '0',
  52. },
  53. })
  54. const spellCheckField = StateField.define<SpellChecker | null>({
  55. create(state) {
  56. const [spellCheckLanguage] = state.facet(spellCheckLanguageFacet)
  57. return spellCheckLanguage ? new SpellChecker(spellCheckLanguage) : null
  58. },
  59. update(value, tr) {
  60. for (const effect of tr.effects) {
  61. if (effect.is(setSpellCheckLanguageEffect)) {
  62. value?.destroy()
  63. return effect.value ? new SpellChecker(effect.value) : null
  64. }
  65. }
  66. return value
  67. },
  68. provide(field) {
  69. return [
  70. ViewPlugin.define(view => {
  71. return {
  72. destroy: () => {
  73. view.state.field(field)?.destroy()
  74. },
  75. }
  76. }),
  77. EditorView.domEventHandlers({
  78. focus: (event, view) => {
  79. if (view.state.facet(EditorView.editable)) {
  80. view.state.field(field)?.spellCheckAsap(view)
  81. }
  82. },
  83. }),
  84. EditorView.updateListener.of(update => {
  85. if (update.state.facet(EditorView.editable)) {
  86. update.state.field(field)?.handleUpdate(update)
  87. }
  88. }),
  89. ]
  90. },
  91. })
  92. const setSpellCheckLanguageEffect = StateEffect.define<string | undefined>()
  93. export const setSpelling = ({
  94. spellCheckLanguage,
  95. }: Options): TransactionSpec => {
  96. return {
  97. effects: [
  98. resetMisspelledWords.of(null),
  99. spellCheckLanguageConf.reconfigure(
  100. spellCheckLanguageFacet.of(spellCheckLanguage)
  101. ),
  102. setSpellCheckLanguageEffect.of(spellCheckLanguage),
  103. ],
  104. }
  105. }
  106. export const addLearnedWord = (
  107. spellCheckLanguage: string,
  108. word: string
  109. ): TransactionSpec => {
  110. return {
  111. effects: [
  112. addWordToCache.of({
  113. lang: spellCheckLanguage,
  114. wordText: word,
  115. value: true,
  116. }),
  117. updateAfterAddingIgnoredWord.of(word),
  118. ],
  119. }
  120. }
  121. export const removeLearnedWord = (
  122. spellCheckLanguage: string,
  123. word: string
  124. ): TransactionSpec => {
  125. return {
  126. effects: [
  127. removeWordFromCache.of({
  128. lang: spellCheckLanguage,
  129. wordText: word,
  130. }),
  131. ],
  132. }
  133. }
  134. export const resetLearnedWords = (): TransactionSpec => {
  135. return {
  136. effects: [resetSpellChecker.of(null)],
  137. }
  138. }