terminal_view: Add support for opening the terminal in the current file directory (#47739)
Closes #14863 Changes: - Added `CurrentFileDirectory` variant to the `WorkingDirectory` enum. - New terminals now open in the directory of the currently active file when this option is set. - Falls back to project directory, then first workspace directory if no file is active. Release Notes: - Added `current_file_directory` option for terminal's `working_directory` setting. Set `"working_directory": current_file_directory"` to open new terminals in the directory of your currently open file. --- Still relatively new to Rust, so happy to receive any feedback! 😁
This commit is contained in:
1 parent
02f3860d25
commit
ae9539ecbd
7 files changed
+127
-20
No files matched your search
@@ -1596,15 +1596,18 @@
|
||||
// Default height when the terminal is docked to the bottom.
|
||||
"default_height": 320,
|
||||
// What working directory to use when launching the terminal.
|
||||
// May take 4 values:
|
||||
// 1. Use the current file's project directory. Fallback to the
|
||||
// May take 5 values:
|
||||
// 1. Use the current file's directory, falling back to the project
|
||||
// directory, then the first project in the workspace.
|
||||
// "working_directory": "current_file_directory"
|
||||
// 2. Use the current file's project directory. Fallback to the
|
||||
// first project directory strategy if unsuccessful
|
||||
// "working_directory": "current_project_directory"
|
||||
// 2. Use the first project in this workspace's directory
|
||||
// 3. Use the first project in this workspace's directory
|
||||
// "working_directory": "first_project_directory"
|
||||
// 3. Always use this platform's home directory (if we can find it)
|
||||
// 4. Always use this platform's home directory (if we can find it)
|
||||
// "working_directory": "always_home"
|
||||
// 4. Always use a specific directory. This value will be shell expanded.
|
||||
// 5. Always use a specific directory. This value will be shell expanded.
|
||||
// If this path is not a valid directory the terminal will default to
|
||||
// this platform's home directory (if we can find it)
|
||||
// "working_directory": {
|
||||
|
||||
@@ -26,6 +26,20 @@ pub struct Terminals {
|
||||
}
|
||||
|
||||
impl Project {
|
||||
pub fn active_entry_directory(&self, cx: &App) -> Option<PathBuf> {
|
||||
let entry_id = self.active_entry()?;
|
||||
let worktree = self.worktree_for_entry(entry_id, cx)?;
|
||||
let worktree = worktree.read(cx);
|
||||
let entry = worktree.entry_for_id(entry_id)?;
|
||||
|
||||
let absolute_path = worktree.absolutize(entry.path.as_ref());
|
||||
if entry.is_dir() {
|
||||
Some(absolute_path)
|
||||
} else {
|
||||
absolute_path.parent().map(|p| p.to_path_buf())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn active_project_directory(&self, cx: &App) -> Option<Arc<Path>> {
|
||||
self.active_entry()
|
||||
.and_then(|entry_id| self.worktree_for_entry(entry_id, cx))
|
||||
|
||||
@@ -219,6 +219,9 @@ pub enum Shell {
|
||||
#[strum_discriminants(derive(strum::VariantArray, strum::VariantNames, strum::FromRepr))]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum WorkingDirectory {
|
||||
/// Use the current file's directory, falling back to the project directory,
|
||||
/// then the first project in the workspace.
|
||||
CurrentFileDirectory,
|
||||
/// Use the current file's project directory. Fallback to the
|
||||
/// first project directory strategy if unsuccessful.
|
||||
CurrentProjectDirectory,
|
||||
|
||||
@@ -5762,6 +5762,9 @@ fn terminal_page() -> SettingsPage {
|
||||
.working_directory
|
||||
.get_or_insert_with(|| settings::WorkingDirectory::CurrentProjectDirectory);
|
||||
*settings_value = match value {
|
||||
settings::WorkingDirectoryDiscriminants::CurrentFileDirectory => {
|
||||
settings::WorkingDirectory::CurrentFileDirectory
|
||||
},
|
||||
settings::WorkingDirectoryDiscriminants::CurrentProjectDirectory => {
|
||||
settings::WorkingDirectory::CurrentProjectDirectory
|
||||
}
|
||||
@@ -5797,6 +5800,7 @@ fn terminal_page() -> SettingsPage {
|
||||
fields: dynamic_variants::<settings::WorkingDirectory>()
|
||||
.into_iter()
|
||||
.map(|variant| match variant {
|
||||
settings::WorkingDirectoryDiscriminants::CurrentFileDirectory => vec![],
|
||||
settings::WorkingDirectoryDiscriminants::CurrentProjectDirectory => vec![],
|
||||
settings::WorkingDirectoryDiscriminants::FirstProjectDirectory => vec![],
|
||||
settings::WorkingDirectoryDiscriminants::AlwaysHome => vec![],
|
||||
|
||||
@@ -1785,13 +1785,12 @@ impl SearchableItem for TerminalView {
|
||||
/// Falls back to home directory when no project directory is available.
|
||||
pub(crate) fn default_working_directory(workspace: &Workspace, cx: &App) -> Option<PathBuf> {
|
||||
let directory = match &TerminalSettings::get_global(cx).working_directory {
|
||||
WorkingDirectory::CurrentProjectDirectory => workspace
|
||||
WorkingDirectory::CurrentFileDirectory => workspace
|
||||
.project()
|
||||
.read(cx)
|
||||
.active_project_directory(cx)
|
||||
.as_deref()
|
||||
.map(Path::to_path_buf)
|
||||
.or_else(|| first_project_directory(workspace, cx)),
|
||||
.active_entry_directory(cx)
|
||||
.or_else(|| current_project_directory(workspace, cx)),
|
||||
WorkingDirectory::CurrentProjectDirectory => current_project_directory(workspace, cx),
|
||||
WorkingDirectory::FirstProjectDirectory => first_project_directory(workspace, cx),
|
||||
WorkingDirectory::AlwaysHome => None,
|
||||
WorkingDirectory::Always { directory } => shellexpand::full(directory)
|
||||
@@ -1801,6 +1800,17 @@ pub(crate) fn default_working_directory(workspace: &Workspace, cx: &App) -> Opti
|
||||
};
|
||||
directory.or_else(dirs::home_dir)
|
||||
}
|
||||
|
||||
fn current_project_directory(workspace: &Workspace, cx: &App) -> Option<PathBuf> {
|
||||
workspace
|
||||
.project()
|
||||
.read(cx)
|
||||
.active_project_directory(cx)
|
||||
.as_deref()
|
||||
.map(Path::to_path_buf)
|
||||
.or_else(|| first_project_directory(workspace, cx))
|
||||
}
|
||||
|
||||
///Gets the first project's home directory, or the home directory
|
||||
fn first_project_directory(workspace: &Workspace, cx: &App) -> Option<PathBuf> {
|
||||
let worktree = workspace.worktrees(cx).next()?.read(cx);
|
||||
@@ -1819,6 +1829,7 @@ mod tests {
|
||||
use gpui::TestAppContext;
|
||||
use project::{Entry, Project, ProjectPath, Worktree};
|
||||
use std::path::Path;
|
||||
use util::paths::PathStyle;
|
||||
use util::rel_path::RelPath;
|
||||
use workspace::AppState;
|
||||
|
||||
@@ -1928,6 +1939,67 @@ mod tests {
|
||||
});
|
||||
}
|
||||
|
||||
// active_entry_directory: No active entry -> returns None (used by CurrentFileDirectory)
|
||||
#[gpui::test]
|
||||
async fn active_entry_directory_no_active_entry(cx: &mut TestAppContext) {
|
||||
let (project, _workspace) = init_test(cx).await;
|
||||
|
||||
let (_wt, _entry) = create_folder_wt(project.clone(), "/root/", cx).await;
|
||||
|
||||
cx.update(|cx| {
|
||||
assert!(project.read(cx).active_entry().is_none());
|
||||
|
||||
let res = project.read(cx).active_entry_directory(cx);
|
||||
assert_eq!(res, None);
|
||||
});
|
||||
}
|
||||
|
||||
// active_entry_directory: Active entry is file -> returns parent directory (used by CurrentFileDirectory)
|
||||
#[gpui::test]
|
||||
async fn active_entry_directory_active_file(cx: &mut TestAppContext) {
|
||||
let (project, _workspace) = init_test(cx).await;
|
||||
|
||||
let (wt, _entry) = create_folder_wt(project.clone(), "/root/", cx).await;
|
||||
let entry = cx
|
||||
.update(|cx| {
|
||||
wt.update(cx, |wt, cx| {
|
||||
wt.create_entry(
|
||||
RelPath::new(Path::new("src/main.rs"), PathStyle::local())
|
||||
.unwrap()
|
||||
.as_ref()
|
||||
.into(),
|
||||
false,
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.into_included()
|
||||
.unwrap();
|
||||
insert_active_entry_for(wt, entry, project.clone(), cx);
|
||||
|
||||
cx.update(|cx| {
|
||||
let res = project.read(cx).active_entry_directory(cx);
|
||||
assert_eq!(res, Some(Path::new("/root/src").to_path_buf()));
|
||||
});
|
||||
}
|
||||
|
||||
// active_entry_directory: Active entry is directory -> returns that directory (used by CurrentFileDirectory)
|
||||
#[gpui::test]
|
||||
async fn active_entry_directory_active_dir(cx: &mut TestAppContext) {
|
||||
let (project, _workspace) = init_test(cx).await;
|
||||
|
||||
let (wt, entry) = create_folder_wt(project.clone(), "/root/", cx).await;
|
||||
insert_active_entry_for(wt, entry, project.clone(), cx);
|
||||
|
||||
cx.update(|cx| {
|
||||
let res = project.read(cx).active_entry_directory(cx);
|
||||
assert_eq!(res, Some(Path::new("/root/").to_path_buf()));
|
||||
});
|
||||
}
|
||||
|
||||
/// Creates a worktree with 1 file: /root.txt
|
||||
pub async fn init_test(cx: &mut TestAppContext) -> (Entity<Project>, Entity<Workspace>) {
|
||||
let params = cx.update(AppState::test);
|
||||
|
||||
@@ -4112,7 +4112,17 @@ Example command to set the title: `echo -e "\e]2;New Title\007";`
|
||||
|
||||
**Options**
|
||||
|
||||
1. Use the current file's project directory. Fallback to the first project directory strategy if unsuccessful.
|
||||
1. Use the current file's directory, falling back to the project directory, then the first project in the workspace.
|
||||
|
||||
```json [settings]
|
||||
{
|
||||
"terminal": {
|
||||
"working_directory": "current_file_directory"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. Use the current file's project directory. Fallback to the first project directory strategy if unsuccessful.
|
||||
|
||||
```json [settings]
|
||||
{
|
||||
@@ -4122,7 +4132,7 @@ Example command to set the title: `echo -e "\e]2;New Title\007";`
|
||||
}
|
||||
```
|
||||
|
||||
2. Use the first project in this workspace's directory. Fallback to using this platform's home directory.
|
||||
3. Use the first project in this workspace's directory. Fallback to using this platform's home directory.
|
||||
|
||||
```json [settings]
|
||||
{
|
||||
@@ -4132,7 +4142,7 @@ Example command to set the title: `echo -e "\e]2;New Title\007";`
|
||||
}
|
||||
```
|
||||
|
||||
3. Always use this platform's home directory if it can be found.
|
||||
4. Always use this platform's home directory if it can be found.
|
||||
|
||||
```json [settings]
|
||||
{
|
||||
@@ -4142,7 +4152,7 @@ Example command to set the title: `echo -e "\e]2;New Title\007";`
|
||||
}
|
||||
```
|
||||
|
||||
4. Always use a specific directory. This value will be shell expanded. If this path is not a valid directory the terminal will default to this platform's home directory.
|
||||
5. Always use a specific directory. This value will be shell expanded. If this path is not a valid directory the terminal will default to this platform's home directory.
|
||||
|
||||
```json [settings]
|
||||
{
|
||||
|
||||
@@ -58,12 +58,13 @@ To pass arguments to your shell:
|
||||
|
||||
Control where new terminals start:
|
||||
|
||||
| Value | Behavior |
|
||||
| --------------------------------------------- | --------------------------------------------------------------- |
|
||||
| `"current_project_directory"` | Uses the project directory of the currently open file (default) |
|
||||
| `"first_project_directory"` | Uses the first project in your workspace |
|
||||
| `"always_home"` | Always starts in your home directory |
|
||||
| `{ "always": { "directory": "~/projects" } }` | Always starts in a specific directory |
|
||||
| Value | Behavior |
|
||||
| --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
|
||||
| `"current_file_directory"` | Uses the current file's directory, falling back to the project directory, then the first project in the workspace |
|
||||
| `"current_project_directory"` | Uses the current file's project directory (default) |
|
||||
| `"first_project_directory"` | Uses the first project in your workspace |
|
||||
| `"always_home"` | Always starts in your home directory |
|
||||
| `{ "always": { "directory": "~/projects" } }` | Always starts in a specific directory |
|
||||
|
||||
```json [settings]
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user