visual.ts 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. import {
  2. Compartment,
  3. EditorState,
  4. Extension,
  5. StateEffect,
  6. StateField,
  7. TransactionSpec,
  8. } from '@codemirror/state'
  9. import { visualHighlightStyle, visualTheme } from './visual-theme'
  10. import { atomicDecorations } from './atomic-decorations'
  11. import { markDecorations } from './mark-decorations'
  12. import importOverleafModules from '../../../../../macros/import-overleaf-module.macro'
  13. import { EditorView, ViewPlugin } from '@codemirror/view'
  14. import { visualKeymap } from './visual-keymap'
  15. import { mousedown, mouseDownEffect } from './selection'
  16. import { forceParsing, syntaxTree } from '@codemirror/language'
  17. import { hasLanguageLoadedEffect } from '../language'
  18. import { restoreScrollPosition } from '../scroll-position'
  19. import { listItemMarker } from './list-item-marker'
  20. import { pasteHtml } from './paste-html'
  21. import { commandTooltip } from '../command-tooltip'
  22. import { tableGeneratorTheme } from './table-generator'
  23. import { debugConsole } from '@/utils/debugging'
  24. import { PreviewPath } from '../../../../../../types/preview-path'
  25. type Options = {
  26. visual: boolean
  27. previewByPath: (path: string) => PreviewPath | null
  28. }
  29. // Language-specific visual editor extensions provided by modules (e.g. the
  30. // markdown visual editor). They live in the visual bundle so they are only
  31. // active in visual mode and react to switching editor modes.
  32. const visualEditorExtensions: Array<(options: Options) => Extension> =
  33. importOverleafModules('sourceEditorMarkdownExtensions').map(
  34. (item: { import: { extension: (options: Options) => Extension } }) =>
  35. item.import.extension
  36. )
  37. const visualConf = new Compartment()
  38. export const toggleVisualEffect = StateEffect.define<boolean>()
  39. const visualState = StateField.define<boolean>({
  40. create() {
  41. return false
  42. },
  43. update(value, tr) {
  44. for (const effect of tr.effects) {
  45. if (effect.is(toggleVisualEffect)) {
  46. return effect.value
  47. }
  48. }
  49. return value
  50. },
  51. })
  52. const configureVisualExtensions = (options: Options) =>
  53. options.visual ? extension(options) : []
  54. export const visual = (options: Options): Extension => {
  55. return [
  56. visualState.init(() => options.visual),
  57. visualConf.of(configureVisualExtensions(options)),
  58. ]
  59. }
  60. export const isVisual = (view: EditorView) => {
  61. return view.state.field(visualState, false) || false
  62. }
  63. export const setVisual = (options: Options): TransactionSpec => {
  64. return {
  65. effects: [
  66. toggleVisualEffect.of(options.visual),
  67. visualConf.reconfigure(configureVisualExtensions(options)),
  68. ],
  69. }
  70. }
  71. export const sourceOnly = (visual: boolean, extension: Extension) => {
  72. const conf = new Compartment()
  73. const configure = (visual: boolean) => (visual ? [] : extension)
  74. return [
  75. conf.of(configure(visual)),
  76. // Respond to switching editor modes
  77. EditorState.transactionExtender.of(tr => {
  78. for (const effect of tr.effects) {
  79. if (effect.is(toggleVisualEffect)) {
  80. return {
  81. effects: conf.reconfigure(configure(effect.value)),
  82. }
  83. }
  84. }
  85. return null
  86. }),
  87. ]
  88. }
  89. const parsedAttributesConf = new Compartment()
  90. /**
  91. * A view plugin which shows the editor content, makes it focusable,
  92. * and restores the scroll position, once the initial decorations have been applied.
  93. */
  94. export const showContentWhenParsed = [
  95. parsedAttributesConf.of([EditorView.editable.of(false)]),
  96. ViewPlugin.define(view => {
  97. const showContent = () => {
  98. view.dispatch(
  99. {
  100. effects: parsedAttributesConf.reconfigure([
  101. EditorView.editorAttributes.of({
  102. class: 'ol-cm-parsed',
  103. }),
  104. EditorView.editable.of(true),
  105. ]),
  106. },
  107. restoreScrollPosition()
  108. )
  109. view.focus()
  110. }
  111. // already parsed
  112. if (syntaxTree(view.state).length === view.state.doc.length) {
  113. window.setTimeout(showContent)
  114. return {}
  115. }
  116. // as a fallback, make sure the content is visible after 5s
  117. const fallbackTimer = window.setTimeout(showContent, 5000)
  118. let languageLoaded = false
  119. return {
  120. update(update) {
  121. // wait for the language to load before telling the parser to run
  122. if (!languageLoaded && hasLanguageLoadedEffect(update)) {
  123. languageLoaded = true
  124. // in a timeout, as this is already in a dispatch cycle
  125. window.setTimeout(() => {
  126. // run asynchronously
  127. new Promise(() => {
  128. // tell the parser to run until the end of the document
  129. forceParsing(view, view.state.doc.length, Infinity)
  130. // clear the fallback timeout
  131. window.clearTimeout(fallbackTimer)
  132. // show the content, in a timeout so the decorations can build first
  133. window.setTimeout(showContent)
  134. }).catch(debugConsole.error)
  135. })
  136. }
  137. },
  138. }
  139. }),
  140. ]
  141. /**
  142. * A transaction extender which scrolls mouse clicks into view, in case decorations have moved the cursor out of view.
  143. */
  144. const scrollJumpAdjuster = EditorState.transactionExtender.of(tr => {
  145. // Attach a "scrollIntoView" effect on all mouse selections to adjust for
  146. // any jumps that may occur when hiding/showing decorations.
  147. if (!tr.scrollIntoView) {
  148. for (const effect of tr.effects) {
  149. if (effect.is(mouseDownEffect) && effect.value === false) {
  150. return {
  151. effects: EditorView.scrollIntoView(tr.newSelection.main.head),
  152. }
  153. }
  154. }
  155. }
  156. return {}
  157. })
  158. const extension = (options: Options) => [
  159. visualTheme,
  160. visualHighlightStyle,
  161. mousedown,
  162. listItemMarker,
  163. atomicDecorations(options),
  164. visualEditorExtensions.map(extension => extension(options)),
  165. markDecorations, // NOTE: must be after atomicDecorations, so that mark decorations wrap inline widgets
  166. visualKeymap,
  167. commandTooltip,
  168. scrollJumpAdjuster,
  169. showContentWhenParsed,
  170. pasteHtml,
  171. tableGeneratorTheme,
  172. EditorView.contentAttributes.of({ 'aria-label': 'Visual Editor editing' }),
  173. ]