Suggest unsaved buffer content text as the default filename (#35707)
Closes #24672 This PR complements a feature added earlier by @JosephTLyons (in https://github.com/zed-industries/zed/pull/32353) where the text is considered as the tab title in a new buffer. It piggybacks off that change and sets the title as the suggested filename in the save dialog (completely mirroring the same functionality in VSCode):  Release Notes: - Text entered in a new untitled buffer is considered as the default filename when saving
This commit is contained in:
parent
485802b9e5
commit
7993ee9c07
10 changed files with 75 additions and 18 deletions
|
@ -654,6 +654,10 @@ impl Item for Editor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn suggested_filename(&self, cx: &App) -> SharedString {
|
||||||
|
self.buffer.read(cx).title(cx).to_string().into()
|
||||||
|
}
|
||||||
|
|
||||||
fn tab_icon(&self, _: &Window, cx: &App) -> Option<Icon> {
|
fn tab_icon(&self, _: &Window, cx: &App) -> Option<Icon> {
|
||||||
ItemSettings::get_global(cx)
|
ItemSettings::get_global(cx)
|
||||||
.file_icons
|
.file_icons
|
||||||
|
|
|
@ -816,8 +816,9 @@ impl App {
|
||||||
pub fn prompt_for_new_path(
|
pub fn prompt_for_new_path(
|
||||||
&self,
|
&self,
|
||||||
directory: &Path,
|
directory: &Path,
|
||||||
|
suggested_name: Option<&str>,
|
||||||
) -> oneshot::Receiver<Result<Option<PathBuf>>> {
|
) -> oneshot::Receiver<Result<Option<PathBuf>>> {
|
||||||
self.platform.prompt_for_new_path(directory)
|
self.platform.prompt_for_new_path(directory, suggested_name)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reveals the specified path at the platform level, such as in Finder on macOS.
|
/// Reveals the specified path at the platform level, such as in Finder on macOS.
|
||||||
|
|
|
@ -220,7 +220,11 @@ pub(crate) trait Platform: 'static {
|
||||||
&self,
|
&self,
|
||||||
options: PathPromptOptions,
|
options: PathPromptOptions,
|
||||||
) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>>;
|
) -> oneshot::Receiver<Result<Option<Vec<PathBuf>>>>;
|
||||||
fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Result<Option<PathBuf>>>;
|
fn prompt_for_new_path(
|
||||||
|
&self,
|
||||||
|
directory: &Path,
|
||||||
|
suggested_name: Option<&str>,
|
||||||
|
) -> oneshot::Receiver<Result<Option<PathBuf>>>;
|
||||||
fn can_select_mixed_files_and_dirs(&self) -> bool;
|
fn can_select_mixed_files_and_dirs(&self) -> bool;
|
||||||
fn reveal_path(&self, path: &Path);
|
fn reveal_path(&self, path: &Path);
|
||||||
fn open_with_system(&self, path: &Path);
|
fn open_with_system(&self, path: &Path);
|
||||||
|
|
|
@ -327,26 +327,35 @@ impl<P: LinuxClient + 'static> Platform for P {
|
||||||
done_rx
|
done_rx
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Result<Option<PathBuf>>> {
|
fn prompt_for_new_path(
|
||||||
|
&self,
|
||||||
|
directory: &Path,
|
||||||
|
suggested_name: Option<&str>,
|
||||||
|
) -> oneshot::Receiver<Result<Option<PathBuf>>> {
|
||||||
let (done_tx, done_rx) = oneshot::channel();
|
let (done_tx, done_rx) = oneshot::channel();
|
||||||
|
|
||||||
#[cfg(not(any(feature = "wayland", feature = "x11")))]
|
#[cfg(not(any(feature = "wayland", feature = "x11")))]
|
||||||
let _ = (done_tx.send(Ok(None)), directory);
|
let _ = (done_tx.send(Ok(None)), directory, suggested_name);
|
||||||
|
|
||||||
#[cfg(any(feature = "wayland", feature = "x11"))]
|
#[cfg(any(feature = "wayland", feature = "x11"))]
|
||||||
self.foreground_executor()
|
self.foreground_executor()
|
||||||
.spawn({
|
.spawn({
|
||||||
let directory = directory.to_owned();
|
let directory = directory.to_owned();
|
||||||
|
let suggested_name = suggested_name.map(|s| s.to_owned());
|
||||||
|
|
||||||
async move {
|
async move {
|
||||||
let request = match ashpd::desktop::file_chooser::SaveFileRequest::default()
|
let mut request_builder =
|
||||||
.modal(true)
|
ashpd::desktop::file_chooser::SaveFileRequest::default()
|
||||||
.title("Save File")
|
.modal(true)
|
||||||
.current_folder(directory)
|
.title("Save File")
|
||||||
.expect("pathbuf should not be nul terminated")
|
.current_folder(directory)
|
||||||
.send()
|
.expect("pathbuf should not be nul terminated");
|
||||||
.await
|
|
||||||
{
|
if let Some(suggested_name) = suggested_name {
|
||||||
|
request_builder = request_builder.current_name(suggested_name.as_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
let request = match request_builder.send().await {
|
||||||
Ok(request) => request,
|
Ok(request) => request,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
let result = match err {
|
let result = match err {
|
||||||
|
|
|
@ -737,8 +737,13 @@ impl Platform for MacPlatform {
|
||||||
done_rx
|
done_rx
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prompt_for_new_path(&self, directory: &Path) -> oneshot::Receiver<Result<Option<PathBuf>>> {
|
fn prompt_for_new_path(
|
||||||
|
&self,
|
||||||
|
directory: &Path,
|
||||||
|
suggested_name: Option<&str>,
|
||||||
|
) -> oneshot::Receiver<Result<Option<PathBuf>>> {
|
||||||
let directory = directory.to_owned();
|
let directory = directory.to_owned();
|
||||||
|
let suggested_name = suggested_name.map(|s| s.to_owned());
|
||||||
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 {
|
||||||
|
@ -748,6 +753,11 @@ impl Platform for MacPlatform {
|
||||||
let url = NSURL::fileURLWithPath_isDirectory_(nil, path, true.to_objc());
|
let url = NSURL::fileURLWithPath_isDirectory_(nil, path, true.to_objc());
|
||||||
panel.setDirectoryURL(url);
|
panel.setDirectoryURL(url);
|
||||||
|
|
||||||
|
if let Some(suggested_name) = suggested_name {
|
||||||
|
let name_string = ns_string(&suggested_name);
|
||||||
|
let _: () = msg_send![panel, setNameFieldStringValue: name_string];
|
||||||
|
}
|
||||||
|
|
||||||
let done_tx = Cell::new(Some(done_tx));
|
let done_tx = Cell::new(Some(done_tx));
|
||||||
let block = ConcreteBlock::new(move |response: NSModalResponse| {
|
let block = ConcreteBlock::new(move |response: NSModalResponse| {
|
||||||
let mut result = None;
|
let mut result = None;
|
||||||
|
|
|
@ -336,6 +336,7 @@ impl Platform for TestPlatform {
|
||||||
fn prompt_for_new_path(
|
fn prompt_for_new_path(
|
||||||
&self,
|
&self,
|
||||||
directory: &std::path::Path,
|
directory: &std::path::Path,
|
||||||
|
_suggested_name: Option<&str>,
|
||||||
) -> oneshot::Receiver<Result<Option<std::path::PathBuf>>> {
|
) -> oneshot::Receiver<Result<Option<std::path::PathBuf>>> {
|
||||||
let (tx, rx) = oneshot::channel();
|
let (tx, rx) = oneshot::channel();
|
||||||
self.background_executor()
|
self.background_executor()
|
||||||
|
|
|
@ -490,13 +490,18 @@ impl Platform for WindowsPlatform {
|
||||||
rx
|
rx
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prompt_for_new_path(&self, directory: &Path) -> Receiver<Result<Option<PathBuf>>> {
|
fn prompt_for_new_path(
|
||||||
|
&self,
|
||||||
|
directory: &Path,
|
||||||
|
suggested_name: Option<&str>,
|
||||||
|
) -> Receiver<Result<Option<PathBuf>>> {
|
||||||
let directory = directory.to_owned();
|
let directory = directory.to_owned();
|
||||||
|
let suggested_name = suggested_name.map(|s| s.to_owned());
|
||||||
let (tx, rx) = oneshot::channel();
|
let (tx, rx) = oneshot::channel();
|
||||||
let window = self.find_current_active_window();
|
let window = self.find_current_active_window();
|
||||||
self.foreground_executor()
|
self.foreground_executor()
|
||||||
.spawn(async move {
|
.spawn(async move {
|
||||||
let _ = tx.send(file_save_dialog(directory, window));
|
let _ = tx.send(file_save_dialog(directory, suggested_name, window));
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
|
@ -804,7 +809,11 @@ fn file_open_dialog(
|
||||||
Ok(Some(paths))
|
Ok(Some(paths))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn file_save_dialog(directory: PathBuf, window: Option<HWND>) -> Result<Option<PathBuf>> {
|
fn file_save_dialog(
|
||||||
|
directory: PathBuf,
|
||||||
|
suggested_name: Option<String>,
|
||||||
|
window: Option<HWND>,
|
||||||
|
) -> Result<Option<PathBuf>> {
|
||||||
let dialog: IFileSaveDialog = unsafe { CoCreateInstance(&FileSaveDialog, None, CLSCTX_ALL)? };
|
let dialog: IFileSaveDialog = unsafe { CoCreateInstance(&FileSaveDialog, None, CLSCTX_ALL)? };
|
||||||
if !directory.to_string_lossy().is_empty() {
|
if !directory.to_string_lossy().is_empty() {
|
||||||
if let Some(full_path) = directory.canonicalize().log_err() {
|
if let Some(full_path) = directory.canonicalize().log_err() {
|
||||||
|
@ -815,6 +824,11 @@ fn file_save_dialog(directory: PathBuf, window: Option<HWND>) -> Result<Option<P
|
||||||
unsafe { dialog.SetFolder(&path_item).log_err() };
|
unsafe { dialog.SetFolder(&path_item).log_err() };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(suggested_name) = suggested_name {
|
||||||
|
unsafe { dialog.SetFileName(&HSTRING::from(suggested_name)).log_err() };
|
||||||
|
}
|
||||||
|
|
||||||
unsafe {
|
unsafe {
|
||||||
dialog.SetFileTypes(&[Common::COMDLG_FILTERSPEC {
|
dialog.SetFileTypes(&[Common::COMDLG_FILTERSPEC {
|
||||||
pszName: windows::core::w!("All files"),
|
pszName: windows::core::w!("All files"),
|
||||||
|
|
|
@ -270,6 +270,12 @@ pub trait Item: Focusable + EventEmitter<Self::Event> + Render + Sized {
|
||||||
/// Returns the textual contents of the tab.
|
/// Returns the textual contents of the tab.
|
||||||
fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString;
|
fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString;
|
||||||
|
|
||||||
|
/// Returns the suggested filename for saving this item.
|
||||||
|
/// By default, returns the tab content text.
|
||||||
|
fn suggested_filename(&self, cx: &App) -> SharedString {
|
||||||
|
self.tab_content_text(0, cx)
|
||||||
|
}
|
||||||
|
|
||||||
fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
|
fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
@ -497,6 +503,7 @@ pub trait ItemHandle: 'static + Send {
|
||||||
) -> gpui::Subscription;
|
) -> gpui::Subscription;
|
||||||
fn tab_content(&self, params: TabContentParams, window: &Window, cx: &App) -> AnyElement;
|
fn tab_content(&self, params: TabContentParams, window: &Window, cx: &App) -> AnyElement;
|
||||||
fn tab_content_text(&self, detail: usize, cx: &App) -> SharedString;
|
fn tab_content_text(&self, detail: usize, cx: &App) -> SharedString;
|
||||||
|
fn suggested_filename(&self, cx: &App) -> SharedString;
|
||||||
fn tab_icon(&self, window: &Window, cx: &App) -> Option<Icon>;
|
fn tab_icon(&self, window: &Window, cx: &App) -> Option<Icon>;
|
||||||
fn tab_tooltip_text(&self, cx: &App) -> Option<SharedString>;
|
fn tab_tooltip_text(&self, cx: &App) -> Option<SharedString>;
|
||||||
fn tab_tooltip_content(&self, cx: &App) -> Option<TabTooltipContent>;
|
fn tab_tooltip_content(&self, cx: &App) -> Option<TabTooltipContent>;
|
||||||
|
@ -631,6 +638,10 @@ impl<T: Item> ItemHandle for Entity<T> {
|
||||||
self.read(cx).tab_content_text(detail, cx)
|
self.read(cx).tab_content_text(detail, cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn suggested_filename(&self, cx: &App) -> SharedString {
|
||||||
|
self.read(cx).suggested_filename(cx)
|
||||||
|
}
|
||||||
|
|
||||||
fn tab_icon(&self, window: &Window, cx: &App) -> Option<Icon> {
|
fn tab_icon(&self, window: &Window, cx: &App) -> Option<Icon> {
|
||||||
self.read(cx).tab_icon(window, cx)
|
self.read(cx).tab_icon(window, cx)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2062,6 +2062,8 @@ impl Pane {
|
||||||
})?
|
})?
|
||||||
.await?;
|
.await?;
|
||||||
} else if can_save_as && is_singleton {
|
} else if can_save_as && is_singleton {
|
||||||
|
let suggested_name =
|
||||||
|
cx.update(|_window, cx| item.suggested_filename(cx).to_string())?;
|
||||||
let new_path = pane.update_in(cx, |pane, window, cx| {
|
let new_path = pane.update_in(cx, |pane, window, cx| {
|
||||||
pane.activate_item(item_ix, true, true, window, cx);
|
pane.activate_item(item_ix, true, true, window, cx);
|
||||||
pane.workspace.update(cx, |workspace, cx| {
|
pane.workspace.update(cx, |workspace, cx| {
|
||||||
|
@ -2073,7 +2075,7 @@ impl Pane {
|
||||||
} else {
|
} else {
|
||||||
DirectoryLister::Project(workspace.project().clone())
|
DirectoryLister::Project(workspace.project().clone())
|
||||||
};
|
};
|
||||||
workspace.prompt_for_new_path(lister, window, cx)
|
workspace.prompt_for_new_path(lister, Some(suggested_name), window, cx)
|
||||||
})
|
})
|
||||||
})??;
|
})??;
|
||||||
let Some(new_path) = new_path.await.ok().flatten().into_iter().flatten().next()
|
let Some(new_path) = new_path.await.ok().flatten().into_iter().flatten().next()
|
||||||
|
|
|
@ -2067,6 +2067,7 @@ impl Workspace {
|
||||||
pub fn prompt_for_new_path(
|
pub fn prompt_for_new_path(
|
||||||
&mut self,
|
&mut self,
|
||||||
lister: DirectoryLister,
|
lister: DirectoryLister,
|
||||||
|
suggested_name: Option<String>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> oneshot::Receiver<Option<Vec<PathBuf>>> {
|
) -> oneshot::Receiver<Option<Vec<PathBuf>>> {
|
||||||
|
@ -2094,7 +2095,7 @@ impl Workspace {
|
||||||
})
|
})
|
||||||
.or_else(std::env::home_dir)
|
.or_else(std::env::home_dir)
|
||||||
.unwrap_or_else(|| PathBuf::from(""));
|
.unwrap_or_else(|| PathBuf::from(""));
|
||||||
cx.prompt_for_new_path(&relative_to)
|
cx.prompt_for_new_path(&relative_to, suggested_name.as_deref())
|
||||||
})?;
|
})?;
|
||||||
let abs_path = match abs_path.await? {
|
let abs_path = match abs_path.await? {
|
||||||
Ok(path) => path,
|
Ok(path) => path,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue