From fb01d7f60a10f4b82643b143d76b1d04255084c0 Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Thu, 2 Jul 2026 14:58:05 -0400 Subject: [PATCH] Fix file_type matching for local single-file worktrees --- crates/language/src/buffer.rs | 22 +++++++++ crates/language/src/buffer_tests.rs | 26 +++++++++++ crates/language/src/language_registry.rs | 7 +-- crates/worktree/src/worktree.rs | 4 ++ .../tests/integration/worktree_tests.rs | 45 +++++++++++++++++++ 5 files changed, 99 insertions(+), 5 deletions(-) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index d41143d12f5d89..10455419958a0b 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -365,6 +365,14 @@ pub trait File: Send + Sync + Any { /// includes the name of the worktree's root folder). fn full_path(&self, cx: &App) -> PathBuf; + /// Returns the absolute path to this file in its backing file system. + /// For remote files, this is an absolute path on the remote host. + fn file_system_abs_path(&self, cx: &App) -> PathBuf { + self.as_local() + .map(|file| file.abs_path(cx)) + .unwrap_or_else(|| self.full_path(cx)) + } + /// Returns the path style of this file. fn path_style(&self, cx: &App) -> PathStyle; @@ -6012,6 +6020,20 @@ impl File for TestFile { PathBuf::from(self.root_name.clone()).join(self.path.as_std_path()) } + fn file_system_abs_path(&self, cx: &App) -> PathBuf { + if self.local_root.is_none() { + return self.full_path(cx); + } + + let abs_path = PathBuf::from(self.local_root.as_ref().unwrap()).join(&self.root_name); + // Mirror worktree::Worktree::absolutize: an empty relative path refers to the root itself. + if self.path.as_std_path().as_os_str().is_empty() { + abs_path + } else { + abs_path.join(self.path.as_std_path()) + } + } + fn as_local(&self) -> Option<&dyn LocalFile> { if self.local_root.is_some() { Some(self) diff --git a/crates/language/src/buffer_tests.rs b/crates/language/src/buffer_tests.rs index cdcd90314cd7a5..5d34ce0ddb286e 100644 --- a/crates/language/src/buffer_tests.rs +++ b/crates/language/src/buffer_tests.rs @@ -288,6 +288,7 @@ async fn test_language_for_file_with_custom_file_types(cx: &mut TestAppContext) "Dockerfile".into(), vec!["Dockerfile".into(), "Dockerfile.*".into()].into(), ), + ("SSH Config".into(), vec!["**/.ssh/config".into()].into()), ]); }) }); @@ -335,6 +336,18 @@ async fn test_language_for_file_with_custom_file_types(cx: &mut TestAppContext) }, ..Default::default() }, + LanguageConfig { + name: "SSH Config".into(), + ..Default::default() + }, + LanguageConfig { + name: "INI".into(), + matcher: LanguageMatcher { + path_suffixes: vec!["config".to_string()], + ..Default::default() + }, + ..Default::default() + }, ] { languages.add(Arc::new(Language::new(config, None))); } @@ -385,6 +398,11 @@ async fn test_language_for_file_with_custom_file_types(cx: &mut TestAppContext) .read(|cx| languages.language_for_file(&file("Dockerfile.dev"), None, cx)) .unwrap(); assert_eq!(language.name(), "Dockerfile"); + + let language = cx + .read(|cx| languages.language_for_file(&local_file("/root/.ssh", "config"), None, cx)) + .unwrap(); + assert_eq!(language.name(), "SSH Config"); } fn file(path: &str) -> Arc { @@ -395,6 +413,14 @@ fn file(path: &str) -> Arc { }) } +fn local_file(local_root: &str, root_name: &str) -> Arc { + Arc::new(TestFile { + path: Arc::from(rel_path("")), + root_name: root_name.into(), + local_root: Some(PathBuf::from(local_root)), + }) +} + #[gpui::test] fn test_edit_events(cx: &mut gpui::App) { let mut now = Instant::now(); diff --git a/crates/language/src/language_registry.rs b/crates/language/src/language_registry.rs index 6207ba7e1bce58..90e7e17dc929b2 100644 --- a/crates/language/src/language_registry.rs +++ b/crates/language/src/language_registry.rs @@ -668,12 +668,9 @@ impl LanguageRegistry { cx: &App, ) -> Option { let user_file_types = all_language_settings(Some(file), cx); + let path = file.file_system_abs_path(cx); - self.language_for_file_internal( - &file.full_path(cx), - content, - Some(&user_file_types.file_types), - ) + self.language_for_file_internal(&path, content, Some(&user_file_types.file_types)) } pub fn language_for_file_path(self: &Arc, path: &Path) -> Option { diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 2711362dd1874c..260c828e18d58e 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -3528,6 +3528,10 @@ impl language::File for File { self.worktree.read(cx).full_path(&self.path) } + fn file_system_abs_path(&self, cx: &App) -> PathBuf { + self.worktree.read(cx).absolutize(&self.path) + } + /// Returns the last component of this handle's absolute path. If this handle refers to the root /// of its worktree, then this method will return the name of the worktree itself. fn file_name<'a>(&'a self, cx: &'a App) -> &'a str { diff --git a/crates/worktree/tests/integration/worktree_tests.rs b/crates/worktree/tests/integration/worktree_tests.rs index 6a4370539afc1e..48bbf6ae2deeb5 100644 --- a/crates/worktree/tests/integration/worktree_tests.rs +++ b/crates/worktree/tests/integration/worktree_tests.rs @@ -5006,6 +5006,51 @@ async fn test_remote_worktree_without_git_emits_root_repo_event_after_first_upda ); } +#[gpui::test] +async fn test_remote_single_file_worktree_abs_path(cx: &mut TestAppContext) { + cx.update(|cx| { + let store = SettingsStore::test(cx); + cx.set_global(store); + }); + + let worktree = cx.update(|cx| { + Worktree::remote( + 1, + clock::ReplicaId::new(1), + proto::WorktreeMetadata { + id: 1, + root_name: "config".to_string(), + visible: true, + abs_path: "/home/user/.ssh/config".to_string(), + root_repo_common_dir: None, + }, + AnyProtoClient::new(NoopProtoClient::new()), + PathStyle::Posix, + cx, + ) + }); + + let file = worktree::File { + worktree, + path: Arc::from(rel_path("")), + disk_state: language::DiskState::New, + entry_id: None, + is_local: false, + is_private: false, + }; + + cx.read(|cx| { + assert_eq!( + language::File::full_path(&file, cx), + PathBuf::from("config") + ); + assert_eq!( + language::File::file_system_abs_path(&file, cx), + PathBuf::from("/home/user/.ssh/config") + ); + }); +} + #[gpui::test] async fn test_remote_worktree_with_git_emits_root_repo_event_when_repo_info_arrives( cx: &mut TestAppContext,