Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,10 @@ Linux CI 的桌面测试使用 `xvfb-run -a`。测试中的模拟更新适配器

空文件显示“新增空文件”,二进制文件和无法读取的文件有明确反馈。预览支持 UTF-8 文本,新增文件/暂存内容及差异输出限制为 1 MiB,差异补丁最多 10,000 行;超限时显示提示。预览不写入文件或暂存区。验证范围和结果见 [新增文件差异预览验收](https://github.com/ShaoClean/remote-git/wiki/Issue-18-Validation)。

工作区侧栏与改动列表支持拖动调宽,也可从“布局设置”使用滑块调整或恢复默认。分隔线支持方向键、Shift 加速和 Home / End;`Cmd/Ctrl + \` 开关侧栏。布局偏好在本机保存,保留现有工作区树的展开与排序设置。
工作区采用左侧仓库导航、中央 Diff、右侧改动与提交布局。顶栏按钮可分别隐藏或重新打开两侧面板,Diff 工具栏的“专注阅读差异”可一次隐藏两侧。左下角的应用菜单集中提供设置与帮助;左栏隐藏时,菜单移至顶栏。

两侧面板支持拖动调宽,也可在“设置 → 布局”调整显隐、宽度、默认 Diff 模式或恢复默认。分隔线支持方向键、Shift 加速和 Home / End;`Cmd/Ctrl+B` 切换左侧,`Cmd/Ctrl+Shift+B` 切换右侧,`Cmd/Ctrl+,` 打开设置,原有 `Cmd/Ctrl+\` 仍可切换左侧。布局偏好在本机保存,恢复默认不会清除工作区树的展开与排序设置。分栏 Diff 的长行自动折行。

窄窗口选择文件或提交后进入检查器,使用“返回列表”继续操作。提交摘要和描述在本次页面会话内按仓库保留;刷新页面会清空草稿。

界面截图、验证结果和测试环境说明见 [工作区布局验收](https://github.com/ShaoClean/remote-git/wiki/Issue-6-Validation)。
本次重构截图与验证结果见 [Issue #31 验收](https://github.com/ShaoClean/remote-git/wiki/Issue-31-Validation)。既有工作区布局记录见 [工作区布局验收](https://github.com/ShaoClean/remote-git/wiki/Issue-6-Validation)。
2 changes: 1 addition & 1 deletion apps/desktop/src/ai-smoke.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ module.exports = async ({ backend, origin, token, window, restore = false }) =>
}; check();
})`);
try {
await execute(`document.querySelector('[aria-label="设置"]').click()`);
await execute(`window.dispatchEvent(new KeyboardEvent('keydown', { key: ',', ctrlKey: true }))`);
window.setSize(390, 844);
await wait(
`document.querySelector('select[aria-label="设置分类"]')?.getClientRects().length && document.querySelector('.provider-list__item')`,
Expand Down
15 changes: 8 additions & 7 deletions apps/desktop/src/diff-smoke.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -92,27 +92,28 @@ module.exports = async ({ window, origin, token, backend }) => {
await waitFor("document.querySelector('.file-row__select')");
await preview(file);
await waitFor(
"document.querySelector('.diff-line--add')?.textContent.includes('desktop first')",
"document.querySelector('.diff-code-row--add')?.textContent.includes('desktop first')",
);
assert.equal(await execute("document.querySelectorAll('.diff-line--remove').length"), 0);
assert.equal(await execute("document.querySelectorAll('.diff-code-row--remove').length"), 0);
await execute(
"Array.from(document.querySelectorAll('.ant-segmented-item')).find(item => item.textContent === '分栏').click()",
);
await waitFor(
"document.querySelector('.diff-split-cell--add')?.textContent.includes('desktop first')",
);
assert.equal(await execute("document.querySelectorAll('.diff-split-cell--remove').length"), 0);
await click('[aria-label="放大查看差异"]');
await click('[aria-label="专注阅读差异"]');
await waitFor(
"document.querySelector('.diff-zoom-modal .diff-split-cell--add')?.textContent.includes('desktop first')",
"document.querySelector('.app-shell--collapsed') && document.querySelector('.workspace-body--right-hidden') && document.querySelector('.diff-split-cell--add')?.textContent.includes('desktop first')",
);
if (process.env.REMOTE_GIT_DIFF_SMOKE_SCREENSHOT) {
require('node:fs').writeFileSync(
process.env.REMOTE_GIT_DIFF_SMOKE_SCREENSHOT,
(await window.webContents.capturePage()).toPNG(),
);
}
await click('.diff-zoom-modal .ant-modal-close');
await click('[aria-label="显示左侧工作区"]');
await click('[aria-label="显示右侧面板"]');
await click(`button[aria-label="暂存 ${file}"]`);
await waitFor("document.querySelector('.diff-shell__title')?.textContent.includes('已暂存')");
edited = true;
Expand All @@ -131,7 +132,7 @@ module.exports = async ({ window, origin, token, backend }) => {
"document.querySelector('.diff-split-cell--add')?.textContent.includes('desktop second')",
);
assert.equal(
await execute("document.querySelector('.diff-split-cell--remove')?.textContent"),
await execute("document.querySelector('.diff-split-cell--remove code')?.textContent"),
'desktop first',
);
await click(`button[aria-label="取消暂存 ${file}"]`);
Expand Down Expand Up @@ -172,7 +173,7 @@ module.exports = async ({ window, origin, token, backend }) => {
await click('[aria-label="关闭差异"]');
await waitFor("!document.querySelector('.diff-shell')");
console.log(
'Desktop new-file diff passed: unified, split, zoom, staging, editing, unstaging, feedback, late responses and closing.',
'Desktop new-file diff passed: unified, split, focus, staging, editing, unstaging, feedback, late responses and closing.',
);
} finally {
connections.ensureConnected = originalConnect;
Expand Down
20 changes: 15 additions & 5 deletions apps/desktop/src/sidebar-smoke.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ module.exports = async ({ window, origin, token, restore }) => {
sidebarWidth: 310,
changesWidth: 430,
sidebarCollapsed: true,
changesCollapsed: false,
diffMode: 'unified',
});
await waitFor("document.querySelector('.app-shell--collapsed') !== null");
}
Expand Down Expand Up @@ -144,24 +146,32 @@ module.exports = async ({ window, origin, token, restore }) => {
await execute(
`document.querySelector('[aria-label="调整工作区宽度"]').dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowLeft', bubbles: true }))`,
);
await execute(`document.querySelector('[aria-label="布局设置"]').click()`);
await waitFor('document.querySelector(\'input[aria-label="改动列表宽度"]\') !== null');
await execute('document.querySelector(\'[aria-label="设置与帮助"]\').click()');
await waitFor('document.querySelector(\'[role="menu"]\') !== null');
await execute('document.querySelector(\'[role="menuitem"]\').click()');
await waitFor("document.querySelector('.settings-navigation') !== null");
await execute(
"Array.from(document.querySelectorAll('.settings-navigation button')).find(button => button.textContent === '布局').click()",
);
await waitFor('document.querySelector(\'input[aria-label="右侧面板宽度"]\') !== null');
await execute(`(() => {
const slider = document.querySelector('input[aria-label="改动列表宽度"]');
const slider = document.querySelector('input[aria-label="右侧面板宽度"]');
Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set.call(slider, '430');
slider.dispatchEvent(new Event('input', { bubbles: true }));
})()`);
await waitFor("document.querySelector('input[aria-label=\"改动列表宽度\"]').value === '430'");
await waitFor("document.querySelector('input[aria-label=\"右侧面板宽度\"]').value === '430'");
await execute(
`Array.from(document.querySelectorAll('.settings-header button')).find(button => button.textContent.replaceAll(' ', '') === '返回工作区').click()`,
);
await execute(`document.querySelector('[aria-label="收起导航"]').click()`);
await execute(`document.querySelector('[aria-label="隐藏左侧工作区"]').click()`);
await waitFor("document.querySelector('.app-shell--collapsed') !== null");
const withLayout = JSON.parse(await execute('window.remoteGitWorkspace.load()')).state;
assert.deepEqual(withLayout.layout, {
sidebarWidth: 310,
changesWidth: 430,
sidebarCollapsed: true,
changesCollapsed: false,
diffMode: 'unified',
});
assert.deepEqual(withLayout.repositoryOrderByConnection[a], expectedOrder);
console.log('Desktop layout UI saved widths and collapse state without altering tree order.');
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src/smoke.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ module.exports = async ({ window, origin, token, updates, closeBackend, backend,
};
check();
})`);
await waitForUI(window, `document.querySelector('.sidebar-footer__version')?.textContent === ${JSON.stringify(`v${version}`)}`);
await waitForUI(window, `document.querySelector('.workspace-menu-version')?.textContent === ${JSON.stringify(`v${version}`)}`);
const renderer = await window.webContents.executeJavaScript(`(async () => ({
node: typeof process,
require: typeof require,
Expand Down Expand Up @@ -172,7 +172,7 @@ module.exports = async ({ window, origin, token, updates, closeBackend, backend,
async function openUpdateSettings(window) {
const current = await window.webContents.executeJavaScript("location.pathname");
if (current !== '/settings/updates') {
await window.webContents.executeJavaScript(`document.querySelector('[aria-label="设置"]').click()`);
await window.webContents.executeJavaScript(`window.dispatchEvent(new KeyboardEvent('keydown', { key: ',', ctrlKey: true }))`);
await waitForUI(window, `document.querySelector('.settings-navigation')`);
await window.webContents.executeJavaScript(`Array.from(document.querySelectorAll('.settings-navigation button')).find(button => button.textContent === '版本更新').click()`);
}
Expand Down
117 changes: 85 additions & 32 deletions apps/web/src/components/DiffViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@ import { useEffect, useRef, useState } from 'react';
import { Button, Modal, Segmented } from 'antd';
import { CloseOutlined, DiffOutlined, ExpandOutlined } from '@ant-design/icons';
import ReactDiffViewer, { DiffMethod } from 'react-diff-viewer-continued';
import { getDiffLines, getDiffNotice } from './diff-lines';
import { getNumberedDiffLines, getDiffNotice } from './diff-lines';
import type { NumberedDiffLine } from './diff-lines';
import { useWorkspaceStore } from '../stores/workspaceStore';

interface Props {
oldCode?: string;
newCode?: string;
diff?: string;
title?: string;
subtitle?: string;
onFocus?: () => void;
splitView?: boolean;
onClose?: () => void;
loading?: boolean;
Expand All @@ -23,10 +27,12 @@ interface SplitDiffRow {
right?: string;
leftKind?: SplitCellKind;
rightKind?: SplitCellKind;
oldLine?: number;
newLine?: number;
}

function getSplitDiffRows(diff: string): SplitDiffRow[] {
const lines = getDiffLines(diff);
const lines = getNumberedDiffLines(diff);
const rows: SplitDiffRow[] = [];

for (let index = 0; index < lines.length; ) {
Expand All @@ -38,21 +44,23 @@ function getSplitDiffRows(diff: string): SplitDiffRow[] {
}

if (line.kind === 'remove') {
const removed: string[] = [];
const removed: NumberedDiffLine[] = [];
while (index < lines.length && lines[index].kind === 'remove') {
removed.push(lines[index].text.slice(1));
removed.push(lines[index]);
index += 1;
}
const added: string[] = [];
const added: NumberedDiffLine[] = [];
while (index < lines.length && lines[index].kind === 'add') {
added.push(lines[index].text.slice(1));
added.push(lines[index]);
index += 1;
}
const rowCount = Math.max(removed.length, added.length);
for (let row = 0; row < rowCount; row += 1) {
rows.push({
left: removed[row] || '',
right: added[row] || '',
left: removed[row]?.text.slice(1) || '',
right: added[row]?.text.slice(1) || '',
oldLine: removed[row]?.oldLine,
newLine: added[row]?.newLine,
leftKind: removed[row] === undefined ? 'empty' : 'remove',
rightKind: added[row] === undefined ? 'empty' : 'add',
});
Expand All @@ -61,19 +69,32 @@ function getSplitDiffRows(diff: string): SplitDiffRow[] {
}

if (line.kind === 'add') {
const added: string[] = [];
const added: NumberedDiffLine[] = [];
while (index < lines.length && lines[index].kind === 'add') {
added.push(lines[index].text.slice(1));
added.push(lines[index]);
index += 1;
}
added.forEach((value) =>
rows.push({ left: '', right: value, leftKind: 'empty', rightKind: 'add' }),
rows.push({
left: '',
right: value.text.slice(1),
newLine: value.newLine,
leftKind: 'empty',
rightKind: 'add',
}),
);
continue;
}

const context = line.text.slice(1);
rows.push({ left: context, right: context, leftKind: 'context', rightKind: 'context' });
rows.push({
left: context,
right: context,
oldLine: line.oldLine,
newLine: line.newLine,
leftKind: 'context',
rightKind: 'context',
});
index += 1;
}

Expand All @@ -85,12 +106,20 @@ export function DiffViewer({
newCode = '',
diff,
title,
splitView = false,
subtitle,
onFocus,
splitView,
onClose,
loading = false,
error,
}: Props) {
const [mode, setMode] = useState<'unified' | 'split'>(splitView ? 'split' : 'unified');
const preferredMode = useWorkspaceStore((state) => state.layout.diffMode);
const [mode, setMode] = useState<'unified' | 'split'>(
splitView === undefined ? preferredMode : splitView ? 'split' : 'unified',
);
useEffect(() => {
setMode(splitView === undefined ? preferredMode : splitView ? 'split' : 'unified');
}, [preferredMode, splitView]);
const [zoomed, setZoomed] = useState(false);
const bodyRef = useRef<HTMLDivElement>(null);
const zoomBodyRef = useRef<HTMLDivElement>(null);
Expand All @@ -102,33 +131,56 @@ export function DiffViewer({
}, [diff, error, loading, mode, title, zoomed]);

const renderUnifiedDiff = (value: string) => {
const lines = getDiffLines(value);
const lines = getNumberedDiffLines(value);
return (
<pre>
{lines.map(({ text: line, kind }, index) => {
const className = kind === 'context' ? undefined : `diff-line--${kind}`;
<div className="diff-unified-view" aria-label="统一差异">
{lines.map(({ text: line, kind, oldLine, newLine }, index) => {
const className = `diff-code-row diff-code-row--${kind}`;
return (
<span className={className} key={`${index}-${line}`}>
{line}
{index < lines.length - 1 ? '\n' : ''}
</span>
<div className={className} key={index}>
{kind !== 'meta' && (
<>
<span className="diff-line-number" aria-hidden="true">
{oldLine}
</span>
<span className="diff-line-number" aria-hidden="true">
{newLine}
</span>
</>
)}
<code>{line}</code>
</div>
);
})}
</pre>
</div>
);
};

const renderSplitDiff = (value: string) => (
<div className="diff-split-view" aria-label="分栏差异">
<div className="diff-split-labels">
<span>原版本</span>
<span>修改后</span>
</div>
{getSplitDiffRows(value).map((row, index) =>
row.meta !== undefined ? (
<div className="diff-split-row diff-split-row--meta" key={`${index}-${row.meta}`}>
<span>{row.meta}</span>
</div>
) : (
<div className="diff-split-row" key={`${index}-${row.left}-${row.right}`}>
<span className={`diff-split-cell diff-split-cell--${row.leftKind}`}>{row.left}</span>
<span className={`diff-split-cell diff-split-cell--${row.rightKind}`}>{row.right}</span>
<span className={`diff-split-cell diff-split-cell--${row.leftKind}`}>
<span className="diff-line-number" aria-hidden="true">
{row.oldLine}
</span>
<code>{row.left}</code>
</span>
<span className={`diff-split-cell diff-split-cell--${row.rightKind}`}>
<span className="diff-line-number" aria-hidden="true">
{row.newLine}
</span>
<code>{row.right}</code>
</span>
</div>
),
)}
Expand Down Expand Up @@ -175,7 +227,10 @@ export function DiffViewer({
<div className="diff-shell__header">
<div className="diff-shell__title" title={title || '差异预览'}>
<DiffOutlined />
<span>{title || '差异预览'}</span>
<div className="diff-shell__filename">
<span>{title || '差异预览'}</span>
{subtitle && <small>{subtitle}</small>}
</div>
</div>
<div className="diff-toolbar">
<span className="diff-mode">视图</span>
Expand All @@ -192,13 +247,11 @@ export function DiffViewer({
type="text"
size="small"
icon={<ExpandOutlined />}
aria-label="放大查看差异"
title="放大查看差异"
aria-label={onFocus ? '专注阅读差异' : '放大查看差异'}
title={onFocus ? '专注阅读差异 · 隐藏两侧面板' : '放大查看差异'}
disabled={loading || Boolean(error) || !hasDiff}
onClick={() => setZoomed(true)}
>
放大
</Button>
onClick={() => (onFocus ? onFocus() : setZoomed(true))}
/>
{onClose && (
<Button
type="text"
Expand Down
Loading
Loading