document-diff-viewer.tsx 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. import { useCallback, useEffect, useState } from 'react'
  2. import {
  3. EditorSelection,
  4. EditorState,
  5. Extension,
  6. StateEffect,
  7. } from '@codemirror/state'
  8. import { EditorView, lineNumbers } from '@codemirror/view'
  9. import { indentationMarkers } from '@replit/codemirror-indentation-markers'
  10. import { highlights, setHighlightsEffect } from '../../extensions/highlights'
  11. import { useUserSettingsContext } from '@/shared/context/user-settings-context'
  12. import { theme, Options, setOptionsTheme } from '../../extensions/theme'
  13. import { indentUnit } from '@codemirror/language'
  14. import { Highlight } from '../../services/types/doc'
  15. import useIsMounted from '../../../../shared/hooks/use-is-mounted'
  16. import {
  17. highlightLocations,
  18. highlightLocationsField,
  19. scrollToHighlight,
  20. } from '../../extensions/highlight-locations'
  21. import { useTranslation } from 'react-i18next'
  22. import { inlineBackground } from '../../../source-editor/extensions/inline-background'
  23. import OLButton from '@/features/ui/components/ol/ol-button'
  24. import { bsVersion } from '@/features/utils/bootstrap-5'
  25. function extensions(themeOptions: Options): Extension[] {
  26. return [
  27. EditorView.editable.of(false),
  28. EditorState.readOnly.of(true),
  29. EditorView.contentAttributes.of({ tabindex: '0' }),
  30. lineNumbers(),
  31. EditorView.lineWrapping,
  32. indentUnit.of(' '), // TODO: Vary this by file type
  33. indentationMarkers({ hideFirstIndent: true, highlightActiveBlock: false }),
  34. highlights(),
  35. highlightLocations(),
  36. theme(themeOptions),
  37. inlineBackground(false),
  38. ]
  39. }
  40. function DocumentDiffViewer({
  41. doc,
  42. highlights,
  43. }: {
  44. doc: string
  45. highlights: Highlight[]
  46. }) {
  47. const { userSettings } = useUserSettingsContext()
  48. const { fontFamily, fontSize, lineHeight } = userSettings
  49. const isMounted = useIsMounted()
  50. const { t } = useTranslation()
  51. const [state, setState] = useState(() => {
  52. return EditorState.create({
  53. doc,
  54. extensions: extensions({
  55. fontSize,
  56. fontFamily,
  57. lineHeight,
  58. }),
  59. })
  60. })
  61. const [view] = useState<EditorView>(() => {
  62. return new EditorView({
  63. state,
  64. dispatch: tr => {
  65. view.update([tr])
  66. if (isMounted.current) {
  67. setState(view.state)
  68. }
  69. },
  70. })
  71. })
  72. const highlightLocations = state.field(highlightLocationsField)
  73. // Append the editor view DOM to the container node when mounted
  74. const containerRef = useCallback(
  75. node => {
  76. if (node) {
  77. node.appendChild(view.dom)
  78. }
  79. },
  80. [view]
  81. )
  82. const scrollToPrevious = useCallback(() => {
  83. if (highlightLocations.previous) {
  84. scrollToHighlight(view, highlightLocations.previous)
  85. }
  86. }, [highlightLocations.previous, view])
  87. const scrollToNext = useCallback(() => {
  88. if (highlightLocations.next) {
  89. scrollToHighlight(view, highlightLocations.next)
  90. }
  91. }, [highlightLocations.next, view])
  92. const { before, after } = highlightLocations
  93. useEffect(() => {
  94. const effects: StateEffect<unknown>[] = [setHighlightsEffect.of(highlights)]
  95. if (highlights.length > 0) {
  96. const { from, to } = highlights[0].range
  97. effects.push(
  98. EditorView.scrollIntoView(EditorSelection.range(from, to), {
  99. y: 'center',
  100. })
  101. )
  102. }
  103. view.dispatch({
  104. changes: { from: 0, to: view.state.doc.length, insert: doc },
  105. effects,
  106. })
  107. }, [doc, highlights, view])
  108. // Update the document diff viewer theme whenever the font size, font family
  109. // or line height user setting changes
  110. useEffect(() => {
  111. view.dispatch(
  112. setOptionsTheme({
  113. fontSize,
  114. fontFamily,
  115. lineHeight,
  116. })
  117. )
  118. }, [view, fontSize, fontFamily, lineHeight])
  119. return (
  120. <div className="document-diff-container">
  121. <div ref={containerRef} className="cm-viewer-container" />
  122. {before > 0 ? (
  123. <OLButton
  124. variant="secondary"
  125. leadingIcon={bsVersion({ bs3: 'arrow-up', bs5: 'arrow_upward' })}
  126. onClick={scrollToPrevious}
  127. className="previous-highlight-button"
  128. >
  129. {t('n_more_updates_above', { count: before })}
  130. </OLButton>
  131. ) : null}
  132. {after > 0 ? (
  133. <OLButton
  134. variant="secondary"
  135. leadingIcon={bsVersion({ bs3: 'arrow-down', bs5: 'arrow_downward' })}
  136. onClick={scrollToNext}
  137. className="next-highlight-button"
  138. >
  139. {t('n_more_updates_below', { count: after })}
  140. </OLButton>
  141. ) : null}
  142. </div>
  143. )
  144. }
  145. export default DocumentDiffViewer