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:
Ruben Fricke authored and GitHub committed 2026-02-02 09:49:09 +01:00
1 parent 02f3860d25
commit ae9539ecbd
7 files changed
+127 -20

No files matched your search

+8 -5
View File
@@ -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": {
+14
View File
@@ -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))
+3
View File
@@ -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,
+4
View File
@@ -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![],
+77 -5
View File
@@ -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);
+14 -4
View File
@@ -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]
{
+7 -6
View File
@@ -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]
{