visual.ts 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  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 { mousedown, mouseDownEffect } 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 { CurrentDoc } from '../../../../../../types/current-doc'
  21. import isValidTeXFile from '../../../../main/is-valid-tex-file'
  22. import { listItemMarker } from './list-item-marker'
  23. import { figureModalPasteHandler } from '../figure-modal'
  24. import { isSplitTestEnabled } from '../../../../utils/splitTestUtils'
  25. import { toolbarPanel } from '../toolbar/toolbar-panel'
  26. import { selectDecoratedArgument } from './select-decorated-argument'
  27. import { pasteHtml } from './paste-html'
  28. import { commandTooltip } from '../command-tooltip'
  29. import { tableGeneratorTheme } from './table-generator'
  30. type Options = {
  31. visual: boolean
  32. fileTreeManager: {
  33. getPreviewByPath: (
  34. path: string
  35. ) => { url: string; extension: string } | null
  36. }
  37. }
  38. const visualConf = new Compartment()
  39. export const toggleVisualEffect = StateEffect.define<boolean>()
  40. export const findToggleVisualEffect = findEffect(toggleVisualEffect)
  41. const visualState = StateField.define<boolean>({
  42. create() {
  43. return false
  44. },
  45. update(value, tr) {
  46. return findToggleVisualEffect(tr)?.value ?? value
  47. },
  48. })
  49. const configureVisualExtensions = (options: Options) =>
  50. options.visual ? extension(options) : []
  51. export const visual = (currentDoc: CurrentDoc, options: Options): Extension => {
  52. if (!isValidTeXFile(currentDoc.docName)) {
  53. return []
  54. }
  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. const effect = findToggleVisualEffect(tr)
  79. if (effect) {
  80. return {
  81. effects: conf.reconfigure(configure(effect.value)),
  82. }
  83. }
  84. return null
  85. }),
  86. // restore the scroll position when switching to source mode
  87. EditorView.updateListener.of(update => {
  88. for (const tr of update.transactions) {
  89. for (const effect of tr.effects) {
  90. if (effect.is(toggleVisualEffect)) {
  91. if (!effect.value) {
  92. // switching to the source editor
  93. window.setTimeout(() => {
  94. update.view.dispatch(restoreScrollPosition())
  95. update.view.focus()
  96. })
  97. }
  98. }
  99. }
  100. }
  101. }),
  102. ]
  103. }
  104. const parsedAttributesConf = new Compartment()
  105. /**
  106. * A view plugin which shows the editor content, makes it focusable,
  107. * and restores the scroll position, once the initial decorations have been applied.
  108. */
  109. const showContentWhenParsed = [
  110. parsedAttributesConf.of([EditorView.editable.of(false)]),
  111. ViewPlugin.define(view => {
  112. const showContent = () => {
  113. view.dispatch(
  114. {
  115. effects: parsedAttributesConf.reconfigure([
  116. EditorView.editorAttributes.of({
  117. class: 'ol-cm-parsed',
  118. }),
  119. EditorView.editable.of(true),
  120. ]),
  121. },
  122. restoreScrollPosition()
  123. )
  124. view.focus()
  125. }
  126. // already parsed
  127. if (syntaxTree(view.state).length === view.state.doc.length) {
  128. window.setTimeout(showContent)
  129. return {}
  130. }
  131. // as a fallback, make sure the content is visible after 5s
  132. const fallbackTimer = window.setTimeout(showContent, 5000)
  133. let languageLoaded = false
  134. return {
  135. update(update) {
  136. // wait for the language to load before telling the parser to run
  137. if (!languageLoaded && hasLanguageLoadedEffect(update)) {
  138. languageLoaded = true
  139. // in a timeout, as this is already in a dispatch cycle
  140. window.setTimeout(() => {
  141. // run asynchronously
  142. new Promise(() => {
  143. // tell the parser to run until the end of the document
  144. forceParsing(view, view.state.doc.length, Infinity)
  145. // clear the fallback timeout
  146. window.clearTimeout(fallbackTimer)
  147. // show the content, in a timeout so the decorations can build first
  148. window.setTimeout(showContent)
  149. }).catch(error => {
  150. console.error(error)
  151. })
  152. })
  153. }
  154. },
  155. }
  156. }),
  157. ]
  158. /**
  159. * A transaction extender which scrolls mouse clicks into view, in case decorations have moved the cursor out of view.
  160. */
  161. const scrollJumpAdjuster = EditorState.transactionExtender.of(tr => {
  162. // Attach a "scrollIntoView" effect on all mouse selections to adjust for
  163. // any jumps that may occur when hiding/showing decorations.
  164. if (!tr.scrollIntoView) {
  165. for (const effect of tr.effects) {
  166. if (effect.is(mouseDownEffect) && effect.value === false) {
  167. return {
  168. effects: EditorView.scrollIntoView(tr.newSelection.main.head),
  169. }
  170. }
  171. }
  172. }
  173. return {}
  174. })
  175. const extension = (options: Options) => [
  176. visualTheme,
  177. visualHighlightStyle,
  178. mousedown,
  179. listItemMarker,
  180. atomicDecorations(options),
  181. markDecorations, // NOTE: must be after atomicDecorations, so that mark decorations wrap inline widgets
  182. skipPreambleWithCursor,
  183. visualKeymap,
  184. commandTooltip,
  185. scrollJumpAdjuster,
  186. isSplitTestEnabled('source-editor-toolbar') ? [] : toolbarPanel(),
  187. selectDecoratedArgument,
  188. showContentWhenParsed,
  189. figureModalPasteHandler(),
  190. isSplitTestEnabled('paste-html') ? pasteHtml : [],
  191. isSplitTestEnabled('table-generator') ? tableGeneratorTheme : [],
  192. ]