Compare commits
26 Commits
agent-perf
...
fix_devcon
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb3e3d01dd | ||
|
|
251033f88f | ||
|
|
9f90c1a1b7 | ||
|
|
d43cc46288 | ||
|
|
fdb8e71b43 | ||
|
|
6bc433ed43 | ||
|
|
1281f4672c | ||
|
|
ed705c0cbc | ||
|
|
8980333e23 | ||
|
|
acee48bfda | ||
|
|
71298e6949 | ||
|
|
07ada58466 | ||
|
|
dd521a96fb | ||
|
|
f9d9721b93 | ||
|
|
cff3ac6f93 | ||
|
|
746b76488c | ||
|
|
397fcf6083 | ||
|
|
9adb3e1daa | ||
|
|
1469d94683 | ||
|
|
3b626c8ac1 | ||
|
|
3dc0614dba | ||
|
|
045e154915 | ||
|
|
dc72e1c4ba | ||
|
|
0884305e43 | ||
|
|
83449293b6 | ||
|
|
213cb30445 |
1
.github/actionlint.yml
vendored
1
.github/actionlint.yml
vendored
@@ -25,6 +25,7 @@ self-hosted-runner:
|
||||
- namespace-profile-32x64-ubuntu-2204
|
||||
# Namespace Ubuntu 24.04 (like ubuntu-latest)
|
||||
- namespace-profile-2x4-ubuntu-2404
|
||||
- namespace-profile-8x32-ubuntu-2404
|
||||
# Namespace Limited Preview
|
||||
- namespace-profile-8x16-ubuntu-2004-arm-m4
|
||||
- namespace-profile-8x32-ubuntu-2004-arm-m4
|
||||
|
||||
4
.github/workflows/extension_tests.yml
vendored
4
.github/workflows/extension_tests.yml
vendored
@@ -51,7 +51,7 @@ jobs:
|
||||
needs:
|
||||
- orchestrate
|
||||
if: needs.orchestrate.outputs.check_rust == 'true'
|
||||
runs-on: namespace-profile-16x32-ubuntu-2204
|
||||
runs-on: namespace-profile-4x8-ubuntu-2204
|
||||
steps:
|
||||
- name: steps::checkout_repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
@@ -79,7 +79,7 @@ jobs:
|
||||
needs:
|
||||
- orchestrate
|
||||
if: needs.orchestrate.outputs.check_extension == 'true'
|
||||
runs-on: namespace-profile-2x4-ubuntu-2404
|
||||
runs-on: namespace-profile-8x32-ubuntu-2404
|
||||
steps:
|
||||
- name: steps::checkout_repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
|
||||
@@ -23,7 +23,6 @@ In particular we love PRs that are:
|
||||
|
||||
If you're looking for concrete ideas:
|
||||
|
||||
- [Curated board of issues](https://github.com/orgs/zed-industries/projects/69) suitable for everyone from first-time contributors to seasoned community champions.
|
||||
- [Triaged bugs with confirmed steps to reproduce](https://github.com/zed-industries/zed/issues?q=is%3Aissue%20state%3Aopen%20type%3ABug%20label%3Astate%3Areproducible).
|
||||
- [Area labels](https://github.com/zed-industries/zed/labels?q=area%3A*) to browse bugs in a specific part of the product you care about (after clicking on an area label, add type:Bug to the search).
|
||||
|
||||
|
||||
15
Cargo.lock
generated
15
Cargo.lock
generated
@@ -5212,6 +5212,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"arrayvec",
|
||||
"brotli",
|
||||
"buffer_diff",
|
||||
"client",
|
||||
"clock",
|
||||
"cloud_api_types",
|
||||
@@ -5249,7 +5250,9 @@ dependencies = [
|
||||
"strum 0.27.2",
|
||||
"telemetry",
|
||||
"telemetry_events",
|
||||
"text",
|
||||
"thiserror 2.0.17",
|
||||
"time",
|
||||
"ui",
|
||||
"util",
|
||||
"uuid",
|
||||
@@ -5354,8 +5357,10 @@ dependencies = [
|
||||
"anyhow",
|
||||
"buffer_diff",
|
||||
"client",
|
||||
"clock",
|
||||
"cloud_llm_client",
|
||||
"codestral",
|
||||
"collections",
|
||||
"command_palette_hooks",
|
||||
"copilot",
|
||||
"edit_prediction",
|
||||
@@ -5364,18 +5369,20 @@ dependencies = [
|
||||
"feature_flags",
|
||||
"fs",
|
||||
"futures 0.3.31",
|
||||
"git",
|
||||
"gpui",
|
||||
"indoc",
|
||||
"language",
|
||||
"log",
|
||||
"language_model",
|
||||
"lsp",
|
||||
"markdown",
|
||||
"menu",
|
||||
"multi_buffer",
|
||||
"paths",
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
"regex",
|
||||
"release_channel",
|
||||
"semver",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"supermaven",
|
||||
@@ -5388,6 +5395,7 @@ dependencies = [
|
||||
"workspace",
|
||||
"zed_actions",
|
||||
"zeta_prompt",
|
||||
"zlog",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8645,6 +8653,7 @@ dependencies = [
|
||||
"extension",
|
||||
"gpui",
|
||||
"language",
|
||||
"lsp",
|
||||
"paths",
|
||||
"project",
|
||||
"schemars",
|
||||
@@ -20969,7 +20978,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_proto"
|
||||
version = "0.3.0"
|
||||
version = "0.3.1"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.7.0",
|
||||
]
|
||||
|
||||
@@ -241,6 +241,7 @@
|
||||
"ctrl-alt-l": "agent::OpenRulesLibrary",
|
||||
"ctrl-i": "agent::ToggleProfileSelector",
|
||||
"ctrl-alt-/": "agent::ToggleModelSelector",
|
||||
"alt-tab": "agent::CycleFavoriteModels",
|
||||
"ctrl-shift-j": "agent::ToggleNavigationMenu",
|
||||
"ctrl-alt-i": "agent::ToggleOptionsMenu",
|
||||
"ctrl-alt-shift-n": "agent::ToggleNewThreadMenu",
|
||||
@@ -253,7 +254,6 @@
|
||||
"ctrl-y": "agent::AllowOnce",
|
||||
"ctrl-alt-y": "agent::AllowAlways",
|
||||
"ctrl-alt-z": "agent::RejectOnce",
|
||||
"alt-tab": "agent::CycleFavoriteModels",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -285,38 +285,6 @@
|
||||
"ctrl-alt-t": "agent::NewThread",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "MessageEditor && !Picker > Editor && !use_modifier_to_send",
|
||||
"bindings": {
|
||||
"enter": "agent::Chat",
|
||||
"ctrl-enter": "agent::ChatWithFollow",
|
||||
"ctrl-i": "agent::ToggleProfileSelector",
|
||||
"shift-ctrl-r": "agent::OpenAgentDiff",
|
||||
"ctrl-shift-y": "agent::KeepAll",
|
||||
"ctrl-shift-n": "agent::RejectAll",
|
||||
"ctrl-shift-v": "agent::PasteRaw",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "MessageEditor && !Picker > Editor && use_modifier_to_send",
|
||||
"bindings": {
|
||||
"ctrl-enter": "agent::Chat",
|
||||
"enter": "editor::Newline",
|
||||
"ctrl-i": "agent::ToggleProfileSelector",
|
||||
"shift-ctrl-r": "agent::OpenAgentDiff",
|
||||
"ctrl-shift-y": "agent::KeepAll",
|
||||
"ctrl-shift-n": "agent::RejectAll",
|
||||
"ctrl-shift-v": "agent::PasteRaw",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "EditMessageEditor > Editor",
|
||||
"bindings": {
|
||||
"escape": "menu::Cancel",
|
||||
"enter": "menu::Confirm",
|
||||
"alt-enter": "editor::Newline",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "AgentFeedbackMessageEditor > Editor",
|
||||
"bindings": {
|
||||
@@ -331,14 +299,25 @@
|
||||
"ctrl-enter": "menu::Confirm",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "AcpThread > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-enter": "agent::ChatWithFollow",
|
||||
"ctrl-i": "agent::ToggleProfileSelector",
|
||||
"ctrl-shift-r": "agent::OpenAgentDiff",
|
||||
"ctrl-shift-y": "agent::KeepAll",
|
||||
"ctrl-shift-n": "agent::RejectAll",
|
||||
"ctrl-shift-v": "agent::PasteRaw",
|
||||
"shift-tab": "agent::CycleModeSelector",
|
||||
"alt-tab": "agent::CycleFavoriteModels",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "AcpThread > Editor && !use_modifier_to_send",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"enter": "agent::Chat",
|
||||
"shift-ctrl-r": "agent::OpenAgentDiff",
|
||||
"ctrl-shift-y": "agent::KeepAll",
|
||||
"ctrl-shift-n": "agent::RejectAll",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -346,11 +325,7 @@
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-enter": "agent::Chat",
|
||||
"shift-ctrl-r": "agent::OpenAgentDiff",
|
||||
"ctrl-shift-y": "agent::KeepAll",
|
||||
"ctrl-shift-n": "agent::RejectAll",
|
||||
"shift-tab": "agent::CycleModeSelector",
|
||||
"alt-tab": "agent::CycleFavoriteModels",
|
||||
"enter": "editor::Newline",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -817,7 +792,7 @@
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "PromptEditor",
|
||||
"context": "InlineAssistant",
|
||||
"bindings": {
|
||||
"ctrl-[": "agent::CyclePreviousInlineAssist",
|
||||
"ctrl-]": "agent::CycleNextInlineAssist",
|
||||
|
||||
@@ -282,6 +282,7 @@
|
||||
"cmd-alt-p": "agent::ManageProfiles",
|
||||
"cmd-i": "agent::ToggleProfileSelector",
|
||||
"cmd-alt-/": "agent::ToggleModelSelector",
|
||||
"alt-tab": "agent::CycleFavoriteModels",
|
||||
"cmd-shift-j": "agent::ToggleNavigationMenu",
|
||||
"cmd-alt-m": "agent::ToggleOptionsMenu",
|
||||
"cmd-alt-shift-n": "agent::ToggleNewThreadMenu",
|
||||
@@ -294,7 +295,6 @@
|
||||
"cmd-y": "agent::AllowOnce",
|
||||
"cmd-alt-y": "agent::AllowAlways",
|
||||
"cmd-alt-z": "agent::RejectOnce",
|
||||
"alt-tab": "agent::CycleFavoriteModels",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -326,41 +326,6 @@
|
||||
"cmd-alt-t": "agent::NewThread",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "MessageEditor && !Picker > Editor && !use_modifier_to_send",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"enter": "agent::Chat",
|
||||
"cmd-enter": "agent::ChatWithFollow",
|
||||
"cmd-i": "agent::ToggleProfileSelector",
|
||||
"shift-ctrl-r": "agent::OpenAgentDiff",
|
||||
"cmd-shift-y": "agent::KeepAll",
|
||||
"cmd-shift-n": "agent::RejectAll",
|
||||
"cmd-shift-v": "agent::PasteRaw",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "MessageEditor && !Picker > Editor && use_modifier_to_send",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-enter": "agent::Chat",
|
||||
"enter": "editor::Newline",
|
||||
"cmd-i": "agent::ToggleProfileSelector",
|
||||
"shift-ctrl-r": "agent::OpenAgentDiff",
|
||||
"cmd-shift-y": "agent::KeepAll",
|
||||
"cmd-shift-n": "agent::RejectAll",
|
||||
"cmd-shift-v": "agent::PasteRaw",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "EditMessageEditor > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"escape": "menu::Cancel",
|
||||
"enter": "menu::Confirm",
|
||||
"alt-enter": "editor::Newline",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "AgentFeedbackMessageEditor > Editor",
|
||||
"use_key_equivalents": true,
|
||||
@@ -382,16 +347,25 @@
|
||||
"cmd-enter": "menu::Confirm",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "AcpThread > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"shift-ctrl-r": "agent::OpenAgentDiff",
|
||||
"cmd-shift-y": "agent::KeepAll",
|
||||
"cmd-shift-n": "agent::RejectAll",
|
||||
"cmd-enter": "agent::ChatWithFollow",
|
||||
"cmd-shift-v": "agent::PasteRaw",
|
||||
"cmd-i": "agent::ToggleProfileSelector",
|
||||
"shift-tab": "agent::CycleModeSelector",
|
||||
"alt-tab": "agent::CycleFavoriteModels",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "AcpThread > Editor && !use_modifier_to_send",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"enter": "agent::Chat",
|
||||
"shift-ctrl-r": "agent::OpenAgentDiff",
|
||||
"cmd-shift-y": "agent::KeepAll",
|
||||
"cmd-shift-n": "agent::RejectAll",
|
||||
"shift-tab": "agent::CycleModeSelector",
|
||||
"alt-tab": "agent::CycleFavoriteModels",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -399,11 +373,7 @@
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-enter": "agent::Chat",
|
||||
"shift-ctrl-r": "agent::OpenAgentDiff",
|
||||
"cmd-shift-y": "agent::KeepAll",
|
||||
"cmd-shift-n": "agent::RejectAll",
|
||||
"shift-tab": "agent::CycleModeSelector",
|
||||
"alt-tab": "agent::CycleFavoriteModels",
|
||||
"enter": "editor::Newline",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -883,7 +853,7 @@
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "PromptEditor",
|
||||
"context": "InlineAssistant > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-alt-/": "agent::ToggleModelSelector",
|
||||
|
||||
@@ -241,6 +241,7 @@
|
||||
"shift-alt-l": "agent::OpenRulesLibrary",
|
||||
"shift-alt-p": "agent::ManageProfiles",
|
||||
"ctrl-i": "agent::ToggleProfileSelector",
|
||||
"alt-tab": "agent::CycleFavoriteModels",
|
||||
"shift-alt-/": "agent::ToggleModelSelector",
|
||||
"shift-alt-j": "agent::ToggleNavigationMenu",
|
||||
"shift-alt-i": "agent::ToggleOptionsMenu",
|
||||
@@ -254,7 +255,6 @@
|
||||
"shift-alt-a": "agent::AllowOnce",
|
||||
"ctrl-alt-y": "agent::AllowAlways",
|
||||
"shift-alt-z": "agent::RejectOnce",
|
||||
"alt-tab": "agent::CycleFavoriteModels",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -287,41 +287,6 @@
|
||||
"ctrl-alt-t": "agent::NewThread",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "MessageEditor && !Picker > Editor && !use_modifier_to_send",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"enter": "agent::Chat",
|
||||
"ctrl-enter": "agent::ChatWithFollow",
|
||||
"ctrl-i": "agent::ToggleProfileSelector",
|
||||
"ctrl-shift-r": "agent::OpenAgentDiff",
|
||||
"ctrl-shift-y": "agent::KeepAll",
|
||||
"ctrl-shift-n": "agent::RejectAll",
|
||||
"ctrl-shift-v": "agent::PasteRaw",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "MessageEditor && !Picker > Editor && use_modifier_to_send",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-enter": "agent::Chat",
|
||||
"enter": "editor::Newline",
|
||||
"ctrl-i": "agent::ToggleProfileSelector",
|
||||
"ctrl-shift-r": "agent::OpenAgentDiff",
|
||||
"ctrl-shift-y": "agent::KeepAll",
|
||||
"ctrl-shift-n": "agent::RejectAll",
|
||||
"ctrl-shift-v": "agent::PasteRaw",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "EditMessageEditor > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"escape": "menu::Cancel",
|
||||
"enter": "menu::Confirm",
|
||||
"alt-enter": "editor::Newline",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "AgentFeedbackMessageEditor > Editor",
|
||||
"use_key_equivalents": true,
|
||||
@@ -337,16 +302,25 @@
|
||||
"ctrl-enter": "menu::Confirm",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "AcpThread > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-enter": "agent::ChatWithFollow",
|
||||
"ctrl-i": "agent::ToggleProfileSelector",
|
||||
"ctrl-shift-r": "agent::OpenAgentDiff",
|
||||
"ctrl-shift-y": "agent::KeepAll",
|
||||
"ctrl-shift-n": "agent::RejectAll",
|
||||
"ctrl-shift-v": "agent::PasteRaw",
|
||||
"shift-tab": "agent::CycleModeSelector",
|
||||
"alt-tab": "agent::CycleFavoriteModels",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "AcpThread > Editor && !use_modifier_to_send",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"enter": "agent::Chat",
|
||||
"ctrl-shift-r": "agent::OpenAgentDiff",
|
||||
"ctrl-shift-y": "agent::KeepAll",
|
||||
"ctrl-shift-n": "agent::RejectAll",
|
||||
"shift-tab": "agent::CycleModeSelector",
|
||||
"alt-tab": "agent::CycleFavoriteModels",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -354,11 +328,7 @@
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-enter": "agent::Chat",
|
||||
"ctrl-shift-r": "agent::OpenAgentDiff",
|
||||
"ctrl-shift-y": "agent::KeepAll",
|
||||
"ctrl-shift-n": "agent::RejectAll",
|
||||
"shift-tab": "agent::CycleModeSelector",
|
||||
"alt-tab": "agent::CycleFavoriteModels",
|
||||
"enter": "editor::Newline",
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -826,7 +796,7 @@
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "PromptEditor",
|
||||
"context": "InlineAssistant",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-[": "agent::CyclePreviousInlineAssist",
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "InlineAssistEditor",
|
||||
"context": "InlineAssistant > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-shift-backspace": "editor::Cancel",
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "InlineAssistEditor",
|
||||
"context": "InlineAssistant > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-shift-backspace": "editor::Cancel",
|
||||
|
||||
@@ -202,12 +202,6 @@ pub trait AgentModelSelector: 'static {
|
||||
fn should_render_footer(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Whether this selector supports the favorites feature.
|
||||
/// Only the native agent uses the model ID format that maps to settings.
|
||||
fn supports_favorites(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Icon for a model in the model selector.
|
||||
|
||||
@@ -1167,10 +1167,6 @@ impl acp_thread::AgentModelSelector for NativeAgentModelSelector {
|
||||
fn should_render_footer(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn supports_favorites(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl acp_thread::AgentConnection for NativeAgentConnection {
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
use std::{any::Any, path::Path, rc::Rc, sync::Arc};
|
||||
|
||||
use agent_client_protocol as acp;
|
||||
use agent_servers::{AgentServer, AgentServerDelegate};
|
||||
use agent_settings::AgentSettings;
|
||||
use anyhow::Result;
|
||||
use collections::HashSet;
|
||||
use fs::Fs;
|
||||
use gpui::{App, Entity, SharedString, Task};
|
||||
use prompt_store::PromptStore;
|
||||
use settings::{LanguageModelSelection, Settings as _, update_settings_file};
|
||||
|
||||
use crate::{HistoryStore, NativeAgent, NativeAgentConnection, templates::Templates};
|
||||
|
||||
@@ -71,6 +75,38 @@ impl AgentServer for NativeAgentServer {
|
||||
fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
|
||||
self
|
||||
}
|
||||
|
||||
fn favorite_model_ids(&self, cx: &mut App) -> HashSet<acp::ModelId> {
|
||||
AgentSettings::get_global(cx).favorite_model_ids()
|
||||
}
|
||||
|
||||
fn toggle_favorite_model(
|
||||
&self,
|
||||
model_id: acp::ModelId,
|
||||
should_be_favorite: bool,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &App,
|
||||
) {
|
||||
let selection = model_id_to_selection(&model_id);
|
||||
update_settings_file(fs, cx, move |settings, _| {
|
||||
let agent = settings.agent.get_or_insert_default();
|
||||
if should_be_favorite {
|
||||
agent.add_favorite_model(selection.clone());
|
||||
} else {
|
||||
agent.remove_favorite_model(&selection);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a ModelId (e.g. "anthropic/claude-3-5-sonnet") to a LanguageModelSelection.
|
||||
fn model_id_to_selection(model_id: &acp::ModelId) -> LanguageModelSelection {
|
||||
let id = model_id.0.as_ref();
|
||||
let (provider, model) = id.split_once('/').unwrap_or(("", id));
|
||||
LanguageModelSelection {
|
||||
provider: provider.to_owned().into(),
|
||||
model: model.to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -4,6 +4,8 @@ mod codex;
|
||||
mod custom;
|
||||
mod gemini;
|
||||
|
||||
use collections::HashSet;
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub mod e2e_tests;
|
||||
|
||||
@@ -56,9 +58,19 @@ impl AgentServerDelegate {
|
||||
pub trait AgentServer: Send {
|
||||
fn logo(&self) -> ui::IconName;
|
||||
fn name(&self) -> SharedString;
|
||||
fn connect(
|
||||
&self,
|
||||
root_dir: Option<&Path>,
|
||||
delegate: AgentServerDelegate,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>>;
|
||||
|
||||
fn into_any(self: Rc<Self>) -> Rc<dyn Any>;
|
||||
|
||||
fn default_mode(&self, _cx: &mut App) -> Option<agent_client_protocol::SessionModeId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn set_default_mode(
|
||||
&self,
|
||||
_mode_id: Option<agent_client_protocol::SessionModeId>,
|
||||
@@ -79,14 +91,18 @@ pub trait AgentServer: Send {
|
||||
) {
|
||||
}
|
||||
|
||||
fn connect(
|
||||
&self,
|
||||
root_dir: Option<&Path>,
|
||||
delegate: AgentServerDelegate,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>>;
|
||||
fn favorite_model_ids(&self, _cx: &mut App) -> HashSet<agent_client_protocol::ModelId> {
|
||||
HashSet::default()
|
||||
}
|
||||
|
||||
fn into_any(self: Rc<Self>) -> Rc<dyn Any>;
|
||||
fn toggle_favorite_model(
|
||||
&self,
|
||||
_model_id: agent_client_protocol::ModelId,
|
||||
_should_be_favorite: bool,
|
||||
_fs: Arc<dyn Fs>,
|
||||
_cx: &App,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
impl dyn AgentServer {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use agent_client_protocol as acp;
|
||||
use collections::HashSet;
|
||||
use fs::Fs;
|
||||
use settings::{SettingsStore, update_settings_file};
|
||||
use std::path::Path;
|
||||
@@ -72,6 +73,48 @@ impl AgentServer for ClaudeCode {
|
||||
});
|
||||
}
|
||||
|
||||
fn favorite_model_ids(&self, cx: &mut App) -> HashSet<acp::ModelId> {
|
||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings.get::<AllAgentServersSettings>(None).claude.clone()
|
||||
});
|
||||
|
||||
settings
|
||||
.as_ref()
|
||||
.map(|s| {
|
||||
s.favorite_models
|
||||
.iter()
|
||||
.map(|id| acp::ModelId::new(id.clone()))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn toggle_favorite_model(
|
||||
&self,
|
||||
model_id: acp::ModelId,
|
||||
should_be_favorite: bool,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &App,
|
||||
) {
|
||||
update_settings_file(fs, cx, move |settings, _| {
|
||||
let favorite_models = &mut settings
|
||||
.agent_servers
|
||||
.get_or_insert_default()
|
||||
.claude
|
||||
.get_or_insert_default()
|
||||
.favorite_models;
|
||||
|
||||
let model_id_str = model_id.to_string();
|
||||
if should_be_favorite {
|
||||
if !favorite_models.contains(&model_id_str) {
|
||||
favorite_models.push(model_id_str);
|
||||
}
|
||||
} else {
|
||||
favorite_models.retain(|id| id != &model_id_str);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn connect(
|
||||
&self,
|
||||
root_dir: Option<&Path>,
|
||||
|
||||
@@ -5,6 +5,7 @@ use std::{any::Any, path::Path};
|
||||
use acp_thread::AgentConnection;
|
||||
use agent_client_protocol as acp;
|
||||
use anyhow::{Context as _, Result};
|
||||
use collections::HashSet;
|
||||
use fs::Fs;
|
||||
use gpui::{App, AppContext as _, SharedString, Task};
|
||||
use project::agent_server_store::{AllAgentServersSettings, CODEX_NAME};
|
||||
@@ -73,6 +74,48 @@ impl AgentServer for Codex {
|
||||
});
|
||||
}
|
||||
|
||||
fn favorite_model_ids(&self, cx: &mut App) -> HashSet<acp::ModelId> {
|
||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings.get::<AllAgentServersSettings>(None).codex.clone()
|
||||
});
|
||||
|
||||
settings
|
||||
.as_ref()
|
||||
.map(|s| {
|
||||
s.favorite_models
|
||||
.iter()
|
||||
.map(|id| acp::ModelId::new(id.clone()))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn toggle_favorite_model(
|
||||
&self,
|
||||
model_id: acp::ModelId,
|
||||
should_be_favorite: bool,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &App,
|
||||
) {
|
||||
update_settings_file(fs, cx, move |settings, _| {
|
||||
let favorite_models = &mut settings
|
||||
.agent_servers
|
||||
.get_or_insert_default()
|
||||
.codex
|
||||
.get_or_insert_default()
|
||||
.favorite_models;
|
||||
|
||||
let model_id_str = model_id.to_string();
|
||||
if should_be_favorite {
|
||||
if !favorite_models.contains(&model_id_str) {
|
||||
favorite_models.push(model_id_str);
|
||||
}
|
||||
} else {
|
||||
favorite_models.retain(|id| id != &model_id_str);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn connect(
|
||||
&self,
|
||||
root_dir: Option<&Path>,
|
||||
|
||||
@@ -2,6 +2,7 @@ use crate::{AgentServer, AgentServerDelegate, load_proxy_env};
|
||||
use acp_thread::AgentConnection;
|
||||
use agent_client_protocol as acp;
|
||||
use anyhow::{Context as _, Result};
|
||||
use collections::HashSet;
|
||||
use fs::Fs;
|
||||
use gpui::{App, AppContext as _, SharedString, Task};
|
||||
use project::agent_server_store::{AllAgentServersSettings, ExternalAgentServerName};
|
||||
@@ -54,6 +55,7 @@ impl AgentServer for CustomAgentServer {
|
||||
.or_insert_with(|| settings::CustomAgentServerSettings::Extension {
|
||||
default_model: None,
|
||||
default_mode: None,
|
||||
favorite_models: Vec::new(),
|
||||
});
|
||||
|
||||
match settings {
|
||||
@@ -90,6 +92,7 @@ impl AgentServer for CustomAgentServer {
|
||||
.or_insert_with(|| settings::CustomAgentServerSettings::Extension {
|
||||
default_model: None,
|
||||
default_mode: None,
|
||||
favorite_models: Vec::new(),
|
||||
});
|
||||
|
||||
match settings {
|
||||
@@ -101,6 +104,66 @@ impl AgentServer for CustomAgentServer {
|
||||
});
|
||||
}
|
||||
|
||||
fn favorite_model_ids(&self, cx: &mut App) -> HashSet<acp::ModelId> {
|
||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings
|
||||
.get::<AllAgentServersSettings>(None)
|
||||
.custom
|
||||
.get(&self.name())
|
||||
.cloned()
|
||||
});
|
||||
|
||||
settings
|
||||
.as_ref()
|
||||
.map(|s| {
|
||||
s.favorite_models()
|
||||
.iter()
|
||||
.map(|id| acp::ModelId::new(id.clone()))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn toggle_favorite_model(
|
||||
&self,
|
||||
model_id: acp::ModelId,
|
||||
should_be_favorite: bool,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &App,
|
||||
) {
|
||||
let name = self.name();
|
||||
update_settings_file(fs, cx, move |settings, _| {
|
||||
let settings = settings
|
||||
.agent_servers
|
||||
.get_or_insert_default()
|
||||
.custom
|
||||
.entry(name.clone())
|
||||
.or_insert_with(|| settings::CustomAgentServerSettings::Extension {
|
||||
default_model: None,
|
||||
default_mode: None,
|
||||
favorite_models: Vec::new(),
|
||||
});
|
||||
|
||||
let favorite_models = match settings {
|
||||
settings::CustomAgentServerSettings::Custom {
|
||||
favorite_models, ..
|
||||
}
|
||||
| settings::CustomAgentServerSettings::Extension {
|
||||
favorite_models, ..
|
||||
} => favorite_models,
|
||||
};
|
||||
|
||||
let model_id_str = model_id.to_string();
|
||||
if should_be_favorite {
|
||||
if !favorite_models.contains(&model_id_str) {
|
||||
favorite_models.push(model_id_str);
|
||||
}
|
||||
} else {
|
||||
favorite_models.retain(|id| id != &model_id_str);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn connect(
|
||||
&self,
|
||||
root_dir: Option<&Path>,
|
||||
|
||||
@@ -460,6 +460,7 @@ pub async fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> {
|
||||
ignore_system_version: None,
|
||||
default_mode: None,
|
||||
default_model: None,
|
||||
favorite_models: vec![],
|
||||
}),
|
||||
gemini: Some(crate::gemini::tests::local_command().into()),
|
||||
codex: Some(BuiltinAgentServerSettings {
|
||||
@@ -469,6 +470,7 @@ pub async fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> {
|
||||
ignore_system_version: None,
|
||||
default_mode: None,
|
||||
default_model: None,
|
||||
favorite_models: vec![],
|
||||
}),
|
||||
custom: collections::HashMap::default(),
|
||||
},
|
||||
|
||||
@@ -31,7 +31,7 @@ use rope::Point;
|
||||
use settings::Settings;
|
||||
use std::{cell::RefCell, fmt::Write, rc::Rc, sync::Arc};
|
||||
use theme::ThemeSettings;
|
||||
use ui::prelude::*;
|
||||
use ui::{ContextMenu, prelude::*};
|
||||
use util::{ResultExt, debug_panic};
|
||||
use workspace::{CollaboratorId, Workspace};
|
||||
use zed_actions::agent::{Chat, PasteRaw};
|
||||
@@ -132,6 +132,21 @@ impl MessageEditor {
|
||||
placement: Some(ContextMenuPlacement::Above),
|
||||
});
|
||||
editor.register_addon(MessageEditorAddon::new());
|
||||
|
||||
editor.set_custom_context_menu(|editor, _point, window, cx| {
|
||||
let has_selection = editor.has_non_empty_selection(&editor.display_snapshot(cx));
|
||||
|
||||
Some(ContextMenu::build(window, cx, |menu, _, _| {
|
||||
menu.action("Cut", Box::new(editor::actions::Cut))
|
||||
.action_disabled_when(
|
||||
!has_selection,
|
||||
"Copy",
|
||||
Box::new(editor::actions::Copy),
|
||||
)
|
||||
.action("Paste", Box::new(editor::actions::Paste))
|
||||
}))
|
||||
});
|
||||
|
||||
editor
|
||||
});
|
||||
let mention_set =
|
||||
|
||||
@@ -3,19 +3,19 @@ use std::{cmp::Reverse, rc::Rc, sync::Arc};
|
||||
use acp_thread::{AgentModelIcon, AgentModelInfo, AgentModelList, AgentModelSelector};
|
||||
use agent_client_protocol::ModelId;
|
||||
use agent_servers::AgentServer;
|
||||
use agent_settings::AgentSettings;
|
||||
use anyhow::Result;
|
||||
use collections::{HashSet, IndexMap};
|
||||
use fs::Fs;
|
||||
use futures::FutureExt;
|
||||
use fuzzy::{StringMatchCandidate, match_strings};
|
||||
use gpui::{
|
||||
Action, AsyncWindowContext, BackgroundExecutor, DismissEvent, FocusHandle, Task, WeakEntity,
|
||||
Action, AsyncWindowContext, BackgroundExecutor, DismissEvent, FocusHandle, Subscription, Task,
|
||||
WeakEntity,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use ordered_float::OrderedFloat;
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use settings::Settings;
|
||||
use settings::SettingsStore;
|
||||
use ui::{DocumentationAside, DocumentationEdge, DocumentationSide, IntoElement, prelude::*};
|
||||
use util::ResultExt;
|
||||
use zed_actions::agent::OpenSettings;
|
||||
@@ -54,7 +54,9 @@ pub struct AcpModelPickerDelegate {
|
||||
selected_index: usize,
|
||||
selected_description: Option<(usize, SharedString, bool)>,
|
||||
selected_model: Option<AgentModelInfo>,
|
||||
favorites: HashSet<ModelId>,
|
||||
_refresh_models_task: Task<()>,
|
||||
_settings_subscription: Subscription,
|
||||
focus_handle: FocusHandle,
|
||||
}
|
||||
|
||||
@@ -102,6 +104,19 @@ impl AcpModelPickerDelegate {
|
||||
})
|
||||
};
|
||||
|
||||
let agent_server_for_subscription = agent_server.clone();
|
||||
let settings_subscription =
|
||||
cx.observe_global_in::<SettingsStore>(window, move |picker, window, cx| {
|
||||
// Only refresh if the favorites actually changed to avoid redundant work
|
||||
// when other settings are modified (e.g., user editing settings.json)
|
||||
let new_favorites = agent_server_for_subscription.favorite_model_ids(cx);
|
||||
if new_favorites != picker.delegate.favorites {
|
||||
picker.delegate.favorites = new_favorites;
|
||||
picker.refresh(window, cx);
|
||||
}
|
||||
});
|
||||
let favorites = agent_server.favorite_model_ids(cx);
|
||||
|
||||
Self {
|
||||
selector,
|
||||
agent_server,
|
||||
@@ -111,7 +126,9 @@ impl AcpModelPickerDelegate {
|
||||
selected_model: None,
|
||||
selected_index: 0,
|
||||
selected_description: None,
|
||||
favorites,
|
||||
_refresh_models_task: refresh_models_task,
|
||||
_settings_subscription: settings_subscription,
|
||||
focus_handle,
|
||||
}
|
||||
}
|
||||
@@ -120,40 +137,37 @@ impl AcpModelPickerDelegate {
|
||||
self.selected_model.as_ref()
|
||||
}
|
||||
|
||||
pub fn favorites_count(&self) -> usize {
|
||||
self.favorites.len()
|
||||
}
|
||||
|
||||
pub fn cycle_favorite_models(&mut self, window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
if !self.selector.supports_favorites() {
|
||||
if self.favorites.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let favorites = AgentSettings::get_global(cx).favorite_model_ids();
|
||||
|
||||
if favorites.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(models) = self.models.clone() else {
|
||||
let Some(models) = &self.models else {
|
||||
return;
|
||||
};
|
||||
|
||||
let all_models: Vec<AgentModelInfo> = match models {
|
||||
AgentModelList::Flat(list) => list,
|
||||
AgentModelList::Grouped(index_map) => index_map
|
||||
.into_values()
|
||||
.flatten()
|
||||
.collect::<Vec<AgentModelInfo>>(),
|
||||
let all_models: Vec<&AgentModelInfo> = match models {
|
||||
AgentModelList::Flat(list) => list.iter().collect(),
|
||||
AgentModelList::Grouped(index_map) => index_map.values().flatten().collect(),
|
||||
};
|
||||
|
||||
let favorite_models = all_models
|
||||
.iter()
|
||||
.filter(|model| favorites.contains(&model.id))
|
||||
let favorite_models: Vec<_> = all_models
|
||||
.into_iter()
|
||||
.filter(|model| self.favorites.contains(&model.id))
|
||||
.unique_by(|model| &model.id)
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
.collect();
|
||||
|
||||
let current_id = self.selected_model.as_ref().map(|m| m.id.clone());
|
||||
if favorite_models.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let current_id = self.selected_model.as_ref().map(|m| &m.id);
|
||||
|
||||
let current_index_in_favorites = current_id
|
||||
.as_ref()
|
||||
.and_then(|id| favorite_models.iter().position(|m| &m.id == id))
|
||||
.unwrap_or(usize::MAX);
|
||||
|
||||
@@ -220,11 +234,7 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Task<()> {
|
||||
let favorites = if self.selector.supports_favorites() {
|
||||
AgentSettings::get_global(cx).favorite_model_ids()
|
||||
} else {
|
||||
Default::default()
|
||||
};
|
||||
let favorites = self.favorites.clone();
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let filtered_models = match this
|
||||
@@ -317,21 +327,20 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
||||
let default_model = self.agent_server.default_model(cx);
|
||||
let is_default = default_model.as_ref() == Some(&model_info.id);
|
||||
|
||||
let supports_favorites = self.selector.supports_favorites();
|
||||
|
||||
let is_favorite = *is_favorite;
|
||||
let handle_action_click = {
|
||||
let model_id = model_info.id.clone();
|
||||
let fs = self.fs.clone();
|
||||
let agent_server = self.agent_server.clone();
|
||||
|
||||
move |cx: &App| {
|
||||
crate::favorite_models::toggle_model_id_in_settings(
|
||||
cx.listener(move |_, _, _, cx| {
|
||||
agent_server.toggle_favorite_model(
|
||||
model_id.clone(),
|
||||
!is_favorite,
|
||||
fs.clone(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
Some(
|
||||
@@ -357,10 +366,8 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
||||
})
|
||||
.is_selected(is_selected)
|
||||
.is_focused(selected)
|
||||
.when(supports_favorites, |this| {
|
||||
this.is_favorite(is_favorite)
|
||||
.on_toggle_favorite(handle_action_click)
|
||||
}),
|
||||
.is_favorite(is_favorite)
|
||||
.on_toggle_favorite(handle_action_click),
|
||||
)
|
||||
.into_any_element(),
|
||||
)
|
||||
@@ -603,6 +610,46 @@ mod tests {
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_fuzzy_match(cx: &mut TestAppContext) {
|
||||
let models = create_model_list(vec![
|
||||
(
|
||||
"zed",
|
||||
vec![
|
||||
"Claude 3.7 Sonnet",
|
||||
"Claude 3.7 Sonnet Thinking",
|
||||
"gpt-4.1",
|
||||
"gpt-4.1-nano",
|
||||
],
|
||||
),
|
||||
("openai", vec!["gpt-3.5-turbo", "gpt-4.1", "gpt-4.1-nano"]),
|
||||
("ollama", vec!["mistral", "deepseek"]),
|
||||
]);
|
||||
|
||||
// Results should preserve models order whenever possible.
|
||||
// In the case below, `zed/gpt-4.1` and `openai/gpt-4.1` have identical
|
||||
// similarity scores, but `zed/gpt-4.1` was higher in the models list,
|
||||
// so it should appear first in the results.
|
||||
let results = fuzzy_search(models.clone(), "41".into(), cx.executor()).await;
|
||||
assert_models_eq(
|
||||
results,
|
||||
vec![
|
||||
("zed", vec!["gpt-4.1", "gpt-4.1-nano"]),
|
||||
("openai", vec!["gpt-4.1", "gpt-4.1-nano"]),
|
||||
],
|
||||
);
|
||||
|
||||
// Fuzzy search
|
||||
let results = fuzzy_search(models.clone(), "4n".into(), cx.executor()).await;
|
||||
assert_models_eq(
|
||||
results,
|
||||
vec![
|
||||
("zed", vec!["gpt-4.1-nano"]),
|
||||
("openai", vec!["gpt-4.1-nano"]),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_favorites_section_appears_when_favorites_exist(_cx: &mut TestAppContext) {
|
||||
let models = create_model_list(vec![
|
||||
@@ -739,42 +786,48 @@ mod tests {
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_fuzzy_match(cx: &mut TestAppContext) {
|
||||
let models = create_model_list(vec![
|
||||
(
|
||||
"zed",
|
||||
vec![
|
||||
"Claude 3.7 Sonnet",
|
||||
"Claude 3.7 Sonnet Thinking",
|
||||
"gpt-4.1",
|
||||
"gpt-4.1-nano",
|
||||
],
|
||||
),
|
||||
("openai", vec!["gpt-3.5-turbo", "gpt-4.1", "gpt-4.1-nano"]),
|
||||
("ollama", vec!["mistral", "deepseek"]),
|
||||
fn test_favorites_count_returns_correct_count(_cx: &mut TestAppContext) {
|
||||
let empty_favorites: HashSet<ModelId> = HashSet::default();
|
||||
assert_eq!(empty_favorites.len(), 0);
|
||||
|
||||
let one_favorite = create_favorites(vec!["model-a"]);
|
||||
assert_eq!(one_favorite.len(), 1);
|
||||
|
||||
let multiple_favorites = create_favorites(vec!["model-a", "model-b", "model-c"]);
|
||||
assert_eq!(multiple_favorites.len(), 3);
|
||||
|
||||
let with_duplicates = create_favorites(vec!["model-a", "model-a", "model-b"]);
|
||||
assert_eq!(with_duplicates.len(), 2);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_is_favorite_flag_set_correctly_in_entries(_cx: &mut TestAppContext) {
|
||||
let models = AgentModelList::Flat(vec![
|
||||
acp_thread::AgentModelInfo {
|
||||
id: acp::ModelId::new("favorite-model".to_string()),
|
||||
name: "Favorite".into(),
|
||||
description: None,
|
||||
icon: None,
|
||||
},
|
||||
acp_thread::AgentModelInfo {
|
||||
id: acp::ModelId::new("regular-model".to_string()),
|
||||
name: "Regular".into(),
|
||||
description: None,
|
||||
icon: None,
|
||||
},
|
||||
]);
|
||||
let favorites = create_favorites(vec!["favorite-model"]);
|
||||
|
||||
// Results should preserve models order whenever possible.
|
||||
// In the case below, `zed/gpt-4.1` and `openai/gpt-4.1` have identical
|
||||
// similarity scores, but `zed/gpt-4.1` was higher in the models list,
|
||||
// so it should appear first in the results.
|
||||
let results = fuzzy_search(models.clone(), "41".into(), cx.executor()).await;
|
||||
assert_models_eq(
|
||||
results,
|
||||
vec![
|
||||
("zed", vec!["gpt-4.1", "gpt-4.1-nano"]),
|
||||
("openai", vec!["gpt-4.1", "gpt-4.1-nano"]),
|
||||
],
|
||||
);
|
||||
let entries = info_list_to_picker_entries(models, &favorites);
|
||||
|
||||
// Fuzzy search
|
||||
let results = fuzzy_search(models.clone(), "4n".into(), cx.executor()).await;
|
||||
assert_models_eq(
|
||||
results,
|
||||
vec![
|
||||
("zed", vec!["gpt-4.1-nano"]),
|
||||
("openai", vec!["gpt-4.1-nano"]),
|
||||
],
|
||||
);
|
||||
for entry in &entries {
|
||||
if let AcpModelPickerEntry::Model(info, is_favorite) = entry {
|
||||
if info.id.0.as_ref() == "favorite-model" {
|
||||
assert!(*is_favorite, "favorite-model should have is_favorite=true");
|
||||
} else if info.id.0.as_ref() == "regular-model" {
|
||||
assert!(!*is_favorite, "regular-model should have is_favorite=false");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,17 +2,13 @@ use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
||||
use acp_thread::{AgentModelIcon, AgentModelInfo, AgentModelSelector};
|
||||
use agent_servers::AgentServer;
|
||||
use agent_settings::AgentSettings;
|
||||
use fs::Fs;
|
||||
use gpui::{Entity, FocusHandle};
|
||||
use picker::popover_menu::PickerPopoverMenu;
|
||||
use settings::Settings as _;
|
||||
use ui::{ButtonLike, KeyBinding, PopoverMenuHandle, TintColor, Tooltip, prelude::*};
|
||||
use zed_actions::agent::ToggleModelSelector;
|
||||
use ui::{ButtonLike, PopoverMenuHandle, TintColor, Tooltip, prelude::*};
|
||||
|
||||
use crate::CycleFavoriteModels;
|
||||
use crate::acp::{AcpModelSelector, model_selector::acp_model_selector};
|
||||
use crate::ui::ModelSelectorTooltip;
|
||||
|
||||
pub struct AcpModelSelectorPopover {
|
||||
selector: Entity<AcpModelSelector>,
|
||||
@@ -23,7 +19,7 @@ pub struct AcpModelSelectorPopover {
|
||||
impl AcpModelSelectorPopover {
|
||||
pub(crate) fn new(
|
||||
selector: Rc<dyn AgentModelSelector>,
|
||||
agent_server: Rc<dyn AgentServer>,
|
||||
agent_server: Rc<dyn agent_servers::AgentServer>,
|
||||
fs: Arc<dyn Fs>,
|
||||
menu_handle: PopoverMenuHandle<AcpModelSelector>,
|
||||
focus_handle: FocusHandle,
|
||||
@@ -64,7 +60,8 @@ impl AcpModelSelectorPopover {
|
||||
|
||||
impl Render for AcpModelSelectorPopover {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let model = self.selector.read(cx).delegate.active_model();
|
||||
let selector = self.selector.read(cx);
|
||||
let model = selector.delegate.active_model();
|
||||
let model_name = model
|
||||
.as_ref()
|
||||
.map(|model| model.name.clone())
|
||||
@@ -80,43 +77,13 @@ impl Render for AcpModelSelectorPopover {
|
||||
(Color::Muted, IconName::ChevronDown)
|
||||
};
|
||||
|
||||
let tooltip = Tooltip::element({
|
||||
move |_, cx| {
|
||||
let focus_handle = focus_handle.clone();
|
||||
let should_show_cycle_row = !AgentSettings::get_global(cx)
|
||||
.favorite_model_ids()
|
||||
.is_empty();
|
||||
let show_cycle_row = selector.delegate.favorites_count() > 1;
|
||||
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.child(Label::new("Change Model"))
|
||||
.child(KeyBinding::for_action_in(
|
||||
&ToggleModelSelector,
|
||||
&focus_handle,
|
||||
cx,
|
||||
)),
|
||||
)
|
||||
.when(should_show_cycle_row, |this| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.pt_1()
|
||||
.gap_2()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.justify_between()
|
||||
.child(Label::new("Cycle Favorited Models"))
|
||||
.child(KeyBinding::for_action_in(
|
||||
&CycleFavoriteModels,
|
||||
&focus_handle,
|
||||
cx,
|
||||
)),
|
||||
)
|
||||
})
|
||||
.into_any()
|
||||
let tooltip = Tooltip::element({
|
||||
move |_, _cx| {
|
||||
ModelSelectorTooltip::new(focus_handle.clone())
|
||||
.show_cycle_row(show_cycle_row)
|
||||
.into_any_element()
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -47,8 +47,9 @@ use terminal_view::terminal_panel::TerminalPanel;
|
||||
use text::Anchor;
|
||||
use theme::{AgentFontSize, ThemeSettings};
|
||||
use ui::{
|
||||
Callout, CommonAnimationExt, Disclosure, Divider, DividerColor, ElevationIndex, KeyBinding,
|
||||
PopoverMenuHandle, SpinnerLabel, TintColor, Tooltip, WithScrollbar, prelude::*,
|
||||
Callout, CommonAnimationExt, ContextMenu, ContextMenuEntry, Disclosure, Divider, DividerColor,
|
||||
ElevationIndex, KeyBinding, PopoverMenuHandle, SpinnerLabel, TintColor, Tooltip, WithScrollbar,
|
||||
prelude::*, right_click_menu,
|
||||
};
|
||||
use util::{ResultExt, size::format_file_size, time::duration_alt_display};
|
||||
use workspace::{CollaboratorId, NewTerminal, Workspace};
|
||||
@@ -2038,7 +2039,7 @@ impl AcpThreadView {
|
||||
}
|
||||
})
|
||||
.text_xs()
|
||||
.child(editor.clone().into_any_element()),
|
||||
.child(editor.clone().into_any_element())
|
||||
)
|
||||
.when(editor_focus, |this| {
|
||||
let base_container = h_flex()
|
||||
@@ -2154,7 +2155,6 @@ impl AcpThreadView {
|
||||
if this_is_blank {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(
|
||||
self.render_thinking_block(
|
||||
entry_ix,
|
||||
@@ -2180,7 +2180,7 @@ impl AcpThreadView {
|
||||
.when(is_last, |this| this.pb_4())
|
||||
.w_full()
|
||||
.text_ui(cx)
|
||||
.child(message_body)
|
||||
.child(self.render_message_context_menu(entry_ix, message_body, cx))
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
@@ -2287,6 +2287,70 @@ impl AcpThreadView {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_message_context_menu(
|
||||
&self,
|
||||
entry_ix: usize,
|
||||
message_body: AnyElement,
|
||||
cx: &Context<Self>,
|
||||
) -> AnyElement {
|
||||
let entity = cx.entity();
|
||||
let workspace = self.workspace.clone();
|
||||
|
||||
right_click_menu(format!("agent_context_menu-{}", entry_ix))
|
||||
.trigger(move |_, _, _| message_body)
|
||||
.menu(move |window, cx| {
|
||||
let focus = window.focused(cx);
|
||||
let entity = entity.clone();
|
||||
let workspace = workspace.clone();
|
||||
|
||||
ContextMenu::build(window, cx, move |menu, _, cx| {
|
||||
let is_at_top = entity.read(cx).list_state.logical_scroll_top().item_ix == 0;
|
||||
|
||||
let scroll_item = if is_at_top {
|
||||
ContextMenuEntry::new("Scroll to Bottom").handler({
|
||||
let entity = entity.clone();
|
||||
move |_, cx| {
|
||||
entity.update(cx, |this, cx| {
|
||||
this.scroll_to_bottom(cx);
|
||||
});
|
||||
}
|
||||
})
|
||||
} else {
|
||||
ContextMenuEntry::new("Scroll to Top").handler({
|
||||
let entity = entity.clone();
|
||||
move |_, cx| {
|
||||
entity.update(cx, |this, cx| {
|
||||
this.scroll_to_top(cx);
|
||||
});
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
let open_thread_as_markdown = ContextMenuEntry::new("Open Thread as Markdown")
|
||||
.handler({
|
||||
let entity = entity.clone();
|
||||
let workspace = workspace.clone();
|
||||
move |window, cx| {
|
||||
if let Some(workspace) = workspace.upgrade() {
|
||||
entity
|
||||
.update(cx, |this, cx| {
|
||||
this.open_thread_as_markdown(workspace, window, cx)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
menu.when_some(focus, |menu, focus| menu.context(focus))
|
||||
.action("Copy", Box::new(markdown::CopyAsMarkdown))
|
||||
.separator()
|
||||
.item(scroll_item)
|
||||
.item(open_thread_as_markdown)
|
||||
})
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn tool_card_header_bg(&self, cx: &Context<Self>) -> Hsla {
|
||||
cx.theme()
|
||||
.colors()
|
||||
@@ -4288,37 +4352,6 @@ impl AcpThreadView {
|
||||
|
||||
v_flex()
|
||||
.on_action(cx.listener(Self::expand_message_editor))
|
||||
.on_action(cx.listener(|this, _: &ToggleProfileSelector, window, cx| {
|
||||
if let Some(profile_selector) = this.profile_selector.as_ref() {
|
||||
profile_selector.read(cx).menu_handle().toggle(window, cx);
|
||||
} else if let Some(mode_selector) = this.mode_selector() {
|
||||
mode_selector.read(cx).menu_handle().toggle(window, cx);
|
||||
}
|
||||
}))
|
||||
.on_action(cx.listener(|this, _: &CycleModeSelector, window, cx| {
|
||||
if let Some(profile_selector) = this.profile_selector.as_ref() {
|
||||
profile_selector.update(cx, |profile_selector, cx| {
|
||||
profile_selector.cycle_profile(cx);
|
||||
});
|
||||
} else if let Some(mode_selector) = this.mode_selector() {
|
||||
mode_selector.update(cx, |mode_selector, cx| {
|
||||
mode_selector.cycle_mode(window, cx);
|
||||
});
|
||||
}
|
||||
}))
|
||||
.on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
|
||||
if let Some(model_selector) = this.model_selector.as_ref() {
|
||||
model_selector
|
||||
.update(cx, |model_selector, cx| model_selector.toggle(window, cx));
|
||||
}
|
||||
}))
|
||||
.on_action(cx.listener(|this, _: &CycleFavoriteModels, window, cx| {
|
||||
if let Some(model_selector) = this.model_selector.as_ref() {
|
||||
model_selector.update(cx, |model_selector, cx| {
|
||||
model_selector.cycle_favorite_models(window, cx);
|
||||
});
|
||||
}
|
||||
}))
|
||||
.p_2()
|
||||
.gap_2()
|
||||
.border_t_1()
|
||||
@@ -6005,6 +6038,37 @@ impl Render for AcpThreadView {
|
||||
.on_action(cx.listener(Self::allow_always))
|
||||
.on_action(cx.listener(Self::allow_once))
|
||||
.on_action(cx.listener(Self::reject_once))
|
||||
.on_action(cx.listener(|this, _: &ToggleProfileSelector, window, cx| {
|
||||
if let Some(profile_selector) = this.profile_selector.as_ref() {
|
||||
profile_selector.read(cx).menu_handle().toggle(window, cx);
|
||||
} else if let Some(mode_selector) = this.mode_selector() {
|
||||
mode_selector.read(cx).menu_handle().toggle(window, cx);
|
||||
}
|
||||
}))
|
||||
.on_action(cx.listener(|this, _: &CycleModeSelector, window, cx| {
|
||||
if let Some(profile_selector) = this.profile_selector.as_ref() {
|
||||
profile_selector.update(cx, |profile_selector, cx| {
|
||||
profile_selector.cycle_profile(cx);
|
||||
});
|
||||
} else if let Some(mode_selector) = this.mode_selector() {
|
||||
mode_selector.update(cx, |mode_selector, cx| {
|
||||
mode_selector.cycle_mode(window, cx);
|
||||
});
|
||||
}
|
||||
}))
|
||||
.on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
|
||||
if let Some(model_selector) = this.model_selector.as_ref() {
|
||||
model_selector
|
||||
.update(cx, |model_selector, cx| model_selector.toggle(window, cx));
|
||||
}
|
||||
}))
|
||||
.on_action(cx.listener(|this, _: &CycleFavoriteModels, window, cx| {
|
||||
if let Some(model_selector) = this.model_selector.as_ref() {
|
||||
model_selector.update(cx, |model_selector, cx| {
|
||||
model_selector.cycle_favorite_models(window, cx);
|
||||
});
|
||||
}
|
||||
}))
|
||||
.track_focus(&self.focus_handle)
|
||||
.bg(cx.theme().colors().panel_background)
|
||||
.child(match &self.thread_state {
|
||||
|
||||
@@ -1370,6 +1370,7 @@ async fn open_new_agent_servers_entry_in_settings_editor(
|
||||
env: Some(HashMap::default()),
|
||||
default_mode: None,
|
||||
default_model: None,
|
||||
favorite_models: vec![],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::{
|
||||
ModelUsageContext,
|
||||
language_model_selector::{LanguageModelSelector, language_model_selector},
|
||||
ui::ModelSelectorTooltip,
|
||||
};
|
||||
use fs::Fs;
|
||||
use gpui::{Entity, FocusHandle, SharedString};
|
||||
@@ -9,7 +10,6 @@ use picker::popover_menu::PickerPopoverMenu;
|
||||
use settings::update_settings_file;
|
||||
use std::sync::Arc;
|
||||
use ui::{ButtonLike, PopoverMenuHandle, TintColor, Tooltip, prelude::*};
|
||||
use zed_actions::agent::ToggleModelSelector;
|
||||
|
||||
pub struct AgentModelSelector {
|
||||
selector: Entity<LanguageModelSelector>,
|
||||
@@ -81,6 +81,12 @@ impl AgentModelSelector {
|
||||
pub fn active_model(&self, cx: &App) -> Option<language_model::ConfiguredModel> {
|
||||
self.selector.read(cx).delegate.active_model(cx)
|
||||
}
|
||||
|
||||
pub fn cycle_favorite_models(&self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.selector.update(cx, |selector, cx| {
|
||||
selector.delegate.cycle_favorite_models(window, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for AgentModelSelector {
|
||||
@@ -98,8 +104,18 @@ impl Render for AgentModelSelector {
|
||||
Color::Muted
|
||||
};
|
||||
|
||||
let show_cycle_row = self.selector.read(cx).delegate.favorites_count() > 1;
|
||||
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
|
||||
let tooltip = Tooltip::element({
|
||||
move |_, _cx| {
|
||||
ModelSelectorTooltip::new(focus_handle.clone())
|
||||
.show_cycle_row(show_cycle_row)
|
||||
.into_any_element()
|
||||
}
|
||||
});
|
||||
|
||||
PickerPopoverMenu::new(
|
||||
self.selector.clone(),
|
||||
ButtonLike::new("active-model")
|
||||
@@ -125,9 +141,7 @@ impl Render for AgentModelSelector {
|
||||
.color(color)
|
||||
.size(IconSize::XSmall),
|
||||
),
|
||||
move |_window, cx| {
|
||||
Tooltip::for_action_in("Change Model", &ToggleModelSelector, &focus_handle, cx)
|
||||
},
|
||||
tooltip,
|
||||
gpui::Corner::TopRight,
|
||||
cx,
|
||||
)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use agent_client_protocol::ModelId;
|
||||
use fs::Fs;
|
||||
use language_model::LanguageModel;
|
||||
use settings::{LanguageModelSelection, update_settings_file};
|
||||
@@ -13,20 +12,11 @@ fn language_model_to_selection(model: &Arc<dyn LanguageModel>) -> LanguageModelS
|
||||
}
|
||||
}
|
||||
|
||||
fn model_id_to_selection(model_id: &ModelId) -> LanguageModelSelection {
|
||||
let id = model_id.0.as_ref();
|
||||
let (provider, model) = id.split_once('/').unwrap_or(("", id));
|
||||
LanguageModelSelection {
|
||||
provider: provider.to_owned().into(),
|
||||
model: model.to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_in_settings(
|
||||
model: Arc<dyn LanguageModel>,
|
||||
should_be_favorite: bool,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &App,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let selection = language_model_to_selection(&model);
|
||||
update_settings_file(fs, cx, move |settings, _| {
|
||||
@@ -38,20 +28,3 @@ pub fn toggle_in_settings(
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn toggle_model_id_in_settings(
|
||||
model_id: ModelId,
|
||||
should_be_favorite: bool,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &App,
|
||||
) {
|
||||
let selection = model_id_to_selection(&model_id);
|
||||
update_settings_file(fs, cx, move |settings, _| {
|
||||
let agent = settings.agent.get_or_insert_default();
|
||||
if should_be_favorite {
|
||||
agent.add_favorite_model(selection.clone());
|
||||
} else {
|
||||
agent.remove_favorite_model(&selection);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -40,7 +40,9 @@ use crate::completion_provider::{
|
||||
use crate::mention_set::paste_images_as_context;
|
||||
use crate::mention_set::{MentionSet, crease_for_mention};
|
||||
use crate::terminal_codegen::TerminalCodegen;
|
||||
use crate::{CycleNextInlineAssist, CyclePreviousInlineAssist, ModelUsageContext};
|
||||
use crate::{
|
||||
CycleFavoriteModels, CycleNextInlineAssist, CyclePreviousInlineAssist, ModelUsageContext,
|
||||
};
|
||||
|
||||
actions!(inline_assistant, [ThumbsUpResult, ThumbsDownResult]);
|
||||
|
||||
@@ -148,7 +150,7 @@ impl<T: 'static> Render for PromptEditor<T> {
|
||||
.into_any_element();
|
||||
|
||||
v_flex()
|
||||
.key_context("PromptEditor")
|
||||
.key_context("InlineAssistant")
|
||||
.capture_action(cx.listener(Self::paste))
|
||||
.block_mouse_except_scroll()
|
||||
.size_full()
|
||||
@@ -162,10 +164,6 @@ impl<T: 'static> Render for PromptEditor<T> {
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.child(
|
||||
h_flex()
|
||||
.on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
|
||||
this.model_selector
|
||||
.update(cx, |model_selector, cx| model_selector.toggle(window, cx));
|
||||
}))
|
||||
.on_action(cx.listener(Self::confirm))
|
||||
.on_action(cx.listener(Self::cancel))
|
||||
.on_action(cx.listener(Self::move_up))
|
||||
@@ -174,6 +172,15 @@ impl<T: 'static> Render for PromptEditor<T> {
|
||||
.on_action(cx.listener(Self::thumbs_down))
|
||||
.capture_action(cx.listener(Self::cycle_prev))
|
||||
.capture_action(cx.listener(Self::cycle_next))
|
||||
.on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
|
||||
this.model_selector
|
||||
.update(cx, |model_selector, cx| model_selector.toggle(window, cx));
|
||||
}))
|
||||
.on_action(cx.listener(|this, _: &CycleFavoriteModels, window, cx| {
|
||||
this.model_selector.update(cx, |model_selector, cx| {
|
||||
model_selector.cycle_favorite_models(window, cx);
|
||||
});
|
||||
}))
|
||||
.child(
|
||||
WithRemSize::new(ui_font_size)
|
||||
.h_full()
|
||||
@@ -855,7 +862,7 @@ impl<T: 'static> PromptEditor<T> {
|
||||
.map(|this| {
|
||||
if rated {
|
||||
this.disabled(true)
|
||||
.icon_color(Color::Ignored)
|
||||
.icon_color(Color::Disabled)
|
||||
.tooltip(move |_, cx| {
|
||||
Tooltip::with_meta(
|
||||
"Good Result",
|
||||
@@ -865,8 +872,15 @@ impl<T: 'static> PromptEditor<T> {
|
||||
)
|
||||
})
|
||||
} else {
|
||||
this.icon_color(Color::Muted)
|
||||
.tooltip(Tooltip::text("Good Result"))
|
||||
this.icon_color(Color::Muted).tooltip(
|
||||
move |_, cx| {
|
||||
Tooltip::for_action(
|
||||
"Good Result",
|
||||
&ThumbsUpResult,
|
||||
cx,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
@@ -879,7 +893,7 @@ impl<T: 'static> PromptEditor<T> {
|
||||
.map(|this| {
|
||||
if rated {
|
||||
this.disabled(true)
|
||||
.icon_color(Color::Ignored)
|
||||
.icon_color(Color::Disabled)
|
||||
.tooltip(move |_, cx| {
|
||||
Tooltip::with_meta(
|
||||
"Bad Result",
|
||||
@@ -889,8 +903,15 @@ impl<T: 'static> PromptEditor<T> {
|
||||
)
|
||||
})
|
||||
} else {
|
||||
this.icon_color(Color::Muted)
|
||||
.tooltip(Tooltip::text("Bad Result"))
|
||||
this.icon_color(Color::Muted).tooltip(
|
||||
move |_, cx| {
|
||||
Tooltip::for_action(
|
||||
"Bad Result",
|
||||
&ThumbsDownResult,
|
||||
cx,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
@@ -1088,7 +1109,6 @@ impl<T: 'static> PromptEditor<T> {
|
||||
let colors = cx.theme().colors();
|
||||
|
||||
div()
|
||||
.key_context("InlineAssistEditor")
|
||||
.size_full()
|
||||
.p_2()
|
||||
.pl_1()
|
||||
|
||||
@@ -20,14 +20,14 @@ use crate::ui::{ModelSelectorFooter, ModelSelectorHeader, ModelSelectorListItem}
|
||||
|
||||
type OnModelChanged = Arc<dyn Fn(Arc<dyn LanguageModel>, &mut App) + 'static>;
|
||||
type GetActiveModel = Arc<dyn Fn(&App) -> Option<ConfiguredModel> + 'static>;
|
||||
type OnToggleFavorite = Arc<dyn Fn(Arc<dyn LanguageModel>, bool, &App) + 'static>;
|
||||
type OnToggleFavorite = Arc<dyn Fn(Arc<dyn LanguageModel>, bool, &mut App) + 'static>;
|
||||
|
||||
pub type LanguageModelSelector = Picker<LanguageModelPickerDelegate>;
|
||||
|
||||
pub fn language_model_selector(
|
||||
get_active_model: impl Fn(&App) -> Option<ConfiguredModel> + 'static,
|
||||
on_model_changed: impl Fn(Arc<dyn LanguageModel>, &mut App) + 'static,
|
||||
on_toggle_favorite: impl Fn(Arc<dyn LanguageModel>, bool, &App) + 'static,
|
||||
on_toggle_favorite: impl Fn(Arc<dyn LanguageModel>, bool, &mut App) + 'static,
|
||||
popover_styles: bool,
|
||||
focus_handle: FocusHandle,
|
||||
window: &mut Window,
|
||||
@@ -133,7 +133,7 @@ impl LanguageModelPickerDelegate {
|
||||
fn new(
|
||||
get_active_model: impl Fn(&App) -> Option<ConfiguredModel> + 'static,
|
||||
on_model_changed: impl Fn(Arc<dyn LanguageModel>, &mut App) + 'static,
|
||||
on_toggle_favorite: impl Fn(Arc<dyn LanguageModel>, bool, &App) + 'static,
|
||||
on_toggle_favorite: impl Fn(Arc<dyn LanguageModel>, bool, &mut App) + 'static,
|
||||
popover_styles: bool,
|
||||
focus_handle: FocusHandle,
|
||||
window: &mut Window,
|
||||
@@ -250,6 +250,10 @@ impl LanguageModelPickerDelegate {
|
||||
(self.get_active_model)(cx)
|
||||
}
|
||||
|
||||
pub fn favorites_count(&self) -> usize {
|
||||
self.all_models.favorites.len()
|
||||
}
|
||||
|
||||
pub fn cycle_favorite_models(&mut self, window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
if self.all_models.favorites.is_empty() {
|
||||
return;
|
||||
@@ -561,7 +565,10 @@ impl PickerDelegate for LanguageModelPickerDelegate {
|
||||
let handle_action_click = {
|
||||
let model = model_info.model.clone();
|
||||
let on_toggle_favorite = self.on_toggle_favorite.clone();
|
||||
move |cx: &App| on_toggle_favorite(model.clone(), !is_favorite, cx)
|
||||
cx.listener(move |picker, _, window, cx| {
|
||||
on_toggle_favorite(model.clone(), !is_favorite, cx);
|
||||
picker.refresh(window, cx);
|
||||
})
|
||||
};
|
||||
|
||||
Some(
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::{
|
||||
language_model_selector::{LanguageModelSelector, language_model_selector},
|
||||
ui::BurnModeTooltip,
|
||||
ui::{BurnModeTooltip, ModelSelectorTooltip},
|
||||
};
|
||||
use agent_settings::{AgentSettings, CompletionMode};
|
||||
use agent_settings::CompletionMode;
|
||||
use anyhow::Result;
|
||||
use assistant_slash_command::{SlashCommand, SlashCommandOutputSection, SlashCommandWorkingSet};
|
||||
use assistant_slash_commands::{DefaultSlashCommand, FileSlashCommand, selections_creases};
|
||||
@@ -2252,43 +2252,18 @@ impl TextThreadEditor {
|
||||
.color(color)
|
||||
.size(IconSize::XSmall);
|
||||
|
||||
let tooltip = Tooltip::element({
|
||||
move |_, cx| {
|
||||
let focus_handle = focus_handle.clone();
|
||||
let should_show_cycle_row = !AgentSettings::get_global(cx)
|
||||
.favorite_model_ids()
|
||||
.is_empty();
|
||||
let show_cycle_row = self
|
||||
.language_model_selector
|
||||
.read(cx)
|
||||
.delegate
|
||||
.favorites_count()
|
||||
> 1;
|
||||
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.child(Label::new("Change Model"))
|
||||
.child(KeyBinding::for_action_in(
|
||||
&ToggleModelSelector,
|
||||
&focus_handle,
|
||||
cx,
|
||||
)),
|
||||
)
|
||||
.when(should_show_cycle_row, |this| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.pt_1()
|
||||
.gap_2()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.justify_between()
|
||||
.child(Label::new("Cycle Favorited Models"))
|
||||
.child(KeyBinding::for_action_in(
|
||||
&CycleFavoriteModels,
|
||||
&focus_handle,
|
||||
cx,
|
||||
)),
|
||||
)
|
||||
})
|
||||
.into_any()
|
||||
let tooltip = Tooltip::element({
|
||||
move |_, _cx| {
|
||||
ModelSelectorTooltip::new(focus_handle.clone())
|
||||
.show_cycle_row(show_cycle_row)
|
||||
.into_any_element()
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use gpui::{Action, FocusHandle, prelude::*};
|
||||
use gpui::{Action, ClickEvent, FocusHandle, prelude::*};
|
||||
use ui::{ElevationIndex, KeyBinding, ListItem, ListItemSpacing, Tooltip, prelude::*};
|
||||
use zed_actions::agent::ToggleModelSelector;
|
||||
|
||||
use crate::CycleFavoriteModels;
|
||||
|
||||
enum ModelIcon {
|
||||
Name(IconName),
|
||||
@@ -48,7 +51,7 @@ pub struct ModelSelectorListItem {
|
||||
is_selected: bool,
|
||||
is_focused: bool,
|
||||
is_favorite: bool,
|
||||
on_toggle_favorite: Option<Box<dyn Fn(&App) + 'static>>,
|
||||
on_toggle_favorite: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
|
||||
}
|
||||
|
||||
impl ModelSelectorListItem {
|
||||
@@ -89,7 +92,10 @@ impl ModelSelectorListItem {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_toggle_favorite(mut self, handler: impl Fn(&App) + 'static) -> Self {
|
||||
pub fn on_toggle_favorite(
|
||||
mut self,
|
||||
handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.on_toggle_favorite = Some(Box::new(handler));
|
||||
self
|
||||
}
|
||||
@@ -141,7 +147,7 @@ impl RenderOnce for ModelSelectorListItem {
|
||||
.icon_color(color)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip(Tooltip::text(tooltip))
|
||||
.on_click(move |_, _, cx| (handle_click)(cx)),
|
||||
.on_click(move |event, window, cx| (handle_click)(event, window, cx)),
|
||||
)
|
||||
}
|
||||
}))
|
||||
@@ -187,3 +193,57 @@ impl RenderOnce for ModelSelectorFooter {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct ModelSelectorTooltip {
|
||||
focus_handle: FocusHandle,
|
||||
show_cycle_row: bool,
|
||||
}
|
||||
|
||||
impl ModelSelectorTooltip {
|
||||
pub fn new(focus_handle: FocusHandle) -> Self {
|
||||
Self {
|
||||
focus_handle,
|
||||
show_cycle_row: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show_cycle_row(mut self, show: bool) -> Self {
|
||||
self.show_cycle_row = show;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for ModelSelectorTooltip {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.child(Label::new("Change Model"))
|
||||
.child(KeyBinding::for_action_in(
|
||||
&ToggleModelSelector,
|
||||
&self.focus_handle,
|
||||
cx,
|
||||
)),
|
||||
)
|
||||
.when(self.show_cycle_row, |this| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.pt_1()
|
||||
.gap_2()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.justify_between()
|
||||
.child(Label::new("Cycle Favorited Models"))
|
||||
.child(KeyBinding::for_action_in(
|
||||
&CycleFavoriteModels,
|
||||
&self.focus_handle,
|
||||
cx,
|
||||
)),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -314,6 +314,12 @@ impl BufferDiffSnapshot {
|
||||
self.inner.hunks.is_empty()
|
||||
}
|
||||
|
||||
pub fn base_text_string(&self) -> Option<String> {
|
||||
self.inner
|
||||
.base_text_exists
|
||||
.then(|| self.inner.base_text.text())
|
||||
}
|
||||
|
||||
pub fn secondary_diff(&self) -> Option<&BufferDiffSnapshot> {
|
||||
self.secondary_diff.as_deref()
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ impl CopilotSweAgentBot {
|
||||
const USER_ID: i32 = 198982749;
|
||||
/// The alias of the GitHub copilot user. Although https://api.github.com/users/copilot
|
||||
/// yields a 404, GitHub still refers to the copilot bot user as @Copilot in some cases.
|
||||
const NAME_ALIAS: &'static str = "copilot";
|
||||
const NAME_ALIAS: &'static str = "Copilot";
|
||||
|
||||
/// Returns the `created_at` timestamp for the Dependabot bot user.
|
||||
fn created_at() -> &'static NaiveDateTime {
|
||||
|
||||
@@ -6745,8 +6745,13 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
|
||||
});
|
||||
|
||||
// Split pane to the right
|
||||
pane.update(cx, |pane, cx| {
|
||||
pane.split(workspace::SplitDirection::Right, cx);
|
||||
pane.update_in(cx, |pane, window, cx| {
|
||||
pane.split(
|
||||
workspace::SplitDirection::Right,
|
||||
workspace::SplitMode::default(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
let right_pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
|
||||
|
||||
@@ -1579,8 +1579,10 @@ impl Panel for DebugPanel {
|
||||
Some(proto::PanelId::DebugPanel)
|
||||
}
|
||||
|
||||
fn icon(&self, _window: &Window, _cx: &App) -> Option<IconName> {
|
||||
Some(IconName::Debug)
|
||||
fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
|
||||
DebuggerSettings::get_global(cx)
|
||||
.button
|
||||
.then_some(IconName::Debug)
|
||||
}
|
||||
|
||||
fn icon_tooltip(&self, _window: &Window, cx: &App) -> Option<&'static str> {
|
||||
|
||||
@@ -19,6 +19,7 @@ ai_onboarding.workspace = true
|
||||
anyhow.workspace = true
|
||||
arrayvec.workspace = true
|
||||
brotli.workspace = true
|
||||
buffer_diff.workspace = true
|
||||
client.workspace = true
|
||||
cloud_llm_client.workspace = true
|
||||
collections.workspace = true
|
||||
@@ -52,7 +53,9 @@ settings.workspace = true
|
||||
strum.workspace = true
|
||||
telemetry.workspace = true
|
||||
telemetry_events.workspace = true
|
||||
text.workspace = true
|
||||
thiserror.workspace = true
|
||||
time.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
|
||||
375
crates/edit_prediction/src/capture_example.rs
Normal file
375
crates/edit_prediction/src/capture_example.rs
Normal file
@@ -0,0 +1,375 @@
|
||||
use crate::{
|
||||
EditPredictionStore, StoredEvent,
|
||||
cursor_excerpt::editable_and_context_ranges_for_cursor_position, example_spec::ExampleSpec,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use buffer_diff::BufferDiffSnapshot;
|
||||
use collections::HashMap;
|
||||
use gpui::{App, Entity, Task};
|
||||
use language::{Buffer, ToPoint as _};
|
||||
use project::Project;
|
||||
use std::{collections::hash_map, fmt::Write as _, path::Path, sync::Arc};
|
||||
use text::{BufferSnapshot as TextBufferSnapshot, ToOffset as _};
|
||||
|
||||
pub fn capture_example(
|
||||
project: Entity<Project>,
|
||||
buffer: Entity<Buffer>,
|
||||
cursor_anchor: language::Anchor,
|
||||
last_event_is_expected_patch: bool,
|
||||
cx: &mut App,
|
||||
) -> Option<Task<Result<ExampleSpec>>> {
|
||||
let ep_store = EditPredictionStore::try_global(cx)?;
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
let file = snapshot.file()?;
|
||||
let worktree_id = file.worktree_id(cx);
|
||||
let repository = project.read(cx).active_repository(cx)?;
|
||||
let repository_snapshot = repository.read(cx).snapshot();
|
||||
let worktree = project.read(cx).worktree_for_id(worktree_id, cx)?;
|
||||
let cursor_path = worktree.read(cx).root_name().join(file.path());
|
||||
if worktree.read(cx).abs_path() != repository_snapshot.work_directory_abs_path {
|
||||
return None;
|
||||
}
|
||||
|
||||
let repository_url = repository_snapshot
|
||||
.remote_origin_url
|
||||
.clone()
|
||||
.or_else(|| repository_snapshot.remote_upstream_url.clone())?;
|
||||
let revision = repository_snapshot.head_commit.as_ref()?.sha.to_string();
|
||||
|
||||
let mut events = ep_store.update(cx, |store, cx| {
|
||||
store.edit_history_for_project_with_pause_split_last_event(&project, cx)
|
||||
});
|
||||
|
||||
let git_store = project.read(cx).git_store().clone();
|
||||
|
||||
Some(cx.spawn(async move |mut cx| {
|
||||
let snapshots_by_path = collect_snapshots(&project, &git_store, &events, &mut cx).await?;
|
||||
let cursor_excerpt = cx
|
||||
.background_executor()
|
||||
.spawn(async move { compute_cursor_excerpt(&snapshot, cursor_anchor) })
|
||||
.await;
|
||||
let uncommitted_diff = cx
|
||||
.background_executor()
|
||||
.spawn(async move { compute_uncommitted_diff(snapshots_by_path) })
|
||||
.await;
|
||||
|
||||
let mut edit_history = String::new();
|
||||
let mut expected_patch = String::new();
|
||||
if last_event_is_expected_patch {
|
||||
if let Some(stored_event) = events.pop() {
|
||||
zeta_prompt::write_event(&mut expected_patch, &stored_event.event);
|
||||
}
|
||||
}
|
||||
|
||||
for stored_event in &events {
|
||||
zeta_prompt::write_event(&mut edit_history, &stored_event.event);
|
||||
if !edit_history.ends_with('\n') {
|
||||
edit_history.push('\n');
|
||||
}
|
||||
}
|
||||
|
||||
let name = generate_timestamp_name();
|
||||
|
||||
Ok(ExampleSpec {
|
||||
name,
|
||||
repository_url,
|
||||
revision,
|
||||
uncommitted_diff,
|
||||
cursor_path: cursor_path.as_std_path().into(),
|
||||
cursor_position: cursor_excerpt,
|
||||
edit_history,
|
||||
expected_patch,
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
fn compute_cursor_excerpt(
|
||||
snapshot: &language::BufferSnapshot,
|
||||
cursor_anchor: language::Anchor,
|
||||
) -> String {
|
||||
let cursor_point = cursor_anchor.to_point(snapshot);
|
||||
let (_editable_range, context_range) =
|
||||
editable_and_context_ranges_for_cursor_position(cursor_point, snapshot, 100, 50);
|
||||
|
||||
let context_start_offset = context_range.start.to_offset(snapshot);
|
||||
let cursor_offset = cursor_anchor.to_offset(snapshot);
|
||||
let cursor_offset_in_excerpt = cursor_offset.saturating_sub(context_start_offset);
|
||||
let mut excerpt = snapshot.text_for_range(context_range).collect::<String>();
|
||||
if cursor_offset_in_excerpt <= excerpt.len() {
|
||||
excerpt.insert_str(cursor_offset_in_excerpt, zeta_prompt::CURSOR_MARKER);
|
||||
}
|
||||
excerpt
|
||||
}
|
||||
|
||||
async fn collect_snapshots(
|
||||
project: &Entity<Project>,
|
||||
git_store: &Entity<project::git_store::GitStore>,
|
||||
events: &[StoredEvent],
|
||||
cx: &mut gpui::AsyncApp,
|
||||
) -> Result<HashMap<Arc<Path>, (TextBufferSnapshot, BufferDiffSnapshot)>> {
|
||||
let mut snapshots_by_path = HashMap::default();
|
||||
for stored_event in events {
|
||||
let zeta_prompt::Event::BufferChange { path, .. } = stored_event.event.as_ref();
|
||||
if let Some((project_path, full_path)) = project.read_with(cx, |project, cx| {
|
||||
let project_path = project.find_project_path(path, cx)?;
|
||||
let full_path = project
|
||||
.worktree_for_id(project_path.worktree_id, cx)?
|
||||
.read(cx)
|
||||
.root_name()
|
||||
.join(&project_path.path)
|
||||
.as_std_path()
|
||||
.into();
|
||||
Some((project_path, full_path))
|
||||
})? {
|
||||
if let hash_map::Entry::Vacant(entry) = snapshots_by_path.entry(full_path) {
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_buffer(project_path.clone(), cx)
|
||||
})?
|
||||
.await?;
|
||||
let diff = git_store
|
||||
.update(cx, |git_store, cx| {
|
||||
git_store.open_uncommitted_diff(buffer.clone(), cx)
|
||||
})?
|
||||
.await?;
|
||||
let diff_snapshot = diff.update(cx, |diff, cx| diff.snapshot(cx))?;
|
||||
entry.insert((stored_event.old_snapshot.clone(), diff_snapshot));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(snapshots_by_path)
|
||||
}
|
||||
|
||||
fn compute_uncommitted_diff(
|
||||
snapshots_by_path: HashMap<Arc<Path>, (TextBufferSnapshot, BufferDiffSnapshot)>,
|
||||
) -> String {
|
||||
let mut uncommitted_diff = String::new();
|
||||
for (full_path, (before_text, diff_snapshot)) in snapshots_by_path {
|
||||
if let Some(head_text) = &diff_snapshot.base_text_string() {
|
||||
let file_diff = language::unified_diff(head_text, &before_text.text());
|
||||
if !file_diff.is_empty() {
|
||||
let path_str = full_path.to_string_lossy();
|
||||
writeln!(uncommitted_diff, "--- a/{path_str}").ok();
|
||||
writeln!(uncommitted_diff, "+++ b/{path_str}").ok();
|
||||
uncommitted_diff.push_str(&file_diff);
|
||||
if !uncommitted_diff.ends_with('\n') {
|
||||
uncommitted_diff.push('\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
uncommitted_diff
|
||||
}
|
||||
|
||||
fn generate_timestamp_name() -> String {
|
||||
let format = time::format_description::parse("[year]-[month]-[day] [hour]:[minute]:[second]");
|
||||
match format {
|
||||
Ok(format) => {
|
||||
let now = time::OffsetDateTime::now_local()
|
||||
.unwrap_or_else(|_| time::OffsetDateTime::now_utc());
|
||||
now.format(&format)
|
||||
.unwrap_or_else(|_| "unknown-time".to_string())
|
||||
}
|
||||
Err(_) => "unknown-time".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use client::{Client, UserStore};
|
||||
use clock::FakeSystemClock;
|
||||
use gpui::{AppContext as _, TestAppContext, http_client::FakeHttpClient};
|
||||
use indoc::indoc;
|
||||
use language::{Anchor, Point};
|
||||
use project::{FakeFs, Project};
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use std::path::Path;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_capture_example(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
|
||||
let committed_contents = indoc! {"
|
||||
fn main() {
|
||||
one();
|
||||
two();
|
||||
three();
|
||||
four();
|
||||
five();
|
||||
six();
|
||||
seven();
|
||||
eight();
|
||||
nine();
|
||||
}
|
||||
"};
|
||||
|
||||
let disk_contents = indoc! {"
|
||||
fn main() {
|
||||
// comment 1
|
||||
one();
|
||||
two();
|
||||
three();
|
||||
four();
|
||||
five();
|
||||
six();
|
||||
seven();
|
||||
eight();
|
||||
// comment 2
|
||||
nine();
|
||||
}
|
||||
"};
|
||||
|
||||
fs.insert_tree(
|
||||
"/project",
|
||||
json!({
|
||||
".git": {},
|
||||
"src": {
|
||||
"main.rs": disk_contents,
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
fs.set_head_for_repo(
|
||||
Path::new("/project/.git"),
|
||||
&[("src/main.rs", committed_contents.to_string())],
|
||||
"abc123def456",
|
||||
);
|
||||
fs.set_remote_for_repo(
|
||||
Path::new("/project/.git"),
|
||||
"origin",
|
||||
"https://github.com/test/repo.git",
|
||||
);
|
||||
|
||||
let project = Project::test(fs.clone(), ["/project".as_ref()], cx).await;
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer("/project/src/main.rs", cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let ep_store = cx.read(|cx| EditPredictionStore::try_global(cx).unwrap());
|
||||
ep_store.update(cx, |ep_store, cx| {
|
||||
ep_store.register_buffer(&buffer, &project, cx)
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
let point = Point::new(6, 0);
|
||||
buffer.edit([(point..point, " // comment 3\n")], None, cx);
|
||||
let point = Point::new(4, 0);
|
||||
buffer.edit([(point..point, " // comment 4\n")], None, cx);
|
||||
|
||||
pretty_assertions::assert_eq!(
|
||||
buffer.text(),
|
||||
indoc! {"
|
||||
fn main() {
|
||||
// comment 1
|
||||
one();
|
||||
two();
|
||||
// comment 4
|
||||
three();
|
||||
four();
|
||||
// comment 3
|
||||
five();
|
||||
six();
|
||||
seven();
|
||||
eight();
|
||||
// comment 2
|
||||
nine();
|
||||
}
|
||||
"}
|
||||
);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
let mut example = cx
|
||||
.update(|cx| {
|
||||
capture_example(project.clone(), buffer.clone(), Anchor::MIN, false, cx).unwrap()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
example.name = "test".to_string();
|
||||
|
||||
pretty_assertions::assert_eq!(
|
||||
example,
|
||||
ExampleSpec {
|
||||
name: "test".to_string(),
|
||||
repository_url: "https://github.com/test/repo.git".to_string(),
|
||||
revision: "abc123def456".to_string(),
|
||||
uncommitted_diff: indoc! {"
|
||||
--- a/project/src/main.rs
|
||||
+++ b/project/src/main.rs
|
||||
@@ -1,4 +1,5 @@
|
||||
fn main() {
|
||||
+ // comment 1
|
||||
one();
|
||||
two();
|
||||
three();
|
||||
@@ -7,5 +8,6 @@
|
||||
six();
|
||||
seven();
|
||||
eight();
|
||||
+ // comment 2
|
||||
nine();
|
||||
}
|
||||
"}
|
||||
.to_string(),
|
||||
cursor_path: Path::new("project/src/main.rs").into(),
|
||||
cursor_position: indoc! {"
|
||||
<|user_cursor|>fn main() {
|
||||
// comment 1
|
||||
one();
|
||||
two();
|
||||
// comment 4
|
||||
three();
|
||||
four();
|
||||
// comment 3
|
||||
five();
|
||||
six();
|
||||
seven();
|
||||
eight();
|
||||
// comment 2
|
||||
nine();
|
||||
}
|
||||
"}
|
||||
.to_string(),
|
||||
edit_history: indoc! {"
|
||||
--- a/project/src/main.rs
|
||||
+++ b/project/src/main.rs
|
||||
@@ -2,8 +2,10 @@
|
||||
// comment 1
|
||||
one();
|
||||
two();
|
||||
+ // comment 4
|
||||
three();
|
||||
four();
|
||||
+ // comment 3
|
||||
five();
|
||||
six();
|
||||
seven();
|
||||
"}
|
||||
.to_string(),
|
||||
expected_patch: "".to_string(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
zlog::init_test();
|
||||
let http_client = FakeHttpClient::with_404_response();
|
||||
let client = Client::new(Arc::new(FakeSystemClock::new()), http_client, cx);
|
||||
language_model::init(client.clone(), cx);
|
||||
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
|
||||
EditPredictionStore::global(&client, &user_store, cx);
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,7 @@ use semver::Version;
|
||||
use serde::de::DeserializeOwned;
|
||||
use settings::{EditPredictionProvider, SettingsStore, update_settings_file};
|
||||
use std::collections::{VecDeque, hash_map};
|
||||
use text::Edit;
|
||||
use workspace::Workspace;
|
||||
|
||||
use std::ops::Range;
|
||||
@@ -57,9 +58,9 @@ pub mod open_ai_response;
|
||||
mod prediction;
|
||||
pub mod sweep_ai;
|
||||
|
||||
#[cfg(any(test, feature = "test-support", feature = "cli-support"))]
|
||||
pub mod udiff;
|
||||
|
||||
mod capture_example;
|
||||
mod zed_edit_prediction_delegate;
|
||||
pub mod zeta1;
|
||||
pub mod zeta2;
|
||||
@@ -74,6 +75,7 @@ pub use crate::prediction::EditPrediction;
|
||||
pub use crate::prediction::EditPredictionId;
|
||||
use crate::prediction::EditPredictionResult;
|
||||
pub use crate::sweep_ai::SweepAi;
|
||||
pub use capture_example::capture_example;
|
||||
pub use language_model::ApiKeyState;
|
||||
pub use telemetry_events::EditPredictionRating;
|
||||
pub use zed_edit_prediction_delegate::ZedEditPredictionDelegate;
|
||||
@@ -231,8 +233,15 @@ pub struct EditPredictionFinishedDebugEvent {
|
||||
|
||||
pub type RequestDebugInfo = predict_edits_v3::DebugInfo;
|
||||
|
||||
/// An event with associated metadata for reconstructing buffer state.
|
||||
#[derive(Clone)]
|
||||
pub struct StoredEvent {
|
||||
pub event: Arc<zeta_prompt::Event>,
|
||||
pub old_snapshot: TextBufferSnapshot,
|
||||
}
|
||||
|
||||
struct ProjectState {
|
||||
events: VecDeque<Arc<zeta_prompt::Event>>,
|
||||
events: VecDeque<StoredEvent>,
|
||||
last_event: Option<LastEvent>,
|
||||
recent_paths: VecDeque<ProjectPath>,
|
||||
registered_buffers: HashMap<gpui::EntityId, RegisteredBuffer>,
|
||||
@@ -248,7 +257,7 @@ struct ProjectState {
|
||||
}
|
||||
|
||||
impl ProjectState {
|
||||
pub fn events(&self, cx: &App) -> Vec<Arc<zeta_prompt::Event>> {
|
||||
pub fn events(&self, cx: &App) -> Vec<StoredEvent> {
|
||||
self.events
|
||||
.iter()
|
||||
.cloned()
|
||||
@@ -260,7 +269,7 @@ impl ProjectState {
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn events_split_by_pause(&self, cx: &App) -> Vec<Arc<zeta_prompt::Event>> {
|
||||
pub fn events_split_by_pause(&self, cx: &App) -> Vec<StoredEvent> {
|
||||
self.events
|
||||
.iter()
|
||||
.cloned()
|
||||
@@ -415,7 +424,7 @@ impl LastEvent {
|
||||
&self,
|
||||
license_detection_watchers: &HashMap<WorktreeId, Rc<LicenseDetectionWatcher>>,
|
||||
cx: &App,
|
||||
) -> Option<Arc<zeta_prompt::Event>> {
|
||||
) -> Option<StoredEvent> {
|
||||
let path = buffer_path_with_id_fallback(self.new_file.as_ref(), &self.new_snapshot, cx);
|
||||
let old_path = buffer_path_with_id_fallback(self.old_file.as_ref(), &self.old_snapshot, cx);
|
||||
|
||||
@@ -430,19 +439,22 @@ impl LastEvent {
|
||||
})
|
||||
});
|
||||
|
||||
let diff = language::unified_diff(&self.old_snapshot.text(), &self.new_snapshot.text());
|
||||
let diff = compute_diff_between_snapshots(&self.old_snapshot, &self.new_snapshot)?;
|
||||
|
||||
if path == old_path && diff.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(Arc::new(zeta_prompt::Event::BufferChange {
|
||||
old_path,
|
||||
path,
|
||||
diff,
|
||||
in_open_source_repo,
|
||||
// TODO: Actually detect if this edit was predicted or not
|
||||
predicted: false,
|
||||
}))
|
||||
Some(StoredEvent {
|
||||
event: Arc::new(zeta_prompt::Event::BufferChange {
|
||||
old_path,
|
||||
path,
|
||||
diff,
|
||||
in_open_source_repo,
|
||||
// TODO: Actually detect if this edit was predicted or not
|
||||
predicted: false,
|
||||
}),
|
||||
old_snapshot: self.old_snapshot.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -475,6 +487,52 @@ impl LastEvent {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn compute_diff_between_snapshots(
|
||||
old_snapshot: &TextBufferSnapshot,
|
||||
new_snapshot: &TextBufferSnapshot,
|
||||
) -> Option<String> {
|
||||
let edits: Vec<Edit<usize>> = new_snapshot
|
||||
.edits_since::<usize>(&old_snapshot.version)
|
||||
.collect();
|
||||
|
||||
let (first_edit, last_edit) = edits.first().zip(edits.last())?;
|
||||
|
||||
let old_start_point = old_snapshot.offset_to_point(first_edit.old.start);
|
||||
let old_end_point = old_snapshot.offset_to_point(last_edit.old.end);
|
||||
let new_start_point = new_snapshot.offset_to_point(first_edit.new.start);
|
||||
let new_end_point = new_snapshot.offset_to_point(last_edit.new.end);
|
||||
|
||||
const CONTEXT_LINES: u32 = 3;
|
||||
|
||||
let old_context_start_row = old_start_point.row.saturating_sub(CONTEXT_LINES);
|
||||
let new_context_start_row = new_start_point.row.saturating_sub(CONTEXT_LINES);
|
||||
let old_context_end_row =
|
||||
(old_end_point.row + 1 + CONTEXT_LINES).min(old_snapshot.max_point().row);
|
||||
let new_context_end_row =
|
||||
(new_end_point.row + 1 + CONTEXT_LINES).min(new_snapshot.max_point().row);
|
||||
|
||||
let old_start_line_offset = old_snapshot.point_to_offset(Point::new(old_context_start_row, 0));
|
||||
let new_start_line_offset = new_snapshot.point_to_offset(Point::new(new_context_start_row, 0));
|
||||
let old_end_line_offset = old_snapshot
|
||||
.point_to_offset(Point::new(old_context_end_row + 1, 0).min(old_snapshot.max_point()));
|
||||
let new_end_line_offset = new_snapshot
|
||||
.point_to_offset(Point::new(new_context_end_row + 1, 0).min(new_snapshot.max_point()));
|
||||
let old_edit_range = old_start_line_offset..old_end_line_offset;
|
||||
let new_edit_range = new_start_line_offset..new_end_line_offset;
|
||||
|
||||
let old_region_text: String = old_snapshot.text_for_range(old_edit_range).collect();
|
||||
let new_region_text: String = new_snapshot.text_for_range(new_edit_range).collect();
|
||||
|
||||
let diff = language::unified_diff_with_offsets(
|
||||
&old_region_text,
|
||||
&new_region_text,
|
||||
old_context_start_row,
|
||||
new_context_start_row,
|
||||
);
|
||||
|
||||
Some(diff)
|
||||
}
|
||||
|
||||
fn buffer_path_with_id_fallback(
|
||||
file: Option<&Arc<dyn File>>,
|
||||
snapshot: &TextBufferSnapshot,
|
||||
@@ -643,7 +701,7 @@ impl EditPredictionStore {
|
||||
&self,
|
||||
project: &Entity<Project>,
|
||||
cx: &App,
|
||||
) -> Vec<Arc<zeta_prompt::Event>> {
|
||||
) -> Vec<StoredEvent> {
|
||||
self.projects
|
||||
.get(&project.entity_id())
|
||||
.map(|project_state| project_state.events(cx))
|
||||
@@ -654,7 +712,7 @@ impl EditPredictionStore {
|
||||
&self,
|
||||
project: &Entity<Project>,
|
||||
cx: &App,
|
||||
) -> Vec<Arc<zeta_prompt::Event>> {
|
||||
) -> Vec<StoredEvent> {
|
||||
self.projects
|
||||
.get(&project.entity_id())
|
||||
.map(|project_state| project_state.events_split_by_pause(cx))
|
||||
@@ -1536,8 +1594,10 @@ impl EditPredictionStore {
|
||||
|
||||
self.get_or_init_project(&project, cx);
|
||||
let project_state = self.projects.get(&project.entity_id()).unwrap();
|
||||
let events = project_state.events(cx);
|
||||
let has_events = !events.is_empty();
|
||||
let stored_events = project_state.events(cx);
|
||||
let has_events = !stored_events.is_empty();
|
||||
let events: Vec<Arc<zeta_prompt::Event>> =
|
||||
stored_events.into_iter().map(|e| e.event).collect();
|
||||
let debug_tx = project_state.debug_tx.clone();
|
||||
|
||||
let snapshot = active_buffer.read(cx).snapshot();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::*;
|
||||
use crate::{udiff::apply_diff_to_string, zeta1::MAX_EVENT_TOKENS};
|
||||
use crate::{compute_diff_between_snapshots, udiff::apply_diff_to_string, zeta1::MAX_EVENT_TOKENS};
|
||||
use client::{UserStore, test::FakeServer};
|
||||
use clock::{FakeSystemClock, ReplicaId};
|
||||
use cloud_api_types::{CreateLlmTokenResponse, LlmToken};
|
||||
@@ -360,7 +360,7 @@ async fn test_edit_history_getter_pause_splits_last_event(cx: &mut TestAppContex
|
||||
ep_store.edit_history_for_project(&project, cx)
|
||||
});
|
||||
assert_eq!(events.len(), 1);
|
||||
let zeta_prompt::Event::BufferChange { diff, .. } = events[0].as_ref();
|
||||
let zeta_prompt::Event::BufferChange { diff, .. } = events[0].event.as_ref();
|
||||
assert_eq!(
|
||||
diff.as_str(),
|
||||
indoc! {"
|
||||
@@ -377,7 +377,7 @@ async fn test_edit_history_getter_pause_splits_last_event(cx: &mut TestAppContex
|
||||
ep_store.edit_history_for_project_with_pause_split_last_event(&project, cx)
|
||||
});
|
||||
assert_eq!(events.len(), 2);
|
||||
let zeta_prompt::Event::BufferChange { diff, .. } = events[0].as_ref();
|
||||
let zeta_prompt::Event::BufferChange { diff, .. } = events[0].event.as_ref();
|
||||
assert_eq!(
|
||||
diff.as_str(),
|
||||
indoc! {"
|
||||
@@ -389,7 +389,7 @@ async fn test_edit_history_getter_pause_splits_last_event(cx: &mut TestAppContex
|
||||
"}
|
||||
);
|
||||
|
||||
let zeta_prompt::Event::BufferChange { diff, .. } = events[1].as_ref();
|
||||
let zeta_prompt::Event::BufferChange { diff, .. } = events[1].event.as_ref();
|
||||
assert_eq!(
|
||||
diff.as_str(),
|
||||
indoc! {"
|
||||
@@ -2082,6 +2082,74 @@ async fn test_unauthenticated_with_custom_url_allows_prediction_impl(cx: &mut Te
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_compute_diff_between_snapshots(cx: &mut TestAppContext) {
|
||||
let buffer = cx.new(|cx| {
|
||||
Buffer::local(
|
||||
indoc! {"
|
||||
zero
|
||||
one
|
||||
two
|
||||
three
|
||||
four
|
||||
five
|
||||
six
|
||||
seven
|
||||
eight
|
||||
nine
|
||||
ten
|
||||
eleven
|
||||
twelve
|
||||
thirteen
|
||||
fourteen
|
||||
fifteen
|
||||
sixteen
|
||||
seventeen
|
||||
eighteen
|
||||
nineteen
|
||||
twenty
|
||||
twenty-one
|
||||
twenty-two
|
||||
twenty-three
|
||||
twenty-four
|
||||
"},
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let old_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
|
||||
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
let point = Point::new(12, 0);
|
||||
buffer.edit([(point..point, "SECOND INSERTION\n")], None, cx);
|
||||
let point = Point::new(8, 0);
|
||||
buffer.edit([(point..point, "FIRST INSERTION\n")], None, cx);
|
||||
});
|
||||
|
||||
let new_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot());
|
||||
|
||||
let diff = compute_diff_between_snapshots(&old_snapshot, &new_snapshot).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
diff,
|
||||
indoc! {"
|
||||
@@ -6,10 +6,12 @@
|
||||
five
|
||||
six
|
||||
seven
|
||||
+FIRST INSERTION
|
||||
eight
|
||||
nine
|
||||
ten
|
||||
eleven
|
||||
+SECOND INSERTION
|
||||
twelve
|
||||
thirteen
|
||||
fourteen
|
||||
"}
|
||||
);
|
||||
}
|
||||
|
||||
#[ctor::ctor]
|
||||
fn init_logger() {
|
||||
zlog::init_test();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{fmt::Write as _, mem, path::Path, sync::Arc};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct ExampleSpec {
|
||||
#[serde(default)]
|
||||
pub name: String,
|
||||
|
||||
@@ -45,6 +45,11 @@ pub async fn run_format_prompt(
|
||||
let snapshot = state.buffer.read_with(&cx, |buffer, _| buffer.snapshot())?;
|
||||
let project = state.project.clone();
|
||||
let (_, input) = ep_store.update(&mut cx, |ep_store, cx| {
|
||||
let events = ep_store
|
||||
.edit_history_for_project(&project, cx)
|
||||
.into_iter()
|
||||
.map(|e| e.event)
|
||||
.collect();
|
||||
anyhow::Ok(zeta2_prompt_input(
|
||||
&snapshot,
|
||||
example
|
||||
@@ -53,7 +58,7 @@ pub async fn run_format_prompt(
|
||||
.context("context must be set")?
|
||||
.files
|
||||
.clone(),
|
||||
ep_store.edit_history_for_project(&project, cx),
|
||||
events,
|
||||
example.spec.cursor_path.clone(),
|
||||
example
|
||||
.buffer
|
||||
|
||||
@@ -15,8 +15,7 @@ doctest = false
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
buffer_diff.workspace = true
|
||||
git.workspace = true
|
||||
log.workspace = true
|
||||
collections.workspace = true
|
||||
time.workspace = true
|
||||
client.workspace = true
|
||||
cloud_llm_client.workspace = true
|
||||
@@ -50,11 +49,18 @@ zed_actions.workspace = true
|
||||
zeta_prompt.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
clock.workspace = true
|
||||
copilot = { workspace = true, features = ["test-support"] }
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
futures.workspace = true
|
||||
indoc.workspace = true
|
||||
language_model.workspace = true
|
||||
lsp = { workspace = true, features = ["test-support"] }
|
||||
pretty_assertions.workspace = true
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
release_channel.workspace = true
|
||||
semver.workspace = true
|
||||
serde_json.workspace = true
|
||||
theme = { workspace = true, features = ["test-support"] }
|
||||
workspace = { workspace = true, features = ["test-support"] }
|
||||
zlog.workspace = true
|
||||
|
||||
@@ -915,11 +915,8 @@ impl EditPredictionButton {
|
||||
.when(
|
||||
cx.has_flag::<PredictEditsRatePredictionsFeatureFlag>(),
|
||||
|this| {
|
||||
this.action(
|
||||
"Capture Edit Prediction Example",
|
||||
CaptureExample.boxed_clone(),
|
||||
)
|
||||
.action("Rate Predictions", RatePredictions.boxed_clone())
|
||||
this.action("Capture Prediction Example", CaptureExample.boxed_clone())
|
||||
.action("Rate Predictions", RatePredictions.boxed_clone())
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,25 +2,17 @@ mod edit_prediction_button;
|
||||
mod edit_prediction_context_view;
|
||||
mod rate_prediction_modal;
|
||||
|
||||
use std::any::{Any as _, TypeId};
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
use edit_prediction::{
|
||||
EditPredictionStore, ResetOnboarding, Zeta2FeatureFlag, example_spec::ExampleSpec,
|
||||
};
|
||||
use edit_prediction::{ResetOnboarding, Zeta2FeatureFlag, capture_example};
|
||||
use edit_prediction_context_view::EditPredictionContextView;
|
||||
use editor::Editor;
|
||||
use feature_flags::FeatureFlagAppExt as _;
|
||||
use git::repository::DiffType;
|
||||
use gpui::{Window, actions};
|
||||
use language::ToPoint as _;
|
||||
use log;
|
||||
use gpui::actions;
|
||||
use language::language_settings::AllLanguageSettings;
|
||||
use project::DisableAiSettings;
|
||||
use rate_prediction_modal::RatePredictionsModal;
|
||||
use settings::{Settings as _, SettingsStore};
|
||||
use text::ToOffset as _;
|
||||
use std::any::{Any as _, TypeId};
|
||||
use ui::{App, prelude::*};
|
||||
use workspace::{SplitDirection, Workspace};
|
||||
|
||||
@@ -56,7 +48,9 @@ pub fn init(cx: &mut App) {
|
||||
}
|
||||
});
|
||||
|
||||
workspace.register_action(capture_edit_prediction_example);
|
||||
workspace.register_action(|workspace, _: &CaptureExample, window, cx| {
|
||||
capture_example_as_markdown(workspace, window, cx);
|
||||
});
|
||||
workspace.register_action_renderer(|div, _, _, cx| {
|
||||
let has_flag = cx.has_flag::<Zeta2FeatureFlag>();
|
||||
div.when(has_flag, |div| {
|
||||
@@ -138,182 +132,48 @@ fn feature_gate_predict_edits_actions(cx: &mut App) {
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn capture_edit_prediction_example(
|
||||
fn capture_example_as_markdown(
|
||||
workspace: &mut Workspace,
|
||||
_: &CaptureExample,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) {
|
||||
let Some(ep_store) = EditPredictionStore::try_global(cx) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let project = workspace.project().clone();
|
||||
|
||||
let (worktree_root, repository) = {
|
||||
let project_ref = project.read(cx);
|
||||
let worktree_root = project_ref
|
||||
.visible_worktrees(cx)
|
||||
.next()
|
||||
.map(|worktree| worktree.read(cx).abs_path());
|
||||
let repository = project_ref.active_repository(cx);
|
||||
(worktree_root, repository)
|
||||
};
|
||||
|
||||
let (Some(worktree_root), Some(repository)) = (worktree_root, repository) else {
|
||||
log::error!("CaptureExampleSpec: missing worktree or active repository");
|
||||
return;
|
||||
};
|
||||
|
||||
let repository_snapshot = repository.read(cx).snapshot();
|
||||
if worktree_root.as_ref() != repository_snapshot.work_directory_abs_path.as_ref() {
|
||||
log::error!(
|
||||
"repository is not at worktree root (repo={:?}, worktree={:?})",
|
||||
repository_snapshot.work_directory_abs_path,
|
||||
worktree_root
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(repository_url) = repository_snapshot
|
||||
.remote_origin_url
|
||||
.clone()
|
||||
.or_else(|| repository_snapshot.remote_upstream_url.clone())
|
||||
else {
|
||||
log::error!("active repository has no origin/upstream remote url");
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(revision) = repository_snapshot
|
||||
.head_commit
|
||||
.as_ref()
|
||||
.map(|commit| commit.sha.to_string())
|
||||
else {
|
||||
log::error!("active repository has no head commit");
|
||||
return;
|
||||
};
|
||||
|
||||
let mut events = ep_store.update(cx, |store, cx| {
|
||||
store.edit_history_for_project_with_pause_split_last_event(&project, cx)
|
||||
});
|
||||
|
||||
let Some(editor) = workspace.active_item_as::<Editor>(cx) else {
|
||||
log::error!("no active editor");
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(project_path) = editor.read(cx).project_path(cx) else {
|
||||
log::error!("active editor has no project path");
|
||||
return;
|
||||
};
|
||||
|
||||
let Some((buffer, cursor_anchor)) = editor
|
||||
.read(cx)
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.text_anchor_for_position(editor.read(cx).selections.newest_anchor().head(), cx)
|
||||
else {
|
||||
log::error!("failed to resolve cursor buffer/anchor");
|
||||
return;
|
||||
};
|
||||
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
let cursor_point = cursor_anchor.to_point(&snapshot);
|
||||
let (_editable_range, context_range) =
|
||||
edit_prediction::cursor_excerpt::editable_and_context_ranges_for_cursor_position(
|
||||
cursor_point,
|
||||
&snapshot,
|
||||
100,
|
||||
50,
|
||||
);
|
||||
|
||||
let cursor_path: Arc<Path> = repository
|
||||
.read(cx)
|
||||
.project_path_to_repo_path(&project_path, cx)
|
||||
.map(|repo_path| Path::new(repo_path.as_unix_str()).into())
|
||||
.unwrap_or_else(|| Path::new(project_path.path.as_unix_str()).into());
|
||||
|
||||
let cursor_position = {
|
||||
let context_start_offset = context_range.start.to_offset(&snapshot);
|
||||
let cursor_offset = cursor_anchor.to_offset(&snapshot);
|
||||
let cursor_offset_in_excerpt = cursor_offset.saturating_sub(context_start_offset);
|
||||
let mut excerpt = snapshot.text_for_range(context_range).collect::<String>();
|
||||
if cursor_offset_in_excerpt <= excerpt.len() {
|
||||
excerpt.insert_str(cursor_offset_in_excerpt, zeta_prompt::CURSOR_MARKER);
|
||||
}
|
||||
excerpt
|
||||
};
|
||||
|
||||
) -> Option<()> {
|
||||
let markdown_language = workspace
|
||||
.app_state()
|
||||
.languages
|
||||
.language_for_name("Markdown");
|
||||
|
||||
let fs = workspace.app_state().fs.clone();
|
||||
let project = workspace.project().clone();
|
||||
let editor = workspace.active_item_as::<Editor>(cx)?;
|
||||
let editor = editor.read(cx);
|
||||
let (buffer, cursor_anchor) = editor
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.text_anchor_for_position(editor.selections.newest_anchor().head(), cx)?;
|
||||
let example = capture_example(project.clone(), buffer, cursor_anchor, true, cx)?;
|
||||
|
||||
let examples_dir = AllLanguageSettings::get_global(cx)
|
||||
.edit_predictions
|
||||
.examples_dir
|
||||
.clone();
|
||||
|
||||
cx.spawn_in(window, async move |workspace_entity, cx| {
|
||||
let markdown_language = markdown_language.await?;
|
||||
let example_spec = example.await?;
|
||||
let buffer = if let Some(dir) = examples_dir {
|
||||
fs.create_dir(&dir).await.ok();
|
||||
let mut path = dir.join(&example_spec.name.replace(' ', "--").replace(':', "-"));
|
||||
path.set_extension("md");
|
||||
project.update(cx, |project, cx| project.open_local_buffer(&path, cx))
|
||||
} else {
|
||||
project.update(cx, |project, cx| project.create_buffer(false, cx))
|
||||
}?
|
||||
.await?;
|
||||
|
||||
let uncommitted_diff_rx = repository.update(cx, |repository, cx| {
|
||||
repository.diff(DiffType::HeadToWorktree, cx)
|
||||
})?;
|
||||
|
||||
let uncommitted_diff = match uncommitted_diff_rx.await {
|
||||
Ok(Ok(diff)) => diff,
|
||||
Ok(Err(error)) => {
|
||||
log::error!("failed to compute uncommitted diff: {error:#}");
|
||||
return Ok(());
|
||||
}
|
||||
Err(error) => {
|
||||
log::error!("uncommitted diff channel dropped: {error:#}");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
let mut edit_history = String::new();
|
||||
let mut expected_patch = String::new();
|
||||
if let Some(last_event) = events.pop() {
|
||||
for event in &events {
|
||||
zeta_prompt::write_event(&mut edit_history, event);
|
||||
if !edit_history.ends_with('\n') {
|
||||
edit_history.push('\n');
|
||||
}
|
||||
edit_history.push('\n');
|
||||
}
|
||||
|
||||
zeta_prompt::write_event(&mut expected_patch, &last_event);
|
||||
}
|
||||
|
||||
let format =
|
||||
time::format_description::parse("[year]-[month]-[day] [hour]:[minute]:[second]");
|
||||
let name = match format {
|
||||
Ok(format) => {
|
||||
let now = time::OffsetDateTime::now_local()
|
||||
.unwrap_or_else(|_| time::OffsetDateTime::now_utc());
|
||||
now.format(&format)
|
||||
.unwrap_or_else(|_| "unknown-time".to_string())
|
||||
}
|
||||
Err(_) => "unknown-time".to_string(),
|
||||
};
|
||||
|
||||
let markdown = ExampleSpec {
|
||||
name,
|
||||
repository_url,
|
||||
revision,
|
||||
uncommitted_diff,
|
||||
cursor_path,
|
||||
cursor_position,
|
||||
edit_history,
|
||||
expected_patch,
|
||||
}
|
||||
.to_markdown();
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| project.create_buffer(false, cx))?
|
||||
.await?;
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.set_text(markdown, cx);
|
||||
buffer.set_text(example_spec.to_markdown(), cx);
|
||||
buffer.set_language(Some(markdown_language), cx);
|
||||
})?;
|
||||
|
||||
workspace_entity.update_in(cx, |workspace, window, cx| {
|
||||
workspace.add_item_to_active_pane(
|
||||
Box::new(
|
||||
@@ -327,4 +187,5 @@ fn capture_edit_prediction_example(
|
||||
})
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
None
|
||||
}
|
||||
|
||||
@@ -18346,7 +18346,7 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppCon
|
||||
);
|
||||
|
||||
update_test_project_settings(cx, |project_settings| {
|
||||
project_settings.lsp.insert(
|
||||
project_settings.lsp.0.insert(
|
||||
"Some other server name".into(),
|
||||
LspSettings {
|
||||
binary: None,
|
||||
@@ -18367,7 +18367,7 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppCon
|
||||
);
|
||||
|
||||
update_test_project_settings(cx, |project_settings| {
|
||||
project_settings.lsp.insert(
|
||||
project_settings.lsp.0.insert(
|
||||
language_server_name.into(),
|
||||
LspSettings {
|
||||
binary: None,
|
||||
@@ -18388,7 +18388,7 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppCon
|
||||
);
|
||||
|
||||
update_test_project_settings(cx, |project_settings| {
|
||||
project_settings.lsp.insert(
|
||||
project_settings.lsp.0.insert(
|
||||
language_server_name.into(),
|
||||
LspSettings {
|
||||
binary: None,
|
||||
@@ -18409,7 +18409,7 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppCon
|
||||
);
|
||||
|
||||
update_test_project_settings(cx, |project_settings| {
|
||||
project_settings.lsp.insert(
|
||||
project_settings.lsp.0.insert(
|
||||
language_server_name.into(),
|
||||
LspSettings {
|
||||
binary: None,
|
||||
@@ -29602,6 +29602,17 @@ async fn test_newline_task_list_continuation(cx: &mut TestAppContext) {
|
||||
- [ ] ˇ
|
||||
"});
|
||||
|
||||
// Case 2.1: Works with uppercase checked marker too
|
||||
cx.set_state(indoc! {"
|
||||
- [X] completed taskˇ
|
||||
"});
|
||||
cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
|
||||
cx.wait_for_autoindent_applied().await;
|
||||
cx.assert_editor_state(indoc! {"
|
||||
- [X] completed task
|
||||
- [ ] ˇ
|
||||
"});
|
||||
|
||||
// Case 3: Cursor position doesn't matter - content after marker is what counts
|
||||
cx.set_state(indoc! {"
|
||||
- [ ] taˇsk
|
||||
|
||||
@@ -164,11 +164,6 @@ pub fn deploy_context_menu(
|
||||
window.focus(&editor.focus_handle(cx), cx);
|
||||
}
|
||||
|
||||
// Don't show context menu for inline editors
|
||||
if !editor.mode().is_full() {
|
||||
return;
|
||||
}
|
||||
|
||||
let display_map = editor.display_snapshot(cx);
|
||||
let source_anchor = display_map.display_point_to_anchor(point, text::Bias::Right);
|
||||
let context_menu = if let Some(custom) = editor.custom_context_menu.take() {
|
||||
@@ -179,6 +174,11 @@ pub fn deploy_context_menu(
|
||||
};
|
||||
menu
|
||||
} else {
|
||||
// Don't show context menu for inline editors (only applies to default menu)
|
||||
if !editor.mode().is_full() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't show the context menu if there isn't a project associated with this editor
|
||||
let Some(project) = editor.project.clone() else {
|
||||
return;
|
||||
|
||||
@@ -1760,16 +1760,19 @@ impl PickerDelegate for FileFinderDelegate {
|
||||
menu.context(focus_handle)
|
||||
.action(
|
||||
"Split Left",
|
||||
pane::SplitLeft.boxed_clone(),
|
||||
pane::SplitLeft::default().boxed_clone(),
|
||||
)
|
||||
.action(
|
||||
"Split Right",
|
||||
pane::SplitRight.boxed_clone(),
|
||||
pane::SplitRight::default().boxed_clone(),
|
||||
)
|
||||
.action(
|
||||
"Split Up",
|
||||
pane::SplitUp::default().boxed_clone(),
|
||||
)
|
||||
.action("Split Up", pane::SplitUp.boxed_clone())
|
||||
.action(
|
||||
"Split Down",
|
||||
pane::SplitDown.boxed_clone(),
|
||||
pane::SplitDown::default().boxed_clone(),
|
||||
)
|
||||
}
|
||||
}))
|
||||
|
||||
@@ -156,8 +156,16 @@ impl GitRepository for FakeGitRepository {
|
||||
})
|
||||
}
|
||||
|
||||
fn remote_url(&self, _name: &str) -> BoxFuture<'_, Option<String>> {
|
||||
async move { None }.boxed()
|
||||
fn remote_url(&self, name: &str) -> BoxFuture<'_, Option<String>> {
|
||||
let name = name.to_string();
|
||||
let fut = self.with_state_async(false, move |state| {
|
||||
state
|
||||
.remotes
|
||||
.get(&name)
|
||||
.context("remote not found")
|
||||
.cloned()
|
||||
});
|
||||
async move { fut.await.ok() }.boxed()
|
||||
}
|
||||
|
||||
fn diff_tree(&self, _request: DiffTreeType) -> BoxFuture<'_, Result<TreeDiff>> {
|
||||
|
||||
@@ -1857,6 +1857,18 @@ impl FakeFs {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub fn set_remote_for_repo(
|
||||
&self,
|
||||
dot_git: &Path,
|
||||
name: impl Into<String>,
|
||||
url: impl Into<String>,
|
||||
) {
|
||||
self.with_git_state(dot_git, true, |state| {
|
||||
state.remotes.insert(name.into(), url.into());
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub fn insert_branches(&self, dot_git: &Path, branches: &[&str]) {
|
||||
self.with_git_state(dot_git, true, |state| {
|
||||
if let Some(first) = branches.first()
|
||||
|
||||
@@ -8,9 +8,9 @@ use git::{
|
||||
parse_git_remote_url,
|
||||
};
|
||||
use gpui::{
|
||||
AnyElement, App, AppContext as _, AsyncApp, AsyncWindowContext, Context, Element, Entity,
|
||||
EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement,
|
||||
PromptLevel, Render, Styled, Task, WeakEntity, Window, actions,
|
||||
AnyElement, App, AppContext as _, AsyncApp, AsyncWindowContext, ClipboardItem, Context,
|
||||
Element, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement,
|
||||
ParentElement, PromptLevel, Render, Styled, Task, WeakEntity, Window, actions,
|
||||
};
|
||||
use language::{
|
||||
Anchor, Buffer, Capability, DiskState, File, LanguageRegistry, LineEnding, OffsetRangeExt as _,
|
||||
@@ -24,7 +24,7 @@ use std::{
|
||||
sync::Arc,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
use ui::{DiffStat, Tooltip, prelude::*};
|
||||
use ui::{ButtonLike, DiffStat, Tooltip, prelude::*};
|
||||
use util::{ResultExt, paths::PathStyle, rel_path::RelPath, truncate_and_trailoff};
|
||||
use workspace::item::TabTooltipContent;
|
||||
use workspace::{
|
||||
@@ -383,6 +383,7 @@ impl CommitView {
|
||||
fn render_header(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let commit = &self.commit;
|
||||
let author_name = commit.author_name.clone();
|
||||
let commit_sha = commit.sha.clone();
|
||||
let commit_date = time::OffsetDateTime::from_unix_timestamp(commit.commit_timestamp)
|
||||
.unwrap_or_else(|_| time::OffsetDateTime::now_utc());
|
||||
let local_offset = time::UtcOffset::current_local_offset().unwrap_or(time::UtcOffset::UTC);
|
||||
@@ -429,6 +430,19 @@ impl CommitView {
|
||||
.full_width()
|
||||
});
|
||||
|
||||
let clipboard_has_link = cx
|
||||
.read_from_clipboard()
|
||||
.and_then(|entry| entry.text())
|
||||
.map_or(false, |clipboard_text| {
|
||||
clipboard_text.trim() == commit_sha.as_ref()
|
||||
});
|
||||
|
||||
let (copy_icon, copy_icon_color) = if clipboard_has_link {
|
||||
(IconName::Check, Color::Success)
|
||||
} else {
|
||||
(IconName::Copy, Color::Muted)
|
||||
};
|
||||
|
||||
h_flex()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
@@ -454,13 +468,47 @@ impl CommitView {
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(Label::new(author_name).color(Color::Default))
|
||||
.child(
|
||||
Label::new(format!("Commit:{}", commit.sha))
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small)
|
||||
.truncate()
|
||||
.buffer_font(cx),
|
||||
),
|
||||
.child({
|
||||
ButtonLike::new("sha")
|
||||
.child(
|
||||
h_flex()
|
||||
.group("sha_btn")
|
||||
.size_full()
|
||||
.max_w_32()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
Label::new(commit_sha.clone())
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small)
|
||||
.truncate()
|
||||
.buffer_font(cx),
|
||||
)
|
||||
.child(
|
||||
div().visible_on_hover("sha_btn").child(
|
||||
Icon::new(copy_icon)
|
||||
.color(copy_icon_color)
|
||||
.size(IconSize::Small),
|
||||
),
|
||||
),
|
||||
)
|
||||
.tooltip({
|
||||
let commit_sha = commit_sha.clone();
|
||||
move |_, cx| {
|
||||
Tooltip::with_meta(
|
||||
"Copy Commit SHA",
|
||||
None,
|
||||
commit_sha.clone(),
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click(move |_, _, cx| {
|
||||
cx.stop_propagation();
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(
|
||||
commit_sha.to_string(),
|
||||
));
|
||||
})
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
|
||||
@@ -3638,7 +3638,7 @@ impl GitPanel {
|
||||
self.entry_count += 1;
|
||||
let is_staging_or_staged = GitPanel::stage_status_for_entry(status_entry, repo)
|
||||
.as_bool()
|
||||
.unwrap_or(false);
|
||||
.unwrap_or(true);
|
||||
|
||||
if repo.had_conflict_on_last_merge_head_change(&status_entry.repo_path) {
|
||||
self.conflicted_count += 1;
|
||||
|
||||
@@ -2154,7 +2154,6 @@ impl Interactivity {
|
||||
|| cx.active_drag.is_some() && !self.drag_over_styles.is_empty()
|
||||
{
|
||||
let hitbox = hitbox.clone();
|
||||
let was_hovered = hitbox.is_hovered(window);
|
||||
let hover_state = self.hover_style.as_ref().and_then(|_| {
|
||||
element_state
|
||||
.as_ref()
|
||||
@@ -2162,8 +2161,12 @@ impl Interactivity {
|
||||
.cloned()
|
||||
});
|
||||
let current_view = window.current_view();
|
||||
|
||||
window.on_mouse_event(move |_: &MouseMoveEvent, phase, window, cx| {
|
||||
let hovered = hitbox.is_hovered(window);
|
||||
let was_hovered = hover_state
|
||||
.as_ref()
|
||||
.is_some_and(|state| state.borrow().element);
|
||||
if phase == DispatchPhase::Capture && hovered != was_hovered {
|
||||
if let Some(hover_state) = &hover_state {
|
||||
hover_state.borrow_mut().element = hovered;
|
||||
@@ -2179,12 +2182,13 @@ impl Interactivity {
|
||||
.as_ref()
|
||||
.and_then(|element| element.hover_state.as_ref())
|
||||
.cloned();
|
||||
|
||||
let was_group_hovered = group_hitbox_id.is_hovered(window);
|
||||
let current_view = window.current_view();
|
||||
|
||||
window.on_mouse_event(move |_: &MouseMoveEvent, phase, window, cx| {
|
||||
let group_hovered = group_hitbox_id.is_hovered(window);
|
||||
let was_group_hovered = hover_state
|
||||
.as_ref()
|
||||
.is_some_and(|state| state.borrow().group);
|
||||
if phase == DispatchPhase::Capture && group_hovered != was_group_hovered {
|
||||
if let Some(hover_state) = &hover_state {
|
||||
hover_state.borrow_mut().group = group_hovered;
|
||||
|
||||
@@ -46,9 +46,9 @@ pub unsafe fn new_renderer(
|
||||
_native_window: *mut c_void,
|
||||
_native_view: *mut c_void,
|
||||
_bounds: crate::Size<f32>,
|
||||
_transparent: bool,
|
||||
transparent: bool,
|
||||
) -> Renderer {
|
||||
MetalRenderer::new(context)
|
||||
MetalRenderer::new(context, transparent)
|
||||
}
|
||||
|
||||
pub(crate) struct InstanceBufferPool {
|
||||
@@ -128,7 +128,7 @@ pub struct PathRasterizationVertex {
|
||||
}
|
||||
|
||||
impl MetalRenderer {
|
||||
pub fn new(instance_buffer_pool: Arc<Mutex<InstanceBufferPool>>) -> Self {
|
||||
pub fn new(instance_buffer_pool: Arc<Mutex<InstanceBufferPool>>, transparent: bool) -> Self {
|
||||
// Prefer low‐power integrated GPUs on Intel Mac. On Apple
|
||||
// Silicon, there is only ever one GPU, so this is equivalent to
|
||||
// `metal::Device::system_default()`.
|
||||
@@ -152,7 +152,9 @@ impl MetalRenderer {
|
||||
let layer = metal::MetalLayer::new();
|
||||
layer.set_device(&device);
|
||||
layer.set_pixel_format(MTLPixelFormat::BGRA8Unorm);
|
||||
layer.set_opaque(false);
|
||||
// Support direct-to-display rendering if the window is not transparent
|
||||
// https://developer.apple.com/documentation/metal/managing-your-game-window-for-metal-in-macos
|
||||
layer.set_opaque(!transparent);
|
||||
layer.set_maximum_drawable_count(3);
|
||||
unsafe {
|
||||
let _: () = msg_send![&*layer, setAllowsNextDrawableTimeout: NO];
|
||||
@@ -352,8 +354,8 @@ impl MetalRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_transparency(&self, _transparent: bool) {
|
||||
// todo(mac)?
|
||||
pub fn update_transparency(&self, transparent: bool) {
|
||||
self.layer.set_opaque(!transparent);
|
||||
}
|
||||
|
||||
pub fn destroy(&self) {
|
||||
|
||||
@@ -42,7 +42,7 @@ impl WindowsWindowInner {
|
||||
let handled = match msg {
|
||||
// eagerly activate the window, so calls to `active_window` will work correctly
|
||||
WM_MOUSEACTIVATE => {
|
||||
unsafe { SetActiveWindow(handle).log_err() };
|
||||
unsafe { SetActiveWindow(handle).ok() };
|
||||
None
|
||||
}
|
||||
WM_ACTIVATE => self.handle_activate_msg(wparam),
|
||||
|
||||
@@ -740,8 +740,8 @@ impl PlatformWindow for WindowsWindow {
|
||||
ShowWindowAsync(hwnd, SW_RESTORE).ok().log_err();
|
||||
}
|
||||
|
||||
SetActiveWindow(hwnd).log_err();
|
||||
SetFocus(Some(hwnd)).log_err();
|
||||
SetActiveWindow(hwnd).ok();
|
||||
SetFocus(Some(hwnd)).ok();
|
||||
}
|
||||
|
||||
// premium ragebait by windows, this is needed because the window
|
||||
|
||||
@@ -20,6 +20,7 @@ dap.workspace = true
|
||||
extension.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
lsp.workspace = true
|
||||
paths.workspace = true
|
||||
project.workspace = true
|
||||
schemars.workspace = true
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
use std::{str::FromStr, sync::Arc};
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use gpui::{App, AsyncApp, BorrowAppContext as _, Entity, WeakEntity};
|
||||
use gpui::{App, AsyncApp, BorrowAppContext as _, Entity, Task, WeakEntity};
|
||||
use language::{LanguageRegistry, language_settings::all_language_settings};
|
||||
use project::LspStore;
|
||||
use lsp::LanguageServerBinaryOptions;
|
||||
use project::{LspStore, lsp_store::LocalLspAdapterDelegate};
|
||||
use settings::LSP_SETTINGS_SCHEMA_URL_PREFIX;
|
||||
use util::schemars::{AllowTrailingCommas, DefaultDenyUnknownFields};
|
||||
|
||||
// Origin: https://github.com/SchemaStore/schemastore
|
||||
@@ -75,23 +77,28 @@ fn handle_schema_request(
|
||||
lsp_store: Entity<LspStore>,
|
||||
uri: String,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<String> {
|
||||
let languages = lsp_store.read_with(cx, |lsp_store, _| lsp_store.languages.clone())?;
|
||||
let schema = resolve_schema_request(&languages, uri, cx)?;
|
||||
serde_json::to_string(&schema).context("Failed to serialize schema")
|
||||
) -> Task<Result<String>> {
|
||||
let languages = lsp_store.read_with(cx, |lsp_store, _| lsp_store.languages.clone());
|
||||
cx.spawn(async move |cx| {
|
||||
let languages = languages?;
|
||||
let schema = resolve_schema_request(&languages, lsp_store, uri, cx).await?;
|
||||
serde_json::to_string(&schema).context("Failed to serialize schema")
|
||||
})
|
||||
}
|
||||
|
||||
pub fn resolve_schema_request(
|
||||
pub async fn resolve_schema_request(
|
||||
languages: &Arc<LanguageRegistry>,
|
||||
lsp_store: Entity<LspStore>,
|
||||
uri: String,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<serde_json::Value> {
|
||||
let path = uri.strip_prefix("zed://schemas/").context("Invalid URI")?;
|
||||
resolve_schema_request_inner(languages, path, cx)
|
||||
resolve_schema_request_inner(languages, lsp_store, path, cx).await
|
||||
}
|
||||
|
||||
pub fn resolve_schema_request_inner(
|
||||
pub async fn resolve_schema_request_inner(
|
||||
languages: &Arc<LanguageRegistry>,
|
||||
lsp_store: Entity<LspStore>,
|
||||
path: &str,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<serde_json::Value> {
|
||||
@@ -99,37 +106,106 @@ pub fn resolve_schema_request_inner(
|
||||
let schema_name = schema_name.unwrap_or(path);
|
||||
|
||||
let schema = match schema_name {
|
||||
"settings" => cx.update(|cx| {
|
||||
let font_names = &cx.text_system().all_font_names();
|
||||
let language_names = &languages
|
||||
.language_names()
|
||||
"settings" if rest.is_some_and(|r| r.starts_with("lsp/")) => {
|
||||
let lsp_name = rest
|
||||
.and_then(|r| {
|
||||
r.strip_prefix(
|
||||
LSP_SETTINGS_SCHEMA_URL_PREFIX
|
||||
.strip_prefix("zed://schemas/settings/")
|
||||
.unwrap(),
|
||||
)
|
||||
})
|
||||
.context("Invalid LSP schema path")?;
|
||||
|
||||
let adapter = languages
|
||||
.all_lsp_adapters()
|
||||
.into_iter()
|
||||
.map(|name| name.to_string())
|
||||
.find(|adapter| adapter.name().as_ref() as &str == lsp_name)
|
||||
.with_context(|| format!("LSP adapter not found: {}", lsp_name))?;
|
||||
|
||||
let delegate = cx.update(|inner_cx| {
|
||||
lsp_store.update(inner_cx, |lsp_store, inner_cx| {
|
||||
let Some(local) = lsp_store.as_local() else {
|
||||
return None;
|
||||
};
|
||||
let Some(worktree) = local.worktree_store.read(inner_cx).worktrees().next() else {
|
||||
return None;
|
||||
};
|
||||
Some(LocalLspAdapterDelegate::from_local_lsp(
|
||||
local, &worktree, inner_cx,
|
||||
))
|
||||
})
|
||||
})?.context("Failed to create adapter delegate - either LSP store is not in local mode or no worktree is available")?;
|
||||
|
||||
let adapter_for_schema = adapter.clone();
|
||||
|
||||
let binary = adapter
|
||||
.get_language_server_command(
|
||||
delegate,
|
||||
None,
|
||||
LanguageServerBinaryOptions {
|
||||
allow_path_lookup: true,
|
||||
allow_binary_download: false,
|
||||
pre_release: false,
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.await
|
||||
.await
|
||||
.0.with_context(|| format!("Failed to find language server {lsp_name} to generate initialization params schema"))?;
|
||||
|
||||
adapter_for_schema
|
||||
.adapter
|
||||
.clone()
|
||||
.initialization_options_schema(&binary)
|
||||
.await
|
||||
.unwrap_or_else(|| {
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"additionalProperties": true
|
||||
})
|
||||
})
|
||||
}
|
||||
"settings" => {
|
||||
let lsp_adapter_names = languages
|
||||
.all_lsp_adapters()
|
||||
.into_iter()
|
||||
.map(|adapter| adapter.name().to_string())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut icon_theme_names = vec![];
|
||||
let mut theme_names = vec![];
|
||||
if let Some(registry) = theme::ThemeRegistry::try_global(cx) {
|
||||
icon_theme_names.extend(
|
||||
registry
|
||||
.list_icon_themes()
|
||||
.into_iter()
|
||||
.map(|icon_theme| icon_theme.name),
|
||||
);
|
||||
theme_names.extend(registry.list_names());
|
||||
}
|
||||
let icon_theme_names = icon_theme_names.as_slice();
|
||||
let theme_names = theme_names.as_slice();
|
||||
cx.update(|cx| {
|
||||
let font_names = &cx.text_system().all_font_names();
|
||||
let language_names = &languages
|
||||
.language_names()
|
||||
.into_iter()
|
||||
.map(|name| name.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
cx.global::<settings::SettingsStore>().json_schema(
|
||||
&settings::SettingsJsonSchemaParams {
|
||||
language_names,
|
||||
font_names,
|
||||
theme_names,
|
||||
icon_theme_names,
|
||||
},
|
||||
)
|
||||
})?,
|
||||
let mut icon_theme_names = vec![];
|
||||
let mut theme_names = vec![];
|
||||
if let Some(registry) = theme::ThemeRegistry::try_global(cx) {
|
||||
icon_theme_names.extend(
|
||||
registry
|
||||
.list_icon_themes()
|
||||
.into_iter()
|
||||
.map(|icon_theme| icon_theme.name),
|
||||
);
|
||||
theme_names.extend(registry.list_names());
|
||||
}
|
||||
let icon_theme_names = icon_theme_names.as_slice();
|
||||
let theme_names = theme_names.as_slice();
|
||||
|
||||
cx.global::<settings::SettingsStore>().json_schema(
|
||||
&settings::SettingsJsonSchemaParams {
|
||||
language_names,
|
||||
font_names,
|
||||
theme_names,
|
||||
icon_theme_names,
|
||||
lsp_adapter_names: &lsp_adapter_names,
|
||||
},
|
||||
)
|
||||
})?
|
||||
}
|
||||
"keymap" => cx.update(settings::KeymapFile::generate_json_schema_for_registered_actions)?,
|
||||
"action" => {
|
||||
let normalized_action_name = rest.context("No Action name provided")?;
|
||||
|
||||
@@ -67,7 +67,7 @@ use task::RunnableTag;
|
||||
pub use task_context::{ContextLocation, ContextProvider, RunnableRange};
|
||||
pub use text_diff::{
|
||||
DiffOptions, apply_diff_patch, line_diff, text_diff, text_diff_with_options, unified_diff,
|
||||
word_diff_ranges,
|
||||
unified_diff_with_offsets, word_diff_ranges,
|
||||
};
|
||||
use theme::SyntaxTheme;
|
||||
pub use toolchain::{
|
||||
@@ -461,6 +461,14 @@ pub trait LspAdapter: 'static + Send + Sync + DynLspInstaller {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Returns the JSON schema of the initialization_options for the language server.
|
||||
async fn initialization_options_schema(
|
||||
self: Arc<Self>,
|
||||
_language_server_binary: &LanguageServerBinary,
|
||||
) -> Option<serde_json::Value> {
|
||||
None
|
||||
}
|
||||
|
||||
async fn workspace_configuration(
|
||||
self: Arc<Self>,
|
||||
_: &Arc<dyn LspAdapterDelegate>,
|
||||
|
||||
@@ -392,6 +392,7 @@ pub struct EditPredictionSettings {
|
||||
/// Whether edit predictions are enabled in the assistant panel.
|
||||
/// This setting has no effect if globally disabled.
|
||||
pub enabled_in_text_threads: bool,
|
||||
pub examples_dir: Option<Arc<Path>>,
|
||||
}
|
||||
|
||||
impl EditPredictionSettings {
|
||||
@@ -699,6 +700,7 @@ impl settings::Settings for AllLanguageSettings {
|
||||
copilot: copilot_settings,
|
||||
codestral: codestral_settings,
|
||||
enabled_in_text_threads,
|
||||
examples_dir: edit_predictions.examples_dir,
|
||||
},
|
||||
defaults: default_language_settings,
|
||||
languages,
|
||||
|
||||
@@ -1,25 +1,139 @@
|
||||
use crate::{CharClassifier, CharKind, CharScopeContext, LanguageScope};
|
||||
use anyhow::{Context, anyhow};
|
||||
use imara_diff::{
|
||||
Algorithm, UnifiedDiffBuilder, diff,
|
||||
intern::{InternedInput, Token},
|
||||
Algorithm, Sink, diff,
|
||||
intern::{InternedInput, Interner, Token},
|
||||
sources::lines_with_terminator,
|
||||
};
|
||||
use std::{iter, ops::Range, sync::Arc};
|
||||
use std::{fmt::Write, iter, ops::Range, sync::Arc};
|
||||
|
||||
const MAX_WORD_DIFF_LEN: usize = 512;
|
||||
const MAX_WORD_DIFF_LINE_COUNT: usize = 8;
|
||||
|
||||
/// Computes a diff between two strings, returning a unified diff string.
|
||||
pub fn unified_diff(old_text: &str, new_text: &str) -> String {
|
||||
unified_diff_with_offsets(old_text, new_text, 0, 0)
|
||||
}
|
||||
|
||||
/// Computes a diff between two strings, returning a unified diff string with
|
||||
/// hunk headers adjusted to reflect the given starting line numbers (1-indexed).
|
||||
pub fn unified_diff_with_offsets(
|
||||
old_text: &str,
|
||||
new_text: &str,
|
||||
old_start_line: u32,
|
||||
new_start_line: u32,
|
||||
) -> String {
|
||||
let input = InternedInput::new(old_text, new_text);
|
||||
diff(
|
||||
Algorithm::Histogram,
|
||||
&input,
|
||||
UnifiedDiffBuilder::new(&input),
|
||||
OffsetUnifiedDiffBuilder::new(&input, old_start_line, new_start_line),
|
||||
)
|
||||
}
|
||||
|
||||
/// A unified diff builder that applies line number offsets to hunk headers.
|
||||
struct OffsetUnifiedDiffBuilder<'a> {
|
||||
before: &'a [Token],
|
||||
after: &'a [Token],
|
||||
interner: &'a Interner<&'a str>,
|
||||
|
||||
pos: u32,
|
||||
before_hunk_start: u32,
|
||||
after_hunk_start: u32,
|
||||
before_hunk_len: u32,
|
||||
after_hunk_len: u32,
|
||||
|
||||
old_line_offset: u32,
|
||||
new_line_offset: u32,
|
||||
|
||||
buffer: String,
|
||||
dst: String,
|
||||
}
|
||||
|
||||
impl<'a> OffsetUnifiedDiffBuilder<'a> {
|
||||
fn new(input: &'a InternedInput<&'a str>, old_line_offset: u32, new_line_offset: u32) -> Self {
|
||||
Self {
|
||||
before_hunk_start: 0,
|
||||
after_hunk_start: 0,
|
||||
before_hunk_len: 0,
|
||||
after_hunk_len: 0,
|
||||
old_line_offset,
|
||||
new_line_offset,
|
||||
buffer: String::with_capacity(8),
|
||||
dst: String::new(),
|
||||
interner: &input.interner,
|
||||
before: &input.before,
|
||||
after: &input.after,
|
||||
pos: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn print_tokens(&mut self, tokens: &[Token], prefix: char) {
|
||||
for &token in tokens {
|
||||
writeln!(&mut self.buffer, "{prefix}{}", self.interner[token]).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&mut self) {
|
||||
if self.before_hunk_len == 0 && self.after_hunk_len == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let end = (self.pos + 3).min(self.before.len() as u32);
|
||||
self.update_pos(end, end);
|
||||
|
||||
writeln!(
|
||||
&mut self.dst,
|
||||
"@@ -{},{} +{},{} @@",
|
||||
self.before_hunk_start + 1 + self.old_line_offset,
|
||||
self.before_hunk_len,
|
||||
self.after_hunk_start + 1 + self.new_line_offset,
|
||||
self.after_hunk_len,
|
||||
)
|
||||
.unwrap();
|
||||
write!(&mut self.dst, "{}", &self.buffer).unwrap();
|
||||
self.buffer.clear();
|
||||
self.before_hunk_len = 0;
|
||||
self.after_hunk_len = 0;
|
||||
}
|
||||
|
||||
fn update_pos(&mut self, print_to: u32, move_to: u32) {
|
||||
self.print_tokens(&self.before[self.pos as usize..print_to as usize], ' ');
|
||||
let len = print_to - self.pos;
|
||||
self.pos = move_to;
|
||||
self.before_hunk_len += len;
|
||||
self.after_hunk_len += len;
|
||||
}
|
||||
}
|
||||
|
||||
impl Sink for OffsetUnifiedDiffBuilder<'_> {
|
||||
type Out = String;
|
||||
|
||||
fn process_change(&mut self, before: Range<u32>, after: Range<u32>) {
|
||||
if before.start - self.pos > 6 {
|
||||
self.flush();
|
||||
}
|
||||
if self.before_hunk_len == 0 && self.after_hunk_len == 0 {
|
||||
self.pos = before.start.saturating_sub(3);
|
||||
self.before_hunk_start = self.pos;
|
||||
self.after_hunk_start = after.start.saturating_sub(3);
|
||||
}
|
||||
self.update_pos(before.start, before.end);
|
||||
self.before_hunk_len += before.end - before.start;
|
||||
self.after_hunk_len += after.end - after.start;
|
||||
self.print_tokens(
|
||||
&self.before[before.start as usize..before.end as usize],
|
||||
'-',
|
||||
);
|
||||
self.print_tokens(&self.after[after.start as usize..after.end as usize], '+');
|
||||
}
|
||||
|
||||
fn finish(mut self) -> Self::Out {
|
||||
self.flush();
|
||||
self.dst
|
||||
}
|
||||
}
|
||||
|
||||
/// Computes a diff between two strings, returning a vector of old and new row
|
||||
/// ranges.
|
||||
pub fn line_diff(old_text: &str, new_text: &str) -> Vec<(Range<u32>, Range<u32>)> {
|
||||
@@ -327,4 +441,30 @@ mod tests {
|
||||
let patch = unified_diff(old_text, new_text);
|
||||
assert_eq!(apply_diff_patch(old_text, &patch).unwrap(), new_text);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unified_diff_with_offsets() {
|
||||
let old_text = "foo\nbar\nbaz\n";
|
||||
let new_text = "foo\nBAR\nbaz\n";
|
||||
|
||||
let expected_diff_body = " foo\n-bar\n+BAR\n baz\n";
|
||||
|
||||
let diff_no_offset = unified_diff(old_text, new_text);
|
||||
assert_eq!(
|
||||
diff_no_offset,
|
||||
format!("@@ -1,3 +1,3 @@\n{}", expected_diff_body)
|
||||
);
|
||||
|
||||
let diff_with_offset = unified_diff_with_offsets(old_text, new_text, 9, 11);
|
||||
assert_eq!(
|
||||
diff_with_offset,
|
||||
format!("@@ -10,3 +12,3 @@\n{}", expected_diff_body)
|
||||
);
|
||||
|
||||
let diff_with_offset = unified_diff_with_offsets(old_text, new_text, 99, 104);
|
||||
assert_eq!(
|
||||
diff_with_offset,
|
||||
format!("@@ -100,3 +105,3 @@\n{}", expected_diff_body)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ rewrap_prefixes = [
|
||||
]
|
||||
unordered_list = ["- ", "* ", "+ "]
|
||||
ordered_list = [{ pattern = "(\\d+)\\. ", format = "{1}. " }]
|
||||
task_list = { prefixes = ["- [ ] ", "- [x] "], continuation = "- [ ] " }
|
||||
task_list = { prefixes = ["- [ ] ", "- [x] ", "- [X] "], continuation = "- [ ] " }
|
||||
|
||||
auto_indent_on_paste = false
|
||||
auto_indent_using_last_non_empty_line = false
|
||||
|
||||
@@ -26,6 +26,7 @@ use settings::Settings;
|
||||
use smol::lock::OnceCell;
|
||||
use std::cmp::{Ordering, Reverse};
|
||||
use std::env::consts;
|
||||
use std::process::Stdio;
|
||||
use terminal::terminal_settings::TerminalSettings;
|
||||
use util::command::new_smol_command;
|
||||
use util::fs::{make_file_executable, remove_matching};
|
||||
@@ -2173,6 +2174,119 @@ pub(crate) struct RuffLspAdapter {
|
||||
fs: Arc<dyn Fs>,
|
||||
}
|
||||
|
||||
impl RuffLspAdapter {
|
||||
fn convert_ruff_schema(raw_schema: &serde_json::Value) -> serde_json::Value {
|
||||
let Some(schema_object) = raw_schema.as_object() else {
|
||||
return raw_schema.clone();
|
||||
};
|
||||
|
||||
let mut root_properties = serde_json::Map::new();
|
||||
|
||||
for (key, value) in schema_object {
|
||||
let parts: Vec<&str> = key.split('.').collect();
|
||||
|
||||
if parts.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut current = &mut root_properties;
|
||||
|
||||
for (i, part) in parts.iter().enumerate() {
|
||||
let is_last = i == parts.len() - 1;
|
||||
|
||||
if is_last {
|
||||
let mut schema_entry = serde_json::Map::new();
|
||||
|
||||
if let Some(doc) = value.get("doc").and_then(|d| d.as_str()) {
|
||||
schema_entry.insert(
|
||||
"markdownDescription".to_string(),
|
||||
serde_json::Value::String(doc.to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(default_val) = value.get("default") {
|
||||
schema_entry.insert("default".to_string(), default_val.clone());
|
||||
}
|
||||
|
||||
if let Some(value_type) = value.get("value_type").and_then(|v| v.as_str()) {
|
||||
if value_type.contains('|') {
|
||||
let enum_values: Vec<serde_json::Value> = value_type
|
||||
.split('|')
|
||||
.map(|s| s.trim().trim_matches('"'))
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| serde_json::Value::String(s.to_string()))
|
||||
.collect();
|
||||
|
||||
if !enum_values.is_empty() {
|
||||
schema_entry
|
||||
.insert("type".to_string(), serde_json::json!("string"));
|
||||
schema_entry.insert(
|
||||
"enum".to_string(),
|
||||
serde_json::Value::Array(enum_values),
|
||||
);
|
||||
}
|
||||
} else if value_type.starts_with("list[") {
|
||||
schema_entry.insert("type".to_string(), serde_json::json!("array"));
|
||||
if let Some(item_type) = value_type
|
||||
.strip_prefix("list[")
|
||||
.and_then(|s| s.strip_suffix(']'))
|
||||
{
|
||||
let json_type = match item_type {
|
||||
"str" => "string",
|
||||
"int" => "integer",
|
||||
"bool" => "boolean",
|
||||
_ => "string",
|
||||
};
|
||||
schema_entry.insert(
|
||||
"items".to_string(),
|
||||
serde_json::json!({"type": json_type}),
|
||||
);
|
||||
}
|
||||
} else if value_type.starts_with("dict[") {
|
||||
schema_entry.insert("type".to_string(), serde_json::json!("object"));
|
||||
} else {
|
||||
let json_type = match value_type {
|
||||
"bool" => "boolean",
|
||||
"int" | "usize" => "integer",
|
||||
"str" => "string",
|
||||
_ => "string",
|
||||
};
|
||||
schema_entry.insert(
|
||||
"type".to_string(),
|
||||
serde_json::Value::String(json_type.to_string()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
current.insert(part.to_string(), serde_json::Value::Object(schema_entry));
|
||||
} else {
|
||||
let next_current = current
|
||||
.entry(part.to_string())
|
||||
.or_insert_with(|| {
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
})
|
||||
})
|
||||
.as_object_mut()
|
||||
.expect("should be an object")
|
||||
.entry("properties")
|
||||
.or_insert_with(|| serde_json::json!({}))
|
||||
.as_object_mut()
|
||||
.expect("properties should be an object");
|
||||
|
||||
current = next_current;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": root_properties
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
impl RuffLspAdapter {
|
||||
const GITHUB_ASSET_KIND: AssetKind = AssetKind::TarGz;
|
||||
@@ -2225,6 +2339,36 @@ impl LspAdapter for RuffLspAdapter {
|
||||
fn name(&self) -> LanguageServerName {
|
||||
Self::SERVER_NAME
|
||||
}
|
||||
|
||||
async fn initialization_options_schema(
|
||||
self: Arc<Self>,
|
||||
language_server_binary: &LanguageServerBinary,
|
||||
) -> Option<serde_json::Value> {
|
||||
let mut command = util::command::new_smol_command(&language_server_binary.path);
|
||||
command
|
||||
.args(&["config", "--output-format", "json"])
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
let cmd = command
|
||||
.spawn()
|
||||
.map_err(|e| log::debug!("failed to spawn command {command:?}: {e}"))
|
||||
.ok()?;
|
||||
let output = cmd
|
||||
.output()
|
||||
.await
|
||||
.map_err(|e| log::debug!("failed to execute command {command:?}: {e}"))
|
||||
.ok()?;
|
||||
if !output.status.success() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let raw_schema: serde_json::Value = serde_json::from_slice(output.stdout.as_slice())
|
||||
.map_err(|e| log::debug!("failed to parse ruff's JSON schema output: {e}"))
|
||||
.ok()?;
|
||||
|
||||
let converted_schema = Self::convert_ruff_schema(&raw_schema);
|
||||
Some(converted_schema)
|
||||
}
|
||||
}
|
||||
|
||||
impl LspInstaller for RuffLspAdapter {
|
||||
@@ -2568,4 +2712,149 @@ mod tests {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_convert_ruff_schema() {
|
||||
use super::RuffLspAdapter;
|
||||
|
||||
let raw_schema = serde_json::json!({
|
||||
"line-length": {
|
||||
"doc": "The line length to use when enforcing long-lines violations",
|
||||
"default": "88",
|
||||
"value_type": "int",
|
||||
"scope": null,
|
||||
"example": "line-length = 120",
|
||||
"deprecated": null
|
||||
},
|
||||
"lint.select": {
|
||||
"doc": "A list of rule codes or prefixes to enable",
|
||||
"default": "[\"E4\", \"E7\", \"E9\", \"F\"]",
|
||||
"value_type": "list[RuleSelector]",
|
||||
"scope": null,
|
||||
"example": "select = [\"E4\", \"E7\", \"E9\", \"F\", \"B\", \"Q\"]",
|
||||
"deprecated": null
|
||||
},
|
||||
"lint.isort.case-sensitive": {
|
||||
"doc": "Sort imports taking into account case sensitivity.",
|
||||
"default": "false",
|
||||
"value_type": "bool",
|
||||
"scope": null,
|
||||
"example": "case-sensitive = true",
|
||||
"deprecated": null
|
||||
},
|
||||
"format.quote-style": {
|
||||
"doc": "Configures the preferred quote character for strings.",
|
||||
"default": "\"double\"",
|
||||
"value_type": "\"double\" | \"single\" | \"preserve\"",
|
||||
"scope": null,
|
||||
"example": "quote-style = \"single\"",
|
||||
"deprecated": null
|
||||
}
|
||||
});
|
||||
|
||||
let converted = RuffLspAdapter::convert_ruff_schema(&raw_schema);
|
||||
|
||||
assert!(converted.is_object());
|
||||
assert_eq!(
|
||||
converted.get("type").and_then(|v| v.as_str()),
|
||||
Some("object")
|
||||
);
|
||||
|
||||
let properties = converted
|
||||
.get("properties")
|
||||
.expect("should have properties")
|
||||
.as_object()
|
||||
.expect("properties should be an object");
|
||||
|
||||
assert!(properties.contains_key("line-length"));
|
||||
assert!(properties.contains_key("lint"));
|
||||
assert!(properties.contains_key("format"));
|
||||
|
||||
let line_length = properties
|
||||
.get("line-length")
|
||||
.expect("should have line-length")
|
||||
.as_object()
|
||||
.expect("line-length should be an object");
|
||||
|
||||
assert_eq!(
|
||||
line_length.get("type").and_then(|v| v.as_str()),
|
||||
Some("integer")
|
||||
);
|
||||
assert_eq!(
|
||||
line_length.get("default").and_then(|v| v.as_str()),
|
||||
Some("88")
|
||||
);
|
||||
|
||||
let lint = properties
|
||||
.get("lint")
|
||||
.expect("should have lint")
|
||||
.as_object()
|
||||
.expect("lint should be an object");
|
||||
|
||||
let lint_props = lint
|
||||
.get("properties")
|
||||
.expect("lint should have properties")
|
||||
.as_object()
|
||||
.expect("lint properties should be an object");
|
||||
|
||||
assert!(lint_props.contains_key("select"));
|
||||
assert!(lint_props.contains_key("isort"));
|
||||
|
||||
let select = lint_props.get("select").expect("should have select");
|
||||
assert_eq!(select.get("type").and_then(|v| v.as_str()), Some("array"));
|
||||
|
||||
let isort = lint_props
|
||||
.get("isort")
|
||||
.expect("should have isort")
|
||||
.as_object()
|
||||
.expect("isort should be an object");
|
||||
|
||||
let isort_props = isort
|
||||
.get("properties")
|
||||
.expect("isort should have properties")
|
||||
.as_object()
|
||||
.expect("isort properties should be an object");
|
||||
|
||||
let case_sensitive = isort_props
|
||||
.get("case-sensitive")
|
||||
.expect("should have case-sensitive");
|
||||
|
||||
assert_eq!(
|
||||
case_sensitive.get("type").and_then(|v| v.as_str()),
|
||||
Some("boolean")
|
||||
);
|
||||
assert!(case_sensitive.get("markdownDescription").is_some());
|
||||
|
||||
let format = properties
|
||||
.get("format")
|
||||
.expect("should have format")
|
||||
.as_object()
|
||||
.expect("format should be an object");
|
||||
|
||||
let format_props = format
|
||||
.get("properties")
|
||||
.expect("format should have properties")
|
||||
.as_object()
|
||||
.expect("format properties should be an object");
|
||||
|
||||
let quote_style = format_props
|
||||
.get("quote-style")
|
||||
.expect("should have quote-style");
|
||||
|
||||
assert_eq!(
|
||||
quote_style.get("type").and_then(|v| v.as_str()),
|
||||
Some("string")
|
||||
);
|
||||
|
||||
let enum_values = quote_style
|
||||
.get("enum")
|
||||
.expect("should have enum")
|
||||
.as_array()
|
||||
.expect("enum should be an array");
|
||||
|
||||
assert_eq!(enum_values.len(), 3);
|
||||
assert!(enum_values.contains(&serde_json::json!("double")));
|
||||
assert!(enum_values.contains(&serde_json::json!("single")));
|
||||
assert!(enum_values.contains(&serde_json::json!("preserve")));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ use smol::fs::{self};
|
||||
use std::cmp::Reverse;
|
||||
use std::fmt::Display;
|
||||
use std::ops::Range;
|
||||
use std::process::Stdio;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
path::{Path, PathBuf},
|
||||
@@ -66,6 +67,68 @@ enum LibcType {
|
||||
}
|
||||
|
||||
impl RustLspAdapter {
|
||||
fn convert_rust_analyzer_schema(raw_schema: &serde_json::Value) -> serde_json::Value {
|
||||
let Some(schema_array) = raw_schema.as_array() else {
|
||||
return raw_schema.clone();
|
||||
};
|
||||
|
||||
let mut root_properties = serde_json::Map::new();
|
||||
|
||||
for item in schema_array {
|
||||
if let Some(props) = item.get("properties").and_then(|p| p.as_object()) {
|
||||
for (key, value) in props {
|
||||
let parts: Vec<&str> = key.split('.').collect();
|
||||
|
||||
if parts.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let parts_to_process = if parts.first() == Some(&"rust-analyzer") {
|
||||
&parts[1..]
|
||||
} else {
|
||||
&parts[..]
|
||||
};
|
||||
|
||||
if parts_to_process.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut current = &mut root_properties;
|
||||
|
||||
for (i, part) in parts_to_process.iter().enumerate() {
|
||||
let is_last = i == parts_to_process.len() - 1;
|
||||
|
||||
if is_last {
|
||||
current.insert(part.to_string(), value.clone());
|
||||
} else {
|
||||
let next_current = current
|
||||
.entry(part.to_string())
|
||||
.or_insert_with(|| {
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
})
|
||||
})
|
||||
.as_object_mut()
|
||||
.expect("should be an object")
|
||||
.entry("properties")
|
||||
.or_insert_with(|| serde_json::json!({}))
|
||||
.as_object_mut()
|
||||
.expect("properties should be an object");
|
||||
|
||||
current = next_current;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": root_properties
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
async fn determine_libc_type() -> LibcType {
|
||||
use futures::pin_mut;
|
||||
@@ -448,6 +511,37 @@ impl LspAdapter for RustLspAdapter {
|
||||
Some(label)
|
||||
}
|
||||
|
||||
async fn initialization_options_schema(
|
||||
self: Arc<Self>,
|
||||
language_server_binary: &LanguageServerBinary,
|
||||
) -> Option<serde_json::Value> {
|
||||
let mut command = util::command::new_smol_command(&language_server_binary.path);
|
||||
command
|
||||
.arg("--print-config-schema")
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
let cmd = command
|
||||
.spawn()
|
||||
.map_err(|e| log::debug!("failed to spawn command {command:?}: {e}"))
|
||||
.ok()?;
|
||||
let output = cmd
|
||||
.output()
|
||||
.await
|
||||
.map_err(|e| log::debug!("failed to execute command {command:?}: {e}"))
|
||||
.ok()?;
|
||||
if !output.status.success() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let raw_schema: serde_json::Value = serde_json::from_slice(output.stdout.as_slice())
|
||||
.map_err(|e| log::debug!("failed to parse rust-analyzer's JSON schema output: {e}"))
|
||||
.ok()?;
|
||||
|
||||
// Convert rust-analyzer's array-based schema format to nested JSON Schema
|
||||
let converted_schema = Self::convert_rust_analyzer_schema(&raw_schema);
|
||||
Some(converted_schema)
|
||||
}
|
||||
|
||||
async fn label_for_symbol(
|
||||
&self,
|
||||
name: &str,
|
||||
@@ -1912,4 +2006,90 @@ mod tests {
|
||||
);
|
||||
check([], "/project/src/main.rs", "--");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_convert_rust_analyzer_schema() {
|
||||
let raw_schema = serde_json::json!([
|
||||
{
|
||||
"title": "Assist",
|
||||
"properties": {
|
||||
"rust-analyzer.assist.emitMustUse": {
|
||||
"markdownDescription": "Insert #[must_use] when generating `as_` methods for enum variants.",
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Assist",
|
||||
"properties": {
|
||||
"rust-analyzer.assist.expressionFillDefault": {
|
||||
"markdownDescription": "Placeholder expression to use for missing expressions in assists.",
|
||||
"default": "todo",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Cache Priming",
|
||||
"properties": {
|
||||
"rust-analyzer.cachePriming.enable": {
|
||||
"markdownDescription": "Warm up caches on project load.",
|
||||
"default": true,
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
let converted = RustLspAdapter::convert_rust_analyzer_schema(&raw_schema);
|
||||
|
||||
assert_eq!(
|
||||
converted.get("type").and_then(|v| v.as_str()),
|
||||
Some("object")
|
||||
);
|
||||
|
||||
let properties = converted
|
||||
.pointer("/properties")
|
||||
.expect("should have properties")
|
||||
.as_object()
|
||||
.expect("properties should be object");
|
||||
|
||||
assert!(properties.contains_key("assist"));
|
||||
assert!(properties.contains_key("cachePriming"));
|
||||
assert!(!properties.contains_key("rust-analyzer"));
|
||||
|
||||
let assist_props = properties
|
||||
.get("assist")
|
||||
.expect("should have assist")
|
||||
.pointer("/properties")
|
||||
.expect("assist should have properties")
|
||||
.as_object()
|
||||
.expect("assist properties should be object");
|
||||
|
||||
assert!(assist_props.contains_key("emitMustUse"));
|
||||
assert!(assist_props.contains_key("expressionFillDefault"));
|
||||
|
||||
let emit_must_use = assist_props
|
||||
.get("emitMustUse")
|
||||
.expect("should have emitMustUse");
|
||||
assert_eq!(
|
||||
emit_must_use.get("type").and_then(|v| v.as_str()),
|
||||
Some("boolean")
|
||||
);
|
||||
assert_eq!(
|
||||
emit_must_use.get("default").and_then(|v| v.as_bool()),
|
||||
Some(false)
|
||||
);
|
||||
|
||||
let cache_priming_props = properties
|
||||
.get("cachePriming")
|
||||
.expect("should have cachePriming")
|
||||
.pointer("/properties")
|
||||
.expect("cachePriming should have properties")
|
||||
.as_object()
|
||||
.expect("cachePriming properties should be object");
|
||||
|
||||
assert!(cache_priming_props.contains_key("enable"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -345,6 +345,7 @@ impl LspAdapter for VtslsLspAdapter {
|
||||
let lsp_settings = content
|
||||
.project
|
||||
.lsp
|
||||
.0
|
||||
.entry(VTSLS_SERVER_NAME.into())
|
||||
.or_default();
|
||||
|
||||
|
||||
@@ -22,9 +22,9 @@ use collections::{HashMap, HashSet};
|
||||
use gpui::{
|
||||
AnyElement, App, BorderStyle, Bounds, ClipboardItem, CursorStyle, DispatchPhase, Edges, Entity,
|
||||
FocusHandle, Focusable, FontStyle, FontWeight, GlobalElementId, Hitbox, Hsla, Image,
|
||||
ImageFormat, KeyContext, Length, MouseDownEvent, MouseEvent, MouseMoveEvent, MouseUpEvent,
|
||||
Point, ScrollHandle, Stateful, StrikethroughStyle, StyleRefinement, StyledText, Task,
|
||||
TextLayout, TextRun, TextStyle, TextStyleRefinement, actions, img, point, quad,
|
||||
ImageFormat, KeyContext, Length, MouseButton, MouseDownEvent, MouseEvent, MouseMoveEvent,
|
||||
MouseUpEvent, Point, ScrollHandle, Stateful, StrikethroughStyle, StyleRefinement, StyledText,
|
||||
Task, TextLayout, TextRun, TextStyle, TextStyleRefinement, actions, img, point, quad,
|
||||
};
|
||||
use language::{Language, LanguageRegistry, Rope};
|
||||
use parser::CodeBlockMetadata;
|
||||
@@ -112,6 +112,7 @@ pub struct Markdown {
|
||||
options: Options,
|
||||
copied_code_blocks: HashSet<ElementId>,
|
||||
code_block_scroll_handles: HashMap<usize, ScrollHandle>,
|
||||
context_menu_selected_text: Option<String>,
|
||||
}
|
||||
|
||||
struct Options {
|
||||
@@ -181,6 +182,7 @@ impl Markdown {
|
||||
},
|
||||
copied_code_blocks: HashSet::default(),
|
||||
code_block_scroll_handles: HashMap::default(),
|
||||
context_menu_selected_text: None,
|
||||
};
|
||||
this.parse(cx);
|
||||
this
|
||||
@@ -205,6 +207,7 @@ impl Markdown {
|
||||
},
|
||||
copied_code_blocks: HashSet::default(),
|
||||
code_block_scroll_handles: HashMap::default(),
|
||||
context_menu_selected_text: None,
|
||||
};
|
||||
this.parse(cx);
|
||||
this
|
||||
@@ -289,6 +292,14 @@ impl Markdown {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn selected_text(&self) -> Option<String> {
|
||||
if self.selection.end <= self.selection.start {
|
||||
None
|
||||
} else {
|
||||
Some(self.source[self.selection.start..self.selection.end].to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn copy(&self, text: &RenderedText, _: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.selection.end <= self.selection.start {
|
||||
return;
|
||||
@@ -297,7 +308,11 @@ impl Markdown {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(text));
|
||||
}
|
||||
|
||||
fn copy_as_markdown(&self, _: &mut Window, cx: &mut Context<Self>) {
|
||||
fn copy_as_markdown(&mut self, _: &mut Window, cx: &mut Context<Self>) {
|
||||
if let Some(text) = self.context_menu_selected_text.take() {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(text));
|
||||
return;
|
||||
}
|
||||
if self.selection.end <= self.selection.start {
|
||||
return;
|
||||
}
|
||||
@@ -305,6 +320,10 @@ impl Markdown {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(text));
|
||||
}
|
||||
|
||||
fn capture_selection_for_context_menu(&mut self) {
|
||||
self.context_menu_selected_text = self.selected_text();
|
||||
}
|
||||
|
||||
fn parse(&mut self, cx: &mut Context<Self>) {
|
||||
if self.source.is_empty() {
|
||||
return;
|
||||
@@ -665,6 +684,19 @@ impl MarkdownElement {
|
||||
|
||||
let on_open_url = self.on_url_click.take();
|
||||
|
||||
self.on_mouse_event(window, cx, {
|
||||
let hitbox = hitbox.clone();
|
||||
move |markdown, event: &MouseDownEvent, phase, window, _| {
|
||||
if phase.capture()
|
||||
&& event.button == MouseButton::Right
|
||||
&& hitbox.is_hovered(window)
|
||||
{
|
||||
// Capture selected text so it survives until menu item is clicked
|
||||
markdown.capture_selection_for_context_menu();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
self.on_mouse_event(window, cx, {
|
||||
let rendered_text = rendered_text.clone();
|
||||
let hitbox = hitbox.clone();
|
||||
@@ -713,7 +745,7 @@ impl MarkdownElement {
|
||||
window.prevent_default();
|
||||
cx.notify();
|
||||
}
|
||||
} else if phase.capture() {
|
||||
} else if phase.capture() && event.button == MouseButton::Left {
|
||||
markdown.selection = Selection::default();
|
||||
markdown.pressed_link = None;
|
||||
cx.notify();
|
||||
|
||||
@@ -1868,6 +1868,7 @@ pub struct BuiltinAgentServerSettings {
|
||||
pub ignore_system_version: Option<bool>,
|
||||
pub default_mode: Option<String>,
|
||||
pub default_model: Option<String>,
|
||||
pub favorite_models: Vec<String>,
|
||||
}
|
||||
|
||||
impl BuiltinAgentServerSettings {
|
||||
@@ -1891,6 +1892,7 @@ impl From<settings::BuiltinAgentServerSettings> for BuiltinAgentServerSettings {
|
||||
ignore_system_version: value.ignore_system_version,
|
||||
default_mode: value.default_mode,
|
||||
default_model: value.default_model,
|
||||
favorite_models: value.favorite_models,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1922,6 +1924,10 @@ pub enum CustomAgentServerSettings {
|
||||
///
|
||||
/// Default: None
|
||||
default_model: Option<String>,
|
||||
/// The favorite models for this agent.
|
||||
///
|
||||
/// Default: []
|
||||
favorite_models: Vec<String>,
|
||||
},
|
||||
Extension {
|
||||
/// The default mode to use for this agent.
|
||||
@@ -1936,6 +1942,10 @@ pub enum CustomAgentServerSettings {
|
||||
///
|
||||
/// Default: None
|
||||
default_model: Option<String>,
|
||||
/// The favorite models for this agent.
|
||||
///
|
||||
/// Default: []
|
||||
favorite_models: Vec<String>,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1962,6 +1972,17 @@ impl CustomAgentServerSettings {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn favorite_models(&self) -> &[String] {
|
||||
match self {
|
||||
CustomAgentServerSettings::Custom {
|
||||
favorite_models, ..
|
||||
}
|
||||
| CustomAgentServerSettings::Extension {
|
||||
favorite_models, ..
|
||||
} => favorite_models,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<settings::CustomAgentServerSettings> for CustomAgentServerSettings {
|
||||
@@ -1973,6 +1994,7 @@ impl From<settings::CustomAgentServerSettings> for CustomAgentServerSettings {
|
||||
env,
|
||||
default_mode,
|
||||
default_model,
|
||||
favorite_models,
|
||||
} => CustomAgentServerSettings::Custom {
|
||||
command: AgentServerCommand {
|
||||
path: PathBuf::from(shellexpand::tilde(&path.to_string_lossy()).as_ref()),
|
||||
@@ -1981,13 +2003,16 @@ impl From<settings::CustomAgentServerSettings> for CustomAgentServerSettings {
|
||||
},
|
||||
default_mode,
|
||||
default_model,
|
||||
favorite_models,
|
||||
},
|
||||
settings::CustomAgentServerSettings::Extension {
|
||||
default_mode,
|
||||
default_model,
|
||||
favorite_models,
|
||||
} => CustomAgentServerSettings::Extension {
|
||||
default_mode,
|
||||
default_model,
|
||||
favorite_models,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -2313,6 +2338,7 @@ mod extension_agent_tests {
|
||||
ignore_system_version: None,
|
||||
default_mode: None,
|
||||
default_model: None,
|
||||
favorite_models: vec![],
|
||||
};
|
||||
|
||||
let BuiltinAgentServerSettings { path, .. } = settings.into();
|
||||
@@ -2329,6 +2355,7 @@ mod extension_agent_tests {
|
||||
env: None,
|
||||
default_mode: None,
|
||||
default_model: None,
|
||||
favorite_models: vec![],
|
||||
};
|
||||
|
||||
let converted: CustomAgentServerSettings = settings.into();
|
||||
|
||||
@@ -5756,6 +5756,7 @@ impl Repository {
|
||||
|
||||
cx.spawn(|_: &mut AsyncApp| async move { rx.await? })
|
||||
}
|
||||
|
||||
fn load_blob_content(&mut self, oid: Oid, cx: &App) -> Task<Result<String>> {
|
||||
let repository_id = self.snapshot.id;
|
||||
let rx = self.send_job(None, move |state, _| async move {
|
||||
|
||||
@@ -257,7 +257,7 @@ struct DynamicRegistrations {
|
||||
|
||||
pub struct LocalLspStore {
|
||||
weak: WeakEntity<LspStore>,
|
||||
worktree_store: Entity<WorktreeStore>,
|
||||
pub worktree_store: Entity<WorktreeStore>,
|
||||
toolchain_store: Entity<LocalToolchainStore>,
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
environment: Entity<ProjectEnvironment>,
|
||||
@@ -13953,7 +13953,7 @@ impl LocalLspAdapterDelegate {
|
||||
})
|
||||
}
|
||||
|
||||
fn from_local_lsp(
|
||||
pub fn from_local_lsp(
|
||||
local: &LocalLspStore,
|
||||
worktree: &Entity<Worktree>,
|
||||
cx: &mut App,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use anyhow::{Context, Result};
|
||||
use gpui::{App, AsyncApp, Entity, Global, WeakEntity};
|
||||
use gpui::{App, AsyncApp, Entity, Global, Task, WeakEntity};
|
||||
use lsp::LanguageServer;
|
||||
|
||||
use crate::LspStore;
|
||||
@@ -22,7 +22,7 @@ impl lsp::request::Request for SchemaContentRequest {
|
||||
const METHOD: &'static str = "vscode/content";
|
||||
}
|
||||
|
||||
type SchemaRequestHandler = fn(Entity<LspStore>, String, &mut AsyncApp) -> Result<String>;
|
||||
type SchemaRequestHandler = fn(Entity<LspStore>, String, &mut AsyncApp) -> Task<Result<String>>;
|
||||
pub struct SchemaHandlingImpl(SchemaRequestHandler);
|
||||
|
||||
impl Global for SchemaHandlingImpl {}
|
||||
@@ -72,9 +72,7 @@ pub fn notify_schema_changed(lsp_store: Entity<LspStore>, uri: String, cx: &App)
|
||||
pub fn register_requests(lsp_store: WeakEntity<LspStore>, language_server: &LanguageServer) {
|
||||
language_server
|
||||
.on_request::<SchemaContentRequest, _, _>(move |params, cx| {
|
||||
let handler = cx.try_read_global::<SchemaHandlingImpl, _>(|handler, _| {
|
||||
handler.0
|
||||
});
|
||||
let handler = cx.try_read_global::<SchemaHandlingImpl, _>(|handler, _| handler.0);
|
||||
let mut cx = cx.clone();
|
||||
let uri = params.clone().pop();
|
||||
let lsp_store = lsp_store.clone();
|
||||
@@ -82,7 +80,7 @@ pub fn register_requests(lsp_store: WeakEntity<LspStore>, language_server: &Lang
|
||||
let lsp_store = lsp_store.upgrade().context("LSP store has been dropped")?;
|
||||
let uri = uri.context("No URI")?;
|
||||
let handle_schema_request = handler.context("No schema handler registered")?;
|
||||
handle_schema_request(lsp_store, uri, &mut cx)
|
||||
handle_schema_request(lsp_store, uri, &mut cx).await
|
||||
};
|
||||
async move {
|
||||
zlog::trace!(LOGGER => "Handling schema request for {:?}", ¶ms);
|
||||
|
||||
@@ -1293,17 +1293,33 @@ impl Project {
|
||||
cx.subscribe(&worktree_store, Self::on_worktree_store_event)
|
||||
.detach();
|
||||
if init_worktree_trust {
|
||||
match &connection_options {
|
||||
RemoteConnectionOptions::Wsl(..) | RemoteConnectionOptions::Ssh(..) => {
|
||||
trusted_worktrees::track_worktree_trust(
|
||||
worktree_store.clone(),
|
||||
Some(RemoteHostLocation::from(connection_options)),
|
||||
None,
|
||||
Some((remote_proto.clone(), REMOTE_SERVER_PROJECT_ID)),
|
||||
cx,
|
||||
);
|
||||
let trust_remote_project = match &connection_options {
|
||||
RemoteConnectionOptions::Ssh(..) | RemoteConnectionOptions::Wsl(..) => false,
|
||||
RemoteConnectionOptions::Docker(..) => true,
|
||||
};
|
||||
let remote_host = RemoteHostLocation::from(connection_options);
|
||||
trusted_worktrees::track_worktree_trust(
|
||||
worktree_store.clone(),
|
||||
Some(remote_host.clone()),
|
||||
None,
|
||||
Some((remote_proto.clone(), REMOTE_SERVER_PROJECT_ID)),
|
||||
cx,
|
||||
);
|
||||
if trust_remote_project {
|
||||
if let Some(trusted_worktres) = TrustedWorktrees::try_get_global(cx) {
|
||||
trusted_worktres.update(cx, |trusted_worktres, cx| {
|
||||
trusted_worktres.trust(
|
||||
worktree_store
|
||||
.read(cx)
|
||||
.worktrees()
|
||||
.map(|worktree| worktree.read(cx).id())
|
||||
.map(PathTrust::Worktree)
|
||||
.collect(),
|
||||
Some(remote_host),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
}
|
||||
RemoteConnectionOptions::Docker(..) => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -337,6 +337,13 @@ impl TrustedWorktreesStore {
|
||||
if restricted_host != remote_host {
|
||||
return true;
|
||||
}
|
||||
|
||||
// When trusting an abs path on the host, we transitively trust all single file worktrees on this host too.
|
||||
if is_file && !new_trusted_abs_paths.is_empty() {
|
||||
trusted_paths.insert(PathTrust::Worktree(*restricted_worktree));
|
||||
return false;
|
||||
}
|
||||
|
||||
let retain = (!is_file || new_trusted_other_worktrees.is_empty())
|
||||
&& new_trusted_abs_paths.iter().all(|new_trusted_path| {
|
||||
!restricted_worktree_path.starts_with(new_trusted_path)
|
||||
@@ -1045,6 +1052,13 @@ mod tests {
|
||||
"single-file worktree should be restricted initially"
|
||||
);
|
||||
|
||||
let can_trust_directory =
|
||||
trusted_worktrees.update(cx, |store, cx| store.can_trust(dir_worktree_id, cx));
|
||||
assert!(
|
||||
!can_trust_directory,
|
||||
"directory worktree should be restricted initially"
|
||||
);
|
||||
|
||||
trusted_worktrees.update(cx, |store, cx| {
|
||||
store.trust(
|
||||
HashSet::from_iter([PathTrust::Worktree(dir_worktree_id)]),
|
||||
@@ -1064,6 +1078,78 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_parent_path_trust_enables_single_file(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
path!("/"),
|
||||
json!({
|
||||
"project": { "main.rs": "fn main() {}" },
|
||||
"standalone.rs": "fn standalone() {}"
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(
|
||||
fs,
|
||||
[path!("/project").as_ref(), path!("/standalone.rs").as_ref()],
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
|
||||
let (dir_worktree_id, file_worktree_id) = worktree_store.read_with(cx, |store, cx| {
|
||||
let worktrees: Vec<_> = store.worktrees().collect();
|
||||
assert_eq!(worktrees.len(), 2);
|
||||
let (dir_worktree, file_worktree) = if worktrees[0].read(cx).is_single_file() {
|
||||
(&worktrees[1], &worktrees[0])
|
||||
} else {
|
||||
(&worktrees[0], &worktrees[1])
|
||||
};
|
||||
assert!(!dir_worktree.read(cx).is_single_file());
|
||||
assert!(file_worktree.read(cx).is_single_file());
|
||||
(dir_worktree.read(cx).id(), file_worktree.read(cx).id())
|
||||
});
|
||||
|
||||
let trusted_worktrees = init_trust_global(worktree_store, cx);
|
||||
|
||||
let can_trust_file =
|
||||
trusted_worktrees.update(cx, |store, cx| store.can_trust(file_worktree_id, cx));
|
||||
assert!(
|
||||
!can_trust_file,
|
||||
"single-file worktree should be restricted initially"
|
||||
);
|
||||
|
||||
let can_trust_directory =
|
||||
trusted_worktrees.update(cx, |store, cx| store.can_trust(dir_worktree_id, cx));
|
||||
assert!(
|
||||
!can_trust_directory,
|
||||
"directory worktree should be restricted initially"
|
||||
);
|
||||
|
||||
trusted_worktrees.update(cx, |store, cx| {
|
||||
store.trust(
|
||||
HashSet::from_iter([PathTrust::AbsPath(PathBuf::from(path!("/project")))]),
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
let can_trust_dir =
|
||||
trusted_worktrees.update(cx, |store, cx| store.can_trust(dir_worktree_id, cx));
|
||||
let can_trust_file_after =
|
||||
trusted_worktrees.update(cx, |store, cx| store.can_trust(file_worktree_id, cx));
|
||||
assert!(
|
||||
can_trust_dir,
|
||||
"directory worktree should be trusted after its parent is trusted"
|
||||
);
|
||||
assert!(
|
||||
can_trust_file_after,
|
||||
"single-file worktree should be trusted after directory worktree trust via its parent directory trust"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_abs_path_trust_covers_multiple_worktrees(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
@@ -53,7 +53,9 @@ async fn check_for_docker() -> Result<(), DevContainerError> {
|
||||
}
|
||||
}
|
||||
|
||||
async fn ensure_devcontainer_cli(node_runtime: NodeRuntime) -> Result<PathBuf, DevContainerError> {
|
||||
async fn ensure_devcontainer_cli(
|
||||
node_runtime: &NodeRuntime,
|
||||
) -> Result<(PathBuf, bool), DevContainerError> {
|
||||
let mut command = util::command::new_smol_command(&dev_container_cli());
|
||||
command.arg("--version");
|
||||
|
||||
@@ -63,23 +65,42 @@ async fn ensure_devcontainer_cli(node_runtime: NodeRuntime) -> Result<PathBuf, D
|
||||
e
|
||||
);
|
||||
|
||||
let Ok(node_runtime_path) = node_runtime.binary_path().await else {
|
||||
return Err(DevContainerError::NodeRuntimeNotAvailable);
|
||||
};
|
||||
|
||||
let datadir_cli_path = paths::devcontainer_dir()
|
||||
.join("node_modules")
|
||||
.join(".bin")
|
||||
.join(&dev_container_cli());
|
||||
.join("@devcontainers")
|
||||
.join("cli")
|
||||
.join(format!("{}.js", &dev_container_cli()));
|
||||
|
||||
log::debug!(
|
||||
"devcontainer not found in path, using local location: ${}",
|
||||
datadir_cli_path.display()
|
||||
);
|
||||
|
||||
let mut command =
|
||||
util::command::new_smol_command(&datadir_cli_path.as_os_str().display().to_string());
|
||||
util::command::new_smol_command(node_runtime_path.as_os_str().display().to_string());
|
||||
command.arg(datadir_cli_path.display().to_string());
|
||||
command.arg("--version");
|
||||
|
||||
if let Err(e) = command.output().await {
|
||||
log::error!(
|
||||
match command.output().await {
|
||||
Err(e) => log::error!(
|
||||
"Unable to find devcontainer CLI in Data dir. Will try to install. Error: {:?}",
|
||||
e
|
||||
);
|
||||
} else {
|
||||
log::info!("Found devcontainer CLI in Data dir");
|
||||
return Ok(datadir_cli_path.clone());
|
||||
),
|
||||
Ok(output) => {
|
||||
if output.status.success() {
|
||||
log::info!("Found devcontainer CLI in Data dir");
|
||||
return Ok((datadir_cli_path.clone(), false));
|
||||
} else {
|
||||
log::error!(
|
||||
"Could not run devcontainer CLI from data_dir. Will try once more to install. Output: {:?}",
|
||||
output
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(e) = fs::create_dir_all(paths::devcontainer_dir()).await {
|
||||
@@ -101,7 +122,9 @@ async fn ensure_devcontainer_cli(node_runtime: NodeRuntime) -> Result<PathBuf, D
|
||||
return Err(DevContainerError::DevContainerCliNotAvailable);
|
||||
};
|
||||
|
||||
let mut command = util::command::new_smol_command(&datadir_cli_path.display().to_string());
|
||||
let mut command =
|
||||
util::command::new_smol_command(node_runtime_path.as_os_str().display().to_string());
|
||||
command.arg(datadir_cli_path.display().to_string());
|
||||
command.arg("--version");
|
||||
if let Err(e) = command.output().await {
|
||||
log::error!(
|
||||
@@ -110,22 +133,42 @@ async fn ensure_devcontainer_cli(node_runtime: NodeRuntime) -> Result<PathBuf, D
|
||||
);
|
||||
Err(DevContainerError::DevContainerCliNotAvailable)
|
||||
} else {
|
||||
Ok(datadir_cli_path)
|
||||
Ok((datadir_cli_path, false))
|
||||
}
|
||||
} else {
|
||||
log::info!("Found devcontainer cli on $PATH, using it");
|
||||
Ok(PathBuf::from(&dev_container_cli()))
|
||||
Ok((PathBuf::from(&dev_container_cli()), true))
|
||||
}
|
||||
}
|
||||
|
||||
async fn devcontainer_up(
|
||||
path_to_cli: &PathBuf,
|
||||
found_in_path: bool,
|
||||
node_runtime: &NodeRuntime,
|
||||
path: Arc<Path>,
|
||||
) -> Result<DevContainerUp, DevContainerError> {
|
||||
let mut command = util::command::new_smol_command(path_to_cli.display().to_string());
|
||||
command.arg("up");
|
||||
command.arg("--workspace-folder");
|
||||
command.arg(path.display().to_string());
|
||||
let Ok(node_runtime_path) = node_runtime.binary_path().await else {
|
||||
log::error!("Unable to find node runtime path");
|
||||
return Err(DevContainerError::NodeRuntimeNotAvailable);
|
||||
};
|
||||
|
||||
let mut command = if found_in_path {
|
||||
let mut command = util::command::new_smol_command(path_to_cli.display().to_string());
|
||||
command.arg("up");
|
||||
command.arg("--workspace-folder");
|
||||
command.arg(path.display().to_string());
|
||||
command
|
||||
} else {
|
||||
let mut command =
|
||||
util::command::new_smol_command(node_runtime_path.as_os_str().display().to_string());
|
||||
command.arg(path_to_cli.display().to_string());
|
||||
command.arg("up");
|
||||
command.arg("--workspace-folder");
|
||||
command.arg(path.display().to_string());
|
||||
command
|
||||
};
|
||||
|
||||
log::debug!("Running full devcontainer up command: {:?}", command);
|
||||
|
||||
match command.output().await {
|
||||
Ok(output) => {
|
||||
@@ -235,7 +278,7 @@ pub(crate) async fn start_dev_container(
|
||||
) -> Result<(Connection, String), DevContainerError> {
|
||||
check_for_docker().await?;
|
||||
|
||||
let path_to_devcontainer_cli = ensure_devcontainer_cli(node_runtime).await?;
|
||||
let (path_to_devcontainer_cli, found_in_path) = ensure_devcontainer_cli(&node_runtime).await?;
|
||||
|
||||
let Some(directory) = project_directory(cx) else {
|
||||
return Err(DevContainerError::DevContainerNotFound);
|
||||
@@ -245,7 +288,13 @@ pub(crate) async fn start_dev_container(
|
||||
container_id,
|
||||
remote_workspace_folder,
|
||||
..
|
||||
}) = devcontainer_up(&path_to_devcontainer_cli, directory.clone()).await
|
||||
}) = devcontainer_up(
|
||||
&path_to_devcontainer_cli,
|
||||
found_in_path,
|
||||
&node_runtime,
|
||||
directory.clone(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
let project_name = get_project_name(
|
||||
&path_to_devcontainer_cli,
|
||||
@@ -273,6 +322,7 @@ pub(crate) enum DevContainerError {
|
||||
DevContainerUpFailed,
|
||||
DevContainerNotFound,
|
||||
DevContainerParseFailed,
|
||||
NodeRuntimeNotAvailable,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -158,6 +158,9 @@ fn handle_rpc_messages_over_child_process_stdio(
|
||||
}
|
||||
};
|
||||
let status = remote_proxy_process.status().await?.code().unwrap_or(1);
|
||||
if status != 0 {
|
||||
anyhow::bail!("Remote server exited with status {status}");
|
||||
}
|
||||
match result {
|
||||
Ok(_) => Ok(status),
|
||||
Err(error) => Err(error),
|
||||
|
||||
@@ -582,19 +582,21 @@ impl RemoteConnection for DockerExecConnection {
|
||||
return Task::ready(Err(anyhow!("Remote binary path not set")));
|
||||
};
|
||||
|
||||
let mut docker_args = vec![
|
||||
"exec".to_string(),
|
||||
"-w".to_string(),
|
||||
self.remote_dir_for_server.clone(),
|
||||
"-i".to_string(),
|
||||
self.connection_options.container_id.to_string(),
|
||||
];
|
||||
let mut docker_args = vec!["exec".to_string()];
|
||||
for env_var in ["RUST_LOG", "RUST_BACKTRACE", "ZED_GENERATE_MINIDUMPS"] {
|
||||
if let Some(value) = std::env::var(env_var).ok() {
|
||||
docker_args.push("-e".to_string());
|
||||
docker_args.push(format!("{}='{}'", env_var, value));
|
||||
}
|
||||
}
|
||||
|
||||
docker_args.extend([
|
||||
"-w".to_string(),
|
||||
self.remote_dir_for_server.clone(),
|
||||
"-i".to_string(),
|
||||
self.connection_options.container_id.to_string(),
|
||||
]);
|
||||
|
||||
let val = remote_binary_relpath
|
||||
.display(self.path_style())
|
||||
.into_owned();
|
||||
|
||||
@@ -56,6 +56,7 @@ merge_from_overwrites!(
|
||||
std::sync::Arc<str>,
|
||||
gpui::SharedString,
|
||||
std::path::PathBuf,
|
||||
std::sync::Arc<std::path::Path>,
|
||||
gpui::Modifiers,
|
||||
gpui::FontFeatures,
|
||||
gpui::FontWeight
|
||||
|
||||
@@ -33,8 +33,9 @@ pub use serde_helper::*;
|
||||
pub use settings_file::*;
|
||||
pub use settings_json::*;
|
||||
pub use settings_store::{
|
||||
InvalidSettingsError, LocalSettingsKind, MigrationStatus, ParseStatus, Settings, SettingsFile,
|
||||
SettingsJsonSchemaParams, SettingsKey, SettingsLocation, SettingsParseResult, SettingsStore,
|
||||
InvalidSettingsError, LSP_SETTINGS_SCHEMA_URL_PREFIX, LocalSettingsKind, MigrationStatus,
|
||||
ParseStatus, Settings, SettingsFile, SettingsJsonSchemaParams, SettingsKey, SettingsLocation,
|
||||
SettingsParseResult, SettingsStore,
|
||||
};
|
||||
|
||||
pub use vscode_import::{VsCodeSettings, VsCodeSettingsSource};
|
||||
|
||||
@@ -363,6 +363,13 @@ pub struct BuiltinAgentServerSettings {
|
||||
///
|
||||
/// Default: None
|
||||
pub default_model: Option<String>,
|
||||
/// The favorite models for this agent.
|
||||
///
|
||||
/// These are the model IDs as reported by the agent.
|
||||
///
|
||||
/// Default: []
|
||||
#[serde(default)]
|
||||
pub favorite_models: Vec<String>,
|
||||
}
|
||||
|
||||
#[with_fallible_options]
|
||||
@@ -387,6 +394,13 @@ pub enum CustomAgentServerSettings {
|
||||
///
|
||||
/// Default: None
|
||||
default_model: Option<String>,
|
||||
/// The favorite models for this agent.
|
||||
///
|
||||
/// These are the model IDs as reported by the agent.
|
||||
///
|
||||
/// Default: []
|
||||
#[serde(default)]
|
||||
favorite_models: Vec<String>,
|
||||
},
|
||||
Extension {
|
||||
/// The default mode to use for this agent.
|
||||
@@ -401,5 +415,12 @@ pub enum CustomAgentServerSettings {
|
||||
///
|
||||
/// Default: None
|
||||
default_model: Option<String>,
|
||||
/// The favorite models for this agent.
|
||||
///
|
||||
/// These are the model IDs as reported by the agent.
|
||||
///
|
||||
/// Default: []
|
||||
#[serde(default)]
|
||||
favorite_models: Vec<String>,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::num::NonZeroU32;
|
||||
use std::{num::NonZeroU32, path::Path};
|
||||
|
||||
use collections::{HashMap, HashSet};
|
||||
use gpui::{Modifiers, SharedString};
|
||||
@@ -167,6 +167,8 @@ pub struct EditPredictionSettingsContent {
|
||||
/// Whether edit predictions are enabled in the assistant prompt editor.
|
||||
/// This has no effect if globally disabled.
|
||||
pub enabled_in_text_threads: Option<bool>,
|
||||
/// The directory where manually captured edit prediction examples are stored.
|
||||
pub examples_dir: Option<Arc<Path>>,
|
||||
}
|
||||
|
||||
#[with_fallible_options]
|
||||
|
||||
@@ -11,6 +11,19 @@ use crate::{
|
||||
SlashCommandSettings,
|
||||
};
|
||||
|
||||
#[with_fallible_options]
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
|
||||
pub struct LspSettingsMap(pub HashMap<Arc<str>, LspSettings>);
|
||||
|
||||
impl IntoIterator for LspSettingsMap {
|
||||
type Item = (Arc<str>, LspSettings);
|
||||
type IntoIter = std::collections::hash_map::IntoIter<Arc<str>, LspSettings>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.0.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
#[with_fallible_options]
|
||||
#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom)]
|
||||
pub struct ProjectSettingsContent {
|
||||
@@ -29,7 +42,7 @@ pub struct ProjectSettingsContent {
|
||||
/// name to the lsp value.
|
||||
/// Default: null
|
||||
#[serde(default)]
|
||||
pub lsp: HashMap<Arc<str>, LspSettings>,
|
||||
pub lsp: LspSettingsMap,
|
||||
|
||||
pub terminal: Option<ProjectTerminalSettingsContent>,
|
||||
|
||||
|
||||
@@ -32,7 +32,8 @@ pub type EditorconfigProperties = ec4rs::Properties;
|
||||
|
||||
use crate::{
|
||||
ActiveSettingsProfileName, FontFamilyName, IconThemeName, LanguageSettingsContent,
|
||||
LanguageToSettingsMap, ThemeName, VsCodeSettings, WorktreeId, fallible_options,
|
||||
LanguageToSettingsMap, LspSettings, LspSettingsMap, ThemeName, VsCodeSettings, WorktreeId,
|
||||
fallible_options,
|
||||
merge_from::MergeFrom,
|
||||
settings_content::{
|
||||
ExtensionsSettingsContent, ProjectSettingsContent, SettingsContent, UserSettingsContent,
|
||||
@@ -41,6 +42,8 @@ use crate::{
|
||||
|
||||
use settings_json::{infer_json_indent_size, parse_json_with_comments, update_value_in_json_text};
|
||||
|
||||
pub const LSP_SETTINGS_SCHEMA_URL_PREFIX: &str = "zed://schemas/settings/lsp/";
|
||||
|
||||
pub trait SettingsKey: 'static + Send + Sync {
|
||||
/// The name of a key within the JSON file from which this setting should
|
||||
/// be deserialized. If this is `None`, then the setting will be deserialized
|
||||
@@ -256,6 +259,7 @@ pub struct SettingsJsonSchemaParams<'a> {
|
||||
pub font_names: &'a [String],
|
||||
pub theme_names: &'a [SharedString],
|
||||
pub icon_theme_names: &'a [SharedString],
|
||||
pub lsp_adapter_names: &'a [String],
|
||||
}
|
||||
|
||||
impl SettingsStore {
|
||||
@@ -1025,6 +1029,14 @@ impl SettingsStore {
|
||||
.subschema_for::<LanguageSettingsContent>()
|
||||
.to_value();
|
||||
|
||||
generator.subschema_for::<LspSettings>();
|
||||
|
||||
let lsp_settings_def = generator
|
||||
.definitions()
|
||||
.get("LspSettings")
|
||||
.expect("LspSettings should be defined")
|
||||
.clone();
|
||||
|
||||
replace_subschema::<LanguageToSettingsMap>(&mut generator, || {
|
||||
json_schema!({
|
||||
"type": "object",
|
||||
@@ -1063,6 +1075,38 @@ impl SettingsStore {
|
||||
})
|
||||
});
|
||||
|
||||
replace_subschema::<LspSettingsMap>(&mut generator, || {
|
||||
let mut lsp_properties = serde_json::Map::new();
|
||||
|
||||
for adapter_name in params.lsp_adapter_names {
|
||||
let mut base_lsp_settings = lsp_settings_def
|
||||
.as_object()
|
||||
.expect("LspSettings should be an object")
|
||||
.clone();
|
||||
|
||||
if let Some(properties) = base_lsp_settings.get_mut("properties") {
|
||||
if let Some(props_obj) = properties.as_object_mut() {
|
||||
props_obj.insert(
|
||||
"initialization_options".to_string(),
|
||||
serde_json::json!({
|
||||
"$ref": format!("{LSP_SETTINGS_SCHEMA_URL_PREFIX}{adapter_name}")
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
lsp_properties.insert(
|
||||
adapter_name.clone(),
|
||||
serde_json::Value::Object(base_lsp_settings),
|
||||
);
|
||||
}
|
||||
|
||||
json_schema!({
|
||||
"type": "object",
|
||||
"properties": lsp_properties,
|
||||
})
|
||||
});
|
||||
|
||||
generator
|
||||
.root_schema_for::<UserSettingsContent>()
|
||||
.to_value()
|
||||
@@ -2304,4 +2348,39 @@ mod tests {
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_lsp_settings_schema_generation(cx: &mut App) {
|
||||
let store = SettingsStore::test(cx);
|
||||
|
||||
let schema = store.json_schema(&SettingsJsonSchemaParams {
|
||||
language_names: &["Rust".to_string(), "TypeScript".to_string()],
|
||||
font_names: &["Zed Mono".to_string()],
|
||||
theme_names: &["One Dark".into()],
|
||||
icon_theme_names: &["Zed Icons".into()],
|
||||
lsp_adapter_names: &[
|
||||
"rust-analyzer".to_string(),
|
||||
"typescript-language-server".to_string(),
|
||||
],
|
||||
});
|
||||
|
||||
let properties = schema
|
||||
.pointer("/$defs/LspSettingsMap/properties")
|
||||
.expect("LspSettingsMap should have properties")
|
||||
.as_object()
|
||||
.unwrap();
|
||||
|
||||
assert!(properties.contains_key("rust-analyzer"));
|
||||
assert!(properties.contains_key("typescript-language-server"));
|
||||
|
||||
let init_options_ref = properties
|
||||
.get("rust-analyzer")
|
||||
.unwrap()
|
||||
.pointer("/properties/initialization_options/$ref")
|
||||
.expect("initialization_options should have a $ref")
|
||||
.as_str()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(init_options_ref, "zed://schemas/settings/lsp/rust-analyzer");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,8 +30,8 @@ use workspace::{
|
||||
ActivateNextPane, ActivatePane, ActivatePaneDown, ActivatePaneLeft, ActivatePaneRight,
|
||||
ActivatePaneUp, ActivatePreviousPane, DraggedSelection, DraggedTab, ItemId, MoveItemToPane,
|
||||
MoveItemToPaneInDirection, MovePaneDown, MovePaneLeft, MovePaneRight, MovePaneUp, NewTerminal,
|
||||
Pane, PaneGroup, SplitDirection, SplitDown, SplitLeft, SplitRight, SplitUp, SwapPaneDown,
|
||||
SwapPaneLeft, SwapPaneRight, SwapPaneUp, ToggleZoom, Workspace,
|
||||
Pane, PaneGroup, SplitDirection, SplitDown, SplitLeft, SplitMode, SplitRight, SplitUp,
|
||||
SwapPaneDown, SwapPaneLeft, SwapPaneRight, SwapPaneUp, ToggleZoom, Workspace,
|
||||
dock::{DockPosition, Panel, PanelEvent, PanelHandle},
|
||||
item::SerializableItem,
|
||||
move_active_item, move_item, pane,
|
||||
@@ -192,10 +192,10 @@ impl TerminalPanel {
|
||||
split_context.clone(),
|
||||
|menu, split_context| menu.context(split_context),
|
||||
)
|
||||
.action("Split Right", SplitRight.boxed_clone())
|
||||
.action("Split Left", SplitLeft.boxed_clone())
|
||||
.action("Split Up", SplitUp.boxed_clone())
|
||||
.action("Split Down", SplitDown.boxed_clone())
|
||||
.action("Split Right", SplitRight::default().boxed_clone())
|
||||
.action("Split Left", SplitLeft::default().boxed_clone())
|
||||
.action("Split Up", SplitUp::default().boxed_clone())
|
||||
.action("Split Down", SplitDown::default().boxed_clone())
|
||||
})
|
||||
.into()
|
||||
}
|
||||
@@ -380,47 +380,49 @@ impl TerminalPanel {
|
||||
}
|
||||
self.serialize(cx);
|
||||
}
|
||||
&pane::Event::Split {
|
||||
direction,
|
||||
clone_active_item,
|
||||
} => {
|
||||
if clone_active_item {
|
||||
let fut = self.new_pane_with_cloned_active_terminal(window, cx);
|
||||
let pane = pane.clone();
|
||||
cx.spawn_in(window, async move |panel, cx| {
|
||||
let Some(new_pane) = fut.await else {
|
||||
&pane::Event::Split { direction, mode } => {
|
||||
match mode {
|
||||
SplitMode::ClonePane | SplitMode::EmptyPane => {
|
||||
let clone = matches!(mode, SplitMode::ClonePane);
|
||||
let new_pane = self.new_pane_with_active_terminal(clone, window, cx);
|
||||
let pane = pane.clone();
|
||||
cx.spawn_in(window, async move |panel, cx| {
|
||||
let Some(new_pane) = new_pane.await else {
|
||||
return;
|
||||
};
|
||||
panel
|
||||
.update_in(cx, |panel, window, cx| {
|
||||
panel
|
||||
.center
|
||||
.split(&pane, &new_pane, direction, cx)
|
||||
.log_err();
|
||||
window.focus(&new_pane.focus_handle(cx), cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
SplitMode::MovePane => {
|
||||
let Some(item) =
|
||||
pane.update(cx, |pane, cx| pane.take_active_item(window, cx))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
panel
|
||||
.update_in(cx, |panel, window, cx| {
|
||||
panel
|
||||
.center
|
||||
.split(&pane, &new_pane, direction, cx)
|
||||
.log_err();
|
||||
window.focus(&new_pane.focus_handle(cx), cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
} else {
|
||||
let Some(item) = pane.update(cx, |pane, cx| pane.take_active_item(window, cx))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let Ok(project) = self
|
||||
.workspace
|
||||
.update(cx, |workspace, _| workspace.project().clone())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let new_pane =
|
||||
new_terminal_pane(self.workspace.clone(), project, false, window, cx);
|
||||
new_pane.update(cx, |pane, cx| {
|
||||
pane.add_item(item, true, true, None, window, cx);
|
||||
});
|
||||
self.center.split(&pane, &new_pane, direction, cx).log_err();
|
||||
window.focus(&new_pane.focus_handle(cx), cx);
|
||||
}
|
||||
let Ok(project) = self
|
||||
.workspace
|
||||
.update(cx, |workspace, _| workspace.project().clone())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let new_pane =
|
||||
new_terminal_pane(self.workspace.clone(), project, false, window, cx);
|
||||
new_pane.update(cx, |pane, cx| {
|
||||
pane.add_item(item, true, true, None, window, cx);
|
||||
});
|
||||
self.center.split(&pane, &new_pane, direction, cx).log_err();
|
||||
window.focus(&new_pane.focus_handle(cx), cx);
|
||||
}
|
||||
};
|
||||
}
|
||||
pane::Event::Focus => {
|
||||
self.active_pane = pane.clone();
|
||||
@@ -433,8 +435,9 @@ impl TerminalPanel {
|
||||
}
|
||||
}
|
||||
|
||||
fn new_pane_with_cloned_active_terminal(
|
||||
fn new_pane_with_active_terminal(
|
||||
&mut self,
|
||||
clone: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Option<Entity<Pane>>> {
|
||||
@@ -446,21 +449,34 @@ impl TerminalPanel {
|
||||
let weak_workspace = self.workspace.clone();
|
||||
let project = workspace.project().clone();
|
||||
let active_pane = &self.active_pane;
|
||||
let terminal_view = active_pane
|
||||
.read(cx)
|
||||
.active_item()
|
||||
.and_then(|item| item.downcast::<TerminalView>());
|
||||
let working_directory = terminal_view
|
||||
.as_ref()
|
||||
.and_then(|terminal_view| {
|
||||
terminal_view
|
||||
.read(cx)
|
||||
.terminal()
|
||||
.read(cx)
|
||||
.working_directory()
|
||||
})
|
||||
.or_else(|| default_working_directory(workspace, cx));
|
||||
let is_zoomed = active_pane.read(cx).is_zoomed();
|
||||
let terminal_view = if clone {
|
||||
active_pane
|
||||
.read(cx)
|
||||
.active_item()
|
||||
.and_then(|item| item.downcast::<TerminalView>())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let working_directory = if clone {
|
||||
terminal_view
|
||||
.as_ref()
|
||||
.and_then(|terminal_view| {
|
||||
terminal_view
|
||||
.read(cx)
|
||||
.terminal()
|
||||
.read(cx)
|
||||
.working_directory()
|
||||
})
|
||||
.or_else(|| default_working_directory(workspace, cx))
|
||||
} else {
|
||||
default_working_directory(workspace, cx)
|
||||
};
|
||||
|
||||
let is_zoomed = if clone {
|
||||
active_pane.read(cx).is_zoomed()
|
||||
} else {
|
||||
false
|
||||
};
|
||||
cx.spawn_in(window, async move |panel, cx| {
|
||||
let terminal = project
|
||||
.update(cx, |project, cx| match terminal_view {
|
||||
@@ -1482,7 +1498,7 @@ impl Render for TerminalPanel {
|
||||
window.focus(&pane.read(cx).focus_handle(cx), cx);
|
||||
} else {
|
||||
let future =
|
||||
terminal_panel.new_pane_with_cloned_active_terminal(window, cx);
|
||||
terminal_panel.new_pane_with_active_terminal(true, window, cx);
|
||||
cx.spawn_in(window, async move |terminal_panel, cx| {
|
||||
if let Some(new_pane) = future.await {
|
||||
_ = terminal_panel.update_in(
|
||||
|
||||
@@ -109,7 +109,9 @@ pub async fn extract_seekable_zip<R: AsyncRead + AsyncSeek + Unpin>(
|
||||
.await
|
||||
.with_context(|| format!("extracting into file {path:?}"))?;
|
||||
|
||||
if let Some(perms) = entry.unix_permissions() {
|
||||
if let Some(perms) = entry.unix_permissions()
|
||||
&& perms != 0o000
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let permissions = std::fs::Permissions::from_mode(u32::from(perms));
|
||||
file.set_permissions(permissions)
|
||||
@@ -132,7 +134,8 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
async fn compress_zip(src_dir: &Path, dst: &Path) -> Result<()> {
|
||||
#[allow(unused_variables)]
|
||||
async fn compress_zip(src_dir: &Path, dst: &Path, keep_file_permissions: bool) -> Result<()> {
|
||||
let mut out = smol::fs::File::create(dst).await?;
|
||||
let mut writer = ZipFileWriter::new(&mut out);
|
||||
|
||||
@@ -155,8 +158,8 @@ mod tests {
|
||||
ZipEntryBuilder::new(filename.into(), async_zip::Compression::Deflate);
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let metadata = std::fs::metadata(path)?;
|
||||
let perms = metadata.permissions().mode() as u16;
|
||||
builder = builder.unix_permissions(perms);
|
||||
let perms = keep_file_permissions.then(|| metadata.permissions().mode() as u16);
|
||||
builder = builder.unix_permissions(perms.unwrap_or_default());
|
||||
writer.write_entry_whole(builder, &data).await?;
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
@@ -206,7 +209,9 @@ mod tests {
|
||||
let zip_file = test_dir.path().join("test.zip");
|
||||
|
||||
smol::block_on(async {
|
||||
compress_zip(test_dir.path(), &zip_file).await.unwrap();
|
||||
compress_zip(test_dir.path(), &zip_file, true)
|
||||
.await
|
||||
.unwrap();
|
||||
let reader = read_archive(&zip_file).await;
|
||||
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
@@ -237,7 +242,9 @@ mod tests {
|
||||
|
||||
// Create zip
|
||||
let zip_file = test_dir.path().join("test.zip");
|
||||
compress_zip(test_dir.path(), &zip_file).await.unwrap();
|
||||
compress_zip(test_dir.path(), &zip_file, true)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Extract to new location
|
||||
let extract_dir = tempfile::tempdir().unwrap();
|
||||
@@ -251,4 +258,39 @@ mod tests {
|
||||
assert_eq!(extracted_perms.mode() & 0o777, 0o755);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn test_extract_zip_sets_default_permissions() {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
smol::block_on(async {
|
||||
let test_dir = tempfile::tempdir().unwrap();
|
||||
let executable_path = test_dir.path().join("my_script");
|
||||
|
||||
// Create an executable file
|
||||
std::fs::write(&executable_path, "#!/bin/bash\necho 'Hello'").unwrap();
|
||||
|
||||
// Create zip
|
||||
let zip_file = test_dir.path().join("test.zip");
|
||||
compress_zip(test_dir.path(), &zip_file, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Extract to new location
|
||||
let extract_dir = tempfile::tempdir().unwrap();
|
||||
let reader = read_archive(&zip_file).await;
|
||||
extract_zip(extract_dir.path(), reader).await.unwrap();
|
||||
|
||||
// Check permissions are preserved
|
||||
let extracted_path = extract_dir.path().join("my_script");
|
||||
assert!(extracted_path.exists());
|
||||
let extracted_perms = std::fs::metadata(&extracted_path).unwrap().permissions();
|
||||
assert_eq!(
|
||||
extracted_perms.mode() & 0o777,
|
||||
0o644,
|
||||
"Expected default set of permissions for unzipped file with no permissions set."
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1468,24 +1468,28 @@ fn generate_commands(_: &App) -> Vec<VimCommand> {
|
||||
action.range.replace(range.clone());
|
||||
Some(Box::new(action))
|
||||
}),
|
||||
VimCommand::new(("sp", "lit"), workspace::SplitHorizontal).filename(|_, filename| {
|
||||
Some(
|
||||
VimSplit {
|
||||
vertical: false,
|
||||
filename,
|
||||
}
|
||||
.boxed_clone(),
|
||||
)
|
||||
}),
|
||||
VimCommand::new(("vs", "plit"), workspace::SplitVertical).filename(|_, filename| {
|
||||
Some(
|
||||
VimSplit {
|
||||
vertical: true,
|
||||
filename,
|
||||
}
|
||||
.boxed_clone(),
|
||||
)
|
||||
}),
|
||||
VimCommand::new(("sp", "lit"), workspace::SplitHorizontal::default()).filename(
|
||||
|_, filename| {
|
||||
Some(
|
||||
VimSplit {
|
||||
vertical: false,
|
||||
filename,
|
||||
}
|
||||
.boxed_clone(),
|
||||
)
|
||||
},
|
||||
),
|
||||
VimCommand::new(("vs", "plit"), workspace::SplitVertical::default()).filename(
|
||||
|_, filename| {
|
||||
Some(
|
||||
VimSplit {
|
||||
vertical: true,
|
||||
filename,
|
||||
}
|
||||
.boxed_clone(),
|
||||
)
|
||||
},
|
||||
),
|
||||
VimCommand::new(("tabe", "dit"), workspace::NewFile)
|
||||
.filename(|_action, filename| Some(VimEdit { filename }.boxed_clone())),
|
||||
VimCommand::new(("tabnew", ""), workspace::NewFile)
|
||||
|
||||
@@ -1037,7 +1037,9 @@ impl Render for PanelButtons {
|
||||
.anchor(menu_anchor)
|
||||
.attach(menu_attach)
|
||||
.trigger(move |is_active, _window, _cx| {
|
||||
IconButton::new(name, icon)
|
||||
// Include active state in element ID to invalidate the cached
|
||||
// tooltip when panel state changes (e.g., via keyboard shortcut)
|
||||
IconButton::new((name, is_active_button as u64), icon)
|
||||
.icon_size(IconSize::Small)
|
||||
.toggle_state(is_active_button)
|
||||
.on_click({
|
||||
|
||||
@@ -197,6 +197,41 @@ pub struct DeploySearch {
|
||||
pub excluded_files: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Debug, Deserialize, JsonSchema, Default)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub enum SplitMode {
|
||||
/// Clone the current pane.
|
||||
#[default]
|
||||
ClonePane,
|
||||
/// Create an empty new pane.
|
||||
EmptyPane,
|
||||
/// Move the item into a new pane. This will map to nop if only one pane exists.
|
||||
MovePane,
|
||||
}
|
||||
|
||||
macro_rules! split_structs {
|
||||
($($name:ident => $doc:literal),* $(,)?) => {
|
||||
$(
|
||||
#[doc = $doc]
|
||||
#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default, Action)]
|
||||
#[action(namespace = pane)]
|
||||
#[serde(deny_unknown_fields, default)]
|
||||
pub struct $name {
|
||||
pub mode: SplitMode,
|
||||
}
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
split_structs!(
|
||||
SplitLeft => "Splits the pane to the left.",
|
||||
SplitRight => "Splits the pane to the right.",
|
||||
SplitUp => "Splits the pane upward.",
|
||||
SplitDown => "Splits the pane downward.",
|
||||
SplitHorizontal => "Splits the pane horizontally.",
|
||||
SplitVertical => "Splits the pane vertically."
|
||||
);
|
||||
|
||||
actions!(
|
||||
pane,
|
||||
[
|
||||
@@ -218,14 +253,6 @@ actions!(
|
||||
JoinAll,
|
||||
/// Reopens the most recently closed item.
|
||||
ReopenClosedItem,
|
||||
/// Splits the pane to the left, cloning the current item.
|
||||
SplitLeft,
|
||||
/// Splits the pane upward, cloning the current item.
|
||||
SplitUp,
|
||||
/// Splits the pane to the right, cloning the current item.
|
||||
SplitRight,
|
||||
/// Splits the pane downward, cloning the current item.
|
||||
SplitDown,
|
||||
/// Splits the pane to the left, moving the current item.
|
||||
SplitAndMoveLeft,
|
||||
/// Splits the pane upward, moving the current item.
|
||||
@@ -234,10 +261,6 @@ actions!(
|
||||
SplitAndMoveRight,
|
||||
/// Splits the pane downward, moving the current item.
|
||||
SplitAndMoveDown,
|
||||
/// Splits the pane horizontally.
|
||||
SplitHorizontal,
|
||||
/// Splits the pane vertically.
|
||||
SplitVertical,
|
||||
/// Swaps the current item with the one to the left.
|
||||
SwapItemLeft,
|
||||
/// Swaps the current item with the one to the right.
|
||||
@@ -279,7 +302,7 @@ pub enum Event {
|
||||
},
|
||||
Split {
|
||||
direction: SplitDirection,
|
||||
clone_active_item: bool,
|
||||
mode: SplitMode,
|
||||
},
|
||||
ItemPinned,
|
||||
ItemUnpinned,
|
||||
@@ -311,13 +334,10 @@ impl fmt::Debug for Event {
|
||||
.debug_struct("RemovedItem")
|
||||
.field("item", &item.item_id())
|
||||
.finish(),
|
||||
Event::Split {
|
||||
direction,
|
||||
clone_active_item,
|
||||
} => f
|
||||
Event::Split { direction, mode } => f
|
||||
.debug_struct("Split")
|
||||
.field("direction", direction)
|
||||
.field("clone_active_item", clone_active_item)
|
||||
.field("mode", mode)
|
||||
.finish(),
|
||||
Event::JoinAll => f.write_str("JoinAll"),
|
||||
Event::JoinIntoNext => f.write_str("JoinIntoNext"),
|
||||
@@ -2295,10 +2315,7 @@ impl Pane {
|
||||
let save_task = if let Some(project_path) = project_path {
|
||||
let (worktree, path) = project_path.await?;
|
||||
let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id())?;
|
||||
let new_path = ProjectPath {
|
||||
worktree_id,
|
||||
path: path,
|
||||
};
|
||||
let new_path = ProjectPath { worktree_id, path };
|
||||
|
||||
pane.update_in(cx, |pane, window, cx| {
|
||||
if let Some(item) = pane.item_for_path(new_path.clone(), cx) {
|
||||
@@ -2357,19 +2374,30 @@ impl Pane {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn split(&mut self, direction: SplitDirection, cx: &mut Context<Self>) {
|
||||
cx.emit(Event::Split {
|
||||
direction,
|
||||
clone_active_item: true,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn split_and_move(&mut self, direction: SplitDirection, cx: &mut Context<Self>) {
|
||||
if self.items.len() > 1 {
|
||||
pub fn split(
|
||||
&mut self,
|
||||
direction: SplitDirection,
|
||||
mode: SplitMode,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.items.len() <= 1 && mode == SplitMode::MovePane {
|
||||
// MovePane with only one pane present behaves like a SplitEmpty in the opposite direction
|
||||
let active_item = self.active_item();
|
||||
cx.emit(Event::Split {
|
||||
direction,
|
||||
clone_active_item: false,
|
||||
direction: direction.opposite(),
|
||||
mode: SplitMode::EmptyPane,
|
||||
});
|
||||
// ensure that we focus the moved pane
|
||||
// in this case we know that the window is the same as the active_item
|
||||
if let Some(active_item) = active_item {
|
||||
cx.defer_in(window, move |_, window, cx| {
|
||||
let focus_handle = active_item.item_focus_handle(cx);
|
||||
window.focus(&focus_handle, cx);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
cx.emit(Event::Split { direction, mode });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3824,16 +3852,17 @@ fn default_render_tab_bar_buttons(
|
||||
.with_handle(pane.split_item_context_menu_handle.clone())
|
||||
.menu(move |window, cx| {
|
||||
ContextMenu::build(window, cx, |menu, _, _| {
|
||||
let mode = SplitMode::MovePane;
|
||||
if can_split_move {
|
||||
menu.action("Split Right", SplitAndMoveRight.boxed_clone())
|
||||
.action("Split Left", SplitAndMoveLeft.boxed_clone())
|
||||
.action("Split Up", SplitAndMoveUp.boxed_clone())
|
||||
.action("Split Down", SplitAndMoveDown.boxed_clone())
|
||||
menu.action("Split Right", SplitRight { mode }.boxed_clone())
|
||||
.action("Split Left", SplitLeft { mode }.boxed_clone())
|
||||
.action("Split Up", SplitUp { mode }.boxed_clone())
|
||||
.action("Split Down", SplitDown { mode }.boxed_clone())
|
||||
} else {
|
||||
menu.action("Split Right", SplitRight.boxed_clone())
|
||||
.action("Split Left", SplitLeft.boxed_clone())
|
||||
.action("Split Up", SplitUp.boxed_clone())
|
||||
.action("Split Down", SplitDown.boxed_clone())
|
||||
menu.action("Split Right", SplitRight::default().boxed_clone())
|
||||
.action("Split Left", SplitLeft::default().boxed_clone())
|
||||
.action("Split Up", SplitUp::default().boxed_clone())
|
||||
.action("Split Down", SplitDown::default().boxed_clone())
|
||||
}
|
||||
})
|
||||
.into()
|
||||
@@ -3892,33 +3921,35 @@ impl Render for Pane {
|
||||
.size_full()
|
||||
.flex_none()
|
||||
.overflow_hidden()
|
||||
.on_action(
|
||||
cx.listener(|pane, _: &SplitLeft, _, cx| pane.split(SplitDirection::Left, cx)),
|
||||
)
|
||||
.on_action(cx.listener(|pane, _: &SplitUp, _, cx| pane.split(SplitDirection::Up, cx)))
|
||||
.on_action(cx.listener(|pane, _: &SplitHorizontal, _, cx| {
|
||||
pane.split(SplitDirection::horizontal(cx), cx)
|
||||
.on_action(cx.listener(|pane, split: &SplitLeft, window, cx| {
|
||||
pane.split(SplitDirection::Left, split.mode, window, cx)
|
||||
}))
|
||||
.on_action(cx.listener(|pane, _: &SplitVertical, _, cx| {
|
||||
pane.split(SplitDirection::vertical(cx), cx)
|
||||
.on_action(cx.listener(|pane, split: &SplitUp, window, cx| {
|
||||
pane.split(SplitDirection::Up, split.mode, window, cx)
|
||||
}))
|
||||
.on_action(
|
||||
cx.listener(|pane, _: &SplitRight, _, cx| pane.split(SplitDirection::Right, cx)),
|
||||
)
|
||||
.on_action(
|
||||
cx.listener(|pane, _: &SplitDown, _, cx| pane.split(SplitDirection::Down, cx)),
|
||||
)
|
||||
.on_action(cx.listener(|pane, _: &SplitAndMoveUp, _, cx| {
|
||||
pane.split_and_move(SplitDirection::Up, cx)
|
||||
.on_action(cx.listener(|pane, split: &SplitHorizontal, window, cx| {
|
||||
pane.split(SplitDirection::horizontal(cx), split.mode, window, cx)
|
||||
}))
|
||||
.on_action(cx.listener(|pane, _: &SplitAndMoveDown, _, cx| {
|
||||
pane.split_and_move(SplitDirection::Down, cx)
|
||||
.on_action(cx.listener(|pane, split: &SplitVertical, window, cx| {
|
||||
pane.split(SplitDirection::vertical(cx), split.mode, window, cx)
|
||||
}))
|
||||
.on_action(cx.listener(|pane, _: &SplitAndMoveLeft, _, cx| {
|
||||
pane.split_and_move(SplitDirection::Left, cx)
|
||||
.on_action(cx.listener(|pane, split: &SplitRight, window, cx| {
|
||||
pane.split(SplitDirection::Right, split.mode, window, cx)
|
||||
}))
|
||||
.on_action(cx.listener(|pane, _: &SplitAndMoveRight, _, cx| {
|
||||
pane.split_and_move(SplitDirection::Right, cx)
|
||||
.on_action(cx.listener(|pane, split: &SplitDown, window, cx| {
|
||||
pane.split(SplitDirection::Down, split.mode, window, cx)
|
||||
}))
|
||||
.on_action(cx.listener(|pane, _: &SplitAndMoveUp, window, cx| {
|
||||
pane.split(SplitDirection::Up, SplitMode::MovePane, window, cx)
|
||||
}))
|
||||
.on_action(cx.listener(|pane, _: &SplitAndMoveDown, window, cx| {
|
||||
pane.split(SplitDirection::Down, SplitMode::MovePane, window, cx)
|
||||
}))
|
||||
.on_action(cx.listener(|pane, _: &SplitAndMoveLeft, window, cx| {
|
||||
pane.split(SplitDirection::Left, SplitMode::MovePane, window, cx)
|
||||
}))
|
||||
.on_action(cx.listener(|pane, _: &SplitAndMoveRight, window, cx| {
|
||||
pane.split(SplitDirection::Right, SplitMode::MovePane, window, cx)
|
||||
}))
|
||||
.on_action(cx.listener(|_, _: &JoinIntoNext, _, cx| {
|
||||
cx.emit(Event::JoinIntoNext);
|
||||
@@ -4443,11 +4474,14 @@ impl Render for DraggedTab {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::num::NonZero;
|
||||
use std::{iter::zip, num::NonZero};
|
||||
|
||||
use super::*;
|
||||
use crate::item::test::{TestItem, TestProjectItem};
|
||||
use gpui::{TestAppContext, VisualTestContext, size};
|
||||
use crate::{
|
||||
Member,
|
||||
item::test::{TestItem, TestProjectItem},
|
||||
};
|
||||
use gpui::{AppContext, Axis, TestAppContext, VisualTestContext, size};
|
||||
use project::FakeFs;
|
||||
use settings::SettingsStore;
|
||||
use theme::LoadThemes;
|
||||
@@ -7125,6 +7159,32 @@ mod tests {
|
||||
assert_item_labels(&pane, ["A", "C*", "B"], cx);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_split_empty(cx: &mut TestAppContext) {
|
||||
for split_direction in SplitDirection::all() {
|
||||
test_single_pane_split(["A"], split_direction, SplitMode::EmptyPane, cx).await;
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_split_clone(cx: &mut TestAppContext) {
|
||||
for split_direction in SplitDirection::all() {
|
||||
test_single_pane_split(["A"], split_direction, SplitMode::ClonePane, cx).await;
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_split_move_right_on_single_pane(cx: &mut TestAppContext) {
|
||||
test_single_pane_split(["A"], SplitDirection::Right, SplitMode::MovePane, cx).await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_split_move(cx: &mut TestAppContext) {
|
||||
for split_direction in SplitDirection::all() {
|
||||
test_single_pane_split(["A", "B"], split_direction, SplitMode::MovePane, cx).await;
|
||||
}
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
@@ -7220,4 +7280,163 @@ mod tests {
|
||||
"pane items do not match expectation"
|
||||
);
|
||||
}
|
||||
|
||||
// Assert the item label, with the active item label expected active index
|
||||
#[track_caller]
|
||||
fn assert_item_labels_active_index(
|
||||
pane: &Entity<Pane>,
|
||||
expected_states: &[&str],
|
||||
expected_active_idx: usize,
|
||||
cx: &mut VisualTestContext,
|
||||
) {
|
||||
let actual_states = pane.update(cx, |pane, cx| {
|
||||
pane.items
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, item)| {
|
||||
let mut state = item
|
||||
.to_any_view()
|
||||
.downcast::<TestItem>()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.label
|
||||
.clone();
|
||||
if ix == pane.active_item_index {
|
||||
assert_eq!(ix, expected_active_idx);
|
||||
}
|
||||
if item.is_dirty(cx) {
|
||||
state.push('^');
|
||||
}
|
||||
if pane.is_tab_pinned(ix) {
|
||||
state.push('!');
|
||||
}
|
||||
state
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
assert_eq!(
|
||||
actual_states, expected_states,
|
||||
"pane items do not match expectation"
|
||||
);
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_pane_ids_on_axis<const COUNT: usize>(
|
||||
workspace: &Entity<Workspace>,
|
||||
expected_ids: [&EntityId; COUNT],
|
||||
expected_axis: Axis,
|
||||
cx: &mut VisualTestContext,
|
||||
) {
|
||||
workspace.read_with(cx, |workspace, _| match &workspace.center.root {
|
||||
Member::Axis(axis) => {
|
||||
assert_eq!(axis.axis, expected_axis);
|
||||
assert_eq!(axis.members.len(), expected_ids.len());
|
||||
assert!(
|
||||
zip(expected_ids, &axis.members).all(|(e, a)| {
|
||||
if let Member::Pane(p) = a {
|
||||
p.entity_id() == *e
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}),
|
||||
"pane ids do not match expectation: {expected_ids:?} != {actual_ids:?}",
|
||||
actual_ids = axis.members
|
||||
);
|
||||
}
|
||||
Member::Pane(_) => panic!("expected axis"),
|
||||
});
|
||||
}
|
||||
|
||||
async fn test_single_pane_split<const COUNT: usize>(
|
||||
pane_labels: [&str; COUNT],
|
||||
direction: SplitDirection,
|
||||
operation: SplitMode,
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
let project = Project::test(fs, None, cx).await;
|
||||
let (workspace, cx) =
|
||||
cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
|
||||
|
||||
let mut pane_before =
|
||||
workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
|
||||
for label in pane_labels {
|
||||
add_labeled_item(&pane_before, label, false, cx);
|
||||
}
|
||||
pane_before.update_in(cx, |pane, window, cx| {
|
||||
pane.split(direction, operation, window, cx)
|
||||
});
|
||||
cx.executor().run_until_parked();
|
||||
let pane_after = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
|
||||
|
||||
let num_labels = pane_labels.len();
|
||||
let last_as_active = format!("{}*", String::from(pane_labels[num_labels - 1]));
|
||||
|
||||
// check labels for all split operations
|
||||
match operation {
|
||||
SplitMode::EmptyPane => {
|
||||
assert_item_labels_active_index(&pane_before, &pane_labels, num_labels - 1, cx);
|
||||
assert_item_labels(&pane_after, [], cx);
|
||||
}
|
||||
SplitMode::ClonePane => {
|
||||
assert_item_labels_active_index(&pane_before, &pane_labels, num_labels - 1, cx);
|
||||
assert_item_labels(&pane_after, [&last_as_active], cx);
|
||||
}
|
||||
SplitMode::MovePane => {
|
||||
let head = &pane_labels[..(num_labels - 1)];
|
||||
if num_labels == 1 {
|
||||
// We special-case this behavior and actually execute an empty pane command
|
||||
// followed by a refocus of the old pane for this case.
|
||||
pane_before = workspace.read_with(cx, |workspace, _cx| {
|
||||
workspace
|
||||
.panes()
|
||||
.into_iter()
|
||||
.find(|pane| *pane != &pane_after)
|
||||
.unwrap()
|
||||
.clone()
|
||||
});
|
||||
};
|
||||
|
||||
assert_item_labels_active_index(
|
||||
&pane_before,
|
||||
&head,
|
||||
head.len().saturating_sub(1),
|
||||
cx,
|
||||
);
|
||||
assert_item_labels(&pane_after, [&last_as_active], cx);
|
||||
pane_after.update_in(cx, |pane, window, cx| {
|
||||
window.focused(cx).is_some_and(|focus_handle| {
|
||||
focus_handle == pane.active_item().unwrap().item_focus_handle(cx)
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// expected axis depends on split direction
|
||||
let expected_axis = match direction {
|
||||
SplitDirection::Right | SplitDirection::Left => Axis::Horizontal,
|
||||
SplitDirection::Up | SplitDirection::Down => Axis::Vertical,
|
||||
};
|
||||
|
||||
// expected ids depends on split direction
|
||||
let expected_ids = match direction {
|
||||
SplitDirection::Right | SplitDirection::Down => {
|
||||
[&pane_before.entity_id(), &pane_after.entity_id()]
|
||||
}
|
||||
SplitDirection::Left | SplitDirection::Up => {
|
||||
[&pane_after.entity_id(), &pane_before.entity_id()]
|
||||
}
|
||||
};
|
||||
|
||||
// check pane axes for all operations
|
||||
match operation {
|
||||
SplitMode::EmptyPane | SplitMode::ClonePane => {
|
||||
assert_pane_ids_on_axis(&workspace, expected_ids, expected_axis, cx);
|
||||
}
|
||||
SplitMode::MovePane => {
|
||||
assert_pane_ids_on_axis(&workspace, expected_ids, expected_axis, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4262,16 +4262,19 @@ impl Workspace {
|
||||
item: item.boxed_clone(),
|
||||
});
|
||||
}
|
||||
pane::Event::Split {
|
||||
direction,
|
||||
clone_active_item,
|
||||
} => {
|
||||
if *clone_active_item {
|
||||
self.split_and_clone(pane.clone(), *direction, window, cx)
|
||||
.detach();
|
||||
} else {
|
||||
self.split_and_move(pane.clone(), *direction, window, cx);
|
||||
}
|
||||
pane::Event::Split { direction, mode } => {
|
||||
match mode {
|
||||
SplitMode::ClonePane => {
|
||||
self.split_and_clone(pane.clone(), *direction, window, cx)
|
||||
.detach();
|
||||
}
|
||||
SplitMode::EmptyPane => {
|
||||
self.split_pane(pane.clone(), *direction, window, cx);
|
||||
}
|
||||
SplitMode::MovePane => {
|
||||
self.split_and_move(pane.clone(), *direction, window, cx);
|
||||
}
|
||||
};
|
||||
}
|
||||
pane::Event::JoinIntoNext => {
|
||||
self.join_pane_into_next(pane.clone(), window, cx);
|
||||
|
||||
@@ -833,12 +833,19 @@ fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, cx: &mut
|
||||
cx.spawn_in(window, async move |workspace, cx| {
|
||||
let res = async move {
|
||||
let json = app_state.languages.language_for_name("JSONC").await.ok();
|
||||
let lsp_store = workspace.update(cx, |workspace, cx| {
|
||||
workspace
|
||||
.project()
|
||||
.update(cx, |project, _| project.lsp_store())
|
||||
})?;
|
||||
let json_schema_content =
|
||||
json_schema_store::resolve_schema_request_inner(
|
||||
&app_state.languages,
|
||||
lsp_store,
|
||||
&schema_path,
|
||||
cx,
|
||||
)?;
|
||||
)
|
||||
.await?;
|
||||
let json_schema_content =
|
||||
serde_json::to_string_pretty(&json_schema_content)
|
||||
.context("Failed to serialize JSON Schema as JSON")?;
|
||||
|
||||
@@ -3817,7 +3817,7 @@ mod tests {
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
cx.dispatch_action(window.into(), pane::SplitRight);
|
||||
cx.dispatch_action(window.into(), pane::SplitRight::default());
|
||||
let editor_2 = cx.update(|cx| {
|
||||
let pane_2 = workspace.read(cx).active_pane().clone();
|
||||
assert_ne!(pane_1, pane_2);
|
||||
|
||||
@@ -32,10 +32,10 @@ pub fn app_menus(cx: &mut App) -> Vec<Menu> {
|
||||
MenuItem::submenu(Menu {
|
||||
name: "Editor Layout".into(),
|
||||
items: vec![
|
||||
MenuItem::action("Split Up", workspace::SplitUp),
|
||||
MenuItem::action("Split Down", workspace::SplitDown),
|
||||
MenuItem::action("Split Left", workspace::SplitLeft),
|
||||
MenuItem::action("Split Right", workspace::SplitRight),
|
||||
MenuItem::action("Split Up", workspace::SplitUp::default()),
|
||||
MenuItem::action("Split Down", workspace::SplitDown::default()),
|
||||
MenuItem::action("Split Left", workspace::SplitLeft::default()),
|
||||
MenuItem::action("Split Right", workspace::SplitRight::default()),
|
||||
],
|
||||
}),
|
||||
MenuItem::separator(),
|
||||
|
||||
@@ -85,6 +85,8 @@ You can type `@` to mention files, directories, symbols, previous threads, and r
|
||||
|
||||
Copying images and pasting them in the panel's message editor is also supported.
|
||||
|
||||
When you paste multi-line code selections copied from an editor buffer, Zed automatically formats them as @mentions with the file context. To paste content without this automatic formatting, use {#kb agent::PasteRaw} to paste raw text directly.
|
||||
|
||||
### Selection as Context
|
||||
|
||||
Additionally, you can also select text in a buffer and add it as context by using the {#kb agent::AddSelectionToThread} keybinding, running the {#action agent::AddSelectionToThread} action, or choosing the "Selection" item in the `@` menu.
|
||||
@@ -100,6 +102,8 @@ You can also do this at any time with an ongoing thread via the "Agent Options"
|
||||
|
||||
After you've configured your LLM providers—either via [a custom API key](./llm-providers.md) or through [Zed's hosted models](./models.md)—you can switch between them by clicking on the model selector on the message editor or by using the {#kb agent::ToggleModelSelector} keybinding.
|
||||
|
||||
If you have favorited models configured, you can cycle through them with {#kb agent::CycleFavoriteModels} without opening the model selector.
|
||||
|
||||
> The same model can be offered via multiple providers - for example, Claude Sonnet 4 is available via Zed Pro, OpenRouter, Anthropic directly, and more.
|
||||
> Make sure you've selected the correct model **_provider_** for the model you'd like to use, delineated by the logo to the left of the model in the model selector.
|
||||
|
||||
|
||||
@@ -305,7 +305,7 @@ To use GitHub Copilot as your provider, set this within `settings.json`:
|
||||
}
|
||||
```
|
||||
|
||||
You should be able to sign-in to GitHub Copilot by clicking on the Copilot icon in the status bar and following the setup instructions.
|
||||
To sign in to GitHub Copilot, click on the Copilot icon in the status bar. A popup window appears displaying a device code. Click the copy button to copy the code, then click "Connect to GitHub" to open the GitHub verification page in your browser. Paste the code when prompted. The popup window closes automatically after successful authorization.
|
||||
|
||||
#### Using GitHub Copilot Enterprise
|
||||
|
||||
@@ -348,10 +348,17 @@ You should be able to sign-in to Supermaven by clicking on the Supermaven icon i
|
||||
|
||||
### Codestral {#codestral}
|
||||
|
||||
To use Mistral's Codestral as your provider, start by going to the Agent Panel settings view by running the {#action agent::OpenSettings} action.
|
||||
Look for the Mistral item and add a Codestral API key in the corresponding text input.
|
||||
To use Mistral's Codestral as your provider:
|
||||
|
||||
After that, you should be able to switch your provider to it in your `settings.json` file:
|
||||
1. Open the Settings Editor (`Cmd+,` on macOS, `Ctrl+,` on Linux/Windows)
|
||||
2. Search for "Edit Predictions" and click **Configure Providers**
|
||||
3. Find the Codestral section and enter your API key from the
|
||||
[Codestral dashboard](https://console.mistral.ai/codestral)
|
||||
|
||||
Alternatively, click the edit prediction icon in the status bar and select
|
||||
**Configure Providers** from the menu.
|
||||
|
||||
After adding your API key, set Codestral as your provider in `settings.json`:
|
||||
|
||||
```json [settings]
|
||||
{
|
||||
|
||||
@@ -62,7 +62,7 @@ The `download_file` capability grants extensions the ability to download files u
|
||||
To allow any file to be downloaded:
|
||||
|
||||
```toml
|
||||
{ kind = "download_file", host = "github.com", path = ["**"] }
|
||||
{ kind = "download_file", host = "*", path = ["**"] }
|
||||
```
|
||||
|
||||
To allow any file to be downloaded from `github.com`:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "zed_proto"
|
||||
version = "0.3.0"
|
||||
version = "0.3.1"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "Apache-2.0"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
id = "proto"
|
||||
name = "Proto"
|
||||
description = "Protocol Buffers support."
|
||||
version = "0.3.0"
|
||||
version = "0.3.1"
|
||||
schema_version = 1
|
||||
authors = ["Zed Industries <support@zed.dev>"]
|
||||
repository = "https://github.com/zed-industries/zed"
|
||||
|
||||
@@ -48,7 +48,7 @@ fn run_clippy() -> Step<Run> {
|
||||
fn check_rust() -> NamedJob {
|
||||
let job = Job::default()
|
||||
.with_repository_owner_guard()
|
||||
.runs_on(runners::LINUX_DEFAULT)
|
||||
.runs_on(runners::LINUX_MEDIUM)
|
||||
.timeout_minutes(3u32)
|
||||
.add_step(steps::checkout_repo())
|
||||
.add_step(steps::cache_rust_dependencies_namespace())
|
||||
@@ -66,7 +66,7 @@ pub(crate) fn check_extension() -> NamedJob {
|
||||
let (cache_download, cache_hit) = cache_zed_extension_cli();
|
||||
let job = Job::default()
|
||||
.with_repository_owner_guard()
|
||||
.runs_on(runners::LINUX_SMALL)
|
||||
.runs_on(runners::LINUX_LARGE_RAM)
|
||||
.timeout_minutes(2u32)
|
||||
.add_step(steps::checkout_repo())
|
||||
.add_step(cache_download)
|
||||
|
||||
@@ -8,6 +8,9 @@ pub const LINUX_MEDIUM: Runner = Runner("namespace-profile-4x8-ubuntu-2204");
|
||||
pub const LINUX_X86_BUNDLER: Runner = Runner("namespace-profile-32x64-ubuntu-2004");
|
||||
pub const LINUX_ARM_BUNDLER: Runner = Runner("namespace-profile-8x32-ubuntu-2004-arm-m4");
|
||||
|
||||
// Larger Ubuntu runner with glibc 2.39 for extension bundling
|
||||
pub const LINUX_LARGE_RAM: Runner = Runner("namespace-profile-8x32-ubuntu-2404");
|
||||
|
||||
pub const MAC_DEFAULT: Runner = Runner("self-mini-macos");
|
||||
pub const WINDOWS_DEFAULT: Runner = Runner("self-32vcpu-windows-2022");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user