Skip to content

Commit 907afad

Browse files
max-sixtyclaude
andauthored
feat(styling): add TOML syntax highlighting for gutter output (#905)
* feat(styling): add TOML syntax highlighting for gutter output Add format_toml() with syntax highlighting for config previews: - Table headers [section] in cyan bold - String values in green - Comments dimmed Update show-theme to demonstrate all three gutter styles: - Error details (plain text) - Config (TOML highlighted) - Shell code (bash highlighted) Co-Authored-By: Claude <noreply@anthropic.com> * fix(styling): add trailing newline after format_toml in config show The format_toml change to remove trailing newlines broke config show output. Use writeln! instead of write! to restore the blank line between sections. Co-Authored-By: Claude <noreply@anthropic.com> * refactor(styling): simplify format_toml token handling Merge TokOpt::Some and TokOpt::None branches to share the unstyled path. Synoptic's TOML highlighter returns TokOpt::None for unstyled content (keys, operators, whitespace), not TokOpt::Some with unrecognized types. Add test verifying both styled and unstyled paths are exercised. Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 54e9ee0 commit 907afad

File tree

5 files changed

+91
-38
lines changed

5 files changed

+91
-38
lines changed

src/commands/config/show.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -402,7 +402,7 @@ fn render_user_config(out: &mut String) -> anyhow::Result<()> {
402402
}
403403

404404
// Display TOML with syntax highlighting (gutter at column 0)
405-
write!(out, "{}", format_toml(&contents))?;
405+
writeln!(out, "{}", format_toml(&contents))?;
406406

407407
Ok(())
408408
}
@@ -523,7 +523,7 @@ fn render_project_config(out: &mut String) -> anyhow::Result<()> {
523523
}
524524

525525
// Display TOML with syntax highlighting (gutter at column 0)
526-
write!(out, "{}", format_toml(&contents))?;
526+
writeln!(out, "{}", format_toml(&contents))?;
527527

528528
Ok(())
529529
}

src/commands/configure_shell.rs

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ use anstyle::Style;
77
use worktrunk::path::format_path_for_display;
88
use worktrunk::shell::{self, Shell};
99
use worktrunk::styling::{
10-
INFO_SYMBOL, SUCCESS_SYMBOL, eprint, eprintln, format_bash_with_gutter, format_with_gutter,
11-
prompt_message, warning_message,
10+
INFO_SYMBOL, SUCCESS_SYMBOL, eprint, eprintln, format_bash_with_gutter, format_toml,
11+
format_with_gutter, prompt_message, warning_message,
1212
};
1313

1414
use crate::output::prompt::{PromptResponse, prompt_yes_no_preview};
@@ -1246,19 +1246,25 @@ pub fn handle_show_theme() {
12461246

12471247
eprintln!();
12481248

1249-
// Gutter - quoted content
1250-
eprintln!("{}", info_message("Gutter formatting (quoted content):"));
1249+
// Gutter - error details (plain text, no syntax highlighting)
1250+
eprintln!("{}", info_message("Gutter formatting (error details):"));
12511251
eprintln!(
12521252
"{}",
1253-
format_with_gutter(
1254-
"[commit-generation]\ncommand = \"llm --model claude\"",
1255-
None,
1256-
)
1253+
format_with_gutter("expected `=`, found newline at line 3 column 1", None,)
12571254
);
12581255

12591256
eprintln!();
12601257

1261-
// Gutter - bash code
1258+
// Gutter - TOML config (syntax highlighted)
1259+
eprintln!("{}", info_message("Gutter formatting (config):"));
1260+
eprintln!(
1261+
"{}",
1262+
format_toml("[commit.generation]\ncommand = \"llm --model claude\"")
1263+
);
1264+
1265+
eprintln!();
1266+
1267+
// Gutter - bash code (syntax highlighted)
12621268
eprintln!("{}", info_message("Gutter formatting (shell code):"));
12631269
eprintln!(
12641270
"{}",

src/output/commit_generation.rs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,7 @@ use std::io::{self, IsTerminal};
88

99
use color_print::cformat;
1010
use worktrunk::config::UserConfig;
11-
use worktrunk::styling::{
12-
eprintln, format_with_gutter, hint_message, info_message, success_message,
13-
};
11+
use worktrunk::styling::{eprintln, format_toml, hint_message, info_message, success_message};
1412

1513
use super::prompt::{PromptResponse, prompt_yes_no_preview};
1614

@@ -146,7 +144,7 @@ pub fn prompt_commit_generation(config: &mut UserConfig) -> anyhow::Result<bool>
146144
"Would add to <bold>~/.config/worktrunk/config.toml</>:"
147145
))
148146
);
149-
eprintln!("{}", format_with_gutter(&config_preview, None));
147+
eprintln!("{}", format_toml(&config_preview));
150148
eprintln!();
151149
},
152150
)?;
@@ -168,7 +166,7 @@ pub fn prompt_commit_generation(config: &mut UserConfig) -> anyhow::Result<bool>
168166

169167
// Show what was added
170168
eprintln!("{}", success_message(cformat!("Added to user config:")));
171-
eprintln!("{}", format_with_gutter(&config_preview, None));
169+
eprintln!("{}", format_toml(&config_preview));
172170
eprintln!(
173171
"{}",
174172
hint_message(cformat!("View config: <bright-black>wt config show</>"))

src/styling/highlighting.rs

Lines changed: 62 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@ pub(super) fn bash_token_style(kind: &str) -> Option<Style> {
8787
// ============================================================================
8888

8989
/// Formats TOML content with syntax highlighting using synoptic
90+
///
91+
/// Returns formatted output without trailing newline (consistent with format_with_gutter
92+
/// and format_bash_with_gutter).
9093
pub fn format_toml(content: &str) -> String {
9194
// synoptic has built-in TOML support, so this always succeeds
9295
let mut highlighter = from_extension("toml", 4).expect("synoptic supports TOML");
@@ -97,29 +100,31 @@ pub fn format_toml(content: &str) -> String {
97100
highlighter.run(&lines);
98101

99102
// Render each line with gutter and appropriate styling
100-
let mut output = String::new();
101-
for (y, line) in lines.iter().enumerate() {
102-
output.push_str(&format!("{gutter} {gutter:#} "));
103-
104-
for token in highlighter.line(y, line) {
105-
match token {
106-
TokOpt::Some(text, kind) => {
107-
if let Some(s) = toml_token_style(&kind) {
108-
output.push_str(&format!("{s}{text}{s:#}"));
109-
} else {
110-
output.push_str(&text);
111-
}
112-
}
113-
TokOpt::None(text) => {
114-
output.push_str(&text);
103+
// Build lines without trailing newline - caller is responsible for element separation
104+
let output_lines: Vec<String> = lines
105+
.iter()
106+
.enumerate()
107+
.map(|(y, line)| {
108+
let mut line_output = format!("{gutter} {gutter:#} ");
109+
110+
for token in highlighter.line(y, line) {
111+
let (text, style) = match token {
112+
TokOpt::Some(text, kind) => (text, toml_token_style(&kind)),
113+
TokOpt::None(text) => (text, None),
114+
};
115+
116+
if let Some(s) = style {
117+
line_output.push_str(&format!("{s}{text}{s:#}"));
118+
} else {
119+
line_output.push_str(&text);
115120
}
116121
}
117-
}
118122

119-
output.push('\n');
120-
}
123+
line_output
124+
})
125+
.collect();
121126

122-
output
127+
output_lines.join("\n")
123128
}
124129

125130
/// Maps TOML token kinds to anstyle styles
@@ -299,6 +304,44 @@ mod tests {
299304
assert!(result.lines().count() >= 2);
300305
}
301306

307+
#[test]
308+
fn test_format_toml_has_styled_and_unstyled_text() {
309+
// This test verifies that format_toml handles both styled tokens (string, table)
310+
// and unstyled text (TokOpt::None for whitespace, punctuation)
311+
use synoptic::{TokOpt, from_extension};
312+
313+
let content = "key = \"value\"";
314+
let mut highlighter = from_extension("toml", 4).expect("synoptic supports TOML");
315+
let lines: Vec<String> = content.lines().map(|s| s.to_string()).collect();
316+
highlighter.run(&lines);
317+
318+
// Collect token types
319+
let mut has_styled = false;
320+
let mut has_unstyled = false;
321+
for (y, line) in lines.iter().enumerate() {
322+
for token in highlighter.line(y, line) {
323+
match token {
324+
TokOpt::Some(_, kind) => {
325+
if toml_token_style(&kind).is_some() {
326+
has_styled = true;
327+
}
328+
}
329+
TokOpt::None(_) => {
330+
has_unstyled = true;
331+
}
332+
}
333+
}
334+
}
335+
336+
// Should have styled token (the string "value")
337+
assert!(has_styled, "Should have at least one styled token");
338+
// Should have unstyled text (whitespace, "=", "key")
339+
assert!(
340+
has_unstyled,
341+
"Should have at least one unstyled text segment"
342+
);
343+
}
344+
302345
#[test]
303346
fn test_format_toml_multiline() {
304347
let content = "[table]\nkey1 = \"value1\"\nkey2 = 42\n# comment\nkey3 = false";

tests/integration_tests/snapshots/integration__integration_tests__config_show_theme__show_theme.snap

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,13 @@ info:
1212
GIT_EDITOR: ""
1313
LANG: C
1414
LC_ALL: C
15+
PSModulePath: ""
1516
RUST_LOG: warn
1617
SHELL: ""
1718
TERM: alacritty
1819
WORKTRUNK_CONFIG_PATH: /nonexistent/test/config.toml
20+
WORKTRUNK_TEST_CLAUDE_INSTALLED: "0"
21+
WORKTRUNK_TEST_POWERSHELL_ENV: "0"
1922
WORKTRUNK_TEST_SKIP_URL_HEALTH_CHECK: "1"
2023
WT_TEST_DELAYED_STREAM_MS: "-1"
2124
WT_TEST_EPOCH: "1735776000"
@@ -32,9 +35,12 @@ exit_code: 0
3235
↳ To rebase onto main, run wt merge
3336
○ Showing 5 worktrees
3437

35-
○ Gutter formatting (quoted content):
36-
  [commit-generation]
37-
  command = "llm --model claude"
38+
○ Gutter formatting (error details):
39+
  expected `=`, found newline at line 3 column 1
40+
41+
○ Gutter formatting (config):
42+
  [commit.generation]
43+
  command = "llm --model claude"
3844

3945
○ Gutter formatting (shell code):
4046
  eval "$(wt config shell init bash)"

0 commit comments

Comments
 (0)