Skip to main content

next_napi_bindings/
code_frame.rs

1use napi::bindgen_prelude::*;
2use napi_derive::napi;
3use next_code_frame::{
4    CodeFrameColorMode, CodeFrameLocation, CodeFrameOptions, Language, Location, render_code_frame,
5};
6
7/// Default max width when the caller doesn't provide one (e.g., no terminal).
8///
9/// When output is captured to a log file or build container, there is no
10/// terminal width to read. A log/build-log viewer can soft-wrap or scroll, so
11/// the cost of wrapping too narrow (scattering the caret line away from the
12/// code) is worse than being a bit wide. We pick a generous value — roughly 2x
13/// a typical editor width — that fits nearly all hand-written source, while
14/// still bounding per-frame output so a minified/generated line can't dump
15/// kilobytes into the log.
16const DEFAULT_MAX_WIDTH: u32 = 240;
17
18#[napi(object)]
19pub struct NapiLocation {
20    pub line: u32,
21    pub column: Option<u32>,
22}
23
24impl From<NapiLocation> for Location {
25    fn from(loc: NapiLocation) -> Self {
26        Location {
27            line: loc.line as usize,
28            column: loc.column.map(|c| c as usize),
29        }
30    }
31}
32
33#[napi(object)]
34pub struct NapiCodeFrameLocation {
35    pub start: NapiLocation,
36    pub end: Option<NapiLocation>,
37}
38
39impl From<NapiCodeFrameLocation> for CodeFrameLocation {
40    fn from(loc: NapiCodeFrameLocation) -> Self {
41        CodeFrameLocation {
42            start: loc.start.into(),
43            end: loc.end.map(Into::into),
44        }
45    }
46}
47
48#[napi]
49#[derive(PartialEq, Eq)]
50pub enum NapiCodeFrameColorMode {
51    Error,
52    Warning,
53    Info,
54}
55
56#[napi(object)]
57#[derive(Default)]
58pub struct NapiCodeFrameOptions {
59    /// Number of lines to show above the error (default: 2)
60    pub lines_above: Option<u32>,
61    /// Number of lines to show below the error (default: 3)
62    pub lines_below: Option<u32>,
63    /// Maximum width of the output in columns (default: 240)
64    pub max_width: Option<u32>,
65    /// Whether to use ANSI colors (default: false)
66    pub color: Option<Either<NapiCodeFrameColorMode, bool>>,
67    /// Whether to highlight code syntax (default: follows color)
68    ///
69    /// This might be useful if syntax highlighting is very expensive or known to be useless for
70    /// this file.  The current syntax rules are optimized for javascript but should work well with
71    /// other C-like languages.
72    pub highlight_code: Option<bool>,
73    /// Optional message to display with the code frame
74    pub message: Option<String>,
75    /// Language hint for keyword highlighting: "javascript" (default) or "css"
76    pub language: Option<String>,
77}
78
79fn parse_language(s: &Option<String>) -> Language {
80    match s.as_deref() {
81        Some("css") => Language::Css,
82        _ => Language::JavaScript,
83    }
84}
85
86impl From<NapiCodeFrameOptions> for CodeFrameOptions {
87    fn from(opts: NapiCodeFrameOptions) -> Self {
88        let color = match opts.color {
89            None | Some(Either::B(false)) => CodeFrameColorMode::None,
90            Some(Either::A(NapiCodeFrameColorMode::Error)) | Some(Either::B(true)) => {
91                CodeFrameColorMode::Error
92            }
93            Some(Either::A(NapiCodeFrameColorMode::Warning)) => CodeFrameColorMode::Warning,
94            Some(Either::A(NapiCodeFrameColorMode::Info)) => CodeFrameColorMode::Info,
95        };
96        CodeFrameOptions {
97            lines_above: opts.lines_above.unwrap_or(2) as usize,
98            lines_below: opts.lines_below.unwrap_or(3) as usize,
99            max_width: opts.max_width.unwrap_or(DEFAULT_MAX_WIDTH) as usize,
100            color,
101            highlight_code: opts
102                .highlight_code
103                .unwrap_or(color != CodeFrameColorMode::None),
104            message: opts.message,
105            language: parse_language(&opts.language),
106        }
107    }
108}
109
110/// Renders a code frame showing the location of an error in source code
111///
112/// This is a Rust implementation that replaces Babel's code-frame for better:
113/// - Performance on large files
114/// - Handling of long lines
115/// - Memory efficiency
116///
117/// # Arguments
118/// * `source` - The source code to render
119/// * `location` - The location to highlight (line and column numbers are 1-indexed)
120/// * `options` - Optional configuration
121///
122/// # Returns
123/// The formatted code frame string, or `undefined` if the location is out of
124/// range (e.g., empty source or line number past end of file).
125#[napi]
126pub fn code_frame_columns(
127    source: String,
128    location: NapiCodeFrameLocation,
129    options: Option<NapiCodeFrameOptions>,
130) -> Result<Option<String>> {
131    let code_frame_location: CodeFrameLocation = location.into();
132    let code_frame_options: CodeFrameOptions = options.unwrap_or_default().into();
133
134    render_code_frame(&source, &code_frame_location, &code_frame_options)
135        .map_err(|e| Error::from_reason(format!("Failed to render code frame: {e:?}")))
136}