document-diff-viewer.tsx 3.7 KB

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