Skip to main content

liora_components/
code_block.rs

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