index.ts 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  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. EditorView.baseTheme({
  32. '.ol-cm-spelling-error': {
  33. textDecorationColor: 'red',
  34. textDecorationLine: 'underline',
  35. textDecorationStyle: 'dotted',
  36. textDecorationThickness: '2px',
  37. textDecorationSkipInk: 'none',
  38. textUnderlineOffset: '0.2em',
  39. },
  40. '.cm-tooltip.ol-cm-spelling-context-menu-tooltip': {
  41. borderWidth: '0',
  42. },
  43. }),
  44. parserWatcher,
  45. spellCheckLanguageConf.of(spellCheckLanguageFacet.of(spellCheckLanguage)),
  46. spellCheckField,
  47. misspelledWordsField,
  48. ignoredWordsField,
  49. cacheField,
  50. spellingMenuField,
  51. ]
  52. }
  53. const spellCheckField = StateField.define<SpellChecker | null>({
  54. create(state) {
  55. const [spellCheckLanguage] = state.facet(spellCheckLanguageFacet)
  56. return spellCheckLanguage ? new SpellChecker(spellCheckLanguage) : null
  57. },
  58. update(value, tr) {
  59. for (const effect of tr.effects) {
  60. if (effect.is(setSpellCheckLanguageEffect)) {
  61. value?.destroy()
  62. return effect.value ? new SpellChecker(effect.value) : null
  63. }
  64. }
  65. return value
  66. },
  67. provide(field) {
  68. return [
  69. ViewPlugin.define(view => {
  70. return {
  71. destroy: () => {
  72. view.state.field(field)?.destroy()
  73. },
  74. }
  75. }),
  76. EditorView.domEventHandlers({
  77. focus: (event, view) => {
  78. if (view.state.facet(EditorView.editable)) {
  79. view.state.field(field)?.spellCheckAsap(view)
  80. }
  81. },
  82. }),
  83. EditorView.updateListener.of(update => {
  84. if (update.state.facet(EditorView.editable)) {
  85. update.state.field(field)?.handleUpdate(update)
  86. }
  87. }),
  88. ]
  89. },
  90. })
  91. const setSpellCheckLanguageEffect = StateEffect.define<string | undefined>()
  92. export const setSpelling = ({
  93. spellCheckLanguage,
  94. }: Options): TransactionSpec => {
  95. return {
  96. effects: [
  97. resetMisspelledWords.of(null),
  98. spellCheckLanguageConf.reconfigure(
  99. spellCheckLanguageFacet.of(spellCheckLanguage)
  100. ),
  101. setSpellCheckLanguageEffect.of(spellCheckLanguage),
  102. ],
  103. }
  104. }
  105. export const addLearnedWord = (
  106. spellCheckLanguage: string,
  107. word: string
  108. ): TransactionSpec => {
  109. return {
  110. effects: [
  111. addWordToCache.of({
  112. lang: spellCheckLanguage,
  113. wordText: word,
  114. value: true,
  115. }),
  116. updateAfterAddingIgnoredWord.of(word),
  117. ],
  118. }
  119. }
  120. export const removeLearnedWord = (
  121. spellCheckLanguage: string,
  122. word: string
  123. ): TransactionSpec => {
  124. return {
  125. effects: [
  126. removeWordFromCache.of({
  127. lang: spellCheckLanguage,
  128. wordText: word,
  129. }),
  130. ],
  131. }
  132. }
  133. export const resetLearnedWords = (): TransactionSpec => {
  134. return {
  135. effects: [resetSpellChecker.of(null)],
  136. }
  137. }