@@ -14,4 +14,4 @@ function Editor() {
)
}
-export default Editor
+export default DiffView
diff --git a/services/web/frontend/js/features/history/components/editor/document-diff-viewer.tsx b/services/web/frontend/js/features/history/components/diff-view/document-diff-viewer.tsx
similarity index 66%
rename from services/web/frontend/js/features/history/components/editor/document-diff-viewer.tsx
rename to services/web/frontend/js/features/history/components/diff-view/document-diff-viewer.tsx
index 4399a5828f..a49bf5b86c 100644
--- a/services/web/frontend/js/features/history/components/editor/document-diff-viewer.tsx
+++ b/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
('settings.fontFamily')
const [fontSize] = useScopeValue('settings.fontSize')
const [lineHeight] = useScopeValue('settings.lineHeight')
@@ -62,16 +52,13 @@ const DocumentDiffViewer: FC<{ doc: string; highlights: Highlight[] }> = ({
})
})
- const viewRef = useRef(null)
- if (viewRef.current === null) {
- viewRef.current = new EditorView({
+ const view = useRef(
+ new EditorView({
state,
})
- }
+ ).current
- const view = viewRef.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
+ return
}
export default withErrorBoundary(DocumentDiffViewer, ErrorBoundaryFallback)
diff --git a/services/web/frontend/js/features/history/components/diff-view/main.tsx b/services/web/frontend/js/features/history/components/diff-view/main.tsx
new file mode 100644
index 0000000000..52248eae3e
--- /dev/null
+++ b/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()
+ 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 (
+
+
+
+ {t('loading')}…
+
+ )
+ }
+
+ if (!diff) {
+ return No document
+ }
+
+ if (diff.binary) {
+ return Binary file
+ }
+
+ if (diff.docDiff) {
+ const { doc, highlights } = diff.docDiff
+ return
+ }
+
+ return No document
+}
+
+export default Main
diff --git a/services/web/frontend/js/features/history/components/editor/toolbar.tsx b/services/web/frontend/js/features/history/components/diff-view/toolbar.tsx
similarity index 100%
rename from services/web/frontend/js/features/history/components/editor/toolbar.tsx
rename to services/web/frontend/js/features/history/components/diff-view/toolbar.tsx
diff --git a/services/web/frontend/js/features/history/components/editor/main.tsx b/services/web/frontend/js/features/history/components/editor/main.tsx
deleted file mode 100644
index 77adc93c26..0000000000
--- a/services/web/frontend/js/features/history/components/editor/main.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-import { useHistoryContext } from '../../context/history-context'
-
-function Main() {
- const { fileSelection } = useHistoryContext()
-
- return Main (editor). File: {fileSelection?.pathname || 'not set'}
-}
-
-export default Main
diff --git a/services/web/frontend/js/features/history/components/history-root.tsx b/services/web/frontend/js/features/history/components/history-root.tsx
index 02755b7cf7..09484c9dc5 100644
--- a/services/web/frontend/js/features/history/components/history-root.tsx
+++ b/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 (
-
+
)
diff --git a/services/web/frontend/js/features/history/components/extensions/highlights.ts b/services/web/frontend/js/features/history/extensions/highlights.ts
similarity index 97%
rename from services/web/frontend/js/features/history/components/extensions/highlights.ts
rename to services/web/frontend/js/features/history/extensions/highlights.ts
index 00d5a0321b..b4ee5a33e8 100644
--- a/services/web/frontend/js/features/history/components/extensions/highlights.ts
+++ b/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()
diff --git a/services/web/frontend/js/features/history/components/extensions/theme.ts b/services/web/frontend/js/features/history/extensions/theme.ts
similarity index 100%
rename from services/web/frontend/js/features/history/components/extensions/theme.ts
rename to services/web/frontend/js/features/history/extensions/theme.ts
diff --git a/services/web/frontend/js/features/history/services/api.ts b/services/web/frontend/js/features/history/services/api.ts
index 20b66a1c4c..8ad132f617 100644
--- a/services/web/frontend/js/features/history/services/api.ts
+++ b/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 = {
+ from: fromV.toString(),
+ to: toV.toString(),
+ pathname,
+ }
+ const queryParamsSerialized = new URLSearchParams(queryParams).toString()
+ const diffUrl = `/project/${projectId}/diff?${queryParamsSerialized}`
+ return getJSON(diffUrl)
+}
diff --git a/services/web/frontend/js/features/history/services/types/doc.ts b/services/web/frontend/js/features/history/services/types/doc.ts
new file mode 100644
index 0000000000..7fd3c51bc4
--- /dev/null
+++ b/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'
+}
diff --git a/services/web/frontend/js/features/history/services/types/shared.ts b/services/web/frontend/js/features/history/services/types/shared.ts
new file mode 100644
index 0000000000..f16696bf94
--- /dev/null
+++ b/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
+}
diff --git a/services/web/frontend/js/features/history/services/types/update.ts b/services/web/frontend/js/features/history/services/types/update.ts
index 9e82a22b3e..5f6f693b14 100644
--- a/services/web/frontend/js/features/history/services/types/update.ts
+++ b/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[]
diff --git a/services/web/frontend/js/features/history/util/highlights-from-diff-response.ts b/services/web/frontend/js/features/history/util/highlights-from-diff-response.ts
new file mode 100644
index 0000000000..ebfb50b4e6
--- /dev/null
+++ b/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 }
+}
diff --git a/services/web/frontend/stories/history/document-diff-viewer.stories.tsx b/services/web/frontend/stories/history/document-diff-viewer.stories.tsx
index 991b056b1a..18df844d4a 100644
--- a/services/web/frontend/stories/history/document-diff-viewer.stories.tsx
+++ b/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',
diff --git a/services/web/frontend/stylesheets/app/editor/history-react.less b/services/web/frontend/stylesheets/app/editor/history-react.less
index 96eeba4440..c616cd3088 100644
--- a/services/web/frontend/stylesheets/app/editor/history-react.less
+++ b/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;
+ }
}