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 @@
1−name: Bug Report (AI Related)
1+name: Bug Report (AI)
22 description: Zed Agent Panel Bugs
33 type: "Bug"
44 labels: ["ai"]
@@ -19,15 +19,14 @@ body: · 14 unmodified lines
1919 2.
2020 3.
2121
22− Actual Behavior:
23− Expected Behavior:
22+ **Expected Behavior**:
23+ **Actual Behavior**:
2424
2525 ### Model Provider Details
2626 - Provider: (Anthropic via ZedPro, Anthropic via API key, Copilot Chat, Mistral, OpenAI, etc)
2727 - Model Name:
2828 - Mode: (Agent Panel, Inline Assistant, Terminal Assistant or Text Threads)
29− - MCP Servers in-use:
30− - Other Details:
29+ - Other Details (MCPs, other settings, etc):
3130 validations:
3231 required: true
3332
.github/ISSUE_TEMPLATE/02_bug_edit_predictions.yml+0−36
@@ -1,36 +0,0 @@
1−name: Bug Report (Edit Predictions)
2−description: Zed Edit Predictions bugs
3−type: "Bug"
4−labels: ["ai", "inline completion", "zeta"]
5−title: "Edit Predictions: <a short description of the Edit Prediction bug>"
6−body:
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 @@
1−name: Bug Report (Git)
2−description: Zed Git-Related Bugs
3−type: "Bug"
4−labels: ["git"]
5−title: "Git: <a short description of the Git bug>"
6−body:
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
1919 2.
2020 3.
2121
22− Actual Behavior:
23− Expected Behavior:
22+ **Expected Behavior**:
23+ **Actual Behavior**:
2424
2525 validations:
2626 required: true
.github/ISSUE_TEMPLATE/10_bug_report.yml+4−2
@@ -18,14 +18,16 @@ body: · 17 unmodified lines
1818 - Issues with insufficient detail may be summarily closed.
1919 -->
2020
21+ DESCRIPTION_HERE
22+
2123 Steps to reproduce:
2224 1.
2325 2.
2426 3.
2527 4.
2628
27− Expected Behavior:
28− Actual Behavior:
29+ **Expected Behavior**:
30+ **Actual Behavior**:
2931
3032 <!-- Before Submitting, did you:
3133 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
2222 - name: Check for broken links
2323 uses: lycheeverse/lychee-action@82202e5e9c2f4ef1a55a3d02563e1cb6041e5332 # v2.4.1
2424 with:
25− args: --no-progress './docs/src/**/*'
25+ args: --no-progress --exclude '^http' './docs/src/**/*'
2626 fail: true
2727
2828 - name: Build book
.github/workflows/ci.yml+4−1
@@ -498,6 +498,7 @@ jobs: · 497 unmodified lines
498498 needs:
499499 - job_spec
500500 - style
501+ - check_docs
501502 - migration_checks
502503 # run_tests: If adding required tests, add them here and to script below.
503504 - workspace_hack
@@ -515,7 +516,8 @@ jobs: · 11 unmodified lines
515516 # Check dependent jobs...
516517 RET_CODE=0
517518 # 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"; }
519521
520522 # Only check test jobs if they were supposed to run
521523 if [[ "${{ needs.job_spec.outputs.run_tests }}" == "true" ]]; then
@@ -801,6 +803,7 @@ jobs: · 279 unmodified lines
801803 name: Build with Nix
802804 uses: ./.github/workflows/nix.yml
803805 if: github.repository_owner == 'zed-industries' && contains(github.event.pull_request.labels.*.name, 'run-nix')
806+ secrets: inherit
804807 with:
805808 flake-output: debug
806809 # excludes the final package to only cache dependencies
.github/workflows/eval.yml+1−0
@@ -30,6 +30,7 @@ jobs: · 29 unmodified lines
3030 noop:
3131 name: No-op
3232 runs-on: ubuntu-latest
33+ if: github.repository_owner == 'zed-industries'
3334 steps:
3435 - name: No-op
3536 run: echo "Nothing to do"
.github/workflows/release_nightly.yml+1−0
@@ -214,6 +214,7 @@ jobs: · 213 unmodified lines
214214 bundle-nix:
215215 name: Build and cache Nix package
216216 needs: tests
217+ secrets: inherit
217218 uses: ./.github/workflows/nix.yml
218219
219220 update-nightly-tag:
.github/workflows/unit_evals.yml+2−1
@@ -19,6 +19,7 @@ env: · 18 unmodified lines
1919
2020 jobs:
2121 unit_evals:
22+ if: github.repository_owner == 'zed-industries'
2223 timeout-minutes: 60
2324 name: Run unit evals
2425 runs-on:
@@ -62,7 +63,7 @@ jobs: · 37 unmodified lines
6263
6364 - name: Run unit evals
6465 shell: bash -euxo pipefail {0}
65− run: cargo nextest run --workspace --no-fail-fast --features eval --no-capture -E 'test(::eval_)' --test-threads 1
66+ run: cargo nextest run --workspace --no-fail-fast --features eval --no-capture -E 'test(::eval_)'
6667 env:
6768 ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
6869
Cargo.lock+242−13
@@ -99,6 +99,7 @@ dependencies = [ · 98 unmodified lines
9999 "paths",
100100 "picker",
101101 "postage",
102+ "pretty_assertions",
102103 "project",
103104 "prompt_store",
104105 "proto",
@@ -490,7 +491,6 @@ dependencies = [ · 385 unmodified lines
490491 "anyhow",
491492 "futures 0.3.31",
492493 "gpui",
493− "shlex",
494494 "smol",
495495 "tempfile",
496496 "util",
@@ -704,6 +704,7 @@ dependencies = [ · 207 unmodified lines
704704 "serde_json",
705705 "settings",
706706 "smallvec",
707+ "smol",
707708 "streaming_diff",
708709 "strsim",
709710 "task",
@@ -2821,9 +2822,11 @@ dependencies = [ · 2111 unmodified lines
28212822 "collections",
28222823 "credentials_provider",
28232824 "feature_flags",
2825+ "fs",
28242826 "futures 0.3.31",
28252827 "gpui",
28262828 "gpui_tokio",
2829+ "hickory-resolver",
28272830 "http_client",
28282831 "http_client_tls",
28292832 "httparse",
@@ -2832,6 +2835,7 @@ dependencies = [ · 2 unmodified lines
28322835 "paths",
28332836 "postage",
28342837 "rand 0.8.5",
2838+ "regex",
28352839 "release_channel",
28362840 "rpc",
28372841 "rustls-pki-types",
@@ -3159,6 +3163,16 @@ dependencies = [ · 321 unmodified lines
31593163 "memchr",
31603164 ]
31613165
3166+[[package]]
3167+name = "command-fds"
3168+version = "0.3.1"
3169+source = "registry+https://github.com/rust-lang/crates.io-index"
3170+checksum = "2ec1052629a80c28594777d1252efc8a6b005d13f9edfd8c3fc0f44d5b32489a"
3171+dependencies = [
3172+ "nix 0.30.1",
3173+ "thiserror 2.0.12",
3174+]
3175+
31623176 [[package]]
31633177 name = "command_palette"
31643178 version = "0.1.0"
@@ -3528,6 +3542,20 @@ dependencies = [ · 363 unmodified lines
35283542 "coreaudio-sys",
35293543 ]
35303544
3545+[[package]]
3546+name = "coreaudio-rs"
3547+version = "0.13.0"
3548+source = "registry+https://github.com/rust-lang/crates.io-index"
3549+checksum = "1aae284fbaf7d27aa0e292f7677dfbe26503b0d555026f702940805a630eac17"
3550+dependencies = [
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+
35313559 [[package]]
35323560 name = "coreaudio-sys"
35333561 version = "0.2.16"
@@ -3563,7 +3591,8 @@ dependencies = [ · 29 unmodified lines
35633591 [[package]]
35643592 name = "cpal"
35653593 version = "0.15.3"
3566−source = "git+https://github.com/zed-industries/cpal?rev=fd8bc2fd39f1f5fdee5a0690656caff9a26d9d50#fd8bc2fd39f1f5fdee5a0690656caff9a26d9d50"
3594+source = "registry+https://github.com/rust-lang/crates.io-index"
3595+checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779"
35673596 dependencies = [
35683597 "alsa",
35693598 "core-foundation-sys",
@@ -3573,7 +3602,7 @@ dependencies = [ · 3 unmodified lines
35733602 "js-sys",
35743603 "libc",
35753604 "mach2",
3576− "ndk",
3605+ "ndk 0.8.0",
35773606 "ndk-context",
35783607 "oboe",
35793608 "wasm-bindgen",
@@ -3582,6 +3611,32 @@ dependencies = [ · 2 unmodified lines
35823611 "windows 0.54.0",
35833612 ]
35843613
3614+[[package]]
3615+name = "cpal"
3616+version = "0.16.0"
3617+source = "registry+https://github.com/rust-lang/crates.io-index"
3618+checksum = "cbd307f43cc2a697e2d1f8bc7a1d824b5269e052209e28883e5bc04d095aaa3f"
3619+dependencies = [
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+
35853640 [[package]]
35863641 name = "cpp_demangle"
35873642 version = "0.4.4"
@@ -4015,6 +4070,7 @@ dependencies = [ · 427 unmodified lines
40154070 "gpui",
40164071 "http_client",
40174072 "language",
4073+ "libc",
40184074 "log",
40194075 "node_runtime",
40204076 "parking_lot",
@@ -4038,7 +4094,7 @@ dependencies = [ · 17 unmodified lines
40384094 [[package]]
40394095 name = "dap-types"
40404096 version = "0.0.1"
4041−source = "git+https://github.com/zed-industries/dap-types?rev=68516de327fa1be15214133a0a2e52a12982ce75#68516de327fa1be15214133a0a2e52a12982ce75"
4097+source = "git+https://github.com/zed-industries/dap-types?rev=b40956a7f4d1939da67429d941389ee306a3a308#b40956a7f4d1939da67429d941389ee306a3a308"
40424098 dependencies = [
40434099 "schemars",
40444100 "serde",
@@ -4051,6 +4107,7 @@ version = "0.1.0" · 6 unmodified lines
40514107 dependencies = [
40524108 "anyhow",
40534109 "async-trait",
4110+ "collections",
40544111 "dap",
40554112 "futures 0.3.31",
40564113 "gpui",
@@ -4682,7 +4739,6 @@ dependencies = [ · 625 unmodified lines
46824739 "client",
46834740 "clock",
46844741 "collections",
4685− "command_palette_hooks",
46864742 "convert_case 0.8.0",
46874743 "ctor",
46884744 "dap",
@@ -4851,6 +4907,18 @@ version = "1.1.0" · 162 unmodified lines
48514907 source = "registry+https://github.com/rust-lang/crates.io-index"
48524908 checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf"
48534909
4910+[[package]]
4911+name = "enum-as-inner"
4912+version = "0.6.1"
4913+source = "registry+https://github.com/rust-lang/crates.io-index"
4914+checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc"
4915+dependencies = [
4916+ "heck 0.5.0",
4917+ "proc-macro2",
4918+ "quote",
4919+ "syn 2.0.101",
4920+]
4921+
48544922 [[package]]
48554923 name = "enumflags2"
48564924 version = "0.7.11"
@@ -6108,6 +6176,7 @@ dependencies = [ · 1251 unmodified lines
61086176 "anyhow",
61096177 "askpass",
61106178 "buffer_diff",
6179+ "call",
61116180 "chrono",
61126181 "collections",
61136182 "command_palette_hooks",
@@ -7428,6 +7497,51 @@ version = "0.2.1" · 1314 unmodified lines
74287497 source = "registry+https://github.com/rust-lang/crates.io-index"
74297498 checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df"
74307499
7500+[[package]]
7501+name = "hickory-proto"
7502+version = "0.24.4"
7503+source = "registry+https://github.com/rust-lang/crates.io-index"
7504+checksum = "92652067c9ce6f66ce53cc38d1169daa36e6e7eb7dd3b63b5103bd9d97117248"
7505+dependencies = [
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]]
7525+name = "hickory-resolver"
7526+version = "0.24.4"
7527+source = "registry+https://github.com/rust-lang/crates.io-index"
7528+checksum = "cbb117a1ca520e111743ab2f6688eddee69db4e0ea242545a604dce8a66fd22e"
7529+dependencies = [
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+
74317545 [[package]]
74327546 name = "hidden-trait"
74337547 version = "0.1.2"
@@ -8297,6 +8411,18 @@ dependencies = [ · 863 unmodified lines
82978411 "windows 0.58.0",
82988412 ]
82998413
8414+[[package]]
8415+name = "ipconfig"
8416+version = "0.3.2"
8417+source = "registry+https://github.com/rust-lang/crates.io-index"
8418+checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f"
8419+dependencies = [
8420+ "socket2",
8421+ "widestring",
8422+ "windows-sys 0.48.0",
8423+ "winreg 0.50.0",
8424+]
8425+
83008426 [[package]]
83018427 name = "ipnet"
83028428 version = "2.11.0"
@@ -8990,7 +9116,6 @@ dependencies = [ · 687 unmodified lines
89909116 "tree-sitter-yaml",
89919117 "unindent",
89929118 "util",
8993− "which 6.0.3",
89949119 "workspace",
89959120 "workspace-hack",
89969121 ]
@@ -9182,6 +9307,12 @@ dependencies = [ · 185 unmodified lines
91829307 "cc",
91839308 ]
91849309
9310+[[package]]
9311+name = "linked-hash-map"
9312+version = "0.5.6"
9313+source = "registry+https://github.com/rust-lang/crates.io-index"
9314+checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
9315+
91859316 [[package]]
91869317 name = "linkify"
91879318 version = "0.10.0"
@@ -9308,7 +9439,7 @@ dependencies = [ · 120 unmodified lines
93089439 "core-foundation 0.10.0",
93099440 "core-video",
93109441 "coreaudio-rs 0.12.1",
9311− "cpal",
9442+ "cpal 0.16.0",
93129443 "futures 0.3.31",
93139444 "gpui",
93149445 "gpui_tokio",
@@ -9442,6 +9573,15 @@ dependencies = [ · 127 unmodified lines
94429573 "hashbrown 0.15.3",
94439574 ]
94449575
9576+[[package]]
9577+name = "lru-cache"
9578+version = "0.1.2"
9579+source = "registry+https://github.com/rust-lang/crates.io-index"
9580+checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c"
9581+dependencies = [
9582+ "linked-hash-map",
9583+]
9584+
94459585 [[package]]
94469586 name = "lsp"
94479587 version = "0.1.0"
@@ -10078,7 +10218,21 @@ dependencies = [ · 630 unmodified lines
1007810218 "bitflags 2.9.0",
1007910219 "jni-sys",
1008010220 "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]]
10227+name = "ndk"
10228+version = "0.9.0"
10229+source = "registry+https://github.com/rust-lang/crates.io-index"
10230+checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4"
10231+dependencies = [
10232+ "bitflags 2.9.0",
10233+ "jni-sys",
10234+ "log",
10235+ "ndk-sys 0.6.0+11769913",
1008210236 "num_enum",
1008310237 "thiserror 1.0.69",
1008410238 ]
@@ -10098,6 +10252,15 @@ dependencies = [ · 13 unmodified lines
1009810252 "jni-sys",
1009910253 ]
1010010254
10255+[[package]]
10256+name = "ndk-sys"
10257+version = "0.6.0+11769913"
10258+source = "registry+https://github.com/rust-lang/crates.io-index"
10259+checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873"
10260+dependencies = [
10261+ "jni-sys",
10262+]
10263+
1010110264 [[package]]
1010210265 name = "new_debug_unreachable"
1010310266 version = "1.0.6"
@@ -10129,6 +10292,18 @@ dependencies = [ · 25 unmodified lines
1012910292 "memoffset",
1013010293 ]
1013110294
10295+[[package]]
10296+name = "nix"
10297+version = "0.30.1"
10298+source = "registry+https://github.com/rust-lang/crates.io-index"
10299+checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
10300+dependencies = [
10301+ "bitflags 2.9.0",
10302+ "cfg-if",
10303+ "cfg_aliases 0.2.1",
10304+ "libc",
10305+]
10306+
1013210307 [[package]]
1013310308 name = "node_runtime"
1013410309 version = "0.1.0"
@@ -10499,6 +10674,43 @@ dependencies = [ · 364 unmodified lines
1049910674 "objc2-quartz-core",
1050010675 ]
1050110676
10677+[[package]]
10678+name = "objc2-audio-toolbox"
10679+version = "0.3.1"
10680+source = "registry+https://github.com/rust-lang/crates.io-index"
10681+checksum = "10cbe18d879e20a4aea544f8befe38bcf52255eb63d3f23eca2842f3319e4c07"
10682+dependencies = [
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]]
10693+name = "objc2-core-audio"
10694+version = "0.3.1"
10695+source = "registry+https://github.com/rust-lang/crates.io-index"
10696+checksum = "ca44961e888e19313b808f23497073e3f6b3c22bb485056674c8b49f3b025c82"
10697+dependencies = [
10698+ "dispatch2",
10699+ "objc2",
10700+ "objc2-core-audio-types",
10701+ "objc2-core-foundation",
10702+]
10703+
10704+[[package]]
10705+name = "objc2-core-audio-types"
10706+version = "0.3.1"
10707+source = "registry+https://github.com/rust-lang/crates.io-index"
10708+checksum = "c0f1cc99bb07ad2ddb6527ddf83db6a15271bb036b3eb94b801cd44fdc666ee1"
10709+dependencies = [
10710+ "bitflags 2.9.0",
10711+ "objc2",
10712+]
10713+
1050210714 [[package]]
1050310715 name = "objc2-core-foundation"
1050410716 version = "0.3.1"
@@ -10604,7 +10816,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" · 99 unmodified lines
1060410816 checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb"
1060510817 dependencies = [
1060610818 "jni",
10607− "ndk",
10819+ "ndk 0.8.0",
1060810820 "ndk-context",
1060910821 "num-derive",
1061010822 "num-traits",
@@ -12109,7 +12321,6 @@ dependencies = [ · 1498 unmodified lines
1210912321 "unindent",
1211012322 "url",
1211112323 "util",
12112− "uuid",
1211312324 "which 6.0.3",
1211412325 "workspace-hack",
1211512326 "worktree",
@@ -13202,6 +13413,7 @@ dependencies = [ · 1086 unmodified lines
1320213413 "futures-core",
1320313414 "futures-util",
1320413415 "h2 0.4.9",
13416+ "hickory-resolver",
1320513417 "http 1.3.1",
1320613418 "http-body 1.0.1",
1320713419 "http-body-util",
@@ -13258,6 +13470,12 @@ dependencies = [ · 50 unmodified lines
1325813470 "workspace-hack",
1325913471 ]
1326013472
13473+[[package]]
13474+name = "resolv-conf"
13475+version = "0.7.4"
13476+source = "registry+https://github.com/rust-lang/crates.io-index"
13477+checksum = "95325155c684b1c89f7765e30bc1c42e4a6da51ca513615660cb8a62ef9a88e3"
13478+
1326113479 [[package]]
1326213480 name = "resvg"
1326313481 version = "0.45.1"
@@ -13377,7 +13595,7 @@ version = "0.20.1" · 113 unmodified lines
1337713595 source = "registry+https://github.com/rust-lang/crates.io-index"
1337813596 checksum = "e7ceb6607dd738c99bc8cb28eff249b7cd5c8ec88b9db96c0608c1480d140fb1"
1337913597 dependencies = [
13380− "cpal",
13598+ "cpal 0.15.3",
1338113599 "hound",
1338213600 ]
1338313601
@@ -14389,12 +14607,12 @@ dependencies = [ · 1005 unmodified lines
1438914607 "fs",
1439014608 "gpui",
1439114609 "log",
14392− "paths",
1439314610 "schemars",
1439414611 "serde",
1439514612 "settings",
1439614613 "theme",
1439714614 "ui",
14615+ "util",
1439814616 "workspace",
1439914617 "workspace-hack",
1440014618 ]
@@ -15725,6 +15943,7 @@ dependencies = [ · 1324 unmodified lines
1572515943 "theme",
1572615944 "thiserror 2.0.12",
1572715945 "url",
15946+ "urlencoding",
1572815947 "util",
1572915948 "windows 0.61.1",
1573015949 "workspace-hack",
@@ -17121,6 +17340,7 @@ dependencies = [ · 1390 unmodified lines
1712117340 "async-fs",
1712217341 "async_zip",
1712317342 "collections",
17343+ "command-fds",
1712417344 "dirs 4.0.0",
1712517345 "dunce",
1712617346 "futures 0.3.31",
@@ -17131,12 +17351,14 @@ dependencies = [ · 4 unmodified lines
1713117351 "itertools 0.14.0",
1713217352 "libc",
1713317353 "log",
17354+ "nix 0.29.0",
1713417355 "rand 0.8.5",
1713517356 "regex",
1713617357 "rust-embed",
1713717358 "serde",
1713817359 "serde_json",
1713917360 "serde_json_lenient",
17361+ "shlex",
1714017362 "smol",
1714117363 "take-until",
1714217364 "tempfile",
@@ -18178,6 +18400,12 @@ dependencies = [ · 1035 unmodified lines
1817818400 "wasite",
1817918401 ]
1818018402
18403+[[package]]
18404+name = "widestring"
18405+version = "1.2.0"
18406+source = "registry+https://github.com/rust-lang/crates.io-index"
18407+checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d"
18408+
1818118409 [[package]]
1818218410 name = "wiggle"
1818318411 version = "29.0.1"
@@ -19298,6 +19526,7 @@ dependencies = [ · 1114 unmodified lines
1929819526 "num-rational",
1929919527 "num-traits",
1930019528 "objc2",
19529+ "objc2-core-foundation",
1930119530 "objc2-foundation",
1930219531 "objc2-metal",
1930319532 "object",
@@ -19718,7 +19947,7 @@ dependencies = [ · 414 unmodified lines
1971819947
1971919948 [[package]]
1972019949 name = "zed"
19721−version = "0.191.0"
19950+version = "0.192.0"
1972219951 dependencies = [
1972319952 "activity_indicator",
1972419953 "agent",
Cargo.toml+5−3
@@ -433,9 +433,10 @@ convert_case = "0.8.0" · 432 unmodified lines
433433 core-foundation = "0.10.0"
434434 core-foundation-sys = "0.8.6"
435435 core-video = { version = "0.4.3", features = ["metal"] }
436+cpal = "0.16"
436437 criterion = { version = "0.5", features = ["html_reports"] }
437438 ctor = "0.4.0"
438−dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "68516de327fa1be15214133a0a2e52a12982ce75" }
439+dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "b40956a7f4d1939da67429d941389ee306a3a308" }
439440 dashmap = "6.0"
440441 derive_more = "0.99.17"
441442 dirs = "4.0"
@@ -523,6 +524,7 @@ reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "951c77 · 81 unmodified lines
523524 "rustls-tls-native-roots",
524525 "socks",
525526 "stream",
527+ "hickory-dns",
526528 ] }
527529 rsa = "0.9.6"
528530 runtimelib = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734", default-features = false, features = [
@@ -682,9 +684,7 @@ features = [ · 153 unmodified lines
682684 "Win32_UI_WindowsAndMessaging",
683685 ]
684686
685−# TODO livekit https://github.com/RustAudio/cpal/pull/891
686687 [patch.crates-io]
687−cpal = { git = "https://github.com/zed-industries/cpal", rev = "fd8bc2fd39f1f5fdee5a0690656caff9a26d9d50" }
688688 notify = { git = "https://github.com/zed-industries/notify.git", rev = "bbb9ea5ae52b253e095737847e367c30653a2e96" }
689689 notify-types = { git = "https://github.com/zed-industries/notify.git", rev = "bbb9ea5ae52b253e095737847e367c30653a2e96" }
690690
@@ -698,6 +698,8 @@ codegen-units = 16 · 7 unmodified lines
698698 [profile.dev.package]
699699 taffy = { opt-level = 3 }
700700 cranelift-codegen = { opt-level = 3 }
701+cranelift-codegen-meta = { opt-level = 3 }
702+cranelift-codegen-shared = { opt-level = 3 }
701703 resvg = { opt-level = 3 }
702704 rustybuzz = { opt-level = 3 }
703705 ttf-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
115115 "ctrl-\"": "editor::ExpandAllDiffHunks",
116116 "ctrl-i": "editor::ShowSignatureHelp",
117117 "alt-g b": "git::Blame",
118+ "alt-g m": "git::OpenModifiedFiles",
118119 "menu": "editor::OpenContextMenu",
119120 "shift-f10": "editor::OpenContextMenu",
120121 "ctrl-shift-e": "editor::ToggleEditPrediction",
assets/keymaps/default-macos.json+1−0
@@ -139,6 +139,7 @@ · 138 unmodified lines
139139 "cmd-'": "editor::ToggleSelectedDiffHunks",
140140 "cmd-\"": "editor::ExpandAllDiffHunks",
141141 "cmd-alt-g b": "git::Blame",
142+ "cmd-alt-g m": "git::OpenModifiedFiles",
142143 "cmd-i": "editor::ShowSignatureHelp",
143144 "f9": "editor::ToggleBreakpoint",
144145 "shift-f9": "editor::EditLogBreakpoint",
assets/keymaps/vim.json+7−1
@@ -184,6 +184,8 @@ · 183 unmodified lines
184184 "z f": "editor::FoldSelectedRanges",
185185 "z shift-m": "editor::FoldAll",
186186 "z shift-r": "editor::UnfoldAll",
187+ "z l": "vim::ColumnRight",
188+ "z h": "vim::ColumnLeft",
187189 "shift-z shift-q": ["pane::CloseActiveItem", { "save_intent": "skip" }],
188190 "shift-z shift-z": ["pane::CloseActiveItem", { "save_intent": "save_all" }],
189191 // Count support
@@ -395,6 +397,8 @@ · 205 unmodified lines
395397 "ctrl-pagedown": "pane::ActivateNextItem",
396398 "ctrl-pageup": "pane::ActivatePreviousItem",
397399 "insert": "vim::InsertBefore",
400+ ".": "vim::Repeat",
401+ "alt-.": "vim::RepeatFind",
398402 // tree-sitter related commands
399403 "[ x": "editor::SelectLargerSyntaxNode",
400404 "] x": "editor::SelectSmallerSyntaxNode",
@@ -421,6 +425,7 @@ · 20 unmodified lines
421425
422426 "x": "editor::SelectLine",
423427 "shift-x": "editor::SelectLine",
428+ "%": "editor::SelectAll",
424429 // Window mode
425430 "space w h": "workspace::ActivatePaneLeft",
426431 "space w l": "workspace::ActivatePaneRight",
@@ -450,7 +455,8 @@ · 23 unmodified lines
450455 "ctrl-c": "editor::ToggleComments",
451456 "d": "vim::HelixDelete",
452457 "c": "vim::Substitute",
453− "shift-c": "editor::AddSelectionBelow"
458+ "shift-c": "editor::AddSelectionBelow",
459+ "alt-shift-c": "editor::AddSelectionAbove"
454460 }
455461 },
456462 {
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
2727 If appropriate, use tool calls to explore the current project, which contains the following root directories:
2828
2929 {{#each worktrees}}
30−- `{{root_name}}`
30+- `{{abs_path}}`
3131 {{/each}}
3232
3333 - 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.
3535 - Before you read or edit a file, you must first find the full path. DO NOT ever guess a file path!
3636 {{# if (has_tool 'grep') }}
3737 - When looking for symbols in the project, prefer the `grep` tool.
assets/settings/default.json+16−3
@@ -307,6 +307,8 @@ · 306 unmodified lines
307307 // "all"
308308 // 4. Draw whitespaces at boundaries only:
309309 // "boundary"
310+ // 5. Draw whitespaces only after non-whitespace characters:
311+ // "trailing"
310312 // For a whitespace to be on a boundary, any of the following conditions need to be met:
311313 // - It is a tab
312314 // - It is adjacent to an edge (start or end)
@@ -445,7 +447,9 @@ · 132 unmodified lines
445447 // Whether to show breakpoints in the gutter.
446448 "breakpoints": true,
447449 // Whether to show fold buttons in the gutter.
448− "folds": true
450+ "folds": true,
451+ // Minimum number of characters to reserve space for in the gutter.
452+ "min_line_number_digits": 4
449453 },
450454 "indent_guides": {
451455 // Whether to show indent guides in the editor.
@@ -604,7 +608,9 @@ · 152 unmodified lines
604608 // 2. Never show indent guides:
605609 // "never"
606610 "show": "always"
607− }
611+ },
612+ // Whether to hide the root entry when only one folder is open in the window.
613+ "hide_root": false
608614 },
609615 "outline_panel": {
610616 // Whether to show the outline panel button in the status bar
@@ -1476,7 +1482,8 @@ · 865 unmodified lines
14761482 "Go": {
14771483 "code_actions_on_format": {
14781484 "source.organizeImports": true
1479− }
1485+ },
1486+ "debuggers": ["Delve"]
14801487 },
14811488 "GraphQL": {
14821489 "prettier": {
@@ -1541,9 +1548,15 @@ · 58 unmodified lines
15411548 "Plain Text": {
15421549 "allow_rewrap": "anywhere"
15431550 },
1551+ "Python": {
1552+ "debuggers": ["Debugpy"]
1553+ },
15441554 "Ruby": {
15451555 "language_servers": ["solargraph", "!ruby-lsp", "!rubocop", "!sorbet", "!steep", "..."]
15461556 },
1557+ "Rust": {
1558+ "debuggers": ["CodeLLDB"]
1559+ },
15471560 "SCSS": {
15481561 "prettier": {
15491562 "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.
22 //
33 // Example:
44 [
assets/themes/one/one.json+2−0
@@ -99,6 +99,8 @@ · 98 unmodified lines
9999 "version_control.added": "#27a657ff",
100100 "version_control.modified": "#d3b020ff",
101101 "version_control.deleted": "#e06c76ff",
102+ "version_control.conflict_marker.ours": "#a1c1811a",
103+ "version_control.conflict_marker.theirs": "#74ade81a",
102104 "conflict": "#dec184ff",
103105 "conflict.background": "#dec1841a",
104106 "conflict.border": "#5d4c2fff",
crates/activity_indicator/src/activity_indicator.rs+166−66
@@ -7,7 +7,10 @@ use gpui::{ · 6 unmodified lines
77 InteractiveElement as _, ParentElement as _, Render, SharedString, StatefulInteractiveElement,
88 Styled, Transformation, Window, actions, percentage,
99 };
10−use language::{BinaryStatus, LanguageRegistry, LanguageServerId};
10+use language::{
11+ BinaryStatus, LanguageRegistry, LanguageServerId, LanguageServerName,
12+ LanguageServerStatusUpdate, ServerHealth,
13+};
1114 use project::{
1215 EnvironmentErrorMessage, LanguageServerProgress, LspStoreEvent, Project,
1316 ProjectEnvironmentEvent,
@@ -16,6 +19,7 @@ use project::{ · 2 unmodified lines
1619 use smallvec::SmallVec;
1720 use std::{
1821 cmp::Reverse,
22+ collections::HashSet,
1923 fmt::Write,
2024 path::Path,
2125 sync::Arc,
@@ -30,9 +34,9 @@ const GIT_OPERATION_DELAY: Duration = Duration::from_millis(0); · 8 unmodified lines
3034 actions!(activity_indicator, [ShowErrorMessage]);
3135
3236 pub enum Event {
33− ShowError {
34− server_name: SharedString,
35− error: String,
37+ ShowStatus {
38+ server_name: LanguageServerName,
39+ status: SharedString,
3640 },
3741 }
3842
@@ -45,8 +49,8 @@ pub struct ActivityIndicator { · 6 unmodified lines
4549
4650 #[derive(Debug)]
4751 struct ServerStatus {
48− name: SharedString,
49− status: BinaryStatus,
52+ name: LanguageServerName,
53+ status: LanguageServerStatusUpdate,
5054 }
5155
5256 struct PendingWork<'a> {
@@ -145,19 +149,19 @@ impl ActivityIndicator { · 92 unmodified lines
145149 });
146150
147151 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+ } => {
149156 let create_buffer = project.update(cx, |project, cx| project.create_buffer(cx));
150157 let project = project.clone();
151− let error = error.clone();
158+ let status = status.clone();
152159 let server_name = server_name.clone();
153160 cx.spawn_in(window, async move |workspace, cx| {
154161 let buffer = create_buffer.await?;
155162 buffer.update(cx, |buffer, cx| {
156163 buffer.edit(
157− [(
158− 0..0,
159− format!("Language server error: {}\n\n{}", server_name, error),
160− )],
164+ [(0..0, format!("Language server {server_name}:\n\n{status}"))],
161165 None,
162166 cx,
163167 );
@@ -166,7 +170,10 @@ impl ActivityIndicator { · 2 unmodified lines
166170 workspace.update_in(cx, |workspace, window, cx| {
167171 workspace.add_item_to_active_pane(
168172 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
170177 })),
171178 None,
172179 true,
@@ -185,19 +192,34 @@ impl ActivityIndicator { · 12 unmodified lines
185192 }
186193
187194 fn show_error_message(&mut self, _: &ShowErrorMessage, _: &mut Window, cx: &mut Context<Self>) {
188− self.statuses.retain(|status| {
189− if let BinaryStatus::Failed { error } = &status.status {
190− cx.emit(Event::ShowError {
195+ let mut status_message_shown = false;
196+ self.statuses.retain(|status| match &status.status {
197+ LanguageServerStatusUpdate::Binary(BinaryStatus::Failed { error })
198+ if !status_message_shown =>
199+ {
200+ cx.emit(Event::ShowStatus {
191201 server_name: status.name.clone(),
192− error: error.clone(),
202+ status: SharedString::from(error),
193203 });
204+ status_message_shown = true;
194205 false
195− } else {
196− true
197206 }
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,
198222 });
199−
200− cx.notify();
201223 }
202224
203225 fn dismiss_error_message(
@@ -267,48 +289,52 @@ impl ActivityIndicator { · 63 unmodified lines
267289 });
268290 }
269291 // 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()
276292 {
277− let mut message = progress
278− .title
279− .as_deref()
280− .unwrap_or(progress_token)
281− .to_string();
282−
283− if let Some(percentage) = progress.percentage {
284− write!(&mut message, " ({}%)", percentage).unwrap();
285− }
293+ let mut pending_work = self.pending_language_server_work(cx);
294+ if let Some(PendingWork {
295+ progress_token,
296+ progress,
297+ ..
298+ }) = pending_work.next()
299+ {
300+ let mut message = progress
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+ }
286309
287− if let Some(progress_message) = progress.message.as_ref() {
288− message.push_str(": ");
289− message.push_str(progress_message);
290− }
310+ if let Some(progress_message) = progress.message.as_ref() {
311+ message.push_str(": ");
312+ message.push_str(progress_message);
313+ }
291314
292− let additional_work_count = pending_work.count();
293− if additional_work_count > 0 {
294− write!(&mut message, " + {} more", additional_work_count).unwrap();
295− }
315+ let additional_work_count = pending_work.count();
316+ if additional_work_count > 0 {
317+ write!(&mut message, " + {} more", additional_work_count).unwrap();
318+ }
296319
297− return Some(Content {
298− icon: Some(
299− Icon::new(IconName::ArrowCircle)
300− .size(IconSize::Small)
301− .with_animation(
302− "arrow-circle",
303− Animation::new(Duration::from_secs(2)).repeat(),
304− |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
305− )
306− .into_any_element(),
307− ),
308− message,
309− on_click: Some(Arc::new(Self::toggle_language_server_work_context_menu)),
310− tooltip_message: None,
311− });
320+ return Some(Content {
321+ icon: Some(
322+ Icon::new(IconName::ArrowCircle)
323+ .size(IconSize::Small)
324+ .with_animation(
325+ "arrow-circle",
326+ Animation::new(Duration::from_secs(2)).repeat(),
327+ |icon, delta| {
328+ icon.transform(Transformation::rotate(percentage(delta)))
329+ },
330+ )
331+ .into_any_element(),
332+ ),
333+ message,
334+ on_click: Some(Arc::new(Self::toggle_language_server_work_context_menu)),
335+ tooltip_message: None,
336+ });
337+ }
312338 }
313339
314340 if let Some(session) = self
@@ -369,14 +395,38 @@ impl ActivityIndicator { · 54 unmodified lines
369395 let mut downloading = SmallVec::<[_; 3]>::new();
370396 let mut checking_for_update = SmallVec::<[_; 3]>::new();
371397 let mut failed = SmallVec::<[_; 3]>::new();
398+ let mut health_messages = SmallVec::<[_; 3]>::new();
399+ let mut servers_to_clear_statuses = HashSet::<LanguageServerName>::default();
372400 for status in &self.statuses {
373− match status.status {
374− BinaryStatus::CheckingForUpdate => checking_for_update.push(status.name.clone()),
375− BinaryStatus::Downloading => downloading.push(status.name.clone()),
376− BinaryStatus::Failed { .. } => failed.push(status.name.clone()),
377− BinaryStatus::None => {}
401+ match &status.status {
402+ LanguageServerStatusUpdate::Binary(BinaryStatus::CheckingForUpdate) => {
403+ checking_for_update.push(status.name.clone());
404+ }
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+ },
378420 }
379421 }
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+ });
380430
381431 if !downloading.is_empty() {
382432 return Some(Content {
@@ -457,7 +507,7 @@ impl ActivityIndicator { · 74 unmodified lines
457507 }),
458508 ),
459509 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)
461511 })),
462512 tooltip_message: None,
463513 });
@@ -471,7 +521,7 @@ impl ActivityIndicator { · 7 unmodified lines
471521 .size(IconSize::Small)
472522 .into_any_element(),
473523 ),
474− message: format!("Formatting failed: {}. Click to see logs.", failure),
524+ message: format!("Formatting failed: {failure}. Click to see logs."),
475525 on_click: Some(Arc::new(|indicator, window, cx| {
476526 indicator.project.update(cx, |project, cx| {
477527 project.reset_last_formatting_failure(cx);
@@ -482,6 +532,56 @@ impl ActivityIndicator { · 4 unmodified lines
482532 });
483533 }
484534
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+
485585 // Show any application auto-update info.
486586 if let Some(updater) = &self.auto_updater {
487587 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
109109 indoc.workspace = true
110110 language = { workspace = true, "features" = ["test-support"] }
111111 language_model = { workspace = true, "features" = ["test-support"] }
112+pretty_assertions.workspace = true
112113 project = { workspace = true, features = ["test-support"] }
113114 rand.workspace = true
crates/agent/src/active_thread.rs+136−26
@@ -1605,6 +1605,7 @@ impl ActiveThread { · 1604 unmodified lines
16051605
16061606 this.thread.update(cx, |thread, cx| {
16071607 thread.advance_prompt_id();
1608+ thread.cancel_last_completion(Some(window.window_handle()), cx);
16081609 thread.send_to_model(
16091610 model.model,
16101611 CompletionIntent::UserPrompt,
@@ -1680,7 +1681,10 @@ impl ActiveThread { · 69 unmodified lines
16801681
16811682 let editor = cx.new(|cx| {
16821683 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+ },
16841688 buffer,
16851689 None,
16861690 window,
@@ -1788,12 +1792,31 @@ impl ActiveThread { · 101 unmodified lines
17881792
17891793 fn render_message(&self, ix: usize, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
17901794 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 {
17921802 return Empty.into_any();
17931803 };
17941804
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+
17951818 if message.is_hidden {
1796− return Empty.into_any();
1819+ return div().children(loading_dots).into_any();
17971820 }
17981821
17991822 let message_creases = message.creases.clone();
@@ -1802,9 +1825,6 @@ impl ActiveThread { · 2 unmodified lines
18021825 return Empty.into_any();
18031826 };
18041827
1805− let workspace = self.workspace.clone();
1806− let thread = self.thread.read(cx);
1807−
18081828 // Get all the data we need from thread before we start using it in closures
18091829 let checkpoint = thread.checkpoint_for_message(message_id);
18101830 let configured_model = thread.configured_model().map(|m| m.model);
@@ -1815,14 +1835,6 @@ impl ActiveThread { · 4 unmodified lines
18151835
18161836 let tool_uses = thread.tool_uses_for_message(message_id, cx);
18171837 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));
18261838
18271839 let editing_message_state = self
18281840 .editing_message
@@ -2238,17 +2250,7 @@ impl ActiveThread { · 409 unmodified lines
22382250 parent.child(self.render_rules_item(cx))
22392251 })
22402252 .child(styled_message)
2241− .when(is_generating && is_last_message, |this| {
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− })
2253+ .children(loading_dots)
22522254 .when(show_feedback, move |parent| {
22532255 parent.child(feedback_items).when_some(
22542256 self.open_feedback_editors.get(&message_id),
@@ -3708,7 +3710,7 @@ mod tests { · 1453 unmodified lines
37083710 use util::path;
37093711 use workspace::CollaboratorId;
37103712
3711− use crate::{ContextLoadResult, thread_store};
3713+ use crate::{ContextLoadResult, thread::MessageSegment, thread_store};
37123714
37133715 use super::*;
37143716
@@ -3842,6 +3844,114 @@ mod tests { · 127 unmodified lines
38423844 });
38433845 }
38443846
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+
38453955 fn init_test_settings(cx: &mut TestAppContext) {
38463956 cx.update(|cx| {
38473957 let settings_store = SettingsStore::test(cx);
crates/agent/src/agent.rs+1−1
@@ -162,7 +162,7 @@ pub fn init( · 161 unmodified lines
162162 assistant_slash_command::init(cx);
163163 thread_store::init(cx);
164164 agent_panel::init(cx);
165− context_server_configuration::init(language_registry, cx);
165+ context_server_configuration::init(language_registry, fs.clone(), cx);
166166
167167 register_slash_commands(cx);
168168 inline_assistant::init(
crates/agent/src/agent_configuration.rs+15−15
@@ -12,7 +12,7 @@ use context_server::ContextServerId; · 11 unmodified lines
1212 use fs::Fs;
1313 use gpui::{
1414 Action, Animation, AnimationExt as _, AnyView, App, Entity, EventEmitter, FocusHandle,
15− Focusable, ScrollHandle, Subscription, pulsating_between,
15+ Focusable, ScrollHandle, Subscription, Transformation, percentage,
1616 };
1717 use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry};
1818 use project::context_server_store::{ContextServerStatus, ContextServerStore};
@@ -475,7 +475,6 @@ impl AgentConfiguration { · 456 unmodified lines
475475 .get(&context_server_id)
476476 .copied()
477477 .unwrap_or_default();
478−
479478 let tools = tools_by_source
480479 .get(&ToolSource::ContextServer {
481480 id: context_server_id.0.clone().into(),
@@ -484,25 +483,23 @@ impl AgentConfiguration { · 2 unmodified lines
484483 let tool_count = tools.len();
485484
486485 let border_color = cx.theme().colors().border.opacity(0.6);
487− let success_color = Color::Success.color(cx);
488486
489487 let (status_indicator, tooltip_text) = match server_status {
490488 ContextServerStatus::Starting => (
491− Indicator::dot()
492− .color(Color::Success)
489+ Icon::new(IconName::LoadCircle)
490+ .size(IconSize::XSmall)
491+ .color(Color::Accent)
493492 .with_animation(
494493 SharedString::from(format!("{}-starting", context_server_id.0.clone(),)),
495− Animation::new(Duration::from_secs(2))
496− .repeat()
497− .with_easing(pulsating_between(0.4, 1.)),
498− move |this, delta| this.color(success_color.alpha(delta).into()),
494+ Animation::new(Duration::from_secs(3)).repeat(),
495+ |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
499496 )
500497 .into_any_element(),
501498 "Server is starting.",
502499 ),
503500 ContextServerStatus::Running => (
504501 Indicator::dot().color(Color::Success).into_any_element(),
505− "Server is running.",
502+ "Server is active.",
506503 ),
507504 ContextServerStatus::Error(_) => (
508505 Indicator::dot().color(Color::Error).into_any_element(),
@@ -526,12 +523,11 @@ impl AgentConfiguration { · 17 unmodified lines
526523 .p_1()
527524 .justify_between()
528525 .when(
529− error.is_some() || are_tools_expanded && tool_count > 1,
526+ error.is_some() || are_tools_expanded && tool_count >= 1,
530527 |element| element.border_b_1().border_color(border_color),
531528 )
532529 .child(
533530 h_flex()
534− .gap_1p5()
535531 .child(
536532 Disclosure::new(
537533 "tool-list-disclosure",
@@ -551,12 +547,16 @@ impl AgentConfiguration { · 13 unmodified lines
551547 })),
552548 )
553549 .child(
554− div()
555− .id(item_id.clone())
550+ h_flex()
551+ .id(SharedString::from(format!("tooltip-{}", item_id)))
552+ .h_full()
553+ .w_3()
554+ .mx_1()
555+ .justify_center()
556556 .tooltip(Tooltip::text(tooltip_text))
557557 .child(status_indicator),
558558 )
559− .child(Label::new(context_server_id.0.clone()).ml_0p5())
559+ .child(Label::new(item_id).ml_0p5().mr_1p5())
560560 .when(is_running, |this| {
561561 this.child(
562562 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
8989 }),
9090 settings_validator,
9191 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);
9393 editor.set_text(config.default_settings.trim(), window, cx);
9494 editor.set_show_gutter(false, cx);
9595 editor.set_soft_wrap_mode(
crates/agent/src/agent_diff.rs+4−4
@@ -31,7 +31,7 @@ use util::ResultExt; · 30 unmodified lines
3131 use workspace::{
3232 Item, ItemHandle, ItemNavHistory, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
3333 Workspace,
34− item::{BreadcrumbText, ItemEvent, TabContentParams},
34+ item::{BreadcrumbText, ItemEvent, SaveOptions, TabContentParams},
3535 searchable::SearchableItemHandle,
3636 };
3737 use zed_actions::assistant::ToggleFocus;
@@ -532,12 +532,12 @@ impl Item for AgentDiffPane { · 494 unmodified lines
532532
533533 fn save(
534534 &mut self,
535− format: bool,
535+ options: SaveOptions,
536536 project: Entity<Project>,
537537 window: &mut Window,
538538 cx: &mut Context<Self>,
539539 ) -> Task<Result<()>> {
540− self.editor.save(format, project, window, cx)
540+ self.editor.save(options, project, window, cx)
541541 }
542542
543543 fn save_as(
@@ -1513,7 +1513,7 @@ impl AgentDiff { · 969 unmodified lines
15131513 multibuffer.add_diff(diff_handle.clone(), cx);
15141514 });
15151515
1516− let new_state = if thread.read(cx).has_pending_edit_tool_uses() {
1516+ let new_state = if thread.read(cx).is_generating() {
15171517 EditorState::Generating
15181518 } else {
15191519 EditorState::Reviewing
crates/agent/src/agent_model_selector.rs+3−2
@@ -91,12 +91,13 @@ impl AgentModelSelector { · 90 unmodified lines
9191
9292 impl Render for AgentModelSelector {
9393 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
94− let focus_handle = self.focus_handle.clone();
95−
9694 let model = self.selector.read(cx).delegate.active_model(cx);
9795 let model_name = model
9896 .map(|model| model.model.name().0)
9997 .unwrap_or_else(|| SharedString::from("No model selected"));
98+
99+ let focus_handle = self.focus_handle.clone();
100+
100101 PickerPopoverMenu::new(
101102 self.selector.clone(),
102103 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
1010 use agent_settings::{AgentDockPosition, AgentSettings, CompletionMode, DefaultView};
1111 use anyhow::{Result, anyhow};
1212 use assistant_context_editor::{
13− AgentPanelDelegate, AssistantContext, ConfigurationError, ContextEditor, ContextEvent,
14− ContextSummary, SlashCommandCompletionProvider, humanize_token_count,
15− make_lsp_adapter_delegate, render_remaining_tokens,
13+ AgentPanelDelegate, AssistantContext, ContextEditor, ContextEvent, ContextSummary,
14+ SlashCommandCompletionProvider, humanize_token_count, make_lsp_adapter_delegate,
15+ render_remaining_tokens,
1616 };
1717 use assistant_slash_command::SlashCommandWorkingSet;
1818 use assistant_tool::ToolWorkingSet;
@@ -29,7 +29,8 @@ use gpui::{ · 10 unmodified lines
2929 };
3030 use language::LanguageRegistry;
3131 use language_model::{
32− LanguageModelProviderTosView, LanguageModelRegistry, RequestUsage, ZED_CLOUD_PROVIDER_ID,
32+ ConfigurationError, LanguageModelProviderTosView, LanguageModelRegistry, RequestUsage,
33+ ZED_CLOUD_PROVIDER_ID,
3334 };
3435 use project::{Project, ProjectPath, Worktree};
3536 use prompt_store::{PromptBuilder, PromptStore, UserPromptId};
@@ -2353,24 +2354,6 @@ impl AgentPanel { · 2317 unmodified lines
23532354 self.thread.clone().into_any_element()
23542355 }
23552356
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−
23742357 fn render_thread_empty_state(
23752358 &self,
23762359 window: &mut Window,
@@ -2380,7 +2363,9 @@ impl AgentPanel { · 3 unmodified lines
23802363 .history_store
23812364 .update(cx, |this, cx| this.recent_entries(6, cx));
23822365
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);
23842369 let no_error = configuration_error.is_none();
23852370 let focus_handle = self.focus_handle(cx);
23862371
@@ -2397,11 +2382,7 @@ impl AgentPanel { · 10 unmodified lines
23972382 .justify_center()
23982383 .items_center()
23992384 .gap_1()
2400− .child(
2401− h_flex().child(
2402− Headline::new("Welcome to the Agent Panel")
2403− ),
2404− )
2385+ .child(h_flex().child(Headline::new("Welcome to the Agent Panel")))
24052386 .when(no_error, |parent| {
24062387 parent
24072388 .child(
@@ -2425,7 +2406,10 @@ impl AgentPanel { · 17 unmodified lines
24252406 cx,
24262407 ))
24272408 .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+ )
24292413 }),
24302414 )
24312415 .child(
@@ -2442,7 +2426,10 @@ impl AgentPanel { · 10 unmodified lines
24422426 cx,
24432427 ))
24442428 .on_click(|_event, window, cx| {
2445− window.dispatch_action(ToggleContextPicker.boxed_clone(), cx)
2429+ window.dispatch_action(
2430+ ToggleContextPicker.boxed_clone(),
2431+ cx,
2432+ )
24462433 }),
24472434 )
24482435 .child(
@@ -2459,7 +2446,10 @@ impl AgentPanel { · 10 unmodified lines
24592446 cx,
24602447 ))
24612448 .on_click(|_event, window, cx| {
2462− window.dispatch_action(ToggleModelSelector.boxed_clone(), cx)
2449+ window.dispatch_action(
2450+ ToggleModelSelector.boxed_clone(),
2451+ cx,
2452+ )
24632453 }),
24642454 )
24652455 .child(
@@ -2476,51 +2466,50 @@ impl AgentPanel { · 10 unmodified lines
24762466 cx,
24772467 ))
24782468 .on_click(|_event, window, cx| {
2479− window.dispatch_action(OpenConfiguration.boxed_clone(), cx)
2469+ window.dispatch_action(
2470+ OpenConfiguration.boxed_clone(),
2471+ cx,
2472+ )
24802473 }),
24812474 )
24822475 })
2483− .map(|parent| {
2484− match configuration_error_ref {
2485− Some(ConfigurationError::ProviderNotAuthenticated)
2486− | Some(ConfigurationError::NoProvider) => {
2487− parent
2488− .child(
2489− h_flex().child(
2490− Label::new("To start using the agent, configure at least one LLM provider.")
2491− .color(Color::Muted)
2492− .mb_2p5()
2493− )
2494− )
2495− .child(
2496− Button::new("settings", "Configure a Provider")
2497− .icon(IconName::Settings)
2498− .icon_position(IconPosition::Start)
2499− .icon_size(IconSize::Small)
2500− .icon_color(Color::Muted)
2501− .full_width()
2502− .key_binding(KeyBinding::for_action_in(
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,
2476+ .map(|parent| match configuration_error_ref {
2477+ Some(
2478+ err @ (ConfigurationError::ModelNotFound
2479+ | ConfigurationError::ProviderNotAuthenticated(_)
2480+ | ConfigurationError::NoProvider),
2481+ ) => parent
2482+ .child(h_flex().child(
2483+ Label::new(err.to_string()).color(Color::Muted).mb_2p5(),
2484+ ))
2485+ .child(
2486+ Button::new("settings", "Configure a Provider")
2487+ .icon(IconName::Settings)
2488+ .icon_position(IconPosition::Start)
2489+ .icon_size(IconSize::Small)
2490+ .icon_color(Color::Muted)
2491+ .full_width()
2492+ .key_binding(KeyBinding::for_action_in(
2493+ &OpenConfiguration,
2494+ &focus_handle,
2495+ window,
25172496 cx,
2518− ),
2519− )
2520− }
2521− None => parent,
2497+ ))
2498+ .on_click(|_event, window, cx| {
2499+ window.dispatch_action(
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+ ))
25222510 }
2523− })
2511+ None => parent,
2512+ }),
25242513 )
25252514 })
25262515 .when(!recent_history.is_empty(), |parent| {
@@ -2555,7 +2544,8 @@ impl AgentPanel { · 28 unmodified lines
25552544 &self.focus_handle(cx),
25562545 window,
25572546 cx,
2558− ).map(|kb| kb.size(rems_from_px(12.))),
2547+ )
2548+ .map(|kb| kb.size(rems_from_px(12.))),
25592549 )
25602550 .on_click(move |_event, window, cx| {
25612551 window.dispatch_action(OpenHistory.boxed_clone(), cx);
@@ -2565,79 +2555,68 @@ impl AgentPanel { · 3 unmodified lines
25652555 .child(
25662556 v_flex()
25672557 .gap_1()
2568− .children(
2569− recent_history.into_iter().enumerate().map(|(index, entry)| {
2558+ .children(recent_history.into_iter().enumerate().map(
2559+ |(index, entry)| {
25702560 // 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);
25722563 HistoryEntryElement::new(entry.clone(), cx.entity().downgrade())
25732564 .hovered(is_hovered)
2574− .on_hover(cx.listener(move |this, is_hovered, _window, cx| {
2575− if *is_hovered {
2576− this.hovered_recent_history_item = Some(index);
2577− } else if this.hovered_recent_history_item == Some(index) {
2578− this.hovered_recent_history_item = None;
2579− }
2580− cx.notify();
2581− }))
2565+ .on_hover(cx.listener(
2566+ move |this, is_hovered, _window, cx| {
2567+ if *is_hovered {
2568+ this.hovered_recent_history_item = Some(index);
2569+ } else if this.hovered_recent_history_item
2570+ == Some(index)
2571+ {
2572+ this.hovered_recent_history_item = None;
2573+ }
2574+ cx.notify();
2575+ },
2576+ ))
25822577 .into_any_element()
2583− }),
2584− )
2578+ },
2579+ )),
25852580 )
2586− .map(|parent| {
2587− match configuration_error_ref {
2588− Some(ConfigurationError::ProviderNotAuthenticated)
2589− | Some(ConfigurationError::NoProvider) => {
2590− parent
2591− .child(
2592− Banner::new()
2593− .severity(ui::Severity::Warning)
2594− .child(
2595− Label::new(
2596− "Configure at least one LLM provider to start using the panel.",
2597− )
2598− .size(LabelSize::Small),
2581+ .map(|parent| match configuration_error_ref {
2582+ Some(
2583+ err @ (ConfigurationError::ModelNotFound
2584+ | ConfigurationError::ProviderNotAuthenticated(_)
2585+ | ConfigurationError::NoProvider),
2586+ ) => parent.child(
2587+ Banner::new()
2588+ .severity(ui::Severity::Warning)
2589+ .child(Label::new(err.to_string()).size(LabelSize::Small))
2590+ .action_slot(
2591+ Button::new("settings", "Configure Provider")
2592+ .style(ButtonStyle::Tinted(ui::TintColor::Warning))
2593+ .label_size(LabelSize::Small)
2594+ .key_binding(
2595+ KeyBinding::for_action_in(
2596+ &OpenConfiguration,
2597+ &focus_handle,
2598+ window,
2599+ cx,
25992600 )
2600− .action_slot(
2601− Button::new("settings", "Configure Provider")
2602− .style(ButtonStyle::Tinted(ui::TintColor::Warning))
2603− .label_size(LabelSize::Small)
2604− .key_binding(
2605− KeyBinding::for_action_in(
2606− &OpenConfiguration,
2607− &focus_handle,
2608− window,
2609− cx,
2610− )
2611− .map(|kb| kb.size(rems_from_px(12.))),
2612− )
2613− .on_click(|_event, window, cx| {
2614− window.dispatch_action(
2615− OpenConfiguration.boxed_clone(),
2616− cx,
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,
2601+ .map(|kb| kb.size(rems_from_px(12.))),
2602+ )
2603+ .on_click(|_event, window, cx| {
2604+ window.dispatch_action(
2605+ OpenConfiguration.boxed_clone(),
2606+ cx,
2607+ )
2608+ }),
2609+ ),
2610+ ),
2611+ Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
2612+ parent.child(Banner::new().severity(ui::Severity::Warning).child(
2613+ h_flex().w_full().children(provider.render_accept_terms(
2614+ LanguageModelProviderTosView::ThreadtEmptyState,
2615+ cx,
2616+ )),
2617+ ))
26402618 }
2619+ None => parent,
26412620 })
26422621 })
26432622 }
crates/agent/src/buffer_codegen.rs+4−2
@@ -386,8 +386,10 @@ impl CodegenAlternative { · 385 unmodified lines
386386 async { Ok(LanguageModelTextStream::default()) }.boxed_local()
387387 } else {
388388 let request = self.build_request(&model, user_prompt, cx)?;
389− cx.spawn(async move |_, cx| model.stream_completion_text(request.await, &cx).await)
390− .boxed_local()
389+ cx.spawn(async move |_, cx| {
390+ Ok(model.stream_completion_text(request.await, &cx).await?)
391+ })
392+ .boxed_local()
391393 };
392394 self.handle_stream(telemetry_id, provider_id.to_string(), api_key, stream, cx);
393395 Ok(())
crates/agent/src/context_picker/completion_provider.rs+9−9
@@ -1066,7 +1066,7 @@ mod tests { · 1065 unmodified lines
10661066 use serde_json::json;
10671067 use settings::SettingsStore;
10681068 use std::{ops::Deref, rc::Rc};
1069− use util::{path, separator};
1069+ use util::path;
10701070 use workspace::{AppState, Item};
10711071
10721072 #[test]
@@ -1217,14 +1217,14 @@ mod tests { · 144 unmodified lines
12171217 let mut cx = VisualTestContext::from_window(*window.deref(), cx);
12181218
12191219 let paths = vec![
1220− separator!("a/one.txt"),
1221− separator!("a/two.txt"),
1222− separator!("a/three.txt"),
1223− separator!("a/four.txt"),
1224− separator!("b/five.txt"),
1225− separator!("b/six.txt"),
1226− separator!("b/seven.txt"),
1227− separator!("b/eight.txt"),
1220+ path!("a/one.txt"),
1221+ path!("a/two.txt"),
1222+ path!("a/three.txt"),
1223+ path!("a/four.txt"),
1224+ path!("b/five.txt"),
1225+ path!("b/six.txt"),
1226+ path!("b/seven.txt"),
1227+ path!("b/eight.txt"),
12281228 ];
12291229
12301230 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
33 use anyhow::Context as _;
44 use context_server::ContextServerId;
55 use extension::{ContextServerConfiguration, ExtensionManifest};
6+use fs::Fs;
67 use gpui::Task;
78 use language::LanguageRegistry;
8−use project::context_server_store::registry::ContextServerDescriptorRegistry;
9+use project::{
10+ context_server_store::registry::ContextServerDescriptorRegistry,
11+ project_settings::ProjectSettings,
12+};
13+use settings::update_settings_file;
914 use ui::prelude::*;
1015 use util::ResultExt;
1116 use workspace::Workspace;
1217
1318 use crate::agent_configuration::ConfigureContextServerModal;
1419
15−pub(crate) fn init(language_registry: Arc<LanguageRegistry>, cx: &mut App) {
20+pub(crate) fn init(language_registry: Arc<LanguageRegistry>, fs: Arc<dyn Fs>, cx: &mut App) {
1621 cx.observe_new(move |_: &mut Workspace, window, cx| {
1722 let Some(window) = window else {
1823 return;
@@ -21,6 +26,7 @@ pub(crate) fn init(language_registry: Arc<LanguageRegistry>, cx: &mut App) { · 2 unmodified lines
2126 if let Some(extension_events) = extension::ExtensionEvents::try_global(cx).as_ref() {
2227 cx.subscribe_in(extension_events, window, {
2328 let language_registry = language_registry.clone();
29+ let fs = fs.clone();
2430 move |workspace, _, event, window, cx| match event {
2531 extension::Event::ExtensionInstalled(manifest) => {
2632 show_configure_mcp_modal(
@@ -31,6 +37,13 @@ pub(crate) fn init(language_registry: Arc<LanguageRegistry>, cx: &mut App) { · 4 unmodified lines
3137 cx,
3238 );
3339 }
40+ extension::Event::ExtensionUninstalled(manifest) => {
41+ remove_context_server_settings(
42+ manifest.context_servers.keys().cloned().collect(),
43+ fs.clone(),
44+ cx,
45+ );
46+ }
3447 extension::Event::ConfigureExtensionRequested(manifest) => {
3548 if !manifest.context_servers.is_empty() {
3649 show_configure_mcp_modal(
@@ -55,6 +68,18 @@ pub(crate) fn init(language_registry: Arc<LanguageRegistry>, cx: &mut App) { · 18 unmodified lines
5568 .detach();
5669 }
5770
71+fn 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+
5883 pub enum Configuration {
5984 NotAvailable(ContextServerId, Option<SharedString>),
6085 Required(
@@ -71,6 +96,10 @@ fn show_configure_mcp_modal( · 10 unmodified lines
7196 window: &mut Window,
7297 cx: &mut Context<'_, Workspace>,
7398 ) {
99+ if !window.is_window_active() {
100+ return;
101+ }
102+
74103 let context_server_store = workspace.project().read(cx).context_server_store();
75104 let repository: Option<SharedString> = manifest.repository.as_ref().map(|s| s.clone().into());
76105
crates/agent/src/context_server_tool.rs+4−1
@@ -105,7 +105,7 @@ impl Tool for ContextServerTool { · 104 unmodified lines
105105 arguments
106106 );
107107 let response = protocol
108− .request::<context_server::types::request::CallTool>(
108+ .request::<context_server::types::requests::CallTool>(
109109 context_server::types::CallToolParams {
110110 name: tool_name,
111111 arguments,
@@ -123,6 +123,9 @@ impl Tool for ContextServerTool { · 11 unmodified lines
123123 types::ToolResponseContent::Image { .. } => {
124124 log::warn!("Ignoring image content from tool response");
125125 }
126+ types::ToolResponseContent::Audio { .. } => {
127+ log::warn!("Ignoring audio content from tool response");
128+ }
126129 types::ToolResponseContent::Resource { .. } => {
127130 log::warn!("Ignoring resource content from tool response");
128131 }
crates/agent/src/inline_assistant.rs+44−43
@@ -24,6 +24,7 @@ use gpui::{ · 23 unmodified lines
2424 WeakEntity, Window, point,
2525 };
2626 use language::{Buffer, Point, Selection, TransactionId};
27+use language_model::ConfigurationError;
2728 use language_model::ConfiguredModel;
2829 use language_model::{LanguageModelRegistry, report_assistant_event};
2930 use multi_buffer::MultiBufferRow;
@@ -38,8 +39,7 @@ use telemetry_events::{AssistantEventData, AssistantKind, AssistantPhase}; · 8 unmodified lines
3839 use terminal_view::{TerminalView, terminal_panel::TerminalPanel};
3940 use text::{OffsetRangeExt, ToPoint as _};
4041 use ui::prelude::*;
41−use util::RangeExt;
42−use util::ResultExt;
42+use util::{RangeExt, ResultExt, maybe};
4343 use workspace::{ItemHandle, Toast, Workspace, dock::Panel, notifications::NotificationId};
4444 use zed_actions::agent::OpenConfiguration;
4545
@@ -233,10 +233,9 @@ impl InlineAssistant { · 187 unmodified lines
233233 return;
234234 };
235235
236− let is_authenticated = || {
237− LanguageModelRegistry::read_global(cx)
238− .inline_assistant_model()
239− .map_or(false, |model| model.provider.is_authenticated(cx))
236+ let configuration_error = || {
237+ let model_registry = LanguageModelRegistry::read_global(cx);
238+ model_registry.configuration_error(model_registry.inline_assistant_model(), cx)
240239 };
241240
242241 let Some(agent_panel) = workspace.panel::<AgentPanel>(cx) else {
@@ -284,20 +283,23 @@ impl InlineAssistant { · 41 unmodified lines
284283 }
285284 };
286285
287− if is_authenticated() {
288− handle_assist(window, cx);
289− } else {
290− cx.spawn_in(window, async move |_workspace, cx| {
291− let Some(task) = cx.update(|_, cx| {
292− LanguageModelRegistry::read_global(cx)
293− .inline_assistant_model()
294− .map_or(None, |model| Some(model.provider.authenticate(cx)))
295− })?
296− else {
286+ if let Some(error) = configuration_error() {
287+ if let ConfigurationError::ProviderNotAuthenticated(provider) = error {
288+ cx.spawn(async move |_, cx| {
289+ cx.update(|cx| provider.authenticate(cx))?.await?;
290+ anyhow::Ok(())
291+ })
292+ .detach_and_log_err(cx);
293+
294+ if configuration_error().is_none() {
295+ handle_assist(window, cx);
296+ }
297+ } else {
298+ cx.spawn_in(window, async move |_, cx| {
297299 let answer = cx
298300 .prompt(
299301 gpui::PromptLevel::Warning,
300− "No language model provider configured",
302+ &error.to_string(),
301303 None,
302304 &["Configure", "Cancel"],
303305 )
@@ -311,17 +313,12 @@ impl InlineAssistant { · 7 unmodified lines
311313 .ok();
312314 }
313315 }
314− return Ok(());
315− };
316− task.await?;
317−
318− anyhow::Ok(())
319− })
320− .detach_and_log_err(cx);
321−
322− if is_authenticated() {
323− handle_assist(window, cx);
316+ anyhow::Ok(())
317+ })
318+ .detach_and_log_err(cx);
324319 }
320+ } else {
321+ handle_assist(window, cx);
325322 }
326323 }
327324
@@ -1171,27 +1168,31 @@ impl InlineAssistant { · 843 unmodified lines
11711168 selections.select_anchor_ranges([position..position])
11721169 });
11731170
1174− let mut scroll_target_top;
1175− let mut scroll_target_bottom;
1171+ let mut scroll_target_range = None;
11761172 if let Some(decorations) = assist.decorations.as_ref() {
1177− scroll_target_top = editor
1178− .row_for_block(decorations.prompt_block_id, cx)
1179− .unwrap()
1180− .0 as f32;
1181− scroll_target_bottom = editor
1182− .row_for_block(decorations.end_block_id, cx)
1183− .unwrap()
1184− .0 as f32;
1185− } else {
1173+ scroll_target_range = maybe!({
1174+ let top = editor.row_for_block(decorations.prompt_block_id, cx)?.0 as f32;
1175+ let bottom = editor.row_for_block(decorations.end_block_id, cx)?.0 as f32;
1176+ Some((top, bottom))
1177+ });
1178+ if scroll_target_range.is_none() {
1179+ log::error!("bug: failed to find blocks for scrolling to inline assist");
1180+ }
1181+ }
1182+ let scroll_target_range = scroll_target_range.unwrap_or_else(|| {
11861183 let snapshot = editor.snapshot(window, cx);
11871184 let start_row = assist
11881185 .range
11891186 .start
11901187 .to_display_point(&snapshot.display_snapshot)
11911188 .row();
1192− scroll_target_top = start_row.0 as f32;
1193− scroll_target_bottom = scroll_target_top + 1.;
1194− }
1189+ let top = start_row.0 as f32;
1190+ let bottom = top + 1.0;
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+
11951196 scroll_target_top -= editor.vertical_scroll_margin() as f32;
11961197 scroll_target_bottom += editor.vertical_scroll_margin() as f32;
11971198
@@ -1331,7 +1332,7 @@ impl InlineAssistant { · 133 unmodified lines
13311332 editor.clear_gutter_highlights::<GutterPendingRange>(cx);
13321333 } else {
13331334 editor.highlight_gutter::<GutterPendingRange>(
1334− &gutter_pending_ranges,
1335+ gutter_pending_ranges,
13351336 |cx| cx.theme().status().info_background,
13361337 cx,
13371338 )
@@ -1342,7 +1343,7 @@ impl InlineAssistant { · 4 unmodified lines
13421343 editor.clear_gutter_highlights::<GutterTransformedRange>(cx);
13431344 } else {
13441345 editor.highlight_gutter::<GutterTransformedRange>(
1345− &gutter_transformed_ranges,
1346+ gutter_transformed_ranges,
13461347 |cx| cx.theme().status().info,
13471348 cx,
13481349 )
crates/agent/src/inline_prompt_editor.rs+3−1
@@ -261,7 +261,7 @@ impl<T: 'static> PromptEditor<T> { · 260 unmodified lines
261261
262262 let focus = self.editor.focus_handle(cx).contains_focused(window, cx);
263263 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);
265265 editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
266266 editor.set_placeholder_text("Add a prompt…", cx);
267267 editor.set_text(prompt, window, cx);
@@ -872,6 +872,7 @@ impl PromptEditor<BufferCodegen> { · 604 unmodified lines
872872 let prompt_editor = cx.new(|cx| {
873873 let mut editor = Editor::new(
874874 EditorMode::AutoHeight {
875+ min_lines: 1,
875876 max_lines: Self::MAX_LINES as usize,
876877 },
877878 prompt_buffer,
@@ -1050,6 +1051,7 @@ impl PromptEditor<TerminalCodegen> { · 172 unmodified lines
10501051 let prompt_editor = cx.new(|cx| {
10511052 let mut editor = Editor::new(
10521053 EditorMode::AutoHeight {
1054+ min_lines: 1,
10531055 max_lines: Self::MAX_LINES as usize,
10541056 },
10551057 prompt_buffer,
crates/agent/src/message_editor.rs+105−96
@@ -39,7 +39,7 @@ use proto::Plan; · 38 unmodified lines
3939 use settings::Settings;
4040 use std::time::Duration;
4141 use theme::ThemeSettings;
42−use ui::{Disclosure, KeyBinding, PopoverMenuHandle, Tooltip, prelude::*};
42+use ui::{Callout, Disclosure, KeyBinding, PopoverMenuHandle, Tooltip, prelude::*};
4343 use util::{ResultExt as _, maybe};
4444 use workspace::{CollaboratorId, Workspace};
4545 use zed_llm_client::CompletionIntent;
@@ -79,6 +79,7 @@ pub struct MessageEditor { · 33 unmodified lines
7979 _subscriptions: Vec<Subscription>,
8080 }
8181
82+const MIN_EDITOR_LINES: usize = 4;
8283 const MAX_EDITOR_LINES: usize = 8;
8384
8485 pub(crate) fn create_editor(
@@ -102,6 +103,7 @@ pub(crate) fn create_editor( · 17 unmodified lines
102103 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
103104 let mut editor = Editor::new(
104105 editor::EditorMode::AutoHeight {
106+ min_lines: MIN_EDITOR_LINES,
105107 max_lines: MAX_EDITOR_LINES,
106108 },
107109 buffer,
@@ -253,6 +255,7 @@ impl MessageEditor { · 145 unmodified lines
253255 })
254256 } else {
255257 editor.set_mode(EditorMode::AutoHeight {
258+ min_lines: MIN_EDITOR_LINES,
256259 max_lines: MAX_EDITOR_LINES,
257260 })
258261 }
@@ -428,10 +431,6 @@ impl MessageEditor { · 169 unmodified lines
428431 }
429432
430433 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−
435434 self.edits_expanded = true;
436435 AgentDiffPane::deploy(self.thread.clone(), self.workspace.clone(), window, cx).log_err();
437436 cx.notify();
@@ -506,7 +505,7 @@ impl MessageEditor { · 68 unmodified lines
506505 cx.notify();
507506 }
508507
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> {
510509 let thread = self.thread.read(cx);
511510 let model = thread.configured_model();
512511 if !model?.model.supports_max_mode() {
@@ -641,96 +640,87 @@ impl MessageEditor { · 128 unmodified lines
641640 .border_color(cx.theme().colors().border)
642641 .child(
643642 h_flex()
644− .items_start()
645643 .justify_between()
646644 .child(self.context_strip.clone())
647− .child(
648− h_flex()
649− .gap_1()
650− .when(focus_handle.is_focused(window), |this| {
651− this.child(
652− IconButton::new("toggle-height", expand_icon)
653− .icon_size(IconSize::XSmall)
654− .icon_color(Color::Muted)
655− .tooltip({
656− let focus_handle = focus_handle.clone();
657− move |window, cx| {
658− let expand_label = if is_editor_expanded {
659− "Minimize Message Editor".to_string()
660− } else {
661− "Expand Message Editor".to_string()
662− };
663−
664− Tooltip::for_action_in(
665− expand_label,
666− &ExpandMessageEditor,
667− &focus_handle,
668− window,
669− cx,
670− )
671− }
672− })
673− .on_click(cx.listener(|_, _, window, cx| {
674− window
675− .dispatch_action(Box::new(ExpandMessageEditor), cx);
676− })),
677− )
678− }),
679− ),
645+ .when(focus_handle.is_focused(window), |this| {
646+ this.child(
647+ IconButton::new("toggle-height", expand_icon)
648+ .icon_size(IconSize::XSmall)
649+ .icon_color(Color::Muted)
650+ .tooltip({
651+ let focus_handle = focus_handle.clone();
652+ move |window, cx| {
653+ let expand_label = if is_editor_expanded {
654+ "Minimize Message Editor".to_string()
655+ } else {
656+ "Expand Message Editor".to_string()
657+ };
658+
659+ Tooltip::for_action_in(
660+ expand_label,
661+ &ExpandMessageEditor,
662+ &focus_handle,
663+ window,
664+ cx,
665+ )
666+ }
667+ })
668+ .on_click(cx.listener(|_, _, window, cx| {
669+ window.dispatch_action(Box::new(ExpandMessageEditor), cx);
670+ })),
671+ )
672+ }),
680673 )
681674 .child(
682675 v_flex()
683676 .size_full()
684− .gap_4()
677+ .gap_1()
685678 .when(is_editor_expanded, |this| {
686679 this.h(vh(0.8, window)).justify_between()
687680 })
688− .child(
689− v_flex()
690− .min_h_16()
691− .when(is_editor_expanded, |this| this.h_full())
692− .child({
693− let settings = ThemeSettings::get_global(cx);
694− let font_size = TextSize::Small
695− .rems(cx)
696− .to_pixels(settings.agent_font_size(cx));
697− let line_height = settings.buffer_line_height.value() * font_size;
698−
699− let text_style = TextStyle {
700− color: cx.theme().colors().text,
701− font_family: settings.buffer_font.family.clone(),
702− font_fallbacks: settings.buffer_font.fallbacks.clone(),
703− font_features: settings.buffer_font.features.clone(),
704− font_size: font_size.into(),
705− line_height: line_height.into(),
706− ..Default::default()
707− };
708−
709− EditorElement::new(
710− &self.editor,
711− EditorStyle {
712− background: editor_bg_color,
713− local_player: cx.theme().players().local(),
714− text: text_style,
715− syntax: cx.theme().syntax().clone(),
716− ..Default::default()
717− },
718− )
719− .into_any()
720− }),
721− )
681+ .child({
682+ let settings = ThemeSettings::get_global(cx);
683+ let font_size = TextSize::Small
684+ .rems(cx)
685+ .to_pixels(settings.agent_font_size(cx));
686+ let line_height = settings.buffer_line_height.value() * font_size;
687+
688+ let text_style = TextStyle {
689+ color: cx.theme().colors().text,
690+ font_family: settings.buffer_font.family.clone(),
691+ font_fallbacks: settings.buffer_font.fallbacks.clone(),
692+ font_features: settings.buffer_font.features.clone(),
693+ font_size: font_size.into(),
694+ line_height: line_height.into(),
695+ ..Default::default()
696+ };
697+
698+ EditorElement::new(
699+ &self.editor,
700+ EditorStyle {
701+ background: editor_bg_color,
702+ local_player: cx.theme().players().local(),
703+ text: text_style,
704+ syntax: cx.theme().syntax().clone(),
705+ ..Default::default()
706+ },
707+ )
708+ .into_any()
709+ })
722710 .child(
723711 h_flex()
724712 .flex_none()
713+ .flex_wrap()
725714 .justify_between()
726715 .child(
727716 h_flex()
728717 .child(self.render_follow_toggle(cx))
729− .children(self.render_max_mode_toggle(cx)),
718+ .children(self.render_burn_mode_toggle(cx)),
730719 )
731720 .child(
732721 h_flex()
733722 .gap_1()
723+ .flex_wrap()
734724 .when(!incompatible_tools.is_empty(), |this| {
735725 this.child(
736726 IconButton::new(
@@ -1183,6 +1173,7 @@ impl MessageEditor { · 446 unmodified lines
11831173 .map_or(false, |model| {
11841174 model.provider.id().0 == ZED_CLOUD_PROVIDER_ID
11851175 });
1176+
11861177 if !is_using_zed_provider {
11871178 return None;
11881179 }
@@ -1237,14 +1228,6 @@ impl MessageEditor { · 48 unmodified lines
12371228 token_usage_ratio: TokenUsageRatio,
12381229 cx: &mut Context<Self>,
12391230 ) -> 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−
12481231 let icon = if token_usage_ratio == TokenUsageRatio::Exceeded {
12491232 Icon::new(IconName::X)
12501233 .color(Color::Error)
@@ -1255,19 +1238,43 @@ impl MessageEditor { · 4 unmodified lines
12551238 .size(IconSize::XSmall)
12561239 };
12571240
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+
12581247 Some(
12591248 div()
1260− .child(ui::Callout::multi_line(
1261− title,
1262− message,
1263− icon,
1264− "Start New Thread",
1265− Box::new(cx.listener(|this, _, window, cx| {
1266− let from_thread_id = Some(this.thread.read(cx).id().clone());
1267− window.dispatch_action(Box::new(NewThread { from_thread_id }), cx);
1268− })),
1269− ))
1270− .line_height(line_height),
1249+ .border_t_1()
1250+ .border_color(cx.theme().colors().border)
1251+ .child(
1252+ Callout::new()
1253+ .line_height(line_height)
1254+ .icon(icon)
1255+ .title(title)
1256+ .description(
1257+ "To continue, start a new thread from a summary or turn burn mode on.",
1258+ )
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+ ),
12711278 )
12721279 }
12731280
@@ -1464,6 +1471,8 @@ impl Render for MessageEditor { · 190 unmodified lines
14641471 total_token_usage.ratio()
14651472 });
14661473
1474+ let burn_mode_enabled = thread.completion_mode() == CompletionMode::Burn;
1475+
14671476 let action_log = self.thread.read(cx).action_log();
14681477 let changed_buffers = action_log.read(cx).changed_buffers(cx);
14691478
@@ -1480,7 +1489,7 @@ impl Render for MessageEditor { · 10 unmodified lines
14801489
14811490 if usage_callout.is_some() {
14821491 usage_callout
1483− } else if token_usage_ratio != TokenUsageRatio::Normal {
1492+ } else if token_usage_ratio != TokenUsageRatio::Normal && !burn_mode_enabled {
14841493 self.render_token_limit_callout(line_height, token_usage_ratio, cx)
14851494 } else {
14861495 None
crates/agent/src/thread.rs+8−5
@@ -195,20 +195,20 @@ impl MessageSegment { · 194 unmodified lines
195195 }
196196 }
197197
198−#[derive(Debug, Clone, Serialize, Deserialize)]
198+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
199199 pub struct ProjectSnapshot {
200200 pub worktree_snapshots: Vec<WorktreeSnapshot>,
201201 pub unsaved_buffer_paths: Vec<String>,
202202 pub timestamp: DateTime<Utc>,
203203 }
204204
205−#[derive(Debug, Clone, Serialize, Deserialize)]
205+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
206206 pub struct WorktreeSnapshot {
207207 pub worktree_path: String,
208208 pub git_state: Option<GitState>,
209209 }
210210
211−#[derive(Debug, Clone, Serialize, Deserialize)]
211+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
212212 pub struct GitState {
213213 pub remote_url: Option<String>,
214214 pub head_sha: Option<String>,
@@ -247,7 +247,7 @@ impl LastRestoreCheckpoint { · 32 unmodified lines
247247 }
248248 }
249249
250−#[derive(Clone, Debug, Default, Serialize, Deserialize)]
250+#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
251251 pub enum DetailedSummaryState {
252252 #[default]
253253 NotGenerated,
@@ -391,7 +391,7 @@ impl ThreadSummary { · 137 unmodified lines
391391 }
392392 }
393393
394−#[derive(Debug, Clone, Serialize, Deserialize)]
394+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
395395 pub struct ExceededWindowError {
396396 /// Model used when last message exceeded context window
397397 model_id: LanguageModelId,
@@ -1563,6 +1563,9 @@ impl Thread { · 1165 unmodified lines
15631563 Err(LanguageModelCompletionError::Other(error)) => {
15641564 return Err(error);
15651565 }
1566+ Err(err @ LanguageModelCompletionError::RateLimit(..)) => {
1567+ return Err(err.into());
1568+ }
15661569 };
15671570
15681571 match event {
crates/agent/src/thread_history.rs+3−2
@@ -594,10 +594,11 @@ impl Render for ThreadHistory { · 593 unmodified lines
594594 view.pr_5()
595595 .child(
596596 uniform_list(
597− cx.entity().clone(),
598597 "thread-history",
599598 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+ }),
601602 )
602603 .p_1()
603604 .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
8989 pub struct SharedProjectContext(Rc<RefCell<Option<ProjectContext>>>);
9090
9191 impl SharedProjectContext {
92− pub fn borrow(&self) -> Ref<Option<ProjectContext>> {
92+ pub fn borrow(&self) -> Ref<'_, Option<ProjectContext>> {
9393 self.0.borrow()
9494 }
9595 }
@@ -305,17 +305,19 @@ impl ThreadStore { · 209 unmodified lines
305305 project: Entity<Project>,
306306 cx: &mut App,
307307 ) -> 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+ };
309317
310318 let rules_task = Self::load_worktree_rules_file(worktree, project, cx);
311319 let Some(rules_task) = rules_task else {
312− return Task::ready((
313− WorktreeContext {
314− root_name,
315− rules_file: None,
316− },
317− None,
318− ));
320+ return Task::ready((context, None));
319321 };
320322
321323 cx.spawn(async move |_| {
@@ -328,11 +330,8 @@ impl ThreadStore { · 6 unmodified lines
328330 }),
329331 ),
330332 };
331− let worktree_info = WorktreeContext {
332− root_name,
333− rules_file,
334− };
335− (worktree_info, rules_file_error)
333+ context.rules_file = rules_file;
334+ (context, rules_file_error)
336335 })
337336 }
338337
@@ -341,12 +340,12 @@ impl ThreadStore { · 2 unmodified lines
341340 project: Entity<Project>,
342341 cx: &mut App,
343342 ) -> Option<Task<Result<RulesFileContext>>> {
344− let worktree_ref = worktree.read(cx);
345− let worktree_id = worktree_ref.id();
343+ let worktree = worktree.read(cx);
344+ let worktree_id = worktree.id();
346345 let selected_rules_file = RULES_FILE_NAMES
347346 .into_iter()
348347 .filter_map(|name| {
349− worktree_ref
348+ worktree
350349 .entry_for_path(name)
351350 .filter(|entry| entry.is_file())
352351 .map(|entry| entry.path.clone())
@@ -562,7 +561,7 @@ impl ThreadStore { · 209 unmodified lines
562561
563562 if protocol.capable(context_server::protocol::ServerCapability::Tools) {
564563 if let Some(response) = protocol
565− .request::<context_server::types::request::ListTools>(())
564+ .request::<context_server::types::requests::ListTools>(())
566565 .await
567566 .log_err()
568567 {
@@ -603,7 +602,7 @@ pub struct SerializedThreadMetadata { · 34 unmodified lines
603602 pub updated_at: DateTime<Utc>,
604603 }
605604
606−#[derive(Serialize, Deserialize, Debug)]
605+#[derive(Serialize, Deserialize, Debug, PartialEq)]
607606 pub struct SerializedThread {
608607 pub version: String,
609608 pub summary: SharedString,
@@ -629,7 +628,7 @@ pub struct SerializedThread { · 19 unmodified lines
629628 pub profile: Option<AgentProfileId>,
630629 }
631630
632−#[derive(Serialize, Deserialize, Debug)]
631+#[derive(Serialize, Deserialize, Debug, PartialEq)]
633632 pub struct SerializedLanguageModel {
634633 pub provider: String,
635634 pub model: String,
@@ -690,11 +689,15 @@ impl SerializedThreadV0_1_0 { · 54 unmodified lines
690689 messages.push(message);
691690 }
692691
693− SerializedThread { messages, ..self.0 }
692+ SerializedThread {
693+ messages,
694+ version: SerializedThread::VERSION.to_string(),
695+ ..self.0
696+ }
694697 }
695698 }
696699
697−#[derive(Debug, Serialize, Deserialize)]
700+#[derive(Debug, Serialize, Deserialize, PartialEq)]
698701 pub struct SerializedMessage {
699702 pub id: MessageId,
700703 pub role: Role,
@@ -712,7 +715,7 @@ pub struct SerializedMessage { · 11 unmodified lines
712715 pub is_hidden: bool,
713716 }
714717
715−#[derive(Debug, Serialize, Deserialize)]
718+#[derive(Debug, Serialize, Deserialize, PartialEq)]
716719 #[serde(tag = "type")]
717720 pub enum SerializedMessageSegment {
718721 #[serde(rename = "text")]
@@ -730,14 +733,14 @@ pub enum SerializedMessageSegment { · 11 unmodified lines
730733 },
731734 }
732735
733−#[derive(Debug, Serialize, Deserialize)]
736+#[derive(Debug, Serialize, Deserialize, PartialEq)]
734737 pub struct SerializedToolUse {
735738 pub id: LanguageModelToolUseId,
736739 pub name: SharedString,
737740 pub input: serde_json::Value,
738741 }
739742
740−#[derive(Debug, Serialize, Deserialize)]
743+#[derive(Debug, Serialize, Deserialize, PartialEq)]
741744 pub struct SerializedToolResult {
742745 pub tool_use_id: LanguageModelToolUseId,
743746 pub is_error: bool,
@@ -800,7 +803,7 @@ impl LegacySerializedMessage { · 56 unmodified lines
800803 }
801804 }
802805
803−#[derive(Debug, Serialize, Deserialize)]
806+#[derive(Debug, Serialize, Deserialize, PartialEq)]
804807 pub struct SerializedCrease {
805808 pub start: usize,
806809 pub end: usize,
@@ -919,7 +922,7 @@ impl ThreadsDatabase { · 112 unmodified lines
919922
920923 fn bytes_encode(
921924 item: &Self::EItem,
922− ) -> Result<std::borrow::Cow<[u8]>, heed::BoxedError> {
925+ ) -> Result<std::borrow::Cow<'_, [u8]>, heed::BoxedError> {
923926 serde_json::to_vec(&item.0)
924927 .map(std::borrow::Cow::Owned)
925928 .map_err(Into::into)
@@ -1057,3 +1060,181 @@ impl ThreadsDatabase { · 131 unmodified lines
10571060 })
10581061 }
10591062 }
1063+
1064+#[cfg(test)]
1065+mod 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
22 use component::{empty_example, example_group_with_title, single_example};
33 use gpui::{AnyElement, App, IntoElement, RenderOnce, Window};
44 use language_model::RequestUsage;
5−use ui::{Callout, Color, Icon, IconName, IconSize, prelude::*};
5+use ui::{Callout, prelude::*};
66 use zed_llm_client::{Plan, UsageLimit};
77
88 #[derive(IntoElement, RegisterComponent)]
@@ -91,16 +91,23 @@ impl RenderOnce for UsageCallout { · 82 unmodified lines
9191 .size(IconSize::XSmall)
9292 };
9393
94− Callout::multi_line(
95− title,
96− message,
97− icon,
98− button_text,
99− Box::new(move |_, _, cx| {
100− cx.open_url(&url);
101− }),
102− )
103− .into_any_element()
94+ div()
95+ .border_t_1()
96+ .border_color(cx.theme().colors().border)
97+ .child(
98+ Callout::new()
99+ .icon(icon)
100+ .title(title)
101+ .description(message)
102+ .primary_action(
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()
104111 }
105112 }
106113
@@ -189,10 +196,8 @@ impl Component for UsageCallout { · 82 unmodified lines
189196 );
190197
191198 Some(
192− div()
199+ v_flex()
193200 .p_4()
194− .flex()
195− .flex_col()
196201 .gap_4()
197202 .child(free_examples)
198203 .child(trial_examples)
crates/anthropic/src/anthropic.rs+103−71
@@ -1,4 +1,5 @@
11 use std::str::FromStr;
2+use std::time::Duration;
23
34 use anyhow::{Context as _, Result, anyhow};
45 use chrono::{DateTime, Utc};
@@ -32,15 +33,6 @@ pub enum AnthropicModelMode { · 27 unmodified lines
3233 #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
3334 #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, EnumIter)]
3435 pub 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,
4436 #[serde(rename = "claude-opus-4", alias = "claude-opus-4-latest")]
4537 ClaudeOpus4,
4638 #[serde(
@@ -56,6 +48,15 @@ pub enum Model { · 9 unmodified lines
5648 alias = "claude-sonnet-4-thinking-latest"
5749 )]
5850 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,
5960 #[serde(rename = "claude-3-5-haiku", alias = "claude-3-5-haiku-latest")]
6061 Claude3_5Haiku,
6162 #[serde(rename = "claude-3-opus", alias = "claude-3-opus-latest")]
@@ -89,46 +90,66 @@ impl Model { · 27 unmodified lines
8990 }
9091
9192 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+
92117 if id.starts_with("claude-3-5-sonnet") {
93− Ok(Self::Claude3_5Sonnet)
94− } else if id.starts_with("claude-3-7-sonnet-thinking") {
95− Ok(Self::Claude3_7SonnetThinking)
96− } else if id.starts_with("claude-3-7-sonnet") {
97− Ok(Self::Claude3_7Sonnet)
98− } else if id.starts_with("claude-3-5-haiku") {
99− Ok(Self::Claude3_5Haiku)
100− } else if id.starts_with("claude-3-opus") {
101− 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}");
118+ return Ok(Self::Claude3_5Sonnet);
119+ }
120+
121+ if id.starts_with("claude-3-5-haiku") {
122+ return Ok(Self::Claude3_5Haiku);
123+ }
124+
125+ if id.starts_with("claude-3-opus") {
126+ return Ok(Self::Claude3Opus);
116127 }
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}"))
117138 }
118139
119140 pub fn id(&self) -> &str {
120141 match self {
121− Model::ClaudeOpus4 => "claude-opus-4-latest",
122− Model::ClaudeOpus4Thinking => "claude-opus-4-thinking-latest",
123− Model::ClaudeSonnet4 => "claude-sonnet-4-latest",
124− Model::ClaudeSonnet4Thinking => "claude-sonnet-4-thinking-latest",
125− Model::Claude3_5Sonnet => "claude-3-5-sonnet-latest",
126− Model::Claude3_7Sonnet => "claude-3-7-sonnet-latest",
127− Model::Claude3_7SonnetThinking => "claude-3-7-sonnet-thinking-latest",
128− Model::Claude3_5Haiku => "claude-3-5-haiku-latest",
129− Model::Claude3Opus => "claude-3-opus-latest",
130− Model::Claude3Sonnet => "claude-3-sonnet-20240229",
131− Model::Claude3Haiku => "claude-3-haiku-20240307",
142+ Self::ClaudeOpus4 => "claude-opus-4-latest",
143+ Self::ClaudeOpus4Thinking => "claude-opus-4-thinking-latest",
144+ Self::ClaudeSonnet4 => "claude-sonnet-4-latest",
145+ Self::ClaudeSonnet4Thinking => "claude-sonnet-4-thinking-latest",
146+ Self::Claude3_5Sonnet => "claude-3-5-sonnet-latest",
147+ Self::Claude3_7Sonnet => "claude-3-7-sonnet-latest",
148+ Self::Claude3_7SonnetThinking => "claude-3-7-sonnet-thinking-latest",
149+ Self::Claude3_5Haiku => "claude-3-5-haiku-latest",
150+ Self::Claude3Opus => "claude-3-opus-latest",
151+ Self::Claude3Sonnet => "claude-3-sonnet-20240229",
152+ Self::Claude3Haiku => "claude-3-haiku-20240307",
132153 Self::Custom { name, .. } => name,
133154 }
134155 }
@@ -136,24 +157,24 @@ impl Model { · 1 unmodified line
136157 /// The id of the model that should be used for making API requests
137158 pub fn request_id(&self) -> &str {
138159 match self {
139− Model::ClaudeOpus4 | Model::ClaudeOpus4Thinking => "claude-opus-4-20250514",
140− Model::ClaudeSonnet4 | Model::ClaudeSonnet4Thinking => "claude-sonnet-4-20250514",
141− Model::Claude3_5Sonnet => "claude-3-5-sonnet-latest",
142− Model::Claude3_7Sonnet | Model::Claude3_7SonnetThinking => "claude-3-7-sonnet-latest",
143− Model::Claude3_5Haiku => "claude-3-5-haiku-latest",
144− Model::Claude3Opus => "claude-3-opus-latest",
145− Model::Claude3Sonnet => "claude-3-sonnet-20240229",
146− Model::Claude3Haiku => "claude-3-haiku-20240307",
160+ Self::ClaudeOpus4 | Self::ClaudeOpus4Thinking => "claude-opus-4-20250514",
161+ Self::ClaudeSonnet4 | Self::ClaudeSonnet4Thinking => "claude-sonnet-4-20250514",
162+ Self::Claude3_5Sonnet => "claude-3-5-sonnet-latest",
163+ Self::Claude3_7Sonnet | Self::Claude3_7SonnetThinking => "claude-3-7-sonnet-latest",
164+ Self::Claude3_5Haiku => "claude-3-5-haiku-latest",
165+ Self::Claude3Opus => "claude-3-opus-latest",
166+ Self::Claude3Sonnet => "claude-3-sonnet-20240229",
167+ Self::Claude3Haiku => "claude-3-haiku-20240307",
147168 Self::Custom { name, .. } => name,
148169 }
149170 }
150171
151172 pub fn display_name(&self) -> &str {
152173 match self {
153− Model::ClaudeOpus4 => "Claude Opus 4",
154− Model::ClaudeOpus4Thinking => "Claude Opus 4 Thinking",
155− Model::ClaudeSonnet4 => "Claude Sonnet 4",
156− Model::ClaudeSonnet4Thinking => "Claude Sonnet 4 Thinking",
174+ Self::ClaudeOpus4 => "Claude Opus 4",
175+ Self::ClaudeOpus4Thinking => "Claude Opus 4 Thinking",
176+ Self::ClaudeSonnet4 => "Claude Sonnet 4",
177+ Self::ClaudeSonnet4Thinking => "Claude Sonnet 4 Thinking",
157178 Self::Claude3_7Sonnet => "Claude 3.7 Sonnet",
158179 Self::Claude3_5Sonnet => "Claude 3.5 Sonnet",
159180 Self::Claude3_7SonnetThinking => "Claude 3.7 Sonnet Thinking",
@@ -209,15 +230,15 @@ impl Model { · 49 unmodified lines
209230
210231 pub fn max_output_tokens(&self) -> u32 {
211232 match self {
212− Self::Claude3Opus | Self::Claude3Sonnet | Self::Claude3Haiku => 4_096,
213− Self::Claude3_5Sonnet
214− | Self::Claude3_7Sonnet
215− | Self::Claude3_7SonnetThinking
216− | Self::Claude3_5Haiku
217− | Self::ClaudeOpus4
233+ Self::ClaudeOpus4
218234 | Self::ClaudeOpus4Thinking
219235 | 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,
221242 Self::Custom {
222243 max_output_tokens, ..
223244 } => max_output_tokens.unwrap_or(4_096),
@@ -246,17 +267,17 @@ impl Model { · 22 unmodified lines
246267
247268 pub fn mode(&self) -> AnthropicModelMode {
248269 match self {
249− Self::Claude3_5Sonnet
270+ Self::ClaudeOpus4
271+ | Self::ClaudeSonnet4
272+ | Self::Claude3_5Sonnet
250273 | Self::Claude3_7Sonnet
251274 | Self::Claude3_5Haiku
252− | Self::ClaudeOpus4
253− | Self::ClaudeSonnet4
254275 | Self::Claude3Opus
255276 | Self::Claude3Sonnet
256277 | Self::Claude3Haiku => AnthropicModelMode::Default,
257− Self::Claude3_7SonnetThinking
258− | Self::ClaudeOpus4Thinking
259− | Self::ClaudeSonnet4Thinking => AnthropicModelMode::Thinking {
278+ Self::ClaudeOpus4Thinking
279+ | Self::ClaudeSonnet4Thinking
280+ | Self::Claude3_7SonnetThinking => AnthropicModelMode::Thinking {
260281 budget_tokens: Some(4_096),
261282 },
262283 Self::Custom { mode, .. } => mode.clone(),
@@ -267,7 +288,7 @@ impl Model { · 4 unmodified lines
267288
268289 pub fn beta_headers(&self) -> String {
269290 let mut headers = Self::DEFAULT_BETA_HEADERS
270− .into_iter()
291+ .iter()
271292 .map(|header| header.to_string())
272293 .collect::<Vec<_>>();
273294
@@ -406,6 +427,7 @@ impl RateLimit { · 132 unmodified lines
406427 /// <https://docs.anthropic.com/en/api/rate-limits#response-headers>
407428 #[derive(Debug)]
408429 pub struct RateLimitInfo {
430+ pub retry_after: Option<Duration>,
409431 pub requests: Option<RateLimit>,
410432 pub tokens: Option<RateLimit>,
411433 pub input_tokens: Option<RateLimit>,
@@ -417,10 +439,11 @@ impl RateLimitInfo { · 5 unmodified lines
417439 // Check if any rate limit headers exist
418440 let has_rate_limit_headers = headers
419441 .keys()
420− .any(|k| k.as_str().starts_with("anthropic-ratelimit-"));
442+ .any(|k| k == "retry-after" || k.as_str().starts_with("anthropic-ratelimit-"));
421443
422444 if !has_rate_limit_headers {
423445 return Self {
446+ retry_after: None,
424447 requests: None,
425448 tokens: None,
426449 input_tokens: None,
@@ -429,6 +452,11 @@ impl RateLimitInfo { · 2 unmodified lines
429452 }
430453
431454 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),
432460 requests: RateLimit::from_headers("requests", headers).ok(),
433461 tokens: RateLimit::from_headers("tokens", headers).ok(),
434462 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
481509 .send(request)
482510 .await
483511 .context("failed to send request to Anthropic")?;
512+ let rate_limits = RateLimitInfo::from_headers(response.headers());
484513 if response.status().is_success() {
485− let rate_limits = RateLimitInfo::from_headers(response.headers());
486514 let reader = BufReader::new(response.into_body());
487515 let stream = reader
488516 .lines()
@@ -500,6 +528,8 @@ pub async fn stream_completion_with_rate_limit_info( · 11 unmodified lines
500528 })
501529 .boxed();
502530 Ok((stream, Some(rate_limits)))
531+ } else if let Some(retry_after) = rate_limits.retry_after {
532+ Err(AnthropicError::RateLimit(retry_after))
503533 } else {
504534 let mut body = Vec::new();
505535 response
@@ -769,6 +799,8 @@ pub struct MessageDelta { · 263 unmodified lines
769799
770800 #[derive(Error, Debug)]
771801 pub enum AnthropicError {
802+ #[error("rate limit exceeded, retry after {0:?}")]
803+ RateLimit(Duration),
772804 #[error("an error occurred while interacting with the Anthropic API: {error_type}: {message}", error_type = .0.error_type, message = .0.message)]
773805 ApiError(ApiError),
774806 #[error("{0}")]
crates/askpass/Cargo.toml+0−1
@@ -15,7 +15,6 @@ path = "src/askpass.rs" · 14 unmodified lines
1515 anyhow.workspace = true
1616 futures.workspace = true
1717 gpui.workspace = true
18−shlex.workspace = true
1918 smol.workspace = true
2019 tempfile.workspace = true
2120 util.workspace = true
crates/askpass/src/askpass.rs+2−32
@@ -16,6 +16,8 @@ use smol::fs; · 15 unmodified lines
1616 use smol::{fs::unix::PermissionsExt as _, net::unix::UnixListener};
1717 #[cfg(unix)]
1818 use util::ResultExt as _;
19+#[cfg(unix)]
20+use util::get_shell_safe_zed_path;
1921
2022 #[derive(PartialEq, Eq)]
2123 pub enum AskPassResult {
@@ -160,38 +162,6 @@ impl AskPassSession { · 138 unmodified lines
160162 }
161163 }
162164
163−#[cfg(unix)]
164−fn 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−
195165 /// The main function for when Zed is running in netcat mode for use in askpass.
196166 /// Called from both the remote server binary and the zed binary in their respective main functions.
197167 #[cfg(unix)]
crates/assistant_context_editor/src/context_editor.rs+19−44
@@ -39,7 +39,7 @@ use language::{ · 38 unmodified lines
3939 language_settings::{SoftWrap, all_language_settings},
4040 };
4141 use language_model::{
42− LanguageModelImage, LanguageModelProvider, LanguageModelProviderTosView, LanguageModelRegistry,
42+ ConfigurationError, LanguageModelImage, LanguageModelProviderTosView, LanguageModelRegistry,
4343 Role,
4444 };
4545 use multi_buffer::MultiBufferRow;
@@ -1887,6 +1887,8 @@ impl ContextEditor { · 1841 unmodified lines
18871887 // value to not show the nudge.
18881888 let nudge = Some(false);
18891889
1890+ let model_registry = LanguageModelRegistry::read_global(cx);
1891+
18901892 if nudge.map_or(false, |value| value) {
18911893 Some(
18921894 h_flex()
@@ -1935,14 +1937,9 @@ impl ContextEditor { · 42 unmodified lines
19351937 )
19361938 .into_any_element(),
19371939 )
1938− } else if let Some(configuration_error) = configuration_error(cx) {
1939− let label = match configuration_error {
1940− ConfigurationError::NoProvider => "No LLM provider selected.",
1941− ConfigurationError::ProviderNotAuthenticated => "LLM provider is not configured.",
1942− ConfigurationError::ProviderPendingTermsAcceptance(_) => {
1943− "LLM provider requires accepting the Terms of Service."
1944− }
1945− };
1940+ } else if let Some(configuration_error) =
1941+ model_registry.configuration_error(model_registry.default_model(), cx)
1942+ {
19461943 Some(
19471944 h_flex()
19481945 .px_3()
@@ -1959,7 +1956,7 @@ impl ContextEditor { · 10 unmodified lines
19591956 .size(IconSize::Small)
19601957 .color(Color::Warning),
19611958 )
1962− .child(Label::new(label)),
1959+ .child(Label::new(configuration_error.to_string())),
19631960 )
19641961 .child(
19651962 Button::new("open-configuration", "Configure Providers")
@@ -2034,14 +2031,19 @@ impl ContextEditor { · 68 unmodified lines
20342031 /// Will return false if the selected provided has a configuration error or
20352032 /// if the user has not accepted the terms of service for this provider.
20362033 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+ };
20382040
2039− let has_configuration_error = configuration_error(cx).is_some();
2040− let needs_to_accept_terms = self.show_accept_terms
2041− && model
2042− .as_ref()
2043− .map_or(false, |model| model.provider.must_accept_terms(cx));
2044− has_configuration_error || needs_to_accept_terms
2041+ match configuration_error {
2042+ ConfigurationError::NoProvider
2043+ | ConfigurationError::ModelNotFound
2044+ | ConfigurationError::ProviderNotAuthenticated(_) => true,
2045+ ConfigurationError::ProviderPendingTermsAcceptance(_) => self.show_accept_terms,
2046+ }
20452047 }
20462048
20472049 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
31803182 }
31813183 }
31823184
3183−pub enum ConfigurationError {
3184− NoProvider,
3185− ProviderNotAuthenticated,
3186− ProviderPendingTermsAcceptance(Arc<dyn LanguageModelProvider>),
3187−}
3188−
3189−fn 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−
32103185 pub fn humanize_token_count(count: usize) -> String {
32113186 match count {
32123187 0..=999 => count.to_string(),
crates/assistant_context_editor/src/context_store.rs+1−1
@@ -869,7 +869,7 @@ impl ContextStore { · 868 unmodified lines
869869
870870 if protocol.capable(context_server::protocol::ServerCapability::Prompts) {
871871 if let Some(response) = protocol
872− .request::<context_server::types::request::PromptsList>(())
872+ .request::<context_server::types::requests::PromptsList>(())
873873 .await
874874 .log_err()
875875 {
crates/assistant_context_editor/src/language_model_selector.rs+3−2
@@ -682,11 +682,12 @@ mod tests { · 681 unmodified lines
682682 _: &AsyncApp,
683683 ) -> BoxFuture<
684684 'static,
685− http_client::Result<
685+ Result<
686686 BoxStream<
687687 'static,
688− http_client::Result<LanguageModelCompletionEvent, LanguageModelCompletionError>,
688+ Result<LanguageModelCompletionEvent, LanguageModelCompletionError>,
689689 >,
690+ LanguageModelCompletionError,
690691 >,
691692 > {
692693 unimplemented!()
crates/assistant_slash_commands/src/context_server_command.rs+2−2
@@ -87,7 +87,7 @@ impl SlashCommand for ContextServerSlashCommand { · 86 unmodified lines
8787 let protocol = server.client().context("Context server not initialized")?;
8888
8989 let response = protocol
90− .request::<context_server::types::request::CompletionComplete>(
90+ .request::<context_server::types::requests::CompletionComplete>(
9191 context_server::types::CompletionCompleteParams {
9292 reference: context_server::types::CompletionReference::Prompt(
9393 context_server::types::PromptReference {
@@ -145,7 +145,7 @@ impl SlashCommand for ContextServerSlashCommand { · 51 unmodified lines
145145 cx.foreground_executor().spawn(async move {
146146 let protocol = server.client().context("Context server not initialized")?;
147147 let response = protocol
148− .request::<context_server::types::request::PromptsGet>(
148+ .request::<context_server::types::requests::PromptsGet>(
149149 context_server::types::PromptsGetParams {
150150 name: prompt_name.clone(),
151151 arguments: Some(prompt_args),
crates/assistant_slash_commands/src/file_command.rs+17−24
@@ -582,7 +582,7 @@ mod test { · 581 unmodified lines
582582 use serde_json::json;
583583 use settings::SettingsStore;
584584 use smol::stream::StreamExt;
585− use util::{path, separator};
585+ use util::path;
586586
587587 use super::collect_files;
588588
@@ -627,7 +627,7 @@ mod test { · 38 unmodified lines
627627 .await
628628 .unwrap();
629629
630− assert!(result_1.text.starts_with(separator!("root/dir")));
630+ assert!(result_1.text.starts_with(path!("root/dir")));
631631 // 4 files + 2 directories
632632 assert_eq!(result_1.sections.len(), 6);
633633
@@ -643,7 +643,7 @@ mod test { · 9 unmodified lines
643643 cx.update(|cx| collect_files(project.clone(), &["root/dir*".to_string()], cx).boxed());
644644 let result = SlashCommandOutput::from_event_stream(result).await.unwrap();
645645
646− assert!(result.text.starts_with(separator!("root/dir")));
646+ assert!(result.text.starts_with(path!("root/dir")));
647647 // 5 files + 2 directories
648648 assert_eq!(result.sections.len(), 7);
649649
@@ -691,24 +691,20 @@ mod test { · 41 unmodified lines
691691 .unwrap();
692692
693693 // Sanity check
694− assert!(result.text.starts_with(separator!("zed/assets/themes\n")));
694+ assert!(result.text.starts_with(path!("zed/assets/themes\n")));
695695 assert_eq!(result.sections.len(), 7);
696696
697697 // Ensure that full file paths are included in the real output
698698 assert!(
699699 result
700700 .text
701− .contains(separator!("zed/assets/themes/andromeda/LICENSE"))
701+ .contains(path!("zed/assets/themes/andromeda/LICENSE"))
702702 );
703+ assert!(result.text.contains(path!("zed/assets/themes/ayu/LICENSE")));
703704 assert!(
704705 result
705706 .text
706− .contains(separator!("zed/assets/themes/ayu/LICENSE"))
707− );
708− assert!(
709− result
710− .text
711− .contains(separator!("zed/assets/themes/summercamp/LICENSE"))
707+ .contains(path!("zed/assets/themes/summercamp/LICENSE"))
712708 );
713709
714710 assert_eq!(result.sections[5].label, "summercamp");
@@ -716,17 +712,17 @@ mod test { · 1 unmodified line
716712 // Ensure that things are in descending order, with properly relativized paths
717713 assert_eq!(
718714 result.sections[0].label,
719− separator!("zed/assets/themes/andromeda/LICENSE")
715+ path!("zed/assets/themes/andromeda/LICENSE")
720716 );
721717 assert_eq!(result.sections[1].label, "andromeda");
722718 assert_eq!(
723719 result.sections[2].label,
724− separator!("zed/assets/themes/ayu/LICENSE")
720+ path!("zed/assets/themes/ayu/LICENSE")
725721 );
726722 assert_eq!(result.sections[3].label, "ayu");
727723 assert_eq!(
728724 result.sections[4].label,
729− separator!("zed/assets/themes/summercamp/LICENSE")
725+ path!("zed/assets/themes/summercamp/LICENSE")
730726 );
731727
732728 // Ensure that the project lasts until after the last await
@@ -767,31 +763,28 @@ mod test { · 34 unmodified lines
767763 .await
768764 .unwrap();
769765
770− assert!(result.text.starts_with(separator!("zed/assets/themes\n")));
771− assert_eq!(
772− result.sections[0].label,
773− separator!("zed/assets/themes/LICENSE")
774− );
766+ assert!(result.text.starts_with(path!("zed/assets/themes\n")));
767+ assert_eq!(result.sections[0].label, path!("zed/assets/themes/LICENSE"));
775768 assert_eq!(
776769 result.sections[1].label,
777− separator!("zed/assets/themes/summercamp/LICENSE")
770+ path!("zed/assets/themes/summercamp/LICENSE")
778771 );
779772 assert_eq!(
780773 result.sections[2].label,
781− separator!("zed/assets/themes/summercamp/subdir/LICENSE")
774+ path!("zed/assets/themes/summercamp/subdir/LICENSE")
782775 );
783776 assert_eq!(
784777 result.sections[3].label,
785− separator!("zed/assets/themes/summercamp/subdir/subsubdir/LICENSE")
778+ path!("zed/assets/themes/summercamp/subdir/subsubdir/LICENSE")
786779 );
787780 assert_eq!(result.sections[4].label, "subsubdir");
788781 assert_eq!(result.sections[5].label, "subdir");
789782 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"));
791784
792785 assert_eq!(
793786 result.text,
794− separator!(
787+ path!(
795788 "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"
796789 )
797790 );
crates/assistant_tools/Cargo.toml+1−0
@@ -80,6 +80,7 @@ rand.workspace = true · 79 unmodified lines
8080 pretty_assertions.workspace = true
8181 reqwest_client.workspace = true
8282 settings = { workspace = true, features = ["test-support"] }
83+smol.workspace = true
8384 task = { workspace = true, features = ["test-support"]}
8485 tempfile.workspace = true
8586 theme.workspace = true
crates/assistant_tools/src/edit_agent.rs+33−17
@@ -286,7 +286,13 @@ impl EditAgent { · 285 unmodified lines
286286 _ => {
287287 let ranges = resolved_old_text
288288 .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+ })
290296 .collect();
291297 output_events
292298 .unbounded_send(EditAgentOutputEvent::AmbiguousEditRange(ranges))
@@ -429,25 +435,25 @@ impl EditAgent { · 136 unmodified lines
429435 let task = cx.background_spawn(async move {
430436 let mut matcher = StreamingFuzzyMatcher::new(snapshot);
431437 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 {
433444 break;
434445 };
435446
436− old_range_tx.send(matcher.push(&chunk))?;
447+ old_range_tx.send(matcher.push(&chunk, line_hint))?;
437448 if done {
438449 break;
439450 }
440451 }
441452
442453 let matches = matcher.finish();
454+ let best_match = matcher.select_best_match();
443455
444− let old_range = if matches.len() == 1 {
445− matches.first()
446− } else {
447− // No matches or multiple ambiguous matches
448− None
449− };
450− old_range_tx.send(old_range.cloned())?;
456+ old_range_tx.send(best_match.clone())?;
451457
452458 let indent = LineIndent::from_iter(
453459 matcher
@@ -456,10 +462,18 @@ impl EditAgent { · 2 unmodified lines
456462 .unwrap_or(&String::new())
457463 .chars(),
458464 );
459− let resolved_old_texts = matches
460− .into_iter()
461− .map(|range| ResolvedOldText { range, indent })
462− .collect::<Vec<_>>();
465+
466+ let resolved_old_texts = if let Some(best_match) = best_match {
467+ vec![ResolvedOldText {
468+ range: best_match,
469+ indent,
470+ }]
471+ } else {
472+ matches
473+ .into_iter()
474+ .map(|range| ResolvedOldText { range, indent })
475+ .collect::<Vec<_>>()
476+ };
463477
464478 Ok((edit_events, resolved_old_texts))
465479 });
@@ -1374,10 +1388,12 @@ mod tests { · 908 unmodified lines
13741388 &agent,
13751389 indoc! {"
13761390 <old_text>
1377− return 42;
1391+ return 42;
1392+ }
13781393 </old_text>
13791394 <new_text>
1380− return 100;
1395+ return 100;
1396+ }
13811397 </new_text>
13821398 "},
13831399 &mut rng,
@@ -1407,7 +1423,7 @@ mod tests { · 23 unmodified lines
14071423
14081424 // And AmbiguousEditRange even should be emitted
14091425 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];
14111427 assert!(
14121428 events.contains(&EditAgentOutputEvent::AmbiguousEditRange(ambiguous_ranges)),
14131429 "Should emit AmbiguousEditRange for non-unique text"
crates/assistant_tools/src/edit_agent/edit_parser.rs+127−9
@@ -1,4 +1,5 @@
11 use derive_more::{Add, AddAssign};
2+use regex::Regex;
23 use schemars::JsonSchema;
34 use serde::{Deserialize, Serialize};
45 use 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
1112
1213 #[derive(Debug)]
1314 pub enum EditParserEvent {
14− OldTextChunk { chunk: String, done: bool },
15− NewTextChunk { chunk: String, done: bool },
15+ OldTextChunk {
16+ chunk: String,
17+ done: bool,
18+ line_hint: Option<u32>,
19+ },
20+ NewTextChunk {
21+ chunk: String,
22+ done: bool,
23+ },
1624 }
1725
1826 #[derive(
@@ -33,7 +41,7 @@ pub struct EditParser { · 14 unmodified lines
3341 #[derive(Debug, PartialEq)]
3442 enum EditParserState {
3543 Pending,
36− WithinOldText { start: bool },
44+ WithinOldText { start: bool, line_hint: Option<u32> },
3745 AfterOldText,
3846 WithinNewText { start: bool },
3947 }
@@ -54,14 +62,24 @@ impl EditParser { · 14 unmodified lines
5462 loop {
5563 match &mut self.state {
5664 EditParserState::Pending => {
57− if let Some(start) = self.buffer.find("<old_text>") {
58− self.buffer.drain(..start + "<old_text>".len());
59− self.state = EditParserState::WithinOldText { start: true };
65+ if let Some(start) = self.buffer.find("<old_text") {
66+ if let Some(tag_end) = self.buffer[start..].find('>') {
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+ }
6078 } else {
6179 break;
6280 }
6381 }
64− EditParserState::WithinOldText { start } => {
82+ EditParserState::WithinOldText { start, line_hint } => {
6583 if !self.buffer.is_empty() {
6684 if *start && self.buffer.starts_with('\n') {
6785 self.buffer.remove(0);
@@ -69,6 +87,7 @@ impl EditParser { · 1 unmodified line
6987 *start = false;
7088 }
7189
90+ let line_hint = *line_hint;
7291 if let Some(tag_range) = self.find_end_tag() {
7392 let mut chunk = self.buffer[..tag_range.start].to_string();
7493 if chunk.ends_with('\n') {
@@ -82,12 +101,17 @@ impl EditParser { · 7 unmodified lines
82101
83102 self.buffer.drain(..tag_range.end);
84103 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+ });
86109 } else {
87110 if !self.ends_with_tag_prefix() {
88111 edit_events.push(EditParserEvent::OldTextChunk {
89112 chunk: mem::take(&mut self.buffer),
90113 done: false,
114+ line_hint,
91115 });
92116 }
93117 break;
@@ -154,6 +178,16 @@ impl EditParser { · 60 unmodified lines
154178 end_prefixes.any(|prefix| self.buffer.ends_with(&prefix))
155179 }
156180
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+
157191 pub fn finish(self) -> EditParserMetrics {
158192 self.metrics
159193 }
@@ -178,6 +212,7 @@ mod tests { · 18 unmodified lines
178212 vec![Edit {
179213 old_text: "original".to_string(),
180214 new_text: "updated".to_string(),
215+ line_hint: None,
181216 }]
182217 );
183218 assert_eq!(
@@ -209,10 +244,12 @@ mod tests { · 25 unmodified lines
209244 Edit {
210245 old_text: "first old".to_string(),
211246 new_text: "first new".to_string(),
247+ line_hint: None,
212248 },
213249 Edit {
214250 old_text: "second old".to_string(),
215251 new_text: "second new".to_string(),
252+ line_hint: None,
216253 },
217254 ]
218255 );
@@ -244,14 +281,17 @@ mod tests { · 25 unmodified lines
244281 Edit {
245282 old_text: "content".to_string(),
246283 new_text: "updated content".to_string(),
284+ line_hint: None,
247285 },
248286 Edit {
249287 old_text: "second item".to_string(),
250288 new_text: "modified second item".to_string(),
289+ line_hint: None,
251290 },
252291 Edit {
253292 old_text: "third case".to_string(),
254293 new_text: "improved third case".to_string(),
294+ line_hint: None,
255295 },
256296 ]
257297 );
@@ -276,6 +316,7 @@ mod tests { · 18 unmodified lines
276316 vec![Edit {
277317 old_text: "code with <tag>nested</tag> elements".to_string(),
278318 new_text: "new <code>content</code>".to_string(),
319+ line_hint: None,
279320 }]
280321 );
281322 assert_eq!(
@@ -299,6 +340,7 @@ mod tests { · 17 unmodified lines
299340 vec![Edit {
300341 old_text: "".to_string(),
301342 new_text: "".to_string(),
343+ line_hint: None,
302344 }]
303345 );
304346 assert_eq!(
@@ -322,6 +364,7 @@ mod tests { · 17 unmodified lines
322364 vec![Edit {
323365 old_text: "line1\nline2\nline3".to_string(),
324366 new_text: "line1\nmodified line2\nline3".to_string(),
367+ line_hint: None,
325368 }]
326369 );
327370 assert_eq!(
@@ -368,10 +411,12 @@ mod tests { · 40 unmodified lines
368411 Edit {
369412 old_text: "a\nb\nc".to_string(),
370413 new_text: "a\nB\nc".to_string(),
414+ line_hint: None,
371415 },
372416 Edit {
373417 old_text: "d\ne\nf".to_string(),
374418 new_text: "D\ne\nF".to_string(),
419+ line_hint: None,
375420 }
376421 ]
377422 );
@@ -402,6 +447,7 @@ mod tests { · 24 unmodified lines
402447 vec![Edit {
403448 old_text: "Lorem".to_string(),
404449 new_text: "LOREM".to_string(),
450+ line_hint: None,
405451 },]
406452 );
407453 assert_eq!(
@@ -413,10 +459,77 @@ mod tests { · 5 unmodified lines
413459 );
414460 }
415461
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+
416528 #[derive(Default, Debug, PartialEq, Eq)]
417529 struct Edit {
418530 old_text: String,
419531 new_text: String,
532+ line_hint: Option<u32>,
420533 }
421534
422535 fn parse_random_chunks(input: &str, parser: &mut EditParser, rng: &mut StdRng) -> Vec<Edit> {
@@ -433,10 +546,15 @@ mod tests { · 10 unmodified lines
433546 for chunk_ix in chunk_indices {
434547 for event in parser.push(&input[last_ix..chunk_ix]) {
435548 match event {
436− EditParserEvent::OldTextChunk { chunk, done } => {
549+ EditParserEvent::OldTextChunk {
550+ chunk,
551+ done,
552+ line_hint,
553+ } => {
437554 old_text.as_mut().unwrap().push_str(&chunk);
438555 if done {
439556 pending_edit.old_text = old_text.take().unwrap();
557+ pending_edit.line_hint = line_hint;
440558 new_text = Some(String::new());
441559 }
442560 }
crates/assistant_tools/src/edit_agent/evals.rs+112−56
@@ -11,7 +11,7 @@ use client::{Client, UserStore}; · 10 unmodified lines
1111 use collections::HashMap;
1212 use fs::FakeFs;
1313 use futures::{FutureExt, future::LocalBoxFuture};
14−use gpui::{AppContext, TestAppContext};
14+use gpui::{AppContext, TestAppContext, Timer};
1515 use indoc::{formatdoc, indoc};
1616 use language_model::{
1717 LanguageModelRegistry, LanguageModelRequestTool, LanguageModelToolResult,
@@ -26,6 +26,7 @@ use std::{ · 8 unmodified lines
2626 cmp::Reverse,
2727 fmt::{self, Display},
2828 io::Write as _,
29+ path::Path,
2930 str::FromStr,
3031 sync::mpsc,
3132 };
@@ -38,10 +39,11 @@ fn eval_extract_handle_command_output() { · 6 unmodified lines
3839 //
3940 // Model | Pass rate
4041 // ----------------------------|----------
41− // claude-3.7-sonnet | 0.98
42− // gemini-2.5-pro-06-05 | 0.77
43− // gemini-2.5-flash | 0.11
44− // gpt-4.1 | 1.00
42+ // claude-3.7-sonnet | 0.99 (2025-06-14)
43+ // claude-sonnet-4 | 0.97 (2025-06-14)
44+ // gemini-2.5-pro-06-05 | 0.77 (2025-05-22)
45+ // gemini-2.5-flash | 0.11 (2025-05-22)
46+ // gpt-4.1 | 1.00 (2025-05-22)
4547
4648 let input_file_path = "root/blame.rs";
4749 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
110112 #[test]
111113 #[cfg_attr(not(feature = "eval"), ignore)]
112114 fn 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 |
113122 let input_file_path = "root/blame.rs";
114123 let input_file_content = include_str!("evals/fixtures/delete_run_git_blame/before.rs");
115124 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
165174 #[test]
166175 #[cfg_attr(not(feature = "eval"), ignore)]
167176 fn eval_translate_doc_comments() {
168− // Results for 2025-05-22
169− //
170177 // Model | Pass rate
171178 // ============================================
172179 //
173− // claude-3.7-sonnet |
174− // gemini-2.5-pro-preview-03-25 | 1.0
180+ // claude-3.7-sonnet | 1.0 (2025-06-14)
181+ // claude-sonnet-4 | 1.0 (2025-06-14)
182+ // gemini-2.5-pro-preview-03-25 | 1.0 (2025-05-22)
175183 // gemini-2.5-flash-preview-04-17 |
176184 // gpt-4.1 |
177185 let input_file_path = "root/canvas.rs";
@@ -228,13 +236,12 @@ fn eval_translate_doc_comments() { · 50 unmodified lines
228236 #[test]
229237 #[cfg_attr(not(feature = "eval"), ignore)]
230238 fn eval_use_wasi_sdk_in_compile_parser_to_wasm() {
231− // Results for 2025-05-22
232− //
233239 // Model | Pass rate
234240 // ============================================
235241 //
236− // claude-3.7-sonnet | 0.98
237− // gemini-2.5-pro-preview-03-25 | 0.99
242+ // claude-3.7-sonnet | 0.96 (2025-06-14)
243+ // claude-sonnet-4 | 0.11 (2025-06-14)
244+ // gemini-2.5-pro-preview-03-25 | 0.99 (2025-05-22)
238245 // gemini-2.5-flash-preview-04-17 |
239246 // gpt-4.1 |
240247 let input_file_path = "root/lib.rs";
@@ -354,13 +361,12 @@ fn eval_use_wasi_sdk_in_compile_parser_to_wasm() { · 113 unmodified lines
354361 #[test]
355362 #[cfg_attr(not(feature = "eval"), ignore)]
356363 fn eval_disable_cursor_blinking() {
357− // Results for 2025-05-22
358− //
359364 // Model | Pass rate
360365 // ============================================
361366 //
362− // claude-3.7-sonnet |
363− // gemini-2.5-pro-preview-03-25 | 1.0
367+ // claude-3.7-sonnet | 0.99 (2025-06-14)
368+ // claude-sonnet-4 | 0.85 (2025-06-14)
369+ // gemini-2.5-pro-preview-03-25 | 1.0 (2025-05-22)
364370 // gemini-2.5-flash-preview-04-17 |
365371 // gpt-4.1 |
366372 let input_file_path = "root/editor.rs";
@@ -438,14 +444,21 @@ fn eval_disable_cursor_blinking() { · 71 unmodified lines
438444 #[test]
439445 #[cfg_attr(not(feature = "eval"), ignore)]
440446 fn eval_from_pixels_constructor() {
441− // Results for 2025-05-22
447+ // 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.
442455 //
443456 // Model | Pass rate
444457 // ============================================
445458 //
446− // claude-3.7-sonnet |
447− // gemini-2.5-pro-preview-03-25 | 0.94
448− // gemini-2.5-flash-preview-04-17 |
459+ // claude-4.0-sonnet | 0.99
460+ // claude-3.7-sonnet | 0.88
461+ // gemini-2.5-pro-preview-03-25 | 0.96
449462 // gpt-4.1 |
450463 let input_file_path = "root/canvas.rs";
451464 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
455468 0.95,
456469 // For whatever reason, this eval produces more mismatched tags.
457470 // Increasing for now, let's see if we can bring this down.
458− 0.2,
471+ 0.25,
459472 EvalInput::from_conversation(
460473 vec![
461474 message(
@@ -641,15 +654,14 @@ fn eval_from_pixels_constructor() { · 179 unmodified lines
641654 #[test]
642655 #[cfg_attr(not(feature = "eval"), ignore)]
643656 fn eval_zode() {
644− // Results for 2025-05-22
645− //
646657 // Model | Pass rate
647658 // ============================================
648659 //
649− // claude-3.7-sonnet | 1.0
650− // gemini-2.5-pro-preview-03-25 | 1.0
651− // gemini-2.5-flash-preview-04-17 | 1.0
652− // gpt-4.1 | 1.0
660+ // claude-3.7-sonnet | 1.0 (2025-06-14)
661+ // claude-sonnet-4 | 1.0 (2025-06-14)
662+ // gemini-2.5-pro-preview-03-25 | 1.0 (2025-05-22)
663+ // gemini-2.5-flash-preview-04-17 | 1.0 (2025-05-22)
664+ // gpt-4.1 | 1.0 (2025-05-22)
653665 let input_file_path = "root/zode.py";
654666 let input_content = None;
655667 let edit_description = "Create the main Zode CLI script";
@@ -748,13 +760,12 @@ fn eval_zode() { · 92 unmodified lines
748760 #[test]
749761 #[cfg_attr(not(feature = "eval"), ignore)]
750762 fn eval_add_overwrite_test() {
751− // Results for 2025-05-22
752− //
753763 // Model | Pass rate
754764 // ============================================
755765 //
756− // claude-3.7-sonnet | 0.16
757− // gemini-2.5-pro-preview-03-25 | 0.35
766+ // claude-3.7-sonnet | 0.65 (2025-06-14)
767+ // claude-sonnet-4 | 0.07 (2025-06-14)
768+ // gemini-2.5-pro-preview-03-25 | 0.35 (2025-05-22)
758769 // gemini-2.5-flash-preview-04-17 |
759770 // gpt-4.1 |
760771 let input_file_path = "root/action_log.rs";
@@ -984,15 +995,14 @@ fn eval_create_empty_file() { · 223 unmodified lines
984995 // thoughts into it. This issue is not specific to empty files, but
985996 // it's easier to reproduce with them.
986997 //
987− // Results for 2025-05-21:
988− //
989998 // Model | Pass rate
990999 // ============================================
9911000 //
992− // claude-3.7-sonnet | 1.00
993− // gemini-2.5-pro-preview-03-25 | 1.00
994− // gemini-2.5-flash-preview-04-17 | 1.00
995− // gpt-4.1 | 1.00
1001+ // claude-3.7-sonnet | 1.00 (2025-06-14)
1002+ // claude-sonnet-4 | 1.00 (2025-06-14)
1003+ // gemini-2.5-pro-preview-03-25 | 1.00 (2025-05-21)
1004+ // gemini-2.5-flash-preview-04-17 | 1.00 (2025-05-21)
1005+ // gpt-4.1 | 1.00 (2025-05-21)
9961006 //
9971007 //
9981008 // TODO: gpt-4.1-mini errored 38 times:
@@ -1255,9 +1265,12 @@ impl EvalAssertion { · 256 unmodified lines
12551265 }],
12561266 ..Default::default()
12571267 };
1258− let mut response = judge
1259− .stream_completion_text(request, &cx.to_async())
1260− .await?;
1268+ let mut response = retry_on_rate_limit(async || {
1269+ Ok(judge
1270+ .stream_completion_text(request.clone(), &cx.to_async())
1271+ .await?)
1272+ })
1273+ .await?;
12611274 let mut output = String::new();
12621275 while let Some(chunk) = response.stream.next().await {
12631276 let chunk = chunk?;
@@ -1308,10 +1321,17 @@ fn eval( · 44 unmodified lines
13081321 run_eval(eval.clone(), tx.clone());
13091322
13101323 let executor = gpui::background_executor();
1324+ let semaphore = Arc::new(smol::lock::Semaphore::new(32));
13111325 for _ in 1..iterations {
13121326 let eval = eval.clone();
13131327 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();
13151335 }
13161336 drop(tx);
13171337
@@ -1539,6 +1559,7 @@ impl EditAgentTest { · 221 unmodified lines
15391559 .collect::<Vec<_>>();
15401560 let worktrees = vec![WorktreeContext {
15411561 root_name: "root".to_string(),
1562+ abs_path: Path::new("/path/to/root").into(),
15421563 rules_file: None,
15431564 }];
15441565 let prompt_builder = PromptBuilder::new(None)?;
@@ -1577,21 +1598,31 @@ impl EditAgentTest { · 32 unmodified lines
15771598 if let Some(input_content) = eval.input_content.as_deref() {
15781599 buffer.update(cx, |buffer, cx| buffer.set_text(input_content, cx));
15791600 }
1580− let (edit_output, _) = self.agent.edit(
1581− buffer.clone(),
1582− eval.edit_file_input.display_description,
1583− &conversation,
1584− &mut cx.to_async(),
1585− );
1586− edit_output.await?
1601+ retry_on_rate_limit(async || {
1602+ self.agent
1603+ .edit(
1604+ buffer.clone(),
1605+ eval.edit_file_input.display_description.clone(),
1606+ &conversation,
1607+ &mut cx.to_async(),
1608+ )
1609+ .0
1610+ .await
1611+ })
1612+ .await?
15871613 } else {
1588− let (edit_output, _) = self.agent.overwrite(
1589− buffer.clone(),
1590− eval.edit_file_input.display_description,
1591− &conversation,
1592− &mut cx.to_async(),
1593− );
1594− edit_output.await?
1614+ retry_on_rate_limit(async || {
1615+ self.agent
1616+ .overwrite(
1617+ buffer.clone(),
1618+ eval.edit_file_input.display_description.clone(),
1619+ &conversation,
1620+ &mut cx.to_async(),
1621+ )
1622+ .0
1623+ .await
1624+ })
1625+ .await?
15951626 };
15961627
15971628 let buffer_text = buffer.read_with(cx, |buffer, _| buffer.text());
@@ -1613,6 +1644,31 @@ impl EditAgentTest { · 15 unmodified lines
16131644 }
16141645 }
16151646
1647+async 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+
16161672 #[derive(Clone, Debug, Eq, PartialEq, Hash)]
16171673 struct EvalAssertionOutcome {
16181674 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
1010 pub struct StreamingFuzzyMatcher {
1111 snapshot: TextBufferSnapshot,
1212 query_lines: Vec<String>,
13+ line_hint: Option<u32>,
1314 incomplete_line: String,
14− best_matches: Vec<Range<usize>>,
15+ matches: Vec<Range<usize>>,
1516 matrix: SearchMatrix,
1617 }
1718
@@ -21,8 +22,9 @@ impl StreamingFuzzyMatcher { · 3 unmodified lines
2122 Self {
2223 snapshot,
2324 query_lines: Vec::new(),
25+ line_hint: None,
2426 incomplete_line: String::new(),
25− best_matches: Vec::new(),
27+ matches: Vec::new(),
2628 matrix: SearchMatrix::new(buffer_line_count + 1),
2729 }
2830 }
@@ -41,9 +43,10 @@ impl StreamingFuzzyMatcher { · 12 unmodified lines
4143 ///
4244 /// Returns `Some(range)` if a match has been found with the accumulated
4345 /// 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>> {
4547 // Add the chunk to our incomplete line buffer
4648 self.incomplete_line.push_str(chunk);
49+ self.line_hint = line_hint;
4750
4851 if let Some((last_pos, _)) = self.incomplete_line.match_indices('\n').next_back() {
4952 let complete_part = &self.incomplete_line[..=last_pos];
@@ -55,20 +58,11 @@ impl StreamingFuzzyMatcher { · 5 unmodified lines
5558
5659 self.incomplete_line.replace_range(..last_pos + 1, "");
5760
58− self.best_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− }
61+ self.matches = self.resolve_location_fuzzy();
7162 }
63+
64+ let best_match = self.select_best_match();
65+ best_match.or_else(|| self.matches.first().cloned())
7266 }
7367
7468 /// Finish processing and return the final best match(es).
@@ -80,9 +74,9 @@ impl StreamingFuzzyMatcher { · 5 unmodified lines
8074 if !self.incomplete_line.is_empty() {
8175 self.query_lines.push(self.incomplete_line.clone());
8276 self.incomplete_line.clear();
83− self.best_matches = self.resolve_location_fuzzy();
77+ self.matches = self.resolve_location_fuzzy();
8478 }
85− self.best_matches.clone()
79+ self.matches.clone()
8680 }
8781
8882 fn resolve_location_fuzzy(&mut self) -> Vec<Range<usize>> {
@@ -198,6 +192,43 @@ impl StreamingFuzzyMatcher { · 109 unmodified lines
198192
199193 valid_matches.into_iter().map(|(_, range)| range).collect()
200194 }
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+ }
201232 }
202233
203234 fn fuzzy_eq(left: &str, right: &str) -> bool {
@@ -640,6 +671,52 @@ mod tests { · 436 unmodified lines
640671 );
641672 }
642673
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+
643720 #[track_caller]
644721 fn assert_location_resolution(text_with_expected_range: &str, query: &str, rng: &mut StdRng) {
645722 let (text, expected_ranges) = marked_text_ranges(text_with_expected_range, false);
@@ -653,7 +730,7 @@ mod tests { · 7 unmodified lines
653730
654731 // Push chunks incrementally
655732 for chunk in &chunks {
656− matcher.push(chunk);
733+ matcher.push(chunk, None);
657734 }
658735
659736 let actual_ranges = matcher.finish();
@@ -706,7 +783,7 @@ mod tests { · 46 unmodified lines
706783
707784 fn push(finder: &mut StreamingFuzzyMatcher, chunk: &str) -> Option<String> {
708785 finder
709− .push(chunk)
786+ .push(chunk, None)
710787 .map(|range| finder.snapshot.text_for_range(range).collect::<String>())
711788 }
712789
crates/assistant_tools/src/edit_file_tool.rs+40−17
@@ -10,7 +10,7 @@ use assistant_tool::{ · 9 unmodified lines
1010 ToolUseStatus,
1111 };
1212 use buffer_diff::{BufferDiff, BufferDiffSnapshot};
13−use editor::{Editor, EditorMode, MinimapVisibility, MultiBuffer, PathKey};
13+use editor::{Editor, EditorMode, MinimapVisibility, MultiBuffer, PathKey, scroll::Autoscroll};
1414 use futures::StreamExt;
1515 use gpui::{
1616 Animation, AnimationExt, AnyWindowHandle, App, AppContext, AsyncApp, Entity, Task,
@@ -69,13 +69,13 @@ pub struct EditFileToolInput { · 52 unmodified lines
6969 /// start each path with one of the project's root directories.
7070 ///
7171 /// The following examples assume we have two root directories in the project:
72− /// - backend
73− /// - frontend
72+ /// - /a/b/backend
73+ /// - /c/d/frontend
7474 ///
7575 /// <example>
7676 /// `backend/src/main.rs`
7777 ///
78− /// Notice how the file path starts with root-1. Without that, the path
78+ /// Notice how the file path starts with `backend`. Without that, the path
7979 /// would be ambiguous and the call would fail!
8080 /// </example>
8181 ///
@@ -333,14 +333,18 @@ impl Tool for EditFileTool { · 251 unmodified lines
333333 );
334334 anyhow::ensure!(
335335 ambiguous_ranges.is_empty(),
336− // TODO: Include ambiguous_ranges, converted to line numbers.
337− // This would work best if we add `line_hint` parameter
338− // to edit_file_tool
339− formatdoc! {"
340− <old_text> matches more than one position in the file. Read the
341− relevant sections of {input_path} again and extend <old_text> so
342− that I can perform the requested edits.
343− "}
336+ {
337+ let line_numbers = ambiguous_ranges
338+ .iter()
339+ .map(|range| range.start.to_string())
340+ .collect::<Vec<_>>()
341+ .join(", ");
342+ formatdoc! {"
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+ }
344348 );
345349 Ok(ToolResultOutput {
346350 content: ToolResultContent::Text("No edits were made.".into()),
@@ -800,11 +804,30 @@ impl ToolCard for EditFileToolCard { · 453 unmodified lines
800804 if let Some(active_editor) = item.downcast::<Editor>() {
801805 active_editor
802806 .update_in(cx, |editor, window, cx| {
803− editor.go_to_singleton_buffer_point(
804− language::Point::new(0, 0),
805− window,
806− cx,
807− );
807+ let snapshot =
808+ editor.buffer().read(cx).snapshot(cx);
809+ let first_hunk = editor
810+ .diff_hunks_in_ranges(
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+ }
808831 })
809832 .log_err();
810833 }
crates/assistant_tools/src/read_file_tool.rs+2−2
@@ -31,8 +31,8 @@ pub struct ReadFileToolInput { · 30 unmodified lines
3131 /// <example>
3232 /// If the project has the following root directories:
3333 ///
34− /// - directory1
35− /// - directory2
34+ /// - /a/b/directory1
35+ /// - /c/d/directory2
3636 ///
3737 /// If you want to access `file.txt` in `directory1`, you should use the path `directory1/file.txt`.
3838 /// 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
33 ```
44 <edits>
55
6−<old_text>
6+<old_text line=10>
77 OLD TEXT 1 HERE
88 </old_text>
99 <new_text>
1010 NEW TEXT 1 HERE
1111 </new_text>
1212
13−<old_text>
13+<old_text line=456>
1414 OLD TEXT 2 HERE
1515 </old_text>
1616 <new_text>
1717 NEW TEXT 2 HERE
1818 </new_text>
1919
20−<old_text>
20+<old_text line=42>
2121 OLD TEXT 3 HERE
2222 </old_text>
2323 <new_text>
@@ -33,6 +33,7 @@ NEW TEXT 3 HERE · 9 unmodified lines
3333 - `<old_text>` must exactly match existing file content, including indentation
3434 - `<old_text>` must come from the actual file, not an outline
3535 - `<old_text>` cannot be empty
36+- `line` should be a starting line number for the text to be replaced
3637 - Be minimal with replacements:
3738 - For unique lines, include only those lines
3839 - 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
4849 <example>
4950 <edits>
5051
51−<old_text>
52+<old_text line=3>
5253 struct User {
5354 name: String,
5455 email: String,
@@ -62,7 +63,7 @@ struct User { · 7 unmodified lines
6263 }
6364 </new_text>
6465
65−<old_text>
66+<old_text line=25>
6667 let user = User {
6768 name: String::from("John"),
6869 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
638638 .bg(cx.theme().colors().editor_background)
639639 .rounded_b_md()
640640 .text_ui_sm(cx)
641− .child(
642− ToolOutputPreview::new(
643− terminal.clone().into_any_element(),
644− terminal.entity_id(),
645− )
646− .with_total_lines(self.content_line_count)
647− .toggle_state(!terminal.read(cx).is_content_limited(window))
648− .on_toggle({
649− let terminal = terminal.clone();
650− move |is_expanded, _, cx| {
651− terminal.update(cx, |terminal, cx| {
652− terminal.set_embedded_mode(
653− if is_expanded {
654− None
655− } else {
656− Some(COLLAPSED_LINES)
657− },
658− cx,
659− );
660− });
661− }
662− }),
663− ),
641+ .child({
642+ let content_mode = terminal.read(cx).content_mode(window, cx);
643+
644+ if content_mode.is_scrollable() {
645+ div().h_72().child(terminal.clone()).into_any_element()
646+ } else {
647+ ToolOutputPreview::new(
648+ terminal.clone().into_any_element(),
649+ terminal.entity_id(),
650+ )
651+ .with_total_lines(self.content_line_count)
652+ .toggle_state(!content_mode.is_limited())
653+ .on_toggle({
654+ let terminal = terminal.clone();
655+ move |is_expanded, _, cx| {
656+ terminal.update(cx, |terminal, cx| {
657+ terminal.set_embedded_mode(
658+ if is_expanded {
659+ None
660+ } else {
661+ Some(COLLAPSED_LINES)
662+ },
663+ cx,
664+ );
665+ });
666+ }
667+ })
668+ .into_any_element()
669+ }
670+ }),
664671 )
665672 },
666673 )
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
221221 }
222222
223223 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));
225225 } else {
226226 drop(window.prompt(
227227 gpui::PromptLevel::Info,
@@ -296,6 +296,11 @@ impl InstallerDir { · 68 unmodified lines
296296 }
297297 }
298298
299+pub enum UpdateCheckType {
300+ Automatic,
301+ Manual,
302+}
303+
299304 impl AutoUpdater {
300305 pub fn get(cx: &mut App) -> Option<Entity<Self>> {
301306 cx.default_global::<GlobalAutoUpdate>().0.clone()
@@ -313,13 +318,13 @@ impl AutoUpdater { · 11 unmodified lines
313318 pub fn start_polling(&self, cx: &mut Context<Self>) -> Task<Result<()>> {
314319 cx.spawn(async move |this, cx| {
315320 loop {
316− this.update(cx, |this, cx| this.poll(cx))?;
321+ this.update(cx, |this, cx| this.poll(UpdateCheckType::Automatic, cx))?;
317322 cx.background_executor().timer(POLL_INTERVAL).await;
318323 }
319324 })
320325 }
321326
322− pub fn poll(&mut self, cx: &mut Context<Self>) {
327+ pub fn poll(&mut self, check_type: UpdateCheckType, cx: &mut Context<Self>) {
323328 if self.pending_poll.is_some() {
324329 return;
325330 }
@@ -331,8 +336,18 @@ impl AutoUpdater { · 5 unmodified lines
331336 this.update(cx, |this, cx| {
332337 this.pending_poll = None;
333338 if let Err(error) = result {
334− log::error!("auto-update failed: error:{:?}", error);
335− this.status = AutoUpdateStatus::Errored;
339+ this.status = match check_type {
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+
336351 cx.notify();
337352 }
338353 })
crates/bedrock/src/models.rs+4−0
@@ -452,6 +452,10 @@ impl Model { · 451 unmodified lines
452452 | Model::Claude3_5SonnetV2
453453 | Model::Claude3_7Sonnet
454454 | Model::Claude3_7SonnetThinking
455+ | Model::ClaudeSonnet4
456+ | Model::ClaudeSonnet4Thinking
457+ | Model::ClaudeOpus4
458+ | Model::ClaudeOpus4Thinking
455459 | Model::Claude3Haiku
456460 | Model::Claude3Opus
457461 | Model::Claude3Sonnet
crates/channel/src/channel_store.rs+1−1
@@ -111,7 +111,7 @@ pub struct ChannelMembership { · 110 unmodified lines
111111 pub role: proto::ChannelRole,
112112 }
113113 impl ChannelMembership {
114− pub fn sort_key(&self) -> MembershipSortKey {
114+ pub fn sort_key(&self) -> MembershipSortKey<'_> {
115115 MembershipSortKey {
116116 role_order: match self.role {
117117 proto::ChannelRole::Admin => 0,
crates/channel/src/channel_store/channel_index.rs+1−1
@@ -32,7 +32,7 @@ impl ChannelIndex { · 31 unmodified lines
3232 .retain(|channel_id| !channels.contains(channel_id));
3333 }
3434
35− pub fn bulk_insert(&mut self) -> ChannelPathsInsertGuard {
35+ pub fn bulk_insert(&mut self) -> ChannelPathsInsertGuard<'_> {
3636 ChannelPathsInsertGuard {
3737 channels_ordered: &mut self.channels_ordered,
3838 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
269269 github_login: "nathansobo".into(),
270270 avatar_url: "http://avatar.com/nathansobo".into(),
271271 name: None,
272− email: None,
273272 }],
274273 },
275274 );
@@ -323,7 +322,6 @@ async fn test_channel_messages(cx: &mut TestAppContext) { · 47 unmodified lines
323322 github_login: "maxbrunsfeld".into(),
324323 avatar_url: "http://avatar.com/maxbrunsfeld".into(),
325324 name: None,
326− email: None,
327325 }],
328326 },
329327 );
@@ -368,7 +366,6 @@ async fn test_channel_messages(cx: &mut TestAppContext) { · 38 unmodified lines
368366 github_login: "as-cii".into(),
369367 avatar_url: "http://avatar.com/as-cii".into(),
370368 name: None,
371− email: None,
372369 }],
373370 },
374371 );
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
127127 }
128128
129129 fn main() -> Result<()> {
130+ #[cfg(unix)]
131+ util::prevent_root_execution();
132+
130133 // Exit flatpak sandbox if needed
131134 #[cfg(any(target_os = "linux", target_os = "freebsd"))]
132135 {
crates/client/Cargo.toml+7−2
@@ -28,6 +28,9 @@ feature_flags.workspace = true · 27 unmodified lines
2828 futures.workspace = true
2929 gpui.workspace = true
3030 gpui_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
33+hickory-resolver = { version = "0.24", features = ["tokio-runtime"] }
3134 http_client.workspace = true
3235 http_client_tls.workspace = true
3336 httparse = "1.10"
@@ -36,6 +39,7 @@ paths.workspace = true · 2 unmodified lines
3639 parking_lot.workspace = true
3740 postage.workspace = true
3841 rand.workspace = true
42+regex.workspace = true
3943 release_channel.workspace = true
4044 rpc = { workspace = true, features = ["gpui"] }
4145 schemars.workspace = true
@@ -48,7 +52,7 @@ telemetry_events.workspace = true · 6 unmodified lines
4852 text.workspace = true
4953 thiserror.workspace = true
5054 time.workspace = true
51−tiny_http = "0.8"
55+tiny_http.workspace = true
5256 tokio-socks = { version = "0.5.2", default-features = false, features = ["futures-io"] }
5357 url.workspace = true
5458 util.workspace = true
@@ -60,11 +64,12 @@ workspace-hack.workspace = true · 5 unmodified lines
6064 [dev-dependencies]
6165 clock = { workspace = true, features = ["test-support"] }
6266 collections = { workspace = true, features = ["test-support"] }
67+fs.workspace = true
6368 gpui = { workspace = true, features = ["test-support"] }
69+http_client = { workspace = true, features = ["test-support"] }
6470 rpc = { workspace = true, features = ["test-support"] }
6571 settings = { workspace = true, features = ["test-support"] }
6672 util = { workspace = true, features = ["test-support"] }
67−http_client = { workspace = true, features = ["test-support"] }
6873
6974 [target.'cfg(target_os = "windows")'.dependencies]
7075 windows.workspace = true
crates/client/src/client.rs+10−2
@@ -1887,8 +1887,16 @@ mod tests { · 1886 unmodified lines
18871887 .set_entity(&entity3, &mut cx.to_async());
18881888 drop(subscription3);
18891889
1890− server.send(proto::JoinProject { project_id: 1 });
1891− server.send(proto::JoinProject { project_id: 2 });
1890+ server.send(proto::JoinProject {
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+ });
18921900 done_rx1.recv().await.unwrap();
18931901 done_rx2.recv().await.unwrap();
18941902 }
crates/client/src/proxy.rs+44−6
@@ -3,20 +3,30 @@ · 2 unmodified lines
33 mod http_proxy;
44 mod socks_proxy;
55
6+use std::sync::LazyLock;
7+
68 use anyhow::{Context as _, Result};
9+use hickory_resolver::{
10+ AsyncResolver, TokioAsyncResolver,
11+ config::LookupIpStrategy,
12+ name_server::{GenericConnector, TokioRuntimeProvider},
13+ system_conf,
14+};
715 use http_client::Url;
816 use http_proxy::{HttpProxyType, connect_http_proxy_stream, parse_http_proxy};
917 use socks_proxy::{SocksVersion, connect_socks_proxy_stream, parse_socks_proxy};
18+use tokio_socks::{IntoTargetAddr, TargetAddr};
19+use util::ResultExt;
1020
1121 pub(crate) async fn connect_proxy_stream(
1222 proxy: &Url,
1323 rpc_host: (&str, u16),
1424 ) -> 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 {
1626 // If parsing the proxy URL fails, we must avoid falling back to an insecure connection.
1727 // SOCKS proxies are often used in contexts where security and privacy are critical,
1828 // so any fallback could expose users to significant risks.
19− anyhow::bail!("Parsing proxy url failed");
29+ anyhow::bail!("Parsing proxy url type failed");
2030 };
2131
2232 // Connect to proxy and wrap protocol later
@@ -39,10 +49,8 @@ enum ProxyType<'t> { · 16 unmodified lines
3949 HttpProxy(HttpProxyType<'t>),
4050 }
4151
42−fn parse_proxy_type(proxy: &Url) -> Option<((String, u16), ProxyType)> {
52+async fn parse_proxy_type(proxy: &Url) -> Option<((String, u16), ProxyType<'_>)> {
4353 let scheme = proxy.scheme();
44− let host = proxy.host()?.to_string();
45− let port = proxy.port_or_known_default()?;
4654 let proxy_type = match scheme {
4755 scheme if scheme.starts_with("socks") => {
4856 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
5260 }
5361 _ => None,
5462 }?;
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+
72+static 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+ });
5578
56− Some(((host, port), proxy_type))
79+async 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+ }
5795 }
5896
5997 pub(crate) trait AsyncReadWrite:
crates/client/src/proxy/socks_proxy.rs+8−2
@@ -1,5 +1,7 @@
11 //! socks proxy
22
3+use std::net::SocketAddr;
4+
35 use anyhow::{Context as _, Result};
46 use http_client::Url;
57 use tokio::net::TcpStream;
@@ -8,6 +10,8 @@ use tokio_socks::{ · 2 unmodified lines
810 tcp::{Socks4Stream, Socks5Stream},
911 };
1012
13+use crate::proxy::SYSTEM_DNS_RESOLVER;
14+
1115 use super::AsyncReadWrite;
1216
1317 /// Identification to a Socks V4 Proxy
@@ -73,12 +77,14 @@ pub(super) async fn connect_socks_proxy_stream( · 59 unmodified lines
7377 };
7478 let rpc_host = match (rpc_host, local_dns) {
7579 (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())
7782 .await
7883 .with_context(|| format!("Failed to lookup domain {}", domain))?
84+ .into_iter()
7985 .next()
8086 .ok_or_else(|| anyhow::anyhow!("Failed to lookup domain {}", domain))?;
81− TargetAddr::Ip(ip_addr)
87+ TargetAddr::Ip(SocketAddr::new(ip_addr, port))
8288 }
8389 (rpc_host, _) => rpc_host,
8490 };
crates/client/src/telemetry.rs+175−67
@@ -8,10 +8,11 @@ use futures::{Future, FutureExt, StreamExt}; · 7 unmodified lines
88 use gpui::{App, AppContext as _, BackgroundExecutor, Task};
99 use http_client::{self, AsyncBody, HttpClient, HttpClientWithUrl, Method, Request};
1010 use parking_lot::Mutex;
11+use regex::Regex;
1112 use release_channel::ReleaseChannel;
1213 use settings::{Settings, SettingsStore};
1314 use sha2::{Digest, Sha256};
14−use std::collections::{HashMap, HashSet};
15+use std::collections::HashSet;
1516 use std::fs::File;
1617 use std::io::Write;
1718 use std::sync::LazyLock;
@@ -45,31 +46,13 @@ struct TelemetryState { · 27 unmodified lines
4546 first_event_date_time: Option<Instant>,
4647 event_coalescer: EventCoalescer,
4748 max_queue_size: usize,
48− worktree_id_map: WorktreeIdMap,
49+ worktrees_with_project_type_events_sent: HashSet<WorktreeId>,
4950
5051 os_name: String,
5152 app_version: String,
5253 os_version: Option<String>,
5354 }
5455
55−#[derive(Debug)]
56−struct WorktreeIdMap(HashMap<String, ProjectCache>);
57−
58−#[derive(Debug)]
59−struct ProjectCache {
60− name: String,
61− worktree_ids_reported: HashSet<WorktreeId>,
62−}
63−
64−impl ProjectCache {
65− fn new(name: String) -> Self {
66− Self {
67− name,
68− worktree_ids_reported: HashSet::default(),
69− }
70− }
71−}
72−
7356 #[cfg(debug_assertions)]
7457 const MAX_QUEUE_LEN: usize = 5;
7558
@@ -91,6 +74,10 @@ static ZED_CLIENT_CHECKSUM_SEED: LazyLock<Option<Vec<u8>>> = LazyLock::new(|| { · 15 unmodified lines
9174 })
9275 });
9376
77+static DOTNET_PROJECT_FILES_REGEX: LazyLock<Regex> = LazyLock::new(|| {
78+ Regex::new(r"^(global\.json|Directory\.Build\.props|.*\.(csproj|fsproj|vbproj|sln))$").unwrap()
79+});
80+
9481 pub fn os_name() -> String {
9582 #[cfg(target_os = "macos")]
9683 {
@@ -194,20 +181,7 @@ impl Telemetry { · 97 unmodified lines
194181 first_event_date_time: None,
195182 event_coalescer: EventCoalescer::new(clock.clone()),
196183 max_queue_size: MAX_QUEUE_LEN,
197− worktree_id_map: WorktreeIdMap(HashMap::from_iter([
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− ])),
184+ worktrees_with_project_type_events_sent: HashSet::new(),
211185
212186 os_version: None,
213187 os_name: os_name(),
@@ -371,49 +345,58 @@ impl Telemetry { · 157 unmodified lines
371345 }
372346 }
373347
374− pub fn report_discovered_project_events(
348+ pub fn report_discovered_project_type_events(
375349 self: &Arc<Self>,
376350 worktree_id: WorktreeId,
377351 updated_entries_set: &UpdatedEntriesSet,
378352 ) {
379− let project_type_names: Vec<String> = {
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− }
353+ let project_type_names = self.detect_project_types(worktree_id, updated_entries_set);
392354
393− let project_file_found = updated_entries_set.iter().any(|(path, _, _)| {
394− path.as_ref()
395− .file_name()
396− .and_then(|name| name.to_str())
397− .map(|name_str| name_str == project_file_name)
398− .unwrap_or(false)
399− });
355+ for project_type_name in project_type_names {
356+ telemetry::event!("Project Opened", project_type = project_type_name);
357+ }
358+ }
400359
401− if !project_file_found {
402− return None;
403− }
360+ fn detect_project_types(
361+ self: &Arc<Self>,
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();
404367
405− project_type_telemetry
406− .worktree_ids_reported
407− .insert(worktree_id);
368+ if state
369+ .worktrees_with_project_type_events_sent
370+ .contains(&worktree_id)
371+ {
372+ return project_names.into_iter().collect();
373+ }
408374
409− Some(project_type_telemetry.name.clone())
410− })
411− .collect()
412− };
375+ for (path, _, _) in updated_entries_set.iter() {
376+ let Some(file_name) = path.file_name().and_then(|f| f.to_str()) else {
377+ continue;
378+ };
413379
414− for project_type_name in project_type_names {
415− telemetry::event!("Project Opened", project_type = project_type_name);
380+ if file_name == "pnpm-lock.yaml" {
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+ }
416389 }
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
417400 }
418401
419402 fn report_event(self: &Arc<Self>, event: Event) {
@@ -578,7 +561,9 @@ mod tests { · 158 unmodified lines
578561 use clock::FakeSystemClock;
579562 use gpui::TestAppContext;
580563 use http_client::FakeHttpClient;
564+ use std::collections::HashMap;
581565 use telemetry_events::FlexibleEvent;
566+ use worktree::{PathChange, ProjectEntryId, WorktreeId};
582567
583568 #[gpui::test]
584569 fn test_telemetry_flush_on_max_queue_size(cx: &mut TestAppContext) {
@@ -696,6 +681,97 @@ mod tests { · 111 unmodified lines
696681 });
697682 }
698683
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+
699775 // TODO:
700776 // Test settings
701777 // Update FakeHTTPClient to keep track of the number of requests and assert on it
@@ -712,4 +788,36 @@ mod tests { · 10 unmodified lines
712788 && telemetry.state.lock().flush_events_task.is_none()
713789 && telemetry.state.lock().first_event_date_time.is_none()
714790 }
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+ }
715823 }
crates/client/src/user.rs+4−2
@@ -49,7 +49,6 @@ pub struct User { · 48 unmodified lines
4949 pub github_login: String,
5050 pub avatar_uri: SharedUri,
5151 pub name: Option<String>,
52− pub email: Option<String>,
5352 }
5453
5554 #[derive(Clone, Debug, PartialEq, Eq)]
@@ -58,6 +57,8 @@ pub struct Collaborator { · 2 unmodified lines
5857 pub replica_id: ReplicaId,
5958 pub user_id: UserId,
6059 pub is_host: bool,
60+ pub committer_name: Option<String>,
61+ pub committer_email: Option<String>,
6162 }
6263
6364 impl PartialOrd for User {
@@ -881,7 +882,6 @@ impl User { · 817 unmodified lines
881882 github_login: message.github_login,
882883 avatar_uri: message.avatar_url.into(),
883884 name: message.name,
884− email: message.email,
885885 })
886886 }
887887 }
@@ -912,6 +912,8 @@ impl Collaborator { · 24 unmodified lines
912912 replica_id: message.replica_id as ReplicaId,
913913 user_id: message.user_id as UserId,
914914 is_host: message.is_host,
915+ committer_name: message.committer_name,
916+ committer_email: message.committer_email,
915917 })
916918 }
917919 }
crates/collab/migrations.sqlite/20221109000000_test_schema.sql+3−1
@@ -185,7 +185,9 @@ CREATE TABLE "project_collaborators" ( · 184 unmodified lines
185185 "connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE,
186186 "user_id" INTEGER NOT NULL,
187187 "replica_id" INTEGER NOT NULL,
188− "is_host" BOOLEAN NOT NULL
188+ "is_host" BOOLEAN NOT NULL,
189+ "committer_name" VARCHAR,
190+ "committer_email" VARCHAR
189191 );
190192
191193 CREATE 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 @@
1+alter table project_collaborators
2+ add column committer_name varchar;
3+alter 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
9797
9898 pub fn routes(rpc_server: Arc<rpc::Server>) -> Router<(), Body> {
9999 Router::new()
100− .route("/user", get(get_authenticated_user))
100+ .route("/user", get(update_or_create_authenticated_user))
101101 .route("/users/look_up", get(look_up_user))
102102 .route("/users/:id/access_tokens", post(create_access_token))
103103 .route("/rpc_server_snapshot", get(get_rpc_server_snapshot))
@@ -157,7 +157,7 @@ struct AuthenticatedUserResponse { · 53 unmodified lines
157157 feature_flags: Vec<String>,
158158 }
159159
160−async fn get_authenticated_user(
160+async fn update_or_create_authenticated_user(
161161 Query(params): Query<AuthenticatedUserParams>,
162162 Extension(app): Extension<Arc<AppState>>,
163163 ) -> Result<Json<AuthenticatedUserResponse>> {
@@ -165,7 +165,7 @@ async fn get_authenticated_user( · 1 unmodified line
165165
166166 let user = app
167167 .db
168− .get_or_create_user_by_github_account(
168+ .update_or_create_user_by_github_account(
169169 &params.github_login,
170170 params.github_user_id,
171171 params.github_email.as_deref(),
crates/collab/src/db.rs+4−0
@@ -751,6 +751,8 @@ pub struct ProjectCollaborator { · 750 unmodified lines
751751 pub user_id: UserId,
752752 pub replica_id: ReplicaId,
753753 pub is_host: bool,
754+ pub committer_name: Option<String>,
755+ pub committer_email: Option<String>,
754756 }
755757
756758 impl ProjectCollaborator {
@@ -760,6 +762,8 @@ impl ProjectCollaborator { · 3 unmodified lines
760762 replica_id: self.replica_id.0 as u32,
761763 user_id: self.user_id.to_proto(),
762764 is_host: self.is_host,
765+ committer_name: self.committer_name.clone(),
766+ committer_email: self.committer_email.clone(),
763767 }
764768 }
765769 }
crates/collab/src/db/queries/buffers.rs+8−0
@@ -118,6 +118,8 @@ impl Database { · 117 unmodified lines
118118 user_id: collaborator.user_id.to_proto(),
119119 replica_id: collaborator.replica_id.0 as u32,
120120 is_host: false,
121+ committer_name: None,
122+ committer_email: None,
121123 })
122124 .collect(),
123125 })
@@ -225,6 +227,8 @@ impl Database { · 101 unmodified lines
225227 user_id: collaborator.user_id.to_proto(),
226228 replica_id: collaborator.replica_id.0 as u32,
227229 is_host: false,
230+ committer_name: None,
231+ committer_email: None,
228232 })
229233 .collect(),
230234 },
@@ -261,6 +265,8 @@ impl Database { · 30 unmodified lines
261265 replica_id: db_collaborator.replica_id.0 as u32,
262266 user_id: db_collaborator.user_id.to_proto(),
263267 is_host: false,
268+ committer_name: None,
269+ committer_email: None,
264270 })
265271 } else {
266272 collaborator_ids_to_remove.push(db_collaborator.id);
@@ -390,6 +396,8 @@ impl Database { · 123 unmodified lines
390396 replica_id: row.replica_id.0 as u32,
391397 user_id: row.user_id.to_proto(),
392398 is_host: false,
399+ committer_name: None,
400+ committer_email: None,
393401 });
394402 }
395403
crates/collab/src/db/queries/channels.rs+4−3
@@ -501,8 +501,10 @@ impl Database { · 500 unmodified lines
501501
502502 /// Returns all channels for the user with the given ID.
503503 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 })
505− .await
504+ self.weak_transaction(
505+ |tx| async move { self.get_user_channels(user_id, None, true, &tx).await },
506+ )
507+ .await
506508 }
507509
508510 /// Returns all channels for the user with the given ID that are descendants
@@ -737,7 +739,6 @@ impl Database { · 228 unmodified lines
737739 ),
738740 github_login: user.github_login,
739741 name: user.name,
740− email: user.email_address,
741742 })
742743 }
743744 proto::ChannelMember {
crates/collab/src/db/queries/contacts.rs+2−2
@@ -15,7 +15,7 @@ impl Database { · 14 unmodified lines
1515 user_b_busy: bool,
1616 }
1717
18− self.transaction(|tx| async move {
18+ self.weak_transaction(|tx| async move {
1919 let user_a_participant = Alias::new("user_a_participant");
2020 let user_b_participant = Alias::new("user_b_participant");
2121 let mut db_contacts = contact::Entity::find()
@@ -91,7 +91,7 @@ impl Database { · 69 unmodified lines
9191
9292 /// Returns whether the given user is a busy (on a call).
9393 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 {
9595 let participant = room_participant::Entity::find()
9696 .filter(room_participant::Column::UserId.eq(user_id))
9797 .one(&*tx)
crates/collab/src/db/queries/contributors.rs+1−1
@@ -71,7 +71,7 @@ impl Database { · 70 unmodified lines
7171 ) -> Result<()> {
7272 self.weak_transaction(|tx| async move {
7373 let user = self
74− .get_or_create_user_by_github_account_tx(
74+ .update_or_create_user_by_github_account_tx(
7575 github_login,
7676 github_user_id,
7777 github_email,
crates/collab/src/db/queries/projects.rs+29−7
@@ -98,7 +98,9 @@ impl Database { · 97 unmodified lines
9898 user_id: ActiveValue::set(participant.user_id),
9999 replica_id: ActiveValue::set(ReplicaId(replica_id)),
100100 is_host: ActiveValue::set(true),
101− ..Default::default()
101+ id: ActiveValue::NotSet,
102+ committer_name: ActiveValue::Set(None),
103+ committer_email: ActiveValue::Set(None),
102104 }
103105 .insert(&*tx)
104106 .await?;
@@ -784,13 +786,27 @@ impl Database { · 679 unmodified lines
784786 project_id: ProjectId,
785787 connection: ConnectionId,
786788 user_id: UserId,
789+ committer_name: Option<String>,
790+ committer_email: Option<String>,
787791 ) -> Result<TransactionGuard<(Project, ReplicaId)>> {
788− self.project_transaction(project_id, |tx| async move {
789− let (project, role) = self
790− .access_project(project_id, connection, Capability::ReadOnly, &tx)
791− .await?;
792− self.join_project_internal(project, user_id, connection, role, &tx)
792+ self.project_transaction(project_id, move |tx| {
793+ let committer_name = committer_name.clone();
794+ let committer_email = committer_email.clone();
795+ async move {
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+ )
793808 .await
809+ }
794810 })
795811 .await
796812 }
@@ -799,6 +815,8 @@ impl Database { · 2 unmodified lines
799815 &self,
800816 project: project::Model,
801817 user_id: UserId,
818+ committer_name: Option<String>,
819+ committer_email: Option<String>,
802820 connection: ConnectionId,
803821 role: ChannelRole,
804822 tx: &DatabaseTransaction,
@@ -822,7 +840,9 @@ impl Database { · 17 unmodified lines
822840 user_id: ActiveValue::set(user_id),
823841 replica_id: ActiveValue::set(replica_id),
824842 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),
826846 }
827847 .insert(tx)
828848 .await?;
@@ -1026,6 +1046,8 @@ impl Database { · 197 unmodified lines
10261046 user_id: collaborator.user_id,
10271047 replica_id: collaborator.replica_id,
10281048 is_host: collaborator.is_host,
1049+ committer_name: collaborator.committer_name,
1050+ committer_email: collaborator.committer_email,
10291051 })
10301052 .collect(),
10311053 worktrees,
crates/collab/src/db/queries/rooms.rs+5−1
@@ -80,7 +80,7 @@ impl Database { · 79 unmodified lines
8080 &self,
8181 user_id: UserId,
8282 ) -> Result<Option<proto::IncomingCall>> {
83− self.transaction(|tx| async move {
83+ self.weak_transaction(|tx| async move {
8484 let pending_participant = room_participant::Entity::find()
8585 .filter(
8686 room_participant::Column::UserId
@@ -553,6 +553,8 @@ impl Database { · 466 unmodified lines
553553 user_id: collaborator.user_id,
554554 replica_id: collaborator.replica_id,
555555 is_host: collaborator.is_host,
556+ committer_name: collaborator.committer_name.clone(),
557+ committer_email: collaborator.committer_email.clone(),
556558 })
557559 .collect(),
558560 worktrees: reshared_project.worktrees.clone(),
@@ -857,6 +859,8 @@ impl Database { · 298 unmodified lines
857859 user_id: collaborator.user_id,
858860 replica_id: collaborator.replica_id,
859861 is_host: collaborator.is_host,
862+ committer_name: collaborator.committer_name,
863+ committer_email: collaborator.committer_email,
860864 })
861865 .collect::<Vec<_>>();
862866
crates/collab/src/db/queries/users.rs+3−3
@@ -111,7 +111,7 @@ impl Database { · 110 unmodified lines
111111 .await
112112 }
113113
114− pub async fn get_or_create_user_by_github_account(
114+ pub async fn update_or_create_user_by_github_account(
115115 &self,
116116 github_login: &str,
117117 github_user_id: i32,
@@ -121,7 +121,7 @@ impl Database { · 3 unmodified lines
121121 initial_channel_id: Option<ChannelId>,
122122 ) -> Result<User> {
123123 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(
125125 github_login,
126126 github_user_id,
127127 github_email,
@@ -135,7 +135,7 @@ impl Database { · 7 unmodified lines
135135 .await
136136 }
137137
138− pub async fn get_or_create_user_by_github_account_tx(
138+ pub async fn update_or_create_user_by_github_account_tx(
139139 &self,
140140 github_login: &str,
141141 github_user_id: i32,
crates/collab/src/db/tables/project_collaborator.rs+2−0
@@ -13,6 +13,8 @@ pub struct Model { · 12 unmodified lines
1313 pub user_id: UserId,
1414 pub replica_id: ReplicaId,
1515 pub is_host: bool,
16+ pub committer_name: Option<String>,
17+ pub committer_email: Option<String>,
1618 }
1719
1820 impl 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
126126 peer_id: Some(rpc::proto::PeerId { id: 1, owner_id }),
127127 replica_id: 0,
128128 is_host: false,
129+ committer_name: None,
130+ committer_email: None,
129131 },
130132 rpc::proto::Collaborator {
131133 user_id: b_id.to_proto(),
132134 peer_id: Some(rpc::proto::PeerId { id: 2, owner_id }),
133135 replica_id: 1,
134136 is_host: false,
137+ committer_name: None,
138+ committer_email: None,
135139 }
136140 ]
137141 );
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
7272 }
7373
7474 test_both_dbs!(
75− test_get_or_create_user_by_github_account,
76− test_get_or_create_user_by_github_account_postgres,
77− test_get_or_create_user_by_github_account_sqlite
75+ test_update_or_create_user_by_github_account,
76+ test_update_or_create_user_by_github_account_postgres,
77+ test_update_or_create_user_by_github_account_sqlite
7878 );
7979
80−async fn test_get_or_create_user_by_github_account(db: &Arc<Database>) {
80+async fn test_update_or_create_user_by_github_account(db: &Arc<Database>) {
8181 db.create_user(
8282 "user1@example.com",
8383 None,
@@ -104,7 +104,14 @@ async fn test_get_or_create_user_by_github_account(db: &Arc<Database>) { · 20 unmodified lines
104104 .user_id;
105105
106106 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+ )
108115 .await
109116 .unwrap();
110117 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
112119 assert_eq!(user.github_user_id, 102);
113120
114121 let user = db
115− .get_or_create_user_by_github_account(
122+ .update_or_create_user_by_github_account(
116123 "login3",
117124 103,
118125 Some("user3@example.com"),
crates/collab/src/llm.rs+6−0
@@ -7,6 +7,12 @@ pub use token::*; · 6 unmodified lines
77
88 pub const AGENT_EXTENDED_TRIAL_FEATURE_FLAG: &str = "agent-extended-trial";
99
10+/// The name of the feature flag that bypasses the account age check.
11+pub 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.
14+pub const MIN_ACCOUNT_AGE_FOR_LLM_USE: chrono::Duration = chrono::Duration::days(30);
15+
1016 /// The default value to use for maximum spend per month if the user did not
1117 /// explicitly set a maximum spend.
1218 ///
crates/collab/src/llm/token.rs+2−2
@@ -1,6 +1,6 @@
11 use crate::db::billing_subscription::SubscriptionKind;
22 use crate::db::{billing_customer, billing_subscription, user};
3−use crate::llm::AGENT_EXTENDED_TRIAL_FEATURE_FLAG;
3+use crate::llm::{AGENT_EXTENDED_TRIAL_FEATURE_FLAG, BYPASS_ACCOUNT_AGE_CHECK_FEATURE_FLAG};
44 use crate::{Config, db::billing_preference};
55 use anyhow::{Context as _, Result};
66 use chrono::{NaiveDateTime, Utc};
@@ -84,7 +84,7 @@ impl LlmTokenClaims { · 77 unmodified lines
8484 .any(|flag| flag == "llm-closed-beta"),
8585 bypass_account_age_check: feature_flags
8686 .iter()
87− .any(|flag| flag == "bypass-account-age-check"),
87+ .any(|flag| flag == BYPASS_ACCOUNT_AGE_CHECK_FEATURE_FLAG),
8888 can_use_web_search_tool: true,
8989 use_llm_request_queue: feature_flags.iter().any(|flag| flag == "llm-request-queue"),
9090 plan,
crates/collab/src/rpc.rs+64−29
@@ -4,14 +4,17 @@ use crate::api::billing::find_or_create_billing_customer; · 3 unmodified lines
44 use crate::api::{CloudflareIpCountryHeader, SystemIdHeader};
55 use crate::db::billing_subscription::SubscriptionKind;
66 use crate::llm::db::LlmDatabase;
7−use crate::llm::{AGENT_EXTENDED_TRIAL_FEATURE_FLAG, LlmTokenClaims};
7+use crate::llm::{
8+ AGENT_EXTENDED_TRIAL_FEATURE_FLAG, BYPASS_ACCOUNT_AGE_CHECK_FEATURE_FLAG, LlmTokenClaims,
9+ MIN_ACCOUNT_AGE_FOR_LLM_USE,
10+};
811 use crate::stripe_client::StripeCustomerId;
912 use crate::{
1013 AppState, Error, Result, auth,
1114 db::{
1215 self, BufferId, Capability, Channel, ChannelId, ChannelRole, ChannelsForUser,
1316 CreatedChannelMessage, Database, InviteMemberResult, MembershipUpdated, MessageId,
14− NotificationId, Project, ProjectId, RejoinedProject, RemoveChannelMemberResult, ReplicaId,
17+ NotificationId, ProjectId, RejoinedProject, RemoveChannelMemberResult,
1518 RespondToChannelInvite, RoomId, ServerId, UpdatedChannelMessage, User, UserId,
1619 },
1720 executor::Executor,
@@ -65,7 +68,7 @@ use std::{ · 47 unmodified lines
6568 rc::Rc,
6669 sync::{
6770 Arc, OnceLock,
68− atomic::{AtomicBool, Ordering::SeqCst},
71+ atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst},
6972 },
7073 time::{Duration, Instant},
7174 };
@@ -86,10 +89,36 @@ pub const CLEANUP_TIMEOUT: Duration = Duration::from_secs(15); · 14 unmodified lines
8689 const MESSAGE_COUNT_PER_PAGE: usize = 100;
8790 const MAX_MESSAGE_LEN: usize = 1024;
8891 const NOTIFICATION_COUNT_PER_PAGE: usize = 50;
92+const MAX_CONCURRENT_CONNECTIONS: usize = 512;
93+
94+static CONCURRENT_CONNECTIONS: AtomicUsize = AtomicUsize::new(0);
8995
9096 type MessageHandler =
9197 Box<dyn Send + Sync + Fn(Box<dyn AnyTypedEnvelope>, Session) -> BoxFuture<'static, ()>>;
9298
99+pub struct ConnectionGuard;
100+
101+impl 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+
116+impl Drop for ConnectionGuard {
117+ fn drop(&mut self) {
118+ CONCURRENT_CONNECTIONS.fetch_sub(1, SeqCst);
119+ }
120+}
121+
93122 struct Response<R> {
94123 peer: Arc<Peer>,
95124 receipt: Receipt<R>,
@@ -722,6 +751,7 @@ impl Server { · 626 unmodified lines
722751 system_id: Option<String>,
723752 send_connection_id: Option<oneshot::Sender<ConnectionId>>,
724753 executor: Executor,
754+ connection_guard: Option<ConnectionGuard>,
725755 ) -> impl Future<Output = ()> + use<> {
726756 let this = self.clone();
727757 let span = info_span!("handle connection", %address,
@@ -742,6 +772,7 @@ impl Server { · 14 unmodified lines
742772 tracing::error!("server is tearing down");
743773 return
744774 }
775+
745776 let (connection_id, handle_io, mut incoming_rx) = this
746777 .peer
747778 .add_connection(connection, {
@@ -783,6 +814,7 @@ impl Server { · 35 unmodified lines
783814 tracing::error!(?error, "failed to send initial client update");
784815 return;
785816 }
817+ drop(connection_guard);
786818
787819 let handle_io = handle_io.fuse();
788820 futures::pin_mut!(handle_io);
@@ -1154,6 +1186,19 @@ pub async fn handle_websocket_request( · 365 unmodified lines
11541186 }
11551187
11561188 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+
11571202 ws.on_upgrade(move |socket| {
11581203 let socket = socket
11591204 .map_ok(to_tungstenite_message)
@@ -1171,6 +1216,7 @@ pub async fn handle_websocket_request( · 11 unmodified lines
11711216 system_id_header.map(|header| header.to_string()),
11721217 None,
11731218 Executor::Production,
1219+ Some(connection_guard),
11741220 )
11751221 .await;
11761222 }
@@ -1844,28 +1890,16 @@ async fn join_project( · 667 unmodified lines
18441890
18451891 let db = session.db().await;
18461892 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+ )
18481900 .await?;
18491901 drop(db);
18501902 tracing::info!(%project_id, "join remote project");
1851− join_project_internal(response, session, project, replica_id)
1852−}
1853−
1854−trait JoinProjectInternalResponse {
1855− fn send(self, result: proto::JoinProjectResponse) -> Result<()>;
1856−}
1857−impl JoinProjectInternalResponse for Response<proto::JoinProject> {
1858− fn send(self, result: proto::JoinProjectResponse) -> Result<()> {
1859− Response::<proto::JoinProject>::send(self, result)
1860− }
1861−}
1862−
1863−fn join_project_internal(
1864− response: impl JoinProjectInternalResponse,
1865− session: Session,
1866− project: &mut Project,
1867− replica_id: &ReplicaId,
1868−) -> Result<()> {
18691903 let collaborators = project
18701904 .collaborators
18711905 .iter()
@@ -1893,6 +1927,8 @@ fn join_project_internal( · 21 unmodified lines
18931927 replica_id: replica_id.0 as u32,
18941928 user_id: guest_user_id.to_proto(),
18951929 is_host: false,
1930+ committer_name: request.committer_name.clone(),
1931+ committer_email: request.committer_email.clone(),
18961932 }),
18971933 };
18981934
@@ -2521,7 +2557,6 @@ async fn get_users( · 622 unmodified lines
25212557 id: user.id.to_proto(),
25222558 avatar_url: format!("https://github.com/{}.png?size=128", user.github_login),
25232559 github_login: user.github_login,
2524− email: user.email_address,
25252560 name: user.name,
25262561 })
25272562 .collect();
@@ -2555,7 +2590,6 @@ async fn fuzzy_search_users( · 27 unmodified lines
25552590 avatar_url: format!("https://github.com/{}.png?size=128", user.github_login),
25562591 github_login: user.github_login,
25572592 name: user.name,
2558− email: user.email_address,
25592593 })
25602594 .collect();
25612595 response.send(proto::UsersResponse { users })?;
@@ -2773,8 +2807,12 @@ async fn make_update_user_plan_message( · 211 unmodified lines
27732807 (None, None)
27742808 };
27752809
2776− let account_too_young =
2777− !matches!(plan, proto::Plan::ZedPro) && user.account_age() < MIN_ACCOUNT_AGE_FOR_LLM_USE;
2810+ let bypass_account_age_check = feature_flags
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;
27782816
27792817 Ok(proto::UpdateUserPlan {
27802818 plan: plan.into(),
@@ -4075,9 +4113,6 @@ async fn accept_terms_of_service( · 1294 unmodified lines
40754113 Ok(())
40764114 }
40774115
4078−/// The minimum account age an account must have in order to use the LLM service.
4079−pub const MIN_ACCOUNT_AGE_FOR_LLM_USE: chrono::Duration = chrono::Duration::days(30);
4080−
40814116 async fn get_llm_api_token(
40824117 _request: proto::GetLlmToken,
40834118 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
127127 log::info!("Seeding {:?} from GitHub", github_user.login);
128128
129129 let user = db
130− .get_or_create_user_by_github_account(
130+ .update_or_create_user_by_github_account(
131131 &github_user.login,
132132 github_user.id,
133133 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
180180 server
181181 .app_state
182182 .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)
184184 .await
185185 .unwrap();
186186
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
16101610 .root(cx_a)
16111611 .unwrap();
16121612
1613+ executor.run_until_parked();
1614+
16131615 workspace_a_project_b.update(cx_a2, |workspace, cx| {
16141616 assert_eq!(workspace.project().read(cx).remote_id(), Some(project_b_id));
16151617 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
5151 time::Duration,
5252 };
5353 use unindent::Unindent as _;
54−use util::{path, separator, uri};
54+use util::{path, uri};
5555 use workspace::Pane;
5656
5757 #[ctor::ctor]
@@ -1676,13 +1676,13 @@ async fn test_project_reconnect( · 1618 unmodified lines
16761676 .map(|p| p.to_str().unwrap())
16771677 .collect::<Vec<_>>(),
16781678 vec![
1679− separator!("a.txt"),
1680− separator!("b.txt"),
1681− separator!("subdir2"),
1682− separator!("subdir2/f.txt"),
1683− separator!("subdir2/g.txt"),
1684− separator!("subdir2/h.txt"),
1685− separator!("subdir2/i.txt")
1679+ path!("a.txt"),
1680+ path!("b.txt"),
1681+ path!("subdir2"),
1682+ path!("subdir2/f.txt"),
1683+ path!("subdir2/g.txt"),
1684+ path!("subdir2/h.txt"),
1685+ path!("subdir2/i.txt")
16861686 ]
16871687 );
16881688 assert!(worktree_a3.read(cx).has_update_observer());
@@ -1709,13 +1709,13 @@ async fn test_project_reconnect( · 20 unmodified lines
17091709 .map(|p| p.to_str().unwrap())
17101710 .collect::<Vec<_>>(),
17111711 vec![
1712− separator!("a.txt"),
1713− separator!("b.txt"),
1714− separator!("subdir2"),
1715− separator!("subdir2/f.txt"),
1716− separator!("subdir2/g.txt"),
1717− separator!("subdir2/h.txt"),
1718− separator!("subdir2/i.txt")
1712+ path!("a.txt"),
1713+ path!("b.txt"),
1714+ path!("subdir2"),
1715+ path!("subdir2/f.txt"),
1716+ path!("subdir2/g.txt"),
1717+ path!("subdir2/h.txt"),
1718+ path!("subdir2/i.txt")
17191719 ]
17201720 );
17211721 assert!(project.worktree_for_id(worktree2_id, cx).is_none());
@@ -1806,13 +1806,13 @@ async fn test_project_reconnect( · 84 unmodified lines
18061806 .map(|p| p.to_str().unwrap())
18071807 .collect::<Vec<_>>(),
18081808 vec![
1809− separator!("a.txt"),
1810− separator!("b.txt"),
1811− separator!("subdir2"),
1812− separator!("subdir2/f.txt"),
1813− separator!("subdir2/g.txt"),
1814− separator!("subdir2/h.txt"),
1815− separator!("subdir2/j.txt")
1809+ path!("a.txt"),
1810+ path!("b.txt"),
1811+ path!("subdir2"),
1812+ path!("subdir2/f.txt"),
1813+ path!("subdir2/g.txt"),
1814+ path!("subdir2/h.txt"),
1815+ path!("subdir2/j.txt")
18161816 ]
18171817 );
18181818 assert!(project.worktree_for_id(worktree2_id, cx).is_none());
@@ -1876,7 +1876,6 @@ async fn test_active_call_events( · 57 unmodified lines
18761876 github_login: "user_a".to_string(),
18771877 avatar_uri: "avatar_a".into(),
18781878 name: None,
1879− email: None,
18801879 }),
18811880 project_id: project_a_id,
18821881 worktree_root_names: vec!["a".to_string()],
@@ -1896,7 +1895,6 @@ async fn test_active_call_events( · 13 unmodified lines
18961895 github_login: "user_b".to_string(),
18971896 avatar_uri: "avatar_b".into(),
18981897 name: None,
1899− email: None,
19001898 }),
19011899 project_id: project_b_id,
19021900 worktree_root_names: vec!["b".to_string()]
@@ -3317,13 +3315,13 @@ async fn test_fs_operations( · 1414 unmodified lines
33173315 .map(|p| p.to_string_lossy())
33183316 .collect::<Vec<_>>(),
33193317 [
3320− separator!("DIR"),
3321− separator!("DIR/SUBDIR"),
3322− separator!("DIR/SUBDIR/f.txt"),
3323− separator!("DIR/e.txt"),
3324− separator!("a.txt"),
3325− separator!("b.txt"),
3326− separator!("d.txt")
3318+ path!("DIR"),
3319+ path!("DIR/SUBDIR"),
3320+ path!("DIR/SUBDIR/f.txt"),
3321+ path!("DIR/e.txt"),
3322+ path!("a.txt"),
3323+ path!("b.txt"),
3324+ path!("d.txt")
33273325 ]
33283326 );
33293327 });
@@ -3335,13 +3333,13 @@ async fn test_fs_operations( · 5 unmodified lines
33353333 .map(|p| p.to_string_lossy())
33363334 .collect::<Vec<_>>(),
33373335 [
3338− separator!("DIR"),
3339− separator!("DIR/SUBDIR"),
3340− separator!("DIR/SUBDIR/f.txt"),
3341− separator!("DIR/e.txt"),
3342− separator!("a.txt"),
3343− separator!("b.txt"),
3344− separator!("d.txt")
3336+ path!("DIR"),
3337+ path!("DIR/SUBDIR"),
3338+ path!("DIR/SUBDIR/f.txt"),
3339+ path!("DIR/e.txt"),
3340+ path!("a.txt"),
3341+ path!("b.txt"),
3342+ path!("d.txt")
33453343 ]
33463344 );
33473345 });
@@ -3361,14 +3359,14 @@ async fn test_fs_operations( · 13 unmodified lines
33613359 .map(|p| p.to_string_lossy())
33623360 .collect::<Vec<_>>(),
33633361 [
3364− separator!("DIR"),
3365− separator!("DIR/SUBDIR"),
3366− separator!("DIR/SUBDIR/f.txt"),
3367− separator!("DIR/e.txt"),
3368− separator!("a.txt"),
3369− separator!("b.txt"),
3370− separator!("d.txt"),
3371− separator!("f.txt")
3362+ path!("DIR"),
3363+ path!("DIR/SUBDIR"),
3364+ path!("DIR/SUBDIR/f.txt"),
3365+ path!("DIR/e.txt"),
3366+ path!("a.txt"),
3367+ path!("b.txt"),
3368+ path!("d.txt"),
3369+ path!("f.txt")
33723370 ]
33733371 );
33743372 });
@@ -3380,14 +3378,14 @@ async fn test_fs_operations( · 5 unmodified lines
33803378 .map(|p| p.to_string_lossy())
33813379 .collect::<Vec<_>>(),
33823380 [
3383− separator!("DIR"),
3384− separator!("DIR/SUBDIR"),
3385− separator!("DIR/SUBDIR/f.txt"),
3386− separator!("DIR/e.txt"),
3387− separator!("a.txt"),
3388− separator!("b.txt"),
3389− separator!("d.txt"),
3390− separator!("f.txt")
3381+ path!("DIR"),
3382+ path!("DIR/SUBDIR"),
3383+ path!("DIR/SUBDIR/f.txt"),
3384+ path!("DIR/e.txt"),
3385+ path!("a.txt"),
3386+ path!("b.txt"),
3387+ path!("d.txt"),
3388+ path!("f.txt")
33913389 ]
33923390 );
33933391 });
crates/collab/src/tests/remote_editing_collaboration_tests.rs+3−3
@@ -30,7 +30,7 @@ use rpc::proto; · 29 unmodified lines
3030 use serde_json::json;
3131 use settings::SettingsStore;
3232 use std::{path::Path, sync::Arc};
33−use util::{path, separator};
33+use util::path;
3434
3535 #[gpui::test(iterations = 10)]
3636 async fn test_sharing_an_ssh_remote_project(
@@ -198,7 +198,7 @@ async fn test_sharing_an_ssh_remote_project( · 161 unmodified lines
198198 .path()
199199 .to_string_lossy()
200200 .to_string(),
201− separator!("src/renamed.rs").to_string()
201+ path!("src/renamed.rs").to_string()
202202 );
203203 });
204204 }
@@ -671,7 +671,7 @@ async fn test_remote_server_debugger( · 466 unmodified lines
671671 });
672672
673673 session.update(cx_a, |session, _| {
674− assert_eq!(session.binary().command, "ssh");
674+ assert_eq!(session.binary().unwrap().command.as_deref(), Some("ssh"));
675675 });
676676
677677 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
258258 None,
259259 Some(connection_id_tx),
260260 Executor::Deterministic(cx.background_executor().clone()),
261+ None,
261262 ))
262263 .detach();
263264 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
8282 {
8383 Ok(github_user) => {
8484 self.db
85− .get_or_create_user_by_github_account(
85+ .update_or_create_user_by_github_account(
8686 &user.github_login,
8787 github_user.id,
8888 user.email_address.as_deref(),
crates/collab_ui/src/chat_panel.rs+1−4
@@ -90,7 +90,7 @@ impl ChatPanel { · 89 unmodified lines
9090 languages.clone(),
9191 user_store.clone(),
9292 None,
93− cx.new(|cx| Editor::auto_height(4, window, cx)),
93+ cx.new(|cx| Editor::auto_height(1, 4, window, cx)),
9494 window,
9595 cx,
9696 )
@@ -1218,7 +1218,6 @@ mod tests { · 1121 unmodified lines
12181218 avatar_uri: "avatar_fgh".into(),
12191219 id: 103,
12201220 name: None,
1221− email: None,
12221221 }),
12231222 nonce: 5,
12241223 mentions: vec![(ranges[0].clone(), 101), (ranges[1].clone(), 102)],
@@ -1274,7 +1273,6 @@ mod tests { · 49 unmodified lines
12741273 avatar_uri: "avatar_fgh".into(),
12751274 id: 103,
12761275 name: None,
1277− email: None,
12781276 }),
12791277 nonce: 5,
12801278 mentions: Vec::new(),
@@ -1323,7 +1321,6 @@ mod tests { · 42 unmodified lines
13231321 avatar_uri: "avatar_fgh".into(),
13241322 id: 103,
13251323 name: None,
1326− email: None,
13271324 }),
13281325 nonce: 5,
13291326 mentions: Vec::new(),
crates/context_server/src/protocol.rs+12−10
@@ -8,7 +8,7 @@ · 7 unmodified lines
88 use anyhow::Result;
99
1010 use crate::client::Client;
11−use crate::types::{self, Request};
11+use crate::types::{self, Notification, Request};
1212
1313 pub struct ModelContextProtocol {
1414 inner: Client,
@@ -20,9 +20,10 @@ impl ModelContextProtocol { · 5 unmodified lines
2020 }
2121
2222 fn supported_protocols() -> Vec<types::ProtocolVersion> {
23− vec![types::ProtocolVersion(
24− types::LATEST_PROTOCOL_VERSION.to_string(),
25− )]
23+ vec![
24+ types::ProtocolVersion(types::LATEST_PROTOCOL_VERSION.to_string()),
25+ types::ProtocolVersion(types::VERSION_2024_11_05.to_string()),
26+ ]
2627 }
2728
2829 pub async fn initialize(
@@ -42,7 +43,7 @@ impl ModelContextProtocol { · 13 unmodified lines
4243
4344 let response: types::InitializeResponse = self
4445 .inner
45− .request(types::request::Initialize::METHOD, params)
46+ .request(types::requests::Initialize::METHOD, params)
4647 .await?;
4748
4849 anyhow::ensure!(
@@ -53,16 +54,13 @@ impl ModelContextProtocol { · 4 unmodified lines
5354
5455 log::trace!("mcp server info {:?}", response.server_info);
5556
56− self.inner.notify(
57− types::NotificationType::Initialized.as_str(),
58− serde_json::json!({}),
59− )?;
60−
6157 let initialized_protocol = InitializedContextServerProtocol {
6258 inner: self.inner,
6359 initialize: response,
6460 };
6561
62+ initialized_protocol.notify::<types::notifications::Initialized>(())?;
63+
6664 Ok(initialized_protocol)
6765 }
6866 }
@@ -96,4 +94,8 @@ impl InitializedContextServerProtocol { · 27 unmodified lines
9694 pub async fn request<T: Request>(&self, params: T::Params) -> Result<T::Response> {
9795 self.inner.request(T::METHOD, params).await
9896 }
97+
98+ pub fn notify<T: Notification>(&self, params: T::Params) -> Result<()> {
99+ self.inner.notify(T::METHOD, params)
100+ }
99101 }
crates/context_server/src/test.rs+1−1
@@ -14,7 +14,7 @@ pub fn create_fake_transport( · 13 unmodified lines
1414 executor: BackgroundExecutor,
1515 ) -> FakeTransport {
1616 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| {
1818 create_initialize_response(name.clone())
1919 })
2020 }
crates/context_server/src/types.rs+91−32
@@ -3,9 +3,10 @@ use serde::de::DeserializeOwned; · 2 unmodified lines
33 use serde::{Deserialize, Serialize};
44 use url::Url;
55
6−pub const LATEST_PROTOCOL_VERSION: &str = "2024-11-05";
6+pub const LATEST_PROTOCOL_VERSION: &str = "2025-03-26";
7+pub const VERSION_2024_11_05: &str = "2024-11-05";
78
8−pub mod request {
9+pub mod requests {
910 use super::*;
1011
1112 macro_rules! request {
@@ -82,6 +83,57 @@ pub trait Request { · 70 unmodified lines
8283 const METHOD: &'static str;
8384 }
8485
86+pub 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+
118+pub 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")]
125+pub 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")]
133+pub struct ResourcesUpdatedParams {
134+ pub uri: String,
135+}
136+
85137 #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
86138 #[serde(transparent)]
87139 pub struct ProtocolVersion(pub String);
@@ -291,13 +343,20 @@ pub enum MessageContent { · 203 unmodified lines
291343 #[serde(skip_serializing_if = "Option::is_none")]
292344 annotations: Option<MessageAnnotations>,
293345 },
294− #[serde(rename = "image")]
346+ #[serde(rename = "image", rename_all = "camelCase")]
295347 Image {
296348 data: String,
297349 mime_type: String,
298350 #[serde(skip_serializing_if = "Option::is_none")]
299351 annotations: Option<MessageAnnotations>,
300352 },
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+ },
301360 #[serde(rename = "resource")]
302361 Resource {
303362 resource: ResourceContents,
@@ -394,6 +453,8 @@ pub struct ServerCapabilities { · 90 unmodified lines
394453 #[serde(skip_serializing_if = "Option::is_none")]
395454 pub logging: Option<serde_json::Value>,
396455 #[serde(skip_serializing_if = "Option::is_none")]
456+ pub completions: Option<serde_json::Value>,
457+ #[serde(skip_serializing_if = "Option::is_none")]
397458 pub prompts: Option<PromptsCapabilities>,
398459 #[serde(skip_serializing_if = "Option::is_none")]
399460 pub resources: Option<ResourcesCapabilities>,
@@ -438,6 +499,28 @@ pub struct Tool { · 38 unmodified lines
438499 #[serde(skip_serializing_if = "Option::is_none")]
439500 pub description: Option<String>,
440501 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")]
508+pub 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>,
441524 }
442525
443526 #[derive(Debug, Serialize, Deserialize)]
@@ -528,34 +611,6 @@ pub struct ModelHint { · 84 unmodified lines
528611 pub name: Option<String>,
529612 }
530613
531−#[derive(Debug, Serialize, Deserialize)]
532−#[serde(rename_all = "camelCase")]
533−pub enum NotificationType {
534− Initialized,
535− Progress,
536− Message,
537− ResourcesUpdated,
538− ResourcesListChanged,
539− ToolsListChanged,
540− PromptsListChanged,
541− RootsListChanged,
542−}
543−
544−impl 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−
559614 #[derive(Debug, Serialize)]
560615 #[serde(untagged)]
561616 pub enum ClientNotification {
@@ -576,12 +631,14 @@ pub enum ProgressToken { · 14 unmodified lines
576631 Number(f64),
577632 }
578633
579−#[derive(Debug, Serialize)]
634+#[derive(Debug, Serialize, Deserialize)]
580635 #[serde(rename_all = "camelCase")]
581636 pub struct ProgressParams {
582637 pub progress_token: ProgressToken,
583638 pub progress: f64,
584639 #[serde(skip_serializing_if = "Option::is_none")]
640+ pub message: Option<String>,
641+ #[serde(skip_serializing_if = "Option::is_none")]
585642 pub total: Option<f64>,
586643 #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
587644 pub meta: Option<HashMap<String, serde_json::Value>>,
@@ -625,6 +682,8 @@ pub enum ToolResponseContent { · 37 unmodified lines
625682 Text { text: String },
626683 #[serde(rename = "image", rename_all = "camelCase")]
627684 Image { data: String, mime_type: String },
685+ #[serde(rename = "audio", rename_all = "camelCase")]
686+ Audio { data: String, mime_type: String },
628687 #[serde(rename = "resource")]
629688 Resource { resource: ResourceContents },
630689 }
crates/copilot/src/copilot.rs+43−37
@@ -26,6 +26,7 @@ use parking_lot::Mutex; · 25 unmodified lines
2626 use request::StatusNotification;
2727 use settings::SettingsStore;
2828 use sign_in::{reinstall_and_sign_in_within_workspace, sign_out_within_workspace};
29+use std::collections::hash_map::Entry;
2930 use std::{
3031 any::TypeId,
3132 env,
@@ -731,42 +732,43 @@ impl Copilot { · 699 unmodified lines
731732 return;
732733 }
733734
734− registered_buffers
735− .entry(buffer.entity_id())
736− .or_insert_with(|| {
737− let uri: lsp::Url = uri_for_buffer(buffer, cx);
738− let language_id = id_for_language(buffer.read(cx).language());
739− let snapshot = buffer.read(cx).snapshot();
740− server
741− .notify::<lsp::notification::DidOpenTextDocument>(
742− &lsp::DidOpenTextDocumentParams {
743− text_document: lsp::TextDocumentItem {
744− uri: uri.clone(),
745− language_id: language_id.clone(),
746− version: 0,
747− text: snapshot.text(),
748− },
735+ let entry = registered_buffers.entry(buffer.entity_id());
736+ if let Entry::Vacant(e) = entry {
737+ let Ok(uri) = uri_for_buffer(buffer, cx) else {
738+ return;
739+ };
740+ let language_id = id_for_language(buffer.read(cx).language());
741+ let snapshot = buffer.read(cx).snapshot();
742+ server
743+ .notify::<lsp::notification::DidOpenTextDocument>(
744+ &lsp::DidOpenTextDocumentParams {
745+ text_document: lsp::TextDocumentItem {
746+ uri: uri.clone(),
747+ language_id: language_id.clone(),
748+ version: 0,
749+ text: snapshot.text(),
749750 },
750− )
751− .ok();
751+ },
752+ )
753+ .ok();
752754
753− RegisteredBuffer {
754− uri,
755− language_id,
756− snapshot,
757− snapshot_version: 0,
758− pending_buffer_change: Task::ready(Some(())),
759− _subscriptions: [
760− cx.subscribe(buffer, |this, buffer, event, cx| {
761− this.handle_buffer_event(buffer, event, cx).log_err();
762− }),
763− cx.observe_release(buffer, move |this, _buffer, _cx| {
764− this.buffers.remove(&weak_buffer);
765− this.unregister_buffer(&weak_buffer);
766− }),
767− ],
768− }
755+ e.insert(RegisteredBuffer {
756+ uri,
757+ language_id,
758+ snapshot,
759+ snapshot_version: 0,
760+ pending_buffer_change: Task::ready(Some(())),
761+ _subscriptions: [
762+ cx.subscribe(buffer, |this, buffer, event, cx| {
763+ this.handle_buffer_event(buffer, event, cx).log_err();
764+ }),
765+ cx.observe_release(buffer, move |this, _buffer, _cx| {
766+ this.buffers.remove(&weak_buffer);
767+ this.unregister_buffer(&weak_buffer);
768+ }),
769+ ],
769770 });
771+ }
770772 }
771773 }
772774
@@ -798,7 +800,9 @@ impl Copilot { · 25 unmodified lines
798800 language::BufferEvent::FileHandleChanged
799801 | language::BufferEvent::LanguageChanged => {
800802 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+ };
802806 if new_uri != registered_buffer.uri
803807 || new_language_id != registered_buffer.language_id
804808 {
@@ -1068,11 +1072,13 @@ fn id_for_language(language: Option<&Arc<Language>>) -> String { · 263 unmodified lines
10681072 .unwrap_or_else(|| "plaintext".to_string())
10691073 }
10701074
1071−fn uri_for_buffer(buffer: &Entity<Buffer>, cx: &App) -> lsp::Url {
1075+fn uri_for_buffer(buffer: &Entity<Buffer>, cx: &App) -> Result<lsp::Url, ()> {
10721076 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))
10741078 } else {
1075− format!("buffer://{}", buffer.entity_id()).parse().unwrap()
1079+ format!("buffer://{}", buffer.entity_id())
1080+ .parse()
1081+ .map_err(|_| ())
10761082 }
10771083 }
10781084
crates/dap/Cargo.toml+3−0
@@ -51,6 +51,9 @@ telemetry.workspace = true · 50 unmodified lines
5151 util.workspace = true
5252 workspace-hack.workspace = true
5353
54+[target.'cfg(not(windows))'.dependencies]
55+libc.workspace = true
56+
5457 [dev-dependencies]
5558 async-pipe.workspace = true
5659 gpui = { 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
9393 }
9494 }
9595
96−#[derive(Debug, Clone, PartialEq)]
96+#[derive(Debug, Clone, PartialEq, Serialize)]
9797 pub struct TcpArguments {
9898 pub host: Ipv4Addr,
9999 pub port: u16,
@@ -179,9 +179,9 @@ impl DebugTaskDefinition { · 79 unmodified lines
179179 }
180180
181181 /// 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)]
183183 pub struct DebugAdapterBinary {
184− pub command: String,
184+ pub command: Option<String>,
185185 pub arguments: Vec<String>,
186186 pub envs: HashMap<String, String>,
187187 pub cwd: Option<PathBuf>,
@@ -369,6 +369,10 @@ pub trait DebugAdapter: 'static + Send + Sync { · 181 unmodified lines
369369 }
370370
371371 async fn dap_schema(&self) -> serde_json::Value;
372+
373+ fn label_for_child_session(&self, _args: &StartDebuggingRequestArguments) -> Option<String> {
374+ None
375+ }
372376 }
373377
374378 #[cfg(any(test, feature = "test-support"))]
@@ -433,7 +437,7 @@ impl DebugAdapter for FakeAdapter { · 58 unmodified lines
433437 _: &mut AsyncApp,
434438 ) -> Result<DebugAdapterBinary> {
435439 Ok(DebugAdapterBinary {
436− command: "command".into(),
440+ command: Some("command".into()),
437441 arguments: vec![],
438442 connection: None,
439443 envs: HashMap::default(),
crates/dap/src/client.rs+4−4
@@ -218,7 +218,7 @@ impl DebugAdapterClient { · 217 unmodified lines
218218
219219 pub fn add_log_handler<F>(&self, f: F, kind: LogKind)
220220 where
221− F: 'static + Send + FnMut(IoKind, &str),
221+ F: 'static + Send + FnMut(IoKind, Option<&str>, &str),
222222 {
223223 self.transport_delegate.add_log_handler(f, kind);
224224 }
@@ -297,7 +297,7 @@ mod tests { · 72 unmodified lines
297297 let client = DebugAdapterClient::start(
298298 crate::client::SessionId(1),
299299 DebugAdapterBinary {
300− command: "command".into(),
300+ command: Some("command".into()),
301301 arguments: Default::default(),
302302 envs: Default::default(),
303303 connection: None,
@@ -367,7 +367,7 @@ mod tests { · 63 unmodified lines
367367 let client = DebugAdapterClient::start(
368368 crate::client::SessionId(1),
369369 DebugAdapterBinary {
370− command: "command".into(),
370+ command: Some("command".into()),
371371 arguments: Default::default(),
372372 envs: Default::default(),
373373 connection: None,
@@ -420,7 +420,7 @@ mod tests { · 46 unmodified lines
420420 let client = DebugAdapterClient::start(
421421 crate::client::SessionId(1),
422422 DebugAdapterBinary {
423− command: "command".into(),
423+ command: Some("command".into()),
424424 arguments: Default::default(),
425425 envs: Default::default(),
426426 connection: None,
crates/dap/src/registry.rs+2−2
@@ -19,11 +19,11 @@ use std::{collections::BTreeMap, sync::Arc}; · 18 unmodified lines
1919 pub trait DapLocator: Send + Sync {
2020 fn name(&self) -> SharedString;
2121 /// Determines whether this locator can generate debug target for given task.
22− fn create_scenario(
22+ async fn create_scenario(
2323 &self,
2424 build_config: &TaskTemplate,
2525 resolved_label: &str,
26− adapter: DebugAdapterName,
26+ adapter: &DebugAdapterName,
2727 ) -> Option<DebugScenario>;
2828
2929 async fn run(&self, build_config: SpawnInTerminal) -> Result<DebugRequest>;
crates/dap/src/transport.rs+157−64
@@ -1,4 +1,4 @@
1−use anyhow::{Context as _, Result, bail};
1+use anyhow::{Context as _, Result, anyhow, bail};
22 use dap_types::{
33 ErrorResponse,
44 messages::{Message, Response},
@@ -12,7 +12,6 @@ use smol::{ · 7 unmodified lines
1212 io::{AsyncBufReadExt as _, AsyncWriteExt, BufReader},
1313 lock::Mutex,
1414 net::{TcpListener, TcpStream},
15− process::Child,
1615 };
1716 use std::{
1817 collections::HashMap,
@@ -22,11 +21,13 @@ use std::{ · 3 unmodified lines
2221 time::Duration,
2322 };
2423 use task::TcpArgumentsTemplate;
25−use util::{ConnectionResult, ResultExt as _};
24+use util::ConnectionResult;
2625
2726 use crate::{adapters::DebugAdapterBinary, debugger_settings::DebuggerSettings};
2827
29−pub type IoHandler = Box<dyn Send + FnMut(IoKind, &str)>;
28+pub(crate) type IoMessage = str;
29+pub(crate) type Command = str;
30+pub type IoHandler = Box<dyn Send + FnMut(IoKind, Option<&Command>, &IoMessage)>;
3031
3132 #[derive(PartialEq, Eq, Clone, Copy)]
3233 pub enum LogKind {
@@ -86,10 +87,12 @@ impl Transport { · 53 unmodified lines
8687 TcpTransport::start(binary, cx)
8788 .await
8889 .map(|(transports, tcp)| (transports, Self::Tcp(tcp)))
90+ .context("Tried to connect to a debug adapter via TCP transport layer")
8991 } else {
9092 StdioTransport::start(binary, cx)
9193 .await
9294 .map(|(transports, stdio)| (transports, Self::Stdio(stdio)))
95+ .context("Tried to connect to a debug adapter via stdin/stdout transport layer")
9396 }
9497 }
9598
@@ -102,7 +105,7 @@ impl Transport { · 6 unmodified lines
102105 }
103106 }
104107
105− async fn kill(&self) -> Result<()> {
108+ async fn kill(&self) {
106109 match self {
107110 Transport::Stdio(stdio_transport) => stdio_transport.kill().await,
108111 Transport::Tcp(tcp_transport) => tcp_transport.kill().await,
@@ -191,7 +194,7 @@ impl TransportDelegate { · 82 unmodified lines
191194 match Self::handle_output(
192195 params.output,
193196 client_tx,
194− pending_requests,
197+ pending_requests.clone(),
195198 output_log_handler,
196199 )
197200 .await
@@ -199,6 +202,12 @@ impl TransportDelegate { · 1 unmodified line
199202 Ok(()) => {}
200203 Err(e) => log::error!("Error handling debugger output: {e}"),
201204 }
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+ });
202211 }));
203212
204213 if let Some(stderr) = params.stderr.take() {
@@ -289,7 +298,7 @@ impl TransportDelegate { · 84 unmodified lines
289298 if let Some(log_handlers) = log_handlers.as_ref() {
290299 for (kind, handler) in log_handlers.lock().iter_mut() {
291300 if matches!(kind, LogKind::Adapter) {
292− handler(IoKind::StdOut, line.as_str());
301+ handler(IoKind::StdOut, None, line.as_str());
293302 }
294303 }
295304 }
@@ -323,6 +332,12 @@ impl TransportDelegate { · 27 unmodified lines
323332 }
324333 }
325334
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+
326341 let message = match serde_json::to_string(&message) {
327342 Ok(message) => message,
328343 Err(e) => break Err(e.into()),
@@ -331,7 +346,7 @@ impl TransportDelegate { · 2 unmodified lines
331346 if let Some(log_handlers) = log_handlers.as_ref() {
332347 for (kind, log_handler) in log_handlers.lock().iter_mut() {
333348 if matches!(kind, LogKind::Rpc) {
334− log_handler(IoKind::StdIn, &message);
349+ log_handler(IoKind::StdIn, command, &message);
335350 }
336351 }
337352 }
@@ -416,7 +431,7 @@ impl TransportDelegate { · 78 unmodified lines
416431 Ok(_) => {
417432 for (kind, log_handler) in log_handlers.lock().iter_mut() {
418433 if matches!(kind, LogKind::Adapter) {
419− log_handler(IoKind::StdErr, buffer.as_str());
434+ log_handler(IoKind::StdErr, None, buffer.as_str());
420435 }
421436 }
422437
@@ -505,17 +520,24 @@ impl TransportDelegate { · 82 unmodified lines
505520 Err(e) => return ConnectionResult::Result(Err(e)),
506521 };
507522
523+ let message =
524+ serde_json::from_str::<Message>(message_str).context("deserializing server message");
525+
508526 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+
509533 for (kind, log_handler) in log_handlers.lock().iter_mut() {
510534 if matches!(kind, LogKind::Rpc) {
511− log_handler(IoKind::StdOut, message_str);
535+ log_handler(IoKind::StdOut, command, message_str);
512536 }
513537 }
514538 }
515539
516− ConnectionResult::Result(
517− serde_json::from_str::<Message>(message_str).context("deserializing server message"),
518− )
540+ ConnectionResult::Result(message)
519541 }
520542
521543 pub async fn shutdown(&self) -> Result<()> {
@@ -531,7 +553,7 @@ impl TransportDelegate { · 9 unmodified lines
531553 current_requests.clear();
532554 pending_requests.clear();
533555
534− let _ = self.transport.kill().await.log_err();
556+ self.transport.kill().await;
535557
536558 drop(current_requests);
537559 drop(pending_requests);
@@ -551,7 +573,7 @@ impl TransportDelegate { · 13 unmodified lines
551573
552574 pub fn add_log_handler<F>(&self, f: F, kind: LogKind)
553575 where
554− F: 'static + Send + FnMut(IoKind, &str),
576+ F: 'static + Send + FnMut(IoKind, Option<&Command>, &IoMessage),
555577 {
556578 let mut log_handlers = self.log_handlers.lock();
557579 log_handlers.push((kind, Box::new(f)));
@@ -562,7 +584,7 @@ pub struct TcpTransport { · 4 unmodified lines
562584 pub port: u16,
563585 pub host: Ipv4Addr,
564586 pub timeout: u64,
565− process: Mutex<Child>,
587+ process: Option<Mutex<Child>>,
566588 }
567589
568590 impl TcpTransport {
@@ -591,26 +613,23 @@ impl TcpTransport { · 22 unmodified lines
591613 let host = connection_args.host;
592614 let port = connection_args.port;
593615
594− let mut command = util::command::new_std_command(&binary.command);
595− util::set_pre_exec_to_start_new_session(&mut command);
596− let mut command = smol::process::Command::from(command);
616+ let mut process = if let Some(command) = &binary.command {
617+ let mut command = util::command::new_std_command(&command);
597618
598− if let Some(cwd) = &binary.cwd {
599− command.current_dir(cwd);
600− }
601−
602− command.args(&binary.arguments);
603− command.envs(&binary.envs);
619+ if let Some(cwd) = &binary.cwd {
620+ command.current_dir(cwd);
621+ }
604622
605− command
606− .stdin(Stdio::null())
607− .stdout(Stdio::piped())
608− .stderr(Stdio::piped())
609− .kill_on_drop(true);
623+ command.args(&binary.arguments);
624+ command.envs(&binary.envs);
610625
611− let mut process = command
612− .spawn()
613− .with_context(|| "failed to start debug adapter.")?;
626+ Some(
627+ Child::spawn(command, Stdio::null())
628+ .with_context(|| "failed to start debug adapter.")?,
629+ )
630+ } else {
631+ None
632+ };
614633
615634 let address = SocketAddrV4::new(host, port);
616635
@@ -628,15 +647,18 @@ impl TcpTransport { · 11 unmodified lines
628647 match TcpStream::connect(address).await {
629648 Ok(stream) => return Ok((process, stream.split())),
630649 Err(_) => {
631− if let Ok(Some(_)) = process.try_status() {
632− let output = process.output().await?;
633− let output = if output.stderr.is_empty() {
634− String::from_utf8_lossy(&output.stdout).to_string()
635− } else {
636− String::from_utf8_lossy(&output.stderr).to_string()
637− };
638− anyhow::bail!("{output}\nerror: process exited before debugger attached.");
650+ if let Some(p) = &mut process {
651+ if let Ok(Some(_)) = p.try_status() {
652+ let output = process.take().unwrap().into_inner().output().await?;
653+ let output = if output.stderr.is_empty() {
654+ String::from_utf8_lossy(&output.stdout).to_string()
655+ } else {
656+ String::from_utf8_lossy(&output.stderr).to_string()
657+ };
658+ anyhow::bail!("{output}\nerror: process exited before debugger attached.");
659+ }
639660 }
661+
640662 cx.background_executor().timer(Duration::from_millis(100)).await;
641663 }
642664 }
@@ -649,13 +671,13 @@ impl TcpTransport { · 6 unmodified lines
649671 host,
650672 port
651673 );
652− let stdout = process.stdout.take();
653− let stderr = process.stderr.take();
674+ let stdout = process.as_mut().and_then(|p| p.stdout.take());
675+ let stderr = process.as_mut().and_then(|p| p.stderr.take());
654676
655677 let this = Self {
656678 port,
657679 host,
658− process: Mutex::new(process),
680+ process: process.map(Mutex::new),
659681 timeout,
660682 };
661683
@@ -673,10 +695,19 @@ impl TcpTransport { · 11 unmodified lines
673695 true
674696 }
675697
676− async fn kill(&self) -> Result<()> {
677− self.process.lock().await.kill()?;
698+ async fn kill(&self) {
699+ if let Some(process) = &self.process {
700+ let mut process = process.lock().await;
701+ Child::kill(&mut process);
702+ }
703+ }
704+}
678705
679− Ok(())
706+impl Drop for TcpTransport {
707+ fn drop(&mut self) {
708+ if let Some(mut p) = self.process.take() {
709+ p.get_mut().kill();
710+ }
680711 }
681712 }
682713
@@ -687,9 +718,12 @@ pub struct StdioTransport { · 4 unmodified lines
687718 impl StdioTransport {
688719 #[allow(dead_code, reason = "This is used in non test builds of Zed")]
689720 async fn start(binary: &DebugAdapterBinary, _: AsyncApp) -> Result<(TransportPipe, Self)> {
690− let mut command = util::command::new_std_command(&binary.command);
691− util::set_pre_exec_to_start_new_session(&mut command);
692− let mut command = smol::process::Command::from(command);
721+ let Some(binary_command) = &binary.command else {
722+ bail!(
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);
693727
694728 if let Some(cwd) = &binary.cwd {
695729 command.current_dir(cwd);
@@ -698,16 +732,10 @@ impl StdioTransport { · 2 unmodified lines
698732 command.args(&binary.arguments);
699733 command.envs(&binary.envs);
700734
701− command
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(|| {
735+ let mut process = Child::spawn(command, Stdio::piped()).with_context(|| {
708736 format!(
709737 "failed to spawn command `{} {}`.",
710− binary.command,
738+ binary_command,
711739 binary.arguments.join(" ")
712740 )
713741 })?;
@@ -722,7 +750,7 @@ impl StdioTransport { · 8 unmodified lines
722750 if stderr.is_none() {
723751 bail!(
724752 "Failed to connect to stderr for debug adapter command {}",
725− &binary.command
753+ &binary_command
726754 );
727755 }
728756
@@ -745,9 +773,15 @@ impl StdioTransport { · 16 unmodified lines
745773 false
746774 }
747775
748− async fn kill(&self) -> Result<()> {
749− self.process.lock().await.kill()?;
750− Ok(())
776+ async fn kill(&self) {
777+ let mut process = self.process.lock().await;
778+ Child::kill(&mut process);
779+ }
780+}
781+
782+impl Drop for StdioTransport {
783+ fn drop(&mut self) {
784+ self.process.get_mut().kill();
751785 }
752786 }
753787
@@ -921,7 +955,66 @@ impl FakeTransport { · 167 unmodified lines
921955 false
922956 }
923957
924− async fn kill(&self) -> Result<()> {
925− Ok(())
958+ async fn kill(&self) {}
959+}
960+
961+struct Child {
962+ process: smol::process::Child,
963+}
964+
965+impl std::ops::Deref for Child {
966+ type Target = smol::process::Child;
967+
968+ fn deref(&self) -> &Self::Target {
969+ &self.process
970+ }
971+}
972+
973+impl std::ops::DerefMut for Child {
974+ fn deref_mut(&mut self) -> &mut Self::Target {
975+ &mut self.process
976+ }
977+}
978+
979+impl 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();
9261019 }
9271020 }
crates/dap_adapters/Cargo.toml+1−0
@@ -23,6 +23,7 @@ doctest = false · 22 unmodified lines
2323 [dependencies]
2424 anyhow.workspace = true
2525 async-trait.workspace = true
26+collections.workspace = true
2627 dap.workspace = true
2728 futures.workspace = true
2829 gpui.workspace = true
crates/dap_adapters/src/codelldb.rs+11−8
@@ -21,18 +21,21 @@ impl CodeLldbDebugAdapter { · 20 unmodified lines
2121
2222 fn request_args(
2323 &self,
24+ delegate: &Arc<dyn DapDelegate>,
2425 task_definition: &DebugTaskDefinition,
2526 ) -> Result<dap::StartDebuggingRequestArguments> {
2627 // CodeLLDB uses `name` for a terminal label.
2728 let mut configuration = task_definition.config.clone();
2829
29− configuration
30+ let obj = configuration
3031 .as_object_mut()
31− .context("CodeLLDB is not a valid json object")?
32− .insert(
33− "name".into(),
34− Value::String(String::from(task_definition.label.as_ref())),
35− );
32+ .context("CodeLLDB is not a valid json object")?;
33+
34+ obj.entry("name")
35+ .or_insert(Value::String(String::from(task_definition.label.as_ref())));
36+
37+ obj.entry("cwd")
38+ .or_insert(delegate.worktree_root_path().to_string_lossy().into());
3639
3740 let request = self.request_kind(&configuration)?;
3841
@@ -359,13 +362,13 @@ impl DebugAdapter for CodeLldbDebugAdapter { · 320 unmodified lines
359362 };
360363
361364 Ok(DebugAdapterBinary {
362− command: command.unwrap(),
365+ command: Some(command.unwrap()),
363366 cwd: Some(delegate.worktree_root_path().to_path_buf()),
364367 arguments: vec![
365368 "--settings".into(),
366369 json!({"sourceLanguages": ["cpp", "rust"]}).to_string(),
367370 ],
368− request_args: self.request_args(&config)?,
371+ request_args: self.request_args(delegate, &config)?,
369372 envs: HashMap::default(),
370373 connection: None,
371374 })
crates/dap_adapters/src/gdb.rs+11−6
@@ -177,18 +177,23 @@ impl DebugAdapter for GdbDebugAdapter { · 176 unmodified lines
177177
178178 let gdb_path = user_setting_path.unwrap_or(gdb_path?);
179179
180− let request_args = StartDebuggingRequestArguments {
181− request: self.request_kind(&config.config)?,
182− configuration: config.config.clone(),
183− };
180+ let mut configuration = config.config.clone();
181+ if let Some(configuration) = configuration.as_object_mut() {
182+ configuration
183+ .entry("cwd")
184+ .or_insert_with(|| delegate.worktree_root_path().to_string_lossy().into());
185+ }
184186
185187 Ok(DebugAdapterBinary {
186− command: gdb_path,
188+ command: Some(gdb_path),
187189 arguments: vec!["-i=dap".into()],
188190 envs: HashMap::default(),
189191 cwd: Some(delegate.worktree_root_path().to_path_buf()),
190192 connection: None,
191− request_args,
193+ request_args: StartDebuggingRequestArguments {
194+ request: self.request_kind(&config.config)?,
195+ configuration,
196+ },
192197 })
193198 }
194199 }
crates/dap_adapters/src/go.rs+48−24
@@ -2,7 +2,7 @@ use anyhow::{Context as _, bail}; · 1 unmodified line
22 use dap::{
33 StartDebuggingRequestArguments,
44 adapters::{
5− DebugTaskDefinition, DownloadedFileType, download_adapter_from_github,
5+ DebugTaskDefinition, DownloadedFileType, TcpArguments, download_adapter_from_github,
66 latest_github_release,
77 },
88 };
@@ -10,6 +10,7 @@ use dap::{ · 1 unmodified line
1010 use gpui::{AsyncApp, SharedString};
1111 use language::LanguageName;
1212 use std::{collections::HashMap, env::consts, ffi::OsStr, path::PathBuf, sync::OnceLock};
13+use task::TcpArgumentsTemplate;
1314 use util;
1415
1516 use crate::*;
@@ -433,10 +434,6 @@ impl DebugAdapter for GoDebugAdapter { · 417 unmodified lines
433434
434435 adapter_path.join("dlv").to_string_lossy().to_string()
435436 };
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?;
440437
441438 let cwd = task_definition
442439 .config
@@ -445,31 +442,58 @@ impl DebugAdapter for GoDebugAdapter { · 2 unmodified lines
445442 .map(PathBuf::from)
446443 .unwrap_or_else(|| delegate.worktree_root_path().to_path_buf());
447444
448− let arguments = if cfg!(windows) {
449− vec![
450− delve_path,
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− };
445+ let arguments;
446+ let command;
447+ let connection;
464448
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+ }
465489 Ok(DebugAdapterBinary {
466− command: minidelve_path.to_string_lossy().into_owned(),
490+ command,
467491 arguments,
468492 cwd: Some(cwd),
469493 envs: HashMap::default(),
470− connection: None,
494+ connection,
471495 request_args: StartDebuggingRequestArguments {
472− configuration: task_definition.config.clone(),
496+ configuration,
473497 request: self.request_kind(&task_definition.config)?,
474498 },
475499 })
crates/dap_adapters/src/javascript.rs+61−8
@@ -2,6 +2,7 @@ use adapters::latest_github_release; · 1 unmodified line
22 use anyhow::Context as _;
33 use dap::{StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
44 use gpui::AsyncApp;
5+use serde_json::Value;
56 use std::{collections::HashMap, path::PathBuf, sync::OnceLock};
67 use task::DebugRequest;
78 use util::ResultExt;
@@ -68,13 +69,44 @@ impl JsDebugAdapter { · 60 unmodified lines
6869 let tcp_connection = task_definition.tcp_connection.clone().unwrap_or_default();
6970 let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
7071
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+
71101 Ok(DebugAdapterBinary {
72− command: delegate
73− .node_runtime()
74− .binary_path()
75− .await?
76− .to_string_lossy()
77− .into_owned(),
102+ command: Some(
103+ delegate
104+ .node_runtime()
105+ .binary_path()
106+ .await?
107+ .to_string_lossy()
108+ .into_owned(),
109+ ),
78110 arguments: vec![
79111 adapter_path
80112 .join(Self::ADAPTER_PATH)
@@ -91,7 +123,7 @@ impl JsDebugAdapter { · 10 unmodified lines
91123 timeout,
92124 }),
93125 request_args: StartDebuggingRequestArguments {
94− configuration: task_definition.config.clone(),
126+ configuration,
95127 request: self.request_kind(&task_definition.config)?,
96128 },
97129 })
@@ -171,7 +203,7 @@ impl DebugAdapter for JsDebugAdapter { · 73 unmodified lines
171203 "properties": {
172204 "type": {
173205 "type": "string",
174− "enum": ["pwa-node", "node", "chrome", "pwa-chrome", "edge", "pwa-edge"],
206+ "enum": ["pwa-node", "node", "chrome", "pwa-chrome", "msedge", "pwa-msedge"],
175207 "description": "The type of debug session",
176208 "default": "pwa-node"
177209 },
@@ -431,4 +463,25 @@ impl DebugAdapter for JsDebugAdapter { · 253 unmodified lines
431463 self.get_installed_binary(delegate, &config, user_installed_path, cx)
432464 .await
433465 }
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+
473+fn 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);
434487 }
crates/dap_adapters/src/php.rs+15−7
@@ -71,13 +71,21 @@ impl PhpDebugAdapter { · 70 unmodified lines
7171 let tcp_connection = task_definition.tcp_connection.clone().unwrap_or_default();
7272 let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
7373
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+
7480 Ok(DebugAdapterBinary {
75− command: delegate
76− .node_runtime()
77− .binary_path()
78− .await?
79− .to_string_lossy()
80− .into_owned(),
81+ command: Some(
82+ delegate
83+ .node_runtime()
84+ .binary_path()
85+ .await?
86+ .to_string_lossy()
87+ .into_owned(),
88+ ),
8189 arguments: vec![
8290 adapter_path
8391 .join(Self::ADAPTER_PATH)
@@ -93,7 +101,7 @@ impl PhpDebugAdapter { · 9 unmodified lines
93101 cwd: Some(delegate.worktree_root_path().to_path_buf()),
94102 envs: HashMap::default(),
95103 request_args: StartDebuggingRequestArguments {
96− configuration: task_definition.config.clone(),
104+ configuration,
97105 request: <Self as DebugAdapter>::request_kind(self, &task_definition.config)?,
98106 },
99107 })
crates/dap_adapters/src/python.rs+8−2
@@ -83,6 +83,7 @@ impl PythonDebugAdapter { · 82 unmodified lines
8383
8484 fn request_args(
8585 &self,
86+ delegate: &Arc<dyn DapDelegate>,
8687 task_definition: &DebugTaskDefinition,
8788 ) -> Result<StartDebuggingRequestArguments> {
8889 let request = self.request_kind(&task_definition.config)?;
@@ -95,6 +96,11 @@ impl PythonDebugAdapter { · 6 unmodified lines
9596 }
9697 }
9798
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+
98104 Ok(StartDebuggingRequestArguments {
99105 configuration,
100106 request,
@@ -187,7 +193,7 @@ impl PythonDebugAdapter { · 86 unmodified lines
187193 );
188194
189195 Ok(DebugAdapterBinary {
190− command: python_command,
196+ command: Some(python_command),
191197 arguments,
192198 connection: Some(adapters::TcpArguments {
193199 host,
@@ -196,7 +202,7 @@ impl PythonDebugAdapter { · 2 unmodified lines
196202 }),
197203 cwd: Some(delegate.worktree_root_path().to_path_buf()),
198204 envs: HashMap::default(),
199− request_args: self.request_args(config)?,
205+ request_args: self.request_args(delegate, config)?,
200206 })
201207 }
202208 }
crates/dap_adapters/src/ruby.rs+107−176
@@ -1,16 +1,18 @@
1−use anyhow::Result;
1+use anyhow::{Result, bail};
22 use async_trait::async_trait;
3+use collections::FxHashMap;
34 use dap::{
4− DebugRequest, StartDebuggingRequestArguments,
5+ DebugRequest, StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest,
56 adapters::{
67 DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName, DebugTaskDefinition,
78 },
89 };
910 use gpui::{AsyncApp, SharedString};
1011 use language::LanguageName;
12+use serde::{Deserialize, Serialize};
1113 use serde_json::json;
1214 use std::path::PathBuf;
13−use std::sync::Arc;
15+use std::{ffi::OsStr, sync::Arc};
1416 use task::{DebugScenario, ZedDebugConfig};
1517 use util::command::new_smol_command;
1618
@@ -21,6 +23,18 @@ impl RubyDebugAdapter { · 4 unmodified lines
2123 const ADAPTER_NAME: &'static str = "Ruby";
2224 }
2325
26+#[derive(Serialize, Deserialize)]
27+struct 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+
2438 #[async_trait(?Send)]
2539 impl DebugAdapter for RubyDebugAdapter {
2640 fn name(&self) -> DebugAdapterName {
@@ -31,185 +45,70 @@ impl DebugAdapter for RubyDebugAdapter { · 4 unmodified lines
3145 Some(SharedString::new_static("Ruby").into())
3246 }
3347
48+ fn request_kind(&self, _: &serde_json::Value) -> Result<StartDebuggingRequestArgumentsRequest> {
49+ Ok(StartDebuggingRequestArgumentsRequest::Launch)
50+ }
51+
3452 async fn dap_schema(&self) -> serde_json::Value {
3553 json!({
36− "oneOf": [
37− {
38− "allOf": [
39− {
40− "type": "object",
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− ]
54+ "type": "object",
55+ "properties": {
56+ "command": {
57+ "type": "string",
58+ "description": "Command name (ruby, rake, bin/rails, bundle exec ruby, etc)",
12459 },
125− {
126− "allOf": [
127− {
128− "type": "object",
129− "required": ["request"],
130− "properties": {
131− "request": {
132− "type": "string",
133− "enum": ["attach"],
134− "description": "Request to attach to an existing process"
135− }
136− }
137− },
138− {
139− "type": "object",
140− "properties": {
141− "rdbgPath": {
142− "type": "string",
143− "description": "Location of the rdbg executable"
144− },
145− "debugPort": {
146− "type": "string",
147− "description": "UNIX domain socket name or TPC/IP host:port"
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− ]
60+ "script": {
61+ "type": "string",
62+ "description": "Absolute path to a Ruby file."
63+ },
64+ "cwd": {
65+ "type": "string",
66+ "description": "Directory to execute the program in",
67+ "default": "${ZED_WORKTREE_ROOT}"
68+ },
69+ "args": {
70+ "type": "array",
71+ "description": "Command line arguments passed to the program",
72+ "items": {
73+ "type": "string"
74+ },
75+ "default": []
76+ },
77+ "env": {
78+ "type": "object",
79+ "description": "Additional environment variables to pass to the debugging (and debugged) process",
80+ "default": {}
81+ },
82+ }
17383 })
17484 }
17585
17686 fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
177− let mut config = serde_json::Map::new();
178−
179− match &zed_scenario.request {
87+ match zed_scenario.request {
18088 DebugRequest::Launch(launch) => {
181− config.insert("request".to_string(), json!("launch"));
182− config.insert("script".to_string(), json!(launch.program));
183− config.insert("command".to_string(), json!("ruby"));
184−
185− if !launch.args.is_empty() {
186− config.insert("args".to_string(), json!(launch.args));
187− }
188−
189− if !launch.env.is_empty() {
190− config.insert("env".to_string(), json!(launch.env));
191− }
192−
193− if let Some(cwd) = &launch.cwd {
194− config.insert("cwd".to_string(), json!(cwd));
195− }
196−
197− // Ruby stops on entry so there's no need to handle that case
89+ let config = RubyDebugConfig {
90+ script_or_command: Some(launch.program),
91+ script: None,
92+ command: None,
93+ args: launch.args,
94+ env: launch.env,
95+ cwd: launch.cwd.clone(),
96+ };
97+
98+ let config = serde_json::to_value(config)?;
99+
100+ Ok(DebugScenario {
101+ adapter: zed_scenario.adapter,
102+ label: zed_scenario.label,
103+ config,
104+ tcp_connection: None,
105+ build: None,
106+ })
198107 }
199− DebugRequest::Attach(attach) => {
200− config.insert("request".to_string(), json!("attach"));
201−
202− config.insert("processId".to_string(), json!(attach.process_id));
108+ DebugRequest::Attach(_) => {
109+ anyhow::bail!("Attach requests are unsupported");
203110 }
204111 }
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− })
213112 }
214113
215114 async fn get_binary(
@@ -247,26 +146,58 @@ impl DebugAdapter for RubyDebugAdapter { · 31 unmodified lines
247146
248147 let tcp_connection = definition.tcp_connection.clone().unwrap_or_default();
249148 let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
149+ let ruby_config = serde_json::from_value::<RubyDebugConfig>(definition.config.clone())?;
250150
251− let arguments = vec![
151+ let mut arguments = vec![
252152 "--open".to_string(),
253153 format!("--port={}", port),
254154 format!("--host={}", host),
255155 ];
256156
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+
257184 Ok(DebugAdapterBinary {
258− command: rdbg_path.to_string_lossy().to_string(),
185+ command: Some(rdbg_path.to_string_lossy().to_string()),
259186 arguments,
260187 connection: Some(dap::adapters::TcpArguments {
261188 host,
262189 port,
263190 timeout,
264191 }),
265− cwd: None,
266− envs: std::collections::HashMap::default(),
192+ cwd: Some(
193+ ruby_config
194+ .cwd
195+ .unwrap_or(delegate.worktree_root_path().to_owned()),
196+ ),
197+ envs: ruby_config.env.into_iter().collect(),
267198 request_args: StartDebuggingRequestArguments {
268199 request: self.request_kind(&definition.config)?,
269− configuration: definition.config.clone(),
200+ configuration,
270201 },
271202 })
272203 }
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
7474 }
7575
7676 async 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());
7878 ThreadSafeConnection::builder::<M>(db_path.to_string_lossy().as_ref(), true)
7979 .with_db_initialization_query(DB_INITIALIZE_QUERY)
8080 .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
8484 }
8585
8686 async fn open_fallback_db<M: Migrator>() -> ThreadSafeConnection {
87− log::info!("Opening fallback db");
87+ log::warn!("Opening fallback in-memory database");
8888 ThreadSafeConnection::builder::<M>(FALLBACK_DB_NAME, false)
8989 .with_db_initialization_query(DB_INITIALIZE_QUERY)
9090 .with_connection_initialize_query(CONNECTION_INITIALIZE_QUERY)
crates/debugger_tools/src/dap_log.rs+320−155
@@ -1,4 +1,5 @@
11 use dap::{
2+ adapters::DebugAdapterName,
23 client::SessionId,
34 debugger_settings::DebuggerSettings,
45 transport::{IoKind, LogKind},
@@ -31,6 +32,13 @@ use workspace::{ · 26 unmodified lines
3132 ui::{Button, Clickable, ContextMenu, Label, LabelCommon, PopoverMenu, h_flex},
3233 };
3334
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+
40+const MAX_SESSIONS: usize = 10;
41+
3442 struct DapLogView {
3543 editor: Entity<Editor>,
3644 focus_handle: FocusHandle,
@@ -43,9 +51,9 @@ struct DapLogView { · 6 unmodified lines
4351
4452 pub struct LogStore {
4553 projects: HashMap<WeakEntity<Project>, ProjectState>,
46− debug_clients: HashMap<SessionId, DebugAdapterState>,
47− rpc_tx: UnboundedSender<(SessionId, IoKind, String)>,
48− adapter_log_tx: UnboundedSender<(SessionId, IoKind, String)>,
54+ debug_sessions: VecDeque<DebugAdapterState>,
55+ rpc_tx: UnboundedSender<(SessionId, IoKind, Option<SharedString>, SharedString)>,
56+ adapter_log_tx: UnboundedSender<(SessionId, IoKind, Option<SharedString>, SharedString)>,
4957 }
5058
5159 struct ProjectState {
@@ -53,13 +61,19 @@ struct ProjectState { · 1 unmodified line
5361 }
5462
5563 struct DebugAdapterState {
56− log_messages: VecDeque<String>,
64+ id: SessionId,
65+ log_messages: VecDeque<SharedString>,
5766 rpc_messages: RpcMessages,
67+ adapter_name: DebugAdapterName,
68+ has_adapter_logs: bool,
69+ is_terminated: bool,
5870 }
5971
6072 struct RpcMessages {
61− messages: VecDeque<String>,
73+ messages: VecDeque<SharedString>,
6274 last_message_kind: Option<MessageKind>,
75+ initialization_sequence: Vec<SharedString>,
76+ last_init_message_kind: Option<MessageKind>,
6377 }
6478
6579 impl RpcMessages {
@@ -68,7 +82,9 @@ impl RpcMessages { · 2 unmodified lines
6882 fn new() -> Self {
6983 Self {
7084 last_message_kind: None,
85+ last_init_message_kind: None,
7186 messages: VecDeque::with_capacity(Self::MESSAGE_QUEUE_LIMIT),
87+ initialization_sequence: Vec::new(),
7288 }
7389 }
7490 }
@@ -92,22 +108,27 @@ impl MessageKind { · 17 unmodified lines
92108 }
93109
94110 impl DebugAdapterState {
95− fn new() -> Self {
111+ fn new(id: SessionId, adapter_name: DebugAdapterName, has_adapter_logs: bool) -> Self {
96112 Self {
113+ id,
97114 log_messages: VecDeque::new(),
98115 rpc_messages: RpcMessages::new(),
116+ adapter_name,
117+ has_adapter_logs,
118+ is_terminated: false,
99119 }
100120 }
101121 }
102122
103123 impl LogStore {
104124 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)>();
106127 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 {
108129 if let Some(this) = this.upgrade() {
109130 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);
111132 })?;
112133 }
113134
@@ -117,12 +138,13 @@ impl LogStore { · 3 unmodified lines
117138 })
118139 .detach_and_log_err(cx);
119140
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)>();
121143 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 {
123145 if let Some(this) = this.upgrade() {
124146 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);
126148 })?;
127149 }
128150
@@ -135,30 +157,10 @@ impl LogStore { · 6 unmodified lines
135157 rpc_tx,
136158 adapter_log_tx,
137159 projects: HashMap::new(),
138− debug_clients: HashMap::new(),
160+ debug_sessions: Default::default(),
139161 }
140162 }
141163
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−
162164 pub fn add_project(&mut self, project: &Entity<Project>, cx: &mut Context<Self>) {
163165 let weak_project = project.downgrade();
164166 self.projects.insert(
@@ -174,13 +176,15 @@ impl LogStore { · 9 unmodified lines
174176 dap_store::DapStoreEvent::DebugClientStarted(session_id) => {
175177 let session = dap_store.read(cx).session_by_id(session_id);
176178 if let Some(session) = session {
177− this.add_debug_client(*session_id, session, cx);
179+ this.add_debug_session(*session_id, session, cx);
178180 }
179181 }
180182 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);
182187 }
183−
184188 _ => {}
185189 },
186190 ),
@@ -190,63 +194,88 @@ impl LogStore { · 3 unmodified lines
190194 }
191195
192196 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)
194200 }
195201
196− fn add_debug_client_message(
202+ fn add_debug_adapter_message(
197203 &mut self,
198204 id: SessionId,
199205 io_kind: IoKind,
200− message: String,
206+ command: Option<SharedString>,
207+ message: SharedString,
201208 cx: &mut Context<Self>,
202209 ) {
203210 let Some(debug_client_state) = self.get_debug_adapter_state(id) else {
204211 return;
205212 };
206213
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+
207221 let kind = match io_kind {
208222 IoKind::StdOut | IoKind::StdErr => MessageKind::Receive,
209223 IoKind::StdIn => MessageKind::Send,
210224 };
211225
212226 let rpc_messages = &mut debug_client_state.rpc_messages;
227+
228+ // Push a separator if the kind has changed
213229 if rpc_messages.last_message_kind != Some(kind) {
214− Self::add_debug_client_entry(
230+ Self::get_debug_adapter_entry(
215231 &mut rpc_messages.messages,
216232 id,
217− kind.label().to_string(),
233+ kind.label().into(),
218234 LogKind::Rpc,
219235 cx,
220236 );
221237 rpc_messages.last_message_kind = Some(kind);
222238 }
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+ }
224257
225258 cx.notify();
226259 }
227260
228− fn add_debug_client_log(
261+ fn add_debug_adapter_log(
229262 &mut self,
230263 id: SessionId,
231264 io_kind: IoKind,
232− message: String,
265+ message: SharedString,
233266 cx: &mut Context<Self>,
234267 ) {
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 {
236269 return;
237270 };
238271
239272 let message = match io_kind {
240− IoKind::StdErr => {
241− let mut message = message.clone();
242− message.insert_str(0, "stderr: ");
243− message
244− }
273+ IoKind::StdErr => format!("stderr: {message}").into(),
245274 _ => message,
246275 };
247276
248− Self::add_debug_client_entry(
249− &mut debug_client_state.log_messages,
277+ Self::get_debug_adapter_entry(
278+ &mut debug_adapter_state.log_messages,
250279 id,
251280 message,
252281 LogKind::Adapter,
@@ -255,13 +284,13 @@ impl LogStore { · 2 unmodified lines
255284 cx.notify();
256285 }
257286
258− fn add_debug_client_entry(
259− log_lines: &mut VecDeque<String>,
287+ fn get_debug_adapter_entry(
288+ log_lines: &mut VecDeque<SharedString>,
260289 id: SessionId,
261− message: String,
290+ message: SharedString,
262291 kind: LogKind,
263292 cx: &mut Context<Self>,
264− ) {
293+ ) -> SharedString {
265294 while log_lines.len() >= RpcMessages::MESSAGE_QUEUE_LIMIT {
266295 log_lines.pop_front();
267296 }
@@ -275,33 +304,69 @@ impl LogStore { · 7 unmodified lines
275304 )
276305 .ok()
277306 })
307+ .map(SharedString::from)
278308 .unwrap_or(message)
279309 } else {
280310 message
281311 };
282312 log_lines.push_back(entry.clone());
283313
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
285321 }
286322
287− fn add_debug_client(
323+ fn add_debug_session(
288324 &mut self,
289− client_id: SessionId,
290− client: Entity<Session>,
291− cx: &App,
292− ) -> Option<&mut DebugAdapterState> {
293− let client_state = self
294− .debug_clients
295− .entry(client_id)
296− .or_insert_with(DebugAdapterState::new);
325+ session_id: SessionId,
326+ session: Entity<Session>,
327+ cx: &mut Context<Self>,
328+ ) {
329+ if self
330+ .debug_sessions
331+ .iter_mut()
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);
297354
298355 let io_tx = self.rpc_tx.clone();
299356
300− let client = client.read(cx).adapter_client()?;
357+ let Some(client) = session.read(cx).adapter_client() else {
358+ return;
359+ };
360+
301361 client.add_log_handler(
302− move |io_kind, message| {
362+ move |io_kind, command, message| {
303363 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+ ))
305370 .ok();
306371 },
307372 LogKind::Rpc,
@@ -309,34 +374,66 @@ impl LogStore { · 1 unmodified line
309374
310375 let log_io_tx = self.adapter_log_tx.clone();
311376 client.add_log_handler(
312− move |io_kind, message| {
377+ move |io_kind, command, message| {
313378 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+ ))
315385 .ok();
316386 },
317387 LogKind::Adapter,
318388 );
319−
320− Some(client_state)
321389 }
322390
323− fn remove_debug_client(&mut self, client_id: SessionId, cx: &mut Context<Self>) {
324− self.debug_clients.remove(&client_id);
391+ fn clean_sessions(&mut self, cx: &mut Context<Self>) {
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+ });
325400 cx.notify();
326401 }
327402
328− fn log_messages_for_client(&mut self, client_id: SessionId) -> Option<&mut VecDeque<String>> {
329− Some(&mut self.debug_clients.get_mut(&client_id)?.log_messages)
403+ fn log_messages_for_session(
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+ })
330424 }
331425
332− fn rpc_messages_for_client(&mut self, client_id: SessionId) -> Option<&mut VecDeque<String>> {
333− Some(
334− &mut self
335− .debug_clients
336− .get_mut(&client_id)?
337− .rpc_messages
338− .messages,
339− )
426+ fn initialization_sequence_for_session(
427+ &mut self,
428+ session_id: SessionId,
429+ ) -> Option<&mut Vec<SharedString>> {
430+ self.debug_sessions.iter_mut().find_map(|state| {
431+ if state.id == session_id {
432+ Some(&mut state.rpc_messages.initialization_sequence)
433+ } else {
434+ None
435+ }
436+ })
340437 }
341438 }
342439
@@ -356,18 +453,15 @@ impl Render for DapLogToolbarItemView { · 13 unmodified lines
356453 return Empty.into_any_element();
357454 };
358455
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| {
360457 (
361− log_view.menu_items(cx).unwrap_or_default(),
362− log_view.current_view.map(|(client_id, _)| client_id),
458+ log_view.menu_items(cx),
459+ log_view.current_view.map(|(session_id, _)| session_id),
363460 )
364461 });
365462
366− let current_client = current_client_id.and_then(|current_client_id| {
367− menu_rows
368− .iter()
369− .find(|row| row.client_id == current_client_id)
370− });
463+ let current_client = current_session_id
464+ .and_then(|session_id| menu_rows.iter().find(|row| row.session_id == session_id));
371465
372466 let dap_menu: PopoverMenu<_> = PopoverMenu::new("DapLogView")
373467 .anchor(gpui::Corner::TopLeft)
@@ -377,8 +471,8 @@ impl Render for DapLogToolbarItemView { · 3 unmodified lines
377471 .map(|sub_item| {
378472 Cow::Owned(format!(
379473 "{} ({}) - {}",
380− sub_item.client_name,
381− sub_item.client_id.0,
474+ sub_item.adapter_name,
475+ sub_item.session_id.0,
382476 match sub_item.selected_entry {
383477 LogKind::Adapter => ADAPTER_LOGS,
384478 LogKind::Rpc => RPC_MESSAGES,
@@ -397,9 +491,10 @@ impl Render for DapLogToolbarItemView { · 12 unmodified lines
397491 .w_full()
398492 .pl_2()
399493 .child(
400− Label::new(
401− format!("{}. {}", row.client_id.0, row.client_name,),
402− )
494+ Label::new(format!(
495+ "{}. {}",
496+ row.session_id.0, row.adapter_name,
497+ ))
403498 .color(workspace::ui::Color::Muted),
404499 )
405500 .into_any_element()
@@ -415,23 +510,40 @@ impl Render for DapLogToolbarItemView { · 9 unmodified lines
415510 .into_any_element()
416511 },
417512 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);
419514 }),
420515 );
421516 }
422517
423− menu = menu.custom_entry(
424− move |_window, _cx| {
425− div()
426− .w_full()
427− .pl_4()
428− .child(Label::new(RPC_MESSAGES))
429− .into_any_element()
430− },
431− window.handler_for(&log_view, move |view, window, cx| {
432− view.show_rpc_trace_for_server(row.client_id, window, cx);
433− }),
434− );
518+ menu = menu
519+ .custom_entry(
520+ move |_window, _cx| {
521+ div()
522+ .w_full()
523+ .pl_4()
524+ .child(Label::new(RPC_MESSAGES))
525+ .into_any_element()
526+ },
527+ window.handler_for(&log_view, move |view, window, cx| {
528+ view.show_rpc_trace_for_server(row.session_id, window, cx);
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+ );
435547 }
436548
437549 menu
@@ -518,7 +630,13 @@ impl DapLogView { · 80 unmodified lines
518630 }
519631 });
520632
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 {
522640 editor,
523641 focus_handle,
524642 project,
@@ -526,7 +644,17 @@ impl DapLogView { · 1 unmodified line
526644 editor_subscriptions,
527645 current_view: None,
528646 _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+ }
529655 }
656+
657+ this
530658 }
531659
532660 fn editor_for_logs(
@@ -559,42 +687,34 @@ impl DapLogView { · 26 unmodified lines
559687 (editor, vec![editor_subscription, search_subscription])
560688 }
561689
562− fn menu_items(&self, cx: &App) -> Option<Vec<DapMenuItem>> {
563− let mut menu_items = self
564− .project
690+ fn menu_items(&self, cx: &App) -> Vec<DapMenuItem> {
691+ self.log_store
565692 .read(cx)
566− .dap_store()
567− .read(cx)
568− .sessions()
569− .filter_map(|session| {
570− let session = session.read(cx);
571− session.adapter();
572− let client = session.adapter_client()?;
573− Some(DapMenuItem {
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− })
693+ .debug_sessions
694+ .iter()
695+ .rev()
696+ .map(|state| DapMenuItem {
697+ session_id: state.id,
698+ adapter_name: state.adapter_name.clone(),
699+ has_adapter_logs: state.has_adapter_logs,
700+ selected_entry: self.current_view.map_or(LogKind::Adapter, |(_, kind)| kind),
579701 })
580− .collect::<Vec<_>>();
581− menu_items.sort_by_key(|item| item.client_id.0);
582− Some(menu_items)
702+ .collect::<Vec<_>>()
583703 }
584704
585705 fn show_rpc_trace_for_server(
586706 &mut self,
587− client_id: SessionId,
707+ session_id: SessionId,
588708 window: &mut Window,
589709 cx: &mut Context<Self>,
590710 ) {
591711 let rpc_log = self.log_store.update(cx, |log_store, _| {
592712 log_store
593− .rpc_messages_for_client(client_id)
594− .map(|state| log_contents(&state))
713+ .rpc_messages_for_session(session_id)
714+ .map(|state| log_contents(state.iter().cloned()))
595715 });
596716 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));
598718 let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, window, cx);
599719 let language = self.project.read(cx).languages().language_for_name("JSON");
600720 editor
@@ -626,17 +746,17 @@ impl DapLogView { · 25 unmodified lines
626746
627747 fn show_log_messages_for_adapter(
628748 &mut self,
629− client_id: SessionId,
749+ session_id: SessionId,
630750 window: &mut Window,
631751 cx: &mut Context<Self>,
632752 ) {
633753 let message_log = self.log_store.update(cx, |log_store, _| {
634754 log_store
635− .log_messages_for_client(client_id)
636− .map(|state| log_contents(&state))
755+ .log_messages_for_session(session_id)
756+ .map(|state| log_contents(state.iter().cloned()))
637757 });
638758 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));
640760 let (editor, editor_subscriptions) = Self::editor_for_logs(message_log, window, cx);
641761 editor
642762 .read(cx)
@@ -652,14 +772,53 @@ impl DapLogView { · 9 unmodified lines
652772
653773 cx.focus_self(window);
654774 }
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+ }
655817 }
656818
657−fn log_contents(lines: &VecDeque<String>) -> String {
658− let (a, b) = lines.as_slices();
659− let a = a.iter().map(move |v| v.as_ref());
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);
819+fn log_contents(lines: impl Iterator<Item = SharedString>) -> String {
820+ lines.fold(String::new(), |mut acc, el| {
821+ acc.push_str(&el);
663822 acc.push('\n');
664823 acc
665824 })
@@ -667,14 +826,15 @@ fn log_contents(lines: &VecDeque<String>) -> String { · 1 unmodified line
667826
668827 #[derive(Clone, PartialEq)]
669828 pub(crate) struct DapMenuItem {
670− pub client_id: SessionId,
671− pub client_name: String,
829+ pub session_id: SessionId,
830+ pub adapter_name: DebugAdapterName,
672831 pub has_adapter_logs: bool,
673832 pub selected_entry: LogKind,
674833 }
675834
676835 const ADAPTER_LOGS: &str = "Adapter Logs";
677836 const RPC_MESSAGES: &str = "RPC Messages";
837+const INITIALIZATION_SEQUENCE: &str = "Initialization Sequence";
678838
679839 impl Render for DapLogView {
680840 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
@@ -836,7 +996,7 @@ impl Focusable for DapLogView { · 155 unmodified lines
836996 pub enum Event {
837997 NewLogEntry {
838998 id: SessionId,
839− entry: String,
999+ entry: SharedString,
8401000 kind: LogKind,
8411001 },
8421002 }
@@ -849,12 +1009,16 @@ impl EventEmitter<SearchEvent> for DapLogView {} · 6 unmodified lines
8491009 #[cfg(any(test, feature = "test-support"))]
8501010 impl LogStore {
8511011 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()
8531016 }
8541017
855− pub fn rpc_messages_for_session_id(&self, session_id: SessionId) -> Vec<String> {
856− self.debug_clients
857− .get(&session_id)
1018+ pub fn rpc_messages_for_session_id(&self, session_id: SessionId) -> Vec<SharedString> {
1019+ self.debug_sessions
1020+ .iter()
1021+ .find(|adapter_state| adapter_state.id == session_id)
8581022 .expect("This session should exist if a test is calling")
8591023 .rpc_messages
8601024 .messages
@@ -862,9 +1026,10 @@ impl LogStore { · 1 unmodified line
8621026 .into()
8631027 }
8641028
865− pub fn log_messages_for_session_id(&self, session_id: SessionId) -> Vec<String> {
866− self.debug_clients
867− .get(&session_id)
1029+ pub fn log_messages_for_session_id(&self, session_id: SessionId) -> Vec<SharedString> {
1030+ self.debug_sessions
1031+ .iter()
1032+ .find(|adapter_state| adapter_state.id == session_id)
8681033 .expect("This session should exist if a test is calling")
8691034 .log_messages
8701035 .clone()
crates/debugger_ui/src/debugger_panel.rs+149−132
@@ -2,25 +2,23 @@ use crate::persistence::DebuggerPaneItem; · 1 unmodified line
22 use crate::session::DebugSession;
33 use crate::session::running::RunningState;
44 use crate::{
5− ClearAllBreakpoints, Continue, Detach, FocusBreakpointList, FocusConsole, FocusFrames,
6− FocusLoadedSources, FocusModules, FocusTerminal, FocusVariables, NewProcessModal,
7− NewProcessMode, Pause, Restart, ShowStackTrace, StepBack, StepInto, StepOut, StepOver, Stop,
8− ToggleExpandItem, ToggleIgnoreBreakpoints, ToggleSessionPicker, ToggleThreadPicker,
9− persistence, spawn_task_or_modal,
5+ ClearAllBreakpoints, Continue, CopyDebugAdapterArguments, Detach, FocusBreakpointList,
6+ FocusConsole, FocusFrames, FocusLoadedSources, FocusModules, FocusTerminal, FocusVariables,
7+ NewProcessModal, NewProcessMode, Pause, Restart, StepInto, StepOut, StepOver, Stop,
8+ ToggleExpandItem, ToggleSessionPicker, ToggleThreadPicker, persistence, spawn_task_or_modal,
109 };
1110 use anyhow::Result;
12−use command_palette_hooks::CommandPaletteFilter;
13−use dap::StartDebuggingRequestArguments;
1411 use dap::adapters::DebugAdapterName;
1512 use dap::debugger_settings::DebugPanelDockPosition;
1613 use dap::{
1714 ContinuedEvent, LoadedSourceEvent, ModuleEvent, OutputEvent, StoppedEvent, ThreadEvent,
1815 client::SessionId, debugger_settings::DebuggerSettings,
1916 };
17+use dap::{DapRegistry, StartDebuggingRequestArguments};
2018 use gpui::{
21− Action, App, AsyncWindowContext, Context, DismissEvent, Entity, EntityId, EventEmitter,
22− FocusHandle, Focusable, MouseButton, MouseDownEvent, Point, Subscription, Task, WeakEntity,
23− actions, anchored, deferred,
19+ Action, App, AsyncWindowContext, ClipboardItem, Context, DismissEvent, Entity, EntityId,
20+ EventEmitter, FocusHandle, Focusable, MouseButton, MouseDownEvent, Point, Subscription, Task,
21+ WeakEntity, actions, anchored, deferred,
2422 };
2523
2624 use language::Buffer;
@@ -29,10 +27,10 @@ use project::{Fs, WorktreeId}; · 2 unmodified lines
2927 use project::{Project, debugger::session::ThreadStatus};
3028 use rpc::proto::{self};
3129 use settings::Settings;
32−use std::any::TypeId;
3330 use std::sync::Arc;
3431 use task::{DebugScenario, TaskContext};
3532 use ui::{ContextMenu, Divider, PopoverMenuHandle, Tooltip, prelude::*};
33+use util::maybe;
3634 use workspace::SplitDirection;
3735 use workspace::{
3836 Pane, Workspace,
@@ -140,82 +138,6 @@ impl DebugPanel { · 101 unmodified lines
140138 .map(|session| session.read(cx).running_state().clone())
141139 }
142140
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−
219141 pub fn load(
220142 workspace: WeakEntity<Workspace>,
221143 cx: &mut AsyncWindowContext,
@@ -233,17 +155,6 @@ impl DebugPanel { · 11 unmodified lines
233155 )
234156 });
235157
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();
247158 workspace.set_debugger_provider(DebuggerProvider(debug_panel.clone()));
248159
249160 debug_panel
@@ -269,6 +180,19 @@ impl DebugPanel { · 19 unmodified lines
269180 cx,
270181 )
271182 });
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+ };
272196 self.debug_scenario_scheduled_last = true;
273197 if let Some(inventory) = self
274198 .project
@@ -303,7 +227,7 @@ impl DebugPanel { · 28 unmodified lines
303227 .await?;
304228 dap_store
305229 .update(cx, |dap_store, cx| {
306− dap_store.boot_session(session.clone(), definition, cx)
230+ dap_store.boot_session(session.clone(), definition, worktree, cx)
307231 })?
308232 .await
309233 }
@@ -412,7 +336,7 @@ impl DebugPanel { · 102 unmodified lines
412336 let dap_store_handle = self.project.read(cx).dap_store().clone();
413337 let label = curr_session.read(cx).label().clone();
414338 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();
416340 let task = curr_session.update(cx, |session, cx| session.shutdown(cx));
417341
418342 cx.spawn_in(window, async move |this, cx| {
@@ -426,8 +350,26 @@ impl DebugPanel { · 7 unmodified lines
426350 });
427351 (session, task)
428352 })?;
429− Self::register_session(this.clone(), session, true, cx).await?;
430− task.await
353+ Self::register_session(this.clone(), session.clone(), true, cx).await?;
354+
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(())
431373 })
432374 .detach_and_log_err(cx);
433375 }
@@ -445,12 +387,12 @@ impl DebugPanel { · 11 unmodified lines
445387 };
446388
447389 let dap_store_handle = self.project.read(cx).dap_store().clone();
448− let mut label = parent_session.read(cx).label().clone();
449− if !label.ends_with("(child)") {
450− label = format!("{label} (child)").into();
451− }
390+ let label = self.label_for_child_session(&parent_session, request, cx);
452391 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+ };
454396 binary.request_args = request.clone();
455397 cx.spawn_in(window, async move |this, cx| {
456398 let (session, task) = dap_store_handle.update(cx, |dap_store, cx| {
@@ -462,7 +404,11 @@ impl DebugPanel { · 5 unmodified lines
462404 });
463405 (session, task)
464406 })?;
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?;
466412 task.await
467413 })
468414 .detach_and_log_err(cx);
@@ -579,6 +525,26 @@ impl DebugPanel { · 110 unmodified lines
579525 }
580526 }
581527
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+
582548 pub(crate) fn top_controls_strip(
583549 &mut self,
584550 window: &mut Window,
@@ -608,6 +574,12 @@ impl DebugPanel { · 23 unmodified lines
608574 }
609575 })
610576 };
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+ };
611583
612584 Some(
613585 div.border_b_1()
@@ -629,6 +601,8 @@ impl DebugPanel { · 15 unmodified lines
629601 project::debugger::session::ThreadStatus::Exited,
630602 );
631603 let capabilities = running_state.read(cx).capabilities(cx);
604+ let supports_detach =
605+ running_state.read(cx).session().read(cx).is_attached();
632606 this.map(|this| {
633607 if thread_status == ThreadStatus::Running {
634608 this.child(
@@ -817,33 +791,48 @@ impl DebugPanel { · 182 unmodified lines
817791 }
818792 }),
819793 )
820− .child(
821− IconButton::new("debug-disconnect", IconName::DebugDetach)
822− .icon_size(IconSize::XSmall)
823− .on_click(window.listener_for(
824− &running_state,
825− |this, _, _, cx| {
826− this.detach_client(cx);
827− },
828− ))
829− .tooltip({
830− let focus_handle = focus_handle.clone();
831− move |window, cx| {
832− Tooltip::for_action_in(
833− "Detach",
834− &Detach,
835− &focus_handle,
836− window,
837− cx,
838− )
839− }
840− }),
794+ .when(
795+ supports_detach,
796+ |div| {
797+ div.child(
798+ IconButton::new(
799+ "debug-disconnect",
800+ IconName::DebugDetach,
801+ )
802+ .disabled(
803+ thread_status != ThreadStatus::Stopped
804+ && thread_status != ThreadStatus::Running,
805+ )
806+ .icon_size(IconSize::XSmall)
807+ .on_click(window.listener_for(
808+ &running_state,
809+ |this, _, _, cx| {
810+ this.detach_client(cx);
811+ },
812+ ))
813+ .tooltip({
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+ },
841827 )
842828 },
843829 ),
844830 )
845831 .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+ }),
847836 )
848837 .child(
849838 h_flex()
@@ -884,7 +873,10 @@ impl DebugPanel { · 34 unmodified lines
884873 window,
885874 cx,
886875 ))
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+ }),
888880 ),
889881 ),
890882 )
@@ -1042,6 +1034,25 @@ impl DebugPanel { · 151 unmodified lines
10421034 cx.emit(PanelEvent::ZoomIn);
10431035 }
10441036 }
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+ }
10451056 }
10461057
10471058 async fn register_session_inner(
@@ -1069,6 +1080,11 @@ async fn register_session_inner( · 21 unmodified lines
10691080 .ok();
10701081 let serialized_layout = persistence::get_serialized_layout(adapter_name).await;
10711082 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();
10721088 this.sessions.retain(|session| {
10731089 !session
10741090 .read(cx)
@@ -1082,8 +1098,8 @@ async fn register_session_inner( · 7 unmodified lines
10821098 let debug_session = DebugSession::running(
10831099 this.project.clone(),
10841100 this.workspace.clone(),
1101+ parent_session.map(|p| p.read(cx).running_state().read(cx).debug_terminal.clone()),
10851102 session,
1086− cx.weak_entity(),
10871103 serialized_layout,
10881104 this.position(window, cx).axis(),
10891105 window,
@@ -1363,6 +1379,7 @@ impl Render for DebugPanel { · 273 unmodified lines
13631379 });
13641380 cx.notify();
13651381 }))
1382+ .on_action(cx.listener(Self::copy_debug_adapter_arguments))
13661383 .when(self.active_session.is_some(), |this| {
13671384 this.on_mouse_down(
13681385 MouseButton::Right,
crates/debugger_ui/src/debugger_ui.rs+231−204
@@ -1,14 +1,17 @@
1+use std::any::TypeId;
2+
13 use dap::debugger_settings::DebuggerSettings;
24 use debugger_panel::{DebugPanel, ToggleFocus};
35 use editor::Editor;
46 use feature_flags::{DebuggerFeatureFlag, FeatureFlagViewExt};
5−use gpui::{App, EntityInputHandler, actions};
7+use gpui::{App, DispatchPhase, EntityInputHandler, actions};
68 use new_process_modal::{NewProcessModal, NewProcessMode};
7−use project::debugger::{self, breakpoint_store::SourceBreakpoint};
9+use project::debugger::{self, breakpoint_store::SourceBreakpoint, session::ThreadStatus};
810 use session::DebugSession;
911 use settings::Settings;
1012 use stack_trace_view::StackTraceView;
1113 use tasks_ui::{Spawn, TaskOverrides};
14+use ui::{FluentBuilder, InteractiveElement};
1215 use util::maybe;
1316 use workspace::{ItemHandle, ShutdownDebugAdapters, Workspace};
1417
@@ -53,6 +56,8 @@ actions!( · 38 unmodified lines
5356 ]
5457 );
5558
59+actions!(dev, [CopyDebugAdapterArguments]);
60+
5661 pub fn init(cx: &mut App) {
5762 DebuggerSettings::register(cx);
5863 workspace::FollowableViewRegistry::register::<DebugSession>(cx);
@@ -68,107 +73,20 @@ pub fn init(cx: &mut App) { · 9 unmodified lines
6873 .register_action(|workspace, _: &ToggleFocus, window, cx| {
6974 workspace.toggle_panel_focus::<DebugPanel>(window, cx);
7075 })
71− .register_action(|workspace, _: &Pause, _, cx| {
72− if let Some(debug_panel) = workspace.panel::<DebugPanel>(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− }
76+ .register_action(|workspace: &mut Workspace, _: &Start, window, cx| {
77+ NewProcessModal::show(workspace, window, NewProcessMode::Debug, None, cx);
17178 })
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+ )
17290 .register_action(
17391 |workspace: &mut Workspace, _: &ShutdownDebugAdapters, _window, cx| {
17492 workspace.project().update(cx, |project, cx| {
@@ -178,135 +96,244 @@ pub fn init(cx: &mut App) { · 3 unmodified lines
17896 })
17997 },
18098 )
181− .register_action(
182− |workspace: &mut Workspace, _: &ShowStackTrace, window, cx| {
183− let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) else {
184− return;
185− };
99+ .register_action_renderer(|div, workspace, _, cx| {
100+ let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) else {
101+ return div;
102+ };
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+ }
186114
187− if let Some(existing) = workspace.item_of_type::<StackTraceView>(cx) {
188− let is_active = workspace
189− .active_item(cx)
190− .is_some_and(|item| item.item_id() == existing.item_id());
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− };
115+ let caps = running_state.capabilities(cx);
116+ let supports_step_back = caps.supports_step_back.unwrap_or_default();
117+ let supports_detach = running_state.session().read(cx).is_attached();
118+ let status = running_state.thread_status(cx);
196119
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+ };
198167
199− let stack_trace_view = active_session.update(cx, |session, cx| {
200− session.stack_trace_view(project, window, cx).clone()
201− });
168+ if let Some(existing) = workspace.item_of_type::<StackTraceView>(cx)
169+ {
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+ };
202181
203− workspace.add_item_to_active_pane(
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− };
182+ let project = workspace.project();
221183
222− debug_panel.update(cx, |debug_panel, cx| {
223− debug_panel.rerun_last_session(workspace, window, cx);
184+ let stack_trace_view =
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();
224206 })
225− },
226− );
207+ })
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+ });
227231 })
228232 })
229233 .detach();
230234
231235 cx.observe_new({
232− move |editor: &mut Editor, _, cx| {
236+ move |editor: &mut Editor, _, _| {
233237 editor
234− .register_action(cx.listener(
235− move |editor, _: &editor::actions::DebuggerRunToCursor, _, cx| {
236− maybe!({
237− let debug_panel =
238− editor.workspace()?.read(cx).panel::<DebugPanel>(cx)?;
239− let cursor_point: language::Point = editor.selections.newest(cx).head();
240− let active_session = debug_panel.read(cx).active_session()?;
238+ .register_action_renderer(move |editor, window, cx| {
239+ let Some(workspace) = editor.workspace() else {
240+ return;
241+ };
242+ let Some(debug_panel) = workspace.read(cx).panel::<DebugPanel>(cx) else {
243+ return;
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();
241264
242− let (buffer, position, _) = editor
243− .buffer()
244− .read(cx)
245− .point_to_buffer_point(cursor_point, cx)?;
265+ editor
266+ .buffer()
267+ .read(cx)
268+ .point_to_buffer_point(cursor_point, cx)
269+ })
270+ .ok()??;
246271
247− let path =
272+ let path =
248273 debugger::breakpoint_store::BreakpointStore::abs_path_from_buffer(
249274 &buffer, cx,
250275 )?;
251276
252− let source_breakpoint = SourceBreakpoint {
253− row: position.row,
254− path,
255− message: None,
256− condition: None,
257− hit_condition: None,
258− state: debugger::breakpoint_store::BreakpointState::Enabled,
259− };
277+ let source_breakpoint = SourceBreakpoint {
278+ row: position.row,
279+ path,
280+ message: None,
281+ condition: None,
282+ hit_condition: None,
283+ state: debugger::breakpoint_store::BreakpointState::Enabled,
284+ };
260285
261− active_session.update(cx, |session, cx| {
262− session.running_state().update(cx, |state, cx| {
263− if let Some(thread_id) = state.selected_thread_id() {
264− state.session().update(cx, |session, cx| {
265− session.run_to_position(
266− source_breakpoint,
267− thread_id,
268− cx,
269− );
270− })
271− }
286+ active_session.update(cx, |session, cx| {
287+ session.running_state().update(cx, |state, cx| {
288+ if let Some(thread_id) = state.selected_thread_id() {
289+ state.session().update(cx, |session, cx| {
290+ session.run_to_position(
291+ source_breakpoint,
292+ thread_id,
293+ cx,
294+ );
295+ })
296+ }
297+ });
272298 });
273− });
274299
275− Some(())
276− });
277− },
278− ))
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()?;
300+ Some(())
301+ });
302+ }
303+ });
288304
289− let text = editor.text_for_range(
290− editor.selections.newest(cx).range(),
291− &mut None,
292− window,
293− cx,
294− )?;
305+ window.on_action(
306+ TypeId::of::<editor::actions::EvaluateSelectedText>(),
307+ move |_, _, window, cx| {
308+ maybe!({
309+ let text = editor
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()??;
295319
296− active_session.update(cx, |session, cx| {
297− session.running_state().update(cx, |state, cx| {
298− let stack_id = state.selected_stack_frame_id(cx);
320+ active_session.update(cx, |session, cx| {
321+ session.running_state().update(cx, |state, cx| {
322+ let stack_id = state.selected_stack_frame_id(cx);
299323
300− state.session().update(cx, |session, cx| {
301− session.evaluate(text, None, stack_id, None, cx).detach();
324+ state.session().update(cx, |session, cx| {
325+ session
326+ .evaluate(text, None, stack_id, None, cx)
327+ .detach();
328+ });
302329 });
303330 });
304− });
305331
306− Some(())
307− });
308− },
309− ))
332+ Some(())
333+ });
334+ },
335+ );
336+ })
310337 .detach();
311338 }
312339 })
crates/debugger_ui/src/new_process_modal.rs+262−105
@@ -1,4 +1,4 @@
1−use collections::FxHashMap;
1+use collections::{FxHashMap, HashMap};
22 use language::LanguageRegistry;
33 use paths::local_debug_file_relative_path;
44 use std::{
@@ -15,9 +15,9 @@ use dap::{ · 10 unmodified lines
1515 use editor::{Editor, EditorElement, EditorStyle};
1616 use fuzzy::{StringMatch, StringMatchCandidate};
1717 use gpui::{
18− App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, HighlightStyle,
19− InteractiveText, KeyContext, PromptButton, PromptLevel, Render, StyledText, Subscription,
20− TextStyle, UnderlineStyle, WeakEntity,
18+ Action, App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
19+ HighlightStyle, InteractiveText, KeyContext, PromptButton, PromptLevel, Render, StyledText,
20+ Subscription, Task, TextStyle, UnderlineStyle, WeakEntity,
2121 };
2222 use itertools::Itertools as _;
2323 use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch};
@@ -28,10 +28,10 @@ use theme::ThemeSettings; · 4 unmodified lines
2828 use ui::{
2929 ActiveTheme, Button, ButtonCommon, ButtonSize, CheckboxWithLabel, Clickable, Color, Context,
3030 ContextMenu, Disableable, DropdownMenu, FluentBuilder, Icon, IconName, IconSize,
31− IconWithIndicator, Indicator, InteractiveElement, IntoElement, Label, LabelCommon as _,
32− ListItem, ListItemSpacing, ParentElement, RenderOnce, SharedString, Styled, StyledExt,
33− StyledTypography, ToggleButton, ToggleState, Toggleable, Tooltip, Window, div, h_flex, px,
34− relative, rems, v_flex,
31+ IconWithIndicator, Indicator, InteractiveElement, IntoElement, KeyBinding, Label,
32+ LabelCommon as _, LabelSize, ListItem, ListItemSpacing, ParentElement, RenderOnce,
33+ SharedString, Styled, StyledExt, StyledTypography, ToggleButton, ToggleState, Toggleable,
34+ Tooltip, Window, div, h_flex, px, relative, rems, v_flex,
3535 };
3636 use util::ResultExt;
3737 use workspace::{ModalView, Workspace, pane};
@@ -50,7 +50,7 @@ pub(super) struct NewProcessModal { · 12 unmodified lines
5050 mode: NewProcessMode,
5151 debug_picker: Entity<Picker<DebugDelegate>>,
5252 attach_mode: Entity<AttachMode>,
53− launch_mode: Entity<ConfigureMode>,
53+ configure_mode: Entity<ConfigureMode>,
5454 task_mode: TaskMode,
5555 debugger: Option<DebugAdapterName>,
5656 // save_scenario_state: Option<SaveScenarioState>,
@@ -194,26 +194,31 @@ impl NewProcessModal { · 137 unmodified lines
194194 return Ok(());
195195 };
196196
197− let (used_tasks, current_resolved_tasks) =
198− task_inventory.update(cx, |task_inventory, cx| {
197+ let (used_tasks, current_resolved_tasks) = task_inventory
198+ .update(cx, |task_inventory, cx| {
199199 task_inventory
200− .used_and_current_resolved_tasks(&task_contexts, cx)
201− })?;
202−
203− debug_picker
204− .update_in(cx, |picker, window, cx| {
205− picker.delegate.tasks_loaded(
206− task_contexts.clone(),
207− languages,
208− lsp_tasks.clone(),
209− current_resolved_tasks.clone(),
210− add_current_language_tasks,
211− cx,
212− );
213− picker.refresh(window, cx);
214− cx.notify();
215− })
216− .ok();
200+ .used_and_current_resolved_tasks(task_contexts.clone(), cx)
201+ })?
202+ .await;
203+
204+ if let Ok(task) = debug_picker.update(cx, |picker, cx| {
205+ picker.delegate.tasks_loaded(
206+ task_contexts.clone(),
207+ languages,
208+ lsp_tasks.clone(),
209+ current_resolved_tasks.clone(),
210+ add_current_language_tasks,
211+ cx,
212+ )
213+ }) {
214+ task.await;
215+ debug_picker
216+ .update_in(cx, |picker, window, cx| {
217+ picker.refresh(window, cx);
218+ cx.notify();
219+ })
220+ .ok();
221+ }
217222
218223 if let Some(active_cwd) = task_contexts
219224 .active_context()
@@ -253,7 +258,7 @@ impl NewProcessModal { · 33 unmodified lines
253258 Self {
254259 debug_picker,
255260 attach_mode,
256− launch_mode: configure_mode,
261+ configure_mode,
257262 task_mode,
258263 debugger: None,
259264 mode,
@@ -283,7 +288,7 @@ impl NewProcessModal { · 23 unmodified lines
283288 NewProcessMode::Attach => self.attach_mode.update(cx, |this, cx| {
284289 this.clone().render(window, cx).into_any_element()
285290 }),
286− NewProcessMode::Launch => self.launch_mode.update(cx, |this, cx| {
291+ NewProcessMode::Launch => self.configure_mode.update(cx, |this, cx| {
287292 this.clone().render(dap_menu, window, cx).into_any_element()
288293 }),
289294 NewProcessMode::Debug => v_flex()
@@ -297,7 +302,7 @@ impl NewProcessModal { · 7 unmodified lines
297302 match self.mode {
298303 NewProcessMode::Task => self.task_mode.task_modal.focus_handle(cx),
299304 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),
301306 NewProcessMode::Debug => self.debug_picker.focus_handle(cx),
302307 }
303308 }
@@ -305,7 +310,7 @@ impl NewProcessModal { · 1 unmodified line
305310 fn debug_scenario(&self, debugger: &str, cx: &App) -> Option<DebugScenario> {
306311 let request = match self.mode {
307312 NewProcessMode::Launch => Some(DebugRequest::Launch(
308− self.launch_mode.read(cx).debug_request(cx),
313+ self.configure_mode.read(cx).debug_request(cx),
309314 )),
310315 NewProcessMode::Attach => Some(DebugRequest::Attach(
311316 self.attach_mode.read(cx).debug_request(),
@@ -315,7 +320,7 @@ impl NewProcessModal { · 3 unmodified lines
315320 let label = suggested_label(&request, debugger);
316321
317322 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())
319324 } else {
320325 None
321326 };
@@ -476,10 +481,9 @@ impl NewProcessModal { · 154 unmodified lines
476481 .get_index_of(adapter.0.as_ref())
477482 .unwrap_or(usize::MAX)
478483 });
479− }
480−
481− if self.debugger.is_none() {
482− self.debugger = available_adapters.first().cloned();
484+ if self.debugger.is_none() {
485+ self.debugger = available_adapters.first().cloned();
486+ }
483487 }
484488
485489 let label = self
@@ -831,7 +835,7 @@ impl Render for NewProcessModal { · 345 unmodified lines
831835 .disabled(
832836 self.debugger.is_none()
833837 || self
834− .launch_mode
838+ .configure_mode
835839 .read(cx)
836840 .program
837841 .read(cx)
@@ -1143,41 +1147,67 @@ impl DebugDelegate { · 305 unmodified lines
11431147 current_resolved_tasks: Vec<(TaskSourceKind, task::ResolvedTask)>,
11441148 add_current_language_tasks: bool,
11451149 cx: &mut Context<Picker<Self>>,
1146− ) {
1150+ ) -> Task<()> {
11471151 self.task_contexts = Some(task_contexts.clone());
1148−
1149− let (recent, scenarios) = self
1150− .task_store
1151− .update(cx, |task_store, cx| {
1152− task_store.task_inventory().map(|inventory| {
1153− inventory.update(cx, |inventory, cx| {
1154− inventory.list_debug_scenarios(
1155− &task_contexts,
1156− lsp_tasks,
1157− current_resolved_tasks,
1158− add_current_language_tasks,
1159− cx,
1160− )
1161− })
1152+ let task = self.task_store.update(cx, |task_store, cx| {
1153+ task_store.task_inventory().map(|inventory| {
1154+ inventory.update(cx, |inventory, cx| {
1155+ inventory.list_debug_scenarios(
1156+ &task_contexts,
1157+ lsp_tasks,
1158+ current_resolved_tasks,
1159+ add_current_language_tasks,
1160+ cx,
1161+ )
11621162 })
11631163 })
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+ };
11651171
1166− if !recent.is_empty() {
1167− self.last_used_candidate_index = Some(recent.len() - 1);
1168− }
1172+ this.update(cx, |this, cx| {
1173+ if !recent.is_empty() {
1174+ this.delegate.last_used_candidate_index = Some(recent.len() - 1);
1175+ }
11691176
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+ });
11711186
1172− self.candidates = recent
1173− .into_iter()
1174− .map(|scenario| Self::get_scenario_kind(&languages, &dap_registry, scenario))
1175− .chain(scenarios.into_iter().map(|(kind, scenario)| {
1176− let (language, scenario) =
1177− Self::get_scenario_kind(&languages, &dap_registry, scenario);
1178− (language.or(Some(kind)), scenario)
1179− }))
1180− .collect();
1187+ this.delegate.candidates = recent
1188+ .into_iter()
1189+ .map(|scenario| Self::get_scenario_kind(&languages, &dap_registry, scenario))
1190+ .chain(
1191+ scenarios
1192+ .into_iter()
1193+ .filter(|(kind, _)| match kind {
1194+ TaskSourceKind::Worktree {
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+ })
11811211 }
11821212 }
11831213
@@ -1202,7 +1232,7 @@ impl PickerDelegate for DebugDelegate { · 18 unmodified lines
12021232 }
12031233
12041234 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()
12061236 }
12071237
12081238 fn update_matches(
@@ -1265,6 +1295,116 @@ impl PickerDelegate for DebugDelegate { · 56 unmodified lines
12651295 }
12661296 }
12671297
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+
12681408 fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<picker::Picker<Self>>) {
12691409 let debug_scenario = self
12701410 .matches
@@ -1300,6 +1440,60 @@ impl PickerDelegate for DebugDelegate { · 29 unmodified lines
13001440 cx.emit(DismissEvent);
13011441 }
13021442
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+
13031497 fn render_match(
13041498 &self,
13051499 ix: usize,
@@ -1369,40 +1563,3 @@ pub(crate) fn resolve_path(path: &mut String) { · 63 unmodified lines
13691563 );
13701564 };
13711565 }
1372−
1373−#[cfg(test)]
1374−impl 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 @@
11 pub mod running;
22
3−use crate::{StackTraceView, debugger_panel::DebugPanel, persistence::SerializedLayout};
3+use crate::{StackTraceView, persistence::SerializedLayout, session::running::DebugTerminal};
44 use dap::client::SessionId;
55 use gpui::{
66 App, Axis, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity,
@@ -22,7 +22,6 @@ pub struct DebugSession { · 15 unmodified lines
2222 running_state: Entity<RunningState>,
2323 label: OnceLock<SharedString>,
2424 stack_trace_view: OnceCell<Entity<StackTraceView>>,
25− _debug_panel: WeakEntity<DebugPanel>,
2625 _worktree_store: WeakEntity<WorktreeStore>,
2726 workspace: WeakEntity<Workspace>,
2827 _subscriptions: [Subscription; 1],
@@ -38,8 +37,8 @@ impl DebugSession { · 9 unmodified lines
3837 pub(crate) fn running(
3938 project: Entity<Project>,
4039 workspace: WeakEntity<Workspace>,
40+ parent_terminal: Option<Entity<DebugTerminal>>,
4141 session: Entity<Session>,
42− _debug_panel: WeakEntity<DebugPanel>,
4342 serialized_layout: Option<SerializedLayout>,
4443 dock_axis: Axis,
4544 window: &mut Window,
@@ -50,6 +49,7 @@ impl DebugSession { · 4 unmodified lines
5049 session.clone(),
5150 project.clone(),
5251 workspace.clone(),
52+ parent_terminal,
5353 serialized_layout,
5454 dock_axis,
5555 window,
@@ -64,7 +64,6 @@ impl DebugSession { · 8 unmodified lines
6464 remote_id: None,
6565 running_state,
6666 label: OnceLock::new(),
67− _debug_panel,
6867 stack_trace_view: OnceCell::new(),
6968 _worktree_store: project.read(cx).worktree_store().downgrade(),
7069 workspace,
crates/debugger_ui/src/session/running.rs+35−30
@@ -605,6 +605,7 @@ impl RunningState { · 604 unmodified lines
605605 session: Entity<Session>,
606606 project: Entity<Project>,
607607 workspace: WeakEntity<Workspace>,
608+ parent_terminal: Option<Entity<DebugTerminal>>,
608609 serialized_pane_layout: Option<SerializedLayout>,
609610 dock_axis: Axis,
610611 window: &mut Window,
@@ -617,7 +618,8 @@ impl RunningState { · 6 unmodified lines
617618 StackFrameList::new(workspace.clone(), session.clone(), weak_state, window, cx)
618619 });
619620
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)));
621623
622624 let variable_list =
623625 cx.new(|cx| VariableList::new(session.clone(), stack_frame_list.clone(), window, cx));
@@ -816,20 +818,20 @@ impl RunningState { · 192 unmodified lines
816818
817819 let request_type = dap_registry
818820 .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))
820822 .and_then(|adapter| adapter.request_kind(&config));
821823
822824 let config_is_valid = request_type.is_ok();
823825
824826 let build_output = if let Some(build) = build {
825− let (task, locator_name) = match build {
827+ let (task_template, locator_name) = match build {
826828 BuildTaskDefinition::Template {
827829 task_template,
828830 locator_name,
829831 } => (task_template, locator_name),
830832 BuildTaskDefinition::ByName(ref label) => {
831− let Some(task) = task_store.update(cx, |this, cx| {
832− this.task_inventory().and_then(|inventory| {
833+ let task = task_store.update(cx, |this, cx| {
834+ this.task_inventory().map(|inventory| {
833835 inventory.read(cx).task_template_by_label(
834836 buffer,
835837 worktree_id,
@@ -837,14 +839,15 @@ impl RunningState { · 1 unmodified line
837839 cx,
838840 )
839841 })
840− })?
841− else {
842− anyhow::bail!("Couldn't find task template for {:?}", build)
843− };
842+ })?;
843+ let task = match task {
844+ Some(task) => task.await,
845+ None => None,
846+ }.with_context(|| format!("Couldn't find task template for {build:?}"))?;
844847 (task, None)
845848 }
846849 };
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 {
848851 anyhow::bail!("Could not resolve task variables within a debug scenario");
849852 };
850853
@@ -852,7 +855,7 @@ impl RunningState { · 1 unmodified line
852855 debug_assert!(!config_is_valid);
853856 Some(locator_name)
854857 } else if !config_is_valid {
855− dap_store
858+ let task = dap_store
856859 .update(cx, |this, cx| {
857860 this.debug_scenario_for_build_task(
858861 task.original_task().clone(),
@@ -860,17 +863,21 @@ impl RunningState { · 1 unmodified line
860863 task.display_label().to_owned().into(),
861864 cx,
862865 )
863− .and_then(|scenario| {
864− match scenario.build {
865− Some(BuildTaskDefinition::Template {
866− locator_name, ..
867− }) => locator_name,
868− _ => None,
869− }
870− })
866+
867+ });
868+ if let Ok(t) = task {
869+ t.await.and_then(|scenario| {
870+ match scenario.build {
871+ Some(BuildTaskDefinition::Template {
872+ locator_name, ..
873+ }) => locator_name,
874+ _ => None,
875+ }
871876 })
872− .ok()
873− .flatten()
877+ } else {
878+ None
879+ }
880+
874881 } else {
875882 None
876883 };
@@ -929,15 +936,13 @@ impl RunningState { · 52 unmodified lines
929936 };
930937
931938 if config_is_valid {
932− // Ok(DebugTaskDefinition {
933− // label,
934− // adapter: DebugAdapterName(adapter),
935− // config,
936− // tcp_connection,
937− // })
938939 } else if let Some((task, locator_name)) = build_output {
939940 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+ })?;
941946 let request = dap_store
942947 .update(cx, |this, cx| {
943948 this.run_debug_locator(&locator_name, task, cx)
@@ -953,7 +958,7 @@ impl RunningState { · 9 unmodified lines
953958
954959 let scenario = dap_registry
955960 .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))
957962 .map(|adapter| adapter.config_from_zed_format(zed_config))??;
958963 config = scenario.config;
959964 Self::substitute_variables_in_config(&mut config, &task_context);
@@ -992,7 +997,7 @@ impl RunningState { · 32 unmodified lines
992997 let cwd = Some(&request.cwd)
993998 .filter(|cwd| cwd.len() > 0)
994999 .map(PathBuf::from)
995− .or_else(|| session.binary().cwd.clone());
1000+ .or_else(|| session.binary().unwrap().cwd.clone());
9961001
9971002 let mut args = request.args.clone();
9981003
crates/debugger_ui/src/session/running/breakpoint_list.rs+3−3
@@ -1,4 +1,5 @@
11 use std::{
2+ ops::Range,
23 path::{Path, PathBuf},
34 sync::Arc,
45 time::Duration,
@@ -277,10 +278,9 @@ impl BreakpointList { · 272 unmodified lines
277278 let selected_ix = self.selected_ix;
278279 let focus_handle = self.focus_handle.clone();
279280 uniform_list(
280− cx.entity(),
281281 "breakpoint-list",
282282 self.breakpoints.len(),
283− move |this, range, window, cx| {
283+ cx.processor(move |this, range: Range<usize>, window, cx| {
284284 range
285285 .clone()
286286 .zip(&mut this.breakpoints[range])
@@ -291,7 +291,7 @@ impl BreakpointList { · 4 unmodified lines
291291 .into_any_element()
292292 })
293293 .collect()
294− },
294+ }),
295295 )
296296 .track_scroll(self.scroll_handle.clone())
297297 .flex_grow()
crates/debugger_ui/src/session/running/module_list.rs+16−12
@@ -8,7 +8,7 @@ use project::{ · 7 unmodified lines
88 ProjectItem as _, ProjectPath,
99 debugger::session::{Session, SessionEvent},
1010 };
11−use std::{path::Path, sync::Arc};
11+use std::{ops::Range, path::Path, sync::Arc};
1212 use ui::{Scrollbar, ScrollbarState, prelude::*};
1313 use workspace::Workspace;
1414
@@ -20,7 +20,7 @@ pub struct ModuleList { · 5 unmodified lines
2020 focus_handle: FocusHandle,
2121 scrollbar_state: ScrollbarState,
2222 entries: Vec<Module>,
23− _rebuild_task: Task<()>,
23+ _rebuild_task: Option<Task<()>>,
2424 _subscription: Subscription,
2525 }
2626
@@ -34,14 +34,16 @@ impl ModuleList { · 7 unmodified lines
3434
3535 let _subscription = cx.subscribe(&session, |this, _, event, cx| match event {
3636 SessionEvent::Stopped(_) | SessionEvent::Modules => {
37− this.schedule_rebuild(cx);
37+ if this._rebuild_task.is_some() {
38+ this.schedule_rebuild(cx);
39+ }
3840 }
3941 _ => {}
4042 });
4143
4244 let scroll_handle = UniformListScrollHandle::new();
4345
44− let mut this = Self {
46+ Self {
4547 scrollbar_state: ScrollbarState::new(scroll_handle.clone()),
4648 scroll_handle,
4749 session,
@@ -50,14 +52,12 @@ impl ModuleList { · 2 unmodified lines
5052 entries: Vec::new(),
5153 selected_ix: None,
5254 _subscription,
53− _rebuild_task: Task::ready(()),
54− };
55− this.schedule_rebuild(cx);
56− this
55+ _rebuild_task: None,
56+ }
5757 }
5858
5959 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| {
6161 this.update(cx, |this, cx| {
6262 let modules = this
6363 .session
@@ -66,7 +66,7 @@ impl ModuleList { · 2 unmodified lines
6666 cx.notify();
6767 })
6868 .ok();
69− });
69+ }));
7070 }
7171
7272 fn open_module(&mut self, path: Arc<Path>, window: &mut Window, cx: &mut Context<Self>) {
@@ -281,10 +281,11 @@ impl ModuleList { · 208 unmodified lines
281281
282282 fn render_list(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
283283 uniform_list(
284− cx.entity(),
285284 "module-list",
286285 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+ }),
288289 )
289290 .track_scroll(self.scroll_handle.clone())
290291 .size_full()
@@ -299,6 +300,9 @@ impl Focusable for ModuleList { · 8 unmodified lines
299300
300301 impl Render for ModuleList {
301302 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+ }
302306 div()
303307 .track_focus(&self.focus_handle)
304308 .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
55 use anyhow::{Context as _, Result, anyhow};
66 use dap::StackFrameId;
77 use gpui::{
8− AnyElement, Entity, EventEmitter, FocusHandle, Focusable, MouseButton, ScrollStrategy,
9− Stateful, Subscription, Task, UniformListScrollHandle, WeakEntity, uniform_list,
8+ AnyElement, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, ListState, MouseButton,
9+ Stateful, Subscription, Task, WeakEntity, list,
1010 };
11+use util::debug_panic;
1112
1213 use crate::StackTraceView;
1314 use language::PointUtf16;
@@ -35,13 +36,16 @@ pub struct StackFrameList { · 21 unmodified lines
3536 selected_ix: Option<usize>,
3637 opened_stack_frame_id: Option<StackFrameId>,
3738 scrollbar_state: ScrollbarState,
38− scroll_handle: UniformListScrollHandle,
39+ list_state: ListState,
40+ error: Option<SharedString>,
3941 _refresh_task: Task<()>,
4042 }
4143
4244 #[derive(Debug, PartialEq, Eq)]
4345 pub enum StackFrameEntry {
4446 Normal(dap::StackFrame),
47+ /// Used to indicate that the frame is artificial and is a visual label or separator
48+ Label(dap::StackFrame),
4549 Collapsed(Vec<dap::StackFrame>),
4650 }
4751
@@ -54,7 +58,6 @@ impl StackFrameList { · 6 unmodified lines
5458 cx: &mut Context<Self>,
5559 ) -> Self {
5660 let focus_handle = cx.focus_handle();
57− let scroll_handle = UniformListScrollHandle::new();
5861
5962 let _subscription =
6063 cx.subscribe_in(&session, window, |this, _, event, window, cx| match event {
@@ -67,17 +70,27 @@ impl StackFrameList { · 6 unmodified lines
6770 _ => {}
6871 });
6972
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+
7082 let mut this = Self {
71− scrollbar_state: ScrollbarState::new(scroll_handle.clone()),
7283 session,
7384 workspace,
7485 focus_handle,
7586 state,
7687 _subscription,
7788 entries: Default::default(),
89+ error: None,
7890 selected_ix: None,
7991 opened_stack_frame_id: None,
80− scroll_handle,
92+ list_state,
93+ scrollbar_state,
8194 _refresh_task: Task::ready(()),
8295 };
8396 this.schedule_refresh(true, window, cx);
@@ -89,37 +102,35 @@ impl StackFrameList { · 5 unmodified lines
89102 &self.entries
90103 }
91104
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> {
93110 self.entries
94111 .iter()
95112 .flat_map(|frame| match frame {
96113 StackFrameEntry::Normal(frame) => vec![frame.clone()],
97− StackFrameEntry::Collapsed(frames) => {
98− if show_collapsed {
99− frames.clone()
100− } else {
101− vec![]
102− }
103− }
114+ StackFrameEntry::Label(frame) if show_labels => vec![frame.clone()],
115+ StackFrameEntry::Collapsed(frames) if show_collapsed => frames.clone(),
116+ _ => vec![],
104117 })
105118 .collect::<Vec<_>>()
106119 }
107120
108− fn stack_frames(&self, cx: &mut App) -> Vec<StackFrame> {
109− self.state
110− .read_with(cx, |state, _| state.thread_id)
111− .ok()
112− .flatten()
113− .map(|thread_id| {
114− self.session
115− .update(cx, |this, cx| this.stack_frames(thread_id, cx))
116− })
117− .unwrap_or_default()
121+ fn stack_frames(&self, cx: &mut App) -> Result<Vec<StackFrame>> {
122+ if let Ok(Some(thread_id)) = self.state.read_with(cx, |state, _| state.thread_id) {
123+ self.session
124+ .update(cx, |this, cx| this.stack_frames(thread_id, cx))
125+ } else {
126+ Ok(Vec::default())
127+ }
118128 }
119129
120130 #[cfg(test)]
121131 pub(crate) fn dap_stack_frames(&self, cx: &mut App) -> Vec<dap::StackFrame> {
122132 self.stack_frames(cx)
133+ .unwrap_or_default()
123134 .into_iter()
124135 .map(|stack_frame| stack_frame.dap.clone())
125136 .collect()
@@ -141,7 +152,7 @@ impl StackFrameList { · 15 unmodified lines
141152 let debounce = this
142153 .update(cx, |this, cx| {
143154 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()
145156 })
146157 .ok()
147158 .unwrap_or_default();
@@ -168,20 +179,34 @@ impl StackFrameList { · 20 unmodified lines
168179 .and_then(|ix| self.entries.get(ix))
169180 .and_then(|entry| match entry {
170181 StackFrameEntry::Normal(stack_frame) => Some(stack_frame.id),
171− StackFrameEntry::Collapsed(stack_frames) => {
172− stack_frames.first().map(|stack_frame| stack_frame.id)
173− }
182+ StackFrameEntry::Collapsed(_) | StackFrameEntry::Label(_) => None,
174183 });
175184 let mut entries = Vec::new();
176185 let mut collapsed_entries = Vec::new();
177186 let mut first_stack_frame = None;
178−
179− let stack_frames = self.stack_frames(cx);
187+ let mut first_stack_frame_with_path = None;
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+ };
180201 for stack_frame in &stack_frames {
181202 match stack_frame.dap.presentation_hint {
182− Some(dap::StackFramePresentationHint::Deemphasize) => {
203+ Some(dap::StackFramePresentationHint::Deemphasize)
204+ | Some(dap::StackFramePresentationHint::Subtle) => {
183205 collapsed_entries.push(stack_frame.dap.clone());
184206 }
207+ Some(dap::StackFramePresentationHint::Label) => {
208+ entries.push(StackFrameEntry::Label(stack_frame.dap.clone()));
209+ }
185210 _ => {
186211 let collapsed_entries = std::mem::take(&mut collapsed_entries);
187212 if !collapsed_entries.is_empty() {
@@ -189,6 +214,15 @@ impl StackFrameList { · 1 unmodified line
189214 }
190215
191216 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+ }
192226 entries.push(StackFrameEntry::Normal(stack_frame.dap.clone()));
193227 }
194228 }
@@ -198,22 +232,23 @@ impl StackFrameList { · 3 unmodified lines
198232 if !collapsed_entries.is_empty() {
199233 entries.push(StackFrameEntry::Collapsed(collapsed_entries.clone()));
200234 }
235+ self.entries = entries;
201236
202− std::mem::swap(&mut self.entries, &mut entries);
203−
204− if let Some(ix) = first_stack_frame.filter(|_| open_first_stack_frame) {
237+ if let Some(ix) = first_stack_frame_with_path
238+ .or(first_stack_frame)
239+ .filter(|_| open_first_stack_frame)
240+ {
205241 self.select_ix(Some(ix), cx);
206242 self.activate_selected_entry(window, cx);
207243 } else if let Some(old_selected_frame_id) = old_selected_frame_id {
208244 let ix = self.entries.iter().position(|entry| match entry {
209245 StackFrameEntry::Normal(frame) => frame.id == old_selected_frame_id,
210− StackFrameEntry::Collapsed(frames) => {
211− frames.iter().any(|frame| frame.id == old_selected_frame_id)
212− }
246+ StackFrameEntry::Collapsed(_) | StackFrameEntry::Label(_) => false,
213247 });
214248 self.selected_ix = ix;
215249 }
216250
251+ self.list_state.reset(self.entries.len());
217252 cx.emit(StackFrameListEvent::BuiltEntries);
218253 cx.notify();
219254 }
@@ -228,6 +263,7 @@ impl StackFrameList { · 8 unmodified lines
228263 .entries
229264 .iter()
230265 .flat_map(|entry| match entry {
266+ StackFrameEntry::Label(stack_frame) => std::slice::from_ref(stack_frame),
231267 StackFrameEntry::Normal(stack_frame) => std::slice::from_ref(stack_frame),
232268 StackFrameEntry::Collapsed(stack_frames) => stack_frames.as_slice(),
233269 })
@@ -340,6 +376,33 @@ impl StackFrameList { · 106 unmodified lines
340376 });
341377 }
342378
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+
343406 fn render_normal_entry(
344407 &self,
345408 ix: usize,
@@ -443,7 +506,7 @@ impl StackFrameList { · 97 unmodified lines
443506 .into_any()
444507 }
445508
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>) {
447510 let Some(StackFrameEntry::Collapsed(stack_frames)) = self.entries.get_mut(ix) else {
448511 return;
449512 };
@@ -452,6 +515,9 @@ impl StackFrameList { · 2 unmodified lines
452515 .map(StackFrameEntry::Normal);
453516 self.entries.splice(ix..ix + 1, entries);
454517 self.selected_ix = Some(ix);
518+ self.list_state.reset(self.entries.len());
519+ cx.emit(StackFrameListEvent::BuiltEntries);
520+ cx.notify();
455521 }
456522
457523 fn render_collapsed_entry(
@@ -501,6 +567,7 @@ impl StackFrameList { · 43 unmodified lines
501567
502568 fn render_entry(&self, ix: usize, cx: &mut Context<Self>) -> AnyElement {
503569 match &self.entries[ix] {
570+ StackFrameEntry::Label(stack_frame) => self.render_label_entry(stack_frame, cx),
504571 StackFrameEntry::Normal(stack_frame) => self.render_normal_entry(ix, stack_frame, cx),
505572 StackFrameEntry::Collapsed(stack_frames) => {
506573 self.render_collapsed_entry(ix, stack_frames, cx)
@@ -543,10 +610,6 @@ impl StackFrameList { · 36 unmodified lines
543610
544611 fn select_ix(&mut self, ix: Option<usize>, cx: &mut Context<Self>) {
545612 self.selected_ix = ix;
546− if let Some(ix) = self.selected_ix {
547− self.scroll_handle
548− .scroll_to_item(ix, ScrollStrategy::Center);
549− }
550613 cx.notify();
551614 }
552615
@@ -621,7 +684,10 @@ impl StackFrameList { · 68 unmodified lines
621684 self.go_to_stack_frame_inner(stack_frame, window, cx)
622685 .detach_and_log_err(cx)
623686 }
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),
625691 }
626692 cx.notify();
627693 }
@@ -630,15 +696,11 @@ impl StackFrameList { · 2 unmodified lines
630696 self.activate_selected_entry(window, cx);
631697 }
632698
633− fn render_list(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
634− uniform_list(
635− cx.entity(),
636− "stack-frame-list",
637− self.entries.len(),
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()
699+ fn render_list(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
700+ div()
701+ .p_1()
702+ .size_full()
703+ .child(list(self.list_state.clone()).size_full())
642704 }
643705 }
644706
@@ -647,12 +709,27 @@ impl Render for StackFrameList { · 2 unmodified lines
647709 div()
648710 .track_focus(&self.focus_handle)
649711 .size_full()
650− .p_1()
651712 .on_action(cx.listener(Self::select_next))
652713 .on_action(cx.listener(Self::select_previous))
653714 .on_action(cx.listener(Self::select_first))
654715 .on_action(cx.listener(Self::select_last))
655716 .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+ })
656733 .child(self.render_list(window, cx))
657734 .child(self.render_vertical_scrollbar(cx))
658735 }
crates/debugger_ui/src/session/running/variable_list.rs+3−2
@@ -980,10 +980,11 @@ impl Render for VariableList { · 979 unmodified lines
980980 .on_action(cx.listener(Self::edit_variable))
981981 .child(
982982 uniform_list(
983− cx.entity().clone(),
984983 "variable-list",
985984 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+ }),
987988 )
988989 .track_scroll(self.list_handle.clone())
989990 .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
1616 use util::ResultExt as _;
1717 use workspace::{
1818 Item, ItemHandle as _, ItemNavHistory, ToolbarItemLocation, Workspace,
19− item::{BreadcrumbText, ItemEvent},
19+ item::{BreadcrumbText, ItemEvent, SaveOptions},
2020 searchable::SearchableItemHandle,
2121 };
2222
@@ -148,7 +148,7 @@ impl StackTraceView { · 125 unmodified lines
148148
149149 let stack_frames = self
150150 .stack_frame_list
151− .read_with(cx, |list, _| list.flatten_entries(false));
151+ .read_with(cx, |list, _| list.flatten_entries(false, false));
152152
153153 let frames_to_open: Vec<_> = stack_frames
154154 .into_iter()
@@ -237,7 +237,7 @@ impl StackTraceView { · 82 unmodified lines
237237
238238 let stack_frames = self
239239 .stack_frame_list
240− .read_with(cx, |session, _| session.flatten_entries(false));
240+ .read_with(cx, |session, _| session.flatten_entries(false, false));
241241
242242 let active_idx = self
243243 .selected_stack_frame_id
@@ -386,12 +386,12 @@ impl Item for StackTraceView { · 142 unmodified lines
386386
387387 fn save(
388388 &mut self,
389− format: bool,
389+ options: SaveOptions,
390390 project: Entity<Project>,
391391 window: &mut Window,
392392 cx: &mut Context<Self>,
393393 ) -> Task<Result<()>> {
394− self.editor.save(format, project, window, cx)
394+ self.editor.save(options, project, window, cx)
395395 }
396396
397397 fn save_as(
crates/debugger_ui/src/tests/debugger_panel.rs+24−8
@@ -33,6 +33,7 @@ use std::{ · 32 unmodified lines
3333 use terminal_view::terminal_panel::TerminalPanel;
3434 use tests::{active_debug_session_panel, init_test, init_test_workspace};
3535 use util::path;
36+use workspace::item::SaveOptions;
3637 use workspace::{Item, dock::Panel};
3738
3839 #[gpui::test]
@@ -443,18 +444,20 @@ async fn test_handle_start_debugging_request( · 404 unmodified lines
443444 .update(cx, |workspace, _window, cx| {
444445 let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
445446
446− // Active session does not change on spawn.
447+ // Active session changes on spawn, as the parent has never stopped.
447448 let active_session = debug_panel
448449 .read(cx)
449450 .active_session()
450451 .unwrap()
451452 .read(cx)
452453 .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−
457454 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+
458461 assert_eq!(current_sessions.len(), 2);
459462 assert_eq!(current_sessions[0], sessions[0]);
460463
@@ -468,14 +471,19 @@ async fn test_handle_start_debugging_request( · 7 unmodified lines
468471
469472 // We should preserve the original binary (params to spawn process etc.) except for launch params
470473 // (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();
472475 original_binary.request_args = StartDebuggingRequestArguments {
473476 request: StartDebuggingRequestArgumentsRequest::Launch,
474477 configuration: fake_config.clone(),
475478 };
476479
477480 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(),
479487 &original_binary
480488 );
481489 })
@@ -1213,7 +1221,15 @@ async fn test_send_breakpoints_when_editor_has_been_saved( · 731 unmodified lines
12131221
12141222 editor
12151223 .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+ )
12171233 })
12181234 .await
12191235 .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
718718 stack_frame_list.entries()
719719 );
720720
721− stack_frame_list.expand_collapsed_entry(1);
721+ stack_frame_list.expand_collapsed_entry(1, cx);
722722
723723 assert_eq!(
724724 &vec![
@@ -735,7 +735,7 @@ async fn test_collapsed_entries(executor: BackgroundExecutor, cx: &mut TestAppCo · 10 unmodified lines
735735 stack_frame_list.entries()
736736 );
737737
738− stack_frame_list.expand_collapsed_entry(4);
738+ stack_frame_list.expand_collapsed_entry(4, cx);
739739
740740 assert_eq!(
741741 &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
191191 running_state.update(cx, |running_state, cx| {
192192 let (stack_frame_list, stack_frame_id) =
193193 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+ )
195198 });
196199
197200 assert_eq!(stack_frames, stack_frame_list);
@@ -432,7 +435,10 @@ async fn test_fetch_variables_for_multiple_scopes( · 234 unmodified lines
432435 running_state.update(cx, |running_state, cx| {
433436 let (stack_frame_list, stack_frame_id) =
434437 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+ )
436442 });
437443
438444 assert_eq!(Some(1), stack_frame_id);
@@ -1459,7 +1465,10 @@ async fn test_variable_list_only_sends_requests_when_rendering( · 1020 unmodified lines
14591465 running_state.update(cx, |running_state, cx| {
14601466 let (stack_frame_list, stack_frame_id) =
14611467 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+ )
14631472 });
14641473
14651474 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
17411750 running_state.update(cx, |running_state, cx| {
17421751 let (stack_frame_list, stack_frame_id) =
17431752 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+ )
17451757 });
17461758
17471759 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
17961808 running_state.update(cx, |running_state, cx| {
17971809 let (stack_frame_list, stack_frame_id) =
17981810 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+ )
18001815 });
18011816
18021817 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
4343 use util::ResultExt;
4444 use workspace::{
4545 ItemNavHistory, ToolbarItemLocation, Workspace,
46− item::{BreadcrumbText, Item, ItemEvent, ItemHandle, TabContentParams},
46+ item::{BreadcrumbText, Item, ItemEvent, ItemHandle, SaveOptions, TabContentParams},
4747 searchable::SearchableItemHandle,
4848 };
4949
@@ -849,12 +849,12 @@ impl Item for ProjectDiagnosticsEditor { · 799 unmodified lines
849849
850850 fn save(
851851 &mut self,
852− format: bool,
852+ options: SaveOptions,
853853 project: Entity<Project>,
854854 window: &mut Window,
855855 cx: &mut Context<Self>,
856856 ) -> Task<Result<()>> {
857− self.editor.save(format, project, window, cx)
857+ self.editor.save(options, project, window, cx)
858858 }
859859
860860 fn save_as(
crates/editor/Cargo.toml+0−1
@@ -35,7 +35,6 @@ assets.workspace = true · 34 unmodified lines
3535 client.workspace = true
3636 clock.workspace = true
3737 collections.workspace = true
38−command_palette_hooks.workspace = true
3938 convert_case.workspace = true
4039 dap.workspace = true
4140 db.workspace = true
crates/editor/src/actions.rs+3−2
@@ -87,6 +87,7 @@ pub struct ToggleCodeActions { · 86 unmodified lines
8787 #[derive(PartialEq, Clone, Debug)]
8888 pub enum CodeActionSource {
8989 Indicator(DisplayRow),
90+ RunMenu(DisplayRow),
9091 QuickActionBar,
9192 }
9293
@@ -243,6 +244,8 @@ impl_actions!( · 150 unmodified lines
243244 ]
244245 );
245246
247+actions!(debugger, [RunToCursor, EvaluateSelectedText]);
248+
246249 actions!(
247250 editor,
248251 [
@@ -426,8 +429,6 @@ actions!( · 177 unmodified lines
426429 DisableBreakpoint,
427430 EnableBreakpoint,
428431 EditLogBreakpoint,
429− DebuggerRunToCursor,
430− DebuggerEvaluateSelectedText,
431432 ToggleAutoSignatureHelp,
432433 ToggleGitBlameInline,
433434 OpenGitBlameCommit,
crates/editor/src/code_completion_tests.rs+389−2276
@@ -1,2382 +1,495 @@
1−use crate::{
2− code_context_menus::{CompletionsMenu, SortableMatch},
3− editor_settings::SnippetSortOrder,
4−};
5−use fuzzy::StringMatch;
1+use crate::{code_context_menus::CompletionsMenu, editor_settings::SnippetSortOrder};
2+use fuzzy::StringMatchCandidate;
63 use gpui::TestAppContext;
4+use language::CodeLabel;
5+use lsp::{CompletionItem, CompletionItemKind, LanguageServerId};
6+use project::{Completion, CompletionSource};
7+use std::sync::Arc;
8+use std::sync::atomic::AtomicBool;
9+use text::Anchor;
710
811 #[gpui::test]
9−fn test_sort_matches_local_variable_over_global_variable(_cx: &mut TestAppContext) {
12+async fn test_sort_matches_local_variable_over_global_variable(cx: &mut TestAppContext) {
1013 // Case 1: "foo"
11− let query: Option<&str> = Some("foo");
12− let mut matches: Vec<SortableMatch<'_>> = vec![
13− SortableMatch {
14− string_match: StringMatch {
15− candidate_id: 0,
16− score: 0.2727272727272727,
17− positions: vec![],
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− },
14+ let completions = vec![
15+ CompletionBuilder::constant("foo_bar_baz", "7fffffff"),
16+ CompletionBuilder::variable("foo_bar_qux", "7ffffffe"),
17+ CompletionBuilder::constant("floorf64", "80000000"),
18+ CompletionBuilder::constant("floorf32", "80000000"),
19+ CompletionBuilder::constant("floorf16", "80000000"),
20+ CompletionBuilder::constant("floorf128", "80000000"),
8521 ];
86− CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default());
87− assert_eq!(
88− matches[0].string_match.string.as_str(),
89− "foo_bar_qux",
90− "Match order not expected"
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− );
22+ let matches = sort_matches("foo", &completions, SnippetSortOrder::default(), cx).await;
23+ assert_eq!(matches[0], "foo_bar_qux");
24+ assert_eq!(matches[1], "foo_bar_baz");
25+ assert_eq!(matches[2], "floorf16");
26+ assert_eq!(matches[3], "floorf32");
10727
10828 // Case 2: "foobar"
109− let query: Option<&str> = Some("foobar");
110− let mut matches: Vec<SortableMatch<'_>> = vec![
111− SortableMatch {
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− },
29+ let completions = vec![
30+ CompletionBuilder::constant("foo_bar_baz", "7fffffff"),
31+ CompletionBuilder::variable("foo_bar_qux", "7ffffffe"),
13532 ];
136− CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default());
137− assert_eq!(
138− matches[0].string_match.string.as_str(),
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− );
33+ let matches = sort_matches("foobar", &completions, SnippetSortOrder::default(), cx).await;
34+ assert_eq!(matches[0], "foo_bar_qux");
35+ assert_eq!(matches[1], "foo_bar_baz");
14736 }
14837
14938 #[gpui::test]
150−fn test_sort_matches_local_variable_over_global_enum(_cx: &mut TestAppContext) {
39+async fn test_sort_matches_local_variable_over_global_enum(cx: &mut TestAppContext) {
15140 // Case 1: "ele"
152− let query: Option<&str> = Some("ele");
153− let mut matches: Vec<SortableMatch<'_>> = vec![
154− SortableMatch {
155− string_match: StringMatch {
156− candidate_id: 0,
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− },
41+ let completions = vec![
42+ CompletionBuilder::constant("ElementType", "7fffffff"),
43+ CompletionBuilder::variable("element_type", "7ffffffe"),
44+ CompletionBuilder::constant("simd_select", "80000000"),
45+ CompletionBuilder::keyword("while let", "7fffffff"),
20246 ];
203− CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default());
204− assert_eq!(
205− matches[0].string_match.string.as_str(),
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− );
47+ let matches = sort_matches("ele", &completions, SnippetSortOrder::default(), cx).await;
48+ assert_eq!(matches[0], "element_type");
49+ assert_eq!(matches[1], "ElementType");
21450
21551 // Case 2: "eleme"
216− let query: Option<&str> = Some("eleme");
217− let mut matches: Vec<SortableMatch<'_>> = vec![
218− SortableMatch {
219− string_match: StringMatch {
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− },
52+ let completions = vec![
53+ CompletionBuilder::constant("ElementType", "7fffffff"),
54+ CompletionBuilder::variable("element_type", "7ffffffe"),
55+ CompletionBuilder::constant("REPLACEMENT_CHARACTER", "80000000"),
25456 ];
255− CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default());
256− assert_eq!(
257− matches[0].string_match.string.as_str(),
258− "element_type",
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− );
57+ let matches = sort_matches("eleme", &completions, SnippetSortOrder::default(), cx).await;
58+ assert_eq!(matches[0], "element_type");
59+ assert_eq!(matches[1], "ElementType");
60+}
26661
267− // Case 3: "Elem"
268− let query: Option<&str> = Some("Elem");
269− let mut matches: Vec<SortableMatch<'_>> = vec![
270− SortableMatch {
271− string_match: StringMatch {
272− candidate_id: 0,
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− },
62+#[gpui::test]
63+async fn test_sort_matches_capitalization(cx: &mut TestAppContext) {
64+ // Case 1: "Elem"
65+ let completions = vec![
66+ CompletionBuilder::constant("ElementType", "7fffffff"),
67+ CompletionBuilder::variable("element_type", "7ffffffe"),
29468 ];
295− CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default());
296− assert_eq!(
297− matches[0].string_match.string.as_str(),
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− );
69+ let matches = sort_matches("Elem", &completions, SnippetSortOrder::default(), cx).await;
70+ assert_eq!(matches[0], "ElementType");
71+ assert_eq!(matches[1], "element_type");
30672 }
30773
30874 #[gpui::test]
309−fn test_sort_matches_for_unreachable(_cx: &mut TestAppContext) {
75+async fn test_sort_matches_for_unreachable(cx: &mut TestAppContext) {
31076 // Case 1: "unre"
311− let query: Option<&str> = Some("unre");
312− let mut matches: Vec<SortableMatch<'_>> = vec![
313− SortableMatch {
314− string_match: StringMatch {
315− candidate_id: 0,
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− },
77+ let completions = vec![
78+ CompletionBuilder::function("unreachable", "80000000"),
79+ CompletionBuilder::function("unreachable!(…)", "7fffffff"),
80+ CompletionBuilder::function("unchecked_rem", "80000000"),
81+ CompletionBuilder::function("unreachable_unchecked", "80000000"),
36182 ];
362− CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default());
363− assert_eq!(
364− matches[0].string_match.string.as_str(),
365− "unreachable!(…)",
366− "Match order not expected"
367− );
83+ let matches = sort_matches("unre", &completions, SnippetSortOrder::default(), cx).await;
84+ assert_eq!(matches[0], "unreachable!(…)");
36885
36986 // Case 2: "unrea"
370− let query: Option<&str> = Some("unrea");
371− let mut matches: Vec<SortableMatch<'_>> = vec![
372− SortableMatch {
373− string_match: StringMatch {
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− },
87+ let completions = vec![
88+ CompletionBuilder::function("unreachable", "80000000"),
89+ CompletionBuilder::function("unreachable!(…)", "7fffffff"),
90+ CompletionBuilder::function("unreachable_unchecked", "80000000"),
40891 ];
409− CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default());
410− assert_eq!(
411− matches[0].string_match.string.as_str(),
412− "unreachable!(…)",
413− "Match order not expected"
414− );
92+ let matches = sort_matches("unrea", &completions, SnippetSortOrder::default(), cx).await;
93+ assert_eq!(matches[0], "unreachable!(…)");
41594
41695 // Case 3: "unreach"
417− let query: Option<&str> = Some("unreach");
418− let mut matches: Vec<SortableMatch<'_>> = vec![
419− SortableMatch {
420− string_match: StringMatch {
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− },
96+ let completions = vec![
97+ CompletionBuilder::function("unreachable", "80000000"),
98+ CompletionBuilder::function("unreachable!(…)", "7fffffff"),
99+ CompletionBuilder::function("unreachable_unchecked", "80000000"),
455100 ];
456− CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default());
457− assert_eq!(
458− matches[0].string_match.string.as_str(),
459− "unreachable!(…)",
460− "Match order not expected"
461− );
101+ let matches = sort_matches("unreach", &completions, SnippetSortOrder::default(), cx).await;
102+ assert_eq!(matches[0], "unreachable!(…)");
462103
463104 // Case 4: "unreachabl"
464− let query: Option<&str> = Some("unreachable");
465− let mut matches: Vec<SortableMatch<'_>> = vec![
466− SortableMatch {
467− string_match: StringMatch {
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− },
105+ let completions = vec![
106+ CompletionBuilder::function("unreachable", "80000000"),
107+ CompletionBuilder::function("unreachable!(…)", "7fffffff"),
108+ CompletionBuilder::function("unreachable_unchecked", "80000000"),
502109 ];
503− CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default());
504− assert_eq!(
505− matches[0].string_match.string.as_str(),
506− "unreachable!(…)",
507− "Match order not expected"
508− );
110+ let matches = sort_matches("unreachable", &completions, SnippetSortOrder::default(), cx).await;
111+ assert_eq!(matches[0], "unreachable!(…)");
509112
510113 // Case 5: "unreachable"
511− let query: Option<&str> = Some("unreachable");
512− let mut matches: Vec<SortableMatch<'_>> = vec![
513− SortableMatch {
514− string_match: StringMatch {
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− },
114+ let completions = vec![
115+ CompletionBuilder::function("unreachable", "80000000"),
116+ CompletionBuilder::function("unreachable!(…)", "7fffffff"),
117+ CompletionBuilder::function("unreachable_unchecked", "80000000"),
549118 ];
550− CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default());
551− assert_eq!(
552− matches[0].string_match.string.as_str(),
553− "unreachable!(…)",
554− "LSP should take over even when fuzzy perfect matches"
555− );
119+ let matches = sort_matches("unreachable", &completions, SnippetSortOrder::default(), cx).await;
120+ assert_eq!(matches[0], "unreachable!(…)");
556121 }
557122
558123 #[gpui::test]
559−fn test_sort_matches_variable_and_constants_over_function(_cx: &mut TestAppContext) {
124+async fn test_sort_matches_variable_and_constants_over_function(cx: &mut TestAppContext) {
560125 // Case 1: "var" as variable
561− let query: Option<&str> = Some("var");
562− let mut matches: Vec<SortableMatch<'_>> = vec![
563− SortableMatch {
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− },
126+ let completions = vec![
127+ CompletionBuilder::function("var", "7fffffff"),
128+ CompletionBuilder::variable("var", "7fffffff"),
587129 ];
588− CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default());
589− assert_eq!(
590− matches[0].string_match.candidate_id, 1,
591− "Match order not expected"
592− );
593− assert_eq!(
594− matches[1].string_match.candidate_id, 0,
595− "Match order not expected"
596− );
130+ let matches = sort_matches("var", &completions, SnippetSortOrder::default(), cx).await;
131+ assert_eq!(matches[0], "var");
132+ assert_eq!(matches[1], "var");
597133
598− // Case 2: "var" as constant
599− let query: Option<&str> = Some("var");
600− let mut matches: Vec<SortableMatch<'_>> = vec![
601− SortableMatch {
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− },
134+ // Case 2: "var" as constant
135+ let completions = vec![
136+ CompletionBuilder::function("var", "7fffffff"),
137+ CompletionBuilder::constant("var", "7fffffff"),
625138 ];
626− CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default());
627− assert_eq!(
628− matches[0].string_match.candidate_id, 1,
629− "Match order not expected"
630− );
631− assert_eq!(
632− matches[1].string_match.candidate_id, 0,
633− "Match order not expected"
634− );
139+ let matches = sort_matches("var", &completions, SnippetSortOrder::default(), cx).await;
140+ assert_eq!(matches[0], "var");
141+ assert_eq!(matches[1], "var");
635142 }
636143
637144 #[gpui::test]
638−fn test_sort_matches_for_jsx_event_handler(_cx: &mut TestAppContext) {
145+async fn test_sort_matches_for_jsx_event_handler(cx: &mut TestAppContext) {
639146 // Case 1: "on"
640− let query: Option<&str> = Some("on");
641− let mut matches: Vec<SortableMatch<'_>> = vec![
642− SortableMatch {
643− string_match: StringMatch {
644− candidate_id: 0,
645− score: 0.3333333333333333,
646− positions: vec![],
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− },
147+ let completions = vec![
148+ CompletionBuilder::function("onCut?", "12"),
149+ CompletionBuilder::function("onPlay?", "12"),
150+ CompletionBuilder::function("color?", "12"),
151+ CompletionBuilder::function("defaultValue?", "12"),
152+ CompletionBuilder::function("style?", "12"),
153+ CompletionBuilder::function("className?", "12"),
714154 ];
715− CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default());
716− assert_eq!(
717− matches[0].string_match.string, "onCut?",
718− "Match order not expected"
719− );
720− assert_eq!(
721− matches[1].string_match.string, "onPlay?",
722− "Match order not expected"
723− );
155+ let matches = sort_matches("on", &completions, SnippetSortOrder::default(), cx).await;
156+ assert_eq!(matches[0], "onCut?");
157+ assert_eq!(matches[1], "onPlay?");
724158
725159 // Case 2: "ona"
726− let query: Option<&str> = Some("ona");
727− let mut matches: Vec<SortableMatch<'_>> = vec![
728− SortableMatch {
729− string_match: StringMatch {
730− candidate_id: 0,
731− score: 0.375,
732− positions: vec![],
733− string: "onAbort?".to_string(),
734− },
735− is_snippet: false,
736− sort_text: Some("12"),
737− sort_kind: 3,
738− sort_label: "onAbort?",
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− },
160+ let completions = vec![
161+ CompletionBuilder::function("onAbort?", "12"),
162+ CompletionBuilder::function("onAuxClick?", "12"),
163+ CompletionBuilder::function("onPlay?", "12"),
164+ CompletionBuilder::function("onLoad?", "12"),
165+ CompletionBuilder::function("onDrag?", "12"),
166+ CompletionBuilder::function("onPause?", "12"),
167+ CompletionBuilder::function("onPaste?", "12"),
168+ CompletionBuilder::function("onAnimationEnd?", "12"),
169+ CompletionBuilder::function("onAbortCapture?", "12"),
170+ CompletionBuilder::function("onChange?", "12"),
171+ CompletionBuilder::function("onWaiting?", "12"),
172+ CompletionBuilder::function("onCanPlay?", "12"),
1064173 ];
1065− CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default());
1066− assert_eq!(
1067− matches
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− );
174+ let matches = sort_matches("ona", &completions, SnippetSortOrder::default(), cx).await;
175+ assert_eq!(matches[0], "onAbort?");
176+ assert_eq!(matches[1], "onAuxClick?");
1087177 }
1088178
1089179 #[gpui::test]
1090−fn test_sort_matches_for_snippets(_cx: &mut TestAppContext) {
180+async fn test_sort_matches_for_snippets(cx: &mut TestAppContext) {
1091181 // Case 1: "prin"
1092− let query: Option<&str> = Some("prin");
1093− let mut matches: Vec<SortableMatch<'_>> = vec![
1094− SortableMatch {
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− },
182+ let completions = vec![
183+ CompletionBuilder::constant("println", "80000000"),
184+ CompletionBuilder::snippet("println!(…)", "80000000"),
1118185 ];
1119− CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::Top);
1120− assert_eq!(
1121− matches[0].string_match.string.as_str(),
1122− "println!(…)",
1123− "Match order not expected"
1124− );
186+ let matches = sort_matches("prin", &completions, SnippetSortOrder::Top, cx).await;
187+ assert_eq!(matches[0], "println!(…)");
1125188 }
1126189
1127190 #[gpui::test]
1128−fn test_sort_matches_for_exact_match(_cx: &mut TestAppContext) {
191+async fn test_sort_matches_for_exact_match(cx: &mut TestAppContext) {
1129192 // Case 1: "set_text"
1130− let query: Option<&str> = Some("set_text");
1131− let mut matches: Vec<SortableMatch<'_>> = vec![
1132− SortableMatch {
1133− string_match: StringMatch {
1134− candidate_id: 0,
1135− score: 1.0,
1136− positions: vec![],
1137− string: "set_text".to_string(),
1138− },
1139− is_snippet: false,
1140− sort_text: Some("7fffffff"),
1141− sort_kind: 3,
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− },
193+ let completions = vec![
194+ CompletionBuilder::function("set_text", "7fffffff"),
195+ CompletionBuilder::function("set_placeholder_text", "7fffffff"),
196+ CompletionBuilder::function("set_text_style_refinement", "7fffffff"),
197+ CompletionBuilder::function("set_context_menu_options", "7fffffff"),
198+ CompletionBuilder::function("select_to_next_word_end", "7fffffff"),
199+ CompletionBuilder::function("select_to_next_subword_end", "7fffffff"),
200+ CompletionBuilder::function("set_custom_context_menu", "7fffffff"),
201+ CompletionBuilder::function("select_to_end_of_excerpt", "7fffffff"),
202+ CompletionBuilder::function("select_to_start_of_excerpt", "7fffffff"),
203+ CompletionBuilder::function("select_to_start_of_next_excerpt", "7fffffff"),
204+ CompletionBuilder::function("select_to_end_of_previous_excerpt", "7fffffff"),
1264205 ];
1265− CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::Top);
1266− assert_eq!(
1267− matches
1268− .iter()
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− );
206+ let matches = sort_matches("set_text", &completions, SnippetSortOrder::Top, cx).await;
207+ assert_eq!(matches[0], "set_text");
208+ assert_eq!(matches[1], "set_text_style_refinement");
209+ assert_eq!(matches[2], "set_context_menu_options");
1285210 }
1286211
1287212 #[gpui::test]
1288−fn test_sort_matches_for_prefix_matches(_cx: &mut TestAppContext) {
213+async fn test_sort_matches_for_prefix_matches(cx: &mut TestAppContext) {
1289214 // Case 1: "set"
1290− let query: Option<&str> = Some("set");
1291− let mut matches: Vec<SortableMatch<'_>> = vec![
1292− SortableMatch {
1293− string_match: StringMatch {
1294− candidate_id: 0,
1295− score: 0.12631578947368421,
1296− positions: vec![],
1297− string: "select_to_beginning".to_string(),
1298− },
1299− is_snippet: false,
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− },
215+ let completions = vec![
216+ CompletionBuilder::function("select_to_beginning", "7fffffff"),
217+ CompletionBuilder::function("set_collapse_matches", "7fffffff"),
218+ CompletionBuilder::function("set_autoindent", "7fffffff"),
219+ CompletionBuilder::function("set_all_diagnostics_active", "7fffffff"),
220+ CompletionBuilder::function("select_to_end_of_line", "7fffffff"),
221+ CompletionBuilder::function("select_all", "7fffffff"),
222+ CompletionBuilder::function("select_line", "7fffffff"),
223+ CompletionBuilder::function("select_left", "7fffffff"),
224+ CompletionBuilder::function("select_down", "7fffffff"),
1400225 ];
1401− CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::Top);
1402− assert_eq!(
1403− matches
1404− .iter()
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− );
226+ let matches = sort_matches("set", &completions, SnippetSortOrder::Top, cx).await;
227+ assert_eq!(matches[0], "set_autoindent");
228+ assert_eq!(matches[1], "set_collapse_matches");
229+ assert_eq!(matches[2], "set_all_diagnostics_active");
1419230 }
1420231
1421232 #[gpui::test]
1422−fn test_sort_matches_for_await(_cx: &mut TestAppContext) {
233+async fn test_sort_matches_for_await(cx: &mut TestAppContext) {
1423234 // Case 1: "awa"
1424− let query: Option<&str> = Some("awa");
1425− let mut matches: Vec<SortableMatch<'_>> = vec![
1426− SortableMatch {
1427− string_match: StringMatch {
1428− candidate_id: 0,
1429− score: 0.6000000000000001,
1430− positions: vec![],
1431− string: "await".to_string(),
1432− },
1433− is_snippet: false,
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− },
235+ let completions = vec![
236+ CompletionBuilder::keyword("await", "7fffffff"),
237+ CompletionBuilder::function("await.ne", "80000010"),
238+ CompletionBuilder::function("await.eq", "80000010"),
239+ CompletionBuilder::function("await.or", "7ffffff8"),
240+ CompletionBuilder::function("await.zip", "80000006"),
241+ CompletionBuilder::function("await.xor", "7ffffff8"),
242+ CompletionBuilder::function("await.and", "80000006"),
243+ CompletionBuilder::function("await.map", "80000006"),
244+ CompletionBuilder::function("await.take", "7ffffff8"),
1534245 ];
1535− CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::Top);
1536− assert_eq!(
1537− matches
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− );
246+ let matches = sort_matches("awa", &completions, SnippetSortOrder::Top, cx).await;
247+ assert_eq!(matches[0], "await");
248+
1553249 // Case 2: "await"
1554− let query: Option<&str> = Some("await");
1555− let mut matches: Vec<SortableMatch<'_>> = vec![
1556− SortableMatch {
1557− string_match: StringMatch {
1558− candidate_id: 0,
1559− score: 1.0,
1560− positions: vec![],
1561− string: "await".to_string(),
1562− },
1563− is_snippet: false,
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− },
250+ let completions = vec![
251+ CompletionBuilder::keyword("await", "7fffffff"),
252+ CompletionBuilder::function("await.ne", "80000010"),
253+ CompletionBuilder::function("await.eq", "80000010"),
254+ CompletionBuilder::function("await.or", "7ffffff8"),
255+ CompletionBuilder::function("await.zip", "80000006"),
256+ CompletionBuilder::function("await.xor", "7ffffff8"),
257+ CompletionBuilder::function("await.and", "80000006"),
258+ CompletionBuilder::function("await.map", "80000006"),
259+ CompletionBuilder::function("await.take", "7ffffff8"),
1664260 ];
1665− CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::Top);
1666− assert_eq!(
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− );
261+ let matches = sort_matches("await", &completions, SnippetSortOrder::Top, cx).await;
262+ assert_eq!(matches[0], "await");
1683263 }
1684264
1685265 #[gpui::test]
1686−fn test_sort_matches_for_python_init(_cx: &mut TestAppContext) {
266+async fn test_sort_matches_for_python_init(cx: &mut TestAppContext) {
1687267 // Case 1: "__in"
1688− let query: Option<&str> = Some("__in");
1689− let mut matches: Vec<SortableMatch<'_>> = vec![
1690− SortableMatch {
1691− string_match: StringMatch {
1692− candidate_id: 211,
1693− score: 0.5,
1694− positions: vec![],
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− },
268+ let completions = vec![
269+ CompletionBuilder::function("__init__", "05.0003.__init__"),
270+ CompletionBuilder::function("__init__", "05.0003"),
271+ CompletionBuilder::function("__instancecheck__", "05.0005.__instancecheck__"),
272+ CompletionBuilder::function("__init_subclass__", "05.0004.__init_subclass__"),
273+ CompletionBuilder::function("__instancecheck__", "05.0005"),
274+ CompletionBuilder::function("__init_subclass__", "05.0004"),
1762275 ];
1763− CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::Top);
1764− assert_eq!(
1765− matches
1766− .iter()
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− );
276+ let matches = sort_matches("__in", &completions, SnippetSortOrder::Top, cx).await;
277+ assert_eq!(matches[0], "__init__");
278+ assert_eq!(matches[1], "__init__");
279+
1778280 // Case 2: "__ini"
1779− let query: Option<&str> = Some("__ini");
1780− let mut matches: Vec<SortableMatch<'_>> = vec![
1781− SortableMatch {
1782− string_match: StringMatch {
1783− candidate_id: 9,
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− },
281+ let completions = vec![
282+ CompletionBuilder::function("__init__", "05.0004.__init__"),
283+ CompletionBuilder::function("__init__", "05.0004"),
284+ CompletionBuilder::function("__init_subclass__", "05.0003.__init_subclass__"),
285+ CompletionBuilder::function("__init_subclass__", "05.0003"),
1829286 ];
1830− CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::Top);
1831− assert_eq!(
1832− matches
1833− .iter()
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− );
287+ let matches = sort_matches("__ini", &completions, SnippetSortOrder::Top, cx).await;
288+ assert_eq!(matches[0], "__init__");
289+ assert_eq!(matches[1], "__init__");
290+
1843291 // Case 3: "__init"
1844− let query: Option<&str> = Some("__init");
1845− let mut matches: Vec<SortableMatch<'_>> = vec![
1846− SortableMatch {
1847− string_match: StringMatch {
1848− candidate_id: 7,
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− },
292+ let completions = vec![
293+ CompletionBuilder::function("__init__", "05.0000.__init__"),
294+ CompletionBuilder::function("__init__", "05.0000"),
295+ CompletionBuilder::function("__init_subclass__", "05.0001.__init_subclass__"),
296+ CompletionBuilder::function("__init_subclass__", "05.0001"),
1894297 ];
1895− CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::Top);
1896− assert_eq!(
1897− matches
1898− .iter()
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− );
298+ let matches = sort_matches("__init", &completions, SnippetSortOrder::Top, cx).await;
299+ assert_eq!(matches[0], "__init__");
300+ assert_eq!(matches[1], "__init__");
301+
1908302 // Case 4: "__init_"
1909− let query: Option<&str> = Some("__init_");
1910− let mut matches: Vec<SortableMatch<'_>> = vec![
1911− SortableMatch {
1912− string_match: StringMatch {
1913− candidate_id: 4,
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− },
303+ let completions = vec![
304+ CompletionBuilder::function("__init__", "11.9999.__init__"),
305+ CompletionBuilder::function("__init__", "11.9999"),
306+ CompletionBuilder::function("__init_subclass__", "05.0000.__init_subclass__"),
307+ CompletionBuilder::function("__init_subclass__", "05.0000"),
1959308 ];
1960− CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::Top);
1961− assert_eq!(
1962− matches
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− );
309+ let matches = sort_matches("__init_", &completions, SnippetSortOrder::Top, cx).await;
310+ assert_eq!(matches[0], "__init__");
311+ assert_eq!(matches[1], "__init__");
1973312 }
1974313
1975314 #[gpui::test]
1976−fn test_sort_matches_for_rust_into(_cx: &mut TestAppContext) {
315+async fn test_sort_matches_for_rust_into(cx: &mut TestAppContext) {
1977316 // Case 1: "int"
1978− let query: Option<&str> = Some("int");
1979− let mut matches: Vec<SortableMatch<'_>> = vec![
1980− SortableMatch {
1981− string_match: StringMatch {
1982− candidate_id: 67,
1983− score: 0.75,
1984− positions: vec![],
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− },
317+ let completions = vec![
318+ CompletionBuilder::function("into", "80000004"),
319+ CompletionBuilder::function("try_into", "80000004"),
320+ CompletionBuilder::snippet("println", "80000004"),
321+ CompletionBuilder::function("clone_into", "80000004"),
322+ CompletionBuilder::function("into_searcher", "80000000"),
323+ CompletionBuilder::snippet("eprintln", "80000004"),
2052324 ];
2053− CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default());
2054− assert_eq!(
2055− matches[0].string_match.string.as_str(),
2056− "into",
2057− "Match order not expected"
2058− );
325+ let matches = sort_matches("int", &completions, SnippetSortOrder::default(), cx).await;
326+ assert_eq!(matches[0], "into");
327+
2059328 // Case 2: "into"
2060− let query: Option<&str> = Some("into");
2061− let mut matches: Vec<SortableMatch<'_>> = vec![
2062− SortableMatch {
2063− string_match: StringMatch {
2064− candidate_id: 65,
2065− score: 1.0,
2066− positions: vec![],
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− },
329+ let completions = vec![
330+ CompletionBuilder::function("into", "80000004"),
331+ CompletionBuilder::function("try_into", "80000004"),
332+ CompletionBuilder::function("clone_into", "80000004"),
333+ CompletionBuilder::function("into_searcher", "80000000"),
334+ CompletionBuilder::function("split_terminator", "7fffffff"),
335+ CompletionBuilder::function("rsplit_terminator", "7fffffff"),
2134336 ];
2135− CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default());
2136− assert_eq!(
2137− matches[0].string_match.string.as_str(),
2138− "into",
2139− "Match order not expected"
2140− );
337+ let matches = sort_matches("into", &completions, SnippetSortOrder::default(), cx).await;
338+ assert_eq!(matches[0], "into");
2141339 }
2142340
2143341 #[gpui::test]
2144−fn test_sort_matches_for_variable_over_function(_cx: &mut TestAppContext) {
342+async fn test_sort_matches_for_variable_over_function(cx: &mut TestAppContext) {
2145343 // Case 1: "serial"
2146− let query: Option<&str> = Some("serial");
2147− let mut matches: Vec<SortableMatch<'_>> = vec![
2148− SortableMatch {
2149− string_match: StringMatch {
2150− candidate_id: 33,
2151− score: 0.6666666666666666,
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− },
344+ let completions = vec![
345+ CompletionBuilder::function("serialize", "80000000"),
346+ CompletionBuilder::function("serialize", "80000000"),
347+ CompletionBuilder::variable("serialization_key", "7ffffffe"),
348+ CompletionBuilder::function("serialize_version", "80000000"),
349+ CompletionBuilder::function("deserialize", "80000000"),
2208350 ];
2209− CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default());
2210− assert_eq!(
2211− matches
2212− .iter()
2213− .map(|m| m.string_match.string.as_str())
2214− .collect::<Vec<&str>>(),
2215− vec![
2216− "serialization_key",
2217− "serialize",
2218− "serialize",
2219− "serialize_version",
2220− "deserialize"
2221− ]
2222− );
351+ let matches = sort_matches("serial", &completions, SnippetSortOrder::default(), cx).await;
352+ assert_eq!(matches[0], "serialization_key");
353+ assert_eq!(matches[1], "serialize");
354+ assert_eq!(matches[2], "serialize");
355+ assert_eq!(matches[3], "serialize_version");
356+ assert_eq!(matches[4], "deserialize");
2223357 }
2224358
2225359 #[gpui::test]
2226−fn test_sort_matches_for_local_methods_over_library(_cx: &mut TestAppContext) {
360+async fn test_sort_matches_for_local_methods_over_library(cx: &mut TestAppContext) {
2227361 // Case 1: "setis"
2228− let query: Option<&str> = Some("setis");
2229− let mut matches: Vec<SortableMatch<'_>> = vec![
2230− SortableMatch {
2231− string_match: StringMatch {
2232− candidate_id: 1200,
2233− score: 0.5555555555555556,
2234− positions: vec![],
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− },
362+ let completions = vec![
363+ CompletionBuilder::variable("setISODay", "16"),
364+ CompletionBuilder::variable("setISOWeek", "16"),
365+ CompletionBuilder::variable("setISOWeekYear", "16"),
366+ CompletionBuilder::function("setISOWeekYear", "16"),
367+ CompletionBuilder::variable("setIsRefreshing", "11"),
368+ CompletionBuilder::function("setFips", "16"),
2302369 ];
2303− CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default());
2304− assert_eq!(
2305− matches
2306− .iter()
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− );
370+ let matches = sort_matches("setis", &completions, SnippetSortOrder::default(), cx).await;
371+ assert_eq!(matches[0], "setIsRefreshing");
372+ assert_eq!(matches[1], "setISODay");
373+ assert_eq!(matches[2], "setISOWeek");
2318374 }
2319375
2320376 #[gpui::test]
2321−fn test_sort_matches_for_priotize_not_exact_match(_cx: &mut TestAppContext) {
377+async fn test_sort_matches_for_prioritize_not_exact_match(cx: &mut TestAppContext) {
2322378 // Case 1: "item"
2323− let query: Option<&str> = Some("item");
2324− let mut matches: Vec<SortableMatch<'_>> = vec![
2325− SortableMatch {
2326− string_match: StringMatch {
2327− candidate_id: 1115,
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− },
379+ let completions = vec![
380+ CompletionBuilder::function("Item", "16"),
381+ CompletionBuilder::variable("Item", "16"),
382+ CompletionBuilder::variable("items", "11"),
383+ CompletionBuilder::function("ItemText", "16"),
2373384 ];
2374− CompletionsMenu::sort_matches(&mut matches, query, SnippetSortOrder::default());
2375− assert_eq!(
2376− matches
2377− .iter()
2378− .map(|m| m.string_match.string.as_str())
2379− .collect::<Vec<&str>>(),
2380− vec!["items", "Item", "Item", "ItemText"]
2381− );
385+ let matches = sort_matches("item", &completions, SnippetSortOrder::default(), cx).await;
386+ assert_eq!(matches[0], "items");
387+ assert_eq!(matches[1], "Item");
388+ assert_eq!(matches[2], "Item");
389+ assert_eq!(matches[3], "ItemText");
390+}
391+
392+#[gpui::test]
393+async 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+
414+struct CompletionBuilder;
415+
416+impl 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+
466+async 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()
2382495 }
crates/editor/src/code_context_menus.rs+47−89
@@ -722,10 +722,9 @@ impl CompletionsMenu { · 721 unmodified lines
722722 let last_rendered_range = self.last_rendered_range.clone();
723723 let style = style.clone();
724724 let list = uniform_list(
725− cx.entity().clone(),
726725 "completions",
727726 self.entries.borrow().len(),
728− move |_editor, range, _window, cx| {
727+ cx.processor(move |_editor, range: Range<usize>, _window, cx| {
729728 last_rendered_range.borrow_mut().replace(range.clone());
730729 let start_ix = range.start;
731730 let completions_guard = completions.borrow_mut();
@@ -837,7 +836,7 @@ impl CompletionsMenu { · 105 unmodified lines
837836 )
838837 })
839838 .collect()
840− },
839+ }),
841840 )
842841 .occlude()
843842 .max_h(max_height_in_lines as f32 * window.line_height())
@@ -1045,59 +1044,19 @@ impl CompletionsMenu { · 201 unmodified lines
10451044 self.handle_selection_changed(provider.as_deref(), window, cx);
10461045 }
10471046
1048− fn sort_string_matches(
1047+ pub fn sort_string_matches(
10491048 matches: Vec<StringMatch>,
10501049 query: Option<&str>,
10511050 snippet_sort_order: SnippetSortOrder,
10521051 completions: &[Completion],
10531052 ) -> Vec<StringMatch> {
1054− let mut sortable_items: Vec<SortableMatch<'_>> = 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− }
1053+ let mut matches = matches;
10911054
1092− pub fn sort_matches(
1093− matches: &mut Vec<SortableMatch<'_>>,
1094− query: Option<&str>,
1095− snippet_sort_order: SnippetSortOrder,
1096− ) {
10971055 #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
10981056 enum MatchTier<'a> {
10991057 WordStartMatch {
1100− sort_mixed_case_prefix_length: Reverse<usize>,
1058+ sort_capitalize: Reverse<usize>,
1059+ sort_positions: Vec<usize>,
11011060 sort_snippet: Reverse<i32>,
11021061 sort_kind: usize,
11031062 sort_fuzzy_bracket: Reverse<usize>,
@@ -1115,10 +1074,7 @@ impl CompletionsMenu { · 11 unmodified lines
11151074
11161075 // In a fuzzy bracket, matches with a score of 1.0 are prioritized.
11171076 // The remaining matches are partitioned into two groups at 3/5 of the max_score.
1118− let max_score = matches
1119− .iter()
1120− .map(|mat| mat.string_match.score)
1121− .fold(0.0, f64::max);
1077+ let max_score = matches.iter().map(|mat| mat.score).fold(0.0, f64::max);
11221078 let fuzzy_bracket_threshold = max_score * (3.0 / 5.0);
11231079
11241080 let query_start_lower = query
@@ -1126,13 +1082,30 @@ impl CompletionsMenu { · 1 unmodified line
11261082 .and_then(|q| q.chars().next())
11271083 .and_then(|c| c.to_lowercase().next());
11281084
1129− matches.sort_unstable_by_key(|mat| {
1130− let score = mat.string_match.score;
1085+ matches.sort_unstable_by_key(|string_match| {
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;
11311104 let sort_score = Reverse(OrderedFloat(score));
11321105
11331106 let query_start_doesnt_match_split_words = query_start_lower
11341107 .map(|query_char| {
1135− !split_words(&mat.string_match.string).any(|word| {
1108+ !split_words(&string_match.string).any(|word| {
11361109 word.chars()
11371110 .next()
11381111 .and_then(|c| c.to_lowercase().next())
@@ -1150,41 +1123,34 @@ impl CompletionsMenu { · 11 unmodified lines
11501123 0
11511124 });
11521125 let sort_snippet = match snippet_sort_order {
1153− SnippetSortOrder::Top => Reverse(if mat.is_snippet { 1 } else { 0 }),
1154− SnippetSortOrder::Bottom => Reverse(if mat.is_snippet { 0 } else { 1 }),
1126+ SnippetSortOrder::Top => Reverse(if is_snippet { 1 } else { 0 }),
1127+ SnippetSortOrder::Bottom => Reverse(if is_snippet { 0 } else { 1 }),
11551128 SnippetSortOrder::Inline => Reverse(0),
11561129 };
1157− let sort_mixed_case_prefix_length = Reverse(
1130+ let sort_capitalize = Reverse(
11581131 query
11591132 .as_ref()
1160− .map(|q| {
1161− q.chars()
1162− .zip(mat.string_match.string.chars())
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− })
1133+ .and_then(|q| q.chars().next())
1134+ .zip(string_match.string.chars().next())
1135+ .map(|(q_char, s_char)| if q_char == s_char { 1 } else { 0 })
11751136 .unwrap_or(0),
11761137 );
1138+ let sort_positions = string_match.positions.clone();
1139+
11771140 MatchTier::WordStartMatch {
1178− sort_mixed_case_prefix_length,
1141+ sort_capitalize,
1142+ sort_positions,
11791143 sort_snippet,
1180− sort_kind: mat.sort_kind,
1144+ sort_kind,
11811145 sort_fuzzy_bracket,
1182− sort_text: mat.sort_text,
1146+ sort_text,
11831147 sort_score,
1184− sort_label: mat.sort_label,
1148+ sort_label,
11851149 }
11861150 }
11871151 });
1152+
1153+ matches
11881154 }
11891155
11901156 pub fn preserve_markdown_cache(&mut self, prev_menu: CompletionsMenu) {
@@ -1216,15 +1182,6 @@ impl CompletionsMenu { · 25 unmodified lines
12161182 }
12171183 }
12181184
1219−#[derive(Debug)]
1220−pub 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−
12281185 #[derive(Clone)]
12291186 pub struct AvailableCodeAction {
12301187 pub excerpt_id: ExcerptId,
@@ -1436,7 +1393,9 @@ impl CodeActionsMenu { · 205 unmodified lines
14361393
14371394 fn origin(&self) -> ContextMenuOrigin {
14381395 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+ }
14401399 Some(CodeActionSource::QuickActionBar) => ContextMenuOrigin::QuickActionBar,
14411400 None => ContextMenuOrigin::Cursor,
14421401 }
@@ -1452,10 +1411,9 @@ impl CodeActionsMenu { · 9 unmodified lines
14521411 let actions = self.actions.clone();
14531412 let selected_item = self.selected_item;
14541413 let list = uniform_list(
1455− cx.entity().clone(),
14561414 "code_actions_menu",
14571415 self.actions.len(),
1458− move |_this, range, _, cx| {
1416+ cx.processor(move |_this, range: Range<usize>, _, cx| {
14591417 actions
14601418 .iter()
14611419 .skip(range.start)
@@ -1518,7 +1476,7 @@ impl CodeActionsMenu { · 56 unmodified lines
15181476 )
15191477 })
15201478 .collect()
1521− },
1479+ }),
15221480 )
15231481 .occlude()
15241482 .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
464464 map
465465 }
466466
467− pub fn read(&self, wrap_snapshot: WrapSnapshot, edits: Patch<u32>) -> BlockMapReader {
467+ pub fn read(&self, wrap_snapshot: WrapSnapshot, edits: Patch<u32>) -> BlockMapReader<'_> {
468468 self.sync(&wrap_snapshot, edits);
469469 *self.wrap_snapshot.borrow_mut() = wrap_snapshot.clone();
470470 BlockMapReader {
@@ -479,7 +479,7 @@ impl BlockMap { · 8 unmodified lines
479479 }
480480 }
481481
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<'_> {
483483 self.sync(&wrap_snapshot, edits);
484484 *self.wrap_snapshot.borrow_mut() = wrap_snapshot;
485485 BlockMapWriter(self)
@@ -1327,7 +1327,7 @@ impl BlockSnapshot { · 841 unmodified lines
13271327 }
13281328 }
13291329
1330− pub(super) fn row_infos(&self, start_row: BlockRow) -> BlockRows {
1330+ pub(super) fn row_infos(&self, start_row: BlockRow) -> BlockRows<'_> {
13311331 let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&());
13321332 cursor.seek(&start_row, Bias::Right, &());
13331333 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
357357 &mut self,
358358 inlay_snapshot: InlaySnapshot,
359359 edits: Vec<InlayEdit>,
360− ) -> (FoldMapWriter, FoldSnapshot, Vec<FoldEdit>) {
360+ ) -> (FoldMapWriter<'_>, FoldSnapshot, Vec<FoldEdit>) {
361361 let (snapshot, edits) = self.read(inlay_snapshot, edits);
362362 (FoldMapWriter(self), snapshot, edits)
363363 }
@@ -730,7 +730,7 @@ impl FoldSnapshot { · 366 unmodified lines
730730 (line_end - line_start) as u32
731731 }
732732
733− pub fn row_infos(&self, start_row: u32) -> FoldRows {
733+ pub fn row_infos(&self, start_row: u32) -> FoldRows<'_> {
734734 if start_row > self.transforms.summary().output.lines.row {
735735 panic!("invalid display row {}", start_row);
736736 }
crates/editor/src/display_map/wrap_map.rs+1−1
@@ -726,7 +726,7 @@ impl WrapSnapshot { · 725 unmodified lines
726726 self.transforms.summary().output.longest_row
727727 }
728728
729− pub fn row_infos(&self, start_row: u32) -> WrapRows {
729+ pub fn row_infos(&self, start_row: u32) -> WrapRows<'_> {
730730 let mut transforms = self.transforms.cursor::<(WrapPoint, TabPoint)>(&());
731731 transforms.seek(&WrapPoint::new(start_row, 0), Bias::Left, &());
732732 let mut input_row = transforms.start().1.row();
crates/editor/src/editor.rs+676−436
@@ -209,7 +209,7 @@ use workspace::{ · 208 unmodified lines
209209 CollaboratorId, Item as WorkspaceItem, ItemId, ItemNavHistory, OpenInTerminal, OpenTerminal,
210210 RestoreOnStartupBehavior, SERIALIZATION_THROTTLE_TIME, SplitDirection, TabBarSettings, Toast,
211211 ViewId, Workspace, WorkspaceId, WorkspaceSettings,
212− item::{ItemHandle, PreviewTabsSettings},
212+ item::{ItemHandle, PreviewTabsSettings, SaveOptions},
213213 notifications::{DetachAndPromptErr, NotificationId, NotifyTaskExt},
214214 searchable::SearchEvent,
215215 };
@@ -241,7 +241,6 @@ pub(crate) const SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT: Duration = Duration: · 25 unmodified lines
241241
242242 pub(crate) const EDIT_PREDICTION_KEY_CONTEXT: &str = "edit_prediction";
243243 pub(crate) const EDIT_PREDICTION_CONFLICT_KEY_CONTEXT: &str = "edit_prediction_conflict";
244−pub(crate) const MIN_LINE_NUMBER_DIGITS: u32 = 4;
245244 pub(crate) const MINIMAP_FONT_SIZE: AbsoluteLength = AbsoluteLength::Pixels(px(2.));
246245
247246 pub type RenderDiffHunkControlsFn = Arc<
@@ -450,6 +449,7 @@ pub enum SelectPhase { · 202 unmodified lines
450449 BeginColumnar {
451450 position: DisplayPoint,
452451 reset: bool,
452+ mode: ColumnarMode,
453453 goal_column: u32,
454454 },
455455 Extend {
@@ -464,6 +464,12 @@ pub enum SelectPhase { · 8 unmodified lines
464464 End,
465465 }
466466
467+#[derive(Clone, Debug, PartialEq)]
468+pub enum ColumnarMode {
469+ FromMouse,
470+ FromSelection,
471+}
472+
467473 #[derive(Clone, Debug)]
468474 pub enum SelectMode {
469475 Character,
@@ -478,6 +484,7 @@ pub enum EditorMode { · 8 unmodified lines
478484 auto_width: bool,
479485 },
480486 AutoHeight {
487+ min_lines: usize,
481488 max_lines: usize,
482489 },
483490 Full {
@@ -700,7 +707,7 @@ impl EditorActionId { · 216 unmodified lines
700707 // type OverrideTextStyle = dyn Fn(&EditorStyle) -> Option<HighlightStyle>;
701708
702709 type BackgroundHighlight = (fn(&ThemeColors) -> Hsla, Arc<[Range<Anchor>]>);
703−type GutterHighlight = (fn(&App) -> Hsla, Arc<[Range<Anchor>]>);
710+type GutterHighlight = (fn(&App) -> Hsla, Vec<Range<Anchor>>);
704711
705712 #[derive(Default)]
706713 struct ScrollbarMarkerState {
@@ -902,7 +909,6 @@ struct InlineBlamePopoverState { · 195 unmodified lines
902909
903910 struct InlineBlamePopover {
904911 position: gpui::Point<Pixels>,
905− show_task: Option<Task<()>>,
906912 hide_task: Option<Task<()>>,
907913 popover_bounds: Option<Bounds<Pixels>>,
908914 popover_state: InlineBlamePopoverState,
@@ -915,17 +921,29 @@ enum SelectionDragState { · 6 unmodified lines
915921 ReadyToDrag {
916922 selection: Selection<Anchor>,
917923 click_position: gpui::Point<Pixels>,
924+ mouse_down_time: Instant,
918925 },
919926 /// State when the mouse is dragging the selection in the editor.
920927 Dragging {
921928 selection: Selection<Anchor>,
922929 drop_cursor: Selection<Anchor>,
930+ hide_drop_cursor: bool,
931+ },
932+}
933+
934+enum ColumnarSelectionState {
935+ FromMouse {
936+ selection_tail: Anchor,
937+ display_point: Option<DisplayPoint>,
938+ },
939+ FromSelection {
940+ selection_tail: Anchor,
923941 },
924942 }
925943
926944 /// Represents a breakpoint indicator that shows up when hovering over lines in the gutter that don't have
927945 /// a breakpoint on them.
928−#[derive(Clone, Copy, Debug)]
946+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
929947 struct PhantomBreakpointIndicator {
930948 display_row: DisplayRow,
931949 /// There's a small debounce between hovering over the line and showing the indicator.
@@ -933,6 +951,7 @@ struct PhantomBreakpointIndicator { · 1 unmodified line
933951 is_active: bool,
934952 collides_with_existing_breakpoint: bool,
935953 }
954+
936955 /// Zed's primary implementation of text input, allowing users to edit a [`MultiBuffer`].
937956 ///
938957 /// See the [module level documentation](self) for more information.
@@ -949,8 +968,7 @@ pub struct Editor { · 10 unmodified lines
949968 /// When inline assist editors are linked, they all render cursors because
950969 /// typing enters text into each of them, even the ones that aren't focused.
951970 pub(crate) show_cursor_when_unfocused: bool,
952− columnar_selection_tail: Option<Anchor>,
953− columnar_display_point: Option<DisplayPoint>,
971+ columnar_selection_state: Option<ColumnarSelectionState>,
954972 add_selections_state: Option<AddSelectionsState>,
955973 select_next_state: Option<SelectNextState>,
956974 select_prev_state: Option<SelectNextState>,
@@ -1007,6 +1025,7 @@ pub struct Editor { · 50 unmodified lines
10071025 mouse_context_menu: Option<MouseContextMenu>,
10081026 completion_tasks: Vec<(CompletionId, Task<()>)>,
10091027 inline_blame_popover: Option<InlineBlamePopover>,
1028+ inline_blame_popover_show_task: Option<Task<()>>,
10101029 signature_help_state: SignatureHelpState,
10111030 auto_signature_help: Option<bool>,
10121031 find_all_references_task_sources: Vec<Anchor>,
@@ -1054,8 +1073,9 @@ pub struct Editor { · 41 unmodified lines
10541073 style: Option<EditorStyle>,
10551074 text_style_refinement: Option<TextStyleRefinement>,
10561075 next_editor_action_id: EditorActionId,
1057− editor_actions:
1058− Rc<RefCell<BTreeMap<EditorActionId, Box<dyn Fn(&mut Window, &mut Context<Self>)>>>>,
1076+ editor_actions: Rc<
1077+ RefCell<BTreeMap<EditorActionId, Box<dyn Fn(&Editor, &mut Window, &mut Context<Self>)>>>,
1078+ >,
10591079 use_autoclose: bool,
10601080 use_auto_surround: bool,
10611081 auto_replace_emoji_shortcode: bool,
@@ -1087,6 +1107,7 @@ pub struct Editor { · 25 unmodified lines
10871107 tasks_update_task: Option<Task<()>>,
10881108 breakpoint_store: Option<Entity<BreakpointStore>>,
10891109 gutter_breakpoint_indicator: (Option<PhantomBreakpointIndicator>, Option<Task<()>>),
1110+ hovered_diff_hunk_row: Option<DisplayRow>,
10901111 pull_diagnostics_task: Task<()>,
10911112 in_project_search: bool,
10921113 previous_search_ranges: Option<Arc<[Range<Anchor>]>>,
@@ -1201,10 +1222,12 @@ struct SelectionHistoryEntry { · 108 unmodified lines
12011222 add_selections_state: Option<AddSelectionsState>,
12021223 }
12031224
1225+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
12041226 enum SelectionHistoryMode {
12051227 Normal,
12061228 Undoing,
12071229 Redoing,
1230+ Skipping,
12081231 }
12091232
12101233 #[derive(Clone, PartialEq, Eq, Hash)]
@@ -1219,10 +1242,55 @@ impl Default for SelectionHistoryMode { · 8 unmodified lines
12191242 }
12201243 }
12211244
1245+#[derive(Debug)]
1246+pub struct SelectionEffects {
1247+ nav_history: bool,
1248+ completions: bool,
1249+ scroll: Option<Autoscroll>,
1250+}
1251+
1252+impl Default for SelectionEffects {
1253+ fn default() -> Self {
1254+ Self {
1255+ nav_history: true,
1256+ completions: true,
1257+ scroll: Some(Autoscroll::fit()),
1258+ }
1259+ }
1260+}
1261+impl 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+
12221291 struct DeferredSelectionEffectsState {
12231292 changed: bool,
1224− should_update_completions: bool,
1225− autoscroll: Option<Autoscroll>,
1293+ effects: SelectionEffects,
12261294 old_cursor_position: Anchor,
12271295 history_entry: SelectionHistoryEntry,
12281296 }
@@ -1238,11 +1306,19 @@ struct SelectionHistory { · 9 unmodified lines
12381306 }
12391307
12401308 impl SelectionHistory {
1309+ #[track_caller]
12411310 fn insert_transaction(
12421311 &mut self,
12431312 transaction_id: TransactionId,
12441313 selections: Arc<[Selection<Anchor>]>,
12451314 ) {
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+ }
12461322 self.selections_by_transaction
12471323 .insert(transaction_id, (selections, None));
12481324 }
@@ -1272,6 +1348,7 @@ impl SelectionHistory { · 23 unmodified lines
12721348 }
12731349 SelectionHistoryMode::Undoing => self.push_redo(entry),
12741350 SelectionHistoryMode::Redoing => self.push_undo(entry),
1351+ SelectionHistoryMode::Skipping => {}
12751352 }
12761353 }
12771354 }
@@ -1486,7 +1563,7 @@ impl InlayHintRefreshReason { · 208 unmodified lines
14861563 }
14871564
14881565 pub enum FormatTarget {
1489− Buffers,
1566+ Buffers(HashSet<Entity<Buffer>>),
14901567 Ranges(Vec<Range<MultiBufferPoint>>),
14911568 }
14921569
@@ -1551,11 +1628,19 @@ impl Editor { · 58 unmodified lines
15511628 )
15521629 }
15531630
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 {
15551637 let buffer = cx.new(|cx| Buffer::local("", cx));
15561638 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
15571639 Self::new(
1558− EditorMode::AutoHeight { max_lines },
1640+ EditorMode::AutoHeight {
1641+ min_lines,
1642+ max_lines,
1643+ },
15591644 buffer,
15601645 None,
15611646 window,
@@ -1817,8 +1902,7 @@ impl Editor { · 255 unmodified lines
18171902 display_map: display_map.clone(),
18181903 selections,
18191904 scroll_manager: ScrollManager::new(cx),
1820− columnar_selection_tail: None,
1821− columnar_display_point: None,
1905+ columnar_selection_state: None,
18221906 add_selections_state: None,
18231907 select_next_state: None,
18241908 select_prev_state: None,
@@ -1872,6 +1956,7 @@ impl Editor { · 47 unmodified lines
18721956 mouse_context_menu: None,
18731957 completion_tasks: Vec::new(),
18741958 inline_blame_popover: None,
1959+ inline_blame_popover_show_task: None,
18751960 signature_help_state: SignatureHelpState::default(),
18761961 auto_signature_help: None,
18771962 find_all_references_task_sources: Vec::new(),
@@ -1949,6 +2034,7 @@ impl Editor { · 71 unmodified lines
19492034
19502035 breakpoint_store,
19512036 gutter_breakpoint_indicator: (None, None),
2037+ hovered_diff_hunk_row: None,
19522038 _subscriptions: vec![
19532039 cx.observe(&buffer, Self::on_buffer_changed),
19542040 cx.subscribe_in(&buffer, window, Self::on_buffer_event),
@@ -2082,7 +2168,11 @@ impl Editor { · 127 unmodified lines
20822168 }
20832169 }
20842170
2171+ // skip adding the initial selection to selection history
2172+ editor.selection_history.mode = SelectionHistoryMode::Skipping;
20852173 editor.end_selection(window, cx);
2174+ editor.selection_history.mode = SelectionHistoryMode::Normal;
2175+
20862176 editor.scroll_manager.show_scrollbars(window, cx);
20872177 jsx_tag_auto_close::refresh_enabled_in_any_buffer(&mut editor, &buffer, cx);
20882178
@@ -2682,7 +2772,7 @@ impl Editor { · 593 unmodified lines
26822772 &mut self,
26832773 local: bool,
26842774 old_cursor_position: &Anchor,
2685− should_update_completions: bool,
2775+ effects: SelectionEffects,
26862776 window: &mut Window,
26872777 cx: &mut Context<Self>,
26882778 ) {
@@ -2740,12 +2830,14 @@ impl Editor { · 51 unmodified lines
27402830 let new_cursor_position = newest_selection.head();
27412831 let selection_start = newest_selection.start;
27422832
2743− self.push_to_nav_history(
2744− *old_cursor_position,
2745− Some(new_cursor_position.to_point(buffer)),
2746− false,
2747− cx,
2748− );
2833+ if effects.nav_history {
2834+ self.push_to_nav_history(
2835+ *old_cursor_position,
2836+ Some(new_cursor_position.to_point(buffer)),
2837+ false,
2838+ cx,
2839+ );
2840+ }
27492841
27502842 if local {
27512843 if let Some(buffer_id) = new_cursor_position.buffer_id {
@@ -2776,7 +2868,7 @@ impl Editor { · 24 unmodified lines
27762868 let completion_position = completion_menu.map(|menu| menu.initial_position);
27772869 drop(context_menu);
27782870
2779− if should_update_completions {
2871+ if effects.completions {
27802872 if let Some(completion_position) = completion_position {
27812873 let start_offset = selection_start.to_offset(buffer);
27822874 let position_matches = start_offset == completion_position.to_offset(buffer);
@@ -2785,7 +2877,8 @@ impl Editor { · 2 unmodified lines
27852877 buffer.char_kind_before(start_offset, true) == Some(CharKind::Word)
27862878 } else {
27872879 // Snippet choices can be shown even when the cursor is in whitespace.
2788− // Dismissing the menu when actions like backspace
2880+ // Dismissing the menu with actions like backspace is handled by
2881+ // invalidation regions.
27892882 true
27902883 }
27912884 } else {
@@ -2851,22 +2944,22 @@ impl Editor { · 59 unmodified lines
28512944 let background_executor = cx.background_executor().clone();
28522945 let editor_id = cx.entity().entity_id().as_u64() as ItemId;
28532946 self.serialize_selections = cx.background_spawn(async move {
2854− background_executor.timer(SERIALIZATION_THROTTLE_TIME).await;
2855− let db_selections = selections
2856− .iter()
2857− .map(|selection| {
2858− (
2859− selection.start.to_offset(&snapshot),
2860− selection.end.to_offset(&snapshot),
2861− )
2862− })
2863− .collect();
2947+ background_executor.timer(SERIALIZATION_THROTTLE_TIME).await;
2948+ let db_selections = selections
2949+ .iter()
2950+ .map(|selection| {
2951+ (
2952+ selection.start.to_offset(&snapshot),
2953+ selection.end.to_offset(&snapshot),
2954+ )
2955+ })
2956+ .collect();
28642957
2865− DB.save_editor_selections(editor_id, workspace_id, db_selections)
2866− .await
2867− .with_context(|| format!("persisting editor selections for editor {editor_id}, workspace {workspace_id:?}"))
2868− .log_err();
2869− });
2958+ DB.save_editor_selections(editor_id, workspace_id, db_selections)
2959+ .await
2960+ .with_context(|| format!("persisting editor selections for editor {editor_id}, workspace {workspace_id:?}"))
2961+ .log_err();
2962+ });
28702963 }
28712964 }
28722965 }
@@ -2983,43 +3076,23 @@ impl Editor { · 110 unmodified lines
29833076 /// effects of selection change occur at the end of the transaction.
29843077 pub fn change_selections<R>(
29853078 &mut self,
2986− autoscroll: Option<Autoscroll>,
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>,
3079+ effects: impl Into<SelectionEffects>,
30083080 window: &mut Window,
30093081 cx: &mut Context<Self>,
30103082 change: impl FnOnce(&mut MutableSelectionsCollection<'_>) -> R,
30113083 ) -> R {
3084+ let effects = effects.into();
30123085 if let Some(state) = &mut self.deferred_selection_effects_state {
3013− state.autoscroll = autoscroll.or(state.autoscroll);
3014− state.should_update_completions = should_update_completions;
3086+ state.effects.scroll = effects.scroll.or(state.effects.scroll);
3087+ state.effects.completions = effects.completions;
3088+ state.effects.nav_history |= effects.nav_history;
30153089 let (changed, result) = self.selections.change_with(cx, change);
30163090 state.changed |= changed;
30173091 return result;
30183092 }
30193093 let mut state = DeferredSelectionEffectsState {
30203094 changed: false,
3021− should_update_completions,
3022− autoscroll,
3095+ effects,
30233096 old_cursor_position: self.selections.newest_anchor().head(),
30243097 history_entry: SelectionHistoryEntry {
30253098 selections: self.selections.disjoint_anchors(),
@@ -3069,19 +3142,13 @@ impl Editor { · 43 unmodified lines
30693142 if state.changed {
30703143 self.selection_history.push(state.history_entry);
30713144
3072− if let Some(autoscroll) = state.autoscroll {
3145+ if let Some(autoscroll) = state.effects.scroll {
30733146 self.request_autoscroll(autoscroll, cx);
30743147 }
30753148
30763149 let old_cursor_position = &state.old_cursor_position;
30773150
3078− self.selections_did_change(
3079− true,
3080− &old_cursor_position,
3081− state.should_update_completions,
3082− window,
3083− cx,
3084− );
3151+ self.selections_did_change(true, &old_cursor_position, state.effects, window, cx);
30853152
30863153 if self.should_open_signature_help_automatically(&old_cursor_position, cx) {
30873154 self.show_signature_help(&ShowSignatureHelp, window, cx);
@@ -3156,7 +3223,8 @@ impl Editor { · 68 unmodified lines
31563223 position,
31573224 goal_column,
31583225 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),
31603228 SelectPhase::Extend {
31613229 position,
31623230 click_count,
@@ -3201,9 +3269,13 @@ impl Editor { · 38 unmodified lines
32013269 _ => {}
32023270 }
32033271
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+ };
32053277
3206− self.change_selections(auto_scroll.then(Autoscroll::fit), window, cx, |s| {
3278+ self.change_selections(effects, window, cx, |s| {
32073279 s.set_pending(pending_selection, pending_mode)
32083280 });
32093281 }
@@ -3309,6 +3381,7 @@ impl Editor { · 99 unmodified lines
33093381 position: DisplayPoint,
33103382 goal_column: u32,
33113383 reset: bool,
3384+ mode: ColumnarMode,
33123385 window: &mut Window,
33133386 cx: &mut Context<Self>,
33143387 ) {
@@ -3331,26 +3404,30 @@ impl Editor { · 16 unmodified lines
33313404 SelectMode::Character,
33323405 );
33333406 });
3334− if position.column() != goal_column {
3335− self.columnar_display_point = Some(DisplayPoint::new(position.row(), goal_column));
3336− } else {
3337− self.columnar_display_point = None;
3338− }
3339− }
3407+ };
33403408
33413409 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+ };
33433428
33443429 if !reset {
3345− self.columnar_display_point = None;
3346− self.select_columns(
3347− tail.to_display_point(&display_map),
3348− position,
3349− goal_column,
3350− &display_map,
3351− window,
3352− cx,
3353− );
3430+ self.select_columns(position, goal_column, &display_map, window, cx);
33543431 }
33553432 }
33563433
@@ -3364,11 +3441,8 @@ impl Editor { · 7 unmodified lines
33643441 ) {
33653442 let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
33663443
3367− if let Some(tail) = self.columnar_selection_tail.as_ref() {
3368− let tail = self
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);
3444+ if self.columnar_selection_state.is_some() {
3445+ self.select_columns(position, goal_column, &display_map, window, cx);
33723446 } else if let Some(mut pending) = self.selections.pending_anchor() {
33733447 let buffer = self.buffer.read(cx).snapshot(cx);
33743448 let head;
@@ -3455,7 +3529,7 @@ impl Editor { · 80 unmodified lines
34553529 }
34563530
34573531 fn end_selection(&mut self, window: &mut Window, cx: &mut Context<Self>) {
3458− self.columnar_selection_tail.take();
3532+ self.columnar_selection_state.take();
34593533 if self.selections.pending_anchor().is_some() {
34603534 let selections = self.selections.all::<usize>(cx);
34613535 self.change_selections(None, window, cx, |s| {
@@ -3467,13 +3541,26 @@ impl Editor { · 5 unmodified lines
34673541
34683542 fn select_columns(
34693543 &mut self,
3470− tail: DisplayPoint,
34713544 head: DisplayPoint,
34723545 goal_column: u32,
34733546 display_map: &DisplaySnapshot,
34743547 window: &mut Window,
34753548 cx: &mut Context<Self>,
34763549 ) {
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+
34773564 let start_row = cmp::min(tail.row(), head.row());
34783565 let end_row = cmp::max(tail.row(), head.row());
34793566 let start_column = cmp::min(tail.column(), goal_column);
@@ -3483,7 +3570,10 @@ impl Editor { · 3 unmodified lines
34833570 let selection_ranges = (start_row.0..=end_row.0)
34843571 .map(DisplayRow)
34853572 .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+ {
34873577 let start = display_map
34883578 .clip_point(DisplayPoint::new(row, start_column), Bias::Left)
34893579 .to_point(display_map);
@@ -3501,15 +3591,19 @@ impl Editor { · 11 unmodified lines
35013591 })
35023592 .collect::<Vec<_>>();
35033593
3504− let mut non_empty_ranges = selection_ranges
3505− .iter()
3506− .filter(|selection_range| selection_range.start != selection_range.end)
3507− .peekable();
3508−
3509− let ranges = if non_empty_ranges.peek().is_some() {
3510− non_empty_ranges.cloned().collect()
3511− } else {
3512− selection_ranges
3594+ let ranges = match columnar_state {
3595+ ColumnarSelectionState::FromMouse { .. } => {
3596+ let mut non_empty_ranges = selection_ranges
3597+ .iter()
3598+ .filter(|selection_range| selection_range.start != selection_range.end)
3599+ .peekable();
3600+ if non_empty_ranges.peek().is_some() {
3601+ non_empty_ranges.cloned().collect()
3602+ } else {
3603+ selection_ranges
3604+ }
3605+ }
3606+ _ => selection_ranges,
35133607 };
35143608
35153609 self.change_selections(None, window, cx, |s| {
@@ -3532,11 +3626,11 @@ impl Editor { · 16 unmodified lines
35323626 };
35333627
35343628 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)
35363630 }
35373631
35383632 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()
35403634 }
35413635
35423636 pub fn cancel(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context<Self>) {
@@ -3990,8 +4084,8 @@ impl Editor { · 447 unmodified lines
39904084 }
39914085
39924086 let had_active_inline_completion = this.has_active_inline_completion();
3993− this.change_selections_without_updating_completions(
3994− Some(Autoscroll::fit()),
4087+ this.change_selections(
4088+ SelectionEffects::scroll(Autoscroll::fit()).completions(false),
39954089 window,
39964090 cx,
39974091 |s| s.select(new_selections),
@@ -5114,10 +5208,13 @@ impl Editor { · 1116 unmodified lines
51145208 .as_ref()
51155209 .map_or(true, |provider| provider.filter_completions());
51165210
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.
51195211 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 {
51215218 // If the new query is a suffix of the old query (typing more characters) and
51225219 // the previous result was complete, the existing completions can be filtered.
51235220 //
@@ -5135,7 +5232,6 @@ impl Editor { · 11 unmodified lines
51355232 menu.initial_position.to_offset(&snapshot) == position.to_offset(&snapshot)
51365233 };
51375234 if position_matches {
5138− menu.filter(query.clone(), provider.clone(), window, cx);
51395235 return;
51405236 }
51415237 }
@@ -5684,70 +5780,60 @@ impl Editor { · 542 unmodified lines
56845780 drop(context_menu);
56855781 let snapshot = self.snapshot(window, cx);
56865782 let deployed_from = action.deployed_from.clone();
5687− let mut task = self.code_actions_task.take();
56885783 let action = action.clone();
5689− cx.spawn_in(window, async move |editor, cx| {
5690− while let Some(prev_task) = task {
5691− prev_task.await.log_err();
5692− task = editor.update(cx, |this, _| this.code_actions_task.take())?;
5784+ self.completion_tasks.clear();
5785+ self.discard_inline_completion(false, cx);
5786+
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)
56935790 }
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()));
56945810
5695− let spawned_test_task = editor.update_in(cx, |editor, window, cx| {
5696− if editor.focus_handle.is_focused(window) {
5697− let multibuffer_point = match &action.deployed_from {
5698− Some(CodeActionSource::Indicator(row)) => {
5699− DisplayPoint::new(*row, 0).to_point(&snapshot)
5700− }
5701− _ => editor.selections.newest::<Point>(cx).head(),
5702− };
5703− let (buffer, buffer_row) = snapshot
5704− .buffer_snapshot
5705− .buffer_line_for_row(MultiBufferRow(multibuffer_point.row))
5706− .and_then(|(buffer_snapshot, range)| {
5707− editor
5708− .buffer
5709− .read(cx)
5710− .buffer(buffer_snapshot.remote_id())
5711− .map(|buffer| (buffer, range.start.row))
5712− })?;
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;
5811+ if !self.focus_handle.is_focused(window) {
5812+ return;
5813+ }
5814+ let project = self.project.clone();
5815+
5816+ let code_actions_task = match deployed_from {
5817+ Some(CodeActionSource::RunMenu(_)) => Task::ready(None),
5818+ _ => self.code_actions(buffer_row, window, cx),
5819+ };
5820+
5821+ let runnable_task = match deployed_from {
5822+ Some(CodeActionSource::Indicator(_)) => Task::ready(Ok(Default::default())),
5823+ _ => {
5824+ let mut task_context_task = Task::ready(None);
5825+ if let Some(tasks) = &tasks {
5826+ if let Some(project) = project {
5827+ task_context_task =
5828+ Self::build_tasks_context(&project, &buffer, buffer_row, &tasks, cx);
57345829 }
5830+ }
57355831
5736− editor.completion_tasks.clear();
5737− editor.discard_inline_completion(false, cx);
5738− let task_context =
5739− tasks
5740− .as_ref()
5741− .zip(editor.project.clone())
5742− .map(|(tasks, project)| {
5743− Self::build_tasks_context(&project, &buffer, buffer_row, tasks, cx)
5744− });
5832+ cx.spawn_in(window, {
5833+ let buffer = buffer.clone();
5834+ async move |editor, cx| {
5835+ let task_context = task_context_task.await;
57455836
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− };
57515837 let resolved_tasks =
57525838 tasks
57535839 .zip(task_context.clone())
@@ -5758,104 +5844,144 @@ impl Editor { · 4 unmodified lines
57585844 tasks.column,
57595845 )),
57605846 });
5761− let debug_scenarios = editor.update(cx, |editor, cx| {
5762− if cx.has_flag::<DebuggerFeatureFlag>() {
5763− maybe!({
5764− let project = editor.project.as_ref()?;
5765− let dap_store = project.read(cx).dap_store();
5766− let mut scenarios = vec![];
5767− let resolved_tasks = resolved_tasks.as_ref()?;
5768− let buffer = buffer.read(cx);
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?;
5847+ let debug_scenarios = editor
5848+ .update(cx, |editor, cx| {
5849+ editor.debug_scenarios(&resolved_tasks, &buffer, cx)
5850+ })?
5851+ .await;
5852+ anyhow::Ok((resolved_tasks, debug_scenarios, task_context))
5853+ }
5854+ })
58525855 }
5856+ };
58535857
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+ })
58555898 })
58565899 .detach_and_log_err(cx);
58575900 }
58585901
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+
58595985 pub fn confirm_code_action(
58605986 &mut self,
58615987 action: &ConfirmCodeAction,
@@ -6193,71 +6319,65 @@ impl Editor { · 331 unmodified lines
61936319 ) {
61946320 if let Some(state) = &mut self.inline_blame_popover {
61956321 state.hide_task.take();
6196− cx.notify();
61976322 } else {
61986323 let delay = EditorSettings::get_global(cx).hover_popover_delay;
6324+ let blame_entry = blame_entry.clone();
61996325 let show_task = cx.spawn(async move |editor, cx| {
62006326 cx.background_executor()
62016327 .timer(std::time::Duration::from_millis(delay))
62026328 .await;
62036329 editor
62046330 .update(cx, |editor, cx| {
6205− if let Some(state) = &mut editor.inline_blame_popover {
6206− state.show_task = None;
6207− cx.notify();
6208− }
6331+ editor.inline_blame_popover_show_task.take();
6332+ let Some(blame) = editor.blame.as_ref() else {
6333+ return;
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();
62096359 })
62106360 .ok();
62116361 });
6212− let Some(blame) = self.blame.as_ref() else {
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− });
6362+ self.inline_blame_popover_show_task = Some(show_task);
62396363 }
62406364 }
62416365
62426366 fn hide_blame_popover(&mut self, cx: &mut Context<Self>) {
6367+ self.inline_blame_popover_show_task.take();
62436368 if let Some(state) = &mut self.inline_blame_popover {
6244− if state.show_task.is_some() {
6245− self.inline_blame_popover.take();
6246− cx.notify();
6247− } else {
6248− let hide_task = cx.spawn(async move |editor, cx| {
6249− cx.background_executor()
6250− .timer(std::time::Duration::from_millis(100))
6251− .await;
6252− editor
6253− .update(cx, |editor, cx| {
6254− editor.inline_blame_popover.take();
6255− cx.notify();
6256− })
6257− .ok();
6258− });
6259− state.hide_task = Some(hide_task);
6260− }
6369+ let hide_task = cx.spawn(async move |editor, cx| {
6370+ cx.background_executor()
6371+ .timer(std::time::Duration::from_millis(100))
6372+ .await;
6373+ editor
6374+ .update(cx, |editor, cx| {
6375+ editor.inline_blame_popover.take();
6376+ cx.notify();
6377+ })
6378+ .ok();
6379+ });
6380+ state.hide_task = Some(hide_task);
62616381 }
62626382 }
62636383
@@ -7098,13 +7218,9 @@ impl Editor { · 834 unmodified lines
70987218 )
70997219 }
71007220
7101− fn multi_cursor_modifier(
7102− cursor_event: bool,
7103− modifiers: &Modifiers,
7104− cx: &mut Context<Self>,
7105− ) -> bool {
7221+ fn multi_cursor_modifier(invert: bool, modifiers: &Modifiers, cx: &mut Context<Self>) -> bool {
71067222 let multi_cursor_setting = EditorSettings::get_global(cx).multi_cursor_modifier;
7107− if cursor_event {
7223+ if invert {
71087224 match multi_cursor_setting {
71097225 MultiCursorModifier::Alt => modifiers.alt,
71107226 MultiCursorModifier::CmdOrCtrl => modifiers.secondary(),
@@ -7117,8 +7233,21 @@ impl Editor { · 6 unmodified lines
71177233 }
71187234 }
71197235
7120− fn columnar_selection_modifiers(multi_cursor_modifier: bool, modifiers: &Modifiers) -> bool {
7121− modifiers.shift && multi_cursor_modifier && modifiers.number_of_modifiers() == 2
7236+ fn columnar_selection_mode(
7237+ 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+ }
71227251 }
71237252
71247253 fn update_selection_mode(
@@ -7128,10 +7257,10 @@ impl Editor { · 3 unmodified lines
71287257 window: &mut Window,
71297258 cx: &mut Context<Self>,
71307259 ) {
7131− let multi_cursor_modifier = Self::multi_cursor_modifier(true, modifiers, cx);
7132− if !Self::columnar_selection_modifiers(multi_cursor_modifier, modifiers)
7133− || self.selections.pending.is_none()
7134− {
7260+ let Some(mode) = Self::columnar_selection_mode(modifiers, cx) else {
7261+ return;
7262+ };
7263+ if self.selections.pending.is_none() {
71357264 return;
71367265 }
71377266
@@ -7143,6 +7272,7 @@ impl Editor { · 5 unmodified lines
71437272 SelectPhase::BeginColumnar {
71447273 position,
71457274 reset: false,
7275+ mode,
71467276 goal_column: point_for_position.exact_unclipped.column(),
71477277 },
71487278 window,
@@ -7519,8 +7649,7 @@ impl Editor { · 370 unmodified lines
75197649 "Set Breakpoint"
75207650 };
75217651
7522− let run_to_cursor = command_palette_hooks::CommandPaletteFilter::try_global(cx)
7523− .map_or(false, |filter| !filter.is_hidden(&DebuggerRunToCursor));
7652+ let run_to_cursor = window.is_action_available(&RunToCursor, cx);
75247653
75257654 let toggle_state_msg = breakpoint.as_ref().map_or(None, |bp| match bp.1.state {
75267655 BreakpointState::Enabled => Some("Disable"),
@@ -7544,7 +7673,7 @@ impl Editor { · 17 unmodified lines
75447673 })
75457674 .ok();
75467675
7547− window.dispatch_action(Box::new(DebuggerRunToCursor), cx);
7676+ window.dispatch_action(Box::new(RunToCursor), cx);
75487677 })
75497678 .separator()
75507679 })
@@ -7899,7 +8028,7 @@ impl Editor { · 348 unmodified lines
78998028 window.focus(&editor.focus_handle(cx));
79008029 editor.toggle_code_actions(
79018030 &ToggleCodeActions {
7902− deployed_from: Some(CodeActionSource::Indicator(row)),
8031+ deployed_from: Some(CodeActionSource::RunMenu(row)),
79038032 quick_launch,
79048033 },
79058034 window,
@@ -14076,7 +14205,8 @@ impl Editor { · 6170 unmodified lines
1407614205 prefer_lsp && !lsp_tasks_by_rows.is_empty(),
1407714206 new_rows,
1407814207 cx.clone(),
14079− );
14208+ )
14209+ .await;
1408014210 editor
1408114211 .update(cx, |editor, _| {
1408214212 editor.clear_tasks();
@@ -14106,35 +14236,40 @@ impl Editor { · 23 unmodified lines
1410614236 snapshot: DisplaySnapshot,
1410714237 prefer_lsp: bool,
1410814238 runnable_ranges: Vec<RunnableRange>,
14109− mut cx: AsyncWindowContext,
14110− ) -> Vec<((BufferId, BufferRow), RunnableTasks)> {
14111− runnable_ranges
14112− .into_iter()
14113− .filter_map(|mut runnable| {
14114− let mut tasks = cx
14239+ cx: AsyncWindowContext,
14240+ ) -> Task<Vec<((BufferId, BufferRow), RunnableTasks)>> {
14241+ cx.spawn(async move |cx| {
14242+ let mut runnable_rows = Vec::with_capacity(runnable_ranges.len());
14243+ for mut runnable in runnable_ranges {
14244+ let Some(tasks) = cx
1411514245 .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+
1411714252 if prefer_lsp {
1411814253 tasks.retain(|(task_kind, _)| {
1411914254 !matches!(task_kind, TaskSourceKind::Language { .. })
1412014255 });
1412114256 }
1412214257 if tasks.is_empty() {
14123− return None;
14258+ continue;
1412414259 }
1412514260
1412614261 let point = runnable.run_range.start.to_point(&snapshot.buffer_snapshot);
14127−
14128− let row = snapshot
14262+ let Some(row) = snapshot
1412914263 .buffer_snapshot
14130− .buffer_line_for_row(MultiBufferRow(point.row))?
14131− .1
14132− .start
14133− .row;
14264+ .buffer_line_for_row(MultiBufferRow(point.row))
14265+ .map(|(_, range)| range.start.row)
14266+ else {
14267+ continue;
14268+ };
1413414269
1413514270 let context_range =
1413614271 BufferOffset(runnable.full_range.start)..BufferOffset(runnable.full_range.end);
14137− Some((
14272+ runnable_rows.push((
1413814273 (runnable.buffer_id, row),
1413914274 RunnableTasks {
1414014275 templates: tasks,
@@ -14145,16 +14280,17 @@ impl Editor { · 4 unmodified lines
1414514280 column: point.column,
1414614281 extra_variables: runnable.extra_captures,
1414714282 },
14148− ))
14149− })
14150− .collect()
14283+ ));
14284+ }
14285+ runnable_rows
14286+ })
1415114287 }
1415214288
1415314289 fn templates_with_tags(
1415414290 project: &Entity<Project>,
1415514291 runnable: &mut Runnable,
1415614292 cx: &mut App,
14157− ) -> Vec<(TaskSourceKind, TaskTemplate)> {
14293+ ) -> Task<Vec<(TaskSourceKind, TaskTemplate)>> {
1415814294 let (inventory, worktree_id, file) = project.read_with(cx, |project, cx| {
1415914295 let (worktree_id, file) = project
1416014296 .buffer_for_id(runnable.buffer, cx)
@@ -14169,39 +14305,40 @@ impl Editor { · 8 unmodified lines
1416914305 )
1417014306 });
1417114307
14172− let mut templates_with_tags = mem::take(&mut runnable.tags)
14173− .into_iter()
14174− .flat_map(|RunnableTag(tag)| {
14175− inventory
14176− .as_ref()
14177− .into_iter()
14178− .flat_map(|inventory| {
14179− inventory.read(cx).list_tasks(
14180− file.clone(),
14181− Some(runnable.language.clone()),
14182− worktree_id,
14183− cx,
14184− )
14185− })
14186− .filter(move |(_, template)| {
14187− template.tags.iter().any(|source_tag| source_tag == &tag)
14188− })
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);
14308+ let tags = mem::take(&mut runnable.tags);
14309+ let language = runnable.language.clone();
14310+ cx.spawn(async move |cx| {
14311+ let mut templates_with_tags = Vec::new();
14312+ if let Some(inventory) = inventory {
14313+ for RunnableTag(tag) in tags {
14314+ let Ok(new_tasks) = inventory.update(cx, |inventory, cx| {
14315+ inventory.list_tasks(file.clone(), Some(language.clone()), worktree_id, cx)
14316+ }) else {
14317+ return templates_with_tags;
14318+ };
14319+ templates_with_tags.extend(new_tasks.await.into_iter().filter(
14320+ move |(_, template)| {
14321+ template.tags.iter().any(|source_tag| source_tag == &tag)
14322+ },
14323+ ));
14324+ }
1420114325 }
14202− }
14326+ templates_with_tags.sort_by_key(|(kind, _)| kind.to_owned());
1420314327
14204− templates_with_tags
14328+ 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+ })
1420514342 }
1420614343
1420714344 pub fn move_to_enclosing_bracket(
@@ -14268,18 +14405,20 @@ impl Editor { · 60 unmodified lines
1426814405 cx: &mut Context<Self>,
1426914406 ) {
1427014407 self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction);
14271− self.end_selection(window, cx);
14272− self.selection_history.mode = SelectionHistoryMode::Undoing;
1427314408 if let Some(entry) = self.selection_history.undo_stack.pop_back() {
14274− self.change_selections(None, window, cx, |s| {
14275− s.select_anchors(entry.selections.to_vec())
14409+ self.selection_history.mode = SelectionHistoryMode::Undoing;
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+ });
1427614415 });
14416+ self.selection_history.mode = SelectionHistoryMode::Normal;
14417+
1427714418 self.select_next_state = entry.select_next_state;
1427814419 self.select_prev_state = entry.select_prev_state;
1427914420 self.add_selections_state = entry.add_selections_state;
14280− self.request_autoscroll(Autoscroll::newest(), cx);
1428114421 }
14282− self.selection_history.mode = SelectionHistoryMode::Normal;
1428314422 }
1428414423
1428514424 pub fn redo_selection(
@@ -14289,18 +14428,20 @@ impl Editor { · 3 unmodified lines
1428914428 cx: &mut Context<Self>,
1429014429 ) {
1429114430 self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction);
14292− self.end_selection(window, cx);
14293− self.selection_history.mode = SelectionHistoryMode::Redoing;
1429414431 if let Some(entry) = self.selection_history.redo_stack.pop_back() {
14295− self.change_selections(None, window, cx, |s| {
14296− s.select_anchors(entry.selections.to_vec())
14432+ self.selection_history.mode = SelectionHistoryMode::Redoing;
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+ });
1429714438 });
14439+ self.selection_history.mode = SelectionHistoryMode::Normal;
14440+
1429814441 self.select_next_state = entry.select_next_state;
1429914442 self.select_prev_state = entry.select_prev_state;
1430014443 self.add_selections_state = entry.add_selections_state;
14301− self.request_autoscroll(Autoscroll::newest(), cx);
1430214444 }
14303− self.selection_history.mode = SelectionHistoryMode::Normal;
1430414445 }
1430514446
1430614447 pub fn expand_excerpts(
@@ -15605,7 +15746,7 @@ impl Editor { · 1298 unmodified lines
1560515746 Some(self.perform_format(
1560615747 project,
1560715748 FormatTrigger::Manual,
15608− FormatTarget::Buffers,
15749+ FormatTarget::Buffers(self.buffer.read(cx).all_buffers()),
1560915750 window,
1561015751 cx,
1561115752 ))
@@ -15650,13 +15791,7 @@ impl Editor { · 38 unmodified lines
1565015791 ) -> Task<Result<()>> {
1565115792 let buffer = self.buffer.clone();
1565215793 let (buffers, target) = match target {
15653− FormatTarget::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− }
15794+ FormatTarget::Buffers(buffers) => (buffers, LspFormatTarget::Buffers),
1566015795 FormatTarget::Ranges(selection_ranges) => {
1566115796 let multi_buffer = buffer.read(cx);
1566215797 let snapshot = multi_buffer.read(cx);
@@ -16113,13 +16248,16 @@ impl Editor { · 450 unmodified lines
1611316248 window: &Window,
1611416249 cx: &mut Context<Self>,
1611516250 ) -> Option<()> {
16116− let project = self.project.as_ref()?.downgrade();
16251+ if !self.mode().is_full() {
16252+ return None;
16253+ }
1611716254 let pull_diagnostics_settings = ProjectSettings::get_global(cx)
1611816255 .diagnostics
1611916256 .lsp_pull_diagnostics;
1612016257 if !pull_diagnostics_settings.enabled {
1612116258 return None;
1612216259 }
16260+ let project = self.project.as_ref()?.downgrade();
1612316261 let debounce = Duration::from_millis(pull_diagnostics_settings.debounce_ms);
1612416262 let mut buffers = self.buffer.read(cx).all_buffers();
1612516263 if let Some(buffer_id) = buffer_id {
@@ -16176,7 +16314,13 @@ impl Editor { · 50 unmodified lines
1617616314 s.clear_pending();
1617716315 }
1617816316 });
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+ );
1618016324 }
1618116325
1618216326 pub fn transact(
@@ -17098,7 +17242,16 @@ impl Editor { · 915 unmodified lines
1709817242 }
1709917243
1710017244 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);
1710217255 }
1710317256 }
1710417257
@@ -17130,7 +17283,16 @@ impl Editor { · 25 unmodified lines
1713017283 });
1713117284
1713217285 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);
1713417296 }
1713517297 }
1713617298
@@ -18119,16 +18281,9 @@ impl Editor { · 982 unmodified lines
1811918281 .selections
1812018282 .disjoint_anchors()
1812118283 .iter()
18122− .map(|selection| {
18123− let range = if selection.reversed {
18124− selection.end.text_anchor..selection.start.text_anchor
18125− } else {
18126− selection.start.text_anchor..selection.end.text_anchor
18127− };
18128− Location {
18129− buffer: buffer.clone(),
18130− range,
18131− }
18284+ .map(|range| Location {
18285+ buffer: buffer.clone(),
18286+ range: range.start.text_anchor..range.end.text_anchor,
1813218287 })
1813318288 .collect::<Vec<_>>();
1813418289
@@ -18383,12 +18538,12 @@ impl Editor { · 248 unmodified lines
1838318538
1838418539 pub fn highlight_gutter<T: 'static>(
1838518540 &mut self,
18386− ranges: &[Range<Anchor>],
18541+ ranges: impl Into<Vec<Range<Anchor>>>,
1838718542 color_fetcher: fn(&App) -> Hsla,
1838818543 cx: &mut Context<Self>,
1838918544 ) {
1839018545 self.gutter_highlights
18391− .insert(TypeId::of::<T>(), (color_fetcher, Arc::from(ranges)));
18546+ .insert(TypeId::of::<T>(), (color_fetcher, ranges.into()));
1839218547 cx.notify();
1839318548 }
1839418549
@@ -18400,6 +18555,65 @@ impl Editor { · 5 unmodified lines
1840018555 self.gutter_highlights.remove(&TypeId::of::<T>())
1840118556 }
1840218557
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+
1840318617 #[cfg(feature = "test-support")]
1840418618 pub fn all_text_background_highlights(
1840518619 &self,
@@ -19662,7 +19876,7 @@ impl Editor { · 1256 unmodified lines
1966219876 .flatten()
1966319877 .filter_map(|keystroke| {
1966419878 if keystroke.modifiers.is_subset_of(&Modifiers::shift()) {
19665− Some(keystroke.key_char.clone().unwrap_or(keystroke.key.clone()))
19879+ keystroke.key_char.clone()
1966619880 } else {
1966719881 None
1966819882 }
@@ -19739,6 +19953,21 @@ impl Editor { · 70 unmodified lines
1973919953 }
1974019954 }
1974119955
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+
1974219971 pub fn register_action<A: Action>(
1974319972 &mut self,
1974419973 listener: impl Fn(&A, &mut Window, &mut App) + 'static,
@@ -19747,7 +19976,7 @@ impl Editor { · 2 unmodified lines
1974719976 let listener = Arc::new(listener);
1974819977 self.editor_actions.borrow_mut().insert(
1974919978 id,
19750− Box::new(move |window, _| {
19979+ Box::new(move |_, window, _| {
1975119980 let listener = listener.clone();
1975219981 window.on_action(TypeId::of::<A>(), move |action, phase, window, cx| {
1975319982 let action = action.downcast_ref().unwrap();
@@ -19945,12 +20174,15 @@ impl Editor { · 191 unmodified lines
1994520174 if !selections.is_empty() {
1994620175 let snapshot =
1994720176 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;
1994820179 self.change_selections(None, window, cx, |s| {
1994920180 s.select_ranges(selections.into_iter().map(|(start, end)| {
1995020181 snapshot.clip_offset(start, Bias::Left)
1995120182 ..snapshot.clip_offset(end, Bias::Right)
1995220183 }));
1995320184 });
20185+ self.selection_history.mode = SelectionHistoryMode::Normal;
1995420186 }
1995520187 };
1995620188 }
@@ -19976,8 +20208,13 @@ fn process_completion_for_edit( · 19 unmodified lines
1997620208 let buffer = buffer.read(cx);
1997720209 let buffer_snapshot = buffer.snapshot();
1997820210 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
1997920214 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) {
1998120218 if scope.prefers_label_for_snippet_in_completion() {
1998220219 if let Some(label) = completion.label() {
1998320220 if matches!(
@@ -21440,8 +21677,8 @@ impl EditorSnapshot { · 1456 unmodified lines
2144021677 return None;
2144121678 }
2144221679
21443− let em_width = cx.text_system().em_width(font_id, font_size).log_err()?;
21444− let em_advance = cx.text_system().em_advance(font_id, font_size).log_err()?;
21680+ let ch_width = cx.text_system().ch_width(font_id, font_size).log_err()?;
21681+ let ch_advance = cx.text_system().ch_advance(font_id, font_size).log_err()?;
2144521682
2144621683 let show_git_gutter = self.show_git_diff_gutter.unwrap_or_else(|| {
2144721684 matches!(
@@ -21454,8 +21691,10 @@ impl EditorSnapshot { · 6 unmodified lines
2145421691 .show_line_numbers
2145521692 .unwrap_or(gutter_settings.line_numbers);
2145621693 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.
21458− let min_width_for_number_on_gutter = em_advance * MIN_LINE_NUMBER_DIGITS as f32;
21694+ // Avoid flicker-like gutter resizes when the line number gains another digit by
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;
2145921698 max_line_number_width.max(min_width_for_number_on_gutter)
2146021699 } else {
2146121700 0.0.into()
@@ -21478,20 +21717,20 @@ impl EditorSnapshot { · 16 unmodified lines
2147821717 + MAX_RELATIVE_TIMESTAMP.len()
2147921718 + SPACING_WIDTH;
2148021719
21481− em_advance * max_char_count
21720+ ch_advance * max_char_count
2148221721 });
2148321722
2148421723 let is_singleton = self.buffer_snapshot.is_singleton();
2148521724
2148621725 let mut left_padding = git_blame_entries_width.unwrap_or(Pixels::ZERO);
2148721726 left_padding += if !is_singleton {
21488− em_width * 4.0
21727+ ch_width * 4.0
2148921728 } else if show_runnables || show_breakpoints {
21490− em_width * 3.0
21729+ ch_width * 3.0
2149121730 } else if show_git_gutter && show_line_numbers {
21492− em_width * 2.0
21731+ ch_width * 2.0
2149321732 } else if show_git_gutter || show_line_numbers {
21494− em_width
21733+ ch_width
2149521734 } else {
2149621735 px(0.)
2149721736 };
@@ -21499,11 +21738,11 @@ impl EditorSnapshot { · 1 unmodified line
2149921738 let shows_folds = is_singleton && gutter_settings.folds;
2150021739
2150121740 let right_padding = if shows_folds && show_line_numbers {
21502− em_width * 4.0
21741+ ch_width * 4.0
2150321742 } else if shows_folds || (!is_singleton && show_line_numbers) {
21504− em_width * 3.0
21743+ ch_width * 3.0
2150521744 } else if show_line_numbers {
21506− em_width
21745+ ch_width
2150721746 } else {
2150821747 px(0.)
2150921748 };
@@ -21709,7 +21948,7 @@ impl Render for Editor { · 199 unmodified lines
2170921948
2171021949 let background = match self.mode {
2171121950 EditorMode::SingleLine { .. } => cx.theme().system().transparent,
21712− EditorMode::AutoHeight { max_lines: _ } => cx.theme().system().transparent,
21951+ EditorMode::AutoHeight { .. } => cx.theme().system().transparent,
2171321952 EditorMode::Full { .. } => cx.theme().colors().editor_background,
2171421953 EditorMode::Minimap { .. } => cx.theme().colors().editor_background.opacity(0.7),
2171521954 };
@@ -22367,6 +22606,7 @@ impl BreakpointPromptEditor { · 651 unmodified lines
2236722606 let prompt = cx.new(|cx| {
2236822607 let mut prompt = Editor::new(
2236922608 EditorMode::AutoHeight {
22609+ min_lines: 1,
2237022610 max_lines: Self::MAX_LINES as usize,
2237122611 },
2237222612 buffer,
crates/editor/src/editor_settings.rs+5−0
@@ -150,6 +150,7 @@ impl Minimap { · 149 unmodified lines
150150
151151 #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
152152 pub struct Gutter {
153+ pub min_line_number_digits: usize,
153154 pub line_numbers: bool,
154155 pub runnables: bool,
155156 pub breakpoints: bool,
@@ -609,6 +610,10 @@ pub struct GutterContent { · 453 unmodified lines
609610 ///
610611 /// Default: true
611612 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>,
612617 /// Whether to show runnable buttons in the gutter.
613618 ///
614619 /// Default: true
crates/editor/src/editor_tests.rs+254−40
@@ -56,7 +56,7 @@ use util::{ · 55 unmodified lines
5656 };
5757 use workspace::{
5858 CloseActiveItem, CloseAllItems, CloseInactiveItems, NavigationEntry, OpenOptions, ViewId,
59− item::{FollowEvent, FollowableItem, Item, ItemHandle},
59+ item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
6060 };
6161
6262 #[gpui::test]
@@ -1907,7 +1907,6 @@ fn test_prev_next_word_boundary(cx: &mut TestAppContext) { · 1844 unmodified lines
19071907 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
19081908 ])
19091909 });
1910−
19111910 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
19121911 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
19131912
@@ -1927,29 +1926,29 @@ fn test_prev_next_word_boundary(cx: &mut TestAppContext) { · 13 unmodified lines
19271926 assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
19281927
19291928 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);
19311930
19321931 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);
19341933
19351934 editor.move_right(&MoveRight, window, cx);
19361935 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
19371936 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()}",
19391938 editor,
19401939 cx,
19411940 );
19421941
19431942 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
19441943 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()}",
19461945 editor,
19471946 cx,
19481947 );
19491948
19501949 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
19511950 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()}",
19531952 editor,
19541953 cx,
19551954 );
@@ -9042,7 +9041,15 @@ async fn test_document_format_during_save(cx: &mut TestAppContext) { · 7086 unmodified lines
90429041 );
90439042 let save = editor
90449043 .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+ )
90469053 })
90479054 .unwrap();
90489055 cx.executor().start_waiting();
@@ -9074,7 +9081,15 @@ async fn test_document_format_during_save(cx: &mut TestAppContext) { · 25 unmodified lines
90749081 );
90759082 let save = editor
90769083 .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+ )
90789093 })
90799094 .unwrap();
90809095 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
@@ -9086,22 +9101,6 @@ async fn test_document_format_during_save(cx: &mut TestAppContext) { · 5 unmodified lines
90869101 );
90879102 }
90889103
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−
91059104 // Set rust language override and assert overridden tabsize is sent to language server
91069105 update_test_language_settings(cx, |settings| {
91079106 settings.languages.insert(
@@ -9129,7 +9128,15 @@ async fn test_document_format_during_save(cx: &mut TestAppContext) { · 21 unmodified lines
91299128 });
91309129 let save = editor
91319130 .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+ )
91339140 })
91349141 .unwrap();
91359142 cx.executor().start_waiting();
@@ -9297,7 +9304,15 @@ async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) { · 161 unmodified lines
92979304 cx.executor().start_waiting();
92989305 let save = multi_buffer_editor
92999306 .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+ )
93019316 })
93029317 .unwrap();
93039318
@@ -9341,6 +9356,170 @@ async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) { · 37 unmodified lines
93419356 });
93429357 }
93439358
9359+#[gpui::test]
9360+async 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+
93449523 #[gpui::test]
93459524 async fn test_range_format_during_save(cx: &mut TestAppContext) {
93469525 init_test(cx, |_| {});
@@ -9384,7 +9563,15 @@ async fn test_range_format_during_save(cx: &mut TestAppContext) { · 37 unmodified lines
93849563
93859564 let save = editor
93869565 .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+ )
93889575 })
93899576 .unwrap();
93909577 fake_server
@@ -9427,7 +9614,15 @@ async fn test_range_format_during_save(cx: &mut TestAppContext) { · 36 unmodified lines
94279614 );
94289615 let save = editor
94299616 .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+ )
94319626 })
94329627 .unwrap();
94339628 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
@@ -9442,12 +9637,20 @@ async fn test_range_format_during_save(cx: &mut TestAppContext) { · 8 unmodified lines
94429637 // For non-dirty buffer, no formatting request should be sent
94439638 let save = editor
94449639 .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+ )
94469649 })
94479650 .unwrap();
94489651 let _pending_format_request = fake_server
94499652 .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");
94519654 })
94529655 .next();
94539656 cx.executor().start_waiting();
@@ -9470,7 +9673,15 @@ async fn test_range_format_during_save(cx: &mut TestAppContext) { · 16 unmodified lines
94709673 assert!(cx.read(|cx| editor.is_dirty(cx)));
94719674 let save = editor
94729675 .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+ )
94749685 })
94759686 .unwrap();
94769687 fake_server
@@ -9554,7 +9765,7 @@ async fn test_document_format_manual_trigger(cx: &mut TestAppContext) { · 77 unmodified lines
95549765 editor.perform_format(
95559766 project.clone(),
95569767 FormatTrigger::Manual,
9557− FormatTarget::Buffers,
9768+ FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
95589769 window,
95599770 cx,
95609771 )
@@ -9600,7 +9811,7 @@ async fn test_document_format_manual_trigger(cx: &mut TestAppContext) { · 39 unmodified lines
96009811 editor.perform_format(
96019812 project,
96029813 FormatTrigger::Manual,
9603− FormatTarget::Buffers,
9814+ FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
96049815 window,
96059816 cx,
96069817 )
@@ -9778,7 +9989,7 @@ async fn test_multiple_formatters(cx: &mut TestAppContext) { · 171 unmodified lines
97789989 editor.perform_format(
97799990 project.clone(),
97809991 FormatTrigger::Manual,
9781− FormatTarget::Buffers,
9992+ FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
97829993 window,
97839994 cx,
97849995 )
@@ -9814,7 +10025,7 @@ async fn test_multiple_formatters(cx: &mut TestAppContext) { · 29 unmodified lines
981410025 editor.perform_format(
981510026 project.clone(),
981610027 FormatTrigger::Manual,
9817− FormatTarget::Buffers,
10028+ FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
981810029 window,
981910030 cx,
982010031 )
@@ -15356,7 +15567,7 @@ async fn test_document_format_with_prettier(cx: &mut TestAppContext) { · 5535 unmodified lines
1535615567 editor.perform_format(
1535715568 project.clone(),
1535815569 FormatTrigger::Manual,
15359− FormatTarget::Buffers,
15570+ FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
1536015571 window,
1536115572 cx,
1536215573 )
@@ -15376,7 +15587,7 @@ async fn test_document_format_with_prettier(cx: &mut TestAppContext) { · 13 unmodified lines
1537615587 editor.perform_format(
1537715588 project.clone(),
1537815589 FormatTrigger::Manual,
15379− FormatTarget::Buffers,
15590+ FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
1538015591 window,
1538115592 cx,
1538215593 )
@@ -21911,6 +22122,7 @@ async fn test_pulling_diagnostics(cx: &mut TestAppContext) { · 6528 unmodified lines
2191122122 .downcast::<Editor>()
2191222123 .unwrap();
2191322124 let fake_server = fake_servers.next().await.unwrap();
22125+ let server_id = fake_server.server.server_id();
2191422126 let mut first_request = fake_server
2191522127 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
2191622128 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
2194222154 .expect("created a singleton buffer")
2194322155 .read(cx)
2194422156 .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);
2194622161 assert_eq!(expected, buffer_result_id);
2194722162 });
2194822163 };
@@ -21988,7 +22203,6 @@ async fn test_pulling_diagnostics(cx: &mut TestAppContext) { · 39 unmodified lines
2198822203 "Cursor movement should not trigger diagnostic request"
2198922204 );
2199022205 ensure_result_id(Some("2".to_string()), cx);
21991−
2199222206 // Multiple rapid edits should be debounced
2199322207 for _ in 0..5 {
2199422208 editor.update_in(cx, |editor, window, cx| {
crates/editor/src/element.rs+347−237
@@ -1,15 +1,15 @@
11 use crate::{
22 ActiveDiagnostic, BlockId, CURSORS_VISIBLE_FOR, ChunkRendererContext, ChunkReplacement,
3− CodeActionSource, ConflictsOurs, ConflictsOursMarker, ConflictsOuter, ConflictsTheirs,
4− ConflictsTheirsMarker, ContextMenuPlacement, CursorShape, CustomBlockId, DisplayDiffHunk,
5− DisplayPoint, DisplayRow, DocumentHighlightRead, DocumentHighlightWrite, EditDisplayMode,
6− Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle, FILE_HEADER_HEIGHT,
7− FocusedBlock, GutterDimensions, HalfPageDown, HalfPageUp, HandleInput, HoveredCursor,
8− InlayHintRefreshReason, InlineCompletion, JumpData, LineDown, LineHighlight, LineUp,
9− MAX_LINE_LEN, MIN_LINE_NUMBER_DIGITS, MINIMAP_FONT_SIZE, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
10− OpenExcerpts, PageDown, PageUp, PhantomBreakpointIndicator, Point, RowExt, RowRangeExt,
11− SelectPhase, SelectedTextHighlight, Selection, SelectionDragState, SoftWrap,
12− StickyHeaderExcerpt, ToPoint, ToggleFold,
3+ CodeActionSource, ColumnarMode, ConflictsOurs, ConflictsOursMarker, ConflictsOuter,
4+ ConflictsTheirs, ConflictsTheirsMarker, ContextMenuPlacement, CursorShape, CustomBlockId,
5+ DisplayDiffHunk, DisplayPoint, DisplayRow, DocumentHighlightRead, DocumentHighlightWrite,
6+ EditDisplayMode, Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle,
7+ FILE_HEADER_HEIGHT, FocusedBlock, GutterDimensions, HalfPageDown, HalfPageUp, HandleInput,
8+ HoveredCursor, InlayHintRefreshReason, InlineCompletion, JumpData, LineDown, LineHighlight,
9+ LineUp, MAX_LINE_LEN, MINIMAP_FONT_SIZE, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, OpenExcerpts,
10+ PageDown, PageUp, PhantomBreakpointIndicator, Point, RowExt, RowRangeExt, SelectPhase,
11+ SelectedTextHighlight, Selection, SelectionDragState, SoftWrap, StickyHeaderExcerpt, ToPoint,
12+ ToggleFold,
1313 code_context_menus::{CodeActionsMenu, MENU_ASIDE_MAX_WIDTH, MENU_ASIDE_MIN_WIDTH, MENU_GAP},
1414 display_map::{
1515 Block, BlockContext, BlockStyle, DisplaySnapshot, EditorMargins, FoldId, HighlightedChunk,
@@ -75,7 +75,7 @@ use std::{ · 59 unmodified lines
7575 ops::{Deref, Range},
7676 rc::Rc,
7777 sync::Arc,
78− time::Duration,
78+ time::{Duration, Instant},
7979 };
8080 use sum_tree::Bias;
8181 use text::{BufferId, SelectionGoal};
@@ -87,6 +87,7 @@ use util::{RangeExt, ResultExt, debug_panic}; · 5 unmodified lines
8787 use workspace::{CollaboratorId, Workspace, item::Item, notifications::NotifyTaskExt};
8888
8989 const INLINE_BLAME_PADDING_EM_WIDTHS: f32 = 7.;
90+const SELECTION_DRAG_DELAY: Duration = Duration::from_millis(300);
9091
9192 /// Determines what kinds of highlights should be applied to a lines background.
9293 #[derive(Clone, Copy, Default)]
@@ -107,6 +108,12 @@ struct SelectionLayout { · 14 unmodified lines
107108 user_name: Option<SharedString>,
108109 }
109110
111+struct InlineBlameLayout {
112+ element: AnyElement,
113+ bounds: Bounds<Pixels>,
114+ entry: BlameEntry,
115+}
116+
110117 impl SelectionLayout {
111118 fn new<T: ToPoint + ToDisplayPoint + Clone>(
112119 selection: Selection<T>,
@@ -187,7 +194,7 @@ impl EditorElement { · 74 unmodified lines
187194 let editor = &self.editor;
188195 editor.update(cx, |editor, cx| {
189196 for action in editor.editor_actions.borrow().values() {
190− (action)(window, cx)
197+ (action)(editor, window, cx)
191198 }
192199 });
193200
@@ -642,6 +649,7 @@ impl EditorElement { · 448 unmodified lines
642649 editor.selection_drag_state = SelectionDragState::ReadyToDrag {
643650 selection: newest_anchor.clone(),
644651 click_position: event.position,
652+ mouse_down_time: Instant::now(),
645653 };
646654 cx.stop_propagation();
647655 return;
@@ -692,12 +700,15 @@ impl EditorElement { · 44 unmodified lines
692700 }
693701
694702 let position = point_for_position.previous_valid;
695− let multi_cursor_modifier = Editor::multi_cursor_modifier(true, &modifiers, cx);
696− if Editor::columnar_selection_modifiers(multi_cursor_modifier, &modifiers) {
703+ if let Some(mode) = Editor::columnar_selection_mode(&modifiers, cx) {
697704 editor.select(
698705 SelectPhase::BeginColumnar {
699706 position,
700− reset: true,
707+ reset: match mode {
708+ ColumnarMode::FromMouse => true,
709+ ColumnarMode::FromSelection => false,
710+ },
711+ mode: mode,
701712 goal_column: point_for_position.exact_unclipped.column(),
702713 },
703714 window,
@@ -717,7 +728,7 @@ impl EditorElement { · 13 unmodified lines
717728 editor.select(
718729 SelectPhase::Begin {
719730 position,
720− add: multi_cursor_modifier,
731+ add: Editor::multi_cursor_modifier(true, &modifiers, cx),
721732 click_count,
722733 },
723734 window,
@@ -814,6 +825,7 @@ impl EditorElement { · 90 unmodified lines
814825 SelectPhase::BeginColumnar {
815826 position,
816827 reset: true,
828+ mode: ColumnarMode::FromMouse,
817829 goal_column: point_for_position.exact_unclipped.column(),
818830 },
819831 window,
@@ -837,6 +849,7 @@ impl EditorElement { · 17 unmodified lines
837849 SelectionDragState::ReadyToDrag {
838850 selection: _,
839851 ref click_position,
852+ mouse_down_time: _,
840853 } => {
841854 if event.position == *click_position {
842855 editor.select(
@@ -851,13 +864,18 @@ impl EditorElement { · 8 unmodified lines
851864 editor.selection_drag_state = SelectionDragState::None;
852865 cx.stop_propagation();
853866 return;
867+ } else {
868+ debug_panic!("drag state can never be in ready state after drag")
854869 }
855870 }
856871 SelectionDragState::Dragging { ref selection, .. } => {
857872 let snapshot = editor.snapshot(window, cx);
858873 let selection_display = selection.map(|anchor| anchor.to_display_point(&snapshot));
859− if !point_for_position.intersects_selection(&selection_display) {
860− let is_cut = !event.modifiers.control;
874+ if !point_for_position.intersects_selection(&selection_display)
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);
861879 editor.move_selection_on_drop(
862880 &selection.clone(),
863881 point_for_position.previous_valid,
@@ -865,10 +883,11 @@ impl EditorElement { · 1 unmodified line
865883 window,
866884 cx,
867885 );
868− editor.selection_drag_state = SelectionDragState::None;
869− cx.stop_propagation();
870− return;
871886 }
887+ editor.selection_drag_state = SelectionDragState::None;
888+ cx.stop_propagation();
889+ cx.notify();
890+ return;
872891 }
873892 _ => {}
874893 }
@@ -941,39 +960,43 @@ impl EditorElement { · 66 unmodified lines
941960 return;
942961 }
943962
944− let text_bounds = position_map.text_hitbox.bounds;
945963 let point_for_position = position_map.point_for_position(event.position);
964+ let text_hitbox = &position_map.text_hitbox;
946965
947− let mut scroll_delta = gpui::Point::<f32>::default();
948− let vertical_margin = position_map.line_height.min(text_bounds.size.height / 3.0);
949− let top = text_bounds.origin.y + vertical_margin;
950− let bottom = text_bounds.bottom_left().y - vertical_margin;
951− if event.position.y < top {
952− scroll_delta.y = -scale_vertical_mouse_autoscroll_delta(top - event.position.y);
953− }
954− if event.position.y > bottom {
955− scroll_delta.y = scale_vertical_mouse_autoscroll_delta(event.position.y - bottom);
956− }
966+ let scroll_delta = {
967+ let text_bounds = text_hitbox.bounds;
968+ let mut scroll_delta = gpui::Point::<f32>::default();
969+ let vertical_margin = position_map.line_height.min(text_bounds.size.height / 3.0);
970+ let top = text_bounds.origin.y + vertical_margin;
971+ let bottom = text_bounds.bottom_left().y - vertical_margin;
972+ if event.position.y < top {
973+ scroll_delta.y = -scale_vertical_mouse_autoscroll_delta(top - event.position.y);
974+ }
975+ if event.position.y > bottom {
976+ scroll_delta.y = scale_vertical_mouse_autoscroll_delta(event.position.y - bottom);
977+ }
957978
958− // We need horizontal width of text
959− let style = editor.style.clone().unwrap_or_default();
960− let font_id = window.text_system().resolve_font(&style.text.font());
961− 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();
979+ // We need horizontal width of text
980+ let style = editor.style.clone().unwrap_or_default();
981+ let font_id = window.text_system().resolve_font(&style.text.font());
982+ let font_size = style.text.font_size.to_pixels(window.rem_size());
983+ let em_width = window.text_system().em_width(font_id, font_size).unwrap();
963984
964− let scroll_margin_x = EditorSettings::get_global(cx).horizontal_scroll_margin;
985+ let scroll_margin_x = EditorSettings::get_global(cx).horizontal_scroll_margin;
965986
966− let scroll_space: Pixels = scroll_margin_x * em_width;
987+ let scroll_space: Pixels = scroll_margin_x * em_width;
967988
968− let left = text_bounds.origin.x + scroll_space;
969− let right = text_bounds.top_right().x - scroll_space;
989+ let left = text_bounds.origin.x + scroll_space;
990+ let right = text_bounds.top_right().x - scroll_space;
970991
971− if event.position.x < left {
972− scroll_delta.x = -scale_horizontal_mouse_autoscroll_delta(left - event.position.x);
973− }
974− if event.position.x > right {
975− scroll_delta.x = scale_horizontal_mouse_autoscroll_delta(event.position.x - right);
976− }
992+ if event.position.x < left {
993+ scroll_delta.x = -scale_horizontal_mouse_autoscroll_delta(left - event.position.x);
994+ }
995+ if event.position.x > right {
996+ scroll_delta.x = scale_horizontal_mouse_autoscroll_delta(event.position.x - right);
997+ }
998+ scroll_delta
999+ };
9771000
9781001 if !editor.has_pending_selection() {
9791002 let drop_anchor = position_map
@@ -982,28 +1005,60 @@ impl EditorElement { · 2 unmodified lines
9821005 match editor.selection_drag_state {
9831006 SelectionDragState::Dragging {
9841007 ref mut drop_cursor,
1008+ ref mut hide_drop_cursor,
9851009 ..
9861010 } => {
9871011 drop_cursor.start = drop_anchor;
9881012 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();
9891016 }
990− SelectionDragState::ReadyToDrag { ref selection, .. } => {
991− let drop_cursor = Selection {
992− id: post_inc(&mut editor.selections.next_selection_id),
993− start: drop_anchor,
994− end: drop_anchor,
995− reversed: false,
996− goal: SelectionGoal::None,
997− };
998− editor.selection_drag_state = SelectionDragState::Dragging {
999− selection: selection.clone(),
1000− drop_cursor,
1001− };
1017+ SelectionDragState::ReadyToDrag {
1018+ ref selection,
1019+ ref click_position,
1020+ ref mouse_down_time,
1021+ } => {
1022+ if mouse_down_time.elapsed() >= SELECTION_DRAG_DELAY {
1023+ let drop_cursor = Selection {
1024+ id: post_inc(&mut editor.selections.next_selection_id),
1025+ start: drop_anchor,
1026+ end: drop_anchor,
1027+ reversed: false,
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+ }
10021059 }
10031060 _ => {}
10041061 }
1005− editor.apply_scroll_delta(scroll_delta, window, cx);
1006− cx.notify();
10071062 } else {
10081063 editor.select(
10091064 SelectPhase::Update {
@@ -1027,17 +1082,70 @@ impl EditorElement { · 17 unmodified lines
10271082 let text_hitbox = &position_map.text_hitbox;
10281083 let gutter_hitbox = &position_map.gutter_hitbox;
10291084 let modifiers = event.modifiers;
1085+ let text_hovered = text_hitbox.is_hovered(window);
10301086 let gutter_hovered = gutter_hitbox.is_hovered(window);
10311087 editor.set_gutter_hovered(gutter_hovered, cx);
10321088 editor.mouse_cursor_hidden = false;
10331089
1034− if gutter_hovered {
1035− let new_point = position_map
1036− .point_for_position(event.position)
1037− .previous_valid;
1090+ let point_for_position = position_map.point_for_position(event.position);
1091+ let valid_point = point_for_position.previous_valid;
1092+
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 {
10381146 let buffer_anchor = position_map
10391147 .snapshot
1040− .display_point_to_anchor(new_point, Bias::Left);
1148+ .display_point_to_anchor(valid_point, Bias::Left);
10411149
10421150 if let Some((buffer_snapshot, file)) = position_map
10431151 .snapshot
@@ -1045,7 +1153,6 @@ impl EditorElement { · 1 unmodified line
10451153 .buffer_for_excerpt(buffer_anchor.excerpt_id)
10461154 .and_then(|buffer| buffer.file().map(|file| (buffer, file)))
10471155 {
1048− let was_hovered = editor.gutter_breakpoint_indicator.0.is_some();
10491156 let as_point = text::ToPoint::to_point(&buffer_anchor.text_anchor, buffer_snapshot);
10501157
10511158 let is_visible = editor
@@ -1073,43 +1180,46 @@ impl EditorElement { · 21 unmodified lines
10731180 .is_some()
10741181 });
10751182
1076− editor.gutter_breakpoint_indicator.0 = Some(PhantomBreakpointIndicator {
1077− display_row: new_point.row(),
1078− is_active: is_visible,
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 {
1183+ if !is_visible {
1184+ editor.gutter_breakpoint_indicator.1.get_or_insert_with(|| {
1185+ cx.spawn(async move |this, cx| {
10851186 cx.background_executor()
10861187 .timer(Duration::from_millis(200))
10871188 .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− }
10941189
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();
10961198 })
1097− .ok();
1098− })
1099− });
1199+ });
1200+ }
1201+
1202+ Some(PhantomBreakpointIndicator {
1203+ display_row: valid_point.row(),
1204+ is_active: is_visible,
1205+ collides_with_existing_breakpoint: has_existing_breakpoint,
1206+ })
11001207 } else {
1101− editor.gutter_breakpoint_indicator = (None, None);
1208+ editor.gutter_breakpoint_indicator.1 = None;
1209+ None
11021210 }
11031211 } else {
1104− editor.gutter_breakpoint_indicator = (None, None);
1105− }
1212+ editor.gutter_breakpoint_indicator.1 = None;
1213+ None
1214+ };
11061215
1107− cx.notify();
1216+ if &breakpoint_indicator != &editor.gutter_breakpoint_indicator.0 {
1217+ editor.gutter_breakpoint_indicator.0 = breakpoint_indicator;
1218+ cx.notify();
1219+ }
11081220
11091221 // Don't trigger hover popover if mouse is hovering over context menu
1110− if text_hitbox.is_hovered(window) {
1111− let point_for_position = position_map.point_for_position(event.position);
1112−
1222+ if text_hovered {
11131223 editor.update_hovered_link(
11141224 point_for_position,
11151225 &position_map.snapshot,
@@ -1247,16 +1357,18 @@ impl EditorElement { · 131 unmodified lines
12471357 if let SelectionDragState::Dragging {
12481358 ref selection,
12491359 ref drop_cursor,
1360+ ref hide_drop_cursor,
12501361 } = editor.selection_drag_state
12511362 {
1252− if drop_cursor
1253− .start
1254− .cmp(&selection.start, &snapshot.buffer_snapshot)
1255− .eq(&Ordering::Less)
1256− || drop_cursor
1257− .end
1258− .cmp(&selection.end, &snapshot.buffer_snapshot)
1259− .eq(&Ordering::Greater)
1363+ if !hide_drop_cursor
1364+ && (drop_cursor
1365+ .start
1366+ .cmp(&selection.start, &snapshot.buffer_snapshot)
1367+ .eq(&Ordering::Less)
1368+ || drop_cursor
1369+ .end
1370+ .cmp(&selection.end, &snapshot.buffer_snapshot)
1371+ .eq(&Ordering::Greater))
12601372 {
12611373 let drag_cursor_layout = SelectionLayout::new(
12621374 drop_cursor.clone(),
@@ -1451,7 +1563,7 @@ impl EditorElement { · 188 unmodified lines
14511563 snapshot
14521564 .grapheme_at(cursor_position)
14531565 .or_else(|| {
1454− if cursor_column == 0 {
1566+ if snapshot.is_empty() {
14551567 snapshot.placeholder_text().and_then(|s| {
14561568 s.graphemes(true).next().map(|s| s.to_string().into())
14571569 })
@@ -2230,7 +2342,7 @@ impl EditorElement { · 772 unmodified lines
22302342 text_hitbox: &Hitbox,
22312343 window: &mut Window,
22322344 cx: &mut App,
2233− ) -> Option<AnyElement> {
2345+ ) -> Option<InlineBlameLayout> {
22342346 if !self
22352347 .editor
22362348 .update(cx, |editor, cx| editor.render_git_blame_inline(window, cx))
@@ -2259,13 +2371,13 @@ impl EditorElement { · 22 unmodified lines
22592371 padding * em_width
22602372 };
22612373
2262− let blame_entry = blame
2374+ let entry = blame
22632375 .update(cx, |blame, cx| {
22642376 blame.blame_for_rows(&[*row_info], cx).next()
22652377 })
22662378 .flatten()?;
22672379
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)?;
22692381
22702382 let start_y = content_origin.y
22712383 + line_height * (display_row.as_f32() - scroll_pixel_position.y / line_height);
@@ -2294,24 +2406,19 @@ impl EditorElement { · 22 unmodified lines
22942406 let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
22952407 let bounds = Bounds::new(absolute_offset, size);
22962408
2297− self.layout_blame_entry_popover(
2298− bounds,
2299− blame_entry,
2300− blame,
2301− line_height,
2302− text_hitbox,
2303− window,
2304− cx,
2305− );
2409+ self.layout_blame_entry_popover(entry.clone(), blame, line_height, text_hitbox, window, cx);
23062410
23072411 element.prepaint_as_root(absolute_offset, AvailableSpace::min_size(), window, cx);
23082412
2309− Some(element)
2413+ Some(InlineBlameLayout {
2414+ element,
2415+ bounds,
2416+ entry,
2417+ })
23102418 }
23112419
23122420 fn layout_blame_entry_popover(
23132421 &self,
2314− parent_bounds: Bounds<Pixels>,
23152422 blame_entry: BlameEntry,
23162423 blame: Entity<GitBlame>,
23172424 line_height: Pixels,
@@ -2319,91 +2426,59 @@ impl EditorElement { · 1 unmodified line
23192426 window: &mut Window,
23202427 cx: &mut App,
23212428 ) {
2322− let mouse_position = window.mouse_position();
2323− let mouse_over_inline_blame = parent_bounds.contains(&mouse_position);
2324− let mouse_over_popover = self.editor.read_with(cx, |editor, _| {
2429+ let Some((popover_state, target_point)) = self.editor.read_with(cx, |editor, _| {
23252430 editor
23262431 .inline_blame_popover
23272432 .as_ref()
2328− .and_then(|state| state.popover_bounds)
2329− .map_or(false, |bounds| bounds.contains(&mouse_position))
2330− });
2331−
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− });
2433+ .map(|state| (state.popover_state.clone(), state.position))
2434+ }) else {
2435+ return;
2436+ };
23392437
2340− let should_draw = self.editor.read_with(cx, |editor, _| {
2341− editor
2342− .inline_blame_popover
2343− .as_ref()
2344− .map_or(false, |state| state.show_task.is_none())
2438+ let workspace = self
2439+ .editor
2440+ .read_with(cx, |editor, _| editor.workspace().map(|w| w.downgrade()));
2441+
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+ )
23452453 });
23462454
2347− if should_draw {
2348− let maybe_element = self.editor.update(cx, |editor, cx| {
2349− editor
2350− .workspace()
2351− .map(|workspace| workspace.downgrade())
2352− .zip(
2353− editor
2354− .inline_blame_popover
2355− .as_ref()
2356− .map(|p| p.popover_state.clone()),
2357− )
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− };
2455+ if let Some(mut element) = maybe_element {
2456+ let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
2457+ let overall_height = size.height + HOVER_POPOVER_GAP;
2458+ let popover_origin = if target_point.y > overall_height {
2459+ point(target_point.x, target_point.y - size.height)
2460+ } else {
2461+ point(
2462+ target_point.x,
2463+ target_point.y + line_height + HOVER_POPOVER_GAP,
2464+ )
2465+ };
23892466
2390− let horizontal_offset = (text_hitbox.top_right().x
2391− - POPOVER_RIGHT_OFFSET
2392− - (popover_origin.x + size.width))
2393− .min(Pixels::ZERO);
2467+ let horizontal_offset = (text_hitbox.top_right().x
2468+ - POPOVER_RIGHT_OFFSET
2469+ - (popover_origin.x + size.width))
2470+ .min(Pixels::ZERO);
23942471
2395− point(popover_origin.x + horizontal_offset, popover_origin.y)
2396− });
2472+ let origin = point(popover_origin.x + horizontal_offset, popover_origin.y);
2473+ let popover_bounds = Bounds::new(origin, size);
23972474
2398− let popover_bounds = Bounds::new(origin, size);
2399− self.editor.update(cx, |editor, _| {
2400− if let Some(state) = &mut editor.inline_blame_popover {
2401− state.popover_bounds = Some(popover_bounds);
2402− }
2403− });
2475+ self.editor.update(cx, |editor, _| {
2476+ if let Some(state) = &mut editor.inline_blame_popover {
2477+ state.popover_bounds = Some(popover_bounds);
2478+ }
2479+ });
24042480
2405− window.defer_draw(element, origin, 2);
2406− }
2481+ window.defer_draw(element, origin, 2);
24072482 }
24082483 }
24092484
@@ -2812,7 +2887,8 @@ impl EditorElement { · 402 unmodified lines
28122887 let available_width = gutter_dimensions.left_padding - git_gutter_width;
28132888
28142889 let editor = self.editor.clone();
2815− let is_wide = max_line_number_length >= MIN_LINE_NUMBER_DIGITS
2890+ let is_wide = max_line_number_length
2891+ >= EditorSettings::get_global(cx).gutter.min_line_number_digits as u32
28162892 && row_info
28172893 .buffer_row
28182894 .is_some_and(|row| (row + 1).ilog10() + 1 == max_line_number_length)
@@ -4730,7 +4806,6 @@ impl EditorElement { · 1911 unmodified lines
47304806 row_range: Range<DisplayRow>,
47314807 row_infos: &[RowInfo],
47324808 text_hitbox: &Hitbox,
4733− position_map: &PositionMap,
47344809 newest_cursor_position: Option<DisplayPoint>,
47354810 line_height: Pixels,
47364811 right_margin: Pixels,
@@ -4740,14 +4815,15 @@ impl EditorElement { · 3 unmodified lines
47404815 editor: Entity<Editor>,
47414816 window: &mut Window,
47424817 cx: &mut App,
4743− ) -> Vec<AnyElement> {
4818+ ) -> (Vec<AnyElement>, Vec<(DisplayRow, Bounds<Pixels>)>) {
47444819 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;
47464821
47474822 let mut controls = vec![];
4823+ let mut control_bounds = vec![];
47484824
47494825 let active_positions = [
4750− Some(point_for_position.previous_valid),
4826+ hovered_diff_hunk_row.map(|row| DisplayPoint::new(row, 0)),
47514827 newest_cursor_position,
47524828 ];
47534829
@@ -4792,6 +4868,7 @@ impl EditorElement { · 38 unmodified lines
47924868 {
47934869 continue;
47944870 }
4871+
47954872 if active_positions
47964873 .iter()
47974874 .any(|p| p.map_or(false, |p| display_row_range.contains(&p.row())))
@@ -4815,6 +4892,9 @@ impl EditorElement { · 17 unmodified lines
48154892
48164893 let x = text_hitbox.bounds.right() - right_margin - px(10.) - size.width;
48174894
4895+ let bounds = Bounds::new(gpui::Point::new(x, y), size);
4896+ control_bounds.push((display_row_range.start, bounds));
4897+
48184898 window.with_absolute_element_offset(gpui::Point::new(x, y), |window| {
48194899 element.prepaint(window, cx)
48204900 });
@@ -4823,7 +4903,7 @@ impl EditorElement { · 2 unmodified lines
48234903 }
48244904 }
48254905
4826− controls
4906+ (controls, control_bounds)
48274907 }
48284908
48294909 fn layout_signature_help(
@@ -5562,6 +5642,12 @@ impl EditorElement { · 732 unmodified lines
55625642 let editor = self.editor.read(cx);
55635643 if editor.mouse_cursor_hidden {
55645644 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);
55655651 } else if editor
55665652 .hovered_link_state
55675653 .as_ref()
@@ -6203,9 +6289,9 @@ impl EditorElement { · 635 unmodified lines
62036289 }
62046290
62056291 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() {
62076293 window.paint_layer(layout.position_map.text_hitbox.bounds, |window| {
6208− inline_blame.paint(window, cx);
6294+ blame_layout.element.paint(window, cx);
62096295 })
62106296 }
62116297 }
@@ -7325,6 +7411,17 @@ impl LineWithInvisibles { · 1113 unmodified lines
73257411 paint(window, cx);
73267412 }),
73277413
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+
73287425 // For a whitespace to be on a boundary, any of the following conditions need to be met:
73297426 // - It is a tab
73307427 // - It is adjacent to an edge (start or end)
@@ -7537,7 +7634,7 @@ impl Element for EditorElement { · 206 unmodified lines
75377634 fn request_layout(
75387635 &mut self,
75397636 _: Option<&GlobalElementId>,
7540− __inspector_id: Option<&gpui::InspectorElementId>,
7637+ _inspector_id: Option<&gpui::InspectorElementId>,
75417638 window: &mut Window,
75427639 cx: &mut App,
75437640 ) -> (gpui::LayoutId, ()) {
@@ -7588,7 +7685,10 @@ impl Element for EditorElement { · 44 unmodified lines
75887685 window.request_layout(style, None, cx)
75897686 }
75907687 }
7591− EditorMode::AutoHeight { max_lines } => {
7688+ EditorMode::AutoHeight {
7689+ min_lines,
7690+ max_lines,
7691+ } => {
75927692 let editor_handle = cx.entity().clone();
75937693 let max_line_number_width =
75947694 self.max_line_number_width(&editor.snapshot(window, cx), window, cx);
@@ -7599,6 +7699,7 @@ impl Element for EditorElement { · 4 unmodified lines
75997699 .update(cx, |editor, cx| {
76007700 compute_auto_height_layout(
76017701 editor,
7702+ min_lines,
76027703 max_lines,
76037704 max_line_number_width,
76047705 known_dimensions,
@@ -7673,8 +7774,7 @@ impl Element for EditorElement { · 68 unmodified lines
76737774 let line_height = style.text.line_height_in_pixels(window.rem_size());
76747775 let em_width = window.text_system().em_width(font_id, font_size).unwrap();
76757776 let em_advance = window.text_system().em_advance(font_id, font_size).unwrap();
7676−
7677− let glyph_grid_cell = size(em_width, line_height);
7777+ let glyph_grid_cell = size(em_advance, line_height);
76787778
76797779 let gutter_dimensions = snapshot
76807780 .gutter_dimensions(
@@ -8233,7 +8333,7 @@ impl Element for EditorElement { · 552 unmodified lines
82338333 MultiBufferRow(end_anchor.to_point(&snapshot.buffer_snapshot).row);
82348334
82358335 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),
82378337 max_scroll_top,
82388338 );
82398339
@@ -8245,7 +8345,7 @@ impl Element for EditorElement { · 5 unmodified lines
82458345 start_row,
82468346 editor_content_width,
82478347 scroll_width,
8248− em_width,
8348+ em_advance,
82498349 &line_layouts,
82508350 cx,
82518351 )
@@ -8260,10 +8360,9 @@ impl Element for EditorElement { · 8 unmodified lines
82608360 });
82618361
82628362 let scroll_pixel_position = point(
8263− scroll_position.x * em_width,
8363+ scroll_position.x * em_advance,
82648364 scroll_position.y * line_height,
82658365 );
8266−
82678366 let indent_guides = self.layout_indent_guides(
82688367 content_origin,
82698368 text_hitbox.origin,
@@ -8328,7 +8427,7 @@ impl Element for EditorElement { · 58 unmodified lines
83288427 cx,
83298428 );
83308429
8331− let mut inline_blame = None;
8430+ let mut inline_blame_layout = None;
83328431 let mut inline_code_actions = None;
83338432 if let Some(newest_selection_head) = newest_selection_head {
83348433 let display_row = newest_selection_head.row();
@@ -8350,7 +8449,7 @@ impl Element for EditorElement { · 15 unmodified lines
83508449 let line_layout = &line_layouts[line_ix];
83518450 let crease_trailer_layout = crease_trailers[line_ix].as_ref();
83528451
8353− inline_blame = self.layout_inline_blame(
8452+ if let Some(layout) = self.layout_inline_blame(
83548453 display_row,
83558454 row_info,
83568455 line_layout,
@@ -8362,8 +8461,8 @@ impl Element for EditorElement { · 5 unmodified lines
83628461 &text_hitbox,
83638462 window,
83648463 cx,
8365− );
8366− if inline_blame.is_some() {
8464+ ) {
8465+ inline_blame_layout = Some(layout);
83678466 // Blame overrides inline diagnostics
83688467 inline_diagnostics.remove(&display_row);
83698468 }
@@ -8633,6 +8732,25 @@ impl Element for EditorElement { · 263 unmodified lines
86338732
86348733 let mode = snapshot.mode.clone();
86358734
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+
86368754 let position_map = Rc::new(PositionMap {
86378755 size: bounds.size,
86388756 visible_row_range,
@@ -8645,32 +8763,17 @@ impl Element for EditorElement { · 6 unmodified lines
86458763 snapshot,
86468764 gutter_hitbox: gutter_hitbox.clone(),
86478765 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(),
86488771 });
86498772
86508773 self.editor.update(cx, |editor, _| {
86518774 editor.last_position_map = Some(position_map.clone())
86528775 });
86538776
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−
86748777 EditorLayout {
86758778 mode,
86768779 position_map,
@@ -8692,7 +8795,7 @@ impl Element for EditorElement { · 15 unmodified lines
86928795 line_numbers,
86938796 blamed_display_rows,
86948797 inline_diagnostics,
8695− inline_blame,
8798+ inline_blame_layout,
86968799 inline_code_actions,
86978800 blocks,
86988801 cursors,
@@ -8718,7 +8821,7 @@ impl Element for EditorElement { · 19 unmodified lines
87188821 fn paint(
87198822 &mut self,
87208823 _: Option<&GlobalElementId>,
8721− __inspector_id: Option<&gpui::InspectorElementId>,
8824+ _inspector_id: Option<&gpui::InspectorElementId>,
87228825 bounds: Bounds<gpui::Pixels>,
87238826 _: &mut Self::RequestLayoutState,
87248827 layout: &mut Self::PrepaintState,
@@ -8873,7 +8976,7 @@ pub struct EditorLayout { · 148 unmodified lines
88738976 display_hunks: Vec<(DisplayDiffHunk, Option<Hitbox>)>,
88748977 blamed_display_rows: Option<Vec<AnyElement>>,
88758978 inline_diagnostics: HashMap<DisplayRow, AnyElement>,
8876− inline_blame: Option<AnyElement>,
8979+ inline_blame_layout: Option<InlineBlameLayout>,
88778980 inline_code_actions: Option<AnyElement>,
88788981 blocks: Vec<BlockLayout>,
88798982 highlighted_ranges: Vec<(Range<DisplayPoint>, Hsla)>,
@@ -9333,6 +9436,9 @@ pub(crate) struct PositionMap { · 453 unmodified lines
93339436 pub snapshot: EditorSnapshot,
93349437 pub text_hitbox: Hitbox,
93359438 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>)>,
93369442 }
93379443
93389444 #[derive(Debug, Copy, Clone)]
@@ -9388,7 +9494,7 @@ impl PositionMap { · 49 unmodified lines
93889494 let scroll_position = self.snapshot.scroll_position();
93899495 let position = position - text_bounds.origin;
93909496 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);
93929498 let row = ((y / self.line_height) + scroll_position.y) as u32;
93939499
93949500 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
97669872
97679873 fn compute_auto_height_layout(
97689874 editor: &mut Editor,
9875+ min_lines: usize,
97699876 max_lines: usize,
97709877 max_line_number_width: Pixels,
97719878 known_dimensions: Size<Option<Pixels>>,
@@ -9813,7 +9920,7 @@ fn compute_auto_height_layout( · 41 unmodified lines
98139920
98149921 let scroll_height = (snapshot.max_point().row().next_row().0 as f32) * line_height;
98159922 let height = scroll_height
9816− .max(line_height)
9923+ .max(line_height * min_lines as f32)
98179924 .min(line_height * max_lines as f32);
98189925
98199926 Some(size(width, height))
@@ -10116,7 +10223,10 @@ mod tests { · 296 unmodified lines
1011610223
1011710224 for editor_mode_without_invisibles in [
1011810225 EditorMode::SingleLine { auto_width: false },
10119− EditorMode::AutoHeight { max_lines: 100 },
10226+ EditorMode::AutoHeight {
10227+ min_lines: 1,
10228+ max_lines: 100,
10229+ },
1012010230 ] {
1012110231 for show_line_numbers in [true, false] {
1012210232 let invisibles = collect_invisibles_from_new_editor(
crates/editor/src/items.rs+43−34
@@ -1,6 +1,7 @@
11 use crate::{
22 Anchor, Autoscroll, Editor, EditorEvent, EditorSettings, ExcerptId, ExcerptRange, FormatTarget,
3− MultiBuffer, MultiBufferSnapshot, NavigationData, SearchWithinRange, ToPoint as _,
3+ MultiBuffer, MultiBufferSnapshot, NavigationData, SearchWithinRange, SelectionEffects,
4+ ToPoint as _,
45 editor_settings::SeedQuerySetting,
56 persistence::{DB, SerializedEditor},
67 scroll::ScrollAnchor,
@@ -40,7 +41,7 @@ use ui::{IconDecorationKind, prelude::*}; · 33 unmodified lines
4041 use util::{ResultExt, TryFutureExt, paths::PathExt};
4142 use workspace::{
4243 CollaboratorId, ItemId, ItemNavHistory, ToolbarItemLocation, ViewId, Workspace, WorkspaceId,
43− item::{FollowableItem, Item, ItemEvent, ProjectItem},
44+ item::{FollowableItem, Item, ItemEvent, ProjectItem, SaveOptions},
4445 searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle},
4546 };
4647 use workspace::{
@@ -611,12 +612,13 @@ impl Item for Editor { · 564 unmodified lines
611612 if newest_selection.head() == offset {
612613 false
613614 } else {
614− let nav_history = self.nav_history.take();
615615 self.set_scroll_anchor(scroll_anchor, window, cx);
616− self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
617− s.select_ranges([offset..offset])
618− });
619− self.nav_history = nav_history;
616+ self.change_selections(
617+ SelectionEffects::default().nav_history(false),
618+ window,
619+ cx,
620+ |s| s.select_ranges([offset..offset]),
621+ );
620622 true
621623 }
622624 } else {
@@ -805,7 +807,7 @@ impl Item for Editor { · 182 unmodified lines
805807
806808 fn save(
807809 &mut self,
808− format: bool,
810+ options: SaveOptions,
809811 project: Entity<Project>,
810812 window: &mut Window,
811813 cx: &mut Context<Self>,
@@ -816,13 +818,25 @@ impl Item for Editor { · 4 unmodified lines
816818 .into_iter()
817819 .map(|handle| handle.read(cx).base_buffer().unwrap_or(handle.clone()))
818820 .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+
819833 cx.spawn_in(window, async move |this, cx| {
820− if format {
834+ if options.format {
821835 this.update_in(cx, |editor, window, cx| {
822836 editor.perform_format(
823837 project.clone(),
824838 FormatTrigger::Save,
825− FormatTarget::Buffers,
839+ FormatTarget::Buffers(buffers_to_save.clone()),
826840 window,
827841 cx,
828842 )
@@ -830,33 +844,28 @@ impl Item for Editor { · 1 unmodified line
830844 .await?;
831845 }
832846
833− if buffers.len() == 1 {
834− // Apply full save routine for singleton buffers, to allow to `touch` the file via the editor.
847+ if !buffers_to_save.is_empty() {
835848 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+ })?
837852 .await?;
838− } else {
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− });
853+ }
847854
848− project
849− .update(cx, |project, cx| project.save_buffers(dirty_buffers, cx))?
850− .await?;
851− for buffer in clean_buffers {
852− buffer
853− .update(cx, |buffer, cx| {
854− let version = buffer.saved_version().clone();
855− let mtime = buffer.saved_mtime();
856− buffer.did_save(version, mtime, cx);
857− })
858− .ok();
859− }
855+ // Notify about clean buffers for language server events
856+ let buffers_that_were_not_saved: Vec<_> = buffers
857+ .into_iter()
858+ .filter(|b| !buffers_to_save.contains(b))
859+ .collect();
860+
861+ for buffer in buffers_that_were_not_saved {
862+ buffer
863+ .update(cx, |buffer, cx| {
864+ let version = buffer.saved_version().clone();
865+ let mtime = buffer.saved_mtime();
866+ buffer.did_save(version, mtime, cx);
867+ })
868+ .ok();
860869 }
861870
862871 Ok(())
crates/editor/src/jsx_tag_auto_close.rs+9−4
@@ -8,7 +8,7 @@ use util::ResultExt as _; · 7 unmodified lines
88 use language::{BufferSnapshot, JsxTagAutoCloseConfig, Node};
99 use text::{Anchor, OffsetRangeExt as _};
1010
11−use crate::Editor;
11+use crate::{Editor, SelectionEffects};
1212
1313 pub struct JsxTagCompletionState {
1414 edit_index: usize,
@@ -600,9 +600,14 @@ pub(crate) fn handle_from( · 585 unmodified lines
600600 })
601601 .collect::<Vec<_>>();
602602 this.update_in(cx, |this, window, cx| {
603− this.change_selections_without_updating_completions(None, window, cx, |s| {
604− s.select(base_selections);
605− });
603+ this.change_selections(
604+ SelectionEffects::no_scroll().completions(false),
605+ window,
606+ cx,
607+ |s| {
608+ s.select(base_selections);
609+ },
610+ );
606611 })
607612 .ok()?;
608613 }
crates/editor/src/mouse_context_menu.rs+6−9
@@ -1,8 +1,8 @@
11 use crate::{
2− Copy, CopyAndTrim, CopyPermalinkToLine, Cut, DebuggerEvaluateSelectedText, DisplayPoint,
3− DisplaySnapshot, Editor, FindAllReferences, GoToDeclaration, GoToDefinition,
4− GoToImplementation, GoToTypeDefinition, Paste, Rename, RevealInFileManager, SelectMode,
5− SelectionExt, ToDisplayPoint, ToggleCodeActions,
2+ Copy, CopyAndTrim, CopyPermalinkToLine, Cut, DisplayPoint, DisplaySnapshot, Editor,
3+ EvaluateSelectedText, FindAllReferences, GoToDeclaration, GoToDefinition, GoToImplementation,
4+ GoToTypeDefinition, Paste, Rename, RevealInFileManager, SelectMode, SelectionExt,
5+ ToDisplayPoint, ToggleCodeActions,
66 actions::{Format, FormatSelections},
77 selections_collection::SelectionsCollection,
88 };
@@ -199,17 +199,14 @@ pub fn deploy_context_menu( · 190 unmodified lines
199199 .is_some()
200200 });
201201
202− let evaluate_selection = command_palette_hooks::CommandPaletteFilter::try_global(cx)
203− .map_or(false, |filter| {
204− !filter.is_hidden(&DebuggerEvaluateSelectedText)
205− });
202+ let evaluate_selection = window.is_action_available(&EvaluateSelectedText, cx);
206203
207204 ui::ContextMenu::build(window, cx, |menu, _window, _cx| {
208205 let builder = menu
209206 .on_blur_subscription(Subscription::new(|| {}))
210207 .when(evaluate_selection && has_selections, |builder| {
211208 builder
212− .action("Evaluate Selection", Box::new(DebuggerEvaluateSelectedText))
209+ .action("Evaluate Selection", Box::new(EvaluateSelectedText))
213210 .separator()
214211 })
215212 .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
5858 map.clip_point(point, Bias::Left)
5959 }
6060
61−/// Returns a column to the right of the current point, doing nothing
62−// if that point is at the end of the line.
61+/// Returns a column to the right of the current point, wrapping
62+/// to the next line if that point is at the end of line.
6363 pub fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
6464 if point.column() < map.line_len(point.row()) {
6565 *point.column_mut() += 1;
@@ -266,10 +266,11 @@ pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> Displa · 200 unmodified lines
266266
267267 let mut is_first_iteration = true;
268268 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.
270270 if is_first_iteration
271271 && classifier.is_punctuation(right)
272272 && !classifier.is_punctuation(left)
273+ && left != '\n'
273274 {
274275 is_first_iteration = false;
275276 return false;
@@ -318,10 +319,11 @@ pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint · 42 unmodified lines
318319 let classifier = map.buffer_snapshot.char_classifier_at(raw_point);
319320 let mut is_first_iteration = true;
320321 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|
322323 if is_first_iteration
323324 && classifier.is_punctuation(left)
324325 && !classifier.is_punctuation(right)
326+ && right != '\n'
325327 {
326328 is_first_iteration = false;
327329 return false;
crates/editor/src/proposed_changes_editor.rs+3−3
@@ -12,7 +12,7 @@ use text::ToOffset; · 11 unmodified lines
1212 use ui::{ButtonLike, KeyBinding, prelude::*};
1313 use workspace::{
1414 Item, ItemHandle as _, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
15− searchable::SearchableItemHandle,
15+ item::SaveOptions, searchable::SearchableItemHandle,
1616 };
1717
1818 pub struct ProposedChangesEditor {
@@ -351,13 +351,13 @@ impl Item for ProposedChangesEditor { · 332 unmodified lines
351351
352352 fn save(
353353 &mut self,
354− format: bool,
354+ options: SaveOptions,
355355 project: Entity<Project>,
356356 window: &mut Window,
357357 cx: &mut Context<Self>,
358358 ) -> Task<anyhow::Result<()>> {
359359 self.editor.update(cx, |editor, cx| {
360− Item::save(editor, format, project, window, cx)
360+ Item::save(editor, options, project, window, cx)
361361 })
362362 }
363363 }
crates/editor/src/scroll.rs+14−3
@@ -669,12 +669,23 @@ impl Editor { · 668 unmodified lines
669669 return;
670670 }
671671
672− let cur_position = self.scroll_position(cx);
672+ let mut current_position = self.scroll_position(cx);
673673 let Some(visible_line_count) = self.visible_line_count() else {
674674 return;
675675 };
676− let new_pos = cur_position + point(0., amount.lines(visible_line_count));
677− self.set_scroll_position(new_pos, window, cx);
676+
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);
678689 }
679690
680691 /// Returns an ordering. The newest selection is:
crates/editor/src/scroll/autoscroll.rs+13−3
@@ -1,12 +1,13 @@
11 use crate::{
2− DisplayRow, Editor, EditorMode, LineWithInvisibles, RowExt, display_map::ToDisplayPoint,
2+ DisplayRow, Editor, EditorMode, LineWithInvisibles, RowExt, SelectionEffects,
3+ display_map::ToDisplayPoint,
34 };
45 use gpui::{Bounds, Context, Pixels, Window, px};
56 use language::Point;
67 use multi_buffer::Anchor;
78 use std::{cmp, f32};
89
9−#[derive(PartialEq, Eq, Clone, Copy)]
10+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
1011 pub enum Autoscroll {
1112 Next,
1213 Strategy(AutoscrollStrategy, Option<Anchor>),
@@ -66,7 +67,16 @@ impl Autoscroll { · 53 unmodified lines
6667 }
6768 }
6869
69−#[derive(PartialEq, Eq, Default, Clone, Copy)]
70+impl 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)]
7080 pub enum AutoscrollStrategy {
7181 Fit,
7282 Newest,
crates/editor/src/scroll/scroll_amount.rs+21−0
@@ -5,6 +5,8 @@ use ui::{Pixels, px}; · 4 unmodified lines
55 pub enum ScrollDirection {
66 Upwards,
77 Downwards,
8+ Rightwards,
9+ Leftwards,
810 }
911
1012 impl ScrollDirection {
@@ -19,6 +21,8 @@ pub enum ScrollAmount { · 8 unmodified lines
1921 Line(f32),
2022 // Scroll N pages (positive is towards the end of the document)
2123 Page(f32),
24+ // Scroll N columns (positive is towards the right of the document)
25+ Column(f32),
2226 }
2327
2428 impl ScrollAmount {
@@ -32,6 +36,15 @@ impl ScrollAmount { · 7 unmodified lines
3236 }
3337 (visible_line_count * count).trunc()
3438 }
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,
3548 }
3649 }
3750
@@ -39,6 +52,12 @@ impl ScrollAmount { · 1 unmodified line
3952 match self {
4053 ScrollAmount::Line(x) => px(line_height.0 * x),
4154 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),
4261 }
4362 }
4463
@@ -53,6 +72,8 @@ impl ScrollAmount { · 8 unmodified lines
5372 match self {
5473 Self::Line(amount) if amount.is_sign_positive() => ScrollDirection::Downwards,
5574 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,
5677 _ => ScrollDirection::Upwards,
5778 }
5879 }
crates/editor/src/selections_collection.rs+2−1
@@ -411,7 +411,7 @@ impl<'a> MutableSelectionsCollection<'a> { · 410 unmodified lines
411411 self.collection.display_map(self.cx)
412412 }
413413
414− pub fn buffer(&self) -> Ref<MultiBufferSnapshot> {
414+ pub fn buffer(&self) -> Ref<'_, MultiBufferSnapshot> {
415415 self.collection.buffer(self.cx)
416416 }
417417
@@ -659,6 +659,7 @@ impl<'a> MutableSelectionsCollection<'a> { · 241 unmodified lines
659659 .collect();
660660 self.select(selections);
661661 }
662+
662663 pub fn reverse_selections(&mut self) {
663664 let map = &self.display_map();
664665 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
169169 .expect("Opened test file wasn't an editor")
170170 });
171171 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));
172178 window.focus(&editor.focus_handle(cx))
173179 });
174180
crates/extension/src/extension_events.rs+1−0
@@ -36,6 +36,7 @@ impl ExtensionEvents { · 35 unmodified lines
3636 #[derive(Clone)]
3737 pub enum Event {
3838 ExtensionInstalled(Arc<ExtensionManifest>),
39+ ExtensionUninstalled(Arc<ExtensionManifest>),
3940 ExtensionsInstalledChanged,
4041 ConfigureExtensionRequested(Arc<ExtensionManifest>),
4142 }
crates/extension_api/wit/since_v0.6.0/dap.wit+1−1
@@ -50,7 +50,7 @@ interface dap { · 49 unmodified lines
5050 }
5151
5252 record debug-adapter-binary {
53− command: string,
53+ command: option<string>,
5454 arguments: list<string>,
5555 envs: env-vars,
5656 cwd: option<string>,
crates/extension_host/src/extension_host.rs+21−4
@@ -132,6 +132,7 @@ pub enum Event { · 131 unmodified lines
132132 ExtensionsUpdated,
133133 StartedReloading,
134134 ExtensionInstalled(Arc<str>),
135+ ExtensionUninstalled(Arc<str>),
135136 ExtensionFailedToLoad(Arc<str>),
136137 }
137138
@@ -842,6 +843,8 @@ impl ExtensionStore { · 704 unmodified lines
842843 let work_dir = self.wasm_host.work_dir.join(extension_id.as_ref());
843844 let fs = self.fs.clone();
844845
846+ let extension_manifest = self.extension_manifest_for_id(&extension_id).cloned();
847+
845848 match self.outstanding_operations.entry(extension_id.clone()) {
846849 btree_map::Entry::Occupied(_) => return,
847850 btree_map::Entry::Vacant(e) => e.insert(ExtensionOperation::Remove),
@@ -878,6 +881,17 @@ impl ExtensionStore { · 30 unmodified lines
878881 )
879882 .await?;
880883
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+
881895 anyhow::Ok(())
882896 })
883897 .detach_and_log_err(cx)
@@ -1682,12 +1696,15 @@ impl ExtensionStore { · 798 unmodified lines
16821696
16831697 pub fn register_ssh_client(&mut self, client: Entity<SshRemoteClient>, cx: &mut Context<Self>) {
16841698 let connection_options = client.read(cx).connection_options();
1685− if self.ssh_clients.contains_key(&connection_options.ssh_url()) {
1686− return;
1699+ let ssh_url = connection_options.ssh_url();
1700+
1701+ if let Some(existing_client) = self.ssh_clients.get(&ssh_url) {
1702+ if existing_client.upgrade().is_some() {
1703+ return;
1704+ }
16871705 }
16881706
1689− self.ssh_clients
1690− .insert(connection_options.ssh_url(), client.downgrade());
1707+ self.ssh_clients.insert(ssh_url, client.downgrade());
16911708 self.ssh_registered_tx.unbounded_send(()).ok();
16921709 }
16931710 }
crates/extension_host/src/extension_store_test.rs+14−5
@@ -8,9 +8,9 @@ use collections::BTreeMap; · 7 unmodified lines
88 use extension::ExtensionHostProxy;
99 use fs::{FakeFs, Fs, RealFs};
1010 use futures::{AsyncReadExt, StreamExt, io::BufReader};
11−use gpui::{AppContext as _, SemanticVersion, SharedString, TestAppContext};
11+use gpui::{AppContext as _, SemanticVersion, TestAppContext};
1212 use http_client::{FakeHttpClient, Response};
13−use language::{BinaryStatus, LanguageMatcher, LanguageRegistry};
13+use language::{BinaryStatus, LanguageMatcher, LanguageRegistry, LanguageServerStatusUpdate};
1414 use lsp::LanguageServerName;
1515 use node_runtime::NodeRuntime;
1616 use parking_lot::Mutex;
@@ -717,9 +717,18 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) { · 700 unmodified lines
717717 status_updates.next().await.unwrap(),
718718 ],
719719 [
720− (SharedString::new("gleam"), BinaryStatus::CheckingForUpdate),
721− (SharedString::new("gleam"), BinaryStatus::Downloading),
722− (SharedString::new("gleam"), BinaryStatus::None)
720+ (
721+ LanguageServerName::new_static("gleam"),
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+ )
723732 ]
724733 );
725734
crates/extension_host/src/wasm_host.rs+2−1
@@ -377,6 +377,7 @@ impl extension::Extension for WasmExtension { · 376 unmodified lines
377377 })
378378 .await
379379 }
380+
380381 async fn get_dap_binary(
381382 &self,
382383 dap_name: Arc<str>,
@@ -724,7 +725,7 @@ impl IncrementalCompilationCache { · 341 unmodified lines
724725 }
725726
726727 impl CacheStore for IncrementalCompilationCache {
727− fn get(&self, key: &[u8]) -> Option<Cow<[u8]>> {
728+ fn get(&self, key: &[u8]) -> Option<Cow<'_, [u8]>> {
728729 self.cache.get(key).map(|v| v.into())
729730 }
730731
crates/extensions_ui/src/extensions_ui.rs+14−22
@@ -554,13 +554,15 @@ impl ExtensionsPage { · 553 unmodified lines
554554 )
555555 .child(
556556 h_flex()
557− .gap_2()
557+ .gap_1()
558558 .justify_between()
559559 .child(
560560 Button::new(
561561 SharedString::from(format!("rebuild-{}", extension.id)),
562562 "Rebuild",
563563 )
564+ .color(Color::Accent)
565+ .disabled(matches!(status, ExtensionStatus::Upgrading))
564566 .on_click({
565567 let extension_id = extension.id.clone();
566568 move |_, _, cx| {
@@ -568,12 +570,12 @@ impl ExtensionsPage { · 1 unmodified line
568570 store.rebuild_dev_extension(extension_id.clone(), cx)
569571 });
570572 }
571− })
572− .color(Color::Accent)
573− .disabled(matches!(status, ExtensionStatus::Upgrading)),
573+ }),
574574 )
575575 .child(
576576 Button::new(SharedString::from(extension.id.clone()), "Uninstall")
577+ .color(Color::Accent)
578+ .disabled(matches!(status, ExtensionStatus::Removing))
577579 .on_click({
578580 let extension_id = extension.id.clone();
579581 move |_, _, cx| {
@@ -581,9 +583,7 @@ impl ExtensionsPage { · 1 unmodified line
581583 store.uninstall_extension(extension_id.clone(), cx)
582584 });
583585 }
584− })
585− .color(Color::Accent)
586− .disabled(matches!(status, ExtensionStatus::Removing)),
586+ }),
587587 )
588588 .when(can_configure, |this| {
589589 this.child(
@@ -591,8 +591,8 @@ impl ExtensionsPage { · 1 unmodified line
591591 SharedString::from(format!("configure-{}", extension.id)),
592592 "Configure",
593593 )
594−
595−
594+ .color(Color::Accent)
595+ .disabled(matches!(status, ExtensionStatus::Installing))
596596 .on_click({
597597 let manifest = Arc::new(extension.clone());
598598 move |_, _, cx| {
@@ -609,9 +609,7 @@ impl ExtensionsPage { · 10 unmodified lines
609609 });
610610 }
611611 }
612− })
613− .color(Color::Accent)
614− .disabled(matches!(status, ExtensionStatus::Installing)),
612+ }),
615613 )
616614 }),
617615 ),
@@ -1479,18 +1477,12 @@ impl Render for ExtensionsPage { · 861 unmodified lines
14791477 return this.py_4().child(self.render_empty_state(cx));
14801478 }
14811479
1482− let extensions_page = cx.entity().clone();
14831480 let scroll_handle = self.list.clone();
14841481 this.child(
1485− uniform_list(
1486− extensions_page,
1487− "entries",
1488− count,
1489− Self::render_extensions,
1490− )
1491− .flex_grow()
1492− .pb_4()
1493− .track_scroll(scroll_handle),
1482+ uniform_list("entries", count, cx.processor(Self::render_extensions))
1483+ .flex_grow()
1484+ .pb_4()
1485+ .track_scroll(scroll_handle),
14941486 )
14951487 .child(
14961488 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
209209 "bandana",
210210 "./bandana",
211211 ".\\bandana",
212− util::separator!("a/bandana"),
212+ util::path!("a/bandana"),
213213 "b/bandana",
214214 "b\\bandana",
215215 " bandana",
@@ -230,7 +230,7 @@ async fn test_matching_paths(cx: &mut TestAppContext) { · 14 unmodified lines
230230 assert_eq!(
231231 picker.delegate.matches.len(),
232232 // 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") {
234234 1
235235 } else {
236236 2
crates/fs/src/fs.rs+15−0
@@ -897,6 +897,7 @@ struct FakeFsState { · 896 unmodified lines
897897 buffered_events: Vec<PathEvent>,
898898 metadata_call_count: usize,
899899 read_dir_call_count: usize,
900+ path_write_counts: std::collections::HashMap<PathBuf, usize>,
900901 moves: std::collections::HashMap<u64, PathBuf>,
901902 home_dir: Option<PathBuf>,
902903 }
@@ -1083,6 +1084,7 @@ impl FakeFs { · 180 unmodified lines
10831084 events_paused: false,
10841085 read_dir_call_count: 0,
10851086 metadata_call_count: 0,
1087+ path_write_counts: Default::default(),
10861088 moves: Default::default(),
10871089 home_dir: None,
10881090 })),
@@ -1173,6 +1175,8 @@ impl FakeFs { · 84 unmodified lines
11731175 recreate_inode: bool,
11741176 ) -> Result<()> {
11751177 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;
11761180 let new_inode = state.get_and_increment_inode();
11771181 let new_mtime = state.get_and_increment_mtime();
11781182 let new_len = new_content.len() as u64;
@@ -1727,6 +1731,17 @@ impl FakeFs { · 548 unmodified lines
17271731 self.state.lock().metadata_call_count
17281732 }
17291733
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+
17301745 fn simulate_random_delay(&self) -> impl futures::Future<Output = ()> {
17311746 self.executor.simulate_random_delay()
17321747 }
crates/git/src/git.rs+1−0
@@ -57,6 +57,7 @@ actions!( · 56 unmodified lines
5757 ExpandCommitEditor,
5858 GenerateCommitMessage,
5959 Init,
60+ OpenModifiedFiles,
6061 ]
6162 );
6263
crates/git/src/repository.rs+72−28
@@ -26,8 +26,8 @@ use std::{ · 25 unmodified lines
2626 };
2727 use sum_tree::MapSeekTarget;
2828 use thiserror::Error;
29−use util::ResultExt;
3029 use util::command::{new_smol_command, new_std_command};
30+use util::{ResultExt, paths};
3131 use uuid::Uuid;
3232
3333 pub use askpass::{AskPassDelegate, AskPassResult, AskPassSession};
@@ -323,7 +323,7 @@ pub trait GitRepository: Send + Sync { · 289 unmodified lines
323323 /// Resolve a list of refs to SHAs.
324324 fn revparse_batch(&self, revs: Vec<String>) -> BoxFuture<Result<Vec<Option<String>>>>;
325325
326− fn head_sha(&self) -> BoxFuture<Option<String>> {
326+ fn head_sha(&self) -> BoxFuture<'_, Option<String>> {
327327 async move {
328328 self.revparse_batch(vec!["HEAD".into()])
329329 .await
@@ -508,6 +508,50 @@ pub struct GitRepositoryCheckpoint { · 178 unmodified lines
508508 pub commit_sha: Oid,
509509 }
510510
511+#[derive(Debug)]
512+pub struct GitCommitter {
513+ pub name: Option<String>,
514+ pub email: Option<String>,
515+}
516+
517+pub 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+
511555 impl GitRepository for RealGitRepository {
512556 fn reload_index(&self) {
513557 if let Ok(mut index) = self.repository.lock().index() {
@@ -525,7 +569,7 @@ impl GitRepository for RealGitRepository { · 11 unmodified lines
525569 repo.commondir().into()
526570 }
527571
528− fn show(&self, commit: String) -> BoxFuture<Result<CommitDetails>> {
572+ fn show(&self, commit: String) -> BoxFuture<'_, Result<CommitDetails>> {
529573 let working_directory = self.working_directory();
530574 self.executor
531575 .spawn(async move {
@@ -561,7 +605,7 @@ impl GitRepository for RealGitRepository { · 29 unmodified lines
561605 .boxed()
562606 }
563607
564− fn load_commit(&self, commit: String, cx: AsyncApp) -> BoxFuture<Result<CommitDiff>> {
608+ fn load_commit(&self, commit: String, cx: AsyncApp) -> BoxFuture<'_, Result<CommitDiff>> {
565609 let Some(working_directory) = self.repository.lock().workdir().map(ToOwned::to_owned)
566610 else {
567611 return future::ready(Err(anyhow!("no working directory"))).boxed();
@@ -668,7 +712,7 @@ impl GitRepository for RealGitRepository { · 100 unmodified lines
668712 commit: String,
669713 mode: ResetMode,
670714 env: Arc<HashMap<String, String>>,
671− ) -> BoxFuture<Result<()>> {
715+ ) -> BoxFuture<'_, Result<()>> {
672716 async move {
673717 let working_directory = self.working_directory();
674718
@@ -698,7 +742,7 @@ impl GitRepository for RealGitRepository { · 23 unmodified lines
698742 commit: String,
699743 paths: Vec<RepoPath>,
700744 env: Arc<HashMap<String, String>>,
701− ) -> BoxFuture<Result<()>> {
745+ ) -> BoxFuture<'_, Result<()>> {
702746 let working_directory = self.working_directory();
703747 let git_binary_path = self.git_binary_path.clone();
704748 async move {
@@ -723,7 +767,7 @@ impl GitRepository for RealGitRepository { · 18 unmodified lines
723767 .boxed()
724768 }
725769
726− fn load_index_text(&self, path: RepoPath) -> BoxFuture<Option<String>> {
770+ fn load_index_text(&self, path: RepoPath) -> BoxFuture<'_, Option<String>> {
727771 // https://git-scm.com/book/en/v2/Git-Internals-Git-Objects
728772 const GIT_MODE_SYMLINK: u32 = 0o120000;
729773
@@ -756,7 +800,7 @@ impl GitRepository for RealGitRepository { · 26 unmodified lines
756800 .boxed()
757801 }
758802
759− fn load_committed_text(&self, path: RepoPath) -> BoxFuture<Option<String>> {
803+ fn load_committed_text(&self, path: RepoPath) -> BoxFuture<'_, Option<String>> {
760804 let repo = self.repository.clone();
761805 self.executor
762806 .spawn(async move {
@@ -777,7 +821,7 @@ impl GitRepository for RealGitRepository { · 14 unmodified lines
777821 path: RepoPath,
778822 content: Option<String>,
779823 env: Arc<HashMap<String, String>>,
780− ) -> BoxFuture<anyhow::Result<()>> {
824+ ) -> BoxFuture<'_, anyhow::Result<()>> {
781825 let working_directory = self.working_directory();
782826 let git_binary_path = self.git_binary_path.clone();
783827 self.executor
@@ -841,7 +885,7 @@ impl GitRepository for RealGitRepository { · 57 unmodified lines
841885 remote.url().map(|url| url.to_string())
842886 }
843887
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>>>> {
845889 let working_directory = self.working_directory();
846890 self.executor
847891 .spawn(async move {
@@ -891,14 +935,14 @@ impl GitRepository for RealGitRepository { · 43 unmodified lines
891935 .boxed()
892936 }
893937
894− fn merge_message(&self) -> BoxFuture<Option<String>> {
938+ fn merge_message(&self) -> BoxFuture<'_, Option<String>> {
895939 let path = self.path().join("MERGE_MSG");
896940 self.executor
897941 .spawn(async move { std::fs::read_to_string(&path).ok() })
898942 .boxed()
899943 }
900944
901− fn status(&self, path_prefixes: &[RepoPath]) -> BoxFuture<Result<GitStatus>> {
945+ fn status(&self, path_prefixes: &[RepoPath]) -> BoxFuture<'_, Result<GitStatus>> {
902946 let git_binary_path = self.git_binary_path.clone();
903947 let working_directory = self.working_directory();
904948 let path_prefixes = path_prefixes.to_owned();
@@ -919,7 +963,7 @@ impl GitRepository for RealGitRepository { · 14 unmodified lines
919963 .boxed()
920964 }
921965
922− fn branches(&self) -> BoxFuture<Result<Vec<Branch>>> {
966+ fn branches(&self) -> BoxFuture<'_, Result<Vec<Branch>>> {
923967 let working_directory = self.working_directory();
924968 let git_binary_path = self.git_binary_path.clone();
925969 self.executor
@@ -986,7 +1030,7 @@ impl GitRepository for RealGitRepository { · 60 unmodified lines
9861030 .boxed()
9871031 }
9881032
989− fn change_branch(&self, name: String) -> BoxFuture<Result<()>> {
1033+ fn change_branch(&self, name: String) -> BoxFuture<'_, Result<()>> {
9901034 let repo = self.repository.clone();
9911035 self.executor
9921036 .spawn(async move {
@@ -1018,7 +1062,7 @@ impl GitRepository for RealGitRepository { · 25 unmodified lines
10181062 .boxed()
10191063 }
10201064
1021− fn create_branch(&self, name: String) -> BoxFuture<Result<()>> {
1065+ fn create_branch(&self, name: String) -> BoxFuture<'_, Result<()>> {
10221066 let repo = self.repository.clone();
10231067 self.executor
10241068 .spawn(async move {
@@ -1030,7 +1074,7 @@ impl GitRepository for RealGitRepository { · 5 unmodified lines
10301074 .boxed()
10311075 }
10321076
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>> {
10341078 let working_directory = self.working_directory();
10351079 let git_binary_path = self.git_binary_path.clone();
10361080
@@ -1052,7 +1096,7 @@ impl GitRepository for RealGitRepository { · 15 unmodified lines
10521096 .boxed()
10531097 }
10541098
1055− fn diff(&self, diff: DiffType) -> BoxFuture<Result<String>> {
1099+ fn diff(&self, diff: DiffType) -> BoxFuture<'_, Result<String>> {
10561100 let working_directory = self.working_directory();
10571101 let git_binary_path = self.git_binary_path.clone();
10581102 self.executor
@@ -1083,7 +1127,7 @@ impl GitRepository for RealGitRepository { · 24 unmodified lines
10831127 &self,
10841128 paths: Vec<RepoPath>,
10851129 env: Arc<HashMap<String, String>>,
1086− ) -> BoxFuture<Result<()>> {
1130+ ) -> BoxFuture<'_, Result<()>> {
10871131 let working_directory = self.working_directory();
10881132 let git_binary_path = self.git_binary_path.clone();
10891133 self.executor
@@ -1111,7 +1155,7 @@ impl GitRepository for RealGitRepository { · 21 unmodified lines
11111155 &self,
11121156 paths: Vec<RepoPath>,
11131157 env: Arc<HashMap<String, String>>,
1114− ) -> BoxFuture<Result<()>> {
1158+ ) -> BoxFuture<'_, Result<()>> {
11151159 let working_directory = self.working_directory();
11161160 let git_binary_path = self.git_binary_path.clone();
11171161
@@ -1143,7 +1187,7 @@ impl GitRepository for RealGitRepository { · 25 unmodified lines
11431187 name_and_email: Option<(SharedString, SharedString)>,
11441188 options: CommitOptions,
11451189 env: Arc<HashMap<String, String>>,
1146− ) -> BoxFuture<Result<()>> {
1190+ ) -> BoxFuture<'_, Result<()>> {
11471191 let working_directory = self.working_directory();
11481192 self.executor
11491193 .spawn(async move {
@@ -1182,7 +1226,7 @@ impl GitRepository for RealGitRepository { · 32 unmodified lines
11821226 ask_pass: AskPassDelegate,
11831227 env: Arc<HashMap<String, String>>,
11841228 cx: AsyncApp,
1185− ) -> BoxFuture<Result<RemoteCommandOutput>> {
1229+ ) -> BoxFuture<'_, Result<RemoteCommandOutput>> {
11861230 let working_directory = self.working_directory();
11871231 let executor = cx.background_executor().clone();
11881232 async move {
@@ -1214,7 +1258,7 @@ impl GitRepository for RealGitRepository { · 25 unmodified lines
12141258 ask_pass: AskPassDelegate,
12151259 env: Arc<HashMap<String, String>>,
12161260 cx: AsyncApp,
1217− ) -> BoxFuture<Result<RemoteCommandOutput>> {
1261+ ) -> BoxFuture<'_, Result<RemoteCommandOutput>> {
12181262 let working_directory = self.working_directory();
12191263 let executor = cx.background_executor().clone();
12201264 async move {
@@ -1239,7 +1283,7 @@ impl GitRepository for RealGitRepository { · 18 unmodified lines
12391283 ask_pass: AskPassDelegate,
12401284 env: Arc<HashMap<String, String>>,
12411285 cx: AsyncApp,
1242− ) -> BoxFuture<Result<RemoteCommandOutput>> {
1286+ ) -> BoxFuture<'_, Result<RemoteCommandOutput>> {
12431287 let working_directory = self.working_directory();
12441288 let remote_name = format!("{}", fetch_options);
12451289 let executor = cx.background_executor().clone();
@@ -1257,7 +1301,7 @@ impl GitRepository for RealGitRepository { · 11 unmodified lines
12571301 .boxed()
12581302 }
12591303
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>>> {
12611305 let working_directory = self.working_directory();
12621306 let git_binary_path = self.git_binary_path.clone();
12631307 self.executor
@@ -1303,7 +1347,7 @@ impl GitRepository for RealGitRepository { · 39 unmodified lines
13031347 .boxed()
13041348 }
13051349
1306− fn check_for_pushed_commit(&self) -> BoxFuture<Result<Vec<SharedString>>> {
1350+ fn check_for_pushed_commit(&self) -> BoxFuture<'_, Result<Vec<SharedString>>> {
13071351 let working_directory = self.working_directory();
13081352 let git_binary_path = self.git_binary_path.clone();
13091353 self.executor
@@ -1396,7 +1440,7 @@ impl GitRepository for RealGitRepository { · 86 unmodified lines
13961440 .boxed()
13971441 }
13981442
1399− fn restore_checkpoint(&self, checkpoint: GitRepositoryCheckpoint) -> BoxFuture<Result<()>> {
1443+ fn restore_checkpoint(&self, checkpoint: GitRepositoryCheckpoint) -> BoxFuture<'_, Result<()>> {
14001444 let working_directory = self.working_directory();
14011445 let git_binary_path = self.git_binary_path.clone();
14021446
@@ -1435,7 +1479,7 @@ impl GitRepository for RealGitRepository { · 32 unmodified lines
14351479 &self,
14361480 left: GitRepositoryCheckpoint,
14371481 right: GitRepositoryCheckpoint,
1438− ) -> BoxFuture<Result<bool>> {
1482+ ) -> BoxFuture<'_, Result<bool>> {
14391483 let working_directory = self.working_directory();
14401484 let git_binary_path = self.git_binary_path.clone();
14411485
@@ -1474,7 +1518,7 @@ impl GitRepository for RealGitRepository { · 32 unmodified lines
14741518 &self,
14751519 base_checkpoint: GitRepositoryCheckpoint,
14761520 target_checkpoint: GitRepositoryCheckpoint,
1477− ) -> BoxFuture<Result<String>> {
1521+ ) -> BoxFuture<'_, Result<String>> {
14781522 let working_directory = self.working_directory();
14791523 let git_binary_path = self.git_binary_path.clone();
14801524
crates/git_ui/Cargo.toml+1−0
@@ -21,6 +21,7 @@ agent_settings.workspace = true · 20 unmodified lines
2121 anyhow.workspace = true
2222 askpass.workspace = true
2323 buffer_diff.workspace = true
24+call.workspace = true
2425 chrono.workspace = true
2526 collections.workspace = true
2627 command_palette_hooks.workspace = true
crates/git_ui/src/commit_modal.rs+1−0
@@ -148,6 +148,7 @@ impl CommitModal { · 147 unmodified lines
148148 }
149149 }
150150 git_panel.set_modal_open(true, cx);
151+ git_panel.load_local_committer(cx);
151152 });
152153
153154 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
248248 removed_block_ids.insert(block_id);
249249 }
250250
251+ editor.remove_gutter_highlights::<ConflictsOuter>(removed_highlighted_ranges.clone(), cx);
252+
251253 editor.remove_highlighted_rows::<ConflictsOuter>(removed_highlighted_ranges.clone(), cx);
252254 editor.remove_highlighted_rows::<ConflictsOurs>(removed_highlighted_ranges.clone(), cx);
253255 editor
@@ -325,8 +327,7 @@ fn update_conflict_highlighting( · 71 unmodified lines
325327 cx: &mut Context<Editor>,
326328 ) {
327329 log::debug!("update conflict highlighting for {conflict:?}");
328− let theme = cx.theme().clone();
329− let colors = theme.colors();
330+
330331 let outer_start = buffer
331332 .anchor_in_excerpt(excerpt_id, conflict.range.start)
332333 .unwrap();
@@ -346,26 +347,29 @@ fn update_conflict_highlighting( · 13 unmodified lines
346347 .anchor_in_excerpt(excerpt_id, conflict.theirs.end)
347348 .unwrap();
348349
349− let ours_background = colors.version_control_conflict_ours_background;
350− let ours_marker = colors.version_control_conflict_ours_marker_background;
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;
350+ let ours_background = cx.theme().colors().version_control_conflict_marker_ours;
351+ let theirs_background = cx.theme().colors().version_control_conflict_marker_theirs;
354352
355353 let options = RowHighlightOptions {
356− include_gutter: false,
354+ include_gutter: true,
357355 ..Default::default()
358356 };
359357
358+ editor.insert_gutter_highlight::<ConflictsOuter>(
359+ outer_start..their_end,
360+ |cx| cx.theme().colors().editor_background,
361+ cx,
362+ );
363+
360364 // Prevent diff hunk highlighting within the entire conflict region.
361− editor.highlight_rows::<ConflictsOuter>(
362− outer_start..outer_end,
363− divider_background,
365+ editor.highlight_rows::<ConflictsOuter>(outer_start..outer_end, theirs_background, options, cx);
366+ editor.highlight_rows::<ConflictsOurs>(our_start..our_end, ours_background, options, cx);
367+ editor.highlight_rows::<ConflictsOursMarker>(
368+ outer_start..our_start,
369+ ours_background,
364370 options,
365371 cx,
366372 );
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);
369373 editor.highlight_rows::<ConflictsTheirs>(
370374 their_start..their_end,
371375 theirs_background,
@@ -374,7 +378,7 @@ fn update_conflict_highlighting( · 2 unmodified lines
374378 );
375379 editor.highlight_rows::<ConflictsTheirsMarker>(
376380 their_end..outer_end,
377− theirs_marker,
381+ theirs_background,
378382 options,
379383 cx,
380384 );
@@ -512,6 +516,9 @@ pub(crate) fn resolve_conflict( · 131 unmodified lines
512516 let end = snapshot
513517 .anchor_in_excerpt(excerpt_id, resolved_conflict.range.end)
514518 .unwrap();
519+
520+ editor.remove_gutter_highlights::<ConflictsOuter>(vec![start..end], cx);
521+
515522 editor.remove_highlighted_rows::<ConflictsOuter>(vec![start..end], cx);
516523 editor.remove_highlighted_rows::<ConflictsOurs>(vec![start..end], cx);
517524 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
2020 use futures::StreamExt as _;
2121 use git::blame::ParsedCommitMessage;
2222 use git::repository::{
23− Branch, CommitDetails, CommitOptions, CommitSummary, DiffType, FetchOptions, PushOptions,
24− Remote, RemoteCommandOutput, ResetMode, Upstream, UpstreamTracking, UpstreamTrackingStatus,
23+ Branch, CommitDetails, CommitOptions, CommitSummary, DiffType, FetchOptions, GitCommitter,
24+ PushOptions, Remote, RemoteCommandOutput, ResetMode, Upstream, UpstreamTracking,
25+ UpstreamTrackingStatus, get_git_committer,
2526 };
2627 use git::status::StageStatus;
2728 use git::{Amend, ToggleStaged, repository::RepoPath, status::FileStatus};
2829 use git::{ExpandCommitEditor, RestoreTrackedFiles, StageAll, TrashUntrackedFiles, UnstageAll};
2930 use gpui::{
30− Action, Animation, AnimationExt as _, Axis, ClickEvent, Corner, DismissEvent, Entity,
31− EventEmitter, FocusHandle, Focusable, KeyContext, ListHorizontalSizingBehavior,
32− ListSizingBehavior, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, Point,
33− PromptLevel, ScrollStrategy, Subscription, Task, Transformation, UniformListScrollHandle,
34− WeakEntity, actions, anchored, deferred, percentage, uniform_list,
31+ Action, Animation, AnimationExt as _, AsyncApp, AsyncWindowContext, Axis, ClickEvent, Corner,
32+ DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, KeyContext,
33+ ListHorizontalSizingBehavior, ListSizingBehavior, Modifiers, ModifiersChangedEvent,
34+ MouseButton, MouseDownEvent, Point, PromptLevel, ScrollStrategy, Subscription, Task,
35+ Transformation, UniformListScrollHandle, WeakEntity, actions, anchored, deferred, percentage,
36+ uniform_list,
3537 };
3638 use itertools::Itertools;
3739 use language::{Buffer, File};
@@ -54,6 +56,7 @@ use project::{ · 16 unmodified lines
5456 use serde::{Deserialize, Serialize};
5557 use settings::{Settings as _, SettingsStore};
5658 use std::future::Future;
59+use std::ops::Range;
5760 use std::path::{Path, PathBuf};
5861 use std::{collections::HashSet, sync::Arc, time::Duration, usize};
5962 use strum::{IntoEnumIterator, VariantNames};
@@ -63,11 +66,11 @@ use ui::{ · 3 unmodified lines
6366 Tooltip, prelude::*,
6467 };
6568 use util::{ResultExt, TryFutureExt, maybe};
66−use workspace::AppState;
69+
6770 use workspace::{
6871 Workspace,
6972 dock::{DockPosition, Panel, PanelEvent},
70− notifications::DetachAndPromptErr,
73+ notifications::{DetachAndPromptErr, ErrorMessagePrompt, NotificationId},
7174 };
7275 use zed_llm_client::CompletionIntent;
7376
@@ -356,6 +359,8 @@ pub struct GitPanel { · 282 unmodified lines
356359 context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
357360 modal_open: bool,
358361 show_placeholders: bool,
362+ local_committer: Option<GitCommitter>,
363+ local_committer_task: Option<Task<()>>,
359364 _settings_subscription: Subscription,
360365 }
361366
@@ -372,7 +377,10 @@ pub(crate) fn commit_message_editor( · 10 unmodified lines
372377 let buffer = cx.new(|cx| MultiBuffer::singleton(commit_message_buffer, cx));
373378 let max_lines = if in_panel { MAX_PANEL_EDITOR_LINES } else { 18 };
374379 let mut commit_editor = Editor::new(
375− EditorMode::AutoHeight { max_lines },
380+ EditorMode::AutoHeight {
381+ min_lines: 1,
382+ max_lines,
383+ },
376384 buffer,
377385 None,
378386 window,
@@ -389,144 +397,150 @@ pub(crate) fn commit_message_editor( · 10 unmodified lines
389397 }
390398
391399 impl GitPanel {
392− pub fn new(
393− workspace: Entity<Workspace>,
394− project: Entity<Project>,
395− app_state: Arc<AppState>,
400+ fn new(
401+ workspace: &mut Workspace,
396402 window: &mut Window,
397− cx: &mut Context<Self>,
398− ) -> Self {
403+ cx: &mut Context<Workspace>,
404+ ) -> Entity<Self> {
405+ let project = workspace.project().clone();
406+ let app_state = workspace.app_state().clone();
399407 let fs = app_state.fs.clone();
400408 let git_store = project.read(cx).git_store().clone();
401409 let active_repository = project.read(cx).active_repository(cx);
402− let workspace = workspace.downgrade();
403410
404− let focus_handle = cx.focus_handle();
405− cx.on_focus(&focus_handle, window, Self::focus_in).detach();
406− cx.on_focus_out(&focus_handle, window, |this, _, window, cx| {
407− this.hide_scrollbars(window, cx);
408− })
409− .detach();
411+ let git_panel = cx.new(|cx| {
412+ let focus_handle = cx.focus_handle();
413+ cx.on_focus(&focus_handle, window, Self::focus_in).detach();
414+ cx.on_focus_out(&focus_handle, window, |this, _, window, cx| {
415+ this.hide_scrollbars(window, cx);
416+ })
417+ .detach();
410418
411− let mut was_sort_by_path = GitPanelSettings::get_global(cx).sort_by_path;
412− cx.observe_global::<SettingsStore>(move |this, cx| {
413− let is_sort_by_path = GitPanelSettings::get_global(cx).sort_by_path;
414− if is_sort_by_path != was_sort_by_path {
415− this.update_visible_entries(cx);
416− }
417− was_sort_by_path = is_sort_by_path
418− })
419− .detach();
419+ let mut was_sort_by_path = GitPanelSettings::get_global(cx).sort_by_path;
420+ cx.observe_global::<SettingsStore>(move |this, cx| {
421+ let is_sort_by_path = GitPanelSettings::get_global(cx).sort_by_path;
422+ if is_sort_by_path != was_sort_by_path {
423+ this.update_visible_entries(cx);
424+ }
425+ was_sort_by_path = is_sort_by_path
426+ })
427+ .detach();
420428
421− // just to let us render a placeholder editor.
422− // Once the active git repo is set, this buffer will be replaced.
423− let temporary_buffer = cx.new(|cx| Buffer::local("", cx));
424− let commit_editor = cx.new(|cx| {
425− commit_message_editor(temporary_buffer, None, project.clone(), true, window, cx)
426− });
429+ // just to let us render a placeholder editor.
430+ // Once the active git repo is set, this buffer will be replaced.
431+ let temporary_buffer = cx.new(|cx| Buffer::local("", cx));
432+ let commit_editor = cx.new(|cx| {
433+ commit_message_editor(temporary_buffer, None, project.clone(), true, window, cx)
434+ });
427435
428− commit_editor.update(cx, |editor, cx| {
429− editor.clear(window, cx);
430− });
436+ commit_editor.update(cx, |editor, cx| {
437+ editor.clear(window, cx);
438+ });
431439
432− let scroll_handle = UniformListScrollHandle::new();
440+ let scroll_handle = UniformListScrollHandle::new();
433441
434− cx.subscribe_in(
435− &git_store,
436− window,
437− move |this, git_store, event, window, cx| match event {
438− GitStoreEvent::ActiveRepositoryChanged(_) => {
439− this.active_repository = git_store.read(cx).active_repository();
440− this.schedule_update(true, window, cx);
441− }
442− GitStoreEvent::RepositoryUpdated(
443− _,
444− RepositoryEvent::Updated { full_scan },
445− true,
446− ) => {
447− this.schedule_update(*full_scan, window, cx);
448− }
442+ let vertical_scrollbar = ScrollbarProperties {
443+ axis: Axis::Vertical,
444+ state: ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity()),
445+ show_scrollbar: false,
446+ show_track: false,
447+ auto_hide: false,
448+ hide_task: None,
449+ };
449450
450− GitStoreEvent::RepositoryAdded(_) | GitStoreEvent::RepositoryRemoved(_) => {
451− this.schedule_update(false, window, cx);
452− }
453− GitStoreEvent::IndexWriteError(error) => {
454− this.workspace
455− .update(cx, |workspace, cx| {
456− workspace.show_error(error, cx);
457− })
458− .ok();
451+ let horizontal_scrollbar = ScrollbarProperties {
452+ axis: Axis::Horizontal,
453+ state: ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity()),
454+ show_scrollbar: false,
455+ show_track: false,
456+ auto_hide: false,
457+ hide_task: None,
458+ };
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();
459465 }
460− GitStoreEvent::RepositoryUpdated(_, _, _) => {}
461− GitStoreEvent::JobsUpdated | GitStoreEvent::ConflictsUpdated => {}
462− },
463− )
464− .detach();
466+ });
465467
466− let vertical_scrollbar = ScrollbarProperties {
467− axis: Axis::Vertical,
468− state: ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity()),
469− show_scrollbar: false,
470− show_track: false,
471− auto_hide: false,
472− hide_task: None,
473− };
468+ cx.subscribe_in(
469+ &git_store,
470+ window,
471+ move |this, _git_store, event, window, cx| match event {
472+ GitStoreEvent::ActiveRepositoryChanged(_) => {
473+ this.active_repository = this.project.read(cx).active_repository(cx);
474+ this.schedule_update(true, window, cx);
475+ }
476+ GitStoreEvent::RepositoryUpdated(
477+ _,
478+ RepositoryEvent::Updated { full_scan },
479+ true,
480+ ) => {
481+ this.schedule_update(*full_scan, window, cx);
482+ }
474483
475− let horizontal_scrollbar = ScrollbarProperties {
476− axis: Axis::Horizontal,
477− state: ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity()),
478− show_scrollbar: false,
479− show_track: false,
480− auto_hide: false,
481− hide_task: None,
482− };
484+ GitStoreEvent::RepositoryAdded(_) | GitStoreEvent::RepositoryRemoved(_) => {
485+ this.schedule_update(false, window, cx);
486+ }
487+ GitStoreEvent::IndexWriteError(error) => {
488+ this.workspace
489+ .update(cx, |workspace, cx| {
490+ workspace.show_error(error, cx);
491+ })
492+ .ok();
493+ }
494+ GitStoreEvent::RepositoryUpdated(_, _, _) => {}
495+ GitStoreEvent::JobsUpdated | GitStoreEvent::ConflictsUpdated => {}
496+ },
497+ )
498+ .detach();
483499
484− let mut assistant_enabled = AgentSettings::get_global(cx).enabled;
485− let _settings_subscription = cx.observe_global::<SettingsStore>(move |_, cx| {
486− if assistant_enabled != AgentSettings::get_global(cx).enabled {
487− assistant_enabled = AgentSettings::get_global(cx).enabled;
488− cx.notify();
489− }
500+ let mut this = Self {
501+ active_repository,
502+ commit_editor,
503+ conflicted_count: 0,
504+ conflicted_staged_count: 0,
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
490542 });
491543
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);
530544 git_panel
531545 }
532546
@@ -1774,7 +1788,19 @@ impl GitPanel { · 1241 unmodified lines
17741788 this.generate_commit_message_task.take();
17751789 });
17761790
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+ };
17781804
17791805 const ONE_MB: usize = 1_000_000;
17801806 if diff_text.len() > ONE_MB {
@@ -1812,26 +1838,37 @@ impl GitPanel { · 31 unmodified lines
18121838 };
18131839
18141840 let stream = model.stream_completion_text(request, &cx);
1815− let mut messages = stream.await?;
1816−
1817− if !text_empty {
1818− this.update(cx, |this, cx| {
1819− this.commit_message_buffer(cx).update(cx, |buffer, cx| {
1820− let insert_position = buffer.anchor_before(buffer.len());
1821− buffer.edit([(insert_position..insert_position, "\n")], None, cx)
1822− });
1823− })?;
1824− }
1825−
1826− while let Some(message) = messages.stream.next().await {
1827− let text = message?;
1841+ match stream.await {
1842+ Ok(mut messages) => {
1843+ if !text_empty {
1844+ this.update(cx, |this, cx| {
1845+ this.commit_message_buffer(cx).update(cx, |buffer, cx| {
1846+ let insert_position = buffer.anchor_before(buffer.len());
1847+ buffer.edit([(insert_position..insert_position, "\n")], None, cx)
1848+ });
1849+ })?;
1850+ }
18281851
1829− this.update(cx, |this, cx| {
1830− this.commit_message_buffer(cx).update(cx, |buffer, cx| {
1831− let insert_position = buffer.anchor_before(buffer.len());
1832− buffer.edit([(insert_position..insert_position, text)], None, cx);
1833− });
1834− })?;
1852+ while let Some(message) = messages.stream.next().await {
1853+ match message {
1854+ Ok(text) => {
1855+ this.update(cx, |this, cx| {
1856+ this.commit_message_buffer(cx).update(cx, |buffer, cx| {
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+ }
18351872 }
18361873
18371874 anyhow::Ok(())
@@ -2221,6 +2258,19 @@ impl GitPanel { · 383 unmodified lines
22212258 }
22222259 }
22232260
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+
22242274 fn potential_co_authors(&self, cx: &App) -> Vec<(String, String)> {
22252275 let mut new_co_authors = Vec::new();
22262276 let project = self.project.read(cx);
@@ -2243,34 +2293,38 @@ impl GitPanel { · 16 unmodified lines
22432293 let Some(participant) = room.remote_participant_for_peer_id(*peer_id) else {
22442294 continue;
22452295 };
2246− if participant.can_write() && participant.user.email.is_some() {
2247− let email = participant.user.email.clone().unwrap();
2248−
2249− new_co_authors.push((
2250− participant
2251− .user
2252− .name
2253− .clone()
2254− .unwrap_or_else(|| participant.user.github_login.clone()),
2255− email,
2256− ))
2296+ if !participant.can_write() {
2297+ continue;
2298+ }
2299+ if let Some(email) = &collaborator.committer_email {
2300+ let name = collaborator
2301+ .committer_name
2302+ .clone()
2303+ .or_else(|| participant.user.name.clone())
2304+ .unwrap_or_else(|| participant.user.github_login.clone());
2305+ new_co_authors.push((name.clone(), email.clone()))
22572306 }
22582307 }
22592308 if !project.is_local() && !project.is_read_only(cx) {
2260− if let Some(user) = room.local_participant_user(cx) {
2261− if let Some(email) = user.email.clone() {
2262− new_co_authors.push((
2263− user.name
2264− .clone()
2265− .unwrap_or_else(|| user.github_login.clone()),
2266− email.clone(),
2267− ))
2268− }
2309+ if let Some(local_committer) = self.local_committer(room, cx) {
2310+ new_co_authors.push(local_committer);
22692311 }
22702312 }
22712313 new_co_authors
22722314 }
22732315
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+
22742328 fn toggle_fill_co_authors(
22752329 &mut self,
22762330 _: &ToggleFillCoAuthors,
@@ -2689,6 +2743,26 @@ impl GitPanel { · 412 unmodified lines
26892743 }
26902744 }
26912745
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+
26922766 fn show_remote_output(&self, action: RemoteAction, info: RemoteCommandOutput, cx: &mut App) {
26932767 let Some(workspace) = self.workspace.upgrade() else {
26942768 return;
@@ -3662,8 +3736,10 @@ impl GitPanel { · 967 unmodified lines
36623736 .relative()
36633737 .overflow_hidden()
36643738 .child(
3665− uniform_list(cx.entity().clone(), "entries", entry_count, {
3666− move |this, range, window, cx| {
3739+ uniform_list(
3740+ "entries",
3741+ entry_count,
3742+ cx.processor(move |this, range: Range<usize>, window, cx| {
36673743 let mut items = Vec::with_capacity(range.end - range.start);
36683744
36693745 for ix in range {
@@ -3691,8 +3767,8 @@ impl GitPanel { · 21 unmodified lines
36913767 }
36923768
36933769 items
3694− }
3695− })
3770+ }),
3771+ )
36963772 .when(
36973773 !self.horizontal_scrollbar.show_track
36983774 && self.horizontal_scrollbar.show_scrollbar,
@@ -4141,6 +4217,32 @@ impl GitPanel { · 442 unmodified lines
41414217 self.amend_pending = value;
41424218 cx.notify();
41434219 }
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+ }
41444246 }
41454247
41464248 fn current_language_model(cx: &Context<'_, GitPanel>) -> Option<Arc<dyn LanguageModel>> {
@@ -4167,8 +4269,9 @@ impl Render for GitPanel { · 20 unmodified lines
41674269 let has_write_access = self.has_write_access(cx);
41684270
41694271 let has_co_authors = room.map_or(false, |room| {
4170− room.read(cx)
4171− .remote_participants()
4272+ self.load_local_committer(cx);
4273+ let room = room.read(cx);
4274+ room.remote_participants()
41724275 .values()
41734276 .any(|remote_participant| remote_participant.can_write())
41744277 });
@@ -4852,7 +4955,7 @@ impl Component for PanelRepoFooter { · 677 unmodified lines
48524955 #[cfg(test)]
48534956 mod tests {
48544957 use git::status::StatusCode;
4855− use gpui::TestAppContext;
4958+ use gpui::{TestAppContext, VisualTestContext};
48564959 use project::{FakeFs, WorktreeSettings};
48574960 use serde_json::json;
48584961 use settings::SettingsStore;
@@ -4916,8 +5019,9 @@ mod tests { · 57 unmodified lines
49165019
49175020 let project =
49185021 Project::test(fs.clone(), [path!("/root/zed/crates/gpui").as_ref()], cx).await;
4919− let (workspace, cx) =
4920− cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
5022+ let workspace =
5023+ cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
5024+ let cx = &mut VisualTestContext::from_window(*workspace, cx);
49215025
49225026 cx.read(|cx| {
49235027 project
@@ -4934,10 +5038,7 @@ mod tests { · 10 unmodified lines
49345038
49355039 cx.executor().run_until_parked();
49365040
4937− let app_state = workspace.read_with(cx, |workspace, _| workspace.app_state().clone());
4938− let panel = cx.new_window_entity(|window, cx| {
4939− GitPanel::new(workspace.clone(), project.clone(), app_state, window, cx)
4940− });
5041+ let panel = workspace.update(cx, GitPanel::new).unwrap();
49415042
49425043 let handle = cx.update_window_entity(&panel, |panel, _, _| {
49435044 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
1010 status::{FileStatus, StatusCode, UnmergedStatus, UnmergedStatusCode},
1111 };
1212 use git_panel_settings::GitPanelSettings;
13−use gpui::{Action, App, FocusHandle, actions};
13+use gpui::{Action, App, Context, FocusHandle, Window, actions};
1414 use onboarding::GitOnboardingModal;
1515 use project_diff::ProjectDiff;
1616 use ui::prelude::*;
@@ -142,10 +142,41 @@ pub fn init(cx: &mut App) { · 125 unmodified lines
142142 panel.git_init(window, cx);
143143 });
144144 });
145+ workspace.register_action(|workspace, _: &git::OpenModifiedFiles, window, cx| {
146+ open_modified_files(workspace, window, cx);
147+ });
145148 })
146149 .detach();
147150 }
148151
152+fn 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+
149180 pub fn git_status_icon(status: FileStatus) -> impl IntoElement {
150181 GitStatusIcon::new(status)
151182 }
crates/git_ui/src/project_diff.rs+12−4
@@ -37,7 +37,7 @@ use util::ResultExt as _; · 36 unmodified lines
3737 use workspace::{
3838 CloseActiveItem, ItemNavHistory, SerializableItem, ToolbarItemEvent, ToolbarItemLocation,
3939 ToolbarItemView, Workspace,
40− item::{BreadcrumbText, Item, ItemEvent, ItemHandle, TabContentParams},
40+ item::{BreadcrumbText, Item, ItemEvent, ItemHandle, SaveOptions, TabContentParams},
4141 searchable::SearchableItemHandle,
4242 };
4343
@@ -632,12 +632,12 @@ impl Item for ProjectDiff { · 588 unmodified lines
632632
633633 fn save(
634634 &mut self,
635− format: bool,
635+ options: SaveOptions,
636636 project: Entity<Project>,
637637 window: &mut Window,
638638 cx: &mut Context<Self>,
639639 ) -> Task<Result<()>> {
640− self.editor.save(format, project, window, cx)
640+ self.editor.save(options, project, window, cx)
641641 }
642642
643643 fn save_as(
@@ -1565,7 +1565,15 @@ mod tests { · 921 unmodified lines
15651565
15661566 cx.update_window_entity(&buffer_editor, |buffer_editor, window, cx| {
15671567 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+ )
15691577 })
15701578 .await
15711579 .unwrap();
crates/gpui/examples/data_table.rs+10−15
@@ -1,8 +1,4 @@
1−use std::{
2− ops::Range,
3− rc::Rc,
4− time::{Duration, Instant},
5−};
1+use std::{ops::Range, rc::Rc, time::Duration};
62
73 use gpui::{
84 App, Application, Bounds, Context, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point,
@@ -22,7 +18,7 @@ pub struct Quote { · 13 unmodified lines
2218 open: f64,
2319 high: f64,
2420 low: f64,
25− timestamp: Instant,
21+ timestamp: Duration,
2622 volume: i64,
2723 turnover: f64,
2824 ttm: f64,
@@ -50,8 +46,7 @@ impl Quote { · 21 unmodified lines
5046 let open = prev_close + rng.gen_range(-3.0..3.0);
5147 let high = (prev_close + rng.gen_range::<f64, _>(0.0..10.0)).max(open);
5248 let low = (prev_close - rng.gen_range::<f64, _>(0.0..10.0)).min(open);
53− // Randomize the timestamp in the past 24 hours
54− let timestamp = Instant::now() - Duration::from_secs(rng.gen_range(0..86400));
49+ let timestamp = Duration::from_secs(rng.gen_range(0..86400));
5550 let volume = rng.gen_range(1_000_000..100_000_000);
5651 let turnover = last_done * volume as f64;
5752 let symbol = {
@@ -170,7 +165,7 @@ impl TableRow { · 112 unmodified lines
170165 .child(format!("{:.2}%", self.quote.change())),
171166 "timestamp" => div()
172167 .text_color(color)
173− .child(format!("{:?}", self.quote.timestamp.elapsed().as_secs())),
168+ .child(format!("{:?}", self.quote.timestamp.as_secs())),
174169 "open" => div()
175170 .text_color(color)
176171 .child(format!("{:.2}", self.quote.open)),
@@ -378,8 +373,6 @@ impl DataTable { · 201 unmodified lines
378373
379374 impl Render for DataTable {
380375 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
381− let entity = cx.entity();
382−
383376 div()
384377 .font_family(".SystemUIFont")
385378 .bg(gpui::white())
@@ -431,8 +424,10 @@ impl Render for DataTable { · 45 unmodified lines
431424 .relative()
432425 .size_full()
433426 .child(
434− uniform_list(entity, "items", self.quotes.len(), {
435− move |this, range, _, _| {
427+ uniform_list(
428+ "items",
429+ self.quotes.len(),
430+ cx.processor(move |this, range: Range<usize>, _, _| {
436431 this.visible_range = range.clone();
437432 let mut items = Vec::with_capacity(range.end - range.start);
438433 for i in range {
@@ -441,8 +436,8 @@ impl Render for DataTable { · 2 unmodified lines
441436 }
442437 }
443438 items
444− }
445− })
439+ }),
440+ )
446441 .size_full()
447442 .track_scroll(self.scroll_handle.clone()),
448443 )
crates/gpui/examples/uniform_list.rs+2−3
@@ -9,10 +9,9 @@ impl Render for UniformListExample { · 8 unmodified lines
99 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1010 div().size_full().bg(rgb(0xffffff)).child(
1111 uniform_list(
12− cx.entity().clone(),
1312 "entries",
1413 50,
15− |_this, range, _window, _cx| {
14+ cx.processor(|_this, range, _window, _cx| {
1615 let mut items = Vec::new();
1716 for ix in range {
1817 let item = ix + 1;
@@ -29,7 +28,7 @@ impl Render for UniformListExample { · 10 unmodified lines
2928 );
3029 }
3130 items
32− },
31+ }),
3332 )
3433 .h_full(),
3534 )
crates/gpui/src/app.rs+3−3
@@ -64,7 +64,7 @@ pub struct AppCell { · 63 unmodified lines
6464 impl AppCell {
6565 #[doc(hidden)]
6666 #[track_caller]
67− pub fn borrow(&self) -> AppRef {
67+ pub fn borrow(&self) -> AppRef<'_> {
6868 if option_env!("TRACK_THREAD_BORROWS").is_some() {
6969 let thread_id = std::thread::current().id();
7070 eprintln!("borrowed {thread_id:?}");
@@ -74,7 +74,7 @@ impl AppCell { · 3 unmodified lines
7474
7575 #[doc(hidden)]
7676 #[track_caller]
77− pub fn borrow_mut(&self) -> AppRefMut {
77+ pub fn borrow_mut(&self) -> AppRefMut<'_> {
7878 if option_env!("TRACK_THREAD_BORROWS").is_some() {
7979 let thread_id = std::thread::current().id();
8080 eprintln!("borrowed {thread_id:?}");
@@ -84,7 +84,7 @@ impl AppCell { · 3 unmodified lines
8484
8585 #[doc(hidden)]
8686 #[track_caller]
87− pub fn try_borrow_mut(&self) -> Result<AppRefMut, BorrowMutError> {
87+ pub fn try_borrow_mut(&self) -> Result<AppRefMut<'_>, BorrowMutError> {
8888 if option_env!("TRACK_THREAD_BORROWS").is_some() {
8989 let thread_id = std::thread::current().id();
9090 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
225225 }
226226 }
227227
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+
228240 /// Run something using this entity and cx, when the returned struct is dropped
229241 pub fn on_drop(
230242 &self,
crates/gpui/src/elements/uniform_list.rs+12−17
@@ -5,10 +5,10 @@ · 4 unmodified lines
55 //! elements with uniform height.
66
77 use crate::{
8− AnyElement, App, AvailableSpace, Bounds, ContentMask, Context, Element, ElementId, Entity,
9− GlobalElementId, Hitbox, InspectorElementId, InteractiveElement, Interactivity, IntoElement,
10− IsZero, LayoutId, ListSizingBehavior, Overflow, Pixels, Render, ScrollHandle, Size,
11− StyleRefinement, Styled, Window, point, size,
8+ AnyElement, App, AvailableSpace, Bounds, ContentMask, Element, ElementId, GlobalElementId,
9+ Hitbox, InspectorElementId, InteractiveElement, Interactivity, IntoElement, IsZero, LayoutId,
10+ ListSizingBehavior, Overflow, Pixels, ScrollHandle, Size, StyleRefinement, Styled, Window,
11+ point, size,
1212 };
1313 use smallvec::SmallVec;
1414 use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
@@ -19,28 +19,23 @@ use super::ListHorizontalSizingBehavior; · 4 unmodified lines
1919 /// When rendered into a container with overflow-y: hidden and a fixed (or max) height,
2020 /// uniform_list will only render the visible subset of items.
2121 #[track_caller]
22−pub fn uniform_list<I, R, V>(
23− view: Entity<V>,
24− id: I,
22+pub fn uniform_list<R>(
23+ id: impl Into<ElementId>,
2524 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>,
2726 ) -> UniformList
2827 where
29− I: Into<ElementId>,
3028 R: IntoElement,
31− V: Render,
3229 {
3330 let id = id.into();
3431 let mut base_style = StyleRefinement::default();
3532 base_style.overflow.y = Some(Overflow::Scroll);
3633
37− let render_range = move |range, window: &mut Window, cx: &mut App| {
38− view.update(cx, |this, cx| {
39− f(this, range, window, cx)
40− .into_iter()
41− .map(|component| component.into_any_element())
42− .collect()
43− })
34+ let render_range = move |range: Range<usize>, window: &mut Window, cx: &mut App| {
35+ f(range, window, cx)
36+ .into_iter()
37+ .map(|component| component.into_any_element())
38+ .collect()
4439 };
4540
4641 UniformList {
crates/gpui/src/gpui.rs+1−1
@@ -255,7 +255,7 @@ pub trait VisualContext: AppContext { · 254 unmodified lines
255255 update: impl FnOnce(&mut T, &mut Window, &mut Context<T>) -> R,
256256 ) -> Self::Result<R>;
257257
258− /// Update a view with the given callback
258+ /// Create a new entity, with access to `Window`.
259259 fn new_window_entity<T: 'static>(
260260 &mut self,
261261 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
142142
143143 #[cfg(target_os = "windows")]
144144 pub(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+ )
146150 }
147151
148152 pub(crate) trait Platform: 'static {
@@ -718,7 +722,7 @@ impl<T> ops::Index<usize> for AtlasTextureList<T> { · 569 unmodified lines
718722
719723 impl<T> AtlasTextureList<T> {
720724 #[allow(unused)]
721− fn drain(&mut self) -> std::vec::Drain<Option<T>> {
725+ fn drain(&mut self) -> std::vec::Drain<'_, Option<T>> {
722726 self.free_list.clear();
723727 self.textures.drain(..)
724728 }
crates/gpui/src/platform/blade/blade_renderer.rs+1−1
@@ -342,7 +342,7 @@ impl BladeRenderer { · 341 unmodified lines
342342 let surface = context
343343 .gpu
344344 .create_surface_configured(window, surface_config)
345− .unwrap();
345+ .map_err(|err| anyhow::anyhow!("Failed to create surface: {err:?}"))?;
346346
347347 let command_encoder = context.gpu.create_command_encoder(gpu::CommandEncoderDesc {
348348 name: "main",
crates/gpui/src/platform/linux/headless/client.rs+1−1
@@ -52,7 +52,7 @@ impl LinuxClient for HeadlessClient { · 51 unmodified lines
5252 }
5353
5454 fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
55− Box::new(LinuxKeyboardLayout::new("unknown".to_string()))
55+ Box::new(LinuxKeyboardLayout::new("unknown".into()))
5656 }
5757
5858 fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
crates/gpui/src/platform/linux/keyboard.rs+7−6
@@ -1,21 +1,22 @@
1−use crate::PlatformKeyboardLayout;
1+use crate::{PlatformKeyboardLayout, SharedString};
22
3+#[derive(Clone)]
34 pub(crate) struct LinuxKeyboardLayout {
4− id: String,
5+ name: SharedString,
56 }
67
78 impl PlatformKeyboardLayout for LinuxKeyboardLayout {
89 fn id(&self) -> &str {
9− &self.id
10+ &self.name
1011 }
1112
1213 fn name(&self) -> &str {
13− &self.id
14+ &self.name
1415 }
1516 }
1617
1718 impl LinuxKeyboardLayout {
18− pub(crate) fn new(id: String) -> Self {
19− Self { id }
19+ pub(crate) fn new(name: SharedString) -> Self {
20+ Self { name }
2021 }
2122 }
crates/gpui/src/platform/linux/wayland/client.rs+61−46
@@ -71,17 +71,6 @@ use super::{ · 70 unmodified lines
7171 window::{ImeInput, WaylandWindowStatePtr},
7272 };
7373
74−use 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−};
8574 use crate::platform::{PlatformWindow, blade::BladeContext};
8675 use crate::{
8776 AnyWindowHandle, Bounds, CursorStyle, DOUBLE_CLICK_INTERVAL, DevicePixels, DisplayId,
@@ -91,10 +80,26 @@ use crate::{ · 3 unmodified lines
9180 PlatformInput, PlatformKeyboardLayout, Point, SCROLL_LINES, ScaledPixels, ScreenCaptureSource,
9281 ScrollDelta, ScrollWheelEvent, Size, TouchPhase, WindowParams, point, px, size,
9382 };
83+use 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+};
9497
9598 /// Used to convert evdev scancode to xkb scancode
9699 const MIN_KEYCODE: u32 = 8;
97100
101+const UNKNOWN_KEYBOARD_LAYOUT_NAME: SharedString = SharedString::new_static("unknown");
102+
98103 #[derive(Clone)]
99104 pub struct Globals {
100105 pub qh: QueueHandle<WaylandClientStatePtr>,
@@ -205,6 +210,7 @@ pub(crate) struct WaylandClientState { · 104 unmodified lines
205210 // Output to scale mapping
206211 outputs: HashMap<ObjectId, Output>,
207212 in_progress_outputs: HashMap<ObjectId, InProgressOutput>,
213+ keyboard_layout: LinuxKeyboardLayout,
208214 keymap_state: Option<xkb::State>,
209215 compose_state: Option<xkb::compose::State>,
210216 drag: DragState,
@@ -335,6 +341,35 @@ impl WaylandClientStatePtr { · 124 unmodified lines
335341 text_input.commit();
336342 }
337343
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+
338373 pub fn drop_window(&self, surface_id: &ObjectId) {
339374 let mut client = self.get_client();
340375 let mut state = client.borrow_mut();
@@ -533,6 +568,7 @@ impl WaylandClient { · 192 unmodified lines
533568 in_progress_outputs,
534569 windows: HashMap::default(),
535570 common,
571+ keyboard_layout: LinuxKeyboardLayout::new(UNKNOWN_KEYBOARD_LAYOUT_NAME),
536572 keymap_state: None,
537573 compose_state: None,
538574 drag: DragState {
@@ -590,17 +626,7 @@ impl WaylandClient { · 51 unmodified lines
590626
591627 impl LinuxClient for WaylandClient {
592628 fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
593− let state = self.0.borrow();
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))
629+ Box::new(self.0.borrow().keyboard_layout.clone())
604630 }
605631
606632 fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
@@ -1181,13 +1207,9 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr { · 574 unmodified lines
11811207 };
11821208 state.keymap_state = Some(xkb::State::new(&keymap));
11831209 state.compose_state = get_xkb_compose_state(&xkb_context);
1210+ drop(state);
11841211
1185− if let Some(mut callback) = state.common.callbacks.keyboard_layout_change.take() {
1186− drop(state);
1187− callback();
1188− state = client.borrow_mut();
1189− state.common.callbacks.keyboard_layout_change = Some(callback);
1190− }
1212+ this.handle_keyboard_layout_change();
11911213 }
11921214 wl_keyboard::Event::Enter { surface, .. } => {
11931215 state.keyboard_focused_window = get_window(&mut state, &surface.id());
@@ -1230,26 +1252,18 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr { · 36 unmodified lines
12301252 keymap_state.update_mask(mods_depressed, mods_latched, mods_locked, 0, 0, group);
12311253 state.modifiers = Modifiers::from_xkb(keymap_state);
12321254
1233− if group != old_layout {
1234− if let Some(mut callback) = state.common.callbacks.keyboard_layout_change.take()
1235− {
1236− drop(state);
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− };
1255+ if let Some(focused_window) = focused_window {
1256+ let input = PlatformInput::ModifiersChanged(ModifiersChangedEvent {
1257+ modifiers: state.modifiers,
1258+ });
12461259
1247− let input = PlatformInput::ModifiersChanged(ModifiersChangedEvent {
1248− modifiers: state.modifiers,
1249− });
1260+ drop(state);
1261+ focused_window.handle_input(input);
1262+ }
12501263
1251− drop(state);
1252− focused_window.handle_input(input);
1264+ if group != old_layout {
1265+ this.handle_keyboard_layout_change();
1266+ }
12531267 }
12541268 wl_keyboard::Event::Key {
12551269 serial,
@@ -1374,6 +1388,7 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr { · 118 unmodified lines
13741388 }
13751389 }
13761390 }
1391+
13771392 impl Dispatch<zwp_text_input_v3::ZwpTextInputV3, ()> for WaylandClientStatePtr {
13781393 fn event(
13791394 this: &mut Self,
crates/gpui/src/platform/linux/x11/client.rs+244−87
@@ -28,7 +28,7 @@ use x11rb::{ · 27 unmodified lines
2828 protocol::xkb::ConnectionExt as _,
2929 protocol::xproto::{
3030 AtomEnum, ChangeWindowAttributesAux, ClientMessageData, ClientMessageEvent,
31− ConnectionExt as _, EventMask, KeyPressEvent,
31+ ConnectionExt as _, EventMask, KeyPressEvent, Visibility,
3232 },
3333 protocol::{Event, randr, render, xinput, xkb, xproto},
3434 resource_manager::Database,
@@ -78,7 +78,10 @@ pub(crate) const XINPUT_ALL_DEVICE_GROUPS: xinput::DeviceId = 1; · 43 unmodified lines
7878
7979 pub(crate) struct WindowRef {
8080 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,
8285 }
8386
8487 impl WindowRef {
@@ -95,6 +98,16 @@ impl Deref for WindowRef { · 10 unmodified lines
9598 }
9699 }
97100
101+enum RefreshState {
102+ Hidden {
103+ refresh_rate: Duration,
104+ },
105+ PeriodicRefresh {
106+ refresh_rate: Duration,
107+ event_loop_token: RegistrationToken,
108+ },
109+}
110+
98111 #[derive(Debug)]
99112 #[non_exhaustive]
100113 pub enum EventHandlerError {
@@ -185,6 +198,7 @@ pub struct X11ClientState { · 84 unmodified lines
185198 pub(crate) keyboard_focused_window: Option<xproto::Window>,
186199 pub(crate) xkb: xkbc::State,
187200 previous_xkb_state: XKBStateNotiy,
201+ keyboard_layout: LinuxKeyboardLayout,
188202 pub(crate) ximc: Option<X11rbClient<Rc<XCBConnection>>>,
189203 pub(crate) xim_handler: Option<XimHandler>,
190204 pub modifiers: Modifiers,
@@ -220,7 +234,14 @@ impl X11ClientStatePtr { · 29 unmodified lines
220234 let mut state = client.0.borrow_mut();
221235
222236 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+ }
224245 }
225246 if state.mouse_focused_window == Some(x_window) {
226247 state.mouse_focused_window = None;
@@ -349,13 +370,21 @@ impl X11Client { · 122 unmodified lines
349370 let events = xkb::EventType::STATE_NOTIFY
350371 | xkb::EventType::MAP_NOTIFY
351372 | 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;
352381 xcb_connection
353382 .xkb_select_events(
354383 xkb::ID::USE_CORE_KBD.into(),
355384 0u8.into(),
356385 events,
357− 0u8.into(),
358− 0u8.into(),
386+ map_notify_parts,
387+ map_notify_parts,
359388 &xkb::SelectEventsAux::new(),
360389 )
361390 .unwrap();
@@ -373,17 +402,22 @@ impl X11Client { · 11 unmodified lines
373402 xkbc::x11::state_new_from_device(&xkb_keymap, &xcb_connection, xkb_device_id)
374403 };
375404 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());
377411
378412 let gpu_context = BladeContext::new().expect("Unable to init GPU context");
379413
414+ let resource_database = x11rb::resource_manager::new_from_default(&xcb_connection).unwrap();
380415 let scale_factor = resource_database
381416 .get_value("Xft.dpi", "Xft.dpi")
382417 .ok()
383418 .flatten()
384419 .map(|dpi: f32| dpi / 96.0)
385420 .unwrap_or(1.0);
386−
387421 let cursor_handle = cursor::Handle::new(&xcb_connection, x_root_index, &resource_database)
388422 .unwrap()
389423 .reply()
@@ -461,6 +495,7 @@ impl X11Client { · 71 unmodified lines
461495 keyboard_focused_window: None,
462496 xkb: xkb_state,
463497 previous_xkb_state: XKBStateNotiy::default(),
498+ keyboard_layout,
464499 ximc,
465500 xim_handler,
466501
@@ -492,6 +527,10 @@ impl X11Client { · 25 unmodified lines
492527 let mut last_key_release = None;
493528 let mut last_key_press: Option<KeyPressEvent> = None;
494529
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+
495534 loop {
496535 match xcb_connection.poll_for_event() {
497536 Ok(Some(event)) => {
@@ -500,9 +539,29 @@ impl X11Client { · 2 unmodified lines
500539 windows_to_refresh.insert(expose_event.window);
501540 }
502541 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+
503552 last_key_release = Some(event);
504553 }
505554 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+
506565 if let Some(last_press) = last_key_press.as_ref() {
507566 if last_press.detail == key_press.detail {
508567 continue;
@@ -523,6 +582,12 @@ impl X11Client { · 14 unmodified lines
523582 events.push(Event::KeyPress(key_press));
524583 last_key_press = Some(key_press);
525584 }
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+ }
526591 _ => {
527592 if let Some(release_event) = last_key_release.take() {
528593 events.push(release_event);
@@ -532,10 +597,6 @@ impl X11Client { · 3 unmodified lines
532597 }
533598 }
534599 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− }
539600 break;
540601 }
541602 Err(e) => {
@@ -545,15 +606,21 @@ impl X11Client { · 3 unmodified lines
545606 }
546607 }
547608
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+
548616 if events.is_empty() && windows_to_refresh.is_empty() {
549617 break;
550618 }
551619
552620 for window in windows_to_refresh.into_iter() {
553− if let Some(window) = self.get_window(window) {
554− window.refresh(RequestFrameOptions {
555− require_presentation: true,
556− });
621+ let mut state = self.0.borrow_mut();
622+ if let Some(window) = state.windows.get_mut(&window) {
623+ window.expose_event_received = true;
557624 }
558625 }
559626
@@ -661,6 +728,27 @@ impl X11Client { · 101 unmodified lines
661728
662729 fn handle_event(&self, event: Event) -> Option<()> {
663730 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+ }
664752 Event::ClientMessage(event) => {
665753 let window = self.get_window(event.window)?;
666754 let [atom, arg1, arg2, arg3, arg4] = event.data.as_data32();
@@ -826,7 +914,7 @@ impl X11Client { · 159 unmodified lines
826914 self.reset_ime();
827915 window.handle_ime_delete();
828916 }
829− Event::XkbNewKeyboardNotify(_) | Event::MapNotify(_) => {
917+ Event::XkbNewKeyboardNotify(_) | Event::XkbMapNotify(_) => {
830918 let mut state = self.0.borrow_mut();
831919 let xkb_state = {
832920 let xkb_keymap = xkbc::x11::keymap_new_from_device(
@@ -850,6 +938,8 @@ impl X11Client { · 17 unmodified lines
850938 locked_layout,
851939 };
852940 state.xkb = xkb_state;
941+ drop(state);
942+ self.handle_keyboard_layout_change();
853943 }
854944 Event::XkbStateNotify(event) => {
855945 let mut state = self.0.borrow_mut();
@@ -869,16 +959,6 @@ impl X11Client { · 13 unmodified lines
869959 locked_layout: event.locked_group.into(),
870960 };
871961
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−
882962 let modifiers = Modifiers::from_xkb(&state.xkb);
883963 if state.last_modifiers_changed_event == modifiers {
884964 drop(state);
@@ -893,6 +973,10 @@ impl X11Client { · 8 unmodified lines
893973 ModifiersChangedEvent { modifiers },
894974 ));
895975 }
976+
977+ if new_layout != old_layout {
978+ self.handle_keyboard_layout_change();
979+ }
896980 }
897981 Event::KeyPress(event) => {
898982 let window = self.get_window(event.event)?;
@@ -1281,6 +1365,22 @@ impl X11Client { · 382 unmodified lines
12811365 drop(state);
12821366 Some(())
12831367 }
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+ }
12841384 }
12851385
12861386 impl LinuxClient for X11Client {
@@ -1294,14 +1394,7 @@ impl LinuxClient for X11Client { · 7 unmodified lines
12941394
12951395 fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout> {
12961396 let state = self.0.borrow();
1297− let layout_idx = state.xkb.serialize_layout(STATE_LAYOUT_EFFECTIVE);
1298− Box::new(LinuxKeyboardLayout::new(
1299− state
1300− .xkb
1301− .get_keymap()
1302− .layout_get_name(layout_idx)
1303− .to_string(),
1304− ))
1397+ Box::new(state.keyboard_layout.clone())
13051398 }
13061399
13071400 fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
@@ -1383,61 +1476,12 @@ impl LinuxClient for X11Client { · 75 unmodified lines
13831476 )
13841477 .unwrap();
13851478
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.