• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

dcdpr / jp / 21478215222

29 Jan 2026 12:30PM UTC coverage: 53.861% (-0.03%) from 53.886%
21478215222

Pull #385

github

web-flow
Merge eb74d97aa into 38392fea9
Pull Request #385: refactor: switch to `camino` for UTF-8 path handling

48 of 217 new or added lines in 22 files covered. (22.12%)

8 existing lines in 5 files now uncovered.

10728 of 19918 relevant lines covered (53.86%)

117.58 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/crates/jp_cli/src/cmd/query/response_handler.rs
1
use std::{fs, sync::Arc, time::Duration};
2

3
use camino::{FromPathBufError, Utf8PathBuf};
4
use crossterm::style::{Color, Stylize as _};
5
use jp_config::style::{LinkStyle, StyleConfig};
6
use jp_printer::{PrintableExt as _, Printer};
7
use jp_term::{code, osc::hyperlink};
8
use termimad::FmtText;
9

10
use super::{Line, LineVariant, RenderMode};
11
use crate::Error;
12

13
#[derive(Debug)]
14
pub(super) struct ResponseHandler {
15
    /// How to render the response.
16
    pub render_mode: RenderMode,
17

18
    /// Whether to render tool call request/results.
19
    pub render_tool_calls: bool,
20

21
    /// The streamed, unprocessed lines received from the LLM.
22
    received: Vec<String>,
23

24
    pub printer: Arc<Printer>,
25

26
    /// The lines that have been parsed so far.
27
    ///
28
    /// If `should_stream` is `true`, these lines have been printed to the
29
    /// terminal. Otherwise they will be printed when the response handler is
30
    /// finished.
31
    pub parsed: Vec<String>,
32

33
    /// A temporary buffer of data received from the LLM.
34
    pub buffer: String,
35

36
    in_fenced_code_block: bool,
37
    // (language, code)
38
    code_buffer: (Option<String>, Vec<String>),
39
    code_line: usize,
40

41
    // The last index of the line that ends a code block.
42
    // (streamed, printed)
43
    last_fenced_code_block_end: (usize, usize),
44
}
45

46
impl ResponseHandler {
47
    pub fn new(render_mode: RenderMode, render_tool_calls: bool, printer: Arc<Printer>) -> Self {
×
48
        Self {
×
49
            render_mode,
×
50
            render_tool_calls,
×
51
            printer,
×
52
            received: vec![],
×
53
            parsed: vec![],
×
54
            buffer: String::new(),
×
55
            in_fenced_code_block: false,
×
56
            code_buffer: (None, vec![]),
×
57
            code_line: 0,
×
58
            last_fenced_code_block_end: (0, 0),
×
59
        }
×
60
    }
×
61

62
    pub fn drain(&mut self, style: &StyleConfig, raw: bool) -> Result<(), Error> {
×
63
        if self.buffer.is_empty() {
×
64
            return Ok(());
×
65
        }
×
66

67
        let line = Line::new(
×
68
            self.buffer.drain(..).collect(),
×
69
            self.in_fenced_code_block,
×
70
            raw,
×
71
        );
72

73
        self.handle_inner(line, style)
×
74
    }
×
75

76
    fn handle_inner(&mut self, line: Line, style: &StyleConfig) -> Result<(), Error> {
×
77
        let Line { content, variant } = line;
×
78
        self.received.push(content);
×
79

80
        let delay = match variant {
×
81
            LineVariant::Code => style.typewriter.code_delay.into(),
×
82
            LineVariant::Normal => style.typewriter.text_delay.into(),
×
83
            _ => Duration::ZERO,
×
84
        };
85

86
        let lines = self.handle_line(&variant, style)?;
×
87
        if !matches!(self.render_mode, RenderMode::Buffered) {
×
88
            self.printer.print(lines.join("\n").typewriter(delay));
×
89
        }
×
90

91
        self.parsed.extend(lines);
×
92

93
        Ok(())
×
94
    }
×
95

96
    #[expect(clippy::too_many_lines)]
97
    fn handle_line(
×
98
        &mut self,
×
99
        variant: &LineVariant,
×
100
        style: &StyleConfig,
×
101
    ) -> Result<Vec<String>, Error> {
×
102
        let Some(content) = self.received.last().map(String::as_str) else {
×
103
            return Ok(vec![]);
×
104
        };
105

106
        match variant {
×
107
            LineVariant::Raw => Ok(content.lines().map(str::to_owned).collect()),
×
108
            LineVariant::Code => {
109
                self.code_line += 1;
×
110
                self.code_buffer.1.push(content.to_owned());
×
111

112
                let mut buf = String::new();
×
113
                let config = code::Config {
×
114
                    language: self.code_buffer.0.clone(),
×
115
                    theme: style.code.color.then(|| style.code.theme.clone()),
×
116
                };
117

118
                if !code::format(content, &mut buf, &config)? {
×
119
                    let config = code::Config {
×
120
                        language: None,
×
121
                        theme: config.theme,
×
122
                    };
×
123

124
                    code::format(content, &mut buf, &config)?;
×
125
                }
×
126

127
                if style.code.line_numbers {
×
128
                    buf.insert_str(
×
129
                        0,
×
130
                        &format!("{:2} │ ", self.code_line)
×
131
                            .with(Color::AnsiValue(238))
×
132
                            .to_string(),
×
133
                    );
×
134
                }
×
135

136
                Ok(vec![buf])
×
137
            }
138
            LineVariant::FencedCodeBlockStart { language } => {
×
139
                self.code_buffer.0.clone_from(language);
×
140
                self.code_buffer.1.clear();
×
141
                self.code_line = 0;
×
142
                self.in_fenced_code_block = true;
×
143

144
                Ok(vec![content.with(Color::AnsiValue(238)).to_string()])
×
145
            }
146
            LineVariant::FencedCodeBlockEnd { indent } => {
×
147
                self.last_fenced_code_block_end = (self.received.len(), self.parsed.len() + 2);
×
148

149
                let path = self.persist_code_block()?;
×
150
                let mut links = vec![];
×
151

152
                match style.code.file_link {
×
153
                    LinkStyle::Off => {}
×
154
                    LinkStyle::Full => {
×
NEW
155
                        links.push(format!("{}see: {path}", " ".repeat(*indent)));
×
156
                    }
×
157
                    LinkStyle::Osc8 => {
×
158
                        links.push(format!(
×
159
                            "{}[{}]",
×
160
                            " ".repeat(*indent),
×
NEW
161
                            hyperlink(format!("file://{path}"), "open in editor".red().to_string())
×
162
                        ));
×
163
                    }
×
164
                }
165

166
                match style.code.copy_link {
×
167
                    LinkStyle::Off => {}
×
168
                    LinkStyle::Full => {
×
NEW
169
                        links.push(format!("{}copy: copy://{path}", " ".repeat(*indent),));
×
170
                    }
×
171
                    LinkStyle::Osc8 => {
×
172
                        links.push(format!(
×
173
                            "{}[{}]",
×
174
                            " ".repeat(*indent),
×
175
                            hyperlink(
×
NEW
176
                                format!("copy://{path}"),
×
177
                                "copy to clipboard".red().to_string()
×
178
                            )
×
179
                        ));
×
180
                    }
×
181
                }
182

183
                self.in_fenced_code_block = false;
×
184

185
                let mut lines = vec![content.with(Color::AnsiValue(238)).to_string()];
×
186
                if !links.is_empty() {
×
187
                    lines.push(links.join(" "));
×
188
                }
×
189

190
                Ok(lines)
×
191
            }
192
            LineVariant::Normal => {
193
                // We feed all the lines for markdown formatting, but only
194
                // print the last one, as the others are already printed.
195
                //
196
                // This helps the parser to use previous context to apply
197
                // the correct formatting to the current line.
198
                //
199
                // We only care about the lines after the last code block
200
                // end, because a) formatting context is reset after a code
201
                // block, and b) we dot not limit the line length of code, makes
202
                // it impossible to correctly find the non-printed lines based
203
                // on wrapped vs non-wrapped lines.
204
                let lines = self
×
205
                    .received
×
206
                    .iter()
×
207
                    .skip(self.last_fenced_code_block_end.0)
×
208
                    .cloned()
×
209
                    .collect::<Vec<_>>();
×
210

211
                // `termimad` removes empty lines at the start or end, but we
212
                // want to keep them as we will have more lines to print.
213
                let empty_lines_start_count = lines.iter().take_while(|s| s.is_empty()).count();
×
214
                let empty_lines_end_count = lines.iter().rev().take_while(|s| s.is_empty()).count();
×
215

216
                let options = comrak::Options {
×
217
                    render: comrak::options::Render {
×
218
                        r#unsafe: true,
×
219
                        prefer_fenced: true,
×
220
                        experimental_minimize_commonmark: true,
×
221
                        ..Default::default()
×
222
                    },
×
223
                    ..Default::default()
×
224
                };
×
225

226
                let formatted = comrak::markdown_to_commonmark(&lines.join("\n"), &options);
×
227

228
                let mut formatted =
×
229
                    FmtText::from(&termimad::MadSkin::default(), &formatted, Some(100)).to_string();
×
230

231
                for _ in 0..empty_lines_start_count {
×
232
                    formatted.insert(0, '\n');
×
233
                }
×
234

235
                // Only add an extra newline if we have more than one line,
236
                // otherwise a single empty line will be interpreted as both a
237
                // missing start and end newline.
238
                if lines.iter().any(|s| !s.is_empty()) {
×
239
                    for _ in 0..empty_lines_end_count {
×
240
                        formatted.push('\n');
×
241
                    }
×
242
                }
×
243

244
                let lines = formatted
×
245
                    .lines()
×
246
                    .skip(self.parsed.len() - self.last_fenced_code_block_end.1)
×
247
                    .map(ToOwned::to_owned)
×
248
                    .collect::<Vec<_>>();
×
249

250
                Ok(lines)
×
251
            }
252
        }
253
    }
×
254

NEW
255
    fn persist_code_block(&self) -> Result<Utf8PathBuf, Error> {
×
256
        let code = self.code_buffer.1.clone();
×
257
        let language = self.code_buffer.0.as_deref().unwrap_or("txt");
×
258
        let ext = match language {
×
259
            "c++" => "cpp",
×
260
            "javascript" => "js",
×
261
            "python" => "py",
×
262
            "ruby" => "rb",
×
263
            "rust" => "rs",
×
264
            "typescript" => "ts",
×
265
            lang => lang,
×
266
        };
267

268
        let millis = std::time::SystemTime::now()
×
269
            .duration_since(std::time::UNIX_EPOCH)
×
270
            .unwrap_or_default()
×
271
            .subsec_millis();
×
272
        let path = std::env::temp_dir().join(format!("code_{millis}.{ext}"));
×
NEW
273
        let path = Utf8PathBuf::try_from(path).map_err(FromPathBufError::into_io_error)?;
×
274

275
        fs::write(&path, code.join("\n"))?;
×
276

277
        Ok(path)
×
278
    }
×
279
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc