Cargo.lock π
@@ -12258,6 +12258,7 @@ dependencies = [
"language",
"log",
"lsp",
+ "markdown",
"node_runtime",
"parking_lot",
"pathdiff",
Shuhei Kadowaki , Fernando Tagawa , Claude , Kirill Bulatov , and Kirill Bulatov created
This PR revives zed-industries/zed#27818 and aims to complete the
partially implemented overloaded signature help feature.
The first commit is a rebase of zed-industries/zed#27818, and the
subsequent commit addresses all review feedback from the original PR.
Now the overloaded signature help works like
https://github.com/user-attachments/assets/e253c9a0-e3a5-4bfe-8003-eb75de41f672
Closes #21493
Release Notes:
- Implemented signature help for overloaded items. Additionally, added a
support for rendering signature help documentation.
---------
Co-authored-by: Fernando Tagawa <tagawafernando@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Kirill Bulatov <mail4score@gmail.com>
Co-authored-by: Kirill Bulatov <kirill@zed.dev>
Cargo.lock | 1
assets/keymaps/default-linux.json | 7
assets/keymaps/default-macos.json | 7
assets/keymaps/linux/emacs.json | 7
assets/keymaps/macos/emacs.json | 7
assets/keymaps/vim.json | 7
crates/editor/src/actions.rs | 2
crates/editor/src/editor.rs | 36 +
crates/editor/src/editor_tests.rs | 146 ++++++
crates/editor/src/element.rs | 4
crates/editor/src/signature_help.rs | 268 ++++++++++--
crates/markdown/src/markdown.rs | 4
crates/project/Cargo.toml | 1
crates/project/src/lsp_command.rs | 25
crates/project/src/lsp_command/signature_help.rs | 398 +++++++++++------
crates/project/src/lsp_store.rs | 1
crates/theme/src/styles/accents.rs | 2
crates/theme/src/styles/colors.rs | 2
crates/theme/src/styles/players.rs | 2
crates/theme/src/styles/system.rs | 2
crates/theme/src/theme.rs | 2
21 files changed, 722 insertions(+), 209 deletions(-)
@@ -12258,6 +12258,7 @@ dependencies = [
"language",
"log",
"lsp",
+ "markdown",
"node_runtime",
"parking_lot",
"pathdiff",
@@ -708,6 +708,13 @@
"pagedown": "editor::ContextMenuLast"
}
},
+ {
+ "context": "Editor && showing_signature_help && !showing_completions",
+ "bindings": {
+ "up": "editor::SignatureHelpPrevious",
+ "down": "editor::SignatureHelpNext"
+ }
+ },
// Custom bindings
{
"bindings": {
@@ -773,6 +773,13 @@
"pagedown": "editor::ContextMenuLast"
}
},
+ {
+ "context": "Editor && showing_signature_help && !showing_completions",
+ "bindings": {
+ "up": "editor::SignatureHelpPrevious",
+ "down": "editor::SignatureHelpNext"
+ }
+ },
// Custom bindings
{
"use_key_equivalents": true,
@@ -98,6 +98,13 @@
"ctrl-n": "editor::ContextMenuNext"
}
},
+ {
+ "context": "Editor && showing_signature_help && !showing_completions",
+ "bindings": {
+ "ctrl-p": "editor::SignatureHelpPrevious",
+ "ctrl-n": "editor::SignatureHelpNext"
+ }
+ },
{
"context": "Workspace",
"bindings": {
@@ -98,6 +98,13 @@
"ctrl-n": "editor::ContextMenuNext"
}
},
+ {
+ "context": "Editor && showing_signature_help && !showing_completions",
+ "bindings": {
+ "ctrl-p": "editor::SignatureHelpPrevious",
+ "ctrl-n": "editor::SignatureHelpNext"
+ }
+ },
{
"context": "Workspace",
"bindings": {
@@ -477,6 +477,13 @@
"ctrl-n": "editor::ShowWordCompletions"
}
},
+ {
+ "context": "vim_mode == insert && showing_signature_help && !showing_completions",
+ "bindings": {
+ "ctrl-p": "editor::SignatureHelpPrevious",
+ "ctrl-n": "editor::SignatureHelpNext"
+ }
+ },
{
"context": "vim_mode == replace",
"bindings": {
@@ -424,6 +424,8 @@ actions!(
ShowSignatureHelp,
ShowWordCompletions,
ShuffleLines,
+ SignatureHelpNext,
+ SignatureHelpPrevious,
SortLinesCaseInsensitive,
SortLinesCaseSensitive,
SplitSelectionIntoLines,
@@ -2362,6 +2362,10 @@ impl Editor {
None => {}
}
+ if self.signature_help_state.has_multiple_signatures() {
+ key_context.add("showing_signature_help");
+ }
+
// Disable vim contexts when a sub-editor (e.g. rename/inline assistant) is focused.
if !self.focus_handle(cx).contains_focused(window, cx)
|| (self.is_focused(window) || self.mouse_menu_is_focused(window, cx))
@@ -12582,6 +12586,38 @@ impl Editor {
}
}
+ pub fn signature_help_prev(
+ &mut self,
+ _: &SignatureHelpPrevious,
+ _: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ if let Some(popover) = self.signature_help_state.popover_mut() {
+ if popover.current_signature == 0 {
+ popover.current_signature = popover.signatures.len() - 1;
+ } else {
+ popover.current_signature -= 1;
+ }
+ cx.notify();
+ }
+ }
+
+ pub fn signature_help_next(
+ &mut self,
+ _: &SignatureHelpNext,
+ _: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ if let Some(popover) = self.signature_help_state.popover_mut() {
+ if popover.current_signature + 1 == popover.signatures.len() {
+ popover.current_signature = 0;
+ } else {
+ popover.current_signature += 1;
+ }
+ cx.notify();
+ }
+ }
+
pub fn move_to_previous_word_start(
&mut self,
_: &MoveToPreviousWordStart,
@@ -10866,9 +10866,10 @@ async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
cx.editor(|editor, _, _| {
let signature_help_state = editor.signature_help_state.popover().cloned();
+ let signature = signature_help_state.unwrap();
assert_eq!(
- signature_help_state.unwrap().label,
- "param1: u8, param2: u8"
+ signature.signatures[signature.current_signature].label,
+ "fn sample(param1: u8, param2: u8)"
);
});
}
@@ -11037,9 +11038,10 @@ async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestA
cx.update_editor(|editor, _, _| {
let signature_help_state = editor.signature_help_state.popover().cloned();
assert!(signature_help_state.is_some());
+ let signature = signature_help_state.unwrap();
assert_eq!(
- signature_help_state.unwrap().label,
- "param1: u8, param2: u8"
+ signature.signatures[signature.current_signature].label,
+ "fn sample(param1: u8, param2: u8)"
);
editor.signature_help_state = SignatureHelpState::default();
});
@@ -11078,9 +11080,10 @@ async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestA
cx.editor(|editor, _, _| {
let signature_help_state = editor.signature_help_state.popover().cloned();
assert!(signature_help_state.is_some());
+ let signature = signature_help_state.unwrap();
assert_eq!(
- signature_help_state.unwrap().label,
- "param1: u8, param2: u8"
+ signature.signatures[signature.current_signature].label,
+ "fn sample(param1: u8, param2: u8)"
);
});
}
@@ -11139,9 +11142,10 @@ async fn test_signature_help(cx: &mut TestAppContext) {
cx.editor(|editor, _, _| {
let signature_help_state = editor.signature_help_state.popover().cloned();
assert!(signature_help_state.is_some());
+ let signature = signature_help_state.unwrap();
assert_eq!(
- signature_help_state.unwrap().label,
- "param1: u8, param2: u8"
+ signature.signatures[signature.current_signature].label,
+ "fn sample(param1: u8, param2: u8)"
);
});
@@ -11349,6 +11353,132 @@ async fn test_signature_help(cx: &mut TestAppContext) {
.await;
}
+#[gpui::test]
+async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+
+ let mut cx = EditorLspTestContext::new_rust(
+ lsp::ServerCapabilities {
+ signature_help_provider: Some(lsp::SignatureHelpOptions {
+ ..Default::default()
+ }),
+ ..Default::default()
+ },
+ cx,
+ )
+ .await;
+
+ cx.set_state(indoc! {"
+ fn main() {
+ overloadedΛ
+ }
+ "});
+
+ cx.update_editor(|editor, window, cx| {
+ editor.handle_input("(", window, cx);
+ editor.show_signature_help(&ShowSignatureHelp, window, cx);
+ });
+
+ // Mock response with 3 signatures
+ let mocked_response = lsp::SignatureHelp {
+ signatures: vec![
+ lsp::SignatureInformation {
+ label: "fn overloaded(x: i32)".to_string(),
+ documentation: None,
+ parameters: Some(vec![lsp::ParameterInformation {
+ label: lsp::ParameterLabel::Simple("x: i32".to_string()),
+ documentation: None,
+ }]),
+ active_parameter: None,
+ },
+ lsp::SignatureInformation {
+ label: "fn overloaded(x: i32, y: i32)".to_string(),
+ documentation: None,
+ parameters: Some(vec![
+ lsp::ParameterInformation {
+ label: lsp::ParameterLabel::Simple("x: i32".to_string()),
+ documentation: None,
+ },
+ lsp::ParameterInformation {
+ label: lsp::ParameterLabel::Simple("y: i32".to_string()),
+ documentation: None,
+ },
+ ]),
+ active_parameter: None,
+ },
+ lsp::SignatureInformation {
+ label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
+ documentation: None,
+ parameters: Some(vec![
+ lsp::ParameterInformation {
+ label: lsp::ParameterLabel::Simple("x: i32".to_string()),
+ documentation: None,
+ },
+ lsp::ParameterInformation {
+ label: lsp::ParameterLabel::Simple("y: i32".to_string()),
+ documentation: None,
+ },
+ lsp::ParameterInformation {
+ label: lsp::ParameterLabel::Simple("z: i32".to_string()),
+ documentation: None,
+ },
+ ]),
+ active_parameter: None,
+ },
+ ],
+ active_signature: Some(1),
+ active_parameter: Some(0),
+ };
+ handle_signature_help_request(&mut cx, mocked_response).await;
+
+ cx.condition(|editor, _| editor.signature_help_state.is_shown())
+ .await;
+
+ // Verify we have multiple signatures and the right one is selected
+ cx.editor(|editor, _, _| {
+ let popover = editor.signature_help_state.popover().cloned().unwrap();
+ assert_eq!(popover.signatures.len(), 3);
+ // active_signature was 1, so that should be the current
+ assert_eq!(popover.current_signature, 1);
+ assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
+ assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
+ assert_eq!(
+ popover.signatures[2].label,
+ "fn overloaded(x: i32, y: i32, z: i32)"
+ );
+ });
+
+ // Test navigation functionality
+ cx.update_editor(|editor, window, cx| {
+ editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
+ });
+
+ cx.editor(|editor, _, _| {
+ let popover = editor.signature_help_state.popover().cloned().unwrap();
+ assert_eq!(popover.current_signature, 2);
+ });
+
+ // Test wrap around
+ cx.update_editor(|editor, window, cx| {
+ editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
+ });
+
+ cx.editor(|editor, _, _| {
+ let popover = editor.signature_help_state.popover().cloned().unwrap();
+ assert_eq!(popover.current_signature, 0);
+ });
+
+ // Test previous navigation
+ cx.update_editor(|editor, window, cx| {
+ editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
+ });
+
+ cx.editor(|editor, _, _| {
+ let popover = editor.signature_help_state.popover().cloned().unwrap();
+ assert_eq!(popover.current_signature, 2);
+ });
+}
+
#[gpui::test]
async fn test_completion_mode(cx: &mut TestAppContext) {
init_test(cx, |_| {});
@@ -546,6 +546,8 @@ impl EditorElement {
}
});
register_action(editor, window, Editor::show_signature_help);
+ register_action(editor, window, Editor::signature_help_prev);
+ register_action(editor, window, Editor::signature_help_next);
register_action(editor, window, Editor::next_edit_prediction);
register_action(editor, window, Editor::previous_edit_prediction);
register_action(editor, window, Editor::show_inline_completion);
@@ -4985,7 +4987,7 @@ impl EditorElement {
let maybe_element = self.editor.update(cx, |editor, cx| {
if let Some(popover) = editor.signature_help_state.popover_mut() {
- let element = popover.render(max_size, cx);
+ let element = popover.render(max_size, window, cx);
Some(element)
} else {
None
@@ -1,18 +1,22 @@
use crate::actions::ShowSignatureHelp;
-use crate::{Editor, EditorSettings, ToggleAutoSignatureHelp};
+use crate::hover_popover::open_markdown_url;
+use crate::{Editor, EditorSettings, ToggleAutoSignatureHelp, hover_markdown_style};
use gpui::{
- App, Context, HighlightStyle, MouseButton, Size, StyledText, Task, TextStyle, Window,
- combine_highlights,
+ App, Context, Div, Entity, HighlightStyle, MouseButton, ScrollHandle, Size, Stateful,
+ StyledText, Task, TextStyle, Window, combine_highlights,
};
use language::BufferSnapshot;
+use markdown::{Markdown, MarkdownElement};
use multi_buffer::{Anchor, ToOffset};
use settings::Settings;
use std::ops::Range;
use text::Rope;
use theme::ThemeSettings;
use ui::{
- ActiveTheme, AnyElement, InteractiveElement, IntoElement, ParentElement, Pixels, SharedString,
- StatefulInteractiveElement, Styled, StyledExt, div, relative,
+ ActiveTheme, AnyElement, ButtonCommon, ButtonStyle, Clickable, FluentBuilder, IconButton,
+ IconButtonShape, IconName, IconSize, InteractiveElement, IntoElement, Label, LabelCommon,
+ LabelSize, ParentElement, Pixels, Scrollbar, ScrollbarState, SharedString,
+ StatefulInteractiveElement, Styled, StyledExt, div, px, relative,
};
// Language-specific settings may define quotes as "brackets", so filter them out separately.
@@ -37,15 +41,14 @@ impl Editor {
.map(|auto_signature_help| !auto_signature_help)
.or_else(|| Some(!EditorSettings::get_global(cx).auto_signature_help));
match self.auto_signature_help {
- Some(auto_signature_help) if auto_signature_help => {
+ Some(true) => {
self.show_signature_help(&ShowSignatureHelp, window, cx);
}
- Some(_) => {
+ Some(false) => {
self.hide_signature_help(cx, SignatureHelpHiddenBy::AutoClose);
}
None => {}
}
- cx.notify();
}
pub(super) fn hide_signature_help(
@@ -54,7 +57,7 @@ impl Editor {
signature_help_hidden_by: SignatureHelpHiddenBy,
) -> bool {
if self.signature_help_state.is_shown() {
- self.signature_help_state.kill_task();
+ self.signature_help_state.task = None;
self.signature_help_state.hide(signature_help_hidden_by);
cx.notify();
true
@@ -187,31 +190,62 @@ impl Editor {
};
if let Some(language) = language {
- let text = Rope::from(signature_help.label.clone());
- let highlights = language
- .highlight_text(&text, 0..signature_help.label.len())
- .into_iter()
- .flat_map(|(range, highlight_id)| {
- Some((range, highlight_id.style(&cx.theme().syntax())?))
- });
- signature_help.highlights =
- combine_highlights(signature_help.highlights, highlights).collect()
+ for signature in &mut signature_help.signatures {
+ let text = Rope::from(signature.label.to_string());
+ let highlights = language
+ .highlight_text(&text, 0..signature.label.len())
+ .into_iter()
+ .flat_map(|(range, highlight_id)| {
+ Some((range, highlight_id.style(&cx.theme().syntax())?))
+ });
+ signature.highlights =
+ combine_highlights(signature.highlights.clone(), highlights)
+ .collect();
+ }
}
let settings = ThemeSettings::get_global(cx);
- let text_style = TextStyle {
+ let style = TextStyle {
color: cx.theme().colors().text,
font_family: settings.buffer_font.family.clone(),
font_fallbacks: settings.buffer_font.fallbacks.clone(),
font_size: settings.buffer_font_size(cx).into(),
font_weight: settings.buffer_font.weight,
line_height: relative(settings.buffer_line_height.value()),
- ..Default::default()
+ ..TextStyle::default()
};
+ let scroll_handle = ScrollHandle::new();
+ let signatures = signature_help
+ .signatures
+ .into_iter()
+ .map(|s| SignatureHelp {
+ label: s.label,
+ documentation: s.documentation,
+ highlights: s.highlights,
+ active_parameter: s.active_parameter,
+ parameter_documentation: s
+ .active_parameter
+ .and_then(|idx| s.parameters.get(idx))
+ .and_then(|param| param.documentation.clone()),
+ })
+ .collect::<Vec<_>>();
+
+ if signatures.is_empty() {
+ editor
+ .signature_help_state
+ .hide(SignatureHelpHiddenBy::AutoClose);
+ return;
+ }
+
+ let current_signature = signature_help
+ .active_signature
+ .min(signatures.len().saturating_sub(1));
let signature_help_popover = SignatureHelpPopover {
- label: signature_help.label.into(),
- highlights: signature_help.highlights,
- style: text_style,
+ scrollbar_state: ScrollbarState::new(scroll_handle.clone()),
+ style,
+ signatures,
+ current_signature,
+ scroll_handle,
};
editor
.signature_help_state
@@ -231,15 +265,11 @@ pub struct SignatureHelpState {
}
impl SignatureHelpState {
- pub fn set_task(&mut self, task: Task<()>) {
+ fn set_task(&mut self, task: Task<()>) {
self.task = Some(task);
self.hidden_by = None;
}
- pub fn kill_task(&mut self) {
- self.task = None;
- }
-
#[cfg(test)]
pub fn popover(&self) -> Option<&SignatureHelpPopover> {
self.popover.as_ref()
@@ -249,25 +279,31 @@ impl SignatureHelpState {
self.popover.as_mut()
}
- pub fn set_popover(&mut self, popover: SignatureHelpPopover) {
+ fn set_popover(&mut self, popover: SignatureHelpPopover) {
self.popover = Some(popover);
self.hidden_by = None;
}
- pub fn hide(&mut self, hidden_by: SignatureHelpHiddenBy) {
+ fn hide(&mut self, hidden_by: SignatureHelpHiddenBy) {
if self.hidden_by.is_none() {
self.popover = None;
self.hidden_by = Some(hidden_by);
}
}
- pub fn hidden_by_selection(&self) -> bool {
+ fn hidden_by_selection(&self) -> bool {
self.hidden_by == Some(SignatureHelpHiddenBy::Selection)
}
pub fn is_shown(&self) -> bool {
self.popover.is_some()
}
+
+ pub fn has_multiple_signatures(&self) -> bool {
+ self.popover
+ .as_ref()
+ .is_some_and(|popover| popover.signatures.len() > 1)
+ }
}
#[cfg(test)]
@@ -278,28 +314,170 @@ impl SignatureHelpState {
}
#[derive(Clone, Debug, PartialEq)]
+pub struct SignatureHelp {
+ pub(crate) label: SharedString,
+ documentation: Option<Entity<Markdown>>,
+ highlights: Vec<(Range<usize>, HighlightStyle)>,
+ active_parameter: Option<usize>,
+ parameter_documentation: Option<Entity<Markdown>>,
+}
+
+#[derive(Clone, Debug)]
pub struct SignatureHelpPopover {
- pub label: SharedString,
pub style: TextStyle,
- pub highlights: Vec<(Range<usize>, HighlightStyle)>,
+ pub signatures: Vec<SignatureHelp>,
+ pub current_signature: usize,
+ scroll_handle: ScrollHandle,
+ scrollbar_state: ScrollbarState,
}
impl SignatureHelpPopover {
- pub fn render(&mut self, max_size: Size<Pixels>, cx: &mut Context<Editor>) -> AnyElement {
+ pub fn render(
+ &mut self,
+ max_size: Size<Pixels>,
+ window: &mut Window,
+ cx: &mut Context<Editor>,
+ ) -> AnyElement {
+ let Some(signature) = self.signatures.get(self.current_signature) else {
+ return div().into_any_element();
+ };
+
+ let main_content = div()
+ .occlude()
+ .p_2()
+ .child(
+ div()
+ .id("signature_help_container")
+ .overflow_y_scroll()
+ .max_w(max_size.width)
+ .max_h(max_size.height)
+ .track_scroll(&self.scroll_handle)
+ .child(
+ StyledText::new(signature.label.clone()).with_default_highlights(
+ &self.style,
+ signature.highlights.iter().cloned(),
+ ),
+ )
+ .when_some(
+ signature.parameter_documentation.clone(),
+ |this, param_doc| {
+ this.child(div().h_px().bg(cx.theme().colors().border_variant).my_1())
+ .child(
+ MarkdownElement::new(
+ param_doc,
+ hover_markdown_style(window, cx),
+ )
+ .code_block_renderer(markdown::CodeBlockRenderer::Default {
+ copy_button: false,
+ border: false,
+ copy_button_on_hover: false,
+ })
+ .on_url_click(open_markdown_url),
+ )
+ },
+ )
+ .when_some(signature.documentation.clone(), |this, description| {
+ this.child(div().h_px().bg(cx.theme().colors().border_variant).my_1())
+ .child(
+ MarkdownElement::new(description, hover_markdown_style(window, cx))
+ .code_block_renderer(markdown::CodeBlockRenderer::Default {
+ copy_button: false,
+ border: false,
+ copy_button_on_hover: false,
+ })
+ .on_url_click(open_markdown_url),
+ )
+ }),
+ )
+ .child(self.render_vertical_scrollbar(cx));
+ let controls = if self.signatures.len() > 1 {
+ let prev_button = IconButton::new("signature_help_prev", IconName::ChevronUp)
+ .shape(IconButtonShape::Square)
+ .style(ButtonStyle::Subtle)
+ .icon_size(IconSize::Small)
+ .tooltip(move |window, cx| {
+ ui::Tooltip::for_action(
+ "Previous Signature",
+ &crate::SignatureHelpPrevious,
+ window,
+ cx,
+ )
+ })
+ .on_click(cx.listener(|editor, _, window, cx| {
+ editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
+ }));
+
+ let next_button = IconButton::new("signature_help_next", IconName::ChevronDown)
+ .shape(IconButtonShape::Square)
+ .style(ButtonStyle::Subtle)
+ .icon_size(IconSize::Small)
+ .tooltip(move |window, cx| {
+ ui::Tooltip::for_action("Next Signature", &crate::SignatureHelpNext, window, cx)
+ })
+ .on_click(cx.listener(|editor, _, window, cx| {
+ editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
+ }));
+
+ let page = Label::new(format!(
+ "{}/{}",
+ self.current_signature + 1,
+ self.signatures.len()
+ ))
+ .size(LabelSize::Small);
+
+ Some(
+ div()
+ .flex()
+ .flex_col()
+ .items_center()
+ .gap_0p5()
+ .px_0p5()
+ .py_0p5()
+ .children([
+ prev_button.into_any_element(),
+ div().child(page).into_any_element(),
+ next_button.into_any_element(),
+ ])
+ .into_any_element(),
+ )
+ } else {
+ None
+ };
div()
- .id("signature_help_popover")
.elevation_2(cx)
- .overflow_y_scroll()
- .max_w(max_size.width)
- .max_h(max_size.height)
- .on_mouse_move(|_, _, cx| cx.stop_propagation())
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
- .child(
- div().px_2().py_0p5().child(
- StyledText::new(self.label.clone())
- .with_default_highlights(&self.style, self.highlights.iter().cloned()),
- ),
- )
+ .on_mouse_move(|_, _, cx| cx.stop_propagation())
+ .flex()
+ .flex_row()
+ .when_some(controls, |this, controls| {
+ this.children(vec![
+ div().flex().items_end().child(controls),
+ div().w_px().bg(cx.theme().colors().border_variant),
+ ])
+ })
+ .child(main_content)
.into_any_element()
}
+
+ fn render_vertical_scrollbar(&self, cx: &mut Context<Editor>) -> Stateful<Div> {
+ div()
+ .occlude()
+ .id("signature_help_scrollbar")
+ .on_mouse_move(cx.listener(|_, _, _, cx| {
+ cx.notify();
+ cx.stop_propagation()
+ }))
+ .on_hover(|_, _, cx| cx.stop_propagation())
+ .on_any_mouse_down(|_, _, cx| cx.stop_propagation())
+ .on_mouse_up(MouseButton::Left, |_, _, cx| cx.stop_propagation())
+ .on_scroll_wheel(cx.listener(|_, _, _, cx| cx.notify()))
+ .h_full()
+ .absolute()
+ .right_1()
+ .top_1()
+ .bottom_1()
+ .w(px(12.))
+ .cursor_default()
+ .children(Scrollbar::vertical(self.scrollbar_state.clone()))
+ }
}
@@ -421,7 +421,7 @@ impl Selection {
}
}
-#[derive(Clone, Default)]
+#[derive(Debug, Clone, Default)]
pub struct ParsedMarkdown {
pub source: SharedString,
pub events: Arc<[(Range<usize>, MarkdownEvent)]>,
@@ -1672,7 +1672,7 @@ struct RenderedText {
links: Rc<[RenderedLink]>,
}
-#[derive(Clone, Eq, PartialEq)]
+#[derive(Debug, Clone, Eq, PartialEq)]
struct RenderedLink {
source_range: Range<usize>,
destination_url: SharedString,
@@ -54,6 +54,7 @@ indexmap.workspace = true
language.workspace = true
log.workspace = true
lsp.workspace = true
+markdown.workspace = true
node_runtime.workspace = true
parking_lot.workspace = true
pathdiff.workspace = true
@@ -1846,12 +1846,15 @@ impl LspCommand for GetSignatureHelp {
async fn response_from_lsp(
self,
message: Option<lsp::SignatureHelp>,
- _: Entity<LspStore>,
+ lsp_store: Entity<LspStore>,
_: Entity<Buffer>,
_: LanguageServerId,
- _: AsyncApp,
+ cx: AsyncApp,
) -> Result<Self::Response> {
- Ok(message.and_then(SignatureHelp::new))
+ let Some(message) = message else {
+ return Ok(None);
+ };
+ cx.update(|cx| SignatureHelp::new(message, Some(lsp_store.read(cx).languages.clone()), cx))
}
fn to_proto(&self, project_id: u64, buffer: &Buffer) -> Self::ProtoRequest {
@@ -1902,14 +1905,18 @@ impl LspCommand for GetSignatureHelp {
async fn response_from_proto(
self,
response: proto::GetSignatureHelpResponse,
- _: Entity<LspStore>,
+ lsp_store: Entity<LspStore>,
_: Entity<Buffer>,
- _: AsyncApp,
+ cx: AsyncApp,
) -> Result<Self::Response> {
- Ok(response
- .signature_help
- .map(proto_to_lsp_signature)
- .and_then(SignatureHelp::new))
+ cx.update(|cx| {
+ response
+ .signature_help
+ .map(proto_to_lsp_signature)
+ .and_then(|signature| {
+ SignatureHelp::new(signature, Some(lsp_store.read(cx).languages.clone()), cx)
+ })
+ })
}
fn buffer_id_from_proto(message: &Self::ProtoRequest) -> Result<BufferId> {
@@ -1,94 +1,143 @@
-use std::ops::Range;
+use std::{ops::Range, sync::Arc};
-use gpui::{FontStyle, FontWeight, HighlightStyle};
+use gpui::{App, AppContext, Entity, FontWeight, HighlightStyle, SharedString};
+use language::LanguageRegistry;
+use markdown::Markdown;
use rpc::proto::{self, documentation};
#[derive(Debug)]
pub struct SignatureHelp {
- pub label: String,
- pub highlights: Vec<(Range<usize>, HighlightStyle)>,
+ pub active_signature: usize,
+ pub signatures: Vec<SignatureHelpData>,
pub(super) original_data: lsp::SignatureHelp,
}
-impl SignatureHelp {
- pub fn new(help: lsp::SignatureHelp) -> Option<Self> {
- let function_options_count = help.signatures.len();
-
- let signature_information = help
- .active_signature
- .and_then(|active_signature| help.signatures.get(active_signature as usize))
- .or_else(|| help.signatures.first())?;
-
- let str_for_join = ", ";
- let parameter_length = signature_information
- .parameters
- .as_ref()
- .map_or(0, |parameters| parameters.len());
- let mut highlight_start = 0;
- let (strings, mut highlights): (Vec<_>, Vec<_>) = signature_information
- .parameters
- .as_ref()?
- .iter()
- .enumerate()
- .map(|(i, parameter_information)| {
- let label = match parameter_information.label.clone() {
- lsp::ParameterLabel::Simple(string) => string,
- lsp::ParameterLabel::LabelOffsets(offset) => signature_information
- .label
- .chars()
- .skip(offset[0] as usize)
- .take((offset[1] - offset[0]) as usize)
- .collect::<String>(),
- };
- let label_length = label.len();
-
- let highlights = help.active_parameter.and_then(|active_parameter| {
- if i == active_parameter as usize {
- Some((
- highlight_start..(highlight_start + label_length),
- HighlightStyle {
- font_weight: Some(FontWeight::EXTRA_BOLD),
- ..Default::default()
- },
- ))
- } else {
- None
- }
- });
+#[derive(Debug, Clone)]
+pub struct SignatureHelpData {
+ pub label: SharedString,
+ pub documentation: Option<Entity<Markdown>>,
+ pub highlights: Vec<(Range<usize>, HighlightStyle)>,
+ pub active_parameter: Option<usize>,
+ pub parameters: Vec<ParameterInfo>,
+}
+
+#[derive(Debug, Clone)]
+pub struct ParameterInfo {
+ pub label_range: Option<Range<usize>>,
+ pub documentation: Option<Entity<Markdown>>,
+}
- if i != parameter_length {
- highlight_start += label_length + str_for_join.len();
+impl SignatureHelp {
+ pub fn new(
+ help: lsp::SignatureHelp,
+ language_registry: Option<Arc<LanguageRegistry>>,
+ cx: &mut App,
+ ) -> Option<Self> {
+ if help.signatures.is_empty() {
+ return None;
+ }
+ let active_signature = help.active_signature.unwrap_or(0) as usize;
+ let mut signatures = Vec::<SignatureHelpData>::with_capacity(help.signatures.capacity());
+ for signature in &help.signatures {
+ let active_parameter = signature
+ .active_parameter
+ .unwrap_or_else(|| help.active_parameter.unwrap_or(0))
+ as usize;
+ let mut highlights = Vec::new();
+ let mut parameter_infos = Vec::new();
+
+ if let Some(parameters) = &signature.parameters {
+ for (index, parameter) in parameters.iter().enumerate() {
+ let label_range = match ¶meter.label {
+ lsp::ParameterLabel::LabelOffsets(parameter_label_offsets) => {
+ let range = *parameter_label_offsets.get(0)? as usize
+ ..*parameter_label_offsets.get(1)? as usize;
+ if index == active_parameter {
+ highlights.push((
+ range.clone(),
+ HighlightStyle {
+ font_weight: Some(FontWeight::EXTRA_BOLD),
+ ..HighlightStyle::default()
+ },
+ ));
+ }
+ Some(range)
+ }
+ lsp::ParameterLabel::Simple(parameter_label) => {
+ if let Some(start) = signature.label.find(parameter_label) {
+ let range = start..start + parameter_label.len();
+ if index == active_parameter {
+ highlights.push((
+ range.clone(),
+ HighlightStyle {
+ font_weight: Some(FontWeight::EXTRA_BOLD),
+ ..HighlightStyle::default()
+ },
+ ));
+ }
+ Some(range)
+ } else {
+ None
+ }
+ }
+ };
+
+ let documentation = parameter
+ .documentation
+ .as_ref()
+ .map(|doc| documentation_to_markdown(doc, language_registry.clone(), cx));
+
+ parameter_infos.push(ParameterInfo {
+ label_range,
+ documentation,
+ });
}
+ }
- (label, highlights)
- })
- .unzip();
-
- if strings.is_empty() {
- None
- } else {
- let mut label = strings.join(str_for_join);
-
- if function_options_count >= 2 {
- let suffix = format!("(+{} overload)", function_options_count - 1);
- let highlight_start = label.len() + 1;
- highlights.push(Some((
- highlight_start..(highlight_start + suffix.len()),
- HighlightStyle {
- font_style: Some(FontStyle::Italic),
- ..Default::default()
- },
- )));
- label.push(' ');
- label.push_str(&suffix);
- };
+ let label = SharedString::from(signature.label.clone());
+ let documentation = signature
+ .documentation
+ .as_ref()
+ .map(|doc| documentation_to_markdown(doc, language_registry.clone(), cx));
- Some(Self {
+ signatures.push(SignatureHelpData {
label,
- highlights: highlights.into_iter().flatten().collect(),
- original_data: help,
- })
+ documentation,
+ highlights,
+ active_parameter: Some(active_parameter),
+ parameters: parameter_infos,
+ });
+ }
+ Some(Self {
+ signatures,
+ active_signature,
+ original_data: help,
+ })
+ }
+}
+
+fn documentation_to_markdown(
+ documentation: &lsp::Documentation,
+ language_registry: Option<Arc<LanguageRegistry>>,
+ cx: &mut App,
+) -> Entity<Markdown> {
+ match documentation {
+ lsp::Documentation::String(string) => {
+ cx.new(|cx| Markdown::new_text(SharedString::from(string), cx))
}
+ lsp::Documentation::MarkupContent(markup) => match markup.kind {
+ lsp::MarkupKind::PlainText => {
+ cx.new(|cx| Markdown::new_text(SharedString::from(&markup.value), cx))
+ }
+ lsp::MarkupKind::Markdown => cx.new(|cx| {
+ Markdown::new(
+ SharedString::from(&markup.value),
+ language_registry,
+ None,
+ cx,
+ )
+ }),
+ },
}
}
@@ -206,7 +255,8 @@ fn proto_to_lsp_documentation(documentation: proto::Documentation) -> Option<lsp
#[cfg(test)]
mod tests {
- use gpui::{FontStyle, FontWeight, HighlightStyle};
+ use gpui::{FontWeight, HighlightStyle, SharedString, TestAppContext};
+ use lsp::{Documentation, MarkupContent, MarkupKind};
use crate::lsp_command::signature_help::SignatureHelp;
@@ -217,19 +267,14 @@ mod tests {
}
}
- fn overload() -> HighlightStyle {
- HighlightStyle {
- font_style: Some(FontStyle::Italic),
- ..Default::default()
- }
- }
-
- #[test]
- fn test_create_signature_help_markdown_string_1() {
+ #[gpui::test]
+ fn test_create_signature_help_markdown_string_1(cx: &mut TestAppContext) {
let signature_help = lsp::SignatureHelp {
signatures: vec![lsp::SignatureInformation {
label: "fn test(foo: u8, bar: &str)".to_string(),
- documentation: None,
+ documentation: Some(Documentation::String(
+ "This is a test documentation".to_string(),
+ )),
parameters: Some(vec![
lsp::ParameterInformation {
label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
@@ -245,26 +290,37 @@ mod tests {
active_signature: Some(0),
active_parameter: Some(0),
};
- let maybe_markdown = SignatureHelp::new(signature_help);
+ let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, cx));
assert!(maybe_markdown.is_some());
let markdown = maybe_markdown.unwrap();
- let markdown = (markdown.label, markdown.highlights);
+ let signature = markdown.signatures[markdown.active_signature].clone();
+ let markdown = (signature.label, signature.highlights);
assert_eq!(
markdown,
(
- "foo: u8, bar: &str".to_string(),
- vec![(0..7, current_parameter())]
+ SharedString::new("fn test(foo: u8, bar: &str)"),
+ vec![(8..15, current_parameter())]
)
);
+ assert_eq!(
+ signature
+ .documentation
+ .unwrap()
+ .update(cx, |documentation, _| documentation.source().to_owned()),
+ "This is a test documentation",
+ )
}
- #[test]
- fn test_create_signature_help_markdown_string_2() {
+ #[gpui::test]
+ fn test_create_signature_help_markdown_string_2(cx: &mut TestAppContext) {
let signature_help = lsp::SignatureHelp {
signatures: vec![lsp::SignatureInformation {
label: "fn test(foo: u8, bar: &str)".to_string(),
- documentation: None,
+ documentation: Some(Documentation::MarkupContent(MarkupContent {
+ kind: MarkupKind::Markdown,
+ value: "This is a test documentation".to_string(),
+ })),
parameters: Some(vec![
lsp::ParameterInformation {
label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
@@ -280,22 +336,30 @@ mod tests {
active_signature: Some(0),
active_parameter: Some(1),
};
- let maybe_markdown = SignatureHelp::new(signature_help);
+ let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, cx));
assert!(maybe_markdown.is_some());
let markdown = maybe_markdown.unwrap();
- let markdown = (markdown.label, markdown.highlights);
+ let signature = markdown.signatures[markdown.active_signature].clone();
+ let markdown = (signature.label, signature.highlights);
assert_eq!(
markdown,
(
- "foo: u8, bar: &str".to_string(),
- vec![(9..18, current_parameter())]
+ SharedString::new("fn test(foo: u8, bar: &str)"),
+ vec![(17..26, current_parameter())]
)
);
+ assert_eq!(
+ signature
+ .documentation
+ .unwrap()
+ .update(cx, |documentation, _| documentation.source().to_owned()),
+ "This is a test documentation",
+ )
}
- #[test]
- fn test_create_signature_help_markdown_string_3() {
+ #[gpui::test]
+ fn test_create_signature_help_markdown_string_3(cx: &mut TestAppContext) {
let signature_help = lsp::SignatureHelp {
signatures: vec![
lsp::SignatureInformation {
@@ -332,22 +396,23 @@ mod tests {
active_signature: Some(0),
active_parameter: Some(0),
};
- let maybe_markdown = SignatureHelp::new(signature_help);
+ let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, cx));
assert!(maybe_markdown.is_some());
let markdown = maybe_markdown.unwrap();
- let markdown = (markdown.label, markdown.highlights);
+ let signature = markdown.signatures[markdown.active_signature].clone();
+ let markdown = (signature.label, signature.highlights);
assert_eq!(
markdown,
(
- "foo: u8, bar: &str (+1 overload)".to_string(),
- vec![(0..7, current_parameter()), (19..32, overload())]
+ SharedString::new("fn test1(foo: u8, bar: &str)"),
+ vec![(9..16, current_parameter())]
)
);
}
- #[test]
- fn test_create_signature_help_markdown_string_4() {
+ #[gpui::test]
+ fn test_create_signature_help_markdown_string_4(cx: &mut TestAppContext) {
let signature_help = lsp::SignatureHelp {
signatures: vec![
lsp::SignatureInformation {
@@ -384,22 +449,23 @@ mod tests {
active_signature: Some(1),
active_parameter: Some(0),
};
- let maybe_markdown = SignatureHelp::new(signature_help);
+ let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, cx));
assert!(maybe_markdown.is_some());
let markdown = maybe_markdown.unwrap();
- let markdown = (markdown.label, markdown.highlights);
+ let signature = markdown.signatures[markdown.active_signature].clone();
+ let markdown = (signature.label, signature.highlights);
assert_eq!(
markdown,
(
- "hoge: String, fuga: bool (+1 overload)".to_string(),
- vec![(0..12, current_parameter()), (25..38, overload())]
+ SharedString::new("fn test2(hoge: String, fuga: bool)"),
+ vec![(9..21, current_parameter())]
)
);
}
- #[test]
- fn test_create_signature_help_markdown_string_5() {
+ #[gpui::test]
+ fn test_create_signature_help_markdown_string_5(cx: &mut TestAppContext) {
let signature_help = lsp::SignatureHelp {
signatures: vec![
lsp::SignatureInformation {
@@ -436,22 +502,23 @@ mod tests {
active_signature: Some(1),
active_parameter: Some(1),
};
- let maybe_markdown = SignatureHelp::new(signature_help);
+ let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, cx));
assert!(maybe_markdown.is_some());
let markdown = maybe_markdown.unwrap();
- let markdown = (markdown.label, markdown.highlights);
+ let signature = markdown.signatures[markdown.active_signature].clone();
+ let markdown = (signature.label, signature.highlights);
assert_eq!(
markdown,
(
- "hoge: String, fuga: bool (+1 overload)".to_string(),
- vec![(14..24, current_parameter()), (25..38, overload())]
+ SharedString::new("fn test2(hoge: String, fuga: bool)"),
+ vec![(23..33, current_parameter())]
)
);
}
- #[test]
- fn test_create_signature_help_markdown_string_6() {
+ #[gpui::test]
+ fn test_create_signature_help_markdown_string_6(cx: &mut TestAppContext) {
let signature_help = lsp::SignatureHelp {
signatures: vec![
lsp::SignatureInformation {
@@ -488,22 +555,23 @@ mod tests {
active_signature: Some(1),
active_parameter: None,
};
- let maybe_markdown = SignatureHelp::new(signature_help);
+ let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, cx));
assert!(maybe_markdown.is_some());
let markdown = maybe_markdown.unwrap();
- let markdown = (markdown.label, markdown.highlights);
+ let signature = markdown.signatures[markdown.active_signature].clone();
+ let markdown = (signature.label, signature.highlights);
assert_eq!(
markdown,
(
- "hoge: String, fuga: bool (+1 overload)".to_string(),
- vec![(25..38, overload())]
+ SharedString::new("fn test2(hoge: String, fuga: bool)"),
+ vec![(9..21, current_parameter())]
)
);
}
- #[test]
- fn test_create_signature_help_markdown_string_7() {
+ #[gpui::test]
+ fn test_create_signature_help_markdown_string_7(cx: &mut TestAppContext) {
let signature_help = lsp::SignatureHelp {
signatures: vec![
lsp::SignatureInformation {
@@ -555,33 +623,34 @@ mod tests {
active_signature: Some(2),
active_parameter: Some(1),
};
- let maybe_markdown = SignatureHelp::new(signature_help);
+ let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, cx));
assert!(maybe_markdown.is_some());
let markdown = maybe_markdown.unwrap();
- let markdown = (markdown.label, markdown.highlights);
+ let signature = markdown.signatures[markdown.active_signature].clone();
+ let markdown = (signature.label, signature.highlights);
assert_eq!(
markdown,
(
- "one: usize, two: u32 (+2 overload)".to_string(),
- vec![(12..20, current_parameter()), (21..34, overload())]
+ SharedString::new("fn test3(one: usize, two: u32)"),
+ vec![(21..29, current_parameter())]
)
);
}
- #[test]
- fn test_create_signature_help_markdown_string_8() {
+ #[gpui::test]
+ fn test_create_signature_help_markdown_string_8(cx: &mut TestAppContext) {
let signature_help = lsp::SignatureHelp {
signatures: vec![],
active_signature: None,
active_parameter: None,
};
- let maybe_markdown = SignatureHelp::new(signature_help);
+ let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, cx));
assert!(maybe_markdown.is_none());
}
- #[test]
- fn test_create_signature_help_markdown_string_9() {
+ #[gpui::test]
+ fn test_create_signature_help_markdown_string_9(cx: &mut TestAppContext) {
let signature_help = lsp::SignatureHelp {
signatures: vec![lsp::SignatureInformation {
label: "fn test(foo: u8, bar: &str)".to_string(),
@@ -601,17 +670,70 @@ mod tests {
active_signature: Some(0),
active_parameter: Some(0),
};
- let maybe_markdown = SignatureHelp::new(signature_help);
+ let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, cx));
assert!(maybe_markdown.is_some());
let markdown = maybe_markdown.unwrap();
- let markdown = (markdown.label, markdown.highlights);
+ let signature = markdown.signatures[markdown.active_signature].clone();
+ let markdown = (signature.label, signature.highlights);
assert_eq!(
markdown,
(
- "foo: u8, bar: &str".to_string(),
- vec![(0..7, current_parameter())]
+ SharedString::new("fn test(foo: u8, bar: &str)"),
+ vec![(8..15, current_parameter())]
)
);
}
+
+ #[gpui::test]
+ fn test_parameter_documentation(cx: &mut TestAppContext) {
+ let signature_help = lsp::SignatureHelp {
+ signatures: vec![lsp::SignatureInformation {
+ label: "fn test(foo: u8, bar: &str)".to_string(),
+ documentation: Some(Documentation::String(
+ "This is a test documentation".to_string(),
+ )),
+ parameters: Some(vec![
+ lsp::ParameterInformation {
+ label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
+ documentation: Some(Documentation::String("The foo parameter".to_string())),
+ },
+ lsp::ParameterInformation {
+ label: lsp::ParameterLabel::Simple("bar: &str".to_string()),
+ documentation: Some(Documentation::String("The bar parameter".to_string())),
+ },
+ ]),
+ active_parameter: None,
+ }],
+ active_signature: Some(0),
+ active_parameter: Some(0),
+ };
+ let maybe_signature_help = cx.update(|cx| SignatureHelp::new(signature_help, None, cx));
+ assert!(maybe_signature_help.is_some());
+
+ let signature_help = maybe_signature_help.unwrap();
+ let signature = &signature_help.signatures[signature_help.active_signature];
+
+ // Check that parameter documentation is extracted
+ assert_eq!(signature.parameters.len(), 2);
+ assert_eq!(
+ signature.parameters[0]
+ .documentation
+ .as_ref()
+ .unwrap()
+ .update(cx, |documentation, _| documentation.source().to_owned()),
+ "The foo parameter",
+ );
+ assert_eq!(
+ signature.parameters[1]
+ .documentation
+ .as_ref()
+ .unwrap()
+ .update(cx, |documentation, _| documentation.source().to_owned()),
+ "The bar parameter",
+ );
+
+ // Check that the active parameter is correct
+ assert_eq!(signature.active_parameter, Some(0));
+ }
}
@@ -6504,7 +6504,6 @@ impl LspStore {
.await
.into_iter()
.flat_map(|(_, actions)| actions)
- .filter(|help| !help.label.is_empty())
.collect::<Vec<_>>()
})
}
@@ -7,7 +7,7 @@ use crate::{
};
/// A collection of colors that are used to color indent aware lines in the editor.
-#[derive(Clone, Deserialize, PartialEq)]
+#[derive(Clone, Debug, Deserialize, PartialEq)]
pub struct AccentColors(pub Vec<Hsla>);
impl Default for AccentColors {
@@ -535,7 +535,7 @@ pub fn all_theme_colors(cx: &mut App) -> Vec<(Hsla, SharedString)> {
.collect()
}
-#[derive(Refineable, Clone, PartialEq)]
+#[derive(Refineable, Clone, Debug, PartialEq)]
pub struct ThemeStyles {
/// The background appearance of the window.
pub window_background_appearance: WindowBackgroundAppearance,
@@ -20,7 +20,7 @@ pub struct PlayerColor {
///
/// The rest of the default colors crisscross back and forth on the
/// color wheel so that the colors are as distinct as possible.
-#[derive(Clone, Deserialize, PartialEq)]
+#[derive(Clone, Debug, Deserialize, PartialEq)]
pub struct PlayerColors(pub Vec<PlayerColor>);
impl Default for PlayerColors {
@@ -2,7 +2,7 @@
use gpui::{Hsla, hsla};
-#[derive(Clone, PartialEq)]
+#[derive(Clone, Debug, PartialEq)]
pub struct SystemColors {
pub transparent: Hsla,
pub mac_os_traffic_light_red: Hsla,
@@ -268,7 +268,7 @@ pub fn refine_theme_family(theme_family_content: ThemeFamilyContent) -> ThemeFam
}
/// A theme is the primary mechanism for defining the appearance of the UI.
-#[derive(Clone, PartialEq)]
+#[derive(Clone, Debug, PartialEq)]
pub struct Theme {
/// The unique identifier for the theme.
pub id: String,