Skip to content

Commit 37485b2

Browse files
committed
add syntax highlighting
1 parent cde686a commit 37485b2

4 files changed

Lines changed: 128 additions & 5 deletions

File tree

src/conversation.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ Your goal is to help users with coding tasks efficiently and accurately."#,
105105

106106
pub fn restore_messages(&mut self, messages: Vec<Message>) {
107107
self.messages = messages;
108+
self.trim_if_needed();
108109
}
109110

110111
pub fn _len(&self) -> usize {
@@ -131,11 +132,11 @@ mod tests {
131132
fn test_message_limit_trimming() {
132133
let mut history = ConversationHistory::new();
133134

134-
for i in 0..60 {
135+
for i in 0..510 {
135136
history.add_user_message(format!("Message {}", i));
136137
}
137138

138-
assert_eq!(history.messages().len(), 50);
139+
assert_eq!(history.messages().len(), 500);
139140

140141
if let crate::api::MessageContent::Text { content } = &history.messages()[0].content {
141142
assert_eq!(content, "Message 10");
@@ -146,14 +147,14 @@ mod tests {
146147
fn test_message_limit_with_blocks() {
147148
let mut history = ConversationHistory::new();
148149

149-
for i in 0..30 {
150+
for i in 0..260 {
150151
history.add_user_message(format!("User {}", i));
151152
history.add_assistant_with_blocks(vec![MessageContentBlock::Text {
152153
text: format!("Assistant {}", i),
153154
}]);
154155
}
155156

156-
assert_eq!(history.messages().len(), 50);
157+
assert_eq!(history.messages().len(), 500);
157158
}
158159

159160
#[test]

src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ mod error;
55
mod history;
66
mod repl;
77
mod session_selector;
8+
mod syntax;
89
mod tools;
910

1011
use api::MorphClient;

src/repl.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use crate::api::{AnthropicClient, ContentBlock, CreateMessageRequest, MorphClien
22
use crate::conversation::ConversationHistory;
33
use crate::error::{Result, SofosError};
44
use crate::history::HistoryManager;
5+
use crate::syntax::SyntaxHighlighter;
56
use crate::tools::{add_code_search_tool, get_tools, get_tools_with_morph, ToolExecutor};
67
use colored::Colorize;
78
use rustyline::error::ReadlineError;
@@ -18,6 +19,7 @@ pub struct Repl {
1819
conversation: ConversationHistory,
1920
tool_executor: ToolExecutor,
2021
history_manager: HistoryManager,
22+
highlighter: SyntaxHighlighter,
2123
editor: DefaultEditor,
2224
model: String,
2325
max_tokens: u32,
@@ -47,11 +49,14 @@ impl Repl {
4749

4850
let session_id = HistoryManager::generate_session_id();
4951

52+
let highlighter = SyntaxHighlighter::new();
53+
5054
Ok(Self {
5155
client,
5256
conversation,
5357
tool_executor,
5458
history_manager,
59+
highlighter,
5560
editor,
5661
model,
5762
max_tokens,
@@ -221,7 +226,8 @@ impl Repl {
221226
if !text_output.is_empty() {
222227
println!("{}", "Assistant:".bright_blue().bold());
223228
for text in &text_output {
224-
println!("{}", text);
229+
let highlighted = self.highlighter.highlight_text(text);
230+
println!("{}", highlighted);
225231
}
226232
println!();
227233
}

src/syntax.rs

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
use colored::Colorize;
2+
use syntect::easy::HighlightLines;
3+
use syntect::highlighting::{Style, ThemeSet};
4+
use syntect::parsing::SyntaxSet;
5+
use syntect::util::as_24_bit_terminal_escaped;
6+
7+
pub struct SyntaxHighlighter {
8+
syntax_set: SyntaxSet,
9+
theme_set: ThemeSet,
10+
}
11+
12+
impl SyntaxHighlighter {
13+
pub fn new() -> Self {
14+
Self {
15+
syntax_set: SyntaxSet::load_defaults_newlines(),
16+
theme_set: ThemeSet::load_defaults(),
17+
}
18+
}
19+
20+
pub fn highlight_text(&self, text: &str) -> String {
21+
let mut result = String::new();
22+
let mut in_code_block = false;
23+
let mut code_block = String::new();
24+
let mut language = String::new();
25+
26+
for line in text.lines() {
27+
if line.starts_with("```") {
28+
if in_code_block {
29+
result.push_str(&self.highlight_code(&code_block, &language));
30+
result.push('\n');
31+
code_block.clear();
32+
language.clear();
33+
in_code_block = false;
34+
} else {
35+
language = line.trim_start_matches('`').trim().to_string();
36+
in_code_block = true;
37+
}
38+
} else if in_code_block {
39+
code_block.push_str(line);
40+
code_block.push('\n');
41+
} else {
42+
result.push_str(line);
43+
result.push('\n');
44+
}
45+
}
46+
47+
if in_code_block {
48+
result.push_str(&self.highlight_code(&code_block, &language));
49+
}
50+
51+
result.trim_end().to_string()
52+
}
53+
54+
fn highlight_code(&self, code: &str, language: &str) -> String {
55+
let syntax = if language.is_empty() {
56+
self.syntax_set.find_syntax_plain_text()
57+
} else {
58+
self.syntax_set
59+
.find_syntax_by_token(language)
60+
.or_else(|| self.syntax_set.find_syntax_by_extension(language))
61+
.unwrap_or_else(|| self.syntax_set.find_syntax_plain_text())
62+
};
63+
64+
let theme = &self.theme_set.themes["base16-ocean.dark"];
65+
let mut highlighter = HighlightLines::new(syntax, theme);
66+
67+
let mut result = String::new();
68+
result.push_str(&format!("{}\n", "┌─────".dimmed()));
69+
70+
for line in code.lines() {
71+
let ranges: Vec<(Style, &str)> = highlighter
72+
.highlight_line(line, &self.syntax_set)
73+
.unwrap_or_default();
74+
let escaped = as_24_bit_terminal_escaped(&ranges[..], false);
75+
result.push_str(&format!("{} {}\n", "│".dimmed(), escaped));
76+
}
77+
78+
result.push_str(&format!("{}", "└─────".dimmed()));
79+
result
80+
}
81+
}
82+
83+
impl Default for SyntaxHighlighter {
84+
fn default() -> Self {
85+
Self::new()
86+
}
87+
}
88+
89+
#[cfg(test)]
90+
mod tests {
91+
use super::*;
92+
93+
#[test]
94+
fn test_highlighter_creation() {
95+
let highlighter = SyntaxHighlighter::new();
96+
assert!(!highlighter.syntax_set.syntaxes().is_empty());
97+
}
98+
99+
#[test]
100+
fn test_plain_text() {
101+
let highlighter = SyntaxHighlighter::new();
102+
let text = "Hello, world!";
103+
let result = highlighter.highlight_text(text);
104+
assert!(result.contains("Hello, world!"));
105+
}
106+
107+
#[test]
108+
fn test_code_block_detection() {
109+
let highlighter = SyntaxHighlighter::new();
110+
let text = "Here is some code:\n```rust\nfn main() {}\n```";
111+
let result = highlighter.highlight_text(text);
112+
assert!(result.contains("Here is some code:"));
113+
assert!(result.contains("main"));
114+
}
115+
}

0 commit comments

Comments
 (0)