on 6c11708 help sign in
helix/helix-view/src/editor.rs
699 lines 24 kB history raw
1use crate::{
2 clipboard::{get_clipboard_provider, ClipboardProvider},
3 document::{Mode, SCRATCH_BUFFER_NAME},
4 graphics::{CursorKind, Rect},
5 input::KeyEvent,
6 theme::{self, Theme},
7 tree::{self, Tree},
8 Document, DocumentId, View, ViewId,
9};
11use futures_util::future;
12use std::{
13 collections::{BTreeMap, HashMap},
14 io::stdin,
15 num::NonZeroUsize,
16 path::{Path, PathBuf},
17 pin::Pin,
18 sync::Arc,
19};
21use tokio::time::{sleep, Duration, Instant, Sleep};
23use anyhow::{bail, Error};
25pub use helix_core::diagnostic::Severity;
26pub use helix_core::register::Registers;
27use helix_core::syntax;
28use helix_core::{Position, Selection};
30use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize};
32fn deserialize_duration_millis<'de, D>(deserializer: D) -> Result<Duration, D::Error>
33where
34 D: serde::Deserializer<'de>,
36 let millis = u64::deserialize(deserializer)?;
37 Ok(Duration::from_millis(millis))
40#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
41#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
42pub struct FilePickerConfig {
43 /// IgnoreOptions
44 /// Enables ignoring hidden files.
45 /// Whether to hide hidden files in file picker and global search results. Defaults to true.
46 pub hidden: bool,
47 /// Enables reading ignore files from parent directories. Defaults to true.
48 pub parents: bool,
49 /// Enables reading `.ignore` files.
50 /// Whether to hide files listed in .ignore in file picker and global search results. Defaults to true.
51 pub ignore: bool,
52 /// Enables reading `.gitignore` files.
53 /// Whether to hide files listed in .gitignore in file picker and global search results. Defaults to true.
54 pub git_ignore: bool,
55 /// Enables reading global .gitignore, whose path is specified in git's config: `core.excludefile` option.
56 /// Whether to hide files listed in global .gitignore in file picker and global search results. Defaults to true.
57 pub git_global: bool,
58 /// Enables reading `.git/info/exclude` files.
59 /// Whether to hide files listed in .git/info/exclude in file picker and global search results. Defaults to true.
60 pub git_exclude: bool,
61 /// WalkBuilder options
62 /// Maximum Depth to recurse directories in file picker and global search. Defaults to `None`.
63 pub max_depth: Option<usize>,
66impl Default for FilePickerConfig {
67 fn default() -> Self {
68 Self {
69 hidden: true,
70 parents: true,
71 ignore: true,
72 git_ignore: true,
73 git_global: true,
74 git_exclude: true,
75 max_depth: None,
76 }
77 }
80#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
81#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
82pub struct Config {
83 /// Padding to keep between the edge of the screen and the cursor when scrolling. Defaults to 5.
84 pub scrolloff: usize,
85 /// Number of lines to scroll at once. Defaults to 3
86 pub scroll_lines: isize,
87 /// Mouse support. Defaults to true.
88 pub mouse: bool,
89 /// Shell to use for shell commands. Defaults to ["cmd", "/C"] on Windows and ["sh", "-c"] otherwise.
90 pub shell: Vec<String>,
91 /// Line number mode.
92 pub line_number: LineNumber,
93 /// Middle click paste support. Defaults to true.
94 pub middle_click_paste: bool,
95 /// Smart case: Case insensitive searching unless pattern contains upper case characters. Defaults to true.
96 pub smart_case: bool,
97 /// Automatic insertion of pairs to parentheses, brackets, etc. Defaults to true.
98 pub auto_pairs: bool,
99 /// Automatic auto-completion, automatically pop up without user trigger. Defaults to true.
100 pub auto_completion: bool,
101 /// Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant. Defaults to 400ms.
102 #[serde(skip_serializing, deserialize_with = "deserialize_duration_millis")]
103 pub idle_timeout: Duration,
104 pub completion_trigger_len: u8,
105 /// Whether to display infoboxes. Defaults to true.
106 pub auto_info: bool,
107 pub file_picker: FilePickerConfig,
108 /// Shape for cursor in each mode
109 pub cursor_shape: CursorShapeConfig,
110 /// Set to `true` to override automatic detection of terminal truecolor support in the event of a false negative. Defaults to `false`.
111 pub true_color: bool,
114// Cursor shape is read and used on every rendered frame and so needs
115// to be fast. Therefore we avoid a hashmap and use an enum indexed array.
116#[derive(Debug, Clone, PartialEq)]
117pub struct CursorShapeConfig([CursorKind; 3]);
119impl CursorShapeConfig {
120 pub fn from_mode(&self, mode: Mode) -> CursorKind {
121 self.get(mode as usize).copied().unwrap_or_default()
122 }
125impl<'de> Deserialize<'de> for CursorShapeConfig {
126 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
127 where
128 D: Deserializer<'de>,
129 {
130 let m = HashMap::<Mode, CursorKind>::deserialize(deserializer)?;
131 let into_cursor = |mode: Mode| m.get(&mode).copied().unwrap_or_default();
132 Ok(CursorShapeConfig([
133 into_cursor(Mode::Normal),
134 into_cursor(Mode::Select),
135 into_cursor(Mode::Insert),
136 ]))
137 }
140impl Serialize for CursorShapeConfig {
141 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
142 where
143 S: serde::Serializer,
144 {
145 let mut map = serializer.serialize_map(Some(self.len()))?;
146 let modes = [Mode::Normal, Mode::Select, Mode::Insert];
147 for mode in modes {
148 map.serialize_entry(&mode, &self.from_mode(mode))?;
149 }
150 map.end()
151 }
154impl std::ops::Deref for CursorShapeConfig {
155 type Target = [CursorKind; 3];
157 fn deref(&self) -> &Self::Target {
158 &self.0
159 }
162impl Default for CursorShapeConfig {
163 fn default() -> Self {
164 Self([CursorKind::Block; 3])
165 }
168#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
169#[serde(rename_all = "kebab-case")]
170pub enum LineNumber {
171 /// Show absolute line number
172 Absolute,
174 /// If focused and in normal/select mode, show relative line number to the primary cursor.
175 /// If unfocused or in insert mode, show absolute line number.
176 Relative,
179impl std::str::FromStr for LineNumber {
180 type Err = anyhow::Error;
182 fn from_str(s: &str) -> Result<Self, Self::Err> {
183 match s.to_lowercase().as_str() {
184 "absolute" | "abs" => Ok(Self::Absolute),
185 "relative" | "rel" => Ok(Self::Relative),
186 _ => anyhow::bail!("Line number can only be `absolute` or `relative`."),
187 }
188 }
191impl Default for Config {
192 fn default() -> Self {
193 Self {
194 scrolloff: 5,
195 scroll_lines: 3,
196 mouse: true,
197 shell: if cfg!(windows) {
198 vec!["cmd".to_owned(), "/C".to_owned()]
199 } else {
200 vec!["sh".to_owned(), "-c".to_owned()]
201 },
202 line_number: LineNumber::Absolute,
203 middle_click_paste: true,
204 smart_case: true,
205 auto_pairs: true,
206 auto_completion: true,
207 idle_timeout: Duration::from_millis(400),
208 completion_trigger_len: 2,
209 auto_info: true,
210 file_picker: FilePickerConfig::default(),
211 cursor_shape: CursorShapeConfig::default(),
212 true_color: false,
213 }
214 }
217pub struct Motion(pub Box<dyn Fn(&mut Editor)>);
218impl Motion {
219 pub fn run(&self, e: &mut Editor) {
220 (self.0)(e)
221 }
223impl std::fmt::Debug for Motion {
224 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
225 f.write_str("motion")
226 }
229#[derive(Debug)]
230pub struct Editor {
231 pub tree: Tree,
232 pub next_document_id: DocumentId,
233 pub documents: BTreeMap<DocumentId, Document>,
234 pub count: Option<std::num::NonZeroUsize>,
235 pub selected_register: Option<char>,
236 pub registers: Registers,
237 pub macro_recording: Option<(char, Vec<KeyEvent>)>,
238 pub theme: Theme,
239 pub language_servers: helix_lsp::Registry,
240 pub clipboard_provider: Box<dyn ClipboardProvider>,
242 pub syn_loader: Arc<syntax::Loader>,
243 pub theme_loader: Arc<theme::Loader>,
245 pub status_msg: Option<(String, Severity)>,
247 pub config: Config,
249 pub idle_timer: Pin<Box<Sleep>>,
250 pub last_motion: Option<Motion>,
252 pub exit_code: i32,
255#[derive(Debug, Copy, Clone)]
256pub enum Action {
257 Load,
258 Replace,
259 HorizontalSplit,
260 VerticalSplit,
263impl Editor {
264 pub fn new(
265 mut area: Rect,
266 theme_loader: Arc<theme::Loader>,
267 syn_loader: Arc<syntax::Loader>,
268 config: Config,
269 ) -> Self {
270 let language_servers = helix_lsp::Registry::new();
272 // HAXX: offset the render area height by 1 to account for prompt/commandline
273 area.height -= 1;
275 Self {
276 tree: Tree::new(area),
277 next_document_id: DocumentId::default(),
278 documents: BTreeMap::new(),
279 count: None,
280 selected_register: None,
281 macro_recording: None,
282 theme: theme_loader.default(),
283 language_servers,
284 syn_loader,
285 theme_loader,
286 registers: Registers::default(),
287 clipboard_provider: get_clipboard_provider(),
288 status_msg: None,
289 idle_timer: Box::pin(sleep(config.idle_timeout)),
290 last_motion: None,
291 config,
292 exit_code: 0,
293 }
294 }
296 pub fn clear_idle_timer(&mut self) {
297 // equivalent to internal Instant::far_future() (30 years)
298 self.idle_timer
299 .as_mut()
300 .reset(Instant::now() + Duration::from_secs(86400 * 365 * 30));
301 }
303 pub fn reset_idle_timer(&mut self) {
304 self.idle_timer
305 .as_mut()
306 .reset(Instant::now() + self.config.idle_timeout);
307 }
309 pub fn clear_status(&mut self) {
310 self.status_msg = None;
311 }
313 pub fn set_status(&mut self, status: String) {
314 self.status_msg = Some((status, Severity::Info));
315 }
317 pub fn set_error(&mut self, error: String) {
318 self.status_msg = Some((error, Severity::Error));
319 }
321 pub fn set_theme(&mut self, theme: Theme) {
322 // `ui.selection` is the only scope required to be able to render a theme.
323 if theme.find_scope_index("ui.selection").is_none() {
324 self.set_error("Invalid theme: `ui.selection` required".to_owned());
325 return;
326 }
328 let scopes = theme.scopes();
329 self.syn_loader.set_scopes(scopes.to_vec());
331 self.theme = theme;
332 self._refresh();
333 }
335 /// Refreshes the language server for a given document
336 pub fn refresh_language_server(&mut self, doc_id: DocumentId) -> Option<()> {
337 let doc = self.documents.get_mut(&doc_id)?;
338 doc.detect_language(self.syn_loader.clone());
339 Self::launch_language_server(&mut self.language_servers, doc)
340 }
342 /// Launch a language server for a given document
343 fn launch_language_server(ls: &mut helix_lsp::Registry, doc: &mut Document) -> Option<()> {
344 // try to find a language server based on the language name
345 let language_server = doc.language.as_ref().and_then(|language| {
346 ls.get(language)
347 .map_err(|e| {
348 log::error!(
349 "Failed to initialize the LSP for `{}` {{ {} }}",
350 language.scope(),
351 e
352 )
353 })
354 .ok()
355 });
356 if let Some(language_server) = language_server {
357 // only spawn a new lang server if the servers aren't the same
358 if Some(language_server.id()) != doc.language_server().map(|server| server.id()) {
359 if let Some(language_server) = doc.language_server() {
360 tokio::spawn(language_server.text_document_did_close(doc.identifier()));
361 }
363 let language_id = doc.language_id().map(ToOwned::to_owned).unwrap_or_default();
365 // TODO: this now races with on_init code if the init happens too quickly
366 tokio::spawn(language_server.text_document_did_open(
367 doc.url().unwrap(),
368 doc.version(),
369 doc.text(),
370 language_id,
371 ));
373 doc.set_language_server(Some(language_server));
374 }
375 }
376 Some(())
377 }
379 fn _refresh(&mut self) {
380 for (view, _) in self.tree.views_mut() {
381 let doc = &self.documents[&view.doc];
382 view.ensure_cursor_in_view(doc, self.config.scrolloff)
383 }
384 }
386 fn replace_document_in_view(&mut self, current_view: ViewId, doc_id: DocumentId) {
387 let view = self.tree.get_mut(current_view);
388 view.doc = doc_id;
389 view.offset = Position::default();
391 let doc = self.documents.get_mut(&doc_id).unwrap();
393 // initialize selection for view
394 doc.selections
395 .entry(view.id)
396 .or_insert_with(|| Selection::point(0));
397 // TODO: reuse align_view
398 let pos = doc
399 .selection(view.id)
400 .primary()
401 .cursor(doc.text().slice(..));
402 let line = doc.text().char_to_line(pos);
403 view.offset.row = line.saturating_sub(view.inner_area().height as usize / 2);
404 }
406 pub fn switch(&mut self, id: DocumentId, action: Action) {
407 use crate::tree::Layout;
409 if !self.documents.contains_key(&id) {
410 log::error!("cannot switch to document that does not exist (anymore)");
411 return;
412 }
414 match action {
415 Action::Replace => {
416 let (view, doc) = current_ref!(self);
417 // If the current view is an empty scratch buffer and is not displayed in any other views, delete it.
418 // Boolean value is determined before the call to `view_mut` because the operation requires a borrow
419 // of `self.tree`, which is mutably borrowed when `view_mut` is called.
420 let remove_empty_scratch = !doc.is_modified()
421 // If the buffer has no path and is not modified, it is an empty scratch buffer.
422 && doc.path().is_none()
423 // If the buffer we are changing to is not this buffer
424 && id != doc.id
425 // Ensure the buffer is not displayed in any other splits.
426 && !self
427 .tree
428 .traverse()
429 .any(|(_, v)| v.doc == doc.id && v.id != view.id);
431 let (view, doc) = current!(self);
432 if remove_empty_scratch {
433 // Copy `doc.id` into a variable before calling `self.documents.remove`, which requires a mutable
434 // borrow, invalidating direct access to `doc.id`.
435 let id = doc.id;
436 self.documents.remove(&id);
437 } else {
438 let jump = (view.doc, doc.selection(view.id).clone());
439 view.jumps.push(jump);
440 // Set last accessed doc if it is a different document
441 if doc.id != id {
442 view.last_accessed_doc = Some(view.doc);
443 // Set last modified doc if modified and last modified doc is different
444 if std::mem::take(&mut doc.modified_since_accessed)
445 && view.last_modified_docs[0] != Some(view.doc)
446 {
447 view.last_modified_docs = [Some(view.doc), view.last_modified_docs[0]];
448 }
449 }
450 }
452 let view_id = view.id;
453 self.replace_document_in_view(view_id, id);
455 return;
456 }
457 Action::Load => {
458 let view_id = view!(self).id;
459 let doc = self.documents.get_mut(&id).unwrap();
460 if doc.selections().is_empty() {
461 doc.selections.insert(view_id, Selection::point(0));
462 }
463 return;
464 }
465 Action::HorizontalSplit | Action::VerticalSplit => {
466 let view = View::new(id);
467 let view_id = self.tree.split(
468 view,
469 match action {
470 Action::HorizontalSplit => Layout::Horizontal,
471 Action::VerticalSplit => Layout::Vertical,
472 _ => unreachable!(),
473 },
474 );
475 // initialize selection for view
476 let doc = self.documents.get_mut(&id).unwrap();
477 doc.selections.insert(view_id, Selection::point(0));
478 }
479 }
481 self._refresh();
482 }
484 /// Generate an id for a new document and register it.
485 fn new_document(&mut self, mut doc: Document) -> DocumentId {
486 let id = self.next_document_id;
487 // Safety: adding 1 from 1 is fine, probably impossible to reach usize max
488 self.next_document_id =
489 DocumentId(unsafe { NonZeroUsize::new_unchecked(self.next_document_id.0.get() + 1) });
490 doc.id = id;
491 self.documents.insert(id, doc);
492 id
493 }
495 fn new_file_from_document(&mut self, action: Action, doc: Document) -> DocumentId {
496 let id = self.new_document(doc);
497 self.switch(id, action);
498 id
499 }
501 pub fn new_file(&mut self, action: Action) -> DocumentId {
502 self.new_file_from_document(action, Document::default())
503 }
505 pub fn new_file_from_stdin(&mut self, action: Action) -> Result<DocumentId, Error> {
506 let (rope, encoding) = crate::document::from_reader(&mut stdin(), None)?;
507 Ok(self.new_file_from_document(action, Document::from(rope, Some(encoding))))
508 }
510 pub fn open(&mut self, path: PathBuf, action: Action) -> Result<DocumentId, Error> {
511 let path = helix_core::path::get_canonicalized_path(&path)?;
512 let id = self.document_by_path(&path).map(|doc| doc.id);
514 let id = if let Some(id) = id {
515 id
516 } else {
517 let mut doc = Document::open(&path, None, Some(self.syn_loader.clone()))?;
519 let _ = Self::launch_language_server(&mut self.language_servers, &mut doc);
521 self.new_document(doc)
522 };
524 self.switch(id, action);
525 Ok(id)
526 }
528 pub fn close(&mut self, id: ViewId) {
529 let view = self.tree.get(self.tree.focus);
530 // remove selection
531 self.documents
532 .get_mut(&view.doc)
533 .unwrap()
534 .selections
535 .remove(&id);
537 self.tree.remove(id);
538 self._refresh();
539 }
541 pub fn close_document(&mut self, doc_id: DocumentId, force: bool) -> anyhow::Result<()> {
542 let doc = match self.documents.get(&doc_id) {
543 Some(doc) => doc,
544 None => bail!("document does not exist"),
545 };
547 if !force && doc.is_modified() {
548 bail!(
549 "buffer {:?} is modified",
550 doc.relative_path()
551 .map(|path| path.to_string_lossy().to_string())
552 .unwrap_or_else(|| SCRATCH_BUFFER_NAME.into())
553 );
554 }
556 if let Some(language_server) = doc.language_server() {
557 tokio::spawn(language_server.text_document_did_close(doc.identifier()));
558 }
560 let views_to_close = self
561 .tree
562 .views()
563 .filter_map(|(view, _focus)| {
564 if view.doc == doc_id {
565 Some(view.id)
566 } else {
567 None
568 }
569 })
570 .collect::<Vec<_>>();
572 for view_id in views_to_close {
573 self.close(view_id);
574 }
576 self.documents.remove(&doc_id);
578 // If the document we removed was visible in all views, we will have no more views. We don't
579 // want to close the editor just for a simple buffer close, so we need to create a new view
580 // containing either an existing document, or a brand new document.
581 if self.tree.views().next().is_none() {
582 let doc_id = self
583 .documents
584 .iter()
585 .map(|(&doc_id, _)| doc_id)
586 .next()
587 .unwrap_or_else(|| self.new_document(Document::default()));
588 let view = View::new(doc_id);
589 let view_id = self.tree.insert(view);
590 let doc = self.documents.get_mut(&doc_id).unwrap();
591 doc.selections.insert(view_id, Selection::point(0));
592 }
594 self._refresh();
596 Ok(())
597 }
599 pub fn resize(&mut self, area: Rect) {
600 if self.tree.resize(area) {
601 self._refresh();
602 };
603 }
605 pub fn focus_next(&mut self) {
606 self.tree.focus_next();
607 }
609 pub fn focus_right(&mut self) {
610 self.tree.focus_direction(tree::Direction::Right);
611 }
613 pub fn focus_left(&mut self) {
614 self.tree.focus_direction(tree::Direction::Left);
615 }
617 pub fn focus_up(&mut self) {
618 self.tree.focus_direction(tree::Direction::Up);
619 }
621 pub fn focus_down(&mut self) {
622 self.tree.focus_direction(tree::Direction::Down);
623 }
625 pub fn should_close(&self) -> bool {
626 self.tree.is_empty()
627 }
629 pub fn ensure_cursor_in_view(&mut self, id: ViewId) {
630 let view = self.tree.get_mut(id);
631 let doc = &self.documents[&view.doc];
632 view.ensure_cursor_in_view(doc, self.config.scrolloff)
633 }
635 #[inline]
636 pub fn document(&self, id: DocumentId) -> Option<&Document> {
637 self.documents.get(&id)
638 }
640 #[inline]
641 pub fn document_mut(&mut self, id: DocumentId) -> Option<&mut Document> {
642 self.documents.get_mut(&id)
643 }
645 #[inline]
646 pub fn documents(&self) -> impl Iterator<Item = &Document> {
647 self.documents.values()
648 }
650 #[inline]
651 pub fn documents_mut(&mut self) -> impl Iterator<Item = &mut Document> {
652 self.documents.values_mut()
653 }
655 pub fn document_by_path<P: AsRef<Path>>(&self, path: P) -> Option<&Document> {
656 self.documents()
657 .find(|doc| doc.path().map(|p| p == path.as_ref()).unwrap_or(false))
658 }
660 pub fn document_by_path_mut<P: AsRef<Path>>(&mut self, path: P) -> Option<&mut Document> {
661 self.documents_mut()
662 .find(|doc| doc.path().map(|p| p == path.as_ref()).unwrap_or(false))
663 }
665 pub fn cursor(&self) -> (Option<Position>, CursorKind) {
666 let (view, doc) = current_ref!(self);
667 let cursor = doc
668 .selection(view.id)
669 .primary()
670 .cursor(doc.text().slice(..));
671 if let Some(mut pos) = view.screen_coords_at_pos(doc, doc.text().slice(..), cursor) {
672 let inner = view.inner_area();
673 pos.col += inner.x as usize;
674 pos.row += inner.y as usize;
675 let cursorkind = self.config.cursor_shape.from_mode(doc.mode());
676 (Some(pos), cursorkind)
677 } else {
678 (None, CursorKind::default())
679 }
680 }
682 /// Closes language servers with timeout. The default timeout is 500 ms, use
683 /// `timeout` parameter to override this.
684 pub async fn close_language_servers(
685 &self,
686 timeout: Option<u64>,
687 ) -> Result<(), tokio::time::error::Elapsed> {
688 tokio::time::timeout(
689 Duration::from_millis(timeout.unwrap_or(500)),
690 future::join_all(
691 self.language_servers
692 .iter_clients()
693 .map(|client| client.force_shutdown()),
694 ),
695 )
696 .await
697 .map(|_| ())
698 }