mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 00:57:45 +00:00
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:
+42
-1
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user