document-diff-viewer.tsx 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. import { useCallback, useEffect, useRef, useState } from 'react'
  2. import withErrorBoundary from '../../../../infrastructure/error-boundary'
  3. import { ErrorBoundaryFallback } from '../../../../shared/components/error-boundary-fallback'
  4. import { EditorState, Extension } from '@codemirror/state'
  5. import { EditorView, lineNumbers } from '@codemirror/view'
  6. import { indentationMarkers } from '@replit/codemirror-indentation-markers'
  7. import { highlights, setHighlightsEffect } from '../../extensions/highlights'
  8. import useScopeValue from '../../../../shared/hooks/use-scope-value'
  9. import { theme, Options } from '../../extensions/theme'
  10. import { indentUnit } from '@codemirror/language'
  11. import { Highlight } from '../../services/types/doc'
  12. import useIsMounted from '../../../../shared/hooks/use-is-mounted'
  13. import {
  14. highlightLocations,
  15. highlightLocationsField,
  16. scrollToHighlight,
  17. } from '../../extensions/highlight-locations'
  18. import Icon from '../../../../shared/components/icon'
  19. import { useTranslation } from 'react-i18next'
  20. type FontFamily = 'monaco' | 'lucida'
  21. type LineHeight = 'compact' | 'normal' | 'wide'
  22. type OverallTheme = '' | 'light-'
  23. function extensions(themeOptions: Options): Extension[] {
  24. return [
  25. EditorView.editable.of(false),
  26. lineNumbers(),
  27. EditorView.lineWrapping,
  28. indentUnit.of(' '), // TODO: Vary this by file type
  29. indentationMarkers({ hideFirstIndent: true, highlightActiveBlock: false }),
  30. highlights(),
  31. highlightLocations(),
  32. theme(themeOptions),
  33. ]
  34. }
  35. function DocumentDiffViewer({
  36. doc,
  37. highlights,
  38. }: {
  39. doc: string
  40. highlights: Highlight[]
  41. }) {
  42. const [fontFamily] = useScopeValue<FontFamily>('settings.fontFamily')
  43. const [fontSize] = useScopeValue<number>('settings.fontSize')
  44. const [lineHeight] = useScopeValue<LineHeight>('settings.lineHeight')
  45. const [overallTheme] = useScopeValue<OverallTheme>('settings.overallTheme')
  46. const isMounted = useIsMounted()
  47. const { t } = useTranslation()
  48. const [state, setState] = useState(() => {
  49. return EditorState.create({
  50. doc,
  51. extensions: extensions({
  52. fontFamily,
  53. fontSize,
  54. lineHeight,
  55. overallTheme,
  56. }),
  57. })
  58. })
  59. const view = useRef(
  60. new EditorView({
  61. state,
  62. dispatch: tr => {
  63. view.update([tr])
  64. if (isMounted.current) {
  65. setState(view.state)
  66. }
  67. },
  68. })
  69. ).current
  70. const highlightLocations = state.field(highlightLocationsField)
  71. // Append the editor view DOM to the container node when mounted
  72. const containerRef = useCallback(
  73. node => {
  74. if (node) {
  75. node.appendChild(view.dom)
  76. }
  77. },
  78. [view]
  79. )
  80. const scrollToPrevious = useCallback(() => {
  81. if (highlightLocations.previous) {
  82. scrollToHighlight(view, highlightLocations.previous)
  83. }
  84. }, [highlightLocations.previous, view])
  85. const scrollToNext = useCallback(() => {
  86. if (highlightLocations.next) {
  87. scrollToHighlight(view, highlightLocations.next)
  88. }
  89. }, [highlightLocations.next, view])
  90. const { before, after } = highlightLocations
  91. useEffect(() => {
  92. view.dispatch({
  93. changes: { from: 0, to: view.state.doc.length, insert: doc },
  94. effects: setHighlightsEffect.of(highlights),
  95. })
  96. }, [doc, highlights, view])
  97. return (
  98. <div className="document-diff-container">
  99. <div ref={containerRef} className="cm-viewer-container" />
  100. {before > 0 ? (
  101. <button
  102. className="btn btn-secondary previous-highlight-button"
  103. onClick={scrollToPrevious}
  104. >
  105. <Icon type="arrow-up" />
  106. &nbsp;
  107. {t('n_more_updates_above', { count: before })}
  108. </button>
  109. ) : null}
  110. {after > 0 ? (
  111. <button
  112. className="btn btn-secondary next-highlight-button"
  113. onClick={scrollToNext}
  114. >
  115. <Icon type="arrow-down" />
  116. &nbsp;
  117. {t('n_more_updates_below', { count: after })}
  118. </button>
  119. ) : null}
  120. </div>
  121. )
  122. }
  123. export default withErrorBoundary(DocumentDiffViewer, ErrorBoundaryFallback)