Trim conflict component render tests (#2313)
* test: trim conflict component render tests * Show unresolved conflicts inline in review panel - Replace the conflict review overview list with read-only inline Monaco views - Add auto-height editor rendering so each conflict file fits its contents - Preload unresolved conflict files for the all-conflicts review view
This commit is contained in:
parent
65658fcba6
commit
6805f20696
|
|
@ -1,172 +1,5 @@
|
|||
import { renderToStaticMarkup } from 'react-dom/server'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import type { OpenFile } from '@/store/slices/editor'
|
||||
import type { GitStatusEntry } from '../../../../shared/types'
|
||||
import { TooltipProvider } from '@/components/ui/tooltip'
|
||||
import {
|
||||
ConflictBanner,
|
||||
ConflictReviewPanel,
|
||||
getNextConflictNavigationIndex
|
||||
} from './ConflictComponents'
|
||||
|
||||
function createConflictReviewFile(overrides: Partial<OpenFile> = {}): OpenFile {
|
||||
return {
|
||||
id: 'repo::/repo::conflict-review',
|
||||
filePath: '/repo',
|
||||
relativePath: 'Conflict Review',
|
||||
worktreeId: 'repo::/repo',
|
||||
language: 'text',
|
||||
isDirty: false,
|
||||
mode: 'conflict-review',
|
||||
conflictReview: {
|
||||
source: 'live-summary',
|
||||
snapshotTimestamp: Date.UTC(2026, 4, 17, 19, 9, 7),
|
||||
entries: [
|
||||
{
|
||||
path: 'src/renderer/src/store/slices/linear.test.ts',
|
||||
conflictKind: 'both_added'
|
||||
},
|
||||
{
|
||||
path: 'src/renderer/src/store/slices/linear.ts',
|
||||
conflictKind: 'both_modified'
|
||||
}
|
||||
]
|
||||
},
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
function createLiveEntry(
|
||||
path: string,
|
||||
status: GitStatusEntry['status'] = 'modified'
|
||||
): GitStatusEntry {
|
||||
return {
|
||||
path,
|
||||
status,
|
||||
area: 'unstaged',
|
||||
conflictKind: path.endsWith('.test.ts') ? 'both_added' : 'both_modified',
|
||||
conflictStatus: 'unresolved',
|
||||
conflictStatusSource: 'git'
|
||||
}
|
||||
}
|
||||
|
||||
describe('ConflictReviewPanel', () => {
|
||||
it('renders unresolved conflicts as a left tree with supplied default content', () => {
|
||||
const file = createConflictReviewFile()
|
||||
const html = renderToStaticMarkup(
|
||||
<ConflictReviewPanel
|
||||
file={file}
|
||||
liveEntries={[
|
||||
createLiveEntry('src/renderer/src/store/slices/linear.test.ts', 'added'),
|
||||
createLiveEntry('src/renderer/src/store/slices/linear.ts')
|
||||
]}
|
||||
onOpenEntry={vi.fn()}
|
||||
selectedFile={null}
|
||||
selectedContent={<div data-conflict-review-default-content="true">file contents</div>}
|
||||
onDismiss={vi.fn()}
|
||||
onRefreshSnapshot={vi.fn()}
|
||||
onReturnToSourceControl={vi.fn()}
|
||||
/>
|
||||
)
|
||||
|
||||
expect(html).toContain('Files')
|
||||
expect(html).toContain('Collapse file tree')
|
||||
expect(html).toContain('src/renderer/src/store/slices')
|
||||
expect(html).toContain('linear.test.ts')
|
||||
expect(html).toContain('linear.ts')
|
||||
expect(html).toContain('data-conflict-review-default-content="true"')
|
||||
expect(html).toContain('file contents')
|
||||
expect(html).not.toContain('Select a conflict from the file tree')
|
||||
})
|
||||
|
||||
it('renders a lightweight overview when no conflict file is selected', () => {
|
||||
const file = createConflictReviewFile()
|
||||
const html = renderToStaticMarkup(
|
||||
<ConflictReviewPanel
|
||||
file={file}
|
||||
liveEntries={[
|
||||
createLiveEntry('src/renderer/src/store/slices/linear.test.ts', 'added'),
|
||||
createLiveEntry('src/renderer/src/store/slices/linear.ts')
|
||||
]}
|
||||
onOpenEntry={vi.fn()}
|
||||
selectedFile={null}
|
||||
selectedContent={null}
|
||||
onDismiss={vi.fn()}
|
||||
onRefreshSnapshot={vi.fn()}
|
||||
onReturnToSourceControl={vi.fn()}
|
||||
/>
|
||||
)
|
||||
|
||||
expect(html).toContain('Conflicts')
|
||||
expect(html).toContain('Open a file to resolve markers in the editor.')
|
||||
expect(html).toContain('Choose which version to keep, or combine them')
|
||||
expect(html).toContain('Resolve the conflict markers')
|
||||
expect(html).not.toContain('Loading conflict contents')
|
||||
})
|
||||
|
||||
it('keeps selected conflict content in a flex-height pane', () => {
|
||||
const file = createConflictReviewFile()
|
||||
const html = renderToStaticMarkup(
|
||||
<ConflictReviewPanel
|
||||
file={file}
|
||||
liveEntries={[
|
||||
createLiveEntry('src/renderer/src/store/slices/linear.test.ts', 'added'),
|
||||
createLiveEntry('src/renderer/src/store/slices/linear.ts')
|
||||
]}
|
||||
onOpenEntry={vi.fn()}
|
||||
selectedFile={{
|
||||
id: '/repo/src/renderer/src/store/slices/linear.ts',
|
||||
filePath: '/repo/src/renderer/src/store/slices/linear.ts',
|
||||
relativePath: 'src/renderer/src/store/slices/linear.ts',
|
||||
worktreeId: 'repo::/repo',
|
||||
language: 'typescript',
|
||||
isDirty: false,
|
||||
mode: 'edit'
|
||||
}}
|
||||
selectedContent={<div data-selected-conflict-content="true" className="flex-1" />}
|
||||
onDismiss={vi.fn()}
|
||||
onRefreshSnapshot={vi.fn()}
|
||||
onReturnToSourceControl={vi.fn()}
|
||||
/>
|
||||
)
|
||||
|
||||
expect(html).toContain(
|
||||
'class="flex min-h-0 flex-1 flex-col"><div data-selected-conflict-content="true"'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('ConflictBanner', () => {
|
||||
it('renders conflict navigation controls when conflict markers are available', () => {
|
||||
const file = createConflictReviewFile({
|
||||
mode: 'edit',
|
||||
conflict: {
|
||||
kind: 'conflict-editable',
|
||||
conflictKind: 'both_modified',
|
||||
conflictStatus: 'unresolved',
|
||||
conflictStatusSource: 'git'
|
||||
}
|
||||
})
|
||||
const html = renderToStaticMarkup(
|
||||
<TooltipProvider>
|
||||
<ConflictBanner
|
||||
file={file}
|
||||
entry={null}
|
||||
conflictNavigation={{
|
||||
currentIndex: 1,
|
||||
total: 3,
|
||||
onJump: vi.fn()
|
||||
}}
|
||||
/>
|
||||
</TooltipProvider>
|
||||
)
|
||||
|
||||
expect(html).toContain('Unresolved conflict')
|
||||
expect(html).toContain('2 / 3')
|
||||
expect(html).toContain('Previous conflict')
|
||||
expect(html).toContain('Next conflict')
|
||||
})
|
||||
})
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { getNextConflictNavigationIndex } from './ConflictComponents'
|
||||
|
||||
describe('getNextConflictNavigationIndex', () => {
|
||||
it('cycles through conflicts in both directions', () => {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import {
|
|||
} from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { getFileTypeIcon } from '@/lib/file-type-icons'
|
||||
import { cn } from '@/lib/utils'
|
||||
import type { ConflictReviewEntry, OpenFile } from '@/store/slices/editor'
|
||||
import type { GitConflictKind, GitStatusEntry } from '../../../../shared/types'
|
||||
|
|
@ -185,105 +184,6 @@ export function ConflictPlaceholderView({ file }: { file: OpenFile }): React.JSX
|
|||
)
|
||||
}
|
||||
|
||||
function ConflictReviewOverview({
|
||||
entries,
|
||||
resolvedCount,
|
||||
onOpenEntry,
|
||||
onReturnToSourceControl,
|
||||
onDismiss
|
||||
}: {
|
||||
entries: readonly ConflictReviewPanelEntry[]
|
||||
resolvedCount: number
|
||||
onOpenEntry: (entry: GitStatusEntry) => void
|
||||
onReturnToSourceControl: () => void
|
||||
onDismiss: () => void
|
||||
}): React.JSX.Element {
|
||||
return (
|
||||
<div className="min-h-0 flex-1 overflow-y-auto scrollbar-sleek">
|
||||
<div className="mx-auto flex w-full max-w-4xl flex-col gap-4 px-5 py-4">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<div className="text-sm font-medium text-foreground">Conflicts</div>
|
||||
<div className="mt-1 text-xs text-muted-foreground">
|
||||
Open a file to resolve markers in the editor.
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
<Button type="button" size="sm" variant="outline" onClick={onReturnToSourceControl}>
|
||||
<GitMerge className="size-3.5" />
|
||||
Source Control
|
||||
</Button>
|
||||
<Button type="button" size="sm" variant="ghost" onClick={onDismiss}>
|
||||
<X className="size-3.5" />
|
||||
Dismiss
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{resolvedCount > 0 && (
|
||||
<div className="rounded-md border border-border bg-muted/30 px-3 py-2 text-xs text-muted-foreground">
|
||||
{resolvedCount} conflict{resolvedCount === 1 ? '' : 's'} no longer live in Git.
|
||||
</div>
|
||||
)}
|
||||
<div className="overflow-hidden rounded-md border border-border">
|
||||
{entries.map((entry) => {
|
||||
const FileIcon = getFileTypeIcon(entry.path)
|
||||
const liveEntry = entry.liveEntry
|
||||
const isUnresolved = liveEntry?.conflictStatus === 'unresolved'
|
||||
const statusLabel = isUnresolved ? 'Unresolved' : liveEntry ? 'Resolved' : 'Gone'
|
||||
|
||||
return (
|
||||
<button
|
||||
key={entry.path}
|
||||
type="button"
|
||||
className={cn(
|
||||
'group flex w-full min-w-0 items-start gap-3 border-b border-border px-3 py-3 text-left transition-colors last:border-b-0 hover:bg-accent/40 disabled:cursor-default disabled:hover:bg-transparent',
|
||||
!liveEntry && 'opacity-65'
|
||||
)}
|
||||
disabled={!liveEntry}
|
||||
onClick={() => {
|
||||
if (liveEntry) {
|
||||
onOpenEntry(liveEntry)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FileIcon
|
||||
className={cn('mt-0.5 size-4 shrink-0', isUnresolved && 'text-destructive')}
|
||||
/>
|
||||
<div className="min-w-0 flex-1 space-y-1">
|
||||
<div className="flex min-w-0 flex-wrap items-center gap-2">
|
||||
<span className="min-w-0 break-all font-mono text-xs text-foreground">
|
||||
{entry.path}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
'shrink-0 rounded-full px-1.5 py-0.5 text-[10px] font-semibold',
|
||||
isUnresolved
|
||||
? 'bg-destructive/12 text-destructive'
|
||||
: 'bg-muted text-muted-foreground'
|
||||
)}
|
||||
>
|
||||
{statusLabel}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{CONFLICT_KIND_LABELS[entry.conflictKind]} ·{' '}
|
||||
{CONFLICT_HINT_MAP[entry.conflictKind]}
|
||||
</div>
|
||||
{liveEntry?.oldPath && (
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Renamed from {liveEntry.oldPath}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function ConflictReviewPanel({
|
||||
file,
|
||||
liveEntries,
|
||||
|
|
@ -323,7 +223,6 @@ export function ConflictReviewPanel({
|
|||
(entry) => entry.liveEntry?.conflictStatus === 'unresolved'
|
||||
)
|
||||
const unresolvedCount = unresolvedSnapshotEntries.length
|
||||
const resolvedCount = Math.max(0, snapshotEntries.length - unresolvedCount)
|
||||
const snapshotTime = new Date(
|
||||
file.conflictReview?.snapshotTimestamp ?? Date.now()
|
||||
).toLocaleTimeString()
|
||||
|
|
@ -401,13 +300,9 @@ export function ConflictReviewPanel({
|
|||
</div>
|
||||
<div className="flex min-h-0 flex-1 flex-col">
|
||||
{selectedContent ?? (
|
||||
<ConflictReviewOverview
|
||||
entries={treeEntries}
|
||||
resolvedCount={resolvedCount}
|
||||
onOpenEntry={onOpenEntry}
|
||||
onReturnToSourceControl={onReturnToSourceControl}
|
||||
onDismiss={onDismiss}
|
||||
/>
|
||||
<div className="flex h-full min-h-0 items-center justify-center px-6 text-center text-sm text-muted-foreground">
|
||||
Loading conflict contents...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ already live in their own modules. */
|
|||
import React, { lazy } from 'react'
|
||||
import { AlertCircle, RefreshCw } from 'lucide-react'
|
||||
import { detectLanguage } from '@/lib/language-detect'
|
||||
import { joinPath } from '@/lib/path'
|
||||
import { useAppStore } from '@/store'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { ChangesModeView } from './ChangesModeView'
|
||||
|
|
@ -232,6 +233,40 @@ export function EditorContent({
|
|||
]
|
||||
)
|
||||
|
||||
const createConflictReviewContentFile = (entry: GitStatusEntry): OpenFile => {
|
||||
const absolutePath = joinPath(activeFile.filePath, entry.path)
|
||||
const conflict =
|
||||
entry.conflictKind && entry.conflictStatus && entry.conflictStatusSource
|
||||
? entry.status === 'deleted'
|
||||
? {
|
||||
kind: 'conflict-placeholder' as const,
|
||||
conflictKind: entry.conflictKind,
|
||||
conflictStatus: entry.conflictStatus,
|
||||
conflictStatusSource: entry.conflictStatusSource,
|
||||
message:
|
||||
'This file is in a conflict state, but no working-tree file is available to edit.',
|
||||
guidance: 'Resolve the conflict in Git or restore one side before reopening it.'
|
||||
}
|
||||
: {
|
||||
kind: 'conflict-editable' as const,
|
||||
conflictKind: entry.conflictKind,
|
||||
conflictStatus: entry.conflictStatus,
|
||||
conflictStatusSource: entry.conflictStatusSource
|
||||
}
|
||||
: undefined
|
||||
|
||||
return {
|
||||
id: absolutePath,
|
||||
filePath: absolutePath,
|
||||
relativePath: entry.path,
|
||||
worktreeId: activeFile.worktreeId,
|
||||
language: detectLanguage(entry.path),
|
||||
isDirty: false,
|
||||
mode: 'edit',
|
||||
conflict
|
||||
}
|
||||
}
|
||||
|
||||
const renderMonacoEditor = (fc: FileContent): React.JSX.Element => (
|
||||
// Why: Without a key, React reuses the same MonacoEditor instance when
|
||||
// switching tabs or split panes, just updating props. That means
|
||||
|
|
@ -400,12 +435,16 @@ export function EditorContent({
|
|||
contentFile,
|
||||
entry,
|
||||
className,
|
||||
viewStateKeySuffix
|
||||
viewStateKeySuffix,
|
||||
readOnly = false,
|
||||
autoHeight = false
|
||||
}: {
|
||||
contentFile: OpenFile
|
||||
entry: GitStatusEntry | null
|
||||
className: string
|
||||
viewStateKeySuffix: string
|
||||
readOnly?: boolean
|
||||
autoHeight?: boolean
|
||||
}): React.JSX.Element => {
|
||||
if (contentFile.conflict?.kind === 'conflict-placeholder') {
|
||||
return (
|
||||
|
|
@ -470,7 +509,7 @@ export function EditorContent({
|
|||
conflictNavigation={getConflictNavigation(contentFile, selectedContent)}
|
||||
/>
|
||||
)}
|
||||
<div className="min-h-0 flex-1">
|
||||
<div className={autoHeight ? 'shrink-0' : 'min-h-0 flex-1'}>
|
||||
<MonacoEditor
|
||||
key={`${viewStateScopeId}:${contentFile.id}:${viewStateKeySuffix}`}
|
||||
filePath={contentFile.filePath}
|
||||
|
|
@ -478,11 +517,15 @@ export function EditorContent({
|
|||
relativePath={contentFile.relativePath}
|
||||
content={selectedContent}
|
||||
language={monacoSelectedLanguage}
|
||||
onContentChange={(content) => handleContentChangeForFile(contentFile, content)}
|
||||
onSave={(content) => handleSaveForFile(contentFile, content)}
|
||||
onContentChange={
|
||||
readOnly ? () => {} : (content) => handleContentChangeForFile(contentFile, content)
|
||||
}
|
||||
onSave={readOnly ? () => {} : (content) => handleSaveForFile(contentFile, content)}
|
||||
worktreeId={contentFile.worktreeId}
|
||||
markdownAnnotationsEnabled={false}
|
||||
conflictDecorationsEnabled={contentFile.conflict?.conflictStatus === 'unresolved'}
|
||||
readOnly={readOnly}
|
||||
autoHeight={autoHeight}
|
||||
revealLine={
|
||||
pendingEditorReveal?.filePath === contentFile.filePath
|
||||
? pendingEditorReveal.line
|
||||
|
|
@ -516,6 +559,34 @@ export function EditorContent({
|
|||
})
|
||||
}
|
||||
|
||||
const renderConflictReviewInlineFile = (entry: GitStatusEntry): React.JSX.Element => {
|
||||
const contentFile = createConflictReviewContentFile(entry)
|
||||
|
||||
return renderConflictReviewEditorContent({
|
||||
contentFile,
|
||||
entry,
|
||||
className: 'flex min-h-[120px] flex-col border-b border-border last:border-b-0',
|
||||
viewStateKeySuffix: `overview:${entry.path}`,
|
||||
readOnly: true,
|
||||
autoHeight: true
|
||||
})
|
||||
}
|
||||
|
||||
const renderConflictReviewAllContent = (): React.JSX.Element => {
|
||||
const snapshotEntries = activeFile.conflictReview?.entries ?? []
|
||||
const liveEntriesByPath = new Map(worktreeEntries.map((entry) => [entry.path, entry]))
|
||||
const unresolvedEntries = snapshotEntries.flatMap((entry) => {
|
||||
const liveEntry = liveEntriesByPath.get(entry.path)
|
||||
return liveEntry?.conflictStatus === 'unresolved' && liveEntry.conflictKind ? [liveEntry] : []
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="min-h-0 flex-1 overflow-y-auto bg-editor-surface scrollbar-sleek">
|
||||
{unresolvedEntries.map(renderConflictReviewInlineFile)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (activeFile.mode === 'conflict-review') {
|
||||
return (
|
||||
<ConflictReviewPanel
|
||||
|
|
@ -526,7 +597,7 @@ export function EditorContent({
|
|||
selectedContent={
|
||||
selectedConflictReviewFile
|
||||
? renderConflictReviewSelectedContent(selectedConflictReviewFile)
|
||||
: null
|
||||
: renderConflictReviewAllContent()
|
||||
}
|
||||
onDismiss={() => closeFile(activeFile.id)}
|
||||
onRefreshSnapshot={() =>
|
||||
|
|
|
|||
|
|
@ -53,6 +53,8 @@ type MonacoEditorProps = {
|
|||
worktreeId?: string
|
||||
markdownAnnotationsEnabled?: boolean
|
||||
conflictDecorationsEnabled?: boolean
|
||||
readOnly?: boolean
|
||||
autoHeight?: boolean
|
||||
}
|
||||
|
||||
export default function MonacoEditor({
|
||||
|
|
@ -69,11 +71,14 @@ export default function MonacoEditor({
|
|||
markdownDocuments,
|
||||
worktreeId,
|
||||
markdownAnnotationsEnabled = false,
|
||||
conflictDecorationsEnabled = false
|
||||
conflictDecorationsEnabled = false,
|
||||
readOnly = false,
|
||||
autoHeight = false
|
||||
}: MonacoEditorProps): React.JSX.Element {
|
||||
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null)
|
||||
const editorContainerRef = useRef<HTMLDivElement | null>(null)
|
||||
const [mountedEditor, setMountedEditor] = useState<editor.IStandaloneCodeEditor | null>(null)
|
||||
const [autoHeightContentHeight, setAutoHeightContentHeight] = useState<number | null>(null)
|
||||
const modelKeyRef = useRef<string | null>(null)
|
||||
const languageRef = useRef(language)
|
||||
languageRef.current = language
|
||||
|
|
@ -113,6 +118,16 @@ export default function MonacoEditor({
|
|||
settings?.terminalFontSize ?? 13,
|
||||
editorFontZoomLevel
|
||||
)
|
||||
const estimatedAutoHeight = useMemo(() => {
|
||||
if (!autoHeight) {
|
||||
return null
|
||||
}
|
||||
const lineHeight = Math.ceil(editorFontSize * 1.45)
|
||||
return Math.max(80, content.split(/\r?\n/).length * lineHeight + 18)
|
||||
}, [autoHeight, content, editorFontSize])
|
||||
const renderedEditorHeight = autoHeight
|
||||
? (autoHeightContentHeight ?? estimatedAutoHeight ?? 80)
|
||||
: null
|
||||
// Why: `keepCurrentModel` retains Monaco models across unmounts, and
|
||||
// @monaco-editor/react skips its value→model sync on the first render after
|
||||
// a remount. Without explicit sync, external file changes that arrived
|
||||
|
|
@ -279,6 +294,24 @@ export default function MonacoEditor({
|
|||
(editorInstance, monaco) => {
|
||||
editorRef.current = editorInstance
|
||||
setMountedEditor(editorInstance)
|
||||
let autoHeightSub: { dispose: () => void } | null = null
|
||||
let autoHeightFrame: number | null = null
|
||||
const updateAutoHeight = (): void => {
|
||||
if (!autoHeight) {
|
||||
return
|
||||
}
|
||||
if (autoHeightFrame !== null) {
|
||||
return
|
||||
}
|
||||
autoHeightFrame = window.requestAnimationFrame(() => {
|
||||
autoHeightFrame = null
|
||||
setAutoHeightContentHeight(Math.ceil(editorInstance.getContentHeight()) + 1)
|
||||
})
|
||||
}
|
||||
if (autoHeight) {
|
||||
updateAutoHeight()
|
||||
autoHeightSub = editorInstance.onDidContentSizeChange(updateAutoHeight)
|
||||
}
|
||||
markdownDocLinkDecorationsRef.current = createMarkdownDocLinkDecorationController(
|
||||
editorInstance,
|
||||
() => languageRef.current
|
||||
|
|
@ -337,6 +370,11 @@ export default function MonacoEditor({
|
|||
})
|
||||
|
||||
editorInstance.onDidDispose(() => {
|
||||
autoHeightSub?.dispose()
|
||||
if (autoHeightFrame !== null) {
|
||||
window.cancelAnimationFrame(autoHeightFrame)
|
||||
autoHeightFrame = null
|
||||
}
|
||||
conflictDecorationsRef.current?.clear()
|
||||
conflictDecorationsRef.current = null
|
||||
editorRef.current = null
|
||||
|
|
@ -413,7 +451,8 @@ export default function MonacoEditor({
|
|||
filePath,
|
||||
setEditorCursorLine,
|
||||
updateMarkdownCompletionDocuments,
|
||||
viewStateKey
|
||||
viewStateKey,
|
||||
autoHeight
|
||||
]
|
||||
)
|
||||
|
||||
|
|
@ -601,7 +640,11 @@ export default function MonacoEditor({
|
|||
}, [queueReveal, revealLine, revealColumn, revealMatchLength, setPendingEditorReveal])
|
||||
|
||||
return (
|
||||
<div ref={editorContainerRef} className="relative h-full">
|
||||
<div
|
||||
ref={editorContainerRef}
|
||||
className={autoHeight ? 'relative' : 'relative h-full'}
|
||||
style={renderedEditorHeight === null ? undefined : { height: renderedEditorHeight }}
|
||||
>
|
||||
{commentPopover && shouldShowMarkdownAnnotations && (
|
||||
<DiffCommentPopover
|
||||
key={commentPopover.lineNumber}
|
||||
|
|
@ -614,7 +657,7 @@ export default function MonacoEditor({
|
|||
/>
|
||||
)}
|
||||
<Editor
|
||||
height="100%"
|
||||
height={renderedEditorHeight === null ? '100%' : `${renderedEditorHeight}px`}
|
||||
language={language}
|
||||
value={content}
|
||||
theme={isDark ? 'vs-dark' : 'vs'}
|
||||
|
|
@ -634,6 +677,8 @@ export default function MonacoEditor({
|
|||
renderLineHighlight: 'line',
|
||||
automaticLayout: true,
|
||||
tabSize: 2,
|
||||
readOnly,
|
||||
scrollbar: autoHeight ? { vertical: 'hidden', handleMouseWheel: false } : undefined,
|
||||
smoothScrolling: true,
|
||||
cursorSmoothCaretAnimation: 'off',
|
||||
padding: { top: 0 },
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import type { OpenFile } from '@/store/slices/editor'
|
||||
import { getConnectionId } from '@/lib/connection-context'
|
||||
import { joinPath } from '@/lib/path'
|
||||
import { useAppStore } from '@/store'
|
||||
import { getRuntimeFileReadScope, readRuntimeFileContent } from '@/runtime/runtime-file-client'
|
||||
import { settingsForRuntimeOwner } from '@/runtime/runtime-rpc-client'
|
||||
|
|
@ -257,6 +258,32 @@ export function useEditorPanelContentState({
|
|||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (activeFile?.mode === 'conflict-review' && !selectedConflictReviewFile) {
|
||||
const snapshotEntries = activeFile.conflictReview?.entries ?? []
|
||||
if (snapshotEntries.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const snapshotPaths = new Set(snapshotEntries.map((entry) => entry.path))
|
||||
const liveEntries = gitStatusByWorktree[activeFile.worktreeId] ?? []
|
||||
for (const entry of liveEntries) {
|
||||
if (
|
||||
!snapshotPaths.has(entry.path) ||
|
||||
entry.conflictStatus !== 'unresolved' ||
|
||||
!entry.conflictKind ||
|
||||
entry.status === 'deleted'
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
const absolutePath = joinPath(activeFile.filePath, entry.path)
|
||||
if (!fileContents[absolutePath]) {
|
||||
void loadFileContent(absolutePath, absolutePath, activeFile.worktreeId, entry.path)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const fileToLoad = selectedConflictReviewFile ?? activeFile
|
||||
if (!fileToLoad || (activeFile?.mode === 'conflict-review' && !selectedConflictReviewFile)) {
|
||||
return
|
||||
|
|
|
|||
Loading…
Reference in New Issue