From 2d1d531550afaac36dbf4d15c383bc0e1cbf248d Mon Sep 17 00:00:00 2001 From: mo8it Date: Thu, 21 Aug 2025 22:43:46 +0200 Subject: [PATCH] Fix file links in VS Code --- src/app_state.rs | 12 ++++++------ src/exercise.rs | 38 +++++++++++++++++++++++++------------- src/list/state.rs | 8 +------- src/main.rs | 2 +- src/run.rs | 6 +++--- src/term.rs | 29 +++++++++++++++++++---------- src/watch/state.rs | 4 ++-- 7 files changed, 57 insertions(+), 42 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index f3f34813..d654d042 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -60,8 +60,7 @@ pub struct AppState { file_buf: Vec, official_exercises: bool, cmd_runner: CmdRunner, - // Running in VS Code. - vs_code: bool, + emit_file_links: bool, } impl AppState { @@ -181,7 +180,8 @@ impl AppState { file_buf, official_exercises: !Path::new("info.toml").exists(), cmd_runner, - vs_code: env::var_os("TERM_PROGRAM").is_some_and(|v| v == "vscode"), + // VS Code has its own file link handling + emit_file_links: env::var_os("TERM_PROGRAM").is_none_or(|v| v != "vscode"), }; Ok((slf, state_file_status)) @@ -218,8 +218,8 @@ impl AppState { } #[inline] - pub fn vs_code(&self) -> bool { - self.vs_code + pub fn emit_file_links(&self) -> bool { + self.emit_file_links } // Write the state file. @@ -621,7 +621,7 @@ mod tests { file_buf: Vec::new(), official_exercises: true, cmd_runner: CmdRunner::build().unwrap(), - vs_code: false, + emit_file_links: true, }; let mut assert = |done: [bool; 3], expected: [Option; 3]| { diff --git a/src/exercise.rs b/src/exercise.rs index fdfbc4f6..6f517bee 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -7,22 +7,28 @@ use std::io::{self, StdoutLock, Write}; use crate::{ cmd::CmdRunner, - term::{self, CountedWrite, terminal_file_link, write_ansi}, + term::{self, CountedWrite, file_path, terminal_file_link, write_ansi}, }; /// The initial capacity of the output buffer. pub const OUTPUT_CAPACITY: usize = 1 << 14; -pub fn solution_link_line(stdout: &mut StdoutLock, solution_path: &str) -> io::Result<()> { +pub fn solution_link_line( + stdout: &mut StdoutLock, + solution_path: &str, + emit_file_links: bool, +) -> io::Result<()> { stdout.queue(SetAttribute(Attribute::Bold))?; stdout.write_all(b"Solution")?; stdout.queue(ResetColor)?; stdout.write_all(b" for comparison: ")?; - if let Some(canonical_path) = term::canonicalize(solution_path) { - terminal_file_link(stdout, solution_path, &canonical_path, Color::Cyan)?; - } else { - stdout.write_all(solution_path.as_bytes())?; - } + file_path(stdout, Color::Cyan, |writer| { + if emit_file_links && let Some(canonical_path) = term::canonicalize(solution_path) { + terminal_file_link(writer, solution_path, &canonical_path) + } else { + writer.stdout().write_all(solution_path.as_bytes()) + } + })?; stdout.write_all(b"\n") } @@ -72,12 +78,18 @@ pub struct Exercise { } impl Exercise { - pub fn terminal_file_link<'a>(&self, writer: &mut impl CountedWrite<'a>) -> io::Result<()> { - if let Some(canonical_path) = self.canonical_path.as_deref() { - return terminal_file_link(writer, self.path, canonical_path, Color::Blue); - } - - writer.write_str(self.path) + pub fn terminal_file_link<'a>( + &self, + writer: &mut impl CountedWrite<'a>, + emit_file_links: bool, + ) -> io::Result<()> { + file_path(writer, Color::Blue, |writer| { + if emit_file_links && let Some(canonical_path) = self.canonical_path.as_deref() { + terminal_file_link(writer, self.path, canonical_path) + } else { + writer.write_str(self.path) + } + }) } } diff --git a/src/list/state.rs b/src/list/state.rs index ae65ec2b..50d06be9 100644 --- a/src/list/state.rs +++ b/src/list/state.rs @@ -186,13 +186,7 @@ impl<'a> ListState<'a> { writer.write_ascii(&self.name_col_padding[exercise.name.len()..])?; - // The list links aren't shown correctly in VS Code on Windows. - // But VS Code shows its own links anyway. - if self.app_state.vs_code() { - writer.write_str(exercise.path)?; - } else { - exercise.terminal_file_link(&mut writer)?; - } + exercise.terminal_file_link(&mut writer, self.app_state.emit_file_links())?; writer.write_ascii(&self.path_col_padding[exercise.path.len()..])?; diff --git a/src/main.rs b/src/main.rs index 29de56b3..ffd2dfa7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -167,7 +167,7 @@ fn main() -> Result { } app_state .current_exercise() - .terminal_file_link(&mut stdout)?; + .terminal_file_link(&mut stdout, app_state.emit_file_links())?; stdout.write_all(b"\n")?; return Ok(ExitCode::FAILURE); diff --git a/src/run.rs b/src/run.rs index 6f4f099b..b473fc20 100644 --- a/src/run.rs +++ b/src/run.rs @@ -27,7 +27,7 @@ pub fn run(app_state: &mut AppState) -> Result { stdout.write_all(b"Ran ")?; app_state .current_exercise() - .terminal_file_link(&mut stdout)?; + .terminal_file_link(&mut stdout, app_state.emit_file_links())?; stdout.write_all(b" with errors\n")?; return Ok(ExitCode::FAILURE); @@ -41,7 +41,7 @@ pub fn run(app_state: &mut AppState) -> Result { if let Some(solution_path) = app_state.current_solution_path()? { stdout.write_all(b"\n")?; - solution_link_line(&mut stdout, &solution_path)?; + solution_link_line(&mut stdout, &solution_path, app_state.emit_file_links())?; stdout.write_all(b"\n")?; } @@ -50,7 +50,7 @@ pub fn run(app_state: &mut AppState) -> Result { stdout.write_all(b"Next exercise: ")?; app_state .current_exercise() - .terminal_file_link(&mut stdout)?; + .terminal_file_link(&mut stdout, app_state.emit_file_links())?; stdout.write_all(b"\n")?; } ExercisesProgress::AllDone => (), diff --git a/src/term.rs b/src/term.rs index b7dcd9f1..3d149b33 100644 --- a/src/term.rs +++ b/src/term.rs @@ -272,22 +272,18 @@ pub fn canonicalize(path: &str) -> Option { }) } -pub fn terminal_file_link<'a>( - writer: &mut impl CountedWrite<'a>, - path: &str, - canonical_path: &str, +pub fn file_path<'a, W: CountedWrite<'a>>( + writer: &mut W, color: Color, + f: impl FnOnce(&mut W) -> io::Result<()>, ) -> io::Result<()> { writer .stdout() .queue(SetForegroundColor(color))? .queue(SetAttribute(Attribute::Underlined))?; - writer.stdout().write_all(b"\x1b]8;;file://")?; - writer.stdout().write_all(canonical_path.as_bytes())?; - writer.stdout().write_all(b"\x1b\\")?; - // Only this part is visible. - writer.write_str(path)?; - writer.stdout().write_all(b"\x1b]8;;\x1b\\")?; + + f(writer)?; + writer .stdout() .queue(SetForegroundColor(Color::Reset))? @@ -296,6 +292,19 @@ pub fn terminal_file_link<'a>( Ok(()) } +pub fn terminal_file_link<'a>( + writer: &mut impl CountedWrite<'a>, + path: &str, + canonical_path: &str, +) -> io::Result<()> { + writer.stdout().write_all(b"\x1b]8;;file://")?; + writer.stdout().write_all(canonical_path.as_bytes())?; + writer.stdout().write_all(b"\x1b\\")?; + // Only this part is visible. + writer.write_str(path)?; + writer.stdout().write_all(b"\x1b]8;;\x1b\\") +} + pub fn write_ansi(output: &mut Vec, command: impl Command) { struct FmtWriter<'a>(&'a mut Vec); diff --git a/src/watch/state.rs b/src/watch/state.rs index 2413becd..a92dd2d6 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -233,7 +233,7 @@ impl<'a> WatchState<'a> { stdout.write_all(b"\n")?; if let DoneStatus::DoneWithSolution(solution_path) = &self.done_status { - solution_link_line(stdout, solution_path)?; + solution_link_line(stdout, solution_path, self.app_state.emit_file_links())?; } stdout.write_all( @@ -252,7 +252,7 @@ impl<'a> WatchState<'a> { stdout.write_all(b"\nCurrent exercise: ")?; self.app_state .current_exercise() - .terminal_file_link(stdout)?; + .terminal_file_link(stdout, self.app_state.emit_file_links())?; stdout.write_all(b"\n\n")?; self.show_prompt(stdout)?;