Merge branch 'main' into mcp-acp-gemini
This commit is contained in:
commit
f028ca4d1a
115 changed files with 3523 additions and 1437 deletions
|
@ -1594,6 +1594,8 @@ mod tests {
|
|||
name: "test",
|
||||
connection,
|
||||
child_status: io_task,
|
||||
current_thread: thread_rc,
|
||||
agent_state: Default::default(),
|
||||
};
|
||||
|
||||
AcpThread::new(
|
||||
|
|
|
@ -13,6 +13,7 @@ use std::{
|
|||
rc::Rc,
|
||||
};
|
||||
use ui::App;
|
||||
use util::ResultExt as _;
|
||||
|
||||
use crate::{AcpThread, AgentConnection};
|
||||
|
||||
|
@ -52,7 +53,7 @@ impl acp_old::Client for OldAcpClientDelegate {
|
|||
thread.push_assistant_content_block(thought.into(), true, cx)
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
.log_err();
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
|
@ -371,6 +372,7 @@ pub struct OldAcpAgentConnection {
|
|||
pub connection: acp_old::AgentConnection,
|
||||
pub child_status: Task<Result<()>>,
|
||||
pub agent_state: Rc<RefCell<acp::AgentState>>,
|
||||
pub current_thread: Rc<RefCell<WeakEntity<AcpThread>>>,
|
||||
}
|
||||
|
||||
impl AgentConnection for OldAcpAgentConnection {
|
||||
|
@ -386,6 +388,7 @@ impl AgentConnection for OldAcpAgentConnection {
|
|||
}
|
||||
.into_any(),
|
||||
);
|
||||
let current_thread = self.current_thread.clone();
|
||||
cx.spawn(async move |cx| {
|
||||
let result = task.await?;
|
||||
let result = acp_old::InitializeParams::response_from_any(result)?;
|
||||
|
@ -399,6 +402,7 @@ impl AgentConnection for OldAcpAgentConnection {
|
|||
let session_id = acp::SessionId("acp-old-no-id".into());
|
||||
AcpThread::new("Gemini", self.clone(), project, session_id, cx)
|
||||
});
|
||||
current_thread.replace(thread.downgrade());
|
||||
thread
|
||||
})
|
||||
})
|
||||
|
|
|
@ -25,6 +25,7 @@ assistant_context.workspace = true
|
|||
assistant_tool.workspace = true
|
||||
chrono.workspace = true
|
||||
client.workspace = true
|
||||
cloud_llm_client.workspace = true
|
||||
collections.workspace = true
|
||||
component.workspace = true
|
||||
context_server.workspace = true
|
||||
|
@ -35,9 +36,9 @@ futures.workspace = true
|
|||
git.workspace = true
|
||||
gpui.workspace = true
|
||||
heed.workspace = true
|
||||
http_client.workspace = true
|
||||
icons.workspace = true
|
||||
indoc.workspace = true
|
||||
http_client.workspace = true
|
||||
itertools.workspace = true
|
||||
language.workspace = true
|
||||
language_model.workspace = true
|
||||
|
@ -63,7 +64,6 @@ time.workspace = true
|
|||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
zed_llm_client.workspace = true
|
||||
zstd.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
|
|
|
@ -13,6 +13,7 @@ use anyhow::{Result, anyhow};
|
|||
use assistant_tool::{ActionLog, AnyToolCard, Tool, ToolWorkingSet};
|
||||
use chrono::{DateTime, Utc};
|
||||
use client::{ModelRequestUsage, RequestUsage};
|
||||
use cloud_llm_client::{CompletionIntent, CompletionRequestStatus, UsageLimit};
|
||||
use collections::HashMap;
|
||||
use feature_flags::{self, FeatureFlagAppExt};
|
||||
use futures::{FutureExt, StreamExt as _, future::Shared};
|
||||
|
@ -49,7 +50,6 @@ use std::{
|
|||
use thiserror::Error;
|
||||
use util::{ResultExt as _, post_inc};
|
||||
use uuid::Uuid;
|
||||
use zed_llm_client::{CompletionIntent, CompletionRequestStatus, UsageLimit};
|
||||
|
||||
const MAX_RETRY_ATTEMPTS: u8 = 4;
|
||||
const BASE_RETRY_DELAY: Duration = Duration::from_secs(5);
|
||||
|
@ -1681,7 +1681,7 @@ impl Thread {
|
|||
|
||||
let completion_mode = request
|
||||
.mode
|
||||
.unwrap_or(zed_llm_client::CompletionMode::Normal);
|
||||
.unwrap_or(cloud_llm_client::CompletionMode::Normal);
|
||||
|
||||
self.last_received_chunk_at = Some(Instant::now());
|
||||
|
||||
|
|
|
@ -32,6 +32,7 @@ impl AcpConnection {
|
|||
pub async fn stdio(
|
||||
server_name: &'static str,
|
||||
command: AgentServerCommand,
|
||||
working_directory: Option<Arc<Path>>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<Self> {
|
||||
let client: Arc<ContextServer> = ContextServer::stdio(
|
||||
|
@ -41,6 +42,7 @@ impl AcpConnection {
|
|||
args: command.args,
|
||||
env: command.env,
|
||||
},
|
||||
working_directory,
|
||||
)
|
||||
.into();
|
||||
ContextServer::start(client.clone(), cx).await?;
|
||||
|
|
|
@ -38,6 +38,7 @@ impl AgentServer for Codex {
|
|||
) -> Task<Result<Rc<dyn AgentConnection>>> {
|
||||
let project = project.clone();
|
||||
let server_name = self.name();
|
||||
let working_directory = project.read(cx).active_project_directory(cx);
|
||||
cx.spawn(async move |cx| {
|
||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings.get::<AllAgentServersSettings>(None).codex.clone()
|
||||
|
@ -50,7 +51,7 @@ impl AgentServer for Codex {
|
|||
};
|
||||
// todo! check supported version
|
||||
|
||||
let conn = AcpConnection::stdio(server_name, command, cx).await?;
|
||||
let conn = AcpConnection::stdio(server_name, command, working_directory, cx).await?;
|
||||
Ok(Rc::new(conn) as _)
|
||||
})
|
||||
}
|
||||
|
@ -70,7 +71,7 @@ pub(crate) mod tests {
|
|||
|
||||
AgentServerCommand {
|
||||
path: cli_path,
|
||||
args: vec!["mcp".into()],
|
||||
args: vec![],
|
||||
env: None,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,7 +12,6 @@ use futures::{FutureExt, StreamExt, channel::mpsc, select};
|
|||
use gpui::{Entity, TestAppContext};
|
||||
use indoc::indoc;
|
||||
use project::{FakeFs, Project};
|
||||
use serde_json::json;
|
||||
use settings::{Settings, SettingsStore};
|
||||
use util::path;
|
||||
|
||||
|
@ -27,7 +26,11 @@ pub async fn test_basic(server: impl AgentServer + 'static, cx: &mut TestAppCont
|
|||
.unwrap();
|
||||
|
||||
thread.read_with(cx, |thread, _| {
|
||||
assert_eq!(thread.entries().len(), 2);
|
||||
assert!(
|
||||
thread.entries().len() >= 2,
|
||||
"Expected at least 2 entries. Got: {:?}",
|
||||
thread.entries()
|
||||
);
|
||||
assert!(matches!(
|
||||
thread.entries()[0],
|
||||
AgentThreadEntry::UserMessage(_)
|
||||
|
@ -108,19 +111,19 @@ pub async fn test_path_mentions(server: impl AgentServer + 'static, cx: &mut Tes
|
|||
}
|
||||
|
||||
pub async fn test_tool_call(server: impl AgentServer + 'static, cx: &mut TestAppContext) {
|
||||
let fs = init_test(cx).await;
|
||||
fs.insert_tree(
|
||||
path!("/private/tmp"),
|
||||
json!({"foo": "Lorem ipsum dolor", "bar": "bar", "baz": "baz"}),
|
||||
)
|
||||
.await;
|
||||
let project = Project::test(fs, [path!("/private/tmp").as_ref()], cx).await;
|
||||
let _fs = init_test(cx).await;
|
||||
|
||||
let tempdir = tempfile::tempdir().unwrap();
|
||||
let foo_path = tempdir.path().join("foo");
|
||||
std::fs::write(&foo_path, "Lorem ipsum dolor").expect("failed to write file");
|
||||
|
||||
let project = Project::example([tempdir.path()], &mut cx.to_async()).await;
|
||||
let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await;
|
||||
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.send_raw(
|
||||
"Read the '/private/tmp/foo' file and tell me what you see.",
|
||||
&format!("Read {} and tell me what you see.", foo_path.display()),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
|
@ -143,6 +146,8 @@ pub async fn test_tool_call(server: impl AgentServer + 'static, cx: &mut TestApp
|
|||
.any(|entry| { matches!(entry, AgentThreadEntry::AssistantMessage(_)) })
|
||||
);
|
||||
});
|
||||
|
||||
drop(tempdir);
|
||||
}
|
||||
|
||||
pub async fn test_tool_call_with_confirmation(
|
||||
|
@ -155,7 +160,7 @@ pub async fn test_tool_call_with_confirmation(
|
|||
let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await;
|
||||
let full_turn = thread.update(cx, |thread, cx| {
|
||||
thread.send_raw(
|
||||
r#"Run `touch hello.txt && echo "Hello, world!" | tee hello.txt`"#,
|
||||
r#"Run exactly `touch hello.txt && echo "Hello, world!" | tee hello.txt` in the terminal."#,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
@ -175,10 +180,10 @@ pub async fn test_tool_call_with_confirmation(
|
|||
)
|
||||
.await;
|
||||
|
||||
let tool_call_id = thread.read_with(cx, |thread, _cx| {
|
||||
let tool_call_id = thread.read_with(cx, |thread, cx| {
|
||||
let AgentThreadEntry::ToolCall(ToolCall {
|
||||
id,
|
||||
content,
|
||||
label,
|
||||
status: ToolCallStatus::WaitingForConfirmation { .. },
|
||||
..
|
||||
}) = &thread
|
||||
|
@ -190,7 +195,8 @@ pub async fn test_tool_call_with_confirmation(
|
|||
panic!();
|
||||
};
|
||||
|
||||
assert!(content.iter().any(|c| c.to_markdown(_cx).contains("touch")));
|
||||
let label = label.read(cx).source();
|
||||
assert!(label.contains("touch"), "Got: {}", label);
|
||||
|
||||
id.clone()
|
||||
});
|
||||
|
@ -242,7 +248,7 @@ pub async fn test_cancel(server: impl AgentServer + 'static, cx: &mut TestAppCon
|
|||
let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await;
|
||||
let full_turn = thread.update(cx, |thread, cx| {
|
||||
thread.send_raw(
|
||||
r#"Run `touch hello.txt && echo "Hello, world!" >> hello.txt`"#,
|
||||
r#"Run exactly `touch hello.txt && echo "Hello, world!" | tee hello.txt` in the terminal."#,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
@ -262,10 +268,10 @@ pub async fn test_cancel(server: impl AgentServer + 'static, cx: &mut TestAppCon
|
|||
)
|
||||
.await;
|
||||
|
||||
thread.read_with(cx, |thread, _cx| {
|
||||
thread.read_with(cx, |thread, cx| {
|
||||
let AgentThreadEntry::ToolCall(ToolCall {
|
||||
id,
|
||||
content,
|
||||
label,
|
||||
status: ToolCallStatus::WaitingForConfirmation { .. },
|
||||
..
|
||||
}) = &thread.entries()[first_tool_call_ix]
|
||||
|
@ -273,7 +279,8 @@ pub async fn test_cancel(server: impl AgentServer + 'static, cx: &mut TestAppCon
|
|||
panic!("{:?}", thread.entries()[1]);
|
||||
};
|
||||
|
||||
assert!(content.iter().any(|c| c.to_markdown(_cx).contains("touch")));
|
||||
let label = label.read(cx).source();
|
||||
assert!(label.contains("touch"), "Got: {}", label);
|
||||
|
||||
id.clone()
|
||||
});
|
||||
|
|
|
@ -41,6 +41,7 @@ impl AgentServer for Gemini {
|
|||
) -> Task<Result<Rc<dyn AgentConnection>>> {
|
||||
let project = project.clone();
|
||||
let server_name = self.name();
|
||||
let working_directory = project.read(cx).active_project_directory(cx);
|
||||
cx.spawn(async move |cx| {
|
||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings.get::<AllAgentServersSettings>(None).gemini.clone()
|
||||
|
@ -53,7 +54,7 @@ impl AgentServer for Gemini {
|
|||
};
|
||||
// todo! check supported version
|
||||
|
||||
let conn = AcpConnection::stdio(server_name, command, cx).await?;
|
||||
let conn = AcpConnection::stdio(server_name, command, working_directory, cx).await?;
|
||||
Ok(Rc::new(conn) as _)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ path = "src/agent_settings.rs"
|
|||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
cloud_llm_client.workspace = true
|
||||
collections.workspace = true
|
||||
gpui.workspace = true
|
||||
language_model.workspace = true
|
||||
|
@ -20,7 +21,6 @@ schemars.workspace = true
|
|||
serde.workspace = true
|
||||
settings.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
zed_llm_client.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
fs.workspace = true
|
||||
|
|
|
@ -321,11 +321,11 @@ pub enum CompletionMode {
|
|||
Burn,
|
||||
}
|
||||
|
||||
impl From<CompletionMode> for zed_llm_client::CompletionMode {
|
||||
impl From<CompletionMode> for cloud_llm_client::CompletionMode {
|
||||
fn from(value: CompletionMode) -> Self {
|
||||
match value {
|
||||
CompletionMode::Normal => zed_llm_client::CompletionMode::Normal,
|
||||
CompletionMode::Burn => zed_llm_client::CompletionMode::Max,
|
||||
CompletionMode::Normal => cloud_llm_client::CompletionMode::Normal,
|
||||
CompletionMode::Burn => cloud_llm_client::CompletionMode::Max,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,6 +31,7 @@ audio.workspace = true
|
|||
buffer_diff.workspace = true
|
||||
chrono.workspace = true
|
||||
client.workspace = true
|
||||
cloud_llm_client.workspace = true
|
||||
collections.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
component.workspace = true
|
||||
|
@ -46,9 +47,9 @@ futures.workspace = true
|
|||
fuzzy.workspace = true
|
||||
gpui.workspace = true
|
||||
html_to_markdown.workspace = true
|
||||
indoc.workspace = true
|
||||
http_client.workspace = true
|
||||
indexed_docs.workspace = true
|
||||
indoc.workspace = true
|
||||
inventory.workspace = true
|
||||
itertools.workspace = true
|
||||
jsonschema.workspace = true
|
||||
|
@ -97,7 +98,6 @@ watch.workspace = true
|
|||
workspace-hack.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
zed_llm_client.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
assistant_tools.workspace = true
|
||||
|
|
|
@ -14,6 +14,7 @@ use agent_settings::{AgentSettings, NotifyWhenAgentWaiting};
|
|||
use anyhow::Context as _;
|
||||
use assistant_tool::ToolUseStatus;
|
||||
use audio::{Audio, Sound};
|
||||
use cloud_llm_client::CompletionIntent;
|
||||
use collections::{HashMap, HashSet};
|
||||
use editor::actions::{MoveUp, Paste};
|
||||
use editor::scroll::Autoscroll;
|
||||
|
@ -52,7 +53,6 @@ use util::ResultExt as _;
|
|||
use util::markdown::MarkdownCodeBlock;
|
||||
use workspace::{CollaboratorId, Workspace};
|
||||
use zed_actions::assistant::OpenRulesLibrary;
|
||||
use zed_llm_client::CompletionIntent;
|
||||
|
||||
const CODEBLOCK_CONTAINER_GROUP: &str = "codeblock_container";
|
||||
const EDIT_PREVIOUS_MESSAGE_MIN_LINES: usize = 1;
|
||||
|
|
|
@ -44,6 +44,7 @@ use assistant_context::{AssistantContext, ContextEvent, ContextSummary};
|
|||
use assistant_slash_command::SlashCommandWorkingSet;
|
||||
use assistant_tool::ToolWorkingSet;
|
||||
use client::{DisableAiSettings, UserStore, zed_urls};
|
||||
use cloud_llm_client::{CompletionIntent, UsageLimit};
|
||||
use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
|
||||
use feature_flags::{self, FeatureFlagAppExt};
|
||||
use fs::Fs;
|
||||
|
@ -80,7 +81,6 @@ use zed_actions::{
|
|||
agent::{OpenConfiguration, OpenOnboardingModal, ResetOnboarding, ToggleModelSelector},
|
||||
assistant::{OpenRulesLibrary, ToggleFocus},
|
||||
};
|
||||
use zed_llm_client::{CompletionIntent, UsageLimit};
|
||||
|
||||
const AGENT_PANEL_KEY: &str = "agent_panel";
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ use agent::{
|
|||
use agent_settings::AgentSettings;
|
||||
use anyhow::{Context as _, Result};
|
||||
use client::telemetry::Telemetry;
|
||||
use cloud_llm_client::CompletionIntent;
|
||||
use collections::HashSet;
|
||||
use editor::{Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset as _, ToPoint};
|
||||
use futures::{
|
||||
|
@ -35,7 +36,6 @@ use std::{
|
|||
};
|
||||
use streaming_diff::{CharOperation, LineDiff, LineOperation, StreamingDiff};
|
||||
use telemetry_events::{AssistantEventData, AssistantKind, AssistantPhase};
|
||||
use zed_llm_client::CompletionIntent;
|
||||
|
||||
pub struct BufferCodegen {
|
||||
alternatives: Vec<Entity<CodegenAlternative>>,
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
#![allow(unused, dead_code)]
|
||||
|
||||
use client::{ModelRequestUsage, RequestUsage};
|
||||
use cloud_llm_client::{Plan, UsageLimit};
|
||||
use gpui::Global;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use ui::prelude::*;
|
||||
use zed_llm_client::{Plan, UsageLimit};
|
||||
|
||||
/// Debug only: Used for testing various account states
|
||||
///
|
||||
|
|
|
@ -18,6 +18,7 @@ use agent_settings::{AgentSettings, CompletionMode};
|
|||
use ai_onboarding::ApiKeysWithProviders;
|
||||
use buffer_diff::BufferDiff;
|
||||
use client::UserStore;
|
||||
use cloud_llm_client::CompletionIntent;
|
||||
use collections::{HashMap, HashSet};
|
||||
use editor::actions::{MoveUp, Paste};
|
||||
use editor::display_map::CreaseId;
|
||||
|
@ -53,7 +54,6 @@ use util::ResultExt as _;
|
|||
use workspace::{CollaboratorId, Workspace};
|
||||
use zed_actions::agent::Chat;
|
||||
use zed_actions::agent::ToggleModelSelector;
|
||||
use zed_llm_client::CompletionIntent;
|
||||
|
||||
use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider, crease_for_mention};
|
||||
use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
|
||||
|
@ -1300,11 +1300,11 @@ impl MessageEditor {
|
|||
let plan = user_store
|
||||
.current_plan()
|
||||
.map(|plan| match plan {
|
||||
Plan::Free => zed_llm_client::Plan::ZedFree,
|
||||
Plan::ZedPro => zed_llm_client::Plan::ZedPro,
|
||||
Plan::ZedProTrial => zed_llm_client::Plan::ZedProTrial,
|
||||
Plan::Free => cloud_llm_client::Plan::ZedFree,
|
||||
Plan::ZedPro => cloud_llm_client::Plan::ZedPro,
|
||||
Plan::ZedProTrial => cloud_llm_client::Plan::ZedProTrial,
|
||||
})
|
||||
.unwrap_or(zed_llm_client::Plan::ZedFree);
|
||||
.unwrap_or(cloud_llm_client::Plan::ZedFree);
|
||||
|
||||
let usage = user_store.model_request_usage()?;
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ use agent::{
|
|||
use agent_settings::AgentSettings;
|
||||
use anyhow::{Context as _, Result};
|
||||
use client::telemetry::Telemetry;
|
||||
use cloud_llm_client::CompletionIntent;
|
||||
use collections::{HashMap, VecDeque};
|
||||
use editor::{MultiBuffer, actions::SelectAll};
|
||||
use fs::Fs;
|
||||
|
@ -27,7 +28,6 @@ use terminal_view::TerminalView;
|
|||
use ui::prelude::*;
|
||||
use util::ResultExt;
|
||||
use workspace::{Toast, Workspace, notifications::NotificationId};
|
||||
use zed_llm_client::CompletionIntent;
|
||||
|
||||
pub fn init(
|
||||
fs: Arc<dyn Fs>,
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
use client::{ModelRequestUsage, RequestUsage, zed_urls};
|
||||
use cloud_llm_client::{Plan, UsageLimit};
|
||||
use component::{empty_example, example_group_with_title, single_example};
|
||||
use gpui::{AnyElement, App, IntoElement, RenderOnce, Window};
|
||||
use ui::{Callout, prelude::*};
|
||||
use zed_llm_client::{Plan, UsageLimit};
|
||||
|
||||
#[derive(IntoElement, RegisterComponent)]
|
||||
pub struct UsageCallout {
|
||||
|
|
|
@ -19,6 +19,7 @@ assistant_slash_commands.workspace = true
|
|||
chrono.workspace = true
|
||||
client.workspace = true
|
||||
clock.workspace = true
|
||||
cloud_llm_client.workspace = true
|
||||
collections.workspace = true
|
||||
context_server.workspace = true
|
||||
fs.workspace = true
|
||||
|
@ -48,7 +49,6 @@ util.workspace = true
|
|||
uuid.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_llm_client.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
indoc.workspace = true
|
||||
|
|
|
@ -11,6 +11,7 @@ use assistant_slash_command::{
|
|||
use assistant_slash_commands::FileCommandMetadata;
|
||||
use client::{self, Client, proto, telemetry::Telemetry};
|
||||
use clock::ReplicaId;
|
||||
use cloud_llm_client::CompletionIntent;
|
||||
use collections::{HashMap, HashSet};
|
||||
use fs::{Fs, RenameOptions};
|
||||
use futures::{FutureExt, StreamExt, future::Shared};
|
||||
|
@ -46,7 +47,6 @@ use text::{BufferSnapshot, ToPoint};
|
|||
use ui::IconName;
|
||||
use util::{ResultExt, TryFutureExt, post_inc};
|
||||
use uuid::Uuid;
|
||||
use zed_llm_client::CompletionIntent;
|
||||
|
||||
pub use crate::context_store::*;
|
||||
|
||||
|
|
|
@ -21,9 +21,11 @@ assistant_tool.workspace = true
|
|||
buffer_diff.workspace = true
|
||||
chrono.workspace = true
|
||||
client.workspace = true
|
||||
cloud_llm_client.workspace = true
|
||||
collections.workspace = true
|
||||
component.workspace = true
|
||||
derive_more.workspace = true
|
||||
diffy = "0.4.2"
|
||||
editor.workspace = true
|
||||
feature_flags.workspace = true
|
||||
futures.workspace = true
|
||||
|
@ -63,8 +65,6 @@ web_search.workspace = true
|
|||
which.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_llm_client.workspace = true
|
||||
diffy = "0.4.2"
|
||||
|
||||
[dev-dependencies]
|
||||
lsp = { workspace = true, features = ["test-support"] }
|
||||
|
|
|
@ -7,6 +7,7 @@ mod streaming_fuzzy_matcher;
|
|||
use crate::{Template, Templates};
|
||||
use anyhow::Result;
|
||||
use assistant_tool::ActionLog;
|
||||
use cloud_llm_client::CompletionIntent;
|
||||
use create_file_parser::{CreateFileParser, CreateFileParserEvent};
|
||||
pub use edit_parser::EditFormat;
|
||||
use edit_parser::{EditParser, EditParserEvent, EditParserMetrics};
|
||||
|
@ -29,7 +30,6 @@ use std::{cmp, iter, mem, ops::Range, path::PathBuf, pin::Pin, sync::Arc, task::
|
|||
use streaming_diff::{CharOperation, StreamingDiff};
|
||||
use streaming_fuzzy_matcher::StreamingFuzzyMatcher;
|
||||
use util::debug_panic;
|
||||
use zed_llm_client::CompletionIntent;
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct CreateFilePromptTemplate {
|
||||
|
|
|
@ -6,6 +6,7 @@ use anyhow::{Context as _, Result, anyhow};
|
|||
use assistant_tool::{
|
||||
ActionLog, Tool, ToolCard, ToolResult, ToolResultContent, ToolResultOutput, ToolUseStatus,
|
||||
};
|
||||
use cloud_llm_client::{WebSearchResponse, WebSearchResult};
|
||||
use futures::{Future, FutureExt, TryFutureExt};
|
||||
use gpui::{
|
||||
AnyWindowHandle, App, AppContext, Context, Entity, IntoElement, Task, WeakEntity, Window,
|
||||
|
@ -17,7 +18,6 @@ use serde::{Deserialize, Serialize};
|
|||
use ui::{IconName, Tooltip, prelude::*};
|
||||
use web_search::WebSearchRegistry;
|
||||
use workspace::Workspace;
|
||||
use zed_llm_client::{WebSearchResponse, WebSearchResult};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct WebSearchToolInput {
|
||||
|
|
|
@ -18,6 +18,6 @@ collections.workspace = true
|
|||
derive_more.workspace = true
|
||||
gpui.workspace = true
|
||||
parking_lot.workspace = true
|
||||
rodio = { version = "0.20.0", default-features = false, features = ["wav"] }
|
||||
rodio = { version = "0.21.1", default-features = false, features = ["wav", "playback", "tracing"] }
|
||||
util.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
|
|
@ -3,12 +3,9 @@ use std::{io::Cursor, sync::Arc};
|
|||
use anyhow::{Context as _, Result};
|
||||
use collections::HashMap;
|
||||
use gpui::{App, AssetSource, Global};
|
||||
use rodio::{
|
||||
Decoder, Source,
|
||||
source::{Buffered, SamplesConverter},
|
||||
};
|
||||
use rodio::{Decoder, Source, source::Buffered};
|
||||
|
||||
type Sound = Buffered<SamplesConverter<Decoder<Cursor<Vec<u8>>>, f32>>;
|
||||
type Sound = Buffered<Decoder<Cursor<Vec<u8>>>>;
|
||||
|
||||
pub struct SoundRegistry {
|
||||
cache: Arc<parking_lot::Mutex<HashMap<String, Sound>>>,
|
||||
|
@ -48,7 +45,7 @@ impl SoundRegistry {
|
|||
.with_context(|| format!("No asset available for path {path}"))??
|
||||
.into_owned();
|
||||
let cursor = Cursor::new(bytes);
|
||||
let source = Decoder::new(cursor)?.convert_samples::<f32>().buffered();
|
||||
let source = Decoder::new(cursor)?.buffered();
|
||||
|
||||
self.cache.lock().insert(name.to_string(), source.clone());
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use assets::SoundRegistry;
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use gpui::{App, AssetSource, BorrowAppContext, Global};
|
||||
use rodio::{OutputStream, OutputStreamHandle};
|
||||
use rodio::{OutputStream, OutputStreamBuilder};
|
||||
use util::ResultExt;
|
||||
|
||||
mod assets;
|
||||
|
@ -37,8 +37,7 @@ impl Sound {
|
|||
|
||||
#[derive(Default)]
|
||||
pub struct Audio {
|
||||
_output_stream: Option<OutputStream>,
|
||||
output_handle: Option<OutputStreamHandle>,
|
||||
output_handle: Option<OutputStream>,
|
||||
}
|
||||
|
||||
#[derive(Deref, DerefMut)]
|
||||
|
@ -51,11 +50,9 @@ impl Audio {
|
|||
Self::default()
|
||||
}
|
||||
|
||||
fn ensure_output_exists(&mut self) -> Option<&OutputStreamHandle> {
|
||||
fn ensure_output_exists(&mut self) -> Option<&OutputStream> {
|
||||
if self.output_handle.is_none() {
|
||||
let (_output_stream, output_handle) = OutputStream::try_default().log_err().unzip();
|
||||
self.output_handle = output_handle;
|
||||
self._output_stream = _output_stream;
|
||||
self.output_handle = OutputStreamBuilder::open_default_stream().log_err();
|
||||
}
|
||||
|
||||
self.output_handle.as_ref()
|
||||
|
@ -69,7 +66,7 @@ impl Audio {
|
|||
cx.update_global::<GlobalAudio, _>(|this, cx| {
|
||||
let output_handle = this.ensure_output_exists()?;
|
||||
let source = SoundRegistry::global(cx).get(sound.file()).log_err()?;
|
||||
output_handle.play_raw(source).log_err()?;
|
||||
output_handle.mixer().add(source);
|
||||
Some(())
|
||||
});
|
||||
}
|
||||
|
@ -80,7 +77,6 @@ impl Audio {
|
|||
}
|
||||
|
||||
cx.update_global::<GlobalAudio, _>(|this, _| {
|
||||
this._output_stream.take();
|
||||
this.output_handle.take();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ async-tungstenite = { workspace = true, features = ["tokio", "tokio-rustls-manua
|
|||
base64.workspace = true
|
||||
chrono = { workspace = true, features = ["serde"] }
|
||||
clock.workspace = true
|
||||
cloud_llm_client.workspace = true
|
||||
collections.workspace = true
|
||||
credentials_provider.workspace = true
|
||||
derive_more.workspace = true
|
||||
|
@ -33,8 +34,8 @@ http_client.workspace = true
|
|||
http_client_tls.workspace = true
|
||||
httparse = "1.10"
|
||||
log.workspace = true
|
||||
paths.workspace = true
|
||||
parking_lot.workspace = true
|
||||
paths.workspace = true
|
||||
postage.workspace = true
|
||||
rand.workspace = true
|
||||
regex.workspace = true
|
||||
|
@ -46,19 +47,18 @@ serde_json.workspace = true
|
|||
settings.workspace = true
|
||||
sha2.workspace = true
|
||||
smol.workspace = true
|
||||
telemetry.workspace = true
|
||||
telemetry_events.workspace = true
|
||||
text.workspace = true
|
||||
thiserror.workspace = true
|
||||
time.workspace = true
|
||||
tiny_http.workspace = true
|
||||
tokio-socks = { version = "0.5.2", default-features = false, features = ["futures-io"] }
|
||||
tokio.workspace = true
|
||||
url.workspace = true
|
||||
util.workspace = true
|
||||
worktree.workspace = true
|
||||
telemetry.workspace = true
|
||||
tokio.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
zed_llm_client.workspace = true
|
||||
worktree.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
clock = { workspace = true, features = ["test-support"] }
|
||||
|
|
|
@ -21,7 +21,7 @@ use futures::{
|
|||
channel::oneshot, future::BoxFuture,
|
||||
};
|
||||
use gpui::{App, AsyncApp, Entity, Global, Task, WeakEntity, actions};
|
||||
use http_client::{AsyncBody, HttpClient, HttpClientWithUrl};
|
||||
use http_client::{AsyncBody, HttpClient, HttpClientWithUrl, http};
|
||||
use parking_lot::RwLock;
|
||||
use postage::watch;
|
||||
use proxy::connect_proxy_stream;
|
||||
|
@ -1138,7 +1138,7 @@ impl Client {
|
|||
.to_str()
|
||||
.map_err(EstablishConnectionError::other)?
|
||||
.to_string();
|
||||
Url::parse(&collab_url).with_context(|| format!("parsing colab rpc url {collab_url}"))
|
||||
Url::parse(&collab_url).with_context(|| format!("parsing collab rpc url {collab_url}"))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1158,6 +1158,7 @@ impl Client {
|
|||
|
||||
let http = self.http.clone();
|
||||
let proxy = http.proxy().cloned();
|
||||
let user_agent = http.user_agent().cloned();
|
||||
let credentials = credentials.clone();
|
||||
let rpc_url = self.rpc_url(http, release_channel);
|
||||
let system_id = self.telemetry.system_id();
|
||||
|
@ -1209,7 +1210,7 @@ impl Client {
|
|||
// We then modify the request to add our desired headers.
|
||||
let request_headers = request.headers_mut();
|
||||
request_headers.insert(
|
||||
"Authorization",
|
||||
http::header::AUTHORIZATION,
|
||||
HeaderValue::from_str(&credentials.authorization_header())?,
|
||||
);
|
||||
request_headers.insert(
|
||||
|
@ -1221,6 +1222,9 @@ impl Client {
|
|||
"x-zed-release-channel",
|
||||
HeaderValue::from_str(release_channel.map(|r| r.dev_name()).unwrap_or("unknown"))?,
|
||||
);
|
||||
if let Some(user_agent) = user_agent {
|
||||
request_headers.insert(http::header::USER_AGENT, user_agent);
|
||||
}
|
||||
if let Some(system_id) = system_id {
|
||||
request_headers.insert("x-zed-system-id", HeaderValue::from_str(&system_id)?);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
use super::{Client, Status, TypedEnvelope, proto};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use chrono::{DateTime, Utc};
|
||||
use cloud_llm_client::{
|
||||
EDIT_PREDICTIONS_USAGE_AMOUNT_HEADER_NAME, EDIT_PREDICTIONS_USAGE_LIMIT_HEADER_NAME,
|
||||
MODEL_REQUESTS_USAGE_AMOUNT_HEADER_NAME, MODEL_REQUESTS_USAGE_LIMIT_HEADER_NAME, UsageLimit,
|
||||
};
|
||||
use collections::{HashMap, HashSet, hash_map::Entry};
|
||||
use derive_more::Deref;
|
||||
use feature_flags::FeatureFlagAppExt;
|
||||
|
@ -17,10 +21,6 @@ use std::{
|
|||
};
|
||||
use text::ReplicaId;
|
||||
use util::{TryFutureExt as _, maybe};
|
||||
use zed_llm_client::{
|
||||
EDIT_PREDICTIONS_USAGE_AMOUNT_HEADER_NAME, EDIT_PREDICTIONS_USAGE_LIMIT_HEADER_NAME,
|
||||
MODEL_REQUESTS_USAGE_AMOUNT_HEADER_NAME, MODEL_REQUESTS_USAGE_LIMIT_HEADER_NAME, UsageLimit,
|
||||
};
|
||||
|
||||
pub type UserId = u64;
|
||||
|
||||
|
|
23
crates/cloud_llm_client/Cargo.toml
Normal file
23
crates/cloud_llm_client/Cargo.toml
Normal file
|
@ -0,0 +1,23 @@
|
|||
[package]
|
||||
name = "cloud_llm_client"
|
||||
version = "0.1.0"
|
||||
publish.workspace = true
|
||||
edition.workspace = true
|
||||
license = "Apache-2.0"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/cloud_llm_client.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
serde = { workspace = true, features = ["derive", "rc"] }
|
||||
serde_json.workspace = true
|
||||
strum = { workspace = true, features = ["derive"] }
|
||||
uuid = { workspace = true, features = ["serde"] }
|
||||
workspace-hack.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions.workspace = true
|
1
crates/cloud_llm_client/LICENSE-APACHE
Symbolic link
1
crates/cloud_llm_client/LICENSE-APACHE
Symbolic link
|
@ -0,0 +1 @@
|
|||
../../LICENSE-APACHE
|
370
crates/cloud_llm_client/src/cloud_llm_client.rs
Normal file
370
crates/cloud_llm_client/src/cloud_llm_client.rs
Normal file
|
@ -0,0 +1,370 @@
|
|||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Context as _;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum::{Display, EnumIter, EnumString};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// The name of the header used to indicate which version of Zed the client is running.
|
||||
pub const ZED_VERSION_HEADER_NAME: &str = "x-zed-version";
|
||||
|
||||
/// The name of the header used to indicate when a request failed due to an
|
||||
/// expired LLM token.
|
||||
///
|
||||
/// The client may use this as a signal to refresh the token.
|
||||
pub const EXPIRED_LLM_TOKEN_HEADER_NAME: &str = "x-zed-expired-token";
|
||||
|
||||
/// The name of the header used to indicate what plan the user is currently on.
|
||||
pub const CURRENT_PLAN_HEADER_NAME: &str = "x-zed-plan";
|
||||
|
||||
/// The name of the header used to indicate the usage limit for model requests.
|
||||
pub const MODEL_REQUESTS_USAGE_LIMIT_HEADER_NAME: &str = "x-zed-model-requests-usage-limit";
|
||||
|
||||
/// The name of the header used to indicate the usage amount for model requests.
|
||||
pub const MODEL_REQUESTS_USAGE_AMOUNT_HEADER_NAME: &str = "x-zed-model-requests-usage-amount";
|
||||
|
||||
/// The name of the header used to indicate the usage limit for edit predictions.
|
||||
pub const EDIT_PREDICTIONS_USAGE_LIMIT_HEADER_NAME: &str = "x-zed-edit-predictions-usage-limit";
|
||||
|
||||
/// The name of the header used to indicate the usage amount for edit predictions.
|
||||
pub const EDIT_PREDICTIONS_USAGE_AMOUNT_HEADER_NAME: &str = "x-zed-edit-predictions-usage-amount";
|
||||
|
||||
/// The name of the header used to indicate the resource for which the subscription limit has been reached.
|
||||
pub const SUBSCRIPTION_LIMIT_RESOURCE_HEADER_NAME: &str = "x-zed-subscription-limit-resource";
|
||||
|
||||
pub const MODEL_REQUESTS_RESOURCE_HEADER_VALUE: &str = "model_requests";
|
||||
pub const EDIT_PREDICTIONS_RESOURCE_HEADER_VALUE: &str = "edit_predictions";
|
||||
|
||||
/// The name of the header used to indicate that the maximum number of consecutive tool uses has been reached.
|
||||
pub const TOOL_USE_LIMIT_REACHED_HEADER_NAME: &str = "x-zed-tool-use-limit-reached";
|
||||
|
||||
/// The name of the header used to indicate the the minimum required Zed version.
|
||||
///
|
||||
/// This can be used to force a Zed upgrade in order to continue communicating
|
||||
/// with the LLM service.
|
||||
pub const MINIMUM_REQUIRED_VERSION_HEADER_NAME: &str = "x-zed-minimum-required-version";
|
||||
|
||||
/// The name of the header used by the client to indicate to the server that it supports receiving status messages.
|
||||
pub const CLIENT_SUPPORTS_STATUS_MESSAGES_HEADER_NAME: &str =
|
||||
"x-zed-client-supports-status-messages";
|
||||
|
||||
/// The name of the header used by the server to indicate to the client that it supports sending status messages.
|
||||
pub const SERVER_SUPPORTS_STATUS_MESSAGES_HEADER_NAME: &str =
|
||||
"x-zed-server-supports-status-messages";
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum UsageLimit {
|
||||
Limited(i32),
|
||||
Unlimited,
|
||||
}
|
||||
|
||||
impl FromStr for UsageLimit {
|
||||
type Err = anyhow::Error;
|
||||
|
||||
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||
match value {
|
||||
"unlimited" => Ok(Self::Unlimited),
|
||||
limit => limit
|
||||
.parse::<i32>()
|
||||
.map(Self::Limited)
|
||||
.context("failed to parse limit"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Plan {
|
||||
#[default]
|
||||
#[serde(alias = "Free")]
|
||||
ZedFree,
|
||||
#[serde(alias = "ZedPro")]
|
||||
ZedPro,
|
||||
#[serde(alias = "ZedProTrial")]
|
||||
ZedProTrial,
|
||||
}
|
||||
|
||||
impl Plan {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Plan::ZedFree => "zed_free",
|
||||
Plan::ZedPro => "zed_pro",
|
||||
Plan::ZedProTrial => "zed_pro_trial",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn model_requests_limit(&self) -> UsageLimit {
|
||||
match self {
|
||||
Plan::ZedPro => UsageLimit::Limited(500),
|
||||
Plan::ZedProTrial => UsageLimit::Limited(150),
|
||||
Plan::ZedFree => UsageLimit::Limited(50),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn edit_predictions_limit(&self) -> UsageLimit {
|
||||
match self {
|
||||
Plan::ZedPro => UsageLimit::Unlimited,
|
||||
Plan::ZedProTrial => UsageLimit::Unlimited,
|
||||
Plan::ZedFree => UsageLimit::Limited(2_000),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Plan {
|
||||
type Err = anyhow::Error;
|
||||
|
||||
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||
match value {
|
||||
"zed_free" => Ok(Plan::ZedFree),
|
||||
"zed_pro" => Ok(Plan::ZedPro),
|
||||
"zed_pro_trial" => Ok(Plan::ZedProTrial),
|
||||
plan => Err(anyhow::anyhow!("invalid plan: {plan:?}")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug, PartialEq, Eq, Hash, Clone, Copy, Serialize, Deserialize, EnumString, EnumIter, Display,
|
||||
)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum LanguageModelProvider {
|
||||
Anthropic,
|
||||
OpenAi,
|
||||
Google,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PredictEditsBody {
|
||||
#[serde(skip_serializing_if = "Option::is_none", default)]
|
||||
pub outline: Option<String>,
|
||||
pub input_events: String,
|
||||
pub input_excerpt: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none", default)]
|
||||
pub speculated_output: Option<String>,
|
||||
/// Whether the user provided consent for sampling this interaction.
|
||||
#[serde(default, alias = "data_collection_permission")]
|
||||
pub can_collect_data: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none", default)]
|
||||
pub diagnostic_groups: Option<Vec<(String, serde_json::Value)>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PredictEditsResponse {
|
||||
pub request_id: Uuid,
|
||||
pub output_excerpt: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AcceptEditPredictionBody {
|
||||
pub request_id: Uuid,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CompletionMode {
|
||||
Normal,
|
||||
Max,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CompletionIntent {
|
||||
UserPrompt,
|
||||
ToolResults,
|
||||
ThreadSummarization,
|
||||
ThreadContextSummarization,
|
||||
CreateFile,
|
||||
EditFile,
|
||||
InlineAssist,
|
||||
TerminalInlineAssist,
|
||||
GenerateGitCommitMessage,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct CompletionBody {
|
||||
#[serde(skip_serializing_if = "Option::is_none", default)]
|
||||
pub thread_id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none", default)]
|
||||
pub prompt_id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none", default)]
|
||||
pub intent: Option<CompletionIntent>,
|
||||
#[serde(skip_serializing_if = "Option::is_none", default)]
|
||||
pub mode: Option<CompletionMode>,
|
||||
pub provider: LanguageModelProvider,
|
||||
pub model: String,
|
||||
pub provider_request: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CompletionRequestStatus {
|
||||
Queued {
|
||||
position: usize,
|
||||
},
|
||||
Started,
|
||||
Failed {
|
||||
code: String,
|
||||
message: String,
|
||||
request_id: Uuid,
|
||||
/// Retry duration in seconds.
|
||||
retry_after: Option<f64>,
|
||||
},
|
||||
UsageUpdated {
|
||||
amount: usize,
|
||||
limit: UsageLimit,
|
||||
},
|
||||
ToolUseLimitReached,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CompletionEvent<T> {
|
||||
Status(CompletionRequestStatus),
|
||||
Event(T),
|
||||
}
|
||||
|
||||
impl<T> CompletionEvent<T> {
|
||||
pub fn into_status(self) -> Option<CompletionRequestStatus> {
|
||||
match self {
|
||||
Self::Status(status) => Some(status),
|
||||
Self::Event(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_event(self) -> Option<T> {
|
||||
match self {
|
||||
Self::Event(event) => Some(event),
|
||||
Self::Status(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct WebSearchBody {
|
||||
pub query: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct WebSearchResponse {
|
||||
pub results: Vec<WebSearchResult>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct WebSearchResult {
|
||||
pub title: String,
|
||||
pub url: String,
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct CountTokensBody {
|
||||
pub provider: LanguageModelProvider,
|
||||
pub model: String,
|
||||
pub provider_request: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct CountTokensResponse {
|
||||
pub tokens: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
|
||||
pub struct LanguageModelId(pub Arc<str>);
|
||||
|
||||
impl std::fmt::Display for LanguageModelId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct LanguageModel {
|
||||
pub provider: LanguageModelProvider,
|
||||
pub id: LanguageModelId,
|
||||
pub display_name: String,
|
||||
pub max_token_count: usize,
|
||||
pub max_token_count_in_max_mode: Option<usize>,
|
||||
pub max_output_tokens: usize,
|
||||
pub supports_tools: bool,
|
||||
pub supports_images: bool,
|
||||
pub supports_thinking: bool,
|
||||
pub supports_max_mode: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ListModelsResponse {
|
||||
pub models: Vec<LanguageModel>,
|
||||
pub default_model: LanguageModelId,
|
||||
pub default_fast_model: LanguageModelId,
|
||||
pub recommended_models: Vec<LanguageModelId>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct GetSubscriptionResponse {
|
||||
pub plan: Plan,
|
||||
pub usage: Option<CurrentUsage>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct CurrentUsage {
|
||||
pub model_requests: UsageData,
|
||||
pub edit_predictions: UsageData,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct UsageData {
|
||||
pub used: u32,
|
||||
pub limit: UsageLimit,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_plan_deserialize_snake_case() {
|
||||
let plan = serde_json::from_value::<Plan>(json!("zed_free")).unwrap();
|
||||
assert_eq!(plan, Plan::ZedFree);
|
||||
|
||||
let plan = serde_json::from_value::<Plan>(json!("zed_pro")).unwrap();
|
||||
assert_eq!(plan, Plan::ZedPro);
|
||||
|
||||
let plan = serde_json::from_value::<Plan>(json!("zed_pro_trial")).unwrap();
|
||||
assert_eq!(plan, Plan::ZedProTrial);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_plan_deserialize_aliases() {
|
||||
let plan = serde_json::from_value::<Plan>(json!("Free")).unwrap();
|
||||
assert_eq!(plan, Plan::ZedFree);
|
||||
|
||||
let plan = serde_json::from_value::<Plan>(json!("ZedPro")).unwrap();
|
||||
assert_eq!(plan, Plan::ZedPro);
|
||||
|
||||
let plan = serde_json::from_value::<Plan>(json!("ZedProTrial")).unwrap();
|
||||
assert_eq!(plan, Plan::ZedProTrial);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_usage_limit_from_str() {
|
||||
let limit = UsageLimit::from_str("unlimited").unwrap();
|
||||
assert!(matches!(limit, UsageLimit::Unlimited));
|
||||
|
||||
let limit = UsageLimit::from_str(&0.to_string()).unwrap();
|
||||
assert!(matches!(limit, UsageLimit::Limited(0)));
|
||||
|
||||
let limit = UsageLimit::from_str(&50.to_string()).unwrap();
|
||||
assert!(matches!(limit, UsageLimit::Limited(50)));
|
||||
|
||||
for value in ["not_a_number", "50xyz"] {
|
||||
let limit = UsageLimit::from_str(value);
|
||||
assert!(limit.is_err());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -23,13 +23,14 @@ async-stripe.workspace = true
|
|||
async-trait.workspace = true
|
||||
async-tungstenite.workspace = true
|
||||
aws-config = { version = "1.1.5" }
|
||||
aws-sdk-s3 = { version = "1.15.0" }
|
||||
aws-sdk-kinesis = "1.51.0"
|
||||
aws-sdk-s3 = { version = "1.15.0" }
|
||||
axum = { version = "0.6", features = ["json", "headers", "ws"] }
|
||||
axum-extra = { version = "0.4", features = ["erased-json"] }
|
||||
base64.workspace = true
|
||||
chrono.workspace = true
|
||||
clock.workspace = true
|
||||
cloud_llm_client.workspace = true
|
||||
collections.workspace = true
|
||||
dashmap.workspace = true
|
||||
derive_more.workspace = true
|
||||
|
@ -75,7 +76,6 @@ tracing-subscriber = { version = "0.3.18", features = ["env-filter", "json", "re
|
|||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
zed_llm_client.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
agent_settings.workspace = true
|
||||
|
|
|
@ -100,7 +100,6 @@ impl std::fmt::Display for SystemIdHeader {
|
|||
|
||||
pub fn routes(rpc_server: Arc<rpc::Server>) -> Router<(), Body> {
|
||||
Router::new()
|
||||
.route("/user", get(update_or_create_authenticated_user))
|
||||
.route("/users/look_up", get(look_up_user))
|
||||
.route("/users/:id/access_tokens", post(create_access_token))
|
||||
.route("/users/:id/refresh_llm_tokens", post(refresh_llm_tokens))
|
||||
|
@ -145,48 +144,6 @@ pub async fn validate_api_token<B>(req: Request<B>, next: Next<B>) -> impl IntoR
|
|||
Ok::<_, Error>(next.run(req).await)
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct AuthenticatedUserParams {
|
||||
github_user_id: i32,
|
||||
github_login: String,
|
||||
github_email: Option<String>,
|
||||
github_name: Option<String>,
|
||||
github_user_created_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct AuthenticatedUserResponse {
|
||||
user: User,
|
||||
metrics_id: String,
|
||||
feature_flags: Vec<String>,
|
||||
}
|
||||
|
||||
async fn update_or_create_authenticated_user(
|
||||
Query(params): Query<AuthenticatedUserParams>,
|
||||
Extension(app): Extension<Arc<AppState>>,
|
||||
) -> Result<Json<AuthenticatedUserResponse>> {
|
||||
let initial_channel_id = app.config.auto_join_channel_id;
|
||||
|
||||
let user = app
|
||||
.db
|
||||
.update_or_create_user_by_github_account(
|
||||
¶ms.github_login,
|
||||
params.github_user_id,
|
||||
params.github_email.as_deref(),
|
||||
params.github_name.as_deref(),
|
||||
params.github_user_created_at,
|
||||
initial_channel_id,
|
||||
)
|
||||
.await?;
|
||||
let metrics_id = app.db.get_user_metrics_id(user.id).await?;
|
||||
let feature_flags = app.db.get_user_flags(user.id).await?;
|
||||
Ok(Json(AuthenticatedUserResponse {
|
||||
user,
|
||||
metrics_id,
|
||||
feature_flags,
|
||||
}))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct LookUpUserParams {
|
||||
identifier: String,
|
||||
|
@ -353,9 +310,9 @@ async fn refresh_llm_tokens(
|
|||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct UpdatePlanBody {
|
||||
pub plan: zed_llm_client::Plan,
|
||||
pub plan: cloud_llm_client::Plan,
|
||||
pub subscription_period: SubscriptionPeriod,
|
||||
pub usage: zed_llm_client::CurrentUsage,
|
||||
pub usage: cloud_llm_client::CurrentUsage,
|
||||
pub trial_started_at: Option<DateTime<Utc>>,
|
||||
pub is_usage_based_billing_enabled: bool,
|
||||
pub is_account_too_young: bool,
|
||||
|
@ -377,9 +334,9 @@ async fn update_plan(
|
|||
extract::Json(body): extract::Json<UpdatePlanBody>,
|
||||
) -> Result<Json<UpdatePlanResponse>> {
|
||||
let plan = match body.plan {
|
||||
zed_llm_client::Plan::ZedFree => proto::Plan::Free,
|
||||
zed_llm_client::Plan::ZedPro => proto::Plan::ZedPro,
|
||||
zed_llm_client::Plan::ZedProTrial => proto::Plan::ZedProTrial,
|
||||
cloud_llm_client::Plan::ZedFree => proto::Plan::Free,
|
||||
cloud_llm_client::Plan::ZedPro => proto::Plan::ZedPro,
|
||||
cloud_llm_client::Plan::ZedProTrial => proto::Plan::ZedProTrial,
|
||||
};
|
||||
|
||||
let update_user_plan = proto::UpdateUserPlan {
|
||||
|
@ -411,15 +368,15 @@ async fn update_plan(
|
|||
Ok(Json(UpdatePlanResponse {}))
|
||||
}
|
||||
|
||||
fn usage_limit_to_proto(limit: zed_llm_client::UsageLimit) -> proto::UsageLimit {
|
||||
fn usage_limit_to_proto(limit: cloud_llm_client::UsageLimit) -> proto::UsageLimit {
|
||||
proto::UsageLimit {
|
||||
variant: Some(match limit {
|
||||
zed_llm_client::UsageLimit::Limited(limit) => {
|
||||
cloud_llm_client::UsageLimit::Limited(limit) => {
|
||||
proto::usage_limit::Variant::Limited(proto::usage_limit::Limited {
|
||||
limit: limit as u32,
|
||||
})
|
||||
}
|
||||
zed_llm_client::UsageLimit::Unlimited => {
|
||||
cloud_llm_client::UsageLimit::Unlimited => {
|
||||
proto::usage_limit::Variant::Unlimited(proto::usage_limit::Unlimited {})
|
||||
}
|
||||
}),
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
use anyhow::{Context as _, bail};
|
||||
use chrono::{DateTime, Utc};
|
||||
use cloud_llm_client::LanguageModelProvider;
|
||||
use collections::{HashMap, HashSet};
|
||||
use sea_orm::ActiveValue;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use stripe::{CancellationDetailsReason, EventObject, EventType, ListEvents, SubscriptionStatus};
|
||||
use util::{ResultExt, maybe};
|
||||
use zed_llm_client::LanguageModelProvider;
|
||||
|
||||
use crate::AppState;
|
||||
use crate::db::billing_subscription::{
|
||||
|
@ -87,6 +87,14 @@ async fn poll_stripe_events(
|
|||
stripe_client: &Arc<dyn StripeClient>,
|
||||
real_stripe_client: &stripe::Client,
|
||||
) -> anyhow::Result<()> {
|
||||
let feature_flags = app.db.list_feature_flags().await?;
|
||||
let sync_events_using_cloud = feature_flags
|
||||
.iter()
|
||||
.any(|flag| flag.flag == "cloud-stripe-events-polling" && flag.enabled_for_all);
|
||||
if sync_events_using_cloud {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
fn event_type_to_string(event_type: EventType) -> String {
|
||||
// Calling `to_string` on `stripe::EventType` members gives us a quoted string,
|
||||
// so we need to unquote it.
|
||||
|
@ -569,6 +577,14 @@ async fn sync_model_request_usage_with_stripe(
|
|||
llm_db: &Arc<LlmDatabase>,
|
||||
stripe_billing: &Arc<StripeBilling>,
|
||||
) -> anyhow::Result<()> {
|
||||
let feature_flags = app.db.list_feature_flags().await?;
|
||||
let sync_model_request_usage_using_cloud = feature_flags
|
||||
.iter()
|
||||
.any(|flag| flag.flag == "cloud-stripe-usage-meters-sync" && flag.enabled_for_all);
|
||||
if sync_model_request_usage_using_cloud {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
log::info!("Stripe usage sync: Starting");
|
||||
let started_at = Utc::now();
|
||||
|
||||
|
|
|
@ -8,7 +8,6 @@ use axum::{
|
|||
use chrono::{NaiveDateTime, SecondsFormat};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::api::AuthenticatedUserParams;
|
||||
use crate::db::ContributorSelector;
|
||||
use crate::{AppState, Result};
|
||||
|
||||
|
@ -104,9 +103,18 @@ impl RenovateBot {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct AddContributorBody {
|
||||
github_user_id: i32,
|
||||
github_login: String,
|
||||
github_email: Option<String>,
|
||||
github_name: Option<String>,
|
||||
github_user_created_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
async fn add_contributor(
|
||||
Extension(app): Extension<Arc<AppState>>,
|
||||
extract::Json(params): extract::Json<AuthenticatedUserParams>,
|
||||
extract::Json(params): extract::Json<AddContributorBody>,
|
||||
) -> Result<()> {
|
||||
let initial_channel_id = app.config.auto_join_channel_id;
|
||||
app.db
|
||||
|
|
|
@ -95,7 +95,7 @@ pub enum SubscriptionKind {
|
|||
ZedFree,
|
||||
}
|
||||
|
||||
impl From<SubscriptionKind> for zed_llm_client::Plan {
|
||||
impl From<SubscriptionKind> for cloud_llm_client::Plan {
|
||||
fn from(value: SubscriptionKind) -> Self {
|
||||
match value {
|
||||
SubscriptionKind::ZedPro => Self::ZedPro,
|
||||
|
|
|
@ -6,11 +6,11 @@ mod tables;
|
|||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
use cloud_llm_client::LanguageModelProvider;
|
||||
use collections::HashMap;
|
||||
pub use ids::*;
|
||||
pub use seed::*;
|
||||
pub use tables::*;
|
||||
use zed_llm_client::LanguageModelProvider;
|
||||
|
||||
#[cfg(test)]
|
||||
pub use tests::TestLlmDb;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use cloud_llm_client::LanguageModelProvider;
|
||||
use pretty_assertions::assert_eq;
|
||||
use zed_llm_client::LanguageModelProvider;
|
||||
|
||||
use crate::llm::db::LlmDatabase;
|
||||
use crate::test_llm_db;
|
||||
|
|
|
@ -4,12 +4,12 @@ use crate::llm::{AGENT_EXTENDED_TRIAL_FEATURE_FLAG, BYPASS_ACCOUNT_AGE_CHECK_FEA
|
|||
use crate::{Config, db::billing_preference};
|
||||
use anyhow::{Context as _, Result};
|
||||
use chrono::{NaiveDateTime, Utc};
|
||||
use cloud_llm_client::Plan;
|
||||
use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::Duration;
|
||||
use thiserror::Error;
|
||||
use uuid::Uuid;
|
||||
use zed_llm_client::Plan;
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
|
|
@ -23,6 +23,7 @@ use anyhow::{Context as _, anyhow, bail};
|
|||
use async_tungstenite::tungstenite::{
|
||||
Message as TungsteniteMessage, protocol::CloseFrame as TungsteniteCloseFrame,
|
||||
};
|
||||
use axum::headers::UserAgent;
|
||||
use axum::{
|
||||
Extension, Router, TypedHeader,
|
||||
body::Body,
|
||||
|
@ -750,6 +751,7 @@ impl Server {
|
|||
address: String,
|
||||
principal: Principal,
|
||||
zed_version: ZedVersion,
|
||||
user_agent: Option<String>,
|
||||
geoip_country_code: Option<String>,
|
||||
system_id: Option<String>,
|
||||
send_connection_id: Option<oneshot::Sender<ConnectionId>>,
|
||||
|
@ -762,9 +764,14 @@ impl Server {
|
|||
user_id=field::Empty,
|
||||
login=field::Empty,
|
||||
impersonator=field::Empty,
|
||||
user_agent=field::Empty,
|
||||
geoip_country_code=field::Empty
|
||||
);
|
||||
principal.update_span(&span);
|
||||
if let Some(user_agent) = user_agent {
|
||||
span.record("user_agent", user_agent);
|
||||
}
|
||||
|
||||
if let Some(country_code) = geoip_country_code.as_ref() {
|
||||
span.record("geoip_country_code", country_code);
|
||||
}
|
||||
|
@ -1172,6 +1179,7 @@ pub async fn handle_websocket_request(
|
|||
ConnectInfo(socket_address): ConnectInfo<SocketAddr>,
|
||||
Extension(server): Extension<Arc<Server>>,
|
||||
Extension(principal): Extension<Principal>,
|
||||
user_agent: Option<TypedHeader<UserAgent>>,
|
||||
country_code_header: Option<TypedHeader<CloudflareIpCountryHeader>>,
|
||||
system_id_header: Option<TypedHeader<SystemIdHeader>>,
|
||||
ws: WebSocketUpgrade,
|
||||
|
@ -1227,6 +1235,7 @@ pub async fn handle_websocket_request(
|
|||
socket_address,
|
||||
principal,
|
||||
version,
|
||||
user_agent.map(|header| header.to_string()),
|
||||
country_code_header.map(|header| header.to_string()),
|
||||
system_id_header.map(|header| header.to_string()),
|
||||
None,
|
||||
|
@ -2859,12 +2868,12 @@ async fn make_update_user_plan_message(
|
|||
}
|
||||
|
||||
fn model_requests_limit(
|
||||
plan: zed_llm_client::Plan,
|
||||
plan: cloud_llm_client::Plan,
|
||||
feature_flags: &Vec<String>,
|
||||
) -> zed_llm_client::UsageLimit {
|
||||
) -> cloud_llm_client::UsageLimit {
|
||||
match plan.model_requests_limit() {
|
||||
zed_llm_client::UsageLimit::Limited(limit) => {
|
||||
let limit = if plan == zed_llm_client::Plan::ZedProTrial
|
||||
cloud_llm_client::UsageLimit::Limited(limit) => {
|
||||
let limit = if plan == cloud_llm_client::Plan::ZedProTrial
|
||||
&& feature_flags
|
||||
.iter()
|
||||
.any(|flag| flag == AGENT_EXTENDED_TRIAL_FEATURE_FLAG)
|
||||
|
@ -2874,9 +2883,9 @@ fn model_requests_limit(
|
|||
limit
|
||||
};
|
||||
|
||||
zed_llm_client::UsageLimit::Limited(limit)
|
||||
cloud_llm_client::UsageLimit::Limited(limit)
|
||||
}
|
||||
zed_llm_client::UsageLimit::Unlimited => zed_llm_client::UsageLimit::Unlimited,
|
||||
cloud_llm_client::UsageLimit::Unlimited => cloud_llm_client::UsageLimit::Unlimited,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2886,21 +2895,21 @@ fn subscription_usage_to_proto(
|
|||
feature_flags: &Vec<String>,
|
||||
) -> proto::SubscriptionUsage {
|
||||
let plan = match plan {
|
||||
proto::Plan::Free => zed_llm_client::Plan::ZedFree,
|
||||
proto::Plan::ZedPro => zed_llm_client::Plan::ZedPro,
|
||||
proto::Plan::ZedProTrial => zed_llm_client::Plan::ZedProTrial,
|
||||
proto::Plan::Free => cloud_llm_client::Plan::ZedFree,
|
||||
proto::Plan::ZedPro => cloud_llm_client::Plan::ZedPro,
|
||||
proto::Plan::ZedProTrial => cloud_llm_client::Plan::ZedProTrial,
|
||||
};
|
||||
|
||||
proto::SubscriptionUsage {
|
||||
model_requests_usage_amount: usage.model_requests as u32,
|
||||
model_requests_usage_limit: Some(proto::UsageLimit {
|
||||
variant: Some(match model_requests_limit(plan, feature_flags) {
|
||||
zed_llm_client::UsageLimit::Limited(limit) => {
|
||||
cloud_llm_client::UsageLimit::Limited(limit) => {
|
||||
proto::usage_limit::Variant::Limited(proto::usage_limit::Limited {
|
||||
limit: limit as u32,
|
||||
})
|
||||
}
|
||||
zed_llm_client::UsageLimit::Unlimited => {
|
||||
cloud_llm_client::UsageLimit::Unlimited => {
|
||||
proto::usage_limit::Variant::Unlimited(proto::usage_limit::Unlimited {})
|
||||
}
|
||||
}),
|
||||
|
@ -2908,12 +2917,12 @@ fn subscription_usage_to_proto(
|
|||
edit_predictions_usage_amount: usage.edit_predictions as u32,
|
||||
edit_predictions_usage_limit: Some(proto::UsageLimit {
|
||||
variant: Some(match plan.edit_predictions_limit() {
|
||||
zed_llm_client::UsageLimit::Limited(limit) => {
|
||||
cloud_llm_client::UsageLimit::Limited(limit) => {
|
||||
proto::usage_limit::Variant::Limited(proto::usage_limit::Limited {
|
||||
limit: limit as u32,
|
||||
})
|
||||
}
|
||||
zed_llm_client::UsageLimit::Unlimited => {
|
||||
cloud_llm_client::UsageLimit::Unlimited => {
|
||||
proto::usage_limit::Variant::Unlimited(proto::usage_limit::Unlimited {})
|
||||
}
|
||||
}),
|
||||
|
@ -2926,21 +2935,21 @@ fn make_default_subscription_usage(
|
|||
feature_flags: &Vec<String>,
|
||||
) -> proto::SubscriptionUsage {
|
||||
let plan = match plan {
|
||||
proto::Plan::Free => zed_llm_client::Plan::ZedFree,
|
||||
proto::Plan::ZedPro => zed_llm_client::Plan::ZedPro,
|
||||
proto::Plan::ZedProTrial => zed_llm_client::Plan::ZedProTrial,
|
||||
proto::Plan::Free => cloud_llm_client::Plan::ZedFree,
|
||||
proto::Plan::ZedPro => cloud_llm_client::Plan::ZedPro,
|
||||
proto::Plan::ZedProTrial => cloud_llm_client::Plan::ZedProTrial,
|
||||
};
|
||||
|
||||
proto::SubscriptionUsage {
|
||||
model_requests_usage_amount: 0,
|
||||
model_requests_usage_limit: Some(proto::UsageLimit {
|
||||
variant: Some(match model_requests_limit(plan, feature_flags) {
|
||||
zed_llm_client::UsageLimit::Limited(limit) => {
|
||||
cloud_llm_client::UsageLimit::Limited(limit) => {
|
||||
proto::usage_limit::Variant::Limited(proto::usage_limit::Limited {
|
||||
limit: limit as u32,
|
||||
})
|
||||
}
|
||||
zed_llm_client::UsageLimit::Unlimited => {
|
||||
cloud_llm_client::UsageLimit::Unlimited => {
|
||||
proto::usage_limit::Variant::Unlimited(proto::usage_limit::Unlimited {})
|
||||
}
|
||||
}),
|
||||
|
@ -2948,12 +2957,12 @@ fn make_default_subscription_usage(
|
|||
edit_predictions_usage_amount: 0,
|
||||
edit_predictions_usage_limit: Some(proto::UsageLimit {
|
||||
variant: Some(match plan.edit_predictions_limit() {
|
||||
zed_llm_client::UsageLimit::Limited(limit) => {
|
||||
cloud_llm_client::UsageLimit::Limited(limit) => {
|
||||
proto::usage_limit::Variant::Limited(proto::usage_limit::Limited {
|
||||
limit: limit as u32,
|
||||
})
|
||||
}
|
||||
zed_llm_client::UsageLimit::Unlimited => {
|
||||
cloud_llm_client::UsageLimit::Unlimited => {
|
||||
proto::usage_limit::Variant::Unlimited(proto::usage_limit::Unlimited {})
|
||||
}
|
||||
}),
|
||||
|
|
|
@ -256,6 +256,7 @@ impl TestServer {
|
|||
ZedVersion(SemanticVersion::new(1, 0, 0)),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(connection_id_tx),
|
||||
Executor::Deterministic(cx.background_executor().clone()),
|
||||
None,
|
||||
|
|
|
@ -158,6 +158,7 @@ impl Client {
|
|||
pub fn stdio(
|
||||
server_id: ContextServerId,
|
||||
binary: ModelContextServerBinary,
|
||||
working_directory: &Option<PathBuf>,
|
||||
cx: AsyncApp,
|
||||
) -> Result<Self> {
|
||||
log::info!(
|
||||
|
@ -172,7 +173,7 @@ impl Client {
|
|||
.map(|name| name.to_string_lossy().to_string())
|
||||
.unwrap_or_else(String::new);
|
||||
|
||||
let transport = Arc::new(StdioTransport::new(binary, &cx)?);
|
||||
let transport = Arc::new(StdioTransport::new(binary, working_directory, &cx)?);
|
||||
Self::new(server_id, server_name.into(), transport, cx)
|
||||
}
|
||||
|
||||
|
|
|
@ -53,7 +53,7 @@ impl std::fmt::Debug for ContextServerCommand {
|
|||
}
|
||||
|
||||
enum ContextServerTransport {
|
||||
Stdio(ContextServerCommand),
|
||||
Stdio(ContextServerCommand, Option<PathBuf>),
|
||||
Custom(Arc<dyn crate::transport::Transport>),
|
||||
}
|
||||
|
||||
|
@ -64,11 +64,18 @@ pub struct ContextServer {
|
|||
}
|
||||
|
||||
impl ContextServer {
|
||||
pub fn stdio(id: ContextServerId, command: ContextServerCommand) -> Self {
|
||||
pub fn stdio(
|
||||
id: ContextServerId,
|
||||
command: ContextServerCommand,
|
||||
working_directory: Option<Arc<Path>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id,
|
||||
client: RwLock::new(None),
|
||||
configuration: ContextServerTransport::Stdio(command),
|
||||
configuration: ContextServerTransport::Stdio(
|
||||
command,
|
||||
working_directory.map(|directory| directory.to_path_buf()),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -90,13 +97,14 @@ impl ContextServer {
|
|||
|
||||
pub async fn start(self: Arc<Self>, cx: &AsyncApp) -> Result<()> {
|
||||
let client = match &self.configuration {
|
||||
ContextServerTransport::Stdio(command) => Client::stdio(
|
||||
ContextServerTransport::Stdio(command, working_directory) => Client::stdio(
|
||||
client::ContextServerId(self.id.0.clone()),
|
||||
client::ModelContextServerBinary {
|
||||
executable: Path::new(&command.path).to_path_buf(),
|
||||
args: command.args.clone(),
|
||||
env: command.env.clone(),
|
||||
},
|
||||
working_directory,
|
||||
cx.clone(),
|
||||
)?,
|
||||
ContextServerTransport::Custom(transport) => Client::new(
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
use std::path::PathBuf;
|
||||
use std::pin::Pin;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
|
@ -22,7 +23,11 @@ pub struct StdioTransport {
|
|||
}
|
||||
|
||||
impl StdioTransport {
|
||||
pub fn new(binary: ModelContextServerBinary, cx: &AsyncApp) -> Result<Self> {
|
||||
pub fn new(
|
||||
binary: ModelContextServerBinary,
|
||||
working_directory: &Option<PathBuf>,
|
||||
cx: &AsyncApp,
|
||||
) -> Result<Self> {
|
||||
let mut command = util::command::new_smol_command(&binary.executable);
|
||||
command
|
||||
.args(&binary.args)
|
||||
|
@ -32,6 +37,10 @@ impl StdioTransport {
|
|||
.stderr(std::process::Stdio::piped())
|
||||
.kill_on_drop(true);
|
||||
|
||||
if let Some(working_directory) = working_directory {
|
||||
command.current_dir(working_directory);
|
||||
}
|
||||
|
||||
let mut server = command.spawn().with_context(|| {
|
||||
format!(
|
||||
"failed to spawn command. (path={:?}, args={:?})",
|
||||
|
|
|
@ -7,17 +7,17 @@ license = "GPL-3.0-or-later"
|
|||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
clap.workspace = true
|
||||
command_palette.workspace = true
|
||||
gpui.workspace = true
|
||||
mdbook = "0.4.40"
|
||||
regex.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
regex.workspace = true
|
||||
util.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
zed.workspace = true
|
||||
gpui.workspace = true
|
||||
command_palette.workspace = true
|
||||
zlog.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
use anyhow::Result;
|
||||
use clap::{Arg, ArgMatches, Command};
|
||||
use anyhow::{Context, Result};
|
||||
use mdbook::BookItem;
|
||||
use mdbook::book::{Book, Chapter};
|
||||
use mdbook::preprocess::CmdPreprocessor;
|
||||
use regex::Regex;
|
||||
use settings::KeymapFile;
|
||||
use std::collections::HashSet;
|
||||
use std::borrow::Cow;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::io::{self, Read};
|
||||
use std::process;
|
||||
use std::sync::LazyLock;
|
||||
use util::paths::PathExt;
|
||||
|
||||
static KEYMAP_MACOS: LazyLock<KeymapFile> = LazyLock::new(|| {
|
||||
load_keymap("keymaps/default-macos.json").expect("Failed to load MacOS keymap")
|
||||
|
@ -20,60 +21,68 @@ static KEYMAP_LINUX: LazyLock<KeymapFile> = LazyLock::new(|| {
|
|||
|
||||
static ALL_ACTIONS: LazyLock<Vec<ActionDef>> = LazyLock::new(dump_all_gpui_actions);
|
||||
|
||||
pub fn make_app() -> Command {
|
||||
Command::new("zed-docs-preprocessor")
|
||||
.about("Preprocesses Zed Docs content to provide rich action & keybinding support and more")
|
||||
.subcommand(
|
||||
Command::new("supports")
|
||||
.arg(Arg::new("renderer").required(true))
|
||||
.about("Check whether a renderer is supported by this preprocessor"),
|
||||
)
|
||||
}
|
||||
const FRONT_MATTER_COMMENT: &'static str = "<!-- ZED_META {} -->";
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let matches = make_app().get_matches();
|
||||
zlog::init();
|
||||
zlog::init_output_stderr();
|
||||
// call a zed:: function so everything in `zed` crate is linked and
|
||||
// all actions in the actual app are registered
|
||||
zed::stdout_is_a_pty();
|
||||
let args = std::env::args().skip(1).collect::<Vec<_>>();
|
||||
|
||||
if let Some(sub_args) = matches.subcommand_matches("supports") {
|
||||
handle_supports(sub_args);
|
||||
} else {
|
||||
handle_preprocessing()?;
|
||||
match args.get(0).map(String::as_str) {
|
||||
Some("supports") => {
|
||||
let renderer = args.get(1).expect("Required argument");
|
||||
let supported = renderer != "not-supported";
|
||||
if supported {
|
||||
process::exit(0);
|
||||
} else {
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
Some("postprocess") => handle_postprocessing()?,
|
||||
_ => handle_preprocessing()?,
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
enum Error {
|
||||
enum PreprocessorError {
|
||||
ActionNotFound { action_name: String },
|
||||
DeprecatedActionUsed { used: String, should_be: String },
|
||||
InvalidFrontmatterLine(String),
|
||||
}
|
||||
|
||||
impl Error {
|
||||
impl PreprocessorError {
|
||||
fn new_for_not_found_action(action_name: String) -> Self {
|
||||
for action in &*ALL_ACTIONS {
|
||||
for alias in action.deprecated_aliases {
|
||||
if alias == &action_name {
|
||||
return Error::DeprecatedActionUsed {
|
||||
return PreprocessorError::DeprecatedActionUsed {
|
||||
used: action_name.clone(),
|
||||
should_be: action.name.to_string(),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Error::ActionNotFound {
|
||||
PreprocessorError::ActionNotFound {
|
||||
action_name: action_name.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Error {
|
||||
impl std::fmt::Display for PreprocessorError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Error::ActionNotFound { action_name } => write!(f, "Action not found: {}", action_name),
|
||||
Error::DeprecatedActionUsed { used, should_be } => write!(
|
||||
PreprocessorError::InvalidFrontmatterLine(line) => {
|
||||
write!(f, "Invalid frontmatter line: {}", line)
|
||||
}
|
||||
PreprocessorError::ActionNotFound { action_name } => {
|
||||
write!(f, "Action not found: {}", action_name)
|
||||
}
|
||||
PreprocessorError::DeprecatedActionUsed { used, should_be } => write!(
|
||||
f,
|
||||
"Deprecated action used: {} should be {}",
|
||||
used, should_be
|
||||
|
@ -89,8 +98,9 @@ fn handle_preprocessing() -> Result<()> {
|
|||
|
||||
let (_ctx, mut book) = CmdPreprocessor::parse_input(input.as_bytes())?;
|
||||
|
||||
let mut errors = HashSet::<Error>::new();
|
||||
let mut errors = HashSet::<PreprocessorError>::new();
|
||||
|
||||
handle_frontmatter(&mut book, &mut errors);
|
||||
template_and_validate_keybindings(&mut book, &mut errors);
|
||||
template_and_validate_actions(&mut book, &mut errors);
|
||||
|
||||
|
@ -108,19 +118,41 @@ fn handle_preprocessing() -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_supports(sub_args: &ArgMatches) -> ! {
|
||||
let renderer = sub_args
|
||||
.get_one::<String>("renderer")
|
||||
.expect("Required argument");
|
||||
let supported = renderer != "not-supported";
|
||||
if supported {
|
||||
process::exit(0);
|
||||
} else {
|
||||
process::exit(1);
|
||||
}
|
||||
fn handle_frontmatter(book: &mut Book, errors: &mut HashSet<PreprocessorError>) {
|
||||
let frontmatter_regex = Regex::new(r"(?s)^\s*---(.*?)---").unwrap();
|
||||
for_each_chapter_mut(book, |chapter| {
|
||||
let new_content = frontmatter_regex.replace(&chapter.content, |caps: ®ex::Captures| {
|
||||
let frontmatter = caps[1].trim();
|
||||
let frontmatter = frontmatter.trim_matches(&[' ', '-', '\n']);
|
||||
let mut metadata = HashMap::<String, String>::default();
|
||||
for line in frontmatter.lines() {
|
||||
let Some((name, value)) = line.split_once(':') else {
|
||||
errors.insert(PreprocessorError::InvalidFrontmatterLine(format!(
|
||||
"{}: {}",
|
||||
chapter_breadcrumbs(&chapter),
|
||||
line
|
||||
)));
|
||||
continue;
|
||||
};
|
||||
let name = name.trim();
|
||||
let value = value.trim();
|
||||
metadata.insert(name.to_string(), value.to_string());
|
||||
}
|
||||
FRONT_MATTER_COMMENT.replace(
|
||||
"{}",
|
||||
&serde_json::to_string(&metadata).expect("Failed to serialize metadata"),
|
||||
)
|
||||
});
|
||||
match new_content {
|
||||
Cow::Owned(content) => {
|
||||
chapter.content = content;
|
||||
}
|
||||
Cow::Borrowed(_) => {}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn template_and_validate_keybindings(book: &mut Book, errors: &mut HashSet<Error>) {
|
||||
fn template_and_validate_keybindings(book: &mut Book, errors: &mut HashSet<PreprocessorError>) {
|
||||
let regex = Regex::new(r"\{#kb (.*?)\}").unwrap();
|
||||
|
||||
for_each_chapter_mut(book, |chapter| {
|
||||
|
@ -128,7 +160,9 @@ fn template_and_validate_keybindings(book: &mut Book, errors: &mut HashSet<Error
|
|||
.replace_all(&chapter.content, |caps: ®ex::Captures| {
|
||||
let action = caps[1].trim();
|
||||
if find_action_by_name(action).is_none() {
|
||||
errors.insert(Error::new_for_not_found_action(action.to_string()));
|
||||
errors.insert(PreprocessorError::new_for_not_found_action(
|
||||
action.to_string(),
|
||||
));
|
||||
return String::new();
|
||||
}
|
||||
let macos_binding = find_binding("macos", action).unwrap_or_default();
|
||||
|
@ -144,7 +178,7 @@ fn template_and_validate_keybindings(book: &mut Book, errors: &mut HashSet<Error
|
|||
});
|
||||
}
|
||||
|
||||
fn template_and_validate_actions(book: &mut Book, errors: &mut HashSet<Error>) {
|
||||
fn template_and_validate_actions(book: &mut Book, errors: &mut HashSet<PreprocessorError>) {
|
||||
let regex = Regex::new(r"\{#action (.*?)\}").unwrap();
|
||||
|
||||
for_each_chapter_mut(book, |chapter| {
|
||||
|
@ -152,7 +186,9 @@ fn template_and_validate_actions(book: &mut Book, errors: &mut HashSet<Error>) {
|
|||
.replace_all(&chapter.content, |caps: ®ex::Captures| {
|
||||
let name = caps[1].trim();
|
||||
let Some(action) = find_action_by_name(name) else {
|
||||
errors.insert(Error::new_for_not_found_action(name.to_string()));
|
||||
errors.insert(PreprocessorError::new_for_not_found_action(
|
||||
name.to_string(),
|
||||
));
|
||||
return String::new();
|
||||
};
|
||||
format!("<code class=\"hljs\">{}</code>", &action.human_name)
|
||||
|
@ -217,6 +253,13 @@ fn name_for_action(action_as_str: String) -> String {
|
|||
.unwrap_or(action_as_str)
|
||||
}
|
||||
|
||||
fn chapter_breadcrumbs(chapter: &Chapter) -> String {
|
||||
let mut breadcrumbs = Vec::with_capacity(chapter.parent_names.len() + 1);
|
||||
breadcrumbs.extend(chapter.parent_names.iter().map(String::as_str));
|
||||
breadcrumbs.push(chapter.name.as_str());
|
||||
format!("[{:?}] {}", chapter.source_path, breadcrumbs.join(" > "))
|
||||
}
|
||||
|
||||
fn load_keymap(asset_path: &str) -> Result<KeymapFile> {
|
||||
let content = util::asset_str::<settings::SettingsAssets>(asset_path);
|
||||
KeymapFile::parse(content.as_ref())
|
||||
|
@ -254,3 +297,126 @@ fn dump_all_gpui_actions() -> Vec<ActionDef> {
|
|||
|
||||
return actions;
|
||||
}
|
||||
|
||||
fn handle_postprocessing() -> Result<()> {
|
||||
let logger = zlog::scoped!("render");
|
||||
let mut ctx = mdbook::renderer::RenderContext::from_json(io::stdin())?;
|
||||
let output = ctx
|
||||
.config
|
||||
.get_mut("output")
|
||||
.expect("has output")
|
||||
.as_table_mut()
|
||||
.expect("output is table");
|
||||
let zed_html = output.remove("zed-html").expect("zed-html output defined");
|
||||
let default_description = zed_html
|
||||
.get("default-description")
|
||||
.expect("Default description not found")
|
||||
.as_str()
|
||||
.expect("Default description not a string")
|
||||
.to_string();
|
||||
let default_title = zed_html
|
||||
.get("default-title")
|
||||
.expect("Default title not found")
|
||||
.as_str()
|
||||
.expect("Default title not a string")
|
||||
.to_string();
|
||||
|
||||
output.insert("html".to_string(), zed_html);
|
||||
mdbook::Renderer::render(&mdbook::renderer::HtmlHandlebars::new(), &ctx)?;
|
||||
let ignore_list = ["toc.html"];
|
||||
|
||||
let root_dir = ctx.destination.clone();
|
||||
let mut files = Vec::with_capacity(128);
|
||||
let mut queue = Vec::with_capacity(64);
|
||||
queue.push(root_dir.clone());
|
||||
while let Some(dir) = queue.pop() {
|
||||
for entry in std::fs::read_dir(&dir).context(dir.to_sanitized_string())? {
|
||||
let Ok(entry) = entry else {
|
||||
continue;
|
||||
};
|
||||
let file_type = entry.file_type().context("Failed to determine file type")?;
|
||||
if file_type.is_dir() {
|
||||
queue.push(entry.path());
|
||||
}
|
||||
if file_type.is_file()
|
||||
&& matches!(
|
||||
entry.path().extension().and_then(std::ffi::OsStr::to_str),
|
||||
Some("html")
|
||||
)
|
||||
{
|
||||
if ignore_list.contains(&&*entry.file_name().to_string_lossy()) {
|
||||
zlog::info!(logger => "Ignoring {}", entry.path().to_string_lossy());
|
||||
} else {
|
||||
files.push(entry.path());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
zlog::info!(logger => "Processing {} `.html` files", files.len());
|
||||
let meta_regex = Regex::new(&FRONT_MATTER_COMMENT.replace("{}", "(.*)")).unwrap();
|
||||
for file in files {
|
||||
let contents = std::fs::read_to_string(&file)?;
|
||||
let mut meta_description = None;
|
||||
let mut meta_title = None;
|
||||
let contents = meta_regex.replace(&contents, |caps: ®ex::Captures| {
|
||||
let metadata: HashMap<String, String> = serde_json::from_str(&caps[1]).with_context(|| format!("JSON Metadata: {:?}", &caps[1])).expect("Failed to deserialize metadata");
|
||||
for (kind, content) in metadata {
|
||||
match kind.as_str() {
|
||||
"description" => {
|
||||
meta_description = Some(content);
|
||||
}
|
||||
"title" => {
|
||||
meta_title = Some(content);
|
||||
}
|
||||
_ => {
|
||||
zlog::warn!(logger => "Unrecognized frontmatter key: {} in {:?}", kind, pretty_path(&file, &root_dir));
|
||||
}
|
||||
}
|
||||
}
|
||||
String::new()
|
||||
});
|
||||
let meta_description = meta_description.as_ref().unwrap_or_else(|| {
|
||||
zlog::warn!(logger => "No meta description found for {:?}", pretty_path(&file, &root_dir));
|
||||
&default_description
|
||||
});
|
||||
let page_title = extract_title_from_page(&contents, pretty_path(&file, &root_dir));
|
||||
let meta_title = meta_title.as_ref().unwrap_or_else(|| {
|
||||
zlog::debug!(logger => "No meta title found for {:?}", pretty_path(&file, &root_dir));
|
||||
&default_title
|
||||
});
|
||||
let meta_title = format!("{} | {}", page_title, meta_title);
|
||||
zlog::trace!(logger => "Updating {:?}", pretty_path(&file, &root_dir));
|
||||
let contents = contents.replace("#description#", meta_description);
|
||||
let contents = TITLE_REGEX
|
||||
.replace(&contents, |_: ®ex::Captures| {
|
||||
format!("<title>{}</title>", meta_title)
|
||||
})
|
||||
.to_string();
|
||||
// let contents = contents.replace("#title#", &meta_title);
|
||||
std::fs::write(file, contents)?;
|
||||
}
|
||||
return Ok(());
|
||||
|
||||
fn pretty_path<'a>(
|
||||
path: &'a std::path::PathBuf,
|
||||
root: &'a std::path::PathBuf,
|
||||
) -> &'a std::path::Path {
|
||||
&path.strip_prefix(&root).unwrap_or(&path)
|
||||
}
|
||||
const TITLE_REGEX: std::cell::LazyCell<Regex> =
|
||||
std::cell::LazyCell::new(|| Regex::new(r"<title>\s*(.*?)\s*</title>").unwrap());
|
||||
fn extract_title_from_page(contents: &str, pretty_path: &std::path::Path) -> String {
|
||||
let title_tag_contents = &TITLE_REGEX
|
||||
.captures(&contents)
|
||||
.with_context(|| format!("Failed to find title in {:?}", pretty_path))
|
||||
.expect("Page has <title> element")[1];
|
||||
let title = title_tag_contents
|
||||
.trim()
|
||||
.strip_suffix("- Zed")
|
||||
.unwrap_or(title_tag_contents)
|
||||
.trim()
|
||||
.to_string();
|
||||
title
|
||||
}
|
||||
}
|
||||
|
|
|
@ -65,7 +65,7 @@ use display_map::*;
|
|||
pub use display_map::{ChunkRenderer, ChunkRendererContext, DisplayPoint, FoldPlaceholder};
|
||||
pub use editor_settings::{
|
||||
CurrentLineHighlight, DocumentColorsRenderMode, EditorSettings, HideMouseMode,
|
||||
ScrollBeyondLastLine, ScrollbarAxes, SearchSettings, ShowScrollbar,
|
||||
ScrollBeyondLastLine, ScrollbarAxes, SearchSettings, ShowMinimap, ShowScrollbar,
|
||||
};
|
||||
use editor_settings::{GoToDefinitionFallback, Minimap as MinimapSettings};
|
||||
pub use editor_settings_controls::*;
|
||||
|
|
|
@ -19,8 +19,8 @@ path = "src/explorer.rs"
|
|||
|
||||
[dependencies]
|
||||
agent.workspace = true
|
||||
agent_ui.workspace = true
|
||||
agent_settings.workspace = true
|
||||
agent_ui.workspace = true
|
||||
anyhow.workspace = true
|
||||
assistant_tool.workspace = true
|
||||
assistant_tools.workspace = true
|
||||
|
@ -29,6 +29,7 @@ buffer_diff.workspace = true
|
|||
chrono.workspace = true
|
||||
clap.workspace = true
|
||||
client.workspace = true
|
||||
cloud_llm_client.workspace = true
|
||||
collections.workspace = true
|
||||
debug_adapter_extension.workspace = true
|
||||
dirs.workspace = true
|
||||
|
@ -68,4 +69,3 @@ util.workspace = true
|
|||
uuid.workspace = true
|
||||
watch.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
zed_llm_client.workspace = true
|
||||
|
|
|
@ -15,11 +15,11 @@ use agent_settings::AgentProfileId;
|
|||
use anyhow::{Result, anyhow};
|
||||
use async_trait::async_trait;
|
||||
use buffer_diff::DiffHunkStatus;
|
||||
use cloud_llm_client::CompletionIntent;
|
||||
use collections::HashMap;
|
||||
use futures::{FutureExt as _, StreamExt, channel::mpsc, select_biased};
|
||||
use gpui::{App, AppContext, AsyncApp, Entity};
|
||||
use language_model::{LanguageModel, Role, StopReason};
|
||||
use zed_llm_client::CompletionIntent;
|
||||
|
||||
pub const THREAD_EVENT_TIMEOUT: Duration = Duration::from_secs(60 * 2);
|
||||
|
||||
|
|
|
@ -106,7 +106,7 @@ impl extension::Extension for WasmExtension {
|
|||
}
|
||||
.boxed()
|
||||
})
|
||||
.await
|
||||
.await?
|
||||
}
|
||||
|
||||
async fn language_server_initialization_options(
|
||||
|
@ -131,7 +131,7 @@ impl extension::Extension for WasmExtension {
|
|||
}
|
||||
.boxed()
|
||||
})
|
||||
.await
|
||||
.await?
|
||||
}
|
||||
|
||||
async fn language_server_workspace_configuration(
|
||||
|
@ -154,7 +154,7 @@ impl extension::Extension for WasmExtension {
|
|||
}
|
||||
.boxed()
|
||||
})
|
||||
.await
|
||||
.await?
|
||||
}
|
||||
|
||||
async fn language_server_additional_initialization_options(
|
||||
|
@ -179,7 +179,7 @@ impl extension::Extension for WasmExtension {
|
|||
}
|
||||
.boxed()
|
||||
})
|
||||
.await
|
||||
.await?
|
||||
}
|
||||
|
||||
async fn language_server_additional_workspace_configuration(
|
||||
|
@ -204,7 +204,7 @@ impl extension::Extension for WasmExtension {
|
|||
}
|
||||
.boxed()
|
||||
})
|
||||
.await
|
||||
.await?
|
||||
}
|
||||
|
||||
async fn labels_for_completions(
|
||||
|
@ -230,7 +230,7 @@ impl extension::Extension for WasmExtension {
|
|||
}
|
||||
.boxed()
|
||||
})
|
||||
.await
|
||||
.await?
|
||||
}
|
||||
|
||||
async fn labels_for_symbols(
|
||||
|
@ -256,7 +256,7 @@ impl extension::Extension for WasmExtension {
|
|||
}
|
||||
.boxed()
|
||||
})
|
||||
.await
|
||||
.await?
|
||||
}
|
||||
|
||||
async fn complete_slash_command_argument(
|
||||
|
@ -275,7 +275,7 @@ impl extension::Extension for WasmExtension {
|
|||
}
|
||||
.boxed()
|
||||
})
|
||||
.await
|
||||
.await?
|
||||
}
|
||||
|
||||
async fn run_slash_command(
|
||||
|
@ -301,7 +301,7 @@ impl extension::Extension for WasmExtension {
|
|||
}
|
||||
.boxed()
|
||||
})
|
||||
.await
|
||||
.await?
|
||||
}
|
||||
|
||||
async fn context_server_command(
|
||||
|
@ -320,7 +320,7 @@ impl extension::Extension for WasmExtension {
|
|||
}
|
||||
.boxed()
|
||||
})
|
||||
.await
|
||||
.await?
|
||||
}
|
||||
|
||||
async fn context_server_configuration(
|
||||
|
@ -347,7 +347,7 @@ impl extension::Extension for WasmExtension {
|
|||
}
|
||||
.boxed()
|
||||
})
|
||||
.await
|
||||
.await?
|
||||
}
|
||||
|
||||
async fn suggest_docs_packages(&self, provider: Arc<str>) -> Result<Vec<String>> {
|
||||
|
@ -362,7 +362,7 @@ impl extension::Extension for WasmExtension {
|
|||
}
|
||||
.boxed()
|
||||
})
|
||||
.await
|
||||
.await?
|
||||
}
|
||||
|
||||
async fn index_docs(
|
||||
|
@ -388,7 +388,7 @@ impl extension::Extension for WasmExtension {
|
|||
}
|
||||
.boxed()
|
||||
})
|
||||
.await
|
||||
.await?
|
||||
}
|
||||
|
||||
async fn get_dap_binary(
|
||||
|
@ -410,7 +410,7 @@ impl extension::Extension for WasmExtension {
|
|||
}
|
||||
.boxed()
|
||||
})
|
||||
.await
|
||||
.await?
|
||||
}
|
||||
async fn dap_request_kind(
|
||||
&self,
|
||||
|
@ -427,7 +427,7 @@ impl extension::Extension for WasmExtension {
|
|||
}
|
||||
.boxed()
|
||||
})
|
||||
.await
|
||||
.await?
|
||||
}
|
||||
|
||||
async fn dap_config_to_scenario(&self, config: ZedDebugConfig) -> Result<DebugScenario> {
|
||||
|
@ -441,7 +441,7 @@ impl extension::Extension for WasmExtension {
|
|||
}
|
||||
.boxed()
|
||||
})
|
||||
.await
|
||||
.await?
|
||||
}
|
||||
|
||||
async fn dap_locator_create_scenario(
|
||||
|
@ -465,7 +465,7 @@ impl extension::Extension for WasmExtension {
|
|||
}
|
||||
.boxed()
|
||||
})
|
||||
.await
|
||||
.await?
|
||||
}
|
||||
async fn run_dap_locator(
|
||||
&self,
|
||||
|
@ -481,7 +481,7 @@ impl extension::Extension for WasmExtension {
|
|||
}
|
||||
.boxed()
|
||||
})
|
||||
.await
|
||||
.await?
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -761,7 +761,7 @@ impl WasmExtension {
|
|||
.with_context(|| format!("failed to load wasm extension {}", manifest.id))
|
||||
}
|
||||
|
||||
pub async fn call<T, Fn>(&self, f: Fn) -> T
|
||||
pub async fn call<T, Fn>(&self, f: Fn) -> Result<T>
|
||||
where
|
||||
T: 'static + Send,
|
||||
Fn: 'static
|
||||
|
@ -777,8 +777,19 @@ impl WasmExtension {
|
|||
}
|
||||
.boxed()
|
||||
}))
|
||||
.expect("wasm extension channel should not be closed yet");
|
||||
return_rx.await.expect("wasm extension channel")
|
||||
.map_err(|_| {
|
||||
anyhow!(
|
||||
"wasm extension channel should not be closed yet, extension {} (id {})",
|
||||
self.manifest.name,
|
||||
self.manifest.id,
|
||||
)
|
||||
})?;
|
||||
return_rx.await.with_context(|| {
|
||||
format!(
|
||||
"wasm extension channel, extension {} (id {})",
|
||||
self.manifest.name, self.manifest.id,
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -799,8 +810,19 @@ impl WasmState {
|
|||
}
|
||||
.boxed_local()
|
||||
}))
|
||||
.expect("main thread message channel should not be closed yet");
|
||||
async move { return_rx.await.expect("main thread message channel") }
|
||||
.unwrap_or_else(|_| {
|
||||
panic!(
|
||||
"main thread message channel should not be closed yet, extension {} (id {})",
|
||||
self.manifest.name, self.manifest.id,
|
||||
)
|
||||
});
|
||||
let name = self.manifest.name.clone();
|
||||
let id = self.manifest.id.clone();
|
||||
async move {
|
||||
return_rx.await.unwrap_or_else(|_| {
|
||||
panic!("main thread message channel, extension {name} (id {id})")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn work_dir(&self) -> PathBuf {
|
||||
|
|
|
@ -1404,14 +1404,21 @@ impl PickerDelegate for FileFinderDelegate {
|
|||
} else {
|
||||
let path_position = PathWithPosition::parse_str(&raw_query);
|
||||
|
||||
#[cfg(windows)]
|
||||
let raw_query = raw_query.trim().to_owned().replace("/", "\\");
|
||||
#[cfg(not(windows))]
|
||||
let raw_query = raw_query.trim().to_owned();
|
||||
|
||||
let file_query_end = if path_position.path.to_str().unwrap_or(&raw_query) == raw_query {
|
||||
None
|
||||
} else {
|
||||
// Safe to unwrap as we won't get here when the unwrap in if fails
|
||||
Some(path_position.path.to_str().unwrap().len())
|
||||
};
|
||||
|
||||
let query = FileSearchQuery {
|
||||
raw_query: raw_query.trim().to_owned(),
|
||||
file_query_end: if path_position.path.to_str().unwrap_or(raw_query) == raw_query {
|
||||
None
|
||||
} else {
|
||||
// Safe to unwrap as we won't get here when the unwrap in if fails
|
||||
Some(path_position.path.to_str().unwrap().len())
|
||||
},
|
||||
raw_query,
|
||||
file_query_end,
|
||||
path_position,
|
||||
};
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ buffer_diff.workspace = true
|
|||
call.workspace = true
|
||||
chrono.workspace = true
|
||||
client.workspace = true
|
||||
cloud_llm_client.workspace = true
|
||||
collections.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
component.workspace = true
|
||||
|
@ -62,7 +63,6 @@ watch.workspace = true
|
|||
workspace-hack.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
zed_llm_client.workspace = true
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows.workspace = true
|
||||
|
|
|
@ -71,12 +71,12 @@ use ui::{
|
|||
use util::{ResultExt, TryFutureExt, maybe};
|
||||
use workspace::SERIALIZATION_THROTTLE_TIME;
|
||||
|
||||
use cloud_llm_client::CompletionIntent;
|
||||
use workspace::{
|
||||
Workspace,
|
||||
dock::{DockPosition, Panel, PanelEvent},
|
||||
notifications::{DetachAndPromptErr, ErrorMessagePrompt, NotificationId},
|
||||
};
|
||||
use zed_llm_client::CompletionIntent;
|
||||
|
||||
actions!(
|
||||
git_panel,
|
||||
|
|
|
@ -47,6 +47,7 @@ wayland = [
|
|||
"wayland-cursor",
|
||||
"wayland-protocols",
|
||||
"wayland-protocols-plasma",
|
||||
"wayland-protocols-wlr",
|
||||
"filedescriptor",
|
||||
"xkbcommon",
|
||||
"open",
|
||||
|
@ -193,6 +194,9 @@ wayland-protocols = { version = "0.31.2", features = [
|
|||
wayland-protocols-plasma = { version = "0.2.0", features = [
|
||||
"client",
|
||||
], optional = true }
|
||||
wayland-protocols-wlr = { version = "0.3.8", features = [
|
||||
"client"
|
||||
], optional = true}
|
||||
|
||||
# X11
|
||||
as-raw-xcb-connection = { version = "1", optional = true }
|
||||
|
|
|
@ -6,6 +6,7 @@ use gpui::{
|
|||
actions!(example, [Tab, TabPrev]);
|
||||
|
||||
struct Example {
|
||||
focus_handle: FocusHandle,
|
||||
items: Vec<FocusHandle>,
|
||||
message: SharedString,
|
||||
}
|
||||
|
@ -20,8 +21,11 @@ impl Example {
|
|||
cx.focus_handle().tab_index(2).tab_stop(true),
|
||||
];
|
||||
|
||||
window.focus(items.first().unwrap());
|
||||
let focus_handle = cx.focus_handle();
|
||||
window.focus(&focus_handle);
|
||||
|
||||
Self {
|
||||
focus_handle,
|
||||
items,
|
||||
message: SharedString::from("Press `Tab`, `Shift-Tab` to switch focus."),
|
||||
}
|
||||
|
@ -40,6 +44,10 @@ impl Example {
|
|||
|
||||
impl Render for Example {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
fn tab_stop_style<T: Styled>(this: T) -> T {
|
||||
this.border_3().border_color(gpui::blue())
|
||||
}
|
||||
|
||||
fn button(id: impl Into<ElementId>) -> Stateful<Div> {
|
||||
div()
|
||||
.id(id)
|
||||
|
@ -52,12 +60,13 @@ impl Render for Example {
|
|||
.border_color(gpui::black())
|
||||
.bg(gpui::black())
|
||||
.text_color(gpui::white())
|
||||
.focus(|this| this.border_color(gpui::blue()))
|
||||
.focus(tab_stop_style)
|
||||
.shadow_sm()
|
||||
}
|
||||
|
||||
div()
|
||||
.id("app")
|
||||
.track_focus(&self.focus_handle)
|
||||
.on_action(cx.listener(Self::on_tab))
|
||||
.on_action(cx.listener(Self::on_tab_prev))
|
||||
.size_full()
|
||||
|
@ -86,7 +95,7 @@ impl Render for Example {
|
|||
.border_color(gpui::black())
|
||||
.when(
|
||||
item_handle.tab_stop && item_handle.is_focused(window),
|
||||
|this| this.border_color(gpui::blue()),
|
||||
tab_stop_style,
|
||||
)
|
||||
.map(|this| match item_handle.tab_stop {
|
||||
true => this
|
||||
|
|
|
@ -2023,6 +2023,10 @@ impl HttpClient for NullHttpClient {
|
|||
.boxed()
|
||||
}
|
||||
|
||||
fn user_agent(&self) -> Option<&http_client::http::HeaderValue> {
|
||||
None
|
||||
}
|
||||
|
||||
fn proxy(&self) -> Option<&Url> {
|
||||
None
|
||||
}
|
||||
|
|
|
@ -1216,6 +1216,10 @@ pub enum WindowKind {
|
|||
/// A window that appears above all other windows, usually used for alerts or popups
|
||||
/// use sparingly!
|
||||
PopUp,
|
||||
/// An overlay such as a notification window, a launcher, ...
|
||||
///
|
||||
/// Only supported on wayland
|
||||
Overlay,
|
||||
}
|
||||
|
||||
/// The appearance of the window, as defined by the operating system.
|
||||
|
|
|
@ -61,6 +61,7 @@ use wayland_protocols::xdg::decoration::zv1::client::{
|
|||
};
|
||||
use wayland_protocols::xdg::shell::client::{xdg_surface, xdg_toplevel, xdg_wm_base};
|
||||
use wayland_protocols_plasma::blur::client::{org_kde_kwin_blur, org_kde_kwin_blur_manager};
|
||||
use wayland_protocols_wlr::layer_shell::v1::client::{zwlr_layer_shell_v1, zwlr_layer_surface_v1};
|
||||
use xkbcommon::xkb::ffi::XKB_KEYMAP_FORMAT_TEXT_V1;
|
||||
use xkbcommon::xkb::{self, KEYMAP_COMPILE_NO_FLAGS, Keycode};
|
||||
|
||||
|
@ -114,6 +115,7 @@ pub struct Globals {
|
|||
pub fractional_scale_manager:
|
||||
Option<wp_fractional_scale_manager_v1::WpFractionalScaleManagerV1>,
|
||||
pub decoration_manager: Option<zxdg_decoration_manager_v1::ZxdgDecorationManagerV1>,
|
||||
pub layer_shell: Option<zwlr_layer_shell_v1::ZwlrLayerShellV1>,
|
||||
pub blur_manager: Option<org_kde_kwin_blur_manager::OrgKdeKwinBlurManager>,
|
||||
pub text_input_manager: Option<zwp_text_input_manager_v3::ZwpTextInputManagerV3>,
|
||||
pub executor: ForegroundExecutor,
|
||||
|
@ -151,6 +153,7 @@ impl Globals {
|
|||
viewporter: globals.bind(&qh, 1..=1, ()).ok(),
|
||||
fractional_scale_manager: globals.bind(&qh, 1..=1, ()).ok(),
|
||||
decoration_manager: globals.bind(&qh, 1..=1, ()).ok(),
|
||||
layer_shell: globals.bind(&qh, 1..=1, ()).ok(),
|
||||
blur_manager: globals.bind(&qh, 1..=1, ()).ok(),
|
||||
text_input_manager: globals.bind(&qh, 1..=1, ()).ok(),
|
||||
executor,
|
||||
|
@ -929,6 +932,7 @@ delegate_noop!(WaylandClientStatePtr: ignore wl_buffer::WlBuffer);
|
|||
delegate_noop!(WaylandClientStatePtr: ignore wl_region::WlRegion);
|
||||
delegate_noop!(WaylandClientStatePtr: ignore wp_fractional_scale_manager_v1::WpFractionalScaleManagerV1);
|
||||
delegate_noop!(WaylandClientStatePtr: ignore zxdg_decoration_manager_v1::ZxdgDecorationManagerV1);
|
||||
delegate_noop!(WaylandClientStatePtr: ignore zwlr_layer_shell_v1::ZwlrLayerShellV1);
|
||||
delegate_noop!(WaylandClientStatePtr: ignore org_kde_kwin_blur_manager::OrgKdeKwinBlurManager);
|
||||
delegate_noop!(WaylandClientStatePtr: ignore zwp_text_input_manager_v3::ZwpTextInputManagerV3);
|
||||
delegate_noop!(WaylandClientStatePtr: ignore org_kde_kwin_blur::OrgKdeKwinBlur);
|
||||
|
@ -1074,6 +1078,31 @@ impl Dispatch<xdg_toplevel::XdgToplevel, ObjectId> for WaylandClientStatePtr {
|
|||
}
|
||||
}
|
||||
|
||||
impl Dispatch<zwlr_layer_surface_v1::ZwlrLayerSurfaceV1, ObjectId> for WaylandClientStatePtr {
|
||||
fn event(
|
||||
this: &mut Self,
|
||||
_: &zwlr_layer_surface_v1::ZwlrLayerSurfaceV1,
|
||||
event: <zwlr_layer_surface_v1::ZwlrLayerSurfaceV1 as Proxy>::Event,
|
||||
surface_id: &ObjectId,
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
) {
|
||||
let client = this.get_client();
|
||||
let mut state = client.borrow_mut();
|
||||
let Some(window) = get_window(&mut state, surface_id) else {
|
||||
return;
|
||||
};
|
||||
drop(state);
|
||||
|
||||
let should_close = window.handle_layersurface_event(event);
|
||||
|
||||
if should_close {
|
||||
// The close logic will be handled in drop_window()
|
||||
window.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<xdg_wm_base::XdgWmBase, ()> for WaylandClientStatePtr {
|
||||
fn event(
|
||||
_: &mut Self,
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
use blade_graphics as gpu;
|
||||
use collections::HashMap;
|
||||
use futures::channel::oneshot::Receiver;
|
||||
use std::{
|
||||
cell::{Ref, RefCell, RefMut},
|
||||
ffi::c_void,
|
||||
|
@ -6,9 +9,14 @@ use std::{
|
|||
sync::Arc,
|
||||
};
|
||||
|
||||
use blade_graphics as gpu;
|
||||
use collections::HashMap;
|
||||
use futures::channel::oneshot::Receiver;
|
||||
use crate::{
|
||||
Capslock,
|
||||
platform::{
|
||||
PlatformAtlas, PlatformInputHandler, PlatformWindow,
|
||||
blade::{BladeContext, BladeRenderer, BladeSurfaceConfig},
|
||||
linux::wayland::{display::WaylandDisplay, serial::SerialKind},
|
||||
},
|
||||
};
|
||||
|
||||
use raw_window_handle as rwh;
|
||||
use wayland_backend::client::ObjectId;
|
||||
|
@ -20,6 +28,8 @@ use wayland_protocols::xdg::decoration::zv1::client::zxdg_toplevel_decoration_v1
|
|||
use wayland_protocols::xdg::shell::client::xdg_surface;
|
||||
use wayland_protocols::xdg::shell::client::xdg_toplevel::{self};
|
||||
use wayland_protocols_plasma::blur::client::org_kde_kwin_blur;
|
||||
use wayland_protocols_wlr::layer_shell::v1::client::zwlr_layer_shell_v1::Layer;
|
||||
use wayland_protocols_wlr::layer_shell::v1::client::zwlr_layer_surface_v1;
|
||||
|
||||
use crate::scene::Scene;
|
||||
use crate::{
|
||||
|
@ -27,15 +37,7 @@ use crate::{
|
|||
PlatformDisplay, PlatformInput, Point, PromptButton, PromptLevel, RequestFrameOptions,
|
||||
ResizeEdge, ScaledPixels, Size, Tiling, WaylandClientStatePtr, WindowAppearance,
|
||||
WindowBackgroundAppearance, WindowBounds, WindowControlArea, WindowControls, WindowDecorations,
|
||||
WindowParams, px, size,
|
||||
};
|
||||
use crate::{
|
||||
Capslock,
|
||||
platform::{
|
||||
PlatformAtlas, PlatformInputHandler, PlatformWindow,
|
||||
blade::{BladeContext, BladeRenderer, BladeSurfaceConfig},
|
||||
linux::wayland::{display::WaylandDisplay, serial::SerialKind},
|
||||
},
|
||||
WindowKind, WindowParams, px, size,
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
|
@ -81,14 +83,12 @@ struct InProgressConfigure {
|
|||
}
|
||||
|
||||
pub struct WaylandWindowState {
|
||||
xdg_surface: xdg_surface::XdgSurface,
|
||||
surface_state: WaylandSurfaceState,
|
||||
acknowledged_first_configure: bool,
|
||||
pub surface: wl_surface::WlSurface,
|
||||
decoration: Option<zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1>,
|
||||
app_id: Option<String>,
|
||||
appearance: WindowAppearance,
|
||||
blur: Option<org_kde_kwin_blur::OrgKdeKwinBlur>,
|
||||
toplevel: xdg_toplevel::XdgToplevel,
|
||||
viewport: Option<wp_viewport::WpViewport>,
|
||||
outputs: HashMap<ObjectId, Output>,
|
||||
display: Option<(ObjectId, Output)>,
|
||||
|
@ -114,6 +114,78 @@ pub struct WaylandWindowState {
|
|||
client_inset: Option<Pixels>,
|
||||
}
|
||||
|
||||
pub enum WaylandSurfaceState {
|
||||
Xdg(WaylandXdgSurfaceState),
|
||||
LayerShell(WaylandLayerSurfaceState),
|
||||
}
|
||||
|
||||
pub struct WaylandXdgSurfaceState {
|
||||
xdg_surface: xdg_surface::XdgSurface,
|
||||
toplevel: xdg_toplevel::XdgToplevel,
|
||||
decoration: Option<zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1>,
|
||||
}
|
||||
|
||||
pub struct WaylandLayerSurfaceState {
|
||||
layer_surface: zwlr_layer_surface_v1::ZwlrLayerSurfaceV1,
|
||||
}
|
||||
|
||||
impl WaylandSurfaceState {
|
||||
fn ack_configure(&self, serial: u32) {
|
||||
match self {
|
||||
WaylandSurfaceState::Xdg(WaylandXdgSurfaceState { xdg_surface, .. }) => {
|
||||
xdg_surface.ack_configure(serial);
|
||||
}
|
||||
WaylandSurfaceState::LayerShell(WaylandLayerSurfaceState { layer_surface, .. }) => {
|
||||
layer_surface.ack_configure(serial);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn decoration(&self) -> Option<&zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1> {
|
||||
if let WaylandSurfaceState::Xdg(WaylandXdgSurfaceState { decoration, .. }) = self {
|
||||
decoration.as_ref()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn toplevel(&self) -> Option<&xdg_toplevel::XdgToplevel> {
|
||||
if let WaylandSurfaceState::Xdg(WaylandXdgSurfaceState { toplevel, .. }) = self {
|
||||
Some(toplevel)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn set_geometry(&self, x: i32, y: i32, width: i32, height: i32) {
|
||||
match self {
|
||||
WaylandSurfaceState::Xdg(WaylandXdgSurfaceState { xdg_surface, .. }) => {
|
||||
xdg_surface.set_window_geometry(x, y, width, height);
|
||||
}
|
||||
WaylandSurfaceState::LayerShell(WaylandLayerSurfaceState { layer_surface, .. }) => {
|
||||
// cannot set window position of a layer surface
|
||||
layer_surface.set_size(width as u32, height as u32);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn destroy(&mut self) {
|
||||
match self {
|
||||
WaylandSurfaceState::Xdg(WaylandXdgSurfaceState {
|
||||
xdg_surface,
|
||||
toplevel,
|
||||
decoration: _decoration,
|
||||
}) => {
|
||||
toplevel.destroy();
|
||||
xdg_surface.destroy();
|
||||
}
|
||||
WaylandSurfaceState::LayerShell(WaylandLayerSurfaceState { layer_surface }) => {
|
||||
layer_surface.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct WaylandWindowStatePtr {
|
||||
state: Rc<RefCell<WaylandWindowState>>,
|
||||
|
@ -124,9 +196,7 @@ impl WaylandWindowState {
|
|||
pub(crate) fn new(
|
||||
handle: AnyWindowHandle,
|
||||
surface: wl_surface::WlSurface,
|
||||
xdg_surface: xdg_surface::XdgSurface,
|
||||
toplevel: xdg_toplevel::XdgToplevel,
|
||||
decoration: Option<zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1>,
|
||||
surface_state: WaylandSurfaceState,
|
||||
appearance: WindowAppearance,
|
||||
viewport: Option<wp_viewport::WpViewport>,
|
||||
client: WaylandClientStatePtr,
|
||||
|
@ -156,13 +226,11 @@ impl WaylandWindowState {
|
|||
};
|
||||
|
||||
Ok(Self {
|
||||
xdg_surface,
|
||||
surface_state,
|
||||
acknowledged_first_configure: false,
|
||||
surface,
|
||||
decoration,
|
||||
app_id: None,
|
||||
blur: None,
|
||||
toplevel,
|
||||
viewport,
|
||||
globals,
|
||||
outputs: HashMap::default(),
|
||||
|
@ -235,17 +303,16 @@ impl Drop for WaylandWindow {
|
|||
let client = state.client.clone();
|
||||
|
||||
state.renderer.destroy();
|
||||
if let Some(decoration) = &state.decoration {
|
||||
if let Some(decoration) = &state.surface_state.decoration() {
|
||||
decoration.destroy();
|
||||
}
|
||||
if let Some(blur) = &state.blur {
|
||||
blur.release();
|
||||
}
|
||||
state.toplevel.destroy();
|
||||
state.surface_state.destroy();
|
||||
if let Some(viewport) = &state.viewport {
|
||||
viewport.destroy();
|
||||
}
|
||||
state.xdg_surface.destroy();
|
||||
state.surface.destroy();
|
||||
|
||||
let state_ptr = self.0.clone();
|
||||
|
@ -279,27 +346,65 @@ impl WaylandWindow {
|
|||
appearance: WindowAppearance,
|
||||
) -> anyhow::Result<(Self, ObjectId)> {
|
||||
let surface = globals.compositor.create_surface(&globals.qh, ());
|
||||
let xdg_surface = globals
|
||||
.wm_base
|
||||
.get_xdg_surface(&surface, &globals.qh, surface.id());
|
||||
let toplevel = xdg_surface.get_toplevel(&globals.qh, surface.id());
|
||||
|
||||
if let Some(size) = params.window_min_size {
|
||||
toplevel.set_min_size(size.width.0 as i32, size.height.0 as i32);
|
||||
}
|
||||
let surface_state = match (params.kind, globals.layer_shell.as_ref()) {
|
||||
// Matching on layer_shell here means that if kind is Overlay, but the compositor doesn't support layer_shell,
|
||||
// we end up defaulting to xdg_surface anyway
|
||||
(WindowKind::Overlay, Some(layer_shell)) => {
|
||||
let layer_surface = layer_shell.get_layer_surface(
|
||||
&surface,
|
||||
None,
|
||||
Layer::Overlay,
|
||||
"".to_string(),
|
||||
&globals.qh,
|
||||
surface.id(),
|
||||
);
|
||||
|
||||
let width = params.bounds.size.width.0;
|
||||
let height = params.bounds.size.height.0;
|
||||
layer_surface.set_size(width as u32, height as u32);
|
||||
layer_surface.set_keyboard_interactivity(
|
||||
zwlr_layer_surface_v1::KeyboardInteractivity::OnDemand,
|
||||
);
|
||||
|
||||
WaylandSurfaceState::LayerShell(WaylandLayerSurfaceState { layer_surface })
|
||||
}
|
||||
_ => {
|
||||
let xdg_surface =
|
||||
globals
|
||||
.wm_base
|
||||
.get_xdg_surface(&surface, &globals.qh, surface.id());
|
||||
|
||||
let toplevel = xdg_surface.get_toplevel(&globals.qh, surface.id());
|
||||
|
||||
if let Some(size) = params.window_min_size {
|
||||
toplevel.set_min_size(size.width.0 as i32, size.height.0 as i32);
|
||||
}
|
||||
|
||||
// Attempt to set up window decorations based on the requested configuration
|
||||
let decoration = globals
|
||||
.decoration_manager
|
||||
.as_ref()
|
||||
.map(|decoration_manager| {
|
||||
decoration_manager.get_toplevel_decoration(
|
||||
&toplevel,
|
||||
&globals.qh,
|
||||
surface.id(),
|
||||
)
|
||||
});
|
||||
|
||||
WaylandSurfaceState::Xdg(WaylandXdgSurfaceState {
|
||||
xdg_surface,
|
||||
toplevel,
|
||||
decoration,
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(fractional_scale_manager) = globals.fractional_scale_manager.as_ref() {
|
||||
fractional_scale_manager.get_fractional_scale(&surface, &globals.qh, surface.id());
|
||||
}
|
||||
|
||||
// Attempt to set up window decorations based on the requested configuration
|
||||
let decoration = globals
|
||||
.decoration_manager
|
||||
.as_ref()
|
||||
.map(|decoration_manager| {
|
||||
decoration_manager.get_toplevel_decoration(&toplevel, &globals.qh, surface.id())
|
||||
});
|
||||
|
||||
let viewport = globals
|
||||
.viewporter
|
||||
.as_ref()
|
||||
|
@ -309,9 +414,7 @@ impl WaylandWindow {
|
|||
state: Rc::new(RefCell::new(WaylandWindowState::new(
|
||||
handle,
|
||||
surface.clone(),
|
||||
xdg_surface,
|
||||
toplevel,
|
||||
decoration,
|
||||
surface_state,
|
||||
appearance,
|
||||
viewport,
|
||||
client,
|
||||
|
@ -403,7 +506,7 @@ impl WaylandWindowStatePtr {
|
|||
}
|
||||
}
|
||||
let mut state = self.state.borrow_mut();
|
||||
state.xdg_surface.ack_configure(serial);
|
||||
state.surface_state.ack_configure(serial);
|
||||
|
||||
let window_geometry = inset_by_tiling(
|
||||
state.bounds.map_origin(|_| px(0.0)),
|
||||
|
@ -413,7 +516,7 @@ impl WaylandWindowStatePtr {
|
|||
.map(|v| v.0 as i32)
|
||||
.map_size(|v| if v <= 0 { 1 } else { v });
|
||||
|
||||
state.xdg_surface.set_window_geometry(
|
||||
state.surface_state.set_geometry(
|
||||
window_geometry.origin.x,
|
||||
window_geometry.origin.y,
|
||||
window_geometry.size.width,
|
||||
|
@ -578,6 +681,42 @@ impl WaylandWindowStatePtr {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn handle_layersurface_event(&self, event: zwlr_layer_surface_v1::Event) -> bool {
|
||||
match event {
|
||||
zwlr_layer_surface_v1::Event::Configure {
|
||||
width,
|
||||
height,
|
||||
serial,
|
||||
} => {
|
||||
let mut size = if width == 0 || height == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(size(px(width as f32), px(height as f32)))
|
||||
};
|
||||
|
||||
let mut state = self.state.borrow_mut();
|
||||
state.in_progress_configure = Some(InProgressConfigure {
|
||||
size,
|
||||
fullscreen: false,
|
||||
maximized: false,
|
||||
resizing: false,
|
||||
tiling: Tiling::default(),
|
||||
});
|
||||
drop(state);
|
||||
|
||||
// just do the same thing we'd do as an xdg_surface
|
||||
self.handle_xdg_surface_event(xdg_surface::Event::Configure { serial });
|
||||
|
||||
false
|
||||
}
|
||||
zwlr_layer_surface_v1::Event::Closed => {
|
||||
// unlike xdg, we don't have a choice here: the surface is closing.
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::mutable_key_type)]
|
||||
pub fn handle_surface_event(
|
||||
&self,
|
||||
|
@ -840,7 +979,7 @@ impl PlatformWindow for WaylandWindow {
|
|||
let state_ptr = self.0.clone();
|
||||
let dp_size = size.to_device_pixels(self.scale_factor());
|
||||
|
||||
state.xdg_surface.set_window_geometry(
|
||||
state.surface_state.set_geometry(
|
||||
state.bounds.origin.x.0 as i32,
|
||||
state.bounds.origin.y.0 as i32,
|
||||
dp_size.width.0,
|
||||
|
@ -934,12 +1073,16 @@ impl PlatformWindow for WaylandWindow {
|
|||
}
|
||||
|
||||
fn set_title(&mut self, title: &str) {
|
||||
self.borrow().toplevel.set_title(title.to_string());
|
||||
if let Some(toplevel) = self.borrow().surface_state.toplevel() {
|
||||
toplevel.set_title(title.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
fn set_app_id(&mut self, app_id: &str) {
|
||||
let mut state = self.borrow_mut();
|
||||
state.toplevel.set_app_id(app_id.to_owned());
|
||||
if let Some(toplevel) = self.borrow().surface_state.toplevel() {
|
||||
toplevel.set_app_id(app_id.to_owned());
|
||||
}
|
||||
state.app_id = Some(app_id.to_owned());
|
||||
}
|
||||
|
||||
|
@ -950,24 +1093,30 @@ impl PlatformWindow for WaylandWindow {
|
|||
}
|
||||
|
||||
fn minimize(&self) {
|
||||
self.borrow().toplevel.set_minimized();
|
||||
if let Some(toplevel) = self.borrow().surface_state.toplevel() {
|
||||
toplevel.set_minimized();
|
||||
}
|
||||
}
|
||||
|
||||
fn zoom(&self) {
|
||||
let state = self.borrow();
|
||||
if !state.maximized {
|
||||
state.toplevel.set_maximized();
|
||||
} else {
|
||||
state.toplevel.unset_maximized();
|
||||
if let Some(toplevel) = state.surface_state.toplevel() {
|
||||
if !state.maximized {
|
||||
toplevel.set_maximized();
|
||||
} else {
|
||||
toplevel.unset_maximized();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_fullscreen(&self) {
|
||||
let mut state = self.borrow_mut();
|
||||
if !state.fullscreen {
|
||||
state.toplevel.set_fullscreen(None);
|
||||
} else {
|
||||
state.toplevel.unset_fullscreen();
|
||||
let mut state = self.borrow();
|
||||
if let Some(toplevel) = state.surface_state.toplevel() {
|
||||
if !state.fullscreen {
|
||||
toplevel.set_fullscreen(None);
|
||||
} else {
|
||||
toplevel.unset_fullscreen();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1032,27 +1181,33 @@ impl PlatformWindow for WaylandWindow {
|
|||
fn show_window_menu(&self, position: Point<Pixels>) {
|
||||
let state = self.borrow();
|
||||
let serial = state.client.get_serial(SerialKind::MousePress);
|
||||
state.toplevel.show_window_menu(
|
||||
&state.globals.seat,
|
||||
serial,
|
||||
position.x.0 as i32,
|
||||
position.y.0 as i32,
|
||||
);
|
||||
if let Some(toplevel) = state.surface_state.toplevel() {
|
||||
toplevel.show_window_menu(
|
||||
&state.globals.seat,
|
||||
serial,
|
||||
position.x.0 as i32,
|
||||
position.y.0 as i32,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn start_window_move(&self) {
|
||||
let state = self.borrow();
|
||||
let serial = state.client.get_serial(SerialKind::MousePress);
|
||||
state.toplevel._move(&state.globals.seat, serial);
|
||||
if let Some(toplevel) = state.surface_state.toplevel() {
|
||||
toplevel._move(&state.globals.seat, serial);
|
||||
}
|
||||
}
|
||||
|
||||
fn start_window_resize(&self, edge: crate::ResizeEdge) {
|
||||
let state = self.borrow();
|
||||
state.toplevel.resize(
|
||||
&state.globals.seat,
|
||||
state.client.get_serial(SerialKind::MousePress),
|
||||
edge.to_xdg(),
|
||||
)
|
||||
if let Some(toplevel) = state.surface_state.toplevel() {
|
||||
toplevel.resize(
|
||||
&state.globals.seat,
|
||||
state.client.get_serial(SerialKind::MousePress),
|
||||
edge.to_xdg(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn window_decorations(&self) -> Decorations {
|
||||
|
@ -1068,7 +1223,7 @@ impl PlatformWindow for WaylandWindow {
|
|||
fn request_decorations(&self, decorations: WindowDecorations) {
|
||||
let mut state = self.borrow_mut();
|
||||
state.decorations = decorations;
|
||||
if let Some(decoration) = state.decoration.as_ref() {
|
||||
if let Some(decoration) = state.surface_state.decoration() {
|
||||
decoration.set_mode(decorations.to_xdg());
|
||||
update_window(state);
|
||||
}
|
||||
|
|
|
@ -559,7 +559,7 @@ impl MacWindow {
|
|||
}
|
||||
|
||||
let native_window: id = match kind {
|
||||
WindowKind::Normal => msg_send![WINDOW_CLASS, alloc],
|
||||
WindowKind::Normal | WindowKind::Overlay => msg_send![WINDOW_CLASS, alloc],
|
||||
WindowKind::PopUp => {
|
||||
style_mask |= NSWindowStyleMaskNonactivatingPanel;
|
||||
msg_send![PANEL_CLASS, alloc]
|
||||
|
@ -711,7 +711,7 @@ impl MacWindow {
|
|||
native_window.makeFirstResponder_(native_view);
|
||||
|
||||
match kind {
|
||||
WindowKind::Normal => {
|
||||
WindowKind::Normal | WindowKind::Overlay => {
|
||||
native_window.setLevel_(NSNormalWindowLevel);
|
||||
native_window.setAcceptsMouseMovedEvents_(YES);
|
||||
}
|
||||
|
|
|
@ -32,20 +32,18 @@ impl TabHandles {
|
|||
self.handles.clear();
|
||||
}
|
||||
|
||||
fn current_index(&self, focused_id: Option<&FocusId>) -> usize {
|
||||
self.handles
|
||||
.iter()
|
||||
.position(|h| Some(&h.id) == focused_id)
|
||||
.unwrap_or_default()
|
||||
fn current_index(&self, focused_id: Option<&FocusId>) -> Option<usize> {
|
||||
self.handles.iter().position(|h| Some(&h.id) == focused_id)
|
||||
}
|
||||
|
||||
pub(crate) fn next(&self, focused_id: Option<&FocusId>) -> Option<FocusHandle> {
|
||||
let ix = self.current_index(focused_id);
|
||||
|
||||
let mut next_ix = ix + 1;
|
||||
if next_ix + 1 > self.handles.len() {
|
||||
next_ix = 0;
|
||||
}
|
||||
let next_ix = self
|
||||
.current_index(focused_id)
|
||||
.and_then(|ix| {
|
||||
let next_ix = ix + 1;
|
||||
(next_ix < self.handles.len()).then_some(next_ix)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
if let Some(next_handle) = self.handles.get(next_ix) {
|
||||
Some(next_handle.clone())
|
||||
|
@ -55,7 +53,7 @@ impl TabHandles {
|
|||
}
|
||||
|
||||
pub(crate) fn prev(&self, focused_id: Option<&FocusId>) -> Option<FocusHandle> {
|
||||
let ix = self.current_index(focused_id);
|
||||
let ix = self.current_index(focused_id).unwrap_or_default();
|
||||
let prev_ix;
|
||||
if ix == 0 {
|
||||
prev_ix = self.handles.len().saturating_sub(1);
|
||||
|
@ -108,8 +106,14 @@ mod tests {
|
|||
]
|
||||
);
|
||||
|
||||
// next
|
||||
assert_eq!(tab.next(None), Some(tab.handles[1].clone()));
|
||||
// Select first tab index if no handle is currently focused.
|
||||
assert_eq!(tab.next(None), Some(tab.handles[0].clone()));
|
||||
// Select last tab index if no handle is currently focused.
|
||||
assert_eq!(
|
||||
tab.prev(None),
|
||||
Some(tab.handles[tab.handles.len() - 1].clone())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
tab.next(Some(&tab.handles[0].id)),
|
||||
Some(tab.handles[1].clone())
|
||||
|
|
|
@ -4,6 +4,7 @@ pub mod github;
|
|||
pub use anyhow::{Result, anyhow};
|
||||
pub use async_body::{AsyncBody, Inner};
|
||||
use derive_more::Deref;
|
||||
use http::HeaderValue;
|
||||
pub use http::{self, Method, Request, Response, StatusCode, Uri};
|
||||
|
||||
use futures::future::BoxFuture;
|
||||
|
@ -39,6 +40,8 @@ impl HttpRequestExt for http::request::Builder {
|
|||
pub trait HttpClient: 'static + Send + Sync {
|
||||
fn type_name(&self) -> &'static str;
|
||||
|
||||
fn user_agent(&self) -> Option<&HeaderValue>;
|
||||
|
||||
fn send(
|
||||
&self,
|
||||
req: http::Request<AsyncBody>,
|
||||
|
@ -118,6 +121,10 @@ impl HttpClient for HttpClientWithProxy {
|
|||
self.client.send(req)
|
||||
}
|
||||
|
||||
fn user_agent(&self) -> Option<&HeaderValue> {
|
||||
self.client.user_agent()
|
||||
}
|
||||
|
||||
fn proxy(&self) -> Option<&Url> {
|
||||
self.proxy.as_ref()
|
||||
}
|
||||
|
@ -135,6 +142,10 @@ impl HttpClient for Arc<HttpClientWithProxy> {
|
|||
self.client.send(req)
|
||||
}
|
||||
|
||||
fn user_agent(&self) -> Option<&HeaderValue> {
|
||||
self.client.user_agent()
|
||||
}
|
||||
|
||||
fn proxy(&self) -> Option<&Url> {
|
||||
self.proxy.as_ref()
|
||||
}
|
||||
|
@ -250,6 +261,10 @@ impl HttpClient for Arc<HttpClientWithUrl> {
|
|||
self.client.send(req)
|
||||
}
|
||||
|
||||
fn user_agent(&self) -> Option<&HeaderValue> {
|
||||
self.client.user_agent()
|
||||
}
|
||||
|
||||
fn proxy(&self) -> Option<&Url> {
|
||||
self.client.proxy.as_ref()
|
||||
}
|
||||
|
@ -267,6 +282,10 @@ impl HttpClient for HttpClientWithUrl {
|
|||
self.client.send(req)
|
||||
}
|
||||
|
||||
fn user_agent(&self) -> Option<&HeaderValue> {
|
||||
self.client.user_agent()
|
||||
}
|
||||
|
||||
fn proxy(&self) -> Option<&Url> {
|
||||
self.client.proxy.as_ref()
|
||||
}
|
||||
|
@ -314,6 +333,10 @@ impl HttpClient for BlockedHttpClient {
|
|||
})
|
||||
}
|
||||
|
||||
fn user_agent(&self) -> Option<&HeaderValue> {
|
||||
None
|
||||
}
|
||||
|
||||
fn proxy(&self) -> Option<&Url> {
|
||||
None
|
||||
}
|
||||
|
@ -334,6 +357,7 @@ type FakeHttpHandler = Box<
|
|||
#[cfg(feature = "test-support")]
|
||||
pub struct FakeHttpClient {
|
||||
handler: FakeHttpHandler,
|
||||
user_agent: HeaderValue,
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
|
@ -348,6 +372,7 @@ impl FakeHttpClient {
|
|||
client: HttpClientWithProxy {
|
||||
client: Arc::new(Self {
|
||||
handler: Box::new(move |req| Box::pin(handler(req))),
|
||||
user_agent: HeaderValue::from_static(type_name::<Self>()),
|
||||
}),
|
||||
proxy: None,
|
||||
},
|
||||
|
@ -390,6 +415,10 @@ impl HttpClient for FakeHttpClient {
|
|||
future
|
||||
}
|
||||
|
||||
fn user_agent(&self) -> Option<&HeaderValue> {
|
||||
Some(&self.user_agent)
|
||||
}
|
||||
|
||||
fn proxy(&self) -> Option<&Url> {
|
||||
None
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ doctest = false
|
|||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
client.workspace = true
|
||||
cloud_llm_client.workspace = true
|
||||
copilot.workspace = true
|
||||
editor.workspace = true
|
||||
feature_flags.workspace = true
|
||||
|
@ -32,7 +33,6 @@ ui.workspace = true
|
|||
workspace-hack.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
zed_llm_client.workspace = true
|
||||
zeta.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use anyhow::Result;
|
||||
use client::{DisableAiSettings, UserStore, zed_urls};
|
||||
use cloud_llm_client::UsageLimit;
|
||||
use copilot::{Copilot, Status};
|
||||
use editor::{
|
||||
Editor, SelectionEffects,
|
||||
|
@ -34,7 +35,6 @@ use workspace::{
|
|||
notifications::NotificationId,
|
||||
};
|
||||
use zed_actions::OpenBrowser;
|
||||
use zed_llm_client::UsageLimit;
|
||||
use zeta::RateCompletions;
|
||||
|
||||
actions!(
|
||||
|
|
|
@ -166,7 +166,6 @@ pub struct CachedLspAdapter {
|
|||
pub reinstall_attempt_count: AtomicU64,
|
||||
cached_binary: futures::lock::Mutex<Option<LanguageServerBinary>>,
|
||||
manifest_name: OnceLock<Option<ManifestName>>,
|
||||
attach_kind: OnceLock<Attach>,
|
||||
}
|
||||
|
||||
impl Debug for CachedLspAdapter {
|
||||
|
@ -202,7 +201,6 @@ impl CachedLspAdapter {
|
|||
adapter,
|
||||
cached_binary: Default::default(),
|
||||
reinstall_attempt_count: AtomicU64::new(0),
|
||||
attach_kind: Default::default(),
|
||||
manifest_name: Default::default(),
|
||||
})
|
||||
}
|
||||
|
@ -288,29 +286,6 @@ impl CachedLspAdapter {
|
|||
.get_or_init(|| self.adapter.manifest_name())
|
||||
.clone()
|
||||
}
|
||||
pub fn attach_kind(&self) -> Attach {
|
||||
*self.attach_kind.get_or_init(|| self.adapter.attach_kind())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Attach {
|
||||
/// Create a single language server instance per subproject root.
|
||||
InstancePerRoot,
|
||||
/// Use one shared language server instance for all subprojects within a project.
|
||||
Shared,
|
||||
}
|
||||
|
||||
impl Attach {
|
||||
pub fn root_path(
|
||||
&self,
|
||||
root_subproject_path: (WorktreeId, Arc<Path>),
|
||||
) -> (WorktreeId, Arc<Path>) {
|
||||
match self {
|
||||
Attach::InstancePerRoot => root_subproject_path,
|
||||
Attach::Shared => (root_subproject_path.0, Arc::from(Path::new(""))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Determines what gets sent out as a workspace folders content
|
||||
|
@ -611,10 +586,6 @@ pub trait LspAdapter: 'static + Send + Sync {
|
|||
Ok(original)
|
||||
}
|
||||
|
||||
fn attach_kind(&self) -> Attach {
|
||||
Attach::Shared
|
||||
}
|
||||
|
||||
/// Determines whether a language server supports workspace folders.
|
||||
///
|
||||
/// And does not trip over itself in the process.
|
||||
|
|
|
@ -20,6 +20,7 @@ anthropic = { workspace = true, features = ["schemars"] }
|
|||
anyhow.workspace = true
|
||||
base64.workspace = true
|
||||
client.workspace = true
|
||||
cloud_llm_client.workspace = true
|
||||
collections.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
|
@ -37,7 +38,6 @@ telemetry_events.workspace = true
|
|||
thiserror.workspace = true
|
||||
util.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
zed_llm_client.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
|
|
|
@ -11,6 +11,7 @@ pub mod fake_provider;
|
|||
use anthropic::{AnthropicError, parse_prompt_too_long};
|
||||
use anyhow::{Result, anyhow};
|
||||
use client::Client;
|
||||
use cloud_llm_client::{CompletionMode, CompletionRequestStatus};
|
||||
use futures::FutureExt;
|
||||
use futures::{StreamExt, future::BoxFuture, stream::BoxStream};
|
||||
use gpui::{AnyElement, AnyView, App, AsyncApp, SharedString, Task, Window};
|
||||
|
@ -26,7 +27,6 @@ use std::time::Duration;
|
|||
use std::{fmt, io};
|
||||
use thiserror::Error;
|
||||
use util::serde::is_default;
|
||||
use zed_llm_client::{CompletionMode, CompletionRequestStatus};
|
||||
|
||||
pub use crate::model::*;
|
||||
pub use crate::rate_limiter::*;
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
use std::io::{Cursor, Write};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::role::Role;
|
||||
use crate::{LanguageModelToolUse, LanguageModelToolUseId};
|
||||
use anyhow::Result;
|
||||
use base64::write::EncoderWriter;
|
||||
use cloud_llm_client::{CompletionIntent, CompletionMode};
|
||||
use gpui::{
|
||||
App, AppContext as _, DevicePixels, Image, ImageFormat, ObjectFit, SharedString, Size, Task,
|
||||
point, px, size,
|
||||
|
@ -12,7 +11,9 @@ use gpui::{
|
|||
use image::codecs::png::PngEncoder;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use util::ResultExt;
|
||||
use zed_llm_client::{CompletionIntent, CompletionMode};
|
||||
|
||||
use crate::role::Role;
|
||||
use crate::{LanguageModelToolUse, LanguageModelToolUseId};
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
|
||||
pub struct LanguageModelImage {
|
||||
|
|
|
@ -16,18 +16,17 @@ ai_onboarding.workspace = true
|
|||
anthropic = { workspace = true, features = ["schemars"] }
|
||||
anyhow.workspace = true
|
||||
aws-config = { workspace = true, features = ["behavior-version-latest"] }
|
||||
aws-credential-types = { workspace = true, features = [
|
||||
"hardcoded-credentials",
|
||||
] }
|
||||
aws-credential-types = { workspace = true, features = ["hardcoded-credentials"] }
|
||||
aws_http_client.workspace = true
|
||||
bedrock.workspace = true
|
||||
chrono.workspace = true
|
||||
client.workspace = true
|
||||
cloud_llm_client.workspace = true
|
||||
collections.workspace = true
|
||||
component.workspace = true
|
||||
credentials_provider.workspace = true
|
||||
convert_case.workspace = true
|
||||
copilot.workspace = true
|
||||
credentials_provider.workspace = true
|
||||
deepseek = { workspace = true, features = ["schemars"] }
|
||||
editor.workspace = true
|
||||
futures.workspace = true
|
||||
|
@ -35,6 +34,7 @@ google_ai = { workspace = true, features = ["schemars"] }
|
|||
gpui.workspace = true
|
||||
gpui_tokio.workspace = true
|
||||
http_client.workspace = true
|
||||
language.workspace = true
|
||||
language_model.workspace = true
|
||||
lmstudio = { workspace = true, features = ["schemars"] }
|
||||
log.workspace = true
|
||||
|
@ -43,8 +43,6 @@ mistral = { workspace = true, features = ["schemars"] }
|
|||
ollama = { workspace = true, features = ["schemars"] }
|
||||
open_ai = { workspace = true, features = ["schemars"] }
|
||||
open_router = { workspace = true, features = ["schemars"] }
|
||||
vercel = { workspace = true, features = ["schemars"] }
|
||||
x_ai = { workspace = true, features = ["schemars"] }
|
||||
partial-json-fixer.workspace = true
|
||||
proto.workspace = true
|
||||
release_channel.workspace = true
|
||||
|
@ -61,9 +59,9 @@ tokio = { workspace = true, features = ["rt", "rt-multi-thread"] }
|
|||
ui.workspace = true
|
||||
ui_input.workspace = true
|
||||
util.workspace = true
|
||||
vercel = { workspace = true, features = ["schemars"] }
|
||||
workspace-hack.workspace = true
|
||||
zed_llm_client.workspace = true
|
||||
language.workspace = true
|
||||
x_ai = { workspace = true, features = ["schemars"] }
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
|
|
|
@ -3,6 +3,13 @@ use anthropic::AnthropicModelMode;
|
|||
use anyhow::{Context as _, Result, anyhow};
|
||||
use chrono::{DateTime, Utc};
|
||||
use client::{Client, ModelRequestUsage, UserStore, zed_urls};
|
||||
use cloud_llm_client::{
|
||||
CLIENT_SUPPORTS_STATUS_MESSAGES_HEADER_NAME, CURRENT_PLAN_HEADER_NAME, CompletionBody,
|
||||
CompletionEvent, CompletionRequestStatus, CountTokensBody, CountTokensResponse,
|
||||
EXPIRED_LLM_TOKEN_HEADER_NAME, ListModelsResponse, MODEL_REQUESTS_RESOURCE_HEADER_VALUE,
|
||||
SERVER_SUPPORTS_STATUS_MESSAGES_HEADER_NAME, SUBSCRIPTION_LIMIT_RESOURCE_HEADER_NAME,
|
||||
TOOL_USE_LIMIT_REACHED_HEADER_NAME, ZED_VERSION_HEADER_NAME,
|
||||
};
|
||||
use futures::{
|
||||
AsyncBufReadExt, FutureExt, Stream, StreamExt, future::BoxFuture, stream::BoxStream,
|
||||
};
|
||||
|
@ -33,13 +40,6 @@ use std::time::Duration;
|
|||
use thiserror::Error;
|
||||
use ui::{TintColor, prelude::*};
|
||||
use util::{ResultExt as _, maybe};
|
||||
use zed_llm_client::{
|
||||
CLIENT_SUPPORTS_STATUS_MESSAGES_HEADER_NAME, CURRENT_PLAN_HEADER_NAME, CompletionBody,
|
||||
CompletionRequestStatus, CountTokensBody, CountTokensResponse, EXPIRED_LLM_TOKEN_HEADER_NAME,
|
||||
ListModelsResponse, MODEL_REQUESTS_RESOURCE_HEADER_VALUE,
|
||||
SERVER_SUPPORTS_STATUS_MESSAGES_HEADER_NAME, SUBSCRIPTION_LIMIT_RESOURCE_HEADER_NAME,
|
||||
TOOL_USE_LIMIT_REACHED_HEADER_NAME, ZED_VERSION_HEADER_NAME,
|
||||
};
|
||||
|
||||
use crate::provider::anthropic::{AnthropicEventMapper, count_anthropic_tokens, into_anthropic};
|
||||
use crate::provider::google::{GoogleEventMapper, into_google};
|
||||
|
@ -120,10 +120,10 @@ pub struct State {
|
|||
user_store: Entity<UserStore>,
|
||||
status: client::Status,
|
||||
accept_terms_of_service_task: Option<Task<Result<()>>>,
|
||||
models: Vec<Arc<zed_llm_client::LanguageModel>>,
|
||||
default_model: Option<Arc<zed_llm_client::LanguageModel>>,
|
||||
default_fast_model: Option<Arc<zed_llm_client::LanguageModel>>,
|
||||
recommended_models: Vec<Arc<zed_llm_client::LanguageModel>>,
|
||||
models: Vec<Arc<cloud_llm_client::LanguageModel>>,
|
||||
default_model: Option<Arc<cloud_llm_client::LanguageModel>>,
|
||||
default_fast_model: Option<Arc<cloud_llm_client::LanguageModel>>,
|
||||
recommended_models: Vec<Arc<cloud_llm_client::LanguageModel>>,
|
||||
_fetch_models_task: Task<()>,
|
||||
_settings_subscription: Subscription,
|
||||
_llm_token_subscription: Subscription,
|
||||
|
@ -238,8 +238,8 @@ impl State {
|
|||
// Right now we represent thinking variants of models as separate models on the client,
|
||||
// so we need to insert variants for any model that supports thinking.
|
||||
if model.supports_thinking {
|
||||
models.push(Arc::new(zed_llm_client::LanguageModel {
|
||||
id: zed_llm_client::LanguageModelId(format!("{}-thinking", model.id).into()),
|
||||
models.push(Arc::new(cloud_llm_client::LanguageModel {
|
||||
id: cloud_llm_client::LanguageModelId(format!("{}-thinking", model.id).into()),
|
||||
display_name: format!("{} Thinking", model.display_name),
|
||||
..model
|
||||
}));
|
||||
|
@ -328,7 +328,7 @@ impl CloudLanguageModelProvider {
|
|||
|
||||
fn create_language_model(
|
||||
&self,
|
||||
model: Arc<zed_llm_client::LanguageModel>,
|
||||
model: Arc<cloud_llm_client::LanguageModel>,
|
||||
llm_api_token: LlmApiToken,
|
||||
) -> Arc<dyn LanguageModel> {
|
||||
Arc::new(CloudLanguageModel {
|
||||
|
@ -518,7 +518,7 @@ fn render_accept_terms(
|
|||
|
||||
pub struct CloudLanguageModel {
|
||||
id: LanguageModelId,
|
||||
model: Arc<zed_llm_client::LanguageModel>,
|
||||
model: Arc<cloud_llm_client::LanguageModel>,
|
||||
llm_api_token: LlmApiToken,
|
||||
client: Arc<Client>,
|
||||
request_limiter: RateLimiter,
|
||||
|
@ -611,12 +611,12 @@ impl CloudLanguageModel {
|
|||
.headers()
|
||||
.get(CURRENT_PLAN_HEADER_NAME)
|
||||
.and_then(|plan| plan.to_str().ok())
|
||||
.and_then(|plan| zed_llm_client::Plan::from_str(plan).ok())
|
||||
.and_then(|plan| cloud_llm_client::Plan::from_str(plan).ok())
|
||||
{
|
||||
let plan = match plan {
|
||||
zed_llm_client::Plan::ZedFree => Plan::Free,
|
||||
zed_llm_client::Plan::ZedPro => Plan::ZedPro,
|
||||
zed_llm_client::Plan::ZedProTrial => Plan::ZedProTrial,
|
||||
cloud_llm_client::Plan::ZedFree => Plan::Free,
|
||||
cloud_llm_client::Plan::ZedPro => Plan::ZedPro,
|
||||
cloud_llm_client::Plan::ZedProTrial => Plan::ZedProTrial,
|
||||
};
|
||||
return Err(anyhow!(ModelRequestLimitReachedError { plan }));
|
||||
}
|
||||
|
@ -729,7 +729,7 @@ impl LanguageModel for CloudLanguageModel {
|
|||
}
|
||||
|
||||
fn upstream_provider_id(&self) -> LanguageModelProviderId {
|
||||
use zed_llm_client::LanguageModelProvider::*;
|
||||
use cloud_llm_client::LanguageModelProvider::*;
|
||||
match self.model.provider {
|
||||
Anthropic => language_model::ANTHROPIC_PROVIDER_ID,
|
||||
OpenAi => language_model::OPEN_AI_PROVIDER_ID,
|
||||
|
@ -738,7 +738,7 @@ impl LanguageModel for CloudLanguageModel {
|
|||
}
|
||||
|
||||
fn upstream_provider_name(&self) -> LanguageModelProviderName {
|
||||
use zed_llm_client::LanguageModelProvider::*;
|
||||
use cloud_llm_client::LanguageModelProvider::*;
|
||||
match self.model.provider {
|
||||
Anthropic => language_model::ANTHROPIC_PROVIDER_NAME,
|
||||
OpenAi => language_model::OPEN_AI_PROVIDER_NAME,
|
||||
|
@ -772,11 +772,11 @@ impl LanguageModel for CloudLanguageModel {
|
|||
|
||||
fn tool_input_format(&self) -> LanguageModelToolSchemaFormat {
|
||||
match self.model.provider {
|
||||
zed_llm_client::LanguageModelProvider::Anthropic
|
||||
| zed_llm_client::LanguageModelProvider::OpenAi => {
|
||||
cloud_llm_client::LanguageModelProvider::Anthropic
|
||||
| cloud_llm_client::LanguageModelProvider::OpenAi => {
|
||||
LanguageModelToolSchemaFormat::JsonSchema
|
||||
}
|
||||
zed_llm_client::LanguageModelProvider::Google => {
|
||||
cloud_llm_client::LanguageModelProvider::Google => {
|
||||
LanguageModelToolSchemaFormat::JsonSchemaSubset
|
||||
}
|
||||
}
|
||||
|
@ -795,15 +795,15 @@ impl LanguageModel for CloudLanguageModel {
|
|||
|
||||
fn cache_configuration(&self) -> Option<LanguageModelCacheConfiguration> {
|
||||
match &self.model.provider {
|
||||
zed_llm_client::LanguageModelProvider::Anthropic => {
|
||||
cloud_llm_client::LanguageModelProvider::Anthropic => {
|
||||
Some(LanguageModelCacheConfiguration {
|
||||
min_total_token: 2_048,
|
||||
should_speculate: true,
|
||||
max_cache_anchors: 4,
|
||||
})
|
||||
}
|
||||
zed_llm_client::LanguageModelProvider::OpenAi
|
||||
| zed_llm_client::LanguageModelProvider::Google => None,
|
||||
cloud_llm_client::LanguageModelProvider::OpenAi
|
||||
| cloud_llm_client::LanguageModelProvider::Google => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -813,15 +813,17 @@ impl LanguageModel for CloudLanguageModel {
|
|||
cx: &App,
|
||||
) -> BoxFuture<'static, Result<u64>> {
|
||||
match self.model.provider {
|
||||
zed_llm_client::LanguageModelProvider::Anthropic => count_anthropic_tokens(request, cx),
|
||||
zed_llm_client::LanguageModelProvider::OpenAi => {
|
||||
cloud_llm_client::LanguageModelProvider::Anthropic => {
|
||||
count_anthropic_tokens(request, cx)
|
||||
}
|
||||
cloud_llm_client::LanguageModelProvider::OpenAi => {
|
||||
let model = match open_ai::Model::from_id(&self.model.id.0) {
|
||||
Ok(model) => model,
|
||||
Err(err) => return async move { Err(anyhow!(err)) }.boxed(),
|
||||
};
|
||||
count_open_ai_tokens(request, model, cx)
|
||||
}
|
||||
zed_llm_client::LanguageModelProvider::Google => {
|
||||
cloud_llm_client::LanguageModelProvider::Google => {
|
||||
let client = self.client.clone();
|
||||
let llm_api_token = self.llm_api_token.clone();
|
||||
let model_id = self.model.id.to_string();
|
||||
|
@ -832,7 +834,7 @@ impl LanguageModel for CloudLanguageModel {
|
|||
let token = llm_api_token.acquire(&client).await?;
|
||||
|
||||
let request_body = CountTokensBody {
|
||||
provider: zed_llm_client::LanguageModelProvider::Google,
|
||||
provider: cloud_llm_client::LanguageModelProvider::Google,
|
||||
model: model_id,
|
||||
provider_request: serde_json::to_value(&google_ai::CountTokensRequest {
|
||||
generate_content_request,
|
||||
|
@ -893,7 +895,7 @@ impl LanguageModel for CloudLanguageModel {
|
|||
let app_version = cx.update(|cx| AppVersion::global(cx)).ok();
|
||||
let thinking_allowed = request.thinking_allowed;
|
||||
match self.model.provider {
|
||||
zed_llm_client::LanguageModelProvider::Anthropic => {
|
||||
cloud_llm_client::LanguageModelProvider::Anthropic => {
|
||||
let request = into_anthropic(
|
||||
request,
|
||||
self.model.id.to_string(),
|
||||
|
@ -924,7 +926,7 @@ impl LanguageModel for CloudLanguageModel {
|
|||
prompt_id,
|
||||
intent,
|
||||
mode,
|
||||
provider: zed_llm_client::LanguageModelProvider::Anthropic,
|
||||
provider: cloud_llm_client::LanguageModelProvider::Anthropic,
|
||||
model: request.model.clone(),
|
||||
provider_request: serde_json::to_value(&request)
|
||||
.map_err(|e| anyhow!(e))?,
|
||||
|
@ -948,7 +950,7 @@ impl LanguageModel for CloudLanguageModel {
|
|||
});
|
||||
async move { Ok(future.await?.boxed()) }.boxed()
|
||||
}
|
||||
zed_llm_client::LanguageModelProvider::OpenAi => {
|
||||
cloud_llm_client::LanguageModelProvider::OpenAi => {
|
||||
let client = self.client.clone();
|
||||
let model = match open_ai::Model::from_id(&self.model.id.0) {
|
||||
Ok(model) => model,
|
||||
|
@ -976,7 +978,7 @@ impl LanguageModel for CloudLanguageModel {
|
|||
prompt_id,
|
||||
intent,
|
||||
mode,
|
||||
provider: zed_llm_client::LanguageModelProvider::OpenAi,
|
||||
provider: cloud_llm_client::LanguageModelProvider::OpenAi,
|
||||
model: request.model.clone(),
|
||||
provider_request: serde_json::to_value(&request)
|
||||
.map_err(|e| anyhow!(e))?,
|
||||
|
@ -996,7 +998,7 @@ impl LanguageModel for CloudLanguageModel {
|
|||
});
|
||||
async move { Ok(future.await?.boxed()) }.boxed()
|
||||
}
|
||||
zed_llm_client::LanguageModelProvider::Google => {
|
||||
cloud_llm_client::LanguageModelProvider::Google => {
|
||||
let client = self.client.clone();
|
||||
let request =
|
||||
into_google(request, self.model.id.to_string(), GoogleModelMode::Default);
|
||||
|
@ -1016,7 +1018,7 @@ impl LanguageModel for CloudLanguageModel {
|
|||
prompt_id,
|
||||
intent,
|
||||
mode,
|
||||
provider: zed_llm_client::LanguageModelProvider::Google,
|
||||
provider: cloud_llm_client::LanguageModelProvider::Google,
|
||||
model: request.model.model_id.clone(),
|
||||
provider_request: serde_json::to_value(&request)
|
||||
.map_err(|e| anyhow!(e))?,
|
||||
|
@ -1040,15 +1042,8 @@ impl LanguageModel for CloudLanguageModel {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CloudCompletionEvent<T> {
|
||||
Status(CompletionRequestStatus),
|
||||
Event(T),
|
||||
}
|
||||
|
||||
fn map_cloud_completion_events<T, F>(
|
||||
stream: Pin<Box<dyn Stream<Item = Result<CloudCompletionEvent<T>>> + Send>>,
|
||||
stream: Pin<Box<dyn Stream<Item = Result<CompletionEvent<T>>> + Send>>,
|
||||
mut map_callback: F,
|
||||
) -> BoxStream<'static, Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>
|
||||
where
|
||||
|
@ -1063,10 +1058,10 @@ where
|
|||
Err(error) => {
|
||||
vec![Err(LanguageModelCompletionError::from(error))]
|
||||
}
|
||||
Ok(CloudCompletionEvent::Status(event)) => {
|
||||
Ok(CompletionEvent::Status(event)) => {
|
||||
vec![Ok(LanguageModelCompletionEvent::StatusUpdate(event))]
|
||||
}
|
||||
Ok(CloudCompletionEvent::Event(event)) => map_callback(event),
|
||||
Ok(CompletionEvent::Event(event)) => map_callback(event),
|
||||
})
|
||||
})
|
||||
.boxed()
|
||||
|
@ -1074,9 +1069,9 @@ where
|
|||
|
||||
fn usage_updated_event<T>(
|
||||
usage: Option<ModelRequestUsage>,
|
||||
) -> impl Stream<Item = Result<CloudCompletionEvent<T>>> {
|
||||
) -> impl Stream<Item = Result<CompletionEvent<T>>> {
|
||||
futures::stream::iter(usage.map(|usage| {
|
||||
Ok(CloudCompletionEvent::Status(
|
||||
Ok(CompletionEvent::Status(
|
||||
CompletionRequestStatus::UsageUpdated {
|
||||
amount: usage.amount as usize,
|
||||
limit: usage.limit,
|
||||
|
@ -1087,9 +1082,9 @@ fn usage_updated_event<T>(
|
|||
|
||||
fn tool_use_limit_reached_event<T>(
|
||||
tool_use_limit_reached: bool,
|
||||
) -> impl Stream<Item = Result<CloudCompletionEvent<T>>> {
|
||||
) -> impl Stream<Item = Result<CompletionEvent<T>>> {
|
||||
futures::stream::iter(tool_use_limit_reached.then(|| {
|
||||
Ok(CloudCompletionEvent::Status(
|
||||
Ok(CompletionEvent::Status(
|
||||
CompletionRequestStatus::ToolUseLimitReached,
|
||||
))
|
||||
}))
|
||||
|
@ -1098,7 +1093,7 @@ fn tool_use_limit_reached_event<T>(
|
|||
fn response_lines<T: DeserializeOwned>(
|
||||
response: Response<AsyncBody>,
|
||||
includes_status_messages: bool,
|
||||
) -> impl Stream<Item = Result<CloudCompletionEvent<T>>> {
|
||||
) -> impl Stream<Item = Result<CompletionEvent<T>>> {
|
||||
futures::stream::try_unfold(
|
||||
(String::new(), BufReader::new(response.into_body())),
|
||||
move |(mut line, mut body)| async move {
|
||||
|
@ -1106,9 +1101,9 @@ fn response_lines<T: DeserializeOwned>(
|
|||
Ok(0) => Ok(None),
|
||||
Ok(_) => {
|
||||
let event = if includes_status_messages {
|
||||
serde_json::from_str::<CloudCompletionEvent<T>>(&line)?
|
||||
serde_json::from_str::<CompletionEvent<T>>(&line)?
|
||||
} else {
|
||||
CloudCompletionEvent::Event(serde_json::from_str::<T>(&line)?)
|
||||
CompletionEvent::Event(serde_json::from_str::<T>(&line)?)
|
||||
};
|
||||
|
||||
line.clear();
|
||||
|
|
|
@ -3,6 +3,7 @@ use std::str::FromStr as _;
|
|||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Result, anyhow};
|
||||
use cloud_llm_client::CompletionIntent;
|
||||
use collections::HashMap;
|
||||
use copilot::copilot_chat::{
|
||||
ChatMessage, ChatMessageContent, ChatMessagePart, CopilotChat, ImageUrl,
|
||||
|
@ -30,7 +31,6 @@ use settings::SettingsStore;
|
|||
use std::time::Duration;
|
||||
use ui::prelude::*;
|
||||
use util::debug_panic;
|
||||
use zed_llm_client::CompletionIntent;
|
||||
|
||||
use super::anthropic::count_anthropic_tokens;
|
||||
use super::google::count_google_tokens;
|
||||
|
|
|
@ -1625,6 +1625,10 @@ impl LspAdapter for BasedPyrightLspAdapter {
|
|||
fn manifest_name(&self) -> Option<ManifestName> {
|
||||
Some(SharedString::new_static("pyproject.toml").into())
|
||||
}
|
||||
|
||||
fn workspace_folders_content(&self) -> WorkspaceFoldersContent {
|
||||
WorkspaceFoldersContent::WorktreeRoot
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
@ -18,12 +18,15 @@ default = []
|
|||
anyhow.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
db.workspace = true
|
||||
editor.workspace = true
|
||||
feature_flags.workspace = true
|
||||
fs.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
project.workspace = true
|
||||
settings.workspace = true
|
||||
theme.workspace = true
|
||||
ui.workspace = true
|
||||
workspace.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
|
|
287
crates/onboarding/src/editing_page.rs
Normal file
287
crates/onboarding/src/editing_page.rs
Normal file
|
@ -0,0 +1,287 @@
|
|||
use editor::{EditorSettings, ShowMinimap};
|
||||
use fs::Fs;
|
||||
use gpui::{App, IntoElement, Pixels, Window};
|
||||
use language::language_settings::AllLanguageSettings;
|
||||
use project::project_settings::ProjectSettings;
|
||||
use settings::{Settings as _, update_settings_file};
|
||||
use theme::{FontFamilyCache, FontFamilyName, ThemeSettings};
|
||||
use ui::{
|
||||
ContextMenu, DropdownMenu, IconButton, Label, LabelCommon, LabelSize, NumericStepper,
|
||||
ParentElement, SharedString, Styled, SwitchColor, SwitchField, ToggleButtonGroup,
|
||||
ToggleButtonGroupStyle, ToggleButtonSimple, ToggleState, div, h_flex, px, v_flex,
|
||||
};
|
||||
|
||||
fn read_show_mini_map(cx: &App) -> ShowMinimap {
|
||||
editor::EditorSettings::get_global(cx).minimap.show
|
||||
}
|
||||
|
||||
fn write_show_mini_map(show: ShowMinimap, cx: &mut App) {
|
||||
let fs = <dyn Fs>::global(cx);
|
||||
|
||||
update_settings_file::<EditorSettings>(fs, cx, move |editor_settings, _| {
|
||||
editor_settings.minimap.get_or_insert_default().show = Some(show);
|
||||
});
|
||||
}
|
||||
|
||||
fn read_inlay_hints(cx: &App) -> bool {
|
||||
AllLanguageSettings::get_global(cx)
|
||||
.defaults
|
||||
.inlay_hints
|
||||
.enabled
|
||||
}
|
||||
|
||||
fn write_inlay_hints(enabled: bool, cx: &mut App) {
|
||||
let fs = <dyn Fs>::global(cx);
|
||||
|
||||
update_settings_file::<AllLanguageSettings>(fs, cx, move |all_language_settings, cx| {
|
||||
all_language_settings
|
||||
.defaults
|
||||
.inlay_hints
|
||||
.get_or_insert_with(|| {
|
||||
AllLanguageSettings::get_global(cx)
|
||||
.clone()
|
||||
.defaults
|
||||
.inlay_hints
|
||||
})
|
||||
.enabled = enabled;
|
||||
});
|
||||
}
|
||||
|
||||
fn read_git_blame(cx: &App) -> bool {
|
||||
ProjectSettings::get_global(cx).git.inline_blame_enabled()
|
||||
}
|
||||
|
||||
fn set_git_blame(enabled: bool, cx: &mut App) {
|
||||
let fs = <dyn Fs>::global(cx);
|
||||
|
||||
update_settings_file::<ProjectSettings>(fs, cx, move |project_settings, _| {
|
||||
project_settings
|
||||
.git
|
||||
.inline_blame
|
||||
.get_or_insert_default()
|
||||
.enabled = enabled;
|
||||
});
|
||||
}
|
||||
|
||||
fn write_ui_font_family(font: SharedString, cx: &mut App) {
|
||||
let fs = <dyn Fs>::global(cx);
|
||||
|
||||
update_settings_file::<ThemeSettings>(fs, cx, move |theme_settings, _| {
|
||||
theme_settings.ui_font_family = Some(FontFamilyName(font.into()));
|
||||
});
|
||||
}
|
||||
|
||||
fn write_ui_font_size(size: Pixels, cx: &mut App) {
|
||||
let fs = <dyn Fs>::global(cx);
|
||||
|
||||
update_settings_file::<ThemeSettings>(fs, cx, move |theme_settings, _| {
|
||||
theme_settings.ui_font_size = Some(size.into());
|
||||
});
|
||||
}
|
||||
|
||||
fn write_buffer_font_size(size: Pixels, cx: &mut App) {
|
||||
let fs = <dyn Fs>::global(cx);
|
||||
|
||||
update_settings_file::<ThemeSettings>(fs, cx, move |theme_settings, _| {
|
||||
theme_settings.buffer_font_size = Some(size.into());
|
||||
});
|
||||
}
|
||||
|
||||
fn write_buffer_font_family(font_family: SharedString, cx: &mut App) {
|
||||
let fs = <dyn Fs>::global(cx);
|
||||
|
||||
update_settings_file::<ThemeSettings>(fs, cx, move |theme_settings, _| {
|
||||
theme_settings.buffer_font_family = Some(FontFamilyName(font_family.into()));
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn render_editing_page(window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let theme_settings = ThemeSettings::get_global(cx);
|
||||
let ui_font_size = theme_settings.ui_font_size(cx);
|
||||
let font_family = theme_settings.buffer_font.family.clone();
|
||||
let buffer_font_size = theme_settings.buffer_font_size(cx);
|
||||
|
||||
v_flex()
|
||||
.gap_4()
|
||||
.child(Label::new("Import Settings").size(LabelSize::Large))
|
||||
.child(
|
||||
Label::new("Automatically pull your settings from other editors.")
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.child(IconButton::new(
|
||||
"import-vs-code-settings",
|
||||
ui::IconName::Code,
|
||||
))
|
||||
.child(IconButton::new(
|
||||
"import-cursor-settings",
|
||||
ui::IconName::CursorIBeam,
|
||||
)),
|
||||
)
|
||||
.child(Label::new("Popular Settings").size(LabelSize::Large))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_4()
|
||||
.justify_between()
|
||||
.child(
|
||||
v_flex()
|
||||
.justify_between()
|
||||
.gap_1()
|
||||
.child(Label::new("UI Font"))
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.gap_2()
|
||||
.child(div().min_w(px(120.)).child(DropdownMenu::new(
|
||||
"ui-font-family",
|
||||
theme_settings.ui_font.family.clone(),
|
||||
ContextMenu::build(window, cx, |mut menu, _, cx| {
|
||||
let font_family_cache = FontFamilyCache::global(cx);
|
||||
|
||||
for font_name in font_family_cache.list_font_families(cx) {
|
||||
menu = menu.custom_entry(
|
||||
{
|
||||
let font_name = font_name.clone();
|
||||
move |_window, _cx| {
|
||||
Label::new(font_name.clone())
|
||||
.into_any_element()
|
||||
}
|
||||
},
|
||||
{
|
||||
let font_name = font_name.clone();
|
||||
move |_window, cx| {
|
||||
write_ui_font_family(font_name.clone(), cx);
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
menu
|
||||
}),
|
||||
)))
|
||||
.child(NumericStepper::new(
|
||||
"ui-font-size",
|
||||
ui_font_size.to_string(),
|
||||
move |_, _, cx| {
|
||||
write_ui_font_size(ui_font_size - px(1.), cx);
|
||||
},
|
||||
move |_, _, cx| {
|
||||
write_ui_font_size(ui_font_size + px(1.), cx);
|
||||
},
|
||||
)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.justify_between()
|
||||
.gap_1()
|
||||
.child(Label::new("Editor Font"))
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.gap_2()
|
||||
.child(DropdownMenu::new(
|
||||
"buffer-font-family",
|
||||
font_family,
|
||||
ContextMenu::build(window, cx, |mut menu, _, cx| {
|
||||
let font_family_cache = FontFamilyCache::global(cx);
|
||||
|
||||
for font_name in font_family_cache.list_font_families(cx) {
|
||||
menu = menu.custom_entry(
|
||||
{
|
||||
let font_name = font_name.clone();
|
||||
move |_window, _cx| {
|
||||
Label::new(font_name.clone())
|
||||
.into_any_element()
|
||||
}
|
||||
},
|
||||
{
|
||||
let font_name = font_name.clone();
|
||||
move |_window, cx| {
|
||||
write_buffer_font_family(
|
||||
font_name.clone(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
menu
|
||||
}),
|
||||
))
|
||||
.child(NumericStepper::new(
|
||||
"buffer-font-size",
|
||||
buffer_font_size.to_string(),
|
||||
move |_, _, cx| {
|
||||
write_buffer_font_size(buffer_font_size - px(1.), cx);
|
||||
},
|
||||
move |_, _, cx| {
|
||||
write_buffer_font_size(buffer_font_size + px(1.), cx);
|
||||
},
|
||||
)),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.child(Label::new("Mini Map"))
|
||||
.child(
|
||||
ToggleButtonGroup::single_row(
|
||||
"onboarding-show-mini-map",
|
||||
[
|
||||
ToggleButtonSimple::new("Auto", |_, _, cx| {
|
||||
write_show_mini_map(ShowMinimap::Auto, cx);
|
||||
}),
|
||||
ToggleButtonSimple::new("Always", |_, _, cx| {
|
||||
write_show_mini_map(ShowMinimap::Always, cx);
|
||||
}),
|
||||
ToggleButtonSimple::new("Never", |_, _, cx| {
|
||||
write_show_mini_map(ShowMinimap::Never, cx);
|
||||
}),
|
||||
],
|
||||
)
|
||||
.selected_index(match read_show_mini_map(cx) {
|
||||
ShowMinimap::Auto => 0,
|
||||
ShowMinimap::Always => 1,
|
||||
ShowMinimap::Never => 2,
|
||||
})
|
||||
.style(ToggleButtonGroupStyle::Outlined)
|
||||
.button_width(ui::rems_from_px(64.)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
SwitchField::new(
|
||||
"onboarding-enable-inlay-hints",
|
||||
"Inlay Hints",
|
||||
"See parameter names for function and method calls inline.",
|
||||
if read_inlay_hints(cx) {
|
||||
ui::ToggleState::Selected
|
||||
} else {
|
||||
ui::ToggleState::Unselected
|
||||
},
|
||||
|toggle_state, _, cx| {
|
||||
write_inlay_hints(toggle_state == &ToggleState::Selected, cx);
|
||||
},
|
||||
)
|
||||
.color(SwitchColor::Accent),
|
||||
)
|
||||
.child(
|
||||
SwitchField::new(
|
||||
"onboarding-git-blame-switch",
|
||||
"Git Blame",
|
||||
"See who committed each line on a given file.",
|
||||
if read_git_blame(cx) {
|
||||
ui::ToggleState::Selected
|
||||
} else {
|
||||
ui::ToggleState::Unselected
|
||||
},
|
||||
|toggle_state, _, cx| {
|
||||
set_git_blame(toggle_state == &ToggleState::Selected, cx);
|
||||
},
|
||||
)
|
||||
.color(SwitchColor::Accent),
|
||||
)
|
||||
}
|
|
@ -21,6 +21,7 @@ use workspace::{
|
|||
open_new, with_active_or_new_workspace,
|
||||
};
|
||||
|
||||
mod editing_page;
|
||||
mod welcome;
|
||||
|
||||
pub struct OnBoardingFeatureFlag {}
|
||||
|
@ -246,7 +247,9 @@ impl Onboarding {
|
|||
fn render_page(&mut self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
|
||||
match self.selected_page {
|
||||
SelectedPage::Basics => self.render_basics_page(window, cx).into_any_element(),
|
||||
SelectedPage::Editing => self.render_editing_page(window, cx).into_any_element(),
|
||||
SelectedPage::Editing => {
|
||||
crate::editing_page::render_editing_page(window, cx).into_any_element()
|
||||
}
|
||||
SelectedPage::AiSetup => self.render_ai_setup_page(window, cx).into_any_element(),
|
||||
}
|
||||
}
|
||||
|
@ -281,11 +284,6 @@ impl Onboarding {
|
|||
)
|
||||
}
|
||||
|
||||
fn render_editing_page(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
||||
// div().child("editing page")
|
||||
"Right"
|
||||
}
|
||||
|
||||
fn render_ai_setup_page(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
||||
div().child("ai setup page")
|
||||
}
|
||||
|
|
|
@ -35,6 +35,7 @@ pub fn remote_server_dir_relative() -> &'static Path {
|
|||
|
||||
/// Sets a custom directory for all user data, overriding the default data directory.
|
||||
/// This function must be called before any other path operations that depend on the data directory.
|
||||
/// The directory's path will be canonicalized to an absolute path by a blocking FS operation.
|
||||
/// The directory will be created if it doesn't exist.
|
||||
///
|
||||
/// # Arguments
|
||||
|
@ -50,13 +51,20 @@ pub fn remote_server_dir_relative() -> &'static Path {
|
|||
///
|
||||
/// Panics if:
|
||||
/// * Called after the data directory has been initialized (e.g., via `data_dir` or `config_dir`)
|
||||
/// * The directory's path cannot be canonicalized to an absolute path
|
||||
/// * The directory cannot be created
|
||||
pub fn set_custom_data_dir(dir: &str) -> &'static PathBuf {
|
||||
if CURRENT_DATA_DIR.get().is_some() || CONFIG_DIR.get().is_some() {
|
||||
panic!("set_custom_data_dir called after data_dir or config_dir was initialized");
|
||||
}
|
||||
CUSTOM_DATA_DIR.get_or_init(|| {
|
||||
let path = PathBuf::from(dir);
|
||||
let mut path = PathBuf::from(dir);
|
||||
if path.is_relative() {
|
||||
let abs_path = path
|
||||
.canonicalize()
|
||||
.expect("failed to canonicalize custom data directory's path to an absolute path");
|
||||
path = PathBuf::from(util::paths::SanitizedPath::from(abs_path))
|
||||
}
|
||||
std::fs::create_dir_all(&path).expect("failed to create custom data directory");
|
||||
path
|
||||
})
|
||||
|
|
|
@ -13,6 +13,7 @@ use settings::{Settings as _, SettingsStore};
|
|||
use util::ResultExt as _;
|
||||
|
||||
use crate::{
|
||||
Project,
|
||||
project_settings::{ContextServerSettings, ProjectSettings},
|
||||
worktree_store::WorktreeStore,
|
||||
};
|
||||
|
@ -144,6 +145,7 @@ pub struct ContextServerStore {
|
|||
context_server_settings: HashMap<Arc<str>, ContextServerSettings>,
|
||||
servers: HashMap<ContextServerId, ContextServerState>,
|
||||
worktree_store: Entity<WorktreeStore>,
|
||||
project: WeakEntity<Project>,
|
||||
registry: Entity<ContextServerDescriptorRegistry>,
|
||||
update_servers_task: Option<Task<Result<()>>>,
|
||||
context_server_factory: Option<ContextServerFactory>,
|
||||
|
@ -161,12 +163,17 @@ pub enum Event {
|
|||
impl EventEmitter<Event> for ContextServerStore {}
|
||||
|
||||
impl ContextServerStore {
|
||||
pub fn new(worktree_store: Entity<WorktreeStore>, cx: &mut Context<Self>) -> Self {
|
||||
pub fn new(
|
||||
worktree_store: Entity<WorktreeStore>,
|
||||
weak_project: WeakEntity<Project>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
Self::new_internal(
|
||||
true,
|
||||
None,
|
||||
ContextServerDescriptorRegistry::default_global(cx),
|
||||
worktree_store,
|
||||
weak_project,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
@ -184,9 +191,10 @@ impl ContextServerStore {
|
|||
pub fn test(
|
||||
registry: Entity<ContextServerDescriptorRegistry>,
|
||||
worktree_store: Entity<WorktreeStore>,
|
||||
weak_project: WeakEntity<Project>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
Self::new_internal(false, None, registry, worktree_store, cx)
|
||||
Self::new_internal(false, None, registry, worktree_store, weak_project, cx)
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
|
@ -194,6 +202,7 @@ impl ContextServerStore {
|
|||
context_server_factory: ContextServerFactory,
|
||||
registry: Entity<ContextServerDescriptorRegistry>,
|
||||
worktree_store: Entity<WorktreeStore>,
|
||||
weak_project: WeakEntity<Project>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
Self::new_internal(
|
||||
|
@ -201,6 +210,7 @@ impl ContextServerStore {
|
|||
Some(context_server_factory),
|
||||
registry,
|
||||
worktree_store,
|
||||
weak_project,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
@ -210,6 +220,7 @@ impl ContextServerStore {
|
|||
context_server_factory: Option<ContextServerFactory>,
|
||||
registry: Entity<ContextServerDescriptorRegistry>,
|
||||
worktree_store: Entity<WorktreeStore>,
|
||||
weak_project: WeakEntity<Project>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let subscriptions = if maintain_server_loop {
|
||||
|
@ -235,6 +246,7 @@ impl ContextServerStore {
|
|||
context_server_settings: Self::resolve_context_server_settings(&worktree_store, cx)
|
||||
.clone(),
|
||||
worktree_store,
|
||||
project: weak_project,
|
||||
registry,
|
||||
needs_server_update: false,
|
||||
servers: HashMap::default(),
|
||||
|
@ -360,7 +372,7 @@ impl ContextServerStore {
|
|||
let configuration = state.configuration();
|
||||
|
||||
self.stop_server(&state.server().id(), cx)?;
|
||||
let new_server = self.create_context_server(id.clone(), configuration.clone())?;
|
||||
let new_server = self.create_context_server(id.clone(), configuration.clone(), cx);
|
||||
self.run_server(new_server, configuration, cx);
|
||||
}
|
||||
Ok(())
|
||||
|
@ -449,14 +461,33 @@ impl ContextServerStore {
|
|||
&self,
|
||||
id: ContextServerId,
|
||||
configuration: Arc<ContextServerConfiguration>,
|
||||
) -> Result<Arc<ContextServer>> {
|
||||
cx: &mut Context<Self>,
|
||||
) -> Arc<ContextServer> {
|
||||
let root_path = self
|
||||
.project
|
||||
.read_with(cx, |project, cx| project.active_project_directory(cx))
|
||||
.ok()
|
||||
.flatten()
|
||||
.or_else(|| {
|
||||
self.worktree_store.read_with(cx, |store, cx| {
|
||||
store.visible_worktrees(cx).fold(None, |acc, item| {
|
||||
if acc.is_none() {
|
||||
item.read(cx).root_dir()
|
||||
} else {
|
||||
acc
|
||||
}
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
if let Some(factory) = self.context_server_factory.as_ref() {
|
||||
Ok(factory(id, configuration))
|
||||
factory(id, configuration)
|
||||
} else {
|
||||
Ok(Arc::new(ContextServer::stdio(
|
||||
Arc::new(ContextServer::stdio(
|
||||
id,
|
||||
configuration.command().clone(),
|
||||
)))
|
||||
root_path,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -553,7 +584,7 @@ impl ContextServerStore {
|
|||
let mut servers_to_remove = HashSet::default();
|
||||
let mut servers_to_stop = HashSet::default();
|
||||
|
||||
this.update(cx, |this, _cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
for server_id in this.servers.keys() {
|
||||
// All servers that are not in desired_servers should be removed from the store.
|
||||
// This can happen if the user removed a server from the context server settings.
|
||||
|
@ -572,14 +603,10 @@ impl ContextServerStore {
|
|||
let existing_config = state.as_ref().map(|state| state.configuration());
|
||||
if existing_config.as_deref() != Some(&config) || is_stopped {
|
||||
let config = Arc::new(config);
|
||||
if let Some(server) = this
|
||||
.create_context_server(id.clone(), config.clone())
|
||||
.log_err()
|
||||
{
|
||||
servers_to_start.push((server, config));
|
||||
if this.servers.contains_key(&id) {
|
||||
servers_to_stop.insert(id);
|
||||
}
|
||||
let server = this.create_context_server(id.clone(), config.clone(), cx);
|
||||
servers_to_start.push((server, config));
|
||||
if this.servers.contains_key(&id) {
|
||||
servers_to_stop.insert(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -630,7 +657,12 @@ mod tests {
|
|||
|
||||
let registry = cx.new(|_| ContextServerDescriptorRegistry::new());
|
||||
let store = cx.new(|cx| {
|
||||
ContextServerStore::test(registry.clone(), project.read(cx).worktree_store(), cx)
|
||||
ContextServerStore::test(
|
||||
registry.clone(),
|
||||
project.read(cx).worktree_store(),
|
||||
project.downgrade(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let server_1_id = ContextServerId(SERVER_1_ID.into());
|
||||
|
@ -705,7 +737,12 @@ mod tests {
|
|||
|
||||
let registry = cx.new(|_| ContextServerDescriptorRegistry::new());
|
||||
let store = cx.new(|cx| {
|
||||
ContextServerStore::test(registry.clone(), project.read(cx).worktree_store(), cx)
|
||||
ContextServerStore::test(
|
||||
registry.clone(),
|
||||
project.read(cx).worktree_store(),
|
||||
project.downgrade(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let server_1_id = ContextServerId(SERVER_1_ID.into());
|
||||
|
@ -758,7 +795,12 @@ mod tests {
|
|||
|
||||
let registry = cx.new(|_| ContextServerDescriptorRegistry::new());
|
||||
let store = cx.new(|cx| {
|
||||
ContextServerStore::test(registry.clone(), project.read(cx).worktree_store(), cx)
|
||||
ContextServerStore::test(
|
||||
registry.clone(),
|
||||
project.read(cx).worktree_store(),
|
||||
project.downgrade(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let server_id = ContextServerId(SERVER_1_ID.into());
|
||||
|
@ -842,6 +884,7 @@ mod tests {
|
|||
}),
|
||||
registry.clone(),
|
||||
project.read(cx).worktree_store(),
|
||||
project.downgrade(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
@ -1074,6 +1117,7 @@ mod tests {
|
|||
}),
|
||||
registry.clone(),
|
||||
project.read(cx).worktree_store(),
|
||||
project.downgrade(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
|
|
@ -2425,36 +2425,12 @@ impl LocalLspStore {
|
|||
let server_id = server_node.server_id_or_init(
|
||||
|LaunchDisposition {
|
||||
server_name,
|
||||
attach,
|
||||
|
||||
path,
|
||||
settings,
|
||||
}| {
|
||||
let server_id = match attach {
|
||||
language::Attach::InstancePerRoot => {
|
||||
// todo: handle instance per root proper.
|
||||
if let Some(server_ids) = self
|
||||
.language_server_ids
|
||||
.get(&(worktree_id, server_name.clone()))
|
||||
{
|
||||
server_ids.iter().cloned().next().unwrap()
|
||||
} else {
|
||||
let language_name = language.name();
|
||||
let adapter = self.languages
|
||||
.lsp_adapters(&language_name)
|
||||
.into_iter()
|
||||
.find(|adapter| &adapter.name() == server_name)
|
||||
.expect("To find LSP adapter");
|
||||
let server_id = self.start_language_server(
|
||||
&worktree,
|
||||
delegate.clone(),
|
||||
adapter,
|
||||
settings,
|
||||
cx,
|
||||
);
|
||||
server_id
|
||||
}
|
||||
}
|
||||
language::Attach::Shared => {
|
||||
let server_id =
|
||||
{
|
||||
let uri = Url::from_file_path(
|
||||
worktree.read(cx).abs_path().join(&path.path),
|
||||
);
|
||||
|
@ -2489,7 +2465,7 @@ impl LocalLspStore {
|
|||
} else {
|
||||
unreachable!("Language server ID should be available, as it's registered on demand")
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
let lsp_store = self.weak.clone();
|
||||
let server_name = server_node.name();
|
||||
|
@ -4705,35 +4681,11 @@ impl LspStore {
|
|||
let server_id = node.server_id_or_init(
|
||||
|LaunchDisposition {
|
||||
server_name,
|
||||
attach,
|
||||
|
||||
path,
|
||||
settings,
|
||||
}| match attach {
|
||||
language::Attach::InstancePerRoot => {
|
||||
// todo: handle instance per root proper.
|
||||
if let Some(server_ids) = local
|
||||
.language_server_ids
|
||||
.get(&(worktree_id, server_name.clone()))
|
||||
{
|
||||
server_ids.iter().cloned().next().unwrap()
|
||||
} else {
|
||||
let adapter = local
|
||||
.languages
|
||||
.lsp_adapters(&language)
|
||||
.into_iter()
|
||||
.find(|adapter| &adapter.name() == server_name)
|
||||
.expect("To find LSP adapter");
|
||||
let server_id = local.start_language_server(
|
||||
&worktree,
|
||||
delegate.clone(),
|
||||
adapter,
|
||||
settings,
|
||||
cx,
|
||||
);
|
||||
server_id
|
||||
}
|
||||
}
|
||||
language::Attach::Shared => {
|
||||
}|
|
||||
{
|
||||
let uri = Url::from_file_path(
|
||||
worktree.read(cx).abs_path().join(&path.path),
|
||||
);
|
||||
|
@ -4762,7 +4714,6 @@ impl LspStore {
|
|||
}
|
||||
server_id
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if let Some(language_server_id) = server_id {
|
||||
|
@ -7442,21 +7393,23 @@ impl LspStore {
|
|||
}
|
||||
|
||||
pub(crate) async fn refresh_workspace_configurations(
|
||||
this: &WeakEntity<Self>,
|
||||
lsp_store: &WeakEntity<Self>,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &mut AsyncApp,
|
||||
) {
|
||||
maybe!(async move {
|
||||
let servers = this
|
||||
.update(cx, |this, cx| {
|
||||
let Some(local) = this.as_local() else {
|
||||
let mut refreshed_servers = HashSet::default();
|
||||
let servers = lsp_store
|
||||
.update(cx, |lsp_store, cx| {
|
||||
let toolchain_store = lsp_store.toolchain_store(cx);
|
||||
let Some(local) = lsp_store.as_local() else {
|
||||
return Vec::default();
|
||||
};
|
||||
local
|
||||
.language_server_ids
|
||||
.iter()
|
||||
.flat_map(|((worktree_id, _), server_ids)| {
|
||||
let worktree = this
|
||||
let worktree = lsp_store
|
||||
.worktree_store
|
||||
.read(cx)
|
||||
.worktree_for_id(*worktree_id, cx);
|
||||
|
@ -7472,43 +7425,54 @@ impl LspStore {
|
|||
)
|
||||
});
|
||||
|
||||
server_ids.iter().filter_map(move |server_id| {
|
||||
let fs = fs.clone();
|
||||
let toolchain_store = toolchain_store.clone();
|
||||
server_ids.iter().filter_map(|server_id| {
|
||||
let delegate = delegate.clone()? as Arc<dyn LspAdapterDelegate>;
|
||||
let states = local.language_servers.get(server_id)?;
|
||||
|
||||
match states {
|
||||
LanguageServerState::Starting { .. } => None,
|
||||
LanguageServerState::Running {
|
||||
adapter, server, ..
|
||||
} => Some((
|
||||
adapter.adapter.clone(),
|
||||
server.clone(),
|
||||
delegate.clone()? as Arc<dyn LspAdapterDelegate>,
|
||||
)),
|
||||
} => {
|
||||
let fs = fs.clone();
|
||||
let toolchain_store = toolchain_store.clone();
|
||||
let adapter = adapter.clone();
|
||||
let server = server.clone();
|
||||
refreshed_servers.insert(server.name());
|
||||
Some(cx.spawn(async move |_, cx| {
|
||||
let settings =
|
||||
LocalLspStore::workspace_configuration_for_adapter(
|
||||
adapter.adapter.clone(),
|
||||
fs.as_ref(),
|
||||
&delegate,
|
||||
toolchain_store,
|
||||
cx,
|
||||
)
|
||||
.await
|
||||
.ok()?;
|
||||
server
|
||||
.notify::<lsp::notification::DidChangeConfiguration>(
|
||||
&lsp::DidChangeConfigurationParams { settings },
|
||||
)
|
||||
.ok()?;
|
||||
Some(())
|
||||
}))
|
||||
}
|
||||
}
|
||||
})
|
||||
}).collect::<Vec<_>>()
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.ok()?;
|
||||
|
||||
let toolchain_store = this.update(cx, |this, cx| this.toolchain_store(cx)).ok()?;
|
||||
for (adapter, server, delegate) in servers {
|
||||
let settings = LocalLspStore::workspace_configuration_for_adapter(
|
||||
adapter,
|
||||
fs.as_ref(),
|
||||
&delegate,
|
||||
toolchain_store.clone(),
|
||||
cx,
|
||||
)
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
server
|
||||
.notify::<lsp::notification::DidChangeConfiguration>(
|
||||
&lsp::DidChangeConfigurationParams { settings },
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
log::info!("Refreshing workspace configurations for servers {refreshed_servers:?}");
|
||||
// TODO this asynchronous job runs concurrently with extension (de)registration and may take enough time for a certain extension
|
||||
// to stop and unregister its language server wrapper.
|
||||
// This is racy : an extension might have already removed all `local.language_servers` state, but here we `.clone()` and hold onto it anyway.
|
||||
// This now causes errors in the logs, we should find a way to remove such servers from the processing everywhere.
|
||||
let _: Vec<Option<()>> = join_all(servers).await;
|
||||
Some(())
|
||||
})
|
||||
.await;
|
||||
|
|
|
@ -13,10 +13,10 @@ use std::{
|
|||
sync::{Arc, Weak},
|
||||
};
|
||||
|
||||
use collections::{HashMap, IndexMap};
|
||||
use collections::IndexMap;
|
||||
use gpui::{App, AppContext as _, Entity, Subscription};
|
||||
use language::{
|
||||
Attach, CachedLspAdapter, LanguageName, LanguageRegistry, ManifestDelegate,
|
||||
CachedLspAdapter, LanguageName, LanguageRegistry, ManifestDelegate,
|
||||
language_settings::AllLanguageSettings,
|
||||
};
|
||||
use lsp::LanguageServerName;
|
||||
|
@ -38,7 +38,6 @@ pub(crate) struct ServersForWorktree {
|
|||
pub struct LanguageServerTree {
|
||||
manifest_tree: Entity<ManifestTree>,
|
||||
pub(crate) instances: BTreeMap<WorktreeId, ServersForWorktree>,
|
||||
attach_kind_cache: HashMap<LanguageServerName, Attach>,
|
||||
languages: Arc<LanguageRegistry>,
|
||||
_subscriptions: Subscription,
|
||||
}
|
||||
|
@ -53,7 +52,6 @@ pub struct LanguageServerTreeNode(Weak<InnerTreeNode>);
|
|||
#[derive(Debug)]
|
||||
pub(crate) struct LaunchDisposition<'a> {
|
||||
pub(crate) server_name: &'a LanguageServerName,
|
||||
pub(crate) attach: Attach,
|
||||
pub(crate) path: ProjectPath,
|
||||
pub(crate) settings: Arc<LspSettings>,
|
||||
}
|
||||
|
@ -62,7 +60,6 @@ impl<'a> From<&'a InnerTreeNode> for LaunchDisposition<'a> {
|
|||
fn from(value: &'a InnerTreeNode) -> Self {
|
||||
LaunchDisposition {
|
||||
server_name: &value.name,
|
||||
attach: value.attach,
|
||||
path: value.path.clone(),
|
||||
settings: value.settings.clone(),
|
||||
}
|
||||
|
@ -105,7 +102,6 @@ impl From<Weak<InnerTreeNode>> for LanguageServerTreeNode {
|
|||
pub struct InnerTreeNode {
|
||||
id: OnceLock<LanguageServerId>,
|
||||
name: LanguageServerName,
|
||||
attach: Attach,
|
||||
path: ProjectPath,
|
||||
settings: Arc<LspSettings>,
|
||||
}
|
||||
|
@ -113,14 +109,12 @@ pub struct InnerTreeNode {
|
|||
impl InnerTreeNode {
|
||||
fn new(
|
||||
name: LanguageServerName,
|
||||
attach: Attach,
|
||||
path: ProjectPath,
|
||||
settings: impl Into<Arc<LspSettings>>,
|
||||
) -> Self {
|
||||
InnerTreeNode {
|
||||
id: Default::default(),
|
||||
name,
|
||||
attach,
|
||||
path,
|
||||
settings: settings.into(),
|
||||
}
|
||||
|
@ -130,8 +124,11 @@ impl InnerTreeNode {
|
|||
/// Determines how the list of adapters to query should be constructed.
|
||||
pub(crate) enum AdapterQuery<'a> {
|
||||
/// Search for roots of all adapters associated with a given language name.
|
||||
/// Layman: Look for all project roots along the queried path that have any
|
||||
/// language server associated with this language running.
|
||||
Language(&'a LanguageName),
|
||||
/// Search for roots of adapter with a given name.
|
||||
/// Layman: Look for all project roots along the queried path that have this server running.
|
||||
Adapter(&'a LanguageServerName),
|
||||
}
|
||||
|
||||
|
@ -147,7 +144,7 @@ impl LanguageServerTree {
|
|||
}),
|
||||
manifest_tree,
|
||||
instances: Default::default(),
|
||||
attach_kind_cache: Default::default(),
|
||||
|
||||
languages,
|
||||
})
|
||||
}
|
||||
|
@ -223,7 +220,6 @@ impl LanguageServerTree {
|
|||
.and_then(|name| roots.get(&name))
|
||||
.cloned()
|
||||
.unwrap_or_else(|| root_path.clone());
|
||||
let attach = adapter.attach_kind();
|
||||
|
||||
let inner_node = self
|
||||
.instances
|
||||
|
@ -237,7 +233,6 @@ impl LanguageServerTree {
|
|||
(
|
||||
Arc::new(InnerTreeNode::new(
|
||||
adapter.name(),
|
||||
attach,
|
||||
root_path.clone(),
|
||||
settings.clone(),
|
||||
)),
|
||||
|
@ -379,7 +374,6 @@ pub(crate) struct ServerTreeRebase<'a> {
|
|||
impl<'tree> ServerTreeRebase<'tree> {
|
||||
fn new(new_tree: &'tree mut LanguageServerTree) -> Self {
|
||||
let old_contents = std::mem::take(&mut new_tree.instances);
|
||||
new_tree.attach_kind_cache.clear();
|
||||
let all_server_ids = old_contents
|
||||
.values()
|
||||
.flat_map(|nodes| {
|
||||
|
@ -446,10 +440,7 @@ impl<'tree> ServerTreeRebase<'tree> {
|
|||
.get(&disposition.path.worktree_id)
|
||||
.and_then(|worktree_nodes| worktree_nodes.roots.get(&disposition.path.path))
|
||||
.and_then(|roots| roots.get(&disposition.name))
|
||||
.filter(|(old_node, _)| {
|
||||
disposition.attach == old_node.attach
|
||||
&& disposition.settings == old_node.settings
|
||||
})
|
||||
.filter(|(old_node, _)| disposition.settings == old_node.settings)
|
||||
else {
|
||||
return Some(node);
|
||||
};
|
||||
|
|
|
@ -998,8 +998,9 @@ impl Project {
|
|||
cx.subscribe(&worktree_store, Self::on_worktree_store_event)
|
||||
.detach();
|
||||
|
||||
let weak_self = cx.weak_entity();
|
||||
let context_server_store =
|
||||
cx.new(|cx| ContextServerStore::new(worktree_store.clone(), cx));
|
||||
cx.new(|cx| ContextServerStore::new(worktree_store.clone(), weak_self, cx));
|
||||
|
||||
let environment = cx.new(|_| ProjectEnvironment::new(env));
|
||||
let manifest_tree = ManifestTree::new(worktree_store.clone(), cx);
|
||||
|
@ -1167,8 +1168,9 @@ impl Project {
|
|||
cx.subscribe(&worktree_store, Self::on_worktree_store_event)
|
||||
.detach();
|
||||
|
||||
let weak_self = cx.weak_entity();
|
||||
let context_server_store =
|
||||
cx.new(|cx| ContextServerStore::new(worktree_store.clone(), cx));
|
||||
cx.new(|cx| ContextServerStore::new(worktree_store.clone(), weak_self, cx));
|
||||
|
||||
let buffer_store = cx.new(|cx| {
|
||||
BufferStore::remote(
|
||||
|
@ -1428,8 +1430,6 @@ impl Project {
|
|||
let image_store = cx.new(|cx| {
|
||||
ImageStore::remote(worktree_store.clone(), client.clone().into(), remote_id, cx)
|
||||
})?;
|
||||
let context_server_store =
|
||||
cx.new(|cx| ContextServerStore::new(worktree_store.clone(), cx))?;
|
||||
|
||||
let environment = cx.new(|_| ProjectEnvironment::new(None))?;
|
||||
|
||||
|
@ -1496,6 +1496,10 @@ impl Project {
|
|||
|
||||
let snippets = SnippetProvider::new(fs.clone(), BTreeSet::from_iter([]), cx);
|
||||
|
||||
let weak_self = cx.weak_entity();
|
||||
let context_server_store =
|
||||
cx.new(|cx| ContextServerStore::new(worktree_store.clone(), weak_self, cx));
|
||||
|
||||
let mut worktrees = Vec::new();
|
||||
for worktree in response.payload.worktrees {
|
||||
let worktree =
|
||||
|
|
|
@ -20,6 +20,7 @@ static REDACT_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"key=[^&]+")
|
|||
pub struct ReqwestClient {
|
||||
client: reqwest::Client,
|
||||
proxy: Option<Url>,
|
||||
user_agent: Option<HeaderValue>,
|
||||
handle: tokio::runtime::Handle,
|
||||
}
|
||||
|
||||
|
@ -44,9 +45,11 @@ impl ReqwestClient {
|
|||
Ok(client.into())
|
||||
}
|
||||
|
||||
pub fn proxy_and_user_agent(proxy: Option<Url>, agent: &str) -> anyhow::Result<Self> {
|
||||
pub fn proxy_and_user_agent(proxy: Option<Url>, user_agent: &str) -> anyhow::Result<Self> {
|
||||
let user_agent = HeaderValue::from_str(user_agent)?;
|
||||
|
||||
let mut map = HeaderMap::new();
|
||||
map.insert(http::header::USER_AGENT, HeaderValue::from_str(agent)?);
|
||||
map.insert(http::header::USER_AGENT, user_agent.clone());
|
||||
let mut client = Self::builder().default_headers(map);
|
||||
let client_has_proxy;
|
||||
|
||||
|
@ -73,6 +76,7 @@ impl ReqwestClient {
|
|||
.build()?;
|
||||
let mut client: ReqwestClient = client.into();
|
||||
client.proxy = client_has_proxy.then_some(proxy).flatten();
|
||||
client.user_agent = Some(user_agent);
|
||||
Ok(client)
|
||||
}
|
||||
}
|
||||
|
@ -96,6 +100,7 @@ impl From<reqwest::Client> for ReqwestClient {
|
|||
client,
|
||||
handle,
|
||||
proxy: None,
|
||||
user_agent: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -216,6 +221,10 @@ impl http_client::HttpClient for ReqwestClient {
|
|||
type_name::<Self>()
|
||||
}
|
||||
|
||||
fn user_agent(&self) -> Option<&HeaderValue> {
|
||||
self.user_agent.as_ref()
|
||||
}
|
||||
|
||||
fn send(
|
||||
&self,
|
||||
req: http::Request<http_client::AsyncBody>,
|
||||
|
|
|
@ -48,3 +48,7 @@ workspace.workspace = true
|
|||
|
||||
[dev-dependencies]
|
||||
db = {"workspace"= true, "features" = ["test-support"]}
|
||||
fs = { workspace = true, features = ["test-support"] }
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
workspace = { workspace = true, features = ["test-support"] }
|
||||
|
|
|
@ -11,11 +11,10 @@ use editor::{CompletionProvider, Editor, EditorEvent};
|
|||
use fs::Fs;
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
Action, Animation, AnimationExt, AppContext as _, AsyncApp, Axis, ClickEvent, Context,
|
||||
DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, Global, IsZero,
|
||||
KeyContext, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, Point, ScrollStrategy,
|
||||
ScrollWheelEvent, Stateful, StyledText, Subscription, Task, TextStyleRefinement, WeakEntity,
|
||||
actions, anchored, deferred, div,
|
||||
Action, AppContext as _, AsyncApp, Axis, ClickEvent, Context, DismissEvent, Entity,
|
||||
EventEmitter, FocusHandle, Focusable, Global, IsZero, KeyContext, Keystroke, MouseButton,
|
||||
Point, ScrollStrategy, ScrollWheelEvent, Stateful, StyledText, Subscription, Task,
|
||||
TextStyleRefinement, WeakEntity, actions, anchored, deferred, div,
|
||||
};
|
||||
use language::{Language, LanguageConfig, ToOffset as _};
|
||||
use notifications::status_toast::{StatusToast, ToastIcon};
|
||||
|
@ -35,7 +34,10 @@ use workspace::{
|
|||
|
||||
use crate::{
|
||||
keybindings::persistence::KEYBINDING_EDITORS,
|
||||
ui_components::table::{ColumnWidths, ResizeBehavior, Table, TableInteractionState},
|
||||
ui_components::{
|
||||
keystroke_input::{ClearKeystrokes, KeystrokeInput, StartRecording, StopRecording},
|
||||
table::{ColumnWidths, ResizeBehavior, Table, TableInteractionState},
|
||||
},
|
||||
};
|
||||
|
||||
const NO_ACTION_ARGUMENTS_TEXT: SharedString = SharedString::new_static("<no arguments>");
|
||||
|
@ -72,18 +74,6 @@ actions!(
|
|||
]
|
||||
);
|
||||
|
||||
actions!(
|
||||
keystroke_input,
|
||||
[
|
||||
/// Starts recording keystrokes
|
||||
StartRecording,
|
||||
/// Stops recording keystrokes
|
||||
StopRecording,
|
||||
/// Clears the recorded keystrokes
|
||||
ClearKeystrokes,
|
||||
]
|
||||
);
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
let keymap_event_channel = KeymapEventChannel::new();
|
||||
cx.set_global(keymap_event_channel);
|
||||
|
@ -393,7 +383,7 @@ impl KeymapEditor {
|
|||
|
||||
let keystroke_editor = cx.new(|cx| {
|
||||
let mut keystroke_editor = KeystrokeInput::new(None, window, cx);
|
||||
keystroke_editor.search = true;
|
||||
keystroke_editor.set_search(true);
|
||||
keystroke_editor
|
||||
});
|
||||
|
||||
|
@ -2979,524 +2969,6 @@ async fn remove_keybinding(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
|
||||
enum CloseKeystrokeResult {
|
||||
Partial,
|
||||
Close,
|
||||
None,
|
||||
}
|
||||
|
||||
struct KeystrokeInput {
|
||||
keystrokes: Vec<Keystroke>,
|
||||
placeholder_keystrokes: Option<Vec<Keystroke>>,
|
||||
outer_focus_handle: FocusHandle,
|
||||
inner_focus_handle: FocusHandle,
|
||||
intercept_subscription: Option<Subscription>,
|
||||
_focus_subscriptions: [Subscription; 2],
|
||||
search: bool,
|
||||
/// Handles tripe escape to stop recording
|
||||
close_keystrokes: Option<Vec<Keystroke>>,
|
||||
close_keystrokes_start: Option<usize>,
|
||||
previous_modifiers: Modifiers,
|
||||
}
|
||||
|
||||
impl KeystrokeInput {
|
||||
const KEYSTROKE_COUNT_MAX: usize = 3;
|
||||
|
||||
fn new(
|
||||
placeholder_keystrokes: Option<Vec<Keystroke>>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let outer_focus_handle = cx.focus_handle();
|
||||
let inner_focus_handle = cx.focus_handle();
|
||||
let _focus_subscriptions = [
|
||||
cx.on_focus_in(&inner_focus_handle, window, Self::on_inner_focus_in),
|
||||
cx.on_focus_out(&inner_focus_handle, window, Self::on_inner_focus_out),
|
||||
];
|
||||
Self {
|
||||
keystrokes: Vec::new(),
|
||||
placeholder_keystrokes,
|
||||
inner_focus_handle,
|
||||
outer_focus_handle,
|
||||
intercept_subscription: None,
|
||||
_focus_subscriptions,
|
||||
search: false,
|
||||
close_keystrokes: None,
|
||||
close_keystrokes_start: None,
|
||||
previous_modifiers: Modifiers::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_keystrokes(&mut self, keystrokes: Vec<Keystroke>, cx: &mut Context<Self>) {
|
||||
self.keystrokes = keystrokes;
|
||||
self.keystrokes_changed(cx);
|
||||
}
|
||||
|
||||
fn dummy(modifiers: Modifiers) -> Keystroke {
|
||||
return Keystroke {
|
||||
modifiers,
|
||||
key: "".to_string(),
|
||||
key_char: None,
|
||||
};
|
||||
}
|
||||
|
||||
fn keystrokes_changed(&self, cx: &mut Context<Self>) {
|
||||
cx.emit(());
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn key_context() -> KeyContext {
|
||||
let mut key_context = KeyContext::default();
|
||||
key_context.add("KeystrokeInput");
|
||||
key_context
|
||||
}
|
||||
|
||||
fn handle_possible_close_keystroke(
|
||||
&mut self,
|
||||
keystroke: &Keystroke,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> CloseKeystrokeResult {
|
||||
let Some(keybind_for_close_action) = window
|
||||
.highest_precedence_binding_for_action_in_context(&StopRecording, Self::key_context())
|
||||
else {
|
||||
log::trace!("No keybinding to stop recording keystrokes in keystroke input");
|
||||
self.close_keystrokes.take();
|
||||
self.close_keystrokes_start.take();
|
||||
return CloseKeystrokeResult::None;
|
||||
};
|
||||
let action_keystrokes = keybind_for_close_action.keystrokes();
|
||||
|
||||
if let Some(mut close_keystrokes) = self.close_keystrokes.take() {
|
||||
let mut index = 0;
|
||||
|
||||
while index < action_keystrokes.len() && index < close_keystrokes.len() {
|
||||
if !close_keystrokes[index].should_match(&action_keystrokes[index]) {
|
||||
break;
|
||||
}
|
||||
index += 1;
|
||||
}
|
||||
if index == close_keystrokes.len() {
|
||||
if index >= action_keystrokes.len() {
|
||||
self.close_keystrokes_start.take();
|
||||
return CloseKeystrokeResult::None;
|
||||
}
|
||||
if keystroke.should_match(&action_keystrokes[index]) {
|
||||
if action_keystrokes.len() >= 1 && index == action_keystrokes.len() - 1 {
|
||||
self.stop_recording(&StopRecording, window, cx);
|
||||
return CloseKeystrokeResult::Close;
|
||||
} else {
|
||||
close_keystrokes.push(keystroke.clone());
|
||||
self.close_keystrokes = Some(close_keystrokes);
|
||||
return CloseKeystrokeResult::Partial;
|
||||
}
|
||||
} else {
|
||||
self.close_keystrokes_start.take();
|
||||
return CloseKeystrokeResult::None;
|
||||
}
|
||||
}
|
||||
} else if let Some(first_action_keystroke) = action_keystrokes.first()
|
||||
&& keystroke.should_match(first_action_keystroke)
|
||||
{
|
||||
self.close_keystrokes = Some(vec![keystroke.clone()]);
|
||||
return CloseKeystrokeResult::Partial;
|
||||
}
|
||||
self.close_keystrokes_start.take();
|
||||
return CloseKeystrokeResult::None;
|
||||
}
|
||||
|
||||
fn on_modifiers_changed(
|
||||
&mut self,
|
||||
event: &ModifiersChangedEvent,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let keystrokes_len = self.keystrokes.len();
|
||||
|
||||
if self.previous_modifiers.modified()
|
||||
&& event.modifiers.is_subset_of(&self.previous_modifiers)
|
||||
{
|
||||
self.previous_modifiers &= event.modifiers;
|
||||
cx.stop_propagation();
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(last) = self.keystrokes.last_mut()
|
||||
&& last.key.is_empty()
|
||||
&& keystrokes_len <= Self::KEYSTROKE_COUNT_MAX
|
||||
{
|
||||
if self.search {
|
||||
if self.previous_modifiers.modified() {
|
||||
last.modifiers |= event.modifiers;
|
||||
self.previous_modifiers |= event.modifiers;
|
||||
} else {
|
||||
self.keystrokes.push(Self::dummy(event.modifiers));
|
||||
self.previous_modifiers |= event.modifiers;
|
||||
}
|
||||
} else if !event.modifiers.modified() {
|
||||
self.keystrokes.pop();
|
||||
} else {
|
||||
last.modifiers = event.modifiers;
|
||||
}
|
||||
|
||||
self.keystrokes_changed(cx);
|
||||
} else if keystrokes_len < Self::KEYSTROKE_COUNT_MAX {
|
||||
self.keystrokes.push(Self::dummy(event.modifiers));
|
||||
if self.search {
|
||||
self.previous_modifiers |= event.modifiers;
|
||||
}
|
||||
self.keystrokes_changed(cx);
|
||||
}
|
||||
cx.stop_propagation();
|
||||
}
|
||||
|
||||
fn handle_keystroke(
|
||||
&mut self,
|
||||
keystroke: &Keystroke,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let close_keystroke_result = self.handle_possible_close_keystroke(keystroke, window, cx);
|
||||
if close_keystroke_result != CloseKeystrokeResult::Close {
|
||||
let key_len = self.keystrokes.len();
|
||||
if let Some(last) = self.keystrokes.last_mut()
|
||||
&& last.key.is_empty()
|
||||
&& key_len <= Self::KEYSTROKE_COUNT_MAX
|
||||
{
|
||||
if self.search {
|
||||
last.key = keystroke.key.clone();
|
||||
if close_keystroke_result == CloseKeystrokeResult::Partial
|
||||
&& self.close_keystrokes_start.is_none()
|
||||
{
|
||||
self.close_keystrokes_start = Some(self.keystrokes.len() - 1);
|
||||
}
|
||||
if self.search {
|
||||
self.previous_modifiers = keystroke.modifiers;
|
||||
}
|
||||
self.keystrokes_changed(cx);
|
||||
cx.stop_propagation();
|
||||
return;
|
||||
} else {
|
||||
self.keystrokes.pop();
|
||||
}
|
||||
}
|
||||
if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX {
|
||||
if close_keystroke_result == CloseKeystrokeResult::Partial
|
||||
&& self.close_keystrokes_start.is_none()
|
||||
{
|
||||
self.close_keystrokes_start = Some(self.keystrokes.len());
|
||||
}
|
||||
self.keystrokes.push(keystroke.clone());
|
||||
if self.search {
|
||||
self.previous_modifiers = keystroke.modifiers;
|
||||
} else if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX {
|
||||
self.keystrokes.push(Self::dummy(keystroke.modifiers));
|
||||
}
|
||||
} else if close_keystroke_result != CloseKeystrokeResult::Partial {
|
||||
self.clear_keystrokes(&ClearKeystrokes, window, cx);
|
||||
}
|
||||
}
|
||||
self.keystrokes_changed(cx);
|
||||
cx.stop_propagation();
|
||||
}
|
||||
|
||||
fn on_inner_focus_in(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.intercept_subscription.is_none() {
|
||||
let listener = cx.listener(|this, event: &gpui::KeystrokeEvent, window, cx| {
|
||||
this.handle_keystroke(&event.keystroke, window, cx);
|
||||
});
|
||||
self.intercept_subscription = Some(cx.intercept_keystrokes(listener))
|
||||
}
|
||||
}
|
||||
|
||||
fn on_inner_focus_out(
|
||||
&mut self,
|
||||
_event: gpui::FocusOutEvent,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.intercept_subscription.take();
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn keystrokes(&self) -> &[Keystroke] {
|
||||
if let Some(placeholders) = self.placeholder_keystrokes.as_ref()
|
||||
&& self.keystrokes.is_empty()
|
||||
{
|
||||
return placeholders;
|
||||
}
|
||||
if !self.search
|
||||
&& self
|
||||
.keystrokes
|
||||
.last()
|
||||
.map_or(false, |last| last.key.is_empty())
|
||||
{
|
||||
return &self.keystrokes[..self.keystrokes.len() - 1];
|
||||
}
|
||||
return &self.keystrokes;
|
||||
}
|
||||
|
||||
fn render_keystrokes(&self, is_recording: bool) -> impl Iterator<Item = Div> {
|
||||
let keystrokes = if let Some(placeholders) = self.placeholder_keystrokes.as_ref()
|
||||
&& self.keystrokes.is_empty()
|
||||
{
|
||||
if is_recording {
|
||||
&[]
|
||||
} else {
|
||||
placeholders.as_slice()
|
||||
}
|
||||
} else {
|
||||
&self.keystrokes
|
||||
};
|
||||
keystrokes.iter().map(move |keystroke| {
|
||||
h_flex().children(ui::render_keystroke(
|
||||
keystroke,
|
||||
Some(Color::Default),
|
||||
Some(rems(0.875).into()),
|
||||
ui::PlatformStyle::platform(),
|
||||
false,
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
fn start_recording(&mut self, _: &StartRecording, window: &mut Window, cx: &mut Context<Self>) {
|
||||
window.focus(&self.inner_focus_handle);
|
||||
self.clear_keystrokes(&ClearKeystrokes, window, cx);
|
||||
self.previous_modifiers = window.modifiers();
|
||||
cx.stop_propagation();
|
||||
}
|
||||
|
||||
fn stop_recording(&mut self, _: &StopRecording, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if !self.inner_focus_handle.is_focused(window) {
|
||||
return;
|
||||
}
|
||||
window.focus(&self.outer_focus_handle);
|
||||
if let Some(close_keystrokes_start) = self.close_keystrokes_start.take()
|
||||
&& close_keystrokes_start < self.keystrokes.len()
|
||||
{
|
||||
self.keystrokes.drain(close_keystrokes_start..);
|
||||
}
|
||||
self.close_keystrokes.take();
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn clear_keystrokes(
|
||||
&mut self,
|
||||
_: &ClearKeystrokes,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.keystrokes.clear();
|
||||
self.keystrokes_changed(cx);
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<()> for KeystrokeInput {}
|
||||
|
||||
impl Focusable for KeystrokeInput {
|
||||
fn focus_handle(&self, _cx: &App) -> FocusHandle {
|
||||
self.outer_focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for KeystrokeInput {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let colors = cx.theme().colors();
|
||||
let is_focused = self.outer_focus_handle.contains_focused(window, cx);
|
||||
let is_recording = self.inner_focus_handle.is_focused(window);
|
||||
|
||||
let horizontal_padding = rems_from_px(64.);
|
||||
|
||||
let recording_bg_color = colors
|
||||
.editor_background
|
||||
.blend(colors.text_accent.opacity(0.1));
|
||||
|
||||
let recording_pulse = |color: Color| {
|
||||
Icon::new(IconName::Circle)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Error)
|
||||
.with_animation(
|
||||
"recording-pulse",
|
||||
Animation::new(std::time::Duration::from_secs(2))
|
||||
.repeat()
|
||||
.with_easing(gpui::pulsating_between(0.4, 0.8)),
|
||||
{
|
||||
let color = color.color(cx);
|
||||
move |this, delta| this.color(Color::Custom(color.opacity(delta)))
|
||||
},
|
||||
)
|
||||
};
|
||||
|
||||
let recording_indicator = h_flex()
|
||||
.h_4()
|
||||
.pr_1()
|
||||
.gap_0p5()
|
||||
.border_1()
|
||||
.border_color(colors.border)
|
||||
.bg(colors
|
||||
.editor_background
|
||||
.blend(colors.text_accent.opacity(0.1)))
|
||||
.rounded_sm()
|
||||
.child(recording_pulse(Color::Error))
|
||||
.child(
|
||||
Label::new("REC")
|
||||
.size(LabelSize::XSmall)
|
||||
.weight(FontWeight::SEMIBOLD)
|
||||
.color(Color::Error),
|
||||
);
|
||||
|
||||
let search_indicator = h_flex()
|
||||
.h_4()
|
||||
.pr_1()
|
||||
.gap_0p5()
|
||||
.border_1()
|
||||
.border_color(colors.border)
|
||||
.bg(colors
|
||||
.editor_background
|
||||
.blend(colors.text_accent.opacity(0.1)))
|
||||
.rounded_sm()
|
||||
.child(recording_pulse(Color::Accent))
|
||||
.child(
|
||||
Label::new("SEARCH")
|
||||
.size(LabelSize::XSmall)
|
||||
.weight(FontWeight::SEMIBOLD)
|
||||
.color(Color::Accent),
|
||||
);
|
||||
|
||||
let record_icon = if self.search {
|
||||
IconName::MagnifyingGlass
|
||||
} else {
|
||||
IconName::PlayFilled
|
||||
};
|
||||
|
||||
h_flex()
|
||||
.id("keystroke-input")
|
||||
.track_focus(&self.outer_focus_handle)
|
||||
.py_2()
|
||||
.px_3()
|
||||
.gap_2()
|
||||
.min_h_10()
|
||||
.w_full()
|
||||
.flex_1()
|
||||
.justify_between()
|
||||
.rounded_lg()
|
||||
.overflow_hidden()
|
||||
.map(|this| {
|
||||
if is_recording {
|
||||
this.bg(recording_bg_color)
|
||||
} else {
|
||||
this.bg(colors.editor_background)
|
||||
}
|
||||
})
|
||||
.border_1()
|
||||
.border_color(colors.border_variant)
|
||||
.when(is_focused, |parent| {
|
||||
parent.border_color(colors.border_focused)
|
||||
})
|
||||
.key_context(Self::key_context())
|
||||
.on_action(cx.listener(Self::start_recording))
|
||||
.on_action(cx.listener(Self::clear_keystrokes))
|
||||
.child(
|
||||
h_flex()
|
||||
.w(horizontal_padding)
|
||||
.gap_0p5()
|
||||
.justify_start()
|
||||
.flex_none()
|
||||
.when(is_recording, |this| {
|
||||
this.map(|this| {
|
||||
if self.search {
|
||||
this.child(search_indicator)
|
||||
} else {
|
||||
this.child(recording_indicator)
|
||||
}
|
||||
})
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.id("keystroke-input-inner")
|
||||
.track_focus(&self.inner_focus_handle)
|
||||
.on_modifiers_changed(cx.listener(Self::on_modifiers_changed))
|
||||
.size_full()
|
||||
.when(!self.search, |this| {
|
||||
this.focus(|mut style| {
|
||||
style.border_color = Some(colors.border_focused);
|
||||
style
|
||||
})
|
||||
})
|
||||
.w_full()
|
||||
.min_w_0()
|
||||
.justify_center()
|
||||
.flex_wrap()
|
||||
.gap(ui::DynamicSpacing::Base04.rems(cx))
|
||||
.children(self.render_keystrokes(is_recording)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.w(horizontal_padding)
|
||||
.gap_0p5()
|
||||
.justify_end()
|
||||
.flex_none()
|
||||
.map(|this| {
|
||||
if is_recording {
|
||||
this.child(
|
||||
IconButton::new("stop-record-btn", IconName::StopFilled)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.map(|this| {
|
||||
this.tooltip(Tooltip::for_action_title(
|
||||
if self.search {
|
||||
"Stop Searching"
|
||||
} else {
|
||||
"Stop Recording"
|
||||
},
|
||||
&StopRecording,
|
||||
))
|
||||
})
|
||||
.icon_color(Color::Error)
|
||||
.on_click(cx.listener(|this, _event, window, cx| {
|
||||
this.stop_recording(&StopRecording, window, cx);
|
||||
})),
|
||||
)
|
||||
} else {
|
||||
this.child(
|
||||
IconButton::new("record-btn", record_icon)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.map(|this| {
|
||||
this.tooltip(Tooltip::for_action_title(
|
||||
if self.search {
|
||||
"Start Searching"
|
||||
} else {
|
||||
"Start Recording"
|
||||
},
|
||||
&StartRecording,
|
||||
))
|
||||
})
|
||||
.when(!is_focused, |this| this.icon_color(Color::Muted))
|
||||
.on_click(cx.listener(|this, _event, window, cx| {
|
||||
this.start_recording(&StartRecording, window, cx);
|
||||
})),
|
||||
)
|
||||
}
|
||||
})
|
||||
.child(
|
||||
IconButton::new("clear-btn", IconName::Delete)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.tooltip(Tooltip::for_action_title(
|
||||
"Clear Keystrokes",
|
||||
&ClearKeystrokes,
|
||||
))
|
||||
.when(!is_recording || !is_focused, |this| {
|
||||
this.icon_color(Color::Muted)
|
||||
})
|
||||
.on_click(cx.listener(|this, _event, window, cx| {
|
||||
this.clear_keystrokes(&ClearKeystrokes, window, cx);
|
||||
})),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_contexts_from_assets() -> Vec<SharedString> {
|
||||
let mut keymap_assets = vec![
|
||||
util::asset_str::<SettingsAssets>(settings::DEFAULT_KEYMAP_PATH),
|
||||
|
|
1388
crates/settings_ui/src/ui_components/keystroke_input.rs
Normal file
1388
crates/settings_ui/src/ui_components/keystroke_input.rs
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1 +1,2 @@
|
|||
pub mod keystroke_input;
|
||||
pub mod table;
|
||||
|
|
|
@ -291,14 +291,18 @@ impl Component for ToggleButton {
|
|||
}
|
||||
}
|
||||
|
||||
mod private {
|
||||
pub trait Sealed {}
|
||||
pub struct ButtonConfiguration {
|
||||
label: SharedString,
|
||||
icon: Option<IconName>,
|
||||
on_click: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
|
||||
}
|
||||
|
||||
pub trait ButtonBuilder: 'static + private::Sealed {
|
||||
fn label(&self) -> impl Into<SharedString>;
|
||||
fn icon(&self) -> Option<IconName>;
|
||||
fn on_click(self) -> Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>;
|
||||
mod private {
|
||||
pub trait ToggleButtonStyle {}
|
||||
}
|
||||
|
||||
pub trait ButtonBuilder: 'static + private::ToggleButtonStyle {
|
||||
fn into_configuration(self) -> ButtonConfiguration;
|
||||
}
|
||||
|
||||
pub struct ToggleButtonSimple {
|
||||
|
@ -318,19 +322,15 @@ impl ToggleButtonSimple {
|
|||
}
|
||||
}
|
||||
|
||||
impl private::Sealed for ToggleButtonSimple {}
|
||||
impl private::ToggleButtonStyle for ToggleButtonSimple {}
|
||||
|
||||
impl ButtonBuilder for ToggleButtonSimple {
|
||||
fn label(&self) -> impl Into<SharedString> {
|
||||
self.label.clone()
|
||||
}
|
||||
|
||||
fn icon(&self) -> Option<IconName> {
|
||||
None
|
||||
}
|
||||
|
||||
fn on_click(self) -> Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static> {
|
||||
self.on_click
|
||||
fn into_configuration(self) -> ButtonConfiguration {
|
||||
ButtonConfiguration {
|
||||
label: self.label,
|
||||
icon: None,
|
||||
on_click: self.on_click,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -354,58 +354,14 @@ impl ToggleButtonWithIcon {
|
|||
}
|
||||
}
|
||||
|
||||
impl private::Sealed for ToggleButtonWithIcon {}
|
||||
impl private::ToggleButtonStyle for ToggleButtonWithIcon {}
|
||||
|
||||
impl ButtonBuilder for ToggleButtonWithIcon {
|
||||
fn label(&self) -> impl Into<SharedString> {
|
||||
self.label.clone()
|
||||
}
|
||||
|
||||
fn icon(&self) -> Option<IconName> {
|
||||
Some(self.icon)
|
||||
}
|
||||
|
||||
fn on_click(self) -> Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static> {
|
||||
self.on_click
|
||||
}
|
||||
}
|
||||
|
||||
struct ToggleButtonRow<T: ButtonBuilder> {
|
||||
items: Vec<T>,
|
||||
index_offset: usize,
|
||||
last_item_idx: usize,
|
||||
is_last_row: bool,
|
||||
}
|
||||
|
||||
impl<T: ButtonBuilder> ToggleButtonRow<T> {
|
||||
fn new(items: Vec<T>, index_offset: usize, is_last_row: bool) -> Self {
|
||||
Self {
|
||||
index_offset,
|
||||
last_item_idx: index_offset + items.len() - 1,
|
||||
is_last_row,
|
||||
items,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum ToggleButtonGroupRows<T: ButtonBuilder> {
|
||||
Single(Vec<T>),
|
||||
Multiple(Vec<T>, Vec<T>),
|
||||
}
|
||||
|
||||
impl<T: ButtonBuilder> ToggleButtonGroupRows<T> {
|
||||
fn items(self) -> impl IntoIterator<Item = ToggleButtonRow<T>> {
|
||||
match self {
|
||||
ToggleButtonGroupRows::Single(items) => {
|
||||
vec![ToggleButtonRow::new(items, 0, true)]
|
||||
}
|
||||
ToggleButtonGroupRows::Multiple(first_row, second_row) => {
|
||||
let row_len = first_row.len();
|
||||
vec![
|
||||
ToggleButtonRow::new(first_row, 0, false),
|
||||
ToggleButtonRow::new(second_row, row_len, true),
|
||||
]
|
||||
}
|
||||
fn into_configuration(self) -> ButtonConfiguration {
|
||||
ButtonConfiguration {
|
||||
label: self.label,
|
||||
icon: Some(self.icon),
|
||||
on_click: self.on_click,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -418,48 +374,42 @@ pub enum ToggleButtonGroupStyle {
|
|||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct ToggleButtonGroup<T>
|
||||
pub struct ToggleButtonGroup<T, const COLS: usize = 3, const ROWS: usize = 1>
|
||||
where
|
||||
T: ButtonBuilder,
|
||||
{
|
||||
group_name: SharedString,
|
||||
rows: ToggleButtonGroupRows<T>,
|
||||
group_name: &'static str,
|
||||
rows: [[T; COLS]; ROWS],
|
||||
style: ToggleButtonGroupStyle,
|
||||
button_width: Rems,
|
||||
selected_index: usize,
|
||||
}
|
||||
|
||||
impl<T: ButtonBuilder> ToggleButtonGroup<T> {
|
||||
pub fn single_row(
|
||||
group_name: impl Into<SharedString>,
|
||||
buttons: impl IntoIterator<Item = T>,
|
||||
) -> Self {
|
||||
impl<T: ButtonBuilder, const COLS: usize> ToggleButtonGroup<T, COLS> {
|
||||
pub fn single_row(group_name: &'static str, buttons: [T; COLS]) -> Self {
|
||||
Self {
|
||||
group_name: group_name.into(),
|
||||
rows: ToggleButtonGroupRows::Single(Vec::from_iter(buttons)),
|
||||
group_name,
|
||||
rows: [buttons],
|
||||
style: ToggleButtonGroupStyle::Transparent,
|
||||
button_width: rems_from_px(100.),
|
||||
selected_index: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn multiple_rows<const ROWS: usize>(
|
||||
group_name: impl Into<SharedString>,
|
||||
first_row: [T; ROWS],
|
||||
second_row: [T; ROWS],
|
||||
) -> Self {
|
||||
impl<T: ButtonBuilder, const COLS: usize> ToggleButtonGroup<T, COLS, 2> {
|
||||
pub fn two_rows(group_name: &'static str, first_row: [T; COLS], second_row: [T; COLS]) -> Self {
|
||||
Self {
|
||||
group_name: group_name.into(),
|
||||
rows: ToggleButtonGroupRows::Multiple(
|
||||
Vec::from_iter(first_row),
|
||||
Vec::from_iter(second_row),
|
||||
),
|
||||
group_name,
|
||||
rows: [first_row, second_row],
|
||||
style: ToggleButtonGroupStyle::Transparent,
|
||||
button_width: rems_from_px(100.),
|
||||
selected_index: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ButtonBuilder, const COLS: usize, const ROWS: usize> ToggleButtonGroup<T, COLS, ROWS> {
|
||||
pub fn style(mut self, style: ToggleButtonGroupStyle) -> Self {
|
||||
self.style = style;
|
||||
self
|
||||
|
@ -476,60 +426,56 @@ impl<T: ButtonBuilder> ToggleButtonGroup<T> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<T: ButtonBuilder> RenderOnce for ToggleButtonGroup<T> {
|
||||
impl<T: ButtonBuilder, const COLS: usize, const ROWS: usize> RenderOnce
|
||||
for ToggleButtonGroup<T, COLS, ROWS>
|
||||
{
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let rows = self.rows.items().into_iter().map(|row| {
|
||||
(
|
||||
row.items
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(move |(index, item)| (index + row.index_offset, row.last_item_idx, item))
|
||||
.map(|(index, last_item_idx, item)| {
|
||||
(
|
||||
ButtonLike::new((self.group_name.clone(), index))
|
||||
.when(index == self.selected_index, |this| {
|
||||
this.toggle_state(true)
|
||||
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
})
|
||||
.rounding(None)
|
||||
.when(self.style == ToggleButtonGroupStyle::Filled, |button| {
|
||||
button.style(ButtonStyle::Filled)
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.min_w(self.button_width)
|
||||
.gap_1p5()
|
||||
.justify_center()
|
||||
.when_some(item.icon(), |this, icon| {
|
||||
this.child(Icon::new(icon).size(IconSize::XSmall).map(
|
||||
|this| {
|
||||
if index == self.selected_index {
|
||||
this.color(Color::Accent)
|
||||
} else {
|
||||
this.color(Color::Muted)
|
||||
}
|
||||
},
|
||||
))
|
||||
})
|
||||
.child(
|
||||
Label::new(item.label())
|
||||
.when(index == self.selected_index, |this| {
|
||||
this.color(Color::Accent)
|
||||
}),
|
||||
),
|
||||
)
|
||||
.on_click(item.on_click()),
|
||||
index == last_item_idx,
|
||||
)
|
||||
}),
|
||||
row.is_last_row,
|
||||
)
|
||||
let entries = self.rows.into_iter().enumerate().map(|(row_index, row)| {
|
||||
row.into_iter().enumerate().map(move |(index, button)| {
|
||||
let ButtonConfiguration {
|
||||
label,
|
||||
icon,
|
||||
on_click,
|
||||
} = button.into_configuration();
|
||||
|
||||
ButtonLike::new((self.group_name, row_index * COLS + index))
|
||||
.when(index == self.selected_index, |this| {
|
||||
this.toggle_state(true)
|
||||
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
})
|
||||
.rounding(None)
|
||||
.when(self.style == ToggleButtonGroupStyle::Filled, |button| {
|
||||
button.style(ButtonStyle::Filled)
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.min_w(self.button_width)
|
||||
.gap_1p5()
|
||||
.justify_center()
|
||||
.when_some(icon, |this, icon| {
|
||||
this.child(Icon::new(icon).size(IconSize::XSmall).map(|this| {
|
||||
if index == self.selected_index {
|
||||
this.color(Color::Accent)
|
||||
} else {
|
||||
this.color(Color::Muted)
|
||||
}
|
||||
}))
|
||||
})
|
||||
.child(
|
||||
Label::new(label).when(index == self.selected_index, |this| {
|
||||
this.color(Color::Accent)
|
||||
}),
|
||||
),
|
||||
)
|
||||
.on_click(on_click)
|
||||
.into_any_element()
|
||||
})
|
||||
});
|
||||
|
||||
let border_color = cx.theme().colors().border.opacity(0.6);
|
||||
let is_outlined_or_filled = self.style == ToggleButtonGroupStyle::Outlined
|
||||
|| self.style == ToggleButtonGroupStyle::Filled;
|
||||
let is_transparent = self.style == ToggleButtonGroupStyle::Transparent;
|
||||
let border_color = cx.theme().colors().border.opacity(0.6);
|
||||
|
||||
v_flex()
|
||||
.rounded_md()
|
||||
|
@ -541,13 +487,15 @@ impl<T: ButtonBuilder> RenderOnce for ToggleButtonGroup<T> {
|
|||
this.border_1().border_color(border_color)
|
||||
}
|
||||
})
|
||||
.children(rows.map(|(items, last_row)| {
|
||||
.children(entries.enumerate().map(|(row_index, row)| {
|
||||
let last_row = row_index == ROWS - 1;
|
||||
h_flex()
|
||||
.when(!is_outlined_or_filled, |this| this.gap_px())
|
||||
.when(is_outlined_or_filled && !last_row, |this| {
|
||||
this.border_b_1().border_color(border_color)
|
||||
})
|
||||
.children(items.map(|(item, last_item)| {
|
||||
.children(row.enumerate().map(|(item_index, item)| {
|
||||
let last_item = item_index == COLS - 1;
|
||||
div()
|
||||
.when(is_outlined_or_filled && !last_item, |this| {
|
||||
this.border_r_1().border_color(border_color)
|
||||
|
@ -566,7 +514,9 @@ component::__private::inventory::submit! {
|
|||
component::ComponentFn::new(register_toggle_button_group)
|
||||
}
|
||||
|
||||
impl<T: ButtonBuilder> Component for ToggleButtonGroup<T> {
|
||||
impl<T: ButtonBuilder, const COLS: usize, const ROWS: usize> Component
|
||||
for ToggleButtonGroup<T, COLS, ROWS>
|
||||
{
|
||||
fn name() -> &'static str {
|
||||
"ToggleButtonGroup"
|
||||
}
|
||||
|
@ -628,7 +578,7 @@ impl<T: ButtonBuilder> Component for ToggleButtonGroup<T> {
|
|||
),
|
||||
single_example(
|
||||
"Multiple Row Group",
|
||||
ToggleButtonGroup::multiple_rows(
|
||||
ToggleButtonGroup::two_rows(
|
||||
"multiple_row_test",
|
||||
[
|
||||
ToggleButtonSimple::new("First", |_, _, _| {}),
|
||||
|
@ -647,7 +597,7 @@ impl<T: ButtonBuilder> Component for ToggleButtonGroup<T> {
|
|||
),
|
||||
single_example(
|
||||
"Multiple Row Group with Icons",
|
||||
ToggleButtonGroup::multiple_rows(
|
||||
ToggleButtonGroup::two_rows(
|
||||
"multiple_row_test_icons",
|
||||
[
|
||||
ToggleButtonWithIcon::new(
|
||||
|
@ -736,7 +686,7 @@ impl<T: ButtonBuilder> Component for ToggleButtonGroup<T> {
|
|||
),
|
||||
single_example(
|
||||
"Multiple Row Group",
|
||||
ToggleButtonGroup::multiple_rows(
|
||||
ToggleButtonGroup::two_rows(
|
||||
"multiple_row_test",
|
||||
[
|
||||
ToggleButtonSimple::new("First", |_, _, _| {}),
|
||||
|
@ -756,7 +706,7 @@ impl<T: ButtonBuilder> Component for ToggleButtonGroup<T> {
|
|||
),
|
||||
single_example(
|
||||
"Multiple Row Group with Icons",
|
||||
ToggleButtonGroup::multiple_rows(
|
||||
ToggleButtonGroup::two_rows(
|
||||
"multiple_row_test",
|
||||
[
|
||||
ToggleButtonWithIcon::new(
|
||||
|
@ -846,7 +796,7 @@ impl<T: ButtonBuilder> Component for ToggleButtonGroup<T> {
|
|||
),
|
||||
single_example(
|
||||
"Multiple Row Group",
|
||||
ToggleButtonGroup::multiple_rows(
|
||||
ToggleButtonGroup::two_rows(
|
||||
"multiple_row_test",
|
||||
[
|
||||
ToggleButtonSimple::new("First", |_, _, _| {}),
|
||||
|
@ -866,7 +816,7 @@ impl<T: ButtonBuilder> Component for ToggleButtonGroup<T> {
|
|||
),
|
||||
single_example(
|
||||
"Multiple Row Group with Icons",
|
||||
ToggleButtonGroup::multiple_rows(
|
||||
ToggleButtonGroup::two_rows(
|
||||
"multiple_row_test",
|
||||
[
|
||||
ToggleButtonWithIcon::new(
|
||||
|
|
|
@ -2,7 +2,7 @@ use gpui::ClickEvent;
|
|||
|
||||
use crate::{IconButtonShape, prelude::*};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
#[derive(IntoElement, RegisterComponent)]
|
||||
pub struct NumericStepper {
|
||||
id: ElementId,
|
||||
value: SharedString,
|
||||
|
@ -93,3 +93,34 @@ impl RenderOnce for NumericStepper {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for NumericStepper {
|
||||
fn scope() -> ComponentScope {
|
||||
ComponentScope::Input
|
||||
}
|
||||
|
||||
fn name() -> &'static str {
|
||||
"NumericStepper"
|
||||
}
|
||||
|
||||
fn sort_name() -> &'static str {
|
||||
Self::name()
|
||||
}
|
||||
|
||||
fn description() -> Option<&'static str> {
|
||||
Some("A button used to increment or decrement a numeric value. ")
|
||||
}
|
||||
|
||||
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
|
||||
Some(
|
||||
div()
|
||||
.child(NumericStepper::new(
|
||||
"numeric-stepper-component-preview",
|
||||
"10",
|
||||
move |_, _, _| {},
|
||||
move |_, _, _| {},
|
||||
))
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -109,7 +109,7 @@ impl Component for Animation {
|
|||
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
|
||||
let container_size = 128.0;
|
||||
let element_size = 32.0;
|
||||
let left_offset = element_size - container_size / 2.0;
|
||||
let offset = container_size / 2.0 - element_size / 2.0;
|
||||
Some(
|
||||
v_flex()
|
||||
.gap_6()
|
||||
|
@ -129,7 +129,7 @@ impl Component for Animation {
|
|||
.id("animate-in-from-bottom")
|
||||
.absolute()
|
||||
.size(px(element_size))
|
||||
.left(px(left_offset))
|
||||
.left(px(offset))
|
||||
.rounded_md()
|
||||
.bg(gpui::red())
|
||||
.animate_in(AnimationDirection::FromBottom, false),
|
||||
|
@ -148,7 +148,7 @@ impl Component for Animation {
|
|||
.id("animate-in-from-top")
|
||||
.absolute()
|
||||
.size(px(element_size))
|
||||
.left(px(left_offset))
|
||||
.left(px(offset))
|
||||
.rounded_md()
|
||||
.bg(gpui::blue())
|
||||
.animate_in(AnimationDirection::FromTop, false),
|
||||
|
@ -167,7 +167,7 @@ impl Component for Animation {
|
|||
.id("animate-in-from-left")
|
||||
.absolute()
|
||||
.size(px(element_size))
|
||||
.left(px(left_offset))
|
||||
.top(px(offset))
|
||||
.rounded_md()
|
||||
.bg(gpui::green())
|
||||
.animate_in(AnimationDirection::FromLeft, false),
|
||||
|
@ -186,7 +186,7 @@ impl Component for Animation {
|
|||
.id("animate-in-from-right")
|
||||
.absolute()
|
||||
.size(px(element_size))
|
||||
.left(px(left_offset))
|
||||
.top(px(offset))
|
||||
.rounded_md()
|
||||
.bg(gpui::yellow())
|
||||
.animate_in(AnimationDirection::FromRight, false),
|
||||
|
@ -211,7 +211,7 @@ impl Component for Animation {
|
|||
.id("fade-animate-in-from-bottom")
|
||||
.absolute()
|
||||
.size(px(element_size))
|
||||
.left(px(left_offset))
|
||||
.left(px(offset))
|
||||
.rounded_md()
|
||||
.bg(gpui::red())
|
||||
.animate_in(AnimationDirection::FromBottom, true),
|
||||
|
@ -230,7 +230,7 @@ impl Component for Animation {
|
|||
.id("fade-animate-in-from-top")
|
||||
.absolute()
|
||||
.size(px(element_size))
|
||||
.left(px(left_offset))
|
||||
.left(px(offset))
|
||||
.rounded_md()
|
||||
.bg(gpui::blue())
|
||||
.animate_in(AnimationDirection::FromTop, true),
|
||||
|
@ -249,7 +249,7 @@ impl Component for Animation {
|
|||
.id("fade-animate-in-from-left")
|
||||
.absolute()
|
||||
.size(px(element_size))
|
||||
.left(px(left_offset))
|
||||
.top(px(offset))
|
||||
.rounded_md()
|
||||
.bg(gpui::green())
|
||||
.animate_in(AnimationDirection::FromLeft, true),
|
||||
|
@ -268,7 +268,7 @@ impl Component for Animation {
|
|||
.id("fade-animate-in-from-right")
|
||||
.absolute()
|
||||
.size(px(element_size))
|
||||
.left(px(left_offset))
|
||||
.top(px(offset))
|
||||
.rounded_md()
|
||||
.bg(gpui::yellow())
|
||||
.animate_in(AnimationDirection::FromRight, true),
|
||||
|
|
|
@ -13,8 +13,8 @@ path = "src/web_search.rs"
|
|||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
cloud_llm_client.workspace = true
|
||||
collections.workspace = true
|
||||
gpui.workspace = true
|
||||
serde.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
zed_llm_client.workspace = true
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use cloud_llm_client::WebSearchResponse;
|
||||
use collections::HashMap;
|
||||
use gpui::{App, AppContext as _, Context, Entity, Global, SharedString, Task};
|
||||
use std::sync::Arc;
|
||||
use zed_llm_client::WebSearchResponse;
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
let registry = cx.new(|_cx| WebSearchRegistry::default());
|
||||
|
|
|
@ -14,6 +14,7 @@ path = "src/web_search_providers.rs"
|
|||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
client.workspace = true
|
||||
cloud_llm_client.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
http_client.workspace = true
|
||||
|
@ -22,4 +23,3 @@ serde.workspace = true
|
|||
serde_json.workspace = true
|
||||
web_search.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
zed_llm_client.workspace = true
|
||||
|
|
|
@ -2,12 +2,12 @@ use std::sync::Arc;
|
|||
|
||||
use anyhow::{Context as _, Result};
|
||||
use client::Client;
|
||||
use cloud_llm_client::{EXPIRED_LLM_TOKEN_HEADER_NAME, WebSearchBody, WebSearchResponse};
|
||||
use futures::AsyncReadExt as _;
|
||||
use gpui::{App, AppContext, Context, Entity, Subscription, Task};
|
||||
use http_client::{HttpClient, Method};
|
||||
use language_model::{LlmApiToken, RefreshLlmTokenListener};
|
||||
use web_search::{WebSearchProvider, WebSearchProviderId};
|
||||
use zed_llm_client::{EXPIRED_LLM_TOKEN_HEADER_NAME, WebSearchBody, WebSearchResponse};
|
||||
|
||||
pub struct CloudWebSearchProvider {
|
||||
state: Entity<State>,
|
||||
|
|
|
@ -1065,7 +1065,6 @@ pub struct Workspace {
|
|||
center: PaneGroup,
|
||||
left_dock: Entity<Dock>,
|
||||
bottom_dock: Entity<Dock>,
|
||||
bottom_dock_layout: BottomDockLayout,
|
||||
right_dock: Entity<Dock>,
|
||||
panes: Vec<Entity<Pane>>,
|
||||
panes_by_item: HashMap<EntityId, WeakEntity<Pane>>,
|
||||
|
@ -1307,7 +1306,6 @@ impl Workspace {
|
|||
)
|
||||
.detach();
|
||||
|
||||
let bottom_dock_layout = WorkspaceSettings::get_global(cx).bottom_dock_layout;
|
||||
let left_dock = Dock::new(DockPosition::Left, modal_layer.clone(), window, cx);
|
||||
let bottom_dock = Dock::new(DockPosition::Bottom, modal_layer.clone(), window, cx);
|
||||
let right_dock = Dock::new(DockPosition::Right, modal_layer.clone(), window, cx);
|
||||
|
@ -1406,7 +1404,6 @@ impl Workspace {
|
|||
suppressed_notifications: HashSet::default(),
|
||||
left_dock,
|
||||
bottom_dock,
|
||||
bottom_dock_layout,
|
||||
right_dock,
|
||||
project: project.clone(),
|
||||
follower_states: Default::default(),
|
||||
|
@ -1633,10 +1630,6 @@ impl Workspace {
|
|||
&self.bottom_dock
|
||||
}
|
||||
|
||||
pub fn bottom_dock_layout(&self) -> BottomDockLayout {
|
||||
self.bottom_dock_layout
|
||||
}
|
||||
|
||||
pub fn set_bottom_dock_layout(
|
||||
&mut self,
|
||||
layout: BottomDockLayout,
|
||||
|
@ -1648,7 +1641,6 @@ impl Workspace {
|
|||
content.bottom_dock_layout = Some(layout);
|
||||
});
|
||||
|
||||
self.bottom_dock_layout = layout;
|
||||
cx.notify();
|
||||
self.serialize_workspace(window, cx);
|
||||
}
|
||||
|
@ -6246,6 +6238,7 @@ impl Render for Workspace {
|
|||
.iter()
|
||||
.map(|(_, notification)| notification.entity_id())
|
||||
.collect::<Vec<_>>();
|
||||
let bottom_dock_layout = WorkspaceSettings::get_global(cx).bottom_dock_layout;
|
||||
|
||||
client_side_decorations(
|
||||
self.actions(div(), window, cx)
|
||||
|
@ -6369,7 +6362,7 @@ impl Render for Workspace {
|
|||
))
|
||||
})
|
||||
.child({
|
||||
match self.bottom_dock_layout {
|
||||
match bottom_dock_layout {
|
||||
BottomDockLayout::Full => div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
|
|
|
@ -1245,16 +1245,6 @@ Root: HKCU; Subkey: "Software\Classes\zed\DefaultIcon"; ValueType: "string"; Val
|
|||
Root: HKCU; Subkey: "Software\Classes\zed\shell\open\command"; ValueType: "string"; ValueData: """{app}\Zed.exe"" ""%1"""
|
||||
|
||||
[Code]
|
||||
function InitializeSetup(): Boolean;
|
||||
begin
|
||||
Result := True;
|
||||
|
||||
if not WizardSilent() and IsAdmin() then begin
|
||||
MsgBox('This User Installer is not meant to be run as an Administrator.', mbError, MB_OK);
|
||||
Result := False;
|
||||
end;
|
||||
end;
|
||||
|
||||
function WizardNotSilent(): Boolean;
|
||||
begin
|
||||
Result := not WizardSilent();
|
||||
|
|
|
@ -63,7 +63,7 @@ pub fn init_panic_hook(
|
|||
location.column(),
|
||||
match app_commit_sha.as_ref() {
|
||||
Some(commit_sha) => format!(
|
||||
"https://github.com/zed-industries/zed/blob/{}/src/{}#L{} \
|
||||
"https://github.com/zed-industries/zed/blob/{}/{}#L{} \
|
||||
(may not be uploaded, line may be incorrect if files modified)\n",
|
||||
commit_sha.full(),
|
||||
location.file(),
|
||||
|
|
|
@ -105,6 +105,7 @@ enum PreviewPage {
|
|||
struct ComponentPreview {
|
||||
active_page: PreviewPage,
|
||||
active_thread: Option<Entity<ActiveThread>>,
|
||||
reset_key: usize,
|
||||
component_list: ListState,
|
||||
component_map: HashMap<ComponentId, ComponentMetadata>,
|
||||
components: Vec<ComponentMetadata>,
|
||||
|
@ -188,6 +189,7 @@ impl ComponentPreview {
|
|||
let mut component_preview = Self {
|
||||
active_page,
|
||||
active_thread: None,
|
||||
reset_key: 0,
|
||||
component_list,
|
||||
component_map: component_registry.component_map(),
|
||||
components: sorted_components,
|
||||
|
@ -265,8 +267,13 @@ impl ComponentPreview {
|
|||
}
|
||||
|
||||
fn set_active_page(&mut self, page: PreviewPage, cx: &mut Context<Self>) {
|
||||
self.active_page = page;
|
||||
cx.emit(ItemEvent::UpdateTab);
|
||||
if self.active_page == page {
|
||||
// Force the current preview page to render again
|
||||
self.reset_key = self.reset_key.wrapping_add(1);
|
||||
} else {
|
||||
self.active_page = page;
|
||||
cx.emit(ItemEvent::UpdateTab);
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
|
@ -690,6 +697,7 @@ impl ComponentPreview {
|
|||
component.clone(),
|
||||
self.workspace.clone(),
|
||||
self.active_thread.clone(),
|
||||
self.reset_key,
|
||||
))
|
||||
.into_any_element()
|
||||
} else {
|
||||
|
@ -1041,6 +1049,7 @@ pub struct ComponentPreviewPage {
|
|||
component: ComponentMetadata,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
active_thread: Option<Entity<ActiveThread>>,
|
||||
reset_key: usize,
|
||||
}
|
||||
|
||||
impl ComponentPreviewPage {
|
||||
|
@ -1048,6 +1057,7 @@ impl ComponentPreviewPage {
|
|||
component: ComponentMetadata,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
active_thread: Option<Entity<ActiveThread>>,
|
||||
reset_key: usize,
|
||||
// languages: Arc<LanguageRegistry>
|
||||
) -> Self {
|
||||
Self {
|
||||
|
@ -1055,6 +1065,7 @@ impl ComponentPreviewPage {
|
|||
component,
|
||||
workspace,
|
||||
active_thread,
|
||||
reset_key,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1155,6 +1166,7 @@ impl ComponentPreviewPage {
|
|||
};
|
||||
|
||||
v_flex()
|
||||
.id(("component-preview", self.reset_key))
|
||||
.size_full()
|
||||
.flex_1()
|
||||
.px_12()
|
||||
|
|
|
@ -21,6 +21,7 @@ ai_onboarding.workspace = true
|
|||
anyhow.workspace = true
|
||||
arrayvec.workspace = true
|
||||
client.workspace = true
|
||||
cloud_llm_client.workspace = true
|
||||
collections.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
copilot.workspace = true
|
||||
|
@ -52,11 +53,10 @@ thiserror.workspace = true
|
|||
ui.workspace = true
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
workspace.workspace = true
|
||||
worktree.workspace = true
|
||||
zed_actions.workspace = true
|
||||
zed_llm_client.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
collections = { workspace = true, features = ["test-support"] }
|
||||
|
|
|
@ -17,6 +17,10 @@ pub use rate_completion_modal::*;
|
|||
use anyhow::{Context as _, Result, anyhow};
|
||||
use arrayvec::ArrayVec;
|
||||
use client::{Client, EditPredictionUsage, UserStore};
|
||||
use cloud_llm_client::{
|
||||
AcceptEditPredictionBody, EXPIRED_LLM_TOKEN_HEADER_NAME, MINIMUM_REQUIRED_VERSION_HEADER_NAME,
|
||||
PredictEditsBody, PredictEditsResponse, ZED_VERSION_HEADER_NAME,
|
||||
};
|
||||
use collections::{HashMap, HashSet, VecDeque};
|
||||
use futures::AsyncReadExt;
|
||||
use gpui::{
|
||||
|
@ -53,10 +57,6 @@ use uuid::Uuid;
|
|||
use workspace::Workspace;
|
||||
use workspace::notifications::{ErrorMessagePrompt, NotificationId};
|
||||
use worktree::Worktree;
|
||||
use zed_llm_client::{
|
||||
AcceptEditPredictionBody, EXPIRED_LLM_TOKEN_HEADER_NAME, MINIMUM_REQUIRED_VERSION_HEADER_NAME,
|
||||
PredictEditsBody, PredictEditsResponse, ZED_VERSION_HEADER_NAME,
|
||||
};
|
||||
|
||||
const CURSOR_MARKER: &'static str = "<|user_cursor_is_here|>";
|
||||
const START_OF_FILE_MARKER: &'static str = "<|start_of_file|>";
|
||||
|
|
|
@ -21,6 +21,8 @@ const ANSI_MAGENTA: &str = "\x1b[35m";
|
|||
|
||||
/// Whether stdout output is enabled.
|
||||
static mut ENABLED_SINKS_STDOUT: bool = false;
|
||||
/// Whether stderr output is enabled.
|
||||
static mut ENABLED_SINKS_STDERR: bool = false;
|
||||
|
||||
/// Is Some(file) if file output is enabled.
|
||||
static ENABLED_SINKS_FILE: Mutex<Option<std::fs::File>> = Mutex::new(None);
|
||||
|
@ -45,6 +47,12 @@ pub fn init_output_stdout() {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn init_output_stderr() {
|
||||
unsafe {
|
||||
ENABLED_SINKS_STDERR = true;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_output_file(
|
||||
path: &'static PathBuf,
|
||||
path_rotate: Option<&'static PathBuf>,
|
||||
|
@ -115,6 +123,21 @@ pub fn submit(record: Record) {
|
|||
},
|
||||
record.message
|
||||
);
|
||||
} else if unsafe { ENABLED_SINKS_STDERR } {
|
||||
let mut stdout = std::io::stderr().lock();
|
||||
_ = writeln!(
|
||||
&mut stdout,
|
||||
"{} {ANSI_BOLD}{}{}{ANSI_RESET} {} {}",
|
||||
chrono::Local::now().format("%Y-%m-%dT%H:%M:%S%:z"),
|
||||
LEVEL_ANSI_COLORS[record.level as usize],
|
||||
LEVEL_OUTPUT_STRINGS[record.level as usize],
|
||||
SourceFmt {
|
||||
scope: record.scope,
|
||||
module_path: record.module_path,
|
||||
ansi: true,
|
||||
},
|
||||
record.message
|
||||
);
|
||||
}
|
||||
let mut file = ENABLED_SINKS_FILE.lock().unwrap_or_else(|handle| {
|
||||
ENABLED_SINKS_FILE.clear_poison();
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue