History

Merge branch 'main' into multi-line-highlight-stack-trace

Piotr Osiewicz committed 1 year ago

Commit 02a8a7d2 parents 1815b53 c72cdfd338 files · +12202 −9055Browse files

Unified · Split

338 files
  1. .github/ISSUE_TEMPLATE/01_bug_ai.yml +4 −5
  2. .github/ISSUE_TEMPLATE/02_bug_edit_predictions.yml +0 −36
  3. .github/ISSUE_TEMPLATE/03_bug_git.yml +0 −35
  4. .github/ISSUE_TEMPLATE/04_bug_debugger.yml +2 −2
  5. .github/ISSUE_TEMPLATE/10_bug_report.yml +4 −2
  6. .github/actions/build_docs/action.yml +1 −1
  7. .github/workflows/ci.yml +4 −1
  8. .github/workflows/eval.yml +1 −0
  9. .github/workflows/release_nightly.yml +1 −0
  10. .github/workflows/unit_evals.yml +2 −1
  11. Cargo.lock +242 −13
  12. Cargo.toml +5 −3
  13. assets/icons/circle_help.svg +1 −0
  14. assets/keymaps/default-linux.json +1 −0
  15. assets/keymaps/default-macos.json +1 −0
  16. assets/keymaps/vim.json +7 −1
  17. assets/prompts/assistant_system_prompt.hbs +2 −2
  18. assets/settings/default.json +16 −3
  19. assets/settings/initial_tasks.json +1 −1
  20. assets/themes/one/one.json +2 −0
  21. crates/activity_indicator/src/activity_indicator.rs +166 −66
  22. crates/agent/Cargo.toml +1 −0
  23. crates/agent/src/active_thread.rs +136 −26
  24. crates/agent/src/agent.rs +1 −1
  25. crates/agent/src/agent_configuration.rs +15 −15
  26. crates/agent/src/agent_configuration/configure_context_server_modal.rs +1 −1
  27. crates/agent/src/agent_diff.rs +4 −4
  28. crates/agent/src/agent_model_selector.rs +3 −2
  29. crates/agent/src/agent_panel.rs +117 −138
  30. crates/agent/src/buffer_codegen.rs +4 −2
  31. crates/agent/src/context_picker/completion_provider.rs +9 −9
  32. crates/agent/src/context_server_configuration.rs +31 −2
  33. crates/agent/src/context_server_tool.rs +4 −1
  34. crates/agent/src/inline_assistant.rs +44 −43
  35. crates/agent/src/inline_prompt_editor.rs +3 −1
  36. crates/agent/src/message_editor.rs +105 −96
  37. crates/agent/src/thread.rs +8 −5
  38. crates/agent/src/thread_history.rs +3 −2
  39. crates/agent/src/thread_store.rs +208 −27
  40. crates/agent/src/ui/preview/usage_callouts.rs +19 −14
  41. crates/anthropic/src/anthropic.rs +103 −71
  42. crates/askpass/Cargo.toml +0 −1
  43. crates/askpass/src/askpass.rs +2 −32
  44. crates/assistant_context_editor/src/context_editor.rs +19 −44
  45. crates/assistant_context_editor/src/context_store.rs +1 −1
  46. crates/assistant_context_editor/src/language_model_selector.rs +3 −2
  47. crates/assistant_slash_commands/src/context_server_command.rs +2 −2
  48. crates/assistant_slash_commands/src/file_command.rs +17 −24
  49. crates/assistant_tools/Cargo.toml +1 −0
  50. crates/assistant_tools/src/edit_agent.rs +33 −17
  51. crates/assistant_tools/src/edit_agent/edit_parser.rs +127 −9
  52. crates/assistant_tools/src/edit_agent/evals.rs +112 −56
  53. crates/assistant_tools/src/edit_agent/streaming_fuzzy_matcher.rs +97 −20
  54. crates/assistant_tools/src/edit_file_tool.rs +40 −17
  55. crates/assistant_tools/src/read_file_tool.rs +2 −2
  56. crates/assistant_tools/src/templates/edit_file_prompt.hbs +6 −5
  57. crates/assistant_tools/src/terminal_tool.rs +30 −23
  58. crates/auto_update/src/auto_update.rs +20 −5
  59. crates/bedrock/src/models.rs +4 −0
  60. crates/channel/src/channel_store.rs +1 −1
  61. crates/channel/src/channel_store/channel_index.rs +1 −1
  62. crates/channel/src/channel_store_tests.rs +0 −3
  63. crates/cli/src/main.rs +3 −0
  64. crates/client/Cargo.toml +7 −2
  65. crates/client/src/client.rs +10 −2
  66. crates/client/src/proxy.rs +44 −6
  67. crates/client/src/proxy/socks_proxy.rs +8 −2
  68. crates/client/src/telemetry.rs +175 −67
  69. crates/client/src/user.rs +4 −2
  70. crates/collab/migrations.sqlite/20221109000000_test_schema.sql +3 −1
  71. crates/collab/migrations/20250612153105_add_collaborator_commit_email.sql +4 −0
  72. crates/collab/src/api.rs +3 −3
  73. crates/collab/src/db.rs +4 −0
  74. crates/collab/src/db/queries/buffers.rs +8 −0
  75. crates/collab/src/db/queries/channels.rs +4 −3
  76. crates/collab/src/db/queries/contacts.rs +2 −2
  77. crates/collab/src/db/queries/contributors.rs +1 −1
  78. crates/collab/src/db/queries/projects.rs +29 −7
  79. crates/collab/src/db/queries/rooms.rs +5 −1
  80. crates/collab/src/db/queries/users.rs +3 −3
  81. crates/collab/src/db/tables/project_collaborator.rs +2 −0
  82. crates/collab/src/db/tests/buffer_tests.rs +4 −0
  83. crates/collab/src/db/tests/db_tests.rs +13 −6
  84. crates/collab/src/llm.rs +6 −0
  85. crates/collab/src/llm/token.rs +2 −2
  86. crates/collab/src/rpc.rs +64 −29
  87. crates/collab/src/seed.rs +1 −1
  88. crates/collab/src/tests/channel_guest_tests.rs +1 −1
  89. crates/collab/src/tests/following_tests.rs +2 −0
  90. crates/collab/src/tests/integration_tests.rs +52 −54
  91. crates/collab/src/tests/remote_editing_collaboration_tests.rs +3 −3
  92. crates/collab/src/tests/test_server.rs +1 −0
  93. crates/collab/src/user_backfiller.rs +1 −1
  94. crates/collab_ui/src/chat_panel.rs +1 −4
  95. crates/context_server/src/protocol.rs +12 −10
  96. crates/context_server/src/test.rs +1 −1
  97. crates/context_server/src/types.rs +91 −32
  98. crates/copilot/src/copilot.rs +43 −37
  99. crates/dap/Cargo.toml +3 −0
  100. crates/dap/src/adapters.rs +8 −4
  101. crates/dap/src/client.rs +4 −4
  102. crates/dap/src/registry.rs +2 −2
  103. crates/dap/src/transport.rs +157 −64
  104. crates/dap_adapters/Cargo.toml +1 −0
  105. crates/dap_adapters/src/codelldb.rs +11 −8
  106. crates/dap_adapters/src/gdb.rs +11 −6
  107. crates/dap_adapters/src/go.rs +48 −24
  108. crates/dap_adapters/src/javascript.rs +61 −8
  109. crates/dap_adapters/src/php.rs +15 −7
  110. crates/dap_adapters/src/python.rs +8 −2
  111. crates/dap_adapters/src/ruby.rs +107 −176
  112. crates/db/src/db.rs +2 −2
  113. crates/debugger_tools/src/dap_log.rs +320 −155
  114. crates/debugger_ui/src/debugger_panel.rs +149 −132
  115. crates/debugger_ui/src/debugger_ui.rs +231 −204
  116. crates/debugger_ui/src/new_process_modal.rs +262 −105
  117. crates/debugger_ui/src/session.rs +3 −4
  118. crates/debugger_ui/src/session/running.rs +35 −30
  119. crates/debugger_ui/src/session/running/breakpoint_list.rs +3 −3
  120. crates/debugger_ui/src/session/running/module_list.rs +16 −12
  121. crates/debugger_ui/src/session/running/stack_frame_list.rs +130 −53
  122. crates/debugger_ui/src/session/running/variable_list.rs +3 −2
  123. crates/debugger_ui/src/stack_trace_view.rs +5 −5
  124. crates/debugger_ui/src/tests/debugger_panel.rs +24 −8
  125. crates/debugger_ui/src/tests/stack_frame_list.rs +2 −2
  126. crates/debugger_ui/src/tests/variable_list.rs +20 −5
  127. crates/diagnostics/src/diagnostics.rs +3 −3
  128. crates/editor/Cargo.toml +0 −1
  129. crates/editor/src/actions.rs +3 −2
  130. crates/editor/src/code_completion_tests.rs +389 −2276
  131. crates/editor/src/code_context_menus.rs +47 −89
  132. crates/editor/src/display_map/block_map.rs +3 −3
  133. crates/editor/src/display_map/fold_map.rs +2 −2
  134. crates/editor/src/display_map/wrap_map.rs +1 −1
  135. crates/editor/src/editor.rs +676 −436
  136. crates/editor/src/editor_settings.rs +5 −0
  137. crates/editor/src/editor_tests.rs +254 −40
  138. crates/editor/src/element.rs +347 −237
  139. crates/editor/src/items.rs +43 −34
  140. crates/editor/src/jsx_tag_auto_close.rs +9 −4
  141. crates/editor/src/mouse_context_menu.rs +6 −9
  142. crates/editor/src/movement.rs +6 −4
  143. crates/editor/src/proposed_changes_editor.rs +3 −3
  144. crates/editor/src/scroll.rs +14 −3
  145. crates/editor/src/scroll/autoscroll.rs +13 −3
  146. crates/editor/src/scroll/scroll_amount.rs +21 −0
  147. crates/editor/src/selections_collection.rs +2 −1
  148. crates/editor/src/test/editor_lsp_test_context.rs +6 −0
  149. crates/extension/src/extension_events.rs +1 −0
  150. crates/extension_api/wit/since_v0.6.0/dap.wit +1 −1
  151. crates/extension_host/src/extension_host.rs +21 −4
  152. crates/extension_host/src/extension_store_test.rs +14 −5
  153. crates/extension_host/src/wasm_host.rs +2 −1
  154. crates/extensions_ui/src/extensions_ui.rs +14 −22
  155. crates/file_finder/src/file_finder_tests.rs +2 −2
  156. crates/fs/src/fs.rs +15 −0
  157. crates/git/src/git.rs +1 −0
  158. crates/git/src/repository.rs +72 −28
  159. crates/git_ui/Cargo.toml +1 −0
  160. crates/git_ui/src/commit_modal.rs +1 −0
  161. crates/git_ui/src/conflict_view.rs +21 −14
  162. crates/git_ui/src/git_panel.rs +285 −184
  163. crates/git_ui/src/git_ui.rs +32 −1
  164. crates/git_ui/src/project_diff.rs +12 −4
  165. crates/gpui/examples/data_table.rs +10 −15
  166. crates/gpui/examples/uniform_list.rs +2 −3
  167. crates/gpui/src/app.rs +3 −3
  168. crates/gpui/src/app/context.rs +12 −0
  169. crates/gpui/src/elements/uniform_list.rs +12 −17
  170. crates/gpui/src/gpui.rs +1 −1
  171. crates/gpui/src/platform.rs +6 −2
  172. crates/gpui/src/platform/blade/blade_renderer.rs +1 −1
  173. crates/gpui/src/platform/linux/headless/client.rs +1 −1
  174. crates/gpui/src/platform/linux/keyboard.rs +7 −6
  175. crates/gpui/src/platform/linux/wayland/client.rs +61 −46
  176. crates/gpui/src/platform/linux/x11/client.rs +244 −87
  177. crates/gpui/src/platform/linux/x11/window.rs +8 −7
  178. crates/gpui/src/platform/mac/events.rs +1 −1
  179. crates/gpui/src/platform/windows/events.rs +27 −20
  180. crates/gpui/src/platform/windows/platform.rs +8 −8
  181. crates/gpui/src/platform/windows/util.rs +12 −1
  182. crates/gpui/src/platform/windows/window.rs +7 −2
  183. crates/gpui/src/scene.rs +1 −1
  184. crates/gpui/src/text_system.rs +14 −0
  185. crates/gpui/src/text_system/line_layout.rs +2 −2
  186. crates/gpui/src/window.rs +17 −13
  187. crates/icons/src/icons.rs +1 −0
  188. crates/language/src/buffer.rs +11 −8
  189. crates/language/src/language.rs +3 −1
  190. crates/language/src/language_registry.rs +32 −10
  191. crates/language/src/language_settings.rs +4 −1
  192. crates/language/src/syntax_map.rs +56 −17
  193. crates/language/src/syntax_map/syntax_map_tests.rs +86 −2
  194. crates/language/src/task_context.rs +6 −5
  195. crates/language_extension/src/extension_lsp_adapter.rs +6 −4
  196. crates/language_model/src/fake_provider.rs +1 −0
  197. crates/language_model/src/language_model.rs +5 −1
  198. crates/language_model/src/rate_limiter.rs +12 −5
  199. crates/language_model/src/registry.rs +59 −0
  200. crates/language_models/src/provider/anthropic.rs +18 −10
  201. crates/language_models/src/provider/bedrock.rs +4 −6
  202. crates/language_models/src/provider/cloud.rs +8 −4
  203. crates/language_models/src/provider/copilot_chat.rs +5 −3
  204. crates/language_models/src/provider/deepseek.rs +1 −0
  205. crates/language_models/src/provider/google.rs +1 −0
  206. crates/language_models/src/provider/lmstudio.rs +1 −0
  207. crates/language_models/src/provider/mistral.rs +1 −0
  208. crates/language_models/src/provider/ollama.rs +2 −1
  209. crates/language_models/src/provider/open_ai.rs +2 −3
  210. crates/language_models/src/provider/open_router.rs +69 −19
  211. crates/language_tools/src/lsp_log.rs +47 −40
  212. crates/language_tools/src/syntax_tree_view.rs +2 −3
  213. crates/languages/Cargo.toml +0 −1
  214. crates/languages/src/go.rs +5 −4
  215. crates/languages/src/javascript/outline.scm +8 −2
  216. crates/languages/src/javascript/runnables.scm +8 −2
  217. crates/languages/src/python.rs +3 −2
  218. crates/languages/src/rust.rs +4 −2
  219. crates/languages/src/tsx/config.toml +1 −1
  220. crates/languages/src/tsx/outline.scm +24 −1
  221. crates/languages/src/tsx/overrides.scm +3 −1
  222. crates/languages/src/tsx/runnables.scm +26 −1
  223. crates/languages/src/typescript.rs +529 −327
  224. crates/languages/src/typescript/config.toml +1 −1
  225. crates/languages/src/typescript/outline.scm +8 −2
  226. crates/languages/src/typescript/overrides.scm +3 −1
  227. crates/languages/src/typescript/runnables.scm +8 −2
  228. crates/livekit_client/Cargo.toml +1 −1
  229. crates/livekit_client/src/livekit_client/playback.rs +1 −1
  230. crates/markdown/src/markdown.rs +1 −1
  231. crates/markdown_preview/src/markdown_parser.rs +4 −4
  232. crates/multi_buffer/src/multi_buffer.rs +22 −15
  233. crates/open_ai/src/open_ai.rs +28 −247
  234. crates/open_router/src/open_router.rs +119 −4
  235. crates/outline_panel/src/outline_panel.rs +6 −4
  236. crates/paths/src/paths.rs +66 −22
  237. crates/picker/src/picker.rs +3 −4
  238. crates/prettier/src/prettier.rs +2 −3
  239. crates/prettier/src/prettier_server.js +19 −50
  240. crates/project/Cargo.toml +0 −1
  241. crates/project/src/debugger/breakpoint_store.rs +1 −1
  242. crates/project/src/debugger/dap_store.rs +48 −18
  243. crates/project/src/debugger/locators/cargo.rs +14 −7
  244. crates/project/src/debugger/locators/go.rs +299 −307
  245. crates/project/src/debugger/locators/node.rs +28 −14
  246. crates/project/src/debugger/locators/python.rs +4 −4
  247. crates/project/src/debugger/session.rs +178 −88
  248. crates/project/src/direnv.rs +19 −11
  249. crates/project/src/environment.rs +8 −2
  250. crates/project/src/git_store/git_traversal.rs +1 −1
  251. crates/project/src/lsp_command.rs +41 −44
  252. crates/project/src/lsp_store.rs +155 −124
  253. crates/project/src/lsp_store/rust_analyzer_ext.rs +24 −47
  254. crates/project/src/project.rs +9 −2
  255. crates/project/src/project_tests.rs +109 −100
  256. crates/project/src/task_inventory.rs +434 −205
  257. crates/project/src/task_store.rs +4 −2
  258. crates/project/src/toolchain_store.rs +1 −1
  259. crates/project_panel/src/project_panel.rs +84 −39
  260. crates/project_panel/src/project_panel_settings.rs +5 −0
  261. crates/project_panel/src/project_panel_tests.rs +652 −124
  262. crates/prompt_store/src/prompts.rs +3 −0
  263. crates/proto/proto/call.proto +2 −0
  264. crates/proto/proto/core.proto +3 −1
  265. crates/proto/proto/debugger.proto +2 −1
  266. crates/recent_projects/src/remote_servers.rs +1 −1
  267. crates/remote_server/src/headless_project.rs +1 −0
  268. crates/remote_server/src/main.rs +8 −0
  269. crates/remote_server/src/remote_editing_tests.rs +2 −2
  270. crates/repl/src/notebook/cell.rs +4 −1
  271. crates/repl/src/notebook/notebook_ui.rs +2 −2
  272. crates/repl/src/outputs/plain.rs +1 −1
  273. crates/rope/src/chunk.rs +2 −2
  274. crates/rope/src/rope.rs +6 −6
  275. crates/search/src/project_search.rs +16 −6
  276. crates/semantic_index/src/project_index_debug_view.rs +3 −4
  277. crates/semantic_index/src/semantic_index.rs +2 −2
  278. crates/settings/src/settings_store.rs +127 −16
  279. crates/settings/src/vscode_import.rs +41 −8
  280. crates/settings_ui/Cargo.toml +4 −4
  281. crates/settings_ui/src/settings_ui.rs +25 −23
  282. crates/storybook/src/stories/auto_height_editor.rs +1 −1
  283. crates/sum_tree/src/sum_tree.rs +1 −1
  284. crates/task/src/lib.rs +15 −0
  285. crates/task/src/vscode_debug_format.rs +38 −26
  286. crates/tasks_ui/src/modal.rs +20 −4
  287. crates/tasks_ui/src/tasks_ui.rs +36 −32
  288. crates/terminal/Cargo.toml +1 −0
  289. crates/terminal/src/terminal.rs +17 −1
  290. crates/terminal/src/terminal_settings.rs +1 −1
  291. crates/terminal_view/src/terminal_element.rs +101 −51
  292. crates/terminal_view/src/terminal_view.rs +67 −12
  293. crates/text/src/text.rs +1 −1
  294. crates/theme/src/default_colors.rs +4 −10
  295. crates/theme/src/fallback_themes.rs +2 −17
  296. crates/theme/src/schema.rs +18 −30
  297. crates/theme/src/styles/colors.rs +2 −5
  298. crates/title_bar/src/platforms/platform_linux.rs +164 −4
  299. crates/title_bar/src/platforms/platform_windows.rs +14 −25
  300. crates/title_bar/src/title_bar.rs +1 −1
  301. crates/title_bar/src/window_controls.rs +0 −165
  302. crates/ui/src/components/callout.rs +163 −95
  303. crates/util/Cargo.toml +3 −0
  304. crates/util/src/shell_env.rs +68 −243
  305. crates/util/src/util.rs +91 −46
  306. crates/util_macros/src/util_macros.rs +16 −9
  307. crates/vim/src/normal/scroll.rs +100 −59
  308. crates/vim/test_data/test_scroll_jumps.json +12 −0
  309. crates/watch/src/watch.rs +1 −1
  310. crates/workspace/src/item.rs +21 −6
  311. crates/workspace/src/pane.rs +220 −17
  312. crates/workspace/src/workspace.rs +25 −5
  313. crates/worktree/src/worktree.rs +5 −5
  314. crates/zed/Cargo.toml +2 −2
  315. crates/zed/src/main.rs +12 −16
  316. crates/zed/src/zed.rs +18 −11
  317. crates/zed/src/zed/app_menus.rs +24 −0
  318. crates/zed/src/zed/component_preview.rs +3 −3
  319. crates/zed/src/zed/quick_action_bar.rs +0 −41
  320. crates/zeta/src/zeta.rs +1 −0
  321. docs/book.toml +1 −0
  322. docs/src/SUMMARY.md +1 −1
  323. docs/src/accounts.md +3 −3
  324. docs/src/ai/configuration.md +2 −2
  325. docs/src/ai/mcp.md +17 −7
  326. docs/src/ai/text-threads.md +1 −1
  327. docs/src/configuring-zed.md +2 −1
  328. docs/src/debugger.md +140 −23
  329. docs/src/development/linux.md +0 −66
  330. docs/src/extensions.md +1 −0
  331. docs/src/extensions/context-servers.md +0 −39
  332. docs/src/extensions/developing-extensions.md +1 −1
  333. docs/src/extensions/mcp-extensions.md +44 −0
  334. docs/src/languages/ruby.md +1 −27
  335. docs/src/languages/sql.md +1 −1
  336. docs/src/linux.md +80 −5
  337. script/check-links +32 −0
  338. tooling/workspace-hack/Cargo.toml +4 −0
.github/ISSUE_TEMPLATE/01_bug_ai.yml+4−5
@@ -1,4 +1,4 @@
1name: Bug Report (AI Related)1name: Bug Report (AI)
2description: Zed Agent Panel Bugs2description: Zed Agent Panel Bugs
3type: "Bug"3type: "Bug"
4labels: ["ai"]4labels: ["ai"]
@@ -19,15 +19,14 @@ body: · 14 unmodified lines
19 2.19 2.
20 3.20 3.
21 21
22 Actual Behavior:22 **Expected Behavior**:
23 Expected Behavior:23 **Actual Behavior**:
24 24
25 ### Model Provider Details25 ### Model Provider Details
26 - Provider: (Anthropic via ZedPro, Anthropic via API key, Copilot Chat, Mistral, OpenAI, etc)26 - Provider: (Anthropic via ZedPro, Anthropic via API key, Copilot Chat, Mistral, OpenAI, etc)
27 - Model Name:27 - Model Name:
28 - Mode: (Agent Panel, Inline Assistant, Terminal Assistant or Text Threads)28 - Mode: (Agent Panel, Inline Assistant, Terminal Assistant or Text Threads)
29 - MCP Servers in-use:29 - Other Details (MCPs, other settings, etc):
30 - Other Details:
31 validations:30 validations:
32 required: true31 required: true
33 32
.github/ISSUE_TEMPLATE/02_bug_edit_predictions.yml+0−36
@@ -1,36 +0,0 @@
1name: Bug Report (Edit Predictions)
2description: Zed Edit Predictions bugs
3type: "Bug"
4labels: ["ai", "inline completion", "zeta"]
5title: "Edit Predictions: <a short description of the Edit Prediction bug>"
6body:
7 - type: textarea
8 attributes:
9 label: Summary
10 description: Describe the bug with a one line summary, and provide detailed reproduction steps
11 value: |
12 <!-- Please insert a one line summary of the issue below -->
13 SUMMARY_SENTENCE_HERE
14
15 ### Description
16 <!-- Describe with sufficient detail to reproduce from a clean Zed install. -->
17 <!-- Please include the LLM provider and model name you are using -->
18 Steps to trigger the problem:
19 1.
20 2.
21 3.
22
23 Actual Behavior:
24 Expected Behavior:
25 validations:
26 required: true
27
28 - type: textarea
29 id: environment
30 attributes:
31 label: Zed Version and System Specs
32 description: 'Open Zed, and in the command palette select "zed: copy system specs into clipboard"'
33 placeholder: |
34 Output of "zed: copy system specs into clipboard"
35 validations:
36 required: true
.github/ISSUE_TEMPLATE/03_bug_git.yml+0−35
@@ -1,35 +0,0 @@
1name: Bug Report (Git)
2description: Zed Git-Related Bugs
3type: "Bug"
4labels: ["git"]
5title: "Git: <a short description of the Git bug>"
6body:
7 - type: textarea
8 attributes:
9 label: Summary
10 description: Describe the bug with a one line summary, and provide detailed reproduction steps
11 value: |
12 <!-- Please insert a one line summary of the issue below -->
13 SUMMARY_SENTENCE_HERE
14
15 ### Description
16 <!-- Describe with sufficient detail to reproduce from a clean Zed install. -->
17 Steps to trigger the problem:
18 1.
19 2.
20 3.
21
22 Actual Behavior:
23 Expected Behavior:
24
25 validations:
26 required: true
27 - type: textarea
28 id: environment
29 attributes:
30 label: Zed Version and System Specs
31 description: 'Open Zed, and in the command palette select "zed: copy system specs into clipboard"'
32 placeholder: |
33 Output of "zed: copy system specs into clipboard"
34 validations:
35 required: true
.github/ISSUE_TEMPLATE/04_bug_debugger.yml+2−2
@@ -19,8 +19,8 @@ body: · 18 unmodified lines
19 2.19 2.
20 3.20 3.
21 21
22 Actual Behavior:22 **Expected Behavior**:
23 Expected Behavior:23 **Actual Behavior**:
24 24
25 validations:25 validations:
26 required: true26 required: true
.github/ISSUE_TEMPLATE/10_bug_report.yml+4−2
@@ -18,14 +18,16 @@ body: · 17 unmodified lines
18 - Issues with insufficient detail may be summarily closed.18 - Issues with insufficient detail may be summarily closed.
19 -->19 -->
20 20
21 DESCRIPTION_HERE
22
21 Steps to reproduce:23 Steps to reproduce:
22 1.24 1.
23 2.25 2.
24 3.26 3.
25 4.27 4.
26 28
27 Expected Behavior:29 **Expected Behavior**:
28 Actual Behavior:30 **Actual Behavior**:
29 31
30 <!-- Before Submitting, did you:32 <!-- Before Submitting, did you:
31 1. Include settings.json, keymap.json, .editorconfig if relevant?33 1. Include settings.json, keymap.json, .editorconfig if relevant?
.github/actions/build_docs/action.yml+1−1
@@ -22,7 +22,7 @@ runs: · 21 unmodified lines
22 - name: Check for broken links22 - name: Check for broken links
23 uses: lycheeverse/lychee-action@82202e5e9c2f4ef1a55a3d02563e1cb6041e5332 # v2.4.123 uses: lycheeverse/lychee-action@82202e5e9c2f4ef1a55a3d02563e1cb6041e5332 # v2.4.1
24 with:24 with:
25 args: --no-progress './docs/src/**/*'25 args: --no-progress --exclude '^http' './docs/src/**/*'
26 fail: true26 fail: true
27 27
28 - name: Build book28 - name: Build book
.github/workflows/ci.yml+4−1
@@ -498,6 +498,7 @@ jobs: · 497 unmodified lines
498 needs:498 needs:
499 - job_spec499 - job_spec
500 - style500 - style
501 - check_docs
501 - migration_checks502 - migration_checks
502 # run_tests: If adding required tests, add them here and to script below.503 # run_tests: If adding required tests, add them here and to script below.
503 - workspace_hack504 - workspace_hack
@@ -515,7 +516,8 @@ jobs: · 11 unmodified lines
515 # Check dependent jobs...516 # Check dependent jobs...
516 RET_CODE=0517 RET_CODE=0
517 # Always check style518 # Always check style
518 [[ "${{ needs.style.result }}" != 'success' ]] && { RET_CODE=1; echo "style tests failed"; }519 [[ "${{ needs.style.result }}" != 'success' ]] && { RET_CODE=1; echo "style tests failed"; }
520 [[ "${{ needs.check_docs.result }}" != 'success' ]] && { RET_CODE=1; echo "docs checks failed"; }
519 521
520 # Only check test jobs if they were supposed to run522 # Only check test jobs if they were supposed to run
521 if [[ "${{ needs.job_spec.outputs.run_tests }}" == "true" ]]; then523 if [[ "${{ needs.job_spec.outputs.run_tests }}" == "true" ]]; then
@@ -801,6 +803,7 @@ jobs: · 279 unmodified lines
801 name: Build with Nix803 name: Build with Nix
802 uses: ./.github/workflows/nix.yml804 uses: ./.github/workflows/nix.yml
803 if: github.repository_owner == 'zed-industries' && contains(github.event.pull_request.labels.*.name, 'run-nix')805 if: github.repository_owner == 'zed-industries' && contains(github.event.pull_request.labels.*.name, 'run-nix')
806 secrets: inherit
804 with:807 with:
805 flake-output: debug808 flake-output: debug
806 # excludes the final package to only cache dependencies809 # excludes the final package to only cache dependencies
.github/workflows/eval.yml+1−0
@@ -30,6 +30,7 @@ jobs: · 29 unmodified lines
30 noop:30 noop:
31 name: No-op31 name: No-op
32 runs-on: ubuntu-latest32 runs-on: ubuntu-latest
33 if: github.repository_owner == 'zed-industries'
33 steps:34 steps:
34 - name: No-op35 - name: No-op
35 run: echo "Nothing to do"36 run: echo "Nothing to do"
.github/workflows/release_nightly.yml+1−0
@@ -214,6 +214,7 @@ jobs: · 213 unmodified lines
214 bundle-nix:214 bundle-nix:
215 name: Build and cache Nix package215 name: Build and cache Nix package
216 needs: tests216 needs: tests
217 secrets: inherit
217 uses: ./.github/workflows/nix.yml218 uses: ./.github/workflows/nix.yml
218 219
219 update-nightly-tag:220 update-nightly-tag:
.github/workflows/unit_evals.yml+2−1
@@ -19,6 +19,7 @@ env: · 18 unmodified lines
19 19
20jobs:20jobs:
21 unit_evals:21 unit_evals:
22 if: github.repository_owner == 'zed-industries'
22 timeout-minutes: 6023 timeout-minutes: 60
23 name: Run unit evals24 name: Run unit evals
24 runs-on:25 runs-on:
@@ -62,7 +63,7 @@ jobs: · 37 unmodified lines
62 63
63 - name: Run unit evals64 - name: Run unit evals
64 shell: bash -euxo pipefail {0}65 shell: bash -euxo pipefail {0}
65 run: cargo nextest run --workspace --no-fail-fast --features eval --no-capture -E 'test(::eval_)' --test-threads 166 run: cargo nextest run --workspace --no-fail-fast --features eval --no-capture -E 'test(::eval_)'
66 env:67 env:
67 ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}68 ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
68 69
Cargo.lock+242−13
@@ -99,6 +99,7 @@ dependencies = [ · 98 unmodified lines
99 "paths",99 "paths",
100 "picker",100 "picker",
101 "postage",101 "postage",
102 "pretty_assertions",
102 "project",103 "project",
103 "prompt_store",104 "prompt_store",
104 "proto",105 "proto",
@@ -490,7 +491,6 @@ dependencies = [ · 385 unmodified lines
490 "anyhow",491 "anyhow",
491 "futures 0.3.31",492 "futures 0.3.31",
492 "gpui",493 "gpui",
493 "shlex",
494 "smol",494 "smol",
495 "tempfile",495 "tempfile",
496 "util",496 "util",
@@ -704,6 +704,7 @@ dependencies = [ · 207 unmodified lines
704 "serde_json",704 "serde_json",
705 "settings",705 "settings",
706 "smallvec",706 "smallvec",
707 "smol",
707 "streaming_diff",708 "streaming_diff",
708 "strsim",709 "strsim",
709 "task",710 "task",
@@ -2821,9 +2822,11 @@ dependencies = [ · 2111 unmodified lines
2821 "collections",2822 "collections",
2822 "credentials_provider",2823 "credentials_provider",
2823 "feature_flags",2824 "feature_flags",
2825 "fs",
2824 "futures 0.3.31",2826 "futures 0.3.31",
2825 "gpui",2827 "gpui",
2826 "gpui_tokio",2828 "gpui_tokio",
2829 "hickory-resolver",
2827 "http_client",2830 "http_client",
2828 "http_client_tls",2831 "http_client_tls",
2829 "httparse",2832 "httparse",
@@ -2832,6 +2835,7 @@ dependencies = [ · 2 unmodified lines
2832 "paths",2835 "paths",
2833 "postage",2836 "postage",
2834 "rand 0.8.5",2837 "rand 0.8.5",
2838 "regex",
2835 "release_channel",2839 "release_channel",
2836 "rpc",2840 "rpc",
2837 "rustls-pki-types",2841 "rustls-pki-types",
@@ -3159,6 +3163,16 @@ dependencies = [ · 321 unmodified lines
3159 "memchr",3163 "memchr",
3160]3164]
3161 3165
3166[[package]]
3167name = "command-fds"
3168version = "0.3.1"
3169source = "registry+https://github.com/rust-lang/crates.io-index"
3170checksum = "2ec1052629a80c28594777d1252efc8a6b005d13f9edfd8c3fc0f44d5b32489a"
3171dependencies = [
3172 "nix 0.30.1",
3173 "thiserror 2.0.12",
3174]
3175
3162[[package]]3176[[package]]
3163name = "command_palette"3177name = "command_palette"
3164version = "0.1.0"3178version = "0.1.0"
@@ -3528,6 +3542,20 @@ dependencies = [ · 363 unmodified lines
3528 "coreaudio-sys",3542 "coreaudio-sys",
3529]3543]
3530 3544
3545[[package]]
3546name = "coreaudio-rs"
3547version = "0.13.0"
3548source = "registry+https://github.com/rust-lang/crates.io-index"
3549checksum = "1aae284fbaf7d27aa0e292f7677dfbe26503b0d555026f702940805a630eac17"
3550dependencies = [
3551 "bitflags 1.3.2",
3552 "libc",
3553 "objc2-audio-toolbox",
3554 "objc2-core-audio",
3555 "objc2-core-audio-types",
3556 "objc2-core-foundation",
3557]
3558
3531[[package]]3559[[package]]
3532name = "coreaudio-sys"3560name = "coreaudio-sys"
3533version = "0.2.16"3561version = "0.2.16"
@@ -3563,7 +3591,8 @@ dependencies = [ · 29 unmodified lines
3563[[package]]3591[[package]]
3564name = "cpal"3592name = "cpal"
3565version = "0.15.3"3593version = "0.15.3"
3566source = "git+https://github.com/zed-industries/cpal?rev=fd8bc2fd39f1f5fdee5a0690656caff9a26d9d50#fd8bc2fd39f1f5fdee5a0690656caff9a26d9d50"3594source = "registry+https://github.com/rust-lang/crates.io-index"
3595checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779"
3567dependencies = [3596dependencies = [
3568 "alsa",3597 "alsa",
3569 "core-foundation-sys",3598 "core-foundation-sys",
@@ -3573,7 +3602,7 @@ dependencies = [ · 3 unmodified lines
3573 "js-sys",3602 "js-sys",
3574 "libc",3603 "libc",
3575 "mach2",3604 "mach2",
3576 "ndk",3605 "ndk 0.8.0",
3577 "ndk-context",3606 "ndk-context",
3578 "oboe",3607 "oboe",
3579 "wasm-bindgen",3608 "wasm-bindgen",
@@ -3582,6 +3611,32 @@ dependencies = [ · 2 unmodified lines
3582 "windows 0.54.0",3611 "windows 0.54.0",
3583]3612]
3584 3613
3614[[package]]
3615name = "cpal"
3616version = "0.16.0"
3617source = "registry+https://github.com/rust-lang/crates.io-index"
3618checksum = "cbd307f43cc2a697e2d1f8bc7a1d824b5269e052209e28883e5bc04d095aaa3f"
3619dependencies = [
3620 "alsa",
3621 "coreaudio-rs 0.13.0",
3622 "dasp_sample",
3623 "jni",
3624 "js-sys",
3625 "libc",
3626 "mach2",
3627 "ndk 0.9.0",
3628 "ndk-context",
3629 "num-derive",
3630 "num-traits",
3631 "objc2-audio-toolbox",
3632 "objc2-core-audio",
3633 "objc2-core-audio-types",
3634 "wasm-bindgen",
3635 "wasm-bindgen-futures",
3636 "web-sys",
3637 "windows 0.54.0",
3638]
3639
3585[[package]]3640[[package]]
3586name = "cpp_demangle"3641name = "cpp_demangle"
3587version = "0.4.4"3642version = "0.4.4"
@@ -4015,6 +4070,7 @@ dependencies = [ · 427 unmodified lines
4015 "gpui",4070 "gpui",
4016 "http_client",4071 "http_client",
4017 "language",4072 "language",
4073 "libc",
4018 "log",4074 "log",
4019 "node_runtime",4075 "node_runtime",
4020 "parking_lot",4076 "parking_lot",
@@ -4038,7 +4094,7 @@ dependencies = [ · 17 unmodified lines
4038[[package]]4094[[package]]
4039name = "dap-types"4095name = "dap-types"
4040version = "0.0.1"4096version = "0.0.1"
4041source = "git+https://github.com/zed-industries/dap-types?rev=68516de327fa1be15214133a0a2e52a12982ce75#68516de327fa1be15214133a0a2e52a12982ce75"4097source = "git+https://github.com/zed-industries/dap-types?rev=b40956a7f4d1939da67429d941389ee306a3a308#b40956a7f4d1939da67429d941389ee306a3a308"
4042dependencies = [4098dependencies = [
4043 "schemars",4099 "schemars",
4044 "serde",4100 "serde",
@@ -4051,6 +4107,7 @@ version = "0.1.0" · 6 unmodified lines
4051dependencies = [4107dependencies = [
4052 "anyhow",4108 "anyhow",
4053 "async-trait",4109 "async-trait",
4110 "collections",
4054 "dap",4111 "dap",
4055 "futures 0.3.31",4112 "futures 0.3.31",
4056 "gpui",4113 "gpui",
@@ -4682,7 +4739,6 @@ dependencies = [ · 625 unmodified lines
4682 "client",4739 "client",
4683 "clock",4740 "clock",
4684 "collections",4741 "collections",
4685 "command_palette_hooks",
4686 "convert_case 0.8.0",4742 "convert_case 0.8.0",
4687 "ctor",4743 "ctor",
4688 "dap",4744 "dap",
@@ -4851,6 +4907,18 @@ version = "1.1.0" · 162 unmodified lines
4851source = "registry+https://github.com/rust-lang/crates.io-index"4907source = "registry+https://github.com/rust-lang/crates.io-index"
4852checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf"4908checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf"
4853 4909
4910[[package]]
4911name = "enum-as-inner"
4912version = "0.6.1"
4913source = "registry+https://github.com/rust-lang/crates.io-index"
4914checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc"
4915dependencies = [
4916 "heck 0.5.0",
4917 "proc-macro2",
4918 "quote",
4919 "syn 2.0.101",
4920]
4921
4854[[package]]4922[[package]]
4855name = "enumflags2"4923name = "enumflags2"
4856version = "0.7.11"4924version = "0.7.11"
@@ -6108,6 +6176,7 @@ dependencies = [ · 1251 unmodified lines
6108 "anyhow",6176 "anyhow",
6109 "askpass",6177 "askpass",
6110 "buffer_diff",6178 "buffer_diff",
6179 "call",
6111 "chrono",6180 "chrono",
6112 "collections",6181 "collections",
6113 "command_palette_hooks",6182 "command_palette_hooks",
@@ -7428,6 +7497,51 @@ version = "0.2.1" · 1314 unmodified lines
7428source = "registry+https://github.com/rust-lang/crates.io-index"7497source = "registry+https://github.com/rust-lang/crates.io-index"
7429checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df"7498checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df"
7430 7499
7500[[package]]
7501name = "hickory-proto"
7502version = "0.24.4"
7503source = "registry+https://github.com/rust-lang/crates.io-index"
7504checksum = "92652067c9ce6f66ce53cc38d1169daa36e6e7eb7dd3b63b5103bd9d97117248"
7505dependencies = [
7506 "async-trait",
7507 "cfg-if",
7508 "data-encoding",
7509 "enum-as-inner",
7510 "futures-channel",
7511 "futures-io",
7512 "futures-util",
7513 "idna",
7514 "ipnet",
7515 "once_cell",
7516 "rand 0.8.5",
7517 "thiserror 1.0.69",
7518 "tinyvec",
7519 "tokio",
7520 "tracing",
7521 "url",
7522]
7523
7524[[package]]
7525name = "hickory-resolver"
7526version = "0.24.4"
7527source = "registry+https://github.com/rust-lang/crates.io-index"
7528checksum = "cbb117a1ca520e111743ab2f6688eddee69db4e0ea242545a604dce8a66fd22e"
7529dependencies = [
7530 "cfg-if",
7531 "futures-util",
7532 "hickory-proto",
7533 "ipconfig",
7534 "lru-cache",
7535 "once_cell",
7536 "parking_lot",
7537 "rand 0.8.5",
7538 "resolv-conf",
7539 "smallvec",
7540 "thiserror 1.0.69",
7541 "tokio",
7542 "tracing",
7543]
7544
7431[[package]]7545[[package]]
7432name = "hidden-trait"7546name = "hidden-trait"
7433version = "0.1.2"7547version = "0.1.2"
@@ -8297,6 +8411,18 @@ dependencies = [ · 863 unmodified lines
8297 "windows 0.58.0",8411 "windows 0.58.0",
8298]8412]
8299 8413
8414[[package]]
8415name = "ipconfig"
8416version = "0.3.2"
8417source = "registry+https://github.com/rust-lang/crates.io-index"
8418checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f"
8419dependencies = [
8420 "socket2",
8421 "widestring",
8422 "windows-sys 0.48.0",
8423 "winreg 0.50.0",
8424]
8425
8300[[package]]8426[[package]]
8301name = "ipnet"8427name = "ipnet"
8302version = "2.11.0"8428version = "2.11.0"
@@ -8990,7 +9116,6 @@ dependencies = [ · 687 unmodified lines
8990 "tree-sitter-yaml",9116 "tree-sitter-yaml",
8991 "unindent",9117 "unindent",
8992 "util",9118 "util",
8993 "which 6.0.3",
8994 "workspace",9119 "workspace",
8995 "workspace-hack",9120 "workspace-hack",
8996]9121]
@@ -9182,6 +9307,12 @@ dependencies = [ · 185 unmodified lines
9182 "cc",9307 "cc",
9183]9308]
9184 9309
9310[[package]]
9311name = "linked-hash-map"
9312version = "0.5.6"
9313source = "registry+https://github.com/rust-lang/crates.io-index"
9314checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
9315
9185[[package]]9316[[package]]
9186name = "linkify"9317name = "linkify"
9187version = "0.10.0"9318version = "0.10.0"
@@ -9308,7 +9439,7 @@ dependencies = [ · 120 unmodified lines
9308 "core-foundation 0.10.0",9439 "core-foundation 0.10.0",
9309 "core-video",9440 "core-video",
9310 "coreaudio-rs 0.12.1",9441 "coreaudio-rs 0.12.1",
9311 "cpal",9442 "cpal 0.16.0",
9312 "futures 0.3.31",9443 "futures 0.3.31",
9313 "gpui",9444 "gpui",
9314 "gpui_tokio",9445 "gpui_tokio",
@@ -9442,6 +9573,15 @@ dependencies = [ · 127 unmodified lines
9442 "hashbrown 0.15.3",9573 "hashbrown 0.15.3",
9443]9574]
9444 9575
9576[[package]]
9577name = "lru-cache"
9578version = "0.1.2"
9579source = "registry+https://github.com/rust-lang/crates.io-index"
9580checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c"
9581dependencies = [
9582 "linked-hash-map",
9583]
9584
9445[[package]]9585[[package]]
9446name = "lsp"9586name = "lsp"
9447version = "0.1.0"9587version = "0.1.0"
@@ -10078,7 +10218,21 @@ dependencies = [ · 630 unmodified lines
10078 "bitflags 2.9.0",10218 "bitflags 2.9.0",
10079 "jni-sys",10219 "jni-sys",
10080 "log",10220 "log",
10081 "ndk-sys",10221 "ndk-sys 0.5.0+25.2.9519653",
10222 "num_enum",
10223 "thiserror 1.0.69",
10224]
10225
10226[[package]]
10227name = "ndk"
10228version = "0.9.0"
10229source = "registry+https://github.com/rust-lang/crates.io-index"
10230checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4"
10231dependencies = [
10232 "bitflags 2.9.0",
10233 "jni-sys",
10234 "log",
10235 "ndk-sys 0.6.0+11769913",
10082 "num_enum",10236 "num_enum",
10083 "thiserror 1.0.69",10237 "thiserror 1.0.69",
10084]10238]
@@ -10098,6 +10252,15 @@ dependencies = [ · 13 unmodified lines
10098 "jni-sys",10252 "jni-sys",
10099]10253]
10100 10254
10255[[package]]
10256name = "ndk-sys"
10257version = "0.6.0+11769913"
10258source = "registry+https://github.com/rust-lang/crates.io-index"
10259checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873"
10260dependencies = [
10261 "jni-sys",
10262]
10263
10101[[package]]10264[[package]]
10102name = "new_debug_unreachable"10265name = "new_debug_unreachable"
10103version = "1.0.6"10266version = "1.0.6"
@@ -10129,6 +10292,18 @@ dependencies = [ · 25 unmodified lines
10129 "memoffset",10292 "memoffset",
10130]10293]
10131 10294
10295[[package]]
10296name = "nix"
10297version = "0.30.1"
10298source = "registry+https://github.com/rust-lang/crates.io-index"
10299checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
10300dependencies = [
10301 "bitflags 2.9.0",
10302 "cfg-if",
10303 "cfg_aliases 0.2.1",
10304 "libc",
10305]
10306
10132[[package]]10307[[package]]
10133name = "node_runtime"10308name = "node_runtime"
10134version = "0.1.0"10309version = "0.1.0"
@@ -10499,6 +10674,43 @@ dependencies = [ · 364 unmodified lines
10499 "objc2-quartz-core",10674 "objc2-quartz-core",
10500]10675]
10501 10676
10677[[package]]
10678name = "objc2-audio-toolbox"
10679version = "0.3.1"
10680source = "registry+https://github.com/rust-lang/crates.io-index"
10681checksum = "10cbe18d879e20a4aea544f8befe38bcf52255eb63d3f23eca2842f3319e4c07"
10682dependencies = [
10683 "bitflags 2.9.0",
10684 "libc",
10685 "objc2",
10686 "objc2-core-audio",
10687 "objc2-core-audio-types",
10688 "objc2-core-foundation",
10689 "objc2-foundation",
10690]
10691
10692[[package]]
10693name = "objc2-core-audio"
10694version = "0.3.1"
10695source = "registry+https://github.com/rust-lang/crates.io-index"
10696checksum = "ca44961e888e19313b808f23497073e3f6b3c22bb485056674c8b49f3b025c82"
10697dependencies = [
10698 "dispatch2",
10699 "objc2",
10700 "objc2-core-audio-types",
10701 "objc2-core-foundation",
10702]
10703
10704[[package]]
10705name = "objc2-core-audio-types"
10706version = "0.3.1"
10707source = "registry+https://github.com/rust-lang/crates.io-index"
10708checksum = "c0f1cc99bb07ad2ddb6527ddf83db6a15271bb036b3eb94b801cd44fdc666ee1"
10709dependencies = [
10710 "bitflags 2.9.0",
10711 "objc2",
10712]
10713
10502[[package]]10714[[package]]
10503name = "objc2-core-foundation"10715name = "objc2-core-foundation"
10504version = "0.3.1"10716version = "0.3.1"
@@ -10604,7 +10816,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" · 99 unmodified lines
10604checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb"10816checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb"
10605dependencies = [10817dependencies = [
10606 "jni",10818 "jni",
10607 "ndk",10819 "ndk 0.8.0",
10608 "ndk-context",10820 "ndk-context",
10609 "num-derive",10821 "num-derive",
10610 "num-traits",10822 "num-traits",
@@ -12109,7 +12321,6 @@ dependencies = [ · 1498 unmodified lines
12109 "unindent",12321 "unindent",
12110 "url",12322 "url",
12111 "util",12323 "util",
12112 "uuid",
12113 "which 6.0.3",12324 "which 6.0.3",
12114 "workspace-hack",12325 "workspace-hack",
12115 "worktree",12326 "worktree",
@@ -13202,6 +13413,7 @@ dependencies = [ · 1086 unmodified lines
13202 "futures-core",13413 "futures-core",
13203 "futures-util",13414 "futures-util",
13204 "h2 0.4.9",13415 "h2 0.4.9",
13416 "hickory-resolver",
13205 "http 1.3.1",13417 "http 1.3.1",
13206 "http-body 1.0.1",13418 "http-body 1.0.1",
13207 "http-body-util",13419 "http-body-util",
@@ -13258,6 +13470,12 @@ dependencies = [ · 50 unmodified lines
13258 "workspace-hack",13470 "workspace-hack",
13259]13471]
13260 13472
13473[[package]]
13474name = "resolv-conf"
13475version = "0.7.4"
13476source = "registry+https://github.com/rust-lang/crates.io-index"
13477checksum = "95325155c684b1c89f7765e30bc1c42e4a6da51ca513615660cb8a62ef9a88e3"
13478
13261[[package]]13479[[package]]
13262name = "resvg"13480name = "resvg"
13263version = "0.45.1"13481version = "0.45.1"
@@ -13377,7 +13595,7 @@ version = "0.20.1" · 113 unmodified lines
13377source = "registry+https://github.com/rust-lang/crates.io-index"13595source = "registry+https://github.com/rust-lang/crates.io-index"
13378checksum = "e7ceb6607dd738c99bc8cb28eff249b7cd5c8ec88b9db96c0608c1480d140fb1"13596checksum = "e7ceb6607dd738c99bc8cb28eff249b7cd5c8ec88b9db96c0608c1480d140fb1"
13379dependencies = [13597dependencies = [
13380 "cpal",13598 "cpal 0.15.3",
13381 "hound",13599 "hound",
13382]13600]
13383 13601
@@ -14389,12 +14607,12 @@ dependencies = [ · 1005 unmodified lines
14389 "fs",14607 "fs",
14390 "gpui",14608 "gpui",
14391 "log",14609 "log",
14392 "paths",
14393 "schemars",14610 "schemars",
14394 "serde",14611 "serde",
14395 "settings",14612 "settings",
14396 "theme",14613 "theme",
14397 "ui",14614 "ui",
14615 "util",
14398 "workspace",14616 "workspace",
14399 "workspace-hack",14617 "workspace-hack",
14400]14618]
@@ -15725,6 +15943,7 @@ dependencies = [ · 1324 unmodified lines
15725 "theme",15943 "theme",
15726 "thiserror 2.0.12",15944 "thiserror 2.0.12",
15727 "url",15945 "url",
15946 "urlencoding",
15728 "util",15947 "util",
15729 "windows 0.61.1",15948 "windows 0.61.1",
15730 "workspace-hack",15949 "workspace-hack",
@@ -17121,6 +17340,7 @@ dependencies = [ · 1390 unmodified lines
17121 "async-fs",17340 "async-fs",
17122 "async_zip",17341 "async_zip",
17123 "collections",17342 "collections",
17343 "command-fds",
17124 "dirs 4.0.0",17344 "dirs 4.0.0",
17125 "dunce",17345 "dunce",
17126 "futures 0.3.31",17346 "futures 0.3.31",
@@ -17131,12 +17351,14 @@ dependencies = [ · 4 unmodified lines
17131 "itertools 0.14.0",17351 "itertools 0.14.0",
17132 "libc",17352 "libc",
17133 "log",17353 "log",
17354 "nix 0.29.0",
17134 "rand 0.8.5",17355 "rand 0.8.5",
17135 "regex",17356 "regex",
17136 "rust-embed",17357 "rust-embed",
17137 "serde",17358 "serde",
17138 "serde_json",17359 "serde_json",
17139 "serde_json_lenient",17360 "serde_json_lenient",
17361 "shlex",
17140 "smol",17362 "smol",
17141 "take-until",17363 "take-until",
17142 "tempfile",17364 "tempfile",
@@ -18178,6 +18400,12 @@ dependencies = [ · 1035 unmodified lines
18178 "wasite",18400 "wasite",
18179]18401]
18180 18402
18403[[package]]
18404name = "widestring"
18405version = "1.2.0"
18406source = "registry+https://github.com/rust-lang/crates.io-index"
18407checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d"
18408
18181[[package]]18409[[package]]
18182name = "wiggle"18410name = "wiggle"
18183version = "29.0.1"18411version = "29.0.1"
@@ -19298,6 +19526,7 @@ dependencies = [ · 1114 unmodified lines
19298 "num-rational",19526 "num-rational",
19299 "num-traits",19527 "num-traits",
19300 "objc2",19528 "objc2",
19529 "objc2-core-foundation",
19301 "objc2-foundation",19530 "objc2-foundation",
19302 "objc2-metal",19531 "objc2-metal",
19303 "object",19532 "object",
@@ -19718,7 +19947,7 @@ dependencies = [ · 414 unmodified lines
19718 19947
19719[[package]]19948[[package]]
19720name = "zed"19949name = "zed"
19721version = "0.191.0"19950version = "0.192.0"
19722dependencies = [19951dependencies = [
19723 "activity_indicator",19952 "activity_indicator",
19724 "agent",19953 "agent",
Cargo.toml+5−3
@@ -433,9 +433,10 @@ convert_case = "0.8.0" · 432 unmodified lines
433core-foundation = "0.10.0"433core-foundation = "0.10.0"
434core-foundation-sys = "0.8.6"434core-foundation-sys = "0.8.6"
435core-video = { version = "0.4.3", features = ["metal"] }435core-video = { version = "0.4.3", features = ["metal"] }
436cpal = "0.16"
436criterion = { version = "0.5", features = ["html_reports"] }437criterion = { version = "0.5", features = ["html_reports"] }
437ctor = "0.4.0"438ctor = "0.4.0"
438dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "68516de327fa1be15214133a0a2e52a12982ce75" }439dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "b40956a7f4d1939da67429d941389ee306a3a308" }
439dashmap = "6.0"440dashmap = "6.0"
440derive_more = "0.99.17"441derive_more = "0.99.17"
441dirs = "4.0"442dirs = "4.0"
@@ -523,6 +524,7 @@ reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "951c77 · 81 unmodified lines
523 "rustls-tls-native-roots",524 "rustls-tls-native-roots",
524 "socks",525 "socks",
525 "stream",526 "stream",
527 "hickory-dns",
526] }528] }
527rsa = "0.9.6"529rsa = "0.9.6"
528runtimelib = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734", default-features = false, features = [530runtimelib = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734", default-features = false, features = [
@@ -682,9 +684,7 @@ features = [ · 153 unmodified lines
682 "Win32_UI_WindowsAndMessaging",684 "Win32_UI_WindowsAndMessaging",
683]685]
684 686
685# TODO livekit https://github.com/RustAudio/cpal/pull/891
686[patch.crates-io]687[patch.crates-io]
687cpal = { git = "https://github.com/zed-industries/cpal", rev = "fd8bc2fd39f1f5fdee5a0690656caff9a26d9d50" }
688notify = { git = "https://github.com/zed-industries/notify.git", rev = "bbb9ea5ae52b253e095737847e367c30653a2e96" }688notify = { git = "https://github.com/zed-industries/notify.git", rev = "bbb9ea5ae52b253e095737847e367c30653a2e96" }
689notify-types = { git = "https://github.com/zed-industries/notify.git", rev = "bbb9ea5ae52b253e095737847e367c30653a2e96" }689notify-types = { git = "https://github.com/zed-industries/notify.git", rev = "bbb9ea5ae52b253e095737847e367c30653a2e96" }
690 690
@@ -698,6 +698,8 @@ codegen-units = 16 · 7 unmodified lines
698[profile.dev.package]698[profile.dev.package]
699taffy = { opt-level = 3 }699taffy = { opt-level = 3 }
700cranelift-codegen = { opt-level = 3 }700cranelift-codegen = { opt-level = 3 }
701cranelift-codegen-meta = { opt-level = 3 }
702cranelift-codegen-shared = { opt-level = 3 }
701resvg = { opt-level = 3 }703resvg = { opt-level = 3 }
702rustybuzz = { opt-level = 3 }704rustybuzz = { opt-level = 3 }
703ttf-parser = { opt-level = 3 }705ttf-parser = { opt-level = 3 }
assets/icons/circle_help.svg+1−0
@@ -0,0 +1 @@
1<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-help-icon lucide-circle-help"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><path d="M12 17h.01"/></svg>
assets/keymaps/default-linux.json+1−0
@@ -115,6 +115,7 @@ · 114 unmodified lines
115 "ctrl-\"": "editor::ExpandAllDiffHunks",115 "ctrl-\"": "editor::ExpandAllDiffHunks",
116 "ctrl-i": "editor::ShowSignatureHelp",116 "ctrl-i": "editor::ShowSignatureHelp",
117 "alt-g b": "git::Blame",117 "alt-g b": "git::Blame",
118 "alt-g m": "git::OpenModifiedFiles",
118 "menu": "editor::OpenContextMenu",119 "menu": "editor::OpenContextMenu",
119 "shift-f10": "editor::OpenContextMenu",120 "shift-f10": "editor::OpenContextMenu",
120 "ctrl-shift-e": "editor::ToggleEditPrediction",121 "ctrl-shift-e": "editor::ToggleEditPrediction",
assets/keymaps/default-macos.json+1−0
@@ -139,6 +139,7 @@ · 138 unmodified lines
139 "cmd-'": "editor::ToggleSelectedDiffHunks",139 "cmd-'": "editor::ToggleSelectedDiffHunks",
140 "cmd-\"": "editor::ExpandAllDiffHunks",140 "cmd-\"": "editor::ExpandAllDiffHunks",
141 "cmd-alt-g b": "git::Blame",141 "cmd-alt-g b": "git::Blame",
142 "cmd-alt-g m": "git::OpenModifiedFiles",
142 "cmd-i": "editor::ShowSignatureHelp",143 "cmd-i": "editor::ShowSignatureHelp",
143 "f9": "editor::ToggleBreakpoint",144 "f9": "editor::ToggleBreakpoint",
144 "shift-f9": "editor::EditLogBreakpoint",145 "shift-f9": "editor::EditLogBreakpoint",
assets/keymaps/vim.json+7−1
@@ -184,6 +184,8 @@ · 183 unmodified lines
184 "z f": "editor::FoldSelectedRanges",184 "z f": "editor::FoldSelectedRanges",
185 "z shift-m": "editor::FoldAll",185 "z shift-m": "editor::FoldAll",
186 "z shift-r": "editor::UnfoldAll",186 "z shift-r": "editor::UnfoldAll",
187 "z l": "vim::ColumnRight",
188 "z h": "vim::ColumnLeft",
187 "shift-z shift-q": ["pane::CloseActiveItem", { "save_intent": "skip" }],189 "shift-z shift-q": ["pane::CloseActiveItem", { "save_intent": "skip" }],
188 "shift-z shift-z": ["pane::CloseActiveItem", { "save_intent": "save_all" }],190 "shift-z shift-z": ["pane::CloseActiveItem", { "save_intent": "save_all" }],
189 // Count support191 // Count support
@@ -395,6 +397,8 @@ · 205 unmodified lines
395 "ctrl-pagedown": "pane::ActivateNextItem",397 "ctrl-pagedown": "pane::ActivateNextItem",
396 "ctrl-pageup": "pane::ActivatePreviousItem",398 "ctrl-pageup": "pane::ActivatePreviousItem",
397 "insert": "vim::InsertBefore",399 "insert": "vim::InsertBefore",
400 ".": "vim::Repeat",
401 "alt-.": "vim::RepeatFind",
398 // tree-sitter related commands402 // tree-sitter related commands
399 "[ x": "editor::SelectLargerSyntaxNode",403 "[ x": "editor::SelectLargerSyntaxNode",
400 "] x": "editor::SelectSmallerSyntaxNode",404 "] x": "editor::SelectSmallerSyntaxNode",
@@ -421,6 +425,7 @@ · 20 unmodified lines
421 425
422 "x": "editor::SelectLine",426 "x": "editor::SelectLine",
423 "shift-x": "editor::SelectLine",427 "shift-x": "editor::SelectLine",
428 "%": "editor::SelectAll",
424 // Window mode429 // Window mode
425 "space w h": "workspace::ActivatePaneLeft",430 "space w h": "workspace::ActivatePaneLeft",
426 "space w l": "workspace::ActivatePaneRight",431 "space w l": "workspace::ActivatePaneRight",
@@ -450,7 +455,8 @@ · 23 unmodified lines
450 "ctrl-c": "editor::ToggleComments",455 "ctrl-c": "editor::ToggleComments",
451 "d": "vim::HelixDelete",456 "d": "vim::HelixDelete",
452 "c": "vim::Substitute",457 "c": "vim::Substitute",
453 "shift-c": "editor::AddSelectionBelow"458 "shift-c": "editor::AddSelectionBelow",
459 "alt-shift-c": "editor::AddSelectionAbove"
454 }460 }
455 },461 },
456 {462 {
assets/prompts/assistant_system_prompt.hbs+2−2
@@ -27,11 +27,11 @@ If you are unsure how to fulfill the user's request, gather more information wit · 26 unmodified lines
27If appropriate, use tool calls to explore the current project, which contains the following root directories:27If appropriate, use tool calls to explore the current project, which contains the following root directories:
28 28
29{{#each worktrees}}29{{#each worktrees}}
30- `{{root_name}}`30- `{{abs_path}}`
31{{/each}}31{{/each}}
32 32
33- Bias towards not asking the user for help if you can find the answer yourself.33- Bias towards not asking the user for help if you can find the answer yourself.
34- When providing paths to tools, the path should always begin with a path that starts with a project root directory listed above.34- When providing paths to tools, the path should always start with the name of a project root directory listed above.
35- Before you read or edit a file, you must first find the full path. DO NOT ever guess a file path!35- Before you read or edit a file, you must first find the full path. DO NOT ever guess a file path!
36{{# if (has_tool 'grep') }}36{{# if (has_tool 'grep') }}
37- When looking for symbols in the project, prefer the `grep` tool.37- When looking for symbols in the project, prefer the `grep` tool.
assets/settings/default.json+16−3
@@ -307,6 +307,8 @@ · 306 unmodified lines
307 // "all"307 // "all"
308 // 4. Draw whitespaces at boundaries only:308 // 4. Draw whitespaces at boundaries only:
309 // "boundary"309 // "boundary"
310 // 5. Draw whitespaces only after non-whitespace characters:
311 // "trailing"
310 // For a whitespace to be on a boundary, any of the following conditions need to be met:312 // For a whitespace to be on a boundary, any of the following conditions need to be met:
311 // - It is a tab313 // - It is a tab
312 // - It is adjacent to an edge (start or end)314 // - It is adjacent to an edge (start or end)
@@ -445,7 +447,9 @@ · 132 unmodified lines
445 // Whether to show breakpoints in the gutter.447 // Whether to show breakpoints in the gutter.
446 "breakpoints": true,448 "breakpoints": true,
447 // Whether to show fold buttons in the gutter.449 // Whether to show fold buttons in the gutter.
448 "folds": true450 "folds": true,
451 // Minimum number of characters to reserve space for in the gutter.
452 "min_line_number_digits": 4
449 },453 },
450 "indent_guides": {454 "indent_guides": {
451 // Whether to show indent guides in the editor.455 // Whether to show indent guides in the editor.
@@ -604,7 +608,9 @@ · 152 unmodified lines
604 // 2. Never show indent guides:608 // 2. Never show indent guides:
605 // "never"609 // "never"
606 "show": "always"610 "show": "always"
607 }611 },
612 // Whether to hide the root entry when only one folder is open in the window.
613 "hide_root": false
608 },614 },
609 "outline_panel": {615 "outline_panel": {
610 // Whether to show the outline panel button in the status bar616 // Whether to show the outline panel button in the status bar
@@ -1476,7 +1482,8 @@ · 865 unmodified lines
1476 "Go": {1482 "Go": {
1477 "code_actions_on_format": {1483 "code_actions_on_format": {
1478 "source.organizeImports": true1484 "source.organizeImports": true
1479 }1485 },
1486 "debuggers": ["Delve"]
1480 },1487 },
1481 "GraphQL": {1488 "GraphQL": {
1482 "prettier": {1489 "prettier": {
@@ -1541,9 +1548,15 @@ · 58 unmodified lines
1541 "Plain Text": {1548 "Plain Text": {
1542 "allow_rewrap": "anywhere"1549 "allow_rewrap": "anywhere"
1543 },1550 },
1551 "Python": {
1552 "debuggers": ["Debugpy"]
1553 },
1544 "Ruby": {1554 "Ruby": {
1545 "language_servers": ["solargraph", "!ruby-lsp", "!rubocop", "!sorbet", "!steep", "..."]1555 "language_servers": ["solargraph", "!ruby-lsp", "!rubocop", "!sorbet", "!steep", "..."]
1546 },1556 },
1557 "Rust": {
1558 "debuggers": ["CodeLLDB"]
1559 },
1547 "SCSS": {1560 "SCSS": {
1548 "prettier": {1561 "prettier": {
1549 "allowed": true1562 "allowed": true
assets/settings/initial_tasks.json+1−1
@@ -1,4 +1,4 @@
1// Static tasks configuration.1// Project tasks configuration. See https://zed.dev/docs/tasks for documentation.
2//2//
3// Example:3// Example:
4[4[
assets/themes/one/one.json+2−0
@@ -99,6 +99,8 @@ · 98 unmodified lines
99 "version_control.added": "#27a657ff",99 "version_control.added": "#27a657ff",
100 "version_control.modified": "#d3b020ff",100 "version_control.modified": "#d3b020ff",
101 "version_control.deleted": "#e06c76ff",101 "version_control.deleted": "#e06c76ff",
102 "version_control.conflict_marker.ours": "#a1c1811a",
103 "version_control.conflict_marker.theirs": "#74ade81a",
102 "conflict": "#dec184ff",104 "conflict": "#dec184ff",
103 "conflict.background": "#dec1841a",105 "conflict.background": "#dec1841a",
104 "conflict.border": "#5d4c2fff",106 "conflict.border": "#5d4c2fff",
crates/activity_indicator/src/activity_indicator.rs+166−66
@@ -7,7 +7,10 @@ use gpui::{ · 6 unmodified lines
7 InteractiveElement as _, ParentElement as _, Render, SharedString, StatefulInteractiveElement,7 InteractiveElement as _, ParentElement as _, Render, SharedString, StatefulInteractiveElement,
8 Styled, Transformation, Window, actions, percentage,8 Styled, Transformation, Window, actions, percentage,
9};9};
10use language::{BinaryStatus, LanguageRegistry, LanguageServerId};10use language::{
11 BinaryStatus, LanguageRegistry, LanguageServerId, LanguageServerName,
12 LanguageServerStatusUpdate, ServerHealth,
13};
11use project::{14use project::{
12 EnvironmentErrorMessage, LanguageServerProgress, LspStoreEvent, Project,15 EnvironmentErrorMessage, LanguageServerProgress, LspStoreEvent, Project,
13 ProjectEnvironmentEvent,16 ProjectEnvironmentEvent,
@@ -16,6 +19,7 @@ use project::{ · 2 unmodified lines
16use smallvec::SmallVec;19use smallvec::SmallVec;
17use std::{20use std::{
18 cmp::Reverse,21 cmp::Reverse,
22 collections::HashSet,
19 fmt::Write,23 fmt::Write,
20 path::Path,24 path::Path,
21 sync::Arc,25 sync::Arc,
@@ -30,9 +34,9 @@ const GIT_OPERATION_DELAY: Duration = Duration::from_millis(0); · 8 unmodified lines
30actions!(activity_indicator, [ShowErrorMessage]);34actions!(activity_indicator, [ShowErrorMessage]);
31 35
32pub enum Event {36pub enum Event {
33 ShowError {37 ShowStatus {
34 server_name: SharedString,38 server_name: LanguageServerName,
35 error: String,39 status: SharedString,
36 },40 },
37}41}
38 42
@@ -45,8 +49,8 @@ pub struct ActivityIndicator { · 6 unmodified lines
45 49
46#[derive(Debug)]50#[derive(Debug)]
47struct ServerStatus {51struct ServerStatus {
48 name: SharedString,52 name: LanguageServerName,
49 status: BinaryStatus,53 status: LanguageServerStatusUpdate,
50}54}
51 55
52struct PendingWork<'a> {56struct PendingWork<'a> {
@@ -145,19 +149,19 @@ impl ActivityIndicator { · 92 unmodified lines
145 });149 });
146 150
147 cx.subscribe_in(&this, window, move |_, _, event, window, cx| match event {151 cx.subscribe_in(&this, window, move |_, _, event, window, cx| match event {
148 Event::ShowError { server_name, error } => {152 Event::ShowStatus {
153 server_name,
154 status,
155 } => {
149 let create_buffer = project.update(cx, |project, cx| project.create_buffer(cx));156 let create_buffer = project.update(cx, |project, cx| project.create_buffer(cx));
150 let project = project.clone();157 let project = project.clone();
151 let error = error.clone();158 let status = status.clone();
152 let server_name = server_name.clone();159 let server_name = server_name.clone();
153 cx.spawn_in(window, async move |workspace, cx| {160 cx.spawn_in(window, async move |workspace, cx| {
154 let buffer = create_buffer.await?;161 let buffer = create_buffer.await?;
155 buffer.update(cx, |buffer, cx| {162 buffer.update(cx, |buffer, cx| {
156 buffer.edit(163 buffer.edit(
157 [(164 [(0..0, format!("Language server {server_name}:\n\n{status}"))],
158 0..0,
159 format!("Language server error: {}\n\n{}", server_name, error),
160 )],
161 None,165 None,
162 cx,166 cx,
163 );167 );
@@ -166,7 +170,10 @@ impl ActivityIndicator { · 2 unmodified lines
166 workspace.update_in(cx, |workspace, window, cx| {170 workspace.update_in(cx, |workspace, window, cx| {
167 workspace.add_item_to_active_pane(171 workspace.add_item_to_active_pane(
168 Box::new(cx.new(|cx| {172 Box::new(cx.new(|cx| {
169 Editor::for_buffer(buffer, Some(project.clone()), window, cx)173 let mut editor =
174 Editor::for_buffer(buffer, Some(project.clone()), window, cx);
175 editor.set_read_only(true);
176 editor
170 })),177 })),
171 None,178 None,
172 true,179 true,
@@ -185,19 +192,34 @@ impl ActivityIndicator { · 12 unmodified lines
185 }192 }
186 193
187 fn show_error_message(&mut self, _: &ShowErrorMessage, _: &mut Window, cx: &mut Context<Self>) {194 fn show_error_message(&mut self, _: &ShowErrorMessage, _: &mut Window, cx: &mut Context<Self>) {
188 self.statuses.retain(|status| {195 let mut status_message_shown = false;
189 if let BinaryStatus::Failed { error } = &status.status {196 self.statuses.retain(|status| match &status.status {
190 cx.emit(Event::ShowError {197 LanguageServerStatusUpdate::Binary(BinaryStatus::Failed { error })
198 if !status_message_shown =>
199 {
200 cx.emit(Event::ShowStatus {
191 server_name: status.name.clone(),201 server_name: status.name.clone(),
192 error: error.clone(),202 status: SharedString::from(error),
193 });203 });
204 status_message_shown = true;
194 false205 false
195 } else {
196 true
197 }206 }
207 LanguageServerStatusUpdate::Health(
208 ServerHealth::Error | ServerHealth::Warning,
209 status_string,
210 ) if !status_message_shown => match status_string {
211 Some(error) => {
212 cx.emit(Event::ShowStatus {
213 server_name: status.name.clone(),
214 status: error.clone(),
215 });
216 status_message_shown = true;
217 false
218 }
219 None => false,
220 },
221 _ => true,
198 });222 });
199
200 cx.notify();
201 }223 }
202 224
203 fn dismiss_error_message(225 fn dismiss_error_message(
@@ -267,48 +289,52 @@ impl ActivityIndicator { · 63 unmodified lines
267 });289 });
268 }290 }
269 // Show any language server has pending activity.291 // Show any language server has pending activity.
270 let mut pending_work = self.pending_language_server_work(cx);
271 if let Some(PendingWork {
272 progress_token,
273 progress,
274 ..
275 }) = pending_work.next()
276 {292 {
277 let mut message = progress293 let mut pending_work = self.pending_language_server_work(cx);
278 .title294 if let Some(PendingWork {
279 .as_deref()295 progress_token,
280 .unwrap_or(progress_token)296 progress,
281 .to_string();297 ..
282 298 }) = pending_work.next()
283 if let Some(percentage) = progress.percentage {299 {
284 write!(&mut message, " ({}%)", percentage).unwrap();300 let mut message = progress
285 }301 .title
302 .as_deref()
303 .unwrap_or(progress_token)
304 .to_string();
305
306 if let Some(percentage) = progress.percentage {
307 write!(&mut message, " ({}%)", percentage).unwrap();
308 }
286 309
287 if let Some(progress_message) = progress.message.as_ref() {310 if let Some(progress_message) = progress.message.as_ref() {
288 message.push_str(": ");311 message.push_str(": ");
289 message.push_str(progress_message);312 message.push_str(progress_message);
290 }313 }
291 314
292 let additional_work_count = pending_work.count();315 let additional_work_count = pending_work.count();
293 if additional_work_count > 0 {316 if additional_work_count > 0 {
294 write!(&mut message, " + {} more", additional_work_count).unwrap();317 write!(&mut message, " + {} more", additional_work_count).unwrap();
295 }318 }
296 319
297 return Some(Content {320 return Some(Content {
298 icon: Some(321 icon: Some(
299 Icon::new(IconName::ArrowCircle)322 Icon::new(IconName::ArrowCircle)
300 .size(IconSize::Small)323 .size(IconSize::Small)
301 .with_animation(324 .with_animation(
302 "arrow-circle",325 "arrow-circle",
303 Animation::new(Duration::from_secs(2)).repeat(),326 Animation::new(Duration::from_secs(2)).repeat(),
304 |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),327 |icon, delta| {
305 )328 icon.transform(Transformation::rotate(percentage(delta)))
306 .into_any_element(),329 },
307 ),330 )
308 message,331 .into_any_element(),
309 on_click: Some(Arc::new(Self::toggle_language_server_work_context_menu)),332 ),
310 tooltip_message: None,333 message,
311 });334 on_click: Some(Arc::new(Self::toggle_language_server_work_context_menu)),
335 tooltip_message: None,
336 });
337 }
312 }338 }
313 339
314 if let Some(session) = self340 if let Some(session) = self
@@ -369,14 +395,38 @@ impl ActivityIndicator { · 54 unmodified lines
369 let mut downloading = SmallVec::<[_; 3]>::new();395 let mut downloading = SmallVec::<[_; 3]>::new();
370 let mut checking_for_update = SmallVec::<[_; 3]>::new();396 let mut checking_for_update = SmallVec::<[_; 3]>::new();
371 let mut failed = SmallVec::<[_; 3]>::new();397 let mut failed = SmallVec::<[_; 3]>::new();
398 let mut health_messages = SmallVec::<[_; 3]>::new();
399 let mut servers_to_clear_statuses = HashSet::<LanguageServerName>::default();
372 for status in &self.statuses {400 for status in &self.statuses {
373 match status.status {401 match &status.status {
374 BinaryStatus::CheckingForUpdate => checking_for_update.push(status.name.clone()),402 LanguageServerStatusUpdate::Binary(BinaryStatus::CheckingForUpdate) => {
375 BinaryStatus::Downloading => downloading.push(status.name.clone()),403 checking_for_update.push(status.name.clone());
376 BinaryStatus::Failed { .. } => failed.push(status.name.clone()),404 }
377 BinaryStatus::None => {}405 LanguageServerStatusUpdate::Binary(BinaryStatus::Downloading) => {
406 downloading.push(status.name.clone());
407 }
408 LanguageServerStatusUpdate::Binary(BinaryStatus::Failed { .. }) => {
409 failed.push(status.name.clone());
410 }
411 LanguageServerStatusUpdate::Binary(BinaryStatus::None) => {}
412 LanguageServerStatusUpdate::Health(health, server_status) => match server_status {
413 Some(server_status) => {
414 health_messages.push((status.name.clone(), *health, server_status.clone()));
415 }
416 None => {
417 servers_to_clear_statuses.insert(status.name.clone());
418 }
419 },
378 }420 }
379 }421 }
422 self.statuses
423 .retain(|status| !servers_to_clear_statuses.contains(&status.name));
424
425 health_messages.sort_by_key(|(_, health, _)| match health {
426 ServerHealth::Error => 2,
427 ServerHealth::Warning => 1,
428 ServerHealth::Ok => 0,
429 });
380 430
381 if !downloading.is_empty() {431 if !downloading.is_empty() {
382 return Some(Content {432 return Some(Content {
@@ -457,7 +507,7 @@ impl ActivityIndicator { · 74 unmodified lines
457 }),507 }),
458 ),508 ),
459 on_click: Some(Arc::new(|this, window, cx| {509 on_click: Some(Arc::new(|this, window, cx| {
460 this.show_error_message(&Default::default(), window, cx)510 this.show_error_message(&ShowErrorMessage, window, cx)
461 })),511 })),
462 tooltip_message: None,512 tooltip_message: None,
463 });513 });
@@ -471,7 +521,7 @@ impl ActivityIndicator { · 7 unmodified lines
471 .size(IconSize::Small)521 .size(IconSize::Small)
472 .into_any_element(),522 .into_any_element(),
473 ),523 ),
474 message: format!("Formatting failed: {}. Click to see logs.", failure),524 message: format!("Formatting failed: {failure}. Click to see logs."),
475 on_click: Some(Arc::new(|indicator, window, cx| {525 on_click: Some(Arc::new(|indicator, window, cx| {
476 indicator.project.update(cx, |project, cx| {526 indicator.project.update(cx, |project, cx| {
477 project.reset_last_formatting_failure(cx);527 project.reset_last_formatting_failure(cx);
@@ -482,6 +532,56 @@ impl ActivityIndicator { · 4 unmodified lines
482 });532 });
483 }533 }
484 534
535 // Show any health messages for the language servers
536 if let Some((server_name, health, message)) = health_messages.pop() {
537 let health_str = match health {
538 ServerHealth::Ok => format!("({server_name}) "),
539 ServerHealth::Warning => format!("({server_name}) Warning: "),
540 ServerHealth::Error => format!("({server_name}) Error: "),
541 };
542 let single_line_message = message
543 .lines()
544 .filter_map(|line| {
545 let line = line.trim();
546 if line.is_empty() { None } else { Some(line) }
547 })
548 .collect::<Vec<_>>()
549 .join(" ");
550 let mut altered_message = single_line_message != message;
551 let truncated_message = truncate_and_trailoff(
552 &single_line_message,
553 MAX_MESSAGE_LEN.saturating_sub(health_str.len()),
554 );
555 altered_message |= truncated_message != single_line_message;
556 let final_message = format!("{health_str}{truncated_message}");
557
558 let tooltip_message = if altered_message {
559 Some(format!("{health_str}{message}"))
560 } else {
561 None
562 };
563
564 return Some(Content {
565 icon: Some(
566 Icon::new(IconName::Warning)
567 .size(IconSize::Small)
568 .into_any_element(),
569 ),
570 message: final_message,
571 tooltip_message,
572 on_click: Some(Arc::new(move |activity_indicator, window, cx| {
573 if altered_message {
574 activity_indicator.show_error_message(&ShowErrorMessage, window, cx)
575 } else {
576 activity_indicator
577 .statuses
578 .retain(|status| status.name != server_name);
579 cx.notify();
580 }
581 })),
582 });
583 }
584
485 // Show any application auto-update info.585 // Show any application auto-update info.
486 if let Some(updater) = &self.auto_updater {586 if let Some(updater) = &self.auto_updater {
487 return match &updater.read(cx).status() {587 return match &updater.read(cx).status() {
crates/agent/Cargo.toml+1−0
@@ -109,5 +109,6 @@ gpui = { workspace = true, "features" = ["test-support"] } · 108 unmodified lines
109indoc.workspace = true109indoc.workspace = true
110language = { workspace = true, "features" = ["test-support"] }110language = { workspace = true, "features" = ["test-support"] }
111language_model = { workspace = true, "features" = ["test-support"] }111language_model = { workspace = true, "features" = ["test-support"] }
112pretty_assertions.workspace = true
112project = { workspace = true, features = ["test-support"] }113project = { workspace = true, features = ["test-support"] }
113rand.workspace = true114rand.workspace = true
crates/agent/src/active_thread.rs+136−26
@@ -1605,6 +1605,7 @@ impl ActiveThread { · 1604 unmodified lines
1605 1605
1606 this.thread.update(cx, |thread, cx| {1606 this.thread.update(cx, |thread, cx| {
1607 thread.advance_prompt_id();1607 thread.advance_prompt_id();
1608 thread.cancel_last_completion(Some(window.window_handle()), cx);
1608 thread.send_to_model(1609 thread.send_to_model(
1609 model.model,1610 model.model,
1610 CompletionIntent::UserPrompt,1611 CompletionIntent::UserPrompt,
@@ -1680,7 +1681,10 @@ impl ActiveThread { · 69 unmodified lines
1680 1681
1681 let editor = cx.new(|cx| {1682 let editor = cx.new(|cx| {
1682 let mut editor = Editor::new(1683 let mut editor = Editor::new(
1683 editor::EditorMode::AutoHeight { max_lines: 4 },1684 editor::EditorMode::AutoHeight {
1685 min_lines: 1,
1686 max_lines: 4,
1687 },
1684 buffer,1688 buffer,
1685 None,1689 None,
1686 window,1690 window,
@@ -1788,12 +1792,31 @@ impl ActiveThread { · 101 unmodified lines
1788 1792
1789 fn render_message(&self, ix: usize, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {1793 fn render_message(&self, ix: usize, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
1790 let message_id = self.messages[ix];1794 let message_id = self.messages[ix];
1791 let Some(message) = self.thread.read(cx).message(message_id) else {1795 let workspace = self.workspace.clone();
1796 let thread = self.thread.read(cx);
1797
1798 let is_first_message = ix == 0;
1799 let is_last_message = ix == self.messages.len() - 1;
1800
1801 let Some(message) = thread.message(message_id) else {
1792 return Empty.into_any();1802 return Empty.into_any();
1793 };1803 };
1794 1804
1805 let is_generating = thread.is_generating();
1806 let is_generating_stale = thread.is_generation_stale().unwrap_or(false);
1807
1808 let loading_dots = (is_generating && is_last_message).then(|| {
1809 h_flex()
1810 .h_8()
1811 .my_3()
1812 .mx_5()
1813 .when(is_generating_stale || message.is_hidden, |this| {
1814 this.child(AnimatedLabel::new("").size(LabelSize::Small))
1815 })
1816 });
1817
1795 if message.is_hidden {1818 if message.is_hidden {
1796 return Empty.into_any();1819 return div().children(loading_dots).into_any();
1797 }1820 }
1798 1821
1799 let message_creases = message.creases.clone();1822 let message_creases = message.creases.clone();
@@ -1802,9 +1825,6 @@ impl ActiveThread { · 2 unmodified lines
1802 return Empty.into_any();1825 return Empty.into_any();
1803 };1826 };
1804 1827
1805 let workspace = self.workspace.clone();
1806 let thread = self.thread.read(cx);
1807
1808 // Get all the data we need from thread before we start using it in closures1828 // Get all the data we need from thread before we start using it in closures
1809 let checkpoint = thread.checkpoint_for_message(message_id);1829 let checkpoint = thread.checkpoint_for_message(message_id);
1810 let configured_model = thread.configured_model().map(|m| m.model);1830 let configured_model = thread.configured_model().map(|m| m.model);
@@ -1815,14 +1835,6 @@ impl ActiveThread { · 4 unmodified lines
1815 1835
1816 let tool_uses = thread.tool_uses_for_message(message_id, cx);1836 let tool_uses = thread.tool_uses_for_message(message_id, cx);
1817 let has_tool_uses = !tool_uses.is_empty();1837 let has_tool_uses = !tool_uses.is_empty();
1818 let is_generating = thread.is_generating();
1819 let is_generating_stale = thread.is_generation_stale().unwrap_or(false);
1820
1821 let is_first_message = ix == 0;
1822 let is_last_message = ix == self.messages.len() - 1;
1823
1824 let loading_dots = (is_generating_stale && is_last_message)
1825 .then(|| AnimatedLabel::new("").size(LabelSize::Small));
1826 1838
1827 let editing_message_state = self1839 let editing_message_state = self
1828 .editing_message1840 .editing_message
@@ -2238,17 +2250,7 @@ impl ActiveThread { · 409 unmodified lines
2238 parent.child(self.render_rules_item(cx))2250 parent.child(self.render_rules_item(cx))
2239 })2251 })
2240 .child(styled_message)2252 .child(styled_message)
2241 .when(is_generating && is_last_message, |this| {2253 .children(loading_dots)
2242 this.child(
2243 h_flex()
2244 .h_8()
2245 .mt_2()
2246 .mb_4()
2247 .ml_4()
2248 .py_1p5()
2249 .when_some(loading_dots, |this, loading_dots| this.child(loading_dots)),
2250 )
2251 })
2252 .when(show_feedback, move |parent| {2254 .when(show_feedback, move |parent| {
2253 parent.child(feedback_items).when_some(2255 parent.child(feedback_items).when_some(
2254 self.open_feedback_editors.get(&message_id),2256 self.open_feedback_editors.get(&message_id),
@@ -3708,7 +3710,7 @@ mod tests { · 1453 unmodified lines
3708 use util::path;3710 use util::path;
3709 use workspace::CollaboratorId;3711 use workspace::CollaboratorId;
3710 3712
3711 use crate::{ContextLoadResult, thread_store};3713 use crate::{ContextLoadResult, thread::MessageSegment, thread_store};
3712 3714
3713 use super::*;3715 use super::*;
3714 3716
@@ -3842,6 +3844,114 @@ mod tests { · 127 unmodified lines
3842 });3844 });
3843 }3845 }
3844 3846
3847 #[gpui::test]
3848 async fn test_editing_message_cancels_previous_completion(cx: &mut TestAppContext) {
3849 init_test_settings(cx);
3850
3851 let project = create_test_project(cx, json!({})).await;
3852
3853 let (cx, active_thread, _, thread, model) =
3854 setup_test_environment(cx, project.clone()).await;
3855
3856 cx.update(|_, cx| {
3857 LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
3858 registry.set_default_model(
3859 Some(ConfiguredModel {
3860 provider: Arc::new(FakeLanguageModelProvider),
3861 model: model.clone(),
3862 }),
3863 cx,
3864 );
3865 });
3866 });
3867
3868 // Track thread events to verify cancellation
3869 let cancellation_events = Arc::new(std::sync::Mutex::new(Vec::new()));
3870 let new_request_events = Arc::new(std::sync::Mutex::new(Vec::new()));
3871
3872 let _subscription = cx.update(|_, cx| {
3873 let cancellation_events = cancellation_events.clone();
3874 let new_request_events = new_request_events.clone();
3875 cx.subscribe(
3876 &thread,
3877 move |_thread, event: &ThreadEvent, _cx| match event {
3878 ThreadEvent::CompletionCanceled => {
3879 cancellation_events.lock().unwrap().push(());
3880 }
3881 ThreadEvent::NewRequest => {
3882 new_request_events.lock().unwrap().push(());
3883 }
3884 _ => {}
3885 },
3886 )
3887 });
3888
3889 // Insert a user message and start streaming a response
3890 let message = thread.update(cx, |thread, cx| {
3891 let message_id = thread.insert_user_message(
3892 "Hello, how are you?",
3893 ContextLoadResult::default(),
3894 None,
3895 vec![],
3896 cx,
3897 );
3898 thread.advance_prompt_id();
3899 thread.send_to_model(
3900 model.clone(),
3901 CompletionIntent::UserPrompt,
3902 cx.active_window(),
3903 cx,
3904 );
3905 thread.message(message_id).cloned().unwrap()
3906 });
3907
3908 cx.run_until_parked();
3909
3910 // Verify that a completion is in progress
3911 assert!(cx.read(|cx| thread.read(cx).is_generating()));
3912 assert_eq!(new_request_events.lock().unwrap().len(), 1);
3913
3914 // Edit the message while the completion is still running
3915 active_thread.update_in(cx, |active_thread, window, cx| {
3916 active_thread.start_editing_message(
3917 message.id,
3918 message.segments.as_slice(),
3919 message.creases.as_slice(),
3920 window,
3921 cx,
3922 );
3923 let editor = active_thread
3924 .editing_message
3925 .as_ref()
3926 .unwrap()
3927 .1
3928 .editor
3929 .clone();
3930 editor.update(cx, |editor, cx| {
3931 editor.set_text("What is the weather like?", window, cx);
3932 });
3933 active_thread.confirm_editing_message(&Default::default(), window, cx);
3934 });
3935
3936 cx.run_until_parked();
3937
3938 // Verify that the previous completion was cancelled
3939 assert_eq!(cancellation_events.lock().unwrap().len(), 1);
3940
3941 // Verify that a new request was started after cancellation
3942 assert_eq!(new_request_events.lock().unwrap().len(), 2);
3943
3944 // Verify that the edited message contains the new text
3945 let edited_message =
3946 thread.update(cx, |thread, _| thread.message(message.id).cloned().unwrap());
3947 match &edited_message.segments[0] {
3948 MessageSegment::Text(text) => {
3949 assert_eq!(text, "What is the weather like?");
3950 }
3951 _ => panic!("Expected text segment"),
3952 }
3953 }
3954
3845 fn init_test_settings(cx: &mut TestAppContext) {3955 fn init_test_settings(cx: &mut TestAppContext) {
3846 cx.update(|cx| {3956 cx.update(|cx| {
3847 let settings_store = SettingsStore::test(cx);3957 let settings_store = SettingsStore::test(cx);
crates/agent/src/agent.rs+1−1
@@ -162,7 +162,7 @@ pub fn init( · 161 unmodified lines
162 assistant_slash_command::init(cx);162 assistant_slash_command::init(cx);
163 thread_store::init(cx);163 thread_store::init(cx);
164 agent_panel::init(cx);164 agent_panel::init(cx);
165 context_server_configuration::init(language_registry, cx);165 context_server_configuration::init(language_registry, fs.clone(), cx);
166 166
167 register_slash_commands(cx);167 register_slash_commands(cx);
168 inline_assistant::init(168 inline_assistant::init(
crates/agent/src/agent_configuration.rs+15−15
@@ -12,7 +12,7 @@ use context_server::ContextServerId; · 11 unmodified lines
12use fs::Fs;12use fs::Fs;
13use gpui::{13use gpui::{
14 Action, Animation, AnimationExt as _, AnyView, App, Entity, EventEmitter, FocusHandle,14 Action, Animation, AnimationExt as _, AnyView, App, Entity, EventEmitter, FocusHandle,
15 Focusable, ScrollHandle, Subscription, pulsating_between,15 Focusable, ScrollHandle, Subscription, Transformation, percentage,
16};16};
17use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry};17use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry};
18use project::context_server_store::{ContextServerStatus, ContextServerStore};18use project::context_server_store::{ContextServerStatus, ContextServerStore};
@@ -475,7 +475,6 @@ impl AgentConfiguration { · 456 unmodified lines
475 .get(&context_server_id)475 .get(&context_server_id)
476 .copied()476 .copied()
477 .unwrap_or_default();477 .unwrap_or_default();
478
479 let tools = tools_by_source478 let tools = tools_by_source
480 .get(&ToolSource::ContextServer {479 .get(&ToolSource::ContextServer {
481 id: context_server_id.0.clone().into(),480 id: context_server_id.0.clone().into(),
@@ -484,25 +483,23 @@ impl AgentConfiguration { · 2 unmodified lines
484 let tool_count = tools.len();483 let tool_count = tools.len();
485 484
486 let border_color = cx.theme().colors().border.opacity(0.6);485 let border_color = cx.theme().colors().border.opacity(0.6);
487 let success_color = Color::Success.color(cx);
488 486
489 let (status_indicator, tooltip_text) = match server_status {487 let (status_indicator, tooltip_text) = match server_status {
490 ContextServerStatus::Starting => (488 ContextServerStatus::Starting => (
491 Indicator::dot()489 Icon::new(IconName::LoadCircle)
492 .color(Color::Success)490 .size(IconSize::XSmall)
491 .color(Color::Accent)
493 .with_animation(492 .with_animation(
494 SharedString::from(format!("{}-starting", context_server_id.0.clone(),)),493 SharedString::from(format!("{}-starting", context_server_id.0.clone(),)),
495 Animation::new(Duration::from_secs(2))494 Animation::new(Duration::from_secs(3)).repeat(),
496 .repeat()495 |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
497 .with_easing(pulsating_between(0.4, 1.)),
498 move |this, delta| this.color(success_color.alpha(delta).into()),
499 )496 )
500 .into_any_element(),497 .into_any_element(),
501 "Server is starting.",498 "Server is starting.",
502 ),499 ),
503 ContextServerStatus::Running => (500 ContextServerStatus::Running => (
504 Indicator::dot().color(Color::Success).into_any_element(),501 Indicator::dot().color(Color::Success).into_any_element(),
505 "Server is running.",502 "Server is active.",
506 ),503 ),
507 ContextServerStatus::Error(_) => (504 ContextServerStatus::Error(_) => (
508 Indicator::dot().color(Color::Error).into_any_element(),505 Indicator::dot().color(Color::Error).into_any_element(),
@@ -526,12 +523,11 @@ impl AgentConfiguration { · 17 unmodified lines
526 .p_1()523 .p_1()
527 .justify_between()524 .justify_between()
528 .when(525 .when(
529 error.is_some() || are_tools_expanded && tool_count > 1,526 error.is_some() || are_tools_expanded && tool_count >= 1,
530 |element| element.border_b_1().border_color(border_color),527 |element| element.border_b_1().border_color(border_color),
531 )528 )
532 .child(529 .child(
533 h_flex()530 h_flex()
534 .gap_1p5()
535 .child(531 .child(
536 Disclosure::new(532 Disclosure::new(
537 "tool-list-disclosure",533 "tool-list-disclosure",
@@ -551,12 +547,16 @@ impl AgentConfiguration { · 13 unmodified lines
551 })),547 })),
552 )548 )
553 .child(549 .child(
554 div()550 h_flex()
555 .id(item_id.clone())551 .id(SharedString::from(format!("tooltip-{}", item_id)))
552 .h_full()
553 .w_3()
554 .mx_1()
555 .justify_center()
556 .tooltip(Tooltip::text(tooltip_text))556 .tooltip(Tooltip::text(tooltip_text))
557 .child(status_indicator),557 .child(status_indicator),
558 )558 )
559 .child(Label::new(context_server_id.0.clone()).ml_0p5())559 .child(Label::new(item_id).ml_0p5().mr_1p5())
560 .when(is_running, |this| {560 .when(is_running, |this| {
561 this.child(561 this.child(
562 Label::new(if tool_count == 1 {562 Label::new(if tool_count == 1 {
crates/agent/src/agent_configuration/configure_context_server_modal.rs+1−1
@@ -89,7 +89,7 @@ impl ConfigureContextServerModal { · 88 unmodified lines
89 }),89 }),
90 settings_validator,90 settings_validator,
91 settings_editor: cx.new(|cx| {91 settings_editor: cx.new(|cx| {
92 let mut editor = Editor::auto_height(16, window, cx);92 let mut editor = Editor::auto_height(1, 16, window, cx);
93 editor.set_text(config.default_settings.trim(), window, cx);93 editor.set_text(config.default_settings.trim(), window, cx);
94 editor.set_show_gutter(false, cx);94 editor.set_show_gutter(false, cx);
95 editor.set_soft_wrap_mode(95 editor.set_soft_wrap_mode(
crates/agent/src/agent_diff.rs+4−4
@@ -31,7 +31,7 @@ use util::ResultExt; · 30 unmodified lines
31use workspace::{31use workspace::{
32 Item, ItemHandle, ItemNavHistory, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,32 Item, ItemHandle, ItemNavHistory, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
33 Workspace,33 Workspace,
34 item::{BreadcrumbText, ItemEvent, TabContentParams},34 item::{BreadcrumbText, ItemEvent, SaveOptions, TabContentParams},
35 searchable::SearchableItemHandle,35 searchable::SearchableItemHandle,
36};36};
37use zed_actions::assistant::ToggleFocus;37use zed_actions::assistant::ToggleFocus;
@@ -532,12 +532,12 @@ impl Item for AgentDiffPane { · 494 unmodified lines
532 532
533 fn save(533 fn save(
534 &mut self,534 &mut self,
535 format: bool,535 options: SaveOptions,
536 project: Entity<Project>,536 project: Entity<Project>,
537 window: &mut Window,537 window: &mut Window,
538 cx: &mut Context<Self>,538 cx: &mut Context<Self>,
539 ) -> Task<Result<()>> {539 ) -> Task<Result<()>> {
540 self.editor.save(format, project, window, cx)540 self.editor.save(options, project, window, cx)
541 }541 }
542 542
543 fn save_as(543 fn save_as(
@@ -1513,7 +1513,7 @@ impl AgentDiff { · 969 unmodified lines
1513 multibuffer.add_diff(diff_handle.clone(), cx);1513 multibuffer.add_diff(diff_handle.clone(), cx);
1514 });1514 });
1515 1515
1516 let new_state = if thread.read(cx).has_pending_edit_tool_uses() {1516 let new_state = if thread.read(cx).is_generating() {
1517 EditorState::Generating1517 EditorState::Generating
1518 } else {1518 } else {
1519 EditorState::Reviewing1519 EditorState::Reviewing
crates/agent/src/agent_model_selector.rs+3−2
@@ -91,12 +91,13 @@ impl AgentModelSelector { · 90 unmodified lines
91 91
92impl Render for AgentModelSelector {92impl Render for AgentModelSelector {
93 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {93 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
94 let focus_handle = self.focus_handle.clone();
95
96 let model = self.selector.read(cx).delegate.active_model(cx);94 let model = self.selector.read(cx).delegate.active_model(cx);
97 let model_name = model95 let model_name = model
98 .map(|model| model.model.name().0)96 .map(|model| model.model.name().0)
99 .unwrap_or_else(|| SharedString::from("No model selected"));97 .unwrap_or_else(|| SharedString::from("No model selected"));
98
99 let focus_handle = self.focus_handle.clone();
100
100 PickerPopoverMenu::new(101 PickerPopoverMenu::new(
101 self.selector.clone(),102 self.selector.clone(),
102 Button::new("active-model", model_name)103 Button::new("active-model", model_name)
crates/agent/src/agent_panel.rs+117−138
@@ -10,9 +10,9 @@ use serde::{Deserialize, Serialize}; · 9 unmodified lines
10use agent_settings::{AgentDockPosition, AgentSettings, CompletionMode, DefaultView};10use agent_settings::{AgentDockPosition, AgentSettings, CompletionMode, DefaultView};
11use anyhow::{Result, anyhow};11use anyhow::{Result, anyhow};
12use assistant_context_editor::{12use assistant_context_editor::{
13 AgentPanelDelegate, AssistantContext, ConfigurationError, ContextEditor, ContextEvent,13 AgentPanelDelegate, AssistantContext, ContextEditor, ContextEvent, ContextSummary,
14 ContextSummary, SlashCommandCompletionProvider, humanize_token_count,14 SlashCommandCompletionProvider, humanize_token_count, make_lsp_adapter_delegate,
15 make_lsp_adapter_delegate, render_remaining_tokens,15 render_remaining_tokens,
16};16};
17use assistant_slash_command::SlashCommandWorkingSet;17use assistant_slash_command::SlashCommandWorkingSet;
18use assistant_tool::ToolWorkingSet;18use assistant_tool::ToolWorkingSet;
@@ -29,7 +29,8 @@ use gpui::{ · 10 unmodified lines
29};29};
30use language::LanguageRegistry;30use language::LanguageRegistry;
31use language_model::{31use language_model::{
32 LanguageModelProviderTosView, LanguageModelRegistry, RequestUsage, ZED_CLOUD_PROVIDER_ID,32 ConfigurationError, LanguageModelProviderTosView, LanguageModelRegistry, RequestUsage,
33 ZED_CLOUD_PROVIDER_ID,
33};34};
34use project::{Project, ProjectPath, Worktree};35use project::{Project, ProjectPath, Worktree};
35use prompt_store::{PromptBuilder, PromptStore, UserPromptId};36use prompt_store::{PromptBuilder, PromptStore, UserPromptId};
@@ -2353,24 +2354,6 @@ impl AgentPanel { · 2317 unmodified lines
2353 self.thread.clone().into_any_element()2354 self.thread.clone().into_any_element()
2354 }2355 }
2355 2356
2356 fn configuration_error(&self, cx: &App) -> Option<ConfigurationError> {
2357 let Some(model) = LanguageModelRegistry::read_global(cx).default_model() else {
2358 return Some(ConfigurationError::NoProvider);
2359 };
2360
2361 if !model.provider.is_authenticated(cx) {
2362 return Some(ConfigurationError::ProviderNotAuthenticated);
2363 }
2364
2365 if model.provider.must_accept_terms(cx) {
2366 return Some(ConfigurationError::ProviderPendingTermsAcceptance(
2367 model.provider,
2368 ));
2369 }
2370
2371 None
2372 }
2373
2374 fn render_thread_empty_state(2357 fn render_thread_empty_state(
2375 &self,2358 &self,
2376 window: &mut Window,2359 window: &mut Window,
@@ -2380,7 +2363,9 @@ impl AgentPanel { · 3 unmodified lines
2380 .history_store2363 .history_store
2381 .update(cx, |this, cx| this.recent_entries(6, cx));2364 .update(cx, |this, cx| this.recent_entries(6, cx));
2382 2365
2383 let configuration_error = self.configuration_error(cx);2366 let model_registry = LanguageModelRegistry::read_global(cx);
2367 let configuration_error =
2368 model_registry.configuration_error(model_registry.default_model(), cx);
2384 let no_error = configuration_error.is_none();2369 let no_error = configuration_error.is_none();
2385 let focus_handle = self.focus_handle(cx);2370 let focus_handle = self.focus_handle(cx);
2386 2371
@@ -2397,11 +2382,7 @@ impl AgentPanel { · 10 unmodified lines
2397 .justify_center()2382 .justify_center()
2398 .items_center()2383 .items_center()
2399 .gap_1()2384 .gap_1()
2400 .child(2385 .child(h_flex().child(Headline::new("Welcome to the Agent Panel")))
2401 h_flex().child(
2402 Headline::new("Welcome to the Agent Panel")
2403 ),
2404 )
2405 .when(no_error, |parent| {2386 .when(no_error, |parent| {
2406 parent2387 parent
2407 .child(2388 .child(
@@ -2425,7 +2406,10 @@ impl AgentPanel { · 17 unmodified lines
2425 cx,2406 cx,
2426 ))2407 ))
2427 .on_click(|_event, window, cx| {2408 .on_click(|_event, window, cx| {
2428 window.dispatch_action(NewThread::default().boxed_clone(), cx)2409 window.dispatch_action(
2410 NewThread::default().boxed_clone(),
2411 cx,
2412 )
2429 }),2413 }),
2430 )2414 )
2431 .child(2415 .child(
@@ -2442,7 +2426,10 @@ impl AgentPanel { · 10 unmodified lines
2442 cx,2426 cx,
2443 ))2427 ))
2444 .on_click(|_event, window, cx| {2428 .on_click(|_event, window, cx| {
2445 window.dispatch_action(ToggleContextPicker.boxed_clone(), cx)2429 window.dispatch_action(
2430 ToggleContextPicker.boxed_clone(),
2431 cx,
2432 )
2446 }),2433 }),
2447 )2434 )
2448 .child(2435 .child(
@@ -2459,7 +2446,10 @@ impl AgentPanel { · 10 unmodified lines
2459 cx,2446 cx,
2460 ))2447 ))
2461 .on_click(|_event, window, cx| {2448 .on_click(|_event, window, cx| {
2462 window.dispatch_action(ToggleModelSelector.boxed_clone(), cx)2449 window.dispatch_action(
2450 ToggleModelSelector.boxed_clone(),
2451 cx,
2452 )
2463 }),2453 }),
2464 )2454 )
2465 .child(2455 .child(
@@ -2476,51 +2466,50 @@ impl AgentPanel { · 10 unmodified lines
2476 cx,2466 cx,
2477 ))2467 ))
2478 .on_click(|_event, window, cx| {2468 .on_click(|_event, window, cx| {
2479 window.dispatch_action(OpenConfiguration.boxed_clone(), cx)2469 window.dispatch_action(
2470 OpenConfiguration.boxed_clone(),
2471 cx,
2472 )
2480 }),2473 }),
2481 )2474 )
2482 })2475 })
2483 .map(|parent| {2476 .map(|parent| match configuration_error_ref {
2484 match configuration_error_ref {2477 Some(
2485 Some(ConfigurationError::ProviderNotAuthenticated)2478 err @ (ConfigurationError::ModelNotFound
2486 | Some(ConfigurationError::NoProvider) => {2479 | ConfigurationError::ProviderNotAuthenticated(_)
2487 parent2480 | ConfigurationError::NoProvider),
2488 .child(2481 ) => parent
2489 h_flex().child(2482 .child(h_flex().child(
2490 Label::new("To start using the agent, configure at least one LLM provider.")2483 Label::new(err.to_string()).color(Color::Muted).mb_2p5(),
2491 .color(Color::Muted)2484 ))
2492 .mb_2p5()2485 .child(
2493 )2486 Button::new("settings", "Configure a Provider")
2494 )2487 .icon(IconName::Settings)
2495 .child(2488 .icon_position(IconPosition::Start)
2496 Button::new("settings", "Configure a Provider")2489 .icon_size(IconSize::Small)
2497 .icon(IconName::Settings)2490 .icon_color(Color::Muted)
2498 .icon_position(IconPosition::Start)2491 .full_width()
2499 .icon_size(IconSize::Small)2492 .key_binding(KeyBinding::for_action_in(
2500 .icon_color(Color::Muted)2493 &OpenConfiguration,
2501 .full_width()2494 &focus_handle,
2502 .key_binding(KeyBinding::for_action_in(2495 window,
2503 &OpenConfiguration,
2504 &focus_handle,
2505 window,
2506 cx,
2507 ))
2508 .on_click(|_event, window, cx| {
2509 window.dispatch_action(OpenConfiguration.boxed_clone(), cx)
2510 }),
2511 )
2512 }
2513 Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
2514 parent.children(
2515 provider.render_accept_terms(
2516 LanguageModelProviderTosView::ThreadFreshStart,
2517 cx,2496 cx,
2518 ),2497 ))
2519 )2498 .on_click(|_event, window, cx| {
2520 }2499 window.dispatch_action(
2521 None => parent,2500 OpenConfiguration.boxed_clone(),
2501 cx,
2502 )
2503 }),
2504 ),
2505 Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
2506 parent.children(provider.render_accept_terms(
2507 LanguageModelProviderTosView::ThreadFreshStart,
2508 cx,
2509 ))
2522 }2510 }
2523 })2511 None => parent,
2512 }),
2524 )2513 )
2525 })2514 })
2526 .when(!recent_history.is_empty(), |parent| {2515 .when(!recent_history.is_empty(), |parent| {
@@ -2555,7 +2544,8 @@ impl AgentPanel { · 28 unmodified lines
2555 &self.focus_handle(cx),2544 &self.focus_handle(cx),
2556 window,2545 window,
2557 cx,2546 cx,
2558 ).map(|kb| kb.size(rems_from_px(12.))),2547 )
2548 .map(|kb| kb.size(rems_from_px(12.))),
2559 )2549 )
2560 .on_click(move |_event, window, cx| {2550 .on_click(move |_event, window, cx| {
2561 window.dispatch_action(OpenHistory.boxed_clone(), cx);2551 window.dispatch_action(OpenHistory.boxed_clone(), cx);
@@ -2565,79 +2555,68 @@ impl AgentPanel { · 3 unmodified lines
2565 .child(2555 .child(
2566 v_flex()2556 v_flex()
2567 .gap_1()2557 .gap_1()
2568 .children(2558 .children(recent_history.into_iter().enumerate().map(
2569 recent_history.into_iter().enumerate().map(|(index, entry)| {2559 |(index, entry)| {
2570 // TODO: Add keyboard navigation.2560 // TODO: Add keyboard navigation.
2571 let is_hovered = self.hovered_recent_history_item == Some(index);2561 let is_hovered =
2562 self.hovered_recent_history_item == Some(index);
2572 HistoryEntryElement::new(entry.clone(), cx.entity().downgrade())2563 HistoryEntryElement::new(entry.clone(), cx.entity().downgrade())
2573 .hovered(is_hovered)2564 .hovered(is_hovered)
2574 .on_hover(cx.listener(move |this, is_hovered, _window, cx| {2565 .on_hover(cx.listener(
2575 if *is_hovered {2566 move |this, is_hovered, _window, cx| {
2576 this.hovered_recent_history_item = Some(index);2567 if *is_hovered {
2577 } else if this.hovered_recent_history_item == Some(index) {2568 this.hovered_recent_history_item = Some(index);
2578 this.hovered_recent_history_item = None;2569 } else if this.hovered_recent_history_item
2579 }2570 == Some(index)
2580 cx.notify();2571 {
2581 }))2572 this.hovered_recent_history_item = None;
2573 }
2574 cx.notify();
2575 },
2576 ))
2582 .into_any_element()2577 .into_any_element()
2583 }),2578 },
2584 )2579 )),
2585 )2580 )
2586 .map(|parent| {2581 .map(|parent| match configuration_error_ref {
2587 match configuration_error_ref {2582 Some(
2588 Some(ConfigurationError::ProviderNotAuthenticated)2583 err @ (ConfigurationError::ModelNotFound
2589 | Some(ConfigurationError::NoProvider) => {2584 | ConfigurationError::ProviderNotAuthenticated(_)
2590 parent2585 | ConfigurationError::NoProvider),
2591 .child(2586 ) => parent.child(
2592 Banner::new()2587 Banner::new()
2593 .severity(ui::Severity::Warning)2588 .severity(ui::Severity::Warning)
2594 .child(2589 .child(Label::new(err.to_string()).size(LabelSize::Small))
2595 Label::new(2590 .action_slot(
2596 "Configure at least one LLM provider to start using the panel.",2591 Button::new("settings", "Configure Provider")
2597 )2592 .style(ButtonStyle::Tinted(ui::TintColor::Warning))
2598 .size(LabelSize::Small),2593 .label_size(LabelSize::Small)
2594 .key_binding(
2595 KeyBinding::for_action_in(
2596 &OpenConfiguration,
2597 &focus_handle,
2598 window,
2599 cx,
2599 )2600 )
2600 .action_slot(2601 .map(|kb| kb.size(rems_from_px(12.))),
2601 Button::new("settings", "Configure Provider")2602 )
2602 .style(ButtonStyle::Tinted(ui::TintColor::Warning))2603 .on_click(|_event, window, cx| {
2603 .label_size(LabelSize::Small)2604 window.dispatch_action(
2604 .key_binding(2605 OpenConfiguration.boxed_clone(),
2605 KeyBinding::for_action_in(2606 cx,
2606 &OpenConfiguration,2607 )
2607 &focus_handle,2608 }),
2608 window,2609 ),
2609 cx,2610 ),
2610 )2611 Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
2611 .map(|kb| kb.size(rems_from_px(12.))),2612 parent.child(Banner::new().severity(ui::Severity::Warning).child(
2612 )2613 h_flex().w_full().children(provider.render_accept_terms(
2613 .on_click(|_event, window, cx| {2614 LanguageModelProviderTosView::ThreadtEmptyState,
2614 window.dispatch_action(2615 cx,
2615 OpenConfiguration.boxed_clone(),2616 )),
2616 cx,2617 ))
2617 )
2618 }),
2619 ),
2620 )
2621 }
2622 Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
2623 parent
2624 .child(
2625 Banner::new()
2626 .severity(ui::Severity::Warning)
2627 .child(
2628 h_flex()
2629 .w_full()
2630 .children(
2631 provider.render_accept_terms(
2632 LanguageModelProviderTosView::ThreadtEmptyState,
2633 cx,
2634 ),
2635 ),
2636 ),
2637 )
2638 }
2639 None => parent,
2640 }2618 }
2619 None => parent,
2641 })2620 })
2642 })2621 })
2643 }2622 }
crates/agent/src/buffer_codegen.rs+4−2
@@ -386,8 +386,10 @@ impl CodegenAlternative { · 385 unmodified lines
386 async { Ok(LanguageModelTextStream::default()) }.boxed_local()386 async { Ok(LanguageModelTextStream::default()) }.boxed_local()
387 } else {387 } else {
388 let request = self.build_request(&model, user_prompt, cx)?;388 let request = self.build_request(&model, user_prompt, cx)?;
389 cx.spawn(async move |_, cx| model.stream_completion_text(request.await, &cx).await)389 cx.spawn(async move |_, cx| {
390 .boxed_local()390 Ok(model.stream_completion_text(request.await, &cx).await?)
391 })
392 .boxed_local()
391 };393 };
392 self.handle_stream(telemetry_id, provider_id.to_string(), api_key, stream, cx);394 self.handle_stream(telemetry_id, provider_id.to_string(), api_key, stream, cx);
393 Ok(())395 Ok(())
crates/agent/src/context_picker/completion_provider.rs+9−9
@@ -1066,7 +1066,7 @@ mod tests { · 1065 unmodified lines
1066 use serde_json::json;1066 use serde_json::json;
1067 use settings::SettingsStore;1067 use settings::SettingsStore;
1068 use std::{ops::Deref, rc::Rc};1068 use std::{ops::Deref, rc::Rc};
1069 use util::{path, separator};1069 use util::path;
1070 use workspace::{AppState, Item};1070 use workspace::{AppState, Item};
1071 1071
1072 #[test]1072 #[test]
@@ -1217,14 +1217,14 @@ mod tests { · 144 unmodified lines
1217 let mut cx = VisualTestContext::from_window(*window.deref(), cx);1217 let mut cx = VisualTestContext::from_window(*window.deref(), cx);
1218 1218
1219 let paths = vec![1219 let paths = vec![
1220 separator!("a/one.txt"),1220 path!("a/one.txt"),
1221 separator!("a/two.txt"),1221 path!("a/two.txt"),
1222 separator!("a/three.txt"),1222 path!("a/three.txt"),
1223 separator!("a/four.txt"),1223 path!("a/four.txt"),
1224 separator!("b/five.txt"),1224 path!("b/five.txt"),
1225 separator!("b/six.txt"),1225 path!("b/six.txt"),
1226 separator!("b/seven.txt"),1226 path!("b/seven.txt"),
1227 separator!("b/eight.txt"),1227 path!("b/eight.txt"),
1228 ];1228 ];
1229 1229
1230 let mut opened_editors = Vec::new();1230 let mut opened_editors = Vec::new();
crates/agent/src/context_server_configuration.rs+31−2
@@ -3,16 +3,21 @@ use std::sync::Arc; · 2 unmodified lines
3use anyhow::Context as _;3use anyhow::Context as _;
4use context_server::ContextServerId;4use context_server::ContextServerId;
5use extension::{ContextServerConfiguration, ExtensionManifest};5use extension::{ContextServerConfiguration, ExtensionManifest};
6use fs::Fs;
6use gpui::Task;7use gpui::Task;
7use language::LanguageRegistry;8use language::LanguageRegistry;
8use project::context_server_store::registry::ContextServerDescriptorRegistry;9use project::{
10 context_server_store::registry::ContextServerDescriptorRegistry,
11 project_settings::ProjectSettings,
12};
13use settings::update_settings_file;
9use ui::prelude::*;14use ui::prelude::*;
10use util::ResultExt;15use util::ResultExt;
11use workspace::Workspace;16use workspace::Workspace;
12 17
13use crate::agent_configuration::ConfigureContextServerModal;18use crate::agent_configuration::ConfigureContextServerModal;
14 19
15pub(crate) fn init(language_registry: Arc<LanguageRegistry>, cx: &mut App) {20pub(crate) fn init(language_registry: Arc<LanguageRegistry>, fs: Arc<dyn Fs>, cx: &mut App) {
16 cx.observe_new(move |_: &mut Workspace, window, cx| {21 cx.observe_new(move |_: &mut Workspace, window, cx| {
17 let Some(window) = window else {22 let Some(window) = window else {
18 return;23 return;
@@ -21,6 +26,7 @@ pub(crate) fn init(language_registry: Arc<LanguageRegistry>, cx: &mut App) { · 2 unmodified lines
21 if let Some(extension_events) = extension::ExtensionEvents::try_global(cx).as_ref() {26 if let Some(extension_events) = extension::ExtensionEvents::try_global(cx).as_ref() {
22 cx.subscribe_in(extension_events, window, {27 cx.subscribe_in(extension_events, window, {
23 let language_registry = language_registry.clone();28 let language_registry = language_registry.clone();
29 let fs = fs.clone();
24 move |workspace, _, event, window, cx| match event {30 move |workspace, _, event, window, cx| match event {
25 extension::Event::ExtensionInstalled(manifest) => {31 extension::Event::ExtensionInstalled(manifest) => {
26 show_configure_mcp_modal(32 show_configure_mcp_modal(
@@ -31,6 +37,13 @@ pub(crate) fn init(language_registry: Arc<LanguageRegistry>, cx: &mut App) { · 4 unmodified lines
31 cx,37 cx,
32 );38 );
33 }39 }
40 extension::Event::ExtensionUninstalled(manifest) => {
41 remove_context_server_settings(
42 manifest.context_servers.keys().cloned().collect(),
43 fs.clone(),
44 cx,
45 );
46 }
34 extension::Event::ConfigureExtensionRequested(manifest) => {47 extension::Event::ConfigureExtensionRequested(manifest) => {
35 if !manifest.context_servers.is_empty() {48 if !manifest.context_servers.is_empty() {
36 show_configure_mcp_modal(49 show_configure_mcp_modal(
@@ -55,6 +68,18 @@ pub(crate) fn init(language_registry: Arc<LanguageRegistry>, cx: &mut App) { · 18 unmodified lines
55 .detach();68 .detach();
56}69}
57 70
71fn remove_context_server_settings(
72 context_server_ids: Vec<Arc<str>>,
73 fs: Arc<dyn Fs>,
74 cx: &mut App,
75) {
76 update_settings_file::<ProjectSettings>(fs, cx, move |settings, _| {
77 settings
78 .context_servers
79 .retain(|server_id, _| !context_server_ids.contains(server_id));
80 });
81}
82
58pub enum Configuration {83pub enum Configuration {
59 NotAvailable(ContextServerId, Option<SharedString>),84 NotAvailable(ContextServerId, Option<SharedString>),
60 Required(85 Required(
@@ -71,6 +96,10 @@ fn show_configure_mcp_modal( · 10 unmodified lines
71 window: &mut Window,96 window: &mut Window,
72 cx: &mut Context<'_, Workspace>,97 cx: &mut Context<'_, Workspace>,
73) {98) {
99 if !window.is_window_active() {
100 return;
101 }
102
74 let context_server_store = workspace.project().read(cx).context_server_store();103 let context_server_store = workspace.project().read(cx).context_server_store();
75 let repository: Option<SharedString> = manifest.repository.as_ref().map(|s| s.clone().into());104 let repository: Option<SharedString> = manifest.repository.as_ref().map(|s| s.clone().into());
76 105
crates/agent/src/context_server_tool.rs+4−1
@@ -105,7 +105,7 @@ impl Tool for ContextServerTool { · 104 unmodified lines
105 arguments105 arguments
106 );106 );
107 let response = protocol107 let response = protocol
108 .request::<context_server::types::request::CallTool>(108 .request::<context_server::types::requests::CallTool>(
109 context_server::types::CallToolParams {109 context_server::types::CallToolParams {
110 name: tool_name,110 name: tool_name,
111 arguments,111 arguments,
@@ -123,6 +123,9 @@ impl Tool for ContextServerTool { · 11 unmodified lines
123 types::ToolResponseContent::Image { .. } => {123 types::ToolResponseContent::Image { .. } => {
124 log::warn!("Ignoring image content from tool response");124 log::warn!("Ignoring image content from tool response");
125 }125 }
126 types::ToolResponseContent::Audio { .. } => {
127 log::warn!("Ignoring audio content from tool response");
128 }
126 types::ToolResponseContent::Resource { .. } => {129 types::ToolResponseContent::Resource { .. } => {
127 log::warn!("Ignoring resource content from tool response");130 log::warn!("Ignoring resource content from tool response");
128 }131 }
crates/agent/src/inline_assistant.rs+44−43
@@ -24,6 +24,7 @@ use gpui::{ · 23 unmodified lines
24 WeakEntity, Window, point,24 WeakEntity, Window, point,
25};25};
26use language::{Buffer, Point, Selection, TransactionId};26use language::{Buffer, Point, Selection, TransactionId};
27use language_model::ConfigurationError;
27use language_model::ConfiguredModel;28use language_model::ConfiguredModel;
28use language_model::{LanguageModelRegistry, report_assistant_event};29use language_model::{LanguageModelRegistry, report_assistant_event};
29use multi_buffer::MultiBufferRow;30use multi_buffer::MultiBufferRow;
@@ -38,8 +39,7 @@ use telemetry_events::{AssistantEventData, AssistantKind, AssistantPhase}; · 8 unmodified lines
38use terminal_view::{TerminalView, terminal_panel::TerminalPanel};39use terminal_view::{TerminalView, terminal_panel::TerminalPanel};
39use text::{OffsetRangeExt, ToPoint as _};40use text::{OffsetRangeExt, ToPoint as _};
40use ui::prelude::*;41use ui::prelude::*;
41use util::RangeExt;42use util::{RangeExt, ResultExt, maybe};
42use util::ResultExt;
43use workspace::{ItemHandle, Toast, Workspace, dock::Panel, notifications::NotificationId};43use workspace::{ItemHandle, Toast, Workspace, dock::Panel, notifications::NotificationId};
44use zed_actions::agent::OpenConfiguration;44use zed_actions::agent::OpenConfiguration;
45 45
@@ -233,10 +233,9 @@ impl InlineAssistant { · 187 unmodified lines
233 return;233 return;
234 };234 };
235 235
236 let is_authenticated = || {236 let configuration_error = || {
237 LanguageModelRegistry::read_global(cx)237 let model_registry = LanguageModelRegistry::read_global(cx);
238 .inline_assistant_model()238 model_registry.configuration_error(model_registry.inline_assistant_model(), cx)
239 .map_or(false, |model| model.provider.is_authenticated(cx))
240 };239 };
241 240
242 let Some(agent_panel) = workspace.panel::<AgentPanel>(cx) else {241 let Some(agent_panel) = workspace.panel::<AgentPanel>(cx) else {
@@ -284,20 +283,23 @@ impl InlineAssistant { · 41 unmodified lines
284 }283 }
285 };284 };
286 285
287 if is_authenticated() {286 if let Some(error) = configuration_error() {
288 handle_assist(window, cx);287 if let ConfigurationError::ProviderNotAuthenticated(provider) = error {
289 } else {288 cx.spawn(async move |_, cx| {
290 cx.spawn_in(window, async move |_workspace, cx| {289 cx.update(|cx| provider.authenticate(cx))?.await?;
291 let Some(task) = cx.update(|_, cx| {290 anyhow::Ok(())
292 LanguageModelRegistry::read_global(cx)291 })
293 .inline_assistant_model()292 .detach_and_log_err(cx);
294 .map_or(None, |model| Some(model.provider.authenticate(cx)))293
295 })?294 if configuration_error().is_none() {
296 else {295 handle_assist(window, cx);
296 }
297 } else {
298 cx.spawn_in(window, async move |_, cx| {
297 let answer = cx299 let answer = cx
298 .prompt(300 .prompt(
299 gpui::PromptLevel::Warning,301 gpui::PromptLevel::Warning,
300 "No language model provider configured",302 &error.to_string(),
301 None,303 None,
302 &["Configure", "Cancel"],304 &["Configure", "Cancel"],
303 )305 )
@@ -311,17 +313,12 @@ impl InlineAssistant { · 7 unmodified lines
311 .ok();313 .ok();
312 }314 }
313 }315 }
314 return Ok(());316 anyhow::Ok(())
315 };317 })
316 task.await?;318 .detach_and_log_err(cx);
317
318 anyhow::Ok(())
319 })
320 .detach_and_log_err(cx);
321
322 if is_authenticated() {
323 handle_assist(window, cx);
324 }319 }
320 } else {
321 handle_assist(window, cx);
325 }322 }
326 }323 }
327 324
@@ -1171,27 +1168,31 @@ impl InlineAssistant { · 843 unmodified lines
1171 selections.select_anchor_ranges([position..position])1168 selections.select_anchor_ranges([position..position])
1172 });1169 });
1173 1170
1174 let mut scroll_target_top;1171 let mut scroll_target_range = None;
1175 let mut scroll_target_bottom;
1176 if let Some(decorations) = assist.decorations.as_ref() {1172 if let Some(decorations) = assist.decorations.as_ref() {
1177 scroll_target_top = editor1173 scroll_target_range = maybe!({
1178 .row_for_block(decorations.prompt_block_id, cx)1174 let top = editor.row_for_block(decorations.prompt_block_id, cx)?.0 as f32;
1179 .unwrap()1175 let bottom = editor.row_for_block(decorations.end_block_id, cx)?.0 as f32;
1180 .0 as f32;1176 Some((top, bottom))
1181 scroll_target_bottom = editor1177 });
1182 .row_for_block(decorations.end_block_id, cx)1178 if scroll_target_range.is_none() {
1183 .unwrap()1179 log::error!("bug: failed to find blocks for scrolling to inline assist");
1184 .0 as f32;1180 }
1185 } else {1181 }
1182 let scroll_target_range = scroll_target_range.unwrap_or_else(|| {
1186 let snapshot = editor.snapshot(window, cx);1183 let snapshot = editor.snapshot(window, cx);
1187 let start_row = assist1184 let start_row = assist
1188 .range1185 .range
1189 .start1186 .start
1190 .to_display_point(&snapshot.display_snapshot)1187 .to_display_point(&snapshot.display_snapshot)
1191 .row();1188 .row();
1192 scroll_target_top = start_row.0 as f32;1189 let top = start_row.0 as f32;
1193 scroll_target_bottom = scroll_target_top + 1.;1190 let bottom = top + 1.0;
1194 }1191 (top, bottom)
1192 });
1193 let mut scroll_target_top = scroll_target_range.0;
1194 let mut scroll_target_bottom = scroll_target_range.1;
1195
1195 scroll_target_top -= editor.vertical_scroll_margin() as f32;1196 scroll_target_top -= editor.vertical_scroll_margin() as f32;
1196 scroll_target_bottom += editor.vertical_scroll_margin() as f32;1197 scroll_target_bottom += editor.vertical_scroll_margin() as f32;
1197 1198
@@ -1331,7 +1332,7 @@ impl InlineAssistant { · 133 unmodified lines
1331 editor.clear_gutter_highlights::<GutterPendingRange>(cx);1332 editor.clear_gutter_highlights::<GutterPendingRange>(cx);
1332 } else {1333 } else {
1333 editor.highlight_gutter::<GutterPendingRange>(1334 editor.highlight_gutter::<GutterPendingRange>(
1334 &gutter_pending_ranges,1335 gutter_pending_ranges,
1335 |cx| cx.theme().status().info_background,1336 |cx| cx.theme().status().info_background,
1336 cx,1337 cx,
1337 )1338 )
@@ -1342,7 +1343,7 @@ impl InlineAssistant { · 4 unmodified lines
1342 editor.clear_gutter_highlights::<GutterTransformedRange>(cx);1343 editor.clear_gutter_highlights::<GutterTransformedRange>(cx);
1343 } else {1344 } else {
1344 editor.highlight_gutter::<GutterTransformedRange>(1345 editor.highlight_gutter::<GutterTransformedRange>(
1345 &gutter_transformed_ranges,1346 gutter_transformed_ranges,
1346 |cx| cx.theme().status().info,1347 |cx| cx.theme().status().info,
1347 cx,1348 cx,
1348 )1349 )
crates/agent/src/inline_prompt_editor.rs+3−1
@@ -261,7 +261,7 @@ impl<T: 'static> PromptEditor<T> { · 260 unmodified lines
261 261
262 let focus = self.editor.focus_handle(cx).contains_focused(window, cx);262 let focus = self.editor.focus_handle(cx).contains_focused(window, cx);
263 self.editor = cx.new(|cx| {263 self.editor = cx.new(|cx| {
264 let mut editor = Editor::auto_height(Self::MAX_LINES as usize, window, cx);264 let mut editor = Editor::auto_height(1, Self::MAX_LINES as usize, window, cx);
265 editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);265 editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
266 editor.set_placeholder_text("Add a prompt…", cx);266 editor.set_placeholder_text("Add a prompt…", cx);
267 editor.set_text(prompt, window, cx);267 editor.set_text(prompt, window, cx);
@@ -872,6 +872,7 @@ impl PromptEditor<BufferCodegen> { · 604 unmodified lines
872 let prompt_editor = cx.new(|cx| {872 let prompt_editor = cx.new(|cx| {
873 let mut editor = Editor::new(873 let mut editor = Editor::new(
874 EditorMode::AutoHeight {874 EditorMode::AutoHeight {
875 min_lines: 1,
875 max_lines: Self::MAX_LINES as usize,876 max_lines: Self::MAX_LINES as usize,
876 },877 },
877 prompt_buffer,878 prompt_buffer,
@@ -1050,6 +1051,7 @@ impl PromptEditor<TerminalCodegen> { · 172 unmodified lines
1050 let prompt_editor = cx.new(|cx| {1051 let prompt_editor = cx.new(|cx| {
1051 let mut editor = Editor::new(1052 let mut editor = Editor::new(
1052 EditorMode::AutoHeight {1053 EditorMode::AutoHeight {
1054 min_lines: 1,
1053 max_lines: Self::MAX_LINES as usize,1055 max_lines: Self::MAX_LINES as usize,
1054 },1056 },
1055 prompt_buffer,1057 prompt_buffer,
crates/agent/src/message_editor.rs+105−96
@@ -39,7 +39,7 @@ use proto::Plan; · 38 unmodified lines
39use settings::Settings;39use settings::Settings;
40use std::time::Duration;40use std::time::Duration;
41use theme::ThemeSettings;41use theme::ThemeSettings;
42use ui::{Disclosure, KeyBinding, PopoverMenuHandle, Tooltip, prelude::*};42use ui::{Callout, Disclosure, KeyBinding, PopoverMenuHandle, Tooltip, prelude::*};
43use util::{ResultExt as _, maybe};43use util::{ResultExt as _, maybe};
44use workspace::{CollaboratorId, Workspace};44use workspace::{CollaboratorId, Workspace};
45use zed_llm_client::CompletionIntent;45use zed_llm_client::CompletionIntent;
@@ -79,6 +79,7 @@ pub struct MessageEditor { · 33 unmodified lines
79 _subscriptions: Vec<Subscription>,79 _subscriptions: Vec<Subscription>,
80}80}
81 81
82const MIN_EDITOR_LINES: usize = 4;
82const MAX_EDITOR_LINES: usize = 8;83const MAX_EDITOR_LINES: usize = 8;
83 84
84pub(crate) fn create_editor(85pub(crate) fn create_editor(
@@ -102,6 +103,7 @@ pub(crate) fn create_editor( · 17 unmodified lines
102 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));103 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
103 let mut editor = Editor::new(104 let mut editor = Editor::new(
104 editor::EditorMode::AutoHeight {105 editor::EditorMode::AutoHeight {
106 min_lines: MIN_EDITOR_LINES,
105 max_lines: MAX_EDITOR_LINES,107 max_lines: MAX_EDITOR_LINES,
106 },108 },
107 buffer,109 buffer,
@@ -253,6 +255,7 @@ impl MessageEditor { · 145 unmodified lines
253 })255 })
254 } else {256 } else {
255 editor.set_mode(EditorMode::AutoHeight {257 editor.set_mode(EditorMode::AutoHeight {
258 min_lines: MIN_EDITOR_LINES,
256 max_lines: MAX_EDITOR_LINES,259 max_lines: MAX_EDITOR_LINES,
257 })260 })
258 }261 }
@@ -428,10 +431,6 @@ impl MessageEditor { · 169 unmodified lines
428 }431 }
429 432
430 fn handle_review_click(&mut self, window: &mut Window, cx: &mut Context<Self>) {433 fn handle_review_click(&mut self, window: &mut Window, cx: &mut Context<Self>) {
431 if self.thread.read(cx).has_pending_edit_tool_uses() {
432 return;
433 }
434
435 self.edits_expanded = true;434 self.edits_expanded = true;
436 AgentDiffPane::deploy(self.thread.clone(), self.workspace.clone(), window, cx).log_err();435 AgentDiffPane::deploy(self.thread.clone(), self.workspace.clone(), window, cx).log_err();
437 cx.notify();436 cx.notify();
@@ -506,7 +505,7 @@ impl MessageEditor { · 68 unmodified lines
506 cx.notify();505 cx.notify();
507 }506 }
508 507
509 fn render_max_mode_toggle(&self, cx: &mut Context<Self>) -> Option<AnyElement> {508 fn render_burn_mode_toggle(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
510 let thread = self.thread.read(cx);509 let thread = self.thread.read(cx);
511 let model = thread.configured_model();510 let model = thread.configured_model();
512 if !model?.model.supports_max_mode() {511 if !model?.model.supports_max_mode() {
@@ -641,96 +640,87 @@ impl MessageEditor { · 128 unmodified lines
641 .border_color(cx.theme().colors().border)640 .border_color(cx.theme().colors().border)
642 .child(641 .child(
643 h_flex()642 h_flex()
644 .items_start()
645 .justify_between()643 .justify_between()
646 .child(self.context_strip.clone())644 .child(self.context_strip.clone())
647 .child(645 .when(focus_handle.is_focused(window), |this| {
648 h_flex()646 this.child(
649 .gap_1()647 IconButton::new("toggle-height", expand_icon)
650 .when(focus_handle.is_focused(window), |this| {648 .icon_size(IconSize::XSmall)
651 this.child(649 .icon_color(Color::Muted)
652 IconButton::new("toggle-height", expand_icon)650 .tooltip({
653 .icon_size(IconSize::XSmall)651 let focus_handle = focus_handle.clone();
654 .icon_color(Color::Muted)652 move |window, cx| {
655 .tooltip({653 let expand_label = if is_editor_expanded {
656 let focus_handle = focus_handle.clone();654 "Minimize Message Editor".to_string()
657 move |window, cx| {655 } else {
658 let expand_label = if is_editor_expanded {656 "Expand Message Editor".to_string()
659 "Minimize Message Editor".to_string()657 };
660 } else {658
661 "Expand Message Editor".to_string()659 Tooltip::for_action_in(
662 };660 expand_label,
663 661 &ExpandMessageEditor,
664 Tooltip::for_action_in(662 &focus_handle,
665 expand_label,663 window,
666 &ExpandMessageEditor,664 cx,
667 &focus_handle,665 )
668 window,666 }
669 cx,667 })
670 )668 .on_click(cx.listener(|_, _, window, cx| {
671 }669 window.dispatch_action(Box::new(ExpandMessageEditor), cx);
672 })670 })),
673 .on_click(cx.listener(|_, _, window, cx| {671 )
674 window672 }),
675 .dispatch_action(Box::new(ExpandMessageEditor), cx);
676 })),
677 )
678 }),
679 ),
680 )673 )
681 .child(674 .child(
682 v_flex()675 v_flex()
683 .size_full()676 .size_full()
684 .gap_4()677 .gap_1()
685 .when(is_editor_expanded, |this| {678 .when(is_editor_expanded, |this| {
686 this.h(vh(0.8, window)).justify_between()679 this.h(vh(0.8, window)).justify_between()
687 })680 })
688 .child(681 .child({
689 v_flex()682 let settings = ThemeSettings::get_global(cx);
690 .min_h_16()683 let font_size = TextSize::Small
691 .when(is_editor_expanded, |this| this.h_full())684 .rems(cx)
692 .child({685 .to_pixels(settings.agent_font_size(cx));
693 let settings = ThemeSettings::get_global(cx);686 let line_height = settings.buffer_line_height.value() * font_size;
694 let font_size = TextSize::Small687
695 .rems(cx)688 let text_style = TextStyle {
696 .to_pixels(settings.agent_font_size(cx));689 color: cx.theme().colors().text,
697 let line_height = settings.buffer_line_height.value() * font_size;690 font_family: settings.buffer_font.family.clone(),
698 691 font_fallbacks: settings.buffer_font.fallbacks.clone(),
699 let text_style = TextStyle {692 font_features: settings.buffer_font.features.clone(),
700 color: cx.theme().colors().text,693 font_size: font_size.into(),
701 font_family: settings.buffer_font.family.clone(),694 line_height: line_height.into(),
702 font_fallbacks: settings.buffer_font.fallbacks.clone(),695 ..Default::default()
703 font_features: settings.buffer_font.features.clone(),696 };
704 font_size: font_size.into(),697
705 line_height: line_height.into(),698 EditorElement::new(
706 ..Default::default()699 &self.editor,
707 };700 EditorStyle {
708 701 background: editor_bg_color,
709 EditorElement::new(702 local_player: cx.theme().players().local(),
710 &self.editor,703 text: text_style,
711 EditorStyle {704 syntax: cx.theme().syntax().clone(),
712 background: editor_bg_color,705 ..Default::default()
713 local_player: cx.theme().players().local(),706 },
714 text: text_style,707 )
715 syntax: cx.theme().syntax().clone(),708 .into_any()
716 ..Default::default()709 })
717 },
718 )
719 .into_any()
720 }),
721 )
722 .child(710 .child(
723 h_flex()711 h_flex()
724 .flex_none()712 .flex_none()
713 .flex_wrap()
725 .justify_between()714 .justify_between()
726 .child(715 .child(
727 h_flex()716 h_flex()
728 .child(self.render_follow_toggle(cx))717 .child(self.render_follow_toggle(cx))
729 .children(self.render_max_mode_toggle(cx)),718 .children(self.render_burn_mode_toggle(cx)),
730 )719 )
731 .child(720 .child(
732 h_flex()721 h_flex()
733 .gap_1()722 .gap_1()
723 .flex_wrap()
734 .when(!incompatible_tools.is_empty(), |this| {724 .when(!incompatible_tools.is_empty(), |this| {
735 this.child(725 this.child(
736 IconButton::new(726 IconButton::new(
@@ -1183,6 +1173,7 @@ impl MessageEditor { · 446 unmodified lines
1183 .map_or(false, |model| {1173 .map_or(false, |model| {
1184 model.provider.id().0 == ZED_CLOUD_PROVIDER_ID1174 model.provider.id().0 == ZED_CLOUD_PROVIDER_ID
1185 });1175 });
1176
1186 if !is_using_zed_provider {1177 if !is_using_zed_provider {
1187 return None;1178 return None;
1188 }1179 }
@@ -1237,14 +1228,6 @@ impl MessageEditor { · 48 unmodified lines
1237 token_usage_ratio: TokenUsageRatio,1228 token_usage_ratio: TokenUsageRatio,
1238 cx: &mut Context<Self>,1229 cx: &mut Context<Self>,
1239 ) -> Option<Div> {1230 ) -> Option<Div> {
1240 let title = if token_usage_ratio == TokenUsageRatio::Exceeded {
1241 "Thread reached the token limit"
1242 } else {
1243 "Thread reaching the token limit soon"
1244 };
1245
1246 let message = "Start a new thread from a summary to continue the conversation.";
1247
1248 let icon = if token_usage_ratio == TokenUsageRatio::Exceeded {1231 let icon = if token_usage_ratio == TokenUsageRatio::Exceeded {
1249 Icon::new(IconName::X)1232 Icon::new(IconName::X)
1250 .color(Color::Error)1233 .color(Color::Error)
@@ -1255,19 +1238,43 @@ impl MessageEditor { · 4 unmodified lines
1255 .size(IconSize::XSmall)1238 .size(IconSize::XSmall)
1256 };1239 };
1257 1240
1241 let title = if token_usage_ratio == TokenUsageRatio::Exceeded {
1242 "Thread reached the token limit"
1243 } else {
1244 "Thread reaching the token limit soon"
1245 };
1246
1258 Some(1247 Some(
1259 div()1248 div()
1260 .child(ui::Callout::multi_line(1249 .border_t_1()
1261 title,1250 .border_color(cx.theme().colors().border)
1262 message,1251 .child(
1263 icon,1252 Callout::new()
1264 "Start New Thread",1253 .line_height(line_height)
1265 Box::new(cx.listener(|this, _, window, cx| {1254 .icon(icon)
1266 let from_thread_id = Some(this.thread.read(cx).id().clone());1255 .title(title)
1267 window.dispatch_action(Box::new(NewThread { from_thread_id }), cx);1256 .description(
1268 })),1257 "To continue, start a new thread from a summary or turn burn mode on.",
1269 ))1258 )
1270 .line_height(line_height),1259 .primary_action(
1260 Button::new("start-new-thread", "Start New Thread")
1261 .label_size(LabelSize::Small)
1262 .on_click(cx.listener(|this, _, window, cx| {
1263 let from_thread_id = Some(this.thread.read(cx).id().clone());
1264 window.dispatch_action(
1265 Box::new(NewThread { from_thread_id }),
1266 cx,
1267 );
1268 })),
1269 )
1270 .secondary_action(
1271 IconButton::new("burn-mode-callout", IconName::ZedBurnMode)
1272 .icon_size(IconSize::XSmall)
1273 .on_click(cx.listener(|this, _event, window, cx| {
1274 this.toggle_burn_mode(&ToggleBurnMode, window, cx);
1275 })),
1276 ),
1277 ),
1271 )1278 )
1272 }1279 }
1273 1280
@@ -1464,6 +1471,8 @@ impl Render for MessageEditor { · 190 unmodified lines
1464 total_token_usage.ratio()1471 total_token_usage.ratio()
1465 });1472 });
1466 1473
1474 let burn_mode_enabled = thread.completion_mode() == CompletionMode::Burn;
1475
1467 let action_log = self.thread.read(cx).action_log();1476 let action_log = self.thread.read(cx).action_log();
1468 let changed_buffers = action_log.read(cx).changed_buffers(cx);1477 let changed_buffers = action_log.read(cx).changed_buffers(cx);
1469 1478
@@ -1480,7 +1489,7 @@ impl Render for MessageEditor { · 10 unmodified lines
1480 1489
1481 if usage_callout.is_some() {1490 if usage_callout.is_some() {
1482 usage_callout1491 usage_callout
1483 } else if token_usage_ratio != TokenUsageRatio::Normal {1492 } else if token_usage_ratio != TokenUsageRatio::Normal && !burn_mode_enabled {
1484 self.render_token_limit_callout(line_height, token_usage_ratio, cx)1493 self.render_token_limit_callout(line_height, token_usage_ratio, cx)
1485 } else {1494 } else {
1486 None1495 None
crates/agent/src/thread.rs+8−5
@@ -195,20 +195,20 @@ impl MessageSegment { · 194 unmodified lines
195 }195 }
196}196}
197 197
198#[derive(Debug, Clone, Serialize, Deserialize)]198#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
199pub struct ProjectSnapshot {199pub struct ProjectSnapshot {
200 pub worktree_snapshots: Vec<WorktreeSnapshot>,200 pub worktree_snapshots: Vec<WorktreeSnapshot>,
201 pub unsaved_buffer_paths: Vec<String>,201 pub unsaved_buffer_paths: Vec<String>,
202 pub timestamp: DateTime<Utc>,202 pub timestamp: DateTime<Utc>,
203}203}
204 204
205#[derive(Debug, Clone, Serialize, Deserialize)]205#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
206pub struct WorktreeSnapshot {206pub struct WorktreeSnapshot {
207 pub worktree_path: String,207 pub worktree_path: String,
208 pub git_state: Option<GitState>,208 pub git_state: Option<GitState>,
209}209}
210 210
211#[derive(Debug, Clone, Serialize, Deserialize)]211#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
212pub struct GitState {212pub struct GitState {
213 pub remote_url: Option<String>,213 pub remote_url: Option<String>,
214 pub head_sha: Option<String>,214 pub head_sha: Option<String>,
@@ -247,7 +247,7 @@ impl LastRestoreCheckpoint { · 32 unmodified lines
247 }247 }
248}248}
249 249
250#[derive(Clone, Debug, Default, Serialize, Deserialize)]250#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
251pub enum DetailedSummaryState {251pub enum DetailedSummaryState {
252 #[default]252 #[default]
253 NotGenerated,253 NotGenerated,
@@ -391,7 +391,7 @@ impl ThreadSummary { · 137 unmodified lines
391 }391 }
392}392}
393 393
394#[derive(Debug, Clone, Serialize, Deserialize)]394#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
395pub struct ExceededWindowError {395pub struct ExceededWindowError {
396 /// Model used when last message exceeded context window396 /// Model used when last message exceeded context window
397 model_id: LanguageModelId,397 model_id: LanguageModelId,
@@ -1563,6 +1563,9 @@ impl Thread { · 1165 unmodified lines
1563 Err(LanguageModelCompletionError::Other(error)) => {1563 Err(LanguageModelCompletionError::Other(error)) => {
1564 return Err(error);1564 return Err(error);
1565 }1565 }
1566 Err(err @ LanguageModelCompletionError::RateLimit(..)) => {
1567 return Err(err.into());
1568 }
1566 };1569 };
1567 1570
1568 match event {1571 match event {
crates/agent/src/thread_history.rs+3−2
@@ -594,10 +594,11 @@ impl Render for ThreadHistory { · 593 unmodified lines
594 view.pr_5()594 view.pr_5()
595 .child(595 .child(
596 uniform_list(596 uniform_list(
597 cx.entity().clone(),
598 "thread-history",597 "thread-history",
599 self.list_item_count(),598 self.list_item_count(),
600 Self::list_items,599 cx.processor(|this, range: Range<usize>, window, cx| {
600 this.list_items(range, window, cx)
601 }),
601 )602 )
602 .p_1()603 .p_1()
603 .track_scroll(self.scroll_handle.clone())604 .track_scroll(self.scroll_handle.clone())
crates/agent/src/thread_store.rs+208−27
@@ -89,7 +89,7 @@ pub fn init(cx: &mut App) { · 88 unmodified lines
89pub struct SharedProjectContext(Rc<RefCell<Option<ProjectContext>>>);89pub struct SharedProjectContext(Rc<RefCell<Option<ProjectContext>>>);
90 90
91impl SharedProjectContext {91impl SharedProjectContext {
92 pub fn borrow(&self) -> Ref<Option<ProjectContext>> {92 pub fn borrow(&self) -> Ref<'_, Option<ProjectContext>> {
93 self.0.borrow()93 self.0.borrow()
94 }94 }
95}95}
@@ -305,17 +305,19 @@ impl ThreadStore { · 209 unmodified lines
305 project: Entity<Project>,305 project: Entity<Project>,
306 cx: &mut App,306 cx: &mut App,
307 ) -> Task<(WorktreeContext, Option<RulesLoadingError>)> {307 ) -> Task<(WorktreeContext, Option<RulesLoadingError>)> {
308 let root_name = worktree.read(cx).root_name().into();308 let tree = worktree.read(cx);
309 let root_name = tree.root_name().into();
310 let abs_path = tree.abs_path();
311
312 let mut context = WorktreeContext {
313 root_name,
314 abs_path,
315 rules_file: None,
316 };
309 317
310 let rules_task = Self::load_worktree_rules_file(worktree, project, cx);318 let rules_task = Self::load_worktree_rules_file(worktree, project, cx);
311 let Some(rules_task) = rules_task else {319 let Some(rules_task) = rules_task else {
312 return Task::ready((320 return Task::ready((context, None));
313 WorktreeContext {
314 root_name,
315 rules_file: None,
316 },
317 None,
318 ));
319 };321 };
320 322
321 cx.spawn(async move |_| {323 cx.spawn(async move |_| {
@@ -328,11 +330,8 @@ impl ThreadStore { · 6 unmodified lines
328 }),330 }),
329 ),331 ),
330 };332 };
331 let worktree_info = WorktreeContext {333 context.rules_file = rules_file;
332 root_name,334 (context, rules_file_error)
333 rules_file,
334 };
335 (worktree_info, rules_file_error)
336 })335 })
337 }336 }
338 337
@@ -341,12 +340,12 @@ impl ThreadStore { · 2 unmodified lines
341 project: Entity<Project>,340 project: Entity<Project>,
342 cx: &mut App,341 cx: &mut App,
343 ) -> Option<Task<Result<RulesFileContext>>> {342 ) -> Option<Task<Result<RulesFileContext>>> {
344 let worktree_ref = worktree.read(cx);343 let worktree = worktree.read(cx);
345 let worktree_id = worktree_ref.id();344 let worktree_id = worktree.id();
346 let selected_rules_file = RULES_FILE_NAMES345 let selected_rules_file = RULES_FILE_NAMES
347 .into_iter()346 .into_iter()
348 .filter_map(|name| {347 .filter_map(|name| {
349 worktree_ref348 worktree
350 .entry_for_path(name)349 .entry_for_path(name)
351 .filter(|entry| entry.is_file())350 .filter(|entry| entry.is_file())
352 .map(|entry| entry.path.clone())351 .map(|entry| entry.path.clone())
@@ -562,7 +561,7 @@ impl ThreadStore { · 209 unmodified lines
562 561
563 if protocol.capable(context_server::protocol::ServerCapability::Tools) {562 if protocol.capable(context_server::protocol::ServerCapability::Tools) {
564 if let Some(response) = protocol563 if let Some(response) = protocol
565 .request::<context_server::types::request::ListTools>(())564 .request::<context_server::types::requests::ListTools>(())
566 .await565 .await
567 .log_err()566 .log_err()
568 {567 {
@@ -603,7 +602,7 @@ pub struct SerializedThreadMetadata { · 34 unmodified lines
603 pub updated_at: DateTime<Utc>,602 pub updated_at: DateTime<Utc>,
604}603}
605 604
606#[derive(Serialize, Deserialize, Debug)]605#[derive(Serialize, Deserialize, Debug, PartialEq)]
607pub struct SerializedThread {606pub struct SerializedThread {
608 pub version: String,607 pub version: String,
609 pub summary: SharedString,608 pub summary: SharedString,
@@ -629,7 +628,7 @@ pub struct SerializedThread { · 19 unmodified lines
629 pub profile: Option<AgentProfileId>,628 pub profile: Option<AgentProfileId>,
630}629}
631 630
632#[derive(Serialize, Deserialize, Debug)]631#[derive(Serialize, Deserialize, Debug, PartialEq)]
633pub struct SerializedLanguageModel {632pub struct SerializedLanguageModel {
634 pub provider: String,633 pub provider: String,
635 pub model: String,634 pub model: String,
@@ -690,11 +689,15 @@ impl SerializedThreadV0_1_0 { · 54 unmodified lines
690 messages.push(message);689 messages.push(message);
691 }690 }
692 691
693 SerializedThread { messages, ..self.0 }692 SerializedThread {
693 messages,
694 version: SerializedThread::VERSION.to_string(),
695 ..self.0
696 }
694 }697 }
695}698}
696 699
697#[derive(Debug, Serialize, Deserialize)]700#[derive(Debug, Serialize, Deserialize, PartialEq)]
698pub struct SerializedMessage {701pub struct SerializedMessage {
699 pub id: MessageId,702 pub id: MessageId,
700 pub role: Role,703 pub role: Role,
@@ -712,7 +715,7 @@ pub struct SerializedMessage { · 11 unmodified lines
712 pub is_hidden: bool,715 pub is_hidden: bool,
713}716}
714 717
715#[derive(Debug, Serialize, Deserialize)]718#[derive(Debug, Serialize, Deserialize, PartialEq)]
716#[serde(tag = "type")]719#[serde(tag = "type")]
717pub enum SerializedMessageSegment {720pub enum SerializedMessageSegment {
718 #[serde(rename = "text")]721 #[serde(rename = "text")]
@@ -730,14 +733,14 @@ pub enum SerializedMessageSegment { · 11 unmodified lines
730 },733 },
731}734}
732 735
733#[derive(Debug, Serialize, Deserialize)]736#[derive(Debug, Serialize, Deserialize, PartialEq)]
734pub struct SerializedToolUse {737pub struct SerializedToolUse {
735 pub id: LanguageModelToolUseId,738 pub id: LanguageModelToolUseId,
736 pub name: SharedString,739 pub name: SharedString,
737 pub input: serde_json::Value,740 pub input: serde_json::Value,
738}741}
739 742
740#[derive(Debug, Serialize, Deserialize)]743#[derive(Debug, Serialize, Deserialize, PartialEq)]
741pub struct SerializedToolResult {744pub struct SerializedToolResult {
742 pub tool_use_id: LanguageModelToolUseId,745 pub tool_use_id: LanguageModelToolUseId,
743 pub is_error: bool,746 pub is_error: bool,
@@ -800,7 +803,7 @@ impl LegacySerializedMessage { · 56 unmodified lines
800 }803 }
801}804}
802 805
803#[derive(Debug, Serialize, Deserialize)]806#[derive(Debug, Serialize, Deserialize, PartialEq)]
804pub struct SerializedCrease {807pub struct SerializedCrease {
805 pub start: usize,808 pub start: usize,
806 pub end: usize,809 pub end: usize,
@@ -919,7 +922,7 @@ impl ThreadsDatabase { · 112 unmodified lines
919 922
920 fn bytes_encode(923 fn bytes_encode(
921 item: &Self::EItem,924 item: &Self::EItem,
922 ) -> Result<std::borrow::Cow<[u8]>, heed::BoxedError> {925 ) -> Result<std::borrow::Cow<'_, [u8]>, heed::BoxedError> {
923 serde_json::to_vec(&item.0)926 serde_json::to_vec(&item.0)
924 .map(std::borrow::Cow::Owned)927 .map(std::borrow::Cow::Owned)
925 .map_err(Into::into)928 .map_err(Into::into)
@@ -1057,3 +1060,181 @@ impl ThreadsDatabase { · 131 unmodified lines
1057 })1060 })
1058 }1061 }
1059}1062}
1063
1064#[cfg(test)]
1065mod tests {
1066 use super::*;
1067 use crate::thread::{DetailedSummaryState, MessageId};
1068 use chrono::Utc;
1069 use language_model::{Role, TokenUsage};
1070 use pretty_assertions::assert_eq;
1071
1072 #[test]
1073 fn test_legacy_serialized_thread_upgrade() {
1074 let updated_at = Utc::now();
1075 let legacy_thread = LegacySerializedThread {
1076 summary: "Test conversation".into(),
1077 updated_at,
1078 messages: vec![LegacySerializedMessage {
1079 id: MessageId(1),
1080 role: Role::User,
1081 text: "Hello, world!".to_string(),
1082 tool_uses: vec![],
1083 tool_results: vec![],
1084 }],
1085 initial_project_snapshot: None,
1086 };
1087
1088 let upgraded = legacy_thread.upgrade();
1089
1090 assert_eq!(
1091 upgraded,
1092 SerializedThread {
1093 summary: "Test conversation".into(),
1094 updated_at,
1095 messages: vec![SerializedMessage {
1096 id: MessageId(1),
1097 role: Role::User,
1098 segments: vec![SerializedMessageSegment::Text {
1099 text: "Hello, world!".to_string()
1100 }],
1101 tool_uses: vec![],
1102 tool_results: vec![],
1103 context: "".to_string(),
1104 creases: vec![],
1105 is_hidden: false
1106 }],
1107 version: SerializedThread::VERSION.to_string(),
1108 initial_project_snapshot: None,
1109 cumulative_token_usage: TokenUsage::default(),
1110 request_token_usage: vec![],
1111 detailed_summary_state: DetailedSummaryState::default(),
1112 exceeded_window_error: None,
1113 model: None,
1114 completion_mode: None,
1115 tool_use_limit_reached: false,
1116 profile: None
1117 }
1118 )
1119 }
1120
1121 #[test]
1122 fn test_serialized_threadv0_1_0_upgrade() {
1123 let updated_at = Utc::now();
1124 let thread_v0_1_0 = SerializedThreadV0_1_0(SerializedThread {
1125 summary: "Test conversation".into(),
1126 updated_at,
1127 messages: vec![
1128 SerializedMessage {
1129 id: MessageId(1),
1130 role: Role::User,
1131 segments: vec![SerializedMessageSegment::Text {
1132 text: "Use tool_1".to_string(),
1133 }],
1134 tool_uses: vec![],
1135 tool_results: vec![],
1136 context: "".to_string(),
1137 creases: vec![],
1138 is_hidden: false,
1139 },
1140 SerializedMessage {
1141 id: MessageId(2),
1142 role: Role::Assistant,
1143 segments: vec![SerializedMessageSegment::Text {
1144 text: "I want to use a tool".to_string(),
1145 }],
1146 tool_uses: vec![SerializedToolUse {
1147 id: "abc".into(),
1148 name: "tool_1".into(),
1149 input: serde_json::Value::Null,
1150 }],
1151 tool_results: vec![],
1152 context: "".to_string(),
1153 creases: vec![],
1154 is_hidden: false,
1155 },
1156 SerializedMessage {
1157 id: MessageId(1),
1158 role: Role::User,
1159 segments: vec![SerializedMessageSegment::Text {
1160 text: "Here is the tool result".to_string(),
1161 }],
1162 tool_uses: vec![],
1163 tool_results: vec![SerializedToolResult {
1164 tool_use_id: "abc".into(),
1165 is_error: false,
1166 content: LanguageModelToolResultContent::Text("abcdef".into()),
1167 output: Some(serde_json::Value::Null),
1168 }],
1169 context: "".to_string(),
1170 creases: vec![],
1171 is_hidden: false,
1172 },
1173 ],
1174 version: SerializedThreadV0_1_0::VERSION.to_string(),
1175 initial_project_snapshot: None,
1176 cumulative_token_usage: TokenUsage::default(),
1177 request_token_usage: vec![],
1178 detailed_summary_state: DetailedSummaryState::default(),
1179 exceeded_window_error: None,
1180 model: None,
1181 completion_mode: None,
1182 tool_use_limit_reached: false,
1183 profile: None,
1184 });
1185 let upgraded = thread_v0_1_0.upgrade();
1186
1187 assert_eq!(
1188 upgraded,
1189 SerializedThread {
1190 summary: "Test conversation".into(),
1191 updated_at,
1192 messages: vec![
1193 SerializedMessage {
1194 id: MessageId(1),
1195 role: Role::User,
1196 segments: vec![SerializedMessageSegment::Text {
1197 text: "Use tool_1".to_string()
1198 }],
1199 tool_uses: vec![],
1200 tool_results: vec![],
1201 context: "".to_string(),
1202 creases: vec![],
1203 is_hidden: false
1204 },
1205 SerializedMessage {
1206 id: MessageId(2),
1207 role: Role::Assistant,
1208 segments: vec![SerializedMessageSegment::Text {
1209 text: "I want to use a tool".to_string(),
1210 }],
1211 tool_uses: vec![SerializedToolUse {
1212 id: "abc".into(),
1213 name: "tool_1".into(),
1214 input: serde_json::Value::Null,
1215 }],
1216 tool_results: vec![SerializedToolResult {
1217 tool_use_id: "abc".into(),
1218 is_error: false,
1219 content: LanguageModelToolResultContent::Text("abcdef".into()),
1220 output: Some(serde_json::Value::Null),
1221 }],
1222 context: "".to_string(),
1223 creases: vec![],
1224 is_hidden: false,
1225 },
1226 ],
1227 version: SerializedThread::VERSION.to_string(),
1228 initial_project_snapshot: None,
1229 cumulative_token_usage: TokenUsage::default(),
1230 request_token_usage: vec![],
1231 detailed_summary_state: DetailedSummaryState::default(),
1232 exceeded_window_error: None,
1233 model: None,
1234 completion_mode: None,
1235 tool_use_limit_reached: false,
1236 profile: None
1237 }
1238 )
1239 }
1240}
crates/agent/src/ui/preview/usage_callouts.rs+19−14
@@ -2,7 +2,7 @@ use client::zed_urls; · 1 unmodified line
2use component::{empty_example, example_group_with_title, single_example};2use component::{empty_example, example_group_with_title, single_example};
3use gpui::{AnyElement, App, IntoElement, RenderOnce, Window};3use gpui::{AnyElement, App, IntoElement, RenderOnce, Window};
4use language_model::RequestUsage;4use language_model::RequestUsage;
5use ui::{Callout, Color, Icon, IconName, IconSize, prelude::*};5use ui::{Callout, prelude::*};
6use zed_llm_client::{Plan, UsageLimit};6use zed_llm_client::{Plan, UsageLimit};
7 7
8#[derive(IntoElement, RegisterComponent)]8#[derive(IntoElement, RegisterComponent)]
@@ -91,16 +91,23 @@ impl RenderOnce for UsageCallout { · 82 unmodified lines
91 .size(IconSize::XSmall)91 .size(IconSize::XSmall)
92 };92 };
93 93
94 Callout::multi_line(94 div()
95 title,95 .border_t_1()
96 message,96 .border_color(cx.theme().colors().border)
97 icon,97 .child(
98 button_text,98 Callout::new()
99 Box::new(move |_, _, cx| {99 .icon(icon)
100 cx.open_url(&url);100 .title(title)
101 }),101 .description(message)
102 )102 .primary_action(
103 .into_any_element()103 Button::new("upgrade", button_text)
104 .label_size(LabelSize::Small)
105 .on_click(move |_, _, cx| {
106 cx.open_url(&url);
107 }),
108 ),
109 )
110 .into_any_element()
104 }111 }
105}112}
106 113
@@ -189,10 +196,8 @@ impl Component for UsageCallout { · 82 unmodified lines
189 );196 );
190 197
191 Some(198 Some(
192 div()199 v_flex()
193 .p_4()200 .p_4()
194 .flex()
195 .flex_col()
196 .gap_4()201 .gap_4()
197 .child(free_examples)202 .child(free_examples)
198 .child(trial_examples)203 .child(trial_examples)
crates/anthropic/src/anthropic.rs+103−71
@@ -1,4 +1,5 @@
1use std::str::FromStr;1use std::str::FromStr;
2use std::time::Duration;
2 3
3use anyhow::{Context as _, Result, anyhow};4use anyhow::{Context as _, Result, anyhow};
4use chrono::{DateTime, Utc};5use chrono::{DateTime, Utc};
@@ -32,15 +33,6 @@ pub enum AnthropicModelMode { · 27 unmodified lines
32#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]33#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
33#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, EnumIter)]34#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, EnumIter)]
34pub enum Model {35pub enum Model {
35 #[serde(rename = "claude-3-5-sonnet", alias = "claude-3-5-sonnet-latest")]
36 Claude3_5Sonnet,
37 #[serde(rename = "claude-3-7-sonnet", alias = "claude-3-7-sonnet-latest")]
38 Claude3_7Sonnet,
39 #[serde(
40 rename = "claude-3-7-sonnet-thinking",
41 alias = "claude-3-7-sonnet-thinking-latest"
42 )]
43 Claude3_7SonnetThinking,
44 #[serde(rename = "claude-opus-4", alias = "claude-opus-4-latest")]36 #[serde(rename = "claude-opus-4", alias = "claude-opus-4-latest")]
45 ClaudeOpus4,37 ClaudeOpus4,
46 #[serde(38 #[serde(
@@ -56,6 +48,15 @@ pub enum Model { · 9 unmodified lines
56 alias = "claude-sonnet-4-thinking-latest"48 alias = "claude-sonnet-4-thinking-latest"
57 )]49 )]
58 ClaudeSonnet4Thinking,50 ClaudeSonnet4Thinking,
51 #[serde(rename = "claude-3-7-sonnet", alias = "claude-3-7-sonnet-latest")]
52 Claude3_7Sonnet,
53 #[serde(
54 rename = "claude-3-7-sonnet-thinking",
55 alias = "claude-3-7-sonnet-thinking-latest"
56 )]
57 Claude3_7SonnetThinking,
58 #[serde(rename = "claude-3-5-sonnet", alias = "claude-3-5-sonnet-latest")]
59 Claude3_5Sonnet,
59 #[serde(rename = "claude-3-5-haiku", alias = "claude-3-5-haiku-latest")]60 #[serde(rename = "claude-3-5-haiku", alias = "claude-3-5-haiku-latest")]
60 Claude3_5Haiku,61 Claude3_5Haiku,
61 #[serde(rename = "claude-3-opus", alias = "claude-3-opus-latest")]62 #[serde(rename = "claude-3-opus", alias = "claude-3-opus-latest")]
@@ -89,46 +90,66 @@ impl Model { · 27 unmodified lines
89 }90 }
90 91
91 pub fn from_id(id: &str) -> Result<Self> {92 pub fn from_id(id: &str) -> Result<Self> {
93 if id.starts_with("claude-opus-4-thinking") {
94 return Ok(Self::ClaudeOpus4Thinking);
95 }
96
97 if id.starts_with("claude-opus-4") {
98 return Ok(Self::ClaudeOpus4);
99 }
100
101 if id.starts_with("claude-sonnet-4-thinking") {
102 return Ok(Self::ClaudeSonnet4Thinking);
103 }
104
105 if id.starts_with("claude-sonnet-4") {
106 return Ok(Self::ClaudeSonnet4);
107 }
108
109 if id.starts_with("claude-3-7-sonnet-thinking") {
110 return Ok(Self::Claude3_7SonnetThinking);
111 }
112
113 if id.starts_with("claude-3-7-sonnet") {
114 return Ok(Self::Claude3_7Sonnet);
115 }
116
92 if id.starts_with("claude-3-5-sonnet") {117 if id.starts_with("claude-3-5-sonnet") {
93 Ok(Self::Claude3_5Sonnet)118 return Ok(Self::Claude3_5Sonnet);
94 } else if id.starts_with("claude-3-7-sonnet-thinking") {119 }
95 Ok(Self::Claude3_7SonnetThinking)120
96 } else if id.starts_with("claude-3-7-sonnet") {121 if id.starts_with("claude-3-5-haiku") {
97 Ok(Self::Claude3_7Sonnet)122 return Ok(Self::Claude3_5Haiku);
98 } else if id.starts_with("claude-3-5-haiku") {123 }
99 Ok(Self::Claude3_5Haiku)124
100 } else if id.starts_with("claude-3-opus") {125 if id.starts_with("claude-3-opus") {
101 Ok(Self::Claude3Opus)126 return Ok(Self::Claude3Opus);
102 } else if id.starts_with("claude-3-sonnet") {
103 Ok(Self::Claude3Sonnet)
104 } else if id.starts_with("claude-3-haiku") {
105 Ok(Self::Claude3Haiku)
106 } else if id.starts_with("claude-opus-4-thinking") {
107 Ok(Self::ClaudeOpus4Thinking)
108 } else if id.starts_with("claude-opus-4") {
109 Ok(Self::ClaudeOpus4)
110 } else if id.starts_with("claude-sonnet-4-thinking") {
111 Ok(Self::ClaudeSonnet4Thinking)
112 } else if id.starts_with("claude-sonnet-4") {
113 Ok(Self::ClaudeSonnet4)
114 } else {
115 anyhow::bail!("invalid model id {id}");
116 }127 }
128
129 if id.starts_with("claude-3-sonnet") {
130 return Ok(Self::Claude3Sonnet);
131 }
132
133 if id.starts_with("claude-3-haiku") {
134 return Ok(Self::Claude3Haiku);
135 }
136
137 Err(anyhow!("invalid model ID: {id}"))
117 }138 }
118 139
119 pub fn id(&self) -> &str {140 pub fn id(&self) -> &str {
120 match self {141 match self {
121 Model::ClaudeOpus4 => "claude-opus-4-latest",142 Self::ClaudeOpus4 => "claude-opus-4-latest",
122 Model::ClaudeOpus4Thinking => "claude-opus-4-thinking-latest",143 Self::ClaudeOpus4Thinking => "claude-opus-4-thinking-latest",
123 Model::ClaudeSonnet4 => "claude-sonnet-4-latest",144 Self::ClaudeSonnet4 => "claude-sonnet-4-latest",
124 Model::ClaudeSonnet4Thinking => "claude-sonnet-4-thinking-latest",145 Self::ClaudeSonnet4Thinking => "claude-sonnet-4-thinking-latest",
125 Model::Claude3_5Sonnet => "claude-3-5-sonnet-latest",146 Self::Claude3_5Sonnet => "claude-3-5-sonnet-latest",
126 Model::Claude3_7Sonnet => "claude-3-7-sonnet-latest",147 Self::Claude3_7Sonnet => "claude-3-7-sonnet-latest",
127 Model::Claude3_7SonnetThinking => "claude-3-7-sonnet-thinking-latest",148 Self::Claude3_7SonnetThinking => "claude-3-7-sonnet-thinking-latest",
128 Model::Claude3_5Haiku => "claude-3-5-haiku-latest",149 Self::Claude3_5Haiku => "claude-3-5-haiku-latest",
129 Model::Claude3Opus => "claude-3-opus-latest",150 Self::Claude3Opus => "claude-3-opus-latest",
130 Model::Claude3Sonnet => "claude-3-sonnet-20240229",151 Self::Claude3Sonnet => "claude-3-sonnet-20240229",
131 Model::Claude3Haiku => "claude-3-haiku-20240307",152 Self::Claude3Haiku => "claude-3-haiku-20240307",
132 Self::Custom { name, .. } => name,153 Self::Custom { name, .. } => name,
133 }154 }
134 }155 }
@@ -136,24 +157,24 @@ impl Model { · 1 unmodified line
136 /// The id of the model that should be used for making API requests157 /// The id of the model that should be used for making API requests
137 pub fn request_id(&self) -> &str {158 pub fn request_id(&self) -> &str {
138 match self {159 match self {
139 Model::ClaudeOpus4 | Model::ClaudeOpus4Thinking => "claude-opus-4-20250514",160 Self::ClaudeOpus4 | Self::ClaudeOpus4Thinking => "claude-opus-4-20250514",
140 Model::ClaudeSonnet4 | Model::ClaudeSonnet4Thinking => "claude-sonnet-4-20250514",161 Self::ClaudeSonnet4 | Self::ClaudeSonnet4Thinking => "claude-sonnet-4-20250514",
141 Model::Claude3_5Sonnet => "claude-3-5-sonnet-latest",162 Self::Claude3_5Sonnet => "claude-3-5-sonnet-latest",
142 Model::Claude3_7Sonnet | Model::Claude3_7SonnetThinking => "claude-3-7-sonnet-latest",163 Self::Claude3_7Sonnet | Self::Claude3_7SonnetThinking => "claude-3-7-sonnet-latest",
143 Model::Claude3_5Haiku => "claude-3-5-haiku-latest",164 Self::Claude3_5Haiku => "claude-3-5-haiku-latest",
144 Model::Claude3Opus => "claude-3-opus-latest",165 Self::Claude3Opus => "claude-3-opus-latest",
145 Model::Claude3Sonnet => "claude-3-sonnet-20240229",166 Self::Claude3Sonnet => "claude-3-sonnet-20240229",
146 Model::Claude3Haiku => "claude-3-haiku-20240307",167 Self::Claude3Haiku => "claude-3-haiku-20240307",
147 Self::Custom { name, .. } => name,168 Self::Custom { name, .. } => name,
148 }169 }
149 }170 }
150 171
151 pub fn display_name(&self) -> &str {172 pub fn display_name(&self) -> &str {
152 match self {173 match self {
153 Model::ClaudeOpus4 => "Claude Opus 4",174 Self::ClaudeOpus4 => "Claude Opus 4",
154 Model::ClaudeOpus4Thinking => "Claude Opus 4 Thinking",175 Self::ClaudeOpus4Thinking => "Claude Opus 4 Thinking",
155 Model::ClaudeSonnet4 => "Claude Sonnet 4",176 Self::ClaudeSonnet4 => "Claude Sonnet 4",
156 Model::ClaudeSonnet4Thinking => "Claude Sonnet 4 Thinking",177 Self::ClaudeSonnet4Thinking => "Claude Sonnet 4 Thinking",
157 Self::Claude3_7Sonnet => "Claude 3.7 Sonnet",178 Self::Claude3_7Sonnet => "Claude 3.7 Sonnet",
158 Self::Claude3_5Sonnet => "Claude 3.5 Sonnet",179 Self::Claude3_5Sonnet => "Claude 3.5 Sonnet",
159 Self::Claude3_7SonnetThinking => "Claude 3.7 Sonnet Thinking",180 Self::Claude3_7SonnetThinking => "Claude 3.7 Sonnet Thinking",
@@ -209,15 +230,15 @@ impl Model { · 49 unmodified lines
209 230
210 pub fn max_output_tokens(&self) -> u32 {231 pub fn max_output_tokens(&self) -> u32 {
211 match self {232 match self {
212 Self::Claude3Opus | Self::Claude3Sonnet | Self::Claude3Haiku => 4_096,233 Self::ClaudeOpus4
213 Self::Claude3_5Sonnet
214 | Self::Claude3_7Sonnet
215 | Self::Claude3_7SonnetThinking
216 | Self::Claude3_5Haiku
217 | Self::ClaudeOpus4
218 | Self::ClaudeOpus4Thinking234 | Self::ClaudeOpus4Thinking
219 | Self::ClaudeSonnet4235 | Self::ClaudeSonnet4
220 | Self::ClaudeSonnet4Thinking => 8_192,236 | Self::ClaudeSonnet4Thinking
237 | Self::Claude3_5Sonnet
238 | Self::Claude3_7Sonnet
239 | Self::Claude3_7SonnetThinking
240 | Self::Claude3_5Haiku => 8_192,
241 Self::Claude3Opus | Self::Claude3Sonnet | Self::Claude3Haiku => 4_096,
221 Self::Custom {242 Self::Custom {
222 max_output_tokens, ..243 max_output_tokens, ..
223 } => max_output_tokens.unwrap_or(4_096),244 } => max_output_tokens.unwrap_or(4_096),
@@ -246,17 +267,17 @@ impl Model { · 22 unmodified lines
246 267
247 pub fn mode(&self) -> AnthropicModelMode {268 pub fn mode(&self) -> AnthropicModelMode {
248 match self {269 match self {
249 Self::Claude3_5Sonnet270 Self::ClaudeOpus4
271 | Self::ClaudeSonnet4
272 | Self::Claude3_5Sonnet
250 | Self::Claude3_7Sonnet273 | Self::Claude3_7Sonnet
251 | Self::Claude3_5Haiku274 | Self::Claude3_5Haiku
252 | Self::ClaudeOpus4
253 | Self::ClaudeSonnet4
254 | Self::Claude3Opus275 | Self::Claude3Opus
255 | Self::Claude3Sonnet276 | Self::Claude3Sonnet
256 | Self::Claude3Haiku => AnthropicModelMode::Default,277 | Self::Claude3Haiku => AnthropicModelMode::Default,
257 Self::Claude3_7SonnetThinking278 Self::ClaudeOpus4Thinking
258 | Self::ClaudeOpus4Thinking279 | Self::ClaudeSonnet4Thinking
259 | Self::ClaudeSonnet4Thinking => AnthropicModelMode::Thinking {280 | Self::Claude3_7SonnetThinking => AnthropicModelMode::Thinking {
260 budget_tokens: Some(4_096),281 budget_tokens: Some(4_096),
261 },282 },
262 Self::Custom { mode, .. } => mode.clone(),283 Self::Custom { mode, .. } => mode.clone(),
@@ -267,7 +288,7 @@ impl Model { · 4 unmodified lines
267 288
268 pub fn beta_headers(&self) -> String {289 pub fn beta_headers(&self) -> String {
269 let mut headers = Self::DEFAULT_BETA_HEADERS290 let mut headers = Self::DEFAULT_BETA_HEADERS
270 .into_iter()291 .iter()
271 .map(|header| header.to_string())292 .map(|header| header.to_string())
272 .collect::<Vec<_>>();293 .collect::<Vec<_>>();
273 294
@@ -406,6 +427,7 @@ impl RateLimit { · 132 unmodified lines
406/// <https://docs.anthropic.com/en/api/rate-limits#response-headers>427/// <https://docs.anthropic.com/en/api/rate-limits#response-headers>
407#[derive(Debug)]428#[derive(Debug)]
408pub struct RateLimitInfo {429pub struct RateLimitInfo {
430 pub retry_after: Option<Duration>,
409 pub requests: Option<RateLimit>,431 pub requests: Option<RateLimit>,
410 pub tokens: Option<RateLimit>,432 pub tokens: Option<RateLimit>,
411 pub input_tokens: Option<RateLimit>,433 pub input_tokens: Option<RateLimit>,
@@ -417,10 +439,11 @@ impl RateLimitInfo { · 5 unmodified lines
417 // Check if any rate limit headers exist439 // Check if any rate limit headers exist
418 let has_rate_limit_headers = headers440 let has_rate_limit_headers = headers
419 .keys()441 .keys()
420 .any(|k| k.as_str().starts_with("anthropic-ratelimit-"));442 .any(|k| k == "retry-after" || k.as_str().starts_with("anthropic-ratelimit-"));
421 443
422 if !has_rate_limit_headers {444 if !has_rate_limit_headers {
423 return Self {445 return Self {
446 retry_after: None,
424 requests: None,447 requests: None,
425 tokens: None,448 tokens: None,
426 input_tokens: None,449 input_tokens: None,
@@ -429,6 +452,11 @@ impl RateLimitInfo { · 2 unmodified lines
429 }452 }
430 453
431 Self {454 Self {
455 retry_after: headers
456 .get("retry-after")
457 .and_then(|v| v.to_str().ok())
458 .and_then(|v| v.parse::<u64>().ok())
459 .map(Duration::from_secs),
432 requests: RateLimit::from_headers("requests", headers).ok(),460 requests: RateLimit::from_headers("requests", headers).ok(),
433 tokens: RateLimit::from_headers("tokens", headers).ok(),461 tokens: RateLimit::from_headers("tokens", headers).ok(),
434 input_tokens: RateLimit::from_headers("input-tokens", headers).ok(),462 input_tokens: RateLimit::from_headers("input-tokens", headers).ok(),
@@ -481,8 +509,8 @@ pub async fn stream_completion_with_rate_limit_info( · 46 unmodified lines
481 .send(request)509 .send(request)
482 .await510 .await
483 .context("failed to send request to Anthropic")?;511 .context("failed to send request to Anthropic")?;
512 let rate_limits = RateLimitInfo::from_headers(response.headers());
484 if response.status().is_success() {513 if response.status().is_success() {
485 let rate_limits = RateLimitInfo::from_headers(response.headers());
486 let reader = BufReader::new(response.into_body());514 let reader = BufReader::new(response.into_body());
487 let stream = reader515 let stream = reader
488 .lines()516 .lines()
@@ -500,6 +528,8 @@ pub async fn stream_completion_with_rate_limit_info( · 11 unmodified lines
500 })528 })
501 .boxed();529 .boxed();
502 Ok((stream, Some(rate_limits)))530 Ok((stream, Some(rate_limits)))
531 } else if let Some(retry_after) = rate_limits.retry_after {
532 Err(AnthropicError::RateLimit(retry_after))
503 } else {533 } else {
504 let mut body = Vec::new();534 let mut body = Vec::new();
505 response535 response
@@ -769,6 +799,8 @@ pub struct MessageDelta { · 263 unmodified lines
769 799
770#[derive(Error, Debug)]800#[derive(Error, Debug)]
771pub enum AnthropicError {801pub enum AnthropicError {
802 #[error("rate limit exceeded, retry after {0:?}")]
803 RateLimit(Duration),
772 #[error("an error occurred while interacting with the Anthropic API: {error_type}: {message}", error_type = .0.error_type, message = .0.message)]804 #[error("an error occurred while interacting with the Anthropic API: {error_type}: {message}", error_type = .0.error_type, message = .0.message)]
773 ApiError(ApiError),805 ApiError(ApiError),
774 #[error("{0}")]806 #[error("{0}")]
crates/askpass/Cargo.toml+0−1
@@ -15,7 +15,6 @@ path = "src/askpass.rs" · 14 unmodified lines
15anyhow.workspace = true15anyhow.workspace = true
16futures.workspace = true16futures.workspace = true
17gpui.workspace = true17gpui.workspace = true
18shlex.workspace = true
19smol.workspace = true18smol.workspace = true
20tempfile.workspace = true19tempfile.workspace = true
21util.workspace = true20util.workspace = true
crates/askpass/src/askpass.rs+2−32
@@ -16,6 +16,8 @@ use smol::fs; · 15 unmodified lines
16use smol::{fs::unix::PermissionsExt as _, net::unix::UnixListener};16use smol::{fs::unix::PermissionsExt as _, net::unix::UnixListener};
17#[cfg(unix)]17#[cfg(unix)]
18use util::ResultExt as _;18use util::ResultExt as _;
19#[cfg(unix)]
20use util::get_shell_safe_zed_path;
19 21
20#[derive(PartialEq, Eq)]22#[derive(PartialEq, Eq)]
21pub enum AskPassResult {23pub enum AskPassResult {
@@ -160,38 +162,6 @@ impl AskPassSession { · 138 unmodified lines
160 }162 }
161}163}
162 164
163#[cfg(unix)]
164fn get_shell_safe_zed_path() -> anyhow::Result<String> {
165 let zed_path = std::env::current_exe()
166 .context("Failed to determine current executable path for use in askpass")?
167 .to_string_lossy()
168 // see https://github.com/rust-lang/rust/issues/69343
169 .trim_end_matches(" (deleted)")
170 .to_string();
171
172 // NOTE: this was previously enabled, however, it caused errors when it shouldn't have
173 // (see https://github.com/zed-industries/zed/issues/29819)
174 // The zed path failing to execute within the askpass script results in very vague ssh
175 // authentication failed errors, so this was done to try and surface a better error
176 //
177 // use std::os::unix::fs::MetadataExt;
178 // let metadata = std::fs::metadata(&zed_path)
179 // .context("Failed to check metadata of Zed executable path for use in askpass")?;
180 // let is_executable = metadata.is_file() && metadata.mode() & 0o111 != 0;
181 // anyhow::ensure!(
182 // is_executable,
183 // "Failed to verify Zed executable path for use in askpass"
184 // );
185
186 // As of writing, this can only be fail if the path contains a null byte, which shouldn't be possible
187 // but shlex has annotated the error as #[non_exhaustive] so we can't make it a compile error if other
188 // errors are introduced in the future :(
189 let zed_path_escaped = shlex::try_quote(&zed_path)
190 .context("Failed to shell-escape Zed executable path for use in askpass")?;
191
192 return Ok(zed_path_escaped.to_string());
193}
194
195/// The main function for when Zed is running in netcat mode for use in askpass.165/// The main function for when Zed is running in netcat mode for use in askpass.
196/// Called from both the remote server binary and the zed binary in their respective main functions.166/// Called from both the remote server binary and the zed binary in their respective main functions.
197#[cfg(unix)]167#[cfg(unix)]
crates/assistant_context_editor/src/context_editor.rs+19−44
@@ -39,7 +39,7 @@ use language::{ · 38 unmodified lines
39 language_settings::{SoftWrap, all_language_settings},39 language_settings::{SoftWrap, all_language_settings},
40};40};
41use language_model::{41use language_model::{
42 LanguageModelImage, LanguageModelProvider, LanguageModelProviderTosView, LanguageModelRegistry,42 ConfigurationError, LanguageModelImage, LanguageModelProviderTosView, LanguageModelRegistry,
43 Role,43 Role,
44};44};
45use multi_buffer::MultiBufferRow;45use multi_buffer::MultiBufferRow;
@@ -1887,6 +1887,8 @@ impl ContextEditor { · 1841 unmodified lines
1887 // value to not show the nudge.1887 // value to not show the nudge.
1888 let nudge = Some(false);1888 let nudge = Some(false);
1889 1889
1890 let model_registry = LanguageModelRegistry::read_global(cx);
1891
1890 if nudge.map_or(false, |value| value) {1892 if nudge.map_or(false, |value| value) {
1891 Some(1893 Some(
1892 h_flex()1894 h_flex()
@@ -1935,14 +1937,9 @@ impl ContextEditor { · 42 unmodified lines
1935 )1937 )
1936 .into_any_element(),1938 .into_any_element(),
1937 )1939 )
1938 } else if let Some(configuration_error) = configuration_error(cx) {1940 } else if let Some(configuration_error) =
1939 let label = match configuration_error {1941 model_registry.configuration_error(model_registry.default_model(), cx)
1940 ConfigurationError::NoProvider => "No LLM provider selected.",1942 {
1941 ConfigurationError::ProviderNotAuthenticated => "LLM provider is not configured.",
1942 ConfigurationError::ProviderPendingTermsAcceptance(_) => {
1943 "LLM provider requires accepting the Terms of Service."
1944 }
1945 };
1946 Some(1943 Some(
1947 h_flex()1944 h_flex()
1948 .px_3()1945 .px_3()
@@ -1959,7 +1956,7 @@ impl ContextEditor { · 10 unmodified lines
1959 .size(IconSize::Small)1956 .size(IconSize::Small)
1960 .color(Color::Warning),1957 .color(Color::Warning),
1961 )1958 )
1962 .child(Label::new(label)),1959 .child(Label::new(configuration_error.to_string())),
1963 )1960 )
1964 .child(1961 .child(
1965 Button::new("open-configuration", "Configure Providers")1962 Button::new("open-configuration", "Configure Providers")
@@ -2034,14 +2031,19 @@ impl ContextEditor { · 68 unmodified lines
2034 /// Will return false if the selected provided has a configuration error or2031 /// Will return false if the selected provided has a configuration error or
2035 /// if the user has not accepted the terms of service for this provider.2032 /// if the user has not accepted the terms of service for this provider.
2036 fn sending_disabled(&self, cx: &mut Context<'_, ContextEditor>) -> bool {2033 fn sending_disabled(&self, cx: &mut Context<'_, ContextEditor>) -> bool {
2037 let model = LanguageModelRegistry::read_global(cx).default_model();2034 let model_registry = LanguageModelRegistry::read_global(cx);
2035 let Some(configuration_error) =
2036 model_registry.configuration_error(model_registry.default_model(), cx)
2037 else {
2038 return false;
2039 };
2038 2040
2039 let has_configuration_error = configuration_error(cx).is_some();2041 match configuration_error {
2040 let needs_to_accept_terms = self.show_accept_terms2042 ConfigurationError::NoProvider
2041 && model2043 | ConfigurationError::ModelNotFound
2042 .as_ref()2044 | ConfigurationError::ProviderNotAuthenticated(_) => true,
2043 .map_or(false, |model| model.provider.must_accept_terms(cx));2045 ConfigurationError::ProviderPendingTermsAcceptance(_) => self.show_accept_terms,
2044 has_configuration_error || needs_to_accept_terms2046 }
2045 }2047 }
2046 2048
2047 fn render_inject_context_menu(&self, cx: &mut Context<Self>) -> impl IntoElement {2049 fn render_inject_context_menu(&self, cx: &mut Context<Self>) -> impl IntoElement {
@@ -3180,33 +3182,6 @@ fn size_for_image(data: &RenderImage, max_size: Size<Pixels>) -> Size<Pixels> { · 1132 unmodified lines
3180 }3182 }
3181}3183}
3182 3184
3183pub enum ConfigurationError {
3184 NoProvider,
3185 ProviderNotAuthenticated,
3186 ProviderPendingTermsAcceptance(Arc<dyn LanguageModelProvider>),
3187}
3188
3189fn configuration_error(cx: &App) -> Option<ConfigurationError> {
3190 let model = LanguageModelRegistry::read_global(cx).default_model();
3191 let is_authenticated = model
3192 .as_ref()
3193 .map_or(false, |model| model.provider.is_authenticated(cx));
3194
3195 if model.is_some() && is_authenticated {
3196 return None;
3197 }
3198
3199 if model.is_none() {
3200 return Some(ConfigurationError::NoProvider);
3201 }
3202
3203 if !is_authenticated {
3204 return Some(ConfigurationError::ProviderNotAuthenticated);
3205 }
3206
3207 None
3208}
3209
3210pub fn humanize_token_count(count: usize) -> String {3185pub fn humanize_token_count(count: usize) -> String {
3211 match count {3186 match count {
3212 0..=999 => count.to_string(),3187 0..=999 => count.to_string(),
crates/assistant_context_editor/src/context_store.rs+1−1
@@ -869,7 +869,7 @@ impl ContextStore { · 868 unmodified lines
869 869
870 if protocol.capable(context_server::protocol::ServerCapability::Prompts) {870 if protocol.capable(context_server::protocol::ServerCapability::Prompts) {
871 if let Some(response) = protocol871 if let Some(response) = protocol
872 .request::<context_server::types::request::PromptsList>(())872 .request::<context_server::types::requests::PromptsList>(())
873 .await873 .await
874 .log_err()874 .log_err()
875 {875 {
crates/assistant_context_editor/src/language_model_selector.rs+3−2
@@ -682,11 +682,12 @@ mod tests { · 681 unmodified lines
682 _: &AsyncApp,682 _: &AsyncApp,
683 ) -> BoxFuture<683 ) -> BoxFuture<
684 'static,684 'static,
685 http_client::Result<685 Result<
686 BoxStream<686 BoxStream<
687 'static,687 'static,
688 http_client::Result<LanguageModelCompletionEvent, LanguageModelCompletionError>,688 Result<LanguageModelCompletionEvent, LanguageModelCompletionError>,
689 >,689 >,
690 LanguageModelCompletionError,
690 >,691 >,
691 > {692 > {
692 unimplemented!()693 unimplemented!()
crates/assistant_slash_commands/src/context_server_command.rs+2−2
@@ -87,7 +87,7 @@ impl SlashCommand for ContextServerSlashCommand { · 86 unmodified lines
87 let protocol = server.client().context("Context server not initialized")?;87 let protocol = server.client().context("Context server not initialized")?;
88 88
89 let response = protocol89 let response = protocol
90 .request::<context_server::types::request::CompletionComplete>(90 .request::<context_server::types::requests::CompletionComplete>(
91 context_server::types::CompletionCompleteParams {91 context_server::types::CompletionCompleteParams {
92 reference: context_server::types::CompletionReference::Prompt(92 reference: context_server::types::CompletionReference::Prompt(
93 context_server::types::PromptReference {93 context_server::types::PromptReference {
@@ -145,7 +145,7 @@ impl SlashCommand for ContextServerSlashCommand { · 51 unmodified lines
145 cx.foreground_executor().spawn(async move {145 cx.foreground_executor().spawn(async move {
146 let protocol = server.client().context("Context server not initialized")?;146 let protocol = server.client().context("Context server not initialized")?;
147 let response = protocol147 let response = protocol
148 .request::<context_server::types::request::PromptsGet>(148 .request::<context_server::types::requests::PromptsGet>(
149 context_server::types::PromptsGetParams {149 context_server::types::PromptsGetParams {
150 name: prompt_name.clone(),150 name: prompt_name.clone(),
151 arguments: Some(prompt_args),151 arguments: Some(prompt_args),
crates/assistant_slash_commands/src/file_command.rs+17−24
@@ -582,7 +582,7 @@ mod test { · 581 unmodified lines
582 use serde_json::json;582 use serde_json::json;
583 use settings::SettingsStore;583 use settings::SettingsStore;
584 use smol::stream::StreamExt;584 use smol::stream::StreamExt;
585 use util::{path, separator};585 use util::path;
586 586
587 use super::collect_files;587 use super::collect_files;
588 588
@@ -627,7 +627,7 @@ mod test { · 38 unmodified lines
627 .await627 .await
628 .unwrap();628 .unwrap();
629 629
630 assert!(result_1.text.starts_with(separator!("root/dir")));630 assert!(result_1.text.starts_with(path!("root/dir")));
631 // 4 files + 2 directories631 // 4 files + 2 directories
632 assert_eq!(result_1.sections.len(), 6);632 assert_eq!(result_1.sections.len(), 6);
633 633
@@ -643,7 +643,7 @@ mod test { · 9 unmodified lines
643 cx.update(|cx| collect_files(project.clone(), &["root/dir*".to_string()], cx).boxed());643 cx.update(|cx| collect_files(project.clone(), &["root/dir*".to_string()], cx).boxed());
644 let result = SlashCommandOutput::from_event_stream(result).await.unwrap();644 let result = SlashCommandOutput::from_event_stream(result).await.unwrap();
645 645
646 assert!(result.text.starts_with(separator!("root/dir")));646 assert!(result.text.starts_with(path!("root/dir")));
647 // 5 files + 2 directories647 // 5 files + 2 directories
648 assert_eq!(result.sections.len(), 7);648 assert_eq!(result.sections.len(), 7);
649 649
@@ -691,24 +691,20 @@ mod test { · 41 unmodified lines
691 .unwrap();691 .unwrap();
692 692
693 // Sanity check693 // Sanity check
694 assert!(result.text.starts_with(separator!("zed/assets/themes\n")));694 assert!(result.text.starts_with(path!("zed/assets/themes\n")));
695 assert_eq!(result.sections.len(), 7);695 assert_eq!(result.sections.len(), 7);
696 696
697 // Ensure that full file paths are included in the real output697 // Ensure that full file paths are included in the real output
698 assert!(698 assert!(
699 result699 result
700 .text700 .text
701 .contains(separator!("zed/assets/themes/andromeda/LICENSE"))701 .contains(path!("zed/assets/themes/andromeda/LICENSE"))
702 );702 );
703 assert!(result.text.contains(path!("zed/assets/themes/ayu/LICENSE")));
703 assert!(704 assert!(
704 result705 result
705 .text706 .text
706 .contains(separator!("zed/assets/themes/ayu/LICENSE"))707 .contains(path!("zed/assets/themes/summercamp/LICENSE"))
707 );
708 assert!(
709 result
710 .text
711 .contains(separator!("zed/assets/themes/summercamp/LICENSE"))
712 );708 );
713 709
714 assert_eq!(result.sections[5].label, "summercamp");710 assert_eq!(result.sections[5].label, "summercamp");
@@ -716,17 +712,17 @@ mod test { · 1 unmodified line
716 // Ensure that things are in descending order, with properly relativized paths712 // Ensure that things are in descending order, with properly relativized paths
717 assert_eq!(713 assert_eq!(
718 result.sections[0].label,714 result.sections[0].label,
719 separator!("zed/assets/themes/andromeda/LICENSE")715 path!("zed/assets/themes/andromeda/LICENSE")
720 );716 );
721 assert_eq!(result.sections[1].label, "andromeda");717 assert_eq!(result.sections[1].label, "andromeda");
722 assert_eq!(718 assert_eq!(
723 result.sections[2].label,719 result.sections[2].label,
724 separator!("zed/assets/themes/ayu/LICENSE")720 path!("zed/assets/themes/ayu/LICENSE")
725 );721 );
726 assert_eq!(result.sections[3].label, "ayu");722 assert_eq!(result.sections[3].label, "ayu");
727 assert_eq!(723 assert_eq!(
728 result.sections[4].label,724 result.sections[4].label,
729 separator!("zed/assets/themes/summercamp/LICENSE")725 path!("zed/assets/themes/summercamp/LICENSE")
730 );726 );
731 727
732 // Ensure that the project lasts until after the last await728 // Ensure that the project lasts until after the last await
@@ -767,31 +763,28 @@ mod test { · 34 unmodified lines
767 .await763 .await
768 .unwrap();764 .unwrap();
769 765
770 assert!(result.text.starts_with(separator!("zed/assets/themes\n")));766 assert!(result.text.starts_with(path!("zed/assets/themes\n")));
771 assert_eq!(767 assert_eq!(result.sections[0].label, path!("zed/assets/themes/LICENSE"));
772 result.sections[0].label,
773 separator!("zed/assets/themes/LICENSE")
774 );
775 assert_eq!(768 assert_eq!(
776 result.sections[1].label,769 result.sections[1].label,
777 separator!("zed/assets/themes/summercamp/LICENSE")770 path!("zed/assets/themes/summercamp/LICENSE")
778 );771 );
779 assert_eq!(772 assert_eq!(
780 result.sections[2].label,773 result.sections[2].label,
781 separator!("zed/assets/themes/summercamp/subdir/LICENSE")774 path!("zed/assets/themes/summercamp/subdir/LICENSE")
782 );775 );
783 assert_eq!(776 assert_eq!(
784 result.sections[3].label,777 result.sections[3].label,
785 separator!("zed/assets/themes/summercamp/subdir/subsubdir/LICENSE")778 path!("zed/assets/themes/summercamp/subdir/subsubdir/LICENSE")
786 );779 );
787 assert_eq!(result.sections[4].label, "subsubdir");780 assert_eq!(result.sections[4].label, "subsubdir");
788 assert_eq!(result.sections[5].label, "subdir");781 assert_eq!(result.sections[5].label, "subdir");
789 assert_eq!(result.sections[6].label, "summercamp");782 assert_eq!(result.sections[6].label, "summercamp");
790 assert_eq!(result.sections[7].label, separator!("zed/assets/themes"));783 assert_eq!(result.sections[7].label, path!("zed/assets/themes"));
791 784
792 assert_eq!(785 assert_eq!(
793 result.text,786 result.text,
794 separator!(787 path!(
795 "zed/assets/themes\n```zed/assets/themes/LICENSE\n1\n```\n\nsummercamp\n```zed/assets/themes/summercamp/LICENSE\n1\n```\n\nsubdir\n```zed/assets/themes/summercamp/subdir/LICENSE\n1\n```\n\nsubsubdir\n```zed/assets/themes/summercamp/subdir/subsubdir/LICENSE\n3\n```\n\n"788 "zed/assets/themes\n```zed/assets/themes/LICENSE\n1\n```\n\nsummercamp\n```zed/assets/themes/summercamp/LICENSE\n1\n```\n\nsubdir\n```zed/assets/themes/summercamp/subdir/LICENSE\n1\n```\n\nsubsubdir\n```zed/assets/themes/summercamp/subdir/subsubdir/LICENSE\n3\n```\n\n"
796 )789 )
797 );790 );
crates/assistant_tools/Cargo.toml+1−0
@@ -80,6 +80,7 @@ rand.workspace = true · 79 unmodified lines
80pretty_assertions.workspace = true80pretty_assertions.workspace = true
81reqwest_client.workspace = true81reqwest_client.workspace = true
82settings = { workspace = true, features = ["test-support"] }82settings = { workspace = true, features = ["test-support"] }
83smol.workspace = true
83task = { workspace = true, features = ["test-support"]}84task = { workspace = true, features = ["test-support"]}
84tempfile.workspace = true85tempfile.workspace = true
85theme.workspace = true86theme.workspace = true
crates/assistant_tools/src/edit_agent.rs+33−17
@@ -286,7 +286,13 @@ impl EditAgent { · 285 unmodified lines
286 _ => {286 _ => {
287 let ranges = resolved_old_text287 let ranges = resolved_old_text
288 .into_iter()288 .into_iter()
289 .map(|text| text.range)289 .map(|text| {
290 let start_line =
291 (snapshot.offset_to_point(text.range.start).row + 1) as usize;
292 let end_line =
293 (snapshot.offset_to_point(text.range.end).row + 1) as usize;
294 start_line..end_line
295 })
290 .collect();296 .collect();
291 output_events297 output_events
292 .unbounded_send(EditAgentOutputEvent::AmbiguousEditRange(ranges))298 .unbounded_send(EditAgentOutputEvent::AmbiguousEditRange(ranges))
@@ -429,25 +435,25 @@ impl EditAgent { · 136 unmodified lines
429 let task = cx.background_spawn(async move {435 let task = cx.background_spawn(async move {
430 let mut matcher = StreamingFuzzyMatcher::new(snapshot);436 let mut matcher = StreamingFuzzyMatcher::new(snapshot);
431 while let Some(edit_event) = edit_events.next().await {437 while let Some(edit_event) = edit_events.next().await {
432 let EditParserEvent::OldTextChunk { chunk, done } = edit_event? else {438 let EditParserEvent::OldTextChunk {
439 chunk,
440 done,
441 line_hint,
442 } = edit_event?
443 else {
433 break;444 break;
434 };445 };
435 446
436 old_range_tx.send(matcher.push(&chunk))?;447 old_range_tx.send(matcher.push(&chunk, line_hint))?;
437 if done {448 if done {
438 break;449 break;
439 }450 }
440 }451 }
441 452
442 let matches = matcher.finish();453 let matches = matcher.finish();
454 let best_match = matcher.select_best_match();
443 455
444 let old_range = if matches.len() == 1 {456 old_range_tx.send(best_match.clone())?;
445 matches.first()
446 } else {
447 // No matches or multiple ambiguous matches
448 None
449 };
450 old_range_tx.send(old_range.cloned())?;
451 457
452 let indent = LineIndent::from_iter(458 let indent = LineIndent::from_iter(
453 matcher459 matcher
@@ -456,10 +462,18 @@ impl EditAgent { · 2 unmodified lines
456 .unwrap_or(&String::new())462 .unwrap_or(&String::new())
457 .chars(),463 .chars(),
458 );464 );
459 let resolved_old_texts = matches465
460 .into_iter()466 let resolved_old_texts = if let Some(best_match) = best_match {
461 .map(|range| ResolvedOldText { range, indent })467 vec![ResolvedOldText {
462 .collect::<Vec<_>>();468 range: best_match,
469 indent,
470 }]
471 } else {
472 matches
473 .into_iter()
474 .map(|range| ResolvedOldText { range, indent })
475 .collect::<Vec<_>>()
476 };
463 477
464 Ok((edit_events, resolved_old_texts))478 Ok((edit_events, resolved_old_texts))
465 });479 });
@@ -1374,10 +1388,12 @@ mod tests { · 908 unmodified lines
1374 &agent,1388 &agent,
1375 indoc! {"1389 indoc! {"
1376 <old_text>1390 <old_text>
1377 return 42;1391 return 42;
1392 }
1378 </old_text>1393 </old_text>
1379 <new_text>1394 <new_text>
1380 return 100;1395 return 100;
1396 }
1381 </new_text>1397 </new_text>
1382 "},1398 "},
1383 &mut rng,1399 &mut rng,
@@ -1407,7 +1423,7 @@ mod tests { · 23 unmodified lines
1407 1423
1408 // And AmbiguousEditRange even should be emitted1424 // And AmbiguousEditRange even should be emitted
1409 let events = drain_events(&mut events);1425 let events = drain_events(&mut events);
1410 let ambiguous_ranges = vec![17..31, 52..66, 87..101];1426 let ambiguous_ranges = vec![2..3, 6..7, 10..11];
1411 assert!(1427 assert!(
1412 events.contains(&EditAgentOutputEvent::AmbiguousEditRange(ambiguous_ranges)),1428 events.contains(&EditAgentOutputEvent::AmbiguousEditRange(ambiguous_ranges)),
1413 "Should emit AmbiguousEditRange for non-unique text"1429 "Should emit AmbiguousEditRange for non-unique text"
crates/assistant_tools/src/edit_agent/edit_parser.rs+127−9
@@ -1,4 +1,5 @@
1use derive_more::{Add, AddAssign};1use derive_more::{Add, AddAssign};
2use regex::Regex;
2use schemars::JsonSchema;3use schemars::JsonSchema;
3use serde::{Deserialize, Serialize};4use serde::{Deserialize, Serialize};
4use smallvec::SmallVec;5use smallvec::SmallVec;
@@ -11,8 +12,15 @@ const END_TAGS: [&str; 3] = [OLD_TEXT_END_TAG, NEW_TEXT_END_TAG, EDITS_END_TAG]; · 6 unmodified lines
11 12
12#[derive(Debug)]13#[derive(Debug)]
13pub enum EditParserEvent {14pub enum EditParserEvent {
14 OldTextChunk { chunk: String, done: bool },15 OldTextChunk {
15 NewTextChunk { chunk: String, done: bool },16 chunk: String,
17 done: bool,
18 line_hint: Option<u32>,
19 },
20 NewTextChunk {
21 chunk: String,
22 done: bool,
23 },
16}24}
17 25
18#[derive(26#[derive(
@@ -33,7 +41,7 @@ pub struct EditParser { · 14 unmodified lines
33#[derive(Debug, PartialEq)]41#[derive(Debug, PartialEq)]
34enum EditParserState {42enum EditParserState {
35 Pending,43 Pending,
36 WithinOldText { start: bool },44 WithinOldText { start: bool, line_hint: Option<u32> },
37 AfterOldText,45 AfterOldText,
38 WithinNewText { start: bool },46 WithinNewText { start: bool },
39}47}
@@ -54,14 +62,24 @@ impl EditParser { · 14 unmodified lines
54 loop {62 loop {
55 match &mut self.state {63 match &mut self.state {
56 EditParserState::Pending => {64 EditParserState::Pending => {
57 if let Some(start) = self.buffer.find("<old_text>") {65 if let Some(start) = self.buffer.find("<old_text") {
58 self.buffer.drain(..start + "<old_text>".len());66 if let Some(tag_end) = self.buffer[start..].find('>') {
59 self.state = EditParserState::WithinOldText { start: true };67 let tag_end = start + tag_end + 1;
68 let tag = &self.buffer[start..tag_end];
69 let line_hint = self.parse_line_hint(tag);
70 self.buffer.drain(..tag_end);
71 self.state = EditParserState::WithinOldText {
72 start: true,
73 line_hint,
74 };
75 } else {
76 break;
77 }
60 } else {78 } else {
61 break;79 break;
62 }80 }
63 }81 }
64 EditParserState::WithinOldText { start } => {82 EditParserState::WithinOldText { start, line_hint } => {
65 if !self.buffer.is_empty() {83 if !self.buffer.is_empty() {
66 if *start && self.buffer.starts_with('\n') {84 if *start && self.buffer.starts_with('\n') {
67 self.buffer.remove(0);85 self.buffer.remove(0);
@@ -69,6 +87,7 @@ impl EditParser { · 1 unmodified line
69 *start = false;87 *start = false;
70 }88 }
71 89
90 let line_hint = *line_hint;
72 if let Some(tag_range) = self.find_end_tag() {91 if let Some(tag_range) = self.find_end_tag() {
73 let mut chunk = self.buffer[..tag_range.start].to_string();92 let mut chunk = self.buffer[..tag_range.start].to_string();
74 if chunk.ends_with('\n') {93 if chunk.ends_with('\n') {
@@ -82,12 +101,17 @@ impl EditParser { · 7 unmodified lines
82 101
83 self.buffer.drain(..tag_range.end);102 self.buffer.drain(..tag_range.end);
84 self.state = EditParserState::AfterOldText;103 self.state = EditParserState::AfterOldText;
85 edit_events.push(EditParserEvent::OldTextChunk { chunk, done: true });104 edit_events.push(EditParserEvent::OldTextChunk {
105 chunk,
106 done: true,
107 line_hint,
108 });
86 } else {109 } else {
87 if !self.ends_with_tag_prefix() {110 if !self.ends_with_tag_prefix() {
88 edit_events.push(EditParserEvent::OldTextChunk {111 edit_events.push(EditParserEvent::OldTextChunk {
89 chunk: mem::take(&mut self.buffer),112 chunk: mem::take(&mut self.buffer),
90 done: false,113 done: false,
114 line_hint,
91 });115 });
92 }116 }
93 break;117 break;
@@ -154,6 +178,16 @@ impl EditParser { · 60 unmodified lines
154 end_prefixes.any(|prefix| self.buffer.ends_with(&prefix))178 end_prefixes.any(|prefix| self.buffer.ends_with(&prefix))
155 }179 }
156 180
181 fn parse_line_hint(&self, tag: &str) -> Option<u32> {
182 static LINE_HINT_REGEX: std::sync::LazyLock<Regex> =
183 std::sync::LazyLock::new(|| Regex::new(r#"line=(?:"?)(\d+)"#).unwrap());
184
185 LINE_HINT_REGEX
186 .captures(tag)
187 .and_then(|caps| caps.get(1))
188 .and_then(|m| m.as_str().parse::<u32>().ok())
189 }
190
157 pub fn finish(self) -> EditParserMetrics {191 pub fn finish(self) -> EditParserMetrics {
158 self.metrics192 self.metrics
159 }193 }
@@ -178,6 +212,7 @@ mod tests { · 18 unmodified lines
178 vec![Edit {212 vec![Edit {
179 old_text: "original".to_string(),213 old_text: "original".to_string(),
180 new_text: "updated".to_string(),214 new_text: "updated".to_string(),
215 line_hint: None,
181 }]216 }]
182 );217 );
183 assert_eq!(218 assert_eq!(
@@ -209,10 +244,12 @@ mod tests { · 25 unmodified lines
209 Edit {244 Edit {
210 old_text: "first old".to_string(),245 old_text: "first old".to_string(),
211 new_text: "first new".to_string(),246 new_text: "first new".to_string(),
247 line_hint: None,
212 },248 },
213 Edit {249 Edit {
214 old_text: "second old".to_string(),250 old_text: "second old".to_string(),
215 new_text: "second new".to_string(),251 new_text: "second new".to_string(),
252 line_hint: None,
216 },253 },
217 ]254 ]
218 );255 );
@@ -244,14 +281,17 @@ mod tests { · 25 unmodified lines
244 Edit {281 Edit {
245 old_text: "content".to_string(),282 old_text: "content".to_string(),
246 new_text: "updated content".to_string(),283 new_text: "updated content".to_string(),
284 line_hint: None,
247 },285 },
248 Edit {286 Edit {
249 old_text: "second item".to_string(),287 old_text: "second item".to_string(),
250 new_text: "modified second item".to_string(),288 new_text: "modified second item".to_string(),
289 line_hint: None,
251 },290 },
252 Edit {291 Edit {
253 old_text: "third case".to_string(),292 old_text: "third case".to_string(),
254 new_text: "improved third case".to_string(),293 new_text: "improved third case".to_string(),
294 line_hint: None,
255 },295 },
256 ]296 ]
257 );297 );
@@ -276,6 +316,7 @@ mod tests { · 18 unmodified lines
276 vec![Edit {316 vec![Edit {
277 old_text: "code with <tag>nested</tag> elements".to_string(),317 old_text: "code with <tag>nested</tag> elements".to_string(),
278 new_text: "new <code>content</code>".to_string(),318 new_text: "new <code>content</code>".to_string(),
319 line_hint: None,
279 }]320 }]
280 );321 );
281 assert_eq!(322 assert_eq!(
@@ -299,6 +340,7 @@ mod tests { · 17 unmodified lines
299 vec![Edit {340 vec![Edit {
300 old_text: "".to_string(),341 old_text: "".to_string(),
301 new_text: "".to_string(),342 new_text: "".to_string(),
343 line_hint: None,
302 }]344 }]
303 );345 );
304 assert_eq!(346 assert_eq!(
@@ -322,6 +364,7 @@ mod tests { · 17 unmodified lines
322 vec![Edit {364 vec![Edit {
323 old_text: "line1\nline2\nline3".to_string(),365 old_text: "line1\nline2\nline3".to_string(),
324 new_text: "line1\nmodified line2\nline3".to_string(),366 new_text: "line1\nmodified line2\nline3".to_string(),
367 line_hint: None,
325 }]368 }]
326 );369 );
327 assert_eq!(370 assert_eq!(
@@ -368,10 +411,12 @@ mod tests { · 40 unmodified lines
368 Edit {411 Edit {
369 old_text: "a\nb\nc".to_string(),412 old_text: "a\nb\nc".to_string(),
370 new_text: "a\nB\nc".to_string(),413 new_text: "a\nB\nc".to_string(),
414 line_hint: None,
371 },415 },
372 Edit {416 Edit {
373 old_text: "d\ne\nf".to_string(),417 old_text: "d\ne\nf".to_string(),
374 new_text: "D\ne\nF".to_string(),418 new_text: "D\ne\nF".to_string(),
419 line_hint: None,
375 }420 }
376 ]421 ]
377 );422 );
@@ -402,6 +447,7 @@ mod tests { · 24 unmodified lines
402 vec![Edit {447 vec![Edit {
403 old_text: "Lorem".to_string(),448 old_text: "Lorem".to_string(),
404 new_text: "LOREM".to_string(),449 new_text: "LOREM".to_string(),
450 line_hint: None,
405 },]451 },]
406 );452 );
407 assert_eq!(453 assert_eq!(
@@ -413,10 +459,77 @@ mod tests { · 5 unmodified lines
413 );459 );
414 }460 }
415 461
462 #[gpui::test(iterations = 100)]
463 fn test_line_hints(mut rng: StdRng) {
464 // Line hint is a single quoted line number
465 let mut parser = EditParser::new();
466
467 let edits = parse_random_chunks(
468 r#"
469 <old_text line="23">original code</old_text>
470 <new_text>updated code</new_text>"#,
471 &mut parser,
472 &mut rng,
473 );
474
475 assert_eq!(edits.len(), 1);
476 assert_eq!(edits[0].old_text, "original code");
477 assert_eq!(edits[0].line_hint, Some(23));
478 assert_eq!(edits[0].new_text, "updated code");
479
480 // Line hint is a single unquoted line number
481 let mut parser = EditParser::new();
482
483 let edits = parse_random_chunks(
484 r#"
485 <old_text line=45>original code</old_text>
486 <new_text>updated code</new_text>"#,
487 &mut parser,
488 &mut rng,
489 );
490
491 assert_eq!(edits.len(), 1);
492 assert_eq!(edits[0].old_text, "original code");
493 assert_eq!(edits[0].line_hint, Some(45));
494 assert_eq!(edits[0].new_text, "updated code");
495
496 // Line hint is a range
497 let mut parser = EditParser::new();
498
499 let edits = parse_random_chunks(
500 r#"
501 <old_text line="23:50">original code</old_text>
502 <new_text>updated code</new_text>"#,
503 &mut parser,
504 &mut rng,
505 );
506
507 assert_eq!(edits.len(), 1);
508 assert_eq!(edits[0].old_text, "original code");
509 assert_eq!(edits[0].line_hint, Some(23));
510 assert_eq!(edits[0].new_text, "updated code");
511
512 // No line hint
513 let mut parser = EditParser::new();
514 let edits = parse_random_chunks(
515 r#"
516 <old_text>old</old_text>
517 <new_text>new</new_text>"#,
518 &mut parser,
519 &mut rng,
520 );
521
522 assert_eq!(edits.len(), 1);
523 assert_eq!(edits[0].old_text, "old");
524 assert_eq!(edits[0].line_hint, None);
525 assert_eq!(edits[0].new_text, "new");
526 }
527
416 #[derive(Default, Debug, PartialEq, Eq)]528 #[derive(Default, Debug, PartialEq, Eq)]
417 struct Edit {529 struct Edit {
418 old_text: String,530 old_text: String,
419 new_text: String,531 new_text: String,
532 line_hint: Option<u32>,
420 }533 }
421 534
422 fn parse_random_chunks(input: &str, parser: &mut EditParser, rng: &mut StdRng) -> Vec<Edit> {535 fn parse_random_chunks(input: &str, parser: &mut EditParser, rng: &mut StdRng) -> Vec<Edit> {
@@ -433,10 +546,15 @@ mod tests { · 10 unmodified lines
433 for chunk_ix in chunk_indices {546 for chunk_ix in chunk_indices {
434 for event in parser.push(&input[last_ix..chunk_ix]) {547 for event in parser.push(&input[last_ix..chunk_ix]) {
435 match event {548 match event {
436 EditParserEvent::OldTextChunk { chunk, done } => {549 EditParserEvent::OldTextChunk {
550 chunk,
551 done,
552 line_hint,
553 } => {
437 old_text.as_mut().unwrap().push_str(&chunk);554 old_text.as_mut().unwrap().push_str(&chunk);
438 if done {555 if done {
439 pending_edit.old_text = old_text.take().unwrap();556 pending_edit.old_text = old_text.take().unwrap();
557 pending_edit.line_hint = line_hint;
440 new_text = Some(String::new());558 new_text = Some(String::new());
441 }559 }
442 }560 }
crates/assistant_tools/src/edit_agent/evals.rs+112−56
@@ -11,7 +11,7 @@ use client::{Client, UserStore}; · 10 unmodified lines
11use collections::HashMap;11use collections::HashMap;
12use fs::FakeFs;12use fs::FakeFs;
13use futures::{FutureExt, future::LocalBoxFuture};13use futures::{FutureExt, future::LocalBoxFuture};
14use gpui::{AppContext, TestAppContext};14use gpui::{AppContext, TestAppContext, Timer};
15use indoc::{formatdoc, indoc};15use indoc::{formatdoc, indoc};
16use language_model::{16use language_model::{
17 LanguageModelRegistry, LanguageModelRequestTool, LanguageModelToolResult,17 LanguageModelRegistry, LanguageModelRequestTool, LanguageModelToolResult,
@@ -26,6 +26,7 @@ use std::{ · 8 unmodified lines
26 cmp::Reverse,26 cmp::Reverse,
27 fmt::{self, Display},27 fmt::{self, Display},
28 io::Write as _,28 io::Write as _,
29 path::Path,
29 str::FromStr,30 str::FromStr,
30 sync::mpsc,31 sync::mpsc,
31};32};
@@ -38,10 +39,11 @@ fn eval_extract_handle_command_output() { · 6 unmodified lines
38 //39 //
39 // Model | Pass rate40 // Model | Pass rate
40 // ----------------------------|----------41 // ----------------------------|----------
41 // claude-3.7-sonnet | 0.9842 // claude-3.7-sonnet | 0.99 (2025-06-14)
42 // gemini-2.5-pro-06-05 | 0.7743 // claude-sonnet-4 | 0.97 (2025-06-14)
43 // gemini-2.5-flash | 0.1144 // gemini-2.5-pro-06-05 | 0.77 (2025-05-22)
44 // gpt-4.1 | 1.0045 // gemini-2.5-flash | 0.11 (2025-05-22)
46 // gpt-4.1 | 1.00 (2025-05-22)
45 47
46 let input_file_path = "root/blame.rs";48 let input_file_path = "root/blame.rs";
47 let input_file_content = include_str!("evals/fixtures/extract_handle_command_output/before.rs");49 let input_file_content = include_str!("evals/fixtures/extract_handle_command_output/before.rs");
@@ -110,6 +112,13 @@ fn eval_extract_handle_command_output() { · 62 unmodified lines
110#[test]112#[test]
111#[cfg_attr(not(feature = "eval"), ignore)]113#[cfg_attr(not(feature = "eval"), ignore)]
112fn eval_delete_run_git_blame() {114fn eval_delete_run_git_blame() {
115 // Model | Pass rate
116 // ----------------------------|----------
117 // claude-3.7-sonnet | 1.0 (2025-06-14)
118 // claude-sonnet-4 | 0.96 (2025-06-14)
119 // gemini-2.5-pro-06-05 |
120 // gemini-2.5-flash |
121 // gpt-4.1 |
113 let input_file_path = "root/blame.rs";122 let input_file_path = "root/blame.rs";
114 let input_file_content = include_str!("evals/fixtures/delete_run_git_blame/before.rs");123 let input_file_content = include_str!("evals/fixtures/delete_run_git_blame/before.rs");
115 let output_file_content = include_str!("evals/fixtures/delete_run_git_blame/after.rs");124 let output_file_content = include_str!("evals/fixtures/delete_run_git_blame/after.rs");
@@ -165,13 +174,12 @@ fn eval_delete_run_git_blame() { · 49 unmodified lines
165#[test]174#[test]
166#[cfg_attr(not(feature = "eval"), ignore)]175#[cfg_attr(not(feature = "eval"), ignore)]
167fn eval_translate_doc_comments() {176fn eval_translate_doc_comments() {
168 // Results for 2025-05-22
169 //
170 // Model | Pass rate177 // Model | Pass rate
171 // ============================================178 // ============================================
172 //179 //
173 // claude-3.7-sonnet |180 // claude-3.7-sonnet | 1.0 (2025-06-14)
174 // gemini-2.5-pro-preview-03-25 | 1.0181 // claude-sonnet-4 | 1.0 (2025-06-14)
182 // gemini-2.5-pro-preview-03-25 | 1.0 (2025-05-22)
175 // gemini-2.5-flash-preview-04-17 |183 // gemini-2.5-flash-preview-04-17 |
176 // gpt-4.1 |184 // gpt-4.1 |
177 let input_file_path = "root/canvas.rs";185 let input_file_path = "root/canvas.rs";
@@ -228,13 +236,12 @@ fn eval_translate_doc_comments() { · 50 unmodified lines
228#[test]236#[test]
229#[cfg_attr(not(feature = "eval"), ignore)]237#[cfg_attr(not(feature = "eval"), ignore)]
230fn eval_use_wasi_sdk_in_compile_parser_to_wasm() {238fn eval_use_wasi_sdk_in_compile_parser_to_wasm() {
231 // Results for 2025-05-22
232 //
233 // Model | Pass rate239 // Model | Pass rate
234 // ============================================240 // ============================================
235 //241 //
236 // claude-3.7-sonnet | 0.98242 // claude-3.7-sonnet | 0.96 (2025-06-14)
237 // gemini-2.5-pro-preview-03-25 | 0.99243 // claude-sonnet-4 | 0.11 (2025-06-14)
244 // gemini-2.5-pro-preview-03-25 | 0.99 (2025-05-22)
238 // gemini-2.5-flash-preview-04-17 |245 // gemini-2.5-flash-preview-04-17 |
239 // gpt-4.1 |246 // gpt-4.1 |
240 let input_file_path = "root/lib.rs";247 let input_file_path = "root/lib.rs";
@@ -354,13 +361,12 @@ fn eval_use_wasi_sdk_in_compile_parser_to_wasm() { · 113 unmodified lines
354#[test]361#[test]
355#[cfg_attr(not(feature = "eval"), ignore)]362#[cfg_attr(not(feature = "eval"), ignore)]
356fn eval_disable_cursor_blinking() {363fn eval_disable_cursor_blinking() {
357 // Results for 2025-05-22
358 //
359 // Model | Pass rate364 // Model | Pass rate
360 // ============================================365 // ============================================
361 //366 //
362 // claude-3.7-sonnet |367 // claude-3.7-sonnet | 0.99 (2025-06-14)
363 // gemini-2.5-pro-preview-03-25 | 1.0368 // claude-sonnet-4 | 0.85 (2025-06-14)
369 // gemini-2.5-pro-preview-03-25 | 1.0 (2025-05-22)
364 // gemini-2.5-flash-preview-04-17 |370 // gemini-2.5-flash-preview-04-17 |
365 // gpt-4.1 |371 // gpt-4.1 |
366 let input_file_path = "root/editor.rs";372 let input_file_path = "root/editor.rs";
@@ -438,14 +444,21 @@ fn eval_disable_cursor_blinking() { · 71 unmodified lines
438#[test]444#[test]
439#[cfg_attr(not(feature = "eval"), ignore)]445#[cfg_attr(not(feature = "eval"), ignore)]
440fn eval_from_pixels_constructor() {446fn eval_from_pixels_constructor() {
441 // Results for 2025-05-22447 // Results for 2025-06-13
448 //
449 // The outcome of this evaluation depends heavily on the LINE_HINT_TOLERANCE
450 // value. Higher values improve the pass rate but may sometimes cause
451 // edits to be misapplied. In the context of this eval, this means
452 // the agent might add from_pixels tests in incorrect locations
453 // (e.g., at the beginning of the file), yet the evaluation may still
454 // rate it highly.
442 //455 //
443 // Model | Pass rate456 // Model | Pass rate
444 // ============================================457 // ============================================
445 //458 //
446 // claude-3.7-sonnet |459 // claude-4.0-sonnet | 0.99
447 // gemini-2.5-pro-preview-03-25 | 0.94460 // claude-3.7-sonnet | 0.88
448 // gemini-2.5-flash-preview-04-17 |461 // gemini-2.5-pro-preview-03-25 | 0.96
449 // gpt-4.1 |462 // gpt-4.1 |
450 let input_file_path = "root/canvas.rs";463 let input_file_path = "root/canvas.rs";
451 let input_file_content = include_str!("evals/fixtures/from_pixels_constructor/before.rs");464 let input_file_content = include_str!("evals/fixtures/from_pixels_constructor/before.rs");
@@ -455,7 +468,7 @@ fn eval_from_pixels_constructor() { · 3 unmodified lines
455 0.95,468 0.95,
456 // For whatever reason, this eval produces more mismatched tags.469 // For whatever reason, this eval produces more mismatched tags.
457 // Increasing for now, let's see if we can bring this down.470 // Increasing for now, let's see if we can bring this down.
458 0.2,471 0.25,
459 EvalInput::from_conversation(472 EvalInput::from_conversation(
460 vec![473 vec![
461 message(474 message(
@@ -641,15 +654,14 @@ fn eval_from_pixels_constructor() { · 179 unmodified lines
641#[test]654#[test]
642#[cfg_attr(not(feature = "eval"), ignore)]655#[cfg_attr(not(feature = "eval"), ignore)]
643fn eval_zode() {656fn eval_zode() {
644 // Results for 2025-05-22
645 //
646 // Model | Pass rate657 // Model | Pass rate
647 // ============================================658 // ============================================
648 //659 //
649 // claude-3.7-sonnet | 1.0660 // claude-3.7-sonnet | 1.0 (2025-06-14)
650 // gemini-2.5-pro-preview-03-25 | 1.0661 // claude-sonnet-4 | 1.0 (2025-06-14)
651 // gemini-2.5-flash-preview-04-17 | 1.0662 // gemini-2.5-pro-preview-03-25 | 1.0 (2025-05-22)
652 // gpt-4.1 | 1.0663 // gemini-2.5-flash-preview-04-17 | 1.0 (2025-05-22)
664 // gpt-4.1 | 1.0 (2025-05-22)
653 let input_file_path = "root/zode.py";665 let input_file_path = "root/zode.py";
654 let input_content = None;666 let input_content = None;
655 let edit_description = "Create the main Zode CLI script";667 let edit_description = "Create the main Zode CLI script";
@@ -748,13 +760,12 @@ fn eval_zode() { · 92 unmodified lines
748#[test]760#[test]
749#[cfg_attr(not(feature = "eval"), ignore)]761#[cfg_attr(not(feature = "eval"), ignore)]
750fn eval_add_overwrite_test() {762fn eval_add_overwrite_test() {
751 // Results for 2025-05-22
752 //
753 // Model | Pass rate763 // Model | Pass rate
754 // ============================================764 // ============================================
755 //765 //
756 // claude-3.7-sonnet | 0.16766 // claude-3.7-sonnet | 0.65 (2025-06-14)
757 // gemini-2.5-pro-preview-03-25 | 0.35767 // claude-sonnet-4 | 0.07 (2025-06-14)
768 // gemini-2.5-pro-preview-03-25 | 0.35 (2025-05-22)
758 // gemini-2.5-flash-preview-04-17 |769 // gemini-2.5-flash-preview-04-17 |
759 // gpt-4.1 |770 // gpt-4.1 |
760 let input_file_path = "root/action_log.rs";771 let input_file_path = "root/action_log.rs";
@@ -984,15 +995,14 @@ fn eval_create_empty_file() { · 223 unmodified lines
984 // thoughts into it. This issue is not specific to empty files, but995 // thoughts into it. This issue is not specific to empty files, but
985 // it's easier to reproduce with them.996 // it's easier to reproduce with them.
986 //997 //
987 // Results for 2025-05-21:
988 //
989 // Model | Pass rate998 // Model | Pass rate
990 // ============================================999 // ============================================
991 //1000 //
992 // claude-3.7-sonnet | 1.001001 // claude-3.7-sonnet | 1.00 (2025-06-14)
993 // gemini-2.5-pro-preview-03-25 | 1.001002 // claude-sonnet-4 | 1.00 (2025-06-14)
994 // gemini-2.5-flash-preview-04-17 | 1.001003 // gemini-2.5-pro-preview-03-25 | 1.00 (2025-05-21)
995 // gpt-4.1 | 1.001004 // gemini-2.5-flash-preview-04-17 | 1.00 (2025-05-21)
1005 // gpt-4.1 | 1.00 (2025-05-21)
996 //1006 //
997 //1007 //
998 // TODO: gpt-4.1-mini errored 38 times:1008 // TODO: gpt-4.1-mini errored 38 times:
@@ -1255,9 +1265,12 @@ impl EvalAssertion { · 256 unmodified lines
1255 }],1265 }],
1256 ..Default::default()1266 ..Default::default()
1257 };1267 };
1258 let mut response = judge1268 let mut response = retry_on_rate_limit(async || {
1259 .stream_completion_text(request, &cx.to_async())1269 Ok(judge
1260 .await?;1270 .stream_completion_text(request.clone(), &cx.to_async())
1271 .await?)
1272 })
1273 .await?;
1261 let mut output = String::new();1274 let mut output = String::new();
1262 while let Some(chunk) = response.stream.next().await {1275 while let Some(chunk) = response.stream.next().await {
1263 let chunk = chunk?;1276 let chunk = chunk?;
@@ -1308,10 +1321,17 @@ fn eval( · 44 unmodified lines
1308 run_eval(eval.clone(), tx.clone());1321 run_eval(eval.clone(), tx.clone());
1309 1322
1310 let executor = gpui::background_executor();1323 let executor = gpui::background_executor();
1324 let semaphore = Arc::new(smol::lock::Semaphore::new(32));
1311 for _ in 1..iterations {1325 for _ in 1..iterations {
1312 let eval = eval.clone();1326 let eval = eval.clone();
1313 let tx = tx.clone();1327 let tx = tx.clone();
1314 executor.spawn(async move { run_eval(eval, tx) }).detach();1328 let semaphore = semaphore.clone();
1329 executor
1330 .spawn(async move {
1331 let _guard = semaphore.acquire().await;
1332 run_eval(eval, tx)
1333 })
1334 .detach();
1315 }1335 }
1316 drop(tx);1336 drop(tx);
1317 1337
@@ -1539,6 +1559,7 @@ impl EditAgentTest { · 221 unmodified lines
1539 .collect::<Vec<_>>();1559 .collect::<Vec<_>>();
1540 let worktrees = vec![WorktreeContext {1560 let worktrees = vec![WorktreeContext {
1541 root_name: "root".to_string(),1561 root_name: "root".to_string(),
1562 abs_path: Path::new("/path/to/root").into(),
1542 rules_file: None,1563 rules_file: None,
1543 }];1564 }];
1544 let prompt_builder = PromptBuilder::new(None)?;1565 let prompt_builder = PromptBuilder::new(None)?;
@@ -1577,21 +1598,31 @@ impl EditAgentTest { · 32 unmodified lines
1577 if let Some(input_content) = eval.input_content.as_deref() {1598 if let Some(input_content) = eval.input_content.as_deref() {
1578 buffer.update(cx, |buffer, cx| buffer.set_text(input_content, cx));1599 buffer.update(cx, |buffer, cx| buffer.set_text(input_content, cx));
1579 }1600 }
1580 let (edit_output, _) = self.agent.edit(1601 retry_on_rate_limit(async || {
1581 buffer.clone(),1602 self.agent
1582 eval.edit_file_input.display_description,1603 .edit(
1583 &conversation,1604 buffer.clone(),
1584 &mut cx.to_async(),1605 eval.edit_file_input.display_description.clone(),
1585 );1606 &conversation,
1586 edit_output.await?1607 &mut cx.to_async(),
1608 )
1609 .0
1610 .await
1611 })
1612 .await?
1587 } else {1613 } else {
1588 let (edit_output, _) = self.agent.overwrite(1614 retry_on_rate_limit(async || {
1589 buffer.clone(),1615 self.agent
1590 eval.edit_file_input.display_description,1616 .overwrite(
1591 &conversation,1617 buffer.clone(),
1592 &mut cx.to_async(),1618 eval.edit_file_input.display_description.clone(),
1593 );1619 &conversation,
1594 edit_output.await?1620 &mut cx.to_async(),
1621 )
1622 .0
1623 .await
1624 })
1625 .await?
1595 };1626 };
1596 1627
1597 let buffer_text = buffer.read_with(cx, |buffer, _| buffer.text());1628 let buffer_text = buffer.read_with(cx, |buffer, _| buffer.text());
@@ -1613,6 +1644,31 @@ impl EditAgentTest { · 15 unmodified lines
1613 }1644 }
1614}1645}
1615 1646
1647async fn retry_on_rate_limit<R>(mut request: impl AsyncFnMut() -> Result<R>) -> Result<R> {
1648 let mut attempt = 0;
1649 loop {
1650 attempt += 1;
1651 match request().await {
1652 Ok(result) => return Ok(result),
1653 Err(err) => match err.downcast::<LanguageModelCompletionError>() {
1654 Ok(err) => match err {
1655 LanguageModelCompletionError::RateLimit(duration) => {
1656 // Wait for the duration supplied, with some jitter to avoid all requests being made at the same time.
1657 let jitter = duration.mul_f64(rand::thread_rng().gen_range(0.0..1.0));
1658 eprintln!(
1659 "Attempt #{attempt}: Rate limit exceeded. Retry after {duration:?} + jitter of {jitter:?}"
1660 );
1661 Timer::after(duration + jitter).await;
1662 continue;
1663 }
1664 _ => return Err(err.into()),
1665 },
1666 Err(err) => return Err(err),
1667 },
1668 }
1669 }
1670}
1671
1616#[derive(Clone, Debug, Eq, PartialEq, Hash)]1672#[derive(Clone, Debug, Eq, PartialEq, Hash)]
1617struct EvalAssertionOutcome {1673struct EvalAssertionOutcome {
1618 score: usize,1674 score: usize,
crates/assistant_tools/src/edit_agent/streaming_fuzzy_matcher.rs+97−20
@@ -10,8 +10,9 @@ const DELETION_COST: u32 = 10; · 9 unmodified lines
10pub struct StreamingFuzzyMatcher {10pub struct StreamingFuzzyMatcher {
11 snapshot: TextBufferSnapshot,11 snapshot: TextBufferSnapshot,
12 query_lines: Vec<String>,12 query_lines: Vec<String>,
13 line_hint: Option<u32>,
13 incomplete_line: String,14 incomplete_line: String,
14 best_matches: Vec<Range<usize>>,15 matches: Vec<Range<usize>>,
15 matrix: SearchMatrix,16 matrix: SearchMatrix,
16}17}
17 18
@@ -21,8 +22,9 @@ impl StreamingFuzzyMatcher { · 3 unmodified lines
21 Self {22 Self {
22 snapshot,23 snapshot,
23 query_lines: Vec::new(),24 query_lines: Vec::new(),
25 line_hint: None,
24 incomplete_line: String::new(),26 incomplete_line: String::new(),
25 best_matches: Vec::new(),27 matches: Vec::new(),
26 matrix: SearchMatrix::new(buffer_line_count + 1),28 matrix: SearchMatrix::new(buffer_line_count + 1),
27 }29 }
28 }30 }
@@ -41,9 +43,10 @@ impl StreamingFuzzyMatcher { · 12 unmodified lines
41 ///43 ///
42 /// Returns `Some(range)` if a match has been found with the accumulated44 /// Returns `Some(range)` if a match has been found with the accumulated
43 /// query so far, or `None` if no suitable match exists yet.45 /// query so far, or `None` if no suitable match exists yet.
44 pub fn push(&mut self, chunk: &str) -> Option<Range<usize>> {46 pub fn push(&mut self, chunk: &str, line_hint: Option<u32>) -> Option<Range<usize>> {
45 // Add the chunk to our incomplete line buffer47 // Add the chunk to our incomplete line buffer
46 self.incomplete_line.push_str(chunk);48 self.incomplete_line.push_str(chunk);
49 self.line_hint = line_hint;
47 50
48 if let Some((last_pos, _)) = self.incomplete_line.match_indices('\n').next_back() {51 if let Some((last_pos, _)) = self.incomplete_line.match_indices('\n').next_back() {
49 let complete_part = &self.incomplete_line[..=last_pos];52 let complete_part = &self.incomplete_line[..=last_pos];
@@ -55,20 +58,11 @@ impl StreamingFuzzyMatcher { · 5 unmodified lines
55 58
56 self.incomplete_line.replace_range(..last_pos + 1, "");59 self.incomplete_line.replace_range(..last_pos + 1, "");
57 60
58 self.best_matches = self.resolve_location_fuzzy();61 self.matches = self.resolve_location_fuzzy();
59
60 if let Some(first_match) = self.best_matches.first() {
61 Some(first_match.clone())
62 } else {
63 None
64 }
65 } else {
66 if let Some(first_match) = self.best_matches.first() {
67 Some(first_match.clone())
68 } else {
69 None
70 }
71 }62 }
63
64 let best_match = self.select_best_match();
65 best_match.or_else(|| self.matches.first().cloned())
72 }66 }
73 67
74 /// Finish processing and return the final best match(es).68 /// Finish processing and return the final best match(es).
@@ -80,9 +74,9 @@ impl StreamingFuzzyMatcher { · 5 unmodified lines
80 if !self.incomplete_line.is_empty() {74 if !self.incomplete_line.is_empty() {
81 self.query_lines.push(self.incomplete_line.clone());75 self.query_lines.push(self.incomplete_line.clone());
82 self.incomplete_line.clear();76 self.incomplete_line.clear();
83 self.best_matches = self.resolve_location_fuzzy();77 self.matches = self.resolve_location_fuzzy();
84 }78 }
85 self.best_matches.clone()79 self.matches.clone()
86 }80 }
87 81
88 fn resolve_location_fuzzy(&mut self) -> Vec<Range<usize>> {82 fn resolve_location_fuzzy(&mut self) -> Vec<Range<usize>> {
@@ -198,6 +192,43 @@ impl StreamingFuzzyMatcher { · 109 unmodified lines
198 192
199 valid_matches.into_iter().map(|(_, range)| range).collect()193 valid_matches.into_iter().map(|(_, range)| range).collect()
200 }194 }
195
196 /// Return the best match with starting position close enough to line_hint.
197 pub fn select_best_match(&self) -> Option<Range<usize>> {
198 // Allow line hint to be off by that many lines.
199 // Higher values increase probability of applying edits to a wrong place,
200 // Lower values increase edits failures and overall conversation length.
201 const LINE_HINT_TOLERANCE: u32 = 200;
202
203 if self.matches.is_empty() {
204 return None;
205 }
206
207 if self.matches.len() == 1 {
208 return self.matches.first().cloned();
209 }
210
211 let Some(line_hint) = self.line_hint else {
212 // Multiple ambiguous matches
213 return None;
214 };
215
216 let mut best_match = None;
217 let mut best_distance = u32::MAX;
218
219 for range in &self.matches {
220 let start_point = self.snapshot.offset_to_point(range.start);
221 let start_line = start_point.row;
222 let distance = start_line.abs_diff(line_hint);
223
224 if distance <= LINE_HINT_TOLERANCE && distance < best_distance {
225 best_distance = distance;
226 best_match = Some(range.clone());
227 }
228 }
229
230 best_match
231 }
201}232}
202 233
203fn fuzzy_eq(left: &str, right: &str) -> bool {234fn fuzzy_eq(left: &str, right: &str) -> bool {
@@ -640,6 +671,52 @@ mod tests { · 436 unmodified lines
640 );671 );
641 }672 }
642 673
674 #[gpui::test]
675 fn test_line_hint_selection() {
676 let text = indoc! {r#"
677 fn first_function() {
678 return 42;
679 }
680
681 fn second_function() {
682 return 42;
683 }
684
685 fn third_function() {
686 return 42;
687 }
688 "#};
689
690 let buffer = TextBuffer::new(0, BufferId::new(1).unwrap(), text.to_string());
691 let snapshot = buffer.snapshot();
692 let mut matcher = StreamingFuzzyMatcher::new(snapshot.clone());
693
694 // Given a query that matches all three functions
695 let query = "return 42;\n";
696
697 // Test with line hint pointing to second function (around line 5)
698 let best_match = matcher.push(query, Some(5)).expect("Failed to match query");
699
700 let matched_text = snapshot
701 .text_for_range(best_match.clone())
702 .collect::<String>();
703 assert!(matched_text.contains("return 42;"));
704 assert_eq!(
705 best_match,
706 63..77,
707 "Expected to match `second_function` based on the line hint"
708 );
709
710 let mut matcher = StreamingFuzzyMatcher::new(snapshot.clone());
711 matcher.push(query, None);
712 matcher.finish();
713 let best_match = matcher.select_best_match();
714 assert!(
715 best_match.is_none(),
716 "Best match should be None when query cannot be uniquely resolved"
717 );
718 }
719
643 #[track_caller]720 #[track_caller]
644 fn assert_location_resolution(text_with_expected_range: &str, query: &str, rng: &mut StdRng) {721 fn assert_location_resolution(text_with_expected_range: &str, query: &str, rng: &mut StdRng) {
645 let (text, expected_ranges) = marked_text_ranges(text_with_expected_range, false);722 let (text, expected_ranges) = marked_text_ranges(text_with_expected_range, false);
@@ -653,7 +730,7 @@ mod tests { · 7 unmodified lines
653 730
654 // Push chunks incrementally731 // Push chunks incrementally
655 for chunk in &chunks {732 for chunk in &chunks {
656 matcher.push(chunk);733 matcher.push(chunk, None);
657 }734 }
658 735
659 let actual_ranges = matcher.finish();736 let actual_ranges = matcher.finish();
@@ -706,7 +783,7 @@ mod tests { · 46 unmodified lines
706 783
707 fn push(finder: &mut StreamingFuzzyMatcher, chunk: &str) -> Option<String> {784 fn push(finder: &mut StreamingFuzzyMatcher, chunk: &str) -> Option<String> {
708 finder785 finder
709 .push(chunk)786 .push(chunk, None)
710 .map(|range| finder.snapshot.text_for_range(range).collect::<String>())787 .map(|range| finder.snapshot.text_for_range(range).collect::<String>())
711 }788 }
712 789
crates/assistant_tools/src/edit_file_tool.rs+40−17
@@ -10,7 +10,7 @@ use assistant_tool::{ · 9 unmodified lines
10 ToolUseStatus,10 ToolUseStatus,
11};11};
12use buffer_diff::{BufferDiff, BufferDiffSnapshot};12use buffer_diff::{BufferDiff, BufferDiffSnapshot};
13use editor::{Editor, EditorMode, MinimapVisibility, MultiBuffer, PathKey};13use editor::{Editor, EditorMode, MinimapVisibility, MultiBuffer, PathKey, scroll::Autoscroll};
14use futures::StreamExt;14use futures::StreamExt;
15use gpui::{15use gpui::{
16 Animation, AnimationExt, AnyWindowHandle, App, AppContext, AsyncApp, Entity, Task,16 Animation, AnimationExt, AnyWindowHandle, App, AppContext, AsyncApp, Entity, Task,
@@ -69,13 +69,13 @@ pub struct EditFileToolInput { · 52 unmodified lines
69 /// start each path with one of the project's root directories.69 /// start each path with one of the project's root directories.
70 ///70 ///
71 /// The following examples assume we have two root directories in the project:71 /// The following examples assume we have two root directories in the project:
72 /// - backend72 /// - /a/b/backend
73 /// - frontend73 /// - /c/d/frontend
74 ///74 ///
75 /// <example>75 /// <example>
76 /// `backend/src/main.rs`76 /// `backend/src/main.rs`
77 ///77 ///
78 /// Notice how the file path starts with root-1. Without that, the path78 /// Notice how the file path starts with `backend`. Without that, the path
79 /// would be ambiguous and the call would fail!79 /// would be ambiguous and the call would fail!
80 /// </example>80 /// </example>
81 ///81 ///
@@ -333,14 +333,18 @@ impl Tool for EditFileTool { · 251 unmodified lines
333 );333 );
334 anyhow::ensure!(334 anyhow::ensure!(
335 ambiguous_ranges.is_empty(),335 ambiguous_ranges.is_empty(),
336 // TODO: Include ambiguous_ranges, converted to line numbers.336 {
337 // This would work best if we add `line_hint` parameter337 let line_numbers = ambiguous_ranges
338 // to edit_file_tool338 .iter()
339 formatdoc! {"339 .map(|range| range.start.to_string())
340 <old_text> matches more than one position in the file. Read the340 .collect::<Vec<_>>()
341 relevant sections of {input_path} again and extend <old_text> so341 .join(", ");
342 that I can perform the requested edits.342 formatdoc! {"
343 "}343 <old_text> matches more than one position in the file (lines: {line_numbers}). Read the
344 relevant sections of {input_path} again and extend <old_text> so
345 that I can perform the requested edits.
346 "}
347 }
344 );348 );
345 Ok(ToolResultOutput {349 Ok(ToolResultOutput {
346 content: ToolResultContent::Text("No edits were made.".into()),350 content: ToolResultContent::Text("No edits were made.".into()),
@@ -800,11 +804,30 @@ impl ToolCard for EditFileToolCard { · 453 unmodified lines
800 if let Some(active_editor) = item.downcast::<Editor>() {804 if let Some(active_editor) = item.downcast::<Editor>() {
801 active_editor805 active_editor
802 .update_in(cx, |editor, window, cx| {806 .update_in(cx, |editor, window, cx| {
803 editor.go_to_singleton_buffer_point(807 let snapshot =
804 language::Point::new(0, 0),808 editor.buffer().read(cx).snapshot(cx);
805 window,809 let first_hunk = editor
806 cx,810 .diff_hunks_in_ranges(
807 );811 &[editor::Anchor::min()
812 ..editor::Anchor::max()],
813 &snapshot,
814 )
815 .next();
816 if let Some(first_hunk) = first_hunk {
817 let first_hunk_start =
818 first_hunk.multi_buffer_range().start;
819 editor.change_selections(
820 Some(Autoscroll::fit()),
821 window,
822 cx,
823 |selections| {
824 selections.select_anchor_ranges([
825 first_hunk_start
826 ..first_hunk_start,
827 ]);
828 },
829 )
830 }
808 })831 })
809 .log_err();832 .log_err();
810 }833 }
crates/assistant_tools/src/read_file_tool.rs+2−2
@@ -31,8 +31,8 @@ pub struct ReadFileToolInput { · 30 unmodified lines
31 /// <example>31 /// <example>
32 /// If the project has the following root directories:32 /// If the project has the following root directories:
33 ///33 ///
34 /// - directory134 /// - /a/b/directory1
35 /// - directory235 /// - /c/d/directory2
36 ///36 ///
37 /// If you want to access `file.txt` in `directory1`, you should use the path `directory1/file.txt`.37 /// If you want to access `file.txt` in `directory1`, you should use the path `directory1/file.txt`.
38 /// If you want to access `file.txt` in `directory2`, you should use the path `directory2/file.txt`.38 /// If you want to access `file.txt` in `directory2`, you should use the path `directory2/file.txt`.
crates/assistant_tools/src/templates/edit_file_prompt.hbs+6−5
@@ -3,21 +3,21 @@ You MUST respond with a series of edits to a file, using the following format: · 2 unmodified lines
3```3```
4<edits>4<edits>
5 5
6<old_text>6<old_text line=10>
7OLD TEXT 1 HERE7OLD TEXT 1 HERE
8</old_text>8</old_text>
9<new_text>9<new_text>
10NEW TEXT 1 HERE10NEW TEXT 1 HERE
11</new_text>11</new_text>
12 12
13<old_text>13<old_text line=456>
14OLD TEXT 2 HERE14OLD TEXT 2 HERE
15</old_text>15</old_text>
16<new_text>16<new_text>
17NEW TEXT 2 HERE17NEW TEXT 2 HERE
18</new_text>18</new_text>
19 19
20<old_text>20<old_text line=42>
21OLD TEXT 3 HERE21OLD TEXT 3 HERE
22</old_text>22</old_text>
23<new_text>23<new_text>
@@ -33,6 +33,7 @@ NEW TEXT 3 HERE · 9 unmodified lines
33- `<old_text>` must exactly match existing file content, including indentation33- `<old_text>` must exactly match existing file content, including indentation
34- `<old_text>` must come from the actual file, not an outline34- `<old_text>` must come from the actual file, not an outline
35- `<old_text>` cannot be empty35- `<old_text>` cannot be empty
36- `line` should be a starting line number for the text to be replaced
36- Be minimal with replacements:37- Be minimal with replacements:
37 - For unique lines, include only those lines38 - For unique lines, include only those lines
38 - For non-unique lines, include enough context to identify them39 - For non-unique lines, include enough context to identify them
@@ -48,7 +49,7 @@ Claude and gpt-4.1 don't really need it. --}} · 9 unmodified lines
48<example>49<example>
49<edits>50<edits>
50 51
51<old_text>52<old_text line=3>
52struct User {53struct User {
53 name: String,54 name: String,
54 email: String,55 email: String,
@@ -62,7 +63,7 @@ struct User { · 7 unmodified lines
62}63}
63</new_text>64</new_text>
64 65
65<old_text>66<old_text line=25>
66 let user = User {67 let user = User {
67 name: String::from("John"),68 name: String::from("John"),
68 email: String::from("john@example.com"),69 email: String::from("john@example.com"),
crates/assistant_tools/src/terminal_tool.rs+30−23
@@ -638,29 +638,36 @@ impl ToolCard for TerminalToolCard { · 637 unmodified lines
638 .bg(cx.theme().colors().editor_background)638 .bg(cx.theme().colors().editor_background)
639 .rounded_b_md()639 .rounded_b_md()
640 .text_ui_sm(cx)640 .text_ui_sm(cx)
641 .child(641 .child({
642 ToolOutputPreview::new(642 let content_mode = terminal.read(cx).content_mode(window, cx);
643 terminal.clone().into_any_element(),643
644 terminal.entity_id(),644 if content_mode.is_scrollable() {
645 )645 div().h_72().child(terminal.clone()).into_any_element()
646 .with_total_lines(self.content_line_count)646 } else {
647 .toggle_state(!terminal.read(cx).is_content_limited(window))647 ToolOutputPreview::new(
648 .on_toggle({648 terminal.clone().into_any_element(),
649 let terminal = terminal.clone();649 terminal.entity_id(),
650 move |is_expanded, _, cx| {650 )
651 terminal.update(cx, |terminal, cx| {651 .with_total_lines(self.content_line_count)
652 terminal.set_embedded_mode(652 .toggle_state(!content_mode.is_limited())
653 if is_expanded {653 .on_toggle({
654 None654 let terminal = terminal.clone();
655 } else {655 move |is_expanded, _, cx| {
656 Some(COLLAPSED_LINES)656 terminal.update(cx, |terminal, cx| {
657 },657 terminal.set_embedded_mode(
658 cx,658 if is_expanded {
659 );659 None
660 });660 } else {
661 }661 Some(COLLAPSED_LINES)
662 }),662 },
663 ),663 cx,
664 );
665 });
666 }
667 })
668 .into_any_element()
669 }
670 }),
664 )671 )
665 },672 },
666 )673 )
crates/auto_update/src/auto_update.rs+20−5
@@ -221,7 +221,7 @@ pub fn check(_: &Check, window: &mut Window, cx: &mut App) { · 220 unmodified lines
221 }221 }
222 222
223 if let Some(updater) = AutoUpdater::get(cx) {223 if let Some(updater) = AutoUpdater::get(cx) {
224 updater.update(cx, |updater, cx| updater.poll(cx));224 updater.update(cx, |updater, cx| updater.poll(UpdateCheckType::Manual, cx));
225 } else {225 } else {
226 drop(window.prompt(226 drop(window.prompt(
227 gpui::PromptLevel::Info,227 gpui::PromptLevel::Info,
@@ -296,6 +296,11 @@ impl InstallerDir { · 68 unmodified lines
296 }296 }
297}297}
298 298
299pub enum UpdateCheckType {
300 Automatic,
301 Manual,
302}
303
299impl AutoUpdater {304impl AutoUpdater {
300 pub fn get(cx: &mut App) -> Option<Entity<Self>> {305 pub fn get(cx: &mut App) -> Option<Entity<Self>> {
301 cx.default_global::<GlobalAutoUpdate>().0.clone()306 cx.default_global::<GlobalAutoUpdate>().0.clone()
@@ -313,13 +318,13 @@ impl AutoUpdater { · 11 unmodified lines
313 pub fn start_polling(&self, cx: &mut Context<Self>) -> Task<Result<()>> {318 pub fn start_polling(&self, cx: &mut Context<Self>) -> Task<Result<()>> {
314 cx.spawn(async move |this, cx| {319 cx.spawn(async move |this, cx| {
315 loop {320 loop {
316 this.update(cx, |this, cx| this.poll(cx))?;321 this.update(cx, |this, cx| this.poll(UpdateCheckType::Automatic, cx))?;
317 cx.background_executor().timer(POLL_INTERVAL).await;322 cx.background_executor().timer(POLL_INTERVAL).await;
318 }323 }
319 })324 })
320 }325 }
321 326
322 pub fn poll(&mut self, cx: &mut Context<Self>) {327 pub fn poll(&mut self, check_type: UpdateCheckType, cx: &mut Context<Self>) {
323 if self.pending_poll.is_some() {328 if self.pending_poll.is_some() {
324 return;329 return;
325 }330 }
@@ -331,8 +336,18 @@ impl AutoUpdater { · 5 unmodified lines
331 this.update(cx, |this, cx| {336 this.update(cx, |this, cx| {
332 this.pending_poll = None;337 this.pending_poll = None;
333 if let Err(error) = result {338 if let Err(error) = result {
334 log::error!("auto-update failed: error:{:?}", error);339 this.status = match check_type {
335 this.status = AutoUpdateStatus::Errored;340 // Be quiet if the check was automated (e.g. when offline)
341 UpdateCheckType::Automatic => {
342 log::info!("auto-update check failed: error:{:?}", error);
343 AutoUpdateStatus::Idle
344 }
345 UpdateCheckType::Manual => {
346 log::error!("auto-update failed: error:{:?}", error);
347 AutoUpdateStatus::Errored
348 }
349 };
350
336 cx.notify();351 cx.notify();
337 }352 }
338 })353 })
crates/bedrock/src/models.rs+4−0
@@ -452,6 +452,10 @@ impl Model { · 451 unmodified lines
452 | Model::Claude3_5SonnetV2452 | Model::Claude3_5SonnetV2
453 | Model::Claude3_7Sonnet453 | Model::Claude3_7Sonnet
454 | Model::Claude3_7SonnetThinking454 | Model::Claude3_7SonnetThinking
455 | Model::ClaudeSonnet4
456 | Model::ClaudeSonnet4Thinking
457 | Model::ClaudeOpus4
458 | Model::ClaudeOpus4Thinking
455 | Model::Claude3Haiku459 | Model::Claude3Haiku
456 | Model::Claude3Opus460 | Model::Claude3Opus
457 | Model::Claude3Sonnet461 | Model::Claude3Sonnet
crates/channel/src/channel_store.rs+1−1
@@ -111,7 +111,7 @@ pub struct ChannelMembership { · 110 unmodified lines
111 pub role: proto::ChannelRole,111 pub role: proto::ChannelRole,
112}112}
113impl ChannelMembership {113impl ChannelMembership {
114 pub fn sort_key(&self) -> MembershipSortKey {114 pub fn sort_key(&self) -> MembershipSortKey<'_> {
115 MembershipSortKey {115 MembershipSortKey {
116 role_order: match self.role {116 role_order: match self.role {
117 proto::ChannelRole::Admin => 0,117 proto::ChannelRole::Admin => 0,
crates/channel/src/channel_store/channel_index.rs+1−1
@@ -32,7 +32,7 @@ impl ChannelIndex { · 31 unmodified lines
32 .retain(|channel_id| !channels.contains(channel_id));32 .retain(|channel_id| !channels.contains(channel_id));
33 }33 }
34 34
35 pub fn bulk_insert(&mut self) -> ChannelPathsInsertGuard {35 pub fn bulk_insert(&mut self) -> ChannelPathsInsertGuard<'_> {
36 ChannelPathsInsertGuard {36 ChannelPathsInsertGuard {
37 channels_ordered: &mut self.channels_ordered,37 channels_ordered: &mut self.channels_ordered,
38 channels_by_id: &mut self.channels_by_id,38 channels_by_id: &mut self.channels_by_id,
crates/channel/src/channel_store_tests.rs+0−3
@@ -269,7 +269,6 @@ async fn test_channel_messages(cx: &mut TestAppContext) { · 268 unmodified lines
269 github_login: "nathansobo".into(),269 github_login: "nathansobo".into(),
270 avatar_url: "http://avatar.com/nathansobo".into(),270 avatar_url: "http://avatar.com/nathansobo".into(),
271 name: None,271 name: None,
272 email: None,
273 }],272 }],
274 },273 },
275 );274 );
@@ -323,7 +322,6 @@ async fn test_channel_messages(cx: &mut TestAppContext) { · 47 unmodified lines
323 github_login: "maxbrunsfeld".into(),322 github_login: "maxbrunsfeld".into(),
324 avatar_url: "http://avatar.com/maxbrunsfeld".into(),323 avatar_url: "http://avatar.com/maxbrunsfeld".into(),
325 name: None,324 name: None,
326 email: None,
327 }],325 }],
328 },326 },
329 );327 );
@@ -368,7 +366,6 @@ async fn test_channel_messages(cx: &mut TestAppContext) { · 38 unmodified lines
368 github_login: "as-cii".into(),366 github_login: "as-cii".into(),
369 avatar_url: "http://avatar.com/as-cii".into(),367 avatar_url: "http://avatar.com/as-cii".into(),
370 name: None,368 name: None,
371 email: None,
372 }],369 }],
373 },370 },
374 );371 );
crates/cli/src/main.rs+3−0
@@ -127,6 +127,9 @@ fn parse_path_with_position(argument_str: &str) -> anyhow::Result<String> { · 126 unmodified lines
127}127}
128 128
129fn main() -> Result<()> {129fn main() -> Result<()> {
130 #[cfg(unix)]
131 util::prevent_root_execution();
132
130 // Exit flatpak sandbox if needed133 // Exit flatpak sandbox if needed
131 #[cfg(any(target_os = "linux", target_os = "freebsd"))]134 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
132 {135 {
crates/client/Cargo.toml+7−2
@@ -28,6 +28,9 @@ feature_flags.workspace = true · 27 unmodified lines
28futures.workspace = true28futures.workspace = true
29gpui.workspace = true29gpui.workspace = true
30gpui_tokio.workspace = true30gpui_tokio.workspace = true
31# Don't update `hickory-resolver`, it has a bug that causes it to not resolve DNS queries correctly.
32# See https://github.com/hickory-dns/hickory-dns/issues/3048
33hickory-resolver = { version = "0.24", features = ["tokio-runtime"] }
31http_client.workspace = true34http_client.workspace = true
32http_client_tls.workspace = true35http_client_tls.workspace = true
33httparse = "1.10"36httparse = "1.10"
@@ -36,6 +39,7 @@ paths.workspace = true · 2 unmodified lines
36parking_lot.workspace = true39parking_lot.workspace = true
37postage.workspace = true40postage.workspace = true
38rand.workspace = true41rand.workspace = true
42regex.workspace = true
39release_channel.workspace = true43release_channel.workspace = true
40rpc = { workspace = true, features = ["gpui"] }44rpc = { workspace = true, features = ["gpui"] }
41schemars.workspace = true45schemars.workspace = true
@@ -48,7 +52,7 @@ telemetry_events.workspace = true · 6 unmodified lines
48text.workspace = true52text.workspace = true
49thiserror.workspace = true53thiserror.workspace = true
50time.workspace = true54time.workspace = true
51tiny_http = "0.8"55tiny_http.workspace = true
52tokio-socks = { version = "0.5.2", default-features = false, features = ["futures-io"] }56tokio-socks = { version = "0.5.2", default-features = false, features = ["futures-io"] }
53url.workspace = true57url.workspace = true
54util.workspace = true58util.workspace = true
@@ -60,11 +64,12 @@ workspace-hack.workspace = true · 5 unmodified lines
60[dev-dependencies]64[dev-dependencies]
61clock = { workspace = true, features = ["test-support"] }65clock = { workspace = true, features = ["test-support"] }
62collections = { workspace = true, features = ["test-support"] }66collections = { workspace = true, features = ["test-support"] }
67fs.workspace = true
63gpui = { workspace = true, features = ["test-support"] }68gpui = { workspace = true, features = ["test-support"] }
69http_client = { workspace = true, features = ["test-support"] }
64rpc = { workspace = true, features = ["test-support"] }70rpc = { workspace = true, features = ["test-support"] }
65settings = { workspace = true, features = ["test-support"] }71settings = { workspace = true, features = ["test-support"] }
66util = { workspace = true, features = ["test-support"] }72util = { workspace = true, features = ["test-support"] }
67http_client = { workspace = true, features = ["test-support"] }
68 73
69[target.'cfg(target_os = "windows")'.dependencies]74[target.'cfg(target_os = "windows")'.dependencies]
70windows.workspace = true75windows.workspace = true
crates/client/src/client.rs+10−2
@@ -1887,8 +1887,16 @@ mod tests { · 1886 unmodified lines
1887 .set_entity(&entity3, &mut cx.to_async());1887 .set_entity(&entity3, &mut cx.to_async());
1888 drop(subscription3);1888 drop(subscription3);
1889 1889
1890 server.send(proto::JoinProject { project_id: 1 });1890 server.send(proto::JoinProject {
1891 server.send(proto::JoinProject { project_id: 2 });1891 project_id: 1,
1892 committer_name: None,
1893 committer_email: None,
1894 });
1895 server.send(proto::JoinProject {
1896 project_id: 2,
1897 committer_name: None,
1898 committer_email: None,
1899 });
1892 done_rx1.recv().await.unwrap();1900 done_rx1.recv().await.unwrap();
1893 done_rx2.recv().await.unwrap();1901 done_rx2.recv().await.unwrap();
1894 }1902 }
crates/client/src/proxy.rs+44−6
@@ -3,20 +3,30 @@ · 2 unmodified lines
3mod http_proxy;3mod http_proxy;
4mod socks_proxy;4mod socks_proxy;
5 5
6use std::sync::LazyLock;
7
6use anyhow::{Context as _, Result};8use anyhow::{Context as _, Result};
9use hickory_resolver::{
10 AsyncResolver, TokioAsyncResolver,
11 config::LookupIpStrategy,
12 name_server::{GenericConnector, TokioRuntimeProvider},
13 system_conf,
14};
7use http_client::Url;15use http_client::Url;
8use http_proxy::{HttpProxyType, connect_http_proxy_stream, parse_http_proxy};16use http_proxy::{HttpProxyType, connect_http_proxy_stream, parse_http_proxy};
9use socks_proxy::{SocksVersion, connect_socks_proxy_stream, parse_socks_proxy};17use socks_proxy::{SocksVersion, connect_socks_proxy_stream, parse_socks_proxy};
18use tokio_socks::{IntoTargetAddr, TargetAddr};
19use util::ResultExt;
10 20
11pub(crate) async fn connect_proxy_stream(21pub(crate) async fn connect_proxy_stream(
12 proxy: &Url,22 proxy: &Url,
13 rpc_host: (&str, u16),23 rpc_host: (&str, u16),
14) -> Result<Box<dyn AsyncReadWrite>> {24) -> Result<Box<dyn AsyncReadWrite>> {
15 let Some(((proxy_domain, proxy_port), proxy_type)) = parse_proxy_type(proxy) else {25 let Some(((proxy_domain, proxy_port), proxy_type)) = parse_proxy_type(proxy).await else {
16 // If parsing the proxy URL fails, we must avoid falling back to an insecure connection.26 // If parsing the proxy URL fails, we must avoid falling back to an insecure connection.
17 // SOCKS proxies are often used in contexts where security and privacy are critical,27 // SOCKS proxies are often used in contexts where security and privacy are critical,
18 // so any fallback could expose users to significant risks.28 // so any fallback could expose users to significant risks.
19 anyhow::bail!("Parsing proxy url failed");29 anyhow::bail!("Parsing proxy url type failed");
20 };30 };
21 31
22 // Connect to proxy and wrap protocol later32 // Connect to proxy and wrap protocol later
@@ -39,10 +49,8 @@ enum ProxyType<'t> { · 16 unmodified lines
39 HttpProxy(HttpProxyType<'t>),49 HttpProxy(HttpProxyType<'t>),
40}50}
41 51
42fn parse_proxy_type(proxy: &Url) -> Option<((String, u16), ProxyType)> {52async fn parse_proxy_type(proxy: &Url) -> Option<((String, u16), ProxyType<'_>)> {
43 let scheme = proxy.scheme();53 let scheme = proxy.scheme();
44 let host = proxy.host()?.to_string();
45 let port = proxy.port_or_known_default()?;
46 let proxy_type = match scheme {54 let proxy_type = match scheme {
47 scheme if scheme.starts_with("socks") => {55 scheme if scheme.starts_with("socks") => {
48 Some(ProxyType::SocksProxy(parse_socks_proxy(scheme, proxy)))56 Some(ProxyType::SocksProxy(parse_socks_proxy(scheme, proxy)))
@@ -52,8 +60,38 @@ fn parse_proxy_type(proxy: &Url) -> Option<((String, u16), ProxyType)> { · 3 unmodified lines
52 }60 }
53 _ => None,61 _ => None,
54 }?;62 }?;
63 let (ip, port) = {
64 let host = proxy.host()?.to_string();
65 let port = proxy.port_or_known_default()?;
66 resolve_proxy_url_if_needed((host, port)).await.log_err()?
67 };
68
69 Some(((ip, port), proxy_type))
70}
71
72static SYSTEM_DNS_RESOLVER: LazyLock<AsyncResolver<GenericConnector<TokioRuntimeProvider>>> =
73 LazyLock::new(|| {
74 let (config, mut opts) = system_conf::read_system_conf().unwrap();
75 opts.ip_strategy = LookupIpStrategy::Ipv4AndIpv6;
76 TokioAsyncResolver::tokio(config, opts)
77 });
55 78
56 Some(((host, port), proxy_type))79async fn resolve_proxy_url_if_needed(proxy: (String, u16)) -> Result<(String, u16)> {
80 let proxy = proxy
81 .into_target_addr()
82 .context("Failed to parse proxy addr")?;
83 match proxy {
84 TargetAddr::Domain(domain, port) => {
85 let ip = SYSTEM_DNS_RESOLVER
86 .lookup_ip(domain.as_ref())
87 .await?
88 .into_iter()
89 .next()
90 .ok_or_else(|| anyhow::anyhow!("No IP found for proxy domain {domain}"))?;
91 Ok((ip.to_string(), port))
92 }
93 TargetAddr::Ip(ip_addr) => Ok((ip_addr.ip().to_string(), ip_addr.port())),
94 }
57}95}
58 96
59pub(crate) trait AsyncReadWrite:97pub(crate) trait AsyncReadWrite:
crates/client/src/proxy/socks_proxy.rs+8−2
@@ -1,5 +1,7 @@
1//! socks proxy1//! socks proxy
2 2
3use std::net::SocketAddr;
4
3use anyhow::{Context as _, Result};5use anyhow::{Context as _, Result};
4use http_client::Url;6use http_client::Url;
5use tokio::net::TcpStream;7use tokio::net::TcpStream;
@@ -8,6 +10,8 @@ use tokio_socks::{ · 2 unmodified lines
8 tcp::{Socks4Stream, Socks5Stream},10 tcp::{Socks4Stream, Socks5Stream},
9};11};
10 12
13use crate::proxy::SYSTEM_DNS_RESOLVER;
14
11use super::AsyncReadWrite;15use super::AsyncReadWrite;
12 16
13/// Identification to a Socks V4 Proxy17/// Identification to a Socks V4 Proxy
@@ -73,12 +77,14 @@ pub(super) async fn connect_socks_proxy_stream( · 59 unmodified lines
73 };77 };
74 let rpc_host = match (rpc_host, local_dns) {78 let rpc_host = match (rpc_host, local_dns) {
75 (TargetAddr::Domain(domain, port), true) => {79 (TargetAddr::Domain(domain, port), true) => {
76 let ip_addr = tokio::net::lookup_host((domain.as_ref(), port))80 let ip_addr = SYSTEM_DNS_RESOLVER
81 .lookup_ip(domain.as_ref())
77 .await82 .await
78 .with_context(|| format!("Failed to lookup domain {}", domain))?83 .with_context(|| format!("Failed to lookup domain {}", domain))?
84 .into_iter()
79 .next()85 .next()
80 .ok_or_else(|| anyhow::anyhow!("Failed to lookup domain {}", domain))?;86 .ok_or_else(|| anyhow::anyhow!("Failed to lookup domain {}", domain))?;
81 TargetAddr::Ip(ip_addr)87 TargetAddr::Ip(SocketAddr::new(ip_addr, port))
82 }88 }
83 (rpc_host, _) => rpc_host,89 (rpc_host, _) => rpc_host,
84 };90 };
crates/client/src/telemetry.rs+175−67
@@ -8,10 +8,11 @@ use futures::{Future, FutureExt, StreamExt}; · 7 unmodified lines
8use gpui::{App, AppContext as _, BackgroundExecutor, Task};8use gpui::{App, AppContext as _, BackgroundExecutor, Task};
9use http_client::{self, AsyncBody, HttpClient, HttpClientWithUrl, Method, Request};9use http_client::{self, AsyncBody, HttpClient, HttpClientWithUrl, Method, Request};
10use parking_lot::Mutex;10use parking_lot::Mutex;
11use regex::Regex;
11use release_channel::ReleaseChannel;12use release_channel::ReleaseChannel;
12use settings::{Settings, SettingsStore};13use settings::{Settings, SettingsStore};
13use sha2::{Digest, Sha256};14use sha2::{Digest, Sha256};
14use std::collections::{HashMap, HashSet};15use std::collections::HashSet;
15use std::fs::File;16use std::fs::File;
16use std::io::Write;17use std::io::Write;
17use std::sync::LazyLock;18use std::sync::LazyLock;
@@ -45,31 +46,13 @@ struct TelemetryState { · 27 unmodified lines
45 first_event_date_time: Option<Instant>,46 first_event_date_time: Option<Instant>,
46 event_coalescer: EventCoalescer,47 event_coalescer: EventCoalescer,
47 max_queue_size: usize,48 max_queue_size: usize,
48 worktree_id_map: WorktreeIdMap,49 worktrees_with_project_type_events_sent: HashSet<WorktreeId>,
49 50
50 os_name: String,51 os_name: String,
51 app_version: String,52 app_version: String,
52 os_version: Option<String>,53 os_version: Option<String>,
53}54}
54 55
55#[derive(Debug)]
56struct WorktreeIdMap(HashMap<String, ProjectCache>);
57
58#[derive(Debug)]
59struct ProjectCache {
60 name: String,
61 worktree_ids_reported: HashSet<WorktreeId>,
62}
63
64impl ProjectCache {
65 fn new(name: String) -> Self {
66 Self {
67 name,
68 worktree_ids_reported: HashSet::default(),
69 }
70 }
71}
72
73#[cfg(debug_assertions)]56#[cfg(debug_assertions)]
74const MAX_QUEUE_LEN: usize = 5;57const MAX_QUEUE_LEN: usize = 5;
75 58
@@ -91,6 +74,10 @@ static ZED_CLIENT_CHECKSUM_SEED: LazyLock<Option<Vec<u8>>> = LazyLock::new(|| { · 15 unmodified lines
91 })74 })
92});75});
93 76
77static DOTNET_PROJECT_FILES_REGEX: LazyLock<Regex> = LazyLock::new(|| {
78 Regex::new(r"^(global\.json|Directory\.Build\.props|.*\.(csproj|fsproj|vbproj|sln))$").unwrap()
79});
80
94pub fn os_name() -> String {81pub fn os_name() -> String {
95 #[cfg(target_os = "macos")]82 #[cfg(target_os = "macos")]
96 {83 {
@@ -194,20 +181,7 @@ impl Telemetry { · 97 unmodified lines
194 first_event_date_time: None,181 first_event_date_time: None,
195 event_coalescer: EventCoalescer::new(clock.clone()),182 event_coalescer: EventCoalescer::new(clock.clone()),
196 max_queue_size: MAX_QUEUE_LEN,183 max_queue_size: MAX_QUEUE_LEN,
197 worktree_id_map: WorktreeIdMap(HashMap::from_iter([184 worktrees_with_project_type_events_sent: HashSet::new(),
198 (
199 "pnpm-lock.yaml".to_string(),
200 ProjectCache::new("pnpm".to_string()),
201 ),
202 (
203 "yarn.lock".to_string(),
204 ProjectCache::new("yarn".to_string()),
205 ),
206 (
207 "package.json".to_string(),
208 ProjectCache::new("node".to_string()),
209 ),
210 ])),
211 185
212 os_version: None,186 os_version: None,
213 os_name: os_name(),187 os_name: os_name(),
@@ -371,49 +345,58 @@ impl Telemetry { · 157 unmodified lines
371 }345 }
372 }346 }
373 347
374 pub fn report_discovered_project_events(348 pub fn report_discovered_project_type_events(
375 self: &Arc<Self>,349 self: &Arc<Self>,
376 worktree_id: WorktreeId,350 worktree_id: WorktreeId,
377 updated_entries_set: &UpdatedEntriesSet,351 updated_entries_set: &UpdatedEntriesSet,
378 ) {352 ) {
379 let project_type_names: Vec<String> = {353 let project_type_names = self.detect_project_types(worktree_id, updated_entries_set);
380 let mut state = self.state.lock();
381 state
382 .worktree_id_map
383 .0
384 .iter_mut()
385 .filter_map(|(project_file_name, project_type_telemetry)| {
386 if project_type_telemetry
387 .worktree_ids_reported
388 .contains(&worktree_id)
389 {
390 return None;
391 }
392 354
393 let project_file_found = updated_entries_set.iter().any(|(path, _, _)| {355 for project_type_name in project_type_names {
394 path.as_ref()356 telemetry::event!("Project Opened", project_type = project_type_name);
395 .file_name()357 }
396 .and_then(|name| name.to_str())358 }
397 .map(|name_str| name_str == project_file_name)
398 .unwrap_or(false)
399 });
400 359
401 if !project_file_found {360 fn detect_project_types(
402 return None;361 self: &Arc<Self>,
403 }362 worktree_id: WorktreeId,
363 updated_entries_set: &UpdatedEntriesSet,
364 ) -> Vec<String> {
365 let mut state = self.state.lock();
366 let mut project_names: HashSet<String> = HashSet::new();
404 367
405 project_type_telemetry368 if state
406 .worktree_ids_reported369 .worktrees_with_project_type_events_sent
407 .insert(worktree_id);370 .contains(&worktree_id)
371 {
372 return project_names.into_iter().collect();
373 }
408 374
409 Some(project_type_telemetry.name.clone())375 for (path, _, _) in updated_entries_set.iter() {
410 })376 let Some(file_name) = path.file_name().and_then(|f| f.to_str()) else {
411 .collect()377 continue;
412 };378 };
413 379
414 for project_type_name in project_type_names {380 if file_name == "pnpm-lock.yaml" {
415 telemetry::event!("Project Opened", project_type = project_type_name);381 project_names.insert("pnpm".to_string());
382 } else if file_name == "yarn.lock" {
383 project_names.insert("yarn".to_string());
384 } else if file_name == "package.json" {
385 project_names.insert("node".to_string());
386 } else if DOTNET_PROJECT_FILES_REGEX.is_match(file_name) {
387 project_names.insert("dotnet".to_string());
388 }
416 }389 }
390
391 if !project_names.is_empty() {
392 state
393 .worktrees_with_project_type_events_sent
394 .insert(worktree_id);
395 }
396
397 let mut project_names_vec: Vec<String> = project_names.into_iter().collect();
398 project_names_vec.sort();
399 project_names_vec
417 }400 }
418 401
419 fn report_event(self: &Arc<Self>, event: Event) {402 fn report_event(self: &Arc<Self>, event: Event) {
@@ -578,7 +561,9 @@ mod tests { · 158 unmodified lines
578 use clock::FakeSystemClock;561 use clock::FakeSystemClock;
579 use gpui::TestAppContext;562 use gpui::TestAppContext;
580 use http_client::FakeHttpClient;563 use http_client::FakeHttpClient;
564 use std::collections::HashMap;
581 use telemetry_events::FlexibleEvent;565 use telemetry_events::FlexibleEvent;
566 use worktree::{PathChange, ProjectEntryId, WorktreeId};
582 567
583 #[gpui::test]568 #[gpui::test]
584 fn test_telemetry_flush_on_max_queue_size(cx: &mut TestAppContext) {569 fn test_telemetry_flush_on_max_queue_size(cx: &mut TestAppContext) {
@@ -696,6 +681,97 @@ mod tests { · 111 unmodified lines
696 });681 });
697 }682 }
698 683
684 #[gpui::test]
685 fn test_project_discovery_does_not_double_report(cx: &mut gpui::TestAppContext) {
686 init_test(cx);
687
688 let clock = Arc::new(FakeSystemClock::new());
689 let http = FakeHttpClient::with_200_response();
690 let telemetry = cx.update(|cx| Telemetry::new(clock.clone(), http, cx));
691 let worktree_id = 1;
692
693 // First scan of worktree 1 returns project types
694 test_project_discovery_helper(
695 telemetry.clone(),
696 vec!["package.json"],
697 vec!["node"],
698 worktree_id,
699 );
700
701 // Rescan of worktree 1 returns nothing as it has already been reported
702 test_project_discovery_helper(telemetry.clone(), vec!["package.json"], vec![], worktree_id);
703 }
704
705 #[gpui::test]
706 fn test_pnpm_project_discovery(cx: &mut gpui::TestAppContext) {
707 init_test(cx);
708
709 let clock = Arc::new(FakeSystemClock::new());
710 let http = FakeHttpClient::with_200_response();
711 let telemetry = cx.update(|cx| Telemetry::new(clock.clone(), http, cx));
712
713 test_project_discovery_helper(
714 telemetry.clone(),
715 vec!["package.json", "pnpm-lock.yaml"],
716 vec!["node", "pnpm"],
717 1,
718 );
719 }
720
721 #[gpui::test]
722 fn test_yarn_project_discovery(cx: &mut gpui::TestAppContext) {
723 init_test(cx);
724
725 let clock = Arc::new(FakeSystemClock::new());
726 let http = FakeHttpClient::with_200_response();
727 let telemetry = cx.update(|cx| Telemetry::new(clock.clone(), http, cx));
728
729 test_project_discovery_helper(
730 telemetry.clone(),
731 vec!["package.json", "yarn.lock"],
732 vec!["node", "yarn"],
733 1,
734 );
735 }
736
737 #[gpui::test]
738 fn test_dotnet_project_discovery(cx: &mut gpui::TestAppContext) {
739 init_test(cx);
740
741 let clock = Arc::new(FakeSystemClock::new());
742 let http = FakeHttpClient::with_200_response();
743 let telemetry = cx.update(|cx| Telemetry::new(clock.clone(), http, cx));
744
745 // Using different worktrees, as production code blocks from reporting a
746 // project type for the same worktree multiple times
747
748 test_project_discovery_helper(
749 telemetry.clone().clone(),
750 vec!["global.json"],
751 vec!["dotnet"],
752 1,
753 );
754 test_project_discovery_helper(
755 telemetry.clone(),
756 vec!["Directory.Build.props"],
757 vec!["dotnet"],
758 2,
759 );
760 test_project_discovery_helper(telemetry.clone(), vec!["file.csproj"], vec!["dotnet"], 3);
761 test_project_discovery_helper(telemetry.clone(), vec!["file.fsproj"], vec!["dotnet"], 4);
762 test_project_discovery_helper(telemetry.clone(), vec!["file.vbproj"], vec!["dotnet"], 5);
763 test_project_discovery_helper(telemetry.clone(), vec!["file.sln"], vec!["dotnet"], 6);
764
765 // Each worktree should only send a single project type event, even when
766 // encountering multiple files associated with that project type
767 test_project_discovery_helper(
768 telemetry,
769 vec!["global.json", "Directory.Build.props"],
770 vec!["dotnet"],
771 7,
772 );
773 }
774
699 // TODO:775 // TODO:
700 // Test settings776 // Test settings
701 // Update FakeHTTPClient to keep track of the number of requests and assert on it777 // Update FakeHTTPClient to keep track of the number of requests and assert on it
@@ -712,4 +788,36 @@ mod tests { · 10 unmodified lines
712 && telemetry.state.lock().flush_events_task.is_none()788 && telemetry.state.lock().flush_events_task.is_none()
713 && telemetry.state.lock().first_event_date_time.is_none()789 && telemetry.state.lock().first_event_date_time.is_none()
714 }790 }
791
792 fn test_project_discovery_helper(
793 telemetry: Arc<Telemetry>,
794 file_paths: Vec<&str>,
795 expected_project_types: Vec<&str>,
796 worktree_id_num: usize,
797 ) {
798 let worktree_id = WorktreeId::from_usize(worktree_id_num);
799 let entries: Vec<_> = file_paths
800 .into_iter()
801 .enumerate()
802 .map(|(i, path)| {
803 (
804 Arc::from(std::path::Path::new(path)),
805 ProjectEntryId::from_proto(i as u64 + 1),
806 PathChange::Added,
807 )
808 })
809 .collect();
810 let updated_entries: UpdatedEntriesSet = Arc::from(entries.as_slice());
811
812 let mut detected_types = telemetry.detect_project_types(worktree_id, &updated_entries);
813 detected_types.sort();
814
815 let mut expected_sorted = expected_project_types
816 .into_iter()
817 .map(String::from)
818 .collect::<Vec<_>>();
819 expected_sorted.sort();
820
821 assert_eq!(detected_types, expected_sorted);
822 }
715}823}
crates/client/src/user.rs+4−2
@@ -49,7 +49,6 @@ pub struct User { · 48 unmodified lines
49 pub github_login: String,49 pub github_login: String,
50 pub avatar_uri: SharedUri,50 pub avatar_uri: SharedUri,
51 pub name: Option<String>,51 pub name: Option<String>,
52 pub email: Option<String>,
53}52}
54 53
55#[derive(Clone, Debug, PartialEq, Eq)]54#[derive(Clone, Debug, PartialEq, Eq)]
@@ -58,6 +57,8 @@ pub struct Collaborator { · 2 unmodified lines
58 pub replica_id: ReplicaId,57 pub replica_id: ReplicaId,
59 pub user_id: UserId,58 pub user_id: UserId,
60 pub is_host: bool,59 pub is_host: bool,
60 pub committer_name: Option<String>,
61 pub committer_email: Option<String>,
61}62}
62 63
63impl PartialOrd for User {64impl PartialOrd for User {
@@ -881,7 +882,6 @@ impl User { · 817 unmodified lines
881 github_login: message.github_login,882 github_login: message.github_login,
882 avatar_uri: message.avatar_url.into(),883 avatar_uri: message.avatar_url.into(),
883 name: message.name,884 name: message.name,
884 email: message.email,
885 })885 })
886 }886 }
887}887}
@@ -912,6 +912,8 @@ impl Collaborator { · 24 unmodified lines
912 replica_id: message.replica_id as ReplicaId,912 replica_id: message.replica_id as ReplicaId,
913 user_id: message.user_id as UserId,913 user_id: message.user_id as UserId,
914 is_host: message.is_host,914 is_host: message.is_host,
915 committer_name: message.committer_name,
916 committer_email: message.committer_email,
915 })917 })
916 }918 }
917}919}
crates/collab/migrations.sqlite/20221109000000_test_schema.sql+3−1
@@ -185,7 +185,9 @@ CREATE TABLE "project_collaborators" ( · 184 unmodified lines
185 "connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE,185 "connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE,
186 "user_id" INTEGER NOT NULL,186 "user_id" INTEGER NOT NULL,
187 "replica_id" INTEGER NOT NULL,187 "replica_id" INTEGER NOT NULL,
188 "is_host" BOOLEAN NOT NULL188 "is_host" BOOLEAN NOT NULL,
189 "committer_name" VARCHAR,
190 "committer_email" VARCHAR
189);191);
190 192
191CREATE INDEX "index_project_collaborators_on_project_id" ON "project_collaborators" ("project_id");193CREATE INDEX "index_project_collaborators_on_project_id" ON "project_collaborators" ("project_id");
crates/collab/migrations/20250612153105_add_collaborator_commit_email.sql+4−0
@@ -0,0 +1,4 @@
1alter table project_collaborators
2 add column committer_name varchar;
3alter table project_collaborators
4 add column committer_email varchar;
crates/collab/src/api.rs+3−3
@@ -97,7 +97,7 @@ impl std::fmt::Display for SystemIdHeader { · 96 unmodified lines
97 97
98pub fn routes(rpc_server: Arc<rpc::Server>) -> Router<(), Body> {98pub fn routes(rpc_server: Arc<rpc::Server>) -> Router<(), Body> {
99 Router::new()99 Router::new()
100 .route("/user", get(get_authenticated_user))100 .route("/user", get(update_or_create_authenticated_user))
101 .route("/users/look_up", get(look_up_user))101 .route("/users/look_up", get(look_up_user))
102 .route("/users/:id/access_tokens", post(create_access_token))102 .route("/users/:id/access_tokens", post(create_access_token))
103 .route("/rpc_server_snapshot", get(get_rpc_server_snapshot))103 .route("/rpc_server_snapshot", get(get_rpc_server_snapshot))
@@ -157,7 +157,7 @@ struct AuthenticatedUserResponse { · 53 unmodified lines
157 feature_flags: Vec<String>,157 feature_flags: Vec<String>,
158}158}
159 159
160async fn get_authenticated_user(160async fn update_or_create_authenticated_user(
161 Query(params): Query<AuthenticatedUserParams>,161 Query(params): Query<AuthenticatedUserParams>,
162 Extension(app): Extension<Arc<AppState>>,162 Extension(app): Extension<Arc<AppState>>,
163) -> Result<Json<AuthenticatedUserResponse>> {163) -> Result<Json<AuthenticatedUserResponse>> {
@@ -165,7 +165,7 @@ async fn get_authenticated_user( · 1 unmodified line
165 165
166 let user = app166 let user = app
167 .db167 .db
168 .get_or_create_user_by_github_account(168 .update_or_create_user_by_github_account(
169 &params.github_login,169 &params.github_login,
170 params.github_user_id,170 params.github_user_id,
171 params.github_email.as_deref(),171 params.github_email.as_deref(),
crates/collab/src/db.rs+4−0
@@ -751,6 +751,8 @@ pub struct ProjectCollaborator { · 750 unmodified lines
751 pub user_id: UserId,751 pub user_id: UserId,
752 pub replica_id: ReplicaId,752 pub replica_id: ReplicaId,
753 pub is_host: bool,753 pub is_host: bool,
754 pub committer_name: Option<String>,
755 pub committer_email: Option<String>,
754}756}
755 757
756impl ProjectCollaborator {758impl ProjectCollaborator {
@@ -760,6 +762,8 @@ impl ProjectCollaborator { · 3 unmodified lines
760 replica_id: self.replica_id.0 as u32,762 replica_id: self.replica_id.0 as u32,
761 user_id: self.user_id.to_proto(),763 user_id: self.user_id.to_proto(),
762 is_host: self.is_host,764 is_host: self.is_host,
765 committer_name: self.committer_name.clone(),
766 committer_email: self.committer_email.clone(),
763 }767 }
764 }768 }
765}769}
crates/collab/src/db/queries/buffers.rs+8−0
@@ -118,6 +118,8 @@ impl Database { · 117 unmodified lines
118 user_id: collaborator.user_id.to_proto(),118 user_id: collaborator.user_id.to_proto(),
119 replica_id: collaborator.replica_id.0 as u32,119 replica_id: collaborator.replica_id.0 as u32,
120 is_host: false,120 is_host: false,
121 committer_name: None,
122 committer_email: None,
121 })123 })
122 .collect(),124 .collect(),
123 })125 })
@@ -225,6 +227,8 @@ impl Database { · 101 unmodified lines
225 user_id: collaborator.user_id.to_proto(),227 user_id: collaborator.user_id.to_proto(),
226 replica_id: collaborator.replica_id.0 as u32,228 replica_id: collaborator.replica_id.0 as u32,
227 is_host: false,229 is_host: false,
230 committer_name: None,
231 committer_email: None,
228 })232 })
229 .collect(),233 .collect(),
230 },234 },
@@ -261,6 +265,8 @@ impl Database { · 30 unmodified lines
261 replica_id: db_collaborator.replica_id.0 as u32,265 replica_id: db_collaborator.replica_id.0 as u32,
262 user_id: db_collaborator.user_id.to_proto(),266 user_id: db_collaborator.user_id.to_proto(),
263 is_host: false,267 is_host: false,
268 committer_name: None,
269 committer_email: None,
264 })270 })
265 } else {271 } else {
266 collaborator_ids_to_remove.push(db_collaborator.id);272 collaborator_ids_to_remove.push(db_collaborator.id);
@@ -390,6 +396,8 @@ impl Database { · 123 unmodified lines
390 replica_id: row.replica_id.0 as u32,396 replica_id: row.replica_id.0 as u32,
391 user_id: row.user_id.to_proto(),397 user_id: row.user_id.to_proto(),
392 is_host: false,398 is_host: false,
399 committer_name: None,
400 committer_email: None,
393 });401 });
394 }402 }
395 403
crates/collab/src/db/queries/channels.rs+4−3
@@ -501,8 +501,10 @@ impl Database { · 500 unmodified lines
501 501
502 /// Returns all channels for the user with the given ID.502 /// Returns all channels for the user with the given ID.
503 pub async fn get_channels_for_user(&self, user_id: UserId) -> Result<ChannelsForUser> {503 pub async fn get_channels_for_user(&self, user_id: UserId) -> Result<ChannelsForUser> {
504 self.transaction(|tx| async move { self.get_user_channels(user_id, None, true, &tx).await })504 self.weak_transaction(
505 .await505 |tx| async move { self.get_user_channels(user_id, None, true, &tx).await },
506 )
507 .await
506 }508 }
507 509
508 /// Returns all channels for the user with the given ID that are descendants510 /// Returns all channels for the user with the given ID that are descendants
@@ -737,7 +739,6 @@ impl Database { · 228 unmodified lines
737 ),739 ),
738 github_login: user.github_login,740 github_login: user.github_login,
739 name: user.name,741 name: user.name,
740 email: user.email_address,
741 })742 })
742 }743 }
743 proto::ChannelMember {744 proto::ChannelMember {
crates/collab/src/db/queries/contacts.rs+2−2
@@ -15,7 +15,7 @@ impl Database { · 14 unmodified lines
15 user_b_busy: bool,15 user_b_busy: bool,
16 }16 }
17 17
18 self.transaction(|tx| async move {18 self.weak_transaction(|tx| async move {
19 let user_a_participant = Alias::new("user_a_participant");19 let user_a_participant = Alias::new("user_a_participant");
20 let user_b_participant = Alias::new("user_b_participant");20 let user_b_participant = Alias::new("user_b_participant");
21 let mut db_contacts = contact::Entity::find()21 let mut db_contacts = contact::Entity::find()
@@ -91,7 +91,7 @@ impl Database { · 69 unmodified lines
91 91
92 /// Returns whether the given user is a busy (on a call).92 /// Returns whether the given user is a busy (on a call).
93 pub async fn is_user_busy(&self, user_id: UserId) -> Result<bool> {93 pub async fn is_user_busy(&self, user_id: UserId) -> Result<bool> {
94 self.transaction(|tx| async move {94 self.weak_transaction(|tx| async move {
95 let participant = room_participant::Entity::find()95 let participant = room_participant::Entity::find()
96 .filter(room_participant::Column::UserId.eq(user_id))96 .filter(room_participant::Column::UserId.eq(user_id))
97 .one(&*tx)97 .one(&*tx)
crates/collab/src/db/queries/contributors.rs+1−1
@@ -71,7 +71,7 @@ impl Database { · 70 unmodified lines
71 ) -> Result<()> {71 ) -> Result<()> {
72 self.weak_transaction(|tx| async move {72 self.weak_transaction(|tx| async move {
73 let user = self73 let user = self
74 .get_or_create_user_by_github_account_tx(74 .update_or_create_user_by_github_account_tx(
75 github_login,75 github_login,
76 github_user_id,76 github_user_id,
77 github_email,77 github_email,
crates/collab/src/db/queries/projects.rs+29−7
@@ -98,7 +98,9 @@ impl Database { · 97 unmodified lines
98 user_id: ActiveValue::set(participant.user_id),98 user_id: ActiveValue::set(participant.user_id),
99 replica_id: ActiveValue::set(ReplicaId(replica_id)),99 replica_id: ActiveValue::set(ReplicaId(replica_id)),
100 is_host: ActiveValue::set(true),100 is_host: ActiveValue::set(true),
101 ..Default::default()101 id: ActiveValue::NotSet,
102 committer_name: ActiveValue::Set(None),
103 committer_email: ActiveValue::Set(None),
102 }104 }
103 .insert(&*tx)105 .insert(&*tx)
104 .await?;106 .await?;
@@ -784,13 +786,27 @@ impl Database { · 679 unmodified lines
784 project_id: ProjectId,786 project_id: ProjectId,
785 connection: ConnectionId,787 connection: ConnectionId,
786 user_id: UserId,788 user_id: UserId,
789 committer_name: Option<String>,
790 committer_email: Option<String>,
787 ) -> Result<TransactionGuard<(Project, ReplicaId)>> {791 ) -> Result<TransactionGuard<(Project, ReplicaId)>> {
788 self.project_transaction(project_id, |tx| async move {792 self.project_transaction(project_id, move |tx| {
789 let (project, role) = self793 let committer_name = committer_name.clone();
790 .access_project(project_id, connection, Capability::ReadOnly, &tx)794 let committer_email = committer_email.clone();
791 .await?;795 async move {
792 self.join_project_internal(project, user_id, connection, role, &tx)796 let (project, role) = self
797 .access_project(project_id, connection, Capability::ReadOnly, &tx)
798 .await?;
799 self.join_project_internal(
800 project,
801 user_id,
802 committer_name,
803 committer_email,
804 connection,
805 role,
806 &tx,
807 )
793 .await808 .await
809 }
794 })810 })
795 .await811 .await
796 }812 }
@@ -799,6 +815,8 @@ impl Database { · 2 unmodified lines
799 &self,815 &self,
800 project: project::Model,816 project: project::Model,
801 user_id: UserId,817 user_id: UserId,
818 committer_name: Option<String>,
819 committer_email: Option<String>,
802 connection: ConnectionId,820 connection: ConnectionId,
803 role: ChannelRole,821 role: ChannelRole,
804 tx: &DatabaseTransaction,822 tx: &DatabaseTransaction,
@@ -822,7 +840,9 @@ impl Database { · 17 unmodified lines
822 user_id: ActiveValue::set(user_id),840 user_id: ActiveValue::set(user_id),
823 replica_id: ActiveValue::set(replica_id),841 replica_id: ActiveValue::set(replica_id),
824 is_host: ActiveValue::set(false),842 is_host: ActiveValue::set(false),
825 ..Default::default()843 id: ActiveValue::NotSet,
844 committer_name: ActiveValue::set(committer_name),
845 committer_email: ActiveValue::set(committer_email),
826 }846 }
827 .insert(tx)847 .insert(tx)
828 .await?;848 .await?;
@@ -1026,6 +1046,8 @@ impl Database { · 197 unmodified lines
1026 user_id: collaborator.user_id,1046 user_id: collaborator.user_id,
1027 replica_id: collaborator.replica_id,1047 replica_id: collaborator.replica_id,
1028 is_host: collaborator.is_host,1048 is_host: collaborator.is_host,
1049 committer_name: collaborator.committer_name,
1050 committer_email: collaborator.committer_email,
1029 })1051 })
1030 .collect(),1052 .collect(),
1031 worktrees,1053 worktrees,
crates/collab/src/db/queries/rooms.rs+5−1
@@ -80,7 +80,7 @@ impl Database { · 79 unmodified lines
80 &self,80 &self,
81 user_id: UserId,81 user_id: UserId,
82 ) -> Result<Option<proto::IncomingCall>> {82 ) -> Result<Option<proto::IncomingCall>> {
83 self.transaction(|tx| async move {83 self.weak_transaction(|tx| async move {
84 let pending_participant = room_participant::Entity::find()84 let pending_participant = room_participant::Entity::find()
85 .filter(85 .filter(
86 room_participant::Column::UserId86 room_participant::Column::UserId
@@ -553,6 +553,8 @@ impl Database { · 466 unmodified lines
553 user_id: collaborator.user_id,553 user_id: collaborator.user_id,
554 replica_id: collaborator.replica_id,554 replica_id: collaborator.replica_id,
555 is_host: collaborator.is_host,555 is_host: collaborator.is_host,
556 committer_name: collaborator.committer_name.clone(),
557 committer_email: collaborator.committer_email.clone(),
556 })558 })
557 .collect(),559 .collect(),
558 worktrees: reshared_project.worktrees.clone(),560 worktrees: reshared_project.worktrees.clone(),
@@ -857,6 +859,8 @@ impl Database { · 298 unmodified lines
857 user_id: collaborator.user_id,859 user_id: collaborator.user_id,
858 replica_id: collaborator.replica_id,860 replica_id: collaborator.replica_id,
859 is_host: collaborator.is_host,861 is_host: collaborator.is_host,
862 committer_name: collaborator.committer_name,
863 committer_email: collaborator.committer_email,
860 })864 })
861 .collect::<Vec<_>>();865 .collect::<Vec<_>>();
862 866
crates/collab/src/db/queries/users.rs+3−3
@@ -111,7 +111,7 @@ impl Database { · 110 unmodified lines
111 .await111 .await
112 }112 }
113 113
114 pub async fn get_or_create_user_by_github_account(114 pub async fn update_or_create_user_by_github_account(
115 &self,115 &self,
116 github_login: &str,116 github_login: &str,
117 github_user_id: i32,117 github_user_id: i32,
@@ -121,7 +121,7 @@ impl Database { · 3 unmodified lines
121 initial_channel_id: Option<ChannelId>,121 initial_channel_id: Option<ChannelId>,
122 ) -> Result<User> {122 ) -> Result<User> {
123 self.transaction(|tx| async move {123 self.transaction(|tx| async move {
124 self.get_or_create_user_by_github_account_tx(124 self.update_or_create_user_by_github_account_tx(
125 github_login,125 github_login,
126 github_user_id,126 github_user_id,
127 github_email,127 github_email,
@@ -135,7 +135,7 @@ impl Database { · 7 unmodified lines
135 .await135 .await
136 }136 }
137 137
138 pub async fn get_or_create_user_by_github_account_tx(138 pub async fn update_or_create_user_by_github_account_tx(
139 &self,139 &self,
140 github_login: &str,140 github_login: &str,
141 github_user_id: i32,141 github_user_id: i32,
crates/collab/src/db/tables/project_collaborator.rs+2−0
@@ -13,6 +13,8 @@ pub struct Model { · 12 unmodified lines
13 pub user_id: UserId,13 pub user_id: UserId,
14 pub replica_id: ReplicaId,14 pub replica_id: ReplicaId,
15 pub is_host: bool,15 pub is_host: bool,
16 pub committer_name: Option<String>,
17 pub committer_email: Option<String>,
16}18}
17 19
18impl Model {20impl Model {
crates/collab/src/db/tests/buffer_tests.rs+4−0
@@ -126,12 +126,16 @@ async fn test_channel_buffers(db: &Arc<Database>) { · 125 unmodified lines
126 peer_id: Some(rpc::proto::PeerId { id: 1, owner_id }),126 peer_id: Some(rpc::proto::PeerId { id: 1, owner_id }),
127 replica_id: 0,127 replica_id: 0,
128 is_host: false,128 is_host: false,
129 committer_name: None,
130 committer_email: None,
129 },131 },
130 rpc::proto::Collaborator {132 rpc::proto::Collaborator {
131 user_id: b_id.to_proto(),133 user_id: b_id.to_proto(),
132 peer_id: Some(rpc::proto::PeerId { id: 2, owner_id }),134 peer_id: Some(rpc::proto::PeerId { id: 2, owner_id }),
133 replica_id: 1,135 replica_id: 1,
134 is_host: false,136 is_host: false,
137 committer_name: None,
138 committer_email: None,
135 }139 }
136 ]140 ]
137 );141 );
crates/collab/src/db/tests/db_tests.rs+13−6
@@ -72,12 +72,12 @@ async fn test_get_users(db: &Arc<Database>) { · 71 unmodified lines
72}72}
73 73
74test_both_dbs!(74test_both_dbs!(
75 test_get_or_create_user_by_github_account,75 test_update_or_create_user_by_github_account,
76 test_get_or_create_user_by_github_account_postgres,76 test_update_or_create_user_by_github_account_postgres,
77 test_get_or_create_user_by_github_account_sqlite77 test_update_or_create_user_by_github_account_sqlite
78);78);
79 79
80async fn test_get_or_create_user_by_github_account(db: &Arc<Database>) {80async fn test_update_or_create_user_by_github_account(db: &Arc<Database>) {
81 db.create_user(81 db.create_user(
82 "user1@example.com",82 "user1@example.com",
83 None,83 None,
@@ -104,7 +104,14 @@ async fn test_get_or_create_user_by_github_account(db: &Arc<Database>) { · 20 unmodified lines
104 .user_id;104 .user_id;
105 105
106 let user = db106 let user = db
107 .get_or_create_user_by_github_account("the-new-login2", 102, None, None, Utc::now(), None)107 .update_or_create_user_by_github_account(
108 "the-new-login2",
109 102,
110 None,
111 None,
112 Utc::now(),
113 None,
114 )
108 .await115 .await
109 .unwrap();116 .unwrap();
110 assert_eq!(user.id, user_id2);117 assert_eq!(user.id, user_id2);
@@ -112,7 +119,7 @@ async fn test_get_or_create_user_by_github_account(db: &Arc<Database>) { · 1 unmodified line
112 assert_eq!(user.github_user_id, 102);119 assert_eq!(user.github_user_id, 102);
113 120
114 let user = db121 let user = db
115 .get_or_create_user_by_github_account(122 .update_or_create_user_by_github_account(
116 "login3",123 "login3",
117 103,124 103,
118 Some("user3@example.com"),125 Some("user3@example.com"),
crates/collab/src/llm.rs+6−0
@@ -7,6 +7,12 @@ pub use token::*; · 6 unmodified lines
7 7
8pub const AGENT_EXTENDED_TRIAL_FEATURE_FLAG: &str = "agent-extended-trial";8pub const AGENT_EXTENDED_TRIAL_FEATURE_FLAG: &str = "agent-extended-trial";
9 9
10/// The name of the feature flag that bypasses the account age check.
11pub const BYPASS_ACCOUNT_AGE_CHECK_FEATURE_FLAG: &str = "bypass-account-age-check";
12
13/// The minimum account age an account must have in order to use the LLM service.
14pub const MIN_ACCOUNT_AGE_FOR_LLM_USE: chrono::Duration = chrono::Duration::days(30);
15
10/// The default value to use for maximum spend per month if the user did not16/// The default value to use for maximum spend per month if the user did not
11/// explicitly set a maximum spend.17/// explicitly set a maximum spend.
12///18///
crates/collab/src/llm/token.rs+2−2
@@ -1,6 +1,6 @@
1use crate::db::billing_subscription::SubscriptionKind;1use crate::db::billing_subscription::SubscriptionKind;
2use crate::db::{billing_customer, billing_subscription, user};2use crate::db::{billing_customer, billing_subscription, user};
3use crate::llm::AGENT_EXTENDED_TRIAL_FEATURE_FLAG;3use crate::llm::{AGENT_EXTENDED_TRIAL_FEATURE_FLAG, BYPASS_ACCOUNT_AGE_CHECK_FEATURE_FLAG};
4use crate::{Config, db::billing_preference};4use crate::{Config, db::billing_preference};
5use anyhow::{Context as _, Result};5use anyhow::{Context as _, Result};
6use chrono::{NaiveDateTime, Utc};6use chrono::{NaiveDateTime, Utc};
@@ -84,7 +84,7 @@ impl LlmTokenClaims { · 77 unmodified lines
84 .any(|flag| flag == "llm-closed-beta"),84 .any(|flag| flag == "llm-closed-beta"),
85 bypass_account_age_check: feature_flags85 bypass_account_age_check: feature_flags
86 .iter()86 .iter()
87 .any(|flag| flag == "bypass-account-age-check"),87 .any(|flag| flag == BYPASS_ACCOUNT_AGE_CHECK_FEATURE_FLAG),
88 can_use_web_search_tool: true,88 can_use_web_search_tool: true,
89 use_llm_request_queue: feature_flags.iter().any(|flag| flag == "llm-request-queue"),89 use_llm_request_queue: feature_flags.iter().any(|flag| flag == "llm-request-queue"),
90 plan,90 plan,
crates/collab/src/rpc.rs+64−29
@@ -4,14 +4,17 @@ use crate::api::billing::find_or_create_billing_customer; · 3 unmodified lines
4use crate::api::{CloudflareIpCountryHeader, SystemIdHeader};4use crate::api::{CloudflareIpCountryHeader, SystemIdHeader};
5use crate::db::billing_subscription::SubscriptionKind;5use crate::db::billing_subscription::SubscriptionKind;
6use crate::llm::db::LlmDatabase;6use crate::llm::db::LlmDatabase;
7use crate::llm::{AGENT_EXTENDED_TRIAL_FEATURE_FLAG, LlmTokenClaims};7use crate::llm::{
8 AGENT_EXTENDED_TRIAL_FEATURE_FLAG, BYPASS_ACCOUNT_AGE_CHECK_FEATURE_FLAG, LlmTokenClaims,
9 MIN_ACCOUNT_AGE_FOR_LLM_USE,
10};
8use crate::stripe_client::StripeCustomerId;11use crate::stripe_client::StripeCustomerId;
9use crate::{12use crate::{
10 AppState, Error, Result, auth,13 AppState, Error, Result, auth,
11 db::{14 db::{
12 self, BufferId, Capability, Channel, ChannelId, ChannelRole, ChannelsForUser,15 self, BufferId, Capability, Channel, ChannelId, ChannelRole, ChannelsForUser,
13 CreatedChannelMessage, Database, InviteMemberResult, MembershipUpdated, MessageId,16 CreatedChannelMessage, Database, InviteMemberResult, MembershipUpdated, MessageId,
14 NotificationId, Project, ProjectId, RejoinedProject, RemoveChannelMemberResult, ReplicaId,17 NotificationId, ProjectId, RejoinedProject, RemoveChannelMemberResult,
15 RespondToChannelInvite, RoomId, ServerId, UpdatedChannelMessage, User, UserId,18 RespondToChannelInvite, RoomId, ServerId, UpdatedChannelMessage, User, UserId,
16 },19 },
17 executor::Executor,20 executor::Executor,
@@ -65,7 +68,7 @@ use std::{ · 47 unmodified lines
65 rc::Rc,68 rc::Rc,
66 sync::{69 sync::{
67 Arc, OnceLock,70 Arc, OnceLock,
68 atomic::{AtomicBool, Ordering::SeqCst},71 atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst},
69 },72 },
70 time::{Duration, Instant},73 time::{Duration, Instant},
71};74};
@@ -86,10 +89,36 @@ pub const CLEANUP_TIMEOUT: Duration = Duration::from_secs(15); · 14 unmodified lines
86const MESSAGE_COUNT_PER_PAGE: usize = 100;89const MESSAGE_COUNT_PER_PAGE: usize = 100;
87const MAX_MESSAGE_LEN: usize = 1024;90const MAX_MESSAGE_LEN: usize = 1024;
88const NOTIFICATION_COUNT_PER_PAGE: usize = 50;91const NOTIFICATION_COUNT_PER_PAGE: usize = 50;
92const MAX_CONCURRENT_CONNECTIONS: usize = 512;
93
94static CONCURRENT_CONNECTIONS: AtomicUsize = AtomicUsize::new(0);
89 95
90type MessageHandler =96type MessageHandler =
91 Box<dyn Send + Sync + Fn(Box<dyn AnyTypedEnvelope>, Session) -> BoxFuture<'static, ()>>;97 Box<dyn Send + Sync + Fn(Box<dyn AnyTypedEnvelope>, Session) -> BoxFuture<'static, ()>>;
92 98
99pub struct ConnectionGuard;
100
101impl ConnectionGuard {
102 pub fn try_acquire() -> Result<Self, ()> {
103 let current_connections = CONCURRENT_CONNECTIONS.fetch_add(1, SeqCst);
104 if current_connections >= MAX_CONCURRENT_CONNECTIONS {
105 CONCURRENT_CONNECTIONS.fetch_sub(1, SeqCst);
106 tracing::error!(
107 "too many concurrent connections: {}",
108 current_connections + 1
109 );
110 return Err(());
111 }
112 Ok(ConnectionGuard)
113 }
114}
115
116impl Drop for ConnectionGuard {
117 fn drop(&mut self) {
118 CONCURRENT_CONNECTIONS.fetch_sub(1, SeqCst);
119 }
120}
121
93struct Response<R> {122struct Response<R> {
94 peer: Arc<Peer>,123 peer: Arc<Peer>,
95 receipt: Receipt<R>,124 receipt: Receipt<R>,
@@ -722,6 +751,7 @@ impl Server { · 626 unmodified lines
722 system_id: Option<String>,751 system_id: Option<String>,
723 send_connection_id: Option<oneshot::Sender<ConnectionId>>,752 send_connection_id: Option<oneshot::Sender<ConnectionId>>,
724 executor: Executor,753 executor: Executor,
754 connection_guard: Option<ConnectionGuard>,
725 ) -> impl Future<Output = ()> + use<> {755 ) -> impl Future<Output = ()> + use<> {
726 let this = self.clone();756 let this = self.clone();
727 let span = info_span!("handle connection", %address,757 let span = info_span!("handle connection", %address,
@@ -742,6 +772,7 @@ impl Server { · 14 unmodified lines
742 tracing::error!("server is tearing down");772 tracing::error!("server is tearing down");
743 return773 return
744 }774 }
775
745 let (connection_id, handle_io, mut incoming_rx) = this776 let (connection_id, handle_io, mut incoming_rx) = this
746 .peer777 .peer
747 .add_connection(connection, {778 .add_connection(connection, {
@@ -783,6 +814,7 @@ impl Server { · 35 unmodified lines
783 tracing::error!(?error, "failed to send initial client update");814 tracing::error!(?error, "failed to send initial client update");
784 return;815 return;
785 }816 }
817 drop(connection_guard);
786 818
787 let handle_io = handle_io.fuse();819 let handle_io = handle_io.fuse();
788 futures::pin_mut!(handle_io);820 futures::pin_mut!(handle_io);
@@ -1154,6 +1186,19 @@ pub async fn handle_websocket_request( · 365 unmodified lines
1154 }1186 }
1155 1187
1156 let socket_address = socket_address.to_string();1188 let socket_address = socket_address.to_string();
1189
1190 // Acquire connection guard before WebSocket upgrade
1191 let connection_guard = match ConnectionGuard::try_acquire() {
1192 Ok(guard) => guard,
1193 Err(()) => {
1194 return (
1195 StatusCode::SERVICE_UNAVAILABLE,
1196 "Too many concurrent connections",
1197 )
1198 .into_response();
1199 }
1200 };
1201
1157 ws.on_upgrade(move |socket| {1202 ws.on_upgrade(move |socket| {
1158 let socket = socket1203 let socket = socket
1159 .map_ok(to_tungstenite_message)1204 .map_ok(to_tungstenite_message)
@@ -1171,6 +1216,7 @@ pub async fn handle_websocket_request( · 11 unmodified lines
1171 system_id_header.map(|header| header.to_string()),1216 system_id_header.map(|header| header.to_string()),
1172 None,1217 None,
1173 Executor::Production,1218 Executor::Production,
1219 Some(connection_guard),
1174 )1220 )
1175 .await;1221 .await;
1176 }1222 }
@@ -1844,28 +1890,16 @@ async fn join_project( · 667 unmodified lines
1844 1890
1845 let db = session.db().await;1891 let db = session.db().await;
1846 let (project, replica_id) = &mut *db1892 let (project, replica_id) = &mut *db
1847 .join_project(project_id, session.connection_id, session.user_id())1893 .join_project(
1894 project_id,
1895 session.connection_id,
1896 session.user_id(),
1897 request.committer_name.clone(),
1898 request.committer_email.clone(),
1899 )
1848 .await?;1900 .await?;
1849 drop(db);1901 drop(db);
1850 tracing::info!(%project_id, "join remote project");1902 tracing::info!(%project_id, "join remote project");
1851 join_project_internal(response, session, project, replica_id)
1852}
1853
1854trait JoinProjectInternalResponse {
1855 fn send(self, result: proto::JoinProjectResponse) -> Result<()>;
1856}
1857impl JoinProjectInternalResponse for Response<proto::JoinProject> {
1858 fn send(self, result: proto::JoinProjectResponse) -> Result<()> {
1859 Response::<proto::JoinProject>::send(self, result)
1860 }
1861}
1862
1863fn join_project_internal(
1864 response: impl JoinProjectInternalResponse,
1865 session: Session,
1866 project: &mut Project,
1867 replica_id: &ReplicaId,
1868) -> Result<()> {
1869 let collaborators = project1903 let collaborators = project
1870 .collaborators1904 .collaborators
1871 .iter()1905 .iter()
@@ -1893,6 +1927,8 @@ fn join_project_internal( · 21 unmodified lines
1893 replica_id: replica_id.0 as u32,1927 replica_id: replica_id.0 as u32,
1894 user_id: guest_user_id.to_proto(),1928 user_id: guest_user_id.to_proto(),
1895 is_host: false,1929 is_host: false,
1930 committer_name: request.committer_name.clone(),
1931 committer_email: request.committer_email.clone(),
1896 }),1932 }),
1897 };1933 };
1898 1934
@@ -2521,7 +2557,6 @@ async fn get_users( · 622 unmodified lines
2521 id: user.id.to_proto(),2557 id: user.id.to_proto(),
2522 avatar_url: format!("https://github.com/{}.png?size=128", user.github_login),2558 avatar_url: format!("https://github.com/{}.png?size=128", user.github_login),
2523 github_login: user.github_login,2559 github_login: user.github_login,
2524 email: user.email_address,
2525 name: user.name,2560 name: user.name,
2526 })2561 })
2527 .collect();2562 .collect();
@@ -2555,7 +2590,6 @@ async fn fuzzy_search_users( · 27 unmodified lines
2555 avatar_url: format!("https://github.com/{}.png?size=128", user.github_login),2590 avatar_url: format!("https://github.com/{}.png?size=128", user.github_login),
2556 github_login: user.github_login,2591 github_login: user.github_login,
2557 name: user.name,2592 name: user.name,
2558 email: user.email_address,
2559 })2593 })
2560 .collect();2594 .collect();
2561 response.send(proto::UsersResponse { users })?;2595 response.send(proto::UsersResponse { users })?;
@@ -2773,8 +2807,12 @@ async fn make_update_user_plan_message( · 211 unmodified lines
2773 (None, None)2807 (None, None)
2774 };2808 };
2775 2809
2776 let account_too_young =2810 let bypass_account_age_check = feature_flags
2777 !matches!(plan, proto::Plan::ZedPro) && user.account_age() < MIN_ACCOUNT_AGE_FOR_LLM_USE;2811 .iter()
2812 .any(|flag| flag == BYPASS_ACCOUNT_AGE_CHECK_FEATURE_FLAG);
2813 let account_too_young = !matches!(plan, proto::Plan::ZedPro)
2814 && !bypass_account_age_check
2815 && user.account_age() < MIN_ACCOUNT_AGE_FOR_LLM_USE;
2778 2816
2779 Ok(proto::UpdateUserPlan {2817 Ok(proto::UpdateUserPlan {
2780 plan: plan.into(),2818 plan: plan.into(),
@@ -4075,9 +4113,6 @@ async fn accept_terms_of_service( · 1294 unmodified lines
4075 Ok(())4113 Ok(())
4076}4114}
4077 4115
4078/// The minimum account age an account must have in order to use the LLM service.
4079pub const MIN_ACCOUNT_AGE_FOR_LLM_USE: chrono::Duration = chrono::Duration::days(30);
4080
4081async fn get_llm_api_token(4116async fn get_llm_api_token(
4082 _request: proto::GetLlmToken,4117 _request: proto::GetLlmToken,
4083 response: Response<proto::GetLlmToken>,4118 response: Response<proto::GetLlmToken>,
crates/collab/src/seed.rs+1−1
@@ -127,7 +127,7 @@ pub async fn seed(config: &Config, db: &Database, force: bool) -> anyhow::Result · 126 unmodified lines
127 log::info!("Seeding {:?} from GitHub", github_user.login);127 log::info!("Seeding {:?} from GitHub", github_user.login);
128 128
129 let user = db129 let user = db
130 .get_or_create_user_by_github_account(130 .update_or_create_user_by_github_account(
131 &github_user.login,131 &github_user.login,
132 github_user.id,132 github_user.id,
133 github_user.email.as_deref(),133 github_user.email.as_deref(),
crates/collab/src/tests/channel_guest_tests.rs+1−1
@@ -180,7 +180,7 @@ async fn test_channel_requires_zed_cla(cx_a: &mut TestAppContext, cx_b: &mut Tes · 179 unmodified lines
180 server180 server
181 .app_state181 .app_state
182 .db182 .db
183 .get_or_create_user_by_github_account("user_b", 100, None, None, Utc::now(), None)183 .update_or_create_user_by_github_account("user_b", 100, None, None, Utc::now(), None)
184 .await184 .await
185 .unwrap();185 .unwrap();
186 186
crates/collab/src/tests/following_tests.rs+2−0
@@ -1610,6 +1610,8 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut · 1609 unmodified lines
1610 .root(cx_a)1610 .root(cx_a)
1611 .unwrap();1611 .unwrap();
1612 1612
1613 executor.run_until_parked();
1614
1613 workspace_a_project_b.update(cx_a2, |workspace, cx| {1615 workspace_a_project_b.update(cx_a2, |workspace, cx| {
1614 assert_eq!(workspace.project().read(cx).remote_id(), Some(project_b_id));1616 assert_eq!(workspace.project().read(cx).remote_id(), Some(project_b_id));
1615 assert!(workspace.is_being_followed(client_b.peer_id().unwrap()));1617 assert!(workspace.is_being_followed(client_b.peer_id().unwrap()));
crates/collab/src/tests/integration_tests.rs+52−54
@@ -51,7 +51,7 @@ use std::{ · 50 unmodified lines
51 time::Duration,51 time::Duration,
52};52};
53use unindent::Unindent as _;53use unindent::Unindent as _;
54use util::{path, separator, uri};54use util::{path, uri};
55use workspace::Pane;55use workspace::Pane;
56 56
57#[ctor::ctor]57#[ctor::ctor]
@@ -1676,13 +1676,13 @@ async fn test_project_reconnect( · 1618 unmodified lines
1676 .map(|p| p.to_str().unwrap())1676 .map(|p| p.to_str().unwrap())
1677 .collect::<Vec<_>>(),1677 .collect::<Vec<_>>(),
1678 vec![1678 vec![
1679 separator!("a.txt"),1679 path!("a.txt"),
1680 separator!("b.txt"),1680 path!("b.txt"),
1681 separator!("subdir2"),1681 path!("subdir2"),
1682 separator!("subdir2/f.txt"),1682 path!("subdir2/f.txt"),
1683 separator!("subdir2/g.txt"),1683 path!("subdir2/g.txt"),
1684 separator!("subdir2/h.txt"),1684 path!("subdir2/h.txt"),
1685 separator!("subdir2/i.txt")1685 path!("subdir2/i.txt")
1686 ]1686 ]
1687 );1687 );
1688 assert!(worktree_a3.read(cx).has_update_observer());1688 assert!(worktree_a3.read(cx).has_update_observer());
@@ -1709,13 +1709,13 @@ async fn test_project_reconnect( · 20 unmodified lines
1709 .map(|p| p.to_str().unwrap())1709 .map(|p| p.to_str().unwrap())
1710 .collect::<Vec<_>>(),1710 .collect::<Vec<_>>(),
1711 vec![1711 vec![
1712 separator!("a.txt"),1712 path!("a.txt"),
1713 separator!("b.txt"),1713 path!("b.txt"),
1714 separator!("subdir2"),1714 path!("subdir2"),
1715 separator!("subdir2/f.txt"),1715 path!("subdir2/f.txt"),
1716 separator!("subdir2/g.txt"),1716 path!("subdir2/g.txt"),
1717 separator!("subdir2/h.txt"),1717 path!("subdir2/h.txt"),
1718 separator!("subdir2/i.txt")1718 path!("subdir2/i.txt")
1719 ]1719 ]
1720 );1720 );
1721 assert!(project.worktree_for_id(worktree2_id, cx).is_none());1721 assert!(project.worktree_for_id(worktree2_id, cx).is_none());
@@ -1806,13 +1806,13 @@ async fn test_project_reconnect( · 84 unmodified lines
1806 .map(|p| p.to_str().unwrap())1806 .map(|p| p.to_str().unwrap())
1807 .collect::<Vec<_>>(),1807 .collect::<Vec<_>>(),
1808 vec![1808 vec![
1809 separator!("a.txt"),1809 path!("a.txt"),
1810 separator!("b.txt"),1810 path!("b.txt"),
1811 separator!("subdir2"),1811 path!("subdir2"),
1812 separator!("subdir2/f.txt"),1812 path!("subdir2/f.txt"),
1813 separator!("subdir2/g.txt"),1813 path!("subdir2/g.txt"),
1814 separator!("subdir2/h.txt"),1814 path!("subdir2/h.txt"),
1815 separator!("subdir2/j.txt")1815 path!("subdir2/j.txt")
1816 ]1816 ]
1817 );1817 );
1818 assert!(project.worktree_for_id(worktree2_id, cx).is_none());1818 assert!(project.worktree_for_id(worktree2_id, cx).is_none());
@@ -1876,7 +1876,6 @@ async fn test_active_call_events( · 57 unmodified lines
1876 github_login: "user_a".to_string(),1876 github_login: "user_a".to_string(),
1877 avatar_uri: "avatar_a".into(),1877 avatar_uri: "avatar_a".into(),
1878 name: None,1878 name: None,
1879 email: None,
1880 }),1879 }),
1881 project_id: project_a_id,1880 project_id: project_a_id,
1882 worktree_root_names: vec!["a".to_string()],1881 worktree_root_names: vec!["a".to_string()],
@@ -1896,7 +1895,6 @@ async fn test_active_call_events( · 13 unmodified lines
1896 github_login: "user_b".to_string(),1895 github_login: "user_b".to_string(),
1897 avatar_uri: "avatar_b".into(),1896 avatar_uri: "avatar_b".into(),
1898 name: None,1897 name: None,
1899 email: None,
1900 }),1898 }),
1901 project_id: project_b_id,1899 project_id: project_b_id,
1902 worktree_root_names: vec!["b".to_string()]1900 worktree_root_names: vec!["b".to_string()]
@@ -3317,13 +3315,13 @@ async fn test_fs_operations( · 1414 unmodified lines
3317 .map(|p| p.to_string_lossy())3315 .map(|p| p.to_string_lossy())
3318 .collect::<Vec<_>>(),3316 .collect::<Vec<_>>(),
3319 [3317 [
3320 separator!("DIR"),3318 path!("DIR"),
3321 separator!("DIR/SUBDIR"),3319 path!("DIR/SUBDIR"),
3322 separator!("DIR/SUBDIR/f.txt"),3320 path!("DIR/SUBDIR/f.txt"),
3323 separator!("DIR/e.txt"),3321 path!("DIR/e.txt"),
3324 separator!("a.txt"),3322 path!("a.txt"),
3325 separator!("b.txt"),3323 path!("b.txt"),
3326 separator!("d.txt")3324 path!("d.txt")
3327 ]3325 ]
3328 );3326 );
3329 });3327 });
@@ -3335,13 +3333,13 @@ async fn test_fs_operations( · 5 unmodified lines
3335 .map(|p| p.to_string_lossy())3333 .map(|p| p.to_string_lossy())
3336 .collect::<Vec<_>>(),3334 .collect::<Vec<_>>(),
3337 [3335 [
3338 separator!("DIR"),3336 path!("DIR"),
3339 separator!("DIR/SUBDIR"),3337 path!("DIR/SUBDIR"),
3340 separator!("DIR/SUBDIR/f.txt"),3338 path!("DIR/SUBDIR/f.txt"),
3341 separator!("DIR/e.txt"),3339 path!("DIR/e.txt"),
3342 separator!("a.txt"),3340 path!("a.txt"),
3343 separator!("b.txt"),3341 path!("b.txt"),
3344 separator!("d.txt")3342 path!("d.txt")
3345 ]3343 ]
3346 );3344 );
3347 });3345 });
@@ -3361,14 +3359,14 @@ async fn test_fs_operations( · 13 unmodified lines
3361 .map(|p| p.to_string_lossy())3359 .map(|p| p.to_string_lossy())
3362 .collect::<Vec<_>>(),3360 .collect::<Vec<_>>(),
3363 [3361 [
3364 separator!("DIR"),3362 path!("DIR"),
3365 separator!("DIR/SUBDIR"),3363 path!("DIR/SUBDIR"),
3366 separator!("DIR/SUBDIR/f.txt"),3364 path!("DIR/SUBDIR/f.txt"),
3367 separator!("DIR/e.txt"),3365 path!("DIR/e.txt"),
3368 separator!("a.txt"),3366 path!("a.txt"),
3369 separator!("b.txt"),3367 path!("b.txt"),
3370 separator!("d.txt"),3368 path!("d.txt"),
3371 separator!("f.txt")3369 path!("f.txt")
3372 ]3370 ]
3373 );3371 );
3374 });3372 });
@@ -3380,14 +3378,14 @@ async fn test_fs_operations( · 5 unmodified lines
3380 .map(|p| p.to_string_lossy())3378 .map(|p| p.to_string_lossy())
3381 .collect::<Vec<_>>(),3379 .collect::<Vec<_>>(),
3382 [3380 [
3383 separator!("DIR"),3381 path!("DIR"),
3384 separator!("DIR/SUBDIR"),3382 path!("DIR/SUBDIR"),
3385 separator!("DIR/SUBDIR/f.txt"),3383 path!("DIR/SUBDIR/f.txt"),
3386 separator!("DIR/e.txt"),3384 path!("DIR/e.txt"),
3387 separator!("a.txt"),3385 path!("a.txt"),
3388 separator!("b.txt"),3386 path!("b.txt"),
3389 separator!("d.txt"),3387 path!("d.txt"),
3390 separator!("f.txt")3388 path!("f.txt")
3391 ]3389 ]
3392 );3390 );
3393 });3391 });
crates/collab/src/tests/remote_editing_collaboration_tests.rs+3−3
@@ -30,7 +30,7 @@ use rpc::proto; · 29 unmodified lines
30use serde_json::json;30use serde_json::json;
31use settings::SettingsStore;31use settings::SettingsStore;
32use std::{path::Path, sync::Arc};32use std::{path::Path, sync::Arc};
33use util::{path, separator};33use util::path;
34 34
35#[gpui::test(iterations = 10)]35#[gpui::test(iterations = 10)]
36async fn test_sharing_an_ssh_remote_project(36async fn test_sharing_an_ssh_remote_project(
@@ -198,7 +198,7 @@ async fn test_sharing_an_ssh_remote_project( · 161 unmodified lines
198 .path()198 .path()
199 .to_string_lossy()199 .to_string_lossy()
200 .to_string(),200 .to_string(),
201 separator!("src/renamed.rs").to_string()201 path!("src/renamed.rs").to_string()
202 );202 );
203 });203 });
204}204}
@@ -671,7 +671,7 @@ async fn test_remote_server_debugger( · 466 unmodified lines
671 });671 });
672 672
673 session.update(cx_a, |session, _| {673 session.update(cx_a, |session, _| {
674 assert_eq!(session.binary().command, "ssh");674 assert_eq!(session.binary().unwrap().command.as_deref(), Some("ssh"));
675 });675 });
676 676
677 let shutdown_session = workspace.update(cx_a, |workspace, cx| {677 let shutdown_session = workspace.update(cx_a, |workspace, cx| {
crates/collab/src/tests/test_server.rs+1−0
@@ -258,6 +258,7 @@ impl TestServer { · 257 unmodified lines
258 None,258 None,
259 Some(connection_id_tx),259 Some(connection_id_tx),
260 Executor::Deterministic(cx.background_executor().clone()),260 Executor::Deterministic(cx.background_executor().clone()),
261 None,
261 ))262 ))
262 .detach();263 .detach();
263 let connection_id = connection_id_rx.await.map_err(|e| {264 let connection_id = connection_id_rx.await.map_err(|e| {
crates/collab/src/user_backfiller.rs+1−1
@@ -82,7 +82,7 @@ impl UserBackfiller { · 81 unmodified lines
82 {82 {
83 Ok(github_user) => {83 Ok(github_user) => {
84 self.db84 self.db
85 .get_or_create_user_by_github_account(85 .update_or_create_user_by_github_account(
86 &user.github_login,86 &user.github_login,
87 github_user.id,87 github_user.id,
88 user.email_address.as_deref(),88 user.email_address.as_deref(),
crates/collab_ui/src/chat_panel.rs+1−4
@@ -90,7 +90,7 @@ impl ChatPanel { · 89 unmodified lines
90 languages.clone(),90 languages.clone(),
91 user_store.clone(),91 user_store.clone(),
92 None,92 None,
93 cx.new(|cx| Editor::auto_height(4, window, cx)),93 cx.new(|cx| Editor::auto_height(1, 4, window, cx)),
94 window,94 window,
95 cx,95 cx,
96 )96 )
@@ -1218,7 +1218,6 @@ mod tests { · 1121 unmodified lines
1218 avatar_uri: "avatar_fgh".into(),1218 avatar_uri: "avatar_fgh".into(),
1219 id: 103,1219 id: 103,
1220 name: None,1220 name: None,
1221 email: None,
1222 }),1221 }),
1223 nonce: 5,1222 nonce: 5,
1224 mentions: vec![(ranges[0].clone(), 101), (ranges[1].clone(), 102)],1223 mentions: vec![(ranges[0].clone(), 101), (ranges[1].clone(), 102)],
@@ -1274,7 +1273,6 @@ mod tests { · 49 unmodified lines
1274 avatar_uri: "avatar_fgh".into(),1273 avatar_uri: "avatar_fgh".into(),
1275 id: 103,1274 id: 103,
1276 name: None,1275 name: None,
1277 email: None,
1278 }),1276 }),
1279 nonce: 5,1277 nonce: 5,
1280 mentions: Vec::new(),1278 mentions: Vec::new(),
@@ -1323,7 +1321,6 @@ mod tests { · 42 unmodified lines
1323 avatar_uri: "avatar_fgh".into(),1321 avatar_uri: "avatar_fgh".into(),
1324 id: 103,1322 id: 103,
1325 name: None,1323 name: None,
1326 email: None,
1327 }),1324 }),
1328 nonce: 5,1325 nonce: 5,
1329 mentions: Vec::new(),1326 mentions: Vec::new(),
crates/context_server/src/protocol.rs+12−10
@@ -8,7 +8,7 @@ · 7 unmodified lines
8use anyhow::Result;8use anyhow::Result;
9 9
10use crate::client::Client;10use crate::client::Client;
11use crate::types::{self, Request};11use crate::types::{self, Notification, Request};
12 12
13pub struct ModelContextProtocol {13pub struct ModelContextProtocol {
14 inner: Client,14 inner: Client,
@@ -20,9 +20,10 @@ impl ModelContextProtocol { · 5 unmodified lines
20 }20 }
21 21
22 fn supported_protocols() -> Vec<types::ProtocolVersion> {22 fn supported_protocols() -> Vec<types::ProtocolVersion> {
23 vec![types::ProtocolVersion(23 vec![
24 types::LATEST_PROTOCOL_VERSION.to_string(),24 types::ProtocolVersion(types::LATEST_PROTOCOL_VERSION.to_string()),
25 )]25 types::ProtocolVersion(types::VERSION_2024_11_05.to_string()),
26 ]
26 }27 }
27 28
28 pub async fn initialize(29 pub async fn initialize(
@@ -42,7 +43,7 @@ impl ModelContextProtocol { · 13 unmodified lines
42 43
43 let response: types::InitializeResponse = self44 let response: types::InitializeResponse = self
44 .inner45 .inner
45 .request(types::request::Initialize::METHOD, params)46 .request(types::requests::Initialize::METHOD, params)
46 .await?;47 .await?;
47 48
48 anyhow::ensure!(49 anyhow::ensure!(
@@ -53,16 +54,13 @@ impl ModelContextProtocol { · 4 unmodified lines
53 54
54 log::trace!("mcp server info {:?}", response.server_info);55 log::trace!("mcp server info {:?}", response.server_info);
55 56
56 self.inner.notify(
57 types::NotificationType::Initialized.as_str(),
58 serde_json::json!({}),
59 )?;
60
61 let initialized_protocol = InitializedContextServerProtocol {57 let initialized_protocol = InitializedContextServerProtocol {
62 inner: self.inner,58 inner: self.inner,
63 initialize: response,59 initialize: response,
64 };60 };
65 61
62 initialized_protocol.notify::<types::notifications::Initialized>(())?;
63
66 Ok(initialized_protocol)64 Ok(initialized_protocol)
67 }65 }
68}66}
@@ -96,4 +94,8 @@ impl InitializedContextServerProtocol { · 27 unmodified lines
96 pub async fn request<T: Request>(&self, params: T::Params) -> Result<T::Response> {94 pub async fn request<T: Request>(&self, params: T::Params) -> Result<T::Response> {
97 self.inner.request(T::METHOD, params).await95 self.inner.request(T::METHOD, params).await
98 }96 }
97
98 pub fn notify<T: Notification>(&self, params: T::Params) -> Result<()> {
99 self.inner.notify(T::METHOD, params)
100 }
99}101}
crates/context_server/src/test.rs+1−1
@@ -14,7 +14,7 @@ pub fn create_fake_transport( · 13 unmodified lines
14 executor: BackgroundExecutor,14 executor: BackgroundExecutor,
15) -> FakeTransport {15) -> FakeTransport {
16 let name = name.into();16 let name = name.into();
17 FakeTransport::new(executor).on_request::<crate::types::request::Initialize>(move |_params| {17 FakeTransport::new(executor).on_request::<crate::types::requests::Initialize>(move |_params| {
18 create_initialize_response(name.clone())18 create_initialize_response(name.clone())
19 })19 })
20}20}
crates/context_server/src/types.rs+91−32
@@ -3,9 +3,10 @@ use serde::de::DeserializeOwned; · 2 unmodified lines
3use serde::{Deserialize, Serialize};3use serde::{Deserialize, Serialize};
4use url::Url;4use url::Url;
5 5
6pub const LATEST_PROTOCOL_VERSION: &str = "2024-11-05";6pub const LATEST_PROTOCOL_VERSION: &str = "2025-03-26";
7pub const VERSION_2024_11_05: &str = "2024-11-05";
7 8
8pub mod request {9pub mod requests {
9 use super::*;10 use super::*;
10 11
11 macro_rules! request {12 macro_rules! request {
@@ -82,6 +83,57 @@ pub trait Request { · 70 unmodified lines
82 const METHOD: &'static str;83 const METHOD: &'static str;
83}84}
84 85
86pub mod notifications {
87 use super::*;
88
89 macro_rules! notification {
90 ($method:expr, $name:ident, $params:ty) => {
91 pub struct $name;
92
93 impl Notification for $name {
94 type Params = $params;
95 const METHOD: &'static str = $method;
96 }
97 };
98 }
99
100 notification!("notifications/initialized", Initialized, ());
101 notification!("notifications/progress", Progress, ProgressParams);
102 notification!("notifications/message", Message, MessageParams);
103 notification!(
104 "notifications/resources/updated",
105 ResourcesUpdated,
106 ResourcesUpdatedParams
107 );
108 notification!(
109 "notifications/resources/list_changed",
110 ResourcesListChanged,
111 ()
112 );
113 notification!("notifications/tools/list_changed", ToolsListChanged, ());
114 notification!("notifications/prompts/list_changed", PromptsListChanged, ());
115 notification!("notifications/roots/list_changed", RootsListChanged, ());
116}
117
118pub trait Notification {
119 type Params: DeserializeOwned + Serialize + Send + Sync + 'static;
120 const METHOD: &'static str;
121}
122
123#[derive(Debug, Serialize, Deserialize)]
124#[serde(rename_all = "camelCase")]
125pub struct MessageParams {
126 pub level: LoggingLevel,
127 pub logger: Option<String>,
128 pub data: serde_json::Value,
129}
130
131#[derive(Debug, Serialize, Deserialize)]
132#[serde(rename_all = "camelCase")]
133pub struct ResourcesUpdatedParams {
134 pub uri: String,
135}
136
85#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]137#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
86#[serde(transparent)]138#[serde(transparent)]
87pub struct ProtocolVersion(pub String);139pub struct ProtocolVersion(pub String);
@@ -291,13 +343,20 @@ pub enum MessageContent { · 203 unmodified lines
291 #[serde(skip_serializing_if = "Option::is_none")]343 #[serde(skip_serializing_if = "Option::is_none")]
292 annotations: Option<MessageAnnotations>,344 annotations: Option<MessageAnnotations>,
293 },345 },
294 #[serde(rename = "image")]346 #[serde(rename = "image", rename_all = "camelCase")]
295 Image {347 Image {
296 data: String,348 data: String,
297 mime_type: String,349 mime_type: String,
298 #[serde(skip_serializing_if = "Option::is_none")]350 #[serde(skip_serializing_if = "Option::is_none")]
299 annotations: Option<MessageAnnotations>,351 annotations: Option<MessageAnnotations>,
300 },352 },
353 #[serde(rename = "audio", rename_all = "camelCase")]
354 Audio {
355 data: String,
356 mime_type: String,
357 #[serde(skip_serializing_if = "Option::is_none")]
358 annotations: Option<MessageAnnotations>,
359 },
301 #[serde(rename = "resource")]360 #[serde(rename = "resource")]
302 Resource {361 Resource {
303 resource: ResourceContents,362 resource: ResourceContents,
@@ -394,6 +453,8 @@ pub struct ServerCapabilities { · 90 unmodified lines
394 #[serde(skip_serializing_if = "Option::is_none")]453 #[serde(skip_serializing_if = "Option::is_none")]
395 pub logging: Option<serde_json::Value>,454 pub logging: Option<serde_json::Value>,
396 #[serde(skip_serializing_if = "Option::is_none")]455 #[serde(skip_serializing_if = "Option::is_none")]
456 pub completions: Option<serde_json::Value>,
457 #[serde(skip_serializing_if = "Option::is_none")]
397 pub prompts: Option<PromptsCapabilities>,458 pub prompts: Option<PromptsCapabilities>,
398 #[serde(skip_serializing_if = "Option::is_none")]459 #[serde(skip_serializing_if = "Option::is_none")]
399 pub resources: Option<ResourcesCapabilities>,460 pub resources: Option<ResourcesCapabilities>,
@@ -438,6 +499,28 @@ pub struct Tool { · 38 unmodified lines
438 #[serde(skip_serializing_if = "Option::is_none")]499 #[serde(skip_serializing_if = "Option::is_none")]
439 pub description: Option<String>,500 pub description: Option<String>,
440 pub input_schema: serde_json::Value,501 pub input_schema: serde_json::Value,
502 #[serde(skip_serializing_if = "Option::is_none")]
503 pub annotations: Option<ToolAnnotations>,
504}
505
506#[derive(Debug, Serialize, Deserialize)]
507#[serde(rename_all = "camelCase")]
508pub struct ToolAnnotations {
509 /// A human-readable title for the tool.
510 #[serde(skip_serializing_if = "Option::is_none")]
511 pub title: Option<String>,
512 /// If true, the tool does not modify its environment.
513 #[serde(skip_serializing_if = "Option::is_none")]
514 pub read_only_hint: Option<bool>,
515 /// If true, the tool may perform destructive updates to its environment.
516 #[serde(skip_serializing_if = "Option::is_none")]
517 pub destructive_hint: Option<bool>,
518 /// If true, calling the tool repeatedly with the same arguments will have no additional effect on its environment.
519 #[serde(skip_serializing_if = "Option::is_none")]
520 pub idempotent_hint: Option<bool>,
521 /// If true, this tool may interact with an "open world" of external entities.
522 #[serde(skip_serializing_if = "Option::is_none")]
523 pub open_world_hint: Option<bool>,
441}524}
442 525
443#[derive(Debug, Serialize, Deserialize)]526#[derive(Debug, Serialize, Deserialize)]
@@ -528,34 +611,6 @@ pub struct ModelHint { · 84 unmodified lines
528 pub name: Option<String>,611 pub name: Option<String>,
529}612}
530 613
531#[derive(Debug, Serialize, Deserialize)]
532#[serde(rename_all = "camelCase")]
533pub enum NotificationType {
534 Initialized,
535 Progress,
536 Message,
537 ResourcesUpdated,
538 ResourcesListChanged,
539 ToolsListChanged,
540 PromptsListChanged,
541 RootsListChanged,
542}
543
544impl NotificationType {
545 pub fn as_str(&self) -> &'static str {
546 match self {
547 NotificationType::Initialized => "notifications/initialized",
548 NotificationType::Progress => "notifications/progress",
549 NotificationType::Message => "notifications/message",
550 NotificationType::ResourcesUpdated => "notifications/resources/updated",
551 NotificationType::ResourcesListChanged => "notifications/resources/list_changed",
552 NotificationType::ToolsListChanged => "notifications/tools/list_changed",
553 NotificationType::PromptsListChanged => "notifications/prompts/list_changed",
554 NotificationType::RootsListChanged => "notifications/roots/list_changed",
555 }
556 }
557}
558
559#[derive(Debug, Serialize)]614#[derive(Debug, Serialize)]
560#[serde(untagged)]615#[serde(untagged)]
561pub enum ClientNotification {616pub enum ClientNotification {
@@ -576,12 +631,14 @@ pub enum ProgressToken { · 14 unmodified lines
576 Number(f64),631 Number(f64),
577}632}
578 633
579#[derive(Debug, Serialize)]634#[derive(Debug, Serialize, Deserialize)]
580#[serde(rename_all = "camelCase")]635#[serde(rename_all = "camelCase")]
581pub struct ProgressParams {636pub struct ProgressParams {
582 pub progress_token: ProgressToken,637 pub progress_token: ProgressToken,
583 pub progress: f64,638 pub progress: f64,
584 #[serde(skip_serializing_if = "Option::is_none")]639 #[serde(skip_serializing_if = "Option::is_none")]
640 pub message: Option<String>,
641 #[serde(skip_serializing_if = "Option::is_none")]
585 pub total: Option<f64>,642 pub total: Option<f64>,
586 #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]643 #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
587 pub meta: Option<HashMap<String, serde_json::Value>>,644 pub meta: Option<HashMap<String, serde_json::Value>>,
@@ -625,6 +682,8 @@ pub enum ToolResponseContent { · 37 unmodified lines
625 Text { text: String },682 Text { text: String },
626 #[serde(rename = "image", rename_all = "camelCase")]683 #[serde(rename = "image", rename_all = "camelCase")]
627 Image { data: String, mime_type: String },684 Image { data: String, mime_type: String },
685 #[serde(rename = "audio", rename_all = "camelCase")]
686 Audio { data: String, mime_type: String },
628 #[serde(rename = "resource")]687 #[serde(rename = "resource")]
629 Resource { resource: ResourceContents },688 Resource { resource: ResourceContents },
630}689}
crates/copilot/src/copilot.rs+43−37
@@ -26,6 +26,7 @@ use parking_lot::Mutex; · 25 unmodified lines
26use request::StatusNotification;26use request::StatusNotification;
27use settings::SettingsStore;27use settings::SettingsStore;
28use sign_in::{reinstall_and_sign_in_within_workspace, sign_out_within_workspace};28use sign_in::{reinstall_and_sign_in_within_workspace, sign_out_within_workspace};
29use std::collections::hash_map::Entry;
29use std::{30use std::{
30 any::TypeId,31 any::TypeId,
31 env,32 env,
@@ -731,42 +732,43 @@ impl Copilot { · 699 unmodified lines
731 return;732 return;
732 }733 }
733 734
734 registered_buffers735 let entry = registered_buffers.entry(buffer.entity_id());
735 .entry(buffer.entity_id())736 if let Entry::Vacant(e) = entry {
736 .or_insert_with(|| {737 let Ok(uri) = uri_for_buffer(buffer, cx) else {
737 let uri: lsp::Url = uri_for_buffer(buffer, cx);738 return;
738 let language_id = id_for_language(buffer.read(cx).language());739 };
739 let snapshot = buffer.read(cx).snapshot();740 let language_id = id_for_language(buffer.read(cx).language());
740 server741 let snapshot = buffer.read(cx).snapshot();
741 .notify::<lsp::notification::DidOpenTextDocument>(742 server
742 &lsp::DidOpenTextDocumentParams {743 .notify::<lsp::notification::DidOpenTextDocument>(
743 text_document: lsp::TextDocumentItem {744 &lsp::DidOpenTextDocumentParams {
744 uri: uri.clone(),745 text_document: lsp::TextDocumentItem {
745 language_id: language_id.clone(),746 uri: uri.clone(),
746 version: 0,747 language_id: language_id.clone(),
747 text: snapshot.text(),748 version: 0,
748 },749 text: snapshot.text(),
749 },750 },
750 )751 },
751 .ok();752 )
753 .ok();
752 754
753 RegisteredBuffer {755 e.insert(RegisteredBuffer {
754 uri,756 uri,
755 language_id,757 language_id,
756 snapshot,758 snapshot,
757 snapshot_version: 0,759 snapshot_version: 0,
758 pending_buffer_change: Task::ready(Some(())),760 pending_buffer_change: Task::ready(Some(())),
759 _subscriptions: [761 _subscriptions: [
760 cx.subscribe(buffer, |this, buffer, event, cx| {762 cx.subscribe(buffer, |this, buffer, event, cx| {
761 this.handle_buffer_event(buffer, event, cx).log_err();763 this.handle_buffer_event(buffer, event, cx).log_err();
762 }),764 }),
763 cx.observe_release(buffer, move |this, _buffer, _cx| {765 cx.observe_release(buffer, move |this, _buffer, _cx| {
764 this.buffers.remove(&weak_buffer);766 this.buffers.remove(&weak_buffer);
765 this.unregister_buffer(&weak_buffer);767 this.unregister_buffer(&weak_buffer);
766 }),768 }),
767 ],769 ],
768 }
769 });770 });
771 }
770 }772 }
771 }773 }
772 774
@@ -798,7 +800,9 @@ impl Copilot { · 25 unmodified lines
798 language::BufferEvent::FileHandleChanged800 language::BufferEvent::FileHandleChanged
799 | language::BufferEvent::LanguageChanged => {801 | language::BufferEvent::LanguageChanged => {
800 let new_language_id = id_for_language(buffer.read(cx).language());802 let new_language_id = id_for_language(buffer.read(cx).language());
801 let new_uri = uri_for_buffer(&buffer, cx);803 let Ok(new_uri) = uri_for_buffer(&buffer, cx) else {
804 return Ok(());
805 };
802 if new_uri != registered_buffer.uri806 if new_uri != registered_buffer.uri
803 || new_language_id != registered_buffer.language_id807 || new_language_id != registered_buffer.language_id
804 {808 {
@@ -1068,11 +1072,13 @@ fn id_for_language(language: Option<&Arc<Language>>) -> String { · 263 unmodified lines
1068 .unwrap_or_else(|| "plaintext".to_string())1072 .unwrap_or_else(|| "plaintext".to_string())
1069}1073}
1070 1074
1071fn uri_for_buffer(buffer: &Entity<Buffer>, cx: &App) -> lsp::Url {1075fn uri_for_buffer(buffer: &Entity<Buffer>, cx: &App) -> Result<lsp::Url, ()> {
1072 if let Some(file) = buffer.read(cx).file().and_then(|file| file.as_local()) {1076 if let Some(file) = buffer.read(cx).file().and_then(|file| file.as_local()) {
1073 lsp::Url::from_file_path(file.abs_path(cx)).unwrap()1077 lsp::Url::from_file_path(file.abs_path(cx))
1074 } else {1078 } else {
1075 format!("buffer://{}", buffer.entity_id()).parse().unwrap()1079 format!("buffer://{}", buffer.entity_id())
1080 .parse()
1081 .map_err(|_| ())
1076 }1082 }
1077}1083}
1078 1084
crates/dap/Cargo.toml+3−0
@@ -51,6 +51,9 @@ telemetry.workspace = true · 50 unmodified lines
51util.workspace = true51util.workspace = true
52workspace-hack.workspace = true52workspace-hack.workspace = true
53 53
54[target.'cfg(not(windows))'.dependencies]
55libc.workspace = true
56
54[dev-dependencies]57[dev-dependencies]
55async-pipe.workspace = true58async-pipe.workspace = true
56gpui = { workspace = true, features = ["test-support"] }59gpui = { workspace = true, features = ["test-support"] }
crates/dap/src/adapters.rs+8−4
@@ -93,7 +93,7 @@ impl<'a> From<&'a str> for DebugAdapterName { · 92 unmodified lines
93 }93 }
94}94}
95 95
96#[derive(Debug, Clone, PartialEq)]96#[derive(Debug, Clone, PartialEq, Serialize)]
97pub struct TcpArguments {97pub struct TcpArguments {
98 pub host: Ipv4Addr,98 pub host: Ipv4Addr,
99 pub port: u16,99 pub port: u16,
@@ -179,9 +179,9 @@ impl DebugTaskDefinition { · 79 unmodified lines
179}179}
180 180
181/// Created from a [DebugTaskDefinition], this struct describes how to spawn the debugger to create a previously-configured debug session.181/// Created from a [DebugTaskDefinition], this struct describes how to spawn the debugger to create a previously-configured debug session.
182#[derive(Debug, Clone, PartialEq)]182#[derive(Debug, Clone, PartialEq, Serialize)]
183pub struct DebugAdapterBinary {183pub struct DebugAdapterBinary {
184 pub command: String,184 pub command: Option<String>,
185 pub arguments: Vec<String>,185 pub arguments: Vec<String>,
186 pub envs: HashMap<String, String>,186 pub envs: HashMap<String, String>,
187 pub cwd: Option<PathBuf>,187 pub cwd: Option<PathBuf>,
@@ -369,6 +369,10 @@ pub trait DebugAdapter: 'static + Send + Sync { · 181 unmodified lines
369 }369 }
370 370
371 async fn dap_schema(&self) -> serde_json::Value;371 async fn dap_schema(&self) -> serde_json::Value;
372
373 fn label_for_child_session(&self, _args: &StartDebuggingRequestArguments) -> Option<String> {
374 None
375 }
372}376}
373 377
374#[cfg(any(test, feature = "test-support"))]378#[cfg(any(test, feature = "test-support"))]
@@ -433,7 +437,7 @@ impl DebugAdapter for FakeAdapter { · 58 unmodified lines
433 _: &mut AsyncApp,437 _: &mut AsyncApp,
434 ) -> Result<DebugAdapterBinary> {438 ) -> Result<DebugAdapterBinary> {
435 Ok(DebugAdapterBinary {439 Ok(DebugAdapterBinary {
436 command: "command".into(),440 command: Some("command".into()),
437 arguments: vec![],441 arguments: vec![],
438 connection: None,442 connection: None,
439 envs: HashMap::default(),443 envs: HashMap::default(),
crates/dap/src/client.rs+4−4
@@ -218,7 +218,7 @@ impl DebugAdapterClient { · 217 unmodified lines
218 218
219 pub fn add_log_handler<F>(&self, f: F, kind: LogKind)219 pub fn add_log_handler<F>(&self, f: F, kind: LogKind)
220 where220 where
221 F: 'static + Send + FnMut(IoKind, &str),221 F: 'static + Send + FnMut(IoKind, Option<&str>, &str),
222 {222 {
223 self.transport_delegate.add_log_handler(f, kind);223 self.transport_delegate.add_log_handler(f, kind);
224 }224 }
@@ -297,7 +297,7 @@ mod tests { · 72 unmodified lines
297 let client = DebugAdapterClient::start(297 let client = DebugAdapterClient::start(
298 crate::client::SessionId(1),298 crate::client::SessionId(1),
299 DebugAdapterBinary {299 DebugAdapterBinary {
300 command: "command".into(),300 command: Some("command".into()),
301 arguments: Default::default(),301 arguments: Default::default(),
302 envs: Default::default(),302 envs: Default::default(),
303 connection: None,303 connection: None,
@@ -367,7 +367,7 @@ mod tests { · 63 unmodified lines
367 let client = DebugAdapterClient::start(367 let client = DebugAdapterClient::start(
368 crate::client::SessionId(1),368 crate::client::SessionId(1),
369 DebugAdapterBinary {369 DebugAdapterBinary {
370 command: "command".into(),370 command: Some("command".into()),
371 arguments: Default::default(),371 arguments: Default::default(),
372 envs: Default::default(),372 envs: Default::default(),
373 connection: None,373 connection: None,
@@ -420,7 +420,7 @@ mod tests { · 46 unmodified lines
420 let client = DebugAdapterClient::start(420 let client = DebugAdapterClient::start(
421 crate::client::SessionId(1),421 crate::client::SessionId(1),
422 DebugAdapterBinary {422 DebugAdapterBinary {
423 command: "command".into(),423 command: Some("command".into()),
424 arguments: Default::default(),424 arguments: Default::default(),
425 envs: Default::default(),425 envs: Default::default(),
426 connection: None,426 connection: None,
crates/dap/src/registry.rs+2−2
@@ -19,11 +19,11 @@ use std::{collections::BTreeMap, sync::Arc}; · 18 unmodified lines
19pub trait DapLocator: Send + Sync {19pub trait DapLocator: Send + Sync {
20 fn name(&self) -> SharedString;20 fn name(&self) -> SharedString;
21 /// Determines whether this locator can generate debug target for given task.21 /// Determines whether this locator can generate debug target for given task.
22 fn create_scenario(22 async fn create_scenario(
23 &self,23 &self,
24 build_config: &TaskTemplate,24 build_config: &TaskTemplate,
25 resolved_label: &str,25 resolved_label: &str,
26 adapter: DebugAdapterName,26 adapter: &DebugAdapterName,
27 ) -> Option<DebugScenario>;27 ) -> Option<DebugScenario>;
28 28
29 async fn run(&self, build_config: SpawnInTerminal) -> Result<DebugRequest>;29 async fn run(&self, build_config: SpawnInTerminal) -> Result<DebugRequest>;
crates/dap/src/transport.rs+157−64
@@ -1,4 +1,4 @@
1use anyhow::{Context as _, Result, bail};1use anyhow::{Context as _, Result, anyhow, bail};
2use dap_types::{2use dap_types::{
3 ErrorResponse,3 ErrorResponse,
4 messages::{Message, Response},4 messages::{Message, Response},
@@ -12,7 +12,6 @@ use smol::{ · 7 unmodified lines
12 io::{AsyncBufReadExt as _, AsyncWriteExt, BufReader},12 io::{AsyncBufReadExt as _, AsyncWriteExt, BufReader},
13 lock::Mutex,13 lock::Mutex,
14 net::{TcpListener, TcpStream},14 net::{TcpListener, TcpStream},
15 process::Child,
16};15};
17use std::{16use std::{
18 collections::HashMap,17 collections::HashMap,
@@ -22,11 +21,13 @@ use std::{ · 3 unmodified lines
22 time::Duration,21 time::Duration,
23};22};
24use task::TcpArgumentsTemplate;23use task::TcpArgumentsTemplate;
25use util::{ConnectionResult, ResultExt as _};24use util::ConnectionResult;
26 25
27use crate::{adapters::DebugAdapterBinary, debugger_settings::DebuggerSettings};26use crate::{adapters::DebugAdapterBinary, debugger_settings::DebuggerSettings};
28 27
29pub type IoHandler = Box<dyn Send + FnMut(IoKind, &str)>;28pub(crate) type IoMessage = str;
29pub(crate) type Command = str;
30pub type IoHandler = Box<dyn Send + FnMut(IoKind, Option<&Command>, &IoMessage)>;
30 31
31#[derive(PartialEq, Eq, Clone, Copy)]32#[derive(PartialEq, Eq, Clone, Copy)]
32pub enum LogKind {33pub enum LogKind {
@@ -86,10 +87,12 @@ impl Transport { · 53 unmodified lines
86 TcpTransport::start(binary, cx)87 TcpTransport::start(binary, cx)
87 .await88 .await
88 .map(|(transports, tcp)| (transports, Self::Tcp(tcp)))89 .map(|(transports, tcp)| (transports, Self::Tcp(tcp)))
90 .context("Tried to connect to a debug adapter via TCP transport layer")
89 } else {91 } else {
90 StdioTransport::start(binary, cx)92 StdioTransport::start(binary, cx)
91 .await93 .await
92 .map(|(transports, stdio)| (transports, Self::Stdio(stdio)))94 .map(|(transports, stdio)| (transports, Self::Stdio(stdio)))
95 .context("Tried to connect to a debug adapter via stdin/stdout transport layer")
93 }96 }
94 }97 }
95 98
@@ -102,7 +105,7 @@ impl Transport { · 6 unmodified lines
102 }105 }
103 }106 }
104 107
105 async fn kill(&self) -> Result<()> {108 async fn kill(&self) {
106 match self {109 match self {
107 Transport::Stdio(stdio_transport) => stdio_transport.kill().await,110 Transport::Stdio(stdio_transport) => stdio_transport.kill().await,
108 Transport::Tcp(tcp_transport) => tcp_transport.kill().await,111 Transport::Tcp(tcp_transport) => tcp_transport.kill().await,
@@ -191,7 +194,7 @@ impl TransportDelegate { · 82 unmodified lines
191 match Self::handle_output(194 match Self::handle_output(
192 params.output,195 params.output,
193 client_tx,196 client_tx,
194 pending_requests,197 pending_requests.clone(),
195 output_log_handler,198 output_log_handler,
196 )199 )
197 .await200 .await
@@ -199,6 +202,12 @@ impl TransportDelegate { · 1 unmodified line
199 Ok(()) => {}202 Ok(()) => {}
200 Err(e) => log::error!("Error handling debugger output: {e}"),203 Err(e) => log::error!("Error handling debugger output: {e}"),
201 }204 }
205 let mut pending_requests = pending_requests.lock().await;
206 pending_requests.drain().for_each(|(_, request)| {
207 request
208 .send(Err(anyhow!("debugger shutdown unexpectedly")))
209 .ok();
210 });
202 }));211 }));
203 212
204 if let Some(stderr) = params.stderr.take() {213 if let Some(stderr) = params.stderr.take() {
@@ -289,7 +298,7 @@ impl TransportDelegate { · 84 unmodified lines
289 if let Some(log_handlers) = log_handlers.as_ref() {298 if let Some(log_handlers) = log_handlers.as_ref() {
290 for (kind, handler) in log_handlers.lock().iter_mut() {299 for (kind, handler) in log_handlers.lock().iter_mut() {
291 if matches!(kind, LogKind::Adapter) {300 if matches!(kind, LogKind::Adapter) {
292 handler(IoKind::StdOut, line.as_str());301 handler(IoKind::StdOut, None, line.as_str());
293 }302 }
294 }303 }
295 }304 }
@@ -323,6 +332,12 @@ impl TransportDelegate { · 27 unmodified lines
323 }332 }
324 }333 }
325 334
335 let command = match &message {
336 Message::Request(request) => Some(request.command.as_str()),
337 Message::Response(response) => Some(response.command.as_str()),
338 _ => None,
339 };
340
326 let message = match serde_json::to_string(&message) {341 let message = match serde_json::to_string(&message) {
327 Ok(message) => message,342 Ok(message) => message,
328 Err(e) => break Err(e.into()),343 Err(e) => break Err(e.into()),
@@ -331,7 +346,7 @@ impl TransportDelegate { · 2 unmodified lines
331 if let Some(log_handlers) = log_handlers.as_ref() {346 if let Some(log_handlers) = log_handlers.as_ref() {
332 for (kind, log_handler) in log_handlers.lock().iter_mut() {347 for (kind, log_handler) in log_handlers.lock().iter_mut() {
333 if matches!(kind, LogKind::Rpc) {348 if matches!(kind, LogKind::Rpc) {
334 log_handler(IoKind::StdIn, &message);349 log_handler(IoKind::StdIn, command, &message);
335 }350 }
336 }351 }
337 }352 }
@@ -416,7 +431,7 @@ impl TransportDelegate { · 78 unmodified lines
416 Ok(_) => {431 Ok(_) => {
417 for (kind, log_handler) in log_handlers.lock().iter_mut() {432 for (kind, log_handler) in log_handlers.lock().iter_mut() {
418 if matches!(kind, LogKind::Adapter) {433 if matches!(kind, LogKind::Adapter) {
419 log_handler(IoKind::StdErr, buffer.as_str());434 log_handler(IoKind::StdErr, None, buffer.as_str());
420 }435 }
421 }436 }
422 437
@@ -505,17 +520,24 @@ impl TransportDelegate { · 82 unmodified lines
505 Err(e) => return ConnectionResult::Result(Err(e)),520 Err(e) => return ConnectionResult::Result(Err(e)),
506 };521 };
507 522
523 let message =
524 serde_json::from_str::<Message>(message_str).context("deserializing server message");
525
508 if let Some(log_handlers) = log_handlers {526 if let Some(log_handlers) = log_handlers {
527 let command = match &message {
528 Ok(Message::Request(request)) => Some(request.command.as_str()),
529 Ok(Message::Response(response)) => Some(response.command.as_str()),
530 _ => None,
531 };
532
509 for (kind, log_handler) in log_handlers.lock().iter_mut() {533 for (kind, log_handler) in log_handlers.lock().iter_mut() {
510 if matches!(kind, LogKind::Rpc) {534 if matches!(kind, LogKind::Rpc) {
511 log_handler(IoKind::StdOut, message_str);535 log_handler(IoKind::StdOut, command, message_str);
512 }536 }
513 }537 }
514 }538 }
515 539
516 ConnectionResult::Result(540 ConnectionResult::Result(message)
517 serde_json::from_str::<Message>(message_str).context("deserializing server message"),
518 )
519 }541 }
520 542
521 pub async fn shutdown(&self) -> Result<()> {543 pub async fn shutdown(&self) -> Result<()> {
@@ -531,7 +553,7 @@ impl TransportDelegate { · 9 unmodified lines
531 current_requests.clear();553 current_requests.clear();
532 pending_requests.clear();554 pending_requests.clear();
533 555
534 let _ = self.transport.kill().await.log_err();556 self.transport.kill().await;
535 557
536 drop(current_requests);558 drop(current_requests);
537 drop(pending_requests);559 drop(pending_requests);
@@ -551,7 +573,7 @@ impl TransportDelegate { · 13 unmodified lines
551 573
552 pub fn add_log_handler<F>(&self, f: F, kind: LogKind)574 pub fn add_log_handler<F>(&self, f: F, kind: LogKind)
553 where575 where
554 F: 'static + Send + FnMut(IoKind, &str),576 F: 'static + Send + FnMut(IoKind, Option<&Command>, &IoMessage),
555 {577 {
556 let mut log_handlers = self.log_handlers.lock();578 let mut log_handlers = self.log_handlers.lock();
557 log_handlers.push((kind, Box::new(f)));579 log_handlers.push((kind, Box::new(f)));
@@ -562,7 +584,7 @@ pub struct TcpTransport { · 4 unmodified lines
562 pub port: u16,584 pub port: u16,
563 pub host: Ipv4Addr,585 pub host: Ipv4Addr,
564 pub timeout: u64,586 pub timeout: u64,
565 process: Mutex<Child>,587 process: Option<Mutex<Child>>,
566}588}
567 589
568impl TcpTransport {590impl TcpTransport {
@@ -591,26 +613,23 @@ impl TcpTransport { · 22 unmodified lines
591 let host = connection_args.host;613 let host = connection_args.host;
592 let port = connection_args.port;614 let port = connection_args.port;
593 615
594 let mut command = util::command::new_std_command(&binary.command);616 let mut process = if let Some(command) = &binary.command {
595 util::set_pre_exec_to_start_new_session(&mut command);617 let mut command = util::command::new_std_command(&command);
596 let mut command = smol::process::Command::from(command);
597 618
598 if let Some(cwd) = &binary.cwd {619 if let Some(cwd) = &binary.cwd {
599 command.current_dir(cwd);620 command.current_dir(cwd);
600 }621 }
601
602 command.args(&binary.arguments);
603 command.envs(&binary.envs);
604 622
605 command623 command.args(&binary.arguments);
606 .stdin(Stdio::null())624 command.envs(&binary.envs);
607 .stdout(Stdio::piped())
608 .stderr(Stdio::piped())
609 .kill_on_drop(true);
610 625
611 let mut process = command626 Some(
612 .spawn()627 Child::spawn(command, Stdio::null())
613 .with_context(|| "failed to start debug adapter.")?;628 .with_context(|| "failed to start debug adapter.")?,
629 )
630 } else {
631 None
632 };
614 633
615 let address = SocketAddrV4::new(host, port);634 let address = SocketAddrV4::new(host, port);
616 635
@@ -628,15 +647,18 @@ impl TcpTransport { · 11 unmodified lines
628 match TcpStream::connect(address).await {647 match TcpStream::connect(address).await {
629 Ok(stream) => return Ok((process, stream.split())),648 Ok(stream) => return Ok((process, stream.split())),
630 Err(_) => {649 Err(_) => {
631 if let Ok(Some(_)) = process.try_status() {650 if let Some(p) = &mut process {
632 let output = process.output().await?;651 if let Ok(Some(_)) = p.try_status() {
633 let output = if output.stderr.is_empty() {652 let output = process.take().unwrap().into_inner().output().await?;
634 String::from_utf8_lossy(&output.stdout).to_string()653 let output = if output.stderr.is_empty() {
635 } else {654 String::from_utf8_lossy(&output.stdout).to_string()
636 String::from_utf8_lossy(&output.stderr).to_string()655 } else {
637 };656 String::from_utf8_lossy(&output.stderr).to_string()
638 anyhow::bail!("{output}\nerror: process exited before debugger attached.");657 };
658 anyhow::bail!("{output}\nerror: process exited before debugger attached.");
659 }
639 }660 }
661
640 cx.background_executor().timer(Duration::from_millis(100)).await;662 cx.background_executor().timer(Duration::from_millis(100)).await;
641 }663 }
642 }664 }
@@ -649,13 +671,13 @@ impl TcpTransport { · 6 unmodified lines
649 host,671 host,
650 port672 port
651 );673 );
652 let stdout = process.stdout.take();674 let stdout = process.as_mut().and_then(|p| p.stdout.take());
653 let stderr = process.stderr.take();675 let stderr = process.as_mut().and_then(|p| p.stderr.take());
654 676
655 let this = Self {677 let this = Self {
656 port,678 port,
657 host,679 host,
658 process: Mutex::new(process),680 process: process.map(Mutex::new),
659 timeout,681 timeout,
660 };682 };
661 683
@@ -673,10 +695,19 @@ impl TcpTransport { · 11 unmodified lines
673 true695 true
674 }696 }
675 697
676 async fn kill(&self) -> Result<()> {698 async fn kill(&self) {
677 self.process.lock().await.kill()?;699 if let Some(process) = &self.process {
700 let mut process = process.lock().await;
701 Child::kill(&mut process);
702 }
703 }
704}
678 705
679 Ok(())706impl Drop for TcpTransport {
707 fn drop(&mut self) {
708 if let Some(mut p) = self.process.take() {
709 p.get_mut().kill();
710 }
680 }711 }
681}712}
682 713
@@ -687,9 +718,12 @@ pub struct StdioTransport { · 4 unmodified lines
687impl StdioTransport {718impl StdioTransport {
688 #[allow(dead_code, reason = "This is used in non test builds of Zed")]719 #[allow(dead_code, reason = "This is used in non test builds of Zed")]
689 async fn start(binary: &DebugAdapterBinary, _: AsyncApp) -> Result<(TransportPipe, Self)> {720 async fn start(binary: &DebugAdapterBinary, _: AsyncApp) -> Result<(TransportPipe, Self)> {
690 let mut command = util::command::new_std_command(&binary.command);721 let Some(binary_command) = &binary.command else {
691 util::set_pre_exec_to_start_new_session(&mut command);722 bail!(
692 let mut command = smol::process::Command::from(command);723 "When using the `stdio` transport, the path to a debug adapter binary must be set by Zed."
724 );
725 };
726 let mut command = util::command::new_std_command(&binary_command);
693 727
694 if let Some(cwd) = &binary.cwd {728 if let Some(cwd) = &binary.cwd {
695 command.current_dir(cwd);729 command.current_dir(cwd);
@@ -698,16 +732,10 @@ impl StdioTransport { · 2 unmodified lines
698 command.args(&binary.arguments);732 command.args(&binary.arguments);
699 command.envs(&binary.envs);733 command.envs(&binary.envs);
700 734
701 command735 let mut process = Child::spawn(command, Stdio::piped()).with_context(|| {
702 .stdin(Stdio::piped())
703 .stdout(Stdio::piped())
704 .stderr(Stdio::piped())
705 .kill_on_drop(true);
706
707 let mut process = command.spawn().with_context(|| {
708 format!(736 format!(
709 "failed to spawn command `{} {}`.",737 "failed to spawn command `{} {}`.",
710 binary.command,738 binary_command,
711 binary.arguments.join(" ")739 binary.arguments.join(" ")
712 )740 )
713 })?;741 })?;
@@ -722,7 +750,7 @@ impl StdioTransport { · 8 unmodified lines
722 if stderr.is_none() {750 if stderr.is_none() {
723 bail!(751 bail!(
724 "Failed to connect to stderr for debug adapter command {}",752 "Failed to connect to stderr for debug adapter command {}",
725 &binary.command753 &binary_command
726 );754 );
727 }755 }
728 756
@@ -745,9 +773,15 @@ impl StdioTransport { · 16 unmodified lines
745 false773 false
746 }774 }
747 775
748 async fn kill(&self) -> Result<()> {776 async fn kill(&self) {
749 self.process.lock().await.kill()?;777 let mut process = self.process.lock().await;
750 Ok(())778 Child::kill(&mut process);
779 }
780}
781
782impl Drop for StdioTransport {
783 fn drop(&mut self) {
784 self.process.get_mut().kill();
751 }785 }
752}786}
753 787
@@ -921,7 +955,66 @@ impl FakeTransport { · 167 unmodified lines
921 false955 false
922 }956 }
923 957
924 async fn kill(&self) -> Result<()> {958 async fn kill(&self) {}
925 Ok(())959}
960
961struct Child {
962 process: smol::process::Child,
963}
964
965impl std::ops::Deref for Child {
966 type Target = smol::process::Child;
967
968 fn deref(&self) -> &Self::Target {
969 &self.process
970 }
971}
972
973impl std::ops::DerefMut for Child {
974 fn deref_mut(&mut self) -> &mut Self::Target {
975 &mut self.process
976 }
977}
978
979impl Child {
980 fn into_inner(self) -> smol::process::Child {
981 self.process
982 }
983
984 #[cfg(not(windows))]
985 fn spawn(mut command: std::process::Command, stdin: Stdio) -> Result<Self> {
986 util::set_pre_exec_to_start_new_session(&mut command);
987 let process = smol::process::Command::from(command)
988 .stdin(stdin)
989 .stdout(Stdio::piped())
990 .stderr(Stdio::piped())
991 .spawn()?;
992 Ok(Self { process })
993 }
994
995 #[cfg(windows)]
996 fn spawn(command: std::process::Command, stdin: Stdio) -> Result<Self> {
997 // TODO(windows): create a job object and add the child process handle to it,
998 // see https://learn.microsoft.com/en-us/windows/win32/procthread/job-objects
999 let process = smol::process::Command::from(command)
1000 .stdin(stdin)
1001 .stdout(Stdio::piped())
1002 .stderr(Stdio::piped())
1003 .spawn()?;
1004 Ok(Self { process })
1005 }
1006
1007 #[cfg(not(windows))]
1008 fn kill(&mut self) {
1009 let pid = self.process.id();
1010 unsafe {
1011 libc::killpg(pid as i32, libc::SIGKILL);
1012 }
1013 }
1014
1015 #[cfg(windows)]
1016 fn kill(&mut self) {
1017 // TODO(windows): terminate the job object in kill
1018 let _ = self.process.kill();
926 }1019 }
927}1020}
crates/dap_adapters/Cargo.toml+1−0
@@ -23,6 +23,7 @@ doctest = false · 22 unmodified lines
23[dependencies]23[dependencies]
24anyhow.workspace = true24anyhow.workspace = true
25async-trait.workspace = true25async-trait.workspace = true
26collections.workspace = true
26dap.workspace = true27dap.workspace = true
27futures.workspace = true28futures.workspace = true
28gpui.workspace = true29gpui.workspace = true
crates/dap_adapters/src/codelldb.rs+11−8
@@ -21,18 +21,21 @@ impl CodeLldbDebugAdapter { · 20 unmodified lines
21 21
22 fn request_args(22 fn request_args(
23 &self,23 &self,
24 delegate: &Arc<dyn DapDelegate>,
24 task_definition: &DebugTaskDefinition,25 task_definition: &DebugTaskDefinition,
25 ) -> Result<dap::StartDebuggingRequestArguments> {26 ) -> Result<dap::StartDebuggingRequestArguments> {
26 // CodeLLDB uses `name` for a terminal label.27 // CodeLLDB uses `name` for a terminal label.
27 let mut configuration = task_definition.config.clone();28 let mut configuration = task_definition.config.clone();
28 29
29 configuration30 let obj = configuration
30 .as_object_mut()31 .as_object_mut()
31 .context("CodeLLDB is not a valid json object")?32 .context("CodeLLDB is not a valid json object")?;
32 .insert(33
33 "name".into(),34 obj.entry("name")
34 Value::String(String::from(task_definition.label.as_ref())),35 .or_insert(Value::String(String::from(task_definition.label.as_ref())));
35 );36
37 obj.entry("cwd")
38 .or_insert(delegate.worktree_root_path().to_string_lossy().into());
36 39
37 let request = self.request_kind(&configuration)?;40 let request = self.request_kind(&configuration)?;
38 41
@@ -359,13 +362,13 @@ impl DebugAdapter for CodeLldbDebugAdapter { · 320 unmodified lines
359 };362 };
360 363
361 Ok(DebugAdapterBinary {364 Ok(DebugAdapterBinary {
362 command: command.unwrap(),365 command: Some(command.unwrap()),
363 cwd: Some(delegate.worktree_root_path().to_path_buf()),366 cwd: Some(delegate.worktree_root_path().to_path_buf()),
364 arguments: vec![367 arguments: vec![
365 "--settings".into(),368 "--settings".into(),
366 json!({"sourceLanguages": ["cpp", "rust"]}).to_string(),369 json!({"sourceLanguages": ["cpp", "rust"]}).to_string(),
367 ],370 ],
368 request_args: self.request_args(&config)?,371 request_args: self.request_args(delegate, &config)?,
369 envs: HashMap::default(),372 envs: HashMap::default(),
370 connection: None,373 connection: None,
371 })374 })
crates/dap_adapters/src/gdb.rs+11−6
@@ -177,18 +177,23 @@ impl DebugAdapter for GdbDebugAdapter { · 176 unmodified lines
177 177
178 let gdb_path = user_setting_path.unwrap_or(gdb_path?);178 let gdb_path = user_setting_path.unwrap_or(gdb_path?);
179 179
180 let request_args = StartDebuggingRequestArguments {180 let mut configuration = config.config.clone();
181 request: self.request_kind(&config.config)?,181 if let Some(configuration) = configuration.as_object_mut() {
182 configuration: config.config.clone(),182 configuration
183 };183 .entry("cwd")
184 .or_insert_with(|| delegate.worktree_root_path().to_string_lossy().into());
185 }
184 186
185 Ok(DebugAdapterBinary {187 Ok(DebugAdapterBinary {
186 command: gdb_path,188 command: Some(gdb_path),
187 arguments: vec!["-i=dap".into()],189 arguments: vec!["-i=dap".into()],
188 envs: HashMap::default(),190 envs: HashMap::default(),
189 cwd: Some(delegate.worktree_root_path().to_path_buf()),191 cwd: Some(delegate.worktree_root_path().to_path_buf()),
190 connection: None,192 connection: None,
191 request_args,193 request_args: StartDebuggingRequestArguments {
194 request: self.request_kind(&config.config)?,
195 configuration,
196 },
192 })197 })
193 }198 }
194}199}
crates/dap_adapters/src/go.rs+48−24
@@ -2,7 +2,7 @@ use anyhow::{Context as _, bail}; · 1 unmodified line
2use dap::{2use dap::{
3 StartDebuggingRequestArguments,3 StartDebuggingRequestArguments,
4 adapters::{4 adapters::{
5 DebugTaskDefinition, DownloadedFileType, download_adapter_from_github,5 DebugTaskDefinition, DownloadedFileType, TcpArguments, download_adapter_from_github,
6 latest_github_release,6 latest_github_release,
7 },7 },
8};8};
@@ -10,6 +10,7 @@ use dap::{ · 1 unmodified line
10use gpui::{AsyncApp, SharedString};10use gpui::{AsyncApp, SharedString};
11use language::LanguageName;11use language::LanguageName;
12use std::{collections::HashMap, env::consts, ffi::OsStr, path::PathBuf, sync::OnceLock};12use std::{collections::HashMap, env::consts, ffi::OsStr, path::PathBuf, sync::OnceLock};
13use task::TcpArgumentsTemplate;
13use util;14use util;
14 15
15use crate::*;16use crate::*;
@@ -433,10 +434,6 @@ impl DebugAdapter for GoDebugAdapter { · 417 unmodified lines
433 434
434 adapter_path.join("dlv").to_string_lossy().to_string()435 adapter_path.join("dlv").to_string_lossy().to_string()
435 };436 };
436 let minidelve_path = self.install_shim(delegate).await?;
437 let tcp_connection = task_definition.tcp_connection.clone().unwrap_or_default();
438
439 let (host, port, _) = crate::configure_tcp_connection(tcp_connection).await?;
440 437
441 let cwd = task_definition438 let cwd = task_definition
442 .config439 .config
@@ -445,31 +442,58 @@ impl DebugAdapter for GoDebugAdapter { · 2 unmodified lines
445 .map(PathBuf::from)442 .map(PathBuf::from)
446 .unwrap_or_else(|| delegate.worktree_root_path().to_path_buf());443 .unwrap_or_else(|| delegate.worktree_root_path().to_path_buf());
447 444
448 let arguments = if cfg!(windows) {445 let arguments;
449 vec![446 let command;
450 delve_path,447 let connection;
451 "dap".into(),
452 "--listen".into(),
453 format!("{}:{}", host, port),
454 "--headless".into(),
455 ]
456 } else {
457 vec![
458 delve_path,
459 "dap".into(),
460 "--listen".into(),
461 format!("{}:{}", host, port),
462 ]
463 };
464 448
449 let mut configuration = task_definition.config.clone();
450 if let Some(configuration) = configuration.as_object_mut() {
451 configuration
452 .entry("cwd")
453 .or_insert_with(|| delegate.worktree_root_path().to_string_lossy().into());
454 }
455
456 if let Some(connection_options) = &task_definition.tcp_connection {
457 command = None;
458 arguments = vec![];
459 let (host, port, timeout) =
460 crate::configure_tcp_connection(connection_options.clone()).await?;
461 connection = Some(TcpArguments {
462 host,
463 port,
464 timeout,
465 });
466 } else {
467 let minidelve_path = self.install_shim(delegate).await?;
468 let (host, port, _) =
469 crate::configure_tcp_connection(TcpArgumentsTemplate::default()).await?;
470 command = Some(minidelve_path.to_string_lossy().into_owned());
471 connection = None;
472 arguments = if cfg!(windows) {
473 vec![
474 delve_path,
475 "dap".into(),
476 "--listen".into(),
477 format!("{}:{}", host, port),
478 "--headless".into(),
479 ]
480 } else {
481 vec![
482 delve_path,
483 "dap".into(),
484 "--listen".into(),
485 format!("{}:{}", host, port),
486 ]
487 };
488 }
465 Ok(DebugAdapterBinary {489 Ok(DebugAdapterBinary {
466 command: minidelve_path.to_string_lossy().into_owned(),490 command,
467 arguments,491 arguments,
468 cwd: Some(cwd),492 cwd: Some(cwd),
469 envs: HashMap::default(),493 envs: HashMap::default(),
470 connection: None,494 connection,
471 request_args: StartDebuggingRequestArguments {495 request_args: StartDebuggingRequestArguments {
472 configuration: task_definition.config.clone(),496 configuration,
473 request: self.request_kind(&task_definition.config)?,497 request: self.request_kind(&task_definition.config)?,
474 },498 },
475 })499 })
crates/dap_adapters/src/javascript.rs+61−8
@@ -2,6 +2,7 @@ use adapters::latest_github_release; · 1 unmodified line
2use anyhow::Context as _;2use anyhow::Context as _;
3use dap::{StartDebuggingRequestArguments, adapters::DebugTaskDefinition};3use dap::{StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
4use gpui::AsyncApp;4use gpui::AsyncApp;
5use serde_json::Value;
5use std::{collections::HashMap, path::PathBuf, sync::OnceLock};6use std::{collections::HashMap, path::PathBuf, sync::OnceLock};
6use task::DebugRequest;7use task::DebugRequest;
7use util::ResultExt;8use util::ResultExt;
@@ -68,13 +69,44 @@ impl JsDebugAdapter { · 60 unmodified lines
68 let tcp_connection = task_definition.tcp_connection.clone().unwrap_or_default();69 let tcp_connection = task_definition.tcp_connection.clone().unwrap_or_default();
69 let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;70 let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
70 71
72 let mut configuration = task_definition.config.clone();
73 if let Some(configuration) = configuration.as_object_mut() {
74 if let Some(program) = configuration
75 .get("program")
76 .cloned()
77 .and_then(|value| value.as_str().map(str::to_owned))
78 {
79 match program.as_str() {
80 "npm" | "pnpm" | "yarn" | "bun"
81 if !configuration.contains_key("runtimeExecutable")
82 && !configuration.contains_key("runtimeArgs") =>
83 {
84 configuration.remove("program");
85 configuration.insert("runtimeExecutable".to_owned(), program.into());
86 if let Some(args) = configuration.remove("args") {
87 configuration.insert("runtimeArgs".to_owned(), args);
88 }
89 }
90 _ => {}
91 }
92 }
93
94 configuration
95 .entry("cwd")
96 .or_insert(delegate.worktree_root_path().to_string_lossy().into());
97
98 configuration.entry("type").and_modify(normalize_task_type);
99 }
100
71 Ok(DebugAdapterBinary {101 Ok(DebugAdapterBinary {
72 command: delegate102 command: Some(
73 .node_runtime()103 delegate
74 .binary_path()104 .node_runtime()
75 .await?105 .binary_path()
76 .to_string_lossy()106 .await?
77 .into_owned(),107 .to_string_lossy()
108 .into_owned(),
109 ),
78 arguments: vec![110 arguments: vec![
79 adapter_path111 adapter_path
80 .join(Self::ADAPTER_PATH)112 .join(Self::ADAPTER_PATH)
@@ -91,7 +123,7 @@ impl JsDebugAdapter { · 10 unmodified lines
91 timeout,123 timeout,
92 }),124 }),
93 request_args: StartDebuggingRequestArguments {125 request_args: StartDebuggingRequestArguments {
94 configuration: task_definition.config.clone(),126 configuration,
95 request: self.request_kind(&task_definition.config)?,127 request: self.request_kind(&task_definition.config)?,
96 },128 },
97 })129 })
@@ -171,7 +203,7 @@ impl DebugAdapter for JsDebugAdapter { · 73 unmodified lines
171 "properties": {203 "properties": {
172 "type": {204 "type": {
173 "type": "string",205 "type": "string",
174 "enum": ["pwa-node", "node", "chrome", "pwa-chrome", "edge", "pwa-edge"],206 "enum": ["pwa-node", "node", "chrome", "pwa-chrome", "msedge", "pwa-msedge"],
175 "description": "The type of debug session",207 "description": "The type of debug session",
176 "default": "pwa-node"208 "default": "pwa-node"
177 },209 },
@@ -431,4 +463,25 @@ impl DebugAdapter for JsDebugAdapter { · 253 unmodified lines
431 self.get_installed_binary(delegate, &config, user_installed_path, cx)463 self.get_installed_binary(delegate, &config, user_installed_path, cx)
432 .await464 .await
433 }465 }
466
467 fn label_for_child_session(&self, args: &StartDebuggingRequestArguments) -> Option<String> {
468 let label = args.configuration.get("name")?.as_str()?;
469 Some(label.to_owned())
470 }
471}
472
473fn normalize_task_type(task_type: &mut Value) {
474 let Some(task_type_str) = task_type.as_str() else {
475 return;
476 };
477
478 let new_name = match task_type_str {
479 "node" | "pwa-node" => "pwa-node",
480 "chrome" | "pwa-chrome" => "pwa-chrome",
481 "edge" | "msedge" | "pwa-edge" | "pwa-msedge" => "pwa-msedge",
482 _ => task_type_str,
483 }
484 .to_owned();
485
486 *task_type = Value::String(new_name);
434}487}
crates/dap_adapters/src/php.rs+15−7
@@ -71,13 +71,21 @@ impl PhpDebugAdapter { · 70 unmodified lines
71 let tcp_connection = task_definition.tcp_connection.clone().unwrap_or_default();71 let tcp_connection = task_definition.tcp_connection.clone().unwrap_or_default();
72 let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;72 let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
73 73
74 let mut configuration = task_definition.config.clone();
75 if let Some(obj) = configuration.as_object_mut() {
76 obj.entry("cwd")
77 .or_insert_with(|| delegate.worktree_root_path().to_string_lossy().into());
78 }
79
74 Ok(DebugAdapterBinary {80 Ok(DebugAdapterBinary {
75 command: delegate81 command: Some(
76 .node_runtime()82 delegate
77 .binary_path()83 .node_runtime()
78 .await?84 .binary_path()
79 .to_string_lossy()85 .await?
80 .into_owned(),86 .to_string_lossy()
87 .into_owned(),
88 ),
81 arguments: vec![89 arguments: vec![
82 adapter_path90 adapter_path
83 .join(Self::ADAPTER_PATH)91 .join(Self::ADAPTER_PATH)
@@ -93,7 +101,7 @@ impl PhpDebugAdapter { · 9 unmodified lines
93 cwd: Some(delegate.worktree_root_path().to_path_buf()),101 cwd: Some(delegate.worktree_root_path().to_path_buf()),
94 envs: HashMap::default(),102 envs: HashMap::default(),
95 request_args: StartDebuggingRequestArguments {103 request_args: StartDebuggingRequestArguments {
96 configuration: task_definition.config.clone(),104 configuration,
97 request: <Self as DebugAdapter>::request_kind(self, &task_definition.config)?,105 request: <Self as DebugAdapter>::request_kind(self, &task_definition.config)?,
98 },106 },
99 })107 })
crates/dap_adapters/src/python.rs+8−2
@@ -83,6 +83,7 @@ impl PythonDebugAdapter { · 82 unmodified lines
83 83
84 fn request_args(84 fn request_args(
85 &self,85 &self,
86 delegate: &Arc<dyn DapDelegate>,
86 task_definition: &DebugTaskDefinition,87 task_definition: &DebugTaskDefinition,
87 ) -> Result<StartDebuggingRequestArguments> {88 ) -> Result<StartDebuggingRequestArguments> {
88 let request = self.request_kind(&task_definition.config)?;89 let request = self.request_kind(&task_definition.config)?;
@@ -95,6 +96,11 @@ impl PythonDebugAdapter { · 6 unmodified lines
95 }96 }
96 }97 }
97 98
99 if let Some(obj) = configuration.as_object_mut() {
100 obj.entry("cwd")
101 .or_insert(delegate.worktree_root_path().to_string_lossy().into());
102 }
103
98 Ok(StartDebuggingRequestArguments {104 Ok(StartDebuggingRequestArguments {
99 configuration,105 configuration,
100 request,106 request,
@@ -187,7 +193,7 @@ impl PythonDebugAdapter { · 86 unmodified lines
187 );193 );
188 194
189 Ok(DebugAdapterBinary {195 Ok(DebugAdapterBinary {
190 command: python_command,196 command: Some(python_command),
191 arguments,197 arguments,
192 connection: Some(adapters::TcpArguments {198 connection: Some(adapters::TcpArguments {
193 host,199 host,
@@ -196,7 +202,7 @@ impl PythonDebugAdapter { · 2 unmodified lines
196 }),202 }),
197 cwd: Some(delegate.worktree_root_path().to_path_buf()),203 cwd: Some(delegate.worktree_root_path().to_path_buf()),
198 envs: HashMap::default(),204 envs: HashMap::default(),
199 request_args: self.request_args(config)?,205 request_args: self.request_args(delegate, config)?,
200 })206 })
201 }207 }
202}208}
crates/dap_adapters/src/ruby.rs+107−176
@@ -1,16 +1,18 @@
1use anyhow::Result;1use anyhow::{Result, bail};
2use async_trait::async_trait;2use async_trait::async_trait;
3use collections::FxHashMap;
3use dap::{4use dap::{
4 DebugRequest, StartDebuggingRequestArguments,5 DebugRequest, StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest,
5 adapters::{6 adapters::{
6 DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName, DebugTaskDefinition,7 DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName, DebugTaskDefinition,
7 },8 },
8};9};
9use gpui::{AsyncApp, SharedString};10use gpui::{AsyncApp, SharedString};
10use language::LanguageName;11use language::LanguageName;
12use serde::{Deserialize, Serialize};
11use serde_json::json;13use serde_json::json;
12use std::path::PathBuf;14use std::path::PathBuf;
13use std::sync::Arc;15use std::{ffi::OsStr, sync::Arc};
14use task::{DebugScenario, ZedDebugConfig};16use task::{DebugScenario, ZedDebugConfig};
15use util::command::new_smol_command;17use util::command::new_smol_command;
16 18
@@ -21,6 +23,18 @@ impl RubyDebugAdapter { · 4 unmodified lines
21 const ADAPTER_NAME: &'static str = "Ruby";23 const ADAPTER_NAME: &'static str = "Ruby";
22}24}
23 25
26#[derive(Serialize, Deserialize)]
27struct RubyDebugConfig {
28 script_or_command: Option<String>,
29 script: Option<String>,
30 command: Option<String>,
31 #[serde(default)]
32 args: Vec<String>,
33 #[serde(default)]
34 env: FxHashMap<String, String>,
35 cwd: Option<PathBuf>,
36}
37
24#[async_trait(?Send)]38#[async_trait(?Send)]
25impl DebugAdapter for RubyDebugAdapter {39impl DebugAdapter for RubyDebugAdapter {
26 fn name(&self) -> DebugAdapterName {40 fn name(&self) -> DebugAdapterName {
@@ -31,185 +45,70 @@ impl DebugAdapter for RubyDebugAdapter { · 4 unmodified lines
31 Some(SharedString::new_static("Ruby").into())45 Some(SharedString::new_static("Ruby").into())
32 }46 }
33 47
48 fn request_kind(&self, _: &serde_json::Value) -> Result<StartDebuggingRequestArgumentsRequest> {
49 Ok(StartDebuggingRequestArgumentsRequest::Launch)
50 }
51
34 async fn dap_schema(&self) -> serde_json::Value {52 async fn dap_schema(&self) -> serde_json::Value {
35 json!({53 json!({
36 "oneOf": [54 "type": "object",
37 {55 "properties": {
38 "allOf": [56 "command": {
39 {57 "type": "string",
40 "type": "object",58 "description": "Command name (ruby, rake, bin/rails, bundle exec ruby, etc)",
41 "required": ["request"],
42 "properties": {
43 "request": {
44 "type": "string",
45 "enum": ["launch"],
46 "description": "Request to launch a new process"
47 }
48 }
49 },
50 {
51 "type": "object",
52 "required": ["script"],
53 "properties": {
54 "command": {
55 "type": "string",
56 "description": "Command name (ruby, rake, bin/rails, bundle exec ruby, etc)",
57 "default": "ruby"
58 },
59 "script": {
60 "type": "string",
61 "description": "Absolute path to a Ruby file."
62 },
63 "cwd": {
64 "type": "string",
65 "description": "Directory to execute the program in",
66 "default": "${ZED_WORKTREE_ROOT}"
67 },
68 "args": {
69 "type": "array",
70 "description": "Command line arguments passed to the program",
71 "items": {
72 "type": "string"
73 },
74 "default": []
75 },
76 "env": {
77 "type": "object",
78 "description": "Additional environment variables to pass to the debugging (and debugged) process",
79 "default": {}
80 },
81 "showProtocolLog": {
82 "type": "boolean",
83 "description": "Show a log of DAP requests, events, and responses",
84 "default": false
85 },
86 "useBundler": {
87 "type": "boolean",
88 "description": "Execute Ruby programs with `bundle exec` instead of directly",
89 "default": false
90 },
91 "bundlePath": {
92 "type": "string",
93 "description": "Location of the bundle executable"
94 },
95 "rdbgPath": {
96 "type": "string",
97 "description": "Location of the rdbg executable"
98 },
99 "askParameters": {
100 "type": "boolean",
101 "description": "Ask parameters at first."
102 },
103 "debugPort": {
104 "type": "string",
105 "description": "UNIX domain socket name or TPC/IP host:port"
106 },
107 "waitLaunchTime": {
108 "type": "number",
109 "description": "Wait time before connection in milliseconds"
110 },
111 "localfs": {
112 "type": "boolean",
113 "description": "true if the VSCode and debugger run on a same machine",
114 "default": false
115 },
116 "useTerminal": {
117 "type": "boolean",
118 "description": "Create a new terminal and then execute commands there",
119 "default": false
120 }
121 }
122 }
123 ]
124 },59 },
125 {60 "script": {
126 "allOf": [61 "type": "string",
127 {62 "description": "Absolute path to a Ruby file."
128 "type": "object",63 },
129 "required": ["request"],64 "cwd": {
130 "properties": {65 "type": "string",
131 "request": {66 "description": "Directory to execute the program in",
132 "type": "string",67 "default": "${ZED_WORKTREE_ROOT}"
133 "enum": ["attach"],68 },
134 "description": "Request to attach to an existing process"69 "args": {
135 }70 "type": "array",
136 }71 "description": "Command line arguments passed to the program",
137 },72 "items": {
138 {73 "type": "string"
139 "type": "object",74 },
140 "properties": {75 "default": []
141 "rdbgPath": {76 },
142 "type": "string",77 "env": {
143 "description": "Location of the rdbg executable"78 "type": "object",
144 },79 "description": "Additional environment variables to pass to the debugging (and debugged) process",
145 "debugPort": {80 "default": {}
146 "type": "string",81 },
147 "description": "UNIX domain socket name or TPC/IP host:port"82 }
148 },
149 "showProtocolLog": {
150 "type": "boolean",
151 "description": "Show a log of DAP requests, events, and responses",
152 "default": false
153 },
154 "localfs": {
155 "type": "boolean",
156 "description": "true if the VSCode and debugger run on a same machine",
157 "default": false
158 },
159 "localfsMap": {
160 "type": "string",
161 "description": "Specify pairs of remote root path and local root path like `/remote_dir:/local_dir`. You can specify multiple pairs like `/rem1:/loc1,/rem2:/loc2` by concatenating with `,`."
162 },
163 "env": {
164 "type": "object",
165 "description": "Additional environment variables to pass to the rdbg process",
166 "default": {}
167 }
168 }
169 }
170 ]
171 }
172 ]
173 })83 })
174 }84 }
175 85
176 fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {86 fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
177 let mut config = serde_json::Map::new();87 match zed_scenario.request {
178
179 match &zed_scenario.request {
180 DebugRequest::Launch(launch) => {88 DebugRequest::Launch(launch) => {
181 config.insert("request".to_string(), json!("launch"));89 let config = RubyDebugConfig {
182 config.insert("script".to_string(), json!(launch.program));90 script_or_command: Some(launch.program),
183 config.insert("command".to_string(), json!("ruby"));91 script: None,
184 92 command: None,
185 if !launch.args.is_empty() {93 args: launch.args,
186 config.insert("args".to_string(), json!(launch.args));94 env: launch.env,
187 }95 cwd: launch.cwd.clone(),
188 96 };
189 if !launch.env.is_empty() {97
190 config.insert("env".to_string(), json!(launch.env));98 let config = serde_json::to_value(config)?;
191 }99
192 100 Ok(DebugScenario {
193 if let Some(cwd) = &launch.cwd {101 adapter: zed_scenario.adapter,
194 config.insert("cwd".to_string(), json!(cwd));102 label: zed_scenario.label,
195 }103 config,
196 104 tcp_connection: None,
197 // Ruby stops on entry so there's no need to handle that case105 build: None,
106 })
198 }107 }
199 DebugRequest::Attach(attach) => {108 DebugRequest::Attach(_) => {
200 config.insert("request".to_string(), json!("attach"));109 anyhow::bail!("Attach requests are unsupported");
201
202 config.insert("processId".to_string(), json!(attach.process_id));
203 }110 }
204 }111 }
205
206 Ok(DebugScenario {
207 adapter: zed_scenario.adapter,
208 label: zed_scenario.label,
209 config: serde_json::Value::Object(config),
210 tcp_connection: None,
211 build: None,
212 })
213 }112 }
214 113
215 async fn get_binary(114 async fn get_binary(
@@ -247,26 +146,58 @@ impl DebugAdapter for RubyDebugAdapter { · 31 unmodified lines
247 146
248 let tcp_connection = definition.tcp_connection.clone().unwrap_or_default();147 let tcp_connection = definition.tcp_connection.clone().unwrap_or_default();
249 let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;148 let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
149 let ruby_config = serde_json::from_value::<RubyDebugConfig>(definition.config.clone())?;
250 150
251 let arguments = vec![151 let mut arguments = vec![
252 "--open".to_string(),152 "--open".to_string(),
253 format!("--port={}", port),153 format!("--port={}", port),
254 format!("--host={}", host),154 format!("--host={}", host),
255 ];155 ];
256 156
157 if let Some(script) = &ruby_config.script {
158 arguments.push(script.clone());
159 } else if let Some(command) = &ruby_config.command {
160 arguments.push("--command".to_string());
161 arguments.push(command.clone());
162 } else if let Some(command_or_script) = &ruby_config.script_or_command {
163 if delegate
164 .which(OsStr::new(&command_or_script))
165 .await
166 .is_some()
167 {
168 arguments.push("--command".to_string());
169 }
170 arguments.push(command_or_script.clone());
171 } else {
172 bail!("Ruby debug config must have 'script' or 'command' args");
173 }
174
175 arguments.extend(ruby_config.args);
176
177 let mut configuration = definition.config.clone();
178 if let Some(configuration) = configuration.as_object_mut() {
179 configuration
180 .entry("cwd")
181 .or_insert_with(|| delegate.worktree_root_path().to_string_lossy().into());
182 }
183
257 Ok(DebugAdapterBinary {184 Ok(DebugAdapterBinary {
258 command: rdbg_path.to_string_lossy().to_string(),185 command: Some(rdbg_path.to_string_lossy().to_string()),
259 arguments,186 arguments,
260 connection: Some(dap::adapters::TcpArguments {187 connection: Some(dap::adapters::TcpArguments {
261 host,188 host,
262 port,189 port,
263 timeout,190 timeout,
264 }),191 }),
265 cwd: None,192 cwd: Some(
266 envs: std::collections::HashMap::default(),193 ruby_config
194 .cwd
195 .unwrap_or(delegate.worktree_root_path().to_owned()),
196 ),
197 envs: ruby_config.env.into_iter().collect(),
267 request_args: StartDebuggingRequestArguments {198 request_args: StartDebuggingRequestArguments {
268 request: self.request_kind(&definition.config)?,199 request: self.request_kind(&definition.config)?,
269 configuration: definition.config.clone(),200 configuration,
270 },201 },
271 })202 })
272 }203 }
crates/db/src/db.rs+2−2
@@ -74,7 +74,7 @@ pub async fn open_db<M: Migrator + 'static>(db_dir: &Path, scope: &str) -> Threa · 73 unmodified lines
74}74}
75 75
76async fn open_main_db<M: Migrator>(db_path: &Path) -> Option<ThreadSafeConnection> {76async fn open_main_db<M: Migrator>(db_path: &Path) -> Option<ThreadSafeConnection> {
77 log::info!("Opening main db");77 log::info!("Opening database {}", db_path.display());
78 ThreadSafeConnection::builder::<M>(db_path.to_string_lossy().as_ref(), true)78 ThreadSafeConnection::builder::<M>(db_path.to_string_lossy().as_ref(), true)
79 .with_db_initialization_query(DB_INITIALIZE_QUERY)79 .with_db_initialization_query(DB_INITIALIZE_QUERY)
80 .with_connection_initialize_query(CONNECTION_INITIALIZE_QUERY)80 .with_connection_initialize_query(CONNECTION_INITIALIZE_QUERY)
@@ -84,7 +84,7 @@ async fn open_main_db<M: Migrator>(db_path: &Path) -> Option<ThreadSafeConnectio · 3 unmodified lines
84}84}
85 85
86async fn open_fallback_db<M: Migrator>() -> ThreadSafeConnection {86async fn open_fallback_db<M: Migrator>() -> ThreadSafeConnection {
87 log::info!("Opening fallback db");87 log::warn!("Opening fallback in-memory database");
88 ThreadSafeConnection::builder::<M>(FALLBACK_DB_NAME, false)88 ThreadSafeConnection::builder::<M>(FALLBACK_DB_NAME, false)
89 .with_db_initialization_query(DB_INITIALIZE_QUERY)89 .with_db_initialization_query(DB_INITIALIZE_QUERY)
90 .with_connection_initialize_query(CONNECTION_INITIALIZE_QUERY)90 .with_connection_initialize_query(CONNECTION_INITIALIZE_QUERY)
crates/debugger_tools/src/dap_log.rs+320−155
@@ -1,4 +1,5 @@
1use dap::{1use dap::{
2 adapters::DebugAdapterName,
2 client::SessionId,3 client::SessionId,
3 debugger_settings::DebuggerSettings,4 debugger_settings::DebuggerSettings,
4 transport::{IoKind, LogKind},5 transport::{IoKind, LogKind},
@@ -31,6 +32,13 @@ use workspace::{ · 26 unmodified lines
31 ui::{Button, Clickable, ContextMenu, Label, LabelCommon, PopoverMenu, h_flex},32 ui::{Button, Clickable, ContextMenu, Label, LabelCommon, PopoverMenu, h_flex},
32};33};
33 34
35// TODO:
36// - [x] stop sorting by session ID
37// - [x] pick the most recent session by default (logs if available, RPC messages otherwise)
38// - [ ] dump the launch/attach request somewhere (logs?)
39
40const MAX_SESSIONS: usize = 10;
41
34struct DapLogView {42struct DapLogView {
35 editor: Entity<Editor>,43 editor: Entity<Editor>,
36 focus_handle: FocusHandle,44 focus_handle: FocusHandle,
@@ -43,9 +51,9 @@ struct DapLogView { · 6 unmodified lines
43 51
44pub struct LogStore {52pub struct LogStore {
45 projects: HashMap<WeakEntity<Project>, ProjectState>,53 projects: HashMap<WeakEntity<Project>, ProjectState>,
46 debug_clients: HashMap<SessionId, DebugAdapterState>,54 debug_sessions: VecDeque<DebugAdapterState>,
47 rpc_tx: UnboundedSender<(SessionId, IoKind, String)>,55 rpc_tx: UnboundedSender<(SessionId, IoKind, Option<SharedString>, SharedString)>,
48 adapter_log_tx: UnboundedSender<(SessionId, IoKind, String)>,56 adapter_log_tx: UnboundedSender<(SessionId, IoKind, Option<SharedString>, SharedString)>,
49}57}
50 58
51struct ProjectState {59struct ProjectState {
@@ -53,13 +61,19 @@ struct ProjectState { · 1 unmodified line
53}61}
54 62
55struct DebugAdapterState {63struct DebugAdapterState {
56 log_messages: VecDeque<String>,64 id: SessionId,
65 log_messages: VecDeque<SharedString>,
57 rpc_messages: RpcMessages,66 rpc_messages: RpcMessages,
67 adapter_name: DebugAdapterName,
68 has_adapter_logs: bool,
69 is_terminated: bool,
58}70}
59 71
60struct RpcMessages {72struct RpcMessages {
61 messages: VecDeque<String>,73 messages: VecDeque<SharedString>,
62 last_message_kind: Option<MessageKind>,74 last_message_kind: Option<MessageKind>,
75 initialization_sequence: Vec<SharedString>,
76 last_init_message_kind: Option<MessageKind>,
63}77}
64 78
65impl RpcMessages {79impl RpcMessages {
@@ -68,7 +82,9 @@ impl RpcMessages { · 2 unmodified lines
68 fn new() -> Self {82 fn new() -> Self {
69 Self {83 Self {
70 last_message_kind: None,84 last_message_kind: None,
85 last_init_message_kind: None,
71 messages: VecDeque::with_capacity(Self::MESSAGE_QUEUE_LIMIT),86 messages: VecDeque::with_capacity(Self::MESSAGE_QUEUE_LIMIT),
87 initialization_sequence: Vec::new(),
72 }88 }
73 }89 }
74}90}
@@ -92,22 +108,27 @@ impl MessageKind { · 17 unmodified lines
92}108}
93 109
94impl DebugAdapterState {110impl DebugAdapterState {
95 fn new() -> Self {111 fn new(id: SessionId, adapter_name: DebugAdapterName, has_adapter_logs: bool) -> Self {
96 Self {112 Self {
113 id,
97 log_messages: VecDeque::new(),114 log_messages: VecDeque::new(),
98 rpc_messages: RpcMessages::new(),115 rpc_messages: RpcMessages::new(),
116 adapter_name,
117 has_adapter_logs,
118 is_terminated: false,
99 }119 }
100 }120 }
101}121}
102 122
103impl LogStore {123impl LogStore {
104 pub fn new(cx: &Context<Self>) -> Self {124 pub fn new(cx: &Context<Self>) -> Self {
105 let (rpc_tx, mut rpc_rx) = unbounded::<(SessionId, IoKind, String)>();125 let (rpc_tx, mut rpc_rx) =
126 unbounded::<(SessionId, IoKind, Option<SharedString>, SharedString)>();
106 cx.spawn(async move |this, cx| {127 cx.spawn(async move |this, cx| {
107 while let Some((client_id, io_kind, message)) = rpc_rx.next().await {128 while let Some((session_id, io_kind, command, message)) = rpc_rx.next().await {
108 if let Some(this) = this.upgrade() {129 if let Some(this) = this.upgrade() {
109 this.update(cx, |this, cx| {130 this.update(cx, |this, cx| {
110 this.on_rpc_log(client_id, io_kind, &message, cx);131 this.add_debug_adapter_message(session_id, io_kind, command, message, cx);
111 })?;132 })?;
112 }133 }
113 134
@@ -117,12 +138,13 @@ impl LogStore { · 3 unmodified lines
117 })138 })
118 .detach_and_log_err(cx);139 .detach_and_log_err(cx);
119 140
120 let (adapter_log_tx, mut adapter_log_rx) = unbounded::<(SessionId, IoKind, String)>();141 let (adapter_log_tx, mut adapter_log_rx) =
142 unbounded::<(SessionId, IoKind, Option<SharedString>, SharedString)>();
121 cx.spawn(async move |this, cx| {143 cx.spawn(async move |this, cx| {
122 while let Some((client_id, io_kind, message)) = adapter_log_rx.next().await {144 while let Some((session_id, io_kind, _, message)) = adapter_log_rx.next().await {
123 if let Some(this) = this.upgrade() {145 if let Some(this) = this.upgrade() {
124 this.update(cx, |this, cx| {146 this.update(cx, |this, cx| {
125 this.on_adapter_log(client_id, io_kind, &message, cx);147 this.add_debug_adapter_log(session_id, io_kind, message, cx);
126 })?;148 })?;
127 }149 }
128 150
@@ -135,30 +157,10 @@ impl LogStore { · 6 unmodified lines
135 rpc_tx,157 rpc_tx,
136 adapter_log_tx,158 adapter_log_tx,
137 projects: HashMap::new(),159 projects: HashMap::new(),
138 debug_clients: HashMap::new(),160 debug_sessions: Default::default(),
139 }161 }
140 }162 }
141 163
142 fn on_rpc_log(
143 &mut self,
144 client_id: SessionId,
145 io_kind: IoKind,
146 message: &str,
147 cx: &mut Context<Self>,
148 ) {
149 self.add_debug_client_message(client_id, io_kind, message.to_string(), cx);
150 }
151
152 fn on_adapter_log(
153 &mut self,
154 client_id: SessionId,
155 io_kind: IoKind,
156 message: &str,
157 cx: &mut Context<Self>,
158 ) {
159 self.add_debug_client_log(client_id, io_kind, message.to_string(), cx);
160 }
161
162 pub fn add_project(&mut self, project: &Entity<Project>, cx: &mut Context<Self>) {164 pub fn add_project(&mut self, project: &Entity<Project>, cx: &mut Context<Self>) {
163 let weak_project = project.downgrade();165 let weak_project = project.downgrade();
164 self.projects.insert(166 self.projects.insert(
@@ -174,13 +176,15 @@ impl LogStore { · 9 unmodified lines
174 dap_store::DapStoreEvent::DebugClientStarted(session_id) => {176 dap_store::DapStoreEvent::DebugClientStarted(session_id) => {
175 let session = dap_store.read(cx).session_by_id(session_id);177 let session = dap_store.read(cx).session_by_id(session_id);
176 if let Some(session) = session {178 if let Some(session) = session {
177 this.add_debug_client(*session_id, session, cx);179 this.add_debug_session(*session_id, session, cx);
178 }180 }
179 }181 }
180 dap_store::DapStoreEvent::DebugClientShutdown(session_id) => {182 dap_store::DapStoreEvent::DebugClientShutdown(session_id) => {
181 this.remove_debug_client(*session_id, cx);183 this.get_debug_adapter_state(*session_id)
184 .iter_mut()
185 .for_each(|state| state.is_terminated = true);
186 this.clean_sessions(cx);
182 }187 }
183
184 _ => {}188 _ => {}
185 },189 },
186 ),190 ),
@@ -190,63 +194,88 @@ impl LogStore { · 3 unmodified lines
190 }194 }
191 195
192 fn get_debug_adapter_state(&mut self, id: SessionId) -> Option<&mut DebugAdapterState> {196 fn get_debug_adapter_state(&mut self, id: SessionId) -> Option<&mut DebugAdapterState> {
193 self.debug_clients.get_mut(&id)197 self.debug_sessions
198 .iter_mut()
199 .find(|adapter_state| adapter_state.id == id)
194 }200 }
195 201
196 fn add_debug_client_message(202 fn add_debug_adapter_message(
197 &mut self,203 &mut self,
198 id: SessionId,204 id: SessionId,
199 io_kind: IoKind,205 io_kind: IoKind,
200 message: String,206 command: Option<SharedString>,
207 message: SharedString,
201 cx: &mut Context<Self>,208 cx: &mut Context<Self>,
202 ) {209 ) {
203 let Some(debug_client_state) = self.get_debug_adapter_state(id) else {210 let Some(debug_client_state) = self.get_debug_adapter_state(id) else {
204 return;211 return;
205 };212 };
206 213
214 let is_init_seq = command.as_ref().is_some_and(|command| {
215 matches!(
216 command.as_ref(),
217 "attach" | "launch" | "initialize" | "configurationDone"
218 )
219 });
220
207 let kind = match io_kind {221 let kind = match io_kind {
208 IoKind::StdOut | IoKind::StdErr => MessageKind::Receive,222 IoKind::StdOut | IoKind::StdErr => MessageKind::Receive,
209 IoKind::StdIn => MessageKind::Send,223 IoKind::StdIn => MessageKind::Send,
210 };224 };
211 225
212 let rpc_messages = &mut debug_client_state.rpc_messages;226 let rpc_messages = &mut debug_client_state.rpc_messages;
227
228 // Push a separator if the kind has changed
213 if rpc_messages.last_message_kind != Some(kind) {229 if rpc_messages.last_message_kind != Some(kind) {
214 Self::add_debug_client_entry(230 Self::get_debug_adapter_entry(
215 &mut rpc_messages.messages,231 &mut rpc_messages.messages,
216 id,232 id,
217 kind.label().to_string(),233 kind.label().into(),
218 LogKind::Rpc,234 LogKind::Rpc,
219 cx,235 cx,
220 );236 );
221 rpc_messages.last_message_kind = Some(kind);237 rpc_messages.last_message_kind = Some(kind);
222 }238 }
223 Self::add_debug_client_entry(&mut rpc_messages.messages, id, message, LogKind::Rpc, cx);239
240 let entry = Self::get_debug_adapter_entry(
241 &mut rpc_messages.messages,
242 id,
243 message,
244 LogKind::Rpc,
245 cx,
246 );
247
248 if is_init_seq {
249 if rpc_messages.last_init_message_kind != Some(kind) {
250 rpc_messages
251 .initialization_sequence
252 .push(SharedString::from(kind.label()));
253 rpc_messages.last_init_message_kind = Some(kind);
254 }
255 rpc_messages.initialization_sequence.push(entry);
256 }
224 257
225 cx.notify();258 cx.notify();
226 }259 }
227 260
228 fn add_debug_client_log(261 fn add_debug_adapter_log(
229 &mut self,262 &mut self,
230 id: SessionId,263 id: SessionId,
231 io_kind: IoKind,264 io_kind: IoKind,
232 message: String,265 message: SharedString,
233 cx: &mut Context<Self>,266 cx: &mut Context<Self>,
234 ) {267 ) {
235 let Some(debug_client_state) = self.get_debug_adapter_state(id) else {268 let Some(debug_adapter_state) = self.get_debug_adapter_state(id) else {
236 return;269 return;
237 };270 };
238 271
239 let message = match io_kind {272 let message = match io_kind {
240 IoKind::StdErr => {273 IoKind::StdErr => format!("stderr: {message}").into(),
241 let mut message = message.clone();
242 message.insert_str(0, "stderr: ");
243 message
244 }
245 _ => message,274 _ => message,
246 };275 };
247 276
248 Self::add_debug_client_entry(277 Self::get_debug_adapter_entry(
249 &mut debug_client_state.log_messages,278 &mut debug_adapter_state.log_messages,
250 id,279 id,
251 message,280 message,
252 LogKind::Adapter,281 LogKind::Adapter,
@@ -255,13 +284,13 @@ impl LogStore { · 2 unmodified lines
255 cx.notify();284 cx.notify();
256 }285 }
257 286
258 fn add_debug_client_entry(287 fn get_debug_adapter_entry(
259 log_lines: &mut VecDeque<String>,288 log_lines: &mut VecDeque<SharedString>,
260 id: SessionId,289 id: SessionId,
261 message: String,290 message: SharedString,
262 kind: LogKind,291 kind: LogKind,
263 cx: &mut Context<Self>,292 cx: &mut Context<Self>,
264 ) {293 ) -> SharedString {
265 while log_lines.len() >= RpcMessages::MESSAGE_QUEUE_LIMIT {294 while log_lines.len() >= RpcMessages::MESSAGE_QUEUE_LIMIT {
266 log_lines.pop_front();295 log_lines.pop_front();
267 }296 }
@@ -275,33 +304,69 @@ impl LogStore { · 7 unmodified lines
275 )304 )
276 .ok()305 .ok()
277 })306 })
307 .map(SharedString::from)
278 .unwrap_or(message)308 .unwrap_or(message)
279 } else {309 } else {
280 message310 message
281 };311 };
282 log_lines.push_back(entry.clone());312 log_lines.push_back(entry.clone());
283 313
284 cx.emit(Event::NewLogEntry { id, entry, kind });314 cx.emit(Event::NewLogEntry {
315 id,
316 entry: entry.clone(),
317 kind,
318 });
319
320 entry
285 }321 }
286 322
287 fn add_debug_client(323 fn add_debug_session(
288 &mut self,324 &mut self,
289 client_id: SessionId,325 session_id: SessionId,
290 client: Entity<Session>,326 session: Entity<Session>,
291 cx: &App,327 cx: &mut Context<Self>,
292 ) -> Option<&mut DebugAdapterState> {328 ) {
293 let client_state = self329 if self
294 .debug_clients330 .debug_sessions
295 .entry(client_id)331 .iter_mut()
296 .or_insert_with(DebugAdapterState::new);332 .any(|adapter_state| adapter_state.id == session_id)
333 {
334 return;
335 }
336
337 let (adapter_name, has_adapter_logs) = session.read_with(cx, |session, _| {
338 (
339 session.adapter(),
340 session
341 .adapter_client()
342 .map(|client| client.has_adapter_logs())
343 .unwrap_or(false),
344 )
345 });
346
347 self.debug_sessions.push_back(DebugAdapterState::new(
348 session_id,
349 adapter_name,
350 has_adapter_logs,
351 ));
352
353 self.clean_sessions(cx);
297 354
298 let io_tx = self.rpc_tx.clone();355 let io_tx = self.rpc_tx.clone();
299 356
300 let client = client.read(cx).adapter_client()?;357 let Some(client) = session.read(cx).adapter_client() else {
358 return;
359 };
360
301 client.add_log_handler(361 client.add_log_handler(
302 move |io_kind, message| {362 move |io_kind, command, message| {
303 io_tx363 io_tx
304 .unbounded_send((client_id, io_kind, message.to_string()))364 .unbounded_send((
365 session_id,
366 io_kind,
367 command.map(|command| command.to_owned().into()),
368 message.to_owned().into(),
369 ))
305 .ok();370 .ok();
306 },371 },
307 LogKind::Rpc,372 LogKind::Rpc,
@@ -309,34 +374,66 @@ impl LogStore { · 1 unmodified line
309 374
310 let log_io_tx = self.adapter_log_tx.clone();375 let log_io_tx = self.adapter_log_tx.clone();
311 client.add_log_handler(376 client.add_log_handler(
312 move |io_kind, message| {377 move |io_kind, command, message| {
313 log_io_tx378 log_io_tx
314 .unbounded_send((client_id, io_kind, message.to_string()))379 .unbounded_send((
380 session_id,
381 io_kind,
382 command.map(|command| command.to_owned().into()),
383 message.to_owned().into(),
384 ))
315 .ok();385 .ok();
316 },386 },
317 LogKind::Adapter,387 LogKind::Adapter,
318 );388 );
319
320 Some(client_state)
321 }389 }
322 390
323 fn remove_debug_client(&mut self, client_id: SessionId, cx: &mut Context<Self>) {391 fn clean_sessions(&mut self, cx: &mut Context<Self>) {
324 self.debug_clients.remove(&client_id);392 let mut to_remove = self.debug_sessions.len().saturating_sub(MAX_SESSIONS);
393 self.debug_sessions.retain(|session| {
394 if to_remove > 0 && session.is_terminated {
395 to_remove -= 1;
396 return false;
397 }
398 true
399 });
325 cx.notify();400 cx.notify();
326 }401 }
327 402
328 fn log_messages_for_client(&mut self, client_id: SessionId) -> Option<&mut VecDeque<String>> {403 fn log_messages_for_session(
329 Some(&mut self.debug_clients.get_mut(&client_id)?.log_messages)404 &mut self,
405 session_id: SessionId,
406 ) -> Option<&mut VecDeque<SharedString>> {
407 self.debug_sessions
408 .iter_mut()
409 .find(|session| session.id == session_id)
410 .map(|state| &mut state.log_messages)
411 }
412
413 fn rpc_messages_for_session(
414 &mut self,
415 session_id: SessionId,
416 ) -> Option<&mut VecDeque<SharedString>> {
417 self.debug_sessions.iter_mut().find_map(|state| {
418 if state.id == session_id {
419 Some(&mut state.rpc_messages.messages)
420 } else {
421 None
422 }
423 })
330 }424 }
331 425
332 fn rpc_messages_for_client(&mut self, client_id: SessionId) -> Option<&mut VecDeque<String>> {426 fn initialization_sequence_for_session(
333 Some(427 &mut self,
334 &mut self428 session_id: SessionId,
335 .debug_clients429 ) -> Option<&mut Vec<SharedString>> {
336 .get_mut(&client_id)?430 self.debug_sessions.iter_mut().find_map(|state| {
337 .rpc_messages431 if state.id == session_id {
338 .messages,432 Some(&mut state.rpc_messages.initialization_sequence)
339 )433 } else {
434 None
435 }
436 })
340 }437 }
341}438}
342 439
@@ -356,18 +453,15 @@ impl Render for DapLogToolbarItemView { · 13 unmodified lines
356 return Empty.into_any_element();453 return Empty.into_any_element();
357 };454 };
358 455
359 let (menu_rows, current_client_id) = log_view.update(cx, |log_view, cx| {456 let (menu_rows, current_session_id) = log_view.update(cx, |log_view, cx| {
360 (457 (
361 log_view.menu_items(cx).unwrap_or_default(),458 log_view.menu_items(cx),
362 log_view.current_view.map(|(client_id, _)| client_id),459 log_view.current_view.map(|(session_id, _)| session_id),
363 )460 )
364 });461 });
365 462
366 let current_client = current_client_id.and_then(|current_client_id| {463 let current_client = current_session_id
367 menu_rows464 .and_then(|session_id| menu_rows.iter().find(|row| row.session_id == session_id));
368 .iter()
369 .find(|row| row.client_id == current_client_id)
370 });
371 465
372 let dap_menu: PopoverMenu<_> = PopoverMenu::new("DapLogView")466 let dap_menu: PopoverMenu<_> = PopoverMenu::new("DapLogView")
373 .anchor(gpui::Corner::TopLeft)467 .anchor(gpui::Corner::TopLeft)
@@ -377,8 +471,8 @@ impl Render for DapLogToolbarItemView { · 3 unmodified lines
377 .map(|sub_item| {471 .map(|sub_item| {
378 Cow::Owned(format!(472 Cow::Owned(format!(
379 "{} ({}) - {}",473 "{} ({}) - {}",
380 sub_item.client_name,474 sub_item.adapter_name,
381 sub_item.client_id.0,475 sub_item.session_id.0,
382 match sub_item.selected_entry {476 match sub_item.selected_entry {
383 LogKind::Adapter => ADAPTER_LOGS,477 LogKind::Adapter => ADAPTER_LOGS,
384 LogKind::Rpc => RPC_MESSAGES,478 LogKind::Rpc => RPC_MESSAGES,
@@ -397,9 +491,10 @@ impl Render for DapLogToolbarItemView { · 12 unmodified lines
397 .w_full()491 .w_full()
398 .pl_2()492 .pl_2()
399 .child(493 .child(
400 Label::new(494 Label::new(format!(
401 format!("{}. {}", row.client_id.0, row.client_name,),495 "{}. {}",
402 )496 row.session_id.0, row.adapter_name,
497 ))
403 .color(workspace::ui::Color::Muted),498 .color(workspace::ui::Color::Muted),
404 )499 )
405 .into_any_element()500 .into_any_element()
@@ -415,23 +510,40 @@ impl Render for DapLogToolbarItemView { · 9 unmodified lines
415 .into_any_element()510 .into_any_element()
416 },511 },
417 window.handler_for(&log_view, move |view, window, cx| {512 window.handler_for(&log_view, move |view, window, cx| {
418 view.show_log_messages_for_adapter(row.client_id, window, cx);513 view.show_log_messages_for_adapter(row.session_id, window, cx);
419 }),514 }),
420 );515 );
421 }516 }
422 517
423 menu = menu.custom_entry(518 menu = menu
424 move |_window, _cx| {519 .custom_entry(
425 div()520 move |_window, _cx| {
426 .w_full()521 div()
427 .pl_4()522 .w_full()
428 .child(Label::new(RPC_MESSAGES))523 .pl_4()
429 .into_any_element()524 .child(Label::new(RPC_MESSAGES))
430 },525 .into_any_element()
431 window.handler_for(&log_view, move |view, window, cx| {526 },
432 view.show_rpc_trace_for_server(row.client_id, window, cx);527 window.handler_for(&log_view, move |view, window, cx| {
433 }),528 view.show_rpc_trace_for_server(row.session_id, window, cx);
434 );529 }),
530 )
531 .custom_entry(
532 move |_window, _cx| {
533 div()
534 .w_full()
535 .pl_4()
536 .child(Label::new(INITIALIZATION_SEQUENCE))
537 .into_any_element()
538 },
539 window.handler_for(&log_view, move |view, window, cx| {
540 view.show_initialization_sequence_for_server(
541 row.session_id,
542 window,
543 cx,
544 );
545 }),
546 );
435 }547 }
436 548
437 menu549 menu
@@ -518,7 +630,13 @@ impl DapLogView { · 80 unmodified lines
518 }630 }
519 });631 });
520 632
521 Self {633 let state_info = log_store
634 .read(cx)
635 .debug_sessions
636 .back()
637 .map(|session| (session.id, session.has_adapter_logs));
638
639 let mut this = Self {
522 editor,640 editor,
523 focus_handle,641 focus_handle,
524 project,642 project,
@@ -526,7 +644,17 @@ impl DapLogView { · 1 unmodified line
526 editor_subscriptions,644 editor_subscriptions,
527 current_view: None,645 current_view: None,
528 _subscriptions: vec![events_subscriptions],646 _subscriptions: vec![events_subscriptions],
647 };
648
649 if let Some((session_id, have_adapter_logs)) = state_info {
650 if have_adapter_logs {
651 this.show_log_messages_for_adapter(session_id, window, cx);
652 } else {
653 this.show_rpc_trace_for_server(session_id, window, cx);
654 }
529 }655 }
656
657 this
530 }658 }
531 659
532 fn editor_for_logs(660 fn editor_for_logs(
@@ -559,42 +687,34 @@ impl DapLogView { · 26 unmodified lines
559 (editor, vec![editor_subscription, search_subscription])687 (editor, vec![editor_subscription, search_subscription])
560 }688 }
561 689
562 fn menu_items(&self, cx: &App) -> Option<Vec<DapMenuItem>> {690 fn menu_items(&self, cx: &App) -> Vec<DapMenuItem> {
563 let mut menu_items = self691 self.log_store
564 .project
565 .read(cx)692 .read(cx)
566 .dap_store()693 .debug_sessions
567 .read(cx)694 .iter()
568 .sessions()695 .rev()
569 .filter_map(|session| {696 .map(|state| DapMenuItem {
570 let session = session.read(cx);697 session_id: state.id,
571 session.adapter();698 adapter_name: state.adapter_name.clone(),
572 let client = session.adapter_client()?;699 has_adapter_logs: state.has_adapter_logs,
573 Some(DapMenuItem {700 selected_entry: self.current_view.map_or(LogKind::Adapter, |(_, kind)| kind),
574 client_id: client.id(),
575 client_name: session.adapter().to_string(),
576 has_adapter_logs: client.has_adapter_logs(),
577 selected_entry: self.current_view.map_or(LogKind::Adapter, |(_, kind)| kind),
578 })
579 })701 })
580 .collect::<Vec<_>>();702 .collect::<Vec<_>>()
581 menu_items.sort_by_key(|item| item.client_id.0);
582 Some(menu_items)
583 }703 }
584 704
585 fn show_rpc_trace_for_server(705 fn show_rpc_trace_for_server(
586 &mut self,706 &mut self,
587 client_id: SessionId,707 session_id: SessionId,
588 window: &mut Window,708 window: &mut Window,
589 cx: &mut Context<Self>,709 cx: &mut Context<Self>,
590 ) {710 ) {
591 let rpc_log = self.log_store.update(cx, |log_store, _| {711 let rpc_log = self.log_store.update(cx, |log_store, _| {
592 log_store712 log_store
593 .rpc_messages_for_client(client_id)713 .rpc_messages_for_session(session_id)
594 .map(|state| log_contents(&state))714 .map(|state| log_contents(state.iter().cloned()))
595 });715 });
596 if let Some(rpc_log) = rpc_log {716 if let Some(rpc_log) = rpc_log {
597 self.current_view = Some((client_id, LogKind::Rpc));717 self.current_view = Some((session_id, LogKind::Rpc));
598 let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, window, cx);718 let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, window, cx);
599 let language = self.project.read(cx).languages().language_for_name("JSON");719 let language = self.project.read(cx).languages().language_for_name("JSON");
600 editor720 editor
@@ -626,17 +746,17 @@ impl DapLogView { · 25 unmodified lines
626 746
627 fn show_log_messages_for_adapter(747 fn show_log_messages_for_adapter(
628 &mut self,748 &mut self,
629 client_id: SessionId,749 session_id: SessionId,
630 window: &mut Window,750 window: &mut Window,
631 cx: &mut Context<Self>,751 cx: &mut Context<Self>,
632 ) {752 ) {
633 let message_log = self.log_store.update(cx, |log_store, _| {753 let message_log = self.log_store.update(cx, |log_store, _| {
634 log_store754 log_store
635 .log_messages_for_client(client_id)755 .log_messages_for_session(session_id)
636 .map(|state| log_contents(&state))756 .map(|state| log_contents(state.iter().cloned()))
637 });757 });
638 if let Some(message_log) = message_log {758 if let Some(message_log) = message_log {
639 self.current_view = Some((client_id, LogKind::Adapter));759 self.current_view = Some((session_id, LogKind::Adapter));
640 let (editor, editor_subscriptions) = Self::editor_for_logs(message_log, window, cx);760 let (editor, editor_subscriptions) = Self::editor_for_logs(message_log, window, cx);
641 editor761 editor
642 .read(cx)762 .read(cx)
@@ -652,14 +772,53 @@ impl DapLogView { · 9 unmodified lines
652 772
653 cx.focus_self(window);773 cx.focus_self(window);
654 }774 }
775
776 fn show_initialization_sequence_for_server(
777 &mut self,
778 session_id: SessionId,
779 window: &mut Window,
780 cx: &mut Context<Self>,
781 ) {
782 let rpc_log = self.log_store.update(cx, |log_store, _| {
783 log_store
784 .initialization_sequence_for_session(session_id)
785 .map(|state| log_contents(state.iter().cloned()))
786 });
787 if let Some(rpc_log) = rpc_log {
788 self.current_view = Some((session_id, LogKind::Rpc));
789 let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, window, cx);
790 let language = self.project.read(cx).languages().language_for_name("JSON");
791 editor
792 .read(cx)
793 .buffer()
794 .read(cx)
795 .as_singleton()
796 .expect("log buffer should be a singleton")
797 .update(cx, |_, cx| {
798 cx.spawn({
799 let buffer = cx.entity();
800 async move |_, cx| {
801 let language = language.await.ok();
802 buffer.update(cx, |buffer, cx| {
803 buffer.set_language(language, cx);
804 })
805 }
806 })
807 .detach_and_log_err(cx);
808 });
809
810 self.editor = editor;
811 self.editor_subscriptions = editor_subscriptions;
812 cx.notify();
813 }
814
815 cx.focus_self(window);
816 }
655}817}
656 818
657fn log_contents(lines: &VecDeque<String>) -> String {819fn log_contents(lines: impl Iterator<Item = SharedString>) -> String {
658 let (a, b) = lines.as_slices();820 lines.fold(String::new(), |mut acc, el| {
659 let a = a.iter().map(move |v| v.as_ref());821 acc.push_str(&el);
660 let b = b.iter().map(move |v| v.as_ref());
661 a.chain(b).fold(String::new(), |mut acc, el| {
662 acc.push_str(el);
663 acc.push('\n');822 acc.push('\n');
664 acc823 acc
665 })824 })
@@ -667,14 +826,15 @@ fn log_contents(lines: &VecDeque<String>) -> String { · 1 unmodified line
667 826
668#[derive(Clone, PartialEq)]827#[derive(Clone, PartialEq)]
669pub(crate) struct DapMenuItem {828pub(crate) struct DapMenuItem {
670 pub client_id: SessionId,829 pub session_id: SessionId,
671 pub client_name: String,830 pub adapter_name: DebugAdapterName,
672 pub has_adapter_logs: bool,831 pub has_adapter_logs: bool,
673 pub selected_entry: LogKind,832 pub selected_entry: LogKind,
674}833}
675 834
676const ADAPTER_LOGS: &str = "Adapter Logs";835const ADAPTER_LOGS: &str = "Adapter Logs";
677const RPC_MESSAGES: &str = "RPC Messages";836const RPC_MESSAGES: &str = "RPC Messages";
837const INITIALIZATION_SEQUENCE: &str = "Initialization Sequence";
678 838
679impl Render for DapLogView {839impl Render for DapLogView {
680 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {840 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
@@ -836,7 +996,7 @@ impl Focusable for DapLogView { · 155 unmodified lines
836pub enum Event {996pub enum Event {
837 NewLogEntry {997 NewLogEntry {
838 id: SessionId,998 id: SessionId,
839 entry: String,999 entry: SharedString,
840 kind: LogKind,1000 kind: LogKind,
841 },1001 },
842}1002}
@@ -849,12 +1009,16 @@ impl EventEmitter<SearchEvent> for DapLogView {} · 6 unmodified lines
849#[cfg(any(test, feature = "test-support"))]1009#[cfg(any(test, feature = "test-support"))]
850impl LogStore {1010impl LogStore {
851 pub fn contained_session_ids(&self) -> Vec<SessionId> {1011 pub fn contained_session_ids(&self) -> Vec<SessionId> {
852 self.debug_clients.keys().cloned().collect()1012 self.debug_sessions
1013 .iter()
1014 .map(|session| session.id)
1015 .collect()
853 }1016 }
854 1017
855 pub fn rpc_messages_for_session_id(&self, session_id: SessionId) -> Vec<String> {1018 pub fn rpc_messages_for_session_id(&self, session_id: SessionId) -> Vec<SharedString> {
856 self.debug_clients1019 self.debug_sessions
857 .get(&session_id)1020 .iter()
1021 .find(|adapter_state| adapter_state.id == session_id)
858 .expect("This session should exist if a test is calling")1022 .expect("This session should exist if a test is calling")
859 .rpc_messages1023 .rpc_messages
860 .messages1024 .messages
@@ -862,9 +1026,10 @@ impl LogStore { · 1 unmodified line
862 .into()1026 .into()
863 }1027 }
864 1028
865 pub fn log_messages_for_session_id(&self, session_id: SessionId) -> Vec<String> {1029 pub fn log_messages_for_session_id(&self, session_id: SessionId) -> Vec<SharedString> {
866 self.debug_clients1030 self.debug_sessions
867 .get(&session_id)1031 .iter()
1032 .find(|adapter_state| adapter_state.id == session_id)
868 .expect("This session should exist if a test is calling")1033 .expect("This session should exist if a test is calling")
869 .log_messages1034 .log_messages
870 .clone()1035 .clone()
crates/debugger_ui/src/debugger_panel.rs+149−132
@@ -2,25 +2,23 @@ use crate::persistence::DebuggerPaneItem; · 1 unmodified line
2use crate::session::DebugSession;2use crate::session::DebugSession;
3use crate::session::running::RunningState;3use crate::session::running::RunningState;
4use crate::{4use crate::{
5 ClearAllBreakpoints, Continue, Detach, FocusBreakpointList, FocusConsole, FocusFrames,5 ClearAllBreakpoints, Continue, CopyDebugAdapterArguments, Detach, FocusBreakpointList,
6 FocusLoadedSources, FocusModules, FocusTerminal, FocusVariables, NewProcessModal,6 FocusConsole, FocusFrames, FocusLoadedSources, FocusModules, FocusTerminal, FocusVariables,
7 NewProcessMode, Pause, Restart, ShowStackTrace, StepBack, StepInto, StepOut, StepOver, Stop,7 NewProcessModal, NewProcessMode, Pause, Restart, StepInto, StepOut, StepOver, Stop,
8 ToggleExpandItem, ToggleIgnoreBreakpoints, ToggleSessionPicker, ToggleThreadPicker,8 ToggleExpandItem, ToggleSessionPicker, ToggleThreadPicker, persistence, spawn_task_or_modal,
9 persistence, spawn_task_or_modal,
10};9};
11use anyhow::Result;10use anyhow::Result;
12use command_palette_hooks::CommandPaletteFilter;
13use dap::StartDebuggingRequestArguments;
14use dap::adapters::DebugAdapterName;11use dap::adapters::DebugAdapterName;
15use dap::debugger_settings::DebugPanelDockPosition;12use dap::debugger_settings::DebugPanelDockPosition;
16use dap::{13use dap::{
17 ContinuedEvent, LoadedSourceEvent, ModuleEvent, OutputEvent, StoppedEvent, ThreadEvent,14 ContinuedEvent, LoadedSourceEvent, ModuleEvent, OutputEvent, StoppedEvent, ThreadEvent,
18 client::SessionId, debugger_settings::DebuggerSettings,15 client::SessionId, debugger_settings::DebuggerSettings,
19};16};
17use dap::{DapRegistry, StartDebuggingRequestArguments};
20use gpui::{18use gpui::{
21 Action, App, AsyncWindowContext, Context, DismissEvent, Entity, EntityId, EventEmitter,19 Action, App, AsyncWindowContext, ClipboardItem, Context, DismissEvent, Entity, EntityId,
22 FocusHandle, Focusable, MouseButton, MouseDownEvent, Point, Subscription, Task, WeakEntity,20 EventEmitter, FocusHandle, Focusable, MouseButton, MouseDownEvent, Point, Subscription, Task,
23 actions, anchored, deferred,21 WeakEntity, actions, anchored, deferred,
24};22};
25 23
26use language::Buffer;24use language::Buffer;
@@ -29,10 +27,10 @@ use project::{Fs, WorktreeId}; · 2 unmodified lines
29use project::{Project, debugger::session::ThreadStatus};27use project::{Project, debugger::session::ThreadStatus};
30use rpc::proto::{self};28use rpc::proto::{self};
31use settings::Settings;29use settings::Settings;
32use std::any::TypeId;
33use std::sync::Arc;30use std::sync::Arc;
34use task::{DebugScenario, TaskContext};31use task::{DebugScenario, TaskContext};
35use ui::{ContextMenu, Divider, PopoverMenuHandle, Tooltip, prelude::*};32use ui::{ContextMenu, Divider, PopoverMenuHandle, Tooltip, prelude::*};
33use util::maybe;
36use workspace::SplitDirection;34use workspace::SplitDirection;
37use workspace::{35use workspace::{
38 Pane, Workspace,36 Pane, Workspace,
@@ -140,82 +138,6 @@ impl DebugPanel { · 101 unmodified lines
140 .map(|session| session.read(cx).running_state().clone())138 .map(|session| session.read(cx).running_state().clone())
141 }139 }
142 140
143 pub(crate) fn filter_action_types(&self, cx: &mut App) {
144 let (has_active_session, supports_restart, support_step_back, status) = self
145 .active_session()
146 .map(|item| {
147 let running = item.read(cx).running_state().clone();
148 let caps = running.read(cx).capabilities(cx);
149 (
150 !running.read(cx).session().read(cx).is_terminated(),
151 caps.supports_restart_request.unwrap_or_default(),
152 caps.supports_step_back.unwrap_or_default(),
153 running.read(cx).thread_status(cx),
154 )
155 })
156 .unwrap_or((false, false, false, None));
157
158 let filter = CommandPaletteFilter::global_mut(cx);
159 let debugger_action_types = [
160 TypeId::of::<Detach>(),
161 TypeId::of::<Stop>(),
162 TypeId::of::<ToggleIgnoreBreakpoints>(),
163 ];
164
165 let running_action_types = [TypeId::of::<Pause>()];
166
167 let stopped_action_type = [
168 TypeId::of::<Continue>(),
169 TypeId::of::<StepOver>(),
170 TypeId::of::<StepInto>(),
171 TypeId::of::<StepOut>(),
172 TypeId::of::<ShowStackTrace>(),
173 TypeId::of::<editor::actions::DebuggerRunToCursor>(),
174 TypeId::of::<editor::actions::DebuggerEvaluateSelectedText>(),
175 ];
176
177 let step_back_action_type = [TypeId::of::<StepBack>()];
178 let restart_action_type = [TypeId::of::<Restart>()];
179
180 if has_active_session {
181 filter.show_action_types(debugger_action_types.iter());
182
183 if supports_restart {
184 filter.show_action_types(restart_action_type.iter());
185 } else {
186 filter.hide_action_types(&restart_action_type);
187 }
188
189 if support_step_back {
190 filter.show_action_types(step_back_action_type.iter());
191 } else {
192 filter.hide_action_types(&step_back_action_type);
193 }
194
195 match status {
196 Some(ThreadStatus::Running) => {
197 filter.show_action_types(running_action_types.iter());
198 filter.hide_action_types(&stopped_action_type);
199 }
200 Some(ThreadStatus::Stopped) => {
201 filter.show_action_types(stopped_action_type.iter());
202 filter.hide_action_types(&running_action_types);
203 }
204 _ => {
205 filter.hide_action_types(&running_action_types);
206 filter.hide_action_types(&stopped_action_type);
207 }
208 }
209 } else {
210 // show only the `debug: start`
211 filter.hide_action_types(&debugger_action_types);
212 filter.hide_action_types(&step_back_action_type);
213 filter.hide_action_types(&restart_action_type);
214 filter.hide_action_types(&running_action_types);
215 filter.hide_action_types(&stopped_action_type);
216 }
217 }
218
219 pub fn load(141 pub fn load(
220 workspace: WeakEntity<Workspace>,142 workspace: WeakEntity<Workspace>,
221 cx: &mut AsyncWindowContext,143 cx: &mut AsyncWindowContext,
@@ -233,17 +155,6 @@ impl DebugPanel { · 11 unmodified lines
233 )155 )
234 });156 });
235 157
236 cx.observe_new::<DebugPanel>(|debug_panel, _, cx| {
237 Self::filter_action_types(debug_panel, cx);
238 })
239 .detach();
240
241 cx.observe(&debug_panel, |_, debug_panel, cx| {
242 debug_panel.update(cx, |debug_panel, cx| {
243 Self::filter_action_types(debug_panel, cx);
244 });
245 })
246 .detach();
247 workspace.set_debugger_provider(DebuggerProvider(debug_panel.clone()));158 workspace.set_debugger_provider(DebuggerProvider(debug_panel.clone()));
248 159
249 debug_panel160 debug_panel
@@ -269,6 +180,19 @@ impl DebugPanel { · 19 unmodified lines
269 cx,180 cx,
270 )181 )
271 });182 });
183 let worktree = worktree_id.or_else(|| {
184 active_buffer
185 .as_ref()
186 .and_then(|buffer| buffer.read(cx).file())
187 .map(|f| f.worktree_id(cx))
188 });
189 let Some(worktree) = worktree
190 .and_then(|id| self.project.read(cx).worktree_for_id(id, cx))
191 .or_else(|| self.project.read(cx).visible_worktrees(cx).next())
192 else {
193 log::debug!("Could not find a worktree to spawn the debug session in");
194 return;
195 };
272 self.debug_scenario_scheduled_last = true;196 self.debug_scenario_scheduled_last = true;
273 if let Some(inventory) = self197 if let Some(inventory) = self
274 .project198 .project
@@ -303,7 +227,7 @@ impl DebugPanel { · 28 unmodified lines
303 .await?;227 .await?;
304 dap_store228 dap_store
305 .update(cx, |dap_store, cx| {229 .update(cx, |dap_store, cx| {
306 dap_store.boot_session(session.clone(), definition, cx)230 dap_store.boot_session(session.clone(), definition, worktree, cx)
307 })?231 })?
308 .await232 .await
309 }233 }
@@ -412,7 +336,7 @@ impl DebugPanel { · 102 unmodified lines
412 let dap_store_handle = self.project.read(cx).dap_store().clone();336 let dap_store_handle = self.project.read(cx).dap_store().clone();
413 let label = curr_session.read(cx).label().clone();337 let label = curr_session.read(cx).label().clone();
414 let adapter = curr_session.read(cx).adapter().clone();338 let adapter = curr_session.read(cx).adapter().clone();
415 let binary = curr_session.read(cx).binary().clone();339 let binary = curr_session.read(cx).binary().cloned().unwrap();
416 let task = curr_session.update(cx, |session, cx| session.shutdown(cx));340 let task = curr_session.update(cx, |session, cx| session.shutdown(cx));
417 341
418 cx.spawn_in(window, async move |this, cx| {342 cx.spawn_in(window, async move |this, cx| {
@@ -426,8 +350,26 @@ impl DebugPanel { · 7 unmodified lines
426 });350 });
427 (session, task)351 (session, task)
428 })?;352 })?;
429 Self::register_session(this.clone(), session, true, cx).await?;353 Self::register_session(this.clone(), session.clone(), true, cx).await?;
430 task.await354
355 if let Err(error) = task.await {
356 session
357 .update(cx, |session, cx| {
358 session
359 .console_output(cx)
360 .unbounded_send(format!(
361 "Session failed to restart with error: {}",
362 error
363 ))
364 .ok();
365 session.shutdown(cx)
366 })?
367 .await;
368
369 return Err(error);
370 };
371
372 Ok(())
431 })373 })
432 .detach_and_log_err(cx);374 .detach_and_log_err(cx);
433 }375 }
@@ -445,12 +387,12 @@ impl DebugPanel { · 11 unmodified lines
445 };387 };
446 388
447 let dap_store_handle = self.project.read(cx).dap_store().clone();389 let dap_store_handle = self.project.read(cx).dap_store().clone();
448 let mut label = parent_session.read(cx).label().clone();390 let label = self.label_for_child_session(&parent_session, request, cx);
449 if !label.ends_with("(child)") {
450 label = format!("{label} (child)").into();
451 }
452 let adapter = parent_session.read(cx).adapter().clone();391 let adapter = parent_session.read(cx).adapter().clone();
453 let mut binary = parent_session.read(cx).binary().clone();392 let Some(mut binary) = parent_session.read(cx).binary().cloned() else {
393 log::error!("Attempted to start a child-session without a binary");
394 return;
395 };
454 binary.request_args = request.clone();396 binary.request_args = request.clone();
455 cx.spawn_in(window, async move |this, cx| {397 cx.spawn_in(window, async move |this, cx| {
456 let (session, task) = dap_store_handle.update(cx, |dap_store, cx| {398 let (session, task) = dap_store_handle.update(cx, |dap_store, cx| {
@@ -462,7 +404,11 @@ impl DebugPanel { · 5 unmodified lines
462 });404 });
463 (session, task)405 (session, task)
464 })?;406 })?;
465 Self::register_session(this, session, false, cx).await?;407 // Focus child sessions if the parent has never emitted a stopped event;
408 // this improves our JavaScript experience, as it always spawns a "main" session that then spawns subsessions.
409 let parent_ever_stopped =
410 parent_session.update(cx, |this, _| this.has_ever_stopped())?;
411 Self::register_session(this, session, !parent_ever_stopped, cx).await?;
466 task.await412 task.await
467 })413 })
468 .detach_and_log_err(cx);414 .detach_and_log_err(cx);
@@ -579,6 +525,26 @@ impl DebugPanel { · 110 unmodified lines
579 }525 }
580 }526 }
581 527
528 fn copy_debug_adapter_arguments(
529 &mut self,
530 _: &CopyDebugAdapterArguments,
531 _window: &mut Window,
532 cx: &mut Context<Self>,
533 ) {
534 let content = maybe!({
535 let mut session = self.active_session()?.read(cx).session(cx);
536 while let Some(parent) = session.read(cx).parent_session().cloned() {
537 session = parent;
538 }
539 let binary = session.read(cx).binary()?;
540 let content = serde_json::to_string_pretty(&binary).ok()?;
541 Some(content)
542 });
543 if let Some(content) = content {
544 cx.write_to_clipboard(ClipboardItem::new_string(content));
545 }
546 }
547
582 pub(crate) fn top_controls_strip(548 pub(crate) fn top_controls_strip(
583 &mut self,549 &mut self,
584 window: &mut Window,550 window: &mut Window,
@@ -608,6 +574,12 @@ impl DebugPanel { · 23 unmodified lines
608 }574 }
609 })575 })
610 };576 };
577 let documentation_button = || {
578 IconButton::new("debug-open-documentation", IconName::CircleHelp)
579 .icon_size(IconSize::Small)
580 .on_click(move |_, _, cx| cx.open_url("https://zed.dev/docs/debugger"))
581 .tooltip(Tooltip::text("Open Documentation"))
582 };
611 583
612 Some(584 Some(
613 div.border_b_1()585 div.border_b_1()
@@ -629,6 +601,8 @@ impl DebugPanel { · 15 unmodified lines
629 project::debugger::session::ThreadStatus::Exited,601 project::debugger::session::ThreadStatus::Exited,
630 );602 );
631 let capabilities = running_state.read(cx).capabilities(cx);603 let capabilities = running_state.read(cx).capabilities(cx);
604 let supports_detach =
605 running_state.read(cx).session().read(cx).is_attached();
632 this.map(|this| {606 this.map(|this| {
633 if thread_status == ThreadStatus::Running {607 if thread_status == ThreadStatus::Running {
634 this.child(608 this.child(
@@ -817,33 +791,48 @@ impl DebugPanel { · 182 unmodified lines
817 }791 }
818 }),792 }),
819 )793 )
820 .child(794 .when(
821 IconButton::new("debug-disconnect", IconName::DebugDetach)795 supports_detach,
822 .icon_size(IconSize::XSmall)796 |div| {
823 .on_click(window.listener_for(797 div.child(
824 &running_state,798 IconButton::new(
825 |this, _, _, cx| {799 "debug-disconnect",
826 this.detach_client(cx);800 IconName::DebugDetach,
827 },801 )
828 ))802 .disabled(
829 .tooltip({803 thread_status != ThreadStatus::Stopped
830 let focus_handle = focus_handle.clone();804 && thread_status != ThreadStatus::Running,
831 move |window, cx| {805 )
832 Tooltip::for_action_in(806 .icon_size(IconSize::XSmall)
833 "Detach",807 .on_click(window.listener_for(
834 &Detach,808 &running_state,
835 &focus_handle,809 |this, _, _, cx| {
836 window,810 this.detach_client(cx);
837 cx,811 },
838 )812 ))
839 }813 .tooltip({
840 }),814 let focus_handle = focus_handle.clone();
815 move |window, cx| {
816 Tooltip::for_action_in(
817 "Detach",
818 &Detach,
819 &focus_handle,
820 window,
821 cx,
822 )
823 }
824 }),
825 )
826 },
841 )827 )
842 },828 },
843 ),829 ),
844 )830 )
845 .justify_around()831 .justify_around()
846 .when(is_side, |this| this.child(new_session_button())),832 .when(is_side, |this| {
833 this.child(new_session_button())
834 .child(documentation_button())
835 }),
847 )836 )
848 .child(837 .child(
849 h_flex()838 h_flex()
@@ -884,7 +873,10 @@ impl DebugPanel { · 34 unmodified lines
884 window,873 window,
885 cx,874 cx,
886 ))875 ))
887 .when(!is_side, |this| this.child(new_session_button())),876 .when(!is_side, |this| {
877 this.child(new_session_button())
878 .child(documentation_button())
879 }),
888 ),880 ),
889 ),881 ),
890 )882 )
@@ -1042,6 +1034,25 @@ impl DebugPanel { · 151 unmodified lines
1042 cx.emit(PanelEvent::ZoomIn);1034 cx.emit(PanelEvent::ZoomIn);
1043 }1035 }
1044 }1036 }
1037
1038 fn label_for_child_session(
1039 &self,
1040 parent_session: &Entity<Session>,
1041 request: &StartDebuggingRequestArguments,
1042 cx: &mut Context<'_, Self>,
1043 ) -> SharedString {
1044 let adapter = parent_session.read(cx).adapter();
1045 if let Some(adapter) = DapRegistry::global(cx).adapter(&adapter) {
1046 if let Some(label) = adapter.label_for_child_session(request) {
1047 return label.into();
1048 }
1049 }
1050 let mut label = parent_session.read(cx).label().clone();
1051 if !label.ends_with("(child)") {
1052 label = format!("{label} (child)").into();
1053 }
1054 label
1055 }
1045}1056}
1046 1057
1047async fn register_session_inner(1058async fn register_session_inner(
@@ -1069,6 +1080,11 @@ async fn register_session_inner( · 21 unmodified lines
1069 .ok();1080 .ok();
1070 let serialized_layout = persistence::get_serialized_layout(adapter_name).await;1081 let serialized_layout = persistence::get_serialized_layout(adapter_name).await;
1071 let debug_session = this.update_in(cx, |this, window, cx| {1082 let debug_session = this.update_in(cx, |this, window, cx| {
1083 let parent_session = this
1084 .sessions
1085 .iter()
1086 .find(|p| Some(p.read(cx).session_id(cx)) == session.read(cx).parent_id(cx))
1087 .cloned();
1072 this.sessions.retain(|session| {1088 this.sessions.retain(|session| {
1073 !session1089 !session
1074 .read(cx)1090 .read(cx)
@@ -1082,8 +1098,8 @@ async fn register_session_inner( · 7 unmodified lines
1082 let debug_session = DebugSession::running(1098 let debug_session = DebugSession::running(
1083 this.project.clone(),1099 this.project.clone(),
1084 this.workspace.clone(),1100 this.workspace.clone(),
1101 parent_session.map(|p| p.read(cx).running_state().read(cx).debug_terminal.clone()),
1085 session,1102 session,
1086 cx.weak_entity(),
1087 serialized_layout,1103 serialized_layout,
1088 this.position(window, cx).axis(),1104 this.position(window, cx).axis(),
1089 window,1105 window,
@@ -1363,6 +1379,7 @@ impl Render for DebugPanel { · 273 unmodified lines
1363 });1379 });
1364 cx.notify();1380 cx.notify();
1365 }))1381 }))
1382 .on_action(cx.listener(Self::copy_debug_adapter_arguments))
1366 .when(self.active_session.is_some(), |this| {1383 .when(self.active_session.is_some(), |this| {
1367 this.on_mouse_down(1384 this.on_mouse_down(
1368 MouseButton::Right,1385 MouseButton::Right,
crates/debugger_ui/src/debugger_ui.rs+231−204
@@ -1,14 +1,17 @@
1use std::any::TypeId;
2
1use dap::debugger_settings::DebuggerSettings;3use dap::debugger_settings::DebuggerSettings;
2use debugger_panel::{DebugPanel, ToggleFocus};4use debugger_panel::{DebugPanel, ToggleFocus};
3use editor::Editor;5use editor::Editor;
4use feature_flags::{DebuggerFeatureFlag, FeatureFlagViewExt};6use feature_flags::{DebuggerFeatureFlag, FeatureFlagViewExt};
5use gpui::{App, EntityInputHandler, actions};7use gpui::{App, DispatchPhase, EntityInputHandler, actions};
6use new_process_modal::{NewProcessModal, NewProcessMode};8use new_process_modal::{NewProcessModal, NewProcessMode};
7use project::debugger::{self, breakpoint_store::SourceBreakpoint};9use project::debugger::{self, breakpoint_store::SourceBreakpoint, session::ThreadStatus};
8use session::DebugSession;10use session::DebugSession;
9use settings::Settings;11use settings::Settings;
10use stack_trace_view::StackTraceView;12use stack_trace_view::StackTraceView;
11use tasks_ui::{Spawn, TaskOverrides};13use tasks_ui::{Spawn, TaskOverrides};
14use ui::{FluentBuilder, InteractiveElement};
12use util::maybe;15use util::maybe;
13use workspace::{ItemHandle, ShutdownDebugAdapters, Workspace};16use workspace::{ItemHandle, ShutdownDebugAdapters, Workspace};
14 17
@@ -53,6 +56,8 @@ actions!( · 38 unmodified lines
53 ]56 ]
54);57);
55 58
59actions!(dev, [CopyDebugAdapterArguments]);
60
56pub fn init(cx: &mut App) {61pub fn init(cx: &mut App) {
57 DebuggerSettings::register(cx);62 DebuggerSettings::register(cx);
58 workspace::FollowableViewRegistry::register::<DebugSession>(cx);63 workspace::FollowableViewRegistry::register::<DebugSession>(cx);
@@ -68,107 +73,20 @@ pub fn init(cx: &mut App) { · 9 unmodified lines
68 .register_action(|workspace, _: &ToggleFocus, window, cx| {73 .register_action(|workspace, _: &ToggleFocus, window, cx| {
69 workspace.toggle_panel_focus::<DebugPanel>(window, cx);74 workspace.toggle_panel_focus::<DebugPanel>(window, cx);
70 })75 })
71 .register_action(|workspace, _: &Pause, _, cx| {76 .register_action(|workspace: &mut Workspace, _: &Start, window, cx| {
72 if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {77 NewProcessModal::show(workspace, window, NewProcessMode::Debug, None, cx);
73 if let Some(active_item) = debug_panel
74 .read(cx)
75 .active_session()
76 .map(|session| session.read(cx).running_state().clone())
77 {
78 active_item.update(cx, |item, cx| item.pause_thread(cx))
79 }
80 }
81 })
82 .register_action(|workspace, _: &Restart, _, cx| {
83 if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
84 if let Some(active_item) = debug_panel
85 .read(cx)
86 .active_session()
87 .map(|session| session.read(cx).running_state().clone())
88 {
89 active_item.update(cx, |item, cx| item.restart_session(cx))
90 }
91 }
92 })
93 .register_action(|workspace, _: &Continue, _, cx| {
94 if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
95 if let Some(active_item) = debug_panel
96 .read(cx)
97 .active_session()
98 .map(|session| session.read(cx).running_state().clone())
99 {
100 active_item.update(cx, |item, cx| item.continue_thread(cx))
101 }
102 }
103 })
104 .register_action(|workspace, _: &StepInto, _, cx| {
105 if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
106 if let Some(active_item) = debug_panel
107 .read(cx)
108 .active_session()
109 .map(|session| session.read(cx).running_state().clone())
110 {
111 active_item.update(cx, |item, cx| item.step_in(cx))
112 }
113 }
114 })
115 .register_action(|workspace, _: &StepOver, _, cx| {
116 if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
117 if let Some(active_item) = debug_panel
118 .read(cx)
119 .active_session()
120 .map(|session| session.read(cx).running_state().clone())
121 {
122 active_item.update(cx, |item, cx| item.step_over(cx))
123 }
124 }
125 })
126 .register_action(|workspace, _: &StepOut, _, cx| {
127 if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
128 if let Some(active_item) = debug_panel.read_with(cx, |panel, cx| {
129 panel
130 .active_session()
131 .map(|session| session.read(cx).running_state().clone())
132 }) {
133 active_item.update(cx, |item, cx| item.step_out(cx))
134 }
135 }
136 })
137 .register_action(|workspace, _: &StepBack, _, cx| {
138 if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
139 if let Some(active_item) = debug_panel
140 .read(cx)
141 .active_session()
142 .map(|session| session.read(cx).running_state().clone())
143 {
144 active_item.update(cx, |item, cx| item.step_back(cx))
145 }
146 }
147 })
148 .register_action(|workspace, _: &Stop, _, cx| {
149 if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
150 if let Some(active_item) = debug_panel
151 .read(cx)
152 .active_session()
153 .map(|session| session.read(cx).running_state().clone())
154 {
155 cx.defer(move |cx| {
156 active_item.update(cx, |item, cx| item.stop_thread(cx))
157 })
158 }
159 }
160 })
161 .register_action(|workspace, _: &ToggleIgnoreBreakpoints, _, cx| {
162 if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
163 if let Some(active_item) = debug_panel
164 .read(cx)
165 .active_session()
166 .map(|session| session.read(cx).running_state().clone())
167 {
168 active_item.update(cx, |item, cx| item.toggle_ignore_breakpoints(cx))
169 }
170 }
171 })78 })
79 .register_action(
80 |workspace: &mut Workspace, _: &RerunLastSession, window, cx| {
81 let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) else {
82 return;
83 };
84
85 debug_panel.update(cx, |debug_panel, cx| {
86 debug_panel.rerun_last_session(workspace, window, cx);
87 })
88 },
89 )
172 .register_action(90 .register_action(
173 |workspace: &mut Workspace, _: &ShutdownDebugAdapters, _window, cx| {91 |workspace: &mut Workspace, _: &ShutdownDebugAdapters, _window, cx| {
174 workspace.project().update(cx, |project, cx| {92 workspace.project().update(cx, |project, cx| {
@@ -178,135 +96,244 @@ pub fn init(cx: &mut App) { · 3 unmodified lines
178 })96 })
179 },97 },
180 )98 )
181 .register_action(99 .register_action_renderer(|div, workspace, _, cx| {
182 |workspace: &mut Workspace, _: &ShowStackTrace, window, cx| {100 let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) else {
183 let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) else {101 return div;
184 return;102 };
185 };103 let Some(active_item) = debug_panel
104 .read(cx)
105 .active_session()
106 .map(|session| session.read(cx).running_state().clone())
107 else {
108 return div;
109 };
110 let running_state = active_item.read(cx);
111 if running_state.session().read(cx).is_terminated() {
112 return div;
113 }
186 114
187 if let Some(existing) = workspace.item_of_type::<StackTraceView>(cx) {115 let caps = running_state.capabilities(cx);
188 let is_active = workspace116 let supports_step_back = caps.supports_step_back.unwrap_or_default();
189 .active_item(cx)117 let supports_detach = running_state.session().read(cx).is_attached();
190 .is_some_and(|item| item.item_id() == existing.item_id());118 let status = running_state.thread_status(cx);
191 workspace.activate_item(&existing, true, !is_active, window, cx);
192 } else {
193 let Some(active_session) = debug_panel.read(cx).active_session() else {
194 return;
195 };
196 119
197 let project = workspace.project();120 let active_item = active_item.downgrade();
121 div.when(status == Some(ThreadStatus::Running), |div| {
122 let active_item = active_item.clone();
123 div.on_action(move |_: &Pause, _, cx| {
124 active_item
125 .update(cx, |item, cx| item.pause_thread(cx))
126 .ok();
127 })
128 })
129 .when(status == Some(ThreadStatus::Stopped), |div| {
130 div.on_action({
131 let active_item = active_item.clone();
132 move |_: &StepInto, _, cx| {
133 active_item.update(cx, |item, cx| item.step_in(cx)).ok();
134 }
135 })
136 .on_action({
137 let active_item = active_item.clone();
138 move |_: &StepOver, _, cx| {
139 active_item.update(cx, |item, cx| item.step_over(cx)).ok();
140 }
141 })
142 .on_action({
143 let active_item = active_item.clone();
144 move |_: &StepOut, _, cx| {
145 active_item.update(cx, |item, cx| item.step_out(cx)).ok();
146 }
147 })
148 .when(supports_step_back, |div| {
149 let active_item = active_item.clone();
150 div.on_action(move |_: &StepBack, _, cx| {
151 active_item.update(cx, |item, cx| item.step_back(cx)).ok();
152 })
153 })
154 .on_action({
155 let active_item = active_item.clone();
156 move |_: &Continue, _, cx| {
157 active_item
158 .update(cx, |item, cx| item.continue_thread(cx))
159 .ok();
160 }
161 })
162 .on_action(cx.listener(
163 |workspace, _: &ShowStackTrace, window, cx| {
164 let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) else {
165 return;
166 };
198 167
199 let stack_trace_view = active_session.update(cx, |session, cx| {168 if let Some(existing) = workspace.item_of_type::<StackTraceView>(cx)
200 session.stack_trace_view(project, window, cx).clone()169 {
201 });170 let is_active = workspace
171 .active_item(cx)
172 .is_some_and(|item| item.item_id() == existing.item_id());
173 workspace
174 .activate_item(&existing, true, !is_active, window, cx);
175 } else {
176 let Some(active_session) =
177 debug_panel.read(cx).active_session()
178 else {
179 return;
180 };
202 181
203 workspace.add_item_to_active_pane(182 let project = workspace.project();
204 Box::new(stack_trace_view),
205 None,
206 true,
207 window,
208 cx,
209 );
210 }
211 },
212 )
213 .register_action(|workspace: &mut Workspace, _: &Start, window, cx| {
214 NewProcessModal::show(workspace, window, NewProcessMode::Debug, None, cx);
215 })
216 .register_action(
217 |workspace: &mut Workspace, _: &RerunLastSession, window, cx| {
218 let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) else {
219 return;
220 };
221 183
222 debug_panel.update(cx, |debug_panel, cx| {184 let stack_trace_view =
223 debug_panel.rerun_last_session(workspace, window, cx);185 active_session.update(cx, |session, cx| {
186 session.stack_trace_view(project, window, cx).clone()
187 });
188
189 workspace.add_item_to_active_pane(
190 Box::new(stack_trace_view),
191 None,
192 true,
193 window,
194 cx,
195 );
196 }
197 },
198 ))
199 })
200 .when(supports_detach, |div| {
201 let active_item = active_item.clone();
202 div.on_action(move |_: &Detach, _, cx| {
203 active_item
204 .update(cx, |item, cx| item.detach_client(cx))
205 .ok();
224 })206 })
225 },207 })
226 );208 .on_action({
209 let active_item = active_item.clone();
210 move |_: &Restart, _, cx| {
211 active_item
212 .update(cx, |item, cx| item.restart_session(cx))
213 .ok();
214 }
215 })
216 .on_action({
217 let active_item = active_item.clone();
218 move |_: &Stop, _, cx| {
219 active_item.update(cx, |item, cx| item.stop_thread(cx)).ok();
220 }
221 })
222 .on_action({
223 let active_item = active_item.clone();
224 move |_: &ToggleIgnoreBreakpoints, _, cx| {
225 active_item
226 .update(cx, |item, cx| item.toggle_ignore_breakpoints(cx))
227 .ok();
228 }
229 })
230 });
227 })231 })
228 })232 })
229 .detach();233 .detach();
230 234
231 cx.observe_new({235 cx.observe_new({
232 move |editor: &mut Editor, _, cx| {236 move |editor: &mut Editor, _, _| {
233 editor237 editor
234 .register_action(cx.listener(238 .register_action_renderer(move |editor, window, cx| {
235 move |editor, _: &editor::actions::DebuggerRunToCursor, _, cx| {239 let Some(workspace) = editor.workspace() else {
236 maybe!({240 return;
237 let debug_panel =241 };
238 editor.workspace()?.read(cx).panel::<DebugPanel>(cx)?;242 let Some(debug_panel) = workspace.read(cx).panel::<DebugPanel>(cx) else {
239 let cursor_point: language::Point = editor.selections.newest(cx).head();243 return;
240 let active_session = debug_panel.read(cx).active_session()?;244 };
245 let Some(active_session) = debug_panel
246 .clone()
247 .update(cx, |panel, _| panel.active_session())
248 else {
249 return;
250 };
251 let editor = cx.entity().downgrade();
252 window.on_action(TypeId::of::<editor::actions::RunToCursor>(), {
253 let editor = editor.clone();
254 let active_session = active_session.clone();
255 move |_, phase, _, cx| {
256 if phase != DispatchPhase::Bubble {
257 return;
258 }
259 maybe!({
260 let (buffer, position, _) = editor
261 .update(cx, |editor, cx| {
262 let cursor_point: language::Point =
263 editor.selections.newest(cx).head();
241 264
242 let (buffer, position, _) = editor265 editor
243 .buffer()266 .buffer()
244 .read(cx)267 .read(cx)
245 .point_to_buffer_point(cursor_point, cx)?;268 .point_to_buffer_point(cursor_point, cx)
269 })
270 .ok()??;
246 271
247 let path =272 let path =
248 debugger::breakpoint_store::BreakpointStore::abs_path_from_buffer(273 debugger::breakpoint_store::BreakpointStore::abs_path_from_buffer(
249 &buffer, cx,274 &buffer, cx,
250 )?;275 )?;
251 276
252 let source_breakpoint = SourceBreakpoint {277 let source_breakpoint = SourceBreakpoint {
253 row: position.row,278 row: position.row,
254 path,279 path,
255 message: None,280 message: None,
256 condition: None,281 condition: None,
257 hit_condition: None,282 hit_condition: None,
258 state: debugger::breakpoint_store::BreakpointState::Enabled,283 state: debugger::breakpoint_store::BreakpointState::Enabled,
259 };284 };
260 285
261 active_session.update(cx, |session, cx| {286 active_session.update(cx, |session, cx| {
262 session.running_state().update(cx, |state, cx| {287 session.running_state().update(cx, |state, cx| {
263 if let Some(thread_id) = state.selected_thread_id() {288 if let Some(thread_id) = state.selected_thread_id() {
264 state.session().update(cx, |session, cx| {289 state.session().update(cx, |session, cx| {
265 session.run_to_position(290 session.run_to_position(
266 source_breakpoint,291 source_breakpoint,
267 thread_id,292 thread_id,
268 cx,293 cx,
269 );294 );
270 })295 })
271 }296 }
297 });
272 });298 });
273 });
274 299
275 Some(())300 Some(())
276 });301 });
277 },302 }
278 ))303 });
279 .detach();
280
281 editor
282 .register_action(cx.listener(
283 move |editor, _: &editor::actions::DebuggerEvaluateSelectedText, window, cx| {
284 maybe!({
285 let debug_panel =
286 editor.workspace()?.read(cx).panel::<DebugPanel>(cx)?;
287 let active_session = debug_panel.read(cx).active_session()?;
288 304
289 let text = editor.text_for_range(305 window.on_action(
290 editor.selections.newest(cx).range(),306 TypeId::of::<editor::actions::EvaluateSelectedText>(),
291 &mut None,307 move |_, _, window, cx| {
292 window,308 maybe!({
293 cx,309 let text = editor
294 )?;310 .update(cx, |editor, cx| {
311 editor.text_for_range(
312 editor.selections.newest(cx).range(),
313 &mut None,
314 window,
315 cx,
316 )
317 })
318 .ok()??;
295 319
296 active_session.update(cx, |session, cx| {320 active_session.update(cx, |session, cx| {
297 session.running_state().update(cx, |state, cx| {321 session.running_state().update(cx, |state, cx| {
298 let stack_id = state.selected_stack_frame_id(cx);322 let stack_id = state.selected_stack_frame_id(cx);
299 323
300 state.session().update(cx, |session, cx| {324 state.session().update(cx, |session, cx| {
301 session.evaluate(text, None, stack_id, None, cx).detach();325 session
326 .evaluate(text, None, stack_id, None, cx)
327 .detach();
328 });
302 });329 });
303 });330 });
304 });
305 331
306 Some(())332 Some(())
307 });333 });
308 },334 },
309 ))335 );
336 })
310 .detach();337 .detach();
311 }338 }
312 })339 })
crates/debugger_ui/src/new_process_modal.rs+262−105
@@ -1,4 +1,4 @@
1use collections::FxHashMap;1use collections::{FxHashMap, HashMap};
2use language::LanguageRegistry;2use language::LanguageRegistry;
3use paths::local_debug_file_relative_path;3use paths::local_debug_file_relative_path;
4use std::{4use std::{
@@ -15,9 +15,9 @@ use dap::{ · 10 unmodified lines
15use editor::{Editor, EditorElement, EditorStyle};15use editor::{Editor, EditorElement, EditorStyle};
16use fuzzy::{StringMatch, StringMatchCandidate};16use fuzzy::{StringMatch, StringMatchCandidate};
17use gpui::{17use gpui::{
18 App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, HighlightStyle,18 Action, App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
19 InteractiveText, KeyContext, PromptButton, PromptLevel, Render, StyledText, Subscription,19 HighlightStyle, InteractiveText, KeyContext, PromptButton, PromptLevel, Render, StyledText,
20 TextStyle, UnderlineStyle, WeakEntity,20 Subscription, Task, TextStyle, UnderlineStyle, WeakEntity,
21};21};
22use itertools::Itertools as _;22use itertools::Itertools as _;
23use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch};23use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch};
@@ -28,10 +28,10 @@ use theme::ThemeSettings; · 4 unmodified lines
28use ui::{28use ui::{
29 ActiveTheme, Button, ButtonCommon, ButtonSize, CheckboxWithLabel, Clickable, Color, Context,29 ActiveTheme, Button, ButtonCommon, ButtonSize, CheckboxWithLabel, Clickable, Color, Context,
30 ContextMenu, Disableable, DropdownMenu, FluentBuilder, Icon, IconName, IconSize,30 ContextMenu, Disableable, DropdownMenu, FluentBuilder, Icon, IconName, IconSize,
31 IconWithIndicator, Indicator, InteractiveElement, IntoElement, Label, LabelCommon as _,31 IconWithIndicator, Indicator, InteractiveElement, IntoElement, KeyBinding, Label,
32 ListItem, ListItemSpacing, ParentElement, RenderOnce, SharedString, Styled, StyledExt,32 LabelCommon as _, LabelSize, ListItem, ListItemSpacing, ParentElement, RenderOnce,
33 StyledTypography, ToggleButton, ToggleState, Toggleable, Tooltip, Window, div, h_flex, px,33 SharedString, Styled, StyledExt, StyledTypography, ToggleButton, ToggleState, Toggleable,
34 relative, rems, v_flex,34 Tooltip, Window, div, h_flex, px, relative, rems, v_flex,
35};35};
36use util::ResultExt;36use util::ResultExt;
37use workspace::{ModalView, Workspace, pane};37use workspace::{ModalView, Workspace, pane};
@@ -50,7 +50,7 @@ pub(super) struct NewProcessModal { · 12 unmodified lines
50 mode: NewProcessMode,50 mode: NewProcessMode,
51 debug_picker: Entity<Picker<DebugDelegate>>,51 debug_picker: Entity<Picker<DebugDelegate>>,
52 attach_mode: Entity<AttachMode>,52 attach_mode: Entity<AttachMode>,
53 launch_mode: Entity<ConfigureMode>,53 configure_mode: Entity<ConfigureMode>,
54 task_mode: TaskMode,54 task_mode: TaskMode,
55 debugger: Option<DebugAdapterName>,55 debugger: Option<DebugAdapterName>,
56 // save_scenario_state: Option<SaveScenarioState>,56 // save_scenario_state: Option<SaveScenarioState>,
@@ -194,26 +194,31 @@ impl NewProcessModal { · 137 unmodified lines
194 return Ok(());194 return Ok(());
195 };195 };
196 196
197 let (used_tasks, current_resolved_tasks) =197 let (used_tasks, current_resolved_tasks) = task_inventory
198 task_inventory.update(cx, |task_inventory, cx| {198 .update(cx, |task_inventory, cx| {
199 task_inventory199 task_inventory
200 .used_and_current_resolved_tasks(&task_contexts, cx)200 .used_and_current_resolved_tasks(task_contexts.clone(), cx)
201 })?;201 })?
202 202 .await;
203 debug_picker203
204 .update_in(cx, |picker, window, cx| {204 if let Ok(task) = debug_picker.update(cx, |picker, cx| {
205 picker.delegate.tasks_loaded(205 picker.delegate.tasks_loaded(
206 task_contexts.clone(),206 task_contexts.clone(),
207 languages,207 languages,
208 lsp_tasks.clone(),208 lsp_tasks.clone(),
209 current_resolved_tasks.clone(),209 current_resolved_tasks.clone(),
210 add_current_language_tasks,210 add_current_language_tasks,
211 cx,211 cx,
212 );212 )
213 picker.refresh(window, cx);213 }) {
214 cx.notify();214 task.await;
215 })215 debug_picker
216 .ok();216 .update_in(cx, |picker, window, cx| {
217 picker.refresh(window, cx);
218 cx.notify();
219 })
220 .ok();
221 }
217 222
218 if let Some(active_cwd) = task_contexts223 if let Some(active_cwd) = task_contexts
219 .active_context()224 .active_context()
@@ -253,7 +258,7 @@ impl NewProcessModal { · 33 unmodified lines
253 Self {258 Self {
254 debug_picker,259 debug_picker,
255 attach_mode,260 attach_mode,
256 launch_mode: configure_mode,261 configure_mode,
257 task_mode,262 task_mode,
258 debugger: None,263 debugger: None,
259 mode,264 mode,
@@ -283,7 +288,7 @@ impl NewProcessModal { · 23 unmodified lines
283 NewProcessMode::Attach => self.attach_mode.update(cx, |this, cx| {288 NewProcessMode::Attach => self.attach_mode.update(cx, |this, cx| {
284 this.clone().render(window, cx).into_any_element()289 this.clone().render(window, cx).into_any_element()
285 }),290 }),
286 NewProcessMode::Launch => self.launch_mode.update(cx, |this, cx| {291 NewProcessMode::Launch => self.configure_mode.update(cx, |this, cx| {
287 this.clone().render(dap_menu, window, cx).into_any_element()292 this.clone().render(dap_menu, window, cx).into_any_element()
288 }),293 }),
289 NewProcessMode::Debug => v_flex()294 NewProcessMode::Debug => v_flex()
@@ -297,7 +302,7 @@ impl NewProcessModal { · 7 unmodified lines
297 match self.mode {302 match self.mode {
298 NewProcessMode::Task => self.task_mode.task_modal.focus_handle(cx),303 NewProcessMode::Task => self.task_mode.task_modal.focus_handle(cx),
299 NewProcessMode::Attach => self.attach_mode.read(cx).attach_picker.focus_handle(cx),304 NewProcessMode::Attach => self.attach_mode.read(cx).attach_picker.focus_handle(cx),
300 NewProcessMode::Launch => self.launch_mode.read(cx).program.focus_handle(cx),305 NewProcessMode::Launch => self.configure_mode.read(cx).program.focus_handle(cx),
301 NewProcessMode::Debug => self.debug_picker.focus_handle(cx),306 NewProcessMode::Debug => self.debug_picker.focus_handle(cx),
302 }307 }
303 }308 }
@@ -305,7 +310,7 @@ impl NewProcessModal { · 1 unmodified line
305 fn debug_scenario(&self, debugger: &str, cx: &App) -> Option<DebugScenario> {310 fn debug_scenario(&self, debugger: &str, cx: &App) -> Option<DebugScenario> {
306 let request = match self.mode {311 let request = match self.mode {
307 NewProcessMode::Launch => Some(DebugRequest::Launch(312 NewProcessMode::Launch => Some(DebugRequest::Launch(
308 self.launch_mode.read(cx).debug_request(cx),313 self.configure_mode.read(cx).debug_request(cx),
309 )),314 )),
310 NewProcessMode::Attach => Some(DebugRequest::Attach(315 NewProcessMode::Attach => Some(DebugRequest::Attach(
311 self.attach_mode.read(cx).debug_request(),316 self.attach_mode.read(cx).debug_request(),
@@ -315,7 +320,7 @@ impl NewProcessModal { · 3 unmodified lines
315 let label = suggested_label(&request, debugger);320 let label = suggested_label(&request, debugger);
316 321
317 let stop_on_entry = if let NewProcessMode::Launch = &self.mode {322 let stop_on_entry = if let NewProcessMode::Launch = &self.mode {
318 Some(self.launch_mode.read(cx).stop_on_entry.selected())323 Some(self.configure_mode.read(cx).stop_on_entry.selected())
319 } else {324 } else {
320 None325 None
321 };326 };
@@ -476,10 +481,9 @@ impl NewProcessModal { · 154 unmodified lines
476 .get_index_of(adapter.0.as_ref())481 .get_index_of(adapter.0.as_ref())
477 .unwrap_or(usize::MAX)482 .unwrap_or(usize::MAX)
478 });483 });
479 }484 if self.debugger.is_none() {
480 485 self.debugger = available_adapters.first().cloned();
481 if self.debugger.is_none() {486 }
482 self.debugger = available_adapters.first().cloned();
483 }487 }
484 488
485 let label = self489 let label = self
@@ -831,7 +835,7 @@ impl Render for NewProcessModal { · 345 unmodified lines
831 .disabled(835 .disabled(
832 self.debugger.is_none()836 self.debugger.is_none()
833 || self837 || self
834 .launch_mode838 .configure_mode
835 .read(cx)839 .read(cx)
836 .program840 .program
837 .read(cx)841 .read(cx)
@@ -1143,41 +1147,67 @@ impl DebugDelegate { · 305 unmodified lines
1143 current_resolved_tasks: Vec<(TaskSourceKind, task::ResolvedTask)>,1147 current_resolved_tasks: Vec<(TaskSourceKind, task::ResolvedTask)>,
1144 add_current_language_tasks: bool,1148 add_current_language_tasks: bool,
1145 cx: &mut Context<Picker<Self>>,1149 cx: &mut Context<Picker<Self>>,
1146 ) {1150 ) -> Task<()> {
1147 self.task_contexts = Some(task_contexts.clone());1151 self.task_contexts = Some(task_contexts.clone());
1148 1152 let task = self.task_store.update(cx, |task_store, cx| {
1149 let (recent, scenarios) = self1153 task_store.task_inventory().map(|inventory| {
1150 .task_store1154 inventory.update(cx, |inventory, cx| {
1151 .update(cx, |task_store, cx| {1155 inventory.list_debug_scenarios(
1152 task_store.task_inventory().map(|inventory| {1156 &task_contexts,
1153 inventory.update(cx, |inventory, cx| {1157 lsp_tasks,
1154 inventory.list_debug_scenarios(1158 current_resolved_tasks,
1155 &task_contexts,1159 add_current_language_tasks,
1156 lsp_tasks,1160 cx,
1157 current_resolved_tasks,1161 )
1158 add_current_language_tasks,
1159 cx,
1160 )
1161 })
1162 })1162 })
1163 })1163 })
1164 .unwrap_or_default();1164 });
1165 cx.spawn(async move |this, cx| {
1166 let (recent, scenarios) = if let Some(task) = task {
1167 task.await
1168 } else {
1169 (Vec::new(), Vec::new())
1170 };
1165 1171
1166 if !recent.is_empty() {1172 this.update(cx, |this, cx| {
1167 self.last_used_candidate_index = Some(recent.len() - 1);1173 if !recent.is_empty() {
1168 }1174 this.delegate.last_used_candidate_index = Some(recent.len() - 1);
1175 }
1169 1176
1170 let dap_registry = cx.global::<DapRegistry>();1177 let dap_registry = cx.global::<DapRegistry>();
1178 let hide_vscode = scenarios.iter().any(|(kind, _)| match kind {
1179 TaskSourceKind::Worktree {
1180 id: _,
1181 directory_in_worktree: dir,
1182 id_base: _,
1183 } => dir.ends_with(".zed"),
1184 _ => false,
1185 });
1171 1186
1172 self.candidates = recent1187 this.delegate.candidates = recent
1173 .into_iter()1188 .into_iter()
1174 .map(|scenario| Self::get_scenario_kind(&languages, &dap_registry, scenario))1189 .map(|scenario| Self::get_scenario_kind(&languages, &dap_registry, scenario))
1175 .chain(scenarios.into_iter().map(|(kind, scenario)| {1190 .chain(
1176 let (language, scenario) =1191 scenarios
1177 Self::get_scenario_kind(&languages, &dap_registry, scenario);1192 .into_iter()
1178 (language.or(Some(kind)), scenario)1193 .filter(|(kind, _)| match kind {
1179 }))1194 TaskSourceKind::Worktree {
1180 .collect();1195 id: _,
1196 directory_in_worktree: dir,
1197 id_base: _,
1198 } => !(hide_vscode && dir.ends_with(".vscode")),
1199 _ => true,
1200 })
1201 .map(|(kind, scenario)| {
1202 let (language, scenario) =
1203 Self::get_scenario_kind(&languages, &dap_registry, scenario);
1204 (language.or(Some(kind)), scenario)
1205 }),
1206 )
1207 .collect();
1208 })
1209 .ok();
1210 })
1181 }1211 }
1182}1212}
1183 1213
@@ -1202,7 +1232,7 @@ impl PickerDelegate for DebugDelegate { · 18 unmodified lines
1202 }1232 }
1203 1233
1204 fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> std::sync::Arc<str> {1234 fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> std::sync::Arc<str> {
1205 "".into()1235 "Find a debug task, or debug a command.".into()
1206 }1236 }
1207 1237
1208 fn update_matches(1238 fn update_matches(
@@ -1265,6 +1295,116 @@ impl PickerDelegate for DebugDelegate { · 56 unmodified lines
1265 }1295 }
1266 }1296 }
1267 1297
1298 fn confirm_input(
1299 &mut self,
1300 _secondary: bool,
1301 window: &mut Window,
1302 cx: &mut Context<Picker<Self>>,
1303 ) {
1304 let text = self.prompt.clone();
1305 let (task_context, worktree_id) = self
1306 .task_contexts
1307 .as_ref()
1308 .and_then(|task_contexts| {
1309 Some((
1310 task_contexts.active_context().cloned()?,
1311 task_contexts.worktree(),
1312 ))
1313 })
1314 .unwrap_or_default();
1315
1316 let mut args = shlex::split(&text).into_iter().flatten().peekable();
1317 let mut env = HashMap::default();
1318 while args.peek().is_some_and(|arg| arg.contains('=')) {
1319 let arg = args.next().unwrap();
1320 let (lhs, rhs) = arg.split_once('=').unwrap();
1321 env.insert(lhs.to_string(), rhs.to_string());
1322 }
1323
1324 let program = if let Some(program) = args.next() {
1325 program
1326 } else {
1327 env = HashMap::default();
1328 text
1329 };
1330
1331 let args = args.collect::<Vec<_>>();
1332 let task = task::TaskTemplate {
1333 label: "one-off".to_owned(),
1334 env,
1335 command: program,
1336 args,
1337 ..Default::default()
1338 };
1339
1340 let Some(location) = self
1341 .task_contexts
1342 .as_ref()
1343 .and_then(|cx| cx.location().cloned())
1344 else {
1345 return;
1346 };
1347 let file = location.buffer.read(cx).file();
1348 let language = location.buffer.read(cx).language();
1349 let language_name = language.as_ref().map(|l| l.name());
1350 let Some(adapter): Option<DebugAdapterName> =
1351 language::language_settings::language_settings(language_name, file, cx)
1352 .debuggers
1353 .first()
1354 .map(SharedString::from)
1355 .map(Into::into)
1356 .or_else(|| {
1357 language.and_then(|l| {
1358 l.config()
1359 .debuggers
1360 .first()
1361 .map(SharedString::from)
1362 .map(Into::into)
1363 })
1364 })
1365 else {
1366 return;
1367 };
1368 let locators = cx.global::<DapRegistry>().locators();
1369 cx.spawn_in(window, async move |this, cx| {
1370 let Some(debug_scenario) = cx
1371 .background_spawn(async move {
1372 for locator in locators {
1373 if let Some(scenario) =
1374 locator.1.create_scenario(&task, "one-off", &adapter).await
1375 {
1376 return Some(scenario);
1377 }
1378 }
1379 None
1380 })
1381 .await
1382 else {
1383 return;
1384 };
1385
1386 this.update_in(cx, |this, window, cx| {
1387 send_telemetry(&debug_scenario, TelemetrySpawnLocation::ScenarioList, cx);
1388 this.delegate
1389 .debug_panel
1390 .update(cx, |panel, cx| {
1391 panel.start_session(
1392 debug_scenario,
1393 task_context,
1394 None,
1395 worktree_id,
1396 window,
1397 cx,
1398 );
1399 })
1400 .ok();
1401 cx.emit(DismissEvent);
1402 })
1403 .ok();
1404 })
1405 .detach();
1406 }
1407
1268 fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<picker::Picker<Self>>) {1408 fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<picker::Picker<Self>>) {
1269 let debug_scenario = self1409 let debug_scenario = self
1270 .matches1410 .matches
@@ -1300,6 +1440,60 @@ impl PickerDelegate for DebugDelegate { · 29 unmodified lines
1300 cx.emit(DismissEvent);1440 cx.emit(DismissEvent);
1301 }1441 }
1302 1442
1443 fn render_footer(
1444 &self,
1445 window: &mut Window,
1446 cx: &mut Context<Picker<Self>>,
1447 ) -> Option<ui::AnyElement> {
1448 let current_modifiers = window.modifiers();
1449 let footer = h_flex()
1450 .w_full()
1451 .h_8()
1452 .p_2()
1453 .justify_between()
1454 .rounded_b_sm()
1455 .bg(cx.theme().colors().ghost_element_selected)
1456 .border_t_1()
1457 .border_color(cx.theme().colors().border_variant)
1458 .child(
1459 // TODO: add button to open selected task in debug.json
1460 h_flex().into_any_element(),
1461 )
1462 .map(|this| {
1463 if (current_modifiers.alt || self.matches.is_empty()) && !self.prompt.is_empty() {
1464 let action = picker::ConfirmInput {
1465 secondary: current_modifiers.secondary(),
1466 }
1467 .boxed_clone();
1468 this.children(KeyBinding::for_action(&*action, window, cx).map(|keybind| {
1469 Button::new("launch-custom", "Launch Custom")
1470 .label_size(LabelSize::Small)
1471 .key_binding(keybind)
1472 .on_click(move |_, window, cx| {
1473 window.dispatch_action(action.boxed_clone(), cx)
1474 })
1475 }))
1476 } else {
1477 this.children(KeyBinding::for_action(&menu::Confirm, window, cx).map(
1478 |keybind| {
1479 let is_recent_selected =
1480 self.divider_index >= Some(self.selected_index);
1481 let run_entry_label =
1482 if is_recent_selected { "Rerun" } else { "Spawn" };
1483
1484 Button::new("spawn", run_entry_label)
1485 .label_size(LabelSize::Small)
1486 .key_binding(keybind)
1487 .on_click(|_, window, cx| {
1488 window.dispatch_action(menu::Confirm.boxed_clone(), cx);
1489 })
1490 },
1491 ))
1492 }
1493 });
1494 Some(footer.into_any_element())
1495 }
1496
1303 fn render_match(1497 fn render_match(
1304 &self,1498 &self,
1305 ix: usize,1499 ix: usize,
@@ -1369,40 +1563,3 @@ pub(crate) fn resolve_path(path: &mut String) { · 63 unmodified lines
1369 );1563 );
1370 };1564 };
1371}1565}
1372
1373#[cfg(test)]
1374impl NewProcessModal {
1375 // #[cfg(test)]
1376 // pub(crate) fn set_configure(
1377 // &mut self,
1378 // program: impl AsRef<str>,
1379 // cwd: impl AsRef<str>,
1380 // stop_on_entry: bool,
1381 // window: &mut Window,
1382 // cx: &mut Context<Self>,
1383 // ) {
1384 // self.mode = NewProcessMode::Launch;
1385 // self.debugger = Some(dap::adapters::DebugAdapterName("fake-adapter".into()));
1386
1387 // self.launch_mode.update(cx, |configure, cx| {
1388 // configure.program.update(cx, |editor, cx| {
1389 // editor.clear(window, cx);
1390 // editor.set_text(program.as_ref(), window, cx);
1391 // });
1392
1393 // configure.cwd.update(cx, |editor, cx| {
1394 // editor.clear(window, cx);
1395 // editor.set_text(cwd.as_ref(), window, cx);
1396 // });
1397
1398 // configure.stop_on_entry = match stop_on_entry {
1399 // true => ToggleState::Selected,
1400 // _ => ToggleState::Unselected,
1401 // }
1402 // })
1403 // }
1404
1405 // pub(crate) fn save_scenario(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {
1406 // self.save_debug_scenario(window, cx);
1407 // }
1408}
crates/debugger_ui/src/session.rs+3−4
@@ -1,6 +1,6 @@
1pub mod running;1pub mod running;
2 2
3use crate::{StackTraceView, debugger_panel::DebugPanel, persistence::SerializedLayout};3use crate::{StackTraceView, persistence::SerializedLayout, session::running::DebugTerminal};
4use dap::client::SessionId;4use dap::client::SessionId;
5use gpui::{5use gpui::{
6 App, Axis, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity,6 App, Axis, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity,
@@ -22,7 +22,6 @@ pub struct DebugSession { · 15 unmodified lines
22 running_state: Entity<RunningState>,22 running_state: Entity<RunningState>,
23 label: OnceLock<SharedString>,23 label: OnceLock<SharedString>,
24 stack_trace_view: OnceCell<Entity<StackTraceView>>,24 stack_trace_view: OnceCell<Entity<StackTraceView>>,
25 _debug_panel: WeakEntity<DebugPanel>,
26 _worktree_store: WeakEntity<WorktreeStore>,25 _worktree_store: WeakEntity<WorktreeStore>,
27 workspace: WeakEntity<Workspace>,26 workspace: WeakEntity<Workspace>,
28 _subscriptions: [Subscription; 1],27 _subscriptions: [Subscription; 1],
@@ -38,8 +37,8 @@ impl DebugSession { · 9 unmodified lines
38 pub(crate) fn running(37 pub(crate) fn running(
39 project: Entity<Project>,38 project: Entity<Project>,
40 workspace: WeakEntity<Workspace>,39 workspace: WeakEntity<Workspace>,
40 parent_terminal: Option<Entity<DebugTerminal>>,
41 session: Entity<Session>,41 session: Entity<Session>,
42 _debug_panel: WeakEntity<DebugPanel>,
43 serialized_layout: Option<SerializedLayout>,42 serialized_layout: Option<SerializedLayout>,
44 dock_axis: Axis,43 dock_axis: Axis,
45 window: &mut Window,44 window: &mut Window,
@@ -50,6 +49,7 @@ impl DebugSession { · 4 unmodified lines
50 session.clone(),49 session.clone(),
51 project.clone(),50 project.clone(),
52 workspace.clone(),51 workspace.clone(),
52 parent_terminal,
53 serialized_layout,53 serialized_layout,
54 dock_axis,54 dock_axis,
55 window,55 window,
@@ -64,7 +64,6 @@ impl DebugSession { · 8 unmodified lines
64 remote_id: None,64 remote_id: None,
65 running_state,65 running_state,
66 label: OnceLock::new(),66 label: OnceLock::new(),
67 _debug_panel,
68 stack_trace_view: OnceCell::new(),67 stack_trace_view: OnceCell::new(),
69 _worktree_store: project.read(cx).worktree_store().downgrade(),68 _worktree_store: project.read(cx).worktree_store().downgrade(),
70 workspace,69 workspace,
crates/debugger_ui/src/session/running.rs+35−30
@@ -605,6 +605,7 @@ impl RunningState { · 604 unmodified lines
605 session: Entity<Session>,605 session: Entity<Session>,
606 project: Entity<Project>,606 project: Entity<Project>,
607 workspace: WeakEntity<Workspace>,607 workspace: WeakEntity<Workspace>,
608 parent_terminal: Option<Entity<DebugTerminal>>,
608 serialized_pane_layout: Option<SerializedLayout>,609 serialized_pane_layout: Option<SerializedLayout>,
609 dock_axis: Axis,610 dock_axis: Axis,
610 window: &mut Window,611 window: &mut Window,
@@ -617,7 +618,8 @@ impl RunningState { · 6 unmodified lines
617 StackFrameList::new(workspace.clone(), session.clone(), weak_state, window, cx)618 StackFrameList::new(workspace.clone(), session.clone(), weak_state, window, cx)
618 });619 });
619 620
620 let debug_terminal = cx.new(|cx| DebugTerminal::empty(window, cx));621 let debug_terminal =
622 parent_terminal.unwrap_or_else(|| cx.new(|cx| DebugTerminal::empty(window, cx)));
621 623
622 let variable_list =624 let variable_list =
623 cx.new(|cx| VariableList::new(session.clone(), stack_frame_list.clone(), window, cx));625 cx.new(|cx| VariableList::new(session.clone(), stack_frame_list.clone(), window, cx));
@@ -816,20 +818,20 @@ impl RunningState { · 192 unmodified lines
816 818
817 let request_type = dap_registry819 let request_type = dap_registry
818 .adapter(&adapter)820 .adapter(&adapter)
819 .ok_or_else(|| anyhow!("{}: is not a valid adapter name", &adapter))821 .with_context(|| format!("{}: is not a valid adapter name", &adapter))
820 .and_then(|adapter| adapter.request_kind(&config));822 .and_then(|adapter| adapter.request_kind(&config));
821 823
822 let config_is_valid = request_type.is_ok();824 let config_is_valid = request_type.is_ok();
823 825
824 let build_output = if let Some(build) = build {826 let build_output = if let Some(build) = build {
825 let (task, locator_name) = match build {827 let (task_template, locator_name) = match build {
826 BuildTaskDefinition::Template {828 BuildTaskDefinition::Template {
827 task_template,829 task_template,
828 locator_name,830 locator_name,
829 } => (task_template, locator_name),831 } => (task_template, locator_name),
830 BuildTaskDefinition::ByName(ref label) => {832 BuildTaskDefinition::ByName(ref label) => {
831 let Some(task) = task_store.update(cx, |this, cx| {833 let task = task_store.update(cx, |this, cx| {
832 this.task_inventory().and_then(|inventory| {834 this.task_inventory().map(|inventory| {
833 inventory.read(cx).task_template_by_label(835 inventory.read(cx).task_template_by_label(
834 buffer,836 buffer,
835 worktree_id,837 worktree_id,
@@ -837,14 +839,15 @@ impl RunningState { · 1 unmodified line
837 cx,839 cx,
838 )840 )
839 })841 })
840 })?842 })?;
841 else {843 let task = match task {
842 anyhow::bail!("Couldn't find task template for {:?}", build)844 Some(task) => task.await,
843 };845 None => None,
846 }.with_context(|| format!("Couldn't find task template for {build:?}"))?;
844 (task, None)847 (task, None)
845 }848 }
846 };849 };
847 let Some(task) = task.resolve_task("debug-build-task", &task_context) else {850 let Some(task) = task_template.resolve_task("debug-build-task", &task_context) else {
848 anyhow::bail!("Could not resolve task variables within a debug scenario");851 anyhow::bail!("Could not resolve task variables within a debug scenario");
849 };852 };
850 853
@@ -852,7 +855,7 @@ impl RunningState { · 1 unmodified line
852 debug_assert!(!config_is_valid);855 debug_assert!(!config_is_valid);
853 Some(locator_name)856 Some(locator_name)
854 } else if !config_is_valid {857 } else if !config_is_valid {
855 dap_store858 let task = dap_store
856 .update(cx, |this, cx| {859 .update(cx, |this, cx| {
857 this.debug_scenario_for_build_task(860 this.debug_scenario_for_build_task(
858 task.original_task().clone(),861 task.original_task().clone(),
@@ -860,17 +863,21 @@ impl RunningState { · 1 unmodified line
860 task.display_label().to_owned().into(),863 task.display_label().to_owned().into(),
861 cx,864 cx,
862 )865 )
863 .and_then(|scenario| {866
864 match scenario.build {867 });
865 Some(BuildTaskDefinition::Template {868 if let Ok(t) = task {
866 locator_name, ..869 t.await.and_then(|scenario| {
867 }) => locator_name,870 match scenario.build {
868 _ => None,871 Some(BuildTaskDefinition::Template {
869 }872 locator_name, ..
870 })873 }) => locator_name,
874 _ => None,
875 }
871 })876 })
872 .ok()877 } else {
873 .flatten()878 None
879 }
880
874 } else {881 } else {
875 None882 None
876 };883 };
@@ -929,15 +936,13 @@ impl RunningState { · 52 unmodified lines
929 };936 };
930 937
931 if config_is_valid {938 if config_is_valid {
932 // Ok(DebugTaskDefinition {
933 // label,
934 // adapter: DebugAdapterName(adapter),
935 // config,
936 // tcp_connection,
937 // })
938 } else if let Some((task, locator_name)) = build_output {939 } else if let Some((task, locator_name)) = build_output {
939 let locator_name =940 let locator_name =
940 locator_name.context("Could not find a valid locator for a build task")?;941 locator_name.with_context(|| {
942 format!("Could not find a valid locator for a build task and configure is invalid with error: {}", request_type.err()
943 .map(|err| err.to_string())
944 .unwrap_or_default())
945 })?;
941 let request = dap_store946 let request = dap_store
942 .update(cx, |this, cx| {947 .update(cx, |this, cx| {
943 this.run_debug_locator(&locator_name, task, cx)948 this.run_debug_locator(&locator_name, task, cx)
@@ -953,7 +958,7 @@ impl RunningState { · 9 unmodified lines
953 958
954 let scenario = dap_registry959 let scenario = dap_registry
955 .adapter(&adapter)960 .adapter(&adapter)
956 .ok_or_else(|| anyhow!("{}: is not a valid adapter name", &adapter))961 .with_context(|| anyhow!("{}: is not a valid adapter name", &adapter))
957 .map(|adapter| adapter.config_from_zed_format(zed_config))??;962 .map(|adapter| adapter.config_from_zed_format(zed_config))??;
958 config = scenario.config;963 config = scenario.config;
959 Self::substitute_variables_in_config(&mut config, &task_context);964 Self::substitute_variables_in_config(&mut config, &task_context);
@@ -992,7 +997,7 @@ impl RunningState { · 32 unmodified lines
992 let cwd = Some(&request.cwd)997 let cwd = Some(&request.cwd)
993 .filter(|cwd| cwd.len() > 0)998 .filter(|cwd| cwd.len() > 0)
994 .map(PathBuf::from)999 .map(PathBuf::from)
995 .or_else(|| session.binary().cwd.clone());1000 .or_else(|| session.binary().unwrap().cwd.clone());
996 1001
997 let mut args = request.args.clone();1002 let mut args = request.args.clone();
998 1003
crates/debugger_ui/src/session/running/breakpoint_list.rs+3−3
@@ -1,4 +1,5 @@
1use std::{1use std::{
2 ops::Range,
2 path::{Path, PathBuf},3 path::{Path, PathBuf},
3 sync::Arc,4 sync::Arc,
4 time::Duration,5 time::Duration,
@@ -277,10 +278,9 @@ impl BreakpointList { · 272 unmodified lines
277 let selected_ix = self.selected_ix;278 let selected_ix = self.selected_ix;
278 let focus_handle = self.focus_handle.clone();279 let focus_handle = self.focus_handle.clone();
279 uniform_list(280 uniform_list(
280 cx.entity(),
281 "breakpoint-list",281 "breakpoint-list",
282 self.breakpoints.len(),282 self.breakpoints.len(),
283 move |this, range, window, cx| {283 cx.processor(move |this, range: Range<usize>, window, cx| {
284 range284 range
285 .clone()285 .clone()
286 .zip(&mut this.breakpoints[range])286 .zip(&mut this.breakpoints[range])
@@ -291,7 +291,7 @@ impl BreakpointList { · 4 unmodified lines
291 .into_any_element()291 .into_any_element()
292 })292 })
293 .collect()293 .collect()
294 },294 }),
295 )295 )
296 .track_scroll(self.scroll_handle.clone())296 .track_scroll(self.scroll_handle.clone())
297 .flex_grow()297 .flex_grow()
crates/debugger_ui/src/session/running/module_list.rs+16−12
@@ -8,7 +8,7 @@ use project::{ · 7 unmodified lines
8 ProjectItem as _, ProjectPath,8 ProjectItem as _, ProjectPath,
9 debugger::session::{Session, SessionEvent},9 debugger::session::{Session, SessionEvent},
10};10};
11use std::{path::Path, sync::Arc};11use std::{ops::Range, path::Path, sync::Arc};
12use ui::{Scrollbar, ScrollbarState, prelude::*};12use ui::{Scrollbar, ScrollbarState, prelude::*};
13use workspace::Workspace;13use workspace::Workspace;
14 14
@@ -20,7 +20,7 @@ pub struct ModuleList { · 5 unmodified lines
20 focus_handle: FocusHandle,20 focus_handle: FocusHandle,
21 scrollbar_state: ScrollbarState,21 scrollbar_state: ScrollbarState,
22 entries: Vec<Module>,22 entries: Vec<Module>,
23 _rebuild_task: Task<()>,23 _rebuild_task: Option<Task<()>>,
24 _subscription: Subscription,24 _subscription: Subscription,
25}25}
26 26
@@ -34,14 +34,16 @@ impl ModuleList { · 7 unmodified lines
34 34
35 let _subscription = cx.subscribe(&session, |this, _, event, cx| match event {35 let _subscription = cx.subscribe(&session, |this, _, event, cx| match event {
36 SessionEvent::Stopped(_) | SessionEvent::Modules => {36 SessionEvent::Stopped(_) | SessionEvent::Modules => {
37 this.schedule_rebuild(cx);37 if this._rebuild_task.is_some() {
38 this.schedule_rebuild(cx);
39 }
38 }40 }
39 _ => {}41 _ => {}
40 });42 });
41 43
42 let scroll_handle = UniformListScrollHandle::new();44 let scroll_handle = UniformListScrollHandle::new();
43 45
44 let mut this = Self {46 Self {
45 scrollbar_state: ScrollbarState::new(scroll_handle.clone()),47 scrollbar_state: ScrollbarState::new(scroll_handle.clone()),
46 scroll_handle,48 scroll_handle,
47 session,49 session,
@@ -50,14 +52,12 @@ impl ModuleList { · 2 unmodified lines
50 entries: Vec::new(),52 entries: Vec::new(),
51 selected_ix: None,53 selected_ix: None,
52 _subscription,54 _subscription,
53 _rebuild_task: Task::ready(()),55 _rebuild_task: None,
54 };56 }
55 this.schedule_rebuild(cx);
56 this
57 }57 }
58 58
59 fn schedule_rebuild(&mut self, cx: &mut Context<Self>) {59 fn schedule_rebuild(&mut self, cx: &mut Context<Self>) {
60 self._rebuild_task = cx.spawn(async move |this, cx| {60 self._rebuild_task = Some(cx.spawn(async move |this, cx| {
61 this.update(cx, |this, cx| {61 this.update(cx, |this, cx| {
62 let modules = this62 let modules = this
63 .session63 .session
@@ -66,7 +66,7 @@ impl ModuleList { · 2 unmodified lines
66 cx.notify();66 cx.notify();
67 })67 })
68 .ok();68 .ok();
69 });69 }));
70 }70 }
71 71
72 fn open_module(&mut self, path: Arc<Path>, window: &mut Window, cx: &mut Context<Self>) {72 fn open_module(&mut self, path: Arc<Path>, window: &mut Window, cx: &mut Context<Self>) {
@@ -281,10 +281,11 @@ impl ModuleList { · 208 unmodified lines
281 281
282 fn render_list(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {282 fn render_list(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
283 uniform_list(283 uniform_list(
284 cx.entity(),
285 "module-list",284 "module-list",
286 self.entries.len(),285 self.entries.len(),
287 |this, range, _window, cx| range.map(|ix| this.render_entry(ix, cx)).collect(),286 cx.processor(|this, range: Range<usize>, _window, cx| {
287 range.map(|ix| this.render_entry(ix, cx)).collect()
288 }),
288 )289 )
289 .track_scroll(self.scroll_handle.clone())290 .track_scroll(self.scroll_handle.clone())
290 .size_full()291 .size_full()
@@ -299,6 +300,9 @@ impl Focusable for ModuleList { · 8 unmodified lines
299 300
300impl Render for ModuleList {301impl Render for ModuleList {
301 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {302 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
303 if self._rebuild_task.is_none() {
304 self.schedule_rebuild(cx);
305 }
302 div()306 div()
303 .track_focus(&self.focus_handle)307 .track_focus(&self.focus_handle)
304 .on_action(cx.listener(Self::select_last))308 .on_action(cx.listener(Self::select_last))
crates/debugger_ui/src/session/running/stack_frame_list.rs+130−53
@@ -5,9 +5,10 @@ use std::time::Duration; · 4 unmodified lines
5use anyhow::{Context as _, Result, anyhow};5use anyhow::{Context as _, Result, anyhow};
6use dap::StackFrameId;6use dap::StackFrameId;
7use gpui::{7use gpui::{
8 AnyElement, Entity, EventEmitter, FocusHandle, Focusable, MouseButton, ScrollStrategy,8 AnyElement, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, ListState, MouseButton,
9 Stateful, Subscription, Task, UniformListScrollHandle, WeakEntity, uniform_list,9 Stateful, Subscription, Task, WeakEntity, list,
10};10};
11use util::debug_panic;
11 12
12use crate::StackTraceView;13use crate::StackTraceView;
13use language::PointUtf16;14use language::PointUtf16;
@@ -35,13 +36,16 @@ pub struct StackFrameList { · 21 unmodified lines
35 selected_ix: Option<usize>,36 selected_ix: Option<usize>,
36 opened_stack_frame_id: Option<StackFrameId>,37 opened_stack_frame_id: Option<StackFrameId>,
37 scrollbar_state: ScrollbarState,38 scrollbar_state: ScrollbarState,
38 scroll_handle: UniformListScrollHandle,39 list_state: ListState,
40 error: Option<SharedString>,
39 _refresh_task: Task<()>,41 _refresh_task: Task<()>,
40}42}
41 43
42#[derive(Debug, PartialEq, Eq)]44#[derive(Debug, PartialEq, Eq)]
43pub enum StackFrameEntry {45pub enum StackFrameEntry {
44 Normal(dap::StackFrame),46 Normal(dap::StackFrame),
47 /// Used to indicate that the frame is artificial and is a visual label or separator
48 Label(dap::StackFrame),
45 Collapsed(Vec<dap::StackFrame>),49 Collapsed(Vec<dap::StackFrame>),
46}50}
47 51
@@ -54,7 +58,6 @@ impl StackFrameList { · 6 unmodified lines
54 cx: &mut Context<Self>,58 cx: &mut Context<Self>,
55 ) -> Self {59 ) -> Self {
56 let focus_handle = cx.focus_handle();60 let focus_handle = cx.focus_handle();
57 let scroll_handle = UniformListScrollHandle::new();
58 61
59 let _subscription =62 let _subscription =
60 cx.subscribe_in(&session, window, |this, _, event, window, cx| match event {63 cx.subscribe_in(&session, window, |this, _, event, window, cx| match event {
@@ -67,17 +70,27 @@ impl StackFrameList { · 6 unmodified lines
67 _ => {}70 _ => {}
68 });71 });
69 72
73 let list_state = ListState::new(0, gpui::ListAlignment::Top, px(1000.), {
74 let this = cx.weak_entity();
75 move |ix, _window, cx| {
76 this.update(cx, |this, cx| this.render_entry(ix, cx))
77 .unwrap_or(div().into_any())
78 }
79 });
80 let scrollbar_state = ScrollbarState::new(list_state.clone());
81
70 let mut this = Self {82 let mut this = Self {
71 scrollbar_state: ScrollbarState::new(scroll_handle.clone()),
72 session,83 session,
73 workspace,84 workspace,
74 focus_handle,85 focus_handle,
75 state,86 state,
76 _subscription,87 _subscription,
77 entries: Default::default(),88 entries: Default::default(),
89 error: None,
78 selected_ix: None,90 selected_ix: None,
79 opened_stack_frame_id: None,91 opened_stack_frame_id: None,
80 scroll_handle,92 list_state,
93 scrollbar_state,
81 _refresh_task: Task::ready(()),94 _refresh_task: Task::ready(()),
82 };95 };
83 this.schedule_refresh(true, window, cx);96 this.schedule_refresh(true, window, cx);
@@ -89,37 +102,35 @@ impl StackFrameList { · 5 unmodified lines
89 &self.entries102 &self.entries
90 }103 }
91 104
92 pub(crate) fn flatten_entries(&self, show_collapsed: bool) -> Vec<dap::StackFrame> {105 pub(crate) fn flatten_entries(
106 &self,
107 show_collapsed: bool,
108 show_labels: bool,
109 ) -> Vec<dap::StackFrame> {
93 self.entries110 self.entries
94 .iter()111 .iter()
95 .flat_map(|frame| match frame {112 .flat_map(|frame| match frame {
96 StackFrameEntry::Normal(frame) => vec![frame.clone()],113 StackFrameEntry::Normal(frame) => vec![frame.clone()],
97 StackFrameEntry::Collapsed(frames) => {114 StackFrameEntry::Label(frame) if show_labels => vec![frame.clone()],
98 if show_collapsed {115 StackFrameEntry::Collapsed(frames) if show_collapsed => frames.clone(),
99 frames.clone()116 _ => vec![],
100 } else {
101 vec![]
102 }
103 }
104 })117 })
105 .collect::<Vec<_>>()118 .collect::<Vec<_>>()
106 }119 }
107 120
108 fn stack_frames(&self, cx: &mut App) -> Vec<StackFrame> {121 fn stack_frames(&self, cx: &mut App) -> Result<Vec<StackFrame>> {
109 self.state122 if let Ok(Some(thread_id)) = self.state.read_with(cx, |state, _| state.thread_id) {
110 .read_with(cx, |state, _| state.thread_id)123 self.session
111 .ok()124 .update(cx, |this, cx| this.stack_frames(thread_id, cx))
112 .flatten()125 } else {
113 .map(|thread_id| {126 Ok(Vec::default())
114 self.session127 }
115 .update(cx, |this, cx| this.stack_frames(thread_id, cx))
116 })
117 .unwrap_or_default()
118 }128 }
119 129
120 #[cfg(test)]130 #[cfg(test)]
121 pub(crate) fn dap_stack_frames(&self, cx: &mut App) -> Vec<dap::StackFrame> {131 pub(crate) fn dap_stack_frames(&self, cx: &mut App) -> Vec<dap::StackFrame> {
122 self.stack_frames(cx)132 self.stack_frames(cx)
133 .unwrap_or_default()
123 .into_iter()134 .into_iter()
124 .map(|stack_frame| stack_frame.dap.clone())135 .map(|stack_frame| stack_frame.dap.clone())
125 .collect()136 .collect()
@@ -141,7 +152,7 @@ impl StackFrameList { · 15 unmodified lines
141 let debounce = this152 let debounce = this
142 .update(cx, |this, cx| {153 .update(cx, |this, cx| {
143 let new_stack_frames = this.stack_frames(cx);154 let new_stack_frames = this.stack_frames(cx);
144 new_stack_frames.is_empty() && !this.entries.is_empty()155 new_stack_frames.unwrap_or_default().is_empty() && !this.entries.is_empty()
145 })156 })
146 .ok()157 .ok()
147 .unwrap_or_default();158 .unwrap_or_default();
@@ -168,20 +179,34 @@ impl StackFrameList { · 20 unmodified lines
168 .and_then(|ix| self.entries.get(ix))179 .and_then(|ix| self.entries.get(ix))
169 .and_then(|entry| match entry {180 .and_then(|entry| match entry {
170 StackFrameEntry::Normal(stack_frame) => Some(stack_frame.id),181 StackFrameEntry::Normal(stack_frame) => Some(stack_frame.id),
171 StackFrameEntry::Collapsed(stack_frames) => {182 StackFrameEntry::Collapsed(_) | StackFrameEntry::Label(_) => None,
172 stack_frames.first().map(|stack_frame| stack_frame.id)
173 }
174 });183 });
175 let mut entries = Vec::new();184 let mut entries = Vec::new();
176 let mut collapsed_entries = Vec::new();185 let mut collapsed_entries = Vec::new();
177 let mut first_stack_frame = None;186 let mut first_stack_frame = None;
178 187 let mut first_stack_frame_with_path = None;
179 let stack_frames = self.stack_frames(cx);188
189 let stack_frames = match self.stack_frames(cx) {
190 Ok(stack_frames) => stack_frames,
191 Err(e) => {
192 self.error = Some(format!("{}", e).into());
193 self.entries.clear();
194 self.selected_ix = None;
195 self.list_state.reset(0);
196 cx.emit(StackFrameListEvent::BuiltEntries);
197 cx.notify();
198 return;
199 }
200 };
180 for stack_frame in &stack_frames {201 for stack_frame in &stack_frames {
181 match stack_frame.dap.presentation_hint {202 match stack_frame.dap.presentation_hint {
182 Some(dap::StackFramePresentationHint::Deemphasize) => {203 Some(dap::StackFramePresentationHint::Deemphasize)
204 | Some(dap::StackFramePresentationHint::Subtle) => {
183 collapsed_entries.push(stack_frame.dap.clone());205 collapsed_entries.push(stack_frame.dap.clone());
184 }206 }
207 Some(dap::StackFramePresentationHint::Label) => {
208 entries.push(StackFrameEntry::Label(stack_frame.dap.clone()));
209 }
185 _ => {210 _ => {
186 let collapsed_entries = std::mem::take(&mut collapsed_entries);211 let collapsed_entries = std::mem::take(&mut collapsed_entries);
187 if !collapsed_entries.is_empty() {212 if !collapsed_entries.is_empty() {
@@ -189,6 +214,15 @@ impl StackFrameList { · 1 unmodified line
189 }214 }
190 215
191 first_stack_frame.get_or_insert(entries.len());216 first_stack_frame.get_or_insert(entries.len());
217
218 if stack_frame
219 .dap
220 .source
221 .as_ref()
222 .is_some_and(|source| source.path.is_some())
223 {
224 first_stack_frame_with_path.get_or_insert(entries.len());
225 }
192 entries.push(StackFrameEntry::Normal(stack_frame.dap.clone()));226 entries.push(StackFrameEntry::Normal(stack_frame.dap.clone()));
193 }227 }
194 }228 }
@@ -198,22 +232,23 @@ impl StackFrameList { · 3 unmodified lines
198 if !collapsed_entries.is_empty() {232 if !collapsed_entries.is_empty() {
199 entries.push(StackFrameEntry::Collapsed(collapsed_entries.clone()));233 entries.push(StackFrameEntry::Collapsed(collapsed_entries.clone()));
200 }234 }
235 self.entries = entries;
201 236
202 std::mem::swap(&mut self.entries, &mut entries);237 if let Some(ix) = first_stack_frame_with_path
203 238 .or(first_stack_frame)
204 if let Some(ix) = first_stack_frame.filter(|_| open_first_stack_frame) {239 .filter(|_| open_first_stack_frame)
240 {
205 self.select_ix(Some(ix), cx);241 self.select_ix(Some(ix), cx);
206 self.activate_selected_entry(window, cx);242 self.activate_selected_entry(window, cx);
207 } else if let Some(old_selected_frame_id) = old_selected_frame_id {243 } else if let Some(old_selected_frame_id) = old_selected_frame_id {
208 let ix = self.entries.iter().position(|entry| match entry {244 let ix = self.entries.iter().position(|entry| match entry {
209 StackFrameEntry::Normal(frame) => frame.id == old_selected_frame_id,245 StackFrameEntry::Normal(frame) => frame.id == old_selected_frame_id,
210 StackFrameEntry::Collapsed(frames) => {246 StackFrameEntry::Collapsed(_) | StackFrameEntry::Label(_) => false,
211 frames.iter().any(|frame| frame.id == old_selected_frame_id)
212 }
213 });247 });
214 self.selected_ix = ix;248 self.selected_ix = ix;
215 }249 }
216 250
251 self.list_state.reset(self.entries.len());
217 cx.emit(StackFrameListEvent::BuiltEntries);252 cx.emit(StackFrameListEvent::BuiltEntries);
218 cx.notify();253 cx.notify();
219 }254 }
@@ -228,6 +263,7 @@ impl StackFrameList { · 8 unmodified lines
228 .entries263 .entries
229 .iter()264 .iter()
230 .flat_map(|entry| match entry {265 .flat_map(|entry| match entry {
266 StackFrameEntry::Label(stack_frame) => std::slice::from_ref(stack_frame),
231 StackFrameEntry::Normal(stack_frame) => std::slice::from_ref(stack_frame),267 StackFrameEntry::Normal(stack_frame) => std::slice::from_ref(stack_frame),
232 StackFrameEntry::Collapsed(stack_frames) => stack_frames.as_slice(),268 StackFrameEntry::Collapsed(stack_frames) => stack_frames.as_slice(),
233 })269 })
@@ -340,6 +376,33 @@ impl StackFrameList { · 106 unmodified lines
340 });376 });
341 }377 }
342 378
379 fn render_label_entry(
380 &self,
381 stack_frame: &dap::StackFrame,
382 _cx: &mut Context<Self>,
383 ) -> AnyElement {
384 h_flex()
385 .rounded_md()
386 .justify_between()
387 .w_full()
388 .group("")
389 .id(("label-stack-frame", stack_frame.id))
390 .p_1()
391 .on_any_mouse_down(|_, _, cx| {
392 cx.stop_propagation();
393 })
394 .child(
395 v_flex().justify_center().gap_0p5().child(
396 Label::new(stack_frame.name.clone())
397 .size(LabelSize::Small)
398 .weight(FontWeight::BOLD)
399 .truncate()
400 .color(Color::Info),
401 ),
402 )
403 .into_any()
404 }
405
343 fn render_normal_entry(406 fn render_normal_entry(
344 &self,407 &self,
345 ix: usize,408 ix: usize,
@@ -443,7 +506,7 @@ impl StackFrameList { · 97 unmodified lines
443 .into_any()506 .into_any()
444 }507 }
445 508
446 pub(crate) fn expand_collapsed_entry(&mut self, ix: usize) {509 pub(crate) fn expand_collapsed_entry(&mut self, ix: usize, cx: &mut Context<Self>) {
447 let Some(StackFrameEntry::Collapsed(stack_frames)) = self.entries.get_mut(ix) else {510 let Some(StackFrameEntry::Collapsed(stack_frames)) = self.entries.get_mut(ix) else {
448 return;511 return;
449 };512 };
@@ -452,6 +515,9 @@ impl StackFrameList { · 2 unmodified lines
452 .map(StackFrameEntry::Normal);515 .map(StackFrameEntry::Normal);
453 self.entries.splice(ix..ix + 1, entries);516 self.entries.splice(ix..ix + 1, entries);
454 self.selected_ix = Some(ix);517 self.selected_ix = Some(ix);
518 self.list_state.reset(self.entries.len());
519 cx.emit(StackFrameListEvent::BuiltEntries);
520 cx.notify();
455 }521 }
456 522
457 fn render_collapsed_entry(523 fn render_collapsed_entry(
@@ -501,6 +567,7 @@ impl StackFrameList { · 43 unmodified lines
501 567
502 fn render_entry(&self, ix: usize, cx: &mut Context<Self>) -> AnyElement {568 fn render_entry(&self, ix: usize, cx: &mut Context<Self>) -> AnyElement {
503 match &self.entries[ix] {569 match &self.entries[ix] {
570 StackFrameEntry::Label(stack_frame) => self.render_label_entry(stack_frame, cx),
504 StackFrameEntry::Normal(stack_frame) => self.render_normal_entry(ix, stack_frame, cx),571 StackFrameEntry::Normal(stack_frame) => self.render_normal_entry(ix, stack_frame, cx),
505 StackFrameEntry::Collapsed(stack_frames) => {572 StackFrameEntry::Collapsed(stack_frames) => {
506 self.render_collapsed_entry(ix, stack_frames, cx)573 self.render_collapsed_entry(ix, stack_frames, cx)
@@ -543,10 +610,6 @@ impl StackFrameList { · 36 unmodified lines
543 610
544 fn select_ix(&mut self, ix: Option<usize>, cx: &mut Context<Self>) {611 fn select_ix(&mut self, ix: Option<usize>, cx: &mut Context<Self>) {
545 self.selected_ix = ix;612 self.selected_ix = ix;
546 if let Some(ix) = self.selected_ix {
547 self.scroll_handle
548 .scroll_to_item(ix, ScrollStrategy::Center);
549 }
550 cx.notify();613 cx.notify();
551 }614 }
552 615
@@ -621,7 +684,10 @@ impl StackFrameList { · 68 unmodified lines
621 self.go_to_stack_frame_inner(stack_frame, window, cx)684 self.go_to_stack_frame_inner(stack_frame, window, cx)
622 .detach_and_log_err(cx)685 .detach_and_log_err(cx)
623 }686 }
624 StackFrameEntry::Collapsed(_) => self.expand_collapsed_entry(ix),687 StackFrameEntry::Label(_) => {
688 debug_panic!("You should not be able to select a label stack frame")
689 }
690 StackFrameEntry::Collapsed(_) => self.expand_collapsed_entry(ix, cx),
625 }691 }
626 cx.notify();692 cx.notify();
627 }693 }
@@ -630,15 +696,11 @@ impl StackFrameList { · 2 unmodified lines
630 self.activate_selected_entry(window, cx);696 self.activate_selected_entry(window, cx);
631 }697 }
632 698
633 fn render_list(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {699 fn render_list(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
634 uniform_list(700 div()
635 cx.entity(),701 .p_1()
636 "stack-frame-list",702 .size_full()
637 self.entries.len(),703 .child(list(self.list_state.clone()).size_full())
638 |this, range, _window, cx| range.map(|ix| this.render_entry(ix, cx)).collect(),
639 )
640 .track_scroll(self.scroll_handle.clone())
641 .size_full()
642 }704 }
643}705}
644 706
@@ -647,12 +709,27 @@ impl Render for StackFrameList { · 2 unmodified lines
647 div()709 div()
648 .track_focus(&self.focus_handle)710 .track_focus(&self.focus_handle)
649 .size_full()711 .size_full()
650 .p_1()
651 .on_action(cx.listener(Self::select_next))712 .on_action(cx.listener(Self::select_next))
652 .on_action(cx.listener(Self::select_previous))713 .on_action(cx.listener(Self::select_previous))
653 .on_action(cx.listener(Self::select_first))714 .on_action(cx.listener(Self::select_first))
654 .on_action(cx.listener(Self::select_last))715 .on_action(cx.listener(Self::select_last))
655 .on_action(cx.listener(Self::confirm))716 .on_action(cx.listener(Self::confirm))
717 .when_some(self.error.clone(), |el, error| {
718 el.child(
719 h_flex()
720 .bg(cx.theme().status().warning_background)
721 .border_b_1()
722 .border_color(cx.theme().status().warning_border)
723 .pl_1()
724 .child(Icon::new(IconName::Warning).color(Color::Warning))
725 .gap_2()
726 .child(
727 Label::new(error)
728 .size(LabelSize::Small)
729 .color(Color::Warning),
730 ),
731 )
732 })
656 .child(self.render_list(window, cx))733 .child(self.render_list(window, cx))
657 .child(self.render_vertical_scrollbar(cx))734 .child(self.render_vertical_scrollbar(cx))
658 }735 }
crates/debugger_ui/src/session/running/variable_list.rs+3−2
@@ -980,10 +980,11 @@ impl Render for VariableList { · 979 unmodified lines
980 .on_action(cx.listener(Self::edit_variable))980 .on_action(cx.listener(Self::edit_variable))
981 .child(981 .child(
982 uniform_list(982 uniform_list(
983 cx.entity().clone(),
984 "variable-list",983 "variable-list",
985 self.entries.len(),984 self.entries.len(),
986 move |this, range, window, cx| this.render_entries(range, window, cx),985 cx.processor(move |this, range: Range<usize>, window, cx| {
986 this.render_entries(range, window, cx)
987 }),
987 )988 )
988 .track_scroll(self.list_handle.clone())989 .track_scroll(self.list_handle.clone())
989 .gap_1_5()990 .gap_1_5()
crates/debugger_ui/src/stack_trace_view.rs+5−5
@@ -16,7 +16,7 @@ use ui::{ActiveTheme as _, Context, ParentElement as _, Styled as _, div}; · 15 unmodified lines
16use util::ResultExt as _;16use util::ResultExt as _;
17use workspace::{17use workspace::{
18 Item, ItemHandle as _, ItemNavHistory, ToolbarItemLocation, Workspace,18 Item, ItemHandle as _, ItemNavHistory, ToolbarItemLocation, Workspace,
19 item::{BreadcrumbText, ItemEvent},19 item::{BreadcrumbText, ItemEvent, SaveOptions},
20 searchable::SearchableItemHandle,20 searchable::SearchableItemHandle,
21};21};
22 22
@@ -148,7 +148,7 @@ impl StackTraceView { · 125 unmodified lines
148 148
149 let stack_frames = self149 let stack_frames = self
150 .stack_frame_list150 .stack_frame_list
151 .read_with(cx, |list, _| list.flatten_entries(false));151 .read_with(cx, |list, _| list.flatten_entries(false, false));
152 152
153 let frames_to_open: Vec<_> = stack_frames153 let frames_to_open: Vec<_> = stack_frames
154 .into_iter()154 .into_iter()
@@ -237,7 +237,7 @@ impl StackTraceView { · 82 unmodified lines
237 237
238 let stack_frames = self238 let stack_frames = self
239 .stack_frame_list239 .stack_frame_list
240 .read_with(cx, |session, _| session.flatten_entries(false));240 .read_with(cx, |session, _| session.flatten_entries(false, false));
241 241
242 let active_idx = self242 let active_idx = self
243 .selected_stack_frame_id243 .selected_stack_frame_id
@@ -386,12 +386,12 @@ impl Item for StackTraceView { · 142 unmodified lines
386 386
387 fn save(387 fn save(
388 &mut self,388 &mut self,
389 format: bool,389 options: SaveOptions,
390 project: Entity<Project>,390 project: Entity<Project>,
391 window: &mut Window,391 window: &mut Window,
392 cx: &mut Context<Self>,392 cx: &mut Context<Self>,
393 ) -> Task<Result<()>> {393 ) -> Task<Result<()>> {
394 self.editor.save(format, project, window, cx)394 self.editor.save(options, project, window, cx)
395 }395 }
396 396
397 fn save_as(397 fn save_as(
crates/debugger_ui/src/tests/debugger_panel.rs+24−8
@@ -33,6 +33,7 @@ use std::{ · 32 unmodified lines
33use terminal_view::terminal_panel::TerminalPanel;33use terminal_view::terminal_panel::TerminalPanel;
34use tests::{active_debug_session_panel, init_test, init_test_workspace};34use tests::{active_debug_session_panel, init_test, init_test_workspace};
35use util::path;35use util::path;
36use workspace::item::SaveOptions;
36use workspace::{Item, dock::Panel};37use workspace::{Item, dock::Panel};
37 38
38#[gpui::test]39#[gpui::test]
@@ -443,18 +444,20 @@ async fn test_handle_start_debugging_request( · 404 unmodified lines
443 .update(cx, |workspace, _window, cx| {444 .update(cx, |workspace, _window, cx| {
444 let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();445 let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
445 446
446 // Active session does not change on spawn.447 // Active session changes on spawn, as the parent has never stopped.
447 let active_session = debug_panel448 let active_session = debug_panel
448 .read(cx)449 .read(cx)
449 .active_session()450 .active_session()
450 .unwrap()451 .unwrap()
451 .read(cx)452 .read(cx)
452 .session(cx);453 .session(cx);
453
454 assert_eq!(active_session, sessions[0].read(cx).session(cx));
455 assert!(active_session.read(cx).parent_session().is_none());
456
457 let current_sessions = debug_panel.read(cx).sessions();454 let current_sessions = debug_panel.read(cx).sessions();
455 assert_eq!(active_session, current_sessions[1].read(cx).session(cx));
456 assert_eq!(
457 active_session.read(cx).parent_session(),
458 Some(&current_sessions[0].read(cx).session(cx))
459 );
460
458 assert_eq!(current_sessions.len(), 2);461 assert_eq!(current_sessions.len(), 2);
459 assert_eq!(current_sessions[0], sessions[0]);462 assert_eq!(current_sessions[0], sessions[0]);
460 463
@@ -468,14 +471,19 @@ async fn test_handle_start_debugging_request( · 7 unmodified lines
468 471
469 // We should preserve the original binary (params to spawn process etc.) except for launch params472 // We should preserve the original binary (params to spawn process etc.) except for launch params
470 // (as they come from reverse spawn request).473 // (as they come from reverse spawn request).
471 let mut original_binary = parent_session.read(cx).binary().clone();474 let mut original_binary = parent_session.read(cx).binary().cloned().unwrap();
472 original_binary.request_args = StartDebuggingRequestArguments {475 original_binary.request_args = StartDebuggingRequestArguments {
473 request: StartDebuggingRequestArgumentsRequest::Launch,476 request: StartDebuggingRequestArgumentsRequest::Launch,
474 configuration: fake_config.clone(),477 configuration: fake_config.clone(),
475 };478 };
476 479
477 assert_eq!(480 assert_eq!(
478 current_sessions[1].read(cx).session(cx).read(cx).binary(),481 current_sessions[1]
482 .read(cx)
483 .session(cx)
484 .read(cx)
485 .binary()
486 .unwrap(),
479 &original_binary487 &original_binary
480 );488 );
481 })489 })
@@ -1213,7 +1221,15 @@ async fn test_send_breakpoints_when_editor_has_been_saved( · 731 unmodified lines
1213 1221
1214 editor1222 editor
1215 .update_in(cx, |editor, window, cx| {1223 .update_in(cx, |editor, window, cx| {
1216 editor.save(true, project.clone(), window, cx)1224 editor.save(
1225 SaveOptions {
1226 format: true,
1227 autosave: false,
1228 },
1229 project.clone(),
1230 window,
1231 cx,
1232 )
1217 })1233 })
1218 .await1234 .await
1219 .unwrap();1235 .unwrap();
crates/debugger_ui/src/tests/stack_frame_list.rs+2−2
@@ -718,7 +718,7 @@ async fn test_collapsed_entries(executor: BackgroundExecutor, cx: &mut TestAppCo · 717 unmodified lines
718 stack_frame_list.entries()718 stack_frame_list.entries()
719 );719 );
720 720
721 stack_frame_list.expand_collapsed_entry(1);721 stack_frame_list.expand_collapsed_entry(1, cx);
722 722
723 assert_eq!(723 assert_eq!(
724 &vec![724 &vec![
@@ -735,7 +735,7 @@ async fn test_collapsed_entries(executor: BackgroundExecutor, cx: &mut TestAppCo · 10 unmodified lines
735 stack_frame_list.entries()735 stack_frame_list.entries()
736 );736 );
737 737
738 stack_frame_list.expand_collapsed_entry(4);738 stack_frame_list.expand_collapsed_entry(4, cx);
739 739
740 assert_eq!(740 assert_eq!(
741 &vec![741 &vec![
crates/debugger_ui/src/tests/variable_list.rs+20−5
@@ -191,7 +191,10 @@ async fn test_basic_fetch_initial_scope_and_variables( · 190 unmodified lines
191 running_state.update(cx, |running_state, cx| {191 running_state.update(cx, |running_state, cx| {
192 let (stack_frame_list, stack_frame_id) =192 let (stack_frame_list, stack_frame_id) =
193 running_state.stack_frame_list().update(cx, |list, _| {193 running_state.stack_frame_list().update(cx, |list, _| {
194 (list.flatten_entries(true), list.opened_stack_frame_id())194 (
195 list.flatten_entries(true, true),
196 list.opened_stack_frame_id(),
197 )
195 });198 });
196 199
197 assert_eq!(stack_frames, stack_frame_list);200 assert_eq!(stack_frames, stack_frame_list);
@@ -432,7 +435,10 @@ async fn test_fetch_variables_for_multiple_scopes( · 234 unmodified lines
432 running_state.update(cx, |running_state, cx| {435 running_state.update(cx, |running_state, cx| {
433 let (stack_frame_list, stack_frame_id) =436 let (stack_frame_list, stack_frame_id) =
434 running_state.stack_frame_list().update(cx, |list, _| {437 running_state.stack_frame_list().update(cx, |list, _| {
435 (list.flatten_entries(true), list.opened_stack_frame_id())438 (
439 list.flatten_entries(true, true),
440 list.opened_stack_frame_id(),
441 )
436 });442 });
437 443
438 assert_eq!(Some(1), stack_frame_id);444 assert_eq!(Some(1), stack_frame_id);
@@ -1459,7 +1465,10 @@ async fn test_variable_list_only_sends_requests_when_rendering( · 1020 unmodified lines
1459 running_state.update(cx, |running_state, cx| {1465 running_state.update(cx, |running_state, cx| {
1460 let (stack_frame_list, stack_frame_id) =1466 let (stack_frame_list, stack_frame_id) =
1461 running_state.stack_frame_list().update(cx, |list, _| {1467 running_state.stack_frame_list().update(cx, |list, _| {
1462 (list.flatten_entries(true), list.opened_stack_frame_id())1468 (
1469 list.flatten_entries(true, true),
1470 list.opened_stack_frame_id(),
1471 )
1463 });1472 });
1464 1473
1465 assert_eq!(Some(1), stack_frame_id);1474 assert_eq!(Some(1), stack_frame_id);
@@ -1741,7 +1750,10 @@ async fn test_it_fetches_scopes_variables_when_you_select_a_stack_frame( · 275 unmodified lines
1741 running_state.update(cx, |running_state, cx| {1750 running_state.update(cx, |running_state, cx| {
1742 let (stack_frame_list, stack_frame_id) =1751 let (stack_frame_list, stack_frame_id) =
1743 running_state.stack_frame_list().update(cx, |list, _| {1752 running_state.stack_frame_list().update(cx, |list, _| {
1744 (list.flatten_entries(true), list.opened_stack_frame_id())1753 (
1754 list.flatten_entries(true, true),
1755 list.opened_stack_frame_id(),
1756 )
1745 });1757 });
1746 1758
1747 let variable_list = running_state.variable_list().read(cx);1759 let variable_list = running_state.variable_list().read(cx);
@@ -1796,7 +1808,10 @@ async fn test_it_fetches_scopes_variables_when_you_select_a_stack_frame( · 48 unmodified lines
1796 running_state.update(cx, |running_state, cx| {1808 running_state.update(cx, |running_state, cx| {
1797 let (stack_frame_list, stack_frame_id) =1809 let (stack_frame_list, stack_frame_id) =
1798 running_state.stack_frame_list().update(cx, |list, _| {1810 running_state.stack_frame_list().update(cx, |list, _| {
1799 (list.flatten_entries(true), list.opened_stack_frame_id())1811 (
1812 list.flatten_entries(true, true),
1813 list.opened_stack_frame_id(),
1814 )
1800 });1815 });
1801 1816
1802 let variable_list = running_state.variable_list().read(cx);1817 let variable_list = running_state.variable_list().read(cx);
crates/diagnostics/src/diagnostics.rs+3−3
@@ -43,7 +43,7 @@ use ui::{Icon, IconName, Label, h_flex, prelude::*}; · 42 unmodified lines
43use util::ResultExt;43use util::ResultExt;
44use workspace::{44use workspace::{
45 ItemNavHistory, ToolbarItemLocation, Workspace,45 ItemNavHistory, ToolbarItemLocation, Workspace,
46 item::{BreadcrumbText, Item, ItemEvent, ItemHandle, TabContentParams},46 item::{BreadcrumbText, Item, ItemEvent, ItemHandle, SaveOptions, TabContentParams},
47 searchable::SearchableItemHandle,47 searchable::SearchableItemHandle,
48};48};
49 49
@@ -849,12 +849,12 @@ impl Item for ProjectDiagnosticsEditor { · 799 unmodified lines
849 849
850 fn save(850 fn save(
851 &mut self,851 &mut self,
852 format: bool,852 options: SaveOptions,
853 project: Entity<Project>,853 project: Entity<Project>,
854 window: &mut Window,854 window: &mut Window,
855 cx: &mut Context<Self>,855 cx: &mut Context<Self>,
856 ) -> Task<Result<()>> {856 ) -> Task<Result<()>> {
857 self.editor.save(format, project, window, cx)857 self.editor.save(options, project, window, cx)
858 }858 }
859 859
860 fn save_as(860 fn save_as(
crates/editor/Cargo.toml+0−1
@@ -35,7 +35,6 @@ assets.workspace = true · 34 unmodified lines
35client.workspace = true35client.workspace = true
36clock.workspace = true36clock.workspace = true
37collections.workspace = true37collections.workspace = true
38command_palette_hooks.workspace = true
39convert_case.workspace = true38convert_case.workspace = true
40dap.workspace = true39dap.workspace = true
41db.workspace = true40db.workspace = true
crates/editor/src/actions.rs+3−2
@@ -87,6 +87,7 @@ pub struct ToggleCodeActions { · 86 unmodified lines
87#[derive(PartialEq, Clone, Debug)]87#[derive(PartialEq, Clone, Debug)]
88pub enum CodeActionSource {88pub enum CodeActionSource {
89 Indicator(DisplayRow),89 Indicator(DisplayRow),
90 RunMenu(DisplayRow),
90 QuickActionBar,91 QuickActionBar,
91}92}
92 93
@@ -243,6 +244,8 @@ impl_actions!( · 150 unmodified lines
243 ]244 ]
244);245);
245 246
247actions!(debugger, [RunToCursor, EvaluateSelectedText]);
248
246actions!(249actions!(
247 editor,250 editor,
248 [251 [
@@ -426,8 +429,6 @@ actions!( · 177 unmodified lines
426 DisableBreakpoint,429 DisableBreakpoint,
427 EnableBreakpoint,430 EnableBreakpoint,
428 EditLogBreakpoint,431 EditLogBreakpoint,
429 DebuggerRunToCursor,
430 DebuggerEvaluateSelectedText,
431 ToggleAutoSignatureHelp,432 ToggleAutoSignatureHelp,
432 ToggleGitBlameInline,433 ToggleGitBlameInline,
433 OpenGitBlameCommit,434 OpenGitBlameCommit,
crates/editor/src/code_completion_tests.rs+389−2276
@@ -1,2382 +1,495 @@
1use crate::{1use crate::{code_context_menus::CompletionsMenu, editor_settings::SnippetSortOrder};
2 code_context_menus::{CompletionsMenu, SortableMatch},2use fuzzy::StringMatchCandidate;
3 editor_settings::SnippetSortOrder,
4};
5use fuzzy::StringMatch;
6use gpui::TestAppContext;3use gpui::TestAppContext;
4use language::CodeLabel;
5use lsp::{CompletionItem, CompletionItemKind, LanguageServerId};
6use project::{Completion, CompletionSource};
7use std::sync::Arc;
8use std::sync::atomic::AtomicBool;
9use text::Anchor;
7 10
8#[gpui::test]11#[gpui::test]
9fn test_sort_matches_local_variable_over_global_variable(_cx: &mut TestAppContext) {12async fn test_sort_matches_local_variable_over_global_variable(cx: &mut TestAppContext) {
10 // Case 1: "foo"13 // Case 1: "foo"
11 let query: Option<&str> = Some("foo");14 let completions = vec![
12 let mut matches: Vec<SortableMatch<'_>> = vec![15 CompletionBuilder::constant("foo_bar_baz", "7fffffff"),
13 SortableMatch {16 CompletionBuilder::variable("foo_bar_qux", "7ffffffe"),
14 string_match: StringMatch {17 CompletionBuilder::constant("floorf64", "80000000"),
15 candidate_id: 0,18 CompletionBuilder::constant("floorf32", "80000000"),
16 score: 0.2727272727272727,19 CompletionBuilder::constant("floorf16", "80000000"),
17 positions: vec![],20 CompletionBuilder::constant("floorf128", "80000000"),
18 string: "foo_bar_baz".to_string(),
19 },
20 is_snippet: false,
21 sort_text: Some("7fffffff"),
22 sort_kind: 2,
23 sort_label: "foo_bar_baz",
24 },
25 SortableMatch {
26 string_match: StringMatch {
27 candidate_id: 0,
28 score: 0.2727272727272727,
29 positions: vec![],
30 string: "foo_bar_qux".to_string(),
31 },
32 is_snippet: false,
33 sort_text: Some("7ffffffe"),
34 sort_kind: 1,
35 sort_label: "foo_bar_qux",
36 },
37 SortableMatch {
38 string_match: StringMatch {
39 candidate_id: 0,
40 score: 0.22499999999999998,
41 positions: vec![],
42 string: "floorf64".to_string(),
43 },
44 is_snippet: false,
45 sort_text: Some("80000000"),
46 sort_kind: 2,
47 sort_label: "floorf64",
48 },
49 SortableMatch {
50 string_match: StringMatch {
51 candidate_id: 0,
52 score: 0.22499999999999998,
53 positions: vec![],
54 string: "floorf32".to_string(),
55 },
56 is_snippet: false,
57 sort_text: Some("80000000"),
58 sort_kind: 2,
59 sort_label: "floorf32",
60 },
61 SortableMatch {
62 string_match: StringMatch {
63 candidate_id: 0,
64 score: 0.22499999999999998,
65 positions: vec![],
66 string: "floorf16".to_string(),
67 },
68 is_snippet: false,
69 sort_text: Some("80000000"),
70 sort_kind: 2,
71 sort_label: "floorf16",
72 },
73 SortableMatch {
74 string_match: StringMatch {
75 candidate_id: 0,
76 score: 0.2,
77 positions: vec![],
78 string: "floorf128".to_string(),
79 },
80 is_snippet: false,
81 sort_text: Some("80000000"),
82 sort_kind: 2,
83 sort_label: "floorf128",
84 },
85 ];21 ];
86 CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default());22 let matches = sort_matches("foo", &completions, SnippetSortOrder::default(), cx).await;
87 assert_eq!(23 assert_eq!(matches[0], "foo_bar_qux");
88 matches[0].string_match.string.as_str(),24 assert_eq!(matches[1], "foo_bar_baz");
89 "foo_bar_qux",25 assert_eq!(matches[2], "floorf16");
90 "Match order not expected"26 assert_eq!(matches[3], "floorf32");
91 );
92 assert_eq!(
93 matches[1].string_match.string.as_str(),
94 "foo_bar_baz",
95 "Match order not expected"
96 );
97 assert_eq!(
98 matches[2].string_match.string.as_str(),
99 "floorf16",
100 "Match order not expected"
101 );
102 assert_eq!(
103 matches[3].string_match.string.as_str(),
104 "floorf32",
105 "Match order not expected"
106 );
107 27
108 // Case 2: "foobar"28 // Case 2: "foobar"
109 let query: Option<&str> = Some("foobar");29 let completions = vec![
110 let mut matches: Vec<SortableMatch<'_>> = vec![30 CompletionBuilder::constant("foo_bar_baz", "7fffffff"),
111 SortableMatch {31 CompletionBuilder::variable("foo_bar_qux", "7ffffffe"),
112 string_match: StringMatch {
113 candidate_id: 0,
114 score: 0.4363636363636364,
115 positions: vec![],
116 string: "foo_bar_baz".to_string(),
117 },
118 is_snippet: false,
119 sort_text: Some("7fffffff"),
120 sort_kind: 2,
121 sort_label: "foo_bar_baz",
122 },
123 SortableMatch {
124 string_match: StringMatch {
125 candidate_id: 0,
126 score: 0.4363636363636364,
127 positions: vec![],
128 string: "foo_bar_qux".to_string(),
129 },
130 is_snippet: false,
131 sort_text: Some("7ffffffe"),
132 sort_kind: 1,
133 sort_label: "foo_bar_qux",
134 },
135 ];32 ];
136 CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default());33 let matches = sort_matches("foobar", &completions, SnippetSortOrder::default(), cx).await;
137 assert_eq!(34 assert_eq!(matches[0], "foo_bar_qux");
138 matches[0].string_match.string.as_str(),35 assert_eq!(matches[1], "foo_bar_baz");
139 "foo_bar_qux",
140 "Match order not expected"
141 );
142 assert_eq!(
143 matches[1].string_match.string.as_str(),
144 "foo_bar_baz",
145 "Match order not expected"
146 );
147}36}
148 37
149#[gpui::test]38#[gpui::test]
150fn test_sort_matches_local_variable_over_global_enum(_cx: &mut TestAppContext) {39async fn test_sort_matches_local_variable_over_global_enum(cx: &mut TestAppContext) {
151 // Case 1: "ele"40 // Case 1: "ele"
152 let query: Option<&str> = Some("ele");41 let completions = vec![
153 let mut matches: Vec<SortableMatch<'_>> = vec![42 CompletionBuilder::constant("ElementType", "7fffffff"),
154 SortableMatch {43 CompletionBuilder::variable("element_type", "7ffffffe"),
155 string_match: StringMatch {44 CompletionBuilder::constant("simd_select", "80000000"),
156 candidate_id: 0,45 CompletionBuilder::keyword("while let", "7fffffff"),
157 score: 0.2727272727272727,
158 positions: vec![],
159 string: "ElementType".to_string(),
160 },
161 is_snippet: false,
162 sort_text: Some("7fffffff"),
163 sort_kind: 2,
164 sort_label: "ElementType",
165 },
166 SortableMatch {
167 string_match: StringMatch {
168 candidate_id: 0,
169 score: 0.25,
170 positions: vec![],
171 string: "element_type".to_string(),
172 },
173 is_snippet: false,
174 sort_text: Some("7ffffffe"),
175 sort_kind: 1,
176 sort_label: "element_type",
177 },
178 SortableMatch {
179 string_match: StringMatch {
180 candidate_id: 0,
181 score: 0.16363636363636364,
182 positions: vec![],
183 string: "simd_select".to_string(),
184 },
185 is_snippet: false,
186 sort_text: Some("80000000"),
187 sort_kind: 2,
188 sort_label: "simd_select",
189 },
190 SortableMatch {
191 string_match: StringMatch {
192 candidate_id: 0,
193 score: 0.16,
194 positions: vec![],
195 string: "while let".to_string(),
196 },
197 is_snippet: false,
198 sort_text: Some("7fffffff"),
199 sort_kind: 0,
200 sort_label: "while let",
201 },
202 ];46 ];
203 CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default());47 let matches = sort_matches("ele", &completions, SnippetSortOrder::default(), cx).await;
204 assert_eq!(48 assert_eq!(matches[0], "element_type");
205 matches[0].string_match.string.as_str(),49 assert_eq!(matches[1], "ElementType");
206 "element_type",
207 "Match order not expected"
208 );
209 assert_eq!(
210 matches[1].string_match.string.as_str(),
211 "ElementType",
212 "Match order not expected"
213 );
214 50
215 // Case 2: "eleme"51 // Case 2: "eleme"
216 let query: Option<&str> = Some("eleme");52 let completions = vec![
217 let mut matches: Vec<SortableMatch<'_>> = vec![53 CompletionBuilder::constant("ElementType", "7fffffff"),
218 SortableMatch {54 CompletionBuilder::variable("element_type", "7ffffffe"),
219 string_match: StringMatch {55 CompletionBuilder::constant("REPLACEMENT_CHARACTER", "80000000"),
220 candidate_id: 0,
221 score: 0.4545454545454546,
222 positions: vec![],
223 string: "ElementType".to_string(),
224 },
225 is_snippet: false,
226 sort_text: Some("7fffffff"),
227 sort_kind: 2,
228 sort_label: "ElementType",
229 },
230 SortableMatch {
231 string_match: StringMatch {
232 candidate_id: 0,
233 score: 0.41666666666666663,
234 positions: vec![],
235 string: "element_type".to_string(),
236 },
237 is_snippet: false,
238 sort_text: Some("7ffffffe"),
239 sort_kind: 1,
240 sort_label: "element_type",
241 },
242 SortableMatch {
243 string_match: StringMatch {
244 candidate_id: 0,
245 score: 0.04714285714285713,
246 positions: vec![],
247 string: "REPLACEMENT_CHARACTER".to_string(),
248 },
249 is_snippet: false,
250 sort_text: Some("80000000"),
251 sort_kind: 2,
252 sort_label: "REPLACEMENT_CHARACTER",
253 },
254 ];56 ];
255 CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default());57 let matches = sort_matches("eleme", &completions, SnippetSortOrder::default(), cx).await;
256 assert_eq!(58 assert_eq!(matches[0], "element_type");
257 matches[0].string_match.string.as_str(),59 assert_eq!(matches[1], "ElementType");
258 "element_type",60}
259 "Match order not expected"
260 );
261 assert_eq!(
262 matches[1].string_match.string.as_str(),
263 "ElementType",
264 "Match order not expected"
265 );
266 61
267 // Case 3: "Elem"62#[gpui::test]
268 let query: Option<&str> = Some("Elem");63async fn test_sort_matches_capitalization(cx: &mut TestAppContext) {
269 let mut matches: Vec<SortableMatch<'_>> = vec![64 // Case 1: "Elem"
270 SortableMatch {65 let completions = vec![
271 string_match: StringMatch {66 CompletionBuilder::constant("ElementType", "7fffffff"),
272 candidate_id: 0,67 CompletionBuilder::variable("element_type", "7ffffffe"),
273 score: 0.36363636363636365,
274 positions: vec![],
275 string: "ElementType".to_string(),
276 },
277 is_snippet: false,
278 sort_text: Some("7fffffff"),
279 sort_kind: 2,
280 sort_label: "ElementType",
281 },
282 SortableMatch {
283 string_match: StringMatch {
284 candidate_id: 0,
285 score: 0.0003333333333333333,
286 positions: vec![],
287 string: "element_type".to_string(),
288 },
289 is_snippet: false,
290 sort_text: Some("7ffffffe"),
291 sort_kind: 1,
292 sort_label: "element_type",
293 },
294 ];68 ];
295 CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default());69 let matches = sort_matches("Elem", &completions, SnippetSortOrder::default(), cx).await;
296 assert_eq!(70 assert_eq!(matches[0], "ElementType");
297 matches[0].string_match.string.as_str(),71 assert_eq!(matches[1], "element_type");
298 "ElementType",
299 "Match order not expected"
300 );
301 assert_eq!(
302 matches[1].string_match.string.as_str(),
303 "element_type",
304 "Match order not expected"
305 );
306}72}
307 73
308#[gpui::test]74#[gpui::test]
309fn test_sort_matches_for_unreachable(_cx: &mut TestAppContext) {75async fn test_sort_matches_for_unreachable(cx: &mut TestAppContext) {
310 // Case 1: "unre"76 // Case 1: "unre"
311 let query: Option<&str> = Some("unre");77 let completions = vec![
312 let mut matches: Vec<SortableMatch<'_>> = vec![78 CompletionBuilder::function("unreachable", "80000000"),
313 SortableMatch {79 CompletionBuilder::function("unreachable!(…)", "7fffffff"),
314 string_match: StringMatch {80 CompletionBuilder::function("unchecked_rem", "80000000"),
315 candidate_id: 0,81 CompletionBuilder::function("unreachable_unchecked", "80000000"),
316 score: 0.36363636363636365,
317 positions: vec![],
318 string: "unreachable".to_string(),
319 },
320 is_snippet: false,
321 sort_text: Some("80000000"),
322 sort_kind: 2,
323 sort_label: "unreachable",
324 },
325 SortableMatch {
326 string_match: StringMatch {
327 candidate_id: 0,
328 score: 0.26666666666666666,
329 positions: vec![],
330 string: "unreachable!(…)".to_string(),
331 },
332 is_snippet: true,
333 sort_text: Some("7fffffff"),
334 sort_kind: 2,
335 sort_label: "unreachable!(…)",
336 },
337 SortableMatch {
338 string_match: StringMatch {
339 candidate_id: 0,
340 score: 0.24615384615384617,
341 positions: vec![],
342 string: "unchecked_rem".to_string(),
343 },
344 is_snippet: false,
345 sort_text: Some("80000000"),
346 sort_kind: 2,
347 sort_label: "unchecked_rem",
348 },
349 SortableMatch {
350 string_match: StringMatch {
351 candidate_id: 0,
352 score: 0.19047619047619047,
353 positions: vec![],
354 string: "unreachable_unchecked".to_string(),
355 },
356 is_snippet: false,
357 sort_text: Some("80000000"),
358 sort_kind: 2,
359 sort_label: "unreachable_unchecked",
360 },
361 ];82 ];
362 CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default());83 let matches = sort_matches("unre", &completions, SnippetSortOrder::default(), cx).await;
363 assert_eq!(84 assert_eq!(matches[0], "unreachable!(…)");
364 matches[0].string_match.string.as_str(),
365 "unreachable!(…)",
366 "Match order not expected"
367 );
368 85
369 // Case 2: "unrea"86 // Case 2: "unrea"
370 let query: Option<&str> = Some("unrea");87 let completions = vec![
371 let mut matches: Vec<SortableMatch<'_>> = vec![88 CompletionBuilder::function("unreachable", "80000000"),
372 SortableMatch {89 CompletionBuilder::function("unreachable!(…)", "7fffffff"),
373 string_match: StringMatch {90 CompletionBuilder::function("unreachable_unchecked", "80000000"),
374 candidate_id: 0,
375 score: 0.4545454545454546,
376 positions: vec![],
377 string: "unreachable".to_string(),
378 },
379 is_snippet: true,
380 sort_text: Some("80000000"),
381 sort_kind: 3,
382 sort_label: "unreachable",
383 },
384 SortableMatch {
385 string_match: StringMatch {
386 candidate_id: 0,
387 score: 0.3333333333333333,
388 positions: vec![],
389 string: "unreachable!(…)".to_string(),
390 },
391 is_snippet: true,
392 sort_text: Some("7fffffff"),
393 sort_kind: 3,
394 sort_label: "unreachable!(…)",
395 },
396 SortableMatch {
397 string_match: StringMatch {
398 candidate_id: 0,
399 score: 0.23809523809523808,
400 positions: vec![],
401 string: "unreachable_unchecked".to_string(),
402 },
403 is_snippet: true,
404 sort_text: Some("80000000"),
405 sort_kind: 3,
406 sort_label: "unreachable_unchecked",
407 },
408 ];91 ];
409 CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default());92 let matches = sort_matches("unrea", &completions, SnippetSortOrder::default(), cx).await;
410 assert_eq!(93 assert_eq!(matches[0], "unreachable!(…)");
411 matches[0].string_match.string.as_str(),
412 "unreachable!(…)",
413 "Match order not expected"
414 );
415 94
416 // Case 3: "unreach"95 // Case 3: "unreach"
417 let query: Option<&str> = Some("unreach");96 let completions = vec![
418 let mut matches: Vec<SortableMatch<'_>> = vec![97 CompletionBuilder::function("unreachable", "80000000"),
419 SortableMatch {98 CompletionBuilder::function("unreachable!(…)", "7fffffff"),
420 string_match: StringMatch {99 CompletionBuilder::function("unreachable_unchecked", "80000000"),
421 candidate_id: 0,
422 score: 0.6363636363636364,
423 positions: vec![],
424 string: "unreachable".to_string(),
425 },
426 is_snippet: false,
427 sort_text: Some("80000000"),
428 sort_kind: 2,
429 sort_label: "unreachable",
430 },
431 SortableMatch {
432 string_match: StringMatch {
433 candidate_id: 0,
434 score: 0.4666666666666667,
435 positions: vec![],
436 string: "unreachable!(…)".to_string(),
437 },
438 is_snippet: true,
439 sort_text: Some("7fffffff"),
440 sort_kind: 2,
441 sort_label: "unreachable!(…)",
442 },
443 SortableMatch {
444 string_match: StringMatch {
445 candidate_id: 0,
446 score: 0.3333333333333333,
447 positions: vec![],
448 string: "unreachable_unchecked".to_string(),
449 },
450 is_snippet: false,
451 sort_text: Some("80000000"),
452 sort_kind: 2,
453 sort_label: "unreachable_unchecked",
454 },
455 ];100 ];
456 CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default());101 let matches = sort_matches("unreach", &completions, SnippetSortOrder::default(), cx).await;
457 assert_eq!(102 assert_eq!(matches[0], "unreachable!(…)");
458 matches[0].string_match.string.as_str(),
459 "unreachable!(…)",
460 "Match order not expected"
461 );
462 103
463 // Case 4: "unreachabl"104 // Case 4: "unreachabl"
464 let query: Option<&str> = Some("unreachable");105 let completions = vec![
465 let mut matches: Vec<SortableMatch<'_>> = vec![106 CompletionBuilder::function("unreachable", "80000000"),
466 SortableMatch {107 CompletionBuilder::function("unreachable!(…)", "7fffffff"),
467 string_match: StringMatch {108 CompletionBuilder::function("unreachable_unchecked", "80000000"),
468 candidate_id: 0,
469 score: 0.9090909090909092,
470 positions: vec![],
471 string: "unreachable".to_string(),
472 },
473 is_snippet: false,
474 sort_text: Some("80000000"),
475 sort_kind: 3,
476 sort_label: "unreachable",
477 },
478 SortableMatch {
479 string_match: StringMatch {
480 candidate_id: 0,
481 score: 0.6666666666666666,
482 positions: vec![],
483 string: "unreachable!(…)".to_string(),
484 },
485 is_snippet: false,
486 sort_text: Some("7fffffff"),
487 sort_kind: 3,
488 sort_label: "unreachable!(…)",
489 },
490 SortableMatch {
491 string_match: StringMatch {
492 candidate_id: 0,
493 score: 0.47619047619047616,
494 positions: vec![],
495 string: "unreachable_unchecked".to_string(),
496 },
497 is_snippet: false,
498 sort_text: Some("80000000"),
499 sort_kind: 3,
500 sort_label: "unreachable_unchecked",
501 },
502 ];109 ];
503 CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default());110 let matches = sort_matches("unreachable", &completions, SnippetSortOrder::default(), cx).await;
504 assert_eq!(111 assert_eq!(matches[0], "unreachable!(…)");
505 matches[0].string_match.string.as_str(),
506 "unreachable!(…)",
507 "Match order not expected"
508 );
509 112
510 // Case 5: "unreachable"113 // Case 5: "unreachable"
511 let query: Option<&str> = Some("unreachable");114 let completions = vec![
512 let mut matches: Vec<SortableMatch<'_>> = vec![115 CompletionBuilder::function("unreachable", "80000000"),
513 SortableMatch {116 CompletionBuilder::function("unreachable!(…)", "7fffffff"),
514 string_match: StringMatch {117 CompletionBuilder::function("unreachable_unchecked", "80000000"),
515 candidate_id: 0,
516 score: 1.0,
517 positions: vec![],
518 string: "unreachable".to_string(),
519 },
520 is_snippet: false,
521 sort_text: Some("80000000"),
522 sort_kind: 2,
523 sort_label: "unreachable",
524 },
525 SortableMatch {
526 string_match: StringMatch {
527 candidate_id: 0,
528 score: 0.7333333333333333,
529 positions: vec![],
530 string: "unreachable!(…)".to_string(),
531 },
532 is_snippet: false,
533 sort_text: Some("7fffffff"),
534 sort_kind: 2,
535 sort_label: "unreachable!(…)",
536 },
537 SortableMatch {
538 string_match: StringMatch {
539 candidate_id: 0,
540 score: 0.5238095238095237,
541 positions: vec![],
542 string: "unreachable_unchecked".to_string(),
543 },
544 is_snippet: false,
545 sort_text: Some("80000000"),
546 sort_kind: 2,
547 sort_label: "unreachable_unchecked",
548 },
549 ];118 ];
550 CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default());119 let matches = sort_matches("unreachable", &completions, SnippetSortOrder::default(), cx).await;
551 assert_eq!(120 assert_eq!(matches[0], "unreachable!(…)");
552 matches[0].string_match.string.as_str(),
553 "unreachable!(…)",
554 "LSP should take over even when fuzzy perfect matches"
555 );
556}121}
557 122
558#[gpui::test]123#[gpui::test]
559fn test_sort_matches_variable_and_constants_over_function(_cx: &mut TestAppContext) {124async fn test_sort_matches_variable_and_constants_over_function(cx: &mut TestAppContext) {
560 // Case 1: "var" as variable125 // Case 1: "var" as variable
561 let query: Option<&str> = Some("var");126 let completions = vec![
562 let mut matches: Vec<SortableMatch<'_>> = vec![127 CompletionBuilder::function("var", "7fffffff"),
563 SortableMatch {128 CompletionBuilder::variable("var", "7fffffff"),
564 string_match: StringMatch {
565 candidate_id: 0,
566 score: 1.0,
567 positions: vec![],
568 string: "var".to_string(),
569 },
570 is_snippet: false,
571 sort_text: Some("7fffffff"),
572 sort_kind: 3,
573 sort_label: "var", // function
574 },
575 SortableMatch {
576 string_match: StringMatch {
577 candidate_id: 1,
578 score: 1.0,
579 positions: vec![],
580 string: "var".to_string(),
581 },
582 is_snippet: false,
583 sort_text: Some("7fffffff"),
584 sort_kind: 1,
585 sort_label: "var", // variable
586 },
587 ];129 ];
588 CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default());130 let matches = sort_matches("var", &completions, SnippetSortOrder::default(), cx).await;
589 assert_eq!(131 assert_eq!(matches[0], "var");
590 matches[0].string_match.candidate_id, 1,132 assert_eq!(matches[1], "var");
591 "Match order not expected"
592 );
593 assert_eq!(
594 matches[1].string_match.candidate_id, 0,
595 "Match order not expected"
596 );
597 133
598 // Case 2: "var" as constant134 // Case 2: "var" as constant
599 let query: Option<&str> = Some("var");135 let completions = vec![
600 let mut matches: Vec<SortableMatch<'_>> = vec![136 CompletionBuilder::function("var", "7fffffff"),
601 SortableMatch {137 CompletionBuilder::constant("var", "7fffffff"),
602 string_match: StringMatch {
603 candidate_id: 0,
604 score: 1.0,
605 positions: vec![],
606 string: "var".to_string(),
607 },
608 is_snippet: false,
609 sort_text: Some("7fffffff"),
610 sort_kind: 3,
611 sort_label: "var", // function
612 },
613 SortableMatch {
614 string_match: StringMatch {
615 candidate_id: 1,
616 score: 1.0,
617 positions: vec![],
618 string: "var".to_string(),
619 },
620 is_snippet: false,
621 sort_text: Some("7fffffff"),
622 sort_kind: 2,
623 sort_label: "var", // constant
624 },
625 ];138 ];
626 CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default());139 let matches = sort_matches("var", &completions, SnippetSortOrder::default(), cx).await;
627 assert_eq!(140 assert_eq!(matches[0], "var");
628 matches[0].string_match.candidate_id, 1,141 assert_eq!(matches[1], "var");
629 "Match order not expected"
630 );
631 assert_eq!(
632 matches[1].string_match.candidate_id, 0,
633 "Match order not expected"
634 );
635}142}
636 143
637#[gpui::test]144#[gpui::test]
638fn test_sort_matches_for_jsx_event_handler(_cx: &mut TestAppContext) {145async fn test_sort_matches_for_jsx_event_handler(cx: &mut TestAppContext) {
639 // Case 1: "on"146 // Case 1: "on"
640 let query: Option<&str> = Some("on");147 let completions = vec![
641 let mut matches: Vec<SortableMatch<'_>> = vec![148 CompletionBuilder::function("onCut?", "12"),
642 SortableMatch {149 CompletionBuilder::function("onPlay?", "12"),
643 string_match: StringMatch {150 CompletionBuilder::function("color?", "12"),
644 candidate_id: 0,151 CompletionBuilder::function("defaultValue?", "12"),
645 score: 0.3333333333333333,152 CompletionBuilder::function("style?", "12"),
646 positions: vec![],153 CompletionBuilder::function("className?", "12"),
647 string: "onCut?".to_string(),
648 },
649 is_snippet: false,
650 sort_text: Some("12"),
651 sort_kind: 3,
652 sort_label: "onCut?",
653 },
654 SortableMatch {
655 string_match: StringMatch {
656 candidate_id: 0,
657 score: 0.2857142857142857,
658 positions: vec![],
659 string: "onPlay?".to_string(),
660 },
661 is_snippet: false,
662 sort_text: Some("12"),
663 sort_kind: 3,
664 sort_label: "onPlay?",
665 },
666 SortableMatch {
667 string_match: StringMatch {
668 candidate_id: 0,
669 score: 0.25,
670 positions: vec![],
671 string: "color?".to_string(),
672 },
673 is_snippet: false,
674 sort_text: Some("12"),
675 sort_kind: 3,
676 sort_label: "color?",
677 },
678 SortableMatch {
679 string_match: StringMatch {
680 candidate_id: 0,
681 score: 0.25,
682 positions: vec![],
683 string: "defaultValue?".to_string(),
684 },
685 is_snippet: false,
686 sort_text: Some("12"),
687 sort_kind: 3,
688 sort_label: "defaultValue?",
689 },
690 SortableMatch {
691 string_match: StringMatch {
692 candidate_id: 0,
693 score: 0.25,
694 positions: vec![],
695 string: "style?".to_string(),
696 },
697 is_snippet: false,
698 sort_text: Some("12"),
699 sort_kind: 3,
700 sort_label: "style?",
701 },
702 SortableMatch {
703 string_match: StringMatch {
704 candidate_id: 0,
705 score: 0.20,
706 positions: vec![],
707 string: "className?".to_string(),
708 },
709 is_snippet: false,
710 sort_text: Some("12"),
711 sort_kind: 3,
712 sort_label: "className?",
713 },
714 ];154 ];
715 CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default());155 let matches = sort_matches("on", &completions, SnippetSortOrder::default(), cx).await;
716 assert_eq!(156 assert_eq!(matches[0], "onCut?");
717 matches[0].string_match.string, "onCut?",157 assert_eq!(matches[1], "onPlay?");
718 "Match order not expected"
719 );
720 assert_eq!(
721 matches[1].string_match.string, "onPlay?",
722 "Match order not expected"
723 );
724 158
725 // Case 2: "ona"159 // Case 2: "ona"
726 let query: Option<&str> = Some("ona");160 let completions = vec![
727 let mut matches: Vec<SortableMatch<'_>> = vec![161 CompletionBuilder::function("onAbort?", "12"),
728 SortableMatch {162 CompletionBuilder::function("onAuxClick?", "12"),
729 string_match: StringMatch {163 CompletionBuilder::function("onPlay?", "12"),
730 candidate_id: 0,164 CompletionBuilder::function("onLoad?", "12"),
731 score: 0.375,165 CompletionBuilder::function("onDrag?", "12"),
732 positions: vec![],166 CompletionBuilder::function("onPause?", "12"),
733 string: "onAbort?".to_string(),167 CompletionBuilder::function("onPaste?", "12"),
734 },168 CompletionBuilder::function("onAnimationEnd?", "12"),
735 is_snippet: false,169 CompletionBuilder::function("onAbortCapture?", "12"),
736 sort_text: Some("12"),170 CompletionBuilder::function("onChange?", "12"),
737 sort_kind: 3,171 CompletionBuilder::function("onWaiting?", "12"),
738 sort_label: "onAbort?",172 CompletionBuilder::function("onCanPlay?", "12"),
739 },
740 SortableMatch {
741 string_match: StringMatch {
742 candidate_id: 0,
743 score: 0.2727272727272727,
744 positions: vec![],
745 string: "onAuxClick?".to_string(),
746 },
747 is_snippet: false,
748 sort_text: Some("12"),
749 sort_kind: 3,
750 sort_label: "onAuxClick?",
751 },
752 SortableMatch {
753 string_match: StringMatch {
754 candidate_id: 0,
755 score: 0.23571428571428565,
756 positions: vec![],
757 string: "onPlay?".to_string(),
758 },
759 is_snippet: false,
760 sort_text: Some("12"),
761 sort_kind: 3,
762 sort_label: "onPlay?",
763 },
764 SortableMatch {
765 string_match: StringMatch {
766 candidate_id: 0,
767 score: 0.23571428571428565,
768 positions: vec![],
769 string: "onLoad?".to_string(),
770 },
771 is_snippet: false,
772 sort_text: Some("12"),
773 sort_kind: 3,
774 sort_label: "onLoad?",
775 },
776 SortableMatch {
777 string_match: StringMatch {
778 candidate_id: 0,
779 score: 0.23571428571428565,
780 positions: vec![],
781 string: "onDrag?".to_string(),
782 },
783 is_snippet: false,
784 sort_text: Some("12"),
785 sort_kind: 3,
786 sort_label: "onDrag?",
787 },
788 SortableMatch {
789 string_match: StringMatch {
790 candidate_id: 0,
791 score: 0.22499999999999998,
792 positions: vec![],
793 string: "onPause?".to_string(),
794 },
795 is_snippet: false,
796 sort_text: Some("12"),
797 sort_kind: 3,
798 sort_label: "onPause?",
799 },
800 SortableMatch {
801 string_match: StringMatch {
802 candidate_id: 0,
803 score: 0.22499999999999998,
804 positions: vec![],
805 string: "onPaste?".to_string(),
806 },
807 is_snippet: false,
808 sort_text: Some("12"),
809 sort_kind: 3,
810 sort_label: "onPaste?",
811 },
812 SortableMatch {
813 string_match: StringMatch {
814 candidate_id: 0,
815 score: 0.2,
816 positions: vec![],
817 string: "onAnimationEnd?".to_string(),
818 },
819 is_snippet: false,
820 sort_text: Some("12"),
821 sort_kind: 3,
822 sort_label: "onAnimationEnd?",
823 },
824 SortableMatch {
825 string_match: StringMatch {
826 candidate_id: 0,
827 score: 0.2,
828 positions: vec![],
829 string: "onAbortCapture?".to_string(),
830 },
831 is_snippet: false,
832 sort_text: Some("12"),
833 sort_kind: 3,
834 sort_label: "onAbortCapture?",
835 },
836 SortableMatch {
837 string_match: StringMatch {
838 candidate_id: 0,
839 score: 0.1833333333333333,
840 positions: vec![],
841 string: "onChange?".to_string(),
842 },
843 is_snippet: false,
844 sort_text: Some("12"),
845 sort_kind: 3,
846 sort_label: "onChange?",
847 },
848 SortableMatch {
849 string_match: StringMatch {
850 candidate_id: 0,
851 score: 0.18,
852 positions: vec![],
853 string: "onWaiting?".to_string(),
854 },
855 is_snippet: false,
856 sort_text: Some("12"),
857 sort_kind: 3,
858 sort_label: "onWaiting?",
859 },
860 SortableMatch {
861 string_match: StringMatch {
862 candidate_id: 0,
863 score: 0.18,
864 positions: vec![],
865 string: "onCanPlay?".to_string(),
866 },
867 is_snippet: false,
868 sort_text: Some("12"),
869 sort_kind: 3,
870 sort_label: "onCanPlay?",
871 },
872 SortableMatch {
873 string_match: StringMatch {
874 candidate_id: 0,
875 score: 0.1764705882352941,
876 positions: vec![],
877 string: "onAnimationStart?".to_string(),
878 },
879 is_snippet: false,
880 sort_text: Some("12"),
881 sort_kind: 3,
882 sort_label: "onAnimationStart?",
883 },
884 SortableMatch {
885 string_match: StringMatch {
886 candidate_id: 0,
887 score: 0.16666666666666666,
888 positions: vec![],
889 string: "onAuxClickCapture?".to_string(),
890 },
891 is_snippet: false,
892 sort_text: Some("12"),
893 sort_kind: 3,
894 sort_label: "onAuxClickCapture?",
895 },
896 SortableMatch {
897 string_match: StringMatch {
898 candidate_id: 0,
899 score: 0.16499999999999998,
900 positions: vec![],
901 string: "onStalled?".to_string(),
902 },
903 is_snippet: false,
904 sort_text: Some("12"),
905 sort_kind: 3,
906 sort_label: "onStalled?",
907 },
908 SortableMatch {
909 string_match: StringMatch {
910 candidate_id: 0,
911 score: 0.16499999999999998,
912 positions: vec![],
913 string: "onPlaying?".to_string(),
914 },
915 is_snippet: false,
916 sort_text: Some("12"),
917 sort_kind: 3,
918 sort_label: "onPlaying?",
919 },
920 SortableMatch {
921 string_match: StringMatch {
922 candidate_id: 0,
923 score: 0.16499999999999998,
924 positions: vec![],
925 string: "onDragEnd?".to_string(),
926 },
927 is_snippet: false,
928 sort_text: Some("12"),
929 sort_kind: 3,
930 sort_label: "onDragEnd?",
931 },
932 SortableMatch {
933 string_match: StringMatch {
934 candidate_id: 0,
935 score: 0.15000000000000002,
936 positions: vec![],
937 string: "onInvalid?".to_string(),
938 },
939 is_snippet: false,
940 sort_text: Some("12"),
941 sort_kind: 3,
942 sort_label: "onInvalid?",
943 },
944 SortableMatch {
945 string_match: StringMatch {
946 candidate_id: 0,
947 score: 0.15,
948 positions: vec![],
949 string: "onDragOver?".to_string(),
950 },
951 is_snippet: false,
952 sort_text: Some("12"),
953 sort_kind: 3,
954 sort_label: "onDragOver?",
955 },
956 SortableMatch {
957 string_match: StringMatch {
958 candidate_id: 0,
959 score: 0.15,
960 positions: vec![],
961 string: "onDragExit?".to_string(),
962 },
963 is_snippet: false,
964 sort_text: Some("12"),
965 sort_kind: 3,
966 sort_label: "onDragExit?",
967 },
968 SortableMatch {
969 string_match: StringMatch {
970 candidate_id: 0,
971 score: 0.14285714285714285,
972 positions: vec![],
973 string: "onAnimationIteration?".to_string(),
974 },
975 is_snippet: false,
976 sort_text: Some("12"),
977 sort_kind: 3,
978 sort_label: "onAnimationIteration?",
979 },
980 SortableMatch {
981 string_match: StringMatch {
982 candidate_id: 0,
983 score: 0.13846153846153847,
984 positions: vec![],
985 string: "onRateChange?".to_string(),
986 },
987 is_snippet: false,
988 sort_text: Some("12"),
989 sort_kind: 3,
990 sort_label: "onRateChange?",
991 },
992 SortableMatch {
993 string_match: StringMatch {
994 candidate_id: 0,
995 score: 0.13749999999999996,
996 positions: vec![],
997 string: "onLoadStart?".to_string(),
998 },
999 is_snippet: false,
1000 sort_text: Some("12"),
1001 sort_kind: 3,
1002 sort_label: "onLoadStart?",
1003 },
1004 SortableMatch {
1005 string_match: StringMatch {
1006 candidate_id: 0,
1007 score: 0.13749999999999996,
1008 positions: vec![],
1009 string: "onDragStart?".to_string(),
1010 },
1011 is_snippet: false,
1012 sort_text: Some("12"),
1013 sort_kind: 3,
1014 sort_label: "onDragStart?",
1015 },
1016 SortableMatch {
1017 string_match: StringMatch {
1018 candidate_id: 0,
1019 score: 0.13749999999999996,
1020 positions: vec![],
1021 string: "onDragLeave?".to_string(),
1022 },
1023 is_snippet: false,
1024 sort_text: Some("12"),
1025 sort_kind: 3,
1026 sort_label: "onDragLeave?",
1027 },
1028 SortableMatch {
1029 string_match: StringMatch {
1030 candidate_id: 0,
1031 score: 0.13749999999999996,
1032 positions: vec![],
1033 string: "onDragEnter?".to_string(),
1034 },
1035 is_snippet: false,
1036 sort_text: Some("12"),
1037 sort_kind: 3,
1038 sort_label: "onDragEnter?",
1039 },
1040 SortableMatch {
1041 string_match: StringMatch {
1042 candidate_id: 0,
1043 score: 0.13636363636363635,
1044 positions: vec![],
1045 string: "onAnimationEndCapture?".to_string(),
1046 },
1047 is_snippet: false,
1048 sort_text: Some("12"),
1049 sort_kind: 3,
1050 sort_label: "onAnimationEndCapture?",
1051 },
1052 SortableMatch {
1053 string_match: StringMatch {
1054 candidate_id: 0,
1055 score: 0.12692307692307692,
1056 positions: vec![],
1057 string: "onLoadedData?".to_string(),
1058 },
1059 is_snippet: false,
1060 sort_text: Some("12"),
1061 sort_kind: 3,
1062 sort_label: "onLoadedData?",
1063 },
1064 ];173 ];
1065 CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default());174 let matches = sort_matches("ona", &completions, SnippetSortOrder::default(), cx).await;
1066 assert_eq!(175 assert_eq!(matches[0], "onAbort?");
1067 matches176 assert_eq!(matches[1], "onAuxClick?");
1068 .iter()
1069 .take(12)
1070 .map(|m| m.string_match.string.as_str())
1071 .collect::<Vec<&str>>(),
1072 vec![
1073 "onAbort?",
1074 "onAuxClick?",
1075 "onAbortCapture?",
1076 "onAnimationEnd?",
1077 "onAnimationStart?",
1078 "onAuxClickCapture?",
1079 "onAnimationIteration?",
1080 "onAnimationEndCapture?",
1081 "onDrag?",
1082 "onLoad?",
1083 "onPlay?",
1084 "onPaste?",
1085 ]
1086 );
1087}177}
1088 178
1089#[gpui::test]179#[gpui::test]
1090fn test_sort_matches_for_snippets(_cx: &mut TestAppContext) {180async fn test_sort_matches_for_snippets(cx: &mut TestAppContext) {
1091 // Case 1: "prin"181 // Case 1: "prin"
1092 let query: Option<&str> = Some("prin");182 let completions = vec![
1093 let mut matches: Vec<SortableMatch<'_>> = vec![183 CompletionBuilder::constant("println", "80000000"),
1094 SortableMatch {184 CompletionBuilder::snippet("println!(…)", "80000000"),
1095 string_match: StringMatch {
1096 candidate_id: 0,
1097 score: 0.2,
1098 positions: vec![],
1099 string: "println".to_string(),
1100 },
1101 is_snippet: false,
1102 sort_text: Some("80000000"),
1103 sort_kind: 2,
1104 sort_label: "println",
1105 },
1106 SortableMatch {
1107 string_match: StringMatch {
1108 candidate_id: 0,
1109 score: 0.2,
1110 positions: vec![],
1111 string: "println!(…)".to_string(),
1112 },
1113 is_snippet: true,
1114 sort_text: Some("80000000"),
1115 sort_kind: 2,
1116 sort_label: "println!(…)",
1117 },
1118 ];185 ];
1119 CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::Top);186 let matches = sort_matches("prin", &completions, SnippetSortOrder::Top, cx).await;
1120 assert_eq!(187 assert_eq!(matches[0], "println!(…)");
1121 matches[0].string_match.string.as_str(),
1122 "println!(…)",
1123 "Match order not expected"
1124 );
1125}188}
1126 189
1127#[gpui::test]190#[gpui::test]
1128fn test_sort_matches_for_exact_match(_cx: &mut TestAppContext) {191async fn test_sort_matches_for_exact_match(cx: &mut TestAppContext) {
1129 // Case 1: "set_text"192 // Case 1: "set_text"
1130 let query: Option<&str> = Some("set_text");193 let completions = vec![
1131 let mut matches: Vec<SortableMatch<'_>> = vec![194 CompletionBuilder::function("set_text", "7fffffff"),
1132 SortableMatch {195 CompletionBuilder::function("set_placeholder_text", "7fffffff"),
1133 string_match: StringMatch {196 CompletionBuilder::function("set_text_style_refinement", "7fffffff"),
1134 candidate_id: 0,197 CompletionBuilder::function("set_context_menu_options", "7fffffff"),
1135 score: 1.0,198 CompletionBuilder::function("select_to_next_word_end", "7fffffff"),
1136 positions: vec![],199 CompletionBuilder::function("select_to_next_subword_end", "7fffffff"),
1137 string: "set_text".to_string(),200 CompletionBuilder::function("set_custom_context_menu", "7fffffff"),
1138 },201 CompletionBuilder::function("select_to_end_of_excerpt", "7fffffff"),
1139 is_snippet: false,202 CompletionBuilder::function("select_to_start_of_excerpt", "7fffffff"),
1140 sort_text: Some("7fffffff"),203 CompletionBuilder::function("select_to_start_of_next_excerpt", "7fffffff"),
1141 sort_kind: 3,204 CompletionBuilder::function("select_to_end_of_previous_excerpt", "7fffffff"),
1142 sort_label: "set_text",
1143 },
1144 SortableMatch {
1145 string_match: StringMatch {
1146 candidate_id: 0,
1147 score: 0.32000000000000006,
1148 positions: vec![],
1149 string: "set_placeholder_text".to_string(),
1150 },
1151 is_snippet: false,
1152 sort_text: Some("7fffffff"),
1153 sort_kind: 3,
1154 sort_label: "set_placeholder_text",
1155 },
1156 SortableMatch {
1157 string_match: StringMatch {
1158 candidate_id: 0,
1159 score: 0.32,
1160 positions: vec![],
1161 string: "set_text_style_refinement".to_string(),
1162 },
1163 is_snippet: false,
1164 sort_text: Some("7fffffff"),
1165 sort_kind: 3,
1166 sort_label: "set_text_style_refinement",
1167 },
1168 SortableMatch {
1169 string_match: StringMatch {
1170 candidate_id: 0,
1171 score: 0.16666666666666666,
1172 positions: vec![],
1173 string: "set_context_menu_options".to_string(),
1174 },
1175 is_snippet: false,
1176 sort_text: Some("7fffffff"),
1177 sort_kind: 3,
1178 sort_label: "set_context_menu_options",
1179 },
1180 SortableMatch {
1181 string_match: StringMatch {
1182 candidate_id: 0,
1183 score: 0.08695652173913043,
1184 positions: vec![],
1185 string: "select_to_next_word_end".to_string(),
1186 },
1187 is_snippet: false,
1188 sort_text: Some("7fffffff"),
1189 sort_kind: 3,
1190 sort_label: "select_to_next_word_end",
1191 },
1192 SortableMatch {
1193 string_match: StringMatch {
1194 candidate_id: 0,
1195 score: 0.07692307692307693,
1196 positions: vec![],
1197 string: "select_to_next_subword_end".to_string(),
1198 },
1199 is_snippet: false,
1200 sort_text: Some("7fffffff"),
1201 sort_kind: 3,
1202 sort_label: "select_to_next_subword_end",
1203 },
1204 SortableMatch {
1205 string_match: StringMatch {
1206 candidate_id: 0,
1207 score: 0.06956521739130435,
1208 positions: vec![],
1209 string: "set_custom_context_menu".to_string(),
1210 },
1211 is_snippet: false,
1212 sort_text: Some("7fffffff"),
1213 sort_kind: 3,
1214 sort_label: "set_custom_context_menu",
1215 },
1216 SortableMatch {
1217 string_match: StringMatch {
1218 candidate_id: 0,
1219 score: 0.06,
1220 positions: vec![],
1221 string: "select_to_end_of_excerpt".to_string(),
1222 },
1223 is_snippet: false,
1224 sort_text: Some("7fffffff"),
1225 sort_kind: 3,
1226 sort_label: "select_to_end_of_excerpt",
1227 },
1228 SortableMatch {
1229 string_match: StringMatch {
1230 candidate_id: 0,
1231 score: 0.055384615384615386,
1232 positions: vec![],
1233 string: "select_to_start_of_excerpt".to_string(),
1234 },
1235 is_snippet: false,
1236 sort_text: Some("7fffffff"),
1237 sort_kind: 3,
1238 sort_label: "select_to_start_of_excerpt",
1239 },
1240 SortableMatch {
1241 string_match: StringMatch {
1242 candidate_id: 0,
1243 score: 0.0464516129032258,
1244 positions: vec![],
1245 string: "select_to_start_of_next_excerpt".to_string(),
1246 },
1247 is_snippet: false,
1248 sort_text: Some("7fffffff"),
1249 sort_kind: 3,
1250 sort_label: "select_to_start_of_next_excerpt",
1251 },
1252 SortableMatch {
1253 string_match: StringMatch {
1254 candidate_id: 0,
1255 score: 0.04363636363636363,
1256 positions: vec![],
1257 string: "select_to_end_of_previous_excerpt".to_string(),
1258 },
1259 is_snippet: false,
1260 sort_text: Some("7fffffff"),
1261 sort_kind: 3,
1262 sort_label: "select_to_end_of_previous_excerpt",
1263 },
1264 ];205 ];
1265 CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::Top);206 let matches = sort_matches("set_text", &completions, SnippetSortOrder::Top, cx).await;
1266 assert_eq!(207 assert_eq!(matches[0], "set_text");
1267 matches208 assert_eq!(matches[1], "set_text_style_refinement");
1268 .iter()209 assert_eq!(matches[2], "set_context_menu_options");
1269 .map(|m| m.string_match.string.as_str())
1270 .collect::<Vec<&str>>(),
1271 vec![
1272 "set_text",
1273 "set_text_style_refinement",
1274 "set_placeholder_text",
1275 "set_context_menu_options",
1276 "set_custom_context_menu",
1277 "select_to_next_word_end",
1278 "select_to_next_subword_end",
1279 "select_to_end_of_excerpt",
1280 "select_to_start_of_excerpt",
1281 "select_to_start_of_next_excerpt",
1282 "select_to_end_of_previous_excerpt",
1283 ]
1284 );
1285}210}
1286 211
1287#[gpui::test]212#[gpui::test]
1288fn test_sort_matches_for_prefix_matches(_cx: &mut TestAppContext) {213async fn test_sort_matches_for_prefix_matches(cx: &mut TestAppContext) {
1289 // Case 1: "set"214 // Case 1: "set"
1290 let query: Option<&str> = Some("set");215 let completions = vec![
1291 let mut matches: Vec<SortableMatch<'_>> = vec![216 CompletionBuilder::function("select_to_beginning", "7fffffff"),
1292 SortableMatch {217 CompletionBuilder::function("set_collapse_matches", "7fffffff"),
1293 string_match: StringMatch {218 CompletionBuilder::function("set_autoindent", "7fffffff"),
1294 candidate_id: 0,219 CompletionBuilder::function("set_all_diagnostics_active", "7fffffff"),
1295 score: 0.12631578947368421,220 CompletionBuilder::function("select_to_end_of_line", "7fffffff"),
1296 positions: vec![],221 CompletionBuilder::function("select_all", "7fffffff"),
1297 string: "select_to_beginning".to_string(),222 CompletionBuilder::function("select_line", "7fffffff"),
1298 },223 CompletionBuilder::function("select_left", "7fffffff"),
1299 is_snippet: false,224 CompletionBuilder::function("select_down", "7fffffff"),
1300 sort_text: Some("7fffffff"),
1301 sort_kind: 3,
1302 sort_label: "select_to_beginning",
1303 },
1304 SortableMatch {
1305 string_match: StringMatch {
1306 candidate_id: 0,
1307 score: 0.15000000000000002,
1308 positions: vec![],
1309 string: "set_collapse_matches".to_string(),
1310 },
1311 is_snippet: false,
1312 sort_text: Some("7fffffff"),
1313 sort_kind: 3,
1314 sort_label: "set_collapse_matches",
1315 },
1316 SortableMatch {
1317 string_match: StringMatch {
1318 candidate_id: 0,
1319 score: 0.21428571428571427,
1320 positions: vec![],
1321 string: "set_autoindent".to_string(),
1322 },
1323 is_snippet: false,
1324 sort_text: Some("7fffffff"),
1325 sort_kind: 3,
1326 sort_label: "set_autoindent",
1327 },
1328 SortableMatch {
1329 string_match: StringMatch {
1330 candidate_id: 0,
1331 score: 0.11538461538461539,
1332 positions: vec![],
1333 string: "set_all_diagnostics_active".to_string(),
1334 },
1335 is_snippet: false,
1336 sort_text: Some("7fffffff"),
1337 sort_kind: 3,
1338 sort_label: "set_all_diagnostics_active",
1339 },
1340 SortableMatch {
1341 string_match: StringMatch {
1342 candidate_id: 0,
1343 score: 0.1142857142857143,
1344 positions: vec![],
1345 string: "select_to_end_of_line".to_string(),
1346 },
1347 is_snippet: false,
1348 sort_text: Some("7fffffff"),
1349 sort_kind: 3,
1350 sort_label: "select_to_end_of_line",
1351 },
1352 SortableMatch {
1353 string_match: StringMatch {
1354 candidate_id: 0,
1355 score: 0.15000000000000002,
1356 positions: vec![],
1357 string: "select_all".to_string(),
1358 },
1359 is_snippet: false,
1360 sort_text: Some("7fffffff"),
1361 sort_kind: 3,
1362 sort_label: "select_all",
1363 },
1364 SortableMatch {
1365 string_match: StringMatch {
1366 candidate_id: 0,
1367 score: 0.13636363636363635,
1368 positions: vec![],
1369 string: "select_line".to_string(),
1370 },
1371 is_snippet: false,
1372 sort_text: Some("7fffffff"),
1373 sort_kind: 3,
1374 sort_label: "select_line",
1375 },
1376 SortableMatch {
1377 string_match: StringMatch {
1378 candidate_id: 0,
1379 score: 0.13636363636363635,
1380 positions: vec![],
1381 string: "select_left".to_string(),
1382 },
1383 is_snippet: false,
1384 sort_text: Some("7fffffff"),
1385 sort_kind: 3,
1386 sort_label: "select_left",
1387 },
1388 SortableMatch {
1389 string_match: StringMatch {
1390 candidate_id: 0,
1391 score: 0.13636363636363635,
1392 positions: vec![],
1393 string: "select_down".to_string(),
1394 },
1395 is_snippet: false,
1396 sort_text: Some("7fffffff"),
1397 sort_kind: 3,
1398 sort_label: "select_down",
1399 },
1400 ];225 ];
1401 CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::Top);226 let matches = sort_matches("set", &completions, SnippetSortOrder::Top, cx).await;
1402 assert_eq!(227 assert_eq!(matches[0], "set_autoindent");
1403 matches228 assert_eq!(matches[1], "set_collapse_matches");
1404 .iter()229 assert_eq!(matches[2], "set_all_diagnostics_active");
1405 .map(|m| m.string_match.string.as_str())
1406 .collect::<Vec<&str>>(),
1407 vec![
1408 "set_autoindent",
1409 "set_collapse_matches",
1410 "set_all_diagnostics_active",
1411 "select_all",
1412 "select_down",
1413 "select_left",
1414 "select_line",
1415 "select_to_beginning",
1416 "select_to_end_of_line",
1417 ]
1418 );
1419}230}
1420 231
1421#[gpui::test]232#[gpui::test]
1422fn test_sort_matches_for_await(_cx: &mut TestAppContext) {233async fn test_sort_matches_for_await(cx: &mut TestAppContext) {
1423 // Case 1: "awa"234 // Case 1: "awa"
1424 let query: Option<&str> = Some("awa");235 let completions = vec![
1425 let mut matches: Vec<SortableMatch<'_>> = vec![236 CompletionBuilder::keyword("await", "7fffffff"),
1426 SortableMatch {237 CompletionBuilder::function("await.ne", "80000010"),
1427 string_match: StringMatch {238 CompletionBuilder::function("await.eq", "80000010"),
1428 candidate_id: 0,239 CompletionBuilder::function("await.or", "7ffffff8"),
1429 score: 0.6000000000000001,240 CompletionBuilder::function("await.zip", "80000006"),
1430 positions: vec![],241 CompletionBuilder::function("await.xor", "7ffffff8"),
1431 string: "await".to_string(),242 CompletionBuilder::function("await.and", "80000006"),
1432 },243 CompletionBuilder::function("await.map", "80000006"),
1433 is_snippet: false,244 CompletionBuilder::function("await.take", "7ffffff8"),
1434 sort_text: Some("7fffffff"),
1435 sort_kind: 0,
1436 sort_label: "await",
1437 },
1438 SortableMatch {
1439 string_match: StringMatch {
1440 candidate_id: 35,
1441 score: 0.375,
1442 positions: vec![],
1443 string: "await.ne".to_string(),
1444 },
1445 is_snippet: false,
1446 sort_text: Some("80000010"),
1447 sort_kind: 3,
1448 sort_label: "await.ne",
1449 },
1450 SortableMatch {
1451 string_match: StringMatch {
1452 candidate_id: 34,
1453 score: 0.375,
1454 positions: vec![],
1455 string: "await.eq".to_string(),
1456 },
1457 is_snippet: false,
1458 sort_text: Some("80000010"),
1459 sort_kind: 3,
1460 sort_label: "await.eq",
1461 },
1462 SortableMatch {
1463 string_match: StringMatch {
1464 candidate_id: 18,
1465 score: 0.375,
1466 positions: vec![],
1467 string: "await.or".to_string(),
1468 },
1469 is_snippet: false,
1470 sort_text: Some("7ffffff8"),
1471 sort_kind: 3,
1472 sort_label: "await.or",
1473 },
1474 SortableMatch {
1475 string_match: StringMatch {
1476 candidate_id: 21,
1477 score: 0.3333333333333333,
1478 positions: vec![],
1479 string: "await.zip".to_string(),
1480 },
1481 is_snippet: false,
1482 sort_text: Some("80000006"),
1483 sort_kind: 3,
1484 sort_label: "await.zip",
1485 },
1486 SortableMatch {
1487 string_match: StringMatch {
1488 candidate_id: 20,
1489 score: 0.3333333333333333,
1490 positions: vec![],
1491 string: "await.xor".to_string(),
1492 },
1493 is_snippet: false,
1494 sort_text: Some("7ffffff8"),
1495 sort_kind: 3,
1496 sort_label: "await.xor",
1497 },
1498 SortableMatch {
1499 string_match: StringMatch {
1500 candidate_id: 15,
1501 score: 0.3333333333333333,
1502 positions: vec![],
1503 string: "await.and".to_string(),
1504 },
1505 is_snippet: false,
1506 sort_text: Some("80000006"),
1507 sort_kind: 3,
1508 sort_label: "await.and",
1509 },
1510 SortableMatch {
1511 string_match: StringMatch {
1512 candidate_id: 9,
1513 score: 0.3333333333333333,
1514 positions: vec![],
1515 string: "await.map".to_string(),
1516 },
1517 is_snippet: false,
1518 sort_text: Some("80000006"),
1519 sort_kind: 3,
1520 sort_label: "await.map",
1521 },
1522 SortableMatch {
1523 string_match: StringMatch {
1524 candidate_id: 47,
1525 score: 0.30000000000000004,
1526 positions: vec![],
1527 string: "await.take".to_string(),
1528 },
1529 is_snippet: false,
1530 sort_text: Some("7ffffff8"),
1531 sort_kind: 3,
1532 sort_label: "await.take",
1533 },
1534 ];245 ];
1535 CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::Top);246 let matches = sort_matches("awa", &completions, SnippetSortOrder::Top, cx).await;
1536 assert_eq!(247 assert_eq!(matches[0], "await");
1537 matches248
1538 .iter()
1539 .map(|m| m.string_match.string.as_str())
1540 .collect::<Vec<&str>>(),
1541 vec![
1542 "await",
1543 "await.or",
1544 "await.eq",
1545 "await.ne",
1546 "await.xor",
1547 "await.take",
1548 "await.and",
1549 "await.map",
1550 "await.zip"
1551 ]
1552 );
1553 // Case 2: "await"249 // Case 2: "await"
1554 let query: Option<&str> = Some("await");250 let completions = vec![
1555 let mut matches: Vec<SortableMatch<'_>> = vec![251 CompletionBuilder::keyword("await", "7fffffff"),
1556 SortableMatch {252 CompletionBuilder::function("await.ne", "80000010"),
1557 string_match: StringMatch {253 CompletionBuilder::function("await.eq", "80000010"),
1558 candidate_id: 0,254 CompletionBuilder::function("await.or", "7ffffff8"),
1559 score: 1.0,255 CompletionBuilder::function("await.zip", "80000006"),
1560 positions: vec![],256 CompletionBuilder::function("await.xor", "7ffffff8"),
1561 string: "await".to_string(),257 CompletionBuilder::function("await.and", "80000006"),
1562 },258 CompletionBuilder::function("await.map", "80000006"),
1563 is_snippet: false,259 CompletionBuilder::function("await.take", "7ffffff8"),
1564 sort_text: Some("7fffffff"),
1565 sort_kind: 0,
1566 sort_label: "await",
1567 },
1568 SortableMatch {
1569 string_match: StringMatch {
1570 candidate_id: 35,
1571 score: 0.625,
1572 positions: vec![],
1573 string: "await.ne".to_string(),
1574 },
1575 is_snippet: false,
1576 sort_text: Some("80000010"),
1577 sort_kind: 3,
1578 sort_label: "await.ne",
1579 },
1580 SortableMatch {
1581 string_match: StringMatch {
1582 candidate_id: 34,
1583 score: 0.625,
1584 positions: vec![],
1585 string: "await.eq".to_string(),
1586 },
1587 is_snippet: false,
1588 sort_text: Some("80000010"),
1589 sort_kind: 3,
1590 sort_label: "await.eq",
1591 },
1592 SortableMatch {
1593 string_match: StringMatch {
1594 candidate_id: 18,
1595 score: 0.625,
1596 positions: vec![],
1597 string: "await.or".to_string(),
1598 },
1599 is_snippet: false,
1600 sort_text: Some("7ffffff8"),
1601 sort_kind: 3,
1602 sort_label: "await.or",
1603 },
1604 SortableMatch {
1605 string_match: StringMatch {
1606 candidate_id: 21,
1607 score: 0.5555555555555556,
1608 positions: vec![],
1609 string: "await.zip".to_string(),
1610 },
1611 is_snippet: false,
1612 sort_text: Some("80000006"),
1613 sort_kind: 3,
1614 sort_label: "await.zip",
1615 },
1616 SortableMatch {
1617 string_match: StringMatch {
1618 candidate_id: 20,
1619 score: 0.5555555555555556,
1620 positions: vec![],
1621 string: "await.xor".to_string(),
1622 },
1623 is_snippet: false,
1624 sort_text: Some("7ffffff8"),
1625 sort_kind: 3,
1626 sort_label: "await.xor",
1627 },
1628 SortableMatch {
1629 string_match: StringMatch {
1630 candidate_id: 15,
1631 score: 0.5555555555555556,
1632 positions: vec![],
1633 string: "await.and".to_string(),
1634 },
1635 is_snippet: false,
1636 sort_text: Some("80000006"),
1637 sort_kind: 3,
1638 sort_label: "await.and",
1639 },
1640 SortableMatch {
1641 string_match: StringMatch {
1642 candidate_id: 9,
1643 score: 0.5555555555555556,
1644 positions: vec![],
1645 string: "await.map".to_string(),
1646 },
1647 is_snippet: false,
1648 sort_text: Some("80000006"),
1649 sort_kind: 3,
1650 sort_label: "await.map",
1651 },
1652 SortableMatch {
1653 string_match: StringMatch {
1654 candidate_id: 47,
1655 score: 0.5,
1656 positions: vec![],
1657 string: "await.take".to_string(),
1658 },
1659 is_snippet: false,
1660 sort_text: Some("7ffffff8"),
1661 sort_kind: 3,
1662 sort_label: "await.take",
1663 },
1664 ];260 ];
1665 CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::Top);261 let matches = sort_matches("await", &completions, SnippetSortOrder::Top, cx).await;
1666 assert_eq!(262 assert_eq!(matches[0], "await");
1667 matches
1668 .iter()
1669 .map(|m| m.string_match.string.as_str())
1670 .collect::<Vec<&str>>(),
1671 vec![
1672 "await",
1673 "await.or",
1674 "await.eq",
1675 "await.ne",
1676 "await.xor",
1677 "await.take",
1678 "await.and",
1679 "await.map",
1680 "await.zip"
1681 ]
1682 );
1683}263}
1684 264
1685#[gpui::test]265#[gpui::test]
1686fn test_sort_matches_for_python_init(_cx: &mut TestAppContext) {266async fn test_sort_matches_for_python_init(cx: &mut TestAppContext) {
1687 // Case 1: "__in"267 // Case 1: "__in"
1688 let query: Option<&str> = Some("__in");268 let completions = vec![
1689 let mut matches: Vec<SortableMatch<'_>> = vec![269 CompletionBuilder::function("__init__", "05.0003.__init__"),
1690 SortableMatch {270 CompletionBuilder::function("__init__", "05.0003"),
1691 string_match: StringMatch {271 CompletionBuilder::function("__instancecheck__", "05.0005.__instancecheck__"),
1692 candidate_id: 211,272 CompletionBuilder::function("__init_subclass__", "05.0004.__init_subclass__"),
1693 score: 0.5,273 CompletionBuilder::function("__instancecheck__", "05.0005"),
1694 positions: vec![],274 CompletionBuilder::function("__init_subclass__", "05.0004"),
1695 string: "__init__".to_string(),
1696 },
1697 is_snippet: false,
1698 sort_text: Some("05.0003.__init__"),
1699 sort_kind: 3,
1700 sort_label: "__init__",
1701 },
1702 SortableMatch {
1703 string_match: StringMatch {
1704 candidate_id: 0,
1705 score: 0.5,
1706 positions: vec![],
1707 string: "__init__".to_string(),
1708 },
1709 is_snippet: false,
1710 sort_text: Some("05.0003"),
1711 sort_kind: 3,
1712 sort_label: "__init__",
1713 },
1714 SortableMatch {
1715 string_match: StringMatch {
1716 candidate_id: 215,
1717 score: 0.23529411764705882,
1718 positions: vec![],
1719 string: "__instancecheck__".to_string(),
1720 },
1721 is_snippet: false,
1722 sort_text: Some("05.0005.__instancecheck__"),
1723 sort_kind: 3,
1724 sort_label: "__instancecheck__",
1725 },
1726 SortableMatch {
1727 string_match: StringMatch {
1728 candidate_id: 213,
1729 score: 0.23529411764705882,
1730 positions: vec![],
1731 string: "__init_subclass__".to_string(),
1732 },
1733 is_snippet: false,
1734 sort_text: Some("05.0004.__init_subclass__"),
1735 sort_kind: 3,
1736 sort_label: "__init_subclass__",
1737 },
1738 SortableMatch {
1739 string_match: StringMatch {
1740 candidate_id: 4,
1741 score: 0.23529411764705882,
1742 positions: vec![],
1743 string: "__instancecheck__".to_string(),
1744 },
1745 is_snippet: false,
1746 sort_text: Some("05.0005"),
1747 sort_kind: 3,
1748 sort_label: "__instancecheck__",
1749 },
1750 SortableMatch {
1751 string_match: StringMatch {
1752 candidate_id: 2,
1753 score: 0.23529411764705882,
1754 positions: vec![],
1755 string: "__init_subclass__".to_string(),
1756 },
1757 is_snippet: false,
1758 sort_text: Some("05.0004"),
1759 sort_kind: 3,
1760 sort_label: "__init_subclass__",
1761 },
1762 ];275 ];
1763 CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::Top);276 let matches = sort_matches("__in", &completions, SnippetSortOrder::Top, cx).await;
1764 assert_eq!(277 assert_eq!(matches[0], "__init__");
1765 matches278 assert_eq!(matches[1], "__init__");
1766 .iter()279
1767 .map(|m| m.string_match.string.as_str())
1768 .collect::<Vec<&str>>(),
1769 vec![
1770 "__init__",
1771 "__init__",
1772 "__init_subclass__",
1773 "__init_subclass__",
1774 "__instancecheck__",
1775 "__instancecheck__",
1776 ]
1777 );
1778 // Case 2: "__ini"280 // Case 2: "__ini"
1779 let query: Option<&str> = Some("__ini");281 let completions = vec![
1780 let mut matches: Vec<SortableMatch<'_>> = vec![282 CompletionBuilder::function("__init__", "05.0004.__init__"),
1781 SortableMatch {283 CompletionBuilder::function("__init__", "05.0004"),
1782 string_match: StringMatch {284 CompletionBuilder::function("__init_subclass__", "05.0003.__init_subclass__"),
1783 candidate_id: 9,285 CompletionBuilder::function("__init_subclass__", "05.0003"),
1784 score: 0.625,
1785 positions: vec![],
1786 string: "__init__".to_string(),
1787 },
1788 is_snippet: false,
1789 sort_text: Some("05.0004.__init__"),
1790 sort_kind: 3,
1791 sort_label: "__init__",
1792 },
1793 SortableMatch {
1794 string_match: StringMatch {
1795 candidate_id: 0,
1796 score: 0.625,
1797 positions: vec![],
1798 string: "__init__".to_string(),
1799 },
1800 is_snippet: false,
1801 sort_text: Some("05.0004"),
1802 sort_kind: 3,
1803 sort_label: "__init__",
1804 },
1805 SortableMatch {
1806 string_match: StringMatch {
1807 candidate_id: 10,
1808 score: 0.29411764705882354,
1809 positions: vec![],
1810 string: "__init_subclass__".to_string(),
1811 },
1812 is_snippet: false,
1813 sort_text: Some("05.0003.__init_subclass__"),
1814 sort_kind: 3,
1815 sort_label: "__init_subclass__",
1816 },
1817 SortableMatch {
1818 string_match: StringMatch {
1819 candidate_id: 1,
1820 score: 0.29411764705882354,
1821 positions: vec![],
1822 string: "__init_subclass__".to_string(),
1823 },
1824 is_snippet: false,
1825 sort_text: Some("05.0003"),
1826 sort_kind: 3,
1827 sort_label: "__init_subclass__",
1828 },
1829 ];286 ];
1830 CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::Top);287 let matches = sort_matches("__ini", &completions, SnippetSortOrder::Top, cx).await;
1831 assert_eq!(288 assert_eq!(matches[0], "__init__");
1832 matches289 assert_eq!(matches[1], "__init__");
1833 .iter()290
1834 .map(|m| m.string_match.string.as_str())
1835 .collect::<Vec<&str>>(),
1836 vec![
1837 "__init__",
1838 "__init__",
1839 "__init_subclass__",
1840 "__init_subclass__",
1841 ]
1842 );
1843 // Case 3: "__init"291 // Case 3: "__init"
1844 let query: Option<&str> = Some("__init");292 let completions = vec![
1845 let mut matches: Vec<SortableMatch<'_>> = vec![293 CompletionBuilder::function("__init__", "05.0000.__init__"),
1846 SortableMatch {294 CompletionBuilder::function("__init__", "05.0000"),
1847 string_match: StringMatch {295 CompletionBuilder::function("__init_subclass__", "05.0001.__init_subclass__"),
1848 candidate_id: 7,296 CompletionBuilder::function("__init_subclass__", "05.0001"),
1849 score: 0.75,
1850 positions: vec![],
1851 string: "__init__".to_string(),
1852 },
1853 is_snippet: false,
1854 sort_text: Some("05.0000.__init__"),
1855 sort_kind: 3,
1856 sort_label: "__init__",
1857 },
1858 SortableMatch {
1859 string_match: StringMatch {
1860 candidate_id: 0,
1861 score: 0.75,
1862 positions: vec![],
1863 string: "__init__".to_string(),
1864 },
1865 is_snippet: false,
1866 sort_text: Some("05.0000"),
1867 sort_kind: 3,
1868 sort_label: "__init__",
1869 },
1870 SortableMatch {
1871 string_match: StringMatch {
1872 candidate_id: 8,
1873 score: 0.3529411764705882,
1874 positions: vec![],
1875 string: "__init_subclass__".to_string(),
1876 },
1877 is_snippet: false,
1878 sort_text: Some("05.0001.__init_subclass__"),
1879 sort_kind: 3,
1880 sort_label: "__init_subclass__",
1881 },
1882 SortableMatch {
1883 string_match: StringMatch {
1884 candidate_id: 1,
1885 score: 0.3529411764705882,
1886 positions: vec![],
1887 string: "__init_subclass__".to_string(),
1888 },
1889 is_snippet: false,
1890 sort_text: Some("05.0001"),
1891 sort_kind: 3,
1892 sort_label: "__init_subclass__",
1893 },
1894 ];297 ];
1895 CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::Top);298 let matches = sort_matches("__init", &completions, SnippetSortOrder::Top, cx).await;
1896 assert_eq!(299 assert_eq!(matches[0], "__init__");
1897 matches300 assert_eq!(matches[1], "__init__");
1898 .iter()301
1899 .map(|m| m.string_match.string.as_str())
1900 .collect::<Vec<&str>>(),
1901 vec![
1902 "__init__",
1903 "__init__",
1904 "__init_subclass__",
1905 "__init_subclass__",
1906 ]
1907 );
1908 // Case 4: "__init_"302 // Case 4: "__init_"
1909 let query: Option<&str> = Some("__init_");303 let completions = vec![
1910 let mut matches: Vec<SortableMatch<'_>> = vec![304 CompletionBuilder::function("__init__", "11.9999.__init__"),
1911 SortableMatch {305 CompletionBuilder::function("__init__", "11.9999"),
1912 string_match: StringMatch {306 CompletionBuilder::function("__init_subclass__", "05.0000.__init_subclass__"),
1913 candidate_id: 4,307 CompletionBuilder::function("__init_subclass__", "05.0000"),
1914 score: 0.875,
1915 positions: vec![],
1916 string: "__init__".to_string(),
1917 },
1918 is_snippet: false,
1919 sort_text: Some("11.9999.__init__"),
1920 sort_kind: 3,
1921 sort_label: "__init__",
1922 },
1923 SortableMatch {
1924 string_match: StringMatch {
1925 candidate_id: 0,
1926 score: 0.875,
1927 positions: vec![],
1928 string: "__init__".to_string(),
1929 },
1930 is_snippet: false,
1931 sort_text: Some("11.9999"),
1932 sort_kind: 3,
1933 sort_label: "__init__",
1934 },
1935 SortableMatch {
1936 string_match: StringMatch {
1937 candidate_id: 5,
1938 score: 0.4117647058823529,
1939 positions: vec![],
1940 string: "__init_subclass__".to_string(),
1941 },
1942 is_snippet: false,
1943 sort_text: Some("05.0000.__init_subclass__"),
1944 sort_kind: 3,
1945 sort_label: "__init_subclass__",
1946 },
1947 SortableMatch {
1948 string_match: StringMatch {
1949 candidate_id: 1,
1950 score: 0.4117647058823529,
1951 positions: vec![],
1952 string: "__init_subclass__".to_string(),
1953 },
1954 is_snippet: false,
1955 sort_text: Some("05.0000"),
1956 sort_kind: 3,
1957 sort_label: "__init_subclass__",
1958 },
1959 ];308 ];
1960 CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::Top);309 let matches = sort_matches("__init_", &completions, SnippetSortOrder::Top, cx).await;
1961 assert_eq!(310 assert_eq!(matches[0], "__init__");
1962 matches311 assert_eq!(matches[1], "__init__");
1963 .iter()
1964 .map(|m| m.string_match.string.as_str())
1965 .collect::<Vec<&str>>(),
1966 vec![
1967 "__init__",
1968 "__init__",
1969 "__init_subclass__",
1970 "__init_subclass__",
1971 ]
1972 );
1973}312}
1974 313
1975#[gpui::test]314#[gpui::test]
1976fn test_sort_matches_for_rust_into(_cx: &mut TestAppContext) {315async fn test_sort_matches_for_rust_into(cx: &mut TestAppContext) {
1977 // Case 1: "int"316 // Case 1: "int"
1978 let query: Option<&str> = Some("int");317 let completions = vec![
1979 let mut matches: Vec<SortableMatch<'_>> = vec![318 CompletionBuilder::function("into", "80000004"),
1980 SortableMatch {319 CompletionBuilder::function("try_into", "80000004"),
1981 string_match: StringMatch {320 CompletionBuilder::snippet("println", "80000004"),
1982 candidate_id: 67,321 CompletionBuilder::function("clone_into", "80000004"),
1983 score: 0.75,322 CompletionBuilder::function("into_searcher", "80000000"),
1984 positions: vec![],323 CompletionBuilder::snippet("eprintln", "80000004"),
1985 string: "into".to_string(),
1986 },
1987 is_snippet: false,
1988 sort_text: Some("80000004"),
1989 sort_kind: 3,
1990 sort_label: "into",
1991 },
1992 SortableMatch {
1993 string_match: StringMatch {
1994 candidate_id: 68,
1995 score: 0.30000000000000004,
1996 positions: vec![],
1997 string: "try_into".to_string(),
1998 },
1999 is_snippet: false,
2000 sort_text: Some("80000004"),
2001 sort_kind: 3,
2002 sort_label: "try_into",
2003 },
2004 SortableMatch {
2005 string_match: StringMatch {
2006 candidate_id: 108,
2007 score: 0.2571428571428571,
2008 positions: vec![],
2009 string: "println".to_string(),
2010 },
2011 is_snippet: true,
2012 sort_text: Some("80000004"),
2013 sort_kind: 3,
2014 sort_label: "println",
2015 },
2016 SortableMatch {
2017 string_match: StringMatch {
2018 candidate_id: 73,
2019 score: 0.24,
2020 positions: vec![],
2021 string: "clone_into".to_string(),
2022 },
2023 is_snippet: false,
2024 sort_text: Some("80000004"),
2025 sort_kind: 3,
2026 sort_label: "clone_into",
2027 },
2028 SortableMatch {
2029 string_match: StringMatch {
2030 candidate_id: 1,
2031 score: 0.23076923076923078,
2032 positions: vec![],
2033 string: "into_searcher".to_string(),
2034 },
2035 is_snippet: false,
2036 sort_text: Some("80000000"),
2037 sort_kind: 3,
2038 sort_label: "into_searcher",
2039 },
2040 SortableMatch {
2041 string_match: StringMatch {
2042 candidate_id: 109,
2043 score: 0.22499999999999998,
2044 positions: vec![],
2045 string: "eprintln".to_string(),
2046 },
2047 is_snippet: true,
2048 sort_text: Some("80000004"),
2049 sort_kind: 3,
2050 sort_label: "eprintln",
2051 },
2052 ];324 ];
2053 CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default());325 let matches = sort_matches("int", &completions, SnippetSortOrder::default(), cx).await;
2054 assert_eq!(326 assert_eq!(matches[0], "into");
2055 matches[0].string_match.string.as_str(),327
2056 "into",
2057 "Match order not expected"
2058 );
2059 // Case 2: "into"328 // Case 2: "into"
2060 let query: Option<&str> = Some("into");329 let completions = vec![
2061 let mut matches: Vec<SortableMatch<'_>> = vec![330 CompletionBuilder::function("into", "80000004"),
2062 SortableMatch {331 CompletionBuilder::function("try_into", "80000004"),
2063 string_match: StringMatch {332 CompletionBuilder::function("clone_into", "80000004"),
2064 candidate_id: 65,333 CompletionBuilder::function("into_searcher", "80000000"),
2065 score: 1.0,334 CompletionBuilder::function("split_terminator", "7fffffff"),
2066 positions: vec![],335 CompletionBuilder::function("rsplit_terminator", "7fffffff"),
2067 string: "into".to_string(),
2068 },
2069 is_snippet: false,
2070 sort_text: Some("80000004"),
2071 sort_kind: 3,
2072 sort_label: "into",
2073 },
2074 SortableMatch {
2075 string_match: StringMatch {
2076 candidate_id: 66,
2077 score: 0.4,
2078 positions: vec![],
2079 string: "try_into".to_string(),
2080 },
2081 is_snippet: false,
2082 sort_text: Some("80000004"),
2083 sort_kind: 3,
2084 sort_label: "try_into",
2085 },
2086 SortableMatch {
2087 string_match: StringMatch {
2088 candidate_id: 71,
2089 score: 0.32,
2090 positions: vec![],
2091 string: "clone_into".to_string(),
2092 },
2093 is_snippet: false,
2094 sort_text: Some("80000004"),
2095 sort_kind: 3,
2096 sort_label: "clone_into",
2097 },
2098 SortableMatch {
2099 string_match: StringMatch {
2100 candidate_id: 0,
2101 score: 0.3076923076923077,
2102 positions: vec![],
2103 string: "into_searcher".to_string(),
2104 },
2105 is_snippet: false,
2106 sort_text: Some("80000000"),
2107 sort_kind: 3,
2108 sort_label: "into_searcher",
2109 },
2110 SortableMatch {
2111 string_match: StringMatch {
2112 candidate_id: 27,
2113 score: 0.09,
2114 positions: vec![],
2115 string: "split_terminator".to_string(),
2116 },
2117 is_snippet: false,
2118 sort_text: Some("7fffffff"),
2119 sort_kind: 3,
2120 sort_label: "split_terminator",
2121 },
2122 SortableMatch {
2123 string_match: StringMatch {
2124 candidate_id: 28,
2125 score: 0.08470588235294117,
2126 positions: vec![],
2127 string: "rsplit_terminator".to_string(),
2128 },
2129 is_snippet: false,
2130 sort_text: Some("7fffffff"),
2131 sort_kind: 3,
2132 sort_label: "rsplit_terminator",
2133 },
2134 ];336 ];
2135 CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default());337 let matches = sort_matches("into", &completions, SnippetSortOrder::default(), cx).await;
2136 assert_eq!(338 assert_eq!(matches[0], "into");
2137 matches[0].string_match.string.as_str(),
2138 "into",
2139 "Match order not expected"
2140 );
2141}339}
2142 340
2143#[gpui::test]341#[gpui::test]
2144fn test_sort_matches_for_variable_over_function(_cx: &mut TestAppContext) {342async fn test_sort_matches_for_variable_over_function(cx: &mut TestAppContext) {
2145 // Case 1: "serial"343 // Case 1: "serial"
2146 let query: Option<&str> = Some("serial");344 let completions = vec![
2147 let mut matches: Vec<SortableMatch<'_>> = vec![345 CompletionBuilder::function("serialize", "80000000"),
2148 SortableMatch {346 CompletionBuilder::function("serialize", "80000000"),
2149 string_match: StringMatch {347 CompletionBuilder::variable("serialization_key", "7ffffffe"),
2150 candidate_id: 33,348 CompletionBuilder::function("serialize_version", "80000000"),
2151 score: 0.6666666666666666,349 CompletionBuilder::function("deserialize", "80000000"),
2152 positions: vec![],
2153 string: "serialize".to_string(),
2154 },
2155 is_snippet: false,
2156 sort_text: Some("80000000"),
2157 sort_kind: 3,
2158 sort_label: "serialize",
2159 },
2160 SortableMatch {
2161 string_match: StringMatch {
2162 candidate_id: 32,
2163 score: 0.6666666666666666,
2164 positions: vec![],
2165 string: "serialize".to_string(),
2166 },
2167 is_snippet: false,
2168 sort_text: Some("80000000"),
2169 sort_kind: 3,
2170 sort_label: "serialize",
2171 },
2172 SortableMatch {
2173 string_match: StringMatch {
2174 candidate_id: 103,
2175 score: 0.3529411764705882,
2176 positions: vec![],
2177 string: "serialization_key".to_string(),
2178 },
2179 is_snippet: false,
2180 sort_text: Some("7ffffffe"),
2181 sort_kind: 1,
2182 sort_label: "serialization_key",
2183 },
2184 SortableMatch {
2185 string_match: StringMatch {
2186 candidate_id: 18,
2187 score: 0.3529411764705882,
2188 positions: vec![],
2189 string: "serialize_version".to_string(),
2190 },
2191 is_snippet: false,
2192 sort_text: Some("80000000"),
2193 sort_kind: 3,
2194 sort_label: "serialize_version",
2195 },
2196 SortableMatch {
2197 string_match: StringMatch {
2198 candidate_id: 65,
2199 score: 0.32727272727272727,
2200 positions: vec![],
2201 string: "deserialize".to_string(),
2202 },
2203 is_snippet: false,
2204 sort_text: Some("80000000"),
2205 sort_kind: 3,
2206 sort_label: "deserialize",
2207 },
2208 ];350 ];
2209 CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default());351 let matches = sort_matches("serial", &completions, SnippetSortOrder::default(), cx).await;
2210 assert_eq!(352 assert_eq!(matches[0], "serialization_key");
2211 matches353 assert_eq!(matches[1], "serialize");
2212 .iter()354 assert_eq!(matches[2], "serialize");
2213 .map(|m| m.string_match.string.as_str())355 assert_eq!(matches[3], "serialize_version");
2214 .collect::<Vec<&str>>(),356 assert_eq!(matches[4], "deserialize");
2215 vec![
2216 "serialization_key",
2217 "serialize",
2218 "serialize",
2219 "serialize_version",
2220 "deserialize"
2221 ]
2222 );
2223}357}
2224 358
2225#[gpui::test]359#[gpui::test]
2226fn test_sort_matches_for_local_methods_over_library(_cx: &mut TestAppContext) {360async fn test_sort_matches_for_local_methods_over_library(cx: &mut TestAppContext) {
2227 // Case 1: "setis"361 // Case 1: "setis"
2228 let query: Option<&str> = Some("setis");362 let completions = vec![
2229 let mut matches: Vec<SortableMatch<'_>> = vec![363 CompletionBuilder::variable("setISODay", "16"),
2230 SortableMatch {364 CompletionBuilder::variable("setISOWeek", "16"),
2231 string_match: StringMatch {365 CompletionBuilder::variable("setISOWeekYear", "16"),
2232 candidate_id: 1200,366 CompletionBuilder::function("setISOWeekYear", "16"),
2233 score: 0.5555555555555556,367 CompletionBuilder::variable("setIsRefreshing", "11"),
2234 positions: vec![],368 CompletionBuilder::function("setFips", "16"),
2235 string: "setISODay".to_string(),
2236 },
2237 is_snippet: false,
2238 sort_text: Some("16"),
2239 sort_kind: 1,
2240 sort_label: "setISODay",
2241 },
2242 SortableMatch {
2243 string_match: StringMatch {
2244 candidate_id: 1216,
2245 score: 0.5,
2246 positions: vec![],
2247 string: "setISOWeek".to_string(),
2248 },
2249 is_snippet: false,
2250 sort_text: Some("16"),
2251 sort_kind: 1,
2252 sort_label: "setISOWeek",
2253 },
2254 SortableMatch {
2255 string_match: StringMatch {
2256 candidate_id: 1232,
2257 score: 0.3571428571428571,
2258 positions: vec![],
2259 string: "setISOWeekYear".to_string(),
2260 },
2261 is_snippet: false,
2262 sort_text: Some("16"),
2263 sort_kind: 1,
2264 sort_label: "setISOWeekYear",
2265 },
2266 SortableMatch {
2267 string_match: StringMatch {
2268 candidate_id: 1217,
2269 score: 0.3571428571428571,
2270 positions: vec![],
2271 string: "setISOWeekYear".to_string(),
2272 },
2273 is_snippet: false,
2274 sort_text: Some("16"),
2275 sort_kind: 3,
2276 sort_label: "setISOWeekYear",
2277 },
2278 SortableMatch {
2279 string_match: StringMatch {
2280 candidate_id: 53,
2281 score: 0.3333333333333333,
2282 positions: vec![],
2283 string: "setIsRefreshing".to_string(),
2284 },
2285 is_snippet: false,
2286 sort_text: Some("11"),
2287 sort_kind: 1,
2288 sort_label: "setIsRefreshing",
2289 },
2290 SortableMatch {
2291 string_match: StringMatch {
2292 candidate_id: 1180,
2293 score: 0.2571428571428571,
2294 positions: vec![],
2295 string: "setFips".to_string(),
2296 },
2297 is_snippet: false,
2298 sort_text: Some("16"),
2299 sort_kind: 3,
2300 sort_label: "setFips",
2301 },
2302 ];369 ];
2303 CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default());370 let matches = sort_matches("setis", &completions, SnippetSortOrder::default(), cx).await;
2304 assert_eq!(371 assert_eq!(matches[0], "setIsRefreshing");
2305 matches372 assert_eq!(matches[1], "setISODay");
2306 .iter()373 assert_eq!(matches[2], "setISOWeek");
2307 .map(|m| m.string_match.string.as_str())
2308 .collect::<Vec<&str>>(),
2309 vec![
2310 "setIsRefreshing",
2311 "setISODay",
2312 "setISOWeek",
2313 "setISOWeekYear",
2314 "setISOWeekYear",
2315 "setFips"
2316 ]
2317 );
2318}374}
2319 375
2320#[gpui::test]376#[gpui::test]
2321fn test_sort_matches_for_priotize_not_exact_match(_cx: &mut TestAppContext) {377async fn test_sort_matches_for_prioritize_not_exact_match(cx: &mut TestAppContext) {
2322 // Case 1: "item"378 // Case 1: "item"
2323 let query: Option<&str> = Some("item");379 let completions = vec![
2324 let mut matches: Vec<SortableMatch<'_>> = vec![380 CompletionBuilder::function("Item", "16"),
2325 SortableMatch {381 CompletionBuilder::variable("Item", "16"),
2326 string_match: StringMatch {382 CompletionBuilder::variable("items", "11"),
2327 candidate_id: 1115,383 CompletionBuilder::function("ItemText", "16"),
2328 score: 1.0,
2329 positions: vec![],
2330 string: "Item".to_string(),
2331 },
2332 is_snippet: false,
2333 sort_text: Some("16"),
2334 sort_kind: 3,
2335 sort_label: "Item",
2336 },
2337 SortableMatch {
2338 string_match: StringMatch {
2339 candidate_id: 1108,
2340 score: 1.0,
2341 positions: vec![],
2342 string: "Item".to_string(),
2343 },
2344 is_snippet: false,
2345 sort_text: Some("16"),
2346 sort_kind: 1,
2347 sort_label: "Item",
2348 },
2349 SortableMatch {
2350 string_match: StringMatch {
2351 candidate_id: 26,
2352 score: 0.8,
2353 positions: vec![],
2354 string: "items".to_string(),
2355 },
2356 is_snippet: false,
2357 sort_text: Some("11"),
2358 sort_kind: 1,
2359 sort_label: "items",
2360 },
2361 SortableMatch {
2362 string_match: StringMatch {
2363 candidate_id: 1138,
2364 score: 0.5,
2365 positions: vec![],
2366 string: "ItemText".to_string(),
2367 },
2368 is_snippet: false,
2369 sort_text: Some("16"),
2370 sort_kind: 3,
2371 sort_label: "ItemText",
2372 },
2373 ];384 ];
2374 CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default());385 let matches = sort_matches("item", &completions, SnippetSortOrder::default(), cx).await;
2375 assert_eq!(386 assert_eq!(matches[0], "items");
2376 matches387 assert_eq!(matches[1], "Item");
2377 .iter()388 assert_eq!(matches[2], "Item");
2378 .map(|m| m.string_match.string.as_str())389 assert_eq!(matches[3], "ItemText");
2379 .collect::<Vec<&str>>(),390}
2380 vec!["items", "Item", "Item", "ItemText"]391
2381 );392#[gpui::test]
393async fn test_sort_matches_for_tailwind_classes(cx: &mut TestAppContext) {
394 let completions = vec![
395 CompletionBuilder::function("rounded-full", "15788"),
396 CompletionBuilder::variable("rounded-t-full", "15846"),
397 CompletionBuilder::variable("rounded-b-full", "15731"),
398 CompletionBuilder::function("rounded-tr-full", "15866"),
399 ];
400 // Case 1: "rounded-full"
401 let matches = sort_matches(
402 "rounded-full",
403 &completions,
404 SnippetSortOrder::default(),
405 cx,
406 )
407 .await;
408 assert_eq!(matches[0], "rounded-full");
409 // Case 2: "roundedfull"
410 let matches = sort_matches("roundedfull", &completions, SnippetSortOrder::default(), cx).await;
411 assert_eq!(matches[0], "rounded-full");
412}
413
414struct CompletionBuilder;
415
416impl CompletionBuilder {
417 fn constant(label: &str, sort_text: &str) -> Completion {
418 Self::new(label, sort_text, CompletionItemKind::CONSTANT)
419 }
420
421 fn function(label: &str, sort_text: &str) -> Completion {
422 Self::new(label, sort_text, CompletionItemKind::FUNCTION)
423 }
424
425 fn variable(label: &str, sort_text: &str) -> Completion {
426 Self::new(label, sort_text, CompletionItemKind::VARIABLE)
427 }
428
429 fn keyword(label: &str, sort_text: &str) -> Completion {
430 Self::new(label, sort_text, CompletionItemKind::KEYWORD)
431 }
432
433 fn snippet(label: &str, sort_text: &str) -> Completion {
434 Self::new(label, sort_text, CompletionItemKind::SNIPPET)
435 }
436
437 fn new(label: &str, sort_text: &str, kind: CompletionItemKind) -> Completion {
438 Completion {
439 replace_range: Anchor::MIN..Anchor::MAX,
440 new_text: label.to_string(),
441 label: CodeLabel {
442 text: label.to_string(),
443 runs: Default::default(),
444 filter_range: 0..label.len(),
445 },
446 documentation: None,
447 source: CompletionSource::Lsp {
448 insert_range: None,
449 server_id: LanguageServerId(0),
450 lsp_completion: Box::new(CompletionItem {
451 label: label.to_string(),
452 kind: Some(kind),
453 sort_text: Some(sort_text.to_string()),
454 ..Default::default()
455 }),
456 lsp_defaults: None,
457 resolved: false,
458 },
459 icon_path: None,
460 insert_text_mode: None,
461 confirm: None,
462 }
463 }
464}
465
466async fn sort_matches(
467 query: &str,
468 completions: &Vec<Completion>,
469 snippet_sort_order: SnippetSortOrder,
470 cx: &mut TestAppContext,
471) -> Vec<String> {
472 let candidates: Arc<[StringMatchCandidate]> = completions
473 .iter()
474 .enumerate()
475 .map(|(id, completion)| StringMatchCandidate::new(id, &completion.label.text))
476 .collect();
477 let cancel_flag = Arc::new(AtomicBool::new(false));
478 let background_executor = cx.executor();
479 let matches = fuzzy::match_strings(
480 &candidates,
481 query,
482 query.chars().any(|c| c.is_uppercase()),
483 100,
484 &cancel_flag,
485 background_executor,
486 )
487 .await;
488 let sorted_matches = CompletionsMenu::sort_string_matches(
489 matches,
490 Some(query),
491 snippet_sort_order,
492 &completions,
493 );
494 sorted_matches.into_iter().map(|m| m.string).collect()
2382}495}
crates/editor/src/code_context_menus.rs+47−89
@@ -722,10 +722,9 @@ impl CompletionsMenu { · 721 unmodified lines
722 let last_rendered_range = self.last_rendered_range.clone();722 let last_rendered_range = self.last_rendered_range.clone();
723 let style = style.clone();723 let style = style.clone();
724 let list = uniform_list(724 let list = uniform_list(
725 cx.entity().clone(),
726 "completions",725 "completions",
727 self.entries.borrow().len(),726 self.entries.borrow().len(),
728 move |_editor, range, _window, cx| {727 cx.processor(move |_editor, range: Range<usize>, _window, cx| {
729 last_rendered_range.borrow_mut().replace(range.clone());728 last_rendered_range.borrow_mut().replace(range.clone());
730 let start_ix = range.start;729 let start_ix = range.start;
731 let completions_guard = completions.borrow_mut();730 let completions_guard = completions.borrow_mut();
@@ -837,7 +836,7 @@ impl CompletionsMenu { · 105 unmodified lines
837 )836 )
838 })837 })
839 .collect()838 .collect()
840 },839 }),
841 )840 )
842 .occlude()841 .occlude()
843 .max_h(max_height_in_lines as f32 * window.line_height())842 .max_h(max_height_in_lines as f32 * window.line_height())
@@ -1045,59 +1044,19 @@ impl CompletionsMenu { · 201 unmodified lines
1045 self.handle_selection_changed(provider.as_deref(), window, cx);1044 self.handle_selection_changed(provider.as_deref(), window, cx);
1046 }1045 }
1047 1046
1048 fn sort_string_matches(1047 pub fn sort_string_matches(
1049 matches: Vec<StringMatch>,1048 matches: Vec<StringMatch>,
1050 query: Option<&str>,1049 query: Option<&str>,
1051 snippet_sort_order: SnippetSortOrder,1050 snippet_sort_order: SnippetSortOrder,
1052 completions: &[Completion],1051 completions: &[Completion],
1053 ) -> Vec<StringMatch> {1052 ) -> Vec<StringMatch> {
1054 let mut sortable_items: Vec<SortableMatch<'_>> = matches1053 let mut matches = matches;
1055 .into_iter()
1056 .map(|string_match| {
1057 let completion = &completions[string_match.candidate_id];
1058
1059 let is_snippet = matches!(
1060 &completion.source,
1061 CompletionSource::Lsp { lsp_completion, .. }
1062 if lsp_completion.kind == Some(CompletionItemKind::SNIPPET)
1063 );
1064
1065 let sort_text =
1066 if let CompletionSource::Lsp { lsp_completion, .. } = &completion.source {
1067 lsp_completion.sort_text.as_deref()
1068 } else {
1069 None
1070 };
1071
1072 let (sort_kind, sort_label) = completion.sort_key();
1073
1074 SortableMatch {
1075 string_match,
1076 is_snippet,
1077 sort_text,
1078 sort_kind,
1079 sort_label,
1080 }
1081 })
1082 .collect();
1083
1084 Self::sort_matches(&mut sortable_items, query, snippet_sort_order);
1085
1086 sortable_items
1087 .into_iter()
1088 .map(|sortable| sortable.string_match)
1089 .collect()
1090 }
1091 1054
1092 pub fn sort_matches(
1093 matches: &mut Vec<SortableMatch<'_>>,
1094 query: Option<&str>,
1095 snippet_sort_order: SnippetSortOrder,
1096 ) {
1097 #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]1055 #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
1098 enum MatchTier<'a> {1056 enum MatchTier<'a> {
1099 WordStartMatch {1057 WordStartMatch {
1100 sort_mixed_case_prefix_length: Reverse<usize>,1058 sort_capitalize: Reverse<usize>,
1059 sort_positions: Vec<usize>,
1101 sort_snippet: Reverse<i32>,1060 sort_snippet: Reverse<i32>,
1102 sort_kind: usize,1061 sort_kind: usize,
1103 sort_fuzzy_bracket: Reverse<usize>,1062 sort_fuzzy_bracket: Reverse<usize>,
@@ -1115,10 +1074,7 @@ impl CompletionsMenu { · 11 unmodified lines
1115 1074
1116 // In a fuzzy bracket, matches with a score of 1.0 are prioritized.1075 // In a fuzzy bracket, matches with a score of 1.0 are prioritized.
1117 // The remaining matches are partitioned into two groups at 3/5 of the max_score.1076 // The remaining matches are partitioned into two groups at 3/5 of the max_score.
1118 let max_score = matches1077 let max_score = matches.iter().map(|mat| mat.score).fold(0.0, f64::max);
1119 .iter()
1120 .map(|mat| mat.string_match.score)
1121 .fold(0.0, f64::max);
1122 let fuzzy_bracket_threshold = max_score * (3.0 / 5.0);1078 let fuzzy_bracket_threshold = max_score * (3.0 / 5.0);
1123 1079
1124 let query_start_lower = query1080 let query_start_lower = query
@@ -1126,13 +1082,30 @@ impl CompletionsMenu { · 1 unmodified line
1126 .and_then(|q| q.chars().next())1082 .and_then(|q| q.chars().next())
1127 .and_then(|c| c.to_lowercase().next());1083 .and_then(|c| c.to_lowercase().next());
1128 1084
1129 matches.sort_unstable_by_key(|mat| {1085 matches.sort_unstable_by_key(|string_match| {
1130 let score = mat.string_match.score;1086 let completion = &completions[string_match.candidate_id];
1087
1088 let is_snippet = matches!(
1089 &completion.source,
1090 CompletionSource::Lsp { lsp_completion, .. }
1091 if lsp_completion.kind == Some(CompletionItemKind::SNIPPET)
1092 );
1093
1094 let sort_text = if let CompletionSource::Lsp { lsp_completion, .. } = &completion.source
1095 {
1096 lsp_completion.sort_text.as_deref()
1097 } else {
1098 None
1099 };
1100
1101 let (sort_kind, sort_label) = completion.sort_key();
1102
1103 let score = string_match.score;
1131 let sort_score = Reverse(OrderedFloat(score));1104 let sort_score = Reverse(OrderedFloat(score));
1132 1105
1133 let query_start_doesnt_match_split_words = query_start_lower1106 let query_start_doesnt_match_split_words = query_start_lower
1134 .map(|query_char| {1107 .map(|query_char| {
1135 !split_words(&mat.string_match.string).any(|word| {1108 !split_words(&string_match.string).any(|word| {
1136 word.chars()1109 word.chars()
1137 .next()1110 .next()
1138 .and_then(|c| c.to_lowercase().next())1111 .and_then(|c| c.to_lowercase().next())
@@ -1150,41 +1123,34 @@ impl CompletionsMenu { · 11 unmodified lines
1150 01123 0
1151 });1124 });
1152 let sort_snippet = match snippet_sort_order {1125 let sort_snippet = match snippet_sort_order {
1153 SnippetSortOrder::Top => Reverse(if mat.is_snippet { 1 } else { 0 }),1126 SnippetSortOrder::Top => Reverse(if is_snippet { 1 } else { 0 }),
1154 SnippetSortOrder::Bottom => Reverse(if mat.is_snippet { 0 } else { 1 }),1127 SnippetSortOrder::Bottom => Reverse(if is_snippet { 0 } else { 1 }),
1155 SnippetSortOrder::Inline => Reverse(0),1128 SnippetSortOrder::Inline => Reverse(0),
1156 };1129 };
1157 let sort_mixed_case_prefix_length = Reverse(1130 let sort_capitalize = Reverse(
1158 query1131 query
1159 .as_ref()1132 .as_ref()
1160 .map(|q| {1133 .and_then(|q| q.chars().next())
1161 q.chars()1134 .zip(string_match.string.chars().next())
1162 .zip(mat.string_match.string.chars())1135 .map(|(q_char, s_char)| if q_char == s_char { 1 } else { 0 })
1163 .enumerate()
1164 .take_while(|(i, (q_char, match_char))| {
1165 if *i == 0 {
1166 // Case-sensitive comparison for first character
1167 q_char == match_char
1168 } else {
1169 // Case-insensitive comparison for other characters
1170 q_char.to_lowercase().eq(match_char.to_lowercase())
1171 }
1172 })
1173 .count()
1174 })
1175 .unwrap_or(0),1136 .unwrap_or(0),
1176 );1137 );
1138 let sort_positions = string_match.positions.clone();
1139
1177 MatchTier::WordStartMatch {1140 MatchTier::WordStartMatch {
1178 sort_mixed_case_prefix_length,1141 sort_capitalize,
1142 sort_positions,
1179 sort_snippet,1143 sort_snippet,
1180 sort_kind: mat.sort_kind,1144 sort_kind,
1181 sort_fuzzy_bracket,1145 sort_fuzzy_bracket,
1182 sort_text: mat.sort_text,1146 sort_text,
1183 sort_score,1147 sort_score,
1184 sort_label: mat.sort_label,1148 sort_label,
1185 }1149 }
1186 }1150 }
1187 });1151 });
1152
1153 matches
1188 }1154 }
1189 1155
1190 pub fn preserve_markdown_cache(&mut self, prev_menu: CompletionsMenu) {1156 pub fn preserve_markdown_cache(&mut self, prev_menu: CompletionsMenu) {
@@ -1216,15 +1182,6 @@ impl CompletionsMenu { · 25 unmodified lines
1216 }1182 }
1217}1183}
1218 1184
1219#[derive(Debug)]
1220pub struct SortableMatch<'a> {
1221 pub string_match: StringMatch,
1222 pub is_snippet: bool,
1223 pub sort_text: Option<&'a str>,
1224 pub sort_kind: usize,
1225 pub sort_label: &'a str,
1226}
1227
1228#[derive(Clone)]1185#[derive(Clone)]
1229pub struct AvailableCodeAction {1186pub struct AvailableCodeAction {
1230 pub excerpt_id: ExcerptId,1187 pub excerpt_id: ExcerptId,
@@ -1436,7 +1393,9 @@ impl CodeActionsMenu { · 205 unmodified lines
1436 1393
1437 fn origin(&self) -> ContextMenuOrigin {1394 fn origin(&self) -> ContextMenuOrigin {
1438 match &self.deployed_from {1395 match &self.deployed_from {
1439 Some(CodeActionSource::Indicator(row)) => ContextMenuOrigin::GutterIndicator(*row),1396 Some(CodeActionSource::Indicator(row)) | Some(CodeActionSource::RunMenu(row)) => {
1397 ContextMenuOrigin::GutterIndicator(*row)
1398 }
1440 Some(CodeActionSource::QuickActionBar) => ContextMenuOrigin::QuickActionBar,1399 Some(CodeActionSource::QuickActionBar) => ContextMenuOrigin::QuickActionBar,
1441 None => ContextMenuOrigin::Cursor,1400 None => ContextMenuOrigin::Cursor,
1442 }1401 }
@@ -1452,10 +1411,9 @@ impl CodeActionsMenu { · 9 unmodified lines
1452 let actions = self.actions.clone();1411 let actions = self.actions.clone();
1453 let selected_item = self.selected_item;1412 let selected_item = self.selected_item;
1454 let list = uniform_list(1413 let list = uniform_list(
1455 cx.entity().clone(),
1456 "code_actions_menu",1414 "code_actions_menu",
1457 self.actions.len(),1415 self.actions.len(),
1458 move |_this, range, _, cx| {1416 cx.processor(move |_this, range: Range<usize>, _, cx| {
1459 actions1417 actions
1460 .iter()1418 .iter()
1461 .skip(range.start)1419 .skip(range.start)
@@ -1518,7 +1476,7 @@ impl CodeActionsMenu { · 56 unmodified lines
1518 )1476 )
1519 })1477 })
1520 .collect()1478 .collect()
1521 },1479 }),
1522 )1480 )
1523 .occlude()1481 .occlude()
1524 .max_h(max_height_in_lines as f32 * window.line_height())1482 .max_h(max_height_in_lines as f32 * window.line_height())
crates/editor/src/display_map/block_map.rs+3−3
@@ -464,7 +464,7 @@ impl BlockMap { · 463 unmodified lines
464 map464 map
465 }465 }
466 466
467 pub fn read(&self, wrap_snapshot: WrapSnapshot, edits: Patch<u32>) -> BlockMapReader {467 pub fn read(&self, wrap_snapshot: WrapSnapshot, edits: Patch<u32>) -> BlockMapReader<'_> {
468 self.sync(&wrap_snapshot, edits);468 self.sync(&wrap_snapshot, edits);
469 *self.wrap_snapshot.borrow_mut() = wrap_snapshot.clone();469 *self.wrap_snapshot.borrow_mut() = wrap_snapshot.clone();
470 BlockMapReader {470 BlockMapReader {
@@ -479,7 +479,7 @@ impl BlockMap { · 8 unmodified lines
479 }479 }
480 }480 }
481 481
482 pub fn write(&mut self, wrap_snapshot: WrapSnapshot, edits: Patch<u32>) -> BlockMapWriter {482 pub fn write(&mut self, wrap_snapshot: WrapSnapshot, edits: Patch<u32>) -> BlockMapWriter<'_> {
483 self.sync(&wrap_snapshot, edits);483 self.sync(&wrap_snapshot, edits);
484 *self.wrap_snapshot.borrow_mut() = wrap_snapshot;484 *self.wrap_snapshot.borrow_mut() = wrap_snapshot;
485 BlockMapWriter(self)485 BlockMapWriter(self)
@@ -1327,7 +1327,7 @@ impl BlockSnapshot { · 841 unmodified lines
1327 }1327 }
1328 }1328 }
1329 1329
1330 pub(super) fn row_infos(&self, start_row: BlockRow) -> BlockRows {1330 pub(super) fn row_infos(&self, start_row: BlockRow) -> BlockRows<'_> {
1331 let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&());1331 let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&());
1332 cursor.seek(&start_row, Bias::Right, &());1332 cursor.seek(&start_row, Bias::Right, &());
1333 let (output_start, input_start) = cursor.start();1333 let (output_start, input_start) = cursor.start();
crates/editor/src/display_map/fold_map.rs+2−2
@@ -357,7 +357,7 @@ impl FoldMap { · 356 unmodified lines
357 &mut self,357 &mut self,
358 inlay_snapshot: InlaySnapshot,358 inlay_snapshot: InlaySnapshot,
359 edits: Vec<InlayEdit>,359 edits: Vec<InlayEdit>,
360 ) -> (FoldMapWriter, FoldSnapshot, Vec<FoldEdit>) {360 ) -> (FoldMapWriter<'_>, FoldSnapshot, Vec<FoldEdit>) {
361 let (snapshot, edits) = self.read(inlay_snapshot, edits);361 let (snapshot, edits) = self.read(inlay_snapshot, edits);
362 (FoldMapWriter(self), snapshot, edits)362 (FoldMapWriter(self), snapshot, edits)
363 }363 }
@@ -730,7 +730,7 @@ impl FoldSnapshot { · 366 unmodified lines
730 (line_end - line_start) as u32730 (line_end - line_start) as u32
731 }731 }
732 732
733 pub fn row_infos(&self, start_row: u32) -> FoldRows {733 pub fn row_infos(&self, start_row: u32) -> FoldRows<'_> {
734 if start_row > self.transforms.summary().output.lines.row {734 if start_row > self.transforms.summary().output.lines.row {
735 panic!("invalid display row {}", start_row);735 panic!("invalid display row {}", start_row);
736 }736 }
crates/editor/src/display_map/wrap_map.rs+1−1
@@ -726,7 +726,7 @@ impl WrapSnapshot { · 725 unmodified lines
726 self.transforms.summary().output.longest_row726 self.transforms.summary().output.longest_row
727 }727 }
728 728
729 pub fn row_infos(&self, start_row: u32) -> WrapRows {729 pub fn row_infos(&self, start_row: u32) -> WrapRows<'_> {
730 let mut transforms = self.transforms.cursor::<(WrapPoint, TabPoint)>(&());730 let mut transforms = self.transforms.cursor::<(WrapPoint, TabPoint)>(&());
731 transforms.seek(&WrapPoint::new(start_row, 0), Bias::Left, &());731 transforms.seek(&WrapPoint::new(start_row, 0), Bias::Left, &());
732 let mut input_row = transforms.start().1.row();732 let mut input_row = transforms.start().1.row();
crates/editor/src/editor.rs+676−436
@@ -209,7 +209,7 @@ use workspace::{ · 208 unmodified lines
209 CollaboratorId, Item as WorkspaceItem, ItemId, ItemNavHistory, OpenInTerminal, OpenTerminal,209 CollaboratorId, Item as WorkspaceItem, ItemId, ItemNavHistory, OpenInTerminal, OpenTerminal,
210 RestoreOnStartupBehavior, SERIALIZATION_THROTTLE_TIME, SplitDirection, TabBarSettings, Toast,210 RestoreOnStartupBehavior, SERIALIZATION_THROTTLE_TIME, SplitDirection, TabBarSettings, Toast,
211 ViewId, Workspace, WorkspaceId, WorkspaceSettings,211 ViewId, Workspace, WorkspaceId, WorkspaceSettings,
212 item::{ItemHandle, PreviewTabsSettings},212 item::{ItemHandle, PreviewTabsSettings, SaveOptions},
213 notifications::{DetachAndPromptErr, NotificationId, NotifyTaskExt},213 notifications::{DetachAndPromptErr, NotificationId, NotifyTaskExt},
214 searchable::SearchEvent,214 searchable::SearchEvent,
215};215};
@@ -241,7 +241,6 @@ pub(crate) const SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT: Duration = Duration: · 25 unmodified lines
241 241
242pub(crate) const EDIT_PREDICTION_KEY_CONTEXT: &str = "edit_prediction";242pub(crate) const EDIT_PREDICTION_KEY_CONTEXT: &str = "edit_prediction";
243pub(crate) const EDIT_PREDICTION_CONFLICT_KEY_CONTEXT: &str = "edit_prediction_conflict";243pub(crate) const EDIT_PREDICTION_CONFLICT_KEY_CONTEXT: &str = "edit_prediction_conflict";
244pub(crate) const MIN_LINE_NUMBER_DIGITS: u32 = 4;
245pub(crate) const MINIMAP_FONT_SIZE: AbsoluteLength = AbsoluteLength::Pixels(px(2.));244pub(crate) const MINIMAP_FONT_SIZE: AbsoluteLength = AbsoluteLength::Pixels(px(2.));
246 245
247pub type RenderDiffHunkControlsFn = Arc<246pub type RenderDiffHunkControlsFn = Arc<
@@ -450,6 +449,7 @@ pub enum SelectPhase { · 202 unmodified lines
450 BeginColumnar {449 BeginColumnar {
451 position: DisplayPoint,450 position: DisplayPoint,
452 reset: bool,451 reset: bool,
452 mode: ColumnarMode,
453 goal_column: u32,453 goal_column: u32,
454 },454 },
455 Extend {455 Extend {
@@ -464,6 +464,12 @@ pub enum SelectPhase { · 8 unmodified lines
464 End,464 End,
465}465}
466 466
467#[derive(Clone, Debug, PartialEq)]
468pub enum ColumnarMode {
469 FromMouse,
470 FromSelection,
471}
472
467#[derive(Clone, Debug)]473#[derive(Clone, Debug)]
468pub enum SelectMode {474pub enum SelectMode {
469 Character,475 Character,
@@ -478,6 +484,7 @@ pub enum EditorMode { · 8 unmodified lines
478 auto_width: bool,484 auto_width: bool,
479 },485 },
480 AutoHeight {486 AutoHeight {
487 min_lines: usize,
481 max_lines: usize,488 max_lines: usize,
482 },489 },
483 Full {490 Full {
@@ -700,7 +707,7 @@ impl EditorActionId { · 216 unmodified lines
700// type OverrideTextStyle = dyn Fn(&EditorStyle) -> Option<HighlightStyle>;707// type OverrideTextStyle = dyn Fn(&EditorStyle) -> Option<HighlightStyle>;
701 708
702type BackgroundHighlight = (fn(&ThemeColors) -> Hsla, Arc<[Range<Anchor>]>);709type BackgroundHighlight = (fn(&ThemeColors) -> Hsla, Arc<[Range<Anchor>]>);
703type GutterHighlight = (fn(&App) -> Hsla, Arc<[Range<Anchor>]>);710type GutterHighlight = (fn(&App) -> Hsla, Vec<Range<Anchor>>);
704 711
705#[derive(Default)]712#[derive(Default)]
706struct ScrollbarMarkerState {713struct ScrollbarMarkerState {
@@ -902,7 +909,6 @@ struct InlineBlamePopoverState { · 195 unmodified lines
902 909
903struct InlineBlamePopover {910struct InlineBlamePopover {
904 position: gpui::Point<Pixels>,911 position: gpui::Point<Pixels>,
905 show_task: Option<Task<()>>,
906 hide_task: Option<Task<()>>,912 hide_task: Option<Task<()>>,
907 popover_bounds: Option<Bounds<Pixels>>,913 popover_bounds: Option<Bounds<Pixels>>,
908 popover_state: InlineBlamePopoverState,914 popover_state: InlineBlamePopoverState,
@@ -915,17 +921,29 @@ enum SelectionDragState { · 6 unmodified lines
915 ReadyToDrag {921 ReadyToDrag {
916 selection: Selection<Anchor>,922 selection: Selection<Anchor>,
917 click_position: gpui::Point<Pixels>,923 click_position: gpui::Point<Pixels>,
924 mouse_down_time: Instant,
918 },925 },
919 /// State when the mouse is dragging the selection in the editor.926 /// State when the mouse is dragging the selection in the editor.
920 Dragging {927 Dragging {
921 selection: Selection<Anchor>,928 selection: Selection<Anchor>,
922 drop_cursor: Selection<Anchor>,929 drop_cursor: Selection<Anchor>,
930 hide_drop_cursor: bool,
931 },
932}
933
934enum ColumnarSelectionState {
935 FromMouse {
936 selection_tail: Anchor,
937 display_point: Option<DisplayPoint>,
938 },
939 FromSelection {
940 selection_tail: Anchor,
923 },941 },
924}942}
925 943
926/// Represents a breakpoint indicator that shows up when hovering over lines in the gutter that don't have944/// Represents a breakpoint indicator that shows up when hovering over lines in the gutter that don't have
927/// a breakpoint on them.945/// a breakpoint on them.
928#[derive(Clone, Copy, Debug)]946#[derive(Clone, Copy, Debug, PartialEq, Eq)]
929struct PhantomBreakpointIndicator {947struct PhantomBreakpointIndicator {
930 display_row: DisplayRow,948 display_row: DisplayRow,
931 /// There's a small debounce between hovering over the line and showing the indicator.949 /// There's a small debounce between hovering over the line and showing the indicator.
@@ -933,6 +951,7 @@ struct PhantomBreakpointIndicator { · 1 unmodified line
933 is_active: bool,951 is_active: bool,
934 collides_with_existing_breakpoint: bool,952 collides_with_existing_breakpoint: bool,
935}953}
954
936/// Zed's primary implementation of text input, allowing users to edit a [`MultiBuffer`].955/// Zed's primary implementation of text input, allowing users to edit a [`MultiBuffer`].
937///956///
938/// See the [module level documentation](self) for more information.957/// See the [module level documentation](self) for more information.
@@ -949,8 +968,7 @@ pub struct Editor { · 10 unmodified lines
949 /// When inline assist editors are linked, they all render cursors because968 /// When inline assist editors are linked, they all render cursors because
950 /// typing enters text into each of them, even the ones that aren't focused.969 /// typing enters text into each of them, even the ones that aren't focused.
951 pub(crate) show_cursor_when_unfocused: bool,970 pub(crate) show_cursor_when_unfocused: bool,
952 columnar_selection_tail: Option<Anchor>,971 columnar_selection_state: Option<ColumnarSelectionState>,
953 columnar_display_point: Option<DisplayPoint>,
954 add_selections_state: Option<AddSelectionsState>,972 add_selections_state: Option<AddSelectionsState>,
955 select_next_state: Option<SelectNextState>,973 select_next_state: Option<SelectNextState>,
956 select_prev_state: Option<SelectNextState>,974 select_prev_state: Option<SelectNextState>,
@@ -1007,6 +1025,7 @@ pub struct Editor { · 50 unmodified lines
1007 mouse_context_menu: Option<MouseContextMenu>,1025 mouse_context_menu: Option<MouseContextMenu>,
1008 completion_tasks: Vec<(CompletionId, Task<()>)>,1026 completion_tasks: Vec<(CompletionId, Task<()>)>,
1009 inline_blame_popover: Option<InlineBlamePopover>,1027 inline_blame_popover: Option<InlineBlamePopover>,
1028 inline_blame_popover_show_task: Option<Task<()>>,
1010 signature_help_state: SignatureHelpState,1029 signature_help_state: SignatureHelpState,
1011 auto_signature_help: Option<bool>,1030 auto_signature_help: Option<bool>,
1012 find_all_references_task_sources: Vec<Anchor>,1031 find_all_references_task_sources: Vec<Anchor>,
@@ -1054,8 +1073,9 @@ pub struct Editor { · 41 unmodified lines
1054 style: Option<EditorStyle>,1073 style: Option<EditorStyle>,
1055 text_style_refinement: Option<TextStyleRefinement>,1074 text_style_refinement: Option<TextStyleRefinement>,
1056 next_editor_action_id: EditorActionId,1075 next_editor_action_id: EditorActionId,
1057 editor_actions:1076 editor_actions: Rc<
1058 Rc<RefCell<BTreeMap<EditorActionId, Box<dyn Fn(&mut Window, &mut Context<Self>)>>>>,1077 RefCell<BTreeMap<EditorActionId, Box<dyn Fn(&Editor, &mut Window, &mut Context<Self>)>>>,
1078 >,
1059 use_autoclose: bool,1079 use_autoclose: bool,
1060 use_auto_surround: bool,1080 use_auto_surround: bool,
1061 auto_replace_emoji_shortcode: bool,1081 auto_replace_emoji_shortcode: bool,
@@ -1087,6 +1107,7 @@ pub struct Editor { · 25 unmodified lines
1087 tasks_update_task: Option<Task<()>>,1107 tasks_update_task: Option<Task<()>>,
1088 breakpoint_store: Option<Entity<BreakpointStore>>,1108 breakpoint_store: Option<Entity<BreakpointStore>>,
1089 gutter_breakpoint_indicator: (Option<PhantomBreakpointIndicator>, Option<Task<()>>),1109 gutter_breakpoint_indicator: (Option<PhantomBreakpointIndicator>, Option<Task<()>>),
1110 hovered_diff_hunk_row: Option<DisplayRow>,
1090 pull_diagnostics_task: Task<()>,1111 pull_diagnostics_task: Task<()>,
1091 in_project_search: bool,1112 in_project_search: bool,
1092 previous_search_ranges: Option<Arc<[Range<Anchor>]>>,1113 previous_search_ranges: Option<Arc<[Range<Anchor>]>>,
@@ -1201,10 +1222,12 @@ struct SelectionHistoryEntry { · 108 unmodified lines
1201 add_selections_state: Option<AddSelectionsState>,1222 add_selections_state: Option<AddSelectionsState>,
1202}1223}
1203 1224
1225#[derive(Copy, Clone, Debug, PartialEq, Eq)]
1204enum SelectionHistoryMode {1226enum SelectionHistoryMode {
1205 Normal,1227 Normal,
1206 Undoing,1228 Undoing,
1207 Redoing,1229 Redoing,
1230 Skipping,
1208}1231}
1209 1232
1210#[derive(Clone, PartialEq, Eq, Hash)]1233#[derive(Clone, PartialEq, Eq, Hash)]
@@ -1219,10 +1242,55 @@ impl Default for SelectionHistoryMode { · 8 unmodified lines
1219 }1242 }
1220}1243}
1221 1244
1245#[derive(Debug)]
1246pub struct SelectionEffects {
1247 nav_history: bool,
1248 completions: bool,
1249 scroll: Option<Autoscroll>,
1250}
1251
1252impl Default for SelectionEffects {
1253 fn default() -> Self {
1254 Self {
1255 nav_history: true,
1256 completions: true,
1257 scroll: Some(Autoscroll::fit()),
1258 }
1259 }
1260}
1261impl SelectionEffects {
1262 pub fn scroll(scroll: Autoscroll) -> Self {
1263 Self {
1264 scroll: Some(scroll),
1265 ..Default::default()
1266 }
1267 }
1268
1269 pub fn no_scroll() -> Self {
1270 Self {
1271 scroll: None,
1272 ..Default::default()
1273 }
1274 }
1275
1276 pub fn completions(self, completions: bool) -> Self {
1277 Self {
1278 completions,
1279 ..self
1280 }
1281 }
1282
1283 pub fn nav_history(self, nav_history: bool) -> Self {
1284 Self {
1285 nav_history,
1286 ..self
1287 }
1288 }
1289}
1290
1222struct DeferredSelectionEffectsState {1291struct DeferredSelectionEffectsState {
1223 changed: bool,1292 changed: bool,
1224 should_update_completions: bool,1293 effects: SelectionEffects,
1225 autoscroll: Option<Autoscroll>,
1226 old_cursor_position: Anchor,1294 old_cursor_position: Anchor,
1227 history_entry: SelectionHistoryEntry,1295 history_entry: SelectionHistoryEntry,
1228}1296}
@@ -1238,11 +1306,19 @@ struct SelectionHistory { · 9 unmodified lines
1238}1306}
1239 1307
1240impl SelectionHistory {1308impl SelectionHistory {
1309 #[track_caller]
1241 fn insert_transaction(1310 fn insert_transaction(
1242 &mut self,1311 &mut self,
1243 transaction_id: TransactionId,1312 transaction_id: TransactionId,
1244 selections: Arc<[Selection<Anchor>]>,1313 selections: Arc<[Selection<Anchor>]>,
1245 ) {1314 ) {
1315 if selections.is_empty() {
1316 log::error!(
1317 "SelectionHistory::insert_transaction called with empty selections. Caller: {}",
1318 std::panic::Location::caller()
1319 );
1320 return;
1321 }
1246 self.selections_by_transaction1322 self.selections_by_transaction
1247 .insert(transaction_id, (selections, None));1323 .insert(transaction_id, (selections, None));
1248 }1324 }
@@ -1272,6 +1348,7 @@ impl SelectionHistory { · 23 unmodified lines
1272 }1348 }
1273 SelectionHistoryMode::Undoing => self.push_redo(entry),1349 SelectionHistoryMode::Undoing => self.push_redo(entry),
1274 SelectionHistoryMode::Redoing => self.push_undo(entry),1350 SelectionHistoryMode::Redoing => self.push_undo(entry),
1351 SelectionHistoryMode::Skipping => {}
1275 }1352 }
1276 }1353 }
1277 }1354 }
@@ -1486,7 +1563,7 @@ impl InlayHintRefreshReason { · 208 unmodified lines
1486}1563}
1487 1564
1488pub enum FormatTarget {1565pub enum FormatTarget {
1489 Buffers,1566 Buffers(HashSet<Entity<Buffer>>),
1490 Ranges(Vec<Range<MultiBufferPoint>>),1567 Ranges(Vec<Range<MultiBufferPoint>>),
1491}1568}
1492 1569
@@ -1551,11 +1628,19 @@ impl Editor { · 58 unmodified lines
1551 )1628 )
1552 }1629 }
1553 1630
1554 pub fn auto_height(max_lines: usize, window: &mut Window, cx: &mut Context<Self>) -> Self {1631 pub fn auto_height(
1632 min_lines: usize,
1633 max_lines: usize,
1634 window: &mut Window,
1635 cx: &mut Context<Self>,
1636 ) -> Self {
1555 let buffer = cx.new(|cx| Buffer::local("", cx));1637 let buffer = cx.new(|cx| Buffer::local("", cx));
1556 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));1638 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
1557 Self::new(1639 Self::new(
1558 EditorMode::AutoHeight { max_lines },1640 EditorMode::AutoHeight {
1641 min_lines,
1642 max_lines,
1643 },
1559 buffer,1644 buffer,
1560 None,1645 None,
1561 window,1646 window,
@@ -1817,8 +1902,7 @@ impl Editor { · 255 unmodified lines
1817 display_map: display_map.clone(),1902 display_map: display_map.clone(),
1818 selections,1903 selections,
1819 scroll_manager: ScrollManager::new(cx),1904 scroll_manager: ScrollManager::new(cx),
1820 columnar_selection_tail: None,1905 columnar_selection_state: None,
1821 columnar_display_point: None,
1822 add_selections_state: None,1906 add_selections_state: None,
1823 select_next_state: None,1907 select_next_state: None,
1824 select_prev_state: None,1908 select_prev_state: None,
@@ -1872,6 +1956,7 @@ impl Editor { · 47 unmodified lines
1872 mouse_context_menu: None,1956 mouse_context_menu: None,
1873 completion_tasks: Vec::new(),1957 completion_tasks: Vec::new(),
1874 inline_blame_popover: None,1958 inline_blame_popover: None,
1959 inline_blame_popover_show_task: None,
1875 signature_help_state: SignatureHelpState::default(),1960 signature_help_state: SignatureHelpState::default(),
1876 auto_signature_help: None,1961 auto_signature_help: None,
1877 find_all_references_task_sources: Vec::new(),1962 find_all_references_task_sources: Vec::new(),
@@ -1949,6 +2034,7 @@ impl Editor { · 71 unmodified lines
1949 2034
1950 breakpoint_store,2035 breakpoint_store,
1951 gutter_breakpoint_indicator: (None, None),2036 gutter_breakpoint_indicator: (None, None),
2037 hovered_diff_hunk_row: None,
1952 _subscriptions: vec![2038 _subscriptions: vec![
1953 cx.observe(&buffer, Self::on_buffer_changed),2039 cx.observe(&buffer, Self::on_buffer_changed),
1954 cx.subscribe_in(&buffer, window, Self::on_buffer_event),2040 cx.subscribe_in(&buffer, window, Self::on_buffer_event),
@@ -2082,7 +2168,11 @@ impl Editor { · 127 unmodified lines
2082 }2168 }
2083 }2169 }
2084 2170
2171 // skip adding the initial selection to selection history
2172 editor.selection_history.mode = SelectionHistoryMode::Skipping;
2085 editor.end_selection(window, cx);2173 editor.end_selection(window, cx);
2174 editor.selection_history.mode = SelectionHistoryMode::Normal;
2175
2086 editor.scroll_manager.show_scrollbars(window, cx);2176 editor.scroll_manager.show_scrollbars(window, cx);
2087 jsx_tag_auto_close::refresh_enabled_in_any_buffer(&mut editor, &buffer, cx);2177 jsx_tag_auto_close::refresh_enabled_in_any_buffer(&mut editor, &buffer, cx);
2088 2178
@@ -2682,7 +2772,7 @@ impl Editor { · 593 unmodified lines
2682 &mut self,2772 &mut self,
2683 local: bool,2773 local: bool,
2684 old_cursor_position: &Anchor,2774 old_cursor_position: &Anchor,
2685 should_update_completions: bool,2775 effects: SelectionEffects,
2686 window: &mut Window,2776 window: &mut Window,
2687 cx: &mut Context<Self>,2777 cx: &mut Context<Self>,
2688 ) {2778 ) {
@@ -2740,12 +2830,14 @@ impl Editor { · 51 unmodified lines
2740 let new_cursor_position = newest_selection.head();2830 let new_cursor_position = newest_selection.head();
2741 let selection_start = newest_selection.start;2831 let selection_start = newest_selection.start;
2742 2832
2743 self.push_to_nav_history(2833 if effects.nav_history {
2744 *old_cursor_position,2834 self.push_to_nav_history(
2745 Some(new_cursor_position.to_point(buffer)),2835 *old_cursor_position,
2746 false,2836 Some(new_cursor_position.to_point(buffer)),
2747 cx,2837 false,
2748 );2838 cx,
2839 );
2840 }
2749 2841
2750 if local {2842 if local {
2751 if let Some(buffer_id) = new_cursor_position.buffer_id {2843 if let Some(buffer_id) = new_cursor_position.buffer_id {
@@ -2776,7 +2868,7 @@ impl Editor { · 24 unmodified lines
2776 let completion_position = completion_menu.map(|menu| menu.initial_position);2868 let completion_position = completion_menu.map(|menu| menu.initial_position);
2777 drop(context_menu);2869 drop(context_menu);
2778 2870
2779 if should_update_completions {2871 if effects.completions {
2780 if let Some(completion_position) = completion_position {2872 if let Some(completion_position) = completion_position {
2781 let start_offset = selection_start.to_offset(buffer);2873 let start_offset = selection_start.to_offset(buffer);
2782 let position_matches = start_offset == completion_position.to_offset(buffer);2874 let position_matches = start_offset == completion_position.to_offset(buffer);
@@ -2785,7 +2877,8 @@ impl Editor { · 2 unmodified lines
2785 buffer.char_kind_before(start_offset, true) == Some(CharKind::Word)2877 buffer.char_kind_before(start_offset, true) == Some(CharKind::Word)
2786 } else {2878 } else {
2787 // Snippet choices can be shown even when the cursor is in whitespace.2879 // Snippet choices can be shown even when the cursor is in whitespace.
2788 // Dismissing the menu when actions like backspace2880 // Dismissing the menu with actions like backspace is handled by
2881 // invalidation regions.
2789 true2882 true
2790 }2883 }
2791 } else {2884 } else {
@@ -2851,22 +2944,22 @@ impl Editor { · 59 unmodified lines
2851 let background_executor = cx.background_executor().clone();2944 let background_executor = cx.background_executor().clone();
2852 let editor_id = cx.entity().entity_id().as_u64() as ItemId;2945 let editor_id = cx.entity().entity_id().as_u64() as ItemId;
2853 self.serialize_selections = cx.background_spawn(async move {2946 self.serialize_selections = cx.background_spawn(async move {
2854 background_executor.timer(SERIALIZATION_THROTTLE_TIME).await;2947 background_executor.timer(SERIALIZATION_THROTTLE_TIME).await;
2855 let db_selections = selections2948 let db_selections = selections
2856 .iter()2949 .iter()
2857 .map(|selection| {2950 .map(|selection| {
2858 (2951 (
2859 selection.start.to_offset(&snapshot),2952 selection.start.to_offset(&snapshot),
2860 selection.end.to_offset(&snapshot),2953 selection.end.to_offset(&snapshot),
2861 )2954 )
2862 })2955 })
2863 .collect();2956 .collect();
2864 2957
2865 DB.save_editor_selections(editor_id, workspace_id, db_selections)2958 DB.save_editor_selections(editor_id, workspace_id, db_selections)
2866 .await2959 .await
2867 .with_context(|| format!("persisting editor selections for editor {editor_id}, workspace {workspace_id:?}"))2960 .with_context(|| format!("persisting editor selections for editor {editor_id}, workspace {workspace_id:?}"))
2868 .log_err();2961 .log_err();
2869 });2962 });
2870 }2963 }
2871 }2964 }
2872 }2965 }
@@ -2983,43 +3076,23 @@ impl Editor { · 110 unmodified lines
2983 /// effects of selection change occur at the end of the transaction.3076 /// effects of selection change occur at the end of the transaction.
2984 pub fn change_selections<R>(3077 pub fn change_selections<R>(
2985 &mut self,3078 &mut self,
2986 autoscroll: Option<Autoscroll>,3079 effects: impl Into<SelectionEffects>,
2987 window: &mut Window,
2988 cx: &mut Context<Self>,
2989 change: impl FnOnce(&mut MutableSelectionsCollection<'_>) -> R,
2990 ) -> R {
2991 self.change_selections_inner(true, autoscroll, window, cx, change)
2992 }
2993
2994 pub(crate) fn change_selections_without_updating_completions<R>(
2995 &mut self,
2996 autoscroll: Option<Autoscroll>,
2997 window: &mut Window,
2998 cx: &mut Context<Self>,
2999 change: impl FnOnce(&mut MutableSelectionsCollection<'_>) -> R,
3000 ) -> R {
3001 self.change_selections_inner(false, autoscroll, window, cx, change)
3002 }
3003
3004 fn change_selections_inner<R>(
3005 &mut self,
3006 should_update_completions: bool,
3007 autoscroll: Option<Autoscroll>,
3008 window: &mut Window,3080 window: &mut Window,
3009 cx: &mut Context<Self>,3081 cx: &mut Context<Self>,
3010 change: impl FnOnce(&mut MutableSelectionsCollection<'_>) -> R,3082 change: impl FnOnce(&mut MutableSelectionsCollection<'_>) -> R,
3011 ) -> R {3083 ) -> R {
3084 let effects = effects.into();
3012 if let Some(state) = &mut self.deferred_selection_effects_state {3085 if let Some(state) = &mut self.deferred_selection_effects_state {
3013 state.autoscroll = autoscroll.or(state.autoscroll);3086 state.effects.scroll = effects.scroll.or(state.effects.scroll);
3014 state.should_update_completions = should_update_completions;3087 state.effects.completions = effects.completions;
3088 state.effects.nav_history |= effects.nav_history;
3015 let (changed, result) = self.selections.change_with(cx, change);3089 let (changed, result) = self.selections.change_with(cx, change);
3016 state.changed |= changed;3090 state.changed |= changed;
3017 return result;3091 return result;
3018 }3092 }
3019 let mut state = DeferredSelectionEffectsState {3093 let mut state = DeferredSelectionEffectsState {
3020 changed: false,3094 changed: false,
3021 should_update_completions,3095 effects,
3022 autoscroll,
3023 old_cursor_position: self.selections.newest_anchor().head(),3096 old_cursor_position: self.selections.newest_anchor().head(),
3024 history_entry: SelectionHistoryEntry {3097 history_entry: SelectionHistoryEntry {
3025 selections: self.selections.disjoint_anchors(),3098 selections: self.selections.disjoint_anchors(),
@@ -3069,19 +3142,13 @@ impl Editor { · 43 unmodified lines
3069 if state.changed {3142 if state.changed {
3070 self.selection_history.push(state.history_entry);3143 self.selection_history.push(state.history_entry);
3071 3144
3072 if let Some(autoscroll) = state.autoscroll {3145 if let Some(autoscroll) = state.effects.scroll {
3073 self.request_autoscroll(autoscroll, cx);3146 self.request_autoscroll(autoscroll, cx);
3074 }3147 }
3075 3148
3076 let old_cursor_position = &state.old_cursor_position;3149 let old_cursor_position = &state.old_cursor_position;
3077 3150
3078 self.selections_did_change(3151 self.selections_did_change(true, &old_cursor_position, state.effects, window, cx);
3079 true,
3080 &old_cursor_position,
3081 state.should_update_completions,
3082 window,
3083 cx,
3084 );
3085 3152
3086 if self.should_open_signature_help_automatically(&old_cursor_position, cx) {3153 if self.should_open_signature_help_automatically(&old_cursor_position, cx) {
3087 self.show_signature_help(&ShowSignatureHelp, window, cx);3154 self.show_signature_help(&ShowSignatureHelp, window, cx);
@@ -3156,7 +3223,8 @@ impl Editor { · 68 unmodified lines
3156 position,3223 position,
3157 goal_column,3224 goal_column,
3158 reset,3225 reset,
3159 } => self.begin_columnar_selection(position, goal_column, reset, window, cx),3226 mode,
3227 } => self.begin_columnar_selection(position, goal_column, reset, mode, window, cx),
3160 SelectPhase::Extend {3228 SelectPhase::Extend {
3161 position,3229 position,
3162 click_count,3230 click_count,
@@ -3201,9 +3269,13 @@ impl Editor { · 38 unmodified lines
3201 _ => {}3269 _ => {}
3202 }3270 }
3203 3271
3204 let auto_scroll = EditorSettings::get_global(cx).autoscroll_on_clicks;3272 let effects = if EditorSettings::get_global(cx).autoscroll_on_clicks {
3273 SelectionEffects::scroll(Autoscroll::fit())
3274 } else {
3275 SelectionEffects::no_scroll()
3276 };
3205 3277
3206 self.change_selections(auto_scroll.then(Autoscroll::fit), window, cx, |s| {3278 self.change_selections(effects, window, cx, |s| {
3207 s.set_pending(pending_selection, pending_mode)3279 s.set_pending(pending_selection, pending_mode)
3208 });3280 });
3209 }3281 }
@@ -3309,6 +3381,7 @@ impl Editor { · 99 unmodified lines
3309 position: DisplayPoint,3381 position: DisplayPoint,
3310 goal_column: u32,3382 goal_column: u32,
3311 reset: bool,3383 reset: bool,
3384 mode: ColumnarMode,
3312 window: &mut Window,3385 window: &mut Window,
3313 cx: &mut Context<Self>,3386 cx: &mut Context<Self>,
3314 ) {3387 ) {
@@ -3331,26 +3404,30 @@ impl Editor { · 16 unmodified lines
3331 SelectMode::Character,3404 SelectMode::Character,
3332 );3405 );
3333 });3406 });
3334 if position.column() != goal_column {3407 };
3335 self.columnar_display_point = Some(DisplayPoint::new(position.row(), goal_column));
3336 } else {
3337 self.columnar_display_point = None;
3338 }
3339 }
3340 3408
3341 let tail = self.selections.newest::<Point>(cx).tail();3409 let tail = self.selections.newest::<Point>(cx).tail();
3342 self.columnar_selection_tail = Some(display_map.buffer_snapshot.anchor_before(tail));3410 let selection_anchor = display_map.buffer_snapshot.anchor_before(tail);
3411 self.columnar_selection_state = match mode {
3412 ColumnarMode::FromMouse => Some(ColumnarSelectionState::FromMouse {
3413 selection_tail: selection_anchor,
3414 display_point: if reset {
3415 if position.column() != goal_column {
3416 Some(DisplayPoint::new(position.row(), goal_column))
3417 } else {
3418 None
3419 }
3420 } else {
3421 None
3422 },
3423 }),
3424 ColumnarMode::FromSelection => Some(ColumnarSelectionState::FromSelection {
3425 selection_tail: selection_anchor,
3426 }),
3427 };
3343 3428
3344 if !reset {3429 if !reset {
3345 self.columnar_display_point = None;3430 self.select_columns(position, goal_column, &display_map, window, cx);
3346 self.select_columns(
3347 tail.to_display_point(&display_map),
3348 position,
3349 goal_column,
3350 &display_map,
3351 window,
3352 cx,
3353 );
3354 }3431 }
3355 }3432 }
3356 3433
@@ -3364,11 +3441,8 @@ impl Editor { · 7 unmodified lines
3364 ) {3441 ) {
3365 let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));3442 let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
3366 3443
3367 if let Some(tail) = self.columnar_selection_tail.as_ref() {3444 if self.columnar_selection_state.is_some() {
3368 let tail = self3445 self.select_columns(position, goal_column, &display_map, window, cx);
3369 .columnar_display_point
3370 .unwrap_or_else(|| tail.to_display_point(&display_map));
3371 self.select_columns(tail, position, goal_column, &display_map, window, cx);
3372 } else if let Some(mut pending) = self.selections.pending_anchor() {3446 } else if let Some(mut pending) = self.selections.pending_anchor() {
3373 let buffer = self.buffer.read(cx).snapshot(cx);3447 let buffer = self.buffer.read(cx).snapshot(cx);
3374 let head;3448 let head;
@@ -3455,7 +3529,7 @@ impl Editor { · 80 unmodified lines
3455 }3529 }
3456 3530
3457 fn end_selection(&mut self, window: &mut Window, cx: &mut Context<Self>) {3531 fn end_selection(&mut self, window: &mut Window, cx: &mut Context<Self>) {
3458 self.columnar_selection_tail.take();3532 self.columnar_selection_state.take();
3459 if self.selections.pending_anchor().is_some() {3533 if self.selections.pending_anchor().is_some() {
3460 let selections = self.selections.all::<usize>(cx);3534 let selections = self.selections.all::<usize>(cx);
3461 self.change_selections(None, window, cx, |s| {3535 self.change_selections(None, window, cx, |s| {
@@ -3467,13 +3541,26 @@ impl Editor { · 5 unmodified lines
3467 3541
3468 fn select_columns(3542 fn select_columns(
3469 &mut self,3543 &mut self,
3470 tail: DisplayPoint,
3471 head: DisplayPoint,3544 head: DisplayPoint,
3472 goal_column: u32,3545 goal_column: u32,
3473 display_map: &DisplaySnapshot,3546 display_map: &DisplaySnapshot,
3474 window: &mut Window,3547 window: &mut Window,
3475 cx: &mut Context<Self>,3548 cx: &mut Context<Self>,
3476 ) {3549 ) {
3550 let Some(columnar_state) = self.columnar_selection_state.as_ref() else {
3551 return;
3552 };
3553
3554 let tail = match columnar_state {
3555 ColumnarSelectionState::FromMouse {
3556 selection_tail,
3557 display_point,
3558 } => display_point.unwrap_or_else(|| selection_tail.to_display_point(&display_map)),
3559 ColumnarSelectionState::FromSelection { selection_tail } => {
3560 selection_tail.to_display_point(&display_map)
3561 }
3562 };
3563
3477 let start_row = cmp::min(tail.row(), head.row());3564 let start_row = cmp::min(tail.row(), head.row());
3478 let end_row = cmp::max(tail.row(), head.row());3565 let end_row = cmp::max(tail.row(), head.row());
3479 let start_column = cmp::min(tail.column(), goal_column);3566 let start_column = cmp::min(tail.column(), goal_column);
@@ -3483,7 +3570,10 @@ impl Editor { · 3 unmodified lines
3483 let selection_ranges = (start_row.0..=end_row.0)3570 let selection_ranges = (start_row.0..=end_row.0)
3484 .map(DisplayRow)3571 .map(DisplayRow)
3485 .filter_map(|row| {3572 .filter_map(|row| {
3486 if !display_map.is_block_line(row) {3573 if (matches!(columnar_state, ColumnarSelectionState::FromMouse { .. })
3574 || start_column <= display_map.line_len(row))
3575 && !display_map.is_block_line(row)
3576 {
3487 let start = display_map3577 let start = display_map
3488 .clip_point(DisplayPoint::new(row, start_column), Bias::Left)3578 .clip_point(DisplayPoint::new(row, start_column), Bias::Left)
3489 .to_point(display_map);3579 .to_point(display_map);
@@ -3501,15 +3591,19 @@ impl Editor { · 11 unmodified lines
3501 })3591 })
3502 .collect::<Vec<_>>();3592 .collect::<Vec<_>>();
3503 3593
3504 let mut non_empty_ranges = selection_ranges3594 let ranges = match columnar_state {
3505 .iter()3595 ColumnarSelectionState::FromMouse { .. } => {
3506 .filter(|selection_range| selection_range.start != selection_range.end)3596 let mut non_empty_ranges = selection_ranges
3507 .peekable();3597 .iter()
3508 3598 .filter(|selection_range| selection_range.start != selection_range.end)
3509 let ranges = if non_empty_ranges.peek().is_some() {3599 .peekable();
3510 non_empty_ranges.cloned().collect()3600 if non_empty_ranges.peek().is_some() {
3511 } else {3601 non_empty_ranges.cloned().collect()
3512 selection_ranges3602 } else {
3603 selection_ranges
3604 }
3605 }
3606 _ => selection_ranges,
3513 };3607 };
3514 3608
3515 self.change_selections(None, window, cx, |s| {3609 self.change_selections(None, window, cx, |s| {
@@ -3532,11 +3626,11 @@ impl Editor { · 16 unmodified lines
3532 };3626 };
3533 3627
3534 pending_nonempty_selection3628 pending_nonempty_selection
3535 || (self.columnar_selection_tail.is_some() && self.selections.disjoint.len() > 1)3629 || (self.columnar_selection_state.is_some() && self.selections.disjoint.len() > 1)
3536 }3630 }
3537 3631
3538 pub fn has_pending_selection(&self) -> bool {3632 pub fn has_pending_selection(&self) -> bool {
3539 self.selections.pending_anchor().is_some() || self.columnar_selection_tail.is_some()3633 self.selections.pending_anchor().is_some() || self.columnar_selection_state.is_some()
3540 }3634 }
3541 3635
3542 pub fn cancel(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context<Self>) {3636 pub fn cancel(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context<Self>) {
@@ -3990,8 +4084,8 @@ impl Editor { · 447 unmodified lines
3990 }4084 }
3991 4085
3992 let had_active_inline_completion = this.has_active_inline_completion();4086 let had_active_inline_completion = this.has_active_inline_completion();
3993 this.change_selections_without_updating_completions(4087 this.change_selections(
3994 Some(Autoscroll::fit()),4088 SelectionEffects::scroll(Autoscroll::fit()).completions(false),
3995 window,4089 window,
3996 cx,4090 cx,
3997 |s| s.select(new_selections),4091 |s| s.select(new_selections),
@@ -5114,10 +5208,13 @@ impl Editor { · 1116 unmodified lines
5114 .as_ref()5208 .as_ref()
5115 .map_or(true, |provider| provider.filter_completions());5209 .map_or(true, |provider| provider.filter_completions());
5116 5210
5117 // When `is_incomplete` is false, can filter completions instead of re-querying when the
5118 // current query is a suffix of the initial query.
5119 if let Some(CodeContextMenu::Completions(menu)) = self.context_menu.borrow_mut().as_mut() {5211 if let Some(CodeContextMenu::Completions(menu)) = self.context_menu.borrow_mut().as_mut() {
5120 if !menu.is_incomplete && filter_completions {5212 if filter_completions {
5213 menu.filter(query.clone(), provider.clone(), window, cx);
5214 }
5215 // When `is_incomplete` is false, no need to re-query completions when the current query
5216 // is a suffix of the initial query.
5217 if !menu.is_incomplete {
5121 // If the new query is a suffix of the old query (typing more characters) and5218 // If the new query is a suffix of the old query (typing more characters) and
5122 // the previous result was complete, the existing completions can be filtered.5219 // the previous result was complete, the existing completions can be filtered.
5123 //5220 //
@@ -5135,7 +5232,6 @@ impl Editor { · 11 unmodified lines
5135 menu.initial_position.to_offset(&snapshot) == position.to_offset(&snapshot)5232 menu.initial_position.to_offset(&snapshot) == position.to_offset(&snapshot)
5136 };5233 };
5137 if position_matches {5234 if position_matches {
5138 menu.filter(query.clone(), provider.clone(), window, cx);
5139 return;5235 return;
5140 }5236 }
5141 }5237 }
@@ -5684,70 +5780,60 @@ impl Editor { · 542 unmodified lines
5684 drop(context_menu);5780 drop(context_menu);
5685 let snapshot = self.snapshot(window, cx);5781 let snapshot = self.snapshot(window, cx);
5686 let deployed_from = action.deployed_from.clone();5782 let deployed_from = action.deployed_from.clone();
5687 let mut task = self.code_actions_task.take();
5688 let action = action.clone();5783 let action = action.clone();
5689 cx.spawn_in(window, async move |editor, cx| {5784 self.completion_tasks.clear();
5690 while let Some(prev_task) = task {5785 self.discard_inline_completion(false, cx);
5691 prev_task.await.log_err();5786
5692 task = editor.update(cx, |this, _| this.code_actions_task.take())?;5787 let multibuffer_point = match &action.deployed_from {
5788 Some(CodeActionSource::Indicator(row)) | Some(CodeActionSource::RunMenu(row)) => {
5789 DisplayPoint::new(*row, 0).to_point(&snapshot)
5693 }5790 }
5791 _ => self.selections.newest::<Point>(cx).head(),
5792 };
5793 let Some((buffer, buffer_row)) = snapshot
5794 .buffer_snapshot
5795 .buffer_line_for_row(MultiBufferRow(multibuffer_point.row))
5796 .and_then(|(buffer_snapshot, range)| {
5797 self.buffer()
5798 .read(cx)
5799 .buffer(buffer_snapshot.remote_id())
5800 .map(|buffer| (buffer, range.start.row))
5801 })
5802 else {
5803 return;
5804 };
5805 let buffer_id = buffer.read(cx).remote_id();
5806 let tasks = self
5807 .tasks
5808 .get(&(buffer_id, buffer_row))
5809 .map(|t| Arc::new(t.to_owned()));
5694 5810
5695 let spawned_test_task = editor.update_in(cx, |editor, window, cx| {5811 if !self.focus_handle.is_focused(window) {
5696 if editor.focus_handle.is_focused(window) {5812 return;
5697 let multibuffer_point = match &action.deployed_from {5813 }
5698 Some(CodeActionSource::Indicator(row)) => {5814 let project = self.project.clone();
5699 DisplayPoint::new(*row, 0).to_point(&snapshot)5815
5700 }5816 let code_actions_task = match deployed_from {
5701 _ => editor.selections.newest::<Point>(cx).head(),5817 Some(CodeActionSource::RunMenu(_)) => Task::ready(None),
5702 };5818 _ => self.code_actions(buffer_row, window, cx),
5703 let (buffer, buffer_row) = snapshot5819 };
5704 .buffer_snapshot5820
5705 .buffer_line_for_row(MultiBufferRow(multibuffer_point.row))5821 let runnable_task = match deployed_from {
5706 .and_then(|(buffer_snapshot, range)| {5822 Some(CodeActionSource::Indicator(_)) => Task::ready(Ok(Default::default())),
5707 editor5823 _ => {
5708 .buffer5824 let mut task_context_task = Task::ready(None);
5709 .read(cx)5825 if let Some(tasks) = &tasks {
5710 .buffer(buffer_snapshot.remote_id())5826 if let Some(project) = project {
5711 .map(|buffer| (buffer, range.start.row))5827 task_context_task =
5712 })?;5828 Self::build_tasks_context(&project, &buffer, buffer_row, &tasks, cx);
5713 let (_, code_actions) = editor
5714 .available_code_actions
5715 .clone()
5716 .and_then(|(location, code_actions)| {
5717 let snapshot = location.buffer.read(cx).snapshot();
5718 let point_range = location.range.to_point(&snapshot);
5719 let point_range = point_range.start.row..=point_range.end.row;
5720 if point_range.contains(&buffer_row) {
5721 Some((location, code_actions))
5722 } else {
5723 None
5724 }
5725 })
5726 .unzip();
5727 let buffer_id = buffer.read(cx).remote_id();
5728 let tasks = editor
5729 .tasks
5730 .get(&(buffer_id, buffer_row))
5731 .map(|t| Arc::new(t.to_owned()));
5732 if tasks.is_none() && code_actions.is_none() {
5733 return None;
5734 }5829 }
5830 }
5735 5831
5736 editor.completion_tasks.clear();5832 cx.spawn_in(window, {
5737 editor.discard_inline_completion(false, cx);5833 let buffer = buffer.clone();
5738 let task_context =5834 async move |editor, cx| {
5739 tasks5835 let task_context = task_context_task.await;
5740 .as_ref()
5741 .zip(editor.project.clone())
5742 .map(|(tasks, project)| {
5743 Self::build_tasks_context(&project, &buffer, buffer_row, tasks, cx)
5744 });
5745 5836
5746 Some(cx.spawn_in(window, async move |editor, cx| {
5747 let task_context = match task_context {
5748 Some(task_context) => task_context.await,
5749 None => None,
5750 };
5751 let resolved_tasks =5837 let resolved_tasks =
5752 tasks5838 tasks
5753 .zip(task_context.clone())5839 .zip(task_context.clone())
@@ -5758,104 +5844,144 @@ impl Editor { · 4 unmodified lines
5758 tasks.column,5844 tasks.column,
5759 )),5845 )),
5760 });5846 });
5761 let debug_scenarios = editor.update(cx, |editor, cx| {5847 let debug_scenarios = editor
5762 if cx.has_flag::<DebuggerFeatureFlag>() {5848 .update(cx, |editor, cx| {
5763 maybe!({5849 editor.debug_scenarios(&resolved_tasks, &buffer, cx)
5764 let project = editor.project.as_ref()?;5850 })?
5765 let dap_store = project.read(cx).dap_store();5851 .await;
5766 let mut scenarios = vec![];5852 anyhow::Ok((resolved_tasks, debug_scenarios, task_context))
5767 let resolved_tasks = resolved_tasks.as_ref()?;5853 }
5768 let buffer = buffer.read(cx);5854 })
5769 let language = buffer.language()?;
5770 let file = buffer.file();
5771 let debug_adapter =
5772 language_settings(language.name().into(), file, cx)
5773 .debuggers
5774 .first()
5775 .map(SharedString::from)
5776 .or_else(|| {
5777 language
5778 .config()
5779 .debuggers
5780 .first()
5781 .map(SharedString::from)
5782 })?;
5783
5784 dap_store.update(cx, |dap_store, cx| {
5785 for (_, task) in &resolved_tasks.templates {
5786 if let Some(scenario) = dap_store
5787 .debug_scenario_for_build_task(
5788 task.original_task().clone(),
5789 debug_adapter.clone().into(),
5790 task.display_label().to_owned().into(),
5791 cx,
5792 )
5793 {
5794 scenarios.push(scenario);
5795 }
5796 }
5797 });
5798 Some(scenarios)
5799 })
5800 .unwrap_or_default()
5801 } else {
5802 vec![]
5803 }
5804 })?;
5805 let spawn_straight_away = quick_launch
5806 && resolved_tasks
5807 .as_ref()
5808 .map_or(false, |tasks| tasks.templates.len() == 1)
5809 && code_actions
5810 .as_ref()
5811 .map_or(true, |actions| actions.is_empty())
5812 && debug_scenarios.is_empty();
5813 if let Ok(task) = editor.update_in(cx, |editor, window, cx| {
5814 crate::hover_popover::hide_hover(editor, cx);
5815 *editor.context_menu.borrow_mut() =
5816 Some(CodeContextMenu::CodeActions(CodeActionsMenu {
5817 buffer,
5818 actions: CodeActionContents::new(
5819 resolved_tasks,
5820 code_actions,
5821 debug_scenarios,
5822 task_context.unwrap_or_default(),
5823 ),
5824 selected_item: Default::default(),
5825 scroll_handle: UniformListScrollHandle::default(),
5826 deployed_from,
5827 }));
5828 if spawn_straight_away {
5829 if let Some(task) = editor.confirm_code_action(
5830 &ConfirmCodeAction { item_ix: Some(0) },
5831 window,
5832 cx,
5833 ) {
5834 cx.notify();
5835 return task;
5836 }
5837 }
5838 cx.notify();
5839 Task::ready(Ok(()))
5840 }) {
5841 task.await
5842 } else {
5843 Ok(())
5844 }
5845 }))
5846 } else {
5847 Some(Task::ready(Ok(())))
5848 }
5849 })?;
5850 if let Some(task) = spawned_test_task {
5851 task.await?;
5852 }5855 }
5856 };
5853 5857
5854 anyhow::Ok(())5858 cx.spawn_in(window, async move |editor, cx| {
5859 let (resolved_tasks, debug_scenarios, task_context) = runnable_task.await?;
5860 let code_actions = code_actions_task.await;
5861 let spawn_straight_away = quick_launch
5862 && resolved_tasks
5863 .as_ref()
5864 .map_or(false, |tasks| tasks.templates.len() == 1)
5865 && code_actions
5866 .as_ref()
5867 .map_or(true, |actions| actions.is_empty())
5868 && debug_scenarios.is_empty();
5869
5870 editor.update_in(cx, |editor, window, cx| {
5871 crate::hover_popover::hide_hover(editor, cx);
5872 *editor.context_menu.borrow_mut() =
5873 Some(CodeContextMenu::CodeActions(CodeActionsMenu {
5874 buffer,
5875 actions: CodeActionContents::new(
5876 resolved_tasks,
5877 code_actions,
5878 debug_scenarios,
5879 task_context.unwrap_or_default(),
5880 ),
5881 selected_item: Default::default(),
5882 scroll_handle: UniformListScrollHandle::default(),
5883 deployed_from,
5884 }));
5885 if spawn_straight_away {
5886 if let Some(task) = editor.confirm_code_action(
5887 &ConfirmCodeAction { item_ix: Some(0) },
5888 window,
5889 cx,
5890 ) {
5891 cx.notify();
5892 return task;
5893 }
5894 }
5895
5896 Task::ready(Ok(()))
5897 })
5855 })5898 })
5856 .detach_and_log_err(cx);5899 .detach_and_log_err(cx);
5857 }5900 }
5858 5901
5902 fn debug_scenarios(
5903 &mut self,
5904 resolved_tasks: &Option<ResolvedTasks>,
5905 buffer: &Entity<Buffer>,
5906 cx: &mut App,
5907 ) -> Task<Vec<task::DebugScenario>> {
5908 if cx.has_flag::<DebuggerFeatureFlag>() {
5909 maybe!({
5910 let project = self.project.as_ref()?;
5911 let dap_store = project.read(cx).dap_store();
5912 let mut scenarios = vec![];
5913 let resolved_tasks = resolved_tasks.as_ref()?;
5914 let buffer = buffer.read(cx);
5915 let language = buffer.language()?;
5916 let file = buffer.file();
5917 let debug_adapter = language_settings(language.name().into(), file, cx)
5918 .debuggers
5919 .first()
5920 .map(SharedString::from)
5921 .or_else(|| language.config().debuggers.first().map(SharedString::from))?;
5922
5923 dap_store.update(cx, |dap_store, cx| {
5924 for (_, task) in &resolved_tasks.templates {
5925 let maybe_scenario = dap_store.debug_scenario_for_build_task(
5926 task.original_task().clone(),
5927 debug_adapter.clone().into(),
5928 task.display_label().to_owned().into(),
5929 cx,
5930 );
5931 scenarios.push(maybe_scenario);
5932 }
5933 });
5934 Some(cx.background_spawn(async move {
5935 let scenarios = futures::future::join_all(scenarios)
5936 .await
5937 .into_iter()
5938 .flatten()
5939 .collect::<Vec<_>>();
5940 scenarios
5941 }))
5942 })
5943 .unwrap_or_else(|| Task::ready(vec![]))
5944 } else {
5945 Task::ready(vec![])
5946 }
5947 }
5948
5949 fn code_actions(
5950 &mut self,
5951 buffer_row: u32,
5952 window: &mut Window,
5953 cx: &mut Context<Self>,
5954 ) -> Task<Option<Rc<[AvailableCodeAction]>>> {
5955 let mut task = self.code_actions_task.take();
5956 cx.spawn_in(window, async move |editor, cx| {
5957 while let Some(prev_task) = task {
5958 prev_task.await.log_err();
5959 task = editor
5960 .update(cx, |this, _| this.code_actions_task.take())
5961 .ok()?;
5962 }
5963
5964 editor
5965 .update(cx, |editor, cx| {
5966 editor
5967 .available_code_actions
5968 .clone()
5969 .and_then(|(location, code_actions)| {
5970 let snapshot = location.buffer.read(cx).snapshot();
5971 let point_range = location.range.to_point(&snapshot);
5972 let point_range = point_range.start.row..=point_range.end.row;
5973 if point_range.contains(&buffer_row) {
5974 Some(code_actions)
5975 } else {
5976 None
5977 }
5978 })
5979 })
5980 .ok()
5981 .flatten()
5982 })
5983 }
5984
5859 pub fn confirm_code_action(5985 pub fn confirm_code_action(
5860 &mut self,5986 &mut self,
5861 action: &ConfirmCodeAction,5987 action: &ConfirmCodeAction,
@@ -6193,71 +6319,65 @@ impl Editor { · 331 unmodified lines
6193 ) {6319 ) {
6194 if let Some(state) = &mut self.inline_blame_popover {6320 if let Some(state) = &mut self.inline_blame_popover {
6195 state.hide_task.take();6321 state.hide_task.take();
6196 cx.notify();
6197 } else {6322 } else {
6198 let delay = EditorSettings::get_global(cx).hover_popover_delay;6323 let delay = EditorSettings::get_global(cx).hover_popover_delay;
6324 let blame_entry = blame_entry.clone();
6199 let show_task = cx.spawn(async move |editor, cx| {6325 let show_task = cx.spawn(async move |editor, cx| {
6200 cx.background_executor()6326 cx.background_executor()
6201 .timer(std::time::Duration::from_millis(delay))6327 .timer(std::time::Duration::from_millis(delay))
6202 .await;6328 .await;
6203 editor6329 editor
6204 .update(cx, |editor, cx| {6330 .update(cx, |editor, cx| {
6205 if let Some(state) = &mut editor.inline_blame_popover {6331 editor.inline_blame_popover_show_task.take();
6206 state.show_task = None;6332 let Some(blame) = editor.blame.as_ref() else {
6207 cx.notify();6333 return;
6208 }6334 };
6335 let blame = blame.read(cx);
6336 let details = blame.details_for_entry(&blame_entry);
6337 let markdown = cx.new(|cx| {
6338 Markdown::new(
6339 details
6340 .as_ref()
6341 .map(|message| message.message.clone())
6342 .unwrap_or_default(),
6343 None,
6344 None,
6345 cx,
6346 )
6347 });
6348 editor.inline_blame_popover = Some(InlineBlamePopover {
6349 position,
6350 hide_task: None,
6351 popover_bounds: None,
6352 popover_state: InlineBlamePopoverState {
6353 scroll_handle: ScrollHandle::new(),
6354 commit_message: details,
6355 markdown,
6356 },
6357 });
6358 cx.notify();
6209 })6359 })
6210 .ok();6360 .ok();
6211 });6361 });
6212 let Some(blame) = self.blame.as_ref() else {6362 self.inline_blame_popover_show_task = Some(show_task);
6213 return;
6214 };
6215 let blame = blame.read(cx);
6216 let details = blame.details_for_entry(&blame_entry);
6217 let markdown = cx.new(|cx| {
6218 Markdown::new(
6219 details
6220 .as_ref()
6221 .map(|message| message.message.clone())
6222 .unwrap_or_default(),
6223 None,
6224 None,
6225 cx,
6226 )
6227 });
6228 self.inline_blame_popover = Some(InlineBlamePopover {
6229 position,
6230 show_task: Some(show_task),
6231 hide_task: None,
6232 popover_bounds: None,
6233 popover_state: InlineBlamePopoverState {
6234 scroll_handle: ScrollHandle::new(),
6235 commit_message: details,
6236 markdown,
6237 },
6238 });
6239 }6363 }
6240 }6364 }
6241 6365
6242 fn hide_blame_popover(&mut self, cx: &mut Context<Self>) {6366 fn hide_blame_popover(&mut self, cx: &mut Context<Self>) {
6367 self.inline_blame_popover_show_task.take();
6243 if let Some(state) = &mut self.inline_blame_popover {6368 if let Some(state) = &mut self.inline_blame_popover {
6244 if state.show_task.is_some() {6369 let hide_task = cx.spawn(async move |editor, cx| {
6245 self.inline_blame_popover.take();6370 cx.background_executor()
6246 cx.notify();6371 .timer(std::time::Duration::from_millis(100))
6247 } else {6372 .await;
6248 let hide_task = cx.spawn(async move |editor, cx| {6373 editor
6249 cx.background_executor()6374 .update(cx, |editor, cx| {
6250 .timer(std::time::Duration::from_millis(100))6375 editor.inline_blame_popover.take();
6251 .await;6376 cx.notify();
6252 editor6377 })
6253 .update(cx, |editor, cx| {6378 .ok();
6254 editor.inline_blame_popover.take();6379 });
6255 cx.notify();6380 state.hide_task = Some(hide_task);
6256 })
6257 .ok();
6258 });
6259 state.hide_task = Some(hide_task);
6260 }
6261 }6381 }
6262 }6382 }
6263 6383
@@ -7098,13 +7218,9 @@ impl Editor { · 834 unmodified lines
7098 )7218 )
7099 }7219 }
7100 7220
7101 fn multi_cursor_modifier(7221 fn multi_cursor_modifier(invert: bool, modifiers: &Modifiers, cx: &mut Context<Self>) -> bool {
7102 cursor_event: bool,
7103 modifiers: &Modifiers,
7104 cx: &mut Context<Self>,
7105 ) -> bool {
7106 let multi_cursor_setting = EditorSettings::get_global(cx).multi_cursor_modifier;7222 let multi_cursor_setting = EditorSettings::get_global(cx).multi_cursor_modifier;
7107 if cursor_event {7223 if invert {
7108 match multi_cursor_setting {7224 match multi_cursor_setting {
7109 MultiCursorModifier::Alt => modifiers.alt,7225 MultiCursorModifier::Alt => modifiers.alt,
7110 MultiCursorModifier::CmdOrCtrl => modifiers.secondary(),7226 MultiCursorModifier::CmdOrCtrl => modifiers.secondary(),
@@ -7117,8 +7233,21 @@ impl Editor { · 6 unmodified lines
7117 }7233 }
7118 }7234 }
7119 7235
7120 fn columnar_selection_modifiers(multi_cursor_modifier: bool, modifiers: &Modifiers) -> bool {7236 fn columnar_selection_mode(
7121 modifiers.shift && multi_cursor_modifier && modifiers.number_of_modifiers() == 27237 modifiers: &Modifiers,
7238 cx: &mut Context<Self>,
7239 ) -> Option<ColumnarMode> {
7240 if modifiers.shift && modifiers.number_of_modifiers() == 2 {
7241 if Self::multi_cursor_modifier(false, modifiers, cx) {
7242 Some(ColumnarMode::FromMouse)
7243 } else if Self::multi_cursor_modifier(true, modifiers, cx) {
7244 Some(ColumnarMode::FromSelection)
7245 } else {
7246 None
7247 }
7248 } else {
7249 None
7250 }
7122 }7251 }
7123 7252
7124 fn update_selection_mode(7253 fn update_selection_mode(
@@ -7128,10 +7257,10 @@ impl Editor { · 3 unmodified lines
7128 window: &mut Window,7257 window: &mut Window,
7129 cx: &mut Context<Self>,7258 cx: &mut Context<Self>,
7130 ) {7259 ) {
7131 let multi_cursor_modifier = Self::multi_cursor_modifier(true, modifiers, cx);7260 let Some(mode) = Self::columnar_selection_mode(modifiers, cx) else {
7132 if !Self::columnar_selection_modifiers(multi_cursor_modifier, modifiers)7261 return;
7133 || self.selections.pending.is_none()7262 };
7134 {7263 if self.selections.pending.is_none() {
7135 return;7264 return;
7136 }7265 }
7137 7266
@@ -7143,6 +7272,7 @@ impl Editor { · 5 unmodified lines
7143 SelectPhase::BeginColumnar {7272 SelectPhase::BeginColumnar {
7144 position,7273 position,
7145 reset: false,7274 reset: false,
7275 mode,
7146 goal_column: point_for_position.exact_unclipped.column(),7276 goal_column: point_for_position.exact_unclipped.column(),
7147 },7277 },
7148 window,7278 window,
@@ -7519,8 +7649,7 @@ impl Editor { · 370 unmodified lines
7519 "Set Breakpoint"7649 "Set Breakpoint"
7520 };7650 };
7521 7651
7522 let run_to_cursor = command_palette_hooks::CommandPaletteFilter::try_global(cx)7652 let run_to_cursor = window.is_action_available(&RunToCursor, cx);
7523 .map_or(false, |filter| !filter.is_hidden(&DebuggerRunToCursor));
7524 7653
7525 let toggle_state_msg = breakpoint.as_ref().map_or(None, |bp| match bp.1.state {7654 let toggle_state_msg = breakpoint.as_ref().map_or(None, |bp| match bp.1.state {
7526 BreakpointState::Enabled => Some("Disable"),7655 BreakpointState::Enabled => Some("Disable"),
@@ -7544,7 +7673,7 @@ impl Editor { · 17 unmodified lines
7544 })7673 })
7545 .ok();7674 .ok();
7546 7675
7547 window.dispatch_action(Box::new(DebuggerRunToCursor), cx);7676 window.dispatch_action(Box::new(RunToCursor), cx);
7548 })7677 })
7549 .separator()7678 .separator()
7550 })7679 })
@@ -7899,7 +8028,7 @@ impl Editor { · 348 unmodified lines
7899 window.focus(&editor.focus_handle(cx));8028 window.focus(&editor.focus_handle(cx));
7900 editor.toggle_code_actions(8029 editor.toggle_code_actions(
7901 &ToggleCodeActions {8030 &ToggleCodeActions {
7902 deployed_from: Some(CodeActionSource::Indicator(row)),8031 deployed_from: Some(CodeActionSource::RunMenu(row)),
7903 quick_launch,8032 quick_launch,
7904 },8033 },
7905 window,8034 window,
@@ -14076,7 +14205,8 @@ impl Editor { · 6170 unmodified lines
14076 prefer_lsp && !lsp_tasks_by_rows.is_empty(),14205 prefer_lsp && !lsp_tasks_by_rows.is_empty(),
14077 new_rows,14206 new_rows,
14078 cx.clone(),14207 cx.clone(),
14079 );14208 )
14209 .await;
14080 editor14210 editor
14081 .update(cx, |editor, _| {14211 .update(cx, |editor, _| {
14082 editor.clear_tasks();14212 editor.clear_tasks();
@@ -14106,35 +14236,40 @@ impl Editor { · 23 unmodified lines
14106 snapshot: DisplaySnapshot,14236 snapshot: DisplaySnapshot,
14107 prefer_lsp: bool,14237 prefer_lsp: bool,
14108 runnable_ranges: Vec<RunnableRange>,14238 runnable_ranges: Vec<RunnableRange>,
14109 mut cx: AsyncWindowContext,14239 cx: AsyncWindowContext,
14110 ) -> Vec<((BufferId, BufferRow), RunnableTasks)> {14240 ) -> Task<Vec<((BufferId, BufferRow), RunnableTasks)>> {
14111 runnable_ranges14241 cx.spawn(async move |cx| {
14112 .into_iter()14242 let mut runnable_rows = Vec::with_capacity(runnable_ranges.len());
14113 .filter_map(|mut runnable| {14243 for mut runnable in runnable_ranges {
14114 let mut tasks = cx14244 let Some(tasks) = cx
14115 .update(|_, cx| Self::templates_with_tags(&project, &mut runnable.runnable, cx))14245 .update(|_, cx| Self::templates_with_tags(&project, &mut runnable.runnable, cx))
14116 .ok()?;14246 .ok()
14247 else {
14248 continue;
14249 };
14250 let mut tasks = tasks.await;
14251
14117 if prefer_lsp {14252 if prefer_lsp {
14118 tasks.retain(|(task_kind, _)| {14253 tasks.retain(|(task_kind, _)| {
14119 !matches!(task_kind, TaskSourceKind::Language { .. })14254 !matches!(task_kind, TaskSourceKind::Language { .. })
14120 });14255 });
14121 }14256 }
14122 if tasks.is_empty() {14257 if tasks.is_empty() {
14123 return None;14258 continue;
14124 }14259 }
14125 14260
14126 let point = runnable.run_range.start.to_point(&snapshot.buffer_snapshot);14261 let point = runnable.run_range.start.to_point(&snapshot.buffer_snapshot);
14127 14262 let Some(row) = snapshot
14128 let row = snapshot
14129 .buffer_snapshot14263 .buffer_snapshot
14130 .buffer_line_for_row(MultiBufferRow(point.row))?14264 .buffer_line_for_row(MultiBufferRow(point.row))
14131 .114265 .map(|(_, range)| range.start.row)
14132 .start14266 else {
14133 .row;14267 continue;
14268 };
14134 14269
14135 let context_range =14270 let context_range =
14136 BufferOffset(runnable.full_range.start)..BufferOffset(runnable.full_range.end);14271 BufferOffset(runnable.full_range.start)..BufferOffset(runnable.full_range.end);
14137 Some((14272 runnable_rows.push((
14138 (runnable.buffer_id, row),14273 (runnable.buffer_id, row),
14139 RunnableTasks {14274 RunnableTasks {
14140 templates: tasks,14275 templates: tasks,
@@ -14145,16 +14280,17 @@ impl Editor { · 4 unmodified lines
14145 column: point.column,14280 column: point.column,
14146 extra_variables: runnable.extra_captures,14281 extra_variables: runnable.extra_captures,
14147 },14282 },
14148 ))14283 ));
14149 })14284 }
14150 .collect()14285 runnable_rows
14286 })
14151 }14287 }
14152 14288
14153 fn templates_with_tags(14289 fn templates_with_tags(
14154 project: &Entity<Project>,14290 project: &Entity<Project>,
14155 runnable: &mut Runnable,14291 runnable: &mut Runnable,
14156 cx: &mut App,14292 cx: &mut App,
14157 ) -> Vec<(TaskSourceKind, TaskTemplate)> {14293 ) -> Task<Vec<(TaskSourceKind, TaskTemplate)>> {
14158 let (inventory, worktree_id, file) = project.read_with(cx, |project, cx| {14294 let (inventory, worktree_id, file) = project.read_with(cx, |project, cx| {
14159 let (worktree_id, file) = project14295 let (worktree_id, file) = project
14160 .buffer_for_id(runnable.buffer, cx)14296 .buffer_for_id(runnable.buffer, cx)
@@ -14169,39 +14305,40 @@ impl Editor { · 8 unmodified lines
14169 )14305 )
14170 });14306 });
14171 14307
14172 let mut templates_with_tags = mem::take(&mut runnable.tags)14308 let tags = mem::take(&mut runnable.tags);
14173 .into_iter()14309 let language = runnable.language.clone();
14174 .flat_map(|RunnableTag(tag)| {14310 cx.spawn(async move |cx| {
14175 inventory14311 let mut templates_with_tags = Vec::new();
14176 .as_ref()14312 if let Some(inventory) = inventory {
14177 .into_iter()14313 for RunnableTag(tag) in tags {
14178 .flat_map(|inventory| {14314 let Ok(new_tasks) = inventory.update(cx, |inventory, cx| {
14179 inventory.read(cx).list_tasks(14315 inventory.list_tasks(file.clone(), Some(language.clone()), worktree_id, cx)
14180 file.clone(),14316 }) else {
14181 Some(runnable.language.clone()),14317 return templates_with_tags;
14182 worktree_id,14318 };
14183 cx,14319 templates_with_tags.extend(new_tasks.await.into_iter().filter(
14184 )14320 move |(_, template)| {
14185 })14321 template.tags.iter().any(|source_tag| source_tag == &tag)
14186 .filter(move |(_, template)| {14322 },
14187 template.tags.iter().any(|source_tag| source_tag == &tag)14323 ));
14188 })14324 }
14189 })
14190 .sorted_by_key(|(kind, _)| kind.to_owned())
14191 .collect::<Vec<_>>();
14192 if let Some((leading_tag_source, _)) = templates_with_tags.first() {
14193 // Strongest source wins; if we have worktree tag binding, prefer that to
14194 // global and language bindings;
14195 // if we have a global binding, prefer that to language binding.
14196 let first_mismatch = templates_with_tags
14197 .iter()
14198 .position(|(tag_source, _)| tag_source != leading_tag_source);
14199 if let Some(index) = first_mismatch {
14200 templates_with_tags.truncate(index);
14201 }14325 }
14202 }14326 templates_with_tags.sort_by_key(|(kind, _)| kind.to_owned());
14203 14327
14204 templates_with_tags14328 if let Some((leading_tag_source, _)) = templates_with_tags.first() {
14329 // Strongest source wins; if we have worktree tag binding, prefer that to
14330 // global and language bindings;
14331 // if we have a global binding, prefer that to language binding.
14332 let first_mismatch = templates_with_tags
14333 .iter()
14334 .position(|(tag_source, _)| tag_source != leading_tag_source);
14335 if let Some(index) = first_mismatch {
14336 templates_with_tags.truncate(index);
14337 }
14338 }
14339
14340 templates_with_tags
14341 })
14205 }14342 }
14206 14343
14207 pub fn move_to_enclosing_bracket(14344 pub fn move_to_enclosing_bracket(
@@ -14268,18 +14405,20 @@ impl Editor { · 60 unmodified lines
14268 cx: &mut Context<Self>,14405 cx: &mut Context<Self>,
14269 ) {14406 ) {
14270 self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction);14407 self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction);
14271 self.end_selection(window, cx);
14272 self.selection_history.mode = SelectionHistoryMode::Undoing;
14273 if let Some(entry) = self.selection_history.undo_stack.pop_back() {14408 if let Some(entry) = self.selection_history.undo_stack.pop_back() {
14274 self.change_selections(None, window, cx, |s| {14409 self.selection_history.mode = SelectionHistoryMode::Undoing;
14275 s.select_anchors(entry.selections.to_vec())14410 self.with_selection_effects_deferred(window, cx, |this, window, cx| {
14411 this.end_selection(window, cx);
14412 this.change_selections(Some(Autoscroll::newest()), window, cx, |s| {
14413 s.select_anchors(entry.selections.to_vec())
14414 });
14276 });14415 });
14416 self.selection_history.mode = SelectionHistoryMode::Normal;
14417
14277 self.select_next_state = entry.select_next_state;14418 self.select_next_state = entry.select_next_state;
14278 self.select_prev_state = entry.select_prev_state;14419 self.select_prev_state = entry.select_prev_state;
14279 self.add_selections_state = entry.add_selections_state;14420 self.add_selections_state = entry.add_selections_state;
14280 self.request_autoscroll(Autoscroll::newest(), cx);
14281 }14421 }
14282 self.selection_history.mode = SelectionHistoryMode::Normal;
14283 }14422 }
14284 14423
14285 pub fn redo_selection(14424 pub fn redo_selection(
@@ -14289,18 +14428,20 @@ impl Editor { · 3 unmodified lines
14289 cx: &mut Context<Self>,14428 cx: &mut Context<Self>,
14290 ) {14429 ) {
14291 self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction);14430 self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction);
14292 self.end_selection(window, cx);
14293 self.selection_history.mode = SelectionHistoryMode::Redoing;
14294 if let Some(entry) = self.selection_history.redo_stack.pop_back() {14431 if let Some(entry) = self.selection_history.redo_stack.pop_back() {
14295 self.change_selections(None, window, cx, |s| {14432 self.selection_history.mode = SelectionHistoryMode::Redoing;
14296 s.select_anchors(entry.selections.to_vec())14433 self.with_selection_effects_deferred(window, cx, |this, window, cx| {
14434 this.end_selection(window, cx);
14435 this.change_selections(Some(Autoscroll::newest()), window, cx, |s| {
14436 s.select_anchors(entry.selections.to_vec())
14437 });
14297 });14438 });
14439 self.selection_history.mode = SelectionHistoryMode::Normal;
14440
14298 self.select_next_state = entry.select_next_state;14441 self.select_next_state = entry.select_next_state;
14299 self.select_prev_state = entry.select_prev_state;14442 self.select_prev_state = entry.select_prev_state;
14300 self.add_selections_state = entry.add_selections_state;14443 self.add_selections_state = entry.add_selections_state;
14301 self.request_autoscroll(Autoscroll::newest(), cx);
14302 }14444 }
14303 self.selection_history.mode = SelectionHistoryMode::Normal;
14304 }14445 }
14305 14446
14306 pub fn expand_excerpts(14447 pub fn expand_excerpts(
@@ -15605,7 +15746,7 @@ impl Editor { · 1298 unmodified lines
15605 Some(self.perform_format(15746 Some(self.perform_format(
15606 project,15747 project,
15607 FormatTrigger::Manual,15748 FormatTrigger::Manual,
15608 FormatTarget::Buffers,15749 FormatTarget::Buffers(self.buffer.read(cx).all_buffers()),
15609 window,15750 window,
15610 cx,15751 cx,
15611 ))15752 ))
@@ -15650,13 +15791,7 @@ impl Editor { · 38 unmodified lines
15650 ) -> Task<Result<()>> {15791 ) -> Task<Result<()>> {
15651 let buffer = self.buffer.clone();15792 let buffer = self.buffer.clone();
15652 let (buffers, target) = match target {15793 let (buffers, target) = match target {
15653 FormatTarget::Buffers => {15794 FormatTarget::Buffers(buffers) => (buffers, LspFormatTarget::Buffers),
15654 let mut buffers = buffer.read(cx).all_buffers();
15655 if trigger == FormatTrigger::Save {
15656 buffers.retain(|buffer| buffer.read(cx).is_dirty());
15657 }
15658 (buffers, LspFormatTarget::Buffers)
15659 }
15660 FormatTarget::Ranges(selection_ranges) => {15795 FormatTarget::Ranges(selection_ranges) => {
15661 let multi_buffer = buffer.read(cx);15796 let multi_buffer = buffer.read(cx);
15662 let snapshot = multi_buffer.read(cx);15797 let snapshot = multi_buffer.read(cx);
@@ -16113,13 +16248,16 @@ impl Editor { · 450 unmodified lines
16113 window: &Window,16248 window: &Window,
16114 cx: &mut Context<Self>,16249 cx: &mut Context<Self>,
16115 ) -> Option<()> {16250 ) -> Option<()> {
16116 let project = self.project.as_ref()?.downgrade();16251 if !self.mode().is_full() {
16252 return None;
16253 }
16117 let pull_diagnostics_settings = ProjectSettings::get_global(cx)16254 let pull_diagnostics_settings = ProjectSettings::get_global(cx)
16118 .diagnostics16255 .diagnostics
16119 .lsp_pull_diagnostics;16256 .lsp_pull_diagnostics;
16120 if !pull_diagnostics_settings.enabled {16257 if !pull_diagnostics_settings.enabled {
16121 return None;16258 return None;
16122 }16259 }
16260 let project = self.project.as_ref()?.downgrade();
16123 let debounce = Duration::from_millis(pull_diagnostics_settings.debounce_ms);16261 let debounce = Duration::from_millis(pull_diagnostics_settings.debounce_ms);
16124 let mut buffers = self.buffer.read(cx).all_buffers();16262 let mut buffers = self.buffer.read(cx).all_buffers();
16125 if let Some(buffer_id) = buffer_id {16263 if let Some(buffer_id) = buffer_id {
@@ -16176,7 +16314,13 @@ impl Editor { · 50 unmodified lines
16176 s.clear_pending();16314 s.clear_pending();
16177 }16315 }
16178 });16316 });
16179 self.selections_did_change(false, &old_cursor_position, true, window, cx);16317 self.selections_did_change(
16318 false,
16319 &old_cursor_position,
16320 SelectionEffects::default(),
16321 window,
16322 cx,
16323 );
16180 }16324 }
16181 16325
16182 pub fn transact(16326 pub fn transact(
@@ -17098,7 +17242,16 @@ impl Editor { · 915 unmodified lines
17098 }17242 }
17099 17243
17100 if let Some(project) = self.project.clone() {17244 if let Some(project) = self.project.clone() {
17101 self.save(true, project, window, cx).detach_and_log_err(cx);17245 self.save(
17246 SaveOptions {
17247 format: true,
17248 autosave: false,
17249 },
17250 project,
17251 window,
17252 cx,
17253 )
17254 .detach_and_log_err(cx);
17102 }17255 }
17103 }17256 }
17104 17257
@@ -17130,7 +17283,16 @@ impl Editor { · 25 unmodified lines
17130 });17283 });
17131 17284
17132 if let Some(project) = self.project.clone() {17285 if let Some(project) = self.project.clone() {
17133 self.save(true, project, window, cx).detach_and_log_err(cx);17286 self.save(
17287 SaveOptions {
17288 format: true,
17289 autosave: false,
17290 },
17291 project,
17292 window,
17293 cx,
17294 )
17295 .detach_and_log_err(cx);
17134 }17296 }
17135 }17297 }
17136 17298
@@ -18119,16 +18281,9 @@ impl Editor { · 982 unmodified lines
18119 .selections18281 .selections
18120 .disjoint_anchors()18282 .disjoint_anchors()
18121 .iter()18283 .iter()
18122 .map(|selection| {18284 .map(|range| Location {
18123 let range = if selection.reversed {18285 buffer: buffer.clone(),
18124 selection.end.text_anchor..selection.start.text_anchor18286 range: range.start.text_anchor..range.end.text_anchor,
18125 } else {
18126 selection.start.text_anchor..selection.end.text_anchor
18127 };
18128 Location {
18129 buffer: buffer.clone(),
18130 range,
18131 }
18132 })18287 })
18133 .collect::<Vec<_>>();18288 .collect::<Vec<_>>();
18134 18289
@@ -18383,12 +18538,12 @@ impl Editor { · 248 unmodified lines
18383 18538
18384 pub fn highlight_gutter<T: 'static>(18539 pub fn highlight_gutter<T: 'static>(
18385 &mut self,18540 &mut self,
18386 ranges: &[Range<Anchor>],18541 ranges: impl Into<Vec<Range<Anchor>>>,
18387 color_fetcher: fn(&App) -> Hsla,18542 color_fetcher: fn(&App) -> Hsla,
18388 cx: &mut Context<Self>,18543 cx: &mut Context<Self>,
18389 ) {18544 ) {
18390 self.gutter_highlights18545 self.gutter_highlights
18391 .insert(TypeId::of::<T>(), (color_fetcher, Arc::from(ranges)));18546 .insert(TypeId::of::<T>(), (color_fetcher, ranges.into()));
18392 cx.notify();18547 cx.notify();
18393 }18548 }
18394 18549
@@ -18400,6 +18555,65 @@ impl Editor { · 5 unmodified lines
18400 self.gutter_highlights.remove(&TypeId::of::<T>())18555 self.gutter_highlights.remove(&TypeId::of::<T>())
18401 }18556 }
18402 18557
18558 pub fn insert_gutter_highlight<T: 'static>(
18559 &mut self,
18560 range: Range<Anchor>,
18561 color_fetcher: fn(&App) -> Hsla,
18562 cx: &mut Context<Self>,
18563 ) {
18564 let snapshot = self.buffer().read(cx).snapshot(cx);
18565 let mut highlights = self
18566 .gutter_highlights
18567 .remove(&TypeId::of::<T>())
18568 .map(|(_, highlights)| highlights)
18569 .unwrap_or_default();
18570 let ix = highlights.binary_search_by(|highlight| {
18571 Ordering::Equal
18572 .then_with(|| highlight.start.cmp(&range.start, &snapshot))
18573 .then_with(|| highlight.end.cmp(&range.end, &snapshot))
18574 });
18575 if let Err(ix) = ix {
18576 highlights.insert(ix, range);
18577 }
18578 self.gutter_highlights
18579 .insert(TypeId::of::<T>(), (color_fetcher, highlights));
18580 }
18581
18582 pub fn remove_gutter_highlights<T: 'static>(
18583 &mut self,
18584 ranges_to_remove: Vec<Range<Anchor>>,
18585 cx: &mut Context<Self>,
18586 ) {
18587 let snapshot = self.buffer().read(cx).snapshot(cx);
18588 let Some((color_fetcher, mut gutter_highlights)) =
18589 self.gutter_highlights.remove(&TypeId::of::<T>())
18590 else {
18591 return;
18592 };
18593 let mut ranges_to_remove = ranges_to_remove.iter().peekable();
18594 gutter_highlights.retain(|highlight| {
18595 while let Some(range_to_remove) = ranges_to_remove.peek() {
18596 match range_to_remove.end.cmp(&highlight.start, &snapshot) {
18597 Ordering::Less | Ordering::Equal => {
18598 ranges_to_remove.next();
18599 }
18600 Ordering::Greater => {
18601 match range_to_remove.start.cmp(&highlight.end, &snapshot) {
18602 Ordering::Less | Ordering::Equal => {
18603 return false;
18604 }
18605 Ordering::Greater => break,
18606 }
18607 }
18608 }
18609 }
18610
18611 true
18612 });
18613 self.gutter_highlights
18614 .insert(TypeId::of::<T>(), (color_fetcher, gutter_highlights));
18615 }
18616
18403 #[cfg(feature = "test-support")]18617 #[cfg(feature = "test-support")]
18404 pub fn all_text_background_highlights(18618 pub fn all_text_background_highlights(
18405 &self,18619 &self,
@@ -19662,7 +19876,7 @@ impl Editor { · 1256 unmodified lines
19662 .flatten()19876 .flatten()
19663 .filter_map(|keystroke| {19877 .filter_map(|keystroke| {
19664 if keystroke.modifiers.is_subset_of(&Modifiers::shift()) {19878 if keystroke.modifiers.is_subset_of(&Modifiers::shift()) {
19665 Some(keystroke.key_char.clone().unwrap_or(keystroke.key.clone()))19879 keystroke.key_char.clone()
19666 } else {19880 } else {
19667 None19881 None
19668 }19882 }
@@ -19739,6 +19953,21 @@ impl Editor { · 70 unmodified lines
19739 }19953 }
19740 }19954 }
19741 19955
19956 pub fn register_action_renderer(
19957 &mut self,
19958 listener: impl Fn(&Editor, &mut Window, &mut Context<Editor>) + 'static,
19959 ) -> Subscription {
19960 let id = self.next_editor_action_id.post_inc();
19961 self.editor_actions
19962 .borrow_mut()
19963 .insert(id, Box::new(listener));
19964
19965 let editor_actions = self.editor_actions.clone();
19966 Subscription::new(move || {
19967 editor_actions.borrow_mut().remove(&id);
19968 })
19969 }
19970
19742 pub fn register_action<A: Action>(19971 pub fn register_action<A: Action>(
19743 &mut self,19972 &mut self,
19744 listener: impl Fn(&A, &mut Window, &mut App) + 'static,19973 listener: impl Fn(&A, &mut Window, &mut App) + 'static,
@@ -19747,7 +19976,7 @@ impl Editor { · 2 unmodified lines
19747 let listener = Arc::new(listener);19976 let listener = Arc::new(listener);
19748 self.editor_actions.borrow_mut().insert(19977 self.editor_actions.borrow_mut().insert(
19749 id,19978 id,
19750 Box::new(move |window, _| {19979 Box::new(move |_, window, _| {
19751 let listener = listener.clone();19980 let listener = listener.clone();
19752 window.on_action(TypeId::of::<A>(), move |action, phase, window, cx| {19981 window.on_action(TypeId::of::<A>(), move |action, phase, window, cx| {
19753 let action = action.downcast_ref().unwrap();19982 let action = action.downcast_ref().unwrap();
@@ -19945,12 +20174,15 @@ impl Editor { · 191 unmodified lines
19945 if !selections.is_empty() {20174 if !selections.is_empty() {
19946 let snapshot =20175 let snapshot =
19947 buffer_snapshot.get_or_init(|| self.buffer.read(cx).snapshot(cx));20176 buffer_snapshot.get_or_init(|| self.buffer.read(cx).snapshot(cx));
20177 // skip adding the initial selection to selection history
20178 self.selection_history.mode = SelectionHistoryMode::Skipping;
19948 self.change_selections(None, window, cx, |s| {20179 self.change_selections(None, window, cx, |s| {
19949 s.select_ranges(selections.into_iter().map(|(start, end)| {20180 s.select_ranges(selections.into_iter().map(|(start, end)| {
19950 snapshot.clip_offset(start, Bias::Left)20181 snapshot.clip_offset(start, Bias::Left)
19951 ..snapshot.clip_offset(end, Bias::Right)20182 ..snapshot.clip_offset(end, Bias::Right)
19952 }));20183 }));
19953 });20184 });
20185 self.selection_history.mode = SelectionHistoryMode::Normal;
19954 }20186 }
19955 };20187 };
19956 }20188 }
@@ -19976,8 +20208,13 @@ fn process_completion_for_edit( · 19 unmodified lines
19976 let buffer = buffer.read(cx);20208 let buffer = buffer.read(cx);
19977 let buffer_snapshot = buffer.snapshot();20209 let buffer_snapshot = buffer.snapshot();
19978 let (snippet, new_text) = if completion.is_snippet() {20210 let (snippet, new_text) = if completion.is_snippet() {
20211 // Workaround for typescript language server issues so that methods don't expand within
20212 // strings and functions with type expressions. The previous point is used because the query
20213 // for function identifier doesn't match when the cursor is immediately after. See PR #30312
19979 let mut snippet_source = completion.new_text.clone();20214 let mut snippet_source = completion.new_text.clone();
19980 if let Some(scope) = buffer_snapshot.language_scope_at(cursor_position) {20215 let mut previous_point = text::ToPoint::to_point(cursor_position, buffer);
20216 previous_point.column = previous_point.column.saturating_sub(1);
20217 if let Some(scope) = buffer_snapshot.language_scope_at(previous_point) {
19981 if scope.prefers_label_for_snippet_in_completion() {20218 if scope.prefers_label_for_snippet_in_completion() {
19982 if let Some(label) = completion.label() {20219 if let Some(label) = completion.label() {
19983 if matches!(20220 if matches!(
@@ -21440,8 +21677,8 @@ impl EditorSnapshot { · 1456 unmodified lines
21440 return None;21677 return None;
21441 }21678 }
21442 21679
21443 let em_width = cx.text_system().em_width(font_id, font_size).log_err()?;21680 let ch_width = cx.text_system().ch_width(font_id, font_size).log_err()?;
21444 let em_advance = cx.text_system().em_advance(font_id, font_size).log_err()?;21681 let ch_advance = cx.text_system().ch_advance(font_id, font_size).log_err()?;
21445 21682
21446 let show_git_gutter = self.show_git_diff_gutter.unwrap_or_else(|| {21683 let show_git_gutter = self.show_git_diff_gutter.unwrap_or_else(|| {
21447 matches!(21684 matches!(
@@ -21454,8 +21691,10 @@ impl EditorSnapshot { · 6 unmodified lines
21454 .show_line_numbers21691 .show_line_numbers
21455 .unwrap_or(gutter_settings.line_numbers);21692 .unwrap_or(gutter_settings.line_numbers);
21456 let line_gutter_width = if show_line_numbers {21693 let line_gutter_width = if show_line_numbers {
21457 // Avoid flicker-like gutter resizes when the line number gains another digit and only resize the gutter on files with N*10^5 lines.21694 // Avoid flicker-like gutter resizes when the line number gains another digit by
21458 let min_width_for_number_on_gutter = em_advance * MIN_LINE_NUMBER_DIGITS as f32;21695 // only resizing the gutter on files with > 10**min_line_number_digits lines.
21696 let min_width_for_number_on_gutter =
21697 ch_advance * gutter_settings.min_line_number_digits as f32;
21459 max_line_number_width.max(min_width_for_number_on_gutter)21698 max_line_number_width.max(min_width_for_number_on_gutter)
21460 } else {21699 } else {
21461 0.0.into()21700 0.0.into()
@@ -21478,20 +21717,20 @@ impl EditorSnapshot { · 16 unmodified lines
21478 + MAX_RELATIVE_TIMESTAMP.len()21717 + MAX_RELATIVE_TIMESTAMP.len()
21479 + SPACING_WIDTH;21718 + SPACING_WIDTH;
21480 21719
21481 em_advance * max_char_count21720 ch_advance * max_char_count
21482 });21721 });
21483 21722
21484 let is_singleton = self.buffer_snapshot.is_singleton();21723 let is_singleton = self.buffer_snapshot.is_singleton();
21485 21724
21486 let mut left_padding = git_blame_entries_width.unwrap_or(Pixels::ZERO);21725 let mut left_padding = git_blame_entries_width.unwrap_or(Pixels::ZERO);
21487 left_padding += if !is_singleton {21726 left_padding += if !is_singleton {
21488 em_width * 4.021727 ch_width * 4.0
21489 } else if show_runnables || show_breakpoints {21728 } else if show_runnables || show_breakpoints {
21490 em_width * 3.021729 ch_width * 3.0
21491 } else if show_git_gutter && show_line_numbers {21730 } else if show_git_gutter && show_line_numbers {
21492 em_width * 2.021731 ch_width * 2.0
21493 } else if show_git_gutter || show_line_numbers {21732 } else if show_git_gutter || show_line_numbers {
21494 em_width21733 ch_width
21495 } else {21734 } else {
21496 px(0.)21735 px(0.)
21497 };21736 };
@@ -21499,11 +21738,11 @@ impl EditorSnapshot { · 1 unmodified line
21499 let shows_folds = is_singleton && gutter_settings.folds;21738 let shows_folds = is_singleton && gutter_settings.folds;
21500 21739
21501 let right_padding = if shows_folds && show_line_numbers {21740 let right_padding = if shows_folds && show_line_numbers {
21502 em_width * 4.021741 ch_width * 4.0
21503 } else if shows_folds || (!is_singleton && show_line_numbers) {21742 } else if shows_folds || (!is_singleton && show_line_numbers) {
21504 em_width * 3.021743 ch_width * 3.0
21505 } else if show_line_numbers {21744 } else if show_line_numbers {
21506 em_width21745 ch_width
21507 } else {21746 } else {
21508 px(0.)21747 px(0.)
21509 };21748 };
@@ -21709,7 +21948,7 @@ impl Render for Editor { · 199 unmodified lines
21709 21948
21710 let background = match self.mode {21949 let background = match self.mode {
21711 EditorMode::SingleLine { .. } => cx.theme().system().transparent,21950 EditorMode::SingleLine { .. } => cx.theme().system().transparent,
21712 EditorMode::AutoHeight { max_lines: _ } => cx.theme().system().transparent,21951 EditorMode::AutoHeight { .. } => cx.theme().system().transparent,
21713 EditorMode::Full { .. } => cx.theme().colors().editor_background,21952 EditorMode::Full { .. } => cx.theme().colors().editor_background,
21714 EditorMode::Minimap { .. } => cx.theme().colors().editor_background.opacity(0.7),21953 EditorMode::Minimap { .. } => cx.theme().colors().editor_background.opacity(0.7),
21715 };21954 };
@@ -22367,6 +22606,7 @@ impl BreakpointPromptEditor { · 651 unmodified lines
22367 let prompt = cx.new(|cx| {22606 let prompt = cx.new(|cx| {
22368 let mut prompt = Editor::new(22607 let mut prompt = Editor::new(
22369 EditorMode::AutoHeight {22608 EditorMode::AutoHeight {
22609 min_lines: 1,
22370 max_lines: Self::MAX_LINES as usize,22610 max_lines: Self::MAX_LINES as usize,
22371 },22611 },
22372 buffer,22612 buffer,
crates/editor/src/editor_settings.rs+5−0
@@ -150,6 +150,7 @@ impl Minimap { · 149 unmodified lines
150 150
151#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]151#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
152pub struct Gutter {152pub struct Gutter {
153 pub min_line_number_digits: usize,
153 pub line_numbers: bool,154 pub line_numbers: bool,
154 pub runnables: bool,155 pub runnables: bool,
155 pub breakpoints: bool,156 pub breakpoints: bool,
@@ -609,6 +610,10 @@ pub struct GutterContent { · 453 unmodified lines
609 ///610 ///
610 /// Default: true611 /// Default: true
611 pub line_numbers: Option<bool>,612 pub line_numbers: Option<bool>,
613 /// Minimum number of characters to reserve space for in the gutter.
614 ///
615 /// Default: 4
616 pub min_line_number_digits: Option<usize>,
612 /// Whether to show runnable buttons in the gutter.617 /// Whether to show runnable buttons in the gutter.
613 ///618 ///
614 /// Default: true619 /// Default: true
crates/editor/src/editor_tests.rs+254−40
@@ -56,7 +56,7 @@ use util::{ · 55 unmodified lines
56};56};
57use workspace::{57use workspace::{
58 CloseActiveItem, CloseAllItems, CloseInactiveItems, NavigationEntry, OpenOptions, ViewId,58 CloseActiveItem, CloseAllItems, CloseInactiveItems, NavigationEntry, OpenOptions, ViewId,
59 item::{FollowEvent, FollowableItem, Item, ItemHandle},59 item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
60};60};
61 61
62#[gpui::test]62#[gpui::test]
@@ -1907,7 +1907,6 @@ fn test_prev_next_word_boundary(cx: &mut TestAppContext) { · 1844 unmodified lines
1907 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),1907 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
1908 ])1908 ])
1909 });1909 });
1910
1911 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);1910 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1912 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);1911 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1913 1912
@@ -1927,29 +1926,29 @@ fn test_prev_next_word_boundary(cx: &mut TestAppContext) { · 13 unmodified lines
1927 assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);1926 assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1928 1927
1929 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);1928 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1930 assert_selection_ranges("use stdˇ::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);1929 assert_selection_ranges("use stdˇ::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
1931 1930
1932 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);1931 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1933 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);1932 assert_selection_ranges("use std::ˇstr::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1934 1933
1935 editor.move_right(&MoveRight, window, cx);1934 editor.move_right(&MoveRight, window, cx);
1936 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);1935 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1937 assert_selection_ranges(1936 assert_selection_ranges(
1938 "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",1937 "use std::«ˇs»tr::{foo, bar}\n«ˇ\n» {baz.qux()}",
1939 editor,1938 editor,
1940 cx,1939 cx,
1941 );1940 );
1942 1941
1943 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);1942 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1944 assert_selection_ranges(1943 assert_selection_ranges(
1945 "use std«ˇ::s»tr::{foo, bar}\n\n«ˇ {b»az.qux()}",1944 "use std«ˇ::s»tr::{foo, bar«ˇ}\n\n» {baz.qux()}",
1946 editor,1945 editor,
1947 cx,1946 cx,
1948 );1947 );
1949 1948
1950 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);1949 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
1951 assert_selection_ranges(1950 assert_selection_ranges(
1952 "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",1951 "use std::«ˇs»tr::{foo, bar}«ˇ\n\n» {baz.qux()}",
1953 editor,1952 editor,
1954 cx,1953 cx,
1955 );1954 );
@@ -9042,7 +9041,15 @@ async fn test_document_format_during_save(cx: &mut TestAppContext) { · 7086 unmodified lines
9042 );9041 );
9043 let save = editor9042 let save = editor
9044 .update_in(cx, |editor, window, cx| {9043 .update_in(cx, |editor, window, cx| {
9045 editor.save(true, project.clone(), window, cx)9044 editor.save(
9045 SaveOptions {
9046 format: true,
9047 autosave: false,
9048 },
9049 project.clone(),
9050 window,
9051 cx,
9052 )
9046 })9053 })
9047 .unwrap();9054 .unwrap();
9048 cx.executor().start_waiting();9055 cx.executor().start_waiting();
@@ -9074,7 +9081,15 @@ async fn test_document_format_during_save(cx: &mut TestAppContext) { · 25 unmodified lines
9074 );9081 );
9075 let save = editor9082 let save = editor
9076 .update_in(cx, |editor, window, cx| {9083 .update_in(cx, |editor, window, cx| {
9077 editor.save(true, project.clone(), window, cx)9084 editor.save(
9085 SaveOptions {
9086 format: true,
9087 autosave: false,
9088 },
9089 project.clone(),
9090 window,
9091 cx,
9092 )
9078 })9093 })
9079 .unwrap();9094 .unwrap();
9080 cx.executor().advance_clock(super::FORMAT_TIMEOUT);9095 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
@@ -9086,22 +9101,6 @@ async fn test_document_format_during_save(cx: &mut TestAppContext) { · 5 unmodified lines
9086 );9101 );
9087 }9102 }
9088 9103
9089 // For non-dirty buffer, no formatting request should be sent
9090 {
9091 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9092
9093 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
9094 panic!("Should not be invoked on non-dirty buffer");
9095 });
9096 let save = editor
9097 .update_in(cx, |editor, window, cx| {
9098 editor.save(true, project.clone(), window, cx)
9099 })
9100 .unwrap();
9101 cx.executor().start_waiting();
9102 save.await;
9103 }
9104
9105 // Set rust language override and assert overridden tabsize is sent to language server9104 // Set rust language override and assert overridden tabsize is sent to language server
9106 update_test_language_settings(cx, |settings| {9105 update_test_language_settings(cx, |settings| {
9107 settings.languages.insert(9106 settings.languages.insert(
@@ -9129,7 +9128,15 @@ async fn test_document_format_during_save(cx: &mut TestAppContext) { · 21 unmodified lines
9129 });9128 });
9130 let save = editor9129 let save = editor
9131 .update_in(cx, |editor, window, cx| {9130 .update_in(cx, |editor, window, cx| {
9132 editor.save(true, project.clone(), window, cx)9131 editor.save(
9132 SaveOptions {
9133 format: true,
9134 autosave: false,
9135 },
9136 project.clone(),
9137 window,
9138 cx,
9139 )
9133 })9140 })
9134 .unwrap();9141 .unwrap();
9135 cx.executor().start_waiting();9142 cx.executor().start_waiting();
@@ -9297,7 +9304,15 @@ async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) { · 161 unmodified lines
9297 cx.executor().start_waiting();9304 cx.executor().start_waiting();
9298 let save = multi_buffer_editor9305 let save = multi_buffer_editor
9299 .update_in(cx, |editor, window, cx| {9306 .update_in(cx, |editor, window, cx| {
9300 editor.save(true, project.clone(), window, cx)9307 editor.save(
9308 SaveOptions {
9309 format: true,
9310 autosave: false,
9311 },
9312 project.clone(),
9313 window,
9314 cx,
9315 )
9301 })9316 })
9302 .unwrap();9317 .unwrap();
9303 9318
@@ -9341,6 +9356,170 @@ async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) { · 37 unmodified lines
9341 });9356 });
9342}9357}
9343 9358
9359#[gpui::test]
9360async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
9361 init_test(cx, |_| {});
9362
9363 let fs = FakeFs::new(cx.executor());
9364 fs.insert_tree(
9365 path!("/dir"),
9366 json!({
9367 "file1.rs": "fn main() { println!(\"hello\"); }",
9368 "file2.rs": "fn test() { println!(\"test\"); }",
9369 "file3.rs": "fn other() { println!(\"other\"); }\n",
9370 }),
9371 )
9372 .await;
9373
9374 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
9375 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
9376 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
9377
9378 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9379 language_registry.add(rust_lang());
9380
9381 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
9382 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
9383
9384 // Open three buffers
9385 let buffer_1 = project
9386 .update(cx, |project, cx| {
9387 project.open_buffer((worktree_id, "file1.rs"), cx)
9388 })
9389 .await
9390 .unwrap();
9391 let buffer_2 = project
9392 .update(cx, |project, cx| {
9393 project.open_buffer((worktree_id, "file2.rs"), cx)
9394 })
9395 .await
9396 .unwrap();
9397 let buffer_3 = project
9398 .update(cx, |project, cx| {
9399 project.open_buffer((worktree_id, "file3.rs"), cx)
9400 })
9401 .await
9402 .unwrap();
9403
9404 // Create a multi-buffer with all three buffers
9405 let multi_buffer = cx.new(|cx| {
9406 let mut multi_buffer = MultiBuffer::new(ReadWrite);
9407 multi_buffer.push_excerpts(
9408 buffer_1.clone(),
9409 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9410 cx,
9411 );
9412 multi_buffer.push_excerpts(
9413 buffer_2.clone(),
9414 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9415 cx,
9416 );
9417 multi_buffer.push_excerpts(
9418 buffer_3.clone(),
9419 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
9420 cx,
9421 );
9422 multi_buffer
9423 });
9424
9425 let editor = cx.new_window_entity(|window, cx| {
9426 Editor::new(
9427 EditorMode::full(),
9428 multi_buffer,
9429 Some(project.clone()),
9430 window,
9431 cx,
9432 )
9433 });
9434
9435 // Edit only the first buffer
9436 editor.update_in(cx, |editor, window, cx| {
9437 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
9438 s.select_ranges(Some(10..10))
9439 });
9440 editor.insert("// edited", window, cx);
9441 });
9442
9443 // Verify that only buffer 1 is dirty
9444 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
9445 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9446 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9447
9448 // Get write counts after file creation (files were created with initial content)
9449 // We expect each file to have been written once during creation
9450 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
9451 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
9452 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
9453
9454 // Perform autosave
9455 let save_task = editor.update_in(cx, |editor, window, cx| {
9456 editor.save(
9457 SaveOptions {
9458 format: true,
9459 autosave: true,
9460 },
9461 project.clone(),
9462 window,
9463 cx,
9464 )
9465 });
9466 save_task.await.unwrap();
9467
9468 // Only the dirty buffer should have been saved
9469 assert_eq!(
9470 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
9471 1,
9472 "Buffer 1 was dirty, so it should have been written once during autosave"
9473 );
9474 assert_eq!(
9475 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
9476 0,
9477 "Buffer 2 was clean, so it should not have been written during autosave"
9478 );
9479 assert_eq!(
9480 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
9481 0,
9482 "Buffer 3 was clean, so it should not have been written during autosave"
9483 );
9484
9485 // Verify buffer states after autosave
9486 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9487 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9488 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
9489
9490 // Now perform a manual save (format = true)
9491 let save_task = editor.update_in(cx, |editor, window, cx| {
9492 editor.save(
9493 SaveOptions {
9494 format: true,
9495 autosave: false,
9496 },
9497 project.clone(),
9498 window,
9499 cx,
9500 )
9501 });
9502 save_task.await.unwrap();
9503
9504 // During manual save, clean buffers don't get written to disk
9505 // They just get did_save called for language server notifications
9506 assert_eq!(
9507 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
9508 1,
9509 "Buffer 1 should only have been written once total (during autosave, not manual save)"
9510 );
9511 assert_eq!(
9512 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
9513 0,
9514 "Buffer 2 should not have been written at all"
9515 );
9516 assert_eq!(
9517 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
9518 0,
9519 "Buffer 3 should not have been written at all"
9520 );
9521}
9522
9344#[gpui::test]9523#[gpui::test]
9345async fn test_range_format_during_save(cx: &mut TestAppContext) {9524async fn test_range_format_during_save(cx: &mut TestAppContext) {
9346 init_test(cx, |_| {});9525 init_test(cx, |_| {});
@@ -9384,7 +9563,15 @@ async fn test_range_format_during_save(cx: &mut TestAppContext) { · 37 unmodified lines
9384 9563
9385 let save = editor9564 let save = editor
9386 .update_in(cx, |editor, window, cx| {9565 .update_in(cx, |editor, window, cx| {
9387 editor.save(true, project.clone(), window, cx)9566 editor.save(
9567 SaveOptions {
9568 format: true,
9569 autosave: false,
9570 },
9571 project.clone(),
9572 window,
9573 cx,
9574 )
9388 })9575 })
9389 .unwrap();9576 .unwrap();
9390 fake_server9577 fake_server
@@ -9427,7 +9614,15 @@ async fn test_range_format_during_save(cx: &mut TestAppContext) { · 36 unmodified lines
9427 );9614 );
9428 let save = editor9615 let save = editor
9429 .update_in(cx, |editor, window, cx| {9616 .update_in(cx, |editor, window, cx| {
9430 editor.save(true, project.clone(), window, cx)9617 editor.save(
9618 SaveOptions {
9619 format: true,
9620 autosave: false,
9621 },
9622 project.clone(),
9623 window,
9624 cx,
9625 )
9431 })9626 })
9432 .unwrap();9627 .unwrap();
9433 cx.executor().advance_clock(super::FORMAT_TIMEOUT);9628 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
@@ -9442,12 +9637,20 @@ async fn test_range_format_during_save(cx: &mut TestAppContext) { · 8 unmodified lines
9442 // For non-dirty buffer, no formatting request should be sent9637 // For non-dirty buffer, no formatting request should be sent
9443 let save = editor9638 let save = editor
9444 .update_in(cx, |editor, window, cx| {9639 .update_in(cx, |editor, window, cx| {
9445 editor.save(true, project.clone(), window, cx)9640 editor.save(
9641 SaveOptions {
9642 format: false,
9643 autosave: false,
9644 },
9645 project.clone(),
9646 window,
9647 cx,
9648 )
9446 })9649 })
9447 .unwrap();9650 .unwrap();
9448 let _pending_format_request = fake_server9651 let _pending_format_request = fake_server
9449 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {9652 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
9450 panic!("Should not be invoked on non-dirty buffer");9653 panic!("Should not be invoked");
9451 })9654 })
9452 .next();9655 .next();
9453 cx.executor().start_waiting();9656 cx.executor().start_waiting();
@@ -9470,7 +9673,15 @@ async fn test_range_format_during_save(cx: &mut TestAppContext) { · 16 unmodified lines
9470 assert!(cx.read(|cx| editor.is_dirty(cx)));9673 assert!(cx.read(|cx| editor.is_dirty(cx)));
9471 let save = editor9674 let save = editor
9472 .update_in(cx, |editor, window, cx| {9675 .update_in(cx, |editor, window, cx| {
9473 editor.save(true, project.clone(), window, cx)9676 editor.save(
9677 SaveOptions {
9678 format: true,
9679 autosave: false,
9680 },
9681 project.clone(),
9682 window,
9683 cx,
9684 )
9474 })9685 })
9475 .unwrap();9686 .unwrap();
9476 fake_server9687 fake_server
@@ -9554,7 +9765,7 @@ async fn test_document_format_manual_trigger(cx: &mut TestAppContext) { · 77 unmodified lines
9554 editor.perform_format(9765 editor.perform_format(
9555 project.clone(),9766 project.clone(),
9556 FormatTrigger::Manual,9767 FormatTrigger::Manual,
9557 FormatTarget::Buffers,9768 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
9558 window,9769 window,
9559 cx,9770 cx,
9560 )9771 )
@@ -9600,7 +9811,7 @@ async fn test_document_format_manual_trigger(cx: &mut TestAppContext) { · 39 unmodified lines
9600 editor.perform_format(9811 editor.perform_format(
9601 project,9812 project,
9602 FormatTrigger::Manual,9813 FormatTrigger::Manual,
9603 FormatTarget::Buffers,9814 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
9604 window,9815 window,
9605 cx,9816 cx,
9606 )9817 )
@@ -9778,7 +9989,7 @@ async fn test_multiple_formatters(cx: &mut TestAppContext) { · 171 unmodified lines
9778 editor.perform_format(9989 editor.perform_format(
9779 project.clone(),9990 project.clone(),
9780 FormatTrigger::Manual,9991 FormatTrigger::Manual,
9781 FormatTarget::Buffers,9992 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
9782 window,9993 window,
9783 cx,9994 cx,
9784 )9995 )
@@ -9814,7 +10025,7 @@ async fn test_multiple_formatters(cx: &mut TestAppContext) { · 29 unmodified lines
9814 editor.perform_format(10025 editor.perform_format(
9815 project.clone(),10026 project.clone(),
9816 FormatTrigger::Manual,10027 FormatTrigger::Manual,
9817 FormatTarget::Buffers,10028 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
9818 window,10029 window,
9819 cx,10030 cx,
9820 )10031 )
@@ -15356,7 +15567,7 @@ async fn test_document_format_with_prettier(cx: &mut TestAppContext) { · 5535 unmodified lines
15356 editor.perform_format(15567 editor.perform_format(
15357 project.clone(),15568 project.clone(),
15358 FormatTrigger::Manual,15569 FormatTrigger::Manual,
15359 FormatTarget::Buffers,15570 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
15360 window,15571 window,
15361 cx,15572 cx,
15362 )15573 )
@@ -15376,7 +15587,7 @@ async fn test_document_format_with_prettier(cx: &mut TestAppContext) { · 13 unmodified lines
15376 editor.perform_format(15587 editor.perform_format(
15377 project.clone(),15588 project.clone(),
15378 FormatTrigger::Manual,15589 FormatTrigger::Manual,
15379 FormatTarget::Buffers,15590 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
15380 window,15591 window,
15381 cx,15592 cx,
15382 )15593 )
@@ -21911,6 +22122,7 @@ async fn test_pulling_diagnostics(cx: &mut TestAppContext) { · 6528 unmodified lines
21911 .downcast::<Editor>()22122 .downcast::<Editor>()
21912 .unwrap();22123 .unwrap();
21913 let fake_server = fake_servers.next().await.unwrap();22124 let fake_server = fake_servers.next().await.unwrap();
22125 let server_id = fake_server.server.server_id();
21914 let mut first_request = fake_server22126 let mut first_request = fake_server
21915 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {22127 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
21916 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;22128 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
@@ -21942,7 +22154,10 @@ async fn test_pulling_diagnostics(cx: &mut TestAppContext) { · 25 unmodified lines
21942 .expect("created a singleton buffer")22154 .expect("created a singleton buffer")
21943 .read(cx)22155 .read(cx)
21944 .remote_id();22156 .remote_id();
21945 let buffer_result_id = project.lsp_store().read(cx).result_id(buffer_id);22157 let buffer_result_id = project
22158 .lsp_store()
22159 .read(cx)
22160 .result_id(server_id, buffer_id, cx);
21946 assert_eq!(expected, buffer_result_id);22161 assert_eq!(expected, buffer_result_id);
21947 });22162 });
21948 };22163 };
@@ -21988,7 +22203,6 @@ async fn test_pulling_diagnostics(cx: &mut TestAppContext) { · 39 unmodified lines
21988 "Cursor movement should not trigger diagnostic request"22203 "Cursor movement should not trigger diagnostic request"
21989 );22204 );
21990 ensure_result_id(Some("2".to_string()), cx);22205 ensure_result_id(Some("2".to_string()), cx);
21991
21992 // Multiple rapid edits should be debounced22206 // Multiple rapid edits should be debounced
21993 for _ in 0..5 {22207 for _ in 0..5 {
21994 editor.update_in(cx, |editor, window, cx| {22208 editor.update_in(cx, |editor, window, cx| {
crates/editor/src/element.rs+347−237
@@ -1,15 +1,15 @@
1use crate::{1use crate::{
2 ActiveDiagnostic, BlockId, CURSORS_VISIBLE_FOR, ChunkRendererContext, ChunkReplacement,2 ActiveDiagnostic, BlockId, CURSORS_VISIBLE_FOR, ChunkRendererContext, ChunkReplacement,
3 CodeActionSource, ConflictsOurs, ConflictsOursMarker, ConflictsOuter, ConflictsTheirs,3 CodeActionSource, ColumnarMode, ConflictsOurs, ConflictsOursMarker, ConflictsOuter,
4 ConflictsTheirsMarker, ContextMenuPlacement, CursorShape, CustomBlockId, DisplayDiffHunk,4 ConflictsTheirs, ConflictsTheirsMarker, ContextMenuPlacement, CursorShape, CustomBlockId,
5 DisplayPoint, DisplayRow, DocumentHighlightRead, DocumentHighlightWrite, EditDisplayMode,5 DisplayDiffHunk, DisplayPoint, DisplayRow, DocumentHighlightRead, DocumentHighlightWrite,
6 Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle, FILE_HEADER_HEIGHT,6 EditDisplayMode, Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle,
7 FocusedBlock, GutterDimensions, HalfPageDown, HalfPageUp, HandleInput, HoveredCursor,7 FILE_HEADER_HEIGHT, FocusedBlock, GutterDimensions, HalfPageDown, HalfPageUp, HandleInput,
8 InlayHintRefreshReason, InlineCompletion, JumpData, LineDown, LineHighlight, LineUp,8 HoveredCursor, InlayHintRefreshReason, InlineCompletion, JumpData, LineDown, LineHighlight,
9 MAX_LINE_LEN, MIN_LINE_NUMBER_DIGITS, MINIMAP_FONT_SIZE, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,9 LineUp, MAX_LINE_LEN, MINIMAP_FONT_SIZE, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, OpenExcerpts,
10 OpenExcerpts, PageDown, PageUp, PhantomBreakpointIndicator, Point, RowExt, RowRangeExt,10 PageDown, PageUp, PhantomBreakpointIndicator, Point, RowExt, RowRangeExt, SelectPhase,
11 SelectPhase, SelectedTextHighlight, Selection, SelectionDragState, SoftWrap,11 SelectedTextHighlight, Selection, SelectionDragState, SoftWrap, StickyHeaderExcerpt, ToPoint,
12 StickyHeaderExcerpt, ToPoint, ToggleFold,12 ToggleFold,
13 code_context_menus::{CodeActionsMenu, MENU_ASIDE_MAX_WIDTH, MENU_ASIDE_MIN_WIDTH, MENU_GAP},13 code_context_menus::{CodeActionsMenu, MENU_ASIDE_MAX_WIDTH, MENU_ASIDE_MIN_WIDTH, MENU_GAP},
14 display_map::{14 display_map::{
15 Block, BlockContext, BlockStyle, DisplaySnapshot, EditorMargins, FoldId, HighlightedChunk,15 Block, BlockContext, BlockStyle, DisplaySnapshot, EditorMargins, FoldId, HighlightedChunk,
@@ -75,7 +75,7 @@ use std::{ · 59 unmodified lines
75 ops::{Deref, Range},75 ops::{Deref, Range},
76 rc::Rc,76 rc::Rc,
77 sync::Arc,77 sync::Arc,
78 time::Duration,78 time::{Duration, Instant},
79};79};
80use sum_tree::Bias;80use sum_tree::Bias;
81use text::{BufferId, SelectionGoal};81use text::{BufferId, SelectionGoal};
@@ -87,6 +87,7 @@ use util::{RangeExt, ResultExt, debug_panic}; · 5 unmodified lines
87use workspace::{CollaboratorId, Workspace, item::Item, notifications::NotifyTaskExt};87use workspace::{CollaboratorId, Workspace, item::Item, notifications::NotifyTaskExt};
88 88
89const INLINE_BLAME_PADDING_EM_WIDTHS: f32 = 7.;89const INLINE_BLAME_PADDING_EM_WIDTHS: f32 = 7.;
90const SELECTION_DRAG_DELAY: Duration = Duration::from_millis(300);
90 91
91/// Determines what kinds of highlights should be applied to a lines background.92/// Determines what kinds of highlights should be applied to a lines background.
92#[derive(Clone, Copy, Default)]93#[derive(Clone, Copy, Default)]
@@ -107,6 +108,12 @@ struct SelectionLayout { · 14 unmodified lines
107 user_name: Option<SharedString>,108 user_name: Option<SharedString>,
108}109}
109 110
111struct InlineBlameLayout {
112 element: AnyElement,
113 bounds: Bounds<Pixels>,
114 entry: BlameEntry,
115}
116
110impl SelectionLayout {117impl SelectionLayout {
111 fn new<T: ToPoint + ToDisplayPoint + Clone>(118 fn new<T: ToPoint + ToDisplayPoint + Clone>(
112 selection: Selection<T>,119 selection: Selection<T>,
@@ -187,7 +194,7 @@ impl EditorElement { · 74 unmodified lines
187 let editor = &self.editor;194 let editor = &self.editor;
188 editor.update(cx, |editor, cx| {195 editor.update(cx, |editor, cx| {
189 for action in editor.editor_actions.borrow().values() {196 for action in editor.editor_actions.borrow().values() {
190 (action)(window, cx)197 (action)(editor, window, cx)
191 }198 }
192 });199 });
193 200
@@ -642,6 +649,7 @@ impl EditorElement { · 448 unmodified lines
642 editor.selection_drag_state = SelectionDragState::ReadyToDrag {649 editor.selection_drag_state = SelectionDragState::ReadyToDrag {
643 selection: newest_anchor.clone(),650 selection: newest_anchor.clone(),
644 click_position: event.position,651 click_position: event.position,
652 mouse_down_time: Instant::now(),
645 };653 };
646 cx.stop_propagation();654 cx.stop_propagation();
647 return;655 return;
@@ -692,12 +700,15 @@ impl EditorElement { · 44 unmodified lines
692 }700 }
693 701
694 let position = point_for_position.previous_valid;702 let position = point_for_position.previous_valid;
695 let multi_cursor_modifier = Editor::multi_cursor_modifier(true, &modifiers, cx);703 if let Some(mode) = Editor::columnar_selection_mode(&modifiers, cx) {
696 if Editor::columnar_selection_modifiers(multi_cursor_modifier, &modifiers) {
697 editor.select(704 editor.select(
698 SelectPhase::BeginColumnar {705 SelectPhase::BeginColumnar {
699 position,706 position,
700 reset: true,707 reset: match mode {
708 ColumnarMode::FromMouse => true,
709 ColumnarMode::FromSelection => false,
710 },
711 mode: mode,
701 goal_column: point_for_position.exact_unclipped.column(),712 goal_column: point_for_position.exact_unclipped.column(),
702 },713 },
703 window,714 window,
@@ -717,7 +728,7 @@ impl EditorElement { · 13 unmodified lines
717 editor.select(728 editor.select(
718 SelectPhase::Begin {729 SelectPhase::Begin {
719 position,730 position,
720 add: multi_cursor_modifier,731 add: Editor::multi_cursor_modifier(true, &modifiers, cx),
721 click_count,732 click_count,
722 },733 },
723 window,734 window,
@@ -814,6 +825,7 @@ impl EditorElement { · 90 unmodified lines
814 SelectPhase::BeginColumnar {825 SelectPhase::BeginColumnar {
815 position,826 position,
816 reset: true,827 reset: true,
828 mode: ColumnarMode::FromMouse,
817 goal_column: point_for_position.exact_unclipped.column(),829 goal_column: point_for_position.exact_unclipped.column(),
818 },830 },
819 window,831 window,
@@ -837,6 +849,7 @@ impl EditorElement { · 17 unmodified lines
837 SelectionDragState::ReadyToDrag {849 SelectionDragState::ReadyToDrag {
838 selection: _,850 selection: _,
839 ref click_position,851 ref click_position,
852 mouse_down_time: _,
840 } => {853 } => {
841 if event.position == *click_position {854 if event.position == *click_position {
842 editor.select(855 editor.select(
@@ -851,13 +864,18 @@ impl EditorElement { · 8 unmodified lines
851 editor.selection_drag_state = SelectionDragState::None;864 editor.selection_drag_state = SelectionDragState::None;
852 cx.stop_propagation();865 cx.stop_propagation();
853 return;866 return;
867 } else {
868 debug_panic!("drag state can never be in ready state after drag")
854 }869 }
855 }870 }
856 SelectionDragState::Dragging { ref selection, .. } => {871 SelectionDragState::Dragging { ref selection, .. } => {
857 let snapshot = editor.snapshot(window, cx);872 let snapshot = editor.snapshot(window, cx);
858 let selection_display = selection.map(|anchor| anchor.to_display_point(&snapshot));873 let selection_display = selection.map(|anchor| anchor.to_display_point(&snapshot));
859 if !point_for_position.intersects_selection(&selection_display) {874 if !point_for_position.intersects_selection(&selection_display)
860 let is_cut = !event.modifiers.control;875 && text_hitbox.is_hovered(window)
876 {
877 let is_cut = !(cfg!(target_os = "macos") && event.modifiers.alt
878 || cfg!(not(target_os = "macos")) && event.modifiers.control);
861 editor.move_selection_on_drop(879 editor.move_selection_on_drop(
862 &selection.clone(),880 &selection.clone(),
863 point_for_position.previous_valid,881 point_for_position.previous_valid,
@@ -865,10 +883,11 @@ impl EditorElement { · 1 unmodified line
865 window,883 window,
866 cx,884 cx,
867 );885 );
868 editor.selection_drag_state = SelectionDragState::None;
869 cx.stop_propagation();
870 return;
871 }886 }
887 editor.selection_drag_state = SelectionDragState::None;
888 cx.stop_propagation();
889 cx.notify();
890 return;
872 }891 }
873 _ => {}892 _ => {}
874 }893 }
@@ -941,39 +960,43 @@ impl EditorElement { · 66 unmodified lines
941 return;960 return;
942 }961 }
943 962
944 let text_bounds = position_map.text_hitbox.bounds;
945 let point_for_position = position_map.point_for_position(event.position);963 let point_for_position = position_map.point_for_position(event.position);
964 let text_hitbox = &position_map.text_hitbox;
946 965
947 let mut scroll_delta = gpui::Point::<f32>::default();966 let scroll_delta = {
948 let vertical_margin = position_map.line_height.min(text_bounds.size.height / 3.0);967 let text_bounds = text_hitbox.bounds;
949 let top = text_bounds.origin.y + vertical_margin;968 let mut scroll_delta = gpui::Point::<f32>::default();
950 let bottom = text_bounds.bottom_left().y - vertical_margin;969 let vertical_margin = position_map.line_height.min(text_bounds.size.height / 3.0);
951 if event.position.y < top {970 let top = text_bounds.origin.y + vertical_margin;
952 scroll_delta.y = -scale_vertical_mouse_autoscroll_delta(top - event.position.y);971 let bottom = text_bounds.bottom_left().y - vertical_margin;
953 }972 if event.position.y < top {
954 if event.position.y > bottom {973 scroll_delta.y = -scale_vertical_mouse_autoscroll_delta(top - event.position.y);
955 scroll_delta.y = scale_vertical_mouse_autoscroll_delta(event.position.y - bottom);974 }
956 }975 if event.position.y > bottom {
976 scroll_delta.y = scale_vertical_mouse_autoscroll_delta(event.position.y - bottom);
977 }
957 978
958 // We need horizontal width of text979 // We need horizontal width of text
959 let style = editor.style.clone().unwrap_or_default();980 let style = editor.style.clone().unwrap_or_default();
960 let font_id = window.text_system().resolve_font(&style.text.font());981 let font_id = window.text_system().resolve_font(&style.text.font());
961 let font_size = style.text.font_size.to_pixels(window.rem_size());982 let font_size = style.text.font_size.to_pixels(window.rem_size());
962 let em_width = window.text_system().em_width(font_id, font_size).unwrap();983 let em_width = window.text_system().em_width(font_id, font_size).unwrap();
963 984
964 let scroll_margin_x = EditorSettings::get_global(cx).horizontal_scroll_margin;985 let scroll_margin_x = EditorSettings::get_global(cx).horizontal_scroll_margin;
965 986
966 let scroll_space: Pixels = scroll_margin_x * em_width;987 let scroll_space: Pixels = scroll_margin_x * em_width;
967 988
968 let left = text_bounds.origin.x + scroll_space;989 let left = text_bounds.origin.x + scroll_space;
969 let right = text_bounds.top_right().x - scroll_space;990 let right = text_bounds.top_right().x - scroll_space;
970 991
971 if event.position.x < left {992 if event.position.x < left {
972 scroll_delta.x = -scale_horizontal_mouse_autoscroll_delta(left - event.position.x);993 scroll_delta.x = -scale_horizontal_mouse_autoscroll_delta(left - event.position.x);
973 }994 }
974 if event.position.x > right {995 if event.position.x > right {
975 scroll_delta.x = scale_horizontal_mouse_autoscroll_delta(event.position.x - right);996 scroll_delta.x = scale_horizontal_mouse_autoscroll_delta(event.position.x - right);
976 }997 }
998 scroll_delta
999 };
977 1000
978 if !editor.has_pending_selection() {1001 if !editor.has_pending_selection() {
979 let drop_anchor = position_map1002 let drop_anchor = position_map
@@ -982,28 +1005,60 @@ impl EditorElement { · 2 unmodified lines
982 match editor.selection_drag_state {1005 match editor.selection_drag_state {
983 SelectionDragState::Dragging {1006 SelectionDragState::Dragging {
984 ref mut drop_cursor,1007 ref mut drop_cursor,
1008 ref mut hide_drop_cursor,
985 ..1009 ..
986 } => {1010 } => {
987 drop_cursor.start = drop_anchor;1011 drop_cursor.start = drop_anchor;
988 drop_cursor.end = drop_anchor;1012 drop_cursor.end = drop_anchor;
1013 *hide_drop_cursor = !text_hitbox.is_hovered(window);
1014 editor.apply_scroll_delta(scroll_delta, window, cx);
1015 cx.notify();
989 }1016 }
990 SelectionDragState::ReadyToDrag { ref selection, .. } => {1017 SelectionDragState::ReadyToDrag {
991 let drop_cursor = Selection {1018 ref selection,
992 id: post_inc(&mut editor.selections.next_selection_id),1019 ref click_position,
993 start: drop_anchor,1020 ref mouse_down_time,
994 end: drop_anchor,1021 } => {
995 reversed: false,1022 if mouse_down_time.elapsed() >= SELECTION_DRAG_DELAY {
996 goal: SelectionGoal::None,1023 let drop_cursor = Selection {
997 };1024 id: post_inc(&mut editor.selections.next_selection_id),
998 editor.selection_drag_state = SelectionDragState::Dragging {1025 start: drop_anchor,
999 selection: selection.clone(),1026 end: drop_anchor,
1000 drop_cursor,1027 reversed: false,
1001 };1028 goal: SelectionGoal::None,
1029 };
1030 editor.selection_drag_state = SelectionDragState::Dragging {
1031 selection: selection.clone(),
1032 drop_cursor,
1033 hide_drop_cursor: false,
1034 };
1035 editor.apply_scroll_delta(scroll_delta, window, cx);
1036 cx.notify();
1037 } else {
1038 let click_point = position_map.point_for_position(*click_position);
1039 editor.selection_drag_state = SelectionDragState::None;
1040 editor.select(
1041 SelectPhase::Begin {
1042 position: click_point.previous_valid,
1043 add: false,
1044 click_count: 1,
1045 },
1046 window,
1047 cx,
1048 );
1049 editor.select(
1050 SelectPhase::Update {
1051 position: point_for_position.previous_valid,
1052 goal_column: point_for_position.exact_unclipped.column(),
1053 scroll_delta,
1054 },
1055 window,
1056 cx,
1057 );
1058 }
1002 }1059 }
1003 _ => {}1060 _ => {}
1004 }1061 }
1005 editor.apply_scroll_delta(scroll_delta, window, cx);
1006 cx.notify();
1007 } else {1062 } else {
1008 editor.select(1063 editor.select(
1009 SelectPhase::Update {1064 SelectPhase::Update {
@@ -1027,17 +1082,70 @@ impl EditorElement { · 17 unmodified lines
1027 let text_hitbox = &position_map.text_hitbox;1082 let text_hitbox = &position_map.text_hitbox;
1028 let gutter_hitbox = &position_map.gutter_hitbox;1083 let gutter_hitbox = &position_map.gutter_hitbox;
1029 let modifiers = event.modifiers;1084 let modifiers = event.modifiers;
1085 let text_hovered = text_hitbox.is_hovered(window);
1030 let gutter_hovered = gutter_hitbox.is_hovered(window);1086 let gutter_hovered = gutter_hitbox.is_hovered(window);
1031 editor.set_gutter_hovered(gutter_hovered, cx);1087 editor.set_gutter_hovered(gutter_hovered, cx);
1032 editor.mouse_cursor_hidden = false;1088 editor.mouse_cursor_hidden = false;
1033 1089
1034 if gutter_hovered {1090 let point_for_position = position_map.point_for_position(event.position);
1035 let new_point = position_map1091 let valid_point = point_for_position.previous_valid;
1036 .point_for_position(event.position)1092
1037 .previous_valid;1093 let hovered_diff_control = position_map
1094 .diff_hunk_control_bounds
1095 .iter()
1096 .find(|(_, bounds)| bounds.contains(&event.position))
1097 .map(|(row, _)| *row);
1098
1099 let hovered_diff_hunk_row = if let Some(control_row) = hovered_diff_control {
1100 Some(control_row)
1101 } else {
1102 if text_hovered {
1103 let current_row = valid_point.row();
1104 position_map.display_hunks.iter().find_map(|(hunk, _)| {
1105 if let DisplayDiffHunk::Unfolded {
1106 display_row_range, ..
1107 } = hunk
1108 {
1109 if display_row_range.contains(&current_row) {
1110 Some(display_row_range.start)
1111 } else {
1112 None
1113 }
1114 } else {
1115 None
1116 }
1117 })
1118 } else {
1119 None
1120 }
1121 };
1122
1123 if hovered_diff_hunk_row != editor.hovered_diff_hunk_row {
1124 editor.hovered_diff_hunk_row = hovered_diff_hunk_row;
1125 cx.notify();
1126 }
1127
1128 if let Some((bounds, blame_entry)) = &position_map.inline_blame_bounds {
1129 let mouse_over_inline_blame = bounds.contains(&event.position);
1130 let mouse_over_popover = editor
1131 .inline_blame_popover
1132 .as_ref()
1133 .and_then(|state| state.popover_bounds)
1134 .map_or(false, |bounds| bounds.contains(&event.position));
1135
1136 if mouse_over_inline_blame || mouse_over_popover {
1137 editor.show_blame_popover(&blame_entry, event.position, cx);
1138 } else {
1139 editor.hide_blame_popover(cx);
1140 }
1141 } else {
1142 editor.hide_blame_popover(cx);
1143 }
1144
1145 let breakpoint_indicator = if gutter_hovered {
1038 let buffer_anchor = position_map1146 let buffer_anchor = position_map
1039 .snapshot1147 .snapshot
1040 .display_point_to_anchor(new_point, Bias::Left);1148 .display_point_to_anchor(valid_point, Bias::Left);
1041 1149
1042 if let Some((buffer_snapshot, file)) = position_map1150 if let Some((buffer_snapshot, file)) = position_map
1043 .snapshot1151 .snapshot
@@ -1045,7 +1153,6 @@ impl EditorElement { · 1 unmodified line
1045 .buffer_for_excerpt(buffer_anchor.excerpt_id)1153 .buffer_for_excerpt(buffer_anchor.excerpt_id)
1046 .and_then(|buffer| buffer.file().map(|file| (buffer, file)))1154 .and_then(|buffer| buffer.file().map(|file| (buffer, file)))
1047 {1155 {
1048 let was_hovered = editor.gutter_breakpoint_indicator.0.is_some();
1049 let as_point = text::ToPoint::to_point(&buffer_anchor.text_anchor, buffer_snapshot);1156 let as_point = text::ToPoint::to_point(&buffer_anchor.text_anchor, buffer_snapshot);
1050 1157
1051 let is_visible = editor1158 let is_visible = editor
@@ -1073,43 +1180,46 @@ impl EditorElement { · 21 unmodified lines
1073 .is_some()1180 .is_some()
1074 });1181 });
1075 1182
1076 editor.gutter_breakpoint_indicator.0 = Some(PhantomBreakpointIndicator {1183 if !is_visible {
1077 display_row: new_point.row(),1184 editor.gutter_breakpoint_indicator.1.get_or_insert_with(|| {
1078 is_active: is_visible,1185 cx.spawn(async move |this, cx| {
1079 collides_with_existing_breakpoint: has_existing_breakpoint,
1080 });
1081
1082 editor.gutter_breakpoint_indicator.1.get_or_insert_with(|| {
1083 cx.spawn(async move |this, cx| {
1084 if !was_hovered {
1085 cx.background_executor()1186 cx.background_executor()
1086 .timer(Duration::from_millis(200))1187 .timer(Duration::from_millis(200))
1087 .await;1188 .await;
1088 }
1089
1090 this.update(cx, |this, cx| {
1091 if let Some(indicator) = this.gutter_breakpoint_indicator.0.as_mut() {
1092 indicator.is_active = true;
1093 }
1094 1189
1095 cx.notify();1190 this.update(cx, |this, cx| {
1191 if let Some(indicator) = this.gutter_breakpoint_indicator.0.as_mut()
1192 {
1193 indicator.is_active = true;
1194 cx.notify();
1195 }
1196 })
1197 .ok();
1096 })1198 })
1097 .ok();1199 });
1098 })1200 }
1099 });1201
1202 Some(PhantomBreakpointIndicator {
1203 display_row: valid_point.row(),
1204 is_active: is_visible,
1205 collides_with_existing_breakpoint: has_existing_breakpoint,
1206 })
1100 } else {1207 } else {
1101 editor.gutter_breakpoint_indicator = (None, None);1208 editor.gutter_breakpoint_indicator.1 = None;
1209 None
1102 }1210 }
1103 } else {1211 } else {
1104 editor.gutter_breakpoint_indicator = (None, None);1212 editor.gutter_breakpoint_indicator.1 = None;
1105 }1213 None
1214 };
1106 1215
1107 cx.notify();1216 if &breakpoint_indicator != &editor.gutter_breakpoint_indicator.0 {
1217 editor.gutter_breakpoint_indicator.0 = breakpoint_indicator;
1218 cx.notify();
1219 }
1108 1220
1109 // Don't trigger hover popover if mouse is hovering over context menu1221 // Don't trigger hover popover if mouse is hovering over context menu
1110 if text_hitbox.is_hovered(window) {1222 if text_hovered {
1111 let point_for_position = position_map.point_for_position(event.position);
1112
1113 editor.update_hovered_link(1223 editor.update_hovered_link(
1114 point_for_position,1224 point_for_position,
1115 &position_map.snapshot,1225 &position_map.snapshot,
@@ -1247,16 +1357,18 @@ impl EditorElement { · 131 unmodified lines
1247 if let SelectionDragState::Dragging {1357 if let SelectionDragState::Dragging {
1248 ref selection,1358 ref selection,
1249 ref drop_cursor,1359 ref drop_cursor,
1360 ref hide_drop_cursor,
1250 } = editor.selection_drag_state1361 } = editor.selection_drag_state
1251 {1362 {
1252 if drop_cursor1363 if !hide_drop_cursor
1253 .start1364 && (drop_cursor
1254 .cmp(&selection.start, &snapshot.buffer_snapshot)1365 .start
1255 .eq(&Ordering::Less)1366 .cmp(&selection.start, &snapshot.buffer_snapshot)
1256 || drop_cursor1367 .eq(&Ordering::Less)
1257 .end1368 || drop_cursor
1258 .cmp(&selection.end, &snapshot.buffer_snapshot)1369 .end
1259 .eq(&Ordering::Greater)1370 .cmp(&selection.end, &snapshot.buffer_snapshot)
1371 .eq(&Ordering::Greater))
1260 {1372 {
1261 let drag_cursor_layout = SelectionLayout::new(1373 let drag_cursor_layout = SelectionLayout::new(
1262 drop_cursor.clone(),1374 drop_cursor.clone(),
@@ -1451,7 +1563,7 @@ impl EditorElement { · 188 unmodified lines
1451 snapshot1563 snapshot
1452 .grapheme_at(cursor_position)1564 .grapheme_at(cursor_position)
1453 .or_else(|| {1565 .or_else(|| {
1454 if cursor_column == 0 {1566 if snapshot.is_empty() {
1455 snapshot.placeholder_text().and_then(|s| {1567 snapshot.placeholder_text().and_then(|s| {
1456 s.graphemes(true).next().map(|s| s.to_string().into())1568 s.graphemes(true).next().map(|s| s.to_string().into())
1457 })1569 })
@@ -2230,7 +2342,7 @@ impl EditorElement { · 772 unmodified lines
2230 text_hitbox: &Hitbox,2342 text_hitbox: &Hitbox,
2231 window: &mut Window,2343 window: &mut Window,
2232 cx: &mut App,2344 cx: &mut App,
2233 ) -> Option<AnyElement> {2345 ) -> Option<InlineBlameLayout> {
2234 if !self2346 if !self
2235 .editor2347 .editor
2236 .update(cx, |editor, cx| editor.render_git_blame_inline(window, cx))2348 .update(cx, |editor, cx| editor.render_git_blame_inline(window, cx))
@@ -2259,13 +2371,13 @@ impl EditorElement { · 22 unmodified lines
2259 padding * em_width2371 padding * em_width
2260 };2372 };
2261 2373
2262 let blame_entry = blame2374 let entry = blame
2263 .update(cx, |blame, cx| {2375 .update(cx, |blame, cx| {
2264 blame.blame_for_rows(&[*row_info], cx).next()2376 blame.blame_for_rows(&[*row_info], cx).next()
2265 })2377 })
2266 .flatten()?;2378 .flatten()?;
2267 2379
2268 let mut element = render_inline_blame_entry(blame_entry.clone(), &self.style, cx)?;2380 let mut element = render_inline_blame_entry(entry.clone(), &self.style, cx)?;
2269 2381
2270 let start_y = content_origin.y2382 let start_y = content_origin.y
2271 + line_height * (display_row.as_f32() - scroll_pixel_position.y / line_height);2383 + line_height * (display_row.as_f32() - scroll_pixel_position.y / line_height);
@@ -2294,24 +2406,19 @@ impl EditorElement { · 22 unmodified lines
2294 let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);2406 let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
2295 let bounds = Bounds::new(absolute_offset, size);2407 let bounds = Bounds::new(absolute_offset, size);
2296 2408
2297 self.layout_blame_entry_popover(2409 self.layout_blame_entry_popover(entry.clone(), blame, line_height, text_hitbox, window, cx);
2298 bounds,
2299 blame_entry,
2300 blame,
2301 line_height,
2302 text_hitbox,
2303 window,
2304 cx,
2305 );
2306 2410
2307 element.prepaint_as_root(absolute_offset, AvailableSpace::min_size(), window, cx);2411 element.prepaint_as_root(absolute_offset, AvailableSpace::min_size(), window, cx);
2308 2412
2309 Some(element)2413 Some(InlineBlameLayout {
2414 element,
2415 bounds,
2416 entry,
2417 })
2310 }2418 }
2311 2419
2312 fn layout_blame_entry_popover(2420 fn layout_blame_entry_popover(
2313 &self,2421 &self,
2314 parent_bounds: Bounds<Pixels>,
2315 blame_entry: BlameEntry,2422 blame_entry: BlameEntry,
2316 blame: Entity<GitBlame>,2423 blame: Entity<GitBlame>,
2317 line_height: Pixels,2424 line_height: Pixels,
@@ -2319,91 +2426,59 @@ impl EditorElement { · 1 unmodified line
2319 window: &mut Window,2426 window: &mut Window,
2320 cx: &mut App,2427 cx: &mut App,
2321 ) {2428 ) {
2322 let mouse_position = window.mouse_position();2429 let Some((popover_state, target_point)) = self.editor.read_with(cx, |editor, _| {
2323 let mouse_over_inline_blame = parent_bounds.contains(&mouse_position);
2324 let mouse_over_popover = self.editor.read_with(cx, |editor, _| {
2325 editor2430 editor
2326 .inline_blame_popover2431 .inline_blame_popover
2327 .as_ref()2432 .as_ref()
2328 .and_then(|state| state.popover_bounds)2433 .map(|state| (state.popover_state.clone(), state.position))
2329 .map_or(false, |bounds| bounds.contains(&mouse_position))2434 }) else {
2330 });2435 return;
2331 2436 };
2332 self.editor.update(cx, |editor, cx| {
2333 if mouse_over_inline_blame || mouse_over_popover {
2334 editor.show_blame_popover(&blame_entry, mouse_position, cx);
2335 } else {
2336 editor.hide_blame_popover(cx);
2337 }
2338 });
2339 2437
2340 let should_draw = self.editor.read_with(cx, |editor, _| {2438 let workspace = self
2341 editor2439 .editor
2342 .inline_blame_popover2440 .read_with(cx, |editor, _| editor.workspace().map(|w| w.downgrade()));
2343 .as_ref()2441
2344 .map_or(false, |state| state.show_task.is_none())2442 let maybe_element = workspace.and_then(|workspace| {
2443 render_blame_entry_popover(
2444 blame_entry,
2445 popover_state.scroll_handle,
2446 popover_state.commit_message,
2447 popover_state.markdown,
2448 workspace,
2449 &blame,
2450 window,
2451 cx,
2452 )
2345 });2453 });
2346 2454
2347 if should_draw {2455 if let Some(mut element) = maybe_element {
2348 let maybe_element = self.editor.update(cx, |editor, cx| {2456 let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
2349 editor2457 let overall_height = size.height + HOVER_POPOVER_GAP;
2350 .workspace()2458 let popover_origin = if target_point.y > overall_height {
2351 .map(|workspace| workspace.downgrade())2459 point(target_point.x, target_point.y - size.height)
2352 .zip(2460 } else {
2353 editor2461 point(
2354 .inline_blame_popover2462 target_point.x,
2355 .as_ref()2463 target_point.y + line_height + HOVER_POPOVER_GAP,
2356 .map(|p| p.popover_state.clone()),2464 )
2357 )2465 };
2358 .and_then(|(workspace, popover_state)| {
2359 render_blame_entry_popover(
2360 blame_entry,
2361 popover_state.scroll_handle,
2362 popover_state.commit_message,
2363 popover_state.markdown,
2364 workspace,
2365 &blame,
2366 window,
2367 cx,
2368 )
2369 })
2370 });
2371
2372 if let Some(mut element) = maybe_element {
2373 let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
2374 let origin = self.editor.read_with(cx, |editor, _| {
2375 let target_point = editor
2376 .inline_blame_popover
2377 .as_ref()
2378 .map_or(mouse_position, |state| state.position);
2379
2380 let overall_height = size.height + HOVER_POPOVER_GAP;
2381 let popover_origin = if target_point.y > overall_height {
2382 point(target_point.x, target_point.y - size.height)
2383 } else {
2384 point(
2385 target_point.x,
2386 target_point.y + line_height + HOVER_POPOVER_GAP,
2387 )
2388 };
2389 2466
2390 let horizontal_offset = (text_hitbox.top_right().x2467 let horizontal_offset = (text_hitbox.top_right().x
2391 - POPOVER_RIGHT_OFFSET2468 - POPOVER_RIGHT_OFFSET
2392 - (popover_origin.x + size.width))2469 - (popover_origin.x + size.width))
2393 .min(Pixels::ZERO);2470 .min(Pixels::ZERO);
2394 2471
2395 point(popover_origin.x + horizontal_offset, popover_origin.y)2472 let origin = point(popover_origin.x + horizontal_offset, popover_origin.y);
2396 });2473 let popover_bounds = Bounds::new(origin, size);
2397 2474
2398 let popover_bounds = Bounds::new(origin, size);2475 self.editor.update(cx, |editor, _| {
2399 self.editor.update(cx, |editor, _| {2476 if let Some(state) = &mut editor.inline_blame_popover {
2400 if let Some(state) = &mut editor.inline_blame_popover {2477 state.popover_bounds = Some(popover_bounds);
2401 state.popover_bounds = Some(popover_bounds);2478 }
2402 }2479 });
2403 });
2404 2480
2405 window.defer_draw(element, origin, 2);2481 window.defer_draw(element, origin, 2);
2406 }
2407 }2482 }
2408 }2483 }
2409 2484
@@ -2812,7 +2887,8 @@ impl EditorElement { · 402 unmodified lines
2812 let available_width = gutter_dimensions.left_padding - git_gutter_width;2887 let available_width = gutter_dimensions.left_padding - git_gutter_width;
2813 2888
2814 let editor = self.editor.clone();2889 let editor = self.editor.clone();
2815 let is_wide = max_line_number_length >= MIN_LINE_NUMBER_DIGITS2890 let is_wide = max_line_number_length
2891 >= EditorSettings::get_global(cx).gutter.min_line_number_digits as u32
2816 && row_info2892 && row_info
2817 .buffer_row2893 .buffer_row
2818 .is_some_and(|row| (row + 1).ilog10() + 1 == max_line_number_length)2894 .is_some_and(|row| (row + 1).ilog10() + 1 == max_line_number_length)
@@ -4730,7 +4806,6 @@ impl EditorElement { · 1911 unmodified lines
4730 row_range: Range<DisplayRow>,4806 row_range: Range<DisplayRow>,
4731 row_infos: &[RowInfo],4807 row_infos: &[RowInfo],
4732 text_hitbox: &Hitbox,4808 text_hitbox: &Hitbox,
4733 position_map: &PositionMap,
4734 newest_cursor_position: Option<DisplayPoint>,4809 newest_cursor_position: Option<DisplayPoint>,
4735 line_height: Pixels,4810 line_height: Pixels,
4736 right_margin: Pixels,4811 right_margin: Pixels,
@@ -4740,14 +4815,15 @@ impl EditorElement { · 3 unmodified lines
4740 editor: Entity<Editor>,4815 editor: Entity<Editor>,
4741 window: &mut Window,4816 window: &mut Window,
4742 cx: &mut App,4817 cx: &mut App,
4743 ) -> Vec<AnyElement> {4818 ) -> (Vec<AnyElement>, Vec<(DisplayRow, Bounds<Pixels>)>) {
4744 let render_diff_hunk_controls = editor.read(cx).render_diff_hunk_controls.clone();4819 let render_diff_hunk_controls = editor.read(cx).render_diff_hunk_controls.clone();
4745 let point_for_position = position_map.point_for_position(window.mouse_position());4820 let hovered_diff_hunk_row = editor.read(cx).hovered_diff_hunk_row;
4746 4821
4747 let mut controls = vec![];4822 let mut controls = vec![];
4823 let mut control_bounds = vec![];
4748 4824
4749 let active_positions = [4825 let active_positions = [
4750 Some(point_for_position.previous_valid),4826 hovered_diff_hunk_row.map(|row| DisplayPoint::new(row, 0)),
4751 newest_cursor_position,4827 newest_cursor_position,
4752 ];4828 ];
4753 4829
@@ -4792,6 +4868,7 @@ impl EditorElement { · 38 unmodified lines
4792 {4868 {
4793 continue;4869 continue;
4794 }4870 }
4871
4795 if active_positions4872 if active_positions
4796 .iter()4873 .iter()
4797 .any(|p| p.map_or(false, |p| display_row_range.contains(&p.row())))4874 .any(|p| p.map_or(false, |p| display_row_range.contains(&p.row())))
@@ -4815,6 +4892,9 @@ impl EditorElement { · 17 unmodified lines
4815 4892
4816 let x = text_hitbox.bounds.right() - right_margin - px(10.) - size.width;4893 let x = text_hitbox.bounds.right() - right_margin - px(10.) - size.width;
4817 4894
4895 let bounds = Bounds::new(gpui::Point::new(x, y), size);
4896 control_bounds.push((display_row_range.start, bounds));
4897
4818 window.with_absolute_element_offset(gpui::Point::new(x, y), |window| {4898 window.with_absolute_element_offset(gpui::Point::new(x, y), |window| {
4819 element.prepaint(window, cx)4899 element.prepaint(window, cx)
4820 });4900 });
@@ -4823,7 +4903,7 @@ impl EditorElement { · 2 unmodified lines
4823 }4903 }
4824 }4904 }
4825 4905
4826 controls4906 (controls, control_bounds)
4827 }4907 }
4828 4908
4829 fn layout_signature_help(4909 fn layout_signature_help(
@@ -5562,6 +5642,12 @@ impl EditorElement { · 732 unmodified lines
5562 let editor = self.editor.read(cx);5642 let editor = self.editor.read(cx);
5563 if editor.mouse_cursor_hidden {5643 if editor.mouse_cursor_hidden {
5564 window.set_window_cursor_style(CursorStyle::None);5644 window.set_window_cursor_style(CursorStyle::None);
5645 } else if matches!(
5646 editor.selection_drag_state,
5647 SelectionDragState::Dragging { .. }
5648 ) {
5649 window
5650 .set_cursor_style(CursorStyle::DragCopy, &layout.position_map.text_hitbox);
5565 } else if editor5651 } else if editor
5566 .hovered_link_state5652 .hovered_link_state
5567 .as_ref()5653 .as_ref()
@@ -6203,9 +6289,9 @@ impl EditorElement { · 635 unmodified lines
6203 }6289 }
6204 6290
6205 fn paint_inline_blame(&mut self, layout: &mut EditorLayout, window: &mut Window, cx: &mut App) {6291 fn paint_inline_blame(&mut self, layout: &mut EditorLayout, window: &mut Window, cx: &mut App) {
6206 if let Some(mut inline_blame) = layout.inline_blame.take() {6292 if let Some(mut blame_layout) = layout.inline_blame_layout.take() {
6207 window.paint_layer(layout.position_map.text_hitbox.bounds, |window| {6293 window.paint_layer(layout.position_map.text_hitbox.bounds, |window| {
6208 inline_blame.paint(window, cx);6294 blame_layout.element.paint(window, cx);
6209 })6295 })
6210 }6296 }
6211 }6297 }
@@ -7325,6 +7411,17 @@ impl LineWithInvisibles { · 1113 unmodified lines
7325 paint(window, cx);7411 paint(window, cx);
7326 }),7412 }),
7327 7413
7414 ShowWhitespaceSetting::Trailing => {
7415 let mut previous_start = self.len;
7416 for ([start, end], paint) in invisible_iter.rev() {
7417 if previous_start != end {
7418 break;
7419 }
7420 previous_start = start;
7421 paint(window, cx);
7422 }
7423 }
7424
7328 // For a whitespace to be on a boundary, any of the following conditions need to be met:7425 // For a whitespace to be on a boundary, any of the following conditions need to be met:
7329 // - It is a tab7426 // - It is a tab
7330 // - It is adjacent to an edge (start or end)7427 // - It is adjacent to an edge (start or end)
@@ -7537,7 +7634,7 @@ impl Element for EditorElement { · 206 unmodified lines
7537 fn request_layout(7634 fn request_layout(
7538 &mut self,7635 &mut self,
7539 _: Option<&GlobalElementId>,7636 _: Option<&GlobalElementId>,
7540 __inspector_id: Option<&gpui::InspectorElementId>,7637 _inspector_id: Option<&gpui::InspectorElementId>,
7541 window: &mut Window,7638 window: &mut Window,
7542 cx: &mut App,7639 cx: &mut App,
7543 ) -> (gpui::LayoutId, ()) {7640 ) -> (gpui::LayoutId, ()) {
@@ -7588,7 +7685,10 @@ impl Element for EditorElement { · 44 unmodified lines
7588 window.request_layout(style, None, cx)7685 window.request_layout(style, None, cx)
7589 }7686 }
7590 }7687 }
7591 EditorMode::AutoHeight { max_lines } => {7688 EditorMode::AutoHeight {
7689 min_lines,
7690 max_lines,
7691 } => {
7592 let editor_handle = cx.entity().clone();7692 let editor_handle = cx.entity().clone();
7593 let max_line_number_width =7693 let max_line_number_width =
7594 self.max_line_number_width(&editor.snapshot(window, cx), window, cx);7694 self.max_line_number_width(&editor.snapshot(window, cx), window, cx);
@@ -7599,6 +7699,7 @@ impl Element for EditorElement { · 4 unmodified lines
7599 .update(cx, |editor, cx| {7699 .update(cx, |editor, cx| {
7600 compute_auto_height_layout(7700 compute_auto_height_layout(
7601 editor,7701 editor,
7702 min_lines,
7602 max_lines,7703 max_lines,
7603 max_line_number_width,7704 max_line_number_width,
7604 known_dimensions,7705 known_dimensions,
@@ -7673,8 +7774,7 @@ impl Element for EditorElement { · 68 unmodified lines
7673 let line_height = style.text.line_height_in_pixels(window.rem_size());7774 let line_height = style.text.line_height_in_pixels(window.rem_size());
7674 let em_width = window.text_system().em_width(font_id, font_size).unwrap();7775 let em_width = window.text_system().em_width(font_id, font_size).unwrap();
7675 let em_advance = window.text_system().em_advance(font_id, font_size).unwrap();7776 let em_advance = window.text_system().em_advance(font_id, font_size).unwrap();
7676 7777 let glyph_grid_cell = size(em_advance, line_height);
7677 let glyph_grid_cell = size(em_width, line_height);
7678 7778
7679 let gutter_dimensions = snapshot7779 let gutter_dimensions = snapshot
7680 .gutter_dimensions(7780 .gutter_dimensions(
@@ -8233,7 +8333,7 @@ impl Element for EditorElement { · 552 unmodified lines
8233 MultiBufferRow(end_anchor.to_point(&snapshot.buffer_snapshot).row);8333 MultiBufferRow(end_anchor.to_point(&snapshot.buffer_snapshot).row);
8234 8334
8235 let scroll_max = point(8335 let scroll_max = point(
8236 ((scroll_width - editor_content_width) / em_width).max(0.0),8336 ((scroll_width - editor_content_width) / em_advance).max(0.0),
8237 max_scroll_top,8337 max_scroll_top,
8238 );8338 );
8239 8339
@@ -8245,7 +8345,7 @@ impl Element for EditorElement { · 5 unmodified lines
8245 start_row,8345 start_row,
8246 editor_content_width,8346 editor_content_width,
8247 scroll_width,8347 scroll_width,
8248 em_width,8348 em_advance,
8249 &line_layouts,8349 &line_layouts,
8250 cx,8350 cx,
8251 )8351 )
@@ -8260,10 +8360,9 @@ impl Element for EditorElement { · 8 unmodified lines
8260 });8360 });
8261 8361
8262 let scroll_pixel_position = point(8362 let scroll_pixel_position = point(
8263 scroll_position.x * em_width,8363 scroll_position.x * em_advance,
8264 scroll_position.y * line_height,8364 scroll_position.y * line_height,
8265 );8365 );
8266
8267 let indent_guides = self.layout_indent_guides(8366 let indent_guides = self.layout_indent_guides(
8268 content_origin,8367 content_origin,
8269 text_hitbox.origin,8368 text_hitbox.origin,
@@ -8328,7 +8427,7 @@ impl Element for EditorElement { · 58 unmodified lines
8328 cx,8427 cx,
8329 );8428 );
8330 8429
8331 let mut inline_blame = None;8430 let mut inline_blame_layout = None;
8332 let mut inline_code_actions = None;8431 let mut inline_code_actions = None;
8333 if let Some(newest_selection_head) = newest_selection_head {8432 if let Some(newest_selection_head) = newest_selection_head {
8334 let display_row = newest_selection_head.row();8433 let display_row = newest_selection_head.row();
@@ -8350,7 +8449,7 @@ impl Element for EditorElement { · 15 unmodified lines
8350 let line_layout = &line_layouts[line_ix];8449 let line_layout = &line_layouts[line_ix];
8351 let crease_trailer_layout = crease_trailers[line_ix].as_ref();8450 let crease_trailer_layout = crease_trailers[line_ix].as_ref();
8352 8451
8353 inline_blame = self.layout_inline_blame(8452 if let Some(layout) = self.layout_inline_blame(
8354 display_row,8453 display_row,
8355 row_info,8454 row_info,
8356 line_layout,8455 line_layout,
@@ -8362,8 +8461,8 @@ impl Element for EditorElement { · 5 unmodified lines
8362 &text_hitbox,8461 &text_hitbox,
8363 window,8462 window,
8364 cx,8463 cx,
8365 );8464 ) {
8366 if inline_blame.is_some() {8465 inline_blame_layout = Some(layout);
8367 // Blame overrides inline diagnostics8466 // Blame overrides inline diagnostics
8368 inline_diagnostics.remove(&display_row);8467 inline_diagnostics.remove(&display_row);
8369 }8468 }
@@ -8633,6 +8732,25 @@ impl Element for EditorElement { · 263 unmodified lines
8633 8732
8634 let mode = snapshot.mode.clone();8733 let mode = snapshot.mode.clone();
8635 8734
8735 let (diff_hunk_controls, diff_hunk_control_bounds) = if is_read_only {
8736 (vec![], vec![])
8737 } else {
8738 self.layout_diff_hunk_controls(
8739 start_row..end_row,
8740 &row_infos,
8741 &text_hitbox,
8742 newest_selection_head,
8743 line_height,
8744 right_margin,
8745 scroll_pixel_position,
8746 &display_hunks,
8747 &highlighted_rows,
8748 self.editor.clone(),
8749 window,
8750 cx,
8751 )
8752 };
8753
8636 let position_map = Rc::new(PositionMap {8754 let position_map = Rc::new(PositionMap {
8637 size: bounds.size,8755 size: bounds.size,
8638 visible_row_range,8756 visible_row_range,
@@ -8645,32 +8763,17 @@ impl Element for EditorElement { · 6 unmodified lines
8645 snapshot,8763 snapshot,
8646 gutter_hitbox: gutter_hitbox.clone(),8764 gutter_hitbox: gutter_hitbox.clone(),
8647 text_hitbox: text_hitbox.clone(),8765 text_hitbox: text_hitbox.clone(),
8766 inline_blame_bounds: inline_blame_layout
8767 .as_ref()
8768 .map(|layout| (layout.bounds, layout.entry.clone())),
8769 display_hunks: display_hunks.clone(),
8770 diff_hunk_control_bounds: diff_hunk_control_bounds.clone(),
8648 });8771 });
8649 8772
8650 self.editor.update(cx, |editor, _| {8773 self.editor.update(cx, |editor, _| {
8651 editor.last_position_map = Some(position_map.clone())8774 editor.last_position_map = Some(position_map.clone())
8652 });8775 });
8653 8776
8654 let diff_hunk_controls = if is_read_only {
8655 vec![]
8656 } else {
8657 self.layout_diff_hunk_controls(
8658 start_row..end_row,
8659 &row_infos,
8660 &text_hitbox,
8661 &position_map,
8662 newest_selection_head,
8663 line_height,
8664 right_margin,
8665 scroll_pixel_position,
8666 &display_hunks,
8667 &highlighted_rows,
8668 self.editor.clone(),
8669 window,
8670 cx,
8671 )
8672 };
8673
8674 EditorLayout {8777 EditorLayout {
8675 mode,8778 mode,
8676 position_map,8779 position_map,
@@ -8692,7 +8795,7 @@ impl Element for EditorElement { · 15 unmodified lines
8692 line_numbers,8795 line_numbers,
8693 blamed_display_rows,8796 blamed_display_rows,
8694 inline_diagnostics,8797 inline_diagnostics,
8695 inline_blame,8798 inline_blame_layout,
8696 inline_code_actions,8799 inline_code_actions,
8697 blocks,8800 blocks,
8698 cursors,8801 cursors,
@@ -8718,7 +8821,7 @@ impl Element for EditorElement { · 19 unmodified lines
8718 fn paint(8821 fn paint(
8719 &mut self,8822 &mut self,
8720 _: Option<&GlobalElementId>,8823 _: Option<&GlobalElementId>,
8721 __inspector_id: Option<&gpui::InspectorElementId>,8824 _inspector_id: Option<&gpui::InspectorElementId>,
8722 bounds: Bounds<gpui::Pixels>,8825 bounds: Bounds<gpui::Pixels>,
8723 _: &mut Self::RequestLayoutState,8826 _: &mut Self::RequestLayoutState,
8724 layout: &mut Self::PrepaintState,8827 layout: &mut Self::PrepaintState,
@@ -8873,7 +8976,7 @@ pub struct EditorLayout { · 148 unmodified lines
8873 display_hunks: Vec<(DisplayDiffHunk, Option<Hitbox>)>,8976 display_hunks: Vec<(DisplayDiffHunk, Option<Hitbox>)>,
8874 blamed_display_rows: Option<Vec<AnyElement>>,8977 blamed_display_rows: Option<Vec<AnyElement>>,
8875 inline_diagnostics: HashMap<DisplayRow, AnyElement>,8978 inline_diagnostics: HashMap<DisplayRow, AnyElement>,
8876 inline_blame: Option<AnyElement>,8979 inline_blame_layout: Option<InlineBlameLayout>,
8877 inline_code_actions: Option<AnyElement>,8980 inline_code_actions: Option<AnyElement>,
8878 blocks: Vec<BlockLayout>,8981 blocks: Vec<BlockLayout>,
8879 highlighted_ranges: Vec<(Range<DisplayPoint>, Hsla)>,8982 highlighted_ranges: Vec<(Range<DisplayPoint>, Hsla)>,
@@ -9333,6 +9436,9 @@ pub(crate) struct PositionMap { · 453 unmodified lines
9333 pub snapshot: EditorSnapshot,9436 pub snapshot: EditorSnapshot,
9334 pub text_hitbox: Hitbox,9437 pub text_hitbox: Hitbox,
9335 pub gutter_hitbox: Hitbox,9438 pub gutter_hitbox: Hitbox,
9439 pub inline_blame_bounds: Option<(Bounds<Pixels>, BlameEntry)>,
9440 pub display_hunks: Vec<(DisplayDiffHunk, Option<Hitbox>)>,
9441 pub diff_hunk_control_bounds: Vec<(DisplayRow, Bounds<Pixels>)>,
9336}9442}
9337 9443
9338#[derive(Debug, Copy, Clone)]9444#[derive(Debug, Copy, Clone)]
@@ -9388,7 +9494,7 @@ impl PositionMap { · 49 unmodified lines
9388 let scroll_position = self.snapshot.scroll_position();9494 let scroll_position = self.snapshot.scroll_position();
9389 let position = position - text_bounds.origin;9495 let position = position - text_bounds.origin;
9390 let y = position.y.max(px(0.)).min(self.size.height);9496 let y = position.y.max(px(0.)).min(self.size.height);
9391 let x = position.x + (scroll_position.x * self.em_width);9497 let x = position.x + (scroll_position.x * self.em_advance);
9392 let row = ((y / self.line_height) + scroll_position.y) as u32;9498 let row = ((y / self.line_height) + scroll_position.y) as u32;
9393 9499
9394 let (column, x_overshoot_after_line_end) = if let Some(line) = self9500 let (column, x_overshoot_after_line_end) = if let Some(line) = self
@@ -9766,6 +9872,7 @@ pub fn register_action<T: Action>( · 371 unmodified lines
9766 9872
9767fn compute_auto_height_layout(9873fn compute_auto_height_layout(
9768 editor: &mut Editor,9874 editor: &mut Editor,
9875 min_lines: usize,
9769 max_lines: usize,9876 max_lines: usize,
9770 max_line_number_width: Pixels,9877 max_line_number_width: Pixels,
9771 known_dimensions: Size<Option<Pixels>>,9878 known_dimensions: Size<Option<Pixels>>,
@@ -9813,7 +9920,7 @@ fn compute_auto_height_layout( · 41 unmodified lines
9813 9920
9814 let scroll_height = (snapshot.max_point().row().next_row().0 as f32) * line_height;9921 let scroll_height = (snapshot.max_point().row().next_row().0 as f32) * line_height;
9815 let height = scroll_height9922 let height = scroll_height
9816 .max(line_height)9923 .max(line_height * min_lines as f32)
9817 .min(line_height * max_lines as f32);9924 .min(line_height * max_lines as f32);
9818 9925
9819 Some(size(width, height))9926 Some(size(width, height))
@@ -10116,7 +10223,10 @@ mod tests { · 296 unmodified lines
10116 10223
10117 for editor_mode_without_invisibles in [10224 for editor_mode_without_invisibles in [
10118 EditorMode::SingleLine { auto_width: false },10225 EditorMode::SingleLine { auto_width: false },
10119 EditorMode::AutoHeight { max_lines: 100 },10226 EditorMode::AutoHeight {
10227 min_lines: 1,
10228 max_lines: 100,
10229 },
10120 ] {10230 ] {
10121 for show_line_numbers in [true, false] {10231 for show_line_numbers in [true, false] {
10122 let invisibles = collect_invisibles_from_new_editor(10232 let invisibles = collect_invisibles_from_new_editor(
crates/editor/src/items.rs+43−34
@@ -1,6 +1,7 @@
1use crate::{1use crate::{
2 Anchor, Autoscroll, Editor, EditorEvent, EditorSettings, ExcerptId, ExcerptRange, FormatTarget,2 Anchor, Autoscroll, Editor, EditorEvent, EditorSettings, ExcerptId, ExcerptRange, FormatTarget,
3 MultiBuffer, MultiBufferSnapshot, NavigationData, SearchWithinRange, ToPoint as _,3 MultiBuffer, MultiBufferSnapshot, NavigationData, SearchWithinRange, SelectionEffects,
4 ToPoint as _,
4 editor_settings::SeedQuerySetting,5 editor_settings::SeedQuerySetting,
5 persistence::{DB, SerializedEditor},6 persistence::{DB, SerializedEditor},
6 scroll::ScrollAnchor,7 scroll::ScrollAnchor,
@@ -40,7 +41,7 @@ use ui::{IconDecorationKind, prelude::*}; · 33 unmodified lines
40use util::{ResultExt, TryFutureExt, paths::PathExt};41use util::{ResultExt, TryFutureExt, paths::PathExt};
41use workspace::{42use workspace::{
42 CollaboratorId, ItemId, ItemNavHistory, ToolbarItemLocation, ViewId, Workspace, WorkspaceId,43 CollaboratorId, ItemId, ItemNavHistory, ToolbarItemLocation, ViewId, Workspace, WorkspaceId,
43 item::{FollowableItem, Item, ItemEvent, ProjectItem},44 item::{FollowableItem, Item, ItemEvent, ProjectItem, SaveOptions},
44 searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle},45 searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle},
45};46};
46use workspace::{47use workspace::{
@@ -611,12 +612,13 @@ impl Item for Editor { · 564 unmodified lines
611 if newest_selection.head() == offset {612 if newest_selection.head() == offset {
612 false613 false
613 } else {614 } else {
614 let nav_history = self.nav_history.take();
615 self.set_scroll_anchor(scroll_anchor, window, cx);615 self.set_scroll_anchor(scroll_anchor, window, cx);
616 self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {616 self.change_selections(
617 s.select_ranges([offset..offset])617 SelectionEffects::default().nav_history(false),
618 });618 window,
619 self.nav_history = nav_history;619 cx,
620 |s| s.select_ranges([offset..offset]),
621 );
620 true622 true
621 }623 }
622 } else {624 } else {
@@ -805,7 +807,7 @@ impl Item for Editor { · 182 unmodified lines
805 807
806 fn save(808 fn save(
807 &mut self,809 &mut self,
808 format: bool,810 options: SaveOptions,
809 project: Entity<Project>,811 project: Entity<Project>,
810 window: &mut Window,812 window: &mut Window,
811 cx: &mut Context<Self>,813 cx: &mut Context<Self>,
@@ -816,13 +818,25 @@ impl Item for Editor { · 4 unmodified lines
816 .into_iter()818 .into_iter()
817 .map(|handle| handle.read(cx).base_buffer().unwrap_or(handle.clone()))819 .map(|handle| handle.read(cx).base_buffer().unwrap_or(handle.clone()))
818 .collect::<HashSet<_>>();820 .collect::<HashSet<_>>();
821
822 // let mut buffers_to_save =
823 let buffers_to_save = if self.buffer.read(cx).is_singleton() && !options.autosave {
824 buffers.clone()
825 } else {
826 buffers
827 .iter()
828 .filter(|buffer| buffer.read(cx).is_dirty())
829 .cloned()
830 .collect()
831 };
832
819 cx.spawn_in(window, async move |this, cx| {833 cx.spawn_in(window, async move |this, cx| {
820 if format {834 if options.format {
821 this.update_in(cx, |editor, window, cx| {835 this.update_in(cx, |editor, window, cx| {
822 editor.perform_format(836 editor.perform_format(
823 project.clone(),837 project.clone(),
824 FormatTrigger::Save,838 FormatTrigger::Save,
825 FormatTarget::Buffers,839 FormatTarget::Buffers(buffers_to_save.clone()),
826 window,840 window,
827 cx,841 cx,
828 )842 )
@@ -830,33 +844,28 @@ impl Item for Editor { · 1 unmodified line
830 .await?;844 .await?;
831 }845 }
832 846
833 if buffers.len() == 1 {847 if !buffers_to_save.is_empty() {
834 // Apply full save routine for singleton buffers, to allow to `touch` the file via the editor.
835 project848 project
836 .update(cx, |project, cx| project.save_buffers(buffers, cx))?849 .update(cx, |project, cx| {
850 project.save_buffers(buffers_to_save.clone(), cx)
851 })?
837 .await?;852 .await?;
838 } else {853 }
839 // For multi-buffers, only format and save the buffers with changes.
840 // For clean buffers, we simulate saving by calling `Buffer::did_save`,
841 // so that language servers or other downstream listeners of save events get notified.
842 let (dirty_buffers, clean_buffers) = buffers.into_iter().partition(|buffer| {
843 buffer
844 .read_with(cx, |buffer, _| buffer.is_dirty() || buffer.has_conflict())
845 .unwrap_or(false)
846 });
847 854
848 project855 // Notify about clean buffers for language server events
849 .update(cx, |project, cx| project.save_buffers(dirty_buffers, cx))?856 let buffers_that_were_not_saved: Vec<_> = buffers
850 .await?;857 .into_iter()
851 for buffer in clean_buffers {858 .filter(|b| !buffers_to_save.contains(b))
852 buffer859 .collect();
853 .update(cx, |buffer, cx| {860
854 let version = buffer.saved_version().clone();861 for buffer in buffers_that_were_not_saved {
855 let mtime = buffer.saved_mtime();862 buffer
856 buffer.did_save(version, mtime, cx);863 .update(cx, |buffer, cx| {
857 })864 let version = buffer.saved_version().clone();
858 .ok();865 let mtime = buffer.saved_mtime();
859 }866 buffer.did_save(version, mtime, cx);
867 })
868 .ok();
860 }869 }
861 870
862 Ok(())871 Ok(())
crates/editor/src/jsx_tag_auto_close.rs+9−4
@@ -8,7 +8,7 @@ use util::ResultExt as _; · 7 unmodified lines
8use language::{BufferSnapshot, JsxTagAutoCloseConfig, Node};8use language::{BufferSnapshot, JsxTagAutoCloseConfig, Node};
9use text::{Anchor, OffsetRangeExt as _};9use text::{Anchor, OffsetRangeExt as _};
10 10
11use crate::Editor;11use crate::{Editor, SelectionEffects};
12 12
13pub struct JsxTagCompletionState {13pub struct JsxTagCompletionState {
14 edit_index: usize,14 edit_index: usize,
@@ -600,9 +600,14 @@ pub(crate) fn handle_from( · 585 unmodified lines
600 })600 })
601 .collect::<Vec<_>>();601 .collect::<Vec<_>>();
602 this.update_in(cx, |this, window, cx| {602 this.update_in(cx, |this, window, cx| {
603 this.change_selections_without_updating_completions(None, window, cx, |s| {603 this.change_selections(
604 s.select(base_selections);604 SelectionEffects::no_scroll().completions(false),
605 });605 window,
606 cx,
607 |s| {
608 s.select(base_selections);
609 },
610 );
606 })611 })
607 .ok()?;612 .ok()?;
608 }613 }
crates/editor/src/mouse_context_menu.rs+6−9
@@ -1,8 +1,8 @@
1use crate::{1use crate::{
2 Copy, CopyAndTrim, CopyPermalinkToLine, Cut, DebuggerEvaluateSelectedText, DisplayPoint,2 Copy, CopyAndTrim, CopyPermalinkToLine, Cut, DisplayPoint, DisplaySnapshot, Editor,
3 DisplaySnapshot, Editor, FindAllReferences, GoToDeclaration, GoToDefinition,3 EvaluateSelectedText, FindAllReferences, GoToDeclaration, GoToDefinition, GoToImplementation,
4 GoToImplementation, GoToTypeDefinition, Paste, Rename, RevealInFileManager, SelectMode,4 GoToTypeDefinition, Paste, Rename, RevealInFileManager, SelectMode, SelectionExt,
5 SelectionExt, ToDisplayPoint, ToggleCodeActions,5 ToDisplayPoint, ToggleCodeActions,
6 actions::{Format, FormatSelections},6 actions::{Format, FormatSelections},
7 selections_collection::SelectionsCollection,7 selections_collection::SelectionsCollection,
8};8};
@@ -199,17 +199,14 @@ pub fn deploy_context_menu( · 190 unmodified lines
199 .is_some()199 .is_some()
200 });200 });
201 201
202 let evaluate_selection = command_palette_hooks::CommandPaletteFilter::try_global(cx)202 let evaluate_selection = window.is_action_available(&EvaluateSelectedText, cx);
203 .map_or(false, |filter| {
204 !filter.is_hidden(&DebuggerEvaluateSelectedText)
205 });
206 203
207 ui::ContextMenu::build(window, cx, |menu, _window, _cx| {204 ui::ContextMenu::build(window, cx, |menu, _window, _cx| {
208 let builder = menu205 let builder = menu
209 .on_blur_subscription(Subscription::new(|| {}))206 .on_blur_subscription(Subscription::new(|| {}))
210 .when(evaluate_selection && has_selections, |builder| {207 .when(evaluate_selection && has_selections, |builder| {
211 builder208 builder
212 .action("Evaluate Selection", Box::new(DebuggerEvaluateSelectedText))209 .action("Evaluate Selection", Box::new(EvaluateSelectedText))
213 .separator()210 .separator()
214 })211 })
215 .action("Go to Definition", Box::new(GoToDefinition))212 .action("Go to Definition", Box::new(GoToDefinition))
crates/editor/src/movement.rs+6−4
@@ -58,8 +58,8 @@ pub fn saturating_left(map: &DisplaySnapshot, mut point: DisplayPoint) -> Displa · 57 unmodified lines
58 map.clip_point(point, Bias::Left)58 map.clip_point(point, Bias::Left)
59}59}
60 60
61/// Returns a column to the right of the current point, doing nothing61/// Returns a column to the right of the current point, wrapping
62// if that point is at the end of the line.62/// to the next line if that point is at the end of line.
63pub fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {63pub fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
64 if point.column() < map.line_len(point.row()) {64 if point.column() < map.line_len(point.row()) {
65 *point.column_mut() += 1;65 *point.column_mut() += 1;
@@ -266,10 +266,11 @@ pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> Displa · 200 unmodified lines
266 266
267 let mut is_first_iteration = true;267 let mut is_first_iteration = true;
268 find_preceding_boundary_display_point(map, point, FindRange::MultiLine, |left, right| {268 find_preceding_boundary_display_point(map, point, FindRange::MultiLine, |left, right| {
269 // Make alt-left skip punctuation on Mac OS to respect Mac VSCode behaviour. For example: hello.| goes to |hello.269 // Make alt-left skip punctuation to respect VSCode behaviour. For example: hello.| goes to |hello.
270 if is_first_iteration270 if is_first_iteration
271 && classifier.is_punctuation(right)271 && classifier.is_punctuation(right)
272 && !classifier.is_punctuation(left)272 && !classifier.is_punctuation(left)
273 && left != '\n'
273 {274 {
274 is_first_iteration = false;275 is_first_iteration = false;
275 return false;276 return false;
@@ -318,10 +319,11 @@ pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint · 42 unmodified lines
318 let classifier = map.buffer_snapshot.char_classifier_at(raw_point);319 let classifier = map.buffer_snapshot.char_classifier_at(raw_point);
319 let mut is_first_iteration = true;320 let mut is_first_iteration = true;
320 find_boundary(map, point, FindRange::MultiLine, |left, right| {321 find_boundary(map, point, FindRange::MultiLine, |left, right| {
321 // Make alt-right skip punctuation on Mac OS to respect the Mac behaviour. For example: |.hello goes to .hello|322 // Make alt-right skip punctuation to respect VSCode behaviour. For example: |.hello goes to .hello|
322 if is_first_iteration323 if is_first_iteration
323 && classifier.is_punctuation(left)324 && classifier.is_punctuation(left)
324 && !classifier.is_punctuation(right)325 && !classifier.is_punctuation(right)
326 && right != '\n'
325 {327 {
326 is_first_iteration = false;328 is_first_iteration = false;
327 return false;329 return false;
crates/editor/src/proposed_changes_editor.rs+3−3
@@ -12,7 +12,7 @@ use text::ToOffset; · 11 unmodified lines
12use ui::{ButtonLike, KeyBinding, prelude::*};12use ui::{ButtonLike, KeyBinding, prelude::*};
13use workspace::{13use workspace::{
14 Item, ItemHandle as _, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,14 Item, ItemHandle as _, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
15 searchable::SearchableItemHandle,15 item::SaveOptions, searchable::SearchableItemHandle,
16};16};
17 17
18pub struct ProposedChangesEditor {18pub struct ProposedChangesEditor {
@@ -351,13 +351,13 @@ impl Item for ProposedChangesEditor { · 332 unmodified lines
351 351
352 fn save(352 fn save(
353 &mut self,353 &mut self,
354 format: bool,354 options: SaveOptions,
355 project: Entity<Project>,355 project: Entity<Project>,
356 window: &mut Window,356 window: &mut Window,
357 cx: &mut Context<Self>,357 cx: &mut Context<Self>,
358 ) -> Task<anyhow::Result<()>> {358 ) -> Task<anyhow::Result<()>> {
359 self.editor.update(cx, |editor, cx| {359 self.editor.update(cx, |editor, cx| {
360 Item::save(editor, format, project, window, cx)360 Item::save(editor, options, project, window, cx)
361 })361 })
362 }362 }
363}363}
crates/editor/src/scroll.rs+14−3
@@ -669,12 +669,23 @@ impl Editor { · 668 unmodified lines
669 return;669 return;
670 }670 }
671 671
672 let cur_position = self.scroll_position(cx);672 let mut current_position = self.scroll_position(cx);
673 let Some(visible_line_count) = self.visible_line_count() else {673 let Some(visible_line_count) = self.visible_line_count() else {
674 return;674 return;
675 };675 };
676 let new_pos = cur_position + point(0., amount.lines(visible_line_count));676
677 self.set_scroll_position(new_pos, window, cx);677 // If the scroll position is currently at the left edge of the document
678 // (x == 0.0) and the intent is to scroll right, the gutter's margin
679 // should first be added to the current position, otherwise the cursor
680 // will end at the column position minus the margin, which looks off.
681 if current_position.x == 0.0 && amount.columns() > 0. {
682 if let Some(last_position_map) = &self.last_position_map {
683 current_position.x += self.gutter_dimensions.margin / last_position_map.em_advance;
684 }
685 }
686 let new_position =
687 current_position + point(amount.columns(), amount.lines(visible_line_count));
688 self.set_scroll_position(new_position, window, cx);
678 }689 }
679 690
680 /// Returns an ordering. The newest selection is:691 /// Returns an ordering. The newest selection is:
crates/editor/src/scroll/autoscroll.rs+13−3
@@ -1,12 +1,13 @@
1use crate::{1use crate::{
2 DisplayRow, Editor, EditorMode, LineWithInvisibles, RowExt, display_map::ToDisplayPoint,2 DisplayRow, Editor, EditorMode, LineWithInvisibles, RowExt, SelectionEffects,
3 display_map::ToDisplayPoint,
3};4};
4use gpui::{Bounds, Context, Pixels, Window, px};5use gpui::{Bounds, Context, Pixels, Window, px};
5use language::Point;6use language::Point;
6use multi_buffer::Anchor;7use multi_buffer::Anchor;
7use std::{cmp, f32};8use std::{cmp, f32};
8 9
9#[derive(PartialEq, Eq, Clone, Copy)]10#[derive(Debug, PartialEq, Eq, Clone, Copy)]
10pub enum Autoscroll {11pub enum Autoscroll {
11 Next,12 Next,
12 Strategy(AutoscrollStrategy, Option<Anchor>),13 Strategy(AutoscrollStrategy, Option<Anchor>),
@@ -66,7 +67,16 @@ impl Autoscroll { · 53 unmodified lines
66 }67 }
67}68}
68 69
69#[derive(PartialEq, Eq, Default, Clone, Copy)]70impl Into<SelectionEffects> for Option<Autoscroll> {
71 fn into(self) -> SelectionEffects {
72 match self {
73 Some(autoscroll) => SelectionEffects::scroll(autoscroll),
74 None => SelectionEffects::no_scroll(),
75 }
76 }
77}
78
79#[derive(Debug, PartialEq, Eq, Default, Clone, Copy)]
70pub enum AutoscrollStrategy {80pub enum AutoscrollStrategy {
71 Fit,81 Fit,
72 Newest,82 Newest,
crates/editor/src/scroll/scroll_amount.rs+21−0
@@ -5,6 +5,8 @@ use ui::{Pixels, px}; · 4 unmodified lines
5pub enum ScrollDirection {5pub enum ScrollDirection {
6 Upwards,6 Upwards,
7 Downwards,7 Downwards,
8 Rightwards,
9 Leftwards,
8}10}
9 11
10impl ScrollDirection {12impl ScrollDirection {
@@ -19,6 +21,8 @@ pub enum ScrollAmount { · 8 unmodified lines
19 Line(f32),21 Line(f32),
20 // Scroll N pages (positive is towards the end of the document)22 // Scroll N pages (positive is towards the end of the document)
21 Page(f32),23 Page(f32),
24 // Scroll N columns (positive is towards the right of the document)
25 Column(f32),
22}26}
23 27
24impl ScrollAmount {28impl ScrollAmount {
@@ -32,6 +36,15 @@ impl ScrollAmount { · 7 unmodified lines
32 }36 }
33 (visible_line_count * count).trunc()37 (visible_line_count * count).trunc()
34 }38 }
39 Self::Column(_count) => 0.0,
40 }
41 }
42
43 pub fn columns(&self) -> f32 {
44 match self {
45 Self::Line(_count) => 0.0,
46 Self::Page(_count) => 0.0,
47 Self::Column(count) => *count,
35 }48 }
36 }49 }
37 50
@@ -39,6 +52,12 @@ impl ScrollAmount { · 1 unmodified line
39 match self {52 match self {
40 ScrollAmount::Line(x) => px(line_height.0 * x),53 ScrollAmount::Line(x) => px(line_height.0 * x),
41 ScrollAmount::Page(x) => px(height.0 * x),54 ScrollAmount::Page(x) => px(height.0 * x),
55 // This function seems to only be leveraged by the popover that is
56 // displayed by the editor when, for example, viewing a function's
57 // documentation. Right now that only supports vertical scrolling,
58 // so I'm leaving this at 0.0 for now to try and make it clear that
59 // this should not have an impact on that?
60 ScrollAmount::Column(_) => px(0.0),
42 }61 }
43 }62 }
44 63
@@ -53,6 +72,8 @@ impl ScrollAmount { · 8 unmodified lines
53 match self {72 match self {
54 Self::Line(amount) if amount.is_sign_positive() => ScrollDirection::Downwards,73 Self::Line(amount) if amount.is_sign_positive() => ScrollDirection::Downwards,
55 Self::Page(amount) if amount.is_sign_positive() => ScrollDirection::Downwards,74 Self::Page(amount) if amount.is_sign_positive() => ScrollDirection::Downwards,
75 Self::Column(amount) if amount.is_sign_positive() => ScrollDirection::Rightwards,
76 Self::Column(amount) if amount.is_sign_negative() => ScrollDirection::Leftwards,
56 _ => ScrollDirection::Upwards,77 _ => ScrollDirection::Upwards,
57 }78 }
58 }79 }
crates/editor/src/selections_collection.rs+2−1
@@ -411,7 +411,7 @@ impl<'a> MutableSelectionsCollection<'a> { · 410 unmodified lines
411 self.collection.display_map(self.cx)411 self.collection.display_map(self.cx)
412 }412 }
413 413
414 pub fn buffer(&self) -> Ref<MultiBufferSnapshot> {414 pub fn buffer(&self) -> Ref<'_, MultiBufferSnapshot> {
415 self.collection.buffer(self.cx)415 self.collection.buffer(self.cx)
416 }416 }
417 417
@@ -659,6 +659,7 @@ impl<'a> MutableSelectionsCollection<'a> { · 241 unmodified lines
659 .collect();659 .collect();
660 self.select(selections);660 self.select(selections);
661 }661 }
662
662 pub fn reverse_selections(&mut self) {663 pub fn reverse_selections(&mut self) {
663 let map = &self.display_map();664 let map = &self.display_map();
664 let mut new_selections: Vec<Selection<Point>> = Vec::new();665 let mut new_selections: Vec<Selection<Point>> = Vec::new();
crates/editor/src/test/editor_lsp_test_context.rs+6−0
@@ -169,6 +169,12 @@ impl EditorLspTestContext { · 168 unmodified lines
169 .expect("Opened test file wasn't an editor")169 .expect("Opened test file wasn't an editor")
170 });170 });
171 editor.update_in(&mut cx, |editor, window, cx| {171 editor.update_in(&mut cx, |editor, window, cx| {
172 let nav_history = workspace
173 .read(cx)
174 .active_pane()
175 .read(cx)
176 .nav_history_for_item(&cx.entity());
177 editor.set_nav_history(Some(nav_history));
172 window.focus(&editor.focus_handle(cx))178 window.focus(&editor.focus_handle(cx))
173 });179 });
174 180
crates/extension/src/extension_events.rs+1−0
@@ -36,6 +36,7 @@ impl ExtensionEvents { · 35 unmodified lines
36#[derive(Clone)]36#[derive(Clone)]
37pub enum Event {37pub enum Event {
38 ExtensionInstalled(Arc<ExtensionManifest>),38 ExtensionInstalled(Arc<ExtensionManifest>),
39 ExtensionUninstalled(Arc<ExtensionManifest>),
39 ExtensionsInstalledChanged,40 ExtensionsInstalledChanged,
40 ConfigureExtensionRequested(Arc<ExtensionManifest>),41 ConfigureExtensionRequested(Arc<ExtensionManifest>),
41}42}
crates/extension_api/wit/since_v0.6.0/dap.wit+1−1
@@ -50,7 +50,7 @@ interface dap { · 49 unmodified lines
50 }50 }
51 51
52 record debug-adapter-binary {52 record debug-adapter-binary {
53 command: string,53 command: option<string>,
54 arguments: list<string>,54 arguments: list<string>,
55 envs: env-vars,55 envs: env-vars,
56 cwd: option<string>,56 cwd: option<string>,
crates/extension_host/src/extension_host.rs+21−4
@@ -132,6 +132,7 @@ pub enum Event { · 131 unmodified lines
132 ExtensionsUpdated,132 ExtensionsUpdated,
133 StartedReloading,133 StartedReloading,
134 ExtensionInstalled(Arc<str>),134 ExtensionInstalled(Arc<str>),
135 ExtensionUninstalled(Arc<str>),
135 ExtensionFailedToLoad(Arc<str>),136 ExtensionFailedToLoad(Arc<str>),
136}137}
137 138
@@ -842,6 +843,8 @@ impl ExtensionStore { · 704 unmodified lines
842 let work_dir = self.wasm_host.work_dir.join(extension_id.as_ref());843 let work_dir = self.wasm_host.work_dir.join(extension_id.as_ref());
843 let fs = self.fs.clone();844 let fs = self.fs.clone();
844 845
846 let extension_manifest = self.extension_manifest_for_id(&extension_id).cloned();
847
845 match self.outstanding_operations.entry(extension_id.clone()) {848 match self.outstanding_operations.entry(extension_id.clone()) {
846 btree_map::Entry::Occupied(_) => return,849 btree_map::Entry::Occupied(_) => return,
847 btree_map::Entry::Vacant(e) => e.insert(ExtensionOperation::Remove),850 btree_map::Entry::Vacant(e) => e.insert(ExtensionOperation::Remove),
@@ -878,6 +881,17 @@ impl ExtensionStore { · 30 unmodified lines
878 )881 )
879 .await?;882 .await?;
880 883
884 this.update(cx, |_, cx| {
885 cx.emit(Event::ExtensionUninstalled(extension_id.clone()));
886 if let Some(events) = ExtensionEvents::try_global(cx) {
887 if let Some(manifest) = extension_manifest {
888 events.update(cx, |this, cx| {
889 this.emit(extension::Event::ExtensionUninstalled(manifest.clone()), cx)
890 });
891 }
892 }
893 })?;
894
881 anyhow::Ok(())895 anyhow::Ok(())
882 })896 })
883 .detach_and_log_err(cx)897 .detach_and_log_err(cx)
@@ -1682,12 +1696,15 @@ impl ExtensionStore { · 798 unmodified lines
1682 1696
1683 pub fn register_ssh_client(&mut self, client: Entity<SshRemoteClient>, cx: &mut Context<Self>) {1697 pub fn register_ssh_client(&mut self, client: Entity<SshRemoteClient>, cx: &mut Context<Self>) {
1684 let connection_options = client.read(cx).connection_options();1698 let connection_options = client.read(cx).connection_options();
1685 if self.ssh_clients.contains_key(&connection_options.ssh_url()) {1699 let ssh_url = connection_options.ssh_url();
1686 return;1700
1701 if let Some(existing_client) = self.ssh_clients.get(&ssh_url) {
1702 if existing_client.upgrade().is_some() {
1703 return;
1704 }
1687 }1705 }
1688 1706
1689 self.ssh_clients1707 self.ssh_clients.insert(ssh_url, client.downgrade());
1690 .insert(connection_options.ssh_url(), client.downgrade());
1691 self.ssh_registered_tx.unbounded_send(()).ok();1708 self.ssh_registered_tx.unbounded_send(()).ok();
1692 }1709 }
1693}1710}
crates/extension_host/src/extension_store_test.rs+14−5
@@ -8,9 +8,9 @@ use collections::BTreeMap; · 7 unmodified lines
8use extension::ExtensionHostProxy;8use extension::ExtensionHostProxy;
9use fs::{FakeFs, Fs, RealFs};9use fs::{FakeFs, Fs, RealFs};
10use futures::{AsyncReadExt, StreamExt, io::BufReader};10use futures::{AsyncReadExt, StreamExt, io::BufReader};
11use gpui::{AppContext as _, SemanticVersion, SharedString, TestAppContext};11use gpui::{AppContext as _, SemanticVersion, TestAppContext};
12use http_client::{FakeHttpClient, Response};12use http_client::{FakeHttpClient, Response};
13use language::{BinaryStatus, LanguageMatcher, LanguageRegistry};13use language::{BinaryStatus, LanguageMatcher, LanguageRegistry, LanguageServerStatusUpdate};
14use lsp::LanguageServerName;14use lsp::LanguageServerName;
15use node_runtime::NodeRuntime;15use node_runtime::NodeRuntime;
16use parking_lot::Mutex;16use parking_lot::Mutex;
@@ -717,9 +717,18 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) { · 700 unmodified lines
717 status_updates.next().await.unwrap(),717 status_updates.next().await.unwrap(),
718 ],718 ],
719 [719 [
720 (SharedString::new("gleam"), BinaryStatus::CheckingForUpdate),720 (
721 (SharedString::new("gleam"), BinaryStatus::Downloading),721 LanguageServerName::new_static("gleam"),
722 (SharedString::new("gleam"), BinaryStatus::None)722 LanguageServerStatusUpdate::Binary(BinaryStatus::CheckingForUpdate)
723 ),
724 (
725 LanguageServerName::new_static("gleam"),
726 LanguageServerStatusUpdate::Binary(BinaryStatus::Downloading)
727 ),
728 (
729 LanguageServerName::new_static("gleam"),
730 LanguageServerStatusUpdate::Binary(BinaryStatus::None)
731 )
723 ]732 ]
724 );733 );
725 734
crates/extension_host/src/wasm_host.rs+2−1
@@ -377,6 +377,7 @@ impl extension::Extension for WasmExtension { · 376 unmodified lines
377 })377 })
378 .await378 .await
379 }379 }
380
380 async fn get_dap_binary(381 async fn get_dap_binary(
381 &self,382 &self,
382 dap_name: Arc<str>,383 dap_name: Arc<str>,
@@ -724,7 +725,7 @@ impl IncrementalCompilationCache { · 341 unmodified lines
724}725}
725 726
726impl CacheStore for IncrementalCompilationCache {727impl CacheStore for IncrementalCompilationCache {
727 fn get(&self, key: &[u8]) -> Option<Cow<[u8]>> {728 fn get(&self, key: &[u8]) -> Option<Cow<'_, [u8]>> {
728 self.cache.get(key).map(|v| v.into())729 self.cache.get(key).map(|v| v.into())
729 }730 }
730 731
crates/extensions_ui/src/extensions_ui.rs+14−22
@@ -554,13 +554,15 @@ impl ExtensionsPage { · 553 unmodified lines
554 )554 )
555 .child(555 .child(
556 h_flex()556 h_flex()
557 .gap_2()557 .gap_1()
558 .justify_between()558 .justify_between()
559 .child(559 .child(
560 Button::new(560 Button::new(
561 SharedString::from(format!("rebuild-{}", extension.id)),561 SharedString::from(format!("rebuild-{}", extension.id)),
562 "Rebuild",562 "Rebuild",
563 )563 )
564 .color(Color::Accent)
565 .disabled(matches!(status, ExtensionStatus::Upgrading))
564 .on_click({566 .on_click({
565 let extension_id = extension.id.clone();567 let extension_id = extension.id.clone();
566 move |_, _, cx| {568 move |_, _, cx| {
@@ -568,12 +570,12 @@ impl ExtensionsPage { · 1 unmodified line
568 store.rebuild_dev_extension(extension_id.clone(), cx)570 store.rebuild_dev_extension(extension_id.clone(), cx)
569 });571 });
570 }572 }
571 })573 }),
572 .color(Color::Accent)
573 .disabled(matches!(status, ExtensionStatus::Upgrading)),
574 )574 )
575 .child(575 .child(
576 Button::new(SharedString::from(extension.id.clone()), "Uninstall")576 Button::new(SharedString::from(extension.id.clone()), "Uninstall")
577 .color(Color::Accent)
578 .disabled(matches!(status, ExtensionStatus::Removing))
577 .on_click({579 .on_click({
578 let extension_id = extension.id.clone();580 let extension_id = extension.id.clone();
579 move |_, _, cx| {581 move |_, _, cx| {
@@ -581,9 +583,7 @@ impl ExtensionsPage { · 1 unmodified line
581 store.uninstall_extension(extension_id.clone(), cx)583 store.uninstall_extension(extension_id.clone(), cx)
582 });584 });
583 }585 }
584 })586 }),
585 .color(Color::Accent)
586 .disabled(matches!(status, ExtensionStatus::Removing)),
587 )587 )
588 .when(can_configure, |this| {588 .when(can_configure, |this| {
589 this.child(589 this.child(
@@ -591,8 +591,8 @@ impl ExtensionsPage { · 1 unmodified line
591 SharedString::from(format!("configure-{}", extension.id)),591 SharedString::from(format!("configure-{}", extension.id)),
592 "Configure",592 "Configure",
593 )593 )
594 594 .color(Color::Accent)
595 595 .disabled(matches!(status, ExtensionStatus::Installing))
596 .on_click({596 .on_click({
597 let manifest = Arc::new(extension.clone());597 let manifest = Arc::new(extension.clone());
598 move |_, _, cx| {598 move |_, _, cx| {
@@ -609,9 +609,7 @@ impl ExtensionsPage { · 10 unmodified lines
609 });609 });
610 }610 }
611 }611 }
612 })612 }),
613 .color(Color::Accent)
614 .disabled(matches!(status, ExtensionStatus::Installing)),
615 )613 )
616 }),614 }),
617 ),615 ),
@@ -1479,18 +1477,12 @@ impl Render for ExtensionsPage { · 861 unmodified lines
1479 return this.py_4().child(self.render_empty_state(cx));1477 return this.py_4().child(self.render_empty_state(cx));
1480 }1478 }
1481 1479
1482 let extensions_page = cx.entity().clone();
1483 let scroll_handle = self.list.clone();1480 let scroll_handle = self.list.clone();
1484 this.child(1481 this.child(
1485 uniform_list(1482 uniform_list("entries", count, cx.processor(Self::render_extensions))
1486 extensions_page,1483 .flex_grow()
1487 "entries",1484 .pb_4()
1488 count,1485 .track_scroll(scroll_handle),
1489 Self::render_extensions,
1490 )
1491 .flex_grow()
1492 .pb_4()
1493 .track_scroll(scroll_handle),
1494 )1486 )
1495 .child(1487 .child(
1496 div()1488 div()
crates/file_finder/src/file_finder_tests.rs+2−2
@@ -209,7 +209,7 @@ async fn test_matching_paths(cx: &mut TestAppContext) { · 208 unmodified lines
209 "bandana",209 "bandana",
210 "./bandana",210 "./bandana",
211 ".\\bandana",211 ".\\bandana",
212 util::separator!("a/bandana"),212 util::path!("a/bandana"),
213 "b/bandana",213 "b/bandana",
214 "b\\bandana",214 "b\\bandana",
215 " bandana",215 " bandana",
@@ -230,7 +230,7 @@ async fn test_matching_paths(cx: &mut TestAppContext) { · 14 unmodified lines
230 assert_eq!(230 assert_eq!(
231 picker.delegate.matches.len(),231 picker.delegate.matches.len(),
232 // existence of CreateNew option depends on whether path already exists232 // existence of CreateNew option depends on whether path already exists
233 if bandana_query == util::separator!("a/bandana") {233 if bandana_query == util::path!("a/bandana") {
234 1234 1
235 } else {235 } else {
236 2236 2
crates/fs/src/fs.rs+15−0
@@ -897,6 +897,7 @@ struct FakeFsState { · 896 unmodified lines
897 buffered_events: Vec<PathEvent>,897 buffered_events: Vec<PathEvent>,
898 metadata_call_count: usize,898 metadata_call_count: usize,
899 read_dir_call_count: usize,899 read_dir_call_count: usize,
900 path_write_counts: std::collections::HashMap<PathBuf, usize>,
900 moves: std::collections::HashMap<u64, PathBuf>,901 moves: std::collections::HashMap<u64, PathBuf>,
901 home_dir: Option<PathBuf>,902 home_dir: Option<PathBuf>,
902}903}
@@ -1083,6 +1084,7 @@ impl FakeFs { · 180 unmodified lines
1083 events_paused: false,1084 events_paused: false,
1084 read_dir_call_count: 0,1085 read_dir_call_count: 0,
1085 metadata_call_count: 0,1086 metadata_call_count: 0,
1087 path_write_counts: Default::default(),
1086 moves: Default::default(),1088 moves: Default::default(),
1087 home_dir: None,1089 home_dir: None,
1088 })),1090 })),
@@ -1173,6 +1175,8 @@ impl FakeFs { · 84 unmodified lines
1173 recreate_inode: bool,1175 recreate_inode: bool,
1174 ) -> Result<()> {1176 ) -> Result<()> {
1175 let mut state = self.state.lock();1177 let mut state = self.state.lock();
1178 let path_buf = path.as_ref().to_path_buf();
1179 *state.path_write_counts.entry(path_buf).or_insert(0) += 1;
1176 let new_inode = state.get_and_increment_inode();1180 let new_inode = state.get_and_increment_inode();
1177 let new_mtime = state.get_and_increment_mtime();1181 let new_mtime = state.get_and_increment_mtime();
1178 let new_len = new_content.len() as u64;1182 let new_len = new_content.len() as u64;
@@ -1727,6 +1731,17 @@ impl FakeFs { · 548 unmodified lines
1727 self.state.lock().metadata_call_count1731 self.state.lock().metadata_call_count
1728 }1732 }
1729 1733
1734 /// How many write operations have been issued for a specific path.
1735 pub fn write_count_for_path(&self, path: impl AsRef<Path>) -> usize {
1736 let path = path.as_ref().to_path_buf();
1737 self.state
1738 .lock()
1739 .path_write_counts
1740 .get(&path)
1741 .copied()
1742 .unwrap_or(0)
1743 }
1744
1730 fn simulate_random_delay(&self) -> impl futures::Future<Output = ()> {1745 fn simulate_random_delay(&self) -> impl futures::Future<Output = ()> {
1731 self.executor.simulate_random_delay()1746 self.executor.simulate_random_delay()
1732 }1747 }
crates/git/src/git.rs+1−0
@@ -57,6 +57,7 @@ actions!( · 56 unmodified lines
57 ExpandCommitEditor,57 ExpandCommitEditor,
58 GenerateCommitMessage,58 GenerateCommitMessage,
59 Init,59 Init,
60 OpenModifiedFiles,
60 ]61 ]
61);62);
62 63
crates/git/src/repository.rs+72−28
@@ -26,8 +26,8 @@ use std::{ · 25 unmodified lines
26};26};
27use sum_tree::MapSeekTarget;27use sum_tree::MapSeekTarget;
28use thiserror::Error;28use thiserror::Error;
29use util::ResultExt;
30use util::command::{new_smol_command, new_std_command};29use util::command::{new_smol_command, new_std_command};
30use util::{ResultExt, paths};
31use uuid::Uuid;31use uuid::Uuid;
32 32
33pub use askpass::{AskPassDelegate, AskPassResult, AskPassSession};33pub use askpass::{AskPassDelegate, AskPassResult, AskPassSession};
@@ -323,7 +323,7 @@ pub trait GitRepository: Send + Sync { · 289 unmodified lines
323 /// Resolve a list of refs to SHAs.323 /// Resolve a list of refs to SHAs.
324 fn revparse_batch(&self, revs: Vec<String>) -> BoxFuture<Result<Vec<Option<String>>>>;324 fn revparse_batch(&self, revs: Vec<String>) -> BoxFuture<Result<Vec<Option<String>>>>;
325 325
326 fn head_sha(&self) -> BoxFuture<Option<String>> {326 fn head_sha(&self) -> BoxFuture<'_, Option<String>> {
327 async move {327 async move {
328 self.revparse_batch(vec!["HEAD".into()])328 self.revparse_batch(vec!["HEAD".into()])
329 .await329 .await
@@ -508,6 +508,50 @@ pub struct GitRepositoryCheckpoint { · 178 unmodified lines
508 pub commit_sha: Oid,508 pub commit_sha: Oid,
509}509}
510 510
511#[derive(Debug)]
512pub struct GitCommitter {
513 pub name: Option<String>,
514 pub email: Option<String>,
515}
516
517pub async fn get_git_committer(cx: &AsyncApp) -> GitCommitter {
518 if cfg!(any(feature = "test-support", test)) {
519 return GitCommitter {
520 name: None,
521 email: None,
522 };
523 }
524
525 let git_binary_path =
526 if cfg!(target_os = "macos") && option_env!("ZED_BUNDLE").as_deref() == Some("true") {
527 cx.update(|cx| {
528 cx.path_for_auxiliary_executable("git")
529 .context("could not find git binary path")
530 .log_err()
531 })
532 .ok()
533 .flatten()
534 } else {
535 None
536 };
537
538 let git = GitBinary::new(
539 git_binary_path.unwrap_or(PathBuf::from("git")),
540 paths::home_dir().clone(),
541 cx.background_executor().clone(),
542 );
543
544 cx.background_spawn(async move {
545 let name = git.run(["config", "--global", "user.name"]).await.log_err();
546 let email = git
547 .run(["config", "--global", "user.email"])
548 .await
549 .log_err();
550 GitCommitter { name, email }
551 })
552 .await
553}
554
511impl GitRepository for RealGitRepository {555impl GitRepository for RealGitRepository {
512 fn reload_index(&self) {556 fn reload_index(&self) {
513 if let Ok(mut index) = self.repository.lock().index() {557 if let Ok(mut index) = self.repository.lock().index() {
@@ -525,7 +569,7 @@ impl GitRepository for RealGitRepository { · 11 unmodified lines
525 repo.commondir().into()569 repo.commondir().into()
526 }570 }
527 571
528 fn show(&self, commit: String) -> BoxFuture<Result<CommitDetails>> {572 fn show(&self, commit: String) -> BoxFuture<'_, Result<CommitDetails>> {
529 let working_directory = self.working_directory();573 let working_directory = self.working_directory();
530 self.executor574 self.executor
531 .spawn(async move {575 .spawn(async move {
@@ -561,7 +605,7 @@ impl GitRepository for RealGitRepository { · 29 unmodified lines
561 .boxed()605 .boxed()
562 }606 }
563 607
564 fn load_commit(&self, commit: String, cx: AsyncApp) -> BoxFuture<Result<CommitDiff>> {608 fn load_commit(&self, commit: String, cx: AsyncApp) -> BoxFuture<'_, Result<CommitDiff>> {
565 let Some(working_directory) = self.repository.lock().workdir().map(ToOwned::to_owned)609 let Some(working_directory) = self.repository.lock().workdir().map(ToOwned::to_owned)
566 else {610 else {
567 return future::ready(Err(anyhow!("no working directory"))).boxed();611 return future::ready(Err(anyhow!("no working directory"))).boxed();
@@ -668,7 +712,7 @@ impl GitRepository for RealGitRepository { · 100 unmodified lines
668 commit: String,712 commit: String,
669 mode: ResetMode,713 mode: ResetMode,
670 env: Arc<HashMap<String, String>>,714 env: Arc<HashMap<String, String>>,
671 ) -> BoxFuture<Result<()>> {715 ) -> BoxFuture<'_, Result<()>> {
672 async move {716 async move {
673 let working_directory = self.working_directory();717 let working_directory = self.working_directory();
674 718
@@ -698,7 +742,7 @@ impl GitRepository for RealGitRepository { · 23 unmodified lines
698 commit: String,742 commit: String,
699 paths: Vec<RepoPath>,743 paths: Vec<RepoPath>,
700 env: Arc<HashMap<String, String>>,744 env: Arc<HashMap<String, String>>,
701 ) -> BoxFuture<Result<()>> {745 ) -> BoxFuture<'_, Result<()>> {
702 let working_directory = self.working_directory();746 let working_directory = self.working_directory();
703 let git_binary_path = self.git_binary_path.clone();747 let git_binary_path = self.git_binary_path.clone();
704 async move {748 async move {
@@ -723,7 +767,7 @@ impl GitRepository for RealGitRepository { · 18 unmodified lines
723 .boxed()767 .boxed()
724 }768 }
725 769
726 fn load_index_text(&self, path: RepoPath) -> BoxFuture<Option<String>> {770 fn load_index_text(&self, path: RepoPath) -> BoxFuture<'_, Option<String>> {
727 // https://git-scm.com/book/en/v2/Git-Internals-Git-Objects771 // https://git-scm.com/book/en/v2/Git-Internals-Git-Objects
728 const GIT_MODE_SYMLINK: u32 = 0o120000;772 const GIT_MODE_SYMLINK: u32 = 0o120000;
729 773
@@ -756,7 +800,7 @@ impl GitRepository for RealGitRepository { · 26 unmodified lines
756 .boxed()800 .boxed()
757 }801 }
758 802
759 fn load_committed_text(&self, path: RepoPath) -> BoxFuture<Option<String>> {803 fn load_committed_text(&self, path: RepoPath) -> BoxFuture<'_, Option<String>> {
760 let repo = self.repository.clone();804 let repo = self.repository.clone();
761 self.executor805 self.executor
762 .spawn(async move {806 .spawn(async move {
@@ -777,7 +821,7 @@ impl GitRepository for RealGitRepository { · 14 unmodified lines
777 path: RepoPath,821 path: RepoPath,
778 content: Option<String>,822 content: Option<String>,
779 env: Arc<HashMap<String, String>>,823 env: Arc<HashMap<String, String>>,
780 ) -> BoxFuture<anyhow::Result<()>> {824 ) -> BoxFuture<'_, anyhow::Result<()>> {
781 let working_directory = self.working_directory();825 let working_directory = self.working_directory();
782 let git_binary_path = self.git_binary_path.clone();826 let git_binary_path = self.git_binary_path.clone();
783 self.executor827 self.executor
@@ -841,7 +885,7 @@ impl GitRepository for RealGitRepository { · 57 unmodified lines
841 remote.url().map(|url| url.to_string())885 remote.url().map(|url| url.to_string())
842 }886 }
843 887
844 fn revparse_batch(&self, revs: Vec<String>) -> BoxFuture<Result<Vec<Option<String>>>> {888 fn revparse_batch(&self, revs: Vec<String>) -> BoxFuture<'_, Result<Vec<Option<String>>>> {
845 let working_directory = self.working_directory();889 let working_directory = self.working_directory();
846 self.executor890 self.executor
847 .spawn(async move {891 .spawn(async move {
@@ -891,14 +935,14 @@ impl GitRepository for RealGitRepository { · 43 unmodified lines
891 .boxed()935 .boxed()
892 }936 }
893 937
894 fn merge_message(&self) -> BoxFuture<Option<String>> {938 fn merge_message(&self) -> BoxFuture<'_, Option<String>> {
895 let path = self.path().join("MERGE_MSG");939 let path = self.path().join("MERGE_MSG");
896 self.executor940 self.executor
897 .spawn(async move { std::fs::read_to_string(&path).ok() })941 .spawn(async move { std::fs::read_to_string(&path).ok() })
898 .boxed()942 .boxed()
899 }943 }
900 944
901 fn status(&self, path_prefixes: &[RepoPath]) -> BoxFuture<Result<GitStatus>> {945 fn status(&self, path_prefixes: &[RepoPath]) -> BoxFuture<'_, Result<GitStatus>> {
902 let git_binary_path = self.git_binary_path.clone();946 let git_binary_path = self.git_binary_path.clone();
903 let working_directory = self.working_directory();947 let working_directory = self.working_directory();
904 let path_prefixes = path_prefixes.to_owned();948 let path_prefixes = path_prefixes.to_owned();
@@ -919,7 +963,7 @@ impl GitRepository for RealGitRepository { · 14 unmodified lines
919 .boxed()963 .boxed()
920 }964 }
921 965
922 fn branches(&self) -> BoxFuture<Result<Vec<Branch>>> {966 fn branches(&self) -> BoxFuture<'_, Result<Vec<Branch>>> {
923 let working_directory = self.working_directory();967 let working_directory = self.working_directory();
924 let git_binary_path = self.git_binary_path.clone();968 let git_binary_path = self.git_binary_path.clone();
925 self.executor969 self.executor
@@ -986,7 +1030,7 @@ impl GitRepository for RealGitRepository { · 60 unmodified lines
986 .boxed()1030 .boxed()
987 }1031 }
988 1032
989 fn change_branch(&self, name: String) -> BoxFuture<Result<()>> {1033 fn change_branch(&self, name: String) -> BoxFuture<'_, Result<()>> {
990 let repo = self.repository.clone();1034 let repo = self.repository.clone();
991 self.executor1035 self.executor
992 .spawn(async move {1036 .spawn(async move {
@@ -1018,7 +1062,7 @@ impl GitRepository for RealGitRepository { · 25 unmodified lines
1018 .boxed()1062 .boxed()
1019 }1063 }
1020 1064
1021 fn create_branch(&self, name: String) -> BoxFuture<Result<()>> {1065 fn create_branch(&self, name: String) -> BoxFuture<'_, Result<()>> {
1022 let repo = self.repository.clone();1066 let repo = self.repository.clone();
1023 self.executor1067 self.executor
1024 .spawn(async move {1068 .spawn(async move {
@@ -1030,7 +1074,7 @@ impl GitRepository for RealGitRepository { · 5 unmodified lines
1030 .boxed()1074 .boxed()
1031 }1075 }
1032 1076
1033 fn blame(&self, path: RepoPath, content: Rope) -> BoxFuture<Result<crate::blame::Blame>> {1077 fn blame(&self, path: RepoPath, content: Rope) -> BoxFuture<'_, Result<crate::blame::Blame>> {
1034 let working_directory = self.working_directory();1078 let working_directory = self.working_directory();
1035 let git_binary_path = self.git_binary_path.clone();1079 let git_binary_path = self.git_binary_path.clone();
1036 1080
@@ -1052,7 +1096,7 @@ impl GitRepository for RealGitRepository { · 15 unmodified lines
1052 .boxed()1096 .boxed()
1053 }1097 }
1054 1098
1055 fn diff(&self, diff: DiffType) -> BoxFuture<Result<String>> {1099 fn diff(&self, diff: DiffType) -> BoxFuture<'_, Result<String>> {
1056 let working_directory = self.working_directory();1100 let working_directory = self.working_directory();
1057 let git_binary_path = self.git_binary_path.clone();1101 let git_binary_path = self.git_binary_path.clone();
1058 self.executor1102 self.executor
@@ -1083,7 +1127,7 @@ impl GitRepository for RealGitRepository { · 24 unmodified lines
1083 &self,1127 &self,
1084 paths: Vec<RepoPath>,1128 paths: Vec<RepoPath>,
1085 env: Arc<HashMap<String, String>>,1129 env: Arc<HashMap<String, String>>,
1086 ) -> BoxFuture<Result<()>> {1130 ) -> BoxFuture<'_, Result<()>> {
1087 let working_directory = self.working_directory();1131 let working_directory = self.working_directory();
1088 let git_binary_path = self.git_binary_path.clone();1132 let git_binary_path = self.git_binary_path.clone();
1089 self.executor1133 self.executor
@@ -1111,7 +1155,7 @@ impl GitRepository for RealGitRepository { · 21 unmodified lines
1111 &self,1155 &self,
1112 paths: Vec<RepoPath>,1156 paths: Vec<RepoPath>,
1113 env: Arc<HashMap<String, String>>,1157 env: Arc<HashMap<String, String>>,
1114 ) -> BoxFuture<Result<()>> {1158 ) -> BoxFuture<'_, Result<()>> {
1115 let working_directory = self.working_directory();1159 let working_directory = self.working_directory();
1116 let git_binary_path = self.git_binary_path.clone();1160 let git_binary_path = self.git_binary_path.clone();
1117 1161
@@ -1143,7 +1187,7 @@ impl GitRepository for RealGitRepository { · 25 unmodified lines
1143 name_and_email: Option<(SharedString, SharedString)>,1187 name_and_email: Option<(SharedString, SharedString)>,
1144 options: CommitOptions,1188 options: CommitOptions,
1145 env: Arc<HashMap<String, String>>,1189 env: Arc<HashMap<String, String>>,
1146 ) -> BoxFuture<Result<()>> {1190 ) -> BoxFuture<'_, Result<()>> {
1147 let working_directory = self.working_directory();1191 let working_directory = self.working_directory();
1148 self.executor1192 self.executor
1149 .spawn(async move {1193 .spawn(async move {
@@ -1182,7 +1226,7 @@ impl GitRepository for RealGitRepository { · 32 unmodified lines
1182 ask_pass: AskPassDelegate,1226 ask_pass: AskPassDelegate,
1183 env: Arc<HashMap<String, String>>,1227 env: Arc<HashMap<String, String>>,
1184 cx: AsyncApp,1228 cx: AsyncApp,
1185 ) -> BoxFuture<Result<RemoteCommandOutput>> {1229 ) -> BoxFuture<'_, Result<RemoteCommandOutput>> {
1186 let working_directory = self.working_directory();1230 let working_directory = self.working_directory();
1187 let executor = cx.background_executor().clone();1231 let executor = cx.background_executor().clone();
1188 async move {1232 async move {
@@ -1214,7 +1258,7 @@ impl GitRepository for RealGitRepository { · 25 unmodified lines
1214 ask_pass: AskPassDelegate,1258 ask_pass: AskPassDelegate,
1215 env: Arc<HashMap<String, String>>,1259 env: Arc<HashMap<String, String>>,
1216 cx: AsyncApp,1260 cx: AsyncApp,
1217 ) -> BoxFuture<Result<RemoteCommandOutput>> {1261 ) -> BoxFuture<'_, Result<RemoteCommandOutput>> {
1218 let working_directory = self.working_directory();1262 let working_directory = self.working_directory();
1219 let executor = cx.background_executor().clone();1263 let executor = cx.background_executor().clone();
1220 async move {1264 async move {
@@ -1239,7 +1283,7 @@ impl GitRepository for RealGitRepository { · 18 unmodified lines
1239 ask_pass: AskPassDelegate,1283 ask_pass: AskPassDelegate,
1240 env: Arc<HashMap<String, String>>,1284 env: Arc<HashMap<String, String>>,
1241 cx: AsyncApp,1285 cx: AsyncApp,
1242 ) -> BoxFuture<Result<RemoteCommandOutput>> {1286 ) -> BoxFuture<'_, Result<RemoteCommandOutput>> {
1243 let working_directory = self.working_directory();1287 let working_directory = self.working_directory();
1244 let remote_name = format!("{}", fetch_options);1288 let remote_name = format!("{}", fetch_options);
1245 let executor = cx.background_executor().clone();1289 let executor = cx.background_executor().clone();
@@ -1257,7 +1301,7 @@ impl GitRepository for RealGitRepository { · 11 unmodified lines
1257 .boxed()1301 .boxed()
1258 }1302 }
1259 1303
1260 fn get_remotes(&self, branch_name: Option<String>) -> BoxFuture<Result<Vec<Remote>>> {1304 fn get_remotes(&self, branch_name: Option<String>) -> BoxFuture<'_, Result<Vec<Remote>>> {
1261 let working_directory = self.working_directory();1305 let working_directory = self.working_directory();
1262 let git_binary_path = self.git_binary_path.clone();1306 let git_binary_path = self.git_binary_path.clone();
1263 self.executor1307 self.executor
@@ -1303,7 +1347,7 @@ impl GitRepository for RealGitRepository { · 39 unmodified lines
1303 .boxed()1347 .boxed()
1304 }1348 }
1305 1349
1306 fn check_for_pushed_commit(&self) -> BoxFuture<Result<Vec<SharedString>>> {1350 fn check_for_pushed_commit(&self) -> BoxFuture<'_, Result<Vec<SharedString>>> {
1307 let working_directory = self.working_directory();1351 let working_directory = self.working_directory();
1308 let git_binary_path = self.git_binary_path.clone();1352 let git_binary_path = self.git_binary_path.clone();
1309 self.executor1353 self.executor
@@ -1396,7 +1440,7 @@ impl GitRepository for RealGitRepository { · 86 unmodified lines
1396 .boxed()1440 .boxed()
1397 }1441 }
1398 1442
1399 fn restore_checkpoint(&self, checkpoint: GitRepositoryCheckpoint) -> BoxFuture<Result<()>> {1443 fn restore_checkpoint(&self, checkpoint: GitRepositoryCheckpoint) -> BoxFuture<'_, Result<()>> {
1400 let working_directory = self.working_directory();1444 let working_directory = self.working_directory();
1401 let git_binary_path = self.git_binary_path.clone();1445 let git_binary_path = self.git_binary_path.clone();
1402 1446
@@ -1435,7 +1479,7 @@ impl GitRepository for RealGitRepository { · 32 unmodified lines
1435 &self,1479 &self,
1436 left: GitRepositoryCheckpoint,1480 left: GitRepositoryCheckpoint,
1437 right: GitRepositoryCheckpoint,1481 right: GitRepositoryCheckpoint,
1438 ) -> BoxFuture<Result<bool>> {1482 ) -> BoxFuture<'_, Result<bool>> {
1439 let working_directory = self.working_directory();1483 let working_directory = self.working_directory();
1440 let git_binary_path = self.git_binary_path.clone();1484 let git_binary_path = self.git_binary_path.clone();
1441 1485
@@ -1474,7 +1518,7 @@ impl GitRepository for RealGitRepository { · 32 unmodified lines
1474 &self,1518 &self,
1475 base_checkpoint: GitRepositoryCheckpoint,1519 base_checkpoint: GitRepositoryCheckpoint,
1476 target_checkpoint: GitRepositoryCheckpoint,1520 target_checkpoint: GitRepositoryCheckpoint,
1477 ) -> BoxFuture<Result<String>> {1521 ) -> BoxFuture<'_, Result<String>> {
1478 let working_directory = self.working_directory();1522 let working_directory = self.working_directory();
1479 let git_binary_path = self.git_binary_path.clone();1523 let git_binary_path = self.git_binary_path.clone();
1480 1524
crates/git_ui/Cargo.toml+1−0
@@ -21,6 +21,7 @@ agent_settings.workspace = true · 20 unmodified lines
21anyhow.workspace = true21anyhow.workspace = true
22askpass.workspace = true22askpass.workspace = true
23buffer_diff.workspace = true23buffer_diff.workspace = true
24call.workspace = true
24chrono.workspace = true25chrono.workspace = true
25collections.workspace = true26collections.workspace = true
26command_palette_hooks.workspace = true27command_palette_hooks.workspace = true
crates/git_ui/src/commit_modal.rs+1−0
@@ -148,6 +148,7 @@ impl CommitModal { · 147 unmodified lines
148 }148 }
149 }149 }
150 git_panel.set_modal_open(true, cx);150 git_panel.set_modal_open(true, cx);
151 git_panel.load_local_committer(cx);
151 });152 });
152 153
153 let dock = workspace.dock_at_position(git_panel.position(window, cx));154 let dock = workspace.dock_at_position(git_panel.position(window, cx));
crates/git_ui/src/conflict_view.rs+21−14
@@ -248,6 +248,8 @@ fn conflicts_updated( · 247 unmodified lines
248 removed_block_ids.insert(block_id);248 removed_block_ids.insert(block_id);
249 }249 }
250 250
251 editor.remove_gutter_highlights::<ConflictsOuter>(removed_highlighted_ranges.clone(), cx);
252
251 editor.remove_highlighted_rows::<ConflictsOuter>(removed_highlighted_ranges.clone(), cx);253 editor.remove_highlighted_rows::<ConflictsOuter>(removed_highlighted_ranges.clone(), cx);
252 editor.remove_highlighted_rows::<ConflictsOurs>(removed_highlighted_ranges.clone(), cx);254 editor.remove_highlighted_rows::<ConflictsOurs>(removed_highlighted_ranges.clone(), cx);
253 editor255 editor
@@ -325,8 +327,7 @@ fn update_conflict_highlighting( · 71 unmodified lines
325 cx: &mut Context<Editor>,327 cx: &mut Context<Editor>,
326) {328) {
327 log::debug!("update conflict highlighting for {conflict:?}");329 log::debug!("update conflict highlighting for {conflict:?}");
328 let theme = cx.theme().clone();330
329 let colors = theme.colors();
330 let outer_start = buffer331 let outer_start = buffer
331 .anchor_in_excerpt(excerpt_id, conflict.range.start)332 .anchor_in_excerpt(excerpt_id, conflict.range.start)
332 .unwrap();333 .unwrap();
@@ -346,26 +347,29 @@ fn update_conflict_highlighting( · 13 unmodified lines
346 .anchor_in_excerpt(excerpt_id, conflict.theirs.end)347 .anchor_in_excerpt(excerpt_id, conflict.theirs.end)
347 .unwrap();348 .unwrap();
348 349
349 let ours_background = colors.version_control_conflict_ours_background;350 let ours_background = cx.theme().colors().version_control_conflict_marker_ours;
350 let ours_marker = colors.version_control_conflict_ours_marker_background;351 let theirs_background = cx.theme().colors().version_control_conflict_marker_theirs;
351 let theirs_background = colors.version_control_conflict_theirs_background;
352 let theirs_marker = colors.version_control_conflict_theirs_marker_background;
353 let divider_background = colors.version_control_conflict_divider_background;
354 352
355 let options = RowHighlightOptions {353 let options = RowHighlightOptions {
356 include_gutter: false,354 include_gutter: true,
357 ..Default::default()355 ..Default::default()
358 };356 };
359 357
358 editor.insert_gutter_highlight::<ConflictsOuter>(
359 outer_start..their_end,
360 |cx| cx.theme().colors().editor_background,
361 cx,
362 );
363
360 // Prevent diff hunk highlighting within the entire conflict region.364 // Prevent diff hunk highlighting within the entire conflict region.
361 editor.highlight_rows::<ConflictsOuter>(365 editor.highlight_rows::<ConflictsOuter>(outer_start..outer_end, theirs_background, options, cx);
362 outer_start..outer_end,366 editor.highlight_rows::<ConflictsOurs>(our_start..our_end, ours_background, options, cx);
363 divider_background,367 editor.highlight_rows::<ConflictsOursMarker>(
368 outer_start..our_start,
369 ours_background,
364 options,370 options,
365 cx,371 cx,
366 );372 );
367 editor.highlight_rows::<ConflictsOurs>(our_start..our_end, ours_background, options, cx);
368 editor.highlight_rows::<ConflictsOursMarker>(outer_start..our_start, ours_marker, options, cx);
369 editor.highlight_rows::<ConflictsTheirs>(373 editor.highlight_rows::<ConflictsTheirs>(
370 their_start..their_end,374 their_start..their_end,
371 theirs_background,375 theirs_background,
@@ -374,7 +378,7 @@ fn update_conflict_highlighting( · 2 unmodified lines
374 );378 );
375 editor.highlight_rows::<ConflictsTheirsMarker>(379 editor.highlight_rows::<ConflictsTheirsMarker>(
376 their_end..outer_end,380 their_end..outer_end,
377 theirs_marker,381 theirs_background,
378 options,382 options,
379 cx,383 cx,
380 );384 );
@@ -512,6 +516,9 @@ pub(crate) fn resolve_conflict( · 131 unmodified lines
512 let end = snapshot516 let end = snapshot
513 .anchor_in_excerpt(excerpt_id, resolved_conflict.range.end)517 .anchor_in_excerpt(excerpt_id, resolved_conflict.range.end)
514 .unwrap();518 .unwrap();
519
520 editor.remove_gutter_highlights::<ConflictsOuter>(vec![start..end], cx);
521
515 editor.remove_highlighted_rows::<ConflictsOuter>(vec![start..end], cx);522 editor.remove_highlighted_rows::<ConflictsOuter>(vec![start..end], cx);
516 editor.remove_highlighted_rows::<ConflictsOurs>(vec![start..end], cx);523 editor.remove_highlighted_rows::<ConflictsOurs>(vec![start..end], cx);
517 editor.remove_highlighted_rows::<ConflictsTheirs>(vec![start..end], cx);524 editor.remove_highlighted_rows::<ConflictsTheirs>(vec![start..end], cx);
crates/git_ui/src/git_panel.rs+285−184
@@ -20,18 +20,20 @@ use editor::{ · 19 unmodified lines
20use futures::StreamExt as _;20use futures::StreamExt as _;
21use git::blame::ParsedCommitMessage;21use git::blame::ParsedCommitMessage;
22use git::repository::{22use git::repository::{
23 Branch, CommitDetails, CommitOptions, CommitSummary, DiffType, FetchOptions, PushOptions,23 Branch, CommitDetails, CommitOptions, CommitSummary, DiffType, FetchOptions, GitCommitter,
24 Remote, RemoteCommandOutput, ResetMode, Upstream, UpstreamTracking, UpstreamTrackingStatus,24 PushOptions, Remote, RemoteCommandOutput, ResetMode, Upstream, UpstreamTracking,
25 UpstreamTrackingStatus, get_git_committer,
25};26};
26use git::status::StageStatus;27use git::status::StageStatus;
27use git::{Amend, ToggleStaged, repository::RepoPath, status::FileStatus};28use git::{Amend, ToggleStaged, repository::RepoPath, status::FileStatus};
28use git::{ExpandCommitEditor, RestoreTrackedFiles, StageAll, TrashUntrackedFiles, UnstageAll};29use git::{ExpandCommitEditor, RestoreTrackedFiles, StageAll, TrashUntrackedFiles, UnstageAll};
29use gpui::{30use gpui::{
30 Action, Animation, AnimationExt as _, Axis, ClickEvent, Corner, DismissEvent, Entity,31 Action, Animation, AnimationExt as _, AsyncApp, AsyncWindowContext, Axis, ClickEvent, Corner,
31 EventEmitter, FocusHandle, Focusable, KeyContext, ListHorizontalSizingBehavior,32 DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, KeyContext,
32 ListSizingBehavior, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, Point,33 ListHorizontalSizingBehavior, ListSizingBehavior, Modifiers, ModifiersChangedEvent,
33 PromptLevel, ScrollStrategy, Subscription, Task, Transformation, UniformListScrollHandle,34 MouseButton, MouseDownEvent, Point, PromptLevel, ScrollStrategy, Subscription, Task,
34 WeakEntity, actions, anchored, deferred, percentage, uniform_list,35 Transformation, UniformListScrollHandle, WeakEntity, actions, anchored, deferred, percentage,
36 uniform_list,
35};37};
36use itertools::Itertools;38use itertools::Itertools;
37use language::{Buffer, File};39use language::{Buffer, File};
@@ -54,6 +56,7 @@ use project::{ · 16 unmodified lines
54use serde::{Deserialize, Serialize};56use serde::{Deserialize, Serialize};
55use settings::{Settings as _, SettingsStore};57use settings::{Settings as _, SettingsStore};
56use std::future::Future;58use std::future::Future;
59use std::ops::Range;
57use std::path::{Path, PathBuf};60use std::path::{Path, PathBuf};
58use std::{collections::HashSet, sync::Arc, time::Duration, usize};61use std::{collections::HashSet, sync::Arc, time::Duration, usize};
59use strum::{IntoEnumIterator, VariantNames};62use strum::{IntoEnumIterator, VariantNames};
@@ -63,11 +66,11 @@ use ui::{ · 3 unmodified lines
63 Tooltip, prelude::*,66 Tooltip, prelude::*,
64};67};
65use util::{ResultExt, TryFutureExt, maybe};68use util::{ResultExt, TryFutureExt, maybe};
66use workspace::AppState;69
67use workspace::{70use workspace::{
68 Workspace,71 Workspace,
69 dock::{DockPosition, Panel, PanelEvent},72 dock::{DockPosition, Panel, PanelEvent},
70 notifications::DetachAndPromptErr,73 notifications::{DetachAndPromptErr, ErrorMessagePrompt, NotificationId},
71};74};
72use zed_llm_client::CompletionIntent;75use zed_llm_client::CompletionIntent;
73 76
@@ -356,6 +359,8 @@ pub struct GitPanel { · 282 unmodified lines
356 context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,359 context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
357 modal_open: bool,360 modal_open: bool,
358 show_placeholders: bool,361 show_placeholders: bool,
362 local_committer: Option<GitCommitter>,
363 local_committer_task: Option<Task<()>>,
359 _settings_subscription: Subscription,364 _settings_subscription: Subscription,
360}365}
361 366
@@ -372,7 +377,10 @@ pub(crate) fn commit_message_editor( · 10 unmodified lines
372 let buffer = cx.new(|cx| MultiBuffer::singleton(commit_message_buffer, cx));377 let buffer = cx.new(|cx| MultiBuffer::singleton(commit_message_buffer, cx));
373 let max_lines = if in_panel { MAX_PANEL_EDITOR_LINES } else { 18 };378 let max_lines = if in_panel { MAX_PANEL_EDITOR_LINES } else { 18 };
374 let mut commit_editor = Editor::new(379 let mut commit_editor = Editor::new(
375 EditorMode::AutoHeight { max_lines },380 EditorMode::AutoHeight {
381 min_lines: 1,
382 max_lines,
383 },
376 buffer,384 buffer,
377 None,385 None,
378 window,386 window,
@@ -389,144 +397,150 @@ pub(crate) fn commit_message_editor( · 10 unmodified lines
389}397}
390 398
391impl GitPanel {399impl GitPanel {
392 pub fn new(400 fn new(
393 workspace: Entity<Workspace>,401 workspace: &mut Workspace,
394 project: Entity<Project>,
395 app_state: Arc<AppState>,
396 window: &mut Window,402 window: &mut Window,
397 cx: &mut Context<Self>,403 cx: &mut Context<Workspace>,
398 ) -> Self {404 ) -> Entity<Self> {
405 let project = workspace.project().clone();
406 let app_state = workspace.app_state().clone();
399 let fs = app_state.fs.clone();407 let fs = app_state.fs.clone();
400 let git_store = project.read(cx).git_store().clone();408 let git_store = project.read(cx).git_store().clone();
401 let active_repository = project.read(cx).active_repository(cx);409 let active_repository = project.read(cx).active_repository(cx);
402 let workspace = workspace.downgrade();
403 410
404 let focus_handle = cx.focus_handle();411 let git_panel = cx.new(|cx| {
405 cx.on_focus(&focus_handle, window, Self::focus_in).detach();412 let focus_handle = cx.focus_handle();
406 cx.on_focus_out(&focus_handle, window, |this, _, window, cx| {413 cx.on_focus(&focus_handle, window, Self::focus_in).detach();
407 this.hide_scrollbars(window, cx);414 cx.on_focus_out(&focus_handle, window, |this, _, window, cx| {
408 })415 this.hide_scrollbars(window, cx);
409 .detach();416 })
417 .detach();
410 418
411 let mut was_sort_by_path = GitPanelSettings::get_global(cx).sort_by_path;419 let mut was_sort_by_path = GitPanelSettings::get_global(cx).sort_by_path;
412 cx.observe_global::<SettingsStore>(move |this, cx| {420 cx.observe_global::<SettingsStore>(move |this, cx| {
413 let is_sort_by_path = GitPanelSettings::get_global(cx).sort_by_path;421 let is_sort_by_path = GitPanelSettings::get_global(cx).sort_by_path;
414 if is_sort_by_path != was_sort_by_path {422 if is_sort_by_path != was_sort_by_path {
415 this.update_visible_entries(cx);423 this.update_visible_entries(cx);
416 }424 }
417 was_sort_by_path = is_sort_by_path425 was_sort_by_path = is_sort_by_path
418 })426 })
419 .detach();427 .detach();
420 428
421 // just to let us render a placeholder editor.429 // just to let us render a placeholder editor.
422 // Once the active git repo is set, this buffer will be replaced.430 // Once the active git repo is set, this buffer will be replaced.
423 let temporary_buffer = cx.new(|cx| Buffer::local("", cx));431 let temporary_buffer = cx.new(|cx| Buffer::local("", cx));
424 let commit_editor = cx.new(|cx| {432 let commit_editor = cx.new(|cx| {
425 commit_message_editor(temporary_buffer, None, project.clone(), true, window, cx)433 commit_message_editor(temporary_buffer, None, project.clone(), true, window, cx)
426 });434 });
427 435
428 commit_editor.update(cx, |editor, cx| {436 commit_editor.update(cx, |editor, cx| {
429 editor.clear(window, cx);437 editor.clear(window, cx);
430 });438 });
431 439
432 let scroll_handle = UniformListScrollHandle::new();440 let scroll_handle = UniformListScrollHandle::new();
433 441
434 cx.subscribe_in(442 let vertical_scrollbar = ScrollbarProperties {
435 &git_store,443 axis: Axis::Vertical,
436 window,444 state: ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity()),
437 move |this, git_store, event, window, cx| match event {445 show_scrollbar: false,
438 GitStoreEvent::ActiveRepositoryChanged(_) => {446 show_track: false,
439 this.active_repository = git_store.read(cx).active_repository();447 auto_hide: false,
440 this.schedule_update(true, window, cx);448 hide_task: None,
441 }449 };
442 GitStoreEvent::RepositoryUpdated(
443 _,
444 RepositoryEvent::Updated { full_scan },
445 true,
446 ) => {
447 this.schedule_update(*full_scan, window, cx);
448 }
449 450
450 GitStoreEvent::RepositoryAdded(_) | GitStoreEvent::RepositoryRemoved(_) => {451 let horizontal_scrollbar = ScrollbarProperties {
451 this.schedule_update(false, window, cx);452 axis: Axis::Horizontal,
452 }453 state: ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity()),
453 GitStoreEvent::IndexWriteError(error) => {454 show_scrollbar: false,
454 this.workspace455 show_track: false,
455 .update(cx, |workspace, cx| {456 auto_hide: false,
456 workspace.show_error(error, cx);457 hide_task: None,
457 })458 };
458 .ok();459
460 let mut assistant_enabled = AgentSettings::get_global(cx).enabled;
461 let _settings_subscription = cx.observe_global::<SettingsStore>(move |_, cx| {
462 if assistant_enabled != AgentSettings::get_global(cx).enabled {
463 assistant_enabled = AgentSettings::get_global(cx).enabled;
464 cx.notify();
459 }465 }
460 GitStoreEvent::RepositoryUpdated(_, _, _) => {}466 });
461 GitStoreEvent::JobsUpdated | GitStoreEvent::ConflictsUpdated => {}
462 },
463 )
464 .detach();
465 467
466 let vertical_scrollbar = ScrollbarProperties {468 cx.subscribe_in(
467 axis: Axis::Vertical,469 &git_store,
468 state: ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity()),470 window,
469 show_scrollbar: false,471 move |this, _git_store, event, window, cx| match event {
470 show_track: false,472 GitStoreEvent::ActiveRepositoryChanged(_) => {
471 auto_hide: false,473 this.active_repository = this.project.read(cx).active_repository(cx);
472 hide_task: None,474 this.schedule_update(true, window, cx);
473 };475 }
476 GitStoreEvent::RepositoryUpdated(
477 _,
478 RepositoryEvent::Updated { full_scan },
479 true,
480 ) => {
481 this.schedule_update(*full_scan, window, cx);
482 }
474 483
475 let horizontal_scrollbar = ScrollbarProperties {484 GitStoreEvent::RepositoryAdded(_) | GitStoreEvent::RepositoryRemoved(_) => {
476 axis: Axis::Horizontal,485 this.schedule_update(false, window, cx);
477 state: ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity()),486 }
478 show_scrollbar: false,487 GitStoreEvent::IndexWriteError(error) => {
479 show_track: false,488 this.workspace
480 auto_hide: false,489 .update(cx, |workspace, cx| {
481 hide_task: None,490 workspace.show_error(error, cx);
482 };491 })
492 .ok();
493 }
494 GitStoreEvent::RepositoryUpdated(_, _, _) => {}
495 GitStoreEvent::JobsUpdated | GitStoreEvent::ConflictsUpdated => {}
496 },
497 )
498 .detach();
483 499
484 let mut assistant_enabled = AgentSettings::get_global(cx).enabled;500 let mut this = Self {
485 let _settings_subscription = cx.observe_global::<SettingsStore>(move |_, cx| {501 active_repository,
486 if assistant_enabled != AgentSettings::get_global(cx).enabled {502 commit_editor,
487 assistant_enabled = AgentSettings::get_global(cx).enabled;503 conflicted_count: 0,
488 cx.notify();504 conflicted_staged_count: 0,
489 }505 current_modifiers: window.modifiers(),
506 add_coauthors: true,
507 generate_commit_message_task: None,
508 entries: Vec::new(),
509 focus_handle: cx.focus_handle(),
510 fs,
511 new_count: 0,
512 new_staged_count: 0,
513 pending: Vec::new(),
514 pending_commit: None,
515 amend_pending: false,
516 pending_serialization: Task::ready(None),
517 single_staged_entry: None,
518 single_tracked_entry: None,
519 project,
520 scroll_handle,
521 max_width_item_index: None,
522 selected_entry: None,
523 marked_entries: Vec::new(),
524 tracked_count: 0,
525 tracked_staged_count: 0,
526 update_visible_entries_task: Task::ready(()),
527 width: None,
528 show_placeholders: false,
529 local_committer: None,
530 local_committer_task: None,
531 context_menu: None,
532 workspace: workspace.weak_handle(),
533 modal_open: false,
534 entry_count: 0,
535 horizontal_scrollbar,
536 vertical_scrollbar,
537 _settings_subscription,
538 };
539
540 this.schedule_update(false, window, cx);
541 this
490 });542 });
491 543
492 let mut git_panel = Self {
493 active_repository,
494 commit_editor,
495 conflicted_count: 0,
496 conflicted_staged_count: 0,
497 current_modifiers: window.modifiers(),
498 add_coauthors: true,
499 generate_commit_message_task: None,
500 entries: Vec::new(),
501 focus_handle: cx.focus_handle(),
502 fs,
503 new_count: 0,
504 new_staged_count: 0,
505 pending: Vec::new(),
506 pending_commit: None,
507 amend_pending: false,
508 pending_serialization: Task::ready(None),
509 single_staged_entry: None,
510 single_tracked_entry: None,
511 project,
512 scroll_handle,
513 max_width_item_index: None,
514 selected_entry: None,
515 marked_entries: Vec::new(),
516 tracked_count: 0,
517 tracked_staged_count: 0,
518 update_visible_entries_task: Task::ready(()),
519 width: None,
520 show_placeholders: false,
521 context_menu: None,
522 workspace,
523 modal_open: false,
524 entry_count: 0,
525 horizontal_scrollbar,
526 vertical_scrollbar,
527 _settings_subscription,
528 };
529 git_panel.schedule_update(false, window, cx);
530 git_panel544 git_panel
531 }545 }
532 546
@@ -1774,7 +1788,19 @@ impl GitPanel { · 1241 unmodified lines
1774 this.generate_commit_message_task.take();1788 this.generate_commit_message_task.take();
1775 });1789 });
1776 1790
1777 let mut diff_text = diff.await??;1791 let mut diff_text = match diff.await {
1792 Ok(result) => match result {
1793 Ok(text) => text,
1794 Err(e) => {
1795 Self::show_commit_message_error(&this, &e, cx);
1796 return anyhow::Ok(());
1797 }
1798 },
1799 Err(e) => {
1800 Self::show_commit_message_error(&this, &e, cx);
1801 return anyhow::Ok(());
1802 }
1803 };
1778 1804
1779 const ONE_MB: usize = 1_000_000;1805 const ONE_MB: usize = 1_000_000;
1780 if diff_text.len() > ONE_MB {1806 if diff_text.len() > ONE_MB {
@@ -1812,26 +1838,37 @@ impl GitPanel { · 31 unmodified lines
1812 };1838 };
1813 1839
1814 let stream = model.stream_completion_text(request, &cx);1840 let stream = model.stream_completion_text(request, &cx);
1815 let mut messages = stream.await?;1841 match stream.await {
1816 1842 Ok(mut messages) => {
1817 if !text_empty {1843 if !text_empty {
1818 this.update(cx, |this, cx| {1844 this.update(cx, |this, cx| {
1819 this.commit_message_buffer(cx).update(cx, |buffer, cx| {1845 this.commit_message_buffer(cx).update(cx, |buffer, cx| {
1820 let insert_position = buffer.anchor_before(buffer.len());1846 let insert_position = buffer.anchor_before(buffer.len());
1821 buffer.edit([(insert_position..insert_position, "\n")], None, cx)1847 buffer.edit([(insert_position..insert_position, "\n")], None, cx)
1822 });1848 });
1823 })?;1849 })?;
1824 }1850 }
1825
1826 while let Some(message) = messages.stream.next().await {
1827 let text = message?;
1828 1851
1829 this.update(cx, |this, cx| {1852 while let Some(message) = messages.stream.next().await {
1830 this.commit_message_buffer(cx).update(cx, |buffer, cx| {1853 match message {
1831 let insert_position = buffer.anchor_before(buffer.len());1854 Ok(text) => {
1832 buffer.edit([(insert_position..insert_position, text)], None, cx);1855 this.update(cx, |this, cx| {
1833 });1856 this.commit_message_buffer(cx).update(cx, |buffer, cx| {
1834 })?;1857 let insert_position = buffer.anchor_before(buffer.len());
1858 buffer.edit([(insert_position..insert_position, text)], None, cx);
1859 });
1860 })?;
1861 }
1862 Err(e) => {
1863 Self::show_commit_message_error(&this, &e, cx);
1864 break;
1865 }
1866 }
1867 }
1868 }
1869 Err(e) => {
1870 Self::show_commit_message_error(&this, &e, cx);
1871 }
1835 }1872 }
1836 1873
1837 anyhow::Ok(())1874 anyhow::Ok(())
@@ -2221,6 +2258,19 @@ impl GitPanel { · 383 unmodified lines
2221 }2258 }
2222 }2259 }
2223 2260
2261 pub fn load_local_committer(&mut self, cx: &Context<Self>) {
2262 if self.local_committer_task.is_none() {
2263 self.local_committer_task = Some(cx.spawn(async move |this, cx| {
2264 let committer = get_git_committer(cx).await;
2265 this.update(cx, |this, cx| {
2266 this.local_committer = Some(committer);
2267 cx.notify()
2268 })
2269 .ok();
2270 }));
2271 }
2272 }
2273
2224 fn potential_co_authors(&self, cx: &App) -> Vec<(String, String)> {2274 fn potential_co_authors(&self, cx: &App) -> Vec<(String, String)> {
2225 let mut new_co_authors = Vec::new();2275 let mut new_co_authors = Vec::new();
2226 let project = self.project.read(cx);2276 let project = self.project.read(cx);
@@ -2243,34 +2293,38 @@ impl GitPanel { · 16 unmodified lines
2243 let Some(participant) = room.remote_participant_for_peer_id(*peer_id) else {2293 let Some(participant) = room.remote_participant_for_peer_id(*peer_id) else {
2244 continue;2294 continue;
2245 };2295 };
2246 if participant.can_write() && participant.user.email.is_some() {2296 if !participant.can_write() {
2247 let email = participant.user.email.clone().unwrap();2297 continue;
2248 2298 }
2249 new_co_authors.push((2299 if let Some(email) = &collaborator.committer_email {
2250 participant2300 let name = collaborator
2251 .user2301 .committer_name
2252 .name2302 .clone()
2253 .clone()2303 .or_else(|| participant.user.name.clone())
2254 .unwrap_or_else(|| participant.user.github_login.clone()),2304 .unwrap_or_else(|| participant.user.github_login.clone());
2255 email,2305 new_co_authors.push((name.clone(), email.clone()))
2256 ))
2257 }2306 }
2258 }2307 }
2259 if !project.is_local() && !project.is_read_only(cx) {2308 if !project.is_local() && !project.is_read_only(cx) {
2260 if let Some(user) = room.local_participant_user(cx) {2309 if let Some(local_committer) = self.local_committer(room, cx) {
2261 if let Some(email) = user.email.clone() {2310 new_co_authors.push(local_committer);
2262 new_co_authors.push((
2263 user.name
2264 .clone()
2265 .unwrap_or_else(|| user.github_login.clone()),
2266 email.clone(),
2267 ))
2268 }
2269 }2311 }
2270 }2312 }
2271 new_co_authors2313 new_co_authors
2272 }2314 }
2273 2315
2316 fn local_committer(&self, room: &call::Room, cx: &App) -> Option<(String, String)> {
2317 let user = room.local_participant_user(cx)?;
2318 let committer = self.local_committer.as_ref()?;
2319 let email = committer.email.clone()?;
2320 let name = committer
2321 .name
2322 .clone()
2323 .or_else(|| user.name.clone())
2324 .unwrap_or_else(|| user.github_login.clone());
2325 Some((name, email))
2326 }
2327
2274 fn toggle_fill_co_authors(2328 fn toggle_fill_co_authors(
2275 &mut self,2329 &mut self,
2276 _: &ToggleFillCoAuthors,2330 _: &ToggleFillCoAuthors,
@@ -2689,6 +2743,26 @@ impl GitPanel { · 412 unmodified lines
2689 }2743 }
2690 }2744 }
2691 2745
2746 fn show_commit_message_error<E>(weak_this: &WeakEntity<Self>, err: &E, cx: &mut AsyncApp)
2747 where
2748 E: std::fmt::Debug + std::fmt::Display,
2749 {
2750 if let Ok(Some(workspace)) = weak_this.update(cx, |this, _cx| this.workspace.upgrade()) {
2751 let _ = workspace.update(cx, |workspace, cx| {
2752 struct CommitMessageError;
2753 let notification_id = NotificationId::unique::<CommitMessageError>();
2754 workspace.show_notification(notification_id, cx, |cx| {
2755 cx.new(|cx| {
2756 ErrorMessagePrompt::new(
2757 format!("Failed to generate commit message: {err}"),
2758 cx,
2759 )
2760 })
2761 });
2762 });
2763 }
2764 }
2765
2692 fn show_remote_output(&self, action: RemoteAction, info: RemoteCommandOutput, cx: &mut App) {2766 fn show_remote_output(&self, action: RemoteAction, info: RemoteCommandOutput, cx: &mut App) {
2693 let Some(workspace) = self.workspace.upgrade() else {2767 let Some(workspace) = self.workspace.upgrade() else {
2694 return;2768 return;
@@ -3662,8 +3736,10 @@ impl GitPanel { · 967 unmodified lines
3662 .relative()3736 .relative()
3663 .overflow_hidden()3737 .overflow_hidden()
3664 .child(3738 .child(
3665 uniform_list(cx.entity().clone(), "entries", entry_count, {3739 uniform_list(
3666 move |this, range, window, cx| {3740 "entries",
3741 entry_count,
3742 cx.processor(move |this, range: Range<usize>, window, cx| {
3667 let mut items = Vec::with_capacity(range.end - range.start);3743 let mut items = Vec::with_capacity(range.end - range.start);
3668 3744
3669 for ix in range {3745 for ix in range {
@@ -3691,8 +3767,8 @@ impl GitPanel { · 21 unmodified lines
3691 }3767 }
3692 3768
3693 items3769 items
3694 }3770 }),
3695 })3771 )
3696 .when(3772 .when(
3697 !self.horizontal_scrollbar.show_track3773 !self.horizontal_scrollbar.show_track
3698 && self.horizontal_scrollbar.show_scrollbar,3774 && self.horizontal_scrollbar.show_scrollbar,
@@ -4141,6 +4217,32 @@ impl GitPanel { · 442 unmodified lines
4141 self.amend_pending = value;4217 self.amend_pending = value;
4142 cx.notify();4218 cx.notify();
4143 }4219 }
4220
4221 pub async fn load(
4222 workspace: WeakEntity<Workspace>,
4223 mut cx: AsyncWindowContext,
4224 ) -> anyhow::Result<Entity<Self>> {
4225 let serialized_panel = cx
4226 .background_spawn(async move { KEY_VALUE_STORE.read_kvp(&GIT_PANEL_KEY) })
4227 .await
4228 .context("loading git panel")
4229 .log_err()
4230 .flatten()
4231 .and_then(|panel| serde_json::from_str::<SerializedGitPanel>(&panel).log_err());
4232
4233 workspace.update_in(&mut cx, |workspace, window, cx| {
4234 let panel = GitPanel::new(workspace, window, cx);
4235
4236 if let Some(serialized_panel) = serialized_panel {
4237 panel.update(cx, |panel, cx| {
4238 panel.width = serialized_panel.width;
4239 cx.notify();
4240 })
4241 }
4242
4243 panel
4244 })
4245 }
4144}4246}
4145 4247
4146fn current_language_model(cx: &Context<'_, GitPanel>) -> Option<Arc<dyn LanguageModel>> {4248fn current_language_model(cx: &Context<'_, GitPanel>) -> Option<Arc<dyn LanguageModel>> {
@@ -4167,8 +4269,9 @@ impl Render for GitPanel { · 20 unmodified lines
4167 let has_write_access = self.has_write_access(cx);4269 let has_write_access = self.has_write_access(cx);
4168 4270
4169 let has_co_authors = room.map_or(false, |room| {4271 let has_co_authors = room.map_or(false, |room| {
4170 room.read(cx)4272 self.load_local_committer(cx);
4171 .remote_participants()4273 let room = room.read(cx);
4274 room.remote_participants()
4172 .values()4275 .values()
4173 .any(|remote_participant| remote_participant.can_write())4276 .any(|remote_participant| remote_participant.can_write())
4174 });4277 });
@@ -4852,7 +4955,7 @@ impl Component for PanelRepoFooter { · 677 unmodified lines
4852#[cfg(test)]4955#[cfg(test)]
4853mod tests {4956mod tests {
4854 use git::status::StatusCode;4957 use git::status::StatusCode;
4855 use gpui::TestAppContext;4958 use gpui::{TestAppContext, VisualTestContext};
4856 use project::{FakeFs, WorktreeSettings};4959 use project::{FakeFs, WorktreeSettings};
4857 use serde_json::json;4960 use serde_json::json;
4858 use settings::SettingsStore;4961 use settings::SettingsStore;
@@ -4916,8 +5019,9 @@ mod tests { · 57 unmodified lines
4916 5019
4917 let project =5020 let project =
4918 Project::test(fs.clone(), [path!("/root/zed/crates/gpui").as_ref()], cx).await;5021 Project::test(fs.clone(), [path!("/root/zed/crates/gpui").as_ref()], cx).await;
4919 let (workspace, cx) =5022 let workspace =
4920 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));5023 cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
5024 let cx = &mut VisualTestContext::from_window(*workspace, cx);
4921 5025
4922 cx.read(|cx| {5026 cx.read(|cx| {
4923 project5027 project
@@ -4934,10 +5038,7 @@ mod tests { · 10 unmodified lines
4934 5038
4935 cx.executor().run_until_parked();5039 cx.executor().run_until_parked();
4936 5040
4937 let app_state = workspace.read_with(cx, |workspace, _| workspace.app_state().clone());5041 let panel = workspace.update(cx, GitPanel::new).unwrap();
4938 let panel = cx.new_window_entity(|window, cx| {
4939 GitPanel::new(workspace.clone(), project.clone(), app_state, window, cx)
4940 });
4941 5042
4942 let handle = cx.update_window_entity(&panel, |panel, _, _| {5043 let handle = cx.update_window_entity(&panel, |panel, _, _| {
4943 std::mem::replace(&mut panel.update_visible_entries_task, Task::ready(()))5044 std::mem::replace(&mut panel.update_visible_entries_task, Task::ready(()))
crates/git_ui/src/git_ui.rs+32−1
@@ -10,7 +10,7 @@ use git::{ · 9 unmodified lines
10 status::{FileStatus, StatusCode, UnmergedStatus, UnmergedStatusCode},10 status::{FileStatus, StatusCode, UnmergedStatus, UnmergedStatusCode},
11};11};
12use git_panel_settings::GitPanelSettings;12use git_panel_settings::GitPanelSettings;
13use gpui::{Action, App, FocusHandle, actions};13use gpui::{Action, App, Context, FocusHandle, Window, actions};
14use onboarding::GitOnboardingModal;14use onboarding::GitOnboardingModal;
15use project_diff::ProjectDiff;15use project_diff::ProjectDiff;
16use ui::prelude::*;16use ui::prelude::*;
@@ -142,10 +142,41 @@ pub fn init(cx: &mut App) { · 125 unmodified lines
142 panel.git_init(window, cx);142 panel.git_init(window, cx);
143 });143 });
144 });144 });
145 workspace.register_action(|workspace, _: &git::OpenModifiedFiles, window, cx| {
146 open_modified_files(workspace, window, cx);
147 });
145 })148 })
146 .detach();149 .detach();
147}150}
148 151
152fn open_modified_files(
153 workspace: &mut Workspace,
154 window: &mut Window,
155 cx: &mut Context<Workspace>,
156) {
157 let Some(panel) = workspace.panel::<git_panel::GitPanel>(cx) else {
158 return;
159 };
160 let modified_paths: Vec<_> = panel.update(cx, |panel, cx| {
161 let Some(repo) = panel.active_repository.as_ref() else {
162 return Vec::new();
163 };
164 let repo = repo.read(cx);
165 repo.cached_status()
166 .filter_map(|entry| {
167 if entry.status.is_modified() {
168 repo.repo_path_to_project_path(&entry.repo_path, cx)
169 } else {
170 None
171 }
172 })
173 .collect()
174 });
175 for path in modified_paths {
176 workspace.open_path(path, None, true, window, cx).detach();
177 }
178}
179
149pub fn git_status_icon(status: FileStatus) -> impl IntoElement {180pub fn git_status_icon(status: FileStatus) -> impl IntoElement {
150 GitStatusIcon::new(status)181 GitStatusIcon::new(status)
151}182}
crates/git_ui/src/project_diff.rs+12−4
@@ -37,7 +37,7 @@ use util::ResultExt as _; · 36 unmodified lines
37use workspace::{37use workspace::{
38 CloseActiveItem, ItemNavHistory, SerializableItem, ToolbarItemEvent, ToolbarItemLocation,38 CloseActiveItem, ItemNavHistory, SerializableItem, ToolbarItemEvent, ToolbarItemLocation,
39 ToolbarItemView, Workspace,39 ToolbarItemView, Workspace,
40 item::{BreadcrumbText, Item, ItemEvent, ItemHandle, TabContentParams},40 item::{BreadcrumbText, Item, ItemEvent, ItemHandle, SaveOptions, TabContentParams},
41 searchable::SearchableItemHandle,41 searchable::SearchableItemHandle,
42};42};
43 43
@@ -632,12 +632,12 @@ impl Item for ProjectDiff { · 588 unmodified lines
632 632
633 fn save(633 fn save(
634 &mut self,634 &mut self,
635 format: bool,635 options: SaveOptions,
636 project: Entity<Project>,636 project: Entity<Project>,
637 window: &mut Window,637 window: &mut Window,
638 cx: &mut Context<Self>,638 cx: &mut Context<Self>,
639 ) -> Task<Result<()>> {639 ) -> Task<Result<()>> {
640 self.editor.save(format, project, window, cx)640 self.editor.save(options, project, window, cx)
641 }641 }
642 642
643 fn save_as(643 fn save_as(
@@ -1565,7 +1565,15 @@ mod tests { · 921 unmodified lines
1565 1565
1566 cx.update_window_entity(&buffer_editor, |buffer_editor, window, cx| {1566 cx.update_window_entity(&buffer_editor, |buffer_editor, window, cx| {
1567 buffer_editor.set_text("different\n", window, cx);1567 buffer_editor.set_text("different\n", window, cx);
1568 buffer_editor.save(false, project.clone(), window, cx)1568 buffer_editor.save(
1569 SaveOptions {
1570 format: false,
1571 autosave: false,
1572 },
1573 project.clone(),
1574 window,
1575 cx,
1576 )
1569 })1577 })
1570 .await1578 .await
1571 .unwrap();1579 .unwrap();
crates/gpui/examples/data_table.rs+10−15
@@ -1,8 +1,4 @@
1use std::{1use std::{ops::Range, rc::Rc, time::Duration};
2 ops::Range,
3 rc::Rc,
4 time::{Duration, Instant},
5};
6 2
7use gpui::{3use gpui::{
8 App, Application, Bounds, Context, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point,4 App, Application, Bounds, Context, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point,
@@ -22,7 +18,7 @@ pub struct Quote { · 13 unmodified lines
22 open: f64,18 open: f64,
23 high: f64,19 high: f64,
24 low: f64,20 low: f64,
25 timestamp: Instant,21 timestamp: Duration,
26 volume: i64,22 volume: i64,
27 turnover: f64,23 turnover: f64,
28 ttm: f64,24 ttm: f64,
@@ -50,8 +46,7 @@ impl Quote { · 21 unmodified lines
50 let open = prev_close + rng.gen_range(-3.0..3.0);46 let open = prev_close + rng.gen_range(-3.0..3.0);
51 let high = (prev_close + rng.gen_range::<f64, _>(0.0..10.0)).max(open);47 let high = (prev_close + rng.gen_range::<f64, _>(0.0..10.0)).max(open);
52 let low = (prev_close - rng.gen_range::<f64, _>(0.0..10.0)).min(open);48 let low = (prev_close - rng.gen_range::<f64, _>(0.0..10.0)).min(open);
53 // Randomize the timestamp in the past 24 hours49 let timestamp = Duration::from_secs(rng.gen_range(0..86400));
54 let timestamp = Instant::now() - Duration::from_secs(rng.gen_range(0..86400));
55 let volume = rng.gen_range(1_000_000..100_000_000);50 let volume = rng.gen_range(1_000_000..100_000_000);
56 let turnover = last_done * volume as f64;51 let turnover = last_done * volume as f64;
57 let symbol = {52 let symbol = {
@@ -170,7 +165,7 @@ impl TableRow { · 112 unmodified lines
170 .child(format!("{:.2}%", self.quote.change())),165 .child(format!("{:.2}%", self.quote.change())),
171 "timestamp" => div()166 "timestamp" => div()
172 .text_color(color)167 .text_color(color)
173 .child(format!("{:?}", self.quote.timestamp.elapsed().as_secs())),168 .child(format!("{:?}", self.quote.timestamp.as_secs())),
174 "open" => div()169 "open" => div()
175 .text_color(color)170 .text_color(color)
176 .child(format!("{:.2}", self.quote.open)),171 .child(format!("{:.2}", self.quote.open)),
@@ -378,8 +373,6 @@ impl DataTable { · 201 unmodified lines
378 373
379impl Render for DataTable {374impl Render for DataTable {
380 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {375 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
381 let entity = cx.entity();
382
383 div()376 div()
384 .font_family(".SystemUIFont")377 .font_family(".SystemUIFont")
385 .bg(gpui::white())378 .bg(gpui::white())
@@ -431,8 +424,10 @@ impl Render for DataTable { · 45 unmodified lines
431 .relative()424 .relative()
432 .size_full()425 .size_full()
433 .child(426 .child(
434 uniform_list(entity, "items", self.quotes.len(), {427 uniform_list(
435 move |this, range, _, _| {428 "items",
429 self.quotes.len(),
430 cx.processor(move |this, range: Range<usize>, _, _| {
436 this.visible_range = range.clone();431 this.visible_range = range.clone();
437 let mut items = Vec::with_capacity(range.end - range.start);432 let mut items = Vec::with_capacity(range.end - range.start);
438 for i in range {433 for i in range {
@@ -441,8 +436,8 @@ impl Render for DataTable { · 2 unmodified lines
441 }436 }
442 }437 }
443 items438 items
444 }439 }),
445 })440 )
446 .size_full()441 .size_full()
447 .track_scroll(self.scroll_handle.clone()),442 .track_scroll(self.scroll_handle.clone()),
448 )443 )
crates/gpui/examples/uniform_list.rs+2−3
@@ -9,10 +9,9 @@ impl Render for UniformListExample { · 8 unmodified lines
9 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {9 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
10 div().size_full().bg(rgb(0xffffff)).child(10 div().size_full().bg(rgb(0xffffff)).child(
11 uniform_list(11 uniform_list(
12 cx.entity().clone(),
13 "entries",12 "entries",
14 50,13 50,
15 |_this, range, _window, _cx| {14 cx.processor(|_this, range, _window, _cx| {
16 let mut items = Vec::new();15 let mut items = Vec::new();
17 for ix in range {16 for ix in range {
18 let item = ix + 1;17 let item = ix + 1;
@@ -29,7 +28,7 @@ impl Render for UniformListExample { · 10 unmodified lines
29 );28 );
30 }29 }
31 items30 items
32 },31 }),
33 )32 )
34 .h_full(),33 .h_full(),
35 )34 )
crates/gpui/src/app.rs+3−3
@@ -64,7 +64,7 @@ pub struct AppCell { · 63 unmodified lines
64impl AppCell {64impl AppCell {
65 #[doc(hidden)]65 #[doc(hidden)]
66 #[track_caller]66 #[track_caller]
67 pub fn borrow(&self) -> AppRef {67 pub fn borrow(&self) -> AppRef<'_> {
68 if option_env!("TRACK_THREAD_BORROWS").is_some() {68 if option_env!("TRACK_THREAD_BORROWS").is_some() {
69 let thread_id = std::thread::current().id();69 let thread_id = std::thread::current().id();
70 eprintln!("borrowed {thread_id:?}");70 eprintln!("borrowed {thread_id:?}");
@@ -74,7 +74,7 @@ impl AppCell { · 3 unmodified lines
74 74
75 #[doc(hidden)]75 #[doc(hidden)]
76 #[track_caller]76 #[track_caller]
77 pub fn borrow_mut(&self) -> AppRefMut {77 pub fn borrow_mut(&self) -> AppRefMut<'_> {
78 if option_env!("TRACK_THREAD_BORROWS").is_some() {78 if option_env!("TRACK_THREAD_BORROWS").is_some() {
79 let thread_id = std::thread::current().id();79 let thread_id = std::thread::current().id();
80 eprintln!("borrowed {thread_id:?}");80 eprintln!("borrowed {thread_id:?}");
@@ -84,7 +84,7 @@ impl AppCell { · 3 unmodified lines
84 84
85 #[doc(hidden)]85 #[doc(hidden)]
86 #[track_caller]86 #[track_caller]
87 pub fn try_borrow_mut(&self) -> Result<AppRefMut, BorrowMutError> {87 pub fn try_borrow_mut(&self) -> Result<AppRefMut<'_>, BorrowMutError> {
88 if option_env!("TRACK_THREAD_BORROWS").is_some() {88 if option_env!("TRACK_THREAD_BORROWS").is_some() {
89 let thread_id = std::thread::current().id();89 let thread_id = std::thread::current().id();
90 eprintln!("borrowed {thread_id:?}");90 eprintln!("borrowed {thread_id:?}");
crates/gpui/src/app/context.rs+12−0
@@ -225,6 +225,18 @@ impl<'a, T: 'static> Context<'a, T> { · 224 unmodified lines
225 }225 }
226 }226 }
227 227
228 /// Convenience method for producing view state in a closure.
229 /// See `listener` for more details.
230 pub fn processor<E, R>(
231 &self,
232 f: impl Fn(&mut T, E, &mut Window, &mut Context<T>) -> R + 'static,
233 ) -> impl Fn(E, &mut Window, &mut App) -> R + 'static {
234 let view = self.entity();
235 move |e: E, window: &mut Window, cx: &mut App| {
236 view.update(cx, |view, cx| f(view, e, window, cx))
237 }
238 }
239
228 /// Run something using this entity and cx, when the returned struct is dropped240 /// Run something using this entity and cx, when the returned struct is dropped
229 pub fn on_drop(241 pub fn on_drop(
230 &self,242 &self,
crates/gpui/src/elements/uniform_list.rs+12−17
@@ -5,10 +5,10 @@ · 4 unmodified lines
5//! elements with uniform height.5//! elements with uniform height.
6 6
7use crate::{7use crate::{
8 AnyElement, App, AvailableSpace, Bounds, ContentMask, Context, Element, ElementId, Entity,8 AnyElement, App, AvailableSpace, Bounds, ContentMask, Element, ElementId, GlobalElementId,
9 GlobalElementId, Hitbox, InspectorElementId, InteractiveElement, Interactivity, IntoElement,9 Hitbox, InspectorElementId, InteractiveElement, Interactivity, IntoElement, IsZero, LayoutId,
10 IsZero, LayoutId, ListSizingBehavior, Overflow, Pixels, Render, ScrollHandle, Size,10 ListSizingBehavior, Overflow, Pixels, ScrollHandle, Size, StyleRefinement, Styled, Window,
11 StyleRefinement, Styled, Window, point, size,11 point, size,
12};12};
13use smallvec::SmallVec;13use smallvec::SmallVec;
14use std::{cell::RefCell, cmp, ops::Range, rc::Rc};14use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
@@ -19,28 +19,23 @@ use super::ListHorizontalSizingBehavior; · 4 unmodified lines
19/// When rendered into a container with overflow-y: hidden and a fixed (or max) height,19/// When rendered into a container with overflow-y: hidden and a fixed (or max) height,
20/// uniform_list will only render the visible subset of items.20/// uniform_list will only render the visible subset of items.
21#[track_caller]21#[track_caller]
22pub fn uniform_list<I, R, V>(22pub fn uniform_list<R>(
23 view: Entity<V>,23 id: impl Into<ElementId>,
24 id: I,
25 item_count: usize,24 item_count: usize,
26 f: impl 'static + Fn(&mut V, Range<usize>, &mut Window, &mut Context<V>) -> Vec<R>,25 f: impl 'static + Fn(Range<usize>, &mut Window, &mut App) -> Vec<R>,
27) -> UniformList26) -> UniformList
28where27where
29 I: Into<ElementId>,
30 R: IntoElement,28 R: IntoElement,
31 V: Render,
32{29{
33 let id = id.into();30 let id = id.into();
34 let mut base_style = StyleRefinement::default();31 let mut base_style = StyleRefinement::default();
35 base_style.overflow.y = Some(Overflow::Scroll);32 base_style.overflow.y = Some(Overflow::Scroll);
36 33
37 let render_range = move |range, window: &mut Window, cx: &mut App| {34 let render_range = move |range: Range<usize>, window: &mut Window, cx: &mut App| {
38 view.update(cx, |this, cx| {35 f(range, window, cx)
39 f(this, range, window, cx)36 .into_iter()
40 .into_iter()37 .map(|component| component.into_any_element())
41 .map(|component| component.into_any_element())38 .collect()
42 .collect()
43 })
44 };39 };
45 40
46 UniformList {41 UniformList {
crates/gpui/src/gpui.rs+1−1
@@ -255,7 +255,7 @@ pub trait VisualContext: AppContext { · 254 unmodified lines
255 update: impl FnOnce(&mut T, &mut Window, &mut Context<T>) -> R,255 update: impl FnOnce(&mut T, &mut Window, &mut Context<T>) -> R,
256 ) -> Self::Result<R>;256 ) -> Self::Result<R>;
257 257
258 /// Update a view with the given callback258 /// Create a new entity, with access to `Window`.
259 fn new_window_entity<T: 'static>(259 fn new_window_entity<T: 'static>(
260 &mut self,260 &mut self,
261 build_entity: impl FnOnce(&mut Window, &mut Context<T>) -> T,261 build_entity: impl FnOnce(&mut Window, &mut Context<T>) -> T,
crates/gpui/src/platform.rs+6−2
@@ -142,7 +142,11 @@ pub fn guess_compositor() -> &'static str { · 141 unmodified lines
142 142
143#[cfg(target_os = "windows")]143#[cfg(target_os = "windows")]
144pub(crate) fn current_platform(_headless: bool) -> Rc<dyn Platform> {144pub(crate) fn current_platform(_headless: bool) -> Rc<dyn Platform> {
145 Rc::new(WindowsPlatform::new())145 Rc::new(
146 WindowsPlatform::new()
147 .inspect_err(|err| show_error("Error: Zed failed to launch", err.to_string()))
148 .unwrap(),
149 )
146}150}
147 151
148pub(crate) trait Platform: 'static {152pub(crate) trait Platform: 'static {
@@ -718,7 +722,7 @@ impl<T> ops::Index<usize> for AtlasTextureList<T> { · 569 unmodified lines
718 722
719impl<T> AtlasTextureList<T> {723impl<T> AtlasTextureList<T> {
720 #[allow(unused)]724 #[allow(unused)]
721 fn drain(&mut self) -> std::vec::Drain<Option<T>> {725 fn drain(&mut self) -> std::vec::Drain<'_, Option<T>> {
722 self.free_list.clear();726 self.free_list.clear();
723 self.textures.drain(..)727 self.textures.drain(..)
724 }728 }
crates/gpui/src/platform/blade/blade_renderer.rs+1−1
@@ -342,7 +342,7 @@ impl BladeRenderer { · 341 unmodified lines
342 let surface = context342 let surface = context
343 .gpu343 .gpu
344 .create_surface_configured(window, surface_config)344 .create_surface_configured(window, surface_config)
345 .unwrap();345 .map_err(|err| anyhow::anyhow!("Failed to create surface: {err:?}"))?;
346 346
347 let command_encoder = context.gpu.create_command_encoder(gpu::CommandEncoderDesc {347 let command_encoder = context.gpu.create_command_encoder(gpu::CommandEncoderDesc {
348 name: "main",348 name: "main",
crates/gpui/src/platform/linux/headless/client.rs+1−1
@@ -52,7 +52,7 @@ impl LinuxClient for HeadlessClient { · 51 unmodified lines
52 }52 }
53 53
54 fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {54 fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
55 Box::new(LinuxKeyboardLayout::new("unknown".to_string()))55 Box::new(LinuxKeyboardLayout::new("unknown".into()))
56 }56 }
57 57
58 fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {58 fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
crates/gpui/src/platform/linux/keyboard.rs+7−6
@@ -1,21 +1,22 @@
1use crate::PlatformKeyboardLayout;1use crate::{PlatformKeyboardLayout, SharedString};
2 2
3#[derive(Clone)]
3pub(crate) struct LinuxKeyboardLayout {4pub(crate) struct LinuxKeyboardLayout {
4 id: String,5 name: SharedString,
5}6}
6 7
7impl PlatformKeyboardLayout for LinuxKeyboardLayout {8impl PlatformKeyboardLayout for LinuxKeyboardLayout {
8 fn id(&self) -> &str {9 fn id(&self) -> &str {
9 &self.id10 &self.name
10 }11 }
11 12
12 fn name(&self) -> &str {13 fn name(&self) -> &str {
13 &self.id14 &self.name
14 }15 }
15}16}
16 17
17impl LinuxKeyboardLayout {18impl LinuxKeyboardLayout {
18 pub(crate) fn new(id: String) -> Self {19 pub(crate) fn new(name: SharedString) -> Self {
19 Self { id }20 Self { name }
20 }21 }
21}22}
crates/gpui/src/platform/linux/wayland/client.rs+61−46
@@ -71,17 +71,6 @@ use super::{ · 70 unmodified lines
71 window::{ImeInput, WaylandWindowStatePtr},71 window::{ImeInput, WaylandWindowStatePtr},
72};72};
73 73
74use crate::platform::linux::{
75 LinuxClient, get_xkb_compose_state, is_within_click_distance, open_uri_internal, read_fd,
76 reveal_path_internal,
77 wayland::{
78 clipboard::{Clipboard, DataOffer, FILE_LIST_MIME_TYPE, TEXT_MIME_TYPES},
79 cursor::Cursor,
80 serial::{SerialKind, SerialTracker},
81 window::WaylandWindow,
82 },
83 xdg_desktop_portal::{Event as XDPEvent, XDPEventSource},
84};
85use crate::platform::{PlatformWindow, blade::BladeContext};74use crate::platform::{PlatformWindow, blade::BladeContext};
86use crate::{75use crate::{
87 AnyWindowHandle, Bounds, CursorStyle, DOUBLE_CLICK_INTERVAL, DevicePixels, DisplayId,76 AnyWindowHandle, Bounds, CursorStyle, DOUBLE_CLICK_INTERVAL, DevicePixels, DisplayId,
@@ -91,10 +80,26 @@ use crate::{ · 3 unmodified lines
91 PlatformInput, PlatformKeyboardLayout, Point, SCROLL_LINES, ScaledPixels, ScreenCaptureSource,80 PlatformInput, PlatformKeyboardLayout, Point, SCROLL_LINES, ScaledPixels, ScreenCaptureSource,
92 ScrollDelta, ScrollWheelEvent, Size, TouchPhase, WindowParams, point, px, size,81 ScrollDelta, ScrollWheelEvent, Size, TouchPhase, WindowParams, point, px, size,
93};82};
83use crate::{
84 SharedString,
85 platform::linux::{
86 LinuxClient, get_xkb_compose_state, is_within_click_distance, open_uri_internal, read_fd,
87 reveal_path_internal,
88 wayland::{
89 clipboard::{Clipboard, DataOffer, FILE_LIST_MIME_TYPE, TEXT_MIME_TYPES},
90 cursor::Cursor,
91 serial::{SerialKind, SerialTracker},
92 window::WaylandWindow,
93 },
94 xdg_desktop_portal::{Event as XDPEvent, XDPEventSource},
95 },
96};
94 97
95/// Used to convert evdev scancode to xkb scancode98/// Used to convert evdev scancode to xkb scancode
96const MIN_KEYCODE: u32 = 8;99const MIN_KEYCODE: u32 = 8;
97 100
101const UNKNOWN_KEYBOARD_LAYOUT_NAME: SharedString = SharedString::new_static("unknown");
102
98#[derive(Clone)]103#[derive(Clone)]
99pub struct Globals {104pub struct Globals {
100 pub qh: QueueHandle<WaylandClientStatePtr>,105 pub qh: QueueHandle<WaylandClientStatePtr>,
@@ -205,6 +210,7 @@ pub(crate) struct WaylandClientState { · 104 unmodified lines
205 // Output to scale mapping210 // Output to scale mapping
206 outputs: HashMap<ObjectId, Output>,211 outputs: HashMap<ObjectId, Output>,
207 in_progress_outputs: HashMap<ObjectId, InProgressOutput>,212 in_progress_outputs: HashMap<ObjectId, InProgressOutput>,
213 keyboard_layout: LinuxKeyboardLayout,
208 keymap_state: Option<xkb::State>,214 keymap_state: Option<xkb::State>,
209 compose_state: Option<xkb::compose::State>,215 compose_state: Option<xkb::compose::State>,
210 drag: DragState,216 drag: DragState,
@@ -335,6 +341,35 @@ impl WaylandClientStatePtr { · 124 unmodified lines
335 text_input.commit();341 text_input.commit();
336 }342 }
337 343
344 pub fn handle_keyboard_layout_change(&self) {
345 let client = self.get_client();
346 let mut state = client.borrow_mut();
347 let changed = if let Some(keymap_state) = &state.keymap_state {
348 let layout_idx = keymap_state.serialize_layout(xkbcommon::xkb::STATE_LAYOUT_EFFECTIVE);
349 let keymap = keymap_state.get_keymap();
350 let layout_name = keymap.layout_get_name(layout_idx);
351 let changed = layout_name != state.keyboard_layout.name();
352 if changed {
353 state.keyboard_layout = LinuxKeyboardLayout::new(layout_name.to_string().into());
354 }
355 changed
356 } else {
357 let changed = &UNKNOWN_KEYBOARD_LAYOUT_NAME != state.keyboard_layout.name();
358 if changed {
359 state.keyboard_layout = LinuxKeyboardLayout::new(UNKNOWN_KEYBOARD_LAYOUT_NAME);
360 }
361 changed
362 };
363 if changed {
364 if let Some(mut callback) = state.common.callbacks.keyboard_layout_change.take() {
365 drop(state);
366 callback();
367 state = client.borrow_mut();
368 state.common.callbacks.keyboard_layout_change = Some(callback);
369 }
370 }
371 }
372
338 pub fn drop_window(&self, surface_id: &ObjectId) {373 pub fn drop_window(&self, surface_id: &ObjectId) {
339 let mut client = self.get_client();374 let mut client = self.get_client();
340 let mut state = client.borrow_mut();375 let mut state = client.borrow_mut();
@@ -533,6 +568,7 @@ impl WaylandClient { · 192 unmodified lines
533 in_progress_outputs,568 in_progress_outputs,
534 windows: HashMap::default(),569 windows: HashMap::default(),
535 common,570 common,
571 keyboard_layout: LinuxKeyboardLayout::new(UNKNOWN_KEYBOARD_LAYOUT_NAME),
536 keymap_state: None,572 keymap_state: None,
537 compose_state: None,573 compose_state: None,
538 drag: DragState {574 drag: DragState {
@@ -590,17 +626,7 @@ impl WaylandClient { · 51 unmodified lines
590 626
591impl LinuxClient for WaylandClient {627impl LinuxClient for WaylandClient {
592 fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {628 fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
593 let state = self.0.borrow();629 Box::new(self.0.borrow().keyboard_layout.clone())
594 let id = if let Some(keymap_state) = &state.keymap_state {
595 let layout_idx = keymap_state.serialize_layout(xkbcommon::xkb::STATE_LAYOUT_EFFECTIVE);
596 keymap_state
597 .get_keymap()
598 .layout_get_name(layout_idx)
599 .to_string()
600 } else {
601 "unknown".to_string()
602 };
603 Box::new(LinuxKeyboardLayout::new(id))
604 }630 }
605 631
606 fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {632 fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
@@ -1181,13 +1207,9 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr { · 574 unmodified lines
1181 };1207 };
1182 state.keymap_state = Some(xkb::State::new(&keymap));1208 state.keymap_state = Some(xkb::State::new(&keymap));
1183 state.compose_state = get_xkb_compose_state(&xkb_context);1209 state.compose_state = get_xkb_compose_state(&xkb_context);
1210 drop(state);
1184 1211
1185 if let Some(mut callback) = state.common.callbacks.keyboard_layout_change.take() {1212 this.handle_keyboard_layout_change();
1186 drop(state);
1187 callback();
1188 state = client.borrow_mut();
1189 state.common.callbacks.keyboard_layout_change = Some(callback);
1190 }
1191 }1213 }
1192 wl_keyboard::Event::Enter { surface, .. } => {1214 wl_keyboard::Event::Enter { surface, .. } => {
1193 state.keyboard_focused_window = get_window(&mut state, &surface.id());1215 state.keyboard_focused_window = get_window(&mut state, &surface.id());
@@ -1230,26 +1252,18 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr { · 36 unmodified lines
1230 keymap_state.update_mask(mods_depressed, mods_latched, mods_locked, 0, 0, group);1252 keymap_state.update_mask(mods_depressed, mods_latched, mods_locked, 0, 0, group);
1231 state.modifiers = Modifiers::from_xkb(keymap_state);1253 state.modifiers = Modifiers::from_xkb(keymap_state);
1232 1254
1233 if group != old_layout {1255 if let Some(focused_window) = focused_window {
1234 if let Some(mut callback) = state.common.callbacks.keyboard_layout_change.take()1256 let input = PlatformInput::ModifiersChanged(ModifiersChangedEvent {
1235 {1257 modifiers: state.modifiers,
1236 drop(state);1258 });
1237 callback();
1238 state = client.borrow_mut();
1239 state.common.callbacks.keyboard_layout_change = Some(callback);
1240 }
1241 }
1242
1243 let Some(focused_window) = focused_window else {
1244 return;
1245 };
1246 1259
1247 let input = PlatformInput::ModifiersChanged(ModifiersChangedEvent {1260 drop(state);
1248 modifiers: state.modifiers,1261 focused_window.handle_input(input);
1249 });1262 }
1250 1263
1251 drop(state);1264 if group != old_layout {
1252 focused_window.handle_input(input);1265 this.handle_keyboard_layout_change();
1266 }
1253 }1267 }
1254 wl_keyboard::Event::Key {1268 wl_keyboard::Event::Key {
1255 serial,1269 serial,
@@ -1374,6 +1388,7 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr { · 118 unmodified lines
1374 }1388 }
1375 }1389 }
1376}1390}
1391
1377impl Dispatch<zwp_text_input_v3::ZwpTextInputV3, ()> for WaylandClientStatePtr {1392impl Dispatch<zwp_text_input_v3::ZwpTextInputV3, ()> for WaylandClientStatePtr {
1378 fn event(1393 fn event(
1379 this: &mut Self,1394 this: &mut Self,
crates/gpui/src/platform/linux/x11/client.rs+244−87
@@ -28,7 +28,7 @@ use x11rb::{ · 27 unmodified lines
28 protocol::xkb::ConnectionExt as _,28 protocol::xkb::ConnectionExt as _,
29 protocol::xproto::{29 protocol::xproto::{
30 AtomEnum, ChangeWindowAttributesAux, ClientMessageData, ClientMessageEvent,30 AtomEnum, ChangeWindowAttributesAux, ClientMessageData, ClientMessageEvent,
31 ConnectionExt as _, EventMask, KeyPressEvent,31 ConnectionExt as _, EventMask, KeyPressEvent, Visibility,
32 },32 },
33 protocol::{Event, randr, render, xinput, xkb, xproto},33 protocol::{Event, randr, render, xinput, xkb, xproto},
34 resource_manager::Database,34 resource_manager::Database,
@@ -78,7 +78,10 @@ pub(crate) const XINPUT_ALL_DEVICE_GROUPS: xinput::DeviceId = 1; · 43 unmodified lines
78 78
79pub(crate) struct WindowRef {79pub(crate) struct WindowRef {
80 window: X11WindowStatePtr,80 window: X11WindowStatePtr,
81 refresh_event_token: RegistrationToken,81 refresh_state: Option<RefreshState>,
82 expose_event_received: bool,
83 last_visibility: Visibility,
84 is_mapped: bool,
82}85}
83 86
84impl WindowRef {87impl WindowRef {
@@ -95,6 +98,16 @@ impl Deref for WindowRef { · 10 unmodified lines
95 }98 }
96}99}
97 100
101enum RefreshState {
102 Hidden {
103 refresh_rate: Duration,
104 },
105 PeriodicRefresh {
106 refresh_rate: Duration,
107 event_loop_token: RegistrationToken,
108 },
109}
110
98#[derive(Debug)]111#[derive(Debug)]
99#[non_exhaustive]112#[non_exhaustive]
100pub enum EventHandlerError {113pub enum EventHandlerError {
@@ -185,6 +198,7 @@ pub struct X11ClientState { · 84 unmodified lines
185 pub(crate) keyboard_focused_window: Option<xproto::Window>,198 pub(crate) keyboard_focused_window: Option<xproto::Window>,
186 pub(crate) xkb: xkbc::State,199 pub(crate) xkb: xkbc::State,
187 previous_xkb_state: XKBStateNotiy,200 previous_xkb_state: XKBStateNotiy,
201 keyboard_layout: LinuxKeyboardLayout,
188 pub(crate) ximc: Option<X11rbClient<Rc<XCBConnection>>>,202 pub(crate) ximc: Option<X11rbClient<Rc<XCBConnection>>>,
189 pub(crate) xim_handler: Option<XimHandler>,203 pub(crate) xim_handler: Option<XimHandler>,
190 pub modifiers: Modifiers,204 pub modifiers: Modifiers,
@@ -220,7 +234,14 @@ impl X11ClientStatePtr { · 29 unmodified lines
220 let mut state = client.0.borrow_mut();234 let mut state = client.0.borrow_mut();
221 235
222 if let Some(window_ref) = state.windows.remove(&x_window) {236 if let Some(window_ref) = state.windows.remove(&x_window) {
223 state.loop_handle.remove(window_ref.refresh_event_token);237 match window_ref.refresh_state {
238 Some(RefreshState::PeriodicRefresh {
239 event_loop_token, ..
240 }) => {
241 state.loop_handle.remove(event_loop_token);
242 }
243 _ => {}
244 }
224 }245 }
225 if state.mouse_focused_window == Some(x_window) {246 if state.mouse_focused_window == Some(x_window) {
226 state.mouse_focused_window = None;247 state.mouse_focused_window = None;
@@ -349,13 +370,21 @@ impl X11Client { · 122 unmodified lines
349 let events = xkb::EventType::STATE_NOTIFY370 let events = xkb::EventType::STATE_NOTIFY
350 | xkb::EventType::MAP_NOTIFY371 | xkb::EventType::MAP_NOTIFY
351 | xkb::EventType::NEW_KEYBOARD_NOTIFY;372 | xkb::EventType::NEW_KEYBOARD_NOTIFY;
373 let map_notify_parts = xkb::MapPart::KEY_TYPES
374 | xkb::MapPart::KEY_SYMS
375 | xkb::MapPart::MODIFIER_MAP
376 | xkb::MapPart::EXPLICIT_COMPONENTS
377 | xkb::MapPart::KEY_ACTIONS
378 | xkb::MapPart::KEY_BEHAVIORS
379 | xkb::MapPart::VIRTUAL_MODS
380 | xkb::MapPart::VIRTUAL_MOD_MAP;
352 xcb_connection381 xcb_connection
353 .xkb_select_events(382 .xkb_select_events(
354 xkb::ID::USE_CORE_KBD.into(),383 xkb::ID::USE_CORE_KBD.into(),
355 0u8.into(),384 0u8.into(),
356 events,385 events,
357 0u8.into(),386 map_notify_parts,
358 0u8.into(),387 map_notify_parts,
359 &xkb::SelectEventsAux::new(),388 &xkb::SelectEventsAux::new(),
360 )389 )
361 .unwrap();390 .unwrap();
@@ -373,17 +402,22 @@ impl X11Client { · 11 unmodified lines
373 xkbc::x11::state_new_from_device(&xkb_keymap, &xcb_connection, xkb_device_id)402 xkbc::x11::state_new_from_device(&xkb_keymap, &xcb_connection, xkb_device_id)
374 };403 };
375 let compose_state = get_xkb_compose_state(&xkb_context);404 let compose_state = get_xkb_compose_state(&xkb_context);
376 let resource_database = x11rb::resource_manager::new_from_default(&xcb_connection).unwrap();405 let layout_idx = xkb_state.serialize_layout(STATE_LAYOUT_EFFECTIVE);
406 let layout_name = xkb_state
407 .get_keymap()
408 .layout_get_name(layout_idx)
409 .to_string();
410 let keyboard_layout = LinuxKeyboardLayout::new(layout_name.into());
377 411
378 let gpu_context = BladeContext::new().expect("Unable to init GPU context");412 let gpu_context = BladeContext::new().expect("Unable to init GPU context");
379 413
414 let resource_database = x11rb::resource_manager::new_from_default(&xcb_connection).unwrap();
380 let scale_factor = resource_database415 let scale_factor = resource_database
381 .get_value("Xft.dpi", "Xft.dpi")416 .get_value("Xft.dpi", "Xft.dpi")
382 .ok()417 .ok()
383 .flatten()418 .flatten()
384 .map(|dpi: f32| dpi / 96.0)419 .map(|dpi: f32| dpi / 96.0)
385 .unwrap_or(1.0);420 .unwrap_or(1.0);
386
387 let cursor_handle = cursor::Handle::new(&xcb_connection, x_root_index, &resource_database)421 let cursor_handle = cursor::Handle::new(&xcb_connection, x_root_index, &resource_database)
388 .unwrap()422 .unwrap()
389 .reply()423 .reply()
@@ -461,6 +495,7 @@ impl X11Client { · 71 unmodified lines
461 keyboard_focused_window: None,495 keyboard_focused_window: None,
462 xkb: xkb_state,496 xkb: xkb_state,
463 previous_xkb_state: XKBStateNotiy::default(),497 previous_xkb_state: XKBStateNotiy::default(),
498 keyboard_layout,
464 ximc,499 ximc,
465 xim_handler,500 xim_handler,
466 501
@@ -492,6 +527,10 @@ impl X11Client { · 25 unmodified lines
492 let mut last_key_release = None;527 let mut last_key_release = None;
493 let mut last_key_press: Option<KeyPressEvent> = None;528 let mut last_key_press: Option<KeyPressEvent> = None;
494 529
530 // event handlers for new keyboard / remapping refresh the state without using event
531 // details, this deduplicates them.
532 let mut last_keymap_change_event: Option<Event> = None;
533
495 loop {534 loop {
496 match xcb_connection.poll_for_event() {535 match xcb_connection.poll_for_event() {
497 Ok(Some(event)) => {536 Ok(Some(event)) => {
@@ -500,9 +539,29 @@ impl X11Client { · 2 unmodified lines
500 windows_to_refresh.insert(expose_event.window);539 windows_to_refresh.insert(expose_event.window);
501 }540 }
502 Event::KeyRelease(_) => {541 Event::KeyRelease(_) => {
542 if let Some(last_keymap_change_event) =
543 last_keymap_change_event.take()
544 {
545 if let Some(last_key_release) = last_key_release.take() {
546 events.push(last_key_release);
547 }
548 last_key_press = None;
549 events.push(last_keymap_change_event);
550 }
551
503 last_key_release = Some(event);552 last_key_release = Some(event);
504 }553 }
505 Event::KeyPress(key_press) => {554 Event::KeyPress(key_press) => {
555 if let Some(last_keymap_change_event) =
556 last_keymap_change_event.take()
557 {
558 if let Some(last_key_release) = last_key_release.take() {
559 events.push(last_key_release);
560 }
561 last_key_press = None;
562 events.push(last_keymap_change_event);
563 }
564
506 if let Some(last_press) = last_key_press.as_ref() {565 if let Some(last_press) = last_key_press.as_ref() {
507 if last_press.detail == key_press.detail {566 if last_press.detail == key_press.detail {
508 continue;567 continue;
@@ -523,6 +582,12 @@ impl X11Client { · 14 unmodified lines
523 events.push(Event::KeyPress(key_press));582 events.push(Event::KeyPress(key_press));
524 last_key_press = Some(key_press);583 last_key_press = Some(key_press);
525 }584 }
585 Event::XkbNewKeyboardNotify(_) | Event::XkbMapNotify(_) => {
586 if let Some(release_event) = last_key_release.take() {
587 events.push(release_event);
588 }
589 last_keymap_change_event = Some(event);
590 }
526 _ => {591 _ => {
527 if let Some(release_event) = last_key_release.take() {592 if let Some(release_event) = last_key_release.take() {
528 events.push(release_event);593 events.push(release_event);
@@ -532,10 +597,6 @@ impl X11Client { · 3 unmodified lines
532 }597 }
533 }598 }
534 Ok(None) => {599 Ok(None) => {
535 // Add any remaining stored KeyRelease event
536 if let Some(release_event) = last_key_release.take() {
537 events.push(release_event);
538 }
539 break;600 break;
540 }601 }
541 Err(e) => {602 Err(e) => {
@@ -545,15 +606,21 @@ impl X11Client { · 3 unmodified lines
545 }606 }
546 }607 }
547 608
609 if let Some(release_event) = last_key_release.take() {
610 events.push(release_event);
611 }
612 if let Some(keymap_change_event) = last_keymap_change_event.take() {
613 events.push(keymap_change_event);
614 }
615
548 if events.is_empty() && windows_to_refresh.is_empty() {616 if events.is_empty() && windows_to_refresh.is_empty() {
549 break;617 break;
550 }618 }
551 619
552 for window in windows_to_refresh.into_iter() {620 for window in windows_to_refresh.into_iter() {
553 if let Some(window) = self.get_window(window) {621 let mut state = self.0.borrow_mut();
554 window.refresh(RequestFrameOptions {622 if let Some(window) = state.windows.get_mut(&window) {
555 require_presentation: true,623 window.expose_event_received = true;
556 });
557 }624 }
558 }625 }
559 626
@@ -661,6 +728,27 @@ impl X11Client { · 101 unmodified lines
661 728
662 fn handle_event(&self, event: Event) -> Option<()> {729 fn handle_event(&self, event: Event) -> Option<()> {
663 match event {730 match event {
731 Event::UnmapNotify(event) => {
732 let mut state = self.0.borrow_mut();
733 if let Some(window_ref) = state.windows.get_mut(&event.window) {
734 window_ref.is_mapped = false;
735 }
736 state.update_refresh_loop(event.window);
737 }
738 Event::MapNotify(event) => {
739 let mut state = self.0.borrow_mut();
740 if let Some(window_ref) = state.windows.get_mut(&event.window) {
741 window_ref.is_mapped = true;
742 }
743 state.update_refresh_loop(event.window);
744 }
745 Event::VisibilityNotify(event) => {
746 let mut state = self.0.borrow_mut();
747 if let Some(window_ref) = state.windows.get_mut(&event.window) {
748 window_ref.last_visibility = event.state;
749 }
750 state.update_refresh_loop(event.window);
751 }
664 Event::ClientMessage(event) => {752 Event::ClientMessage(event) => {
665 let window = self.get_window(event.window)?;753 let window = self.get_window(event.window)?;
666 let [atom, arg1, arg2, arg3, arg4] = event.data.as_data32();754 let [atom, arg1, arg2, arg3, arg4] = event.data.as_data32();
@@ -826,7 +914,7 @@ impl X11Client { · 159 unmodified lines
826 self.reset_ime();914 self.reset_ime();
827 window.handle_ime_delete();915 window.handle_ime_delete();
828 }916 }
829 Event::XkbNewKeyboardNotify(_) | Event::MapNotify(_) => {917 Event::XkbNewKeyboardNotify(_) | Event::XkbMapNotify(_) => {
830 let mut state = self.0.borrow_mut();918 let mut state = self.0.borrow_mut();
831 let xkb_state = {919 let xkb_state = {
832 let xkb_keymap = xkbc::x11::keymap_new_from_device(920 let xkb_keymap = xkbc::x11::keymap_new_from_device(
@@ -850,6 +938,8 @@ impl X11Client { · 17 unmodified lines
850 locked_layout,938 locked_layout,
851 };939 };
852 state.xkb = xkb_state;940 state.xkb = xkb_state;
941 drop(state);
942 self.handle_keyboard_layout_change();
853 }943 }
854 Event::XkbStateNotify(event) => {944 Event::XkbStateNotify(event) => {
855 let mut state = self.0.borrow_mut();945 let mut state = self.0.borrow_mut();
@@ -869,16 +959,6 @@ impl X11Client { · 13 unmodified lines
869 locked_layout: event.locked_group.into(),959 locked_layout: event.locked_group.into(),
870 };960 };
871 961
872 if new_layout != old_layout {
873 if let Some(mut callback) = state.common.callbacks.keyboard_layout_change.take()
874 {
875 drop(state);
876 callback();
877 state = self.0.borrow_mut();
878 state.common.callbacks.keyboard_layout_change = Some(callback);
879 }
880 }
881
882 let modifiers = Modifiers::from_xkb(&state.xkb);962 let modifiers = Modifiers::from_xkb(&state.xkb);
883 if state.last_modifiers_changed_event == modifiers {963 if state.last_modifiers_changed_event == modifiers {
884 drop(state);964 drop(state);
@@ -893,6 +973,10 @@ impl X11Client { · 8 unmodified lines
893 ModifiersChangedEvent { modifiers },973 ModifiersChangedEvent { modifiers },
894 ));974 ));
895 }975 }
976
977 if new_layout != old_layout {
978 self.handle_keyboard_layout_change();
979 }
896 }980 }
897 Event::KeyPress(event) => {981 Event::KeyPress(event) => {
898 let window = self.get_window(event.event)?;982 let window = self.get_window(event.event)?;
@@ -1281,6 +1365,22 @@ impl X11Client { · 382 unmodified lines
1281 drop(state);1365 drop(state);
1282 Some(())1366 Some(())
1283 }1367 }
1368
1369 fn handle_keyboard_layout_change(&self) {
1370 let mut state = self.0.borrow_mut();
1371 let layout_idx = state.xkb.serialize_layout(STATE_LAYOUT_EFFECTIVE);
1372 let keymap = state.xkb.get_keymap();
1373 let layout_name = keymap.layout_get_name(layout_idx);
1374 if layout_name != state.keyboard_layout.name() {
1375 state.keyboard_layout = LinuxKeyboardLayout::new(layout_name.to_string().into());
1376 if let Some(mut callback) = state.common.callbacks.keyboard_layout_change.take() {
1377 drop(state);
1378 callback();
1379 state = self.0.borrow_mut();
1380 state.common.callbacks.keyboard_layout_change = Some(callback);
1381 }
1382 }
1383 }
1284}1384}
1285 1385
1286impl LinuxClient for X11Client {1386impl LinuxClient for X11Client {
@@ -1294,14 +1394,7 @@ impl LinuxClient for X11Client { · 7 unmodified lines
1294 1394
1295 fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {1395 fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
1296 let state = self.0.borrow();1396 let state = self.0.borrow();
1297 let layout_idx = state.xkb.serialize_layout(STATE_LAYOUT_EFFECTIVE);1397 Box::new(state.keyboard_layout.clone())
1298 Box::new(LinuxKeyboardLayout::new(
1299 state
1300 .xkb
1301 .get_keymap()
1302 .layout_get_name(layout_idx)
1303 .to_string(),
1304 ))
1305 }1398 }
1306 1399
1307 fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {1400 fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
@@ -1383,61 +1476,12 @@ impl LinuxClient for X11Client { · 75 unmodified lines
1383 )1476 )
1384 .unwrap();1477 .unwrap();
1385 1478
1386 let screen_resources = state
1387 .xcb_connection
1388 .randr_get_screen_resources(x_window)
1389 .unwrap()
1390 .reply()
1391 .expect("Could not find available screens");
1392
1393 let mode = screen_resources
1394 .crtcs
1395 .iter()
1396 .find_map(|crtc| {
1397 let crtc_info = state
1398 .xcb_connection
1399 .randr_get_crtc_info(*crtc, x11rb::CURRENT_TIME)
1400 .ok()?
1401 .reply()
1402 .ok()?;
1403
1404 screen_resources
1405 .modes
1406 .iter()
1407 .find(|m| m.id == crtc_info.mode)
1408 })
1409 .expect("Unable to find screen refresh rate");

158 more lines aren’t shown here.

crates/gpui/src/platform/linux/x11/window.rs+8−7

This diff is large: its 64 lines aren’t shown here.

crates/gpui/src/platform/mac/events.rs+1−1

This diff is large: its 9 lines aren’t shown here.

crates/gpui/src/platform/windows/events.rs+27−20

This diff is large: its 68 lines aren’t shown here.

crates/gpui/src/platform/windows/platform.rs+8−8

This diff is large: its 46 lines aren’t shown here.

crates/gpui/src/platform/windows/util.rs+12−1

This diff is large: its 24 lines aren’t shown here.

crates/gpui/src/platform/windows/window.rs+7−2

This diff is large: its 23 lines aren’t shown here.

crates/gpui/src/scene.rs+1−1

This diff is large: its 9 lines aren’t shown here.

crates/gpui/src/text_system.rs+14−0

This diff is large: its 21 lines aren’t shown here.

crates/gpui/src/text_system/line_layout.rs+2−2

This diff is large: its 18 lines aren’t shown here.

crates/gpui/src/window.rs+17−13

This diff is large: its 87 lines aren’t shown here.

crates/icons/src/icons.rs+1−0

This diff is large: its 8 lines aren’t shown here.

crates/language/src/buffer.rs+11−8

This diff is large: its 74 lines aren’t shown here.

crates/language/src/language.rs+3−1

This diff is large: its 11 lines aren’t shown here.

crates/language/src/language_registry.rs+32−10

This diff is large: its 110 lines aren’t shown here.

crates/language/src/language_settings.rs+4−1

This diff is large: its 19 lines aren’t shown here.

crates/language/src/syntax_map.rs+56−17

This diff is large: its 182 lines aren’t shown here.

crates/language/src/syntax_map/syntax_map_tests.rs+86−2

This diff is large: its 102 lines aren’t shown here.

crates/language/src/task_context.rs+6−5

This diff is large: its 24 lines aren’t shown here.

crates/language_extension/src/extension_lsp_adapter.rs+6−4

This diff is large: its 24 lines aren’t shown here.

crates/language_model/src/fake_provider.rs+1−0

This diff is large: its 8 lines aren’t shown here.

crates/language_model/src/language_model.rs+5−1

This diff is large: its 34 lines aren’t shown here.

crates/language_model/src/rate_limiter.rs+12−5

This diff is large: its 44 lines aren’t shown here.

crates/language_model/src/registry.rs+59−0

This diff is large: its 80 lines aren’t shown here.

crates/language_models/src/provider/anthropic.rs+18−10

This diff is large: its 61 lines aren’t shown here.

crates/language_models/src/provider/bedrock.rs+4−6

This diff is large: its 36 lines aren’t shown here.

crates/language_models/src/provider/cloud.rs+8−4

This diff is large: its 47 lines aren’t shown here.

crates/language_models/src/provider/copilot_chat.rs+5−3

This diff is large: its 33 lines aren’t shown here.

crates/language_models/src/provider/deepseek.rs+1−0

This diff is large: its 8 lines aren’t shown here.

crates/language_models/src/provider/google.rs+1−0

This diff is large: its 8 lines aren’t shown here.

crates/language_models/src/provider/lmstudio.rs+1−0

This diff is large: its 8 lines aren’t shown here.

crates/language_models/src/provider/mistral.rs+1−0

This diff is large: its 8 lines aren’t shown here.

crates/language_models/src/provider/ollama.rs+2−1

This diff is large: its 17 lines aren’t shown here.

crates/language_models/src/provider/open_ai.rs+2−3

This diff is large: its 26 lines aren’t shown here.

crates/language_models/src/provider/open_router.rs+69−19

This diff is large: its 144 lines aren’t shown here.

crates/language_tools/src/lsp_log.rs+47−40

This diff is large: its 183 lines aren’t shown here.

crates/language_tools/src/syntax_tree_view.rs+2−3

This diff is large: its 21 lines aren’t shown here.

crates/languages/Cargo.toml+0−1

This diff is large: its 8 lines aren’t shown here.

crates/languages/src/go.rs+5−4

This diff is large: its 31 lines aren’t shown here.

crates/languages/src/javascript/outline.scm+8−2

This diff is large: its 24 lines aren’t shown here.

crates/languages/src/javascript/runnables.scm+8−2

This diff is large: its 24 lines aren’t shown here.

crates/languages/src/python.rs+3−2

This diff is large: its 21 lines aren’t shown here.

crates/languages/src/rust.rs+4−2

This diff is large: its 29 lines aren’t shown here.

crates/languages/src/tsx/config.toml+1−1

This diff is large: its 7 lines aren’t shown here.

crates/languages/src/tsx/outline.scm+24−1

This diff is large: its 32 lines aren’t shown here.

crates/languages/src/tsx/overrides.scm+3−1

This diff is large: its 8 lines aren’t shown here.

crates/languages/src/tsx/runnables.scm+26−1

This diff is large: its 34 lines aren’t shown here.

crates/languages/src/typescript.rs+529−327

This diff is large: its 981 lines aren’t shown here.

crates/languages/src/typescript/config.toml+1−1

This diff is large: its 7 lines aren’t shown here.

crates/languages/src/typescript/outline.scm+8−2

This diff is large: its 24 lines aren’t shown here.

crates/languages/src/typescript/overrides.scm+3−1

This diff is large: its 8 lines aren’t shown here.

crates/languages/src/typescript/runnables.scm+8−2

This diff is large: its 24 lines aren’t shown here.

crates/livekit_client/Cargo.toml+1−1

This diff is large: its 9 lines aren’t shown here.

crates/livekit_client/src/livekit_client/playback.rs+1−1

This diff is large: its 9 lines aren’t shown here.

crates/markdown/src/markdown.rs+1−1

This diff is large: its 9 lines aren’t shown here.

crates/markdown_preview/src/markdown_parser.rs+4−4

This diff is large: its 30 lines aren’t shown here.

crates/multi_buffer/src/multi_buffer.rs+22−15

This diff is large: its 130 lines aren’t shown here.

crates/open_ai/src/open_ai.rs+28−247

This diff is large: its 371 lines aren’t shown here.

crates/open_router/src/open_router.rs+119−4

This diff is large: its 183 lines aren’t shown here.

crates/outline_panel/src/outline_panel.rs+6−4

This diff is large: its 24 lines aren’t shown here.

crates/paths/src/paths.rs+66−22

This diff is large: its 112 lines aren’t shown here.

crates/picker/src/picker.rs+3−4

This diff is large: its 26 lines aren’t shown here.

crates/prettier/src/prettier.rs+2−3

This diff is large: its 17 lines aren’t shown here.

crates/prettier/src/prettier_server.js+19−50

This diff is large: its 154 lines aren’t shown here.

crates/project/Cargo.toml+0−1

This diff is large: its 8 lines aren’t shown here.

crates/project/src/debugger/breakpoint_store.rs+1−1

This diff is large: its 9 lines aren’t shown here.

crates/project/src/debugger/dap_store.rs+48−18

This diff is large: its 144 lines aren’t shown here.

crates/project/src/debugger/locators/cargo.rs+14−7

This diff is large: its 62 lines aren’t shown here.

crates/project/src/debugger/locators/go.rs+299−307

This diff is large: its 703 lines aren’t shown here.

crates/project/src/debugger/locators/node.rs+28−14

This diff is large: its 83 lines aren’t shown here.

crates/project/src/debugger/locators/python.rs+4−4

This diff is large: its 26 lines aren’t shown here.

crates/project/src/debugger/session.rs+178−88

This diff is large: its 511 lines aren’t shown here.

crates/project/src/direnv.rs+19−11

This diff is large: its 62 lines aren’t shown here.

crates/project/src/environment.rs+8−2

This diff is large: its 24 lines aren’t shown here.

crates/project/src/git_store/git_traversal.rs+1−1

This diff is large: its 9 lines aren’t shown here.

crates/project/src/lsp_command.rs+41−44

This diff is large: its 107 lines aren’t shown here.

crates/project/src/lsp_store.rs+155−124

This diff is large: its 490 lines aren’t shown here.

crates/project/src/lsp_store/rust_analyzer_ext.rs+24−47

This diff is large: its 99 lines aren’t shown here.

crates/project/src/project.rs+9−2

This diff is large: its 63 lines aren’t shown here.

crates/project/src/project_tests.rs+109−100

This diff is large: its 441 lines aren’t shown here.

crates/project/src/task_inventory.rs+434−205

This diff is large: its 944 lines aren’t shown here.

crates/project/src/task_store.rs+4−2

This diff is large: its 34 lines aren’t shown here.

crates/project/src/toolchain_store.rs+1−1

This diff is large: its 9 lines aren’t shown here.

crates/project_panel/src/project_panel.rs+84−39

This diff is large: its 229 lines aren’t shown here.

crates/project_panel/src/project_panel_settings.rs+5−0

This diff is large: its 19 lines aren’t shown here.

crates/project_panel/src/project_panel_tests.rs+652−124

This diff is large: its 946 lines aren’t shown here.

crates/prompt_store/src/prompts.rs+3−0

This diff is large: its 24 lines aren’t shown here.

crates/proto/proto/call.proto+2−0

This diff is large: its 9 lines aren’t shown here.

crates/proto/proto/core.proto+3−1

This diff is large: its 19 lines aren’t shown here.

crates/proto/proto/debugger.proto+2−1

This diff is large: its 13 lines aren’t shown here.

crates/recent_projects/src/remote_servers.rs+1−1

This diff is large: its 9 lines aren’t shown here.

crates/remote_server/src/headless_project.rs+1−0

This diff is large: its 8 lines aren’t shown here.

crates/remote_server/src/main.rs+8−0

This diff is large: its 22 lines aren’t shown here.

crates/remote_server/src/remote_editing_tests.rs+2−2

This diff is large: its 18 lines aren’t shown here.

crates/repl/src/notebook/cell.rs+4−1

This diff is large: its 12 lines aren’t shown here.

crates/repl/src/notebook/notebook_ui.rs+2−2

This diff is large: its 18 lines aren’t shown here.

crates/repl/src/outputs/plain.rs+1−1

This diff is large: its 9 lines aren’t shown here.

crates/rope/src/chunk.rs+2−2

This diff is large: its 18 lines aren’t shown here.

crates/rope/src/rope.rs+6−6

This diff is large: its 38 lines aren’t shown here.

crates/search/src/project_search.rs+16−6

This diff is large: its 48 lines aren’t shown here.

crates/semantic_index/src/project_index_debug_view.rs+3−4

This diff is large: its 30 lines aren’t shown here.

crates/semantic_index/src/semantic_index.rs+2−2

This diff is large: its 18 lines aren’t shown here.

crates/settings/src/settings_store.rs+127−16

This diff is large: its 184 lines aren’t shown here.

crates/settings/src/vscode_import.rs+41−8

This diff is large: its 77 lines aren’t shown here.

crates/settings_ui/Cargo.toml+4−4

This diff is large: its 16 lines aren’t shown here.

crates/settings_ui/src/settings_ui.rs+25−23

This diff is large: its 76 lines aren’t shown here.

crates/storybook/src/stories/auto_height_editor.rs+1−1

This diff is large: its 9 lines aren’t shown here.

crates/sum_tree/src/sum_tree.rs+1−1

This diff is large: its 9 lines aren’t shown here.

crates/task/src/lib.rs+15−0

This diff is large: its 22 lines aren’t shown here.

crates/task/src/vscode_debug_format.rs+38−26

This diff is large: its 128 lines aren’t shown here.

crates/tasks_ui/src/modal.rs+20−4

This diff is large: its 46 lines aren’t shown here.

crates/tasks_ui/src/tasks_ui.rs+36−32

This diff is large: its 103 lines aren’t shown here.

crates/terminal/Cargo.toml+1−0

This diff is large: its 8 lines aren’t shown here.

crates/terminal/src/terminal.rs+17−1

This diff is large: its 32 lines aren’t shown here.

crates/terminal/src/terminal_settings.rs+1−1

This diff is large: its 9 lines aren’t shown here.

crates/terminal_view/src/terminal_element.rs+101−51

This diff is large: its 289 lines aren’t shown here.

crates/terminal_view/src/terminal_view.rs+67−12

This diff is large: its 124 lines aren’t shown here.

crates/text/src/text.rs+1−1

This diff is large: its 9 lines aren’t shown here.

crates/theme/src/default_colors.rs+4−10

This diff is large: its 28 lines aren’t shown here.

crates/theme/src/fallback_themes.rs+2−17

This diff is large: its 26 lines aren’t shown here.

crates/theme/src/schema.rs+18−30

This diff is large: its 69 lines aren’t shown here.

crates/theme/src/styles/colors.rs+2−5

This diff is large: its 15 lines aren’t shown here.

crates/title_bar/src/platforms/platform_linux.rs+164−4

This diff is large: its 178 lines aren’t shown here.

crates/title_bar/src/platforms/platform_windows.rs+14−25

This diff is large: its 100 lines aren’t shown here.

crates/title_bar/src/title_bar.rs+1−1

This diff is large: its 16 lines aren’t shown here.

crates/title_bar/src/window_controls.rs+0−165

This diff is large: its 166 lines aren’t shown here.

crates/ui/src/components/callout.rs+163−95

This diff is large: its 304 lines aren’t shown here.

crates/util/Cargo.toml+3−0

This diff is large: its 18 lines aren’t shown here.

crates/util/src/shell_env.rs+68−243

This diff is large: its 341 lines aren’t shown here.

crates/util/src/util.rs+91−46

This diff is large: its 186 lines aren’t shown here.

crates/util_macros/src/util_macros.rs+16−9

This diff is large: its 42 lines aren’t shown here.

crates/vim/src/normal/scroll.rs+100−59

This diff is large: its 204 lines aren’t shown here.

crates/vim/test_data/test_scroll_jumps.json+12−0

This diff is large: its 13 lines aren’t shown here.

crates/watch/src/watch.rs+1−1

This diff is large: its 9 lines aren’t shown here.

crates/workspace/src/item.rs+21−6

This diff is large: its 73 lines aren’t shown here.

crates/workspace/src/pane.rs+220−17

This diff is large: its 423 lines aren’t shown here.

crates/workspace/src/workspace.rs+25−5

This diff is large: its 79 lines aren’t shown here.

crates/worktree/src/worktree.rs+5−5

This diff is large: its 33 lines aren’t shown here.

crates/zed/Cargo.toml+2−2

This diff is large: its 18 lines aren’t shown here.

crates/zed/src/main.rs+12−16

This diff is large: its 54 lines aren’t shown here.

crates/zed/src/zed.rs+18−11

This diff is large: its 94 lines aren’t shown here.

crates/zed/src/zed/app_menus.rs+24−0

This diff is large: its 31 lines aren’t shown here.

crates/zed/src/zed/component_preview.rs+3−3

This diff is large: its 29 lines aren’t shown here.

crates/zed/src/zed/quick_action_bar.rs+0−41

This diff is large: its 55 lines aren’t shown here.

crates/zeta/src/zeta.rs+1−0

This diff is large: its 8 lines aren’t shown here.

docs/book.toml+1−0

This diff is large: its 8 lines aren’t shown here.

docs/src/SUMMARY.md+1−1

This diff is large: its 9 lines aren’t shown here.

docs/src/accounts.md+3−3

This diff is large: its 12 lines aren’t shown here.

docs/src/ai/configuration.md+2−2

This diff is large: its 18 lines aren’t shown here.

docs/src/ai/mcp.md+17−7

This diff is large: its 44 lines aren’t shown here.

docs/src/ai/text-threads.md+1−1

This diff is large: its 9 lines aren’t shown here.

docs/src/configuring-zed.md+2−1

This diff is large: its 10 lines aren’t shown here.

docs/src/debugger.md+140−23

This diff is large: its 194 lines aren’t shown here.

docs/src/development/linux.md+0−66

This diff is large: its 70 lines aren’t shown here.

docs/src/extensions.md+1−0

This diff is large: its 5 lines aren’t shown here.

docs/src/extensions/context-servers.md+0−39

This diff is large: its 40 lines aren’t shown here.

docs/src/extensions/developing-extensions.md+1−1

This diff is large: its 9 lines aren’t shown here.

docs/src/extensions/mcp-extensions.md+44−0

This diff is large: its 45 lines aren’t shown here.

docs/src/languages/ruby.md+1−27

This diff is large: its 39 lines aren’t shown here.

docs/src/languages/sql.md+1−1

This diff is large: its 9 lines aren’t shown here.

docs/src/linux.md+80−5

This diff is large: its 114 lines aren’t shown here.

script/check-links+32−0

This diff is large: its 33 lines aren’t shown here.

tooling/workspace-hack/Cargo.toml+4−0

This diff is large: its 32 lines aren’t shown here.