Skip to main content

liora_components/
code_block.rs

1use crate::gpui_compat::element_id;
2use gpui::{
3    App, Bounds, ClipboardItem, Component, Context, ElementId, Entity, FocusHandle, Focusable,
4    FontStyle, FontWeight, GlobalElementId, Hsla, IntoElement, LayoutId, MouseButton,
5    MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, Pixels, Point, Render, RenderOnce,
6    Rgba, ShapedLine, SharedString, Style, StyledText, TextRun, TextStyle, UnderlineStyle,
7    WhiteSpace, Window, actions, div, fill, point, prelude::*, px, relative, size,
8};
9use liora_core::{Config, stable_unique_id};
10use liora_icons::Icon;
11use liora_icons_lucide::IconName;
12use std::{
13    collections::{HashMap, VecDeque},
14    hash::{Hash, Hasher},
15    ops::Range,
16    sync::{Arc, Mutex, MutexGuard, OnceLock},
17    time::{Duration, Instant},
18};
19use syntect::{
20    easy::HighlightLines,
21    highlighting::{FontStyle as SyntectFontStyle, Style as SyntectStyle, Theme},
22    parsing::SyntaxSet,
23    util::LinesWithEndings,
24};
25use two_face::theme::{EmbeddedLazyThemeSet, EmbeddedThemeName};
26
27actions!(code_block_actions, [CodeSelectAll, CodeCopy]);
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
30pub enum CodeLanguage {
31    PlainText,
32    Rust,
33    Toml,
34    Json,
35    Markdown,
36    Shell,
37    TypeScript,
38    JavaScript,
39}
40
41impl CodeLanguage {
42    pub fn label(self) -> &'static str {
43        match self {
44            Self::PlainText => "text",
45            Self::Rust => "rust",
46            Self::Toml => "toml",
47            Self::Json => "json",
48            Self::Markdown => "markdown",
49            Self::Shell => "shell",
50            Self::TypeScript => "typescript",
51            Self::JavaScript => "javascript",
52        }
53    }
54
55    fn syntect_token(self) -> &'static str {
56        match self {
57            Self::PlainText => "txt",
58            Self::Rust => "rs",
59            Self::Toml => "toml",
60            Self::Json => "json",
61            Self::Markdown => "md",
62            Self::Shell => "sh",
63            Self::TypeScript => "ts",
64            Self::JavaScript => "js",
65        }
66    }
67
68    pub fn from_label(label: &str) -> Self {
69        match label.trim().to_ascii_lowercase().as_str() {
70            "rs" | "rust" => Self::Rust,
71            "toml" => Self::Toml,
72            "json" => Self::Json,
73            "md" | "markdown" => Self::Markdown,
74            "sh" | "bash" | "shell" | "zsh" => Self::Shell,
75            "ts" | "tsx" | "typescript" => Self::TypeScript,
76            "js" | "jsx" | "javascript" => Self::JavaScript,
77            _ => Self::PlainText,
78        }
79    }
80}
81
82impl From<&str> for CodeLanguage {
83    fn from(value: &str) -> Self {
84        Self::from_label(value)
85    }
86}
87
88impl From<String> for CodeLanguage {
89    fn from(value: String) -> Self {
90        Self::from_label(&value)
91    }
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95pub enum CodeFormat {
96    Block,
97    Inline,
98}
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
101pub enum CodeHighlighter {
102    Syntect,
103}
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
106pub enum CodeTheme {
107    Auto,
108    Light,
109    Dark,
110    LioraLight,
111    LioraDark,
112    GitHubLight,
113    GitHubDark,
114    OneDark,
115    Nord,
116    Dracula,
117}
118
119#[derive(Debug, Clone, Copy, PartialEq, Eq)]
120enum CodeThemeMode {
121    Light,
122    Dark,
123}
124
125#[derive(Debug, Clone, Copy, PartialEq, Eq)]
126struct ResolvedCodeTheme {
127    theme: CodeTheme,
128    mode: CodeThemeMode,
129}
130
131impl CodeTheme {
132    pub fn label(self) -> &'static str {
133        match self {
134            Self::Auto => "auto",
135            Self::Light | Self::LioraLight => "liora-light",
136            Self::Dark | Self::LioraDark => "liora-dark",
137            Self::GitHubLight => "github-light",
138            Self::GitHubDark => "github-dark",
139            Self::OneDark => "one-dark",
140            Self::Nord => "nord",
141            Self::Dracula => "dracula",
142        }
143    }
144
145    fn mode(self) -> CodeThemeMode {
146        match self {
147            Self::Auto | Self::Light | Self::LioraLight | Self::GitHubLight => CodeThemeMode::Light,
148            Self::Dark
149            | Self::LioraDark
150            | Self::GitHubDark
151            | Self::OneDark
152            | Self::Nord
153            | Self::Dracula => CodeThemeMode::Dark,
154        }
155    }
156
157    fn embedded_theme(self) -> EmbeddedThemeName {
158        match self {
159            Self::Auto | Self::Light | Self::LioraLight => EmbeddedThemeName::CatppuccinLatte,
160            Self::Dark | Self::LioraDark => EmbeddedThemeName::CatppuccinMocha,
161            Self::GitHubLight => EmbeddedThemeName::Github,
162            Self::GitHubDark => EmbeddedThemeName::OneHalfDark,
163            Self::OneDark => EmbeddedThemeName::TwoDark,
164            Self::Nord => EmbeddedThemeName::Nord,
165            Self::Dracula => EmbeddedThemeName::Dracula,
166        }
167    }
168}
169
170pub struct CodeBlock {
171    code: SharedString,
172    language: CodeLanguage,
173    format: CodeFormat,
174    highlighter: CodeHighlighter,
175    theme: CodeTheme,
176    copyable: bool,
177    selectable: bool,
178    id: Option<ElementId>,
179    on_copy: Option<Arc<CodeCopyCallback>>,
180}
181
182type CodeCopyCallback = dyn Fn(&str, &mut Window, &mut App) + 'static;
183
184impl CodeBlock {
185    pub fn new(code: impl Into<SharedString>) -> Self {
186        Self {
187            code: code.into(),
188            language: CodeLanguage::PlainText,
189            format: CodeFormat::Block,
190            highlighter: CodeHighlighter::Syntect,
191            theme: CodeTheme::Auto,
192            copyable: true,
193            selectable: true,
194            id: None,
195            on_copy: None,
196        }
197    }
198
199    pub fn language(mut self, language: impl Into<CodeLanguage>) -> Self {
200        self.language = language.into();
201        self
202    }
203
204    pub fn rust(self) -> Self {
205        self.language(CodeLanguage::Rust)
206    }
207
208    pub fn toml(self) -> Self {
209        self.language(CodeLanguage::Toml)
210    }
211
212    pub fn json(self) -> Self {
213        self.language(CodeLanguage::Json)
214    }
215
216    pub fn markdown(self) -> Self {
217        self.language(CodeLanguage::Markdown)
218    }
219
220    pub fn shell(self) -> Self {
221        self.language(CodeLanguage::Shell)
222    }
223
224    pub fn typescript(self) -> Self {
225        self.language(CodeLanguage::TypeScript)
226    }
227
228    pub fn javascript(self) -> Self {
229        self.language(CodeLanguage::JavaScript)
230    }
231
232    pub fn format(mut self, format: CodeFormat) -> Self {
233        self.format = format;
234        self
235    }
236
237    pub fn inline(mut self) -> Self {
238        self.format = CodeFormat::Inline;
239        self.copyable = false;
240        self
241    }
242
243    pub fn highlighter(mut self, highlighter: CodeHighlighter) -> Self {
244        self.highlighter = highlighter;
245        self
246    }
247
248    pub fn syntect(self) -> Self {
249        self.highlighter(CodeHighlighter::Syntect)
250    }
251
252    pub fn theme(mut self, theme: CodeTheme) -> Self {
253        self.theme = theme;
254        self
255    }
256
257    pub fn auto_theme(self) -> Self {
258        self.theme(CodeTheme::Auto)
259    }
260
261    pub fn light_theme(self) -> Self {
262        self.theme(CodeTheme::Light)
263    }
264
265    pub fn dark_theme(self) -> Self {
266        self.theme(CodeTheme::Dark)
267    }
268
269    pub fn liora_light_theme(self) -> Self {
270        self.theme(CodeTheme::LioraLight)
271    }
272
273    pub fn liora_dark_theme(self) -> Self {
274        self.theme(CodeTheme::LioraDark)
275    }
276
277    pub fn github_light_theme(self) -> Self {
278        self.theme(CodeTheme::GitHubLight)
279    }
280
281    pub fn github_dark_theme(self) -> Self {
282        self.theme(CodeTheme::GitHubDark)
283    }
284
285    pub fn one_dark_theme(self) -> Self {
286        self.theme(CodeTheme::OneDark)
287    }
288
289    pub fn nord_theme(self) -> Self {
290        self.theme(CodeTheme::Nord)
291    }
292
293    pub fn dracula_theme(self) -> Self {
294        self.theme(CodeTheme::Dracula)
295    }
296
297    pub fn copyable(mut self, copyable: bool) -> Self {
298        self.copyable = copyable;
299        self
300    }
301
302    pub fn on_copy(mut self, callback: impl Fn(&str, &mut Window, &mut App) + 'static) -> Self {
303        self.on_copy = Some(Arc::new(callback));
304        self
305    }
306
307    pub fn selectable(mut self, selectable: bool) -> Self {
308        self.selectable = selectable;
309        self
310    }
311
312    pub fn register_key_bindings(cx: &mut App) {
313        cx.bind_keys([
314            gpui::KeyBinding::new("cmd-a", CodeSelectAll, Some("CodeBlock")),
315            gpui::KeyBinding::new("ctrl-a", CodeSelectAll, Some("CodeBlock")),
316            gpui::KeyBinding::new("cmd-c", CodeCopy, Some("CodeBlock")),
317            gpui::KeyBinding::new("ctrl-c", CodeCopy, Some("CodeBlock")),
318        ]);
319        Self::prewarm_highlighter();
320    }
321
322    pub fn prewarm_highlighter() {
323        let _ = syntax_set();
324        let themes = theme_set();
325        for theme in [
326            CodeTheme::LioraLight,
327            CodeTheme::LioraDark,
328            CodeTheme::GitHubLight,
329            CodeTheme::GitHubDark,
330            CodeTheme::OneDark,
331            CodeTheme::Nord,
332            CodeTheme::Dracula,
333        ] {
334            let _ = themes.get(theme.embedded_theme());
335        }
336    }
337
338    pub fn id(mut self, id: impl Into<ElementId>) -> Self {
339        self.id = Some(id.into());
340        self
341    }
342}
343
344impl RenderOnce for CodeBlock {
345    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
346        let theme = cx.global::<Config>().theme.clone();
347        let id = self.id.clone().unwrap_or_else(|| {
348            stable_unique_id(
349                format!(
350                    "liora-code-block:{}:{:016x}:{}:{:?}:{:?}:{:?}:copyable={}:selectable={}",
351                    self.language.label(),
352                    hash_code_text(self.code.as_ref()),
353                    self.code.len(),
354                    self.format,
355                    self.highlighter,
356                    self.theme,
357                    self.copyable,
358                    self.selectable
359                ),
360                "liora-code-block",
361                window,
362                cx,
363            )
364            .into()
365        });
366
367        match self.format {
368            CodeFormat::Inline => render_inline_code(
369                self.code,
370                self.language,
371                self.highlighter,
372                self.theme,
373                &theme,
374            ),
375            CodeFormat::Block => render_block_code(
376                id,
377                self.code,
378                self.language,
379                self.copyable,
380                self.selectable,
381                self.highlighter,
382                self.theme,
383                &theme,
384                self.on_copy,
385                window,
386                cx,
387            ),
388        }
389    }
390}
391
392impl IntoElement for CodeBlock {
393    type Element = Component<Self>;
394
395    fn into_element(self) -> Self::Element {
396        Component::new(self)
397    }
398}
399
400fn render_inline_code(
401    code: SharedString,
402    language: CodeLanguage,
403    highlighter: CodeHighlighter,
404    code_theme: CodeTheme,
405    theme: &liora_theme::Theme,
406) -> gpui::AnyElement {
407    let resolved_theme = resolve_code_theme(code_theme, theme);
408    div()
409        .rounded(px(theme.radius.sm))
410        .bg(code_surface(resolved_theme).opacity(0.72))
411        .border_1()
412        .border_color(code_border(resolved_theme))
413        .px_1()
414        .py_0p5()
415        .child(render_highlighted_text(
416            code,
417            language,
418            highlighter,
419            resolved_theme,
420            theme,
421            false,
422        ))
423        .into_any_element()
424}
425
426fn render_block_code(
427    id: ElementId,
428    code: SharedString,
429    language: CodeLanguage,
430    copyable: bool,
431    selectable: bool,
432    highlighter: CodeHighlighter,
433    code_theme: CodeTheme,
434    theme: &liora_theme::Theme,
435    on_copy: Option<Arc<CodeCopyCallback>>,
436    window: &mut Window,
437    cx: &mut App,
438) -> gpui::AnyElement {
439    let resolved_theme = resolve_code_theme(code_theme, theme);
440    let copied_code = code.to_string();
441    let scroll_id = format!("{id}-scroll");
442    let should_render_code = should_render_code_now(
443        id.clone(),
444        code.as_ref(),
445        language,
446        highlighter,
447        resolved_theme,
448        theme,
449        window,
450        cx,
451    );
452
453    let mut header = div()
454        .flex()
455        .items_center()
456        .justify_between()
457        .gap_2()
458        .px_4()
459        .py_2()
460        .border_b_1()
461        .border_color(code_border(resolved_theme))
462        .bg(code_header_surface(resolved_theme))
463        .child(
464            div()
465                .flex()
466                .items_center()
467                .gap_2()
468                .text_color(code_muted_text(resolved_theme))
469                .text_xs()
470                .font_weight(FontWeight::BOLD)
471                .child(
472                    Icon::new(IconName::FileCode)
473                        .size(px(14.0))
474                        .color(code_muted_text(resolved_theme)),
475                )
476                .child(language.label()),
477        );
478
479    if copyable {
480        header = header.child(
481            div()
482                .id(element_id(format!("{id}-copy")))
483                .flex()
484                .items_center()
485                .gap_1()
486                .px_2()
487                .py_1()
488                .rounded(px(theme.radius.sm))
489                .text_xs()
490                .text_color(code_muted_text(resolved_theme))
491                .cursor_pointer()
492                .hover(|style| {
493                    style
494                        .bg(code_hover_surface(resolved_theme))
495                        .text_color(code_accent(theme, resolved_theme))
496                })
497                .on_click(move |_, window, cx| {
498                    cx.write_to_clipboard(ClipboardItem::new_string(copied_code.clone()));
499                    if let Some(on_copy) = on_copy.as_ref() {
500                        on_copy(copied_code.as_str(), window, cx);
501                    }
502                })
503                .child(
504                    Icon::new(IconName::Copy)
505                        .size(px(12.0))
506                        .color(code_muted_text(resolved_theme)),
507                )
508                .child("Copy"),
509        );
510    }
511
512    div()
513        .id(id.clone())
514        .w_full()
515        .rounded(px(theme.radius.lg))
516        .border_1()
517        .border_color(code_border(resolved_theme))
518        .bg(code_surface(resolved_theme))
519        .overflow_hidden()
520        .child(header)
521        .child(
522            div()
523                .id(element_id(scroll_id))
524                .overflow_x_scroll()
525                .p_4()
526                .bg(code_surface(resolved_theme))
527                .cursor_text()
528                .child(if should_render_code {
529                    render_code_content(
530                        id,
531                        code,
532                        language,
533                        highlighter,
534                        resolved_theme,
535                        selectable,
536                        theme,
537                        window,
538                        cx,
539                    )
540                } else {
541                    render_code_placeholder(code, resolved_theme, theme)
542                }),
543        )
544        .into_any_element()
545}
546
547fn should_render_code_now(
548    id: ElementId,
549    code: &str,
550    language: CodeLanguage,
551    highlighter: CodeHighlighter,
552    code_theme: ResolvedCodeTheme,
553    theme: &liora_theme::Theme,
554    window: &mut Window,
555    cx: &mut App,
556) -> bool {
557    let cache_key = HighlightCacheKey::new(code, language, highlighter, code_theme, true, theme);
558    if lock_highlight_cache().runs.contains_key(&cache_key) {
559        return true;
560    }
561
562    let state_key = ElementId::NamedChild(Box::new(id), SharedString::from("deferred-code-ready"));
563    let ready = window.use_keyed_state(state_key, cx, |_, _| false);
564
565    if !*ready.read(cx) {
566        ready.update(cx, |ready, cx| {
567            *ready = true;
568            cx.notify();
569        });
570        return false;
571    }
572
573    if take_deferred_highlight_slot() {
574        true
575    } else {
576        ready.update(cx, |_, cx| {
577            cx.notify();
578        });
579        false
580    }
581}
582
583#[derive(Debug)]
584struct DeferredHighlightBudget {
585    window_start: Instant,
586    remaining: usize,
587}
588
589fn take_deferred_highlight_slot() -> bool {
590    static BUDGET: OnceLock<Mutex<DeferredHighlightBudget>> = OnceLock::new();
591    const FRAME_BUDGET: usize = 1;
592    const FRAME_WINDOW: Duration = Duration::from_millis(12);
593
594    let mut budget = BUDGET
595        .get_or_init(|| {
596            Mutex::new(DeferredHighlightBudget {
597                window_start: Instant::now(),
598                remaining: FRAME_BUDGET,
599            })
600        })
601        .lock()
602        .unwrap_or_else(|poisoned| poisoned.into_inner());
603
604    let now = Instant::now();
605    if now.duration_since(budget.window_start) >= FRAME_WINDOW {
606        budget.window_start = now;
607        budget.remaining = FRAME_BUDGET;
608    }
609
610    if budget.remaining > 0 {
611        budget.remaining -= 1;
612        true
613    } else {
614        false
615    }
616}
617
618fn render_code_placeholder(
619    code: SharedString,
620    code_theme: ResolvedCodeTheme,
621    theme: &liora_theme::Theme,
622) -> gpui::AnyElement {
623    let line_count = code.as_ref().lines().count().max(1).min(6);
624    div()
625        .flex()
626        .flex_col()
627        .gap_2()
628        .children((0..line_count).map(|index| {
629            let width = match index % 4 {
630                0 => px(520.0),
631                1 => px(380.0),
632                2 => px(460.0),
633                _ => px(300.0),
634            };
635            div()
636                .h(px(theme.font_size.sm * 1.15))
637                .w(width)
638                .rounded(px(theme.radius.sm))
639                .bg(code_muted_text(code_theme).opacity(0.16))
640        }))
641        .into_any_element()
642}
643
644fn render_highlighted_text(
645    code: SharedString,
646    language: CodeLanguage,
647    highlighter: CodeHighlighter,
648    code_theme: ResolvedCodeTheme,
649    theme: &liora_theme::Theme,
650    block: bool,
651) -> StyledText {
652    let runs = cached_highlight_runs(
653        code.as_ref(),
654        language,
655        highlighter,
656        code_theme,
657        theme,
658        block,
659    );
660    StyledText::new(code).with_runs(runs.to_vec())
661}
662
663fn render_code_content(
664    id: ElementId,
665    code: SharedString,
666    language: CodeLanguage,
667    highlighter: CodeHighlighter,
668    code_theme: ResolvedCodeTheme,
669    selectable: bool,
670    theme: &liora_theme::Theme,
671    window: &mut Window,
672    cx: &mut App,
673) -> gpui::AnyElement {
674    let (highlight_key, runs) = cached_highlight_runs_with_key(
675        code.as_ref(),
676        language,
677        highlighter,
678        code_theme,
679        theme,
680        true,
681    );
682
683    if selectable {
684        let state_key = ElementId::NamedChild(
685            Box::new(id.clone()),
686            SharedString::from("selectable-code-text"),
687        );
688        let initial_id = id.clone();
689        let initial_code = code.clone();
690        let initial_runs = runs.clone();
691        let initial_theme = theme.clone();
692        let initial_highlight_key = highlight_key.clone();
693        let input = window.use_keyed_state(state_key, cx, move |_, cx| {
694            SelectableCodeText::new(
695                cx,
696                initial_id,
697                initial_code,
698                initial_runs,
699                initial_highlight_key,
700                &initial_theme,
701            )
702        });
703        input.update(cx, |text, cx| {
704            text.update_content(id, code, runs, highlight_key, theme, cx);
705        });
706        SelectableCodeTextView { input }.into_any_element()
707    } else {
708        let state_key = ElementId::NamedChild(
709            Box::new(id.clone()),
710            SharedString::from("read-only-code-text"),
711        );
712        let initial_code = code.clone();
713        let initial_runs = runs.clone();
714        let initial_theme = theme.clone();
715        let initial_highlight_key = highlight_key.clone();
716        let input = window.use_keyed_state(state_key, cx, move |_, _| {
717            ReadOnlyCodeText::new(
718                initial_code,
719                initial_runs,
720                initial_highlight_key,
721                &initial_theme,
722            )
723        });
724        input.update(cx, |text, cx| {
725            text.update_content(code, runs, highlight_key, theme, cx);
726        });
727        ReadOnlyCodeTextView { input }.into_any_element()
728    }
729}
730
731struct SelectableCodeTextView {
732    input: Entity<SelectableCodeText>,
733}
734
735impl IntoElement for SelectableCodeTextView {
736    type Element = Component<Self>;
737
738    fn into_element(self) -> Self::Element {
739        Component::new(self)
740    }
741}
742
743impl RenderOnce for SelectableCodeTextView {
744    fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
745        self.input.into_any_element()
746    }
747}
748
749struct ReadOnlyCodeTextView {
750    input: Entity<ReadOnlyCodeText>,
751}
752
753impl IntoElement for ReadOnlyCodeTextView {
754    type Element = Component<Self>;
755
756    fn into_element(self) -> Self::Element {
757        Component::new(self)
758    }
759}
760
761impl RenderOnce for ReadOnlyCodeTextView {
762    fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
763        self.input.into_any_element()
764    }
765}
766
767fn cached_highlight_runs(
768    text: &str,
769    language: CodeLanguage,
770    highlighter: CodeHighlighter,
771    code_theme: ResolvedCodeTheme,
772    theme: &liora_theme::Theme,
773    block: bool,
774) -> Vec<TextRun> {
775    cached_highlight_runs_with_key(text, language, highlighter, code_theme, theme, block)
776        .1
777        .to_vec()
778}
779
780fn cached_highlight_runs_with_key(
781    text: &str,
782    language: CodeLanguage,
783    highlighter: CodeHighlighter,
784    code_theme: ResolvedCodeTheme,
785    theme: &liora_theme::Theme,
786    block: bool,
787) -> (HighlightCacheKey, HighlightRuns) {
788    let key = HighlightCacheKey::new(text, language, highlighter, code_theme, block, theme);
789    if let Some(runs) = lock_highlight_cache().runs.get(&key).cloned() {
790        return (key, runs);
791    }
792
793    let runs = HighlightRuns::from(highlight_runs(
794        text,
795        language,
796        highlighter,
797        code_theme,
798        theme,
799        block,
800    ));
801    let mut cache = lock_highlight_cache();
802    cache.insert(key.clone(), runs.clone());
803    (key, runs)
804}
805
806#[derive(Clone, Debug, PartialEq, Eq, Hash)]
807struct HighlightCacheKey {
808    text_hash: u64,
809    text_len: usize,
810    language: CodeLanguage,
811    highlighter: CodeHighlighter,
812    theme: CodeTheme,
813    block: bool,
814    font_size_bits: u32,
815}
816
817impl HighlightCacheKey {
818    fn new(
819        text: &str,
820        language: CodeLanguage,
821        highlighter: CodeHighlighter,
822        code_theme: ResolvedCodeTheme,
823        block: bool,
824        theme: &liora_theme::Theme,
825    ) -> Self {
826        let mut hasher = std::collections::hash_map::DefaultHasher::new();
827        text.hash(&mut hasher);
828        Self {
829            text_hash: hasher.finish(),
830            text_len: text.len(),
831            language,
832            highlighter,
833            theme: code_theme.theme,
834            block,
835            font_size_bits: if block {
836                theme.font_size.sm
837            } else {
838                theme.font_size.md
839            }
840            .to_bits(),
841        }
842    }
843}
844
845const HIGHLIGHT_CACHE_CAPACITY: usize = 256;
846type HighlightRuns = Arc<[TextRun]>;
847
848#[derive(Default)]
849struct HighlightCache {
850    runs: HashMap<HighlightCacheKey, HighlightRuns>,
851    order: VecDeque<HighlightCacheKey>,
852}
853
854impl HighlightCache {
855    fn insert(&mut self, key: HighlightCacheKey, runs: HighlightRuns) {
856        if self.runs.contains_key(&key) {
857            self.runs.insert(key, runs);
858            return;
859        }
860
861        self.runs.insert(key.clone(), runs);
862        self.order.push_back(key);
863        self.evict_over_capacity();
864    }
865
866    fn evict_over_capacity(&mut self) {
867        while self.runs.len() > HIGHLIGHT_CACHE_CAPACITY {
868            let Some(oldest) = self.order.pop_front() else {
869                break;
870            };
871            self.runs.remove(&oldest);
872        }
873    }
874}
875
876fn highlight_cache() -> &'static Mutex<HighlightCache> {
877    static CACHE: OnceLock<Mutex<HighlightCache>> = OnceLock::new();
878    CACHE.get_or_init(|| Mutex::new(HighlightCache::default()))
879}
880
881fn lock_highlight_cache() -> MutexGuard<'static, HighlightCache> {
882    highlight_cache()
883        .lock()
884        .unwrap_or_else(|poisoned| poisoned.into_inner())
885}
886
887fn highlight_runs(
888    text: &str,
889    language: CodeLanguage,
890    highlighter: CodeHighlighter,
891    code_theme: ResolvedCodeTheme,
892    theme: &liora_theme::Theme,
893    block: bool,
894) -> Vec<TextRun> {
895    match highlighter {
896        CodeHighlighter::Syntect => syntect_runs(text, language, code_theme, theme, block),
897    }
898}
899
900fn syntect_runs(
901    text: &str,
902    language: CodeLanguage,
903    code_theme: ResolvedCodeTheme,
904    theme: &liora_theme::Theme,
905    block: bool,
906) -> Vec<TextRun> {
907    if text.is_empty() {
908        return vec![base_style(theme, code_theme, block).to_run(0)];
909    }
910
911    let syntax_set = syntax_set();
912    let syntax = syntax_set
913        .find_syntax_by_token(language.syntect_token())
914        .or_else(|| syntax_set.find_syntax_by_extension(language.syntect_token()))
915        .unwrap_or_else(|| syntax_set.find_syntax_plain_text());
916    let syntect_theme = syntect_theme(code_theme);
917    let mut highlighter = HighlightLines::new(syntax, syntect_theme);
918    let mut runs = Vec::new();
919
920    for line in LinesWithEndings::from(text) {
921        match highlighter.highlight_line(line, syntax_set) {
922            Ok(regions) => {
923                for (style, slice) in regions {
924                    if !slice.is_empty() {
925                        push_run(
926                            &mut runs,
927                            syntect_style_run(slice.len(), style, theme, code_theme, block),
928                        );
929                    }
930                }
931            }
932            Err(_) => push_run(
933                &mut runs,
934                base_style(theme, code_theme, block).to_run(line.len()),
935            ),
936        }
937    }
938
939    if runs.is_empty() {
940        runs.push(base_style(theme, code_theme, block).to_run(text.len()));
941    }
942
943    runs
944}
945
946fn push_run(runs: &mut Vec<TextRun>, run: TextRun) {
947    if run.len == 0 {
948        return;
949    }
950
951    if let Some(last) = runs.last_mut() {
952        if last.font == run.font
953            && last.color == run.color
954            && last.background_color == run.background_color
955            && last.underline == run.underline
956            && last.strikethrough == run.strikethrough
957        {
958            last.len += run.len;
959            return;
960        }
961    }
962
963    runs.push(run);
964}
965
966fn syntect_style_run(
967    len: usize,
968    syntect_style: SyntectStyle,
969    theme: &liora_theme::Theme,
970    code_theme: ResolvedCodeTheme,
971    block: bool,
972) -> TextRun {
973    let mut style = base_style(theme, code_theme, block);
974    style.color = syntect_color(syntect_style.foreground);
975
976    if syntect_style.font_style.contains(SyntectFontStyle::BOLD) {
977        style.font_weight = FontWeight::BOLD;
978    }
979
980    if syntect_style.font_style.contains(SyntectFontStyle::ITALIC) {
981        style.font_style = FontStyle::Italic;
982    }
983
984    if syntect_style
985        .font_style
986        .contains(SyntectFontStyle::UNDERLINE)
987    {
988        style.underline = Some(UnderlineStyle {
989            thickness: px(1.0),
990            color: Some(style.color),
991            ..Default::default()
992        });
993    }
994
995    style.to_run(len)
996}
997
998fn base_style(theme: &liora_theme::Theme, code_theme: ResolvedCodeTheme, block: bool) -> TextStyle {
999    let mut style = TextStyle::default();
1000    style.font_family = "Monospace".into();
1001    style.font_size = px(if block {
1002        theme.font_size.sm
1003    } else {
1004        theme.font_size.md
1005    })
1006    .into();
1007    style.line_height = px(theme.font_size.md * 1.7).into();
1008    style.white_space = WhiteSpace::Nowrap;
1009    style.color = code_text(code_theme);
1010    style
1011}
1012
1013fn syntax_set() -> &'static SyntaxSet {
1014    static SYNTAX_SET: OnceLock<SyntaxSet> = OnceLock::new();
1015    SYNTAX_SET.get_or_init(two_face::syntax::extra_newlines)
1016}
1017
1018fn resolve_code_theme(code_theme: CodeTheme, theme: &liora_theme::Theme) -> ResolvedCodeTheme {
1019    let resolved = match code_theme {
1020        CodeTheme::Auto if theme.name.eq_ignore_ascii_case("dark") => CodeTheme::LioraDark,
1021        CodeTheme::Auto => CodeTheme::LioraLight,
1022        CodeTheme::Light => CodeTheme::LioraLight,
1023        CodeTheme::Dark => CodeTheme::LioraDark,
1024        theme => theme,
1025    };
1026
1027    ResolvedCodeTheme {
1028        theme: resolved,
1029        mode: resolved.mode(),
1030    }
1031}
1032
1033fn theme_set() -> &'static EmbeddedLazyThemeSet {
1034    static THEME_SET: OnceLock<EmbeddedLazyThemeSet> = OnceLock::new();
1035    THEME_SET.get_or_init(two_face::theme::extra)
1036}
1037
1038fn syntect_theme(code_theme: ResolvedCodeTheme) -> &'static Theme {
1039    theme_set().get(code_theme.theme.embedded_theme())
1040}
1041
1042fn syntect_color(color: syntect::highlighting::Color) -> Hsla {
1043    Rgba {
1044        r: color.r as f32 / 255.0,
1045        g: color.g as f32 / 255.0,
1046        b: color.b as f32 / 255.0,
1047        a: color.a as f32 / 255.0,
1048    }
1049    .into()
1050}
1051
1052fn code_surface(code_theme: ResolvedCodeTheme) -> Hsla {
1053    match code_theme.mode {
1054        CodeThemeMode::Light => rgb(0xf7f8fa),
1055        CodeThemeMode::Dark => rgb(0x1b2b34),
1056    }
1057}
1058
1059fn code_header_surface(code_theme: ResolvedCodeTheme) -> Hsla {
1060    match code_theme.mode {
1061        CodeThemeMode::Light => rgb(0xf0f2f5),
1062        CodeThemeMode::Dark => rgb(0x16242c),
1063    }
1064}
1065
1066fn code_hover_surface(code_theme: ResolvedCodeTheme) -> Hsla {
1067    match code_theme.mode {
1068        CodeThemeMode::Light => rgb(0xe8edf3),
1069        CodeThemeMode::Dark => rgb(0x253c49),
1070    }
1071}
1072
1073fn code_border(code_theme: ResolvedCodeTheme) -> Hsla {
1074    match code_theme.mode {
1075        CodeThemeMode::Light => rgb(0xd8dee8),
1076        CodeThemeMode::Dark => rgb(0x334d5c),
1077    }
1078}
1079
1080fn code_text(code_theme: ResolvedCodeTheme) -> Hsla {
1081    match code_theme.mode {
1082        CodeThemeMode::Light => rgb(0x2b303b),
1083        CodeThemeMode::Dark => rgb(0xc0c5ce),
1084    }
1085}
1086
1087fn code_muted_text(code_theme: ResolvedCodeTheme) -> Hsla {
1088    match code_theme.mode {
1089        CodeThemeMode::Light => rgb(0x65737e),
1090        CodeThemeMode::Dark => rgb(0xa7adba),
1091    }
1092}
1093
1094fn code_accent(theme: &liora_theme::Theme, code_theme: ResolvedCodeTheme) -> Hsla {
1095    match code_theme.mode {
1096        CodeThemeMode::Light => theme.info.base,
1097        CodeThemeMode::Dark => rgb(0x96b5b4),
1098    }
1099}
1100
1101struct ReadOnlyCodeText {
1102    code: SharedString,
1103    runs: HighlightRuns,
1104    highlight_key: HighlightCacheKey,
1105    theme: liora_theme::Theme,
1106    layout: Option<Arc<SelectableCodeLayout>>,
1107}
1108
1109impl ReadOnlyCodeText {
1110    fn new(
1111        code: SharedString,
1112        runs: HighlightRuns,
1113        highlight_key: HighlightCacheKey,
1114        theme: &liora_theme::Theme,
1115    ) -> Self {
1116        Self {
1117            code,
1118            runs,
1119            highlight_key,
1120            theme: theme.clone(),
1121            layout: None,
1122        }
1123    }
1124
1125    fn update_content(
1126        &mut self,
1127        code: SharedString,
1128        runs: HighlightRuns,
1129        highlight_key: HighlightCacheKey,
1130        theme: &liora_theme::Theme,
1131        cx: &mut Context<Self>,
1132    ) {
1133        let changed = self.highlight_key != highlight_key
1134            || self.theme.name != theme.name
1135            || self.theme.font_size.sm != theme.font_size.sm
1136            || self.theme.font_size.md != theme.font_size.md
1137            || self.theme.primary.base != theme.primary.base;
1138        if !changed {
1139            return;
1140        }
1141
1142        self.code = code;
1143        self.runs = runs;
1144        self.highlight_key = highlight_key;
1145        self.theme = theme.clone();
1146        self.layout = None;
1147        cx.notify();
1148    }
1149
1150    fn font_size(&self) -> Pixels {
1151        px(self.theme.font_size.md)
1152    }
1153
1154    fn line_height(&self) -> Pixels {
1155        px(self.theme.font_size.md * 1.7)
1156    }
1157
1158    fn ensure_layout(&mut self, window: &mut Window) -> Arc<SelectableCodeLayout> {
1159        if let Some(layout) = self.layout.as_ref() {
1160            return layout.clone();
1161        }
1162
1163        let layout = Arc::new(build_code_layout(
1164            self.code.as_ref(),
1165            &self.runs,
1166            self.font_size(),
1167            self.line_height(),
1168            window,
1169        ));
1170        self.layout = Some(layout.clone());
1171        layout
1172    }
1173}
1174
1175impl Render for ReadOnlyCodeText {
1176    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1177        div()
1178            .id("read-only-code-text")
1179            .child(ReadOnlyCodeElement { input: cx.entity() })
1180    }
1181}
1182
1183struct ReadOnlyCodeElement {
1184    input: Entity<ReadOnlyCodeText>,
1185}
1186
1187struct ReadOnlyCodePrepaint {
1188    layout: Arc<SelectableCodeLayout>,
1189}
1190
1191impl IntoElement for ReadOnlyCodeElement {
1192    type Element = Self;
1193
1194    fn into_element(self) -> Self::Element {
1195        self
1196    }
1197}
1198
1199impl Element for ReadOnlyCodeElement {
1200    type RequestLayoutState = Arc<SelectableCodeLayout>;
1201    type PrepaintState = ReadOnlyCodePrepaint;
1202
1203    fn id(&self) -> Option<ElementId> {
1204        None
1205    }
1206
1207    fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
1208        None
1209    }
1210
1211    fn request_layout(
1212        &mut self,
1213        _: Option<&GlobalElementId>,
1214        _: Option<&gpui::InspectorElementId>,
1215        window: &mut Window,
1216        cx: &mut App,
1217    ) -> (LayoutId, Arc<SelectableCodeLayout>) {
1218        let layout = self
1219            .input
1220            .update(cx, |input, _| input.ensure_layout(window));
1221        let mut style = Style::default();
1222        style.size.width = layout.width.into();
1223        style.min_size.width = relative(1.).into();
1224        style.size.height = layout.height.into();
1225        (window.request_layout(style, [], cx), layout)
1226    }
1227
1228    fn prepaint(
1229        &mut self,
1230        _: Option<&GlobalElementId>,
1231        _: Option<&gpui::InspectorElementId>,
1232        _bounds: Bounds<Pixels>,
1233        layout: &mut Arc<SelectableCodeLayout>,
1234        _window: &mut Window,
1235        _cx: &mut App,
1236    ) -> ReadOnlyCodePrepaint {
1237        ReadOnlyCodePrepaint {
1238            layout: layout.clone(),
1239        }
1240    }
1241
1242    fn paint(
1243        &mut self,
1244        _: Option<&GlobalElementId>,
1245        _: Option<&gpui::InspectorElementId>,
1246        bounds: Bounds<Pixels>,
1247        _: &mut Arc<SelectableCodeLayout>,
1248        prepaint: &mut ReadOnlyCodePrepaint,
1249        window: &mut Window,
1250        cx: &mut App,
1251    ) {
1252        let line_height = self.input.read(cx).line_height();
1253        for line in &prepaint.layout.lines {
1254            let _ = line.shaped.paint(
1255                point(bounds.left(), bounds.top() + line.y),
1256                line_height,
1257                window,
1258                cx,
1259            );
1260        }
1261    }
1262}
1263
1264#[derive(Clone)]
1265struct SelectableCodeState {
1266    selected_range: Range<usize>,
1267    selection_reversed: bool,
1268    selecting: bool,
1269    lines: Vec<(ShapedLine, Pixels, usize)>,
1270    bounds: Option<Bounds<Pixels>>,
1271}
1272
1273impl Default for SelectableCodeState {
1274    fn default() -> Self {
1275        Self {
1276            selected_range: 0..0,
1277            selection_reversed: false,
1278            selecting: false,
1279            lines: Vec::new(),
1280            bounds: None,
1281        }
1282    }
1283}
1284
1285#[derive(Clone)]
1286struct SelectableCodeLayout {
1287    lines: Vec<SelectableCodeLine>,
1288    width: Pixels,
1289    height: Pixels,
1290}
1291
1292#[derive(Clone)]
1293struct SelectableCodeLine {
1294    shaped: ShapedLine,
1295    start: usize,
1296    y: Pixels,
1297}
1298
1299fn selectable_state_map() -> &'static Mutex<HashMap<String, SelectableCodeState>> {
1300    static STATES: OnceLock<Mutex<HashMap<String, SelectableCodeState>>> = OnceLock::new();
1301    STATES.get_or_init(|| Mutex::new(HashMap::new()))
1302}
1303
1304fn lock_selectable_state_map() -> MutexGuard<'static, HashMap<String, SelectableCodeState>> {
1305    selectable_state_map()
1306        .lock()
1307        .unwrap_or_else(|poisoned| poisoned.into_inner())
1308}
1309
1310fn set_selectable_layout_state(
1311    id: &ElementId,
1312    lines: Vec<(ShapedLine, Pixels, usize)>,
1313    bounds: Bounds<Pixels>,
1314) {
1315    with_selectable_state(id, |state| {
1316        state.lines = lines;
1317        state.bounds = Some(bounds);
1318    });
1319}
1320
1321fn selectable_key(id: &ElementId) -> String {
1322    id.to_string()
1323}
1324
1325fn with_selectable_state<R>(id: &ElementId, f: impl FnOnce(&mut SelectableCodeState) -> R) -> R {
1326    let mut states = lock_selectable_state_map();
1327    f(states.entry(selectable_key(id)).or_default())
1328}
1329
1330fn selectable_state_snapshot(id: &ElementId) -> SelectableCodeState {
1331    lock_selectable_state_map()
1332        .get(&selectable_key(id))
1333        .cloned()
1334        .unwrap_or_default()
1335}
1336
1337struct SelectableCodeText {
1338    id: ElementId,
1339    focus_handle: FocusHandle,
1340    code: SharedString,
1341    runs: HighlightRuns,
1342    highlight_key: HighlightCacheKey,
1343    theme: liora_theme::Theme,
1344    layout: Option<Arc<SelectableCodeLayout>>,
1345}
1346
1347impl SelectableCodeText {
1348    fn new(
1349        cx: &mut Context<Self>,
1350        id: ElementId,
1351        code: SharedString,
1352        runs: HighlightRuns,
1353        highlight_key: HighlightCacheKey,
1354        theme: &liora_theme::Theme,
1355    ) -> Self {
1356        Self {
1357            id,
1358            focus_handle: cx.focus_handle(),
1359            code,
1360            runs,
1361            highlight_key,
1362            theme: theme.clone(),
1363            layout: None,
1364        }
1365    }
1366
1367    fn update_content(
1368        &mut self,
1369        id: ElementId,
1370        code: SharedString,
1371        runs: HighlightRuns,
1372        highlight_key: HighlightCacheKey,
1373        theme: &liora_theme::Theme,
1374        cx: &mut Context<Self>,
1375    ) {
1376        let changed = self.id != id
1377            || self.highlight_key != highlight_key
1378            || self.theme.name != theme.name
1379            || self.theme.font_size.sm != theme.font_size.sm
1380            || self.theme.font_size.md != theme.font_size.md
1381            || self.theme.primary.base != theme.primary.base;
1382        if !changed {
1383            return;
1384        }
1385
1386        let old_id = self.id.clone();
1387        self.id = id;
1388        self.code = code;
1389        self.runs = runs;
1390        self.highlight_key = highlight_key;
1391        self.theme = theme.clone();
1392        self.layout = None;
1393
1394        if old_id != self.id {
1395            let old_state = selectable_state_snapshot(&old_id);
1396            with_selectable_state(&self.id, |state| *state = old_state);
1397        }
1398
1399        with_selectable_state(&self.id, |state| {
1400            state.selected_range.start = self.clamp_boundary(state.selected_range.start);
1401            state.selected_range.end = self.clamp_boundary(state.selected_range.end);
1402            if state.selected_range.end < state.selected_range.start {
1403                state.selected_range = state.selected_range.end..state.selected_range.start;
1404                state.selection_reversed = !state.selection_reversed;
1405            }
1406        });
1407        cx.notify();
1408    }
1409
1410    fn move_to(&self, state: &mut SelectableCodeState, offset: usize) -> bool {
1411        let offset = self.clamp_boundary(offset);
1412        if state.selected_range == (offset..offset) && !state.selection_reversed {
1413            return false;
1414        }
1415        state.selected_range = offset..offset;
1416        state.selection_reversed = false;
1417        true
1418    }
1419
1420    fn select_to(&self, state: &mut SelectableCodeState, offset: usize) -> bool {
1421        let offset = self.clamp_boundary(offset);
1422        let previous_range = state.selected_range.clone();
1423        let previous_reversed = state.selection_reversed;
1424        if state.selection_reversed {
1425            state.selected_range.start = offset;
1426        } else {
1427            state.selected_range.end = offset;
1428        }
1429        if state.selected_range.end < state.selected_range.start {
1430            state.selection_reversed = !state.selection_reversed;
1431            state.selected_range = state.selected_range.end..state.selected_range.start;
1432        }
1433        if state.selected_range == previous_range && state.selection_reversed == previous_reversed {
1434            return false;
1435        }
1436        true
1437    }
1438
1439    fn clamp_boundary(&self, mut offset: usize) -> usize {
1440        offset = offset.min(self.code.len());
1441        while offset > 0 && !self.code.is_char_boundary(offset) {
1442            offset -= 1;
1443        }
1444        offset
1445    }
1446
1447    fn index_for_point(&self, pt: Point<Pixels>) -> usize {
1448        let state = selectable_state_snapshot(&self.id);
1449        let Some(bounds) = state.bounds.as_ref() else {
1450            return self.code.len();
1451        };
1452        if state.lines.is_empty() {
1453            return 0;
1454        }
1455
1456        let mut chosen = 0;
1457        for (ix, (_line, y, _start)) in state.lines.iter().enumerate() {
1458            let line_height = self.line_height();
1459            if pt.y >= *y && pt.y < *y + line_height {
1460                chosen = ix;
1461                break;
1462            }
1463            if pt.y >= *y {
1464                chosen = ix;
1465            }
1466        }
1467
1468        let (line, _y, start) = &state.lines[chosen];
1469        let x = pt.x - bounds.left();
1470        let line_index = line.index_for_x(x).unwrap_or(line.len());
1471        self.clamp_boundary(*start + line_index)
1472    }
1473
1474    fn select_all(&mut self, _: &CodeSelectAll, _: &mut Window, cx: &mut Context<Self>) {
1475        let changed = with_selectable_state(&self.id, |state| {
1476            let changed = state.selected_range != (0..self.code.len()) || state.selection_reversed;
1477            state.selected_range = 0..self.code.len();
1478            state.selection_reversed = false;
1479            changed
1480        });
1481        if changed {
1482            cx.notify();
1483        }
1484    }
1485
1486    fn copy(&mut self, _: &CodeCopy, _: &mut Window, cx: &mut Context<Self>) {
1487        let selected_range = selectable_state_snapshot(&self.id).selected_range;
1488        if !selected_range.is_empty() {
1489            cx.write_to_clipboard(ClipboardItem::new_string(
1490                self.code[selected_range].to_string(),
1491            ));
1492        }
1493    }
1494
1495    fn on_mouse_down(
1496        &mut self,
1497        event: &MouseDownEvent,
1498        window: &mut Window,
1499        cx: &mut Context<Self>,
1500    ) {
1501        window.focus(&self.focus_handle);
1502        let idx = self.index_for_point(event.position);
1503        let changed = with_selectable_state(&self.id, |state| {
1504            let was_selecting = state.selecting;
1505            state.selecting = true;
1506            if event.modifiers.shift {
1507                self.select_to(state, idx) || !was_selecting
1508            } else if event.click_count >= 3 {
1509                let changed = state.selected_range != (0..self.code.len())
1510                    || state.selection_reversed
1511                    || !was_selecting;
1512                state.selected_range = 0..self.code.len();
1513                state.selection_reversed = false;
1514                changed
1515            } else if event.click_count == 2 {
1516                let range = self.word_range_at(idx);
1517                let changed =
1518                    state.selected_range != range || state.selection_reversed || !was_selecting;
1519                state.selected_range = range;
1520                state.selection_reversed = false;
1521                changed
1522            } else {
1523                self.move_to(state, idx) || !was_selecting
1524            }
1525        });
1526        if changed {
1527            cx.notify();
1528        }
1529    }
1530
1531    fn on_mouse_move(&mut self, event: &MouseMoveEvent, cx: &mut Context<Self>) {
1532        let dragging = event.pressed_button == Some(MouseButton::Left);
1533        let idx = dragging.then(|| self.index_for_point(event.position));
1534        let changed = with_selectable_state(&self.id, |state| {
1535            if !dragging {
1536                let changed = state.selecting;
1537                state.selecting = false;
1538                changed
1539            } else if state.selecting {
1540                self.select_to(state, idx.unwrap_or(self.code.len()))
1541            } else {
1542                false
1543            }
1544        });
1545        if changed {
1546            cx.notify();
1547        }
1548    }
1549
1550    fn on_mouse_up(&mut self, _: &MouseUpEvent, _: &mut Window, cx: &mut Context<Self>) {
1551        let changed = with_selectable_state(&self.id, |state| {
1552            let changed = state.selecting;
1553            state.selecting = false;
1554            changed
1555        });
1556        if changed {
1557            cx.notify();
1558        }
1559    }
1560
1561    fn word_range_at(&self, idx: usize) -> Range<usize> {
1562        let text = self.code.as_ref();
1563        if text.is_empty() {
1564            return 0..0;
1565        }
1566        let idx = self.clamp_boundary(idx);
1567        let mut start = idx;
1568        while start > 0 {
1569            let prev = self.prev_char(start);
1570            let ch = text[prev..start].chars().next().unwrap_or(' ');
1571            if !ch.is_alphanumeric() && ch != '_' {
1572                break;
1573            }
1574            start = prev;
1575        }
1576        let mut end = idx;
1577        while end < text.len() {
1578            let next = self.next_char(end);
1579            let ch = text[end..next].chars().next().unwrap_or(' ');
1580            if !ch.is_alphanumeric() && ch != '_' {
1581                break;
1582            }
1583            end = next;
1584        }
1585        start..end
1586    }
1587
1588    fn prev_char(&self, offset: usize) -> usize {
1589        if offset == 0 {
1590            return 0;
1591        }
1592        let mut prev = offset - 1;
1593        while prev > 0 && !self.code.is_char_boundary(prev) {
1594            prev -= 1;
1595        }
1596        prev
1597    }
1598
1599    fn next_char(&self, offset: usize) -> usize {
1600        if offset >= self.code.len() {
1601            return self.code.len();
1602        }
1603        let mut next = offset + 1;
1604        while next < self.code.len() && !self.code.is_char_boundary(next) {
1605            next += 1;
1606        }
1607        next
1608    }
1609
1610    fn font_size(&self) -> Pixels {
1611        px(self.theme.font_size.md)
1612    }
1613
1614    fn line_height(&self) -> Pixels {
1615        px(self.theme.font_size.md * 1.7)
1616    }
1617
1618    fn ensure_layout(&mut self, window: &mut Window) -> Arc<SelectableCodeLayout> {
1619        if let Some(layout) = self.layout.as_ref() {
1620            return layout.clone();
1621        }
1622
1623        let layout = Arc::new(build_code_layout(
1624            self.code.as_ref(),
1625            &self.runs,
1626            self.font_size(),
1627            self.line_height(),
1628            window,
1629        ));
1630        self.layout = Some(layout.clone());
1631        layout
1632    }
1633}
1634
1635impl Focusable for SelectableCodeText {
1636    fn focus_handle(&self, _cx: &App) -> FocusHandle {
1637        self.focus_handle.clone()
1638    }
1639}
1640
1641struct SelectableCodeElement {
1642    id: ElementId,
1643    input: Entity<SelectableCodeText>,
1644}
1645
1646struct SelectableCodePrepaint {
1647    layout: Arc<SelectableCodeLayout>,
1648    selection: Vec<PaintQuad>,
1649    hitbox: gpui::Hitbox,
1650}
1651
1652impl IntoElement for SelectableCodeElement {
1653    type Element = Self;
1654
1655    fn into_element(self) -> Self::Element {
1656        self
1657    }
1658}
1659
1660impl Element for SelectableCodeElement {
1661    type RequestLayoutState = Arc<SelectableCodeLayout>;
1662    type PrepaintState = SelectableCodePrepaint;
1663
1664    fn id(&self) -> Option<ElementId> {
1665        Some(self.id.clone())
1666    }
1667
1668    fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
1669        None
1670    }
1671
1672    fn request_layout(
1673        &mut self,
1674        _: Option<&GlobalElementId>,
1675        _: Option<&gpui::InspectorElementId>,
1676        window: &mut Window,
1677        cx: &mut App,
1678    ) -> (LayoutId, Arc<SelectableCodeLayout>) {
1679        let layout = self
1680            .input
1681            .update(cx, |input, _| input.ensure_layout(window));
1682        let mut style = Style::default();
1683        style.size.width = layout.width.into();
1684        style.min_size.width = relative(1.).into();
1685        style.size.height = layout.height.into();
1686        (window.request_layout(style, [], cx), layout)
1687    }
1688
1689    fn prepaint(
1690        &mut self,
1691        _: Option<&GlobalElementId>,
1692        _: Option<&gpui::InspectorElementId>,
1693        bounds: Bounds<Pixels>,
1694        layout: &mut Arc<SelectableCodeLayout>,
1695        window: &mut Window,
1696        cx: &mut App,
1697    ) -> SelectableCodePrepaint {
1698        let input = self.input.read(cx);
1699        let line_height = input.line_height();
1700        let mut state_lines = Vec::new();
1701        let mut selection_quads = Vec::new();
1702        let selected_range = selectable_state_snapshot(&input.id).selected_range;
1703
1704        for line in &layout.lines {
1705            let y = bounds.top() + line.y;
1706            if !selected_range.is_empty() {
1707                let line_end = line.start + line.shaped.len();
1708                let start = selected_range.start.max(line.start);
1709                let end = selected_range.end.min(line_end);
1710                if start < end {
1711                    let x_start = line.shaped.x_for_index(start - line.start);
1712                    let x_end = line.shaped.x_for_index(end - line.start);
1713                    selection_quads.push(fill(
1714                        Bounds::new(
1715                            point(bounds.left() + x_start, y),
1716                            size(x_end - x_start, line_height),
1717                        ),
1718                        input.theme.primary.base.opacity(0.28),
1719                    ));
1720                }
1721            }
1722
1723            state_lines.push((line.shaped.clone(), y, line.start));
1724        }
1725
1726        let hitbox = window.insert_hitbox(bounds, gpui::HitboxBehavior::Normal);
1727        set_selectable_layout_state(&input.id, state_lines, bounds);
1728
1729        SelectableCodePrepaint {
1730            layout: layout.clone(),
1731            selection: selection_quads,
1732            hitbox,
1733        }
1734    }
1735
1736    fn paint(
1737        &mut self,
1738        _: Option<&GlobalElementId>,
1739        _: Option<&gpui::InspectorElementId>,
1740        bounds: Bounds<Pixels>,
1741        _: &mut Arc<SelectableCodeLayout>,
1742        prepaint: &mut SelectableCodePrepaint,
1743        window: &mut Window,
1744        cx: &mut App,
1745    ) {
1746        let focus_handle = self.input.read(cx).focus_handle.clone();
1747        window.set_cursor_style(gpui::CursorStyle::IBeam, &prepaint.hitbox);
1748
1749        let input = self.input.clone();
1750        let focus_handle_for_down = focus_handle.clone();
1751        let hitbox = prepaint.hitbox.clone();
1752        window.on_mouse_event(move |event: &MouseDownEvent, phase, window, cx| {
1753            if phase.bubble() && event.button == MouseButton::Left && hitbox.is_hovered(window) {
1754                window.focus(&focus_handle_for_down);
1755                input.update(cx, |input, cx| input.on_mouse_down(event, window, cx));
1756                cx.stop_propagation();
1757            }
1758        });
1759
1760        let input = self.input.clone();
1761        window.on_mouse_event(move |event: &MouseMoveEvent, phase, _window, cx| {
1762            if phase.capture() {
1763                input.update(cx, |input, cx| input.on_mouse_move(event, cx));
1764            }
1765        });
1766
1767        let input = self.input.clone();
1768        window.on_mouse_event(move |event: &MouseUpEvent, phase, window, cx| {
1769            if phase.capture() && event.button == MouseButton::Left {
1770                input.update(cx, |input, cx| input.on_mouse_up(event, window, cx));
1771            }
1772        });
1773
1774        for selection in prepaint.selection.drain(..) {
1775            window.paint_quad(selection);
1776        }
1777
1778        for line in &prepaint.layout.lines {
1779            let _ = line.shaped.paint(
1780                point(bounds.left(), bounds.top() + line.y),
1781                self.input.read(cx).line_height(),
1782                window,
1783                cx,
1784            );
1785        }
1786    }
1787}
1788
1789impl Render for SelectableCodeText {
1790    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1791        div()
1792            .id(element_id(format!("{}-selectable", self.id)))
1793            .key_context("CodeBlock")
1794            .track_focus(&self.focus_handle(cx))
1795            .cursor_text()
1796            .on_action(cx.listener(Self::select_all))
1797            .on_action(cx.listener(Self::copy))
1798            .child(SelectableCodeElement {
1799                id: element_id(format!("{}-text", self.id)),
1800                input: cx.entity(),
1801            })
1802    }
1803}
1804
1805fn build_code_layout(
1806    code: &str,
1807    runs: &[TextRun],
1808    font_size: Pixels,
1809    line_height: Pixels,
1810    window: &mut Window,
1811) -> SelectableCodeLayout {
1812    let mut max_width = px(1.0);
1813    let mut offset = 0;
1814    let mut y = px(0.0);
1815    let mut lines = Vec::new();
1816    for line in code_lines(code) {
1817        let line_len = line.len();
1818        let line_runs = slice_runs(runs, offset, offset + line_len);
1819        let shaped = window.text_system().shape_line(
1820            SharedString::from(line.to_string()),
1821            font_size,
1822            &line_runs,
1823            None,
1824        );
1825        max_width = max_width.max(shaped.width);
1826        lines.push(SelectableCodeLine {
1827            shaped,
1828            start: offset,
1829            y,
1830        });
1831        offset += line_len + 1;
1832        y += line_height;
1833    }
1834
1835    SelectableCodeLayout {
1836        height: line_height * lines.len() as f32,
1837        lines,
1838        width: max_width,
1839    }
1840}
1841
1842fn hash_code_text(text: &str) -> u64 {
1843    let mut hasher = std::collections::hash_map::DefaultHasher::new();
1844    text.hash(&mut hasher);
1845    hasher.finish()
1846}
1847
1848fn code_lines(text: &str) -> impl Iterator<Item = &str> {
1849    text.strip_suffix('\n').unwrap_or(text).split('\n')
1850}
1851
1852fn slice_runs(runs: &[TextRun], start: usize, end: usize) -> Vec<TextRun> {
1853    let mut sliced = Vec::new();
1854    let mut offset = 0;
1855    for run in runs {
1856        let run_start = offset;
1857        let run_end = offset + run.len;
1858        let overlap_start = start.max(run_start);
1859        let overlap_end = end.min(run_end);
1860        if overlap_start < overlap_end {
1861            sliced.push(TextRun {
1862                len: overlap_end - overlap_start,
1863                ..run.clone()
1864            });
1865        }
1866        offset = run_end;
1867        if offset >= end {
1868            break;
1869        }
1870    }
1871    if sliced.is_empty() && start == end {
1872        return sliced;
1873    }
1874    sliced
1875}
1876
1877fn rgb(hex: u32) -> Hsla {
1878    Rgba {
1879        r: ((hex >> 16) & 0xff) as f32 / 255.0,
1880        g: ((hex >> 8) & 0xff) as f32 / 255.0,
1881        b: (hex & 0xff) as f32 / 255.0,
1882        a: 1.0,
1883    }
1884    .into()
1885}
1886
1887#[cfg(test)]
1888mod tests {
1889    use super::*;
1890
1891    #[test]
1892    fn language_labels_parse_common_aliases() {
1893        assert_eq!(CodeLanguage::from_label("rs"), CodeLanguage::Rust);
1894        assert_eq!(CodeLanguage::from_label("bash"), CodeLanguage::Shell);
1895        assert_eq!(CodeLanguage::from_label("tsx"), CodeLanguage::TypeScript);
1896        assert_eq!(CodeLanguage::from_label("unknown"), CodeLanguage::PlainText);
1897    }
1898
1899    #[test]
1900    fn syntect_highlighter_generates_multiple_styled_runs_for_rust() {
1901        let theme = liora_theme::Theme::light();
1902        let code_theme = resolve_code_theme(CodeTheme::Auto, &theme);
1903        let runs = syntect_runs(
1904            "fn main() { let n = 42; // ok\n println!(\"hi\"); }",
1905            CodeLanguage::Rust,
1906            code_theme,
1907            &theme,
1908            true,
1909        );
1910
1911        assert!(runs.len() > 3);
1912        assert_eq!(runs.iter().map(|run| run.len).sum::<usize>(), 48);
1913        assert!(runs.iter().any(|run| run.color != code_text(code_theme)));
1914    }
1915
1916    #[test]
1917    fn themes_resolve_to_distinct_syntect_and_surface_palettes() {
1918        let light = liora_theme::Theme::light();
1919        let dark = liora_theme::Theme::dark();
1920
1921        assert_eq!(
1922            resolve_code_theme(CodeTheme::Auto, &light),
1923            ResolvedCodeTheme {
1924                theme: CodeTheme::LioraLight,
1925                mode: CodeThemeMode::Light
1926            }
1927        );
1928        assert_eq!(
1929            resolve_code_theme(CodeTheme::Auto, &dark),
1930            ResolvedCodeTheme {
1931                theme: CodeTheme::LioraDark,
1932                mode: CodeThemeMode::Dark
1933            }
1934        );
1935        assert_ne!(
1936            syntect_theme(ResolvedCodeTheme {
1937                theme: CodeTheme::LioraLight,
1938                mode: CodeThemeMode::Light,
1939            })
1940            .settings
1941            .background,
1942            syntect_theme(ResolvedCodeTheme {
1943                theme: CodeTheme::LioraDark,
1944                mode: CodeThemeMode::Dark,
1945            })
1946            .settings
1947            .background
1948        );
1949        assert_ne!(
1950            code_surface(ResolvedCodeTheme {
1951                theme: CodeTheme::LioraLight,
1952                mode: CodeThemeMode::Light,
1953            }),
1954            code_surface(ResolvedCodeTheme {
1955                theme: CodeTheme::LioraDark,
1956                mode: CodeThemeMode::Dark,
1957            })
1958        );
1959    }
1960
1961    #[test]
1962    fn cached_highlight_runs_reuses_render_runs_for_same_code_and_theme() {
1963        let theme = liora_theme::Theme::light();
1964        let code_theme = resolve_code_theme(CodeTheme::Auto, &theme);
1965        let first = cached_highlight_runs(
1966            "let cached = true;",
1967            CodeLanguage::Rust,
1968            CodeHighlighter::Syntect,
1969            code_theme,
1970            &theme,
1971            true,
1972        );
1973        let second = cached_highlight_runs(
1974            "let cached = true;",
1975            CodeLanguage::Rust,
1976            CodeHighlighter::Syntect,
1977            code_theme,
1978            &theme,
1979            true,
1980        );
1981
1982        assert_eq!(first, second);
1983    }
1984
1985    #[test]
1986    fn cached_highlight_runs_share_arc_storage_for_block_layouts() {
1987        let theme = liora_theme::Theme::light();
1988        let code_theme = resolve_code_theme(CodeTheme::Auto, &theme);
1989        let (_, first) = cached_highlight_runs_with_key(
1990            "fn shared_runs() { println!(\"cache\"); }",
1991            CodeLanguage::Rust,
1992            CodeHighlighter::Syntect,
1993            code_theme,
1994            &theme,
1995            true,
1996        );
1997        let (_, second) = cached_highlight_runs_with_key(
1998            "fn shared_runs() { println!(\"cache\"); }",
1999            CodeLanguage::Rust,
2000            CodeHighlighter::Syntect,
2001            code_theme,
2002            &theme,
2003            true,
2004        );
2005
2006        assert!(Arc::ptr_eq(&first, &second));
2007    }
2008
2009    #[test]
2010    fn highlight_cache_evicts_incrementally_without_clearing_all_runs() {
2011        let mut cache = HighlightCache::default();
2012        let theme = liora_theme::Theme::light();
2013        let code_theme = resolve_code_theme(CodeTheme::Auto, &theme);
2014        let first_key = HighlightCacheKey::new(
2015            "let item_0 = 0;",
2016            CodeLanguage::Rust,
2017            CodeHighlighter::Syntect,
2018            code_theme,
2019            true,
2020            &theme,
2021        );
2022        let last_key = HighlightCacheKey::new(
2023            "let item_256 = 256;",
2024            CodeLanguage::Rust,
2025            CodeHighlighter::Syntect,
2026            code_theme,
2027            true,
2028            &theme,
2029        );
2030
2031        for index in 0..=HIGHLIGHT_CACHE_CAPACITY {
2032            let text = format!("let item_{index} = {index};");
2033            let key = HighlightCacheKey::new(
2034                &text,
2035                CodeLanguage::Rust,
2036                CodeHighlighter::Syntect,
2037                code_theme,
2038                true,
2039                &theme,
2040            );
2041            cache.insert(
2042                key,
2043                HighlightRuns::from(vec![
2044                    base_style(&theme, code_theme, true).to_run(text.len()),
2045                ]),
2046            );
2047        }
2048
2049        assert_eq!(cache.runs.len(), HIGHLIGHT_CACHE_CAPACITY);
2050        assert!(!cache.runs.contains_key(&first_key));
2051        assert!(cache.runs.contains_key(&last_key));
2052    }
2053
2054    #[test]
2055    fn block_code_defers_expensive_rendering_for_first_frame() {
2056        let source = include_str!("code_block.rs");
2057
2058        assert!(source.contains("should_render_code_now"));
2059        assert!(source.contains("deferred-code-ready"));
2060        assert!(source.contains("take_deferred_highlight_slot"));
2061        assert!(source.contains("FRAME_BUDGET"));
2062        assert!(source.contains("render_code_placeholder"));
2063        assert!(source.contains("cx.notify()"));
2064    }
2065
2066    #[test]
2067    fn component_uses_syntect_and_supports_copyable_block_and_inline_format() {
2068        let source = include_str!("code_block.rs");
2069
2070        assert!(source.contains("HighlightLines"));
2071        assert!(source.contains("SyntaxSet::load_defaults_newlines"));
2072        assert!(source.contains("ThemeSet::load_defaults"));
2073        assert!(source.contains("ClipboardItem::new_string"));
2074        assert!(source.contains("on_copy"));
2075        assert!(source.contains("CodeCopyCallback"));
2076        assert!(source.contains("CodeFormat::Inline"));
2077        assert!(source.contains("selectable"));
2078        assert!(source.contains("SelectableCodeText"));
2079        assert!(source.contains("SelectableCodeState"));
2080        assert!(source.contains("selectable_state_map"));
2081        assert!(source.contains("lines: Vec<(ShapedLine"));
2082        assert!(source.contains("bounds: Option<Bounds"));
2083        assert!(source.contains("with_selectable_state(&self.id"));
2084        assert!(source.contains("prewarm_highlighter"));
2085        assert!(source.contains("SelectableCodeLayout"));
2086        assert!(source.contains("ReadOnlyCodeText"));
2087        assert!(source.contains("build_code_layout"));
2088        assert!(source.contains("set_selectable_layout_state"));
2089        assert!(source.contains("fn id(&self) -> Option<ElementId>"));
2090        assert!(source.contains("fn font_size(&self) -> Pixels"));
2091        assert!(source.contains("theme.font_size.md"));
2092        assert!(source.contains("cached_highlight_runs"));
2093        assert!(source.contains("HighlightCacheKey"));
2094        assert!(source.contains("CodeHighlighter::Syntect"));
2095        assert!(source.contains("CodeTheme::Auto"));
2096        assert!(source.contains("light_theme"));
2097        assert!(source.contains("dark_theme"));
2098        assert!(source.contains("github_dark_theme"));
2099        assert!(source.contains("two_face::syntax::extra_newlines"));
2100        assert!(source.contains("StyledText::new"));
2101        assert!(source.contains("with_runs"));
2102    }
2103}