fix(editor): keep conflict navigation in its owning pane (#34902)

Conflict Next/Previous published a path-only reveal, so every editable pane
showing that file jumped, and overview mirrors never moved. Target the reveal
at the requesting file id and pane view id, and let read-only overview
mirrors accept only reveals that name them.

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
OrcaWin
2026-10-28 17:41:33 -07:00
committed by GitHub
co-authored by OrcaWin
parent f0c86458c3
commit 53cbc49e76
4 changed files with 73 additions and 11 deletions
@@ -20,6 +20,7 @@ type MonacoProps = {
worktreeId?: string
passiveMirror?: boolean
revealLine?: number
viewStateId?: string
onContentChange: (content: string) => void
onSave: (content: string) => void
}
@@ -239,7 +240,47 @@ describe('conflict-review overview as a live read-only view of the record', () =
expect(reloadContent).toHaveBeenCalledWith(view, { wakeWslDistro: true })
})
it("mounts as a passive mirror: the view's workspace, no cursor tracking, no reveals", () => {
it('applies a reveal explicitly owned by this overview and rejects another pane', () => {
const { store, view, review } = setup()
const reveal = { fileId: view.id, filePath: PATH, line: 9, column: 1, matchLength: 0 }
const ownOverview = render(store, review, 'disk', {
pendingEditorReveal: { ...reveal, viewStateId: 'panel-1:overview:file.ts' }
})
expect(ownOverview.revealLine).toBe(9)
const anotherPane = render(store, review, 'disk', {
pendingEditorReveal: { ...reveal, viewStateId: 'another-pane' }
})
expect(anotherPane.revealLine).toBeUndefined()
})
it('keeps a selected review editor scoped to its review pane', () => {
const { store, review } = setup()
store.getState().openConflictReviewFile(review.id, 'wt-2', '/repo', ENTRY, 'typescript')
const selectedReview = store.getState().openFiles.find((file) => file.id === review.id)
if (!selectedReview?.conflictReview?.selectedFileId) {
throw new Error('Expected selected conflict')
}
const reveal = {
fileId: selectedReview.conflictReview.selectedFileId,
filePath: PATH,
line: 9,
column: 1,
matchLength: 0
}
expect(
render(store, selectedReview, 'disk', {
pendingEditorReveal: { ...reveal, viewStateId: 'another-pane' }
}).revealLine
).toBeUndefined()
expect(
render(store, selectedReview, 'disk', {
pendingEditorReveal: { ...reveal, viewStateId: 'panel-1' }
}).revealLine
).toBe(9)
})
it("mounts as a passive mirror: the view's workspace, no cursor tracking, ignores untargeted reveals", () => {
const { store, view, review } = setup()
const overview = render(store, review, 'disk', {
pendingEditorReveal: { fileId: view.id, filePath: PATH, line: 9, column: 1, matchLength: 0 }
@@ -35,7 +35,11 @@ export function EditorConflictReviewSurface({
openFiles: OpenFile[]
worktreeEntries: GitStatusEntry[]
pendingEditorReveal: PendingEditorReveal | null
getConflictNavigation: (file: OpenFile, content: string) => EditorConflictNavigation | undefined
getConflictNavigation: (
file: OpenFile,
content: string,
viewStateId: string
) => EditorConflictNavigation | undefined
handleContentChangeForFile: (file: OpenFile, content: string) => void
handleSaveForFile: (file: OpenFile, content: string) => Promise<boolean>
reloadContent: (file: OpenFile, options?: { wakeWslDistro?: boolean }) => void
@@ -190,9 +194,13 @@ export function EditorConflictReviewSurface({
const monacoLanguage = selectedLanguage === 'notebook' ? 'json' : selectedLanguage
const selectedViewStateKey = `${contentFile.filePath}::${viewStateScopeId}:${viewStateKeySuffix}`
const selectedContent = editBuffers[contentFile.id] ?? fileContent.content
// Read-only overviews must not consume reveals intended for the borrowed tab.
const editorViewStateId = readOnly
? `${viewStateScopeId}:${viewStateKeySuffix}`
: viewStateScopeId
// Overview navigation stays in its own mirror; borrowed-tab reveals belong to that tab.
const reveal =
!readOnly && matchesPendingEditorReveal(pendingEditorReveal, contentFile)
matchesPendingEditorReveal(pendingEditorReveal, contentFile, editorViewStateId) &&
(!readOnly || pendingEditorReveal.viewStateId === editorViewStateId)
? pendingEditorReveal
: null
@@ -202,7 +210,11 @@ export function EditorConflictReviewSurface({
<ConflictBanner
file={contentFile}
entry={entry}
conflictNavigation={getConflictNavigation(contentFile, selectedContent)}
conflictNavigation={getConflictNavigation(
contentFile,
selectedContent,
editorViewStateId
)}
/>
)}
<div className={autoHeight ? 'shrink-0' : 'min-h-0 flex-1'}>
@@ -211,6 +223,7 @@ export function EditorConflictReviewSurface({
fileId={contentFile.id}
filePath={contentFile.filePath}
viewStateKey={selectedViewStateKey}
viewStateId={editorViewStateId}
relativePath={contentFile.relativePath}
content={selectedContent}
language={monacoLanguage}
@@ -300,9 +313,10 @@ export function EditorConflictReviewSurface({
function matchesPendingEditorReveal(
reveal: PendingEditorReveal | null,
file: Pick<OpenFile, 'id' | 'filePath'>
file: Pick<OpenFile, 'id' | 'filePath'>,
viewStateId: string
): reveal is PendingEditorReveal {
if (!reveal) {
if (!reveal || (reveal.viewStateId && reveal.viewStateId !== viewStateId)) {
return false
}
return reveal.fileId ? reveal.fileId === file.id : reveal.filePath === file.filePath
@@ -89,7 +89,11 @@ export function EditorEditFileSurface({
markdownAnnotationsEnabled: boolean
pendingEditorReveal: PendingEditorReveal | null
markdownDocuments: MarkdownDocumentsController
getConflictNavigation: (file: OpenFile, content: string) => EditorConflictNavigation | undefined
getConflictNavigation: (
file: OpenFile,
content: string,
viewStateId: string
) => EditorConflictNavigation | undefined
getMarkdownSourceLineOffset: (frontMatterRaw: string) => number
handleContentChange: (content: string) => void
handleDirtyStateHint: (dirty: boolean) => void
@@ -316,7 +320,7 @@ export function EditorEditFileSurface({
<ConflictBanner
file={activeFile}
entry={activeConflictEntry}
conflictNavigation={getConflictNavigation(activeFile, currentContent)}
conflictNavigation={getConflictNavigation(activeFile, currentContent, viewStateScopeId)}
/>
)}
<div className="min-h-0 flex-1 relative">{editorSurface}</div>
@@ -15,13 +15,14 @@ export type EditorConflictNavigation = {
export function useEditorConflictNavigation(): (
file: OpenFile,
content: string
content: string,
viewStateId: string
) => EditorConflictNavigation | undefined {
const setPendingEditorReveal = useAppStore((state) => state.setPendingEditorReveal)
const [navigationIndexByFile, setNavigationIndexByFile] = useState<Record<string, number>>({})
return useCallback(
(file: OpenFile, content: string) => {
(file: OpenFile, content: string, viewStateId: string) => {
const blocks = findGitConflictBlocks(content)
if (blocks.length === 0) {
return undefined
@@ -48,6 +49,8 @@ export function useEditorConflictNavigation(): (
queueMicrotask(() => {
setPendingEditorReveal({
filePath: file.filePath,
fileId: file.id,
viewStateId,
line,
column: 1,
matchLength: markerLineLength