From f49641a6c320a8fafa72f200d39432d5158fa04b Mon Sep 17 00:00:00 2001 From: Rizky Mirzaviandy Priambodo <142987522+Xavrir@users.noreply.github.com> Date: Sun, 8 Mar 2026 12:07:44 +0700 Subject: [PATCH] Fix syntax highlighting for symlinked files by resolving target path When a symlink name has no recognizable extension (e.g. Aliases/0install), syntax detection fails because the symlink path doesn't match any syntax. The target file may have a recognizable extension (e.g. Formula/zero-install.rb) but was never consulted. When path-based syntax detection fails with UndetectedSyntax, canonicalize the path to resolve symlinks and retry detection with the target path. This preserves existing behavior where the symlink path itself matches (e.g. .ssh/config), since the original path is tried first. Closes #1001 --- CHANGELOG.md | 1 + src/assets.rs | 63 +++++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 57 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 21b98d46..5d30114d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ ## Bugfixes - Fix `BAT_CONFIG_DIR` pointing at system config directory causing duplicate flag errors. Closes #3589, see #3620 (@Xavrir) +- Fix syntax highlighting for symlinked files when the symlink name has no extension but the target does. Closes #1001, see #3621 (@Xavrir) - Report error when pager is missing instead of silently falling back, see #3588 (@IMaloney) - Fix `--wrap=never` and `-S` flags being ignored when piping to pager, see #3592 (@IMaloney) - Fix crash with BusyBox `less` on Windows, see #3527 (@Anchal-T) diff --git a/src/assets.rs b/src/assets.rs index 1315537f..29247bd7 100644 --- a/src/assets.rs +++ b/src/assets.rs @@ -223,18 +223,40 @@ impl HighlightingAssets { } let path = input.path(); - let path_syntax = if let Some(path) = path { - self.get_syntax_for_path( - PathAbs::new(path).map_or_else(|_| path.to_owned(), |p| p.as_path().to_path_buf()), - mapping, - ) + let absolute_path = path.and_then(|p| { + PathAbs::new(p) + .ok() + .map(|abs| abs.as_path().to_path_buf()) + .or_else(|| Some(p.to_owned())) + }); + + let path_syntax = if let Some(ref path) = absolute_path { + self.get_syntax_for_path(path, mapping).or_else(|e| { + // If syntax detection failed on the given path, retry with the + // canonicalized path (which resolves symlinks). This handles + // cases like `Aliases/0install -> ../Formula/zero-install.rb` + // where the symlink name has no extension but the target does. + // See #1001. + if matches!(e, Error::UndetectedSyntax(_)) { + if let Ok(resolved) = fs::canonicalize(path) { + if resolved != *path { + return match self.get_syntax_for_path(&resolved, mapping) { + Ok(syntax) => Ok(syntax), + Err(Error::UndetectedSyntax(_)) => Err(e), + Err(err) => Err(err), + }; + } + } + } + Err(e) + }) } else { Err(Error::UndetectedSyntax("[unknown]".into())) }; + // If a path wasn't provided, or if path based syntax detection + // above failed, we fall back to first-line syntax detection. match path_syntax { - // If a path wasn't provided, or if path based syntax detection - // above failed, we fall back to first-line syntax detection. Err(Error::UndetectedSyntax(path)) => { if let Some(syntax_in_set) = self.get_first_line_syntax(&mut input.reader)? { Ok(syntax_in_set) @@ -765,4 +787,31 @@ contexts: "SSH Config" ); } + + #[cfg(unix)] + #[test] + fn syntax_detection_for_symlinked_file_by_target_extension() { + use std::os::unix::fs::symlink; + + let test = SyntaxDetectionTest::new(); + + let formula_dir = test.temp_dir.path().join("Formula"); + std::fs::create_dir(&formula_dir).unwrap(); + let target = formula_dir.join("zero-install.rb"); + File::create(&target).unwrap(); + + let aliases_dir = test.temp_dir.path().join("Aliases"); + std::fs::create_dir(&aliases_dir).unwrap(); + let link = aliases_dir.join("0install"); + symlink(&target, &link).unwrap(); + + let input = Input::ordinary_file(&link); + let dummy_stdin: &[u8] = &[]; + let mut opened_input = input.open(dummy_stdin, None).unwrap(); + + assert_eq!( + test.get_syntax_name(None, None, &mut opened_input, &test.syntax_mapping), + "Ruby" + ); + } }