chore: audit components
This commit is contained in:
@@ -11,10 +11,12 @@ use gpui::{
|
||||
actions, canvas, div, uniform_list,
|
||||
};
|
||||
use gpui_native_core::scrollbar::{Scrollbar, ScrollbarHandle, ScrollbarVisibility};
|
||||
use gpui_native_theme::{ActiveTheme as _, ColorTokens, MONO_ADVANCE_RATIO, Ramps};
|
||||
use gpui_native_theme::{
|
||||
ActiveTheme as _, ColorTokens, MONO_ADVANCE_RATIO, Ramps, WASH_ALPHA_SOFT,
|
||||
};
|
||||
|
||||
use crate::highlight::{HighlightedCode, Language, Role, highlight};
|
||||
use crate::utils::{gutter_cell, run_style};
|
||||
use crate::utils::gutter_cell;
|
||||
|
||||
actions!(code_view, [Copy]);
|
||||
|
||||
@@ -83,7 +85,7 @@ impl CodeViewState {
|
||||
active_line: None,
|
||||
selection: None,
|
||||
selecting: false,
|
||||
focus_handle: cx.focus_handle(),
|
||||
focus_handle: cx.focus_handle().tab_stop(true),
|
||||
a11y_label: None,
|
||||
}
|
||||
}
|
||||
@@ -223,14 +225,17 @@ impl CodeViewState {
|
||||
char_width: Pixels,
|
||||
text_x: Pixels,
|
||||
) -> SelectionPoint {
|
||||
// Saturating so an empty document cannot invert the clamp.
|
||||
let last_row = self.code.lines.len().saturating_sub(1);
|
||||
let row = (((position.y - bounds.origin.y - scroll_y) / line_height).floor() as isize)
|
||||
.clamp(0, self.code.lines.len() as isize - 1) as usize;
|
||||
.clamp(0, last_row as isize) as usize;
|
||||
|
||||
let chars = self.code.lines[row]
|
||||
.spans
|
||||
.iter()
|
||||
.map(|span| span.text.chars().count())
|
||||
.sum::<usize>();
|
||||
let chars = self.code.lines.get(row).map_or(0, |line| {
|
||||
line.spans
|
||||
.iter()
|
||||
.map(|span| span.text.chars().count())
|
||||
.sum::<usize>()
|
||||
});
|
||||
|
||||
let col = (((position.x - text_x) / char_width).round() as isize).clamp(0, chars as isize)
|
||||
as usize;
|
||||
@@ -503,7 +508,7 @@ fn render_row(
|
||||
if char_ix > 0 {
|
||||
highlights.push((
|
||||
run_start..run_end,
|
||||
run_style(run_role, run_selected, colors, ramps),
|
||||
run_role.run_style(run_selected, colors, ramps),
|
||||
));
|
||||
}
|
||||
run_start = byte_ix;
|
||||
@@ -516,7 +521,7 @@ fn render_row(
|
||||
if run_end > run_start || !text.is_empty() {
|
||||
highlights.push((
|
||||
run_start..run_end,
|
||||
run_style(run_role, run_selected, colors, ramps),
|
||||
run_role.run_style(run_selected, colors, ramps),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -539,7 +544,7 @@ fn render_row(
|
||||
.overflow_hidden()
|
||||
.when(active, |row| {
|
||||
row.bg(Hsla {
|
||||
a: colors.muted.a * 0.45,
|
||||
a: colors.muted.a * WASH_ALPHA_SOFT,
|
||||
..colors.muted
|
||||
})
|
||||
})
|
||||
|
||||
+243
-144
@@ -3,7 +3,7 @@
|
||||
//! bars instead of markers, tinted number gutters, word-level intra-line
|
||||
//! highlights and hunk separators. Rows reuse the code view's highlighting;
|
||||
//! consumers that already hold a git diff can build [`DiffRow`]s themselves
|
||||
//! instead of calling [`diff_rows`].
|
||||
//! instead of calling [`DiffViewState::diff_rows`].
|
||||
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
@@ -14,12 +14,12 @@ use gpui::{
|
||||
};
|
||||
use gpui_native_core::scrollbar::{Scrollbar, ScrollbarHandle, ScrollbarVisibility};
|
||||
use gpui_native_theme::{ActiveTheme as _, MONO_ADVANCE_RATIO};
|
||||
use similar::{ChangeTag, TextDiff};
|
||||
use similar::{Change, ChangeTag, TextDiff};
|
||||
|
||||
use std::ops::Range;
|
||||
|
||||
use crate::highlight::{Language, Line, Role, highlight};
|
||||
use crate::utils::{gutter_cell, line_row, role_color, wash, wash_gutter, wash_strong};
|
||||
use crate::highlight::{HighlightedCode, Language, Line, Role, highlight};
|
||||
use crate::utils::{gutter_cell, wash, wash_gutter, wash_strong};
|
||||
|
||||
/// What a diff row did to the file.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
@@ -96,147 +96,82 @@ impl Default for DiffViewOptions {
|
||||
}
|
||||
|
||||
impl DiffRow {
|
||||
/// Assembles a diff line row from a `similar` change.
|
||||
pub(crate) fn from_change(
|
||||
change: &Change<&str>,
|
||||
code: &HighlightedCode,
|
||||
changed_ranges: Vec<Range<usize>>,
|
||||
) -> DiffRow {
|
||||
let content = match change.tag() {
|
||||
ChangeTag::Equal => change
|
||||
.new_index()
|
||||
.and_then(|ix| code.lines.get(ix))
|
||||
.cloned()
|
||||
.unwrap_or_default(),
|
||||
ChangeTag::Delete => change
|
||||
.old_index()
|
||||
.and_then(|ix| code.lines.get(ix))
|
||||
.cloned()
|
||||
.unwrap_or_default(),
|
||||
ChangeTag::Insert => change
|
||||
.new_index()
|
||||
.and_then(|ix| code.lines.get(ix))
|
||||
.cloned()
|
||||
.unwrap_or_default(),
|
||||
};
|
||||
|
||||
let (old_number, new_number) = match change.tag() {
|
||||
ChangeTag::Equal => (
|
||||
change.old_index().map(|ix| ix + 1),
|
||||
change.new_index().map(|ix| ix + 1),
|
||||
),
|
||||
ChangeTag::Delete => (change.old_index().map(|ix| ix + 1), None),
|
||||
ChangeTag::Insert => (None, change.new_index().map(|ix| ix + 1)),
|
||||
};
|
||||
|
||||
DiffRow {
|
||||
kind: match change.tag() {
|
||||
ChangeTag::Equal => DiffRowKind::Context,
|
||||
ChangeTag::Delete => DiffRowKind::Removed,
|
||||
ChangeTag::Insert => DiffRowKind::Added,
|
||||
},
|
||||
content,
|
||||
old_number,
|
||||
new_number,
|
||||
changed_ranges,
|
||||
header: Default::default(),
|
||||
hidden_lines: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// The row's text length, in characters.
|
||||
fn char_count(&self) -> usize {
|
||||
self.content
|
||||
.spans
|
||||
.iter()
|
||||
.map(|span| span.text.chars().count())
|
||||
.sum()
|
||||
self.content.text().chars().count()
|
||||
}
|
||||
}
|
||||
|
||||
/// Computes unified rows between two texts, highlighting every line with `language`.
|
||||
pub fn diff_rows(old: &str, new: &str, language: Language) -> Vec<DiffRow> {
|
||||
let old_code = highlight(old, language);
|
||||
let new_code = highlight(new, language);
|
||||
let diff = TextDiff::from_lines(old, new);
|
||||
|
||||
let mut rows = Vec::new();
|
||||
let mut collapsed_before = 0usize;
|
||||
|
||||
for group in diff.grouped_ops(3) {
|
||||
let first = group.first().expect("hunk groups are never empty");
|
||||
let last = group.last().expect("hunk groups are never empty");
|
||||
|
||||
let old_range = first.old_range().start..last.old_range().end;
|
||||
let new_range = first.new_range().start..last.new_range().end;
|
||||
|
||||
// git prints the line before a pure insertion when the count is zero.
|
||||
let old_start = if old_range.is_empty() {
|
||||
old_range.start
|
||||
} else {
|
||||
old_range.start + 1
|
||||
};
|
||||
|
||||
let new_start = if new_range.is_empty() {
|
||||
new_range.start
|
||||
} else {
|
||||
new_range.start + 1
|
||||
};
|
||||
|
||||
let collapsed = first.old_range().start - collapsed_before;
|
||||
if collapsed > 0 {
|
||||
rows.push(DiffRow {
|
||||
kind: DiffRowKind::Hunk,
|
||||
content: Line::default(),
|
||||
old_number: None,
|
||||
new_number: None,
|
||||
changed_ranges: Vec::new(),
|
||||
header: format!(
|
||||
"@@ -{},{} +{},{} @@",
|
||||
old_start,
|
||||
old_range.len(),
|
||||
new_start,
|
||||
new_range.len()
|
||||
)
|
||||
.into(),
|
||||
hidden_lines: collapsed,
|
||||
});
|
||||
}
|
||||
collapsed_before = last.old_range().end;
|
||||
|
||||
for op in &group {
|
||||
let changes: Vec<_> = diff.iter_changes(op).collect();
|
||||
let mut ix = 0;
|
||||
|
||||
while ix < changes.len() {
|
||||
if changes[ix].tag() == ChangeTag::Equal {
|
||||
rows.push(line_row(&changes[ix], &new_code, Vec::new()));
|
||||
ix += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
let deletions = changes[ix..]
|
||||
.iter()
|
||||
.take_while(|change| change.tag() == ChangeTag::Delete)
|
||||
.count();
|
||||
|
||||
let insertions = changes[ix + deletions..]
|
||||
.iter()
|
||||
.take_while(|change| change.tag() == ChangeTag::Insert)
|
||||
.count();
|
||||
|
||||
let mut deletion_ranges = vec![Vec::new(); deletions];
|
||||
let mut insertion_ranges = vec![Vec::new(); insertions];
|
||||
|
||||
for pair in 0..deletions.min(insertions) {
|
||||
let old_text = changes[ix + pair].value().trim_end_matches('\n');
|
||||
let new_text = changes[ix + deletions + pair]
|
||||
.value()
|
||||
.trim_end_matches('\n');
|
||||
let words = TextDiff::from_words(old_text, new_text);
|
||||
|
||||
let (mut old_offset, mut new_offset) = (0usize, 0usize);
|
||||
|
||||
for word in words.iter_all_changes() {
|
||||
let len = word.value().len();
|
||||
|
||||
match word.tag() {
|
||||
ChangeTag::Delete => {
|
||||
deletion_ranges[pair].push(old_offset..old_offset + len);
|
||||
old_offset += len;
|
||||
}
|
||||
ChangeTag::Insert => {
|
||||
insertion_ranges[pair].push(new_offset..new_offset + len);
|
||||
new_offset += len;
|
||||
}
|
||||
ChangeTag::Equal => {
|
||||
old_offset += len;
|
||||
new_offset += len;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for pair in 0..deletions {
|
||||
rows.push(line_row(
|
||||
&changes[ix + pair],
|
||||
&old_code,
|
||||
deletion_ranges[pair].clone(),
|
||||
));
|
||||
}
|
||||
|
||||
for pair in 0..insertions {
|
||||
rows.push(line_row(
|
||||
&changes[ix + deletions + pair],
|
||||
&new_code,
|
||||
insertion_ranges[pair].clone(),
|
||||
));
|
||||
}
|
||||
|
||||
ix += deletions + insertions;
|
||||
/// Stable element id: the kind and line numbers packed into one integer.
|
||||
fn element_id(&self, hunk_ordinal: usize) -> ElementId {
|
||||
let kind = match self.kind {
|
||||
DiffRowKind::Context => 0u64,
|
||||
DiffRowKind::Added => 1,
|
||||
DiffRowKind::Removed => 2,
|
||||
DiffRowKind::Hunk => {
|
||||
return ElementId::named_usize("diff-hunk", hunk_ordinal);
|
||||
}
|
||||
}
|
||||
};
|
||||
// Line numbers saturate at 2^31 so the two fields never overlap.
|
||||
let old = self.old_number.unwrap_or(0).min((1 << 31) - 1) as u64;
|
||||
let new = self.new_number.unwrap_or(0).min((1 << 31) - 1) as u64;
|
||||
ElementId::named_usize("diff-row", ((kind << 62) | (old << 31) | new) as usize)
|
||||
}
|
||||
|
||||
rows
|
||||
}
|
||||
|
||||
/// Retained state for a diff view.
|
||||
pub struct DiffViewState {
|
||||
id: ElementId,
|
||||
rows: Vec<DiffRow>,
|
||||
/// Cached per-row id and text length, parallel to `rows`.
|
||||
row_layouts: Vec<RowLayout>,
|
||||
/// The row whose width drives horizontal scroll extent, `rows`-derived.
|
||||
widest_row: Option<usize>,
|
||||
options: DiffViewOptions,
|
||||
@@ -245,6 +180,12 @@ pub struct DiffViewState {
|
||||
a11y_label: Option<SharedString>,
|
||||
}
|
||||
|
||||
/// Cached per-row render data, computed once when the rows are set.
|
||||
struct RowLayout {
|
||||
id: ElementId,
|
||||
char_count: usize,
|
||||
}
|
||||
|
||||
impl DiffViewState {
|
||||
/// Builds the state, diffing `old` against `new`.
|
||||
pub fn new<I, O, N>(id: I, old: O, new: N, language: Language) -> Self
|
||||
@@ -256,6 +197,7 @@ impl DiffViewState {
|
||||
let mut state = Self {
|
||||
id: id.into(),
|
||||
rows: Vec::new(),
|
||||
row_layouts: Vec::new(),
|
||||
widest_row: None,
|
||||
options: DiffViewOptions::default(),
|
||||
handle: UniformListScrollHandle::new(),
|
||||
@@ -265,20 +207,168 @@ impl DiffViewState {
|
||||
state
|
||||
}
|
||||
|
||||
/// Replaces the rows, recomputing `widest_row` with them.
|
||||
/// Replaces the rows, recomputing the per-row layout cache with them.
|
||||
fn set_rows<O, N>(&mut self, old: O, new: N, language: Language)
|
||||
where
|
||||
O: AsRef<str>,
|
||||
N: AsRef<str>,
|
||||
{
|
||||
self.rows = diff_rows(old.as_ref(), new.as_ref(), language);
|
||||
self.rows = Self::diff_rows(old.as_ref(), new.as_ref(), language);
|
||||
|
||||
let mut hunk_ordinal = 0usize;
|
||||
self.row_layouts = self
|
||||
.rows
|
||||
.iter()
|
||||
.map(|row| {
|
||||
let ordinal = if row.kind == DiffRowKind::Hunk {
|
||||
hunk_ordinal += 1;
|
||||
hunk_ordinal
|
||||
} else {
|
||||
0
|
||||
};
|
||||
RowLayout {
|
||||
id: row.element_id(ordinal),
|
||||
char_count: row.char_count(),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
self.widest_row = self
|
||||
.rows
|
||||
.iter()
|
||||
.zip(&self.row_layouts)
|
||||
.enumerate()
|
||||
.filter(|(_, row)| row.kind != DiffRowKind::Hunk)
|
||||
.max_by_key(|(_, row)| row.char_count())
|
||||
.map(|(ix, _)| ix);
|
||||
.filter(|(_, (row, _))| row.kind != DiffRowKind::Hunk)
|
||||
.max_by_key(|(_, (_, layout))| layout.char_count)
|
||||
.map(|(widest, _)| widest);
|
||||
}
|
||||
|
||||
/// Computes unified rows between two texts, highlighting every line with `language`.
|
||||
pub fn diff_rows(old: &str, new: &str, language: Language) -> Vec<DiffRow> {
|
||||
let old_code = highlight(old, language);
|
||||
let new_code = highlight(new, language);
|
||||
let diff = TextDiff::from_lines(old, new);
|
||||
|
||||
let mut rows = Vec::new();
|
||||
let mut collapsed_before = 0usize;
|
||||
|
||||
for group in diff.grouped_ops(3) {
|
||||
let (Some(first), Some(last)) = (group.first(), group.last()) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let old_range = first.old_range().start..last.old_range().end;
|
||||
let new_range = first.new_range().start..last.new_range().end;
|
||||
|
||||
// git prints the line before a pure insertion when the count is zero.
|
||||
let old_start = if old_range.is_empty() {
|
||||
old_range.start
|
||||
} else {
|
||||
old_range.start + 1
|
||||
};
|
||||
|
||||
let new_start = if new_range.is_empty() {
|
||||
new_range.start
|
||||
} else {
|
||||
new_range.start + 1
|
||||
};
|
||||
|
||||
let collapsed = first.old_range().start.saturating_sub(collapsed_before);
|
||||
if collapsed > 0 {
|
||||
rows.push(DiffRow {
|
||||
kind: DiffRowKind::Hunk,
|
||||
content: Line::default(),
|
||||
old_number: None,
|
||||
new_number: None,
|
||||
changed_ranges: Vec::new(),
|
||||
header: format!(
|
||||
"@@ -{},{} +{},{} @@",
|
||||
old_start,
|
||||
old_range.len(),
|
||||
new_start,
|
||||
new_range.len()
|
||||
)
|
||||
.into(),
|
||||
hidden_lines: collapsed,
|
||||
});
|
||||
}
|
||||
collapsed_before = last.old_range().end;
|
||||
|
||||
for op in &group {
|
||||
let changes: Vec<_> = diff.iter_changes(op).collect();
|
||||
let mut index = 0;
|
||||
|
||||
while index < changes.len() {
|
||||
if changes[index].tag() == ChangeTag::Equal {
|
||||
rows.push(DiffRow::from_change(&changes[index], &new_code, Vec::new()));
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
let deletions = changes[index..]
|
||||
.iter()
|
||||
.take_while(|change| change.tag() == ChangeTag::Delete)
|
||||
.count();
|
||||
|
||||
let insertions = changes[index + deletions..]
|
||||
.iter()
|
||||
.take_while(|change| change.tag() == ChangeTag::Insert)
|
||||
.count();
|
||||
|
||||
let mut deletion_ranges = vec![Vec::new(); deletions];
|
||||
let mut insertion_ranges = vec![Vec::new(); insertions];
|
||||
|
||||
for pair in 0..deletions.min(insertions) {
|
||||
let old_text = changes[index + pair].value().trim_end_matches('\n');
|
||||
let new_text = changes[index + deletions + pair]
|
||||
.value()
|
||||
.trim_end_matches('\n');
|
||||
let words = TextDiff::from_words(old_text, new_text);
|
||||
|
||||
let (mut old_offset, mut new_offset) = (0usize, 0usize);
|
||||
|
||||
for word in words.iter_all_changes() {
|
||||
let len = word.value().len();
|
||||
|
||||
match word.tag() {
|
||||
ChangeTag::Delete => {
|
||||
deletion_ranges[pair].push(old_offset..old_offset + len);
|
||||
old_offset += len;
|
||||
}
|
||||
ChangeTag::Insert => {
|
||||
insertion_ranges[pair].push(new_offset..new_offset + len);
|
||||
new_offset += len;
|
||||
}
|
||||
ChangeTag::Equal => {
|
||||
old_offset += len;
|
||||
new_offset += len;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for pair in 0..deletions {
|
||||
rows.push(DiffRow::from_change(
|
||||
&changes[index + pair],
|
||||
&old_code,
|
||||
deletion_ranges[pair].clone(),
|
||||
));
|
||||
}
|
||||
|
||||
for pair in 0..insertions {
|
||||
rows.push(DiffRow::from_change(
|
||||
&changes[index + deletions + pair],
|
||||
&new_code,
|
||||
insertion_ranges[pair].clone(),
|
||||
));
|
||||
}
|
||||
|
||||
index += deletions + insertions;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rows
|
||||
}
|
||||
|
||||
/// Sets how changed rows are marked.
|
||||
@@ -355,7 +445,15 @@ impl Render for DiffViewState {
|
||||
move |range, _window, cx| {
|
||||
let diff = state.read(cx);
|
||||
range
|
||||
.map(|row_ix| render_row(&diff.rows[row_ix], row_ix, options, cx))
|
||||
.map(|row_index| {
|
||||
render_row(
|
||||
&diff.rows[row_index],
|
||||
&diff.row_layouts[row_index],
|
||||
row_index,
|
||||
options,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
})
|
||||
@@ -389,7 +487,8 @@ impl Render for DiffViewState {
|
||||
|
||||
fn render_row(
|
||||
row: &DiffRow,
|
||||
row_ix: usize,
|
||||
layout: &RowLayout,
|
||||
row_index: usize,
|
||||
options: DiffViewOptions,
|
||||
cx: &App,
|
||||
) -> gpui::AnyElement {
|
||||
@@ -402,8 +501,8 @@ fn render_row(
|
||||
let char_width = text_size * MONO_ADVANCE_RATIO;
|
||||
|
||||
let frame = div()
|
||||
.debug_selector(move || format!("diff-row-{row_ix}"))
|
||||
.id(("diff-row", row_ix))
|
||||
.debug_selector(move || format!("diff-row-{row_index}"))
|
||||
.id(layout.id.clone())
|
||||
.w_full()
|
||||
.h(line_height)
|
||||
.font_family(mono_font)
|
||||
@@ -496,7 +595,7 @@ fn render_row(
|
||||
&& *role != Role::Text
|
||||
{
|
||||
segment = segment.highlight(HighlightStyle {
|
||||
color: Some(role_color(*role, colors, ramps)),
|
||||
color: Some(role.color(colors, ramps)),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
@@ -582,7 +681,7 @@ fn render_row(
|
||||
div()
|
||||
.flex_none()
|
||||
.ml_2()
|
||||
.w(char_width * row.char_count() as f32)
|
||||
.w(char_width * layout.char_count as f32)
|
||||
.whitespace_nowrap()
|
||||
.child(gpui::StyledText::new(text).with_default_highlights(&style, highlights)),
|
||||
)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
//! code without re-parsing it.
|
||||
|
||||
use gpui::SharedString;
|
||||
use gpui_native_utils::ResultLogErr;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use tree_sitter::StreamingIterator as _;
|
||||
@@ -29,7 +30,7 @@ pub enum Language {
|
||||
}
|
||||
|
||||
/// The semantic role a span plays.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum Role {
|
||||
Text,
|
||||
Keyword,
|
||||
@@ -47,20 +48,31 @@ pub enum Role {
|
||||
}
|
||||
|
||||
/// A run of text sharing one role.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct Span {
|
||||
pub text: SharedString,
|
||||
pub role: Role,
|
||||
}
|
||||
|
||||
/// One rendered line of code.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
|
||||
pub struct Line {
|
||||
pub spans: Vec<Span>,
|
||||
}
|
||||
|
||||
impl Line {
|
||||
/// The line's flat text, in character order.
|
||||
pub fn text(&self) -> String {
|
||||
let mut text = String::new();
|
||||
for span in &self.spans {
|
||||
text.push_str(&span.text);
|
||||
}
|
||||
text
|
||||
}
|
||||
}
|
||||
|
||||
/// Source text split into role-styled lines.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
|
||||
pub struct HighlightedCode {
|
||||
pub lines: Vec<Line>,
|
||||
}
|
||||
@@ -77,10 +89,8 @@ pub fn highlight(source: &str, language: Language) -> HighlightedCode {
|
||||
.entry(language)
|
||||
.or_insert_with(|| {
|
||||
let mut parser = tree_sitter::Parser::new();
|
||||
if parser.set_language(&grammar).is_err() {
|
||||
return None;
|
||||
}
|
||||
let query = tree_sitter::Query::new(&grammar, query_source).ok()?;
|
||||
parser.set_language(&grammar).log_err()?;
|
||||
let query = tree_sitter::Query::new(&grammar, query_source).log_err()?;
|
||||
let roles = query
|
||||
.capture_names()
|
||||
.iter()
|
||||
@@ -99,6 +109,7 @@ pub fn highlight(source: &str, language: Language) -> HighlightedCode {
|
||||
};
|
||||
|
||||
let Some(tree) = highlighter.parser.parse(source, None) else {
|
||||
log::warn!("tree-sitter failed to parse {language:?}; rendering plain text");
|
||||
return plain(source);
|
||||
};
|
||||
|
||||
@@ -137,33 +148,73 @@ pub(crate) fn plain(source: &str) -> HighlightedCode {
|
||||
/// The grammar and its highlight query for a language, when compiled in.
|
||||
fn grammar_for(language: Language) -> Option<(tree_sitter::Language, &'static str)> {
|
||||
match language {
|
||||
Language::Rust => cfg!(feature = "rust").then(|| {
|
||||
(
|
||||
tree_sitter_rust::LANGUAGE.into(),
|
||||
tree_sitter_rust::HIGHLIGHTS_QUERY,
|
||||
)
|
||||
}),
|
||||
Language::TypeScript => cfg!(feature = "typescript").then(|| {
|
||||
(
|
||||
tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
|
||||
tree_sitter_typescript::HIGHLIGHTS_QUERY,
|
||||
)
|
||||
}),
|
||||
Language::JavaScript => cfg!(feature = "javascript").then(|| {
|
||||
(
|
||||
tree_sitter_javascript::LANGUAGE.into(),
|
||||
tree_sitter_javascript::HIGHLIGHT_QUERY,
|
||||
)
|
||||
}),
|
||||
Language::Python => cfg!(feature = "python").then(|| {
|
||||
(
|
||||
tree_sitter_python::LANGUAGE.into(),
|
||||
tree_sitter_python::HIGHLIGHTS_QUERY,
|
||||
)
|
||||
}),
|
||||
Language::Rust => rust_grammar(),
|
||||
Language::TypeScript => typescript_grammar(),
|
||||
Language::JavaScript => javascript_grammar(),
|
||||
Language::Python => python_grammar(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The Rust grammar, compiled in only when the `rust` feature is on.
|
||||
#[cfg(feature = "rust")]
|
||||
fn rust_grammar() -> Option<(tree_sitter::Language, &'static str)> {
|
||||
Some((
|
||||
tree_sitter_rust::LANGUAGE.into(),
|
||||
tree_sitter_rust::HIGHLIGHTS_QUERY,
|
||||
))
|
||||
}
|
||||
|
||||
/// Without the feature, Rust sources render as plain text.
|
||||
#[cfg(not(feature = "rust"))]
|
||||
fn rust_grammar() -> Option<(tree_sitter::Language, &'static str)> {
|
||||
None
|
||||
}
|
||||
|
||||
/// The TypeScript grammar, compiled in only when the `typescript` feature is on.
|
||||
#[cfg(feature = "typescript")]
|
||||
fn typescript_grammar() -> Option<(tree_sitter::Language, &'static str)> {
|
||||
Some((
|
||||
tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
|
||||
tree_sitter_typescript::HIGHLIGHTS_QUERY,
|
||||
))
|
||||
}
|
||||
|
||||
/// Without the feature, TypeScript sources render as plain text.
|
||||
#[cfg(not(feature = "typescript"))]
|
||||
fn typescript_grammar() -> Option<(tree_sitter::Language, &'static str)> {
|
||||
None
|
||||
}
|
||||
|
||||
/// The JavaScript grammar, compiled in only when the `javascript` feature is on.
|
||||
#[cfg(feature = "javascript")]
|
||||
fn javascript_grammar() -> Option<(tree_sitter::Language, &'static str)> {
|
||||
Some((
|
||||
tree_sitter_javascript::LANGUAGE.into(),
|
||||
tree_sitter_javascript::HIGHLIGHT_QUERY,
|
||||
))
|
||||
}
|
||||
|
||||
/// Without the feature, JavaScript sources render as plain text.
|
||||
#[cfg(not(feature = "javascript"))]
|
||||
fn javascript_grammar() -> Option<(tree_sitter::Language, &'static str)> {
|
||||
None
|
||||
}
|
||||
|
||||
/// The Python grammar, compiled in only when the `python` feature is on.
|
||||
#[cfg(feature = "python")]
|
||||
fn python_grammar() -> Option<(tree_sitter::Language, &'static str)> {
|
||||
Some((
|
||||
tree_sitter_python::LANGUAGE.into(),
|
||||
tree_sitter_python::HIGHLIGHTS_QUERY,
|
||||
))
|
||||
}
|
||||
|
||||
/// Without the feature, Python sources render as plain text.
|
||||
#[cfg(not(feature = "python"))]
|
||||
fn python_grammar() -> Option<(tree_sitter::Language, &'static str)> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Maps a query capture name to a role; unmapped captures render as text.
|
||||
fn role_for_capture(name: &str) -> Option<Role> {
|
||||
match name.split('.').next()? {
|
||||
|
||||
@@ -13,13 +13,15 @@ use gpui::{
|
||||
};
|
||||
use gpui_native_core::scrollbar::{Scrollbar, ScrollbarVisibility};
|
||||
use gpui_native_icons::{IconRole, icon};
|
||||
use gpui_native_theme::ActiveTheme as _;
|
||||
use gpui_native_theme::{ActiveTheme as _, BORDER_WIDTH, CODE_OVERSCROLL_MAX};
|
||||
use pulldown_cmark::{CodeBlockKind, Event, HeadingLevel, Options, Parser, Tag, TagEnd};
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::hash::{DefaultHasher, Hash, Hasher};
|
||||
use std::ops::Range;
|
||||
|
||||
use crate::highlight::{HighlightedCode, highlight, plain};
|
||||
use crate::utils::{code_line, grammar_language};
|
||||
use crate::highlight::{HighlightedCode, Language, highlight, plain};
|
||||
use crate::utils::code_line;
|
||||
|
||||
/// Heading scale steps relative to the root font size, H1 first.
|
||||
const HEADING_SCALES: [f32; 6] = [1.9, 1.5, 1.25, 1.1, 1.0, 0.95];
|
||||
@@ -39,7 +41,7 @@ const BODY_LINE_HEIGHT: f32 = 1.6;
|
||||
const CODE_LINE_HEIGHT: f32 = 1.5;
|
||||
|
||||
/// A heading level, 1 to 6.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum MarkdownHeading {
|
||||
H1,
|
||||
H2,
|
||||
@@ -64,7 +66,7 @@ impl MarkdownHeading {
|
||||
}
|
||||
|
||||
/// The marker drawn before a list item.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum ListMarker {
|
||||
/// An unordered bullet.
|
||||
Bullet,
|
||||
@@ -75,14 +77,14 @@ pub enum ListMarker {
|
||||
}
|
||||
|
||||
/// One run of inline text sharing a style.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct InlineSpan {
|
||||
pub text: SharedString,
|
||||
pub style: InlineStyle,
|
||||
}
|
||||
|
||||
/// The inline style a text run carries.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum InlineStyle {
|
||||
Text,
|
||||
Emphasis,
|
||||
@@ -98,26 +100,26 @@ pub enum InlineStyle {
|
||||
}
|
||||
|
||||
/// One cell of a table row.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
|
||||
pub struct MarkdownTableCell {
|
||||
pub spans: Vec<InlineSpan>,
|
||||
}
|
||||
|
||||
/// A table row; the header row precedes the body rows.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
|
||||
pub struct MarkdownTableRow {
|
||||
pub cells: Vec<MarkdownTableCell>,
|
||||
pub header: bool,
|
||||
}
|
||||
|
||||
/// A table, header row first.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
|
||||
pub struct MarkdownTable {
|
||||
pub rows: Vec<MarkdownTableRow>,
|
||||
}
|
||||
|
||||
/// One rendered block of a markdown document.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum MarkdownBlock {
|
||||
Heading {
|
||||
level: MarkdownHeading,
|
||||
@@ -160,11 +162,21 @@ pub enum MarkdownBlock {
|
||||
},
|
||||
}
|
||||
|
||||
impl MarkdownBlock {
|
||||
/// Hashes the block so element ids track content, not list position.
|
||||
fn content_hash(&self) -> u64 {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
self.hash(&mut hasher);
|
||||
hasher.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// Retained state for a markdown view.
|
||||
pub struct MarkdownViewState {
|
||||
id: ElementId,
|
||||
source: SharedString,
|
||||
blocks: Vec<MarkdownBlock>,
|
||||
block_ids: Vec<ElementId>,
|
||||
/// Whether fenced code blocks highlight with the code view's palette.
|
||||
highlight_code: bool,
|
||||
/// Drives the vertical scroll of the virtualized block list.
|
||||
@@ -179,16 +191,19 @@ impl MarkdownViewState {
|
||||
I: Into<ElementId>,
|
||||
S: Into<SharedString>,
|
||||
{
|
||||
let id = id.into();
|
||||
let source = source.into();
|
||||
let blocks = parse(&source, true);
|
||||
let block_ids = Self::stable_block_ids(&id, &blocks);
|
||||
let count = blocks.len();
|
||||
|
||||
Self {
|
||||
id: id.into(),
|
||||
id,
|
||||
source,
|
||||
blocks,
|
||||
block_ids,
|
||||
highlight_code: true,
|
||||
handle: ListState::new(count, ListAlignment::Top, px(400.)),
|
||||
handle: ListState::new(count, ListAlignment::Top, CODE_OVERSCROLL_MAX),
|
||||
a11y_label: None,
|
||||
}
|
||||
}
|
||||
@@ -215,11 +230,31 @@ impl MarkdownViewState {
|
||||
}
|
||||
|
||||
fn reparse(&mut self) {
|
||||
self.blocks = parse(&self.source, self.highlight_code);
|
||||
let blocks = parse(&self.source, self.highlight_code);
|
||||
let block_ids = Self::stable_block_ids(&self.id, &blocks);
|
||||
self.blocks = blocks;
|
||||
self.block_ids = block_ids;
|
||||
self.handle.reset(self.blocks.len());
|
||||
self.handle.scroll_to_reveal_item(0);
|
||||
}
|
||||
|
||||
/// Ids built from the block's content hash plus an ordinal among blocks of equal content.
|
||||
fn stable_block_ids(id: &ElementId, blocks: &[MarkdownBlock]) -> Vec<ElementId> {
|
||||
let mut ids = Vec::with_capacity(blocks.len());
|
||||
let mut occurrences = HashMap::with_capacity(blocks.len());
|
||||
for block in blocks {
|
||||
let hash = block.content_hash();
|
||||
let occurrence = *occurrences
|
||||
.entry(hash)
|
||||
.and_modify(|count| *count += 1)
|
||||
.or_insert(0);
|
||||
ids.push(ElementId::from(SharedString::from(format!(
|
||||
"{id}-block-{hash:x}-{occurrence}"
|
||||
))));
|
||||
}
|
||||
ids
|
||||
}
|
||||
|
||||
/// The markdown source the view renders.
|
||||
pub fn source(&self) -> &str {
|
||||
&self.source
|
||||
@@ -230,6 +265,11 @@ impl MarkdownViewState {
|
||||
&self.blocks
|
||||
}
|
||||
|
||||
/// The stable element id of the block at `block_index`.
|
||||
pub fn block_id(&self, block_index: usize) -> Option<ElementId> {
|
||||
self.block_ids.get(block_index).cloned()
|
||||
}
|
||||
|
||||
/// Whether fenced code highlights with the code view's palette.
|
||||
pub fn highlight_code(&self) -> bool {
|
||||
self.highlight_code
|
||||
@@ -259,10 +299,14 @@ impl Render for MarkdownViewState {
|
||||
let id = self.id.clone();
|
||||
let state = cx.entity();
|
||||
|
||||
let blocks = list(self.handle.clone(), move |block_ix, _window, cx| {
|
||||
let blocks = list(self.handle.clone(), move |block_index, _window, cx| {
|
||||
let view = state.read(cx);
|
||||
match view.blocks.get(block_ix) {
|
||||
Some(block) => render_block(block, block_ix, &view.id, cx),
|
||||
match view
|
||||
.blocks
|
||||
.get(block_index)
|
||||
.zip(view.block_ids.get(block_index))
|
||||
{
|
||||
Some((block, block_id)) => render_block(block, block_index, block_id, cx),
|
||||
None => div().into_any_element(),
|
||||
}
|
||||
})
|
||||
@@ -566,7 +610,7 @@ fn parse(source: &str, highlight_code: bool) -> Vec<MarkdownBlock> {
|
||||
let text = built.trim_end_matches('\n');
|
||||
let language = code_language.take();
|
||||
let lines = match highlight_code
|
||||
.then(|| language.as_deref().and_then(grammar_language))
|
||||
.then(|| language.as_deref().and_then(Language::from_fence_info))
|
||||
.flatten()
|
||||
{
|
||||
Some(grammar) => highlight(text, grammar),
|
||||
@@ -694,10 +738,14 @@ fn flush_item_inline(
|
||||
}
|
||||
|
||||
/// Renders one parsed block as a list row.
|
||||
fn render_block(block: &MarkdownBlock, block_ix: usize, id: &ElementId, cx: &App) -> AnyElement {
|
||||
fn render_block(
|
||||
block: &MarkdownBlock,
|
||||
block_index: usize,
|
||||
block_id: &ElementId,
|
||||
cx: &App,
|
||||
) -> AnyElement {
|
||||
let theme = cx.theme();
|
||||
let gap = theme.root_font_size * BLOCK_GAP;
|
||||
let block_id = ElementId::from(SharedString::from(format!("{id}-block-{block_ix}")));
|
||||
|
||||
match block {
|
||||
MarkdownBlock::Heading {
|
||||
@@ -710,15 +758,15 @@ fn render_block(block: &MarkdownBlock, block_ix: usize, id: &ElementId, cx: &App
|
||||
spans,
|
||||
*indent,
|
||||
*quote,
|
||||
if block_ix == 0 { px(0.) } else { gap * 0.5 },
|
||||
&block_id,
|
||||
if block_index == 0 { px(0.) } else { gap * 0.5 },
|
||||
block_id,
|
||||
cx,
|
||||
),
|
||||
MarkdownBlock::Paragraph {
|
||||
spans,
|
||||
indent,
|
||||
quote,
|
||||
} => text_row(spans, *indent, *quote, gap, None, &block_id, cx),
|
||||
} => text_row(spans, *indent, *quote, gap, None, block_id, cx),
|
||||
MarkdownBlock::ListItem {
|
||||
marker,
|
||||
spans,
|
||||
@@ -730,7 +778,7 @@ fn render_block(block: &MarkdownBlock, block_ix: usize, id: &ElementId, cx: &App
|
||||
*quote,
|
||||
theme.root_font_size * ITEM_GAP,
|
||||
Some(*marker),
|
||||
&block_id,
|
||||
block_id,
|
||||
cx,
|
||||
),
|
||||
MarkdownBlock::Code {
|
||||
@@ -738,13 +786,13 @@ fn render_block(block: &MarkdownBlock, block_ix: usize, id: &ElementId, cx: &App
|
||||
indent,
|
||||
quote,
|
||||
..
|
||||
} => code_row(lines, *indent, *quote, &block_id, cx),
|
||||
MarkdownBlock::ThematicBreak { indent, quote } => rule_row(*indent, *quote, &block_id, cx),
|
||||
} => code_row(lines, *indent, *quote, block_id, cx),
|
||||
MarkdownBlock::ThematicBreak { indent, quote } => rule_row(*indent, *quote, block_id, cx),
|
||||
MarkdownBlock::Table {
|
||||
table,
|
||||
indent,
|
||||
quote,
|
||||
} => table_row(table, *indent, *quote, &block_id, cx),
|
||||
} => table_row(table, *indent, *quote, block_id, cx),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -937,7 +985,7 @@ fn rule_row(indent: usize, quote: bool, id: &ElementId, cx: &App) -> AnyElement
|
||||
id,
|
||||
cx,
|
||||
)
|
||||
.child(div().h(px(1.)).w_full().bg(theme.colors.border))
|
||||
.child(div().h(BORDER_WIDTH).w_full().bg(theme.colors.border))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
@@ -966,11 +1014,11 @@ fn table_row(
|
||||
.border_color(colors.border)
|
||||
.rounded(theme.radius.control)
|
||||
.overflow_hidden()
|
||||
.children(table.rows.iter().enumerate().map(|(row_ix, row)| {
|
||||
.children(table.rows.iter().enumerate().map(|(row_index, row)| {
|
||||
div()
|
||||
.w_full()
|
||||
.flex()
|
||||
.when(row_ix + 1 < row_count, |row_element| {
|
||||
.when(row_index + 1 < row_count, |row_element| {
|
||||
row_element.border_b_1().border_color(colors.border)
|
||||
})
|
||||
.when(row.header, |row_element| row_element.bg(colors.muted))
|
||||
@@ -978,9 +1026,9 @@ fn table_row(
|
||||
row.cells
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(column_ix, cell)| {
|
||||
.map(|(column_index, cell)| {
|
||||
let cell_id = ElementId::from(SharedString::from(format!(
|
||||
"{id}-cell-{row_ix}-{column_ix}"
|
||||
"{id}-cell-{row_index}-{column_index}"
|
||||
)));
|
||||
div()
|
||||
.flex_1()
|
||||
@@ -1055,7 +1103,7 @@ fn text_child(spans: &[InlineSpan], id: &ElementId, cx: &App) -> AnyElement {
|
||||
range,
|
||||
HighlightStyle {
|
||||
strikethrough: Some(StrikethroughStyle {
|
||||
thickness: px(1.),
|
||||
thickness: BORDER_WIDTH,
|
||||
color: Some(theme.colors.muted_foreground),
|
||||
}),
|
||||
..Default::default()
|
||||
@@ -1077,7 +1125,7 @@ fn text_child(spans: &[InlineSpan], id: &ElementId, cx: &App) -> AnyElement {
|
||||
HighlightStyle {
|
||||
color: Some(theme.colors.accent),
|
||||
underline: Some(UnderlineStyle {
|
||||
thickness: px(1.),
|
||||
thickness: BORDER_WIDTH,
|
||||
color: None,
|
||||
wavy: false,
|
||||
}),
|
||||
|
||||
+62
-103
@@ -1,54 +1,71 @@
|
||||
//! Shared helpers for the editor's views: gutter cells, role colors, code
|
||||
//! line text, language mapping, change washes and diff row building.
|
||||
//! Shared helpers for the editor's views: gutter cells, role colors and run
|
||||
//! styles, code line text, language mapping and change washes.
|
||||
|
||||
#[cfg(any(feature = "diff_view", feature = "markdown_view"))]
|
||||
#[cfg(feature = "markdown_view")]
|
||||
use std::ops::Range;
|
||||
|
||||
#[cfg(feature = "markdown_view")]
|
||||
use crate::highlight::{Language, Line};
|
||||
#[cfg(feature = "markdown_view")]
|
||||
use gpui::StyledText;
|
||||
#[cfg(feature = "code_view")]
|
||||
use gpui::{App, HighlightStyle, Hsla, IntoElement, ParentElement as _, Styled as _, div};
|
||||
#[cfg(feature = "code_view")]
|
||||
use gpui_native_theme::{ActiveTheme as _, ColorTokens, Ramps};
|
||||
#[cfg(feature = "diff_view")]
|
||||
use similar::{Change, ChangeTag};
|
||||
use gpui_native_theme::{WASH_ALPHA, WASH_ALPHA_GUTTER, WASH_ALPHA_STRONG};
|
||||
|
||||
#[cfg(feature = "diff_view")]
|
||||
use crate::diff_view::{DiffRow, DiffRowKind};
|
||||
#[cfg(feature = "diff_view")]
|
||||
use crate::highlight::HighlightedCode;
|
||||
#[cfg(feature = "code_view")]
|
||||
use crate::highlight::Role;
|
||||
#[cfg(feature = "markdown_view")]
|
||||
use crate::highlight::{Language, Line};
|
||||
|
||||
/// Maps a highlight role to the theme's semantic palette.
|
||||
pub(crate) fn role_color(role: Role, colors: ColorTokens, ramps: Ramps) -> Hsla {
|
||||
match role {
|
||||
Role::Text => colors.foreground,
|
||||
Role::Keyword => ramps.primary.base,
|
||||
Role::String | Role::Escape => ramps.success.base,
|
||||
Role::Comment | Role::Punctuation => colors.muted_foreground,
|
||||
Role::Function | Role::Property => ramps.info.base,
|
||||
Role::Type => ramps.warning.base,
|
||||
Role::Number | Role::Constant => ramps.secondary.base,
|
||||
Role::Operator => colors.foreground,
|
||||
Role::Tag => ramps.destructive.base,
|
||||
#[cfg(feature = "code_view")]
|
||||
impl Role {
|
||||
/// Maps the role to the theme's semantic palette.
|
||||
pub(crate) fn color(self, colors: ColorTokens, ramps: Ramps) -> Hsla {
|
||||
match self {
|
||||
Role::Text => colors.foreground,
|
||||
Role::Keyword => ramps.primary.base,
|
||||
Role::String | Role::Escape => ramps.success.base,
|
||||
Role::Comment | Role::Punctuation => colors.muted_foreground,
|
||||
Role::Function | Role::Property => ramps.info.base,
|
||||
Role::Type => ramps.warning.base,
|
||||
Role::Number | Role::Constant => ramps.secondary.base,
|
||||
Role::Operator => colors.foreground,
|
||||
Role::Tag => ramps.destructive.base,
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds the highlight style for one run of a code view row.
|
||||
pub(crate) fn run_style(
|
||||
self,
|
||||
selected: bool,
|
||||
colors: ColorTokens,
|
||||
ramps: Ramps,
|
||||
) -> HighlightStyle {
|
||||
HighlightStyle {
|
||||
color: Some(self.color(colors, ramps)),
|
||||
background_color: selected.then_some(colors.selection),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds the highlight style for one run of a code view row.
|
||||
pub(crate) fn run_style(
|
||||
role: Role,
|
||||
selected: bool,
|
||||
colors: ColorTokens,
|
||||
ramps: Ramps,
|
||||
) -> HighlightStyle {
|
||||
HighlightStyle {
|
||||
color: Some(role_color(role, colors, ramps)),
|
||||
background_color: selected.then_some(colors.selection),
|
||||
..Default::default()
|
||||
#[cfg(feature = "markdown_view")]
|
||||
impl Language {
|
||||
/// Maps a fence info string to a bundled grammar.
|
||||
pub(crate) fn from_fence_info(info: &str) -> Option<Language> {
|
||||
match info.to_ascii_lowercase().as_str() {
|
||||
"rust" | "rs" => Some(Language::Rust),
|
||||
"typescript" | "ts" => Some(Language::TypeScript),
|
||||
"javascript" | "js" => Some(Language::JavaScript),
|
||||
"python" | "py" => Some(Language::Python),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A gutter cell sized to the whole row.
|
||||
#[cfg(feature = "code_view")]
|
||||
pub(crate) fn gutter_cell<E>(cx: &App, color: Hsla, content: E) -> gpui::Div
|
||||
where
|
||||
E: IntoElement,
|
||||
@@ -68,75 +85,29 @@ where
|
||||
.child(content)
|
||||
}
|
||||
|
||||
/// Builds a diff line row from a `similar` change.
|
||||
#[cfg(feature = "diff_view")]
|
||||
pub(crate) fn line_row(
|
||||
change: &Change<&str>,
|
||||
code: &HighlightedCode,
|
||||
changed_ranges: Vec<Range<usize>>,
|
||||
) -> DiffRow {
|
||||
let content = match change.tag() {
|
||||
ChangeTag::Equal => change
|
||||
.new_index()
|
||||
.and_then(|ix| code.lines.get(ix))
|
||||
.cloned()
|
||||
.unwrap_or_default(),
|
||||
ChangeTag::Delete => change
|
||||
.old_index()
|
||||
.and_then(|ix| code.lines.get(ix))
|
||||
.cloned()
|
||||
.unwrap_or_default(),
|
||||
ChangeTag::Insert => change
|
||||
.new_index()
|
||||
.and_then(|ix| code.lines.get(ix))
|
||||
.cloned()
|
||||
.unwrap_or_default(),
|
||||
};
|
||||
|
||||
let (old_number, new_number) = match change.tag() {
|
||||
ChangeTag::Equal => (
|
||||
change.old_index().map(|ix| ix + 1),
|
||||
change.new_index().map(|ix| ix + 1),
|
||||
),
|
||||
ChangeTag::Delete => (change.old_index().map(|ix| ix + 1), None),
|
||||
ChangeTag::Insert => (None, change.new_index().map(|ix| ix + 1)),
|
||||
};
|
||||
|
||||
DiffRow {
|
||||
kind: match change.tag() {
|
||||
ChangeTag::Equal => DiffRowKind::Context,
|
||||
ChangeTag::Delete => DiffRowKind::Removed,
|
||||
ChangeTag::Insert => DiffRowKind::Added,
|
||||
},
|
||||
content,
|
||||
old_number,
|
||||
new_number,
|
||||
changed_ranges,
|
||||
header: Default::default(),
|
||||
hidden_lines: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// A change wash: the ramp color at the alpha rows tint with.
|
||||
#[cfg(feature = "diff_view")]
|
||||
pub(crate) fn wash(color: Hsla) -> Hsla {
|
||||
Hsla {
|
||||
a: color.a * 0.14,
|
||||
a: color.a * WASH_ALPHA,
|
||||
..color
|
||||
}
|
||||
}
|
||||
|
||||
/// The stronger wash under number gutters and intra-line word changes.
|
||||
#[cfg(feature = "diff_view")]
|
||||
pub(crate) fn wash_strong(color: Hsla) -> Hsla {
|
||||
Hsla {
|
||||
a: color.a * 0.34,
|
||||
a: color.a * WASH_ALPHA_STRONG,
|
||||
..color
|
||||
}
|
||||
}
|
||||
|
||||
/// The subtle wash that sets the number gutter off from the code.
|
||||
#[cfg(feature = "diff_view")]
|
||||
pub(crate) fn wash_gutter(colors: &ColorTokens) -> Hsla {
|
||||
Hsla {
|
||||
a: colors.muted.a * 0.18,
|
||||
a: colors.muted.a * WASH_ALPHA_GUTTER,
|
||||
..colors.muted
|
||||
}
|
||||
}
|
||||
@@ -144,34 +115,22 @@ pub(crate) fn wash_gutter(colors: &ColorTokens) -> Hsla {
|
||||
/// One highlighted code line as styled text, roles resolved from the theme.
|
||||
#[cfg(feature = "markdown_view")]
|
||||
pub(crate) fn code_line(line: &Line, colors: ColorTokens, ramps: Ramps) -> StyledText {
|
||||
let mut text = String::new();
|
||||
let mut highlights: Vec<(Range<usize>, HighlightStyle)> = Vec::new();
|
||||
let mut offset = 0usize;
|
||||
|
||||
for span in &line.spans {
|
||||
let start = text.len();
|
||||
text.push_str(&span.text);
|
||||
let end = offset + span.text.len();
|
||||
if span.role != Role::Text {
|
||||
highlights.push((
|
||||
start..text.len(),
|
||||
offset..end,
|
||||
HighlightStyle {
|
||||
color: Some(role_color(span.role, colors, ramps)),
|
||||
color: Some(span.role.color(colors, ramps)),
|
||||
..Default::default()
|
||||
},
|
||||
));
|
||||
}
|
||||
offset = end;
|
||||
}
|
||||
|
||||
StyledText::new(text).with_highlights(highlights)
|
||||
}
|
||||
|
||||
/// Maps a fence info string to a bundled grammar.
|
||||
#[cfg(feature = "markdown_view")]
|
||||
pub(crate) fn grammar_language(info: &str) -> Option<Language> {
|
||||
match info.to_ascii_lowercase().as_str() {
|
||||
"rust" | "rs" => Some(Language::Rust),
|
||||
"typescript" | "ts" => Some(Language::TypeScript),
|
||||
"javascript" | "js" => Some(Language::JavaScript),
|
||||
"python" | "py" => Some(Language::Python),
|
||||
_ => None,
|
||||
}
|
||||
StyledText::new(line.text()).with_highlights(highlights)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user