✨ feat(vscode): 代码阅读的名字高亮更显眼(当前那处描框、别处画细线),⌘↓ / ⌘↑ 在各处之间跳,跳到折叠处自动展开,筛选栏写第几处
This commit is contained in:
1 parent
c5c5d2b517
commit
49f253b80f
18 files changed
+731
-53
No files matched your search
@@ -1,4 +1,4 @@
|
||||
import type { Occurrence, Segment, WebviewToExtension } from './types'
|
||||
import type { Inline, KeyCombo, Occurrence, Segment, WebviewToExtension } from './types'
|
||||
|
||||
/** 高亮画成哪种底色,和 VS Code 编辑器一样:写入(定义、赋值)用强高亮色,读取和按文字认出的用普通高亮色。 */
|
||||
export type OccurrenceStyle = 'strong' | 'normal'
|
||||
@@ -12,13 +12,9 @@ export function occurrenceStyle(kind: Occurrence['kind']): OccurrenceStyle {
|
||||
* 同时碰上写入和读取的按写入画。没有下标的(译词、提示词、编辑状态的源码)不画。
|
||||
*/
|
||||
export function segmentOccurrence(segment: Segment, occurrences: readonly Occurrence[]): OccurrenceStyle | undefined {
|
||||
const { offset } = segment
|
||||
if (offset === undefined)
|
||||
return undefined
|
||||
const end = offset + segment.text.length
|
||||
let style: OccurrenceStyle | undefined
|
||||
for (const occurrence of occurrences) {
|
||||
if (occurrence.start < end && offset < occurrence.end) {
|
||||
if (overlaps(segment, occurrence)) {
|
||||
style = occurrenceStyle(occurrence.kind)
|
||||
if (style === 'strong')
|
||||
return style
|
||||
@@ -27,6 +23,17 @@ export function segmentOccurrence(segment: Segment, occurrences: readonly Occurr
|
||||
return style
|
||||
}
|
||||
|
||||
/** 段照抄自源码、在源码里占的 [offset, offset + 字数) 和这处出现重叠;没有下标的段不算。 */
|
||||
export function overlaps(segment: Segment, occurrence: Occurrence): boolean {
|
||||
const { offset } = segment
|
||||
return offset !== undefined && occurrence.start < offset + segment.text.length && offset < occurrence.end
|
||||
}
|
||||
|
||||
/** 文字里(连框里)有段和这处出现重叠。 */
|
||||
export function showsOccurrence(pieces: readonly Inline[], occurrence: Occurrence): boolean {
|
||||
return pieces.some(piece => 'role' in piece ? overlaps(piece, occurrence) : showsOccurrence(piece.segments, occurrence))
|
||||
}
|
||||
|
||||
/** 一次点击的情况,useOccurrences 从事件和页面上读出来。 */
|
||||
export interface Click {
|
||||
/** 按着 ⌘ / Ctrl / Alt / Shift:不是单纯的单击(⌘ / Ctrl + 点击归跳到定义) */
|
||||
@@ -62,20 +69,68 @@ export function clickAction(click: Click): ClickAction {
|
||||
/** 眼下高亮着的名字:点中时它的文字(筛选栏上显示)和它在文档里的各处出现。 */
|
||||
export interface Highlight {
|
||||
name: string
|
||||
/** 照源码先后排 */
|
||||
occurrences: readonly Occurrence[]
|
||||
/** 当前那一处(点中的、用键盘跳到的)在 occurrences 里的序号;点中的名字不在回答里时没有 */
|
||||
current?: number
|
||||
}
|
||||
|
||||
/** 段是不是当前那一处出现(点中的、用键盘跳到的)。 */
|
||||
export function isCurrent(segment: Segment, highlight: Highlight): boolean {
|
||||
const current = highlight.current === undefined ? undefined : highlight.occurrences[highlight.current]
|
||||
return current !== undefined && overlaps(segment, current)
|
||||
}
|
||||
|
||||
/** 筛选栏上名字后面写的:有当前那一处时「第几处/共几处」(2/5),没有时「N 处」;看不见的也算在总数里。 */
|
||||
export function occurrenceCount(highlight: Highlight): string {
|
||||
const total = highlight.occurrences.length
|
||||
return highlight.current === undefined ? `${total} 处` : `${highlight.current + 1}/${total}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 往后(step 1)/ 往前(step -1)数,下一处能跳到的出现的序号,到头了从另一头接着数;一处都跳不到时没有。
|
||||
* 从当前那一处(current)数起;没有当前那一处时从源码下标 from(点中的名字)数起。
|
||||
*/
|
||||
export function stepIndex(
|
||||
occurrences: readonly Occurrence[],
|
||||
current: number | undefined,
|
||||
from: number,
|
||||
step: 1 | -1,
|
||||
reachable: (occurrence: Occurrence) => boolean,
|
||||
): number | undefined {
|
||||
const count = occurrences.length
|
||||
// 没有当前那一处时,起点是 from 前面最后一处(往后数)/ 后面第一处(往前数),数的时候跳过它本身所在的位置
|
||||
const start = current ?? (step === 1
|
||||
? occurrences.filter(occurrence => occurrence.start <= from).length - 1
|
||||
: count - occurrences.filter(occurrence => occurrence.start >= from).length)
|
||||
for (let k = 1; k <= count; k++) {
|
||||
const index = (((start + step * k) % count) + count) % count
|
||||
if (reachable(occurrences[index]))
|
||||
return index
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/** 按下的键是不是这个组合:键名(字母不分大小写)和四个修饰键都对上。 */
|
||||
export function matchesKey(event: Pick<KeyboardEvent, 'key' | 'ctrlKey' | 'shiftKey' | 'altKey' | 'metaKey'>, combo: KeyCombo): boolean {
|
||||
return event.key.toLowerCase() === combo.key.toLowerCase()
|
||||
&& event.ctrlKey === combo.ctrl && event.shiftKey === combo.shift && event.altKey === combo.alt && event.metaKey === combo.meta
|
||||
}
|
||||
|
||||
/**
|
||||
* 单击名字高亮它在文档里的各处出现,和编辑器里一样每回点都现问扩展端,回答不留到下次用;
|
||||
* 只认最近一次询问的回答。回答到之前旧高亮留着(点同一个名字的另一处不闪),回答是空的就不高亮。
|
||||
* 高亮着时能用键盘在各处出现之间跳(move),跳到的那处是当前那一处。
|
||||
*/
|
||||
export class OccurrenceHighlight {
|
||||
private version: number | undefined
|
||||
private translatorId: string | undefined
|
||||
/** 最近一次询问的 id;别的回答(点了别处、清掉之后才到的)都丢掉 */
|
||||
private request = 0
|
||||
/** 最近一次询问点中的名字;答过或清掉后没有 */
|
||||
private asked: string | undefined
|
||||
/** 最近一次询问点中的那段名字;答过或清掉后没有 */
|
||||
private asked: Segment | undefined
|
||||
/** 高亮着的名字没有当前那一处时,从这个源码下标(点中的名字)数起跳到哪一处 */
|
||||
private from = 0
|
||||
private current: Highlight | undefined
|
||||
|
||||
constructor(
|
||||
@@ -103,12 +158,29 @@ export class OccurrenceHighlight {
|
||||
this.find(action.offset, action.name)
|
||||
}
|
||||
|
||||
/** 回答照源码先后排好;点中的那处就是当前那一处。 */
|
||||
answer(id: number, occurrences: Occurrence[]): void {
|
||||
if (id !== this.request || this.asked === undefined)
|
||||
const asked = this.asked
|
||||
if (id !== this.request || asked === undefined)
|
||||
return
|
||||
const name = this.asked
|
||||
this.asked = undefined
|
||||
this.show(occurrences.length > 0 ? { name, occurrences } : undefined)
|
||||
const sorted = [...occurrences].sort((a, b) => a.start - b.start || a.end - b.end)
|
||||
const current = sorted.findIndex(occurrence => overlaps(asked, occurrence))
|
||||
this.from = asked.offset ?? 0
|
||||
this.show(sorted.length > 0 ? { name: asked.text, occurrences: sorted, ...(current >= 0 && { current }) } : undefined)
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前那一处换成往后(step 1)/ 往前(step -1)的下一处能跳到的出现(见 stepIndex),返回它;
|
||||
* 没有高亮、一处都跳不到时不动,返回 undefined。
|
||||
*/
|
||||
move(step: 1 | -1, reachable: (occurrence: Occurrence) => boolean): Occurrence | undefined {
|
||||
const highlight = this.current
|
||||
const index = highlight && stepIndex(highlight.occurrences, highlight.current, this.from, step, reachable)
|
||||
if (!highlight || index === undefined)
|
||||
return undefined
|
||||
this.show({ ...highlight, current: index })
|
||||
return highlight.occurrences[index]
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
@@ -120,7 +192,7 @@ export class OccurrenceHighlight {
|
||||
private find(offset: number, name: string): void {
|
||||
if (this.version === undefined)
|
||||
return
|
||||
this.asked = name
|
||||
this.asked = { text: name, role: 'plain', offset }
|
||||
this.post({ type: 'findOccurrences', id: ++this.request, version: this.version, offset })
|
||||
}
|
||||
|
||||
@@ -131,3 +203,53 @@ export class OccurrenceHighlight {
|
||||
this.changed(highlight)
|
||||
}
|
||||
}
|
||||
|
||||
/** 能不能用键盘跳要看的几件事。 */
|
||||
export interface NavigationFacts {
|
||||
/** 有名字高亮着 */
|
||||
highlighted: boolean
|
||||
/** 页面有焦点:按的键才会到这个页面 */
|
||||
focused: boolean
|
||||
/** 焦点在输入框里:⌘↑ / ⌘↓ 这些键归输入框移光标 */
|
||||
typing: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* 眼下能不能用键盘在高亮着的名字的各处出现之间跳:有高亮、页面有焦点、焦点不在输入框里。变了才告诉扩展端,
|
||||
* 扩展端跟着设快捷键 when 子句用的上下文键;页面也按它决定要不要拦下这些键在浏览器里的默认动作(滚到顶、滚到底)。
|
||||
*/
|
||||
export class OccurrenceNavigation {
|
||||
private facts: NavigationFacts
|
||||
private reported: boolean | undefined
|
||||
|
||||
constructor(private readonly post: (msg: WebviewToExtension) => void, initial: Omit<NavigationFacts, 'highlighted'>) {
|
||||
this.facts = { highlighted: false, ...initial }
|
||||
}
|
||||
|
||||
get available(): boolean {
|
||||
const { highlighted, focused, typing } = this.facts
|
||||
return highlighted && focused && !typing
|
||||
}
|
||||
|
||||
/** 改了哪几件事;第一次调用时不管变没变都告诉扩展端(页面重新加载后扩展端记的可能是旧页面的) */
|
||||
update(change: Partial<NavigationFacts>): void {
|
||||
this.facts = { ...this.facts, ...change }
|
||||
this.report()
|
||||
}
|
||||
|
||||
/**
|
||||
* 这个键要不要拦下浏览器的默认动作:能跳、又是那两条命令绑的键。VS Code 把页面上按的每个键都转给自己的快捷键系统,
|
||||
* 但不回头告诉页面有没有命令接了它,浏览器的默认动作又只能在按下的当下拦,所以只能照命令绑的键(keys)当场判断。
|
||||
*/
|
||||
blocks(event: Pick<KeyboardEvent, 'key' | 'ctrlKey' | 'shiftKey' | 'altKey' | 'metaKey'>, keys: readonly KeyCombo[]): boolean {
|
||||
return this.available && keys.some(combo => matchesKey(event, combo))
|
||||
}
|
||||
|
||||
private report(): void {
|
||||
const { available } = this
|
||||
if (available === this.reported)
|
||||
return
|
||||
this.reported = available
|
||||
this.post({ type: 'occurrenceNavigation', available })
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user