Skip to content

Commit 6d50b02

Browse files
committed
refactor(core): 重构视图触发器的懒加载机制
- 新增 `LazyViewHost` 类,统一管理视图触发器的懒加载代理 - 重构 `FeatureLoader`,将视图触发器的注册逻辑委托给 `LazyViewHost` - 更新相关单元测试,验证代理机制能正确传递数据与刷新事件 - 新增智能翻译 API 切换的设计文档与实现计划
1 parent 2f48d9c commit 6d50b02

5 files changed

Lines changed: 337 additions & 38 deletions

File tree

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Smart Translate API Switch Design
2+
3+
**Goal:** 将智能翻译底层接口从当前谷歌接口切换到 `https://fanyi.sisyphean.top/single`,同时保持现有交互与“中文转英文,非中文转中文”的业务规则不变。
4+
5+
**Scope:** 仅调整 `src/features/smartTranslate/translator.ts` 的请求与响应解析,并补充对应单元测试。
6+
7+
**Design:**
8+
- 保留本地 `LanguageDetector` 的方向判断:`zh -> en`,其他 -> `zh`
9+
- 调用新接口时,将内部目标语言 `zh` 映射为接口所需的 `zh_CN`;英文保持 `en`
10+
- 响应解析从旧的 `sentences[].trans` 改为读取 `translation`
11+
- 如果返回 `info.detectedSource`,则优先作为结果中的 `sourceLanguage`,否则回退到本地检测值。
12+
- 保留现有缓存、复合词预处理与错误抛出行为。
13+
14+
**Testing:**
15+
- 新增 `src/test/smartTranslate/translator.test.ts`
16+
- 覆盖中文转英文与英文转中文两个方向的参数映射和响应解析。
17+
- 通过子类覆写 HTTP 请求方法避免真实网络请求。
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Smart Translate API Switch Implementation Plan
2+
3+
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
4+
5+
**Goal:** 把智能翻译改为调用新的 `/single` GET 接口,并保持原有中文/非中文的翻译方向规则。
6+
7+
**Architecture:** 仅修改翻译服务层,维持上层选择监听、状态栏展示、命名建议逻辑不变。通过新增翻译服务单测,先验证新接口的 URL 参数和响应解析,再做最小实现。
8+
9+
**Tech Stack:** TypeScript, Mocha, VS Code extension test setup
10+
11+
---
12+
13+
### Task 1: 为翻译服务补充失败测试
14+
15+
**Files:**
16+
- Create: `src/test/smartTranslate/translator.test.ts`
17+
- Test: `src/test/smartTranslate/translator.test.ts`
18+
19+
**Step 1: Write the failing test**
20+
- 验证中文输入会请求 `tl=en`
21+
- 验证英文输入会请求 `tl=zh_CN`
22+
- 验证响应从 `translation` 字段解析译文
23+
- 验证 `info.detectedSource` 会回填到结果中
24+
25+
**Step 2: Run test to verify it fails**
26+
- Run: `npx tsc -p . --outDir out`
27+
- Expected: 测试编译通过,但新测试因当前实现 URL/解析不匹配而失败(随后运行测试或静态断言可见)
28+
29+
### Task 2: 最小化修改翻译服务
30+
31+
**Files:**
32+
- Modify: `src/features/smartTranslate/translator.ts`
33+
- Test: `src/test/smartTranslate/translator.test.ts`
34+
35+
**Step 1: Write minimal implementation**
36+
- 提取新接口地址常量
37+
- 映射目标语言到接口值
38+
- 按新 JSON 结构解析 `translation`
39+
- 保持错误显式抛出,不加兜底
40+
41+
**Step 2: Run test to verify it passes**
42+
- Run: `npm run compile-tests`
43+
- Expected: 编译通过
44+
- Run: `npm run lint -- src/features/smartTranslate/translator.ts src/test/smartTranslate/translator.test.ts`
45+
- Expected: 相关文件无 lint 错误
46+
47+
### Task 3: 做一次收尾验证
48+
49+
**Files:**
50+
- Modify: `src/features/smartTranslate/translator.ts`
51+
- Test: `src/test/smartTranslate/translator.test.ts`
52+
53+
**Step 1: Verify focused checks**
54+
- Run: `npm run compile-tests`
55+
- Run: `npm run lint`
56+
57+
**Step 2: Confirm behavior surface**
58+
- 确认未改动 `src/features/smartTranslate/index.ts` 的调用方式
59+
- 确认命名建议仍依赖 `sourceLanguage === "zh"`

src/featureLoader.ts

Lines changed: 21 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as vscode from "vscode";
22
import { FeatureActivationResult, FeatureModule } from "./types/feature";
33
import { Logger } from "./logger";
4+
import { LazyViewHost } from "./lazyViewHost";
45

56
const PERF_PREFIX = "[perf]";
67

@@ -17,7 +18,8 @@ export class FeatureLoader {
1718
private activatedModules = new Set<string>();
1819
private activationResults = new Map<string, FeatureActivationResult>();
1920
private initializingModules = new Map<string, Promise<FeatureActivationResult>>();
20-
private lazyEntrypoints = new Map<string, vscode.Disposable[]>();
21+
private lazyCommandEntrypoints = new Map<string, vscode.Disposable[]>();
22+
private lazyViewHost = new LazyViewHost();
2123

2224
register(module: FeatureModule): void {
2325
if (this.modules.has(module.name)) {
@@ -151,7 +153,7 @@ export class FeatureLoader {
151153
disposable?.dispose();
152154
await vscode.commands.executeCommand(commandId, ...args);
153155
});
154-
this.trackLazyEntrypoint(module.name, disposable);
156+
this.trackLazyCommandEntrypoint(module.name, disposable);
155157
context.subscriptions.push(disposable);
156158
}
157159
}
@@ -162,20 +164,12 @@ export class FeatureLoader {
162164
): void {
163165
const triggers = module.viewTriggers ?? [];
164166
for (const viewId of triggers) {
165-
let disposable: vscode.Disposable | undefined;
166-
disposable = vscode.window.registerTreeDataProvider(viewId, {
167-
getTreeItem: (element: vscode.TreeItem) => element,
168-
getChildren: async () => {
169-
const result = await this.ensureModuleActivated(module.name, context);
170-
if (!result.success) {
171-
return [new vscode.TreeItem(`模块 ${module.name} 加载失败`)];
172-
}
173-
disposable?.dispose();
174-
return [];
175-
},
176-
});
177-
this.trackLazyEntrypoint(module.name, disposable);
178-
context.subscriptions.push(disposable);
167+
this.lazyViewHost.registerPlaceholder(
168+
viewId,
169+
module.name,
170+
() => this.ensureModuleActivated(module.name, context),
171+
context
172+
);
179173
}
180174
}
181175

@@ -215,8 +209,11 @@ export class FeatureLoader {
215209
try {
216210
const started = Date.now();
217211
Logger.debug(`正在激活模块: ${module.name}...`);
218-
this.disposeLazyEntrypoints(module.name);
219-
const result = await module.activate(context);
212+
this.disposeLazyCommandEntrypoints(module.name);
213+
const result = await this.lazyViewHost.withInterception(
214+
module.viewTriggers,
215+
() => module.activate(context)
216+
);
220217
const elapsed = Date.now() - started;
221218
Logger.info(`${PERF_PREFIX}[module:${module.name}] 激活耗时 ${elapsed}ms`);
222219

@@ -258,22 +255,22 @@ export class FeatureLoader {
258255
});
259256
}
260257

261-
private trackLazyEntrypoint(moduleName: string, disposable: vscode.Disposable): void {
262-
const entrypoints = this.lazyEntrypoints.get(moduleName) ?? [];
258+
private trackLazyCommandEntrypoint(moduleName: string, disposable: vscode.Disposable): void {
259+
const entrypoints = this.lazyCommandEntrypoints.get(moduleName) ?? [];
263260
entrypoints.push(disposable);
264-
this.lazyEntrypoints.set(moduleName, entrypoints);
261+
this.lazyCommandEntrypoints.set(moduleName, entrypoints);
265262
}
266263

267-
private disposeLazyEntrypoints(moduleName: string): void {
268-
const entrypoints = this.lazyEntrypoints.get(moduleName);
264+
private disposeLazyCommandEntrypoints(moduleName: string): void {
265+
const entrypoints = this.lazyCommandEntrypoints.get(moduleName);
269266
if (!entrypoints) {
270267
return;
271268
}
272269

273270
for (const entrypoint of entrypoints) {
274271
entrypoint.dispose();
275272
}
276-
this.lazyEntrypoints.delete(moduleName);
273+
this.lazyCommandEntrypoints.delete(moduleName);
277274
}
278275
}
279276

src/lazyViewHost.ts

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import * as vscode from "vscode";
2+
import { FeatureActivationResult } from "./types/feature";
3+
4+
type ViewProvider = vscode.TreeDataProvider<any>;
5+
type ViewEntry = {
6+
proxy: LazyTreeDataProviderProxy;
7+
treeView: vscode.TreeView<any>;
8+
};
9+
10+
class LazyTreeDataProviderProxy implements vscode.TreeDataProvider<any>, vscode.Disposable {
11+
private delegate?: ViewProvider;
12+
private delegateSubscription?: vscode.Disposable;
13+
private readonly emitter = new vscode.EventEmitter<any>();
14+
readonly onDidChangeTreeData = this.emitter.event;
15+
16+
constructor(
17+
private readonly moduleName: string,
18+
private readonly viewId: string,
19+
private readonly activate: () => Promise<FeatureActivationResult>
20+
) {}
21+
22+
attach(provider: ViewProvider): void {
23+
this.delegate = provider;
24+
this.delegateSubscription?.dispose();
25+
this.delegateSubscription = provider.onDidChangeTreeData?.(() => this.emitter.fire(undefined));
26+
this.emitter.fire(undefined);
27+
}
28+
29+
detach(provider?: ViewProvider): void {
30+
if (provider && this.delegate !== provider) {
31+
return;
32+
}
33+
34+
this.delegateSubscription?.dispose();
35+
this.delegateSubscription = undefined;
36+
this.delegate = undefined;
37+
this.emitter.fire(undefined);
38+
}
39+
40+
getTreeItem(element: any): vscode.TreeItem | Thenable<vscode.TreeItem> {
41+
if (this.delegate) {
42+
return this.delegate.getTreeItem(element);
43+
}
44+
return element instanceof vscode.TreeItem ? element : new vscode.TreeItem(String(element));
45+
}
46+
47+
async getChildren(element?: any): Promise<any[]> {
48+
if (!this.delegate) {
49+
const result = await this.activate();
50+
if (!result.success) {
51+
return [new vscode.TreeItem(`模块 ${this.moduleName} 加载失败`)];
52+
}
53+
if (result.disabled) {
54+
return [new vscode.TreeItem(`功能 \"${this.moduleName}\" 已被禁用`)];
55+
}
56+
if (!this.delegate) {
57+
return [new vscode.TreeItem(`视图 ${this.viewId} 未注册数据提供器`)];
58+
}
59+
}
60+
61+
return (await this.delegate.getChildren(element)) ?? [];
62+
}
63+
64+
dispose(): void {
65+
this.detach();
66+
this.emitter.dispose();
67+
}
68+
}
69+
70+
export class LazyViewHost {
71+
private entries = new Map<string, ViewEntry>();
72+
73+
registerPlaceholder(
74+
viewId: string,
75+
moduleName: string,
76+
activate: () => Promise<FeatureActivationResult>,
77+
context: vscode.ExtensionContext
78+
): void {
79+
const proxy = new LazyTreeDataProviderProxy(moduleName, viewId, activate);
80+
const treeView = vscode.window.createTreeView(viewId, {
81+
treeDataProvider: proxy,
82+
showCollapseAll: true,
83+
});
84+
85+
this.entries.set(viewId, { proxy, treeView });
86+
context.subscriptions.push(proxy, treeView);
87+
}
88+
89+
async withInterception<T>(viewIds: readonly string[] | undefined, task: () => Promise<T> | T): Promise<T> {
90+
if (!viewIds?.length) {
91+
return await Promise.resolve(task());
92+
}
93+
94+
const originalRegister = vscode.window.registerTreeDataProvider;
95+
const originalCreate = vscode.window.createTreeView;
96+
97+
vscode.window.registerTreeDataProvider = ((viewId: string, provider: ViewProvider) => {
98+
const entry = this.match(viewIds, viewId);
99+
if (!entry) {
100+
return originalRegister(viewId, provider);
101+
}
102+
103+
entry.proxy.attach(provider);
104+
return new vscode.Disposable(() => entry.proxy.detach(provider));
105+
}) as typeof vscode.window.registerTreeDataProvider;
106+
107+
vscode.window.createTreeView = ((viewId: string, options: vscode.TreeViewOptions<any>) => {
108+
const entry = this.match(viewIds, viewId);
109+
if (!entry) {
110+
return (originalCreate as any)(viewId, options);
111+
}
112+
113+
entry.proxy.attach(options.treeDataProvider);
114+
return entry.treeView;
115+
}) as typeof vscode.window.createTreeView;
116+
117+
try {
118+
return await Promise.resolve(task());
119+
} finally {
120+
vscode.window.registerTreeDataProvider = originalRegister;
121+
vscode.window.createTreeView = originalCreate;
122+
}
123+
}
124+
125+
private match(viewIds: readonly string[], viewId: string): ViewEntry | undefined {
126+
if (!viewIds.includes(viewId)) {
127+
return undefined;
128+
}
129+
return this.entries.get(viewId);
130+
}
131+
}

0 commit comments

Comments
 (0)