helix-mods/helix-view/src/editor.rs

395 lines
12 KiB
Rust
Raw Normal View History

2021-06-19 13:26:52 +02:00
use crate::{
clipboard::{get_clipboard_provider, ClipboardProvider},
graphics::{CursorKind, Rect},
2021-06-19 13:26:52 +02:00
theme::{self, Theme},
tree::Tree,
Document, DocumentId, View, ViewId,
2021-06-19 13:26:52 +02:00
};
use futures_util::future;
use std::{
path::{Path, PathBuf},
sync::Arc,
time::Duration,
};
2021-03-24 06:03:20 +01:00
use slotmap::SlotMap;
2021-02-19 09:46:43 +01:00
use anyhow::Error;
pub use helix_core::diagnostic::Severity;
pub use helix_core::register::Registers;
use helix_core::syntax;
2021-06-23 08:03:34 +02:00
use helix_core::Position;
use serde::Deserialize;
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(rename_all = "kebab-case", default)]
pub struct Config {
/// Padding to keep between the edge of the screen and the cursor when scrolling. Defaults to 5.
pub scrolloff: usize,
/// Number of lines to scroll at once. Defaults to 3
pub scroll_lines: isize,
/// Mouse support. Defaults to true.
pub mouse: bool,
/// Shell to use for shell commands. Defaults to ["cmd", "/C"] on Windows and ["sh", "-c"] otherwise.
pub shell: Vec<String>,
/// Line number mode.
pub line_number: LineNumber,
Support primary clipboard (#548) * clipboard-none: add in-memory fallback buffer Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * view: add Wayland primary clipboard Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * Format Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * helix-term: copy to primary selection after mouse move stops Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * helix-term: don't update primary selection if it is a single character Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * helix-term: discard result of setting primary selection Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * helix-term: add commands for interaction with primary clipboard Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * editor: implement primary selection copy/paste using commands Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * clipboard: support xsel for primary selection Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * clipboard: support xclip for primary selection Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * helix-term: multiple cursor support for middle click paste Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * rename primary selection to primary clipboard in scope of PR Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * helix-term: make middle click paste optional Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * Format Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * Update helix-term/src/ui/editor.rs * fix formatting Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * config: correct defaults if terminal prop is not set Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * refactor: merge clipboard and primary selection implementations Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * Tidy up code Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * view: remove names for different clipboard/selection providers Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * Update helix-view/src/clipboard.rs Co-authored-by: Gokul Soumya <gokulps15@gmail.com> * helix-view: tidy macros Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * helix-term: refactor paste-replace commands Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * helix-term: use new config for middle-click-paste Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * clipboard: remove memory fallback for command and windows providers Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * clipboard-win: fix build Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * clipboard: return empty string when primary clipboard is missing Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * clipboard: fix errors in Windows build Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> Co-authored-by: Gokul Soumya <gokulps15@gmail.com>
2021-08-12 04:53:48 +02:00
/// Middle click paste support. Defaults to true
pub middle_click_paste: bool,
/// Smart case: Case insensitive searching unless pattern contains upper case characters. Defaults to true.
pub smart_case: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum LineNumber {
/// Show absolute line number
Absolute,
/// Show relative line number to the primary cursor
Relative,
}
impl Default for Config {
fn default() -> Self {
Self {
scrolloff: 5,
scroll_lines: 3,
mouse: true,
shell: if cfg!(windows) {
vec!["cmd".to_owned(), "/C".to_owned()]
} else {
vec!["sh".to_owned(), "-c".to_owned()]
},
line_number: LineNumber::Absolute,
Support primary clipboard (#548) * clipboard-none: add in-memory fallback buffer Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * view: add Wayland primary clipboard Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * Format Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * helix-term: copy to primary selection after mouse move stops Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * helix-term: don't update primary selection if it is a single character Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * helix-term: discard result of setting primary selection Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * helix-term: add commands for interaction with primary clipboard Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * editor: implement primary selection copy/paste using commands Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * clipboard: support xsel for primary selection Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * clipboard: support xclip for primary selection Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * helix-term: multiple cursor support for middle click paste Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * rename primary selection to primary clipboard in scope of PR Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * helix-term: make middle click paste optional Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * Format Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * Update helix-term/src/ui/editor.rs * fix formatting Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * config: correct defaults if terminal prop is not set Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * refactor: merge clipboard and primary selection implementations Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * Tidy up code Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * view: remove names for different clipboard/selection providers Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * Update helix-view/src/clipboard.rs Co-authored-by: Gokul Soumya <gokulps15@gmail.com> * helix-view: tidy macros Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * helix-term: refactor paste-replace commands Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * helix-term: use new config for middle-click-paste Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * clipboard: remove memory fallback for command and windows providers Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * clipboard-win: fix build Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * clipboard: return empty string when primary clipboard is missing Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> * clipboard: fix errors in Windows build Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com> Co-authored-by: Gokul Soumya <gokulps15@gmail.com>
2021-08-12 04:53:48 +02:00
middle_click_paste: true,
smart_case: true,
}
}
}
#[derive(Debug)]
pub struct Editor {
pub tree: Tree,
pub documents: SlotMap<DocumentId, Document>,
2021-06-08 05:24:27 +02:00
pub count: Option<std::num::NonZeroUsize>,
pub selected_register: Option<char>,
pub registers: Registers,
pub theme: Theme,
pub language_servers: helix_lsp::Registry,
pub clipboard_provider: Box<dyn ClipboardProvider>,
2021-06-19 13:26:52 +02:00
pub syn_loader: Arc<syntax::Loader>,
pub theme_loader: Arc<theme::Loader>,
pub status_msg: Option<(String, Severity)>,
pub config: Config,
}
#[derive(Debug, Copy, Clone)]
pub enum Action {
Load,
Replace,
HorizontalSplit,
VerticalSplit,
}
impl Editor {
2021-06-19 13:26:52 +02:00
pub fn new(
mut area: Rect,
2021-06-19 13:26:52 +02:00
themes: Arc<theme::Loader>,
config_loader: Arc<syntax::Loader>,
config: Config,
2021-06-19 13:26:52 +02:00
) -> Self {
let language_servers = helix_lsp::Registry::new();
2021-02-09 07:59:42 +01:00
// HAXX: offset the render area height by 1 to account for prompt/commandline
area.height -= 1;
2020-10-16 07:37:12 +02:00
Self {
tree: Tree::new(area),
documents: SlotMap::with_key(),
2021-02-09 08:39:17 +01:00
count: None,
selected_register: None,
2021-06-19 13:26:52 +02:00
theme: themes.default(),
language_servers,
2021-06-19 13:26:52 +02:00
syn_loader: config_loader,
theme_loader: themes,
registers: Registers::default(),
clipboard_provider: get_clipboard_provider(),
status_msg: None,
config,
2020-10-16 07:37:12 +02:00
}
}
pub fn clear_status(&mut self) {
self.status_msg = None;
}
pub fn set_status(&mut self, status: String) {
self.status_msg = Some((status, Severity::Info));
}
pub fn set_error(&mut self, error: String) {
self.status_msg = Some((error, Severity::Error));
}
2021-06-19 13:26:52 +02:00
pub fn set_theme(&mut self, theme: Theme) {
let scopes = theme.scopes();
for config in self
.syn_loader
.language_configs_iter()
.filter(|cfg| cfg.is_highlight_initialized())
{
config.reconfigure(scopes);
2021-06-19 13:26:52 +02:00
}
self.theme = theme;
self._refresh();
}
pub fn set_theme_from_name(&mut self, theme: &str) -> anyhow::Result<()> {
use anyhow::Context;
let theme = self
.theme_loader
.load(theme.as_ref())
.with_context(|| format!("failed setting theme `{}`", theme))?;
2021-06-19 13:26:52 +02:00
self.set_theme(theme);
Ok(())
2021-06-19 13:26:52 +02:00
}
fn _refresh(&mut self) {
for (view, _) in self.tree.views_mut() {
let doc = &self.documents[view.doc];
view.ensure_cursor_in_view(doc, self.config.scrolloff)
}
}
pub fn switch(&mut self, id: DocumentId, action: Action) {
use crate::tree::Layout;
use helix_core::Selection;
if !self.documents.contains_key(id) {
log::error!("cannot switch to document that does not exist (anymore)");
return;
}
match action {
Action::Replace => {
let view = view!(self);
let jump = (
view.doc,
self.documents[view.doc].selection(view.id).clone(),
);
2021-03-24 10:01:26 +01:00
let view = view_mut!(self);
2021-03-24 10:01:26 +01:00
view.jumps.push(jump);
view.last_accessed_doc = Some(view.doc);
2021-03-24 08:56:29 +01:00
view.doc = id;
view.offset = Position::default();
let (view, doc) = current!(self);
// initialize selection for view
doc.selections
2021-05-17 16:01:45 +02:00
.entry(view.id)
.or_insert_with(|| Selection::point(0));
// TODO: reuse align_view
let pos = doc
.selection(view.id)
.primary()
.cursor(doc.text().slice(..));
let line = doc.text().char_to_line(pos);
view.offset.row = line.saturating_sub(view.inner_area().height as usize / 2);
return;
}
Action::Load => {
return;
}
Action::HorizontalSplit => {
let view = View::new(id);
let view_id = self.tree.split(view, Layout::Horizontal);
// initialize selection for view
let doc = &mut self.documents[id];
doc.selections.insert(view_id, Selection::point(0));
}
Action::VerticalSplit => {
let view = View::new(id);
let view_id = self.tree.split(view, Layout::Vertical);
// initialize selection for view
let doc = &mut self.documents[id];
doc.selections.insert(view_id, Selection::point(0));
}
}
self._refresh();
}
2020-10-19 09:20:59 +02:00
pub fn new_file(&mut self, action: Action) -> DocumentId {
2021-06-23 08:03:34 +02:00
let doc = Document::default();
let id = self.documents.insert(doc);
self.documents[id].id = id;
self.switch(id, action);
id
}
pub fn open(&mut self, path: PathBuf, action: Action) -> Result<DocumentId, Error> {
let path = helix_core::path::get_canonicalized_path(&path)?;
let id = self
.documents()
.find(|doc| doc.path() == Some(&path))
.map(|doc| doc.id);
let id = if let Some(id) = id {
id
} else {
let mut doc = Document::open(&path, None, Some(&self.theme), Some(&self.syn_loader))?;
// try to find a language server based on the language name
let language_server = doc.language.as_ref().and_then(|language| {
self.language_servers
.get(language)
.map_err(|e| {
log::error!("Failed to get LSP, {}, for `{}`", e, language.scope())
})
.ok()
});
if let Some(language_server) = language_server {
let language_id = doc
.language()
.and_then(|s| s.split('.').last()) // source.rust
.map(ToOwned::to_owned)
.unwrap_or_default();
// TODO: this now races with on_init code if the init happens too quickly
tokio::spawn(language_server.text_document_did_open(
doc.url().unwrap(),
doc.version(),
doc.text(),
language_id,
));
doc.set_language_server(Some(language_server));
}
let id = self.documents.insert(doc);
self.documents[id].id = id;
id
};
self.switch(id, action);
Ok(id)
}
pub fn close(&mut self, id: ViewId, close_buffer: bool) {
let view = self.tree.get(self.tree.focus);
// remove selection
self.documents[view.doc].selections.remove(&id);
if close_buffer {
// get around borrowck issues
let doc = &self.documents[view.doc];
if let Some(language_server) = doc.language_server() {
tokio::spawn(language_server.text_document_did_close(doc.identifier()));
}
self.documents.remove(view.doc);
}
self.tree.remove(id);
self._refresh();
}
pub fn resize(&mut self, area: Rect) {
if self.tree.resize(area) {
self._refresh();
};
}
pub fn focus_next(&mut self) {
self.tree.focus_next();
2021-02-19 09:46:43 +01:00
}
2021-03-18 06:48:42 +01:00
pub fn should_close(&self) -> bool {
2021-02-19 09:46:43 +01:00
self.tree.is_empty()
}
2021-03-24 06:03:20 +01:00
pub fn ensure_cursor_in_view(&mut self, id: ViewId) {
let view = self.tree.get_mut(id);
let doc = &self.documents[view.doc];
view.ensure_cursor_in_view(doc, self.config.scrolloff)
}
2021-09-02 05:52:32 +02:00
#[inline]
pub fn document(&self, id: DocumentId) -> Option<&Document> {
self.documents.get(id)
}
2021-09-02 05:52:32 +02:00
#[inline]
2021-06-23 20:35:39 +02:00
pub fn document_mut(&mut self, id: DocumentId) -> Option<&mut Document> {
self.documents.get_mut(id)
}
2021-09-02 05:52:32 +02:00
#[inline]
pub fn documents(&self) -> impl Iterator<Item = &Document> {
2021-09-02 05:52:32 +02:00
self.documents.values()
}
2021-09-02 05:52:32 +02:00
#[inline]
2021-06-19 13:26:52 +02:00
pub fn documents_mut(&mut self) -> impl Iterator<Item = &mut Document> {
2021-09-02 05:52:32 +02:00
self.documents.values_mut()
2021-06-19 13:26:52 +02:00
}
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))
}
pub fn cursor(&self) -> (Option<Position>, CursorKind) {
let view = view!(self);
let doc = &self.documents[view.doc];
let cursor = doc
.selection(view.id)
.primary()
.cursor(doc.text().slice(..));
if let Some(mut pos) = view.screen_coords_at_pos(doc, doc.text().slice(..), cursor) {
let inner = view.inner_area();
pos.col += inner.x as usize;
pos.row += inner.y as usize;
(Some(pos), CursorKind::Hidden)
} else {
(None, CursorKind::Hidden)
}
}
/// Closes language servers with timeout. The default timeout is 500 ms, use
/// `timeout` parameter to override this.
pub async fn close_language_servers(
&self,
timeout: Option<u64>,
) -> Result<(), tokio::time::error::Elapsed> {
tokio::time::timeout(
Duration::from_millis(timeout.unwrap_or(500)),
future::join_all(
self.language_servers
.iter_clients()
.map(|client| client.force_shutdown()),
),
)
.await
.map(|_| ())
}
}