♻️ refactor(vscode): 代码阅读不再在页面里拦 ⌘↓ / ⌘↑,快捷键只在「键盘快捷方式」里绑;跳到下一处时把浏览器自己的滚动拉回来,页面不再先滚到底
This commit is contained in:
1 parent
0cad5722bb
commit
1b1299dc1f
10 files changed
+157
-165
No files matched your search
@@ -32,19 +32,6 @@ export interface Occurrence {
|
||||
kind: 'text' | 'read' | 'write'
|
||||
}
|
||||
|
||||
/**
|
||||
* 一个按键组合,按 KeyboardEvent 的写法:key 是 KeyboardEvent.key('ArrowDown'),四个修饰键都要对上。
|
||||
* webview 收到的每个按键 VS Code 都会转给自己的快捷键系统,但不告诉 webview 有没有命令接了它;
|
||||
* 浏览器对这个键的默认动作(⌘↓ 滚到底)又只能在按下的当下拦,所以 webview 要事先知道命令绑的是哪些键。
|
||||
*/
|
||||
export interface KeyCombo {
|
||||
key: string
|
||||
ctrl: boolean
|
||||
shift: boolean
|
||||
alt: boolean
|
||||
meta: boolean
|
||||
}
|
||||
|
||||
export interface DocumentMessage {
|
||||
type: 'document'
|
||||
/** TextDocument.version:webview 靠它判断是不是新一份文档 */
|
||||
@@ -98,7 +85,6 @@ export interface QaMessage {
|
||||
* askProfiles:「问 AI」输入框旁边的 profile 下拉:选项和选中的那项;ready 后发一次,任何一个预览改了都发给所有预览
|
||||
* answer:askId 那个问题的回答长了(text 是到现在为止的全文)、或换了在做的事
|
||||
* answerEnd:askId 那个问题答完了:done 带最终的回答和存下的那条的 id,failed 带给人看的原因,stopped 是被叫停
|
||||
* occurrenceKeys:「跳到下一处 / 上一处高亮」这两条命令在 package.json 里默认绑的键;ready 后发一次,见 KeyCombo
|
||||
* moveOccurrence:跳到高亮着的名字的下一处(step 1)/ 上一处(step -1)出现,由这两条命令发给活动的预览
|
||||
*/
|
||||
export type ExtensionToWebview
|
||||
@@ -106,7 +92,6 @@ export type ExtensionToWebview
|
||||
| ({ type: 'reveal' } & Reveal)
|
||||
| { type: 'definitionProbed', id: number, found: boolean }
|
||||
| { type: 'occurrencesFound', id: number, occurrences: Occurrence[] }
|
||||
| { type: 'occurrenceKeys', keys: KeyCombo[] }
|
||||
| { type: 'moveOccurrence', step: 1 | -1 }
|
||||
| { type: 'filter', excluded: Excluded }
|
||||
| QaMessage
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import * as path from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { NavigationContext, navigationKeys, NEXT_OCCURRENCE, OCCURRENCES_CONTEXT, parseKey, PREVIOUS_OCCURRENCE } from './occurrenceNavigation'
|
||||
import { NavigationContext, NEXT_OCCURRENCE, OCCURRENCES_CONTEXT, PREVIOUS_OCCURRENCE } from './occurrenceNavigation'
|
||||
|
||||
interface Manifest {
|
||||
contributes: {
|
||||
@@ -37,33 +37,11 @@ describe('跳到下一处 / 上一处高亮:package.json 里的命令和快捷
|
||||
])
|
||||
})
|
||||
|
||||
it('页面要拦的键取自这两条命令默认绑的键,各平台的都算、重复的去掉', () => {
|
||||
expect(navigationKeys(manifest)).toEqual([
|
||||
{ key: 'ArrowDown', ctrl: true, shift: false, alt: false, meta: false },
|
||||
{ key: 'ArrowDown', ctrl: false, shift: false, alt: false, meta: true },
|
||||
{ key: 'ArrowUp', ctrl: true, shift: false, alt: false, meta: false },
|
||||
{ key: 'ArrowUp', ctrl: false, shift: false, alt: false, meta: true },
|
||||
])
|
||||
expect(navigationKeys({ contributes: { keybindings: [
|
||||
{ command: NEXT_OCCURRENCE, key: 'alt+j', linux: 'alt+j', win: 'ctrl+k ctrl+j' },
|
||||
{ command: 'other', key: 'cmd+down' },
|
||||
] } })).toEqual([{ key: 'j', ctrl: false, shift: false, alt: true, meta: false }])
|
||||
expect(navigationKeys(undefined)).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('快捷键写法 → 按键组合', () => {
|
||||
it('修饰键、方向键、字母;cmd / meta / win 都是 meta', () => {
|
||||
expect(parseKey('cmd+down')).toEqual({ key: 'ArrowDown', ctrl: false, shift: false, alt: false, meta: true })
|
||||
expect(parseKey('Ctrl+Shift+PageUp')).toEqual({ key: 'PageUp', ctrl: true, shift: true, alt: false, meta: false })
|
||||
expect(parseKey('win+k')).toEqual({ key: 'k', ctrl: false, shift: false, alt: false, meta: true })
|
||||
})
|
||||
|
||||
it('分两下按的、认不得的键名、认不得的修饰键没有', () => {
|
||||
expect(parseKey('ctrl+k ctrl+x')).toBeUndefined()
|
||||
expect(parseKey('ctrl+f5')).toBeUndefined()
|
||||
expect(parseKey('hyper+down')).toBeUndefined()
|
||||
expect(parseKey('ctrl+')).toBeUndefined()
|
||||
it('键只在 package.json 里绑,扩展、页面的代码里都不写死键:用户改绑后原来的键照常', () => {
|
||||
const sources = ['occurrenceNavigation.ts', 'previewEditor.ts', 'messages.ts', '../index.ts'].map(file => readFileSync(path.resolve(__dirname, file), 'utf8'))
|
||||
const webview = ['occurrences.ts', 'useOccurrences.ts', 'types.ts'].map(file => readFileSync(path.resolve(__dirname, '../../../webview-ui/src/codeReader', file), 'utf8'))
|
||||
for (const text of [...sources, ...webview])
|
||||
expect(text).not.toMatch(/ArrowDown|ArrowUp|cmd\+|ctrl\+|KeyCombo|keybindings|preventDefault/)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import type { KeyCombo } from './messages'
|
||||
|
||||
/** 在高亮着的名字的各处出现之间跳:两条命令、快捷键的 when 子句用的上下文键。 */
|
||||
|
||||
export const NEXT_OCCURRENCE = 'superpowers.codeReader.nextOccurrence'
|
||||
@@ -7,54 +5,6 @@ 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)
|
||||
}
|
||||
|
||||
/**
|
||||
* 上下文键跟着各预览报来的状态走:有一个预览能跳就是真(同一时刻只有一个页面有焦点),变了才设。
|
||||
* 预览关掉、页面被销毁(标签页切走)时当它不能跳。
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { CodeReaderRegistry } from '../registry'
|
||||
import type { TreeSitterRuntime } from '../runtime'
|
||||
import type { BlockFilter } from './blockFilter'
|
||||
import type { Definition, FoundLocation } from './definitions'
|
||||
import type { ExtensionToWebview, KeyCombo, Reveal, WebviewToExtension } from './messages'
|
||||
import type { ExtensionToWebview, Reveal, WebviewToExtension } from './messages'
|
||||
import { commands, Range, window, workspace, WorkspaceEdit } from 'vscode'
|
||||
import { logger } from '../../logging/logger'
|
||||
import { webviewHtml, webviewUiRoot } from '../../webview/html'
|
||||
@@ -46,8 +46,6 @@ export class PreviewEditorProvider implements CustomTextEditorProvider {
|
||||
readonly resolveReference: (reference: RuleReference) => Promise<RuleLocation[]>,
|
||||
/** 问 AI:各预览共用,同一份文件开在几处时问答、正在答的问题都一样 */
|
||||
readonly qa: CodeQa,
|
||||
/** 「跳到下一处 / 上一处高亮」默认绑的键:页面在这些键上拦下浏览器自己的滚动 */
|
||||
readonly occurrenceKeys: readonly KeyCombo[],
|
||||
) {}
|
||||
|
||||
resolveCustomTextEditor(document: TextDocument, panel: WebviewPanel): void {
|
||||
@@ -282,7 +280,6 @@ class Preview {
|
||||
this.ready = true
|
||||
// 先于文档发:第一份文档按筛选画,不会先整份露出来再藏
|
||||
this.post({ type: 'filter', excluded: this.filter.excluded })
|
||||
this.post({ type: 'occurrenceKeys', keys: [...this.owner.occurrenceKeys] })
|
||||
this.postQa()
|
||||
void this.postProfiles()
|
||||
await this.push()
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { CodeReaderApi, RuleLocation, RuleReference } from './contract'
|
||||
import { commands, window, workspace } from 'vscode'
|
||||
import { logger } from '../logging/logger'
|
||||
import { BlockFilter } from './editor/blockFilter'
|
||||
import { navigationKeys, NEXT_OCCURRENCE, PREVIOUS_OCCURRENCE } from './editor/occurrenceNavigation'
|
||||
import { NEXT_OCCURRENCE, PREVIOUS_OCCURRENCE } from './editor/occurrenceNavigation'
|
||||
import { PREVIEW_VIEW_TYPE, PreviewEditorProvider } from './editor/previewEditor'
|
||||
import { showPreview, showSource } from './editor/switchEditor'
|
||||
import { registerCopyCommands } from './explorer/copyCommands'
|
||||
@@ -66,7 +66,7 @@ export function registerCodeReader(context: ExtensionContext): CodeReaderApi {
|
||||
}, report))
|
||||
const resolve = (reference: RuleReference): Promise<RuleLocation[]> => resolveReference(reference, registry.libraryRules(), indexes, report)
|
||||
const qa = new CodeQa(context.globalState)
|
||||
const previews = new PreviewEditorProvider(context.extensionUri, registry, runtime, new BlockFilter(context.workspaceState), resolve, qa, navigationKeys(context.extension.packageJSON))
|
||||
const previews = new PreviewEditorProvider(context.extensionUri, registry, runtime, new BlockFilter(context.workspaceState), resolve, qa)
|
||||
const notes = new FileNotes()
|
||||
const files = new FileTreeProvider(OPEN_COMMAND, notes)
|
||||
|
||||
|
||||
Reference in new issue
Block a user