Skip to main content

liora_components/
code_block.rs

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