Merge branch 'main' into mcp-acp-gemini

This commit is contained in:
Ben Brandt 2025-07-30 12:24:01 +02:00
commit f028ca4d1a
No known key found for this signature in database
GPG key ID: D4618C5D3B500571
115 changed files with 3523 additions and 1437 deletions

View file

@ -1594,6 +1594,8 @@ mod tests {
name: "test",
connection,
child_status: io_task,
current_thread: thread_rc,
agent_state: Default::default(),
};
AcpThread::new(

View file

@ -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
})
})

View file

@ -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]

View file

@ -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());

View file

@ -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?;

View file

@ -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,
}
}

View file

@ -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()
});

View file

@ -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 _)
})
}

View file

@ -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

View file

@ -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,
}
}
}

View file

@ -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

View file

@ -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;

View file

@ -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";

View file

@ -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>>,

View file

@ -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
///

View file

@ -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()?;

View file

@ -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>,

View file

@ -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 {

View file

@ -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

View file

@ -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::*;

View file

@ -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"] }

View file

@ -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 {

View file

@ -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 {

View file

@ -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

View file

@ -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());

View file

@ -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();
});
}

View file

@ -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"] }

View file

@ -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)?);
}

View file

@ -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;

View 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

View file

@ -0,0 +1 @@
../../LICENSE-APACHE

View 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());
}
}
}

View file

@ -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

View file

@ -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(
&params.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 {})
}
}),

View file

@ -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();

View file

@ -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

View file

@ -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,

View file

@ -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;

View file

@ -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;

View file

@ -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")]

View file

@ -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 {})
}
}),

View file

@ -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,

View file

@ -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)
}

View file

@ -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(

View file

@ -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={:?})",

View file

@ -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

View file

@ -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: &regex::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: &regex::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: &regex::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: &regex::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, |_: &regex::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
}
}

View file

@ -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::*;

View file

@ -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

View file

@ -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);

View file

@ -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 {

View file

@ -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,
};

View file

@ -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

View file

@ -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,

View file

@ -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 }

View file

@ -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

View file

@ -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
}

View file

@ -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.

View file

@ -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,

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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())

View file

@ -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
}

View file

@ -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]

View file

@ -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!(

View file

@ -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.

View file

@ -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"] }

View file

@ -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::*;

View file

@ -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 {

View file

@ -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"] }

View file

@ -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();

View file

@ -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;

View file

@ -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)]

View file

@ -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

View 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),
)
}

View file

@ -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")
}

View file

@ -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
})

View file

@ -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,
)
});

View file

@ -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;

View file

@ -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);
};

View file

@ -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 =

View file

@ -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>,

View file

@ -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"] }

View file

@ -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),

File diff suppressed because it is too large Load diff

View file

@ -1 +1,2 @@
pub mod keystroke_input;
pub mod table;

View file

@ -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(

View file

@ -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(),
)
}
}

View file

@ -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),

View file

@ -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

View file

@ -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());

View file

@ -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

View file

@ -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>,

View file

@ -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()

View file

@ -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();

View file

@ -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(),

View 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()

View file

@ -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"] }

View file

@ -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|>";

View 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