🐛 fix(vscode): 调用树弹框路径太长时不再挤乱标题、溢出卡片:文件名在前、目录淡色省略
This commit is contained in:
1 parent
4d29b6aab8
commit
6cf053dcd0
3 files changed
+119
-19
No files matched your search
@@ -8,7 +8,7 @@ import { act, useState } from 'react'
|
|||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client'
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
import { BlockLevel } from './BlockTree'
|
import { BlockLevel } from './BlockTree'
|
||||||
import { CallTreeDialog } from './CallTreeDialog'
|
import { CallTreeDialog, splitWhere } from './CallTreeDialog'
|
||||||
|
|
||||||
vi.mock('./useOccurrences', async () => {
|
vi.mock('./useOccurrences', async () => {
|
||||||
const { createContext } = await import('react')
|
const { createContext } = await import('react')
|
||||||
@@ -155,7 +155,7 @@ describe('调用树弹框', () => {
|
|||||||
expect(dialog).not.toBeNull()
|
expect(dialog).not.toBeNull()
|
||||||
expect(dialog!.textContent).toContain('调用树')
|
expect(dialog!.textContent).toContain('调用树')
|
||||||
expect(dialog!.textContent).toContain('mask_phone')
|
expect(dialog!.textContent).toContain('mask_phone')
|
||||||
expect(dialog!.textContent).toContain('utils/phone.py:28')
|
expect(dialog!.querySelector('.cr-call-title [data-tooltip="utils/phone.py:28"]')).not.toBeNull()
|
||||||
expect(dialog!.textContent).not.toContain('读取用户资料')
|
expect(dialog!.textContent).not.toContain('读取用户资料')
|
||||||
expect(dialog!.querySelector('[role="tablist"]')).toBeNull()
|
expect(dialog!.querySelector('[role="tablist"]')).toBeNull()
|
||||||
|
|
||||||
@@ -274,4 +274,39 @@ describe('调用树弹框', () => {
|
|||||||
await act(async () => rows[1].click())
|
await act(async () => rows[1].click())
|
||||||
expect(jumps).toEqual([{ uri: 'file:///web/src/lib/agents.ts', line: 10, character: 20 }])
|
expect(jumps).toEqual([{ uri: 'file:///web/src/lib/agents.ts', line: 10, character: 20 }])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('标题和卡片的位置拆成「文件名:行号」和淡色的所在目录,悬停看完整路径;卡片第一行放名字和 detail,第二行只放位置;每层缩进 16px', async () => {
|
||||||
|
const where = 'backend/src/luffy_agent/agent/runtime/runner.py:1325'
|
||||||
|
const run = node('run', {
|
||||||
|
detail: 'class SessionRunner (runner.py)',
|
||||||
|
where,
|
||||||
|
children: [node('mask_phone', { current: true, where: 'phone.py:28' })],
|
||||||
|
})
|
||||||
|
await render(<CallTreeDialog kind="function" name="run" where={where} status="ready" trees={[{ id: 'run', name: 'run', hits: 1, root: run }]} onClose={() => {}} onJump={() => {}} />)
|
||||||
|
const title = document.body.querySelector('.cr-call-title')!
|
||||||
|
const place = title.querySelector<HTMLElement>('[data-tooltip]')!
|
||||||
|
expect(place.dataset.tooltip).toBe(where)
|
||||||
|
expect(place.querySelector('.cr-call-file')!.textContent).toBe('runner.py:1325')
|
||||||
|
expect(place.querySelector('.cr-call-dir')!.textContent).toBe('backend/src/luffy_agent/agent/runtime')
|
||||||
|
|
||||||
|
const [outer, inner] = [...document.body.querySelectorAll('button.cr-call-card')]
|
||||||
|
const lines = (card: Element) => [...card.children].map(line => [...line.querySelectorAll('.cr-call-name, .cr-call-detail, .cr-call-pill, .cr-call-file, .cr-call-dir')].map(part => part.textContent))
|
||||||
|
expect(lines(outer)).toEqual([['run', 'class SessionRunner (runner.py)'], ['runner.py:1325', 'backend/src/luffy_agent/agent/runtime']])
|
||||||
|
expect(outer.children[1].getAttribute('data-tooltip')).toBe(where)
|
||||||
|
expect(lines(inner)).toEqual([['mask_phone', '当前'], ['phone.py:28']])
|
||||||
|
|
||||||
|
const rows = [...document.body.querySelectorAll<HTMLElement>('.cr-call-row')]
|
||||||
|
expect(rows.map(row => row.style.paddingLeft)).toEqual(['0px', '16px'])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('位置拆成文件名和所在目录', () => {
|
||||||
|
it('按最后一个 / 拆:「文件名:行号」和它所在的目录', () => {
|
||||||
|
expect(splitWhere('backend/src/luffy_agent/agent/runtime/runner.py:1325')).toEqual({ file: 'runner.py:1325', dir: 'backend/src/luffy_agent/agent/runtime' })
|
||||||
|
expect(splitWhere('web/src/lib/agents.ts:11')).toEqual({ file: 'agents.ts:11', dir: 'web/src/lib' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('没有目录时只有文件名', () => {
|
||||||
|
expect(splitWhere('runner.py:12')).toEqual({ file: 'runner.py:12', dir: '' })
|
||||||
|
})
|
||||||
})
|
})
|
||||||
@@ -61,7 +61,7 @@ export function CallTreeDialog({ kind, badge, name, where, status, trees, contra
|
|||||||
<div className="cr-call-title">
|
<div className="cr-call-title">
|
||||||
<span className="cr-call-heading">调用树</span>
|
<span className="cr-call-heading">调用树</span>
|
||||||
{name && <span className="cr-call-symbol">{name}</span>}
|
{name && <span className="cr-call-symbol">{name}</span>}
|
||||||
{where && <span className="cr-call-loc">{where}</span>}
|
{where && <CallWhere where={where} />}
|
||||||
<button type="button" className="cr-tool cr-call-close" aria-label="关闭" onClick={close}>
|
<button type="button" className="cr-tool cr-call-close" aria-label="关闭" onClick={close}>
|
||||||
<X size={16} strokeWidth={1.8} />
|
<X size={16} strokeWidth={1.8} />
|
||||||
</button>
|
</button>
|
||||||
@@ -117,30 +117,30 @@ function ContractNote({ miss }: { miss: ContractMiss }): ReactElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const NODE_ICONS: Record<string, typeof Braces> = { class: Box, request: Send }
|
const NODE_ICONS: Record<string, typeof Braces> = { class: Box, request: Send }
|
||||||
|
/** 每深一层往右缩这么多:六七层深时卡片还够宽 */
|
||||||
|
const INDENT_PX = 16
|
||||||
|
|
||||||
function CallNodes({ node, depth, path, onJump }: { node: CallTreeNode, depth: number, path: string, onJump: (jump: CallJump) => void }): ReactElement {
|
function CallNodes({ node, depth, path, onJump }: { node: CallTreeNode, depth: number, path: string, onJump: (jump: CallJump) => void }): ReactElement {
|
||||||
const Icon = NODE_ICONS[node.kind] ?? Braces
|
const Icon = NODE_ICONS[node.kind] ?? Braces
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="cr-call-row" style={{ paddingLeft: depth * 22 }}>
|
<div className="cr-call-row" style={{ paddingLeft: depth * INDENT_PX }}>
|
||||||
<span className="cr-call-guide" />
|
<span className="cr-call-guide" />
|
||||||
<button type="button" className="cr-call-card" data-current={node.current ? '' : undefined} onClick={() => onJump(node.jump)}>
|
<button type="button" className="cr-call-card" data-current={node.current ? '' : undefined} onClick={() => onJump(node.jump)}>
|
||||||
<span className="cr-call-line">
|
<span className="cr-call-line">
|
||||||
<Icon size={14} strokeWidth={1.8} aria-hidden />
|
<Icon size={14} strokeWidth={1.8} aria-hidden />
|
||||||
<span className="cr-call-name">{node.name}</span>
|
<span className="cr-call-name">{node.name}</span>
|
||||||
|
{node.detail && <span className="cr-call-detail">{node.detail}</span>}
|
||||||
{node.current && <span className="cr-call-pill">当前</span>}
|
{node.current && <span className="cr-call-pill">当前</span>}
|
||||||
</span>
|
</span>
|
||||||
<span className="cr-call-sub">
|
<CallWhere where={node.where} />
|
||||||
{node.detail && <span>{node.detail}</span>}
|
|
||||||
<span className="cr-call-loc">{node.where}</span>
|
|
||||||
</span>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{node.children.map((child, index) => (
|
{node.children.map((child, index) => (
|
||||||
<CallNodes key={`${path}/${index}`} node={child} depth={depth + 1} path={`${path}/${index}`} onJump={onJump} />
|
<CallNodes key={`${path}/${index}`} node={child} depth={depth + 1} path={`${path}/${index}`} onJump={onJump} />
|
||||||
))}
|
))}
|
||||||
{node.otherCalls !== undefined && node.otherCalls > 0 && (
|
{node.otherCalls !== undefined && node.otherCalls > 0 && (
|
||||||
<div className="cr-call-row" style={{ paddingLeft: (depth + 1) * 22 }}>
|
<div className="cr-call-row" style={{ paddingLeft: (depth + 1) * INDENT_PX }}>
|
||||||
<span className="cr-call-guide" data-folded="" />
|
<span className="cr-call-guide" data-folded="" />
|
||||||
<span className="cr-call-folded">{`另外 ${node.otherCalls} 处调用`}</span>
|
<span className="cr-call-folded">{`另外 ${node.otherCalls} 处调用`}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -148,3 +148,20 @@ function CallNodes({ node, depth, path, onJump }: { node: CallTreeNode, depth: n
|
|||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 位置照 VS Code 快速打开的样子摆:「文件名:行号」在前,所在目录淡色跟在后面、放不下末尾省略,悬停看完整路径。 */
|
||||||
|
function CallWhere({ where }: { where: string }): ReactElement {
|
||||||
|
const { file, dir } = splitWhere(where)
|
||||||
|
return (
|
||||||
|
<span className="cr-call-where" data-tooltip={where}>
|
||||||
|
<span className="cr-call-file">{file}</span>
|
||||||
|
{dir && <span className="cr-call-dir">{dir}</span>}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** `目录/文件名:行号` 按最后一个 `/` 拆成文件名(带行号)和所在目录;没有目录时 dir 是空的。 */
|
||||||
|
export function splitWhere(where: string): { file: string, dir: string } {
|
||||||
|
const slash = where.lastIndexOf('/')
|
||||||
|
return { file: where.slice(slash + 1), dir: where.slice(0, Math.max(slash, 0)) }
|
||||||
|
}
|
||||||
@@ -1312,7 +1312,7 @@ del.cr-diff-word {
|
|||||||
.cr-call-dialog {
|
.cr-call-dialog {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: min(640px, 100%);
|
width: min(880px, 100%);
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 1px solid var(--vscode-editorWidget-border, var(--vscode-widget-border, transparent));
|
border: 1px solid var(--vscode-editorWidget-border, var(--vscode-widget-border, transparent));
|
||||||
@@ -1321,23 +1321,56 @@ del.cr-diff-word {
|
|||||||
color: var(--vscode-editorWidget-foreground, var(--vscode-foreground));
|
color: var(--vscode-editorWidget-foreground, var(--vscode-foreground));
|
||||||
box-shadow: 0 8px 28px var(--vscode-widget-shadow);
|
box-shadow: 0 8px 28px var(--vscode-widget-shadow);
|
||||||
}
|
}
|
||||||
|
/* 标题一行:「调用树」和符号名整个留着,位置占剩下的宽度 */
|
||||||
.cr-call-title {
|
.cr-call-title {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
height: 44px;
|
min-height: 44px;
|
||||||
padding: 0 14px;
|
padding: 8px 14px;
|
||||||
border-bottom: 1px solid var(--vscode-widget-border, var(--vscode-panel-border, transparent));
|
border-bottom: 1px solid var(--vscode-widget-border, var(--vscode-panel-border, transparent));
|
||||||
}
|
}
|
||||||
.cr-call-heading {
|
.cr-call-heading {
|
||||||
|
flex-shrink: 0;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.cr-call-symbol,
|
.cr-call-symbol,
|
||||||
.cr-call-name {
|
.cr-call-name {
|
||||||
color: var(--kind);
|
color: var(--kind);
|
||||||
font-family: var(--vscode-editor-font-family, monospace);
|
font-family: var(--vscode-editor-font-family, monospace);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.cr-call-symbol {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
/* 位置:文件名先放全,目录拿剩下的宽度、放不下末尾省略;文件名自己也放不下时才省略 */
|
||||||
|
.cr-call-where {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 0;
|
||||||
|
font-family: var(--vscode-editor-font-family, monospace);
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
.cr-call-file,
|
||||||
|
.cr-call-dir {
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.cr-call-file {
|
||||||
|
flex: 0 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
color: var(--vscode-foreground);
|
||||||
|
}
|
||||||
|
.cr-call-dir {
|
||||||
|
flex: 1 1 0;
|
||||||
|
min-width: 0;
|
||||||
|
color: var(--vscode-disabledForeground, var(--vscode-descriptionForeground));
|
||||||
}
|
}
|
||||||
.cr-call-loc {
|
.cr-call-loc {
|
||||||
color: var(--vscode-disabledForeground, var(--vscode-descriptionForeground));
|
color: var(--vscode-disabledForeground, var(--vscode-descriptionForeground));
|
||||||
@@ -1349,6 +1382,7 @@ del.cr-diff-word {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 24px;
|
width: 24px;
|
||||||
|
flex-shrink: 0;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
border: 0;
|
border: 0;
|
||||||
@@ -1447,19 +1481,33 @@ del.cr-diff-word {
|
|||||||
border-color: var(--kind);
|
border-color: var(--kind);
|
||||||
background: color-mix(in srgb, var(--kind) 14%, transparent);
|
background: color-mix(in srgb, var(--kind) 14%, transparent);
|
||||||
}
|
}
|
||||||
.cr-call-line,
|
/* 卡片第一行:图标、名字、detail 占名字后面剩下的宽度,「当前」靠右;第二行只放位置 */
|
||||||
.cr-call-sub {
|
.cr-call-line {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
|
||||||
.cr-call-line {
|
|
||||||
color: var(--kind);
|
color: var(--kind);
|
||||||
}
|
}
|
||||||
.cr-call-sub {
|
.cr-call-line > svg {
|
||||||
margin-top: 2px;
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.cr-call-name {
|
||||||
|
overflow: hidden;
|
||||||
|
flex: 0 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.cr-call-detail {
|
||||||
|
overflow: hidden;
|
||||||
|
flex: 1 1 0;
|
||||||
|
min-width: 0;
|
||||||
color: var(--vscode-descriptionForeground);
|
color: var(--vscode-descriptionForeground);
|
||||||
font-size: 12px;
|
font-size: 11px;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.cr-call-card .cr-call-where {
|
||||||
|
margin-top: 2px;
|
||||||
}
|
}
|
||||||
.cr-call-pill {
|
.cr-call-pill {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
Reference in new issue
Block a user