Connect up document diff viewer to history state

GitOrigin-RevId: 610a254ea77c194969033d0791ecf1129e02c4bf
This commit is contained in:
Tim Down 2023-03-30 10:16:26 +01:00 committed by Copybot
parent 867b37b76f
commit 11f8905be4
15 changed files with 220 additions and 61 deletions

View file

@ -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

View file

@ -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,
})
}
).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 <div ref={containerRef} style={{ height: '100%' }} />
return <div ref={containerRef} className="document-diff-container" />
}
export default withErrorBoundary(DocumentDiffViewer, ErrorBoundaryFallback)

View file

@ -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

View file

@ -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

View file

@ -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>
)

View file

@ -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[]>()

View file

@ -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)
}

View file

@ -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'
}

View file

@ -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
}

View file

@ -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[]

View file

@ -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 }
}

View file

@ -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',

View file

@ -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;
}
}