✨ feat(vscode): 代码阅读的名字高亮更显眼(当前那处描框、别处画细线),⌘↓ / ⌘↑ 在各处之间跳,跳到折叠处自动展开,筛选栏写第几处

This commit is contained in:
cruldra committed 2026-09-26 23:47:43 +08:00
1 parent c5c5d2b517
commit 49f253b80f
18 files changed
+731 -53

No files matched your search

@@ -0,0 +1,85 @@
import type { KeyCombo } from './messages'
/** 在高亮着的名字的各处出现之间跳:两条命令、快捷键的 when 子句用的上下文键。 */
export const NEXT_OCCURRENCE = 'superpowers.codeReader.nextOccurrence'
export const PREVIOUS_OCCURRENCE = 'superpowers.codeReader.previousOccurrence'
/** 活动的预览里能用键盘跳(有高亮、页面有焦点、焦点不在输入框里)。 */
export const OCCURRENCES_CONTEXT = 'superpowers.codeReader.hasOccurrences'
/** VS Code 快捷键写法里的键名 → KeyboardEvent.key;单个字母、数字照写。 */
const KEY_NAMES: Record<string, string> = {
up: 'ArrowUp',
down: 'ArrowDown',
left: 'ArrowLeft',
right: 'ArrowRight',
home: 'Home',
end: 'End',
pageup: 'PageUp',
pagedown: 'PageDown',
}
/** `cmd+down` 这种一下按完的组合 → KeyCombo;`ctrl+k ctrl+x` 这种分两下按的、认不得的键名没有。 */
export function parseKey(text: string): KeyCombo | undefined {
const parts = text.trim().toLowerCase().split('+')
if (parts.some(part => part === '' || /\s/.test(part)))
return undefined
const name = parts.pop() ?? ''
const key = KEY_NAMES[name] ?? (/^[a-z0-9]$/.test(name) ? name : undefined)
const modifiers = new Set(parts)
if (!key || [...modifiers].some(modifier => !['ctrl', 'shift', 'alt', 'cmd', 'meta', 'win'].includes(modifier)))
return undefined
return {
key,
ctrl: modifiers.has('ctrl'),
shift: modifiers.has('shift'),
alt: modifiers.has('alt'),
meta: modifiers.has('cmd') || modifiers.has('meta') || modifiers.has('win'),
}
}
interface Keybinding { command?: unknown, key?: unknown, mac?: unknown, linux?: unknown, win?: unknown }
/**
* package.json 给这两条命令默认绑的键,各平台的都算(扩展可能跑在远程机器上,看不出界面在哪个系统)。
* 用户在「键盘快捷方式」里改绑的读不到:VS Code 没有给扩展查快捷键的接口。
*/
export function navigationKeys(manifest: unknown): KeyCombo[] {
const bindings = (manifest as { contributes?: { keybindings?: Keybinding[] } } | undefined)?.contributes?.keybindings ?? []
const combos = bindings
.filter(binding => binding.command === NEXT_OCCURRENCE || binding.command === PREVIOUS_OCCURRENCE)
.flatMap(binding => [binding.key, binding.mac, binding.linux, binding.win])
.filter((text): text is string => typeof text === 'string')
.map(parseKey)
.filter((combo): combo is KeyCombo => combo !== undefined)
return combos.filter((combo, i) => combos.findIndex(other => JSON.stringify(other) === JSON.stringify(combo)) === i)
}
/**
* 上下文键跟着各预览报来的状态走:有一个预览能跳就是真(同一时刻只有一个页面有焦点),变了才设。
* 预览关掉、页面被销毁(标签页切走)时当它不能跳。
*/
export class NavigationContext<Owner> {
private readonly available = new Set<Owner>()
private value = false
constructor(private readonly set: (value: boolean) => void) {}
update(owner: Owner, available: boolean): void {
if (available)
this.available.add(owner)
else
this.available.delete(owner)
const value = this.available.size > 0
if (value === this.value)
return
this.value = value
this.set(value)
}
/** 命令发给哪个预览:报了能跳的那个;从命令面板运行时页面没焦点,就发给活动的那个。 */
target(owners: Iterable<Owner>, active: (owner: Owner) => boolean): Owner | undefined {
const list = [...owners]
return list.find(owner => this.available.has(owner)) ?? list.find(active)
}
}