use crate::editor::Action;
use crate::Editor;
use crate::{DocumentId, ViewId};
use helix_core::Uri;
use helix_lsp::util::generate_transaction_from_edits;
use helix_lsp::{lsp, OffsetEncoding};
pub enum CompletionEvent {
/// Auto completion was triggered by typing a word char
AutoTrigger {
cursor: usize,
doc: DocumentId,
view: ViewId,
},
/// Auto completion was triggered by typing a trigger char
/// specified by the LSP
TriggerChar {
cursor: usize,
doc: DocumentId,
view: ViewId,
},
/// A completion was manually requested (c-x)
ManualTrigger {
cursor: usize,
doc: DocumentId,
view: ViewId,
},
/// Some text was deleted and the cursor is now at `pos`
DeleteText { cursor: usize },
/// Invalidate the current auto completion trigger
Cancel,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum SignatureHelpInvoked {
Automatic,
Manual,
}
pub enum SignatureHelpEvent {
Invoked,
Trigger,
ReTrigger,
Cancel,
RequestComplete { open: bool },
}
#[derive(Debug)]
pub struct ApplyEditError {
pub kind: ApplyEditErrorKind,
pub failed_change_idx: usize,
}
#[derive(Debug)]
pub enum ApplyEditErrorKind {
DocumentChanged,
FileNotFound,
InvalidUrl(helix_core::uri::UrlConversionError),
IoError(std::io::Error),
// TODO: check edits before applying and propagate failure
// InvalidEdit,
}
impl From<std::io::Error> for ApplyEditErrorKind {
fn from(err: std::io::Error) -> Self {
ApplyEditErrorKind::IoError(err)
}
}
impl From<helix_core::uri::UrlConversionError> for ApplyEditErrorKind {
fn from(err: helix_core::uri::UrlConversionError) -> Self {
ApplyEditErrorKind::InvalidUrl(err)
}
}
impl ToString for ApplyEditErrorKind {
fn to_string(&self) -> String {
match self {
ApplyEditErrorKind::DocumentChanged => "document has changed".to_string(),
ApplyEditErrorKind::FileNotFound => "file not found".to_string(),
ApplyEditErrorKind::InvalidUrl(err) => err.to_string(),
ApplyEditErrorKind::IoError(err) => err.to_string(),
}
}
}
impl Editor {
fn apply_text_edits(
&mut self,
url: &helix_lsp::Url,
version: Option<i32>,
text_edits: Vec<lsp::TextEdit>,
offset_encoding: OffsetEncoding,
) -> Result<(), ApplyEditErrorKind> {
let uri = match Uri::try_from(url) {
Ok(uri) => uri,
Err(err) => {
log::error!("{err}");
return Err(err.into());
}
};
let path = uri.as_path().expect("URIs are valid paths");
let doc_id = match self.open(path, Action::Load) {
Ok(doc_id) => doc_id,
Err(err) => {
let err = format!(
"failed to open document: {}: {}",
path.to_string_lossy(),
err
);
log::error!("{}", err);
self.set_error(err);
return Err(ApplyEditErrorKind::FileNotFound);
}
};
let doc = doc_mut!(self, &doc_id);
if let Some(version) = version {
if version != doc.version() {
let err = format!("outdated workspace edit for {path:?}");
log::error!("{err}, expected {} but got {version}", doc.version());
self.set_error(err);
return Err(ApplyEditErrorKind::DocumentChanged);
}
}
// Need to determine a view for apply/append_changes_to_history
let view_id = self.get_synced_view_id(doc_id);
let doc = doc_mut!(self, &doc_id);
let transaction = generate_transaction_from_edits(doc.text(), text_edits, offset_encoding);
let view = view_mut!(self, view_id);
doc.apply(&transaction, view.id);
doc.append_changes_to_history(view);
Ok(())
}
// TODO make this transactional (and set failureMode to transactional)
pub fn apply_workspace_edit(
&mut self,
offset_encoding: OffsetEncoding,
workspace_edit: &lsp::WorkspaceEdit,
) -> Result<(), ApplyEditError> {
if let Some(ref document_changes) = workspace_edit.document_changes {
match document_changes {
lsp::DocumentChanges::Edits(document_edits) => {
for (i, document_edit) in document_edits.iter().enumerate() {
let edits = document_edit
.edits
.iter()
.map(|edit| match edit {
lsp::OneOf::Left(text_edit) => text_edit,
lsp::OneOf::Right(annotated_text_edit) => {
&annotated_text_edit.text_edit
}
use helix_core::{coords_at_pos, encoding, Position};
use helix_lsp::lsp::DiagnosticSeverity;
use helix_view::{
document::{Mode, SCRATCH_BUFFER_NAME},
graphics::Rect,
theme::Style,
Document, Editor, View,
};
use crate::ui::ProgressSpinners;
use helix_view::editor::StatusLineElement as StatusLineElementID;
use tui::buffer::Buffer as Surface;
use tui::text::{Span, Spans};
pub struct RenderContext<'a> {
pub editor: &'a Editor,
pub doc: &'a Document,
pub view: &'a View,
pub focused: bool,
pub spinners: &'a ProgressSpinners,
pub parts: RenderBuffer<'a>,
}
impl<'a> RenderContext<'a> {
pub fn new(
editor: &'a Editor,
doc: &'a Document,
view: &'a View,
focused: bool,
spinners: &'a ProgressSpinners,
) -> Self {
RenderContext {
editor,
doc,
view,
focused,
spinners,
parts: RenderBuffer::default(),
}
}
}
#[derive(Default)]
pub struct RenderBuffer<'a> {
pub left: Spans<'a>,
pub center: Spans<'a>,
pub right: Spans<'a>,
}
pub fn render(context: &mut RenderContext, viewport: Rect, surface: &mut Surface) {
let base_style = if context.focused {
context.editor.theme.get("ui.statusline")
} else {
context.editor.theme.get("ui.statusline.inactive")
};
surface.set_style(viewport.with_height(1), base_style);
let write_left = |context: &mut RenderContext, text, style| {
append(&mut context.parts.left, text, &base_style, style)
};
let write_center = |context: &mut RenderContext, text, style| {
append(&mut context.parts.center, text, &base_style, style)
};
let write_right = |context: &mut RenderContext, text, style| {
append(&mut context.parts.right, text, &base_style, style)
};
// Left side of the status line.
let config = context.editor.config();
let element_ids = &config.statusline.left;
element_ids
.iter()
.map(|element_id| get_render_function(*element_id))
.for_each(|render| render(context, write_left));
surface.set_spans(
viewport.x,
viewport.y,
&context.parts.left,
context.parts.left.width() as u16,
);
// Right side of the status line.
let element_ids = &config.statusline.right;
element_ids
.iter()
.map(|element_id| get_render_function(*element_id))
.for_each(|render| render(context, write_right));
surface.set_spans(
viewport.x
+ viewport
.width
.saturating_sub(context.parts.right.width() as u16),
viewport.y,
&context.parts.right,
context.parts.right.width() as u16,
);
// Center of the status line.
let element_ids = &config.statusline.center;
element_ids
.iter()
.map(|element_id| get_render_function(*element_id))
.for_each(|render| render(context, write_center));
// Width of the empty space between the left and center area and between the center and right area.
let spacing = 1u16;
let edge_width = context.parts.left.width().max(context.parts.right.width()) as u16;
let center_max_width = viewport.width.saturating_sub(2 * edge_width + 2 * spacing);
let center_width = center_max_width.min(context.parts.center.width() as u16);
surface.set_spans(
viewport.x + viewport.width / 2 - center_width / 2,
viewport.y,
&context.parts.center,
center_width,
);
}
fn append(buffer: &mut Spans, text: String, base_style: &Style, style: Option<Style>) {
buffer.0.push(Span::styled(
text,
style.map_or(*base_style, |s| (*base_style).patch(s)),
));
}
fn get_render_function<F>(element_id: StatusLineElementID) -> impl Fn(&mut RenderContext, F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
match element_id {
helix_view::editor::StatusLineElement::Mode => render_mode,
helix_view::editor::StatusLineElement::Spinner => render_lsp_spinner,
helix_view::editor::StatusLineElement::FileName => render_file_name,
helix_view::editor::StatusLineElement::FileEncoding => render_file_encoding,
helix_view::editor::StatusLineElement::FileLineEnding => render_file_line_ending,
helix_view::editor::StatusLineElement::FileType => render_file_type,
helix_view::editor::StatusLineElement::Diagnostics => render_diagnostics,
helix_view::editor::StatusLineElement::WorkspaceDiagnostics => render_workspace_diagnostics,
helix_view::editor::StatusLineElement::Selections => render_selections,
helix_view::editor::StatusLineElement::PrimarySelectionLength => {
render_primary_selection_length
}
helix_view::editor::StatusLineElement::Position => render_position,
helix_view::editor::StatusLineElement::PositionPercentage => render_position_percentage,
helix_view::editor::StatusLineElement::TotalLineNumbers => render_total_line_numbers,
helix_view::editor::StatusLineElement::Separator => render_separator,
helix_view::editor::StatusLineElement::Spacer => render_spacer,
}
}
fn render_mode<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
let visible = context.focused;
let config = context.editor.config();
let modenames = &config.statusline.mode;
write(
context,
format!(
" {} ",
if visible {
match context.editor.mode() {
Mode::Insert => &modenames.insert,
Mode