Compare commits
200 Commits
v0.185.10
...
temperatur
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d3af4d2d2 | ||
|
|
c92b2e31e1 | ||
|
|
09d3ff9dbe | ||
|
|
544e8fc46c | ||
|
|
b83d00d69b | ||
|
|
7a9165d5ce | ||
|
|
80236d0bb9 | ||
|
|
a743035286 | ||
|
|
bbfcd885ab | ||
|
|
a378b3f300 | ||
|
|
6d2c39c265 | ||
|
|
1a80103eaf | ||
|
|
6cb436565f | ||
|
|
007fd0586a | ||
|
|
7d361ec97e | ||
|
|
a9d5b2064e | ||
|
|
0f50e6b1d1 | ||
|
|
fed5f89f8d | ||
|
|
096355915a | ||
|
|
e44367c6d0 | ||
|
|
07e6e49583 | ||
|
|
6e9f8f997e | ||
|
|
daba603e27 | ||
|
|
ac007139ab | ||
|
|
68793c0ac2 | ||
|
|
de554589a8 | ||
|
|
4fdd14c3d8 | ||
|
|
848c4f77a6 | ||
|
|
06794f35bc | ||
|
|
ef31252ef8 | ||
|
|
5640265160 | ||
|
|
9d97e08e4f | ||
|
|
6b37646179 | ||
|
|
da3a696a60 | ||
|
|
6bacea28bc | ||
|
|
3b90d62bb2 | ||
|
|
55a0bb2a91 | ||
|
|
210c338df4 | ||
|
|
86cc5c2b55 | ||
|
|
a0bfe4d293 | ||
|
|
52ea501f4f | ||
|
|
a07ba3c718 | ||
|
|
2eb10ab9fb | ||
|
|
55fd8352e4 | ||
|
|
0b10eb7577 | ||
|
|
3d737fd268 | ||
|
|
c5d8407df4 | ||
|
|
377909a646 | ||
|
|
bdd911f89e | ||
|
|
34e10e4e56 | ||
|
|
275c808b03 | ||
|
|
b214c9e4a8 | ||
|
|
2aa06d1d0f | ||
|
|
9568fa1166 | ||
|
|
b4653c15b8 | ||
|
|
4896e0bc02 | ||
|
|
0bf682a0d5 | ||
|
|
3d0c4d716d | ||
|
|
b6c7df8183 | ||
|
|
1aa92d9928 | ||
|
|
6e28400e17 | ||
|
|
78545a93ea | ||
|
|
dd79c29af9 | ||
|
|
7f868a2eff | ||
|
|
6497aa5341 | ||
|
|
55b908a8bf | ||
|
|
ff215b4f11 | ||
|
|
c12e6376b8 | ||
|
|
9cb5ffac25 | ||
|
|
8199664a5a | ||
|
|
7dfbe0b908 | ||
|
|
e64f5ff358 | ||
|
|
181cd6294f | ||
|
|
769ec59162 | ||
|
|
e9616259d0 | ||
|
|
7164124512 | ||
|
|
76c0eded0d | ||
|
|
c56a1cf2b1 | ||
|
|
4b9b908233 | ||
|
|
10bdf39497 | ||
|
|
07b4480396 | ||
|
|
b0414df921 | ||
|
|
0246ec2dab | ||
|
|
a72ade8762 | ||
|
|
1c44cabaea | ||
|
|
5674b5cd4d | ||
|
|
4a7b3aa4b8 | ||
|
|
c765da1c82 | ||
|
|
b404024c7a | ||
|
|
ce053c9bff | ||
|
|
251f26d48a | ||
|
|
7133699335 | ||
|
|
1adb4ecc95 | ||
|
|
0048e67832 | ||
|
|
0119b66426 | ||
|
|
45fe158bc9 | ||
|
|
55eb0710ed | ||
|
|
3e2abbf53b | ||
|
|
a2fa10f35f | ||
|
|
3db4744e18 | ||
|
|
fe177f5d69 | ||
|
|
a19687a815 | ||
|
|
eb15ed7d60 | ||
|
|
52da375a9d | ||
|
|
3594a52bee | ||
|
|
76ad1a29a5 | ||
|
|
86484233c0 | ||
|
|
f4e9ea3cd8 | ||
|
|
161f6dfcb6 | ||
|
|
a0895a6ed8 | ||
|
|
bb82d9ca82 | ||
|
|
007685f6d4 | ||
|
|
c3d9cdecab | ||
|
|
3984531a45 | ||
|
|
cceb13b7cd | ||
|
|
427101b634 | ||
|
|
4d51602e7b | ||
|
|
ca1dc821cf | ||
|
|
2e3baef299 | ||
|
|
545ae27079 | ||
|
|
425f32e068 | ||
|
|
9c11d24887 | ||
|
|
1fc57ea9f5 | ||
|
|
c3d2831d86 | ||
|
|
c1247977ed | ||
|
|
12c26a4fa6 | ||
|
|
7f8e3fd482 | ||
|
|
f0515d1c34 | ||
|
|
10a7f2a972 | ||
|
|
5053562e28 | ||
|
|
1877fce609 | ||
|
|
64316309aa | ||
|
|
04772bf17d | ||
|
|
4d1df7bcd7 | ||
|
|
9547d42b15 | ||
|
|
c918f6cde1 | ||
|
|
da98e300cc | ||
|
|
e6b0d8e48b | ||
|
|
9147f89257 | ||
|
|
9efc09c5a6 | ||
|
|
e6f6b351b7 | ||
|
|
fde621f0e3 | ||
|
|
c4556e9909 | ||
|
|
7e2de84155 | ||
|
|
d1b35be353 | ||
|
|
49a71ec3b8 | ||
|
|
3bd7ae6e5b | ||
|
|
225deb6785 | ||
|
|
33011f2eaf | ||
|
|
e14d078f8a | ||
|
|
460ac96df4 | ||
|
|
35539847a4 | ||
|
|
f619d5f02a | ||
|
|
ba59305510 | ||
|
|
672a1dd553 | ||
|
|
93cc4946d8 | ||
|
|
0c0a4ed866 | ||
|
|
51f1998107 | ||
|
|
1ffedf4a08 | ||
|
|
d25da9728b | ||
|
|
e1e3f2e423 | ||
|
|
92b9ecd7d2 | ||
|
|
758d260cec | ||
|
|
8d4d3badf3 | ||
|
|
7c23d13773 | ||
|
|
ad87c545c7 | ||
|
|
23fbab15ee | ||
|
|
d7e181576e | ||
|
|
9788aff4b1 | ||
|
|
2a319efade | ||
|
|
50ec26c163 | ||
|
|
39dd133b1c | ||
|
|
24eb039752 | ||
|
|
bffa53d706 | ||
|
|
0e5e8f9f8d | ||
|
|
96d785cb45 | ||
|
|
57610c9935 | ||
|
|
5bf1b4f0a8 | ||
|
|
f891dfb358 | ||
|
|
e3a2d52472 | ||
|
|
122af4fd53 | ||
|
|
e07ffe7cf1 | ||
|
|
5e4be013af | ||
|
|
f055dca592 | ||
|
|
5872276511 | ||
|
|
1bf9e15f26 | ||
|
|
f046d70625 | ||
|
|
afeb3d4fd9 | ||
|
|
92dd6b67c7 | ||
|
|
38ede4bae3 | ||
|
|
fc920bf63d | ||
|
|
04c68dc0cf | ||
|
|
399eced884 | ||
|
|
50f705e779 | ||
|
|
8173534ad5 | ||
|
|
8c03934b26 | ||
|
|
84e4891d54 | ||
|
|
d03d8ccec1 | ||
|
|
4d934f2884 | ||
|
|
e697cf9747 |
2
.github/workflows/eval.yml
vendored
2
.github/workflows/eval.yml
vendored
@@ -69,7 +69,7 @@ jobs:
|
||||
run: cargo build --package=eval
|
||||
|
||||
- name: Run eval
|
||||
run: cargo run --package=eval -- --repetitions=3 --concurrency=1
|
||||
run: cargo run --package=eval -- --repetitions=8 --concurrency=1
|
||||
|
||||
# Even the Linux runner is not stateful, in theory there is no need to do this cleanup.
|
||||
# But, to avoid potential issues in the future if we choose to use a stateful Linux runner and forget to add code
|
||||
|
||||
@@ -46,5 +46,17 @@
|
||||
"formatter": "auto",
|
||||
"remove_trailing_whitespace_on_save": true,
|
||||
"ensure_final_newline_on_save": true,
|
||||
"file_scan_exclusions": ["crates/eval/worktrees/", "crates/eval/repos/"]
|
||||
"file_scan_exclusions": [
|
||||
"crates/eval/worktrees/",
|
||||
"crates/eval/repos/",
|
||||
"**/.git",
|
||||
"**/.svn",
|
||||
"**/.hg",
|
||||
"**/.jj",
|
||||
"**/CVS",
|
||||
"**/.DS_Store",
|
||||
"**/Thumbs.db",
|
||||
"**/.classpath",
|
||||
"**/.settings"
|
||||
]
|
||||
}
|
||||
|
||||
19
Cargo.lock
generated
19
Cargo.lock
generated
@@ -62,6 +62,7 @@ dependencies = [
|
||||
"chrono",
|
||||
"client",
|
||||
"collections",
|
||||
"command_palette_hooks",
|
||||
"component",
|
||||
"context_server",
|
||||
"convert_case 0.8.0",
|
||||
@@ -546,6 +547,7 @@ dependencies = [
|
||||
"collections",
|
||||
"context_server",
|
||||
"editor",
|
||||
"feature_flags",
|
||||
"fs",
|
||||
"futures 0.3.31",
|
||||
"fuzzy",
|
||||
@@ -593,8 +595,8 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anthropic",
|
||||
"anyhow",
|
||||
"collections",
|
||||
"deepseek",
|
||||
"feature_flags",
|
||||
"fs",
|
||||
"gpui",
|
||||
"indexmap",
|
||||
@@ -610,7 +612,6 @@ dependencies = [
|
||||
"serde_json_lenient",
|
||||
"settings",
|
||||
"workspace-hack",
|
||||
"zed_llm_client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3004,7 +3005,6 @@ dependencies = [
|
||||
"anyhow",
|
||||
"assistant",
|
||||
"assistant_context_editor",
|
||||
"assistant_settings",
|
||||
"assistant_slash_command",
|
||||
"assistant_tool",
|
||||
"async-stripe",
|
||||
@@ -3238,6 +3238,7 @@ dependencies = [
|
||||
"gpui",
|
||||
"linkme",
|
||||
"parking_lot",
|
||||
"strum 0.27.1",
|
||||
"theme",
|
||||
"workspace-hack",
|
||||
]
|
||||
@@ -4100,6 +4101,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"dap",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"language",
|
||||
"lsp-types",
|
||||
@@ -4393,6 +4395,7 @@ dependencies = [
|
||||
"ctor",
|
||||
"editor",
|
||||
"env_logger 0.11.8",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"indoc",
|
||||
"language",
|
||||
@@ -7981,6 +7984,7 @@ dependencies = [
|
||||
"log",
|
||||
"lsp",
|
||||
"node_runtime",
|
||||
"parking_lot",
|
||||
"paths",
|
||||
"pet",
|
||||
"pet-conda",
|
||||
@@ -14744,6 +14748,7 @@ dependencies = [
|
||||
"log",
|
||||
"project",
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
"schemars",
|
||||
"search",
|
||||
"serde",
|
||||
@@ -15080,6 +15085,7 @@ dependencies = [
|
||||
"client",
|
||||
"collections",
|
||||
"db",
|
||||
"feature_flags",
|
||||
"gpui",
|
||||
"http_client",
|
||||
"notifications",
|
||||
@@ -17085,18 +17091,22 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"client",
|
||||
"component",
|
||||
"db",
|
||||
"documented",
|
||||
"editor",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
"install_cli",
|
||||
"language",
|
||||
"linkme",
|
||||
"picker",
|
||||
"project",
|
||||
"schemars",
|
||||
"serde",
|
||||
"settings",
|
||||
"telemetry",
|
||||
"theme",
|
||||
"ui",
|
||||
"util",
|
||||
"vim_mode_setting",
|
||||
@@ -18148,6 +18158,7 @@ dependencies = [
|
||||
"itertools 0.14.0",
|
||||
"language",
|
||||
"log",
|
||||
"menu",
|
||||
"node_runtime",
|
||||
"parking_lot",
|
||||
"postage",
|
||||
@@ -18683,7 +18694,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.185.10"
|
||||
version = "0.186.0"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"agent",
|
||||
|
||||
@@ -433,6 +433,7 @@ dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "be69a0
|
||||
dashmap = "6.0"
|
||||
derive_more = "0.99.17"
|
||||
dirs = "4.0"
|
||||
documented = "0.9.1"
|
||||
dotenv = "0.15.0"
|
||||
ec4rs = "1.1"
|
||||
emojis = "0.6.1"
|
||||
@@ -797,5 +798,6 @@ ignored = [
|
||||
"serde",
|
||||
"component",
|
||||
"linkme",
|
||||
"documented",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
1
assets/icons/user_check.svg
Normal file
1
assets/icons/user_check.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-user-round-check-icon lucide-user-round-check"><path d="M2 21a8 8 0 0 1 13.292-6"/><circle cx="10" cy="8" r="5"/><path d="m16 19 2 2 4-4"/></svg>
|
||||
|
After Width: | Height: | Size: 348 B |
@@ -237,14 +237,11 @@
|
||||
"save": "workspace::Save",
|
||||
"ctrl->": "assistant::QuoteSelection",
|
||||
"ctrl-<": "assistant::InsertIntoEditor",
|
||||
"ctrl-alt-/": "agent::ToggleModelSelector",
|
||||
"shift-enter": "assistant::Split",
|
||||
"ctrl-r": "assistant::CycleMessageRole",
|
||||
"enter": "assistant::ConfirmCommand",
|
||||
"alt-enter": "editor::Newline",
|
||||
"ctrl-k c": "assistant::CopyCode",
|
||||
"ctrl-g": "search::SelectNextMatch",
|
||||
"ctrl-shift-g": "search::SelectPreviousMatch",
|
||||
"ctrl-k l": "agent::OpenRulesLibrary"
|
||||
"alt-enter": "editor::Newline"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -261,8 +258,7 @@
|
||||
"ctrl-shift-o": "agent::ToggleNavigationMenu",
|
||||
"ctrl-shift-i": "agent::ToggleOptionsMenu",
|
||||
"shift-escape": "agent::ExpandMessageEditor",
|
||||
"ctrl-alt-e": "agent::RemoveAllContext",
|
||||
"ctrl-shift-e": "project_panel::ToggleFocus"
|
||||
"ctrl-alt-e": "agent::RemoveAllContext"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -983,5 +979,12 @@
|
||||
"enter": "editor::Newline",
|
||||
"ctrl-enter": "menu::Confirm"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Diagnostics",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-r": "diagnostics::ToggleDiagnosticsRefresh"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1091,5 +1091,12 @@
|
||||
"enter": "editor::Newline",
|
||||
"cmd-enter": "menu::Confirm"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Diagnostics",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-r": "diagnostics::ToggleDiagnosticsRefresh"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -307,6 +307,11 @@
|
||||
// Whether to show agent review buttons in the editor toolbar.
|
||||
"agent_review": true
|
||||
},
|
||||
// Titlebar related settings
|
||||
"title_bar": {
|
||||
// Whether to show the branch icon beside branch switcher in the title bar.
|
||||
"show_branch_icon": false
|
||||
},
|
||||
// Scrollbar related settings
|
||||
"scrollbar": {
|
||||
// When to show the scrollbar in the editor.
|
||||
@@ -605,11 +610,13 @@
|
||||
//
|
||||
// Default: main
|
||||
"fallback_branch_name": "main",
|
||||
|
||||
// Whether to sort entries in the panel by path
|
||||
// or by status (the default).
|
||||
//
|
||||
// Default: false
|
||||
"sort_by_path": false,
|
||||
|
||||
"scrollbar": {
|
||||
// When to show the scrollbar in the git panel.
|
||||
//
|
||||
@@ -635,8 +642,6 @@
|
||||
"version": "2",
|
||||
// Whether the agent is enabled.
|
||||
"enabled": true,
|
||||
/// What completion mode to start new threads in, if available. Can be 'normal' or 'max'.
|
||||
"preferred_completion_mode": "normal",
|
||||
// Whether to show the agent panel button in the status bar.
|
||||
"button": true,
|
||||
// Where to dock the agent panel. Can be 'left', 'right' or 'bottom'.
|
||||
@@ -659,28 +664,6 @@
|
||||
// The model to use.
|
||||
"model": "claude-3-7-sonnet-latest"
|
||||
},
|
||||
// Additional parameters for language model requests. When making a request to a model, parameters will be taken
|
||||
// from the last entry in this list that matches the model's provider and name. In each entry, both provider
|
||||
// and model are optional, so that you can specify parameters for either one.
|
||||
"model_parameters": [
|
||||
// To set parameters for all requests to OpenAI models:
|
||||
// {
|
||||
// "provider": "openai",
|
||||
// "temperature": 0.5
|
||||
// }
|
||||
//
|
||||
// To set parameters for all requests in general:
|
||||
// {
|
||||
// "temperature": 0
|
||||
// }
|
||||
//
|
||||
// To set parameters for a specific provider and model:
|
||||
// {
|
||||
// "provider": "zed.dev",
|
||||
// "model": "claude-3-7-sonnet-latest",
|
||||
// "temperature": 1.0
|
||||
// }
|
||||
],
|
||||
// When enabled, the agent can run potentially destructive actions without asking for your confirmation.
|
||||
"always_allow_tool_actions": false,
|
||||
// When enabled, the agent will stream edits.
|
||||
@@ -860,7 +843,20 @@
|
||||
// "modal_max_width": "full"
|
||||
//
|
||||
// Default: small
|
||||
"modal_max_width": "small"
|
||||
"modal_max_width": "small",
|
||||
// Determines whether the file finder should skip focus for the active file in search results.
|
||||
// There are 2 possible values:
|
||||
//
|
||||
// 1. true: When searching for files, if the currently active file appears as the first result,
|
||||
// auto-focus will skip it and focus the second result instead.
|
||||
// "skip_focus_for_active_in_search": true
|
||||
//
|
||||
// 2. false: When searching for files, the first result will always receive focus,
|
||||
// even if it's the currently active file.
|
||||
// "skip_focus_for_active_in_search": false
|
||||
//
|
||||
// Default: true
|
||||
"skip_focus_for_active_in_search": true
|
||||
},
|
||||
// Whether or not to remove any trailing whitespace from lines of a buffer
|
||||
// before saving it.
|
||||
@@ -912,6 +908,8 @@
|
||||
"hard_tabs": false,
|
||||
// How many columns a tab should occupy.
|
||||
"tab_size": 4,
|
||||
// What debuggers are preferred by default for all languages.
|
||||
"debuggers": [],
|
||||
// Control what info is collected by Zed.
|
||||
"telemetry": {
|
||||
// Send debug info like crash reports.
|
||||
@@ -943,6 +941,11 @@
|
||||
// The minimum severity of the diagnostics to show inline.
|
||||
// Shows all diagnostics when not specified.
|
||||
"max_severity": null
|
||||
},
|
||||
"cargo": {
|
||||
// When enabled, Zed disables rust-analyzer's check on save and starts to query
|
||||
// Cargo diagnostics separately.
|
||||
"fetch_cargo_diagnostics": false
|
||||
}
|
||||
},
|
||||
// Files or globs of files that will be excluded by Zed entirely. They will be skipped during file
|
||||
@@ -1326,6 +1329,9 @@
|
||||
"Elixir": {
|
||||
"language_servers": ["elixir-ls", "!next-ls", "!lexical", "..."]
|
||||
},
|
||||
"Elm": {
|
||||
"tab_size": 4
|
||||
},
|
||||
"Erlang": {
|
||||
"language_servers": ["erlang-ls", "!elp", "..."]
|
||||
},
|
||||
|
||||
@@ -84,19 +84,6 @@ impl ActivityIndicator {
|
||||
})
|
||||
.detach();
|
||||
|
||||
let mut status_events = languages.dap_server_binary_statuses();
|
||||
cx.spawn(async move |this, cx| {
|
||||
while let Some((name, status)) = status_events.next().await {
|
||||
this.update(cx, |this, cx| {
|
||||
this.statuses.retain(|s| s.name != name);
|
||||
this.statuses.push(ServerStatus { name, status });
|
||||
cx.notify();
|
||||
})?;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.subscribe(
|
||||
&project.read(cx).lsp_store(),
|
||||
|_, _, event, cx| match event {
|
||||
|
||||
@@ -29,6 +29,7 @@ buffer_diff.workspace = true
|
||||
chrono.workspace = true
|
||||
client.workspace = true
|
||||
collections.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
component.workspace = true
|
||||
context_server.workspace = true
|
||||
convert_case.workspace = true
|
||||
|
||||
@@ -1417,10 +1417,7 @@ impl ActiveThread {
|
||||
messages: vec![request_message],
|
||||
tools: vec![],
|
||||
stop: vec![],
|
||||
temperature: AssistantSettings::temperature_for_model(
|
||||
&configured_model.model,
|
||||
cx,
|
||||
),
|
||||
temperature: None,
|
||||
};
|
||||
|
||||
Some(configured_model.model.count_tokens(request, cx))
|
||||
@@ -3466,11 +3463,6 @@ pub(crate) fn open_active_thread_as_markdown(
|
||||
.unwrap_or_else(|| "Thread".to_string());
|
||||
|
||||
let project = workspace.project().clone();
|
||||
|
||||
if !project.read(cx).is_local() {
|
||||
anyhow::bail!("failed to open active thread as markdown in remote project");
|
||||
}
|
||||
|
||||
let buffer = project.update(cx, |project, cx| {
|
||||
project.create_local_buffer(&markdown, Some(markdown_language), cx)
|
||||
});
|
||||
|
||||
@@ -29,6 +29,8 @@ use std::sync::Arc;
|
||||
|
||||
use assistant_settings::{AgentProfileId, AssistantSettings};
|
||||
use client::Client;
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
use feature_flags::{Assistant2FeatureFlag, FeatureFlagAppExt};
|
||||
use fs::Fs;
|
||||
use gpui::{App, actions, impl_actions};
|
||||
use language::LanguageRegistry;
|
||||
@@ -105,6 +107,8 @@ impl ManageProfiles {
|
||||
|
||||
impl_actions!(agent, [NewThread, ManageProfiles]);
|
||||
|
||||
const NAMESPACE: &str = "agent";
|
||||
|
||||
/// Initializes the `agent` crate.
|
||||
pub fn init(
|
||||
fs: Arc<dyn Fs>,
|
||||
@@ -132,4 +136,25 @@ pub fn init(
|
||||
);
|
||||
cx.observe_new(AddContextServerModal::register).detach();
|
||||
cx.observe_new(ManageProfilesModal::register).detach();
|
||||
|
||||
feature_gate_agent_actions(cx);
|
||||
}
|
||||
|
||||
fn feature_gate_agent_actions(cx: &mut App) {
|
||||
CommandPaletteFilter::update_global(cx, |filter, _cx| {
|
||||
filter.hide_namespace(NAMESPACE);
|
||||
});
|
||||
|
||||
cx.observe_flag::<Assistant2FeatureFlag, _>(move |is_enabled, cx| {
|
||||
if is_enabled {
|
||||
CommandPaletteFilter::update_global(cx, |filter, _cx| {
|
||||
filter.show_namespace(NAMESPACE);
|
||||
});
|
||||
} else {
|
||||
CommandPaletteFilter::update_global(cx, |filter, _cx| {
|
||||
filter.hide_namespace(NAMESPACE);
|
||||
});
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ use ui::{
|
||||
use util::{ResultExt as _, maybe};
|
||||
use workspace::dock::{DockPosition, Panel, PanelEvent};
|
||||
use workspace::{CollaboratorId, DraggedSelection, DraggedTab, ToolbarItemView, Workspace};
|
||||
use zed_actions::agent::{OpenConfiguration, OpenOnboardingModal, ResetOnboarding};
|
||||
use zed_actions::agent::OpenConfiguration;
|
||||
use zed_actions::assistant::{OpenRulesLibrary, ToggleFocus};
|
||||
use zed_actions::{DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize};
|
||||
use zed_llm_client::UsageLimit;
|
||||
@@ -59,7 +59,6 @@ use crate::message_editor::{MessageEditor, MessageEditorEvent};
|
||||
use crate::thread::{Thread, ThreadError, ThreadId, TokenUsageRatio};
|
||||
use crate::thread_history::{EntryTimeFormat, PastContext, PastThread, ThreadHistory};
|
||||
use crate::thread_store::ThreadStore;
|
||||
use crate::ui::AgentOnboardingModal;
|
||||
use crate::{
|
||||
AddContextServer, AgentDiffPane, ContextStore, DeleteRecentlyOpenThread, ExpandMessageEditor,
|
||||
Follow, InlineAssistant, NewTextThread, NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff,
|
||||
@@ -146,13 +145,6 @@ pub fn init(cx: &mut App) {
|
||||
});
|
||||
}
|
||||
})
|
||||
.register_action(|workspace, _: &OpenOnboardingModal, window, cx| {
|
||||
AgentOnboardingModal::toggle(workspace, window, cx)
|
||||
})
|
||||
.register_action(|_workspace, _: &ResetOnboarding, window, cx| {
|
||||
window.dispatch_action(workspace::RestoreBanner.boxed_clone(), cx);
|
||||
window.refresh();
|
||||
})
|
||||
.register_action(|_workspace, _: &ResetTrialUpsell, _window, cx| {
|
||||
set_trial_upsell_dismissed(false, cx);
|
||||
});
|
||||
@@ -1470,7 +1462,6 @@ impl AssistantPanel {
|
||||
let thread = active_thread.thread().read(cx);
|
||||
let thread_id = thread.id().clone();
|
||||
let is_empty = active_thread.is_empty();
|
||||
let editor_empty = self.message_editor.read(cx).is_editor_fully_empty(cx);
|
||||
let last_usage = active_thread.thread().read(cx).last_usage().or_else(|| {
|
||||
maybe!({
|
||||
let amount = user_store.model_request_usage_amount()?;
|
||||
@@ -1493,7 +1484,7 @@ impl AssistantPanel {
|
||||
let account_url = zed_urls::account_url(cx);
|
||||
|
||||
let show_token_count = match &self.active_view {
|
||||
ActiveView::Thread { .. } => !is_empty || !editor_empty,
|
||||
ActiveView::Thread { .. } => !is_empty,
|
||||
ActiveView::PromptEditor { .. } => true,
|
||||
_ => false,
|
||||
};
|
||||
@@ -1813,10 +1804,6 @@ impl AssistantPanel {
|
||||
}
|
||||
|
||||
fn should_render_upsell(&self, cx: &mut Context<Self>) -> bool {
|
||||
if !matches!(self.active_view, ActiveView::Thread { .. }) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if self.hide_trial_upsell || dismissed_trial_upsell() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ use crate::context::ContextLoadResult;
|
||||
use crate::inline_prompt_editor::CodegenStatus;
|
||||
use crate::{context::load_context, context_store::ContextStore};
|
||||
use anyhow::Result;
|
||||
use assistant_settings::AssistantSettings;
|
||||
use client::telemetry::Telemetry;
|
||||
use collections::HashSet;
|
||||
use editor::{Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset as _, ToPoint};
|
||||
@@ -384,7 +383,7 @@ impl CodegenAlternative {
|
||||
if user_prompt.trim().to_lowercase() == "delete" {
|
||||
async { Ok(LanguageModelTextStream::default()) }.boxed_local()
|
||||
} else {
|
||||
let request = self.build_request(&model, user_prompt, cx)?;
|
||||
let request = self.build_request(user_prompt, cx)?;
|
||||
cx.spawn(async move |_, cx| model.stream_completion_text(request.await, &cx).await)
|
||||
.boxed_local()
|
||||
};
|
||||
@@ -394,7 +393,6 @@ impl CodegenAlternative {
|
||||
|
||||
fn build_request(
|
||||
&self,
|
||||
model: &Arc<dyn LanguageModel>,
|
||||
user_prompt: String,
|
||||
cx: &mut App,
|
||||
) -> Result<Task<LanguageModelRequest>> {
|
||||
@@ -443,8 +441,6 @@ impl CodegenAlternative {
|
||||
}
|
||||
});
|
||||
|
||||
let temperature = AssistantSettings::temperature_for_model(&model, cx);
|
||||
|
||||
Ok(cx.spawn(async move |_cx| {
|
||||
let mut request_message = LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
@@ -467,7 +463,7 @@ impl CodegenAlternative {
|
||||
mode: None,
|
||||
tools: Vec::new(),
|
||||
stop: Vec::new(),
|
||||
temperature,
|
||||
temperature: None,
|
||||
messages: vec![request_message],
|
||||
}
|
||||
}))
|
||||
|
||||
@@ -17,6 +17,7 @@ use editor::{
|
||||
ToDisplayPoint,
|
||||
},
|
||||
};
|
||||
use feature_flags::{Assistant2FeatureFlag, FeatureFlagViewExt as _};
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
App, Context, Entity, Focusable, Global, HighlightStyle, Subscription, Task, UpdateGlobal,
|
||||
@@ -65,6 +66,15 @@ pub fn init(
|
||||
InlineAssistant::update_global(cx, |inline_assistant, cx| {
|
||||
inline_assistant.register_workspace(&workspace, window, cx)
|
||||
});
|
||||
|
||||
cx.observe_flag::<Assistant2FeatureFlag, _>(window, {
|
||||
|is_assistant2_enabled, _workspace, _window, cx| {
|
||||
InlineAssistant::update_global(cx, |inline_assistant, _cx| {
|
||||
inline_assistant.is_assistant2_enabled = is_assistant2_enabled;
|
||||
});
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
@@ -87,6 +97,7 @@ pub struct InlineAssistant {
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
telemetry: Arc<Telemetry>,
|
||||
fs: Arc<dyn Fs>,
|
||||
is_assistant2_enabled: bool,
|
||||
}
|
||||
|
||||
impl Global for InlineAssistant {}
|
||||
@@ -108,6 +119,7 @@ impl InlineAssistant {
|
||||
prompt_builder,
|
||||
telemetry,
|
||||
fs,
|
||||
is_assistant2_enabled: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,7 +188,7 @@ impl InlineAssistant {
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let is_assistant2_enabled = true;
|
||||
let is_assistant2_enabled = self.is_assistant2_enabled;
|
||||
|
||||
if let Some(editor) = item.act_as::<Editor>(cx) {
|
||||
editor.update(cx, |editor, cx| {
|
||||
|
||||
@@ -8,7 +8,6 @@ use crate::ui::{
|
||||
AnimatedLabel, MaxModeTooltip,
|
||||
preview::{AgentPreview, UsageCallout},
|
||||
};
|
||||
use assistant_settings::{AssistantSettings, CompletionMode};
|
||||
use buffer_diff::BufferDiff;
|
||||
use client::UserStore;
|
||||
use collections::{HashMap, HashSet};
|
||||
@@ -17,6 +16,7 @@ use editor::{
|
||||
AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorEvent,
|
||||
EditorMode, EditorStyle, MultiBuffer,
|
||||
};
|
||||
use feature_flags::{FeatureFlagAppExt, NewBillingFeatureFlag};
|
||||
use file_icons::FileIcons;
|
||||
use fs::Fs;
|
||||
use futures::future::Shared;
|
||||
@@ -42,6 +42,7 @@ use ui::{Disclosure, DocumentationSide, KeyBinding, PopoverMenuHandle, Tooltip,
|
||||
use util::{ResultExt as _, maybe};
|
||||
use workspace::dock::DockPosition;
|
||||
use workspace::{CollaboratorId, Workspace};
|
||||
use zed_llm_client::CompletionMode;
|
||||
|
||||
use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider, crease_for_mention};
|
||||
use crate::context_store::ContextStore;
|
||||
@@ -314,10 +315,6 @@ impl MessageEditor {
|
||||
self.editor.read(cx).text(cx).trim().is_empty()
|
||||
}
|
||||
|
||||
pub fn is_editor_fully_empty(&self, cx: &App) -> bool {
|
||||
self.editor.read(cx).is_empty(cx)
|
||||
}
|
||||
|
||||
fn send_to_model(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(ConfiguredModel { model, provider }) = self
|
||||
.thread
|
||||
@@ -463,6 +460,10 @@ impl MessageEditor {
|
||||
}
|
||||
|
||||
fn render_max_mode_toggle(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
|
||||
if !cx.has_flag::<NewBillingFeatureFlag>() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let thread = self.thread.read(cx);
|
||||
let model = thread.configured_model();
|
||||
if !model?.model.supports_max_mode() {
|
||||
@@ -637,7 +638,7 @@ impl MessageEditor {
|
||||
this.h(vh(0.8, window)).justify_between()
|
||||
})
|
||||
.child(
|
||||
v_flex()
|
||||
div()
|
||||
.min_h_16()
|
||||
.when(is_editor_expanded, |this| this.h_full())
|
||||
.child({
|
||||
@@ -1069,6 +1070,10 @@ impl MessageEditor {
|
||||
}
|
||||
|
||||
fn render_usage_callout(&self, line_height: Pixels, cx: &mut Context<Self>) -> Option<Div> {
|
||||
if !cx.has_flag::<NewBillingFeatureFlag>() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let is_using_zed_provider = self
|
||||
.thread
|
||||
.read(cx)
|
||||
@@ -1130,6 +1135,10 @@ impl MessageEditor {
|
||||
token_usage_ratio: TokenUsageRatio,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<Div> {
|
||||
if !cx.has_flag::<NewBillingFeatureFlag>() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let title = if token_usage_ratio == TokenUsageRatio::Exceeded {
|
||||
"Thread reached the token limit"
|
||||
} else {
|
||||
@@ -1260,7 +1269,7 @@ impl MessageEditor {
|
||||
messages: vec![request_message],
|
||||
tools: vec![],
|
||||
stop: vec![],
|
||||
temperature: AssistantSettings::temperature_for_model(&model.model, cx),
|
||||
temperature: None,
|
||||
};
|
||||
|
||||
Some(model.model.count_tokens(request, cx))
|
||||
|
||||
@@ -6,7 +6,6 @@ use crate::inline_prompt_editor::{
|
||||
use crate::terminal_codegen::{CLEAR_INPUT, CodegenEvent, TerminalCodegen};
|
||||
use crate::thread_store::{TextThreadStore, ThreadStore};
|
||||
use anyhow::{Context as _, Result};
|
||||
use assistant_settings::AssistantSettings;
|
||||
use client::telemetry::Telemetry;
|
||||
use collections::{HashMap, VecDeque};
|
||||
use editor::{MultiBuffer, actions::SelectAll};
|
||||
@@ -267,12 +266,6 @@ impl TerminalInlineAssistant {
|
||||
load_context(contexts, project, &assist.prompt_store, cx)
|
||||
})?;
|
||||
|
||||
let ConfiguredModel { model, .. } = LanguageModelRegistry::read_global(cx)
|
||||
.inline_assistant_model()
|
||||
.context("No inline assistant model")?;
|
||||
|
||||
let temperature = AssistantSettings::temperature_for_model(&model, cx);
|
||||
|
||||
Ok(cx.background_spawn(async move {
|
||||
let mut request_message = LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
@@ -294,7 +287,7 @@ impl TerminalInlineAssistant {
|
||||
messages: vec![request_message],
|
||||
tools: Vec::new(),
|
||||
stop: Vec::new(),
|
||||
temperature,
|
||||
temperature: None,
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_settings::{AssistantSettings, CompletionMode};
|
||||
use assistant_settings::AssistantSettings;
|
||||
use assistant_tool::{ActionLog, AnyToolCard, Tool, ToolWorkingSet};
|
||||
use chrono::{DateTime, Utc};
|
||||
use collections::HashMap;
|
||||
@@ -37,7 +37,7 @@ use settings::Settings;
|
||||
use thiserror::Error;
|
||||
use util::{ResultExt as _, TryFutureExt as _, post_inc};
|
||||
use uuid::Uuid;
|
||||
use zed_llm_client::CompletionRequestStatus;
|
||||
use zed_llm_client::{CompletionMode, CompletionRequestStatus};
|
||||
|
||||
use crate::ThreadStore;
|
||||
use crate::context::{AgentContext, AgentContextHandle, ContextLoadResult, LoadedContext};
|
||||
@@ -267,7 +267,7 @@ impl DetailedSummaryState {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
#[derive(Default)]
|
||||
pub struct TotalTokenUsage {
|
||||
pub total: usize,
|
||||
pub max: usize,
|
||||
@@ -312,6 +312,14 @@ pub enum TokenUsageRatio {
|
||||
Exceeded,
|
||||
}
|
||||
|
||||
fn default_completion_mode(cx: &App) -> CompletionMode {
|
||||
if cx.is_staff() {
|
||||
CompletionMode::Max
|
||||
} else {
|
||||
CompletionMode::Normal
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum QueueState {
|
||||
Sending,
|
||||
@@ -328,7 +336,7 @@ pub struct Thread {
|
||||
detailed_summary_task: Task<Option<()>>,
|
||||
detailed_summary_tx: postage::watch::Sender<DetailedSummaryState>,
|
||||
detailed_summary_rx: postage::watch::Receiver<DetailedSummaryState>,
|
||||
completion_mode: assistant_settings::CompletionMode,
|
||||
completion_mode: CompletionMode,
|
||||
messages: Vec<Message>,
|
||||
next_message_id: MessageId,
|
||||
last_prompt_id: PromptId,
|
||||
@@ -387,7 +395,7 @@ impl Thread {
|
||||
detailed_summary_task: Task::ready(None),
|
||||
detailed_summary_tx,
|
||||
detailed_summary_rx,
|
||||
completion_mode: AssistantSettings::get_global(cx).preferred_completion_mode,
|
||||
completion_mode: default_completion_mode(cx),
|
||||
messages: Vec::new(),
|
||||
next_message_id: MessageId(0),
|
||||
last_prompt_id: PromptId::new(),
|
||||
@@ -456,10 +464,6 @@ impl Thread {
|
||||
.or_else(|| registry.default_model())
|
||||
});
|
||||
|
||||
let completion_mode = serialized
|
||||
.completion_mode
|
||||
.unwrap_or_else(|| AssistantSettings::get_global(cx).preferred_completion_mode);
|
||||
|
||||
Self {
|
||||
id,
|
||||
updated_at: serialized.updated_at,
|
||||
@@ -468,7 +472,7 @@ impl Thread {
|
||||
detailed_summary_task: Task::ready(None),
|
||||
detailed_summary_tx,
|
||||
detailed_summary_rx,
|
||||
completion_mode,
|
||||
completion_mode: default_completion_mode(cx),
|
||||
messages: serialized
|
||||
.messages
|
||||
.into_iter()
|
||||
@@ -1091,7 +1095,6 @@ impl Thread {
|
||||
provider: model.provider.id().0.to_string(),
|
||||
model: model.model.id().0.to_string(),
|
||||
}),
|
||||
completion_mode: Some(this.completion_mode),
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -1145,7 +1148,7 @@ impl Thread {
|
||||
messages: vec![],
|
||||
tools: Vec::new(),
|
||||
stop: Vec::new(),
|
||||
temperature: AssistantSettings::temperature_for_model(&model, cx),
|
||||
temperature: None,
|
||||
};
|
||||
|
||||
let available_tools = self.available_tools(cx, model.clone());
|
||||
@@ -1243,20 +1246,15 @@ impl Thread {
|
||||
|
||||
request.tools = available_tools;
|
||||
request.mode = if model.supports_max_mode() {
|
||||
Some(self.completion_mode.into())
|
||||
Some(self.completion_mode)
|
||||
} else {
|
||||
Some(CompletionMode::Normal.into())
|
||||
Some(CompletionMode::Normal)
|
||||
};
|
||||
|
||||
request
|
||||
}
|
||||
|
||||
fn to_summarize_request(
|
||||
&self,
|
||||
model: &Arc<dyn LanguageModel>,
|
||||
added_user_message: String,
|
||||
cx: &App,
|
||||
) -> LanguageModelRequest {
|
||||
fn to_summarize_request(&self, added_user_message: String) -> LanguageModelRequest {
|
||||
let mut request = LanguageModelRequest {
|
||||
thread_id: None,
|
||||
prompt_id: None,
|
||||
@@ -1264,7 +1262,7 @@ impl Thread {
|
||||
messages: vec![],
|
||||
tools: Vec::new(),
|
||||
stop: Vec::new(),
|
||||
temperature: AssistantSettings::temperature_for_model(model, cx),
|
||||
temperature: None,
|
||||
};
|
||||
|
||||
for message in &self.messages {
|
||||
@@ -1701,7 +1699,7 @@ impl Thread {
|
||||
If the conversation is about a specific subject, include it in the title. \
|
||||
Be descriptive. DO NOT speak in the first person.";
|
||||
|
||||
let request = self.to_summarize_request(&model.model, added_user_message.into(), cx);
|
||||
let request = self.to_summarize_request(added_user_message.into());
|
||||
|
||||
self.pending_summary = cx.spawn(async move |this, cx| {
|
||||
async move {
|
||||
@@ -1787,7 +1785,7 @@ impl Thread {
|
||||
4. Any action items or next steps if any\n\
|
||||
Format it in Markdown with headings and bullet points.";
|
||||
|
||||
let request = self.to_summarize_request(&model, added_user_message.into(), cx);
|
||||
let request = self.to_summarize_request(added_user_message.into());
|
||||
|
||||
*self.detailed_summary_tx.borrow_mut() = DetailedSummaryState::Generating {
|
||||
message_id: last_message_id,
|
||||
@@ -2309,7 +2307,7 @@ impl Thread {
|
||||
.map(|repo| {
|
||||
repo.update(cx, |repo, _| {
|
||||
let current_branch =
|
||||
repo.branch.as_ref().map(|branch| branch.name.to_string());
|
||||
repo.branch.as_ref().map(|branch| branch.name().to_owned());
|
||||
repo.send_job(None, |state, _| async move {
|
||||
let RepositoryState::Local { backend, .. } = state else {
|
||||
return GitState {
|
||||
@@ -2660,7 +2658,7 @@ struct PendingCompletion {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{ThreadStore, context::load_context, context_store::ContextStore, thread_store};
|
||||
use assistant_settings::{AssistantSettings, LanguageModelParameters};
|
||||
use assistant_settings::AssistantSettings;
|
||||
use assistant_tool::ToolRegistry;
|
||||
use editor::EditorSettings;
|
||||
use gpui::TestAppContext;
|
||||
@@ -3071,100 +3069,6 @@ fn main() {{
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_temperature_setting(cx: &mut TestAppContext) {
|
||||
init_test_settings(cx);
|
||||
|
||||
let project = create_test_project(
|
||||
cx,
|
||||
json!({"code.rs": "fn main() {\n println!(\"Hello, world!\");\n}"}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let (_workspace, _thread_store, thread, _context_store, model) =
|
||||
setup_test_environment(cx, project.clone()).await;
|
||||
|
||||
// Both model and provider
|
||||
cx.update(|cx| {
|
||||
AssistantSettings::override_global(
|
||||
AssistantSettings {
|
||||
model_parameters: vec![LanguageModelParameters {
|
||||
provider: Some(model.provider_id().0.to_string().into()),
|
||||
model: Some(model.id().0.clone()),
|
||||
temperature: Some(0.66),
|
||||
}],
|
||||
..AssistantSettings::get_global(cx).clone()
|
||||
},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
let request = thread.update(cx, |thread, cx| {
|
||||
thread.to_completion_request(model.clone(), cx)
|
||||
});
|
||||
assert_eq!(request.temperature, Some(0.66));
|
||||
|
||||
// Only model
|
||||
cx.update(|cx| {
|
||||
AssistantSettings::override_global(
|
||||
AssistantSettings {
|
||||
model_parameters: vec![LanguageModelParameters {
|
||||
provider: None,
|
||||
model: Some(model.id().0.clone()),
|
||||
temperature: Some(0.66),
|
||||
}],
|
||||
..AssistantSettings::get_global(cx).clone()
|
||||
},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
let request = thread.update(cx, |thread, cx| {
|
||||
thread.to_completion_request(model.clone(), cx)
|
||||
});
|
||||
assert_eq!(request.temperature, Some(0.66));
|
||||
|
||||
// Only provider
|
||||
cx.update(|cx| {
|
||||
AssistantSettings::override_global(
|
||||
AssistantSettings {
|
||||
model_parameters: vec![LanguageModelParameters {
|
||||
provider: Some(model.provider_id().0.to_string().into()),
|
||||
model: None,
|
||||
temperature: Some(0.66),
|
||||
}],
|
||||
..AssistantSettings::get_global(cx).clone()
|
||||
},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
let request = thread.update(cx, |thread, cx| {
|
||||
thread.to_completion_request(model.clone(), cx)
|
||||
});
|
||||
assert_eq!(request.temperature, Some(0.66));
|
||||
|
||||
// Same model name, different provider
|
||||
cx.update(|cx| {
|
||||
AssistantSettings::override_global(
|
||||
AssistantSettings {
|
||||
model_parameters: vec![LanguageModelParameters {
|
||||
provider: Some("anthropic".into()),
|
||||
model: Some(model.id().0.clone()),
|
||||
temperature: Some(0.66),
|
||||
}],
|
||||
..AssistantSettings::get_global(cx).clone()
|
||||
},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
let request = thread.update(cx, |thread, cx| {
|
||||
thread.to_completion_request(model.clone(), cx)
|
||||
});
|
||||
assert_eq!(request.temperature, None);
|
||||
}
|
||||
|
||||
fn init_test_settings(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_settings::{AgentProfile, AgentProfileId, AssistantSettings, CompletionMode};
|
||||
use assistant_settings::{AgentProfile, AgentProfileId, AssistantSettings};
|
||||
use assistant_tool::{ToolId, ToolSource, ToolWorkingSet};
|
||||
use chrono::{DateTime, Utc};
|
||||
use collections::HashMap;
|
||||
@@ -651,8 +651,6 @@ pub struct SerializedThread {
|
||||
pub exceeded_window_error: Option<ExceededWindowError>,
|
||||
#[serde(default)]
|
||||
pub model: Option<SerializedLanguageModel>,
|
||||
#[serde(default)]
|
||||
pub completion_mode: Option<CompletionMode>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
@@ -796,7 +794,6 @@ impl LegacySerializedThread {
|
||||
detailed_summary_state: DetailedSummaryState::default(),
|
||||
exceeded_window_error: None,
|
||||
model: None,
|
||||
completion_mode: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ mod agent_notification;
|
||||
mod animated_label;
|
||||
mod context_pill;
|
||||
mod max_mode_tooltip;
|
||||
mod onboarding_modal;
|
||||
pub mod preview;
|
||||
mod upsell;
|
||||
|
||||
@@ -10,4 +9,3 @@ pub use agent_notification::*;
|
||||
pub use animated_label::*;
|
||||
pub use context_pill::*;
|
||||
pub use max_mode_tooltip::*;
|
||||
pub use onboarding_modal::*;
|
||||
|
||||
@@ -1,157 +0,0 @@
|
||||
use gpui::{
|
||||
ClickEvent, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, MouseDownEvent, Render,
|
||||
};
|
||||
use ui::{TintColor, Vector, VectorName, prelude::*};
|
||||
use workspace::{ModalView, Workspace};
|
||||
|
||||
use crate::assistant_panel::AssistantPanel;
|
||||
|
||||
macro_rules! agent_onboarding_event {
|
||||
($name:expr) => {
|
||||
telemetry::event!($name, source = "Agent Onboarding");
|
||||
};
|
||||
($name:expr, $($key:ident $(= $value:expr)?),+ $(,)?) => {
|
||||
telemetry::event!($name, source = "Agent Onboarding", $($key $(= $value)?),+);
|
||||
};
|
||||
}
|
||||
|
||||
pub struct AgentOnboardingModal {
|
||||
focus_handle: FocusHandle,
|
||||
workspace: Entity<Workspace>,
|
||||
}
|
||||
|
||||
impl AgentOnboardingModal {
|
||||
pub fn toggle(workspace: &mut Workspace, window: &mut Window, cx: &mut Context<Workspace>) {
|
||||
let workspace_entity = cx.entity();
|
||||
workspace.toggle_modal(window, cx, |_window, cx| Self {
|
||||
workspace: workspace_entity,
|
||||
focus_handle: cx.focus_handle(),
|
||||
});
|
||||
}
|
||||
|
||||
fn open_panel(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.workspace.update(cx, |workspace, cx| {
|
||||
workspace.focus_panel::<AssistantPanel>(window, cx);
|
||||
});
|
||||
|
||||
cx.emit(DismissEvent);
|
||||
|
||||
agent_onboarding_event!("Open Panel Clicked");
|
||||
}
|
||||
|
||||
fn view_blog(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context<Self>) {
|
||||
cx.open_url("http://zed.dev/blog/fastest-ai-code-editor");
|
||||
cx.notify();
|
||||
|
||||
agent_onboarding_event!("Blog Link Clicked");
|
||||
}
|
||||
|
||||
fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for AgentOnboardingModal {}
|
||||
|
||||
impl Focusable for AgentOnboardingModal {
|
||||
fn focus_handle(&self, _cx: &App) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl ModalView for AgentOnboardingModal {}
|
||||
|
||||
impl Render for AgentOnboardingModal {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let window_height = window.viewport_size().height;
|
||||
let max_height = window_height - px(200.);
|
||||
|
||||
let base = v_flex()
|
||||
.id("agent-onboarding")
|
||||
.key_context("AgentOnboardingModal")
|
||||
.relative()
|
||||
.w(px(450.))
|
||||
.h_full()
|
||||
.max_h(max_height)
|
||||
.p_4()
|
||||
.gap_2()
|
||||
.elevation_3(cx)
|
||||
.track_focus(&self.focus_handle(cx))
|
||||
.overflow_hidden()
|
||||
.on_action(cx.listener(Self::cancel))
|
||||
.on_action(cx.listener(|_, _: &menu::Cancel, _window, cx| {
|
||||
agent_onboarding_event!("Canceled", trigger = "Action");
|
||||
cx.emit(DismissEvent);
|
||||
}))
|
||||
.on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, _cx| {
|
||||
this.focus_handle.focus(window);
|
||||
}))
|
||||
.child(
|
||||
div()
|
||||
.absolute()
|
||||
.top_0()
|
||||
.right(px(-1.0))
|
||||
.w(px(441.))
|
||||
.h(px(167.))
|
||||
.child(
|
||||
Vector::new(VectorName::Grid, rems_from_px(441.), rems_from_px(167.))
|
||||
.color(ui::Color::Custom(cx.theme().colors().text.alpha(0.1))),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.absolute()
|
||||
.top(px(-8.0))
|
||||
.right_0()
|
||||
.w(px(400.))
|
||||
.h(px(92.))
|
||||
.child(
|
||||
Vector::new(VectorName::AiGrid, rems_from_px(400.), rems_from_px(92.))
|
||||
.color(ui::Color::Custom(cx.theme().colors().text.alpha(0.32))),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.w_full()
|
||||
.gap_1()
|
||||
.child(
|
||||
Label::new("Introducing")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(Headline::new("Agentic Editing in Zed").size(HeadlineSize::Large)),
|
||||
)
|
||||
.child(h_flex().absolute().top_2().right_2().child(
|
||||
IconButton::new("cancel", IconName::X).on_click(cx.listener(
|
||||
|_, _: &ClickEvent, _window, cx| {
|
||||
agent_onboarding_event!("Cancelled", trigger = "X click");
|
||||
cx.emit(DismissEvent);
|
||||
},
|
||||
)),
|
||||
));
|
||||
|
||||
let open_panel_button = Button::new("open-panel", "Get Started with the Agent Panel")
|
||||
.icon_size(IconSize::Indicator)
|
||||
.style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
.full_width()
|
||||
.on_click(cx.listener(Self::open_panel));
|
||||
|
||||
let blog_post_button = Button::new("view-blog", "Check out the Blog Post")
|
||||
.icon(IconName::ArrowUpRight)
|
||||
.icon_size(IconSize::Indicator)
|
||||
.icon_color(Color::Muted)
|
||||
.full_width()
|
||||
.on_click(cx.listener(Self::view_blog));
|
||||
|
||||
let copy = "Zed now natively supports agentic editing, enabling fluid collaboration between humans and AI.";
|
||||
|
||||
base.child(Label::new(copy).color(Color::Muted)).child(
|
||||
v_flex()
|
||||
.w_full()
|
||||
.mt_2()
|
||||
.gap_2()
|
||||
.child(open_panel_button)
|
||||
.child(blog_post_button),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -163,7 +163,7 @@ fn update_active_language_model_from_settings(cx: &mut App) {
|
||||
|
||||
fn to_selected_model(selection: &LanguageModelSelection) -> language_model::SelectedModel {
|
||||
language_model::SelectedModel {
|
||||
provider: LanguageModelProviderId::from(selection.provider.0.clone()),
|
||||
provider: LanguageModelProviderId::from(selection.provider.clone()),
|
||||
model: LanguageModelId::from(selection.model.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,9 @@ use editor::{
|
||||
ToDisplayPoint,
|
||||
},
|
||||
};
|
||||
use feature_flags::{FeatureFlagAppExt as _, ZedProFeatureFlag};
|
||||
use feature_flags::{
|
||||
Assistant2FeatureFlag, FeatureFlagAppExt as _, FeatureFlagViewExt as _, ZedProFeatureFlag,
|
||||
};
|
||||
use fs::Fs;
|
||||
use futures::{
|
||||
SinkExt, Stream, StreamExt, TryStreamExt as _,
|
||||
@@ -72,19 +74,25 @@ pub fn init(
|
||||
cx: &mut App,
|
||||
) {
|
||||
cx.set_global(InlineAssistant::new(fs, prompt_builder, telemetry));
|
||||
// Don't register now that the Agent is released.
|
||||
if false {
|
||||
cx.observe_new(|_, window, cx| {
|
||||
let Some(window) = window else {
|
||||
return;
|
||||
};
|
||||
let workspace = cx.entity().clone();
|
||||
InlineAssistant::update_global(cx, |inline_assistant, cx| {
|
||||
inline_assistant.register_workspace(&workspace, window, cx)
|
||||
});
|
||||
cx.observe_new(|_, window, cx| {
|
||||
let Some(window) = window else {
|
||||
return;
|
||||
};
|
||||
let workspace = cx.entity().clone();
|
||||
InlineAssistant::update_global(cx, |inline_assistant, cx| {
|
||||
inline_assistant.register_workspace(&workspace, window, cx)
|
||||
});
|
||||
|
||||
cx.observe_flag::<Assistant2FeatureFlag, _>(window, {
|
||||
|is_assistant2_enabled, _workspace, _window, cx| {
|
||||
InlineAssistant::update_global(cx, |inline_assistant, _cx| {
|
||||
inline_assistant.is_assistant2_enabled = is_assistant2_enabled;
|
||||
});
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
const PROMPT_HISTORY_MAX_LEN: usize = 20;
|
||||
@@ -100,6 +108,7 @@ pub struct InlineAssistant {
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
telemetry: Arc<Telemetry>,
|
||||
fs: Arc<dyn Fs>,
|
||||
is_assistant2_enabled: bool,
|
||||
}
|
||||
|
||||
impl Global for InlineAssistant {}
|
||||
@@ -121,6 +130,7 @@ impl InlineAssistant {
|
||||
prompt_builder,
|
||||
telemetry,
|
||||
fs,
|
||||
is_assistant2_enabled: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,7 +199,7 @@ impl InlineAssistant {
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let is_assistant2_enabled = true;
|
||||
let is_assistant2_enabled = self.is_assistant2_enabled;
|
||||
|
||||
if let Some(editor) = item.act_as::<Editor>(cx) {
|
||||
editor.update(cx, |editor, cx| {
|
||||
@@ -2474,7 +2484,7 @@ impl InlineAssist {
|
||||
.read(cx)
|
||||
.active_context(cx)?
|
||||
.read(cx)
|
||||
.to_completion_request(None, RequestType::Chat, cx),
|
||||
.to_completion_request(RequestType::Chat, cx),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
@@ -2860,8 +2870,7 @@ impl CodegenAlternative {
|
||||
if let Some(ConfiguredModel { model, .. }) =
|
||||
LanguageModelRegistry::read_global(cx).inline_assistant_model()
|
||||
{
|
||||
let request =
|
||||
self.build_request(&model, user_prompt, assistant_panel_context.clone(), cx);
|
||||
let request = self.build_request(user_prompt, assistant_panel_context.clone(), cx);
|
||||
match request {
|
||||
Ok(request) => {
|
||||
let total_count = model.count_tokens(request.clone(), cx);
|
||||
@@ -2906,8 +2915,7 @@ impl CodegenAlternative {
|
||||
if user_prompt.trim().to_lowercase() == "delete" {
|
||||
async { Ok(LanguageModelTextStream::default()) }.boxed_local()
|
||||
} else {
|
||||
let request =
|
||||
self.build_request(&model, user_prompt, assistant_panel_context, cx)?;
|
||||
let request = self.build_request(user_prompt, assistant_panel_context, cx)?;
|
||||
self.request = Some(request.clone());
|
||||
|
||||
cx.spawn(async move |_, cx| model.stream_completion_text(request, &cx).await)
|
||||
@@ -2919,7 +2927,6 @@ impl CodegenAlternative {
|
||||
|
||||
fn build_request(
|
||||
&self,
|
||||
model: &Arc<dyn LanguageModel>,
|
||||
user_prompt: String,
|
||||
assistant_panel_context: Option<LanguageModelRequest>,
|
||||
cx: &App,
|
||||
@@ -2974,7 +2981,7 @@ impl CodegenAlternative {
|
||||
messages,
|
||||
tools: Vec::new(),
|
||||
stop: Vec::new(),
|
||||
temperature: AssistantSettings::temperature_for_model(&model, cx),
|
||||
temperature: None,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -261,7 +261,7 @@ impl TerminalInlineAssistant {
|
||||
.read(cx)
|
||||
.active_context(cx)?
|
||||
.read(cx)
|
||||
.to_completion_request(None, RequestType::Chat, cx),
|
||||
.to_completion_request(RequestType::Chat, cx),
|
||||
)
|
||||
})
|
||||
} else {
|
||||
|
||||
@@ -22,6 +22,7 @@ clock.workspace = true
|
||||
collections.workspace = true
|
||||
context_server.workspace = true
|
||||
editor.workspace = true
|
||||
feature_flags.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
fuzzy.workspace = true
|
||||
|
||||
@@ -3,7 +3,6 @@ mod context_tests;
|
||||
|
||||
use crate::patch::{AssistantEdit, AssistantPatch, AssistantPatchStatus};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_settings::AssistantSettings;
|
||||
use assistant_slash_command::{
|
||||
SlashCommandContent, SlashCommandEvent, SlashCommandLine, SlashCommandOutputSection,
|
||||
SlashCommandResult, SlashCommandWorkingSet,
|
||||
@@ -1274,10 +1273,10 @@ impl AssistantContext {
|
||||
pub(crate) fn count_remaining_tokens(&mut self, cx: &mut Context<Self>) {
|
||||
// Assume it will be a Chat request, even though that takes fewer tokens (and risks going over the limit),
|
||||
// because otherwise you see in the UI that your empty message has a bunch of tokens already used.
|
||||
let request = self.to_completion_request(RequestType::Chat, cx);
|
||||
let Some(model) = LanguageModelRegistry::read_global(cx).default_model() else {
|
||||
return;
|
||||
};
|
||||
let request = self.to_completion_request(Some(&model.model), RequestType::Chat, cx);
|
||||
let debounce = self.token_count.is_some();
|
||||
self.pending_token_count = cx.spawn(async move |this, cx| {
|
||||
async move {
|
||||
@@ -1423,7 +1422,7 @@ impl AssistantContext {
|
||||
}
|
||||
|
||||
let request = {
|
||||
let mut req = self.to_completion_request(Some(&model), RequestType::Chat, cx);
|
||||
let mut req = self.to_completion_request(RequestType::Chat, cx);
|
||||
// Skip the last message because it's likely to change and
|
||||
// therefore would be a waste to cache.
|
||||
req.messages.pop();
|
||||
@@ -2322,7 +2321,7 @@ impl AssistantContext {
|
||||
// Compute which messages to cache, including the last one.
|
||||
self.mark_cache_anchors(&model.cache_configuration(), false, cx);
|
||||
|
||||
let request = self.to_completion_request(Some(&model), request_type, cx);
|
||||
let request = self.to_completion_request(request_type, cx);
|
||||
|
||||
let assistant_message = self
|
||||
.insert_message_after(last_message_id, Role::Assistant, MessageStatus::Pending, cx)
|
||||
@@ -2562,7 +2561,6 @@ impl AssistantContext {
|
||||
|
||||
pub fn to_completion_request(
|
||||
&self,
|
||||
model: Option<&Arc<dyn LanguageModel>>,
|
||||
request_type: RequestType,
|
||||
cx: &App,
|
||||
) -> LanguageModelRequest {
|
||||
@@ -2586,8 +2584,7 @@ impl AssistantContext {
|
||||
messages: Vec::new(),
|
||||
tools: Vec::new(),
|
||||
stop: Vec::new(),
|
||||
temperature: model
|
||||
.and_then(|model| AssistantSettings::temperature_for_model(model, cx)),
|
||||
temperature: None,
|
||||
};
|
||||
for message in self.messages(cx) {
|
||||
if message.status != MessageStatus::Done {
|
||||
@@ -2984,7 +2981,7 @@ impl AssistantContext {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut request = self.to_completion_request(Some(&model.model), RequestType::Chat, cx);
|
||||
let mut request = self.to_completion_request(RequestType::Chat, cx);
|
||||
request.messages.push(LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
content: vec![
|
||||
|
||||
@@ -43,8 +43,9 @@ use workspace::Workspace;
|
||||
|
||||
#[gpui::test]
|
||||
fn test_inserting_and_removing_messages(cx: &mut App) {
|
||||
init_test(cx);
|
||||
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
LanguageModelRegistry::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
|
||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||
let context = cx.new(|cx| {
|
||||
@@ -181,8 +182,9 @@ fn test_inserting_and_removing_messages(cx: &mut App) {
|
||||
|
||||
#[gpui::test]
|
||||
fn test_message_splitting(cx: &mut App) {
|
||||
init_test(cx);
|
||||
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
LanguageModelRegistry::test(cx);
|
||||
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
|
||||
|
||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||
@@ -283,8 +285,9 @@ fn test_message_splitting(cx: &mut App) {
|
||||
|
||||
#[gpui::test]
|
||||
fn test_messages_for_offsets(cx: &mut App) {
|
||||
init_test(cx);
|
||||
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
LanguageModelRegistry::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
|
||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||
let context = cx.new(|cx| {
|
||||
@@ -375,8 +378,10 @@ fn test_messages_for_offsets(cx: &mut App) {
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_slash_commands(cx: &mut TestAppContext) {
|
||||
cx.update(init_test);
|
||||
|
||||
let settings_store = cx.update(SettingsStore::test);
|
||||
cx.set_global(settings_store);
|
||||
cx.update(LanguageModelRegistry::test);
|
||||
cx.update(Project::init_settings);
|
||||
let fs = FakeFs::new(cx.background_executor.clone());
|
||||
|
||||
fs.insert_tree(
|
||||
@@ -666,19 +671,22 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
|
||||
cx.update(prompt_store::init);
|
||||
let mut settings_store = cx.update(SettingsStore::test);
|
||||
cx.update(|cx| {
|
||||
init_test(cx);
|
||||
cx.update_global(|settings_store: &mut SettingsStore, cx| {
|
||||
settings_store
|
||||
.set_user_settings(
|
||||
r#"{ "assistant": { "enable_experimental_live_diffs": true } }"#,
|
||||
cx,
|
||||
)
|
||||
.unwrap()
|
||||
})
|
||||
settings_store
|
||||
.set_user_settings(
|
||||
r#"{ "assistant": { "enable_experimental_live_diffs": true } }"#,
|
||||
cx,
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
cx.set_global(settings_store);
|
||||
cx.update(language::init);
|
||||
cx.update(Project::init_settings);
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
let project = Project::test(fs, [Path::new("/root")], cx).await;
|
||||
cx.update(LanguageModelRegistry::test);
|
||||
|
||||
let registry = Arc::new(LanguageRegistry::test(cx.executor()));
|
||||
|
||||
@@ -1061,8 +1069,9 @@ async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_serialization(cx: &mut TestAppContext) {
|
||||
cx.update(init_test);
|
||||
|
||||
let settings_store = cx.update(SettingsStore::test);
|
||||
cx.set_global(settings_store);
|
||||
cx.update(LanguageModelRegistry::test);
|
||||
let registry = Arc::new(LanguageRegistry::test(cx.executor()));
|
||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||
let context = cx.new(|cx| {
|
||||
@@ -1138,8 +1147,6 @@ async fn test_serialization(cx: &mut TestAppContext) {
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: StdRng) {
|
||||
cx.update(init_test);
|
||||
|
||||
let min_peers = env::var("MIN_PEERS")
|
||||
.map(|i| i.parse().expect("invalid `MIN_PEERS` variable"))
|
||||
.unwrap_or(2);
|
||||
@@ -1150,6 +1157,10 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
|
||||
.map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
|
||||
.unwrap_or(50);
|
||||
|
||||
let settings_store = cx.update(SettingsStore::test);
|
||||
cx.set_global(settings_store);
|
||||
cx.update(LanguageModelRegistry::test);
|
||||
|
||||
let slash_commands = cx.update(SlashCommandRegistry::default_global);
|
||||
slash_commands.register_command(FakeSlashCommand("cmd-1".into()), false);
|
||||
slash_commands.register_command(FakeSlashCommand("cmd-2".into()), false);
|
||||
@@ -1418,8 +1429,9 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
|
||||
|
||||
#[gpui::test]
|
||||
fn test_mark_cache_anchors(cx: &mut App) {
|
||||
init_test(cx);
|
||||
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
LanguageModelRegistry::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
|
||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||
let context = cx.new(|cx| {
|
||||
@@ -1594,16 +1606,6 @@ fn messages_cache(
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut App) {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
prompt_store::init(cx);
|
||||
LanguageModelRegistry::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
language::init(cx);
|
||||
assistant_settings::init(cx);
|
||||
Project::init_settings(cx);
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct FakeSlashCommand(String);
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ use editor::{
|
||||
scroll::Autoscroll,
|
||||
};
|
||||
use editor::{FoldPlaceholder, display_map::CreaseId};
|
||||
use feature_flags::{Assistant2FeatureFlag, FeatureFlagAppExt as _};
|
||||
use fs::Fs;
|
||||
use futures::FutureExt;
|
||||
use gpui::{
|
||||
@@ -2394,11 +2395,19 @@ impl ContextEditor {
|
||||
.on_click({
|
||||
let focus_handle = self.focus_handle(cx).clone();
|
||||
move |_event, window, cx| {
|
||||
focus_handle.dispatch_action(
|
||||
&zed_actions::agent::OpenConfiguration,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
if cx.has_flag::<Assistant2FeatureFlag>() {
|
||||
focus_handle.dispatch_action(
|
||||
&zed_actions::agent::OpenConfiguration,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
} else {
|
||||
focus_handle.dispatch_action(
|
||||
&zed_actions::assistant::ShowConfiguration,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
};
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -14,7 +14,7 @@ path = "src/assistant_settings.rs"
|
||||
[dependencies]
|
||||
anthropic = { workspace = true, features = ["schemars"] }
|
||||
anyhow.workspace = true
|
||||
collections.workspace = true
|
||||
feature_flags.workspace = true
|
||||
gpui.workspace = true
|
||||
indexmap.workspace = true
|
||||
language_model.workspace = true
|
||||
@@ -27,7 +27,6 @@ schemars.workspace = true
|
||||
serde.workspace = true
|
||||
settings.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
zed_llm_client.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
fs.workspace = true
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use collections::IndexMap;
|
||||
use gpui::SharedString;
|
||||
use indexmap::IndexMap;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
|
||||
@@ -5,9 +5,10 @@ use std::sync::Arc;
|
||||
use ::open_ai::Model as OpenAiModel;
|
||||
use anthropic::Model as AnthropicModel;
|
||||
use anyhow::{Result, bail};
|
||||
use collections::IndexMap;
|
||||
use deepseek::Model as DeepseekModel;
|
||||
use gpui::{App, Pixels, SharedString};
|
||||
use feature_flags::{AgentStreamEditsFeatureFlag, Assistant2FeatureFlag, FeatureFlagAppExt};
|
||||
use gpui::{App, Pixels};
|
||||
use indexmap::IndexMap;
|
||||
use language_model::{CloudModel, LanguageModel};
|
||||
use lmstudio::Model as LmStudioModel;
|
||||
use ollama::Model as OllamaModel;
|
||||
@@ -17,10 +18,6 @@ use settings::{Settings, SettingsSources};
|
||||
|
||||
pub use crate::agent_profile::*;
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
AssistantSettings::register(cx);
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AssistantDockPosition {
|
||||
@@ -92,71 +89,31 @@ pub struct AssistantSettings {
|
||||
pub notify_when_agent_waiting: NotifyWhenAgentWaiting,
|
||||
pub stream_edits: bool,
|
||||
pub single_file_review: bool,
|
||||
pub model_parameters: Vec<LanguageModelParameters>,
|
||||
pub preferred_completion_mode: CompletionMode,
|
||||
}
|
||||
|
||||
impl AssistantSettings {
|
||||
pub fn temperature_for_model(model: &Arc<dyn LanguageModel>, cx: &App) -> Option<f32> {
|
||||
let settings = Self::get_global(cx);
|
||||
settings
|
||||
.model_parameters
|
||||
.iter()
|
||||
.rfind(|setting| setting.matches(model))
|
||||
.and_then(|m| m.temperature)
|
||||
pub fn stream_edits(&self, cx: &App) -> bool {
|
||||
cx.has_flag::<AgentStreamEditsFeatureFlag>() || self.stream_edits
|
||||
}
|
||||
|
||||
pub fn stream_edits(&self, _cx: &App) -> bool {
|
||||
// TODO: Remove the `stream_edits` setting.
|
||||
true
|
||||
}
|
||||
pub fn are_live_diffs_enabled(&self, cx: &App) -> bool {
|
||||
if cx.has_flag::<Assistant2FeatureFlag>() {
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn are_live_diffs_enabled(&self, _cx: &App) -> bool {
|
||||
false
|
||||
cx.is_staff() || self.enable_experimental_live_diffs
|
||||
}
|
||||
|
||||
pub fn set_inline_assistant_model(&mut self, provider: String, model: String) {
|
||||
self.inline_assistant_model = Some(LanguageModelSelection {
|
||||
provider: provider.into(),
|
||||
model,
|
||||
});
|
||||
self.inline_assistant_model = Some(LanguageModelSelection { provider, model });
|
||||
}
|
||||
|
||||
pub fn set_commit_message_model(&mut self, provider: String, model: String) {
|
||||
self.commit_message_model = Some(LanguageModelSelection {
|
||||
provider: provider.into(),
|
||||
model,
|
||||
});
|
||||
self.commit_message_model = Some(LanguageModelSelection { provider, model });
|
||||
}
|
||||
|
||||
pub fn set_thread_summary_model(&mut self, provider: String, model: String) {
|
||||
self.thread_summary_model = Some(LanguageModelSelection {
|
||||
provider: provider.into(),
|
||||
model,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
pub struct LanguageModelParameters {
|
||||
pub provider: Option<LanguageModelProviderSetting>,
|
||||
pub model: Option<SharedString>,
|
||||
pub temperature: Option<f32>,
|
||||
}
|
||||
|
||||
impl LanguageModelParameters {
|
||||
pub fn matches(&self, model: &Arc<dyn LanguageModel>) -> bool {
|
||||
if let Some(provider) = &self.provider {
|
||||
if provider.0 != model.provider_id().0 {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if let Some(setting_model) = &self.model {
|
||||
if *setting_model != model.id().0 {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
self.thread_summary_model = Some(LanguageModelSelection { provider, model });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,37 +180,37 @@ impl AssistantSettingsContent {
|
||||
.and_then(|provider| match provider {
|
||||
AssistantProviderContentV1::ZedDotDev { default_model } => {
|
||||
default_model.map(|model| LanguageModelSelection {
|
||||
provider: "zed.dev".into(),
|
||||
provider: "zed.dev".to_string(),
|
||||
model: model.id().to_string(),
|
||||
})
|
||||
}
|
||||
AssistantProviderContentV1::OpenAi { default_model, .. } => {
|
||||
default_model.map(|model| LanguageModelSelection {
|
||||
provider: "openai".into(),
|
||||
provider: "openai".to_string(),
|
||||
model: model.id().to_string(),
|
||||
})
|
||||
}
|
||||
AssistantProviderContentV1::Anthropic { default_model, .. } => {
|
||||
default_model.map(|model| LanguageModelSelection {
|
||||
provider: "anthropic".into(),
|
||||
provider: "anthropic".to_string(),
|
||||
model: model.id().to_string(),
|
||||
})
|
||||
}
|
||||
AssistantProviderContentV1::Ollama { default_model, .. } => {
|
||||
default_model.map(|model| LanguageModelSelection {
|
||||
provider: "ollama".into(),
|
||||
provider: "ollama".to_string(),
|
||||
model: model.id().to_string(),
|
||||
})
|
||||
}
|
||||
AssistantProviderContentV1::LmStudio { default_model, .. } => {
|
||||
default_model.map(|model| LanguageModelSelection {
|
||||
provider: "lmstudio".into(),
|
||||
provider: "lmstudio".to_string(),
|
||||
model: model.id().to_string(),
|
||||
})
|
||||
}
|
||||
AssistantProviderContentV1::DeepSeek { default_model, .. } => {
|
||||
default_model.map(|model| LanguageModelSelection {
|
||||
provider: "deepseek".into(),
|
||||
provider: "deepseek".to_string(),
|
||||
model: model.id().to_string(),
|
||||
})
|
||||
}
|
||||
@@ -269,8 +226,6 @@ impl AssistantSettingsContent {
|
||||
notify_when_agent_waiting: None,
|
||||
stream_edits: None,
|
||||
single_file_review: None,
|
||||
model_parameters: Vec::new(),
|
||||
preferred_completion_mode: None,
|
||||
},
|
||||
VersionedAssistantSettingsContent::V2(ref settings) => settings.clone(),
|
||||
},
|
||||
@@ -281,7 +236,7 @@ impl AssistantSettingsContent {
|
||||
default_width: settings.default_width,
|
||||
default_height: settings.default_height,
|
||||
default_model: Some(LanguageModelSelection {
|
||||
provider: "openai".into(),
|
||||
provider: "openai".to_string(),
|
||||
model: settings
|
||||
.default_open_ai_model
|
||||
.clone()
|
||||
@@ -300,8 +255,6 @@ impl AssistantSettingsContent {
|
||||
notify_when_agent_waiting: None,
|
||||
stream_edits: None,
|
||||
single_file_review: None,
|
||||
model_parameters: Vec::new(),
|
||||
preferred_completion_mode: None,
|
||||
},
|
||||
None => AssistantSettingsContentV2::default(),
|
||||
}
|
||||
@@ -414,10 +367,7 @@ impl AssistantSettingsContent {
|
||||
}
|
||||
}
|
||||
VersionedAssistantSettingsContent::V2(ref mut settings) => {
|
||||
settings.default_model = Some(LanguageModelSelection {
|
||||
provider: provider.into(),
|
||||
model,
|
||||
});
|
||||
settings.default_model = Some(LanguageModelSelection { provider, model });
|
||||
}
|
||||
},
|
||||
Some(AssistantSettingsContentInner::Legacy(settings)) => {
|
||||
@@ -428,10 +378,7 @@ impl AssistantSettingsContent {
|
||||
None => {
|
||||
self.inner = Some(AssistantSettingsContentInner::for_v2(
|
||||
AssistantSettingsContentV2 {
|
||||
default_model: Some(LanguageModelSelection {
|
||||
provider: provider.into(),
|
||||
model,
|
||||
}),
|
||||
default_model: Some(LanguageModelSelection { provider, model }),
|
||||
..Default::default()
|
||||
},
|
||||
));
|
||||
@@ -441,10 +388,7 @@ impl AssistantSettingsContent {
|
||||
|
||||
pub fn set_inline_assistant_model(&mut self, provider: String, model: String) {
|
||||
self.v2_setting(|setting| {
|
||||
setting.inline_assistant_model = Some(LanguageModelSelection {
|
||||
provider: provider.into(),
|
||||
model,
|
||||
});
|
||||
setting.inline_assistant_model = Some(LanguageModelSelection { provider, model });
|
||||
Ok(())
|
||||
})
|
||||
.ok();
|
||||
@@ -452,10 +396,7 @@ impl AssistantSettingsContent {
|
||||
|
||||
pub fn set_commit_message_model(&mut self, provider: String, model: String) {
|
||||
self.v2_setting(|setting| {
|
||||
setting.commit_message_model = Some(LanguageModelSelection {
|
||||
provider: provider.into(),
|
||||
model,
|
||||
});
|
||||
setting.commit_message_model = Some(LanguageModelSelection { provider, model });
|
||||
Ok(())
|
||||
})
|
||||
.ok();
|
||||
@@ -483,10 +424,7 @@ impl AssistantSettingsContent {
|
||||
|
||||
pub fn set_thread_summary_model(&mut self, provider: String, model: String) {
|
||||
self.v2_setting(|setting| {
|
||||
setting.thread_summary_model = Some(LanguageModelSelection {
|
||||
provider: provider.into(),
|
||||
model,
|
||||
});
|
||||
setting.thread_summary_model = Some(LanguageModelSelection { provider, model });
|
||||
Ok(())
|
||||
})
|
||||
.ok();
|
||||
@@ -582,8 +520,6 @@ impl Default for VersionedAssistantSettingsContent {
|
||||
notify_when_agent_waiting: None,
|
||||
stream_edits: None,
|
||||
single_file_review: None,
|
||||
model_parameters: Vec::new(),
|
||||
preferred_completion_mode: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -647,88 +583,37 @@ pub struct AssistantSettingsContentV2 {
|
||||
///
|
||||
/// Default: true
|
||||
single_file_review: Option<bool>,
|
||||
/// Additional parameters for language model requests. When making a request
|
||||
/// to a model, parameters will be taken from the last entry in this list
|
||||
/// that matches the model's provider and name. In each entry, both provider
|
||||
/// and model are optional, so that you can specify parameters for either
|
||||
/// one.
|
||||
///
|
||||
/// Default: []
|
||||
#[serde(default)]
|
||||
model_parameters: Vec<LanguageModelParameters>,
|
||||
|
||||
/// What completion mode to enable for new threads
|
||||
///
|
||||
/// Default: normal
|
||||
preferred_completion_mode: Option<CompletionMode>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Default)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CompletionMode {
|
||||
#[default]
|
||||
Normal,
|
||||
Max,
|
||||
}
|
||||
|
||||
impl From<CompletionMode> for zed_llm_client::CompletionMode {
|
||||
fn from(value: CompletionMode) -> Self {
|
||||
match value {
|
||||
CompletionMode::Normal => zed_llm_client::CompletionMode::Normal,
|
||||
CompletionMode::Max => zed_llm_client::CompletionMode::Max,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
pub struct LanguageModelSelection {
|
||||
pub provider: LanguageModelProviderSetting,
|
||||
#[schemars(schema_with = "providers_schema")]
|
||||
pub provider: String,
|
||||
pub model: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct LanguageModelProviderSetting(pub String);
|
||||
|
||||
impl JsonSchema for LanguageModelProviderSetting {
|
||||
fn schema_name() -> String {
|
||||
"LanguageModelProviderSetting".into()
|
||||
}
|
||||
|
||||
fn json_schema(_: &mut schemars::r#gen::SchemaGenerator) -> Schema {
|
||||
schemars::schema::SchemaObject {
|
||||
enum_values: Some(vec![
|
||||
"anthropic".into(),
|
||||
"bedrock".into(),
|
||||
"google".into(),
|
||||
"lmstudio".into(),
|
||||
"ollama".into(),
|
||||
"openai".into(),
|
||||
"zed.dev".into(),
|
||||
"copilot_chat".into(),
|
||||
"deepseek".into(),
|
||||
]),
|
||||
..Default::default()
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for LanguageModelProviderSetting {
|
||||
fn from(provider: String) -> Self {
|
||||
Self(provider)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for LanguageModelProviderSetting {
|
||||
fn from(provider: &str) -> Self {
|
||||
Self(provider.to_string())
|
||||
fn providers_schema(_: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
|
||||
schemars::schema::SchemaObject {
|
||||
enum_values: Some(vec![
|
||||
"anthropic".into(),
|
||||
"bedrock".into(),
|
||||
"google".into(),
|
||||
"lmstudio".into(),
|
||||
"ollama".into(),
|
||||
"openai".into(),
|
||||
"zed.dev".into(),
|
||||
"copilot_chat".into(),
|
||||
"deepseek".into(),
|
||||
]),
|
||||
..Default::default()
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
impl Default for LanguageModelSelection {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
provider: LanguageModelProviderSetting("openai".to_string()),
|
||||
provider: "openai".to_string(),
|
||||
model: "gpt-4".to_string(),
|
||||
}
|
||||
}
|
||||
@@ -865,14 +750,6 @@ impl Settings for AssistantSettings {
|
||||
merge(&mut settings.stream_edits, value.stream_edits);
|
||||
merge(&mut settings.single_file_review, value.single_file_review);
|
||||
merge(&mut settings.default_profile, value.default_profile);
|
||||
merge(
|
||||
&mut settings.preferred_completion_mode,
|
||||
value.preferred_completion_mode,
|
||||
);
|
||||
|
||||
settings
|
||||
.model_parameters
|
||||
.extend_from_slice(&value.model_parameters);
|
||||
|
||||
if let Some(profiles) = value.profiles {
|
||||
settings
|
||||
@@ -1006,8 +883,6 @@ mod tests {
|
||||
notify_when_agent_waiting: None,
|
||||
stream_edits: None,
|
||||
single_file_review: None,
|
||||
model_parameters: Vec::new(),
|
||||
preferred_completion_mode: None,
|
||||
},
|
||||
)),
|
||||
}
|
||||
@@ -1070,7 +945,7 @@ mod tests {
|
||||
AssistantSettingsContentV2 {
|
||||
enabled: Some(false),
|
||||
default_model: Some(LanguageModelSelection {
|
||||
provider: "xai".to_owned().into(),
|
||||
provider: "xai".to_owned(),
|
||||
model: "grok".to_owned(),
|
||||
}),
|
||||
..Default::default()
|
||||
|
||||
@@ -27,6 +27,7 @@ use std::sync::Arc;
|
||||
use assistant_settings::AssistantSettings;
|
||||
use assistant_tool::ToolRegistry;
|
||||
use copy_path_tool::CopyPathTool;
|
||||
use feature_flags::{AgentStreamEditsFeatureFlag, FeatureFlagAppExt};
|
||||
use gpui::{App, Entity};
|
||||
use http_client::HttpClientWithUrl;
|
||||
use language_model::LanguageModelRegistry;
|
||||
@@ -76,6 +77,8 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
|
||||
registry.register_tool(FetchTool::new(http_client));
|
||||
|
||||
register_edit_file_tool(cx);
|
||||
cx.observe_flag::<AgentStreamEditsFeatureFlag, _>(|_, cx| register_edit_file_tool(cx))
|
||||
.detach();
|
||||
cx.observe_global::<SettingsStore>(register_edit_file_tool)
|
||||
.detach();
|
||||
|
||||
|
||||
@@ -536,6 +536,7 @@ impl EditAgent {
|
||||
|
||||
let request = LanguageModelRequest {
|
||||
messages,
|
||||
// temperature: Some(0.5),
|
||||
..Default::default()
|
||||
};
|
||||
Ok(self.model.stream_completion_text(request, cx).await?.stream)
|
||||
|
||||
@@ -1033,7 +1033,7 @@ impl EvalAssertion {
|
||||
|
||||
fn eval(iterations: usize, expected_pass_ratio: f32, mut eval: EvalInput) {
|
||||
let mut evaluated_count = 0;
|
||||
report_progress(evaluated_count, iterations);
|
||||
report_progress(evaluated_count, 0, iterations);
|
||||
|
||||
let (tx, rx) = mpsc::channel();
|
||||
|
||||
@@ -1075,7 +1075,7 @@ fn eval(iterations: usize, expected_pass_ratio: f32, mut eval: EvalInput) {
|
||||
}
|
||||
|
||||
evaluated_count += 1;
|
||||
report_progress(evaluated_count, iterations);
|
||||
report_progress(evaluated_count, failed_count, iterations);
|
||||
}
|
||||
|
||||
let actual_pass_ratio = (iterations - failed_count) as f32 / iterations as f32;
|
||||
@@ -1146,8 +1146,13 @@ impl Display for EvalOutput {
|
||||
}
|
||||
}
|
||||
|
||||
fn report_progress(evaluated_count: usize, iterations: usize) {
|
||||
print!("\r\x1b[KEvaluated {}/{}", evaluated_count, iterations);
|
||||
fn report_progress(evaluated_count: usize, failed_count: usize, iterations: usize) {
|
||||
let failing_rate = failed_count as f32 / evaluated_count as f32;
|
||||
let passing_rate = ((1. - failing_rate) * 100.).round() as usize;
|
||||
print!(
|
||||
"\r\x1b[KEvaluated {}/{} ({}% passing)",
|
||||
evaluated_count, iterations, passing_rate
|
||||
);
|
||||
std::io::stdout().flush().unwrap();
|
||||
}
|
||||
|
||||
|
||||
@@ -581,18 +581,18 @@ impl ToolCard for EditFileToolCard {
|
||||
(IconName::ChevronDown, "Expand Code Block")
|
||||
};
|
||||
|
||||
let gradient_overlay =
|
||||
div()
|
||||
.absolute()
|
||||
.bottom_0()
|
||||
.left_0()
|
||||
.w_full()
|
||||
.h_2_5()
|
||||
.bg(gpui::linear_gradient(
|
||||
0.,
|
||||
gpui::linear_color_stop(cx.theme().colors().editor_background, 0.),
|
||||
gpui::linear_color_stop(cx.theme().colors().editor_background.opacity(0.), 1.),
|
||||
));
|
||||
let gradient_overlay = div()
|
||||
.absolute()
|
||||
.bottom_0()
|
||||
.left_0()
|
||||
.w_full()
|
||||
.h_2_5()
|
||||
.rounded_b_lg()
|
||||
.bg(gpui::linear_gradient(
|
||||
0.,
|
||||
gpui::linear_color_stop(cx.theme().colors().editor_background, 0.),
|
||||
gpui::linear_color_stop(cx.theme().colors().editor_background.opacity(0.), 1.),
|
||||
));
|
||||
|
||||
let border_color = cx.theme().colors().border.opacity(0.6);
|
||||
|
||||
@@ -610,9 +610,8 @@ impl ToolCard for EditFileToolCard {
|
||||
|
||||
let mut container = v_flex()
|
||||
.p_3()
|
||||
.gap_1()
|
||||
.gap_1p5()
|
||||
.border_t_1()
|
||||
.rounded_md()
|
||||
.border_color(border_color)
|
||||
.bg(cx.theme().colors().editor_background);
|
||||
|
||||
@@ -627,7 +626,7 @@ impl ToolCard for EditFileToolCard {
|
||||
_ => div().w_1_2(),
|
||||
}
|
||||
.id("loading_div")
|
||||
.h_1()
|
||||
.h_2()
|
||||
.rounded_full()
|
||||
.bg(cx.theme().colors().element_active)
|
||||
.with_animation(
|
||||
@@ -649,7 +648,7 @@ impl ToolCard for EditFileToolCard {
|
||||
.border_1()
|
||||
.when(failed, |card| card.border_dashed())
|
||||
.border_color(border_color)
|
||||
.rounded_md()
|
||||
.rounded_lg()
|
||||
.overflow_hidden()
|
||||
.child(codeblock_header)
|
||||
.when(failed && self.error_expanded, |card| {
|
||||
@@ -703,8 +702,8 @@ impl ToolCard for EditFileToolCard {
|
||||
|editor_container| editor_container.child(gradient_overlay),
|
||||
),
|
||||
)
|
||||
.when(is_collapsible, |card| {
|
||||
card.child(
|
||||
.when(is_collapsible, |editor_container| {
|
||||
editor_container.child(
|
||||
h_flex()
|
||||
.id(("expand-button", self.editor_unique_id))
|
||||
.flex_none()
|
||||
@@ -712,7 +711,6 @@ impl ToolCard for EditFileToolCard {
|
||||
.h_5()
|
||||
.justify_center()
|
||||
.border_t_1()
|
||||
.rounded_b_md()
|
||||
.border_color(border_color)
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.hover(|style| {
|
||||
|
||||
@@ -47,6 +47,7 @@ use std::{
|
||||
};
|
||||
use telemetry::Telemetry;
|
||||
use thiserror::Error;
|
||||
use tokio::net::TcpStream;
|
||||
use url::Url;
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
|
||||
@@ -1127,7 +1128,10 @@ impl Client {
|
||||
let stream = {
|
||||
let handle = cx.update(|cx| gpui_tokio::Tokio::handle(cx)).ok().unwrap();
|
||||
let _guard = handle.enter();
|
||||
connect_socks_proxy_stream(proxy.as_ref(), rpc_host).await?
|
||||
match proxy {
|
||||
Some(proxy) => connect_socks_proxy_stream(&proxy, rpc_host).await?,
|
||||
None => Box::new(TcpStream::connect(rpc_host).await?),
|
||||
}
|
||||
};
|
||||
|
||||
log::info!("connected to rpc endpoint {}", rpc_url);
|
||||
|
||||
@@ -1,61 +1,98 @@
|
||||
//! socks proxy
|
||||
use anyhow::{Result, anyhow};
|
||||
use anyhow::{Context, Result, anyhow};
|
||||
use http_client::Url;
|
||||
use tokio_socks::tcp::{Socks4Stream, Socks5Stream};
|
||||
|
||||
pub(crate) async fn connect_socks_proxy_stream(
|
||||
proxy: Option<&Url>,
|
||||
rpc_host: (&str, u16),
|
||||
) -> Result<Box<dyn AsyncReadWrite>> {
|
||||
let stream = match parse_socks_proxy(proxy) {
|
||||
Some((socks_proxy, SocksVersion::V4)) => {
|
||||
let stream = Socks4Stream::connect_with_socket(
|
||||
tokio::net::TcpStream::connect(socks_proxy).await?,
|
||||
rpc_host,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| anyhow!("error connecting to socks {}", err))?;
|
||||
Box::new(stream) as Box<dyn AsyncReadWrite>
|
||||
}
|
||||
Some((socks_proxy, SocksVersion::V5)) => Box::new(
|
||||
Socks5Stream::connect_with_socket(
|
||||
tokio::net::TcpStream::connect(socks_proxy).await?,
|
||||
rpc_host,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| anyhow!("error connecting to socks {}", err))?,
|
||||
) as Box<dyn AsyncReadWrite>,
|
||||
None => {
|
||||
Box::new(tokio::net::TcpStream::connect(rpc_host).await?) as Box<dyn AsyncReadWrite>
|
||||
}
|
||||
};
|
||||
Ok(stream)
|
||||
/// Identification to a Socks V4 Proxy
|
||||
struct Socks4Identification<'a> {
|
||||
user_id: &'a str,
|
||||
}
|
||||
|
||||
fn parse_socks_proxy(proxy: Option<&Url>) -> Option<((String, u16), SocksVersion)> {
|
||||
let proxy_url = proxy?;
|
||||
let scheme = proxy_url.scheme();
|
||||
/// Authorization to a Socks V5 Proxy
|
||||
struct Socks5Authorization<'a> {
|
||||
username: &'a str,
|
||||
password: &'a str,
|
||||
}
|
||||
|
||||
/// Socks Proxy Protocol Version
|
||||
///
|
||||
/// V4 allows idenfication using a user_id
|
||||
/// V5 allows authorization using a username and password
|
||||
enum SocksVersion<'a> {
|
||||
V4(Option<Socks4Identification<'a>>),
|
||||
V5(Option<Socks5Authorization<'a>>),
|
||||
}
|
||||
|
||||
pub(crate) async fn connect_socks_proxy_stream(
|
||||
proxy: &Url,
|
||||
rpc_host: (&str, u16),
|
||||
) -> Result<Box<dyn AsyncReadWrite>> {
|
||||
let Some((socks_proxy, version)) = parse_socks_proxy(proxy) else {
|
||||
// If parsing the proxy URL fails, we must avoid falling back to an insecure connection.
|
||||
// SOCKS proxies are often used in contexts where security and privacy are critical,
|
||||
// so any fallback could expose users to significant risks.
|
||||
return Err(anyhow!("Parsing proxy url failed"));
|
||||
};
|
||||
|
||||
// Connect to proxy and wrap protocol later
|
||||
let stream = tokio::net::TcpStream::connect(socks_proxy)
|
||||
.await
|
||||
.context("Failed to connect to socks proxy")?;
|
||||
|
||||
let socks: Box<dyn AsyncReadWrite> = match version {
|
||||
SocksVersion::V4(None) => {
|
||||
let socks = Socks4Stream::connect_with_socket(stream, rpc_host)
|
||||
.await
|
||||
.context("error connecting to socks")?;
|
||||
Box::new(socks)
|
||||
}
|
||||
SocksVersion::V4(Some(Socks4Identification { user_id })) => {
|
||||
let socks = Socks4Stream::connect_with_userid_and_socket(stream, rpc_host, user_id)
|
||||
.await
|
||||
.context("error connecting to socks")?;
|
||||
Box::new(socks)
|
||||
}
|
||||
SocksVersion::V5(None) => {
|
||||
let socks = Socks5Stream::connect_with_socket(stream, rpc_host)
|
||||
.await
|
||||
.context("error connecting to socks")?;
|
||||
Box::new(socks)
|
||||
}
|
||||
SocksVersion::V5(Some(Socks5Authorization { username, password })) => {
|
||||
let socks = Socks5Stream::connect_with_password_and_socket(
|
||||
stream, rpc_host, username, password,
|
||||
)
|
||||
.await
|
||||
.context("error connecting to socks")?;
|
||||
Box::new(socks)
|
||||
}
|
||||
};
|
||||
|
||||
Ok(socks)
|
||||
}
|
||||
|
||||
fn parse_socks_proxy(proxy: &Url) -> Option<((String, u16), SocksVersion<'_>)> {
|
||||
let scheme = proxy.scheme();
|
||||
let socks_version = if scheme.starts_with("socks4") {
|
||||
// socks4
|
||||
SocksVersion::V4
|
||||
let identification = match proxy.username() {
|
||||
"" => None,
|
||||
username => Some(Socks4Identification { user_id: username }),
|
||||
};
|
||||
SocksVersion::V4(identification)
|
||||
} else if scheme.starts_with("socks") {
|
||||
// socks, socks5
|
||||
SocksVersion::V5
|
||||
let authorization = proxy.password().map(|password| Socks5Authorization {
|
||||
username: proxy.username(),
|
||||
password,
|
||||
});
|
||||
SocksVersion::V5(authorization)
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
if let Some((host, port)) = proxy_url.host().zip(proxy_url.port_or_known_default()) {
|
||||
Some(((host.to_string(), port), socks_version))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// private helper structs and traits
|
||||
let host = proxy.host()?.to_string();
|
||||
let port = proxy.port_or_known_default()?;
|
||||
|
||||
enum SocksVersion {
|
||||
V4,
|
||||
V5,
|
||||
Some(((host, port), socks_version))
|
||||
}
|
||||
|
||||
pub(crate) trait AsyncReadWrite:
|
||||
@@ -66,3 +103,74 @@ impl<T: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static> A
|
||||
for T
|
||||
{
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use url::Url;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_socks4() {
|
||||
let proxy = Url::parse("socks4://proxy.example.com:1080").unwrap();
|
||||
|
||||
let ((host, port), version) = parse_socks_proxy(&proxy).unwrap();
|
||||
assert_eq!(host, "proxy.example.com");
|
||||
assert_eq!(port, 1080);
|
||||
assert!(matches!(version, SocksVersion::V4(None)))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_socks4_with_identification() {
|
||||
let proxy = Url::parse("socks4://userid@proxy.example.com:1080").unwrap();
|
||||
|
||||
let ((host, port), version) = parse_socks_proxy(&proxy).unwrap();
|
||||
assert_eq!(host, "proxy.example.com");
|
||||
assert_eq!(port, 1080);
|
||||
assert!(matches!(
|
||||
version,
|
||||
SocksVersion::V4(Some(Socks4Identification { user_id: "userid" }))
|
||||
))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_socks5() {
|
||||
let proxy = Url::parse("socks5://proxy.example.com:1080").unwrap();
|
||||
|
||||
let ((host, port), version) = parse_socks_proxy(&proxy).unwrap();
|
||||
assert_eq!(host, "proxy.example.com");
|
||||
assert_eq!(port, 1080);
|
||||
assert!(matches!(version, SocksVersion::V5(None)))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_socks5_with_authorization() {
|
||||
let proxy = Url::parse("socks5://username:password@proxy.example.com:1080").unwrap();
|
||||
|
||||
let ((host, port), version) = parse_socks_proxy(&proxy).unwrap();
|
||||
assert_eq!(host, "proxy.example.com");
|
||||
assert_eq!(port, 1080);
|
||||
assert!(matches!(
|
||||
version,
|
||||
SocksVersion::V5(Some(Socks5Authorization {
|
||||
username: "username",
|
||||
password: "password"
|
||||
}))
|
||||
))
|
||||
}
|
||||
|
||||
/// If parsing the proxy URL fails, we must avoid falling back to an insecure connection.
|
||||
/// SOCKS proxies are often used in contexts where security and privacy are critical,
|
||||
/// so any fallback could expose users to significant risks.
|
||||
#[tokio::test]
|
||||
async fn fails_on_bad_proxy() {
|
||||
// Should fail connecting because http is not a valid Socks proxy scheme
|
||||
let proxy = Url::parse("http://localhost:2313").unwrap();
|
||||
|
||||
let result = connect_socks_proxy_stream(&proxy, ("test", 1080)).await;
|
||||
match result {
|
||||
Err(e) => assert_eq!(e.to_string(), "Parsing proxy url failed"),
|
||||
Ok(_) => panic!("Connecting on bad proxy should fail"),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +78,6 @@ zed_llm_client.workspace = true
|
||||
[dev-dependencies]
|
||||
assistant = { workspace = true, features = ["test-support"] }
|
||||
assistant_context_editor.workspace = true
|
||||
assistant_settings.workspace = true
|
||||
assistant_slash_command.workspace = true
|
||||
assistant_tool.workspace = true
|
||||
async-trait.workspace = true
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
create table subscription_usages_v2 (
|
||||
id uuid primary key,
|
||||
user_id integer not null,
|
||||
period_start_at timestamp without time zone not null,
|
||||
period_end_at timestamp without time zone not null,
|
||||
plan text not null,
|
||||
model_requests int not null default 0,
|
||||
edit_predictions int not null default 0
|
||||
);
|
||||
|
||||
create unique index uix_subscription_usages_v2_on_user_id_start_at_end_at on subscription_usages_v2 (user_id, period_start_at, period_end_at);
|
||||
|
||||
create index ix_subscription_usages_v2_on_plan on subscription_usages_v2 (plan);
|
||||
|
||||
create table subscription_usage_meters_v2 (
|
||||
id uuid primary key,
|
||||
subscription_usage_id uuid not null references subscription_usages_v2 (id) on delete cascade,
|
||||
model_id integer not null references models (id) on delete cascade,
|
||||
mode text not null,
|
||||
requests integer not null default 0
|
||||
);
|
||||
|
||||
create unique index uix_subscription_usage_meters_v2_on_usage_model_mode on subscription_usage_meters_v2 (subscription_usage_id, model_id, mode);
|
||||
@@ -0,0 +1,2 @@
|
||||
drop table subscription_usage_meters;
|
||||
drop table subscription_usages;
|
||||
@@ -27,7 +27,9 @@ use crate::db::billing_subscription::{
|
||||
StripeCancellationReason, StripeSubscriptionStatus, SubscriptionKind,
|
||||
};
|
||||
use crate::llm::db::subscription_usage_meter::CompletionMode;
|
||||
use crate::llm::{DEFAULT_MAX_MONTHLY_SPEND, FREE_TIER_MONTHLY_SPENDING_LIMIT};
|
||||
use crate::llm::{
|
||||
AGENT_EXTENDED_TRIAL_FEATURE_FLAG, DEFAULT_MAX_MONTHLY_SPEND, FREE_TIER_MONTHLY_SPENDING_LIMIT,
|
||||
};
|
||||
use crate::rpc::{ResultExt as _, Server};
|
||||
use crate::{AppState, Cents, Error, Result};
|
||||
use crate::{db::UserId, llm::db::LlmDatabase};
|
||||
@@ -54,6 +56,10 @@ pub fn router() -> Router {
|
||||
"/billing/subscriptions/manage",
|
||||
post(manage_billing_subscription),
|
||||
)
|
||||
.route(
|
||||
"/billing/subscriptions/migrate",
|
||||
post(migrate_to_new_billing),
|
||||
)
|
||||
.route("/billing/monthly_spend", get(get_monthly_spend))
|
||||
.route("/billing/usage", get(get_current_usage))
|
||||
}
|
||||
@@ -256,6 +262,7 @@ async fn list_billing_subscriptions(
|
||||
enum ProductCode {
|
||||
ZedPro,
|
||||
ZedProTrial,
|
||||
ZedFree,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -362,12 +369,7 @@ async fn create_billing_subscription(
|
||||
let checkout_session_url = match body.product {
|
||||
Some(ProductCode::ZedPro) => {
|
||||
stripe_billing
|
||||
.checkout_with_price(
|
||||
app.config.zed_pro_price_id()?,
|
||||
customer_id,
|
||||
&user.github_login,
|
||||
&success_url,
|
||||
)
|
||||
.checkout_with_zed_pro(customer_id, &user.github_login, &success_url)
|
||||
.await?
|
||||
}
|
||||
Some(ProductCode::ZedProTrial) => {
|
||||
@@ -384,7 +386,6 @@ async fn create_billing_subscription(
|
||||
|
||||
stripe_billing
|
||||
.checkout_with_zed_pro_trial(
|
||||
app.config.zed_pro_price_id()?,
|
||||
customer_id,
|
||||
&user.github_login,
|
||||
feature_flags,
|
||||
@@ -392,6 +393,11 @@ async fn create_billing_subscription(
|
||||
)
|
||||
.await?
|
||||
}
|
||||
Some(ProductCode::ZedFree) => {
|
||||
stripe_billing
|
||||
.checkout_with_zed_free(customer_id, &user.github_login, &success_url)
|
||||
.await?
|
||||
}
|
||||
None => {
|
||||
let default_model = llm_db.model(
|
||||
zed_llm_client::LanguageModelProvider::Anthropic,
|
||||
@@ -458,6 +464,14 @@ async fn manage_billing_subscription(
|
||||
))?
|
||||
};
|
||||
|
||||
let Some(stripe_billing) = app.stripe_billing.clone() else {
|
||||
log::error!("failed to retrieve Stripe billing object");
|
||||
Err(Error::http(
|
||||
StatusCode::NOT_IMPLEMENTED,
|
||||
"not supported".into(),
|
||||
))?
|
||||
};
|
||||
|
||||
let customer = app
|
||||
.db
|
||||
.get_billing_customer_by_user_id(user.id)
|
||||
@@ -508,8 +522,8 @@ async fn manage_billing_subscription(
|
||||
let flow = match body.intent {
|
||||
ManageSubscriptionIntent::ManageSubscription => None,
|
||||
ManageSubscriptionIntent::UpgradeToPro => {
|
||||
let zed_pro_price_id = app.config.zed_pro_price_id()?;
|
||||
let zed_free_price_id = app.config.zed_free_price_id()?;
|
||||
let zed_pro_price_id = stripe_billing.zed_pro_price_id().await?;
|
||||
let zed_free_price_id = stripe_billing.zed_free_price_id().await?;
|
||||
|
||||
let stripe_subscription =
|
||||
Subscription::retrieve(&stripe_client, &subscription_id, &[]).await?;
|
||||
@@ -602,6 +616,86 @@ async fn manage_billing_subscription(
|
||||
}))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct MigrateToNewBillingBody {
|
||||
github_user_id: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct MigrateToNewBillingResponse {
|
||||
/// The ID of the subscription that was canceled.
|
||||
canceled_subscription_id: Option<String>,
|
||||
}
|
||||
|
||||
async fn migrate_to_new_billing(
|
||||
Extension(app): Extension<Arc<AppState>>,
|
||||
extract::Json(body): extract::Json<MigrateToNewBillingBody>,
|
||||
) -> Result<Json<MigrateToNewBillingResponse>> {
|
||||
let Some(stripe_client) = app.stripe_client.clone() else {
|
||||
log::error!("failed to retrieve Stripe client");
|
||||
Err(Error::http(
|
||||
StatusCode::NOT_IMPLEMENTED,
|
||||
"not supported".into(),
|
||||
))?
|
||||
};
|
||||
|
||||
let user = app
|
||||
.db
|
||||
.get_user_by_github_user_id(body.github_user_id)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("user not found"))?;
|
||||
|
||||
let old_billing_subscriptions_by_user = app
|
||||
.db
|
||||
.get_active_billing_subscriptions(HashSet::from_iter([user.id]))
|
||||
.await?;
|
||||
|
||||
let canceled_subscription_id = if let Some((_billing_customer, billing_subscription)) =
|
||||
old_billing_subscriptions_by_user.get(&user.id)
|
||||
{
|
||||
let stripe_subscription_id = billing_subscription
|
||||
.stripe_subscription_id
|
||||
.parse::<stripe::SubscriptionId>()
|
||||
.context("failed to parse Stripe subscription ID from database")?;
|
||||
|
||||
Subscription::cancel(
|
||||
&stripe_client,
|
||||
&stripe_subscription_id,
|
||||
stripe::CancelSubscription {
|
||||
invoice_now: Some(true),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
Some(stripe_subscription_id)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let all_feature_flags = app.db.list_feature_flags().await?;
|
||||
let user_feature_flags = app.db.get_user_flags(user.id).await?;
|
||||
|
||||
for feature_flag in ["new-billing", "assistant2"] {
|
||||
let already_in_feature_flag = user_feature_flags.iter().any(|flag| flag == feature_flag);
|
||||
if already_in_feature_flag {
|
||||
continue;
|
||||
}
|
||||
|
||||
let feature_flag = all_feature_flags
|
||||
.iter()
|
||||
.find(|flag| flag.flag == feature_flag)
|
||||
.context("failed to find feature flag: {feature_flag:?}")?;
|
||||
|
||||
app.db.add_user_flag(user.id, feature_flag.id).await?;
|
||||
}
|
||||
|
||||
Ok(Json(MigrateToNewBillingResponse {
|
||||
canceled_subscription_id: canceled_subscription_id
|
||||
.map(|subscription_id| subscription_id.to_string()),
|
||||
}))
|
||||
}
|
||||
|
||||
/// The amount of time we wait in between each poll of Stripe events.
|
||||
///
|
||||
/// This value should strike a balance between:
|
||||
@@ -856,9 +950,11 @@ async fn handle_customer_subscription_event(
|
||||
|
||||
log::info!("handling Stripe {} event: {}", event.type_, event.id);
|
||||
|
||||
let subscription_kind = maybe!({
|
||||
let zed_pro_price_id = app.config.zed_pro_price_id().ok()?;
|
||||
let zed_free_price_id = app.config.zed_free_price_id().ok()?;
|
||||
let subscription_kind = maybe!(async {
|
||||
let stripe_billing = app.stripe_billing.clone()?;
|
||||
|
||||
let zed_pro_price_id = stripe_billing.zed_pro_price_id().await.ok()?;
|
||||
let zed_free_price_id = stripe_billing.zed_free_price_id().await.ok()?;
|
||||
|
||||
subscription.items.data.iter().find_map(|item| {
|
||||
let price = item.price.as_ref()?;
|
||||
@@ -875,7 +971,8 @@ async fn handle_customer_subscription_event(
|
||||
None
|
||||
}
|
||||
})
|
||||
});
|
||||
})
|
||||
.await;
|
||||
|
||||
let billing_customer =
|
||||
find_or_create_billing_customer(app, stripe_client, subscription.customer)
|
||||
@@ -943,6 +1040,7 @@ async fn handle_customer_subscription_event(
|
||||
billing_customer.user_id,
|
||||
&existing_subscription,
|
||||
subscription_kind,
|
||||
subscription.status.into(),
|
||||
new_period_start_at,
|
||||
new_period_end_at,
|
||||
)
|
||||
@@ -1091,11 +1189,25 @@ struct UsageCounts {
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct GetCurrentUsageResponse {
|
||||
struct ModelRequestUsage {
|
||||
pub model: String,
|
||||
pub mode: CompletionMode,
|
||||
pub requests: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct CurrentUsage {
|
||||
pub model_requests: UsageCounts,
|
||||
pub model_request_usage: Vec<ModelRequestUsage>,
|
||||
pub edit_predictions: UsageCounts,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize)]
|
||||
struct GetCurrentUsageResponse {
|
||||
pub plan: String,
|
||||
pub current_usage: Option<CurrentUsage>,
|
||||
}
|
||||
|
||||
async fn get_current_usage(
|
||||
Extension(app): Extension<Arc<AppState>>,
|
||||
Query(params): Query<GetCurrentUsageParams>,
|
||||
@@ -1106,6 +1218,11 @@ async fn get_current_usage(
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("user not found"))?;
|
||||
|
||||
let feature_flags = app.db.get_user_flags(user.id).await?;
|
||||
let has_extended_trial = feature_flags
|
||||
.iter()
|
||||
.any(|flag| flag == AGENT_EXTENDED_TRIAL_FEATURE_FLAG);
|
||||
|
||||
let Some(llm_db) = app.llm_db.clone() else {
|
||||
return Err(Error::http(
|
||||
StatusCode::NOT_IMPLEMENTED,
|
||||
@@ -1113,21 +1230,8 @@ async fn get_current_usage(
|
||||
));
|
||||
};
|
||||
|
||||
let empty_usage = GetCurrentUsageResponse {
|
||||
model_requests: UsageCounts {
|
||||
used: 0,
|
||||
limit: Some(0),
|
||||
remaining: Some(0),
|
||||
},
|
||||
edit_predictions: UsageCounts {
|
||||
used: 0,
|
||||
limit: Some(0),
|
||||
remaining: Some(0),
|
||||
},
|
||||
};
|
||||
|
||||
let Some(subscription) = app.db.get_active_billing_subscription(user.id).await? else {
|
||||
return Ok(Json(empty_usage));
|
||||
return Ok(Json(GetCurrentUsageResponse::default()));
|
||||
};
|
||||
|
||||
let subscription_period = maybe!({
|
||||
@@ -1138,42 +1242,93 @@ async fn get_current_usage(
|
||||
});
|
||||
|
||||
let Some((period_start_at, period_end_at)) = subscription_period else {
|
||||
return Ok(Json(empty_usage));
|
||||
return Ok(Json(GetCurrentUsageResponse::default()));
|
||||
};
|
||||
|
||||
let usage = llm_db
|
||||
.get_subscription_usage_for_period(user.id, period_start_at, period_end_at)
|
||||
.await?;
|
||||
let Some(usage) = usage else {
|
||||
return Ok(Json(empty_usage));
|
||||
};
|
||||
|
||||
let plan = match usage.plan {
|
||||
SubscriptionKind::ZedPro => zed_llm_client::Plan::ZedPro,
|
||||
SubscriptionKind::ZedProTrial => zed_llm_client::Plan::ZedProTrial,
|
||||
SubscriptionKind::ZedFree => zed_llm_client::Plan::Free,
|
||||
};
|
||||
let plan = usage
|
||||
.as_ref()
|
||||
.map(|usage| usage.plan.into())
|
||||
.unwrap_or_else(|| {
|
||||
subscription
|
||||
.kind
|
||||
.map(Into::into)
|
||||
.unwrap_or(zed_llm_client::Plan::Free)
|
||||
});
|
||||
|
||||
let model_requests_limit = match plan.model_requests_limit() {
|
||||
zed_llm_client::UsageLimit::Limited(limit) => Some(limit),
|
||||
zed_llm_client::UsageLimit::Limited(limit) => {
|
||||
let limit = if plan == zed_llm_client::Plan::ZedProTrial && has_extended_trial {
|
||||
1_000
|
||||
} else {
|
||||
limit
|
||||
};
|
||||
|
||||
Some(limit)
|
||||
}
|
||||
zed_llm_client::UsageLimit::Unlimited => None,
|
||||
};
|
||||
let edit_prediction_limit = match plan.edit_predictions_limit() {
|
||||
|
||||
let edit_predictions_limit = match plan.edit_predictions_limit() {
|
||||
zed_llm_client::UsageLimit::Limited(limit) => Some(limit),
|
||||
zed_llm_client::UsageLimit::Unlimited => None,
|
||||
};
|
||||
|
||||
let Some(usage) = usage else {
|
||||
return Ok(Json(GetCurrentUsageResponse {
|
||||
plan: plan.as_str().to_string(),
|
||||
current_usage: Some(CurrentUsage {
|
||||
model_requests: UsageCounts {
|
||||
used: 0,
|
||||
limit: model_requests_limit,
|
||||
remaining: model_requests_limit,
|
||||
},
|
||||
model_request_usage: Vec::new(),
|
||||
edit_predictions: UsageCounts {
|
||||
used: 0,
|
||||
limit: edit_predictions_limit,
|
||||
remaining: edit_predictions_limit,
|
||||
},
|
||||
}),
|
||||
}));
|
||||
};
|
||||
|
||||
let subscription_usage_meters = llm_db
|
||||
.get_current_subscription_usage_meters_for_user(user.id, Utc::now())
|
||||
.await?;
|
||||
|
||||
let model_request_usage = subscription_usage_meters
|
||||
.into_iter()
|
||||
.filter_map(|(usage_meter, _usage)| {
|
||||
let model = llm_db.model_by_id(usage_meter.model_id).ok()?;
|
||||
|
||||
Some(ModelRequestUsage {
|
||||
model: model.name.clone(),
|
||||
mode: usage_meter.mode,
|
||||
requests: usage_meter.requests,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(Json(GetCurrentUsageResponse {
|
||||
model_requests: UsageCounts {
|
||||
used: usage.model_requests,
|
||||
limit: model_requests_limit,
|
||||
remaining: model_requests_limit.map(|limit| (limit - usage.model_requests).max(0)),
|
||||
},
|
||||
edit_predictions: UsageCounts {
|
||||
used: usage.edit_predictions,
|
||||
limit: edit_prediction_limit,
|
||||
remaining: edit_prediction_limit.map(|limit| (limit - usage.edit_predictions).max(0)),
|
||||
},
|
||||
plan: plan.as_str().to_string(),
|
||||
current_usage: Some(CurrentUsage {
|
||||
model_requests: UsageCounts {
|
||||
used: usage.model_requests,
|
||||
limit: model_requests_limit,
|
||||
remaining: model_requests_limit.map(|limit| (limit - usage.model_requests).max(0)),
|
||||
},
|
||||
model_request_usage,
|
||||
edit_predictions: UsageCounts {
|
||||
used: usage.edit_predictions,
|
||||
limit: edit_predictions_limit,
|
||||
remaining: edit_predictions_limit
|
||||
.map(|limit| (limit - usage.edit_predictions).max(0)),
|
||||
},
|
||||
}),
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -1358,9 +1513,19 @@ async fn sync_model_request_usage_with_stripe(
|
||||
llm_db: &Arc<LlmDatabase>,
|
||||
stripe_billing: &Arc<StripeBilling>,
|
||||
) -> anyhow::Result<()> {
|
||||
let staff_users = app.db.get_staff_users().await?;
|
||||
let staff_user_ids = staff_users
|
||||
.iter()
|
||||
.map(|user| user.id)
|
||||
.collect::<HashSet<UserId>>();
|
||||
|
||||
let usage_meters = llm_db
|
||||
.get_current_subscription_usage_meters(Utc::now())
|
||||
.await?;
|
||||
let usage_meters = usage_meters
|
||||
.into_iter()
|
||||
.filter(|(_, usage)| !staff_user_ids.contains(&usage.user_id))
|
||||
.collect::<Vec<_>>();
|
||||
let user_ids = usage_meters
|
||||
.iter()
|
||||
.map(|(_, usage)| usage.user_id)
|
||||
@@ -1402,12 +1567,12 @@ async fn sync_model_request_usage_with_stripe(
|
||||
|
||||
let model = llm_db.model_by_id(usage_meter.model_id)?;
|
||||
|
||||
let (price_id, meter_event_name) = match model.name.as_str() {
|
||||
"claude-3-5-sonnet" => (&claude_3_5_sonnet.id, "claude_3_5_sonnet/requests"),
|
||||
let (price, meter_event_name) = match model.name.as_str() {
|
||||
"claude-3-5-sonnet" => (&claude_3_5_sonnet, "claude_3_5_sonnet/requests"),
|
||||
"claude-3-7-sonnet" => match usage_meter.mode {
|
||||
CompletionMode::Normal => (&claude_3_7_sonnet.id, "claude_3_7_sonnet/requests"),
|
||||
CompletionMode::Normal => (&claude_3_7_sonnet, "claude_3_7_sonnet/requests"),
|
||||
CompletionMode::Max => {
|
||||
(&claude_3_7_sonnet_max.id, "claude_3_7_sonnet/requests/max")
|
||||
(&claude_3_7_sonnet_max, "claude_3_7_sonnet/requests/max")
|
||||
}
|
||||
},
|
||||
model_name => {
|
||||
@@ -1416,7 +1581,7 @@ async fn sync_model_request_usage_with_stripe(
|
||||
};
|
||||
|
||||
stripe_billing
|
||||
.subscribe_to_price(&stripe_subscription_id, price_id)
|
||||
.subscribe_to_price(&stripe_subscription_id, price)
|
||||
.await?;
|
||||
stripe_billing
|
||||
.bill_model_request_usage(
|
||||
|
||||
@@ -65,6 +65,18 @@ impl Database {
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns all users flagged as staff.
|
||||
pub async fn get_staff_users(&self) -> Result<Vec<user::Model>> {
|
||||
self.transaction(|tx| async {
|
||||
let tx = tx;
|
||||
Ok(user::Entity::find()
|
||||
.filter(user::Column::Admin.eq(true))
|
||||
.all(&*tx)
|
||||
.await?)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns a user by email address. There are no access checks here, so this should only be used internally.
|
||||
pub async fn get_user_by_email(&self, email: &str) -> Result<Option<User>> {
|
||||
self.transaction(|tx| async move {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::db::{BillingCustomerId, BillingSubscriptionId};
|
||||
use chrono::{Datelike as _, NaiveDate, Utc};
|
||||
use sea_orm::entity::prelude::*;
|
||||
use serde::Serialize;
|
||||
|
||||
@@ -29,6 +30,38 @@ impl Model {
|
||||
let period_end = self.stripe_current_period_end?;
|
||||
chrono::DateTime::from_timestamp(period_end, 0)
|
||||
}
|
||||
|
||||
pub fn current_period(
|
||||
subscription: Option<Self>,
|
||||
is_staff: bool,
|
||||
) -> Option<(DateTimeUtc, DateTimeUtc)> {
|
||||
if is_staff {
|
||||
let now = Utc::now();
|
||||
let year = now.year();
|
||||
let month = now.month();
|
||||
|
||||
let first_day_of_this_month =
|
||||
NaiveDate::from_ymd_opt(year, month, 1)?.and_hms_opt(0, 0, 0)?;
|
||||
|
||||
let next_month = if month == 12 { 1 } else { month + 1 };
|
||||
let next_month_year = if month == 12 { year + 1 } else { year };
|
||||
let first_day_of_next_month =
|
||||
NaiveDate::from_ymd_opt(next_month_year, next_month, 1)?.and_hms_opt(23, 59, 59)?;
|
||||
|
||||
let last_day_of_this_month = first_day_of_next_month - chrono::Days::new(1);
|
||||
|
||||
Some((
|
||||
first_day_of_this_month.and_utc(),
|
||||
last_day_of_this_month.and_utc(),
|
||||
))
|
||||
} else {
|
||||
let subscription = subscription?;
|
||||
let period_start_at = subscription.current_period_start_at()?;
|
||||
let period_end_at = subscription.current_period_end_at()?;
|
||||
|
||||
Some((period_start_at, period_end_at))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
@@ -61,6 +94,16 @@ pub enum SubscriptionKind {
|
||||
ZedFree,
|
||||
}
|
||||
|
||||
impl From<SubscriptionKind> for zed_llm_client::Plan {
|
||||
fn from(value: SubscriptionKind) -> Self {
|
||||
match value {
|
||||
SubscriptionKind::ZedPro => Self::ZedPro,
|
||||
SubscriptionKind::ZedProTrial => Self::ZedProTrial,
|
||||
SubscriptionKind::ZedFree => Self::Free,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The status of a Stripe subscription.
|
||||
///
|
||||
/// [Stripe docs](https://docs.stripe.com/api/subscriptions/object#subscription_object-status)
|
||||
|
||||
@@ -180,9 +180,6 @@ pub struct Config {
|
||||
pub slack_panics_webhook: Option<String>,
|
||||
pub auto_join_channel_id: Option<ChannelId>,
|
||||
pub stripe_api_key: Option<String>,
|
||||
pub stripe_zed_pro_price_id: Option<String>,
|
||||
pub stripe_zed_pro_trial_price_id: Option<String>,
|
||||
pub stripe_zed_free_price_id: Option<String>,
|
||||
pub supermaven_admin_api_key: Option<Arc<str>>,
|
||||
pub user_backfiller_github_access_token: Option<Arc<str>>,
|
||||
}
|
||||
@@ -201,22 +198,6 @@ impl Config {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn zed_pro_price_id(&self) -> anyhow::Result<stripe::PriceId> {
|
||||
Self::parse_stripe_price_id("Zed Pro", self.stripe_zed_pro_price_id.as_deref())
|
||||
}
|
||||
|
||||
pub fn zed_free_price_id(&self) -> anyhow::Result<stripe::PriceId> {
|
||||
Self::parse_stripe_price_id("Zed Free", self.stripe_zed_pro_price_id.as_deref())
|
||||
}
|
||||
|
||||
fn parse_stripe_price_id(name: &str, value: Option<&str>) -> anyhow::Result<stripe::PriceId> {
|
||||
use std::str::FromStr as _;
|
||||
|
||||
let price_id = value.ok_or_else(|| anyhow!("{name} price ID not set"))?;
|
||||
|
||||
Ok(stripe::PriceId::from_str(price_id)?)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn test() -> Self {
|
||||
Self {
|
||||
@@ -254,9 +235,6 @@ impl Config {
|
||||
migrations_path: None,
|
||||
seed_path: None,
|
||||
stripe_api_key: None,
|
||||
stripe_zed_pro_price_id: None,
|
||||
stripe_zed_pro_trial_price_id: None,
|
||||
stripe_zed_free_price_id: None,
|
||||
supermaven_admin_api_key: None,
|
||||
user_backfiller_github_access_token: None,
|
||||
kinesis_region: None,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::db::UserId;
|
||||
use crate::llm::db::queries::subscription_usages::convert_chrono_to_time;
|
||||
|
||||
use super::*;
|
||||
@@ -34,4 +35,38 @@ impl LlmDatabase {
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns all current subscription usage meters for the given user as of the given timestamp.
|
||||
pub async fn get_current_subscription_usage_meters_for_user(
|
||||
&self,
|
||||
user_id: UserId,
|
||||
now: DateTimeUtc,
|
||||
) -> Result<Vec<(subscription_usage_meter::Model, subscription_usage::Model)>> {
|
||||
let now = convert_chrono_to_time(now)?;
|
||||
|
||||
self.transaction(|tx| async move {
|
||||
let result = subscription_usage_meter::Entity::find()
|
||||
.inner_join(subscription_usage::Entity)
|
||||
.filter(subscription_usage::Column::UserId.eq(user_id))
|
||||
.filter(
|
||||
subscription_usage::Column::PeriodStartAt
|
||||
.lte(now)
|
||||
.and(subscription_usage::Column::PeriodEndAt.gte(now)),
|
||||
)
|
||||
.select_also(subscription_usage::Entity)
|
||||
.all(&*tx)
|
||||
.await?;
|
||||
|
||||
let result = result
|
||||
.into_iter()
|
||||
.filter_map(|(meter, usage)| {
|
||||
let usage = usage?;
|
||||
Some((meter, usage))
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(result)
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use chrono::Timelike;
|
||||
use time::PrimitiveDateTime;
|
||||
|
||||
use crate::db::billing_subscription::SubscriptionKind;
|
||||
use crate::db::billing_subscription::{StripeSubscriptionStatus, SubscriptionKind};
|
||||
use crate::db::{UserId, billing_subscription};
|
||||
|
||||
use super::*;
|
||||
@@ -69,7 +69,7 @@ impl LlmDatabase {
|
||||
|
||||
Ok(
|
||||
subscription_usage::Entity::insert(subscription_usage::ActiveModel {
|
||||
id: ActiveValue::not_set(),
|
||||
id: ActiveValue::set(Uuid::now_v7()),
|
||||
user_id: ActiveValue::set(user_id),
|
||||
period_start_at: ActiveValue::set(period_start_at),
|
||||
period_end_at: ActiveValue::set(period_end_at),
|
||||
@@ -120,12 +120,13 @@ impl LlmDatabase {
|
||||
user_id: UserId,
|
||||
existing_subscription: &billing_subscription::Model,
|
||||
new_subscription_kind: Option<SubscriptionKind>,
|
||||
new_subscription_status: StripeSubscriptionStatus,
|
||||
new_period_start_at: DateTimeUtc,
|
||||
new_period_end_at: DateTimeUtc,
|
||||
) -> Result<Option<subscription_usage::Model>> {
|
||||
self.transaction(|tx| async move {
|
||||
match existing_subscription.kind {
|
||||
Some(SubscriptionKind::ZedProTrial) => {
|
||||
match (existing_subscription.kind, new_subscription_status) {
|
||||
(Some(SubscriptionKind::ZedProTrial), StripeSubscriptionStatus::Active) => {
|
||||
let trial_period_start_at = existing_subscription
|
||||
.current_period_start_at()
|
||||
.ok_or_else(|| anyhow!("No trial subscription period start"))?;
|
||||
|
||||
@@ -4,10 +4,10 @@ use sea_orm::entity::prelude::*;
|
||||
use time::PrimitiveDateTime;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "subscription_usages")]
|
||||
#[sea_orm(table_name = "subscription_usages_v2")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
pub id: Uuid,
|
||||
pub user_id: UserId,
|
||||
pub period_start_at: PrimitiveDateTime,
|
||||
pub period_end_at: PrimitiveDateTime,
|
||||
|
||||
@@ -4,11 +4,11 @@ use serde::Serialize;
|
||||
use crate::llm::db::ModelId;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "subscription_usage_meters")]
|
||||
#[sea_orm(table_name = "subscription_usage_meters_v2")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
pub subscription_usage_id: i32,
|
||||
pub id: Uuid,
|
||||
pub subscription_usage_id: Uuid,
|
||||
pub model_id: ModelId,
|
||||
pub mode: CompletionMode,
|
||||
pub requests: i32,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use chrono::{Duration, Utc};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use crate::db::billing_subscription::SubscriptionKind;
|
||||
use crate::db::billing_subscription::{StripeSubscriptionStatus, SubscriptionKind};
|
||||
use crate::db::{UserId, billing_subscription};
|
||||
use crate::llm::db::LlmDatabase;
|
||||
use crate::test_llm_db;
|
||||
@@ -12,58 +12,108 @@ test_llm_db!(
|
||||
);
|
||||
|
||||
async fn test_transfer_existing_subscription_usage(db: &mut LlmDatabase) {
|
||||
let user_id = UserId(1);
|
||||
// Test when an existing Zed Pro trial subscription is upgraded to Zed Pro.
|
||||
{
|
||||
let user_id = UserId(1);
|
||||
|
||||
let now = Utc::now();
|
||||
let now = Utc::now();
|
||||
|
||||
let trial_period_start_at = now - Duration::days(14);
|
||||
let trial_period_end_at = now;
|
||||
let trial_period_start_at = now - Duration::days(14);
|
||||
let trial_period_end_at = now;
|
||||
|
||||
let new_period_start_at = now;
|
||||
let new_period_end_at = now + Duration::days(30);
|
||||
let new_period_start_at = now;
|
||||
let new_period_end_at = now + Duration::days(30);
|
||||
|
||||
let existing_subscription = billing_subscription::Model {
|
||||
kind: Some(SubscriptionKind::ZedProTrial),
|
||||
stripe_current_period_start: Some(trial_period_start_at.timestamp()),
|
||||
stripe_current_period_end: Some(trial_period_end_at.timestamp()),
|
||||
..Default::default()
|
||||
};
|
||||
let existing_subscription = billing_subscription::Model {
|
||||
kind: Some(SubscriptionKind::ZedProTrial),
|
||||
stripe_current_period_start: Some(trial_period_start_at.timestamp()),
|
||||
stripe_current_period_end: Some(trial_period_end_at.timestamp()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let existing_usage = db
|
||||
.create_subscription_usage(
|
||||
user_id,
|
||||
trial_period_start_at,
|
||||
trial_period_end_at,
|
||||
SubscriptionKind::ZedProTrial,
|
||||
25,
|
||||
1_000,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let existing_usage = db
|
||||
.create_subscription_usage(
|
||||
user_id,
|
||||
trial_period_start_at,
|
||||
trial_period_end_at,
|
||||
SubscriptionKind::ZedProTrial,
|
||||
25,
|
||||
1_000,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let transferred_usage = db
|
||||
.transfer_existing_subscription_usage(
|
||||
user_id,
|
||||
&existing_subscription,
|
||||
Some(SubscriptionKind::ZedPro),
|
||||
new_period_start_at,
|
||||
new_period_end_at,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let transferred_usage = db
|
||||
.transfer_existing_subscription_usage(
|
||||
user_id,
|
||||
&existing_subscription,
|
||||
Some(SubscriptionKind::ZedPro),
|
||||
StripeSubscriptionStatus::Active,
|
||||
new_period_start_at,
|
||||
new_period_end_at,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
transferred_usage.is_some(),
|
||||
"subscription usage not transferred successfully"
|
||||
);
|
||||
let transferred_usage = transferred_usage.unwrap();
|
||||
assert!(
|
||||
transferred_usage.is_some(),
|
||||
"subscription usage not transferred successfully"
|
||||
);
|
||||
let transferred_usage = transferred_usage.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
transferred_usage.model_requests,
|
||||
existing_usage.model_requests
|
||||
);
|
||||
assert_eq!(
|
||||
transferred_usage.edit_predictions,
|
||||
existing_usage.edit_predictions
|
||||
);
|
||||
assert_eq!(
|
||||
transferred_usage.model_requests,
|
||||
existing_usage.model_requests
|
||||
);
|
||||
assert_eq!(
|
||||
transferred_usage.edit_predictions,
|
||||
existing_usage.edit_predictions
|
||||
);
|
||||
}
|
||||
|
||||
// Test when an existing Zed Pro trial subscription is canceled.
|
||||
{
|
||||
let user_id = UserId(2);
|
||||
|
||||
let now = Utc::now();
|
||||
|
||||
let trial_period_start_at = now - Duration::days(14);
|
||||
let trial_period_end_at = now;
|
||||
|
||||
let existing_subscription = billing_subscription::Model {
|
||||
kind: Some(SubscriptionKind::ZedProTrial),
|
||||
stripe_current_period_start: Some(trial_period_start_at.timestamp()),
|
||||
stripe_current_period_end: Some(trial_period_end_at.timestamp()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let _existing_usage = db
|
||||
.create_subscription_usage(
|
||||
user_id,
|
||||
trial_period_start_at,
|
||||
trial_period_end_at,
|
||||
SubscriptionKind::ZedProTrial,
|
||||
25,
|
||||
1_000,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let transferred_usage = db
|
||||
.transfer_existing_subscription_usage(
|
||||
user_id,
|
||||
&existing_subscription,
|
||||
Some(SubscriptionKind::ZedPro),
|
||||
StripeSubscriptionStatus::Canceled,
|
||||
trial_period_start_at,
|
||||
trial_period_end_at,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
transferred_usage.is_none(),
|
||||
"subscription usage was transferred when it should not have been"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::Duration;
|
||||
use thiserror::Error;
|
||||
use util::maybe;
|
||||
use uuid::Uuid;
|
||||
use zed_llm_client::Plan;
|
||||
|
||||
@@ -30,8 +29,12 @@ pub struct LlmTokenClaims {
|
||||
pub has_llm_closed_beta_feature_flag: bool,
|
||||
pub bypass_account_age_check: bool,
|
||||
pub has_llm_subscription: bool,
|
||||
#[serde(default)]
|
||||
pub use_llm_request_queue: bool,
|
||||
pub max_monthly_spend_in_cents: u32,
|
||||
pub custom_llm_monthly_allowance_in_cents: Option<u32>,
|
||||
#[serde(default)]
|
||||
pub use_new_billing: bool,
|
||||
pub plan: Plan,
|
||||
#[serde(default)]
|
||||
pub has_extended_trial: bool,
|
||||
@@ -90,24 +93,28 @@ impl LlmTokenClaims {
|
||||
custom_llm_monthly_allowance_in_cents: user
|
||||
.custom_llm_monthly_allowance_in_cents
|
||||
.map(|allowance| allowance as u32),
|
||||
plan: subscription
|
||||
.as_ref()
|
||||
.and_then(|subscription| subscription.kind)
|
||||
.map_or(Plan::Free, |kind| match kind {
|
||||
SubscriptionKind::ZedFree => Plan::Free,
|
||||
SubscriptionKind::ZedPro => Plan::ZedPro,
|
||||
SubscriptionKind::ZedProTrial => Plan::ZedProTrial,
|
||||
}),
|
||||
use_new_billing: feature_flags.iter().any(|flag| flag == "new-billing"),
|
||||
use_llm_request_queue: feature_flags.iter().any(|flag| flag == "llm-request-queue"),
|
||||
plan: if is_staff {
|
||||
Plan::ZedPro
|
||||
} else {
|
||||
subscription
|
||||
.as_ref()
|
||||
.and_then(|subscription| subscription.kind)
|
||||
.map_or(Plan::Free, |kind| match kind {
|
||||
SubscriptionKind::ZedFree => Plan::Free,
|
||||
SubscriptionKind::ZedPro => Plan::ZedPro,
|
||||
SubscriptionKind::ZedProTrial => Plan::ZedProTrial,
|
||||
})
|
||||
},
|
||||
has_extended_trial: feature_flags
|
||||
.iter()
|
||||
.any(|flag| flag == AGENT_EXTENDED_TRIAL_FEATURE_FLAG),
|
||||
subscription_period: maybe!({
|
||||
let subscription = subscription?;
|
||||
let period_start_at = subscription.current_period_start_at()?;
|
||||
let period_end_at = subscription.current_period_end_at()?;
|
||||
|
||||
Some((period_start_at.naive_utc(), period_end_at.naive_utc()))
|
||||
}),
|
||||
subscription_period: billing_subscription::Model::current_period(
|
||||
subscription,
|
||||
is_staff,
|
||||
)
|
||||
.map(|(start, end)| (start.naive_utc(), end.naive_utc())),
|
||||
enable_model_request_overages: billing_preferences
|
||||
.as_ref()
|
||||
.map_or(false, |preferences| {
|
||||
|
||||
@@ -37,7 +37,6 @@ use core::fmt::{self, Debug, Formatter};
|
||||
use reqwest_client::ReqwestClient;
|
||||
use rpc::proto::split_repository_update;
|
||||
use supermaven_api::{CreateExternalUserRequest, SupermavenAdminApi};
|
||||
use util::maybe;
|
||||
|
||||
use futures::{
|
||||
FutureExt, SinkExt, StreamExt, TryStreamExt, channel::oneshot, future::BoxFuture,
|
||||
@@ -181,6 +180,10 @@ impl Session {
|
||||
}
|
||||
|
||||
pub async fn current_plan(&self, db: &MutexGuard<'_, DbHandle>) -> anyhow::Result<proto::Plan> {
|
||||
if self.is_staff() {
|
||||
return Ok(proto::Plan::ZedPro);
|
||||
}
|
||||
|
||||
let user_id = self.user_id();
|
||||
|
||||
let subscription = db.get_active_billing_subscription(user_id).await?;
|
||||
@@ -328,6 +331,10 @@ impl Server {
|
||||
.add_request_handler(
|
||||
forward_read_only_project_request::<proto::LspExtSwitchSourceHeader>,
|
||||
)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::LspExtGoToParentModule>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::LspExtCancelFlycheck>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::LspExtRunFlycheck>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::LspExtClearFlycheck>)
|
||||
.add_request_handler(
|
||||
forward_read_only_project_request::<proto::LanguageServerIdForName>,
|
||||
)
|
||||
@@ -2705,13 +2712,10 @@ async fn update_user_plan(user_id: UserId, session: &Session) -> Result<()> {
|
||||
let usage = if let Some(llm_db) = session.app_state.llm_db.clone() {
|
||||
let subscription = db.get_active_billing_subscription(user_id).await?;
|
||||
|
||||
let subscription_period = maybe!({
|
||||
let subscription = subscription?;
|
||||
let period_start_at = subscription.current_period_start_at()?;
|
||||
let period_end_at = subscription.current_period_end_at()?;
|
||||
|
||||
Some((period_start_at, period_end_at))
|
||||
});
|
||||
let subscription_period = crate::db::billing_subscription::Model::current_period(
|
||||
subscription,
|
||||
session.is_staff(),
|
||||
);
|
||||
|
||||
if let Some((period_start_at, period_end_at)) = subscription_period {
|
||||
llm_db
|
||||
@@ -2733,8 +2737,12 @@ async fn update_user_plan(user_id: UserId, session: &Session) -> Result<()> {
|
||||
trial_started_at: billing_customer
|
||||
.and_then(|billing_customer| billing_customer.trial_started_at)
|
||||
.map(|trial_started_at| trial_started_at.and_utc().timestamp() as u64),
|
||||
is_usage_based_billing_enabled: billing_preferences
|
||||
.map(|preferences| preferences.model_request_overages_enabled),
|
||||
is_usage_based_billing_enabled: if session.is_staff() {
|
||||
Some(true)
|
||||
} else {
|
||||
billing_preferences
|
||||
.map(|preferences| preferences.model_request_overages_enabled)
|
||||
},
|
||||
usage: usage.map(|usage| {
|
||||
let plan = match plan {
|
||||
proto::Plan::Free => zed_llm_client::Plan::Free,
|
||||
|
||||
@@ -81,6 +81,24 @@ impl StripeBilling {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn zed_pro_price_id(&self) -> Result<PriceId> {
|
||||
self.find_price_id_by_lookup_key("zed-pro").await
|
||||
}
|
||||
|
||||
pub async fn zed_free_price_id(&self) -> Result<PriceId> {
|
||||
self.find_price_id_by_lookup_key("zed-free").await
|
||||
}
|
||||
|
||||
pub async fn find_price_id_by_lookup_key(&self, lookup_key: &str) -> Result<PriceId> {
|
||||
self.state
|
||||
.read()
|
||||
.await
|
||||
.prices_by_lookup_key
|
||||
.get(lookup_key)
|
||||
.map(|price| price.id.clone())
|
||||
.ok_or_else(|| crate::Error::Internal(anyhow!("no price ID found for {lookup_key:?}")))
|
||||
}
|
||||
|
||||
pub async fn find_price_by_lookup_key(&self, lookup_key: &str) -> Result<stripe::Price> {
|
||||
self.state
|
||||
.read()
|
||||
@@ -88,7 +106,7 @@ impl StripeBilling {
|
||||
.prices_by_lookup_key
|
||||
.get(lookup_key)
|
||||
.cloned()
|
||||
.ok_or_else(|| crate::Error::Internal(anyhow!("no price ID found for {lookup_key:?}")))
|
||||
.ok_or_else(|| crate::Error::Internal(anyhow!("no price found for {lookup_key:?}")))
|
||||
}
|
||||
|
||||
pub async fn register_model_for_token_based_usage(
|
||||
@@ -230,21 +248,26 @@ impl StripeBilling {
|
||||
pub async fn subscribe_to_price(
|
||||
&self,
|
||||
subscription_id: &stripe::SubscriptionId,
|
||||
price_id: &stripe::PriceId,
|
||||
price: &stripe::Price,
|
||||
) -> Result<()> {
|
||||
let subscription =
|
||||
stripe::Subscription::retrieve(&self.client, &subscription_id, &[]).await?;
|
||||
|
||||
if subscription_contains_price(&subscription, price_id) {
|
||||
if subscription_contains_price(&subscription, &price.id) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
const BILLING_THRESHOLD_IN_CENTS: i64 = 20 * 100;
|
||||
|
||||
let price_per_unit = price.unit_amount.unwrap_or_default();
|
||||
let _units_for_billing_threshold = BILLING_THRESHOLD_IN_CENTS / price_per_unit;
|
||||
|
||||
stripe::Subscription::update(
|
||||
&self.client,
|
||||
subscription_id,
|
||||
stripe::UpdateSubscription {
|
||||
items: Some(vec![stripe::UpdateSubscriptionItems {
|
||||
price: Some(price_id.to_string()),
|
||||
price: Some(price.id.to_string()),
|
||||
..Default::default()
|
||||
}]),
|
||||
trial_settings: Some(stripe::UpdateSubscriptionTrialSettings {
|
||||
@@ -463,19 +486,20 @@ impl StripeBilling {
|
||||
Ok(session.url.context("no checkout session URL")?)
|
||||
}
|
||||
|
||||
pub async fn checkout_with_price(
|
||||
pub async fn checkout_with_zed_pro(
|
||||
&self,
|
||||
price_id: PriceId,
|
||||
customer_id: stripe::CustomerId,
|
||||
github_login: &str,
|
||||
success_url: &str,
|
||||
) -> Result<String> {
|
||||
let zed_pro_price_id = self.zed_pro_price_id().await?;
|
||||
|
||||
let mut params = stripe::CreateCheckoutSession::new();
|
||||
params.mode = Some(stripe::CheckoutSessionMode::Subscription);
|
||||
params.customer = Some(customer_id);
|
||||
params.client_reference_id = Some(github_login);
|
||||
params.line_items = Some(vec![stripe::CreateCheckoutSessionLineItems {
|
||||
price: Some(price_id.to_string()),
|
||||
price: Some(zed_pro_price_id.to_string()),
|
||||
quantity: Some(1),
|
||||
..Default::default()
|
||||
}]);
|
||||
@@ -487,12 +511,13 @@ impl StripeBilling {
|
||||
|
||||
pub async fn checkout_with_zed_pro_trial(
|
||||
&self,
|
||||
zed_pro_price_id: PriceId,
|
||||
customer_id: stripe::CustomerId,
|
||||
github_login: &str,
|
||||
feature_flags: Vec<String>,
|
||||
success_url: &str,
|
||||
) -> Result<String> {
|
||||
let zed_pro_price_id = self.zed_pro_price_id().await?;
|
||||
|
||||
let eligible_for_extended_trial = feature_flags
|
||||
.iter()
|
||||
.any(|flag| flag == AGENT_EXTENDED_TRIAL_FEATURE_FLAG);
|
||||
@@ -537,6 +562,29 @@ impl StripeBilling {
|
||||
let session = stripe::CheckoutSession::create(&self.client, params).await?;
|
||||
Ok(session.url.context("no checkout session URL")?)
|
||||
}
|
||||
|
||||
pub async fn checkout_with_zed_free(
|
||||
&self,
|
||||
customer_id: stripe::CustomerId,
|
||||
github_login: &str,
|
||||
success_url: &str,
|
||||
) -> Result<String> {
|
||||
let zed_free_price_id = self.zed_free_price_id().await?;
|
||||
|
||||
let mut params = stripe::CreateCheckoutSession::new();
|
||||
params.mode = Some(stripe::CheckoutSessionMode::Subscription);
|
||||
params.customer = Some(customer_id);
|
||||
params.client_reference_id = Some(github_login);
|
||||
params.line_items = Some(vec![stripe::CreateCheckoutSessionLineItems {
|
||||
price: Some(zed_free_price_id.to_string()),
|
||||
quantity: Some(1),
|
||||
..Default::default()
|
||||
}]);
|
||||
params.success_url = Some(success_url);
|
||||
|
||||
let session = stripe::CheckoutSession::create(&self.client, params).await?;
|
||||
Ok(session.url.context("no checkout session URL")?)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
|
||||
@@ -25,7 +25,7 @@ use language::{
|
||||
use project::{
|
||||
ProjectPath, SERVER_PROGRESS_THROTTLE_TIMEOUT,
|
||||
lsp_store::{
|
||||
lsp_ext_command::{ExpandedMacro, LspExpandMacro},
|
||||
lsp_ext_command::{ExpandedMacro, LspExtExpandMacro},
|
||||
rust_analyzer_ext::RUST_ANALYZER_NAME,
|
||||
},
|
||||
project_settings::{InlineBlameSettings, ProjectSettings},
|
||||
@@ -2704,8 +2704,8 @@ async fn test_client_can_query_lsp_ext(cx_a: &mut TestAppContext, cx_b: &mut Tes
|
||||
let fake_language_server = fake_language_servers.next().await.unwrap();
|
||||
|
||||
// host
|
||||
let mut expand_request_a =
|
||||
fake_language_server.set_request_handler::<LspExpandMacro, _, _>(|params, _| async move {
|
||||
let mut expand_request_a = fake_language_server.set_request_handler::<LspExtExpandMacro, _, _>(
|
||||
|params, _| async move {
|
||||
assert_eq!(
|
||||
params.text_document.uri,
|
||||
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
|
||||
@@ -2715,7 +2715,8 @@ async fn test_client_can_query_lsp_ext(cx_a: &mut TestAppContext, cx_b: &mut Tes
|
||||
name: "test_macro_name".to_string(),
|
||||
expansion: "test_macro_expansion on the host".to_string(),
|
||||
}))
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
editor_a.update_in(cx_a, |editor, window, cx| {
|
||||
expand_macro_recursively(editor, &ExpandMacroRecursively, window, cx)
|
||||
@@ -2738,8 +2739,8 @@ async fn test_client_can_query_lsp_ext(cx_a: &mut TestAppContext, cx_b: &mut Tes
|
||||
});
|
||||
|
||||
// client
|
||||
let mut expand_request_b =
|
||||
fake_language_server.set_request_handler::<LspExpandMacro, _, _>(|params, _| async move {
|
||||
let mut expand_request_b = fake_language_server.set_request_handler::<LspExtExpandMacro, _, _>(
|
||||
|params, _| async move {
|
||||
assert_eq!(
|
||||
params.text_document.uri,
|
||||
lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
|
||||
@@ -2749,7 +2750,8 @@ async fn test_client_can_query_lsp_ext(cx_a: &mut TestAppContext, cx_b: &mut Tes
|
||||
name: "test_macro_name".to_string(),
|
||||
expansion: "test_macro_expansion on the client".to_string(),
|
||||
}))
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
editor_b.update_in(cx_b, |editor, window, cx| {
|
||||
expand_macro_recursively(editor, &ExpandMacroRecursively, window, cx)
|
||||
|
||||
@@ -2902,7 +2902,7 @@ async fn test_git_branch_name(
|
||||
.read(cx)
|
||||
.branch
|
||||
.as_ref()
|
||||
.map(|branch| branch.name.to_string()),
|
||||
.map(|branch| branch.name().to_owned()),
|
||||
branch_name
|
||||
)
|
||||
}
|
||||
@@ -6862,7 +6862,7 @@ async fn test_remote_git_branches(
|
||||
|
||||
let branches_b = branches_b
|
||||
.into_iter()
|
||||
.map(|branch| branch.name.to_string())
|
||||
.map(|branch| branch.name().to_string())
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
assert_eq!(branches_b, branches_set);
|
||||
@@ -6893,7 +6893,7 @@ async fn test_remote_git_branches(
|
||||
})
|
||||
});
|
||||
|
||||
assert_eq!(host_branch.name, branches[2]);
|
||||
assert_eq!(host_branch.name(), branches[2]);
|
||||
|
||||
// Also try creating a new branch
|
||||
cx_b.update(|cx| {
|
||||
@@ -6931,5 +6931,5 @@ async fn test_remote_git_branches(
|
||||
})
|
||||
});
|
||||
|
||||
assert_eq!(host_branch.name, "totally-new-branch");
|
||||
assert_eq!(host_branch.name(), "totally-new-branch");
|
||||
}
|
||||
|
||||
@@ -293,7 +293,7 @@ async fn test_ssh_collaboration_git_branches(
|
||||
|
||||
let branches_b = branches_b
|
||||
.into_iter()
|
||||
.map(|branch| branch.name.to_string())
|
||||
.map(|branch| branch.name().to_string())
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
assert_eq!(&branches_b, &branches_set);
|
||||
@@ -326,7 +326,7 @@ async fn test_ssh_collaboration_git_branches(
|
||||
})
|
||||
});
|
||||
|
||||
assert_eq!(server_branch.name, branches[2]);
|
||||
assert_eq!(server_branch.name(), branches[2]);
|
||||
|
||||
// Also try creating a new branch
|
||||
cx_b.update(|cx| {
|
||||
@@ -366,7 +366,7 @@ async fn test_ssh_collaboration_git_branches(
|
||||
})
|
||||
});
|
||||
|
||||
assert_eq!(server_branch.name, "totally-new-branch");
|
||||
assert_eq!(server_branch.name(), "totally-new-branch");
|
||||
|
||||
// Remove the git repository and check that all participants get the update.
|
||||
remote_fs
|
||||
|
||||
@@ -307,7 +307,6 @@ impl TestServer {
|
||||
);
|
||||
language_model::LanguageModelRegistry::test(cx);
|
||||
assistant_context_editor::init(client.clone(), cx);
|
||||
assistant_settings::init(cx);
|
||||
});
|
||||
|
||||
client
|
||||
@@ -555,9 +554,6 @@ impl TestServer {
|
||||
migrations_path: None,
|
||||
seed_path: None,
|
||||
stripe_api_key: None,
|
||||
stripe_zed_pro_price_id: None,
|
||||
stripe_zed_pro_trial_price_id: None,
|
||||
stripe_zed_free_price_id: None,
|
||||
supermaven_admin_api_key: None,
|
||||
user_backfiller_github_access_token: None,
|
||||
kinesis_region: None,
|
||||
|
||||
@@ -22,7 +22,9 @@ use ui::{
|
||||
Avatar, Button, Icon, IconButton, IconName, Label, Tab, Tooltip, h_flex, prelude::*, v_flex,
|
||||
};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
use workspace::notifications::{Notification as WorkspaceNotification, NotificationId};
|
||||
use workspace::notifications::{
|
||||
Notification as WorkspaceNotification, NotificationId, SuppressEvent,
|
||||
};
|
||||
use workspace::{
|
||||
Workspace,
|
||||
dock::{DockPosition, Panel, PanelEvent},
|
||||
@@ -823,6 +825,11 @@ impl Render for NotificationToast {
|
||||
IconButton::new("close", IconName::Close)
|
||||
.on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("suppress", IconName::XCircle)
|
||||
.tooltip(Tooltip::text("Do not show until restart"))
|
||||
.on_click(cx.listener(|_, _, _, cx| cx.emit(SuppressEvent))),
|
||||
)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.focus_notification_panel(window, cx);
|
||||
cx.emit(DismissEvent);
|
||||
@@ -831,3 +838,4 @@ impl Render for NotificationToast {
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for NotificationToast {}
|
||||
impl EventEmitter<SuppressEvent> for NotificationToast {}
|
||||
|
||||
@@ -16,6 +16,7 @@ collections.workspace = true
|
||||
gpui.workspace = true
|
||||
linkme.workspace = true
|
||||
parking_lot.workspace = true
|
||||
strum.workspace = true
|
||||
theme.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
|
||||
@@ -1,26 +1,197 @@
|
||||
use std::fmt::Display;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
//! # Component
|
||||
//!
|
||||
//! This module provides the Component trait, which is used to define
|
||||
//! components for visual testing and debugging.
|
||||
//!
|
||||
//! Additionally, it includes layouts for rendering component examples
|
||||
//! and example groups, as well as the distributed slice mechanism for
|
||||
//! registering components.
|
||||
|
||||
mod component_layout;
|
||||
|
||||
pub use component_layout::*;
|
||||
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use collections::HashMap;
|
||||
use gpui::{
|
||||
AnyElement, App, IntoElement, Pixels, RenderOnce, SharedString, Window, div, pattern_slash,
|
||||
prelude::*, px, rems,
|
||||
};
|
||||
use gpui::{AnyElement, App, SharedString, Window};
|
||||
use linkme::distributed_slice;
|
||||
use parking_lot::RwLock;
|
||||
use theme::ActiveTheme;
|
||||
use strum::{Display, EnumString};
|
||||
|
||||
pub fn components() -> ComponentRegistry {
|
||||
COMPONENT_DATA.read().clone()
|
||||
}
|
||||
|
||||
pub fn init() {
|
||||
let component_fns: Vec<_> = __ALL_COMPONENTS.iter().cloned().collect();
|
||||
for f in component_fns {
|
||||
f();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register_component<T: Component>() {
|
||||
let id = T::id();
|
||||
let metadata = ComponentMetadata {
|
||||
id: id.clone(),
|
||||
description: T::description().map(Into::into),
|
||||
name: SharedString::new_static(T::name()),
|
||||
preview: Some(T::preview),
|
||||
scope: T::scope(),
|
||||
sort_name: SharedString::new_static(T::sort_name()),
|
||||
status: T::status(),
|
||||
};
|
||||
|
||||
let mut data = COMPONENT_DATA.write();
|
||||
data.components.insert(id, metadata);
|
||||
}
|
||||
|
||||
#[distributed_slice]
|
||||
pub static __ALL_COMPONENTS: [fn()] = [..];
|
||||
|
||||
pub static COMPONENT_DATA: LazyLock<RwLock<ComponentRegistry>> =
|
||||
LazyLock::new(|| RwLock::new(ComponentRegistry::default()));
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct ComponentRegistry {
|
||||
components: HashMap<ComponentId, ComponentMetadata>,
|
||||
}
|
||||
|
||||
impl ComponentRegistry {
|
||||
pub fn previews(&self) -> Vec<&ComponentMetadata> {
|
||||
self.components
|
||||
.values()
|
||||
.filter(|c| c.preview.is_some())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn sorted_previews(&self) -> Vec<ComponentMetadata> {
|
||||
let mut previews: Vec<ComponentMetadata> = self.previews().into_iter().cloned().collect();
|
||||
previews.sort_by_key(|a| a.name());
|
||||
previews
|
||||
}
|
||||
|
||||
pub fn components(&self) -> Vec<&ComponentMetadata> {
|
||||
self.components.values().collect()
|
||||
}
|
||||
|
||||
pub fn sorted_components(&self) -> Vec<ComponentMetadata> {
|
||||
let mut components: Vec<ComponentMetadata> =
|
||||
self.components().into_iter().cloned().collect();
|
||||
components.sort_by_key(|a| a.name());
|
||||
components
|
||||
}
|
||||
|
||||
pub fn component_map(&self) -> HashMap<ComponentId, ComponentMetadata> {
|
||||
self.components.clone()
|
||||
}
|
||||
|
||||
pub fn get(&self, id: &ComponentId) -> Option<&ComponentMetadata> {
|
||||
self.components.get(id)
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.components.len()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct ComponentId(pub &'static str);
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ComponentMetadata {
|
||||
id: ComponentId,
|
||||
description: Option<SharedString>,
|
||||
name: SharedString,
|
||||
preview: Option<fn(&mut Window, &mut App) -> Option<AnyElement>>,
|
||||
scope: ComponentScope,
|
||||
sort_name: SharedString,
|
||||
status: ComponentStatus,
|
||||
}
|
||||
|
||||
impl ComponentMetadata {
|
||||
pub fn id(&self) -> ComponentId {
|
||||
self.id.clone()
|
||||
}
|
||||
|
||||
pub fn description(&self) -> Option<SharedString> {
|
||||
self.description.clone()
|
||||
}
|
||||
|
||||
pub fn name(&self) -> SharedString {
|
||||
self.name.clone()
|
||||
}
|
||||
|
||||
pub fn preview(&self) -> Option<fn(&mut Window, &mut App) -> Option<AnyElement>> {
|
||||
self.preview
|
||||
}
|
||||
|
||||
pub fn scope(&self) -> ComponentScope {
|
||||
self.scope.clone()
|
||||
}
|
||||
|
||||
pub fn sort_name(&self) -> SharedString {
|
||||
self.sort_name.clone()
|
||||
}
|
||||
|
||||
pub fn scopeless_name(&self) -> SharedString {
|
||||
self.name
|
||||
.clone()
|
||||
.split("::")
|
||||
.last()
|
||||
.unwrap_or(&self.name)
|
||||
.to_string()
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn status(&self) -> ComponentStatus {
|
||||
self.status.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// Implement this trait to define a UI component. This will allow you to
|
||||
/// derive `RegisterComponent` on it, in tutn allowing you to preview the
|
||||
/// contents of the preview fn in `workspace: open component preview`.
|
||||
///
|
||||
/// This can be useful for visual debugging and testing, documenting UI
|
||||
/// patterns, or simply showing all the variants of a component.
|
||||
///
|
||||
/// Generally you will want to implement at least `scope` and `preview`
|
||||
/// from this trait, so you can preview the component, and it will show up
|
||||
/// in a section that makes sense.
|
||||
pub trait Component {
|
||||
/// The component's unique identifier.
|
||||
///
|
||||
/// Used to access previews, or state for more
|
||||
/// complex, stateful components.
|
||||
fn id() -> ComponentId {
|
||||
ComponentId(Self::name())
|
||||
}
|
||||
/// Returns the scope of the component.
|
||||
///
|
||||
/// This scope is used to determine how components and
|
||||
/// their previews are displayed and organized.
|
||||
fn scope() -> ComponentScope {
|
||||
ComponentScope::None
|
||||
}
|
||||
/// The ready status of this component.
|
||||
///
|
||||
/// Use this to mark when components are:
|
||||
/// - `WorkInProgress`: Still being designed or are partially implemented.
|
||||
/// - `EngineeringReady`: Ready to be implemented.
|
||||
/// - `Deprecated`: No longer recommended for use.
|
||||
///
|
||||
/// Defaults to [`Live`](ComponentStatus::Live).
|
||||
fn status() -> ComponentStatus {
|
||||
ComponentStatus::Live
|
||||
}
|
||||
/// The name of the component.
|
||||
///
|
||||
/// This name is used to identify the component
|
||||
/// and is usually derived from the component's type.
|
||||
fn name() -> &'static str {
|
||||
std::any::type_name::<Self>()
|
||||
}
|
||||
fn id() -> ComponentId {
|
||||
ComponentId(Self::name())
|
||||
}
|
||||
/// Returns a name that the component should be sorted by.
|
||||
///
|
||||
/// Implement this if the component should be sorted in an alternate order than its name.
|
||||
@@ -37,408 +208,107 @@ pub trait Component {
|
||||
fn sort_name() -> &'static str {
|
||||
Self::name()
|
||||
}
|
||||
/// An optional description of the component.
|
||||
///
|
||||
/// This will be displayed in the component's preview. To show a
|
||||
/// component's doc comment as it's description, derive `Documented`.
|
||||
///
|
||||
/// Example:
|
||||
///
|
||||
/// ```
|
||||
/// /// This is a doc comment.
|
||||
/// #[derive(Documented)]
|
||||
/// struct MyComponent;
|
||||
///
|
||||
/// impl MyComponent {
|
||||
/// fn description() -> Option<&'static str> {
|
||||
/// Some(Self::DOCS)
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// This will result in "This is a doc comment." being passed
|
||||
/// to the component's description.
|
||||
fn description() -> Option<&'static str> {
|
||||
None
|
||||
}
|
||||
/// The component's preview.
|
||||
///
|
||||
/// An element returned here will be shown in the component's preview.
|
||||
///
|
||||
/// Useful component helpers:
|
||||
/// - [`component::single_example`]
|
||||
/// - [`component::component_group`]
|
||||
/// - [`component::component_group_with_title`]
|
||||
///
|
||||
/// Note: Any arbitrary element can be returned here.
|
||||
///
|
||||
/// This is useful for displaying related UI to the component you are
|
||||
/// trying to preview, such as a button that opens a modal or shows a
|
||||
/// tooltip on hover, or a grid of icons showcasing all the icons available.
|
||||
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[distributed_slice]
|
||||
pub static __ALL_COMPONENTS: [fn()] = [..];
|
||||
|
||||
pub static COMPONENT_DATA: LazyLock<RwLock<ComponentRegistry>> =
|
||||
LazyLock::new(|| RwLock::new(ComponentRegistry::new()));
|
||||
|
||||
pub struct ComponentRegistry {
|
||||
components: Vec<(
|
||||
ComponentScope,
|
||||
// name
|
||||
&'static str,
|
||||
// sort name
|
||||
&'static str,
|
||||
// description
|
||||
Option<&'static str>,
|
||||
)>,
|
||||
previews: HashMap<&'static str, fn(&mut Window, &mut App) -> Option<AnyElement>>,
|
||||
/// The ready status of this component.
|
||||
///
|
||||
/// Use this to mark when components are:
|
||||
/// - `WorkInProgress`: Still being designed or are partially implemented.
|
||||
/// - `EngineeringReady`: Ready to be implemented.
|
||||
/// - `Deprecated`: No longer recommended for use.
|
||||
///
|
||||
/// Defaults to [`Live`](ComponentStatus::Live).
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Display, EnumString)]
|
||||
pub enum ComponentStatus {
|
||||
#[strum(serialize = "Work In Progress")]
|
||||
WorkInProgress,
|
||||
#[strum(serialize = "Ready To Build")]
|
||||
EngineeringReady,
|
||||
Live,
|
||||
Deprecated,
|
||||
}
|
||||
|
||||
impl ComponentRegistry {
|
||||
fn new() -> Self {
|
||||
ComponentRegistry {
|
||||
components: Vec::new(),
|
||||
previews: HashMap::default(),
|
||||
impl ComponentStatus {
|
||||
pub fn description(&self) -> &str {
|
||||
match self {
|
||||
ComponentStatus::WorkInProgress => {
|
||||
"These components are still being designed or refined. They shouldn't be used in the app yet."
|
||||
}
|
||||
ComponentStatus::EngineeringReady => {
|
||||
"These components are design complete or partially implemented, and are ready for an engineer to complete their implementation."
|
||||
}
|
||||
ComponentStatus::Live => "These components are ready for use in the app.",
|
||||
ComponentStatus::Deprecated => {
|
||||
"These components are no longer recommended for use in the app, and may be removed in a future release."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init() {
|
||||
let component_fns: Vec<_> = __ALL_COMPONENTS.iter().cloned().collect();
|
||||
for f in component_fns {
|
||||
f();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register_component<T: Component>() {
|
||||
let component_data = (T::scope(), T::name(), T::sort_name(), T::description());
|
||||
let mut data = COMPONENT_DATA.write();
|
||||
data.components.push(component_data);
|
||||
data.previews.insert(T::id().0, T::preview);
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct ComponentId(pub &'static str);
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ComponentMetadata {
|
||||
id: ComponentId,
|
||||
name: SharedString,
|
||||
sort_name: SharedString,
|
||||
scope: ComponentScope,
|
||||
description: Option<SharedString>,
|
||||
preview: Option<fn(&mut Window, &mut App) -> Option<AnyElement>>,
|
||||
}
|
||||
|
||||
impl ComponentMetadata {
|
||||
pub fn id(&self) -> ComponentId {
|
||||
self.id.clone()
|
||||
}
|
||||
pub fn name(&self) -> SharedString {
|
||||
self.name.clone()
|
||||
}
|
||||
|
||||
pub fn sort_name(&self) -> SharedString {
|
||||
self.sort_name.clone()
|
||||
}
|
||||
|
||||
pub fn scopeless_name(&self) -> SharedString {
|
||||
self.name
|
||||
.clone()
|
||||
.split("::")
|
||||
.last()
|
||||
.unwrap_or(&self.name)
|
||||
.to_string()
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn scope(&self) -> ComponentScope {
|
||||
self.scope.clone()
|
||||
}
|
||||
pub fn description(&self) -> Option<SharedString> {
|
||||
self.description.clone()
|
||||
}
|
||||
pub fn preview(&self) -> Option<fn(&mut Window, &mut App) -> Option<AnyElement>> {
|
||||
self.preview
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AllComponents(pub HashMap<ComponentId, ComponentMetadata>);
|
||||
|
||||
impl AllComponents {
|
||||
pub fn new() -> Self {
|
||||
AllComponents(HashMap::default())
|
||||
}
|
||||
pub fn all_previews(&self) -> Vec<&ComponentMetadata> {
|
||||
self.0.values().filter(|c| c.preview.is_some()).collect()
|
||||
}
|
||||
pub fn all_previews_sorted(&self) -> Vec<ComponentMetadata> {
|
||||
let mut previews: Vec<ComponentMetadata> =
|
||||
self.all_previews().into_iter().cloned().collect();
|
||||
previews.sort_by_key(|a| a.name());
|
||||
previews
|
||||
}
|
||||
pub fn all(&self) -> Vec<&ComponentMetadata> {
|
||||
self.0.values().collect()
|
||||
}
|
||||
pub fn all_sorted(&self) -> Vec<ComponentMetadata> {
|
||||
let mut components: Vec<ComponentMetadata> = self.all().into_iter().cloned().collect();
|
||||
components.sort_by_key(|a| a.name());
|
||||
components
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for AllComponents {
|
||||
type Target = HashMap<ComponentId, ComponentMetadata>;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for AllComponents {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn components() -> AllComponents {
|
||||
let data = COMPONENT_DATA.read();
|
||||
let mut all_components = AllComponents::new();
|
||||
for (scope, name, sort_name, description) in &data.components {
|
||||
let preview = data.previews.get(name).cloned();
|
||||
let component_name = SharedString::new_static(name);
|
||||
let sort_name = SharedString::new_static(sort_name);
|
||||
let id = ComponentId(name);
|
||||
all_components.insert(
|
||||
id.clone(),
|
||||
ComponentMetadata {
|
||||
id,
|
||||
name: component_name,
|
||||
sort_name,
|
||||
scope: scope.clone(),
|
||||
description: description.map(Into::into),
|
||||
preview,
|
||||
},
|
||||
);
|
||||
}
|
||||
all_components
|
||||
}
|
||||
|
||||
// #[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
// pub enum ComponentStatus {
|
||||
// WorkInProgress,
|
||||
// EngineeringReady,
|
||||
// Live,
|
||||
// Deprecated,
|
||||
// }
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Display, EnumString)]
|
||||
pub enum ComponentScope {
|
||||
Agent,
|
||||
Collaboration,
|
||||
#[strum(serialize = "Data Display")]
|
||||
DataDisplay,
|
||||
Editor,
|
||||
#[strum(serialize = "Images & Icons")]
|
||||
Images,
|
||||
#[strum(serialize = "Forms & Input")]
|
||||
Input,
|
||||
#[strum(serialize = "Layout & Structure")]
|
||||
Layout,
|
||||
#[strum(serialize = "Loading & Progress")]
|
||||
Loading,
|
||||
Navigation,
|
||||
#[strum(serialize = "Unsorted")]
|
||||
None,
|
||||
Notification,
|
||||
#[strum(serialize = "Overlays & Layering")]
|
||||
Overlays,
|
||||
Status,
|
||||
Typography,
|
||||
#[strum(serialize = "Version Control")]
|
||||
VersionControl,
|
||||
}
|
||||
|
||||
impl Display for ComponentScope {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ComponentScope::Agent => write!(f, "Agent"),
|
||||
ComponentScope::Collaboration => write!(f, "Collaboration"),
|
||||
ComponentScope::DataDisplay => write!(f, "Data Display"),
|
||||
ComponentScope::Editor => write!(f, "Editor"),
|
||||
ComponentScope::Images => write!(f, "Images & Icons"),
|
||||
ComponentScope::Input => write!(f, "Forms & Input"),
|
||||
ComponentScope::Layout => write!(f, "Layout & Structure"),
|
||||
ComponentScope::Loading => write!(f, "Loading & Progress"),
|
||||
ComponentScope::Navigation => write!(f, "Navigation"),
|
||||
ComponentScope::None => write!(f, "Unsorted"),
|
||||
ComponentScope::Notification => write!(f, "Notification"),
|
||||
ComponentScope::Overlays => write!(f, "Overlays & Layering"),
|
||||
ComponentScope::Status => write!(f, "Status"),
|
||||
ComponentScope::Typography => write!(f, "Typography"),
|
||||
ComponentScope::VersionControl => write!(f, "Version Control"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A single example of a component.
|
||||
#[derive(IntoElement)]
|
||||
pub struct ComponentExample {
|
||||
pub variant_name: SharedString,
|
||||
pub description: Option<SharedString>,
|
||||
pub element: AnyElement,
|
||||
pub width: Option<Pixels>,
|
||||
}
|
||||
|
||||
impl RenderOnce for ComponentExample {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
div()
|
||||
.pt_2()
|
||||
.map(|this| {
|
||||
if let Some(width) = self.width {
|
||||
this.w(width)
|
||||
} else {
|
||||
this.w_full()
|
||||
}
|
||||
})
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.child(
|
||||
div()
|
||||
.child(self.variant_name.clone())
|
||||
.text_size(rems(1.0))
|
||||
.text_color(cx.theme().colors().text),
|
||||
)
|
||||
.when_some(self.description, |this, description| {
|
||||
this.child(
|
||||
div()
|
||||
.text_size(rems(0.875))
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
.child(description.clone()),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.w_full()
|
||||
.rounded_xl()
|
||||
.min_h(px(100.))
|
||||
.justify_center()
|
||||
.p_8()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border.opacity(0.5))
|
||||
.bg(pattern_slash(
|
||||
cx.theme().colors().surface_background.opacity(0.5),
|
||||
12.0,
|
||||
12.0,
|
||||
))
|
||||
.shadow_sm()
|
||||
.child(self.element),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
impl ComponentExample {
|
||||
pub fn new(variant_name: impl Into<SharedString>, element: AnyElement) -> Self {
|
||||
Self {
|
||||
variant_name: variant_name.into(),
|
||||
element,
|
||||
description: None,
|
||||
width: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn description(mut self, description: impl Into<SharedString>) -> Self {
|
||||
self.description = Some(description.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn width(mut self, width: Pixels) -> Self {
|
||||
self.width = Some(width);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// A group of component examples.
|
||||
#[derive(IntoElement)]
|
||||
pub struct ComponentExampleGroup {
|
||||
pub title: Option<SharedString>,
|
||||
pub examples: Vec<ComponentExample>,
|
||||
pub width: Option<Pixels>,
|
||||
pub grow: bool,
|
||||
pub vertical: bool,
|
||||
}
|
||||
|
||||
impl RenderOnce for ComponentExampleGroup {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
div()
|
||||
.flex_col()
|
||||
.text_sm()
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
.map(|this| {
|
||||
if let Some(width) = self.width {
|
||||
this.w(width)
|
||||
} else {
|
||||
this.w_full()
|
||||
}
|
||||
})
|
||||
.when_some(self.title, |this, title| {
|
||||
this.gap_4().child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_3()
|
||||
.pb_1()
|
||||
.child(div().h_px().w_4().bg(cx.theme().colors().border))
|
||||
.child(
|
||||
div()
|
||||
.flex_none()
|
||||
.text_size(px(10.))
|
||||
.child(title.to_uppercase()),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.h_px()
|
||||
.w_full()
|
||||
.flex_1()
|
||||
.bg(cx.theme().colors().border),
|
||||
),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.items_start()
|
||||
.w_full()
|
||||
.gap_6()
|
||||
.children(self.examples)
|
||||
.into_any_element(),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
impl ComponentExampleGroup {
|
||||
pub fn new(examples: Vec<ComponentExample>) -> Self {
|
||||
Self {
|
||||
title: None,
|
||||
examples,
|
||||
width: None,
|
||||
grow: false,
|
||||
vertical: false,
|
||||
}
|
||||
}
|
||||
pub fn with_title(title: impl Into<SharedString>, examples: Vec<ComponentExample>) -> Self {
|
||||
Self {
|
||||
title: Some(title.into()),
|
||||
examples,
|
||||
width: None,
|
||||
grow: false,
|
||||
vertical: false,
|
||||
}
|
||||
}
|
||||
pub fn width(mut self, width: Pixels) -> Self {
|
||||
self.width = Some(width);
|
||||
self
|
||||
}
|
||||
pub fn grow(mut self) -> Self {
|
||||
self.grow = true;
|
||||
self
|
||||
}
|
||||
pub fn vertical(mut self) -> Self {
|
||||
self.vertical = true;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub fn single_example(
|
||||
variant_name: impl Into<SharedString>,
|
||||
example: AnyElement,
|
||||
) -> ComponentExample {
|
||||
ComponentExample::new(variant_name, example)
|
||||
}
|
||||
|
||||
pub fn empty_example(variant_name: impl Into<SharedString>) -> ComponentExample {
|
||||
ComponentExample::new(variant_name, div().w_full().text_center().items_center().text_xs().opacity(0.4).child("This space is intentionally left blank. It indicates a case that should render nothing.").into_any_element())
|
||||
}
|
||||
|
||||
pub fn example_group(examples: Vec<ComponentExample>) -> ComponentExampleGroup {
|
||||
ComponentExampleGroup::new(examples)
|
||||
}
|
||||
|
||||
pub fn example_group_with_title(
|
||||
title: impl Into<SharedString>,
|
||||
examples: Vec<ComponentExample>,
|
||||
) -> ComponentExampleGroup {
|
||||
ComponentExampleGroup::with_title(title, examples)
|
||||
}
|
||||
|
||||
205
crates/component/src/component_layout.rs
Normal file
205
crates/component/src/component_layout.rs
Normal file
@@ -0,0 +1,205 @@
|
||||
use gpui::{
|
||||
AnyElement, App, IntoElement, Pixels, RenderOnce, SharedString, Window, div, pattern_slash,
|
||||
prelude::*, px, rems,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
|
||||
/// A single example of a component.
|
||||
#[derive(IntoElement)]
|
||||
pub struct ComponentExample {
|
||||
pub variant_name: SharedString,
|
||||
pub description: Option<SharedString>,
|
||||
pub element: AnyElement,
|
||||
pub width: Option<Pixels>,
|
||||
}
|
||||
|
||||
impl RenderOnce for ComponentExample {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
div()
|
||||
.pt_2()
|
||||
.map(|this| {
|
||||
if let Some(width) = self.width {
|
||||
this.w(width)
|
||||
} else {
|
||||
this.w_full()
|
||||
}
|
||||
})
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.child(
|
||||
div()
|
||||
.child(self.variant_name.clone())
|
||||
.text_size(rems(1.0))
|
||||
.text_color(cx.theme().colors().text),
|
||||
)
|
||||
.when_some(self.description, |this, description| {
|
||||
this.child(
|
||||
div()
|
||||
.text_size(rems(0.875))
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
.child(description.clone()),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.w_full()
|
||||
.rounded_xl()
|
||||
.min_h(px(100.))
|
||||
.justify_center()
|
||||
.p_8()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border.opacity(0.5))
|
||||
.bg(pattern_slash(
|
||||
cx.theme().colors().surface_background.opacity(0.5),
|
||||
12.0,
|
||||
12.0,
|
||||
))
|
||||
.shadow_sm()
|
||||
.child(self.element),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
impl ComponentExample {
|
||||
pub fn new(variant_name: impl Into<SharedString>, element: AnyElement) -> Self {
|
||||
Self {
|
||||
variant_name: variant_name.into(),
|
||||
element,
|
||||
description: None,
|
||||
width: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn description(mut self, description: impl Into<SharedString>) -> Self {
|
||||
self.description = Some(description.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn width(mut self, width: Pixels) -> Self {
|
||||
self.width = Some(width);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// A group of component examples.
|
||||
#[derive(IntoElement)]
|
||||
pub struct ComponentExampleGroup {
|
||||
pub title: Option<SharedString>,
|
||||
pub examples: Vec<ComponentExample>,
|
||||
pub width: Option<Pixels>,
|
||||
pub grow: bool,
|
||||
pub vertical: bool,
|
||||
}
|
||||
|
||||
impl RenderOnce for ComponentExampleGroup {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
div()
|
||||
.flex_col()
|
||||
.text_sm()
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
.map(|this| {
|
||||
if let Some(width) = self.width {
|
||||
this.w(width)
|
||||
} else {
|
||||
this.w_full()
|
||||
}
|
||||
})
|
||||
.when_some(self.title, |this, title| {
|
||||
this.gap_4().child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_3()
|
||||
.pb_1()
|
||||
.child(div().h_px().w_4().bg(cx.theme().colors().border))
|
||||
.child(
|
||||
div()
|
||||
.flex_none()
|
||||
.text_size(px(10.))
|
||||
.child(title.to_uppercase()),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.h_px()
|
||||
.w_full()
|
||||
.flex_1()
|
||||
.bg(cx.theme().colors().border),
|
||||
),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.items_start()
|
||||
.w_full()
|
||||
.gap_6()
|
||||
.children(self.examples)
|
||||
.into_any_element(),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
impl ComponentExampleGroup {
|
||||
pub fn new(examples: Vec<ComponentExample>) -> Self {
|
||||
Self {
|
||||
title: None,
|
||||
examples,
|
||||
width: None,
|
||||
grow: false,
|
||||
vertical: false,
|
||||
}
|
||||
}
|
||||
pub fn with_title(title: impl Into<SharedString>, examples: Vec<ComponentExample>) -> Self {
|
||||
Self {
|
||||
title: Some(title.into()),
|
||||
examples,
|
||||
width: None,
|
||||
grow: false,
|
||||
vertical: false,
|
||||
}
|
||||
}
|
||||
pub fn width(mut self, width: Pixels) -> Self {
|
||||
self.width = Some(width);
|
||||
self
|
||||
}
|
||||
pub fn grow(mut self) -> Self {
|
||||
self.grow = true;
|
||||
self
|
||||
}
|
||||
pub fn vertical(mut self) -> Self {
|
||||
self.vertical = true;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub fn single_example(
|
||||
variant_name: impl Into<SharedString>,
|
||||
example: AnyElement,
|
||||
) -> ComponentExample {
|
||||
ComponentExample::new(variant_name, example)
|
||||
}
|
||||
|
||||
pub fn empty_example(variant_name: impl Into<SharedString>) -> ComponentExample {
|
||||
ComponentExample::new(variant_name, div().w_full().text_center().items_center().text_xs().opacity(0.4).child("This space is intentionally left blank. It indicates a case that should render nothing.").into_any_element())
|
||||
}
|
||||
|
||||
pub fn example_group(examples: Vec<ComponentExample>) -> ComponentExampleGroup {
|
||||
ComponentExampleGroup::new(examples)
|
||||
}
|
||||
|
||||
pub fn example_group_with_title(
|
||||
title: impl Into<SharedString>,
|
||||
examples: Vec<ComponentExample>,
|
||||
) -> ComponentExampleGroup {
|
||||
ComponentExampleGroup::with_title(title, examples)
|
||||
}
|
||||
@@ -5,12 +5,13 @@
|
||||
mod persistence;
|
||||
mod preview_support;
|
||||
|
||||
use std::iter::Iterator;
|
||||
use std::sync::Arc;
|
||||
|
||||
use std::iter::Iterator;
|
||||
|
||||
use agent::{ActiveThread, TextThreadStore, ThreadStore};
|
||||
use client::UserStore;
|
||||
use component::{ComponentId, ComponentMetadata, components};
|
||||
use component::{ComponentId, ComponentMetadata, ComponentStatus, components};
|
||||
use gpui::{
|
||||
App, Entity, EventEmitter, FocusHandle, Focusable, Task, WeakEntity, Window, list, prelude::*,
|
||||
};
|
||||
@@ -25,7 +26,7 @@ use preview_support::active_thread::{
|
||||
load_preview_text_thread_store, load_preview_thread_store, static_active_thread,
|
||||
};
|
||||
use project::Project;
|
||||
use ui::{Divider, HighlightedLabel, ListItem, ListSubHeader, prelude::*};
|
||||
use ui::{ButtonLike, Divider, HighlightedLabel, ListItem, ListSubHeader, Tooltip, prelude::*};
|
||||
use ui_input::SingleLineInput;
|
||||
use util::ResultExt as _;
|
||||
use workspace::{AppState, ItemId, SerializableItem, delete_unloaded_items};
|
||||
@@ -104,26 +105,24 @@ enum PreviewPage {
|
||||
}
|
||||
|
||||
struct ComponentPreview {
|
||||
workspace_id: Option<WorkspaceId>,
|
||||
focus_handle: FocusHandle,
|
||||
_view_scroll_handle: ScrollHandle,
|
||||
nav_scroll_handle: UniformListScrollHandle,
|
||||
component_map: HashMap<ComponentId, ComponentMetadata>,
|
||||
active_page: PreviewPage,
|
||||
components: Vec<ComponentMetadata>,
|
||||
active_thread: Option<Entity<ActiveThread>>,
|
||||
component_list: ListState,
|
||||
component_map: HashMap<ComponentId, ComponentMetadata>,
|
||||
components: Vec<ComponentMetadata>,
|
||||
cursor_index: usize,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
project: Entity<Project>,
|
||||
user_store: Entity<UserStore>,
|
||||
filter_editor: Entity<SingleLineInput>,
|
||||
filter_text: String,
|
||||
|
||||
// preview support
|
||||
thread_store: Option<Entity<ThreadStore>>,
|
||||
focus_handle: FocusHandle,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
nav_scroll_handle: UniformListScrollHandle,
|
||||
project: Entity<Project>,
|
||||
text_thread_store: Option<Entity<TextThreadStore>>,
|
||||
active_thread: Option<Entity<ActiveThread>>,
|
||||
thread_store: Option<Entity<ThreadStore>>,
|
||||
user_store: Entity<UserStore>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
workspace_id: Option<WorkspaceId>,
|
||||
_view_scroll_handle: ScrollHandle,
|
||||
}
|
||||
|
||||
impl ComponentPreview {
|
||||
@@ -164,7 +163,8 @@ impl ComponentPreview {
|
||||
})
|
||||
.detach();
|
||||
|
||||
let sorted_components = components().all_sorted();
|
||||
let component_registry = Arc::new(components());
|
||||
let sorted_components = component_registry.sorted_components();
|
||||
let selected_index = selected_index.into().unwrap_or(0);
|
||||
let active_page = active_page.unwrap_or(PreviewPage::AllComponents);
|
||||
let filter_editor =
|
||||
@@ -188,24 +188,24 @@ impl ComponentPreview {
|
||||
);
|
||||
|
||||
let mut component_preview = Self {
|
||||
workspace_id: None,
|
||||
focus_handle: cx.focus_handle(),
|
||||
_view_scroll_handle: ScrollHandle::new(),
|
||||
nav_scroll_handle: UniformListScrollHandle::new(),
|
||||
language_registry,
|
||||
user_store,
|
||||
workspace,
|
||||
project,
|
||||
active_page,
|
||||
component_map: components().0,
|
||||
components: sorted_components,
|
||||
active_thread: None,
|
||||
component_list,
|
||||
component_map: component_registry.component_map(),
|
||||
components: sorted_components,
|
||||
cursor_index: selected_index,
|
||||
filter_editor,
|
||||
filter_text: String::new(),
|
||||
thread_store: None,
|
||||
focus_handle: cx.focus_handle(),
|
||||
language_registry,
|
||||
nav_scroll_handle: UniformListScrollHandle::new(),
|
||||
project,
|
||||
text_thread_store: None,
|
||||
active_thread: None,
|
||||
thread_store: None,
|
||||
user_store,
|
||||
workspace,
|
||||
workspace_id: None,
|
||||
_view_scroll_handle: ScrollHandle::new(),
|
||||
};
|
||||
|
||||
if component_preview.cursor_index > 0 {
|
||||
@@ -412,6 +412,88 @@ impl ComponentPreview {
|
||||
entries
|
||||
}
|
||||
|
||||
fn update_component_list(&mut self, cx: &mut Context<Self>) {
|
||||
let entries = self.scope_ordered_entries();
|
||||
let new_len = entries.len();
|
||||
let weak_entity = cx.entity().downgrade();
|
||||
|
||||
if new_len > 0 {
|
||||
self.nav_scroll_handle
|
||||
.scroll_to_item(0, ScrollStrategy::Top);
|
||||
}
|
||||
|
||||
let filtered_components = self.filtered_components();
|
||||
|
||||
if !self.filter_text.is_empty() && !matches!(self.active_page, PreviewPage::AllComponents) {
|
||||
if let PreviewPage::Component(ref component_id) = self.active_page {
|
||||
let component_still_visible = filtered_components
|
||||
.iter()
|
||||
.any(|component| component.id() == *component_id);
|
||||
|
||||
if !component_still_visible {
|
||||
if !filtered_components.is_empty() {
|
||||
let first_component = &filtered_components[0];
|
||||
self.set_active_page(PreviewPage::Component(first_component.id()), cx);
|
||||
} else {
|
||||
self.set_active_page(PreviewPage::AllComponents, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.component_list = ListState::new(
|
||||
filtered_components.len(),
|
||||
gpui::ListAlignment::Top,
|
||||
px(1500.0),
|
||||
{
|
||||
let components = filtered_components.clone();
|
||||
let this = cx.entity().downgrade();
|
||||
move |ix, window: &mut Window, cx: &mut App| {
|
||||
if ix >= components.len() {
|
||||
return div().w_full().h_0().into_any_element();
|
||||
}
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
let component = &components[ix];
|
||||
this.render_preview(component, window, cx)
|
||||
.into_any_element()
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
let new_list = ListState::new(
|
||||
new_len,
|
||||
gpui::ListAlignment::Top,
|
||||
px(1500.0),
|
||||
move |ix, window, cx| {
|
||||
if ix >= entries.len() {
|
||||
return div().w_full().h_0().into_any_element();
|
||||
}
|
||||
|
||||
let entry = &entries[ix];
|
||||
|
||||
weak_entity
|
||||
.update(cx, |this, cx| match entry {
|
||||
PreviewEntry::Component(component, _) => this
|
||||
.render_preview(component, window, cx)
|
||||
.into_any_element(),
|
||||
PreviewEntry::SectionHeader(shared_string) => this
|
||||
.render_scope_header(ix, shared_string.clone(), window, cx)
|
||||
.into_any_element(),
|
||||
PreviewEntry::AllComponents => div().w_full().h_0().into_any_element(),
|
||||
PreviewEntry::ActiveThread => div().w_full().h_0().into_any_element(),
|
||||
PreviewEntry::Separator => div().w_full().h_0().into_any_element(),
|
||||
})
|
||||
.unwrap()
|
||||
},
|
||||
);
|
||||
|
||||
self.component_list = new_list;
|
||||
cx.emit(ItemEvent::UpdateTab);
|
||||
}
|
||||
|
||||
fn render_sidebar_entry(
|
||||
&self,
|
||||
ix: usize,
|
||||
@@ -495,88 +577,6 @@ impl ComponentPreview {
|
||||
}
|
||||
}
|
||||
|
||||
fn update_component_list(&mut self, cx: &mut Context<Self>) {
|
||||
let entries = self.scope_ordered_entries();
|
||||
let new_len = entries.len();
|
||||
let weak_entity = cx.entity().downgrade();
|
||||
|
||||
if new_len > 0 {
|
||||
self.nav_scroll_handle
|
||||
.scroll_to_item(0, ScrollStrategy::Top);
|
||||
}
|
||||
|
||||
let filtered_components = self.filtered_components();
|
||||
|
||||
if !self.filter_text.is_empty() && !matches!(self.active_page, PreviewPage::AllComponents) {
|
||||
if let PreviewPage::Component(ref component_id) = self.active_page {
|
||||
let component_still_visible = filtered_components
|
||||
.iter()
|
||||
.any(|component| component.id() == *component_id);
|
||||
|
||||
if !component_still_visible {
|
||||
if !filtered_components.is_empty() {
|
||||
let first_component = &filtered_components[0];
|
||||
self.set_active_page(PreviewPage::Component(first_component.id()), cx);
|
||||
} else {
|
||||
self.set_active_page(PreviewPage::AllComponents, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.component_list = ListState::new(
|
||||
filtered_components.len(),
|
||||
gpui::ListAlignment::Top,
|
||||
px(1500.0),
|
||||
{
|
||||
let components = filtered_components.clone();
|
||||
let this = cx.entity().downgrade();
|
||||
move |ix, window: &mut Window, cx: &mut App| {
|
||||
if ix >= components.len() {
|
||||
return div().w_full().h_0().into_any_element();
|
||||
}
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
let component = &components[ix];
|
||||
this.render_preview(component, window, cx)
|
||||
.into_any_element()
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
let new_list = ListState::new(
|
||||
new_len,
|
||||
gpui::ListAlignment::Top,
|
||||
px(1500.0),
|
||||
move |ix, window, cx| {
|
||||
if ix >= entries.len() {
|
||||
return div().w_full().h_0().into_any_element();
|
||||
}
|
||||
|
||||
let entry = &entries[ix];
|
||||
|
||||
weak_entity
|
||||
.update(cx, |this, cx| match entry {
|
||||
PreviewEntry::Component(component, _) => this
|
||||
.render_preview(component, window, cx)
|
||||
.into_any_element(),
|
||||
PreviewEntry::SectionHeader(shared_string) => this
|
||||
.render_scope_header(ix, shared_string.clone(), window, cx)
|
||||
.into_any_element(),
|
||||
PreviewEntry::AllComponents => div().w_full().h_0().into_any_element(),
|
||||
PreviewEntry::ActiveThread => div().w_full().h_0().into_any_element(),
|
||||
PreviewEntry::Separator => div().w_full().h_0().into_any_element(),
|
||||
})
|
||||
.unwrap()
|
||||
},
|
||||
);
|
||||
|
||||
self.component_list = new_list;
|
||||
cx.emit(ItemEvent::UpdateTab);
|
||||
}
|
||||
|
||||
fn render_scope_header(
|
||||
&self,
|
||||
_ix: usize,
|
||||
@@ -695,7 +695,7 @@ impl ComponentPreview {
|
||||
if let Some(component) = component {
|
||||
v_flex()
|
||||
.id("render-component-page")
|
||||
.size_full()
|
||||
.flex_1()
|
||||
.child(ComponentPreviewPage::new(
|
||||
component.clone(),
|
||||
self.workspace.clone(),
|
||||
@@ -971,7 +971,7 @@ impl SerializableItem for ComponentPreview {
|
||||
} else {
|
||||
let component_str = deserialized_active_page.0;
|
||||
let component_registry = components();
|
||||
let all_components = component_registry.all();
|
||||
let all_components = component_registry.components();
|
||||
let found_component = all_components.iter().find(|c| c.id().0 == component_str);
|
||||
|
||||
if let Some(component) = found_component {
|
||||
@@ -1065,6 +1065,43 @@ impl ComponentPreviewPage {
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders the component status when it would be useful
|
||||
///
|
||||
/// Doesn't render if the component is `ComponentStatus::Live`
|
||||
/// as that is the default state
|
||||
fn render_component_status(&self, cx: &App) -> Option<impl IntoElement> {
|
||||
let status = self.component.status();
|
||||
let status_description = status.description().to_string();
|
||||
|
||||
let color = match status {
|
||||
ComponentStatus::Deprecated => Color::Error,
|
||||
ComponentStatus::EngineeringReady => Color::Info,
|
||||
ComponentStatus::Live => Color::Success,
|
||||
ComponentStatus::WorkInProgress => Color::Warning,
|
||||
};
|
||||
|
||||
if status != ComponentStatus::Live {
|
||||
Some(
|
||||
ButtonLike::new("component_status")
|
||||
.child(
|
||||
div()
|
||||
.px_1p5()
|
||||
.rounded_sm()
|
||||
.bg(color.color(cx).alpha(0.12))
|
||||
.child(
|
||||
Label::new(status.clone().to_string())
|
||||
.size(LabelSize::Small)
|
||||
.color(color),
|
||||
),
|
||||
)
|
||||
.tooltip(Tooltip::text(status_description))
|
||||
.disabled(true),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn render_header(&self, _: &Window, cx: &App) -> impl IntoElement {
|
||||
v_flex()
|
||||
.px_12()
|
||||
@@ -1083,7 +1120,14 @@ impl ComponentPreviewPage {
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
Headline::new(self.component.scopeless_name()).size(HeadlineSize::XLarge),
|
||||
h_flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.child(
|
||||
Headline::new(self.component.scopeless_name())
|
||||
.size(HeadlineSize::XLarge),
|
||||
)
|
||||
.children(self.render_component_status(cx)),
|
||||
),
|
||||
)
|
||||
.when_some(self.component.description(), |this, description| {
|
||||
|
||||
@@ -278,7 +278,7 @@ impl RegisteredBuffer {
|
||||
content_changes,
|
||||
},
|
||||
)
|
||||
.log_err();
|
||||
.ok();
|
||||
}
|
||||
let _ = done_tx.send((buffer.snapshot_version, buffer.snapshot.clone()));
|
||||
Some(())
|
||||
@@ -732,7 +732,7 @@ impl Copilot {
|
||||
},
|
||||
},
|
||||
)
|
||||
.log_err();
|
||||
.ok();
|
||||
|
||||
RegisteredBuffer {
|
||||
uri,
|
||||
@@ -827,7 +827,7 @@ impl Copilot {
|
||||
text_document: lsp::TextDocumentIdentifier::new(buffer.uri),
|
||||
},
|
||||
)
|
||||
.log_err();
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,10 +12,9 @@ use language::LanguageToolchainStore;
|
||||
use node_runtime::NodeRuntime;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::WorktreeId;
|
||||
use smol::{self, fs::File, lock::Mutex};
|
||||
use smol::{self, fs::File};
|
||||
use std::{
|
||||
borrow::Borrow,
|
||||
collections::HashSet,
|
||||
ffi::OsStr,
|
||||
fmt::Debug,
|
||||
net::Ipv4Addr,
|
||||
@@ -24,7 +23,6 @@ use std::{
|
||||
sync::Arc,
|
||||
};
|
||||
use task::{AttachRequest, DebugRequest, DebugScenario, LaunchRequest, TcpArgumentsTemplate};
|
||||
use util::ResultExt;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum DapStatus {
|
||||
@@ -41,8 +39,7 @@ pub trait DapDelegate {
|
||||
fn node_runtime(&self) -> NodeRuntime;
|
||||
fn toolchain_store(&self) -> Arc<dyn LanguageToolchainStore>;
|
||||
fn fs(&self) -> Arc<dyn Fs>;
|
||||
fn updated_adapters(&self) -> Arc<Mutex<HashSet<DebugAdapterName>>>;
|
||||
fn update_status(&self, dap_name: DebugAdapterName, status: DapStatus);
|
||||
fn output_to_console(&self, msg: String);
|
||||
fn which(&self, command: &OsStr) -> Option<PathBuf>;
|
||||
async fn shell_env(&self) -> collections::HashMap<String, String>;
|
||||
}
|
||||
@@ -88,7 +85,7 @@ impl<'a> From<&'a str> for DebugAdapterName {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct TcpArguments {
|
||||
pub host: Ipv4Addr,
|
||||
pub port: u16,
|
||||
@@ -127,7 +124,7 @@ impl TcpArguments {
|
||||
)]
|
||||
pub struct DebugTaskDefinition {
|
||||
pub label: SharedString,
|
||||
pub adapter: SharedString,
|
||||
pub adapter: DebugAdapterName,
|
||||
pub request: DebugRequest,
|
||||
/// Additional initialization arguments to be sent on DAP initialization
|
||||
pub initialize_args: Option<serde_json::Value>,
|
||||
@@ -153,7 +150,7 @@ impl DebugTaskDefinition {
|
||||
pub fn to_scenario(&self) -> DebugScenario {
|
||||
DebugScenario {
|
||||
label: self.label.clone(),
|
||||
adapter: self.adapter.clone(),
|
||||
adapter: self.adapter.clone().into(),
|
||||
build: None,
|
||||
request: Some(self.request.clone()),
|
||||
stop_on_entry: self.stop_on_entry,
|
||||
@@ -207,7 +204,7 @@ impl DebugTaskDefinition {
|
||||
.map(TcpArgumentsTemplate::from_proto)
|
||||
.transpose()?,
|
||||
stop_on_entry: proto.stop_on_entry,
|
||||
adapter: proto.adapter.into(),
|
||||
adapter: DebugAdapterName(proto.adapter.into()),
|
||||
request: match request {
|
||||
proto::debug_task_definition::Request::DebugAttachRequest(config) => {
|
||||
DebugRequest::Attach(AttachRequest {
|
||||
@@ -229,7 +226,7 @@ impl DebugTaskDefinition {
|
||||
}
|
||||
|
||||
/// Created from a [DebugTaskDefinition], this struct describes how to spawn the debugger to create a previously-configured debug session.
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct DebugAdapterBinary {
|
||||
pub command: String,
|
||||
pub arguments: Vec<String>,
|
||||
@@ -293,7 +290,7 @@ impl DebugAdapterBinary {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AdapterVersion {
|
||||
pub tag_name: String,
|
||||
pub url: String,
|
||||
@@ -335,6 +332,7 @@ pub async fn download_adapter_from_github(
|
||||
adapter_name,
|
||||
&github_version.url,
|
||||
);
|
||||
delegate.output_to_console(format!("Downloading from {}...", github_version.url));
|
||||
|
||||
let mut response = delegate
|
||||
.http_client()
|
||||
@@ -418,84 +416,6 @@ pub trait DebugAdapter: 'static + Send + Sync {
|
||||
config: &DebugTaskDefinition,
|
||||
user_installed_path: Option<PathBuf>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<DebugAdapterBinary> {
|
||||
if delegate
|
||||
.updated_adapters()
|
||||
.lock()
|
||||
.await
|
||||
.contains(&self.name())
|
||||
{
|
||||
log::info!("Using cached debug adapter binary {}", self.name());
|
||||
|
||||
if let Some(binary) = self
|
||||
.get_installed_binary(delegate, &config, user_installed_path.clone(), cx)
|
||||
.await
|
||||
.log_err()
|
||||
{
|
||||
return Ok(binary);
|
||||
}
|
||||
|
||||
log::info!(
|
||||
"Cached binary {} is corrupt falling back to install",
|
||||
self.name()
|
||||
);
|
||||
}
|
||||
|
||||
log::info!("Getting latest version of debug adapter {}", self.name());
|
||||
delegate.update_status(self.name(), DapStatus::CheckingForUpdate);
|
||||
if let Some(version) = self.fetch_latest_adapter_version(delegate).await.log_err() {
|
||||
log::info!(
|
||||
"Installiing latest version of debug adapter {}",
|
||||
self.name()
|
||||
);
|
||||
delegate.update_status(self.name(), DapStatus::Downloading);
|
||||
match self.install_binary(version, delegate).await {
|
||||
Ok(_) => {
|
||||
delegate.update_status(self.name(), DapStatus::None);
|
||||
}
|
||||
Err(error) => {
|
||||
delegate.update_status(
|
||||
self.name(),
|
||||
DapStatus::Failed {
|
||||
error: error.to_string(),
|
||||
},
|
||||
);
|
||||
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
|
||||
delegate
|
||||
.updated_adapters()
|
||||
.lock_arc()
|
||||
.await
|
||||
.insert(self.name());
|
||||
}
|
||||
|
||||
self.get_installed_binary(delegate, &config, user_installed_path, cx)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn fetch_latest_adapter_version(
|
||||
&self,
|
||||
delegate: &dyn DapDelegate,
|
||||
) -> Result<AdapterVersion>;
|
||||
|
||||
/// Installs the binary for the debug adapter.
|
||||
/// This method is called when the adapter binary is not found or needs to be updated.
|
||||
/// It should download and install the necessary files for the debug adapter to function.
|
||||
async fn install_binary(
|
||||
&self,
|
||||
version: AdapterVersion,
|
||||
delegate: &dyn DapDelegate,
|
||||
) -> Result<()>;
|
||||
|
||||
async fn get_installed_binary(
|
||||
&self,
|
||||
delegate: &dyn DapDelegate,
|
||||
config: &DebugTaskDefinition,
|
||||
user_installed_path: Option<PathBuf>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<DebugAdapterBinary>;
|
||||
|
||||
fn inline_value_provider(&self) -> Option<Box<dyn InlineValueProvider>> {
|
||||
@@ -564,29 +484,4 @@ impl DebugAdapter for FakeAdapter {
|
||||
request_args: self.request_args(config),
|
||||
})
|
||||
}
|
||||
|
||||
async fn fetch_latest_adapter_version(
|
||||
&self,
|
||||
_delegate: &dyn DapDelegate,
|
||||
) -> Result<AdapterVersion> {
|
||||
unimplemented!("fetch latest adapter version");
|
||||
}
|
||||
|
||||
async fn install_binary(
|
||||
&self,
|
||||
_version: AdapterVersion,
|
||||
_delegate: &dyn DapDelegate,
|
||||
) -> Result<()> {
|
||||
unimplemented!("install binary");
|
||||
}
|
||||
|
||||
async fn get_installed_binary(
|
||||
&self,
|
||||
_: &dyn DapDelegate,
|
||||
_: &DebugTaskDefinition,
|
||||
_: Option<PathBuf>,
|
||||
_: &mut AsyncApp,
|
||||
) -> Result<DebugAdapterBinary> {
|
||||
unimplemented!("get installed binary");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,14 @@ use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources};
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum DebugPanelDockPosition {
|
||||
Left,
|
||||
Bottom,
|
||||
Right,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, JsonSchema, Clone, Copy)]
|
||||
#[serde(default)]
|
||||
pub struct DebuggerSettings {
|
||||
@@ -31,6 +39,10 @@ pub struct DebuggerSettings {
|
||||
///
|
||||
/// Default: true
|
||||
pub format_dap_log_messages: bool,
|
||||
/// The dock position of the debug panel
|
||||
///
|
||||
/// Default: Bottom
|
||||
pub dock: DebugPanelDockPosition,
|
||||
}
|
||||
|
||||
impl Default for DebuggerSettings {
|
||||
@@ -42,6 +54,7 @@ impl Default for DebuggerSettings {
|
||||
timeout: 2000,
|
||||
log_dap_communications: true,
|
||||
format_dap_log_messages: true,
|
||||
dock: DebugPanelDockPosition::Bottom,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use collections::FxHashMap;
|
||||
use gpui::{App, Global};
|
||||
use gpui::{App, Global, SharedString};
|
||||
use parking_lot::RwLock;
|
||||
use task::{DebugRequest, SpawnInTerminal};
|
||||
use task::{DebugRequest, DebugScenario, SpawnInTerminal, TaskTemplate};
|
||||
|
||||
use crate::adapters::{DebugAdapter, DebugAdapterName};
|
||||
use std::{collections::BTreeMap, sync::Arc};
|
||||
@@ -11,15 +11,17 @@ use std::{collections::BTreeMap, sync::Arc};
|
||||
/// Given a user build configuration, locator creates a fill-in debug target ([DebugRequest]) on behalf of the user.
|
||||
#[async_trait]
|
||||
pub trait DapLocator: Send + Sync {
|
||||
fn name(&self) -> SharedString;
|
||||
/// Determines whether this locator can generate debug target for given task.
|
||||
fn accepts(&self, build_config: &SpawnInTerminal) -> bool;
|
||||
fn create_scenario(&self, build_config: &TaskTemplate, adapter: &str) -> Option<DebugScenario>;
|
||||
|
||||
async fn run(&self, build_config: SpawnInTerminal) -> Result<DebugRequest>;
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct DapRegistryState {
|
||||
adapters: BTreeMap<DebugAdapterName, Arc<dyn DebugAdapter>>,
|
||||
locators: FxHashMap<String, Arc<dyn DapLocator>>,
|
||||
locators: FxHashMap<SharedString, Arc<dyn DapLocator>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
@@ -48,15 +50,15 @@ impl DapRegistry {
|
||||
);
|
||||
}
|
||||
|
||||
pub fn add_locator(&self, name: String, locator: Arc<dyn DapLocator>) {
|
||||
let _previous_value = self.0.write().locators.insert(name, locator);
|
||||
pub fn add_locator(&self, locator: Arc<dyn DapLocator>) {
|
||||
let _previous_value = self.0.write().locators.insert(locator.name(), locator);
|
||||
debug_assert!(
|
||||
_previous_value.is_none(),
|
||||
"Attempted to insert a new debug locator when one is already registered"
|
||||
);
|
||||
}
|
||||
|
||||
pub fn locators(&self) -> FxHashMap<String, Arc<dyn DapLocator>> {
|
||||
pub fn locators(&self) -> FxHashMap<SharedString, Arc<dyn DapLocator>> {
|
||||
self.0.read().locators.clone()
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ doctest = false
|
||||
anyhow.workspace = true
|
||||
async-trait.workspace = true
|
||||
dap.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
lsp-types.workspace = true
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
use std::{collections::HashMap, path::PathBuf, sync::OnceLock};
|
||||
|
||||
use anyhow::{Result, bail};
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use dap::adapters::{DebugTaskDefinition, InlineValueProvider, latest_github_release};
|
||||
use futures::StreamExt;
|
||||
use gpui::AsyncApp;
|
||||
use task::DebugRequest;
|
||||
use util::fs::remove_matching;
|
||||
|
||||
use crate::*;
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct CodeLldbDebugAdapter {
|
||||
last_known_version: OnceLock<String>,
|
||||
path_to_codelldb: OnceLock<String>,
|
||||
}
|
||||
|
||||
impl CodeLldbDebugAdapter {
|
||||
@@ -54,29 +56,6 @@ impl CodeLldbDebugAdapter {
|
||||
configuration,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl DebugAdapter for CodeLldbDebugAdapter {
|
||||
fn name(&self) -> DebugAdapterName {
|
||||
DebugAdapterName(Self::ADAPTER_NAME.into())
|
||||
}
|
||||
|
||||
async fn install_binary(
|
||||
&self,
|
||||
version: AdapterVersion,
|
||||
delegate: &dyn DapDelegate,
|
||||
) -> Result<()> {
|
||||
adapters::download_adapter_from_github(
|
||||
self.name(),
|
||||
version,
|
||||
adapters::DownloadedFileType::Vsix,
|
||||
delegate,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn fetch_latest_adapter_version(
|
||||
&self,
|
||||
@@ -107,7 +86,6 @@ impl DebugAdapter for CodeLldbDebugAdapter {
|
||||
}
|
||||
};
|
||||
let asset_name = format!("codelldb-{platform}-{arch}.vsix");
|
||||
let _ = self.last_known_version.set(release.tag_name.clone());
|
||||
let ret = AdapterVersion {
|
||||
tag_name: release.tag_name,
|
||||
url: release
|
||||
@@ -121,28 +99,56 @@ impl DebugAdapter for CodeLldbDebugAdapter {
|
||||
|
||||
Ok(ret)
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_installed_binary(
|
||||
#[async_trait(?Send)]
|
||||
impl DebugAdapter for CodeLldbDebugAdapter {
|
||||
fn name(&self) -> DebugAdapterName {
|
||||
DebugAdapterName(Self::ADAPTER_NAME.into())
|
||||
}
|
||||
|
||||
async fn get_binary(
|
||||
&self,
|
||||
_: &dyn DapDelegate,
|
||||
delegate: &dyn DapDelegate,
|
||||
config: &DebugTaskDefinition,
|
||||
_: Option<PathBuf>,
|
||||
user_installed_path: Option<PathBuf>,
|
||||
_: &mut AsyncApp,
|
||||
) -> Result<DebugAdapterBinary> {
|
||||
let Some(version) = self.last_known_version.get() else {
|
||||
bail!("Could not determine latest CodeLLDB version");
|
||||
};
|
||||
let adapter_path = paths::debug_adapters_dir().join(&Self::ADAPTER_NAME);
|
||||
let version_path = adapter_path.join(format!("{}_{}", Self::ADAPTER_NAME, version));
|
||||
let mut command = user_installed_path
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.or(self.path_to_codelldb.get().cloned());
|
||||
|
||||
if command.is_none() {
|
||||
delegate.output_to_console(format!("Checking latest version of {}...", self.name()));
|
||||
let adapter_path = paths::debug_adapters_dir().join(&Self::ADAPTER_NAME);
|
||||
let version_path =
|
||||
if let Ok(version) = self.fetch_latest_adapter_version(delegate).await {
|
||||
adapters::download_adapter_from_github(
|
||||
self.name(),
|
||||
version.clone(),
|
||||
adapters::DownloadedFileType::Vsix,
|
||||
delegate,
|
||||
)
|
||||
.await?;
|
||||
let version_path =
|
||||
adapter_path.join(format!("{}_{}", Self::ADAPTER_NAME, version.tag_name));
|
||||
remove_matching(&adapter_path, |entry| entry != version_path).await;
|
||||
version_path
|
||||
} else {
|
||||
let mut paths = delegate.fs().read_dir(&adapter_path).await?;
|
||||
paths
|
||||
.next()
|
||||
.await
|
||||
.ok_or_else(|| anyhow!("No adapter found"))??
|
||||
};
|
||||
let adapter_dir = version_path.join("extension").join("adapter");
|
||||
let path = adapter_dir.join("codelldb").to_string_lossy().to_string();
|
||||
self.path_to_codelldb.set(path.clone()).ok();
|
||||
command = Some(path);
|
||||
};
|
||||
|
||||
let adapter_dir = version_path.join("extension").join("adapter");
|
||||
let command = adapter_dir.join("codelldb");
|
||||
let command = command
|
||||
.to_str()
|
||||
.map(ToOwned::to_owned)
|
||||
.ok_or_else(|| anyhow!("Adapter path is expected to be valid UTF-8"))?;
|
||||
Ok(DebugAdapterBinary {
|
||||
command,
|
||||
command: command.unwrap(),
|
||||
cwd: None,
|
||||
arguments: vec![
|
||||
"--settings".into(),
|
||||
|
||||
@@ -29,9 +29,9 @@ use task::TcpArgumentsTemplate;
|
||||
pub fn init(cx: &mut App) {
|
||||
cx.update_default_global(|registry: &mut DapRegistry, _cx| {
|
||||
registry.add_adapter(Arc::from(CodeLldbDebugAdapter::default()));
|
||||
registry.add_adapter(Arc::from(PythonDebugAdapter));
|
||||
registry.add_adapter(Arc::from(PhpDebugAdapter));
|
||||
registry.add_adapter(Arc::from(JsDebugAdapter));
|
||||
registry.add_adapter(Arc::from(PythonDebugAdapter::default()));
|
||||
registry.add_adapter(Arc::from(PhpDebugAdapter::default()));
|
||||
registry.add_adapter(Arc::from(JsDebugAdapter::default()));
|
||||
registry.add_adapter(Arc::from(GoDebugAdapter));
|
||||
registry.add_adapter(Arc::from(GdbDebugAdapter));
|
||||
})
|
||||
|
||||
@@ -90,26 +90,4 @@ impl DebugAdapter for GdbDebugAdapter {
|
||||
request_args: self.request_args(config),
|
||||
})
|
||||
}
|
||||
|
||||
async fn install_binary(
|
||||
&self,
|
||||
_version: AdapterVersion,
|
||||
_delegate: &dyn DapDelegate,
|
||||
) -> Result<()> {
|
||||
unimplemented!("GDB debug adapter cannot be installed by Zed (yet)")
|
||||
}
|
||||
|
||||
async fn fetch_latest_adapter_version(&self, _: &dyn DapDelegate) -> Result<AdapterVersion> {
|
||||
unimplemented!("Fetch latest GDB version not implemented (yet)")
|
||||
}
|
||||
|
||||
async fn get_installed_binary(
|
||||
&self,
|
||||
_: &dyn DapDelegate,
|
||||
_: &DebugTaskDefinition,
|
||||
_: Option<std::path::PathBuf>,
|
||||
_: &mut AsyncApp,
|
||||
) -> Result<DebugAdapterBinary> {
|
||||
unimplemented!("GDB cannot be installed by Zed (yet)")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,41 +46,8 @@ impl DebugAdapter for GoDebugAdapter {
|
||||
&self,
|
||||
delegate: &dyn DapDelegate,
|
||||
config: &DebugTaskDefinition,
|
||||
user_installed_path: Option<PathBuf>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<DebugAdapterBinary> {
|
||||
self.get_installed_binary(delegate, config, user_installed_path, cx)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn fetch_latest_adapter_version(
|
||||
&self,
|
||||
_delegate: &dyn DapDelegate,
|
||||
) -> Result<AdapterVersion> {
|
||||
unimplemented!("This adapter is used from path for now");
|
||||
}
|
||||
|
||||
async fn install_binary(
|
||||
&self,
|
||||
version: AdapterVersion,
|
||||
delegate: &dyn DapDelegate,
|
||||
) -> Result<()> {
|
||||
adapters::download_adapter_from_github(
|
||||
self.name(),
|
||||
version,
|
||||
adapters::DownloadedFileType::Zip,
|
||||
delegate,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_installed_binary(
|
||||
&self,
|
||||
delegate: &dyn DapDelegate,
|
||||
config: &DebugTaskDefinition,
|
||||
_: Option<PathBuf>,
|
||||
_: &mut AsyncApp,
|
||||
_user_installed_path: Option<PathBuf>,
|
||||
_cx: &mut AsyncApp,
|
||||
) -> Result<DebugAdapterBinary> {
|
||||
let delve_path = delegate
|
||||
.which(OsStr::new("dlv"))
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
use adapters::latest_github_release;
|
||||
use dap::{StartDebuggingRequestArguments, adapters::DebugTaskDefinition};
|
||||
use gpui::AsyncApp;
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
use std::{collections::HashMap, path::PathBuf, sync::OnceLock};
|
||||
use task::DebugRequest;
|
||||
use util::ResultExt;
|
||||
|
||||
use crate::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct JsDebugAdapter;
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct JsDebugAdapter {
|
||||
checked: OnceLock<()>,
|
||||
}
|
||||
|
||||
impl JsDebugAdapter {
|
||||
const ADAPTER_NAME: &'static str = "JavaScript";
|
||||
@@ -47,13 +50,6 @@ impl JsDebugAdapter {
|
||||
request: config.request.to_dap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl DebugAdapter for JsDebugAdapter {
|
||||
fn name(&self) -> DebugAdapterName {
|
||||
DebugAdapterName(Self::ADAPTER_NAME.into())
|
||||
}
|
||||
|
||||
async fn fetch_latest_adapter_version(
|
||||
&self,
|
||||
@@ -130,20 +126,35 @@ impl DebugAdapter for JsDebugAdapter {
|
||||
request_args: self.request_args(config),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn install_binary(
|
||||
#[async_trait(?Send)]
|
||||
impl DebugAdapter for JsDebugAdapter {
|
||||
fn name(&self) -> DebugAdapterName {
|
||||
DebugAdapterName(Self::ADAPTER_NAME.into())
|
||||
}
|
||||
|
||||
async fn get_binary(
|
||||
&self,
|
||||
version: AdapterVersion,
|
||||
delegate: &dyn DapDelegate,
|
||||
) -> Result<()> {
|
||||
adapters::download_adapter_from_github(
|
||||
self.name(),
|
||||
version,
|
||||
adapters::DownloadedFileType::GzipTar,
|
||||
delegate,
|
||||
)
|
||||
.await?;
|
||||
config: &DebugTaskDefinition,
|
||||
user_installed_path: Option<PathBuf>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<DebugAdapterBinary> {
|
||||
if self.checked.set(()).is_ok() {
|
||||
delegate.output_to_console(format!("Checking latest version of {}...", self.name()));
|
||||
if let Some(version) = self.fetch_latest_adapter_version(delegate).await.log_err() {
|
||||
adapters::download_adapter_from_github(
|
||||
self.name(),
|
||||
version,
|
||||
adapters::DownloadedFileType::GzipTar,
|
||||
delegate,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
self.get_installed_binary(delegate, &config, user_installed_path, cx)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
use adapters::latest_github_release;
|
||||
use dap::adapters::{DebugTaskDefinition, TcpArguments};
|
||||
use gpui::AsyncApp;
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
use std::{collections::HashMap, path::PathBuf, sync::OnceLock};
|
||||
use util::ResultExt;
|
||||
|
||||
use crate::*;
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct PhpDebugAdapter;
|
||||
pub(crate) struct PhpDebugAdapter {
|
||||
checked: OnceLock<()>,
|
||||
}
|
||||
|
||||
impl PhpDebugAdapter {
|
||||
const ADAPTER_NAME: &'static str = "PHP";
|
||||
@@ -32,13 +35,6 @@ impl PhpDebugAdapter {
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl DebugAdapter for PhpDebugAdapter {
|
||||
fn name(&self) -> DebugAdapterName {
|
||||
DebugAdapterName(Self::ADAPTER_NAME.into())
|
||||
}
|
||||
|
||||
async fn fetch_latest_adapter_version(
|
||||
&self,
|
||||
@@ -114,20 +110,35 @@ impl DebugAdapter for PhpDebugAdapter {
|
||||
request_args: self.request_args(config)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn install_binary(
|
||||
#[async_trait(?Send)]
|
||||
impl DebugAdapter for PhpDebugAdapter {
|
||||
fn name(&self) -> DebugAdapterName {
|
||||
DebugAdapterName(Self::ADAPTER_NAME.into())
|
||||
}
|
||||
|
||||
async fn get_binary(
|
||||
&self,
|
||||
version: AdapterVersion,
|
||||
delegate: &dyn DapDelegate,
|
||||
) -> Result<()> {
|
||||
adapters::download_adapter_from_github(
|
||||
self.name(),
|
||||
version,
|
||||
adapters::DownloadedFileType::Vsix,
|
||||
delegate,
|
||||
)
|
||||
.await?;
|
||||
config: &DebugTaskDefinition,
|
||||
user_installed_path: Option<PathBuf>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<DebugAdapterBinary> {
|
||||
if self.checked.set(()).is_ok() {
|
||||
delegate.output_to_console(format!("Checking latest version of {}...", self.name()));
|
||||
if let Some(version) = self.fetch_latest_adapter_version(delegate).await.log_err() {
|
||||
adapters::download_adapter_from_github(
|
||||
self.name(),
|
||||
version,
|
||||
adapters::DownloadedFileType::Vsix,
|
||||
delegate,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
self.get_installed_binary(delegate, &config, user_installed_path, cx)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,13 @@ use dap::{
|
||||
adapters::InlineValueProvider,
|
||||
};
|
||||
use gpui::AsyncApp;
|
||||
use std::{collections::HashMap, ffi::OsStr, path::PathBuf};
|
||||
use std::{collections::HashMap, ffi::OsStr, path::PathBuf, sync::OnceLock};
|
||||
use util::ResultExt;
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct PythonDebugAdapter;
|
||||
pub(crate) struct PythonDebugAdapter {
|
||||
checked: OnceLock<()>,
|
||||
}
|
||||
|
||||
impl PythonDebugAdapter {
|
||||
const ADAPTER_NAME: &'static str = "Debugpy";
|
||||
@@ -46,14 +49,6 @@ impl PythonDebugAdapter {
|
||||
request: config.request.to_dap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl DebugAdapter for PythonDebugAdapter {
|
||||
fn name(&self) -> DebugAdapterName {
|
||||
DebugAdapterName(Self::ADAPTER_NAME.into())
|
||||
}
|
||||
|
||||
async fn fetch_latest_adapter_version(
|
||||
&self,
|
||||
delegate: &dyn DapDelegate,
|
||||
@@ -162,6 +157,31 @@ impl DebugAdapter for PythonDebugAdapter {
|
||||
request_args: self.request_args(config),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl DebugAdapter for PythonDebugAdapter {
|
||||
fn name(&self) -> DebugAdapterName {
|
||||
DebugAdapterName(Self::ADAPTER_NAME.into())
|
||||
}
|
||||
|
||||
async fn get_binary(
|
||||
&self,
|
||||
delegate: &dyn DapDelegate,
|
||||
config: &DebugTaskDefinition,
|
||||
user_installed_path: Option<PathBuf>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<DebugAdapterBinary> {
|
||||
if self.checked.set(()).is_ok() {
|
||||
delegate.output_to_console(format!("Checking latest version of {}...", self.name()));
|
||||
if let Some(version) = self.fetch_latest_adapter_version(delegate).await.log_err() {
|
||||
self.install_binary(version, delegate).await?;
|
||||
}
|
||||
}
|
||||
|
||||
self.get_installed_binary(delegate, &config, user_installed_path, cx)
|
||||
.await
|
||||
}
|
||||
|
||||
fn inline_value_provider(&self) -> Option<Box<dyn InlineValueProvider>> {
|
||||
Some(Box::new(PythonInlineValueProvider))
|
||||
|
||||
@@ -568,11 +568,11 @@ impl DapLogView {
|
||||
.sessions()
|
||||
.filter_map(|session| {
|
||||
let session = session.read(cx);
|
||||
session.adapter_name();
|
||||
session.adapter();
|
||||
let client = session.adapter_client()?;
|
||||
Some(DapMenuItem {
|
||||
client_id: client.id(),
|
||||
client_name: session.adapter_name().to_string(),
|
||||
client_name: session.adapter().to_string(),
|
||||
has_adapter_logs: client.has_adapter_logs(),
|
||||
selected_entry: self.current_view.map_or(LogKind::Adapter, |(_, kind)| kind),
|
||||
})
|
||||
|
||||
@@ -237,7 +237,7 @@ impl PickerDelegate for AttachModalDelegate {
|
||||
.flatten();
|
||||
if let Some(panel) = panel {
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.start_session(scenario, Default::default(), None, window, cx);
|
||||
panel.start_session(scenario, Default::default(), None, None, window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -153,27 +153,63 @@ pub fn init(cx: &mut App) {
|
||||
let weak_panel = debug_panel.downgrade();
|
||||
let weak_workspace = cx.weak_entity();
|
||||
|
||||
workspace.toggle_modal(window, cx, |window, cx| {
|
||||
NewSessionModal::new(
|
||||
debug_panel.read(cx).past_debug_definition.clone(),
|
||||
weak_panel,
|
||||
weak_workspace,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let task_contexts = this
|
||||
.update_in(cx, |workspace, window, cx| {
|
||||
tasks_ui::task_contexts(workspace, window, cx)
|
||||
})?
|
||||
.await;
|
||||
this.update_in(cx, |workspace, window, cx| {
|
||||
workspace.toggle_modal(window, cx, |window, cx| {
|
||||
NewSessionModal::new(
|
||||
debug_panel.read(cx).past_debug_definition.clone(),
|
||||
weak_panel,
|
||||
weak_workspace,
|
||||
None,
|
||||
task_contexts,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
})?;
|
||||
|
||||
Result::<_, anyhow::Error>::Ok(())
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
},
|
||||
)
|
||||
.register_action(|workspace: &mut Workspace, _: &Start, window, cx| {
|
||||
tasks_ui::toggle_modal(
|
||||
workspace,
|
||||
None,
|
||||
task::TaskModal::DebugModal,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.detach();
|
||||
if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
|
||||
let weak_panel = debug_panel.downgrade();
|
||||
let weak_workspace = cx.weak_entity();
|
||||
let task_store = workspace.project().read(cx).task_store().clone();
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let task_contexts = this
|
||||
.update_in(cx, |workspace, window, cx| {
|
||||
tasks_ui::task_contexts(workspace, window, cx)
|
||||
})?
|
||||
.await;
|
||||
|
||||
this.update_in(cx, |workspace, window, cx| {
|
||||
workspace.toggle_modal(window, cx, |window, cx| {
|
||||
NewSessionModal::new(
|
||||
debug_panel.read(cx).past_debug_definition.clone(),
|
||||
weak_panel,
|
||||
weak_workspace,
|
||||
Some(task_store),
|
||||
task_contexts,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
})?;
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach()
|
||||
}
|
||||
});
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,37 +1,48 @@
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
cmp::Reverse,
|
||||
ops::Not,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use dap::{DapRegistry, DebugRequest, adapters::DebugTaskDefinition};
|
||||
use editor::{Editor, EditorElement, EditorStyle};
|
||||
use gpui::{
|
||||
App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Render, TextStyle,
|
||||
WeakEntity,
|
||||
use collections::{HashMap, HashSet};
|
||||
use dap::{
|
||||
DapRegistry, DebugRequest,
|
||||
adapters::{DebugAdapterName, DebugTaskDefinition},
|
||||
};
|
||||
use editor::{Editor, EditorElement, EditorStyle};
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
App, AppContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Render,
|
||||
Subscription, TextStyle, WeakEntity,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch};
|
||||
use project::{TaskContexts, TaskSourceKind, task_store::TaskStore};
|
||||
use settings::Settings;
|
||||
use task::{DebugScenario, LaunchRequest, TaskContext};
|
||||
use task::{DebugScenario, LaunchRequest};
|
||||
use theme::ThemeSettings;
|
||||
use ui::{
|
||||
ActiveTheme, Button, ButtonCommon, ButtonSize, CheckboxWithLabel, Clickable, Color, Context,
|
||||
ContextMenu, Disableable, DropdownMenu, FluentBuilder, InteractiveElement, IntoElement, Label,
|
||||
LabelCommon as _, ParentElement, RenderOnce, SharedString, Styled, StyledExt, ToggleButton,
|
||||
ToggleState, Toggleable, Window, div, h_flex, relative, rems, v_flex,
|
||||
ContextMenu, Disableable, DropdownMenu, FluentBuilder, Icon, IconName, InteractiveElement,
|
||||
IntoElement, Label, LabelCommon as _, ListItem, ListItemSpacing, ParentElement, RenderOnce,
|
||||
SharedString, Styled, StyledExt, ToggleButton, ToggleState, Toggleable, Window, div, h_flex,
|
||||
relative, rems, v_flex,
|
||||
};
|
||||
use util::ResultExt;
|
||||
use workspace::{ModalView, Workspace};
|
||||
|
||||
use crate::{attach_modal::AttachModal, debugger_panel::DebugPanel};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(super) struct NewSessionModal {
|
||||
workspace: WeakEntity<Workspace>,
|
||||
debug_panel: WeakEntity<DebugPanel>,
|
||||
mode: NewSessionMode,
|
||||
stop_on_entry: ToggleState,
|
||||
initialize_args: Option<serde_json::Value>,
|
||||
debugger: Option<SharedString>,
|
||||
debugger: Option<DebugAdapterName>,
|
||||
last_selected_profile_name: Option<SharedString>,
|
||||
task_contexts: Arc<TaskContexts>,
|
||||
}
|
||||
|
||||
fn suggested_label(request: &DebugRequest, debugger: &str) -> SharedString {
|
||||
@@ -57,6 +68,8 @@ impl NewSessionModal {
|
||||
past_debug_definition: Option<DebugTaskDefinition>,
|
||||
debug_panel: WeakEntity<DebugPanel>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
task_store: Option<Entity<TaskStore>>,
|
||||
task_contexts: TaskContexts,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
@@ -73,6 +86,18 @@ impl NewSessionModal {
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(task_store) = task_store {
|
||||
cx.defer_in(window, |this, window, cx| {
|
||||
this.mode = NewSessionMode::scenario(
|
||||
this.debug_panel.clone(),
|
||||
this.workspace.clone(),
|
||||
task_store,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
Self {
|
||||
workspace: workspace.clone(),
|
||||
debugger,
|
||||
@@ -83,13 +108,14 @@ impl NewSessionModal {
|
||||
.unwrap_or(ToggleState::Unselected),
|
||||
last_selected_profile_name: None,
|
||||
initialize_args: None,
|
||||
task_contexts: Arc::new(task_contexts),
|
||||
}
|
||||
}
|
||||
|
||||
fn debug_config(&self, cx: &App, debugger: &str) -> DebugScenario {
|
||||
let request = self.mode.debug_task(cx);
|
||||
fn debug_config(&self, cx: &App, debugger: &str) -> Option<DebugScenario> {
|
||||
let request = self.mode.debug_task(cx)?;
|
||||
let label = suggested_label(&request, debugger);
|
||||
DebugScenario {
|
||||
Some(DebugScenario {
|
||||
adapter: debugger.to_owned().into(),
|
||||
label,
|
||||
request: Some(request),
|
||||
@@ -100,21 +126,35 @@ impl NewSessionModal {
|
||||
_ => None,
|
||||
},
|
||||
build: None,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn start_new_session(&self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(debugger) = self.debugger.as_ref() else {
|
||||
// todo: show in UI.
|
||||
// todo(debugger): show in UI.
|
||||
log::error!("No debugger selected");
|
||||
return;
|
||||
};
|
||||
let config = self.debug_config(cx, debugger);
|
||||
let debug_panel = self.debug_panel.clone();
|
||||
|
||||
if let NewSessionMode::Scenario(picker) = &self.mode {
|
||||
picker.update(cx, |picker, cx| {
|
||||
picker.delegate.confirm(false, window, cx);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(config) = self.debug_config(cx, debugger) else {
|
||||
log::error!("debug config not found in mode: {}", self.mode);
|
||||
return;
|
||||
};
|
||||
|
||||
let debug_panel = self.debug_panel.clone();
|
||||
let task_contexts = self.task_contexts.clone();
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let task_context = task_contexts.active_context().cloned().unwrap_or_default();
|
||||
let worktree_id = task_contexts.worktree();
|
||||
debug_panel.update_in(cx, |debug_panel, window, cx| {
|
||||
debug_panel.start_session(config, TaskContext::default(), None, window, cx)
|
||||
debug_panel.start_session(config, task_context, None, worktree_id, window, cx)
|
||||
})?;
|
||||
this.update(cx, |_, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
@@ -127,18 +167,17 @@ impl NewSessionModal {
|
||||
|
||||
fn update_attach_picker(
|
||||
attach: &Entity<AttachMode>,
|
||||
selected_debugger: &str,
|
||||
adapter: &DebugAdapterName,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
attach.update(cx, |this, cx| {
|
||||
if selected_debugger != this.definition.adapter.as_ref() {
|
||||
let adapter: SharedString = selected_debugger.to_owned().into();
|
||||
if adapter != &this.definition.adapter {
|
||||
this.definition.adapter = adapter.clone();
|
||||
|
||||
this.attach_picker.update(cx, |this, cx| {
|
||||
this.picker.update(cx, |this, cx| {
|
||||
this.delegate.definition.adapter = adapter;
|
||||
this.delegate.definition.adapter = adapter.clone();
|
||||
this.focus(window, cx);
|
||||
})
|
||||
});
|
||||
@@ -151,18 +190,32 @@ impl NewSessionModal {
|
||||
&self,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> ui::DropdownMenu {
|
||||
) -> Option<ui::DropdownMenu> {
|
||||
let workspace = self.workspace.clone();
|
||||
let language_registry = self
|
||||
.workspace
|
||||
.update(cx, |this, _| this.app_state().languages.clone())
|
||||
.ok()?;
|
||||
let weak = cx.weak_entity();
|
||||
let debugger = self.debugger.clone();
|
||||
let label = self
|
||||
.debugger
|
||||
.as_ref()
|
||||
.map(|d| d.0.clone())
|
||||
.unwrap_or_else(|| SELECT_DEBUGGER_LABEL.clone());
|
||||
let active_buffer_language_name =
|
||||
self.task_contexts
|
||||
.active_item_context
|
||||
.as_ref()
|
||||
.and_then(|item| {
|
||||
item.1
|
||||
.as_ref()
|
||||
.and_then(|location| location.buffer.read(cx).language()?.name().into())
|
||||
});
|
||||
DropdownMenu::new(
|
||||
"dap-adapter-picker",
|
||||
debugger
|
||||
.as_ref()
|
||||
.unwrap_or_else(|| &SELECT_DEBUGGER_LABEL)
|
||||
.clone(),
|
||||
label,
|
||||
ContextMenu::build(window, cx, move |mut menu, _, cx| {
|
||||
let setter_for_name = |name: SharedString| {
|
||||
let setter_for_name = |name: DebugAdapterName| {
|
||||
let weak = weak.clone();
|
||||
move |window: &mut Window, cx: &mut App| {
|
||||
weak.update(cx, |this, cx| {
|
||||
@@ -176,17 +229,50 @@ impl NewSessionModal {
|
||||
}
|
||||
};
|
||||
|
||||
let available_adapters = workspace
|
||||
let available_languages = language_registry.language_names();
|
||||
let mut debugger_to_languages = HashMap::default();
|
||||
for language in available_languages {
|
||||
let Some(language) =
|
||||
language_registry.available_language_for_name(language.as_str())
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
language.config().debuggers.iter().for_each(|adapter| {
|
||||
debugger_to_languages
|
||||
.entry(adapter.clone())
|
||||
.or_insert_with(HashSet::default)
|
||||
.insert(language.name());
|
||||
});
|
||||
}
|
||||
let mut available_adapters = workspace
|
||||
.update(cx, |_, cx| DapRegistry::global(cx).enumerate_adapters())
|
||||
.ok()
|
||||
.unwrap_or_default();
|
||||
|
||||
for adapter in available_adapters {
|
||||
menu = menu.entry(adapter.0.clone(), None, setter_for_name(adapter.0.clone()));
|
||||
available_adapters.sort_by_key(|name| {
|
||||
let languages_for_debugger = debugger_to_languages.get(name.as_ref());
|
||||
let languages_count =
|
||||
languages_for_debugger.map_or(0, |languages| languages.len());
|
||||
let contains_language_of_active_buffer = languages_for_debugger
|
||||
.zip(active_buffer_language_name.as_ref())
|
||||
.map_or(false, |(languages, active_buffer_language)| {
|
||||
languages.contains(active_buffer_language)
|
||||
});
|
||||
|
||||
(
|
||||
Reverse(contains_language_of_active_buffer),
|
||||
Reverse(languages_count),
|
||||
)
|
||||
});
|
||||
|
||||
for adapter in available_adapters.into_iter() {
|
||||
menu = menu.entry(adapter.0.clone(), None, setter_for_name(adapter.clone()));
|
||||
}
|
||||
menu
|
||||
}),
|
||||
)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn debug_config_drop_down_menu(
|
||||
@@ -211,7 +297,7 @@ impl NewSessionModal {
|
||||
move |window: &mut Window, cx: &mut App| {
|
||||
weak.update(cx, |this, cx| {
|
||||
this.last_selected_profile_name = Some(SharedString::from(&task.label));
|
||||
this.debugger = Some(task.adapter.clone());
|
||||
this.debugger = Some(DebugAdapterName(task.adapter.clone()));
|
||||
this.initialize_args = task.initialize_args.clone();
|
||||
match &task.request {
|
||||
Some(DebugRequest::Launch(launch_config)) => {
|
||||
@@ -256,9 +342,14 @@ impl NewSessionModal {
|
||||
.iter()
|
||||
.flat_map(|task_inventory| {
|
||||
task_inventory.read(cx).list_debug_scenarios(
|
||||
worktree.as_ref().map(|worktree| worktree.read(cx).id()),
|
||||
worktree
|
||||
.as_ref()
|
||||
.map(|worktree| worktree.read(cx).id())
|
||||
.iter()
|
||||
.copied(),
|
||||
)
|
||||
})
|
||||
.map(|(_source_kind, scenario)| scenario)
|
||||
.collect()
|
||||
})
|
||||
.ok()
|
||||
@@ -277,102 +368,22 @@ impl NewSessionModal {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct LaunchMode {
|
||||
program: Entity<Editor>,
|
||||
cwd: Entity<Editor>,
|
||||
}
|
||||
|
||||
impl LaunchMode {
|
||||
fn new(
|
||||
past_launch_config: Option<LaunchRequest>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Entity<Self> {
|
||||
let (past_program, past_cwd) = past_launch_config
|
||||
.map(|config| (Some(config.program), config.cwd))
|
||||
.unwrap_or_else(|| (None, None));
|
||||
|
||||
let program = cx.new(|cx| Editor::single_line(window, cx));
|
||||
program.update(cx, |this, cx| {
|
||||
this.set_placeholder_text("Program path", cx);
|
||||
|
||||
if let Some(past_program) = past_program {
|
||||
this.set_text(past_program, window, cx);
|
||||
};
|
||||
});
|
||||
let cwd = cx.new(|cx| Editor::single_line(window, cx));
|
||||
cwd.update(cx, |this, cx| {
|
||||
this.set_placeholder_text("Working Directory", cx);
|
||||
if let Some(past_cwd) = past_cwd {
|
||||
this.set_text(past_cwd.to_string_lossy(), window, cx);
|
||||
};
|
||||
});
|
||||
cx.new(|_| Self { program, cwd })
|
||||
}
|
||||
|
||||
fn debug_task(&self, cx: &App) -> task::LaunchRequest {
|
||||
let path = self.cwd.read(cx).text(cx);
|
||||
task::LaunchRequest {
|
||||
program: self.program.read(cx).text(cx),
|
||||
cwd: path.is_empty().not().then(|| PathBuf::from(path)),
|
||||
args: Default::default(),
|
||||
env: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct AttachMode {
|
||||
definition: DebugTaskDefinition,
|
||||
attach_picker: Entity<AttachModal>,
|
||||
}
|
||||
|
||||
impl AttachMode {
|
||||
fn new(
|
||||
debugger: Option<SharedString>,
|
||||
workspace: Entity<Workspace>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<NewSessionModal>,
|
||||
) -> Entity<Self> {
|
||||
let definition = DebugTaskDefinition {
|
||||
adapter: debugger.clone().unwrap_or_default(),
|
||||
label: "Attach New Session Setup".into(),
|
||||
request: dap::DebugRequest::Attach(task::AttachRequest { process_id: None }),
|
||||
initialize_args: None,
|
||||
tcp_connection: None,
|
||||
stop_on_entry: Some(false),
|
||||
};
|
||||
let attach_picker = cx.new(|cx| {
|
||||
let modal = AttachModal::new(definition.clone(), workspace, false, window, cx);
|
||||
window.focus(&modal.focus_handle(cx));
|
||||
|
||||
modal
|
||||
});
|
||||
cx.new(|_| Self {
|
||||
definition,
|
||||
attach_picker,
|
||||
})
|
||||
}
|
||||
fn debug_task(&self) -> task::AttachRequest {
|
||||
task::AttachRequest { process_id: None }
|
||||
}
|
||||
}
|
||||
|
||||
static SELECT_DEBUGGER_LABEL: SharedString = SharedString::new_static("Select Debugger");
|
||||
static SELECT_SCENARIO_LABEL: SharedString = SharedString::new_static("Select Profile");
|
||||
|
||||
#[derive(Clone)]
|
||||
enum NewSessionMode {
|
||||
Launch(Entity<LaunchMode>),
|
||||
Scenario(Entity<Picker<DebugScenarioDelegate>>),
|
||||
Attach(Entity<AttachMode>),
|
||||
}
|
||||
|
||||
impl NewSessionMode {
|
||||
fn debug_task(&self, cx: &App) -> DebugRequest {
|
||||
fn debug_task(&self, cx: &App) -> Option<DebugRequest> {
|
||||
match self {
|
||||
NewSessionMode::Launch(entity) => entity.read(cx).debug_task(cx).into(),
|
||||
NewSessionMode::Attach(entity) => entity.read(cx).debug_task().into(),
|
||||
NewSessionMode::Launch(entity) => Some(entity.read(cx).debug_task(cx).into()),
|
||||
NewSessionMode::Attach(entity) => Some(entity.read(cx).debug_task().into()),
|
||||
NewSessionMode::Scenario(_) => None,
|
||||
}
|
||||
}
|
||||
fn as_attach(&self) -> Option<&Entity<AttachMode>> {
|
||||
@@ -382,6 +393,78 @@ impl NewSessionMode {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn scenario(
|
||||
debug_panel: WeakEntity<DebugPanel>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
task_store: Entity<TaskStore>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<NewSessionModal>,
|
||||
) -> NewSessionMode {
|
||||
let picker = cx.new(|cx| {
|
||||
Picker::uniform_list(
|
||||
DebugScenarioDelegate::new(debug_panel, workspace, task_store),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.modal(false)
|
||||
});
|
||||
|
||||
cx.subscribe(&picker, |_, _, _, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
})
|
||||
.detach();
|
||||
|
||||
picker.focus_handle(cx).focus(window);
|
||||
NewSessionMode::Scenario(picker)
|
||||
}
|
||||
|
||||
fn attach(
|
||||
debugger: Option<DebugAdapterName>,
|
||||
workspace: Entity<Workspace>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<NewSessionModal>,
|
||||
) -> Self {
|
||||
Self::Attach(AttachMode::new(debugger, workspace, window, cx))
|
||||
}
|
||||
|
||||
fn launch(
|
||||
past_launch_config: Option<LaunchRequest>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<NewSessionModal>,
|
||||
) -> Self {
|
||||
Self::Launch(LaunchMode::new(past_launch_config, window, cx))
|
||||
}
|
||||
|
||||
fn has_match(&self, cx: &App) -> bool {
|
||||
match self {
|
||||
NewSessionMode::Scenario(picker) => picker.read(cx).delegate.match_count() > 0,
|
||||
NewSessionMode::Attach(picker) => {
|
||||
picker
|
||||
.read(cx)
|
||||
.attach_picker
|
||||
.read(cx)
|
||||
.picker
|
||||
.read(cx)
|
||||
.delegate
|
||||
.match_count()
|
||||
> 0
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for NewSessionMode {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let mode = match self {
|
||||
NewSessionMode::Launch(_) => "launch".to_owned(),
|
||||
NewSessionMode::Attach(_) => "attach".to_owned(),
|
||||
NewSessionMode::Scenario(_) => "scenario picker".to_owned(),
|
||||
};
|
||||
|
||||
write!(f, "{}", mode)
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for NewSessionMode {
|
||||
@@ -389,45 +472,11 @@ impl Focusable for NewSessionMode {
|
||||
match &self {
|
||||
NewSessionMode::Launch(entity) => entity.read(cx).program.focus_handle(cx),
|
||||
NewSessionMode::Attach(entity) => entity.read(cx).attach_picker.focus_handle(cx),
|
||||
NewSessionMode::Scenario(entity) => entity.read(cx).focus_handle(cx),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for LaunchMode {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
v_flex()
|
||||
.p_2()
|
||||
.w_full()
|
||||
.gap_3()
|
||||
.track_focus(&self.program.focus_handle(cx))
|
||||
.child(
|
||||
div().child(
|
||||
Label::new("Program")
|
||||
.size(ui::LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.child(render_editor(&self.program, window, cx))
|
||||
.child(
|
||||
div().child(
|
||||
Label::new("Working Directory")
|
||||
.size(ui::LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.child(render_editor(&self.cwd, window, cx))
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for AttachMode {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
v_flex()
|
||||
.w_full()
|
||||
.track_focus(&self.attach_picker.focus_handle(cx))
|
||||
.child(self.attach_picker.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for NewSessionMode {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl ui::IntoElement {
|
||||
match self {
|
||||
@@ -437,27 +486,14 @@ impl RenderOnce for NewSessionMode {
|
||||
NewSessionMode::Attach(entity) => entity.update(cx, |this, cx| {
|
||||
this.clone().render(window, cx).into_any_element()
|
||||
}),
|
||||
NewSessionMode::Scenario(entity) => v_flex()
|
||||
.w(rems(34.))
|
||||
.child(entity.clone())
|
||||
.into_any_element(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl NewSessionMode {
|
||||
fn attach(
|
||||
debugger: Option<SharedString>,
|
||||
workspace: Entity<Workspace>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<NewSessionModal>,
|
||||
) -> Self {
|
||||
Self::Attach(AttachMode::new(debugger, workspace, window, cx))
|
||||
}
|
||||
fn launch(
|
||||
past_launch_config: Option<LaunchRequest>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<NewSessionModal>,
|
||||
) -> Self {
|
||||
Self::Launch(LaunchMode::new(past_launch_config, window, cx))
|
||||
}
|
||||
}
|
||||
fn render_editor(editor: &Entity<Editor>, window: &mut Window, cx: &App) -> impl IntoElement {
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let theme = cx.theme();
|
||||
@@ -519,6 +555,34 @@ impl Render for NewSessionModal {
|
||||
h_flex()
|
||||
.justify_start()
|
||||
.w_full()
|
||||
.child(
|
||||
ToggleButton::new("debugger-session-ui-picker-button", "Scenarios")
|
||||
.size(ButtonSize::Default)
|
||||
.style(ui::ButtonStyle::Subtle)
|
||||
.toggle_state(matches!(self.mode, NewSessionMode::Scenario(_)))
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
let Some(task_store) = this
|
||||
.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.project().read(cx).task_store().clone()
|
||||
})
|
||||
.ok()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
this.mode = NewSessionMode::scenario(
|
||||
this.debug_panel.clone(),
|
||||
this.workspace.clone(),
|
||||
task_store,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
cx.notify();
|
||||
}))
|
||||
.first(),
|
||||
)
|
||||
.child(
|
||||
ToggleButton::new(
|
||||
"debugger-session-ui-launch-button",
|
||||
@@ -532,7 +596,7 @@ impl Render for NewSessionModal {
|
||||
this.mode.focus_handle(cx).focus(window);
|
||||
cx.notify();
|
||||
}))
|
||||
.first(),
|
||||
.middle(),
|
||||
)
|
||||
.child(
|
||||
ToggleButton::new(
|
||||
@@ -565,7 +629,9 @@ impl Render for NewSessionModal {
|
||||
),
|
||||
)
|
||||
.justify_between()
|
||||
.child(self.adapter_drop_down_menu(window, cx))
|
||||
.when(!matches!(self.mode, NewSessionMode::Scenario(_)), |this| {
|
||||
this.children(self.adapter_drop_down_menu(window, cx))
|
||||
})
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.border_b_1(),
|
||||
)
|
||||
@@ -601,10 +667,21 @@ impl Render for NewSessionModal {
|
||||
})
|
||||
.child(
|
||||
Button::new("debugger-spawn", "Start")
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.start_new_session(window, cx);
|
||||
.on_click(cx.listener(|this, _, window, cx| match &this.mode {
|
||||
NewSessionMode::Scenario(picker) => {
|
||||
picker.update(cx, |picker, cx| {
|
||||
picker.delegate.confirm(true, window, cx)
|
||||
})
|
||||
}
|
||||
_ => this.start_new_session(window, cx),
|
||||
}))
|
||||
.disabled(self.debugger.is_none()),
|
||||
.disabled(match self.mode {
|
||||
NewSessionMode::Scenario(_) => !self.mode.has_match(cx),
|
||||
NewSessionMode::Attach(_) => {
|
||||
self.debugger.is_none() || !self.mode.has_match(cx)
|
||||
}
|
||||
NewSessionMode::Launch(_) => self.debugger.is_none(),
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -619,3 +696,351 @@ impl Focusable for NewSessionModal {
|
||||
}
|
||||
|
||||
impl ModalView for NewSessionModal {}
|
||||
|
||||
impl RenderOnce for LaunchMode {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
v_flex()
|
||||
.p_2()
|
||||
.w_full()
|
||||
.gap_3()
|
||||
.track_focus(&self.program.focus_handle(cx))
|
||||
.child(
|
||||
div().child(
|
||||
Label::new("Program")
|
||||
.size(ui::LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.child(render_editor(&self.program, window, cx))
|
||||
.child(
|
||||
div().child(
|
||||
Label::new("Working Directory")
|
||||
.size(ui::LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.child(render_editor(&self.cwd, window, cx))
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for AttachMode {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
v_flex()
|
||||
.w_full()
|
||||
.track_focus(&self.attach_picker.focus_handle(cx))
|
||||
.child(self.attach_picker.clone())
|
||||
}
|
||||
}
|
||||
|
||||
use std::rc::Rc;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(super) struct LaunchMode {
|
||||
program: Entity<Editor>,
|
||||
cwd: Entity<Editor>,
|
||||
}
|
||||
|
||||
impl LaunchMode {
|
||||
pub(super) fn new(
|
||||
past_launch_config: Option<LaunchRequest>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Entity<Self> {
|
||||
let (past_program, past_cwd) = past_launch_config
|
||||
.map(|config| (Some(config.program), config.cwd))
|
||||
.unwrap_or_else(|| (None, None));
|
||||
|
||||
let program = cx.new(|cx| Editor::single_line(window, cx));
|
||||
program.update(cx, |this, cx| {
|
||||
this.set_placeholder_text("Program path", cx);
|
||||
|
||||
if let Some(past_program) = past_program {
|
||||
this.set_text(past_program, window, cx);
|
||||
};
|
||||
});
|
||||
let cwd = cx.new(|cx| Editor::single_line(window, cx));
|
||||
cwd.update(cx, |this, cx| {
|
||||
this.set_placeholder_text("Working Directory", cx);
|
||||
if let Some(past_cwd) = past_cwd {
|
||||
this.set_text(past_cwd.to_string_lossy(), window, cx);
|
||||
};
|
||||
});
|
||||
cx.new(|_| Self { program, cwd })
|
||||
}
|
||||
|
||||
pub(super) fn debug_task(&self, cx: &App) -> task::LaunchRequest {
|
||||
let path = self.cwd.read(cx).text(cx);
|
||||
task::LaunchRequest {
|
||||
program: self.program.read(cx).text(cx),
|
||||
cwd: path.is_empty().not().then(|| PathBuf::from(path)),
|
||||
args: Default::default(),
|
||||
env: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(super) struct AttachMode {
|
||||
pub(super) definition: DebugTaskDefinition,
|
||||
pub(super) attach_picker: Entity<AttachModal>,
|
||||
_subscription: Rc<Subscription>,
|
||||
}
|
||||
|
||||
impl AttachMode {
|
||||
pub(super) fn new(
|
||||
debugger: Option<DebugAdapterName>,
|
||||
workspace: Entity<Workspace>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<NewSessionModal>,
|
||||
) -> Entity<Self> {
|
||||
let definition = DebugTaskDefinition {
|
||||
adapter: debugger.unwrap_or(DebugAdapterName("".into())),
|
||||
label: "Attach New Session Setup".into(),
|
||||
request: dap::DebugRequest::Attach(task::AttachRequest { process_id: None }),
|
||||
initialize_args: None,
|
||||
tcp_connection: None,
|
||||
stop_on_entry: Some(false),
|
||||
};
|
||||
let attach_picker = cx.new(|cx| {
|
||||
let modal = AttachModal::new(definition.clone(), workspace, false, window, cx);
|
||||
window.focus(&modal.focus_handle(cx));
|
||||
|
||||
modal
|
||||
});
|
||||
|
||||
let subscription = cx.subscribe(&attach_picker, |_, _, _, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
});
|
||||
|
||||
cx.new(|_| Self {
|
||||
definition,
|
||||
attach_picker,
|
||||
_subscription: Rc::new(subscription),
|
||||
})
|
||||
}
|
||||
pub(super) fn debug_task(&self) -> task::AttachRequest {
|
||||
task::AttachRequest { process_id: None }
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct DebugScenarioDelegate {
|
||||
task_store: Entity<TaskStore>,
|
||||
candidates: Option<Vec<(TaskSourceKind, DebugScenario)>>,
|
||||
selected_index: usize,
|
||||
matches: Vec<StringMatch>,
|
||||
prompt: String,
|
||||
debug_panel: WeakEntity<DebugPanel>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
}
|
||||
|
||||
impl DebugScenarioDelegate {
|
||||
pub(super) fn new(
|
||||
debug_panel: WeakEntity<DebugPanel>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
task_store: Entity<TaskStore>,
|
||||
) -> Self {
|
||||
Self {
|
||||
task_store,
|
||||
candidates: None,
|
||||
selected_index: 0,
|
||||
matches: Vec::new(),
|
||||
prompt: String::new(),
|
||||
debug_panel,
|
||||
workspace,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PickerDelegate for DebugScenarioDelegate {
|
||||
type ListItem = ui::ListItem;
|
||||
|
||||
fn match_count(&self) -> usize {
|
||||
self.matches.len()
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
self.selected_index
|
||||
}
|
||||
|
||||
fn set_selected_index(
|
||||
&mut self,
|
||||
ix: usize,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<picker::Picker<Self>>,
|
||||
) {
|
||||
self.selected_index = ix;
|
||||
}
|
||||
|
||||
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> std::sync::Arc<str> {
|
||||
"".into()
|
||||
}
|
||||
|
||||
fn update_matches(
|
||||
&mut self,
|
||||
query: String,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<picker::Picker<Self>>,
|
||||
) -> gpui::Task<()> {
|
||||
let candidates: Vec<_> = match &self.candidates {
|
||||
Some(candidates) => candidates
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, (_, candidate))| {
|
||||
StringMatchCandidate::new(index, candidate.label.as_ref())
|
||||
})
|
||||
.collect(),
|
||||
None => {
|
||||
let worktree_ids: Vec<_> = self
|
||||
.workspace
|
||||
.update(cx, |this, cx| {
|
||||
this.visible_worktrees(cx)
|
||||
.map(|tree| tree.read(cx).id())
|
||||
.collect()
|
||||
})
|
||||
.ok()
|
||||
.unwrap_or_default();
|
||||
|
||||
let scenarios: Vec<_> = self
|
||||
.task_store
|
||||
.read(cx)
|
||||
.task_inventory()
|
||||
.map(|item| item.read(cx).list_debug_scenarios(worktree_ids.into_iter()))
|
||||
.unwrap_or_default();
|
||||
|
||||
self.candidates = Some(scenarios.clone());
|
||||
|
||||
scenarios
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, (_, candidate))| {
|
||||
StringMatchCandidate::new(index, candidate.label.as_ref())
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
};
|
||||
|
||||
cx.spawn_in(window, async move |picker, cx| {
|
||||
let matches = fuzzy::match_strings(
|
||||
&candidates,
|
||||
&query,
|
||||
true,
|
||||
1000,
|
||||
&Default::default(),
|
||||
cx.background_executor().clone(),
|
||||
)
|
||||
.await;
|
||||
|
||||
picker
|
||||
.update(cx, |picker, _| {
|
||||
let delegate = &mut picker.delegate;
|
||||
|
||||
delegate.matches = matches;
|
||||
delegate.prompt = query;
|
||||
|
||||
if delegate.matches.is_empty() {
|
||||
delegate.selected_index = 0;
|
||||
} else {
|
||||
delegate.selected_index =
|
||||
delegate.selected_index.min(delegate.matches.len() - 1);
|
||||
}
|
||||
})
|
||||
.log_err();
|
||||
})
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<picker::Picker<Self>>) {
|
||||
let debug_scenario = self
|
||||
.matches
|
||||
.get(self.selected_index())
|
||||
.and_then(|match_candidate| {
|
||||
self.candidates
|
||||
.as_ref()
|
||||
.map(|candidates| candidates[match_candidate.candidate_id].clone())
|
||||
});
|
||||
|
||||
let Some((task_source_kind, debug_scenario)) = debug_scenario else {
|
||||
return;
|
||||
};
|
||||
|
||||
let task_context = if let TaskSourceKind::Worktree {
|
||||
id: worktree_id,
|
||||
directory_in_worktree: _,
|
||||
id_base: _,
|
||||
} = task_source_kind
|
||||
{
|
||||
let workspace = self.workspace.clone();
|
||||
|
||||
cx.spawn_in(window, async move |_, cx| {
|
||||
workspace
|
||||
.update_in(cx, |workspace, window, cx| {
|
||||
tasks_ui::task_contexts(workspace, window, cx)
|
||||
})
|
||||
.ok()?
|
||||
.await
|
||||
.task_context_for_worktree_id(worktree_id)
|
||||
.cloned()
|
||||
.map(|context| (context, Some(worktree_id)))
|
||||
})
|
||||
} else {
|
||||
gpui::Task::ready(None)
|
||||
};
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let (task_context, worktree_id) = task_context.await.unwrap_or_default();
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.delegate
|
||||
.debug_panel
|
||||
.update(cx, |panel, cx| {
|
||||
panel.start_session(
|
||||
debug_scenario,
|
||||
task_context,
|
||||
None,
|
||||
worktree_id,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
|
||||
cx.emit(DismissEvent);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, _: &mut Window, cx: &mut Context<picker::Picker<Self>>) {
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<picker::Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let hit = &self.matches[ix];
|
||||
|
||||
let highlighted_location = HighlightedMatch {
|
||||
text: hit.string.clone(),
|
||||
highlight_positions: hit.positions.clone(),
|
||||
char_count: hit.string.chars().count(),
|
||||
color: Color::Default,
|
||||
};
|
||||
|
||||
let icon = Icon::new(IconName::FileTree)
|
||||
.color(Color::Muted)
|
||||
.size(ui::IconSize::Small);
|
||||
|
||||
Some(
|
||||
ListItem::new(SharedString::from(format!("debug-scenario-selection-{ix}")))
|
||||
.inset(true)
|
||||
.start_slot::<Icon>(icon)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.toggle_state(selected)
|
||||
.child(highlighted_location.render(window, cx)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use collections::HashMap;
|
||||
use dap::Capabilities;
|
||||
use dap::{Capabilities, adapters::DebugAdapterName};
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use gpui::{Axis, Context, Entity, EntityId, Focusable, Subscription, WeakEntity, Window};
|
||||
use project::Project;
|
||||
@@ -69,19 +69,22 @@ impl From<DebuggerPaneItem> for SharedString {
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub(crate) struct SerializedAxis(pub Axis);
|
||||
pub(crate) struct SerializedLayout {
|
||||
pub(crate) panes: SerializedPaneLayout,
|
||||
pub(crate) dock_axis: Axis,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub(crate) enum SerializedPaneLayout {
|
||||
Pane(SerializedPane),
|
||||
Group {
|
||||
axis: SerializedAxis,
|
||||
axis: Axis,
|
||||
flexes: Option<Vec<f32>>,
|
||||
children: Vec<SerializedPaneLayout>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub(crate) struct SerializedPane {
|
||||
pub children: Vec<DebuggerPaneItem>,
|
||||
pub active_item: Option<DebuggerPaneItem>,
|
||||
@@ -90,8 +93,8 @@ pub(crate) struct SerializedPane {
|
||||
const DEBUGGER_PANEL_PREFIX: &str = "debugger_panel_";
|
||||
|
||||
pub(crate) async fn serialize_pane_layout(
|
||||
adapter_name: SharedString,
|
||||
pane_group: SerializedPaneLayout,
|
||||
adapter_name: DebugAdapterName,
|
||||
pane_group: SerializedLayout,
|
||||
) -> anyhow::Result<()> {
|
||||
if let Ok(serialized_pane_group) = serde_json::to_string(&pane_group) {
|
||||
KEY_VALUE_STORE
|
||||
@@ -107,10 +110,18 @@ pub(crate) async fn serialize_pane_layout(
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn build_serialized_pane_layout(
|
||||
pub(crate) fn build_serialized_layout(
|
||||
pane_group: &Member,
|
||||
cx: &mut App,
|
||||
) -> SerializedPaneLayout {
|
||||
dock_axis: Axis,
|
||||
cx: &App,
|
||||
) -> SerializedLayout {
|
||||
SerializedLayout {
|
||||
dock_axis,
|
||||
panes: build_serialized_pane_layout(pane_group, cx),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn build_serialized_pane_layout(pane_group: &Member, cx: &App) -> SerializedPaneLayout {
|
||||
match pane_group {
|
||||
Member::Axis(PaneAxis {
|
||||
axis,
|
||||
@@ -118,7 +129,7 @@ pub(crate) fn build_serialized_pane_layout(
|
||||
flexes,
|
||||
bounding_boxes: _,
|
||||
}) => SerializedPaneLayout::Group {
|
||||
axis: SerializedAxis(*axis),
|
||||
axis: *axis,
|
||||
children: members
|
||||
.iter()
|
||||
.map(|member| build_serialized_pane_layout(member, cx))
|
||||
@@ -129,7 +140,7 @@ pub(crate) fn build_serialized_pane_layout(
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize_pane(pane: &Entity<Pane>, cx: &mut App) -> SerializedPane {
|
||||
fn serialize_pane(pane: &Entity<Pane>, cx: &App) -> SerializedPane {
|
||||
let pane = pane.read(cx);
|
||||
let children = pane
|
||||
.items()
|
||||
@@ -150,20 +161,21 @@ fn serialize_pane(pane: &Entity<Pane>, cx: &mut App) -> SerializedPane {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn get_serialized_pane_layout(
|
||||
pub(crate) async fn get_serialized_layout(
|
||||
adapter_name: impl AsRef<str>,
|
||||
) -> Option<SerializedPaneLayout> {
|
||||
) -> Option<SerializedLayout> {
|
||||
let key = format!("{DEBUGGER_PANEL_PREFIX}-{}", adapter_name.as_ref());
|
||||
|
||||
KEY_VALUE_STORE
|
||||
.read_kvp(&key)
|
||||
.log_err()
|
||||
.flatten()
|
||||
.and_then(|value| serde_json::from_str::<SerializedPaneLayout>(&value).ok())
|
||||
.and_then(|value| serde_json::from_str::<SerializedLayout>(&value).ok())
|
||||
}
|
||||
|
||||
pub(crate) fn deserialize_pane_layout(
|
||||
serialized: SerializedPaneLayout,
|
||||
should_invert: bool,
|
||||
workspace: &WeakEntity<Workspace>,
|
||||
project: &Entity<Project>,
|
||||
stack_frame_list: &Entity<StackFrameList>,
|
||||
@@ -187,6 +199,7 @@ pub(crate) fn deserialize_pane_layout(
|
||||
for child in children {
|
||||
if let Some(new_member) = deserialize_pane_layout(
|
||||
child,
|
||||
should_invert,
|
||||
workspace,
|
||||
project,
|
||||
stack_frame_list,
|
||||
@@ -213,7 +226,7 @@ pub(crate) fn deserialize_pane_layout(
|
||||
}
|
||||
|
||||
Some(Member::Axis(PaneAxis::load(
|
||||
axis.0,
|
||||
if should_invert { axis.invert() } else { axis },
|
||||
members,
|
||||
flexes.clone(),
|
||||
)))
|
||||
@@ -307,3 +320,28 @@ pub(crate) fn deserialize_pane_layout(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl SerializedPaneLayout {
|
||||
pub(crate) fn in_order(&self) -> Vec<SerializedPaneLayout> {
|
||||
let mut panes = vec![];
|
||||
|
||||
Self::inner_in_order(&self, &mut panes);
|
||||
panes
|
||||
}
|
||||
|
||||
fn inner_in_order(&self, panes: &mut Vec<SerializedPaneLayout>) {
|
||||
match self {
|
||||
SerializedPaneLayout::Pane(_) => panes.push((*self).clone()),
|
||||
SerializedPaneLayout::Group {
|
||||
axis: _,
|
||||
flexes: _,
|
||||
children,
|
||||
} => {
|
||||
for child in children {
|
||||
child.inner_in_order(panes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@ pub mod running;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use dap::client::SessionId;
|
||||
use gpui::{App, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity};
|
||||
use gpui::{
|
||||
App, Axis, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity,
|
||||
};
|
||||
use project::Project;
|
||||
use project::debugger::session::Session;
|
||||
use project::worktree_store::WorktreeStore;
|
||||
@@ -15,8 +17,7 @@ use workspace::{
|
||||
item::{self, Item},
|
||||
};
|
||||
|
||||
use crate::debugger_panel::DebugPanel;
|
||||
use crate::persistence::SerializedPaneLayout;
|
||||
use crate::{debugger_panel::DebugPanel, persistence::SerializedLayout};
|
||||
|
||||
pub struct DebugSession {
|
||||
remote_id: Option<workspace::ViewId>,
|
||||
@@ -40,7 +41,8 @@ impl DebugSession {
|
||||
workspace: WeakEntity<Workspace>,
|
||||
session: Entity<Session>,
|
||||
_debug_panel: WeakEntity<DebugPanel>,
|
||||
serialized_pane_layout: Option<SerializedPaneLayout>,
|
||||
serialized_layout: Option<SerializedLayout>,
|
||||
dock_axis: Axis,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Entity<Self> {
|
||||
@@ -49,7 +51,8 @@ impl DebugSession {
|
||||
session.clone(),
|
||||
project.clone(),
|
||||
workspace.clone(),
|
||||
serialized_pane_layout,
|
||||
serialized_layout,
|
||||
dock_axis,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
|
||||
@@ -7,7 +7,7 @@ pub mod variable_list;
|
||||
|
||||
use std::{any::Any, ops::ControlFlow, path::PathBuf, sync::Arc, time::Duration};
|
||||
|
||||
use crate::persistence::{self, DebuggerPaneItem, SerializedPaneLayout};
|
||||
use crate::persistence::{self, DebuggerPaneItem, SerializedLayout};
|
||||
|
||||
use super::DebugPanelItemEvent;
|
||||
use anyhow::{Result, anyhow};
|
||||
@@ -15,18 +15,21 @@ use breakpoint_list::BreakpointList;
|
||||
use collections::{HashMap, IndexMap};
|
||||
use console::Console;
|
||||
use dap::{
|
||||
Capabilities, RunInTerminalRequestArguments, Thread, client::SessionId,
|
||||
Capabilities, RunInTerminalRequestArguments, Thread,
|
||||
adapters::{DebugAdapterName, DebugTaskDefinition},
|
||||
client::SessionId,
|
||||
debugger_settings::DebuggerSettings,
|
||||
};
|
||||
use futures::{SinkExt, channel::mpsc};
|
||||
use gpui::{
|
||||
Action as _, AnyView, AppContext, Entity, EntityId, EventEmitter, FocusHandle, Focusable,
|
||||
Action as _, AnyView, AppContext, Axis, Entity, EntityId, EventEmitter, FocusHandle, Focusable,
|
||||
NoAction, Pixels, Point, Subscription, Task, WeakEntity,
|
||||
};
|
||||
use language::Buffer;
|
||||
use loaded_source_list::LoadedSourceList;
|
||||
use module_list::ModuleList;
|
||||
use project::{
|
||||
Project,
|
||||
Project, WorktreeId,
|
||||
debugger::session::{Session, SessionEvent, ThreadId, ThreadStatus},
|
||||
terminals::TerminalKind,
|
||||
};
|
||||
@@ -34,6 +37,10 @@ use rpc::proto::ViewId;
|
||||
use serde_json::Value;
|
||||
use settings::Settings;
|
||||
use stack_frame_list::StackFrameList;
|
||||
use task::{
|
||||
BuildTaskDefinition, DebugScenario, LaunchRequest, ShellBuilder, SpawnInTerminal, TaskContext,
|
||||
substitute_variables_in_map, substitute_variables_in_str,
|
||||
};
|
||||
use terminal_view::TerminalView;
|
||||
use ui::{
|
||||
ActiveTheme, AnyElement, App, ButtonCommon as _, Clickable as _, Context, ContextMenu,
|
||||
@@ -66,6 +73,7 @@ pub struct RunningState {
|
||||
panes: PaneGroup,
|
||||
active_pane: Option<Entity<Pane>>,
|
||||
pane_close_subscriptions: HashMap<EntityId, Subscription>,
|
||||
dock_axis: Axis,
|
||||
_schedule_serialize: Option<Task<()>>,
|
||||
}
|
||||
|
||||
@@ -188,7 +196,7 @@ impl Render for SubView {
|
||||
cx.notify();
|
||||
}))
|
||||
.size_full()
|
||||
// Add border uncoditionally to prevent layout shifts on focus changes.
|
||||
// Add border unconditionally to prevent layout shifts on focus changes.
|
||||
.border_1()
|
||||
.when(self.pane_focus_handle.contains_focused(window, cx), |el| {
|
||||
el.border_color(cx.theme().colors().pane_focused_border)
|
||||
@@ -503,7 +511,8 @@ impl RunningState {
|
||||
session: Entity<Session>,
|
||||
project: Entity<Project>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
serialized_pane_layout: Option<SerializedPaneLayout>,
|
||||
serialized_pane_layout: Option<SerializedLayout>,
|
||||
dock_axis: Axis,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
@@ -582,7 +591,8 @@ impl RunningState {
|
||||
let mut pane_close_subscriptions = HashMap::default();
|
||||
let panes = if let Some(root) = serialized_pane_layout.and_then(|serialized_layout| {
|
||||
persistence::deserialize_pane_layout(
|
||||
serialized_layout,
|
||||
serialized_layout.panes,
|
||||
dock_axis != serialized_layout.dock_axis,
|
||||
&workspace,
|
||||
&project,
|
||||
&stack_frame_list,
|
||||
@@ -610,6 +620,7 @@ impl RunningState {
|
||||
&loaded_source_list,
|
||||
&console,
|
||||
&breakpoint_list,
|
||||
dock_axis,
|
||||
&mut pane_close_subscriptions,
|
||||
window,
|
||||
cx,
|
||||
@@ -636,6 +647,7 @@ impl RunningState {
|
||||
loaded_sources_list: loaded_source_list,
|
||||
pane_close_subscriptions,
|
||||
debug_terminal,
|
||||
dock_axis,
|
||||
_schedule_serialize: None,
|
||||
}
|
||||
}
|
||||
@@ -667,6 +679,196 @@ impl RunningState {
|
||||
self.panes.pane_at_pixel_position(position).is_some()
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_scenario(
|
||||
&self,
|
||||
scenario: DebugScenario,
|
||||
task_context: TaskContext,
|
||||
buffer: Option<Entity<Buffer>>,
|
||||
worktree_id: Option<WorktreeId>,
|
||||
window: &Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<DebugTaskDefinition>> {
|
||||
let Some(workspace) = self.workspace.upgrade() else {
|
||||
return Task::ready(Err(anyhow!("no workspace")));
|
||||
};
|
||||
let project = workspace.read(cx).project().clone();
|
||||
let dap_store = project.read(cx).dap_store().downgrade();
|
||||
let task_store = project.read(cx).task_store().downgrade();
|
||||
let weak_project = project.downgrade();
|
||||
let weak_workspace = workspace.downgrade();
|
||||
let is_local = project.read(cx).is_local();
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let DebugScenario {
|
||||
adapter,
|
||||
label,
|
||||
build,
|
||||
request,
|
||||
initialize_args,
|
||||
tcp_connection,
|
||||
stop_on_entry,
|
||||
} = scenario;
|
||||
let build_output = if let Some(build) = build {
|
||||
let (task, locator_name) = match build {
|
||||
BuildTaskDefinition::Template {
|
||||
task_template,
|
||||
locator_name,
|
||||
} => (task_template, locator_name),
|
||||
BuildTaskDefinition::ByName(ref label) => {
|
||||
let Some(task) = task_store.update(cx, |this, cx| {
|
||||
this.task_inventory().and_then(|inventory| {
|
||||
inventory.read(cx).task_template_by_label(
|
||||
buffer,
|
||||
worktree_id,
|
||||
&label,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})?
|
||||
else {
|
||||
anyhow::bail!("Couldn't find task template for {:?}", build)
|
||||
};
|
||||
(task, None)
|
||||
}
|
||||
};
|
||||
let locator_name = if let Some(locator_name) = locator_name {
|
||||
debug_assert!(request.is_none());
|
||||
Some(locator_name)
|
||||
} else if request.is_none() {
|
||||
dap_store
|
||||
.update(cx, |this, cx| {
|
||||
this.debug_scenario_for_build_task(task.clone(), adapter.clone(), cx)
|
||||
.and_then(|scenario| match scenario.build {
|
||||
Some(BuildTaskDefinition::Template {
|
||||
locator_name, ..
|
||||
}) => locator_name,
|
||||
_ => None,
|
||||
})
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let Some(task) = task.resolve_task("debug-build-task", &task_context) else {
|
||||
anyhow::bail!("Could not resolve task variables within a debug scenario");
|
||||
};
|
||||
|
||||
let builder = ShellBuilder::new(is_local, &task.resolved.shell);
|
||||
let command_label = builder.command_label(&task.resolved.command_label);
|
||||
let (command, args) =
|
||||
builder.build(task.resolved.command.clone(), &task.resolved.args);
|
||||
|
||||
let task_with_shell = SpawnInTerminal {
|
||||
command_label,
|
||||
command,
|
||||
args,
|
||||
..task.resolved.clone()
|
||||
};
|
||||
let terminal = project
|
||||
.update_in(cx, |project, window, cx| {
|
||||
project.create_terminal(
|
||||
TerminalKind::Task(task_with_shell.clone()),
|
||||
window.window_handle(),
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
let terminal_view = cx.new_window_entity(|window, cx| {
|
||||
TerminalView::new(
|
||||
terminal.clone(),
|
||||
weak_workspace,
|
||||
None,
|
||||
weak_project,
|
||||
false,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})?;
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.ensure_pane_item(DebuggerPaneItem::Terminal, window, cx);
|
||||
this.debug_terminal.update(cx, |debug_terminal, cx| {
|
||||
debug_terminal.terminal = Some(terminal_view);
|
||||
cx.notify();
|
||||
});
|
||||
})?;
|
||||
|
||||
let exit_status = terminal
|
||||
.read_with(cx, |terminal, cx| terminal.wait_for_completed_task(cx))?
|
||||
.await
|
||||
.ok_or_else(|| anyhow!("Failed to wait for completed task"))?;
|
||||
|
||||
if !exit_status.success() {
|
||||
anyhow::bail!("Build failed");
|
||||
}
|
||||
Some((task.resolved.clone(), locator_name))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let request = if let Some(request) = request {
|
||||
request
|
||||
} else if let Some((task, locator_name)) = build_output {
|
||||
let locator_name = locator_name
|
||||
.ok_or_else(|| anyhow!("Could not find a valid locator for a build task"))?;
|
||||
dap_store
|
||||
.update(cx, |this, cx| {
|
||||
this.run_debug_locator(&locator_name, task, cx)
|
||||
})?
|
||||
.await?
|
||||
} else {
|
||||
return Err(anyhow!("No request or build provided"));
|
||||
};
|
||||
let request = match request {
|
||||
dap::DebugRequest::Launch(launch_request) => {
|
||||
let cwd = match launch_request.cwd.as_deref().and_then(|path| path.to_str()) {
|
||||
Some(cwd) => {
|
||||
let substituted_cwd = substitute_variables_in_str(&cwd, &task_context)
|
||||
.ok_or_else(|| anyhow!("Failed to substitute variables in cwd"))?;
|
||||
Some(PathBuf::from(substituted_cwd))
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
let env = substitute_variables_in_map(
|
||||
&launch_request.env.into_iter().collect(),
|
||||
&task_context,
|
||||
)
|
||||
.ok_or_else(|| anyhow!("Failed to substitute variables in env"))?
|
||||
.into_iter()
|
||||
.collect();
|
||||
let new_launch_request = LaunchRequest {
|
||||
program: substitute_variables_in_str(
|
||||
&launch_request.program,
|
||||
&task_context,
|
||||
)
|
||||
.ok_or_else(|| anyhow!("Failed to substitute variables in program"))?,
|
||||
args: launch_request
|
||||
.args
|
||||
.into_iter()
|
||||
.map(|arg| substitute_variables_in_str(&arg, &task_context))
|
||||
.collect::<Option<Vec<_>>>()
|
||||
.ok_or_else(|| anyhow!("Failed to substitute variables in args"))?,
|
||||
cwd,
|
||||
env,
|
||||
};
|
||||
|
||||
dap::DebugRequest::Launch(new_launch_request)
|
||||
}
|
||||
request @ dap::DebugRequest::Attach(_) => request,
|
||||
};
|
||||
Ok(DebugTaskDefinition {
|
||||
label,
|
||||
adapter: DebugAdapterName(adapter),
|
||||
request,
|
||||
initialize_args,
|
||||
stop_on_entry,
|
||||
tcp_connection,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_run_in_terminal(
|
||||
&self,
|
||||
request: &RunInTerminalRequestArguments,
|
||||
@@ -912,12 +1114,16 @@ impl RunningState {
|
||||
.timer(Duration::from_millis(100))
|
||||
.await;
|
||||
|
||||
let Some((adapter_name, pane_group)) = this
|
||||
.update(cx, |this, cx| {
|
||||
let adapter_name = this.session.read(cx).adapter_name();
|
||||
let Some((adapter_name, pane_layout)) = this
|
||||
.read_with(cx, |this, cx| {
|
||||
let adapter_name = this.session.read(cx).adapter();
|
||||
(
|
||||
adapter_name,
|
||||
persistence::build_serialized_pane_layout(&this.panes.root, cx),
|
||||
persistence::build_serialized_layout(
|
||||
&this.panes.root,
|
||||
this.dock_axis,
|
||||
cx,
|
||||
),
|
||||
)
|
||||
})
|
||||
.ok()
|
||||
@@ -925,7 +1131,7 @@ impl RunningState {
|
||||
return;
|
||||
};
|
||||
|
||||
persistence::serialize_pane_layout(adapter_name, pane_group)
|
||||
persistence::serialize_pane_layout(adapter_name, pane_layout)
|
||||
.await
|
||||
.log_err();
|
||||
|
||||
@@ -1051,6 +1257,11 @@ impl RunningState {
|
||||
&self.variable_list
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn serialized_layout(&self, cx: &App) -> SerializedLayout {
|
||||
persistence::build_serialized_layout(&self.panes.root, self.dock_axis, cx)
|
||||
}
|
||||
|
||||
pub fn capabilities(&self, cx: &App) -> Capabilities {
|
||||
self.session().read(cx).capabilities().clone()
|
||||
}
|
||||
@@ -1264,6 +1475,7 @@ impl RunningState {
|
||||
loaded_source_list: &Entity<LoadedSourceList>,
|
||||
console: &Entity<Console>,
|
||||
breakpoints: &Entity<BreakpointList>,
|
||||
dock_axis: Axis,
|
||||
subscriptions: &mut HashMap<EntityId, Subscription>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<'_, RunningState>,
|
||||
@@ -1384,7 +1596,7 @@ impl RunningState {
|
||||
);
|
||||
|
||||
let group_root = workspace::PaneAxis::new(
|
||||
gpui::Axis::Horizontal,
|
||||
dock_axis.invert(),
|
||||
[leftmost_pane, center_pane, rightmost_pane]
|
||||
.into_iter()
|
||||
.map(workspace::Member::Pane)
|
||||
@@ -1393,6 +1605,11 @@ impl RunningState {
|
||||
|
||||
Member::Axis(group_root)
|
||||
}
|
||||
|
||||
pub(crate) fn invert_axies(&mut self) {
|
||||
self.dock_axis = self.dock_axis.invert();
|
||||
self.panes.invert_axies();
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DebugPanelItemEvent> for RunningState {}
|
||||
|
||||
@@ -148,23 +148,6 @@ impl Console {
|
||||
expression
|
||||
});
|
||||
|
||||
self.add_messages(
|
||||
[OutputEvent {
|
||||
category: None,
|
||||
output: format!("> {expression}"),
|
||||
group: None,
|
||||
variables_reference: None,
|
||||
source: None,
|
||||
line: None,
|
||||
column: None,
|
||||
data: None,
|
||||
location_reference: None,
|
||||
}]
|
||||
.iter(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
self.session.update(cx, |session, cx| {
|
||||
session
|
||||
.evaluate(
|
||||
|
||||
@@ -23,6 +23,8 @@ mod debugger_panel;
|
||||
#[cfg(test)]
|
||||
mod module_list;
|
||||
#[cfg(test)]
|
||||
mod persistence;
|
||||
#[cfg(test)]
|
||||
mod stack_frame_list;
|
||||
#[cfg(test)]
|
||||
mod variable_list;
|
||||
|
||||
@@ -444,11 +444,13 @@ async fn test_handle_start_debugging_request(
|
||||
.read(cx)
|
||||
.session(cx);
|
||||
let parent_session = active_session.read(cx).parent_session().unwrap();
|
||||
let mut original_binary = parent_session.read(cx).binary().clone();
|
||||
original_binary.request_args = StartDebuggingRequestArguments {
|
||||
request: StartDebuggingRequestArgumentsRequest::Launch,
|
||||
configuration: fake_config.clone(),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
active_session.read(cx).definition(),
|
||||
parent_session.read(cx).definition()
|
||||
);
|
||||
assert_eq!(active_session.read(cx).binary(), &original_binary);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
@@ -1663,6 +1665,33 @@ async fn test_active_debug_line_setting(executor: BackgroundExecutor, cx: &mut T
|
||||
"Second stacktrace request handler was not called"
|
||||
);
|
||||
|
||||
client
|
||||
.fake_event(dap::messages::Events::Continued(dap::ContinuedEvent {
|
||||
thread_id: 0,
|
||||
all_threads_continued: Some(true),
|
||||
}))
|
||||
.await;
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
second_editor.update(cx, |editor, _| {
|
||||
let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
|
||||
|
||||
assert!(
|
||||
active_debug_lines.is_empty(),
|
||||
"There shouldn't be any active debug lines"
|
||||
);
|
||||
});
|
||||
|
||||
main_editor.update(cx, |editor, _| {
|
||||
let active_debug_lines: Vec<_> = editor.highlighted_rows::<ActiveDebugLine>().collect();
|
||||
|
||||
assert!(
|
||||
active_debug_lines.is_empty(),
|
||||
"There shouldn't be any active debug lines"
|
||||
);
|
||||
});
|
||||
|
||||
// Clean up
|
||||
let shutdown_session = project.update(cx, |project, cx| {
|
||||
project.dap_store().update(cx, |dap_store, cx| {
|
||||
|
||||
131
crates/debugger_ui/src/tests/persistence.rs
Normal file
131
crates/debugger_ui/src/tests/persistence.rs
Normal file
@@ -0,0 +1,131 @@
|
||||
use std::iter::zip;
|
||||
|
||||
use crate::{
|
||||
debugger_panel::DebugPanel,
|
||||
persistence::SerializedPaneLayout,
|
||||
tests::{init_test, init_test_workspace, start_debug_session},
|
||||
};
|
||||
use dap::{StoppedEvent, StoppedEventReason, messages::Events};
|
||||
use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext};
|
||||
use project::{FakeFs, Project};
|
||||
use serde_json::json;
|
||||
use util::path;
|
||||
use workspace::{Panel, dock::DockPosition};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_invert_axis_on_panel_position_change(
|
||||
executor: BackgroundExecutor,
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(executor.clone());
|
||||
fs.insert_tree(
|
||||
path!("/project"),
|
||||
json!({
|
||||
"main.rs": "fn main() {\n println!(\"Hello, world!\");\n}",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
|
||||
let workspace = init_test_workspace(&project, cx).await;
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
|
||||
// Start a debug session
|
||||
let session = start_debug_session(&workspace, cx, |_| {}).unwrap();
|
||||
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
|
||||
|
||||
// Setup thread response
|
||||
client.on_request::<dap::requests::Threads, _>(move |_, _| {
|
||||
Ok(dap::ThreadsResponse { threads: vec![] })
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
client
|
||||
.fake_event(Events::Stopped(StoppedEvent {
|
||||
reason: StoppedEventReason::Pause,
|
||||
description: None,
|
||||
thread_id: Some(1),
|
||||
preserve_focus_hint: None,
|
||||
text: None,
|
||||
all_threads_stopped: None,
|
||||
hit_breakpoint_ids: None,
|
||||
}))
|
||||
.await;
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
let (debug_panel, dock_position) = workspace
|
||||
.update(cx, |workspace, window, cx| {
|
||||
let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap();
|
||||
let dock_position = debug_panel.read(cx).position(window, cx);
|
||||
(debug_panel, dock_position)
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
dock_position,
|
||||
DockPosition::Bottom,
|
||||
"Default dock position should be bottom for debug panel"
|
||||
);
|
||||
|
||||
let pre_serialized_layout = debug_panel
|
||||
.read_with(cx, |panel, cx| {
|
||||
panel
|
||||
.active_session()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.running_state()
|
||||
.read(cx)
|
||||
.serialized_layout(cx)
|
||||
})
|
||||
.panes;
|
||||
|
||||
let post_serialized_layout = debug_panel
|
||||
.update_in(cx, |panel, window, cx| {
|
||||
panel.set_position(DockPosition::Right, window, cx);
|
||||
|
||||
panel
|
||||
.active_session()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.running_state()
|
||||
.read(cx)
|
||||
.serialized_layout(cx)
|
||||
})
|
||||
.panes;
|
||||
|
||||
let pre_panes = pre_serialized_layout.in_order();
|
||||
let post_panes = post_serialized_layout.in_order();
|
||||
|
||||
assert_eq!(pre_panes.len(), post_panes.len());
|
||||
|
||||
for (pre, post) in zip(pre_panes, post_panes) {
|
||||
match (pre, post) {
|
||||
(
|
||||
SerializedPaneLayout::Group {
|
||||
axis: pre_axis,
|
||||
flexes: pre_flexes,
|
||||
children: _,
|
||||
},
|
||||
SerializedPaneLayout::Group {
|
||||
axis: post_axis,
|
||||
flexes: post_flexes,
|
||||
children: _,
|
||||
},
|
||||
) => {
|
||||
assert_ne!(pre_axis, post_axis);
|
||||
assert_eq!(pre_flexes, post_flexes);
|
||||
}
|
||||
(SerializedPaneLayout::Pane(pre_pane), SerializedPaneLayout::Pane(post_pane)) => {
|
||||
assert_eq!(pre_pane.children, post_pane.children);
|
||||
assert_eq!(pre_pane.active_item, post_pane.active_item);
|
||||
}
|
||||
_ => {
|
||||
panic!("Variants don't match")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ component.workspace = true
|
||||
ctor.workspace = true
|
||||
editor.workspace = true
|
||||
env_logger.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
indoc.workspace = true
|
||||
language.workspace = true
|
||||
@@ -29,6 +30,7 @@ markdown.workspace = true
|
||||
project.workspace = true
|
||||
rand.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
text.workspace = true
|
||||
theme.workspace = true
|
||||
|
||||
@@ -14,6 +14,7 @@ use editor::{
|
||||
display_map::{BlockPlacement, BlockProperties, BlockStyle, CustomBlockId},
|
||||
scroll::Autoscroll,
|
||||
};
|
||||
use futures::future::join_all;
|
||||
use gpui::{
|
||||
AnyElement, AnyView, App, AsyncApp, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
Global, InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled,
|
||||
@@ -23,13 +24,15 @@ use language::{
|
||||
Bias, Buffer, BufferRow, BufferSnapshot, DiagnosticEntry, Point, ToTreeSitterPoint,
|
||||
};
|
||||
use lsp::DiagnosticSeverity;
|
||||
|
||||
use project::{DiagnosticSummary, Project, ProjectPath, project_settings::ProjectSettings};
|
||||
use project::{
|
||||
DiagnosticSummary, Project, ProjectPath,
|
||||
lsp_store::rust_analyzer_ext::{cancel_flycheck, run_flycheck},
|
||||
project_settings::ProjectSettings,
|
||||
};
|
||||
use settings::Settings;
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
cmp,
|
||||
cmp::Ordering,
|
||||
cmp::{self, Ordering},
|
||||
ops::{Range, RangeInclusive},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
@@ -45,7 +48,10 @@ use workspace::{
|
||||
searchable::SearchableItemHandle,
|
||||
};
|
||||
|
||||
actions!(diagnostics, [Deploy, ToggleWarnings]);
|
||||
actions!(
|
||||
diagnostics,
|
||||
[Deploy, ToggleWarnings, ToggleDiagnosticsRefresh]
|
||||
);
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct IncludeWarnings(bool);
|
||||
@@ -68,9 +74,16 @@ pub(crate) struct ProjectDiagnosticsEditor {
|
||||
paths_to_update: BTreeSet<ProjectPath>,
|
||||
include_warnings: bool,
|
||||
update_excerpts_task: Option<Task<Result<()>>>,
|
||||
cargo_diagnostics_fetch: CargoDiagnosticsFetchState,
|
||||
_subscription: Subscription,
|
||||
}
|
||||
|
||||
struct CargoDiagnosticsFetchState {
|
||||
fetch_task: Option<Task<()>>,
|
||||
cancel_task: Option<Task<()>>,
|
||||
diagnostic_sources: Arc<Vec<ProjectPath>>,
|
||||
}
|
||||
|
||||
impl EventEmitter<EditorEvent> for ProjectDiagnosticsEditor {}
|
||||
|
||||
const DIAGNOSTICS_UPDATE_DELAY: Duration = Duration::from_millis(50);
|
||||
@@ -126,6 +139,7 @@ impl Render for ProjectDiagnosticsEditor {
|
||||
.track_focus(&self.focus_handle(cx))
|
||||
.size_full()
|
||||
.on_action(cx.listener(Self::toggle_warnings))
|
||||
.on_action(cx.listener(Self::toggle_diagnostics_refresh))
|
||||
.child(child)
|
||||
}
|
||||
}
|
||||
@@ -212,7 +226,11 @@ impl ProjectDiagnosticsEditor {
|
||||
cx.observe_global_in::<IncludeWarnings>(window, |this, window, cx| {
|
||||
this.include_warnings = cx.global::<IncludeWarnings>().0;
|
||||
this.diagnostics.clear();
|
||||
this.update_all_excerpts(window, cx);
|
||||
this.update_all_diagnostics(false, window, cx);
|
||||
})
|
||||
.detach();
|
||||
cx.observe_release(&cx.entity(), |editor, _, cx| {
|
||||
editor.stop_cargo_diagnostics_fetch(cx);
|
||||
})
|
||||
.detach();
|
||||
|
||||
@@ -229,9 +247,14 @@ impl ProjectDiagnosticsEditor {
|
||||
editor,
|
||||
paths_to_update: Default::default(),
|
||||
update_excerpts_task: None,
|
||||
cargo_diagnostics_fetch: CargoDiagnosticsFetchState {
|
||||
fetch_task: None,
|
||||
cancel_task: None,
|
||||
diagnostic_sources: Arc::new(Vec::new()),
|
||||
},
|
||||
_subscription: project_event_subscription,
|
||||
};
|
||||
this.update_all_excerpts(window, cx);
|
||||
this.update_all_diagnostics(true, window, cx);
|
||||
this
|
||||
}
|
||||
|
||||
@@ -239,15 +262,17 @@ impl ProjectDiagnosticsEditor {
|
||||
if self.update_excerpts_task.is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
let project_handle = self.project.clone();
|
||||
self.update_excerpts_task = Some(cx.spawn_in(window, async move |this, cx| {
|
||||
cx.background_executor()
|
||||
.timer(DIAGNOSTICS_UPDATE_DELAY)
|
||||
.await;
|
||||
loop {
|
||||
let Some(path) = this.update(cx, |this, _| {
|
||||
let Some(path) = this.update(cx, |this, cx| {
|
||||
let Some(path) = this.paths_to_update.pop_first() else {
|
||||
this.update_excerpts_task.take();
|
||||
this.update_excerpts_task = None;
|
||||
cx.notify();
|
||||
return None;
|
||||
};
|
||||
Some(path)
|
||||
@@ -307,6 +332,32 @@ impl ProjectDiagnosticsEditor {
|
||||
cx.set_global(IncludeWarnings(!self.include_warnings));
|
||||
}
|
||||
|
||||
fn toggle_diagnostics_refresh(
|
||||
&mut self,
|
||||
_: &ToggleDiagnosticsRefresh,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let fetch_cargo_diagnostics = ProjectSettings::get_global(cx)
|
||||
.diagnostics
|
||||
.fetch_cargo_diagnostics();
|
||||
|
||||
if fetch_cargo_diagnostics {
|
||||
if self.cargo_diagnostics_fetch.fetch_task.is_some() {
|
||||
self.stop_cargo_diagnostics_fetch(cx);
|
||||
} else {
|
||||
self.update_all_diagnostics(false, window, cx);
|
||||
}
|
||||
} else {
|
||||
if self.update_excerpts_task.is_some() {
|
||||
self.update_excerpts_task = None;
|
||||
} else {
|
||||
self.update_all_diagnostics(false, window, cx);
|
||||
}
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn focus_in(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.focus_handle.is_focused(window) && !self.multibuffer.read(cx).is_empty() {
|
||||
self.editor.focus_handle(cx).focus(window)
|
||||
@@ -320,6 +371,73 @@ impl ProjectDiagnosticsEditor {
|
||||
}
|
||||
}
|
||||
|
||||
fn update_all_diagnostics(
|
||||
&mut self,
|
||||
first_launch: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let cargo_diagnostics_sources = self.cargo_diagnostics_sources(cx);
|
||||
if cargo_diagnostics_sources.is_empty() {
|
||||
self.update_all_excerpts(window, cx);
|
||||
} else if first_launch && !self.summary.is_empty() {
|
||||
self.update_all_excerpts(window, cx);
|
||||
} else {
|
||||
self.fetch_cargo_diagnostics(Arc::new(cargo_diagnostics_sources), cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn fetch_cargo_diagnostics(
|
||||
&mut self,
|
||||
diagnostics_sources: Arc<Vec<ProjectPath>>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let project = self.project.clone();
|
||||
self.cargo_diagnostics_fetch.cancel_task = None;
|
||||
self.cargo_diagnostics_fetch.fetch_task = None;
|
||||
self.cargo_diagnostics_fetch.diagnostic_sources = diagnostics_sources.clone();
|
||||
if self.cargo_diagnostics_fetch.diagnostic_sources.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.cargo_diagnostics_fetch.fetch_task = Some(cx.spawn(async move |editor, cx| {
|
||||
let mut fetch_tasks = Vec::new();
|
||||
for buffer_path in diagnostics_sources.iter().cloned() {
|
||||
if cx
|
||||
.update(|cx| {
|
||||
fetch_tasks.push(run_flycheck(project.clone(), buffer_path, cx));
|
||||
})
|
||||
.is_err()
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let _ = join_all(fetch_tasks).await;
|
||||
editor
|
||||
.update(cx, |editor, _| {
|
||||
editor.cargo_diagnostics_fetch.fetch_task = None;
|
||||
})
|
||||
.ok();
|
||||
}));
|
||||
}
|
||||
|
||||
fn stop_cargo_diagnostics_fetch(&mut self, cx: &mut App) {
|
||||
self.cargo_diagnostics_fetch.fetch_task = None;
|
||||
let mut cancel_gasks = Vec::new();
|
||||
for buffer_path in std::mem::take(&mut self.cargo_diagnostics_fetch.diagnostic_sources)
|
||||
.iter()
|
||||
.cloned()
|
||||
{
|
||||
cancel_gasks.push(cancel_flycheck(self.project.clone(), buffer_path, cx));
|
||||
}
|
||||
|
||||
self.cargo_diagnostics_fetch.cancel_task = Some(cx.background_spawn(async move {
|
||||
let _ = join_all(cancel_gasks).await;
|
||||
log::info!("Finished fetching cargo diagnostics");
|
||||
}));
|
||||
}
|
||||
|
||||
/// Enqueue an update of all excerpts. Updates all paths that either
|
||||
/// currently have diagnostics or are currently present in this view.
|
||||
fn update_all_excerpts(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
@@ -422,20 +540,17 @@ impl ProjectDiagnosticsEditor {
|
||||
})?;
|
||||
|
||||
for item in more {
|
||||
let insert_pos = blocks
|
||||
.binary_search_by(|existing| {
|
||||
match existing.initial_range.start.cmp(&item.initial_range.start) {
|
||||
Ordering::Equal => item
|
||||
.initial_range
|
||||
.end
|
||||
.cmp(&existing.initial_range.end)
|
||||
.reverse(),
|
||||
other => other,
|
||||
}
|
||||
let i = blocks
|
||||
.binary_search_by(|probe| {
|
||||
probe
|
||||
.initial_range
|
||||
.start
|
||||
.cmp(&item.initial_range.start)
|
||||
.then(probe.initial_range.end.cmp(&item.initial_range.end))
|
||||
.then(Ordering::Greater)
|
||||
})
|
||||
.unwrap_or_else(|pos| pos);
|
||||
|
||||
blocks.insert(insert_pos, item);
|
||||
.unwrap_or_else(|i| i);
|
||||
blocks.insert(i, item);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -448,10 +563,25 @@ impl ProjectDiagnosticsEditor {
|
||||
&mut cx,
|
||||
)
|
||||
.await;
|
||||
excerpt_ranges.push(ExcerptRange {
|
||||
context: excerpt_range,
|
||||
primary: b.initial_range.clone(),
|
||||
})
|
||||
let i = excerpt_ranges
|
||||
.binary_search_by(|probe| {
|
||||
probe
|
||||
.context
|
||||
.start
|
||||
.cmp(&excerpt_range.start)
|
||||
.then(probe.context.end.cmp(&excerpt_range.end))
|
||||
.then(probe.primary.start.cmp(&b.initial_range.start))
|
||||
.then(probe.primary.end.cmp(&b.initial_range.end))
|
||||
.then(cmp::Ordering::Greater)
|
||||
})
|
||||
.unwrap_or_else(|i| i);
|
||||
excerpt_ranges.insert(
|
||||
i,
|
||||
ExcerptRange {
|
||||
context: excerpt_range,
|
||||
primary: b.initial_range.clone(),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
@@ -534,6 +664,30 @@ impl ProjectDiagnosticsEditor {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn cargo_diagnostics_sources(&self, cx: &App) -> Vec<ProjectPath> {
|
||||
let fetch_cargo_diagnostics = ProjectSettings::get_global(cx)
|
||||
.diagnostics
|
||||
.fetch_cargo_diagnostics();
|
||||
if !fetch_cargo_diagnostics {
|
||||
return Vec::new();
|
||||
}
|
||||
self.project
|
||||
.read(cx)
|
||||
.worktrees(cx)
|
||||
.filter_map(|worktree| {
|
||||
let _cargo_toml_entry = worktree.read(cx).entry_for_path("Cargo.toml")?;
|
||||
let rust_file_entry = worktree.read(cx).entries(false, 0).find(|entry| {
|
||||
entry
|
||||
.path
|
||||
.extension()
|
||||
.and_then(|extension| extension.to_str())
|
||||
== Some("rs")
|
||||
})?;
|
||||
self.project.read(cx).path_for_entry(rust_file_entry.id, cx)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for ProjectDiagnosticsEditor {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use crate::ProjectDiagnosticsEditor;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{ProjectDiagnosticsEditor, ToggleDiagnosticsRefresh};
|
||||
use gpui::{Context, Entity, EventEmitter, ParentElement, Render, WeakEntity, Window};
|
||||
use ui::prelude::*;
|
||||
use ui::{IconButton, IconButtonShape, IconName, Tooltip};
|
||||
@@ -13,18 +15,26 @@ impl Render for ToolbarControls {
|
||||
let mut include_warnings = false;
|
||||
let mut has_stale_excerpts = false;
|
||||
let mut is_updating = false;
|
||||
let cargo_diagnostics_sources = Arc::new(self.diagnostics().map_or(Vec::new(), |editor| {
|
||||
editor.read(cx).cargo_diagnostics_sources(cx)
|
||||
}));
|
||||
let fetch_cargo_diagnostics = !cargo_diagnostics_sources.is_empty();
|
||||
|
||||
if let Some(editor) = self.diagnostics() {
|
||||
let diagnostics = editor.read(cx);
|
||||
include_warnings = diagnostics.include_warnings;
|
||||
has_stale_excerpts = !diagnostics.paths_to_update.is_empty();
|
||||
is_updating = diagnostics.update_excerpts_task.is_some()
|
||||
|| diagnostics
|
||||
.project
|
||||
.read(cx)
|
||||
.language_servers_running_disk_based_diagnostics(cx)
|
||||
.next()
|
||||
.is_some();
|
||||
is_updating = if fetch_cargo_diagnostics {
|
||||
diagnostics.cargo_diagnostics_fetch.fetch_task.is_some()
|
||||
} else {
|
||||
diagnostics.update_excerpts_task.is_some()
|
||||
|| diagnostics
|
||||
.project
|
||||
.read(cx)
|
||||
.language_servers_running_disk_based_diagnostics(cx)
|
||||
.next()
|
||||
.is_some()
|
||||
};
|
||||
}
|
||||
|
||||
let tooltip = if include_warnings {
|
||||
@@ -41,21 +51,56 @@ impl Render for ToolbarControls {
|
||||
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.when(has_stale_excerpts, |div| {
|
||||
div.child(
|
||||
IconButton::new("update-excerpts", IconName::Update)
|
||||
.icon_color(Color::Info)
|
||||
.shape(IconButtonShape::Square)
|
||||
.disabled(is_updating)
|
||||
.tooltip(Tooltip::text("Update excerpts"))
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
if let Some(diagnostics) = this.diagnostics() {
|
||||
diagnostics.update(cx, |diagnostics, cx| {
|
||||
diagnostics.update_all_excerpts(window, cx);
|
||||
});
|
||||
}
|
||||
})),
|
||||
)
|
||||
.map(|div| {
|
||||
if is_updating {
|
||||
div.child(
|
||||
IconButton::new("stop-updating", IconName::StopFilled)
|
||||
.icon_color(Color::Info)
|
||||
.shape(IconButtonShape::Square)
|
||||
.tooltip(Tooltip::for_action_title(
|
||||
"Stop diagnostics update",
|
||||
&ToggleDiagnosticsRefresh,
|
||||
))
|
||||
.on_click(cx.listener(move |toolbar_controls, _, _, cx| {
|
||||
if let Some(diagnostics) = toolbar_controls.diagnostics() {
|
||||
diagnostics.update(cx, |diagnostics, cx| {
|
||||
diagnostics.stop_cargo_diagnostics_fetch(cx);
|
||||
diagnostics.update_excerpts_task = None;
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
})),
|
||||
)
|
||||
} else {
|
||||
div.child(
|
||||
IconButton::new("refresh-diagnostics", IconName::Update)
|
||||
.icon_color(Color::Info)
|
||||
.shape(IconButtonShape::Square)
|
||||
.disabled(!has_stale_excerpts && !fetch_cargo_diagnostics)
|
||||
.tooltip(Tooltip::for_action_title(
|
||||
"Refresh diagnostics",
|
||||
&ToggleDiagnosticsRefresh,
|
||||
))
|
||||
.on_click(cx.listener({
|
||||
move |toolbar_controls, _, window, cx| {
|
||||
if let Some(diagnostics) = toolbar_controls.diagnostics() {
|
||||
let cargo_diagnostics_sources =
|
||||
Arc::clone(&cargo_diagnostics_sources);
|
||||
diagnostics.update(cx, move |diagnostics, cx| {
|
||||
if fetch_cargo_diagnostics {
|
||||
diagnostics.fetch_cargo_diagnostics(
|
||||
cargo_diagnostics_sources,
|
||||
cx,
|
||||
);
|
||||
} else {
|
||||
diagnostics.update_all_excerpts(window, cx);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
})),
|
||||
)
|
||||
}
|
||||
})
|
||||
.child(
|
||||
IconButton::new("toggle-warnings", IconName::Warning)
|
||||
|
||||
@@ -249,7 +249,9 @@ actions!(
|
||||
ApplyDiffHunk,
|
||||
Backspace,
|
||||
Cancel,
|
||||
CancelFlycheck,
|
||||
CancelLanguageServerWork,
|
||||
ClearFlycheck,
|
||||
ConfirmRename,
|
||||
ConfirmCompletionInsert,
|
||||
ConfirmCompletionReplace,
|
||||
@@ -308,6 +310,7 @@ actions!(
|
||||
GoToImplementation,
|
||||
GoToImplementationSplit,
|
||||
GoToNextChange,
|
||||
GoToParentModule,
|
||||
GoToPreviousChange,
|
||||
GoToPreviousDiagnostic,
|
||||
GoToTypeDefinition,
|
||||
@@ -371,6 +374,7 @@ actions!(
|
||||
RevertFile,
|
||||
ReloadFile,
|
||||
Rewrap,
|
||||
RunFlycheck,
|
||||
ScrollCursorBottom,
|
||||
ScrollCursorCenter,
|
||||
ScrollCursorCenterTopBottom,
|
||||
|
||||
@@ -2952,7 +2952,9 @@ impl Editor {
|
||||
_ => {}
|
||||
}
|
||||
|
||||
self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
|
||||
let auto_scroll = EditorSettings::get_global(cx).autoscroll_on_clicks;
|
||||
|
||||
self.change_selections(auto_scroll.then(Autoscroll::fit), window, cx, |s| {
|
||||
s.set_pending(pending_selection, pending_mode)
|
||||
});
|
||||
}
|
||||
@@ -2972,7 +2974,6 @@ impl Editor {
|
||||
|
||||
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
let buffer = &display_map.buffer_snapshot;
|
||||
let newest_selection = self.selections.newest_anchor().clone();
|
||||
let position = display_map.clip_point(position, Bias::Left);
|
||||
|
||||
let start;
|
||||
@@ -3047,8 +3048,6 @@ impl Editor {
|
||||
} else {
|
||||
if !add {
|
||||
s.clear_disjoint();
|
||||
} else if click_count > 1 {
|
||||
s.delete(newest_selection.id)
|
||||
}
|
||||
|
||||
s.set_pending_anchor_range(start..end, mode);
|
||||
@@ -5207,20 +5206,27 @@ impl Editor {
|
||||
let dap_store = project.read(cx).dap_store();
|
||||
let mut scenarios = vec![];
|
||||
let resolved_tasks = resolved_tasks.as_ref()?;
|
||||
let debug_adapter: SharedString = buffer
|
||||
.read(cx)
|
||||
.language()?
|
||||
.context_provider()?
|
||||
.debug_adapter()?
|
||||
.into();
|
||||
let buffer = buffer.read(cx);
|
||||
let language = buffer.language()?;
|
||||
let file = buffer.file();
|
||||
let debug_adapter =
|
||||
language_settings(language.name().into(), file, cx)
|
||||
.debuggers
|
||||
.first()
|
||||
.map(SharedString::from)
|
||||
.or_else(|| {
|
||||
language
|
||||
.config()
|
||||
.debuggers
|
||||
.first()
|
||||
.map(SharedString::from)
|
||||
})?;
|
||||
|
||||
dap_store.update(cx, |this, cx| {
|
||||
for (_, task) in &resolved_tasks.templates {
|
||||
if let Some(scenario) = this
|
||||
.debug_scenario_for_build_task(
|
||||
task.resolved.clone(),
|
||||
SharedString::from(
|
||||
task.original_task().label.clone(),
|
||||
),
|
||||
task.original_task().clone(),
|
||||
debug_adapter.clone(),
|
||||
cx,
|
||||
)
|
||||
@@ -12131,6 +12137,28 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
fn select_match_ranges(
|
||||
&mut self,
|
||||
range: Range<usize>,
|
||||
reversed: bool,
|
||||
replace_newest: bool,
|
||||
auto_scroll: Option<Autoscroll>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
self.unfold_ranges(&[range.clone()], false, auto_scroll.is_some(), cx);
|
||||
self.change_selections(auto_scroll, window, cx, |s| {
|
||||
if replace_newest {
|
||||
s.delete(s.newest_anchor().id);
|
||||
}
|
||||
if reversed {
|
||||
s.insert_range(range.end..range.start);
|
||||
} else {
|
||||
s.insert_range(range);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn select_next_match_internal(
|
||||
&mut self,
|
||||
display_map: &DisplaySnapshot,
|
||||
@@ -12139,28 +12167,6 @@ impl Editor {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Result<()> {
|
||||
fn select_next_match_ranges(
|
||||
this: &mut Editor,
|
||||
range: Range<usize>,
|
||||
reversed: bool,
|
||||
replace_newest: bool,
|
||||
auto_scroll: Option<Autoscroll>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
this.unfold_ranges(&[range.clone()], false, auto_scroll.is_some(), cx);
|
||||
this.change_selections(auto_scroll, window, cx, |s| {
|
||||
if replace_newest {
|
||||
s.delete(s.newest_anchor().id);
|
||||
}
|
||||
if reversed {
|
||||
s.insert_range(range.end..range.start);
|
||||
} else {
|
||||
s.insert_range(range);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let buffer = &display_map.buffer_snapshot;
|
||||
let mut selections = self.selections.all::<usize>(cx);
|
||||
if let Some(mut select_next_state) = self.select_next_state.take() {
|
||||
@@ -12205,8 +12211,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
if let Some(next_selected_range) = next_selected_range {
|
||||
select_next_match_ranges(
|
||||
self,
|
||||
self.select_match_ranges(
|
||||
next_selected_range,
|
||||
last_selection.reversed,
|
||||
replace_newest,
|
||||
@@ -12264,8 +12269,7 @@ impl Editor {
|
||||
selection.end = word_range.end.to_offset(display_map, Bias::Left);
|
||||
selection.goal = SelectionGoal::None;
|
||||
selection.reversed = false;
|
||||
select_next_match_ranges(
|
||||
self,
|
||||
self.select_match_ranges(
|
||||
selection.start..selection.end,
|
||||
selection.reversed,
|
||||
replace_newest,
|
||||
@@ -12430,17 +12434,14 @@ impl Editor {
|
||||
}
|
||||
|
||||
if let Some(next_selected_range) = next_selected_range {
|
||||
self.unfold_ranges(&[next_selected_range.clone()], false, true, cx);
|
||||
self.change_selections(Some(Autoscroll::newest()), window, cx, |s| {
|
||||
if action.replace_newest {
|
||||
s.delete(s.newest_anchor().id);
|
||||
}
|
||||
if last_selection.reversed {
|
||||
s.insert_range(next_selected_range.end..next_selected_range.start);
|
||||
} else {
|
||||
s.insert_range(next_selected_range);
|
||||
}
|
||||
});
|
||||
self.select_match_ranges(
|
||||
next_selected_range,
|
||||
last_selection.reversed,
|
||||
action.replace_newest,
|
||||
Some(Autoscroll::newest()),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
} else {
|
||||
select_prev_state.done = true;
|
||||
}
|
||||
@@ -12491,6 +12492,14 @@ impl Editor {
|
||||
selection.end = word_range.end.to_offset(&display_map, Bias::Left);
|
||||
selection.goal = SelectionGoal::None;
|
||||
selection.reversed = false;
|
||||
self.select_match_ranges(
|
||||
selection.start..selection.end,
|
||||
selection.reversed,
|
||||
action.replace_newest,
|
||||
Some(Autoscroll::newest()),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
if selections.len() == 1 {
|
||||
let selection = selections
|
||||
@@ -12509,16 +12518,6 @@ impl Editor {
|
||||
} else {
|
||||
self.select_prev_state = None;
|
||||
}
|
||||
|
||||
self.unfold_ranges(
|
||||
&selections.iter().map(|s| s.range()).collect::<Vec<_>>(),
|
||||
false,
|
||||
true,
|
||||
cx,
|
||||
);
|
||||
self.change_selections(Some(Autoscroll::newest()), window, cx, |s| {
|
||||
s.select(selections);
|
||||
});
|
||||
} else if let Some(selected_text) = selected_text {
|
||||
self.select_prev_state = Some(SelectNextState {
|
||||
query: AhoCorasick::new(&[selected_text.chars().rev().collect::<String>()])?,
|
||||
@@ -13001,10 +13000,9 @@ impl Editor {
|
||||
}
|
||||
|
||||
let mut new_range = old_range.clone();
|
||||
let mut new_node = None;
|
||||
while let Some((node, containing_range)) = buffer.syntax_ancestor(new_range.clone())
|
||||
while let Some((_node, containing_range)) =
|
||||
buffer.syntax_ancestor(new_range.clone())
|
||||
{
|
||||
new_node = Some(node);
|
||||
new_range = match containing_range {
|
||||
MultiOrSingleBufferOffsetRange::Single(_) => break,
|
||||
MultiOrSingleBufferOffsetRange::Multi(range) => range,
|
||||
@@ -13016,17 +13014,6 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(node) = new_node {
|
||||
// Log the ancestor, to support using this action as a way to explore TreeSitter
|
||||
// nodes. Parent and grandparent are also logged because this operation will not
|
||||
// visit nodes that have the same range as their parent.
|
||||
log::info!("Node: {node:?}");
|
||||
let parent = node.parent();
|
||||
log::info!("Parent: {parent:?}");
|
||||
let grandparent = parent.and_then(|x| x.parent());
|
||||
log::info!("Grandparent: {grandparent:?}");
|
||||
}
|
||||
|
||||
selected_larger_node |= new_range != old_range;
|
||||
Selection {
|
||||
id: selection.id,
|
||||
|
||||
@@ -6130,11 +6130,7 @@ async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
|
||||
|
||||
cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
|
||||
.unwrap();
|
||||
cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndef«abcˇ»\n«abcˇ»");
|
||||
|
||||
cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
|
||||
.unwrap();
|
||||
cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
|
||||
cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -6406,6 +6402,68 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let language = Arc::new(Language::new(
|
||||
LanguageConfig::default(),
|
||||
Some(tree_sitter_rust::LANGUAGE.into()),
|
||||
));
|
||||
|
||||
let text = "let a = 2;";
|
||||
|
||||
let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
|
||||
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
|
||||
|
||||
editor
|
||||
.condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
|
||||
.await;
|
||||
|
||||
// Test case 1: Cursor at end of word
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
editor.change_selections(None, window, cx, |s| {
|
||||
s.select_display_ranges([
|
||||
DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
|
||||
]);
|
||||
});
|
||||
});
|
||||
editor.update(cx, |editor, cx| {
|
||||
assert_text_with_selections(editor, "let aˇ = 2;", cx);
|
||||
});
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
|
||||
});
|
||||
editor.update(cx, |editor, cx| {
|
||||
assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
|
||||
});
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
|
||||
});
|
||||
editor.update(cx, |editor, cx| {
|
||||
assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
|
||||
});
|
||||
|
||||
// Test case 2: Cursor at end of statement
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
editor.change_selections(None, window, cx, |s| {
|
||||
s.select_display_ranges([
|
||||
DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
|
||||
]);
|
||||
});
|
||||
});
|
||||
editor.update(cx, |editor, cx| {
|
||||
assert_text_with_selections(editor, "let a = 2;ˇ", cx);
|
||||
});
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
|
||||
});
|
||||
editor.update(cx, |editor, cx| {
|
||||
assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
@@ -25,7 +25,7 @@ use crate::{
|
||||
inlay_hint_settings,
|
||||
items::BufferSearchHighlights,
|
||||
mouse_context_menu::{self, MenuPosition},
|
||||
scroll::scroll_amount::ScrollAmount,
|
||||
scroll::{ActiveScrollbarState, ScrollbarThumbState, scroll_amount::ScrollAmount},
|
||||
};
|
||||
use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind};
|
||||
use collections::{BTreeMap, HashMap};
|
||||
@@ -1449,7 +1449,7 @@ impl EditorElement {
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<EditorScrollbars> {
|
||||
if !snapshot.mode.is_full() {
|
||||
if !snapshot.mode.is_full() || !self.editor.read(cx).show_scrollbars {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -1457,41 +1457,40 @@ impl EditorElement {
|
||||
// cancel the scrollbar drag.
|
||||
if cx.has_active_drag() {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.scroll_manager.reset_scrollbar_dragging_state(cx)
|
||||
editor.scroll_manager.reset_scrollbar_state(cx)
|
||||
});
|
||||
}
|
||||
|
||||
let scrollbar_settings = EditorSettings::get_global(cx).scrollbar;
|
||||
let show_scrollbars = self.editor.read(cx).show_scrollbars
|
||||
&& match scrollbar_settings.show {
|
||||
ShowScrollbar::Auto => {
|
||||
let editor = self.editor.read(cx);
|
||||
let is_singleton = editor.is_singleton(cx);
|
||||
// Git
|
||||
(is_singleton && scrollbar_settings.git_diff && snapshot.buffer_snapshot.has_diff_hunks())
|
||||
||
|
||||
// Buffer Search Results
|
||||
(is_singleton && scrollbar_settings.search_results && editor.has_background_highlights::<BufferSearchHighlights>())
|
||||
||
|
||||
// Selected Text Occurrences
|
||||
(is_singleton && scrollbar_settings.selected_text && editor.has_background_highlights::<SelectedTextHighlight>())
|
||||
||
|
||||
// Selected Symbol Occurrences
|
||||
(is_singleton && scrollbar_settings.selected_symbol && (editor.has_background_highlights::<DocumentHighlightRead>() || editor.has_background_highlights::<DocumentHighlightWrite>()))
|
||||
||
|
||||
// Diagnostics
|
||||
(is_singleton && scrollbar_settings.diagnostics != ScrollbarDiagnostics::None && snapshot.buffer_snapshot.has_diagnostics())
|
||||
||
|
||||
// Cursors out of sight
|
||||
non_visible_cursors
|
||||
||
|
||||
// Scrollmanager
|
||||
editor.scroll_manager.scrollbars_visible()
|
||||
}
|
||||
ShowScrollbar::System => self.editor.read(cx).scroll_manager.scrollbars_visible(),
|
||||
ShowScrollbar::Always => true,
|
||||
ShowScrollbar::Never => return None,
|
||||
};
|
||||
let show_scrollbars = match scrollbar_settings.show {
|
||||
ShowScrollbar::Auto => {
|
||||
let editor = self.editor.read(cx);
|
||||
let is_singleton = editor.is_singleton(cx);
|
||||
// Git
|
||||
(is_singleton && scrollbar_settings.git_diff && snapshot.buffer_snapshot.has_diff_hunks())
|
||||
||
|
||||
// Buffer Search Results
|
||||
(is_singleton && scrollbar_settings.search_results && editor.has_background_highlights::<BufferSearchHighlights>())
|
||||
||
|
||||
// Selected Text Occurrences
|
||||
(is_singleton && scrollbar_settings.selected_text && editor.has_background_highlights::<SelectedTextHighlight>())
|
||||
||
|
||||
// Selected Symbol Occurrences
|
||||
(is_singleton && scrollbar_settings.selected_symbol && (editor.has_background_highlights::<DocumentHighlightRead>() || editor.has_background_highlights::<DocumentHighlightWrite>()))
|
||||
||
|
||||
// Diagnostics
|
||||
(is_singleton && scrollbar_settings.diagnostics != ScrollbarDiagnostics::None && snapshot.buffer_snapshot.has_diagnostics())
|
||||
||
|
||||
// Cursors out of sight
|
||||
non_visible_cursors
|
||||
||
|
||||
// Scrollmanager
|
||||
editor.scroll_manager.scrollbars_visible()
|
||||
}
|
||||
ShowScrollbar::System => self.editor.read(cx).scroll_manager.scrollbars_visible(),
|
||||
ShowScrollbar::Always => true,
|
||||
ShowScrollbar::Never => return None,
|
||||
};
|
||||
|
||||
Some(EditorScrollbars::from_scrollbar_axes(
|
||||
scrollbar_settings.axes,
|
||||
@@ -1500,6 +1499,7 @@ impl EditorElement {
|
||||
scroll_position,
|
||||
self.style.scrollbar_width,
|
||||
show_scrollbars,
|
||||
self.editor.read(cx).scroll_manager.active_scrollbar_state(),
|
||||
window,
|
||||
))
|
||||
}
|
||||
@@ -2285,6 +2285,9 @@ impl EditorElement {
|
||||
}
|
||||
|
||||
let display_row = multibuffer_point.to_display_point(snapshot).row();
|
||||
if !range.contains(&display_row) {
|
||||
return None;
|
||||
}
|
||||
if row_infos
|
||||
.get((display_row - range.start).0 as usize)
|
||||
.is_some_and(|row_info| row_info.expand_info.is_some())
|
||||
@@ -5101,7 +5104,7 @@ impl EditorElement {
|
||||
}
|
||||
|
||||
fn paint_scrollbars(&mut self, layout: &mut EditorLayout, window: &mut Window, cx: &mut App) {
|
||||
let Some(scrollbars_layout) = &layout.scrollbars_layout else {
|
||||
let Some(scrollbars_layout) = layout.scrollbars_layout.take() else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -5150,10 +5153,16 @@ impl EditorElement {
|
||||
}
|
||||
}
|
||||
|
||||
let scrollbar_thumb_color = match scrollbar_layout.thumb_state {
|
||||
ScrollbarThumbState::Dragging | ScrollbarThumbState::Hovered => {
|
||||
cx.theme().colors().scrollbar_thumb_hover_background
|
||||
}
|
||||
ScrollbarThumbState::Idle => cx.theme().colors().scrollbar_thumb_background,
|
||||
};
|
||||
window.paint_quad(quad(
|
||||
thumb_bounds,
|
||||
Corners::default(),
|
||||
cx.theme().colors().scrollbar_thumb_background,
|
||||
scrollbar_thumb_color,
|
||||
scrollbar_edges,
|
||||
cx.theme().colors().scrollbar_thumb_border,
|
||||
BorderStyle::Solid,
|
||||
@@ -5200,13 +5209,22 @@ impl EditorElement {
|
||||
});
|
||||
editor.set_scroll_position(position, window, cx);
|
||||
}
|
||||
cx.stop_propagation();
|
||||
} else {
|
||||
editor.scroll_manager.reset_scrollbar_dragging_state(cx);
|
||||
}
|
||||
|
||||
if scrollbars_layout.get_hovered_axis(window).is_some() {
|
||||
editor.scroll_manager.show_scrollbars(window, cx);
|
||||
cx.stop_propagation();
|
||||
} else if let Some((layout, axis)) = scrollbars_layout.get_hovered_axis(window)
|
||||
{
|
||||
if layout.thumb_bounds().contains(&event.position) {
|
||||
editor
|
||||
.scroll_manager
|
||||
.set_hovered_scroll_thumb_axis(axis, cx);
|
||||
} else {
|
||||
editor.scroll_manager.reset_scrollbar_state(cx);
|
||||
}
|
||||
|
||||
editor.scroll_manager.show_scrollbars(window, cx);
|
||||
} else {
|
||||
editor.scroll_manager.reset_scrollbar_state(cx);
|
||||
}
|
||||
|
||||
mouse_position = event.position;
|
||||
@@ -5217,13 +5235,19 @@ impl EditorElement {
|
||||
if self.editor.read(cx).scroll_manager.any_scrollbar_dragged() {
|
||||
window.on_mouse_event({
|
||||
let editor = self.editor.clone();
|
||||
move |_: &MouseUpEvent, phase, _, cx| {
|
||||
move |_: &MouseUpEvent, phase, window, cx| {
|
||||
if phase == DispatchPhase::Capture {
|
||||
return;
|
||||
}
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.scroll_manager.reset_scrollbar_dragging_state(cx);
|
||||
if let Some((_, axis)) = scrollbars_layout.get_hovered_axis(window) {
|
||||
editor
|
||||
.scroll_manager
|
||||
.set_hovered_scroll_thumb_axis(axis, cx);
|
||||
} else {
|
||||
editor.scroll_manager.reset_scrollbar_state(cx);
|
||||
}
|
||||
cx.stop_propagation();
|
||||
});
|
||||
}
|
||||
@@ -5231,7 +5255,6 @@ impl EditorElement {
|
||||
} else {
|
||||
window.on_mouse_event({
|
||||
let editor = self.editor.clone();
|
||||
let scrollbars_layout = scrollbars_layout.clone();
|
||||
|
||||
move |event: &MouseDownEvent, phase, window, cx| {
|
||||
if phase == DispatchPhase::Capture {
|
||||
@@ -5252,7 +5275,9 @@ impl EditorElement {
|
||||
let thumb_bounds = scrollbar_layout.thumb_bounds();
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.scroll_manager.set_dragged_scrollbar_axis(axis, cx);
|
||||
editor
|
||||
.scroll_manager
|
||||
.set_dragged_scroll_thumb_axis(axis, cx);
|
||||
|
||||
let event_position = event.position.along(axis);
|
||||
|
||||
@@ -8034,6 +8059,7 @@ impl EditorScrollbars {
|
||||
scroll_position: gpui::Point<f32>,
|
||||
scrollbar_width: Pixels,
|
||||
show_scrollbars: bool,
|
||||
scrollbar_state: Option<&ActiveScrollbarState>,
|
||||
window: &mut Window,
|
||||
) -> Self {
|
||||
let ScrollbarLayoutInformation {
|
||||
@@ -8079,6 +8105,10 @@ impl EditorScrollbars {
|
||||
axis != ScrollbarAxis::Horizontal || editor_content_size < scroll_range
|
||||
})
|
||||
.map(|(editor_content_size, scroll_range)| {
|
||||
let thumb_state = scrollbar_state
|
||||
.and_then(|state| state.thumb_state_for_axis(axis))
|
||||
.unwrap_or(ScrollbarThumbState::Idle);
|
||||
|
||||
ScrollbarLayout::new(
|
||||
window.insert_hitbox(scrollbar_bounds_for(axis), false),
|
||||
editor_content_size,
|
||||
@@ -8086,6 +8116,7 @@ impl EditorScrollbars {
|
||||
glyph_grid_cell.along(axis),
|
||||
content_offset.along(axis),
|
||||
scroll_position.along(axis),
|
||||
thumb_state,
|
||||
axis,
|
||||
)
|
||||
})
|
||||
@@ -8121,6 +8152,7 @@ struct ScrollbarLayout {
|
||||
text_unit_size: Pixels,
|
||||
content_offset: Pixels,
|
||||
thumb_size: Pixels,
|
||||
thumb_state: ScrollbarThumbState,
|
||||
axis: ScrollbarAxis,
|
||||
}
|
||||
|
||||
@@ -8137,6 +8169,7 @@ impl ScrollbarLayout {
|
||||
glyph_space: Pixels,
|
||||
content_offset: Pixels,
|
||||
scroll_position: f32,
|
||||
thumb_state: ScrollbarThumbState,
|
||||
axis: ScrollbarAxis,
|
||||
) -> Self {
|
||||
let track_bounds = scrollbar_track_hitbox.bounds;
|
||||
@@ -8161,6 +8194,7 @@ impl ScrollbarLayout {
|
||||
text_unit_size,
|
||||
content_offset,
|
||||
thumb_size,
|
||||
thumb_state,
|
||||
axis,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ use std::sync::Arc;
|
||||
use crate::Editor;
|
||||
use collections::HashMap;
|
||||
use futures::stream::FuturesUnordered;
|
||||
use gpui::AsyncApp;
|
||||
use gpui::{App, AppContext as _, Entity, Task};
|
||||
use itertools::Itertools;
|
||||
use language::Buffer;
|
||||
@@ -74,6 +75,39 @@ where
|
||||
})
|
||||
}
|
||||
|
||||
async fn lsp_task_context(
|
||||
project: &Entity<Project>,
|
||||
buffer: &Entity<Buffer>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Option<TaskContext> {
|
||||
let worktree_store = project
|
||||
.update(cx, |project, _| project.worktree_store())
|
||||
.ok()?;
|
||||
|
||||
let worktree_abs_path = cx
|
||||
.update(|cx| {
|
||||
let worktree_id = buffer.read(cx).file().map(|f| f.worktree_id(cx));
|
||||
|
||||
worktree_id
|
||||
.and_then(|worktree_id| worktree_store.read(cx).worktree_for_id(worktree_id, cx))
|
||||
.and_then(|worktree| worktree.read(cx).root_dir())
|
||||
})
|
||||
.ok()?;
|
||||
|
||||
let project_env = project
|
||||
.update(cx, |project, cx| {
|
||||
project.buffer_environment(&buffer, &worktree_store, cx)
|
||||
})
|
||||
.ok()?
|
||||
.await;
|
||||
|
||||
Some(TaskContext {
|
||||
cwd: worktree_abs_path.map(|p| p.to_path_buf()),
|
||||
project_env: project_env.unwrap_or_default(),
|
||||
..TaskContext::default()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn lsp_tasks(
|
||||
project: Entity<Project>,
|
||||
task_sources: &HashMap<LanguageServerName, Vec<BufferId>>,
|
||||
@@ -97,13 +131,16 @@ pub fn lsp_tasks(
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let mut lsp_tasks = Vec::new();
|
||||
let lsp_task_context = TaskContext::default();
|
||||
while let Some(server_to_query) = lsp_task_sources.next().await {
|
||||
if let Some((server_id, buffers)) = server_to_query {
|
||||
let source_kind = TaskSourceKind::Lsp(server_id);
|
||||
let id_base = source_kind.to_id_base();
|
||||
let mut new_lsp_tasks = Vec::new();
|
||||
for buffer in buffers {
|
||||
let lsp_buffer_context = lsp_task_context(&project, &buffer, cx)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
if let Ok(runnables_task) = project.update(cx, |project, cx| {
|
||||
let buffer_id = buffer.read(cx).remote_id();
|
||||
project.request_lsp(
|
||||
@@ -120,7 +157,7 @@ pub fn lsp_tasks(
|
||||
new_lsp_tasks.extend(new_runnables.runnables.into_iter().filter_map(
|
||||
|(location, runnable)| {
|
||||
let resolved_task =
|
||||
runnable.resolve_task(&id_base, &lsp_task_context)?;
|
||||
runnable.resolve_task(&id_base, &lsp_buffer_context)?;
|
||||
Some((location, resolved_task))
|
||||
},
|
||||
));
|
||||
|
||||
@@ -233,7 +233,7 @@ pub fn deploy_context_menu(
|
||||
.separator()
|
||||
.action("Cut", Box::new(Cut))
|
||||
.action("Copy", Box::new(Copy))
|
||||
.action("Copy and trim", Box::new(CopyAndTrim))
|
||||
.action("Copy and Trim", Box::new(CopyAndTrim))
|
||||
.action("Paste", Box::new(Paste))
|
||||
.separator()
|
||||
.map(|builder| {
|
||||
|
||||
@@ -4,15 +4,20 @@ use anyhow::Context as _;
|
||||
use gpui::{App, AppContext as _, Context, Entity, Window};
|
||||
use language::{Capability, Language, proto::serialize_anchor};
|
||||
use multi_buffer::MultiBuffer;
|
||||
use project::lsp_store::{
|
||||
lsp_ext_command::{DocsUrls, ExpandMacro, ExpandedMacro},
|
||||
rust_analyzer_ext::RUST_ANALYZER_NAME,
|
||||
use project::{
|
||||
ProjectItem,
|
||||
lsp_command::location_link_from_proto,
|
||||
lsp_store::{
|
||||
lsp_ext_command::{DocsUrls, ExpandMacro, ExpandedMacro},
|
||||
rust_analyzer_ext::{RUST_ANALYZER_NAME, cancel_flycheck, clear_flycheck, run_flycheck},
|
||||
},
|
||||
};
|
||||
use rpc::proto;
|
||||
use text::ToPointUtf16;
|
||||
|
||||
use crate::{
|
||||
Editor, ExpandMacroRecursively, OpenDocs, element::register_action,
|
||||
CancelFlycheck, ClearFlycheck, Editor, ExpandMacroRecursively, GoToParentModule,
|
||||
GotoDefinitionKind, OpenDocs, RunFlycheck, element::register_action, hover_links::HoverLink,
|
||||
lsp_ext::find_specific_language_server_in_selection,
|
||||
};
|
||||
|
||||
@@ -30,11 +35,97 @@ pub fn apply_related_actions(editor: &Entity<Editor>, window: &mut Window, cx: &
|
||||
.filter_map(|buffer| buffer.read(cx).language())
|
||||
.any(|language| is_rust_language(language))
|
||||
{
|
||||
register_action(&editor, window, go_to_parent_module);
|
||||
register_action(&editor, window, expand_macro_recursively);
|
||||
register_action(&editor, window, open_docs);
|
||||
register_action(&editor, window, cancel_flycheck_action);
|
||||
register_action(&editor, window, run_flycheck_action);
|
||||
register_action(&editor, window, clear_flycheck_action);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn go_to_parent_module(
|
||||
editor: &mut Editor,
|
||||
_: &GoToParentModule,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
if editor.selections.count() == 0 {
|
||||
return;
|
||||
}
|
||||
let Some(project) = &editor.project else {
|
||||
return;
|
||||
};
|
||||
|
||||
let server_lookup = find_specific_language_server_in_selection(
|
||||
editor,
|
||||
cx,
|
||||
is_rust_language,
|
||||
RUST_ANALYZER_NAME,
|
||||
);
|
||||
|
||||
let project = project.clone();
|
||||
let lsp_store = project.read(cx).lsp_store();
|
||||
let upstream_client = lsp_store.read(cx).upstream_client();
|
||||
cx.spawn_in(window, async move |editor, cx| {
|
||||
let Some((trigger_anchor, _, server_to_query, buffer)) = server_lookup.await else {
|
||||
return anyhow::Ok(());
|
||||
};
|
||||
|
||||
let location_links = if let Some((client, project_id)) = upstream_client {
|
||||
let buffer_id = buffer.update(cx, |buffer, _| buffer.remote_id())?;
|
||||
|
||||
let request = proto::LspExtGoToParentModule {
|
||||
project_id,
|
||||
buffer_id: buffer_id.to_proto(),
|
||||
position: Some(serialize_anchor(&trigger_anchor.text_anchor)),
|
||||
};
|
||||
let response = client
|
||||
.request(request)
|
||||
.await
|
||||
.context("lsp ext go to parent module proto request")?;
|
||||
futures::future::join_all(
|
||||
response
|
||||
.links
|
||||
.into_iter()
|
||||
.map(|link| location_link_from_proto(link, lsp_store.clone(), cx)),
|
||||
)
|
||||
.await
|
||||
.into_iter()
|
||||
.collect::<anyhow::Result<_>>()
|
||||
.context("go to parent module via collab")?
|
||||
} else {
|
||||
let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot())?;
|
||||
let position = trigger_anchor.text_anchor.to_point_utf16(&buffer_snapshot);
|
||||
project
|
||||
.update(cx, |project, cx| {
|
||||
project.request_lsp(
|
||||
buffer,
|
||||
project::LanguageServerToQuery::Other(server_to_query),
|
||||
project::lsp_store::lsp_ext_command::GoToParentModule { position },
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
.await
|
||||
.context("go to parent module")?
|
||||
};
|
||||
|
||||
editor
|
||||
.update_in(cx, |editor, window, cx| {
|
||||
editor.navigate_to_hover_links(
|
||||
Some(GotoDefinitionKind::Declaration),
|
||||
location_links.into_iter().map(HoverLink::Text).collect(),
|
||||
false,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
.await?;
|
||||
Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
pub fn expand_macro_recursively(
|
||||
editor: &mut Editor,
|
||||
_: &ExpandMacroRecursively,
|
||||
@@ -213,3 +304,87 @@ pub fn open_docs(editor: &mut Editor, _: &OpenDocs, window: &mut Window, cx: &mu
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn cancel_flycheck_action(
|
||||
editor: &mut Editor,
|
||||
_: &CancelFlycheck,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
let Some(project) = &editor.project else {
|
||||
return;
|
||||
};
|
||||
let Some(buffer_id) = editor
|
||||
.selections
|
||||
.disjoint_anchors()
|
||||
.iter()
|
||||
.find_map(|selection| {
|
||||
let buffer_id = selection.start.buffer_id.or(selection.end.buffer_id)?;
|
||||
let project = project.read(cx);
|
||||
let entry_id = project
|
||||
.buffer_for_id(buffer_id, cx)?
|
||||
.read(cx)
|
||||
.entry_id(cx)?;
|
||||
project.path_for_entry(entry_id, cx)
|
||||
})
|
||||
else {
|
||||
return;
|
||||
};
|
||||
cancel_flycheck(project.clone(), buffer_id, cx).detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn run_flycheck_action(
|
||||
editor: &mut Editor,
|
||||
_: &RunFlycheck,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
let Some(project) = &editor.project else {
|
||||
return;
|
||||
};
|
||||
let Some(buffer_id) = editor
|
||||
.selections
|
||||
.disjoint_anchors()
|
||||
.iter()
|
||||
.find_map(|selection| {
|
||||
let buffer_id = selection.start.buffer_id.or(selection.end.buffer_id)?;
|
||||
let project = project.read(cx);
|
||||
let entry_id = project
|
||||
.buffer_for_id(buffer_id, cx)?
|
||||
.read(cx)
|
||||
.entry_id(cx)?;
|
||||
project.path_for_entry(entry_id, cx)
|
||||
})
|
||||
else {
|
||||
return;
|
||||
};
|
||||
run_flycheck(project.clone(), buffer_id, cx).detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn clear_flycheck_action(
|
||||
editor: &mut Editor,
|
||||
_: &ClearFlycheck,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
let Some(project) = &editor.project else {
|
||||
return;
|
||||
};
|
||||
let Some(buffer_id) = editor
|
||||
.selections
|
||||
.disjoint_anchors()
|
||||
.iter()
|
||||
.find_map(|selection| {
|
||||
let buffer_id = selection.start.buffer_id.or(selection.end.buffer_id)?;
|
||||
let project = project.read(cx);
|
||||
let entry_id = project
|
||||
.buffer_for_id(buffer_id, cx)?
|
||||
.read(cx)
|
||||
.entry_id(cx)?;
|
||||
project.path_for_entry(entry_id, cx)
|
||||
})
|
||||
else {
|
||||
return;
|
||||
};
|
||||
clear_flycheck(project.clone(), buffer_id, cx).detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
@@ -123,6 +123,29 @@ impl OngoingScroll {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
pub enum ScrollbarThumbState {
|
||||
Idle,
|
||||
Hovered,
|
||||
Dragging,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
pub struct ActiveScrollbarState {
|
||||
axis: Axis,
|
||||
thumb_state: ScrollbarThumbState,
|
||||
}
|
||||
|
||||
impl ActiveScrollbarState {
|
||||
pub fn new(axis: Axis, thumb_state: ScrollbarThumbState) -> Self {
|
||||
ActiveScrollbarState { axis, thumb_state }
|
||||
}
|
||||
|
||||
pub fn thumb_state_for_axis(&self, axis: Axis) -> Option<ScrollbarThumbState> {
|
||||
(self.axis == axis).then_some(self.thumb_state)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ScrollManager {
|
||||
pub(crate) vertical_scroll_margin: f32,
|
||||
anchor: ScrollAnchor,
|
||||
@@ -131,7 +154,7 @@ pub struct ScrollManager {
|
||||
last_autoscroll: Option<(gpui::Point<f32>, f32, f32, AutoscrollStrategy)>,
|
||||
show_scrollbars: bool,
|
||||
hide_scrollbar_task: Option<Task<()>>,
|
||||
dragging_scrollbar: Option<Axis>,
|
||||
active_scrollbar: Option<ActiveScrollbarState>,
|
||||
visible_line_count: Option<f32>,
|
||||
forbid_vertical_scroll: bool,
|
||||
}
|
||||
@@ -145,7 +168,7 @@ impl ScrollManager {
|
||||
autoscroll_request: None,
|
||||
show_scrollbars: true,
|
||||
hide_scrollbar_task: None,
|
||||
dragging_scrollbar: None,
|
||||
active_scrollbar: None,
|
||||
last_autoscroll: None,
|
||||
visible_line_count: None,
|
||||
forbid_vertical_scroll: false,
|
||||
@@ -322,24 +345,53 @@ impl ScrollManager {
|
||||
self.autoscroll_request.map(|(autoscroll, _)| autoscroll)
|
||||
}
|
||||
|
||||
pub fn active_scrollbar_state(&self) -> Option<&ActiveScrollbarState> {
|
||||
self.active_scrollbar.as_ref()
|
||||
}
|
||||
|
||||
pub fn dragging_scrollbar_axis(&self) -> Option<Axis> {
|
||||
self.dragging_scrollbar
|
||||
self.active_scrollbar
|
||||
.as_ref()
|
||||
.map(|scrollbar| scrollbar.axis)
|
||||
}
|
||||
|
||||
pub fn any_scrollbar_dragged(&self) -> bool {
|
||||
self.dragging_scrollbar.is_some()
|
||||
self.active_scrollbar
|
||||
.as_ref()
|
||||
.is_some_and(|scrollbar| scrollbar.thumb_state == ScrollbarThumbState::Dragging)
|
||||
}
|
||||
|
||||
pub fn set_dragged_scrollbar_axis(&mut self, axis: Axis, cx: &mut Context<Editor>) {
|
||||
if self.dragging_scrollbar != Some(axis) {
|
||||
self.dragging_scrollbar = Some(axis);
|
||||
cx.notify();
|
||||
}
|
||||
pub fn set_hovered_scroll_thumb_axis(&mut self, axis: Axis, cx: &mut Context<Editor>) {
|
||||
self.update_active_scrollbar_state(
|
||||
Some(ActiveScrollbarState::new(
|
||||
axis,
|
||||
ScrollbarThumbState::Hovered,
|
||||
)),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn reset_scrollbar_dragging_state(&mut self, cx: &mut Context<Editor>) {
|
||||
if self.dragging_scrollbar.is_some() {
|
||||
self.dragging_scrollbar = None;
|
||||
pub fn set_dragged_scroll_thumb_axis(&mut self, axis: Axis, cx: &mut Context<Editor>) {
|
||||
self.update_active_scrollbar_state(
|
||||
Some(ActiveScrollbarState::new(
|
||||
axis,
|
||||
ScrollbarThumbState::Dragging,
|
||||
)),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn reset_scrollbar_state(&mut self, cx: &mut Context<Editor>) {
|
||||
self.update_active_scrollbar_state(None, cx);
|
||||
}
|
||||
|
||||
fn update_active_scrollbar_state(
|
||||
&mut self,
|
||||
new_state: Option<ActiveScrollbarState>,
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
if self.active_scrollbar != new_state {
|
||||
self.active_scrollbar = new_state;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,16 +46,19 @@ struct Args {
|
||||
/// Runs all examples and threads that contain these substrings. If unspecified, all examples and threads are run.
|
||||
#[arg(value_name = "EXAMPLE_SUBSTRING")]
|
||||
filter: Vec<String>,
|
||||
/// Model to use (default: "claude-3-7-sonnet-latest")
|
||||
/// ID of model to use.
|
||||
#[arg(long, default_value = "claude-3-7-sonnet-latest")]
|
||||
model: String,
|
||||
#[arg(long, value_delimiter = ',', default_value = "rs,ts")]
|
||||
/// Model provider to use.
|
||||
#[arg(long, default_value = "anthropic")]
|
||||
provider: String,
|
||||
#[arg(long, value_delimiter = ',', default_value = "rs,ts,py")]
|
||||
languages: Vec<String>,
|
||||
/// How many times to run each example.
|
||||
#[arg(long, default_value = "1")]
|
||||
#[arg(long, default_value = "8")]
|
||||
repetitions: usize,
|
||||
/// Maximum number of examples to run concurrently.
|
||||
#[arg(long, default_value = "10")]
|
||||
#[arg(long, default_value = "4")]
|
||||
concurrency: usize,
|
||||
}
|
||||
|
||||
@@ -123,7 +126,7 @@ fn main() {
|
||||
let mut cumulative_tool_metrics = ToolMetrics::default();
|
||||
|
||||
let model_registry = LanguageModelRegistry::read_global(cx);
|
||||
let model = find_model("claude-3-7-sonnet-latest", model_registry, cx).unwrap();
|
||||
let model = find_model(&args.provider, &args.model, model_registry, cx).unwrap();
|
||||
let model_provider_id = model.provider_id();
|
||||
let model_provider = model_registry.provider(&model_provider_id).unwrap();
|
||||
|
||||
@@ -451,27 +454,36 @@ pub fn init(cx: &mut App) -> Arc<AgentAppState> {
|
||||
}
|
||||
|
||||
pub fn find_model(
|
||||
model_name: &str,
|
||||
provider_id: &str,
|
||||
model_id: &str,
|
||||
model_registry: &LanguageModelRegistry,
|
||||
cx: &App,
|
||||
) -> anyhow::Result<Arc<dyn LanguageModel>> {
|
||||
let model = model_registry
|
||||
let matching_models = model_registry
|
||||
.available_models(cx)
|
||||
.find(|model| model.id().0 == model_name);
|
||||
.filter(|model| model.id().0 == model_id && model.provider_id().0 == provider_id)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let Some(model) = model else {
|
||||
return Err(anyhow!(
|
||||
"No language model named {} was available. Available models: {}",
|
||||
model_name,
|
||||
match matching_models.as_slice() {
|
||||
[model] => Ok(model.clone()),
|
||||
[] => Err(anyhow!(
|
||||
"No language model with ID {} was available. Available models: {}",
|
||||
model_id,
|
||||
model_registry
|
||||
.available_models(cx)
|
||||
.map(|model| model.id().0.clone())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
));
|
||||
};
|
||||
|
||||
Ok(model)
|
||||
)),
|
||||
_ => Err(anyhow!(
|
||||
"Multiple language models with ID {} available - use `--provider` to choose one of: {:?}",
|
||||
model_id,
|
||||
matching_models
|
||||
.iter()
|
||||
.map(|model| model.provider_id().0)
|
||||
.collect::<Vec<_>>()
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn commit_sha_for_path(repo_path: &Path) -> String {
|
||||
|
||||
@@ -100,54 +100,64 @@ impl Example for CodeBlockCitations {
|
||||
|
||||
if let Some(content_len) = content_len {
|
||||
// + 1 because there's a newline character after the citation.
|
||||
let content =
|
||||
&text[(citation.len() + 1)..content_len - (citation.len() + 1)];
|
||||
let start_index = citation.len() + 1;
|
||||
let end_index = content_len.saturating_sub(start_index);
|
||||
|
||||
// deindent (trim the start of each line) because sometimes the model
|
||||
// chooses to deindent its code snippets for the sake of readability,
|
||||
// which in markdown is not only reasonable but usually desirable.
|
||||
cx.assert(
|
||||
deindent(&buffer_text)
|
||||
.trim()
|
||||
.contains(deindent(&content).trim()),
|
||||
"Code block content was found in file",
|
||||
)
|
||||
.ok();
|
||||
|
||||
if let Some(range) = path_range.range {
|
||||
let start_line_index = range.start.line.saturating_sub(1);
|
||||
let line_count =
|
||||
range.end.line.saturating_sub(start_line_index);
|
||||
let mut snippet = buffer_text
|
||||
.lines()
|
||||
.skip(start_line_index as usize)
|
||||
.take(line_count as usize)
|
||||
.collect::<Vec<&str>>()
|
||||
.join("\n");
|
||||
|
||||
if let Some(start_col) = range.start.col {
|
||||
snippet = snippet[start_col as usize..].to_string();
|
||||
}
|
||||
|
||||
if let Some(end_col) = range.end.col {
|
||||
let last_line = snippet.lines().last().unwrap();
|
||||
snippet = snippet
|
||||
[..snippet.len() - last_line.len() + end_col as usize]
|
||||
.to_string();
|
||||
}
|
||||
if cx
|
||||
.assert(
|
||||
start_index <= end_index,
|
||||
"Code block had a valid citation",
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
let content = &text[start_index..end_index];
|
||||
|
||||
// deindent (trim the start of each line) because sometimes the model
|
||||
// chooses to deindent its code snippets for the sake of readability,
|
||||
// which in markdown is not only reasonable but usually desirable.
|
||||
cx.assert_eq(
|
||||
deindent(snippet.as_str()).trim(),
|
||||
deindent(content).trim(),
|
||||
format!(
|
||||
"Code block was at {:?}-{:?}",
|
||||
range.start, range.end
|
||||
),
|
||||
cx.assert(
|
||||
deindent(&buffer_text)
|
||||
.trim()
|
||||
.contains(deindent(&content).trim()),
|
||||
"Code block content was found in file",
|
||||
)
|
||||
.ok();
|
||||
|
||||
if let Some(range) = path_range.range {
|
||||
let start_line_index = range.start.line.saturating_sub(1);
|
||||
let line_count =
|
||||
range.end.line.saturating_sub(start_line_index);
|
||||
let mut snippet = buffer_text
|
||||
.lines()
|
||||
.skip(start_line_index as usize)
|
||||
.take(line_count as usize)
|
||||
.collect::<Vec<&str>>()
|
||||
.join("\n");
|
||||
|
||||
if let Some(start_col) = range.start.col {
|
||||
snippet = snippet[start_col as usize..].to_string();
|
||||
}
|
||||
|
||||
if let Some(end_col) = range.end.col {
|
||||
let last_line = snippet.lines().last().unwrap();
|
||||
snippet = snippet[..snippet.len() - last_line.len()
|
||||
+ end_col as usize]
|
||||
.to_string();
|
||||
}
|
||||
|
||||
// deindent (trim the start of each line) because sometimes the model
|
||||
// chooses to deindent its code snippets for the sake of readability,
|
||||
// which in markdown is not only reasonable but usually desirable.
|
||||
cx.assert_eq(
|
||||
deindent(snippet.as_str()).trim(),
|
||||
deindent(content).trim(),
|
||||
format!(
|
||||
"Code block was at {:?}-{:?}",
|
||||
range.start, range.end
|
||||
),
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::sync::Arc;
|
||||
use anyhow::Result;
|
||||
use fs::Fs;
|
||||
use gpui::{App, Global, ReadGlobal, SharedString, Task};
|
||||
use language::{BinaryStatus, LanguageMatcher, LanguageName, LoadedLanguage};
|
||||
use language::{BinaryStatus, LanguageConfig, LanguageName, LoadedLanguage};
|
||||
use lsp::LanguageServerName;
|
||||
use parking_lot::RwLock;
|
||||
|
||||
@@ -224,10 +224,7 @@ impl ExtensionGrammarProxy for ExtensionHostProxy {
|
||||
pub trait ExtensionLanguageProxy: Send + Sync + 'static {
|
||||
fn register_language(
|
||||
&self,
|
||||
language: LanguageName,
|
||||
grammar: Option<Arc<str>>,
|
||||
matcher: LanguageMatcher,
|
||||
hidden: bool,
|
||||
config: LanguageConfig,
|
||||
load: Arc<dyn Fn() -> Result<LoadedLanguage> + Send + Sync + 'static>,
|
||||
);
|
||||
|
||||
@@ -241,17 +238,14 @@ pub trait ExtensionLanguageProxy: Send + Sync + 'static {
|
||||
impl ExtensionLanguageProxy for ExtensionHostProxy {
|
||||
fn register_language(
|
||||
&self,
|
||||
language: LanguageName,
|
||||
grammar: Option<Arc<str>>,
|
||||
matcher: LanguageMatcher,
|
||||
hidden: bool,
|
||||
language: LanguageConfig,
|
||||
load: Arc<dyn Fn() -> Result<LoadedLanguage> + Send + Sync + 'static>,
|
||||
) {
|
||||
let Some(proxy) = self.language_proxy.read().clone() else {
|
||||
return;
|
||||
};
|
||||
|
||||
proxy.register_language(language, grammar, matcher, hidden, load)
|
||||
proxy.register_language(language, load)
|
||||
}
|
||||
|
||||
fn remove_languages(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user