Compare commits

...

29 Commits

Author SHA1 Message Date
Junkui Zhang
a4e8d2501a init 2025-08-01 18:00:12 +08:00
Junkui Zhang
3be48a9813 remove unneeded change 2025-08-01 17:32:48 +08:00
Junkui Zhang
7984e05d35 add comments 2025-08-01 16:59:37 +08:00
Junkui Zhang
88b01e5e31 fix raster_bounds 2025-08-01 16:55:12 +08:00
Junkui Zhang
e599352fc2 remove the use of bitmap scaler 2025-08-01 16:48:36 +08:00
Kate
c3f210eb5e improve rendering (i think) 2025-07-31 17:24:47 +02:00
Junkui Zhang
d84cf7ef03 update rendering quality of small fonts 2025-07-31 21:30:44 +08:00
Junkui Zhang
aa68b3e8ef revert wrong blend state change 2025-07-31 19:46:17 +08:00
Junkui Zhang
fad7aa6643 fix 2025-07-31 18:41:25 +08:00
Junkui Zhang
d50b3e172e cleanup 2025-07-31 18:16:20 +08:00
Junkui Zhang
a8d3e5530b DXGI_ALPHA_MODE_PREMULTIPLIED 2025-07-31 17:53:40 +08:00
Junkui Zhang
fe096ad205 fix monochrome 2025-07-31 17:48:09 +08:00
Junkui Zhang
30f6699b56 checkpoint 2025-07-31 17:27:06 +08:00
Junkui Zhang
a1001079ba properly calculate bounds 2025-07-31 16:20:58 +08:00
Kate
da83011fda Rasterize glyphs without D2D
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
Co-authored-by: Julia <julia@zed.dev>
2025-07-30 17:08:47 -07:00
Marshall Bowers
bb1a7ccbba client: Add CloudUserStore (#35370)
This PR adds a new `CloudUserStore` for storing information about the
user retrieved from Cloud instead of Collab.

Release Notes:

- N/A
2025-07-30 18:43:10 -04:00
Marshall Bowers
289f420504 Sort crate members in Cargo.toml (#35371)
This PR sorts the crate members in the `Cargo.toml` file, as they had
gotten unsorted.

Release Notes:

- N/A
2025-07-30 22:35:17 +00:00
张小白
15ad986329 windows: Port to DirectX 11 (#34374)
Closes #16713
Closes #19739
Closes #33191
Closes #26692
Closes #17374
Closes #35077
Closes https://github.com/zed-industries/zed/issues/35205
Closes https://github.com/zed-industries/zed/issues/35262


Compared to the current Vulkan implementation, this PR brings several
improvements:

- Fewer weird bugs
- Better hardware compatibility
- VSync support
- More accurate colors
- Lower memory usage
- Graceful handling of device loss

---

**TODO:**

- [x] Don’t use AGS binaries directly
- [ ] The message loop is using too much CPU when ths app is idle
- [x] There’s a
[bug](https://github.com/zed-industries/zed/issues/33191#issuecomment-3109306630)
in how `Path` is being rendered.

---

Release Notes:

- N/A

---------

Co-authored-by: Kate <kate@zed.dev>
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
2025-07-30 15:27:58 -07:00
Finn Evers
0d9715325c docs: Add section about terminal contrast adjustments (#35369)
Closes #35146

This change adds documentation for the `terminal.minimum_contrast`
setting to the docs as we've had a lot of reports regarding the contrast
adjustments, yet are missing proper documentation (aside from that in
the `defaults.json`) for it.

Release Notes:

- N/A
2025-07-31 00:19:56 +02:00
Joseph T. Lyons
5ef5f3c5ca Introduce settings profiles (#35339)
Settings Profiles

- [X] Allow profiles to be defined, where each profile can be any of
Zed's settings
    - [X] Autocompletion of all settings
    - [X] Errors on invalid keys
- [X] Action brings up modal that shows user-defined profiles
- [X] Alphabetize profiles
- [X] Ability to filter down via keyboard, and navigate via arrow up and
down
- [X] Auto select Disabled option by default (first in list, after
alphabetizing user-defined profiles)
- [X] Automatically select active profile on next picker summoning
- [X] Persist settings until toggled off
- [X] Show live preview as you select from the profile picker
- [X] Tweaking a setting, while in a profile, updates the profile live
- [X] Make sure actions that live update Zed, such as `cmd-0`, `cmd-+`,
and `cmd--`, work while in a profile
- [X] Add a test to track state

Release Notes:

- Added the ability to configure settings profiles, via the "profiles"
key. Example:

```json
{
  "profiles": {
    "Streaming": {
      "agent_font_size": 20,
      "buffer_font_size": 20,
      "theme": "One Light",
      "ui_font_size": 20
    }
  }
}
```

To set a profile, use `settings profile selector: toggle`
2025-07-30 21:48:24 +00:00
Anthony Eid
2d4afd2119 Polish onboarding page (#35367)
- Added borders to the numeric stepper.
- Changed the hover mouse style for SwitchField.
- Made the edit page toggle buttons more responsive.

Release Notes:

- N/A
2025-07-30 20:35:21 +00:00
Ben Kunkle
afcb8f2a3f onboarding: Wire up settings import (#35366)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-07-30 20:09:11 +00:00
Smit Barmase
cdce3b3620 linux: Fix caps lock not working consistently for certain X11 systems (#35361)
Closes #35316

Bug in https://github.com/zed-industries/zed/pull/34514

Turns out you are not supposed to call `update_key` for modifiers on
`KeyPress`/`KeyRelease`, as modifiers are already updated in
`XkbStateNotify` events. Not sure why this only causes issues on a few
systems and works on others.

Tested on Ubuntu 24.04.2 LTS (initial bug) and Kubuntu 25.04 (worked
fine before too).

Release Notes:

- Fixed an issue where caps lock stopped working consistently on some
Linux X11 systems.
2025-07-31 00:56:36 +05:30
Marshall Bowers
bc6bb42745 Add cloud_api_client and cloud_api_types crates (#35357)
This PR adds two new crates for interacting with Cloud:

- `cloud_api_client` - The client that will be used to talk to Cloud.
- `cloud_api_types` - The types for the Cloud API that are shared
between Zed and Cloud.

Release Notes:

- N/A
2025-07-30 18:57:51 +00:00
Marshall Bowers
7695c4b82e collab: Temporarily add back GET /user endpoint for local development (#35358)
This PR temporarily adds back the `GET /user` endpoint to Collab since
we're still using it for local development.

Will remove it again once we update the local development process to
leverage Cloud.

Release Notes:

- N/A
2025-07-30 18:54:44 +00:00
Smit Barmase
794ade8b6d ui_prompt: Fix prompt dialog is hard to see on large screen (#35348)
Closes #18516

Release Notes:

- Improved visibility of prompt dialog on Linux by dimming the
background.
2025-07-30 23:03:53 +05:30
Smit Barmase
f4bd524d7f ui_prompt: Fix copy version number from About Zed (#35346)
Closes #29361

Release Notes:

- Fixed not selectable version number in About Zed prompt on Linux.
2025-07-30 22:51:59 +05:30
Joseph T. Lyons
9d82e148de Bump Zed to v0.199 (#35343)
Release Notes:

-N/A
2025-07-30 16:53:53 +00:00
Finn Evers
f8d1062484 onboarding: Fix keybindings showing up after a delay (#35342)
This fixes an issue where keybinds would only show up after a delay on
the welcome page upon re-opening it. It also binds one of the buttons to
the corresponding action.

Release Notes:

- N/A
2025-07-30 16:18:14 +00:00
57 changed files with 5591 additions and 674 deletions

49
Cargo.lock generated
View File

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

View File

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

View File

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

View File

@@ -22,6 +22,7 @@ async-tungstenite = { workspace = true, features = ["tokio", "tokio-rustls-manua
base64.workspace = true
chrono = { workspace = true, features = ["serde"] }
clock.workspace = true
cloud_api_client.workspace = true
cloud_llm_client.workspace = true
collections.workspace = true
credentials_provider.workspace = true

View File

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

View File

@@ -0,0 +1,3 @@
mod user_store;
pub use user_store::*;

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

View 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

View File

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

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

View 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

View File

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

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

View File

@@ -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(
&params.github_login,
params.github_user_id,
params.github_email.as_deref(),
params.github_name.as_deref(),
params.github_user_created_at,
initial_channel_id,
)
.await?;
let metrics_id = app.db.get_user_metrics_id(user.id).await?;
let feature_flags = app.db.get_user_flags(user.id).await?;
Ok(Json(AuthenticatedUserResponse {
user,
metrics_id,
feature_flags,
}))
}
#[derive(Debug, Deserialize)]
struct LookUpUserParams {
identifier: String,

View File

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

View File

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

View File

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

View File

@@ -198,7 +198,7 @@ impl RenderOnce for CharacterGrid {
"χ", "ψ", "", "а", "в", "Ж", "ж", "З", "з", "К", "к", "л", "м", "Н", "н", "Р", "р",
"У", "у", "ф", "ч", "ь", "ы", "Э", "э", "Я", "я", "ij", "öẋ", ".,", "⣝⣑", "~", "*",
"_", "^", "`", "'", "(", "{", "«", "#", "&", "@", "$", "¢", "%", "|", "?", "", "µ",
"", "<=", "!=", "==", "--", "++", "=>", "->",
"", "<=", "!=", "==", "--", "++", "=>", "->", "🏀", "🎊", "😍", "❤️", "👍", "👎",
];
let columns = 11;

View File

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

View File

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

View File

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

View File

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

View 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

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4366,6 +4366,7 @@ mod tests {
"repl",
"rules_library",
"search",
"settings_profile_selector",
"snippets",
"supermaven",
"svg",

View File

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

View File

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

View File

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

View File

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

View File

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