Compare commits
29 Commits
v0.198.5
...
remove-d2d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4e8d2501a | ||
|
|
3be48a9813 | ||
|
|
7984e05d35 | ||
|
|
88b01e5e31 | ||
|
|
e599352fc2 | ||
|
|
c3f210eb5e | ||
|
|
d84cf7ef03 | ||
|
|
aa68b3e8ef | ||
|
|
fad7aa6643 | ||
|
|
d50b3e172e | ||
|
|
a8d3e5530b | ||
|
|
fe096ad205 | ||
|
|
30f6699b56 | ||
|
|
a1001079ba | ||
|
|
da83011fda | ||
|
|
bb1a7ccbba | ||
|
|
289f420504 | ||
|
|
15ad986329 | ||
|
|
0d9715325c | ||
|
|
5ef5f3c5ca | ||
|
|
2d4afd2119 | ||
|
|
afcb8f2a3f | ||
|
|
cdce3b3620 | ||
|
|
bc6bb42745 | ||
|
|
7695c4b82e | ||
|
|
794ade8b6d | ||
|
|
f4bd524d7f | ||
|
|
9d82e148de | ||
|
|
f8d1062484 |
49
Cargo.lock
generated
49
Cargo.lock
generated
@@ -2976,6 +2976,7 @@ dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
"clock",
|
||||
"cloud_api_client",
|
||||
"cloud_llm_client",
|
||||
"cocoa 0.26.0",
|
||||
"collections",
|
||||
@@ -3031,6 +3032,27 @@ dependencies = [
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cloud_api_client"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cloud_api_types",
|
||||
"futures 0.3.31",
|
||||
"http_client",
|
||||
"parking_lot",
|
||||
"serde_json",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cloud_api_types"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cloud_llm_client"
|
||||
version = "0.1.0"
|
||||
@@ -10960,12 +10982,16 @@ dependencies = [
|
||||
"gpui",
|
||||
"language",
|
||||
"project",
|
||||
"schemars",
|
||||
"serde",
|
||||
"settings",
|
||||
"theme",
|
||||
"ui",
|
||||
"util",
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
"zed_actions",
|
||||
"zlog",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -14729,6 +14755,25 @@ dependencies = [
|
||||
"zlog",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "settings_profile_selector"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"editor",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
"language",
|
||||
"menu",
|
||||
"picker",
|
||||
"project",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"ui",
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
"zed_actions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "settings_ui"
|
||||
version = "0.1.0"
|
||||
@@ -14751,7 +14796,6 @@ dependencies = [
|
||||
"notifications",
|
||||
"paths",
|
||||
"project",
|
||||
"schemars",
|
||||
"search",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -20193,7 +20237,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.198.0"
|
||||
version = "0.199.0"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"agent",
|
||||
@@ -20296,6 +20340,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"session",
|
||||
"settings",
|
||||
"settings_profile_selector",
|
||||
"settings_ui",
|
||||
"shellexpand 2.1.2",
|
||||
"smol",
|
||||
|
||||
23
Cargo.toml
23
Cargo.toml
@@ -1,13 +1,13 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"crates/activity_indicator",
|
||||
"crates/acp_thread",
|
||||
"crates/agent_ui",
|
||||
"crates/activity_indicator",
|
||||
"crates/agent",
|
||||
"crates/agent_settings",
|
||||
"crates/ai_onboarding",
|
||||
"crates/agent_servers",
|
||||
"crates/agent_settings",
|
||||
"crates/agent_ui",
|
||||
"crates/ai_onboarding",
|
||||
"crates/anthropic",
|
||||
"crates/askpass",
|
||||
"crates/assets",
|
||||
@@ -29,6 +29,8 @@ members = [
|
||||
"crates/cli",
|
||||
"crates/client",
|
||||
"crates/clock",
|
||||
"crates/cloud_api_client",
|
||||
"crates/cloud_api_types",
|
||||
"crates/cloud_llm_client",
|
||||
"crates/collab",
|
||||
"crates/collab_ui",
|
||||
@@ -49,8 +51,8 @@ members = [
|
||||
"crates/diagnostics",
|
||||
"crates/docs_preprocessor",
|
||||
"crates/editor",
|
||||
"crates/explorer_command_injector",
|
||||
"crates/eval",
|
||||
"crates/explorer_command_injector",
|
||||
"crates/extension",
|
||||
"crates/extension_api",
|
||||
"crates/extension_cli",
|
||||
@@ -99,7 +101,6 @@ members = [
|
||||
"crates/markdown_preview",
|
||||
"crates/media",
|
||||
"crates/menu",
|
||||
"crates/svg_preview",
|
||||
"crates/migrator",
|
||||
"crates/mistral",
|
||||
"crates/multi_buffer",
|
||||
@@ -140,6 +141,7 @@ members = [
|
||||
"crates/semantic_version",
|
||||
"crates/session",
|
||||
"crates/settings",
|
||||
"crates/settings_profile_selector",
|
||||
"crates/settings_ui",
|
||||
"crates/snippet",
|
||||
"crates/snippet_provider",
|
||||
@@ -152,6 +154,7 @@ members = [
|
||||
"crates/sum_tree",
|
||||
"crates/supermaven",
|
||||
"crates/supermaven_api",
|
||||
"crates/svg_preview",
|
||||
"crates/tab_switcher",
|
||||
"crates/task",
|
||||
"crates/tasks_ui",
|
||||
@@ -251,6 +254,8 @@ channel = { path = "crates/channel" }
|
||||
cli = { path = "crates/cli" }
|
||||
client = { path = "crates/client" }
|
||||
clock = { path = "crates/clock" }
|
||||
cloud_api_client = { path = "crates/cloud_api_client" }
|
||||
cloud_api_types = { path = "crates/cloud_api_types" }
|
||||
cloud_llm_client = { path = "crates/cloud_llm_client" }
|
||||
collab = { path = "crates/collab" }
|
||||
collab_ui = { path = "crates/collab_ui" }
|
||||
@@ -338,6 +343,7 @@ picker = { path = "crates/picker" }
|
||||
plugin = { path = "crates/plugin" }
|
||||
plugin_macros = { path = "crates/plugin_macros" }
|
||||
prettier = { path = "crates/prettier" }
|
||||
settings_profile_selector = { path = "crates/settings_profile_selector" }
|
||||
project = { path = "crates/project" }
|
||||
project_panel = { path = "crates/project_panel" }
|
||||
project_symbols = { path = "crates/project_symbols" }
|
||||
@@ -674,8 +680,13 @@ features = [
|
||||
"Win32_Globalization",
|
||||
"Win32_Graphics_Direct2D",
|
||||
"Win32_Graphics_Direct2D_Common",
|
||||
"Win32_Graphics_Direct3D",
|
||||
"Win32_Graphics_Direct3D11",
|
||||
"Win32_Graphics_Direct3D_Fxc",
|
||||
"Win32_Graphics_DirectComposition",
|
||||
"Win32_Graphics_DirectWrite",
|
||||
"Win32_Graphics_Dwm",
|
||||
"Win32_Graphics_Dxgi",
|
||||
"Win32_Graphics_Dxgi_Common",
|
||||
"Win32_Graphics_Gdi",
|
||||
"Win32_Graphics_Imaging",
|
||||
|
||||
@@ -1877,5 +1877,8 @@
|
||||
"save_breakpoints": true,
|
||||
"dock": "bottom",
|
||||
"button": true
|
||||
}
|
||||
},
|
||||
// Configures any number of settings profiles that are temporarily applied
|
||||
// when selected from `settings profile selector: toggle`.
|
||||
"profiles": []
|
||||
}
|
||||
|
||||
@@ -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_api_client.workspace = true
|
||||
cloud_llm_client.workspace = true
|
||||
collections.workspace = true
|
||||
credentials_provider.workspace = true
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub mod test;
|
||||
|
||||
mod cloud;
|
||||
mod proxy;
|
||||
pub mod telemetry;
|
||||
pub mod user;
|
||||
@@ -15,6 +16,7 @@ use async_tungstenite::tungstenite::{
|
||||
};
|
||||
use chrono::{DateTime, Utc};
|
||||
use clock::SystemClock;
|
||||
use cloud_api_client::CloudApiClient;
|
||||
use credentials_provider::CredentialsProvider;
|
||||
use futures::{
|
||||
AsyncReadExt, FutureExt, SinkExt, Stream, StreamExt, TryFutureExt as _, TryStreamExt,
|
||||
@@ -51,6 +53,7 @@ use tokio::net::TcpStream;
|
||||
use url::Url;
|
||||
use util::{ConnectionResult, ResultExt};
|
||||
|
||||
pub use cloud::*;
|
||||
pub use rpc::*;
|
||||
pub use telemetry_events::Event;
|
||||
pub use user::*;
|
||||
@@ -213,6 +216,7 @@ pub struct Client {
|
||||
id: AtomicU64,
|
||||
peer: Arc<Peer>,
|
||||
http: Arc<HttpClientWithUrl>,
|
||||
cloud_client: Arc<CloudApiClient>,
|
||||
telemetry: Arc<Telemetry>,
|
||||
credentials_provider: ClientCredentialsProvider,
|
||||
state: RwLock<ClientState>,
|
||||
@@ -586,6 +590,7 @@ impl Client {
|
||||
id: AtomicU64::new(0),
|
||||
peer: Peer::new(0),
|
||||
telemetry: Telemetry::new(clock, http.clone(), cx),
|
||||
cloud_client: Arc::new(CloudApiClient::new(http.clone())),
|
||||
http,
|
||||
credentials_provider: ClientCredentialsProvider::new(cx),
|
||||
state: Default::default(),
|
||||
@@ -618,6 +623,10 @@ impl Client {
|
||||
self.http.clone()
|
||||
}
|
||||
|
||||
pub fn cloud_client(&self) -> Arc<CloudApiClient> {
|
||||
self.cloud_client.clone()
|
||||
}
|
||||
|
||||
pub fn set_id(&self, id: u64) -> &Self {
|
||||
self.id.store(id, Ordering::SeqCst);
|
||||
self
|
||||
@@ -930,6 +939,8 @@ impl Client {
|
||||
}
|
||||
let credentials = credentials.unwrap();
|
||||
self.set_id(credentials.user_id);
|
||||
self.cloud_client
|
||||
.set_credentials(credentials.user_id as u32, credentials.access_token.clone());
|
||||
|
||||
if was_disconnected {
|
||||
self.set_status(Status::Connecting, cx);
|
||||
|
||||
3
crates/client/src/cloud.rs
Normal file
3
crates/client/src/cloud.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
mod user_store;
|
||||
|
||||
pub use user_store::*;
|
||||
41
crates/client/src/cloud/user_store.rs
Normal file
41
crates/client/src/cloud/user_store.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Context as _;
|
||||
use cloud_api_client::{AuthenticatedUser, CloudApiClient};
|
||||
use gpui::{Context, Task};
|
||||
use util::{ResultExt as _, maybe};
|
||||
|
||||
pub struct CloudUserStore {
|
||||
authenticated_user: Option<AuthenticatedUser>,
|
||||
_fetch_authenticated_user_task: Task<()>,
|
||||
}
|
||||
|
||||
impl CloudUserStore {
|
||||
pub fn new(cloud_client: Arc<CloudApiClient>, cx: &mut Context<Self>) -> Self {
|
||||
Self {
|
||||
authenticated_user: None,
|
||||
_fetch_authenticated_user_task: cx.spawn(async move |this, cx| {
|
||||
maybe!(async move {
|
||||
loop {
|
||||
if cloud_client.has_credentials() {
|
||||
break;
|
||||
}
|
||||
|
||||
cx.background_executor()
|
||||
.timer(Duration::from_millis(100))
|
||||
.await;
|
||||
}
|
||||
|
||||
let response = cloud_client.get_authenticated_user().await?;
|
||||
this.update(cx, |this, _cx| {
|
||||
this.authenticated_user = Some(response.user);
|
||||
})
|
||||
})
|
||||
.await
|
||||
.context("failed to fetch authenticated user")
|
||||
.log_err();
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
21
crates/cloud_api_client/Cargo.toml
Normal file
21
crates/cloud_api_client/Cargo.toml
Normal file
@@ -0,0 +1,21 @@
|
||||
[package]
|
||||
name = "cloud_api_client"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "Apache-2.0"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/cloud_api_client.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
cloud_api_types.workspace = true
|
||||
futures.workspace = true
|
||||
http_client.workspace = true
|
||||
parking_lot.workspace = true
|
||||
serde_json.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
1
crates/cloud_api_client/LICENSE-APACHE
Symbolic link
1
crates/cloud_api_client/LICENSE-APACHE
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-APACHE
|
||||
79
crates/cloud_api_client/src/cloud_api_client.rs
Normal file
79
crates/cloud_api_client/src/cloud_api_client.rs
Normal file
@@ -0,0 +1,79 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Result, anyhow};
|
||||
pub use cloud_api_types::*;
|
||||
use futures::AsyncReadExt as _;
|
||||
use http_client::{AsyncBody, HttpClientWithUrl, Method, Request};
|
||||
use parking_lot::RwLock;
|
||||
|
||||
struct Credentials {
|
||||
user_id: u32,
|
||||
access_token: String,
|
||||
}
|
||||
|
||||
pub struct CloudApiClient {
|
||||
credentials: RwLock<Option<Credentials>>,
|
||||
http_client: Arc<HttpClientWithUrl>,
|
||||
}
|
||||
|
||||
impl CloudApiClient {
|
||||
pub fn new(http_client: Arc<HttpClientWithUrl>) -> Self {
|
||||
Self {
|
||||
credentials: RwLock::new(None),
|
||||
http_client,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_credentials(&self) -> bool {
|
||||
self.credentials.read().is_some()
|
||||
}
|
||||
|
||||
pub fn set_credentials(&self, user_id: u32, access_token: String) {
|
||||
*self.credentials.write() = Some(Credentials {
|
||||
user_id,
|
||||
access_token,
|
||||
});
|
||||
}
|
||||
|
||||
fn authorization_header(&self) -> Result<String> {
|
||||
let guard = self.credentials.read();
|
||||
let credentials = guard
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow!("No credentials provided"))?;
|
||||
|
||||
Ok(format!(
|
||||
"{} {}",
|
||||
credentials.user_id, credentials.access_token
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn get_authenticated_user(&self) -> Result<GetAuthenticatedUserResponse> {
|
||||
let request = Request::builder()
|
||||
.method(Method::GET)
|
||||
.uri(
|
||||
self.http_client
|
||||
.build_zed_cloud_url("/client/users/me", &[])?
|
||||
.as_ref(),
|
||||
)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Authorization", self.authorization_header()?)
|
||||
.body(AsyncBody::default())?;
|
||||
|
||||
let mut response = self.http_client.send(request).await?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let mut body = String::new();
|
||||
response.body_mut().read_to_string(&mut body).await?;
|
||||
|
||||
anyhow::bail!(
|
||||
"Failed to get authenticated user.\nStatus: {:?}\nBody: {body}",
|
||||
response.status()
|
||||
)
|
||||
}
|
||||
|
||||
let mut body = String::new();
|
||||
response.body_mut().read_to_string(&mut body).await?;
|
||||
|
||||
Ok(serde_json::from_str(&body)?)
|
||||
}
|
||||
}
|
||||
16
crates/cloud_api_types/Cargo.toml
Normal file
16
crates/cloud_api_types/Cargo.toml
Normal file
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "cloud_api_types"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "Apache-2.0"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/cloud_api_types.rs"
|
||||
|
||||
[dependencies]
|
||||
serde.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
1
crates/cloud_api_types/LICENSE-APACHE
Symbolic link
1
crates/cloud_api_types/LICENSE-APACHE
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-APACHE
|
||||
14
crates/cloud_api_types/src/cloud_api_types.rs
Normal file
14
crates/cloud_api_types/src/cloud_api_types.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct GetAuthenticatedUserResponse {
|
||||
pub user: AuthenticatedUser,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct AuthenticatedUser {
|
||||
pub id: i32,
|
||||
pub avatar_url: String,
|
||||
pub github_login: String,
|
||||
pub name: Option<String>,
|
||||
}
|
||||
@@ -100,6 +100,7 @@ impl std::fmt::Display for SystemIdHeader {
|
||||
|
||||
pub fn routes(rpc_server: Arc<rpc::Server>) -> Router<(), Body> {
|
||||
Router::new()
|
||||
.route("/user", get(legacy_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))
|
||||
@@ -144,6 +145,51 @@ 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>,
|
||||
}
|
||||
|
||||
/// This is a legacy endpoint that is no longer used in production.
|
||||
///
|
||||
/// It currently only exists to be used when developing Collab locally.
|
||||
async fn legacy_update_or_create_authenticated_user(
|
||||
Query(params): Query<AuthenticatedUserParams>,
|
||||
Extension(app): Extension<Arc<AppState>>,
|
||||
) -> Result<Json<AuthenticatedUserResponse>> {
|
||||
let initial_channel_id = app.config.auto_join_channel_id;
|
||||
|
||||
let user = app
|
||||
.db
|
||||
.update_or_create_user_by_github_account(
|
||||
¶ms.github_login,
|
||||
params.github_user_id,
|
||||
params.github_email.as_deref(),
|
||||
params.github_name.as_deref(),
|
||||
params.github_user_created_at,
|
||||
initial_channel_id,
|
||||
)
|
||||
.await?;
|
||||
let metrics_id = app.db.get_user_metrics_id(user.id).await?;
|
||||
let feature_flags = app.db.get_user_flags(user.id).await?;
|
||||
Ok(Json(AuthenticatedUserResponse {
|
||||
user,
|
||||
metrics_id,
|
||||
feature_flags,
|
||||
}))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct LookUpUserParams {
|
||||
identifier: String,
|
||||
|
||||
@@ -8,6 +8,7 @@ use crate::{
|
||||
use anyhow::anyhow;
|
||||
use call::ActiveCall;
|
||||
use channel::{ChannelBuffer, ChannelStore};
|
||||
use client::CloudUserStore;
|
||||
use client::{
|
||||
self, ChannelId, Client, Connection, Credentials, EstablishConnectionError, UserStore,
|
||||
proto::PeerId,
|
||||
@@ -281,12 +282,14 @@ impl TestServer {
|
||||
.register_hosting_provider(Arc::new(git_hosting_providers::Github::public_instance()));
|
||||
|
||||
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
|
||||
let cloud_user_store = cx.new(|cx| CloudUserStore::new(client.cloud_client(), cx));
|
||||
let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx));
|
||||
let language_registry = Arc::new(LanguageRegistry::test(cx.executor()));
|
||||
let session = cx.new(|cx| AppSession::new(Session::test(), cx));
|
||||
let app_state = Arc::new(workspace::AppState {
|
||||
client: client.clone(),
|
||||
user_store: user_store.clone(),
|
||||
cloud_user_store,
|
||||
workspace_store,
|
||||
languages: language_registry,
|
||||
fs: fs.clone(),
|
||||
|
||||
@@ -216,10 +216,6 @@ xim = { git = "https://github.com/XDeme1/xim-rs", rev = "d50d461764c2213655cd9cf
|
||||
x11-clipboard = { version = "0.9.3", optional = true }
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
blade-util.workspace = true
|
||||
bytemuck = "1"
|
||||
blade-graphics.workspace = true
|
||||
blade-macros.workspace = true
|
||||
flume = "0.11"
|
||||
rand.workspace = true
|
||||
windows.workspace = true
|
||||
@@ -240,7 +236,6 @@ util = { workspace = true, features = ["test-support"] }
|
||||
|
||||
[target.'cfg(target_os = "windows")'.build-dependencies]
|
||||
embed-resource = "3.0"
|
||||
naga.workspace = true
|
||||
|
||||
[target.'cfg(target_os = "macos")'.build-dependencies]
|
||||
bindgen = "0.71"
|
||||
|
||||
@@ -9,7 +9,10 @@ fn main() {
|
||||
let target = env::var("CARGO_CFG_TARGET_OS");
|
||||
println!("cargo::rustc-check-cfg=cfg(gles)");
|
||||
|
||||
#[cfg(any(not(target_os = "macos"), feature = "macos-blade"))]
|
||||
#[cfg(any(
|
||||
not(any(target_os = "macos", target_os = "windows")),
|
||||
all(target_os = "macos", feature = "macos-blade")
|
||||
))]
|
||||
check_wgsl_shaders();
|
||||
|
||||
match target.as_deref() {
|
||||
@@ -17,21 +20,18 @@ fn main() {
|
||||
#[cfg(target_os = "macos")]
|
||||
macos::build();
|
||||
}
|
||||
#[cfg(all(target_os = "windows", feature = "windows-manifest"))]
|
||||
Ok("windows") => {
|
||||
let manifest = std::path::Path::new("resources/windows/gpui.manifest.xml");
|
||||
let rc_file = std::path::Path::new("resources/windows/gpui.rc");
|
||||
println!("cargo:rerun-if-changed={}", manifest.display());
|
||||
println!("cargo:rerun-if-changed={}", rc_file.display());
|
||||
embed_resource::compile(rc_file, embed_resource::NONE)
|
||||
.manifest_required()
|
||||
.unwrap();
|
||||
#[cfg(target_os = "windows")]
|
||||
windows::build();
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[cfg(any(
|
||||
not(any(target_os = "macos", target_os = "windows")),
|
||||
all(target_os = "macos", feature = "macos-blade")
|
||||
))]
|
||||
fn check_wgsl_shaders() {
|
||||
use std::path::PathBuf;
|
||||
use std::process;
|
||||
@@ -243,3 +243,215 @@ mod macos {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
mod windows {
|
||||
use std::{
|
||||
fs,
|
||||
io::Write,
|
||||
path::{Path, PathBuf},
|
||||
process::{self, Command},
|
||||
};
|
||||
|
||||
pub(super) fn build() {
|
||||
// Compile HLSL shaders
|
||||
#[cfg(not(debug_assertions))]
|
||||
compile_shaders();
|
||||
|
||||
// Embed the Windows manifest and resource file
|
||||
#[cfg(feature = "windows-manifest")]
|
||||
embed_resource();
|
||||
}
|
||||
|
||||
#[cfg(feature = "windows-manifest")]
|
||||
fn embed_resource() {
|
||||
let manifest = std::path::Path::new("resources/windows/gpui.manifest.xml");
|
||||
let rc_file = std::path::Path::new("resources/windows/gpui.rc");
|
||||
println!("cargo:rerun-if-changed={}", manifest.display());
|
||||
println!("cargo:rerun-if-changed={}", rc_file.display());
|
||||
embed_resource::compile(rc_file, embed_resource::NONE)
|
||||
.manifest_required()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// You can set the `GPUI_FXC_PATH` environment variable to specify the path to the fxc.exe compiler.
|
||||
fn compile_shaders() {
|
||||
let shader_path = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap())
|
||||
.join("src/platform/windows/shaders.hlsl");
|
||||
let out_dir = std::env::var("OUT_DIR").unwrap();
|
||||
|
||||
println!("cargo:rerun-if-changed={}", shader_path.display());
|
||||
|
||||
// Check if fxc.exe is available
|
||||
let fxc_path = find_fxc_compiler();
|
||||
|
||||
// Define all modules
|
||||
let modules = [
|
||||
"quad",
|
||||
"shadow",
|
||||
"path_rasterization",
|
||||
"path_sprite",
|
||||
"underline",
|
||||
"monochrome_sprite",
|
||||
"polychrome_sprite",
|
||||
];
|
||||
|
||||
let rust_binding_path = format!("{}/shaders_bytes.rs", out_dir);
|
||||
if Path::new(&rust_binding_path).exists() {
|
||||
fs::remove_file(&rust_binding_path)
|
||||
.expect("Failed to remove existing Rust binding file");
|
||||
}
|
||||
for module in modules {
|
||||
compile_shader_for_module(
|
||||
module,
|
||||
&out_dir,
|
||||
&fxc_path,
|
||||
shader_path.to_str().unwrap(),
|
||||
&rust_binding_path,
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let shader_path = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap())
|
||||
.join("src/platform/windows/color_text_raster.hlsl");
|
||||
compile_shader_for_module(
|
||||
"emoji_rasterization",
|
||||
&out_dir,
|
||||
&fxc_path,
|
||||
shader_path.to_str().unwrap(),
|
||||
&rust_binding_path,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// You can set the `GPUI_FXC_PATH` environment variable to specify the path to the fxc.exe compiler.
|
||||
fn find_fxc_compiler() -> String {
|
||||
// Check environment variable
|
||||
if let Ok(path) = std::env::var("GPUI_FXC_PATH") {
|
||||
if Path::new(&path).exists() {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
// Try to find in PATH
|
||||
// NOTE: This has to be `where.exe` on Windows, not `where`, it must be ended with `.exe`
|
||||
if let Ok(output) = std::process::Command::new("where.exe")
|
||||
.arg("fxc.exe")
|
||||
.output()
|
||||
{
|
||||
if output.status.success() {
|
||||
let path = String::from_utf8_lossy(&output.stdout);
|
||||
return path.trim().to_string();
|
||||
}
|
||||
}
|
||||
|
||||
// Check the default path
|
||||
if Path::new(r"C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x64\fxc.exe")
|
||||
.exists()
|
||||
{
|
||||
return r"C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x64\fxc.exe"
|
||||
.to_string();
|
||||
}
|
||||
|
||||
panic!("Failed to find fxc.exe");
|
||||
}
|
||||
|
||||
fn compile_shader_for_module(
|
||||
module: &str,
|
||||
out_dir: &str,
|
||||
fxc_path: &str,
|
||||
shader_path: &str,
|
||||
rust_binding_path: &str,
|
||||
) {
|
||||
// Compile vertex shader
|
||||
let output_file = format!("{}/{}_vs.h", out_dir, module);
|
||||
let const_name = format!("{}_VERTEX_BYTES", module.to_uppercase());
|
||||
compile_shader_impl(
|
||||
fxc_path,
|
||||
&format!("{module}_vertex"),
|
||||
&output_file,
|
||||
&const_name,
|
||||
shader_path,
|
||||
"vs_4_1",
|
||||
);
|
||||
generate_rust_binding(&const_name, &output_file, &rust_binding_path);
|
||||
|
||||
// Compile fragment shader
|
||||
let output_file = format!("{}/{}_ps.h", out_dir, module);
|
||||
let const_name = format!("{}_FRAGMENT_BYTES", module.to_uppercase());
|
||||
compile_shader_impl(
|
||||
fxc_path,
|
||||
&format!("{module}_fragment"),
|
||||
&output_file,
|
||||
&const_name,
|
||||
shader_path,
|
||||
"ps_4_1",
|
||||
);
|
||||
generate_rust_binding(&const_name, &output_file, &rust_binding_path);
|
||||
}
|
||||
|
||||
fn compile_shader_impl(
|
||||
fxc_path: &str,
|
||||
entry_point: &str,
|
||||
output_path: &str,
|
||||
var_name: &str,
|
||||
shader_path: &str,
|
||||
target: &str,
|
||||
) {
|
||||
let output = Command::new(fxc_path)
|
||||
.args([
|
||||
"/T",
|
||||
target,
|
||||
"/E",
|
||||
entry_point,
|
||||
"/Fh",
|
||||
output_path,
|
||||
"/Vn",
|
||||
var_name,
|
||||
"/O3",
|
||||
shader_path,
|
||||
])
|
||||
.output();
|
||||
|
||||
match output {
|
||||
Ok(result) => {
|
||||
if result.status.success() {
|
||||
return;
|
||||
}
|
||||
eprintln!(
|
||||
"Shader compilation failed for {}:\n{}",
|
||||
entry_point,
|
||||
String::from_utf8_lossy(&result.stderr)
|
||||
);
|
||||
process::exit(1);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to run fxc for {}: {}", entry_point, e);
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_rust_binding(const_name: &str, head_file: &str, output_path: &str) {
|
||||
let header_content = fs::read_to_string(head_file).expect("Failed to read header file");
|
||||
let const_definition = {
|
||||
let global_var_start = header_content.find("const BYTE").unwrap();
|
||||
let global_var = &header_content[global_var_start..];
|
||||
let equal = global_var.find('=').unwrap();
|
||||
global_var[equal + 1..].trim()
|
||||
};
|
||||
let rust_binding = format!(
|
||||
"const {}: &[u8] = &{}\n",
|
||||
const_name,
|
||||
const_definition.replace('{', "[").replace('}', "]")
|
||||
);
|
||||
let mut options = fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(output_path)
|
||||
.expect("Failed to open Rust binding file");
|
||||
options
|
||||
.write_all(rust_binding.as_bytes())
|
||||
.expect("Failed to write Rust binding file");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,7 +198,7 @@ impl RenderOnce for CharacterGrid {
|
||||
"χ", "ψ", "∂", "а", "в", "Ж", "ж", "З", "з", "К", "к", "л", "м", "Н", "н", "Р", "р",
|
||||
"У", "у", "ф", "ч", "ь", "ы", "Э", "э", "Я", "я", "ij", "öẋ", ".,", "⣝⣑", "~", "*",
|
||||
"_", "^", "`", "'", "(", "{", "«", "#", "&", "@", "$", "¢", "%", "|", "?", "¶", "µ",
|
||||
"❮", "<=", "!=", "==", "--", "++", "=>", "->",
|
||||
"❮", "<=", "!=", "==", "--", "++", "=>", "->", "🏀", "🎊", "😍", "❤️", "👍", "👎",
|
||||
];
|
||||
|
||||
let columns = 11;
|
||||
|
||||
@@ -35,6 +35,7 @@ pub(crate) fn swap_rgba_pa_to_bgra(color: &mut [u8]) {
|
||||
|
||||
/// An RGBA color
|
||||
#[derive(PartialEq, Clone, Copy, Default)]
|
||||
#[repr(C)]
|
||||
pub struct Rgba {
|
||||
/// The red component of the color, in the range 0.0 to 1.0
|
||||
pub r: f32,
|
||||
|
||||
@@ -13,8 +13,7 @@ mod mac;
|
||||
any(target_os = "linux", target_os = "freebsd"),
|
||||
any(feature = "x11", feature = "wayland")
|
||||
),
|
||||
target_os = "windows",
|
||||
feature = "macos-blade"
|
||||
all(target_os = "macos", feature = "macos-blade")
|
||||
))]
|
||||
mod blade;
|
||||
|
||||
@@ -448,6 +447,8 @@ impl Tiling {
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
|
||||
pub(crate) struct RequestFrameOptions {
|
||||
pub(crate) require_presentation: bool,
|
||||
/// Force refresh of all rendering states when true
|
||||
pub(crate) force_render: bool,
|
||||
}
|
||||
|
||||
pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
|
||||
|
||||
@@ -1004,12 +1004,13 @@ impl X11Client {
|
||||
let mut keystroke = crate::Keystroke::from_xkb(&state.xkb, modifiers, code);
|
||||
let keysym = state.xkb.key_get_one_sym(code);
|
||||
|
||||
// should be called after key_get_one_sym
|
||||
state.xkb.update_key(code, xkbc::KeyDirection::Down);
|
||||
|
||||
if keysym.is_modifier_key() {
|
||||
return Some(());
|
||||
}
|
||||
|
||||
// should be called after key_get_one_sym
|
||||
state.xkb.update_key(code, xkbc::KeyDirection::Down);
|
||||
|
||||
if let Some(mut compose_state) = state.compose_state.take() {
|
||||
compose_state.feed(keysym);
|
||||
match compose_state.status() {
|
||||
@@ -1067,12 +1068,13 @@ impl X11Client {
|
||||
let keystroke = crate::Keystroke::from_xkb(&state.xkb, modifiers, code);
|
||||
let keysym = state.xkb.key_get_one_sym(code);
|
||||
|
||||
// should be called after key_get_one_sym
|
||||
state.xkb.update_key(code, xkbc::KeyDirection::Up);
|
||||
|
||||
if keysym.is_modifier_key() {
|
||||
return Some(());
|
||||
}
|
||||
|
||||
// should be called after key_get_one_sym
|
||||
state.xkb.update_key(code, xkbc::KeyDirection::Up);
|
||||
|
||||
keystroke
|
||||
};
|
||||
drop(state);
|
||||
@@ -1793,6 +1795,7 @@ impl X11ClientState {
|
||||
drop(state);
|
||||
window.refresh(RequestFrameOptions {
|
||||
require_presentation: expose_event_received,
|
||||
force_render: false,
|
||||
});
|
||||
}
|
||||
xcb_connection
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
mod clipboard;
|
||||
mod destination_list;
|
||||
mod direct_write;
|
||||
mod directx_atlas;
|
||||
mod directx_renderer;
|
||||
mod dispatcher;
|
||||
mod display;
|
||||
mod events;
|
||||
@@ -14,6 +16,8 @@ mod wrapper;
|
||||
pub(crate) use clipboard::*;
|
||||
pub(crate) use destination_list::*;
|
||||
pub(crate) use direct_write::*;
|
||||
pub(crate) use directx_atlas::*;
|
||||
pub(crate) use directx_renderer::*;
|
||||
pub(crate) use dispatcher::*;
|
||||
pub(crate) use display::*;
|
||||
pub(crate) use events::*;
|
||||
|
||||
39
crates/gpui/src/platform/windows/color_text_raster.hlsl
Normal file
39
crates/gpui/src/platform/windows/color_text_raster.hlsl
Normal file
@@ -0,0 +1,39 @@
|
||||
struct RasterVertexOutput {
|
||||
float4 position : SV_Position;
|
||||
float2 texcoord : TEXCOORD0;
|
||||
};
|
||||
|
||||
RasterVertexOutput emoji_rasterization_vertex(uint vertexID : SV_VERTEXID)
|
||||
{
|
||||
RasterVertexOutput output;
|
||||
output.texcoord = float2((vertexID << 1) & 2, vertexID & 2);
|
||||
output.position = float4(output.texcoord * 2.0f - 1.0f, 0.0f, 1.0f);
|
||||
output.position.y = -output.position.y;
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
struct PixelInput {
|
||||
float4 position: SV_Position;
|
||||
float2 texcoord : TEXCOORD0;
|
||||
};
|
||||
|
||||
struct Bounds {
|
||||
int2 origin;
|
||||
int2 size;
|
||||
};
|
||||
|
||||
Texture2D<float4> t_layer : register(t0);
|
||||
SamplerState s_layer : register(s0);
|
||||
|
||||
cbuffer GlyphLayerTextureParams : register(b0) {
|
||||
Bounds bounds;
|
||||
float4 run_color;
|
||||
};
|
||||
|
||||
float4 emoji_rasterization_fragment(PixelInput input): SV_Target {
|
||||
float3 sampled = t_layer.Sample(s_layer, input.texcoord.xy).rgb;
|
||||
float alpha = (sampled.r + sampled.g + sampled.b) / 3;
|
||||
|
||||
return float4(run_color.rgb, alpha);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
309
crates/gpui/src/platform/windows/directx_atlas.rs
Normal file
309
crates/gpui/src/platform/windows/directx_atlas.rs
Normal file
@@ -0,0 +1,309 @@
|
||||
use collections::FxHashMap;
|
||||
use etagere::BucketedAtlasAllocator;
|
||||
use parking_lot::Mutex;
|
||||
use windows::Win32::Graphics::{
|
||||
Direct3D11::{
|
||||
D3D11_BIND_SHADER_RESOURCE, D3D11_BOX, D3D11_CPU_ACCESS_WRITE, D3D11_TEXTURE2D_DESC,
|
||||
D3D11_USAGE_DEFAULT, ID3D11Device, ID3D11DeviceContext, ID3D11ShaderResourceView,
|
||||
ID3D11Texture2D,
|
||||
},
|
||||
Dxgi::Common::*,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
AtlasKey, AtlasTextureId, AtlasTextureKind, AtlasTile, Bounds, DevicePixels, PlatformAtlas,
|
||||
Point, Size, platform::AtlasTextureList,
|
||||
};
|
||||
|
||||
pub(crate) struct DirectXAtlas(Mutex<DirectXAtlasState>);
|
||||
|
||||
struct DirectXAtlasState {
|
||||
device: ID3D11Device,
|
||||
device_context: ID3D11DeviceContext,
|
||||
monochrome_textures: AtlasTextureList<DirectXAtlasTexture>,
|
||||
polychrome_textures: AtlasTextureList<DirectXAtlasTexture>,
|
||||
tiles_by_key: FxHashMap<AtlasKey, AtlasTile>,
|
||||
}
|
||||
|
||||
struct DirectXAtlasTexture {
|
||||
id: AtlasTextureId,
|
||||
bytes_per_pixel: u32,
|
||||
allocator: BucketedAtlasAllocator,
|
||||
texture: ID3D11Texture2D,
|
||||
view: [Option<ID3D11ShaderResourceView>; 1],
|
||||
live_atlas_keys: u32,
|
||||
}
|
||||
|
||||
impl DirectXAtlas {
|
||||
pub(crate) fn new(device: &ID3D11Device, device_context: &ID3D11DeviceContext) -> Self {
|
||||
DirectXAtlas(Mutex::new(DirectXAtlasState {
|
||||
device: device.clone(),
|
||||
device_context: device_context.clone(),
|
||||
monochrome_textures: Default::default(),
|
||||
polychrome_textures: Default::default(),
|
||||
tiles_by_key: Default::default(),
|
||||
}))
|
||||
}
|
||||
|
||||
pub(crate) fn get_texture_view(
|
||||
&self,
|
||||
id: AtlasTextureId,
|
||||
) -> [Option<ID3D11ShaderResourceView>; 1] {
|
||||
let lock = self.0.lock();
|
||||
let tex = lock.texture(id);
|
||||
tex.view.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn handle_device_lost(
|
||||
&self,
|
||||
device: &ID3D11Device,
|
||||
device_context: &ID3D11DeviceContext,
|
||||
) {
|
||||
let mut lock = self.0.lock();
|
||||
lock.device = device.clone();
|
||||
lock.device_context = device_context.clone();
|
||||
lock.monochrome_textures = AtlasTextureList::default();
|
||||
lock.polychrome_textures = AtlasTextureList::default();
|
||||
lock.tiles_by_key.clear();
|
||||
}
|
||||
}
|
||||
|
||||
impl PlatformAtlas for DirectXAtlas {
|
||||
fn get_or_insert_with<'a>(
|
||||
&self,
|
||||
key: &AtlasKey,
|
||||
build: &mut dyn FnMut() -> anyhow::Result<
|
||||
Option<(Size<DevicePixels>, std::borrow::Cow<'a, [u8]>)>,
|
||||
>,
|
||||
) -> anyhow::Result<Option<AtlasTile>> {
|
||||
let mut lock = self.0.lock();
|
||||
if let Some(tile) = lock.tiles_by_key.get(key) {
|
||||
Ok(Some(tile.clone()))
|
||||
} else {
|
||||
let Some((size, bytes)) = build()? else {
|
||||
return Ok(None);
|
||||
};
|
||||
let tile = lock
|
||||
.allocate(size, key.texture_kind())
|
||||
.ok_or_else(|| anyhow::anyhow!("failed to allocate"))?;
|
||||
let texture = lock.texture(tile.texture_id);
|
||||
texture.upload(&lock.device_context, tile.bounds, &bytes);
|
||||
lock.tiles_by_key.insert(key.clone(), tile.clone());
|
||||
Ok(Some(tile))
|
||||
}
|
||||
}
|
||||
|
||||
fn remove(&self, key: &AtlasKey) {
|
||||
let mut lock = self.0.lock();
|
||||
|
||||
let Some(id) = lock.tiles_by_key.remove(key).map(|tile| tile.texture_id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let textures = match id.kind {
|
||||
AtlasTextureKind::Monochrome => &mut lock.monochrome_textures,
|
||||
AtlasTextureKind::Polychrome => &mut lock.polychrome_textures,
|
||||
};
|
||||
|
||||
let Some(texture_slot) = textures.textures.get_mut(id.index as usize) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(mut texture) = texture_slot.take() {
|
||||
texture.decrement_ref_count();
|
||||
if texture.is_unreferenced() {
|
||||
textures.free_list.push(texture.id.index as usize);
|
||||
lock.tiles_by_key.remove(key);
|
||||
} else {
|
||||
*texture_slot = Some(texture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DirectXAtlasState {
|
||||
fn allocate(
|
||||
&mut self,
|
||||
size: Size<DevicePixels>,
|
||||
texture_kind: AtlasTextureKind,
|
||||
) -> Option<AtlasTile> {
|
||||
{
|
||||
let textures = match texture_kind {
|
||||
AtlasTextureKind::Monochrome => &mut self.monochrome_textures,
|
||||
AtlasTextureKind::Polychrome => &mut self.polychrome_textures,
|
||||
};
|
||||
|
||||
if let Some(tile) = textures
|
||||
.iter_mut()
|
||||
.rev()
|
||||
.find_map(|texture| texture.allocate(size))
|
||||
{
|
||||
return Some(tile);
|
||||
}
|
||||
}
|
||||
|
||||
let texture = self.push_texture(size, texture_kind)?;
|
||||
texture.allocate(size)
|
||||
}
|
||||
|
||||
fn push_texture(
|
||||
&mut self,
|
||||
min_size: Size<DevicePixels>,
|
||||
kind: AtlasTextureKind,
|
||||
) -> Option<&mut DirectXAtlasTexture> {
|
||||
const DEFAULT_ATLAS_SIZE: Size<DevicePixels> = Size {
|
||||
width: DevicePixels(1024),
|
||||
height: DevicePixels(1024),
|
||||
};
|
||||
// Max texture size for DirectX. See:
|
||||
// https://learn.microsoft.com/en-us/windows/win32/direct3d11/overviews-direct3d-11-resources-limits
|
||||
const MAX_ATLAS_SIZE: Size<DevicePixels> = Size {
|
||||
width: DevicePixels(16384),
|
||||
height: DevicePixels(16384),
|
||||
};
|
||||
let size = min_size.min(&MAX_ATLAS_SIZE).max(&DEFAULT_ATLAS_SIZE);
|
||||
let pixel_format;
|
||||
let bind_flag;
|
||||
let bytes_per_pixel;
|
||||
match kind {
|
||||
AtlasTextureKind::Monochrome => {
|
||||
pixel_format = DXGI_FORMAT_R8_UNORM;
|
||||
bind_flag = D3D11_BIND_SHADER_RESOURCE;
|
||||
bytes_per_pixel = 1;
|
||||
}
|
||||
AtlasTextureKind::Polychrome => {
|
||||
pixel_format = DXGI_FORMAT_B8G8R8A8_UNORM;
|
||||
bind_flag = D3D11_BIND_SHADER_RESOURCE;
|
||||
bytes_per_pixel = 4;
|
||||
}
|
||||
}
|
||||
let texture_desc = D3D11_TEXTURE2D_DESC {
|
||||
Width: size.width.0 as u32,
|
||||
Height: size.height.0 as u32,
|
||||
MipLevels: 1,
|
||||
ArraySize: 1,
|
||||
Format: pixel_format,
|
||||
SampleDesc: DXGI_SAMPLE_DESC {
|
||||
Count: 1,
|
||||
Quality: 0,
|
||||
},
|
||||
Usage: D3D11_USAGE_DEFAULT,
|
||||
BindFlags: bind_flag.0 as u32,
|
||||
CPUAccessFlags: D3D11_CPU_ACCESS_WRITE.0 as u32,
|
||||
MiscFlags: 0,
|
||||
};
|
||||
let mut texture: Option<ID3D11Texture2D> = None;
|
||||
unsafe {
|
||||
// This only returns None if the device is lost, which we will recreate later.
|
||||
// So it's ok to return None here.
|
||||
self.device
|
||||
.CreateTexture2D(&texture_desc, None, Some(&mut texture))
|
||||
.ok()?;
|
||||
}
|
||||
let texture = texture.unwrap();
|
||||
|
||||
let texture_list = match kind {
|
||||
AtlasTextureKind::Monochrome => &mut self.monochrome_textures,
|
||||
AtlasTextureKind::Polychrome => &mut self.polychrome_textures,
|
||||
};
|
||||
let index = texture_list.free_list.pop();
|
||||
let view = unsafe {
|
||||
let mut view = None;
|
||||
self.device
|
||||
.CreateShaderResourceView(&texture, None, Some(&mut view))
|
||||
.ok()?;
|
||||
[view]
|
||||
};
|
||||
let atlas_texture = DirectXAtlasTexture {
|
||||
id: AtlasTextureId {
|
||||
index: index.unwrap_or(texture_list.textures.len()) as u32,
|
||||
kind,
|
||||
},
|
||||
bytes_per_pixel,
|
||||
allocator: etagere::BucketedAtlasAllocator::new(size.into()),
|
||||
texture,
|
||||
view,
|
||||
live_atlas_keys: 0,
|
||||
};
|
||||
if let Some(ix) = index {
|
||||
texture_list.textures[ix] = Some(atlas_texture);
|
||||
texture_list.textures.get_mut(ix).unwrap().as_mut()
|
||||
} else {
|
||||
texture_list.textures.push(Some(atlas_texture));
|
||||
texture_list.textures.last_mut().unwrap().as_mut()
|
||||
}
|
||||
}
|
||||
|
||||
fn texture(&self, id: AtlasTextureId) -> &DirectXAtlasTexture {
|
||||
let textures = match id.kind {
|
||||
crate::AtlasTextureKind::Monochrome => &self.monochrome_textures,
|
||||
crate::AtlasTextureKind::Polychrome => &self.polychrome_textures,
|
||||
};
|
||||
textures[id.index as usize].as_ref().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl DirectXAtlasTexture {
|
||||
fn allocate(&mut self, size: Size<DevicePixels>) -> Option<AtlasTile> {
|
||||
let allocation = self.allocator.allocate(size.into())?;
|
||||
let tile = AtlasTile {
|
||||
texture_id: self.id,
|
||||
tile_id: allocation.id.into(),
|
||||
bounds: Bounds {
|
||||
origin: allocation.rectangle.min.into(),
|
||||
size,
|
||||
},
|
||||
padding: 0,
|
||||
};
|
||||
self.live_atlas_keys += 1;
|
||||
Some(tile)
|
||||
}
|
||||
|
||||
fn upload(
|
||||
&self,
|
||||
device_context: &ID3D11DeviceContext,
|
||||
bounds: Bounds<DevicePixels>,
|
||||
bytes: &[u8],
|
||||
) {
|
||||
unsafe {
|
||||
device_context.UpdateSubresource(
|
||||
&self.texture,
|
||||
0,
|
||||
Some(&D3D11_BOX {
|
||||
left: bounds.left().0 as u32,
|
||||
top: bounds.top().0 as u32,
|
||||
front: 0,
|
||||
right: bounds.right().0 as u32,
|
||||
bottom: bounds.bottom().0 as u32,
|
||||
back: 1,
|
||||
}),
|
||||
bytes.as_ptr() as _,
|
||||
bounds.size.width.to_bytes(self.bytes_per_pixel as u8),
|
||||
0,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn decrement_ref_count(&mut self) {
|
||||
self.live_atlas_keys -= 1;
|
||||
}
|
||||
|
||||
fn is_unreferenced(&mut self) -> bool {
|
||||
self.live_atlas_keys == 0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Size<DevicePixels>> for etagere::Size {
|
||||
fn from(size: Size<DevicePixels>) -> Self {
|
||||
etagere::Size::new(size.width.into(), size.height.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<etagere::Point> for Point<DevicePixels> {
|
||||
fn from(value: etagere::Point) -> Self {
|
||||
Point {
|
||||
x: DevicePixels::from(value.x),
|
||||
y: DevicePixels::from(value.y),
|
||||
}
|
||||
}
|
||||
}
|
||||
1789
crates/gpui/src/platform/windows/directx_renderer.rs
Normal file
1789
crates/gpui/src/platform/windows/directx_renderer.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -23,6 +23,7 @@ pub(crate) const WM_GPUI_CURSOR_STYLE_CHANGED: u32 = WM_USER + 1;
|
||||
pub(crate) const WM_GPUI_CLOSE_ONE_WINDOW: u32 = WM_USER + 2;
|
||||
pub(crate) const WM_GPUI_TASK_DISPATCHED_ON_MAIN_THREAD: u32 = WM_USER + 3;
|
||||
pub(crate) const WM_GPUI_DOCK_MENU_ACTION: u32 = WM_USER + 4;
|
||||
pub(crate) const WM_GPUI_FORCE_UPDATE_WINDOW: u32 = WM_USER + 5;
|
||||
|
||||
const SIZE_MOVE_LOOP_TIMER_ID: usize = 1;
|
||||
const AUTO_HIDE_TASKBAR_THICKNESS_PX: i32 = 1;
|
||||
@@ -37,6 +38,7 @@ pub(crate) fn handle_msg(
|
||||
let handled = match msg {
|
||||
WM_ACTIVATE => handle_activate_msg(wparam, state_ptr),
|
||||
WM_CREATE => handle_create_msg(handle, state_ptr),
|
||||
WM_DEVICECHANGE => handle_device_change_msg(handle, wparam, state_ptr),
|
||||
WM_MOVE => handle_move_msg(handle, lparam, state_ptr),
|
||||
WM_SIZE => handle_size_msg(wparam, lparam, state_ptr),
|
||||
WM_GETMINMAXINFO => handle_get_min_max_info_msg(lparam, state_ptr),
|
||||
@@ -48,7 +50,7 @@ pub(crate) fn handle_msg(
|
||||
WM_DISPLAYCHANGE => handle_display_change_msg(handle, state_ptr),
|
||||
WM_NCHITTEST => handle_hit_test_msg(handle, msg, wparam, lparam, state_ptr),
|
||||
WM_PAINT => handle_paint_msg(handle, state_ptr),
|
||||
WM_CLOSE => handle_close_msg(handle, state_ptr),
|
||||
WM_CLOSE => handle_close_msg(state_ptr),
|
||||
WM_DESTROY => handle_destroy_msg(handle, state_ptr),
|
||||
WM_MOUSEMOVE => handle_mouse_move_msg(handle, lparam, wparam, state_ptr),
|
||||
WM_MOUSELEAVE | WM_NCMOUSELEAVE => handle_mouse_leave_msg(state_ptr),
|
||||
@@ -96,6 +98,7 @@ pub(crate) fn handle_msg(
|
||||
WM_SETTINGCHANGE => handle_system_settings_changed(handle, wparam, lparam, state_ptr),
|
||||
WM_INPUTLANGCHANGE => handle_input_language_changed(lparam, state_ptr),
|
||||
WM_GPUI_CURSOR_STYLE_CHANGED => handle_cursor_changed(lparam, state_ptr),
|
||||
WM_GPUI_FORCE_UPDATE_WINDOW => draw_window(handle, true, state_ptr),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(n) = handled {
|
||||
@@ -181,11 +184,9 @@ fn handle_size_msg(
|
||||
let new_size = size(DevicePixels(width), DevicePixels(height));
|
||||
let scale_factor = lock.scale_factor;
|
||||
if lock.restore_from_minimized.is_some() {
|
||||
lock.renderer
|
||||
.update_drawable_size_even_if_unchanged(new_size);
|
||||
lock.callbacks.request_frame = lock.restore_from_minimized.take();
|
||||
} else {
|
||||
lock.renderer.update_drawable_size(new_size);
|
||||
lock.renderer.resize(new_size).log_err();
|
||||
}
|
||||
let new_size = new_size.to_pixels(scale_factor);
|
||||
lock.logical_size = new_size;
|
||||
@@ -238,40 +239,14 @@ fn handle_timer_msg(
|
||||
}
|
||||
|
||||
fn handle_paint_msg(handle: HWND, state_ptr: Rc<WindowsWindowStatePtr>) -> Option<isize> {
|
||||
let mut lock = state_ptr.state.borrow_mut();
|
||||
if let Some(mut request_frame) = lock.callbacks.request_frame.take() {
|
||||
drop(lock);
|
||||
request_frame(Default::default());
|
||||
state_ptr.state.borrow_mut().callbacks.request_frame = Some(request_frame);
|
||||
}
|
||||
unsafe { ValidateRect(Some(handle), None).ok().log_err() };
|
||||
Some(0)
|
||||
draw_window(handle, false, state_ptr)
|
||||
}
|
||||
|
||||
fn handle_close_msg(handle: HWND, state_ptr: Rc<WindowsWindowStatePtr>) -> Option<isize> {
|
||||
let mut lock = state_ptr.state.borrow_mut();
|
||||
let output = if let Some(mut callback) = lock.callbacks.should_close.take() {
|
||||
drop(lock);
|
||||
let should_close = callback();
|
||||
state_ptr.state.borrow_mut().callbacks.should_close = Some(callback);
|
||||
if should_close { None } else { Some(0) }
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Workaround as window close animation is not played with `WS_EX_LAYERED` enabled.
|
||||
if output.is_none() {
|
||||
unsafe {
|
||||
let current_style = get_window_long(handle, GWL_EXSTYLE);
|
||||
set_window_long(
|
||||
handle,
|
||||
GWL_EXSTYLE,
|
||||
current_style & !WS_EX_LAYERED.0 as isize,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
output
|
||||
fn handle_close_msg(state_ptr: Rc<WindowsWindowStatePtr>) -> Option<isize> {
|
||||
let mut callback = state_ptr.state.borrow_mut().callbacks.should_close.take()?;
|
||||
let should_close = callback();
|
||||
state_ptr.state.borrow_mut().callbacks.should_close = Some(callback);
|
||||
if should_close { None } else { Some(0) }
|
||||
}
|
||||
|
||||
fn handle_destroy_msg(handle: HWND, state_ptr: Rc<WindowsWindowStatePtr>) -> Option<isize> {
|
||||
@@ -1223,6 +1198,53 @@ fn handle_input_language_changed(
|
||||
Some(0)
|
||||
}
|
||||
|
||||
fn handle_device_change_msg(
|
||||
handle: HWND,
|
||||
wparam: WPARAM,
|
||||
state_ptr: Rc<WindowsWindowStatePtr>,
|
||||
) -> Option<isize> {
|
||||
if wparam.0 == DBT_DEVNODES_CHANGED as usize {
|
||||
// The reason for sending this message is to actually trigger a redraw of the window.
|
||||
unsafe {
|
||||
PostMessageW(
|
||||
Some(handle),
|
||||
WM_GPUI_FORCE_UPDATE_WINDOW,
|
||||
WPARAM(0),
|
||||
LPARAM(0),
|
||||
)
|
||||
.log_err();
|
||||
}
|
||||
// If the GPU device is lost, this redraw will take care of recreating the device context.
|
||||
// The WM_GPUI_FORCE_UPDATE_WINDOW message will take care of redrawing the window, after
|
||||
// the device context has been recreated.
|
||||
draw_window(handle, true, state_ptr)
|
||||
} else {
|
||||
// Other device change messages are not handled.
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn draw_window(
|
||||
handle: HWND,
|
||||
force_render: bool,
|
||||
state_ptr: Rc<WindowsWindowStatePtr>,
|
||||
) -> Option<isize> {
|
||||
let mut request_frame = state_ptr
|
||||
.state
|
||||
.borrow_mut()
|
||||
.callbacks
|
||||
.request_frame
|
||||
.take()?;
|
||||
request_frame(RequestFrameOptions {
|
||||
require_presentation: false,
|
||||
force_render,
|
||||
});
|
||||
state_ptr.state.borrow_mut().callbacks.request_frame = Some(request_frame);
|
||||
unsafe { ValidateRect(Some(handle), None).ok().log_err() };
|
||||
Some(0)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn parse_char_message(wparam: WPARAM, state_ptr: &Rc<WindowsWindowStatePtr>) -> Option<String> {
|
||||
let code_point = wparam.loword();
|
||||
|
||||
@@ -28,13 +28,12 @@ use windows::{
|
||||
core::*,
|
||||
};
|
||||
|
||||
use crate::{platform::blade::BladeContext, *};
|
||||
use crate::*;
|
||||
|
||||
pub(crate) struct WindowsPlatform {
|
||||
state: RefCell<WindowsPlatformState>,
|
||||
raw_window_handles: RwLock<SmallVec<[HWND; 4]>>,
|
||||
// The below members will never change throughout the entire lifecycle of the app.
|
||||
gpu_context: BladeContext,
|
||||
icon: HICON,
|
||||
main_receiver: flume::Receiver<Runnable>,
|
||||
background_executor: BackgroundExecutor,
|
||||
@@ -45,6 +44,7 @@ pub(crate) struct WindowsPlatform {
|
||||
drop_target_helper: IDropTargetHelper,
|
||||
validation_number: usize,
|
||||
main_thread_id_win32: u32,
|
||||
disable_direct_composition: bool,
|
||||
}
|
||||
|
||||
pub(crate) struct WindowsPlatformState {
|
||||
@@ -94,14 +94,18 @@ impl WindowsPlatform {
|
||||
main_thread_id_win32,
|
||||
validation_number,
|
||||
));
|
||||
let disable_direct_composition = std::env::var(DISABLE_DIRECT_COMPOSITION)
|
||||
.is_ok_and(|value| value == "true" || value == "1");
|
||||
let background_executor = BackgroundExecutor::new(dispatcher.clone());
|
||||
let foreground_executor = ForegroundExecutor::new(dispatcher);
|
||||
let directx_devices = DirectXDevices::new(disable_direct_composition)
|
||||
.context("Unable to init directx devices.")?;
|
||||
let bitmap_factory = ManuallyDrop::new(unsafe {
|
||||
CoCreateInstance(&CLSID_WICImagingFactory, None, CLSCTX_INPROC_SERVER)
|
||||
.context("Error creating bitmap factory.")?
|
||||
});
|
||||
let text_system = Arc::new(
|
||||
DirectWriteTextSystem::new(&bitmap_factory)
|
||||
DirectWriteTextSystem::new(&directx_devices, &bitmap_factory)
|
||||
.context("Error creating DirectWriteTextSystem")?,
|
||||
);
|
||||
let drop_target_helper: IDropTargetHelper = unsafe {
|
||||
@@ -111,18 +115,17 @@ impl WindowsPlatform {
|
||||
let icon = load_icon().unwrap_or_default();
|
||||
let state = RefCell::new(WindowsPlatformState::new());
|
||||
let raw_window_handles = RwLock::new(SmallVec::new());
|
||||
let gpu_context = BladeContext::new().context("Unable to init GPU context")?;
|
||||
let windows_version = WindowsVersion::new().context("Error retrieve windows version")?;
|
||||
|
||||
Ok(Self {
|
||||
state,
|
||||
raw_window_handles,
|
||||
gpu_context,
|
||||
icon,
|
||||
main_receiver,
|
||||
background_executor,
|
||||
foreground_executor,
|
||||
text_system,
|
||||
disable_direct_composition,
|
||||
windows_version,
|
||||
bitmap_factory,
|
||||
drop_target_helper,
|
||||
@@ -187,6 +190,7 @@ impl WindowsPlatform {
|
||||
validation_number: self.validation_number,
|
||||
main_receiver: self.main_receiver.clone(),
|
||||
main_thread_id_win32: self.main_thread_id_win32,
|
||||
disable_direct_composition: self.disable_direct_composition,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -343,27 +347,11 @@ impl Platform for WindowsPlatform {
|
||||
|
||||
fn run(&self, on_finish_launching: Box<dyn 'static + FnOnce()>) {
|
||||
on_finish_launching();
|
||||
let vsync_event = unsafe { Owned::new(CreateEventW(None, false, false, None).unwrap()) };
|
||||
begin_vsync(*vsync_event);
|
||||
'a: loop {
|
||||
let wait_result = unsafe {
|
||||
MsgWaitForMultipleObjects(Some(&[*vsync_event]), false, INFINITE, QS_ALLINPUT)
|
||||
};
|
||||
|
||||
match wait_result {
|
||||
// compositor clock ticked so we should draw a frame
|
||||
WAIT_EVENT(0) => self.redraw_all(),
|
||||
// Windows thread messages are posted
|
||||
WAIT_EVENT(1) => {
|
||||
if self.handle_events() {
|
||||
break 'a;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
log::error!("Something went wrong while waiting {:?}", wait_result);
|
||||
break;
|
||||
}
|
||||
loop {
|
||||
if self.handle_events() {
|
||||
break;
|
||||
}
|
||||
self.redraw_all();
|
||||
}
|
||||
|
||||
if let Some(ref mut callback) = self.state.borrow_mut().callbacks.quit {
|
||||
@@ -455,12 +443,7 @@ impl Platform for WindowsPlatform {
|
||||
handle: AnyWindowHandle,
|
||||
options: WindowParams,
|
||||
) -> Result<Box<dyn PlatformWindow>> {
|
||||
let window = WindowsWindow::new(
|
||||
handle,
|
||||
options,
|
||||
self.generate_creation_info(),
|
||||
&self.gpu_context,
|
||||
)?;
|
||||
let window = WindowsWindow::new(handle, options, self.generate_creation_info())?;
|
||||
let handle = window.get_raw_handle();
|
||||
self.raw_window_handles.write().push(handle);
|
||||
|
||||
@@ -739,6 +722,7 @@ pub(crate) struct WindowCreationInfo {
|
||||
pub(crate) validation_number: usize,
|
||||
pub(crate) main_receiver: flume::Receiver<Runnable>,
|
||||
pub(crate) main_thread_id_win32: u32,
|
||||
pub(crate) disable_direct_composition: bool,
|
||||
}
|
||||
|
||||
fn open_target(target: &str) {
|
||||
@@ -846,16 +830,6 @@ fn file_save_dialog(directory: PathBuf, window: Option<HWND>) -> Result<Option<P
|
||||
Ok(Some(PathBuf::from(file_path_string)))
|
||||
}
|
||||
|
||||
fn begin_vsync(vsync_event: HANDLE) {
|
||||
let event: SafeHandle = vsync_event.into();
|
||||
std::thread::spawn(move || unsafe {
|
||||
loop {
|
||||
windows::Win32::Graphics::Dwm::DwmFlush().log_err();
|
||||
SetEvent(*event).log_err();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn load_icon() -> Result<HICON> {
|
||||
let module = unsafe { GetModuleHandleW(None).context("unable to get module handle")? };
|
||||
let handle = unsafe {
|
||||
|
||||
1159
crates/gpui/src/platform/windows/shaders.hlsl
Normal file
1159
crates/gpui/src/platform/windows/shaders.hlsl
Normal file
File diff suppressed because it is too large
Load Diff
@@ -26,7 +26,6 @@ use windows::{
|
||||
core::*,
|
||||
};
|
||||
|
||||
use crate::platform::blade::{BladeContext, BladeRenderer};
|
||||
use crate::*;
|
||||
|
||||
pub(crate) struct WindowsWindow(pub Rc<WindowsWindowStatePtr>);
|
||||
@@ -49,7 +48,7 @@ pub struct WindowsWindowState {
|
||||
pub system_key_handled: bool,
|
||||
pub hovered: bool,
|
||||
|
||||
pub renderer: BladeRenderer,
|
||||
pub renderer: DirectXRenderer,
|
||||
|
||||
pub click_state: ClickState,
|
||||
pub system_settings: WindowsSystemSettings,
|
||||
@@ -80,13 +79,12 @@ pub(crate) struct WindowsWindowStatePtr {
|
||||
impl WindowsWindowState {
|
||||
fn new(
|
||||
hwnd: HWND,
|
||||
transparent: bool,
|
||||
cs: &CREATESTRUCTW,
|
||||
current_cursor: Option<HCURSOR>,
|
||||
display: WindowsDisplay,
|
||||
gpu_context: &BladeContext,
|
||||
min_size: Option<Size<Pixels>>,
|
||||
appearance: WindowAppearance,
|
||||
disable_direct_composition: bool,
|
||||
) -> Result<Self> {
|
||||
let scale_factor = {
|
||||
let monitor_dpi = unsafe { GetDpiForWindow(hwnd) } as f32;
|
||||
@@ -103,7 +101,8 @@ impl WindowsWindowState {
|
||||
};
|
||||
let border_offset = WindowBorderOffset::default();
|
||||
let restore_from_minimized = None;
|
||||
let renderer = windows_renderer::init(gpu_context, hwnd, transparent)?;
|
||||
let renderer = DirectXRenderer::new(hwnd, disable_direct_composition)
|
||||
.context("Creating DirectX renderer")?;
|
||||
let callbacks = Callbacks::default();
|
||||
let input_handler = None;
|
||||
let pending_surrogate = None;
|
||||
@@ -206,13 +205,12 @@ impl WindowsWindowStatePtr {
|
||||
fn new(context: &WindowCreateContext, hwnd: HWND, cs: &CREATESTRUCTW) -> Result<Rc<Self>> {
|
||||
let state = RefCell::new(WindowsWindowState::new(
|
||||
hwnd,
|
||||
context.transparent,
|
||||
cs,
|
||||
context.current_cursor,
|
||||
context.display,
|
||||
context.gpu_context,
|
||||
context.min_size,
|
||||
context.appearance,
|
||||
context.disable_direct_composition,
|
||||
)?);
|
||||
|
||||
Ok(Rc::new_cyclic(|this| Self {
|
||||
@@ -329,12 +327,11 @@ pub(crate) struct Callbacks {
|
||||
pub(crate) appearance_changed: Option<Box<dyn FnMut()>>,
|
||||
}
|
||||
|
||||
struct WindowCreateContext<'a> {
|
||||
struct WindowCreateContext {
|
||||
inner: Option<Result<Rc<WindowsWindowStatePtr>>>,
|
||||
handle: AnyWindowHandle,
|
||||
hide_title_bar: bool,
|
||||
display: WindowsDisplay,
|
||||
transparent: bool,
|
||||
is_movable: bool,
|
||||
min_size: Option<Size<Pixels>>,
|
||||
executor: ForegroundExecutor,
|
||||
@@ -343,9 +340,9 @@ struct WindowCreateContext<'a> {
|
||||
drop_target_helper: IDropTargetHelper,
|
||||
validation_number: usize,
|
||||
main_receiver: flume::Receiver<Runnable>,
|
||||
gpu_context: &'a BladeContext,
|
||||
main_thread_id_win32: u32,
|
||||
appearance: WindowAppearance,
|
||||
disable_direct_composition: bool,
|
||||
}
|
||||
|
||||
impl WindowsWindow {
|
||||
@@ -353,7 +350,6 @@ impl WindowsWindow {
|
||||
handle: AnyWindowHandle,
|
||||
params: WindowParams,
|
||||
creation_info: WindowCreationInfo,
|
||||
gpu_context: &BladeContext,
|
||||
) -> Result<Self> {
|
||||
let WindowCreationInfo {
|
||||
icon,
|
||||
@@ -364,6 +360,7 @@ impl WindowsWindow {
|
||||
validation_number,
|
||||
main_receiver,
|
||||
main_thread_id_win32,
|
||||
disable_direct_composition,
|
||||
} = creation_info;
|
||||
let classname = register_wnd_class(icon);
|
||||
let hide_title_bar = params
|
||||
@@ -379,14 +376,18 @@ impl WindowsWindow {
|
||||
.map(|title| title.as_ref())
|
||||
.unwrap_or(""),
|
||||
);
|
||||
let (dwexstyle, mut dwstyle) = if params.kind == WindowKind::PopUp {
|
||||
(WS_EX_TOOLWINDOW | WS_EX_LAYERED, WINDOW_STYLE(0x0))
|
||||
|
||||
let (mut dwexstyle, dwstyle) = if params.kind == WindowKind::PopUp {
|
||||
(WS_EX_TOOLWINDOW, WINDOW_STYLE(0x0))
|
||||
} else {
|
||||
(
|
||||
WS_EX_APPWINDOW | WS_EX_LAYERED,
|
||||
WS_EX_APPWINDOW,
|
||||
WS_THICKFRAME | WS_SYSMENU | WS_MAXIMIZEBOX | WS_MINIMIZEBOX,
|
||||
)
|
||||
};
|
||||
if !disable_direct_composition {
|
||||
dwexstyle |= WS_EX_NOREDIRECTIONBITMAP;
|
||||
}
|
||||
|
||||
let hinstance = get_module_handle();
|
||||
let display = if let Some(display_id) = params.display_id {
|
||||
@@ -401,7 +402,6 @@ impl WindowsWindow {
|
||||
handle,
|
||||
hide_title_bar,
|
||||
display,
|
||||
transparent: true,
|
||||
is_movable: params.is_movable,
|
||||
min_size: params.window_min_size,
|
||||
executor,
|
||||
@@ -410,9 +410,9 @@ impl WindowsWindow {
|
||||
drop_target_helper,
|
||||
validation_number,
|
||||
main_receiver,
|
||||
gpu_context,
|
||||
main_thread_id_win32,
|
||||
appearance,
|
||||
disable_direct_composition,
|
||||
};
|
||||
let lpparam = Some(&context as *const _ as *const _);
|
||||
let creation_result = unsafe {
|
||||
@@ -453,14 +453,6 @@ impl WindowsWindow {
|
||||
state: WindowOpenState::Windowed,
|
||||
});
|
||||
}
|
||||
// The render pipeline will perform compositing on the GPU when the
|
||||
// swapchain is configured correctly (see downstream of
|
||||
// update_transparency).
|
||||
// The following configuration is a one-time setup to ensure that the
|
||||
// window is going to be composited with per-pixel alpha, but the render
|
||||
// pipeline is responsible for effectively calling UpdateLayeredWindow
|
||||
// at the appropriate time.
|
||||
unsafe { SetLayeredWindowAttributes(hwnd, COLORREF(0), 255, LWA_ALPHA)? };
|
||||
|
||||
Ok(Self(state_ptr))
|
||||
}
|
||||
@@ -485,7 +477,6 @@ impl rwh::HasDisplayHandle for WindowsWindow {
|
||||
|
||||
impl Drop for WindowsWindow {
|
||||
fn drop(&mut self) {
|
||||
self.0.state.borrow_mut().renderer.destroy();
|
||||
// clone this `Rc` to prevent early release of the pointer
|
||||
let this = self.0.clone();
|
||||
self.0
|
||||
@@ -705,24 +696,21 @@ impl PlatformWindow for WindowsWindow {
|
||||
}
|
||||
|
||||
fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) {
|
||||
let mut window_state = self.0.state.borrow_mut();
|
||||
window_state
|
||||
.renderer
|
||||
.update_transparency(background_appearance != WindowBackgroundAppearance::Opaque);
|
||||
let hwnd = self.0.hwnd;
|
||||
|
||||
match background_appearance {
|
||||
WindowBackgroundAppearance::Opaque => {
|
||||
// ACCENT_DISABLED
|
||||
set_window_composition_attribute(window_state.hwnd, None, 0);
|
||||
set_window_composition_attribute(hwnd, None, 0);
|
||||
}
|
||||
WindowBackgroundAppearance::Transparent => {
|
||||
// Use ACCENT_ENABLE_TRANSPARENTGRADIENT for transparent background
|
||||
set_window_composition_attribute(window_state.hwnd, None, 2);
|
||||
set_window_composition_attribute(hwnd, None, 2);
|
||||
}
|
||||
WindowBackgroundAppearance::Blurred => {
|
||||
// Enable acrylic blur
|
||||
// ACCENT_ENABLE_ACRYLICBLURBEHIND
|
||||
set_window_composition_attribute(window_state.hwnd, Some((0, 0, 0, 0)), 4);
|
||||
set_window_composition_attribute(hwnd, Some((0, 0, 0, 0)), 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -794,11 +782,11 @@ impl PlatformWindow for WindowsWindow {
|
||||
}
|
||||
|
||||
fn draw(&self, scene: &Scene) {
|
||||
self.0.state.borrow_mut().renderer.draw(scene)
|
||||
self.0.state.borrow_mut().renderer.draw(scene).log_err();
|
||||
}
|
||||
|
||||
fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas> {
|
||||
self.0.state.borrow().renderer.sprite_atlas().clone()
|
||||
self.0.state.borrow().renderer.sprite_atlas()
|
||||
}
|
||||
|
||||
fn get_raw_handle(&self) -> HWND {
|
||||
@@ -806,11 +794,11 @@ impl PlatformWindow for WindowsWindow {
|
||||
}
|
||||
|
||||
fn gpu_specs(&self) -> Option<GpuSpecs> {
|
||||
Some(self.0.state.borrow().renderer.gpu_specs())
|
||||
self.0.state.borrow().renderer.gpu_specs().log_err()
|
||||
}
|
||||
|
||||
fn update_ime_position(&self, _bounds: Bounds<ScaledPixels>) {
|
||||
// todo(windows)
|
||||
// There is no such thing on Windows.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1306,52 +1294,6 @@ fn set_window_composition_attribute(hwnd: HWND, color: Option<Color>, state: u32
|
||||
}
|
||||
}
|
||||
|
||||
mod windows_renderer {
|
||||
use crate::platform::blade::{BladeContext, BladeRenderer, BladeSurfaceConfig};
|
||||
use raw_window_handle as rwh;
|
||||
use std::num::NonZeroIsize;
|
||||
use windows::Win32::{Foundation::HWND, UI::WindowsAndMessaging::GWLP_HINSTANCE};
|
||||
|
||||
use crate::{get_window_long, show_error};
|
||||
|
||||
pub(super) fn init(
|
||||
context: &BladeContext,
|
||||
hwnd: HWND,
|
||||
transparent: bool,
|
||||
) -> anyhow::Result<BladeRenderer> {
|
||||
let raw = RawWindow { hwnd };
|
||||
let config = BladeSurfaceConfig {
|
||||
size: Default::default(),
|
||||
transparent,
|
||||
};
|
||||
BladeRenderer::new(context, &raw, config)
|
||||
.inspect_err(|err| show_error("Failed to initialize BladeRenderer", err.to_string()))
|
||||
}
|
||||
|
||||
struct RawWindow {
|
||||
hwnd: HWND,
|
||||
}
|
||||
|
||||
impl rwh::HasWindowHandle for RawWindow {
|
||||
fn window_handle(&self) -> Result<rwh::WindowHandle<'_>, rwh::HandleError> {
|
||||
Ok(unsafe {
|
||||
let hwnd = NonZeroIsize::new_unchecked(self.hwnd.0 as isize);
|
||||
let mut handle = rwh::Win32WindowHandle::new(hwnd);
|
||||
let hinstance = get_window_long(self.hwnd, GWLP_HINSTANCE);
|
||||
handle.hinstance = NonZeroIsize::new(hinstance);
|
||||
rwh::WindowHandle::borrow_raw(handle.into())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl rwh::HasDisplayHandle for RawWindow {
|
||||
fn display_handle(&self) -> Result<rwh::DisplayHandle<'_>, rwh::HandleError> {
|
||||
let handle = rwh::WindowsDisplayHandle::new();
|
||||
Ok(unsafe { rwh::DisplayHandle::borrow_raw(handle.into()) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::ClickState;
|
||||
|
||||
@@ -1020,7 +1020,7 @@ impl Window {
|
||||
|| (active.get()
|
||||
&& last_input_timestamp.get().elapsed() < Duration::from_secs(1));
|
||||
|
||||
if invalidator.is_dirty() {
|
||||
if invalidator.is_dirty() || request_frame_options.force_render {
|
||||
measure("frame duration", || {
|
||||
handle
|
||||
.update(&mut cx, |_, window, cx| {
|
||||
|
||||
@@ -236,6 +236,22 @@ impl HttpClientWithUrl {
|
||||
)?)
|
||||
}
|
||||
|
||||
/// Builds a Zed Cloud URL using the given path.
|
||||
pub fn build_zed_cloud_url(&self, path: &str, query: &[(&str, &str)]) -> Result<Url> {
|
||||
let base_url = self.base_url();
|
||||
let base_api_url = match base_url.as_ref() {
|
||||
"https://zed.dev" => "https://cloud.zed.dev",
|
||||
"https://staging.zed.dev" => "https://cloud.zed.dev",
|
||||
"http://localhost:3000" => "http://localhost:8787",
|
||||
other => other,
|
||||
};
|
||||
|
||||
Ok(Url::parse_with_params(
|
||||
&format!("{}{}", base_api_url, path),
|
||||
query,
|
||||
)?)
|
||||
}
|
||||
|
||||
/// Builds a Zed LLM URL using the given path.
|
||||
pub fn build_zed_llm_url(&self, path: &str, query: &[(&str, &str)]) -> Result<Url> {
|
||||
let base_url = self.base_url();
|
||||
|
||||
@@ -24,9 +24,13 @@ fs.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
project.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
settings.workspace = true
|
||||
theme.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
zlog.workspace = true
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
use editor::{EditorSettings, ShowMinimap};
|
||||
use fs::Fs;
|
||||
use gpui::{App, IntoElement, Pixels, Window};
|
||||
use gpui::{Action, 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,
|
||||
Clickable, ContextMenu, DropdownMenu, IconButton, Label, LabelCommon, LabelSize,
|
||||
NumericStepper, ParentElement, SharedString, Styled, SwitchColor, SwitchField,
|
||||
ToggleButtonGroup, ToggleButtonGroupStyle, ToggleButtonSimple, ToggleState, div, h_flex, px,
|
||||
v_flex,
|
||||
};
|
||||
|
||||
use crate::{ImportCursorSettings, ImportVsCodeSettings};
|
||||
|
||||
fn read_show_mini_map(cx: &App) -> ShowMinimap {
|
||||
editor::EditorSettings::get_global(cx).minimap.show
|
||||
}
|
||||
@@ -18,6 +21,14 @@ fn read_show_mini_map(cx: &App) -> ShowMinimap {
|
||||
fn write_show_mini_map(show: ShowMinimap, cx: &mut App) {
|
||||
let fs = <dyn Fs>::global(cx);
|
||||
|
||||
// This is used to speed up the UI
|
||||
// the UI reads the current values to get what toggle state to show on buttons
|
||||
// there's a slight delay if we just call update_settings_file so we manually set
|
||||
// the value here then call update_settings file to get around the delay
|
||||
let mut curr_settings = EditorSettings::get_global(cx).clone();
|
||||
curr_settings.minimap.show = show;
|
||||
EditorSettings::override_global(curr_settings, cx);
|
||||
|
||||
update_settings_file::<EditorSettings>(fs, cx, move |editor_settings, _| {
|
||||
editor_settings.minimap.get_or_insert_default().show = Some(show);
|
||||
});
|
||||
@@ -33,6 +44,10 @@ fn read_inlay_hints(cx: &App) -> bool {
|
||||
fn write_inlay_hints(enabled: bool, cx: &mut App) {
|
||||
let fs = <dyn Fs>::global(cx);
|
||||
|
||||
let mut curr_settings = AllLanguageSettings::get_global(cx).clone();
|
||||
curr_settings.defaults.inlay_hints.enabled = enabled;
|
||||
AllLanguageSettings::override_global(curr_settings, cx);
|
||||
|
||||
update_settings_file::<AllLanguageSettings>(fs, cx, move |all_language_settings, cx| {
|
||||
all_language_settings
|
||||
.defaults
|
||||
@@ -54,6 +69,14 @@ fn read_git_blame(cx: &App) -> bool {
|
||||
fn set_git_blame(enabled: bool, cx: &mut App) {
|
||||
let fs = <dyn Fs>::global(cx);
|
||||
|
||||
let mut curr_settings = ProjectSettings::get_global(cx).clone();
|
||||
curr_settings
|
||||
.git
|
||||
.inline_blame
|
||||
.get_or_insert_default()
|
||||
.enabled = enabled;
|
||||
ProjectSettings::override_global(curr_settings, cx);
|
||||
|
||||
update_settings_file::<ProjectSettings>(fs, cx, move |project_settings, _| {
|
||||
project_settings
|
||||
.git
|
||||
@@ -110,14 +133,22 @@ pub(crate) fn render_editing_page(window: &mut Window, cx: &mut App) -> impl Int
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.child(IconButton::new(
|
||||
"import-vs-code-settings",
|
||||
ui::IconName::Code,
|
||||
))
|
||||
.child(IconButton::new(
|
||||
"import-cursor-settings",
|
||||
ui::IconName::CursorIBeam,
|
||||
)),
|
||||
.child(
|
||||
IconButton::new("import-vs-code-settings", ui::IconName::Code).on_click(
|
||||
|_, window, cx| {
|
||||
window
|
||||
.dispatch_action(ImportVsCodeSettings::default().boxed_clone(), cx)
|
||||
},
|
||||
),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("import-cursor-settings", ui::IconName::CursorIBeam).on_click(
|
||||
|_, window, cx| {
|
||||
window
|
||||
.dispatch_action(ImportCursorSettings::default().boxed_clone(), cx)
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(Label::new("Popular Settings").size(LabelSize::Large))
|
||||
.child(
|
||||
@@ -160,16 +191,19 @@ pub(crate) fn render_editing_page(window: &mut Window, cx: &mut App) -> impl Int
|
||||
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(
|
||||
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);
|
||||
},
|
||||
)
|
||||
.border(),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
@@ -211,16 +245,19 @@ pub(crate) fn render_editing_page(window: &mut Window, cx: &mut App) -> impl Int
|
||||
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(
|
||||
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);
|
||||
},
|
||||
)
|
||||
.border(),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -4,10 +4,13 @@ use db::kvp::KEY_VALUE_STORE;
|
||||
use feature_flags::{FeatureFlag, FeatureFlagViewExt as _};
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
IntoElement, Render, SharedString, Subscription, Task, WeakEntity, Window, actions,
|
||||
Action, AnyElement, App, AppContext, AsyncWindowContext, Context, Entity, EventEmitter,
|
||||
FocusHandle, Focusable, IntoElement, Render, SharedString, Subscription, Task, WeakEntity,
|
||||
Window, actions,
|
||||
};
|
||||
use settings::{Settings, SettingsStore, update_settings_file};
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use settings::{Settings, SettingsStore, VsCodeSettingsSource, update_settings_file};
|
||||
use std::sync::Arc;
|
||||
use theme::{ThemeMode, ThemeSettings};
|
||||
use ui::{
|
||||
@@ -30,6 +33,24 @@ impl FeatureFlag for OnBoardingFeatureFlag {
|
||||
const NAME: &'static str = "onboarding";
|
||||
}
|
||||
|
||||
/// Imports settings from Visual Studio Code.
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq, Deserialize, JsonSchema, Action)]
|
||||
#[action(namespace = zed)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ImportVsCodeSettings {
|
||||
#[serde(default)]
|
||||
pub skip_prompt: bool,
|
||||
}
|
||||
|
||||
/// Imports settings from Cursor editor.
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq, Deserialize, JsonSchema, Action)]
|
||||
#[action(namespace = zed)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ImportCursorSettings {
|
||||
#[serde(default)]
|
||||
pub skip_prompt: bool,
|
||||
}
|
||||
|
||||
pub const FIRST_OPEN: &str = "first_open";
|
||||
|
||||
actions!(
|
||||
@@ -81,7 +102,7 @@ pub fn init(cx: &mut App) {
|
||||
if let Some(existing) = existing {
|
||||
workspace.activate_item(&existing, true, true, window, cx);
|
||||
} else {
|
||||
let settings_page = WelcomePage::new(cx);
|
||||
let settings_page = WelcomePage::new(window, cx);
|
||||
workspace.add_item_to_active_pane(
|
||||
Box::new(settings_page),
|
||||
None,
|
||||
@@ -95,6 +116,43 @@ pub fn init(cx: &mut App) {
|
||||
});
|
||||
});
|
||||
|
||||
cx.observe_new(|workspace: &mut Workspace, _window, _cx| {
|
||||
workspace.register_action(|_workspace, action: &ImportVsCodeSettings, window, cx| {
|
||||
let fs = <dyn Fs>::global(cx);
|
||||
let action = *action;
|
||||
|
||||
window
|
||||
.spawn(cx, async move |cx: &mut AsyncWindowContext| {
|
||||
handle_import_vscode_settings(
|
||||
VsCodeSettingsSource::VsCode,
|
||||
action.skip_prompt,
|
||||
fs,
|
||||
cx,
|
||||
)
|
||||
.await
|
||||
})
|
||||
.detach();
|
||||
});
|
||||
|
||||
workspace.register_action(|_workspace, action: &ImportCursorSettings, window, cx| {
|
||||
let fs = <dyn Fs>::global(cx);
|
||||
let action = *action;
|
||||
|
||||
window
|
||||
.spawn(cx, async move |cx: &mut AsyncWindowContext| {
|
||||
handle_import_vscode_settings(
|
||||
VsCodeSettingsSource::Cursor,
|
||||
action.skip_prompt,
|
||||
fs,
|
||||
cx,
|
||||
)
|
||||
.await
|
||||
})
|
||||
.detach();
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.observe_new::<Workspace>(|_, window, cx| {
|
||||
let Some(window) = window else {
|
||||
return;
|
||||
@@ -318,7 +376,7 @@ impl Render for Onboarding {
|
||||
),
|
||||
)
|
||||
.p_1()
|
||||
.child(Divider::horizontal_dashed())
|
||||
.child(Divider::horizontal())
|
||||
.child(
|
||||
v_flex().gap_1().children([
|
||||
self.render_page_nav(SelectedPage::Basics, window, cx)
|
||||
@@ -330,7 +388,7 @@ impl Render for Onboarding {
|
||||
]),
|
||||
),
|
||||
)
|
||||
// .child(Divider::vertical_dashed())
|
||||
.child(div().child(Divider::vertical()).h_full())
|
||||
.child(div().w_2_3().h_full().child(self.render_page(window, cx)))
|
||||
}
|
||||
}
|
||||
@@ -371,3 +429,54 @@ impl Item for Onboarding {
|
||||
f(*event)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn handle_import_vscode_settings(
|
||||
source: VsCodeSettingsSource,
|
||||
skip_prompt: bool,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &mut AsyncWindowContext,
|
||||
) {
|
||||
use util::truncate_and_remove_front;
|
||||
|
||||
let vscode_settings =
|
||||
match settings::VsCodeSettings::load_user_settings(source, fs.clone()).await {
|
||||
Ok(vscode_settings) => vscode_settings,
|
||||
Err(err) => {
|
||||
zlog::error!("{err}");
|
||||
let _ = cx.prompt(
|
||||
gpui::PromptLevel::Info,
|
||||
&format!("Could not find or load a {source} settings file"),
|
||||
None,
|
||||
&["Ok"],
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if !skip_prompt {
|
||||
let prompt = cx.prompt(
|
||||
gpui::PromptLevel::Warning,
|
||||
&format!(
|
||||
"Importing {} settings may overwrite your existing settings. \
|
||||
Will import settings from {}",
|
||||
vscode_settings.source,
|
||||
truncate_and_remove_front(&vscode_settings.path.to_string_lossy(), 128),
|
||||
),
|
||||
None,
|
||||
&["Ok", "Cancel"],
|
||||
);
|
||||
let result = cx.spawn(async move |_| prompt.await.ok()).await;
|
||||
if result != Some(0) {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
cx.update(|_, cx| {
|
||||
let source = vscode_settings.source;
|
||||
let path = vscode_settings.path.clone();
|
||||
cx.global::<SettingsStore>()
|
||||
.import_vscode_settings(fs, vscode_settings);
|
||||
zlog::info!("Imported {source} settings from {}", path.display());
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ use workspace::{
|
||||
NewFile, Open, Workspace, WorkspaceId,
|
||||
item::{Item, ItemEvent},
|
||||
};
|
||||
use zed_actions::{Extensions, OpenSettings, command_palette};
|
||||
use zed_actions::{Extensions, OpenSettings, agent, command_palette};
|
||||
|
||||
actions!(
|
||||
zed,
|
||||
@@ -55,8 +55,7 @@ const CONTENT: (Section<4>, Section<3>) = (
|
||||
SectionEntry {
|
||||
icon: IconName::ZedAssistant,
|
||||
title: "View AI Settings",
|
||||
// TODO: use proper action
|
||||
action: &NoAction,
|
||||
action: &agent::OpenSettings,
|
||||
},
|
||||
SectionEntry {
|
||||
icon: IconName::Blocks,
|
||||
@@ -228,12 +227,14 @@ impl Render for WelcomePage {
|
||||
}
|
||||
|
||||
impl WelcomePage {
|
||||
pub fn new(cx: &mut Context<Workspace>) -> Entity<Self> {
|
||||
let this = cx.new(|cx| WelcomePage {
|
||||
focus_handle: cx.focus_handle(),
|
||||
});
|
||||
pub fn new(window: &mut Window, cx: &mut Context<Workspace>) -> Entity<Self> {
|
||||
cx.new(|cx| {
|
||||
let focus_handle = cx.focus_handle();
|
||||
cx.on_focus(&focus_handle, window, |_, _, cx| cx.notify())
|
||||
.detach();
|
||||
|
||||
this
|
||||
WelcomePage { focus_handle }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ mod settings_json;
|
||||
mod settings_store;
|
||||
mod vscode_import;
|
||||
|
||||
use gpui::App;
|
||||
use gpui::{App, Global};
|
||||
use rust_embed::RustEmbed;
|
||||
use std::{borrow::Cow, fmt, str};
|
||||
use util::asset_str;
|
||||
@@ -27,6 +27,11 @@ pub use settings_store::{
|
||||
};
|
||||
pub use vscode_import::{VsCodeSettings, VsCodeSettingsSource};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct ActiveSettingsProfileName(pub String);
|
||||
|
||||
impl Global for ActiveSettingsProfileName {}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)]
|
||||
pub struct WorktreeId(usize);
|
||||
|
||||
@@ -74,6 +79,7 @@ pub fn init(cx: &mut App) {
|
||||
.unwrap();
|
||||
cx.set_global(settings);
|
||||
BaseKeymap::register(cx);
|
||||
SettingsStore::observe_active_settings_profile_name(cx).detach();
|
||||
}
|
||||
|
||||
pub fn default_settings() -> Cow<'static, str> {
|
||||
|
||||
@@ -26,8 +26,8 @@ use util::{
|
||||
pub type EditorconfigProperties = ec4rs::Properties;
|
||||
|
||||
use crate::{
|
||||
ParameterizedJsonSchema, SettingsJsonSchemaParams, VsCodeSettings, WorktreeId,
|
||||
parse_json_with_comments, update_value_in_json_text,
|
||||
ActiveSettingsProfileName, ParameterizedJsonSchema, SettingsJsonSchemaParams, VsCodeSettings,
|
||||
WorktreeId, parse_json_with_comments, update_value_in_json_text,
|
||||
};
|
||||
|
||||
/// A value that can be defined as a user setting.
|
||||
@@ -122,6 +122,8 @@ pub struct SettingsSources<'a, T> {
|
||||
pub user: Option<&'a T>,
|
||||
/// The user settings for the current release channel.
|
||||
pub release_channel: Option<&'a T>,
|
||||
/// The settings associated with an enabled settings profile
|
||||
pub profile: Option<&'a T>,
|
||||
/// The server's settings.
|
||||
pub server: Option<&'a T>,
|
||||
/// The project settings, ordered from least specific to most specific.
|
||||
@@ -141,6 +143,7 @@ impl<'a, T: Serialize> SettingsSources<'a, T> {
|
||||
.chain(self.extensions)
|
||||
.chain(self.user)
|
||||
.chain(self.release_channel)
|
||||
.chain(self.profile)
|
||||
.chain(self.server)
|
||||
.chain(self.project.iter().copied())
|
||||
}
|
||||
@@ -282,6 +285,14 @@ impl SettingsStore {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn observe_active_settings_profile_name(cx: &mut App) -> gpui::Subscription {
|
||||
cx.observe_global::<ActiveSettingsProfileName>(|cx| {
|
||||
Self::update_global(cx, |store, cx| {
|
||||
store.recompute_values(None, cx).log_err();
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
pub fn update<C, R>(cx: &mut C, f: impl FnOnce(&mut Self, &mut C) -> R) -> R
|
||||
where
|
||||
C: BorrowAppContext,
|
||||
@@ -321,6 +332,17 @@ impl SettingsStore {
|
||||
.log_err();
|
||||
}
|
||||
|
||||
let mut profile_value = None;
|
||||
if let Some(active_profile) = cx.try_global::<ActiveSettingsProfileName>() {
|
||||
if let Some(profiles) = self.raw_user_settings.get("profiles") {
|
||||
if let Some(profile_settings) = profiles.get(&active_profile.0) {
|
||||
profile_value = setting_value
|
||||
.deserialize_setting(profile_settings)
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let server_value = self
|
||||
.raw_server_settings
|
||||
.as_ref()
|
||||
@@ -340,6 +362,7 @@ impl SettingsStore {
|
||||
extensions: extension_value.as_ref(),
|
||||
user: user_value.as_ref(),
|
||||
release_channel: release_channel_value.as_ref(),
|
||||
profile: profile_value.as_ref(),
|
||||
server: server_value.as_ref(),
|
||||
project: &[],
|
||||
},
|
||||
@@ -402,6 +425,16 @@ impl SettingsStore {
|
||||
&self.raw_user_settings
|
||||
}
|
||||
|
||||
/// Get the configured settings profile names.
|
||||
pub fn configured_settings_profiles(&self) -> impl Iterator<Item = &str> {
|
||||
self.raw_user_settings
|
||||
.get("profiles")
|
||||
.and_then(|v| v.as_object())
|
||||
.into_iter()
|
||||
.flat_map(|obj| obj.keys())
|
||||
.map(|s| s.as_str())
|
||||
}
|
||||
|
||||
/// Access the raw JSON value of the global settings.
|
||||
pub fn raw_global_settings(&self) -> Option<&Value> {
|
||||
self.raw_global_settings.as_ref()
|
||||
@@ -532,7 +565,9 @@ impl SettingsStore {
|
||||
}))
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
impl SettingsStore {
|
||||
/// Updates the value of a setting in a JSON file, returning the new text
|
||||
/// for that JSON file.
|
||||
pub fn new_text_for_update<T: Settings>(
|
||||
@@ -1001,18 +1036,18 @@ impl SettingsStore {
|
||||
const ZED_SETTINGS: &str = "ZedSettings";
|
||||
let zed_settings_ref = add_new_subschema(&mut generator, ZED_SETTINGS, combined_schema);
|
||||
|
||||
// add `ZedReleaseStageSettings` which is the same as `ZedSettings` except that unknown
|
||||
// fields are rejected.
|
||||
let mut zed_release_stage_settings = zed_settings_ref.clone();
|
||||
zed_release_stage_settings.insert("unevaluatedProperties".to_string(), false.into());
|
||||
let zed_release_stage_settings_ref = add_new_subschema(
|
||||
// add `ZedSettingsOverride` which is the same as `ZedSettings` except that unknown
|
||||
// fields are rejected. This is used for release stage settings and profiles.
|
||||
let mut zed_settings_override = zed_settings_ref.clone();
|
||||
zed_settings_override.insert("unevaluatedProperties".to_string(), false.into());
|
||||
let zed_settings_override_ref = add_new_subschema(
|
||||
&mut generator,
|
||||
"ZedReleaseStageSettings",
|
||||
zed_release_stage_settings.to_value(),
|
||||
"ZedSettingsOverride",
|
||||
zed_settings_override.to_value(),
|
||||
);
|
||||
|
||||
// Remove `"additionalProperties": false` added by `DefaultDenyUnknownFields` so that
|
||||
// unknown fields can be handled by the root schema and `ZedReleaseStageSettings`.
|
||||
// unknown fields can be handled by the root schema and `ZedSettingsOverride`.
|
||||
let mut definitions = generator.take_definitions(true);
|
||||
definitions
|
||||
.get_mut(ZED_SETTINGS)
|
||||
@@ -1032,15 +1067,20 @@ impl SettingsStore {
|
||||
"$schema": meta_schema,
|
||||
"title": "Zed Settings",
|
||||
"unevaluatedProperties": false,
|
||||
// ZedSettings + settings overrides for each release stage
|
||||
// ZedSettings + settings overrides for each release stage / profiles
|
||||
"allOf": [
|
||||
zed_settings_ref,
|
||||
{
|
||||
"properties": {
|
||||
"dev": zed_release_stage_settings_ref,
|
||||
"nightly": zed_release_stage_settings_ref,
|
||||
"stable": zed_release_stage_settings_ref,
|
||||
"preview": zed_release_stage_settings_ref,
|
||||
"dev": zed_settings_override_ref,
|
||||
"nightly": zed_settings_override_ref,
|
||||
"stable": zed_settings_override_ref,
|
||||
"preview": zed_settings_override_ref,
|
||||
"profiles": {
|
||||
"type": "object",
|
||||
"description": "Configures any number of settings profiles that are temporarily applied when selected from `settings profile selector: toggle`.",
|
||||
"additionalProperties": zed_settings_override_ref
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -1099,6 +1139,16 @@ impl SettingsStore {
|
||||
}
|
||||
}
|
||||
|
||||
let mut profile_settings = None;
|
||||
if let Some(active_profile) = cx.try_global::<ActiveSettingsProfileName>() {
|
||||
if let Some(profiles) = self.raw_user_settings.get("profiles") {
|
||||
if let Some(profile_json) = profiles.get(&active_profile.0) {
|
||||
profile_settings =
|
||||
setting_value.deserialize_setting(profile_json).log_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the global settings file changed, reload the global value for the field.
|
||||
if changed_local_path.is_none() {
|
||||
if let Some(value) = setting_value
|
||||
@@ -1109,6 +1159,7 @@ impl SettingsStore {
|
||||
extensions: extension_settings.as_ref(),
|
||||
user: user_settings.as_ref(),
|
||||
release_channel: release_channel_settings.as_ref(),
|
||||
profile: profile_settings.as_ref(),
|
||||
server: server_settings.as_ref(),
|
||||
project: &[],
|
||||
},
|
||||
@@ -1161,6 +1212,7 @@ impl SettingsStore {
|
||||
extensions: extension_settings.as_ref(),
|
||||
user: user_settings.as_ref(),
|
||||
release_channel: release_channel_settings.as_ref(),
|
||||
profile: profile_settings.as_ref(),
|
||||
server: server_settings.as_ref(),
|
||||
project: &project_settings_stack.iter().collect::<Vec<_>>(),
|
||||
},
|
||||
@@ -1286,6 +1338,9 @@ impl<T: Settings> AnySettingValue for SettingValue<T> {
|
||||
release_channel: values
|
||||
.release_channel
|
||||
.map(|value| value.0.downcast_ref::<T::FileContent>().unwrap()),
|
||||
profile: values
|
||||
.profile
|
||||
.map(|value| value.0.downcast_ref::<T::FileContent>().unwrap()),
|
||||
server: values
|
||||
.server
|
||||
.map(|value| value.0.downcast_ref::<T::FileContent>().unwrap()),
|
||||
|
||||
33
crates/settings_profile_selector/Cargo.toml
Normal file
33
crates/settings_profile_selector/Cargo.toml
Normal file
@@ -0,0 +1,33 @@
|
||||
[package]
|
||||
name = "settings_profile_selector"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/settings_profile_selector.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
fuzzy.workspace = true
|
||||
gpui.workspace = true
|
||||
picker.workspace = true
|
||||
settings.workspace = true
|
||||
ui.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
language = { workspace = true, features = ["test-support"] }
|
||||
menu.workspace = true
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
serde_json.workspace = true
|
||||
settings = { workspace = true, features = ["test-support"] }
|
||||
workspace = { workspace = true, features = ["test-support"] }
|
||||
1
crates/settings_profile_selector/LICENSE-GPL
Symbolic link
1
crates/settings_profile_selector/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
@@ -0,0 +1,548 @@
|
||||
use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
|
||||
use gpui::{
|
||||
App, Context, DismissEvent, Entity, EventEmitter, Focusable, Render, Task, WeakEntity, Window,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use settings::{ActiveSettingsProfileName, SettingsStore};
|
||||
use ui::{HighlightedLabel, ListItem, ListItemSpacing, prelude::*};
|
||||
use workspace::{ModalView, Workspace};
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
cx.on_action(|_: &zed_actions::settings_profile_selector::Toggle, cx| {
|
||||
workspace::with_active_or_new_workspace(cx, |workspace, window, cx| {
|
||||
toggle_settings_profile_selector(workspace, window, cx);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn toggle_settings_profile_selector(
|
||||
workspace: &mut Workspace,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) {
|
||||
workspace.toggle_modal(window, cx, |window, cx| {
|
||||
let delegate = SettingsProfileSelectorDelegate::new(cx.entity().downgrade(), window, cx);
|
||||
SettingsProfileSelector::new(delegate, window, cx)
|
||||
});
|
||||
}
|
||||
|
||||
pub struct SettingsProfileSelector {
|
||||
picker: Entity<Picker<SettingsProfileSelectorDelegate>>,
|
||||
}
|
||||
|
||||
impl ModalView for SettingsProfileSelector {}
|
||||
|
||||
impl EventEmitter<DismissEvent> for SettingsProfileSelector {}
|
||||
|
||||
impl Focusable for SettingsProfileSelector {
|
||||
fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
|
||||
self.picker.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for SettingsProfileSelector {
|
||||
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
||||
v_flex().w(rems(34.)).child(self.picker.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl SettingsProfileSelector {
|
||||
pub fn new(
|
||||
delegate: SettingsProfileSelectorDelegate,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
|
||||
Self { picker }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SettingsProfileSelectorDelegate {
|
||||
matches: Vec<StringMatch>,
|
||||
profile_names: Vec<Option<String>>,
|
||||
original_profile_name: Option<String>,
|
||||
selected_profile_name: Option<String>,
|
||||
selected_index: usize,
|
||||
selection_completed: bool,
|
||||
selector: WeakEntity<SettingsProfileSelector>,
|
||||
}
|
||||
|
||||
impl SettingsProfileSelectorDelegate {
|
||||
fn new(
|
||||
selector: WeakEntity<SettingsProfileSelector>,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<SettingsProfileSelector>,
|
||||
) -> Self {
|
||||
let settings_store = cx.global::<SettingsStore>();
|
||||
let mut profile_names: Vec<String> = settings_store
|
||||
.configured_settings_profiles()
|
||||
.map(|s| s.to_string())
|
||||
.collect();
|
||||
|
||||
profile_names.sort();
|
||||
let mut profile_names: Vec<_> = profile_names.into_iter().map(Some).collect();
|
||||
profile_names.insert(0, None);
|
||||
|
||||
let matches = profile_names
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, profile_name)| StringMatch {
|
||||
candidate_id: ix,
|
||||
score: 0.0,
|
||||
positions: Default::default(),
|
||||
string: display_name(profile_name),
|
||||
})
|
||||
.collect();
|
||||
|
||||
let profile_name = cx
|
||||
.try_global::<ActiveSettingsProfileName>()
|
||||
.map(|p| p.0.clone());
|
||||
|
||||
let mut this = Self {
|
||||
matches,
|
||||
profile_names,
|
||||
original_profile_name: profile_name.clone(),
|
||||
selected_profile_name: None,
|
||||
selected_index: 0,
|
||||
selection_completed: false,
|
||||
selector,
|
||||
};
|
||||
|
||||
if let Some(profile_name) = profile_name {
|
||||
this.select_if_matching(&profile_name);
|
||||
}
|
||||
|
||||
this
|
||||
}
|
||||
|
||||
fn select_if_matching(&mut self, profile_name: &str) {
|
||||
self.selected_index = self
|
||||
.matches
|
||||
.iter()
|
||||
.position(|mat| mat.string == profile_name)
|
||||
.unwrap_or(self.selected_index);
|
||||
}
|
||||
|
||||
fn set_selected_profile(
|
||||
&self,
|
||||
cx: &mut Context<Picker<SettingsProfileSelectorDelegate>>,
|
||||
) -> Option<String> {
|
||||
let mat = self.matches.get(self.selected_index)?;
|
||||
let profile_name = self.profile_names.get(mat.candidate_id)?;
|
||||
return Self::update_active_profile_name_global(profile_name.clone(), cx);
|
||||
}
|
||||
|
||||
fn update_active_profile_name_global(
|
||||
profile_name: Option<String>,
|
||||
cx: &mut Context<Picker<SettingsProfileSelectorDelegate>>,
|
||||
) -> Option<String> {
|
||||
if let Some(profile_name) = profile_name {
|
||||
cx.set_global(ActiveSettingsProfileName(profile_name.clone()));
|
||||
return Some(profile_name.clone());
|
||||
}
|
||||
|
||||
if cx.has_global::<ActiveSettingsProfileName>() {
|
||||
cx.remove_global::<ActiveSettingsProfileName>();
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl PickerDelegate for SettingsProfileSelectorDelegate {
|
||||
type ListItem = ListItem;
|
||||
|
||||
fn placeholder_text(&self, _: &mut Window, _: &mut App) -> std::sync::Arc<str> {
|
||||
"Select a settings profile...".into()
|
||||
}
|
||||
|
||||
fn match_count(&self) -> usize {
|
||||
self.matches.len()
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
self.selected_index
|
||||
}
|
||||
|
||||
fn set_selected_index(
|
||||
&mut self,
|
||||
ix: usize,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Picker<SettingsProfileSelectorDelegate>>,
|
||||
) {
|
||||
self.selected_index = ix;
|
||||
self.selected_profile_name = self.set_selected_profile(cx);
|
||||
}
|
||||
|
||||
fn update_matches(
|
||||
&mut self,
|
||||
query: String,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Picker<SettingsProfileSelectorDelegate>>,
|
||||
) -> Task<()> {
|
||||
let background = cx.background_executor().clone();
|
||||
let candidates = self
|
||||
.profile_names
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(id, profile_name)| StringMatchCandidate::new(id, &display_name(profile_name)))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let matches = if query.is_empty() {
|
||||
candidates
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, candidate)| StringMatch {
|
||||
candidate_id: index,
|
||||
string: candidate.string,
|
||||
positions: Vec::new(),
|
||||
score: 0.0,
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
match_strings(
|
||||
&candidates,
|
||||
&query,
|
||||
false,
|
||||
true,
|
||||
100,
|
||||
&Default::default(),
|
||||
background,
|
||||
)
|
||||
.await
|
||||
};
|
||||
|
||||
this.update_in(cx, |this, _, cx| {
|
||||
this.delegate.matches = matches;
|
||||
this.delegate.selected_index = this
|
||||
.delegate
|
||||
.selected_index
|
||||
.min(this.delegate.matches.len().saturating_sub(1));
|
||||
this.delegate.selected_profile_name = this.delegate.set_selected_profile(cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
}
|
||||
|
||||
fn confirm(
|
||||
&mut self,
|
||||
_: bool,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Picker<SettingsProfileSelectorDelegate>>,
|
||||
) {
|
||||
self.selection_completed = true;
|
||||
self.selector
|
||||
.update(cx, |_, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
fn dismissed(
|
||||
&mut self,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Picker<SettingsProfileSelectorDelegate>>,
|
||||
) {
|
||||
if !self.selection_completed {
|
||||
SettingsProfileSelectorDelegate::update_active_profile_name_global(
|
||||
self.original_profile_name.clone(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
self.selector.update(cx, |_, cx| cx.emit(DismissEvent)).ok();
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
_: &mut Window,
|
||||
_: &mut Context<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let mat = &self.matches[ix];
|
||||
let profile_name = &self.profile_names[mat.candidate_id];
|
||||
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.toggle_state(selected)
|
||||
.child(HighlightedLabel::new(
|
||||
display_name(profile_name),
|
||||
mat.positions.clone(),
|
||||
)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn display_name(profile_name: &Option<String>) -> String {
|
||||
profile_name.clone().unwrap_or("Disabled".into())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use editor;
|
||||
use gpui::{TestAppContext, UpdateGlobal, VisualTestContext};
|
||||
use language;
|
||||
use menu::{Cancel, Confirm, SelectNext, SelectPrevious};
|
||||
use project::{FakeFs, Project};
|
||||
use serde_json::json;
|
||||
use workspace::{self, AppState};
|
||||
use zed_actions::settings_profile_selector;
|
||||
|
||||
async fn init_test(
|
||||
profiles_json: serde_json::Value,
|
||||
cx: &mut TestAppContext,
|
||||
) -> (Entity<Workspace>, &mut VisualTestContext) {
|
||||
cx.update(|cx| {
|
||||
let state = AppState::test(cx);
|
||||
language::init(cx);
|
||||
super::init(cx);
|
||||
editor::init(cx);
|
||||
workspace::init_settings(cx);
|
||||
Project::init_settings(cx);
|
||||
state
|
||||
});
|
||||
|
||||
cx.update(|cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
let settings_json = json!({
|
||||
"profiles": profiles_json
|
||||
});
|
||||
|
||||
store
|
||||
.set_user_settings(&settings_json.to_string(), cx)
|
||||
.unwrap();
|
||||
});
|
||||
});
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
let project = Project::test(fs, ["/test".as_ref()], cx).await;
|
||||
let (workspace, cx) =
|
||||
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
|
||||
cx.update(|_, cx| {
|
||||
assert!(!cx.has_global::<ActiveSettingsProfileName>());
|
||||
});
|
||||
|
||||
(workspace, cx)
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn active_settings_profile_picker(
|
||||
workspace: &Entity<Workspace>,
|
||||
cx: &mut VisualTestContext,
|
||||
) -> Entity<Picker<SettingsProfileSelectorDelegate>> {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace
|
||||
.active_modal::<SettingsProfileSelector>(cx)
|
||||
.expect("settings profile selector is not open")
|
||||
.read(cx)
|
||||
.picker
|
||||
.clone()
|
||||
})
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_settings_profile_selector_state(cx: &mut TestAppContext) {
|
||||
let profiles_json = json!({
|
||||
"Demo Videos": {
|
||||
"buffer_font_size": 14
|
||||
},
|
||||
"Classroom / Streaming": {
|
||||
"buffer_font_size": 16,
|
||||
"vim_mode": true
|
||||
}
|
||||
});
|
||||
let (workspace, cx) = init_test(profiles_json.clone(), cx).await;
|
||||
|
||||
cx.dispatch_action(settings_profile_selector::Toggle);
|
||||
|
||||
let picker = active_settings_profile_picker(&workspace, cx);
|
||||
|
||||
picker.read_with(cx, |picker, cx| {
|
||||
assert_eq!(picker.delegate.matches.len(), 3);
|
||||
assert_eq!(picker.delegate.matches[0].string, "Disabled");
|
||||
assert_eq!(picker.delegate.matches[1].string, "Classroom / Streaming");
|
||||
assert_eq!(picker.delegate.matches[2].string, "Demo Videos");
|
||||
assert_eq!(picker.delegate.matches.get(3), None);
|
||||
|
||||
assert_eq!(picker.delegate.selected_index, 0);
|
||||
assert_eq!(picker.delegate.selected_profile_name, None);
|
||||
|
||||
assert_eq!(cx.try_global::<ActiveSettingsProfileName>(), None);
|
||||
});
|
||||
|
||||
cx.dispatch_action(Confirm);
|
||||
|
||||
cx.update(|_, cx| {
|
||||
assert_eq!(cx.try_global::<ActiveSettingsProfileName>(), None);
|
||||
});
|
||||
|
||||
cx.dispatch_action(settings_profile_selector::Toggle);
|
||||
let picker = active_settings_profile_picker(&workspace, cx);
|
||||
cx.dispatch_action(SelectNext);
|
||||
|
||||
picker.read_with(cx, |picker, cx| {
|
||||
assert_eq!(picker.delegate.selected_index, 1);
|
||||
assert_eq!(
|
||||
picker.delegate.selected_profile_name,
|
||||
Some("Classroom / Streaming".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
cx.try_global::<ActiveSettingsProfileName>()
|
||||
.map(|p| p.0.clone()),
|
||||
Some("Classroom / Streaming".to_string())
|
||||
);
|
||||
});
|
||||
|
||||
cx.dispatch_action(Cancel);
|
||||
|
||||
cx.update(|_, cx| {
|
||||
assert_eq!(cx.try_global::<ActiveSettingsProfileName>(), None);
|
||||
});
|
||||
|
||||
cx.dispatch_action(settings_profile_selector::Toggle);
|
||||
let picker = active_settings_profile_picker(&workspace, cx);
|
||||
|
||||
cx.dispatch_action(SelectNext);
|
||||
|
||||
picker.read_with(cx, |picker, cx| {
|
||||
assert_eq!(picker.delegate.selected_index, 1);
|
||||
assert_eq!(
|
||||
picker.delegate.selected_profile_name,
|
||||
Some("Classroom / Streaming".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
cx.try_global::<ActiveSettingsProfileName>()
|
||||
.map(|p| p.0.clone()),
|
||||
Some("Classroom / Streaming".to_string())
|
||||
);
|
||||
});
|
||||
|
||||
cx.dispatch_action(SelectNext);
|
||||
|
||||
picker.read_with(cx, |picker, cx| {
|
||||
assert_eq!(picker.delegate.selected_index, 2);
|
||||
assert_eq!(
|
||||
picker.delegate.selected_profile_name,
|
||||
Some("Demo Videos".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
cx.try_global::<ActiveSettingsProfileName>()
|
||||
.map(|p| p.0.clone()),
|
||||
Some("Demo Videos".to_string())
|
||||
);
|
||||
});
|
||||
|
||||
cx.dispatch_action(Confirm);
|
||||
|
||||
cx.update(|_, cx| {
|
||||
assert_eq!(
|
||||
cx.try_global::<ActiveSettingsProfileName>()
|
||||
.map(|p| p.0.clone()),
|
||||
Some("Demo Videos".to_string())
|
||||
);
|
||||
});
|
||||
|
||||
cx.dispatch_action(settings_profile_selector::Toggle);
|
||||
let picker = active_settings_profile_picker(&workspace, cx);
|
||||
|
||||
picker.read_with(cx, |picker, cx| {
|
||||
assert_eq!(picker.delegate.selected_index, 2);
|
||||
assert_eq!(
|
||||
picker.delegate.selected_profile_name,
|
||||
Some("Demo Videos".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
cx.try_global::<ActiveSettingsProfileName>()
|
||||
.map(|p| p.0.clone()),
|
||||
Some("Demo Videos".to_string())
|
||||
);
|
||||
});
|
||||
|
||||
cx.dispatch_action(SelectPrevious);
|
||||
|
||||
picker.read_with(cx, |picker, cx| {
|
||||
assert_eq!(picker.delegate.selected_index, 1);
|
||||
assert_eq!(
|
||||
picker.delegate.selected_profile_name,
|
||||
Some("Classroom / Streaming".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
cx.try_global::<ActiveSettingsProfileName>()
|
||||
.map(|p| p.0.clone()),
|
||||
Some("Classroom / Streaming".to_string())
|
||||
);
|
||||
});
|
||||
|
||||
cx.dispatch_action(Cancel);
|
||||
|
||||
cx.update(|_, cx| {
|
||||
assert_eq!(
|
||||
cx.try_global::<ActiveSettingsProfileName>()
|
||||
.map(|p| p.0.clone()),
|
||||
Some("Demo Videos".to_string())
|
||||
);
|
||||
});
|
||||
|
||||
cx.dispatch_action(settings_profile_selector::Toggle);
|
||||
let picker = active_settings_profile_picker(&workspace, cx);
|
||||
|
||||
picker.read_with(cx, |picker, cx| {
|
||||
assert_eq!(picker.delegate.selected_index, 2);
|
||||
assert_eq!(
|
||||
picker.delegate.selected_profile_name,
|
||||
Some("Demo Videos".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
cx.try_global::<ActiveSettingsProfileName>()
|
||||
.map(|p| p.0.clone()),
|
||||
Some("Demo Videos".to_string())
|
||||
);
|
||||
});
|
||||
|
||||
cx.dispatch_action(SelectPrevious);
|
||||
|
||||
picker.read_with(cx, |picker, cx| {
|
||||
assert_eq!(picker.delegate.selected_index, 1);
|
||||
assert_eq!(
|
||||
picker.delegate.selected_profile_name,
|
||||
Some("Classroom / Streaming".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
cx.try_global::<ActiveSettingsProfileName>()
|
||||
.map(|p| p.0.clone()),
|
||||
Some("Classroom / Streaming".to_string())
|
||||
);
|
||||
});
|
||||
|
||||
cx.dispatch_action(SelectPrevious);
|
||||
|
||||
picker.read_with(cx, |picker, cx| {
|
||||
assert_eq!(picker.delegate.selected_index, 0);
|
||||
assert_eq!(picker.delegate.selected_profile_name, None);
|
||||
|
||||
assert_eq!(
|
||||
cx.try_global::<ActiveSettingsProfileName>()
|
||||
.map(|p| p.0.clone()),
|
||||
None
|
||||
);
|
||||
});
|
||||
|
||||
cx.dispatch_action(Confirm);
|
||||
|
||||
cx.update(|_, cx| {
|
||||
assert_eq!(cx.try_global::<ActiveSettingsProfileName>(), None);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,6 @@ menu.workspace = true
|
||||
notifications.workspace = true
|
||||
paths.workspace = true
|
||||
project.workspace = true
|
||||
schemars.workspace = true
|
||||
search.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
@@ -1,20 +1,12 @@
|
||||
mod appearance_settings_controls;
|
||||
|
||||
use std::any::TypeId;
|
||||
use std::sync::Arc;
|
||||
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
use editor::EditorSettingsControls;
|
||||
use feature_flags::{FeatureFlag, FeatureFlagViewExt};
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
Action, App, AsyncWindowContext, Entity, EventEmitter, FocusHandle, Focusable, Task, actions,
|
||||
};
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use settings::{SettingsStore, VsCodeSettingsSource};
|
||||
use gpui::{App, Entity, EventEmitter, FocusHandle, Focusable, actions};
|
||||
use ui::prelude::*;
|
||||
use util::truncate_and_remove_front;
|
||||
use workspace::item::{Item, ItemEvent};
|
||||
use workspace::{Workspace, with_active_or_new_workspace};
|
||||
|
||||
@@ -29,23 +21,6 @@ impl FeatureFlag for SettingsUiFeatureFlag {
|
||||
const NAME: &'static str = "settings-ui";
|
||||
}
|
||||
|
||||
/// Imports settings from Visual Studio Code.
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq, Deserialize, JsonSchema, Action)]
|
||||
#[action(namespace = zed)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ImportVsCodeSettings {
|
||||
#[serde(default)]
|
||||
pub skip_prompt: bool,
|
||||
}
|
||||
|
||||
/// Imports settings from Cursor editor.
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq, Deserialize, JsonSchema, Action)]
|
||||
#[action(namespace = zed)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ImportCursorSettings {
|
||||
#[serde(default)]
|
||||
pub skip_prompt: bool,
|
||||
}
|
||||
actions!(
|
||||
zed,
|
||||
[
|
||||
@@ -72,45 +47,11 @@ pub fn init(cx: &mut App) {
|
||||
});
|
||||
});
|
||||
|
||||
cx.observe_new(|workspace: &mut Workspace, window, cx| {
|
||||
cx.observe_new(|_workspace: &mut Workspace, window, cx| {
|
||||
let Some(window) = window else {
|
||||
return;
|
||||
};
|
||||
|
||||
workspace.register_action(|_workspace, action: &ImportVsCodeSettings, window, cx| {
|
||||
let fs = <dyn Fs>::global(cx);
|
||||
let action = *action;
|
||||
|
||||
window
|
||||
.spawn(cx, async move |cx: &mut AsyncWindowContext| {
|
||||
handle_import_vscode_settings(
|
||||
VsCodeSettingsSource::VsCode,
|
||||
action.skip_prompt,
|
||||
fs,
|
||||
cx,
|
||||
)
|
||||
.await
|
||||
})
|
||||
.detach();
|
||||
});
|
||||
|
||||
workspace.register_action(|_workspace, action: &ImportCursorSettings, window, cx| {
|
||||
let fs = <dyn Fs>::global(cx);
|
||||
let action = *action;
|
||||
|
||||
window
|
||||
.spawn(cx, async move |cx: &mut AsyncWindowContext| {
|
||||
handle_import_vscode_settings(
|
||||
VsCodeSettingsSource::Cursor,
|
||||
action.skip_prompt,
|
||||
fs,
|
||||
cx,
|
||||
)
|
||||
.await
|
||||
})
|
||||
.detach();
|
||||
});
|
||||
|
||||
let settings_ui_actions = [TypeId::of::<OpenSettingsEditor>()];
|
||||
|
||||
CommandPaletteFilter::update_global(cx, |filter, _cx| {
|
||||
@@ -138,57 +79,6 @@ pub fn init(cx: &mut App) {
|
||||
keybindings::init(cx);
|
||||
}
|
||||
|
||||
async fn handle_import_vscode_settings(
|
||||
source: VsCodeSettingsSource,
|
||||
skip_prompt: bool,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &mut AsyncWindowContext,
|
||||
) {
|
||||
let vscode_settings =
|
||||
match settings::VsCodeSettings::load_user_settings(source, fs.clone()).await {
|
||||
Ok(vscode_settings) => vscode_settings,
|
||||
Err(err) => {
|
||||
log::error!("{err}");
|
||||
let _ = cx.prompt(
|
||||
gpui::PromptLevel::Info,
|
||||
&format!("Could not find or load a {source} settings file"),
|
||||
None,
|
||||
&["Ok"],
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let prompt = if skip_prompt {
|
||||
Task::ready(Some(0))
|
||||
} else {
|
||||
let prompt = cx.prompt(
|
||||
gpui::PromptLevel::Warning,
|
||||
&format!(
|
||||
"Importing {} settings may overwrite your existing settings. \
|
||||
Will import settings from {}",
|
||||
vscode_settings.source,
|
||||
truncate_and_remove_front(&vscode_settings.path.to_string_lossy(), 128),
|
||||
),
|
||||
None,
|
||||
&["Ok", "Cancel"],
|
||||
);
|
||||
cx.spawn(async move |_| prompt.await.ok())
|
||||
};
|
||||
if prompt.await != Some(0) {
|
||||
return;
|
||||
}
|
||||
|
||||
cx.update(|_, cx| {
|
||||
let source = vscode_settings.source;
|
||||
let path = vscode_settings.path.clone();
|
||||
cx.global::<SettingsStore>()
|
||||
.import_vscode_settings(fs, vscode_settings);
|
||||
log::info!("Imported {source} settings from {}", path.display());
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
pub struct SettingsPage {
|
||||
focus_handle: FocusHandle,
|
||||
}
|
||||
|
||||
@@ -867,6 +867,7 @@ impl settings::Settings for ThemeSettings {
|
||||
.user
|
||||
.into_iter()
|
||||
.chain(sources.release_channel)
|
||||
.chain(sources.profile)
|
||||
.chain(sources.server)
|
||||
{
|
||||
if let Some(value) = value.ui_density {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use gpui::ClickEvent;
|
||||
|
||||
use crate::{IconButtonShape, prelude::*};
|
||||
use crate::{Divider, IconButtonShape, prelude::*};
|
||||
|
||||
#[derive(IntoElement, RegisterComponent)]
|
||||
pub struct NumericStepper {
|
||||
@@ -11,6 +11,7 @@ pub struct NumericStepper {
|
||||
/// Whether to reserve space for the reset button.
|
||||
reserve_space_for_reset: bool,
|
||||
on_reset: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
|
||||
border: bool,
|
||||
}
|
||||
|
||||
impl NumericStepper {
|
||||
@@ -25,6 +26,7 @@ impl NumericStepper {
|
||||
value: value.into(),
|
||||
on_decrement: Box::new(on_decrement),
|
||||
on_increment: Box::new(on_increment),
|
||||
border: false,
|
||||
reserve_space_for_reset: false,
|
||||
on_reset: None,
|
||||
}
|
||||
@@ -42,6 +44,11 @@ impl NumericStepper {
|
||||
self.on_reset = Some(Box::new(on_reset));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn border(mut self) -> Self {
|
||||
self.border = true;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for NumericStepper {
|
||||
@@ -74,8 +81,11 @@ impl RenderOnce for NumericStepper {
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.when(self.border, |this| {
|
||||
this.border_1().border_color(cx.theme().colors().border)
|
||||
})
|
||||
.px_1()
|
||||
.rounded_xs()
|
||||
.rounded_sm()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.child(
|
||||
IconButton::new("decrement", IconName::Dash)
|
||||
@@ -83,7 +93,13 @@ impl RenderOnce for NumericStepper {
|
||||
.icon_size(icon_size)
|
||||
.on_click(self.on_decrement),
|
||||
)
|
||||
.when(self.border, |this| {
|
||||
this.child(Divider::vertical().color(super::DividerColor::Border))
|
||||
})
|
||||
.child(Label::new(self.value))
|
||||
.when(self.border, |this| {
|
||||
this.child(Divider::vertical().color(super::DividerColor::Border))
|
||||
})
|
||||
.child(
|
||||
IconButton::new("increment", IconName::Plus)
|
||||
.shape(shape)
|
||||
@@ -113,12 +129,27 @@ impl Component for NumericStepper {
|
||||
|
||||
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
|
||||
Some(
|
||||
div()
|
||||
.child(NumericStepper::new(
|
||||
"numeric-stepper-component-preview",
|
||||
"10",
|
||||
move |_, _, _| {},
|
||||
move |_, _, _| {},
|
||||
v_flex()
|
||||
.child(single_example(
|
||||
"Borderless",
|
||||
NumericStepper::new(
|
||||
"numeric-stepper-component-preview",
|
||||
"10",
|
||||
move |_, _, _| {},
|
||||
move |_, _, _| {},
|
||||
)
|
||||
.into_any_element(),
|
||||
))
|
||||
.child(single_example(
|
||||
"Border",
|
||||
NumericStepper::new(
|
||||
"numeric-stepper-with-border-component-preview",
|
||||
"10",
|
||||
move |_, _, _| {},
|
||||
move |_, _, _| {},
|
||||
)
|
||||
.border()
|
||||
.into_any_element(),
|
||||
))
|
||||
.into_any_element(),
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use gpui::{
|
||||
AnyElement, AnyView, ClickEvent, ElementId, Hsla, IntoElement, Styled, Window, div, hsla,
|
||||
prelude::*,
|
||||
AnyElement, AnyView, ClickEvent, CursorStyle, ElementId, Hsla, IntoElement, Styled, Window,
|
||||
div, hsla, prelude::*,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -609,6 +609,9 @@ impl RenderOnce for SwitchField {
|
||||
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
||||
h_flex()
|
||||
.id(SharedString::from(format!("{}-container", self.id)))
|
||||
.when(!self.disabled, |this| {
|
||||
this.hover(|this| this.cursor(CursorStyle::PointingHand))
|
||||
})
|
||||
.w_full()
|
||||
.gap_4()
|
||||
.justify_between()
|
||||
|
||||
@@ -43,7 +43,7 @@ fn zed_prompt_renderer(
|
||||
let renderer = cx.new({
|
||||
|cx| ZedPromptRenderer {
|
||||
_level: level,
|
||||
message: message.to_string(),
|
||||
message: cx.new(|cx| Markdown::new(SharedString::new(message), None, None, cx)),
|
||||
actions: actions.iter().map(|a| a.label().to_string()).collect(),
|
||||
focus: cx.focus_handle(),
|
||||
active_action_id: 0,
|
||||
@@ -58,7 +58,7 @@ fn zed_prompt_renderer(
|
||||
|
||||
pub struct ZedPromptRenderer {
|
||||
_level: PromptLevel,
|
||||
message: String,
|
||||
message: Entity<Markdown>,
|
||||
actions: Vec<String>,
|
||||
focus: FocusHandle,
|
||||
active_action_id: usize,
|
||||
@@ -114,7 +114,7 @@ impl ZedPromptRenderer {
|
||||
impl Render for ZedPromptRenderer {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let font_family = settings.ui_font.family.clone();
|
||||
let font_size = settings.ui_font_size(cx).into();
|
||||
let prompt = v_flex()
|
||||
.key_context("Prompt")
|
||||
.cursor_default()
|
||||
@@ -130,24 +130,38 @@ impl Render for ZedPromptRenderer {
|
||||
.overflow_hidden()
|
||||
.p_4()
|
||||
.gap_4()
|
||||
.font_family(font_family)
|
||||
.font_family(settings.ui_font.family.clone())
|
||||
.child(
|
||||
div()
|
||||
.w_full()
|
||||
.font_weight(FontWeight::BOLD)
|
||||
.child(self.message.clone())
|
||||
.text_color(ui::Color::Default.color(cx)),
|
||||
.child(MarkdownElement::new(self.message.clone(), {
|
||||
let mut base_text_style = window.text_style();
|
||||
base_text_style.refine(&TextStyleRefinement {
|
||||
font_family: Some(settings.ui_font.family.clone()),
|
||||
font_size: Some(font_size),
|
||||
font_weight: Some(FontWeight::BOLD),
|
||||
color: Some(ui::Color::Default.color(cx)),
|
||||
..Default::default()
|
||||
});
|
||||
MarkdownStyle {
|
||||
base_text_style,
|
||||
selection_background_color: cx
|
||||
.theme()
|
||||
.colors()
|
||||
.element_selection_background,
|
||||
..Default::default()
|
||||
}
|
||||
})),
|
||||
)
|
||||
.children(self.detail.clone().map(|detail| {
|
||||
div()
|
||||
.w_full()
|
||||
.text_xs()
|
||||
.child(MarkdownElement::new(detail, {
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let mut base_text_style = window.text_style();
|
||||
base_text_style.refine(&TextStyleRefinement {
|
||||
font_family: Some(settings.ui_font.family.clone()),
|
||||
font_size: Some(settings.ui_font_size(cx).into()),
|
||||
font_size: Some(font_size),
|
||||
color: Some(ui::Color::Muted.color(cx)),
|
||||
..Default::default()
|
||||
});
|
||||
@@ -176,24 +190,28 @@ impl Render for ZedPromptRenderer {
|
||||
}),
|
||||
));
|
||||
|
||||
div().size_full().occlude().child(
|
||||
div()
|
||||
.size_full()
|
||||
.absolute()
|
||||
.top_0()
|
||||
.left_0()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.justify_around()
|
||||
.child(
|
||||
div()
|
||||
.w_full()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.justify_around()
|
||||
.child(prompt),
|
||||
),
|
||||
)
|
||||
div()
|
||||
.size_full()
|
||||
.occlude()
|
||||
.bg(gpui::black().opacity(0.2))
|
||||
.child(
|
||||
div()
|
||||
.size_full()
|
||||
.absolute()
|
||||
.top_0()
|
||||
.left_0()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.justify_around()
|
||||
.child(
|
||||
div()
|
||||
.w_full()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.justify_around()
|
||||
.child(prompt),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ mod toast_layer;
|
||||
mod toolbar;
|
||||
mod workspace_settings;
|
||||
|
||||
use client::CloudUserStore;
|
||||
pub use toast_layer::{ToastAction, ToastLayer, ToastView};
|
||||
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
@@ -839,6 +840,7 @@ pub struct AppState {
|
||||
pub languages: Arc<LanguageRegistry>,
|
||||
pub client: Arc<Client>,
|
||||
pub user_store: Entity<UserStore>,
|
||||
pub cloud_user_store: Entity<CloudUserStore>,
|
||||
pub workspace_store: Entity<WorkspaceStore>,
|
||||
pub fs: Arc<dyn fs::Fs>,
|
||||
pub build_window_options: fn(Option<Uuid>, &mut App) -> WindowOptions,
|
||||
@@ -911,6 +913,7 @@ impl AppState {
|
||||
let client = Client::new(clock, http_client.clone(), cx);
|
||||
let session = cx.new(|cx| AppSession::new(Session::test(), cx));
|
||||
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
|
||||
let cloud_user_store = cx.new(|cx| CloudUserStore::new(client.cloud_client(), cx));
|
||||
let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx));
|
||||
|
||||
theme::init(theme::LoadThemes::JustBase, cx);
|
||||
@@ -922,6 +925,7 @@ impl AppState {
|
||||
fs,
|
||||
languages,
|
||||
user_store,
|
||||
cloud_user_store,
|
||||
workspace_store,
|
||||
node_runtime: NodeRuntime::unavailable(),
|
||||
build_window_options: |_, _| Default::default(),
|
||||
@@ -5689,6 +5693,7 @@ impl Workspace {
|
||||
|
||||
let client = project.read(cx).client();
|
||||
let user_store = project.read(cx).user_store();
|
||||
let cloud_user_store = cx.new(|cx| CloudUserStore::new(client.cloud_client(), cx));
|
||||
|
||||
let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx));
|
||||
let session = cx.new(|cx| AppSession::new(Session::test(), cx));
|
||||
@@ -5696,6 +5701,7 @@ impl Workspace {
|
||||
let app_state = Arc::new(AppState {
|
||||
languages: project.read(cx).languages().clone(),
|
||||
workspace_store,
|
||||
cloud_user_store,
|
||||
client,
|
||||
user_store,
|
||||
fs: project.read(cx).fs().clone(),
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
description = "The fast, collaborative code editor."
|
||||
edition.workspace = true
|
||||
name = "zed"
|
||||
version = "0.198.0"
|
||||
version = "0.199.0"
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
authors = ["Zed Team <hi@zed.dev>"]
|
||||
@@ -106,6 +106,7 @@ outline_panel.workspace = true
|
||||
parking_lot.workspace = true
|
||||
paths.workspace = true
|
||||
picker.workspace = true
|
||||
settings_profile_selector.workspace = true
|
||||
profiling.workspace = true
|
||||
project.workspace = true
|
||||
project_panel.workspace = true
|
||||
|
||||
@@ -62,6 +62,7 @@ Source: "{#ResourcesDir}\Zed.exe"; DestDir: "{code:GetInstallDir}"; Flags: ignor
|
||||
Source: "{#ResourcesDir}\bin\*"; DestDir: "{code:GetInstallDir}\bin"; Flags: ignoreversion
|
||||
Source: "{#ResourcesDir}\tools\*"; DestDir: "{app}\tools"; Flags: ignoreversion
|
||||
Source: "{#ResourcesDir}\appx\*"; DestDir: "{app}\appx"; BeforeInstall: RemoveAppxPackage; AfterInstall: AddAppxPackage; Flags: ignoreversion; Check: IsWindows11OrLater
|
||||
Source: "{#ResourcesDir}\amd_ags_x64.dll"; DestDir: "{app}"; Flags: ignoreversion
|
||||
|
||||
[Icons]
|
||||
Name: "{group}\{#AppName}"; Filename: "{app}\{#AppExeName}.exe"; AppUserModelID: "{#AppUserId}"
|
||||
|
||||
@@ -5,7 +5,7 @@ use agent_ui::AgentPanel;
|
||||
use anyhow::{Context as _, Result};
|
||||
use clap::{Parser, command};
|
||||
use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
|
||||
use client::{Client, ProxySettings, UserStore, parse_zed_link};
|
||||
use client::{Client, CloudUserStore, ProxySettings, UserStore, parse_zed_link};
|
||||
use collab_ui::channel_view::ChannelView;
|
||||
use collections::HashMap;
|
||||
use db::kvp::{GLOBAL_KEY_VALUE_STORE, KEY_VALUE_STORE};
|
||||
@@ -457,6 +457,7 @@ pub fn main() {
|
||||
language::init(cx);
|
||||
languages::init(languages.clone(), node_runtime.clone(), cx);
|
||||
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
|
||||
let cloud_user_store = cx.new(|cx| CloudUserStore::new(client.cloud_client(), cx));
|
||||
let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx));
|
||||
|
||||
language_extension::init(
|
||||
@@ -516,6 +517,7 @@ pub fn main() {
|
||||
languages: languages.clone(),
|
||||
client: client.clone(),
|
||||
user_store: user_store.clone(),
|
||||
cloud_user_store,
|
||||
fs: fs.clone(),
|
||||
build_window_options,
|
||||
workspace_store,
|
||||
@@ -613,6 +615,7 @@ pub fn main() {
|
||||
language_selector::init(cx);
|
||||
toolchain_selector::init(cx);
|
||||
theme_selector::init(cx);
|
||||
settings_profile_selector::init(cx);
|
||||
language_tools::init(cx);
|
||||
call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
|
||||
notifications::init(app_state.client.clone(), app_state.user_store.clone(), cx);
|
||||
|
||||
@@ -4366,6 +4366,7 @@ mod tests {
|
||||
"repl",
|
||||
"rules_library",
|
||||
"search",
|
||||
"settings_profile_selector",
|
||||
"snippets",
|
||||
"supermaven",
|
||||
"svg",
|
||||
|
||||
@@ -260,6 +260,16 @@ pub mod icon_theme_selector {
|
||||
}
|
||||
}
|
||||
|
||||
pub mod settings_profile_selector {
|
||||
use gpui::Action;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)]
|
||||
#[action(namespace = settings_profile_selector)]
|
||||
pub struct Toggle;
|
||||
}
|
||||
|
||||
pub mod agent {
|
||||
use gpui::actions;
|
||||
|
||||
|
||||
@@ -2588,6 +2588,7 @@ List of `integer` column numbers
|
||||
"font_features": null,
|
||||
"font_size": null,
|
||||
"line_height": "comfortable",
|
||||
"minimum_contrast": 45,
|
||||
"option_as_meta": false,
|
||||
"button": true,
|
||||
"shell": "system",
|
||||
@@ -2883,6 +2884,30 @@ See Buffer Font Features
|
||||
}
|
||||
```
|
||||
|
||||
### Terminal: Minimum Contrast
|
||||
|
||||
- Description: Controls the minimum contrast between foreground and background colors in the terminal. Uses the APCA (Accessible Perceptual Contrast Algorithm) for color adjustments. Set this to 0 to disable this feature.
|
||||
- Setting: `minimum_contrast`
|
||||
- Default: `45`
|
||||
|
||||
**Options**
|
||||
|
||||
`integer` values from 0 to 106. Common recommended values:
|
||||
|
||||
- `0`: No contrast adjustment
|
||||
- `45`: Minimum for large fluent text (default)
|
||||
- `60`: Minimum for other content text
|
||||
- `75`: Minimum for body text
|
||||
- `90`: Preferred for body text
|
||||
|
||||
```json
|
||||
{
|
||||
"terminal": {
|
||||
"minimum_contrast": 45
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Terminal: Option As Meta
|
||||
|
||||
- Description: Re-interprets the option keys to act like a 'meta' key, like in Emacs.
|
||||
|
||||
@@ -136,11 +136,22 @@ function SignZedAndItsFriends {
|
||||
& "$innoDir\sign.ps1" $files
|
||||
}
|
||||
|
||||
function DownloadAMDGpuServices {
|
||||
# If you update the AGS SDK version, please also update the version in `crates/gpui/src/platform/windows/directx_renderer.rs`
|
||||
$url = "https://codeload.github.com/GPUOpen-LibrariesAndSDKs/AGS_SDK/zip/refs/tags/v6.3.0"
|
||||
$zipPath = ".\AGS_SDK_v6.3.0.zip"
|
||||
# Download the AGS SDK zip file
|
||||
Invoke-WebRequest -Uri $url -OutFile $zipPath
|
||||
# Extract the AGS SDK zip file
|
||||
Expand-Archive -Path $zipPath -DestinationPath "." -Force
|
||||
}
|
||||
|
||||
function CollectFiles {
|
||||
Move-Item -Path "$innoDir\zed_explorer_command_injector.appx" -Destination "$innoDir\appx\zed_explorer_command_injector.appx" -Force
|
||||
Move-Item -Path "$innoDir\zed_explorer_command_injector.dll" -Destination "$innoDir\appx\zed_explorer_command_injector.dll" -Force
|
||||
Move-Item -Path "$innoDir\cli.exe" -Destination "$innoDir\bin\zed.exe" -Force
|
||||
Move-Item -Path "$innoDir\auto_update_helper.exe" -Destination "$innoDir\tools\auto_update_helper.exe" -Force
|
||||
Move-Item -Path ".\AGS_SDK-6.3.0\ags_lib\lib\amd_ags_x64.dll" -Destination "$innoDir\amd_ags_x64.dll" -Force
|
||||
}
|
||||
|
||||
function BuildInstaller {
|
||||
@@ -211,7 +222,6 @@ function BuildInstaller {
|
||||
# Windows runner 2022 default has iscc in PATH, https://github.com/actions/runner-images/blob/main/images/windows/Windows2022-Readme.md
|
||||
# Currently, we are using Windows 2022 runner.
|
||||
# Windows runner 2025 doesn't have iscc in PATH for now, https://github.com/actions/runner-images/issues/11228
|
||||
# $innoSetupPath = "iscc.exe"
|
||||
$innoSetupPath = "C:\Program Files (x86)\Inno Setup 6\ISCC.exe"
|
||||
|
||||
$definitions = @{
|
||||
@@ -268,6 +278,7 @@ BuildZedAndItsFriends
|
||||
MakeAppx
|
||||
SignZedAndItsFriends
|
||||
ZipZedAndItsFriendsDebug
|
||||
DownloadAMDGpuServices
|
||||
CollectFiles
|
||||
BuildInstaller
|
||||
|
||||
|
||||
@@ -558,7 +558,6 @@ getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-f
|
||||
getrandom-6f8ce4dd05d13bba = { package = "getrandom", version = "0.2", default-features = false, features = ["js", "rdrand"] }
|
||||
hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] }
|
||||
itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" }
|
||||
naga = { version = "25", features = ["spv-out", "wgsl-in"] }
|
||||
ring = { version = "0.17", features = ["std"] }
|
||||
rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["event"] }
|
||||
scopeguard = { version = "1" }
|
||||
@@ -582,7 +581,6 @@ getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-f
|
||||
getrandom-6f8ce4dd05d13bba = { package = "getrandom", version = "0.2", default-features = false, features = ["js", "rdrand"] }
|
||||
hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] }
|
||||
itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" }
|
||||
naga = { version = "25", features = ["spv-out", "wgsl-in"] }
|
||||
proc-macro2 = { version = "1", default-features = false, features = ["span-locations"] }
|
||||
ring = { version = "0.17", features = ["std"] }
|
||||
rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["event"] }
|
||||
|
||||
@@ -71,6 +71,10 @@ extend-ignore-re = [
|
||||
# Not an actual typo but an intentionally invalid color, in `color_extractor`
|
||||
"#fof",
|
||||
# Stripped version of reserved keyword `type`
|
||||
"typ"
|
||||
"typ",
|
||||
# AMD GPU Services
|
||||
"ags",
|
||||
# AMD GPU Services
|
||||
"AGS"
|
||||
]
|
||||
check-filename = true
|
||||
|
||||
Reference in New Issue
Block a user