Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
137 changes: 122 additions & 15 deletions src/ui/components.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ use ratatui::{

use crate::db::SslMode;
use crate::ui::{
is_sql_keyword, is_sql_type, App, Focus, SidebarTab, StatusType, Theme, SPINNER_FRAMES,
is_sql_function, is_sql_keyword, is_sql_type, App, Focus, SidebarTab, StatusType, Theme,
SPINNER_FRAMES,
};

pub fn draw(frame: &mut Frame, app: &App) {
Expand Down Expand Up @@ -339,6 +340,27 @@ fn draw_editor(frame: &mut Frame, app: &App, area: Rect) {
}
}

/// Determine if a line starts inside a block comment by scanning all previous lines.
fn is_in_block_comment(lines: &[String], current_line: usize) -> bool {
let mut depth = 0i32;
for line in lines.iter().take(current_line) {
let chars: Vec<char> = line.chars().collect();
let mut i = 0;
while i < chars.len() {
if i + 1 < chars.len() && chars[i] == '/' && chars[i + 1] == '*' {
depth += 1;
i += 2;
} else if i + 1 < chars.len() && chars[i] == '*' && chars[i + 1] == '/' {
depth = (depth - 1).max(0);
i += 2;
} else {
i += 1;
}
}
}
depth > 0
}

fn highlight_sql_line<'a>(
line: &'a str,
theme: &Theme,
Expand All @@ -350,7 +372,11 @@ fn highlight_sql_line<'a>(
let mut current_word = String::new();
let mut in_string = false;
let mut string_char = '"';
let in_comment = false;
let mut in_block_comment = is_in_block_comment(&editor.lines, line_number);

let chars: Vec<char> = line.chars().collect();
let len = chars.len();
let mut i = 0;

// Show a gutter marker for the active query block
if in_active_query {
Expand All @@ -365,16 +391,19 @@ fn highlight_sql_line<'a>(
));
}

for (i, c) in line.char_indices() {
// Check for selection
while i < len {
let c = chars[i];
// Compute byte index for selection check
let byte_idx: usize = chars[..i].iter().map(|ch| ch.len_utf8()).sum();

let is_selected = if let Some(((start_x, start_y), (end_x, end_y))) = editor.get_selection()
{
if start_y == end_y && line_number == start_y {
i >= start_x && i < end_x
byte_idx >= start_x && byte_idx < end_x
} else if line_number == start_y {
i >= start_x
byte_idx >= start_x
} else if line_number == end_y {
i < end_x
byte_idx < end_x
} else {
line_number > start_y && line_number < end_y
}
Expand All @@ -388,22 +417,61 @@ fn highlight_sql_line<'a>(
Style::default()
};

// Handle comments
if !in_string && line[i..].starts_with("--") {
// Handle block comments
if in_block_comment {
if i + 1 < len && c == '*' && chars[i + 1] == '/' {
spans.push(Span::styled(
"*/".to_string(),
base_style.fg(theme.syntax_comment),
));
in_block_comment = false;
i += 2;
} else {
spans.push(Span::styled(
c.to_string(),
base_style.fg(theme.syntax_comment),
));
i += 1;
}
continue;
}

// Start block comment
if !in_string && i + 1 < len && c == '/' && chars[i + 1] == '*' {
if !current_word.is_empty() {
spans.push(create_word_span(&current_word, theme, base_style));
current_word.clear();
}
spans.push(Span::styled(
line[i..].to_string(),
"/*".to_string(),
base_style.fg(theme.syntax_comment),
));
in_block_comment = true;
i += 2;
continue;
}

// Handle line comments
if !in_string && i + 1 < len && c == '-' && chars[i + 1] == '-' {
if !current_word.is_empty() {
spans.push(create_word_span(&current_word, theme, base_style));
current_word.clear();
}
let rest: String = chars[i..].iter().collect();
spans.push(Span::styled(rest, base_style.fg(theme.syntax_comment)));
break;
}

// Handle strings
if (c == '\'' || c == '"') && !in_comment {
if (c == '\'' || c == '"') && !in_block_comment {
if in_string && c == string_char {
// Check for escaped quotes ('')
if c == '\'' && i + 1 < len && chars[i + 1] == '\'' {
current_word.push(c);
current_word.push(c);
i += 2;
continue;
}
current_word.push(c);
spans.push(Span::styled(
current_word.clone(),
Expand All @@ -422,14 +490,46 @@ fn highlight_sql_line<'a>(
} else {
current_word.push(c);
}
i += 1;
continue;
}

if in_string {
current_word.push(c);
i += 1;
continue;
}

// Handle PostgreSQL operators: ::, ->, ->>, #>, #>>, @>, <@, ?|, ?&, ||
if i + 1 < len {
let two_char: String = chars[i..i + 2].iter().collect();
let is_pg_operator = matches!(
two_char.as_str(),
"::" | "->" | "#>" | "@>" | "<@" | "?|" | "?&" | "||" | "!=" | "<>" | ">=" | "<="
);
if is_pg_operator {
if !current_word.is_empty() {
spans.push(create_word_span(&current_word, theme, base_style));
current_word.clear();
}
// Check for 3-char operators: ->>, #>>
if i + 2 < len {
let three_char: String = chars[i..i + 3].iter().collect();
if matches!(three_char.as_str(), "->>" | "#>>") {
spans.push(Span::styled(
three_char,
base_style.fg(theme.syntax_operator),
));
i += 3;
continue;
}
}
spans.push(Span::styled(two_char, base_style.fg(theme.syntax_operator)));
i += 2;
continue;
}
}

// Handle word boundaries
if c.is_alphanumeric() || c == '_' {
current_word.push(c);
Expand All @@ -443,19 +543,24 @@ fn highlight_sql_line<'a>(
let style = match c {
'(' | ')' | '[' | ']' | '{' | '}' => base_style.fg(theme.text_primary),
',' | ';' => base_style.fg(theme.text_secondary),
'=' | '>' | '<' | '!' | '+' | '-' | '*' | '/' | '%' => {
base_style.fg(theme.syntax_operator)
}
'=' | '>' | '<' | '!' | '+' | '-' | '*' | '/' | '%' | '~' | '&' | '|' | '^'
| '#' | '@' | '?' => base_style.fg(theme.syntax_operator),
':' => base_style.fg(theme.syntax_operator),
'.' => base_style.fg(theme.text_muted),
_ => base_style.fg(theme.text_primary),
};
spans.push(Span::styled(c.to_string(), style));
}

i += 1;
}

// Handle remaining word
if !current_word.is_empty() {
let style = if in_string {
Style::default().fg(theme.syntax_string)
} else if in_block_comment {
Style::default().fg(theme.syntax_comment)
} else {
Style::default()
};
Expand All @@ -470,9 +575,11 @@ fn create_word_span<'a>(word: &str, theme: &Theme, base_style: Style) -> Span<'a
base_style
.fg(theme.syntax_keyword)
.add_modifier(Modifier::BOLD)
} else if is_sql_function(word) {
base_style.fg(theme.syntax_function)
} else if is_sql_type(word) {
base_style.fg(theme.syntax_type)
} else if word.chars().all(|c| c.is_ascii_digit() || c == '.') {
} else if word.chars().all(|c| c.is_ascii_digit() || c == '.') && !word.is_empty() {
base_style.fg(theme.syntax_number)
} else {
base_style.fg(theme.text_primary)
Expand Down
Loading