Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
234 changes: 133 additions & 101 deletions helix-term/src/ui/completion.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
use crate::compositor::{Component, Context, Event, EventResult};
use crate::{
compositor::{Component, Context, Event, EventResult},
ctrl, key,
};
use helix_view::{apply_transaction, editor::CompleteAction};
use tui::buffer::Buffer as Surface;
use tui::text::Spans;

use std::borrow::Cow;

use helix_core::{Change, Transaction};
use helix_view::{
graphics::Rect,
input::{KeyCode, KeyEvent},
Document, Editor,
};
use helix_view::{graphics::Rect, Document, Editor};

use crate::commands;
use crate::ui::{menu, Markdown, Menu, Popup, PromptEvent};
Expand Down Expand Up @@ -85,6 +84,8 @@ pub struct Completion {
#[allow(dead_code)]
trigger_offset: usize,
// TODO: maintain a completioncontext with trigger kind & trigger char
active_markdown_doc: (String, Option<Box<Popup<Markdown>>>),
markdown_area: Rect,
}

impl Completion {
Expand Down Expand Up @@ -224,6 +225,8 @@ impl Completion {
popup,
start_offset,
trigger_offset,
active_markdown_doc: (String::new(), None),
markdown_area: Rect::default(),
};

// need to recompute immediately in case start_offset != trigger_offset
Expand Down Expand Up @@ -320,13 +323,21 @@ impl Completion {

impl Component for Completion {
fn handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult {
// let the Editor handle Esc instead
if let Event::Key(KeyEvent {
code: KeyCode::Esc, ..
}) = event
{
return EventResult::Ignored(None);
if let Event::Key(key_event) = event {
match key_event {
// let the Editor handle Esc instead
key!(Esc) => {
return EventResult::Ignored(None);
}

ctrl!('u') | ctrl!('d') if self.active_markdown_doc.1.is_some() => {
let popup = self.active_markdown_doc.1.as_mut().unwrap();
return popup.handle_event(event, cx);
}
_ => {}
}
}

self.popup.handle_event(event, cx)
}

Expand All @@ -339,102 +350,123 @@ impl Component for Completion {

// if we have a selection, render a markdown popup on top/below with info
if let Some(option) = self.popup.contents().selection() {
// need to render:
// option.detail
// ---
// option.documentation

let (view, doc) = current!(cx.editor);
let language = doc.language_name().unwrap_or("");
let text = doc.text().slice(..);
let cursor_pos = doc.selection(view.id).primary().cursor(text);
let coords = helix_core::visual_coords_at_pos(text, cursor_pos, doc.tab_width());
let cursor_pos = (coords.row - view.offset.row) as u16;

let mut markdown_doc = match &option.documentation {
Some(lsp::Documentation::String(contents))
| Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
kind: lsp::MarkupKind::PlainText,
value: contents,
})) => {
// TODO: convert to wrapped text
Markdown::new(
format!(
"```{}\n{}\n```\n{}",
language,
option.detail.as_deref().unwrap_or_default(),
contents.clone()
),
cx.editor.syn_loader.clone(),
)
}
Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
kind: lsp::MarkupKind::Markdown,
value: contents,
})) => {
// TODO: set language based on doc scope
Markdown::new(
format!(
"```{}\n{}\n```\n{}",
language,
option.detail.as_deref().unwrap_or_default(),
contents.clone()
),
cx.editor.syn_loader.clone(),
)
}
None if option.detail.is_some() => {
// TODO: copied from above

// TODO: set language based on doc scope
Markdown::new(
format!(
"```{}\n{}\n```",
language,
option.detail.as_deref().unwrap_or_default(),
),
cx.editor.syn_loader.clone(),
)
}
None => return,
};
// update with the new option's doc
if option.label != self.active_markdown_doc.0 {
// need to render:
// option.detail
// ---
// option.documentation

let (view, doc) = current!(cx.editor);
let language = doc.language_name().unwrap_or("");
let text = doc.text().slice(..);
let cursor_pos = doc.selection(view.id).primary().cursor(text);
let coords = helix_core::visual_coords_at_pos(text, cursor_pos, doc.tab_width());
let cursor_pos = (coords.row - view.offset.row) as u16;

let markdown_doc = match &option.documentation {
Some(lsp::Documentation::String(contents))
| Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
kind: lsp::MarkupKind::PlainText,
value: contents,
})) => {
// TODO: convert to wrapped text
Markdown::new(
format!(
"```{}\n{}\n```\n{}",
language,
option.detail.as_deref().unwrap_or_default(),
contents.clone()
),
cx.editor.syn_loader.clone(),
)
}
Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
kind: lsp::MarkupKind::Markdown,
value: contents,
})) => {
// TODO: set language based on doc scope
Markdown::new(
format!(
"```{}\n{}\n```\n{}",
language,
option.detail.as_deref().unwrap_or_default(),
contents.clone()
),
cx.editor.syn_loader.clone(),
)
}
None if option.detail.is_some() => {
// TODO: copied from above

// TODO: set language based on doc scope
Markdown::new(
format!(
"```{}\n{}\n```",
language,
option.detail.as_deref().unwrap_or_default(),
),
cx.editor.syn_loader.clone(),
)
}
None => return,
};

let (popup_x, popup_y) = self.popup.get_rel_position(area, cx);
let (popup_width, _popup_height) = self.popup.get_size();
let mut width = area
.width
.saturating_sub(popup_x)
.saturating_sub(popup_width);
let area = if width > 30 {
let mut height = area.height.saturating_sub(popup_y);
let x = popup_x + popup_width;
let y = popup_y;

if let Some((rel_width, rel_height)) = markdown_doc.required_size((width, height)) {
width = rel_width.min(width);
height = rel_height.min(height);
}
Rect::new(x, y, width, height)
} else {
let half = area.height / 2;
let height = 15.min(half);
// we want to make sure the cursor is visible (not hidden behind the documentation)
let y = if cursor_pos + area.y
>= (cx.editor.tree.area().height - height - 2/* statusline + commandline */)
{
0
self.active_markdown_doc = (
option.label.clone(),
Some(Box::new(
Popup::new("documentation-popup", markdown_doc).force_viewport_render(true),
)),
);

let markdown_popup = self.active_markdown_doc.1.as_mut().unwrap();

let (popup_x, popup_y) = self.popup.get_rel_position(area, cx);
let (popup_width, _popup_height) = self.popup.get_size();
let mut width = area
.width
.saturating_sub(popup_x)
.saturating_sub(popup_width);
let area = if width > 30 {
let mut height = area.height.saturating_sub(popup_y);
let x = popup_x + popup_width;
let y = popup_y;

if let Some((rel_width, rel_height)) =
markdown_popup.required_size((width, height))
{
width = rel_width.min(width);
height = rel_height.min(height);
}
Rect::new(x, y, width, height)
} else {
// -2 to subtract command line + statusline. a bit of a hack, because of splits.
area.height.saturating_sub(height).saturating_sub(2)
let half = area.height / 2;
let height = 15.min(half);
// we want to make sure the cursor is visible (not hidden behind the documentation)
let y = if cursor_pos + area.y
>= (cx.editor.tree.area().height - height - 2/* statusline + commandline */)
{
0
} else {
// -2 to subtract command line + statusline. a bit of a hack, because of splits.
area.height.saturating_sub(height).saturating_sub(2)
};

Rect::new(0, y, area.width, height)
};

Rect::new(0, y, area.width, height)
};
self.markdown_area = area;
}

// render documentation
// clear area
let background = cx.editor.theme.get("ui.popup");
surface.clear_with(area, background);
markdown_doc.render(area, surface, cx);
surface.clear_with(self.markdown_area, background);
self.active_markdown_doc
.1
.as_mut()
.unwrap()
.render(self.markdown_area, surface, cx);
}
}
}
32 changes: 27 additions & 5 deletions helix-term/src/ui/popup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ pub struct Popup<T: Component> {
auto_close: bool,
ignore_escape_key: bool,
id: &'static str,
is_forced_on_viewport: bool,
}

impl<T: Component> Popup<T> {
Expand All @@ -37,6 +38,7 @@ impl<T: Component> Popup<T> {
auto_close: false,
ignore_escape_key: false,
id,
is_forced_on_viewport: false,
}
}

Expand Down Expand Up @@ -76,6 +78,15 @@ impl<T: Component> Popup<T> {
self
}

/// Makes the Popup render inside the whole given viewport
/// in `Popup::render()`, utilising all of its space.
/// This ignores the predefined (max_width, max_height) limitations
/// in `required_size()` and circumvents `get_rel_position()`.
pub fn force_viewport_render(mut self, render_in_viewport: bool) -> Self {
self.is_forced_on_viewport = render_in_viewport;
self
}

pub fn get_rel_position(&mut self, viewport: Rect, cx: &Context) -> (u16, u16) {
let position = self
.position
Expand Down Expand Up @@ -188,8 +199,16 @@ impl<T: Component> Component for Popup<T> {
}

fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
let max_width = 120.min(viewport.0);
let max_height = 26.min(viewport.1.saturating_sub(2)); // add some spacing in the viewport
let max_width = if !self.is_forced_on_viewport {
120.min(viewport.0)
} else {
viewport.0
};
let max_height = if !self.is_forced_on_viewport {
26.min(viewport.1.saturating_sub(2))
} else {
viewport.1
}; // add some spacing in the viewport

let inner = Rect::new(0, 0, max_width, max_height).inner(&self.margin);

Expand Down Expand Up @@ -217,10 +236,13 @@ impl<T: Component> Component for Popup<T> {

cx.scroll = Some(self.scroll);

let (rel_x, rel_y) = self.get_rel_position(viewport, cx);

// clip to viewport
let area = viewport.intersection(Rect::new(rel_x, rel_y, self.size.0, self.size.1));
let area = if !self.is_forced_on_viewport {
let (rel_x, rel_y) = self.get_rel_position(viewport, cx);
viewport.intersection(Rect::new(rel_x, rel_y, self.size.0, self.size.1))
} else {
viewport
};

// clear area
let background = cx.editor.theme.get("ui.popup");
Expand Down