document-diff-viewer.tsx 3.8 KB

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