document-diff-viewer.tsx 4.4 KB

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