mod intraline;
mod segment;
mod syntax;
mod tokenize;
use std::collections::HashMap;
pub use intraline::{EmphSpan, emphasis_file};
pub use segment::{AttributedSegment, attributed_segments};
use syntax::syntax_for_paths;
use tokenize::LineTokenizer;
use crate::model::{DiffFile, DiffRowKind};
pub type RowKey = (usize, usize);
pub fn highlight_file(file: &DiffFile) -> HashMap<RowKey, Vec<TokenSpan>> {
let mut out = HashMap::new();
if file.is_binary || file.is_submodule || file.is_mode_only {
return out;
}
let Some(syntax) = syntax_for_paths(file.new_path.as_deref(), file.old_path.as_deref()) else {
return out; };
for (h, hunk) in file.hunks.iter().enumerate() {
let mut old_tk = LineTokenizer::new(syntax);
let mut new_tk = LineTokenizer::new(syntax);
for (r, row) in hunk.rows.iter().enumerate() {
let spans = match row.kind {
DiffRowKind::Context => {
let _ = old_tk.next_line(&row.text);
new_tk.next_line(&row.text)
}
DiffRowKind::Removed => old_tk.next_line(&row.text),
DiffRowKind::Added => new_tk.next_line(&row.text),
};
if !spans.is_empty() {
out.insert((h, r), spans);
}
}
}
out
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TokenKind {
Keyword,
String,
Comment,
Number,
Type,
Function,
Constant,
Operator,
Punctuation,
Variable,
Plain,
}
impl TokenKind {
pub fn as_str(self) -> &'static str {
match self {
TokenKind::Keyword => "keyword",
TokenKind::String => "string",
TokenKind::Comment => "comment",
TokenKind::Number => "number",
TokenKind::Type => "type",
TokenKind::Function => "function",
TokenKind::Constant => "constant",
TokenKind::Operator => "operator",
TokenKind::Punctuation => "punctuation",
TokenKind::Variable => "variable",
TokenKind::Plain => "plain",
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct TokenSpan {
pub start: usize,
pub end: usize,
pub kind: TokenKind,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::{DiffFile, DiffRow, DiffRowKind, FileId, FileStatus, HunkId, ReviewHunk};
fn row(kind: DiffRowKind, text: &str) -> DiffRow {
DiffRow {
kind,
old_line: None,
new_line: None,
text: text.to_owned(),
}
}
fn file_with(new_path: Option<&str>, rows: Vec<DiffRow>) -> DiffFile {
DiffFile {
id: FileId::new("file:a"),
status: FileStatus::Modified,
old_path: new_path.map(str::to_owned),
new_path: new_path.map(str::to_owned),
old_mode: None,
new_mode: None,
old_oid: None,
new_oid: None,
similarity: None,
is_binary: false,
is_submodule: false,
is_mode_only: false,
synthetic: false,
metadata_rows: Vec::new(),
hunks: vec![ReviewHunk {
id: HunkId::new("hunk:1"),
header: "@@ -1,4 +1,4 @@".to_owned(),
old_start: 1,
old_lines: 4,
new_start: 1,
new_lines: 4,
rows,
}],
}
}
fn rust_file_one_hunk() -> DiffFile {
file_with(
Some("a.rs"),
vec![
row(DiffRowKind::Context, "let a = 1;"),
row(DiffRowKind::Removed, "let b = 2;"),
row(DiffRowKind::Added, "let b = 3;"),
row(DiffRowKind::Context, "use x;"),
],
)
}
fn binary_file() -> DiffFile {
let mut f = file_with(Some("a.rs"), Vec::new());
f.is_binary = true;
f
}
fn mode_only_file() -> DiffFile {
let mut f = file_with(Some("a.rs"), Vec::new());
f.is_mode_only = true;
f
}
fn file_with_path(path: &str) -> DiffFile {
file_with(
Some(path),
vec![row(DiffRowKind::Context, "some notes here")],
)
}
fn synthetic_rs_file() -> DiffFile {
let mut f = file_with(
Some("new.rs"),
vec![row(DiffRowKind::Added, "fn main() { let n = 1; }")],
);
f.synthetic = true;
f.status = FileStatus::Added;
f
}
#[test]
fn maps_old_and_new_side_tokens_to_their_rows() {
let file = rust_file_one_hunk();
let map = highlight_file(&file);
let ctx0 = map.get(&(0, 0)).unwrap();
assert!(ctx0.iter().any(|s| s.kind == TokenKind::Keyword)); let removed = map.get(&(0, 1)).unwrap();
assert!(removed.iter().any(|s| s.kind == TokenKind::Keyword));
let added = map.get(&(0, 2)).unwrap();
assert!(added.iter().any(|s| s.kind == TokenKind::Keyword));
}
#[test]
fn binary_and_mode_only_files_get_no_tokens() {
assert!(highlight_file(&binary_file()).is_empty());
assert!(highlight_file(&mode_only_file()).is_empty());
}
#[test]
fn unknown_language_file_gets_no_tokens() {
assert!(highlight_file(&file_with_path("notes.xyzzy")).is_empty());
}
#[test]
fn synthetic_untracked_file_is_highlighted() {
assert!(!highlight_file(&synthetic_rs_file()).is_empty());
}
#[test]
fn token_kind_renders_lowercase_wire_str() {
assert_eq!(TokenKind::Keyword.as_str(), "keyword");
assert_eq!(TokenKind::Punctuation.as_str(), "punctuation");
assert_eq!(TokenKind::Plain.as_str(), "plain");
}
#[test]
fn token_span_holds_byte_range_and_kind() {
let s = TokenSpan {
start: 0,
end: 3,
kind: TokenKind::Keyword,
};
assert_eq!((s.start, s.end), (0, 3));
assert_eq!(s.kind, TokenKind::Keyword);
}
}