Add editor search-in-files action (#3360)
* Add editor search-in-files action * Resolve Monaco editor cleanup conflict Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com> Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
parent
b8868023d9
commit
a3ca79b67e
|
|
@ -14,6 +14,7 @@ import { registerFileSearchSelectedTextProvider } from '@/lib/file-search-select
|
|||
import { useContextualCopySetup } from './useContextualCopySetup'
|
||||
import { MAX_REVEAL_CONTENT_WAIT_FRAMES, performReveal } from './monaco-reveal'
|
||||
import { syncContentOnMount, syncContentUpdate } from './monaco-content-sync'
|
||||
import { getMonacoCodebaseSearchQuery } from './monaco-codebase-search'
|
||||
import {
|
||||
beginProgrammaticContentSync,
|
||||
endProgrammaticContentSync,
|
||||
|
|
@ -371,6 +372,29 @@ export default function MonacoEditor({
|
|||
propsRef.current.onSave(value)
|
||||
}
|
||||
)
|
||||
const searchInFilesAction = editorInstance.addAction({
|
||||
id: 'orca.searchInFiles',
|
||||
label: 'Search in Files',
|
||||
contextMenuGroupId: 'navigation',
|
||||
contextMenuOrder: 2,
|
||||
run: () => {
|
||||
if (!worktreeId) {
|
||||
return
|
||||
}
|
||||
const query = getMonacoCodebaseSearchQuery(
|
||||
editorInstance.getModel(),
|
||||
editorInstance.getSelection(),
|
||||
editorInstance.getPosition()
|
||||
)
|
||||
if (!query) {
|
||||
return
|
||||
}
|
||||
const state = useAppStore.getState()
|
||||
state.seedFileSearchQuery(worktreeId, query)
|
||||
state.setRightSidebarTab('search')
|
||||
state.setRightSidebarOpen(true)
|
||||
}
|
||||
})
|
||||
|
||||
// Track cursor line for "copy path to line" feature
|
||||
const pos = editorInstance.getPosition()
|
||||
|
|
@ -423,6 +447,7 @@ export default function MonacoEditor({
|
|||
scrollStateSub.dispose()
|
||||
gutterMouseDownSub.dispose()
|
||||
cleanupSaveShortcut()
|
||||
searchInFilesAction.dispose()
|
||||
autoHeightSub?.dispose()
|
||||
if (autoHeightFrame !== null) {
|
||||
window.cancelAnimationFrame(autoHeightFrame)
|
||||
|
|
@ -478,7 +503,8 @@ export default function MonacoEditor({
|
|||
setEditorCursorLine,
|
||||
updateMarkdownCompletionDocuments,
|
||||
viewStateKey,
|
||||
autoHeight
|
||||
autoHeight,
|
||||
worktreeId
|
||||
]
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
import type { IPosition, IRange } from 'monaco-editor'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { getMonacoCodebaseSearchQuery } from './monaco-codebase-search'
|
||||
|
||||
type FakeSelection = IRange & {
|
||||
isEmpty: () => boolean
|
||||
}
|
||||
|
||||
function selection(empty: boolean): FakeSelection {
|
||||
return {
|
||||
startLineNumber: 1,
|
||||
startColumn: 1,
|
||||
endLineNumber: 1,
|
||||
endColumn: 1,
|
||||
isEmpty: () => empty
|
||||
}
|
||||
}
|
||||
|
||||
function position(): IPosition {
|
||||
return { lineNumber: 1, column: 5 }
|
||||
}
|
||||
|
||||
function model(args: { selectedText?: string; word?: string }) {
|
||||
return {
|
||||
getValueInRange: vi.fn((_range: IRange) => args.selectedText ?? ''),
|
||||
getWordAtPosition: vi.fn((_position: IPosition) =>
|
||||
args.word === undefined ? null : { word: args.word }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
describe('getMonacoCodebaseSearchQuery', () => {
|
||||
it('prefers normalized selected text over the cursor word', () => {
|
||||
const fakeModel = model({ selectedText: ' foo\r\n bar ', word: 'fallback' })
|
||||
|
||||
expect(getMonacoCodebaseSearchQuery(fakeModel, selection(false), position())).toBe('foo bar')
|
||||
expect(fakeModel.getWordAtPosition).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('falls back to the cursor word when there is no selection', () => {
|
||||
expect(
|
||||
getMonacoCodebaseSearchQuery(model({ word: 'needle' }), selection(true), position())
|
||||
).toBe('needle')
|
||||
})
|
||||
|
||||
it('falls back to the cursor word when the selection normalizes to empty', () => {
|
||||
expect(
|
||||
getMonacoCodebaseSearchQuery(
|
||||
model({ selectedText: ' \n\t ', word: 'cursorWord' }),
|
||||
selection(false),
|
||||
position()
|
||||
)
|
||||
).toBe('cursorWord')
|
||||
})
|
||||
|
||||
it('returns null when neither selection nor cursor word yields a query', () => {
|
||||
expect(getMonacoCodebaseSearchQuery(model({}), selection(true), position())).toBeNull()
|
||||
expect(getMonacoCodebaseSearchQuery(null, selection(true), position())).toBeNull()
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import type { IPosition, IRange } from 'monaco-editor'
|
||||
import { normalizeSelectedTextForFileSearch } from '@/lib/file-search-selection'
|
||||
|
||||
type MonacoCodebaseSearchModel = {
|
||||
getValueInRange: (range: IRange) => string
|
||||
getWordAtPosition: (position: IPosition) => { word: string } | null
|
||||
}
|
||||
|
||||
type MonacoCodebaseSearchSelection = IRange & {
|
||||
isEmpty: () => boolean
|
||||
}
|
||||
|
||||
export function getMonacoCodebaseSearchQuery(
|
||||
model: MonacoCodebaseSearchModel | null,
|
||||
selection: MonacoCodebaseSearchSelection | null,
|
||||
position: IPosition | null
|
||||
): string | null {
|
||||
if (!model) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (selection && !selection.isEmpty()) {
|
||||
const selectedQuery = normalizeSelectedTextForFileSearch(model.getValueInRange(selection))
|
||||
if (selectedQuery) {
|
||||
return selectedQuery
|
||||
}
|
||||
}
|
||||
|
||||
if (!position) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Why: until Orca has semantic LSP references, the editor affordance should
|
||||
// still work from a cursor by searching the visible symbol text in files.
|
||||
return normalizeSelectedTextForFileSearch(model.getWordAtPosition(position)?.word)
|
||||
}
|
||||
Loading…
Reference in New Issue