diff --git a/Cargo.lock b/Cargo.lock index 2fca74cd7c..34cdcf1e69 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1994,6 +1994,7 @@ dependencies = [ "parking_lot 0.11.2", "postage", "project", + "pulldown-cmark", "rand 0.8.5", "rpc", "serde", @@ -4727,7 +4728,6 @@ dependencies = [ "parking_lot 0.11.2", "postage", "pretty_assertions", - "pulldown-cmark", "rand 0.8.5", "regex", "rpc", diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 5c19226960..7756f31efa 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -23,7 +23,7 @@ use language::{ }; use live_kit_client::MacOSDisplay; use lsp::LanguageServerId; -use project::{search::SearchQuery, DiagnosticSummary, Project, ProjectPath}; +use project::{search::SearchQuery, DiagnosticSummary, HoverBlockKind, Project, ProjectPath}; use rand::prelude::*; use serde_json::json; use settings::{Formatter, Settings}; @@ -4693,11 +4693,13 @@ async fn test_lsp_hover( vec![ project::HoverBlock { text: "Test hover content.".to_string(), - language: None, + kind: HoverBlockKind::Markdown, }, project::HoverBlock { text: "let foo = 42;".to_string(), - language: Some("Rust".to_string()), + kind: HoverBlockKind::Code { + language: "Rust".to_string() + }, } ] ); diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index 37b4dccb81..8835147ba6 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -55,6 +55,7 @@ log.workspace = true ordered-float.workspace = true parking_lot.workspace = true postage.workspace = true +pulldown-cmark = { version = "0.9.1", default-features = false } rand = { workspace = true, optional = true } serde.workspace = true serde_derive.workspace = true diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 563c0aa132..261c506967 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -463,6 +463,7 @@ pub struct EditorStyle { pub text: TextStyle, pub placeholder_text: Option, pub theme: theme::Editor, + pub theme_id: usize, } type CompletionId = usize; @@ -7319,6 +7320,7 @@ fn build_style( ) -> EditorStyle { let font_cache = cx.font_cache(); + let theme_id = settings.theme.meta.id; let mut theme = settings.theme.editor.clone(); let mut style = if let Some(get_field_editor_theme) = get_field_editor_theme { let field_editor_theme = get_field_editor_theme(&settings.theme); @@ -7332,6 +7334,7 @@ fn build_style( text: field_editor_theme.text, placeholder_text: field_editor_theme.placeholder_text, theme, + theme_id, } } else { let font_family_id = settings.buffer_font_family; @@ -7353,6 +7356,7 @@ fn build_style( }, placeholder_text: None, theme, + theme_id, } }; diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 7c62f71bda..70f9c8c88c 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -1,15 +1,17 @@ use futures::FutureExt; use gpui::{ actions, - elements::{Flex, MouseEventHandler, Padding, Text}, + color::Color, + elements::{Flex, MouseEventHandler, Padding, ParentElement, Text}, + fonts::{HighlightStyle, Underline, Weight}, impl_internal_actions, platform::{CursorStyle, MouseButton}, - AnyElement, AppContext, Axis, Element, ModelHandle, Task, ViewContext, + AnyElement, AppContext, Element, ModelHandle, MouseRegion, Task, ViewContext, }; -use language::{Bias, DiagnosticEntry, DiagnosticSeverity}; -use project::{HoverBlock, Project}; +use language::{Bias, DiagnosticEntry, DiagnosticSeverity, Language, LanguageRegistry}; +use project::{HoverBlock, HoverBlockKind, Project}; use settings::Settings; -use std::{ops::Range, time::Duration}; +use std::{ops::Range, sync::Arc, time::Duration}; use util::TryFutureExt; use crate::{ @@ -235,7 +237,8 @@ fn show_hover( Some(InfoPopover { project: project.clone(), symbol_range: range, - contents: hover_result.contents, + blocks: hover_result.contents, + rendered_content: None, }) }); @@ -264,6 +267,191 @@ fn show_hover( editor.hover_state.info_task = Some(task); } +fn render_blocks( + theme_id: usize, + blocks: &[HoverBlock], + language_registry: &Arc, + style: &EditorStyle, +) -> RenderedInfo { + let mut text = String::new(); + let mut highlights = Vec::new(); + let mut link_ranges = Vec::new(); + let mut link_urls = Vec::new(); + + for block in blocks { + match &block.kind { + HoverBlockKind::PlainText => { + new_paragraph(&mut text); + text.push_str(&block.text); + } + HoverBlockKind::Markdown => { + use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag}; + + let mut bold_depth = 0; + let mut italic_depth = 0; + let mut link_url = None; + let mut current_language = None; + let mut list_stack = Vec::new(); + + for event in Parser::new_ext(&block.text, Options::all()) { + let prev_len = text.len(); + match event { + Event::Text(t) => { + if let Some(language) = ¤t_language { + render_code( + &mut text, + &mut highlights, + t.as_ref(), + language, + style, + ); + } else { + text.push_str(t.as_ref()); + + let mut style = HighlightStyle::default(); + if bold_depth > 0 { + style.weight = Some(Weight::BOLD); + } + if italic_depth > 0 { + style.italic = Some(true); + } + if link_url.is_some() { + style.underline = Some(Underline { + thickness: 1.0.into(), + ..Default::default() + }); + } + + if style != HighlightStyle::default() { + let mut new_highlight = true; + if let Some((last_range, last_style)) = highlights.last_mut() { + if last_range.end == prev_len && last_style == &style { + last_range.end = text.len(); + new_highlight = false; + } + } + if new_highlight { + highlights.push((prev_len..text.len(), style)); + } + } + } + } + Event::Code(t) => { + text.push_str(t.as_ref()); + highlights.push(( + prev_len..text.len(), + HighlightStyle { + color: Some(Color::red()), + ..Default::default() + }, + )); + } + Event::Start(tag) => match tag { + Tag::Paragraph => new_paragraph(&mut text), + Tag::Heading(_, _, _) => { + new_paragraph(&mut text); + bold_depth += 1; + } + Tag::CodeBlock(kind) => { + new_paragraph(&mut text); + if let CodeBlockKind::Fenced(language) = kind { + current_language = language_registry + .language_for_name(language.as_ref()) + .now_or_never() + .and_then(Result::ok); + } + } + Tag::Emphasis => italic_depth += 1, + Tag::Strong => bold_depth += 1, + Tag::Link(_, url, _) => link_url = Some((prev_len, url)), + Tag::List(number) => list_stack.push(number), + Tag::Item => { + let len = list_stack.len(); + if let Some(list_state) = list_stack.last_mut() { + new_paragraph(&mut text); + for _ in 0..len - 1 { + text.push_str(" "); + } + if let Some(number) = list_state { + text.push_str(&format!("{}. ", number)); + *number += 1; + } else { + text.push_str("* "); + } + } + } + _ => {} + }, + Event::End(tag) => match tag { + Tag::Heading(_, _, _) => bold_depth -= 1, + Tag::CodeBlock(_) => current_language = None, + Tag::Emphasis => italic_depth -= 1, + Tag::Strong => bold_depth -= 1, + Tag::Link(_, _, _) => { + if let Some((start_offset, link_url)) = link_url.take() { + link_ranges.push(start_offset..text.len()); + link_urls.push(link_url.to_string()); + } + } + Tag::List(_) => { + list_stack.pop(); + } + _ => {} + }, + Event::HardBreak => text.push('\n'), + Event::SoftBreak => text.push(' '), + _ => {} + } + } + } + HoverBlockKind::Code { language } => { + if let Some(language) = language_registry + .language_for_name(language) + .now_or_never() + .and_then(Result::ok) + { + render_code(&mut text, &mut highlights, &block.text, &language, style); + } else { + text.push_str(&block.text); + } + } + } + } + + RenderedInfo { + theme_id, + text, + highlights, + link_ranges, + link_urls, + } +} + +fn render_code( + text: &mut String, + highlights: &mut Vec<(Range, HighlightStyle)>, + content: &str, + language: &Arc, + style: &EditorStyle, +) { + let prev_len = text.len(); + text.push_str(content); + for (range, highlight_id) in language.highlight_text(&content.into(), 0..content.len()) { + if let Some(style) = highlight_id.style(&style.syntax) { + highlights.push((prev_len + range.start..prev_len + range.end, style)); + } + } +} + +fn new_paragraph(text: &mut String) { + if !text.is_empty() { + if !text.ends_with('\n') { + text.push('\n'); + } + text.push('\n'); + } +} + #[derive(Default)] pub struct HoverState { pub info_popover: Option, @@ -278,7 +466,7 @@ impl HoverState { } pub fn render( - &self, + &mut self, snapshot: &EditorSnapshot, style: &EditorStyle, visible_rows: Range, @@ -307,7 +495,7 @@ impl HoverState { if let Some(diagnostic_popover) = self.diagnostic_popover.as_ref() { elements.push(diagnostic_popover.render(style, cx)); } - if let Some(info_popover) = self.info_popover.as_ref() { + if let Some(info_popover) = self.info_popover.as_mut() { elements.push(info_popover.render(style, cx)); } @@ -319,44 +507,66 @@ impl HoverState { pub struct InfoPopover { pub project: ModelHandle, pub symbol_range: Range, - pub contents: Vec, + pub blocks: Vec, + rendered_content: Option, +} + +#[derive(Debug, Clone)] +struct RenderedInfo { + theme_id: usize, + text: String, + highlights: Vec<(Range, HighlightStyle)>, + link_ranges: Vec>, + link_urls: Vec, } impl InfoPopover { - pub fn render(&self, style: &EditorStyle, cx: &mut ViewContext) -> AnyElement { + pub fn render( + &mut self, + style: &EditorStyle, + cx: &mut ViewContext, + ) -> AnyElement { + if let Some(rendered) = &self.rendered_content { + if rendered.theme_id != style.theme_id { + self.rendered_content = None; + } + } + + let rendered_content = self.rendered_content.get_or_insert_with(|| { + render_blocks( + style.theme_id, + &self.blocks, + self.project.read(cx).languages(), + style, + ) + }); + MouseEventHandler::::new(0, cx, |_, cx| { - let mut flex = Flex::new(Axis::Vertical).scrollable::(1, None, cx); - flex.extend(self.contents.iter().map(|content| { - let languages = self.project.read(cx).languages(); - if let Some(language) = content.language.clone().and_then(|language| { - languages.language_for_name(&language).now_or_never()?.ok() - }) { - let runs = language - .highlight_text(&content.text.as_str().into(), 0..content.text.len()); + let mut region_id = 0; + let view_id = cx.view_id(); - Text::new(content.text.clone(), style.text.clone()) - .with_soft_wrap(true) - .with_highlights( - runs.iter() - .filter_map(|(range, id)| { - id.style(style.theme.syntax.as_ref()) - .map(|style| (range.clone(), style)) - }) - .collect(), + let link_urls = rendered_content.link_urls.clone(); + Flex::column() + .scrollable::(1, None, cx) + .with_child( + Text::new(rendered_content.text.clone(), style.text.clone()) + .with_highlights(rendered_content.highlights.clone()) + .with_mouse_regions( + rendered_content.link_ranges.clone(), + move |ix, bounds| { + region_id += 1; + let url = link_urls[ix].clone(); + MouseRegion::new::(view_id, region_id, bounds) + .on_click::(MouseButton::Left, move |_, _, cx| { + println!("clicked link {url}"); + cx.platform().open_url(&url); + }) + }, ) - .into_any() - } else { - let mut text_style = style.hover_popover.prose.clone(); - text_style.font_size = style.text.font_size; - - Text::new(content.text.clone(), text_style) - .with_soft_wrap(true) - .contained() - .with_style(style.hover_popover.block_style) - .into_any() - } - })); - flex.contained().with_style(style.hover_popover.container) + .with_soft_wrap(true), + ) + .contained() + .with_style(style.hover_popover.container) }) .on_move(|_, _, _| {}) // Consume move events so they don't reach regions underneath. .with_cursor_style(CursorStyle::Arrow) @@ -430,16 +640,15 @@ impl DiagnosticPopover { #[cfg(test)] mod tests { + use super::*; + use crate::test::editor_lsp_test_context::EditorLspTestContext; + use gpui::fonts::Weight; use indoc::indoc; - use language::{Diagnostic, DiagnosticSet}; use lsp::LanguageServerId; - use project::HoverBlock; + use project::{HoverBlock, HoverBlockKind}; use smol::stream::StreamExt; - - use crate::test::editor_lsp_test_context::EditorLspTestContext; - - use super::*; + use util::test::marked_text_ranges; #[gpui::test] async fn test_mouse_hover_info_popover(cx: &mut gpui::TestAppContext) { @@ -480,10 +689,7 @@ mod tests { Ok(Some(lsp::Hover { contents: lsp::HoverContents::Markup(lsp::MarkupContent { kind: lsp::MarkupKind::Markdown, - value: indoc! {" - # Some basic docs - Some test documentation"} - .to_string(), + value: "some basic docs".to_string(), }), range: Some(symbol_range), })) @@ -495,17 +701,11 @@ mod tests { cx.editor(|editor, _| { assert!(editor.hover_state.visible()); assert_eq!( - editor.hover_state.info_popover.clone().unwrap().contents, - vec![ - HoverBlock { - text: "Some basic docs".to_string(), - language: None - }, - HoverBlock { - text: "Some test documentation".to_string(), - language: None - } - ] + editor.hover_state.info_popover.clone().unwrap().blocks, + vec![HoverBlock { + text: "some basic docs".to_string(), + kind: HoverBlockKind::Markdown, + },] ) }); @@ -556,10 +756,7 @@ mod tests { Ok(Some(lsp::Hover { contents: lsp::HoverContents::Markup(lsp::MarkupContent { kind: lsp::MarkupKind::Markdown, - value: indoc! {" - # Some other basic docs - Some other test documentation"} - .to_string(), + value: "some other basic docs".to_string(), }), range: Some(symbol_range), })) @@ -570,17 +767,11 @@ mod tests { cx.condition(|editor, _| editor.hover_state.visible()).await; cx.editor(|editor, _| { assert_eq!( - editor.hover_state.info_popover.clone().unwrap().contents, - vec![ - HoverBlock { - text: "Some other basic docs".to_string(), - language: None - }, - HoverBlock { - text: "Some other test documentation".to_string(), - language: None - } - ] + editor.hover_state.info_popover.clone().unwrap().blocks, + vec![HoverBlock { + text: "some other basic docs".to_string(), + kind: HoverBlockKind::Markdown, + }] ) }); } @@ -637,10 +828,7 @@ mod tests { Ok(Some(lsp::Hover { contents: lsp::HoverContents::Markup(lsp::MarkupContent { kind: lsp::MarkupKind::Markdown, - value: indoc! {" - # Some other basic docs - Some other test documentation"} - .to_string(), + value: "some new docs".to_string(), }), range: Some(range), })) @@ -653,4 +841,72 @@ mod tests { hover_state.diagnostic_popover.is_some() && hover_state.info_task.is_some() }); } + + #[gpui::test] + fn test_render_blocks(cx: &mut gpui::TestAppContext) { + Settings::test_async(cx); + cx.add_window(|cx| { + let editor = Editor::single_line(None, cx); + let style = editor.style(cx); + + struct Row { + blocks: Vec, + expected_marked_text: &'static str, + expected_styles: Vec, + } + + let rows = &[ + Row { + blocks: vec![HoverBlock { + text: "one **two** three".to_string(), + kind: HoverBlockKind::Markdown, + }], + expected_marked_text: "one «two» three", + expected_styles: vec![HighlightStyle { + weight: Some(Weight::BOLD), + ..Default::default() + }], + }, + Row { + blocks: vec![HoverBlock { + text: "one [two](the-url) three".to_string(), + kind: HoverBlockKind::Markdown, + }], + expected_marked_text: "one «two» three", + expected_styles: vec![HighlightStyle { + underline: Some(Underline { + thickness: 1.0.into(), + ..Default::default() + }), + ..Default::default() + }], + }, + ]; + + for Row { + blocks, + expected_marked_text, + expected_styles, + } in rows + { + let rendered = render_blocks(0, &blocks, &Default::default(), &style); + + let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false); + let expected_highlights = ranges + .into_iter() + .zip(expected_styles.iter().cloned()) + .collect::>(); + assert_eq!( + rendered.text, expected_text, + "wrong text for input {blocks:?}" + ); + assert_eq!( + rendered.highlights, expected_highlights, + "wrong highlights for input {blocks:?}" + ); + } + + editor + }); + } } diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index a0e6b31ec8..56803bb062 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -37,6 +37,7 @@ settings = { path = "../settings" } sum_tree = { path = "../sum_tree" } terminal = { path = "../terminal" } util = { path = "../util" } + aho-corasick = "0.7" anyhow.workspace = true async-trait.workspace = true @@ -47,7 +48,6 @@ lazy_static.workspace = true log.workspace = true parking_lot.workspace = true postage.workspace = true -pulldown-cmark = { version = "0.9.1", default-features = false } rand.workspace = true regex.workspace = true serde.workspace = true diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index b26987694e..ddae9b59ae 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -1,5 +1,6 @@ use crate::{ - DocumentHighlight, Hover, HoverBlock, Location, LocationLink, Project, ProjectTransaction, + DocumentHighlight, Hover, HoverBlock, HoverBlockKind, Location, LocationLink, Project, + ProjectTransaction, }; use anyhow::{anyhow, Result}; use async_trait::async_trait; @@ -13,7 +14,6 @@ use language::{ Completion, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Unclipped, }; use lsp::{DocumentHighlightKind, LanguageServer, LanguageServerId, ServerCapabilities}; -use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag}; use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc}; #[async_trait(?Send)] @@ -1092,76 +1092,49 @@ impl LspCommand for GetHover { }) }); - let contents = cx.read(|_| match hover.contents { - lsp::HoverContents::Scalar(marked_string) => { - HoverBlock::try_new(marked_string).map(|contents| vec![contents]) - } - lsp::HoverContents::Array(marked_strings) => { - let content: Vec = marked_strings - .into_iter() - .filter_map(HoverBlock::try_new) - .collect(); - if content.is_empty() { - None - } else { - Some(content) - } - } - lsp::HoverContents::Markup(markup_content) => { - let mut contents = Vec::new(); - let mut language = None; - let mut current_text = String::new(); - for event in Parser::new_ext(&markup_content.value, Options::all()) { - match event { - Event::SoftBreak => { - current_text.push(' '); - } - Event::Text(text) | Event::Code(text) => { - current_text.push_str(&text.to_string()); - } - Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(new_language))) => { - if !current_text.is_empty() { - let text = std::mem::take(&mut current_text).trim().to_string(); - contents.push(HoverBlock { text, language }); - } - - language = if new_language.is_empty() { - None - } else { - Some(new_language.to_string()) - }; - } - Event::End(Tag::CodeBlock(_)) - | Event::End(Tag::Paragraph) - | Event::End(Tag::Heading(_, _, _)) - | Event::End(Tag::BlockQuote) - | Event::HardBreak => { - if !current_text.is_empty() { - let text = std::mem::take(&mut current_text).trim().to_string(); - contents.push(HoverBlock { text, language }); - } - language = None; - } - _ => {} + fn hover_blocks_from_marked_string( + marked_string: lsp::MarkedString, + ) -> Option { + let block = match marked_string { + lsp::MarkedString::String(content) => HoverBlock { + text: content, + kind: HoverBlockKind::Markdown, + }, + lsp::MarkedString::LanguageString(lsp::LanguageString { language, value }) => { + HoverBlock { + text: value, + kind: HoverBlockKind::Code { language }, } } - - if !current_text.trim().is_empty() { - contents.push(HoverBlock { - text: current_text, - language, - }); - } - - if contents.is_empty() { - None - } else { - Some(contents) - } + }; + if block.text.is_empty() { + None + } else { + Some(block) } + } + + let contents = cx.read(|_| match hover.contents { + lsp::HoverContents::Scalar(marked_string) => { + hover_blocks_from_marked_string(marked_string) + .into_iter() + .collect() + } + lsp::HoverContents::Array(marked_strings) => marked_strings + .into_iter() + .filter_map(hover_blocks_from_marked_string) + .collect(), + lsp::HoverContents::Markup(markup_content) => vec![HoverBlock { + text: markup_content.value, + kind: if markup_content.kind == lsp::MarkupKind::Markdown { + HoverBlockKind::Markdown + } else { + HoverBlockKind::PlainText + }, + }], }); - contents.map(|contents| Hover { contents, range }) + Some(Hover { contents, range }) })) } @@ -1218,7 +1191,12 @@ impl LspCommand for GetHover { .into_iter() .map(|block| proto::HoverBlock { text: block.text, - language: block.language, + is_markdown: block.kind == HoverBlockKind::Markdown, + language: if let HoverBlockKind::Code { language } = block.kind { + Some(language) + } else { + None + }, }) .collect(); @@ -1255,7 +1233,13 @@ impl LspCommand for GetHover { .into_iter() .map(|block| HoverBlock { text: block.text, - language: block.language, + kind: if let Some(language) = block.language { + HoverBlockKind::Code { language } + } else if block.is_markdown { + HoverBlockKind::Markdown + } else { + HoverBlockKind::PlainText + }, }) .collect(); diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index c82855b03c..51d29d7296 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -36,7 +36,7 @@ use language::{ }; use lsp::{ DiagnosticSeverity, DiagnosticTag, DidChangeWatchedFilesRegistrationOptions, - DocumentHighlightKind, LanguageServer, LanguageServerId, LanguageString, MarkedString, + DocumentHighlightKind, LanguageServer, LanguageServerId, }; use lsp_command::*; use lsp_glob_set::LspGlobSet; @@ -287,27 +287,14 @@ pub struct Symbol { #[derive(Clone, Debug, PartialEq)] pub struct HoverBlock { pub text: String, - pub language: Option, + pub kind: HoverBlockKind, } -impl HoverBlock { - fn try_new(marked_string: MarkedString) -> Option { - let result = match marked_string { - MarkedString::LanguageString(LanguageString { language, value }) => HoverBlock { - text: value, - language: Some(language), - }, - MarkedString::String(text) => HoverBlock { - text, - language: None, - }, - }; - if result.text.is_empty() { - None - } else { - Some(result) - } - } +#[derive(Clone, Debug, PartialEq)] +pub enum HoverBlockKind { + PlainText, + Markdown, + Code { language: String }, } #[derive(Debug)] diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 599d80e2ba..86f8f38ff3 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -633,6 +633,7 @@ message GetHoverResponse { message HoverBlock { string text = 1; optional string language = 2; + bool is_markdown = 3; } message ApplyCodeAction { diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index b98dd5483e..fbf4ea6b70 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -46,6 +46,8 @@ pub struct Theme { #[derive(Deserialize, Default, Clone)] pub struct ThemeMeta { + #[serde(skip_deserializing)] + pub id: usize, pub name: String, pub is_light: bool, } diff --git a/crates/theme/src/theme_registry.rs b/crates/theme/src/theme_registry.rs index a82ede59f7..f9f89b7adc 100644 --- a/crates/theme/src/theme_registry.rs +++ b/crates/theme/src/theme_registry.rs @@ -4,13 +4,20 @@ use gpui::{fonts, AssetSource, FontCache}; use parking_lot::Mutex; use serde::Deserialize; use serde_json::Value; -use std::{collections::HashMap, sync::Arc}; +use std::{ + collections::HashMap, + sync::{ + atomic::{AtomicUsize, Ordering::SeqCst}, + Arc, + }, +}; pub struct ThemeRegistry { assets: Box, themes: Mutex>>, theme_data: Mutex>>, font_cache: Arc, + next_theme_id: AtomicUsize, } impl ThemeRegistry { @@ -19,6 +26,7 @@ impl ThemeRegistry { assets: Box::new(source), themes: Default::default(), theme_data: Default::default(), + next_theme_id: Default::default(), font_cache, }) } @@ -66,6 +74,7 @@ impl ThemeRegistry { // Reset name to be the file path, so that we can use it to access the stored themes theme.meta.name = name.into(); + theme.meta.id = self.next_theme_id.fetch_add(1, SeqCst); let theme: Arc = theme.into(); self.themes.lock().insert(name.to_string(), theme.clone()); Ok(theme)