visual.ts 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  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 { EditorView, ViewPlugin } from '@codemirror/view'
  13. import { visualKeymap } from './visual-keymap'
  14. import { skipPreambleWithCursor } from './skip-preamble-cursor'
  15. import { mouseDownEffect, mouseDownListener } from './selection'
  16. import { findEffect } from '../../utils/effects'
  17. import { forceParsing, syntaxTree } from '@codemirror/language'
  18. import { hasLanguageLoadedEffect } from '../language'
  19. import { restoreScrollPosition } from '../scroll-position'
  20. import { toolbarPanel } from '../toolbar/toolbar-panel'
  21. import { CurrentDoc } from '../../../../../../types/current-doc'
  22. import isValidTeXFile from '../../../../main/is-valid-tex-file'
  23. import { listItemMarker } from './list-item-marker'
  24. import { figureModalPasteHandler } from '../figure-modal'
  25. type Options = {
  26. visual: boolean
  27. fileTreeManager: {
  28. getPreviewByPath: (
  29. path: string
  30. ) => { url: string; extension: string } | null
  31. }
  32. }
  33. const visualConf = new Compartment()
  34. export const toggleVisualEffect = StateEffect.define<boolean>()
  35. export const findToggleVisualEffect = findEffect(toggleVisualEffect)
  36. const visualState = StateField.define<boolean>({
  37. create() {
  38. return false
  39. },
  40. update(value, tr) {
  41. return findToggleVisualEffect(tr)?.value ?? value
  42. },
  43. })
  44. const configureVisualExtensions = (options: Options) =>
  45. options.visual ? extension(options) : []
  46. export const visual = (currentDoc: CurrentDoc, options: Options): Extension => {
  47. if (!isValidTeXFile(currentDoc.docName)) {
  48. return []
  49. }
  50. return [
  51. visualState.init(() => options.visual),
  52. visualConf.of(configureVisualExtensions(options)),
  53. ]
  54. }
  55. export const isVisual = (view: EditorView) => {
  56. return view.state.field(visualState, false) || false
  57. }
  58. export const setVisual = (options: Options): TransactionSpec => {
  59. return {
  60. effects: [
  61. toggleVisualEffect.of(options.visual),
  62. visualConf.reconfigure(configureVisualExtensions(options)),
  63. ],
  64. }
  65. }
  66. export const sourceOnly = (visual: boolean, extension: Extension) => {
  67. const conf = new Compartment()
  68. const configure = (visual: boolean) => (visual ? [] : extension)
  69. return [
  70. conf.of(configure(visual)),
  71. // Respond to switching editor modes
  72. EditorState.transactionExtender.of(tr => {
  73. const effect = findToggleVisualEffect(tr)
  74. if (effect) {
  75. return {
  76. effects: conf.reconfigure(configure(effect.value)),
  77. }
  78. }
  79. return null
  80. }),
  81. // restore the scroll position when switching to source mode
  82. EditorView.updateListener.of(update => {
  83. for (const tr of update.transactions) {
  84. for (const effect of tr.effects) {
  85. if (effect.is(toggleVisualEffect)) {
  86. if (!effect.value) {
  87. // switching to the source editor
  88. window.setTimeout(() => {
  89. update.view.dispatch(restoreScrollPosition())
  90. update.view.focus()
  91. })
  92. }
  93. }
  94. }
  95. }
  96. }),
  97. ]
  98. }
  99. const parsedAttributesConf = new Compartment()
  100. const showContentWhenParsed = [
  101. parsedAttributesConf.of([EditorView.editable.of(false)]),
  102. ViewPlugin.define(view => {
  103. const showContent = () => {
  104. view.dispatch(
  105. {
  106. effects: parsedAttributesConf.reconfigure([
  107. EditorView.editorAttributes.of({
  108. class: 'ol-cm-parsed',
  109. }),
  110. EditorView.editable.of(true),
  111. ]),
  112. },
  113. restoreScrollPosition()
  114. )
  115. view.focus()
  116. }
  117. // already parsed
  118. if (syntaxTree(view.state).length === view.state.doc.length) {
  119. window.setTimeout(showContent)
  120. return {}
  121. }
  122. // as a fallback, make sure the content is visible after 5s
  123. const fallbackTimer = window.setTimeout(showContent, 5000)
  124. let languageLoaded = false
  125. return {
  126. update(update) {
  127. // wait for the language to load before telling the parser to run
  128. if (!languageLoaded && hasLanguageLoadedEffect(update)) {
  129. languageLoaded = true
  130. // in a timeout, as this is already in a dispatch cycle
  131. window.setTimeout(() => {
  132. // run asynchronously
  133. new Promise(() => {
  134. // tell the parser to run until the end of the document
  135. forceParsing(view, view.state.doc.length, Infinity)
  136. // clear the fallback timeout
  137. window.clearTimeout(fallbackTimer)
  138. // show the content, in a timeout so the decorations can build first
  139. window.setTimeout(showContent)
  140. }).catch(error => {
  141. console.error(error)
  142. })
  143. })
  144. }
  145. },
  146. }
  147. }),
  148. ]
  149. const scrollJumpAdjuster = EditorState.transactionExtender.of(tr => {
  150. // Attach a "scrollIntoView" effect on all mouse selections to adjust for
  151. // any jumps that may occur when hiding/showing decorations.
  152. if (!tr.scrollIntoView) {
  153. for (const effect of tr.effects) {
  154. if (effect.is(mouseDownEffect) && effect.value === false) {
  155. return {
  156. effects: EditorView.scrollIntoView(tr.newSelection.main.head),
  157. }
  158. }
  159. }
  160. }
  161. return {}
  162. })
  163. const extension = (options: Options) => [
  164. visualTheme,
  165. visualHighlightStyle,
  166. mouseDownListener,
  167. listItemMarker,
  168. markDecorations,
  169. atomicDecorations(options),
  170. skipPreambleWithCursor,
  171. visualKeymap,
  172. toolbarPanel(),
  173. scrollJumpAdjuster,
  174. showContentWhenParsed,
  175. figureModalPasteHandler(),
  176. ]