Просмотр исходного кода

Connect up document diff viewer to history state

GitOrigin-RevId: 610a254ea77c194969033d0791ecf1129e02c4bf
Tim Down 3 лет назад
Родитель
Сommit
11f8905be4

+ 2 - 2
services/web/frontend/js/features/history/components/editor/editor.tsx → services/web/frontend/js/features/history/components/diff-view/diff-view.tsx

@@ -1,7 +1,7 @@
 import Toolbar from './toolbar'
 import Main from './main'
 
-function Editor() {
+function DiffView() {
   return (
     <div className="doc-panel">
       <div className="history-header toolbar-container">
@@ -14,4 +14,4 @@ function Editor() {
   )
 }
 
-export default Editor
+export default DiffView

+ 20 - 31
services/web/frontend/js/features/history/components/editor/document-diff-viewer.tsx → services/web/frontend/js/features/history/components/diff-view/document-diff-viewer.tsx

@@ -1,33 +1,20 @@
-import { FC, useCallback, useRef, useState } from 'react'
+import { useCallback, useEffect, useRef, useState } from 'react'
 import withErrorBoundary from '../../../../infrastructure/error-boundary'
 import { ErrorBoundaryFallback } from '../../../../shared/components/error-boundary-fallback'
 import { EditorState, Extension } from '@codemirror/state'
 import { EditorView } from '@codemirror/view'
 import { lineNumbers } from '../../../../../../modules/source-editor/frontend/js/extensions/line-numbers'
 import { indentationMarkers } from '@replit/codemirror-indentation-markers'
-import { highlights, setHighlightsEffect } from '../extensions/highlights'
+import { highlights, setHighlightsEffect } from '../../extensions/highlights'
 import useScopeValue from '../../../../shared/hooks/use-scope-value'
 import {
   FontFamily,
   LineHeight,
   OverallTheme,
 } from '../../../../../../modules/source-editor/frontend/js/extensions/theme'
-import { theme, Options } from '../extensions/theme'
+import { theme, Options } from '../../extensions/theme'
 import { indentUnit } from '@codemirror/language'
-
-interface Range {
-  from: number
-  to: number
-}
-
-type HighlightType = 'addition' | 'deletion'
-
-export interface Highlight {
-  label: string
-  hue: number
-  range: Range
-  type: HighlightType
-}
+import { Highlight } from '../../services/types/doc'
 
 function extensions(themeOptions: Options): Extension[] {
   return [
@@ -41,10 +28,13 @@ function extensions(themeOptions: Options): Extension[] {
   ]
 }
 
-const DocumentDiffViewer: FC<{ doc: string; highlights: Highlight[] }> = ({
+function DocumentDiffViewer({
   doc,
   highlights,
-}) => {
+}: {
+  doc: string
+  highlights: Highlight[]
+}) {
   const [fontFamily] = useScopeValue<FontFamily>('settings.fontFamily')
   const [fontSize] = useScopeValue<number>('settings.fontSize')
   const [lineHeight] = useScopeValue<LineHeight>('settings.lineHeight')
@@ -62,16 +52,13 @@ const DocumentDiffViewer: FC<{ doc: string; highlights: Highlight[] }> = ({
     })
   })
 
-  const viewRef = useRef<EditorView | null>(null)
-  if (viewRef.current === null) {
-    viewRef.current = new EditorView({
+  const view = useRef(
+    new EditorView({
       state,
     })
-  }
-
-  const view = viewRef.current
+  ).current
 
-  // Append the editor view dom to the container node when mounted
+  // Append the editor view DOM to the container node when mounted
   const containerRef = useCallback(
     node => {
       if (node) {
@@ -81,12 +68,14 @@ const DocumentDiffViewer: FC<{ doc: string; highlights: Highlight[] }> = ({
     [view]
   )
 
-  view.dispatch({
-    changes: { from: 0, to: view.state.doc.length, insert: doc },
-    effects: setHighlightsEffect.of(highlights),
-  })
+  useEffect(() => {
+    view.dispatch({
+      changes: { from: 0, to: view.state.doc.length, insert: doc },
+      effects: setHighlightsEffect.of(highlights),
+    })
+  }, [doc, highlights, view])
 
-  return <div ref={containerRef} style={{ height: '100%' }} />
+  return <div ref={containerRef} className="document-diff-container" />
 }
 
 export default withErrorBoundary(DocumentDiffViewer, ErrorBoundaryFallback)

+ 68 - 0
services/web/frontend/js/features/history/components/diff-view/main.tsx

@@ -0,0 +1,68 @@
+import { useHistoryContext } from '../../context/history-context'
+import { diffDoc } from '../../services/api'
+import { useEffect } from 'react'
+import { DocDiffResponse, Highlight } from '../../services/types/doc'
+import { highlightsFromDiffResponse } from '../../util/highlights-from-diff-response'
+import DocumentDiffViewer from './document-diff-viewer'
+import useAsync from '../../../../shared/hooks/use-async'
+import { useTranslation } from 'react-i18next'
+
+type Diff = {
+  binary: boolean
+  docDiff?: {
+    doc: string
+    highlights: Highlight[]
+  }
+}
+
+function Main() {
+  const { t } = useTranslation()
+  const { projectId, updateSelection, fileSelection } = useHistoryContext()
+  const { isLoading, runAsync, data } = useAsync<DocDiffResponse>()
+  let diff: Diff | undefined
+  if (data?.diff) {
+    if ('binary' in data.diff) {
+      diff = { binary: true }
+    } else {
+      diff = { binary: false, docDiff: highlightsFromDiffResponse(data.diff) }
+    }
+  }
+
+  useEffect(() => {
+    if (!updateSelection || !fileSelection || !fileSelection.pathname) {
+      return
+    }
+
+    const { fromV, toV } = updateSelection.update
+
+    // TODO: Error handling
+    runAsync(diffDoc(projectId, fromV, toV, fileSelection.pathname))
+  }, [fileSelection, projectId, runAsync, updateSelection])
+
+  if (isLoading) {
+    return (
+      <div className="history-loading-panel">
+        <i className="fa fa-spin fa-refresh" />
+        &nbsp;&nbsp;
+        {t('loading')}…
+      </div>
+    )
+  }
+
+  if (!diff) {
+    return <div>No document</div>
+  }
+
+  if (diff.binary) {
+    return <div>Binary file</div>
+  }
+
+  if (diff.docDiff) {
+    const { doc, highlights } = diff.docDiff
+    return <DocumentDiffViewer doc={doc} highlights={highlights} />
+  }
+
+  return <div>No document</div>
+}
+
+export default Main

+ 0 - 0
services/web/frontend/js/features/history/components/editor/toolbar.tsx → services/web/frontend/js/features/history/components/diff-view/toolbar.tsx


+ 0 - 9
services/web/frontend/js/features/history/components/editor/main.tsx

@@ -1,9 +0,0 @@
-import { useHistoryContext } from '../../context/history-context'
-
-function Main() {
-  const { fileSelection } = useHistoryContext()
-
-  return <div>Main (editor). File: {fileSelection?.pathname || 'not set'}</div>
-}
-
-export default Main

+ 2 - 2
services/web/frontend/js/features/history/components/history-root.tsx

@@ -1,5 +1,5 @@
 import ChangeList from './change-list/change-list'
-import Editor from './editor/editor'
+import DiffView from './diff-view/diff-view'
 import { useLayoutContext } from '../../../shared/context/layout-context'
 import { useHistoryContext } from '../context/history-context'
 
@@ -13,7 +13,7 @@ export default function HistoryRoot() {
 
   return (
     <div className="history-react">
-      <Editor />
+      <DiffView />
       <ChangeList />
     </div>
   )

+ 1 - 1
services/web/frontend/js/features/history/components/extensions/highlights.ts → services/web/frontend/js/features/history/extensions/highlights.ts

@@ -1,6 +1,6 @@
 import { StateEffect, StateField } from '@codemirror/state'
 import { Decoration, EditorView, hoverTooltip, Tooltip } from '@codemirror/view'
-import { Highlight } from '../editor/document-diff-viewer'
+import { Highlight } from '../services/types/doc'
 
 export const setHighlightsEffect = StateEffect.define<Highlight[]>()
 

+ 0 - 0
services/web/frontend/js/features/history/components/extensions/theme.ts → services/web/frontend/js/features/history/extensions/theme.ts


+ 17 - 0
services/web/frontend/js/features/history/services/api.ts

@@ -1,6 +1,7 @@
 import { getJSON } from '../../../infrastructure/fetch-json'
 import { FileDiff } from './types/file'
 import { Update } from './types/update'
+import { DocDiffResponse } from './types/doc'
 
 const BATCH_SIZE = 10
 
@@ -27,3 +28,19 @@ export function diffFiles(projectId: string, fromV: number, toV: number) {
   const diffUrl = `/project/${projectId}/filetree/diff?${queryParamsSerialized}`
   return getJSON<{ diff: FileDiff[] }>(diffUrl)
 }
+
+export function diffDoc(
+  projectId: string,
+  fromV: number,
+  toV: number,
+  pathname: string
+) {
+  const queryParams: Record<string, string> = {
+    from: fromV.toString(),
+    to: toV.toString(),
+    pathname,
+  }
+  const queryParamsSerialized = new URLSearchParams(queryParams).toString()
+  const diffUrl = `/project/${projectId}/diff?${queryParamsSerialized}`
+  return getJSON<DocDiffResponse>(diffUrl)
+}

+ 26 - 0
services/web/frontend/js/features/history/services/types/doc.ts

@@ -0,0 +1,26 @@
+import { Meta } from './shared'
+
+export interface DocDiffChunk {
+  u?: string
+  i?: string
+  d?: string
+  meta?: Meta
+}
+
+export interface BinaryDiffResponse {
+  binary: true
+}
+
+export type DocDiffResponse = { diff: DocDiffChunk[] | BinaryDiffResponse }
+
+interface Range {
+  from: number
+  to: number
+}
+
+export interface Highlight {
+  label: string
+  hue: number
+  range: Range
+  type: 'addition' | 'deletion'
+}

+ 12 - 0
services/web/frontend/js/features/history/services/types/shared.ts

@@ -0,0 +1,12 @@
+export interface User {
+  first_name: string
+  last_name: string
+  email: string
+  id: string
+}
+
+export interface Meta {
+  users: User[]
+  start_ts: number
+  end_ts: number
+}

+ 2 - 13
services/web/frontend/js/features/history/services/types/update.ts

@@ -1,15 +1,4 @@
-interface User {
-  first_name: string
-  last_name: string
-  email: string
-  id: string
-}
-
-interface UpdateMeta {
-  users: User[]
-  start_ts: number
-  end_ts: number
-}
+import { Meta } from './shared'
 
 interface UpdateLabel {
   id: string
@@ -33,7 +22,7 @@ interface ProjectOp {
 export interface Update {
   fromV: number
   toV: number
-  meta: UpdateMeta
+  meta: Meta
   labels?: Label[]
   pathnames: string[]
   project_ops: ProjectOp[]

+ 51 - 0
services/web/frontend/js/features/history/util/highlights-from-diff-response.ts

@@ -0,0 +1,51 @@
+import displayNameForUser from '../../../ide/history/util/displayNameForUser'
+import moment from 'moment/moment'
+import ColorManager from '../../../ide/colors/ColorManager'
+import { DocDiffChunk, Highlight } from '../services/types/doc'
+
+export function highlightsFromDiffResponse(chunks: DocDiffChunk[]) {
+  let pos = 0
+  const highlights: Highlight[] = []
+  let doc = ''
+
+  for (const entry of chunks) {
+    const content = entry.u || entry.i || entry.d || ''
+    doc += content
+    const from = pos
+    const to = doc.length
+    pos = to
+    const range = { from, to }
+
+    const isInsertion = typeof entry.i === 'string'
+    const isDeletion = typeof entry.d === 'string'
+
+    if (isInsertion || isDeletion) {
+      const meta = entry.meta
+      if (!meta) {
+        throw new Error('No meta found')
+      }
+      const user = meta.users?.[0]
+      const name = displayNameForUser(user)
+      const date = moment(meta.end_ts).format('Do MMM YYYY, h:mm a')
+      if (isInsertion) {
+        highlights.push({
+          type: 'addition',
+          // There doesn't seem to be a convenient way to make this translatable
+          label: `Added by ${name} on ${date}`,
+          range,
+          hue: ColorManager.getHueForUserId(user.id),
+        })
+      } else if (isDeletion) {
+        highlights.push({
+          type: 'deletion',
+          // There doesn't seem to be a convenient way to make this translatable
+          label: `Deleted by ${name} on ${date}`,
+          range,
+          hue: ColorManager.getHueForUserId(user.id),
+        })
+      }
+    }
+  }
+
+  return { doc, highlights }
+}

+ 2 - 3
services/web/frontend/stories/history/document-diff-viewer.stories.tsx

@@ -1,8 +1,7 @@
 import { ScopeDecorator } from '../decorators/scope'
-import DocumentDiffViewer, {
-  Highlight,
-} from '../../js/features/history/components/editor/document-diff-viewer'
+import DocumentDiffViewer from '../../js/features/history/components/diff-view/document-diff-viewer'
 import React from 'react'
+import { Highlight } from '../../js/features/history/services/types/doc'
 
 export default {
   title: 'History / Document Diff Viewer',

+ 17 - 0
services/web/frontend/stylesheets/app/editor/history-react.less

@@ -34,6 +34,17 @@ history-root {
 
     .doc-container {
       flex: 1;
+      overflow-y: auto;
+
+      .document-diff-container {
+        height: 100%;
+        display: flex;
+        flex-direction: column;
+
+        .cm-editor {
+          height: 100%;
+        }
+      }
     }
   }
 
@@ -54,4 +65,10 @@ history-root {
       display: block;
     }
   }
+
+  .history-loading-panel {
+    padding-top: 10rem;
+    font-family: @font-family-serif;
+    text-align: center;
+  }
 }