mirror of
https://github.com/stablyai/orca.git
synced 2026-10-09 08:02:35 +00:00
Avoid repeatedly extracting source text for the copy-context hint (#26115)
This commit is contained in:
@@ -1,5 +1,33 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { formatCopiedSelectionWithContext, getContextualCopyLineRange } from './selection-copy'
|
||||
import {
|
||||
canCopySelectionWithContext,
|
||||
formatCopiedSelectionWithContext,
|
||||
getContextualCopyLineRange
|
||||
} from './selection-copy'
|
||||
|
||||
describe('contextual copy eligibility', () => {
|
||||
it('matches formatting eligibility for line boundaries and whitespace-only selections', () => {
|
||||
for (const startLineNumber of [1, 2, 44]) {
|
||||
for (const endLineNumber of [1, 2, 3, 44, 45]) {
|
||||
for (const endColumn of [1, 2, 15]) {
|
||||
const selection = { startLineNumber, startColumn: 1, endLineNumber, endColumn }
|
||||
const expected =
|
||||
endLineNumber !== startLineNumber &&
|
||||
(endColumn === 1 ? endLineNumber - 1 : endLineNumber) >= startLineNumber
|
||||
expect(canCopySelectionWithContext(selection)).toBe(expected)
|
||||
expect(
|
||||
formatCopiedSelectionWithContext({
|
||||
relativePath: '',
|
||||
language: 'plaintext',
|
||||
selection,
|
||||
selectedText: ' \n'
|
||||
}) !== null
|
||||
).toBe(expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('formatCopiedSelectionWithContext', () => {
|
||||
it('formats multi-line selections with file and line context', () => {
|
||||
|
||||
@@ -13,15 +13,10 @@ export function formatCopiedSelectionWithContext({
|
||||
selection,
|
||||
selectedText
|
||||
}: FormatCopiedSelectionArgs): string | null {
|
||||
if (!canCopySelectionWithContext(selection)) {
|
||||
return null
|
||||
}
|
||||
const { startLine, endLine } = getContextualCopyLineRange(selection)
|
||||
const isSingleLineSelection = selection.startLineNumber === selection.endLineNumber
|
||||
if (isSingleLineSelection) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (endLine < startLine) {
|
||||
return null
|
||||
}
|
||||
|
||||
const codeFenceLanguage = getCodeFenceLanguage(language)
|
||||
const codeBlock = selectedText.endsWith('\n') ? selectedText : `${selectedText}\n`
|
||||
@@ -30,6 +25,13 @@ export function formatCopiedSelectionWithContext({
|
||||
return `File: ${relativePath}\n${lineLabel}\n\n\`\`\`${codeFenceLanguage}\n${codeBlock}\`\`\``
|
||||
}
|
||||
|
||||
export function canCopySelectionWithContext(selection: IRange): boolean {
|
||||
return (
|
||||
selection.startLineNumber !== selection.endLineNumber &&
|
||||
getInclusiveEndLine(selection) >= selection.startLineNumber
|
||||
)
|
||||
}
|
||||
|
||||
export function getContextualCopyLineRange(selection: IRange): {
|
||||
startLine: number
|
||||
endLine: number
|
||||
|
||||
@@ -29,6 +29,8 @@ vi.mock('@/lib/primary-selection', () => ({
|
||||
setPrimarySelectionText: () => {}
|
||||
}))
|
||||
|
||||
vi.mock('./editor-shortcuts', () => ({ editorShortcutMatches: () => true }))
|
||||
|
||||
describe('setupContextualCopy', () => {
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals()
|
||||
@@ -85,9 +87,16 @@ describe('setupContextualCopy', () => {
|
||||
expect(setInterval).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('polls a focused editor while a contextual copy hint is visible', () => {
|
||||
const setInterval = vi.fn(() => 1)
|
||||
it('refreshes a visible hint without reading selection text and extracts it only on copy', async () => {
|
||||
let refreshHint = (): void => {}
|
||||
let keydown = (_event: KeyboardEvent): void => {}
|
||||
const setInterval = vi.fn((callback: () => void) => {
|
||||
refreshHint = callback
|
||||
return 1
|
||||
})
|
||||
const writeClipboardText = vi.fn(async (_text: string) => {})
|
||||
vi.stubGlobal('window', {
|
||||
api: { ui: { writeClipboardText } },
|
||||
clearInterval: vi.fn(),
|
||||
clearTimeout: vi.fn(),
|
||||
setInterval,
|
||||
@@ -111,16 +120,24 @@ describe('setupContextualCopy', () => {
|
||||
getStartPosition: () => ({ lineNumber: 1, column: 1 }),
|
||||
getEndPosition: () => ({ lineNumber: 2, column: 4 })
|
||||
}
|
||||
const selectedText = `${'x'.repeat(4 * 1024 * 1024)}\ntwo`
|
||||
const getValueInRange = vi.fn(() => selectedText)
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The editor stub implements every method setupContextualCopy invokes in this scenario.
|
||||
const editorInstance = {
|
||||
addContentWidget: vi.fn(),
|
||||
getContainerDomNode: () => ({
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn()
|
||||
addEventListener: (type: string, callback: (event: KeyboardEvent) => void) => {
|
||||
if (type === 'keydown') {
|
||||
keydown = callback
|
||||
}
|
||||
},
|
||||
removeEventListener: vi.fn(),
|
||||
getBoundingClientRect: () => ({ left: 0, top: 0, width: 500 })
|
||||
}),
|
||||
getLayoutInfo: () => ({ height: 500 }),
|
||||
getModel: () => ({
|
||||
getLineMaxColumn: () => 4,
|
||||
getValueInRange: () => 'one\ntwo'
|
||||
getValueInRange
|
||||
}),
|
||||
getScrolledVisiblePosition: () => ({ top: 20, left: 8, height: 16 }),
|
||||
getSelection: () => selection,
|
||||
@@ -148,6 +165,20 @@ describe('setupContextualCopy', () => {
|
||||
})
|
||||
|
||||
expect(setInterval).toHaveBeenCalledTimes(1)
|
||||
for (let tick = 0; tick < 20; tick += 1) {
|
||||
refreshHint()
|
||||
}
|
||||
expect(getValueInRange).not.toHaveBeenCalled()
|
||||
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Shortcut matching is mocked; the handler reads only these event methods.
|
||||
keydown({ preventDefault: vi.fn(), stopPropagation: vi.fn() } as unknown as KeyboardEvent)
|
||||
await Promise.resolve()
|
||||
expect(getValueInRange).toHaveBeenCalledTimes(1)
|
||||
expect(writeClipboardText).toHaveBeenCalledWith(
|
||||
`File: src/example.ts\nLines: 1-2\n\n\`\`\`ts\n${selectedText}\n\`\`\``
|
||||
)
|
||||
refreshHint()
|
||||
expect(getValueInRange).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('clears editor-scoped contextual copy cleanup on dispose', () => {
|
||||
|
||||
@@ -3,7 +3,11 @@ import { formatShortcutLabel } from '@/hooks/useShortcutLabel'
|
||||
import { monaco } from '@/lib/monaco-setup'
|
||||
import { useAppStore } from '@/store'
|
||||
import { editorShortcutMatches } from './editor-shortcuts'
|
||||
import { formatCopiedSelectionWithContext, getContextualCopyLineRange } from './selection-copy'
|
||||
import {
|
||||
canCopySelectionWithContext,
|
||||
formatCopiedSelectionWithContext,
|
||||
getContextualCopyLineRange
|
||||
} from './selection-copy'
|
||||
import {
|
||||
PRIMARY_SELECTION_MAX_LENGTH,
|
||||
isPrimarySelectionEnabled,
|
||||
@@ -87,8 +91,10 @@ export function setupContextualCopy({
|
||||
|
||||
const updateCopyHint = (): void => {
|
||||
updateCopyHintLabel()
|
||||
const contextualCopyText = getContextualCopyText()
|
||||
if (!contextualCopyText) {
|
||||
const model = editorInstance.getModel()
|
||||
const selection = editorInstance.getSelection()
|
||||
// Why: hint ticks need selection bounds; extracting the selected text scales with file size.
|
||||
if (!model || !selection || selection.isEmpty() || !canCopySelectionWithContext(selection)) {
|
||||
copyHintNode.style.display = 'none'
|
||||
copyHintWidgetPosition = null
|
||||
editorInstance.layoutContentWidget(copyHintWidget)
|
||||
@@ -102,15 +108,6 @@ export function setupContextualCopy({
|
||||
return
|
||||
}
|
||||
|
||||
const model = editorInstance.getModel()
|
||||
const selection = editorInstance.getSelection()
|
||||
if (!model || !selection) {
|
||||
copyHintNode.style.display = 'none'
|
||||
copyHintWidgetPosition = null
|
||||
editorInstance.layoutContentWidget(copyHintWidget)
|
||||
return
|
||||
}
|
||||
|
||||
const { startLine, endLine } = getContextualCopyLineRange(selection)
|
||||
const startVisiblePosition = editorInstance.getScrolledVisiblePosition(
|
||||
selection.getStartPosition()
|
||||
|
||||
Reference in New Issue
Block a user