Timeline 时间线
时间线视图(Timeline View)显示文件的变更历史:Git 提交、文件快照、本地历史。编辑器底部的时间线面板让"这个文件发生过什么"一目了然。
Timeline 是什么
时间线视图位于编辑器底部,针对当前打开的文件展示按时间排序的事件列表:
text
┌─────────────────────────────────┐
│ Timeline │
│ 2028-01-15 14:30 │
│ ● fix: 修复登录跳转 │
│ ● feat: 增加暗色主题 │
│ 2028-01-12 09:00 │
│ ● docs: 更新 README │
└─────────────────────────────────┘Git 扩展内置了提交历史时间线。自定义时间线通过 TimelineProvider 注册。
contributes.timeline 注册
在 package.json 中声明时间线提供者:
json
{
"contributes": {
"timeline": [
{
"id": "myscm.history",
"label": "MySCM 历史",
"itemLabel": "快照",
"enablement": "workspaceFolderCount > 0",
"fileAccess": "reopen",
"when": "resourceScheme == file"
}
]
}
}| 字段 | 说明 |
|---|---|
id | 时间线提供者唯一 ID |
label | 时间线视图中的名称 |
itemLabel | 时间线项的类型名称 |
enablement | 何时可用的条件表达式 |
fileAccess | 时间线是否需要重新打开文件(reopen/current) |
when | 何时显示的条件表达式 |
TimelineItem 属性
时间线项由 TimelineItem 描述:
typescript
interface TimelineItem {
// 唯一 ID(同一时间线内)
id?: string;
// 显示文本
label: string;
// 描述文字(次要信息)
description?: string;
// 时间戳(毫秒),决定排序
timestamp: number;
// 点击项时执行的命令
command?: vscode.Command;
// 上下文值(菜单 when 条件)
contextValue?: string;
// 图标
iconPath?: vscode.ThemeIcon | vscode.Uri;
// 工具提示
tooltip?: string;
// 相对时间等展示提示
detail?: string;
}创建时间线项
typescript
import * as vscode from 'vscode';
function createTimelineItem(
uri: vscode.Uri,
snapshot: Snapshot
): vscode.TimelineItem {
return {
id: snapshot.hash,
label: snapshot.message,
description: snapshot.author,
timestamp: snapshot.timestamp,
tooltip: new vscode.MarkdownString(`**${snapshot.message}**\n\n提交者: ${snapshot.author}`),
iconPath: new vscode.ThemeIcon('history'),
contextValue: 'myscm.snapshot',
command: {
command: 'vscode.diff',
title: '查看快照差异',
arguments: [
snapshot.originalUri, // 原始版本
uri, // 当前文件
`${snapshot.message} ↔ 当前`
]
}
};
}时间线项分组
时间线项通过 TimelineItemGroup 按日期分组显示:
typescript
interface TimelineItemGroup {
id: string;
label: string;
items: vscode.TimelineItem[];
// 是否可折叠
collapsibleState?: vscode.TreeItemCollapsibleState;
}typescript
function groupByDay(items: vscode.TimelineItem[]): vscode.TimelineItemGroup[] {
const groups = new Map<string, vscode.TimelineItem[]>();
for (const item of items) {
const day = new Date(item.timestamp).toISOString().slice(0, 10);
if (!groups.has(day)) {
groups.set(day, []);
}
groups.get(day)!.push(item);
}
return [...groups.entries()].map(([day, dayItems]) => ({
id: `day:${day}`,
label: formatDayLabel(day),
items: dayItems,
collapsibleState: vscode.TreeItemCollapsibleState.Expanded
}));
}
function formatDayLabel(day: string): string {
const d = new Date(day + 'T00:00:00');
const today = new Date().toISOString().slice(0, 10);
if (day === today) return '今天';
const yesterday = new Date(Date.now() - 86400000).toISOString().slice(0, 10);
if (day === yesterday) return '昨天';
return day;
}时间线视图的菜单
json
{
"contributes": {
"menus": {
"timeline/item/context": [
{
"command": "myscm.viewSnapshot",
"when": "timelineItem.contextValue == myscm.snapshot",
"group": "1_actions",
"title": "查看快照"
}
],
"timeline/title": [
{
"command": "myscm.refreshTimeline",
"when": "timelineProviderId == myscm.history",
"group": "navigation",
"title": "刷新"
}
]
}
}
}上下文变量
| 变量 | 说明 |
|---|---|
timelineProviderId | 当前时间线提供者 ID |
timelineItem.contextValue | 选中项的 contextValue |
timelineItem.id | 选中项 ID |
时间线与文件关联
时间线自动绑定当前活动文件,provideTimeline 收到其 URI:
typescript
// 提供者实现骨架
class MyTimelineProvider implements vscode.TimelineProvider {
private _onDidChange = new vscode.EventEmitter<vscode.Uri | undefined>();
readonly onDidChange = this._onDidChange.event;
// 关键方法:为指定文件提供时间线
async provideTimeline(
uri: vscode.Uri, // 当前文件
options: vscode.TimelineOptions, // 分页与过滤
token: vscode.CancellationToken
): Promise<vscode.TimelineResult> {
// 按文件读取历史
const history = await loadHistoryForFile(uri);
// 应用分页
const cursor = options.cursor;
const limit = options.limit ?? 100;
const startIndex = cursor ? findIndex(history, cursor) : 0;
const items = history
.slice(startIndex, startIndex + limit)
.map((h) => createTimelineItem(uri, h));
return {
items,
// 还有更多时返回分页游标
cursor: startIndex + limit < history.length
? String(startIndex + limit)
: undefined
};
}
}注册提供者
typescript
import * as vscode from 'vscode';
const provider = new MyTimelineProvider();
context.subscriptions.push(
vscode.window.registerTimelineProvider('myscm.history', provider)
);registerTimelineProvider 的 id 必须与 contributes.timeline 中声明的一致。
刷新时间线
数据变化时触发 onDidChange:
typescript
// 提交新快照后刷新时间线
async function onCommit() {
await createSnapshot();
// 通知时间线视图重新拉取
this._onDidChange.fire(undefined);
}
// 只刷新某个文件的时间线
this._onDidChange.fire(fileUri);时间线与 SCM 联动
时间线视图可以复用 SCM 数据,展示"提交"与"当前状态"对照:
typescript
function buildMergedTimeline(uri: vscode.Uri): vscode.TimelineItem[] {
const items: vscode.TimelineItem[] = [];
// 历史快照
const snapshots = loadSnapshots(uri);
for (const s of snapshots) {
items.push(createTimelineItem(uri, s));
}
// 未提交的当前更改(置顶显示)
if (isModified(uri)) {
items.unshift({
id: 'working',
label: '未提交的更改',
description: '工作区',
timestamp: Date.now(),
iconPath: new vscode.ThemeIcon('circle-filled'),
contextValue: 'myscm.working',
command: {
command: 'vscode.diff',
title: '查看更改',
arguments: [getLastSnapshotUri(uri), uri, '当前更改']
}
});
}
return items;
}时间线视图丰富了 SCM 的展示维度,下一篇实现完整的时间线提供者。