Compare commits
108 Commits
fix-highli
...
fix-code-a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6418b05a4f | ||
|
|
cdaacd4803 | ||
|
|
53fab9730b | ||
|
|
ea6853d35c | ||
|
|
c37a2f885a | ||
|
|
9c70ba7dcc | ||
|
|
5c4f1e6b85 | ||
|
|
86ce4ef3ab | ||
|
|
9948778e96 | ||
|
|
c2ace408d9 | ||
|
|
e016c05959 | ||
|
|
f2e8d0cc08 | ||
|
|
e406ac6db9 | ||
|
|
546715634c | ||
|
|
db4b86e0c8 | ||
|
|
35f5eb1fe7 | ||
|
|
5939cae6fa | ||
|
|
83f9f9d9e3 | ||
|
|
43baa5d8b8 | ||
|
|
f4609c04eb | ||
|
|
28e14a361d | ||
|
|
77933f83e5 | ||
|
|
186237bb1a | ||
|
|
500acc9511 | ||
|
|
49acfd2602 | ||
|
|
ecb016081a | ||
|
|
bb0cc1059c | ||
|
|
3e2680d650 | ||
|
|
35595fe3c2 | ||
|
|
ce2259ce51 | ||
|
|
6f97d74ff9 | ||
|
|
d6c9d00a4c | ||
|
|
85c2dc909d | ||
|
|
c814b99fcb | ||
|
|
07ccff217a | ||
|
|
8ab52f3491 | ||
|
|
ecf410e57d | ||
|
|
ec0eeaf69d | ||
|
|
376335496d | ||
|
|
4f656cedfa | ||
|
|
0e9ee3cb55 | ||
|
|
bbe764794d | ||
|
|
3882323f79 | ||
|
|
b0b83ef5aa | ||
|
|
7beae757b8 | ||
|
|
a6e99c1c16 | ||
|
|
6d8d2e2989 | ||
|
|
877790a105 | ||
|
|
0c08bbca05 | ||
|
|
ba0b68779d | ||
|
|
45af5e4239 | ||
|
|
01f9b1e9b4 | ||
|
|
635b71c486 | ||
|
|
c4a7552a04 | ||
|
|
918aee550c | ||
|
|
5c194f7cdc | ||
|
|
54df5812d9 | ||
|
|
0d84651f14 | ||
|
|
06af052e6d | ||
|
|
f1786b3b5f | ||
|
|
f348240a8c | ||
|
|
762fa9b3c7 | ||
|
|
1bd34e0db0 | ||
|
|
ce696c18ed | ||
|
|
9d23527663 | ||
|
|
fc2b3b2e45 | ||
|
|
8c7fb26af0 | ||
|
|
867b5df070 | ||
|
|
c5bbd556ea | ||
|
|
4a84b78093 | ||
|
|
fd63d432e9 | ||
|
|
ab70555a8a | ||
|
|
474eb8db77 | ||
|
|
da5f25d9b0 | ||
|
|
83ba05eb32 | ||
|
|
da583e5943 | ||
|
|
9ad6196150 | ||
|
|
d4cc4f8ca7 | ||
|
|
c61429e166 | ||
|
|
4c70d55546 | ||
|
|
025938b4a5 | ||
|
|
cc9af8d036 | ||
|
|
ee60d5855c | ||
|
|
97f398e677 | ||
|
|
6a2bad4e11 | ||
|
|
ad4a53c71c | ||
|
|
160fca029c | ||
|
|
6a1648825c | ||
|
|
f0d097c66a | ||
|
|
a3bcf6fe21 | ||
|
|
ac8e2f0576 | ||
|
|
5c4649bd37 | ||
|
|
bd13c90acc | ||
|
|
997f6c6a19 | ||
|
|
8dfbafd345 | ||
|
|
677d6acc9d | ||
|
|
96add6c9de | ||
|
|
f76eecd758 | ||
|
|
bec2bfeb8b | ||
|
|
9edf1f8f04 | ||
|
|
23fe74ebc5 | ||
|
|
46fff9979d | ||
|
|
e7b19ab0b1 | ||
|
|
ce8d5e41a5 | ||
|
|
dac5725246 | ||
|
|
f1db1f3a3c | ||
|
|
02bdba80a4 | ||
|
|
af0cd30a9c |
2079
Cargo.lock
generated
2079
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
33
Cargo.toml
33
Cargo.toml
@@ -222,7 +222,7 @@ members = [
|
||||
|
||||
"tooling/perf",
|
||||
"tooling/workspace-hack",
|
||||
"tooling/xtask",
|
||||
"tooling/xtask", "crates/fs_benchmarks", "crates/worktree_benchmarks",
|
||||
]
|
||||
default-members = ["crates/zed"]
|
||||
|
||||
@@ -274,7 +274,7 @@ cloud_llm_client = { path = "crates/cloud_llm_client" }
|
||||
cloud_zeta2_prompt = { path = "crates/cloud_zeta2_prompt" }
|
||||
collab = { path = "crates/collab" }
|
||||
collab_ui = { path = "crates/collab_ui" }
|
||||
collections = { path = "crates/collections", package = "zed-collections", version = "0.1.0" }
|
||||
collections = { path = "crates/collections", version = "0.1.0" }
|
||||
command_palette = { path = "crates/command_palette" }
|
||||
command_palette_hooks = { path = "crates/command_palette_hooks" }
|
||||
component = { path = "crates/component" }
|
||||
@@ -290,7 +290,7 @@ debug_adapter_extension = { path = "crates/debug_adapter_extension" }
|
||||
debugger_tools = { path = "crates/debugger_tools" }
|
||||
debugger_ui = { path = "crates/debugger_ui" }
|
||||
deepseek = { path = "crates/deepseek" }
|
||||
derive_refineable = { path = "crates/refineable/derive_refineable", package = "zed-derive-refineable", version = "0.1.0" }
|
||||
derive_refineable = { path = "crates/refineable/derive_refineable" }
|
||||
diagnostics = { path = "crates/diagnostics" }
|
||||
editor = { path = "crates/editor" }
|
||||
extension = { path = "crates/extension" }
|
||||
@@ -309,10 +309,10 @@ git_ui = { path = "crates/git_ui" }
|
||||
go_to_line = { path = "crates/go_to_line" }
|
||||
google_ai = { path = "crates/google_ai" }
|
||||
gpui = { path = "crates/gpui", default-features = false }
|
||||
gpui_macros = { path = "crates/gpui_macros", package = "gpui-macros", version = "0.1.0" }
|
||||
gpui_macros = { path = "crates/gpui_macros" }
|
||||
gpui_tokio = { path = "crates/gpui_tokio" }
|
||||
html_to_markdown = { path = "crates/html_to_markdown" }
|
||||
http_client = { path = "crates/http_client", package = "zed-http-client", version = "0.1.0" }
|
||||
http_client = { path = "crates/http_client" }
|
||||
http_client_tls = { path = "crates/http_client_tls" }
|
||||
icons = { path = "crates/icons" }
|
||||
image_viewer = { path = "crates/image_viewer" }
|
||||
@@ -341,7 +341,7 @@ lsp = { path = "crates/lsp" }
|
||||
markdown = { path = "crates/markdown" }
|
||||
markdown_preview = { path = "crates/markdown_preview" }
|
||||
svg_preview = { path = "crates/svg_preview" }
|
||||
media = { path = "crates/media", package = "zed-media", version = "0.1.0" }
|
||||
media = { path = "crates/media" }
|
||||
menu = { path = "crates/menu" }
|
||||
migrator = { path = "crates/migrator" }
|
||||
mistral = { path = "crates/mistral" }
|
||||
@@ -358,7 +358,7 @@ outline = { path = "crates/outline" }
|
||||
outline_panel = { path = "crates/outline_panel" }
|
||||
panel = { path = "crates/panel" }
|
||||
paths = { path = "crates/paths" }
|
||||
perf = { path = "tooling/perf", package = "zed-perf", version = "0.1.0" }
|
||||
perf = { path = "tooling/perf" }
|
||||
picker = { path = "crates/picker" }
|
||||
plugin = { path = "crates/plugin" }
|
||||
plugin_macros = { path = "crates/plugin_macros" }
|
||||
@@ -370,7 +370,7 @@ project_symbols = { path = "crates/project_symbols" }
|
||||
prompt_store = { path = "crates/prompt_store" }
|
||||
proto = { path = "crates/proto" }
|
||||
recent_projects = { path = "crates/recent_projects" }
|
||||
refineable = { path = "crates/refineable", package = "zed-refineable", version = "0.1.0" }
|
||||
refineable = { path = "crates/refineable" }
|
||||
release_channel = { path = "crates/release_channel" }
|
||||
scheduler = { path = "crates/scheduler" }
|
||||
remote = { path = "crates/remote" }
|
||||
@@ -383,7 +383,7 @@ rope = { path = "crates/rope" }
|
||||
rpc = { path = "crates/rpc" }
|
||||
rules_library = { path = "crates/rules_library" }
|
||||
search = { path = "crates/search" }
|
||||
semantic_version = { path = "crates/semantic_version", package = "zed-semantic-version", version = "0.1.0" }
|
||||
semantic_version = { path = "crates/semantic_version" }
|
||||
session = { path = "crates/session" }
|
||||
settings = { path = "crates/settings" }
|
||||
settings_macros = { path = "crates/settings_macros" }
|
||||
@@ -396,7 +396,7 @@ sqlez_macros = { path = "crates/sqlez_macros" }
|
||||
story = { path = "crates/story" }
|
||||
storybook = { path = "crates/storybook" }
|
||||
streaming_diff = { path = "crates/streaming_diff" }
|
||||
sum_tree = { path = "crates/sum_tree", package = "zed-sum-tree", version = "0.1.0" }
|
||||
sum_tree = { path = "crates/sum_tree" }
|
||||
supermaven = { path = "crates/supermaven" }
|
||||
supermaven_api = { path = "crates/supermaven_api" }
|
||||
codestral = { path = "crates/codestral" }
|
||||
@@ -420,8 +420,8 @@ ui = { path = "crates/ui" }
|
||||
ui_input = { path = "crates/ui_input" }
|
||||
ui_macros = { path = "crates/ui_macros" }
|
||||
ui_prompt = { path = "crates/ui_prompt" }
|
||||
util = { path = "crates/util", package = "zed-util", version = "0.1.0" }
|
||||
util_macros = { path = "crates/util_macros", package = "zed-util-macros", version = "0.1.0" }
|
||||
util = { path = "crates/util" }
|
||||
util_macros = { path = "crates/util_macros" }
|
||||
vercel = { path = "crates/vercel" }
|
||||
vim = { path = "crates/vim" }
|
||||
vim_mode_setting = { path = "crates/vim_mode_setting" }
|
||||
@@ -455,6 +455,7 @@ async-compat = "0.2.1"
|
||||
async-compression = { version = "0.4", features = ["gzip", "futures-io"] }
|
||||
async-dispatcher = "0.1"
|
||||
async-fs = "2.1"
|
||||
async-lock = "2.1"
|
||||
async-pipe = { git = "https://github.com/zed-industries/async-pipe-rs", rev = "82d00a04211cf4e1236029aa03e6b6ce2a74c553" }
|
||||
async-recursion = "1.0.0"
|
||||
async-tar = "0.5.0"
|
||||
@@ -654,7 +655,7 @@ strum = { version = "0.27.0", features = ["derive"] }
|
||||
subtle = "2.5.0"
|
||||
syn = { version = "2.0.101", features = ["full", "extra-traits", "visit-mut"] }
|
||||
sys-locale = "0.3.1"
|
||||
sysinfo = "0.31.0"
|
||||
sysinfo = "0.37.0"
|
||||
take-until = "0.2.0"
|
||||
tempfile = "3.20.0"
|
||||
thiserror = "2.0.12"
|
||||
@@ -805,7 +806,7 @@ wasmtime = { opt-level = 3 }
|
||||
activity_indicator = { codegen-units = 1 }
|
||||
assets = { codegen-units = 1 }
|
||||
breadcrumbs = { codegen-units = 1 }
|
||||
zed-collections = { codegen-units = 1 }
|
||||
collections = { codegen-units = 1 }
|
||||
command_palette = { codegen-units = 1 }
|
||||
command_palette_hooks = { codegen-units = 1 }
|
||||
extension_cli = { codegen-units = 1 }
|
||||
@@ -825,11 +826,11 @@ outline = { codegen-units = 1 }
|
||||
paths = { codegen-units = 1 }
|
||||
prettier = { codegen-units = 1 }
|
||||
project_symbols = { codegen-units = 1 }
|
||||
zed-refineable = { codegen-units = 1 }
|
||||
refineable = { codegen-units = 1 }
|
||||
release_channel = { codegen-units = 1 }
|
||||
reqwest_client = { codegen-units = 1 }
|
||||
rich_text = { codegen-units = 1 }
|
||||
zed-semantic-version = { codegen-units = 1 }
|
||||
semantic_version = { codegen-units = 1 }
|
||||
session = { codegen-units = 1 }
|
||||
snippet = { codegen-units = 1 }
|
||||
snippets_ui = { codegen-units = 1 }
|
||||
|
||||
@@ -1 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M2.6 5v3.6h3.6"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M13.4 11A5.4 5.4 0 0 0 8 5.6a5.4 5.4 0 0 0-3.6 1.38L2.6 8.6"/></svg>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.125 9.25001L3 6.125L6.125 3" stroke="#C4CAD4" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M3 6.125H9.56251C10.0139 6.125 10.4609 6.21391 10.878 6.38666C11.295 6.55942 11.674 6.81262 11.9932 7.13182C12.3124 7.45102 12.5656 7.82997 12.7383 8.24703C12.9111 8.66408 13 9.11108 13 9.5625C13 10.0139 12.9111 10.4609 12.7383 10.878C12.5656 11.295 12.3124 11.674 11.9932 11.9932C11.674 12.3124 11.295 12.5656 10.878 12.7383C10.4609 12.9111 10.0139 13 9.56251 13H7.375" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 339 B After Width: | Height: | Size: 692 B |
@@ -491,8 +491,8 @@
|
||||
"bindings": {
|
||||
"ctrl-[": "editor::Outdent",
|
||||
"ctrl-]": "editor::Indent",
|
||||
"shift-alt-up": "editor::AddSelectionAbove", // Insert Cursor Above
|
||||
"shift-alt-down": "editor::AddSelectionBelow", // Insert Cursor Below
|
||||
"shift-alt-up": ["editor::AddSelectionAbove", { "skip_soft_wrap": true }], // Insert Cursor Above
|
||||
"shift-alt-down": ["editor::AddSelectionBelow", { "skip_soft_wrap": true }], // Insert Cursor Below
|
||||
"ctrl-shift-k": "editor::DeleteLine",
|
||||
"alt-up": "editor::MoveLineUp",
|
||||
"alt-down": "editor::MoveLineDown",
|
||||
@@ -1249,6 +1249,7 @@
|
||||
"escape": "workspace::CloseWindow",
|
||||
"ctrl-m": "settings_editor::Minimize",
|
||||
"ctrl-f": "search::FocusSearch",
|
||||
"left": "settings_editor::ToggleFocusNav",
|
||||
"ctrl-shift-e": "settings_editor::ToggleFocusNav",
|
||||
// todo(settings_ui): cut this down based on the max files and overflow UI
|
||||
"ctrl-1": ["settings_editor::FocusFile", 0],
|
||||
@@ -1269,6 +1270,8 @@
|
||||
"context": "SettingsWindow > NavigationMenu",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"up": "settings_editor::FocusPreviousNavEntry",
|
||||
"down": "settings_editor::FocusNextNavEntry",
|
||||
"right": "settings_editor::ExpandNavEntry",
|
||||
"left": "settings_editor::CollapseNavEntry",
|
||||
"pageup": "settings_editor::FocusPreviousRootNavEntry",
|
||||
|
||||
@@ -539,10 +539,10 @@
|
||||
"bindings": {
|
||||
"cmd-[": "editor::Outdent",
|
||||
"cmd-]": "editor::Indent",
|
||||
"cmd-ctrl-p": "editor::AddSelectionAbove", // Insert cursor above
|
||||
"cmd-alt-up": "editor::AddSelectionAbove",
|
||||
"cmd-ctrl-n": "editor::AddSelectionBelow", // Insert cursor below
|
||||
"cmd-alt-down": "editor::AddSelectionBelow",
|
||||
"cmd-ctrl-p": ["editor::AddSelectionAbove", { "skip_soft_wrap": false }], // Insert cursor above
|
||||
"cmd-alt-up": ["editor::AddSelectionAbove", { "skip_soft_wrap": true }],
|
||||
"cmd-ctrl-n": ["editor::AddSelectionBelow", { "skip_soft_wrap": false }], // Insert cursor below
|
||||
"cmd-alt-down": ["editor::AddSelectionBelow", { "skip_soft_wrap": true }],
|
||||
"cmd-shift-k": "editor::DeleteLine",
|
||||
"alt-up": "editor::MoveLineUp",
|
||||
"alt-down": "editor::MoveLineDown",
|
||||
@@ -1354,6 +1354,7 @@
|
||||
"escape": "workspace::CloseWindow",
|
||||
"cmd-m": "settings_editor::Minimize",
|
||||
"cmd-f": "search::FocusSearch",
|
||||
"left": "settings_editor::ToggleFocusNav",
|
||||
"cmd-shift-e": "settings_editor::ToggleFocusNav",
|
||||
// todo(settings_ui): cut this down based on the max files and overflow UI
|
||||
"ctrl-1": ["settings_editor::FocusFile", 0],
|
||||
@@ -1374,6 +1375,8 @@
|
||||
"context": "SettingsWindow > NavigationMenu",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"up": "settings_editor::FocusPreviousNavEntry",
|
||||
"down": "settings_editor::FocusNextNavEntry",
|
||||
"right": "settings_editor::ExpandNavEntry",
|
||||
"left": "settings_editor::CollapseNavEntry",
|
||||
"pageup": "settings_editor::FocusPreviousRootNavEntry",
|
||||
|
||||
@@ -500,8 +500,8 @@
|
||||
"bindings": {
|
||||
"ctrl-[": "editor::Outdent",
|
||||
"ctrl-]": "editor::Indent",
|
||||
"ctrl-shift-alt-up": "editor::AddSelectionAbove", // Insert Cursor Above
|
||||
"ctrl-shift-alt-down": "editor::AddSelectionBelow", // Insert Cursor Below
|
||||
"ctrl-shift-alt-up": ["editor::AddSelectionAbove", { "skip_soft_wrap": true }], // Insert Cursor Above
|
||||
"ctrl-shift-alt-down": ["editor::AddSelectionBelow", { "skip_soft_wrap": true }], // Insert Cursor Below
|
||||
"ctrl-shift-k": "editor::DeleteLine",
|
||||
"alt-up": "editor::MoveLineUp",
|
||||
"alt-down": "editor::MoveLineDown",
|
||||
@@ -1270,6 +1270,7 @@
|
||||
"escape": "workspace::CloseWindow",
|
||||
"ctrl-m": "settings_editor::Minimize",
|
||||
"ctrl-f": "search::FocusSearch",
|
||||
"left": "settings_editor::ToggleFocusNav",
|
||||
"ctrl-shift-e": "settings_editor::ToggleFocusNav",
|
||||
// todo(settings_ui): cut this down based on the max files and overflow UI
|
||||
"ctrl-1": ["settings_editor::FocusFile", 0],
|
||||
@@ -1290,6 +1291,8 @@
|
||||
"context": "SettingsWindow > NavigationMenu",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"up": "settings_editor::FocusPreviousNavEntry",
|
||||
"down": "settings_editor::FocusNextNavEntry",
|
||||
"right": "settings_editor::ExpandNavEntry",
|
||||
"left": "settings_editor::CollapseNavEntry",
|
||||
"pageup": "settings_editor::FocusPreviousRootNavEntry",
|
||||
|
||||
@@ -24,8 +24,8 @@
|
||||
"ctrl-<": "editor::ScrollCursorCenter", // editor:scroll-to-cursor
|
||||
"f3": ["editor::SelectNext", { "replace_newest": true }], // find-and-replace:find-next
|
||||
"shift-f3": ["editor::SelectPrevious", { "replace_newest": true }], //find-and-replace:find-previous
|
||||
"alt-shift-down": "editor::AddSelectionBelow", // editor:add-selection-below
|
||||
"alt-shift-up": "editor::AddSelectionAbove", // editor:add-selection-above
|
||||
"alt-shift-down": ["editor::AddSelectionBelow", { "skip_soft_wrap": true }], // editor:add-selection-below
|
||||
"alt-shift-up": ["editor::AddSelectionAbove", { "skip_soft_wrap": true }], // editor:add-selection-above
|
||||
"ctrl-j": "editor::JoinLines", // editor:join-lines
|
||||
"ctrl-shift-d": "editor::DuplicateLineDown", // editor:duplicate-lines
|
||||
"ctrl-up": "editor::MoveLineUp", // editor:move-line-up
|
||||
|
||||
@@ -28,8 +28,8 @@
|
||||
{
|
||||
"context": "Editor",
|
||||
"bindings": {
|
||||
"ctrl-alt-up": "editor::AddSelectionAbove",
|
||||
"ctrl-alt-down": "editor::AddSelectionBelow",
|
||||
"ctrl-alt-up": ["editor::AddSelectionAbove", { "skip_soft_wrap": false }],
|
||||
"ctrl-alt-down": ["editor::AddSelectionBelow", { "skip_soft_wrap": false }],
|
||||
"ctrl-shift-up": "editor::MoveLineUp",
|
||||
"ctrl-shift-down": "editor::MoveLineDown",
|
||||
"ctrl-shift-m": "editor::SelectLargerSyntaxNode",
|
||||
|
||||
@@ -25,8 +25,8 @@
|
||||
"cmd-<": "editor::ScrollCursorCenter",
|
||||
"cmd-g": ["editor::SelectNext", { "replace_newest": true }],
|
||||
"cmd-shift-g": ["editor::SelectPrevious", { "replace_newest": true }],
|
||||
"ctrl-shift-down": "editor::AddSelectionBelow",
|
||||
"ctrl-shift-up": "editor::AddSelectionAbove",
|
||||
"ctrl-shift-down": ["editor::AddSelectionBelow", { "skip_soft_wrap": true }],
|
||||
"ctrl-shift-up": ["editor::AddSelectionAbove", { "skip_soft_wrap": true }],
|
||||
"alt-enter": "editor::Newline",
|
||||
"cmd-shift-d": "editor::DuplicateLineDown",
|
||||
"ctrl-cmd-up": "editor::MoveLineUp",
|
||||
|
||||
@@ -28,8 +28,8 @@
|
||||
{
|
||||
"context": "Editor",
|
||||
"bindings": {
|
||||
"ctrl-shift-up": "editor::AddSelectionAbove",
|
||||
"ctrl-shift-down": "editor::AddSelectionBelow",
|
||||
"ctrl-shift-up": ["editor::AddSelectionAbove", { "skip_soft_wrap": false }],
|
||||
"ctrl-shift-down": ["editor::AddSelectionBelow", { "skip_soft_wrap": false }],
|
||||
"cmd-ctrl-up": "editor::MoveLineUp",
|
||||
"cmd-ctrl-down": "editor::MoveLineDown",
|
||||
"cmd-shift-space": "editor::SelectAll",
|
||||
|
||||
@@ -95,8 +95,6 @@
|
||||
"g g": "vim::StartOfDocument",
|
||||
"g h": "editor::Hover",
|
||||
"g B": "editor::BlameHover",
|
||||
"g t": "vim::GoToTab",
|
||||
"g shift-t": "vim::GoToPreviousTab",
|
||||
"g d": "editor::GoToDefinition",
|
||||
"g shift-d": "editor::GoToDeclaration",
|
||||
"g y": "editor::GoToTypeDefinition",
|
||||
@@ -500,8 +498,8 @@
|
||||
"ctrl-c": "editor::ToggleComments",
|
||||
"d": "vim::HelixDelete",
|
||||
"c": "vim::Substitute",
|
||||
"shift-c": "editor::AddSelectionBelow",
|
||||
"alt-shift-c": "editor::AddSelectionAbove"
|
||||
"shift-c": ["editor::AddSelectionBelow", { "skip_soft_wrap": true }],
|
||||
"alt-shift-c": ["editor::AddSelectionAbove", { "skip_soft_wrap": true }]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -811,7 +809,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "VimControl || !Editor && !Terminal",
|
||||
"context": "VimControl && !menu || !Editor && !Terminal",
|
||||
"bindings": {
|
||||
// window related commands (ctrl-w X)
|
||||
"ctrl-w": null,
|
||||
@@ -865,7 +863,9 @@
|
||||
"ctrl-w ctrl-o": "workspace::CloseInactiveTabsAndPanes",
|
||||
"ctrl-w o": "workspace::CloseInactiveTabsAndPanes",
|
||||
"ctrl-w ctrl-n": "workspace::NewFileSplitHorizontal",
|
||||
"ctrl-w n": "workspace::NewFileSplitHorizontal"
|
||||
"ctrl-w n": "workspace::NewFileSplitHorizontal",
|
||||
"g t": "vim::GoToTab",
|
||||
"g shift-t": "vim::GoToPreviousTab"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -724,7 +724,9 @@
|
||||
// Whether to hide the root entry when only one folder is open in the window.
|
||||
"hide_root": false,
|
||||
// Whether to hide the hidden entries in the project panel.
|
||||
"hide_hidden": false
|
||||
"hide_hidden": false,
|
||||
// Whether to automatically open files when pasting them in the project panel.
|
||||
"open_file_on_paste": true
|
||||
},
|
||||
"outline_panel": {
|
||||
// Whether to show the outline panel button in the status bar
|
||||
@@ -906,6 +908,7 @@
|
||||
"now": true,
|
||||
"find_path": true,
|
||||
"read_file": true,
|
||||
"open": true,
|
||||
"grep": true,
|
||||
"terminal": true,
|
||||
"thinking": true,
|
||||
@@ -917,7 +920,6 @@
|
||||
// We don't know which of the context server tools are safe for the "Ask" profile, so we don't enable them by default.
|
||||
// "enable_all_context_servers": true,
|
||||
"tools": {
|
||||
"contents": true,
|
||||
"diagnostics": true,
|
||||
"fetch": true,
|
||||
"list_directory": true,
|
||||
@@ -1107,22 +1109,28 @@
|
||||
// Whether or not to perform a buffer format before saving: [on, off]
|
||||
// Keep in mind, if the autosave with delay is enabled, format_on_save will be ignored
|
||||
"format_on_save": "on",
|
||||
// How to perform a buffer format. This setting can take 4 values:
|
||||
// How to perform a buffer format. This setting can take multiple values:
|
||||
//
|
||||
// 1. Format code using the current language server:
|
||||
// 1. Default. Format files using Zed's Prettier integration (if applicable),
|
||||
// or falling back to formatting via language server:
|
||||
// "formatter": "auto"
|
||||
// 2. Format code using the current language server:
|
||||
// "formatter": "language_server"
|
||||
// 2. Format code using an external command:
|
||||
// 3. Format code using a specific language server:
|
||||
// "formatter": {"language_server": {"name": "ruff"}}
|
||||
// 4. Format code using an external command:
|
||||
// "formatter": {
|
||||
// "external": {
|
||||
// "command": "prettier",
|
||||
// "arguments": ["--stdin-filepath", "{buffer_path}"]
|
||||
// }
|
||||
// }
|
||||
// 3. Format code using Zed's Prettier integration:
|
||||
// 5. Format code using Zed's Prettier integration:
|
||||
// "formatter": "prettier"
|
||||
// 4. Default. Format files using Zed's Prettier integration (if applicable),
|
||||
// or falling back to formatting via language server:
|
||||
// "formatter": "auto"
|
||||
// 6. Format code using a code action
|
||||
// "formatter": {"code_action": "source.fixAll.eslint"}
|
||||
// 7. An array of any format step specified above to apply in order
|
||||
// "formatter": [{"code_action": "source.fixAll.eslint"}, "prettier"]
|
||||
"formatter": "auto",
|
||||
// How to soft-wrap long lines of text.
|
||||
// Possible values:
|
||||
@@ -1690,7 +1698,7 @@
|
||||
"preferred_line_length": 72
|
||||
},
|
||||
"Go": {
|
||||
"formatter": [{ "code_action": "source.organizeImports" }, { "language_server": {} }],
|
||||
"formatter": [{ "code_action": "source.organizeImports" }, "language_server"],
|
||||
"debuggers": ["Delve"]
|
||||
},
|
||||
"GraphQL": {
|
||||
|
||||
@@ -2112,6 +2112,7 @@ impl AcpThread {
|
||||
|
||||
let project = self.project.clone();
|
||||
let language_registry = project.read(cx).languages().clone();
|
||||
let is_windows = project.read(cx).path_style(cx).is_windows();
|
||||
|
||||
let terminal_id = acp::TerminalId(Uuid::new_v4().to_string().into());
|
||||
let terminal_task = cx.spawn({
|
||||
@@ -2125,9 +2126,10 @@ impl AcpThread {
|
||||
.and_then(|r| r.read(cx).default_system_shell())
|
||||
})?
|
||||
.unwrap_or_else(|| get_default_system_shell_preferring_bash());
|
||||
let (task_command, task_args) = ShellBuilder::new(&Shell::Program(shell))
|
||||
.redirect_stdin_to_dev_null()
|
||||
.build(Some(command.clone()), &args);
|
||||
let (task_command, task_args) =
|
||||
ShellBuilder::new(&Shell::Program(shell), is_windows)
|
||||
.redirect_stdin_to_dev_null()
|
||||
.build(Some(command.clone()), &args);
|
||||
let terminal = project
|
||||
.update(cx, |project, cx| {
|
||||
project.create_terminal_task(
|
||||
|
||||
@@ -4,22 +4,26 @@ use std::{
|
||||
fmt::Display,
|
||||
rc::{Rc, Weak},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use agent_client_protocol as acp;
|
||||
use collections::HashMap;
|
||||
use gpui::{
|
||||
App, Empty, Entity, EventEmitter, FocusHandle, Focusable, Global, ListAlignment, ListState,
|
||||
StyleRefinement, Subscription, Task, TextStyleRefinement, Window, actions, list, prelude::*,
|
||||
App, ClipboardItem, Empty, Entity, EventEmitter, FocusHandle, Focusable, Global, ListAlignment,
|
||||
ListState, StyleRefinement, Subscription, Task, TextStyleRefinement, Window, actions, list,
|
||||
prelude::*,
|
||||
};
|
||||
use language::LanguageRegistry;
|
||||
use markdown::{CodeBlockRenderer, Markdown, MarkdownElement, MarkdownStyle};
|
||||
use project::Project;
|
||||
use settings::Settings;
|
||||
use theme::ThemeSettings;
|
||||
use ui::prelude::*;
|
||||
use ui::{Tooltip, prelude::*};
|
||||
use util::ResultExt as _;
|
||||
use workspace::{Item, Workspace};
|
||||
use workspace::{
|
||||
Item, ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
|
||||
};
|
||||
|
||||
actions!(dev, [OpenAcpLogs]);
|
||||
|
||||
@@ -227,6 +231,34 @@ impl AcpTools {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn serialize_observed_messages(&self) -> Option<String> {
|
||||
let connection = self.watched_connection.as_ref()?;
|
||||
|
||||
let messages: Vec<serde_json::Value> = connection
|
||||
.messages
|
||||
.iter()
|
||||
.filter_map(|message| {
|
||||
let params = match &message.params {
|
||||
Ok(Some(params)) => params.clone(),
|
||||
Ok(None) => serde_json::Value::Null,
|
||||
Err(err) => serde_json::to_value(err).ok()?,
|
||||
};
|
||||
Some(serde_json::json!({
|
||||
"_direction": match message.direction {
|
||||
acp::StreamMessageDirection::Incoming => "incoming",
|
||||
acp::StreamMessageDirection::Outgoing => "outgoing",
|
||||
},
|
||||
"_type": message.message_type.to_string().to_lowercase(),
|
||||
"id": message.request_id,
|
||||
"method": message.name.to_string(),
|
||||
"params": params,
|
||||
}))
|
||||
})
|
||||
.collect();
|
||||
|
||||
serde_json::to_string_pretty(&messages).ok()
|
||||
}
|
||||
|
||||
fn render_message(
|
||||
&mut self,
|
||||
index: usize,
|
||||
@@ -492,3 +524,92 @@ impl Render for AcpTools {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AcpToolsToolbarItemView {
|
||||
acp_tools: Option<Entity<AcpTools>>,
|
||||
just_copied: bool,
|
||||
}
|
||||
|
||||
impl AcpToolsToolbarItemView {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
acp_tools: None,
|
||||
just_copied: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for AcpToolsToolbarItemView {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let Some(acp_tools) = self.acp_tools.as_ref() else {
|
||||
return Empty.into_any_element();
|
||||
};
|
||||
|
||||
let acp_tools = acp_tools.clone();
|
||||
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
IconButton::new(
|
||||
"copy_all_messages",
|
||||
if self.just_copied {
|
||||
IconName::Check
|
||||
} else {
|
||||
IconName::Copy
|
||||
},
|
||||
)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip(Tooltip::text(if self.just_copied {
|
||||
"Copied!"
|
||||
} else {
|
||||
"Copy All Messages"
|
||||
}))
|
||||
.disabled(
|
||||
acp_tools
|
||||
.read(cx)
|
||||
.watched_connection
|
||||
.as_ref()
|
||||
.is_none_or(|connection| connection.messages.is_empty()),
|
||||
)
|
||||
.on_click(cx.listener(move |this, _, _window, cx| {
|
||||
if let Some(content) = acp_tools.read(cx).serialize_observed_messages() {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(content));
|
||||
|
||||
this.just_copied = true;
|
||||
cx.spawn(async move |this, cx| {
|
||||
cx.background_executor().timer(Duration::from_secs(2)).await;
|
||||
this.update(cx, |this, cx| {
|
||||
this.just_copied = false;
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
})),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<ToolbarItemEvent> for AcpToolsToolbarItemView {}
|
||||
|
||||
impl ToolbarItemView for AcpToolsToolbarItemView {
|
||||
fn set_active_pane_item(
|
||||
&mut self,
|
||||
active_pane_item: Option<&dyn ItemHandle>,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> ToolbarItemLocation {
|
||||
if let Some(item) = active_pane_item
|
||||
&& let Some(acp_tools) = item.downcast::<AcpTools>()
|
||||
{
|
||||
self.acp_tools = Some(acp_tools);
|
||||
cx.notify();
|
||||
return ToolbarItemLocation::PrimaryRight;
|
||||
}
|
||||
if self.acp_tools.take().is_some() {
|
||||
cx.notify();
|
||||
}
|
||||
ToolbarItemLocation::Hidden
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,6 @@ heed.workspace = true
|
||||
http_client.workspace = true
|
||||
icons.workspace = true
|
||||
indoc.workspace = true
|
||||
itertools.workspace = true
|
||||
language.workspace = true
|
||||
language_model.workspace = true
|
||||
log.workspace = true
|
||||
|
||||
@@ -2,7 +2,6 @@ pub mod agent_profile;
|
||||
pub mod context;
|
||||
pub mod context_server_tool;
|
||||
pub mod context_store;
|
||||
pub mod history_store;
|
||||
pub mod thread;
|
||||
pub mod thread_store;
|
||||
pub mod tool_use;
|
||||
|
||||
@@ -1,253 +0,0 @@
|
||||
use crate::{ThreadId, thread_store::SerializedThreadMetadata};
|
||||
use anyhow::{Context as _, Result};
|
||||
use assistant_context::SavedContextMetadata;
|
||||
use chrono::{DateTime, Utc};
|
||||
use gpui::{App, AsyncApp, Entity, SharedString, Task, prelude::*};
|
||||
use itertools::Itertools;
|
||||
use paths::contexts_dir;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{collections::VecDeque, path::Path, sync::Arc, time::Duration};
|
||||
use util::ResultExt as _;
|
||||
|
||||
const MAX_RECENTLY_OPENED_ENTRIES: usize = 6;
|
||||
const NAVIGATION_HISTORY_PATH: &str = "agent-navigation-history.json";
|
||||
const SAVE_RECENTLY_OPENED_ENTRIES_DEBOUNCE: Duration = Duration::from_millis(50);
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum HistoryEntry {
|
||||
Thread(SerializedThreadMetadata),
|
||||
Context(SavedContextMetadata),
|
||||
}
|
||||
|
||||
impl HistoryEntry {
|
||||
pub fn updated_at(&self) -> DateTime<Utc> {
|
||||
match self {
|
||||
HistoryEntry::Thread(thread) => thread.updated_at,
|
||||
HistoryEntry::Context(context) => context.mtime.to_utc(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn id(&self) -> HistoryEntryId {
|
||||
match self {
|
||||
HistoryEntry::Thread(thread) => HistoryEntryId::Thread(thread.id.clone()),
|
||||
HistoryEntry::Context(context) => HistoryEntryId::Context(context.path.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn title(&self) -> &SharedString {
|
||||
match self {
|
||||
HistoryEntry::Thread(thread) => &thread.summary,
|
||||
HistoryEntry::Context(context) => &context.title,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generic identifier for a history entry.
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub enum HistoryEntryId {
|
||||
Thread(ThreadId),
|
||||
Context(Arc<Path>),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
enum SerializedRecentOpen {
|
||||
Thread(String),
|
||||
ContextName(String),
|
||||
/// Old format which stores the full path
|
||||
Context(String),
|
||||
}
|
||||
|
||||
pub struct HistoryStore {
|
||||
context_store: Entity<assistant_context::ContextStore>,
|
||||
recently_opened_entries: VecDeque<HistoryEntryId>,
|
||||
_subscriptions: Vec<gpui::Subscription>,
|
||||
_save_recently_opened_entries_task: Task<()>,
|
||||
}
|
||||
|
||||
impl HistoryStore {
|
||||
pub fn new(
|
||||
context_store: Entity<assistant_context::ContextStore>,
|
||||
initial_recent_entries: impl IntoIterator<Item = HistoryEntryId>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let subscriptions = vec![cx.observe(&context_store, |_, _, cx| cx.notify())];
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
let entries = Self::load_recently_opened_entries(cx).await.log_err()?;
|
||||
this.update(cx, |this, _| {
|
||||
this.recently_opened_entries
|
||||
.extend(
|
||||
entries.into_iter().take(
|
||||
MAX_RECENTLY_OPENED_ENTRIES
|
||||
.saturating_sub(this.recently_opened_entries.len()),
|
||||
),
|
||||
);
|
||||
})
|
||||
.ok()
|
||||
})
|
||||
.detach();
|
||||
|
||||
Self {
|
||||
context_store,
|
||||
recently_opened_entries: initial_recent_entries.into_iter().collect(),
|
||||
_subscriptions: subscriptions,
|
||||
_save_recently_opened_entries_task: Task::ready(()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn entries(&self, cx: &mut Context<Self>) -> Vec<HistoryEntry> {
|
||||
let mut history_entries = Vec::new();
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
if std::env::var("ZED_SIMULATE_NO_THREAD_HISTORY").is_ok() {
|
||||
return history_entries;
|
||||
}
|
||||
|
||||
history_entries.extend(
|
||||
self.context_store
|
||||
.read(cx)
|
||||
.unordered_contexts()
|
||||
.cloned()
|
||||
.map(HistoryEntry::Context),
|
||||
);
|
||||
|
||||
history_entries.sort_unstable_by_key(|entry| std::cmp::Reverse(entry.updated_at()));
|
||||
history_entries
|
||||
}
|
||||
|
||||
pub fn recent_entries(&self, limit: usize, cx: &mut Context<Self>) -> Vec<HistoryEntry> {
|
||||
self.entries(cx).into_iter().take(limit).collect()
|
||||
}
|
||||
|
||||
pub fn recently_opened_entries(&self, cx: &App) -> Vec<HistoryEntry> {
|
||||
#[cfg(debug_assertions)]
|
||||
if std::env::var("ZED_SIMULATE_NO_THREAD_HISTORY").is_ok() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let context_entries =
|
||||
self.context_store
|
||||
.read(cx)
|
||||
.unordered_contexts()
|
||||
.flat_map(|context| {
|
||||
self.recently_opened_entries
|
||||
.iter()
|
||||
.enumerate()
|
||||
.flat_map(|(index, entry)| match entry {
|
||||
HistoryEntryId::Context(path) if &context.path == path => {
|
||||
Some((index, HistoryEntry::Context(context.clone())))
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
});
|
||||
|
||||
context_entries
|
||||
// optimization to halt iteration early
|
||||
.take(self.recently_opened_entries.len())
|
||||
.sorted_unstable_by_key(|(index, _)| *index)
|
||||
.map(|(_, entry)| entry)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn save_recently_opened_entries(&mut self, cx: &mut Context<Self>) {
|
||||
let serialized_entries = self
|
||||
.recently_opened_entries
|
||||
.iter()
|
||||
.filter_map(|entry| match entry {
|
||||
HistoryEntryId::Context(path) => path.file_name().map(|file| {
|
||||
SerializedRecentOpen::ContextName(file.to_string_lossy().into_owned())
|
||||
}),
|
||||
HistoryEntryId::Thread(id) => Some(SerializedRecentOpen::Thread(id.to_string())),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
self._save_recently_opened_entries_task = cx.spawn(async move |_, cx| {
|
||||
cx.background_executor()
|
||||
.timer(SAVE_RECENTLY_OPENED_ENTRIES_DEBOUNCE)
|
||||
.await;
|
||||
cx.background_spawn(async move {
|
||||
let path = paths::data_dir().join(NAVIGATION_HISTORY_PATH);
|
||||
let content = serde_json::to_string(&serialized_entries)?;
|
||||
std::fs::write(path, content)?;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.await
|
||||
.log_err();
|
||||
});
|
||||
}
|
||||
|
||||
fn load_recently_opened_entries(cx: &AsyncApp) -> Task<Result<Vec<HistoryEntryId>>> {
|
||||
cx.background_spawn(async move {
|
||||
let path = paths::data_dir().join(NAVIGATION_HISTORY_PATH);
|
||||
let contents = match smol::fs::read_to_string(path).await {
|
||||
Ok(it) => it,
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(e)
|
||||
.context("deserializing persisted agent panel navigation history");
|
||||
}
|
||||
};
|
||||
let entries = serde_json::from_str::<Vec<SerializedRecentOpen>>(&contents)
|
||||
.context("deserializing persisted agent panel navigation history")?
|
||||
.into_iter()
|
||||
.take(MAX_RECENTLY_OPENED_ENTRIES)
|
||||
.flat_map(|entry| match entry {
|
||||
SerializedRecentOpen::Thread(id) => {
|
||||
Some(HistoryEntryId::Thread(id.as_str().into()))
|
||||
}
|
||||
SerializedRecentOpen::ContextName(file_name) => Some(HistoryEntryId::Context(
|
||||
contexts_dir().join(file_name).into(),
|
||||
)),
|
||||
SerializedRecentOpen::Context(path) => {
|
||||
Path::new(&path).file_name().map(|file_name| {
|
||||
HistoryEntryId::Context(contexts_dir().join(file_name).into())
|
||||
})
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
Ok(entries)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn push_recently_opened_entry(&mut self, entry: HistoryEntryId, cx: &mut Context<Self>) {
|
||||
self.recently_opened_entries
|
||||
.retain(|old_entry| old_entry != &entry);
|
||||
self.recently_opened_entries.push_front(entry);
|
||||
self.recently_opened_entries
|
||||
.truncate(MAX_RECENTLY_OPENED_ENTRIES);
|
||||
self.save_recently_opened_entries(cx);
|
||||
}
|
||||
|
||||
pub fn remove_recently_opened_thread(&mut self, id: ThreadId, cx: &mut Context<Self>) {
|
||||
self.recently_opened_entries.retain(
|
||||
|entry| !matches!(entry, HistoryEntryId::Thread(thread_id) if thread_id == &id),
|
||||
);
|
||||
self.save_recently_opened_entries(cx);
|
||||
}
|
||||
|
||||
pub fn replace_recently_opened_text_thread(
|
||||
&mut self,
|
||||
old_path: &Path,
|
||||
new_path: &Arc<Path>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
for entry in &mut self.recently_opened_entries {
|
||||
match entry {
|
||||
HistoryEntryId::Context(path) if path.as_ref() == old_path => {
|
||||
*entry = HistoryEntryId::Context(new_path.clone());
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
self.save_recently_opened_entries(cx);
|
||||
}
|
||||
|
||||
pub fn remove_recently_opened_entry(&mut self, entry: &HistoryEntryId, cx: &mut Context<Self>) {
|
||||
self.recently_opened_entries
|
||||
.retain(|old_entry| old_entry != entry);
|
||||
self.save_recently_opened_entries(cx);
|
||||
}
|
||||
}
|
||||
@@ -790,7 +790,7 @@ mod tests {
|
||||
store.update_user_settings(cx, |settings| {
|
||||
settings.project.all_languages.defaults.format_on_save = Some(FormatOnSave::On);
|
||||
settings.project.all_languages.defaults.formatter =
|
||||
Some(language::language_settings::SelectedFormatter::Auto);
|
||||
Some(language::language_settings::FormatterList::default());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,8 +9,9 @@ use futures::io::BufReader;
|
||||
use project::Project;
|
||||
use project::agent_server_store::AgentServerCommand;
|
||||
use serde::Deserialize;
|
||||
use settings::{Settings as _, SettingsLocation};
|
||||
use task::Shell;
|
||||
use util::ResultExt as _;
|
||||
use util::{ResultExt as _, get_default_system_shell_preferring_bash};
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::{any::Any, cell::RefCell};
|
||||
@@ -22,7 +23,7 @@ use gpui::{App, AppContext as _, AsyncApp, Entity, SharedString, Task, WeakEntit
|
||||
|
||||
use acp_thread::{AcpThread, AuthRequired, LoadError, TerminalProviderEvent};
|
||||
use terminal::TerminalBuilder;
|
||||
use terminal::terminal_settings::{AlternateScroll, CursorShape};
|
||||
use terminal::terminal_settings::{AlternateScroll, CursorShape, TerminalSettings};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[error("Unsupported version")]
|
||||
@@ -168,7 +169,10 @@ impl AcpConnection {
|
||||
meta: None,
|
||||
},
|
||||
terminal: true,
|
||||
meta: None,
|
||||
meta: Some(serde_json::json!({
|
||||
// Experimental: Allow for rendering terminal output from the agents
|
||||
"terminal_output": true,
|
||||
})),
|
||||
},
|
||||
meta: None,
|
||||
})
|
||||
@@ -815,13 +819,25 @@ impl acp::Client for ClientDelegate {
|
||||
let mut env = if let Some(dir) = &args.cwd {
|
||||
project
|
||||
.update(&mut self.cx.clone(), |project, cx| {
|
||||
project.directory_environment(&task::Shell::System, dir.clone().into(), cx)
|
||||
let worktree = project.find_worktree(dir.as_path(), cx);
|
||||
let shell = TerminalSettings::get(
|
||||
worktree.as_ref().map(|(worktree, path)| SettingsLocation {
|
||||
worktree_id: worktree.read(cx).id(),
|
||||
path: &path,
|
||||
}),
|
||||
cx,
|
||||
)
|
||||
.shell
|
||||
.clone();
|
||||
project.directory_environment(&shell, dir.clone().into(), cx)
|
||||
})?
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
// Disables paging for `git` and hopefully other commands
|
||||
env.insert("PAGER".into(), "".into());
|
||||
for var in args.env {
|
||||
env.insert(var.name, var.value);
|
||||
}
|
||||
@@ -834,8 +850,11 @@ impl acp::Client for ClientDelegate {
|
||||
.and_then(|r| r.read(cx).default_system_shell())
|
||||
.map(Shell::Program)
|
||||
})?
|
||||
.unwrap_or(task::Shell::System);
|
||||
let (task_command, task_args) = task::ShellBuilder::new(&shell)
|
||||
.unwrap_or_else(|| Shell::Program(get_default_system_shell_preferring_bash()));
|
||||
let is_windows = project
|
||||
.read_with(&self.cx, |project, cx| project.path_style(cx).is_windows())
|
||||
.unwrap_or(cfg!(windows));
|
||||
let (task_command, task_args) = task::ShellBuilder::new(&shell, is_windows)
|
||||
.redirect_stdin_to_dev_null()
|
||||
.build(Some(args.command.clone()), &args.args);
|
||||
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
use std::{any::Any, path::Path};
|
||||
|
||||
use crate::{AgentServer, AgentServerDelegate, load_proxy_env};
|
||||
use acp_thread::AgentConnection;
|
||||
use agent_client_protocol as acp;
|
||||
use anyhow::{Context as _, Result};
|
||||
use gpui::{App, SharedString, Task};
|
||||
use project::agent_server_store::CODEX_NAME;
|
||||
use fs::Fs;
|
||||
use gpui::{App, AppContext as _, SharedString, Task};
|
||||
use project::agent_server_store::{AllAgentServersSettings, CODEX_NAME};
|
||||
use settings::{SettingsStore, update_settings_file};
|
||||
|
||||
use crate::{AgentServer, AgentServerDelegate, load_proxy_env};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Codex;
|
||||
@@ -30,6 +35,27 @@ impl AgentServer for Codex {
|
||||
ui::IconName::AiOpenAi
|
||||
}
|
||||
|
||||
fn default_mode(&self, cx: &mut App) -> Option<acp::SessionModeId> {
|
||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings.get::<AllAgentServersSettings>(None).codex.clone()
|
||||
});
|
||||
|
||||
settings
|
||||
.as_ref()
|
||||
.and_then(|s| s.default_mode.clone().map(|m| acp::SessionModeId(m.into())))
|
||||
}
|
||||
|
||||
fn set_default_mode(&self, mode_id: Option<acp::SessionModeId>, fs: Arc<dyn Fs>, cx: &mut App) {
|
||||
update_settings_file(fs, cx, |settings, _| {
|
||||
settings
|
||||
.agent_servers
|
||||
.get_or_insert_default()
|
||||
.codex
|
||||
.get_or_insert_default()
|
||||
.default_mode = mode_id.map(|m| m.to_string())
|
||||
});
|
||||
}
|
||||
|
||||
fn connect(
|
||||
&self,
|
||||
root_dir: Option<&Path>,
|
||||
|
||||
@@ -12,7 +12,7 @@ use anyhow::Result;
|
||||
use editor::{CompletionProvider, Editor, ExcerptId};
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{App, Entity, Task, WeakEntity};
|
||||
use language::{Buffer, CodeLabel, HighlightId};
|
||||
use language::{Buffer, CodeLabel, CodeLabelBuilder, HighlightId};
|
||||
use lsp::CompletionContext;
|
||||
use project::lsp_store::{CompletionDocumentation, SymbolLocation};
|
||||
use project::{
|
||||
@@ -27,7 +27,7 @@ use util::rel_path::RelPath;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::AgentPanel;
|
||||
use crate::acp::message_editor::{MessageEditor, MessageEditorEvent};
|
||||
use crate::acp::message_editor::MessageEditor;
|
||||
use crate::context_picker::file_context_picker::{FileMatch, search_files};
|
||||
use crate::context_picker::rules_context_picker::{RulesContextEntry, search_rules};
|
||||
use crate::context_picker::symbol_context_picker::SymbolMatch;
|
||||
@@ -673,7 +673,7 @@ impl ContextPickerCompletionProvider {
|
||||
|
||||
fn build_code_label_for_full_path(file_name: &str, directory: Option<&str>, cx: &App) -> CodeLabel {
|
||||
let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
|
||||
let mut label = CodeLabel::default();
|
||||
let mut label = CodeLabelBuilder::default();
|
||||
|
||||
label.push_str(file_name, None);
|
||||
label.push_str(" ", None);
|
||||
@@ -682,9 +682,7 @@ fn build_code_label_for_full_path(file_name: &str, directory: Option<&str>, cx:
|
||||
label.push_str(directory, comment_id);
|
||||
}
|
||||
|
||||
label.filter_range = 0..label.text().len();
|
||||
|
||||
label
|
||||
label.build()
|
||||
}
|
||||
|
||||
impl CompletionProvider for ContextPickerCompletionProvider {
|
||||
@@ -759,13 +757,13 @@ impl CompletionProvider for ContextPickerCompletionProvider {
|
||||
let editor = editor.clone();
|
||||
move |cx| {
|
||||
editor
|
||||
.update(cx, |_editor, cx| {
|
||||
.update(cx, |editor, cx| {
|
||||
match intent {
|
||||
CompletionIntent::Complete
|
||||
| CompletionIntent::CompleteWithInsert
|
||||
| CompletionIntent::CompleteWithReplace => {
|
||||
if !is_missing_argument {
|
||||
cx.emit(MessageEditorEvent::Send);
|
||||
editor.send(cx);
|
||||
}
|
||||
}
|
||||
CompletionIntent::Compose => {}
|
||||
@@ -775,7 +773,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
|
||||
}
|
||||
});
|
||||
}
|
||||
is_missing_argument
|
||||
false
|
||||
}
|
||||
})),
|
||||
}
|
||||
@@ -910,6 +908,17 @@ impl CompletionProvider for ContextPickerCompletionProvider {
|
||||
offset_to_line,
|
||||
self.prompt_capabilities.borrow().embedded_context,
|
||||
)
|
||||
.filter(|completion| {
|
||||
// Right now we don't support completing arguments of slash commands
|
||||
let is_slash_command_with_argument = matches!(
|
||||
completion,
|
||||
ContextCompletion::SlashCommand(SlashCommandCompletion {
|
||||
argument: Some(_),
|
||||
..
|
||||
})
|
||||
);
|
||||
!is_slash_command_with_argument
|
||||
})
|
||||
.map(|completion| {
|
||||
completion.source_range().start <= offset_to_line + position.column as usize
|
||||
&& completion.source_range().end >= offset_to_line + position.column as usize
|
||||
|
||||
@@ -141,7 +141,9 @@ impl MessageEditor {
|
||||
|
||||
subscriptions.push(cx.subscribe_in(&editor, window, {
|
||||
move |this, editor, event, window, cx| {
|
||||
if let EditorEvent::Edited { .. } = event {
|
||||
if let EditorEvent::Edited { .. } = event
|
||||
&& !editor.read(cx).read_only(cx)
|
||||
{
|
||||
let snapshot = editor.update(cx, |editor, cx| {
|
||||
let new_hints = this
|
||||
.command_hint(editor.buffer(), cx)
|
||||
@@ -823,13 +825,20 @@ impl MessageEditor {
|
||||
});
|
||||
}
|
||||
|
||||
fn send(&mut self, _: &Chat, _: &mut Window, cx: &mut Context<Self>) {
|
||||
pub fn send(&mut self, cx: &mut Context<Self>) {
|
||||
if self.is_empty(cx) {
|
||||
return;
|
||||
}
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.clear_inlay_hints(cx);
|
||||
});
|
||||
cx.emit(MessageEditorEvent::Send)
|
||||
}
|
||||
|
||||
fn chat(&mut self, _: &Chat, _: &mut Window, cx: &mut Context<Self>) {
|
||||
self.send(cx);
|
||||
}
|
||||
|
||||
fn cancel(&mut self, _: &editor::actions::Cancel, _: &mut Window, cx: &mut Context<Self>) {
|
||||
cx.emit(MessageEditorEvent::Cancel)
|
||||
}
|
||||
@@ -1288,7 +1297,7 @@ impl Render for MessageEditor {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.key_context("MessageEditor")
|
||||
.on_action(cx.listener(Self::send))
|
||||
.on_action(cx.listener(Self::chat))
|
||||
.on_action(cx.listener(Self::cancel))
|
||||
.capture_action(cx.listener(Self::paste))
|
||||
.flex_1()
|
||||
@@ -2012,21 +2021,11 @@ mod tests {
|
||||
editor.update_in(&mut cx, |editor, _window, cx| {
|
||||
assert_eq!(editor.text(cx), "/say-hello ");
|
||||
assert_eq!(editor.display_text(cx), "/say-hello <name>");
|
||||
assert!(editor.has_visible_completions_menu());
|
||||
|
||||
assert_eq!(
|
||||
current_completion_labels_with_documentation(editor),
|
||||
&[("say-hello".into(), "Say hello to whoever you want".into())]
|
||||
);
|
||||
assert!(!editor.has_visible_completions_menu());
|
||||
});
|
||||
|
||||
cx.simulate_input("GPT5");
|
||||
|
||||
editor.update_in(&mut cx, |editor, window, cx| {
|
||||
assert!(editor.has_visible_completions_menu());
|
||||
editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx);
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
editor.update_in(&mut cx, |editor, window, cx| {
|
||||
@@ -2035,7 +2034,7 @@ mod tests {
|
||||
assert!(!editor.has_visible_completions_menu());
|
||||
|
||||
// Delete argument
|
||||
for _ in 0..4 {
|
||||
for _ in 0..5 {
|
||||
editor.backspace(&editor::actions::Backspace, window, cx);
|
||||
}
|
||||
});
|
||||
@@ -2043,13 +2042,12 @@ mod tests {
|
||||
cx.run_until_parked();
|
||||
|
||||
editor.update_in(&mut cx, |editor, window, cx| {
|
||||
assert_eq!(editor.text(cx), "/say-hello ");
|
||||
assert_eq!(editor.text(cx), "/say-hello");
|
||||
// Hint is visible because argument was deleted
|
||||
assert_eq!(editor.display_text(cx), "/say-hello <name>");
|
||||
|
||||
// Delete last command letter
|
||||
editor.backspace(&editor::actions::Backspace, window, cx);
|
||||
editor.backspace(&editor::actions::Backspace, window, cx);
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
@@ -174,11 +174,16 @@ impl Render for ModeSelector {
|
||||
|
||||
let this = cx.entity();
|
||||
|
||||
let icon = if self.menu_handle.is_deployed() {
|
||||
IconName::ChevronUp
|
||||
} else {
|
||||
IconName::ChevronDown
|
||||
};
|
||||
|
||||
let trigger_button = Button::new("mode-selector-trigger", current_mode_name)
|
||||
.label_size(LabelSize::Small)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.color(Color::Muted)
|
||||
.icon(IconName::ChevronDown)
|
||||
.icon(icon)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_position(IconPosition::End)
|
||||
.icon_color(Color::Muted)
|
||||
|
||||
@@ -5,12 +5,12 @@ use anyhow::Result;
|
||||
use collections::IndexMap;
|
||||
use futures::FutureExt;
|
||||
use fuzzy::{StringMatchCandidate, match_strings};
|
||||
use gpui::{Action, AsyncWindowContext, BackgroundExecutor, DismissEvent, Task, WeakEntity};
|
||||
use gpui::{AsyncWindowContext, BackgroundExecutor, DismissEvent, Task, WeakEntity};
|
||||
use ordered_float::OrderedFloat;
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use ui::{
|
||||
AnyElement, App, Context, DocumentationAside, DocumentationEdge, DocumentationSide,
|
||||
IntoElement, ListItem, ListItemSpacing, SharedString, Window, prelude::*, rems,
|
||||
DocumentationAside, DocumentationEdge, DocumentationSide, IntoElement, ListItem,
|
||||
ListItemSpacing, prelude::*,
|
||||
};
|
||||
use util::ResultExt;
|
||||
|
||||
@@ -278,36 +278,6 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_footer(
|
||||
&self,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Option<gpui::AnyElement> {
|
||||
Some(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.p_1()
|
||||
.gap_4()
|
||||
.justify_between()
|
||||
.child(
|
||||
Button::new("configure", "Configure")
|
||||
.icon(IconName::Settings)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_position(IconPosition::Start)
|
||||
.on_click(|_, window, cx| {
|
||||
window.dispatch_action(
|
||||
zed_actions::agent::OpenSettings.boxed_clone(),
|
||||
cx,
|
||||
);
|
||||
}),
|
||||
)
|
||||
.into_any(),
|
||||
)
|
||||
}
|
||||
|
||||
fn documentation_aside(
|
||||
&self,
|
||||
_window: &mut Window,
|
||||
@@ -317,7 +287,7 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
||||
let description = description.clone();
|
||||
DocumentationAside::new(
|
||||
DocumentationSide::Left,
|
||||
DocumentationEdge::Bottom,
|
||||
DocumentationEdge::Top,
|
||||
Rc::new(move |_| Label::new(description.clone()).into_any_element()),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -57,30 +57,26 @@ impl Render for AcpModelSelectorPopover {
|
||||
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
|
||||
let color = if self.menu_handle.is_deployed() {
|
||||
Color::Accent
|
||||
let (color, icon) = if self.menu_handle.is_deployed() {
|
||||
(Color::Accent, IconName::ChevronUp)
|
||||
} else {
|
||||
Color::Muted
|
||||
(Color::Muted, IconName::ChevronDown)
|
||||
};
|
||||
|
||||
PickerPopoverMenu::new(
|
||||
self.selector.clone(),
|
||||
ButtonLike::new("active-model")
|
||||
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
.when_some(model_icon, |this, icon| {
|
||||
this.child(Icon::new(icon).color(color).size(IconSize::XSmall))
|
||||
})
|
||||
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
.child(
|
||||
Label::new(model_name)
|
||||
.color(color)
|
||||
.size(LabelSize::Small)
|
||||
.ml_0p5(),
|
||||
)
|
||||
.child(
|
||||
Icon::new(IconName::ChevronDown)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::XSmall),
|
||||
),
|
||||
.child(Icon::new(icon).color(Color::Muted).size(IconSize::XSmall)),
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Change Model",
|
||||
|
||||
@@ -278,7 +278,7 @@ pub struct AcpThreadView {
|
||||
thread_feedback: ThreadFeedbackState,
|
||||
list_state: ListState,
|
||||
auth_task: Option<Task<()>>,
|
||||
expanded_tool_calls: HashSet<acp::ToolCallId>,
|
||||
collapsed_tool_calls: HashSet<acp::ToolCallId>,
|
||||
expanded_thinking_blocks: HashSet<(usize, usize)>,
|
||||
edits_expanded: bool,
|
||||
plan_expanded: bool,
|
||||
@@ -292,6 +292,8 @@ pub struct AcpThreadView {
|
||||
resume_thread_metadata: Option<DbThreadMetadata>,
|
||||
_cancel_task: Option<Task<()>>,
|
||||
_subscriptions: [Subscription; 5],
|
||||
#[cfg(target_os = "windows")]
|
||||
show_codex_windows_warning: bool,
|
||||
}
|
||||
|
||||
enum ThreadState {
|
||||
@@ -335,7 +337,10 @@ impl AcpThreadView {
|
||||
|
||||
let placeholder = if agent.name() == "Zed Agent" {
|
||||
format!("Message the {} — @ to include context", agent.name())
|
||||
} else if agent.name() == "Claude Code" || !available_commands.borrow().is_empty() {
|
||||
} else if agent.name() == "Claude Code"
|
||||
|| agent.name() == "Codex"
|
||||
|| !available_commands.borrow().is_empty()
|
||||
{
|
||||
format!(
|
||||
"Message {} — @ to include context, / for commands",
|
||||
agent.name()
|
||||
@@ -394,6 +399,10 @@ impl AcpThreadView {
|
||||
),
|
||||
];
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
let show_codex_windows_warning = crate::ExternalAgent::parse_built_in(agent.as_ref())
|
||||
== Some(crate::ExternalAgent::Codex);
|
||||
|
||||
Self {
|
||||
agent: agent.clone(),
|
||||
workspace: workspace.clone(),
|
||||
@@ -419,7 +428,7 @@ impl AcpThreadView {
|
||||
thread_error: None,
|
||||
thread_feedback: Default::default(),
|
||||
auth_task: None,
|
||||
expanded_tool_calls: HashSet::default(),
|
||||
collapsed_tool_calls: HashSet::default(),
|
||||
expanded_thinking_blocks: HashSet::default(),
|
||||
editing_message: None,
|
||||
edits_expanded: false,
|
||||
@@ -436,6 +445,8 @@ impl AcpThreadView {
|
||||
focus_handle: cx.focus_handle(),
|
||||
new_server_version_available: None,
|
||||
resume_thread_metadata: resume_thread,
|
||||
#[cfg(target_os = "windows")]
|
||||
show_codex_windows_warning,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -954,17 +965,17 @@ impl AcpThreadView {
|
||||
) {
|
||||
match &event.view_event {
|
||||
ViewEvent::NewDiff(tool_call_id) => {
|
||||
if AgentSettings::get_global(cx).expand_edit_card {
|
||||
self.expanded_tool_calls.insert(tool_call_id.clone());
|
||||
if !AgentSettings::get_global(cx).expand_edit_card {
|
||||
self.collapsed_tool_calls.insert(tool_call_id.clone());
|
||||
}
|
||||
}
|
||||
ViewEvent::NewTerminal(tool_call_id) => {
|
||||
if AgentSettings::get_global(cx).expand_terminal_card {
|
||||
self.expanded_tool_calls.insert(tool_call_id.clone());
|
||||
if !AgentSettings::get_global(cx).expand_terminal_card {
|
||||
self.collapsed_tool_calls.insert(tool_call_id.clone());
|
||||
}
|
||||
}
|
||||
ViewEvent::TerminalMovedToBackground(tool_call_id) => {
|
||||
self.expanded_tool_calls.remove(tool_call_id);
|
||||
self.collapsed_tool_calls.insert(tool_call_id.clone());
|
||||
}
|
||||
ViewEvent::MessageEditorEvent(_editor, MessageEditorEvent::Focus) => {
|
||||
if let Some(thread) = self.thread()
|
||||
@@ -1055,6 +1066,9 @@ impl AcpThreadView {
|
||||
.iter()
|
||||
.any(|command| command.name == "logout");
|
||||
if can_login && !logout_supported {
|
||||
self.message_editor
|
||||
.update(cx, |editor, cx| editor.clear(window, cx));
|
||||
|
||||
let this = cx.weak_entity();
|
||||
let agent = self.agent.clone();
|
||||
window.defer(cx, |window, cx| {
|
||||
@@ -1251,12 +1265,6 @@ impl AcpThreadView {
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn open_agent_diff(&mut self, _: &OpenAgentDiff, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if let Some(thread) = self.thread() {
|
||||
AgentDiffPane::deploy(thread.clone(), self.workspace.clone(), window, cx).log_err();
|
||||
}
|
||||
}
|
||||
|
||||
fn open_edited_buffer(
|
||||
&mut self,
|
||||
buffer: &Entity<Buffer>,
|
||||
@@ -2122,7 +2130,7 @@ impl AcpThreadView {
|
||||
|
||||
let is_collapsible = !tool_call.content.is_empty() && !needs_confirmation;
|
||||
|
||||
let is_open = needs_confirmation || self.expanded_tool_calls.contains(&tool_call.id);
|
||||
let is_open = needs_confirmation || !self.collapsed_tool_calls.contains(&tool_call.id);
|
||||
|
||||
let tool_output_display =
|
||||
if is_open {
|
||||
@@ -2272,9 +2280,9 @@ impl AcpThreadView {
|
||||
let id = tool_call.id.clone();
|
||||
move |this: &mut Self, _, _, cx: &mut Context<Self>| {
|
||||
if is_open {
|
||||
this.expanded_tool_calls.remove(&id);
|
||||
this.collapsed_tool_calls.insert(id.clone());
|
||||
} else {
|
||||
this.expanded_tool_calls.insert(id.clone());
|
||||
this.collapsed_tool_calls.remove(&id);
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
@@ -2476,7 +2484,7 @@ impl AcpThreadView {
|
||||
.icon_color(Color::Muted)
|
||||
.on_click(cx.listener({
|
||||
move |this: &mut Self, _, _, cx: &mut Context<Self>| {
|
||||
this.expanded_tool_calls.remove(&tool_call_id);
|
||||
this.collapsed_tool_calls.insert(tool_call_id.clone());
|
||||
cx.notify();
|
||||
}
|
||||
})),
|
||||
@@ -2754,7 +2762,7 @@ impl AcpThreadView {
|
||||
.map(|path| path.display().to_string())
|
||||
.unwrap_or_else(|| "current directory".to_string());
|
||||
|
||||
let is_expanded = self.expanded_tool_calls.contains(&tool_call.id);
|
||||
let is_expanded = !self.collapsed_tool_calls.contains(&tool_call.id);
|
||||
|
||||
let header = h_flex()
|
||||
.id(header_id)
|
||||
@@ -2889,9 +2897,9 @@ impl AcpThreadView {
|
||||
let id = tool_call.id.clone();
|
||||
move |this, _event, _window, _cx| {
|
||||
if is_expanded {
|
||||
this.expanded_tool_calls.remove(&id);
|
||||
this.collapsed_tool_calls.insert(id.clone());
|
||||
} else {
|
||||
this.expanded_tool_calls.insert(id.clone());
|
||||
this.collapsed_tool_calls.remove(&id);
|
||||
}
|
||||
}
|
||||
})),
|
||||
@@ -5028,6 +5036,49 @@ impl AcpThreadView {
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn render_codex_windows_warning(&self, cx: &mut Context<Self>) -> Option<Callout> {
|
||||
if self.show_codex_windows_warning {
|
||||
Some(
|
||||
Callout::new()
|
||||
.icon(IconName::Warning)
|
||||
.severity(Severity::Warning)
|
||||
.title("Codex on Windows")
|
||||
.description(
|
||||
"For best performance, run Codex in Windows Subsystem for Linux (WSL2)",
|
||||
)
|
||||
.actions_slot(
|
||||
Button::new("open-wsl-modal", "Open in WSL")
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.on_click(cx.listener({
|
||||
move |_, _, window, cx| {
|
||||
window.dispatch_action(
|
||||
zed_actions::wsl_actions::OpenWsl::default().boxed_clone(),
|
||||
cx,
|
||||
);
|
||||
cx.notify();
|
||||
}
|
||||
})),
|
||||
)
|
||||
.dismiss_action(
|
||||
IconButton::new("dismiss", IconName::Close)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.tooltip(Tooltip::text("Dismiss Warning"))
|
||||
.on_click(cx.listener({
|
||||
move |this, _, _, cx| {
|
||||
this.show_codex_windows_warning = false;
|
||||
cx.notify();
|
||||
}
|
||||
})),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn render_thread_error(&self, window: &mut Window, cx: &mut Context<Self>) -> Option<Div> {
|
||||
let content = match self.thread_error.as_ref()? {
|
||||
ThreadError::Other(error) => self.render_any_thread_error(error.clone(), cx),
|
||||
@@ -5442,7 +5493,6 @@ impl Render for AcpThreadView {
|
||||
v_flex()
|
||||
.size_full()
|
||||
.key_context("AcpThread")
|
||||
.on_action(cx.listener(Self::open_agent_diff))
|
||||
.on_action(cx.listener(Self::toggle_burn_mode))
|
||||
.on_action(cx.listener(Self::keep_all))
|
||||
.on_action(cx.listener(Self::reject_all))
|
||||
@@ -5516,6 +5566,16 @@ impl Render for AcpThreadView {
|
||||
_ => this,
|
||||
})
|
||||
.children(self.render_thread_retry_status_callout(window, cx))
|
||||
.children({
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
self.render_codex_windows_warning(cx)
|
||||
}
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
Vec::<Empty>::new()
|
||||
}
|
||||
})
|
||||
.children(self.render_thread_error(window, cx))
|
||||
.when_some(
|
||||
self.new_server_version_available.as_ref().filter(|_| {
|
||||
|
||||
@@ -6,7 +6,6 @@ mod tool_picker;
|
||||
|
||||
use std::{ops::Range, sync::Arc};
|
||||
|
||||
use agent_settings::AgentSettings;
|
||||
use anyhow::Result;
|
||||
use assistant_tool::{ToolSource, ToolWorkingSet};
|
||||
use cloud_llm_client::{Plan, PlanV1, PlanV2};
|
||||
@@ -29,10 +28,10 @@ use project::{
|
||||
agent_server_store::{AgentServerStore, CLAUDE_CODE_NAME, CODEX_NAME, GEMINI_NAME},
|
||||
context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore},
|
||||
};
|
||||
use settings::{Settings, SettingsStore, update_settings_file};
|
||||
use settings::{SettingsStore, update_settings_file};
|
||||
use ui::{
|
||||
Chip, CommonAnimationExt, ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex,
|
||||
Indicator, PopoverMenu, Switch, SwitchColor, SwitchField, Tooltip, WithScrollbar, prelude::*,
|
||||
Indicator, PopoverMenu, Switch, SwitchColor, Tooltip, WithScrollbar, prelude::*,
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
use workspace::{Workspace, create_and_open_local_file};
|
||||
@@ -402,101 +401,6 @@ impl AgentConfiguration {
|
||||
)
|
||||
}
|
||||
|
||||
fn render_command_permission(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let always_allow_tool_actions = AgentSettings::get_global(cx).always_allow_tool_actions;
|
||||
let fs = self.fs.clone();
|
||||
|
||||
SwitchField::new(
|
||||
"always-allow-tool-actions-switch",
|
||||
Some("Allow running commands without asking for confirmation"),
|
||||
Some(
|
||||
"The agent can perform potentially destructive actions without asking for your confirmation.".into(),
|
||||
),
|
||||
always_allow_tool_actions,
|
||||
move |state, _window, cx| {
|
||||
let allow = state == &ToggleState::Selected;
|
||||
update_settings_file(fs.clone(), cx, move |settings, _| {
|
||||
settings.agent.get_or_insert_default().set_always_allow_tool_actions(allow);
|
||||
});
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn render_single_file_review(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let single_file_review = AgentSettings::get_global(cx).single_file_review;
|
||||
let fs = self.fs.clone();
|
||||
|
||||
SwitchField::new(
|
||||
"single-file-review",
|
||||
Some("Enable single-file agent reviews"),
|
||||
Some("Agent edits are also displayed in single-file editors for review.".into()),
|
||||
single_file_review,
|
||||
move |state, _window, cx| {
|
||||
let allow = state == &ToggleState::Selected;
|
||||
update_settings_file(fs.clone(), cx, move |settings, _| {
|
||||
settings
|
||||
.agent
|
||||
.get_or_insert_default()
|
||||
.set_single_file_review(allow);
|
||||
});
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn render_sound_notification(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let play_sound_when_agent_done = AgentSettings::get_global(cx).play_sound_when_agent_done;
|
||||
let fs = self.fs.clone();
|
||||
|
||||
SwitchField::new(
|
||||
"sound-notification",
|
||||
Some("Play sound when finished generating"),
|
||||
Some(
|
||||
"Hear a notification sound when the agent is done generating changes or needs your input.".into(),
|
||||
),
|
||||
play_sound_when_agent_done,
|
||||
move |state, _window, cx| {
|
||||
let allow = state == &ToggleState::Selected;
|
||||
update_settings_file(fs.clone(), cx, move |settings, _| {
|
||||
settings.agent.get_or_insert_default().set_play_sound_when_agent_done(allow);
|
||||
});
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn render_modifier_to_send(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let use_modifier_to_send = AgentSettings::get_global(cx).use_modifier_to_send;
|
||||
let fs = self.fs.clone();
|
||||
|
||||
SwitchField::new(
|
||||
"modifier-send",
|
||||
Some("Use modifier to submit a message"),
|
||||
Some(
|
||||
"Make a modifier (cmd-enter on macOS, ctrl-enter on Linux or Windows) required to send messages.".into(),
|
||||
),
|
||||
use_modifier_to_send,
|
||||
move |state, _window, cx| {
|
||||
let allow = state == &ToggleState::Selected;
|
||||
update_settings_file(fs.clone(), cx, move |settings, _| {
|
||||
settings.agent.get_or_insert_default().set_use_modifier_to_send(allow);
|
||||
});
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn render_general_settings_section(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.p(DynamicSpacing::Base16.rems(cx))
|
||||
.pr(DynamicSpacing::Base20.rems(cx))
|
||||
.gap_2p5()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(Headline::new("General Settings"))
|
||||
.child(self.render_command_permission(cx))
|
||||
.child(self.render_single_file_review(cx))
|
||||
.child(self.render_sound_notification(cx))
|
||||
.child(self.render_modifier_to_send(cx))
|
||||
}
|
||||
|
||||
fn render_zed_plan_info(&self, plan: Option<Plan>, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
if let Some(plan) = plan {
|
||||
let free_chip_bg = cx
|
||||
@@ -1141,7 +1045,6 @@ impl Render for AgentConfiguration {
|
||||
.track_scroll(&self.scroll_handle)
|
||||
.size_full()
|
||||
.overflow_y_scroll()
|
||||
.child(self.render_general_settings_section(cx))
|
||||
.child(self.render_agent_servers_section(cx))
|
||||
.child(self.render_context_servers_section(window, cx))
|
||||
.child(self.render_provider_configuration_section(cx)),
|
||||
|
||||
@@ -7,7 +7,7 @@ use gpui::{Entity, FocusHandle, SharedString};
|
||||
use picker::popover_menu::PickerPopoverMenu;
|
||||
use settings::update_settings_file;
|
||||
use std::sync::Arc;
|
||||
use ui::{ButtonLike, PopoverMenuHandle, Tooltip, prelude::*};
|
||||
use ui::{ButtonLike, PopoverMenuHandle, TintColor, Tooltip, prelude::*};
|
||||
use zed_actions::agent::ToggleModelSelector;
|
||||
|
||||
pub struct AgentModelSelector {
|
||||
@@ -70,6 +70,11 @@ impl Render for AgentModelSelector {
|
||||
.unwrap_or_else(|| SharedString::from("Select a Model"));
|
||||
|
||||
let provider_icon = model.as_ref().map(|model| model.provider.icon());
|
||||
let color = if self.menu_handle.is_deployed() {
|
||||
Color::Accent
|
||||
} else {
|
||||
Color::Muted
|
||||
};
|
||||
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
|
||||
@@ -77,17 +82,18 @@ impl Render for AgentModelSelector {
|
||||
self.selector.clone(),
|
||||
ButtonLike::new("active-model")
|
||||
.when_some(provider_icon, |this, icon| {
|
||||
this.child(Icon::new(icon).color(Color::Muted).size(IconSize::XSmall))
|
||||
this.child(Icon::new(icon).color(color).size(IconSize::XSmall))
|
||||
})
|
||||
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
.child(
|
||||
Label::new(model_name)
|
||||
.color(Color::Muted)
|
||||
.color(color)
|
||||
.size(LabelSize::Small)
|
||||
.ml_0p5(),
|
||||
)
|
||||
.child(
|
||||
Icon::new(IconName::ChevronDown)
|
||||
.color(Color::Muted)
|
||||
.color(color)
|
||||
.size(IconSize::XSmall),
|
||||
),
|
||||
move |window, cx| {
|
||||
@@ -99,10 +105,14 @@ impl Render for AgentModelSelector {
|
||||
cx,
|
||||
)
|
||||
},
|
||||
gpui::Corner::BottomRight,
|
||||
gpui::Corner::TopRight,
|
||||
cx,
|
||||
)
|
||||
.with_handle(self.menu_handle.clone())
|
||||
.offset(gpui::Point {
|
||||
x: px(0.0),
|
||||
y: px(2.0),
|
||||
})
|
||||
.render(window, cx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,9 +19,10 @@ use zed_actions::agent::{OpenClaudeCodeOnboardingModal, ReauthenticateAgent};
|
||||
use crate::acp::{AcpThreadHistory, ThreadHistoryEvent};
|
||||
use crate::ui::{AcpOnboardingModal, ClaudeCodeOnboardingModal};
|
||||
use crate::{
|
||||
AddContextServer, DeleteRecentlyOpenThread, Follow, InlineAssistant, NewTextThread, NewThread,
|
||||
OpenActiveThreadAsMarkdown, OpenHistory, ResetTrialEndUpsell, ResetTrialUpsell,
|
||||
ToggleNavigationMenu, ToggleNewThreadMenu, ToggleOptionsMenu,
|
||||
AddContextServer, AgentDiffPane, DeleteRecentlyOpenThread, Follow, InlineAssistant,
|
||||
NewTextThread, NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff, OpenHistory,
|
||||
ResetTrialEndUpsell, ResetTrialUpsell, ToggleNavigationMenu, ToggleNewThreadMenu,
|
||||
ToggleOptionsMenu,
|
||||
acp::AcpThreadView,
|
||||
agent_configuration::{AgentConfiguration, AssistantConfigurationEvent},
|
||||
slash_command::SlashCommandCompletionProvider,
|
||||
@@ -33,7 +34,6 @@ use crate::{
|
||||
};
|
||||
use agent::{
|
||||
context_store::ContextStore,
|
||||
history_store::{HistoryEntryId, HistoryStore},
|
||||
thread_store::{TextThreadStore, ThreadStore},
|
||||
};
|
||||
use agent_settings::AgentSettings;
|
||||
@@ -140,6 +140,16 @@ pub fn init(cx: &mut App) {
|
||||
.register_action(|workspace, _: &Follow, window, cx| {
|
||||
workspace.follow(CollaboratorId::Agent, window, cx);
|
||||
})
|
||||
.register_action(|workspace, _: &OpenAgentDiff, window, cx| {
|
||||
let thread = workspace
|
||||
.panel::<AgentPanel>(cx)
|
||||
.and_then(|panel| panel.read(cx).active_thread_view().cloned())
|
||||
.and_then(|thread_view| thread_view.read(cx).thread().cloned());
|
||||
|
||||
if let Some(thread) = thread {
|
||||
AgentDiffPane::deploy_in_workspace(thread, workspace, window, cx);
|
||||
}
|
||||
})
|
||||
.register_action(|workspace, _: &ToggleNavigationMenu, window, cx| {
|
||||
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
|
||||
workspace.focus_panel::<AgentPanel>(window, cx);
|
||||
@@ -212,12 +222,11 @@ enum WhichFontSize {
|
||||
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum AgentType {
|
||||
#[default]
|
||||
Zed,
|
||||
NativeAgent,
|
||||
TextThread,
|
||||
Gemini,
|
||||
ClaudeCode,
|
||||
Codex,
|
||||
NativeAgent,
|
||||
Custom {
|
||||
name: SharedString,
|
||||
command: AgentServerCommand,
|
||||
@@ -227,8 +236,7 @@ pub enum AgentType {
|
||||
impl AgentType {
|
||||
fn label(&self) -> SharedString {
|
||||
match self {
|
||||
Self::Zed | Self::TextThread => "Zed Agent".into(),
|
||||
Self::NativeAgent => "Agent 2".into(),
|
||||
Self::NativeAgent | Self::TextThread => "Zed Agent".into(),
|
||||
Self::Gemini => "Gemini CLI".into(),
|
||||
Self::ClaudeCode => "Claude Code".into(),
|
||||
Self::Codex => "Codex".into(),
|
||||
@@ -238,7 +246,7 @@ impl AgentType {
|
||||
|
||||
fn icon(&self) -> Option<IconName> {
|
||||
match self {
|
||||
Self::Zed | Self::NativeAgent | Self::TextThread => None,
|
||||
Self::NativeAgent | Self::TextThread => None,
|
||||
Self::Gemini => Some(IconName::AiGemini),
|
||||
Self::ClaudeCode => Some(IconName::AiClaude),
|
||||
Self::Codex => Some(IconName::AiOpenAi),
|
||||
@@ -298,7 +306,6 @@ impl ActiveView {
|
||||
|
||||
pub fn prompt_editor(
|
||||
context_editor: Entity<TextThreadEditor>,
|
||||
history_store: Entity<HistoryStore>,
|
||||
acp_history_store: Entity<agent2::HistoryStore>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
window: &mut Window,
|
||||
@@ -366,18 +373,6 @@ impl ActiveView {
|
||||
})
|
||||
}
|
||||
ContextEvent::PathChanged { old_path, new_path } => {
|
||||
history_store.update(cx, |history_store, cx| {
|
||||
if let Some(old_path) = old_path {
|
||||
history_store
|
||||
.replace_recently_opened_text_thread(old_path, new_path, cx);
|
||||
} else {
|
||||
history_store.push_recently_opened_entry(
|
||||
HistoryEntryId::Context(new_path.clone()),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
acp_history_store.update(cx, |history_store, cx| {
|
||||
if let Some(old_path) = old_path {
|
||||
history_store
|
||||
@@ -419,7 +414,7 @@ pub struct AgentPanel {
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
thread_store: Entity<ThreadStore>,
|
||||
acp_history: Entity<AcpThreadHistory>,
|
||||
acp_history_store: Entity<agent2::HistoryStore>,
|
||||
history_store: Entity<agent2::HistoryStore>,
|
||||
context_store: Entity<TextThreadStore>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
inline_assist_context_store: Entity<ContextStore>,
|
||||
@@ -427,7 +422,6 @@ pub struct AgentPanel {
|
||||
configuration_subscription: Option<Subscription>,
|
||||
active_view: ActiveView,
|
||||
previous_view: Option<ActiveView>,
|
||||
history_store: Entity<HistoryStore>,
|
||||
new_thread_menu_handle: PopoverMenuHandle<ContextMenu>,
|
||||
agent_panel_menu_handle: PopoverMenuHandle<ContextMenu>,
|
||||
assistant_navigation_menu_handle: PopoverMenuHandle<ContextMenu>,
|
||||
@@ -560,10 +554,8 @@ impl AgentPanel {
|
||||
let inline_assist_context_store =
|
||||
cx.new(|_cx| ContextStore::new(project.downgrade(), Some(thread_store.downgrade())));
|
||||
|
||||
let history_store = cx.new(|cx| HistoryStore::new(context_store.clone(), [], cx));
|
||||
|
||||
let acp_history_store = cx.new(|cx| agent2::HistoryStore::new(context_store.clone(), cx));
|
||||
let acp_history = cx.new(|cx| AcpThreadHistory::new(acp_history_store.clone(), window, cx));
|
||||
let history_store = cx.new(|cx| agent2::HistoryStore::new(context_store.clone(), cx));
|
||||
let acp_history = cx.new(|cx| AcpThreadHistory::new(history_store.clone(), window, cx));
|
||||
cx.subscribe_in(
|
||||
&acp_history,
|
||||
window,
|
||||
@@ -585,14 +577,12 @@ impl AgentPanel {
|
||||
)
|
||||
.detach();
|
||||
|
||||
cx.observe(&history_store, |_, _, cx| cx.notify()).detach();
|
||||
|
||||
let panel_type = AgentSettings::get_global(cx).default_view;
|
||||
let active_view = match panel_type {
|
||||
DefaultView::Thread => ActiveView::native_agent(
|
||||
fs.clone(),
|
||||
prompt_store.clone(),
|
||||
acp_history_store.clone(),
|
||||
history_store.clone(),
|
||||
project.clone(),
|
||||
workspace.clone(),
|
||||
window,
|
||||
@@ -618,7 +608,6 @@ impl AgentPanel {
|
||||
ActiveView::prompt_editor(
|
||||
context_editor,
|
||||
history_store.clone(),
|
||||
acp_history_store.clone(),
|
||||
language_registry.clone(),
|
||||
window,
|
||||
cx,
|
||||
@@ -684,7 +673,6 @@ impl AgentPanel {
|
||||
configuration_subscription: None,
|
||||
inline_assist_context_store,
|
||||
previous_view: None,
|
||||
history_store: history_store.clone(),
|
||||
new_thread_menu_handle: PopoverMenuHandle::default(),
|
||||
agent_panel_menu_handle: PopoverMenuHandle::default(),
|
||||
assistant_navigation_menu_handle: PopoverMenuHandle::default(),
|
||||
@@ -695,7 +683,7 @@ impl AgentPanel {
|
||||
pending_serialization: None,
|
||||
onboarding,
|
||||
acp_history,
|
||||
acp_history_store,
|
||||
history_store,
|
||||
selected_agent: AgentType::default(),
|
||||
loading: false,
|
||||
}
|
||||
@@ -749,7 +737,7 @@ impl AgentPanel {
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(thread) = self
|
||||
.acp_history_store
|
||||
.history_store
|
||||
.read(cx)
|
||||
.thread_from_session_id(&action.from_session_id)
|
||||
else {
|
||||
@@ -798,7 +786,6 @@ impl AgentPanel {
|
||||
ActiveView::prompt_editor(
|
||||
context_editor.clone(),
|
||||
self.history_store.clone(),
|
||||
self.acp_history_store.clone(),
|
||||
self.language_registry.clone(),
|
||||
window,
|
||||
cx,
|
||||
@@ -824,13 +811,13 @@ impl AgentPanel {
|
||||
|
||||
const LAST_USED_EXTERNAL_AGENT_KEY: &str = "agent_panel__last_used_external_agent";
|
||||
|
||||
#[derive(Default, Serialize, Deserialize)]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct LastUsedExternalAgent {
|
||||
agent: crate::ExternalAgent,
|
||||
}
|
||||
|
||||
let loading = self.loading;
|
||||
let history = self.acp_history_store.clone();
|
||||
let history = self.history_store.clone();
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let ext_agent = match agent_choice {
|
||||
@@ -865,18 +852,18 @@ impl AgentPanel {
|
||||
.and_then(|value| {
|
||||
serde_json::from_str::<LastUsedExternalAgent>(&value).log_err()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
.agent
|
||||
.map(|agent| agent.agent)
|
||||
.unwrap_or(ExternalAgent::NativeAgent)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if !loading {
|
||||
telemetry::event!("Agent Thread Started", agent = ext_agent.name());
|
||||
}
|
||||
|
||||
let server = ext_agent.server(fs, history);
|
||||
|
||||
if !loading {
|
||||
telemetry::event!("Agent Thread Started", agent = server.telemetry_id());
|
||||
}
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
let selected_agent = ext_agent.into();
|
||||
if this.selected_agent != selected_agent {
|
||||
@@ -891,7 +878,7 @@ impl AgentPanel {
|
||||
summarize_thread,
|
||||
workspace.clone(),
|
||||
project,
|
||||
this.acp_history_store.clone(),
|
||||
this.history_store.clone(),
|
||||
this.prompt_store.clone(),
|
||||
window,
|
||||
cx,
|
||||
@@ -989,7 +976,6 @@ impl AgentPanel {
|
||||
ActiveView::prompt_editor(
|
||||
editor,
|
||||
self.history_store.clone(),
|
||||
self.acp_history_store.clone(),
|
||||
self.language_registry.clone(),
|
||||
window,
|
||||
cx,
|
||||
@@ -1253,11 +1239,6 @@ impl AgentPanel {
|
||||
match &new_view {
|
||||
ActiveView::TextThread { context_editor, .. } => {
|
||||
self.history_store.update(cx, |store, cx| {
|
||||
if let Some(path) = context_editor.read(cx).context().read(cx).path() {
|
||||
store.push_recently_opened_entry(HistoryEntryId::Context(path.clone()), cx)
|
||||
}
|
||||
});
|
||||
self.acp_history_store.update(cx, |store, cx| {
|
||||
if let Some(path) = context_editor.read(cx).context().read(cx).path() {
|
||||
store.push_recently_opened_entry(
|
||||
agent2::HistoryEntryId::TextThread(path.clone()),
|
||||
@@ -1291,7 +1272,7 @@ impl AgentPanel {
|
||||
) -> ContextMenu {
|
||||
let entries = panel
|
||||
.read(cx)
|
||||
.acp_history_store
|
||||
.history_store
|
||||
.read(cx)
|
||||
.recently_opened_entries(cx);
|
||||
|
||||
@@ -1336,7 +1317,7 @@ impl AgentPanel {
|
||||
move |_window, cx| {
|
||||
panel
|
||||
.update(cx, |this, cx| {
|
||||
this.acp_history_store.update(cx, |history_store, cx| {
|
||||
this.history_store.update(cx, |history_store, cx| {
|
||||
history_store.remove_recently_opened_entry(&id, cx);
|
||||
});
|
||||
})
|
||||
@@ -1362,15 +1343,6 @@ impl AgentPanel {
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match agent {
|
||||
AgentType::Zed => {
|
||||
window.dispatch_action(
|
||||
NewThread {
|
||||
from_thread_id: None,
|
||||
}
|
||||
.boxed_clone(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
AgentType::TextThread => {
|
||||
window.dispatch_action(NewTextThread.boxed_clone(), cx);
|
||||
}
|
||||
@@ -2167,10 +2139,7 @@ impl AgentPanel {
|
||||
false
|
||||
}
|
||||
_ => {
|
||||
let history_is_empty = self.acp_history_store.read(cx).is_empty(cx)
|
||||
&& self
|
||||
.history_store
|
||||
.update(cx, |store, cx| store.recent_entries(1, cx).is_empty());
|
||||
let history_is_empty = self.history_store.read(cx).is_empty(cx);
|
||||
|
||||
let has_configured_non_zed_providers = LanguageModelRegistry::read_global(cx)
|
||||
.providers()
|
||||
|
||||
@@ -161,10 +161,9 @@ pub struct NewNativeAgentThreadFromSummary {
|
||||
}
|
||||
|
||||
// TODO unify this with AgentType
|
||||
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
enum ExternalAgent {
|
||||
#[default]
|
||||
pub enum ExternalAgent {
|
||||
Gemini,
|
||||
ClaudeCode,
|
||||
Codex,
|
||||
@@ -184,13 +183,13 @@ fn placeholder_command() -> AgentServerCommand {
|
||||
}
|
||||
|
||||
impl ExternalAgent {
|
||||
fn name(&self) -> &'static str {
|
||||
match self {
|
||||
Self::NativeAgent => "zed",
|
||||
Self::Gemini => "gemini-cli",
|
||||
Self::ClaudeCode => "claude-code",
|
||||
Self::Codex => "codex",
|
||||
Self::Custom { .. } => "custom",
|
||||
pub fn parse_built_in(server: &dyn agent_servers::AgentServer) -> Option<Self> {
|
||||
match server.telemetry_id() {
|
||||
"gemini-cli" => Some(Self::Gemini),
|
||||
"claude-code" => Some(Self::ClaudeCode),
|
||||
"codex" => Some(Self::Codex),
|
||||
"zed" => Some(Self::NativeAgent),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{App, Entity, Task, WeakEntity};
|
||||
use http_client::HttpClientWithUrl;
|
||||
use itertools::Itertools;
|
||||
use language::{Buffer, CodeLabel, HighlightId};
|
||||
use language::{Buffer, CodeLabel, CodeLabelBuilder, HighlightId};
|
||||
use lsp::CompletionContext;
|
||||
use project::lsp_store::SymbolLocation;
|
||||
use project::{
|
||||
@@ -686,7 +686,8 @@ impl ContextPickerCompletionProvider {
|
||||
};
|
||||
|
||||
let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
|
||||
let mut label = CodeLabel::plain(symbol.name.clone(), None);
|
||||
let mut label = CodeLabelBuilder::default();
|
||||
label.push_str(&symbol.name, None);
|
||||
label.push_str(" ", None);
|
||||
label.push_str(&file_name, comment_id);
|
||||
label.push_str(&format!(" L{}", symbol.range.start.0.row + 1), comment_id);
|
||||
@@ -696,7 +697,7 @@ impl ContextPickerCompletionProvider {
|
||||
Some(Completion {
|
||||
replace_range: source_range.clone(),
|
||||
new_text,
|
||||
label,
|
||||
label: label.build(),
|
||||
documentation: None,
|
||||
source: project::CompletionSource::Custom,
|
||||
icon_path: Some(IconName::Code.path().into()),
|
||||
@@ -729,7 +730,7 @@ impl ContextPickerCompletionProvider {
|
||||
|
||||
fn build_code_label_for_full_path(file_name: &str, directory: Option<&str>, cx: &App) -> CodeLabel {
|
||||
let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
|
||||
let mut label = CodeLabel::default();
|
||||
let mut label = CodeLabelBuilder::default();
|
||||
|
||||
label.push_str(file_name, None);
|
||||
label.push_str(" ", None);
|
||||
@@ -738,9 +739,7 @@ fn build_code_label_for_full_path(file_name: &str, directory: Option<&str>, cx:
|
||||
label.push_str(directory, comment_id);
|
||||
}
|
||||
|
||||
label.filter_range = 0..label.text().len();
|
||||
|
||||
label
|
||||
label.build()
|
||||
}
|
||||
|
||||
impl CompletionProvider for ContextPickerCompletionProvider {
|
||||
|
||||
@@ -144,10 +144,16 @@ impl Render for ProfileSelector {
|
||||
.unwrap_or_else(|| "Unknown".into());
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
|
||||
let icon = if self.picker_handle.is_deployed() {
|
||||
IconName::ChevronUp
|
||||
} else {
|
||||
IconName::ChevronDown
|
||||
};
|
||||
|
||||
let trigger_button = Button::new("profile-selector", selected_profile)
|
||||
.label_size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.icon(IconName::ChevronDown)
|
||||
.icon(icon)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_position(IconPosition::End)
|
||||
.icon_color(Color::Muted)
|
||||
|
||||
@@ -1977,7 +1977,9 @@ impl TextThreadEditor {
|
||||
cx.entity().downgrade(),
|
||||
IconButton::new("trigger", IconName::Plus)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted),
|
||||
.icon_color(Color::Muted)
|
||||
.selected_icon_color(Color::Accent)
|
||||
.selected_style(ButtonStyle::Filled),
|
||||
move |window, cx| {
|
||||
Tooltip::with_meta(
|
||||
"Add Context",
|
||||
@@ -2052,30 +2054,27 @@ impl TextThreadEditor {
|
||||
};
|
||||
|
||||
let focus_handle = self.editor().focus_handle(cx);
|
||||
let (color, icon) = if self.language_model_selector_menu_handle.is_deployed() {
|
||||
(Color::Accent, IconName::ChevronUp)
|
||||
} else {
|
||||
(Color::Muted, IconName::ChevronDown)
|
||||
};
|
||||
|
||||
PickerPopoverMenu::new(
|
||||
self.language_model_selector.clone(),
|
||||
ButtonLike::new("active-model")
|
||||
.style(ButtonStyle::Subtle)
|
||||
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
Icon::new(provider_icon)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::XSmall),
|
||||
)
|
||||
.child(Icon::new(provider_icon).color(color).size(IconSize::XSmall))
|
||||
.child(
|
||||
Label::new(model_name)
|
||||
.color(Color::Muted)
|
||||
.color(color)
|
||||
.size(LabelSize::Small)
|
||||
.ml_0p5(),
|
||||
)
|
||||
.child(
|
||||
Icon::new(IconName::ChevronDown)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::XSmall),
|
||||
),
|
||||
.child(Icon::new(icon).color(color).size(IconSize::XSmall)),
|
||||
),
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
@@ -2086,7 +2085,7 @@ impl TextThreadEditor {
|
||||
cx,
|
||||
)
|
||||
},
|
||||
gpui::Corner::BottomLeft,
|
||||
gpui::Corner::BottomRight,
|
||||
cx,
|
||||
)
|
||||
.with_handle(self.language_model_selector_menu_handle.clone())
|
||||
|
||||
@@ -9,6 +9,7 @@ use anyhow::Result;
|
||||
use futures::StreamExt;
|
||||
use futures::stream::{self, BoxStream};
|
||||
use gpui::{App, SharedString, Task, WeakEntity, Window};
|
||||
use language::CodeLabelBuilder;
|
||||
use language::HighlightId;
|
||||
use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate, OffsetRangeExt};
|
||||
pub use language_model::Role;
|
||||
@@ -328,15 +329,15 @@ impl SlashCommandLine {
|
||||
}
|
||||
|
||||
pub fn create_label_for_command(command_name: &str, arguments: &[&str], cx: &App) -> CodeLabel {
|
||||
let mut label = CodeLabel::default();
|
||||
let mut label = CodeLabelBuilder::default();
|
||||
label.push_str(command_name, None);
|
||||
label.respan_filter_range(None);
|
||||
label.push_str(" ", None);
|
||||
label.push_str(
|
||||
&arguments.join(" "),
|
||||
cx.theme().syntax().highlight_id("comment").map(HighlightId),
|
||||
);
|
||||
label.filter_range = 0..command_name.len();
|
||||
label
|
||||
label.build()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -7,7 +7,7 @@ use futures::Stream;
|
||||
use futures::channel::mpsc;
|
||||
use fuzzy::PathMatch;
|
||||
use gpui::{App, Entity, Task, WeakEntity};
|
||||
use language::{BufferSnapshot, CodeLabel, HighlightId, LineEnding, LspAdapterDelegate};
|
||||
use language::{BufferSnapshot, CodeLabelBuilder, HighlightId, LineEnding, LspAdapterDelegate};
|
||||
use project::{PathMatchCandidateSet, Project};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use smol::stream::StreamExt;
|
||||
@@ -168,7 +168,7 @@ impl SlashCommand for FileSlashCommand {
|
||||
.display(path_style)
|
||||
.to_string();
|
||||
|
||||
let mut label = CodeLabel::default();
|
||||
let mut label = CodeLabelBuilder::default();
|
||||
let file_name = path_match.path.file_name()?;
|
||||
let label_text = if path_match.is_dir {
|
||||
format!("{}/ ", file_name)
|
||||
@@ -178,10 +178,10 @@ impl SlashCommand for FileSlashCommand {
|
||||
|
||||
label.push_str(label_text.as_str(), None);
|
||||
label.push_str(&text, comment_id);
|
||||
label.filter_range = 0..file_name.len();
|
||||
label.respan_filter_range(Some(file_name));
|
||||
|
||||
Some(ArgumentCompletion {
|
||||
label,
|
||||
label: label.build(),
|
||||
new_text: text,
|
||||
after_completion: AfterCompletion::Compose,
|
||||
replace_previous_arguments: false,
|
||||
|
||||
@@ -7,7 +7,7 @@ use collections::{HashMap, HashSet};
|
||||
use editor::Editor;
|
||||
use futures::future::join_all;
|
||||
use gpui::{Task, WeakEntity};
|
||||
use language::{BufferSnapshot, CodeLabel, HighlightId, LspAdapterDelegate};
|
||||
use language::{BufferSnapshot, CodeLabel, CodeLabelBuilder, HighlightId, LspAdapterDelegate};
|
||||
use std::sync::{Arc, atomic::AtomicBool};
|
||||
use ui::{ActiveTheme, App, Window, prelude::*};
|
||||
use util::{ResultExt, paths::PathStyle};
|
||||
@@ -308,10 +308,10 @@ fn create_tab_completion_label(
|
||||
comment_id: Option<HighlightId>,
|
||||
) -> CodeLabel {
|
||||
let (parent_path, file_name) = path_style.split(path);
|
||||
let mut label = CodeLabel::default();
|
||||
let mut label = CodeLabelBuilder::default();
|
||||
label.push_str(file_name, None);
|
||||
label.push_str(" ", None);
|
||||
label.push_str(parent_path.unwrap_or_default(), comment_id);
|
||||
label.filter_range = 0..file_name.len();
|
||||
label
|
||||
label.respan_filter_range(Some(file_name));
|
||||
label.build()
|
||||
}
|
||||
|
||||
@@ -1538,7 +1538,7 @@ mod tests {
|
||||
store.update_user_settings(cx, |settings| {
|
||||
settings.project.all_languages.defaults.format_on_save = Some(FormatOnSave::On);
|
||||
settings.project.all_languages.defaults.formatter =
|
||||
Some(language::language_settings::SelectedFormatter::Auto);
|
||||
Some(language::language_settings::FormatterList::default());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -136,6 +136,7 @@ impl Tool for TerminalTool {
|
||||
}),
|
||||
None => Task::ready(None).shared(),
|
||||
};
|
||||
let is_windows = project.read(cx).path_style(cx).is_windows();
|
||||
let shell = project
|
||||
.update(cx, |project, cx| {
|
||||
project
|
||||
@@ -155,7 +156,7 @@ impl Tool for TerminalTool {
|
||||
let build_cmd = {
|
||||
let input_command = input.command.clone();
|
||||
move || {
|
||||
ShellBuilder::new(&Shell::Program(shell))
|
||||
ShellBuilder::new(&Shell::Program(shell), is_windows)
|
||||
.redirect_stdin_to_dev_null()
|
||||
.build(Some(input_command), &[])
|
||||
}
|
||||
|
||||
@@ -68,10 +68,13 @@ struct Args {
|
||||
#[arg(short, long, overrides_with = "add")]
|
||||
new: bool,
|
||||
/// Sets a custom directory for all user data (e.g., database, extensions, logs).
|
||||
/// This overrides the default platform-specific data directory location.
|
||||
/// On macOS, the default is `~/Library/Application Support/Zed`.
|
||||
/// On Linux/FreeBSD, the default is `$XDG_DATA_HOME/zed`.
|
||||
/// On Windows, the default is `%LOCALAPPDATA%\Zed`.
|
||||
/// This overrides the default platform-specific data directory location:
|
||||
#[cfg_attr(target_os = "macos", doc = "`~/Library/Application Support/Zed`.")]
|
||||
#[cfg_attr(target_os = "windows", doc = "`%LOCALAPPDATA%\\Zed`.")]
|
||||
#[cfg_attr(
|
||||
not(any(target_os = "windows", target_os = "macos")),
|
||||
doc = "`$XDG_DATA_HOME/zed`."
|
||||
)]
|
||||
#[arg(long, value_name = "DIR")]
|
||||
user_data_dir: Option<String>,
|
||||
/// The paths to open in Zed (space-separated).
|
||||
|
||||
@@ -322,6 +322,9 @@ pub struct LanguageModel {
|
||||
pub supports_images: bool,
|
||||
pub supports_thinking: bool,
|
||||
pub supports_max_mode: bool,
|
||||
// only used by OpenAI and xAI
|
||||
#[serde(default)]
|
||||
pub supports_parallel_tool_calls: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use chrono::Duration;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
ops::Range,
|
||||
ops::{Add, Range, Sub},
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
@@ -18,8 +18,8 @@ pub struct PredictEditsRequest {
|
||||
pub excerpt_path: Arc<Path>,
|
||||
/// Within file
|
||||
pub excerpt_range: Range<usize>,
|
||||
/// Within `excerpt`
|
||||
pub cursor_offset: usize,
|
||||
pub excerpt_line_range: Range<Line>,
|
||||
pub cursor_point: Point,
|
||||
/// Within `signatures`
|
||||
pub excerpt_parent: Option<usize>,
|
||||
pub signatures: Vec<Signature>,
|
||||
@@ -47,12 +47,13 @@ pub struct PredictEditsRequest {
|
||||
pub enum PromptFormat {
|
||||
MarkedExcerpt,
|
||||
LabeledSections,
|
||||
NumberedLines,
|
||||
/// Prompt format intended for use via zeta_cli
|
||||
OnlySnippets,
|
||||
}
|
||||
|
||||
impl PromptFormat {
|
||||
pub const DEFAULT: PromptFormat = PromptFormat::LabeledSections;
|
||||
pub const DEFAULT: PromptFormat = PromptFormat::NumberedLines;
|
||||
}
|
||||
|
||||
impl Default for PromptFormat {
|
||||
@@ -73,6 +74,7 @@ impl std::fmt::Display for PromptFormat {
|
||||
PromptFormat::MarkedExcerpt => write!(f, "Marked Excerpt"),
|
||||
PromptFormat::LabeledSections => write!(f, "Labeled Sections"),
|
||||
PromptFormat::OnlySnippets => write!(f, "Only Snippets"),
|
||||
PromptFormat::NumberedLines => write!(f, "Numbered Lines"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -97,7 +99,7 @@ pub struct Signature {
|
||||
pub parent_index: Option<usize>,
|
||||
/// Range of `text` within the file, possibly truncated according to `text_is_truncated`. The
|
||||
/// file is implicitly the file that contains the descendant declaration or excerpt.
|
||||
pub range: Range<usize>,
|
||||
pub range: Range<Line>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -106,7 +108,7 @@ pub struct ReferencedDeclaration {
|
||||
pub text: String,
|
||||
pub text_is_truncated: bool,
|
||||
/// Range of `text` within file, possibly truncated according to `text_is_truncated`
|
||||
pub range: Range<usize>,
|
||||
pub range: Range<Line>,
|
||||
/// Range within `text`
|
||||
pub signature_range: Range<usize>,
|
||||
/// Index within `signatures`.
|
||||
@@ -169,10 +171,36 @@ pub struct DebugInfo {
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Edit {
|
||||
pub path: Arc<Path>,
|
||||
pub range: Range<usize>,
|
||||
pub range: Range<Line>,
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
fn is_default<T: Default + PartialEq>(value: &T) -> bool {
|
||||
*value == T::default()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, PartialOrd, Eq, Ord)]
|
||||
pub struct Point {
|
||||
pub line: Line,
|
||||
pub column: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, PartialOrd, Eq, Ord)]
|
||||
#[serde(transparent)]
|
||||
pub struct Line(pub u32);
|
||||
|
||||
impl Add for Line {
|
||||
type Output = Self;
|
||||
|
||||
fn add(self, rhs: Self) -> Self::Output {
|
||||
Self(self.0 + rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub for Line {
|
||||
type Output = Self;
|
||||
|
||||
fn sub(self, rhs: Self) -> Self::Output {
|
||||
Self(self.0 - rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
//! Zeta2 prompt planning and generation code shared with cloud.
|
||||
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use cloud_llm_client::predict_edits_v3::{self, Event, PromptFormat, ReferencedDeclaration};
|
||||
use cloud_llm_client::predict_edits_v3::{
|
||||
self, Event, Line, Point, PromptFormat, ReferencedDeclaration,
|
||||
};
|
||||
use indoc::indoc;
|
||||
use ordered_float::OrderedFloat;
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
@@ -43,6 +45,42 @@ const LABELED_SECTIONS_SYSTEM_PROMPT: &str = indoc! {r#"
|
||||
}
|
||||
"#};
|
||||
|
||||
const NUMBERED_LINES_SYSTEM_PROMPT: &str = indoc! {r#"
|
||||
# Instructions
|
||||
|
||||
You are a code completion assistant helping a programmer finish their work. Your task is to:
|
||||
|
||||
1. Analyze the edit history to understand what the programmer is trying to achieve
|
||||
2. Identify any incomplete refactoring or changes that need to be finished
|
||||
3. Make the remaining edits that a human programmer would logically make next
|
||||
4. Apply systematic changes consistently across the entire codebase - if you see a pattern starting, complete it everywhere.
|
||||
|
||||
Focus on:
|
||||
- Understanding the intent behind the changes (e.g., improving error handling, refactoring APIs, fixing bugs)
|
||||
- Completing any partially-applied changes across the codebase
|
||||
- Ensuring consistency with the programming style and patterns already established
|
||||
- Making edits that maintain or improve code quality
|
||||
- If the programmer started refactoring one instance of a pattern, find and update ALL similar instances
|
||||
- Don't write a lot of code if you're not sure what to do
|
||||
|
||||
Rules:
|
||||
- Do not just mechanically apply patterns - reason about what changes make sense given the context and the programmer's apparent goals.
|
||||
- Do not just fix syntax errors - look for the broader refactoring pattern and apply it systematically throughout the code.
|
||||
- Write the edits in the unified diff format as shown in the example.
|
||||
|
||||
# Example output:
|
||||
|
||||
```
|
||||
--- a/distill-claude/tmp-outs/edits_history.txt
|
||||
+++ b/distill-claude/tmp-outs/edits_history.txt
|
||||
@@ -1,3 +1,3 @@
|
||||
-
|
||||
-
|
||||
-import sys
|
||||
+import json
|
||||
```
|
||||
"#};
|
||||
|
||||
pub struct PlannedPrompt<'a> {
|
||||
request: &'a predict_edits_v3::PredictEditsRequest,
|
||||
/// Snippets to include in the prompt. These may overlap - they are merged / deduplicated in
|
||||
@@ -55,6 +93,7 @@ pub fn system_prompt(format: PromptFormat) -> &'static str {
|
||||
match format {
|
||||
PromptFormat::MarkedExcerpt => MARKED_EXCERPT_SYSTEM_PROMPT,
|
||||
PromptFormat::LabeledSections => LABELED_SECTIONS_SYSTEM_PROMPT,
|
||||
PromptFormat::NumberedLines => NUMBERED_LINES_SYSTEM_PROMPT,
|
||||
// only intended for use via zeta_cli
|
||||
PromptFormat::OnlySnippets => "",
|
||||
}
|
||||
@@ -63,7 +102,7 @@ pub fn system_prompt(format: PromptFormat) -> &'static str {
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PlannedSnippet<'a> {
|
||||
path: Arc<Path>,
|
||||
range: Range<usize>,
|
||||
range: Range<Line>,
|
||||
text: &'a str,
|
||||
// TODO: Indicate this in the output
|
||||
#[allow(dead_code)]
|
||||
@@ -79,7 +118,7 @@ pub enum DeclarationStyle {
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct SectionLabels {
|
||||
pub excerpt_index: usize,
|
||||
pub section_ranges: Vec<(Arc<Path>, Range<usize>)>,
|
||||
pub section_ranges: Vec<(Arc<Path>, Range<Line>)>,
|
||||
}
|
||||
|
||||
impl<'a> PlannedPrompt<'a> {
|
||||
@@ -196,10 +235,24 @@ impl<'a> PlannedPrompt<'a> {
|
||||
declaration.text.len()
|
||||
));
|
||||
};
|
||||
let signature_start_line = declaration.range.start
|
||||
+ Line(
|
||||
declaration.text[..declaration.signature_range.start]
|
||||
.lines()
|
||||
.count() as u32,
|
||||
);
|
||||
let signature_end_line = signature_start_line
|
||||
+ Line(
|
||||
declaration.text
|
||||
[declaration.signature_range.start..declaration.signature_range.end]
|
||||
.lines()
|
||||
.count() as u32,
|
||||
);
|
||||
let range = signature_start_line..signature_end_line;
|
||||
|
||||
PlannedSnippet {
|
||||
path: declaration.path.clone(),
|
||||
range: (declaration.signature_range.start + declaration.range.start)
|
||||
..(declaration.signature_range.end + declaration.range.start),
|
||||
range,
|
||||
text,
|
||||
text_is_truncated: declaration.text_is_truncated,
|
||||
}
|
||||
@@ -318,7 +371,7 @@ impl<'a> PlannedPrompt<'a> {
|
||||
}
|
||||
let excerpt_snippet = PlannedSnippet {
|
||||
path: self.request.excerpt_path.clone(),
|
||||
range: self.request.excerpt_range.clone(),
|
||||
range: self.request.excerpt_line_range.clone(),
|
||||
text: &self.request.excerpt,
|
||||
text_is_truncated: false,
|
||||
};
|
||||
@@ -328,32 +381,33 @@ impl<'a> PlannedPrompt<'a> {
|
||||
let mut excerpt_file_insertions = match self.request.prompt_format {
|
||||
PromptFormat::MarkedExcerpt => vec![
|
||||
(
|
||||
self.request.excerpt_range.start,
|
||||
Point {
|
||||
line: self.request.excerpt_line_range.start,
|
||||
column: 0,
|
||||
},
|
||||
EDITABLE_REGION_START_MARKER_WITH_NEWLINE,
|
||||
),
|
||||
(self.request.cursor_point, CURSOR_MARKER),
|
||||
(
|
||||
self.request.excerpt_range.start + self.request.cursor_offset,
|
||||
CURSOR_MARKER,
|
||||
),
|
||||
(
|
||||
self.request
|
||||
.excerpt_range
|
||||
.end
|
||||
.saturating_sub(0)
|
||||
.max(self.request.excerpt_range.start),
|
||||
Point {
|
||||
line: self.request.excerpt_line_range.end,
|
||||
column: 0,
|
||||
},
|
||||
EDITABLE_REGION_END_MARKER_WITH_NEWLINE,
|
||||
),
|
||||
],
|
||||
PromptFormat::LabeledSections => vec![(
|
||||
self.request.excerpt_range.start + self.request.cursor_offset,
|
||||
CURSOR_MARKER,
|
||||
)],
|
||||
PromptFormat::LabeledSections => vec![(self.request.cursor_point, CURSOR_MARKER)],
|
||||
PromptFormat::NumberedLines => vec![(self.request.cursor_point, CURSOR_MARKER)],
|
||||
PromptFormat::OnlySnippets => vec![],
|
||||
};
|
||||
|
||||
let mut prompt = String::new();
|
||||
prompt.push_str("## User Edits\n\n");
|
||||
Self::push_events(&mut prompt, &self.request.events);
|
||||
if self.request.events.is_empty() {
|
||||
prompt.push_str("No edits yet.\n");
|
||||
} else {
|
||||
Self::push_events(&mut prompt, &self.request.events);
|
||||
}
|
||||
|
||||
prompt.push_str("\n## Code\n\n");
|
||||
let section_labels =
|
||||
@@ -391,13 +445,17 @@ impl<'a> PlannedPrompt<'a> {
|
||||
if *predicted {
|
||||
writeln!(
|
||||
output,
|
||||
"User accepted prediction {:?}:\n```diff\n{}\n```\n",
|
||||
"User accepted prediction {:?}:\n`````diff\n{}\n`````\n",
|
||||
path, diff
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
writeln!(output, "User edited {:?}:\n```diff\n{}\n```\n", path, diff)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
"User edited {:?}:\n`````diff\n{}\n`````\n",
|
||||
path, diff
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -407,7 +465,7 @@ impl<'a> PlannedPrompt<'a> {
|
||||
fn push_file_snippets(
|
||||
&self,
|
||||
output: &mut String,
|
||||
excerpt_file_insertions: &mut Vec<(usize, &'static str)>,
|
||||
excerpt_file_insertions: &mut Vec<(Point, &'static str)>,
|
||||
file_snippets: Vec<(&'a Path, Vec<&'a PlannedSnippet>, bool)>,
|
||||
) -> Result<SectionLabels> {
|
||||
let mut section_ranges = Vec::new();
|
||||
@@ -417,15 +475,13 @@ impl<'a> PlannedPrompt<'a> {
|
||||
snippets.sort_by_key(|s| (s.range.start, Reverse(s.range.end)));
|
||||
|
||||
// TODO: What if the snippets get expanded too large to be editable?
|
||||
let mut current_snippet: Option<(&PlannedSnippet, Range<usize>)> = None;
|
||||
let mut disjoint_snippets: Vec<(&PlannedSnippet, Range<usize>)> = Vec::new();
|
||||
let mut current_snippet: Option<(&PlannedSnippet, Range<Line>)> = None;
|
||||
let mut disjoint_snippets: Vec<(&PlannedSnippet, Range<Line>)> = Vec::new();
|
||||
for snippet in snippets {
|
||||
if let Some((_, current_snippet_range)) = current_snippet.as_mut()
|
||||
&& snippet.range.start < current_snippet_range.end
|
||||
&& snippet.range.start <= current_snippet_range.end
|
||||
{
|
||||
if snippet.range.end > current_snippet_range.end {
|
||||
current_snippet_range.end = snippet.range.end;
|
||||
}
|
||||
current_snippet_range.end = current_snippet_range.end.max(snippet.range.end);
|
||||
continue;
|
||||
}
|
||||
if let Some(current_snippet) = current_snippet.take() {
|
||||
@@ -437,21 +493,24 @@ impl<'a> PlannedPrompt<'a> {
|
||||
disjoint_snippets.push(current_snippet);
|
||||
}
|
||||
|
||||
writeln!(output, "```{}", file_path.display()).ok();
|
||||
// TODO: remove filename=?
|
||||
writeln!(output, "`````filename={}", file_path.display()).ok();
|
||||
let mut skipped_last_snippet = false;
|
||||
for (snippet, range) in disjoint_snippets {
|
||||
let section_index = section_ranges.len();
|
||||
|
||||
match self.request.prompt_format {
|
||||
PromptFormat::MarkedExcerpt | PromptFormat::OnlySnippets => {
|
||||
if range.start > 0 && !skipped_last_snippet {
|
||||
PromptFormat::MarkedExcerpt
|
||||
| PromptFormat::OnlySnippets
|
||||
| PromptFormat::NumberedLines => {
|
||||
if range.start.0 > 0 && !skipped_last_snippet {
|
||||
output.push_str("…\n");
|
||||
}
|
||||
}
|
||||
PromptFormat::LabeledSections => {
|
||||
if is_excerpt_file
|
||||
&& range.start <= self.request.excerpt_range.start
|
||||
&& range.end >= self.request.excerpt_range.end
|
||||
&& range.start <= self.request.excerpt_line_range.start
|
||||
&& range.end >= self.request.excerpt_line_range.end
|
||||
{
|
||||
writeln!(output, "<|current_section|>").ok();
|
||||
} else {
|
||||
@@ -460,46 +519,83 @@ impl<'a> PlannedPrompt<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
let push_full_snippet = |output: &mut String| {
|
||||
if self.request.prompt_format == PromptFormat::NumberedLines {
|
||||
for (i, line) in snippet.text.lines().enumerate() {
|
||||
writeln!(output, "{}|{}", i as u32 + range.start.0 + 1, line)?;
|
||||
}
|
||||
} else {
|
||||
output.push_str(&snippet.text);
|
||||
}
|
||||
anyhow::Ok(())
|
||||
};
|
||||
|
||||
if is_excerpt_file {
|
||||
if self.request.prompt_format == PromptFormat::OnlySnippets {
|
||||
if range.start >= self.request.excerpt_range.start
|
||||
&& range.end <= self.request.excerpt_range.end
|
||||
if range.start >= self.request.excerpt_line_range.start
|
||||
&& range.end <= self.request.excerpt_line_range.end
|
||||
{
|
||||
skipped_last_snippet = true;
|
||||
} else {
|
||||
skipped_last_snippet = false;
|
||||
output.push_str(snippet.text);
|
||||
}
|
||||
} else {
|
||||
let mut last_offset = range.start;
|
||||
let mut i = 0;
|
||||
while i < excerpt_file_insertions.len() {
|
||||
let (offset, insertion) = &excerpt_file_insertions[i];
|
||||
let found = *offset >= range.start && *offset <= range.end;
|
||||
} else if !excerpt_file_insertions.is_empty() {
|
||||
let lines = snippet.text.lines().collect::<Vec<_>>();
|
||||
let push_line = |output: &mut String, line_ix: usize| {
|
||||
if self.request.prompt_format == PromptFormat::NumberedLines {
|
||||
write!(output, "{}|", line_ix as u32 + range.start.0 + 1)?;
|
||||
}
|
||||
anyhow::Ok(writeln!(output, "{}", lines[line_ix])?)
|
||||
};
|
||||
let mut last_line_ix = 0;
|
||||
let mut insertion_ix = 0;
|
||||
while insertion_ix < excerpt_file_insertions.len() {
|
||||
let (point, insertion) = &excerpt_file_insertions[insertion_ix];
|
||||
let found = point.line >= range.start && point.line <= range.end;
|
||||
if found {
|
||||
excerpt_index = Some(section_index);
|
||||
output.push_str(
|
||||
&snippet.text[last_offset - range.start..offset - range.start],
|
||||
);
|
||||
output.push_str(insertion);
|
||||
last_offset = *offset;
|
||||
excerpt_file_insertions.remove(i);
|
||||
let insertion_line_ix = (point.line.0 - range.start.0) as usize;
|
||||
for line_ix in last_line_ix..insertion_line_ix {
|
||||
push_line(output, line_ix)?;
|
||||
}
|
||||
if let Some(next_line) = lines.get(insertion_line_ix) {
|
||||
if self.request.prompt_format == PromptFormat::NumberedLines {
|
||||
write!(
|
||||
output,
|
||||
"{}|",
|
||||
insertion_line_ix as u32 + range.start.0 + 1
|
||||
)?
|
||||
}
|
||||
output.push_str(&next_line[..point.column as usize]);
|
||||
output.push_str(insertion);
|
||||
writeln!(output, "{}", &next_line[point.column as usize..])?;
|
||||
} else {
|
||||
writeln!(output, "{}", insertion)?;
|
||||
}
|
||||
last_line_ix = insertion_line_ix + 1;
|
||||
excerpt_file_insertions.remove(insertion_ix);
|
||||
continue;
|
||||
}
|
||||
i += 1;
|
||||
insertion_ix += 1;
|
||||
}
|
||||
skipped_last_snippet = false;
|
||||
output.push_str(&snippet.text[last_offset - range.start..]);
|
||||
for line_ix in last_line_ix..lines.len() {
|
||||
push_line(output, line_ix)?;
|
||||
}
|
||||
} else {
|
||||
skipped_last_snippet = false;
|
||||
push_full_snippet(output)?;
|
||||
}
|
||||
} else {
|
||||
skipped_last_snippet = false;
|
||||
output.push_str(snippet.text);
|
||||
push_full_snippet(output)?;
|
||||
}
|
||||
|
||||
section_ranges.push((snippet.path.clone(), range));
|
||||
}
|
||||
|
||||
output.push_str("```\n\n");
|
||||
output.push_str("`````\n\n");
|
||||
}
|
||||
|
||||
Ok(SectionLabels {
|
||||
|
||||
@@ -30,9 +30,9 @@ impl fmt::Display for ZedVersion {
|
||||
|
||||
impl ZedVersion {
|
||||
pub fn can_collaborate(&self) -> bool {
|
||||
// v0.198.4 is the first version where we no longer connect to Collab automatically.
|
||||
// We reject any clients older than that to prevent them from connecting to Collab just for authentication.
|
||||
if self.0 < SemanticVersion::new(0, 198, 4) {
|
||||
// v0.204.1 was the first version after the auto-update bug.
|
||||
// We reject any clients older than that to hope we can persuade them to upgrade.
|
||||
if self.0 < SemanticVersion::new(0, 204, 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ use gpui::{
|
||||
use language::{
|
||||
Diagnostic, DiagnosticEntry, DiagnosticSourceKind, FakeLspAdapter, Language, LanguageConfig,
|
||||
LanguageMatcher, LineEnding, OffsetRangeExt, Point, Rope,
|
||||
language_settings::{Formatter, FormatterList, SelectedFormatter},
|
||||
language_settings::{Formatter, FormatterList},
|
||||
tree_sitter_rust, tree_sitter_typescript,
|
||||
};
|
||||
use lsp::{LanguageServerId, OneOf};
|
||||
@@ -39,7 +39,7 @@ use project::{
|
||||
use prompt_store::PromptBuilder;
|
||||
use rand::prelude::*;
|
||||
use serde_json::json;
|
||||
use settings::{PrettierSettingsContent, SettingsStore};
|
||||
use settings::{LanguageServerFormatterSpecifier, PrettierSettingsContent, SettingsStore};
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
env, future, mem,
|
||||
@@ -4610,14 +4610,13 @@ async fn test_formatting_buffer(
|
||||
cx_a.update(|cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings(cx, |file| {
|
||||
file.project.all_languages.defaults.formatter = Some(SelectedFormatter::List(
|
||||
FormatterList::Single(Formatter::External {
|
||||
file.project.all_languages.defaults.formatter =
|
||||
Some(FormatterList::Single(Formatter::External {
|
||||
command: "awk".into(),
|
||||
arguments: Some(
|
||||
vec!["{sub(/two/,\"{buffer_path}\")}1".to_string()].into(),
|
||||
),
|
||||
}),
|
||||
));
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -4708,7 +4707,7 @@ async fn test_prettier_formatting_buffer(
|
||||
cx_a.update(|cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings(cx, |file| {
|
||||
file.project.all_languages.defaults.formatter = Some(SelectedFormatter::Auto);
|
||||
file.project.all_languages.defaults.formatter = Some(FormatterList::default());
|
||||
file.project.all_languages.defaults.prettier = Some(PrettierSettingsContent {
|
||||
allowed: Some(true),
|
||||
..Default::default()
|
||||
@@ -4719,8 +4718,8 @@ async fn test_prettier_formatting_buffer(
|
||||
cx_b.update(|cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings(cx, |file| {
|
||||
file.project.all_languages.defaults.formatter = Some(SelectedFormatter::List(
|
||||
FormatterList::Single(Formatter::LanguageServer { name: None }),
|
||||
file.project.all_languages.defaults.formatter = Some(FormatterList::Single(
|
||||
Formatter::LanguageServer(LanguageServerFormatterSpecifier::Current),
|
||||
));
|
||||
file.project.all_languages.defaults.prettier = Some(PrettierSettingsContent {
|
||||
allowed: Some(true),
|
||||
|
||||
@@ -14,7 +14,7 @@ use gpui::{
|
||||
use http_client::BlockedHttpClient;
|
||||
use language::{
|
||||
FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, LanguageRegistry,
|
||||
language_settings::{Formatter, FormatterList, SelectedFormatter, language_settings},
|
||||
language_settings::{Formatter, FormatterList, language_settings},
|
||||
tree_sitter_typescript,
|
||||
};
|
||||
use node_runtime::NodeRuntime;
|
||||
@@ -27,7 +27,7 @@ use remote::RemoteClient;
|
||||
use remote_server::{HeadlessAppState, HeadlessProject};
|
||||
use rpc::proto;
|
||||
use serde_json::json;
|
||||
use settings::{PrettierSettingsContent, SettingsStore};
|
||||
use settings::{LanguageServerFormatterSpecifier, PrettierSettingsContent, SettingsStore};
|
||||
use std::{
|
||||
path::Path,
|
||||
sync::{Arc, atomic::AtomicUsize},
|
||||
@@ -491,7 +491,7 @@ async fn test_ssh_collaboration_formatting_with_prettier(
|
||||
cx_a.update(|cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings(cx, |file| {
|
||||
file.project.all_languages.defaults.formatter = Some(SelectedFormatter::Auto);
|
||||
file.project.all_languages.defaults.formatter = Some(FormatterList::default());
|
||||
file.project.all_languages.defaults.prettier = Some(PrettierSettingsContent {
|
||||
allowed: Some(true),
|
||||
..Default::default()
|
||||
@@ -502,8 +502,8 @@ async fn test_ssh_collaboration_formatting_with_prettier(
|
||||
cx_b.update(|cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings(cx, |file| {
|
||||
file.project.all_languages.defaults.formatter = Some(SelectedFormatter::List(
|
||||
FormatterList::Single(Formatter::LanguageServer { name: None }),
|
||||
file.project.all_languages.defaults.formatter = Some(FormatterList::Single(
|
||||
Formatter::LanguageServer(LanguageServerFormatterSpecifier::Current),
|
||||
));
|
||||
file.project.all_languages.defaults.prettier = Some(PrettierSettingsContent {
|
||||
allowed: Some(true),
|
||||
@@ -550,7 +550,7 @@ async fn test_ssh_collaboration_formatting_with_prettier(
|
||||
cx_a.update(|cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings(cx, |file| {
|
||||
file.project.all_languages.defaults.formatter = Some(SelectedFormatter::Auto);
|
||||
file.project.all_languages.defaults.formatter = Some(FormatterList::default());
|
||||
file.project.all_languages.defaults.prettier = Some(PrettierSettingsContent {
|
||||
allowed: Some(true),
|
||||
..Default::default()
|
||||
|
||||
@@ -2250,7 +2250,7 @@ impl CollabPanel {
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
div().flex().w_full().items_center().child(
|
||||
v_flex().w_full().items_center().child(
|
||||
Label::new("Sign in to enable collaboration.")
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small),
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
[package]
|
||||
name = "zed-collections"
|
||||
name = "collections"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish = true
|
||||
publish = false
|
||||
license = "Apache-2.0"
|
||||
description = "Standard collection type re-exports used by Zed and GPUI"
|
||||
|
||||
|
||||
@@ -92,7 +92,10 @@ pub async fn init(crash_init: InitCrashHandler) {
|
||||
#[cfg(target_os = "macos")]
|
||||
suspend_all_other_threads();
|
||||
|
||||
client.ping().unwrap();
|
||||
// on macos this "ping" is needed to ensure that all our
|
||||
// `client.send_message` calls have been processed before we trigger the
|
||||
// minidump request.
|
||||
client.ping().ok();
|
||||
client.request_dump(crash_context).is_ok()
|
||||
} else {
|
||||
true
|
||||
|
||||
@@ -262,11 +262,15 @@ impl TransportDelegate {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up logs by trimming unnecessary whitespace/newlines before inserting into log.
|
||||
let line = line.trim();
|
||||
|
||||
log::debug!("stderr: {line}");
|
||||
|
||||
for (kind, handler) in log_handlers.lock().iter_mut() {
|
||||
if matches!(kind, LogKind::Adapter) {
|
||||
handler(iokind, None, line.as_str());
|
||||
handler(iokind, None, line);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -649,7 +653,7 @@ impl Drop for TcpTransport {
|
||||
}
|
||||
|
||||
pub struct StdioTransport {
|
||||
process: Mutex<Option<Child>>,
|
||||
process: Mutex<Child>,
|
||||
_stderr_task: Option<Task<()>>,
|
||||
}
|
||||
|
||||
@@ -676,7 +680,7 @@ impl StdioTransport {
|
||||
|
||||
let mut process = Child::spawn(command, Stdio::piped())?;
|
||||
|
||||
let err_task = process.stderr.take().map(|stderr| {
|
||||
let _stderr_task = process.stderr.take().map(|stderr| {
|
||||
cx.background_spawn(TransportDelegate::handle_adapter_log(
|
||||
stderr,
|
||||
IoKind::StdErr,
|
||||
@@ -684,24 +688,22 @@ impl StdioTransport {
|
||||
))
|
||||
});
|
||||
|
||||
let process = Mutex::new(Some(process));
|
||||
let process = Mutex::new(process);
|
||||
|
||||
Ok(Self {
|
||||
process,
|
||||
_stderr_task: err_task,
|
||||
_stderr_task,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Transport for StdioTransport {
|
||||
fn has_adapter_logs(&self) -> bool {
|
||||
false
|
||||
true
|
||||
}
|
||||
|
||||
fn kill(&mut self) {
|
||||
if let Some(process) = &mut *self.process.lock() {
|
||||
process.kill();
|
||||
}
|
||||
self.process.lock().kill();
|
||||
}
|
||||
|
||||
fn connect(
|
||||
@@ -713,8 +715,7 @@ impl Transport for StdioTransport {
|
||||
)>,
|
||||
> {
|
||||
let result = util::maybe!({
|
||||
let mut guard = self.process.lock();
|
||||
let process = guard.as_mut().context("oops")?;
|
||||
let mut process = self.process.lock();
|
||||
Ok((
|
||||
Box::new(process.stdin.take().context("Cannot reconnect")?) as _,
|
||||
Box::new(process.stdout.take().context("Cannot reconnect")?) as _,
|
||||
@@ -730,9 +731,7 @@ impl Transport for StdioTransport {
|
||||
|
||||
impl Drop for StdioTransport {
|
||||
fn drop(&mut self) {
|
||||
if let Some(process) = &mut *self.process.lock() {
|
||||
process.kill();
|
||||
}
|
||||
self.process.lock().kill();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::{collections::HashMap, path::PathBuf, sync::OnceLock};
|
||||
use std::{path::PathBuf, sync::OnceLock};
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use async_trait::async_trait;
|
||||
@@ -377,6 +377,12 @@ impl DebugAdapter for CodeLldbDebugAdapter {
|
||||
command = Some(path);
|
||||
};
|
||||
let mut json_config = config.config.clone();
|
||||
|
||||
// Enable info level for CodeLLDB by default.
|
||||
// Logs can then be viewed in our DAP logs.
|
||||
let mut envs = collections::HashMap::default();
|
||||
envs.insert("RUST_LOG".to_string(), "info".to_string());
|
||||
|
||||
Ok(DebugAdapterBinary {
|
||||
command: Some(command.unwrap()),
|
||||
cwd: Some(delegate.worktree_root_path().to_path_buf()),
|
||||
@@ -401,7 +407,7 @@ impl DebugAdapter for CodeLldbDebugAdapter {
|
||||
request_args: self
|
||||
.request_args(delegate, json_config, &config.label)
|
||||
.await?,
|
||||
envs: HashMap::default(),
|
||||
envs,
|
||||
connection: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use crate::*;
|
||||
use anyhow::Context as _;
|
||||
use anyhow::{Context as _, bail};
|
||||
use dap::{DebugRequest, StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
|
||||
use fs::RemoveOptions;
|
||||
use futures::{StreamExt, TryStreamExt};
|
||||
use gpui::http_client::AsyncBody;
|
||||
use gpui::{AsyncApp, SharedString};
|
||||
use json_dotpath::DotPaths;
|
||||
use language::LanguageName;
|
||||
use language::{LanguageName, Toolchain};
|
||||
use paths::debug_adapters_dir;
|
||||
use serde_json::Value;
|
||||
use smol::fs::File;
|
||||
@@ -20,7 +20,8 @@ use std::{
|
||||
ffi::OsStr,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use util::{ResultExt, maybe, paths::PathStyle, rel_path::RelPath};
|
||||
use util::command::new_smol_command;
|
||||
use util::{ResultExt, paths::PathStyle, rel_path::RelPath};
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct PythonDebugAdapter {
|
||||
@@ -92,12 +93,16 @@ impl PythonDebugAdapter {
|
||||
})
|
||||
}
|
||||
|
||||
async fn fetch_wheel(&self, delegate: &Arc<dyn DapDelegate>) -> Result<Arc<Path>, String> {
|
||||
async fn fetch_wheel(
|
||||
&self,
|
||||
toolchain: Option<Toolchain>,
|
||||
delegate: &Arc<dyn DapDelegate>,
|
||||
) -> Result<Arc<Path>> {
|
||||
let download_dir = debug_adapters_dir().join(Self::ADAPTER_NAME).join("wheels");
|
||||
std::fs::create_dir_all(&download_dir).map_err(|e| e.to_string())?;
|
||||
let system_python = self.base_venv_path(delegate).await?;
|
||||
std::fs::create_dir_all(&download_dir)?;
|
||||
let venv_python = self.base_venv_path(toolchain, delegate).await?;
|
||||
|
||||
let installation_succeeded = util::command::new_smol_command(system_python.as_ref())
|
||||
let installation_succeeded = util::command::new_smol_command(venv_python.as_ref())
|
||||
.args([
|
||||
"-m",
|
||||
"pip",
|
||||
@@ -109,36 +114,36 @@ impl PythonDebugAdapter {
|
||||
])
|
||||
.output()
|
||||
.await
|
||||
.map_err(|e| format!("{e}"))?
|
||||
.context("spawn system python")?
|
||||
.status
|
||||
.success();
|
||||
if !installation_succeeded {
|
||||
return Err("debugpy installation failed (could not fetch Debugpy's wheel)".into());
|
||||
bail!("debugpy installation failed (could not fetch Debugpy's wheel)");
|
||||
}
|
||||
|
||||
let wheel_path = std::fs::read_dir(&download_dir)
|
||||
.map_err(|e| e.to_string())?
|
||||
let wheel_path = std::fs::read_dir(&download_dir)?
|
||||
.find_map(|entry| {
|
||||
entry.ok().filter(|e| {
|
||||
e.file_type().is_ok_and(|typ| typ.is_file())
|
||||
&& Path::new(&e.file_name()).extension() == Some("whl".as_ref())
|
||||
})
|
||||
})
|
||||
.ok_or_else(|| String::from("Did not find a .whl in {download_dir}"))?;
|
||||
.with_context(|| format!("Did not find a .whl in {download_dir:?}"))?;
|
||||
|
||||
util::archive::extract_zip(
|
||||
&debug_adapters_dir().join(Self::ADAPTER_NAME),
|
||||
File::open(&wheel_path.path())
|
||||
.await
|
||||
.map_err(|e| e.to_string())?,
|
||||
File::open(&wheel_path.path()).await?,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
.await?;
|
||||
|
||||
Ok(Arc::from(wheel_path.path()))
|
||||
}
|
||||
|
||||
async fn maybe_fetch_new_wheel(&self, delegate: &Arc<dyn DapDelegate>) {
|
||||
async fn maybe_fetch_new_wheel(
|
||||
&self,
|
||||
toolchain: Option<Toolchain>,
|
||||
delegate: &Arc<dyn DapDelegate>,
|
||||
) -> Result<()> {
|
||||
let latest_release = delegate
|
||||
.http_client()
|
||||
.get(
|
||||
@@ -148,62 +153,61 @@ impl PythonDebugAdapter {
|
||||
)
|
||||
.await
|
||||
.log_err();
|
||||
maybe!(async move {
|
||||
let response = latest_release.filter(|response| response.status().is_success())?;
|
||||
let response = latest_release
|
||||
.filter(|response| response.status().is_success())
|
||||
.context("getting latest release")?;
|
||||
|
||||
let download_dir = debug_adapters_dir().join(Self::ADAPTER_NAME);
|
||||
std::fs::create_dir_all(&download_dir).ok()?;
|
||||
let download_dir = debug_adapters_dir().join(Self::ADAPTER_NAME);
|
||||
std::fs::create_dir_all(&download_dir)?;
|
||||
|
||||
let mut output = String::new();
|
||||
response
|
||||
.into_body()
|
||||
.read_to_string(&mut output)
|
||||
.await
|
||||
.ok()?;
|
||||
let as_json = serde_json::Value::from_str(&output).ok()?;
|
||||
let latest_version = as_json.get("info").and_then(|info| {
|
||||
let mut output = String::new();
|
||||
response.into_body().read_to_string(&mut output).await?;
|
||||
let as_json = serde_json::Value::from_str(&output)?;
|
||||
let latest_version = as_json
|
||||
.get("info")
|
||||
.and_then(|info| {
|
||||
info.get("version")
|
||||
.and_then(|version| version.as_str())
|
||||
.map(ToOwned::to_owned)
|
||||
})?;
|
||||
let dist_info_dirname: OsString = format!("debugpy-{latest_version}.dist-info").into();
|
||||
let is_up_to_date = delegate
|
||||
.fs()
|
||||
.read_dir(&debug_adapters_dir().join(Self::ADAPTER_NAME))
|
||||
.await
|
||||
.ok()?
|
||||
.into_stream()
|
||||
.any(async |entry| {
|
||||
entry.is_ok_and(|e| e.file_name().is_some_and(|name| name == dist_info_dirname))
|
||||
})
|
||||
.await;
|
||||
})
|
||||
.context("parsing latest release information")?;
|
||||
let dist_info_dirname: OsString = format!("debugpy-{latest_version}.dist-info").into();
|
||||
let is_up_to_date = delegate
|
||||
.fs()
|
||||
.read_dir(&debug_adapters_dir().join(Self::ADAPTER_NAME))
|
||||
.await?
|
||||
.into_stream()
|
||||
.any(async |entry| {
|
||||
entry.is_ok_and(|e| e.file_name().is_some_and(|name| name == dist_info_dirname))
|
||||
})
|
||||
.await;
|
||||
|
||||
if !is_up_to_date {
|
||||
delegate
|
||||
.fs()
|
||||
.remove_dir(
|
||||
&debug_adapters_dir().join(Self::ADAPTER_NAME),
|
||||
RemoveOptions {
|
||||
recursive: true,
|
||||
ignore_if_not_exists: true,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.ok()?;
|
||||
self.fetch_wheel(delegate).await.ok()?;
|
||||
}
|
||||
Some(())
|
||||
})
|
||||
.await;
|
||||
if !is_up_to_date {
|
||||
delegate
|
||||
.fs()
|
||||
.remove_dir(
|
||||
&debug_adapters_dir().join(Self::ADAPTER_NAME),
|
||||
RemoveOptions {
|
||||
recursive: true,
|
||||
ignore_if_not_exists: true,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
self.fetch_wheel(toolchain, delegate).await?;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
}
|
||||
|
||||
async fn fetch_debugpy_whl(
|
||||
&self,
|
||||
toolchain: Option<Toolchain>,
|
||||
delegate: &Arc<dyn DapDelegate>,
|
||||
) -> Result<Arc<Path>, String> {
|
||||
self.debugpy_whl_base_path
|
||||
.get_or_init(|| async move {
|
||||
self.maybe_fetch_new_wheel(delegate).await;
|
||||
self.maybe_fetch_new_wheel(toolchain, delegate)
|
||||
.await
|
||||
.map_err(|e| format!("{e}"))?;
|
||||
Ok(Arc::from(
|
||||
debug_adapters_dir()
|
||||
.join(Self::ADAPTER_NAME)
|
||||
@@ -216,12 +220,24 @@ impl PythonDebugAdapter {
|
||||
.clone()
|
||||
}
|
||||
|
||||
async fn base_venv_path(&self, delegate: &Arc<dyn DapDelegate>) -> Result<Arc<Path>, String> {
|
||||
self.base_venv_path
|
||||
async fn base_venv_path(
|
||||
&self,
|
||||
toolchain: Option<Toolchain>,
|
||||
delegate: &Arc<dyn DapDelegate>,
|
||||
) -> Result<Arc<Path>> {
|
||||
let result = self.base_venv_path
|
||||
.get_or_init(|| async {
|
||||
let base_python = Self::system_python_name(delegate)
|
||||
.await
|
||||
.ok_or_else(|| String::from("Could not find a Python installation"))?;
|
||||
let base_python = if let Some(toolchain) = toolchain {
|
||||
toolchain.path.to_string()
|
||||
} else {
|
||||
Self::system_python_name(delegate).await.ok_or_else(|| {
|
||||
let mut message = "Could not find a Python installation".to_owned();
|
||||
if cfg!(windows){
|
||||
message.push_str(". Install Python from the Microsoft Store, or manually from https://www.python.org/downloads/windows.")
|
||||
}
|
||||
message
|
||||
})?
|
||||
};
|
||||
|
||||
let did_succeed = util::command::new_smol_command(base_python)
|
||||
.args(["-m", "venv", "zed_base_venv"])
|
||||
@@ -239,35 +255,50 @@ impl PythonDebugAdapter {
|
||||
return Err("Failed to create base virtual environment".into());
|
||||
}
|
||||
|
||||
const DIR: &str = if cfg!(target_os = "windows") {
|
||||
"Scripts"
|
||||
const PYTHON_PATH: &str = if cfg!(target_os = "windows") {
|
||||
"Scripts/python.exe"
|
||||
} else {
|
||||
"bin"
|
||||
"bin/python3"
|
||||
};
|
||||
Ok(Arc::from(
|
||||
paths::debug_adapters_dir()
|
||||
.join(Self::DEBUG_ADAPTER_NAME.as_ref())
|
||||
.join("zed_base_venv")
|
||||
.join(DIR)
|
||||
.join("python3")
|
||||
.join(PYTHON_PATH)
|
||||
.as_ref(),
|
||||
))
|
||||
})
|
||||
.await
|
||||
.clone()
|
||||
.clone();
|
||||
match result {
|
||||
Ok(path) => Ok(path),
|
||||
Err(e) => Err(anyhow::anyhow!("{e}")),
|
||||
}
|
||||
}
|
||||
async fn system_python_name(delegate: &Arc<dyn DapDelegate>) -> Option<String> {
|
||||
const BINARY_NAMES: [&str; 3] = ["python3", "python", "py"];
|
||||
let mut name = None;
|
||||
|
||||
for cmd in BINARY_NAMES {
|
||||
name = delegate
|
||||
.which(OsStr::new(cmd))
|
||||
let Some(path) = delegate.which(OsStr::new(cmd)).await else {
|
||||
continue;
|
||||
};
|
||||
// Try to detect situations where `python3` exists but is not a real Python interpreter.
|
||||
// Notably, on fresh Windows installs, `python3` is a shim that opens the Microsoft Store app
|
||||
// when run with no arguments, and just fails otherwise.
|
||||
let Some(output) = new_smol_command(&path)
|
||||
.args(["-c", "print(1 + 2)"])
|
||||
.output()
|
||||
.await
|
||||
.map(|path| path.to_string_lossy().into_owned());
|
||||
if name.is_some() {
|
||||
break;
|
||||
.ok()
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
if output.stdout.trim_ascii() != b"3" {
|
||||
continue;
|
||||
}
|
||||
name = Some(path.to_string_lossy().into_owned());
|
||||
break;
|
||||
}
|
||||
name
|
||||
}
|
||||
@@ -746,15 +777,10 @@ impl DebugAdapter for PythonDebugAdapter {
|
||||
)
|
||||
.await;
|
||||
|
||||
let debugpy_path = self
|
||||
.fetch_debugpy_whl(delegate)
|
||||
self.fetch_debugpy_whl(toolchain.clone(), delegate)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
if let Some(toolchain) = &toolchain {
|
||||
log::debug!(
|
||||
"Found debugpy in toolchain environment: {}",
|
||||
debugpy_path.display()
|
||||
);
|
||||
return self
|
||||
.get_installed_binary(
|
||||
delegate,
|
||||
|
||||
@@ -268,12 +268,12 @@ impl DebugPanel {
|
||||
|
||||
async move |_, cx| {
|
||||
if let Err(error) = task.await {
|
||||
log::error!("{error}");
|
||||
log::error!("{error:#}");
|
||||
session
|
||||
.update(cx, |session, cx| {
|
||||
session
|
||||
.console_output(cx)
|
||||
.unbounded_send(format!("error: {}", error))
|
||||
.unbounded_send(format!("error: {:#}", error))
|
||||
.ok();
|
||||
session.shutdown(cx)
|
||||
})?
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use collections::HashMap;
|
||||
use gpui::{Entity, WeakEntity};
|
||||
use gpui::{Corner, Entity, WeakEntity};
|
||||
use project::debugger::session::{ThreadId, ThreadStatus};
|
||||
use ui::{CommonAnimationExt, ContextMenu, DropdownMenu, DropdownStyle, Indicator, prelude::*};
|
||||
use util::{maybe, truncate_and_trailoff};
|
||||
@@ -211,6 +211,7 @@ impl DebugPanel {
|
||||
this
|
||||
}),
|
||||
)
|
||||
.attach(Corner::BottomLeft)
|
||||
.style(DropdownStyle::Ghost)
|
||||
.handle(self.session_picker_menu_handle.clone());
|
||||
|
||||
@@ -322,6 +323,7 @@ impl DebugPanel {
|
||||
this
|
||||
}),
|
||||
)
|
||||
.attach(Corner::BottomLeft)
|
||||
.disabled(session_terminated)
|
||||
.style(DropdownStyle::Ghost)
|
||||
.handle(self.thread_picker_menu_handle.clone()),
|
||||
|
||||
@@ -937,6 +937,7 @@ impl RunningState {
|
||||
let task_store = project.read(cx).task_store().downgrade();
|
||||
let weak_project = project.downgrade();
|
||||
let weak_workspace = workspace.downgrade();
|
||||
let is_windows = project.read(cx).path_style(cx).is_windows();
|
||||
let remote_shell = project
|
||||
.read(cx)
|
||||
.remote_client()
|
||||
@@ -1029,7 +1030,7 @@ impl RunningState {
|
||||
task.resolved.shell = Shell::Program(remote_shell);
|
||||
}
|
||||
|
||||
let builder = ShellBuilder::new(&task.resolved.shell);
|
||||
let builder = ShellBuilder::new(&task.resolved.shell, is_windows);
|
||||
let command_label = builder.command_label(task.resolved.command.as_deref().unwrap_or(""));
|
||||
let (command, args) =
|
||||
builder.build(task.resolved.command.clone(), &task.resolved.args);
|
||||
|
||||
@@ -669,11 +669,7 @@ impl ConsoleQueryBarCompletionProvider {
|
||||
&snapshot,
|
||||
),
|
||||
new_text: string_match.string.clone(),
|
||||
label: CodeLabel {
|
||||
filter_range: 0..string_match.string.len(),
|
||||
text: string_match.string.clone(),
|
||||
runs: Vec::new(),
|
||||
},
|
||||
label: CodeLabel::plain(string_match.string.clone(), None),
|
||||
icon_path: None,
|
||||
documentation: Some(CompletionDocumentation::MultiLineMarkdown(
|
||||
variable_value.into(),
|
||||
@@ -782,11 +778,7 @@ impl ConsoleQueryBarCompletionProvider {
|
||||
&snapshot,
|
||||
),
|
||||
new_text,
|
||||
label: CodeLabel {
|
||||
filter_range: 0..completion.label.len(),
|
||||
text: completion.label,
|
||||
runs: Vec::new(),
|
||||
},
|
||||
label: CodeLabel::plain(completion.label, None),
|
||||
icon_path: None,
|
||||
documentation: completion.detail.map(|detail| {
|
||||
CompletionDocumentation::MultiLineMarkdown(detail.into())
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use cloud_llm_client::predict_edits_v3::{self, Line};
|
||||
use language::{Language, LanguageId};
|
||||
use project::ProjectEntryId;
|
||||
use std::ops::Range;
|
||||
@@ -91,6 +92,18 @@ impl Declaration {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn item_line_range(&self) -> Range<Line> {
|
||||
match self {
|
||||
Declaration::File { declaration, .. } => declaration.item_line_range.clone(),
|
||||
Declaration::Buffer {
|
||||
declaration, rope, ..
|
||||
} => {
|
||||
Line(rope.offset_to_point(declaration.item_range.start).row)
|
||||
..Line(rope.offset_to_point(declaration.item_range.end).row)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn item_text(&self) -> (Cow<'_, str>, bool) {
|
||||
match self {
|
||||
Declaration::File { declaration, .. } => (
|
||||
@@ -130,6 +143,18 @@ impl Declaration {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn signature_line_range(&self) -> Range<Line> {
|
||||
match self {
|
||||
Declaration::File { declaration, .. } => declaration.signature_line_range.clone(),
|
||||
Declaration::Buffer {
|
||||
declaration, rope, ..
|
||||
} => {
|
||||
Line(rope.offset_to_point(declaration.signature_range.start).row)
|
||||
..Line(rope.offset_to_point(declaration.signature_range.end).row)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn signature_range_in_item_text(&self) -> Range<usize> {
|
||||
let signature_range = self.signature_range();
|
||||
let item_range = self.item_range();
|
||||
@@ -142,7 +167,7 @@ fn expand_range_to_line_boundaries_and_truncate(
|
||||
range: &Range<usize>,
|
||||
limit: usize,
|
||||
rope: &Rope,
|
||||
) -> (Range<usize>, bool) {
|
||||
) -> (Range<usize>, Range<predict_edits_v3::Line>, bool) {
|
||||
let mut point_range = rope.offset_to_point(range.start)..rope.offset_to_point(range.end);
|
||||
point_range.start.column = 0;
|
||||
point_range.end.row += 1;
|
||||
@@ -155,7 +180,10 @@ fn expand_range_to_line_boundaries_and_truncate(
|
||||
item_range.end = item_range.start + limit;
|
||||
}
|
||||
item_range.end = rope.clip_offset(item_range.end, Bias::Left);
|
||||
(item_range, is_truncated)
|
||||
|
||||
let line_range =
|
||||
predict_edits_v3::Line(point_range.start.row)..predict_edits_v3::Line(point_range.end.row);
|
||||
(item_range, line_range, is_truncated)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -164,25 +192,30 @@ pub struct FileDeclaration {
|
||||
pub identifier: Identifier,
|
||||
/// offset range of the declaration in the file, expanded to line boundaries and truncated
|
||||
pub item_range: Range<usize>,
|
||||
/// line range of the declaration in the file, potentially truncated
|
||||
pub item_line_range: Range<predict_edits_v3::Line>,
|
||||
/// text of `item_range`
|
||||
pub text: Arc<str>,
|
||||
/// whether `text` was truncated
|
||||
pub text_is_truncated: bool,
|
||||
/// offset range of the signature in the file, expanded to line boundaries and truncated
|
||||
pub signature_range: Range<usize>,
|
||||
/// line range of the signature in the file, truncated
|
||||
pub signature_line_range: Range<Line>,
|
||||
/// whether `signature` was truncated
|
||||
pub signature_is_truncated: bool,
|
||||
}
|
||||
|
||||
impl FileDeclaration {
|
||||
pub fn from_outline(declaration: OutlineDeclaration, rope: &Rope) -> FileDeclaration {
|
||||
let (item_range_in_file, text_is_truncated) = expand_range_to_line_boundaries_and_truncate(
|
||||
&declaration.item_range,
|
||||
ITEM_TEXT_TRUNCATION_LENGTH,
|
||||
rope,
|
||||
);
|
||||
let (item_range_in_file, item_line_range_in_file, text_is_truncated) =
|
||||
expand_range_to_line_boundaries_and_truncate(
|
||||
&declaration.item_range,
|
||||
ITEM_TEXT_TRUNCATION_LENGTH,
|
||||
rope,
|
||||
);
|
||||
|
||||
let (mut signature_range_in_file, mut signature_is_truncated) =
|
||||
let (mut signature_range_in_file, signature_line_range, mut signature_is_truncated) =
|
||||
expand_range_to_line_boundaries_and_truncate(
|
||||
&declaration.signature_range,
|
||||
ITEM_TEXT_TRUNCATION_LENGTH,
|
||||
@@ -202,6 +235,7 @@ impl FileDeclaration {
|
||||
parent: None,
|
||||
identifier: declaration.identifier,
|
||||
signature_range: signature_range_in_file,
|
||||
signature_line_range,
|
||||
signature_is_truncated,
|
||||
text: rope
|
||||
.chunks_in_range(item_range_in_file.clone())
|
||||
@@ -209,6 +243,7 @@ impl FileDeclaration {
|
||||
.into(),
|
||||
text_is_truncated,
|
||||
item_range: item_range_in_file,
|
||||
item_line_range: item_line_range_in_file,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -225,12 +260,13 @@ pub struct BufferDeclaration {
|
||||
|
||||
impl BufferDeclaration {
|
||||
pub fn from_outline(declaration: OutlineDeclaration, rope: &Rope) -> Self {
|
||||
let (item_range, item_range_is_truncated) = expand_range_to_line_boundaries_and_truncate(
|
||||
&declaration.item_range,
|
||||
ITEM_TEXT_TRUNCATION_LENGTH,
|
||||
rope,
|
||||
);
|
||||
let (signature_range, signature_range_is_truncated) =
|
||||
let (item_range, _item_line_range, item_range_is_truncated) =
|
||||
expand_range_to_line_boundaries_and_truncate(
|
||||
&declaration.item_range,
|
||||
ITEM_TEXT_TRUNCATION_LENGTH,
|
||||
rope,
|
||||
);
|
||||
let (signature_range, _signature_line_range, signature_range_is_truncated) =
|
||||
expand_range_to_line_boundaries_and_truncate(
|
||||
&declaration.signature_range,
|
||||
ITEM_TEXT_TRUNCATION_LENGTH,
|
||||
|
||||
@@ -9,6 +9,7 @@ pub mod text_similarity;
|
||||
|
||||
use std::{path::Path, sync::Arc};
|
||||
|
||||
use cloud_llm_client::predict_edits_v3;
|
||||
use collections::HashMap;
|
||||
use gpui::{App, AppContext as _, Entity, Task};
|
||||
use language::BufferSnapshot;
|
||||
@@ -21,6 +22,8 @@ pub use imports::*;
|
||||
pub use reference::*;
|
||||
pub use syntax_index::*;
|
||||
|
||||
pub use predict_edits_v3::Line;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct EditPredictionContextOptions {
|
||||
pub use_imports: bool,
|
||||
@@ -32,7 +35,7 @@ pub struct EditPredictionContextOptions {
|
||||
pub struct EditPredictionContext {
|
||||
pub excerpt: EditPredictionExcerpt,
|
||||
pub excerpt_text: EditPredictionExcerptText,
|
||||
pub cursor_offset_in_excerpt: usize,
|
||||
pub cursor_point: Point,
|
||||
pub declarations: Vec<ScoredDeclaration>,
|
||||
}
|
||||
|
||||
@@ -124,8 +127,6 @@ impl EditPredictionContext {
|
||||
);
|
||||
|
||||
let cursor_offset_in_file = cursor_point.to_offset(buffer);
|
||||
// TODO fix this to not need saturating_sub
|
||||
let cursor_offset_in_excerpt = cursor_offset_in_file.saturating_sub(excerpt.range.start);
|
||||
|
||||
let declarations = if let Some(index_state) = index_state {
|
||||
let references = get_references(&excerpt, &excerpt_text, buffer);
|
||||
@@ -148,7 +149,7 @@ impl EditPredictionContext {
|
||||
Some(Self {
|
||||
excerpt,
|
||||
excerpt_text,
|
||||
cursor_offset_in_excerpt,
|
||||
cursor_point,
|
||||
declarations,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use text::{Point, ToOffset as _, ToPoint as _};
|
||||
use tree_sitter::{Node, TreeCursor};
|
||||
use util::RangeExt;
|
||||
|
||||
use crate::{BufferDeclaration, declaration::DeclarationId, syntax_index::SyntaxIndexState};
|
||||
use crate::{BufferDeclaration, Line, declaration::DeclarationId, syntax_index::SyntaxIndexState};
|
||||
|
||||
// TODO:
|
||||
//
|
||||
@@ -35,6 +35,7 @@ pub struct EditPredictionExcerptOptions {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EditPredictionExcerpt {
|
||||
pub range: Range<usize>,
|
||||
pub line_range: Range<Line>,
|
||||
pub parent_declarations: Vec<(DeclarationId, Range<usize>)>,
|
||||
pub size: usize,
|
||||
}
|
||||
@@ -86,12 +87,19 @@ impl EditPredictionExcerpt {
|
||||
buffer.len(),
|
||||
options.max_bytes
|
||||
);
|
||||
return Some(EditPredictionExcerpt::new(0..buffer.len(), Vec::new()));
|
||||
let offset_range = 0..buffer.len();
|
||||
let line_range = Line(0)..Line(buffer.max_point().row);
|
||||
return Some(EditPredictionExcerpt::new(
|
||||
offset_range,
|
||||
line_range,
|
||||
Vec::new(),
|
||||
));
|
||||
}
|
||||
|
||||
let query_offset = query_point.to_offset(buffer);
|
||||
let query_range = Point::new(query_point.row, 0).to_offset(buffer)
|
||||
..Point::new(query_point.row + 1, 0).to_offset(buffer);
|
||||
let query_line_range = query_point.row..query_point.row + 1;
|
||||
let query_range = Point::new(query_line_range.start, 0).to_offset(buffer)
|
||||
..Point::new(query_line_range.end, 0).to_offset(buffer);
|
||||
if query_range.len() >= options.max_bytes {
|
||||
return None;
|
||||
}
|
||||
@@ -107,6 +115,7 @@ impl EditPredictionExcerpt {
|
||||
let excerpt_selector = ExcerptSelector {
|
||||
query_offset,
|
||||
query_range,
|
||||
query_line_range: Line(query_line_range.start)..Line(query_line_range.end),
|
||||
parent_declarations: &parent_declarations,
|
||||
buffer,
|
||||
options,
|
||||
@@ -130,7 +139,11 @@ impl EditPredictionExcerpt {
|
||||
excerpt_selector.select_lines()
|
||||
}
|
||||
|
||||
fn new(range: Range<usize>, parent_declarations: Vec<(DeclarationId, Range<usize>)>) -> Self {
|
||||
fn new(
|
||||
range: Range<usize>,
|
||||
line_range: Range<Line>,
|
||||
parent_declarations: Vec<(DeclarationId, Range<usize>)>,
|
||||
) -> Self {
|
||||
let size = range.len()
|
||||
+ parent_declarations
|
||||
.iter()
|
||||
@@ -140,10 +153,11 @@ impl EditPredictionExcerpt {
|
||||
range,
|
||||
parent_declarations,
|
||||
size,
|
||||
line_range,
|
||||
}
|
||||
}
|
||||
|
||||
fn with_expanded_range(&self, new_range: Range<usize>) -> Self {
|
||||
fn with_expanded_range(&self, new_range: Range<usize>, new_line_range: Range<Line>) -> Self {
|
||||
if !new_range.contains_inclusive(&self.range) {
|
||||
// this is an issue because parent_signature_ranges may be incorrect
|
||||
log::error!("bug: with_expanded_range called with disjoint range");
|
||||
@@ -155,7 +169,7 @@ impl EditPredictionExcerpt {
|
||||
}
|
||||
parent_declarations.push((*declaration_id, range.clone()));
|
||||
}
|
||||
Self::new(new_range, parent_declarations)
|
||||
Self::new(new_range, new_line_range, parent_declarations)
|
||||
}
|
||||
|
||||
fn parent_signatures_size(&self) -> usize {
|
||||
@@ -166,6 +180,7 @@ impl EditPredictionExcerpt {
|
||||
struct ExcerptSelector<'a> {
|
||||
query_offset: usize,
|
||||
query_range: Range<usize>,
|
||||
query_line_range: Range<Line>,
|
||||
parent_declarations: &'a [(DeclarationId, &'a BufferDeclaration)],
|
||||
buffer: &'a BufferSnapshot,
|
||||
options: &'a EditPredictionExcerptOptions,
|
||||
@@ -178,10 +193,13 @@ impl<'a> ExcerptSelector<'a> {
|
||||
let mut cursor = selected_layer_root.walk();
|
||||
|
||||
loop {
|
||||
let excerpt_range = node_line_start(cursor.node()).to_offset(&self.buffer)
|
||||
..node_line_end(cursor.node()).to_offset(&self.buffer);
|
||||
let line_start = node_line_start(cursor.node());
|
||||
let line_end = node_line_end(cursor.node());
|
||||
let line_range = Line(line_start.row)..Line(line_end.row);
|
||||
let excerpt_range =
|
||||
line_start.to_offset(&self.buffer)..line_end.to_offset(&self.buffer);
|
||||
if excerpt_range.contains_inclusive(&self.query_range) {
|
||||
let excerpt = self.make_excerpt(excerpt_range);
|
||||
let excerpt = self.make_excerpt(excerpt_range, line_range);
|
||||
if excerpt.size <= self.options.max_bytes {
|
||||
return Some(self.expand_to_siblings(&mut cursor, excerpt));
|
||||
}
|
||||
@@ -272,9 +290,13 @@ impl<'a> ExcerptSelector<'a> {
|
||||
|
||||
let mut forward = None;
|
||||
while !forward_done {
|
||||
let new_end = node_line_end(forward_cursor.node()).to_offset(&self.buffer);
|
||||
let new_end_point = node_line_end(forward_cursor.node());
|
||||
let new_end = new_end_point.to_offset(&self.buffer);
|
||||
if new_end > excerpt.range.end {
|
||||
let new_excerpt = excerpt.with_expanded_range(excerpt.range.start..new_end);
|
||||
let new_excerpt = excerpt.with_expanded_range(
|
||||
excerpt.range.start..new_end,
|
||||
excerpt.line_range.start..Line(new_end_point.row),
|
||||
);
|
||||
if new_excerpt.size <= self.options.max_bytes {
|
||||
forward = Some(new_excerpt);
|
||||
break;
|
||||
@@ -289,9 +311,13 @@ impl<'a> ExcerptSelector<'a> {
|
||||
|
||||
let mut backward = None;
|
||||
while !backward_done {
|
||||
let new_start = node_line_start(backward_cursor.node()).to_offset(&self.buffer);
|
||||
let new_start_point = node_line_start(backward_cursor.node());
|
||||
let new_start = new_start_point.to_offset(&self.buffer);
|
||||
if new_start < excerpt.range.start {
|
||||
let new_excerpt = excerpt.with_expanded_range(new_start..excerpt.range.end);
|
||||
let new_excerpt = excerpt.with_expanded_range(
|
||||
new_start..excerpt.range.end,
|
||||
Line(new_start_point.row)..excerpt.line_range.end,
|
||||
);
|
||||
if new_excerpt.size <= self.options.max_bytes {
|
||||
backward = Some(new_excerpt);
|
||||
break;
|
||||
@@ -339,7 +365,7 @@ impl<'a> ExcerptSelector<'a> {
|
||||
|
||||
fn select_lines(&self) -> Option<EditPredictionExcerpt> {
|
||||
// early return if line containing query_offset is already too large
|
||||
let excerpt = self.make_excerpt(self.query_range.clone());
|
||||
let excerpt = self.make_excerpt(self.query_range.clone(), self.query_line_range.clone());
|
||||
if excerpt.size > self.options.max_bytes {
|
||||
log::debug!(
|
||||
"excerpt for cursor line is {} bytes, which exceeds the window",
|
||||
@@ -353,24 +379,24 @@ impl<'a> ExcerptSelector<'a> {
|
||||
let before_bytes =
|
||||
(self.options.target_before_cursor_over_total_bytes * bytes_remaining as f32) as usize;
|
||||
|
||||
let start_point = {
|
||||
let start_line = {
|
||||
let offset = self.query_offset.saturating_sub(before_bytes);
|
||||
let point = offset.to_point(self.buffer);
|
||||
Point::new(point.row + 1, 0)
|
||||
Line(point.row + 1)
|
||||
};
|
||||
let start_offset = start_point.to_offset(&self.buffer);
|
||||
let end_point = {
|
||||
let start_offset = Point::new(start_line.0, 0).to_offset(&self.buffer);
|
||||
let end_line = {
|
||||
let offset = start_offset + bytes_remaining;
|
||||
let point = offset.to_point(self.buffer);
|
||||
Point::new(point.row, 0)
|
||||
Line(point.row)
|
||||
};
|
||||
let end_offset = end_point.to_offset(&self.buffer);
|
||||
let end_offset = Point::new(end_line.0, 0).to_offset(&self.buffer);
|
||||
|
||||
// this could be expanded further since recalculated `signature_size` may be smaller, but
|
||||
// skipping that for now for simplicity
|
||||
//
|
||||
// TODO: could also consider checking if lines immediately before / after fit.
|
||||
let excerpt = self.make_excerpt(start_offset..end_offset);
|
||||
let excerpt = self.make_excerpt(start_offset..end_offset, start_line..end_line);
|
||||
if excerpt.size > self.options.max_bytes {
|
||||
log::error!(
|
||||
"bug: line-based excerpt selection has size {}, \
|
||||
@@ -382,14 +408,14 @@ impl<'a> ExcerptSelector<'a> {
|
||||
return Some(excerpt);
|
||||
}
|
||||
|
||||
fn make_excerpt(&self, range: Range<usize>) -> EditPredictionExcerpt {
|
||||
fn make_excerpt(&self, range: Range<usize>, line_range: Range<Line>) -> EditPredictionExcerpt {
|
||||
let parent_declarations = self
|
||||
.parent_declarations
|
||||
.iter()
|
||||
.filter(|(_, declaration)| declaration.item_range.contains_inclusive(&range))
|
||||
.map(|(id, declaration)| (*id, declaration.signature_range.clone()))
|
||||
.collect();
|
||||
EditPredictionExcerpt::new(range, parent_declarations)
|
||||
EditPredictionExcerpt::new(range, line_range, parent_declarations)
|
||||
}
|
||||
|
||||
/// Returns `true` if the `forward` excerpt is a better choice than the `backward` excerpt.
|
||||
|
||||
@@ -318,6 +318,24 @@ pub struct GoToPreviousDiagnostic {
|
||||
pub severity: GoToDiagnosticSeverityFilter,
|
||||
}
|
||||
|
||||
/// Adds a cursor above the current selection.
|
||||
#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)]
|
||||
#[action(namespace = editor)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct AddSelectionAbove {
|
||||
#[serde(default = "default_true")]
|
||||
pub skip_soft_wrap: bool,
|
||||
}
|
||||
|
||||
/// Adds a cursor below the current selection.
|
||||
#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)]
|
||||
#[action(namespace = editor)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct AddSelectionBelow {
|
||||
#[serde(default = "default_true")]
|
||||
pub skip_soft_wrap: bool,
|
||||
}
|
||||
|
||||
actions!(
|
||||
debugger,
|
||||
[
|
||||
@@ -345,10 +363,6 @@ actions!(
|
||||
/// Accepts a partial edit prediction.
|
||||
#[action(deprecated_aliases = ["editor::AcceptPartialCopilotSuggestion"])]
|
||||
AcceptPartialEditPrediction,
|
||||
/// Adds a cursor above the current selection.
|
||||
AddSelectionAbove,
|
||||
/// Adds a cursor below the current selection.
|
||||
AddSelectionBelow,
|
||||
/// Applies all diff hunks in the editor.
|
||||
ApplyAllDiffHunks,
|
||||
/// Applies the diff hunk at the current position.
|
||||
|
||||
@@ -328,11 +328,7 @@ impl CompletionsMenu {
|
||||
.map(|choice| Completion {
|
||||
replace_range: selection.start.text_anchor..selection.end.text_anchor,
|
||||
new_text: choice.to_string(),
|
||||
label: CodeLabel {
|
||||
text: choice.to_string(),
|
||||
runs: Default::default(),
|
||||
filter_range: Default::default(),
|
||||
},
|
||||
label: CodeLabel::plain(choice.to_string(), None),
|
||||
icon_path: None,
|
||||
documentation: None,
|
||||
confirm: None,
|
||||
@@ -1518,6 +1514,7 @@ impl CodeActionsMenu {
|
||||
this.child(
|
||||
h_flex()
|
||||
.overflow_hidden()
|
||||
.when(is_quick_action_bar, |this| this.text_ui(cx))
|
||||
.child(task.resolved_label.replace("\n", ""))
|
||||
.when(selected, |this| {
|
||||
this.text_color(colors.text_accent)
|
||||
@@ -1528,6 +1525,7 @@ impl CodeActionsMenu {
|
||||
this.child(
|
||||
h_flex()
|
||||
.overflow_hidden()
|
||||
.when(is_quick_action_bar, |this| this.text_ui(cx))
|
||||
.child("debug: ")
|
||||
.child(scenario.label.clone())
|
||||
.when(selected, |this| {
|
||||
|
||||
@@ -1401,6 +1401,26 @@ impl DisplaySnapshot {
|
||||
pub fn excerpt_header_height(&self) -> u32 {
|
||||
self.block_snapshot.excerpt_header_height
|
||||
}
|
||||
|
||||
/// Given a `DisplayPoint`, returns another `DisplayPoint` corresponding to
|
||||
/// the start of the buffer row that is a given number of buffer rows away
|
||||
/// from the provided point.
|
||||
///
|
||||
/// This moves by buffer rows instead of display rows, a distinction that is
|
||||
/// important when soft wrapping is enabled.
|
||||
pub fn start_of_relative_buffer_row(&self, point: DisplayPoint, times: isize) -> DisplayPoint {
|
||||
let start = self.display_point_to_fold_point(point, Bias::Left);
|
||||
let target = start.row() as isize + times;
|
||||
let new_row = (target.max(0) as u32).min(self.fold_snapshot().max_point().row());
|
||||
|
||||
self.clip_point(
|
||||
self.fold_point_to_display_point(
|
||||
self.fold_snapshot()
|
||||
.clip_point(FoldPoint::new(new_row, 0), Bias::Right),
|
||||
),
|
||||
Bias::Right,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Default, Eq, Ord, PartialOrd, PartialEq)]
|
||||
|
||||
@@ -1090,8 +1090,8 @@ pub struct Editor {
|
||||
next_completion_id: CompletionId,
|
||||
available_code_actions: Option<(Location, Rc<[AvailableCodeAction]>)>,
|
||||
code_actions_task: Option<Task<Result<()>>>,
|
||||
quick_selection_highlight_task: Option<(Range<usize>, Task<()>)>,
|
||||
debounced_selection_highlight_task: Option<(Range<usize>, Task<()>)>,
|
||||
quick_selection_highlight_task: Option<(Range<Anchor>, Task<()>)>,
|
||||
debounced_selection_highlight_task: Option<(Range<Anchor>, Task<()>)>,
|
||||
document_highlights_task: Option<Task<()>>,
|
||||
linked_editing_range_task: Option<Task<Option<()>>>,
|
||||
linked_edit_ranges: linked_editing_ranges::LinkedEditingRanges,
|
||||
@@ -5243,15 +5243,7 @@ impl Editor {
|
||||
if enabled {
|
||||
(InvalidationStrategy::RefreshRequested, None)
|
||||
} else {
|
||||
self.splice_inlays(
|
||||
&self
|
||||
.visible_inlay_hints(cx)
|
||||
.iter()
|
||||
.map(|inlay| inlay.id)
|
||||
.collect::<Vec<InlayId>>(),
|
||||
Vec::new(),
|
||||
cx,
|
||||
);
|
||||
self.clear_inlay_hints(cx);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -5263,15 +5255,7 @@ impl Editor {
|
||||
if enabled {
|
||||
(InvalidationStrategy::RefreshRequested, None)
|
||||
} else {
|
||||
self.splice_inlays(
|
||||
&self
|
||||
.visible_inlay_hints(cx)
|
||||
.iter()
|
||||
.map(|inlay| inlay.id)
|
||||
.collect::<Vec<InlayId>>(),
|
||||
Vec::new(),
|
||||
cx,
|
||||
);
|
||||
self.clear_inlay_hints(cx);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
@@ -5282,7 +5266,7 @@ impl Editor {
|
||||
match self.inlay_hint_cache.update_settings(
|
||||
&self.buffer,
|
||||
new_settings,
|
||||
self.visible_inlay_hints(cx),
|
||||
self.visible_inlay_hints(cx).cloned().collect::<Vec<_>>(),
|
||||
cx,
|
||||
) {
|
||||
ControlFlow::Break(Some(InlaySplice {
|
||||
@@ -5332,13 +5316,25 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
fn visible_inlay_hints(&self, cx: &Context<Editor>) -> Vec<Inlay> {
|
||||
pub fn clear_inlay_hints(&self, cx: &mut Context<Editor>) {
|
||||
self.splice_inlays(
|
||||
&self
|
||||
.visible_inlay_hints(cx)
|
||||
.map(|inlay| inlay.id)
|
||||
.collect::<Vec<_>>(),
|
||||
Vec::new(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
fn visible_inlay_hints<'a>(
|
||||
&'a self,
|
||||
cx: &'a Context<Editor>,
|
||||
) -> impl Iterator<Item = &'a Inlay> {
|
||||
self.display_map
|
||||
.read(cx)
|
||||
.current_inlays()
|
||||
.filter(move |inlay| matches!(inlay.id, InlayId::Hint(_)))
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn visible_excerpts(
|
||||
@@ -7005,6 +7001,7 @@ impl Editor {
|
||||
) else {
|
||||
return Vec::default();
|
||||
};
|
||||
let query_range = query_range.to_anchors(&multi_buffer_snapshot);
|
||||
for (buffer_snapshot, search_range, excerpt_id) in buffer_ranges {
|
||||
match_ranges.extend(
|
||||
regex
|
||||
@@ -7128,12 +7125,11 @@ impl Editor {
|
||||
return;
|
||||
};
|
||||
let multi_buffer_snapshot = self.buffer().read(cx).snapshot(cx);
|
||||
let query_offset = query_range.to_offset(&multi_buffer_snapshot);
|
||||
if on_buffer_edit
|
||||
|| self
|
||||
.quick_selection_highlight_task
|
||||
.as_ref()
|
||||
.is_none_or(|(prev_query_offset, _)| prev_query_offset != &query_offset)
|
||||
.is_none_or(|(prev_anchor_range, _)| prev_anchor_range != &query_range)
|
||||
{
|
||||
let multi_buffer_visible_start = self
|
||||
.scroll_manager
|
||||
@@ -7147,7 +7143,7 @@ impl Editor {
|
||||
);
|
||||
let multi_buffer_visible_range = multi_buffer_visible_start..multi_buffer_visible_end;
|
||||
self.quick_selection_highlight_task = Some((
|
||||
query_offset.clone(),
|
||||
query_range.clone(),
|
||||
self.update_selection_occurrence_highlights(
|
||||
query_text.clone(),
|
||||
query_range.clone(),
|
||||
@@ -7162,7 +7158,7 @@ impl Editor {
|
||||
|| self
|
||||
.debounced_selection_highlight_task
|
||||
.as_ref()
|
||||
.is_none_or(|(prev_query_offset, _)| prev_query_offset != &query_offset)
|
||||
.is_none_or(|(prev_anchor_range, _)| prev_anchor_range != &query_range)
|
||||
{
|
||||
let multi_buffer_start = multi_buffer_snapshot
|
||||
.anchor_before(0)
|
||||
@@ -7172,7 +7168,7 @@ impl Editor {
|
||||
.to_point(&multi_buffer_snapshot);
|
||||
let multi_buffer_full_range = multi_buffer_start..multi_buffer_end;
|
||||
self.debounced_selection_highlight_task = Some((
|
||||
query_offset,
|
||||
query_range.clone(),
|
||||
self.update_selection_occurrence_highlights(
|
||||
query_text,
|
||||
query_range,
|
||||
@@ -10500,29 +10496,33 @@ impl Editor {
|
||||
|
||||
let buffer = display_map.buffer_snapshot();
|
||||
let mut edit_start = ToOffset::to_offset(&Point::new(rows.start.0, 0), buffer);
|
||||
let edit_end = if buffer.max_point().row >= rows.end.0 {
|
||||
let (edit_end, target_row) = if buffer.max_point().row >= rows.end.0 {
|
||||
// If there's a line after the range, delete the \n from the end of the row range
|
||||
ToOffset::to_offset(&Point::new(rows.end.0, 0), buffer)
|
||||
(
|
||||
ToOffset::to_offset(&Point::new(rows.end.0, 0), buffer),
|
||||
rows.end,
|
||||
)
|
||||
} else {
|
||||
// If there isn't a line after the range, delete the \n from the line before the
|
||||
// start of the row range
|
||||
edit_start = edit_start.saturating_sub(1);
|
||||
buffer.len()
|
||||
(buffer.len(), rows.start.previous_row())
|
||||
};
|
||||
|
||||
let (cursor, goal) = movement::down_by_rows(
|
||||
&display_map,
|
||||
let text_layout_details = self.text_layout_details(window);
|
||||
let x = display_map.x_for_display_point(
|
||||
selection.head().to_display_point(&display_map),
|
||||
rows.len() as u32,
|
||||
selection.goal,
|
||||
false,
|
||||
&self.text_layout_details(window),
|
||||
&text_layout_details,
|
||||
);
|
||||
let row = Point::new(target_row.0, 0)
|
||||
.to_display_point(&display_map)
|
||||
.row();
|
||||
let column = display_map.display_column_for_x(row, x, &text_layout_details);
|
||||
|
||||
new_cursors.push((
|
||||
selection.id,
|
||||
buffer.anchor_after(cursor.to_point(&display_map)),
|
||||
goal,
|
||||
buffer.anchor_after(DisplayPoint::new(row, column).to_point(&display_map)),
|
||||
SelectionGoal::None,
|
||||
));
|
||||
edit_ranges.push(edit_start..edit_end);
|
||||
}
|
||||
@@ -14236,23 +14236,29 @@ impl Editor {
|
||||
|
||||
pub fn add_selection_above(
|
||||
&mut self,
|
||||
_: &AddSelectionAbove,
|
||||
action: &AddSelectionAbove,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.add_selection(true, window, cx);
|
||||
self.add_selection(true, action.skip_soft_wrap, window, cx);
|
||||
}
|
||||
|
||||
pub fn add_selection_below(
|
||||
&mut self,
|
||||
_: &AddSelectionBelow,
|
||||
action: &AddSelectionBelow,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.add_selection(false, window, cx);
|
||||
self.add_selection(false, action.skip_soft_wrap, window, cx);
|
||||
}
|
||||
|
||||
fn add_selection(&mut self, above: bool, window: &mut Window, cx: &mut Context<Self>) {
|
||||
fn add_selection(
|
||||
&mut self,
|
||||
above: bool,
|
||||
skip_soft_wrap: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx);
|
||||
|
||||
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
@@ -14339,12 +14345,19 @@ impl Editor {
|
||||
};
|
||||
|
||||
let mut maybe_new_selection = None;
|
||||
let direction = if above { -1 } else { 1 };
|
||||
|
||||
while row != end_row {
|
||||
if above {
|
||||
if skip_soft_wrap {
|
||||
row = display_map
|
||||
.start_of_relative_buffer_row(DisplayPoint::new(row, 0), direction)
|
||||
.row();
|
||||
} else if above {
|
||||
row.0 -= 1;
|
||||
} else {
|
||||
row.0 += 1;
|
||||
}
|
||||
|
||||
if let Some(new_selection) = self.selections.build_columnar_selection(
|
||||
&display_map,
|
||||
row,
|
||||
@@ -23077,11 +23090,7 @@ fn snippet_completions(
|
||||
}),
|
||||
lsp_defaults: None,
|
||||
},
|
||||
label: CodeLabel {
|
||||
text: matching_prefix.clone(),
|
||||
runs: Vec::new(),
|
||||
filter_range: 0..matching_prefix.len(),
|
||||
},
|
||||
label: CodeLabel::plain(matching_prefix.clone(), None),
|
||||
icon_path: None,
|
||||
documentation: Some(CompletionDocumentation::SingleLineAndMultiLinePlainText {
|
||||
single_line: snippet.name.clone().into(),
|
||||
|
||||
@@ -27,7 +27,6 @@ use language::{
|
||||
LanguageConfigOverride, LanguageMatcher, LanguageName, Override, Point,
|
||||
language_settings::{
|
||||
CompletionSettingsContent, FormatterList, LanguageSettingsContent, LspInsertMode,
|
||||
SelectedFormatter,
|
||||
},
|
||||
tree_sitter_python,
|
||||
};
|
||||
@@ -4387,8 +4386,8 @@ fn test_delete_line(cx: &mut TestAppContext) {
|
||||
assert_eq!(
|
||||
editor.selections.display_ranges(cx),
|
||||
vec![
|
||||
DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
|
||||
DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
|
||||
DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
|
||||
]
|
||||
);
|
||||
});
|
||||
@@ -4410,6 +4409,24 @@ fn test_delete_line(cx: &mut TestAppContext) {
|
||||
vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
|
||||
);
|
||||
});
|
||||
|
||||
let editor = cx.add_window(|window, cx| {
|
||||
let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n\njkl\nmno", cx);
|
||||
build_editor(buffer, window, cx)
|
||||
});
|
||||
_ = editor.update(cx, |editor, window, cx| {
|
||||
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
|
||||
s.select_display_ranges([
|
||||
DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(2), 1)
|
||||
])
|
||||
});
|
||||
editor.delete_line(&DeleteLine, window, cx);
|
||||
assert_eq!(editor.display_text(cx), "\njkl\nmno");
|
||||
assert_eq!(
|
||||
editor.selections.display_ranges(cx),
|
||||
vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -11890,8 +11907,8 @@ async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppC
|
||||
#[gpui::test]
|
||||
async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
|
||||
init_test(cx, |settings| {
|
||||
settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
|
||||
Formatter::LanguageServer { name: None },
|
||||
settings.defaults.formatter = Some(FormatterList::Single(Formatter::LanguageServer(
|
||||
settings::LanguageServerFormatterSpecifier::Current,
|
||||
)))
|
||||
});
|
||||
|
||||
@@ -12016,11 +12033,11 @@ async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
|
||||
async fn test_multiple_formatters(cx: &mut TestAppContext) {
|
||||
init_test(cx, |settings| {
|
||||
settings.defaults.remove_trailing_whitespace_on_save = Some(true);
|
||||
settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
|
||||
Formatter::LanguageServer { name: None },
|
||||
settings.defaults.formatter = Some(FormatterList::Vec(vec![
|
||||
Formatter::LanguageServer(settings::LanguageServerFormatterSpecifier::Current),
|
||||
Formatter::CodeAction("code-action-1".into()),
|
||||
Formatter::CodeAction("code-action-2".into()),
|
||||
])))
|
||||
]))
|
||||
});
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
@@ -12275,9 +12292,9 @@ async fn test_multiple_formatters(cx: &mut TestAppContext) {
|
||||
#[gpui::test]
|
||||
async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
|
||||
init_test(cx, |settings| {
|
||||
settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
|
||||
Formatter::LanguageServer { name: None },
|
||||
])))
|
||||
settings.defaults.formatter = Some(FormatterList::Vec(vec![Formatter::LanguageServer(
|
||||
settings::LanguageServerFormatterSpecifier::Current,
|
||||
)]))
|
||||
});
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
@@ -12480,7 +12497,7 @@ async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
|
||||
#[gpui::test]
|
||||
async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
|
||||
init_test(cx, |settings| {
|
||||
settings.defaults.formatter = Some(SelectedFormatter::Auto)
|
||||
settings.defaults.formatter = Some(FormatterList::default())
|
||||
});
|
||||
|
||||
let mut cx = EditorLspTestContext::new_rust(
|
||||
@@ -12492,6 +12509,7 @@ async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
|
||||
)
|
||||
.await;
|
||||
|
||||
cx.run_until_parked();
|
||||
// Set up a buffer white some trailing whitespace and no trailing newline.
|
||||
cx.set_state(
|
||||
&[
|
||||
@@ -14861,12 +14879,7 @@ async fn test_multiline_completion(cx: &mut TestAppContext) {
|
||||
} else {
|
||||
item.label.clone()
|
||||
};
|
||||
let len = text.len();
|
||||
Some(language::CodeLabel {
|
||||
text,
|
||||
runs: Vec::new(),
|
||||
filter_range: 0..len,
|
||||
})
|
||||
Some(language::CodeLabel::plain(text, None))
|
||||
})),
|
||||
..FakeLspAdapter::default()
|
||||
},
|
||||
@@ -18169,9 +18182,7 @@ fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
|
||||
#[gpui::test]
|
||||
async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
|
||||
init_test(cx, |settings| {
|
||||
settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
|
||||
Formatter::Prettier,
|
||||
)))
|
||||
settings.defaults.formatter = Some(FormatterList::Single(Formatter::Prettier))
|
||||
});
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
@@ -18238,7 +18249,7 @@ async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
|
||||
);
|
||||
|
||||
update_test_language_settings(cx, |settings| {
|
||||
settings.defaults.formatter = Some(SelectedFormatter::Auto)
|
||||
settings.defaults.formatter = Some(FormatterList::default())
|
||||
});
|
||||
let format = editor.update_in(cx, |editor, window, cx| {
|
||||
editor.perform_format(
|
||||
@@ -25671,6 +25682,83 @@ async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppC
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_add_selection_skip_soft_wrap_option(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
|
||||
cx.set_state(indoc!(
|
||||
r#"ˇThis is a very long line that will be wrapped when soft wrapping is enabled
|
||||
Second line here"#
|
||||
));
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
// Enable soft wrapping with a narrow width to force soft wrapping and
|
||||
// confirm that more than 2 rows are being displayed.
|
||||
editor.set_wrap_width(Some(100.0.into()), cx);
|
||||
assert!(editor.display_text(cx).lines().count() > 2);
|
||||
|
||||
editor.add_selection_below(
|
||||
&AddSelectionBelow {
|
||||
skip_soft_wrap: true,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
editor.selections.display_ranges(cx),
|
||||
&[
|
||||
DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
|
||||
DisplayPoint::new(DisplayRow(8), 0)..DisplayPoint::new(DisplayRow(8), 0),
|
||||
]
|
||||
);
|
||||
|
||||
editor.add_selection_above(
|
||||
&AddSelectionAbove {
|
||||
skip_soft_wrap: true,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
editor.selections.display_ranges(cx),
|
||||
&[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
|
||||
);
|
||||
|
||||
editor.add_selection_below(
|
||||
&AddSelectionBelow {
|
||||
skip_soft_wrap: false,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
editor.selections.display_ranges(cx),
|
||||
&[
|
||||
DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
|
||||
DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
|
||||
]
|
||||
);
|
||||
|
||||
editor.add_selection_above(
|
||||
&AddSelectionAbove {
|
||||
skip_soft_wrap: false,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
editor.selections.display_ranges(cx),
|
||||
&[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_document_colors(cx: &mut TestAppContext) {
|
||||
let expected_color = Rgba {
|
||||
|
||||
@@ -307,7 +307,6 @@ pub fn update_inlay_link_and_hover_points(
|
||||
buffer_snapshot.anchor_after(point_for_position.next_valid.to_point(snapshot));
|
||||
if let Some(hovered_hint) = editor
|
||||
.visible_inlay_hints(cx)
|
||||
.into_iter()
|
||||
.skip_while(|hint| {
|
||||
hint.position
|
||||
.cmp(&previous_valid_anchor, &buffer_snapshot)
|
||||
|
||||
@@ -1013,7 +1013,7 @@ fn fetch_and_update_hints(
|
||||
.cloned()
|
||||
})?;
|
||||
|
||||
let visible_hints = editor.update(cx, |editor, cx| editor.visible_inlay_hints(cx))?;
|
||||
let visible_hints = editor.update(cx, |editor, cx| editor.visible_inlay_hints(cx).cloned().collect::<Vec<_>>())?;
|
||||
let new_hints = match inlay_hints_fetch_task {
|
||||
Some(fetch_task) => {
|
||||
log::debug!(
|
||||
@@ -3570,7 +3570,6 @@ pub mod tests {
|
||||
pub fn visible_hint_labels(editor: &Editor, cx: &Context<Editor>) -> Vec<String> {
|
||||
editor
|
||||
.visible_inlay_hints(cx)
|
||||
.into_iter()
|
||||
.map(|hint| hint.text().to_string())
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -392,6 +392,11 @@ impl SelectionsCollection {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Attempts to build a selection in the provided `DisplayRow` within the
|
||||
/// same range as the provided range of `Pixels`.
|
||||
/// Returns `None` if the range is not empty but it starts past the line's
|
||||
/// length, meaning that the line isn't long enough to be contained within
|
||||
/// part of the provided range.
|
||||
pub fn build_columnar_selection(
|
||||
&mut self,
|
||||
display_map: &DisplaySnapshot,
|
||||
@@ -610,21 +615,32 @@ impl<'a> MutableSelectionsCollection<'a> {
|
||||
self.select(selections);
|
||||
}
|
||||
|
||||
pub fn select<T>(&mut self, mut selections: Vec<Selection<T>>)
|
||||
pub fn select<T>(&mut self, selections: Vec<Selection<T>>)
|
||||
where
|
||||
T: ToOffset + ToPoint + Ord + std::marker::Copy + std::fmt::Debug,
|
||||
T: ToOffset + std::marker::Copy + std::fmt::Debug,
|
||||
{
|
||||
let buffer = self.buffer.read(self.cx).snapshot(self.cx);
|
||||
let mut selections = selections
|
||||
.into_iter()
|
||||
.map(|selection| selection.map(|it| it.to_offset(&buffer)))
|
||||
.map(|mut selection| {
|
||||
if selection.start > selection.end {
|
||||
mem::swap(&mut selection.start, &mut selection.end);
|
||||
selection.reversed = true
|
||||
}
|
||||
selection
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
selections.sort_unstable_by_key(|s| s.start);
|
||||
// Merge overlapping selections.
|
||||
let mut i = 1;
|
||||
while i < selections.len() {
|
||||
if selections[i - 1].end >= selections[i].start {
|
||||
if selections[i].start <= selections[i - 1].end {
|
||||
let removed = selections.remove(i);
|
||||
if removed.start < selections[i - 1].start {
|
||||
selections[i - 1].start = removed.start;
|
||||
}
|
||||
if removed.end > selections[i - 1].end {
|
||||
if selections[i - 1].end < removed.end {
|
||||
selections[i - 1].end = removed.end;
|
||||
}
|
||||
} else {
|
||||
@@ -968,13 +984,10 @@ impl DerefMut for MutableSelectionsCollection<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
fn selection_to_anchor_selection<T>(
|
||||
selection: Selection<T>,
|
||||
fn selection_to_anchor_selection(
|
||||
selection: Selection<usize>,
|
||||
buffer: &MultiBufferSnapshot,
|
||||
) -> Selection<Anchor>
|
||||
where
|
||||
T: ToOffset + Ord,
|
||||
{
|
||||
) -> Selection<Anchor> {
|
||||
let end_bias = if selection.start == selection.end {
|
||||
Bias::Right
|
||||
} else {
|
||||
@@ -1012,7 +1025,7 @@ fn resolve_selections_point<'a>(
|
||||
})
|
||||
}
|
||||
|
||||
// Panics if passed selections are not in order
|
||||
/// Panics if passed selections are not in order
|
||||
fn resolve_selections_display<'a>(
|
||||
selections: impl 'a + IntoIterator<Item = &'a Selection<Anchor>>,
|
||||
map: &'a DisplaySnapshot,
|
||||
@@ -1044,7 +1057,7 @@ fn resolve_selections_display<'a>(
|
||||
coalesce_selections(selections)
|
||||
}
|
||||
|
||||
// Panics if passed selections are not in order
|
||||
/// Panics if passed selections are not in order
|
||||
pub(crate) fn resolve_selections<'a, D, I>(
|
||||
selections: I,
|
||||
map: &'a DisplaySnapshot,
|
||||
|
||||
@@ -866,7 +866,9 @@ impl FileFinderDelegate {
|
||||
let worktrees = self
|
||||
.project
|
||||
.read(cx)
|
||||
.visible_worktrees(cx)
|
||||
.worktree_store()
|
||||
.read(cx)
|
||||
.visible_worktrees_and_single_files(cx)
|
||||
.collect::<Vec<_>>();
|
||||
let include_root_name = worktrees.len() > 1;
|
||||
let candidate_sets = worktrees
|
||||
|
||||
@@ -8,7 +8,7 @@ use pretty_assertions::{assert_eq, assert_matches};
|
||||
use project::{FS_WATCH_LATENCY, RemoveOptions};
|
||||
use serde_json::json;
|
||||
use util::{path, rel_path::rel_path};
|
||||
use workspace::{AppState, CloseActiveItem, OpenOptions, ToggleFileFinder, Workspace};
|
||||
use workspace::{AppState, CloseActiveItem, OpenOptions, ToggleFileFinder, Workspace, open_paths};
|
||||
|
||||
#[ctor::ctor]
|
||||
fn init_logger() {
|
||||
@@ -2337,7 +2337,6 @@ async fn test_search_results_refreshed_on_worktree_updates(cx: &mut gpui::TestAp
|
||||
assert_match_at_position(finder, 1, "main.rs");
|
||||
assert_match_at_position(finder, 2, "rs");
|
||||
});
|
||||
|
||||
// Delete main.rs
|
||||
app_state
|
||||
.fs
|
||||
@@ -2370,6 +2369,64 @@ async fn test_search_results_refreshed_on_worktree_updates(cx: &mut gpui::TestAp
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_search_results_refreshed_on_standalone_file_creation(cx: &mut gpui::TestAppContext) {
|
||||
let app_state = init_test(cx);
|
||||
|
||||
app_state
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree(
|
||||
"/src",
|
||||
json!({
|
||||
"lib.rs": "// Lib file",
|
||||
"main.rs": "// Bar file",
|
||||
"read.me": "// Readme file",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
app_state
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree(
|
||||
"/test",
|
||||
json!({
|
||||
"new.rs": "// New file",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
|
||||
let (workspace, cx) =
|
||||
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
|
||||
cx.update(|_, cx| {
|
||||
open_paths(
|
||||
&[PathBuf::from(path!("/test/new.rs"))],
|
||||
app_state,
|
||||
workspace::OpenOptions::default(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(cx.update(|_, cx| cx.windows().len()), 1);
|
||||
|
||||
let initial_history = open_close_queried_buffer("new", 1, "new.rs", &workspace, cx).await;
|
||||
assert_eq!(
|
||||
initial_history.first().unwrap().absolute,
|
||||
PathBuf::from(path!("/test/new.rs")),
|
||||
"Should show 1st opened item in the history when opening the 2nd item"
|
||||
);
|
||||
|
||||
let history_after_first = open_close_queried_buffer("lib", 1, "lib.rs", &workspace, cx).await;
|
||||
assert_eq!(
|
||||
history_after_first.first().unwrap().absolute,
|
||||
PathBuf::from(path!("/test/new.rs")),
|
||||
"Should show 1st opened item in the history when opening the 2nd item"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_search_results_refreshed_on_adding_and_removing_worktrees(
|
||||
cx: &mut gpui::TestAppContext,
|
||||
|
||||
@@ -755,7 +755,7 @@ impl PickerDelegate for OpenPathDelegate {
|
||||
.with_default_highlights(
|
||||
&window.text_style(),
|
||||
vec![(
|
||||
delta..delta + label_len,
|
||||
delta..label_len,
|
||||
HighlightStyle::color(Color::Conflict.color(cx)),
|
||||
)],
|
||||
)
|
||||
@@ -765,7 +765,7 @@ impl PickerDelegate for OpenPathDelegate {
|
||||
.with_default_highlights(
|
||||
&window.text_style(),
|
||||
vec![(
|
||||
delta..delta + label_len,
|
||||
delta..label_len,
|
||||
HighlightStyle::color(Color::Created.color(cx)),
|
||||
)],
|
||||
)
|
||||
|
||||
@@ -7,6 +7,7 @@ pub mod fs_watcher;
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
use ashpd::desktop::trash;
|
||||
use futures::stream::iter;
|
||||
use gpui::App;
|
||||
use gpui::BackgroundExecutor;
|
||||
use gpui::Global;
|
||||
@@ -562,12 +563,17 @@ impl Fs for RealFs {
|
||||
|
||||
async fn load(&self, path: &Path) -> Result<String> {
|
||||
let path = path.to_path_buf();
|
||||
let text = smol::unblock(|| std::fs::read_to_string(path)).await?;
|
||||
Ok(text)
|
||||
self.executor
|
||||
.spawn(async move { Ok(std::fs::read_to_string(path)?) })
|
||||
.await
|
||||
}
|
||||
|
||||
async fn load_bytes(&self, path: &Path) -> Result<Vec<u8>> {
|
||||
let path = path.to_path_buf();
|
||||
let bytes = smol::unblock(|| std::fs::read(path)).await?;
|
||||
let bytes = self
|
||||
.executor
|
||||
.spawn(async move { std::fs::read(path) })
|
||||
.await?;
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
@@ -635,30 +641,46 @@ impl Fs for RealFs {
|
||||
if let Some(path) = path.parent() {
|
||||
self.create_dir(path).await?;
|
||||
}
|
||||
smol::fs::write(path, content).await?;
|
||||
Ok(())
|
||||
let path = path.to_owned();
|
||||
let contents = content.to_owned();
|
||||
self.executor
|
||||
.spawn(async move {
|
||||
std::fs::write(path, contents)?;
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn canonicalize(&self, path: &Path) -> Result<PathBuf> {
|
||||
Ok(smol::fs::canonicalize(path)
|
||||
let path = path.to_owned();
|
||||
self.executor
|
||||
.spawn(async move {
|
||||
std::fs::canonicalize(&path).with_context(|| format!("canonicalizing {path:?}"))
|
||||
})
|
||||
.await
|
||||
.with_context(|| format!("canonicalizing {path:?}"))?)
|
||||
}
|
||||
|
||||
async fn is_file(&self, path: &Path) -> bool {
|
||||
smol::fs::metadata(path)
|
||||
let path = path.to_owned();
|
||||
self.executor
|
||||
.spawn(async move { std::fs::metadata(path).is_ok_and(|metadata| metadata.is_file()) })
|
||||
.await
|
||||
.is_ok_and(|metadata| metadata.is_file())
|
||||
}
|
||||
|
||||
async fn is_dir(&self, path: &Path) -> bool {
|
||||
smol::fs::metadata(path)
|
||||
let path = path.to_owned();
|
||||
self.executor
|
||||
.spawn(async move { std::fs::metadata(path).is_ok_and(|metadata| metadata.is_dir()) })
|
||||
.await
|
||||
.is_ok_and(|metadata| metadata.is_dir())
|
||||
}
|
||||
|
||||
async fn metadata(&self, path: &Path) -> Result<Option<Metadata>> {
|
||||
let symlink_metadata = match smol::fs::symlink_metadata(path).await {
|
||||
let path_buf = path.to_owned();
|
||||
let symlink_metadata = match self
|
||||
.executor
|
||||
.spawn(async move { std::fs::symlink_metadata(&path_buf) })
|
||||
.await
|
||||
{
|
||||
Ok(metadata) => metadata,
|
||||
Err(err) => {
|
||||
return match (err.kind(), err.raw_os_error()) {
|
||||
@@ -669,19 +691,28 @@ impl Fs for RealFs {
|
||||
}
|
||||
};
|
||||
|
||||
let path_buf = path.to_path_buf();
|
||||
let path_exists = smol::unblock(move || {
|
||||
path_buf
|
||||
.try_exists()
|
||||
.with_context(|| format!("checking existence for path {path_buf:?}"))
|
||||
})
|
||||
.await?;
|
||||
let is_symlink = symlink_metadata.file_type().is_symlink();
|
||||
let metadata = match (is_symlink, path_exists) {
|
||||
(true, true) => smol::fs::metadata(path)
|
||||
.await
|
||||
.with_context(|| "accessing symlink for path {path}")?,
|
||||
_ => symlink_metadata,
|
||||
let metadata = if is_symlink {
|
||||
let path_buf = path.to_path_buf();
|
||||
let path_exists = self
|
||||
.executor
|
||||
.spawn(async move {
|
||||
path_buf
|
||||
.try_exists()
|
||||
.with_context(|| format!("checking existence for path {path_buf:?}"))
|
||||
})
|
||||
.await?;
|
||||
if path_exists {
|
||||
let path_buf = path.to_path_buf();
|
||||
self.executor
|
||||
.spawn(async move { std::fs::metadata(path_buf) })
|
||||
.await
|
||||
.with_context(|| "accessing symlink for path {path}")?
|
||||
} else {
|
||||
symlink_metadata
|
||||
}
|
||||
} else {
|
||||
symlink_metadata
|
||||
};
|
||||
|
||||
#[cfg(unix)]
|
||||
@@ -707,7 +738,11 @@ impl Fs for RealFs {
|
||||
}
|
||||
|
||||
async fn read_link(&self, path: &Path) -> Result<PathBuf> {
|
||||
let path = smol::fs::read_link(path).await?;
|
||||
let path = path.to_owned();
|
||||
let path = self
|
||||
.executor
|
||||
.spawn(async move { std::fs::read_link(&path) })
|
||||
.await?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
@@ -715,7 +750,13 @@ impl Fs for RealFs {
|
||||
&self,
|
||||
path: &Path,
|
||||
) -> Result<Pin<Box<dyn Send + Stream<Item = Result<PathBuf>>>>> {
|
||||
let result = smol::fs::read_dir(path).await?.map(|entry| match entry {
|
||||
let path = path.to_owned();
|
||||
let result = iter(
|
||||
self.executor
|
||||
.spawn(async move { std::fs::read_dir(path) })
|
||||
.await?,
|
||||
)
|
||||
.map(|entry| match entry {
|
||||
Ok(entry) => Ok(entry.path()),
|
||||
Err(error) => Err(anyhow!("failed to read dir entry {error:?}")),
|
||||
});
|
||||
@@ -749,6 +790,7 @@ impl Fs for RealFs {
|
||||
events
|
||||
.into_iter()
|
||||
.map(|event| {
|
||||
log::trace!("fs path event: {event:?}");
|
||||
let kind = if event.flags.contains(StreamFlags::ITEM_REMOVED) {
|
||||
Some(PathEventKind::Removed)
|
||||
} else if event.flags.contains(StreamFlags::ITEM_CREATED) {
|
||||
@@ -806,6 +848,7 @@ impl Fs for RealFs {
|
||||
|
||||
// Check if path is a symlink and follow the target parent
|
||||
if let Some(mut target) = self.read_link(path).await.ok() {
|
||||
log::trace!("watch symlink {path:?} -> {target:?}");
|
||||
// Check if symlink target is relative path, if so make it absolute
|
||||
if target.is_relative()
|
||||
&& let Some(parent) = path.parent()
|
||||
|
||||
@@ -46,6 +46,7 @@ impl Drop for FsWatcher {
|
||||
|
||||
impl Watcher for FsWatcher {
|
||||
fn add(&self, path: &std::path::Path) -> anyhow::Result<()> {
|
||||
log::trace!("watcher add: {path:?}");
|
||||
let tx = self.tx.clone();
|
||||
let pending_paths = self.pending_path_events.clone();
|
||||
|
||||
@@ -63,11 +64,15 @@ impl Watcher for FsWatcher {
|
||||
.next_back()
|
||||
&& path.starts_with(watched_path.as_ref())
|
||||
{
|
||||
log::trace!(
|
||||
"path to watch is covered by existing registration: {path:?}, {watched_path:?}"
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
log::trace!("path to watch is already watched: {path:?}");
|
||||
if self.registrations.lock().contains_key(path) {
|
||||
return Ok(());
|
||||
}
|
||||
@@ -85,6 +90,7 @@ impl Watcher for FsWatcher {
|
||||
let path = path.clone();
|
||||
|g| {
|
||||
g.add(path, mode, move |event: ¬ify::Event| {
|
||||
log::trace!("watcher received event: {event:?}");
|
||||
let kind = match event.kind {
|
||||
EventKind::Create(_) => Some(PathEventKind::Created),
|
||||
EventKind::Modify(_) => Some(PathEventKind::Changed),
|
||||
@@ -126,6 +132,7 @@ impl Watcher for FsWatcher {
|
||||
}
|
||||
|
||||
fn remove(&self, path: &std::path::Path) -> anyhow::Result<()> {
|
||||
log::trace!("remove watched path: {path:?}");
|
||||
let Some(registration) = self.registrations.lock().remove(path) else {
|
||||
return Ok(());
|
||||
};
|
||||
@@ -215,6 +222,7 @@ static FS_WATCHER_INSTANCE: OnceLock<anyhow::Result<GlobalWatcher, notify::Error
|
||||
OnceLock::new();
|
||||
|
||||
fn handle_event(event: Result<notify::Event, notify::Error>) {
|
||||
log::trace!("global handle event: {event:?}");
|
||||
// Filter out access events, which could lead to a weird bug on Linux after upgrading notify
|
||||
// https://github.com/zed-industries/zed/actions/runs/14085230504/job/39449448832
|
||||
let Some(event) = event
|
||||
|
||||
@@ -32,6 +32,7 @@ impl MacWatcher {
|
||||
|
||||
impl Watcher for MacWatcher {
|
||||
fn add(&self, path: &Path) -> Result<()> {
|
||||
log::trace!("mac watcher add: {:?}", path);
|
||||
let handles = self
|
||||
.handles
|
||||
.upgrade()
|
||||
@@ -44,6 +45,9 @@ impl Watcher for MacWatcher {
|
||||
.next_back()
|
||||
&& path.starts_with(watched_path)
|
||||
{
|
||||
log::trace!(
|
||||
"mac watched path starts with existing watched path: {watched_path:?}, {path:?}"
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
|
||||
13
crates/fs_benchmarks/Cargo.toml
Normal file
13
crates/fs_benchmarks/Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "fs_benchmarks"
|
||||
version = "0.1.0"
|
||||
publish.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
fs.workspace = true
|
||||
gpui = {workspace = true, features = ["windows-manifest"]}
|
||||
workspace-hack.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
1
crates/fs_benchmarks/LICENSE-GPL
Symbolic link
1
crates/fs_benchmarks/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
32
crates/fs_benchmarks/src/main.rs
Normal file
32
crates/fs_benchmarks/src/main.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
use fs::Fs;
|
||||
use gpui::{AppContext, Application};
|
||||
fn main() {
|
||||
let Some(path_to_read) = std::env::args().nth(1) else {
|
||||
println!("Expected path to read as 1st argument.");
|
||||
return;
|
||||
};
|
||||
|
||||
let _ = Application::headless().run(|cx| {
|
||||
let fs = fs::RealFs::new(None, cx.background_executor().clone());
|
||||
cx.background_spawn(async move {
|
||||
let timer = std::time::Instant::now();
|
||||
let result = fs.load_bytes(path_to_read.as_ref()).await;
|
||||
let elapsed = timer.elapsed();
|
||||
if let Err(e) = result {
|
||||
println!("Failed `load_bytes` after {elapsed:?} with error `{e}`");
|
||||
} else {
|
||||
println!("Took {elapsed:?} to read {} bytes", result.unwrap().len());
|
||||
};
|
||||
let timer = std::time::Instant::now();
|
||||
let result = fs.metadata(path_to_read.as_ref()).await;
|
||||
let elapsed = timer.elapsed();
|
||||
if let Err(e) = result {
|
||||
println!("Failed `metadata` after {elapsed:?} with error `{e}`");
|
||||
} else {
|
||||
println!("Took {elapsed:?} to query metadata");
|
||||
};
|
||||
std::process::exit(0);
|
||||
})
|
||||
.detach();
|
||||
});
|
||||
}
|
||||
@@ -8,8 +8,8 @@ use git::{
|
||||
repository::CommitSummary,
|
||||
};
|
||||
use gpui::{
|
||||
ClipboardItem, Entity, Hsla, MouseButton, ScrollHandle, Subscription, TextStyle, WeakEntity,
|
||||
prelude::*,
|
||||
ClipboardItem, Entity, Hsla, MouseButton, ScrollHandle, Subscription, TextStyle,
|
||||
TextStyleRefinement, UnderlineStyle, WeakEntity, prelude::*,
|
||||
};
|
||||
use markdown::{Markdown, MarkdownElement};
|
||||
use project::{git_store::Repository, project_settings::ProjectSettings};
|
||||
@@ -17,7 +17,7 @@ use settings::Settings as _;
|
||||
use theme::ThemeSettings;
|
||||
use time::OffsetDateTime;
|
||||
use time_format::format_local_timestamp;
|
||||
use ui::{ContextMenu, Divider, IconButtonShape, prelude::*, tooltip_container};
|
||||
use ui::{ContextMenu, Divider, prelude::*, tooltip_container};
|
||||
use workspace::Workspace;
|
||||
|
||||
const GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED: usize = 20;
|
||||
@@ -61,16 +61,15 @@ impl BlameRenderer for GitBlameRenderer {
|
||||
.mr_2()
|
||||
.child(
|
||||
h_flex()
|
||||
.id(("blame", ix))
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.font_family(style.font().family)
|
||||
.line_height(style.line_height)
|
||||
.id(("blame", ix))
|
||||
.text_color(cx.theme().status().hint)
|
||||
.gap_2()
|
||||
.child(
|
||||
h_flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.child(div().text_color(sha_color).child(short_commit_id))
|
||||
.children(avatar)
|
||||
@@ -209,11 +208,21 @@ impl BlameRenderer for GitBlameRenderer {
|
||||
OffsetDateTime::now_utc(),
|
||||
time_format::TimestampFormat::MediumAbsolute,
|
||||
);
|
||||
let link_color = cx.theme().colors().text_accent;
|
||||
let markdown_style = {
|
||||
let mut style = hover_markdown_style(window, cx);
|
||||
if let Some(code_block) = &style.code_block.text {
|
||||
style.base_text_style.refine(code_block);
|
||||
}
|
||||
style.link.refine(&TextStyleRefinement {
|
||||
color: Some(link_color),
|
||||
underline: Some(UnderlineStyle {
|
||||
color: Some(link_color.opacity(0.4)),
|
||||
thickness: px(1.0),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
});
|
||||
style
|
||||
};
|
||||
|
||||
@@ -250,20 +259,21 @@ impl BlameRenderer for GitBlameRenderer {
|
||||
};
|
||||
|
||||
Some(
|
||||
tooltip_container(cx, |d, cx| {
|
||||
d.occlude()
|
||||
tooltip_container(cx, |this, cx| {
|
||||
this.occlude()
|
||||
.on_mouse_move(|_, _, cx| cx.stop_propagation())
|
||||
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
|
||||
.child(
|
||||
v_flex()
|
||||
.w(gpui::rems(30.))
|
||||
.gap_4()
|
||||
.child(
|
||||
h_flex()
|
||||
.pb_1p5()
|
||||
.gap_x_2()
|
||||
.pb_1()
|
||||
.gap_2()
|
||||
.overflow_x_hidden()
|
||||
.flex_wrap()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.children(avatar)
|
||||
.child(author)
|
||||
.when(!author_email.is_empty(), |this| {
|
||||
@@ -272,30 +282,29 @@ impl BlameRenderer for GitBlameRenderer {
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
.child(author_email.to_owned()),
|
||||
)
|
||||
})
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border_variant),
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("inline-blame-commit-message")
|
||||
.child(message)
|
||||
.track_scroll(&scroll_handle)
|
||||
.py_1p5()
|
||||
.max_h(message_max_height)
|
||||
.overflow_y_scroll()
|
||||
.track_scroll(&scroll_handle),
|
||||
.child(message),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.pt_1p5()
|
||||
.pt_1()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.child(absolute_timestamp)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.gap_1()
|
||||
.when_some(pull_request, |this, pr| {
|
||||
this.child(
|
||||
Button::new(
|
||||
@@ -306,24 +315,24 @@ impl BlameRenderer for GitBlameRenderer {
|
||||
.icon(IconName::PullRequest)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_position(IconPosition::Start)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click(move |_, _, cx| {
|
||||
cx.stop_propagation();
|
||||
cx.open_url(pr.url.as_str())
|
||||
}),
|
||||
)
|
||||
.child(Divider::vertical())
|
||||
})
|
||||
.child(Divider::vertical())
|
||||
.child(
|
||||
Button::new(
|
||||
"commit-sha-button",
|
||||
short_commit_id.clone(),
|
||||
)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.color(Color::Muted)
|
||||
.icon(IconName::FileGit)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click(move |_, window, cx| {
|
||||
CommitView::open(
|
||||
commit_summary.clone(),
|
||||
@@ -337,7 +346,6 @@ impl BlameRenderer for GitBlameRenderer {
|
||||
)
|
||||
.child(
|
||||
IconButton::new("copy-sha-button", IconName::Copy)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.on_click(move |_, _, cx| {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "gpui"
|
||||
version = "0.2.0"
|
||||
version = "0.2.1"
|
||||
edition.workspace = true
|
||||
authors = ["Nathan Sobo <nathan@zed.dev>"]
|
||||
description = "Zed's GPU-accelerated UI framework"
|
||||
|
||||
@@ -374,7 +374,6 @@ impl DataTable {
|
||||
impl Render for DataTable {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.font_family(".SystemUIFont")
|
||||
.bg(gpui::white())
|
||||
.text_sm()
|
||||
.size_full()
|
||||
|
||||
@@ -20,7 +20,6 @@ impl Render for GradientViewer {
|
||||
let color_space = self.color_space;
|
||||
|
||||
div()
|
||||
.font_family(".SystemUIFont")
|
||||
.bg(gpui::white())
|
||||
.size_full()
|
||||
.p_4()
|
||||
|
||||
@@ -47,7 +47,6 @@ impl Render for ImageGallery {
|
||||
div()
|
||||
.image_cache(self.image_cache.clone())
|
||||
.id("main")
|
||||
.font_family(".SystemUIFont")
|
||||
.text_color(gpui::black())
|
||||
.bg(rgb(0xE9E9E9))
|
||||
.overflow_y_scroll()
|
||||
@@ -102,7 +101,6 @@ impl Render for ImageGallery {
|
||||
.child(image_cache(simple_lru_cache("lru-cache", IMAGES_IN_GALLERY)).child(
|
||||
div()
|
||||
.id("main")
|
||||
.font_family(".SystemUIFont")
|
||||
.bg(rgb(0xE9E9E9))
|
||||
.text_color(gpui::black())
|
||||
.overflow_y_scroll()
|
||||
|
||||
@@ -328,7 +328,6 @@ impl Render for PaintingViewer {
|
||||
let dashed = self.dashed;
|
||||
|
||||
div()
|
||||
.font_family(".SystemUIFont")
|
||||
.bg(gpui::white())
|
||||
.size_full()
|
||||
.p_4()
|
||||
|
||||
@@ -70,6 +70,7 @@ struct StateInner {
|
||||
#[allow(clippy::type_complexity)]
|
||||
scroll_handler: Option<Box<dyn FnMut(&ListScrollEvent, &mut Window, &mut App)>>,
|
||||
scrollbar_drag_start_height: Option<Pixels>,
|
||||
measuring_behavior: ListMeasuringBehavior,
|
||||
}
|
||||
|
||||
/// Whether the list is scrolling from top to bottom or bottom to top.
|
||||
@@ -103,6 +104,26 @@ pub enum ListSizingBehavior {
|
||||
Auto,
|
||||
}
|
||||
|
||||
/// The measuring behavior to apply during layout.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum ListMeasuringBehavior {
|
||||
/// Measure all items in the list.
|
||||
/// Note: This can be expensive for the first frame in a large list.
|
||||
Measure(bool),
|
||||
/// Only measure visible items
|
||||
#[default]
|
||||
Visible,
|
||||
}
|
||||
|
||||
impl ListMeasuringBehavior {
|
||||
fn reset(&mut self) {
|
||||
match self {
|
||||
ListMeasuringBehavior::Measure(has_measured) => *has_measured = false,
|
||||
ListMeasuringBehavior::Visible => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The horizontal sizing behavior to apply during layout.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum ListHorizontalSizingBehavior {
|
||||
@@ -203,11 +224,20 @@ impl ListState {
|
||||
scroll_handler: None,
|
||||
reset: false,
|
||||
scrollbar_drag_start_height: None,
|
||||
measuring_behavior: ListMeasuringBehavior::default(),
|
||||
})));
|
||||
this.splice(0..0, item_count);
|
||||
this
|
||||
}
|
||||
|
||||
/// Set the list to measure all items in the list in the first layout phase.
|
||||
///
|
||||
/// This is useful for ensuring that the scrollbar size is correct instead of based on only rendered elements.
|
||||
pub fn measure_all(self) -> Self {
|
||||
self.0.borrow_mut().measuring_behavior = ListMeasuringBehavior::Measure(false);
|
||||
self
|
||||
}
|
||||
|
||||
/// Reset this instantiation of the list state.
|
||||
///
|
||||
/// Note that this will cause scroll events to be dropped until the next paint.
|
||||
@@ -215,6 +245,7 @@ impl ListState {
|
||||
let old_count = {
|
||||
let state = &mut *self.0.borrow_mut();
|
||||
state.reset = true;
|
||||
state.measuring_behavior.reset();
|
||||
state.logical_scroll_top = None;
|
||||
state.scrollbar_drag_start_height = None;
|
||||
state.items.summary().count
|
||||
@@ -524,6 +555,48 @@ impl StateInner {
|
||||
cursor.start().height + logical_scroll_top.offset_in_item
|
||||
}
|
||||
|
||||
fn layout_all_items(
|
||||
&mut self,
|
||||
available_width: Pixels,
|
||||
render_item: &mut RenderItemFn,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
match &mut self.measuring_behavior {
|
||||
ListMeasuringBehavior::Visible => {
|
||||
return;
|
||||
}
|
||||
ListMeasuringBehavior::Measure(has_measured) => {
|
||||
if *has_measured {
|
||||
return;
|
||||
}
|
||||
*has_measured = true;
|
||||
}
|
||||
}
|
||||
|
||||
let mut cursor = self.items.cursor::<Count>(());
|
||||
let available_item_space = size(
|
||||
AvailableSpace::Definite(available_width),
|
||||
AvailableSpace::MinContent,
|
||||
);
|
||||
|
||||
let mut measured_items = Vec::default();
|
||||
|
||||
for (ix, item) in cursor.enumerate() {
|
||||
let size = item.size().unwrap_or_else(|| {
|
||||
let mut element = render_item(ix, window, cx);
|
||||
element.layout_as_root(available_item_space, window, cx)
|
||||
});
|
||||
|
||||
measured_items.push(ListItem::Measured {
|
||||
size,
|
||||
focus_handle: item.focus_handle(),
|
||||
});
|
||||
}
|
||||
|
||||
self.items = SumTree::from_iter(measured_items, ());
|
||||
}
|
||||
|
||||
fn layout_items(
|
||||
&mut self,
|
||||
available_width: Option<Pixels>,
|
||||
@@ -711,6 +784,13 @@ impl StateInner {
|
||||
cx: &mut App,
|
||||
) -> Result<LayoutItemsResponse, ListOffset> {
|
||||
window.transact(|window| {
|
||||
match self.measuring_behavior {
|
||||
ListMeasuringBehavior::Measure(has_measured) if !has_measured => {
|
||||
self.layout_all_items(bounds.size.width, render_item, window, cx);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let mut layout_response = self.layout_items(
|
||||
Some(bounds.size.width),
|
||||
bounds.size.height,
|
||||
|
||||
@@ -180,8 +180,7 @@ impl StyledText {
|
||||
"Can't use `with_default_highlights` and `with_highlights`"
|
||||
);
|
||||
let runs = Self::compute_runs(&self.text, default_style, highlights);
|
||||
self.runs = Some(runs);
|
||||
self
|
||||
self.with_runs(runs)
|
||||
}
|
||||
|
||||
/// Set the styling attributes for the given text, as well as
|
||||
@@ -194,7 +193,15 @@ impl StyledText {
|
||||
self.runs.is_none(),
|
||||
"Can't use `with_highlights` and `with_default_highlights`"
|
||||
);
|
||||
self.delayed_highlights = Some(highlights.into_iter().collect::<Vec<_>>());
|
||||
self.delayed_highlights = Some(
|
||||
highlights
|
||||
.into_iter()
|
||||
.inspect(|(run, _)| {
|
||||
debug_assert!(self.text.is_char_boundary(run.start));
|
||||
debug_assert!(self.text.is_char_boundary(run.end));
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
@@ -207,8 +214,10 @@ impl StyledText {
|
||||
let mut ix = 0;
|
||||
for (range, highlight) in highlights {
|
||||
if ix < range.start {
|
||||
debug_assert!(text.is_char_boundary(range.start));
|
||||
runs.push(default_style.clone().to_run(range.start - ix));
|
||||
}
|
||||
debug_assert!(text.is_char_boundary(range.end));
|
||||
runs.push(
|
||||
default_style
|
||||
.clone()
|
||||
@@ -225,6 +234,11 @@ impl StyledText {
|
||||
|
||||
/// Set the text runs for this piece of text.
|
||||
pub fn with_runs(mut self, runs: Vec<TextRun>) -> Self {
|
||||
let mut text = &**self.text;
|
||||
for run in &runs {
|
||||
text = text.get(run.len..).expect("invalid text run");
|
||||
}
|
||||
assert!(text.is_empty(), "invalid text run");
|
||||
self.runs = Some(runs);
|
||||
self
|
||||
}
|
||||
|
||||
@@ -530,8 +530,18 @@ impl WindowsWindowInner {
|
||||
};
|
||||
let scale_factor = lock.scale_factor;
|
||||
let wheel_scroll_amount = match modifiers.shift {
|
||||
true => lock.system_settings.mouse_wheel_settings.wheel_scroll_chars,
|
||||
false => lock.system_settings.mouse_wheel_settings.wheel_scroll_lines,
|
||||
true => {
|
||||
self.system_settings
|
||||
.borrow()
|
||||
.mouse_wheel_settings
|
||||
.wheel_scroll_chars
|
||||
}
|
||||
false => {
|
||||
self.system_settings
|
||||
.borrow()
|
||||
.mouse_wheel_settings
|
||||
.wheel_scroll_lines
|
||||
}
|
||||
};
|
||||
drop(lock);
|
||||
|
||||
@@ -574,7 +584,11 @@ impl WindowsWindowInner {
|
||||
return Some(1);
|
||||
};
|
||||
let scale_factor = lock.scale_factor;
|
||||
let wheel_scroll_chars = lock.system_settings.mouse_wheel_settings.wheel_scroll_chars;
|
||||
let wheel_scroll_chars = self
|
||||
.system_settings
|
||||
.borrow()
|
||||
.mouse_wheel_settings
|
||||
.wheel_scroll_chars;
|
||||
drop(lock);
|
||||
|
||||
let wheel_distance =
|
||||
@@ -707,11 +721,8 @@ impl WindowsWindowInner {
|
||||
// used by Chrome. However, it may result in one row of pixels being obscured
|
||||
// in our client area. But as Chrome says, "there seems to be no better solution."
|
||||
if is_maximized
|
||||
&& let Some(ref taskbar_position) = self
|
||||
.state
|
||||
.borrow()
|
||||
.system_settings
|
||||
.auto_hide_taskbar_position
|
||||
&& let Some(ref taskbar_position) =
|
||||
self.system_settings.borrow().auto_hide_taskbar_position
|
||||
{
|
||||
// For the auto-hide taskbar, adjust in by 1 pixel on taskbar edge,
|
||||
// so the window isn't treated as a "fullscreen app", which would cause
|
||||
@@ -1101,9 +1112,11 @@ impl WindowsWindowInner {
|
||||
if wparam.0 != 0 {
|
||||
let mut lock = self.state.borrow_mut();
|
||||
let display = lock.display;
|
||||
lock.system_settings.update(display, wparam.0);
|
||||
lock.click_state.system_update(wparam.0);
|
||||
lock.border_offset.update(handle).log_err();
|
||||
// system settings may emit a window message which wants to take the refcell lock, so drop it
|
||||
drop(lock);
|
||||
self.system_settings.borrow_mut().update(display, wparam.0);
|
||||
} else {
|
||||
self.handle_system_theme_changed(handle, lparam)?;
|
||||
};
|
||||
|
||||
@@ -390,7 +390,7 @@ float4 gradient_color(Background background,
|
||||
float pattern_period = pattern_height * sin(stripe_angle);
|
||||
float2x2 rotation = rotate2d(stripe_angle);
|
||||
float2 relative_position = position - bounds.origin;
|
||||
float2 rotated_point = mul(rotation, relative_position);
|
||||
float2 rotated_point = mul(relative_position, rotation);
|
||||
float pattern = fmod(rotated_point.x, pattern_period);
|
||||
float distance = min(pattern, pattern_period - pattern) - pattern_period * (pattern_width / pattern_height) / 2.0f;
|
||||
color = solid_color;
|
||||
|
||||
@@ -5,23 +5,10 @@ use std::{
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use util::ResultExt;
|
||||
use windows::{
|
||||
Win32::{
|
||||
Foundation::{HANDLE, HWND},
|
||||
Graphics::{
|
||||
DirectComposition::{
|
||||
COMPOSITION_FRAME_ID_COMPLETED, COMPOSITION_FRAME_ID_TYPE, COMPOSITION_FRAME_STATS,
|
||||
COMPOSITION_TARGET_ID,
|
||||
},
|
||||
Dwm::{DWM_TIMING_INFO, DwmFlush, DwmGetCompositionTimingInfo},
|
||||
},
|
||||
System::{
|
||||
LibraryLoader::{GetModuleHandleA, GetProcAddress},
|
||||
Performance::QueryPerformanceFrequency,
|
||||
Threading::INFINITE,
|
||||
},
|
||||
},
|
||||
core::{HRESULT, s},
|
||||
use windows::Win32::{
|
||||
Foundation::HWND,
|
||||
Graphics::Dwm::{DWM_TIMING_INFO, DwmFlush, DwmGetCompositionTimingInfo},
|
||||
System::Performance::QueryPerformanceFrequency,
|
||||
};
|
||||
|
||||
static QPC_TICKS_PER_SECOND: LazyLock<u64> = LazyLock::new(|| {
|
||||
@@ -35,20 +22,6 @@ static QPC_TICKS_PER_SECOND: LazyLock<u64> = LazyLock::new(|| {
|
||||
const VSYNC_INTERVAL_THRESHOLD: Duration = Duration::from_millis(1);
|
||||
const DEFAULT_VSYNC_INTERVAL: Duration = Duration::from_micros(16_666); // ~60Hz
|
||||
|
||||
// Here we are using dynamic loading of DirectComposition functions,
|
||||
// or the app will refuse to start on windows systems that do not support DirectComposition.
|
||||
type DCompositionGetFrameId =
|
||||
unsafe extern "system" fn(frameidtype: COMPOSITION_FRAME_ID_TYPE, frameid: *mut u64) -> HRESULT;
|
||||
type DCompositionGetStatistics = unsafe extern "system" fn(
|
||||
frameid: u64,
|
||||
framestats: *mut COMPOSITION_FRAME_STATS,
|
||||
targetidcount: u32,
|
||||
targetids: *mut COMPOSITION_TARGET_ID,
|
||||
actualtargetidcount: *mut u32,
|
||||
) -> HRESULT;
|
||||
type DCompositionWaitForCompositorClock =
|
||||
unsafe extern "system" fn(count: u32, handles: *const HANDLE, timeoutinms: u32) -> u32;
|
||||
|
||||
pub(crate) struct VSyncProvider {
|
||||
interval: Duration,
|
||||
f: Box<dyn Fn() -> bool>,
|
||||
@@ -56,35 +29,12 @@ pub(crate) struct VSyncProvider {
|
||||
|
||||
impl VSyncProvider {
|
||||
pub(crate) fn new() -> Self {
|
||||
if let Some((get_frame_id, get_statistics, wait_for_comp_clock)) =
|
||||
initialize_direct_composition()
|
||||
.context("Retrieving DirectComposition functions")
|
||||
.log_with_level(log::Level::Warn)
|
||||
{
|
||||
let interval = get_dwm_interval_from_direct_composition(get_frame_id, get_statistics)
|
||||
.context("Failed to get DWM interval from DirectComposition")
|
||||
.log_err()
|
||||
.unwrap_or(DEFAULT_VSYNC_INTERVAL);
|
||||
log::info!(
|
||||
"DirectComposition is supported for VSync, interval: {:?}",
|
||||
interval
|
||||
);
|
||||
let f = Box::new(move || unsafe {
|
||||
wait_for_comp_clock(0, std::ptr::null(), INFINITE) == 0
|
||||
});
|
||||
Self { interval, f }
|
||||
} else {
|
||||
let interval = get_dwm_interval()
|
||||
.context("Failed to get DWM interval")
|
||||
.log_err()
|
||||
.unwrap_or(DEFAULT_VSYNC_INTERVAL);
|
||||
log::info!(
|
||||
"DirectComposition is not supported for VSync, falling back to DWM, interval: {:?}",
|
||||
interval
|
||||
);
|
||||
let f = Box::new(|| unsafe { DwmFlush().is_ok() });
|
||||
Self { interval, f }
|
||||
}
|
||||
let interval = get_dwm_interval()
|
||||
.context("Failed to get DWM interval")
|
||||
.log_err()
|
||||
.unwrap_or(DEFAULT_VSYNC_INTERVAL);
|
||||
let f = Box::new(|| unsafe { DwmFlush().is_ok() });
|
||||
Self { interval, f }
|
||||
}
|
||||
|
||||
pub(crate) fn wait_for_vsync(&self) {
|
||||
@@ -105,49 +55,6 @@ impl VSyncProvider {
|
||||
}
|
||||
}
|
||||
|
||||
fn initialize_direct_composition() -> Result<(
|
||||
DCompositionGetFrameId,
|
||||
DCompositionGetStatistics,
|
||||
DCompositionWaitForCompositorClock,
|
||||
)> {
|
||||
unsafe {
|
||||
// Load DLL at runtime since older Windows versions don't have dcomp.
|
||||
let hmodule = GetModuleHandleA(s!("dcomp.dll")).context("Loading dcomp.dll")?;
|
||||
let get_frame_id_addr = GetProcAddress(hmodule, s!("DCompositionGetFrameId"))
|
||||
.context("Function DCompositionGetFrameId not found")?;
|
||||
let get_statistics_addr = GetProcAddress(hmodule, s!("DCompositionGetStatistics"))
|
||||
.context("Function DCompositionGetStatistics not found")?;
|
||||
let wait_for_compositor_clock_addr =
|
||||
GetProcAddress(hmodule, s!("DCompositionWaitForCompositorClock"))
|
||||
.context("Function DCompositionWaitForCompositorClock not found")?;
|
||||
let get_frame_id: DCompositionGetFrameId = std::mem::transmute(get_frame_id_addr);
|
||||
let get_statistics: DCompositionGetStatistics = std::mem::transmute(get_statistics_addr);
|
||||
let wait_for_compositor_clock: DCompositionWaitForCompositorClock =
|
||||
std::mem::transmute(wait_for_compositor_clock_addr);
|
||||
Ok((get_frame_id, get_statistics, wait_for_compositor_clock))
|
||||
}
|
||||
}
|
||||
|
||||
fn get_dwm_interval_from_direct_composition(
|
||||
get_frame_id: DCompositionGetFrameId,
|
||||
get_statistics: DCompositionGetStatistics,
|
||||
) -> Result<Duration> {
|
||||
let mut frame_id = 0;
|
||||
unsafe { get_frame_id(COMPOSITION_FRAME_ID_COMPLETED, &mut frame_id) }.ok()?;
|
||||
let mut stats = COMPOSITION_FRAME_STATS::default();
|
||||
unsafe {
|
||||
get_statistics(
|
||||
frame_id,
|
||||
&mut stats,
|
||||
0,
|
||||
std::ptr::null_mut(),
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
}
|
||||
.ok()?;
|
||||
Ok(retrieve_duration(stats.framePeriod, *QPC_TICKS_PER_SECOND))
|
||||
}
|
||||
|
||||
fn get_dwm_interval() -> Result<Duration> {
|
||||
let mut timing_info = DWM_TIMING_INFO {
|
||||
cbSize: std::mem::size_of::<DWM_TIMING_INFO>() as u32,
|
||||
|
||||
@@ -51,7 +51,6 @@ pub struct WindowsWindowState {
|
||||
pub renderer: DirectXRenderer,
|
||||
|
||||
pub click_state: ClickState,
|
||||
pub system_settings: WindowsSystemSettings,
|
||||
pub current_cursor: Option<HCURSOR>,
|
||||
pub nc_button_pressed: Option<u32>,
|
||||
|
||||
@@ -66,6 +65,7 @@ pub(crate) struct WindowsWindowInner {
|
||||
pub(super) this: Weak<Self>,
|
||||
drop_target_helper: IDropTargetHelper,
|
||||
pub(crate) state: RefCell<WindowsWindowState>,
|
||||
pub(crate) system_settings: RefCell<WindowsSystemSettings>,
|
||||
pub(crate) handle: AnyWindowHandle,
|
||||
pub(crate) hide_title_bar: bool,
|
||||
pub(crate) is_movable: bool,
|
||||
@@ -115,7 +115,6 @@ impl WindowsWindowState {
|
||||
let system_key_handled = false;
|
||||
let hovered = false;
|
||||
let click_state = ClickState::new();
|
||||
let system_settings = WindowsSystemSettings::new(display);
|
||||
let nc_button_pressed = None;
|
||||
let fullscreen = None;
|
||||
let initial_placement = None;
|
||||
@@ -138,7 +137,6 @@ impl WindowsWindowState {
|
||||
hovered,
|
||||
renderer,
|
||||
click_state,
|
||||
system_settings,
|
||||
current_cursor,
|
||||
nc_button_pressed,
|
||||
display,
|
||||
@@ -231,6 +229,7 @@ impl WindowsWindowInner {
|
||||
validation_number: context.validation_number,
|
||||
main_receiver: context.main_receiver.clone(),
|
||||
platform_window_handle: context.platform_window_handle,
|
||||
system_settings: RefCell::new(WindowsSystemSettings::new(context.display)),
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -644,10 +643,12 @@ impl PlatformWindow for WindowsWindow {
|
||||
let mut btn_encoded = Vec::new();
|
||||
for (index, btn) in answers.iter().enumerate() {
|
||||
let encoded = HSTRING::from(btn.label().as_ref());
|
||||
let button_id = if btn.is_cancel() {
|
||||
IDCANCEL.0
|
||||
} else {
|
||||
index as i32 - 100
|
||||
let button_id = match btn {
|
||||
PromptButton::Ok(_) => IDOK.0,
|
||||
PromptButton::Cancel(_) => IDCANCEL.0,
|
||||
// the first few low integer values are reserved for known buttons
|
||||
// so for simplicity we just go backwards from -1
|
||||
PromptButton::Other(_) => -(index as i32) - 1,
|
||||
};
|
||||
button_id_map.push(button_id);
|
||||
buttons.push(TASKDIALOG_BUTTON {
|
||||
@@ -665,11 +666,11 @@ impl PlatformWindow for WindowsWindow {
|
||||
.context("unable to create task dialog")
|
||||
.log_err();
|
||||
|
||||
let clicked = button_id_map
|
||||
.iter()
|
||||
.position(|&button_id| button_id == res)
|
||||
.unwrap();
|
||||
let _ = done_tx.send(clicked);
|
||||
if let Some(clicked) =
|
||||
button_id_map.iter().position(|&button_id| button_id == res)
|
||||
{
|
||||
let _ = done_tx.send(clicked);
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
@@ -403,13 +403,7 @@ impl Default for TextStyle {
|
||||
TextStyle {
|
||||
color: black(),
|
||||
// todo(linux) make this configurable or choose better default
|
||||
font_family: if cfg!(any(target_os = "linux", target_os = "freebsd")) {
|
||||
"FreeMono".into()
|
||||
} else if cfg!(target_os = "windows") {
|
||||
"Segoe UI".into()
|
||||
} else {
|
||||
"Helvetica".into()
|
||||
},
|
||||
font_family: ".SystemUIFont".into(),
|
||||
font_features: FontFeatures::default(),
|
||||
font_fallbacks: None,
|
||||
font_size: rems(1.).into(),
|
||||
|
||||
@@ -73,12 +73,15 @@ impl TextSystem {
|
||||
fallback_font_stack: smallvec![
|
||||
// TODO: Remove this when Linux have implemented setting fallbacks.
|
||||
font(".ZedMono"),
|
||||
font(".ZedSans"),
|
||||
font("Helvetica"),
|
||||
font("Segoe UI"), // Windows
|
||||
font("Cantarell"), // Gnome
|
||||
font("Ubuntu"), // Gnome (Ubuntu)
|
||||
font("Noto Sans"), // KDE
|
||||
font("DejaVu Sans")
|
||||
font("Segoe UI"), // Windows
|
||||
font("Ubuntu"), // Gnome (Ubuntu)
|
||||
font("Adwaita Sans"), // Gnome 47
|
||||
font("Cantarell"), // Gnome
|
||||
font("Noto Sans"), // KDE
|
||||
font("DejaVu Sans"),
|
||||
font("Arial"), // macOS, Windows
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,19 +225,15 @@ impl LineWrapper {
|
||||
|
||||
fn update_runs_after_truncation(result: &str, ellipsis: &str, runs: &mut Vec<TextRun>) {
|
||||
let mut truncate_at = result.len() - ellipsis.len();
|
||||
let mut run_end = None;
|
||||
for (run_index, run) in runs.iter_mut().enumerate() {
|
||||
if run.len <= truncate_at {
|
||||
truncate_at -= run.len;
|
||||
} else {
|
||||
run.len = truncate_at + ellipsis.len();
|
||||
run_end = Some(run_index + 1);
|
||||
runs.truncate(run_index + 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if let Some(run_end) = run_end {
|
||||
runs.truncate(run_end);
|
||||
}
|
||||
}
|
||||
|
||||
/// A fragment of a line that can be wrapped.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
[package]
|
||||
name = "gpui-macros"
|
||||
name = "gpui_macros"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish = true
|
||||
publish = false
|
||||
license = "Apache-2.0"
|
||||
description = "Macros used by gpui"
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
[package]
|
||||
name = "zed-http-client"
|
||||
name = "http_client"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish = true
|
||||
publish = false
|
||||
license = "Apache-2.0"
|
||||
description = "A HTTP client library for Zed and GPUI"
|
||||
|
||||
|
||||
@@ -188,6 +188,9 @@ impl Item for ImageView {
|
||||
fn has_deleted_file(&self, cx: &App) -> bool {
|
||||
self.image_item.read(cx).file.disk_state() == DiskState::Deleted
|
||||
}
|
||||
fn buffer_kind(&self, _: &App) -> workspace::item::ItemBufferKind {
|
||||
workspace::item::ItemBufferKind::Singleton
|
||||
}
|
||||
}
|
||||
|
||||
fn breadcrumbs_text_for_image(project: &Project, image: &ImageItem, cx: &App) -> String {
|
||||
|
||||
@@ -23,7 +23,9 @@ use gpui::{
|
||||
use language::{Language, LanguageConfig, ToOffset as _};
|
||||
use notifications::status_toast::{StatusToast, ToastIcon};
|
||||
use project::{CompletionDisplayOptions, Project};
|
||||
use settings::{BaseKeymap, KeybindSource, KeymapFile, Settings as _, SettingsAssets};
|
||||
use settings::{
|
||||
BaseKeymap, KeybindSource, KeymapFile, Settings as _, SettingsAssets, infer_json_indent_size,
|
||||
};
|
||||
use ui::{
|
||||
ActiveTheme as _, App, Banner, BorrowAppContext, ContextMenu, IconButtonShape, Indicator,
|
||||
Modal, ModalFooter, ModalHeader, ParentElement as _, PopoverMenu, Render, Section,
|
||||
@@ -1198,13 +1200,12 @@ impl KeymapEditor {
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let tab_size = cx.global::<settings::SettingsStore>().json_tab_size();
|
||||
self.previous_edit = Some(PreviousEdit::ScrollBarOffset(
|
||||
self.table_interaction_state.read(cx).scroll_offset(),
|
||||
));
|
||||
let keyboard_mapper = cx.keyboard_mapper().clone();
|
||||
cx.spawn(async move |_, _| {
|
||||
remove_keybinding(to_remove, &fs, tab_size, keyboard_mapper.as_ref()).await
|
||||
remove_keybinding(to_remove, &fs, keyboard_mapper.as_ref()).await
|
||||
})
|
||||
.detach_and_notify_err(window, cx);
|
||||
}
|
||||
@@ -2288,7 +2289,6 @@ impl KeybindingEditorModal {
|
||||
fn save(&mut self, cx: &mut Context<Self>) -> Result<(), InputError> {
|
||||
let existing_keybind = self.editing_keybind.clone();
|
||||
let fs = self.fs.clone();
|
||||
let tab_size = cx.global::<settings::SettingsStore>().json_tab_size();
|
||||
|
||||
let mut new_keystrokes = self.validate_keystrokes(cx).map_err(InputError::error)?;
|
||||
new_keystrokes
|
||||
@@ -2367,7 +2367,6 @@ impl KeybindingEditorModal {
|
||||
&action_mapping,
|
||||
new_action_args.as_deref(),
|
||||
&fs,
|
||||
tab_size,
|
||||
keyboard_mapper.as_ref(),
|
||||
)
|
||||
.await
|
||||
@@ -3019,13 +3018,14 @@ async fn save_keybinding_update(
|
||||
action_mapping: &ActionMapping,
|
||||
new_args: Option<&str>,
|
||||
fs: &Arc<dyn Fs>,
|
||||
tab_size: usize,
|
||||
keyboard_mapper: &dyn PlatformKeyboardMapper,
|
||||
) -> anyhow::Result<()> {
|
||||
let keymap_contents = settings::KeymapFile::load_keymap_file(fs)
|
||||
.await
|
||||
.context("Failed to load keymap file")?;
|
||||
|
||||
let tab_size = infer_json_indent_size(&keymap_contents);
|
||||
|
||||
let existing_keystrokes = existing.keystrokes().unwrap_or_default();
|
||||
let existing_context = existing.context().and_then(KeybindContextString::local_str);
|
||||
let existing_args = existing
|
||||
@@ -3089,7 +3089,6 @@ async fn save_keybinding_update(
|
||||
async fn remove_keybinding(
|
||||
existing: ProcessedBinding,
|
||||
fs: &Arc<dyn Fs>,
|
||||
tab_size: usize,
|
||||
keyboard_mapper: &dyn PlatformKeyboardMapper,
|
||||
) -> anyhow::Result<()> {
|
||||
let Some(keystrokes) = existing.keystrokes() else {
|
||||
@@ -3098,6 +3097,7 @@ async fn remove_keybinding(
|
||||
let keymap_contents = settings::KeymapFile::load_keymap_file(fs)
|
||||
.await
|
||||
.context("Failed to load keymap file")?;
|
||||
let tab_size = infer_json_indent_size(&keymap_contents);
|
||||
|
||||
let operation = settings::KeybindUpdateOperation::Remove {
|
||||
target: settings::KeybindUpdateTarget {
|
||||
|
||||
@@ -269,7 +269,7 @@ async fn test_first_line_pattern(cx: &mut TestAppContext) {
|
||||
async fn test_language_for_file_with_custom_file_types(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
init_settings(cx, |settings| {
|
||||
settings.file_types.extend([
|
||||
settings.file_types.get_or_insert_default().extend([
|
||||
("TypeScript".into(), vec!["js".into()].into()),
|
||||
(
|
||||
"JavaScript".into(),
|
||||
|
||||
@@ -670,6 +670,16 @@ pub struct CodeLabel {
|
||||
pub filter_range: Range<usize>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub struct CodeLabelBuilder {
|
||||
/// The text to display.
|
||||
text: String,
|
||||
/// Syntax highlighting runs.
|
||||
runs: Vec<(Range<usize>, HighlightId)>,
|
||||
/// The portion of the text that should be used in fuzzy filtering.
|
||||
filter_range: Range<usize>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, JsonSchema)]
|
||||
pub struct LanguageConfig {
|
||||
/// Human-readable name of the language.
|
||||
@@ -2223,6 +2233,34 @@ impl Grammar {
|
||||
}
|
||||
}
|
||||
|
||||
impl CodeLabelBuilder {
|
||||
pub fn respan_filter_range(&mut self, filter_text: Option<&str>) {
|
||||
self.filter_range = filter_text
|
||||
.and_then(|filter| self.text.find(filter).map(|ix| ix..ix + filter.len()))
|
||||
.unwrap_or(0..self.text.len());
|
||||
}
|
||||
|
||||
pub fn push_str(&mut self, text: &str, highlight: Option<HighlightId>) {
|
||||
let start_ix = self.text.len();
|
||||
self.text.push_str(text);
|
||||
if let Some(highlight) = highlight {
|
||||
let end_ix = self.text.len();
|
||||
self.runs.push((start_ix..end_ix, highlight));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build(mut self) -> CodeLabel {
|
||||
if self.filter_range.end == 0 {
|
||||
self.respan_filter_range(None);
|
||||
}
|
||||
CodeLabel {
|
||||
text: self.text,
|
||||
runs: self.runs,
|
||||
filter_range: self.filter_range,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CodeLabel {
|
||||
pub fn fallback_for_completion(
|
||||
item: &lsp::CompletionItem,
|
||||
@@ -2286,22 +2324,36 @@ impl CodeLabel {
|
||||
}
|
||||
|
||||
pub fn plain(text: String, filter_text: Option<&str>) -> Self {
|
||||
Self::filtered(text, filter_text, Vec::new())
|
||||
}
|
||||
|
||||
pub fn filtered(
|
||||
text: String,
|
||||
filter_text: Option<&str>,
|
||||
runs: Vec<(Range<usize>, HighlightId)>,
|
||||
) -> Self {
|
||||
let filter_range = filter_text
|
||||
.and_then(|filter| text.find(filter).map(|ix| ix..ix + filter.len()))
|
||||
.unwrap_or(0..text.len());
|
||||
Self {
|
||||
runs: Vec::new(),
|
||||
filter_range,
|
||||
text,
|
||||
}
|
||||
Self::new(text, filter_range, runs)
|
||||
}
|
||||
|
||||
pub fn push_str(&mut self, text: &str, highlight: Option<HighlightId>) {
|
||||
let start_ix = self.text.len();
|
||||
self.text.push_str(text);
|
||||
let end_ix = self.text.len();
|
||||
if let Some(highlight) = highlight {
|
||||
self.runs.push((start_ix..end_ix, highlight));
|
||||
pub fn new(
|
||||
text: String,
|
||||
filter_range: Range<usize>,
|
||||
runs: Vec<(Range<usize>, HighlightId)>,
|
||||
) -> Self {
|
||||
assert!(
|
||||
text.get(filter_range.clone()).is_some(),
|
||||
"invalid filter range"
|
||||
);
|
||||
runs.iter().for_each(|(range, _)| {
|
||||
assert!(text.get(range.clone()).is_some(), "invalid run range");
|
||||
});
|
||||
Self {
|
||||
runs,
|
||||
filter_range,
|
||||
text,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ use itertools::{Either, Itertools};
|
||||
pub use settings::{
|
||||
CompletionSettingsContent, EditPredictionProvider, EditPredictionsMode, FormatOnSave,
|
||||
Formatter, FormatterList, InlayHintKind, LanguageSettingsContent, LspInsertMode,
|
||||
RewrapBehavior, SelectedFormatter, ShowWhitespaceSetting, SoftWrap, WordsCompletionMode,
|
||||
RewrapBehavior, ShowWhitespaceSetting, SoftWrap, WordsCompletionMode,
|
||||
};
|
||||
use settings::{ExtendingVec, Settings, SettingsContent, SettingsLocation, SettingsStore};
|
||||
use shellexpand;
|
||||
@@ -96,7 +96,7 @@ pub struct LanguageSettings {
|
||||
/// when saving it.
|
||||
pub ensure_final_newline_on_save: bool,
|
||||
/// How to perform a buffer format.
|
||||
pub formatter: settings::SelectedFormatter,
|
||||
pub formatter: settings::FormatterList,
|
||||
/// Zed's Prettier integration settings.
|
||||
pub prettier: PrettierSettings,
|
||||
/// Whether to automatically close JSX tags.
|
||||
@@ -639,7 +639,7 @@ impl settings::Settings for AllLanguageSettings {
|
||||
|
||||
let mut file_types: FxHashMap<Arc<str>, GlobSet> = FxHashMap::default();
|
||||
|
||||
for (language, patterns) in &all_languages.file_types {
|
||||
for (language, patterns) in all_languages.file_types.iter().flatten() {
|
||||
let mut builder = GlobSetBuilder::new();
|
||||
|
||||
for pattern in &patterns.0 {
|
||||
@@ -778,6 +778,7 @@ impl settings::Settings for AllLanguageSettings {
|
||||
.project
|
||||
.all_languages
|
||||
.file_types
|
||||
.get_or_insert_default()
|
||||
.extend(associations);
|
||||
|
||||
// cursor global ignore list applies to cursor-tab, so transfer it to edit_predictions.disabled_globs
|
||||
|
||||
@@ -463,11 +463,7 @@ fn build_code_label(
|
||||
|
||||
let filter_range = label.filter_range.clone();
|
||||
text.get(filter_range.clone())?;
|
||||
Some(CodeLabel {
|
||||
text,
|
||||
runs,
|
||||
filter_range,
|
||||
})
|
||||
Some(CodeLabel::new(text, filter_range, runs))
|
||||
}
|
||||
|
||||
fn lsp_completion_to_extension(value: lsp::CompletionItem) -> extension::Completion {
|
||||
@@ -615,11 +611,7 @@ fn test_build_code_label() {
|
||||
|
||||
assert_eq!(
|
||||
label,
|
||||
CodeLabel {
|
||||
text: label_text,
|
||||
runs: label_runs,
|
||||
filter_range: label.filter_range.clone()
|
||||
}
|
||||
CodeLabel::new(label_text, label.filter_range.clone(), label_runs)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -810,15 +810,11 @@ impl LanguageModel for CloudLanguageModel {
|
||||
}
|
||||
cloud_llm_client::LanguageModelProvider::OpenAi => {
|
||||
let client = self.client.clone();
|
||||
let model = match open_ai::Model::from_id(&self.model.id.0) {
|
||||
Ok(model) => model,
|
||||
Err(err) => return async move { Err(anyhow!(err).into()) }.boxed(),
|
||||
};
|
||||
let request = into_open_ai(
|
||||
request,
|
||||
model.id(),
|
||||
model.supports_parallel_tool_calls(),
|
||||
model.supports_prompt_cache_key(),
|
||||
&self.model.id.0,
|
||||
self.model.supports_parallel_tool_calls,
|
||||
true,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
@@ -860,15 +856,11 @@ impl LanguageModel for CloudLanguageModel {
|
||||
}
|
||||
cloud_llm_client::LanguageModelProvider::XAi => {
|
||||
let client = self.client.clone();
|
||||
let model = match x_ai::Model::from_id(&self.model.id.0) {
|
||||
Ok(model) => model,
|
||||
Err(err) => return async move { Err(anyhow!(err).into()) }.boxed(),
|
||||
};
|
||||
let request = into_open_ai(
|
||||
request,
|
||||
model.id(),
|
||||
model.supports_parallel_tool_calls(),
|
||||
model.supports_prompt_cache_key(),
|
||||
&self.model.id.0,
|
||||
self.model.supports_parallel_tool_calls,
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
@@ -188,11 +188,7 @@ impl super::LspAdapter for CLspAdapter {
|
||||
.map(|start| start..start + filter_text.len())
|
||||
})
|
||||
.unwrap_or(detail.len() + 1..text.len());
|
||||
return Some(CodeLabel {
|
||||
filter_range,
|
||||
text,
|
||||
runs,
|
||||
});
|
||||
return Some(CodeLabel::new(text, filter_range, runs));
|
||||
}
|
||||
Some(lsp::CompletionItemKind::CONSTANT | lsp::CompletionItemKind::VARIABLE)
|
||||
if completion.detail.is_some() =>
|
||||
@@ -208,11 +204,7 @@ impl super::LspAdapter for CLspAdapter {
|
||||
.map(|start| start..start + filter_text.len())
|
||||
})
|
||||
.unwrap_or(detail.len() + 1..text.len());
|
||||
return Some(CodeLabel {
|
||||
filter_range,
|
||||
text,
|
||||
runs,
|
||||
});
|
||||
return Some(CodeLabel::new(text, filter_range, runs));
|
||||
}
|
||||
Some(lsp::CompletionItemKind::FUNCTION | lsp::CompletionItemKind::METHOD)
|
||||
if completion.detail.is_some() =>
|
||||
@@ -236,11 +228,7 @@ impl super::LspAdapter for CLspAdapter {
|
||||
filter_start..filter_end
|
||||
});
|
||||
|
||||
return Some(CodeLabel {
|
||||
filter_range,
|
||||
text,
|
||||
runs,
|
||||
});
|
||||
return Some(CodeLabel::new(text, filter_range, runs));
|
||||
}
|
||||
Some(kind) => {
|
||||
let highlight_name = match kind {
|
||||
@@ -324,11 +312,11 @@ impl super::LspAdapter for CLspAdapter {
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
Some(CodeLabel {
|
||||
runs: language.highlight_text(&text.as_str().into(), display_range.clone()),
|
||||
text: text[display_range].to_string(),
|
||||
Some(CodeLabel::new(
|
||||
text[display_range.clone()].to_string(),
|
||||
filter_range,
|
||||
})
|
||||
language.highlight_text(&text.as_str().into(), display_range),
|
||||
))
|
||||
}
|
||||
|
||||
fn prepare_initialize_params(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user