Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
725bef9
Be defensive rendering code frames
lukesandberg Oct 30, 2025
1563ca6
Add next-code-frame crate with core rendering logic
lukesandberg Oct 31, 2025
538cc95
Fix multiline error markers and add column validation tests
lukesandberg Oct 31, 2025
8f4e827
Add spanning markers for multiline errors
lukesandberg Oct 31, 2025
67a350f
Simplify marker rendering logic with upfront normalization
lukesandberg Oct 31, 2025
9ec6a8c
Simplify marker column calculation with saturating arithmetic
lukesandberg Oct 31, 2025
d7a6dbe
Optimize repeated character output to avoid allocations
lukesandberg Oct 31, 2025
a244a43
Extract repeat_char_into helper for efficient character repetition
lukesandberg Oct 31, 2025
1204575
Apply clippy suggestions for idiomatic Rust
lukesandberg Oct 31, 2025
0c5a4a5
Fix some more clippy issues
lukesandberg Oct 31, 2025
427ae1c
Extract helper functions for line truncation and marker calculation
lukesandberg Oct 31, 2025
7dcd5e5
Clarify end_column semantics with detailed comments
lukesandberg Oct 31, 2025
6ece5d4
Convert code frame API to nested Location structure
lukesandberg Oct 31, 2025
873d84b
Migrate tests to use insta snapshot testing
lukesandberg Oct 31, 2025
fa68205
use inline snapshots
lukesandberg Oct 31, 2025
d630d5b
Implement Phase 4: Syntax highlighting architecture with OXC
lukesandberg Oct 31, 2025
d45da75
Phase 4: Fix swc_ecma_lexer import errors
lukesandberg Oct 31, 2025
e861779
Phase 4: Add syntax highlighting tests and fix BytePos offset
lukesandberg Oct 31, 2025
0f9bf70
Phase 4: Integrate syntax highlighting into code frame rendering
lukesandberg Oct 31, 2025
b4c11c5
Add syntax highlighting demo example
lukesandberg Oct 31, 2025
0838c8e
Add comments and punctuation highlighting matching Babel
lukesandberg Oct 31, 2025
597c3d7
Add comprehensive syntax highlighting demo
lukesandberg Oct 31, 2025
125dbaf
Move strip_ansi_codes to test module and enable highlighting in all t…
lukesandberg Oct 31, 2025
2376fe2
Refactor highlighting to use TokenAndSpan.had_line_break and simplify…
lukesandberg Oct 31, 2025
6d919d6
Optimize highlighting: use had_line_break and line_bounds API
lukesandberg Oct 31, 2025
7006eb0
Optimize highlight.rs to only produce markers for visible lines
lukesandberg Nov 1, 2025
babdb85
Add NAPI bindings for next-code-frame
lukesandberg Nov 1, 2025
814b58d
Fix the napi bindings and support wasm as well.
lukesandberg Nov 4, 2025
8e9cbdd
more async hacks
lukesandberg Nov 4, 2025
e8d5fc3
Make code frame rendering synchronous
lukesandberg Nov 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Add comments and punctuation highlighting matching Babel
Added comprehensive token highlighting to match Babel's behavior:

Comments Support:
- Integrated SWC's SingleThreadedComments API
- Extract both leading and trailing comments
- Supports single-line (//) and multi-line (/* */) comments
- Comments styled in gray matching Babel

Punctuation Support:
- All punctuation tokens highlighted in yellow (Babel style)
- Includes: = ; , . ... => : ? + - * / % operators etc
- Brackets ()[]{}  remain unstyled (matching Babel)
- Added @ # $ { for template literals

Token Classification:
- Keywords (cyan): const, let, var, if, etc.
- Identifiers (yellow): variable names
- Strings (green): "...", '...', template literals
- Numbers (magenta): 123, 0x42, bigints
- Regex (magenta): /pattern/flags
- Punctuation (yellow): all non-bracket punctuation
- Comments (gray): // and /* */
- Brackets: unstyled

Tests Added:
- test_comments_and_punctuation: Verifies inline comments and operators
- test_multiline_comment: Verifies multi-line comment handling

Demo:
- comments_punctuation_demo.rs: Shows comments and punctuation highlighting

All 31 tests pass. Highlighting now matches Babel's babel-code-frame.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
  • Loading branch information
lukesandberg and claude committed Nov 8, 2025
commit 0838c8eb28811052849bbd81b38e6690ce5db960
103 changes: 0 additions & 103 deletions crates/next-code-frame/COLUMN_SEMANTICS.md

This file was deleted.

27 changes: 27 additions & 0 deletions crates/next-code-frame/examples/comments_punctuation_demo.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
use next_code_frame::{CodeFrameLocation, CodeFrameOptions, Location, render_code_frame};

fn main() {
let source = r#"// This is a comment
const x = 42; // inline comment
const obj = { foo: 'bar' };
/* Multi-line
comment */
const result = x > 10 ? 'yes' : 'no';"#;

let location = CodeFrameLocation {
start: Location { line: 2, column: 1 },
end: Some(Location {
line: 2,
column: 14, // Mark "const x = 42;"
}),
};

println!("=== With Syntax Highlighting (showing comments and punctuation) ===");
let options = CodeFrameOptions {
use_colors: true,
highlight_code: true,
..Default::default()
};
let result = render_code_frame(source, &location, &options).unwrap();
println!("{}", result);
}
121 changes: 112 additions & 9 deletions crates/next-code-frame/src/highlight.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use swc_common::{SourceMap, Span};
use swc_common::{SourceMap, Span, comments::SingleThreadedComments};
use swc_ecma_lexer::{Lexer, StringInput, Syntax, TsSyntax, token::Token};

/// A style marker at a specific byte offset in the source
Expand Down Expand Up @@ -127,9 +127,12 @@ pub fn extract_highlights(source: &str) -> Vec<LineHighlight> {
..Default::default()
});

// Create lexer
// Create comments handler to capture comments
let comments = SingleThreadedComments::default();

// Create lexer with comments enabled
let input = StringInput::from(&*fm);
let mut lexer = Lexer::new(syntax, Default::default(), input, None);
let mut lexer = Lexer::new(syntax, Default::default(), input, Some(&comments));

// Lex all tokens and extract style markers
let mut markers = Vec::new();
Expand All @@ -153,6 +156,24 @@ pub fn extract_highlights(source: &str) -> Vec<LineHighlight> {
}
}

// Add comment markers
// SWC stores comments separately - we need to extract them from the comments handler
let (leading, trailing) = comments.borrow_all();

// Process leading comments
for (_pos, comment_vec) in leading.iter() {
for comment in comment_vec {
add_token_markers(&mut markers, comment.span, TokenType::Comment);
}
}

// Process trailing comments
for (_pos, comment_vec) in trailing.iter() {
for comment in comment_vec {
add_token_markers(&mut markers, comment.span, TokenType::Comment);
}
}

// Sort markers by offset
markers.sort();

Expand All @@ -165,10 +186,10 @@ fn classify_token(token: &Token) -> Option<TokenType> {
match token {
// Keywords
Token::Word(word) => match word {
swc_ecma_lexer::token::Word::Keyword(_) => Some(TokenType::Keyword),
swc_ecma_lexer::token::Word::Null
| swc_ecma_lexer::token::Word::True
| swc_ecma_lexer::token::Word::False => Some(TokenType::Keyword),
| swc_ecma_lexer::token::Word::False
| swc_ecma_lexer::token::Word::Keyword(_) => Some(TokenType::Keyword),
swc_ecma_lexer::token::Word::Ident(_) => Some(TokenType::Identifier),
},

Expand All @@ -180,10 +201,36 @@ fn classify_token(token: &Token) -> Option<TokenType> {
// JSX
Token::JSXTagStart | Token::JSXTagEnd => Some(TokenType::JsxTag),

// Comments - SWC lexer doesn't emit comments by default
// We'd need to enable them in lexer config if needed

// Skip punctuation and other tokens for now
// Brackets - leave unstyled like Babel does
Token::LParen
| Token::RParen
| Token::LBrace
| Token::RBrace
| Token::LBracket
| Token::RBracket => None,

// Punctuation - all other punctuation tokens (yellow in Babel)
// This includes: ; , . ... => : ? operators @ # etc
Token::Arrow
| Token::Dot
| Token::DotDotDot
| Token::Semi
| Token::Comma
| Token::Colon
| Token::QuestionMark
| Token::BinOp(_)
| Token::AssignOp(_)
| Token::Bang
| Token::Tilde
| Token::PlusPlus
| Token::MinusMinus
| Token::Hash
| Token::At
| Token::BackQuote
| Token::DollarLBrace => Some(TokenType::Punctuation),

// Comments are handled separately via the comments API
// Everything else is left unstyled
_ => None,
}
}
Expand Down Expand Up @@ -501,4 +548,60 @@ mod tests {
// There should be at least one start marker in the visible range
assert!(adjusted.markers.iter().any(|m| m.is_start));
}

#[test]
fn test_comments_and_punctuation() {
let source = "const x = 42; // comment\nobj.foo = 10;";
let highlights = extract_highlights(source);

// Should have 2 lines
assert_eq!(highlights.len(), 2);

// First line should have comment markers
let line1_has_comment = highlights[0]
.markers
.iter()
.any(|m| m.token_type == TokenType::Comment);
assert!(line1_has_comment, "First line should have comment markers");

// Both lines should have punctuation markers (=, ;, .)
let line1_has_punctuation = highlights[0]
.markers
.iter()
.any(|m| m.token_type == TokenType::Punctuation);
let line2_has_punctuation = highlights[1]
.markers
.iter()
.any(|m| m.token_type == TokenType::Punctuation);
assert!(
line1_has_punctuation,
"First line should have punctuation markers"
);
assert!(
line2_has_punctuation,
"Second line should have punctuation markers"
);
}

#[test]
fn test_multiline_comment() {
let source = "const x = 1;\n/* multi\n line */\nconst y = 2;";
let highlights = extract_highlights(source);

// Should have 4 lines
assert_eq!(highlights.len(), 4);

// Lines 2 and 3 should have comment markers
let line2_has_comment = highlights[1]
.markers
.iter()
.any(|m| m.token_type == TokenType::Comment);
let line3_has_comment = highlights[2]
.markers
.iter()
.any(|m| m.token_type == TokenType::Comment);

assert!(line2_has_comment, "Line 2 should have comment marker");
assert!(line3_has_comment, "Line 3 should have comment marker");
}
}