mirror of
https://github.com/zed-industries/zed.git
synced 2024-09-18 18:08:07 +03:00
linux: Show warning if file picker portal is missing (#14401)
This PR adds a warning when the file chooser couldn't be opened on Linux It's quite confusing when trying to open a file and apparently nothing happens: fixes https://github.com/zed-industries/zed/issues/11089, https://github.com/zed-industries/zed/issues/14328, https://github.com/zed-industries/zed/issues/13753#issuecomment-2225812703, https://github.com/zed-industries/zed/issues/13766, https://github.com/zed-industries/zed/issues/14384, https://github.com/zed-industries/zed/issues/14353, https://github.com/zed-industries/zed/issues/9209 ![image](https://github.com/user-attachments/assets/5acabdaa-7a9d-4225-9480-e371d20387c3) Release Notes: - N/A
This commit is contained in:
parent
5d860e2286
commit
f3ddd18201
5
Cargo.lock
generated
5
Cargo.lock
generated
@ -341,8 +341,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ashpd"
|
name = "ashpd"
|
||||||
version = "0.9.0"
|
version = "0.9.1"
|
||||||
source = "git+https://github.com/bilelmoussaoui/ashpd?rev=29f2e1a#29f2e1a6f4b0911f504658f5f4630c02e01b13f2"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bfe7e0dd0ac5a401dc116ed9f9119cf9decc625600474cb41f0fc0a0050abc9a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-fs 2.1.1",
|
"async-fs 2.1.1",
|
||||||
"async-net 2.0.0",
|
"async-net 2.0.0",
|
||||||
|
@ -274,7 +274,7 @@ zed_actions = { path = "crates/zed_actions" }
|
|||||||
alacritty_terminal = "0.23"
|
alacritty_terminal = "0.23"
|
||||||
any_vec = "0.13"
|
any_vec = "0.13"
|
||||||
anyhow = "1.0.57"
|
anyhow = "1.0.57"
|
||||||
ashpd = { git = "https://github.com/bilelmoussaoui/ashpd", rev = "29f2e1a" }
|
ashpd = "0.9.1"
|
||||||
async-compression = { version = "0.4", features = ["gzip", "futures-io"] }
|
async-compression = { version = "0.4", features = ["gzip", "futures-io"] }
|
||||||
async-dispatcher = { version = "0.1" }
|
async-dispatcher = { version = "0.1" }
|
||||||
async-fs = "1.6"
|
async-fs = "1.6"
|
||||||
|
@ -12,7 +12,7 @@ use editor::{Editor, EditorElement, EditorStyle};
|
|||||||
use extension::{ExtensionManifest, ExtensionOperation, ExtensionStore};
|
use extension::{ExtensionManifest, ExtensionOperation, ExtensionStore};
|
||||||
use fuzzy::{match_strings, StringMatchCandidate};
|
use fuzzy::{match_strings, StringMatchCandidate};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, uniform_list, AnyElement, AppContext, EventEmitter, FocusableView, FontStyle,
|
actions, uniform_list, AnyElement, AppContext, EventEmitter, Flatten, FocusableView, FontStyle,
|
||||||
InteractiveElement, KeyContext, ParentElement, Render, Styled, Task, TextStyle,
|
InteractiveElement, KeyContext, ParentElement, Render, Styled, Task, TextStyle,
|
||||||
UniformListScrollHandle, View, ViewContext, VisualContext, WeakView, WhiteSpace, WindowContext,
|
UniformListScrollHandle, View, ViewContext, VisualContext, WeakView, WhiteSpace, WindowContext,
|
||||||
};
|
};
|
||||||
@ -24,7 +24,6 @@ use std::time::Duration;
|
|||||||
use std::{ops::Range, sync::Arc};
|
use std::{ops::Range, sync::Arc};
|
||||||
use theme::ThemeSettings;
|
use theme::ThemeSettings;
|
||||||
use ui::{prelude::*, ContextMenu, PopoverMenu, ToggleButton, Tooltip};
|
use ui::{prelude::*, ContextMenu, PopoverMenu, ToggleButton, Tooltip};
|
||||||
use util::ResultExt as _;
|
|
||||||
use workspace::item::TabContentParams;
|
use workspace::item::TabContentParams;
|
||||||
use workspace::{
|
use workspace::{
|
||||||
item::{Item, ItemEvent},
|
item::{Item, ItemEvent},
|
||||||
@ -58,9 +57,23 @@ pub fn init(cx: &mut AppContext) {
|
|||||||
multiple: false,
|
multiple: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let workspace_handle = cx.view().downgrade();
|
||||||
cx.deref_mut()
|
cx.deref_mut()
|
||||||
.spawn(|mut cx| async move {
|
.spawn(|mut cx| async move {
|
||||||
let extension_path = prompt.await.log_err()??.pop()?;
|
let extension_path =
|
||||||
|
match Flatten::flatten(prompt.await.map_err(|e| e.into())) {
|
||||||
|
Ok(Some(mut paths)) => paths.pop()?,
|
||||||
|
Ok(None) => return None,
|
||||||
|
Err(err) => {
|
||||||
|
workspace_handle
|
||||||
|
.update(&mut cx, |workspace, cx| {
|
||||||
|
workspace.show_portal_error(err.to_string(), cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
store
|
store
|
||||||
.update(&mut cx, |store, cx| {
|
.update(&mut cx, |store, cx| {
|
||||||
store
|
store
|
||||||
|
@ -612,10 +612,11 @@ impl AppContext {
|
|||||||
/// Displays a platform modal for selecting paths.
|
/// Displays a platform modal for selecting paths.
|
||||||
/// When one or more paths are selected, they'll be relayed asynchronously via the returned oneshot channel.
|
/// When one or more paths are selected, they'll be relayed asynchronously via the returned oneshot channel.
|
||||||
/// If cancelled, a `None` will be relayed instead.
|
/// If cancelled, a `None` will be relayed instead.
|
||||||
|
/// May return an error on Linux if the file picker couldn't be opened.
|
||||||
pub fn prompt_for_paths(
|
pub fn prompt_for_paths(
|
||||||
&self,
|
&self,
|
||||||
options: PathPromptOptions,
|
options: PathPromptOptions,
|
||||||
) -> oneshot::Receiver<Option<Vec<PathBuf>>> {
|
) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>> {
|
||||||
self.platform.prompt_for_paths(options)
|
self.platform.prompt_for_paths(options)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -623,7 +624,11 @@ impl AppContext {
|
|||||||
/// The provided directory will be used to set the initial location.
|
/// The provided directory will be used to set the initial location.
|
||||||
/// When a path is selected, it is relayed asynchronously via the returned oneshot channel.
|
/// When a path is selected, it is relayed asynchronously via the returned oneshot channel.
|
||||||
/// If cancelled, a `None` will be relayed instead.
|
/// If cancelled, a `None` will be relayed instead.
|
||||||
pub fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Option<PathBuf>> {
|
/// May return an error on Linux if the file picker couldn't be opened.
|
||||||
|
pub fn prompt_for_new_path(
|
||||||
|
&self,
|
||||||
|
directory: &Path,
|
||||||
|
) -> oneshot::Receiver<Result<Option<PathBuf>>> {
|
||||||
self.platform.prompt_for_new_path(directory)
|
self.platform.prompt_for_new_path(directory)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -137,8 +137,8 @@ pub(crate) trait Platform: 'static {
|
|||||||
fn prompt_for_paths(
|
fn prompt_for_paths(
|
||||||
&self,
|
&self,
|
||||||
options: PathPromptOptions,
|
options: PathPromptOptions,
|
||||||
) -> oneshot::Receiver<Option<Vec<PathBuf>>>;
|
) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>>;
|
||||||
fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Option<PathBuf>>;
|
fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Result<Option<PathBuf>>>;
|
||||||
fn reveal_path(&self, path: &Path);
|
fn reveal_path(&self, path: &Path);
|
||||||
|
|
||||||
fn on_quit(&self, callback: Box<dyn FnMut()>);
|
fn on_quit(&self, callback: Box<dyn FnMut()>);
|
||||||
|
@ -21,6 +21,7 @@ use std::{
|
|||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use ashpd::desktop::file_chooser::{OpenFileRequest, SaveFileRequest};
|
use ashpd::desktop::file_chooser::{OpenFileRequest, SaveFileRequest};
|
||||||
use ashpd::desktop::open_uri::{OpenDirectoryRequest, OpenFileRequest as OpenUriRequest};
|
use ashpd::desktop::open_uri::{OpenDirectoryRequest, OpenFileRequest as OpenUriRequest};
|
||||||
|
use ashpd::desktop::ResponseError;
|
||||||
use ashpd::{url, ActivationToken};
|
use ashpd::{url, ActivationToken};
|
||||||
use async_task::Runnable;
|
use async_task::Runnable;
|
||||||
use calloop::channel::Channel;
|
use calloop::channel::Channel;
|
||||||
@ -54,6 +55,9 @@ pub(crate) const DOUBLE_CLICK_INTERVAL: Duration = Duration::from_millis(400);
|
|||||||
pub(crate) const DOUBLE_CLICK_DISTANCE: Pixels = px(5.0);
|
pub(crate) const DOUBLE_CLICK_DISTANCE: Pixels = px(5.0);
|
||||||
pub(crate) const KEYRING_LABEL: &str = "zed-github-account";
|
pub(crate) const KEYRING_LABEL: &str = "zed-github-account";
|
||||||
|
|
||||||
|
const FILE_PICKER_PORTAL_MISSING: &str =
|
||||||
|
"Couldn't open file picker due to missing xdg-desktop-portal implementation.";
|
||||||
|
|
||||||
pub trait LinuxClient {
|
pub trait LinuxClient {
|
||||||
fn compositor_name(&self) -> &'static str;
|
fn compositor_name(&self) -> &'static str;
|
||||||
fn with_common<R>(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R;
|
fn with_common<R>(&self, f: impl FnOnce(&mut LinuxCommon) -> R) -> R;
|
||||||
@ -256,7 +260,7 @@ impl<P: LinuxClient + 'static> Platform for P {
|
|||||||
fn prompt_for_paths(
|
fn prompt_for_paths(
|
||||||
&self,
|
&self,
|
||||||
options: PathPromptOptions,
|
options: PathPromptOptions,
|
||||||
) -> oneshot::Receiver<Option<Vec<PathBuf>>> {
|
) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>> {
|
||||||
let (done_tx, done_rx) = oneshot::channel();
|
let (done_tx, done_rx) = oneshot::channel();
|
||||||
self.foreground_executor()
|
self.foreground_executor()
|
||||||
.spawn(async move {
|
.spawn(async move {
|
||||||
@ -274,7 +278,7 @@ impl<P: LinuxClient + 'static> Platform for P {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let result = OpenFileRequest::default()
|
let request = match OpenFileRequest::default()
|
||||||
.modal(true)
|
.modal(true)
|
||||||
.title(title)
|
.title(title)
|
||||||
.accept_label("Select")
|
.accept_label("Select")
|
||||||
@ -282,49 +286,68 @@ impl<P: LinuxClient + 'static> Platform for P {
|
|||||||
.directory(options.directories)
|
.directory(options.directories)
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.ok()
|
{
|
||||||
.and_then(|request| request.response().ok())
|
Ok(request) => request,
|
||||||
.and_then(|response| {
|
Err(err) => {
|
||||||
|
let result = match err {
|
||||||
|
ashpd::Error::PortalNotFound(_) => anyhow!(FILE_PICKER_PORTAL_MISSING),
|
||||||
|
err => err.into(),
|
||||||
|
};
|
||||||
|
done_tx.send(Err(result));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = match request.response() {
|
||||||
|
Ok(response) => Ok(Some(
|
||||||
response
|
response
|
||||||
.uris()
|
.uris()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|uri| uri.to_file_path().ok())
|
.filter_map(|uri| uri.to_file_path().ok())
|
||||||
.collect()
|
.collect::<Vec<_>>(),
|
||||||
});
|
)),
|
||||||
|
Err(ashpd::Error::Response(ResponseError::Cancelled)) => Ok(None),
|
||||||
|
Err(e) => Err(e.into()),
|
||||||
|
};
|
||||||
done_tx.send(result);
|
done_tx.send(result);
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
done_rx
|
done_rx
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Option<PathBuf>> {
|
fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Result<Option<PathBuf>>> {
|
||||||
let (done_tx, done_rx) = oneshot::channel();
|
let (done_tx, done_rx) = oneshot::channel();
|
||||||
let directory = directory.to_owned();
|
let directory = directory.to_owned();
|
||||||
self.foreground_executor()
|
self.foreground_executor()
|
||||||
.spawn(async move {
|
.spawn(async move {
|
||||||
let request = SaveFileRequest::default()
|
let request = match SaveFileRequest::default()
|
||||||
.modal(true)
|
.modal(true)
|
||||||
.title("Select new path")
|
.title("Select new path")
|
||||||
.accept_label("Accept")
|
.accept_label("Accept")
|
||||||
.current_folder(directory);
|
.current_folder(directory)
|
||||||
|
.expect("pathbuf should not be nul terminated")
|
||||||
let result = if let Ok(request) = request {
|
|
||||||
request
|
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.ok()
|
{
|
||||||
.and_then(|request| request.response().ok())
|
Ok(request) => request,
|
||||||
.and_then(|response| {
|
Err(err) => {
|
||||||
response
|
let result = match err {
|
||||||
.uris()
|
ashpd::Error::PortalNotFound(_) => anyhow!(FILE_PICKER_PORTAL_MISSING),
|
||||||
.first()
|
err => err.into(),
|
||||||
.and_then(|uri| uri.to_file_path().ok())
|
};
|
||||||
})
|
done_tx.send(Err(result));
|
||||||
} else {
|
return;
|
||||||
None
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let result = match request.response() {
|
||||||
|
Ok(response) => Ok(response
|
||||||
|
.uris()
|
||||||
|
.first()
|
||||||
|
.and_then(|uri| uri.to_file_path().ok())),
|
||||||
|
Err(ashpd::Error::Response(ResponseError::Cancelled)) => Ok(None),
|
||||||
|
Err(e) => Err(e.into()),
|
||||||
|
};
|
||||||
done_tx.send(result);
|
done_tx.send(result);
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
@ -602,7 +602,7 @@ impl Platform for MacPlatform {
|
|||||||
fn prompt_for_paths(
|
fn prompt_for_paths(
|
||||||
&self,
|
&self,
|
||||||
options: PathPromptOptions,
|
options: PathPromptOptions,
|
||||||
) -> oneshot::Receiver<Option<Vec<PathBuf>>> {
|
) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>> {
|
||||||
let (done_tx, done_rx) = oneshot::channel();
|
let (done_tx, done_rx) = oneshot::channel();
|
||||||
self.foreground_executor()
|
self.foreground_executor()
|
||||||
.spawn(async move {
|
.spawn(async move {
|
||||||
@ -632,7 +632,7 @@ impl Platform for MacPlatform {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if let Some(done_tx) = done_tx.take() {
|
if let Some(done_tx) = done_tx.take() {
|
||||||
let _ = done_tx.send(result);
|
let _ = done_tx.send(Ok(result));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
let block = block.copy();
|
let block = block.copy();
|
||||||
@ -643,7 +643,7 @@ impl Platform for MacPlatform {
|
|||||||
done_rx
|
done_rx
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Option<PathBuf>> {
|
fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Result<Option<PathBuf>>> {
|
||||||
let directory = directory.to_owned();
|
let directory = directory.to_owned();
|
||||||
let (done_tx, done_rx) = oneshot::channel();
|
let (done_tx, done_rx) = oneshot::channel();
|
||||||
self.foreground_executor()
|
self.foreground_executor()
|
||||||
@ -665,7 +665,7 @@ impl Platform for MacPlatform {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(done_tx) = done_tx.take() {
|
if let Some(done_tx) = done_tx.take() {
|
||||||
let _ = done_tx.send(result);
|
let _ = done_tx.send(Ok(result));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
let block = block.copy();
|
let block = block.copy();
|
||||||
|
@ -34,7 +34,7 @@ pub(crate) struct TestPlatform {
|
|||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub(crate) struct TestPrompts {
|
pub(crate) struct TestPrompts {
|
||||||
multiple_choice: VecDeque<oneshot::Sender<usize>>,
|
multiple_choice: VecDeque<oneshot::Sender<usize>>,
|
||||||
new_path: VecDeque<(PathBuf, oneshot::Sender<Option<PathBuf>>)>,
|
new_path: VecDeque<(PathBuf, oneshot::Sender<Result<Option<PathBuf>>>)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TestPlatform {
|
impl TestPlatform {
|
||||||
@ -80,7 +80,7 @@ impl TestPlatform {
|
|||||||
.new_path
|
.new_path
|
||||||
.pop_front()
|
.pop_front()
|
||||||
.expect("no pending new path prompt");
|
.expect("no pending new path prompt");
|
||||||
tx.send(select_path(&path)).ok();
|
tx.send(Ok(select_path(&path))).ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn simulate_prompt_answer(&self, response_ix: usize) {
|
pub(crate) fn simulate_prompt_answer(&self, response_ix: usize) {
|
||||||
@ -216,14 +216,14 @@ impl Platform for TestPlatform {
|
|||||||
fn prompt_for_paths(
|
fn prompt_for_paths(
|
||||||
&self,
|
&self,
|
||||||
_options: crate::PathPromptOptions,
|
_options: crate::PathPromptOptions,
|
||||||
) -> oneshot::Receiver<Option<Vec<std::path::PathBuf>>> {
|
) -> oneshot::Receiver<Result<Option<Vec<std::path::PathBuf>>>> {
|
||||||
unimplemented!()
|
unimplemented!()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prompt_for_new_path(
|
fn prompt_for_new_path(
|
||||||
&self,
|
&self,
|
||||||
directory: &std::path::Path,
|
directory: &std::path::Path,
|
||||||
) -> oneshot::Receiver<Option<std::path::PathBuf>> {
|
) -> oneshot::Receiver<Result<Option<std::path::PathBuf>>> {
|
||||||
let (tx, rx) = oneshot::channel();
|
let (tx, rx) = oneshot::channel();
|
||||||
self.prompts
|
self.prompts
|
||||||
.borrow_mut()
|
.borrow_mut()
|
||||||
|
@ -335,7 +335,10 @@ impl Platform for WindowsPlatform {
|
|||||||
self.state.borrow_mut().callbacks.open_urls = Some(callback);
|
self.state.borrow_mut().callbacks.open_urls = Some(callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prompt_for_paths(&self, options: PathPromptOptions) -> Receiver<Option<Vec<PathBuf>>> {
|
fn prompt_for_paths(
|
||||||
|
&self,
|
||||||
|
options: PathPromptOptions,
|
||||||
|
) -> Receiver<Result<Option<Vec<PathBuf>>>> {
|
||||||
let (tx, rx) = oneshot::channel();
|
let (tx, rx) = oneshot::channel();
|
||||||
|
|
||||||
self.foreground_executor()
|
self.foreground_executor()
|
||||||
@ -374,7 +377,7 @@ impl Platform for WindowsPlatform {
|
|||||||
if hr.unwrap_err().code() == HRESULT(0x800704C7u32 as i32) {
|
if hr.unwrap_err().code() == HRESULT(0x800704C7u32 as i32) {
|
||||||
// user canceled error
|
// user canceled error
|
||||||
if let Some(tx) = tx.take() {
|
if let Some(tx) = tx.take() {
|
||||||
tx.send(None).unwrap();
|
tx.send(Ok(None)).unwrap();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -393,10 +396,10 @@ impl Platform for WindowsPlatform {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(tx) = tx.take() {
|
if let Some(tx) = tx.take() {
|
||||||
if paths.len() == 0 {
|
if paths.is_empty() {
|
||||||
tx.send(None).unwrap();
|
tx.send(Ok(None)).unwrap();
|
||||||
} else {
|
} else {
|
||||||
tx.send(Some(paths)).unwrap();
|
tx.send(Ok(Some(paths))).unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -405,27 +408,27 @@ impl Platform for WindowsPlatform {
|
|||||||
rx
|
rx
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prompt_for_new_path(&self, directory: &Path) -> Receiver<Option<PathBuf>> {
|
fn prompt_for_new_path(&self, directory: &Path) -> Receiver<Result<Option<PathBuf>>> {
|
||||||
let directory = directory.to_owned();
|
let directory = directory.to_owned();
|
||||||
let (tx, rx) = oneshot::channel();
|
let (tx, rx) = oneshot::channel();
|
||||||
self.foreground_executor()
|
self.foreground_executor()
|
||||||
.spawn(async move {
|
.spawn(async move {
|
||||||
unsafe {
|
unsafe {
|
||||||
let Ok(dialog) = show_savefile_dialog(directory) else {
|
let Ok(dialog) = show_savefile_dialog(directory) else {
|
||||||
let _ = tx.send(None);
|
let _ = tx.send(Ok(None));
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let Ok(_) = dialog.Show(None) else {
|
let Ok(_) = dialog.Show(None) else {
|
||||||
let _ = tx.send(None); // user cancel
|
let _ = tx.send(Ok(None)); // user cancel
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
if let Ok(shell_item) = dialog.GetResult() {
|
if let Ok(shell_item) = dialog.GetResult() {
|
||||||
if let Ok(file) = shell_item.GetDisplayName(SIGDN_FILESYSPATH) {
|
if let Ok(file) = shell_item.GetDisplayName(SIGDN_FILESYSPATH) {
|
||||||
let _ = tx.send(Some(PathBuf::from(file.to_string().unwrap())));
|
let _ = tx.send(Ok(Some(PathBuf::from(file.to_string().unwrap()))));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let _ = tx.send(None);
|
let _ = tx.send(Ok(None));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
@ -160,14 +160,23 @@ impl Workspace {
|
|||||||
self.show_notification(
|
self.show_notification(
|
||||||
NotificationId::unique::<WorkspaceErrorNotification>(),
|
NotificationId::unique::<WorkspaceErrorNotification>(),
|
||||||
cx,
|
cx,
|
||||||
|cx| {
|
|cx| cx.new_view(|_cx| ErrorMessagePrompt::new(format!("Error: {err:#}"))),
|
||||||
cx.new_view(|_cx| {
|
|
||||||
simple_message_notification::MessageNotification::new(format!("Error: {err:#}"))
|
|
||||||
})
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn show_portal_error(&mut self, err: String, cx: &mut ViewContext<Self>) {
|
||||||
|
struct PortalError;
|
||||||
|
|
||||||
|
self.show_notification(NotificationId::unique::<PortalError>(), cx, |cx| {
|
||||||
|
cx.new_view(|_cx| {
|
||||||
|
ErrorMessagePrompt::new(err.to_string()).with_link_button(
|
||||||
|
"See docs",
|
||||||
|
"https://zed.dev/docs/linux#i-cant-open-any-files",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
pub fn dismiss_notification(&mut self, id: &NotificationId, cx: &mut ViewContext<Self>) {
|
pub fn dismiss_notification(&mut self, id: &NotificationId, cx: &mut ViewContext<Self>) {
|
||||||
self.dismiss_notification_internal(id, cx)
|
self.dismiss_notification_internal(id, cx)
|
||||||
}
|
}
|
||||||
@ -362,6 +371,84 @@ impl Render for LanguageServerPrompt {
|
|||||||
|
|
||||||
impl EventEmitter<DismissEvent> for LanguageServerPrompt {}
|
impl EventEmitter<DismissEvent> for LanguageServerPrompt {}
|
||||||
|
|
||||||
|
pub struct ErrorMessagePrompt {
|
||||||
|
message: SharedString,
|
||||||
|
label_and_url_button: Option<(SharedString, SharedString)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ErrorMessagePrompt {
|
||||||
|
pub fn new<S>(message: S) -> Self
|
||||||
|
where
|
||||||
|
S: Into<SharedString>,
|
||||||
|
{
|
||||||
|
Self {
|
||||||
|
message: message.into(),
|
||||||
|
label_and_url_button: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_link_button<S>(mut self, label: S, url: S) -> Self
|
||||||
|
where
|
||||||
|
S: Into<SharedString>,
|
||||||
|
{
|
||||||
|
self.label_and_url_button = Some((label.into(), url.into()));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for ErrorMessagePrompt {
|
||||||
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
|
h_flex()
|
||||||
|
.id("error_message_prompt_notification")
|
||||||
|
.occlude()
|
||||||
|
.elevation_3(cx)
|
||||||
|
.items_start()
|
||||||
|
.justify_between()
|
||||||
|
.p_2()
|
||||||
|
.gap_2()
|
||||||
|
.w_full()
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.w_full()
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.w_full()
|
||||||
|
.justify_between()
|
||||||
|
.child(
|
||||||
|
svg()
|
||||||
|
.size(cx.text_style().font_size)
|
||||||
|
.flex_none()
|
||||||
|
.mr_2()
|
||||||
|
.mt(px(-2.0))
|
||||||
|
.map(|icon| {
|
||||||
|
icon.path(IconName::ExclamationTriangle.path())
|
||||||
|
.text_color(Color::Error.color(cx))
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
ui::IconButton::new("close", ui::IconName::Close)
|
||||||
|
.on_click(cx.listener(|_, _, cx| cx.emit(gpui::DismissEvent))),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.max_w_80()
|
||||||
|
.child(Label::new(self.message.clone()).size(LabelSize::Small)),
|
||||||
|
)
|
||||||
|
.when_some(self.label_and_url_button.clone(), |elm, (label, url)| {
|
||||||
|
elm.child(
|
||||||
|
div().mt_2().child(
|
||||||
|
ui::Button::new("error_message_prompt_notification_button", label)
|
||||||
|
.on_click(move |_, cx| cx.open_url(&url)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventEmitter<DismissEvent> for ErrorMessagePrompt {}
|
||||||
|
|
||||||
pub mod simple_message_notification {
|
pub mod simple_message_notification {
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, DismissEvent, EventEmitter, InteractiveElement, ParentElement, Render, SharedString,
|
div, DismissEvent, EventEmitter, InteractiveElement, ParentElement, Render, SharedString,
|
||||||
|
@ -30,10 +30,10 @@ use gpui::{
|
|||||||
action_as, actions, canvas, impl_action_as, impl_actions, point, relative, size,
|
action_as, actions, canvas, impl_action_as, impl_actions, point, relative, size,
|
||||||
transparent_black, Action, AnyElement, AnyView, AnyWeakView, AppContext, AsyncAppContext,
|
transparent_black, Action, AnyElement, AnyView, AnyWeakView, AppContext, AsyncAppContext,
|
||||||
AsyncWindowContext, Bounds, CursorStyle, Decorations, DragMoveEvent, Entity as _, EntityId,
|
AsyncWindowContext, Bounds, CursorStyle, Decorations, DragMoveEvent, Entity as _, EntityId,
|
||||||
EventEmitter, FocusHandle, FocusableView, Global, Hsla, KeyContext, Keystroke, ManagedView,
|
EventEmitter, Flatten, FocusHandle, FocusableView, Global, Hsla, KeyContext, Keystroke,
|
||||||
Model, ModelContext, MouseButton, PathPromptOptions, Point, PromptLevel, Render, ResizeEdge,
|
ManagedView, Model, ModelContext, MouseButton, PathPromptOptions, Point, PromptLevel, Render,
|
||||||
Size, Stateful, Subscription, Task, Tiling, View, WeakView, WindowBounds, WindowHandle,
|
ResizeEdge, Size, Stateful, Subscription, Task, Tiling, View, WeakView, WindowBounds,
|
||||||
WindowOptions,
|
WindowHandle, WindowOptions,
|
||||||
};
|
};
|
||||||
use item::{
|
use item::{
|
||||||
FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, PreviewTabsSettings,
|
FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, PreviewTabsSettings,
|
||||||
@ -305,13 +305,31 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
|
|||||||
|
|
||||||
if let Some(app_state) = app_state.upgrade() {
|
if let Some(app_state) = app_state.upgrade() {
|
||||||
cx.spawn(move |cx| async move {
|
cx.spawn(move |cx| async move {
|
||||||
if let Some(paths) = paths.await.log_err().flatten() {
|
match Flatten::flatten(paths.await.map_err(|e| e.into())) {
|
||||||
|
Ok(Some(paths)) => {
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
open_paths(&paths, app_state, OpenOptions::default(), cx)
|
open_paths(&paths, app_state, OpenOptions::default(), cx)
|
||||||
.detach_and_log_err(cx)
|
.detach_and_log_err(cx)
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
|
Ok(None) => {}
|
||||||
|
Err(err) => {
|
||||||
|
cx.update(|cx| {
|
||||||
|
if let Some(workspace_window) = cx
|
||||||
|
.active_window()
|
||||||
|
.and_then(|window| window.downcast::<Workspace>())
|
||||||
|
{
|
||||||
|
workspace_window
|
||||||
|
.update(cx, |workspace, cx| {
|
||||||
|
workspace.show_portal_error(err.to_string(), cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
};
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
@ -1321,7 +1339,15 @@ impl Workspace {
|
|||||||
let (tx, rx) = oneshot::channel();
|
let (tx, rx) = oneshot::channel();
|
||||||
let abs_path = cx.prompt_for_new_path(&start_abs_path);
|
let abs_path = cx.prompt_for_new_path(&start_abs_path);
|
||||||
cx.spawn(|this, mut cx| async move {
|
cx.spawn(|this, mut cx| async move {
|
||||||
let abs_path = abs_path.await?;
|
let abs_path: Option<PathBuf> =
|
||||||
|
Flatten::flatten(abs_path.await.map_err(|e| e.into())).map_err(|err| {
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
this.show_portal_error(err.to_string(), cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
err
|
||||||
|
})?;
|
||||||
|
|
||||||
let project_path = abs_path.and_then(|abs_path| {
|
let project_path = abs_path.and_then(|abs_path| {
|
||||||
this.update(&mut cx, |this, cx| {
|
this.update(&mut cx, |this, cx| {
|
||||||
this.project.update(cx, |project, cx| {
|
this.project.update(cx, |project, cx| {
|
||||||
@ -1610,8 +1636,16 @@ impl Workspace {
|
|||||||
});
|
});
|
||||||
|
|
||||||
cx.spawn(|this, mut cx| async move {
|
cx.spawn(|this, mut cx| async move {
|
||||||
let Some(paths) = paths.await.log_err().flatten() else {
|
let paths = match Flatten::flatten(paths.await.map_err(|e| e.into())) {
|
||||||
|
Ok(Some(paths)) => paths,
|
||||||
|
Ok(None) => return,
|
||||||
|
Err(err) => {
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
this.show_portal_error(err.to_string(), cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(task) = this
|
if let Some(task) = this
|
||||||
@ -1773,7 +1807,14 @@ impl Workspace {
|
|||||||
multiple: true,
|
multiple: true,
|
||||||
});
|
});
|
||||||
cx.spawn(|this, mut cx| async move {
|
cx.spawn(|this, mut cx| async move {
|
||||||
if let Some(paths) = paths.await.log_err().flatten() {
|
let paths = Flatten::flatten(paths.await.map_err(|e| e.into())).map_err(|err| {
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
this.show_portal_error(err.to_string(), cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
err
|
||||||
|
})?;
|
||||||
|
if let Some(paths) = paths {
|
||||||
let results = this
|
let results = this
|
||||||
.update(&mut cx, |this, cx| {
|
.update(&mut cx, |this, cx| {
|
||||||
this.open_paths(paths, OpenVisible::All, None, cx)
|
this.open_paths(paths, OpenVisible::All, None, cx)
|
||||||
|
Loading…
Reference in New Issue
Block a user