diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs index b229cd1a2..77e877e4c 100644 --- a/helix-lsp/src/client.rs +++ b/helix-lsp/src/client.rs @@ -201,11 +201,12 @@ impl Client { context_support: None, // additional context information Some(true) ..Default::default() }), - // { completion: { - // dynamic_registration: bool - // completion_item: { snippet, documentation_format, ... } - // completion_item_kind: { } - // } } + hover: Some(lsp::HoverClientCapabilities { + // if not specified, rust-analyzer returns plaintext marked as markdown but + // badly formatted. + content_format: Some(vec![lsp::MarkupKind::Markdown]), + ..Default::default() + }), ..Default::default() }), ..Default::default() @@ -458,4 +459,51 @@ impl Client { Ok(items) } + + pub async fn text_document_signature_help( + &self, + text_document: lsp::TextDocumentIdentifier, + position: lsp::Position, + ) -> anyhow::Result> { + let params = lsp::SignatureHelpParams { + text_document_position_params: lsp::TextDocumentPositionParams { + text_document, + position, + }, + // TODO: support these tokens + work_done_progress_params: lsp::WorkDoneProgressParams { + work_done_token: None, + }, + context: None, + // lsp::SignatureHelpContext + }; + + let response = self + .request::(params) + .await?; + + Ok(response) + } + + pub async fn text_document_hover( + &self, + text_document: lsp::TextDocumentIdentifier, + position: lsp::Position, + ) -> anyhow::Result> { + let params = lsp::HoverParams { + text_document_position_params: lsp::TextDocumentPositionParams { + text_document, + position, + }, + // TODO: support these tokens + work_done_progress_params: lsp::WorkDoneProgressParams { + work_done_token: None, + }, + // lsp::SignatureHelpContext + }; + + let response = self.request::(params).await?; + + Ok(response) + } } diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 6b1109ba4..c5c2ecf18 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -11,7 +11,7 @@ use helix_core::{ use once_cell::sync::Lazy; use crate::compositor::Compositor; -use crate::ui::{self, Prompt, PromptEvent}; +use crate::ui::{self, Popup, Prompt, PromptEvent}; use helix_view::{ document::Mode, @@ -1000,6 +1000,63 @@ pub fn completion(cx: &mut Context) { } } +pub fn hover(cx: &mut Context) { + use helix_lsp::lsp; + + let doc = cx.doc(); + + let language_server = match doc.language_server.as_ref() { + Some(language_server) => language_server, + None => return, + }; + + // TODO: factor out a doc.position_identifier() that returns lsp::TextDocumentPositionIdentifier + + // TODO: blocking here is not ideal, make commands async fn? + // not like we can process additional input meanwhile though + let pos = helix_lsp::util::pos_to_lsp_pos(doc.text().slice(..), doc.selection().cursor()); + + // TODO: handle fails + let res = smol::block_on(language_server.text_document_hover(doc.identifier(), pos)) + .unwrap_or_default(); + + if let Some(hover) = res { + // hover.contents / .range <- used for visualizing + let contents = match hover.contents { + lsp::HoverContents::Scalar(contents) => { + // markedstring(string/languagestring to be highlighted) + // TODO + unimplemented!("{:?}", contents) + } + lsp::HoverContents::Array(contents) => { + unimplemented!("{:?}", contents) + } + // TODO: render markdown + lsp::HoverContents::Markup(contents) => contents.value, + }; + + // skip if contents empty + + // Popup: box frame + Box for internal content. + // it will use the contents.size_hint/required size to figure out sizing & positioning + // can also use render_buffer to render the content. + // render_buffer(highlights/scopes, text, surface, theme) + // + let mut popup = Popup::new(contents); + + cx.callback = Some(Box::new( + move |compositor: &mut Compositor, editor: &mut Editor| { + let area = tui::layout::Rect::default(); // TODO: unused remove from cursor_position + let mut pos = compositor.cursor_position(area, editor); + pos.row += 1; // shift down by one row + popup.set_position(pos); + + compositor.push(Box::new(popup)); + }, + )); + } +} + // view movements pub fn next_view(cx: &mut Context) { cx.editor.tree.focus_next() diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 81665bbe0..ac4a2bf24 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -203,6 +203,8 @@ pub fn default() -> Keymaps { // move under c vec![ctrl!('c')] => commands::toggle_comments, + // was K, figure out a key + vec![ctrl!('k')] => commands::hover, ), Mode::Insert => hashmap!( vec![Key { diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index af9d04143..b071292c5 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -100,6 +100,7 @@ impl EditorView { let mut spans = Vec::new(); let mut visual_x = 0; let mut line = 0u16; + let text = view.doc.text(); 'outer: for event in highlights { match event.unwrap() { @@ -113,7 +114,6 @@ impl EditorView { // TODO: filter out spans out of viewport for now.. // TODO: do these before iterating - let text = view.doc.text(); let start = text.byte_to_char(start); let end = text.byte_to_char(end); @@ -160,8 +160,7 @@ impl EditorView { let grapheme = Cow::from(grapheme); let width = grapheme_width(&grapheme) as u16; - // ugh, improve with a traverse method - // or interleave highlight spans with selection and diagnostic spans + // ugh,interleave highlight spans with diagnostic spans let is_diagnostic = view.doc.diagnostics.iter().any(|diagnostic| { diagnostic.range.0 <= char_index && diagnostic.range.1 > char_index }); @@ -191,12 +190,12 @@ impl EditorView { // render selections if is_focused { - let text = view.doc.text().slice(..); let screen = { let start = text.line_to_char(view.first_line); let end = text.line_to_char(last_line + 1); Range::new(start, end) }; + let text = text.slice(..); let cursor_style = Style::default().bg(Color::Rgb(255, 255, 255)); // cedar diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index e18991444..ea1d22abe 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -1,11 +1,13 @@ mod editor; mod menu; mod picker; +mod popup; mod prompt; pub use editor::EditorView; pub use menu::Menu; pub use picker::Picker; +pub use popup::Popup; pub use prompt::{Prompt, PromptEvent}; pub use tui::layout::Rect; diff --git a/helix-term/src/ui/popup.rs b/helix-term/src/ui/popup.rs new file mode 100644 index 000000000..ca8607070 --- /dev/null +++ b/helix-term/src/ui/popup.rs @@ -0,0 +1,95 @@ +use crate::compositor::{Component, Compositor, Context, EventResult}; +use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers}; +use tui::buffer::Buffer as Surface; +use tui::{ + layout::Rect, + style::{Color, Style}, + widgets::{Block, Borders}, +}; + +use std::borrow::Cow; + +use helix_core::Position; +use helix_view::Editor; + +pub struct Popup { + contents: String, + position: Position, +} + +impl Popup { + // TODO: it's like a slimmed down picker, share code? (picker = menu + prompt with different + // rendering) + pub fn new(contents: String) -> Self { + Self { + contents, + position: Position::default(), + } + } + + pub fn set_position(&mut self, pos: Position) { + self.position = pos; + } +} + +impl Component for Popup { + fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult { + let event = match event { + Event::Key(event) => event, + _ => return EventResult::Ignored, + }; + + let close_fn = EventResult::Consumed(Some(Box::new( + |compositor: &mut Compositor, editor: &mut Editor| { + // remove the layer + compositor.pop(); + }, + ))); + + match event { + // esc or ctrl-c aborts the completion and closes the menu + KeyEvent { + code: KeyCode::Esc, .. + } + | KeyEvent { + code: KeyCode::Char('c'), + modifiers: KeyModifiers::CONTROL, + } => { + return close_fn; + } + _ => (), + } + // for some events, we want to process them but send ignore, specifically all input except + // tab/enter/ctrl-k or whatever will confirm the selection/ ctrl-n/ctrl-p for scroll. + // EventResult::Consumed(None) + EventResult::Consumed(None) + } + fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) { + // render a box at x, y. Width equal to max width of item. + const MAX: usize = 15; + let rows = std::cmp::min(self.contents.lines().count(), MAX) as u16; // inefficient + let area = Rect::new(self.position.col as u16, self.position.row as u16, 80, rows); + + // clear area + let background = cx.editor.theme.get("ui.popup"); + for y in area.top()..area.bottom() { + for x in area.left()..area.right() { + let cell = surface.get_mut(x, y); + cell.reset(); + // cell.symbol.clear(); + cell.set_style(background); + } + } + + // -- Render the contents: + + let style = Style::default().fg(Color::Rgb(164, 160, 232)); // lavender + + use tui::text::Text; + use tui::widgets::{Paragraph, Widget, Wrap}; + let contents = Text::from(self.contents.clone()); + let par = Paragraph::new(contents).wrap(Wrap { trim: false }); + + par.render(area, surface); + } +}