✨ feat(vscode): 代码阅读预览加 ⌘F 查找栏:整份文件查(折叠着的块里的也算,跳过去时展开),按页面上显示的字不分大小写匹配、中文名也认原名,Enter / ⌘G 跳、Esc 关,高亮直接画在页面上不重画卡片
This commit is contained in:
1 parent
cc3fcda051
commit
bd474986bd
27 files changed
+1833
-114
No files matched your search
@@ -0,0 +1,50 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import * as path from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { FIND, FIND_CONTEXT, FIND_NEXT, FIND_PREVIOUS } from './findCommands'
|
||||
|
||||
interface Manifest {
|
||||
contributes: {
|
||||
commands: { command: string, title: string, category?: string }[]
|
||||
keybindings: { command: string, key: string, mac?: string, when?: string }[]
|
||||
customEditors: { viewType: string }[]
|
||||
menus: { commandPalette: { command: string, when: string }[] }
|
||||
}
|
||||
}
|
||||
|
||||
const manifest = JSON.parse(readFileSync(path.resolve(__dirname, '../../../package.json'), 'utf8')) as Manifest
|
||||
const { commands, keybindings, customEditors, menus } = manifest.contributes
|
||||
const PREVIEW = customEditors[0].viewType
|
||||
const FIND_COMMANDS = [FIND, FIND_NEXT, FIND_PREVIOUS]
|
||||
|
||||
describe('预览里的查找栏:package.json 里的命令和快捷键', () => {
|
||||
it('三条命令叫「代码阅读:查找 / 查找下一处 / 查找上一处」', () => {
|
||||
expect(commands.filter(c => FIND_COMMANDS.includes(c.command)).map(c => `${c.category}:${c.title}`))
|
||||
.toEqual(['代码阅读:查找', '代码阅读:查找下一处', '代码阅读:查找上一处'])
|
||||
})
|
||||
|
||||
it('⌘F(别的系统 Ctrl+F)在代码阅读预览是活动编辑器时打开查找栏;⌘G / ⇧⌘G(别的系统 F3 / Shift+F3)只在查找栏开着时跳', () => {
|
||||
const open = `activeCustomEditorId == ${PREVIEW} && ${FIND_CONTEXT}`
|
||||
expect(keybindings.filter(k => FIND_COMMANDS.includes(k.command))).toEqual([
|
||||
{ command: FIND, key: 'ctrl+f', mac: 'cmd+f', when: `activeCustomEditorId == ${PREVIEW}` },
|
||||
{ command: FIND_NEXT, key: 'f3', mac: 'cmd+g', when: open },
|
||||
{ command: FIND_PREVIOUS, key: 'shift+f3', mac: 'shift+cmd+g', when: open },
|
||||
])
|
||||
})
|
||||
|
||||
it('命令面板里只在代码阅读预览是活动编辑器时出现', () => {
|
||||
expect(menus.commandPalette.filter(item => FIND_COMMANDS.includes(item.command))).toEqual(
|
||||
FIND_COMMANDS.map(command => ({ command, when: `activeCustomEditorId == ${PREVIEW}` })),
|
||||
)
|
||||
})
|
||||
|
||||
it('扩展端不写死键,只转命令;Enter、Esc 这些查找栏自己的键归页面', () => {
|
||||
const sources = ['findCommands.ts', 'previewEditor.ts', 'messages.ts', '../index.ts'].map(file => readFileSync(path.resolve(__dirname, file), 'utf8'))
|
||||
for (const text of sources)
|
||||
expect(text).not.toMatch(/cmd\+|ctrl\+|f3|Escape|'Enter'/)
|
||||
const page = ['FindBar.tsx', 'useFind.ts'].map(file => readFileSync(path.resolve(__dirname, '../../../webview-ui/src/codeReader', file), 'utf8')).join('\n')
|
||||
expect(page).toContain(`'Escape'`)
|
||||
expect(page).toContain(`'Enter'`)
|
||||
expect(page).toContain(`msg.type === 'moveFind'`)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,7 @@
|
||||
/** 预览里的查找栏:三条命令、快捷键 when 子句用的上下文键。键写在 package.json 里,扩展端只把命令转给页面。 */
|
||||
|
||||
export const FIND = 'superpowers.codeReader.find'
|
||||
export const FIND_NEXT = 'superpowers.codeReader.findNext'
|
||||
export const FIND_PREVIOUS = 'superpowers.codeReader.findPrevious'
|
||||
/** 活动的预览里查找栏开着、页面有焦点:查找下一处 / 上一处的快捷键这时才归它。 */
|
||||
export const FIND_CONTEXT = 'superpowers.codeReader.findOpen'
|
||||
@@ -197,6 +197,8 @@ export interface StickyScrollSettings {
|
||||
* rewriteEnd:askId 那一轮重写结束了:done 是模型答完、这一轮记下了,带交回的正文(text)、存下的那条的 id,没写进文件时带原因(error);
|
||||
* failed 带给人看的原因,stopped 是被叫停,这两种都没记、文件没改
|
||||
* moveOccurrence:跳到高亮着的名字的下一处(step 1)/ 上一处(step -1)出现,由这两条命令发给活动的预览
|
||||
* openFind:打开查找栏,由「查找」命令发给活动的预览
|
||||
* moveFind:查找栏里跳到下一处(step 1)/ 上一处(step -1)命中,由「查找下一处 / 上一处」命令发给查找栏开着的预览
|
||||
* displayNames:version 这份文档里显示成中文名的名字,查到一步发一步(先本文件定义的,再别的文件定义的);不是页面上这个版本的不理
|
||||
* diff:这份文件相比对比基准改了什么(审查工作副本里的、从「未提交的更改」点开的有改动的才有,见 [对比基准](vscode/src/codeReader/diff/diffBase.ts#DiffBase)):每发一份文档之后算好了补发,
|
||||
* 基准变了(提交后 HEAD 动了、换了审查的工单、文件有了改动、这个标签页换了从哪儿打开)再发;null 是不比了(退出了审查、改动提交了、从别处点开了)
|
||||
@@ -219,6 +221,8 @@ export type ExtensionToWebview
|
||||
| ({ type: 'stickyScroll' } & StickyScrollSettings)
|
||||
| { type: 'occurrencesFound', id: number, occurrences: Occurrence[] }
|
||||
| { type: 'moveOccurrence', step: 1 | -1 }
|
||||
| { type: 'openFind' }
|
||||
| { type: 'moveFind', step: 1 | -1 }
|
||||
| { type: 'displayNames', version: number, names: ShownName[] }
|
||||
| ({ type: 'filter' } & FilterState)
|
||||
| ({ type: 'codeColors' } & CodeColors)
|
||||
@@ -262,6 +266,7 @@ export type ExtensionToWebview
|
||||
* selectProfile:kind 那种对话换 profile(id 是 profile 名,「默认」是 ''),扩展端记下后把这种的 profiles 发给所有预览
|
||||
* occurrenceNavigation:眼下能不能用键盘在高亮着的名字的各处出现之间跳(有高亮、页面有焦点、焦点不在输入框里),变了才发;
|
||||
* 扩展端按它设 when 子句用的上下文键
|
||||
* findNavigation:眼下查找栏的快捷键归不归这个页面(查找栏开着、页面有焦点),变了才发;扩展端按它设 when 子句用的上下文键
|
||||
* openRepoLink:点了注释里的仓库路径链接,打开 path(从仓库根算起)。symbol 是类名、函数名或「类名.成员名」,没有就只打开文件或在代码阅读侧栏的树上定位目录;
|
||||
* 没有 path 是不带路径的引用(`[Name]`、`[Class.member]`),按这份文件找 symbol:block 是它写在哪块的注释里(见 SymbolReference),
|
||||
* 从那块的声明起一层层往外找本文件定义的,再是导入的(见 symbolLinks.ts)
|
||||
@@ -291,6 +296,7 @@ export type WebviewToExtension
|
||||
| { type: 'deleteQa', anchor: string, entryId: string }
|
||||
| { type: 'selectProfile', kind: ChatKind, id: string }
|
||||
| { type: 'occurrenceNavigation', available: boolean }
|
||||
| { type: 'findNavigation', available: boolean }
|
||||
| { type: 'openRepoLink', path?: string, symbol?: string, block?: string }
|
||||
| { type: 'checkRepoLinks', id: number, paths: string[], references: SymbolReference[] }
|
||||
| { type: 'checkRepoImages', id: number, paths: string[] }
|
||||
|
||||
@@ -9,6 +9,7 @@ import { describe, expect, it, vi } from 'vitest'
|
||||
import { commands, window, workspace } from 'vscode'
|
||||
import { documentMessage } from './documentMessage'
|
||||
import { documentSymbolNodes } from './documentSymbols'
|
||||
import { FIND_CONTEXT } from './findCommands'
|
||||
import { OCCURRENCES_CONTEXT } from './occurrenceNavigation'
|
||||
import { PREVIEW_VIEW_TYPE, PreviewEditorProvider } from './previewEditor'
|
||||
|
||||
@@ -563,3 +564,46 @@ describe('注释里不带路径的引用按它写在哪块的注释里找', () =
|
||||
expect(page.posted()).toContainEqual({ type: 'repoLinkBroken', symbol: 'get_agent_admitted', block: HELPER })
|
||||
})
|
||||
})
|
||||
|
||||
describe('预览里的查找栏', () => {
|
||||
const sent = (page: ReturnType<typeof tab>): ExtensionToWebview[] => page.posted().filter(msg => msg.type === 'openFind' || msg.type === 'moveFind')
|
||||
|
||||
it('「查找」发给活动的预览;「查找下一处 / 上一处」发给查找栏开着、有焦点的那个,都没有时发给活动的;还没 ready 的不发', async () => {
|
||||
const { provider, document } = opening()
|
||||
const first = tab()
|
||||
const second = tab()
|
||||
await provider.open(document.uri as never, {}, async () => provider.resolveCustomTextEditor(document as never, first.panel))
|
||||
await provider.open(document.uri as never, {}, async () => provider.resolveCustomTextEditor(document as never, second.panel))
|
||||
provider.find()
|
||||
expect([...sent(first), ...sent(second)]).toEqual([])
|
||||
|
||||
await first.send({ type: 'ready' })
|
||||
await second.send({ type: 'ready' })
|
||||
first.hide()
|
||||
provider.find()
|
||||
expect(sent(first)).toEqual([])
|
||||
expect(sent(second)).toEqual([{ type: 'openFind' }])
|
||||
|
||||
second.clear()
|
||||
await first.send({ type: 'findNavigation', available: true })
|
||||
provider.moveFind(1)
|
||||
expect(sent(first)).toEqual([{ type: 'moveFind', step: 1 }])
|
||||
expect(sent(second)).toEqual([])
|
||||
|
||||
await first.send({ type: 'findNavigation', available: false })
|
||||
provider.moveFind(-1)
|
||||
expect(sent(second)).toEqual([{ type: 'moveFind', step: -1 }])
|
||||
})
|
||||
|
||||
it('页面报来查找栏开着、有焦点:设上下文键,⌘G 这时才归查找栏;变了才设', async () => {
|
||||
const { provider, document } = opening()
|
||||
const page = tab()
|
||||
await provider.open(document.uri as never, {}, async () => provider.resolveCustomTextEditor(document as never, page.panel))
|
||||
vi.mocked(commands.executeCommand).mockClear()
|
||||
await page.send({ type: 'findNavigation', available: true })
|
||||
await page.send({ type: 'findNavigation', available: true })
|
||||
await page.send({ type: 'findNavigation', available: false })
|
||||
expect(vi.mocked(commands.executeCommand).mock.calls.filter(([command, key]) => command === 'setContext' && key === FIND_CONTEXT))
|
||||
.toEqual([['setContext', FIND_CONTEXT, true], ['setContext', FIND_CONTEXT, false]])
|
||||
})
|
||||
})
|
||||
@@ -44,6 +44,7 @@ import { definitionKind, findDefinitions, languageDefinitions, pickDefinition, r
|
||||
import { DefinitionSources } from './definitionSources'
|
||||
import { documentMessage, pageDocument } from './documentMessage'
|
||||
import { documentSymbolNodes } from './documentSymbols'
|
||||
import { FIND_CONTEXT } from './findCommands'
|
||||
import { NavigationContext, OCCURRENCES_CONTEXT } from './occurrenceNavigation'
|
||||
import { occurrencesOf } from './occurrences'
|
||||
import { languageOf, openingFor } from './openTarget'
|
||||
@@ -80,6 +81,8 @@ export class PreviewEditorProvider implements CustomTextEditorProvider {
|
||||
private readonly requests = new Map<string, OpenRequest>()
|
||||
/** 哪个预览眼下能用键盘在高亮的各处出现之间跳,跟着它设快捷键 when 子句用的上下文键 */
|
||||
readonly navigation = new NavigationContext<Preview>(value => void commands.executeCommand('setContext', OCCURRENCES_CONTEXT, value))
|
||||
/** 哪个预览眼下查找栏开着、有焦点,跟着它设查找下一处 / 上一处快捷键 when 子句用的上下文键 */
|
||||
readonly finding = new NavigationContext<Preview>(value => void commands.executeCommand('setContext', FIND_CONTEXT, value))
|
||||
/** 调用的名字的定义是哪种块:各预览共用,定义所在的文件同一版只解析一次 */
|
||||
readonly definitionKinds: DefinitionKinds
|
||||
|
||||
@@ -153,6 +156,7 @@ export class PreviewEditorProvider implements CustomTextEditorProvider {
|
||||
panel.onDidDispose(() => {
|
||||
this.previews.delete(preview)
|
||||
this.navigation.update(preview, false)
|
||||
this.finding.update(preview, false)
|
||||
preview.dispose()
|
||||
})
|
||||
}
|
||||
@@ -162,6 +166,16 @@ export class PreviewEditorProvider implements CustomTextEditorProvider {
|
||||
this.navigation.target(this.previews, preview => preview.active)?.moveOccurrence(step)
|
||||
}
|
||||
|
||||
/** 「查找」:打开查找栏。发给查找栏开着、有焦点的那个预览,没有就发给活动的预览。 */
|
||||
find(): void {
|
||||
this.finding.target(this.previews, preview => preview.active)?.find()
|
||||
}
|
||||
|
||||
/** 「查找下一处 / 上一处」:发给查找栏开着、有焦点的那个预览,从命令面板运行时发给活动的预览。 */
|
||||
moveFind(step: 1 | -1): void {
|
||||
this.finding.target(this.previews, preview => preview.active)?.moveFind(step)
|
||||
}
|
||||
|
||||
/** 让已打开的预览按当前注册表重发;给了 languageId 就只刷这种语言的。 */
|
||||
refresh(languageId?: string): void {
|
||||
for (const preview of this.previews) {
|
||||
@@ -539,6 +553,18 @@ class Preview {
|
||||
this.post({ type: 'moveOccurrence', step })
|
||||
}
|
||||
|
||||
/** 打开查找栏;页面还没 ready 就算了。 */
|
||||
find(): void {
|
||||
if (this.ready)
|
||||
this.post({ type: 'openFind' })
|
||||
}
|
||||
|
||||
/** 查找栏里跳到下一处 / 上一处命中;页面还没 ready 就算了(还没有查找栏)。 */
|
||||
moveFind(step: 1 | -1): void {
|
||||
if (this.ready)
|
||||
this.post({ type: 'moveFind', step })
|
||||
}
|
||||
|
||||
/** 开着时又被点开(见 [PreviewEditorProvider.open]):这次说了从哪儿打开的就按它重新比,没说的照旧;要滚到哪、要不要聚焦输入框照这次的。 */
|
||||
reopen(request: OpenRequest): void {
|
||||
if (request.from !== undefined) {
|
||||
@@ -744,6 +770,9 @@ class Preview {
|
||||
return
|
||||
case 'occurrenceNavigation':
|
||||
this.owner.navigation.update(this, msg.available)
|
||||
return
|
||||
case 'findNavigation':
|
||||
this.owner.finding.update(this, msg.available)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import { shellCommandRules } from './commands/shellCommands'
|
||||
import { systemCommandRules } from './commands/systemCommands'
|
||||
import { previewDiffBases } from './diff/diffBase'
|
||||
import { BlockFilter } from './editor/blockFilter'
|
||||
import { FIND, FIND_NEXT, FIND_PREVIOUS } from './editor/findCommands'
|
||||
import { NEXT_OCCURRENCE, PREVIOUS_OCCURRENCE } from './editor/occurrenceNavigation'
|
||||
import { languageOf, openingFor } from './editor/openTarget'
|
||||
import { PATH_PREVIEW_SCHEME } from './editor/pathPreview'
|
||||
@@ -212,6 +213,9 @@ export function registerCodeReader(context: ExtensionContext): CodeReaderApi {
|
||||
commands.registerCommand('superpowers.codeReader.showSource', (...args: unknown[]) => switcher.showSource(args)),
|
||||
commands.registerCommand(NEXT_OCCURRENCE, () => previews.moveOccurrence(1)),
|
||||
commands.registerCommand(PREVIOUS_OCCURRENCE, () => previews.moveOccurrence(-1)),
|
||||
commands.registerCommand(FIND, () => previews.find()),
|
||||
commands.registerCommand(FIND_NEXT, () => previews.moveFind(1)),
|
||||
commands.registerCommand(FIND_PREVIOUS, () => previews.moveFind(-1)),
|
||||
)
|
||||
|
||||
const api: CodeReaderApi = registry
|
||||
|
||||
Reference in new issue
Block a user