fix: improve GPUI blur and WSL integration
This commit is contained in:
1 parent
cb2a8e0256
commit
10be219f3e
8 files changed
+841
-143
No files matched your search
@@ -72,4 +72,7 @@ This file contains changes that are not part of a numbered release yet.
|
||||
The PowerShell/OSC changes need first-prompt checks for all profile creation
|
||||
routes. The mixed-DPI change still requires a real Windows two-monitor matrix,
|
||||
including Windows 11 150% -> 125% and the reverse direction. A compile check
|
||||
alone is not sufficient to claim that the physical monitor seam is fixed.
|
||||
alone is not sufficient to claim that the physical monitor seam is fixed. The
|
||||
WSL changes still require guest-path checks for both `/mnt/<drive>` and native
|
||||
Linux repositories, and the blur change requires a live Windows DWM toggle
|
||||
check in both directions.
|
||||
@@ -30,6 +30,9 @@ pub enum PanelView {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FileRow {
|
||||
pub path: PathBuf,
|
||||
/// WSL 来宾中的真实绝对路径。`path` 仍作为现有选择/展开状态的稳定键,
|
||||
/// 但绝不能把它交给宿主文件 API;来宾操作必须显式使用这一字段。
|
||||
pub guest_path: Option<String>,
|
||||
pub name: String,
|
||||
pub depth: usize,
|
||||
pub is_dir: bool,
|
||||
@@ -211,8 +214,15 @@ pub struct SidePanel {
|
||||
/// Latest focused pane cwd, retained while a custom root is active so the
|
||||
/// panel can resume following immediately without persisting any setting.
|
||||
followed_cwd: Option<PathBuf>,
|
||||
/// 聚焦终端是 WSL 且停在来宾目录时的位置。宿主未必看得见 WSL 文件系统
|
||||
/// (9P 重定向不一定可用),所以这不是"另一种 cwd"——它是让 Git 视图
|
||||
/// 改用「在来宾里跑 git」的开关,与 [`Self::followed_cwd`] 各管一段。
|
||||
followed_wsl: Option<crate::shell_detect::WslCwd>,
|
||||
/// Window-local override selected from the Files view.
|
||||
custom_root: Option<PathBuf>,
|
||||
/// WSL guest tree 的窗口内浏览根(例如点 `..` 后得到的 `/home`)。与
|
||||
/// `custom_root` 互斥,且不改变终端实际 cwd / Git 跟随位置。
|
||||
custom_wsl_root: Option<crate::shell_detect::WslCwd>,
|
||||
/// Visible feedback for an invalid/disappeared custom root.
|
||||
root_notice: Option<String>,
|
||||
/// Flattened visible tree rows for the Files view.
|
||||
@@ -276,6 +286,8 @@ struct PanelSnapshot {
|
||||
/// Root the snapshot was built from — stale snapshots (root changed while
|
||||
/// the worker ran) are dropped on harvest.
|
||||
root: PathBuf,
|
||||
/// Files 快照所属的来宾位置;避免两个发行版恰好同为 `/home` 时串结果。
|
||||
files_wsl: Option<crate::shell_detect::WslCwd>,
|
||||
rows: Vec<FileRow>,
|
||||
git: Option<GitInfo>,
|
||||
}
|
||||
@@ -483,7 +495,9 @@ impl SidePanel {
|
||||
view: PanelView::Files,
|
||||
root: None,
|
||||
followed_cwd: None,
|
||||
followed_wsl: None,
|
||||
custom_root: None,
|
||||
custom_wsl_root: None,
|
||||
root_notice: None,
|
||||
rows: Vec::new(),
|
||||
expanded: HashSet::new(),
|
||||
@@ -530,25 +544,63 @@ impl SidePanel {
|
||||
/// refresh was requested (toggle), or the throttle window has elapsed.
|
||||
/// Called once per drawn frame from the window context; cheap when nothing
|
||||
/// changed. Returns whether the snapshot was rebuilt (i.e. needs redraw).
|
||||
///
|
||||
/// 抽屉内部的刷新(文件操作后、根切换后)走这一层就够;跟随聚焦终端的
|
||||
/// 调用方请用 [`Self::sync_at`] 一并交出 WSL 位置。
|
||||
pub fn sync(&mut self, cwd: Option<PathBuf>) -> bool {
|
||||
self.sync_at(cwd, None)
|
||||
}
|
||||
|
||||
/// [`Self::sync`] 加上 WSL 位置。`wsl` = 聚焦终端所在的 WSL 发行版与来宾
|
||||
/// 目录([`crate::shell_detect::wsl_cwd`] 解析),`None` = 聚焦的是宿主
|
||||
/// 终端或压根认不出发行版。
|
||||
pub fn sync_at(
|
||||
&mut self,
|
||||
cwd: Option<PathBuf>,
|
||||
wsl: Option<crate::shell_detect::WslCwd>,
|
||||
) -> bool {
|
||||
if !self.open {
|
||||
return false;
|
||||
}
|
||||
// 先收割落地的后台快照——旧内容在工人跑动期间一直显示,这里一次
|
||||
// 性换成新内容(先显示旧的、再更新,VSCode 的树刷新模式)。
|
||||
let mut changed = self.harvest_snapshot();
|
||||
let changed = self.harvest_snapshot();
|
||||
// 聚焦 pane 报不出本地有效目录时(SSH 远端路径、未映射的 WSL 路
|
||||
// 径、shell 还没发 OSC)保持现状:跟随的语义是"最后一个已知有效
|
||||
// 目录",清空只会让切到远程 tab 的瞬间目录树闪成空白。
|
||||
if cwd.is_some() {
|
||||
// 先记下这次到底给出了哪些位置——两个字段各自会 move 掉入参。
|
||||
let had_cwd = cwd.is_some();
|
||||
let has_guest_cwd = !had_cwd && wsl.is_some();
|
||||
if had_cwd {
|
||||
self.followed_cwd = cwd;
|
||||
} else if has_guest_cwd {
|
||||
// `/mnt/d` 能映射为 D:\,而 `/home` 只能由来宾读取。从前这里保留
|
||||
// 旧 D:\,导致新 WSL 位置永远被宿主根盖住。
|
||||
self.followed_cwd = None;
|
||||
}
|
||||
// WSL 位置沿用同一套"最后一个已知有效位置"语义,但多一条清除规则:
|
||||
// 切回一个宿主终端(cwd 有值而 wsl 没有)必须把它清掉,否则 Git 面板
|
||||
// 会继续显示上一个 WSL 仓库的状态。两者都拿不到(SSH tab)才保持现状。
|
||||
let wsl_before = self.followed_wsl.clone();
|
||||
if wsl.is_some() {
|
||||
self.followed_wsl = wsl;
|
||||
} else if had_cwd {
|
||||
self.followed_wsl = None;
|
||||
}
|
||||
// 换了 WSL 仓库、或从 WSL 切回宿主,都要立刻重算 Git 快照:这类切换
|
||||
// 不改 `root`(宿主看不见来宾目录,树停在原处),光靠 `root_changed`
|
||||
// 抓不到,只等节流窗口的话面板会挂着上一个仓库的分支名不动。
|
||||
let wsl_changed = wsl_before != self.followed_wsl;
|
||||
let custom_invalidated = self.custom_root.as_ref().is_some_and(|root| !root.is_dir());
|
||||
if custom_invalidated {
|
||||
self.custom_root = None;
|
||||
self.root_notice = Some("所选目录不可用,已跟随当前目录".to_owned());
|
||||
}
|
||||
let next_root = self.custom_root.clone().or_else(|| self.followed_cwd.clone());
|
||||
let next_root = self
|
||||
.custom_root
|
||||
.clone()
|
||||
.or_else(|| self.custom_wsl_root.as_ref().map(wsl_root_key))
|
||||
.or_else(|| self.followed_tree_root());
|
||||
let root_changed = next_root != self.root;
|
||||
// While a filter query is live, skip the periodic re-snapshot: it
|
||||
// would drop and rebuild the search index under the user's fingers.
|
||||
@@ -559,7 +611,7 @@ impl SidePanel {
|
||||
if self.op_done.swap(false, std::sync::atomic::Ordering::Relaxed) {
|
||||
self.needs_refresh = true;
|
||||
}
|
||||
if !(root_changed || custom_invalidated || stale || self.needs_refresh) {
|
||||
if !(root_changed || custom_invalidated || stale || wsl_changed || self.needs_refresh) {
|
||||
return changed;
|
||||
}
|
||||
if root_changed {
|
||||
@@ -569,7 +621,7 @@ impl SidePanel {
|
||||
self.selected = None;
|
||||
}
|
||||
self.refresh();
|
||||
changed || root_changed || custom_invalidated
|
||||
changed || root_changed || custom_invalidated || wsl_changed
|
||||
}
|
||||
|
||||
/// 测试辅助:等后台快照工人落地并收割。生产路径永不阻塞。
|
||||
@@ -598,7 +650,9 @@ impl SidePanel {
|
||||
};
|
||||
let Some(snapshot) = snapshot else { return false };
|
||||
// 工人跑动期间根又变了:这份快照已经过期,丢弃。新刷新在路上。
|
||||
if self.root.as_ref() != Some(&snapshot.root) {
|
||||
if self.root.as_ref() != Some(&snapshot.root)
|
||||
|| self.file_wsl_root().cloned() != snapshot.files_wsl
|
||||
{
|
||||
return false;
|
||||
}
|
||||
self.rows = snapshot.rows;
|
||||
@@ -617,6 +671,7 @@ impl SidePanel {
|
||||
}
|
||||
let changed = self.custom_root.as_ref() != Some(&root) || self.root.as_ref() != Some(&root);
|
||||
self.custom_root = Some(root.clone());
|
||||
self.custom_wsl_root = None;
|
||||
self.root_notice = None;
|
||||
if self.root.as_ref() != Some(&root) {
|
||||
self.root = Some(root);
|
||||
@@ -630,11 +685,13 @@ impl SidePanel {
|
||||
|
||||
/// Resume following the most recently observed focused pane cwd.
|
||||
pub fn clear_custom_root(&mut self) -> bool {
|
||||
if self.custom_root.take().is_none() {
|
||||
let cleared_host = self.custom_root.take().is_some();
|
||||
let cleared_wsl = self.custom_wsl_root.take().is_some();
|
||||
if !cleared_host && !cleared_wsl {
|
||||
return false;
|
||||
}
|
||||
self.root_notice = None;
|
||||
let next_root = self.followed_cwd.clone();
|
||||
let next_root = self.followed_tree_root();
|
||||
if self.root != next_root {
|
||||
self.root = next_root;
|
||||
self.expanded.clear();
|
||||
@@ -646,7 +703,38 @@ impl SidePanel {
|
||||
}
|
||||
|
||||
pub fn custom_root_active(&self) -> bool {
|
||||
self.custom_root.is_some()
|
||||
self.custom_root.is_some() || self.custom_wsl_root.is_some()
|
||||
}
|
||||
|
||||
/// Files 视图当前是否由 WSL guest 提供。宿主可达的 `/mnt/<盘>` 与 UNC
|
||||
/// 不会进入这里,仍保留完整的宿主打开、回收站和 Explorer 操作。
|
||||
pub fn file_wsl_root(&self) -> Option<&crate::shell_detect::WslCwd> {
|
||||
self.custom_wsl_root.as_ref().or_else(|| {
|
||||
(self.custom_root.is_none() && self.followed_cwd.is_none())
|
||||
.then_some(self.followed_wsl.as_ref())
|
||||
.flatten()
|
||||
})
|
||||
}
|
||||
|
||||
fn followed_tree_root(&self) -> Option<PathBuf> {
|
||||
self.followed_cwd.clone().or_else(|| self.followed_wsl.as_ref().map(wsl_root_key))
|
||||
}
|
||||
|
||||
fn set_custom_wsl_root(&mut self, located: crate::shell_detect::WslCwd) -> bool {
|
||||
let root = wsl_root_key(&located);
|
||||
let changed =
|
||||
self.custom_wsl_root.as_ref() != Some(&located) || self.root.as_ref() != Some(&root);
|
||||
self.custom_root = None;
|
||||
self.custom_wsl_root = Some(located);
|
||||
self.root_notice = None;
|
||||
if self.root.as_ref() != Some(&root) {
|
||||
self.root = Some(root);
|
||||
self.expanded.clear();
|
||||
self.scroll = 0;
|
||||
self.selected = None;
|
||||
self.refresh();
|
||||
}
|
||||
changed
|
||||
}
|
||||
|
||||
/// 目录:Git/SVN 状态所属的那一个。始终是终端当前目录,与目录树的浏览
|
||||
@@ -706,34 +794,54 @@ impl SidePanel {
|
||||
return;
|
||||
}
|
||||
let expanded = self.expanded.clone();
|
||||
let files_wsl = self.file_wsl_root().cloned();
|
||||
// VCS 状态始终读**终端当前目录**,不跟着目录树的浏览位置走。在树里点
|
||||
// `..` 往上翻是纯浏览动作(`custom_root`,窗口内覆盖),把仓库状态一起
|
||||
// 带走的后果是:翻到仓库外面之后 Git 视图只剩「当前目录不在 Git/SVN
|
||||
// 仓库中」,而回头的入口偏偏只画在 Files 视图里。行与状态在这里解耦:
|
||||
// 行看 `root`,git 看 `followed_cwd`。
|
||||
let git_root = self.followed_cwd.clone().unwrap_or_else(|| root.clone());
|
||||
let wsl = self.followed_wsl.clone();
|
||||
let running = std::sync::Arc::clone(&self.snapshot_running);
|
||||
let slot = std::sync::Arc::clone(&self.snapshot_slot);
|
||||
std::thread::spawn(move || {
|
||||
let rows = SidePanel::tree_rows(&root, &expanded);
|
||||
let rows = match &files_wsl {
|
||||
Some(located) => SidePanel::tree_rows_wsl(located, &expanded),
|
||||
None => SidePanel::tree_rows(&root, &expanded),
|
||||
};
|
||||
// 设置可强制只认 Git / SVN(混合仓库场景);Auto 保持既有探测:
|
||||
// a checkout nested inside a Git tree must remain visible as SVN.
|
||||
// Prefer SVN only when its metadata is in the current path's
|
||||
// ancestor chain; ordinary Git directories keep the cheaper Git
|
||||
// first path and only probe SVN as a fallback.
|
||||
let git = match nebula_settings::RuntimeSettings::load().vcs_display {
|
||||
nebula_settings::VcsDisplayName::Git => read_git(&git_root),
|
||||
nebula_settings::VcsDisplayName::Svn => read_svn(&git_root),
|
||||
nebula_settings::VcsDisplayName::Auto => {
|
||||
if svn_dir_hint(&git_root) {
|
||||
read_svn(&git_root).or_else(|| read_git(&git_root))
|
||||
} else {
|
||||
read_git(&git_root).or_else(|| read_svn(&git_root))
|
||||
}
|
||||
let git = match &wsl {
|
||||
// 聚焦的是 WSL 终端:宿主的 `git_root` 与来宾目录毫无关系
|
||||
// (UNC 不可达时它还停在上一个宿主目录),必须在来宾里跑
|
||||
// git。SVN 不走这条——WSL 里用 svn 没有实据,不加没验证过
|
||||
// 的分支;`vcs_display` 强制 Svn 时同理保持"无仓库"。
|
||||
Some(located)
|
||||
if !matches!(
|
||||
nebula_settings::RuntimeSettings::load().vcs_display,
|
||||
nebula_settings::VcsDisplayName::Svn
|
||||
) =>
|
||||
{
|
||||
read_git_wsl(located)
|
||||
},
|
||||
Some(_) => None,
|
||||
None => match nebula_settings::RuntimeSettings::load().vcs_display {
|
||||
nebula_settings::VcsDisplayName::Git => read_git(&git_root),
|
||||
nebula_settings::VcsDisplayName::Svn => read_svn(&git_root),
|
||||
nebula_settings::VcsDisplayName::Auto => {
|
||||
if svn_dir_hint(&git_root) {
|
||||
read_svn(&git_root).or_else(|| read_git(&git_root))
|
||||
} else {
|
||||
read_git(&git_root).or_else(|| read_svn(&git_root))
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
if let Ok(mut slot) = slot.lock() {
|
||||
*slot = Some(PanelSnapshot { root, rows, git });
|
||||
*slot = Some(PanelSnapshot { root, files_wsl, rows, git });
|
||||
}
|
||||
running.store(false, std::sync::atomic::Ordering::Release);
|
||||
});
|
||||
@@ -746,7 +854,10 @@ impl SidePanel {
|
||||
let Some(root) = self.root.clone() else { return };
|
||||
let needle = self.search.trim().to_lowercase();
|
||||
if needle.is_empty() {
|
||||
self.rows = Self::tree_rows(&root, &self.expanded);
|
||||
self.rows = match self.file_wsl_root() {
|
||||
Some(located) => Self::tree_rows_wsl(located, &self.expanded),
|
||||
None => Self::tree_rows(&root, &self.expanded),
|
||||
};
|
||||
return;
|
||||
}
|
||||
// Filter mode: string-match against the cached flat index. The index
|
||||
@@ -756,8 +867,12 @@ impl SidePanel {
|
||||
if self.search_index.is_none() {
|
||||
let mut index = Vec::new();
|
||||
let mut budget = SEARCH_VISIT_BUDGET;
|
||||
build_search_index(&root, 0, &mut index, &mut budget);
|
||||
Self::mark_ignored(&root, &mut index);
|
||||
if let Some(located) = self.file_wsl_root() {
|
||||
build_wsl_search_index(located, &mut index, &mut budget);
|
||||
} else {
|
||||
build_search_index(&root, 0, &mut index, &mut budget);
|
||||
Self::mark_ignored(&root, &mut index);
|
||||
}
|
||||
self.search_index = Some(index);
|
||||
}
|
||||
let index = self.search_index.as_ref().unwrap();
|
||||
@@ -879,7 +994,25 @@ impl SidePanel {
|
||||
}
|
||||
|
||||
fn spawn_git(&mut self, args: Vec<String>) {
|
||||
self.spawn_vcs("git", args.into_iter().map(OsString::from).collect());
|
||||
if let Some(located) = self.followed_wsl.clone() {
|
||||
let mut guest_args: Vec<OsString> = [
|
||||
"-d",
|
||||
located.distro.as_str(),
|
||||
"--",
|
||||
"git",
|
||||
"-C",
|
||||
located.guest.as_str(),
|
||||
"--no-optional-locks",
|
||||
]
|
||||
.into_iter()
|
||||
.map(OsString::from)
|
||||
.collect();
|
||||
guest_args.extend(args.into_iter().map(OsString::from));
|
||||
let host_cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
|
||||
self.spawn_vcs_at(PathBuf::from("wsl.exe"), guest_args, host_cwd);
|
||||
} else {
|
||||
self.spawn_vcs("git", args.into_iter().map(OsString::from).collect());
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_svn_mutation(&mut self, operation: SvnMutation) {
|
||||
@@ -1148,6 +1281,7 @@ impl SidePanel {
|
||||
if let Some(parent) = root.parent() {
|
||||
rows.push(FileRow {
|
||||
path: parent.to_path_buf(),
|
||||
guest_path: None,
|
||||
name: "..".to_owned(),
|
||||
depth: 0,
|
||||
is_dir: true,
|
||||
@@ -1161,6 +1295,68 @@ impl SidePanel {
|
||||
rows
|
||||
}
|
||||
|
||||
/// UNC 不可达时直接从 WSL guest 枚举 Files 树。命令参数逐项传给
|
||||
/// `CreateProcessW`,不经过 shell,因此发行版名、路径和文件名都不会参与
|
||||
/// 命令拼接。`find -printf` 用 NUL 分隔,换行文件名也不会破坏记录边界。
|
||||
fn tree_rows_wsl(
|
||||
located: &crate::shell_detect::WslCwd,
|
||||
expanded: &HashSet<PathBuf>,
|
||||
) -> Vec<FileRow> {
|
||||
let root = normalize_wsl_guest_path(&located.guest);
|
||||
let mut rows = Vec::new();
|
||||
if let Some(parent) = wsl_guest_parent(&root) {
|
||||
rows.push(FileRow {
|
||||
path: PathBuf::from(&parent),
|
||||
guest_path: Some(parent),
|
||||
name: "..".to_owned(),
|
||||
depth: 0,
|
||||
is_dir: true,
|
||||
expanded: false,
|
||||
is_parent: true,
|
||||
ignored: false,
|
||||
});
|
||||
}
|
||||
Self::flatten_wsl_dir_into(&mut rows, expanded, &located.distro, &root, 0);
|
||||
rows
|
||||
}
|
||||
|
||||
fn flatten_wsl_dir_into(
|
||||
rows: &mut Vec<FileRow>,
|
||||
expanded: &HashSet<PathBuf>,
|
||||
distro: &str,
|
||||
guest_dir: &str,
|
||||
depth: usize,
|
||||
) {
|
||||
if rows.len() >= MAX_ROWS {
|
||||
return;
|
||||
}
|
||||
let mut entries = wsl_read_dir(distro, guest_dir);
|
||||
entries.sort_by(|a, b| b.0.cmp(&a.0).then(a.1.to_lowercase().cmp(&b.1.to_lowercase())));
|
||||
entries.truncate(MAX_PER_DIR);
|
||||
for (is_dir, name, guest_path) in entries {
|
||||
if rows.len() >= MAX_ROWS {
|
||||
return;
|
||||
}
|
||||
// 该 PathBuf 只作为展开/选中的稳定键;真实来宾路径始终取
|
||||
// `guest_path`,绝不把这个键交给宿主文件系统。
|
||||
let path_key = PathBuf::from(&guest_path);
|
||||
let is_expanded = is_dir && expanded.contains(&path_key);
|
||||
rows.push(FileRow {
|
||||
path: path_key.clone(),
|
||||
guest_path: Some(guest_path.clone()),
|
||||
name,
|
||||
depth,
|
||||
is_dir,
|
||||
expanded: is_expanded,
|
||||
is_parent: false,
|
||||
ignored: false,
|
||||
});
|
||||
if is_expanded {
|
||||
Self::flatten_wsl_dir_into(rows, expanded, distro, &guest_path, depth + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn flatten_dir_into(
|
||||
rows: &mut Vec<FileRow>,
|
||||
expanded: &HashSet<PathBuf>,
|
||||
@@ -1190,6 +1386,7 @@ impl SidePanel {
|
||||
let is_expanded = is_dir && expanded.contains(&path);
|
||||
rows.push(FileRow {
|
||||
path: path.clone(),
|
||||
guest_path: None,
|
||||
name,
|
||||
depth,
|
||||
is_dir,
|
||||
@@ -1283,9 +1480,16 @@ impl SidePanel {
|
||||
return false;
|
||||
}
|
||||
let path = row.path.clone();
|
||||
let guest_path = row.guest_path.clone();
|
||||
if row.is_parent {
|
||||
// 返回上级只改变窗口内的目录树根节点,不能连带修改终端 cwd;
|
||||
// 用户仍可通过现有“跟随”操作回到当前终端目录。
|
||||
if let (Some(current), Some(guest)) = (self.file_wsl_root(), guest_path) {
|
||||
return self.set_custom_wsl_root(crate::shell_detect::WslCwd {
|
||||
distro: current.distro.clone(),
|
||||
guest,
|
||||
});
|
||||
}
|
||||
return self.set_custom_root(path);
|
||||
}
|
||||
if !self.expanded.remove(&path) {
|
||||
@@ -1542,6 +1746,7 @@ fn build_search_index(dir: &Path, depth: usize, index: &mut Vec<FileRow>, budget
|
||||
let path = entry.path();
|
||||
index.push(FileRow {
|
||||
path: path.clone(),
|
||||
guest_path: None,
|
||||
name,
|
||||
depth: 0,
|
||||
is_dir,
|
||||
@@ -1555,6 +1760,122 @@ fn build_search_index(dir: &Path, depth: usize, index: &mut Vec<FileRow>, budget
|
||||
}
|
||||
}
|
||||
|
||||
fn wsl_root_key(located: &crate::shell_detect::WslCwd) -> PathBuf {
|
||||
PathBuf::from(normalize_wsl_guest_path(&located.guest))
|
||||
}
|
||||
|
||||
fn normalize_wsl_guest_path(path: &str) -> String {
|
||||
let normalized = path.trim().replace('\\', "/");
|
||||
if normalized == "/" {
|
||||
return normalized;
|
||||
}
|
||||
normalized.trim_end_matches('/').to_owned()
|
||||
}
|
||||
|
||||
fn wsl_guest_parent(path: &str) -> Option<String> {
|
||||
let normalized = normalize_wsl_guest_path(path);
|
||||
if normalized == "/" {
|
||||
return None;
|
||||
}
|
||||
let parent = normalized
|
||||
.rsplit_once('/')
|
||||
.map_or("/", |(parent, _)| if parent.is_empty() { "/" } else { parent });
|
||||
Some(parent.to_owned())
|
||||
}
|
||||
|
||||
fn wsl_guest_join(parent: &str, name: &str) -> String {
|
||||
if parent == "/" {
|
||||
format!("/{name}")
|
||||
} else {
|
||||
format!("{}/{name}", parent.trim_end_matches('/'))
|
||||
}
|
||||
}
|
||||
|
||||
/// `find` 输出为重复的 `type NUL value NUL`。只接受完整记录;来宾进程被
|
||||
/// 中断时尾部半条记录会被自然丢弃,不会生成指向错误位置的行。
|
||||
fn parse_wsl_find_pairs(output: &[u8]) -> Vec<(u8, String)> {
|
||||
let fields: Vec<&[u8]> = output
|
||||
.split_inclusive(|byte| *byte == 0)
|
||||
.filter_map(|field| field.strip_suffix(&[0]))
|
||||
.collect();
|
||||
|
||||
fields
|
||||
.chunks_exact(2)
|
||||
.filter_map(|pair| {
|
||||
let kind = pair[0].first().copied()?;
|
||||
Some((kind, String::from_utf8_lossy(pair[1]).into_owned()))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn run_wsl_find(distro: &str, args: impl IntoIterator<Item = OsString>) -> Option<Vec<u8>> {
|
||||
let mut command = std::process::Command::new("wsl.exe");
|
||||
command.args(["-d", distro, "--", "find"]).args(args);
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use std::os::windows::process::CommandExt;
|
||||
command.creation_flags(0x0800_0000); // CREATE_NO_WINDOW
|
||||
}
|
||||
let output = command.output().ok()?;
|
||||
if !output.status.success() {
|
||||
let reason = String::from_utf8_lossy(&output.stderr);
|
||||
log::debug!("WSL file tree find failed in {distro}: {}", reason.trim());
|
||||
return None;
|
||||
}
|
||||
Some(output.stdout)
|
||||
}
|
||||
|
||||
fn wsl_read_dir(distro: &str, guest_dir: &str) -> Vec<(bool, String, String)> {
|
||||
let args = [guest_dir, "-mindepth", "1", "-maxdepth", "1", "-printf", r"%y\0%f\0"]
|
||||
.into_iter()
|
||||
.map(OsString::from);
|
||||
let Some(output) = run_wsl_find(distro, args) else { return Vec::new() };
|
||||
parse_wsl_find_pairs(&output)
|
||||
.into_iter()
|
||||
.filter_map(|(kind, name)| {
|
||||
(name != ".git").then(|| {
|
||||
let guest_path = wsl_guest_join(guest_dir, &name);
|
||||
(kind == b'd', name, guest_path)
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn build_wsl_search_index(
|
||||
located: &crate::shell_detect::WslCwd,
|
||||
index: &mut Vec<FileRow>,
|
||||
budget: &mut usize,
|
||||
) {
|
||||
let root = normalize_wsl_guest_path(&located.guest);
|
||||
let mut args: Vec<OsString> =
|
||||
[root.as_str(), "-mindepth", "1", "-maxdepth", "8", "(", "-type", "d", "(", "-name", ".*"]
|
||||
.into_iter()
|
||||
.map(OsString::from)
|
||||
.collect();
|
||||
for skipped in SEARCH_SKIP_DIRS.iter().filter(|name| !name.starts_with('.')) {
|
||||
args.extend([OsString::from("-o"), OsString::from("-name"), OsString::from(skipped)]);
|
||||
}
|
||||
args.extend([")", "-prune", ")", "-o", "-printf", r"%y\0%p\0"].into_iter().map(OsString::from));
|
||||
let Some(output) = run_wsl_find(&located.distro, args) else { return };
|
||||
for (kind, guest_path) in parse_wsl_find_pairs(&output) {
|
||||
if *budget == 0 || index.len() >= SEARCH_INDEX_CAP {
|
||||
break;
|
||||
}
|
||||
*budget -= 1;
|
||||
let name = guest_path.rsplit('/').next().unwrap_or(&guest_path).to_owned();
|
||||
index.push(FileRow {
|
||||
path: PathBuf::from(&guest_path),
|
||||
guest_path: Some(guest_path),
|
||||
name,
|
||||
depth: 0,
|
||||
is_dir: kind == b'd',
|
||||
expanded: false,
|
||||
is_parent: false,
|
||||
ignored: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Snapshot git state for `root`: branch, ±line counts, changed files./// `None` when git is missing or `root` isn't inside a work tree. Runs
|
||||
/// synchronously — callers throttle (see [`SidePanel::sync`]).
|
||||
fn read_git(root: &Path) -> Option<GitInfo> {
|
||||
@@ -1567,27 +1888,63 @@ fn read_git(root: &Path) -> Option<GitInfo> {
|
||||
// the user is already browsing carries none of the write risks the global
|
||||
// opt-in guards against.
|
||||
let safe_directory = format!("safe.directory={}", root.display());
|
||||
let run = |args: &[&str]| -> Option<String> {
|
||||
let location = root.display().to_string();
|
||||
collect_git_info(|args| {
|
||||
let mut cmd = Command::new("git");
|
||||
cmd.args(["-c", &safe_directory, "--no-optional-locks"]).args(args).current_dir(root);
|
||||
// Suppress the console window that `Command` flashes on Windows GUI apps.
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use std::os::windows::process::CommandExt;
|
||||
cmd.creation_flags(0x0800_0000); // CREATE_NO_WINDOW
|
||||
}
|
||||
let out = cmd.output().ok()?;
|
||||
if !out.status.success() {
|
||||
// Leave a trace instead of a silent blank panel: the first stderr
|
||||
// line names the actual refusal (ownership, not-a-repo, …).
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
let reason = stderr.lines().next().unwrap_or("unknown error");
|
||||
log::debug!("git {:?} failed in {}: {reason}", args.first(), root.display());
|
||||
return None;
|
||||
}
|
||||
Some(String::from_utf8_lossy(&out.stdout).into_owned())
|
||||
};
|
||||
run_git(cmd, args, &location)
|
||||
})
|
||||
}
|
||||
|
||||
/// 在 WSL 来宾里读 git 状态:`wsl.exe -d <发行版> -- git -C <来宾目录> …`。
|
||||
///
|
||||
/// # 为什么不走宿主 git
|
||||
///
|
||||
/// 宿主 git 要读 WSL 仓库,只能经 `\\wsl.localhost\…` UNC,而那条路依赖 9P
|
||||
/// 文件重定向——实测在 WSL 2.7.8 + Windows 22631 上完全不可达(见
|
||||
/// [`crate::shell_detect::wsl_unc_cwd`])。来宾里的 git 反而什么都不缺:
|
||||
/// 输出格式与宿主 `git status --porcelain -b` 逐字相同,所以解析与宿主
|
||||
/// 路径完全共用 [`collect_git_info`],没有第二套解析要维护。
|
||||
///
|
||||
/// 也不需要 `safe.directory`:来宾里的仓库属于来宾用户自己,不会触发
|
||||
/// dubious ownership——那本来就是跨 UNC 读别人的文件才有的问题。
|
||||
///
|
||||
/// 代价是每次快照多一次 `wsl.exe` 进程往返(发行版没运行时还会把它拉起
|
||||
/// 来)。快照本来就在后台线程上、且有节流,不进渲染路径。
|
||||
fn read_git_wsl(located: &crate::shell_detect::WslCwd) -> Option<GitInfo> {
|
||||
use std::process::Command;
|
||||
let location = format!("{}:{}", located.distro, located.guest);
|
||||
collect_git_info(|args| {
|
||||
let mut cmd = Command::new("wsl.exe");
|
||||
cmd.args(["-d", &located.distro, "--", "git", "-C", &located.guest, "--no-optional-locks"])
|
||||
.args(args);
|
||||
run_git(cmd, args, &location)
|
||||
})
|
||||
}
|
||||
|
||||
/// 跑一条已配好的 git 命令,取 stdout。`location` 只用于失败日志。
|
||||
fn run_git(mut cmd: std::process::Command, args: &[&str], location: &str) -> Option<String> {
|
||||
// Suppress the console window that `Command` flashes on Windows GUI apps.
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use std::os::windows::process::CommandExt;
|
||||
cmd.creation_flags(0x0800_0000); // CREATE_NO_WINDOW
|
||||
}
|
||||
let out = cmd.output().ok()?;
|
||||
if !out.status.success() {
|
||||
// Leave a trace instead of a silent blank panel: the first stderr
|
||||
// line names the actual refusal (ownership, not-a-repo, …).
|
||||
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||
let reason = stderr.lines().next().unwrap_or("unknown error");
|
||||
log::debug!("git {:?} failed in {location}: {reason}", args.first());
|
||||
return None;
|
||||
}
|
||||
Some(String::from_utf8_lossy(&out.stdout).into_owned())
|
||||
}
|
||||
|
||||
/// 宿主与 WSL 来宾两条运行路径共用的 porcelain 解析。`run` 收一组 git 参数、
|
||||
/// 回 stdout(失败即 `None`)。
|
||||
fn collect_git_info(run: impl Fn(&[&str]) -> Option<String>) -> Option<GitInfo> {
|
||||
// `-b --porcelain` yields `## branch...upstream [ahead N]` + one `XY path`
|
||||
// per change, X = index (staged) status, Y = worktree status.
|
||||
let status = run(&["status", "--porcelain", "-b"])?;
|
||||
@@ -2737,6 +3094,41 @@ mod tests {
|
||||
assert_eq!(panel.root_notice(), Some("所选目录不可用,已跟随当前目录"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_wsl_cwd_replaces_the_stale_host_root() {
|
||||
let host = std::env::temp_dir();
|
||||
let mut panel = SidePanel::new();
|
||||
panel.toggle(PanelView::Files);
|
||||
// 这里只验证状态切换,不让单测启动真实 WSL/文件快照工人。
|
||||
panel.snapshot_running.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
assert!(panel.sync(Some(host)));
|
||||
|
||||
let located = crate::shell_detect::WslCwd {
|
||||
distro: "Debian".to_owned(),
|
||||
guest: "/home/hello".to_owned(),
|
||||
};
|
||||
assert!(panel.sync_at(None, Some(located.clone())));
|
||||
assert_eq!(panel.root(), Some(Path::new("/home/hello")));
|
||||
assert_eq!(panel.file_wsl_root(), Some(&located));
|
||||
assert!(panel.followed_cwd.is_none(), "旧宿主根不能盖住来宾 cwd");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wsl_find_records_preserve_newlines_and_drop_partial_tails() {
|
||||
let parsed = parse_wsl_find_pairs(b"d\0hello\0f\0line\nname.txt\0f\0partial");
|
||||
assert_eq!(parsed, vec![(b'd', "hello".to_owned()), (b'f', "line\nname.txt".to_owned())]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wsl_guest_path_helpers_keep_root_and_parent_boundaries() {
|
||||
assert_eq!(normalize_wsl_guest_path("/home/hello/"), "/home/hello");
|
||||
assert_eq!(wsl_guest_parent("/home/hello"), Some("/home".to_owned()));
|
||||
assert_eq!(wsl_guest_parent("/home"), Some("/".to_owned()));
|
||||
assert_eq!(wsl_guest_parent("/"), None);
|
||||
assert_eq!(wsl_guest_join("/", "home"), "/home");
|
||||
assert_eq!(wsl_guest_join("/home", "hello"), "/home/hello");
|
||||
}
|
||||
|
||||
/// The per-directory cap must be applied *after* ordering. Capping the
|
||||
/// `read_dir` iterator instead samples filesystem order, which in a
|
||||
/// dot-heavy repo root pushes the real source directories past the cap and
|
||||
@@ -2795,6 +3187,7 @@ mod tests {
|
||||
let mut rows = vec![
|
||||
FileRow {
|
||||
path: PathBuf::from("src"),
|
||||
guest_path: None,
|
||||
name: "src".into(),
|
||||
depth: 0,
|
||||
is_dir: true,
|
||||
@@ -2804,6 +3197,7 @@ mod tests {
|
||||
},
|
||||
FileRow {
|
||||
path: PathBuf::from("target"),
|
||||
guest_path: None,
|
||||
name: "target".into(),
|
||||
depth: 0,
|
||||
is_dir: true,
|
||||
|
||||
@@ -100,6 +100,14 @@ pub fn spawn(
|
||||
options.shell = shell;
|
||||
options.working_directory = cwd;
|
||||
options.env.insert(crate::ai_hook::PANE_ENV.to_owned(), pane_id.to_string());
|
||||
// WSL tab 的 cwd 上报(OSC 7):来宾登录 shell 默认不发,目录树与 Git 视图
|
||||
// 因此永远停在"等待终端上报工作目录"。只对 `wsl.exe` 启动生效,见
|
||||
// [`crate::shell_detect::wsl_cwd_report_env`]。
|
||||
if let Some(launch) = &options.shell {
|
||||
let program = launch.program().to_owned();
|
||||
let args = launch.args().to_vec();
|
||||
options.env.extend(crate::shell_detect::wsl_cwd_report_env(&program, &args));
|
||||
}
|
||||
tty::setup_env();
|
||||
let pty = tty::new(&options, window_size, 0)?;
|
||||
let shell_pid = pty.child_pid().unwrap_or(0);
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
//! GPUI 壳的窗口视效:背景模糊 / 窗口透明度 / 壁纸。
|
||||
//!
|
||||
//! 语义全部对齐旧壳:
|
||||
//! - 模糊 = DWM 系统 backdrop(`DWMSBT_TRANSIENTWINDOW`,旧壳
|
||||
//! `display::window::apply_windows_backdrop` 同款调用;< 22621 的系统该
|
||||
//! 调用无效,自动回落纯 alpha 透明)。
|
||||
//! 语义全部对齐旧壳,但**模糊的实现手段不能照抄旧壳**(见
|
||||
//! [`background_appearance`]):一律走 GPUI 自己的
|
||||
//! [`WindowBackgroundAppearance`],由平台层落到各自的原生 API。
|
||||
//! - 透明度 = 壳底色与终端默认背景的 alpha(文字与彩色单元背景保持不
|
||||
//! 透明,对比度不塌——旧壳 `draw_window_backdrop` 裁定)。
|
||||
//! - 壁纸 = 底色之上、单元格之下的一层图(旧壳 `renderer::image` 的
|
||||
@@ -74,7 +73,6 @@ struct CardWallpaper {
|
||||
/// 随后把窗口层效果(背景外观 + DWM backdrop)应用到所有已开窗口。
|
||||
pub fn refresh(cx: &mut App) {
|
||||
let rt = nebula_settings::RuntimeSettings::load();
|
||||
let previous_blur = cx.try_global::<VisualEffects>().map(|effects| effects.blur);
|
||||
|
||||
let wallpaper = rt.background_image.as_ref().and_then(|raw_path| {
|
||||
let path = PathBuf::from(raw_path);
|
||||
@@ -132,7 +130,7 @@ pub fn refresh(cx: &mut App) {
|
||||
});
|
||||
|
||||
cx.set_global(VisualEffects { opacity: rt.opacity, blur: rt.blur, wallpaper });
|
||||
apply_window_effects(previous_blur, cx);
|
||||
apply_window_effects(cx);
|
||||
}
|
||||
|
||||
/// 当前窗口透明度(无全局时视为不透明)。
|
||||
@@ -141,89 +139,130 @@ pub fn window_opacity(cx: &App) -> f32 {
|
||||
cx.try_global::<VisualEffects>().map(|v| v.opacity).unwrap_or(1.0)
|
||||
}
|
||||
|
||||
/// 壳/卡要透出亚克力或铺满壁纸时,不能停在用户滑块的 1.0:
|
||||
/// 旧壳注释写的是「半透明壳浮在壁纸上」,但滑块默认 100%,实心壳会把
|
||||
/// 效果全部盖掉,开关看起来像坏了。模糊开着时顶到 0.88,铺满 chrome
|
||||
/// 再顶到 0.78,文字仍可读。
|
||||
/// 壳/卡透明度严格跟随用户滑块。模糊是独立的窗口背景外观属性,不能反向
|
||||
/// 篡改透明度;否则打开模糊会把用户设置的 100% 偷改成 88%,与旧壳语义
|
||||
/// 不一致。铺满 chrome 的壁纸仍沿用自己的可见性上限。
|
||||
///
|
||||
/// 推论:不透明度 100% 时开模糊在画面上看不出变化——模糊的是窗口**后方**
|
||||
/// 的内容,被完全不透明的像素挡住了。这是两个开关正交的必然结果,不是 bug。
|
||||
pub fn chrome_surface_opacity(cx: &App) -> f32 {
|
||||
let Some(effects) = cx.try_global::<VisualEffects>() else {
|
||||
return 1.0;
|
||||
};
|
||||
let mut alpha = effects.opacity.clamp(0.0, 1.0);
|
||||
if effects.blur {
|
||||
alpha = alpha.min(0.88);
|
||||
}
|
||||
if effects.wallpaper.as_ref().is_some_and(|wp| wp.cover_chrome) {
|
||||
alpha = alpha.min(0.78);
|
||||
}
|
||||
alpha
|
||||
}
|
||||
|
||||
/// 开窗参数用。Windows 上必须一开始就声明最终外观:GPUI 创建后再改
|
||||
/// appearance,合成器往往不补 alpha 通道,后开的模糊/铺满壁纸会失效。
|
||||
/// 开窗参数用。GPUI 通用层在窗口创建时就会把这个值下发到平台层
|
||||
/// (`gpui::Window::new` → `platform_window.set_background_appearance`),
|
||||
/// 所以启动即模糊不需要等 [`refresh`] 补第二次。
|
||||
pub fn initial_background_appearance() -> WindowBackgroundAppearance {
|
||||
background_appearance(nebula_settings::RuntimeSettings::load().blur)
|
||||
}
|
||||
|
||||
/// `blur` 开关 → GPUI 窗口外观。
|
||||
/// 模糊开关 → 窗口背景外观。**唯一落笔点**,启动与热应用共用。
|
||||
///
|
||||
/// **这里是模糊此前完全无效的根因。** GPUI 的 Windows 后端不走 DWM 系统
|
||||
/// backdrop,而是未文档化的 `SetWindowCompositionAttribute` ACCENT_POLICY
|
||||
/// (fork 内 `platform/windows/window.rs::set_background_appearance`):
|
||||
/// # 为什么不能照抄旧壳的 DWM system backdrop
|
||||
///
|
||||
/// | appearance | accent state | 实际效果 |
|
||||
/// | ------------- | --------------------------------- | ---------------- |
|
||||
/// | `Opaque` | ACCENT_DISABLED | 不透明 |
|
||||
/// | `Transparent` | ACCENT_ENABLE_TRANSPARENTGRADIENT | **只透明,不模糊** |
|
||||
/// | `Blurred` | ACCENT_ENABLE_ACRYLICBLURBEHIND | 真正的亚克力模糊 |
|
||||
/// 旧壳(winit)用 `DwmSetWindowAttribute(DWMWA_SYSTEMBACKDROP_TYPE,
|
||||
/// DWMSBT_TRANSIENTWINDOW)` 三行就拿到 Acrylic。同一段代码搬到 GPUI 壳上
|
||||
/// 调用成功、画面零变化——**因为窗口的合成结构不同**:GPUI 只要没禁用
|
||||
/// DirectComposition 就给窗口加 `WS_EX_NOREDIRECTIONBITMAP`
|
||||
/// (`gpui/src/platform/windows/window.rs`),DWM 因此不为它创建重定向
|
||||
/// 表面,system backdrop 材质没有挂载点,`DwmSetWindowAttribute` 返回
|
||||
/// `S_OK` 却什么都不画。旧壳是普通重定向窗口,才没这个问题。
|
||||
///
|
||||
/// accent policy 一旦生效就接管窗口合成,旧壳那三行
|
||||
/// `DWMWA_SYSTEMBACKDROP_TYPE` 会被它压掉。此前本模块无论开关都恒设
|
||||
/// `Transparent`,于是用户拉低透明度只看到「透出了桌面但没有一点模糊」
|
||||
/// —— 正是 TRANSPARENTGRADIENT 的行为,不是设置没生效。
|
||||
/// 上游 Zed 走过同一条弯路:PR #41842 第一版把 `Blurred` 改成
|
||||
/// `DWMSBT_TRANSIENTWINDOW`,第二版又改回 `SetWindowCompositionAttribute`,
|
||||
/// 只把 Mica / MicaAlt 留给 DWM backdrop。所以这里直接用 GPUI 的
|
||||
/// [`WindowBackgroundAppearance::Blurred`]——Windows 平台层落到
|
||||
/// `ACCENT_ENABLE_ACRYLICBLURBEHIND`(且已处理 tint alpha 为 0 时 DWM 跳过
|
||||
/// 模糊的坑),macOS / Linux 各自落到原生毛玻璃。一处开关,三平台生效,
|
||||
/// 不必碰 fork 也不必自写 FFI。
|
||||
///
|
||||
/// # 关闭时为什么是 `Opaque` 而不是 `Transparent`
|
||||
///
|
||||
/// Windows 上窗口透明度完全由我们绘制的像素 alpha 决定(DirectComposition
|
||||
/// swapchain 固定预乘 alpha),`ACCENT_DISABLED` 下 0% 不透明度实测就能透
|
||||
/// 出后方内容。`Transparent` 会额外挂 `ACCENT_ENABLE_TRANSPARENTGRADIENT`
|
||||
/// 的纯色渐变层,对我们没有收益。其他平台维持原有 `Transparent` 语义。
|
||||
fn background_appearance(blur: bool) -> WindowBackgroundAppearance {
|
||||
if blur {
|
||||
WindowBackgroundAppearance::Blurred
|
||||
} else {
|
||||
return WindowBackgroundAppearance::Blurred;
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
WindowBackgroundAppearance::Opaque
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
WindowBackgroundAppearance::Transparent
|
||||
}
|
||||
}
|
||||
|
||||
/// 把窗口层效果应用到所有窗口。
|
||||
///
|
||||
/// 模糊只走 accent policy 一条路。**不再叠加 DWM `DWMWA_SYSTEMBACKDROP_TYPE`**:
|
||||
/// 旧壳靠它拿 acrylic,但 GPUI 的 accent policy 会接管合成,两套同时写只会
|
||||
/// 让 DWM 的内部状态含混——实测表现是模糊打开正常、关闭却关不掉。
|
||||
///
|
||||
/// 关闭时不能在同一个 DWM 合成帧里连续写 `Opaque` 和 `Transparent`:Win32
|
||||
/// 调用虽然都已同步执行,合成器仍可能只呈现最后一个状态,让旧 acrylic 层
|
||||
/// 留到下一次窗口变化。先让 `ACCENT_DISABLED` 真正呈现一帧,再恢复纯透明。
|
||||
fn apply_window_effects(previous_blur: Option<bool>, cx: &mut App) {
|
||||
/// 把窗口层效果应用到所有窗口。透明度完全由绘制像素 alpha 控制,因此模糊
|
||||
/// 开关与 0%..100% 透明度互不绑死、无需跨帧时序补丁。
|
||||
fn apply_window_effects(cx: &mut App) {
|
||||
let blur = cx.try_global::<VisualEffects>().map(|v| v.blur).unwrap_or(false);
|
||||
let appearance = background_appearance(blur);
|
||||
for handle in cx.windows() {
|
||||
let _ = handle.update(cx, |_, window, _| {
|
||||
if previous_blur == Some(true) && !blur {
|
||||
window.set_background_appearance(WindowBackgroundAppearance::Opaque);
|
||||
window.refresh();
|
||||
window.on_next_frame(|window, cx| {
|
||||
// 回调执行前用户可能又打开了模糊;以最新全局值收敛,避免
|
||||
// 旧的关闭回调在快速连点后反向覆盖新设置。
|
||||
let latest_blur = cx
|
||||
.try_global::<VisualEffects>()
|
||||
.map(|effects| effects.blur)
|
||||
.unwrap_or(false);
|
||||
window.set_background_appearance(background_appearance(latest_blur));
|
||||
window.refresh();
|
||||
});
|
||||
} else {
|
||||
window.set_background_appearance(appearance);
|
||||
window.refresh();
|
||||
window.set_background_appearance(appearance);
|
||||
#[cfg(windows)]
|
||||
if !blur {
|
||||
clear_windows_system_backdrop(window);
|
||||
}
|
||||
window.refresh();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// 清掉 Windows system-backdrop 层可能残留的材质。
|
||||
///
|
||||
/// GPUI 的 [`WindowBackgroundAppearance::Opaque`] 已经通过 AccentPolicy 写入
|
||||
/// `ACCENT_DISABLED`,它仍是主关闭入口。旧壳还会用官方
|
||||
/// `DwmSetWindowAttribute(DWMWA_SYSTEMBACKDROP_TYPE, DWMSBT_NONE)` 清 system
|
||||
/// backdrop;实机证明只发 `SWP_FRAMECHANGED` 不会清属性,所以 GPUI 壳在关闭
|
||||
/// 时也补同一条清理。开启仍只走 GPUI `Blurred`,不叠加第二套材质。
|
||||
///
|
||||
/// 微软文档对 `DWMSBT_NONE` 的合同是 “Don't draw any system backdrop”。失败
|
||||
/// 必须留日志,否则视觉残留会再次被误判成“DWM 没重绘”。
|
||||
#[cfg(windows)]
|
||||
fn clear_windows_system_backdrop(window: &Window) {
|
||||
use windows_sys::Win32::Graphics::Dwm::{
|
||||
DWMSBT_NONE, DWMWA_SYSTEMBACKDROP_TYPE, DwmSetWindowAttribute,
|
||||
};
|
||||
use winit::raw_window_handle::{HasWindowHandle, RawWindowHandle};
|
||||
|
||||
let Ok(handle) = HasWindowHandle::window_handle(window) else {
|
||||
return;
|
||||
};
|
||||
let RawWindowHandle::Win32(handle) = handle.as_raw() else {
|
||||
return;
|
||||
};
|
||||
let hwnd = handle.hwnd.get() as *mut core::ffi::c_void;
|
||||
let backdrop: i32 = DWMSBT_NONE;
|
||||
// SAFETY: hwnd 来自当前存活的 GPUI 窗口;值与长度严格匹配 DWM 的
|
||||
// `DWM_SYSTEMBACKDROP_TYPE`(i32)合同。无效句柄由 HRESULT 安全报告。
|
||||
let result = unsafe {
|
||||
DwmSetWindowAttribute(
|
||||
hwnd,
|
||||
DWMWA_SYSTEMBACKDROP_TYPE as u32,
|
||||
&backdrop as *const _ as *const core::ffi::c_void,
|
||||
std::mem::size_of::<i32>() as u32,
|
||||
)
|
||||
};
|
||||
if result < 0 {
|
||||
log::warn!(
|
||||
"DwmSetWindowAttribute(DWMWA_SYSTEMBACKDROP_TYPE=NONE) failed: HRESULT=0x{:08X}",
|
||||
result as u32
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 解码壁纸:RGBA8 → 把图片透明度烘进 alpha → BGRA(gpui 图像帧的
|
||||
/// 通道序,zed 的图片资产装载器同款处理)。
|
||||
fn decode(
|
||||
|
||||
@@ -2027,11 +2027,51 @@ impl NebulaWorkspace {
|
||||
window.defer(cx, move |window, _| window.focus(&focus));
|
||||
}
|
||||
|
||||
/// 聚焦 tab 的**宿主可见** cwd。WSL tab 报的是来宾路径(`/home/x`、
|
||||
/// `/mnt/d/x`),在宿主侧不是有效目录,此时交给
|
||||
/// [`crate::shell_detect::wsl_host_cwd`] 翻译:`/mnt/<盘>` 直接落到宿主盘
|
||||
/// (永远可达),来宾自有路径退一步试 `\\wsl.localhost\…`(9P 不可用的机器
|
||||
/// 上会失败,抽屉按既有语义保持上一个已知目录)。旧壳
|
||||
/// `focused_cwd().or_else(focused_wsl_cwd)` 同合同,判定共用一份。
|
||||
///
|
||||
/// Git 视图不受这里成败影响——它另有来宾直读路径,见 [`Self::active_wsl_cwd`]。
|
||||
fn active_local_cwd(&self, cx: &App) -> Option<std::path::PathBuf> {
|
||||
self.tabs
|
||||
.get(self.active)
|
||||
.and_then(WorkspaceTab::focused_view)
|
||||
.and_then(|view| view.read(cx).local_cwd())
|
||||
let view = self.tabs.get(self.active).and_then(WorkspaceTab::focused_view)?;
|
||||
if let Some(cwd) = view.read(cx).local_cwd() {
|
||||
return Some(cwd);
|
||||
}
|
||||
let located = self.active_wsl_cwd(cx)?;
|
||||
crate::shell_detect::wsl_host_cwd(&located)
|
||||
}
|
||||
|
||||
/// 聚焦 tab 所在的 WSL 发行版 + 来宾目录;不是 WSL、或发行版无从确定
|
||||
/// (裸 `wsl` 启动)时为 `None`。Git 视图拿它在来宾里直接跑 git,不经
|
||||
/// 任何 UNC 映射,所以宿主看不见 WSL 文件系统时依然有效。
|
||||
fn active_wsl_cwd(&self, cx: &App) -> Option<crate::shell_detect::WslCwd> {
|
||||
let view = self.tabs.get(self.active).and_then(WorkspaceTab::focused_view)?;
|
||||
let raw = view.read(cx).cwd.clone();
|
||||
let Some(crate::session::LaunchSession::Shell { program, args, .. }) =
|
||||
self.meta(self.active).launch
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
crate::shell_detect::wsl_cwd(&raw, &program, &args)
|
||||
}
|
||||
|
||||
/// 抽屉这一帧该跟随的位置:宿主 cwd,加上**仅当**宿主侧拿不到等价路径时
|
||||
/// 才交出的 WSL 位置。
|
||||
///
|
||||
/// 为什么要这个门:`/mnt/<盘>/…` 已经被 [`Self::active_local_cwd`] 落回宿
|
||||
/// 主盘,此时宿主 git 与来宾 git 读的是同一个工作树,而宿主 git 快得多
|
||||
/// (来宾路径每次快照都要起一个 `wsl.exe`,`/mnt` 上的 git 本身也慢)。
|
||||
/// 只有 `/home/…` 这类来宾自有路径宿主根本读不到,才值得走来宾。
|
||||
fn side_panel_follow(
|
||||
&self,
|
||||
cx: &App,
|
||||
) -> (Option<std::path::PathBuf>, Option<crate::shell_detect::WslCwd>) {
|
||||
let cwd = self.active_local_cwd(cx);
|
||||
let wsl = if cwd.is_some() { None } else { self.active_wsl_cwd(cx) };
|
||||
(cwd, wsl)
|
||||
}
|
||||
|
||||
fn toggle_side_panel(
|
||||
@@ -2047,8 +2087,8 @@ impl NebulaWorkspace {
|
||||
return;
|
||||
}
|
||||
|
||||
let cwd = self.active_local_cwd(cx);
|
||||
self.side_panel.sync(cwd);
|
||||
let (cwd, wsl) = self.side_panel_follow(cx);
|
||||
self.side_panel.sync_at(cwd, wsl);
|
||||
|
||||
// The shared model builds snapshots on a worker and exposes a cheap,
|
||||
// throttled `sync`. GPUI polls only while the drawer is open; it does
|
||||
@@ -2065,8 +2105,8 @@ impl NebulaWorkspace {
|
||||
workspace.side_panel_polling = false;
|
||||
return false;
|
||||
}
|
||||
let cwd = workspace.active_local_cwd(cx);
|
||||
if workspace.side_panel.sync(cwd) {
|
||||
let (cwd, wsl) = workspace.side_panel_follow(cx);
|
||||
if workspace.side_panel.sync_at(cwd, wsl) {
|
||||
cx.notify();
|
||||
}
|
||||
true
|
||||
@@ -2719,8 +2759,8 @@ impl NebulaWorkspace {
|
||||
}
|
||||
self.file_tree_menu = None;
|
||||
self.side_panel.toggle(view);
|
||||
let cwd = self.active_local_cwd(cx);
|
||||
self.side_panel.sync(cwd);
|
||||
let (cwd, wsl) = self.side_panel_follow(cx);
|
||||
self.side_panel.sync_at(cwd, wsl);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -3328,8 +3368,8 @@ impl NebulaWorkspace {
|
||||
.tooltip(format!("刷新 {vcs_label} 状态"))
|
||||
.on_click(cx.listener(|this, _, _, cx| {
|
||||
this.side_panel.request_refresh();
|
||||
let cwd = this.active_local_cwd(cx);
|
||||
this.side_panel.sync(cwd);
|
||||
let (cwd, wsl) = this.side_panel_follow(cx);
|
||||
this.side_panel.sync_at(cwd, wsl);
|
||||
cx.notify();
|
||||
})),
|
||||
)
|
||||
@@ -3374,6 +3414,16 @@ impl NebulaWorkspace {
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
/// 复制标签页:另开一个**同身份**的 Tab,不克隆活动 PTY 也不克隆分屏树
|
||||
/// (旧壳 `window_context::duplicate_tab` 同合同——副本是新进程/新会话,
|
||||
/// 与 Windows Terminal 一致,也避开 PTY 共享所有权)。
|
||||
///
|
||||
/// 关键是继承 launch 身份。WSL / Git Bash / Nushell 这类 Tab 的
|
||||
/// program+args 记在 [`TabMeta::launch`],而此前这里一律走
|
||||
/// `add_terminal_at`——那是"现取设置里的默认 shell",于是复制一个
|
||||
/// WSL Tab 会得到 pwsh。`Default`(含旧快照没记身份的 `None`)仍按
|
||||
/// "复制这一刻"的默认 shell 解析,与旧壳 `spawn_tab_at` 一致;用户
|
||||
/// 元数据(自定义名字 / 色标)照旧壳一并带到副本上。
|
||||
fn duplicate_tab(&mut self, ix: usize, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(tab) = self.tabs.get(ix) else { return };
|
||||
let Some(view) = tab.focused_view() else { return };
|
||||
@@ -3381,11 +3431,24 @@ impl NebulaWorkspace {
|
||||
let view = view.read(cx);
|
||||
(view.ssh_destination.clone(), view.local_cwd())
|
||||
};
|
||||
let meta = self.meta(ix);
|
||||
if let Some(destination) = ssh {
|
||||
self.add_ssh_terminal(destination, window, cx);
|
||||
} else {
|
||||
self.add_terminal_at(cwd, None, window, cx);
|
||||
let launch = match meta.launch {
|
||||
None | Some(crate::session::LaunchSession::Default) => {
|
||||
Self::configured_local_launch(cx)
|
||||
},
|
||||
Some(launch) => launch,
|
||||
};
|
||||
self.add_terminal_with(launch, cwd, None, window, cx);
|
||||
}
|
||||
// 两个 add_* 入口都已把副本设为 active,名字与色标在这里覆写。
|
||||
if let Some(target) = self.tab_meta.get_mut(self.active) {
|
||||
target.custom_name = meta.custom_name;
|
||||
target.color = meta.color;
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// 进入行内重命名:对照旧壳 `TabRequest::BeginRename`
|
||||
|
||||
@@ -51,6 +51,8 @@ impl NebulaWorkspace {
|
||||
let path = row.path.clone();
|
||||
let open_path = path.clone();
|
||||
let menu_path = path.clone();
|
||||
let guest_path = row.guest_path.clone();
|
||||
let menu_guest_path = guest_path.clone();
|
||||
let is_dir = row.is_dir;
|
||||
let is_parent = row.is_parent;
|
||||
let selected = selected_path.as_ref() == Some(&path);
|
||||
@@ -130,7 +132,7 @@ impl NebulaWorkspace {
|
||||
cx.notify();
|
||||
}
|
||||
}))
|
||||
.when(!is_dir && !is_parent, |item| {
|
||||
.when(!is_dir && !is_parent && guest_path.is_none(), |item| {
|
||||
item.on_double_click(cx.listener(move |this, _, window, cx| {
|
||||
// 与旧壳 chrome 同合同:应用内能读的(图片/可读文本)
|
||||
// 开查看 tab,其余交系统处理器。
|
||||
@@ -147,6 +149,7 @@ impl NebulaWorkspace {
|
||||
cx.stop_propagation();
|
||||
this.open_file_tree_context_menu(
|
||||
menu_path.clone(),
|
||||
menu_guest_path.clone(),
|
||||
is_dir,
|
||||
event.position,
|
||||
window,
|
||||
@@ -199,8 +202,8 @@ impl NebulaWorkspace {
|
||||
.tooltip("刷新目录树")
|
||||
.on_click(cx.listener(|this, _, _, cx| {
|
||||
this.side_panel.request_refresh();
|
||||
let cwd = this.active_local_cwd(cx);
|
||||
this.side_panel.sync(cwd);
|
||||
let (cwd, wsl) = this.side_panel_follow(cx);
|
||||
this.side_panel.sync_at(cwd, wsl);
|
||||
cx.notify();
|
||||
})),
|
||||
)
|
||||
@@ -231,6 +234,7 @@ impl NebulaWorkspace {
|
||||
fn open_file_tree_context_menu(
|
||||
&mut self,
|
||||
path: PathBuf,
|
||||
guest_path: Option<String>,
|
||||
is_dir: bool,
|
||||
position: Point<Pixels>,
|
||||
window: &mut Window,
|
||||
@@ -239,7 +243,14 @@ impl NebulaWorkspace {
|
||||
self.side_panel.selected = Some(path.clone());
|
||||
let workspace = cx.entity().downgrade();
|
||||
let menu = PopupMenu::build(window, cx, move |menu, window, _cx| {
|
||||
file_tree_popup_menu(menu.external_link_icon(false), workspace, path, is_dir, window)
|
||||
file_tree_popup_menu(
|
||||
menu.external_link_icon(false),
|
||||
workspace,
|
||||
path,
|
||||
guest_path,
|
||||
is_dir,
|
||||
window,
|
||||
)
|
||||
});
|
||||
menu.focus_handle(cx).focus(window);
|
||||
let subscription = cx.subscribe_in(&menu, window, |this, _, _: &DismissEvent, _, cx| {
|
||||
@@ -325,9 +336,17 @@ fn file_tree_popup_menu(
|
||||
menu: PopupMenu,
|
||||
workspace: gpui::WeakEntity<NebulaWorkspace>,
|
||||
path: PathBuf,
|
||||
guest_path: Option<String>,
|
||||
is_dir: bool,
|
||||
_window: &mut Window,
|
||||
) -> PopupMenu {
|
||||
if let Some(guest_path) = guest_path {
|
||||
return menu.item(PopupMenuItem::new("复制 Linux 路径").icon(IconName::Copy).on_click(
|
||||
move |_, _, cx| {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(guest_path.clone()));
|
||||
},
|
||||
));
|
||||
}
|
||||
let first = if is_dir {
|
||||
let open_here = workspace.clone();
|
||||
let dir = path.clone();
|
||||
|
||||
@@ -426,6 +426,145 @@ pub fn shell_short_tag(name_or_id: &str) -> String {
|
||||
lower.split_whitespace().next().unwrap_or("").chars().take(10).collect()
|
||||
}
|
||||
|
||||
/// 一次 WSL 启动用的发行版名:只认 `-d` / `--distribution` 显式给出的那个。
|
||||
///
|
||||
/// 裸 `wsl` 启动跑的是系统默认发行版,名字我们无从得知——宁可返回 `None`
|
||||
/// 让调用方保持现状,也不猜一个可能错的发行版去拼路径。非 WSL 程序同样
|
||||
/// 返回 `None`(按 `file_stem` 判断,`wsl.exe` / 全路径都认)。
|
||||
///
|
||||
/// 旧壳 `window_context::focused_wsl_cwd` 的判定原样固化在这里,两壳共用。
|
||||
pub fn wsl_launch_distro<'a>(program: &str, args: &'a [String]) -> Option<&'a str> {
|
||||
let stem = std::path::Path::new(program)
|
||||
.file_stem()
|
||||
.and_then(|stem| stem.to_str())
|
||||
.unwrap_or_default()
|
||||
.to_ascii_lowercase();
|
||||
if stem != "wsl" {
|
||||
return None;
|
||||
}
|
||||
let index = args.iter().position(|arg| arg == "-d" || arg == "--distribution")?;
|
||||
args.get(index + 1).map(String::as_str).filter(|distro| !distro.is_empty())
|
||||
}
|
||||
|
||||
/// 终端报的 cwd 是来宾侧的绝对路径吗(`/home/x`)。宿主路径(`D:\…`)不需要
|
||||
/// 映射,交给普通路径分支即可。
|
||||
pub fn wsl_guest_cwd(cwd: &str) -> Option<&str> {
|
||||
let trimmed = cwd.trim();
|
||||
trimmed.starts_with('/').then_some(trimmed)
|
||||
}
|
||||
|
||||
/// 来宾绝对路径 → `\\wsl.localhost\<发行版>\…` 形式的宿主 UNC 路径(纯拼接,
|
||||
/// 不碰文件系统,便于单测)。
|
||||
///
|
||||
/// 用 `\\wsl.localhost\` 而非旧壳的 `\\wsl$\`:后者是 WSL 早期形式,新版
|
||||
/// Windows 只保留 `wsl.localhost` 作为正式名(`wsl_distro_names` 的注释里
|
||||
/// 早就写明目录选择器钉的是 `\\wsl.localhost\<名>`)。两种形式在不支持
|
||||
/// 9P 重定向的机器上都不可达,见 [`wsl_unc_cwd`]。
|
||||
pub fn wsl_unc_path(distro: &str, guest_path: &str) -> std::path::PathBuf {
|
||||
std::path::PathBuf::from(format!(
|
||||
"\\\\wsl.localhost\\{distro}{}",
|
||||
guest_path.replace('/', "\\")
|
||||
))
|
||||
}
|
||||
|
||||
/// 一个 WSL 终端的位置:发行版名 + 来宾绝对路径。
|
||||
///
|
||||
/// 即使宿主看不见来宾文件系统(9P 重定向不可用,见 [`wsl_unc_cwd`])这个位置
|
||||
/// 依然成立——拿着它可以用 `wsl.exe -d <发行版> -- <命令>` 直接在来宾里干活。
|
||||
/// Git 视图识别 WSL 仓库靠的就是它,不依赖任何 UNC 映射。
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct WslCwd {
|
||||
pub distro: String,
|
||||
pub guest: String,
|
||||
}
|
||||
|
||||
/// 认出「聚焦终端属于某个 WSL 发行版、且正停在来宾的某个目录」。
|
||||
///
|
||||
/// 刻意**不**验证目录存在:宿主无法可靠地看到来宾文件系统,能验证的地方只有
|
||||
/// 来宾里——而那正是调用方紧接着要跑的那条命令本身。
|
||||
pub fn wsl_cwd(cwd: &str, program: &str, args: &[String]) -> Option<WslCwd> {
|
||||
let guest = wsl_guest_cwd(cwd)?;
|
||||
let distro = wsl_launch_distro(program, args)?;
|
||||
Some(WslCwd { distro: distro.to_owned(), guest: guest.to_owned() })
|
||||
}
|
||||
|
||||
/// [`WslCwd`] → 宿主可见的 UNC 路径,**且确认它真的可达**。两壳的抽屉都经这
|
||||
/// 一个入口,`is_dir` 把关只写一遍。
|
||||
///
|
||||
/// # 为什么必须 `is_dir()` 把关
|
||||
///
|
||||
/// `\\wsl.localhost\…` 依赖 WSL 的 9P 文件重定向(P9NP 网络提供程序)。
|
||||
/// 发行版没启动、或这台机器的 9P 重定向根本不工作时,UNC 路径不可达——
|
||||
/// 2026-08-20 实测:WSL 2.7.8 + Windows 22631 上 `\\wsl$\` 与
|
||||
/// `\\wsl.localhost\` 两种形式经 cmd / PowerShell / .NET 三条路都是"系统
|
||||
/// 找不到指定的路径",而发行版 `wsl -l -v` 明确 Running、P9NP 也在
|
||||
/// `ProviderOrder` 里、进程未提升(排除 UAC 会话隔离)。这种环境下本函数
|
||||
/// 一律返回 `None`,调用方必须能接受「拿不到宿主路径」并另寻出路(拿
|
||||
/// [`WslCwd`] 在来宾里直接跑命令),不能假定映射一定成功。
|
||||
pub fn wsl_unc_cwd(located: &WslCwd) -> Option<std::path::PathBuf> {
|
||||
let path = wsl_unc_path(&located.distro, &located.guest);
|
||||
path.is_dir().then_some(path)
|
||||
}
|
||||
|
||||
/// 让 WSL 来宾在每个提示符前上报 cwd(OSC 7)所需的环境变量;非 WSL 启动返回空。
|
||||
///
|
||||
/// # 为什么只能走环境变量
|
||||
///
|
||||
/// `wsl.exe` 必须原样启动来宾的登录 shell(`chsh` 语义,见
|
||||
/// [`ShellIntegration::WslDefault`]——追加 `--exec bash` 是 1.1.0 的实际回归),
|
||||
/// 所以既不能改启动参数、也没有宿主侧的 rc 文件可注入。可行的只剩环境变量:
|
||||
/// bash 每画一次提示符都会执行 `$PROMPT_COMMAND`,而 `WSLENV` 会把点名的宿主
|
||||
/// 变量原样送进来宾。
|
||||
///
|
||||
/// 少了这条,WSL tab 永远停在"等待终端上报工作目录"——目录树与 Git 视图都是
|
||||
/// 靠 shell 上报的 cwd 驱动的(`TermEvent::CwdReport`),而 Debian/Ubuntu 的
|
||||
/// 默认 `.bashrc` 只设 OSC 0 窗口标题,不发 OSC 7(实测 `PROMPT_COMMAND` 为空)。
|
||||
///
|
||||
/// 尽力而为、失败无害:来宾默认 shell 是 zsh/fish(不认 `PROMPT_COMMAND`),
|
||||
/// 或用户自己在 rc 里直接赋值把它覆盖掉,都只是回到"不上报"的现状,不会影响
|
||||
/// shell 正常启动。
|
||||
pub fn wsl_cwd_report_env(program: &str, args: &[String]) -> Vec<(String, String)> {
|
||||
if wsl_launch_distro(program, args).is_none() {
|
||||
return Vec::new();
|
||||
}
|
||||
// OSC 7 = `ESC ] 7 ; file://<host><path> BEL`。host 会被解析端丢掉(取第一个
|
||||
// `/` 之后的部分),给个兜底值就够。用 `$PWD` 而不是 `$(pwd)`:每个提示符
|
||||
// 都要跑一次,不能带 fork 开销。
|
||||
const REPORT: &str = r#"printf '\033]7;file://%s%s\007' "${HOSTNAME:-wsl}" "$PWD""#;
|
||||
// 宿主侧可能已经有 WSLENV(别的工具设的),必须追加而不是覆盖。
|
||||
let mut wslenv = std::env::var("WSLENV").unwrap_or_default();
|
||||
if !wslenv.is_empty() && !wslenv.split(':').any(|entry| entry == "PROMPT_COMMAND") {
|
||||
wslenv.push(':');
|
||||
}
|
||||
if !wslenv.split(':').any(|entry| entry == "PROMPT_COMMAND") {
|
||||
wslenv.push_str("PROMPT_COMMAND");
|
||||
}
|
||||
vec![("PROMPT_COMMAND".to_owned(), REPORT.to_owned()), ("WSLENV".to_owned(), wslenv)]
|
||||
}
|
||||
|
||||
/// [`WslCwd`] → 宿主可见且**已确认可达**的路径,按两条路依次尝试。
|
||||
///
|
||||
/// 1. `/mnt/<盘>/…`(automount——WSL 里访问宿主盘的常规形态,也是绝大多数人
|
||||
/// 实际工作的位置)本来就是宿主路径 `<盘>:\…`。这条不经任何网络重定向,
|
||||
/// 永远可达,所以目录树在 `/mnt/d/...` 下能真正跟随 WSL 终端。
|
||||
/// 2. 来宾自有路径(`/home/…`)只能走 `\\wsl.localhost\…`,见 [`wsl_unc_cwd`]
|
||||
/// ——9P 重定向不可用的机器上会失败并返回 `None`,调用方按既有语义保持
|
||||
/// 上一个已知目录。
|
||||
///
|
||||
/// 两条都失败也不影响 Git 视图:那条路走 [`WslCwd`] 在来宾里直接跑 git。
|
||||
pub fn wsl_host_cwd(located: &WslCwd) -> Option<std::path::PathBuf> {
|
||||
// `/mnt/d/x` → `file:///mnt/d/x` → `D:\x`,复用 OSC 8 链接那套 automount /
|
||||
// MSYS / cygwin 路径翻译(含"盘符真的存在"的门控),不另写一份。
|
||||
#[cfg(windows)]
|
||||
if let Some(path) =
|
||||
crate::file_uri::file_uri_to_local_path(&format!("file://{}", located.guest))
|
||||
&& path.is_dir()
|
||||
{
|
||||
return Some(path);
|
||||
}
|
||||
wsl_unc_cwd(located)
|
||||
}
|
||||
|
||||
/// 已注册 WSL 发行版的名字(注册表 `Lxss` 各子键的 `DistributionName`),
|
||||
/// 字母序。目录选择器用它把 `\\wsl.localhost\<名>` 钉进侧栏——系统的
|
||||
/// 「Linux」导航节点不是每台 Win11 都有(issue #12 的现场就没有),钉进
|
||||
@@ -506,6 +645,54 @@ fn find_wsl_distros() -> Vec<DetectedShell> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
/// WSL 位置识别:发行版只认显式 `-d` / `--distribution`。裸 `wsl` 用的是
|
||||
/// 系统默认发行版,名字无从得知——必须放弃而不是猜,否则会拼出一条指向
|
||||
/// 错误发行版的路径。
|
||||
#[test]
|
||||
fn wsl_location_only_trusts_an_explicit_distribution() {
|
||||
let owned =
|
||||
|args: &[&str]| -> Vec<String> { args.iter().map(|arg| (*arg).to_owned()).collect() };
|
||||
|
||||
for flag in ["-d", "--distribution"] {
|
||||
let args = owned(&[flag, "Debian"]);
|
||||
assert_eq!(super::wsl_launch_distro("wsl.exe", &args), Some("Debian"));
|
||||
let located = super::wsl_cwd("/home/hello", r"C:\Windows\System32\wsl.exe", &args)
|
||||
.expect("WSL 位置");
|
||||
assert_eq!(located.distro, "Debian");
|
||||
assert_eq!(located.guest, "/home/hello");
|
||||
}
|
||||
|
||||
// 裸 `wsl`、缺发行版名、以及非 WSL 程序都不识别。
|
||||
assert_eq!(super::wsl_launch_distro("wsl.exe", &[]), None);
|
||||
assert_eq!(super::wsl_launch_distro("wsl.exe", &owned(&["-d"])), None);
|
||||
assert_eq!(super::wsl_launch_distro("wsl.exe", &owned(&["-d", ""])), None);
|
||||
assert_eq!(super::wsl_launch_distro("pwsh.exe", &owned(&["-d", "Debian"])), None);
|
||||
}
|
||||
|
||||
/// 只有来宾绝对路径需要映射;宿主路径与空 cwd 走普通分支。
|
||||
#[test]
|
||||
fn wsl_guest_cwd_accepts_only_absolute_guest_paths() {
|
||||
assert_eq!(super::wsl_guest_cwd(" /home/hello "), Some("/home/hello"));
|
||||
assert_eq!(super::wsl_guest_cwd("/"), Some("/"));
|
||||
assert_eq!(super::wsl_guest_cwd(r"D:\temp_build"), None);
|
||||
assert_eq!(super::wsl_guest_cwd(""), None);
|
||||
assert_eq!(super::wsl_guest_cwd("home/hello"), None);
|
||||
}
|
||||
|
||||
/// UNC 形式必须是 `\\wsl.localhost\<发行版>\…`:旧壳原来拼的 `\\wsl$\` 是
|
||||
/// WSL 早期形式,新版 Windows 只保证 `wsl.localhost` 这个名字。
|
||||
#[test]
|
||||
fn wsl_unc_path_uses_the_localhost_form_with_backslashes() {
|
||||
assert_eq!(
|
||||
super::wsl_unc_path("Debian", "/home/hello/src"),
|
||||
std::path::PathBuf::from(r"\\wsl.localhost\Debian\home\hello\src")
|
||||
);
|
||||
assert_eq!(
|
||||
super::wsl_unc_path("Ubuntu-24.04", "/"),
|
||||
std::path::PathBuf::from(r"\\wsl.localhost\Ubuntu-24.04\")
|
||||
);
|
||||
}
|
||||
|
||||
fn detected(id: &str, program: &str, args: &[&str]) -> super::DetectedShell {
|
||||
super::DetectedShell {
|
||||
name: id.to_owned(),
|
||||
|
||||
@@ -2065,41 +2065,26 @@ impl WindowContext {
|
||||
session::valid_dir(&cwd)
|
||||
}
|
||||
|
||||
/// The focused pane's cwd mapped through `\\wsl$` when the pane belongs
|
||||
/// to a WSL tab reporting a Linux path — so the directory tree can follow
|
||||
/// a WSL shell. Only the drawer uses this: spawning terminals in a UNC
|
||||
/// directory has its own semantics and is deliberately not affected.
|
||||
/// The focused pane's cwd mapped through `\\wsl.localhost` when the pane
|
||||
/// belongs to a WSL tab reporting a Linux path — so the directory tree can
|
||||
/// follow a WSL shell. Only the drawer uses this: spawning terminals in a
|
||||
/// UNC directory has its own semantics and is deliberately not affected.
|
||||
///
|
||||
/// 判定与拼接走 [`crate::shell_detect`] 的公共入口,与 GPUI 壳同一份
|
||||
/// 规则。注意它在 9P 重定向不可用的机器上一律返回 `None`(见
|
||||
/// [`crate::shell_detect::wsl_unc_cwd`]),届时目录树保持上一个已知根,
|
||||
/// 不会跳到坏路径。GPUI 壳另有「在来宾里直接跑 git」的 Git 视图路径;
|
||||
/// 旧壳刻意不接那条链——新功能只在主壳(GPUI)上长。
|
||||
fn focused_wsl_cwd(&self) -> Option<std::path::PathBuf> {
|
||||
let raw = self.pane(self.focused_pane_id()).map(|p| p.nebula_state.cwd.clone())?;
|
||||
let raw = raw.trim();
|
||||
if !raw.starts_with('/') {
|
||||
return None;
|
||||
}
|
||||
let tab = self.tabs.iter().find(|tab| {
|
||||
let mut ids = Vec::new();
|
||||
tab.layout.leaves(&mut ids);
|
||||
ids.contains(&self.focused_pane_id())
|
||||
})?;
|
||||
let TabLaunch::Shell { shell, .. } = &tab.launch else { return None };
|
||||
let program = std::path::Path::new(shell.program())
|
||||
.file_stem()
|
||||
.and_then(|stem| stem.to_str())
|
||||
.unwrap_or_default()
|
||||
.to_ascii_lowercase();
|
||||
if program != "wsl" {
|
||||
return None;
|
||||
}
|
||||
// `wsl -d <distro>` / `--distribution <distro>`; a bare `wsl` launch
|
||||
// uses the default distro whose name we cannot know — skip mapping
|
||||
// and let the tree keep its last known root.
|
||||
let args = shell.args();
|
||||
let distro = args
|
||||
.iter()
|
||||
.position(|arg| arg == "-d" || arg == "--distribution")
|
||||
.and_then(|index| args.get(index + 1))?;
|
||||
let unc = format!("\\\\wsl$\\{distro}{}", raw.replace('/', "\\"));
|
||||
let path = std::path::PathBuf::from(unc);
|
||||
path.is_dir().then_some(path)
|
||||
let located = crate::shell_detect::wsl_cwd(&raw, shell.program(), shell.args())?;
|
||||
crate::shell_detect::wsl_unc_cwd(&located)
|
||||
}
|
||||
|
||||
/// Name of the first busy program under any of `pane_ids`, for the close
|
||||
|
||||
Reference in new issue
Block a user