mirror of
https://github.com/stablyai/orca.git
synced 2026-09-28 16:02:45 +00:00
feat(diffs): port the copy-context hint and Linux primary selection to Pierre
Completes the contextual-copy port. Monaco drew the "Copy context" hint as a content widget; Pierre has no widget layer, so it becomes an overlay pinned to the selection's client rect, shown only for multi-line selections (single-line copies plainly, matching formatCopiedSelectionWithContext). Primary selection (X11 middle-click paste) is restored from the shadow-root selection, gated on isPrimarySelectionEnabled and PRIMARY_SELECTION_MAX_LENGTH and debounced 200ms so a drag does not churn the buffer.
This commit is contained in:
@@ -1,5 +1,14 @@
|
||||
import { formatCopiedSelectionWithContext } from '../selection-copy'
|
||||
import { editorShortcutMatches } from '../editor-shortcuts'
|
||||
import { formatShortcutLabel } from '@/hooks/useShortcutLabel'
|
||||
import { useAppStore } from '@/store'
|
||||
import {
|
||||
PRIMARY_SELECTION_MAX_LENGTH,
|
||||
isPrimarySelectionEnabled,
|
||||
setPrimarySelectionText
|
||||
} from '@/lib/primary-selection'
|
||||
|
||||
const PRIMARY_SELECTION_DEBOUNCE_MS = 200
|
||||
|
||||
/** Resolves the 1-based line of the row containing a selection boundary node. */
|
||||
function lineOf(node: Node | null): number | null {
|
||||
@@ -19,6 +28,69 @@ export function installPierreContextualCopy(
|
||||
container: HTMLElement,
|
||||
getFileInfo: () => { relativePath: string; language: string }
|
||||
): () => void {
|
||||
// Why: Monaco drew this as a content widget; Pierre has no widget layer, so it
|
||||
// is an overlay pinned to the selection's client rect instead.
|
||||
const hint = document.createElement('div')
|
||||
hint.className =
|
||||
'pointer-events-none fixed z-50 rounded-md border border-border/90 bg-background px-2.5 py-1 text-xs font-medium text-foreground shadow-[0_6px_18px_rgba(15,23,42,0.18)] backdrop-blur whitespace-nowrap'
|
||||
hint.style.display = 'none'
|
||||
document.body.appendChild(hint)
|
||||
let primarySelectionTimer: number | null = null
|
||||
|
||||
const readSelection = (): Selection | null => {
|
||||
const root = container.querySelector('diffs-container')?.shadowRoot
|
||||
return (root as unknown as { getSelection?: () => Selection | null })?.getSelection?.() ?? null
|
||||
}
|
||||
|
||||
const hideHint = (): void => {
|
||||
hint.style.display = 'none'
|
||||
}
|
||||
|
||||
const updateHint = (): void => {
|
||||
const selection = readSelection()
|
||||
const text = selection?.toString() ?? ''
|
||||
const startLine = lineOf(selection?.anchorNode ?? null)
|
||||
const endLine = lineOf(selection?.focusNode ?? null)
|
||||
// Why: copy-with-context is a multi-line affordance; a single line copies plainly.
|
||||
if (!text || startLine == null || endLine == null || startLine === endLine) {
|
||||
hideHint()
|
||||
return
|
||||
}
|
||||
hint.textContent = `Copy context ${formatShortcutLabel(
|
||||
'editor.copyContext',
|
||||
useAppStore.getState().keybindings
|
||||
)}`
|
||||
const rect = selection?.getRangeAt(0).getBoundingClientRect()
|
||||
if (!rect || rect.width === 0) {
|
||||
hideHint()
|
||||
return
|
||||
}
|
||||
hint.style.display = 'block'
|
||||
const above = rect.top > hint.offsetHeight + 12
|
||||
hint.style.left = `${Math.round(rect.left)}px`
|
||||
hint.style.top = `${Math.round(above ? rect.top - hint.offsetHeight - 8 : rect.bottom + 8)}px`
|
||||
}
|
||||
|
||||
// Why: mirrors the editor's selection-clipboard debounce so a drag does not churn X11's buffer.
|
||||
const updatePrimarySelection = (): void => {
|
||||
if (!isPrimarySelectionEnabled()) {
|
||||
return
|
||||
}
|
||||
const text = readSelection()?.toString() ?? ''
|
||||
if (!text || text.length > PRIMARY_SELECTION_MAX_LENGTH) {
|
||||
return
|
||||
}
|
||||
setPrimarySelectionText(text)
|
||||
}
|
||||
|
||||
const handleSelectionChange = (): void => {
|
||||
updateHint()
|
||||
if (primarySelectionTimer !== null) {
|
||||
window.clearTimeout(primarySelectionTimer)
|
||||
}
|
||||
primarySelectionTimer = window.setTimeout(updatePrimarySelection, PRIMARY_SELECTION_DEBOUNCE_MS)
|
||||
}
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent): void => {
|
||||
if (!editorShortcutMatches('editor.copyContext', event)) {
|
||||
return
|
||||
@@ -56,8 +128,17 @@ export function installPierreContextualCopy(
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
void navigator.clipboard.writeText(formatted)
|
||||
hideHint()
|
||||
}
|
||||
|
||||
container.addEventListener('keydown', handleKeyDown, true)
|
||||
return () => container.removeEventListener('keydown', handleKeyDown, true)
|
||||
document.addEventListener('selectionchange', handleSelectionChange)
|
||||
return () => {
|
||||
container.removeEventListener('keydown', handleKeyDown, true)
|
||||
document.removeEventListener('selectionchange', handleSelectionChange)
|
||||
if (primarySelectionTimer !== null) {
|
||||
window.clearTimeout(primarySelectionTimer)
|
||||
}
|
||||
hint.remove()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user