-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathextension.ts
More file actions
356 lines (339 loc) · 18.4 KB
/
Copy pathextension.ts
File metadata and controls
356 lines (339 loc) · 18.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
import * as vscode from 'vscode';
import { NullChangelistGrouper } from './agent/grouper';
import { NullCommitMessageProvider } from './agent/commit-message';
import { NullConflictResolver } from './agent/conflict';
import { NullLlmProvider } from './agent/llm-provider';
import { NullPreCommitInspector } from './agent/pre-commit';
import { registerChangesCommands } from './adapter/commands';
import { ChangelistRegistry } from './adapter/changelist-registry';
import { BranchFavorites } from './adapter/branch-favorites';
import { CommitService } from './adapter/commit/commit-service';
import { BranchesTreeProvider } from './adapter/tree/branches-tree';
import { LogWebviewProvider } from './adapter/webview/log-webview';
import { registerHistoryCommands } from './adapter/history-commands';
import { registerStashCommands } from './adapter/stash-commands';
import { registerGitCliCommands } from './adapter/git-cli-commands';
import { registerPartialCommands } from './adapter/partial-commands';
import { registerAdvancedCommands } from './adapter/advanced-commands';
import { registerRemoteCommands } from './adapter/remote-commands';
import { StashTreeProvider } from './adapter/tree/stash-tree';
import { WorktreeTreeProvider } from './adapter/tree/worktree-tree';
import { registerWorktreeCommands } from './adapter/worktree-commands';
import { CommitWebviewProvider } from './adapter/webview/commit-webview';
import { showGitConsole, disposeGitConsole } from './infra/git-console';
import { InlineCommitCodeLensProvider, registerInlineCommitCommand } from './adapter/editor/inline-commit-codelens';
import { BlameAnnotationController } from './adapter/editor/blame-annotation';
import { ShelfService, ShelfTreeProvider, registerShelfCommands } from './adapter/shelf';
import { RebaseWebview } from './adapter/webview/rebase-webview';
import { MergeEditorWebview, registerMergeCommands } from './adapter/webview/merge-editor';
import { registerMiscCommands } from './adapter/misc-commands';
import { registerClaudeCommands } from './adapter/claude-commands';
import { registerRepositorySelectionCommand } from './adapter/repository-selection';
import { getGitApi } from './adapter/git-api';
import { GitRepositoryService } from './adapter/git-repository-service';
import { GitHubAuth } from './adapter/ci/github-auth';
import { GitHubCiService } from './adapter/ci/github-ci-service';
import { createLogger } from './infra/logger';
/** 无 git 时的占位树 provider(空树,触发 viewsWelcome)。原 EmptyChangesProvider 随 Changes 视图移除后内联于此。 */
class EmptyTreeProvider implements vscode.TreeDataProvider<never> {
getTreeItem(): vscode.TreeItem {
return new vscode.TreeItem('');
}
getChildren(): never[] {
return [];
}
}
/** git 扩展不可用时 webview 视图(Commit/Graph)的占位 provider:静态说明页(无脚本,CSP 最小化)。 */
class UnavailableViewProvider implements vscode.WebviewViewProvider {
constructor(private readonly viewName: string) {}
resolveWebviewView(view: vscode.WebviewView): void {
view.webview.options = { enableScripts: false, localResourceRoots: [] };
view.webview.html = `<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta http-equiv="Content-Security-Policy" content="default-src 'none'">
<style>body { font-family: var(--vscode-font-family); color: var(--vscode-foreground); background: var(--vscode-sideBar-background); padding: 14px 16px; font-size: var(--vscode-font-size); }</style>
</head>
<body><strong>${this.viewName}</strong><p style="color: var(--vscode-descriptionForeground)">The built-in Git extension is unavailable, so ${this.viewName} cannot load. Enable the Git extension and reload the window.</p></body>
</html>`;
}
}
/**
* 扩展入口。仅做装配(DI 注册),业务逻辑下沉到 engine/adapter 层。
* 返回 `{ service }` 供集成测试程序化驱动仓库切换(vscode.git 同款导出模式)。
*/
export async function activate(
context: vscode.ExtensionContext,
): Promise<{ service: GitRepositoryService } | void> {
const logger = createLogger();
// LogOutputChannel 随扩展释放(懒创建,无日志的会话零开销)。
context.subscriptions.push(logger, new vscode.Disposable(disposeGitConsole));
logger.info('Hyper Git activated');
const llm = new NullLlmProvider();
context.subscriptions.push(
vscode.commands.registerCommand('hyperGit.showVersion', () => {
const version: string = context.extension.packageJSON.version;
vscode.window.showInformationMessage(`Hyper Git v${version}`);
logger.info(`version=${version}, llmSource=${llm.sourceId}`);
}),
);
const api = await getGitApi();
if (!api) {
logger.warn('vscode.git API 不可用,视图保持空状态');
const empty = new EmptyTreeProvider();
context.subscriptions.push(
// 全部声明的树视图注册空 provider:未注册的视图显示 "no tree view registered" 报错而非 viewsWelcome。
vscode.window.registerTreeDataProvider('hyperGit.branches', empty),
vscode.window.registerTreeDataProvider('hyperGit.stash', empty),
vscode.window.registerTreeDataProvider('hyperGit.worktrees', empty),
vscode.window.createTreeView('hyperGit.shelf', { treeDataProvider: empty }),
// webview 视图渲染静态占位说明页。
vscode.window.registerWebviewViewProvider('hyperGit.commit', new UnavailableViewProvider('Commit')),
vscode.window.registerWebviewViewProvider('hyperGit.log', new UnavailableViewProvider('Graph')),
);
return;
}
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? 'default';
const service = new GitRepositoryService(api, context.workspaceState);
const registry = new ChangelistRegistry(context.workspaceState, service.repoRoot ?? workspaceRoot);
const favorites = new BranchFavorites(context.workspaceState, service.repoRoot ?? workspaceRoot);
const githubAuth = new GitHubAuth(context.subscriptions);
const ciService = new GitHubCiService(service, githubAuth, logger);
// AI 接缝注入(Null 实现,M5 替换为真实 provider)
const commit = new CommitService(context, service, context.workspaceState, {
llm,
commitMessage: new NullCommitMessageProvider(),
preCommit: new NullPreCommitInspector(),
grouper: new NullChangelistGrouper(),
conflict: new NullConflictResolver(),
});
const commitView = new CommitWebviewProvider(service, registry, commit, context.workspaceState);
const logTree = new LogWebviewProvider(service, ciService, context.workspaceState);
const branchesTree = new BranchesTreeProvider(
service,
favorites,
context.workspaceState,
service.repoRoot ?? workspaceRoot,
);
// Branches 视图启用多选(canSelectMany 仅 createTreeView 支持,registerTreeDataProvider 不支持);
// 多选后批量操作(删除分支/标签、复制引用、收藏)作用于整个选区。showCollapseAll 供前缀分组树一键折叠。
const branchesView = vscode.window.createTreeView('hyperGit.branches', {
treeDataProvider: branchesTree,
canSelectMany: true,
showCollapseAll: true,
});
const stashTree = new StashTreeProvider(service);
const worktreeTree = new WorktreeTreeProvider(service);
const inlineLens = new InlineCommitCodeLensProvider(service);
const blame = new BlameAnnotationController(service);
const shelfService = new ShelfService(service, context.globalStorageUri.fsPath);
const shelfTree = new ShelfTreeProvider(shelfService);
// 未提交数角标承载:隐藏 TreeView(package.json 中 when:false,永不渲染)。createTreeView 于 activate
// 即实例化视图对象,其 badge 聚合到容器图标/页签——规避 WebviewView.badge 在 resolveWebviewView(用户
// 至少打开过一次视图)前无法显示的已知限制(microsoft/vscode#164974、#146330)。v0.0.13 起容器由活动栏
// 迁至底部 Panel(issue #13):角标仅在 Panel 展开时可见,活动栏「始终可见」特性失效(用户已接受 trade-off)。
// 复用占位 EmptyTreeProvider(空树)。
const badgeView = vscode.window.createTreeView('hyperGit.changesBadge', {
treeDataProvider: new EmptyTreeProvider(),
});
// Stash/Worktrees 统一 createTreeView(原 registerTreeDataProvider 拿不到 TreeView 句柄):
// canSelectMany 启用批量 Drop/Remove;provider 加载失败经 TreeView.message 以视图内联错误呈现。
const stashView = vscode.window.createTreeView('hyperGit.stash', {
treeDataProvider: stashTree,
canSelectMany: true,
});
const worktreesView = vscode.window.createTreeView('hyperGit.worktrees', {
treeDataProvider: worktreeTree,
canSelectMany: true,
});
stashTree.setViewMessageSink((message) => {
stashView.message = message;
});
worktreeTree.setViewMessageSink((message) => {
worktreesView.message = message;
});
const focusCommitView = (): void => {
void vscode.commands.executeCommand('hyperGit.commit.focus');
};
// Branches 「平铺 ⇄ 树」切换:context key 驱动工具栏两个按钮互斥显示(见 package.json when 子句),
// 初值取自 provider 的持久化状态;命令翻转分组态并同步 context key。
const setBranchesGroupingContext = (v: boolean): void => {
void vscode.commands.executeCommand('setContext', 'hyperGit.branchesGrouping', v);
};
setBranchesGroupingContext(branchesTree.grouping);
const applyBranchesGrouping = (v: boolean): void => {
branchesTree.setGrouping(v);
setBranchesGroupingContext(v);
};
// 多根仓库切换级联(issue #107):构造期快照 repoRoot 的持久化组件在此 rebind(换 memento key、
// 重载状态、fire 刷新),并同步 branchesGrouping context key(否则仅 activate 设一次,切库后按钮显隐错)。
// 顺序不变量:本订阅先于下方 service.onDidChange(refreshAll) 注册,且切换事件在 applyRepository 内
// 先于 onDidChange fire —— rebind 同步完成后,防抖刷新才重取数据。root 为 null(全部仓库关闭)时
// 跳过:无 repo 的一切写路径均短路,旧 key 残留无害。
const rebindRepoScopedState = (root: string): void => {
registry.setRepoRoot(root);
favorites.setRepoRoot(root);
branchesTree.setRepoRoot(root);
setBranchesGroupingContext(branchesTree.grouping);
};
context.subscriptions.push(service.onDidChangeRepository((e) => {
if (e.root) {
rebindRepoScopedState(e.root);
}
}));
context.subscriptions.push(
service,
registry,
favorites,
commit,
ciService,
commitView,
logTree,
branchesTree,
stashTree,
worktreeTree,
shelfTree,
blame,
inlineLens,
branchesView,
badgeView,
vscode.window.registerWebviewViewProvider(CommitWebviewProvider.viewType, commitView),
vscode.window.registerWebviewViewProvider(LogWebviewProvider.viewType, logTree),
stashView,
vscode.window.createTreeView('hyperGit.shelf', { treeDataProvider: shelfTree }),
worktreesView,
...registerChangesCommands(service, registry),
...registerHistoryCommands(service, logTree, branchesTree, favorites),
...registerGitCliCommands(service, branchesTree, logTree),
...registerPartialCommands(service, registry),
...registerAdvancedCommands(service, branchesTree),
...registerRemoteCommands(service, branchesTree, logTree),
...registerMergeCommands(service),
// 窗口 reload 后 rebase/merge 面板恢复(retainContextWhenHidden 仅保活不跨 reload,serializer 补齐)。
RebaseWebview.registerSerializer(service),
MergeEditorWebview.registerSerializer(service),
...registerMiscCommands(service, branchesTree, logTree),
...registerClaudeCommands(),
registerRepositorySelectionCommand(service),
vscode.commands.registerCommand('hyperGit.toggleBlameAnnotation', () => blame.toggle()),
vscode.commands.registerCommand('hyperGit.branchesGroupByPrefix', () => applyBranchesGrouping(true)),
vscode.commands.registerCommand('hyperGit.branchesFlatten', () => applyBranchesGrouping(false)),
...registerStashCommands(service, stashTree),
...registerShelfCommands(service, shelfService, shelfTree),
...registerWorktreeCommands(service, worktreeTree),
vscode.commands.registerCommand('hyperGit.commit', focusCommitView),
vscode.commands.registerCommand('hyperGit.commitAndPush', focusCommitView),
vscode.commands.registerCommand('hyperGit.ci.signIn', () => ciService.signIn()),
// Graph 标题栏视图级控件(原 webview 工具栏上移):Scope 下拉子菜单 + Changed Files List/Tree 互斥图标。
vscode.commands.registerCommand('hyperGit.log.scopeAll', () => logTree.setScope('all')),
vscode.commands.registerCommand('hyperGit.log.scopeCurrent', () => logTree.setScope('current')),
vscode.commands.registerCommand('hyperGit.log.scopeCheckpointer', () => logTree.setScope('checkpointer')),
vscode.commands.registerCommand('hyperGit.log.detailTree', () => logTree.setDetailMode('tree')),
vscode.commands.registerCommand('hyperGit.log.detailFlat', () => logTree.setDetailMode('flat')),
// Commit 标题栏 List/Tree 互斥图标切换(镜像 Graph 范式,context key hyperGit.commit.tree 驱动显隐;
// commitView 的 onDidChangeRepository 订阅先于 refreshAll 注册,切库时 dmode 重载先于视图刷新)。
vscode.commands.registerCommand('hyperGit.commit.detailTree', () => commitView.setDetailMode('tree')),
vscode.commands.registerCommand('hyperGit.commit.detailFlat', () => commitView.setDetailMode('flat')),
// 标题栏「…」批量 Discard:范围 = 勾选集镜像(webview 经 checkedChanged 单向同步),
// 确认与执行复用 discardChanges(单/多统一路径)。
vscode.commands.registerCommand('hyperGit.commit.discardSelected', () => commitView.discardChecked()),
vscode.commands.registerCommand('hyperGit.showConsole', () => showGitConsole()),
vscode.commands.registerCommand('hyperGit.startRebase', () => RebaseWebview.open(service)),
vscode.languages.registerCodeLensProvider({ scheme: 'file' }, inlineLens),
registerInlineCommitCommand(service, inlineLens),
);
// 未提交数角标:承载于隐藏的 changesBadge TreeView(见其创建处说明)。
// 计数复用 service.getChangeCount()(index+工作区+未跟踪去重),为 0 时清空,对齐原生 SCM 行为。
// v0.0.13:容器迁至 Panel 后,角标仅在 Panel 展开时聚合显示(issue #13)。
const updateBadge = (): void => {
const n = service.getChangeCount();
badgeView.badge = n > 0 ? { value: n, tooltip: `${n} uncommitted change(s)` } : undefined;
// 冲突存在性 context key:acceptOurs/acceptTheirs 等命令在 commandPalette 的显隐依据(见 package.json)。
// 事实源用 vscode.git 的 mergeChanges——7 种 unmerged 状态只进该数组,index/workingTree 变更永不承载
// 冲突(getChanges 的 status 映射因此恒判不出冲突,曾致 context key 恒 false、palette 命令永久隐藏)。
void vscode.commands.executeCommand(
'setContext',
'hyperGit.hasConflicts',
(service.repo?.state.mergeChanges.length ?? 0) > 0,
);
};
// 角标走独立快路径(~40ms 微防抖):即便视图未 resolve / Panel 未激活也近实时更新计数(Panel 展开即呈现),
// 并合并 add -A/checkout 等事件风暴,与下方重刷新(150ms)解耦,避免被 log/branches 等高频重拉阻塞。
let badgeTimer: ReturnType<typeof setTimeout> | undefined;
const scheduleBadge = (): void => {
clearTimeout(badgeTimer);
badgeTimer = setTimeout(updateBadge, 40);
};
// git 状态变化频繁(add/checkout/diff 缓存失效均触发),重刷新防抖合并避免 log/stash 高频重拉。
let refreshTimer: ReturnType<typeof setTimeout> | undefined;
const refreshAll = (): void => {
scheduleBadge();
clearTimeout(refreshTimer);
refreshTimer = setTimeout(() => {
commitView.refresh();
logTree.refresh();
branchesTree.refresh();
stashTree.refresh();
worktreeTree.refresh();
shelfTree.refresh();
inlineLens.refresh();
}, 150);
};
context.subscriptions.push(
service.onDidChange(refreshAll),
registry.onDidChange(refreshAll),
commit.onDidChange(refreshAll),
// 释放期清理悬挂定时器,避免回调触及已 dispose 的视图。
new vscode.Disposable(() => {
clearTimeout(badgeTimer);
clearTimeout(refreshTimer);
}),
);
// 首帧同步:即便后续无事件也确保角标初值正确。
updateBadge();
// 视图标题栏图标默认仅在 hover/聚焦时显示(VS Code 平台行为:ViewPane 依 workbench.view.alwaysShowHeaderActions
// 切换 CSS 类 actions-always-visible,扩展贡献点无法覆盖,只能引导用户改设置——改动即时生效,无需重载)。
// 双入口:① 命令 hyperGit.alwaysShowHeaderActions 随时手动开启;② 首次激活一次性提示(用户确认才写全局设置)。
const HEADER_ACTIONS_HINT_KEY = 'hyperGit.hint.alwaysShowHeaderActions';
const enableAlwaysShowHeaderActions = async (): Promise<void> => {
const cfg = vscode.workspace.getConfiguration('workbench.view');
if (cfg.get<boolean>('alwaysShowHeaderActions')) {
void vscode.window.showInformationMessage('View toolbar icons are already always visible.');
return;
}
await cfg.update('alwaysShowHeaderActions', true, vscode.ConfigurationTarget.Global);
await context.globalState.update(HEADER_ACTIONS_HINT_KEY, true);
void vscode.window.showInformationMessage('View toolbar icons are now always visible.');
};
context.subscriptions.push(
vscode.commands.registerCommand('hyperGit.alwaysShowHeaderActions', enableAlwaysShowHeaderActions),
);
void (async (): Promise<void> => {
const cfg = vscode.workspace.getConfiguration('workbench.view');
if (context.globalState.get<boolean>(HEADER_ACTIONS_HINT_KEY) || cfg.get<boolean>('alwaysShowHeaderActions')) {
return;
}
const pick = await vscode.window.showInformationMessage(
'Show Hyper Git view toolbar icons at all times? VS Code hides view header actions until hover by default.',
'Always Show',
'Don\'t Show Again',
);
if (pick === 'Always Show') {
await enableAlwaysShowHeaderActions();
} else if (pick === 'Don\'t Show Again') {
await context.globalState.update(HEADER_ACTIONS_HINT_KEY, true);
}
})();
// 首帧保险:若 repo 在 activate 前已就绪,GitRepositoryService 构造函数的 _onDidChange.fire()
// 早于任何订阅者挂载而被丢失,state.onDidChange 此后可能不再触发。主动刷新一次确保
// Branches/Log 不停留在首帧空状态(getChildren 内已对未就绪数据做 CLI 兜底与空安全处理)。
setTimeout(() => {
branchesTree.refresh();
logTree.refresh();
worktreeTree.refresh();
updateBadge();
}, 500);
return { service };
}
export function deactivate(): void {
// 预留:M3+ 在此释放长生命周期资源。
}