Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make repeat operator work with completion edits #1640

Merged
merged 15 commits into from
Mar 1, 2022
Merged
96 changes: 93 additions & 3 deletions helix-term/src/ui/completion.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use crate::compositor::{Component, Context, EventResult};
use crossterm::event::{Event, KeyCode, KeyEvent};
use helix_view::editor::CompleteAction;
use helix_view::ViewId;
use tui::buffer::Buffer as Surface;

use std::borrow::Cow;
Expand Down Expand Up @@ -92,13 +94,14 @@ impl Completion {
start_offset: usize,
trigger_offset: usize,
) -> Transaction {
if let Some(edit) = &item.text_edit {
let transaction = if let Some(edit) = &item.text_edit {
let edit = match edit {
lsp::CompletionTextEdit::Edit(edit) => edit.clone(),
lsp::CompletionTextEdit::InsertAndReplace(item) => {
unimplemented!("completion: insert_and_replace {:?}", item)
}
};

util::generate_transaction_from_edits(
doc.text(),
vec![edit],
Expand All @@ -114,7 +117,63 @@ impl Completion {
doc.text(),
vec![(trigger_offset, trigger_offset, Some(text.into()))].into_iter(),
)
}
};

transaction
}

fn fetch_complete_action(
doc: &Document,
item: &CompletionItem,
offset_encoding: helix_lsp::OffsetEncoding,
start_offset: usize,
trigger_offset: usize,
view_id: ViewId,
) -> Option<CompleteAction> {
let text = doc.text().slice(..);
let cursor = doc.selection(view_id).primary().cursor(text);

let action = if let Some(edit) = &item.text_edit {
let edit = match edit {
lsp::CompletionTextEdit::Edit(edit) => edit,
lsp::CompletionTextEdit::InsertAndReplace(item) => {
unimplemented!("completion: insert_and_replace {:?}", item)
}
};

let triger_relative = |pos| {
let pos = util::lsp_pos_to_pos(doc.text(), pos, offset_encoding)?;

let relative = if pos >= cursor {
(pos - cursor) as isize
} else {
-((cursor - pos) as isize)
};
Some(relative)
};

let start = triger_relative(edit.range.start)?;
let end = triger_relative(edit.range.end)?;

CompleteAction {
start,
end,
text: edit.new_text.clone(),
}
} else {
let text = item.insert_text.as_ref().unwrap_or(&item.label);
// Some LSPs just give you an insertText with no offset ¯\_(ツ)_/¯
// in these cases we need to check for a common prefix and remove it
let prefix = Cow::from(doc.text().slice(start_offset..trigger_offset));
let text = text.trim_start_matches::<&str>(&prefix).to_owned();
CompleteAction {
start: 0,
end: 0,
text,
}
};

Some(action)
}
matszczygiel marked this conversation as resolved.
Show resolved Hide resolved

let (view, doc) = current!(editor);
Expand All @@ -123,7 +182,9 @@ impl Completion {
doc.restore(view.id);

match event {
PromptEvent::Abort => {}
PromptEvent::Abort => {
editor.last_completion = None;
}
PromptEvent::Update => {
// always present here
let item = item.unwrap();
Expand All @@ -135,11 +196,25 @@ impl Completion {
start_offset,
trigger_offset,
);
let completion = fetch_complete_action(
doc,
item,
offset_encoding,
start_offset,
trigger_offset,
view.id,
);

// initialize a savepoint
doc.savepoint();

doc.apply(&transaction, view.id);

if let Some(completion) = completion {
editor.last_completion = Some(completion);
} else {
log::warn!("Failed to generate completion action");
}
}
PromptEvent::Validate => {
// always present here
Expand All @@ -152,8 +227,23 @@ impl Completion {
start_offset,
trigger_offset,
);
let completion = fetch_complete_action(
doc,
item,
offset_encoding,
start_offset,
trigger_offset,
view.id,
);

doc.apply(&transaction, view.id);

if let Some(completion) = completion {
editor.last_completion = Some(completion);
} else {
log::warn!("Failed to generate completion action");
}

// apply additional edits, mostly used to auto import unqualified types
let resolved_additional_text_edits = if item.additional_text_edits.is_some() {
None
Expand Down
65 changes: 57 additions & 8 deletions helix-term/src/ui/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ use helix_core::{
syntax::{self, HighlightEvent},
unicode::segmentation::UnicodeSegmentation,
unicode::width::UnicodeWidthStr,
LineEnding, Position, Range, Selection,
LineEnding, Position, Range, Selection, Transaction,
};
use helix_view::{
document::{Mode, SCRATCH_BUFFER_NAME},
editor::CursorShapeConfig,
editor::{CompleteAction, CursorShapeConfig},
graphics::{CursorKind, Modifier, Rect, Style},
info::Info,
input::KeyEvent,
Expand All @@ -34,12 +34,18 @@ use tui::buffer::Buffer as Surface;
pub struct EditorView {
keymaps: Keymaps,
on_next_key: Option<Box<dyn FnOnce(&mut commands::Context, KeyEvent)>>,
last_insert: (commands::MappableCommand, Vec<KeyEvent>),
last_insert: (commands::MappableCommand, Vec<InsertEvent>),
pub(crate) completion: Option<Completion>,
spinners: ProgressSpinners,
autoinfo: Option<Info>,
}

#[derive(Debug, Clone)]
archseer marked this conversation as resolved.
Show resolved Hide resolved
pub enum InsertEvent {
Key(KeyEvent),
Completion(CompleteAction),
}

impl Default for EditorView {
fn default() -> Self {
Self::new(Keymaps::default())
Expand Down Expand Up @@ -735,8 +741,41 @@ impl EditorView {
// first execute whatever put us into insert mode
self.last_insert.0.execute(cxt);
// then replay the inputs
for &key in &self.last_insert.1.clone() {
self.insert_mode(cxt, key)
for key in self.last_insert.1.clone() {
match key {
InsertEvent::Key(key) => self.insert_mode(cxt, key),
InsertEvent::Completion(compl) => {
let (view, doc) = current!(cxt.editor);

let text = doc.text().slice(..);
let cursor = doc.selection(view.id).primary().cursor(text);

let cursor_offset = |offset: isize| {
if offset >= 0 {
Some(cursor + (offset as usize))
} else {
let minus_offset = (-offset) as usize;
if cursor >= minus_offset {
Some(cursor - minus_offset)
} else {
None
}
}
};

if let (Some(start), Some(end)) =
(cursor_offset(compl.start), cursor_offset(compl.end))
{
let tr = Transaction::change(
doc.text(),
[(start, end, Some(compl.text.into()))].into_iter(),
matszczygiel marked this conversation as resolved.
Show resolved Hide resolved
);
doc.apply(&tr, view.id);
} else {
log::warn!("Skipping completion replay: {compl:?}");
}
}
}
}
}
_ => {
Expand Down Expand Up @@ -777,6 +816,8 @@ impl EditorView {
// Immediately initialize a savepoint
doc_mut!(editor).savepoint();

editor.last_completion = None;

// TODO : propagate required size on resize to completion too
completion.required_size((size.width, size.height));
self.completion = Some(completion);
Expand Down Expand Up @@ -981,9 +1022,6 @@ impl Component for EditorView {
} else {
match mode {
Mode::Insert => {
// record last_insert key
self.last_insert.1.push(key);

// let completion swallow the event if necessary
let mut consumed = false;
if let Some(completion) = &mut self.completion {
Expand All @@ -999,6 +1037,10 @@ impl Component for EditorView {
consumed = true;

if callback.is_some() {
if let Some(compl) = cx.editor.last_completion.take() {
self.last_insert.1.push(InsertEvent::Completion(compl));
}

// assume close_fn
self.clear_completion(cx.editor);
}
Expand All @@ -1007,8 +1049,15 @@ impl Component for EditorView {

// if completion didn't take the event, we pass it onto commands
if !consumed {
if let Some(compl) = cx.editor.last_completion.take() {
self.last_insert.1.push(InsertEvent::Completion(compl));
}

self.insert_mode(&mut cx, key);

// record last_insert key
self.last_insert.1.push(InsertEvent::Key(key));

// lastly we recalculate completion
if let Some(completion) = &mut self.completion {
completion.update(&mut cx);
Expand Down
13 changes: 13 additions & 0 deletions helix-view/src/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -249,9 +249,21 @@ pub struct Editor {
pub idle_timer: Pin<Box<Sleep>>,
pub last_motion: Option<Motion>,

pub last_completion: Option<CompleteAction>,

pub exit_code: i32,
}

#[derive(Default, Debug, Clone)]
matszczygiel marked this conversation as resolved.
Show resolved Hide resolved
pub struct CompleteAction {
// Relative to cursor position
pub start: isize,
// Relative to cursor position
pub end: isize,

pub text: String,
}

#[derive(Debug, Copy, Clone)]
pub enum Action {
Load,
Expand Down Expand Up @@ -288,6 +300,7 @@ impl Editor {
status_msg: None,
idle_timer: Box::pin(sleep(config.idle_timeout)),
last_motion: None,
last_completion: None,
config,
exit_code: 0,
}
Expand Down