mirror of https://github.com/helix-editor/helix
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1152 lines
40 KiB
Rust
1152 lines
40 KiB
Rust
use crate::{
|
|
align_view,
|
|
document::{DocumentSavedEventFuture, DocumentSavedEventResult, Mode, SavePoint},
|
|
graphics::{CursorKind, Rect},
|
|
info::Info,
|
|
input::KeyEvent,
|
|
register::Registers,
|
|
theme::{self, Theme},
|
|
tree::{self, Tree},
|
|
view::ViewPosition,
|
|
Align, Document, DocumentId, View, ViewId,
|
|
};
|
|
use dap::StackFrame;
|
|
use helix_vcs::DiffProviderRegistry;
|
|
|
|
use futures_util::stream::select_all::SelectAll;
|
|
use futures_util::{future, StreamExt};
|
|
use helix_lsp::Call;
|
|
use tokio_stream::wrappers::UnboundedReceiverStream;
|
|
|
|
use std::{
|
|
borrow::Cow,
|
|
cell::Cell,
|
|
collections::{BTreeMap, HashMap},
|
|
io::stdin,
|
|
num::NonZeroUsize,
|
|
path::{Path, PathBuf},
|
|
pin::Pin,
|
|
sync::Arc,
|
|
};
|
|
|
|
use tokio::{
|
|
sync::{
|
|
mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender},
|
|
oneshot,
|
|
},
|
|
time::{sleep, Duration, Instant, Sleep},
|
|
};
|
|
|
|
use anyhow::{anyhow, bail, Error};
|
|
|
|
pub use helix_core::diagnostic::Severity;
|
|
use helix_core::{
|
|
syntax::{self, LanguageServerFeature},
|
|
Change, Position, Selection,
|
|
};
|
|
use helix_dap as dap;
|
|
use helix_lsp::lsp;
|
|
|
|
use arc_swap::access::DynGuard;
|
|
|
|
#[derive(Debug, Clone, Default)]
|
|
pub struct Breakpoint {
|
|
pub id: Option<usize>,
|
|
pub verified: bool,
|
|
pub message: Option<String>,
|
|
|
|
pub line: usize,
|
|
pub column: Option<usize>,
|
|
pub condition: Option<String>,
|
|
pub hit_condition: Option<String>,
|
|
pub log_message: Option<String>,
|
|
}
|
|
|
|
use futures_util::stream::{Flatten, Once};
|
|
|
|
pub struct Editor {
|
|
/// Current editing mode.
|
|
pub mode: Mode,
|
|
pub tree: Tree,
|
|
pub next_document_id: DocumentId,
|
|
pub documents: BTreeMap<DocumentId, Document>,
|
|
|
|
// We Flatten<> to resolve the inner DocumentSavedEventFuture. For that we need a stream of streams, hence the Once<>.
|
|
// https://stackoverflow.com/a/66875668
|
|
pub saves: HashMap<DocumentId, UnboundedSender<Once<DocumentSavedEventFuture>>>,
|
|
pub save_queue: SelectAll<Flatten<UnboundedReceiverStream<Once<DocumentSavedEventFuture>>>>,
|
|
pub write_count: usize,
|
|
|
|
pub count: Option<std::num::NonZeroUsize>,
|
|
pub selected_register: Option<char>,
|
|
pub registers: Registers,
|
|
pub macro_recording: Option<(char, Vec<KeyEvent>)>,
|
|
pub macro_replaying: Vec<char>,
|
|
pub language_servers: helix_lsp::Registry,
|
|
pub diagnostics: BTreeMap<lsp::Url, Vec<(lsp::Diagnostic, usize)>>,
|
|
pub diff_providers: DiffProviderRegistry,
|
|
|
|
pub debugger: Option<dap::Client>,
|
|
pub debugger_events: SelectAll<UnboundedReceiverStream<dap::Payload>>,
|
|
pub breakpoints: HashMap<PathBuf, Vec<Breakpoint>>,
|
|
|
|
pub syn_loader: Arc<syntax::Loader>,
|
|
pub theme_loader: Arc<theme::Loader>,
|
|
/// last_theme is used for theme previews. We store the current theme here,
|
|
/// and if previewing is cancelled, we can return to it.
|
|
pub last_theme: Option<Theme>,
|
|
/// The currently applied editor theme. While previewing a theme, the previewed theme
|
|
/// is set here.
|
|
pub theme: Theme,
|
|
|
|
/// The primary Selection prior to starting a goto_line_number preview. This is
|
|
/// restored when the preview is aborted, or added to the jumplist when it is
|
|
/// confirmed.
|
|
pub last_selection: Option<Selection>,
|
|
|
|
pub status_msg: Option<(Cow<'static, str>, Severity)>,
|
|
pub autoinfo: Option<Info>,
|
|
|
|
pub config: Arc<dyn DynAccess<Config>>,
|
|
pub auto_pairs: Option<AutoPairs>,
|
|
|
|
pub idle_timer: Pin<Box<Sleep>>,
|
|
redraw_timer: Pin<Box<Sleep>>,
|
|
last_motion: Option<Motion>,
|
|
pub last_completion: Option<CompleteAction>,
|
|
|
|
pub exit_code: i32,
|
|
|
|
pub config_events: (UnboundedSender<ConfigEvent>, UnboundedReceiver<ConfigEvent>),
|
|
pub needs_redraw: bool,
|
|
/// Cached position of the cursor calculated during rendering.
|
|
/// The content of `cursor_cache` is returned by `Editor::cursor` if
|
|
/// set to `Some(_)`. The value will be cleared after it's used.
|
|
/// If `cursor_cache` is `None` then the `Editor::cursor` function will
|
|
/// calculate the cursor position.
|
|
///
|
|
/// `Some(None)` represents a cursor position outside of the visible area.
|
|
/// This will just cause `Editor::cursor` to return `None`.
|
|
///
|
|
/// This cache is only a performance optimization to
|
|
/// avoid calculating the cursor position multiple
|
|
/// times during rendering and should not be set by other functions.
|
|
pub cursor_cache: Cell<Option<Option<Position>>>,
|
|
/// When a new completion request is sent to the server old
|
|
/// unfinished request must be dropped. Each completion
|
|
/// request is associated with a channel that cancels
|
|
/// when the channel is dropped. That channel is stored
|
|
/// here. When a new completion request is sent this
|
|
/// field is set and any old requests are automatically
|
|
/// canceled as a result
|
|
pub completion_request_handle: Option<oneshot::Sender<()>>,
|
|
}
|
|
|
|
pub type Motion = Box<dyn Fn(&mut Editor)>;
|
|
|
|
#[derive(Debug)]
|
|
pub enum EditorEvent {
|
|
DocumentSaved(DocumentSavedEventResult),
|
|
ConfigEvent(ConfigEvent),
|
|
LanguageServerMessage((usize, Call)),
|
|
DebuggerEvent(dap::Payload),
|
|
IdleTimer,
|
|
Redraw,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub enum ConfigEvent {
|
|
Refresh,
|
|
Update(Box<Config>),
|
|
}
|
|
|
|
enum ThemeAction {
|
|
Set,
|
|
Preview,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub enum CompleteAction {
|
|
Applied {
|
|
trigger_offset: usize,
|
|
changes: Vec<Change>,
|
|
},
|
|
/// A savepoint of the currently selected completion. The savepoint
|
|
/// MUST be restored before sending any event to the LSP
|
|
Selected { savepoint: Arc<SavePoint> },
|
|
}
|
|
|
|
#[derive(Debug, Copy, Clone)]
|
|
pub enum Action {
|
|
Load,
|
|
Replace,
|
|
HorizontalSplit,
|
|
VerticalSplit,
|
|
}
|
|
|
|
impl Action {
|
|
/// Whether to align the view to the cursor after executing this action
|
|
pub fn align_view(&self, view: &View, new_doc: DocumentId) -> bool {
|
|
!matches!((self, view.doc == new_doc), (Action::Load, false))
|
|
}
|
|
}
|
|
|
|
/// Error thrown on failed document closed
|
|
pub enum CloseError {
|
|
/// Document doesn't exist
|
|
DoesNotExist,
|
|
/// Buffer is modified
|
|
BufferModified(String),
|
|
/// Document failed to save
|
|
SaveError(anyhow::Error),
|
|
}
|
|
|
|
impl Editor {
|
|
pub fn new(
|
|
mut area: Rect,
|
|
theme_loader: Arc<theme::Loader>,
|
|
syn_loader: Arc<syntax::Loader>,
|
|
config: Arc<dyn DynAccess<Config>>,
|
|
) -> Self {
|
|
let language_servers = helix_lsp::Registry::new(syn_loader.clone());
|
|
let conf = config.load();
|
|
let auto_pairs = (&conf.auto_pairs).into();
|
|
|
|
// HAXX: offset the render area height by 1 to account for prompt/commandline
|
|
area.height -= 1;
|
|
|
|
Self {
|
|
mode: Mode::Normal,
|
|
tree: Tree::new(area),
|
|
next_document_id: DocumentId::default(),
|
|
documents: BTreeMap::new(),
|
|
saves: HashMap::new(),
|
|
save_queue: SelectAll::new(),
|
|
write_count: 0,
|
|
count: None,
|
|
selected_register: None,
|
|
macro_recording: None,
|
|
macro_replaying: Vec::new(),
|
|
theme: theme_loader.default(),
|
|
language_servers,
|
|
diagnostics: BTreeMap::new(),
|
|
diff_providers: DiffProviderRegistry::default(),
|
|
debugger: None,
|
|
debugger_events: SelectAll::new(),
|
|
breakpoints: HashMap::new(),
|
|
syn_loader,
|
|
theme_loader,
|
|
last_theme: None,
|
|
last_selection: None,
|
|
registers: Registers::default(),
|
|
status_msg: None,
|
|
autoinfo: None,
|
|
idle_timer: Box::pin(sleep(conf.idle_timeout)),
|
|
redraw_timer: Box::pin(sleep(Duration::MAX)),
|
|
last_motion: None,
|
|
last_completion: None,
|
|
config,
|
|
auto_pairs,
|
|
exit_code: 0,
|
|
config_events: unbounded_channel(),
|
|
needs_redraw: false,
|
|
cursor_cache: Cell::new(None),
|
|
completion_request_handle: None,
|
|
}
|
|
}
|
|
|
|
pub fn popup_border(&self) -> bool {
|
|
self.config().popup_border == PopupBorderConfig::All
|
|
|| self.config().popup_border == PopupBorderConfig::Popup
|
|
}
|
|
|
|
pub fn menu_border(&self) -> bool {
|
|
self.config().popup_border == PopupBorderConfig::All
|
|
|| self.config().popup_border == PopupBorderConfig::Menu
|
|
}
|
|
|
|
pub fn apply_motion<F: Fn(&mut Self) + 'static>(&mut self, motion: F) {
|
|
motion(self);
|
|
self.last_motion = Some(Box::new(motion));
|
|
}
|
|
|
|
pub fn repeat_last_motion(&mut self, count: usize) {
|
|
if let Some(motion) = self.last_motion.take() {
|
|
for _ in 0..count {
|
|
motion(self);
|
|
}
|
|
self.last_motion = Some(motion);
|
|
}
|
|
}
|
|
/// Current editing mode for the [`Editor`].
|
|
pub fn mode(&self) -> Mode {
|
|
self.mode
|
|
}
|
|
|
|
pub fn config(&self) -> DynGuard<Config> {
|
|
self.config.load()
|
|
}
|
|
|
|
/// Call if the config has changed to let the editor update all
|
|
/// relevant members.
|
|
pub fn refresh_config(&mut self) {
|
|
let config = self.config();
|
|
self.auto_pairs = (&config.auto_pairs).into();
|
|
self.reset_idle_timer();
|
|
self._refresh();
|
|
}
|
|
|
|
pub fn clear_idle_timer(&mut self) {
|
|
// equivalent to internal Instant::far_future() (30 years)
|
|
self.idle_timer
|
|
.as_mut()
|
|
.reset(Instant::now() + Duration::from_secs(86400 * 365 * 30));
|
|
}
|
|
|
|
pub fn reset_idle_timer(&mut self) {
|
|
let config = self.config();
|
|
self.idle_timer
|
|
.as_mut()
|
|
.reset(Instant::now() + config.idle_timeout);
|
|
}
|
|
|
|
pub fn clear_status(&mut self) {
|
|
self.status_msg = None;
|
|
}
|
|
|
|
#[inline]
|
|
pub fn set_status<T: Into<Cow<'static, str>>>(&mut self, status: T) {
|
|
let status = status.into();
|
|
log::debug!("editor status: {}", status);
|
|
self.status_msg = Some((status, Severity::Info));
|
|
}
|
|
|
|
#[inline]
|
|
pub fn set_error<T: Into<Cow<'static, str>>>(&mut self, error: T) {
|
|
let error = error.into();
|
|
log::error!("editor error: {}", error);
|
|
self.status_msg = Some((error, Severity::Error));
|
|
}
|
|
|
|
#[inline]
|
|
pub fn get_status(&self) -> Option<(&Cow<'static, str>, &Severity)> {
|
|
self.status_msg.as_ref().map(|(status, sev)| (status, sev))
|
|
}
|
|
|
|
/// Returns true if the current status is an error
|
|
#[inline]
|
|
pub fn is_err(&self) -> bool {
|
|
self.status_msg
|
|
.as_ref()
|
|
.map(|(_, sev)| *sev == Severity::Error)
|
|
.unwrap_or(false)
|
|
}
|
|
|
|
pub fn unset_theme_preview(&mut self) {
|
|
if let Some(last_theme) = self.last_theme.take() {
|
|
self.set_theme(last_theme);
|
|
}
|
|
// None likely occurs when the user types ":theme" and then exits before previewing
|
|
}
|
|
|
|
pub fn set_theme_preview(&mut self, theme: Theme) {
|
|
self.set_theme_impl(theme, ThemeAction::Preview);
|
|
}
|
|
|
|
pub fn set_theme(&mut self, theme: Theme) {
|
|
self.set_theme_impl(theme, ThemeAction::Set);
|
|
}
|
|
|
|
fn set_theme_impl(&mut self, theme: Theme, preview: ThemeAction) {
|
|
// `ui.selection` is the only scope required to be able to render a theme.
|
|
if theme.find_scope_index_exact("ui.selection").is_none() {
|
|
self.set_error("Invalid theme: `ui.selection` required");
|
|
return;
|
|
}
|
|
|
|
let scopes = theme.scopes();
|
|
self.syn_loader.set_scopes(scopes.to_vec());
|
|
|
|
match preview {
|
|
ThemeAction::Preview => {
|
|
let last_theme = std::mem::replace(&mut self.theme, theme);
|
|
// only insert on first preview: this will be the last theme the user has saved
|
|
self.last_theme.get_or_insert(last_theme);
|
|
}
|
|
ThemeAction::Set => {
|
|
self.last_theme = None;
|
|
self.theme = theme;
|
|
}
|
|
}
|
|
|
|
self._refresh();
|
|
}
|
|
|
|
#[inline]
|
|
pub fn language_server_by_id(&self, language_server_id: usize) -> Option<&helix_lsp::Client> {
|
|
self.language_servers.get_by_id(language_server_id)
|
|
}
|
|
|
|
/// Refreshes the language server for a given document
|
|
pub fn refresh_language_servers(&mut self, doc_id: DocumentId) {
|
|
self.launch_language_servers(doc_id)
|
|
}
|
|
|
|
/// Launch a language server for a given document
|
|
fn launch_language_servers(&mut self, doc_id: DocumentId) {
|
|
if !self.config().lsp.enable {
|
|
return;
|
|
}
|
|
// if doc doesn't have a URL it's a scratch buffer, ignore it
|
|
let Some(doc) = self.documents.get_mut(&doc_id) else {
|
|
return;
|
|
};
|
|
let Some(doc_url) = doc.url() else {
|
|
return;
|
|
};
|
|
let (lang, path) = (doc.language.clone(), doc.path().cloned());
|
|
let config = doc.config.load();
|
|
let root_dirs = &config.workspace_lsp_roots;
|
|
|
|
// store only successfully started language servers
|
|
let language_servers = lang.as_ref().map_or_else(HashMap::default, |language| {
|
|
self.language_servers
|
|
.get(language, path.as_ref(), root_dirs, config.lsp.snippets)
|
|
.filter_map(|(lang, client)| match client {
|
|
Ok(client) => Some((lang, client)),
|
|
Err(err) => {
|
|
log::error!(
|
|
"Failed to initialize the language servers for `{}` {{ {} }}",
|
|
lang,
|
|
err
|
|
);
|
|
None
|
|
}
|
|
})
|
|
.collect::<HashMap<_, _>>()
|
|
});
|
|
|
|
if language_servers.is_empty() {
|
|
return;
|
|
}
|
|
|
|
let language_id = doc.language_id().map(ToOwned::to_owned).unwrap_or_default();
|
|
|
|
// only spawn new language servers if the servers aren't the same
|
|
let doc_language_servers_not_in_registry =
|
|
doc.language_servers.iter().filter(|(name, doc_ls)| {
|
|
language_servers
|
|
.get(*name)
|
|
.map_or(true, |ls| ls.id() != doc_ls.id())
|
|
});
|
|
|
|
for (_, language_server) in doc_language_servers_not_in_registry {
|
|
tokio::spawn(language_server.text_document_did_close(doc.identifier()));
|
|
}
|
|
|
|
let language_servers_not_in_doc = language_servers.iter().filter(|(name, ls)| {
|
|
doc.language_servers
|
|
.get(*name)
|
|
.map_or(true, |doc_ls| ls.id() != doc_ls.id())
|
|
});
|
|
|
|
for (_, language_server) in language_servers_not_in_doc {
|
|
// TODO: this now races with on_init code if the init happens too quickly
|
|
tokio::spawn(language_server.text_document_did_open(
|
|
doc_url.clone(),
|
|
doc.version(),
|
|
doc.text(),
|
|
language_id.clone(),
|
|
));
|
|
}
|
|
|
|
doc.language_servers = language_servers;
|
|
}
|
|
|
|
fn _refresh(&mut self) {
|
|
let config = self.config();
|
|
|
|
// Reset the inlay hints annotations *before* updating the views, that way we ensure they
|
|
// will disappear during the `.sync_change(doc)` call below.
|
|
//
|
|
// We can't simply check this config when rendering because inlay hints are only parts of
|
|
// the possible annotations, and others could still be active, so we need to selectively
|
|
// drop the inlay hints.
|
|
if !config.lsp.display_inlay_hints {
|
|
for doc in self.documents_mut() {
|
|
doc.reset_all_inlay_hints();
|
|
}
|
|
}
|
|
|
|
for (view, _) in self.tree.views_mut() {
|
|
let doc = doc_mut!(self, &view.doc);
|
|
view.sync_changes(doc);
|
|
view.gutters = config.gutters.clone();
|
|
view.ensure_cursor_in_view(doc, config.scrolloff)
|
|
}
|
|
}
|
|
|
|
fn replace_document_in_view(&mut self, current_view: ViewId, doc_id: DocumentId) {
|
|
let view = self.tree.get_mut(current_view);
|
|
view.doc = doc_id;
|
|
view.offset = ViewPosition::default();
|
|
|
|
let doc = doc_mut!(self, &doc_id);
|
|
doc.ensure_view_init(view.id);
|
|
view.sync_changes(doc);
|
|
doc.mark_as_focused();
|
|
|
|
align_view(doc, view, Align::Center);
|
|
}
|
|
|
|
pub fn switch(&mut self, id: DocumentId, action: Action) {
|
|
use crate::tree::Layout;
|
|
|
|
if !self.documents.contains_key(&id) {
|
|
log::error!("cannot switch to document that does not exist (anymore)");
|
|
return;
|
|
}
|
|
|
|
self.enter_normal_mode();
|
|
|
|
match action {
|
|
Action::Replace => {
|
|
let (view, doc) = current_ref!(self);
|
|
// If the current view is an empty scratch buffer and is not displayed in any other views, delete it.
|
|
// Boolean value is determined before the call to `view_mut` because the operation requires a borrow
|
|
// of `self.tree`, which is mutably borrowed when `view_mut` is called.
|
|
let remove_empty_scratch = !doc.is_modified()
|
|
// If the buffer has no path and is not modified, it is an empty scratch buffer.
|
|
&& doc.path().is_none()
|
|
// If the buffer we are changing to is not this buffer
|
|
&& id != doc.id
|
|
// Ensure the buffer is not displayed in any other splits.
|
|
&& !self
|
|
.tree
|
|
.traverse()
|
|
.any(|(_, v)| v.doc == doc.id && v.id != view.id);
|
|
|
|
let (view, doc) = current!(self);
|
|
let view_id = view.id;
|
|
|
|
// Append any outstanding changes to history in the old document.
|
|
doc.append_changes_to_history(view);
|
|
|
|
if remove_empty_scratch {
|
|
// Copy `doc.id` into a variable before calling `self.documents.remove`, which requires a mutable
|
|
// borrow, invalidating direct access to `doc.id`.
|
|
let id = doc.id;
|
|
self.documents.remove(&id);
|
|
|
|
// Remove the scratch buffer from any jumplists
|
|
for (view, _) in self.tree.views_mut() {
|
|
view.remove_document(&id);
|
|
}
|
|
} else {
|
|
let jump = (view.doc, doc.selection(view.id).clone());
|
|
view.jumps.push(jump);
|
|
// Set last accessed doc if it is a different document
|
|
if doc.id != id {
|
|
view.add_to_history(view.doc);
|
|
// Set last modified doc if modified and last modified doc is different
|
|
if std::mem::take(&mut doc.modified_since_accessed)
|
|
&& view.last_modified_docs[0] != Some(view.doc)
|
|
{
|
|
view.last_modified_docs = [Some(view.doc), view.last_modified_docs[0]];
|
|
}
|
|
}
|
|
}
|
|
|
|
self.replace_document_in_view(view_id, id);
|
|
|
|
return;
|
|
}
|
|
Action::Load => {
|
|
let view_id = view!(self).id;
|
|
let doc = doc_mut!(self, &id);
|
|
doc.ensure_view_init(view_id);
|
|
doc.mark_as_focused();
|
|
return;
|
|
}
|
|
Action::HorizontalSplit | Action::VerticalSplit => {
|
|
// copy the current view, unless there is no view yet
|
|
let view = self
|
|
.tree
|
|
.try_get(self.tree.focus)
|
|
.filter(|v| id == v.doc) // Different Document
|
|
.cloned()
|
|
.unwrap_or_else(|| View::new(id, self.config().gutters.clone()));
|
|
let view_id = self.tree.split(
|
|
view,
|
|
match action {
|
|
Action::HorizontalSplit => Layout::Horizontal,
|
|
Action::VerticalSplit => Layout::Vertical,
|
|
_ => unreachable!(),
|
|
},
|
|
);
|
|
// initialize selection for view
|
|
let doc = doc_mut!(self, &id);
|
|
doc.ensure_view_init(view_id);
|
|
doc.mark_as_focused();
|
|
}
|
|
}
|
|
|
|
self._refresh();
|
|
}
|
|
|
|
/// Generate an id for a new document and register it.
|
|
fn new_document(&mut self, mut doc: Document) -> DocumentId {
|
|
let id = self.next_document_id;
|
|
// Safety: adding 1 from 1 is fine, probably impossible to reach usize max
|
|
self.next_document_id =
|
|
DocumentId(unsafe { NonZeroUsize::new_unchecked(self.next_document_id.0.get() + 1) });
|
|
doc.id = id;
|
|
self.documents.insert(id, doc);
|
|
|
|
let (save_sender, save_receiver) = tokio::sync::mpsc::unbounded_channel();
|
|
self.saves.insert(id, save_sender);
|
|
|
|
let stream = UnboundedReceiverStream::new(save_receiver).flatten();
|
|
self.save_queue.push(stream);
|
|
|
|
id
|
|
}
|
|
|
|
fn new_file_from_document(&mut self, action: Action, doc: Document) -> DocumentId {
|
|
let id = self.new_document(doc);
|
|
self.switch(id, action);
|
|
id
|
|
}
|
|
|
|
pub fn new_file(&mut self, action: Action) -> DocumentId {
|
|
self.new_file_from_document(action, Document::default(self.config.clone()))
|
|
}
|
|
|
|
pub fn new_file_from_stdin(&mut self, action: Action) -> Result<DocumentId, Error> {
|
|
let (stdin, encoding, has_bom) = crate::document::read_to_string(&mut stdin(), None)?;
|
|
let doc = Document::from(
|
|
helix_core::Rope::default(),
|
|
Some((encoding, has_bom)),
|
|
self.config.clone(),
|
|
);
|
|
let doc_id = self.new_file_from_document(action, doc);
|
|
let doc = doc_mut!(self, &doc_id);
|
|
let view = view_mut!(self);
|
|
doc.ensure_view_init(view.id);
|
|
let transaction =
|
|
helix_core::Transaction::insert(doc.text(), doc.selection(view.id), stdin.into())
|
|
.with_selection(Selection::point(0));
|
|
doc.apply(&transaction, view.id);
|
|
doc.append_changes_to_history(view);
|
|
Ok(doc_id)
|
|
}
|
|
|
|
// ??? possible use for integration tests
|
|
pub fn open(&mut self, path: &Path, action: Action) -> Result<DocumentId, Error> {
|
|
let path = helix_core::path::get_canonicalized_path(path);
|
|
let id = self.document_by_path(&path).map(|doc| doc.id);
|
|
|
|
let id = if let Some(id) = id {
|
|
id
|
|
} else {
|
|
let mut doc = Document::open(
|
|
&path,
|
|
None,
|
|
Some(self.syn_loader.clone()),
|
|
self.config.clone(),
|
|
)?;
|
|
|
|
let diagnostics =
|
|
Editor::doc_diagnostics(&self.language_servers, &self.diagnostics, &doc);
|
|
doc.replace_diagnostics(diagnostics, &[], None);
|
|
|
|
if let Some(diff_base) = self.diff_providers.get_diff_base(&path) {
|
|
doc.set_diff_base(diff_base);
|
|
}
|
|
doc.set_version_control_head(self.diff_providers.get_current_head_name(&path));
|
|
|
|
let id = self.new_document(doc);
|
|
self.launch_language_servers(id);
|
|
|
|
id
|
|
};
|
|
|
|
self.switch(id, action);
|
|
Ok(id)
|
|
}
|
|
|
|
pub fn close(&mut self, id: ViewId) {
|
|
// Remove selections for the closed view on all documents.
|
|
for doc in self.documents_mut() {
|
|
doc.remove_view(id);
|
|
}
|
|
self.tree.remove(id);
|
|
self._refresh();
|
|
}
|
|
|
|
pub fn close_document(&mut self, doc_id: DocumentId, force: bool) -> Result<(), CloseError> {
|
|
let doc = match self.documents.get_mut(&doc_id) {
|
|
Some(doc) => doc,
|
|
None => return Err(CloseError::DoesNotExist),
|
|
};
|
|
if !force && doc.is_modified() {
|
|
return Err(CloseError::BufferModified(doc.display_name().into_owned()));
|
|
}
|
|
|
|
// This will also disallow any follow-up writes
|
|
self.saves.remove(&doc_id);
|
|
|
|
for language_server in doc.language_servers() {
|
|
// TODO: track error
|
|
tokio::spawn(language_server.text_document_did_close(doc.identifier()));
|
|
}
|
|
|
|
enum Action {
|
|
Close(ViewId),
|
|
ReplaceDoc(ViewId, DocumentId),
|
|
}
|
|
|
|
let actions: Vec<Action> = self
|
|
.tree
|
|
.views_mut()
|
|
.filter_map(|(view, _focus)| {
|
|
view.remove_document(&doc_id);
|
|
|
|
if view.doc == doc_id {
|
|
// something was previously open in the view, switch to previous doc
|
|
if let Some(prev_doc) = view.docs_access_history.pop() {
|
|
Some(Action::ReplaceDoc(view.id, prev_doc))
|
|
} else {
|
|
// only the document that is being closed was in the view, close it
|
|
Some(Action::Close(view.id))
|
|
}
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.collect();
|
|
|
|
for action in actions {
|
|
match action {
|
|
Action::Close(view_id) => {
|
|
self.close(view_id);
|
|
}
|
|
Action::ReplaceDoc(view_id, doc_id) => {
|
|
self.replace_document_in_view(view_id, doc_id);
|
|
}
|
|
}
|
|
}
|
|
|
|
self.documents.remove(&doc_id);
|
|
|
|
// If the document we removed was visible in all views, we will have no more views. We don't
|
|
// want to close the editor just for a simple buffer close, so we need to create a new view
|
|
// containing either an existing document, or a brand new document.
|
|
if self.tree.views().next().is_none() {
|
|
let doc_id = self
|
|
.documents
|
|
.iter()
|
|
.map(|(&doc_id, _)| doc_id)
|
|
.next()
|
|
.unwrap_or_else(|| self.new_document(Document::default(self.config.clone())));
|
|
let view = View::new(doc_id, self.config().gutters.clone());
|
|
let view_id = self.tree.insert(view);
|
|
let doc = doc_mut!(self, &doc_id);
|
|
doc.ensure_view_init(view_id);
|
|
doc.mark_as_focused();
|
|
}
|
|
|
|
self._refresh();
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn save<P: Into<PathBuf>>(
|
|
&mut self,
|
|
doc_id: DocumentId,
|
|
path: Option<P>,
|
|
force: bool,
|
|
) -> anyhow::Result<()> {
|
|
// convert a channel of futures to pipe into main queue one by one
|
|
// via stream.then() ? then push into main future
|
|
|
|
let path = path.map(|path| path.into());
|
|
let doc = doc_mut!(self, &doc_id);
|
|
let doc_save_future = doc.save(path, force)?;
|
|
|
|
// When a file is written to, notify the file event handler.
|
|
// Note: This can be removed once proper file watching is implemented.
|
|
let handler = self.language_servers.file_event_handler.clone();
|
|
let future = async move {
|
|
let res = doc_save_future.await;
|
|
if let Ok(event) = &res {
|
|
handler.file_changed(event.path.clone());
|
|
}
|
|
res
|
|
};
|
|
|
|
use futures_util::stream;
|
|
|
|
self.saves
|
|
.get(&doc_id)
|
|
.ok_or_else(|| anyhow::format_err!("saves are closed for this document!"))?
|
|
.send(stream::once(Box::pin(future)))
|
|
.map_err(|err| anyhow!("failed to send save event: {}", err))?;
|
|
|
|
self.write_count += 1;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn resize(&mut self, area: Rect) {
|
|
if self.tree.resize(area) {
|
|
self._refresh();
|
|
};
|
|
}
|
|
|
|
pub fn focus(&mut self, view_id: ViewId) {
|
|
let prev_id = std::mem::replace(&mut self.tree.focus, view_id);
|
|
|
|
// if leaving the view: mode should reset and the cursor should be
|
|
// within view
|
|
if prev_id != view_id {
|
|
self.enter_normal_mode();
|
|
self.ensure_cursor_in_view(view_id);
|
|
|
|
// Update jumplist selections with new document changes.
|
|
for (view, _focused) in self.tree.views_mut() {
|
|
let doc = doc_mut!(self, &view.doc);
|
|
view.sync_changes(doc);
|
|
}
|
|
}
|
|
|
|
let view = view!(self, view_id);
|
|
let doc = doc_mut!(self, &view.doc);
|
|
doc.mark_as_focused();
|
|
}
|
|
|
|
pub fn focus_next(&mut self) {
|
|
self.focus(self.tree.next());
|
|
}
|
|
|
|
pub fn focus_prev(&mut self) {
|
|
self.focus(self.tree.prev());
|
|
}
|
|
|
|
pub fn focus_direction(&mut self, direction: tree::Direction) {
|
|
let current_view = self.tree.focus;
|
|
if let Some(id) = self.tree.find_split_in_direction(current_view, direction) {
|
|
self.focus(id)
|
|
}
|
|
}
|
|
|
|
pub fn swap_split_in_direction(&mut self, direction: tree::Direction) {
|
|
self.tree.swap_split_in_direction(direction);
|
|
}
|
|
|
|
pub fn transpose_view(&mut self) {
|
|
self.tree.transpose();
|
|
}
|
|
|
|
pub fn should_close(&self) -> bool {
|
|
self.tree.is_empty()
|
|
}
|
|
|
|
pub fn ensure_cursor_in_view(&mut self, id: ViewId) {
|
|
let config = self.config();
|
|
let view = self.tree.get_mut(id);
|
|
let doc = &self.documents[&view.doc];
|
|
view.ensure_cursor_in_view(doc, config.scrolloff)
|
|
}
|
|
|
|
#[inline]
|
|
pub fn document(&self, id: DocumentId) -> Option<&Document> {
|
|
self.documents.get(&id)
|
|
}
|
|
|
|
#[inline]
|
|
pub fn document_mut(&mut self, id: DocumentId) -> Option<&mut Document> {
|
|
self.documents.get_mut(&id)
|
|
}
|
|
|
|
#[inline]
|
|
pub fn documents(&self) -> impl Iterator<Item = &Document> {
|
|
self.documents.values()
|
|
}
|
|
|
|
#[inline]
|
|
pub fn documents_mut(&mut self) -> impl Iterator<Item = &mut Document> {
|
|
self.documents.values_mut()
|
|
}
|
|
|
|
pub fn document_by_path<P: AsRef<Path>>(&self, path: P) -> Option<&Document> {
|
|
self.documents()
|
|
.find(|doc| doc.path().map(|p| p == path.as_ref()).unwrap_or(false))
|
|
}
|
|
|
|
pub fn document_by_path_mut<P: AsRef<Path>>(&mut self, path: P) -> Option<&mut Document> {
|
|
self.documents_mut()
|
|
.find(|doc| doc.path().map(|p| p == path.as_ref()).unwrap_or(false))
|
|
}
|
|
|
|
/// Returns all supported diagnostics for the document
|
|
pub fn doc_diagnostics<'a>(
|
|
language_servers: &'a helix_lsp::Registry,
|
|
diagnostics: &'a BTreeMap<lsp::Url, Vec<(lsp::Diagnostic, usize)>>,
|
|
document: &Document,
|
|
) -> impl Iterator<Item = helix_core::Diagnostic> + 'a {
|
|
Editor::doc_diagnostics_with_filter(language_servers, diagnostics, document, |_, _| true)
|
|
}
|
|
|
|
/// Returns all supported diagnostics for the document
|
|
/// filtered by `filter` which is invocated with the raw `lsp::Diagnostic` and the language server id it came from
|
|
pub fn doc_diagnostics_with_filter<'a>(
|
|
language_servers: &'a helix_lsp::Registry,
|
|
diagnostics: &'a BTreeMap<lsp::Url, Vec<(lsp::Diagnostic, usize)>>,
|
|
|
|
document: &Document,
|
|
filter: impl Fn(&lsp::Diagnostic, usize) -> bool + 'a,
|
|
) -> impl Iterator<Item = helix_core::Diagnostic> + 'a {
|
|
let text = document.text().clone();
|
|
let language_config = document.language.clone();
|
|
document
|
|
.path()
|
|
.and_then(|path| url::Url::from_file_path(path).ok()) // TODO log error?
|
|
.and_then(|uri| diagnostics.get(&uri))
|
|
.map(|diags| {
|
|
diags.iter().filter_map(move |(diagnostic, lsp_id)| {
|
|
let ls = language_servers.get_by_id(*lsp_id)?;
|
|
language_config
|
|
.as_ref()
|
|
.and_then(|c| {
|
|
c.language_servers.iter().find(|features| {
|
|
features.name == ls.name()
|
|
&& features.has_feature(LanguageServerFeature::Diagnostics)
|
|
})
|
|
})
|
|
.and_then(|_| {
|
|
if filter(diagnostic, *lsp_id) {
|
|
Document::lsp_diagnostic_to_diagnostic(
|
|
&text,
|
|
language_config.as_deref(),
|
|
diagnostic,
|
|
*lsp_id,
|
|
ls.offset_encoding(),
|
|
)
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
})
|
|
})
|
|
.into_iter()
|
|
.flatten()
|
|
}
|
|
|
|
/// Gets the primary cursor position in screen coordinates,
|
|
/// or `None` if the primary cursor is not visible on screen.
|
|
pub fn cursor(&self) -> (Option<Position>, CursorKind) {
|
|
let config = self.config();
|
|
let (view, doc) = current_ref!(self);
|
|
let cursor = doc
|
|
.selection(view.id)
|
|
.primary()
|
|
.cursor(doc.text().slice(..));
|
|
let pos = self
|
|
.cursor_cache
|
|
.get()
|
|
.unwrap_or_else(|| view.screen_coords_at_pos(doc, doc.text().slice(..), cursor));
|
|
if let Some(mut pos) = pos {
|
|
let inner = view.inner_area(doc);
|
|
pos.col += inner.x as usize;
|
|
pos.row += inner.y as usize;
|
|
let cursorkind = config.cursor_shape.from_mode(self.mode);
|
|
(Some(pos), cursorkind)
|
|
} else {
|
|
(None, CursorKind::default())
|
|
}
|
|
}
|
|
|
|
/// Closes language servers with timeout. The default timeout is 10000 ms, use
|
|
/// `timeout` parameter to override this.
|
|
pub async fn close_language_servers(
|
|
&self,
|
|
timeout: Option<u64>,
|
|
) -> Result<(), tokio::time::error::Elapsed> {
|
|
// Remove all language servers from the file event handler.
|
|
// Note: this is non-blocking.
|
|
for client in self.language_servers.iter_clients() {
|
|
self.language_servers
|
|
.file_event_handler
|
|
.remove_client(client.id());
|
|
}
|
|
|
|
tokio::time::timeout(
|
|
Duration::from_millis(timeout.unwrap_or(3000)),
|
|
future::join_all(
|
|
self.language_servers
|
|
.iter_clients()
|
|
.map(|client| client.force_shutdown()),
|
|
),
|
|
)
|
|
.await
|
|
.map(|_| ())
|
|
}
|
|
|
|
pub async fn wait_event(&mut self) -> EditorEvent {
|
|
// the loop only runs once or twice and would be better implemented with a recursion + const generic
|
|
// however due to limitations with async functions that can not be implemented right now
|
|
loop {
|
|
tokio::select! {
|
|
biased;
|
|
|
|
Some(event) = self.save_queue.next() => {
|
|
self.write_count -= 1;
|
|
return EditorEvent::DocumentSaved(event)
|
|
}
|
|
Some(config_event) = self.config_events.1.recv() => {
|
|
return EditorEvent::ConfigEvent(config_event)
|
|
}
|
|
Some(message) = self.language_servers.incoming.next() => {
|
|
return EditorEvent::LanguageServerMessage(message)
|
|
}
|
|
Some(event) = self.debugger_events.next() => {
|
|
return EditorEvent::DebuggerEvent(event)
|
|
}
|
|
|
|
_ = helix_event::redraw_requested() => {
|
|
if !self.needs_redraw{
|
|
self.needs_redraw = true;
|
|
let timeout = Instant::now() + Duration::from_millis(33);
|
|
if timeout < self.idle_timer.deadline() && timeout < self.redraw_timer.deadline(){
|
|
self.redraw_timer.as_mut().reset(timeout)
|
|
}
|
|
}
|
|
}
|
|
|
|
_ = &mut self.redraw_timer => {
|
|
self.redraw_timer.as_mut().reset(Instant::now() + Duration::from_secs(86400 * 365 * 30));
|
|
return EditorEvent::Redraw
|
|
}
|
|
_ = &mut self.idle_timer => {
|
|
return EditorEvent::IdleTimer
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn flush_writes(&mut self) -> anyhow::Result<()> {
|
|
while self.write_count > 0 {
|
|
if let Some(save_event) = self.save_queue.next().await {
|
|
self.write_count -= 1;
|
|
|
|
let save_event = match save_event {
|
|
Ok(event) => event,
|
|
Err(err) => {
|
|
self.set_error(err.to_string());
|
|
bail!(err);
|
|
}
|
|
};
|
|
|
|
let doc = doc_mut!(self, &save_event.doc_id);
|
|
doc.set_last_saved_revision(save_event.revision);
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Switches the editor into normal mode.
|
|
pub fn enter_normal_mode(&mut self) {
|
|
use helix_core::{graphemes, Range};
|
|
|
|
if self.mode == Mode::Normal {
|
|
return;
|
|
}
|
|
|
|
self.mode = Mode::Normal;
|
|
let (view, doc) = current!(self);
|
|
|
|
try_restore_indent(doc, view);
|
|
|
|
// if leaving append mode, move cursor back by 1
|
|
if doc.restore_cursor {
|
|
let text = doc.text().slice(..);
|
|
let selection = doc.selection(view.id).clone().transform(|range| {
|
|
Range::new(
|
|
range.from(),
|
|
graphemes::prev_grapheme_boundary(text, range.to()),
|
|
)
|
|
});
|
|
|
|
doc.set_selection(view.id, selection);
|
|
doc.restore_cursor = false;
|
|
}
|
|
}
|
|
|
|
pub fn current_stack_frame(&self) -> Option<&StackFrame> {
|
|
self.debugger
|
|
.as_ref()
|
|
.and_then(|debugger| debugger.current_stack_frame())
|
|
}
|
|
|
|
/// Returns the id of a view that this doc contains a selection for,
|
|
/// making sure it is synced with the current changes
|
|
/// if possible or there are no selections returns current_view
|
|
/// otherwise uses an arbitrary view
|
|
pub fn get_synced_view_id(&mut self, id: DocumentId) -> ViewId {
|
|
let current_view = view_mut!(self);
|
|
let doc = self.documents.get_mut(&id).unwrap();
|
|
if doc.selections().contains_key(¤t_view.id) {
|
|
// only need to sync current view if this is not the current doc
|
|
if current_view.doc != id {
|
|
current_view.sync_changes(doc);
|
|
}
|
|
current_view.id
|
|
} else if let Some(view_id) = doc.selections().keys().next() {
|
|
let view_id = *view_id;
|
|
let view = self.tree.get_mut(view_id);
|
|
view.sync_changes(doc);
|
|
view_id
|
|
} else {
|
|
doc.ensure_view_init(current_view.id);
|
|
current_view.id
|
|
}
|
|
}
|
|
}
|
|
|
|
fn try_restore_indent(doc: &mut Document, view: &mut View) {
|
|
use helix_core::{
|
|
chars::char_is_whitespace, line_ending::line_end_char_index, Operation, Transaction,
|
|
};
|
|
|
|
fn inserted_a_new_blank_line(changes: &[Operation], pos: usize, line_end_pos: usize) -> bool {
|
|
if let [Operation::Retain(move_pos), Operation::Insert(ref inserted_str), Operation::Retain(_)] =
|
|
changes
|
|
{
|
|
move_pos + inserted_str.len() == pos
|
|
&& inserted_str.starts_with('\n')
|
|
&& inserted_str.chars().skip(1).all(char_is_whitespace)
|
|
&& pos == line_end_pos // ensure no characters exists after current position
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
let doc_changes = doc.changes().changes();
|
|
let text = doc.text().slice(..);
|
|
let range = doc.selection(view.id).primary();
|
|
let pos = range.cursor(text);
|
|
let line_end_pos = line_end_char_index(&text, range.cursor_line(text));
|
|
|
|
if inserted_a_new_blank_line(doc_changes, pos, line_end_pos) {
|
|
// Removes tailing whitespaces.
|
|
let transaction =
|
|
Transaction::change_by_selection(doc.text(), doc.selection(view.id), |range| {
|
|
let line_start_pos = text.line_to_char(range.cursor_line(text));
|
|
(line_start_pos, pos, None)
|
|
});
|
|
doc.apply(&transaction, view.id);
|
|
}
|
|
}
|