autosnippet 2.7.0 → 2.8.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 +138 -66
- package/bin/api-server.js +5 -0
- package/bin/cli.js +26 -0
- package/bin/mcp-server.js +22 -0
- package/dashboard/dist/assets/{icons-Cq4-iQhP.js → icons-B_Xg4B-s.js} +61 -61
- package/dashboard/dist/assets/index-CkIih2CC.css +1 -0
- package/dashboard/dist/assets/index-Duc8Qk-c.js +197 -0
- package/dashboard/dist/index.html +3 -3
- package/lib/bootstrap.js +17 -0
- package/lib/cli/SetupService.js +53 -0
- package/lib/external/ai/providers/ClaudeProvider.js +12 -1
- package/lib/external/ai/providers/GoogleGeminiProvider.js +13 -1
- package/lib/external/ai/providers/OpenAiProvider.js +13 -3
- package/lib/external/mcp/McpServer.js +11 -4
- package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +194 -5
- package/lib/external/mcp/handlers/bootstrap/pipeline/tier-scheduler.js +8 -10
- package/lib/external/mcp/handlers/bootstrap.js +8 -0
- package/lib/external/mcp/handlers/skill.js +202 -0
- package/lib/external/mcp/tools.js +54 -1
- package/lib/http/routes/ai.js +155 -1
- package/lib/infrastructure/config/Paths.js +3 -0
- package/lib/infrastructure/database/DatabaseConnection.js +6 -1
- package/lib/infrastructure/vector/JsonVectorAdapter.js +2 -0
- package/lib/service/automation/handlers/AlinkHandler.js +43 -4
- package/lib/service/candidate/CandidateFileWriter.js +4 -0
- package/lib/service/chat/AnalystAgent.js +37 -8
- package/lib/service/chat/CandidateGuardrail.js +3 -3
- package/lib/service/chat/ChatAgent.js +20 -1
- package/lib/service/chat/ConversationStore.js +3 -0
- package/lib/service/chat/HandoffProtocol.js +1 -0
- package/lib/service/chat/Memory.js +3 -0
- package/lib/service/chat/ProducerAgent.js +53 -0
- package/lib/service/chat/tools.js +13 -6
- package/lib/service/guard/ExclusionManager.js +2 -0
- package/lib/service/guard/RuleLearner.js +2 -0
- package/lib/service/quality/FeedbackCollector.js +2 -0
- package/lib/service/recipe/RecipeFileWriter.js +4 -0
- package/lib/service/recipe/RecipeStatsTracker.js +2 -0
- package/lib/service/skills/SignalCollector.js +2 -0
- package/lib/shared/PathGuard.js +314 -0
- package/package.json +1 -1
- package/resources/native-ui/combined-window.swift +494 -0
- package/dashboard/dist/assets/index-DBxH7pVn.css +0 -1
- package/dashboard/dist/assets/index-Dw2F6qAS.js +0 -197
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
import AppKit
|
|
2
|
+
import Foundation
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* AutoSnippet 组合窗口 - 列表 + 预览一体化
|
|
6
|
+
* 左侧:搜索结果列表
|
|
7
|
+
* 右侧:代码预览(实时更新)
|
|
8
|
+
* 底部:操作按钮
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// MARK: - 语法高亮
|
|
12
|
+
|
|
13
|
+
struct SyntaxHighlighter {
|
|
14
|
+
static let keywords = ["func", "var", "let", "if", "else", "guard", "return", "import", "class", "struct", "enum", "protocol", "extension", "private", "public", "internal", "fileprivate", "static", "override", "init", "deinit", "self", "super", "nil", "true", "false", "for", "while", "repeat", "switch", "case", "default", "break", "continue", "fallthrough", "where", "in", "throws", "throw", "try", "catch", "async", "await", "typealias", "associatedtype", "weak", "unowned", "lazy", "final", "required", "convenience", "mutating", "nonmutating", "open", "inout", "some", "any",
|
|
15
|
+
// Objective-C
|
|
16
|
+
"@interface", "@implementation", "@end", "@property", "@synthesize", "@dynamic", "@class", "@protocol", "@optional", "@required", "@public", "@private", "@protected", "@package", "@selector", "@encode", "@synchronized", "@autoreleasepool", "@try", "@catch", "@finally", "@throw", "YES", "NO", "NULL", "nil", "self", "super", "id", "Class", "SEL", "IMP", "BOOL", "instancetype", "void", "char", "short", "int", "long", "float", "double", "signed", "unsigned", "const", "static", "extern", "auto", "register", "volatile", "inline", "restrict", "typedef", "sizeof", "typeof", "return", "if", "else", "for", "while", "do", "switch", "case", "default", "break", "continue", "goto", "struct", "union", "enum",
|
|
17
|
+
// TypeScript/JavaScript
|
|
18
|
+
"function", "const", "async", "await", "new", "this", "typeof", "instanceof", "export", "import", "from", "as", "default", "extends", "implements", "interface", "type", "namespace", "module", "declare", "abstract", "readonly", "keyof", "infer", "never", "unknown", "any", "void", "null", "undefined", "number", "string", "boolean", "symbol", "bigint", "object"
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
static let typeKeywords = ["String", "Int", "Double", "Float", "Bool", "Array", "Dictionary", "Set", "Optional", "Any", "AnyObject", "Void", "Never", "Error", "Result", "URL", "Data", "Date", "UUID", "NSObject", "NSString", "NSNumber", "NSArray", "NSDictionary", "NSSet", "NSData", "NSDate", "NSURL", "CGFloat", "CGPoint", "CGSize", "CGRect", "UIView", "UIViewController", "UIButton", "UILabel", "UIImage", "UIColor", "NSView", "NSViewController", "NSButton", "NSTextField", "NSImage", "NSColor", "Promise", "Observable", "Subject"]
|
|
22
|
+
|
|
23
|
+
static func highlight(_ code: String) -> NSAttributedString {
|
|
24
|
+
let attributed = NSMutableAttributedString(string: code)
|
|
25
|
+
let fullRange = NSRange(location: 0, length: code.utf16.count)
|
|
26
|
+
|
|
27
|
+
// 基础样式 - SF Mono 字体 (Xcode 默认) - 增大到 14
|
|
28
|
+
let baseFont = NSFont(name: "SFMono-Regular", size: 14) ?? NSFont.monospacedSystemFont(ofSize: 14, weight: .regular)
|
|
29
|
+
// 普通文本 - Xcode 默认浅灰白色
|
|
30
|
+
let baseColor = NSColor(calibratedRed: 0.83, green: 0.84, blue: 0.85, alpha: 1.0) // #D4D4D6
|
|
31
|
+
attributed.addAttribute(.font, value: baseFont, range: fullRange)
|
|
32
|
+
attributed.addAttribute(.foregroundColor, value: baseColor, range: fullRange)
|
|
33
|
+
|
|
34
|
+
// 高亮注释 - Xcode 默认绿色 (系统 Green)
|
|
35
|
+
let commentColor = NSColor(calibratedRed: 0.42, green: 0.75, blue: 0.31, alpha: 1.0) // #6BBF4F
|
|
36
|
+
highlightPattern(attributed, pattern: "//.*$", color: commentColor, options: .anchorsMatchLines)
|
|
37
|
+
highlightPattern(attributed, pattern: "/\\*[\\s\\S]*?\\*/", color: commentColor)
|
|
38
|
+
|
|
39
|
+
// 高亮字符串 - Xcode 默认橙红色 (系统 Red/Orange 混合)
|
|
40
|
+
let stringColor = NSColor(calibratedRed: 0.98, green: 0.42, blue: 0.33, alpha: 1.0) // #FA6B54
|
|
41
|
+
highlightPattern(attributed, pattern: "\"(?:[^\"\\\\]|\\\\.)*\"", color: stringColor)
|
|
42
|
+
highlightPattern(attributed, pattern: "'(?:[^'\\\\]|\\\\.)*'", color: stringColor)
|
|
43
|
+
highlightPattern(attributed, pattern: "@\"(?:[^\"\\\\]|\\\\.)*\"", color: stringColor) // ObjC string
|
|
44
|
+
|
|
45
|
+
// 高亮数字 - Xcode 默认紫色 (Light Purple)
|
|
46
|
+
let numberColor = NSColor(calibratedRed: 0.69, green: 0.54, blue: 0.89, alpha: 1.0) // #B08AE3
|
|
47
|
+
highlightPattern(attributed, pattern: "\\b\\d+\\.?\\d*\\b", color: numberColor)
|
|
48
|
+
|
|
49
|
+
// 高亮类型关键字 - Xcode 默认青绿色 (Teal/Cyan)
|
|
50
|
+
let typeColor = NSColor(calibratedRed: 0.40, green: 0.84, blue: 0.89, alpha: 1.0) // #66D7E3
|
|
51
|
+
for keyword in typeKeywords {
|
|
52
|
+
highlightWord(attributed, word: keyword, color: typeColor)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 高亮关键字 - Xcode 默认粉紫色 (Magenta/Pink)
|
|
56
|
+
let keywordColor = NSColor(calibratedRed: 0.98, green: 0.42, blue: 0.69, alpha: 1.0) // #FA6BB0
|
|
57
|
+
// Objective-C @ 前缀关键字 - 使用浅棕色
|
|
58
|
+
let atKeywordColor = NSColor(calibratedRed: 0.83, green: 0.60, blue: 0.45, alpha: 1.0) // #D49973
|
|
59
|
+
for keyword in keywords {
|
|
60
|
+
if keyword.hasPrefix("@") {
|
|
61
|
+
highlightPattern(attributed, pattern: "\(keyword)\\b", color: atKeywordColor)
|
|
62
|
+
} else {
|
|
63
|
+
highlightWord(attributed, word: keyword, color: keywordColor)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return attributed
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private static func highlightPattern(_ attributed: NSMutableAttributedString, pattern: String, color: NSColor, options: NSRegularExpression.Options = []) {
|
|
71
|
+
guard let regex = try? NSRegularExpression(pattern: pattern, options: options) else { return }
|
|
72
|
+
let string = attributed.string
|
|
73
|
+
let range = NSRange(location: 0, length: string.utf16.count)
|
|
74
|
+
|
|
75
|
+
for match in regex.matches(in: string, options: [], range: range) {
|
|
76
|
+
attributed.addAttribute(.foregroundColor, value: color, range: match.range)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private static func highlightWord(_ attributed: NSMutableAttributedString, word: String, color: NSColor) {
|
|
81
|
+
let pattern = "\\b\(NSRegularExpression.escapedPattern(for: word))\\b"
|
|
82
|
+
highlightPattern(attributed, pattern: pattern, color: color)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// MARK: - 数据结构
|
|
87
|
+
|
|
88
|
+
struct SearchItem {
|
|
89
|
+
let title: String
|
|
90
|
+
let code: String
|
|
91
|
+
let explanation: String
|
|
92
|
+
let groupSize: Int
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// MARK: - Combined Window Controller
|
|
96
|
+
|
|
97
|
+
class CombinedSearchWindowController: NSObject, NSTableViewDataSource, NSTableViewDelegate, NSSearchFieldDelegate {
|
|
98
|
+
private var panel: NSPanel!
|
|
99
|
+
private var tableView: NSTableView!
|
|
100
|
+
private var searchField: NSSearchField!
|
|
101
|
+
private var codeTextView: NSTextView!
|
|
102
|
+
private var allItems: [SearchItem] = []
|
|
103
|
+
private var filteredItems: [SearchItem] = []
|
|
104
|
+
private var selectedIndex: Int = -1
|
|
105
|
+
private var confirmed: Bool = false
|
|
106
|
+
|
|
107
|
+
enum Result {
|
|
108
|
+
case confirmed(Int) // 用户确认插入,返回选中的索引
|
|
109
|
+
case cancelled // 用户取消
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
func show(items: [SearchItem], keyword: String) -> Result {
|
|
113
|
+
self.allItems = items
|
|
114
|
+
self.filteredItems = items
|
|
115
|
+
|
|
116
|
+
let panelWidth: CGFloat = 1400
|
|
117
|
+
let panelHeight: CGFloat = 680
|
|
118
|
+
|
|
119
|
+
// 创建面板
|
|
120
|
+
panel = NSPanel(
|
|
121
|
+
contentRect: NSRect(x: 0, y: 0, width: panelWidth, height: panelHeight),
|
|
122
|
+
styleMask: [.titled, .closable, .resizable, .fullSizeContentView],
|
|
123
|
+
backing: .buffered,
|
|
124
|
+
defer: false
|
|
125
|
+
)
|
|
126
|
+
panel.title = "AutoSnippet 搜索结果"
|
|
127
|
+
panel.level = .floating
|
|
128
|
+
panel.center()
|
|
129
|
+
panel.minSize = NSSize(width: 1000, height: 500)
|
|
130
|
+
panel.titlebarAppearsTransparent = true
|
|
131
|
+
panel.isMovableByWindowBackground = true
|
|
132
|
+
|
|
133
|
+
// 模糊背景
|
|
134
|
+
let visualEffect = NSVisualEffectView(frame: NSRect(x: 0, y: 0, width: panelWidth, height: panelHeight))
|
|
135
|
+
visualEffect.material = .hudWindow
|
|
136
|
+
visualEffect.blendingMode = .behindWindow
|
|
137
|
+
visualEffect.state = .active
|
|
138
|
+
visualEffect.autoresizingMask = [.width, .height]
|
|
139
|
+
panel.contentView = visualEffect
|
|
140
|
+
|
|
141
|
+
let contentView = NSView(frame: NSRect(x: 0, y: 0, width: panelWidth, height: panelHeight))
|
|
142
|
+
contentView.autoresizingMask = [.width, .height]
|
|
143
|
+
visualEffect.addSubview(contentView)
|
|
144
|
+
|
|
145
|
+
// 顶部标题和搜索框
|
|
146
|
+
let titleLabel = NSTextField(labelWithString: "找到 \(items.count) 个匹配")
|
|
147
|
+
titleLabel.font = NSFont.systemFont(ofSize: 13, weight: .medium)
|
|
148
|
+
titleLabel.textColor = .secondaryLabelColor
|
|
149
|
+
titleLabel.frame = NSRect(x: 20, y: panelHeight - 50, width: 200, height: 20)
|
|
150
|
+
titleLabel.autoresizingMask = [.minYMargin]
|
|
151
|
+
contentView.addSubview(titleLabel)
|
|
152
|
+
|
|
153
|
+
// 搜索框
|
|
154
|
+
searchField = NSSearchField(frame: NSRect(x: 20, y: panelHeight - 85, width: 350, height: 28))
|
|
155
|
+
searchField.placeholderString = "过滤结果..."
|
|
156
|
+
searchField.stringValue = keyword
|
|
157
|
+
searchField.autoresizingMask = [.width, .minYMargin]
|
|
158
|
+
searchField.target = self
|
|
159
|
+
searchField.action = #selector(searchFieldDidChange)
|
|
160
|
+
searchField.font = NSFont.systemFont(ofSize: 13)
|
|
161
|
+
contentView.addSubview(searchField)
|
|
162
|
+
|
|
163
|
+
// 分隔线
|
|
164
|
+
let separator = NSBox(frame: NSRect(x: 0, y: panelHeight - 95, width: panelWidth, height: 1))
|
|
165
|
+
separator.boxType = .separator
|
|
166
|
+
separator.autoresizingMask = [.width, .minYMargin]
|
|
167
|
+
contentView.addSubview(separator)
|
|
168
|
+
|
|
169
|
+
// 左侧:列表区域
|
|
170
|
+
let listWidth: CGFloat = 360
|
|
171
|
+
let listScrollView = NSScrollView(frame: NSRect(x: 20, y: 70, width: listWidth, height: panelHeight - 175))
|
|
172
|
+
listScrollView.hasVerticalScroller = true
|
|
173
|
+
listScrollView.borderType = .noBorder
|
|
174
|
+
listScrollView.autoresizingMask = [.height, .minYMargin]
|
|
175
|
+
listScrollView.wantsLayer = true
|
|
176
|
+
listScrollView.layer?.cornerRadius = 8
|
|
177
|
+
listScrollView.layer?.masksToBounds = true
|
|
178
|
+
listScrollView.backgroundColor = NSColor(calibratedRed: 0.12, green: 0.12, blue: 0.14, alpha: 1.0)
|
|
179
|
+
// 使用现代 overlay 滚动条样式(悬浮自动隐藏)
|
|
180
|
+
listScrollView.scrollerStyle = .overlay
|
|
181
|
+
listScrollView.autohidesScrollers = true
|
|
182
|
+
listScrollView.scrollerKnobStyle = .light
|
|
183
|
+
// 强制刷新滚动条样式
|
|
184
|
+
listScrollView.flashScrollers()
|
|
185
|
+
|
|
186
|
+
tableView = NSTableView()
|
|
187
|
+
tableView.headerView = nil
|
|
188
|
+
tableView.rowHeight = 60
|
|
189
|
+
tableView.intercellSpacing = NSSize(width: 0, height: 2)
|
|
190
|
+
tableView.backgroundColor = NSColor(calibratedRed: 0.12, green: 0.12, blue: 0.14, alpha: 1.0)
|
|
191
|
+
tableView.usesAlternatingRowBackgroundColors = false
|
|
192
|
+
tableView.selectionHighlightStyle = .regular
|
|
193
|
+
tableView.target = self
|
|
194
|
+
tableView.action = #selector(tableViewClicked)
|
|
195
|
+
tableView.doubleAction = #selector(tableViewDoubleClicked)
|
|
196
|
+
|
|
197
|
+
let column = NSTableColumn(identifier: NSUserInterfaceItemIdentifier("item"))
|
|
198
|
+
column.width = listWidth - 25
|
|
199
|
+
tableView.addTableColumn(column)
|
|
200
|
+
|
|
201
|
+
tableView.dataSource = self
|
|
202
|
+
tableView.delegate = self
|
|
203
|
+
|
|
204
|
+
listScrollView.documentView = tableView
|
|
205
|
+
contentView.addSubview(listScrollView)
|
|
206
|
+
|
|
207
|
+
// 右侧:代码预览区域
|
|
208
|
+
let codeX = 20 + listWidth + 15
|
|
209
|
+
let codeWidth = panelWidth - codeX - 20
|
|
210
|
+
|
|
211
|
+
let previewLabel = NSTextField(labelWithString: "代码预览")
|
|
212
|
+
previewLabel.font = NSFont.systemFont(ofSize: 11, weight: .semibold)
|
|
213
|
+
previewLabel.textColor = .secondaryLabelColor
|
|
214
|
+
previewLabel.frame = NSRect(x: codeX, y: panelHeight - 115, width: codeWidth, height: 16)
|
|
215
|
+
previewLabel.autoresizingMask = [.width, .minYMargin]
|
|
216
|
+
contentView.addSubview(previewLabel)
|
|
217
|
+
|
|
218
|
+
let codeScrollView = NSScrollView(frame: NSRect(x: codeX, y: 70, width: codeWidth, height: panelHeight - 195))
|
|
219
|
+
codeScrollView.hasVerticalScroller = true
|
|
220
|
+
codeScrollView.hasHorizontalScroller = true
|
|
221
|
+
codeScrollView.borderType = .noBorder
|
|
222
|
+
codeScrollView.autoresizingMask = [.width, .height]
|
|
223
|
+
codeScrollView.wantsLayer = true
|
|
224
|
+
codeScrollView.layer?.cornerRadius = 8
|
|
225
|
+
codeScrollView.layer?.masksToBounds = true
|
|
226
|
+
codeScrollView.backgroundColor = NSColor(calibratedRed: 0.12, green: 0.12, blue: 0.14, alpha: 1.0)
|
|
227
|
+
// 使用现代 overlay 滚动条样式(悬浮自动隐藏)
|
|
228
|
+
codeScrollView.scrollerStyle = .overlay
|
|
229
|
+
codeScrollView.autohidesScrollers = true
|
|
230
|
+
codeScrollView.scrollerKnobStyle = .light
|
|
231
|
+
// 启用平滑滚动
|
|
232
|
+
codeScrollView.usesPredominantAxisScrolling = false
|
|
233
|
+
codeScrollView.horizontalScrollElasticity = .automatic
|
|
234
|
+
codeScrollView.verticalScrollElasticity = .automatic
|
|
235
|
+
// 强制刷新滚动条样式
|
|
236
|
+
codeScrollView.flashScrollers()
|
|
237
|
+
|
|
238
|
+
codeTextView = NSTextView(frame: codeScrollView.bounds)
|
|
239
|
+
codeTextView.isEditable = false
|
|
240
|
+
codeTextView.isSelectable = true
|
|
241
|
+
codeTextView.backgroundColor = NSColor(calibratedRed: 0.12, green: 0.12, blue: 0.14, alpha: 1.0)
|
|
242
|
+
codeTextView.textContainerInset = NSSize(width: 16, height: 16)
|
|
243
|
+
codeTextView.isHorizontallyResizable = true
|
|
244
|
+
codeTextView.isVerticallyResizable = true
|
|
245
|
+
codeTextView.textContainer?.widthTracksTextView = false
|
|
246
|
+
codeTextView.textContainer?.heightTracksTextView = false
|
|
247
|
+
codeTextView.textContainer?.containerSize = NSSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)
|
|
248
|
+
codeTextView.autoresizingMask = []
|
|
249
|
+
codeTextView.textContainer?.lineFragmentPadding = 10
|
|
250
|
+
// 设置默认字体大小
|
|
251
|
+
codeTextView.font = NSFont(name: "SFMono-Regular", size: 14) ?? NSFont.monospacedSystemFont(ofSize: 14, weight: .regular)
|
|
252
|
+
|
|
253
|
+
if let textContainer = codeTextView.textContainer {
|
|
254
|
+
textContainer.size = NSSize(width: codeScrollView.bounds.width - 40, height: CGFloat.greatestFiniteMagnitude)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
codeScrollView.documentView = codeTextView
|
|
258
|
+
contentView.addSubview(codeScrollView)
|
|
259
|
+
|
|
260
|
+
// 底部按钮
|
|
261
|
+
let buttonY: CGFloat = 20
|
|
262
|
+
|
|
263
|
+
let cancelButton = NSButton(title: "取消", target: self, action: #selector(cancelClicked))
|
|
264
|
+
cancelButton.bezelStyle = .rounded
|
|
265
|
+
cancelButton.frame = NSRect(x: panelWidth - 310, y: buttonY, width: 90, height: 32)
|
|
266
|
+
cancelButton.keyEquivalent = "\u{1b}"
|
|
267
|
+
cancelButton.autoresizingMask = [.minXMargin, .maxYMargin]
|
|
268
|
+
cancelButton.wantsLayer = true
|
|
269
|
+
cancelButton.layer?.cornerRadius = 6
|
|
270
|
+
contentView.addSubview(cancelButton)
|
|
271
|
+
|
|
272
|
+
let copyButton = NSButton(title: "复制代码", target: self, action: #selector(copyClicked))
|
|
273
|
+
copyButton.bezelStyle = .rounded
|
|
274
|
+
copyButton.frame = NSRect(x: panelWidth - 210, y: buttonY, width: 90, height: 32)
|
|
275
|
+
copyButton.autoresizingMask = [.minXMargin, .maxYMargin]
|
|
276
|
+
copyButton.wantsLayer = true
|
|
277
|
+
copyButton.layer?.cornerRadius = 6
|
|
278
|
+
contentView.addSubview(copyButton)
|
|
279
|
+
|
|
280
|
+
let okButton = NSButton(title: "立即插入", target: self, action: #selector(okClicked))
|
|
281
|
+
okButton.bezelStyle = .rounded
|
|
282
|
+
okButton.frame = NSRect(x: panelWidth - 110, y: buttonY, width: 90, height: 32)
|
|
283
|
+
okButton.keyEquivalent = "\r"
|
|
284
|
+
okButton.autoresizingMask = [.minXMargin, .maxYMargin]
|
|
285
|
+
okButton.wantsLayer = true
|
|
286
|
+
okButton.layer?.cornerRadius = 6
|
|
287
|
+
contentView.addSubview(okButton)
|
|
288
|
+
|
|
289
|
+
// 初始化显示
|
|
290
|
+
tableView.reloadData()
|
|
291
|
+
if !filteredItems.isEmpty {
|
|
292
|
+
tableView.selectRowIndexes(IndexSet(integer: 0), byExtendingSelection: false)
|
|
293
|
+
updatePreview(index: 0)
|
|
294
|
+
} else {
|
|
295
|
+
showEmptyPreview()
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// 显示窗口
|
|
299
|
+
panel.alphaValue = 0
|
|
300
|
+
NSApp.activate(ignoringOtherApps: true)
|
|
301
|
+
panel.makeKeyAndOrderFront(nil)
|
|
302
|
+
panel.makeFirstResponder(searchField)
|
|
303
|
+
|
|
304
|
+
NSAnimationContext.runAnimationGroup({ context in
|
|
305
|
+
context.duration = 0.2
|
|
306
|
+
panel.animator().alphaValue = 1.0
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
NSApp.runModal(for: panel)
|
|
310
|
+
|
|
311
|
+
// 返回结果
|
|
312
|
+
if confirmed && selectedIndex >= 0 {
|
|
313
|
+
// 找到在原始数组中的索引
|
|
314
|
+
if let selected = filteredItems[safe: selectedIndex] {
|
|
315
|
+
if let originalIndex = allItems.firstIndex(where: { $0.title == selected.title }) {
|
|
316
|
+
return .confirmed(originalIndex)
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return .confirmed(selectedIndex)
|
|
320
|
+
}
|
|
321
|
+
return .cancelled
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// MARK: - Table View Data Source
|
|
325
|
+
|
|
326
|
+
func numberOfRows(in tableView: NSTableView) -> Int {
|
|
327
|
+
return filteredItems.count
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
|
|
331
|
+
let item = filteredItems[row]
|
|
332
|
+
|
|
333
|
+
let cellView = NSTableCellView()
|
|
334
|
+
cellView.wantsLayer = true
|
|
335
|
+
|
|
336
|
+
// 标题 - 支持两行显示
|
|
337
|
+
let textField = NSTextField(labelWithString: item.title)
|
|
338
|
+
textField.font = NSFont.systemFont(ofSize: 14, weight: .medium)
|
|
339
|
+
textField.textColor = .labelColor
|
|
340
|
+
textField.lineBreakMode = .byWordWrapping // 改为自动换行
|
|
341
|
+
textField.maximumNumberOfLines = 2 // 最多显示两行
|
|
342
|
+
textField.frame = NSRect(x: 12, y: 24, width: tableView.bounds.width - 24, height: 36) // 增加高度到36,调整y位置
|
|
343
|
+
cellView.addSubview(textField)
|
|
344
|
+
|
|
345
|
+
// 说明
|
|
346
|
+
var subtitleParts: [String] = []
|
|
347
|
+
if item.groupSize > 1 {
|
|
348
|
+
subtitleParts.append("同类\(item.groupSize)")
|
|
349
|
+
}
|
|
350
|
+
if !item.explanation.isEmpty {
|
|
351
|
+
subtitleParts.append(item.explanation)
|
|
352
|
+
}
|
|
353
|
+
let subtitle = subtitleParts.joined(separator: " · ")
|
|
354
|
+
if !subtitle.isEmpty {
|
|
355
|
+
let subtitleField = NSTextField(labelWithString: subtitle)
|
|
356
|
+
subtitleField.font = NSFont.systemFont(ofSize: 11, weight: .regular)
|
|
357
|
+
subtitleField.textColor = .secondaryLabelColor
|
|
358
|
+
subtitleField.lineBreakMode = .byTruncatingTail
|
|
359
|
+
subtitleField.frame = NSRect(x: 12, y: 8, width: tableView.bounds.width - 24, height: 16) // 调整y位置为8
|
|
360
|
+
cellView.addSubview(subtitleField)
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return cellView
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
func tableViewSelectionDidChange(_ notification: Notification) {
|
|
367
|
+
let row = tableView.selectedRow
|
|
368
|
+
if row >= 0 {
|
|
369
|
+
selectedIndex = row
|
|
370
|
+
updatePreview(index: row)
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// MARK: - Actions
|
|
375
|
+
|
|
376
|
+
@objc func tableViewClicked() {
|
|
377
|
+
let row = tableView.clickedRow
|
|
378
|
+
if row >= 0 {
|
|
379
|
+
selectedIndex = row
|
|
380
|
+
updatePreview(index: row)
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
@objc func tableViewDoubleClicked() {
|
|
385
|
+
let row = tableView.clickedRow
|
|
386
|
+
if row >= 0 {
|
|
387
|
+
selectedIndex = row
|
|
388
|
+
confirmed = true
|
|
389
|
+
panel.close()
|
|
390
|
+
NSApp.stopModal()
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
@objc func searchFieldDidChange() {
|
|
395
|
+
let query = searchField.stringValue.lowercased().trimmingCharacters(in: .whitespaces)
|
|
396
|
+
|
|
397
|
+
if query.isEmpty {
|
|
398
|
+
filteredItems = allItems
|
|
399
|
+
} else {
|
|
400
|
+
filteredItems = allItems.filter { $0.title.lowercased().contains(query) }
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
tableView.reloadData()
|
|
404
|
+
|
|
405
|
+
if !filteredItems.isEmpty {
|
|
406
|
+
tableView.selectRowIndexes(IndexSet(integer: 0), byExtendingSelection: false)
|
|
407
|
+
selectedIndex = 0
|
|
408
|
+
updatePreview(index: 0)
|
|
409
|
+
} else {
|
|
410
|
+
showEmptyPreview()
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
@objc func cancelClicked() {
|
|
415
|
+
confirmed = false
|
|
416
|
+
panel.close()
|
|
417
|
+
NSApp.stopModal()
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
@objc func copyClicked() {
|
|
421
|
+
if selectedIndex >= 0, let item = filteredItems[safe: selectedIndex] {
|
|
422
|
+
let pureCode = extractPureCode(item.code)
|
|
423
|
+
let pasteboard = NSPasteboard.general
|
|
424
|
+
pasteboard.clearContents()
|
|
425
|
+
pasteboard.setString(pureCode, forType: .string)
|
|
426
|
+
print("✅ 代码已复制到剪贴板")
|
|
427
|
+
// 复制后也关闭窗口
|
|
428
|
+
confirmed = true
|
|
429
|
+
panel.close()
|
|
430
|
+
NSApp.stopModal()
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
@objc func okClicked() {
|
|
435
|
+
if selectedIndex >= 0 {
|
|
436
|
+
confirmed = true
|
|
437
|
+
// 复制代码到剪贴板
|
|
438
|
+
if let item = filteredItems[safe: selectedIndex] {
|
|
439
|
+
let pureCode = extractPureCode(item.code)
|
|
440
|
+
let pasteboard = NSPasteboard.general
|
|
441
|
+
pasteboard.clearContents()
|
|
442
|
+
pasteboard.setString(pureCode, forType: .string)
|
|
443
|
+
}
|
|
444
|
+
panel.close()
|
|
445
|
+
NSApp.stopModal()
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// MARK: - Helper Methods
|
|
450
|
+
|
|
451
|
+
private func updatePreview(index: Int) {
|
|
452
|
+
guard let item = filteredItems[safe: index] else {
|
|
453
|
+
showEmptyPreview()
|
|
454
|
+
return
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
let processedCode = item.code.replacingOccurrences(of: "\\n", with: "\n")
|
|
458
|
+
let highlightedCode = SyntaxHighlighter.highlight(processedCode)
|
|
459
|
+
codeTextView.textStorage?.setAttributedString(highlightedCode)
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
private func showEmptyPreview() {
|
|
463
|
+
let emptyText = NSAttributedString(
|
|
464
|
+
string: "无内容",
|
|
465
|
+
attributes: [
|
|
466
|
+
.font: NSFont.systemFont(ofSize: 14),
|
|
467
|
+
.foregroundColor: NSColor.tertiaryLabelColor
|
|
468
|
+
]
|
|
469
|
+
)
|
|
470
|
+
codeTextView.textStorage?.setAttributedString(emptyText)
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
private func extractPureCode(_ code: String) -> String {
|
|
474
|
+
var lines = code.split(separator: "\n", omittingEmptySubsequences: false).map(String.init)
|
|
475
|
+
|
|
476
|
+
// 移除首尾的代码块标记(```swift、```等)
|
|
477
|
+
while !lines.isEmpty && (lines.first?.trimmingCharacters(in: .whitespaces).starts(with: "```") ?? false) {
|
|
478
|
+
lines.removeFirst()
|
|
479
|
+
}
|
|
480
|
+
while !lines.isEmpty && (lines.last?.trimmingCharacters(in: .whitespaces).starts(with: "```") ?? false) {
|
|
481
|
+
lines.removeLast()
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return lines.joined(separator: "\n")
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// MARK: - Array Safe Subscript
|
|
489
|
+
|
|
490
|
+
extension Array {
|
|
491
|
+
subscript(safe index: Int) -> Element? {
|
|
492
|
+
return indices.contains(index) ? self[index] : nil
|
|
493
|
+
}
|
|
494
|
+
}
|