Compare commits
40 Commits
linux/keys
...
vim-initia
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c010cd369 | ||
|
|
0437ecc901 | ||
|
|
a804e57bd4 | ||
|
|
7bd38164fc | ||
|
|
828129c46c | ||
|
|
dde5fac4b9 | ||
|
|
71fb17c507 | ||
|
|
97e437c632 | ||
|
|
66667d1eef | ||
|
|
dce22a965e | ||
|
|
5f452dbca2 | ||
|
|
b2a92097ee | ||
|
|
eb35d25a7d | ||
|
|
8742d4ab90 | ||
|
|
b829f72c17 | ||
|
|
ffa8310d04 | ||
|
|
3fda539c46 | ||
|
|
b444b326cb | ||
|
|
f196288e2d | ||
|
|
e30cc131b4 | ||
|
|
09c8a84935 | ||
|
|
6e5996a815 | ||
|
|
c8f56e38b1 | ||
|
|
cfd3b0ff7b | ||
|
|
afe23cf85a | ||
|
|
f915c24279 | ||
|
|
bdd9e015ab | ||
|
|
6bbab4b55a | ||
|
|
7450b788f3 | ||
|
|
0c03519393 | ||
|
|
636eff2e9a | ||
|
|
6c8f4002d9 | ||
|
|
91bc5aefa4 | ||
|
|
2f3564b85f | ||
|
|
d61a544400 | ||
|
|
8061bacee3 | ||
|
|
77dadfedfe | ||
|
|
0023b37bfc | ||
|
|
4ece4a635f | ||
|
|
77c2aecf93 |
102
Cargo.lock
generated
102
Cargo.lock
generated
@@ -86,7 +86,6 @@ dependencies = [
|
||||
"jsonschema",
|
||||
"language",
|
||||
"language_model",
|
||||
"language_model_selector",
|
||||
"log",
|
||||
"lsp",
|
||||
"markdown",
|
||||
@@ -492,6 +491,7 @@ dependencies = [
|
||||
"collections",
|
||||
"context_server",
|
||||
"editor",
|
||||
"feature_flags",
|
||||
"fs",
|
||||
"futures 0.3.31",
|
||||
"fuzzy",
|
||||
@@ -499,17 +499,18 @@ dependencies = [
|
||||
"indexed_docs",
|
||||
"language",
|
||||
"language_model",
|
||||
"language_model_selector",
|
||||
"languages",
|
||||
"log",
|
||||
"multi_buffer",
|
||||
"open_ai",
|
||||
"ordered-float 2.10.1",
|
||||
"parking_lot",
|
||||
"paths",
|
||||
"picker",
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
"prompt_store",
|
||||
"proto",
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
"rope",
|
||||
@@ -611,7 +612,6 @@ dependencies = [
|
||||
"serde_json",
|
||||
"settings",
|
||||
"smol",
|
||||
"terminal_view",
|
||||
"text",
|
||||
"toml 0.8.20",
|
||||
"ui",
|
||||
@@ -688,6 +688,7 @@ dependencies = [
|
||||
"portable-pty",
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
"prompt_store",
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
"reqwest_client",
|
||||
@@ -3636,9 +3637,12 @@ dependencies = [
|
||||
"gimli",
|
||||
"hashbrown 0.14.5",
|
||||
"log",
|
||||
"postcard",
|
||||
"regalloc2",
|
||||
"rustc-hash 2.1.1",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"sha2",
|
||||
"smallvec",
|
||||
"target-lexicon 0.13.2",
|
||||
]
|
||||
@@ -4052,6 +4056,7 @@ dependencies = [
|
||||
"paths",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"smol",
|
||||
"task",
|
||||
"util",
|
||||
"workspace-hack",
|
||||
@@ -5016,6 +5021,7 @@ dependencies = [
|
||||
"shellexpand 2.1.2",
|
||||
"smol",
|
||||
"telemetry",
|
||||
"terminal_view",
|
||||
"toml 0.8.20",
|
||||
"unindent",
|
||||
"util",
|
||||
@@ -5154,6 +5160,7 @@ dependencies = [
|
||||
"language_extension",
|
||||
"log",
|
||||
"lsp",
|
||||
"moka",
|
||||
"node_runtime",
|
||||
"parking_lot",
|
||||
"paths",
|
||||
@@ -5910,6 +5917,20 @@ dependencies = [
|
||||
"thread_local",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generator"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d18470a76cb7f8ff746cf1f7470914f900252ec36bbc40b569d74b1258446827"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"log",
|
||||
"rustversion",
|
||||
"windows 0.61.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
@@ -8753,25 +8774,6 @@ dependencies = [
|
||||
"zed_llm_client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "language_model_selector"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"collections",
|
||||
"feature_flags",
|
||||
"futures 0.3.31",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
"language_model",
|
||||
"log",
|
||||
"ordered-float 2.10.1",
|
||||
"picker",
|
||||
"proto",
|
||||
"ui",
|
||||
"workspace-hack",
|
||||
"zed_actions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "language_models"
|
||||
version = "0.1.0"
|
||||
@@ -9348,6 +9350,19 @@ dependencies = [
|
||||
"logos-codegen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "loom"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"generator",
|
||||
"scoped-tls",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "loop9"
|
||||
version = "0.1.5"
|
||||
@@ -9526,6 +9541,7 @@ dependencies = [
|
||||
"async-recursion 1.1.1",
|
||||
"collections",
|
||||
"editor",
|
||||
"fs",
|
||||
"gpui",
|
||||
"language",
|
||||
"linkify",
|
||||
@@ -9846,6 +9862,25 @@ dependencies = [
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "moka"
|
||||
version = "0.12.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9321642ca94a4282428e6ea4af8cc2ca4eac48ac7a6a4ea8f33f76d0ce70926"
|
||||
dependencies = [
|
||||
"crossbeam-channel",
|
||||
"crossbeam-epoch",
|
||||
"crossbeam-utils",
|
||||
"loom",
|
||||
"parking_lot",
|
||||
"portable-atomic",
|
||||
"rustc_version",
|
||||
"smallvec",
|
||||
"tagptr",
|
||||
"thiserror 1.0.69",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "msvc_spectre_libs"
|
||||
version = "0.1.3"
|
||||
@@ -10043,7 +10078,6 @@ dependencies = [
|
||||
"async-tar",
|
||||
"async-trait",
|
||||
"async-watch",
|
||||
"async_zip",
|
||||
"futures 0.3.31",
|
||||
"http_client",
|
||||
"log",
|
||||
@@ -10052,9 +10086,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"smol",
|
||||
"tempfile",
|
||||
"util",
|
||||
"walkdir",
|
||||
"which 6.0.3",
|
||||
"workspace-hack",
|
||||
]
|
||||
@@ -12782,6 +12814,7 @@ dependencies = [
|
||||
"hashbrown 0.15.3",
|
||||
"log",
|
||||
"rustc-hash 2.1.1",
|
||||
"serde",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
@@ -15433,6 +15466,12 @@ dependencies = [
|
||||
"slotmap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tagptr"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417"
|
||||
|
||||
[[package]]
|
||||
name = "take-until"
|
||||
version = "0.2.0"
|
||||
@@ -15616,6 +15655,7 @@ name = "terminal_view"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"assistant_slash_command",
|
||||
"async-recursion 1.1.1",
|
||||
"breadcrumbs",
|
||||
"client",
|
||||
@@ -16988,6 +17028,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-fs",
|
||||
"async_zip",
|
||||
"collections",
|
||||
"dirs 4.0.0",
|
||||
"dunce",
|
||||
@@ -17009,6 +17050,7 @@ dependencies = [
|
||||
"tendril",
|
||||
"unicase",
|
||||
"util_macros",
|
||||
"walkdir",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
@@ -19096,6 +19138,7 @@ dependencies = [
|
||||
"aho-corasick",
|
||||
"anstream",
|
||||
"arrayvec",
|
||||
"async-compression",
|
||||
"async-std",
|
||||
"async-tungstenite",
|
||||
"aws-config",
|
||||
@@ -19128,7 +19171,9 @@ dependencies = [
|
||||
"core-foundation 0.9.4",
|
||||
"core-foundation-sys",
|
||||
"coreaudio-sys",
|
||||
"cranelift-codegen",
|
||||
"crc32fast",
|
||||
"crossbeam-epoch",
|
||||
"crossbeam-utils",
|
||||
"crypto-common",
|
||||
"deranged",
|
||||
@@ -19204,6 +19249,7 @@ dependencies = [
|
||||
"rand 0.9.1",
|
||||
"rand_chacha 0.3.1",
|
||||
"rand_core 0.6.4",
|
||||
"regalloc2",
|
||||
"regex",
|
||||
"regex-automata 0.4.9",
|
||||
"regex-syntax 0.8.5",
|
||||
@@ -19608,7 +19654,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.188.0"
|
||||
version = "0.189.0"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"agent",
|
||||
@@ -19803,9 +19849,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_llm_client"
|
||||
version = "0.8.1"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "16d993fc42f9ec43ab76fa46c6eb579a66e116bb08cd2bc9a67f3afcaa05d39d"
|
||||
checksum = "9be71e2f9b271e1eb8eb3e0d986075e770d1a0a299fb036abc3f1fc13a2fa7eb"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
|
||||
@@ -80,7 +80,6 @@ members = [
|
||||
"crates/language",
|
||||
"crates/language_extension",
|
||||
"crates/language_model",
|
||||
"crates/language_model_selector",
|
||||
"crates/language_models",
|
||||
"crates/language_selector",
|
||||
"crates/language_tools",
|
||||
@@ -287,7 +286,6 @@ journal = { path = "crates/journal" }
|
||||
language = { path = "crates/language" }
|
||||
language_extension = { path = "crates/language_extension" }
|
||||
language_model = { path = "crates/language_model" }
|
||||
language_model_selector = { path = "crates/language_model_selector" }
|
||||
language_models = { path = "crates/language_models" }
|
||||
language_selector = { path = "crates/language_selector" }
|
||||
language_tools = { path = "crates/language_tools" }
|
||||
@@ -476,6 +474,7 @@ lsp-types = { git = "https://github.com/zed-industries/lsp-types", rev = "c9c189
|
||||
markup5ever_rcdom = "0.3.0"
|
||||
metal = "0.29"
|
||||
mlua = { version = "0.10", features = ["lua54", "vendored", "async", "send"] }
|
||||
moka = { version = "0.12.10", features = ["sync"] }
|
||||
naga = { version = "25.0", features = ["wgsl-in"] }
|
||||
nanoid = "0.4"
|
||||
nbformat = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
|
||||
@@ -599,7 +598,7 @@ unindent = "0.2.0"
|
||||
url = "2.2"
|
||||
urlencoding = "2.1.2"
|
||||
uuid = { version = "1.1.2", features = ["v4", "v5", "v7", "serde"] }
|
||||
walkdir = "2.3"
|
||||
walkdir = "2.5"
|
||||
wasi-preview1-component-adapter-provider = "29"
|
||||
wasm-encoder = "0.221"
|
||||
wasmparser = "0.221"
|
||||
@@ -609,13 +608,14 @@ wasmtime = { version = "29", default-features = false, features = [
|
||||
"runtime",
|
||||
"cranelift",
|
||||
"component-model",
|
||||
"incremental-cache",
|
||||
"parallel-compilation",
|
||||
] }
|
||||
wasmtime-wasi = "29"
|
||||
which = "6.0.0"
|
||||
wit-component = "0.221"
|
||||
workspace-hack = "0.1.0"
|
||||
zed_llm_client = "0.8.1"
|
||||
zed_llm_client = "0.8.2"
|
||||
zstd = "0.11"
|
||||
|
||||
[workspace.dependencies.async-stripe]
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
"f4": "debugger::Start",
|
||||
"f5": "debugger::Continue",
|
||||
"shift-f5": "debugger::Stop",
|
||||
"ctrl-shift-f5": "debugger::Restart",
|
||||
"f6": "debugger::Pause",
|
||||
"f7": "debugger::StepOver",
|
||||
"cmd-f11": "debugger::StepInto",
|
||||
@@ -558,6 +559,7 @@
|
||||
"ctrl-shift-e": "project_panel::ToggleFocus",
|
||||
"ctrl-shift-b": "outline_panel::ToggleFocus",
|
||||
"ctrl-shift-g": "git_panel::ToggleFocus",
|
||||
"ctrl-shift-d": "debug_panel::ToggleFocus",
|
||||
"ctrl-?": "agent::ToggleFocus",
|
||||
"alt-save": "workspace::SaveAll",
|
||||
"ctrl-alt-s": "workspace::SaveAll",
|
||||
@@ -595,7 +597,6 @@
|
||||
{
|
||||
"context": "Editor",
|
||||
"bindings": {
|
||||
"ctrl-shift-d": "editor::DuplicateLineDown",
|
||||
"ctrl-shift-j": "editor::JoinLines",
|
||||
"ctrl-alt-backspace": "editor::DeleteToPreviousSubwordStart",
|
||||
"ctrl-alt-h": "editor::DeleteToPreviousSubwordStart",
|
||||
@@ -862,6 +863,13 @@
|
||||
"alt-l": "git::GenerateCommitMessage"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "DebugPanel",
|
||||
"bindings": {
|
||||
"ctrl-t": "debugger::ToggleThreadPicker",
|
||||
"ctrl-i": "debugger::ToggleSessionPicker"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "CollabPanel && not_editing",
|
||||
"bindings": {
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"f4": "debugger::Start",
|
||||
"f5": "debugger::Continue",
|
||||
"shift-f5": "debugger::Stop",
|
||||
"shift-cmd-f5": "debugger::Restart",
|
||||
"f6": "debugger::Pause",
|
||||
"f7": "debugger::StepOver",
|
||||
"f11": "debugger::StepInto",
|
||||
@@ -624,6 +625,7 @@
|
||||
"cmd-shift-e": "project_panel::ToggleFocus",
|
||||
"cmd-shift-b": "outline_panel::ToggleFocus",
|
||||
"ctrl-shift-g": "git_panel::ToggleFocus",
|
||||
"cmd-shift-d": "debug_panel::ToggleFocus",
|
||||
"cmd-?": "agent::ToggleFocus",
|
||||
"cmd-alt-s": "workspace::SaveAll",
|
||||
"cmd-k m": "language_selector::Toggle",
|
||||
@@ -929,6 +931,13 @@
|
||||
"alt-tab": "git::GenerateCommitMessage"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "DebugPanel",
|
||||
"bindings": {
|
||||
"cmd-t": "debugger::ToggleThreadPicker",
|
||||
"cmd-i": "debugger::ToggleSessionPicker"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "CollabPanel && not_editing",
|
||||
"use_key_equivalents": true,
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor",
|
||||
"context": "Editor && vim_mode == insert",
|
||||
"bindings": {
|
||||
// "j k": ["workspace::SendKeystrokes", "escape"]
|
||||
}
|
||||
|
||||
@@ -52,7 +52,6 @@ itertools.workspace = true
|
||||
jsonschema.workspace = true
|
||||
language.workspace = true
|
||||
language_model.workspace = true
|
||||
language_model_selector.workspace = true
|
||||
log.workspace = true
|
||||
lsp.workspace = true
|
||||
markdown.workspace = true
|
||||
|
||||
@@ -217,7 +217,6 @@ fn register_slash_commands(cx: &mut App) {
|
||||
slash_command_registry.register_command(assistant_slash_commands::PromptSlashCommand, true);
|
||||
slash_command_registry.register_command(assistant_slash_commands::SelectionCommand, true);
|
||||
slash_command_registry.register_command(assistant_slash_commands::DefaultSlashCommand, false);
|
||||
slash_command_registry.register_command(assistant_slash_commands::TerminalSlashCommand, true);
|
||||
slash_command_registry.register_command(assistant_slash_commands::NowSlashCommand, false);
|
||||
slash_command_registry
|
||||
.register_command(assistant_slash_commands::DiagnosticsSlashCommand, true);
|
||||
|
||||
@@ -3,10 +3,10 @@ use fs::Fs;
|
||||
use gpui::{Entity, FocusHandle, SharedString};
|
||||
|
||||
use crate::Thread;
|
||||
use language_model::{ConfiguredModel, LanguageModelRegistry};
|
||||
use language_model_selector::{
|
||||
use assistant_context_editor::language_model_selector::{
|
||||
LanguageModelSelector, LanguageModelSelectorPopoverMenu, ToggleModelSelector,
|
||||
};
|
||||
use language_model::{ConfiguredModel, LanguageModelRegistry};
|
||||
use settings::update_settings_file;
|
||||
use std::sync::Arc;
|
||||
use ui::{PopoverMenuHandle, Tooltip, prelude::*};
|
||||
|
||||
@@ -17,6 +17,7 @@ use assistant_settings::{AssistantDockPosition, AssistantSettings};
|
||||
use assistant_slash_command::SlashCommandWorkingSet;
|
||||
use assistant_tool::ToolWorkingSet;
|
||||
|
||||
use assistant_context_editor::language_model_selector::ToggleModelSelector;
|
||||
use client::{UserStore, zed_urls};
|
||||
use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
|
||||
use fs::Fs;
|
||||
@@ -30,7 +31,6 @@ use language::LanguageRegistry;
|
||||
use language_model::{
|
||||
LanguageModelProviderTosView, LanguageModelRegistry, RequestUsage, ZED_CLOUD_PROVIDER_ID,
|
||||
};
|
||||
use language_model_selector::ToggleModelSelector;
|
||||
use project::{Project, ProjectPath, Worktree};
|
||||
use prompt_store::{PromptBuilder, PromptStore, UserPromptId};
|
||||
use proto::Plan;
|
||||
|
||||
@@ -9,6 +9,7 @@ use crate::terminal_codegen::TerminalCodegen;
|
||||
use crate::thread_store::{TextThreadStore, ThreadStore};
|
||||
use crate::{CycleNextInlineAssist, CyclePreviousInlineAssist};
|
||||
use crate::{RemoveAllContext, ToggleContextPicker};
|
||||
use assistant_context_editor::language_model_selector::ToggleModelSelector;
|
||||
use client::ErrorExt;
|
||||
use collections::VecDeque;
|
||||
use db::kvp::Dismissable;
|
||||
@@ -24,7 +25,6 @@ use gpui::{
|
||||
Focusable, FontWeight, Subscription, TextStyle, WeakEntity, Window, anchored, deferred, point,
|
||||
};
|
||||
use language_model::{LanguageModel, LanguageModelRegistry};
|
||||
use language_model_selector::ToggleModelSelector;
|
||||
use parking_lot::Mutex;
|
||||
use settings::Settings;
|
||||
use std::cmp;
|
||||
|
||||
@@ -8,6 +8,7 @@ use crate::ui::{
|
||||
AnimatedLabel, MaxModeTooltip,
|
||||
preview::{AgentPreview, UsageCallout},
|
||||
};
|
||||
use assistant_context_editor::language_model_selector::ToggleModelSelector;
|
||||
use assistant_settings::{AssistantSettings, CompletionMode};
|
||||
use buffer_diff::BufferDiff;
|
||||
use client::UserStore;
|
||||
@@ -30,7 +31,6 @@ use language_model::{
|
||||
ConfiguredModel, LanguageModelRequestMessage, MessageContent, RequestUsage,
|
||||
ZED_CLOUD_PROVIDER_ID,
|
||||
};
|
||||
use language_model_selector::ToggleModelSelector;
|
||||
use multi_buffer;
|
||||
use project::Project;
|
||||
use prompt_store::PromptStore;
|
||||
|
||||
@@ -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
|
||||
@@ -29,15 +30,16 @@ gpui.workspace = true
|
||||
indexed_docs.workspace = true
|
||||
language.workspace = true
|
||||
language_model.workspace = true
|
||||
language_model_selector.workspace = true
|
||||
log.workspace = true
|
||||
multi_buffer.workspace = true
|
||||
open_ai.workspace = true
|
||||
ordered-float.workspace = true
|
||||
parking_lot.workspace = true
|
||||
paths.workspace = true
|
||||
picker.workspace = true
|
||||
project.workspace = true
|
||||
prompt_store.workspace = true
|
||||
proto.workspace = true
|
||||
regex.workspace = true
|
||||
rope.workspace = true
|
||||
rpc.workspace = true
|
||||
|
||||
@@ -2,6 +2,7 @@ mod context;
|
||||
mod context_editor;
|
||||
mod context_history;
|
||||
mod context_store;
|
||||
pub mod language_model_selector;
|
||||
mod slash_command;
|
||||
mod slash_command_picker;
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
use crate::language_model_selector::{
|
||||
LanguageModelSelector, LanguageModelSelectorPopoverMenu, ToggleModelSelector,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use assistant_settings::AssistantSettings;
|
||||
use assistant_slash_command::{SlashCommand, SlashCommandOutputSection, SlashCommandWorkingSet};
|
||||
@@ -36,9 +39,6 @@ use language_model::{
|
||||
LanguageModelImage, LanguageModelProvider, LanguageModelProviderTosView, LanguageModelRegistry,
|
||||
Role,
|
||||
};
|
||||
use language_model_selector::{
|
||||
LanguageModelSelector, LanguageModelSelectorPopoverMenu, ToggleModelSelector,
|
||||
};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use picker::Picker;
|
||||
use project::{Project, Worktree};
|
||||
|
||||
@@ -9,6 +9,7 @@ use anyhow::Result;
|
||||
use futures::StreamExt;
|
||||
use futures::stream::{self, BoxStream};
|
||||
use gpui::{App, SharedString, Task, WeakEntity, Window};
|
||||
use language::HighlightId;
|
||||
use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate, OffsetRangeExt};
|
||||
pub use language_model::Role;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -16,6 +17,7 @@ use std::{
|
||||
ops::Range,
|
||||
sync::{Arc, atomic::AtomicBool},
|
||||
};
|
||||
use ui::ActiveTheme;
|
||||
use workspace::{Workspace, ui::IconName};
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
@@ -325,6 +327,18 @@ impl SlashCommandLine {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_label_for_command(command_name: &str, arguments: &[&str], cx: &App) -> CodeLabel {
|
||||
let mut label = CodeLabel::default();
|
||||
label.push_str(command_name, None);
|
||||
label.push_str(" ", None);
|
||||
label.push_str(
|
||||
&arguments.join(" "),
|
||||
cx.theme().syntax().highlight_id("comment").map(HighlightId),
|
||||
);
|
||||
label.filter_range = 0..command_name.len();
|
||||
label
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
@@ -35,7 +35,6 @@ rope.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
smol.workspace = true
|
||||
terminal_view.workspace = true
|
||||
text.workspace = true
|
||||
toml.workspace = true
|
||||
ui.workspace = true
|
||||
|
||||
@@ -12,11 +12,6 @@ mod selection_command;
|
||||
mod streaming_example_command;
|
||||
mod symbols_command;
|
||||
mod tab_command;
|
||||
mod terminal_command;
|
||||
|
||||
use gpui::App;
|
||||
use language::{CodeLabel, HighlightId};
|
||||
use ui::ActiveTheme as _;
|
||||
|
||||
pub use crate::cargo_workspace_command::*;
|
||||
pub use crate::context_server_command::*;
|
||||
@@ -32,16 +27,5 @@ pub use crate::selection_command::*;
|
||||
pub use crate::streaming_example_command::*;
|
||||
pub use crate::symbols_command::*;
|
||||
pub use crate::tab_command::*;
|
||||
pub use crate::terminal_command::*;
|
||||
|
||||
pub fn create_label_for_command(command_name: &str, arguments: &[&str], cx: &App) -> CodeLabel {
|
||||
let mut label = CodeLabel::default();
|
||||
label.push_str(command_name, None);
|
||||
label.push_str(" ", None);
|
||||
label.push_str(
|
||||
&arguments.join(" "),
|
||||
cx.theme().syntax().highlight_id("comment").map(HighlightId),
|
||||
);
|
||||
label.filter_range = 0..command_name.len();
|
||||
label
|
||||
}
|
||||
use assistant_slash_command::create_label_for_command;
|
||||
|
||||
@@ -41,6 +41,7 @@ open.workspace = true
|
||||
paths.workspace = true
|
||||
portable-pty.workspace = true
|
||||
project.workspace = true
|
||||
prompt_store.workspace = true
|
||||
regex.workspace = true
|
||||
rust-embed.workspace = true
|
||||
schemars.workspace = true
|
||||
|
||||
@@ -18,6 +18,7 @@ use language_model::{
|
||||
LanguageModelToolResultContent, LanguageModelToolUse, LanguageModelToolUseId, SelectedModel,
|
||||
};
|
||||
use project::Project;
|
||||
use prompt_store::{ModelContext, ProjectContext, PromptBuilder, WorktreeContext};
|
||||
use rand::prelude::*;
|
||||
use reqwest_client::ReqwestClient;
|
||||
use serde_json::json;
|
||||
@@ -895,52 +896,24 @@ fn eval_add_overwrite_test() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // until we figure out the mystery described in the comments
|
||||
// #[cfg_attr(not(feature = "eval"), ignore)]
|
||||
#[cfg_attr(not(feature = "eval"), ignore)]
|
||||
fn eval_create_empty_file() {
|
||||
// Check that Edit Agent can create a file without writing its
|
||||
// thoughts into it. This issue is not specific to empty files, but
|
||||
// it's easier to reproduce with them.
|
||||
//
|
||||
// NOTE: For some mysterious reason, I could easily reproduce this
|
||||
// issue roughly 90% of the time in actual Zed. However, once I
|
||||
// extract the exact LLM request before the failure point and
|
||||
// generate from that, the reproduction rate drops to 2%!
|
||||
//
|
||||
// Things I've tried to make sure it's not a fluke: disabling prompt
|
||||
// caching, capturing the LLM request via a proxy server, running the
|
||||
// prompt on Claude separately from evals. Every time it was mostly
|
||||
// giving good outcomes, which doesn't match my actual experience in
|
||||
// Zed.
|
||||
//
|
||||
// At some point I discovered that simply adding one insignificant
|
||||
// space or a newline to the prompt suddenly results in an outcome I
|
||||
// tried to reproduce almost perfectly.
|
||||
//
|
||||
// This weirdness happens even outside of the Zed code base and even
|
||||
// when using a different subscription. The result is the same: an
|
||||
// extra newline or space changes the model behavior significantly
|
||||
// enough, so that the pass rate drops from 99% to 0-3%
|
||||
//
|
||||
// I have no explanation to this.
|
||||
//
|
||||
//
|
||||
// Model | Pass rate
|
||||
// ============================================
|
||||
//
|
||||
// --------------------------------------------
|
||||
// Prompt version: 2025-05-19
|
||||
// Prompt version: 2025-05-21
|
||||
// --------------------------------------------
|
||||
//
|
||||
// claude-3.7-sonnet | 0.98
|
||||
// + one extra space in prompt | 0.00
|
||||
// + original prompt again | 0.99
|
||||
// + extra newline | 0.03
|
||||
// claude-3.7-sonnet | 1.00
|
||||
// gemini-2.5-pro-preview-03-25 | 1.00
|
||||
// gemini-2.5-flash-preview-04-17 | 1.00
|
||||
// + one extra space | 1.00
|
||||
// gpt-4.1 | 1.00
|
||||
// + one extra space | 1.00
|
||||
//
|
||||
//
|
||||
// TODO: gpt-4.1-mini errored 38 times:
|
||||
@@ -949,8 +922,8 @@ fn eval_create_empty_file() {
|
||||
let input_file_content = None;
|
||||
let expected_output_content = String::new();
|
||||
eval(
|
||||
1,
|
||||
1.0,
|
||||
100,
|
||||
0.99,
|
||||
EvalInput::from_conversation(
|
||||
vec![
|
||||
message(User, [text("Create a second empty todo file ")]),
|
||||
@@ -1442,24 +1415,59 @@ impl EditAgentTest {
|
||||
.update(cx, |project, cx| project.open_buffer(path, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let conversation = LanguageModelRequest {
|
||||
messages: eval.conversation,
|
||||
tools: cx.update(|cx| {
|
||||
ToolRegistry::default_global(cx)
|
||||
.tools()
|
||||
.into_iter()
|
||||
.filter_map(|tool| {
|
||||
let input_schema = tool
|
||||
.input_schema(self.agent.model.tool_input_format())
|
||||
.ok()?;
|
||||
Some(LanguageModelRequestTool {
|
||||
name: tool.name(),
|
||||
description: tool.description(),
|
||||
input_schema,
|
||||
})
|
||||
let tools = cx.update(|cx| {
|
||||
ToolRegistry::default_global(cx)
|
||||
.tools()
|
||||
.into_iter()
|
||||
.filter_map(|tool| {
|
||||
let input_schema = tool
|
||||
.input_schema(self.agent.model.tool_input_format())
|
||||
.ok()?;
|
||||
Some(LanguageModelRequestTool {
|
||||
name: tool.name(),
|
||||
description: tool.description(),
|
||||
input_schema,
|
||||
})
|
||||
.collect()
|
||||
}),
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
let tool_names = tools
|
||||
.iter()
|
||||
.map(|tool| tool.name.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let worktrees = vec![WorktreeContext {
|
||||
root_name: "root".to_string(),
|
||||
rules_file: None,
|
||||
}];
|
||||
let prompt_builder = PromptBuilder::new(None)?;
|
||||
let project_context = ProjectContext::new(worktrees, Vec::default());
|
||||
let system_prompt = prompt_builder.generate_assistant_system_prompt(
|
||||
&project_context,
|
||||
&ModelContext {
|
||||
available_tools: tool_names,
|
||||
},
|
||||
)?;
|
||||
|
||||
let has_system_prompt = eval
|
||||
.conversation
|
||||
.first()
|
||||
.map_or(false, |msg| msg.role == Role::System);
|
||||
let messages = if has_system_prompt {
|
||||
eval.conversation
|
||||
} else {
|
||||
[LanguageModelRequestMessage {
|
||||
role: Role::System,
|
||||
content: vec![MessageContent::Text(system_prompt)],
|
||||
cache: true,
|
||||
}]
|
||||
.into_iter()
|
||||
.chain(eval.conversation)
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
let conversation = LanguageModelRequest {
|
||||
messages,
|
||||
tools,
|
||||
..Default::default()
|
||||
};
|
||||
let edit_output = if matches!(eval.edit_file_input.mode, EditFileMode::Edit) {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
You are an expert engineer and your task is to write a new file from scratch.
|
||||
|
||||
<file_to_edit>
|
||||
You MUST respond directly with the file's content, without explanations, additional text or triple backticks.
|
||||
The text you output will be saved verbatim as the content of the file.
|
||||
Tool calls have been disabled. You MUST start your response directly with the file's new content.
|
||||
|
||||
<file_path>
|
||||
{{path}}
|
||||
</file_to_edit>
|
||||
</file_path>
|
||||
|
||||
<edit_description>
|
||||
{{edit_description}}
|
||||
</edit_description>
|
||||
|
||||
You MUST respond directly with the file's content, without explanations, additional text or triple backticks.
|
||||
The text you output will be saved verbatim as the content of the file.
|
||||
|
||||
@@ -39,7 +39,7 @@ struct UpdateRequestBody {
|
||||
destination: &'static str,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum VersionCheckType {
|
||||
Sha(String),
|
||||
Semantic(SemanticVersion),
|
||||
@@ -491,62 +491,38 @@ impl AutoUpdater {
|
||||
Self::get_release(this, asset, os, arch, None, release_channel, cx).await
|
||||
}
|
||||
|
||||
fn installed_update_version(&self) -> Option<VersionCheckType> {
|
||||
match &self.status {
|
||||
AutoUpdateStatus::Updated { version, .. } => Some(version.clone()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
async fn update(this: Entity<Self>, mut cx: AsyncApp) -> Result<()> {
|
||||
let (client, current_version, installed_update_version, release_channel) =
|
||||
let (client, installed_version, status, release_channel) =
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.status = AutoUpdateStatus::Checking;
|
||||
cx.notify();
|
||||
(
|
||||
this.http_client.clone(),
|
||||
this.current_version,
|
||||
this.installed_update_version(),
|
||||
this.status.clone(),
|
||||
ReleaseChannel::try_global(cx),
|
||||
)
|
||||
})?;
|
||||
|
||||
let release =
|
||||
let fetched_release_data =
|
||||
Self::get_latest_release(&this, "zed", OS, ARCH, release_channel, &mut cx).await?;
|
||||
let fetched_version = fetched_release_data.clone().version;
|
||||
let app_commit_sha = cx.update(|cx| AppCommitSha::try_global(cx).map(|sha| sha.0));
|
||||
let newer_version = Self::check_for_newer_version(
|
||||
*RELEASE_CHANNEL,
|
||||
app_commit_sha,
|
||||
installed_version,
|
||||
status,
|
||||
fetched_version,
|
||||
)?;
|
||||
|
||||
let update_version_to_install = match *RELEASE_CHANNEL {
|
||||
ReleaseChannel::Nightly => {
|
||||
let should_download = cx
|
||||
.update(|cx| AppCommitSha::try_global(cx).map(|sha| release.version != sha.0))
|
||||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or(true);
|
||||
|
||||
should_download.then(|| VersionCheckType::Sha(release.version.clone()))
|
||||
}
|
||||
_ => {
|
||||
let installed_version =
|
||||
installed_update_version.unwrap_or(VersionCheckType::Semantic(current_version));
|
||||
match installed_version {
|
||||
VersionCheckType::Sha(_) => {
|
||||
log::warn!("Unexpected SHA-based version in non-nightly build");
|
||||
Some(installed_version)
|
||||
}
|
||||
VersionCheckType::Semantic(semantic_comparison_version) => {
|
||||
let latest_release_version = release.version.parse::<SemanticVersion>()?;
|
||||
let should_download = latest_release_version > semantic_comparison_version;
|
||||
should_download.then(|| VersionCheckType::Semantic(latest_release_version))
|
||||
}
|
||||
let Some(newer_version) = newer_version else {
|
||||
return this.update(&mut cx, |this, cx| {
|
||||
if !matches!(this.status, AutoUpdateStatus::Updated { .. }) {
|
||||
this.status = AutoUpdateStatus::Idle;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let Some(update_version) = update_version_to_install else {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.status = AutoUpdateStatus::Idle;
|
||||
cx.notify();
|
||||
})?;
|
||||
return Ok(());
|
||||
});
|
||||
};
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
@@ -555,6 +531,71 @@ impl AutoUpdater {
|
||||
})?;
|
||||
|
||||
let installer_dir = InstallerDir::new().await?;
|
||||
let target_path = Self::target_path(&installer_dir).await?;
|
||||
download_release(&target_path, fetched_release_data, client, &cx).await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.status = AutoUpdateStatus::Installing;
|
||||
cx.notify();
|
||||
})?;
|
||||
|
||||
let binary_path = Self::binary_path(installer_dir, target_path, &cx).await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.set_should_show_update_notification(true, cx)
|
||||
.detach_and_log_err(cx);
|
||||
this.status = AutoUpdateStatus::Updated {
|
||||
binary_path,
|
||||
version: newer_version,
|
||||
};
|
||||
cx.notify();
|
||||
})
|
||||
}
|
||||
|
||||
fn check_for_newer_version(
|
||||
release_channel: ReleaseChannel,
|
||||
app_commit_sha: Result<Option<String>>,
|
||||
installed_version: SemanticVersion,
|
||||
status: AutoUpdateStatus,
|
||||
fetched_version: String,
|
||||
) -> Result<Option<VersionCheckType>> {
|
||||
let parsed_fetched_version = fetched_version.parse::<SemanticVersion>();
|
||||
|
||||
if let AutoUpdateStatus::Updated { version, .. } = status {
|
||||
match version {
|
||||
VersionCheckType::Sha(cached_version) => {
|
||||
let should_download = fetched_version != cached_version;
|
||||
let newer_version =
|
||||
should_download.then(|| VersionCheckType::Sha(fetched_version));
|
||||
return Ok(newer_version);
|
||||
}
|
||||
VersionCheckType::Semantic(cached_version) => {
|
||||
return Self::check_for_newer_version_non_nightly(
|
||||
cached_version,
|
||||
parsed_fetched_version?,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match release_channel {
|
||||
ReleaseChannel::Nightly => {
|
||||
let should_download = app_commit_sha
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|sha| fetched_version != sha)
|
||||
.unwrap_or(true);
|
||||
let newer_version = should_download.then(|| VersionCheckType::Sha(fetched_version));
|
||||
Ok(newer_version)
|
||||
}
|
||||
_ => Self::check_for_newer_version_non_nightly(
|
||||
installed_version,
|
||||
parsed_fetched_version?,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
async fn target_path(installer_dir: &InstallerDir) -> Result<PathBuf> {
|
||||
let filename = match OS {
|
||||
"macos" => anyhow::Ok("Zed.dmg"),
|
||||
"linux" => Ok("zed.tar.gz"),
|
||||
@@ -568,32 +609,29 @@ impl AutoUpdater {
|
||||
"Aborting. Could not find rsync which is required for auto-updates."
|
||||
);
|
||||
|
||||
let downloaded_asset = installer_dir.path().join(filename);
|
||||
download_release(&downloaded_asset, release.clone(), client, &cx).await?;
|
||||
Ok(installer_dir.path().join(filename))
|
||||
}
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.status = AutoUpdateStatus::Installing;
|
||||
cx.notify();
|
||||
})?;
|
||||
|
||||
let binary_path = match OS {
|
||||
"macos" => install_release_macos(&installer_dir, downloaded_asset, &cx).await,
|
||||
"linux" => install_release_linux(&installer_dir, downloaded_asset, &cx).await,
|
||||
"windows" => install_release_windows(downloaded_asset).await,
|
||||
async fn binary_path(
|
||||
installer_dir: InstallerDir,
|
||||
target_path: PathBuf,
|
||||
cx: &AsyncApp,
|
||||
) -> Result<PathBuf> {
|
||||
match OS {
|
||||
"macos" => install_release_macos(&installer_dir, target_path, cx).await,
|
||||
"linux" => install_release_linux(&installer_dir, target_path, cx).await,
|
||||
"windows" => install_release_windows(target_path).await,
|
||||
unsupported_os => anyhow::bail!("not supported: {unsupported_os}"),
|
||||
}?;
|
||||
}
|
||||
}
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.set_should_show_update_notification(true, cx)
|
||||
.detach_and_log_err(cx);
|
||||
this.status = AutoUpdateStatus::Updated {
|
||||
binary_path,
|
||||
version: update_version,
|
||||
};
|
||||
cx.notify();
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
fn check_for_newer_version_non_nightly(
|
||||
installed_version: SemanticVersion,
|
||||
fetched_version: SemanticVersion,
|
||||
) -> Result<Option<VersionCheckType>> {
|
||||
let should_download = fetched_version > installed_version;
|
||||
let newer_version = should_download.then(|| VersionCheckType::Semantic(fetched_version));
|
||||
Ok(newer_version)
|
||||
}
|
||||
|
||||
pub fn set_should_show_update_notification(
|
||||
@@ -868,3 +906,255 @@ pub fn check_pending_installation() -> bool {
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_stable_does_not_update_when_fetched_version_is_not_higher() {
|
||||
let release_channel = ReleaseChannel::Stable;
|
||||
let app_commit_sha = Ok(Some("a".to_string()));
|
||||
let installed_version = SemanticVersion::new(1, 0, 0);
|
||||
let status = AutoUpdateStatus::Idle;
|
||||
let fetched_version = SemanticVersion::new(1, 0, 0);
|
||||
|
||||
let newer_version = AutoUpdater::check_for_newer_version(
|
||||
release_channel,
|
||||
app_commit_sha,
|
||||
installed_version,
|
||||
status,
|
||||
fetched_version.to_string(),
|
||||
);
|
||||
|
||||
assert_eq!(newer_version.unwrap(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stable_does_update_when_fetched_version_is_higher() {
|
||||
let release_channel = ReleaseChannel::Stable;
|
||||
let app_commit_sha = Ok(Some("a".to_string()));
|
||||
let installed_version = SemanticVersion::new(1, 0, 0);
|
||||
let status = AutoUpdateStatus::Idle;
|
||||
let fetched_version = SemanticVersion::new(1, 0, 1);
|
||||
|
||||
let newer_version = AutoUpdater::check_for_newer_version(
|
||||
release_channel,
|
||||
app_commit_sha,
|
||||
installed_version,
|
||||
status,
|
||||
fetched_version.to_string(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
newer_version.unwrap(),
|
||||
Some(VersionCheckType::Semantic(fetched_version))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stable_does_not_update_when_fetched_version_is_not_higher_than_cached() {
|
||||
let release_channel = ReleaseChannel::Stable;
|
||||
let app_commit_sha = Ok(Some("a".to_string()));
|
||||
let installed_version = SemanticVersion::new(1, 0, 0);
|
||||
let status = AutoUpdateStatus::Updated {
|
||||
binary_path: PathBuf::new(),
|
||||
version: VersionCheckType::Semantic(SemanticVersion::new(1, 0, 1)),
|
||||
};
|
||||
let fetched_version = SemanticVersion::new(1, 0, 1);
|
||||
|
||||
let newer_version = AutoUpdater::check_for_newer_version(
|
||||
release_channel,
|
||||
app_commit_sha,
|
||||
installed_version,
|
||||
status,
|
||||
fetched_version.to_string(),
|
||||
);
|
||||
|
||||
assert_eq!(newer_version.unwrap(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stable_does_update_when_fetched_version_is_higher_than_cached() {
|
||||
let release_channel = ReleaseChannel::Stable;
|
||||
let app_commit_sha = Ok(Some("a".to_string()));
|
||||
let installed_version = SemanticVersion::new(1, 0, 0);
|
||||
let status = AutoUpdateStatus::Updated {
|
||||
binary_path: PathBuf::new(),
|
||||
version: VersionCheckType::Semantic(SemanticVersion::new(1, 0, 1)),
|
||||
};
|
||||
let fetched_version = SemanticVersion::new(1, 0, 2);
|
||||
|
||||
let newer_version = AutoUpdater::check_for_newer_version(
|
||||
release_channel,
|
||||
app_commit_sha,
|
||||
installed_version,
|
||||
status,
|
||||
fetched_version.to_string(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
newer_version.unwrap(),
|
||||
Some(VersionCheckType::Semantic(fetched_version))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nightly_does_not_update_when_fetched_sha_is_same() {
|
||||
let release_channel = ReleaseChannel::Nightly;
|
||||
let app_commit_sha = Ok(Some("a".to_string()));
|
||||
let installed_version = SemanticVersion::new(1, 0, 0);
|
||||
let status = AutoUpdateStatus::Idle;
|
||||
let fetched_sha = "a".to_string();
|
||||
|
||||
let newer_version = AutoUpdater::check_for_newer_version(
|
||||
release_channel,
|
||||
app_commit_sha,
|
||||
installed_version,
|
||||
status,
|
||||
fetched_sha,
|
||||
);
|
||||
|
||||
assert_eq!(newer_version.unwrap(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nightly_does_update_when_fetched_sha_is_not_same() {
|
||||
let release_channel = ReleaseChannel::Nightly;
|
||||
let app_commit_sha = Ok(Some("a".to_string()));
|
||||
let installed_version = SemanticVersion::new(1, 0, 0);
|
||||
let status = AutoUpdateStatus::Idle;
|
||||
let fetched_sha = "b".to_string();
|
||||
|
||||
let newer_version = AutoUpdater::check_for_newer_version(
|
||||
release_channel,
|
||||
app_commit_sha,
|
||||
installed_version,
|
||||
status,
|
||||
fetched_sha.clone(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
newer_version.unwrap(),
|
||||
Some(VersionCheckType::Sha(fetched_sha))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nightly_does_not_update_when_fetched_sha_is_same_as_cached() {
|
||||
let release_channel = ReleaseChannel::Nightly;
|
||||
let app_commit_sha = Ok(Some("a".to_string()));
|
||||
let installed_version = SemanticVersion::new(1, 0, 0);
|
||||
let status = AutoUpdateStatus::Updated {
|
||||
binary_path: PathBuf::new(),
|
||||
version: VersionCheckType::Sha("b".to_string()),
|
||||
};
|
||||
let fetched_sha = "b".to_string();
|
||||
|
||||
let newer_version = AutoUpdater::check_for_newer_version(
|
||||
release_channel,
|
||||
app_commit_sha,
|
||||
installed_version,
|
||||
status,
|
||||
fetched_sha,
|
||||
);
|
||||
|
||||
assert_eq!(newer_version.unwrap(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nightly_does_update_when_fetched_sha_is_not_same_as_cached() {
|
||||
let release_channel = ReleaseChannel::Nightly;
|
||||
let app_commit_sha = Ok(Some("a".to_string()));
|
||||
let installed_version = SemanticVersion::new(1, 0, 0);
|
||||
let status = AutoUpdateStatus::Updated {
|
||||
binary_path: PathBuf::new(),
|
||||
version: VersionCheckType::Sha("b".to_string()),
|
||||
};
|
||||
let fetched_sha = "c".to_string();
|
||||
|
||||
let newer_version = AutoUpdater::check_for_newer_version(
|
||||
release_channel,
|
||||
app_commit_sha,
|
||||
installed_version,
|
||||
status,
|
||||
fetched_sha.clone(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
newer_version.unwrap(),
|
||||
Some(VersionCheckType::Sha(fetched_sha))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nightly_does_update_when_installed_versions_sha_cannot_be_retrieved() {
|
||||
let release_channel = ReleaseChannel::Nightly;
|
||||
let app_commit_sha = Ok(None);
|
||||
let installed_version = SemanticVersion::new(1, 0, 0);
|
||||
let status = AutoUpdateStatus::Idle;
|
||||
let fetched_sha = "a".to_string();
|
||||
|
||||
let newer_version = AutoUpdater::check_for_newer_version(
|
||||
release_channel,
|
||||
app_commit_sha,
|
||||
installed_version,
|
||||
status,
|
||||
fetched_sha.clone(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
newer_version.unwrap(),
|
||||
Some(VersionCheckType::Sha(fetched_sha))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nightly_does_not_update_when_cached_update_is_same_as_fetched_and_installed_versions_sha_cannot_be_retrieved()
|
||||
{
|
||||
let release_channel = ReleaseChannel::Nightly;
|
||||
let app_commit_sha = Ok(None);
|
||||
let installed_version = SemanticVersion::new(1, 0, 0);
|
||||
let status = AutoUpdateStatus::Updated {
|
||||
binary_path: PathBuf::new(),
|
||||
version: VersionCheckType::Sha("b".to_string()),
|
||||
};
|
||||
let fetched_sha = "b".to_string();
|
||||
|
||||
let newer_version = AutoUpdater::check_for_newer_version(
|
||||
release_channel,
|
||||
app_commit_sha,
|
||||
installed_version,
|
||||
status,
|
||||
fetched_sha,
|
||||
);
|
||||
|
||||
assert_eq!(newer_version.unwrap(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nightly_does_update_when_cached_update_is_not_same_as_fetched_and_installed_versions_sha_cannot_be_retrieved()
|
||||
{
|
||||
let release_channel = ReleaseChannel::Nightly;
|
||||
let app_commit_sha = Ok(None);
|
||||
let installed_version = SemanticVersion::new(1, 0, 0);
|
||||
let status = AutoUpdateStatus::Updated {
|
||||
binary_path: PathBuf::new(),
|
||||
version: VersionCheckType::Sha("b".to_string()),
|
||||
};
|
||||
let fetched_sha = "c".to_string();
|
||||
|
||||
let newer_version = AutoUpdater::check_for_newer_version(
|
||||
release_channel,
|
||||
app_commit_sha,
|
||||
installed_version,
|
||||
status,
|
||||
fetched_sha.clone(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
newer_version.unwrap(),
|
||||
Some(VersionCheckType::Sha(fetched_sha))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
use anyhow::{Context as _, Result};
|
||||
use http_client::Url;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio_socks::tcp::{Socks4Stream, Socks5Stream};
|
||||
use tokio_socks::{
|
||||
IntoTargetAddr, TargetAddr,
|
||||
tcp::{Socks4Stream, Socks5Stream},
|
||||
};
|
||||
|
||||
use super::AsyncReadWrite;
|
||||
|
||||
@@ -23,8 +26,14 @@ pub(super) struct Socks5Authorization<'a> {
|
||||
/// V4 allows idenfication using a user_id
|
||||
/// V5 allows authorization using a username and password
|
||||
pub(super) enum SocksVersion<'a> {
|
||||
V4(Option<Socks4Identification<'a>>),
|
||||
V5(Option<Socks5Authorization<'a>>),
|
||||
V4 {
|
||||
local_dns: bool,
|
||||
identification: Option<Socks4Identification<'a>>,
|
||||
},
|
||||
V5 {
|
||||
local_dns: bool,
|
||||
authorization: Option<Socks5Authorization<'a>>,
|
||||
},
|
||||
}
|
||||
|
||||
pub(super) fn parse_socks_proxy<'t>(scheme: &str, proxy: &'t Url) -> SocksVersion<'t> {
|
||||
@@ -33,13 +42,19 @@ pub(super) fn parse_socks_proxy<'t>(scheme: &str, proxy: &'t Url) -> SocksVersio
|
||||
"" => None,
|
||||
username => Some(Socks4Identification { user_id: username }),
|
||||
};
|
||||
SocksVersion::V4(identification)
|
||||
SocksVersion::V4 {
|
||||
local_dns: scheme != "socks4a",
|
||||
identification,
|
||||
}
|
||||
} else {
|
||||
let authorization = proxy.password().map(|password| Socks5Authorization {
|
||||
username: proxy.username(),
|
||||
password,
|
||||
});
|
||||
SocksVersion::V5(authorization)
|
||||
SocksVersion::V5 {
|
||||
local_dns: scheme != "socks5h",
|
||||
authorization,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,26 +63,58 @@ pub(super) async fn connect_socks_proxy_stream(
|
||||
socks_version: SocksVersion<'_>,
|
||||
rpc_host: (&str, u16),
|
||||
) -> Result<Box<dyn AsyncReadWrite>> {
|
||||
let rpc_host = rpc_host
|
||||
.into_target_addr()
|
||||
.context("Failed to parse target addr")?;
|
||||
|
||||
let local_dns = match &socks_version {
|
||||
SocksVersion::V4 { local_dns, .. } => local_dns,
|
||||
SocksVersion::V5 { local_dns, .. } => local_dns,
|
||||
};
|
||||
let rpc_host = match (rpc_host, local_dns) {
|
||||
(TargetAddr::Domain(domain, port), true) => {
|
||||
let ip_addr = tokio::net::lookup_host((domain.as_ref(), port))
|
||||
.await
|
||||
.with_context(|| format!("Failed to lookup domain {}", domain))?
|
||||
.next()
|
||||
.ok_or_else(|| anyhow::anyhow!("Failed to lookup domain {}", domain))?;
|
||||
TargetAddr::Ip(ip_addr)
|
||||
}
|
||||
(rpc_host, _) => rpc_host,
|
||||
};
|
||||
|
||||
match socks_version {
|
||||
SocksVersion::V4(None) => {
|
||||
SocksVersion::V4 {
|
||||
identification: None,
|
||||
..
|
||||
} => {
|
||||
let socks = Socks4Stream::connect_with_socket(stream, rpc_host)
|
||||
.await
|
||||
.context("error connecting to socks")?;
|
||||
Ok(Box::new(socks))
|
||||
}
|
||||
SocksVersion::V4(Some(Socks4Identification { user_id })) => {
|
||||
SocksVersion::V4 {
|
||||
identification: Some(Socks4Identification { user_id }),
|
||||
..
|
||||
} => {
|
||||
let socks = Socks4Stream::connect_with_userid_and_socket(stream, rpc_host, user_id)
|
||||
.await
|
||||
.context("error connecting to socks")?;
|
||||
Ok(Box::new(socks))
|
||||
}
|
||||
SocksVersion::V5(None) => {
|
||||
SocksVersion::V5 {
|
||||
authorization: None,
|
||||
..
|
||||
} => {
|
||||
let socks = Socks5Stream::connect_with_socket(stream, rpc_host)
|
||||
.await
|
||||
.context("error connecting to socks")?;
|
||||
Ok(Box::new(socks))
|
||||
}
|
||||
SocksVersion::V5(Some(Socks5Authorization { username, password })) => {
|
||||
SocksVersion::V5 {
|
||||
authorization: Some(Socks5Authorization { username, password }),
|
||||
..
|
||||
} => {
|
||||
let socks = Socks5Stream::connect_with_password_and_socket(
|
||||
stream, rpc_host, username, password,
|
||||
)
|
||||
@@ -90,7 +137,13 @@ mod tests {
|
||||
let scheme = proxy.scheme();
|
||||
|
||||
let version = parse_socks_proxy(scheme, &proxy);
|
||||
assert!(matches!(version, SocksVersion::V4(None)))
|
||||
assert!(matches!(
|
||||
version,
|
||||
SocksVersion::V4 {
|
||||
local_dns: true,
|
||||
identification: None
|
||||
}
|
||||
))
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -101,7 +154,25 @@ mod tests {
|
||||
let version = parse_socks_proxy(scheme, &proxy);
|
||||
assert!(matches!(
|
||||
version,
|
||||
SocksVersion::V4(Some(Socks4Identification { user_id: "userid" }))
|
||||
SocksVersion::V4 {
|
||||
local_dns: true,
|
||||
identification: Some(Socks4Identification { user_id: "userid" })
|
||||
}
|
||||
))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_socks4_with_remote_dns() {
|
||||
let proxy = Url::parse("socks4a://proxy.example.com:1080").unwrap();
|
||||
let scheme = proxy.scheme();
|
||||
|
||||
let version = parse_socks_proxy(scheme, &proxy);
|
||||
assert!(matches!(
|
||||
version,
|
||||
SocksVersion::V4 {
|
||||
local_dns: false,
|
||||
identification: None
|
||||
}
|
||||
))
|
||||
}
|
||||
|
||||
@@ -111,7 +182,13 @@ mod tests {
|
||||
let scheme = proxy.scheme();
|
||||
|
||||
let version = parse_socks_proxy(scheme, &proxy);
|
||||
assert!(matches!(version, SocksVersion::V5(None)))
|
||||
assert!(matches!(
|
||||
version,
|
||||
SocksVersion::V5 {
|
||||
local_dns: true,
|
||||
authorization: None
|
||||
}
|
||||
))
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -122,10 +199,28 @@ mod tests {
|
||||
let version = parse_socks_proxy(scheme, &proxy);
|
||||
assert!(matches!(
|
||||
version,
|
||||
SocksVersion::V5(Some(Socks5Authorization {
|
||||
username: "username",
|
||||
password: "password"
|
||||
}))
|
||||
SocksVersion::V5 {
|
||||
local_dns: true,
|
||||
authorization: Some(Socks5Authorization {
|
||||
username: "username",
|
||||
password: "password"
|
||||
})
|
||||
}
|
||||
))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_socks5_with_remote_dns() {
|
||||
let proxy = Url::parse("socks5h://proxy.example.com:1080").unwrap();
|
||||
let scheme = proxy.scheme();
|
||||
|
||||
let version = parse_socks_proxy(scheme, &proxy);
|
||||
assert!(matches!(
|
||||
version,
|
||||
SocksVersion::V5 {
|
||||
local_dns: false,
|
||||
authorization: None
|
||||
}
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
drop table monthly_usages;
|
||||
drop table lifetime_usages;
|
||||
@@ -0,0 +1 @@
|
||||
drop table billing_events;
|
||||
@@ -27,11 +27,9 @@ use crate::db::billing_subscription::{
|
||||
StripeCancellationReason, StripeSubscriptionStatus, SubscriptionKind,
|
||||
};
|
||||
use crate::llm::db::subscription_usage_meter::CompletionMode;
|
||||
use crate::llm::{
|
||||
AGENT_EXTENDED_TRIAL_FEATURE_FLAG, DEFAULT_MAX_MONTHLY_SPEND, FREE_TIER_MONTHLY_SPENDING_LIMIT,
|
||||
};
|
||||
use crate::llm::{AGENT_EXTENDED_TRIAL_FEATURE_FLAG, DEFAULT_MAX_MONTHLY_SPEND};
|
||||
use crate::rpc::{ResultExt as _, Server};
|
||||
use crate::{AppState, Cents, Error, Result};
|
||||
use crate::{AppState, Error, Result};
|
||||
use crate::{db::UserId, llm::db::LlmDatabase};
|
||||
use crate::{
|
||||
db::{
|
||||
@@ -64,7 +62,6 @@ pub fn router() -> Router {
|
||||
"/billing/subscriptions/sync",
|
||||
post(sync_billing_subscription),
|
||||
)
|
||||
.route("/billing/monthly_spend", get(get_monthly_spend))
|
||||
.route("/billing/usage", get(get_current_usage))
|
||||
}
|
||||
|
||||
@@ -1223,54 +1220,6 @@ async fn handle_customer_subscription_event(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct GetMonthlySpendParams {
|
||||
github_user_id: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct GetMonthlySpendResponse {
|
||||
monthly_free_tier_spend_in_cents: u32,
|
||||
monthly_free_tier_allowance_in_cents: u32,
|
||||
monthly_spend_in_cents: u32,
|
||||
}
|
||||
|
||||
async fn get_monthly_spend(
|
||||
Extension(app): Extension<Arc<AppState>>,
|
||||
Query(params): Query<GetMonthlySpendParams>,
|
||||
) -> Result<Json<GetMonthlySpendResponse>> {
|
||||
let user = app
|
||||
.db
|
||||
.get_user_by_github_user_id(params.github_user_id)
|
||||
.await?
|
||||
.context("user not found")?;
|
||||
|
||||
let Some(llm_db) = app.llm_db.clone() else {
|
||||
return Err(Error::http(
|
||||
StatusCode::NOT_IMPLEMENTED,
|
||||
"LLM database not available".into(),
|
||||
));
|
||||
};
|
||||
|
||||
let free_tier = user
|
||||
.custom_llm_monthly_allowance_in_cents
|
||||
.map(|allowance| Cents(allowance as u32))
|
||||
.unwrap_or(FREE_TIER_MONTHLY_SPENDING_LIMIT);
|
||||
|
||||
let spending_for_month = llm_db
|
||||
.get_user_spending_for_month(user.id, Utc::now())
|
||||
.await?;
|
||||
|
||||
let free_tier_spend = Cents::min(spending_for_month, free_tier);
|
||||
let monthly_spend = spending_for_month.saturating_sub(free_tier);
|
||||
|
||||
Ok(Json(GetMonthlySpendResponse {
|
||||
monthly_free_tier_spend_in_cents: free_tier_spend.0,
|
||||
monthly_free_tier_allowance_in_cents: free_tier.0,
|
||||
monthly_spend_in_cents: monthly_spend.0,
|
||||
}))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct GetCurrentUsageParams {
|
||||
github_user_id: i32,
|
||||
@@ -1344,15 +1293,10 @@ async fn get_current_usage(
|
||||
.get_subscription_usage_for_period(user.id, period_start_at, period_end_at)
|
||||
.await?;
|
||||
|
||||
let plan = usage
|
||||
.as_ref()
|
||||
.map(|usage| usage.plan.into())
|
||||
.unwrap_or_else(|| {
|
||||
subscription
|
||||
.kind
|
||||
.map(Into::into)
|
||||
.unwrap_or(zed_llm_client::Plan::ZedFree)
|
||||
});
|
||||
let plan = subscription
|
||||
.kind
|
||||
.map(Into::into)
|
||||
.unwrap_or(zed_llm_client::Plan::ZedFree);
|
||||
|
||||
let model_requests_limit = match plan.model_requests_limit() {
|
||||
zed_llm_client::UsageLimit::Limited(limit) => {
|
||||
|
||||
@@ -7,10 +7,6 @@ pub use token::*;
|
||||
|
||||
pub const AGENT_EXTENDED_TRIAL_FEATURE_FLAG: &str = "agent-extended-trial";
|
||||
|
||||
/// The maximum monthly spending an individual user can reach on the free tier
|
||||
/// before they have to pay.
|
||||
pub const FREE_TIER_MONTHLY_SPENDING_LIMIT: Cents = Cents::from_dollars(10);
|
||||
|
||||
/// The default value to use for maximum spend per month if the user did not
|
||||
/// explicitly set a maximum spend.
|
||||
///
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
use crate::db::UserId;
|
||||
use crate::llm::Cents;
|
||||
use chrono::Datelike;
|
||||
use futures::StreamExt as _;
|
||||
use std::str::FromStr;
|
||||
use strum::IntoEnumIterator as _;
|
||||
|
||||
@@ -45,68 +41,4 @@ impl LlmDatabase {
|
||||
.collect();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_user_spending_for_month(
|
||||
&self,
|
||||
user_id: UserId,
|
||||
now: DateTimeUtc,
|
||||
) -> Result<Cents> {
|
||||
self.transaction(|tx| async move {
|
||||
let month = now.date_naive().month() as i32;
|
||||
let year = now.date_naive().year();
|
||||
|
||||
let mut monthly_usages = monthly_usage::Entity::find()
|
||||
.filter(
|
||||
monthly_usage::Column::UserId
|
||||
.eq(user_id)
|
||||
.and(monthly_usage::Column::Month.eq(month))
|
||||
.and(monthly_usage::Column::Year.eq(year)),
|
||||
)
|
||||
.stream(&*tx)
|
||||
.await?;
|
||||
let mut monthly_spending = Cents::ZERO;
|
||||
|
||||
while let Some(usage) = monthly_usages.next().await {
|
||||
let usage = usage?;
|
||||
let Ok(model) = self.model_by_id(usage.model_id) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
monthly_spending += calculate_spending(
|
||||
model,
|
||||
usage.input_tokens as usize,
|
||||
usage.cache_creation_input_tokens as usize,
|
||||
usage.cache_read_input_tokens as usize,
|
||||
usage.output_tokens as usize,
|
||||
);
|
||||
}
|
||||
|
||||
Ok(monthly_spending)
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
fn calculate_spending(
|
||||
model: &model::Model,
|
||||
input_tokens_this_month: usize,
|
||||
cache_creation_input_tokens_this_month: usize,
|
||||
cache_read_input_tokens_this_month: usize,
|
||||
output_tokens_this_month: usize,
|
||||
) -> Cents {
|
||||
let input_token_cost =
|
||||
input_tokens_this_month * model.price_per_million_input_tokens as usize / 1_000_000;
|
||||
let cache_creation_input_token_cost = cache_creation_input_tokens_this_month
|
||||
* model.price_per_million_cache_creation_input_tokens as usize
|
||||
/ 1_000_000;
|
||||
let cache_read_input_token_cost = cache_read_input_tokens_this_month
|
||||
* model.price_per_million_cache_read_input_tokens as usize
|
||||
/ 1_000_000;
|
||||
let output_token_cost =
|
||||
output_tokens_this_month * model.price_per_million_output_tokens as usize / 1_000_000;
|
||||
let spending = input_token_cost
|
||||
+ cache_creation_input_token_cost
|
||||
+ cache_read_input_token_cost
|
||||
+ output_token_cost;
|
||||
Cents::new(spending as u32)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
pub mod model;
|
||||
pub mod monthly_usage;
|
||||
pub mod provider;
|
||||
pub mod subscription_usage;
|
||||
pub mod subscription_usage_meter;
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
use crate::{db::UserId, llm::db::ModelId};
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "monthly_usages")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
pub user_id: UserId,
|
||||
pub model_id: ModelId,
|
||||
pub month: i32,
|
||||
pub year: i32,
|
||||
pub input_tokens: i64,
|
||||
pub cache_creation_input_tokens: i64,
|
||||
pub cache_read_input_tokens: i64,
|
||||
pub output_tokens: i64,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
@@ -581,6 +581,15 @@ async fn stream_completion(
|
||||
api_key: String,
|
||||
request: Request,
|
||||
) -> Result<BoxStream<'static, Result<ResponseEvent>>> {
|
||||
let is_vision_request = request.messages.last().map_or(false, |message| match message {
|
||||
ChatMessage::User { content }
|
||||
| ChatMessage::Assistant { content, .. }
|
||||
| ChatMessage::Tool { content, .. } => {
|
||||
matches!(content, ChatMessageContent::Multipart(parts) if parts.iter().any(|part| matches!(part, ChatMessagePart::Image { .. })))
|
||||
}
|
||||
_ => false,
|
||||
});
|
||||
|
||||
let request_builder = HttpRequest::builder()
|
||||
.method(Method::POST)
|
||||
.uri(COPILOT_CHAT_COMPLETION_URL)
|
||||
@@ -594,7 +603,7 @@ async fn stream_completion(
|
||||
.header("Authorization", format!("Bearer {}", api_key))
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Copilot-Integration-Id", "vscode-chat")
|
||||
.header("Copilot-Vision-Request", "true");
|
||||
.header("Copilot-Vision-Request", is_vision_request.to_string());
|
||||
|
||||
let is_streaming = request.stream;
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ use language::{LanguageName, LanguageToolchainStore};
|
||||
use node_runtime::NodeRuntime;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::WorktreeId;
|
||||
use smol::{self, fs::File};
|
||||
use smol::fs::File;
|
||||
use std::{
|
||||
borrow::Borrow,
|
||||
ffi::OsStr,
|
||||
@@ -23,6 +23,7 @@ use std::{
|
||||
sync::Arc,
|
||||
};
|
||||
use task::{AttachRequest, DebugRequest, DebugScenario, LaunchRequest, TcpArgumentsTemplate};
|
||||
use util::archive::extract_zip;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum DapStatus {
|
||||
@@ -358,17 +359,13 @@ pub async fn download_adapter_from_github(
|
||||
}
|
||||
DownloadedFileType::Zip | DownloadedFileType::Vsix => {
|
||||
let zip_path = version_path.with_extension("zip");
|
||||
|
||||
let mut file = File::create(&zip_path).await?;
|
||||
futures::io::copy(response.body_mut(), &mut file).await?;
|
||||
|
||||
// we cannot check the status as some adapter include files with names that trigger `Illegal byte sequence`
|
||||
util::command::new_smol_command("unzip")
|
||||
.arg(&zip_path)
|
||||
.arg("-d")
|
||||
.arg(&version_path)
|
||||
.output()
|
||||
.await?;
|
||||
let file = File::open(&zip_path).await?;
|
||||
extract_zip(&version_path, BufReader::new(file))
|
||||
.await
|
||||
// we cannot check the status as some adapter include files with names that trigger `Illegal byte sequence`
|
||||
.ok();
|
||||
|
||||
util::fs::remove_matching(&adapter_path, |entry| {
|
||||
entry
|
||||
|
||||
@@ -30,6 +30,7 @@ language.workspace = true
|
||||
paths.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
smol.workspace = true
|
||||
task.workspace = true
|
||||
util.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
@@ -136,6 +136,34 @@ impl DebugAdapter for CodeLldbDebugAdapter {
|
||||
};
|
||||
let adapter_dir = version_path.join("extension").join("adapter");
|
||||
let path = adapter_dir.join("codelldb").to_string_lossy().to_string();
|
||||
// todo("windows")
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
use smol::fs;
|
||||
|
||||
fs::set_permissions(
|
||||
&path,
|
||||
<fs::Permissions as fs::unix::PermissionsExt>::from_mode(0o755),
|
||||
)
|
||||
.await
|
||||
.with_context(|| format!("Settings executable permissions to {path:?}"))?;
|
||||
|
||||
let lldb_binaries_dir = version_path.join("extension").join("lldb").join("bin");
|
||||
let mut lldb_binaries =
|
||||
fs::read_dir(&lldb_binaries_dir).await.with_context(|| {
|
||||
format!("reading lldb binaries dir contents {lldb_binaries_dir:?}")
|
||||
})?;
|
||||
while let Some(binary) = lldb_binaries.next().await {
|
||||
let binary_entry = binary?;
|
||||
let path = binary_entry.path();
|
||||
fs::set_permissions(
|
||||
&path,
|
||||
<fs::Permissions as fs::unix::PermissionsExt>::from_mode(0o755),
|
||||
)
|
||||
.await
|
||||
.with_context(|| format!("Settings executable permissions to {path:?}"))?;
|
||||
}
|
||||
}
|
||||
self.path_to_codelldb.set(path.clone()).ok();
|
||||
command = Some(path);
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::{
|
||||
ClearAllBreakpoints, Continue, Detach, FocusBreakpointList, FocusConsole, FocusFrames,
|
||||
FocusLoadedSources, FocusModules, FocusTerminal, FocusVariables, Pause, Restart,
|
||||
ShowStackTrace, StepBack, StepInto, StepOut, StepOver, Stop, ToggleIgnoreBreakpoints,
|
||||
persistence,
|
||||
ToggleSessionPicker, ToggleThreadPicker, persistence,
|
||||
};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
@@ -31,7 +31,7 @@ use settings::Settings;
|
||||
use std::any::TypeId;
|
||||
use std::sync::Arc;
|
||||
use task::{DebugScenario, TaskContext};
|
||||
use ui::{ContextMenu, Divider, Tooltip, prelude::*};
|
||||
use ui::{ContextMenu, Divider, PopoverMenuHandle, Tooltip, prelude::*};
|
||||
use workspace::SplitDirection;
|
||||
use workspace::{
|
||||
Pane, Workspace,
|
||||
@@ -65,6 +65,8 @@ pub struct DebugPanel {
|
||||
workspace: WeakEntity<Workspace>,
|
||||
focus_handle: FocusHandle,
|
||||
context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
|
||||
pub(crate) thread_picker_menu_handle: PopoverMenuHandle<ContextMenu>,
|
||||
pub(crate) session_picker_menu_handle: PopoverMenuHandle<ContextMenu>,
|
||||
fs: Arc<dyn Fs>,
|
||||
}
|
||||
|
||||
@@ -77,6 +79,8 @@ impl DebugPanel {
|
||||
cx.new(|cx| {
|
||||
let project = workspace.project().clone();
|
||||
let focus_handle = cx.focus_handle();
|
||||
let thread_picker_menu_handle = PopoverMenuHandle::default();
|
||||
let session_picker_menu_handle = PopoverMenuHandle::default();
|
||||
|
||||
let debug_panel = Self {
|
||||
size: px(300.),
|
||||
@@ -87,6 +91,8 @@ impl DebugPanel {
|
||||
workspace: workspace.weak_handle(),
|
||||
context_menu: None,
|
||||
fs: workspace.app_state().fs.clone(),
|
||||
thread_picker_menu_handle,
|
||||
session_picker_menu_handle,
|
||||
};
|
||||
|
||||
debug_panel
|
||||
@@ -746,55 +752,6 @@ impl DebugPanel {
|
||||
}),
|
||||
)
|
||||
.child(Divider::vertical())
|
||||
.child(
|
||||
IconButton::new(
|
||||
"debug-enable-breakpoint",
|
||||
IconName::DebugDisabledBreakpoint,
|
||||
)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.disabled(thread_status != ThreadStatus::Stopped),
|
||||
)
|
||||
.child(
|
||||
IconButton::new(
|
||||
"debug-disable-breakpoint",
|
||||
IconName::CircleOff,
|
||||
)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.disabled(thread_status != ThreadStatus::Stopped),
|
||||
)
|
||||
.child(
|
||||
IconButton::new(
|
||||
"debug-disable-all-breakpoints",
|
||||
IconName::BugOff,
|
||||
)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.disabled(
|
||||
thread_status == ThreadStatus::Exited
|
||||
|| thread_status == ThreadStatus::Ended,
|
||||
)
|
||||
.on_click(window.listener_for(
|
||||
&running_state,
|
||||
|this, _, _window, cx| {
|
||||
this.toggle_ignore_breakpoints(cx);
|
||||
},
|
||||
))
|
||||
.tooltip({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Disable all breakpoints",
|
||||
&ToggleIgnoreBreakpoints,
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
}),
|
||||
)
|
||||
.child(Divider::vertical())
|
||||
.child(
|
||||
IconButton::new("debug-restart", IconName::DebugRestart)
|
||||
.icon_size(IconSize::XSmall)
|
||||
@@ -1033,6 +990,14 @@ impl DebugPanel {
|
||||
})
|
||||
.unwrap_or_else(|err| Task::ready(Err(err)))
|
||||
}
|
||||
|
||||
pub(crate) fn toggle_thread_picker(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.thread_picker_menu_handle.toggle(window, cx);
|
||||
}
|
||||
|
||||
pub(crate) fn toggle_session_picker(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.session_picker_menu_handle.toggle(window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<PanelEvent> for DebugPanel {}
|
||||
@@ -1249,6 +1214,24 @@ impl Render for DebugPanel {
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.on_action({
|
||||
let this = this.clone();
|
||||
move |_: &ToggleThreadPicker, window, cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
this.toggle_thread_picker(window, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.on_action({
|
||||
let this = this.clone();
|
||||
move |_: &ToggleSessionPicker, window, cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
this.toggle_session_picker(window, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.when(self.active_session.is_some(), |this| {
|
||||
this.on_mouse_down(
|
||||
MouseButton::Right,
|
||||
|
||||
@@ -45,6 +45,8 @@ actions!(
|
||||
FocusLoadedSources,
|
||||
FocusTerminal,
|
||||
ShowStackTrace,
|
||||
ToggleThreadPicker,
|
||||
ToggleSessionPicker,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -93,6 +95,17 @@ pub fn init(cx: &mut App) {
|
||||
}
|
||||
}
|
||||
})
|
||||
.register_action(|workspace, _: &Continue, _, cx| {
|
||||
if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
|
||||
if let Some(active_item) = debug_panel.read_with(cx, |panel, cx| {
|
||||
panel
|
||||
.active_session()
|
||||
.map(|session| session.read(cx).running_state().clone())
|
||||
}) {
|
||||
active_item.update(cx, |item, cx| item.continue_thread(cx))
|
||||
}
|
||||
}
|
||||
})
|
||||
.register_action(|workspace, _: &StepInto, _, cx| {
|
||||
if let Some(debug_panel) = workspace.panel::<DebugPanel>(cx) {
|
||||
if let Some(active_item) = debug_panel.read_with(cx, |panel, cx| {
|
||||
|
||||
@@ -132,7 +132,8 @@ impl DebugPanel {
|
||||
this
|
||||
}),
|
||||
)
|
||||
.style(DropdownStyle::Ghost),
|
||||
.style(DropdownStyle::Ghost)
|
||||
.handle(self.session_picker_menu_handle.clone()),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
@@ -163,7 +164,7 @@ impl DebugPanel {
|
||||
DropdownMenu::new_with_element(
|
||||
("thread-list", session_id.0),
|
||||
trigger,
|
||||
ContextMenu::build_eager(window, cx, move |mut this, _, _| {
|
||||
ContextMenu::build(window, cx, move |mut this, _, _| {
|
||||
for (thread, _) in threads {
|
||||
let running_state = running_state.clone();
|
||||
let thread_id = thread.id;
|
||||
@@ -177,7 +178,8 @@ impl DebugPanel {
|
||||
}),
|
||||
)
|
||||
.disabled(session_terminated)
|
||||
.style(DropdownStyle::Ghost),
|
||||
.style(DropdownStyle::Ghost)
|
||||
.handle(self.thread_picker_menu_handle.clone()),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
|
||||
@@ -96,7 +96,7 @@ impl Render for RunningState {
|
||||
.find(|pane| pane.read(cx).is_zoomed());
|
||||
|
||||
let active = self.panes.panes().into_iter().next();
|
||||
let x = if let Some(ref zoomed_pane) = zoomed_pane {
|
||||
let pane = if let Some(ref zoomed_pane) = zoomed_pane {
|
||||
zoomed_pane.update(cx, |pane, cx| pane.render(window, cx).into_any_element())
|
||||
} else if let Some(active) = active {
|
||||
self.panes
|
||||
@@ -122,7 +122,7 @@ impl Render for RunningState {
|
||||
.size_full()
|
||||
.key_context("DebugSessionItem")
|
||||
.track_focus(&self.focus_handle(cx))
|
||||
.child(h_flex().flex_1().child(x))
|
||||
.child(h_flex().flex_1().child(pane))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -628,10 +628,9 @@ impl RunningState {
|
||||
&workspace,
|
||||
&stack_frame_list,
|
||||
&variable_list,
|
||||
&module_list,
|
||||
&loaded_source_list,
|
||||
&console,
|
||||
&breakpoint_list,
|
||||
&debug_terminal,
|
||||
dock_axis,
|
||||
&mut pane_close_subscriptions,
|
||||
window,
|
||||
@@ -1468,10 +1467,9 @@ impl RunningState {
|
||||
workspace: &WeakEntity<Workspace>,
|
||||
stack_frame_list: &Entity<StackFrameList>,
|
||||
variable_list: &Entity<VariableList>,
|
||||
module_list: &Entity<ModuleList>,
|
||||
loaded_source_list: &Entity<LoadedSourceList>,
|
||||
console: &Entity<Console>,
|
||||
breakpoints: &Entity<BreakpointList>,
|
||||
debug_terminal: &Entity<DebugTerminal>,
|
||||
dock_axis: Axis,
|
||||
subscriptions: &mut HashMap<EntityId, Subscription>,
|
||||
window: &mut Window,
|
||||
@@ -1512,6 +1510,26 @@ impl RunningState {
|
||||
let center_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx);
|
||||
|
||||
center_pane.update(cx, |this, cx| {
|
||||
let weak_console = console.downgrade();
|
||||
this.add_item(
|
||||
Box::new(SubView::new(
|
||||
console.focus_handle(cx),
|
||||
console.clone().into(),
|
||||
DebuggerPaneItem::Console,
|
||||
Some(Box::new(move |cx| {
|
||||
weak_console
|
||||
.read_with(cx, |console, cx| console.show_indicator(cx))
|
||||
.unwrap_or_default()
|
||||
})),
|
||||
cx,
|
||||
)),
|
||||
true,
|
||||
false,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
this.add_item(
|
||||
Box::new(SubView::new(
|
||||
variable_list.focus_handle(cx),
|
||||
@@ -1526,54 +1544,20 @@ impl RunningState {
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
this.add_item(
|
||||
Box::new(SubView::new(
|
||||
module_list.focus_handle(cx),
|
||||
module_list.clone().into(),
|
||||
DebuggerPaneItem::Modules,
|
||||
None,
|
||||
cx,
|
||||
)),
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
this.add_item(
|
||||
Box::new(SubView::new(
|
||||
loaded_source_list.focus_handle(cx),
|
||||
loaded_source_list.clone().into(),
|
||||
DebuggerPaneItem::LoadedSources,
|
||||
None,
|
||||
cx,
|
||||
)),
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
this.activate_item(0, false, false, window, cx);
|
||||
});
|
||||
|
||||
let rightmost_pane = new_debugger_pane(workspace.clone(), project.clone(), window, cx);
|
||||
rightmost_pane.update(cx, |this, cx| {
|
||||
let weak_console = console.downgrade();
|
||||
this.add_item(
|
||||
Box::new(SubView::new(
|
||||
this.focus_handle(cx),
|
||||
console.clone().into(),
|
||||
DebuggerPaneItem::Console,
|
||||
Some(Box::new(move |cx| {
|
||||
weak_console
|
||||
.read_with(cx, |console, cx| console.show_indicator(cx))
|
||||
.unwrap_or_default()
|
||||
})),
|
||||
debug_terminal.focus_handle(cx),
|
||||
debug_terminal.clone().into(),
|
||||
DebuggerPaneItem::Terminal,
|
||||
None,
|
||||
cx,
|
||||
)),
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
window,
|
||||
|
||||
@@ -14,7 +14,7 @@ use language::{Buffer, CodeLabel, ToOffset};
|
||||
use menu::Confirm;
|
||||
use project::{
|
||||
Completion,
|
||||
debugger::session::{CompletionsQuery, OutputToken, Session},
|
||||
debugger::session::{CompletionsQuery, OutputToken, Session, SessionEvent},
|
||||
};
|
||||
use settings::Settings;
|
||||
use std::{cell::RefCell, rc::Rc, usize};
|
||||
@@ -79,6 +79,11 @@ impl Console {
|
||||
|
||||
let _subscriptions = vec![
|
||||
cx.subscribe(&stack_frame_list, Self::handle_stack_frame_list_events),
|
||||
cx.subscribe_in(&session, window, |this, _, event, window, cx| {
|
||||
if let SessionEvent::ConsoleOutput = event {
|
||||
this.update_output(window, cx)
|
||||
}
|
||||
}),
|
||||
cx.on_focus_in(&focus_handle, window, |console, window, cx| {
|
||||
if console.is_running(cx) {
|
||||
console.query_bar.focus_handle(cx).focus(window);
|
||||
@@ -200,12 +205,11 @@ impl Console {
|
||||
fn render_query_bar(&self, cx: &Context<Self>) -> impl IntoElement {
|
||||
EditorElement::new(&self.query_bar, self.editor_style(cx))
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Console {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
fn update_output(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let session = self.session.clone();
|
||||
let token = self.last_token;
|
||||
|
||||
self.update_output_task = cx.spawn_in(window, async move |this, cx| {
|
||||
_ = session.update_in(cx, move |session, window, cx| {
|
||||
let (output, last_processed_token) = session.output(token);
|
||||
@@ -220,7 +224,11 @@ impl Render for Console {
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Console {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.track_focus(&self.focus_handle)
|
||||
.key_context("DebugConsole")
|
||||
|
||||
@@ -154,12 +154,15 @@ impl VariableList {
|
||||
|
||||
let _subscriptions = vec![
|
||||
cx.subscribe(&stack_frame_list, Self::handle_stack_frame_list_events),
|
||||
cx.subscribe(&session, |this, _, event, _| match event {
|
||||
cx.subscribe(&session, |this, _, event, cx| match event {
|
||||
SessionEvent::Stopped(_) => {
|
||||
this.selection.take();
|
||||
this.edited_path.take();
|
||||
this.selected_stack_frame_id.take();
|
||||
}
|
||||
SessionEvent::Variables => {
|
||||
this.build_entries(cx);
|
||||
}
|
||||
_ => {}
|
||||
}),
|
||||
cx.on_focus_out(&focus_handle, window, |this, _, _, cx| {
|
||||
@@ -300,7 +303,7 @@ impl VariableList {
|
||||
match event {
|
||||
StackFrameListEvent::SelectedStackFrameChanged(stack_frame_id) => {
|
||||
self.selected_stack_frame_id = Some(*stack_frame_id);
|
||||
cx.notify();
|
||||
self.build_entries(cx);
|
||||
}
|
||||
StackFrameListEvent::BuiltEntries => {}
|
||||
}
|
||||
@@ -344,14 +347,14 @@ impl VariableList {
|
||||
};
|
||||
|
||||
entry.is_expanded = !entry.is_expanded;
|
||||
cx.notify();
|
||||
self.build_entries(cx);
|
||||
}
|
||||
|
||||
fn select_first(&mut self, _: &SelectFirst, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.cancel_variable_edit(&Default::default(), window, cx);
|
||||
if let Some(variable) = self.entries.first() {
|
||||
self.selection = Some(variable.path.clone());
|
||||
cx.notify();
|
||||
self.build_entries(cx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -359,7 +362,7 @@ impl VariableList {
|
||||
self.cancel_variable_edit(&Default::default(), window, cx);
|
||||
if let Some(variable) = self.entries.last() {
|
||||
self.selection = Some(variable.path.clone());
|
||||
cx.notify();
|
||||
self.build_entries(cx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -378,7 +381,7 @@ impl VariableList {
|
||||
index.and_then(|ix| self.entries.get(ix).map(|var| var.path.clone()))
|
||||
{
|
||||
self.selection = Some(new_selection);
|
||||
cx.notify();
|
||||
self.build_entries(cx);
|
||||
} else {
|
||||
self.select_last(&SelectLast, window, cx);
|
||||
}
|
||||
@@ -402,7 +405,7 @@ impl VariableList {
|
||||
index.and_then(|ix| self.entries.get(ix).map(|var| var.path.clone()))
|
||||
{
|
||||
self.selection = Some(new_selection);
|
||||
cx.notify();
|
||||
self.build_entries(cx);
|
||||
} else {
|
||||
self.select_first(&SelectFirst, window, cx);
|
||||
}
|
||||
@@ -464,7 +467,7 @@ impl VariableList {
|
||||
self.select_prev(&SelectPrevious, window, cx);
|
||||
} else {
|
||||
entry_state.is_expanded = false;
|
||||
cx.notify();
|
||||
self.build_entries(cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -485,7 +488,7 @@ impl VariableList {
|
||||
self.select_next(&SelectNext, window, cx);
|
||||
} else {
|
||||
entry_state.is_expanded = true;
|
||||
cx.notify();
|
||||
self.build_entries(cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -929,8 +932,6 @@ impl Focusable for VariableList {
|
||||
|
||||
impl Render for VariableList {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
self.build_entries(cx);
|
||||
|
||||
v_flex()
|
||||
.track_focus(&self.focus_handle)
|
||||
.key_context("VariableList")
|
||||
@@ -946,7 +947,6 @@ impl Render for VariableList {
|
||||
.on_action(cx.listener(Self::collapse_selected_entry))
|
||||
.on_action(cx.listener(Self::cancel_variable_edit))
|
||||
.on_action(cx.listener(Self::confirm_variable_edit))
|
||||
//
|
||||
.child(
|
||||
uniform_list(
|
||||
cx.entity().clone(),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::{
|
||||
debugger_panel::DebugPanel,
|
||||
persistence::DebuggerPaneItem,
|
||||
tests::{active_debug_session_panel, init_test, init_test_workspace, start_debug_session},
|
||||
};
|
||||
use dap::{
|
||||
@@ -110,7 +111,8 @@ async fn test_module_list(executor: BackgroundExecutor, cx: &mut TestAppContext)
|
||||
});
|
||||
|
||||
running_state.update_in(cx, |this, window, cx| {
|
||||
this.activate_item(crate::persistence::DebuggerPaneItem::Modules, window, cx);
|
||||
this.ensure_pane_item(DebuggerPaneItem::Modules, window, cx);
|
||||
this.activate_item(DebuggerPaneItem::Modules, window, cx);
|
||||
cx.refresh_windows();
|
||||
});
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ use std::sync::{
|
||||
|
||||
use crate::{
|
||||
DebugPanel,
|
||||
persistence::DebuggerPaneItem,
|
||||
session::running::variable_list::{CollapseSelectedEntry, ExpandSelectedEntry},
|
||||
tests::{active_debug_session_panel, init_test, init_test_workspace, start_debug_session},
|
||||
};
|
||||
@@ -706,7 +707,13 @@ async fn test_keyboard_navigation(executor: BackgroundExecutor, cx: &mut TestApp
|
||||
cx.focus_self(window);
|
||||
let running = item.running_state().clone();
|
||||
|
||||
let variable_list = running.read_with(cx, |state, _| state.variable_list().clone());
|
||||
let variable_list = running.update(cx, |state, cx| {
|
||||
// have to do this because the variable list pane should be shown/active
|
||||
// for testing keyboard navigation
|
||||
state.activate_item(DebuggerPaneItem::Variables, window, cx);
|
||||
|
||||
state.variable_list().clone()
|
||||
});
|
||||
variable_list.update(cx, |_, cx| cx.focus_self(window));
|
||||
running
|
||||
});
|
||||
|
||||
@@ -3964,15 +3964,18 @@ impl Editor {
|
||||
.skip(num_of_whitespaces)
|
||||
.take(max_len_of_delimiter)
|
||||
.collect::<String>();
|
||||
let (delimiter, trimmed_len) =
|
||||
delimiters.iter().find_map(|delimiter| {
|
||||
let trimmed = delimiter.trim_end();
|
||||
if comment_candidate.starts_with(trimmed) {
|
||||
Some((delimiter, trimmed.len()))
|
||||
let (delimiter, trimmed_len) = delimiters
|
||||
.iter()
|
||||
.filter_map(|delimiter| {
|
||||
let prefix = delimiter.trim_end();
|
||||
if comment_candidate.starts_with(prefix) {
|
||||
Some((delimiter, prefix.len()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})?;
|
||||
})
|
||||
.max_by_key(|(_, len)| *len)?;
|
||||
|
||||
let cursor_is_placed_after_comment_marker =
|
||||
num_of_whitespaces + trimmed_len <= start_point.column as usize;
|
||||
if cursor_is_placed_after_comment_marker {
|
||||
@@ -7263,24 +7266,22 @@ impl Editor {
|
||||
..Default::default()
|
||||
};
|
||||
let primary_action_text = if breakpoint.is_disabled() {
|
||||
"enable"
|
||||
"Enable breakpoint"
|
||||
} else if is_phantom && !collides_with_existing {
|
||||
"set"
|
||||
"Set breakpoint"
|
||||
} else {
|
||||
"unset"
|
||||
"Unset breakpoint"
|
||||
};
|
||||
let mut primary_text = format!("Click to {primary_action_text}");
|
||||
if collides_with_existing && !breakpoint.is_disabled() {
|
||||
use std::fmt::Write;
|
||||
write!(primary_text, ", {alt_as_text}-click to disable").ok();
|
||||
}
|
||||
let primary_text = SharedString::from(primary_text);
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
|
||||
let meta = if is_rejected {
|
||||
"No executable code is associated with this line."
|
||||
SharedString::from("No executable code is associated with this line.")
|
||||
} else if collides_with_existing && !breakpoint.is_disabled() {
|
||||
SharedString::from(format!(
|
||||
"{alt_as_text}-click to disable,\nright-click for more options."
|
||||
))
|
||||
} else {
|
||||
"Right-click for more options."
|
||||
SharedString::from("Right-click for more options.")
|
||||
};
|
||||
IconButton::new(("breakpoint_indicator", row.0 as usize), icon)
|
||||
.icon_size(IconSize::XSmall)
|
||||
@@ -7319,7 +7320,14 @@ impl Editor {
|
||||
);
|
||||
}))
|
||||
.tooltip(move |window, cx| {
|
||||
Tooltip::with_meta_in(primary_text.clone(), None, meta, &focus_handle, window, cx)
|
||||
Tooltip::with_meta_in(
|
||||
primary_action_text,
|
||||
Some(&ToggleBreakpoint),
|
||||
meta.clone(),
|
||||
&focus_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -20236,6 +20244,7 @@ impl SemanticsProvider for Entity<Project> {
|
||||
fn inline_values(
|
||||
&self,
|
||||
buffer_handle: Entity<Buffer>,
|
||||
|
||||
range: Range<text::Anchor>,
|
||||
cx: &mut App,
|
||||
) -> Option<Task<anyhow::Result<Vec<InlayHint>>>> {
|
||||
|
||||
@@ -2820,6 +2820,42 @@ async fn test_newline_comments(cx: &mut TestAppContext) {
|
||||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
|
||||
init_test(cx, |settings| {
|
||||
settings.defaults.tab_size = NonZeroU32::new(4)
|
||||
});
|
||||
|
||||
let language = Arc::new(Language::new(
|
||||
LanguageConfig {
|
||||
line_comments: vec!["// ".into(), "/// ".into()],
|
||||
..LanguageConfig::default()
|
||||
},
|
||||
None,
|
||||
));
|
||||
{
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
|
||||
cx.set_state(indoc! {"
|
||||
//ˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.assert_editor_state(indoc! {"
|
||||
//
|
||||
// ˇ
|
||||
"});
|
||||
|
||||
cx.set_state(indoc! {"
|
||||
///ˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.assert_editor_state(indoc! {"
|
||||
///
|
||||
/// ˇ
|
||||
"});
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
|
||||
init_test(cx, |settings| {
|
||||
@@ -5965,8 +6001,34 @@ async fn test_add_selection_above_below(cx: &mut TestAppContext) {
|
||||
cx.assert_editor_state(indoc!(
|
||||
r#"abc
|
||||
defˇghi
|
||||
|
||||
ˇ
|
||||
jk
|
||||
nlmo
|
||||
"#
|
||||
));
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.add_selection_below(&Default::default(), window, cx);
|
||||
});
|
||||
|
||||
cx.assert_editor_state(indoc!(
|
||||
r#"abc
|
||||
defˇghi
|
||||
ˇ
|
||||
jkˇ
|
||||
nlmo
|
||||
"#
|
||||
));
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.add_selection_below(&Default::default(), window, cx);
|
||||
});
|
||||
|
||||
cx.assert_editor_state(indoc!(
|
||||
r#"abc
|
||||
defˇghi
|
||||
ˇ
|
||||
jkˇ
|
||||
nlmˇo
|
||||
"#
|
||||
));
|
||||
@@ -5978,10 +6040,10 @@ async fn test_add_selection_above_below(cx: &mut TestAppContext) {
|
||||
cx.assert_editor_state(indoc!(
|
||||
r#"abc
|
||||
defˇghi
|
||||
|
||||
jk
|
||||
ˇ
|
||||
jkˇ
|
||||
nlmˇo
|
||||
"#
|
||||
ˇ"#
|
||||
));
|
||||
|
||||
// change selections
|
||||
|
||||
@@ -3597,7 +3597,7 @@ impl EditorElement {
|
||||
style: &EditorStyle,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
) -> Option<ContextMenuLayout> {
|
||||
let mut min_menu_height = Pixels::ZERO;
|
||||
let mut max_menu_height = Pixels::ZERO;
|
||||
let mut height_above_menu = Pixels::ZERO;
|
||||
@@ -3638,7 +3638,7 @@ impl EditorElement {
|
||||
|
||||
let visible = edit_prediction_popover_visible || context_menu_visible;
|
||||
if !visible {
|
||||
return;
|
||||
return None;
|
||||
}
|
||||
|
||||
let cursor_row_layout = &line_layouts[cursor.row().minus(start_row) as usize];
|
||||
@@ -3663,7 +3663,7 @@ impl EditorElement {
|
||||
|
||||
let min_height = height_above_menu + min_menu_height + height_below_menu;
|
||||
let max_height = height_above_menu + max_menu_height + height_below_menu;
|
||||
let Some((laid_out_popovers, y_flipped)) = self.layout_popovers_above_or_below_line(
|
||||
let (laid_out_popovers, y_flipped) = self.layout_popovers_above_or_below_line(
|
||||
target_position,
|
||||
line_height,
|
||||
min_height,
|
||||
@@ -3721,16 +3721,11 @@ impl EditorElement {
|
||||
.flatten()
|
||||
.collect::<Vec<_>>()
|
||||
},
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
)?;
|
||||
|
||||
let Some((menu_ix, (_, menu_bounds))) = laid_out_popovers
|
||||
let (menu_ix, (_, menu_bounds)) = laid_out_popovers
|
||||
.iter()
|
||||
.find_position(|(x, _)| matches!(x, CursorPopoverType::CodeContextMenu))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
.find_position(|(x, _)| matches!(x, CursorPopoverType::CodeContextMenu))?;
|
||||
let last_ix = laid_out_popovers.len() - 1;
|
||||
let menu_is_last = menu_ix == last_ix;
|
||||
let first_popover_bounds = laid_out_popovers[0].1;
|
||||
@@ -3771,7 +3766,7 @@ impl EditorElement {
|
||||
false
|
||||
};
|
||||
|
||||
self.layout_context_menu_aside(
|
||||
let aside_bounds = self.layout_context_menu_aside(
|
||||
y_flipped,
|
||||
*menu_bounds,
|
||||
target_bounds,
|
||||
@@ -3783,6 +3778,23 @@ impl EditorElement {
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
if let Some(menu_bounds) = laid_out_popovers.iter().find_map(|(popover_type, bounds)| {
|
||||
if matches!(popover_type, CursorPopoverType::CodeContextMenu) {
|
||||
Some(*bounds)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}) {
|
||||
let bounds = if let Some(aside_bounds) = aside_bounds {
|
||||
menu_bounds.union(&aside_bounds)
|
||||
} else {
|
||||
menu_bounds
|
||||
};
|
||||
return Some(ContextMenuLayout { y_flipped, bounds });
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn layout_gutter_menu(
|
||||
@@ -3988,7 +4000,7 @@ impl EditorElement {
|
||||
viewport_bounds: Bounds<Pixels>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
) -> Option<Bounds<Pixels>> {
|
||||
let available_within_viewport = target_bounds.space_within(&viewport_bounds);
|
||||
let positioned_aside = if available_within_viewport.right >= MENU_ASIDE_MIN_WIDTH
|
||||
&& !must_place_above_or_below
|
||||
@@ -3997,16 +4009,14 @@ impl EditorElement {
|
||||
available_within_viewport.right - px(1.),
|
||||
MENU_ASIDE_MAX_WIDTH,
|
||||
);
|
||||
let Some(mut aside) = self.render_context_menu_aside(
|
||||
let mut aside = self.render_context_menu_aside(
|
||||
size(max_width, max_height - POPOVER_Y_PADDING),
|
||||
window,
|
||||
cx,
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
aside.layout_as_root(AvailableSpace::min_size(), window, cx);
|
||||
)?;
|
||||
let size = aside.layout_as_root(AvailableSpace::min_size(), window, cx);
|
||||
let right_position = point(target_bounds.right(), menu_bounds.origin.y);
|
||||
Some((aside, right_position))
|
||||
Some((aside, right_position, size))
|
||||
} else {
|
||||
let max_size = size(
|
||||
// TODO(mgsloan): Once the menu is bounded by viewport width the bound on viewport
|
||||
@@ -4023,9 +4033,7 @@ impl EditorElement {
|
||||
),
|
||||
) - POPOVER_Y_PADDING,
|
||||
);
|
||||
let Some(mut aside) = self.render_context_menu_aside(max_size, window, cx) else {
|
||||
return;
|
||||
};
|
||||
let mut aside = self.render_context_menu_aside(max_size, window, cx)?;
|
||||
let actual_size = aside.layout_as_root(AvailableSpace::min_size(), window, cx);
|
||||
|
||||
let top_position = point(
|
||||
@@ -4059,13 +4067,17 @@ impl EditorElement {
|
||||
// Fallback: fit actual size in window.
|
||||
.or_else(|| fit_within(available_within_viewport, actual_size));
|
||||
|
||||
aside_position.map(|position| (aside, position))
|
||||
aside_position.map(|position| (aside, position, actual_size))
|
||||
};
|
||||
|
||||
// Skip drawing if it doesn't fit anywhere.
|
||||
if let Some((aside, position)) = positioned_aside {
|
||||
if let Some((aside, position, size)) = positioned_aside {
|
||||
let aside_bounds = Bounds::new(position, size);
|
||||
window.defer_draw(aside, position, 2);
|
||||
return Some(aside_bounds);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn render_context_menu(
|
||||
@@ -4174,13 +4186,13 @@ impl EditorElement {
|
||||
&self,
|
||||
snapshot: &EditorSnapshot,
|
||||
hitbox: &Hitbox,
|
||||
text_hitbox: &Hitbox,
|
||||
visible_display_row_range: Range<DisplayRow>,
|
||||
content_origin: gpui::Point<Pixels>,
|
||||
scroll_pixel_position: gpui::Point<Pixels>,
|
||||
line_layouts: &[LineWithInvisibles],
|
||||
line_height: Pixels,
|
||||
em_width: Pixels,
|
||||
context_menu_layout: Option<ContextMenuLayout>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
@@ -4224,21 +4236,24 @@ impl EditorElement {
|
||||
|
||||
let mut overall_height = Pixels::ZERO;
|
||||
let mut measured_hover_popovers = Vec::new();
|
||||
for mut hover_popover in hover_popovers {
|
||||
for (position, mut hover_popover) in hover_popovers.into_iter().with_position() {
|
||||
let size = hover_popover.layout_as_root(AvailableSpace::min_size(), window, cx);
|
||||
let horizontal_offset =
|
||||
(text_hitbox.top_right().x - POPOVER_RIGHT_OFFSET - (hovered_point.x + size.width))
|
||||
(hitbox.top_right().x - POPOVER_RIGHT_OFFSET - (hovered_point.x + size.width))
|
||||
.min(Pixels::ZERO);
|
||||
|
||||
overall_height += HOVER_POPOVER_GAP + size.height;
|
||||
|
||||
match position {
|
||||
itertools::Position::Middle | itertools::Position::Last => {
|
||||
overall_height += HOVER_POPOVER_GAP
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
overall_height += size.height;
|
||||
measured_hover_popovers.push(MeasuredHoverPopover {
|
||||
element: hover_popover,
|
||||
size,
|
||||
horizontal_offset,
|
||||
});
|
||||
}
|
||||
overall_height += HOVER_POPOVER_GAP;
|
||||
|
||||
fn draw_occluder(
|
||||
width: Pixels,
|
||||
@@ -4255,8 +4270,12 @@ impl EditorElement {
|
||||
window.defer_draw(occlusion, origin, 2);
|
||||
}
|
||||
|
||||
if hovered_point.y > overall_height {
|
||||
// There is enough space above. Render popovers above the hovered point
|
||||
fn place_popovers_above(
|
||||
hovered_point: gpui::Point<Pixels>,
|
||||
measured_hover_popovers: Vec<MeasuredHoverPopover>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let mut current_y = hovered_point.y;
|
||||
for (position, popover) in measured_hover_popovers.into_iter().with_position() {
|
||||
let size = popover.size;
|
||||
@@ -4273,8 +4292,15 @@ impl EditorElement {
|
||||
|
||||
current_y = popover_origin.y - HOVER_POPOVER_GAP;
|
||||
}
|
||||
} else {
|
||||
// There is not enough space above. Render popovers below the hovered point
|
||||
}
|
||||
|
||||
fn place_popovers_below(
|
||||
hovered_point: gpui::Point<Pixels>,
|
||||
measured_hover_popovers: Vec<MeasuredHoverPopover>,
|
||||
line_height: Pixels,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let mut current_y = hovered_point.y + line_height;
|
||||
for (position, popover) in measured_hover_popovers.into_iter().with_position() {
|
||||
let size = popover.size;
|
||||
@@ -4289,6 +4315,123 @@ impl EditorElement {
|
||||
current_y = popover_origin.y + size.height + HOVER_POPOVER_GAP;
|
||||
}
|
||||
}
|
||||
|
||||
let intersects_menu = |bounds: Bounds<Pixels>| -> bool {
|
||||
context_menu_layout
|
||||
.as_ref()
|
||||
.map_or(false, |menu| bounds.intersects(&menu.bounds))
|
||||
};
|
||||
|
||||
let can_place_above = {
|
||||
let mut bounds_above = Vec::new();
|
||||
let mut current_y = hovered_point.y;
|
||||
for popover in &measured_hover_popovers {
|
||||
let size = popover.size;
|
||||
let popover_origin = point(
|
||||
hovered_point.x + popover.horizontal_offset,
|
||||
current_y - size.height,
|
||||
);
|
||||
bounds_above.push(Bounds::new(popover_origin, size));
|
||||
current_y = popover_origin.y - HOVER_POPOVER_GAP;
|
||||
}
|
||||
bounds_above
|
||||
.iter()
|
||||
.all(|b| b.is_contained_within(hitbox) && !intersects_menu(*b))
|
||||
};
|
||||
|
||||
let can_place_below = || {
|
||||
let mut bounds_below = Vec::new();
|
||||
let mut current_y = hovered_point.y + line_height;
|
||||
for popover in &measured_hover_popovers {
|
||||
let size = popover.size;
|
||||
let popover_origin = point(hovered_point.x + popover.horizontal_offset, current_y);
|
||||
bounds_below.push(Bounds::new(popover_origin, size));
|
||||
current_y = popover_origin.y + size.height + HOVER_POPOVER_GAP;
|
||||
}
|
||||
bounds_below
|
||||
.iter()
|
||||
.all(|b| b.is_contained_within(hitbox) && !intersects_menu(*b))
|
||||
};
|
||||
|
||||
if can_place_above {
|
||||
// try placing above hovered point
|
||||
place_popovers_above(hovered_point, measured_hover_popovers, window, cx);
|
||||
} else if can_place_below() {
|
||||
// try placing below hovered point
|
||||
place_popovers_below(
|
||||
hovered_point,
|
||||
measured_hover_popovers,
|
||||
line_height,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
} else {
|
||||
// try to place popovers around the context menu
|
||||
let origin_surrounding_menu = context_menu_layout.as_ref().and_then(|menu| {
|
||||
let total_width = measured_hover_popovers
|
||||
.iter()
|
||||
.map(|p| p.size.width)
|
||||
.max()
|
||||
.unwrap_or(Pixels::ZERO);
|
||||
let y_for_horizontal_positioning = if menu.y_flipped {
|
||||
menu.bounds.bottom() - overall_height
|
||||
} else {
|
||||
menu.bounds.top()
|
||||
};
|
||||
let possible_origins = vec![
|
||||
// left of context menu
|
||||
point(
|
||||
menu.bounds.left() - total_width - HOVER_POPOVER_GAP,
|
||||
y_for_horizontal_positioning,
|
||||
),
|
||||
// right of context menu
|
||||
point(
|
||||
menu.bounds.right() + HOVER_POPOVER_GAP,
|
||||
y_for_horizontal_positioning,
|
||||
),
|
||||
// top of context menu
|
||||
point(
|
||||
menu.bounds.left(),
|
||||
menu.bounds.top() - overall_height - HOVER_POPOVER_GAP,
|
||||
),
|
||||
// bottom of context menu
|
||||
point(menu.bounds.left(), menu.bounds.bottom() + HOVER_POPOVER_GAP),
|
||||
];
|
||||
possible_origins.into_iter().find(|&origin| {
|
||||
Bounds::new(origin, size(total_width, overall_height))
|
||||
.is_contained_within(hitbox)
|
||||
})
|
||||
});
|
||||
if let Some(origin) = origin_surrounding_menu {
|
||||
let mut current_y = origin.y;
|
||||
for (position, popover) in measured_hover_popovers.into_iter().with_position() {
|
||||
let size = popover.size;
|
||||
let popover_origin = point(origin.x, current_y);
|
||||
|
||||
window.defer_draw(popover.element, popover_origin, 2);
|
||||
if position != itertools::Position::Last {
|
||||
let origin = point(popover_origin.x, popover_origin.y + size.height);
|
||||
draw_occluder(size.width, origin, window, cx);
|
||||
}
|
||||
|
||||
current_y = popover_origin.y + size.height + HOVER_POPOVER_GAP;
|
||||
}
|
||||
} else {
|
||||
// fallback to existing above/below cursor logic
|
||||
// this might overlap menu or overflow in rare case
|
||||
if can_place_above {
|
||||
place_popovers_above(hovered_point, measured_hover_popovers, window, cx);
|
||||
} else {
|
||||
place_popovers_below(
|
||||
hovered_point,
|
||||
measured_hover_popovers,
|
||||
line_height,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn layout_diff_hunk_controls(
|
||||
@@ -4395,7 +4538,6 @@ impl EditorElement {
|
||||
fn layout_signature_help(
|
||||
&self,
|
||||
hitbox: &Hitbox,
|
||||
text_hitbox: &Hitbox,
|
||||
content_origin: gpui::Point<Pixels>,
|
||||
scroll_pixel_position: gpui::Point<Pixels>,
|
||||
newest_selection_head: Option<DisplayPoint>,
|
||||
@@ -4403,6 +4545,7 @@ impl EditorElement {
|
||||
line_layouts: &[LineWithInvisibles],
|
||||
line_height: Pixels,
|
||||
em_width: Pixels,
|
||||
context_menu_layout: Option<ContextMenuLayout>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
@@ -4448,22 +4591,82 @@ impl EditorElement {
|
||||
let target_point = content_origin + point(target_x, target_y);
|
||||
|
||||
let actual_size = element.layout_as_root(Size::<AvailableSpace>::default(), window, cx);
|
||||
let overall_height = actual_size.height + HOVER_POPOVER_GAP;
|
||||
|
||||
let popover_origin = if target_point.y > overall_height {
|
||||
point(target_point.x, target_point.y - actual_size.height)
|
||||
} else {
|
||||
point(
|
||||
target_point.x,
|
||||
target_point.y + line_height + HOVER_POPOVER_GAP,
|
||||
let (popover_bounds_above, popover_bounds_below) = {
|
||||
let horizontal_offset = (hitbox.top_right().x
|
||||
- POPOVER_RIGHT_OFFSET
|
||||
- (target_point.x + actual_size.width))
|
||||
.min(Pixels::ZERO);
|
||||
let initial_x = target_point.x + horizontal_offset;
|
||||
(
|
||||
Bounds::new(
|
||||
point(initial_x, target_point.y - actual_size.height),
|
||||
actual_size,
|
||||
),
|
||||
Bounds::new(
|
||||
point(initial_x, target_point.y + line_height + HOVER_POPOVER_GAP),
|
||||
actual_size,
|
||||
),
|
||||
)
|
||||
};
|
||||
|
||||
let horizontal_offset = (text_hitbox.top_right().x
|
||||
- POPOVER_RIGHT_OFFSET
|
||||
- (popover_origin.x + actual_size.width))
|
||||
.min(Pixels::ZERO);
|
||||
let final_origin = point(popover_origin.x + horizontal_offset, popover_origin.y);
|
||||
let intersects_menu = |bounds: Bounds<Pixels>| -> bool {
|
||||
context_menu_layout
|
||||
.as_ref()
|
||||
.map_or(false, |menu| bounds.intersects(&menu.bounds))
|
||||
};
|
||||
|
||||
let final_origin = if popover_bounds_above.is_contained_within(hitbox)
|
||||
&& !intersects_menu(popover_bounds_above)
|
||||
{
|
||||
// try placing above cursor
|
||||
popover_bounds_above.origin
|
||||
} else if popover_bounds_below.is_contained_within(hitbox)
|
||||
&& !intersects_menu(popover_bounds_below)
|
||||
{
|
||||
// try placing below cursor
|
||||
popover_bounds_below.origin
|
||||
} else {
|
||||
// try surrounding context menu if exists
|
||||
let origin_surrounding_menu = context_menu_layout.as_ref().and_then(|menu| {
|
||||
let y_for_horizontal_positioning = if menu.y_flipped {
|
||||
menu.bounds.bottom() - actual_size.height
|
||||
} else {
|
||||
menu.bounds.top()
|
||||
};
|
||||
let possible_origins = vec![
|
||||
// left of context menu
|
||||
point(
|
||||
menu.bounds.left() - actual_size.width - HOVER_POPOVER_GAP,
|
||||
y_for_horizontal_positioning,
|
||||
),
|
||||
// right of context menu
|
||||
point(
|
||||
menu.bounds.right() + HOVER_POPOVER_GAP,
|
||||
y_for_horizontal_positioning,
|
||||
),
|
||||
// top of context menu
|
||||
point(
|
||||
menu.bounds.left(),
|
||||
menu.bounds.top() - actual_size.height - HOVER_POPOVER_GAP,
|
||||
),
|
||||
// bottom of context menu
|
||||
point(menu.bounds.left(), menu.bounds.bottom() + HOVER_POPOVER_GAP),
|
||||
];
|
||||
possible_origins
|
||||
.into_iter()
|
||||
.find(|&origin| Bounds::new(origin, actual_size).is_contained_within(hitbox))
|
||||
});
|
||||
origin_surrounding_menu.unwrap_or_else(|| {
|
||||
// fallback to existing above/below cursor logic
|
||||
// this might overlap menu or overflow in rare case
|
||||
if popover_bounds_above.is_contained_within(hitbox) {
|
||||
popover_bounds_above.origin
|
||||
} else {
|
||||
popover_bounds_below.origin
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
window.defer_draw(element, final_origin, 2);
|
||||
}
|
||||
@@ -7884,27 +8087,31 @@ impl Element for EditorElement {
|
||||
|
||||
let gutter_settings = EditorSettings::get_global(cx).gutter;
|
||||
|
||||
if let Some(newest_selection_head) = newest_selection_head {
|
||||
let newest_selection_point =
|
||||
newest_selection_head.to_point(&snapshot.display_snapshot);
|
||||
|
||||
if (start_row..end_row).contains(&newest_selection_head.row()) {
|
||||
self.layout_cursor_popovers(
|
||||
line_height,
|
||||
&text_hitbox,
|
||||
content_origin,
|
||||
right_margin,
|
||||
start_row,
|
||||
scroll_pixel_position,
|
||||
&line_layouts,
|
||||
newest_selection_head,
|
||||
newest_selection_point,
|
||||
&style,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
let context_menu_layout =
|
||||
if let Some(newest_selection_head) = newest_selection_head {
|
||||
let newest_selection_point =
|
||||
newest_selection_head.to_point(&snapshot.display_snapshot);
|
||||
if (start_row..end_row).contains(&newest_selection_head.row()) {
|
||||
self.layout_cursor_popovers(
|
||||
line_height,
|
||||
&text_hitbox,
|
||||
content_origin,
|
||||
right_margin,
|
||||
start_row,
|
||||
scroll_pixel_position,
|
||||
&line_layouts,
|
||||
newest_selection_head,
|
||||
newest_selection_point,
|
||||
&style,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
self.layout_gutter_menu(
|
||||
line_height,
|
||||
@@ -7958,7 +8165,6 @@ impl Element for EditorElement {
|
||||
|
||||
self.layout_signature_help(
|
||||
&hitbox,
|
||||
&text_hitbox,
|
||||
content_origin,
|
||||
scroll_pixel_position,
|
||||
newest_selection_head,
|
||||
@@ -7966,6 +8172,7 @@ impl Element for EditorElement {
|
||||
&line_layouts,
|
||||
line_height,
|
||||
em_width,
|
||||
context_menu_layout,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
@@ -7974,13 +8181,13 @@ impl Element for EditorElement {
|
||||
self.layout_hover_popovers(
|
||||
&snapshot,
|
||||
&hitbox,
|
||||
&text_hitbox,
|
||||
start_row..end_row,
|
||||
content_origin,
|
||||
scroll_pixel_position,
|
||||
&line_layouts,
|
||||
line_height,
|
||||
em_width,
|
||||
context_menu_layout,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
@@ -8212,6 +8419,12 @@ pub(super) fn gutter_bounds(
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct ContextMenuLayout {
|
||||
y_flipped: bool,
|
||||
bounds: Bounds<Pixels>,
|
||||
}
|
||||
|
||||
/// Holds information required for layouting the editor scrollbars.
|
||||
struct ScrollbarLayoutInformation {
|
||||
/// The bounds of the editor area (excluding the content offset).
|
||||
|
||||
@@ -352,28 +352,32 @@ impl SelectionsCollection {
|
||||
) -> Option<Selection<Point>> {
|
||||
let is_empty = positions.start == positions.end;
|
||||
let line_len = display_map.line_len(row);
|
||||
|
||||
let line = display_map.layout_row(row, text_layout_details);
|
||||
|
||||
let start_col = line.closest_index_for_x(positions.start) as u32;
|
||||
if start_col < line_len || (is_empty && positions.start == line.width) {
|
||||
|
||||
let (start, end) = if is_empty {
|
||||
let point = DisplayPoint::new(row, std::cmp::min(start_col, line_len));
|
||||
(point, point)
|
||||
} else {
|
||||
if start_col >= line_len {
|
||||
return None;
|
||||
}
|
||||
let start = DisplayPoint::new(row, start_col);
|
||||
let end_col = line.closest_index_for_x(positions.end) as u32;
|
||||
let end = DisplayPoint::new(row, end_col);
|
||||
(start, end)
|
||||
};
|
||||
|
||||
Some(Selection {
|
||||
id: post_inc(&mut self.next_selection_id),
|
||||
start: start.to_point(display_map),
|
||||
end: end.to_point(display_map),
|
||||
reversed,
|
||||
goal: SelectionGoal::HorizontalRange {
|
||||
start: positions.start.into(),
|
||||
end: positions.end.into(),
|
||||
},
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
Some(Selection {
|
||||
id: post_inc(&mut self.next_selection_id),
|
||||
start: start.to_point(display_map),
|
||||
end: end.to_point(display_map),
|
||||
reversed,
|
||||
goal: SelectionGoal::HorizontalRange {
|
||||
start: positions.start.into(),
|
||||
end: positions.end.into(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
pub fn change_with<R>(
|
||||
|
||||
@@ -61,6 +61,7 @@ settings.workspace = true
|
||||
shellexpand.workspace = true
|
||||
smol.workspace = true
|
||||
telemetry.workspace = true
|
||||
terminal_view.workspace = true
|
||||
toml.workspace = true
|
||||
unindent.workspace = true
|
||||
util.workspace = true
|
||||
|
||||
@@ -424,6 +424,7 @@ pub fn init(cx: &mut App) -> Arc<AgentAppState> {
|
||||
language_models::init(user_store.clone(), client.clone(), fs.clone(), cx);
|
||||
languages::init(languages.clone(), node_runtime.clone(), cx);
|
||||
prompt_store::init(cx);
|
||||
terminal_view::init(cx);
|
||||
let stdout_is_a_pty = false;
|
||||
let prompt_builder = PromptBuilder::load(fs.clone(), stdout_is_a_pty, cx);
|
||||
agent::init(
|
||||
|
||||
@@ -31,6 +31,7 @@ http_client.workspace = true
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
lsp.workspace = true
|
||||
moka.workspace = true
|
||||
node_runtime.workspace = true
|
||||
paths.workspace = true
|
||||
project.workspace = true
|
||||
|
||||
@@ -22,15 +22,18 @@ use gpui::{App, AsyncApp, BackgroundExecutor, Task};
|
||||
use http_client::HttpClient;
|
||||
use language::LanguageName;
|
||||
use lsp::LanguageServerName;
|
||||
use moka::sync::Cache;
|
||||
use node_runtime::NodeRuntime;
|
||||
use release_channel::ReleaseChannel;
|
||||
use semantic_version::SemanticVersion;
|
||||
use std::borrow::Cow;
|
||||
use std::sync::LazyLock;
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::{Arc, OnceLock},
|
||||
sync::Arc,
|
||||
};
|
||||
use wasmtime::{
|
||||
Engine, Store,
|
||||
CacheStore, Engine, Store,
|
||||
component::{Component, ResourceTable},
|
||||
};
|
||||
use wasmtime_wasi::{self as wasi, WasiView};
|
||||
@@ -411,16 +414,23 @@ type ExtensionCall = Box<
|
||||
>;
|
||||
|
||||
fn wasm_engine() -> wasmtime::Engine {
|
||||
static WASM_ENGINE: OnceLock<wasmtime::Engine> = OnceLock::new();
|
||||
static WASM_ENGINE: LazyLock<wasmtime::Engine> = LazyLock::new(|| {
|
||||
let mut config = wasmtime::Config::new();
|
||||
config.wasm_component_model(true);
|
||||
config.async_support(true);
|
||||
config
|
||||
.enable_incremental_compilation(cache_store())
|
||||
.unwrap();
|
||||
wasmtime::Engine::new(&config).unwrap()
|
||||
});
|
||||
|
||||
WASM_ENGINE
|
||||
.get_or_init(|| {
|
||||
let mut config = wasmtime::Config::new();
|
||||
config.wasm_component_model(true);
|
||||
config.async_support(true);
|
||||
wasmtime::Engine::new(&config).unwrap()
|
||||
})
|
||||
.clone()
|
||||
WASM_ENGINE.clone()
|
||||
}
|
||||
|
||||
fn cache_store() -> Arc<IncrementalCompilationCache> {
|
||||
static CACHE_STORE: LazyLock<Arc<IncrementalCompilationCache>> =
|
||||
LazyLock::new(|| Arc::new(IncrementalCompilationCache::new()));
|
||||
CACHE_STORE.clone()
|
||||
}
|
||||
|
||||
impl WasmHost {
|
||||
@@ -667,3 +677,133 @@ impl wasi::WasiView for WasmState {
|
||||
&mut self.ctx
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper around a mini-moka bounded cache for storing incremental compilation artifacts.
|
||||
/// Since wasm modules have many similar elements, this can save us a lot of work at the
|
||||
/// cost of a small memory footprint. However, we don't want this to be unbounded, so we use
|
||||
/// a LFU/LRU cache to evict less used cache entries.
|
||||
#[derive(Debug)]
|
||||
struct IncrementalCompilationCache {
|
||||
cache: Cache<Vec<u8>, Vec<u8>>,
|
||||
}
|
||||
|
||||
impl IncrementalCompilationCache {
|
||||
fn new() -> Self {
|
||||
let cache = Cache::builder()
|
||||
// Cap this at 32 MB for now. Our extensions turn into roughly 512kb in the cache,
|
||||
// which means we could store 64 completely novel extensions in the cache, but in
|
||||
// practice we will more than that, which is more than enough for our use case.
|
||||
.max_capacity(32 * 1024 * 1024)
|
||||
.weigher(|k: &Vec<u8>, v: &Vec<u8>| (k.len() + v.len()).try_into().unwrap_or(u32::MAX))
|
||||
.build();
|
||||
Self { cache }
|
||||
}
|
||||
}
|
||||
|
||||
impl CacheStore for IncrementalCompilationCache {
|
||||
fn get(&self, key: &[u8]) -> Option<Cow<[u8]>> {
|
||||
self.cache.get(key).map(|v| v.into())
|
||||
}
|
||||
|
||||
fn insert(&self, key: &[u8], value: Vec<u8>) -> bool {
|
||||
self.cache.insert(key.to_vec(), value);
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use extension::{
|
||||
ExtensionCapability, ExtensionLibraryKind, LanguageServerManifestEntry, LibManifestEntry,
|
||||
SchemaVersion,
|
||||
extension_builder::{CompileExtensionOptions, ExtensionBuilder},
|
||||
};
|
||||
use gpui::TestAppContext;
|
||||
use reqwest_client::ReqwestClient;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[gpui::test]
|
||||
fn test_cache_size_for_test_extension(cx: &TestAppContext) {
|
||||
let cache_store = cache_store();
|
||||
let engine = wasm_engine();
|
||||
let wasm_bytes = wasm_bytes(cx, &mut manifest());
|
||||
|
||||
Component::new(&engine, wasm_bytes).unwrap();
|
||||
|
||||
cache_store.cache.run_pending_tasks();
|
||||
let size: usize = cache_store
|
||||
.cache
|
||||
.iter()
|
||||
.map(|(k, v)| k.len() + v.len())
|
||||
.sum();
|
||||
// If this assertion fails, it means extensions got larger and we may want to
|
||||
// reconsider our cache size.
|
||||
assert!(size < 512 * 1024);
|
||||
}
|
||||
|
||||
fn wasm_bytes(cx: &TestAppContext, manifest: &mut ExtensionManifest) -> Vec<u8> {
|
||||
let extension_builder = extension_builder();
|
||||
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.parent()
|
||||
.unwrap()
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join("extensions/test-extension");
|
||||
cx.executor()
|
||||
.block(extension_builder.compile_extension(
|
||||
&path,
|
||||
manifest,
|
||||
CompileExtensionOptions { release: true },
|
||||
))
|
||||
.unwrap();
|
||||
std::fs::read(path.join("extension.wasm")).unwrap()
|
||||
}
|
||||
|
||||
fn extension_builder() -> ExtensionBuilder {
|
||||
let user_agent = format!(
|
||||
"Zed Extension CLI/{} ({}; {})",
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
std::env::consts::OS,
|
||||
std::env::consts::ARCH
|
||||
);
|
||||
let http_client = Arc::new(ReqwestClient::user_agent(&user_agent).unwrap());
|
||||
// Local dir so that we don't have to download it on every run
|
||||
let build_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("benches/.build");
|
||||
ExtensionBuilder::new(http_client, build_dir)
|
||||
}
|
||||
|
||||
fn manifest() -> ExtensionManifest {
|
||||
ExtensionManifest {
|
||||
id: "test-extension".into(),
|
||||
name: "Test Extension".into(),
|
||||
version: "0.1.0".into(),
|
||||
schema_version: SchemaVersion(1),
|
||||
description: Some("An extension for use in tests.".into()),
|
||||
authors: Vec::new(),
|
||||
repository: None,
|
||||
themes: Default::default(),
|
||||
icon_themes: Vec::new(),
|
||||
lib: LibManifestEntry {
|
||||
kind: Some(ExtensionLibraryKind::Rust),
|
||||
version: Some(SemanticVersion::new(0, 1, 0)),
|
||||
},
|
||||
languages: Vec::new(),
|
||||
grammars: BTreeMap::default(),
|
||||
language_servers: [("gleam".into(), LanguageServerManifestEntry::default())]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
context_servers: BTreeMap::default(),
|
||||
slash_commands: BTreeMap::default(),
|
||||
indexed_docs_providers: BTreeMap::default(),
|
||||
snippets: None,
|
||||
capabilities: vec![ExtensionCapability::ProcessExec {
|
||||
command: "echo".into(),
|
||||
args: vec!["hello!".into()],
|
||||
}],
|
||||
debug_adapters: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::{Arc, OnceLock},
|
||||
};
|
||||
use util::archive::extract_zip;
|
||||
use util::maybe;
|
||||
use wasmtime::component::{Linker, Resource};
|
||||
|
||||
@@ -543,9 +544,9 @@ impl ExtensionImports for WasmState {
|
||||
}
|
||||
DownloadedFileType::Zip => {
|
||||
futures::pin_mut!(body);
|
||||
node_runtime::extract_zip(&destination_path, body)
|
||||
extract_zip(&destination_path, body)
|
||||
.await
|
||||
.with_context(|| format!("failed to unzip {} archive", path.display()))?;
|
||||
.with_context(|| format!("unzipping {path:?} archive"))?;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::{Arc, OnceLock},
|
||||
};
|
||||
use util::maybe;
|
||||
use util::{archive::extract_zip, maybe};
|
||||
use wasmtime::component::{Linker, Resource};
|
||||
|
||||
pub const MIN_VERSION: SemanticVersion = SemanticVersion::new(0, 6, 0);
|
||||
@@ -906,9 +906,9 @@ impl ExtensionImports for WasmState {
|
||||
}
|
||||
DownloadedFileType::Zip => {
|
||||
futures::pin_mut!(body);
|
||||
node_runtime::extract_zip(&destination_path, body)
|
||||
extract_zip(&destination_path, body)
|
||||
.await
|
||||
.with_context(|| format!("failed to unzip {} archive", path.display()))?;
|
||||
.with_context(|| format!("unzipping {path:?} archive"))?;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
use crate::file_finder_settings::FileFinderSettings;
|
||||
use file_icons::FileIcons;
|
||||
use futures::channel::oneshot;
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use project::{DirectoryItem, DirectoryLister};
|
||||
use settings::Settings;
|
||||
use std::{
|
||||
path::{MAIN_SEPARATOR_STR, Path, PathBuf},
|
||||
path::{self, MAIN_SEPARATOR_STR, Path, PathBuf},
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{self, AtomicBool},
|
||||
@@ -349,8 +352,9 @@ impl PickerDelegate for OpenPathDelegate {
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
_window: &mut Window,
|
||||
_: &mut Context<Picker<Self>>,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let settings = FileFinderSettings::get_global(cx);
|
||||
let m = self.matches.get(ix)?;
|
||||
let directory_state = self.directory_state.as_ref()?;
|
||||
let candidate = directory_state.match_candidates.get(*m)?;
|
||||
@@ -361,9 +365,23 @@ impl PickerDelegate for OpenPathDelegate {
|
||||
.map(|string_match| string_match.positions.clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
let file_icon = maybe!({
|
||||
if !settings.file_icons {
|
||||
return None;
|
||||
}
|
||||
let icon = if candidate.is_dir {
|
||||
FileIcons::get_folder_icon(false, cx)?
|
||||
} else {
|
||||
let path = path::Path::new(&candidate.path.string);
|
||||
FileIcons::get_icon(&path, cx)?
|
||||
};
|
||||
Some(Icon::from_path(icon).color(Color::Muted))
|
||||
});
|
||||
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.start_slot::<Icon>(file_icon)
|
||||
.inset(true)
|
||||
.toggle_state(selected)
|
||||
.child(HighlightedLabel::new(
|
||||
|
||||
@@ -1388,6 +1388,44 @@ where
|
||||
&& point.y <= self.origin.y.clone() + self.size.height.clone()
|
||||
}
|
||||
|
||||
/// Checks if this bounds is completely contained within another bounds.
|
||||
///
|
||||
/// This method determines whether the current bounds is entirely enclosed by the given bounds.
|
||||
/// A bounds is considered to be contained within another if its origin (top-left corner) and
|
||||
/// its bottom-right corner are both contained within the other bounds.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `other` - A reference to another `Bounds` that might contain this bounds.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Returns `true` if this bounds is completely inside the other bounds, `false` otherwise.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use gpui::{Bounds, Point, Size};
|
||||
/// let outer_bounds = Bounds {
|
||||
/// origin: Point { x: 0, y: 0 },
|
||||
/// size: Size { width: 20, height: 20 },
|
||||
/// };
|
||||
/// let inner_bounds = Bounds {
|
||||
/// origin: Point { x: 5, y: 5 },
|
||||
/// size: Size { width: 10, height: 10 },
|
||||
/// };
|
||||
/// let overlapping_bounds = Bounds {
|
||||
/// origin: Point { x: 15, y: 15 },
|
||||
/// size: Size { width: 10, height: 10 },
|
||||
/// };
|
||||
///
|
||||
/// assert!(inner_bounds.is_contained_within(&outer_bounds));
|
||||
/// assert!(!overlapping_bounds.is_contained_within(&outer_bounds));
|
||||
/// ```
|
||||
pub fn is_contained_within(&self, other: &Self) -> bool {
|
||||
other.contains(&self.origin) && other.contains(&self.bottom_right())
|
||||
}
|
||||
|
||||
/// Applies a function to the origin and size of the bounds, producing a new `Bounds<U>`.
|
||||
///
|
||||
/// This method allows for converting a `Bounds<T>` to a `Bounds<U>` by specifying a closure
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
[package]
|
||||
name = "language_model_selector"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/language_model_selector.rs"
|
||||
|
||||
[features]
|
||||
test-support = [
|
||||
"gpui/test-support",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
collections.workspace = true
|
||||
feature_flags.workspace = true
|
||||
futures.workspace = true
|
||||
fuzzy.workspace = true
|
||||
gpui.workspace = true
|
||||
language_model.workspace = true
|
||||
log.workspace = true
|
||||
ordered-float.workspace = true
|
||||
picker.workspace = true
|
||||
proto.workspace = true
|
||||
ui.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
zed_actions.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
gpui = { workspace = true, "features" = ["test-support"] }
|
||||
language_model = { workspace = true, "features" = ["test-support"] }
|
||||
@@ -1 +0,0 @@
|
||||
../../LICENSE-GPL
|
||||
@@ -7,9 +7,9 @@ pub use language::*;
|
||||
use lsp::{DiagnosticTag, InitializeParams, LanguageServerBinary, LanguageServerName};
|
||||
use project::lsp_store::clangd_ext;
|
||||
use serde_json::json;
|
||||
use smol::fs::{self, File};
|
||||
use smol::{fs, io::BufReader};
|
||||
use std::{any::Any, env::consts, path::PathBuf, sync::Arc};
|
||||
use util::{ResultExt, fs::remove_matching, maybe, merge_json_value_into};
|
||||
use util::{ResultExt, archive::extract_zip, fs::remove_matching, maybe, merge_json_value_into};
|
||||
|
||||
pub struct CLspAdapter;
|
||||
|
||||
@@ -32,7 +32,7 @@ impl super::LspAdapter for CLspAdapter {
|
||||
let path = delegate.which(Self::SERVER_NAME.as_ref()).await?;
|
||||
Some(LanguageServerBinary {
|
||||
path,
|
||||
arguments: vec![],
|
||||
arguments: Vec::new(),
|
||||
env: None,
|
||||
})
|
||||
}
|
||||
@@ -69,7 +69,6 @@ impl super::LspAdapter for CLspAdapter {
|
||||
delegate: &dyn LspAdapterDelegate,
|
||||
) -> Result<LanguageServerBinary> {
|
||||
let version = version.downcast::<GitHubLspBinaryVersion>().unwrap();
|
||||
let zip_path = container_dir.join(format!("clangd_{}.zip", version.name));
|
||||
let version_dir = container_dir.join(format!("clangd_{}", version.name));
|
||||
let binary_path = version_dir.join("bin/clangd");
|
||||
|
||||
@@ -79,28 +78,31 @@ impl super::LspAdapter for CLspAdapter {
|
||||
.get(&version.url, Default::default(), true)
|
||||
.await
|
||||
.context("error downloading release")?;
|
||||
let mut file = File::create(&zip_path).await?;
|
||||
anyhow::ensure!(
|
||||
response.status().is_success(),
|
||||
"download failed with status {}",
|
||||
response.status().to_string()
|
||||
);
|
||||
futures::io::copy(response.body_mut(), &mut file).await?;
|
||||
|
||||
let unzip_status = util::command::new_smol_command("unzip")
|
||||
.current_dir(&container_dir)
|
||||
.arg(&zip_path)
|
||||
.output()
|
||||
.await?
|
||||
.status;
|
||||
anyhow::ensure!(unzip_status.success(), "failed to unzip clangd archive");
|
||||
extract_zip(&container_dir, BufReader::new(response.body_mut()))
|
||||
.await
|
||||
.with_context(|| format!("unzipping clangd archive to {container_dir:?}"))?;
|
||||
remove_matching(&container_dir, |entry| entry != version_dir).await;
|
||||
|
||||
// todo("windows")
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
fs::set_permissions(
|
||||
&binary_path,
|
||||
<fs::Permissions as fs::unix::PermissionsExt>::from_mode(0o755),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(LanguageServerBinary {
|
||||
path: binary_path,
|
||||
env: None,
|
||||
arguments: vec![],
|
||||
arguments: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -306,7 +308,7 @@ impl super::LspAdapter for CLspAdapter {
|
||||
.map(move |diag| {
|
||||
let range =
|
||||
language::range_to_lsp(diag.range.to_point_utf16(&snapshot)).unwrap();
|
||||
let mut tags = vec![];
|
||||
let mut tags = Vec::with_capacity(1);
|
||||
if diag.diagnostic.is_unnecessary {
|
||||
tags.push(DiagnosticTag::UNNECESSARY);
|
||||
}
|
||||
@@ -344,7 +346,7 @@ async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServ
|
||||
Ok(LanguageServerBinary {
|
||||
path: clangd_bin,
|
||||
env: None,
|
||||
arguments: vec![],
|
||||
arguments: Vec::new(),
|
||||
})
|
||||
})
|
||||
.await
|
||||
|
||||
@@ -26,7 +26,7 @@ use std::{
|
||||
sync::Arc,
|
||||
};
|
||||
use task::{TaskTemplate, TaskTemplates, VariableName};
|
||||
use util::{ResultExt, fs::remove_matching, maybe, merge_json_value_into};
|
||||
use util::{ResultExt, archive::extract_zip, fs::remove_matching, maybe, merge_json_value_into};
|
||||
|
||||
const SERVER_PATH: &str =
|
||||
"node_modules/vscode-langservers-extracted/bin/vscode-json-language-server";
|
||||
@@ -429,7 +429,7 @@ impl LspAdapter for NodeVersionAdapter {
|
||||
.await
|
||||
.context("downloading release")?;
|
||||
if version.url.ends_with(".zip") {
|
||||
node_runtime::extract_zip(
|
||||
extract_zip(
|
||||
&destination_container_path,
|
||||
BufReader::new(response.body_mut()),
|
||||
)
|
||||
|
||||
@@ -22,6 +22,7 @@ use std::{
|
||||
sync::{Arc, LazyLock},
|
||||
};
|
||||
use task::{TaskTemplate, TaskTemplates, TaskVariables, VariableName};
|
||||
use util::archive::extract_zip;
|
||||
use util::merge_json_value_into;
|
||||
use util::{ResultExt, fs::remove_matching, maybe};
|
||||
|
||||
@@ -215,14 +216,11 @@ impl LspAdapter for RustLspAdapter {
|
||||
})?;
|
||||
}
|
||||
AssetKind::Zip => {
|
||||
node_runtime::extract_zip(
|
||||
&destination_path,
|
||||
BufReader::new(response.body_mut()),
|
||||
)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("unzipping {} to {:?}", version.url, destination_path)
|
||||
})?;
|
||||
extract_zip(&destination_path, BufReader::new(response.body_mut()))
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("unzipping {} to {:?}", version.url, destination_path)
|
||||
})?;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ use std::{
|
||||
sync::Arc,
|
||||
};
|
||||
use task::{TaskTemplate, TaskTemplates, VariableName};
|
||||
use util::archive::extract_zip;
|
||||
use util::{ResultExt, fs::remove_matching, maybe};
|
||||
|
||||
pub(super) fn typescript_task_context() -> ContextProviderWithTasks {
|
||||
@@ -514,14 +515,11 @@ impl LspAdapter for EsLintLspAdapter {
|
||||
})?;
|
||||
}
|
||||
AssetKind::Zip => {
|
||||
node_runtime::extract_zip(
|
||||
&destination_path,
|
||||
BufReader::new(response.body_mut()),
|
||||
)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("unzipping {} to {:?}", version.url, destination_path)
|
||||
})?;
|
||||
extract_zip(&destination_path, BufReader::new(response.body_mut()))
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("unzipping {} to {:?}", version.url, destination_path)
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
fs.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::markdown_elements::{
|
||||
ParsedMarkdownHeading, ParsedMarkdownListItem, ParsedMarkdownListItemType, ParsedMarkdownTable,
|
||||
ParsedMarkdownTableAlignment, ParsedMarkdownTableRow,
|
||||
};
|
||||
use fs::normalize_path;
|
||||
use gpui::{
|
||||
AbsoluteLength, AnyElement, App, AppContext as _, ClipboardItem, Context, DefiniteLength, Div,
|
||||
Element, ElementId, Entity, HighlightStyle, Hsla, ImageSource, InteractiveText, IntoElement,
|
||||
@@ -680,7 +681,7 @@ fn render_markdown_text(parsed_new: &MarkdownParagraph, cx: &mut RenderContext)
|
||||
_ = workspace.update(cx, |workspace, cx| {
|
||||
workspace
|
||||
.open_abs_path(
|
||||
path.clone(),
|
||||
normalize_path(path.clone().as_path()),
|
||||
OpenOptions {
|
||||
visible: Some(OpenVisible::None),
|
||||
..Default::default()
|
||||
|
||||
@@ -13,7 +13,7 @@ path = "src/node_runtime.rs"
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
test-support = ["tempfile"]
|
||||
test-support = []
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
@@ -21,7 +21,6 @@ async-compression.workspace = true
|
||||
async-watch.workspace = true
|
||||
async-tar.workspace = true
|
||||
async-trait.workspace = true
|
||||
async_zip.workspace = true
|
||||
futures.workspace = true
|
||||
http_client.workspace = true
|
||||
log.workspace = true
|
||||
@@ -30,14 +29,9 @@ semver.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
smol.workspace = true
|
||||
tempfile = { workspace = true, optional = true }
|
||||
util.workspace = true
|
||||
walkdir = "2.5.0"
|
||||
which.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
async-std = { version = "1.12.0", features = ["unstable"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile.workspace = true
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
mod archive;
|
||||
|
||||
use anyhow::{Context as _, Result, anyhow, bail};
|
||||
pub use archive::extract_zip;
|
||||
use async_compression::futures::bufread::GzipDecoder;
|
||||
use async_tar::Archive;
|
||||
use futures::{AsyncReadExt, FutureExt as _, channel::oneshot, future::Shared};
|
||||
@@ -19,6 +16,7 @@ use std::{
|
||||
sync::Arc,
|
||||
};
|
||||
use util::ResultExt;
|
||||
use util::archive::extract_zip;
|
||||
|
||||
const NODE_CA_CERTS_ENV_VAR: &str = "NODE_EXTRA_CA_CERTS";
|
||||
|
||||
@@ -353,7 +351,7 @@ impl ManagedNodeRuntime {
|
||||
let archive = Archive::new(decompressed_bytes);
|
||||
archive.unpack(&node_containing_dir).await?;
|
||||
}
|
||||
ArchiveType::Zip => archive::extract_zip(&node_containing_dir, body).await?,
|
||||
ArchiveType::Zip => extract_zip(&node_containing_dir, body).await?,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ use dap::{
|
||||
messages::{Events, Message},
|
||||
};
|
||||
use dap::{
|
||||
ExceptionBreakpointsFilter, ExceptionFilterOptions, OutputEventCategory,
|
||||
ExceptionBreakpointsFilter, ExceptionFilterOptions, OutputEvent, OutputEventCategory,
|
||||
RunInTerminalRequestArguments, StartDebuggingRequestArguments,
|
||||
};
|
||||
use futures::channel::{mpsc, oneshot};
|
||||
@@ -674,6 +674,7 @@ pub enum SessionEvent {
|
||||
request: RunInTerminalRequestArguments,
|
||||
sender: mpsc::Sender<Result<u32>>,
|
||||
},
|
||||
ConsoleOutput,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
@@ -885,9 +886,8 @@ impl Session {
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
while let Some(output) = rx.next().await {
|
||||
this.update(cx, |this, _| {
|
||||
this.output_token.0 += 1;
|
||||
this.output.push_back(dap::OutputEvent {
|
||||
this.update(cx, |this, cx| {
|
||||
let event = dap::OutputEvent {
|
||||
category: None,
|
||||
output,
|
||||
group: None,
|
||||
@@ -897,7 +897,8 @@ impl Session {
|
||||
column: None,
|
||||
data: None,
|
||||
location_reference: None,
|
||||
});
|
||||
};
|
||||
this.push_output(event, cx);
|
||||
})?;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
@@ -1266,8 +1267,7 @@ impl Session {
|
||||
return;
|
||||
}
|
||||
|
||||
self.output.push_back(event);
|
||||
self.output_token.0 += 1;
|
||||
self.push_output(event, cx);
|
||||
cx.notify();
|
||||
}
|
||||
Events::Breakpoint(event) => self.breakpoint_store.update(cx, |store, _| {
|
||||
@@ -1445,6 +1445,12 @@ impl Session {
|
||||
});
|
||||
}
|
||||
|
||||
fn push_output(&mut self, event: OutputEvent, cx: &mut Context<Self>) {
|
||||
self.output.push_back(event);
|
||||
self.output_token.0 += 1;
|
||||
cx.emit(SessionEvent::ConsoleOutput);
|
||||
}
|
||||
|
||||
pub fn any_stopped_thread(&self) -> bool {
|
||||
self.thread_states.any_stopped_thread()
|
||||
}
|
||||
@@ -2063,8 +2069,7 @@ impl Session {
|
||||
source: Option<Source>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<()> {
|
||||
self.output_token.0 += 1;
|
||||
self.output.push_back(dap::OutputEvent {
|
||||
let event = dap::OutputEvent {
|
||||
category: None,
|
||||
output: format!("> {expression}"),
|
||||
group: None,
|
||||
@@ -2074,7 +2079,8 @@ impl Session {
|
||||
column: None,
|
||||
data: None,
|
||||
location_reference: None,
|
||||
});
|
||||
};
|
||||
self.push_output(event, cx);
|
||||
let request = self.mode.request_dap(EvaluateCommand {
|
||||
expression,
|
||||
context,
|
||||
@@ -2086,8 +2092,7 @@ impl Session {
|
||||
this.update(cx, |this, cx| {
|
||||
match response {
|
||||
Ok(response) => {
|
||||
this.output_token.0 += 1;
|
||||
this.output.push_back(dap::OutputEvent {
|
||||
let event = dap::OutputEvent {
|
||||
category: None,
|
||||
output: format!("< {}", &response.result),
|
||||
group: None,
|
||||
@@ -2097,11 +2102,11 @@ impl Session {
|
||||
column: None,
|
||||
data: None,
|
||||
location_reference: None,
|
||||
});
|
||||
};
|
||||
this.push_output(event, cx);
|
||||
}
|
||||
Err(e) => {
|
||||
this.output_token.0 += 1;
|
||||
this.output.push_back(dap::OutputEvent {
|
||||
let event = dap::OutputEvent {
|
||||
category: None,
|
||||
output: format!("{}", e),
|
||||
group: None,
|
||||
@@ -2111,7 +2116,8 @@ impl Session {
|
||||
column: None,
|
||||
data: None,
|
||||
location_reference: None,
|
||||
});
|
||||
};
|
||||
this.push_output(event, cx);
|
||||
}
|
||||
};
|
||||
this.invalidate_command_type::<ScopesCommand>();
|
||||
|
||||
@@ -348,11 +348,11 @@ impl LocalLspStore {
|
||||
delegate.update_status(
|
||||
adapter.name(),
|
||||
BinaryStatus::Failed {
|
||||
error: format!("{err}\n-- stderr--\n{}", log),
|
||||
error: format!("{err}\n-- stderr--\n{log}"),
|
||||
},
|
||||
);
|
||||
log::error!("Failed to start language server {server_name:?}: {err}");
|
||||
log::error!("server stderr: {:?}", log);
|
||||
log::error!("Failed to start language server {server_name:?}: {err:#?}");
|
||||
log::error!("server stderr: {log}");
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3662,9 +3662,8 @@ impl Project {
|
||||
// ranges in the buffer matched by the query.
|
||||
let mut chunks = pin!(chunks);
|
||||
'outer: while let Some(matching_buffer_chunk) = chunks.next().await {
|
||||
let mut chunk_results = Vec::new();
|
||||
let mut chunk_results = Vec::with_capacity(matching_buffer_chunk.len());
|
||||
for buffer in matching_buffer_chunk {
|
||||
let buffer = buffer.clone();
|
||||
let query = query.clone();
|
||||
let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot())?;
|
||||
chunk_results.push(cx.background_spawn(async move {
|
||||
|
||||
@@ -15,7 +15,7 @@ use anyhow::Result;
|
||||
use collections::HashMap;
|
||||
use fs::Fs;
|
||||
use gpui::{App, AppContext as _, Context, Entity, Task};
|
||||
use util::ResultExt;
|
||||
use util::{ResultExt, archive::extract_zip};
|
||||
|
||||
pub(crate) struct YarnPathStore {
|
||||
temp_dirs: HashMap<Arc<Path>, tempfile::TempDir>,
|
||||
@@ -131,7 +131,7 @@ fn zip_path(path: &Path) -> Option<&Path> {
|
||||
async fn dump_zip(path: Arc<Path>, fs: Arc<dyn Fs>) -> Result<tempfile::TempDir> {
|
||||
let dir = tempfile::tempdir()?;
|
||||
let contents = fs.load_bytes(&path).await?;
|
||||
node_runtime::extract_zip(dir.path(), futures::io::Cursor::new(contents)).await?;
|
||||
extract_zip(dir.path(), futures::io::Cursor::new(contents)).await?;
|
||||
Ok(dir)
|
||||
}
|
||||
|
||||
|
||||
@@ -97,7 +97,7 @@ pub fn run(
|
||||
};
|
||||
|
||||
let (runnable_ranges, next_cell_point) =
|
||||
runnable_ranges(&buffer.read(cx).snapshot(), selected_range);
|
||||
runnable_ranges(&buffer.read(cx).snapshot(), selected_range, cx);
|
||||
|
||||
for runnable_range in runnable_ranges {
|
||||
let Some(language) = multibuffer.read(cx).language_at(runnable_range.start, cx) else {
|
||||
@@ -215,7 +215,8 @@ pub fn session(editor: WeakEntity<Editor>, cx: &mut App) -> SessionSupport {
|
||||
match kernelspec {
|
||||
Some(kernelspec) => SessionSupport::Inactive(kernelspec),
|
||||
None => {
|
||||
if language_supported(&language.clone()) {
|
||||
// For language_supported, need to check available kernels for language
|
||||
if language_supported(&language.clone(), cx) {
|
||||
SessionSupport::RequiresSetup(language.name())
|
||||
} else {
|
||||
SessionSupport::Unsupported
|
||||
@@ -414,10 +415,11 @@ fn jupytext_cells(
|
||||
fn runnable_ranges(
|
||||
buffer: &BufferSnapshot,
|
||||
range: Range<Point>,
|
||||
cx: &mut App,
|
||||
) -> (Vec<Range<Point>>, Option<Point>) {
|
||||
if let Some(language) = buffer.language() {
|
||||
if language.name() == "Markdown".into() {
|
||||
return (markdown_code_blocks(buffer, range.clone()), None);
|
||||
return (markdown_code_blocks(buffer, range.clone(), cx), None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -442,21 +444,30 @@ fn runnable_ranges(
|
||||
|
||||
// We allow markdown code blocks to end in a trailing newline in order to render the output
|
||||
// below the final code fence. This is different than our behavior for selections and Jupytext cells.
|
||||
fn markdown_code_blocks(buffer: &BufferSnapshot, range: Range<Point>) -> Vec<Range<Point>> {
|
||||
fn markdown_code_blocks(
|
||||
buffer: &BufferSnapshot,
|
||||
range: Range<Point>,
|
||||
cx: &mut App,
|
||||
) -> Vec<Range<Point>> {
|
||||
buffer
|
||||
.injections_intersecting_range(range)
|
||||
.filter(|(_, language)| language_supported(language))
|
||||
.filter(|(_, language)| language_supported(language, cx))
|
||||
.map(|(content_range, _)| {
|
||||
buffer.offset_to_point(content_range.start)..buffer.offset_to_point(content_range.end)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn language_supported(language: &Arc<Language>) -> bool {
|
||||
match language.name().as_ref() {
|
||||
"TypeScript" | "Python" => true,
|
||||
_ => false,
|
||||
}
|
||||
fn language_supported(language: &Arc<Language>, cx: &mut App) -> bool {
|
||||
let store = ReplStore::global(cx);
|
||||
let store_read = store.read(cx);
|
||||
|
||||
// Since we're just checking for general language support, we only need to look at
|
||||
// the pure Jupyter kernels - these are all the globally available ones
|
||||
store_read.pure_jupyter_kernel_specifications().any(|spec| {
|
||||
// Convert to lowercase for case-insensitive comparison since kernels might report "python" while our language is "Python"
|
||||
spec.language().as_ref().to_lowercase() == language.name().as_ref().to_lowercase()
|
||||
})
|
||||
}
|
||||
|
||||
fn get_language(editor: WeakEntity<Editor>, cx: &mut App) -> Option<Arc<Language>> {
|
||||
@@ -506,7 +517,7 @@ mod tests {
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
|
||||
// Single-point selection
|
||||
let (snippets, _) = runnable_ranges(&snapshot, Point::new(0, 4)..Point::new(0, 4));
|
||||
let (snippets, _) = runnable_ranges(&snapshot, Point::new(0, 4)..Point::new(0, 4), cx);
|
||||
let snippets = snippets
|
||||
.into_iter()
|
||||
.map(|range| snapshot.text_for_range(range).collect::<String>())
|
||||
@@ -514,7 +525,7 @@ mod tests {
|
||||
assert_eq!(snippets, vec!["print(1 + 1)"]);
|
||||
|
||||
// Multi-line selection
|
||||
let (snippets, _) = runnable_ranges(&snapshot, Point::new(0, 5)..Point::new(2, 0));
|
||||
let (snippets, _) = runnable_ranges(&snapshot, Point::new(0, 5)..Point::new(2, 0), cx);
|
||||
let snippets = snippets
|
||||
.into_iter()
|
||||
.map(|range| snapshot.text_for_range(range).collect::<String>())
|
||||
@@ -527,7 +538,7 @@ mod tests {
|
||||
);
|
||||
|
||||
// Trimming multiple trailing blank lines
|
||||
let (snippets, _) = runnable_ranges(&snapshot, Point::new(0, 5)..Point::new(5, 0));
|
||||
let (snippets, _) = runnable_ranges(&snapshot, Point::new(0, 5)..Point::new(5, 0), cx);
|
||||
|
||||
let snippets = snippets
|
||||
.into_iter()
|
||||
@@ -580,7 +591,7 @@ mod tests {
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
|
||||
// Jupytext snippet surrounding an empty selection
|
||||
let (snippets, _) = runnable_ranges(&snapshot, Point::new(2, 5)..Point::new(2, 5));
|
||||
let (snippets, _) = runnable_ranges(&snapshot, Point::new(2, 5)..Point::new(2, 5), cx);
|
||||
|
||||
let snippets = snippets
|
||||
.into_iter()
|
||||
@@ -596,7 +607,7 @@ mod tests {
|
||||
);
|
||||
|
||||
// Jupytext snippets intersecting a non-empty selection
|
||||
let (snippets, _) = runnable_ranges(&snapshot, Point::new(2, 5)..Point::new(6, 2));
|
||||
let (snippets, _) = runnable_ranges(&snapshot, Point::new(2, 5)..Point::new(6, 2), cx);
|
||||
let snippets = snippets
|
||||
.into_iter()
|
||||
.map(|range| snapshot.text_for_range(range).collect::<String>())
|
||||
@@ -623,6 +634,49 @@ mod tests {
|
||||
|
||||
#[gpui::test]
|
||||
fn test_markdown_code_blocks(cx: &mut App) {
|
||||
use crate::kernels::LocalKernelSpecification;
|
||||
use jupyter_protocol::JupyterKernelspec;
|
||||
|
||||
// Initialize settings
|
||||
settings::init(cx);
|
||||
editor::init(cx);
|
||||
|
||||
// Initialize the ReplStore with a fake filesystem
|
||||
let fs = Arc::new(project::RealFs::new(None, cx.background_executor().clone()));
|
||||
ReplStore::init(fs, cx);
|
||||
|
||||
// Add mock kernel specifications for TypeScript and Python
|
||||
let store = ReplStore::global(cx);
|
||||
store.update(cx, |store, cx| {
|
||||
let typescript_spec = KernelSpecification::Jupyter(LocalKernelSpecification {
|
||||
name: "typescript".into(),
|
||||
kernelspec: JupyterKernelspec {
|
||||
argv: vec![],
|
||||
display_name: "TypeScript".into(),
|
||||
language: "typescript".into(),
|
||||
interrupt_mode: None,
|
||||
metadata: None,
|
||||
env: None,
|
||||
},
|
||||
path: std::path::PathBuf::new(),
|
||||
});
|
||||
|
||||
let python_spec = KernelSpecification::Jupyter(LocalKernelSpecification {
|
||||
name: "python".into(),
|
||||
kernelspec: JupyterKernelspec {
|
||||
argv: vec![],
|
||||
display_name: "Python".into(),
|
||||
language: "python".into(),
|
||||
interrupt_mode: None,
|
||||
metadata: None,
|
||||
env: None,
|
||||
},
|
||||
path: std::path::PathBuf::new(),
|
||||
});
|
||||
|
||||
store.set_kernel_specs_for_testing(vec![typescript_spec, python_spec], cx);
|
||||
});
|
||||
|
||||
let markdown = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
|
||||
let typescript = languages::language(
|
||||
"typescript",
|
||||
@@ -658,7 +712,7 @@ mod tests {
|
||||
});
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
|
||||
let (snippets, _) = runnable_ranges(&snapshot, Point::new(3, 5)..Point::new(8, 5));
|
||||
let (snippets, _) = runnable_ranges(&snapshot, Point::new(3, 5)..Point::new(8, 5), cx);
|
||||
let snippets = snippets
|
||||
.into_iter()
|
||||
.map(|range| snapshot.text_for_range(range).collect::<String>())
|
||||
@@ -703,7 +757,7 @@ mod tests {
|
||||
});
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
|
||||
let (snippets, _) = runnable_ranges(&snapshot, Point::new(3, 5)..Point::new(12, 5));
|
||||
let (snippets, _) = runnable_ranges(&snapshot, Point::new(3, 5)..Point::new(12, 5), cx);
|
||||
let snippets = snippets
|
||||
.into_iter()
|
||||
.map(|range| snapshot.text_for_range(range).collect::<String>())
|
||||
@@ -742,7 +796,7 @@ mod tests {
|
||||
});
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
|
||||
let (snippets, _) = runnable_ranges(&snapshot, Point::new(4, 5)..Point::new(5, 5));
|
||||
let (snippets, _) = runnable_ranges(&snapshot, Point::new(4, 5)..Point::new(5, 5), cx);
|
||||
let snippets = snippets
|
||||
.into_iter()
|
||||
.map(|range| snapshot.text_for_range(range).collect::<String>())
|
||||
|
||||
@@ -279,4 +279,14 @@ impl ReplStore {
|
||||
pub fn remove_session(&mut self, entity_id: EntityId) {
|
||||
self.sessions.remove(&entity_id);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn set_kernel_specs_for_testing(
|
||||
&mut self,
|
||||
specs: Vec<KernelSpecification>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.kernel_specifications = specs;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1460,7 +1460,6 @@ impl BufferSearchBar {
|
||||
self.select_next_match(&SelectNextMatch, window, cx);
|
||||
}
|
||||
should_propagate = false;
|
||||
self.focus_editor(&FocusEditor, window, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -324,24 +324,24 @@ impl ProjectSearch {
|
||||
}
|
||||
}
|
||||
|
||||
let excerpts = project_search
|
||||
.update(cx, |project_search, _| project_search.excerpts.clone())
|
||||
.ok()?;
|
||||
let mut new_ranges = excerpts
|
||||
.update(cx, |excerpts, cx| {
|
||||
buffers_with_ranges
|
||||
.into_iter()
|
||||
.map(|(buffer, ranges)| {
|
||||
excerpts.set_anchored_excerpts_for_path(
|
||||
buffer,
|
||||
ranges,
|
||||
editor::DEFAULT_MULTIBUFFER_CONTEXT,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.collect::<FuturesOrdered<_>>()
|
||||
let mut new_ranges = project_search
|
||||
.update(cx, |project_search, cx| {
|
||||
project_search.excerpts.update(cx, |excerpts, cx| {
|
||||
buffers_with_ranges
|
||||
.into_iter()
|
||||
.map(|(buffer, ranges)| {
|
||||
excerpts.set_anchored_excerpts_for_path(
|
||||
buffer,
|
||||
ranges,
|
||||
editor::DEFAULT_MULTIBUFFER_CONTEXT,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.collect::<FuturesOrdered<_>>()
|
||||
})
|
||||
})
|
||||
.ok()?;
|
||||
|
||||
while let Some(new_ranges) = new_ranges.next().await {
|
||||
project_search
|
||||
.update(cx, |project_search, _| {
|
||||
@@ -1031,6 +1031,12 @@ impl ProjectSearchView {
|
||||
.update(cx, |editor, cx| editor.set_text(included_files, window, cx));
|
||||
search.filters_enabled = true;
|
||||
}
|
||||
if let Some(excluded_files) = action.excluded_files.as_deref() {
|
||||
search
|
||||
.excluded_files_editor
|
||||
.update(cx, |editor, cx| editor.set_text(excluded_files, window, cx));
|
||||
search.filters_enabled = true;
|
||||
}
|
||||
search.focus_query_editor(window, cx)
|
||||
});
|
||||
}
|
||||
@@ -1055,16 +1061,17 @@ impl ProjectSearchView {
|
||||
|
||||
let is_dirty = self.is_dirty(cx);
|
||||
|
||||
let skip_save_on_close = self
|
||||
.workspace
|
||||
.read_with(cx, |workspace, cx| {
|
||||
workspace::Pane::skip_save_on_close(&self.results_editor, workspace, cx)
|
||||
})
|
||||
.unwrap_or(false);
|
||||
|
||||
let should_prompt_to_save = !skip_save_on_close && !will_autosave && is_dirty;
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let skip_save_on_close = this
|
||||
.read_with(cx, |this, cx| {
|
||||
this.workspace.read_with(cx, |workspace, cx| {
|
||||
workspace::Pane::skip_save_on_close(&this.results_editor, workspace, cx)
|
||||
})
|
||||
})?
|
||||
.unwrap_or(false);
|
||||
|
||||
let should_prompt_to_save = !skip_save_on_close && !will_autosave && is_dirty;
|
||||
|
||||
let should_search = if should_prompt_to_save {
|
||||
let options = &["Save", "Don't Save", "Cancel"];
|
||||
let result_channel = this.update_in(cx, |_, window, cx| {
|
||||
|
||||
@@ -18,6 +18,7 @@ doctest = false
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
async-recursion.workspace = true
|
||||
assistant_slash_command.workspace = true
|
||||
breadcrumbs.workspace = true
|
||||
collections.workspace = true
|
||||
db.workspace = true
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
|
||||
use crate::{TerminalView, terminal_panel::TerminalPanel};
|
||||
use anyhow::Result;
|
||||
use assistant_slash_command::{
|
||||
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
|
||||
@@ -8,11 +9,10 @@ use assistant_slash_command::{
|
||||
};
|
||||
use gpui::{App, Entity, Task, WeakEntity};
|
||||
use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate};
|
||||
use terminal_view::{TerminalView, terminal_panel::TerminalPanel};
|
||||
use ui::prelude::*;
|
||||
use workspace::{Workspace, dock::Panel};
|
||||
|
||||
use super::create_label_for_command;
|
||||
use assistant_slash_command::create_label_for_command;
|
||||
|
||||
pub struct TerminalSlashCommand;
|
||||
|
||||
@@ -2,8 +2,10 @@ mod persistence;
|
||||
pub mod terminal_element;
|
||||
pub mod terminal_panel;
|
||||
pub mod terminal_scrollbar;
|
||||
mod terminal_slash_command;
|
||||
pub mod terminal_tab_tooltip;
|
||||
|
||||
use assistant_slash_command::SlashCommandRegistry;
|
||||
use editor::{Editor, EditorSettings, actions::SelectAll, scroll::ScrollbarAutoHide};
|
||||
use gpui::{
|
||||
AnyElement, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, KeyContext,
|
||||
@@ -29,6 +31,7 @@ use terminal::{
|
||||
use terminal_element::{TerminalElement, is_blank};
|
||||
use terminal_panel::TerminalPanel;
|
||||
use terminal_scrollbar::TerminalScrollHandle;
|
||||
use terminal_slash_command::TerminalSlashCommand;
|
||||
use terminal_tab_tooltip::TerminalTooltip;
|
||||
use ui::{
|
||||
ContextMenu, Icon, IconName, Label, Scrollbar, ScrollbarState, Tooltip, h_flex, prelude::*,
|
||||
@@ -78,6 +81,7 @@ actions!(terminal, [RerunTask]);
|
||||
impl_actions!(terminal, [SendText, SendKeystroke]);
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
assistant_slash_command::init(cx);
|
||||
terminal_panel::init(cx);
|
||||
terminal::init(cx);
|
||||
|
||||
@@ -87,6 +91,7 @@ pub fn init(cx: &mut App) {
|
||||
workspace.register_action(TerminalView::deploy);
|
||||
})
|
||||
.detach();
|
||||
SlashCommandRegistry::global(cx).register_command(TerminalSlashCommand, true);
|
||||
}
|
||||
|
||||
pub struct BlockProperties {
|
||||
|
||||
@@ -154,7 +154,6 @@ pub struct ContextMenu {
|
||||
key_context: SharedString,
|
||||
_on_blur_subscription: Subscription,
|
||||
keep_open_on_confirm: bool,
|
||||
eager: bool,
|
||||
documentation_aside: Option<(usize, DocumentationAside)>,
|
||||
fixed_width: Option<DefiniteLength>,
|
||||
}
|
||||
@@ -207,7 +206,6 @@ impl ContextMenu {
|
||||
key_context: "menu".into(),
|
||||
_on_blur_subscription,
|
||||
keep_open_on_confirm: false,
|
||||
eager: false,
|
||||
documentation_aside: None,
|
||||
fixed_width: None,
|
||||
end_slot_action: None,
|
||||
@@ -250,43 +248,6 @@ impl ContextMenu {
|
||||
key_context: "menu".into(),
|
||||
_on_blur_subscription,
|
||||
keep_open_on_confirm: true,
|
||||
eager: false,
|
||||
documentation_aside: None,
|
||||
fixed_width: None,
|
||||
end_slot_action: None,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn build_eager(
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
f: impl FnOnce(Self, &mut Window, &mut Context<Self>) -> Self,
|
||||
) -> Entity<Self> {
|
||||
cx.new(|cx| {
|
||||
let focus_handle = cx.focus_handle();
|
||||
let _on_blur_subscription = cx.on_blur(
|
||||
&focus_handle,
|
||||
window,
|
||||
|this: &mut ContextMenu, window, cx| this.cancel(&menu::Cancel, window, cx),
|
||||
);
|
||||
window.refresh();
|
||||
f(
|
||||
Self {
|
||||
builder: None,
|
||||
items: Default::default(),
|
||||
focus_handle,
|
||||
action_context: None,
|
||||
selected_index: None,
|
||||
delayed: false,
|
||||
clicked: false,
|
||||
key_context: "menu".into(),
|
||||
_on_blur_subscription,
|
||||
keep_open_on_confirm: false,
|
||||
eager: true,
|
||||
documentation_aside: None,
|
||||
fixed_width: None,
|
||||
end_slot_action: None,
|
||||
@@ -327,7 +288,6 @@ impl ContextMenu {
|
||||
|this: &mut ContextMenu, window, cx| this.cancel(&menu::Cancel, window, cx),
|
||||
),
|
||||
keep_open_on_confirm: false,
|
||||
eager: false,
|
||||
documentation_aside: None,
|
||||
fixed_width: None,
|
||||
end_slot_action: None,
|
||||
@@ -634,10 +594,7 @@ impl ContextMenu {
|
||||
..
|
||||
})
|
||||
| ContextMenuItem::CustomEntry { handler, .. },
|
||||
) = self
|
||||
.selected_index
|
||||
.and_then(|ix| self.items.get(ix))
|
||||
.filter(|_| !self.eager)
|
||||
) = self.selected_index.and_then(|ix| self.items.get(ix))
|
||||
{
|
||||
(handler)(context, window, cx)
|
||||
}
|
||||
@@ -740,10 +697,9 @@ impl ContextMenu {
|
||||
fn select_index(
|
||||
&mut self,
|
||||
ix: usize,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Self>,
|
||||
) -> Option<usize> {
|
||||
let context = self.action_context.as_ref();
|
||||
self.documentation_aside = None;
|
||||
let item = self.items.get(ix)?;
|
||||
if item.is_selectable() {
|
||||
@@ -752,9 +708,6 @@ impl ContextMenu {
|
||||
if let Some(callback) = &entry.documentation_aside {
|
||||
self.documentation_aside = Some((ix, callback.clone()));
|
||||
}
|
||||
if self.eager && !entry.disabled {
|
||||
(entry.handler)(context, window, cx)
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(ix)
|
||||
|
||||
@@ -2,6 +2,8 @@ use gpui::{ClickEvent, Corner, CursorStyle, Entity, Hsla, MouseButton};
|
||||
|
||||
use crate::{ContextMenu, PopoverMenu, prelude::*};
|
||||
|
||||
use super::PopoverMenuHandle;
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum DropdownStyle {
|
||||
#[default]
|
||||
@@ -22,6 +24,7 @@ pub struct DropdownMenu {
|
||||
menu: Entity<ContextMenu>,
|
||||
full_width: bool,
|
||||
disabled: bool,
|
||||
handle: Option<PopoverMenuHandle<ContextMenu>>,
|
||||
}
|
||||
|
||||
impl DropdownMenu {
|
||||
@@ -37,6 +40,7 @@ impl DropdownMenu {
|
||||
menu,
|
||||
full_width: false,
|
||||
disabled: false,
|
||||
handle: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +56,7 @@ impl DropdownMenu {
|
||||
menu,
|
||||
full_width: false,
|
||||
disabled: false,
|
||||
handle: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,6 +69,11 @@ impl DropdownMenu {
|
||||
self.full_width = full_width;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn handle(mut self, handle: PopoverMenuHandle<ContextMenu>) -> Self {
|
||||
self.handle = Some(handle);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Disableable for DropdownMenu {
|
||||
@@ -85,6 +95,7 @@ impl RenderOnce for DropdownMenu {
|
||||
.style(self.style),
|
||||
)
|
||||
.attach(Corner::BottomLeft)
|
||||
.when_some(self.handle.clone(), |el, handle| el.with_handle(handle))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,17 +170,11 @@ pub struct DropdownTriggerStyle {
|
||||
impl DropdownTriggerStyle {
|
||||
pub fn for_style(style: DropdownStyle, cx: &App) -> Self {
|
||||
let colors = cx.theme().colors();
|
||||
|
||||
if style == DropdownStyle::Solid {
|
||||
Self {
|
||||
// why is this editor_background?
|
||||
bg: colors.editor_background,
|
||||
}
|
||||
} else {
|
||||
Self {
|
||||
bg: colors.ghost_element_background,
|
||||
}
|
||||
}
|
||||
let bg = match style {
|
||||
DropdownStyle::Solid => colors.editor_background,
|
||||
DropdownStyle::Ghost => colors.ghost_element_background,
|
||||
};
|
||||
Self { bg }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ test-support = ["tempfile", "git2", "rand", "util_macros"]
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
async-fs.workspace = true
|
||||
async_zip.workspace = true
|
||||
collections.workspace = true
|
||||
dirs.workspace = true
|
||||
futures-lite.workspace = true
|
||||
@@ -36,6 +37,7 @@ take-until.workspace = true
|
||||
tempfile = { workspace = true, optional = true }
|
||||
unicase.workspace = true
|
||||
util_macros = { workspace = true, optional = true }
|
||||
walkdir.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod arc_cow;
|
||||
pub mod archive;
|
||||
pub mod command;
|
||||
pub mod fs;
|
||||
pub mod markdown;
|
||||
|
||||
@@ -156,6 +156,8 @@ pub struct DeploySearch {
|
||||
pub replace_enabled: bool,
|
||||
#[serde(default)]
|
||||
pub included_files: Option<String>,
|
||||
#[serde(default)]
|
||||
pub excluded_files: Option<String>,
|
||||
}
|
||||
|
||||
impl_actions!(
|
||||
@@ -203,6 +205,7 @@ impl DeploySearch {
|
||||
Self {
|
||||
replace_enabled: false,
|
||||
included_files: None,
|
||||
excluded_files: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3114,6 +3117,7 @@ fn default_render_tab_bar_buttons(
|
||||
DeploySearch {
|
||||
replace_enabled: false,
|
||||
included_files: None,
|
||||
excluded_files: None,
|
||||
}
|
||||
.boxed_clone(),
|
||||
)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
description = "The fast, collaborative code editor."
|
||||
edition.workspace = true
|
||||
name = "zed"
|
||||
version = "0.188.0"
|
||||
version = "0.189.0"
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
authors = ["Zed Team <hi@zed.dev>"]
|
||||
|
||||
@@ -45,7 +45,7 @@ pub fn app_menus() -> Vec<Menu> {
|
||||
#[cfg(target_os = "macos")]
|
||||
MenuItem::action("Show All", super::ShowAll),
|
||||
MenuItem::separator(),
|
||||
MenuItem::action("Quit", Quit),
|
||||
MenuItem::action("Quit Zed", Quit),
|
||||
],
|
||||
},
|
||||
Menu {
|
||||
|
||||
@@ -14,7 +14,7 @@ use license_detection::LICENSE_FILES_TO_CHECK;
|
||||
pub use license_detection::is_license_eligible_for_data_collection;
|
||||
pub use rate_completion_modal::*;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use arrayvec::ArrayVec;
|
||||
use client::{Client, UserStore};
|
||||
use collections::{HashMap, HashSet, VecDeque};
|
||||
@@ -23,7 +23,7 @@ use gpui::{
|
||||
App, AppContext as _, AsyncApp, Context, Entity, EntityId, Global, SemanticVersion,
|
||||
Subscription, Task, WeakEntity, actions,
|
||||
};
|
||||
use http_client::{HttpClient, Method};
|
||||
use http_client::{AsyncBody, HttpClient, Method, Request, Response};
|
||||
use input_excerpt::excerpt_for_cursor_position;
|
||||
use language::{
|
||||
Anchor, Buffer, BufferSnapshot, EditPreview, OffsetRangeExt, ToOffset, ToPoint, text_diff,
|
||||
@@ -54,8 +54,8 @@ use workspace::Workspace;
|
||||
use workspace::notifications::{ErrorMessagePrompt, NotificationId};
|
||||
use worktree::Worktree;
|
||||
use zed_llm_client::{
|
||||
EXPIRED_LLM_TOKEN_HEADER_NAME, MINIMUM_REQUIRED_VERSION_HEADER_NAME, PredictEditsBody,
|
||||
PredictEditsResponse, ZED_VERSION_HEADER_NAME,
|
||||
AcceptEditPredictionBody, EXPIRED_LLM_TOKEN_HEADER_NAME, MINIMUM_REQUIRED_VERSION_HEADER_NAME,
|
||||
PredictEditsBody, PredictEditsResponse, ZED_VERSION_HEADER_NAME,
|
||||
};
|
||||
|
||||
const CURSOR_MARKER: &'static str = "<|user_cursor_is_here|>";
|
||||
@@ -823,6 +823,74 @@ and then another
|
||||
}
|
||||
}
|
||||
|
||||
fn accept_edit_prediction(
|
||||
&mut self,
|
||||
request_id: InlineCompletionId,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let client = self.client.clone();
|
||||
let llm_token = self.llm_token.clone();
|
||||
let app_version = AppVersion::global(cx);
|
||||
cx.spawn(async move |this, cx| {
|
||||
let http_client = client.http_client();
|
||||
let mut response = llm_token_retry(&llm_token, &client, |token| {
|
||||
let request_builder = http_client::Request::builder().method(Method::POST);
|
||||
let request_builder =
|
||||
if let Ok(accept_prediction_url) = std::env::var("ZED_ACCEPT_PREDICTION_URL") {
|
||||
request_builder.uri(accept_prediction_url)
|
||||
} else {
|
||||
request_builder.uri(
|
||||
http_client
|
||||
.build_zed_llm_url("/predict_edits/accept", &[])?
|
||||
.as_ref(),
|
||||
)
|
||||
};
|
||||
Ok(request_builder
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Authorization", format!("Bearer {}", token))
|
||||
.header(ZED_VERSION_HEADER_NAME, app_version.to_string())
|
||||
.body(
|
||||
serde_json::to_string(&AcceptEditPredictionBody {
|
||||
request_id: request_id.0,
|
||||
})?
|
||||
.into(),
|
||||
)?)
|
||||
})
|
||||
.await?;
|
||||
|
||||
if let Some(minimum_required_version) = response
|
||||
.headers()
|
||||
.get(MINIMUM_REQUIRED_VERSION_HEADER_NAME)
|
||||
.and_then(|version| SemanticVersion::from_str(version.to_str().ok()?).ok())
|
||||
{
|
||||
if app_version < minimum_required_version {
|
||||
return Err(anyhow!(ZedUpdateRequiredError {
|
||||
minimum_version: minimum_required_version
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
if response.status().is_success() {
|
||||
if let Some(usage) = EditPredictionUsage::from_headers(response.headers()).ok() {
|
||||
this.update(cx, |this, cx| {
|
||||
this.last_usage = Some(usage);
|
||||
cx.notify();
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
let mut body = String::new();
|
||||
response.body_mut().read_to_string(&mut body).await?;
|
||||
Err(anyhow!(
|
||||
"error accepting edit prediction.\nStatus: {:?}\nBody: {}",
|
||||
response.status(),
|
||||
body
|
||||
))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn process_completion_response(
|
||||
prediction_response: PredictEditsResponse,
|
||||
buffer: Entity<Buffer>,
|
||||
@@ -1381,6 +1449,34 @@ impl ProviderDataCollection {
|
||||
}
|
||||
}
|
||||
|
||||
async fn llm_token_retry(
|
||||
llm_token: &LlmApiToken,
|
||||
client: &Arc<Client>,
|
||||
build_request: impl Fn(String) -> Result<Request<AsyncBody>>,
|
||||
) -> Result<Response<AsyncBody>> {
|
||||
let mut did_retry = false;
|
||||
let http_client = client.http_client();
|
||||
let mut token = llm_token.acquire(client).await?;
|
||||
loop {
|
||||
let request = build_request(token.clone())?;
|
||||
let response = http_client.send(request).await?;
|
||||
|
||||
if !did_retry
|
||||
&& !response.status().is_success()
|
||||
&& response
|
||||
.headers()
|
||||
.get(EXPIRED_LLM_TOKEN_HEADER_NAME)
|
||||
.is_some()
|
||||
{
|
||||
did_retry = true;
|
||||
token = llm_token.refresh(client).await?;
|
||||
continue;
|
||||
}
|
||||
|
||||
return Ok(response);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ZetaInlineCompletionProvider {
|
||||
zeta: Entity<Zeta>,
|
||||
pending_completions: ArrayVec<PendingCompletion, 2>,
|
||||
@@ -1597,7 +1693,18 @@ impl inline_completion::EditPredictionProvider for ZetaInlineCompletionProvider
|
||||
// Right now we don't support cycling.
|
||||
}
|
||||
|
||||
fn accept(&mut self, _cx: &mut Context<Self>) {
|
||||
fn accept(&mut self, cx: &mut Context<Self>) {
|
||||
let completion_id = self
|
||||
.current_completion
|
||||
.as_ref()
|
||||
.map(|completion| completion.completion.id);
|
||||
if let Some(completion_id) = completion_id {
|
||||
self.zeta
|
||||
.update(cx, |zeta, cx| {
|
||||
zeta.accept_edit_prediction(completion_id, cx)
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
self.pending_completions.clear();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Configuration
|
||||
|
||||
There are various aspects about the Agent Panel that you can customize.
|
||||
All of them can be seen by either visiting [the Configuring Zed page](/configuring-zed.md#agent) or by running the `zed: open default settings` action and searching for `"agent"`.
|
||||
All of them can be seen by either visiting [the Configuring Zed page](./configuring-zed.md#agent) or by running the `zed: open default settings` action and searching for `"agent"`.
|
||||
Alternatively, you can also visit the panel's Settings view by running the `agent: open configuration` action or going to the top-right menu and hitting "Settings".
|
||||
|
||||
## LLM Providers
|
||||
@@ -9,17 +9,17 @@ Alternatively, you can also visit the panel's Settings view by running the `agen
|
||||
Zed supports multiple large language model providers.
|
||||
Here's an overview of the supported providers and tool call support:
|
||||
|
||||
| Provider | Tool Use Supported |
|
||||
| ----------------------------------------------- | ------------------ |
|
||||
| [Anthropic](#anthropic) | ✅ |
|
||||
| [GitHub Copilot Chat](#github-copilot-chat) | In Some Cases |
|
||||
| [Google AI](#google-ai) | ✅ |
|
||||
| [Mistral](#mistral) | ✅ |
|
||||
| [Ollama](#ollama) | ✅ |
|
||||
| [OpenAI](#openai) | ✅ |
|
||||
| [DeepSeek](#deepseek) | 🚫 |
|
||||
| [OpenAI API Compatible](#openai-api-compatible) | 🚫 |
|
||||
| [LM Studio](#lmstudio) | 🚫 |
|
||||
| Provider | Tool Use Supported |
|
||||
| ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| [Anthropic](#anthropic) | ✅ |
|
||||
| [GitHub Copilot Chat](#github-copilot-chat) | For Some Models ([link](https://github.com/zed-industries/zed/blob/9e0330ba7d848755c9734bf456c716bddf0973f3/crates/language_models/src/provider/copilot_chat.rs#L189-L198)) |
|
||||
| [Google AI](#google-ai) | ✅ |
|
||||
| [Mistral](#mistral) | ✅ |
|
||||
| [Ollama](#ollama) | ✅ |
|
||||
| [OpenAI](#openai) | ✅ |
|
||||
| [DeepSeek](#deepseek) | 🚫 |
|
||||
| [OpenAI API Compatible](#openai-api-compatible) | 🚫 |
|
||||
| [LM Studio](#lmstudio) | 🚫 |
|
||||
|
||||
## Use Your Own Keys {#use-your-own-keys}
|
||||
|
||||
|
||||
@@ -191,6 +191,8 @@ let
|
||||
wayland
|
||||
]
|
||||
}";
|
||||
|
||||
NIX_OUTPATH_USED_AS_RANDOM_SEED = "norebuilds";
|
||||
};
|
||||
|
||||
# prevent nix from removing the "unused" wayland/gpu-lib rpaths
|
||||
|
||||
@@ -19,6 +19,7 @@ ahash = { version = "0.8", features = ["serde"] }
|
||||
aho-corasick = { version = "1" }
|
||||
anstream = { version = "0.6" }
|
||||
arrayvec = { version = "0.7", features = ["serde"] }
|
||||
async-compression = { version = "0.4", default-features = false, features = ["deflate", "deflate64", "futures-io", "gzip"] }
|
||||
async-std = { version = "1", features = ["attributes", "unstable"] }
|
||||
async-tungstenite = { version = "0.29", features = ["tokio-rustls-manual-roots"] }
|
||||
aws-config = { version = "1", features = ["behavior-version-latest"] }
|
||||
@@ -44,7 +45,9 @@ chrono = { version = "0.4", features = ["serde"] }
|
||||
clap = { version = "4", features = ["cargo", "derive", "string", "wrap_help"] }
|
||||
clap_builder = { version = "4", default-features = false, features = ["cargo", "color", "std", "string", "suggestions", "usage", "wrap_help"] }
|
||||
concurrent-queue = { version = "2" }
|
||||
cranelift-codegen = { version = "0.116", default-features = false, features = ["host-arch", "incremental-cache", "std", "timing", "unwind"] }
|
||||
crc32fast = { version = "1" }
|
||||
crossbeam-epoch = { version = "0.9" }
|
||||
crossbeam-utils = { version = "0.8" }
|
||||
deranged = { version = "0.4", default-features = false, features = ["powerfmt", "serde", "std"] }
|
||||
digest = { version = "0.10", features = ["mac", "oid", "std"] }
|
||||
@@ -95,6 +98,7 @@ prost-types = { version = "0.9" }
|
||||
rand-c38e5c1d305a1b54 = { package = "rand", version = "0.8", features = ["small_rng"] }
|
||||
rand_chacha = { version = "0.3" }
|
||||
rand_core = { version = "0.6", default-features = false, features = ["std"] }
|
||||
regalloc2 = { version = "0.11", features = ["checker", "enable-serde"] }
|
||||
regex = { version = "1" }
|
||||
regex-automata = { version = "0.4" }
|
||||
regex-syntax = { version = "0.8" }
|
||||
@@ -132,8 +136,8 @@ url = { version = "2", features = ["serde"] }
|
||||
uuid = { version = "1", features = ["serde", "v4", "v5", "v7"] }
|
||||
wasm-encoder = { version = "0.221", features = ["wasmparser"] }
|
||||
wasmparser = { version = "0.221" }
|
||||
wasmtime = { version = "29", default-features = false, features = ["async", "component-model", "cranelift", "demangle", "gc-drc", "parallel-compilation"] }
|
||||
wasmtime-cranelift = { version = "29", default-features = false, features = ["component-model", "gc-drc"] }
|
||||
wasmtime = { version = "29", default-features = false, features = ["async", "component-model", "cranelift", "demangle", "gc-drc", "incremental-cache", "parallel-compilation"] }
|
||||
wasmtime-cranelift = { version = "29", default-features = false, features = ["component-model", "gc-drc", "incremental-cache"] }
|
||||
wasmtime-environ = { version = "29", default-features = false, features = ["compile", "component-model", "demangle", "gc-drc"] }
|
||||
winnow = { version = "0.7", features = ["simd"] }
|
||||
|
||||
@@ -142,6 +146,7 @@ ahash = { version = "0.8", features = ["serde"] }
|
||||
aho-corasick = { version = "1" }
|
||||
anstream = { version = "0.6" }
|
||||
arrayvec = { version = "0.7", features = ["serde"] }
|
||||
async-compression = { version = "0.4", default-features = false, features = ["deflate", "deflate64", "futures-io", "gzip"] }
|
||||
async-std = { version = "1", features = ["attributes", "unstable"] }
|
||||
async-tungstenite = { version = "0.29", features = ["tokio-rustls-manual-roots"] }
|
||||
aws-config = { version = "1", features = ["behavior-version-latest"] }
|
||||
@@ -168,7 +173,9 @@ chrono = { version = "0.4", features = ["serde"] }
|
||||
clap = { version = "4", features = ["cargo", "derive", "string", "wrap_help"] }
|
||||
clap_builder = { version = "4", default-features = false, features = ["cargo", "color", "std", "string", "suggestions", "usage", "wrap_help"] }
|
||||
concurrent-queue = { version = "2" }
|
||||
cranelift-codegen = { version = "0.116", default-features = false, features = ["host-arch", "incremental-cache", "std", "timing", "unwind"] }
|
||||
crc32fast = { version = "1" }
|
||||
crossbeam-epoch = { version = "0.9" }
|
||||
crossbeam-utils = { version = "0.8" }
|
||||
deranged = { version = "0.4", default-features = false, features = ["powerfmt", "serde", "std"] }
|
||||
digest = { version = "0.10", features = ["mac", "oid", "std"] }
|
||||
@@ -224,6 +231,7 @@ quote = { version = "1" }
|
||||
rand-c38e5c1d305a1b54 = { package = "rand", version = "0.8", features = ["small_rng"] }
|
||||
rand_chacha = { version = "0.3" }
|
||||
rand_core = { version = "0.6", default-features = false, features = ["std"] }
|
||||
regalloc2 = { version = "0.11", features = ["checker", "enable-serde"] }
|
||||
regex = { version = "1" }
|
||||
regex-automata = { version = "0.4" }
|
||||
regex-syntax = { version = "0.8" }
|
||||
@@ -267,8 +275,8 @@ url = { version = "2", features = ["serde"] }
|
||||
uuid = { version = "1", features = ["serde", "v4", "v5", "v7"] }
|
||||
wasm-encoder = { version = "0.221", features = ["wasmparser"] }
|
||||
wasmparser = { version = "0.221" }
|
||||
wasmtime = { version = "29", default-features = false, features = ["async", "component-model", "cranelift", "demangle", "gc-drc", "parallel-compilation"] }
|
||||
wasmtime-cranelift = { version = "29", default-features = false, features = ["component-model", "gc-drc"] }
|
||||
wasmtime = { version = "29", default-features = false, features = ["async", "component-model", "cranelift", "demangle", "gc-drc", "incremental-cache", "parallel-compilation"] }
|
||||
wasmtime-cranelift = { version = "29", default-features = false, features = ["component-model", "gc-drc", "incremental-cache"] }
|
||||
wasmtime-environ = { version = "29", default-features = false, features = ["compile", "component-model", "demangle", "gc-drc"] }
|
||||
winnow = { version = "0.7", features = ["simd"] }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user