diff --git a/packages/git/src/Git.ts b/packages/git/src/Git.ts index 2ae3a855..29502c7a 100644 --- a/packages/git/src/Git.ts +++ b/packages/git/src/Git.ts @@ -20,6 +20,7 @@ import type { WatcherCallback, WatcherEvent, } from './types' +import { FileText } from 'src' const PROFILING = true let perfStart = (name: string) => { @@ -561,6 +562,49 @@ export class Git { } } + getFilePlainText = async ( + filePath: string | null, + sha: string | null = null, + ): Promise => { + const spawn = await this._getSpawn() + if (!spawn) { + return null + } + + if (!filePath) { + return { + path: '', + type: '', + text: '', + } + } + + const fileType = path.extname(filePath) + + let plainText = null + if (sha) { + const cmd = ['show', `${sha}:${filePath}`] + plainText = await spawn(cmd) + } else { + const absoluteFilePath = path.join(this.cwd, filePath) + plainText = await new Promise((resolve, reject) => { + fs.readFile(absoluteFilePath, (err, data) => { + if (err) { + reject(err) + } else { + resolve(data.toString()) + } + }) + }) + } + + return { + path: filePath, + text: plainText, + type: fileType, + } + } + getDiffFromShas = async ( shaNew: string, shaOld: string | null = null, diff --git a/packages/git/src/types.ts b/packages/git/src/types.ts index d285c519..f239ccd9 100644 --- a/packages/git/src/types.ts +++ b/packages/git/src/types.ts @@ -65,3 +65,9 @@ export type GitFileOp = | 'X' // Unknown | 'B' // Broken | undefined + +export interface FileText { + path: string + text: string + type: string +} diff --git a/packages/giterm/app/renderer/components/diff/Diff.tsx b/packages/giterm/app/renderer/components/diff/Diff.tsx index d2b76347..429dfa4f 100644 --- a/packages/giterm/app/renderer/components/diff/Diff.tsx +++ b/packages/giterm/app/renderer/components/diff/Diff.tsx @@ -1,62 +1,96 @@ -import React from 'react' -import PropTypes from 'prop-types' +import React, { useCallback, useMemo, useRef } from 'react' import styled from 'styled-components' import { List } from 'app/lib/primitives' -import { DiffEditor } from 'app/lib/monaco' +import { DiffEditor, useMonaco } from 'app/lib/monaco' import type { editor } from 'monaco-editor' -import { FilePatch } from './types' +import { FileText } from './useDiffData' +import { useValueEffect } from 'app/lib/hooks' interface Props { - filePatch: FilePatch + left: FileText + right: FileText } -export function Diff({ filePatch }: Props) { - const isRenamed = - filePatch.oldName !== filePatch.newName && - !!filePatch.oldName && - !!filePatch.newName +export function Diff({ left, right }: Props) { + const monaco = useMonaco() + const editorRef = useRef() + const isRenamed = left.path !== right.path const options: editor.IDiffEditorOptions = { - renderSideBySide: false, - renderOverviewRuler: false, + renderSideBySide: true, + readOnly: true, } + const [leftLang, rightLang] = useMemo(() => { + if (!monaco) { + return ['', ''] + } + + const allLanguages = monaco.languages.getLanguages() + + let leftLang = '' + let rightLang = '' + for (const language of allLanguages) { + if (leftLang && rightLang) { + break + } + + const extensions = language.extensions ?? [] + + if (extensions.includes(left.type)) { + leftLang = language.id + } + if (extensions.includes(right.type)) { + rightLang = language.id + } + } + + return [leftLang, rightLang] + }, [left.type, monaco, right.type]) + + useValueEffect( + left.path, + useCallback(() => { + if (editorRef.current) { + editorRef.current.revealPosition({ column: 0, lineNumber: 0 }, 1) + } + }, []), + ) + return ( {isRenamed ? ( <> - {filePatch.newName} + {left.path || '(Created)'} {'->'} - {filePatch.oldName} + {right.path || '(Deleted)'} ) : ( - {filePatch.oldName ?? filePatch.selectedFileName} + {right.path} )} (editorRef.current = editor)} /> ) } -Diff.propTypes = { - filePatch: PropTypes.object.isRequired, -} - const DiffContainer = styled.div` display: flex; flex-direction: column; diff --git a/packages/giterm/app/renderer/components/diff/DiffPanel.tsx b/packages/giterm/app/renderer/components/diff/DiffPanel.tsx index 8bbe39e2..e7006924 100644 --- a/packages/giterm/app/renderer/components/diff/DiffPanel.tsx +++ b/packages/giterm/app/renderer/components/diff/DiffPanel.tsx @@ -1,5 +1,4 @@ import React from 'react' -import PropTypes from 'prop-types' import styled from 'styled-components' import { Panel } from 'app/lib/primitives' @@ -10,9 +9,9 @@ import { Diff } from './Diff' import { useDiffData } from './useDiffData' export function DiffPanel() { - const { loading, filePath, setFilePath, filePatch, diff } = useDiffData() + const { loading, filePath, setFilePath, diff, left, right } = useDiffData() - if (loading || !diff || !filePatch || !filePath) { + if (loading || !diff || !left || !right) { return ( Loading @@ -26,17 +25,11 @@ export function DiffPanel() { - + ) } -DiffPanel.propTypes = { - mode: PropTypes.oneOf(['shas', 'index']), - shaNew: PropTypes.string, - shaOld: PropTypes.string, -} - const StyledPanel = styled(Panel)` display: flex; flex: 1 1 0; diff --git a/packages/giterm/app/renderer/components/diff/types.ts b/packages/giterm/app/renderer/components/diff/types.ts index a0005514..d64523bd 100644 --- a/packages/giterm/app/renderer/components/diff/types.ts +++ b/packages/giterm/app/renderer/components/diff/types.ts @@ -15,5 +15,7 @@ export type FilePatch = Modify< { selectedFileName: string blocks: FilePatchBlock[] + originalText: string + modifiedText: string } > diff --git a/packages/giterm/app/renderer/components/diff/useDiffData.ts b/packages/giterm/app/renderer/components/diff/useDiffData.ts index 0f5a7289..4f8c5930 100644 --- a/packages/giterm/app/renderer/components/diff/useDiffData.ts +++ b/packages/giterm/app/renderer/components/diff/useDiffData.ts @@ -1,17 +1,18 @@ import { useMemo, useState, useEffect, useCallback } from 'react' import { useSelector, useDispatch } from 'react-redux' -import { Git, DiffResult } from '@giterm/git' +import { Git, DiffResult, DiffFile, FileText } from '@giterm/git' import { diffFileSelected } from 'app/store/diff/actions' -import { DiffLine, FilePatch } from './types' +export type { FileText } export interface DiffData { loading: boolean - filePath: string | null + filePath: string setFilePath: (path: string) => void diff: DiffResult | null - filePatch: FilePatch | null + left: FileText | null + right: FileText | null } export function useDiffData({ contextLines = 5 } = {}): DiffData { @@ -32,6 +33,8 @@ export function useDiffData({ contextLines = 5 } = {}): DiffData { const [loading, setLoading] = useState(true) const [diff, setDiff] = useState(null) + const [left, setLeft] = useState(null) + const [right, setRight] = useState(null) useEffect(() => { let cancelled = false @@ -46,7 +49,10 @@ export function useDiffData({ contextLines = 5 } = {}): DiffData { : await git.getDiffFromIndex({ contextLines }) if (!cancelled) { - diff != null && setDiff(diff) + if (diff) { + setDiff(diff) + } + setLoading(false) } } @@ -58,73 +64,66 @@ export function useDiffData({ contextLines = 5 } = {}): DiffData { } }, [contextLines, cwd, mode, shaNew, shaOld]) - const filePath = useMemo(() => { - if ( - !_filePath || - !diff?.files?.some( + const fileDiff = useMemo(() => { + if (_filePath) { + const fileDiff = diff?.files?.find( (patch) => (patch.newName ?? patch.oldName) === _filePath, ) - ) { - return diff?.files[0]?.newName ?? null + return fileDiff ?? null } else { - return _filePath + return diff?.files[0] ?? null } }, [_filePath, diff?.files]) - const filePatch = useMemo(() => { - if (!diff || !filePath) { - return null - } - const file = diff.files.find( - (file) => file.oldName === filePath || file.newName === filePath, - ) - if (!file) { - return null + useEffect(() => { + if (!fileDiff) { + return } - const filePatch: FilePatch = { - ...file, - selectedFileName: filePath, - blocks: [], - } + let cancelled = false - for (const block of file.blocks) { - const linesLeft: DiffLine[] = [] - const linesRight: DiffLine[] = [] - - for (const line of block.lines) { - const isLeft = typeof line.oldNumber === 'number' && line.oldNumber >= 0 - const isRight = - typeof line.newNumber === 'number' && line.newNumber >= 0 - - // Where line has not changed at-all we fix the row to the same index in both columns - if (isLeft && isRight) { - const headIndex = Math.max(linesLeft.length, linesRight.length) - linesLeft[headIndex] = line - linesRight[headIndex] = line - continue - } + const leftName = fileDiff.oldName + const rightName = fileDiff.newName - // Otherwise push one at a time to keep the diff compact - if (isLeft) { - linesLeft.push(line) - } - if (isRight) { - linesRight.push(line) - } + async function fetch() { + const git = new Git(cwd) + + let left: FileText | null + let right: FileText | null + if (mode === 'shas') { + const shaOldRelative = shaOld ?? `${shaNew}~1` + left = await git.getFilePlainText(leftName, shaOldRelative) + right = await git.getFilePlainText(rightName, shaNew) + } else { + left = await git.getFilePlainText(leftName, 'HEAD') + right = await git.getFilePlainText(rightName) } - filePatch.blocks.push({ ...block, linesLeft, linesRight }) + if (!cancelled) { + if (left && right) { + setLeft(left) + setRight(right) + } + + setLoading(false) + } } - return filePatch - }, [diff, filePath]) + fetch().catch((e) => { + throw e + }) + + return () => { + cancelled = true + } + }, [cwd, fileDiff, mode, shaNew, shaOld]) return { loading, - filePath, + filePath: _filePath, setFilePath, diff, - filePatch, + left, + right, } }