Fix terminal code pointer reveals (#1991)
This commit is contained in:
parent
a1a78d12d5
commit
c393e4e779
|
|
@ -28,6 +28,7 @@ const runtimeEnvironmentCallMock = vi.fn()
|
|||
const runtimeEnvironmentTransportCallMock = vi.fn()
|
||||
const setActiveWorktreeMock = vi.fn()
|
||||
const createBrowserTabMock = vi.fn()
|
||||
const setPendingEditorRevealMock = vi.fn()
|
||||
|
||||
const deps = { worktreeId: 'wt-1', worktreePath: '/tmp' }
|
||||
const storeState = {
|
||||
|
|
@ -36,7 +37,8 @@ const storeState = {
|
|||
| undefined,
|
||||
setActiveWorktree: setActiveWorktreeMock,
|
||||
createBrowserTab: createBrowserTabMock,
|
||||
openFile: openFileMock
|
||||
openFile: openFileMock,
|
||||
setPendingEditorReveal: setPendingEditorRevealMock
|
||||
}
|
||||
|
||||
vi.mock('@/store', () => ({
|
||||
|
|
@ -65,6 +67,23 @@ function setPlatform(userAgent: string): void {
|
|||
vi.stubGlobal('navigator', { userAgent })
|
||||
}
|
||||
|
||||
function createDeferred<T>(): { promise: Promise<T>; resolve: (value: T) => void } {
|
||||
let resolve!: (value: T) => void
|
||||
const promise = new Promise<T>((res) => {
|
||||
resolve = res
|
||||
})
|
||||
return { promise, resolve }
|
||||
}
|
||||
|
||||
async function flushAsyncWork(): Promise<void> {
|
||||
await new Promise((resolve) => setTimeout(resolve, 0))
|
||||
}
|
||||
|
||||
async function flushDoubleRaf(): Promise<void> {
|
||||
await flushAsyncWork()
|
||||
await flushAsyncWork()
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
clearRuntimeCompatibilityCacheForTests()
|
||||
vi.clearAllMocks()
|
||||
|
|
@ -76,6 +95,7 @@ beforeEach(() => {
|
|||
storeState.settings = undefined
|
||||
registerHttpLinkStoreAccessor(() => storeState)
|
||||
vi.stubGlobal('window', {
|
||||
dispatchEvent: vi.fn(),
|
||||
api: {
|
||||
shell: {
|
||||
openUrl: openUrlMock,
|
||||
|
|
@ -90,6 +110,9 @@ beforeEach(() => {
|
|||
runtimeEnvironments: { call: runtimeEnvironmentTransportCallMock }
|
||||
}
|
||||
})
|
||||
vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback): number => {
|
||||
return setTimeout(() => callback(0), 0) as unknown as number
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
|
|
@ -191,6 +214,7 @@ describe('handleOscLink', () => {
|
|||
|
||||
// Why: .html should not open Monaco — it should render in the browser tab.
|
||||
expect(openFileMock).not.toHaveBeenCalled()
|
||||
expect(setPendingEditorRevealMock).not.toHaveBeenCalled()
|
||||
expect(createBrowserTabMock).toHaveBeenCalledWith(
|
||||
'wt-1',
|
||||
'file:///tmp/report.html',
|
||||
|
|
@ -205,6 +229,7 @@ describe('handleOscLink', () => {
|
|||
await new Promise((resolve) => setTimeout(resolve, 0))
|
||||
|
||||
expect(openFileMock).not.toHaveBeenCalled()
|
||||
expect(setPendingEditorRevealMock).not.toHaveBeenCalled()
|
||||
expect(createBrowserTabMock).toHaveBeenCalledWith(
|
||||
'wt-1',
|
||||
'file:///tmp/legacy.HTM',
|
||||
|
|
@ -212,6 +237,41 @@ describe('handleOscLink', () => {
|
|||
)
|
||||
})
|
||||
|
||||
it('schedules Monaco reveal with default column 1 for :line links', async () => {
|
||||
setPlatform('Macintosh')
|
||||
|
||||
openDetectedFilePath('/tmp/src/main.ts', 42, null, deps)
|
||||
await flushAsyncWork()
|
||||
await flushDoubleRaf()
|
||||
|
||||
expect(openFileMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ filePath: '/tmp/src/main.ts' })
|
||||
)
|
||||
expect(setPendingEditorRevealMock).toHaveBeenNthCalledWith(1, null)
|
||||
expect(setPendingEditorRevealMock).toHaveBeenNthCalledWith(2, {
|
||||
filePath: '/tmp/src/main.ts',
|
||||
line: 42,
|
||||
column: 1,
|
||||
matchLength: 0
|
||||
})
|
||||
})
|
||||
|
||||
it('preserves explicit column for :line:column links', async () => {
|
||||
setPlatform('Macintosh')
|
||||
|
||||
openDetectedFilePath('/tmp/src/main.ts', 42, 7, deps)
|
||||
await flushAsyncWork()
|
||||
await flushDoubleRaf()
|
||||
|
||||
expect(setPendingEditorRevealMock).toHaveBeenNthCalledWith(1, null)
|
||||
expect(setPendingEditorRevealMock).toHaveBeenNthCalledWith(2, {
|
||||
filePath: '/tmp/src/main.ts',
|
||||
line: 42,
|
||||
column: 7,
|
||||
matchLength: 0
|
||||
})
|
||||
})
|
||||
|
||||
it('advertises the browser-open behavior in the html hover hint', () => {
|
||||
setPlatform('Macintosh')
|
||||
expect(getTerminalHtmlFileOpenHint()).toBe('⌘+click to open in browser')
|
||||
|
|
@ -386,6 +446,39 @@ describe('handleOscLink', () => {
|
|||
expect(openFilePathMock).not.toHaveBeenCalled()
|
||||
expect(openFileMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('ignores stale async completion so latest click wins for open and reveal', async () => {
|
||||
setPlatform('Macintosh')
|
||||
const firstStat = createDeferred<{ isDirectory: boolean }>()
|
||||
const secondStat = createDeferred<{ isDirectory: boolean }>()
|
||||
statMock
|
||||
.mockImplementationOnce(() => firstStat.promise)
|
||||
.mockImplementationOnce(() => secondStat.promise)
|
||||
|
||||
openDetectedFilePath('/tmp/src/first.ts', 10, 2, deps)
|
||||
openDetectedFilePath('/tmp/src/second.ts', 20, 3, deps)
|
||||
|
||||
secondStat.resolve({ isDirectory: false })
|
||||
await flushAsyncWork()
|
||||
await flushDoubleRaf()
|
||||
|
||||
firstStat.resolve({ isDirectory: false })
|
||||
await flushAsyncWork()
|
||||
await flushDoubleRaf()
|
||||
|
||||
expect(openFileMock).toHaveBeenCalledTimes(1)
|
||||
expect(openFileMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ filePath: '/tmp/src/second.ts' })
|
||||
)
|
||||
expect(setPendingEditorRevealMock).toHaveBeenNthCalledWith(1, null)
|
||||
expect(setPendingEditorRevealMock).toHaveBeenNthCalledWith(2, {
|
||||
filePath: '/tmp/src/second.ts',
|
||||
line: 20,
|
||||
column: 3,
|
||||
matchLength: 0
|
||||
})
|
||||
expect(setPendingEditorRevealMock).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('createFilePathLinkProvider range bounds', () => {
|
||||
|
|
|
|||
|
|
@ -87,6 +87,8 @@ function getTerminalFileContext(
|
|||
}
|
||||
}
|
||||
|
||||
let latestOpenDetectedFilePathRequestId = 0
|
||||
|
||||
export function openDetectedFilePath(
|
||||
filePath: string,
|
||||
line: number | null,
|
||||
|
|
@ -94,6 +96,7 @@ export function openDetectedFilePath(
|
|||
deps: Pick<LinkHandlerDeps, 'worktreeId' | 'worktreePath' | 'runtimeEnvironmentId'>
|
||||
): void {
|
||||
const { runtimeEnvironmentId, worktreeId, worktreePath } = deps
|
||||
const requestId = ++latestOpenDetectedFilePathRequestId
|
||||
|
||||
void (async () => {
|
||||
let statResult
|
||||
|
|
@ -109,6 +112,10 @@ export function openDetectedFilePath(
|
|||
return
|
||||
}
|
||||
|
||||
if (requestId !== latestOpenDetectedFilePathRequestId) {
|
||||
return
|
||||
}
|
||||
|
||||
if (statResult.isDirectory) {
|
||||
const fileContext = getTerminalFileContext(worktreeId, worktreePath, runtimeEnvironmentId)
|
||||
if (fileContext.connectionId || isRemoteRuntimeFileOperation(fileContext, filePath)) {
|
||||
|
|
@ -158,11 +165,22 @@ export function openDetectedFilePath(
|
|||
})
|
||||
|
||||
if (line !== null) {
|
||||
const targetColumn = column ?? 1
|
||||
store.setPendingEditorReveal(null)
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
if (requestId !== latestOpenDetectedFilePathRequestId) {
|
||||
return
|
||||
}
|
||||
store.setPendingEditorReveal({
|
||||
filePath,
|
||||
line,
|
||||
column: targetColumn,
|
||||
matchLength: 0
|
||||
})
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('orca:editor-reveal-location', {
|
||||
detail: { filePath, line, column }
|
||||
detail: { filePath, line, column: targetColumn }
|
||||
})
|
||||
)
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in New Issue