Compare commits

..

7 Commits

Author SHA1 Message Date
Piotr Osiewicz
f6946ad4e8 Add missed offender 2025-10-07 14:33:21 +02:00
Piotr Osiewicz
c9972c2972 thanks @SomeoneToIgnore for saving my ass 2025-10-07 14:04:56 +02:00
Piotr Osiewicz
afdc53fdb7 agent: Cache away results of converting rules file names into relpaths 2025-10-07 13:58:12 +02:00
Piotr Osiewicz
d2e5947cf3 paths: Cache away results of static construction of RelPath
These functions started showing up in my profiles after a RelPath refactor, as RelPath::unix is not a no-op - it parses the path, which is way costlier than returning a reference to a static
2025-10-07 13:52:52 +02:00
Finn Evers
b02b130b7c extensions_ui: Fix uneven horizontal padding (#39627)
This fixes an issue where the horizontal padding on the extensions page
was uneven and where the padding on the right side would be much larger.

| Before | After |
| --- | --- |
| <img width="2550" height="1694" alt="Bildschirmfoto 2025-10-06 um 19
26 56"
src="https://github.com/user-attachments/assets/cf05b77b-4a9e-4ad9-8fa7-381f9b6b45af"
/> | <img width="2546" height="1694" alt="Bildschirmfoto 2025-10-06 um
19 25 49"
src="https://github.com/user-attachments/assets/493ba188-534a-4e7a-b2c1-2b1380be7150"
/> |

Release Notes:

- Improved the horizontal padding on the extensions tab.
2025-10-07 13:23:02 +02:00
Piotr Osiewicz
41ac6a8764 windows: Use nc-esque ssh askpass auth for remoting (#39646)
This lets us avoid storing user PW in ZED_ASKPASS_PASSWORD env var.
Release Notes:

- N/A
2025-10-07 09:48:03 +02:00
Danilo Leal
963204c99d settings ui: Add new batch of settings (#39650)
Release Notes:

- N/A
2025-10-07 00:27:58 -03:00
30 changed files with 1473 additions and 822 deletions

View File

@@ -18,9 +18,6 @@ runs:
shell: bash -euxo pipefail {0}
run: script/clear-target-dir-if-larger-than 100
# FIXME
- name: Run tests
shell: bash -euxo pipefail {0}
env:
RUST_LOG: project=debug,project_panel=debug
run: cargo nextest run -p project_panel --no-fail-fast --failure-output immediate-final --no-capture
run: cargo nextest run --workspace --no-fail-fast --failure-output immediate-final

2
Cargo.lock generated
View File

@@ -808,7 +808,6 @@ dependencies = [
"gpui",
"log",
"net",
"proto",
"smol",
"tempfile",
"windows 0.61.1",
@@ -12078,7 +12077,6 @@ dependencies = [
"criterion",
"db",
"editor",
"env_logger 0.11.8",
"file_icons",
"git",
"git_ui",

View File

@@ -38,7 +38,7 @@ use std::{
cell::{Ref, RefCell},
path::{Path, PathBuf},
rc::Rc,
sync::{Arc, Mutex},
sync::{Arc, LazyLock, Mutex},
};
use util::{ResultExt as _, rel_path::RelPath};
@@ -74,17 +74,19 @@ impl Column for DataType {
}
}
const RULES_FILE_NAMES: [&str; 9] = [
".rules",
".cursorrules",
".windsurfrules",
".clinerules",
".github/copilot-instructions.md",
"CLAUDE.md",
"AGENT.md",
"AGENTS.md",
"GEMINI.md",
];
static RULES_FILE_NAMES: LazyLock<[&RelPath; 9]> = LazyLock::new(|| {
[
RelPath::unix(".rules").unwrap(),
RelPath::unix(".cursorrules").unwrap(),
RelPath::unix(".windsurfrules").unwrap(),
RelPath::unix(".clinerules").unwrap(),
RelPath::unix(".github/copilot-instructions.md").unwrap(),
RelPath::unix("CLAUDE.md").unwrap(),
RelPath::unix("AGENT.md").unwrap(),
RelPath::unix("AGENTS.md").unwrap(),
RelPath::unix("GEMINI.md").unwrap(),
]
});
pub fn init(fs: Arc<dyn Fs>, cx: &mut App) {
ThreadsDatabase::init(fs, cx);
@@ -232,11 +234,10 @@ impl ThreadStore {
self.enqueue_system_prompt_reload();
}
project::Event::WorktreeUpdatedEntries(_, items) => {
if items.iter().any(|(path, _, _)| {
RULES_FILE_NAMES
.iter()
.any(|name| path.as_ref() == RelPath::unix(name).unwrap())
}) {
if items
.iter()
.any(|(path, _, _)| RULES_FILE_NAMES.iter().any(|name| path.as_ref() == *name))
{
self.enqueue_system_prompt_reload();
}
}
@@ -368,7 +369,7 @@ impl ThreadStore {
.into_iter()
.filter_map(|name| {
worktree
.entry_for_path(RelPath::unix(name).unwrap())
.entry_for_path(name)
.filter(|entry| entry.is_file())
.map(|entry| entry.path.clone())
})

View File

@@ -25,21 +25,23 @@ use std::any::Any;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::rc::Rc;
use std::sync::Arc;
use std::sync::{Arc, LazyLock};
use util::ResultExt;
use util::rel_path::RelPath;
const RULES_FILE_NAMES: [&str; 9] = [
".rules",
".cursorrules",
".windsurfrules",
".clinerules",
".github/copilot-instructions.md",
"CLAUDE.md",
"AGENT.md",
"AGENTS.md",
"GEMINI.md",
];
static RULES_FILE_NAMES: LazyLock<[&RelPath; 9]> = LazyLock::new(|| {
[
RelPath::unix(".rules").unwrap(),
RelPath::unix(".cursorrules").unwrap(),
RelPath::unix(".windsurfrules").unwrap(),
RelPath::unix(".clinerules").unwrap(),
RelPath::unix(".github/copilot-instructions.md").unwrap(),
RelPath::unix("CLAUDE.md").unwrap(),
RelPath::unix("AGENT.md").unwrap(),
RelPath::unix("AGENTS.md").unwrap(),
RelPath::unix("GEMINI.md").unwrap(),
]
});
pub struct RulesLoadingError {
pub message: SharedString,
@@ -475,7 +477,7 @@ impl NativeAgent {
.into_iter()
.filter_map(|name| {
worktree
.entry_for_path(RelPath::unix(name).unwrap())
.entry_for_path(name)
.filter(|entry| entry.is_file())
.map(|entry| entry.path.clone())
})
@@ -556,11 +558,10 @@ impl NativeAgent {
self.project_context_needs_refresh.send(()).ok();
}
project::Event::WorktreeUpdatedEntries(_, items) => {
if items.iter().any(|(path, _, _)| {
RULES_FILE_NAMES
.iter()
.any(|name| path.as_ref() == RelPath::unix(name).unwrap())
}) {
if items
.iter()
.any(|(path, _, _)| RULES_FILE_NAMES.iter().any(|name| path.as_ref() == *name))
{
self.project_context_needs_refresh.send(()).ok();
}
}

View File

@@ -16,7 +16,6 @@ anyhow.workspace = true
futures.workspace = true
gpui.workspace = true
net.workspace = true
proto.workspace = true
smol.workspace = true
log.workspace = true
tempfile.workspace = true

View File

@@ -1,10 +1,16 @@
mod encrypted_password;
pub use encrypted_password::{EncryptedPassword, ProcessExt};
use util::paths::PathExt;
pub use encrypted_password::{EncryptedPassword, IKnowWhatIAmDoingAndIHaveReadTheDocs};
use net::async_net::UnixListener;
use smol::lock::Mutex;
use util::fs::make_file_executable;
use std::ffi::OsStr;
use std::ops::ControlFlow;
use std::sync::Arc;
use std::sync::OnceLock;
use std::{ffi::OsStr, time::Duration};
use std::time::Duration;
use anyhow::{Context as _, Result};
use futures::channel::{mpsc, oneshot};
@@ -14,9 +20,7 @@ use futures::{
};
use gpui::{AsyncApp, BackgroundExecutor, Task};
use smol::fs;
use util::{ResultExt as _, debug_panic};
use crate::encrypted_password::decrypt;
use util::{ResultExt as _, debug_panic, maybe, paths::PathExt};
/// Path to the program used for askpass
///
@@ -32,6 +36,7 @@ pub enum AskPassResult {
pub struct AskPassDelegate {
tx: mpsc::UnboundedSender<(String, oneshot::Sender<EncryptedPassword>)>,
executor: BackgroundExecutor,
_task: Task<()>,
}
@@ -49,24 +54,27 @@ impl AskPassDelegate {
password_prompt(prompt, channel, cx);
}
});
Self { tx, _task: task }
Self {
tx,
_task: task,
executor: cx.background_executor().clone(),
}
}
pub async fn ask_password(&mut self, prompt: String) -> Option<EncryptedPassword> {
let (tx, rx) = oneshot::channel();
self.tx.send((prompt, tx)).await.ok()?;
rx.await.ok()
pub fn ask_password(&mut self, prompt: String) -> Task<Option<EncryptedPassword>> {
let mut this_tx = self.tx.clone();
self.executor.spawn(async move {
let (tx, rx) = oneshot::channel();
this_tx.send((prompt, tx)).await.ok()?;
rx.await.ok()
})
}
}
pub struct AskPassSession {
#[cfg(not(target_os = "windows"))]
script_path: std::path::PathBuf,
#[cfg(target_os = "windows")]
askpass_helper: String,
#[cfg(target_os = "windows")]
secret: std::sync::Arc<OnceLock<EncryptedPassword>>,
_askpass_task: Task<()>,
askpass_task: PasswordProxy,
askpass_opened_rx: Option<oneshot::Receiver<()>>,
askpass_kill_master_rx: Option<oneshot::Receiver<()>>,
}
@@ -81,103 +89,57 @@ impl AskPassSession {
/// You must retain this session until the master process exits.
#[must_use]
pub async fn new(executor: &BackgroundExecutor, mut delegate: AskPassDelegate) -> Result<Self> {
use net::async_net::UnixListener;
use util::fs::make_file_executable;
#[cfg(target_os = "windows")]
let secret = std::sync::Arc::new(OnceLock::new());
let temp_dir = tempfile::Builder::new().prefix("zed-askpass").tempdir()?;
let askpass_socket = temp_dir.path().join("askpass.sock");
let askpass_script_path = temp_dir.path().join(ASKPASS_SCRIPT_NAME);
let (askpass_opened_tx, askpass_opened_rx) = oneshot::channel::<()>();
let listener = UnixListener::bind(&askpass_socket).context("creating askpass socket")?;
let current_exec =
std::env::current_exe().context("Failed to determine current zed executable path.")?;
let askpass_program = ASKPASS_PROGRAM
.get_or_init(|| current_exec)
.try_shell_safe()
.context("Failed to shell-escape Askpass program path.")?
.to_string();
let askpass_opened_tx = Arc::new(Mutex::new(Some(askpass_opened_tx)));
let (askpass_kill_master_tx, askpass_kill_master_rx) = oneshot::channel::<()>();
let mut kill_tx = Some(askpass_kill_master_tx);
let kill_tx = Arc::new(Mutex::new(Some(askpass_kill_master_tx)));
#[cfg(target_os = "windows")]
let askpass_secret = secret.clone();
let askpass_task = executor.spawn(async move {
let mut askpass_opened_tx = Some(askpass_opened_tx);
let get_password = {
let executor = executor.clone();
while let Ok((mut stream, _)) = listener.accept().await {
if let Some(askpass_opened_tx) = askpass_opened_tx.take() {
askpass_opened_tx.send(()).ok();
}
let mut buffer = Vec::new();
let mut reader = BufReader::new(&mut stream);
if reader.read_until(b'\0', &mut buffer).await.is_err() {
buffer.clear();
}
let prompt = String::from_utf8_lossy(&buffer);
if let Some(password) = delegate.ask_password(prompt.into_owned()).await {
#[cfg(target_os = "windows")]
{
askpass_secret.get_or_init(|| password.clone());
move |prompt| {
let prompt = delegate.ask_password(prompt);
let kill_tx = kill_tx.clone();
let askpass_opened_tx = askpass_opened_tx.clone();
#[cfg(target_os = "windows")]
let askpass_secret = askpass_secret.clone();
executor.spawn(async move {
if let Some(askpass_opened_tx) = askpass_opened_tx.lock().await.take() {
askpass_opened_tx.send(()).ok();
}
if let Ok(decrypted) = decrypt(password) {
stream.write_all(decrypted.as_bytes()).await.log_err();
if let Some(password) = prompt.await {
#[cfg(target_os = "windows")]
{
_ = askpass_secret.set(password.clone());
}
ControlFlow::Continue(Ok(password))
} else {
if let Some(kill_tx) = kill_tx.lock().await.take() {
kill_tx.send(()).log_err();
}
ControlFlow::Break(())
}
} else {
if let Some(kill_tx) = kill_tx.take() {
kill_tx.send(()).log_err();
}
// note: we expect the caller to drop this task when it's done.
// We need to keep the stream open until the caller is done to avoid
// spurious errors from ssh.
std::future::pending::<()>().await;
drop(stream);
}
})
}
drop(temp_dir)
});
// Create an askpass script that communicates back to this process.
let askpass_script = generate_askpass_script(&askpass_program, &askpass_socket);
fs::write(&askpass_script_path, askpass_script)
.await
.with_context(|| format!("creating askpass script at {askpass_script_path:?}"))?;
make_file_executable(&askpass_script_path).await?;
#[cfg(target_os = "windows")]
let askpass_helper = format!(
"powershell.exe -ExecutionPolicy Bypass -File {}",
askpass_script_path.display()
);
};
let askpass_task = PasswordProxy::new(get_password, executor.clone()).await?;
Ok(Self {
#[cfg(not(target_os = "windows"))]
script_path: askpass_script_path,
#[cfg(target_os = "windows")]
secret,
#[cfg(target_os = "windows")]
askpass_helper,
_askpass_task: askpass_task,
askpass_task,
askpass_kill_master_rx: Some(askpass_kill_master_rx),
askpass_opened_rx: Some(askpass_opened_rx),
})
}
#[cfg(not(target_os = "windows"))]
pub fn script_path(&self) -> impl AsRef<OsStr> {
&self.script_path
}
#[cfg(target_os = "windows")]
pub fn script_path(&self) -> impl AsRef<OsStr> {
&self.askpass_helper
}
// This will run the askpass task forever, resolving as many authentication requests as needed.
// The caller is responsible for examining the result of their own commands and cancelling this
// future when this is no longer needed. Note that this can only be called once, but due to the
@@ -209,8 +171,109 @@ impl AskPassSession {
pub fn get_password(&self) -> Option<EncryptedPassword> {
self.secret.get().cloned()
}
pub fn script_path(&self) -> impl AsRef<OsStr> {
self.askpass_task.script_path()
}
}
pub struct PasswordProxy {
_task: Task<()>,
#[cfg(not(target_os = "windows"))]
askpass_script_path: std::path::PathBuf,
#[cfg(target_os = "windows")]
askpass_helper: String,
}
impl PasswordProxy {
pub async fn new(
mut get_password: impl FnMut(String) -> Task<ControlFlow<(), Result<EncryptedPassword>>>
+ 'static
+ Send
+ Sync,
executor: BackgroundExecutor,
) -> Result<Self> {
let temp_dir = tempfile::Builder::new().prefix("zed-askpass").tempdir()?;
let askpass_socket = temp_dir.path().join("askpass.sock");
let askpass_script_path = temp_dir.path().join(ASKPASS_SCRIPT_NAME);
let current_exec =
std::env::current_exe().context("Failed to determine current zed executable path.")?;
let askpass_program = ASKPASS_PROGRAM
.get_or_init(|| current_exec)
.try_shell_safe()
.context("Failed to shell-escape Askpass program path.")?
.to_string();
// Create an askpass script that communicates back to this process.
let askpass_script = generate_askpass_script(&askpass_program, &askpass_socket);
let _task = executor.spawn(async move {
maybe!(async move {
let listener =
UnixListener::bind(&askpass_socket).context("creating askpass socket")?;
while let Ok((mut stream, _)) = listener.accept().await {
let mut buffer = Vec::new();
let mut reader = BufReader::new(&mut stream);
if reader.read_until(b'\0', &mut buffer).await.is_err() {
buffer.clear();
}
let prompt = String::from_utf8_lossy(&buffer).into_owned();
let password = get_password(prompt).await;
match password {
ControlFlow::Continue(password) => {
if let Ok(password) = password
&& let Ok(decrypted) =
password.decrypt(IKnowWhatIAmDoingAndIHaveReadTheDocs)
{
stream.write_all(decrypted.as_bytes()).await.log_err();
}
}
ControlFlow::Break(()) => {
// note: we expect the caller to drop this task when it's done.
// We need to keep the stream open until the caller is done to avoid
// spurious errors from ssh.
std::future::pending::<()>().await;
drop(stream);
}
}
}
drop(temp_dir);
Result::<_, anyhow::Error>::Ok(())
})
.await
.log_err();
});
fs::write(&askpass_script_path, askpass_script)
.await
.with_context(|| format!("creating askpass script at {askpass_script_path:?}"))?;
make_file_executable(&askpass_script_path).await?;
#[cfg(target_os = "windows")]
let askpass_helper = format!(
"powershell.exe -ExecutionPolicy Bypass -File {}",
askpass_script_path.display()
);
Ok(Self {
_task,
#[cfg(not(target_os = "windows"))]
askpass_script_path,
#[cfg(target_os = "windows")]
askpass_helper,
})
}
pub fn script_path(&self) -> impl AsRef<OsStr> {
#[cfg(not(target_os = "windows"))]
{
&self.askpass_script_path
}
#[cfg(target_os = "windows")]
{
&self.askpass_helper
}
}
}
/// The main function for when Zed is running in netcat mode for use in askpass.
/// Called from both the remote server binary and the zed binary in their respective main functions.
pub fn main(socket: &str) {

View File

@@ -21,27 +21,6 @@ type LengthWithoutPadding = u32;
#[derive(Clone)]
pub struct EncryptedPassword(Vec<u8>, LengthWithoutPadding);
pub trait ProcessExt {
fn encrypted_env(&mut self, name: &str, value: EncryptedPassword) -> &mut Self;
}
impl ProcessExt for smol::process::Command {
fn encrypted_env(&mut self, name: &str, value: EncryptedPassword) -> &mut Self {
if let Ok(password) = decrypt(value) {
self.env(name, password);
}
self
}
}
impl TryFrom<EncryptedPassword> for proto::AskPassResponse {
type Error = anyhow::Error;
fn try_from(pw: EncryptedPassword) -> Result<Self, Self::Error> {
let pw = decrypt(pw)?;
Ok(Self { response: pw })
}
}
impl Drop for EncryptedPassword {
fn drop(&mut self) {
self.0.zeroize();
@@ -79,38 +58,45 @@ impl TryFrom<&str> for EncryptedPassword {
}
}
pub(crate) fn decrypt(mut password: EncryptedPassword) -> Result<String> {
#[cfg(windows)]
{
use anyhow::Context;
use windows::Win32::Security::Cryptography::{
CRYPTPROTECTMEMORY_BLOCK_SIZE, CRYPTPROTECTMEMORY_SAME_PROCESS, CryptUnprotectMemory,
};
assert_eq!(
password.0.len() % CRYPTPROTECTMEMORY_BLOCK_SIZE as usize,
0,
"Violated pre-condition (buffer size <{}> must be a multiple of CRYPTPROTECTMEMORY_BLOCK_SIZE <{}>) for CryptUnprotectMemory.",
password.0.len(),
CRYPTPROTECTMEMORY_BLOCK_SIZE
);
if password.1 != 0 {
unsafe {
CryptUnprotectMemory(
password.0.as_mut_ptr() as _,
password.0.len().try_into()?,
CRYPTPROTECTMEMORY_SAME_PROCESS,
)
.context("while decrypting a SSH password")?
/// Read the docs for [EncryptedPassword]; please take care of not storing the plaintext string in memory for extended
/// periods of time.
pub struct IKnowWhatIAmDoingAndIHaveReadTheDocs;
impl EncryptedPassword {
pub fn decrypt(mut self, _: IKnowWhatIAmDoingAndIHaveReadTheDocs) -> Result<String> {
#[cfg(windows)]
{
use anyhow::Context;
use windows::Win32::Security::Cryptography::{
CRYPTPROTECTMEMORY_BLOCK_SIZE, CRYPTPROTECTMEMORY_SAME_PROCESS,
CryptUnprotectMemory,
};
assert_eq!(
self.0.len() % CRYPTPROTECTMEMORY_BLOCK_SIZE as usize,
0,
"Violated pre-condition (buffer size <{}> must be a multiple of CRYPTPROTECTMEMORY_BLOCK_SIZE <{}>) for CryptUnprotectMemory.",
self.0.len(),
CRYPTPROTECTMEMORY_BLOCK_SIZE
);
if self.1 != 0 {
unsafe {
CryptUnprotectMemory(
self.0.as_mut_ptr() as _,
self.0.len().try_into()?,
CRYPTPROTECTMEMORY_SAME_PROCESS,
)
.context("while decrypting a SSH password")?
};
{
// Remove padding
_ = password.0.drain(password.1 as usize..);
{
// Remove padding
_ = self.0.drain(self.1 as usize..);
}
}
}
Ok(String::from_utf8(std::mem::take(&mut password.0))?)
Ok(String::from_utf8(std::mem::take(&mut self.0))?)
}
#[cfg(not(windows))]
Ok(String::from_utf8(std::mem::take(&mut self.0))?)
}
#[cfg(not(windows))]
Ok(String::from_utf8(std::mem::take(&mut password.0))?)
}

View File

@@ -46,7 +46,6 @@ pub trait DapDelegate: Send + Sync + 'static {
async fn which(&self, command: &OsStr) -> Option<PathBuf>;
async fn read_text_file(&self, path: &RelPath) -> Result<String>;
async fn shell_env(&self) -> collections::HashMap<String, String>;
fn is_headless(&self) -> bool;
}
#[derive(

View File

@@ -120,13 +120,6 @@ impl JsDebugAdapter {
configuration
.entry("sourceMapRenames")
.or_insert(true.into());
// Set up remote browser debugging
if delegate.is_headless() {
configuration
.entry("browserLaunchLocation")
.or_insert("ui".into());
}
}
let adapter_path = if let Some(user_installed_path) = user_installed_path {

View File

@@ -350,7 +350,6 @@ pub fn init(cx: &mut App) {
cx.set_global(GlobalBlameRenderer(Arc::new(())));
dbg!();
workspace::register_project_item::<Editor>(cx);
workspace::FollowableViewRegistry::register::<Editor>(cx);
workspace::register_serializable_item::<Editor>(cx);

View File

@@ -1503,39 +1503,28 @@ impl Render for ExtensionsPage {
})),
)
.child(self.render_feature_upsells(cx))
.child(
v_flex()
.pl_4()
.pr_6()
.size_full()
.overflow_y_hidden()
.map(|this| {
let mut count = self.filtered_remote_extension_indices.len();
if self.filter.include_dev_extensions() {
count += self.dev_extension_entries.len();
}
.child(v_flex().px_4().size_full().overflow_y_hidden().map(|this| {
let mut count = self.filtered_remote_extension_indices.len();
if self.filter.include_dev_extensions() {
count += self.dev_extension_entries.len();
}
if count == 0 {
this.py_4()
.child(self.render_empty_state(cx))
.into_any_element()
} else {
let scroll_handle = self.list.clone();
this.child(
uniform_list(
"entries",
count,
cx.processor(Self::render_extensions),
)
.flex_grow()
.pb_4()
.track_scroll(scroll_handle.clone()),
)
.vertical_scrollbar_for(scroll_handle, window, cx)
.into_any_element()
}
}),
)
if count == 0 {
this.py_4()
.child(self.render_empty_state(cx))
.into_any_element()
} else {
let scroll_handle = self.list.clone();
this.child(
uniform_list("entries", count, cx.processor(Self::render_extensions))
.flex_grow()
.pb_4()
.track_scroll(scroll_handle.clone()),
)
.vertical_scrollbar_for(scroll_handle, window, cx)
.into_any_element()
}
}))
}
}

View File

@@ -2,7 +2,7 @@
use std::env;
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
use std::sync::{LazyLock, OnceLock};
pub use util::paths::home_dir;
use util::rel_path::RelPath;
@@ -31,12 +31,16 @@ static CONFIG_DIR: OnceLock<PathBuf> = OnceLock::new();
/// Returns the relative path to the zed_server directory on the ssh host.
pub fn remote_server_dir_relative() -> &'static RelPath {
RelPath::unix(".zed_server").unwrap()
static CACHED: LazyLock<&'static RelPath> =
LazyLock::new(|| RelPath::unix(".zed_server").unwrap());
*CACHED
}
/// Returns the relative path to the zed_wsl_server directory on the wsl host.
pub fn remote_wsl_server_dir_relative() -> &'static RelPath {
RelPath::unix(".zed_wsl_server").unwrap()
static CACHED: LazyLock<&'static RelPath> =
LazyLock::new(|| RelPath::unix(".zed_wsl_server").unwrap());
*CACHED
}
/// Sets a custom directory for all user data, overriding the default data directory.
@@ -410,17 +414,23 @@ pub fn local_vscode_folder_name() -> &'static str {
/// Returns the relative path to a `settings.json` file within a project.
pub fn local_settings_file_relative_path() -> &'static RelPath {
RelPath::unix(".zed/settings.json").unwrap()
static CACHED: LazyLock<&'static RelPath> =
LazyLock::new(|| RelPath::unix(".zed/settings.json").unwrap());
*CACHED
}
/// Returns the relative path to a `tasks.json` file within a project.
pub fn local_tasks_file_relative_path() -> &'static RelPath {
RelPath::unix(".zed/tasks.json").unwrap()
static CACHED: LazyLock<&'static RelPath> =
LazyLock::new(|| RelPath::unix(".zed/tasks.json").unwrap());
*CACHED
}
/// Returns the relative path to a `.vscode/tasks.json` file within a project.
pub fn local_vscode_tasks_file_relative_path() -> &'static RelPath {
RelPath::unix(".vscode/tasks.json").unwrap()
static CACHED: LazyLock<&'static RelPath> =
LazyLock::new(|| RelPath::unix(".vscode/tasks.json").unwrap());
*CACHED
}
pub fn debug_task_file_name() -> &'static str {
@@ -434,12 +444,16 @@ pub fn task_file_name() -> &'static str {
/// Returns the relative path to a `debug.json` file within a project.
/// .zed/debug.json
pub fn local_debug_file_relative_path() -> &'static RelPath {
RelPath::unix(".zed/debug.json").unwrap()
static CACHED: LazyLock<&'static RelPath> =
LazyLock::new(|| RelPath::unix(".zed/debug.json").unwrap());
*CACHED
}
/// Returns the relative path to a `.vscode/launch.json` file within a project.
pub fn local_vscode_launch_file_relative_path() -> &'static RelPath {
RelPath::unix(".vscode/launch.json").unwrap()
static CACHED: LazyLock<&'static RelPath> =
LazyLock::new(|| RelPath::unix(".vscode/launch.json").unwrap());
*CACHED
}
pub fn user_ssh_config_file() -> PathBuf {

View File

@@ -164,7 +164,6 @@ pub struct BreakpointStore {
impl BreakpointStore {
pub fn init(client: &AnyProtoClient) {
log::error!("breakpoint store init");
client.add_entity_request_handler(Self::handle_toggle_breakpoint);
client.add_entity_message_handler(Self::handle_breakpoints_for_file);
}

View File

@@ -22,9 +22,9 @@ use dap::{
inline_value::VariableLookupKind,
messages::Message,
};
use fs::{Fs, RemoveOptions};
use fs::Fs;
use futures::{
StreamExt, TryStreamExt as _,
StreamExt,
channel::mpsc::{self, UnboundedSender},
future::{Shared, join_all},
};
@@ -78,15 +78,12 @@ pub struct LocalDapStore {
http_client: Arc<dyn HttpClient>,
environment: Entity<ProjectEnvironment>,
toolchain_store: Arc<dyn LanguageToolchainStore>,
is_headless: bool,
}
pub struct RemoteDapStore {
remote_client: Entity<RemoteClient>,
upstream_client: AnyProtoClient,
upstream_project_id: u64,
node_runtime: NodeRuntime,
http_client: Arc<dyn HttpClient>,
}
pub struct DapStore {
@@ -137,19 +134,17 @@ impl DapStore {
toolchain_store: Arc<dyn LanguageToolchainStore>,
worktree_store: Entity<WorktreeStore>,
breakpoint_store: Entity<BreakpointStore>,
is_headless: bool,
cx: &mut Context<Self>,
) -> Self {
let mode = DapStoreMode::Local(LocalDapStore {
fs: fs.clone(),
fs,
environment,
http_client,
node_runtime,
toolchain_store,
is_headless,
});
Self::new(mode, breakpoint_store, worktree_store, fs, cx)
Self::new(mode, breakpoint_store, worktree_store, cx)
}
pub fn new_remote(
@@ -157,20 +152,15 @@ impl DapStore {
remote_client: Entity<RemoteClient>,
breakpoint_store: Entity<BreakpointStore>,
worktree_store: Entity<WorktreeStore>,
node_runtime: NodeRuntime,
http_client: Arc<dyn HttpClient>,
fs: Arc<dyn Fs>,
cx: &mut Context<Self>,
) -> Self {
let mode = DapStoreMode::Remote(RemoteDapStore {
upstream_client: remote_client.read(cx).proto_client(),
remote_client,
upstream_project_id: project_id,
node_runtime,
http_client,
});
Self::new(mode, breakpoint_store, worktree_store, fs, cx)
Self::new(mode, breakpoint_store, worktree_store, cx)
}
pub fn new_collab(
@@ -178,55 +168,17 @@ impl DapStore {
_upstream_client: AnyProtoClient,
breakpoint_store: Entity<BreakpointStore>,
worktree_store: Entity<WorktreeStore>,
fs: Arc<dyn Fs>,
cx: &mut Context<Self>,
) -> Self {
Self::new(
DapStoreMode::Collab,
breakpoint_store,
worktree_store,
fs,
cx,
)
Self::new(DapStoreMode::Collab, breakpoint_store, worktree_store, cx)
}
fn new(
mode: DapStoreMode,
breakpoint_store: Entity<BreakpointStore>,
worktree_store: Entity<WorktreeStore>,
fs: Arc<dyn Fs>,
cx: &mut Context<Self>,
_cx: &mut Context<Self>,
) -> Self {
cx.background_spawn(async move {
let dir = paths::debug_adapters_dir().join("js-debug-companion");
let mut children = fs.read_dir(&dir).await?.try_collect::<Vec<_>>().await?;
children.sort_by_key(|child| semver::Version::parse(child.file_name()?.to_str()?).ok());
if let Some(child) = children.last()
&& let Some(name) = child.file_name()
&& let Some(name) = name.to_str()
&& semver::Version::parse(name).is_ok()
{
children.pop();
}
for child in children {
fs.remove_dir(
&child,
RemoveOptions {
recursive: true,
ignore_if_not_exists: true,
},
)
.await
.ok();
}
anyhow::Ok(())
})
.detach();
Self {
mode,
next_session_id: 0,
@@ -449,15 +401,6 @@ impl DapStore {
});
}
let (remote_client, node_runtime, http_client) = match &self.mode {
DapStoreMode::Local(_) => (None, None, None),
DapStoreMode::Remote(remote_dap_store) => (
Some(remote_dap_store.remote_client.clone()),
Some(remote_dap_store.node_runtime.clone()),
Some(remote_dap_store.http_client.clone()),
),
DapStoreMode::Collab => (None, None, None),
};
let session = Session::new(
self.breakpoint_store.clone(),
session_id,
@@ -466,9 +409,6 @@ impl DapStore {
adapter,
task_context,
quirks,
remote_client,
node_runtime,
http_client,
cx,
);
@@ -598,7 +538,6 @@ impl DapStore {
local_store.environment.update(cx, |env, cx| {
env.get_worktree_environment(worktree.clone(), cx)
}),
local_store.is_headless,
))
}
@@ -931,7 +870,6 @@ pub struct DapAdapterDelegate {
http_client: Arc<dyn HttpClient>,
toolchain_store: Arc<dyn LanguageToolchainStore>,
load_shell_env_task: Shared<Task<Option<HashMap<String, String>>>>,
is_headless: bool,
}
impl DapAdapterDelegate {
@@ -943,7 +881,6 @@ impl DapAdapterDelegate {
http_client: Arc<dyn HttpClient>,
toolchain_store: Arc<dyn LanguageToolchainStore>,
load_shell_env_task: Shared<Task<Option<HashMap<String, String>>>>,
is_headless: bool,
) -> Self {
Self {
fs,
@@ -953,7 +890,6 @@ impl DapAdapterDelegate {
node_runtime,
toolchain_store,
load_shell_env_task,
is_headless,
}
}
}
@@ -1017,8 +953,4 @@ impl dap::adapters::DapDelegate for DapAdapterDelegate {
self.fs.load(&abs_path).await
}
fn is_headless(&self) -> bool {
self.is_headless
}
}

View File

@@ -31,28 +31,21 @@ use dap::{
RunInTerminalRequestArguments, StackFramePresentationHint, StartDebuggingRequestArguments,
StartDebuggingRequestArgumentsRequest, VariablePresentationHint, WriteMemoryArguments,
};
use futures::SinkExt;
use futures::channel::mpsc::UnboundedSender;
use futures::channel::{mpsc, oneshot};
use futures::io::BufReader;
use futures::{AsyncBufReadExt as _, SinkExt, StreamExt, TryStreamExt};
use futures::{FutureExt, future::Shared};
use gpui::{
App, AppContext, AsyncApp, BackgroundExecutor, Context, Entity, EventEmitter, SharedString,
Task, WeakEntity,
};
use http_client::HttpClient;
use node_runtime::NodeRuntime;
use remote::RemoteClient;
use rpc::ErrorExt;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use smol::net::TcpListener;
use smol::stream::StreamExt;
use std::any::TypeId;
use std::collections::BTreeMap;
use std::ops::RangeInclusive;
use std::path::PathBuf;
use std::process::Stdio;
use std::u64;
use std::{
any::Any,
@@ -63,7 +56,6 @@ use std::{
};
use task::TaskContext;
use text::{PointUtf16, ToPointUtf16};
use util::command::new_smol_command;
use util::{ResultExt, debug_panic, maybe};
use worktree::Worktree;
@@ -704,10 +696,6 @@ pub struct Session {
task_context: TaskContext,
memory: memory::Memory,
quirks: SessionQuirks,
remote_client: Option<Entity<RemoteClient>>,
node_runtime: Option<NodeRuntime>,
http_client: Option<Arc<dyn HttpClient>>,
companion_port: Option<u16>,
}
trait CacheableCommand: Any + Send + Sync {
@@ -824,9 +812,6 @@ impl Session {
adapter: DebugAdapterName,
task_context: TaskContext,
quirks: SessionQuirks,
remote_client: Option<Entity<RemoteClient>>,
node_runtime: Option<NodeRuntime>,
http_client: Option<Arc<dyn HttpClient>>,
cx: &mut App,
) -> Entity<Self> {
cx.new::<Self>(|cx| {
@@ -882,10 +867,6 @@ impl Session {
task_context,
memory: memory::Memory::new(),
quirks,
remote_client,
node_runtime,
http_client,
companion_port: None,
}
})
}
@@ -1576,21 +1557,7 @@ impl Session {
Events::ProgressStart(_) => {}
Events::ProgressUpdate(_) => {}
Events::Invalidated(_) => {}
Events::Other(event) => {
if event.event == "launchBrowserInCompanion" {
let Some(request) = serde_json::from_value(event.body).ok() else {
log::error!("failed to deserialize launchBrowserInCompanion event");
return;
};
self.launch_browser_for_remote_server(request, cx);
} else if event.event == "killCompanionBrowser" {
let Some(request) = serde_json::from_value(event.body).ok() else {
log::error!("failed to deserialize killCompanionBrowser event");
return;
};
self.kill_browser(request, cx);
}
}
Events::Other(_) => {}
}
}
@@ -2749,304 +2716,4 @@ impl Session {
pub fn quirks(&self) -> SessionQuirks {
self.quirks
}
fn launch_browser_for_remote_server(
&mut self,
mut request: LaunchBrowserInCompanionParams,
cx: &mut Context<Self>,
) {
let Some(remote_client) = self.remote_client.clone() else {
log::error!("can't launch browser in companion for non-remote project");
return;
};
let Some(http_client) = self.http_client.clone() else {
return;
};
let Some(node_runtime) = self.node_runtime.clone() else {
return;
};
let mut console_output = self.console_output(cx);
let task = cx.spawn(async move |this, cx| {
let (dap_port, _child) =
if remote_client.read_with(cx, |client, _| client.shares_network_interface())? {
(request.server_port, None)
} else {
let port = {
let listener = TcpListener::bind("127.0.0.1:0")
.await
.context("getting port for DAP")?;
listener.local_addr()?.port()
};
let child = remote_client.update(cx, |client, _| {
let command = client.build_forward_port_command(
port,
"localhost".into(),
request.server_port,
)?;
let child = new_smol_command(command.program)
.args(command.args)
.envs(command.env)
.spawn()
.context("spawning port forwarding process")?;
anyhow::Ok(child)
})??;
(port, Some(child))
};
let mut companion_process = None;
let companion_port =
if let Some(companion_port) = this.read_with(cx, |this, _| this.companion_port)? {
companion_port
} else {
let task = cx.spawn(async move |cx| spawn_companion(node_runtime, cx).await);
match task.await {
Ok((port, child)) => {
companion_process = Some(child);
port
}
Err(e) => {
console_output
.send(format!("Failed to launch browser companion process: {e}"))
.await
.ok();
return Err(e);
}
}
};
this.update(cx, |this, cx| {
this.companion_port = Some(companion_port);
let Some(mut child) = companion_process else {
return;
};
if let Some(stderr) = child.stderr.take() {
let mut console_output = console_output.clone();
this.background_tasks.push(cx.spawn(async move |_, _| {
let mut stderr = BufReader::new(stderr);
let mut line = String::new();
while let Ok(n) = stderr.read_line(&mut line).await
&& n > 0
{
console_output
.send(format!("companion stderr: {line}"))
.await
.ok();
line.clear();
}
}));
}
this.background_tasks.push(cx.spawn({
let mut console_output = console_output.clone();
async move |_, _| match child.status().await {
Ok(status) => {
if status.success() {
console_output
.send("Companion process exited normally".into())
.await
.ok();
} else {
console_output
.send(format!(
"Companion process exited abnormally with {status:?}"
))
.await
.ok();
}
}
Err(e) => {
console_output
.send(format!("Failed to join companion process: {e}"))
.await
.ok();
}
}
}))
})?;
request
.other
.insert("proxyUri".into(), format!("127.0.0.1:{dap_port}").into());
// TODO pass wslInfo as needed
let response = http_client
.post_json(
&format!("http://127.0.0.1:{companion_port}/launch-and-attach"),
serde_json::to_string(&request)
.context("serializing request")?
.into(),
)
.await;
match response {
Ok(response) => {
if !response.status().is_success() {
console_output
.send("Launch request to companion failed".into())
.await
.ok();
return Err(anyhow!("launch request failed"));
}
}
Err(e) => {
console_output
.send("Failed to read response from companion".into())
.await
.ok();
return Err(e);
}
}
anyhow::Ok(())
});
self.background_tasks.push(cx.spawn(async move |_, _| {
task.await.log_err();
}));
}
fn kill_browser(&self, request: KillCompanionBrowserParams, cx: &mut App) {
let Some(companion_port) = self.companion_port else {
log::error!("received killCompanionBrowser but js-debug-companion is not running");
return;
};
let Some(http_client) = self.http_client.clone() else {
return;
};
cx.spawn(async move |_| {
http_client
.post_json(
&format!("http://127.0.0.1:{companion_port}/kill"),
serde_json::to_string(&request)
.context("serializing request")?
.into(),
)
.await?;
anyhow::Ok(())
})
.detach_and_log_err(cx)
}
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct LaunchBrowserInCompanionParams {
server_port: u16,
#[serde(flatten)]
other: HashMap<String, serde_json::Value>,
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct KillCompanionBrowserParams {
launch_id: u64,
}
async fn spawn_companion(
node_runtime: NodeRuntime,
cx: &mut AsyncApp,
) -> Result<(u16, smol::process::Child)> {
let binary_path = node_runtime
.binary_path()
.await
.context("getting node path")?;
let path = cx
.spawn(async move |cx| get_or_install_companion(node_runtime, cx).await)
.await?;
log::info!("will launch js-debug-companion version {path:?}");
let port = {
let listener = TcpListener::bind("127.0.0.1:0")
.await
.context("getting port for companion")?;
listener.local_addr()?.port()
};
let dir = paths::data_dir()
.join("js_debug_companion_state")
.to_string_lossy()
.to_string();
let child = new_smol_command(binary_path)
.arg(path)
.args([
format!("--listen=127.0.0.1:{port}"),
format!("--state={dir}"),
])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.context("spawning companion child process")?;
Ok((port, child))
}
async fn get_or_install_companion(node: NodeRuntime, cx: &mut AsyncApp) -> Result<PathBuf> {
const PACKAGE_NAME: &str = "@zed-industries/js-debug-companion-cli";
async fn install_latest_version(dir: PathBuf, node: NodeRuntime) -> Result<PathBuf> {
let temp_dir = tempfile::tempdir().context("creating temporary directory")?;
node.npm_install_packages(temp_dir.path(), &[(PACKAGE_NAME, "latest")])
.await
.context("installing latest companion package")?;
let version = node
.npm_package_installed_version(temp_dir.path(), PACKAGE_NAME)
.await
.context("getting installed companion version")?
.context("companion was not installed")?;
smol::fs::rename(temp_dir.path(), dir.join(&version))
.await
.context("moving companion package into place")?;
Ok(dir.join(version))
}
let dir = paths::debug_adapters_dir().join("js-debug-companion");
let (latest_installed_version, latest_version) = cx
.background_spawn({
let dir = dir.clone();
let node = node.clone();
async move {
smol::fs::create_dir_all(&dir)
.await
.context("creating companion installation directory")?;
let mut children = smol::fs::read_dir(&dir)
.await
.context("reading companion installation directory")?
.try_collect::<Vec<_>>()
.await
.context("reading companion installation directory entries")?;
children
.sort_by_key(|child| semver::Version::parse(child.file_name().to_str()?).ok());
let latest_installed_version = children.last().and_then(|child| {
let version = child.file_name().into_string().ok()?;
Some((child.path(), version))
});
let latest_version = node
.npm_package_latest_version(PACKAGE_NAME)
.await
.log_err();
anyhow::Ok((latest_installed_version, latest_version))
}
})
.await?;
let path = if let Some((installed_path, installed_version)) = latest_installed_version {
if let Some(latest_version) = latest_version
&& latest_version != installed_version
{
cx.background_spawn(install_latest_version(dir.clone(), node.clone()))
.detach();
}
Ok(installed_path)
} else {
cx.background_spawn(install_latest_version(dir.clone(), node.clone()))
.await
};
Ok(path?
.join("node_modules")
.join(PACKAGE_NAME)
.join("out")
.join("cli.js"))
}

View File

@@ -7,7 +7,7 @@ use crate::{
worktree_store::{WorktreeStore, WorktreeStoreEvent},
};
use anyhow::{Context as _, Result, anyhow, bail};
use askpass::{AskPassDelegate, EncryptedPassword};
use askpass::{AskPassDelegate, EncryptedPassword, IKnowWhatIAmDoingAndIHaveReadTheDocs};
use buffer_diff::{BufferDiff, BufferDiffEvent};
use client::ProjectId;
use collections::HashMap;
@@ -2120,7 +2120,10 @@ impl GitStore {
.lock()
.insert(envelope.payload.askpass_id, askpass);
response.try_into()
// In fact, we don't quite know what we're doing here, as we're sending askpass password unencrypted, but..
Ok(proto::AskPassResponse {
response: response.decrypt(IKnowWhatIAmDoingAndIHaveReadTheDocs)?,
})
}
async fn handle_check_for_pushed_commits(

View File

@@ -1084,7 +1084,6 @@ impl Project {
toolchain_store.read(cx).as_language_toolchain_store(),
worktree_store.clone(),
breakpoint_store.clone(),
false,
cx,
)
});
@@ -1307,9 +1306,6 @@ impl Project {
remote.clone(),
breakpoint_store.clone(),
worktree_store.clone(),
node.clone(),
client.http_client(),
fs.clone(),
cx,
)
});
@@ -1507,7 +1503,6 @@ impl Project {
client.clone().into(),
breakpoint_store.clone(),
worktree_store.clone(),
fs.clone(),
cx,
)
})?;

View File

@@ -51,7 +51,6 @@ workspace-hack.workspace = true
client = { workspace = true, features = ["test-support"] }
criterion.workspace = true
editor = { workspace = true, features = ["test-support"] }
env_logger.workspace = true
gpui = { workspace = true, features = ["test-support"] }
language = { workspace = true, features = ["test-support"] }
serde_json.workspace = true

View File

@@ -102,7 +102,7 @@ impl State {
max_width_item_index: None,
edit_state: old.edit_state.clone(),
unfolded_dir_ids: old.unfolded_dir_ids.clone(),
selection: dbg!(old.selection),
selection: old.selection,
expanded_dir_ids: old.expanded_dir_ids.clone(),
}
}
@@ -398,7 +398,6 @@ pub fn init(cx: &mut App) {
if let Some(first_marked) = panel.marked_entries.first() {
let first_marked = *first_marked;
panel.marked_entries.clear();
dbg!();
panel.state.selection = Some(first_marked);
}
panel.rename(action, window, cx);
@@ -730,10 +729,8 @@ impl ProjectPanel {
focus_opened_item,
allow_preview,
} => {
dbg!();
if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx)
&& let Some(entry) = worktree.read(cx).entry_for_id(entry_id) {
dbg!();
let file_path = entry.path.clone();
let worktree_id = worktree.read(cx).id();
let entry_id = entry.id;
@@ -770,13 +767,12 @@ impl ProjectPanel {
});
if let Some(project_panel) = project_panel.upgrade() {
dbg!();
// Always select and mark the entry, regardless of whether it is opened or not.
project_panel.update(cx, |project_panel, _| {
let entry = SelectedEntry { worktree_id, entry_id };
project_panel.marked_entries.clear();
project_panel.marked_entries.push(entry);
project_panel.state.selection = Some(dbg!(entry));
project_panel.state.selection = Some(entry);
});
if !focus_opened_item {
let focus_handle = project_panel.read(cx).focus_handle.clone();
@@ -961,7 +957,6 @@ impl ProjectPanel {
return;
};
dbg!();
self.state.selection = Some(SelectedEntry {
worktree_id,
entry_id,
@@ -1403,7 +1398,6 @@ impl ProjectPanel {
worktree_id: *worktree_id,
entry_id: entries[entry_ix].id,
};
dbg!();
self.state.selection = Some(selection);
if window.modifiers().shift {
self.marked_entries.push(selection);
@@ -1581,7 +1575,6 @@ impl ProjectPanel {
let edit_task;
let edited_entry_id;
if is_new_entry {
dbg!();
self.state.selection = Some(SelectedEntry {
worktree_id,
entry_id: NEW_ENTRY_ID,
@@ -1636,7 +1629,6 @@ impl ProjectPanel {
project_panel.update_in( cx, |project_panel, window, cx| {
if let Some(selection) = &mut project_panel.state.selection
&& selection.entry_id == edited_entry_id {
dbg!();
selection.worktree_id = worktree_id;
selection.entry_id = new_entry.id;
project_panel.marked_entries.clear();
@@ -1697,7 +1689,6 @@ impl ProjectPanel {
if let Some(previously_focused) =
previous_edit_state.and_then(|edit_state| edit_state.previously_focused)
{
dbg!();
self.state.selection = Some(previously_focused);
self.autoscroll(cx);
}
@@ -1758,7 +1749,6 @@ impl ProjectPanel {
.read(cx)
.id();
dbg!();
self.state.selection = Some(SelectedEntry {
worktree_id,
entry_id,
@@ -2263,7 +2253,6 @@ impl ProjectPanel {
worktree_id: *worktree_id,
entry_id: entry.id,
};
dbg!();
self.state.selection = Some(selection);
if window.modifiers().shift {
self.marked_entries.push(selection);
@@ -2303,7 +2292,6 @@ impl ProjectPanel {
);
if let Some(selection) = selection {
dbg!();
self.state.selection = Some(selection);
self.expand_entry(selection.worktree_id, selection.entry_id, cx);
self.update_visible_entries(
@@ -2343,7 +2331,6 @@ impl ProjectPanel {
);
if let Some(selection) = selection {
dbg!();
self.state.selection = Some(selection);
self.expand_entry(selection.worktree_id, selection.entry_id, cx);
self.update_visible_entries(
@@ -2382,7 +2369,6 @@ impl ProjectPanel {
);
if let Some(selection) = selection {
dbg!();
self.state.selection = Some(selection);
self.expand_entry(selection.worktree_id, selection.entry_id, cx);
self.update_visible_entries(
@@ -2418,7 +2404,6 @@ impl ProjectPanel {
);
if let Some(selection) = selection {
dbg!();
self.state.selection = Some(selection);
self.autoscroll(cx);
cx.notify();
@@ -2447,7 +2432,6 @@ impl ProjectPanel {
);
if let Some(selection) = selection {
dbg!();
self.state.selection = Some(selection);
self.autoscroll(cx);
cx.notify();
@@ -2477,7 +2461,6 @@ impl ProjectPanel {
);
if let Some(selection) = selection {
dbg!();
self.state.selection = Some(selection);
self.expand_entry(selection.worktree_id, selection.entry_id, cx);
self.update_visible_entries(
@@ -2496,7 +2479,6 @@ impl ProjectPanel {
if let Some(parent) = entry.path.parent() {
let worktree = worktree.read(cx);
if let Some(parent_entry) = worktree.entry_for_path(parent) {
dbg!();
self.state.selection = Some(SelectedEntry {
worktree_id: worktree.id(),
entry_id: parent_entry.id,
@@ -2522,7 +2504,6 @@ impl ProjectPanel {
worktree_id: *worktree_id,
entry_id: entry.id,
};
dbg!();
self.state.selection = Some(selection);
if window.modifiers().shift {
self.marked_entries.push(selection);
@@ -2547,7 +2528,6 @@ impl ProjectPanel {
worktree_id: *worktree_id,
entry_id: entry.id,
};
dbg!();
self.state.selection = Some(selection);
self.autoscroll(cx);
cx.notify();
@@ -2699,19 +2679,16 @@ impl ProjectPanel {
}
// update selection
if let Some(entry) = last_succeed {
dbg!();
project_panel
.update_in(cx, |project_panel, window, cx| {
dbg!();
project_panel.state.selection = Some(SelectedEntry {
worktree_id,
entry_id: dbg!(entry.id),
entry_id: entry.id,
});
if item_count == 1 {
// open entry if not dir, and only focus if rename is not pending
if !dbg!(entry.is_dir()) {
dbg!();
if !entry.is_dir() {
project_panel.open_entry(
entry.id,
disambiguation_range.is_none(),
@@ -2722,7 +2699,6 @@ impl ProjectPanel {
// if only one entry was pasted and it was disambiguated, open the rename editor
if disambiguation_range.is_some() {
dbg!();
cx.defer_in(window, |this, window, cx| {
this.rename_impl(disambiguation_range, window, cx);
});
@@ -3216,7 +3192,6 @@ impl ProjectPanel {
let old_ancestors = self.state.ancestors.clone();
let mut new_state = State::derive(&self.state);
dbg!(&new_state.selection);
new_state.last_worktree_root_id = project
.visible_worktrees(cx)
.next_back()
@@ -3444,17 +3419,15 @@ impl ProjectPanel {
}
}
if let Some((worktree_id, entry_id)) = new_selected_entry {
dbg!();
new_state.selection = Some(dbg!(SelectedEntry {
new_state.selection = Some(SelectedEntry {
worktree_id,
entry_id,
}));
});
}
new_state
})
.await;
this.update_in(cx, |this, window, cx| {
dbg!();
this.state = new_state;
let elapsed = now.elapsed();
if this.last_reported_update.elapsed() > Duration::from_secs(3600) {
@@ -3666,7 +3639,6 @@ impl ProjectPanel {
if let Some(entry_id) = last_succeed {
project_panel
.update_in(cx, |project_panel, window, cx| {
dbg!();
project_panel.state.selection = Some(SelectedEntry {
worktree_id,
entry_id,
@@ -4621,7 +4593,6 @@ impl ProjectPanel {
}
}
dbg!();
project_panel.state.selection = Some(clicked_entry);
if !project_panel.marked_entries.contains(&clicked_entry) {
project_panel.marked_entries.push(clicked_entry);
@@ -4631,7 +4602,6 @@ impl ProjectPanel {
if event.click_count() > 1 {
project_panel.split_entry(entry_id, false, None, cx);
} else {
dbg!();
project_panel.state.selection = Some(selection);
if let Some(position) = project_panel.marked_entries.iter().position(|e| *e == selection) {
project_panel.marked_entries.remove(position);
@@ -5018,9 +4988,6 @@ impl ProjectPanel {
};
let is_marked = self.marked_entries.contains(&selection);
let is_selected = self.state.selection == Some(selection);
if is_selected {
dbg!(&entry.path, &self.state.selection);
}
let diagnostic_severity = self
.diagnostics
@@ -5485,7 +5452,6 @@ impl Render for ProjectPanel {
return;
};
dbg!();
this.state.selection = Some(SelectedEntry {
worktree_id,
entry_id,

View File

@@ -1445,10 +1445,6 @@ async fn test_cut_paste_between_different_worktrees(cx: &mut gpui::TestAppContex
async fn test_copy_paste_between_different_worktrees(cx: &mut gpui::TestAppContext) {
init_test(cx);
cx.update(|cx| {
register_project_item::<TestProjectItemView>(cx);
});
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
"/root1",
@@ -1493,16 +1489,9 @@ async fn test_copy_paste_between_different_worktrees(cx: &mut gpui::TestAppConte
select_path(&panel, "root2/one.txt", cx);
panel.update_in(cx, |panel, window, cx| {
panel.select_next(&Default::default(), window, cx);
dbg!();
});
cx.executor().run_until_parked();
dbg!(visible_entries_as_strings(&panel, 0..50, cx));
panel.update_in(cx, |panel, window, cx| {
panel.paste(&Default::default(), window, cx);
dbg!();
});
cx.executor().run_until_parked();
dbg!();
assert_eq!(
visible_entries_as_strings(&panel, 0..50, cx),
&[
@@ -3114,7 +3103,6 @@ async fn test_rename_with_hide_root(cx: &mut gpui::TestAppContext) {
let project = panel.project.read(cx);
let worktree = project.visible_worktrees(cx).next().unwrap();
let root_entry = worktree.read(cx).root_entry().unwrap();
dbg!();
panel.state.selection = Some(SelectedEntry {
worktree_id: worktree.read(cx).id(),
entry_id: root_entry.id,
@@ -6695,7 +6683,7 @@ fn select_path(panel: &Entity<ProjectPanel>, path: &str, cx: &mut VisualTestCont
panel.update_in(cx, |panel, window, cx| {
for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
let worktree = worktree.read(cx);
if let Ok(relative_path) = path.strip_prefix(dbg!(worktree.root_name())) {
if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
panel.update_visible_entries(
Some((worktree.id(), entry_id)),
@@ -6704,7 +6692,6 @@ fn select_path(panel: &Entity<ProjectPanel>, path: &str, cx: &mut VisualTestCont
window,
cx,
);
dbg!(path);
return;
}
}
@@ -6727,7 +6714,6 @@ fn select_path_with_mark(panel: &Entity<ProjectPanel>, path: &str, cx: &mut Visu
if !panel.marked_entries.contains(&entry) {
panel.marked_entries.push(entry);
}
dbg!();
panel.state.selection = Some(entry);
return;
}
@@ -6813,7 +6799,6 @@ fn visible_entries_as_strings(
fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| {
env_logger::try_init().ok();
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
init_settings(cx);

View File

@@ -836,18 +836,6 @@ impl RemoteClient {
connection.build_command(program, args, env, working_dir, port_forward)
}
pub fn build_forward_port_command(
&self,
local_port: u16,
host: String,
remote_port: u16,
) -> Result<CommandTemplate> {
let Some(connection) = self.remote_connection() else {
return Err(anyhow!("no ssh connection"));
};
connection.build_forward_port_command(local_port, host, remote_port)
}
pub fn upload_directory(
&self,
src_path: PathBuf,
@@ -1116,12 +1104,6 @@ pub(crate) trait RemoteConnection: Send + Sync {
working_dir: Option<String>,
port_forward: Option<(u16, String, u16)>,
) -> Result<CommandTemplate>;
fn build_forward_port_command(
&self,
local_port: u16,
remote: String,
remote_port: u16,
) -> Result<CommandTemplate>;
fn connection_options(&self) -> RemoteConnectionOptions;
fn path_style(&self) -> PathStyle;
fn shell(&self) -> String;
@@ -1551,23 +1533,6 @@ mod fake {
})
}
fn build_forward_port_command(
&self,
local_port: u16,
host: String,
remote_port: u16,
) -> anyhow::Result<CommandTemplate> {
Ok(CommandTemplate {
program: "ssh".into(),
args: vec![
"-N".into(),
"-L".into(),
format!("{local_port}:{host}:{remote_port}"),
],
env: Default::default(),
})
}
fn upload_directory(
&self,
_src_path: PathBuf,

View File

@@ -70,14 +70,13 @@ impl From<settings::SshConnection> for SshConnectionOptions {
}
}
#[derive(Clone)]
struct SshSocket {
connection_options: SshConnectionOptions,
#[cfg(not(target_os = "windows"))]
socket_path: PathBuf,
socket_path: std::path::PathBuf,
envs: HashMap<String, String>,
#[cfg(target_os = "windows")]
password: askpass::EncryptedPassword,
_proxy: askpass::PasswordProxy,
}
macro_rules! shell_script {
@@ -146,23 +145,6 @@ impl RemoteConnection for SshRemoteConnection {
)
}
fn build_forward_port_command(
&self,
local_port: u16,
host: String,
remote_port: u16,
) -> Result<CommandTemplate> {
Ok(CommandTemplate {
program: "ssh".into(),
args: vec![
"-N".into(),
"-L".into(),
format!("{local_port}:{host}:{remote_port}"),
],
env: Default::default(),
})
}
fn upload_directory(
&self,
src_path: PathBuf,
@@ -360,16 +342,17 @@ impl SshRemoteConnection {
}
#[cfg(not(target_os = "windows"))]
let socket = SshSocket::new(connection_options, socket_path)?;
let socket = SshSocket::new(connection_options, socket_path).await?;
#[cfg(target_os = "windows")]
let socket = SshSocket::new(
connection_options,
&temp_dir,
askpass
.get_password()
.or_else(|| askpass::EncryptedPassword::try_from("").ok())
.context("Failed to fetch askpass password")?,
)?;
cx.background_executor().clone(),
)
.await?;
drop(askpass);
let ssh_platform = socket.platform().await?;
@@ -676,7 +659,7 @@ impl SshRemoteConnection {
impl SshSocket {
#[cfg(not(target_os = "windows"))]
fn new(options: SshConnectionOptions, socket_path: PathBuf) -> Result<Self> {
async fn new(options: SshConnectionOptions, socket_path: PathBuf) -> Result<Self> {
Ok(Self {
connection_options: options,
envs: HashMap::default(),
@@ -685,21 +668,26 @@ impl SshSocket {
}
#[cfg(target_os = "windows")]
fn new(
async fn new(
options: SshConnectionOptions,
temp_dir: &TempDir,
password: askpass::EncryptedPassword,
executor: gpui::BackgroundExecutor,
) -> Result<Self> {
let askpass_script = temp_dir.path().join("askpass.bat");
std::fs::write(&askpass_script, "@ECHO OFF\necho %ZED_SSH_ASKPASS%")?;
let mut envs = HashMap::default();
let get_password =
move |_| Task::ready(std::ops::ControlFlow::Continue(Ok(password.clone())));
let _proxy = askpass::PasswordProxy::new(get_password, executor).await?;
envs.insert("SSH_ASKPASS_REQUIRE".into(), "force".into());
envs.insert("SSH_ASKPASS".into(), askpass_script.display().to_string());
envs.insert(
"SSH_ASKPASS".into(),
_proxy.script_path().as_ref().display().to_string(),
);
Ok(Self {
connection_options: options,
envs,
password,
_proxy,
})
}
@@ -753,14 +741,12 @@ impl SshSocket {
#[cfg(target_os = "windows")]
fn ssh_options<'a>(&self, command: &'a mut process::Command) -> &'a mut process::Command {
use askpass::ProcessExt;
command
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.args(self.connection_options.additional_args())
.envs(self.envs.clone())
.encrypted_env("ZED_SSH_ASKPASS", self.password.clone())
}
// On Windows, we need to use `SSH_ASKPASS` to provide the password to ssh.

View File

@@ -433,15 +433,6 @@ impl RemoteConnection for WslRemoteConnection {
})
}
fn build_forward_port_command(
&self,
_: u16,
_: String,
_: u16,
) -> anyhow::Result<CommandTemplate> {
Err(anyhow!("WSL shares a network interface with the host"))
}
fn connection_options(&self) -> RemoteConnectionOptions {
RemoteConnectionOptions::Wsl(self.connection_options.clone())
}

View File

@@ -123,7 +123,6 @@ impl HeadlessProject {
toolchain_store.read(cx).as_language_toolchain_store(),
worktree_store.clone(),
breakpoint_store.clone(),
true,
cx,
);
dap_store.shared(REMOTE_SERVER_PROJECT_ID, session.clone(), cx);

View File

@@ -397,7 +397,19 @@ pub enum SteppingGranularity {
Instruction,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)]
#[derive(
Copy,
Clone,
Debug,
Serialize,
Deserialize,
JsonSchema,
MergeFrom,
PartialEq,
Eq,
strum::VariantArray,
strum::VariantNames,
)]
#[serde(rename_all = "snake_case")]
pub enum DockPosition {
Left,
@@ -571,7 +583,18 @@ pub struct FileFinderSettingsContent {
}
#[derive(
Debug, PartialEq, Eq, Clone, Copy, Default, Serialize, Deserialize, JsonSchema, MergeFrom,
Debug,
PartialEq,
Eq,
Clone,
Copy,
Default,
Serialize,
Deserialize,
JsonSchema,
MergeFrom,
strum::VariantArray,
strum::VariantNames,
)]
#[serde(rename_all = "lowercase")]
pub enum FileFinderWidthContent {
@@ -733,7 +756,19 @@ pub enum DockSide {
Right,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize, Serialize, JsonSchema, MergeFrom)]
#[derive(
Copy,
Clone,
Debug,
PartialEq,
Eq,
Deserialize,
Serialize,
JsonSchema,
MergeFrom,
strum::VariantArray,
strum::VariantNames,
)]
#[serde(rename_all = "snake_case")]
pub enum ShowIndentGuides {
Always,

View File

@@ -541,7 +541,18 @@ pub struct ProjectPanelSettingsContent {
}
#[derive(
Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq,
Copy,
Clone,
Debug,
Default,
Serialize,
Deserialize,
JsonSchema,
MergeFrom,
PartialEq,
Eq,
strum::VariantArray,
strum::VariantNames,
)]
#[serde(rename_all = "snake_case")]
pub enum ProjectPanelEntrySpacing {

File diff suppressed because it is too large Load Diff

View File

@@ -278,6 +278,9 @@ fn init_renderers(cx: &mut App) {
.add_renderer::<settings::TerminalDockPosition>(|settings_field, file, _, window, cx| {
render_dropdown(*settings_field, file, window, cx)
})
.add_renderer::<settings::DockPosition>(|settings_field, file, _, window, cx| {
render_dropdown(*settings_field, file, window, cx)
})
.add_renderer::<settings::GitGutterSetting>(|settings_field, file, _, window, cx| {
render_dropdown(*settings_field, file, window, cx)
})
@@ -309,6 +312,11 @@ fn init_renderers(cx: &mut App) {
.add_renderer::<settings::ShowCloseButton>(|settings_field, file, _, window, cx| {
render_dropdown(*settings_field, file, window, cx)
})
.add_renderer::<settings::ProjectPanelEntrySpacing>(
|settings_field, file, _, window, cx| {
render_dropdown(*settings_field, file, window, cx)
},
)
.add_renderer::<settings::RewrapBehavior>(|settings_field, file, _, window, cx| {
render_dropdown(*settings_field, file, window, cx)
})
@@ -323,6 +331,12 @@ fn init_renderers(cx: &mut App) {
render_dropdown(*settings_field, file, window, cx)
},
)
.add_renderer::<settings::FileFinderWidthContent>(|settings_field, file, _, window, cx| {
render_dropdown(*settings_field, file, window, cx)
})
.add_renderer::<settings::ShowDiagnostics>(|settings_field, file, _, window, cx| {
render_dropdown(*settings_field, file, window, cx)
})
.add_renderer::<settings::WordsCompletionMode>(|settings_field, file, _, window, cx| {
render_dropdown(*settings_field, file, window, cx)
})

View File

@@ -624,14 +624,12 @@ impl ProjectItemRegistry {
let project_path = project_path.clone();
let is_file = project
.read(cx)
.entry_for_path(dbg!(&project_path), cx)
.entry_for_path(&project_path, cx)
.is_some_and(|entry| entry.is_file());
let entry_abs_path = project.read(cx).absolute_path(&project_path, cx);
dbg!(&entry_abs_path);
let is_local = project.read(cx).is_local();
let project_item =
<T::Item as project::ProjectItem>::try_open(project, &project_path, cx)?;
dbg!();
let project = project.clone();
Some(window.spawn(cx, async move |cx| {
match project_item.await.with_context(|| {
@@ -695,7 +693,6 @@ impl ProjectItemRegistry {
window: &mut Window,
cx: &mut App,
) -> Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>> {
dbg!(self.build_project_item_for_path_fns.len());
let Some(open_project_item) = self
.build_project_item_for_path_fns
.iter()

View File

@@ -1350,7 +1350,7 @@ mod tests {
let (res_tx, res_rx) = oneshot::channel();
req_tx.unbounded_send((req, res_tx)).unwrap();
serde_json::to_string(&res_rx.await?).unwrap()
serde_json::to_string(&res_rx.await.unwrap()).unwrap()
}
_ => {
panic!("Unexpected path: {}", uri)