2 clipboard::{get_clipboard_provider, ClipboardProvider},
3 document::{Mode, SCRATCH_BUFFER_NAME},
4 graphics::{CursorKind, Rect},
9 Document, DocumentId, View, ViewId,
12use futures_util::future;
13use futures_util::stream::select_all::SelectAll;
14use tokio_stream::wrappers::UnboundedReceiverStream;
18 collections::{BTreeMap, HashMap},
21 path::{Path, PathBuf},
27 sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender},
28 time::{sleep, Duration, Instant, Sleep},
31use anyhow::{bail, Error};
33pub use helix_core::diagnostic::Severity;
34pub use helix_core::register::Registers;
35use helix_core::Position;
37 auto_pairs::AutoPairs,
38 syntax::{self, AutoPairConfig},
44use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer};
46use arc_swap::access::{DynAccess, DynGuard};
48fn deserialize_duration_millis<'de, D>(deserializer: D) -> Result<Duration, D::Error>
50 D: serde::Deserializer<'de>,
52 let millis = u64::deserialize(deserializer)?;
53 Ok(Duration::from_millis(millis))
56fn serialize_duration_millis<S>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error>
60 serializer.serialize_u64(
64 .map_err(|_| serde::ser::Error::custom("duration value overflowed u64"))?,
68#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
69#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
70pub struct FilePickerConfig {
72 /// Enables ignoring hidden files.
73 /// Whether to hide hidden files in file picker and global search results. Defaults to true.
75 /// Enables following symlinks.
76 /// Whether to follow symbolic links in file picker and file or directory completions. Defaults to true.
77 pub follow_symlinks: bool,
78 /// Enables reading ignore files from parent directories. Defaults to true.
80 /// Enables reading `.ignore` files.
81 /// Whether to hide files listed in .ignore in file picker and global search results. Defaults to true.
83 /// Enables reading `.gitignore` files.
84 /// Whether to hide files listed in .gitignore in file picker and global search results. Defaults to true.
86 /// Enables reading global .gitignore, whose path is specified in git's config: `core.excludefile` option.
87 /// Whether to hide files listed in global .gitignore in file picker and global search results. Defaults to true.
89 /// Enables reading `.git/info/exclude` files.
90 /// Whether to hide files listed in .git/info/exclude in file picker and global search results. Defaults to true.
91 pub git_exclude: bool,
92 /// WalkBuilder options
93 /// Maximum Depth to recurse directories in file picker and global search. Defaults to `None`.
94 pub max_depth: Option<usize>,
97impl Default for FilePickerConfig {
98 fn default() -> Self {
101 follow_symlinks: true,
112#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
113#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
115 /// Padding to keep between the edge of the screen and the cursor when scrolling. Defaults to 5.
116 pub scrolloff: usize,
117 /// Number of lines to scroll at once. Defaults to 3
118 pub scroll_lines: isize,
119 /// Mouse support. Defaults to true.
121 /// Shell to use for shell commands. Defaults to ["cmd", "/C"] on Windows and ["sh", "-c"] otherwise.
122 pub shell: Vec<String>,
123 /// Line number mode.
124 pub line_number: LineNumber,
125 /// Highlight the lines cursors are currently on. Defaults to false.
126 pub cursorline: bool,
127 /// Gutters. Default ["diagnostics", "line-numbers"]
128 pub gutters: Vec<GutterType>,
129 /// Middle click paste support. Defaults to true.
130 pub middle_click_paste: bool,
131 /// Automatic insertion of pairs to parentheses, brackets,
132 /// etc. Optionally, this can be a list of 2-tuples to specify a
133 /// global list of characters to pair. Defaults to true.
134 pub auto_pairs: AutoPairConfig,
135 /// Automatic auto-completion, automatically pop up without user trigger. Defaults to true.
136 pub auto_completion: bool,
137 /// Automatic formatting on save. Defaults to true.
138 pub auto_format: bool,
139 /// Time in milliseconds since last keypress before idle timers trigger.
140 /// Used for autocompletion, set to 0 for instant. Defaults to 400ms.
142 serialize_with = "serialize_duration_millis",
143 deserialize_with = "deserialize_duration_millis"
145 pub idle_timeout: Duration,
146 pub completion_trigger_len: u8,
147 /// Whether to display infoboxes. Defaults to true.
149 pub file_picker: FilePickerConfig,
150 /// Configuration of the statusline elements
151 pub statusline: StatusLineConfig,
152 /// Shape for cursor in each mode
153 pub cursor_shape: CursorShapeConfig,
154 /// Set to `true` to override automatic detection of terminal truecolor support in the event of a false negative. Defaults to `false`.
155 pub true_color: bool,
156 /// Search configuration.
158 pub search: SearchConfig,
160 /// Column numbers at which to draw the rulers. Default to `[]`, meaning no rulers.
161 pub rulers: Vec<u16>,
163 pub whitespace: WhitespaceConfig,
164 /// Vertical indent width guides.
165 pub indent_guides: IndentGuidesConfig,
166 /// Whether to color modes with different colors. Defaults to `false`.
167 pub color_modes: bool,
170#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
171#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
172pub struct LspConfig {
173 /// Display LSP progress messages below statusline
174 pub display_messages: bool,
175 /// Enable automatic pop up of signature help (parameter hints)
176 pub auto_signature_help: bool,
177 /// Display docs under signature help popup
178 pub display_signature_help_docs: bool,
181impl Default for LspConfig {
182 fn default() -> Self {
184 display_messages: false,
185 auto_signature_help: true,
186 display_signature_help_docs: true,
191#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
192#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
193pub struct SearchConfig {
194 /// Smart case: Case insensitive searching unless pattern contains upper case characters. Defaults to true.
195 pub smart_case: bool,
196 /// Whether the search should wrap after depleting the matches. Default to true.
197 pub wrap_around: bool,
200#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
201#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
202pub struct StatusLineConfig {
203 pub left: Vec<StatusLineElement>,
204 pub center: Vec<StatusLineElement>,
205 pub right: Vec<StatusLineElement>,
206 pub separator: String,
209impl Default for StatusLineConfig {
210 fn default() -> Self {
211 use StatusLineElement as E;
214 left: vec![E::Mode, E::Spinner, E::FileName],
216 right: vec![E::Diagnostics, E::Selections, E::Position, E::FileEncoding],
217 separator: String::from("│"),
222#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
223#[serde(rename_all = "kebab-case")]
224pub enum StatusLineElement {
225 /// The editor mode (Normal, Insert, Visual/Selection)
228 /// The LSP activity spinner
231 /// The file nane/path, including a dirty flag if it's unsaved
234 /// The file encoding
237 /// The file line endings (CRLF or LF)
240 /// The file type (language ID or "text")
243 /// A summary of the number of errors and warnings
246 /// The number of selections (cursors)
249 /// The cursor position
252 /// The separator string
255 /// The cursor position as a percent of the total file
262// Cursor shape is read and used on every rendered frame and so needs
263// to be fast. Therefore we avoid a hashmap and use an enum indexed array.
264#[derive(Debug, Clone, PartialEq)]
265pub struct CursorShapeConfig([CursorKind; 3]);
267impl CursorShapeConfig {
268 pub fn from_mode(&self, mode: Mode) -> CursorKind {
269 self.get(mode as usize).copied().unwrap_or_default()
273impl<'de> Deserialize<'de> for CursorShapeConfig {
274 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
276 D: Deserializer<'de>,
278 let m = HashMap::<Mode, CursorKind>::deserialize(deserializer)?;
279 let into_cursor = |mode: Mode| m.get(&mode).copied().unwrap_or_default();
280 Ok(CursorShapeConfig([
281 into_cursor(Mode::Normal),
282 into_cursor(Mode::Select),
283 into_cursor(Mode::Insert),
288impl Serialize for CursorShapeConfig {
289 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
291 S: serde::Serializer,
293 let mut map = serializer.serialize_map(Some(self.len()))?;
294 let modes = [Mode::Normal, Mode::Select, Mode::Insert];
296 map.serialize_entry(&mode, &self.from_mode(mode))?;
302impl std::ops::Deref for CursorShapeConfig {
303 type Target = [CursorKind; 3];
305 fn deref(&self) -> &Self::Target {
310impl Default for CursorShapeConfig {
311 fn default() -> Self {
312 Self([CursorKind::Block; 3])
316#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
317#[serde(rename_all = "kebab-case")]
319 /// Show absolute line number
322 /// If focused and in normal/select mode, show relative line number to the primary cursor.
323 /// If unfocused or in insert mode, show absolute line number.
327impl std::str::FromStr for LineNumber {
328 type Err = anyhow::Error;
330 fn from_str(s: &str) -> Result<Self, Self::Err> {
331 match s.to_lowercase().as_str() {
332 "absolute" | "abs" => Ok(Self::Absolute),
333 "relative" | "rel" => Ok(Self::Relative),
334 _ => anyhow::bail!("Line number can only be `absolute` or `relative`."),
339#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
340#[serde(rename_all = "kebab-case")]
342 /// Show diagnostics and other features like breakpoints
344 /// Show line numbers
346 /// Show one blank space
350impl std::str::FromStr for GutterType {
351 type Err = anyhow::Error;
353 fn from_str(s: &str) -> Result<Self, Self::Err> {
354 match s.to_lowercase().as_str() {
355 "diagnostics" => Ok(Self::Diagnostics),
356 "line-numbers" => Ok(Self::LineNumbers),
357 _ => anyhow::bail!("Gutter type can only be `diagnostics` or `line-numbers`."),
362#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
364pub struct WhitespaceConfig {
365 pub render: WhitespaceRender,
366 pub characters: WhitespaceCharacters,
369impl Default for WhitespaceConfig {
370 fn default() -> Self {
372 render: WhitespaceRender::Basic(WhitespaceRenderValue::None),
373 characters: WhitespaceCharacters::default(),
378#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
379#[serde(untagged, rename_all = "kebab-case")]
380pub enum WhitespaceRender {
381 Basic(WhitespaceRenderValue),
383 default: Option<WhitespaceRenderValue>,
384 space: Option<WhitespaceRenderValue>,
385 nbsp: Option<WhitespaceRenderValue>,
386 tab: Option<WhitespaceRenderValue>,
387 newline: Option<WhitespaceRenderValue>,
391#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
392#[serde(rename_all = "kebab-case")]
393pub enum WhitespaceRenderValue {
400impl WhitespaceRender {
401 pub fn space(&self) -> WhitespaceRenderValue {
403 Self::Basic(val) => val,
404 Self::Specific { default, space, .. } => {
405 space.or(default).unwrap_or(WhitespaceRenderValue::None)
409 pub fn nbsp(&self) -> WhitespaceRenderValue {
411 Self::Basic(val) => val,
412 Self::Specific { default, nbsp, .. } => {
413 nbsp.or(default).unwrap_or(WhitespaceRenderValue::None)
417 pub fn tab(&self) -> WhitespaceRenderValue {
419 Self::Basic(val) => val,
420 Self::Specific { default, tab, .. } => {
421 tab.or(default).unwrap_or(WhitespaceRenderValue::None)
425 pub fn newline(&self) -> WhitespaceRenderValue {
427 Self::Basic(val) => val,
430 } => newline.or(default).unwrap_or(WhitespaceRenderValue::None),
435#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
437pub struct WhitespaceCharacters {
444impl Default for WhitespaceCharacters {
445 fn default() -> Self {
447 space: '·', // U+00B7
450 newline: '⏎', // U+23CE
455#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
457pub struct IndentGuidesConfig {
462impl Default for IndentGuidesConfig {
463 fn default() -> Self {
471impl Default for Config {
472 fn default() -> Self {
477 shell: if cfg!(windows) {
478 vec!["cmd".to_owned(), "/C".to_owned()]
480 vec!["sh".to_owned(), "-c".to_owned()]
482 line_number: LineNumber::Absolute,
484 gutters: vec![GutterType::Diagnostics, GutterType::LineNumbers],
485 middle_click_paste: true,
486 auto_pairs: AutoPairConfig::default(),
487 auto_completion: true,
489 idle_timeout: Duration::from_millis(400),
490 completion_trigger_len: 2,
492 file_picker: FilePickerConfig::default(),
493 statusline: StatusLineConfig::default(),
494 cursor_shape: CursorShapeConfig::default(),
496 search: SearchConfig::default(),
497 lsp: LspConfig::default(),
499 whitespace: WhitespaceConfig::default(),
500 indent_guides: IndentGuidesConfig::default(),
506impl Default for SearchConfig {
507 fn default() -> Self {
515pub struct Motion(pub Box<dyn Fn(&mut Editor)>);
517 pub fn run(&self, e: &mut Editor) {
521impl std::fmt::Debug for Motion {
522 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
523 f.write_str("motion")
527#[derive(Debug, Clone, Default)]
528pub struct Breakpoint {
529 pub id: Option<usize>,
531 pub message: Option<String>,
534 pub column: Option<usize>,
535 pub condition: Option<String>,
536 pub hit_condition: Option<String>,
537 pub log_message: Option<String>,
542 pub next_document_id: DocumentId,
543 pub documents: BTreeMap<DocumentId, Document>,
544 pub count: Option<std::num::NonZeroUsize>,
545 pub selected_register: Option<char>,
546 pub registers: Registers,
547 pub macro_recording: Option<(char, Vec<KeyEvent>)>,
548 pub macro_replaying: Vec<char>,
549 pub language_servers: helix_lsp::Registry,
550 pub diagnostics: BTreeMap<lsp::Url, Vec<lsp::Diagnostic>>,
552 pub debugger: Option<dap::Client>,
553 pub debugger_events: SelectAll<UnboundedReceiverStream<dap::Payload>>,
554 pub breakpoints: HashMap<PathBuf, Vec<Breakpoint>>,
556 pub clipboard_provider: Box<dyn ClipboardProvider>,
558 pub syn_loader: Arc<syntax::Loader>,
559 pub theme_loader: Arc<theme::Loader>,
560 /// last_theme is used for theme previews. We store the current theme here,
561 /// and if previewing is cancelled, we can return to it.
562 pub last_theme: Option<Theme>,
563 /// The currently applied editor theme. While previewing a theme, the previewed theme
567 pub status_msg: Option<(Cow<'static, str>, Severity)>,
568 pub autoinfo: Option<Info>,
570 pub config: Box<dyn DynAccess<Config>>,
571 pub auto_pairs: Option<AutoPairs>,
573 pub idle_timer: Pin<Box<Sleep>>,
574 pub last_motion: Option<Motion>,
575 pub pseudo_pending: Option<String>,
577 pub last_completion: Option<CompleteAction>,
581 pub config_events: (UnboundedSender<ConfigEvent>, UnboundedReceiver<ConfigEvent>),
584#[derive(Debug, Clone)]
585pub enum ConfigEvent {
595#[derive(Debug, Clone)]
596pub struct CompleteAction {
597 pub trigger_offset: usize,
598 pub changes: Vec<Change>,
601#[derive(Debug, Copy, Clone)]
612 theme_loader: Arc<theme::Loader>,
613 syn_loader: Arc<syntax::Loader>,
614 config: Box<dyn DynAccess<Config>>,
616 let conf = config.load();
617 let auto_pairs = (&conf.auto_pairs).into();
619 // HAXX: offset the render area height by 1 to account for prompt/commandline
623 tree: Tree::new(area),
624 next_document_id: DocumentId::default(),
625 documents: BTreeMap::new(),
627 selected_register: None,
628 macro_recording: None,
629 macro_replaying: Vec::new(),
630 theme: theme_loader.default(),
631 language_servers: helix_lsp::Registry::new(),
632 diagnostics: BTreeMap::new(),
634 debugger_events: SelectAll::new(),
635 breakpoints: HashMap::new(),
639 registers: Registers::default(),
640 clipboard_provider: get_clipboard_provider(),
643 idle_timer: Box::pin(sleep(conf.idle_timeout)),
645 last_completion: None,
646 pseudo_pending: None,
650 config_events: unbounded_channel(),
654 pub fn config(&self) -> DynGuard<Config> {
658 /// Call if the config has changed to let the editor update all
659 /// relevant members.
660 pub fn refresh_config(&mut self) {
661 let config = self.config();
662 self.auto_pairs = (&config.auto_pairs).into();
663 self.reset_idle_timer();
666 pub fn clear_idle_timer(&mut self) {
667 // equivalent to internal Instant::far_future() (30 years)
670 .reset(Instant::now() + Duration::from_secs(86400 * 365 * 30));
673 pub fn reset_idle_timer(&mut self) {
674 let config = self.config();
677 .reset(Instant::now() + config.idle_timeout);
680 pub fn clear_status(&mut self) {
681 self.status_msg = None;
685 pub fn set_status<T: Into<Cow<'static, str>>>(&mut self, status: T) {
686 self.status_msg = Some((status.into(), Severity::Info));
690 pub fn set_error<T: Into<Cow<'static, str>>>(&mut self, error: T) {
691 self.status_msg = Some((error.into(), Severity::Error));
695 pub fn get_status(&self) -> Option<(&Cow<'static, str>, &Severity)> {
696 self.status_msg.as_ref().map(|(status, sev)| (status, sev))
699 /// Returns true if the current status is an error
701 pub fn is_err(&self) -> bool {
704 .map(|(_, sev)| *sev == Severity::Error)
708 pub fn unset_theme_preview(&mut self) {
709 if let Some(last_theme) = self.last_theme.take() {
710 self.set_theme(last_theme);
712 // None likely occurs when the user types ":theme" and then exits before previewing
715 pub fn set_theme_preview(&mut self, theme: Theme) {
716 self.set_theme_impl(theme, ThemeAction::Preview);
719 pub fn set_theme(&mut self, theme: Theme) {
720 self.set_theme_impl(theme, ThemeAction::Set);
723 fn set_theme_impl(&mut self, theme: Theme, preview: ThemeAction) {
724 // `ui.selection` is the only scope required to be able to render a theme.
725 if theme.find_scope_index("ui.selection").is_none() {
726 self.set_error("Invalid theme: `ui.selection` required");
730 let scopes = theme.scopes();
731 self.syn_loader.set_scopes(scopes.to_vec());
734 ThemeAction::Preview => {
735 let last_theme = std::mem::replace(&mut self.theme, theme);
736 // only insert on first preview: this will be the last theme the user has saved
737 self.last_theme.get_or_insert(last_theme);
739 ThemeAction::Set => {
740 self.last_theme = None;
748 /// Refreshes the language server for a given document
749 pub fn refresh_language_server(&mut self, doc_id: DocumentId) -> Option<()> {
750 let doc = self.documents.get_mut(&doc_id)?;
751 Self::launch_language_server(&mut self.language_servers, doc)
754 /// Launch a language server for a given document
755 fn launch_language_server(ls: &mut helix_lsp::Registry, doc: &mut Document) -> Option<()> {
756 // if doc doesn't have a URL it's a scratch buffer, ignore it
757 let doc_url = doc.url()?;
759 // try to find a language server based on the language name
760 let language_server = doc.language.as_ref().and_then(|language| {
764 "Failed to initialize the LSP for `{}` {{ {} }}",
771 if let Some(language_server) = language_server {
772 // only spawn a new lang server if the servers aren't the same
773 if Some(language_server.id()) != doc.language_server().map(|server| server.id()) {
774 if let Some(language_server) = doc.language_server() {
775 tokio::spawn(language_server.text_document_did_close(doc.identifier()));
778 let language_id = doc.language_id().map(ToOwned::to_owned).unwrap_or_default();
780 // TODO: this now races with on_init code if the init happens too quickly
781 tokio::spawn(language_server.text_document_did_open(
788 doc.set_language_server(Some(language_server));
794 fn _refresh(&mut self) {
795 let config = self.config();
796 for (view, _) in self.tree.views_mut() {
797 let doc = &self.documents[&view.doc];
798 view.ensure_cursor_in_view(doc, config.scrolloff)
802 fn replace_document_in_view(&mut self, current_view: ViewId, doc_id: DocumentId) {
803 let view = self.tree.get_mut(current_view);
805 view.offset = Position::default();
807 let doc = self.documents.get_mut(&doc_id).unwrap();
808 doc.ensure_view_init(view.id);
810 // TODO: reuse align_view
814 .cursor(doc.text().slice(..));
815 let line = doc.text().char_to_line(pos);
816 view.offset.row = line.saturating_sub(view.inner_area().height as usize / 2);
819 pub fn switch(&mut self, id: DocumentId, action: Action) {
820 use crate::tree::Layout;
822 if !self.documents.contains_key(&id) {
823 log::error!("cannot switch to document that does not exist (anymore)");
829 let (view, doc) = current_ref!(self);
830 // If the current view is an empty scratch buffer and is not displayed in any other views, delete it.
831 // Boolean value is determined before the call to `view_mut` because the operation requires a borrow
832 // of `self.tree`, which is mutably borrowed when `view_mut` is called.
833 let remove_empty_scratch = !doc.is_modified()
834 // If the buffer has no path and is not modified, it is an empty scratch buffer.
835 && doc.path().is_none()
836 // If the buffer we are changing to is not this buffer
838 // Ensure the buffer is not displayed in any other splits.
842 .any(|(_, v)| v.doc == doc.id && v.id != view.id);
844 let (view, doc) = current!(self);
845 let view_id = view.id;
847 if remove_empty_scratch {
848 // Copy `doc.id` into a variable before calling `self.documents.remove`, which requires a mutable
849 // borrow, invalidating direct access to `doc.id`.
851 self.documents.remove(&id);
853 // Remove the scratch buffer from any jumplists
854 for (view, _) in self.tree.views_mut() {
855 view.remove_document(&id);
858 let jump = (view.doc, doc.selection(view.id).clone());
859 view.jumps.push(jump);
860 // Set last accessed doc if it is a different document
862 view.add_to_history(view.doc);
863 // Set last modified doc if modified and last modified doc is different
864 if std::mem::take(&mut doc.modified_since_accessed)
865 && view.last_modified_docs[0] != Some(view.doc)
867 view.last_modified_docs = [Some(view.doc), view.last_modified_docs[0]];
872 self.replace_document_in_view(view_id, id);
877 let view_id = view!(self).id;
878 let doc = self.documents.get_mut(&id).unwrap();
879 doc.ensure_view_init(view_id);
882 Action::HorizontalSplit | Action::VerticalSplit => {
883 // copy the current view, unless there is no view yet
886 .try_get(self.tree.focus)
887 .filter(|v| id == v.doc) // Different Document
889 .unwrap_or_else(|| View::new(id, self.config().gutters.clone()));
890 let view_id = self.tree.split(
893 Action::HorizontalSplit => Layout::Horizontal,
894 Action::VerticalSplit => Layout::Vertical,
898 // initialize selection for view
899 let doc = self.documents.get_mut(&id).unwrap();
900 doc.ensure_view_init(view_id);
907 /// Generate an id for a new document and register it.
908 fn new_document(&mut self, mut doc: Document) -> DocumentId {
909 let id = self.next_document_id;
910 // Safety: adding 1 from 1 is fine, probably impossible to reach usize max
911 self.next_document_id =
912 DocumentId(unsafe { NonZeroUsize::new_unchecked(self.next_document_id.0.get() + 1) });
914 self.documents.insert(id, doc);
918 fn new_file_from_document(&mut self, action: Action, doc: Document) -> DocumentId {
919 let id = self.new_document(doc);
920 self.switch(id, action);
924 pub fn new_file(&mut self, action: Action) -> DocumentId {
925 self.new_file_from_document(action, Document::default())
928 pub fn new_file_from_stdin(&mut self, action: Action) -> Result<DocumentId, Error> {
929 let (rope, encoding) = crate::document::from_reader(&mut stdin(), None)?;
930 Ok(self.new_file_from_document(action, Document::from(rope, Some(encoding))))
933 // ??? possible use for integration tests
934 pub fn open(&mut self, path: &Path, action: Action) -> Result<DocumentId, Error> {
935 let path = helix_core::path::get_canonicalized_path(path)?;
936 let id = self.document_by_path(&path).map(|doc| doc.id);
938 let id = if let Some(id) = id {
941 let mut doc = Document::open(&path, None, Some(self.syn_loader.clone()))?;
943 let _ = Self::launch_language_server(&mut self.language_servers, &mut doc);
945 self.new_document(doc)
948 self.switch(id, action);
952 pub fn close(&mut self, id: ViewId) {
953 let view = self.tree.get(self.tree.focus);
955 self.documents.get_mut(&view.doc).unwrap().remove_view(id);
956 self.tree.remove(id);
960 pub fn close_document(&mut self, doc_id: DocumentId, force: bool) -> anyhow::Result<()> {
961 let doc = match self.documents.get(&doc_id) {
963 None => bail!("document does not exist"),
966 if !force && doc.is_modified() {
968 "buffer {:?} is modified",
970 .map(|path| path.to_string_lossy().to_string())
971 .unwrap_or_else(|| SCRATCH_BUFFER_NAME.into())
975 if let Some(language_server) = doc.language_server() {
976 tokio::spawn(language_server.text_document_did_close(doc.identifier()));
981 ReplaceDoc(ViewId, DocumentId),
984 let actions: Vec<Action> = self
987 .filter_map(|(view, _focus)| {
988 view.remove_document(&doc_id);
990 if view.doc == doc_id {
991 // something was previously open in the view, switch to previous doc
992 if let Some(prev_doc) = view.docs_access_history.pop() {
993 Some(Action::ReplaceDoc(view.id, prev_doc))
995 // only the document that is being closed was in the view, close it
996 Some(Action::Close(view.id))
1004 for action in actions {
1006 Action::Close(view_id) => {
1007 self.close(view_id);
1009 Action::ReplaceDoc(view_id, doc_id) => {
1010 self.replace_document_in_view(view_id, doc_id);
1015 self.documents.remove(&doc_id);
1017 // If the document we removed was visible in all views, we will have no more views. We don't
1018 // want to close the editor just for a simple buffer close, so we need to create a new view
1019 // containing either an existing document, or a brand new document.
1020 if self.tree.views().next().is_none() {
1024 .map(|(&doc_id, _)| doc_id)
1026 .unwrap_or_else(|| self.new_document(Document::default()));
1027 let view = View::new(doc_id, self.config().gutters.clone());
1028 let view_id = self.tree.insert(view);
1029 let doc = self.documents.get_mut(&doc_id).unwrap();
1030 doc.ensure_view_init(view_id);
1038 pub fn resize(&mut self, area: Rect) {
1039 if self.tree.resize(area) {
1044 pub fn focus_next(&mut self) {
1045 self.tree.focus_next();
1048 pub fn focus_right(&mut self) {
1049 self.tree.focus_direction(tree::Direction::Right);
1052 pub fn focus_left(&mut self) {
1053 self.tree.focus_direction(tree::Direction::Left);
1056 pub fn focus_up(&mut self) {
1057 self.tree.focus_direction(tree::Direction::Up);
1060 pub fn focus_down(&mut self) {
1061 self.tree.focus_direction(tree::Direction::Down);
1064 pub fn swap_right(&mut self) {
1065 self.tree.swap_split_in_direction(tree::Direction::Right);
1068 pub fn swap_left(&mut self) {
1069 self.tree.swap_split_in_direction(tree::Direction::Left);
1072 pub fn swap_up(&mut self) {
1073 self.tree.swap_split_in_direction(tree::Direction::Up);
1076 pub fn swap_down(&mut self) {
1077 self.tree.swap_split_in_direction(tree::Direction::Down);
1080 pub fn transpose_view(&mut self) {
1081 self.tree.transpose();
1084 pub fn should_close(&self) -> bool {
1085 self.tree.is_empty()
1088 pub fn ensure_cursor_in_view(&mut self, id: ViewId) {
1089 let config = self.config();
1090 let view = self.tree.get_mut(id);
1091 let doc = &self.documents[&view.doc];
1092 view.ensure_cursor_in_view(doc, config.scrolloff)
1096 pub fn document(&self, id: DocumentId) -> Option<&Document> {
1097 self.documents.get(&id)
1101 pub fn document_mut(&mut self, id: DocumentId) -> Option<&mut Document> {
1102 self.documents.get_mut(&id)
1106 pub fn documents(&self) -> impl Iterator<Item = &Document> {
1107 self.documents.values()
1111 pub fn documents_mut(&mut self) -> impl Iterator<Item = &mut Document> {
1112 self.documents.values_mut()
1115 pub fn document_by_path<P: AsRef<Path>>(&self, path: P) -> Option<&Document> {
1117 .find(|doc| doc.path().map(|p| p == path.as_ref()).unwrap_or(false))
1120 pub fn document_by_path_mut<P: AsRef<Path>>(&mut self, path: P) -> Option<&mut Document> {
1121 self.documents_mut()
1122 .find(|doc| doc.path().map(|p| p == path.as_ref()).unwrap_or(false))
1125 pub fn cursor(&self) -> (Option<Position>, CursorKind) {
1126 let config = self.config();
1127 let (view, doc) = current_ref!(self);
1131 .cursor(doc.text().slice(..));
1132 if let Some(mut pos) = view.screen_coords_at_pos(doc, doc.text().slice(..), cursor) {
1133 let inner = view.inner_area();
1134 pos.col += inner.x as usize;
1135 pos.row += inner.y as usize;
1136 let cursorkind = config.cursor_shape.from_mode(doc.mode());
1137 (Some(pos), cursorkind)
1139 (None, CursorKind::default())
1143 /// Closes language servers with timeout. The default timeout is 500 ms, use
1144 /// `timeout` parameter to override this.
1145 pub async fn close_language_servers(
1147 timeout: Option<u64>,
1148 ) -> Result<(), tokio::time::error::Elapsed> {
1149 tokio::time::timeout(
1150 Duration::from_millis(timeout.unwrap_or(500)),
1152 self.language_servers
1154 .map(|client| client.force_shutdown()),
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list