@@ -1527,6 +1527,84 @@ fn tree_sitter_scopes(
15271527 Ok(())
15281528}
15291529
1530+fn tree_sitter_highlight_name(
1531+ cx: &mut compositor::Context,
1532+ _args: &[Cow<str>],
1533+ event: PromptEvent,
1534+) -> anyhow::Result<()> {
1535+ fn find_highlight_at_cursor(
1536+ cx: &mut compositor::Context<'_>,
1537+ ) -> Option<helix_core::syntax::Highlight> {
1538+ use helix_core::syntax::HighlightEvent;
1539+
1540+ let (view, doc) = current!(cx.editor);
1541+ let syntax = doc.syntax()?;
1542+ let text = doc.text().slice(..);
1543+ let cursor = doc.selection(view.id).primary().cursor(text);
1544+ let byte = text.char_to_byte(cursor);
1545+ let node = syntax
1546+ .tree()
1547+ .root_node()
1548+ .descendant_for_byte_range(byte, byte)?;
1549+ // Query the same range as the one used in syntax highlighting.
1550+ let range = {
1551+ // Calculate viewport byte ranges:
1552+ let row = text.char_to_line(view.offset.anchor.min(text.len_chars()));
1553+ // Saturating subs to make it inclusive zero indexing.
1554+ let last_line = text.len_lines().saturating_sub(1);
1555+ let height = view.inner_area(doc).height;
1556+ let last_visible_line = (row + height as usize).saturating_sub(1).min(last_line);
1557+ let start = text.line_to_byte(row.min(last_line));
1558+ let end = text.line_to_byte(last_visible_line + 1);
1559+
1560+ start..end
1561+ };
1562+
1563+ let mut highlight = None;
1564+
1565+ for event in syntax.highlight_iter(text, Some(range), None) {
1566+ match event.unwrap() {
1567+ HighlightEvent::Source { start, end }
1568+ if start == node.start_byte() && end == node.end_byte() =>
1569+ {
1570+ return highlight;
1571+ }
1572+ HighlightEvent::HighlightStart(hl) => {
1573+ highlight = Some(hl);
1574+ }
1575+ _ => (),
1576+ }
1577+ }
1578+
1579+ None
1580+ }
1581+
1582+ if event != PromptEvent::Validate {
1583+ return Ok(());
1584+ }
1585+
1586+ let Some(highlight) = find_highlight_at_cursor(cx) else {
1587+ return Ok(());
1588+ };
1589+
1590+ let content = cx.editor.theme.scope(highlight.0).to_string();
1591+
1592+ let callback = async move {
1593+ let call: job::Callback = Callback::EditorCompositor(Box::new(
1594+ move |editor: &mut Editor, compositor: &mut Compositor| {
1595+ let content = ui::Markdown::new(content, editor.syn_loader.clone());
1596+ let popup = Popup::new("hover", content).auto_close(true);
1597+ compositor.replace_or_push("hover", popup);
1598+ },
1599+ ));
1600+ Ok(call)
1601+ };
1602+
1603+ cx.jobs.callback(callback);
1604+
1605+ Ok(())
1606+}
1607+
15301608fn vsplit(
15311609 cx: &mut compositor::Context,
15321610 args: &[Cow<str>],
@@ -2703,6 +2781,13 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
27032781 fun: tree_sitter_scopes,
27042782 signature: CommandSignature::none(),
27052783 },
2784+ TypableCommand {
2785+ name: "tree-sitter-highlight-name",
2786+ aliases: &[],
2787+ doc: "Display name of tree-sitter highlight scope under the cursor.",
2788+ fun: tree_sitter_highlight_name,
2789+ signature: CommandSignature::none(),
2790+ },
27062791 TypableCommand {
27072792 name: "debug-start",
27082793 aliases: &["dbg"],