document-diff-viewer.tsx 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  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 Icon from '../../../../shared/components/icon'
  22. import { useTranslation } from 'react-i18next'
  23. import { inlineBackground } from '../../../source-editor/extensions/inline-background'
  24. function extensions(themeOptions: Options): Extension[] {
  25. return [
  26. EditorView.editable.of(false),
  27. EditorView.contentAttributes.of({ tabindex: '0' }),
  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(themeOptions),
  35. inlineBackground(false),
  36. ]
  37. }
  38. function DocumentDiffViewer({
  39. doc,
  40. highlights,
  41. }: {
  42. doc: string
  43. highlights: Highlight[]
  44. }) {
  45. const { userSettings } = useUserSettingsContext()
  46. const { fontFamily, fontSize, lineHeight } = userSettings
  47. const isMounted = useIsMounted()
  48. const { t } = useTranslation()
  49. const [state, setState] = useState(() => {
  50. return EditorState.create({
  51. doc,
  52. extensions: extensions({
  53. fontSize,
  54. fontFamily,
  55. lineHeight,
  56. }),
  57. })
  58. })
  59. const [view] = useState<EditorView>(() => {
  60. return new EditorView({
  61. state,
  62. dispatch: tr => {
  63. view.update([tr])
  64. if (isMounted.current) {
  65. setState(view.state)
  66. }
  67. },
  68. })
  69. })
  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. const effects: StateEffect<unknown>[] = [setHighlightsEffect.of(highlights)]
  93. if (highlights.length > 0) {
  94. const { from, to } = highlights[0].range
  95. effects.push(
  96. EditorView.scrollIntoView(EditorSelection.range(from, to), {
  97. y: 'center',
  98. })
  99. )
  100. }
  101. view.dispatch({
  102. changes: { from: 0, to: view.state.doc.length, insert: doc },
  103. effects,
  104. })
  105. }, [doc, highlights, view])
  106. // Update the document diff viewer theme whenever the font size, font family
  107. // or line height user setting changes
  108. useEffect(() => {
  109. view.dispatch(
  110. setOptionsTheme({
  111. fontSize,
  112. fontFamily,
  113. lineHeight,
  114. })
  115. )
  116. }, [view, fontSize, fontFamily, lineHeight])
  117. return (
  118. <div className="document-diff-container">
  119. <div ref={containerRef} className="cm-viewer-container" />
  120. {before > 0 ? (
  121. <button
  122. className="btn btn-secondary previous-highlight-button"
  123. onClick={scrollToPrevious}
  124. >
  125. <Icon type="arrow-up" />
  126. &nbsp;
  127. {t('n_more_updates_above', { count: before })}
  128. </button>
  129. ) : null}
  130. {after > 0 ? (
  131. <button
  132. className="btn btn-secondary next-highlight-button"
  133. onClick={scrollToNext}
  134. >
  135. <Icon type="arrow-down" />
  136. &nbsp;
  137. {t('n_more_updates_below', { count: after })}
  138. </button>
  139. ) : null}
  140. </div>
  141. )
  142. }
  143. export default DocumentDiffViewer