feat: syntax highlighting for diff view
This commit is contained in:
136
src/util/diff.rs
136
src/util/diff.rs
@@ -1,13 +1,8 @@
|
||||
use std::{ops::Range, slice::Iter, sync::Arc, thread::current};
|
||||
use std::sync::Arc;
|
||||
|
||||
use memchr::{memchr2, memchr2_iter, memchr3_iter};
|
||||
use similar::DiffableStr;
|
||||
|
||||
pub(crate) struct Span {
|
||||
pub(crate) op: Op,
|
||||
pub(crate) old_range: Range<usize>,
|
||||
pub(crate) new_range: Range<usize>,
|
||||
}
|
||||
use crate::util;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum Op {
|
||||
@@ -17,39 +12,32 @@ pub(crate) enum Op {
|
||||
Replace,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) enum DiffSide {
|
||||
Old,
|
||||
New,
|
||||
}
|
||||
|
||||
pub(crate) struct DiffRow {
|
||||
pub(crate) op_index: usize,
|
||||
pub(crate) op: Op,
|
||||
pub(crate) old_line: Option<usize>,
|
||||
pub(crate) old_content_range: Option<Range<usize>>,
|
||||
pub(crate) new_line: Option<usize>,
|
||||
pub(crate) new_content_range: Option<Range<usize>>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct DiffLine {
|
||||
pub(crate) op: Op,
|
||||
pub(crate) old_content: Option<Arc<str>>,
|
||||
pub(crate) old_line: usize,
|
||||
pub(crate) old_line: Option<usize>,
|
||||
pub(crate) old_byte_range: std::ops::Range<usize>,
|
||||
pub(crate) new_content: Option<Arc<str>>,
|
||||
pub(crate) new_line: usize,
|
||||
pub(crate) new_line: Option<usize>,
|
||||
pub(crate) new_byte_range: std::ops::Range<usize>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct ContentDiff(Vec<DiffLine>);
|
||||
pub(crate) struct ContentDiff {
|
||||
pub(crate) diff_lines: Vec<DiffLine>,
|
||||
pub(crate) old_content: bytes::Bytes,
|
||||
pub(crate) old_line_count: usize,
|
||||
pub(crate) new_content: bytes::Bytes,
|
||||
pub(crate) new_line_count: usize,
|
||||
}
|
||||
|
||||
pub(crate) fn diff_content(
|
||||
old_content: bytes::Bytes,
|
||||
new_content: bytes::Bytes,
|
||||
) -> Option<ContentDiff> {
|
||||
let old_line_ranges = line_ranges(&old_content);
|
||||
let new_line_ranges = line_ranges(&new_content);
|
||||
let old_line_ranges = util::file::line_ranges(&old_content);
|
||||
let new_line_ranges = util::file::line_ranges(&new_content);
|
||||
let diff = similar::TextDiff::from_lines::<[u8]>(&old_content, &new_content);
|
||||
|
||||
let mut diff_lines: Vec<DiffLine> = Vec::new();
|
||||
@@ -68,28 +56,30 @@ pub(crate) fn diff_content(
|
||||
let content = Arc::from(old_content.slice(old_line_range.clone()).as_str()?);
|
||||
diff_lines.push(DiffLine {
|
||||
op: Op::Equal,
|
||||
old_line,
|
||||
old_line: Some(old_line),
|
||||
old_content: Some(Arc::clone(&content)),
|
||||
new_line,
|
||||
old_byte_range: old_line_range.clone(),
|
||||
new_line: Some(new_line),
|
||||
new_content: Some(content),
|
||||
new_byte_range: new_line_ranges[new_line].clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
| &similar::DiffOp::Insert {
|
||||
old_index,
|
||||
new_index,
|
||||
new_len,
|
||||
new_index, new_len, ..
|
||||
} => {
|
||||
for i in 0..new_len {
|
||||
let new_line_range = &new_line_ranges[new_index + i];
|
||||
let content = Arc::from(new_content.slice(new_line_range.clone()).as_str()?);
|
||||
diff_lines.push(DiffLine {
|
||||
op: Op::Insert,
|
||||
old_line: old_index,
|
||||
old_line: None,
|
||||
old_content: None,
|
||||
new_line: new_index + i,
|
||||
old_byte_range: 0..0,
|
||||
new_line: Some(new_index + i),
|
||||
new_content: Some(content),
|
||||
new_byte_range: new_line_range.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -108,26 +98,32 @@ pub(crate) fn diff_content(
|
||||
{
|
||||
| (Some(old_range), Some(new_range)) => DiffLine {
|
||||
op: Op::Replace,
|
||||
old_line,
|
||||
old_line: Some(old_line),
|
||||
old_content: Some(Arc::from(old_content.slice(old_range.clone()).as_str()?)),
|
||||
new_line: new_index + i,
|
||||
old_byte_range: old_range.clone(),
|
||||
new_line: Some(new_line),
|
||||
new_content: Some(Arc::from(new_content.slice(new_range.clone()).as_str()?)),
|
||||
new_byte_range: new_range.clone(),
|
||||
},
|
||||
|
||||
| (None, Some(new_range)) => DiffLine {
|
||||
op: Op::Replace,
|
||||
old_line: old_index + old_len,
|
||||
old_line: None,
|
||||
old_content: None,
|
||||
new_line: new_index + i,
|
||||
old_byte_range: 0..0,
|
||||
new_line: Some(new_index + i),
|
||||
new_content: Some(Arc::from(new_content.slice(new_range.clone()).as_str()?)),
|
||||
new_byte_range: new_range.clone(),
|
||||
},
|
||||
|
||||
| (Some(old_range), None) => DiffLine {
|
||||
op: Op::Replace,
|
||||
old_line: old_index + i,
|
||||
old_line: Some(old_index + i),
|
||||
old_content: Some(Arc::from(old_content.slice(old_range.clone()).as_str()?)),
|
||||
new_line: new_index + new_len,
|
||||
old_byte_range: old_range.clone(),
|
||||
new_line: None,
|
||||
new_content: None,
|
||||
new_byte_range: 0..0,
|
||||
},
|
||||
|
||||
| (None, None) => {
|
||||
@@ -143,74 +139,44 @@ pub(crate) fn diff_content(
|
||||
}
|
||||
|
||||
| &similar::DiffOp::Delete {
|
||||
old_index,
|
||||
old_len,
|
||||
new_index,
|
||||
old_index, old_len, ..
|
||||
} => {
|
||||
for i in 0..old_len {
|
||||
let old_line_range = &old_line_ranges[old_index];
|
||||
let content = Arc::from(old_content.slice(old_line_range.clone()).as_str()?);
|
||||
diff_lines.push(DiffLine {
|
||||
op: Op::Delete,
|
||||
old_line: old_index + i,
|
||||
old_line: Some(old_index + i),
|
||||
old_content: Some(content),
|
||||
new_line: new_index,
|
||||
old_byte_range: old_line_range.clone(),
|
||||
new_line: None,
|
||||
new_content: None,
|
||||
new_byte_range: 0..0,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some(ContentDiff(diff_lines))
|
||||
Some(ContentDiff {
|
||||
diff_lines,
|
||||
old_content,
|
||||
old_line_count: old_line_ranges.len(),
|
||||
new_content,
|
||||
new_line_count: new_line_ranges.len(),
|
||||
})
|
||||
}
|
||||
|
||||
impl ContentDiff {
|
||||
pub(crate) fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
self.diff_lines.len()
|
||||
}
|
||||
|
||||
pub(crate) fn get(&self, i: usize) -> &DiffLine {
|
||||
&self.0[i]
|
||||
&self.diff_lines[i]
|
||||
}
|
||||
|
||||
pub(crate) fn last(&self) -> Option<&DiffLine> {
|
||||
self.0.last()
|
||||
self.diff_lines.last()
|
||||
}
|
||||
}
|
||||
|
||||
fn line_ranges(content: &[u8]) -> Vec<Range<usize>> {
|
||||
let mut ranges: Vec<std::ops::Range<usize>> = Vec::new();
|
||||
let mut line_start: usize = 0;
|
||||
let mut skip_next = false;
|
||||
|
||||
for i in memchr2_iter(b'\n', b'\r', content) {
|
||||
if skip_next {
|
||||
skip_next = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
let c = content[i];
|
||||
|
||||
match (c, content.get(i + 1)) {
|
||||
| (b'\r', Some(b'\n')) => {
|
||||
// if \r found, check if its \r\n or if its a lone \r
|
||||
// if \r\n, then treat as one line break
|
||||
ranges.push(line_start..i + 1);
|
||||
// because we already counted the \n byte, the next iter into it needs to be skipped
|
||||
skip_next = true;
|
||||
line_start = i + 2;
|
||||
}
|
||||
| _ => {
|
||||
ranges.push(line_start..i);
|
||||
line_start = i + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if line_start < content.len() {
|
||||
ranges.push(line_start..content.len());
|
||||
}
|
||||
|
||||
ranges
|
||||
}
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
use memchr::memchr;
|
||||
use std::path::Path;
|
||||
|
||||
use memchr::{memchr, memchr2_iter};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum ContentType {
|
||||
Text,
|
||||
Binary,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum FileType {
|
||||
Rust,
|
||||
JavaScript,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
pub(crate) fn classify_content(content: &[u8]) -> ContentType {
|
||||
if content.is_empty() {
|
||||
ContentType::Text
|
||||
@@ -17,8 +27,52 @@ pub(crate) fn classify_content(content: &[u8]) -> ContentType {
|
||||
ContentType::Text
|
||||
} else {
|
||||
match memchr(0, &content[..content.len().min(8192)]) {
|
||||
| None => ContentType::Text,
|
||||
| Some(_) => ContentType::Binary,
|
||||
| None => ContentType::Text,
|
||||
| Some(_) => ContentType::Binary,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn file_type_from_path(path: &str) -> FileType {
|
||||
match Path::new(path).extension().map(|it| it.to_str()).flatten() {
|
||||
| Some("rs") => FileType::Rust,
|
||||
| Some("js") | Some("jsx") => FileType::JavaScript,
|
||||
| _ => FileType::Unknown,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn line_ranges(content: &[u8]) -> Vec<std::ops::Range<usize>> {
|
||||
let mut ranges: Vec<std::ops::Range<usize>> = Vec::new();
|
||||
let mut line_start: usize = 0;
|
||||
let mut skip_next = false;
|
||||
|
||||
for i in memchr2_iter(b'\n', b'\r', content) {
|
||||
if skip_next {
|
||||
skip_next = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
let c = content[i];
|
||||
|
||||
match (c, content.get(i + 1)) {
|
||||
| (b'\r', Some(b'\n')) => {
|
||||
// if \r found, check if its \r\n or if its a lone \r
|
||||
// if \r\n, then treat as one line break
|
||||
ranges.push(line_start..i + 1);
|
||||
// because we already counted the \n byte, the next iter into it needs to be skipped
|
||||
skip_next = true;
|
||||
line_start = i + 2;
|
||||
}
|
||||
| _ => {
|
||||
ranges.push(line_start..i);
|
||||
line_start = i + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if line_start < content.len() {
|
||||
ranges.push(line_start..content.len());
|
||||
}
|
||||
|
||||
ranges
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
pub(crate) mod diff;
|
||||
pub(crate) mod file;
|
||||
pub(crate) mod str;
|
||||
pub(crate) mod syntax_highlight;
|
||||
pub(crate) mod timeout;
|
||||
|
||||
91
src/util/syntax_highlight.rs
Normal file
91
src/util/syntax_highlight.rs
Normal file
@@ -0,0 +1,91 @@
|
||||
use crate::{theme, util};
|
||||
|
||||
pub(crate) struct HighlightedContent(Vec<Vec<(std::ops::Range<usize>, gpui::HighlightStyle)>>);
|
||||
|
||||
fn ts_highlight_configuration_for_file_type(
|
||||
file_type: util::file::FileType,
|
||||
) -> Option<tree_sitter_highlight::HighlightConfiguration> {
|
||||
match file_type {
|
||||
| util::file::FileType::Rust => tree_sitter_highlight::HighlightConfiguration::new(
|
||||
tree_sitter_rust::LANGUAGE.into(),
|
||||
"rust",
|
||||
tree_sitter_rust::HIGHLIGHTS_QUERY,
|
||||
tree_sitter_rust::INJECTIONS_QUERY,
|
||||
"",
|
||||
)
|
||||
.ok(),
|
||||
|
||||
| _ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn highlight_content(
|
||||
content: impl AsRef<[u8]>,
|
||||
file_type: util::file::FileType,
|
||||
theme_syntax: &theme::ThemeSyntax,
|
||||
) -> Option<HighlightedContent> {
|
||||
let mut config = ts_highlight_configuration_for_file_type(file_type)?;
|
||||
|
||||
config.configure(theme::syntax::HIGHLIGHT_NAMES);
|
||||
|
||||
let mut highlighter = tree_sitter_highlight::Highlighter::new();
|
||||
let events = highlighter
|
||||
.highlight(&config, content.as_ref(), None, |_| None)
|
||||
.ok()?;
|
||||
|
||||
let default_highlight = theme_syntax.resolve(theme::syntax::key::TEXT_LITERAL);
|
||||
let mut highlight = default_highlight;
|
||||
|
||||
let line_ranges = util::file::line_ranges(content.as_ref());
|
||||
|
||||
let mut highlights: Vec<Vec<(std::ops::Range<usize>, gpui::HighlightStyle)>> =
|
||||
Vec::with_capacity(line_ranges.len());
|
||||
let mut current_line: usize = 0;
|
||||
|
||||
for highlight_event in events {
|
||||
match highlight_event.ok()? {
|
||||
| tree_sitter_highlight::HighlightEvent::HighlightStart(h) => {
|
||||
highlight = theme_syntax.as_slice()[h.0];
|
||||
}
|
||||
| tree_sitter_highlight::HighlightEvent::Source { start, end } => {
|
||||
while current_line < line_ranges.len() && start >= line_ranges[current_line].end {
|
||||
highlights.push(Vec::new());
|
||||
current_line += 1;
|
||||
}
|
||||
let mut line = current_line;
|
||||
while line < line_ranges.len() && end > line_ranges[line].start {
|
||||
if highlights.get(line).is_none() {
|
||||
highlights.push(Vec::new());
|
||||
}
|
||||
let line_range = &line_ranges[line];
|
||||
let highlight_start = start.max(line_range.start);
|
||||
let highlight_end = end.min(line_range.end);
|
||||
highlights[line].push((
|
||||
(highlight_start - line_range.start)..(highlight_end - line_range.start),
|
||||
gpui::HighlightStyle {
|
||||
color: Some(highlight.color.into()),
|
||||
font_weight: highlight.font_weight,
|
||||
font_style: Some(highlight.font_style),
|
||||
..Default::default()
|
||||
},
|
||||
));
|
||||
line += 1;
|
||||
}
|
||||
}
|
||||
| tree_sitter_highlight::HighlightEvent::HighlightEnd => {
|
||||
highlight = default_highlight;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some(HighlightedContent(highlights))
|
||||
}
|
||||
|
||||
impl HighlightedContent {
|
||||
pub(crate) fn highlights_at_line(
|
||||
&self,
|
||||
line: usize,
|
||||
) -> &Vec<(std::ops::Range<usize>, gpui::HighlightStyle)> {
|
||||
&self.0[line]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user