autosnippet 2.8.3 → 2.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/bin/cli.js +5 -33
- package/config/constitution.yaml +9 -2
- package/dashboard/dist/assets/{icons-B_Xg4B-s.js → icons-BkT3XrKf.js} +105 -100
- package/dashboard/dist/assets/index-BsB7DzW4.css +1 -0
- package/dashboard/dist/assets/index-DdmQMrJJ.js +155 -0
- package/dashboard/dist/index.html +3 -3
- package/lib/cli/AiScanService.js +13 -11
- package/lib/cli/KnowledgeSyncService.js +343 -0
- package/lib/cli/SetupService.js +9 -27
- package/lib/core/ast/ProjectGraph.js +160 -0
- package/lib/core/gateway/GatewayActionRegistry.js +48 -58
- package/lib/domain/index.js +16 -11
- package/lib/domain/knowledge/KnowledgeEntry.js +351 -0
- package/lib/domain/knowledge/KnowledgeRepository.js +123 -0
- package/lib/domain/knowledge/Lifecycle.js +109 -0
- package/lib/domain/knowledge/index.js +27 -0
- package/lib/domain/knowledge/values/Constraints.js +125 -0
- package/lib/domain/knowledge/values/Content.js +86 -0
- package/lib/domain/knowledge/values/Quality.js +93 -0
- package/lib/domain/knowledge/values/Reasoning.js +69 -0
- package/lib/domain/knowledge/values/Relations.js +168 -0
- package/lib/domain/knowledge/values/Stats.js +87 -0
- package/lib/domain/knowledge/values/index.js +9 -0
- package/lib/external/ai/AiProvider.js +48 -0
- package/lib/external/ai/providers/GoogleGeminiProvider.js +12 -3
- package/lib/external/mcp/McpServer.js +7 -5
- package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +3 -2
- package/lib/external/mcp/handlers/bootstrap.js +121 -12
- package/lib/external/mcp/handlers/browse.js +77 -73
- package/lib/external/mcp/handlers/candidate.js +29 -276
- package/lib/external/mcp/handlers/guard.js +2 -0
- package/lib/external/mcp/handlers/knowledge.js +205 -0
- package/lib/external/mcp/handlers/skill.js +4 -2
- package/lib/external/mcp/handlers/structure.js +25 -23
- package/lib/external/mcp/handlers/system.js +10 -12
- package/lib/external/mcp/tools.js +125 -138
- package/lib/http/HttpServer.js +4 -8
- package/lib/http/middleware/requestLogger.js +3 -3
- package/lib/http/routes/ai.js +17 -1
- package/lib/http/routes/extract.js +48 -4
- package/lib/http/routes/knowledge.js +246 -0
- package/lib/http/routes/search.js +12 -17
- package/lib/http/routes/skills.js +44 -1
- package/lib/infrastructure/cache/GraphCache.js +143 -0
- package/lib/infrastructure/database/migrations/015_create_token_usage.js +27 -0
- package/lib/infrastructure/database/migrations/016_unified_knowledge_entries.js +395 -0
- package/lib/infrastructure/external/XcodeAutomation.js +187 -103
- package/lib/infrastructure/realtime/RealtimeService.js +14 -2
- package/lib/injection/ServiceContainer.js +164 -63
- package/lib/repository/knowledge/KnowledgeRepository.impl.js +373 -0
- package/lib/repository/token/TokenUsageStore.js +162 -0
- package/lib/service/automation/DirectiveDetector.js +2 -3
- package/lib/service/automation/FileWatcher.js +67 -28
- package/lib/service/automation/XcodeIntegration.js +931 -156
- package/lib/service/automation/handlers/AlinkHandler.js +6 -4
- package/lib/service/automation/handlers/CreateHandler.js +53 -18
- package/lib/service/automation/handlers/GuardHandler.js +183 -20
- package/lib/service/automation/handlers/SearchHandler.js +35 -17
- package/lib/service/chat/AnalystAgent.js +25 -14
- package/lib/service/chat/CandidateGuardrail.js +1 -1
- package/lib/service/chat/ChatAgent.js +280 -48
- package/lib/service/chat/ContextWindow.js +92 -8
- package/lib/service/chat/HandoffProtocol.js +26 -1
- package/lib/service/chat/ProducerAgent.js +11 -9
- package/lib/service/chat/tools.js +298 -194
- package/lib/service/guard/GuardCheckEngine.js +114 -10
- package/lib/service/guard/GuardService.js +59 -48
- package/lib/service/knowledge/ConfidenceRouter.js +159 -0
- package/lib/service/knowledge/KnowledgeFileWriter.js +602 -0
- package/lib/service/knowledge/KnowledgeService.js +725 -0
- package/lib/service/search/SearchEngine.js +92 -19
- package/lib/service/skills/SignalCollector.js +15 -9
- package/lib/service/skills/SkillAdvisor.js +13 -11
- package/lib/service/snippet/SnippetFactory.js +5 -5
- package/lib/service/spm/SpmService.js +119 -18
- package/package.json +1 -1
- package/scripts/install-cursor-skill.js +0 -6
- package/scripts/migrate-md-to-knowledge.mjs +364 -0
- package/skills/autosnippet-analysis/SKILL.md +15 -7
- package/skills/autosnippet-candidates/SKILL.md +6 -6
- package/skills/autosnippet-coldstart/SKILL.md +7 -3
- package/skills/autosnippet-concepts/SKILL.md +7 -6
- package/skills/autosnippet-create/SKILL.md +13 -13
- package/skills/autosnippet-intent/SKILL.md +3 -2
- package/skills/autosnippet-lifecycle/SKILL.md +5 -5
- package/skills/autosnippet-recipes/SKILL.md +16 -4
- package/templates/constitution.yaml +1 -1
- package/templates/copilot-instructions.md +6 -6
- package/templates/recipes-setup/README.md +3 -3
- package/dashboard/dist/assets/index-CkIih2CC.css +0 -1
- package/dashboard/dist/assets/index-Duc8Qk-c.js +0 -197
- package/lib/cli/CandidateSyncService.js +0 -261
- package/lib/cli/SyncService.js +0 -356
- package/lib/domain/candidate/Candidate.js +0 -196
- package/lib/domain/candidate/CandidateRepository.js +0 -107
- package/lib/domain/candidate/Reasoning.js +0 -52
- package/lib/domain/recipe/Recipe.js +0 -421
- package/lib/domain/recipe/RecipeRepository.js +0 -54
- package/lib/domain/types/CandidateStatus.js +0 -52
- package/lib/http/routes/candidates.js +0 -559
- package/lib/http/routes/recipes.js +0 -397
- package/lib/repository/candidate/CandidateRepository.impl.js +0 -230
- package/lib/repository/recipe/RecipeRepository.impl.js +0 -498
- package/lib/service/candidate/CandidateAggregator.js +0 -52
- package/lib/service/candidate/CandidateFileWriter.js +0 -383
- package/lib/service/candidate/CandidateService.js +0 -973
- package/lib/service/recipe/RecipeFileWriter.js +0 -514
- package/lib/service/recipe/RecipeService.js +0 -786
- package/lib/service/recipe/RecipeStatsTracker.js +0 -148
|
@@ -1,42 +1,230 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* XcodeIntegration — Xcode IDE
|
|
3
|
-
*
|
|
2
|
+
* XcodeIntegration — Xcode IDE 代码自动插入服务
|
|
3
|
+
*
|
|
4
|
+
* 核心能力:
|
|
5
|
+
* §1 import 语句解析 — 支持 ObjC (#import/#include/@import) 和 Swift (import)
|
|
6
|
+
* §2 头文件搜索 — 在 target 源目录中递归查找头文件并计算相对路径
|
|
7
|
+
* §3 import 格式化 — 根据 同target/跨target 关系生成正确的引号/尖括号格式
|
|
8
|
+
* §4 三级去重 — 精确匹配 → 模块匹配 → 相似头文件名匹配
|
|
9
|
+
* §5 SPM 依赖决策 — block(循环依赖) / review(缺失可补) / continue(已存在)
|
|
10
|
+
* §6 Xcode 自动插入 — osascript 跳转+粘贴,保持 Undo 可用
|
|
11
|
+
* §7 文件写入回退 — Xcode 失败时直接写文件,Xcode 自动 reload
|
|
12
|
+
* §8 粘贴行号偏移 — headers 插入后自动修正代码粘贴位置
|
|
13
|
+
* §9 完整插入流程 — cut 触发行 → preflight → headers → offset → paste
|
|
4
14
|
*/
|
|
5
15
|
|
|
6
|
-
import { readFileSync, writeFileSync } from 'node:fs';
|
|
16
|
+
import { readFileSync, writeFileSync, existsSync, readdirSync, statSync } from 'node:fs';
|
|
17
|
+
import { basename, dirname, relative, resolve as pathResolve, sep } from 'node:path';
|
|
18
|
+
import { saveEventFilter } from './SaveEventFilter.js';
|
|
19
|
+
|
|
20
|
+
// ═══════════════════════════════════════════════════════════════
|
|
21
|
+
// §1 常量与工具函数
|
|
22
|
+
// ═══════════════════════════════════════════════════════════════
|
|
7
23
|
|
|
8
24
|
function _sleep(ms) {
|
|
9
25
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
10
26
|
}
|
|
11
27
|
|
|
12
28
|
/**
|
|
13
|
-
*
|
|
14
|
-
* #import <Module/Header.h> → Module
|
|
15
|
-
* @import Module; → Module
|
|
16
|
-
* import Module → Module
|
|
17
|
-
* #import "Local.h" → null(本地头文件不检查)
|
|
29
|
+
* 在 import 行末尾附加来源标记注释
|
|
18
30
|
*/
|
|
19
|
-
function
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
function _withAutoSnippetNote(importLine) {
|
|
32
|
+
if (!importLine) return importLine;
|
|
33
|
+
const note = '// AutoSnippet: 自动插入';
|
|
34
|
+
if (importLine.includes(note)) return importLine;
|
|
35
|
+
return `${importLine} ${note}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 解析原始 header 字符串,提取 moduleName 和 headerName
|
|
40
|
+
*
|
|
41
|
+
* 支持格式:
|
|
42
|
+
* #import <Module/Header.h> → { moduleName: 'Module', headerName: 'Header.h', isAngle: true }
|
|
43
|
+
* #import "Header.h" → { moduleName: '', headerName: 'Header.h', isAngle: false }
|
|
44
|
+
* @import Module; → { moduleName: 'Module', headerName: '', isAngle: false, isAtImport: true }
|
|
45
|
+
* import Module (Swift) → { moduleName: 'Module', headerName: '', isAngle: false, isSwiftImport: true }
|
|
46
|
+
* Header.h → { moduleName: '', headerName: 'Header.h', isAngle: false, isRaw: true }
|
|
47
|
+
*/
|
|
48
|
+
function _parseHeaderString(header) {
|
|
49
|
+
const t = header.trim();
|
|
50
|
+
// #import <Module/Header.h>
|
|
51
|
+
let m = t.match(/^#(?:import|include)\s+<([^/> ]+)\/([^>]+)>/);
|
|
52
|
+
if (m) return { moduleName: m[1], headerName: m[2], isAngle: true };
|
|
53
|
+
// #import <Module> (framework umbrella)
|
|
54
|
+
m = t.match(/^#(?:import|include)\s+<([^>]+)>/);
|
|
55
|
+
if (m) return { moduleName: m[1], headerName: '', isAngle: true };
|
|
56
|
+
// #import "Header.h" or #import "Dir/Header.h"
|
|
57
|
+
m = t.match(/^#(?:import|include)\s+"([^"]+)"/);
|
|
58
|
+
if (m) {
|
|
59
|
+
const parts = m[1].split('/');
|
|
60
|
+
return { moduleName: '', headerName: parts[parts.length - 1], isAngle: false, quotedPath: m[1] };
|
|
61
|
+
}
|
|
62
|
+
// @import Module;
|
|
63
|
+
m = t.match(/^@import\s+(\w+)/);
|
|
64
|
+
if (m) return { moduleName: m[1], headerName: '', isAngle: false, isAtImport: true };
|
|
65
|
+
// import Module (Swift)
|
|
66
|
+
m = t.match(/^import\s+(\w+)/);
|
|
67
|
+
if (m && !['class', 'struct', 'enum', 'protocol', 'func', 'var', 'let'].includes(m[1])) {
|
|
68
|
+
return { moduleName: m[1], headerName: '', isAngle: false, isSwiftImport: true };
|
|
69
|
+
}
|
|
70
|
+
// 裸 header 名: Header.h
|
|
71
|
+
if (/\.(h|hpp|hh)$/i.test(t)) {
|
|
72
|
+
return { moduleName: '', headerName: t, isAngle: false, isRaw: true };
|
|
73
|
+
}
|
|
74
|
+
return { moduleName: '', headerName: t, isAngle: false, isRaw: true };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 在 target 源目录中搜索头文件,返回相对于当前文件的路径
|
|
79
|
+
*
|
|
80
|
+
* 搜索策略:
|
|
81
|
+
* 1. 当前文件同目录
|
|
82
|
+
* 2. 从项目根目录递归查找(最多深度 6 层,优先 Sources/ 下)
|
|
83
|
+
* 3. 找到后计算相对于当前文件目录的路径
|
|
84
|
+
*
|
|
85
|
+
* @param {string} headerName - 头文件名 (如 "Foo.h")
|
|
86
|
+
* @param {string} currentFilePath - 当前正在编辑的文件绝对路径
|
|
87
|
+
* @param {string} [projectRoot] - 项目根目录
|
|
88
|
+
* @returns {string|null} 相对路径 (如 "Foo.h" 或 "../SubDir/Foo.h"),null 表示未找到
|
|
89
|
+
*/
|
|
90
|
+
function _findHeaderRelativePath(headerName, currentFilePath, projectRoot) {
|
|
91
|
+
if (!headerName || !currentFilePath) return null;
|
|
92
|
+
try {
|
|
93
|
+
const currentDir = dirname(currentFilePath);
|
|
94
|
+
|
|
95
|
+
// 1. 同目录检查
|
|
96
|
+
const sameDir = pathResolve(currentDir, headerName);
|
|
97
|
+
if (existsSync(sameDir)) return headerName;
|
|
98
|
+
|
|
99
|
+
// 2. 向上找 Sources/ 或 target 根目录,在其下递归搜索
|
|
100
|
+
const searchRoots = [];
|
|
101
|
+
if (projectRoot) {
|
|
102
|
+
const sourcesDir = pathResolve(projectRoot, 'Sources');
|
|
103
|
+
if (existsSync(sourcesDir)) searchRoots.push(sourcesDir);
|
|
104
|
+
searchRoots.push(projectRoot);
|
|
105
|
+
}
|
|
106
|
+
// 也从当前文件向上找 Sources 目录
|
|
107
|
+
let dir = currentDir;
|
|
108
|
+
for (let i = 0; i < 8; i++) {
|
|
109
|
+
const base = basename(dir);
|
|
110
|
+
if (base === 'Sources' || base === 'Source' || base === 'src') {
|
|
111
|
+
searchRoots.unshift(dir);
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
const parent = dirname(dir);
|
|
115
|
+
if (parent === dir) break;
|
|
116
|
+
dir = parent;
|
|
34
117
|
}
|
|
118
|
+
|
|
119
|
+
// 在 searchRoots 中递归查找 headerName(限深度 6)
|
|
120
|
+
for (const root of searchRoots) {
|
|
121
|
+
const found = _findFileRecursive(root, headerName, 6);
|
|
122
|
+
if (found) {
|
|
123
|
+
let rel = relative(currentDir, found);
|
|
124
|
+
// 统一用 / 分隔
|
|
125
|
+
rel = rel.split(sep).join('/');
|
|
126
|
+
return rel;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return null;
|
|
131
|
+
} catch {
|
|
132
|
+
return null;
|
|
35
133
|
}
|
|
36
|
-
return [...modules];
|
|
37
134
|
}
|
|
38
135
|
|
|
39
|
-
|
|
136
|
+
/**
|
|
137
|
+
* 递归查找文件(限最大深度)
|
|
138
|
+
*/
|
|
139
|
+
function _findFileRecursive(dir, fileName, maxDepth) {
|
|
140
|
+
if (maxDepth <= 0) return null;
|
|
141
|
+
try {
|
|
142
|
+
const entries = readdirSync(dir);
|
|
143
|
+
// 先在当前层查找
|
|
144
|
+
for (const e of entries) {
|
|
145
|
+
if (e === fileName) return pathResolve(dir, e);
|
|
146
|
+
}
|
|
147
|
+
// 再递归子目录(跳过隐藏目录和常见无关目录)
|
|
148
|
+
for (const e of entries) {
|
|
149
|
+
if (e.startsWith('.') || e === 'node_modules' || e === 'build' || e === 'DerivedData') continue;
|
|
150
|
+
const full = pathResolve(dir, e);
|
|
151
|
+
try {
|
|
152
|
+
if (statSync(full).isDirectory()) {
|
|
153
|
+
const found = _findFileRecursive(full, fileName, maxDepth - 1);
|
|
154
|
+
if (found) return found;
|
|
155
|
+
}
|
|
156
|
+
} catch { /* 跳过不可访问的目录 */ }
|
|
157
|
+
}
|
|
158
|
+
} catch { /* 跳过不可读目录 */ }
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* 根据当前文件 target 和 header 的 module 关系,生成正确格式的 import 行
|
|
164
|
+
*
|
|
165
|
+
* 规则:
|
|
166
|
+
* Swift: 始终 `import Module`(无 quote/angle 区别)
|
|
167
|
+
* ObjC 同 target: `#import "Header.h"` (引号格式)
|
|
168
|
+
* ObjC 跨 target: `#import <Module/Header.h>` (尖括号格式)
|
|
169
|
+
* @import 格式保持原样(已经模块级)
|
|
170
|
+
*
|
|
171
|
+
* @param {string} rawHeader 原始 header 字符串
|
|
172
|
+
* @param {object} ctx { currentTarget, headerModuleName, isSwift, fullPath, projectRoot }
|
|
173
|
+
* - currentTarget: 当前文件所属的 target 名
|
|
174
|
+
* - headerModuleName: header 所属的 module/target 名(来自 recipe.moduleName 或推断)
|
|
175
|
+
* - isSwift: 目标文件是否是 Swift
|
|
176
|
+
* - fullPath: 当前编辑文件的绝对路径(用于计算同 target 相对路径)
|
|
177
|
+
* - projectRoot: 项目根目录(用于搜索头文件物理位置)
|
|
178
|
+
* @returns {string} 格式化后的完整 import 行
|
|
179
|
+
*/
|
|
180
|
+
function _resolveHeaderFormat(rawHeader, ctx) {
|
|
181
|
+
const { currentTarget, headerModuleName, isSwift, fullPath, projectRoot } = ctx;
|
|
182
|
+
const parsed = _parseHeaderString(rawHeader);
|
|
183
|
+
|
|
184
|
+
// Swift: 始终 `import Module`
|
|
185
|
+
if (isSwift || parsed.isSwiftImport) {
|
|
186
|
+
// 已经是完整 swift import 语句
|
|
187
|
+
if (parsed.isSwiftImport) return rawHeader.trim();
|
|
188
|
+
// 从 ObjC 格式推断 swift import
|
|
189
|
+
const mod = parsed.moduleName || headerModuleName || '';
|
|
190
|
+
if (mod) return `import ${mod}`;
|
|
191
|
+
return rawHeader.trim(); // 无法推断,原样返回
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// @import 保持原样(模块级引用不受 target 影响)
|
|
195
|
+
if (parsed.isAtImport) return rawHeader.trim();
|
|
196
|
+
|
|
197
|
+
// 已经是尖括号格式 → 保持(明确的跨模块引用)
|
|
198
|
+
if (parsed.isAngle) return rawHeader.trim();
|
|
199
|
+
|
|
200
|
+
// ── ObjC: 判断同 target vs 跨 target ──
|
|
201
|
+
const effectiveModule = parsed.moduleName || headerModuleName || '';
|
|
202
|
+
|
|
203
|
+
// 如果没有 target 信息,无法判断,保持原样
|
|
204
|
+
if (!currentTarget || !effectiveModule) return rawHeader.trim();
|
|
205
|
+
|
|
206
|
+
const isSameTarget = currentTarget === effectiveModule;
|
|
207
|
+
|
|
208
|
+
if (isSameTarget) {
|
|
209
|
+
// 同 target → 引号格式,计算相对路径
|
|
210
|
+
if (parsed.headerName && fullPath) {
|
|
211
|
+
const relPath = _findHeaderRelativePath(parsed.headerName, fullPath, projectRoot);
|
|
212
|
+
if (relPath) return `#import "${relPath}"`;
|
|
213
|
+
}
|
|
214
|
+
if (parsed.quotedPath) return `#import "${parsed.quotedPath}"`;
|
|
215
|
+
if (parsed.headerName) return `#import "${parsed.headerName}"`;
|
|
216
|
+
return rawHeader.trim();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// 跨 target → 尖括号格式 <Module/Header.h>
|
|
220
|
+
if (parsed.headerName) {
|
|
221
|
+
return `#import <${effectiveModule}/${parsed.headerName}>`;
|
|
222
|
+
}
|
|
223
|
+
// 没有 headerName(裸模块名),用 @import
|
|
224
|
+
return `@import ${effectiveModule};`;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** 常见 Apple 系统框架(无需 SPM 依赖检查) */
|
|
40
228
|
const _SYSTEM_FRAMEWORKS = new Set([
|
|
41
229
|
'Foundation', 'UIKit', 'AppKit', 'SwiftUI', 'Combine', 'CoreFoundation',
|
|
42
230
|
'CoreGraphics', 'CoreData', 'CoreAnimation', 'CoreLocation', 'CoreMedia',
|
|
@@ -50,19 +238,366 @@ const _SYSTEM_FRAMEWORKS = new Set([
|
|
|
50
238
|
'Dispatch', 'XCTest',
|
|
51
239
|
]);
|
|
52
240
|
|
|
241
|
+
// ═══════════════════════════════════════════════════════════════
|
|
242
|
+
// §4 三级 import 去重
|
|
243
|
+
// ═══════════════════════════════════════════════════════════════
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* 从文件中收集已有的 import 语句
|
|
247
|
+
*/
|
|
248
|
+
function _collectImportsFromFile(filePath, isSwift) {
|
|
249
|
+
try {
|
|
250
|
+
if (!existsSync(filePath)) return [];
|
|
251
|
+
const content = readFileSync(filePath, 'utf8');
|
|
252
|
+
const lines = content.split(/\r?\n/);
|
|
253
|
+
const imports = [];
|
|
254
|
+
for (const line of lines) {
|
|
255
|
+
const t = line.trim();
|
|
256
|
+
if (isSwift) {
|
|
257
|
+
if (t.startsWith('import ')) imports.push(t);
|
|
258
|
+
} else {
|
|
259
|
+
if (t.startsWith('#import ') || t.startsWith('@import ') || t.startsWith('#include ')) {
|
|
260
|
+
imports.push(t);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return imports;
|
|
265
|
+
} catch {
|
|
266
|
+
return [];
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
53
270
|
/**
|
|
54
|
-
*
|
|
271
|
+
* 收集 .m 文件对应 .h 文件中的 imports(ObjC 接口/实现配对去重)
|
|
272
|
+
*/
|
|
273
|
+
function _collectImportsFromHeaderFile(sourcePath, importArray) {
|
|
274
|
+
const dotIndex = sourcePath.lastIndexOf('.');
|
|
275
|
+
if (dotIndex <= 0) return;
|
|
276
|
+
const headerPath = sourcePath.substring(0, dotIndex) + '.h';
|
|
277
|
+
const importReg = /^#import\s*<[A-Za-z0-9_]+\/[A-Za-z0-9_+.-]+\.h>$/;
|
|
278
|
+
try {
|
|
279
|
+
if (!existsSync(headerPath)) return;
|
|
280
|
+
const data = readFileSync(headerPath, 'utf8');
|
|
281
|
+
for (const line of data.split('\n')) {
|
|
282
|
+
const t = line.trim();
|
|
283
|
+
if (importReg.test(t) && !importArray.includes(t)) {
|
|
284
|
+
importArray.push(t);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
} catch { /* ignore */ }
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* 三级 import 去重检查
|
|
55
292
|
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
293
|
+
* hasHeader 精确匹配(同一 import 行)
|
|
294
|
+
* hasModule 模块级匹配(同模块不同头文件,或 @import)
|
|
295
|
+
* hasSimilarHeader 文件名 case-insensitive 匹配
|
|
296
|
+
*
|
|
297
|
+
* @param {string[]} importArray 已有的 import 行
|
|
298
|
+
* @param {string} headerLine 待插入的 import 行
|
|
299
|
+
* @param {boolean} isSwift
|
|
300
|
+
*/
|
|
301
|
+
function _checkImportStatus(importArray, headerLine, isSwift) {
|
|
302
|
+
const trimmed = headerLine.trim();
|
|
303
|
+
|
|
304
|
+
// 提取 module / headerFileName
|
|
305
|
+
let moduleName = '';
|
|
306
|
+
let headerFileName = '';
|
|
307
|
+
|
|
308
|
+
if (isSwift) {
|
|
309
|
+
const m = trimmed.match(/^import\s+(\w+)/);
|
|
310
|
+
if (m) moduleName = m[1];
|
|
311
|
+
headerFileName = moduleName;
|
|
312
|
+
} else {
|
|
313
|
+
const angle = trimmed.match(/<([^/]+)\/([^>]+)>/);
|
|
314
|
+
if (angle) {
|
|
315
|
+
moduleName = angle[1];
|
|
316
|
+
headerFileName = angle[2];
|
|
317
|
+
}
|
|
318
|
+
const quote = trimmed.match(/"([^"]+)"/);
|
|
319
|
+
if (quote) {
|
|
320
|
+
headerFileName = basename(quote[1]);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const headerFileNameLower = headerFileName.toLowerCase();
|
|
325
|
+
|
|
326
|
+
for (const imp of importArray) {
|
|
327
|
+
const impT = imp.trim();
|
|
328
|
+
|
|
329
|
+
// ── 级别 1: 精确匹配 ──
|
|
330
|
+
if (impT === trimmed) {
|
|
331
|
+
return { hasHeader: true, hasModule: false, hasSimilarHeader: false };
|
|
332
|
+
}
|
|
333
|
+
// 去掉可能的 AutoSnippet 注释后缀再比较
|
|
334
|
+
const impTClean = impT.replace(/\s*\/\/\s*AutoSnippet.*$/, '').trim();
|
|
335
|
+
if (impTClean === trimmed) {
|
|
336
|
+
return { hasHeader: true, hasModule: false, hasSimilarHeader: false };
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (isSwift) {
|
|
340
|
+
// ── 级别 2: Swift 模块匹配 ──
|
|
341
|
+
const m2 = impT.match(/^import\s+(\w+)/);
|
|
342
|
+
if (m2 && m2[1] === moduleName) {
|
|
343
|
+
return { hasHeader: false, hasModule: true, hasSimilarHeader: false };
|
|
344
|
+
}
|
|
345
|
+
} else {
|
|
346
|
+
// ── 级别 2: ObjC 模块匹配(<Module/xxx> 或 @import Module) ──
|
|
347
|
+
if (moduleName) {
|
|
348
|
+
const impAngle = impT.match(/<([^/]+)\//);
|
|
349
|
+
if (impAngle && impAngle[1] === moduleName) {
|
|
350
|
+
return { hasHeader: false, hasModule: true, hasSimilarHeader: false };
|
|
351
|
+
}
|
|
352
|
+
const impAt = impT.match(/@import\s+(\w+)/);
|
|
353
|
+
if (impAt && impAt[1] === moduleName) {
|
|
354
|
+
return { hasHeader: false, hasModule: true, hasSimilarHeader: false };
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// ── 级别 3: 相似头文件名匹配(case-insensitive) ──
|
|
359
|
+
if (headerFileNameLower) {
|
|
360
|
+
let importedFileName = null;
|
|
361
|
+
const a = impT.match(/<[^/]+\/([^>]+)>/);
|
|
362
|
+
if (a) importedFileName = a[1].toLowerCase();
|
|
363
|
+
const q = impT.match(/"([^"]+)"/);
|
|
364
|
+
if (q) importedFileName = basename(q[1]).toLowerCase();
|
|
365
|
+
if (importedFileName && importedFileName === headerFileNameLower) {
|
|
366
|
+
return { hasHeader: false, hasModule: false, hasSimilarHeader: true };
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return { hasHeader: false, hasModule: false, hasSimilarHeader: false };
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// ═══════════════════════════════════════════════════════════════
|
|
376
|
+
// §5 模块名推断
|
|
377
|
+
// ═══════════════════════════════════════════════════════════════
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* 从 import 语句推断模块名
|
|
381
|
+
*
|
|
382
|
+
* #import <Module/Header.h> → Module
|
|
383
|
+
* @import Module; → Module
|
|
384
|
+
* import Module (Swift) → Module
|
|
385
|
+
* #import "Local.h" → null
|
|
386
|
+
*/
|
|
387
|
+
function _inferModulesFromHeaders(headers) {
|
|
388
|
+
const modules = new Set();
|
|
389
|
+
for (const h of headers) {
|
|
390
|
+
const t = h.trim();
|
|
391
|
+
let m;
|
|
392
|
+
m = t.match(/^#import\s+<([^/> ]+)/);
|
|
393
|
+
if (m) { modules.add(m[1]); continue; }
|
|
394
|
+
m = t.match(/^@import\s+(\w+)/);
|
|
395
|
+
if (m) { modules.add(m[1]); continue; }
|
|
396
|
+
m = t.match(/^import\s+(\w+)/);
|
|
397
|
+
if (m && !['class', 'struct', 'enum', 'protocol'].includes(m[1])) {
|
|
398
|
+
modules.add(m[1]);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return [...modules];
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// ═══════════════════════════════════════════════════════════════
|
|
405
|
+
// §6 SPM 依赖检查决策引擎
|
|
406
|
+
// ═══════════════════════════════════════════════════════════════
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* 将 SpmService.ensureDependency 返回值映射为三种动作:
|
|
410
|
+
* continue — 依赖已存在
|
|
411
|
+
* block — 循环/反向依赖,禁止插入
|
|
412
|
+
* review — 依赖缺失但可添加,需用户确认
|
|
413
|
+
*/
|
|
414
|
+
function _evaluateDepResult(ensureResult, from, to) {
|
|
415
|
+
if (ensureResult.exists) return { action: 'continue' };
|
|
416
|
+
if (!ensureResult.canAdd) {
|
|
417
|
+
return { action: 'block', reason: ensureResult.reason || 'cycleBlocked', from, to };
|
|
418
|
+
}
|
|
419
|
+
return { action: 'review', reason: ensureResult.reason || 'missingDependency', from, to };
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* 公共依赖审查弹窗逻辑(insertHeaders 和 _preflightDeps 共享)
|
|
424
|
+
*
|
|
425
|
+
* @param {object} ctx - { spmService, currentTarget, mod, ensureResult, NU, depWarnings, label }
|
|
426
|
+
* @returns {{ blocked: boolean }}
|
|
427
|
+
*/
|
|
428
|
+
function _handleDepReview(ctx) {
|
|
429
|
+
const { spmService, currentTarget, mod, ensureResult, NU, depWarnings, label = '' } = ctx;
|
|
430
|
+
|
|
431
|
+
const fixMode = spmService.getFixMode();
|
|
432
|
+
const buttons = fixMode === 'fix'
|
|
433
|
+
? ['直接插入(信任架构)', '提示操作插入', '自动修复依赖', '取消操作']
|
|
434
|
+
: ['直接插入(信任架构)', '提示操作插入', '取消操作'];
|
|
435
|
+
|
|
436
|
+
const crossTag = ensureResult.crossPackage ? ' (跨包)' : '';
|
|
437
|
+
const prefix = label ? `[${label}] ` : '';
|
|
438
|
+
console.log(` ⚠️ ${prefix}依赖缺失: ${currentTarget} -> ${mod}`);
|
|
439
|
+
|
|
440
|
+
const userChoice = NU.promptWithButtons(
|
|
441
|
+
`检测到依赖缺失:${currentTarget} -> ${mod}${crossTag}\n\n请选择处理方式:`,
|
|
442
|
+
buttons,
|
|
443
|
+
'AutoSnippet SPM 依赖决策',
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
if (userChoice === '取消操作' || (!userChoice && !['直接插入(信任架构)', '提示操作插入', '自动修复依赖'].includes(userChoice))) {
|
|
447
|
+
return { blocked: true };
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (userChoice === '提示操作插入') {
|
|
451
|
+
console.log(` 📋 ${prefix}提示操作:依赖缺失 ${currentTarget} -> ${mod}`);
|
|
452
|
+
depWarnings.set(mod, `${currentTarget} -> ${mod}`);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (userChoice === '自动修复依赖') {
|
|
456
|
+
const fixResult = spmService.addDependency(currentTarget, mod);
|
|
457
|
+
if (fixResult.ok) {
|
|
458
|
+
console.log(` ✅ ${prefix}已自动补齐依赖: ${currentTarget} -> ${mod}${fixResult.crossPackage ? ' (跨包)' : ''} (${fixResult.file})`);
|
|
459
|
+
NU.notify(`已补齐依赖:${currentTarget} -> ${mod}`, 'AutoSnippet SPM');
|
|
460
|
+
} else {
|
|
461
|
+
console.warn(` ⚠️ ${prefix}自动修复失败: ${fixResult.error},继续插入`);
|
|
462
|
+
depWarnings.set(mod, `${currentTarget} -> ${mod}`);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return { blocked: false };
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// ═══════════════════════════════════════════════════════════════
|
|
470
|
+
// §7 Xcode osascript 单条 import 写入
|
|
471
|
+
// ═══════════════════════════════════════════════════════════════
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* 通过 Xcode 自动化插入一条 import,保持 Xcode Undo 可用。
|
|
475
|
+
*
|
|
476
|
+
* 流程:保存剪贴板 → 写入 import 内容 → osascript 跳转+粘贴 → 恢复剪贴板
|
|
477
|
+
*
|
|
478
|
+
* @param {string} importLine 完整的 import 文本
|
|
479
|
+
* @param {number} insertLine 1-based 行号
|
|
480
|
+
* @param {object} XA XcodeAutomation 模块
|
|
481
|
+
* @param {object} CM ClipboardManager 模块
|
|
482
|
+
* @returns {boolean}
|
|
483
|
+
*/
|
|
484
|
+
function _writeImportLineXcode(importLine, insertLine, XA, CM) {
|
|
485
|
+
if (!XA.isXcodeRunning()) return false;
|
|
486
|
+
try {
|
|
487
|
+
const contentToWrite = String(importLine).trim() + '\n';
|
|
488
|
+
const previousClipboard = CM.read();
|
|
489
|
+
|
|
490
|
+
CM.write(contentToWrite);
|
|
491
|
+
const ok = XA.insertAtLineStartInXcode(insertLine);
|
|
492
|
+
|
|
493
|
+
// 始终恢复剪贴板
|
|
494
|
+
if (typeof previousClipboard === 'string') {
|
|
495
|
+
CM.write(previousClipboard);
|
|
496
|
+
}
|
|
497
|
+
return ok;
|
|
498
|
+
} catch {
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// ═══════════════════════════════════════════════════════════════
|
|
504
|
+
// §8 文件写入回退
|
|
505
|
+
// ═══════════════════════════════════════════════════════════════
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* 纯文件写入插入单条 import。
|
|
509
|
+
* Xcode 会因文件变更而自动 reload。
|
|
510
|
+
*/
|
|
511
|
+
function _writeImportLineFile(filePath, importLine, isSwift) {
|
|
512
|
+
try {
|
|
513
|
+
const content = readFileSync(filePath, 'utf8');
|
|
514
|
+
const lines = content.split('\n');
|
|
515
|
+
let lastImportIdx = -1;
|
|
516
|
+
for (let i = 0; i < lines.length; i++) {
|
|
517
|
+
const t = lines[i].trim();
|
|
518
|
+
if (isSwift) {
|
|
519
|
+
if (t.startsWith('import ') && !t.startsWith('import (')) lastImportIdx = i;
|
|
520
|
+
} else {
|
|
521
|
+
if (t.startsWith('#import ') || t.startsWith('#include ') || t.startsWith('@import ')) {
|
|
522
|
+
lastImportIdx = i;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
const insertAt = lastImportIdx >= 0 ? lastImportIdx + 1 : 0;
|
|
527
|
+
lines.splice(insertAt, 0, importLine);
|
|
528
|
+
const newContent = lines.join('\n');
|
|
529
|
+
saveEventFilter.markWrite(filePath, newContent);
|
|
530
|
+
writeFileSync(filePath, newContent, 'utf8');
|
|
531
|
+
return true;
|
|
532
|
+
} catch {
|
|
533
|
+
return false;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// ═══════════════════════════════════════════════════════════════
|
|
538
|
+
// §9 粘贴行号偏移计算
|
|
539
|
+
// ═══════════════════════════════════════════════════════════════
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* 查找文件中最后一个 import 行的行号(1-based,0 表示无 import)
|
|
543
|
+
*/
|
|
544
|
+
function _getLastImportLine(filePath) {
|
|
545
|
+
try {
|
|
546
|
+
if (!existsSync(filePath)) return 0;
|
|
547
|
+
const content = readFileSync(filePath, 'utf8');
|
|
548
|
+
const lines = content.split(/\r?\n/);
|
|
549
|
+
let lastIdx = -1;
|
|
550
|
+
for (let i = 0; i < lines.length; i++) {
|
|
551
|
+
const t = lines[i].trim();
|
|
552
|
+
if (t.startsWith('#import ') || t.startsWith('@import ')
|
|
553
|
+
|| t.startsWith('#include ') || t.startsWith('import ')) {
|
|
554
|
+
lastIdx = i;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
return lastIdx >= 0 ? lastIdx + 1 : 0;
|
|
558
|
+
} catch {
|
|
559
|
+
return 0;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* 计算代码粘贴行号
|
|
565
|
+
*
|
|
566
|
+
* 如果 headers 插入在 trigger 行之前(import 区),trigger 行号需要向下偏移。
|
|
567
|
+
*/
|
|
568
|
+
function _computePasteLineNumber(triggerLineNumber, headerInsertCount, filePath, options = {}) {
|
|
569
|
+
const expectedCount = Number.isFinite(options.expectedHeaderCount)
|
|
570
|
+
? options.expectedHeaderCount
|
|
571
|
+
: headerInsertCount;
|
|
572
|
+
if (expectedCount > 0) {
|
|
573
|
+
if (options.forceOffset) {
|
|
574
|
+
return triggerLineNumber + expectedCount;
|
|
575
|
+
}
|
|
576
|
+
const headerInsertPosition = _getLastImportLine(filePath);
|
|
577
|
+
if (headerInsertPosition > 0 && headerInsertPosition < triggerLineNumber) {
|
|
578
|
+
return triggerLineNumber + expectedCount;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
return triggerLineNumber;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// ═══════════════════════════════════════════════════════════════
|
|
585
|
+
// §10 导出:insertHeaders
|
|
586
|
+
// ═══════════════════════════════════════════════════════════════
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* 统一的头文件插入方法
|
|
590
|
+
*
|
|
591
|
+
* 逐条处理:
|
|
592
|
+
* 1. 三级去重
|
|
593
|
+
* 2. SPM 依赖检查(block/review/continue 决策)
|
|
594
|
+
* 3. Xcode osascript 自动插入,失败则文件写入回退
|
|
595
|
+
* 4. 附加 AutoSnippet 注释后缀
|
|
61
596
|
*
|
|
62
597
|
* @param {import('./FileWatcher.js').FileWatcher} watcher
|
|
63
|
-
* @param {string}
|
|
64
|
-
* @param {string[]} headers
|
|
65
|
-
* @param {object}
|
|
598
|
+
* @param {string} fullPath 目标文件绝对路径
|
|
599
|
+
* @param {string[]} headers 待插入的 import 行数组
|
|
600
|
+
* @param {object} [opts]
|
|
66
601
|
* @returns {Promise<{inserted: string[], skipped: string[], cancelled: boolean}>}
|
|
67
602
|
*/
|
|
68
603
|
export async function insertHeaders(watcher, fullPath, headers, opts = {}) {
|
|
@@ -71,119 +606,172 @@ export async function insertHeaders(watcher, fullPath, headers, opts = {}) {
|
|
|
71
606
|
const NU = await import('../../infrastructure/external/NativeUi.js');
|
|
72
607
|
|
|
73
608
|
const result = { inserted: [], skipped: [], cancelled: false };
|
|
609
|
+
/** @type {Map<string, string>} 模块名 → 提示注释('提示操作插入'按钮选择时记录) */
|
|
610
|
+
const depWarnings = opts.depWarnings instanceof Map ? new Map(opts.depWarnings) : new Map();
|
|
74
611
|
if (!headers || headers.length === 0) return result;
|
|
75
612
|
|
|
76
613
|
const isSwift = opts.isSwift ?? fullPath.endsWith('.swift');
|
|
77
614
|
|
|
78
|
-
// ── 1
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
return result;
|
|
615
|
+
// ── Step 1: 收集已有 imports ──
|
|
616
|
+
const importArray = _collectImportsFromFile(fullPath, isSwift);
|
|
617
|
+
// .m 文件还要收集对应 .h 的 imports
|
|
618
|
+
if (!isSwift && !fullPath.endsWith('.h')) {
|
|
619
|
+
_collectImportsFromHeaderFile(fullPath, importArray);
|
|
84
620
|
}
|
|
85
621
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
622
|
+
// ── Step 2: SPM 服务准备 ──
|
|
623
|
+
// 优先复用 opts 传入的 spmService/currentTarget(避免与 _preflightDeps 重复 load)
|
|
624
|
+
let spmService = opts._spmService || null;
|
|
625
|
+
let currentTarget = opts._currentTarget || null;
|
|
626
|
+
if (!spmService && !opts.skipDepCheck) {
|
|
627
|
+
const inferredModules = _inferModulesFromHeaders(headers);
|
|
628
|
+
if (opts.moduleName && !inferredModules.includes(opts.moduleName)) {
|
|
629
|
+
inferredModules.push(opts.moduleName);
|
|
91
630
|
}
|
|
92
|
-
}
|
|
93
|
-
const newHeaders = headers.filter(h => !existingImports.has(h.trim()));
|
|
94
|
-
if (newHeaders.length === 0) {
|
|
95
|
-
result.skipped = [...headers];
|
|
96
|
-
return result;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// ── 2. 依赖检查(自动推断模块名,过滤系统框架) ──
|
|
100
|
-
if (!opts.skipDepCheck) {
|
|
101
|
-
const inferredModules = opts.moduleName
|
|
102
|
-
? [opts.moduleName]
|
|
103
|
-
: _inferModulesFromHeaders(newHeaders);
|
|
104
|
-
|
|
105
|
-
// 过滤掉系统框架
|
|
106
631
|
const thirdPartyModules = inferredModules.filter(m => !_SYSTEM_FRAMEWORKS.has(m));
|
|
107
|
-
|
|
108
632
|
if (thirdPartyModules.length > 0) {
|
|
109
|
-
const missingModules = [];
|
|
110
|
-
let spmAvailable = false;
|
|
111
|
-
|
|
112
633
|
try {
|
|
113
634
|
const { ServiceContainer } = await import('../../injection/ServiceContainer.js');
|
|
114
635
|
const container = ServiceContainer.getInstance();
|
|
115
|
-
|
|
636
|
+
spmService = container.get('spmService');
|
|
116
637
|
if (spmService) {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
if (!targetNames.has(mod)) {
|
|
123
|
-
missingModules.push(mod);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
638
|
+
if (spmService.getFixMode() === 'off') {
|
|
639
|
+
spmService = null;
|
|
640
|
+
} else {
|
|
641
|
+
try { await spmService.load(); } catch { /* Package.swift 不存在则跳过 */ }
|
|
642
|
+
currentTarget = spmService.resolveCurrentTarget(fullPath);
|
|
126
643
|
}
|
|
127
644
|
}
|
|
128
|
-
} catch {
|
|
129
|
-
|
|
130
|
-
|
|
645
|
+
} catch { /* SPM 检查异常不阻断 */ }
|
|
646
|
+
}
|
|
647
|
+
}
|
|
131
648
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
649
|
+
// ── Step 3: Xcode 自动化准备 ──
|
|
650
|
+
const xcodeReady = XA.isXcodeRunning();
|
|
651
|
+
// 从当前文件内容计算 import 插入基准行(1-based)
|
|
652
|
+
let content;
|
|
653
|
+
try { content = readFileSync(fullPath, 'utf8'); } catch { return result; }
|
|
654
|
+
const baseInsertLine = findImportInsertLine(content, isSwift) + 1; // 0-based → 1-based
|
|
655
|
+
let xcodeOffset = 0; // 每次 Xcode 插入成功后 +1(修正多条 header 行号偏移)
|
|
656
|
+
let fileWriteUsed = false; // 一旦使用文件写入,后续全部走文件写入(避免 Xcode reload 冲突)
|
|
657
|
+
|
|
658
|
+
// ── Step 4: 逐条处理 ──
|
|
659
|
+
for (const header of headers) {
|
|
660
|
+
const headerTrimmed = header.trim();
|
|
661
|
+
if (!headerTrimmed) continue;
|
|
662
|
+
|
|
663
|
+
// ── 三级去重 ──
|
|
664
|
+
// 先按原始格式检查,再按解析后格式检查(同一 header 可能格式不同)
|
|
665
|
+
const preResolvedHeader = _resolveHeaderFormat(headerTrimmed, {
|
|
666
|
+
currentTarget,
|
|
667
|
+
headerModuleName: opts.moduleName || null,
|
|
668
|
+
isSwift,
|
|
669
|
+
fullPath,
|
|
670
|
+
projectRoot: watcher?.projectRoot || null,
|
|
671
|
+
});
|
|
672
|
+
const status = _checkImportStatus(importArray, headerTrimmed, isSwift);
|
|
673
|
+
const statusResolved = (preResolvedHeader !== headerTrimmed)
|
|
674
|
+
? _checkImportStatus(importArray, preResolvedHeader, isSwift)
|
|
675
|
+
: status;
|
|
676
|
+
if (status.hasHeader || statusResolved.hasHeader) {
|
|
677
|
+
console.log(` ⏭️ 已存在(精确匹配): ${preResolvedHeader}`);
|
|
678
|
+
result.skipped.push(preResolvedHeader);
|
|
679
|
+
continue;
|
|
680
|
+
}
|
|
681
|
+
if (status.hasModule || statusResolved.hasModule) {
|
|
682
|
+
console.log(` ⏭️ 已存在(模块匹配): ${preResolvedHeader}`);
|
|
683
|
+
result.skipped.push(preResolvedHeader);
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
686
|
+
if (status.hasSimilarHeader || statusResolved.hasSimilarHeader) {
|
|
687
|
+
console.log(` ⏭️ 已存在(相似头文件): ${preResolvedHeader}`);
|
|
688
|
+
result.skipped.push(preResolvedHeader);
|
|
689
|
+
continue;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// ── SPM 依赖检查 ──
|
|
693
|
+
const headerModules = _inferModulesFromHeaders([headerTrimmed]);
|
|
694
|
+
if (spmService && currentTarget && !opts.skipDepCheck) {
|
|
695
|
+
for (const mod of headerModules) {
|
|
696
|
+
if (_SYSTEM_FRAMEWORKS.has(mod) || mod === currentTarget) continue;
|
|
697
|
+
|
|
698
|
+
const ensureResult = spmService.ensureDependency(currentTarget, mod);
|
|
699
|
+
const decision = _evaluateDepResult(ensureResult, currentTarget, mod);
|
|
700
|
+
|
|
701
|
+
if (decision.action === 'block') {
|
|
702
|
+
console.warn(` ⛔ 依赖被阻止: ${currentTarget} -> ${mod} (${decision.reason})`);
|
|
703
|
+
NU.notify(
|
|
704
|
+
`已阻止依赖注入\n${currentTarget} -> ${mod}\n${decision.reason}`,
|
|
705
|
+
'AutoSnippet SPM 依赖策略',
|
|
706
|
+
);
|
|
145
707
|
result.cancelled = true;
|
|
146
708
|
return result;
|
|
147
709
|
}
|
|
710
|
+
|
|
711
|
+
if (decision.action === 'review') {
|
|
712
|
+
const reviewResult = _handleDepReview({
|
|
713
|
+
spmService, currentTarget, mod, ensureResult, NU, depWarnings,
|
|
714
|
+
});
|
|
715
|
+
if (reviewResult.blocked) {
|
|
716
|
+
console.log(` ⏹️ 用户取消`);
|
|
717
|
+
result.cancelled = true;
|
|
718
|
+
return result;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
148
721
|
}
|
|
149
722
|
}
|
|
150
|
-
}
|
|
151
723
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
const
|
|
724
|
+
// ── 构建带注释后缀的 import 行 ──
|
|
725
|
+
// 复用 dedup 阶段已计算的 preResolvedHeader
|
|
726
|
+
const resolvedHeader = preResolvedHeader;
|
|
727
|
+
const depHint = headerModules.find(m => depWarnings.has(m));
|
|
728
|
+
const importLine = depHint
|
|
729
|
+
? _withAutoSnippetNote(resolvedHeader) + ` // ⚠️ 依赖缺失: ${depWarnings.get(depHint)},需手动补齐 Package.swift`
|
|
730
|
+
: _withAutoSnippetNote(resolvedHeader);
|
|
157
731
|
|
|
158
|
-
//
|
|
159
|
-
|
|
160
|
-
const needsBlankLine = lineAfterInsert.trim().length > 0;
|
|
161
|
-
const toInsert = needsBlankLine ? [...newHeaders, ''] : [...newHeaders];
|
|
732
|
+
// ── 写入:Xcode 自动化优先 → 文件写入回退 ──
|
|
733
|
+
let inserted = false;
|
|
162
734
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
735
|
+
if (xcodeReady && !fileWriteUsed) {
|
|
736
|
+
// 逐条 osascript 跳转 + 粘贴
|
|
737
|
+
inserted = _writeImportLineXcode(importLine, baseInsertLine + xcodeOffset, XA, CM);
|
|
738
|
+
if (inserted) {
|
|
739
|
+
xcodeOffset++;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
if (!inserted) {
|
|
744
|
+
_writeImportLineFile(fullPath, importLine, isSwift);
|
|
745
|
+
fileWriteUsed = true;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
result.inserted.push(resolvedHeader);
|
|
749
|
+
importArray.push(resolvedHeader); // 添加到去重列表(用解析后格式)
|
|
750
|
+
console.log(` + ${resolvedHeader}`);
|
|
169
751
|
}
|
|
170
752
|
|
|
171
|
-
|
|
172
|
-
console.log(`
|
|
753
|
+
if (result.inserted.length > 0) {
|
|
754
|
+
console.log(` 📦 已添加 ${result.inserted.length} 个依赖`);
|
|
173
755
|
}
|
|
174
756
|
return result;
|
|
175
757
|
}
|
|
176
758
|
|
|
759
|
+
// ═══════════════════════════════════════════════════════════════
|
|
760
|
+
// §11 导出:insertCodeToXcode
|
|
761
|
+
// ═══════════════════════════════════════════════════════════════
|
|
762
|
+
|
|
177
763
|
/**
|
|
178
|
-
* 将选中的搜索结果代码插入 Xcode
|
|
764
|
+
* 将选中的搜索结果代码插入 Xcode
|
|
179
765
|
*
|
|
180
|
-
*
|
|
766
|
+
* 流程:
|
|
181
767
|
* 1. 找到触发行号
|
|
182
|
-
* 2.
|
|
183
|
-
* 3.
|
|
184
|
-
* 4.
|
|
185
|
-
* 5.
|
|
186
|
-
* 6.
|
|
768
|
+
* 2. Preflight — 预检依赖决策(不实际写入)
|
|
769
|
+
* 3. Cut 触发行内容(Xcode 剪切,不写文件)
|
|
770
|
+
* 4. 构建带缩进 + 注释标记的代码块
|
|
771
|
+
* 5. 插入 Headers(Xcode osascript / 文件写入)
|
|
772
|
+
* 6. 计算偏移后的粘贴行号(computePasteLineNumber)
|
|
773
|
+
* 7. Jump 到粘贴行 → 选中行内容 → Cmd+V 粘贴替换
|
|
774
|
+
* 8. 任一步失败 → 降级到纯文件写入
|
|
187
775
|
*
|
|
188
776
|
* @param {import('./FileWatcher.js').FileWatcher} watcher
|
|
189
777
|
*/
|
|
@@ -199,67 +787,116 @@ export async function insertCodeToXcode(watcher, fullPath, selected, triggerLine
|
|
|
199
787
|
}
|
|
200
788
|
|
|
201
789
|
const headersToInsert = (selected.headers || []).filter(h => h && h.trim());
|
|
790
|
+
const isSwift = fullPath.endsWith('.swift');
|
|
202
791
|
|
|
203
792
|
// ═══════════════════════════════════════════════════════
|
|
204
|
-
// 主路径:Xcode
|
|
793
|
+
// 主路径:Xcode 自动化
|
|
205
794
|
// ═══════════════════════════════════════════════════════
|
|
206
795
|
if (XA.isXcodeRunning()) {
|
|
207
|
-
//
|
|
208
|
-
|
|
796
|
+
// ── 窗口上下文验证 ──
|
|
797
|
+
if (!XA.isXcodeFrontmost()) {
|
|
798
|
+
console.warn(` ⚠️ Xcode 不是前台应用,自动化操作可能不准确`);
|
|
799
|
+
// 宽松模式:仅警告,不阻断
|
|
800
|
+
// 如需严格模式,可设置 ASD_XCODE_STRICT_FOCUS=1
|
|
801
|
+
if (process.env.ASD_XCODE_STRICT_FOCUS === '1') {
|
|
802
|
+
console.warn(` ⏹️ ASD_XCODE_STRICT_FOCUS=1, 跳过自动化`);
|
|
803
|
+
return _fileInsertFallback(fullPath, selected, triggerLine, headersToInsert, watcher);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
// ── Step 1: 找到触发行号 ──
|
|
807
|
+
let content;
|
|
808
|
+
try { content = readFileSync(fullPath, 'utf8'); } catch {
|
|
809
|
+
return _fileInsertFallback(fullPath, selected, triggerLine, headersToInsert, watcher);
|
|
810
|
+
}
|
|
209
811
|
const triggerLineNumber = findTriggerLineNumber(content, triggerLine);
|
|
210
812
|
if (triggerLineNumber < 0) {
|
|
211
813
|
console.warn(` ⚠️ 未在文件中找到触发行,降级为文件写入`);
|
|
212
814
|
return _fileInsertFallback(fullPath, selected, triggerLine, headersToInsert, watcher);
|
|
213
815
|
}
|
|
214
816
|
|
|
215
|
-
//
|
|
817
|
+
// 计算触发行缩进
|
|
818
|
+
const lines = content.split(/\r?\n/);
|
|
819
|
+
const triggerContent = lines[triggerLineNumber - 1] || '';
|
|
820
|
+
const indentMatch = triggerContent.match(/^(\s*)/);
|
|
821
|
+
const indent = indentMatch ? indentMatch[1] : '';
|
|
822
|
+
|
|
823
|
+
// ── Step 2: Preflight 预检依赖 ──
|
|
824
|
+
let preflightDepWarnings = null;
|
|
825
|
+
let _spmServiceCached = null;
|
|
826
|
+
let _currentTargetCached = null;
|
|
827
|
+
if (headersToInsert.length > 0) {
|
|
828
|
+
const preflight = await _preflightDeps(fullPath, headersToInsert, selected, NU);
|
|
829
|
+
if (preflight.blocked) {
|
|
830
|
+
console.log(` ⏹️ 依赖检查被阻止,跳过代码插入`);
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
if (preflight.depWarnings && preflight.depWarnings.size > 0) {
|
|
834
|
+
preflightDepWarnings = preflight.depWarnings;
|
|
835
|
+
}
|
|
836
|
+
// 缓存 spmService/currentTarget 供 insertHeaders 复用(避免重复 load)
|
|
837
|
+
_spmServiceCached = preflight._spmService || null;
|
|
838
|
+
_currentTargetCached = preflight._currentTarget || null;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// ── Step 3: 剪切触发行内容 ──
|
|
216
842
|
const cutOk = XA.cutLineInXcode(triggerLineNumber);
|
|
217
843
|
if (!cutOk) {
|
|
218
844
|
console.warn(` ⚠️ 自动剪切失败,降级为文件写入`);
|
|
219
|
-
|
|
845
|
+
// Preflight 已通过,skipDepCheck 避免重复弹窗
|
|
846
|
+
return _fileInsertFallback(fullPath, selected, triggerLine, headersToInsert, watcher, { skipDepCheck: true });
|
|
220
847
|
}
|
|
221
848
|
await _sleep(300);
|
|
222
849
|
|
|
223
|
-
// Step
|
|
850
|
+
// ── Step 4: 构建带缩进的代码块 ──
|
|
851
|
+
const codeLines = code.split(/\r?\n/);
|
|
852
|
+
// 移除末尾空行
|
|
853
|
+
while (codeLines.length > 0 && !codeLines[codeLines.length - 1].trim()) {
|
|
854
|
+
codeLines.pop();
|
|
855
|
+
}
|
|
856
|
+
const indentedLines = codeLines.map(line => line ? indent + line : line);
|
|
857
|
+
// 注释标记
|
|
858
|
+
const commentMarker = _generateInsertMarker(fullPath, selected);
|
|
859
|
+
const markedLines = commentMarker
|
|
860
|
+
? [indent + commentMarker, ...indentedLines]
|
|
861
|
+
: indentedLines;
|
|
862
|
+
const indentedCode = markedLines.join('\n');
|
|
863
|
+
|
|
864
|
+
// ── Step 5: 插入 Headers ──
|
|
224
865
|
let headerInsertCount = 0;
|
|
225
866
|
if (headersToInsert.length > 0) {
|
|
226
867
|
const headerResult = await insertHeaders(watcher, fullPath, headersToInsert, {
|
|
227
868
|
moduleName: selected.moduleName || null,
|
|
869
|
+
isSwift,
|
|
870
|
+
skipDepCheck: true, // Preflight 已检查过
|
|
871
|
+
depWarnings: preflightDepWarnings,
|
|
872
|
+
_spmService: _spmServiceCached,
|
|
873
|
+
_currentTarget: _currentTargetCached,
|
|
228
874
|
});
|
|
229
875
|
if (headerResult.cancelled) {
|
|
230
|
-
console.log(` ⏹️
|
|
876
|
+
console.log(` ⏹️ Headers 插入被取消`);
|
|
231
877
|
return;
|
|
232
878
|
}
|
|
233
879
|
headerInsertCount = headerResult.inserted.length;
|
|
234
880
|
}
|
|
235
881
|
|
|
236
|
-
// Step
|
|
237
|
-
//
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
// 如果补了空行,再加 1
|
|
246
|
-
const lines = content.split('\n');
|
|
247
|
-
const lineAfterHeaders = lines[importInsertLine + headerInsertCount - 1];
|
|
248
|
-
if (lineAfterHeaders !== undefined && lineAfterHeaders.trim() === '') {
|
|
249
|
-
// insertHeaders 补了空行
|
|
250
|
-
pasteLineNumber += 1;
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
}
|
|
882
|
+
// ── Step 6: 计算偏移后的粘贴行号 ──
|
|
883
|
+
// 使用实际插入的 header 数量计算偏移,而非期望数量
|
|
884
|
+
// 当 headers 全部重复被跳过时,headerInsertCount = 0,不应偏移
|
|
885
|
+
const pasteLineNumber = _computePasteLineNumber(
|
|
886
|
+
triggerLineNumber,
|
|
887
|
+
headerInsertCount,
|
|
888
|
+
fullPath,
|
|
889
|
+
{ forceOffset: headerInsertCount > 0, expectedHeaderCount: headerInsertCount },
|
|
890
|
+
);
|
|
254
891
|
|
|
255
|
-
//
|
|
892
|
+
// 如果 headers 通过文件写入,等待 Xcode reload
|
|
256
893
|
if (headerInsertCount > 0) {
|
|
257
894
|
await _sleep(600);
|
|
258
895
|
}
|
|
259
896
|
|
|
260
|
-
// Step
|
|
897
|
+
// ── Step 7: Jump + 选中行内容 + 粘贴替换 ──
|
|
261
898
|
await CM.withClipboardSave(async () => {
|
|
262
|
-
const wrote = CM.write(
|
|
899
|
+
const wrote = CM.write(indentedCode);
|
|
263
900
|
if (!wrote) {
|
|
264
901
|
console.warn(` ⚠️ 剪贴板写入失败`);
|
|
265
902
|
return;
|
|
@@ -272,7 +909,7 @@ export async function insertCodeToXcode(watcher, fullPath, selected, triggerLine
|
|
|
272
909
|
});
|
|
273
910
|
|
|
274
911
|
console.log(` ✅ 代码已粘贴到 Xcode(可 Cmd+Z 撤销)`);
|
|
275
|
-
NU.notify(`已插入「${selected.title}」`, 'AutoSnippet');
|
|
912
|
+
NU.notify(`已插入「${selected.title || '代码片段'}」`, 'AutoSnippet');
|
|
276
913
|
return;
|
|
277
914
|
}
|
|
278
915
|
|
|
@@ -282,14 +919,90 @@ export async function insertCodeToXcode(watcher, fullPath, selected, triggerLine
|
|
|
282
919
|
return _fileInsertFallback(fullPath, selected, triggerLine, headersToInsert, watcher);
|
|
283
920
|
}
|
|
284
921
|
|
|
922
|
+
// ═══════════════════════════════════════════════════════════════
|
|
923
|
+
// §12 Preflight 依赖预检
|
|
924
|
+
// ═══════════════════════════════════════════════════════════════
|
|
925
|
+
|
|
285
926
|
/**
|
|
286
|
-
*
|
|
927
|
+
* 预检所有 headers 的 SPM 依赖状态
|
|
928
|
+
*
|
|
929
|
+
* 不实际写入文件,只检查并弹窗确认。
|
|
930
|
+
* 返回 { blocked: true } 表示有依赖被阻止或用户取消。
|
|
287
931
|
*/
|
|
288
|
-
async function
|
|
932
|
+
async function _preflightDeps(fullPath, headers, selected, NU) {
|
|
933
|
+
const result = { blocked: false };
|
|
934
|
+
|
|
935
|
+
// 始终从所有 headers 推断模块(不仅依赖 selected.moduleName)
|
|
936
|
+
const inferredModules = _inferModulesFromHeaders(headers);
|
|
937
|
+
if (selected.moduleName && !inferredModules.includes(selected.moduleName)) {
|
|
938
|
+
inferredModules.push(selected.moduleName);
|
|
939
|
+
}
|
|
940
|
+
const thirdPartyModules = inferredModules.filter(m => !_SYSTEM_FRAMEWORKS.has(m));
|
|
941
|
+
if (thirdPartyModules.length === 0) return result;
|
|
942
|
+
|
|
943
|
+
try {
|
|
944
|
+
const { ServiceContainer } = await import('../../injection/ServiceContainer.js');
|
|
945
|
+
const container = ServiceContainer.getInstance();
|
|
946
|
+
const spmService = container.get('spmService');
|
|
947
|
+
if (!spmService) return result;
|
|
948
|
+
|
|
949
|
+
// Fix Mode 检查:off 模式完全跳过
|
|
950
|
+
if (spmService.getFixMode() === 'off') return result;
|
|
951
|
+
|
|
952
|
+
try { await spmService.load(); } catch { return result; }
|
|
953
|
+
|
|
954
|
+
const currentTarget = spmService.resolveCurrentTarget(fullPath);
|
|
955
|
+
if (!currentTarget) return result;
|
|
956
|
+
|
|
957
|
+
for (const mod of thirdPartyModules) {
|
|
958
|
+
if (mod === currentTarget) continue;
|
|
959
|
+
|
|
960
|
+
const ensureResult = spmService.ensureDependency(currentTarget, mod);
|
|
961
|
+
const decision = _evaluateDepResult(ensureResult, currentTarget, mod);
|
|
962
|
+
|
|
963
|
+
if (decision.action === 'block') {
|
|
964
|
+
console.warn(` ⛔ [Preflight] 依赖被阻止: ${currentTarget} -> ${mod} (${decision.reason})`);
|
|
965
|
+
NU.notify(
|
|
966
|
+
`已阻止依赖注入\n${currentTarget} -> ${mod}\n${decision.reason}`,
|
|
967
|
+
'AutoSnippet SPM 依赖策略',
|
|
968
|
+
);
|
|
969
|
+
result.blocked = true;
|
|
970
|
+
return result;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
if (decision.action === 'review') {
|
|
974
|
+
if (!result.depWarnings) result.depWarnings = new Map();
|
|
975
|
+
const reviewResult = _handleDepReview({
|
|
976
|
+
spmService, currentTarget, mod, ensureResult, NU,
|
|
977
|
+
depWarnings: result.depWarnings, label: 'Preflight',
|
|
978
|
+
});
|
|
979
|
+
if (reviewResult.blocked) {
|
|
980
|
+
result.blocked = true;
|
|
981
|
+
return result;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// 缓存 spmService/currentTarget 供下游 insertHeaders 复用
|
|
987
|
+
result._spmService = spmService;
|
|
988
|
+
result._currentTarget = currentTarget;
|
|
989
|
+
} catch (err) {
|
|
990
|
+
console.warn(` ⚠️ Preflight 依赖检查异常: ${err.message}`);
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
return result;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// ═══════════════════════════════════════════════════════════════
|
|
997
|
+
// §13 文件写入降级
|
|
998
|
+
// ═══════════════════════════════════════════════════════════════
|
|
999
|
+
|
|
1000
|
+
async function _fileInsertFallback(fullPath, selected, triggerLine, headersToInsert, watcher, opts = {}) {
|
|
289
1001
|
// 先写 headers
|
|
290
1002
|
if (headersToInsert.length > 0) {
|
|
291
1003
|
const headerResult = await insertHeaders(watcher, fullPath, headersToInsert, {
|
|
292
1004
|
moduleName: selected.moduleName || null,
|
|
1005
|
+
skipDepCheck: opts.skipDepCheck || false, // Preflight 已通过时跳过重复检查
|
|
293
1006
|
});
|
|
294
1007
|
if (headerResult.cancelled) return;
|
|
295
1008
|
}
|
|
@@ -298,12 +1011,48 @@ async function _fileInsertFallback(fullPath, selected, triggerLine, headersToIns
|
|
|
298
1011
|
const code = selected.code || '';
|
|
299
1012
|
try {
|
|
300
1013
|
const content = readFileSync(fullPath, 'utf8');
|
|
301
|
-
const
|
|
302
|
-
|
|
1014
|
+
const lines = content.split(/\r?\n/);
|
|
1015
|
+
const triggerTrimmed = triggerLine.trim();
|
|
1016
|
+
|
|
1017
|
+
// 从后往前查找触发行
|
|
1018
|
+
let found = -1;
|
|
1019
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
1020
|
+
if (lines[i].trim() === triggerTrimmed) {
|
|
1021
|
+
found = i;
|
|
1022
|
+
break;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
if (found >= 0) {
|
|
1027
|
+
// 计算缩进 → 对齐 → 替换
|
|
1028
|
+
const triggerContent = lines[found];
|
|
1029
|
+
const indentMatch = triggerContent.match(/^(\s*)/);
|
|
1030
|
+
const indent = indentMatch ? indentMatch[1] : '';
|
|
1031
|
+
|
|
1032
|
+
const codeLines = code.split(/\r?\n/);
|
|
1033
|
+
while (codeLines.length > 0 && !codeLines[codeLines.length - 1].trim()) {
|
|
1034
|
+
codeLines.pop();
|
|
1035
|
+
}
|
|
1036
|
+
const indentedLines = codeLines.map(line => line ? indent + line : line);
|
|
1037
|
+
|
|
1038
|
+
const commentMarker = _generateInsertMarker(fullPath, selected);
|
|
1039
|
+
const markedLines = commentMarker
|
|
1040
|
+
? [indent + commentMarker, ...indentedLines]
|
|
1041
|
+
: indentedLines;
|
|
1042
|
+
|
|
1043
|
+
while (markedLines.length > 0 && !markedLines[markedLines.length - 1].trim()) {
|
|
1044
|
+
markedLines.pop();
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
const newLines = [...lines.slice(0, found), ...markedLines, ...lines.slice(found + 1)];
|
|
1048
|
+
const newContent = newLines.join('\n');
|
|
1049
|
+
saveEventFilter.markWrite(fullPath, newContent);
|
|
303
1050
|
writeFileSync(fullPath, newContent, 'utf8');
|
|
304
1051
|
console.log(` ✅ 代码已写入文件(替换触发行)`);
|
|
305
1052
|
} else {
|
|
306
|
-
|
|
1053
|
+
const appendContent = content + '\n' + code + '\n';
|
|
1054
|
+
saveEventFilter.markWrite(fullPath, appendContent);
|
|
1055
|
+
writeFileSync(fullPath, appendContent, 'utf8');
|
|
307
1056
|
console.log(` ✅ 代码已追加到文件末尾`);
|
|
308
1057
|
}
|
|
309
1058
|
} catch (err) {
|
|
@@ -311,8 +1060,38 @@ async function _fileInsertFallback(fullPath, selected, triggerLine, headersToIns
|
|
|
311
1060
|
}
|
|
312
1061
|
}
|
|
313
1062
|
|
|
1063
|
+
// ═══════════════════════════════════════════════════════════════
|
|
1064
|
+
// §14 注释标记生成
|
|
1065
|
+
// ═══════════════════════════════════════════════════════════════
|
|
1066
|
+
|
|
1067
|
+
function _generateInsertMarker(filePath, selected) {
|
|
1068
|
+
try {
|
|
1069
|
+
const ext = (filePath.match(/\.[^.]+$/) || [''])[0].toLowerCase();
|
|
1070
|
+
const trigger = selected.trigger ? `[${selected.trigger}]` : '';
|
|
1071
|
+
const recipeName = selected.name ? ` from ${selected.name}` : '';
|
|
1072
|
+
const timestamp = new Date().toLocaleString('zh-CN', {
|
|
1073
|
+
year: 'numeric', month: '2-digit', day: '2-digit',
|
|
1074
|
+
hour: '2-digit', minute: '2-digit',
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
const marker = `🤖 AutoSnippet${trigger}${recipeName} @ ${timestamp}`;
|
|
1078
|
+
|
|
1079
|
+
if (['.py', '.rb'].includes(ext)) return `# ${marker}`;
|
|
1080
|
+
if (['.lua', '.sql'].includes(ext)) return `-- ${marker}`;
|
|
1081
|
+
if (['.html', '.xml', '.svg'].includes(ext)) return `<!-- ${marker} -->`;
|
|
1082
|
+
if (['.css', '.scss', '.less'].includes(ext)) return `/* ${marker} */`;
|
|
1083
|
+
return `// ${marker}`;
|
|
1084
|
+
} catch {
|
|
1085
|
+
return null;
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
// ═══════════════════════════════════════════════════════════════
|
|
1090
|
+
// §15 工具函数
|
|
1091
|
+
// ═══════════════════════════════════════════════════════════════
|
|
1092
|
+
|
|
314
1093
|
/**
|
|
315
|
-
*
|
|
1094
|
+
* 查找触发行的行号(1-based,-1 表示未找到)
|
|
316
1095
|
*/
|
|
317
1096
|
export function findTriggerLineNumber(content, triggerLine) {
|
|
318
1097
|
if (!content || !triggerLine) return -1;
|
|
@@ -325,7 +1104,7 @@ export function findTriggerLineNumber(content, triggerLine) {
|
|
|
325
1104
|
}
|
|
326
1105
|
|
|
327
1106
|
/**
|
|
328
|
-
* 查找 import
|
|
1107
|
+
* 查找 import 语句的插入位置(0-based 行索引,在最后一个 import 之后)
|
|
329
1108
|
*/
|
|
330
1109
|
export function findImportInsertLine(content, isSwift) {
|
|
331
1110
|
const lines = content.split('\n');
|
|
@@ -333,13 +1112,9 @@ export function findImportInsertLine(content, isSwift) {
|
|
|
333
1112
|
for (let i = 0; i < lines.length; i++) {
|
|
334
1113
|
const t = lines[i].trim();
|
|
335
1114
|
if (isSwift) {
|
|
336
|
-
if (t.startsWith('import ') && !t.startsWith('import ('))
|
|
337
|
-
lastImportLine = i;
|
|
338
|
-
}
|
|
1115
|
+
if (t.startsWith('import ') && !t.startsWith('import (')) lastImportLine = i;
|
|
339
1116
|
} else {
|
|
340
|
-
if (t.startsWith('#import') || t.startsWith('@import'))
|
|
341
|
-
lastImportLine = i;
|
|
342
|
-
}
|
|
1117
|
+
if (t.startsWith('#import') || t.startsWith('@import')) lastImportLine = i;
|
|
343
1118
|
}
|
|
344
1119
|
}
|
|
345
1120
|
return lastImportLine >= 0 ? lastImportLine + 1 : 0;
|