siada-cli 0.0.1__py3-none-any.whl

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.
Files changed (215) hide show
  1. agent_config.yaml +34 -0
  2. siada/__init__.py +13 -0
  3. siada/agent_hub/__init__.py +10 -0
  4. siada/agent_hub/coder/bug_fix_agent.py +398 -0
  5. siada/agent_hub/coder/code_gen_agent.py +235 -0
  6. siada/agent_hub/coder/fe_gen_agent.py +37 -0
  7. siada/agent_hub/coder/issue_review_agent.py +68 -0
  8. siada/agent_hub/coder/prompt/__init__.py +0 -0
  9. siada/agent_hub/coder/prompt/base/__init__.py +0 -0
  10. siada/agent_hub/coder/prompt/base/capabilities.py +18 -0
  11. siada/agent_hub/coder/prompt/base/non_interactive.py +14 -0
  12. siada/agent_hub/coder/prompt/base/prompt_builder.py +33 -0
  13. siada/agent_hub/coder/prompt/base/rules.py +40 -0
  14. siada/agent_hub/coder/prompt/base/tool_use.py +14 -0
  15. siada/agent_hub/coder/prompt/bug_prompt/bug_fix_prompt.py +51 -0
  16. siada/agent_hub/coder/prompt/bug_prompt/rules.py +53 -0
  17. siada/agent_hub/coder/prompt/code_gen_prompt.py +45 -0
  18. siada/agent_hub/coder/prompt/fe_gen_prompt.py +45 -0
  19. siada/agent_hub/coder/prompt/issue_review_prompt.py +121 -0
  20. siada/agent_hub/coder/tracing/__init__.py +22 -0
  21. siada/agent_hub/coder/tracing/logger_tracing_processor.py +458 -0
  22. siada/agent_hub/hooks/__init__.py +0 -0
  23. siada/agent_hub/hooks/io_hooks.py +30 -0
  24. siada/agent_hub/siada_agent.py +214 -0
  25. siada/config/__init__.py +0 -0
  26. siada/config/config_loader.py +67 -0
  27. siada/entrypoint/__init__.py +3 -0
  28. siada/entrypoint/args_parser/__init__.py +0 -0
  29. siada/entrypoint/args_parser/args.py +195 -0
  30. siada/entrypoint/interaction/__init__.py +0 -0
  31. siada/entrypoint/interaction/base_turn.py +107 -0
  32. siada/entrypoint/interaction/config.py +30 -0
  33. siada/entrypoint/interaction/controller.py +122 -0
  34. siada/entrypoint/interaction/nointeractive_controller.py +27 -0
  35. siada/entrypoint/interaction/turn/interface.py +116 -0
  36. siada/entrypoint/interaction/turn/models.py +34 -0
  37. siada/entrypoint/interaction/turn/run_turn.py +641 -0
  38. siada/entrypoint/interaction/turn_factory.py +35 -0
  39. siada/entrypoint/interaction/turn_models.py +28 -0
  40. siada/entrypoint/siadahub.py +413 -0
  41. siada/foundation/__init__.py +0 -0
  42. siada/foundation/code_agent_context.py +48 -0
  43. siada/foundation/config.py +59 -0
  44. siada/foundation/constants.py +2 -0
  45. siada/foundation/context.py +76 -0
  46. siada/foundation/logging.py +341 -0
  47. siada/foundation/tools/__init__.py +0 -0
  48. siada/foundation/tools/get_git_diff.py +105 -0
  49. siada/io/__init__.py +0 -0
  50. siada/io/banner.py +148 -0
  51. siada/io/color_settings.py +207 -0
  52. siada/io/color_utils.py +16 -0
  53. siada/io/components/__init__.py +0 -0
  54. siada/io/components/mdstream.py +204 -0
  55. siada/io/console_printer.py +68 -0
  56. siada/io/io.py +661 -0
  57. siada/io/key_bindings.py +129 -0
  58. siada/io/notification_command.py +40 -0
  59. siada/io/test_banner.py +64 -0
  60. siada/models/__init__.py +0 -0
  61. siada/models/agent.py +47 -0
  62. siada/models/coder/__init__.py +0 -0
  63. siada/models/converter.py +27 -0
  64. siada/models/model_base_config.py +121 -0
  65. siada/models/model_run_config.py +153 -0
  66. siada/provider/__init__.py +0 -0
  67. siada/provider/client_factory.py +112 -0
  68. siada/provider/llm_client.py +11 -0
  69. siada/provider/openrouter/__init__.py +0 -0
  70. siada/provider/openrouter/coverter.py +17 -0
  71. siada/provider/openrouter/openroute_provider.py +33 -0
  72. siada/provider/provider_factory.py +53 -0
  73. siada/queries/tree-sitter-language-pack/README.md +7 -0
  74. siada/queries/tree-sitter-language-pack/arduino-tags.scm +5 -0
  75. siada/queries/tree-sitter-language-pack/c-tags.scm +9 -0
  76. siada/queries/tree-sitter-language-pack/chatito-tags.scm +16 -0
  77. siada/queries/tree-sitter-language-pack/commonlisp-tags.scm +122 -0
  78. siada/queries/tree-sitter-language-pack/cpp-tags.scm +15 -0
  79. siada/queries/tree-sitter-language-pack/csharp-tags.scm +26 -0
  80. siada/queries/tree-sitter-language-pack/d-tags.scm +26 -0
  81. siada/queries/tree-sitter-language-pack/dart-tags.scm +92 -0
  82. siada/queries/tree-sitter-language-pack/elisp-tags.scm +5 -0
  83. siada/queries/tree-sitter-language-pack/elixir-tags.scm +54 -0
  84. siada/queries/tree-sitter-language-pack/elm-tags.scm +19 -0
  85. siada/queries/tree-sitter-language-pack/gleam-tags.scm +41 -0
  86. siada/queries/tree-sitter-language-pack/go-tags.scm +42 -0
  87. siada/queries/tree-sitter-language-pack/java-tags.scm +20 -0
  88. siada/queries/tree-sitter-language-pack/javascript-tags.scm +88 -0
  89. siada/queries/tree-sitter-language-pack/lua-tags.scm +34 -0
  90. siada/queries/tree-sitter-language-pack/ocaml-tags.scm +115 -0
  91. siada/queries/tree-sitter-language-pack/ocaml_interface-tags.scm +98 -0
  92. siada/queries/tree-sitter-language-pack/pony-tags.scm +39 -0
  93. siada/queries/tree-sitter-language-pack/properties-tags.scm +5 -0
  94. siada/queries/tree-sitter-language-pack/python-tags.scm +14 -0
  95. siada/queries/tree-sitter-language-pack/r-tags.scm +21 -0
  96. siada/queries/tree-sitter-language-pack/racket-tags.scm +12 -0
  97. siada/queries/tree-sitter-language-pack/ruby-tags.scm +64 -0
  98. siada/queries/tree-sitter-language-pack/rust-tags.scm +60 -0
  99. siada/queries/tree-sitter-language-pack/solidity-tags.scm +43 -0
  100. siada/queries/tree-sitter-language-pack/swift-tags.scm +51 -0
  101. siada/queries/tree-sitter-language-pack/udev-tags.scm +20 -0
  102. siada/queries/tree-sitter-languages/README.md +23 -0
  103. siada/queries/tree-sitter-languages/c-tags.scm +9 -0
  104. siada/queries/tree-sitter-languages/c_sharp-tags.scm +46 -0
  105. siada/queries/tree-sitter-languages/cpp-tags.scm +15 -0
  106. siada/queries/tree-sitter-languages/dart-tags.scm +91 -0
  107. siada/queries/tree-sitter-languages/elisp-tags.scm +8 -0
  108. siada/queries/tree-sitter-languages/elixir-tags.scm +54 -0
  109. siada/queries/tree-sitter-languages/elm-tags.scm +19 -0
  110. siada/queries/tree-sitter-languages/go-tags.scm +30 -0
  111. siada/queries/tree-sitter-languages/hcl-tags.scm +77 -0
  112. siada/queries/tree-sitter-languages/java-tags.scm +20 -0
  113. siada/queries/tree-sitter-languages/javascript-tags.scm +88 -0
  114. siada/queries/tree-sitter-languages/kotlin-tags.scm +27 -0
  115. siada/queries/tree-sitter-languages/ocaml-tags.scm +115 -0
  116. siada/queries/tree-sitter-languages/ocaml_interface-tags.scm +98 -0
  117. siada/queries/tree-sitter-languages/php-tags.scm +26 -0
  118. siada/queries/tree-sitter-languages/python-tags.scm +12 -0
  119. siada/queries/tree-sitter-languages/ql-tags.scm +26 -0
  120. siada/queries/tree-sitter-languages/ruby-tags.scm +64 -0
  121. siada/queries/tree-sitter-languages/rust-tags.scm +60 -0
  122. siada/queries/tree-sitter-languages/scala-tags.scm +65 -0
  123. siada/queries/tree-sitter-languages/typescript-tags.scm +41 -0
  124. siada/services/__init__.py +30 -0
  125. siada/services/at_file_recommendation_design_document.md +1213 -0
  126. siada/services/code_context_manager.py +136 -0
  127. siada/services/enhanced_fix_result_check.py +546 -0
  128. siada/services/execution_trace_collector.py +207 -0
  129. siada/services/file_recommendation/__init__.py +136 -0
  130. siada/services/file_recommendation/core/__init__.py +17 -0
  131. siada/services/file_recommendation/core/completion_engine.py +351 -0
  132. siada/services/file_recommendation/core/config.py +62 -0
  133. siada/services/file_recommendation/core/file_discovery.py +353 -0
  134. siada/services/file_recommendation/core/suggestion.py +110 -0
  135. siada/services/file_recommendation/utils/__init__.py +22 -0
  136. siada/services/file_recommendation/utils/path_utils.py +138 -0
  137. siada/services/file_recommendation/utils/text_utils.py +253 -0
  138. siada/services/fix_result_check.py +377 -0
  139. siada/services/handle_at_command/README.md +247 -0
  140. siada/services/handle_at_command/__init__.py +115 -0
  141. siada/services/handle_at_command/exceptions.py +46 -0
  142. siada/services/handle_at_command/models.py +92 -0
  143. siada/services/handle_at_command/parser.py +189 -0
  144. siada/services/handle_at_command/processor.py +321 -0
  145. siada/services/handle_at_command/resolver.py +318 -0
  146. siada/services/handle_at_command/utils.py +345 -0
  147. siada/services/model_info_service.py +103 -0
  148. siada/services/siada_runner.py +170 -0
  149. siada/session/__init__.py +29 -0
  150. siada/session/session_manager.py +66 -0
  151. siada/session/session_models.py +43 -0
  152. siada/support/__init__.py +0 -0
  153. siada/support/completer.py +204 -0
  154. siada/support/editor.py +146 -0
  155. siada/support/envprocessor.py +54 -0
  156. siada/support/repo.py +51 -0
  157. siada/support/slash_commands.py +410 -0
  158. siada/support/spinner.py +320 -0
  159. siada/tools/__init__.py +0 -0
  160. siada/tools/ast/README.md +345 -0
  161. siada/tools/ast/ast_tool.py +341 -0
  162. siada/tools/ast/models.py +11 -0
  163. siada/tools/coder/__init__.py +0 -0
  164. siada/tools/coder/ask_followup_question.py +70 -0
  165. siada/tools/coder/cmd_runner.py +132 -0
  166. siada/tools/coder/file_operator.py +284 -0
  167. siada/tools/coder/file_search/README.md +223 -0
  168. siada/tools/coder/file_search/__init__.py +10 -0
  169. siada/tools/coder/file_search/bin/rg-linux-arm64 +0 -0
  170. siada/tools/coder/file_search/bin/rg-linux-x64 +0 -0
  171. siada/tools/coder/file_search/bin/rg-macos-arm64 +0 -0
  172. siada/tools/coder/file_search/bin/rg-macos-x64 +0 -0
  173. siada/tools/coder/file_search/bin/rg.exe +0 -0
  174. siada/tools/coder/file_search/search.py +437 -0
  175. siada/tools/coder/file_search/setup_package.py +150 -0
  176. siada/tools/coder/file_search/test_search.py +142 -0
  177. siada/tools/coder/files.py +147 -0
  178. siada/tools/coder/fix_attempt_completion.py +58 -0
  179. siada/tools/coder/issue_review_completion.py +29 -0
  180. siada/tools/coder/observation/__init__.py +0 -0
  181. siada/tools/coder/observation/error.py +22 -0
  182. siada/tools/coder/observation/file_observation.py +216 -0
  183. siada/tools/coder/observation/observation.py +71 -0
  184. siada/tools/coder/repo_map/__init__.py +157 -0
  185. siada/tools/coder/repo_map/dump.py +29 -0
  186. siada/tools/coder/repo_map/io.py +226 -0
  187. siada/tools/coder/repo_map/repo_map.py +881 -0
  188. siada/tools/coder/repo_map/special.py +203 -0
  189. siada/tools/coder/repo_map/token_counter.py +154 -0
  190. siada/tools/coder/repo_map/waiting.py +221 -0
  191. siada/tools/coder/reproduce_completion.py +39 -0
  192. siada/tools/coder/run_cmd.py +60 -0
  193. siada/tools/coder/test_completion.py +25 -0
  194. siada/tools/coder/tool_docs.py +44 -0
  195. siada/tools/compression_tool.py +200 -0
  196. siada/tools/read_many_files/README.md +220 -0
  197. siada/tools/read_many_files/__init__.py +25 -0
  198. siada/tools/read_many_files/file_processor.py +486 -0
  199. siada/tools/read_many_files/filters.py +326 -0
  200. siada/tools/read_many_files/formatters.py +204 -0
  201. siada/tools/read_many_files/models.py +189 -0
  202. siada/tools/read_many_files_tool.py +233 -0
  203. siada/tools/tool_call_format/__init__.py +55 -0
  204. siada/tools/tool_call_format/file_to_language.py +231 -0
  205. siada/tools/tool_call_format/formatter_factory.py +80 -0
  206. siada/tools/tool_call_format/formatters.py +282 -0
  207. siada/tools/tool_call_format/tool_call_formatter.py +54 -0
  208. siada/tools/web/__init__.py +0 -0
  209. siada/tools/web/web_crawl.py +208 -0
  210. siada/utils.py +194 -0
  211. siada_cli-0.0.1.dist-info/LICENSE +201 -0
  212. siada_cli-0.0.1.dist-info/METADATA +457 -0
  213. siada_cli-0.0.1.dist-info/RECORD +215 -0
  214. siada_cli-0.0.1.dist-info/WHEEL +4 -0
  215. siada_cli-0.0.1.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,1213 @@
1
+ # @ 文件推荐功能设计文档
2
+
3
+ ## 文档概述
4
+
5
+ 本文档详细描述了 Gemini CLI 中 @ 文件推荐功能的完整技术规范,包括核心算法、系统架构、用户交互模式和实现细节。该文档旨在为 Python 版本的实现提供完整的技术指导。
6
+
7
+ **版本**: 1.0
8
+ **创建日期**: 2025-01-06
9
+ **目标**: 为 Python 实现提供完整的技术规范
10
+
11
+ ---
12
+
13
+ ## 1. 功能概述
14
+
15
+ ### 1.1 核心功能
16
+ @ 文件推荐功能是一个实时文件路径自动完成系统,当用户在输入框中键入 `@` 字符后,系统会:
17
+ - 实时搜索和推荐可用的文件和目录
18
+ - 支持递归文件搜索和模糊匹配
19
+ - 提供智能过滤(.gitignore、.geminiignore)
20
+ - 支持键盘导航和自动完成
21
+
22
+ ### 1.2 用户场景
23
+ ```
24
+ 用户输入: "@" → 显示当前目录下的所有文件
25
+ 用户输入: "@src" → 显示以 "src" 开头的文件和目录
26
+ 用户输入: "@src/" → 显示 src 目录下的文件
27
+ 用户输入: "@config.j" → 显示匹配 "config.j" 的文件(如 config.json)
28
+ ```
29
+
30
+ ---
31
+
32
+ ## 2. 系统架构
33
+
34
+ ### 2.1 核心组件架构
35
+
36
+ ```
37
+ ┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
38
+ │ InputHandler │───▶│ CompletionEngine │───▶│ SuggestionsList │
39
+ └─────────────────┘ └──────────────────┘ └─────────────────┘
40
+ │ │ │
41
+ ▼ ▼ ▼
42
+ ┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
43
+ │ TextBuffer │ │ FileDiscoveryService │ │ UI Display │
44
+ └─────────────────┘ └──────────────────┘ └─────────────────┘
45
+ │ │
46
+ ▼ ▼
47
+ ┌─────────────────┐ ┌──────────────────┐
48
+ │ CursorTracker │ │ FilterService │
49
+ └─────────────────┘ └──────────────────┘
50
+ ```
51
+
52
+ ### 2.2 数据流图
53
+
54
+ ```
55
+ 用户输入 "@src"
56
+ ↓
57
+ [触发检测] - 检查光标位置和 @ 字符
58
+ ↓
59
+ [路径解析] - 解析部分路径 "src"
60
+ ↓
61
+ [文件搜索] - 搜索匹配的文件/目录
62
+ ↓
63
+ [过滤处理] - 应用 gitignore/geminiignore 规则
64
+ ↓
65
+ [排序优化] - 按深度、类型、名称排序
66
+ ↓
67
+ [UI 显示] - 显示推荐列表
68
+ ↓
69
+ [用户选择] - Tab/Enter 选择,上下箭头导航
70
+ ```
71
+
72
+ ---
73
+
74
+ ## 3. 核心算法
75
+
76
+ ### 3.1 触发检测算法
77
+
78
+ **激活条件判断逻辑**:
79
+
80
+ ```python
81
+ def is_completion_active(text: str, cursor_row: int, cursor_col: int, lines: List[str]) -> bool:
82
+ """
83
+ 检查是否应该激活 @ 文件推荐功能
84
+
85
+ Args:
86
+ text: 完整输入文本
87
+ cursor_row: 光标行号
88
+ cursor_col: 光标列号
89
+ lines: 文本行列表
90
+
91
+ Returns:
92
+ bool: 是否激活推荐功能
93
+ """
94
+ # 如果是斜杠命令,优先处理
95
+ if text.strip().startswith('/'):
96
+ return True
97
+
98
+ # 获取当前行
99
+ current_line = lines[cursor_row] if cursor_row < len(lines) else ""
100
+ code_points = list(current_line) # Unicode 代码点数组
101
+
102
+ # 从光标位置向前搜索
103
+ for i in range(cursor_col - 1, -1, -1):
104
+ char = code_points[i]
105
+
106
+ if char == ' ':
107
+ # 检查是否为转义空格
108
+ backslash_count = 0
109
+ j = i - 1
110
+ while j >= 0 and code_points[j] == '\\':
111
+ backslash_count += 1
112
+ j -= 1
113
+
114
+ # 偶数个反斜杠表示未转义的空格
115
+ if backslash_count % 2 == 0:
116
+ return False
117
+
118
+ elif char == '@':
119
+ # 找到 @ 字符,激活推荐
120
+ return True
121
+
122
+ return False
123
+ ```
124
+
125
+ ### 3.2 路径解析算法
126
+
127
+ ```python
128
+ def parse_at_command_path(text: str) -> Tuple[str, str, str]:
129
+ """
130
+ 解析 @ 命令中的路径信息
131
+
132
+ Args:
133
+ text: 输入文本
134
+
135
+ Returns:
136
+ Tuple[基础目录, 路径前缀, 原始部分路径]
137
+ """
138
+ at_index = text.rfind('@')
139
+ if at_index == -1:
140
+ return ".", "", ""
141
+
142
+ partial_path = text[at_index + 1:]
143
+ last_slash_index = partial_path.rfind('/')
144
+
145
+ if last_slash_index == -1:
146
+ base_dir_relative = "."
147
+ prefix = unescape_path(partial_path)
148
+ else:
149
+ base_dir_relative = partial_path[:last_slash_index + 1]
150
+ prefix = unescape_path(partial_path[last_slash_index + 1:])
151
+
152
+ return base_dir_relative, prefix, partial_path
153
+ ```
154
+
155
+ ### 3.3 文件搜索算法
156
+
157
+ #### 3.3.1 递归搜索算法
158
+
159
+ ```python
160
+ async def find_files_recursively(
161
+ start_dir: str,
162
+ search_prefix: str,
163
+ file_discovery: FileDiscoveryService,
164
+ filter_options: FilterOptions,
165
+ current_relative_path: str = "",
166
+ depth: int = 0,
167
+ max_depth: int = 10,
168
+ max_results: int = 50
169
+ ) -> List[Suggestion]:
170
+ """
171
+ 递归搜索文件和目录
172
+
173
+ Args:
174
+ start_dir: 起始搜索目录
175
+ search_prefix: 搜索前缀
176
+ file_discovery: 文件发现服务
177
+ filter_options: 过滤选项
178
+ current_relative_path: 当前相对路径
179
+ depth: 当前递归深度
180
+ max_depth: 最大递归深度
181
+ max_results: 最大结果数量
182
+
183
+ Returns:
184
+ List[Suggestion]: 建议列表
185
+ """
186
+ if depth > max_depth:
187
+ return []
188
+
189
+ lower_search_prefix = search_prefix.lower()
190
+ found_suggestions = []
191
+
192
+ try:
193
+ entries = await fs.readdir(start_dir, with_file_types=True)
194
+
195
+ for entry in entries:
196
+ if len(found_suggestions) >= max_results:
197
+ break
198
+
199
+ entry_path_relative = os.path.join(current_relative_path, entry.name)
200
+ entry_path_from_root = os.path.relpath(
201
+ os.path.join(start_dir, entry.name), start_dir
202
+ )
203
+
204
+ # 跳过隐藏文件(除非搜索前缀以.开头)
205
+ if not search_prefix.startswith('.') and entry.name.startswith('.'):
206
+ continue
207
+
208
+ # 检查文件过滤规则
209
+ if file_discovery and file_discovery.should_ignore_file(
210
+ entry_path_from_root, filter_options
211
+ ):
212
+ continue
213
+
214
+ # 检查名称匹配
215
+ if entry.name.lower().startswith(lower_search_prefix):
216
+ suffix = '/' if entry.is_dir() else ''
217
+ found_suggestions.append({
218
+ 'label': entry_path_relative + suffix,
219
+ 'value': escape_path(entry_path_relative + suffix)
220
+ })
221
+
222
+ # 递归搜索子目录
223
+ if (entry.is_dir() and
224
+ entry.name != 'node_modules' and
225
+ not entry.name.startswith('.') and
226
+ len(found_suggestions) < max_results):
227
+
228
+ sub_suggestions = await find_files_recursively(
229
+ os.path.join(start_dir, entry.name),
230
+ search_prefix,
231
+ file_discovery,
232
+ filter_options,
233
+ entry_path_relative,
234
+ depth + 1,
235
+ max_depth,
236
+ max_results - len(found_suggestions)
237
+ )
238
+ found_suggestions.extend(sub_suggestions)
239
+
240
+ except Exception:
241
+ # 忽略权限错误等异常
242
+ pass
243
+
244
+ return found_suggestions[:max_results]
245
+ ```
246
+
247
+ #### 3.3.2 Glob 搜索算法
248
+
249
+ ```python
250
+ async def find_files_with_glob(
251
+ search_prefix: str,
252
+ file_discovery_service: FileDiscoveryService,
253
+ filter_options: FilterOptions,
254
+ search_dir: str,
255
+ max_results: int = 50
256
+ ) -> List[Suggestion]:
257
+ """
258
+ 使用 glob 模式搜索文件
259
+
260
+ Args:
261
+ search_prefix: 搜索前缀
262
+ file_discovery_service: 文件发现服务
263
+ filter_options: 过滤选项
264
+ search_dir: 搜索目录
265
+ max_results: 最大结果数
266
+
267
+ Returns:
268
+ List[Suggestion]: 建议列表
269
+ """
270
+ glob_pattern = f"**/{search_prefix}*"
271
+
272
+ files = await glob.glob(
273
+ glob_pattern,
274
+ cwd=search_dir,
275
+ dot=search_prefix.startswith('.'),
276
+ case_sensitive=False
277
+ )
278
+
279
+ suggestions = []
280
+ for file in files[:max_results]:
281
+ if file_discovery_service:
282
+ if file_discovery_service.should_ignore_file(file, filter_options):
283
+ continue
284
+
285
+ absolute_path = os.path.abspath(os.path.join(search_dir, file))
286
+ label = os.path.relpath(absolute_path, os.getcwd())
287
+
288
+ suggestions.append({
289
+ 'label': label,
290
+ 'value': escape_path(label)
291
+ })
292
+
293
+ return suggestions
294
+ ```
295
+
296
+ ### 3.4 排序算法
297
+
298
+ ```python
299
+ def sort_suggestions(suggestions: List[Suggestion]) -> List[Suggestion]:
300
+ """
301
+ 对建议列表进行排序
302
+
303
+ 排序优先级:
304
+ 1. 路径深度(浅层优先)
305
+ 2. 目录类型(目录优先于文件)
306
+ 3. 文件名(不包括扩展名)
307
+ 4. 完整文件名
308
+
309
+ Args:
310
+ suggestions: 原始建议列表
311
+
312
+ Returns:
313
+ List[Suggestion]: 排序后的建议列表
314
+ """
315
+ def sort_key(suggestion: Suggestion) -> Tuple[int, bool, str, str]:
316
+ label = suggestion['label']
317
+
318
+ # 1. 计算路径深度
319
+ depth = label.count('/')
320
+
321
+ # 2. 判断是否为目录
322
+ is_dir = label.endswith('/')
323
+ dir_priority = 0 if is_dir else 1 # 目录优先
324
+
325
+ # 3. 获取文件名(不含扩展名)
326
+ basename = os.path.basename(label.rstrip('/'))
327
+ name_without_ext = os.path.splitext(basename)[0]
328
+
329
+ # 4. 完整标签名
330
+ full_label = label
331
+
332
+ return (depth, dir_priority, name_without_ext.lower(), full_label.lower())
333
+
334
+ return sorted(suggestions, key=sort_key)
335
+ ```
336
+
337
+ ---
338
+
339
+ ## 4. 文件过滤系统
340
+
341
+ ### 4.1 过滤服务架构
342
+
343
+ ```python
344
+ class FileDiscoveryService:
345
+ """文件发现和过滤服务"""
346
+
347
+ def __init__(self, project_root: str):
348
+ self.project_root = os.path.abspath(project_root)
349
+ self.git_ignore_filter = None
350
+ self.gemini_ignore_filter = None
351
+
352
+ # 初始化 Git 忽略过滤器
353
+ if self._is_git_repository(self.project_root):
354
+ self.git_ignore_filter = GitIgnoreParser(self.project_root)
355
+ try:
356
+ self.git_ignore_filter.load_git_repo_patterns()
357
+ except FileNotFoundError:
358
+ pass
359
+
360
+ # 初始化 Gemini 忽略过滤器
361
+ self.gemini_ignore_filter = GitIgnoreParser(self.project_root)
362
+ try:
363
+ self.gemini_ignore_filter.load_patterns('.geminiignore')
364
+ except FileNotFoundError:
365
+ pass
366
+
367
+ def should_ignore_file(self, file_path: str, options: FilterOptions) -> bool:
368
+ """
369
+ 检查文件是否应该被忽略
370
+
371
+ Args:
372
+ file_path: 文件路径
373
+ options: 过滤选项
374
+
375
+ Returns:
376
+ bool: 是否应该忽略
377
+ """
378
+ if options.get('respect_git_ignore', True) and self._should_git_ignore_file(file_path):
379
+ return True
380
+
381
+ if options.get('respect_gemini_ignore', True) and self._should_gemini_ignore_file(file_path):
382
+ return True
383
+
384
+ return False
385
+
386
+ def _should_git_ignore_file(self, file_path: str) -> bool:
387
+ """检查文件是否被 git 忽略"""
388
+ if self.git_ignore_filter:
389
+ return self.git_ignore_filter.is_ignored(file_path)
390
+ return False
391
+
392
+ def _should_gemini_ignore_file(self, file_path: str) -> bool:
393
+ """检查文件是否被 gemini 忽略"""
394
+ if self.gemini_ignore_filter:
395
+ return self.gemini_ignore_filter.is_ignored(file_path)
396
+ return False
397
+ ```
398
+
399
+ ### 4.2 过滤配置
400
+
401
+ ```python
402
+ @dataclass
403
+ class FilterOptions:
404
+ """文件过滤配置选项"""
405
+ respect_git_ignore: bool = True
406
+ respect_gemini_ignore: bool = True
407
+
408
+ DEFAULT_FILTER_OPTIONS = FilterOptions(
409
+ respect_git_ignore=True,
410
+ respect_gemini_ignore=True
411
+ )
412
+ ```
413
+
414
+ ---
415
+
416
+ ## 5. 路径处理工具
417
+
418
+ ### 5.1 路径转义处理
419
+
420
+ ```python
421
+ def escape_path(file_path: str) -> str:
422
+ """
423
+ 转义文件路径中的空格
424
+
425
+ Args:
426
+ file_path: 原始文件路径
427
+
428
+ Returns:
429
+ str: 转义后的路径
430
+ """
431
+ result = ""
432
+ for i, char in enumerate(file_path):
433
+ # 只转义未被转义的空格
434
+ if char == ' ' and (i == 0 or file_path[i-1] != '\\'):
435
+ result += '\\ '
436
+ else:
437
+ result += char
438
+ return result
439
+
440
+ def unescape_path(file_path: str) -> str:
441
+ """
442
+ 反转义文件路径中的空格
443
+
444
+ Args:
445
+ file_path: 转义的文件路径
446
+
447
+ Returns:
448
+ str: 反转义后的路径
449
+ """
450
+ return file_path.replace('\\ ', ' ')
451
+ ```
452
+
453
+ ### 5.2 Unicode 文本处理
454
+
455
+ ```python
456
+ def to_code_points(text: str) -> List[str]:
457
+ """
458
+ 将字符串转换为 Unicode 代码点数组
459
+
460
+ Args:
461
+ text: 输入字符串
462
+
463
+ Returns:
464
+ List[str]: Unicode 代码点列表
465
+ """
466
+ return list(text)
467
+
468
+ def cp_len(text: str) -> int:
469
+ """
470
+ 获取字符串的 Unicode 代码点长度
471
+
472
+ Args:
473
+ text: 输入字符串
474
+
475
+ Returns:
476
+ int: 代码点长度
477
+ """
478
+ return len(list(text))
479
+
480
+ def cp_slice(text: str, start: int, end: Optional[int] = None) -> str:
481
+ """
482
+ 按 Unicode 代码点切片字符串
483
+
484
+ Args:
485
+ text: 输入字符串
486
+ start: 起始位置
487
+ end: 结束位置
488
+
489
+ Returns:
490
+ str: 切片后的字符串
491
+ """
492
+ code_points = list(text)
493
+ return ''.join(code_points[start:end])
494
+ ```
495
+
496
+ ---
497
+
498
+ ## 6. 用户交互系统
499
+
500
+ ### 6.1 键盘导航
501
+
502
+ ```python
503
+ class SuggestionNavigator:
504
+ """建议列表导航器"""
505
+
506
+ def __init__(self, max_visible: int = 8):
507
+ self.suggestions: List[Suggestion] = []
508
+ self.active_index: int = -1
509
+ self.visible_start_index: int = 0
510
+ self.max_visible = max_visible
511
+
512
+ def navigate_up(self) -> None:
513
+ """向上导航"""
514
+ if not self.suggestions:
515
+ return
516
+
517
+ # 计算新的活动索引
518
+ new_active_index = (
519
+ len(self.suggestions) - 1
520
+ if self.active_index <= 0
521
+ else self.active_index - 1
522
+ )
523
+
524
+ # 调整滚动位置
525
+ if new_active_index == len(self.suggestions) - 1:
526
+ # 回绕到最后一项
527
+ if len(self.suggestions) > self.max_visible:
528
+ self.visible_start_index = max(0, len(self.suggestions) - self.max_visible)
529
+ elif new_active_index < self.visible_start_index:
530
+ # 向上滚动
531
+ self.visible_start_index = new_active_index
532
+
533
+ self.active_index = new_active_index
534
+
535
+ def navigate_down(self) -> None:
536
+ """向下导航"""
537
+ if not self.suggestions:
538
+ return
539
+
540
+ # 计算新的活动索引
541
+ new_active_index = (
542
+ 0
543
+ if self.active_index >= len(self.suggestions) - 1
544
+ else self.active_index + 1
545
+ )
546
+
547
+ # 调整滚动位置
548
+ if new_active_index == 0:
549
+ # 回绕到第一项
550
+ self.visible_start_index = 0
551
+ elif new_active_index >= self.visible_start_index + self.max_visible:
552
+ # 向下滚动
553
+ self.visible_start_index = new_active_index - self.max_visible + 1
554
+
555
+ self.active_index = new_active_index
556
+
557
+ def get_visible_suggestions(self) -> List[Suggestion]:
558
+ """获取当前可见的建议"""
559
+ end_index = min(
560
+ self.visible_start_index + self.max_visible,
561
+ len(self.suggestions)
562
+ )
563
+ return self.suggestions[self.visible_start_index:end_index]
564
+ ```
565
+
566
+ ### 6.2 自动完成处理
567
+
568
+ ```python
569
+ def handle_autocomplete(
570
+ buffer: TextBuffer,
571
+ suggestions: List[Suggestion],
572
+ selected_index: int
573
+ ) -> None:
574
+ """
575
+ 处理自动完成选择
576
+
577
+ Args:
578
+ buffer: 文本缓冲区
579
+ suggestions: 建议列表
580
+ selected_index: 选中的索引
581
+ """
582
+ if selected_index < 0 or selected_index >= len(suggestions):
583
+ return
584
+
585
+ query = buffer.get_text()
586
+ suggestion = suggestions[selected_index]['value']
587
+
588
+ if query.strip().startswith('/'):
589
+ # 处理斜杠命令自动完成
590
+ _handle_slash_command_autocomplete(buffer, query, suggestion)
591
+ else:
592
+ # 处理 @ 路径自动完成
593
+ _handle_at_path_autocomplete(buffer, query, suggestion)
594
+
595
+ def _handle_at_path_autocomplete(buffer: TextBuffer, query: str, suggestion: str) -> None:
596
+ """处理 @ 路径自动完成"""
597
+ at_index = query.rfind('@')
598
+ if at_index == -1:
599
+ return
600
+
601
+ path_part = query[at_index + 1:]
602
+ last_slash_index = path_part.rfind('/')
603
+
604
+ if last_slash_index == -1:
605
+ autocomplete_start_index = at_index + 1
606
+ else:
607
+ autocomplete_start_index = at_index + 1 + last_slash_index + 1
608
+
609
+ # 替换从自动完成位置到文本末尾的内容
610
+ buffer.replace_range_by_offset(
611
+ autocomplete_start_index,
612
+ len(query),
613
+ suggestion
614
+ )
615
+ ```
616
+
617
+ ---
618
+
619
+ ## 7. 配置系统
620
+
621
+ ### 7.1 完成配置
622
+
623
+ ```python
624
+ @dataclass
625
+ class CompletionConfig:
626
+ """自动完成配置"""
627
+
628
+ # 搜索配置
629
+ enable_recursive_search: bool = True
630
+ max_search_depth: int = 10
631
+ max_results: int = 50
632
+ debounce_delay_ms: int = 100
633
+
634
+ # 显示配置
635
+ max_visible_suggestions: int = 8
636
+ suggestion_width: int = 60
637
+
638
+ # 过滤配置
639
+ respect_git_ignore: bool = True
640
+ respect_gemini_ignore: bool = True
641
+
642
+ # 性能配置
643
+ search_timeout_ms: int = 5000
644
+
645
+ # 用户体验配置
646
+ show_loading_indicator: bool = True
647
+ auto_select_first: bool = True
648
+
649
+ DEFAULT_COMPLETION_CONFIG = CompletionConfig()
650
+ ```
651
+
652
+ ### 7.2 工作区配置
653
+
654
+ ```python
655
+ @dataclass
656
+ class WorkspaceConfig:
657
+ """工作区配置"""
658
+ directories: List[str]
659
+ current_directory: str
660
+ project_root: str
661
+
662
+ def get_search_directories(self) -> List[str]:
663
+ """获取搜索目录列表"""
664
+ return self.directories if self.directories else [self.current_directory]
665
+ ```
666
+
667
+ ---
668
+
669
+ ## 8. 性能优化策略
670
+
671
+ ### 8.1 搜索优化
672
+
673
+ ```python
674
+ class SearchOptimizer:
675
+ """搜索性能优化器"""
676
+
677
+ def __init__(self):
678
+ self.cache = {}
679
+ self.cache_ttl = 30 # 缓存30秒
680
+
681
+ async def optimized_search(
682
+ self,
683
+ search_prefix: str,
684
+ directories: List[str],
685
+ config: CompletionConfig
686
+ ) -> List[Suggestion]:
687
+ """
688
+ 优化的搜索实现
689
+
690
+ 策略:
691
+ 1. 缓存搜索结果
692
+ 2. 增量搜索
693
+ 3. 异步并发搜索
694
+ 4. 早期返回机制
695
+ """
696
+ cache_key = f"{search_prefix}:{':'.join(directories)}"
697
+
698
+ # 检查缓存
699
+ if cache_key in self.cache:
700
+ cached_result, timestamp = self.cache[cache_key]
701
+ if time.time() - timestamp < self.cache_ttl:
702
+ return self._filter_cached_results(cached_result, search_prefix)
703
+
704
+ # 并发搜索多个目录
705
+ search_tasks = []
706
+ for directory in directories:
707
+ task = self._search_directory(directory, search_prefix, config)
708
+ search_tasks.append(task)
709
+
710
+ # 等待所有搜索完成,但有超时机制
711
+ try:
712
+ results = await asyncio.wait_for(
713
+ asyncio.gather(*search_tasks),
714
+ timeout=config.search_timeout_ms / 1000
715
+ )
716
+ except asyncio.TimeoutError:
717
+ # 超时时返回已有结果
718
+ results = []
719
+ for task in search_tasks:
720
+ if task.done():
721
+ results.append(task.result())
722
+
723
+ # 合并和排序结果
724
+ all_suggestions = []
725
+ for result in results:
726
+ all_suggestions.extend(result)
727
+
728
+ sorted_suggestions = sort_suggestions(all_suggestions)
729
+ limited_suggestions = sorted_suggestions[:config.max_results]
730
+
731
+ # 更新缓存
732
+ self.cache[cache_key] = (limited_suggestions, time.time())
733
+
734
+ return limited_suggestions
735
+ ```
736
+
737
+ ### 8.2 防抖处理
738
+
739
+ ```python
740
+ class Debouncer:
741
+ """输入防抖处理器"""
742
+
743
+ def __init__(self, delay_ms: int = 100):
744
+ self.delay_ms = delay_ms
745
+ self.timer = None
746
+
747
+ def debounce(self, func: Callable, *args, **kwargs) -> None:
748
+ """
749
+ 防抖执行函数
750
+
751
+ Args:
752
+ func: 要执行的函数
753
+ *args: 函数参数
754
+ **kwargs: 函数关键字参数
755
+ """
756
+ if self.timer:
757
+ self.timer.cancel()
758
+
759
+ self.timer = threading.Timer(
760
+ self.delay_ms / 1000,
761
+ lambda: func(*args, **kwargs)
762
+ )
763
+ self.timer.start()
764
+
765
+ def cancel(self) -> None:
766
+ """取消待执行的函数"""
767
+ if self.timer:
768
+ self.timer.cancel()
769
+ self.timer = None
770
+ ```
771
+
772
+ ---
773
+
774
+ ## 9. 错误处理机制
775
+
776
+ ### 9.1 异常处理策略
777
+
778
+ ```python
779
+ class CompletionError(Exception):
780
+ """自动完成异常基类"""
781
+ pass
782
+
783
+ class SearchTimeoutError(CompletionError):
784
+ """搜索超时异常"""
785
+ pass
786
+
787
+ class FileAccessError(CompletionError):
788
+ """文件访问异常"""
789
+ pass
790
+
791
+ async def safe_file_search(
792
+ search_func: Callable,
793
+ error_handler: Optional[Callable] = None
794
+ ) -> List[Suggestion]:
795
+ """
796
+ 安全的文件搜索包装器
797
+
798
+ Args:
799
+ search_func: 搜索函数
800
+ error_handler: 错误处理函数
801
+
802
+ Returns:
803
+ List[Suggestion]: 搜索结果(错误时返回空列表)
804
+ """
805
+ try:
806
+ return await search_func()
807
+ except PermissionError:
808
+ # 权限错误 - 静默忽略
809
+ if error_handler:
810
+ error_handler("Permission denied for some directories")
811
+ return []
812
+ except FileNotFoundError:
813
+ # 文件不存在 - 静默忽略
814
+ if error_handler:
815
+ error_handler("Some directories do not exist")
816
+ return []
817
+ except asyncio.TimeoutError:
818
+ # 搜索超时
819
+ if error_handler:
820
+ error_handler("Search timeout - showing partial results")
821
+ return []
822
+ except Exception as e:
823
+ # 其他未知错误
824
+ if error_handler:
825
+ error_handler(f"Search error: {str(e)}")
826
+ return []
827
+ ```
828
+
829
+ ### 9.2 降级策略
830
+
831
+ ```python
832
+ class GracefulDegradation:
833
+ """优雅降级处理"""
834
+
835
+ @staticmethod
836
+ def fallback_search(
837
+ search_prefix: str,
838
+ current_directory: str
839
+ ) -> List[Suggestion]:
840
+ """
841
+ 降级搜索实现(仅搜索当前目录)
842
+
843
+ Args:
844
+ search_prefix: 搜索前缀
845
+ current_directory: 当前目录
846
+
847
+ Returns:
848
+ List[Suggestion]: 基础搜索结果
849
+ """
850
+ suggestions = []
851
+ try:
852
+ entries = os.listdir(current_directory)
853
+ for entry in entries:
854
+ if entry.startswith(search_prefix):
855
+ full_path = os.path.join(current_directory, entry)
856
+ is_dir = os.path.isdir(full_path)
857
+ suggestions.append({
858
+ 'label': entry + ('/' if is_dir else ''),
859
+ 'value': escape_path(entry + ('/' if is_dir else ''))
860
+ })
861
+ except Exception:
862
+ pass # 静默失败
863
+
864
+ return suggestions[:20] # 限制结果数量
865
+ ```
866
+
867
+ ---
868
+
869
+ ## 10. 测试用例规范
870
+
871
+ ### 10.1 功能测试用例
872
+
873
+ ```python
874
+ class TestFileRecommendation:
875
+ """文件推荐功能测试用例"""
876
+
877
+ def test_basic_at_detection(self):
878
+ """测试基本 @ 检测功能"""
879
+ test_cases = [
880
+ ("@", 0, 1, True), # 基本 @ 字符
881
+ ("hello @", 0, 7, True), # @ 前有文本
882
+ ("@file", 0, 5, True), # @ 后有文件名
883
+ ("user\\@domain", 0, 11, False), # 转义的 @
884
+ ("no at symbol", 0, 12, False), # 无 @ 字符
885
+ ]
886
+
887
+ for text, row, col, expected in test_cases:
888
+ lines = text.split('\n')
889
+ result = is_completion_active(text, row, col, lines)
890
+ assert result == expected, f"Failed for: {text}"
891
+
892
+ def test_path_parsing(self):
893
+ """测试路径解析功能"""
894
+ test_cases = [
895
+ ("@file.txt", (".", "file.txt", "file.txt")),
896
+ ("@src/", ("src/", "", "src/")),
897
+ ("@src/main.py", ("src/", "main.py", "src/main.py")),
898
+ ("@../config", ("../", "config", "../config")),
899
+ ("hello @world", (".", "world", "world")),
900
+ ]
901
+
902
+ for input_text, expected in test_cases:
903
+ base_dir, prefix, partial = parse_at_command_path(input_text)
904
+ result = (base_dir, prefix, partial)
905
+ assert result == expected, f"Failed for: {input_text}, got {result}, expected {expected}"
906
+
907
+ def test_file_search(self):
908
+ """测试文件搜索功能"""
909
+ # 创建临时测试目录结构
910
+ test_structure = {
911
+ "config.json": "",
912
+ "config.yaml": "",
913
+ "src/": {
914
+ "main.py": "",
915
+ "utils.py": "",
916
+ "tests/": {
917
+ "test_main.py": ""
918
+ }
919
+ },
920
+ "docs/": {
921
+ "README.md": ""
922
+ }
923
+ }
924
+
925
+ with create_temp_directory(test_structure) as temp_dir:
926
+ # 测试基本搜索
927
+ suggestions = find_files_in_directory(temp_dir, "config")
928
+ assert len(suggestions) == 2
929
+ assert any(s['label'] == 'config.json' for s in suggestions)
930
+ assert any(s['label'] == 'config.yaml' for s in suggestions)
931
+
932
+ # 测试目录搜索
933
+ suggestions = find_files_in_directory(temp_dir, "src")
934
+ assert len(suggestions) >= 1
935
+ assert any(s['label'] == 'src/' for s in suggestions)
936
+
937
+ def test_sorting(self):
938
+ """测试排序功能"""
939
+ suggestions = [
940
+ {'label': 'deep/nested/file.txt', 'value': 'deep/nested/file.txt'},
941
+ {'label': 'config.json', 'value': 'config.json'},
942
+ {'label': 'src/', 'value': 'src/'},
943
+ {'label': 'src/main.py', 'value': 'src/main.py'},
944
+ {'label': 'docs/', 'value': 'docs/'},
945
+ ]
946
+
947
+ sorted_suggestions = sort_suggestions(suggestions)
948
+
949
+ # 检查排序结果
950
+ labels = [s['label'] for s in sorted_suggestions]
951
+
952
+ # 浅层文件应该排在前面
953
+ shallow_files = [l for l in labels if '/' not in l.rstrip('/')]
954
+ deep_files = [l for l in labels if l.count('/') > 1]
955
+
956
+ # 目录应该优先于同级文件
957
+ assert labels.index('src/') < labels.index('src/main.py')
958
+
959
+ def test_keyboard_navigation(self):
960
+ """测试键盘导航功能"""
961
+ navigator = SuggestionNavigator(max_visible=3)
962
+ suggestions = [
963
+ {'label': 'file1.txt', 'value': 'file1.txt'},
964
+ {'label': 'file2.txt', 'value': 'file2.txt'},
965
+ {'label': 'file3.txt', 'value': 'file3.txt'},
966
+ {'label': 'file4.txt', 'value': 'file4.txt'},
967
+ {'label': 'file5.txt', 'value': 'file5.txt'},
968
+ ]
969
+ navigator.suggestions = suggestions
970
+ navigator.active_index = 0
971
+
972
+ # 测试向下导航
973
+ navigator.navigate_down()
974
+ assert navigator.active_index == 1
975
+
976
+ # 测试向上导航
977
+ navigator.navigate_up()
978
+ assert navigator.active_index == 0
979
+
980
+ # 测试回绕
981
+ navigator.navigate_up()
982
+ assert navigator.active_index == 4 # 应该回绕到最后一项
983
+
984
+ ### 10.2 集成测试用例
985
+
986
+ ```python
987
+ class TestIntegration:
988
+ """集成测试用例"""
989
+
990
+ def test_complete_workflow(self):
991
+ """测试完整的推荐工作流程"""
992
+ # 模拟用户输入序列
993
+ inputs = [
994
+ ("@", "应该显示所有顶级文件"),
995
+ ("@con", "应该过滤显示以con开头的文件"),
996
+ ("@config.", "应该显示config.开头的文件"),
997
+ ("@src/", "应该显示src目录下的文件"),
998
+ ]
999
+
1000
+ completion_engine = CompletionEngine()
1001
+
1002
+ for input_text, description in inputs:
1003
+ suggestions = completion_engine.get_suggestions(input_text)
1004
+ assert len(suggestions) >= 0, f"Failed: {description}"
1005
+
1006
+ def test_performance_benchmark(self):
1007
+ """性能基准测试"""
1008
+ import time
1009
+
1010
+ # 创建大型目录结构
1011
+ large_structure = create_large_test_structure(1000) # 1000个文件
1012
+
1013
+ with create_temp_directory(large_structure) as temp_dir:
1014
+ start_time = time.time()
1015
+
1016
+ completion_engine = CompletionEngine(
1017
+ search_directories=[temp_dir],
1018
+ config=CompletionConfig(max_results=50)
1019
+ )
1020
+
1021
+ suggestions = completion_engine.get_suggestions("@test")
1022
+
1023
+ end_time = time.time()
1024
+ search_time = end_time - start_time
1025
+
1026
+ # 搜索应该在1秒内完成
1027
+ assert search_time < 1.0, f"Search took too long: {search_time}s"
1028
+ assert len(suggestions) > 0, "Should find some suggestions"
1029
+
1030
+ ### 10.3 边界条件测试
1031
+
1032
+ ```python
1033
+ class TestEdgeCases:
1034
+ """边界条件测试"""
1035
+
1036
+ def test_empty_input(self):
1037
+ """测试空输入"""
1038
+ assert not is_completion_active("", 0, 0, [""])
1039
+
1040
+ def test_special_characters(self):
1041
+ """测试特殊字符"""
1042
+ test_cases = [
1043
+ "@file with spaces.txt",
1044
+ "@中文文件.txt",
1045
+ "@file-with-dashes.txt",
1046
+ "@file_with_underscores.txt",
1047
+ "@file.with.dots.txt",
1048
+ ]
1049
+
1050
+ for case in test_cases:
1051
+ base_dir, prefix, partial = parse_at_command_path(case)
1052
+ assert prefix != "", f"Failed to parse: {case}"
1053
+
1054
+ def test_large_file_count(self):
1055
+ """测试大量文件情况"""
1056
+ # 测试有10000个文件的目录
1057
+ suggestions = []
1058
+ for i in range(10000):
1059
+ suggestions.append({
1060
+ 'label': f'file_{i:04d}.txt',
1061
+ 'value': f'file_{i:04d}.txt'
1062
+ })
1063
+
1064
+ # 排序应该仍然能够处理
1065
+ sorted_suggestions = sort_suggestions(suggestions)
1066
+ assert len(sorted_suggestions) == 10000
1067
+
1068
+ def test_permission_errors(self):
1069
+ """测试权限错误处理"""
1070
+ # 模拟权限错误
1071
+ def mock_search_with_permission_error():
1072
+ raise PermissionError("Access denied")
1073
+
1074
+ suggestions = safe_file_search(mock_search_with_permission_error)
1075
+ assert suggestions == [] # 应该返回空列表而不是抛出异常
1076
+ ```
1077
+
1078
+ ---
1079
+
1080
+ ## 11. Python 实现指南
1081
+
1082
+ ### 11.1 依赖库推荐
1083
+
1084
+ ```python
1085
+ # requirements.txt
1086
+ asyncio>=3.4.3 # 异步IO支持
1087
+ pathlib>=1.0.1 # 现代路径处理
1088
+ typing>=3.7.4 # 类型注解
1089
+ dataclasses>=0.6 # 数据类支持
1090
+ fnmatch>=1.0.2 # 文件名模式匹配
1091
+ glob2>=0.7 # 扩展glob支持
1092
+ gitignore-parser>=0.1.0 # .gitignore解析
1093
+ rich>=13.0.0 # 终端UI库
1094
+ prompt-toolkit>=3.0.0 # 高级输入处理
1095
+ ```
1096
+
1097
+ ### 11.2 项目结构建议
1098
+
1099
+ ```
1100
+ file_recommendation/
1101
+ ├── __init__.py
1102
+ ├── core/
1103
+ │ ├── __init__.py
1104
+ │ ├── completion_engine.py # 主要完成引擎
1105
+ │ ├── file_discovery.py # 文件发现服务
1106
+ │ ├── suggestion.py # 建议数据结构
1107
+ │ └── config.py # 配置管理
1108
+ ├── utils/
1109
+ │ ├── __init__.py
1110
+ │ ├── path_utils.py # 路径处理工具
1111
+ │ ├── text_utils.py # 文本处理工具
1112
+ │ └── performance.py # 性能优化工具
1113
+ ├── ui/
1114
+ │ ├── __init__.py
1115
+ │ ├── input_handler.py # 输入处理
1116
+ │ ├── suggestion_display.py # 建议显示
1117
+ │ └── keyboard_nav.py # 键盘导航
1118
+ ├── filters/
1119
+ │ ├── __init__.py
1120
+ │ ├── git_filter.py # Git忽略过滤
1121
+ │ └── custom_filter.py # 自定义过滤
1122
+ └── tests/
1123
+ ├── __init__.py
1124
+ ├── test_completion.py # 完成功能测试
1125
+ ├── test_performance.py # 性能测试
1126
+ └── test_integration.py # 集成测试
1127
+ ```
1128
+
1129
+ ### 11.3 主要入口点
1130
+
1131
+ ```python
1132
+ # main.py - 主要API入口
1133
+ from file_recommendation import FileRecommendationEngine
1134
+
1135
+ def main():
1136
+ """主要使用示例"""
1137
+ engine = FileRecommendationEngine(
1138
+ current_directory=os.getcwd(),
1139
+ config=CompletionConfig(
1140
+ enable_recursive_search=True,
1141
+ max_results=50,
1142
+ respect_git_ignore=True
1143
+ )
1144
+ )
1145
+
1146
+ # 模拟用户输入
1147
+ user_input = "@src/main"
1148
+
1149
+ if engine.should_show_suggestions(user_input):
1150
+ suggestions = engine.get_suggestions(user_input)
1151
+
1152
+ # 显示建议
1153
+ for i, suggestion in enumerate(suggestions):
1154
+ print(f"{i+1}. {suggestion['label']}")
1155
+
1156
+ return 0
1157
+
1158
+ if __name__ == "__main__":
1159
+ exit(main())
1160
+ ```
1161
+
1162
+ ---
1163
+
1164
+ ## 12. 实现优先级
1165
+
1166
+ ### 12.1 MVP (最小可行产品)
1167
+
1168
+ **第一阶段 - 基础功能**:
1169
+ 1. 基本 @ 字符检测
1170
+ 2. 简单文件搜索(当前目录)
1171
+ 3. 基础建议显示
1172
+ 4. Tab 键自动完成
1173
+
1174
+ **预计工作量**: 1-2周
1175
+
1176
+ ### 12.2 完整功能
1177
+
1178
+ **第二阶段 - 增强功能**:
1179
+ 1. 递归文件搜索
1180
+ 2. .gitignore 过滤支持
1181
+ 3. 键盘导航(上下箭头)
1182
+ 4. 路径转义处理
1183
+
1184
+ **预计工作量**: 2-3周
1185
+
1186
+ ### 12.3 性能优化
1187
+
1188
+ **第三阶段 - 优化功能**:
1189
+ 1. 搜索结果缓存
1190
+ 2. 异步搜索
1191
+ 3. 防抖处理
1192
+ 4. 性能监控
1193
+
1194
+ **预计工作量**: 1-2周
1195
+
1196
+ ---
1197
+
1198
+ ## 13. 总结
1199
+
1200
+ 本设计文档提供了 @ 文件推荐功能的完整技术规范,包括:
1201
+
1202
+ 1. **核心算法**: 触发检测、路径解析、文件搜索和排序
1203
+ 2. **系统架构**: 模块化设计和清晰的组件分离
1204
+ 3. **用户交互**: 键盘导航和自动完成处理
1205
+ 4. **性能优化**: 缓存、异步处理和防抖机制
1206
+ 5. **错误处理**: 优雅降级和异常处理策略
1207
+ 6. **测试用例**: 功能测试、集成测试和边界条件测试
1208
+
1209
+ 该文档为 Python 实现提供了详细的技术指导,确保功能的完整性和一致性。实现团队可以按照文档中的算法和架构进行开发,并使用提供的测试用例验证功能正确性。
1210
+
1211
+ ---
1212
+
1213
+ **文档维护**: 本文档应随着功能演进而更新,确保技术规范的准确性和完整性。