3 clipboard::{get_clipboard_provider, ClipboardProvider},
4 document::{DocumentSavedEventFuture, DocumentSavedEventResult, Mode},
5 graphics::{CursorKind, Rect},
11 Align, Document, DocumentId, View, ViewId,
13use helix_vcs::DiffProviderRegistry;
15use futures_util::stream::select_all::SelectAll;
16use futures_util::{future, StreamExt};
18use tokio_stream::wrappers::UnboundedReceiverStream;
23 collections::{BTreeMap, HashMap},
26 path::{Path, PathBuf},
33 mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender},
36 time::{sleep, Duration, Instant, Sleep},
39use anyhow::{anyhow, bail, Error};
41pub use helix_core::diagnostic::Severity;
42pub use helix_core::register::Registers;
43use helix_core::Position;
45 auto_pairs::AutoPairs,
46 syntax::{self, AutoPairConfig},
52use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer};
54use arc_swap::access::{DynAccess, DynGuard};
56fn deserialize_duration_millis<'de, D>(deserializer: D) -> Result<Duration, D::Error>
58 D: serde::Deserializer<'de>,
60 let millis = u64::deserialize(deserializer)?;
61 Ok(Duration::from_millis(millis))
64fn serialize_duration_millis<S>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error>
68 serializer.serialize_u64(
72 .map_err(|_| serde::ser::Error::custom("duration value overflowed u64"))?,
76#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
77#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
78pub struct GutterConfig {
80 pub layout: Vec<GutterType>,
81 /// Options specific to the "line-numbers" gutter
82 pub line_numbers: GutterLineNumbersConfig,
85impl Default for GutterConfig {
86 fn default() -> Self {
89 GutterType::Diagnostics,
91 GutterType::LineNumbers,
95 line_numbers: GutterLineNumbersConfig::default(),
100impl From<Vec<GutterType>> for GutterConfig {
101 fn from(x: Vec<GutterType>) -> Self {
109fn deserialize_gutter_seq_or_struct<'de, D>(deserializer: D) -> Result<GutterConfig, D::Error>
111 D: Deserializer<'de>,
113 struct GutterVisitor;
115 impl<'de> serde::de::Visitor<'de> for GutterVisitor {
116 type Value = GutterConfig;
118 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
121 "an array of gutter names or a detailed gutter configuration"
125 fn visit_seq<S>(self, mut seq: S) -> Result<Self::Value, S::Error>
127 S: serde::de::SeqAccess<'de>,
129 let mut gutters = Vec::new();
130 while let Some(gutter) = seq.next_element::<String>()? {
133 .parse::<GutterType>()
134 .map_err(serde::de::Error::custom)?,
141 fn visit_map<M>(self, map: M) -> Result<Self::Value, M::Error>
143 M: serde::de::MapAccess<'de>,
145 let deserializer = serde::de::value::MapAccessDeserializer::new(map);
146 Deserialize::deserialize(deserializer)
150 deserializer.deserialize_any(GutterVisitor)
153#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
154#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
155pub struct GutterLineNumbersConfig {
156 /// Minimum number of characters to use for line number gutter. Defaults to 3.
157 pub min_width: usize,
160impl Default for GutterLineNumbersConfig {
161 fn default() -> Self {
162 Self { min_width: 3 }
166#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
167#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
168pub struct FilePickerConfig {
170 /// Enables ignoring hidden files.
171 /// Whether to hide hidden files in file picker and global search results. Defaults to true.
173 /// Enables following symlinks.
174 /// Whether to follow symbolic links in file picker and file or directory completions. Defaults to true.
175 pub follow_symlinks: bool,
176 /// Hides symlinks that point into the current directory. Defaults to true.
177 pub deduplicate_links: bool,
178 /// Enables reading ignore files from parent directories. Defaults to true.
180 /// Enables reading `.ignore` files.
181 /// Whether to hide files listed in .ignore in file picker and global search results. Defaults to true.
183 /// Enables reading `.gitignore` files.
184 /// Whether to hide files listed in .gitignore in file picker and global search results. Defaults to true.
185 pub git_ignore: bool,
186 /// Enables reading global .gitignore, whose path is specified in git's config: `core.excludefile` option.
187 /// Whether to hide files listed in global .gitignore in file picker and global search results. Defaults to true.
188 pub git_global: bool,
189 /// Enables reading `.git/info/exclude` files.
190 /// Whether to hide files listed in .git/info/exclude in file picker and global search results. Defaults to true.
191 pub git_exclude: bool,
192 /// WalkBuilder options
193 /// Maximum Depth to recurse directories in file picker and global search. Defaults to `None`.
194 pub max_depth: Option<usize>,
197impl Default for FilePickerConfig {
198 fn default() -> Self {
201 follow_symlinks: true,
202 deduplicate_links: true,
213#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
214#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
216 /// Padding to keep between the edge of the screen and the cursor when scrolling. Defaults to 5.
217 pub scrolloff: usize,
218 /// Number of lines to scroll at once. Defaults to 3
219 pub scroll_lines: isize,
220 /// Mouse support. Defaults to true.
222 /// Shell to use for shell commands. Defaults to ["cmd", "/C"] on Windows and ["sh", "-c"] otherwise.
223 pub shell: Vec<String>,
224 /// Line number mode.
225 pub line_number: LineNumber,
226 /// Highlight the lines cursors are currently on. Defaults to false.
227 pub cursorline: bool,
228 /// Highlight the columns cursors are currently on. Defaults to false.
229 pub cursorcolumn: bool,
230 #[serde(deserialize_with = "deserialize_gutter_seq_or_struct")]
231 pub gutters: GutterConfig,
232 /// Middle click paste support. Defaults to true.
233 pub middle_click_paste: bool,
234 /// Automatic insertion of pairs to parentheses, brackets,
235 /// etc. Optionally, this can be a list of 2-tuples to specify a
236 /// global list of characters to pair. Defaults to true.
237 pub auto_pairs: AutoPairConfig,
238 /// Automatic auto-completion, automatically pop up without user trigger. Defaults to true.
239 pub auto_completion: bool,
240 /// Automatic formatting on save. Defaults to true.
241 pub auto_format: bool,
242 /// Automatic save on focus lost. Defaults to false.
244 /// Time in milliseconds since last keypress before idle timers trigger.
245 /// Used for autocompletion, set to 0 for instant. Defaults to 400ms.
247 serialize_with = "serialize_duration_millis",
248 deserialize_with = "deserialize_duration_millis"
250 pub idle_timeout: Duration,
251 pub completion_trigger_len: u8,
252 /// Whether to display infoboxes. Defaults to true.
254 pub file_picker: FilePickerConfig,
255 /// Configuration of the statusline elements
256 pub statusline: StatusLineConfig,
257 /// Shape for cursor in each mode
258 pub cursor_shape: CursorShapeConfig,
259 /// Set to `true` to override automatic detection of terminal truecolor support in the event of a false negative. Defaults to `false`.
260 pub true_color: bool,
261 /// Search configuration.
263 pub search: SearchConfig,
265 pub terminal: Option<TerminalConfig>,
266 /// Column numbers at which to draw the rulers. Default to `[]`, meaning no rulers.
267 pub rulers: Vec<u16>,
269 pub whitespace: WhitespaceConfig,
270 /// Persistently display open buffers along the top
271 pub bufferline: BufferLine,
272 /// Vertical indent width guides.
273 pub indent_guides: IndentGuidesConfig,
274 /// Whether to color modes with different colors. Defaults to `false`.
275 pub color_modes: bool,
276 pub soft_wrap: SoftWrap,
279#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
280#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
282 /// Soft wrap lines that exceed viewport width. Default to off
284 /// Maximum space left free at the end of the line.
285 /// This space is used to wrap text at word boundaries. If that is not possible within this limit
286 /// the word is simply split at the end of the line.
288 /// This is automatically hard-limited to a quarter of the viewport to ensure correct display on small views.
292 /// Maximum number of indentation that can be carried over from the previous line when softwrapping.
293 /// If a line is indented further then this limit it is rendered at the start of the viewport instead.
295 /// This is automatically hard-limited to a quarter of the viewport to ensure correct display on small views.
298 pub max_indent_retain: u16,
299 /// Indicator placed at the beginning of softwrapped lines
302 pub wrap_indicator: String,
305impl Default for SoftWrap {
306 fn default() -> Self {
310 max_indent_retain: 40,
311 wrap_indicator: "↪ ".into(),
316#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
317#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
318pub struct TerminalConfig {
321 #[serde(skip_serializing_if = "Vec::is_empty")]
322 pub args: Vec<String>,
326pub fn get_terminal_provider() -> Option<TerminalConfig> {
327 use crate::env::binary_exists;
329 if binary_exists("wt") {
330 return Some(TerminalConfig {
331 command: "wt".to_string(),
333 "new-tab".to_string(),
334 "--title".to_string(),
342 Some(TerminalConfig {
343 command: "conhost".to_string(),
344 args: vec!["cmd".to_string(), "/C".to_string()],
348#[cfg(not(any(windows, target_os = "wasm32")))]
349pub fn get_terminal_provider() -> Option<TerminalConfig> {
350 use crate::env::{binary_exists, env_var_is_set};
352 if env_var_is_set("TMUX") && binary_exists("tmux") {
353 return Some(TerminalConfig {
354 command: "tmux".to_string(),
355 args: vec!["split-window".to_string()],
359 if env_var_is_set("WEZTERM_UNIX_SOCKET") && binary_exists("wezterm") {
360 return Some(TerminalConfig {
361 command: "wezterm".to_string(),
362 args: vec!["cli".to_string(), "split-pane".to_string()],
369#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
370#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
371pub struct LspConfig {
374 /// Display LSP progress messages below statusline
375 pub display_messages: bool,
376 /// Enable automatic pop up of signature help (parameter hints)
377 pub auto_signature_help: bool,
378 /// Display docs under signature help popup
379 pub display_signature_help_docs: bool,
382impl Default for LspConfig {
383 fn default() -> Self {
386 display_messages: false,
387 auto_signature_help: true,
388 display_signature_help_docs: true,
393#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
394#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
395pub struct SearchConfig {
396 /// Smart case: Case insensitive searching unless pattern contains upper case characters. Defaults to true.
397 pub smart_case: bool,
398 /// Whether the search should wrap after depleting the matches. Default to true.
399 pub wrap_around: bool,
402#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
403#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
404pub struct StatusLineConfig {
405 pub left: Vec<StatusLineElement>,
406 pub center: Vec<StatusLineElement>,
407 pub right: Vec<StatusLineElement>,
408 pub separator: String,
409 pub mode: ModeConfig,
412impl Default for StatusLineConfig {
413 fn default() -> Self {
414 use StatusLineElement as E;
421 E::FileModificationIndicator,
424 right: vec![E::Diagnostics, E::Selections, E::Position, E::FileEncoding],
425 separator: String::from("│"),
426 mode: ModeConfig::default(),
431#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
432#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
433pub struct ModeConfig {
439impl Default for ModeConfig {
440 fn default() -> Self {
442 normal: String::from("NOR"),
443 insert: String::from("INS"),
444 select: String::from("SEL"),
449#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
450#[serde(rename_all = "kebab-case")]
451pub enum StatusLineElement {
452 /// The editor mode (Normal, Insert, Visual/Selection)
455 /// The LSP activity spinner
458 /// The base file name, including a dirty flag if it's unsaved
461 /// The relative file path, including a dirty flag if it's unsaved
464 // The file modification indicator
465 FileModificationIndicator,
467 /// The file encoding
470 /// The file line endings (CRLF or LF)
473 /// The file type (language ID or "text")
476 /// A summary of the number of errors and warnings
479 /// A summary of the number of errors and warnings on file and workspace
480 WorkspaceDiagnostics,
482 /// The number of selections (cursors)
485 /// The number of characters currently in primary selection
486 PrimarySelectionLength,
488 /// The cursor position
491 /// The separator string
494 /// The cursor position as a percent of the total file
497 /// The total line numbers of the current file
504// Cursor shape is read and used on every rendered frame and so needs
505// to be fast. Therefore we avoid a hashmap and use an enum indexed array.
506#[derive(Debug, Clone, PartialEq, Eq)]
507pub struct CursorShapeConfig([CursorKind; 3]);
509impl CursorShapeConfig {
510 pub fn from_mode(&self, mode: Mode) -> CursorKind {
511 self.get(mode as usize).copied().unwrap_or_default()
515impl<'de> Deserialize<'de> for CursorShapeConfig {
516 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
518 D: Deserializer<'de>,
520 let m = HashMap::<Mode, CursorKind>::deserialize(deserializer)?;
521 let into_cursor = |mode: Mode| m.get(&mode).copied().unwrap_or_default();
522 Ok(CursorShapeConfig([
523 into_cursor(Mode::Normal),
524 into_cursor(Mode::Select),
525 into_cursor(Mode::Insert),
530impl Serialize for CursorShapeConfig {
531 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
533 S: serde::Serializer,
535 let mut map = serializer.serialize_map(Some(self.len()))?;
536 let modes = [Mode::Normal, Mode::Select, Mode::Insert];
538 map.serialize_entry(&mode, &self.from_mode(mode))?;
544impl std::ops::Deref for CursorShapeConfig {
545 type Target = [CursorKind; 3];
547 fn deref(&self) -> &Self::Target {
552impl Default for CursorShapeConfig {
553 fn default() -> Self {
554 Self([CursorKind::Block; 3])
558/// bufferline render modes
559#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
560#[serde(rename_all = "kebab-case")]
562 /// Don't render bufferline
566 /// Only if multiple buffers are open
570impl Default for BufferLine {
571 fn default() -> Self {
576#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
577#[serde(rename_all = "kebab-case")]
579 /// Show absolute line number
582 /// If focused and in normal/select mode, show relative line number to the primary cursor.
583 /// If unfocused or in insert mode, show absolute line number.
587impl std::str::FromStr for LineNumber {
588 type Err = anyhow::Error;
590 fn from_str(s: &str) -> Result<Self, Self::Err> {
591 match s.to_lowercase().as_str() {
592 "absolute" | "abs" => Ok(Self::Absolute),
593 "relative" | "rel" => Ok(Self::Relative),
594 _ => anyhow::bail!("Line number can only be `absolute` or `relative`."),
599#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
600#[serde(rename_all = "kebab-case")]
602 /// Show diagnostics and other features like breakpoints
604 /// Show line numbers
606 /// Show one blank space
608 /// Highlight local changes
612impl std::str::FromStr for GutterType {
613 type Err = anyhow::Error;
615 fn from_str(s: &str) -> Result<Self, Self::Err> {
616 match s.to_lowercase().as_str() {
617 "diagnostics" => Ok(Self::Diagnostics),
618 "spacer" => Ok(Self::Spacer),
619 "line-numbers" => Ok(Self::LineNumbers),
620 "diff" => Ok(Self::Diff),
621 _ => anyhow::bail!("Gutter type can only be `diagnostics` or `line-numbers`."),
626#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
628pub struct WhitespaceConfig {
629 pub render: WhitespaceRender,
630 pub characters: WhitespaceCharacters,
633impl Default for WhitespaceConfig {
634 fn default() -> Self {
636 render: WhitespaceRender::Basic(WhitespaceRenderValue::None),
637 characters: WhitespaceCharacters::default(),
642#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
643#[serde(untagged, rename_all = "kebab-case")]
644pub enum WhitespaceRender {
645 Basic(WhitespaceRenderValue),
647 default: Option<WhitespaceRenderValue>,
648 space: Option<WhitespaceRenderValue>,
649 nbsp: Option<WhitespaceRenderValue>,
650 tab: Option<WhitespaceRenderValue>,
651 newline: Option<WhitespaceRenderValue>,
655#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
656#[serde(rename_all = "kebab-case")]
657pub enum WhitespaceRenderValue {
664impl WhitespaceRender {
665 pub fn space(&self) -> WhitespaceRenderValue {
667 Self::Basic(val) => val,
668 Self::Specific { default, space, .. } => {
669 space.or(default).unwrap_or(WhitespaceRenderValue::None)
673 pub fn nbsp(&self) -> WhitespaceRenderValue {
675 Self::Basic(val) => val,
676 Self::Specific { default, nbsp, .. } => {
677 nbsp.or(default).unwrap_or(WhitespaceRenderValue::None)
681 pub fn tab(&self) -> WhitespaceRenderValue {
683 Self::Basic(val) => val,
684 Self::Specific { default, tab, .. } => {
685 tab.or(default).unwrap_or(WhitespaceRenderValue::None)
689 pub fn newline(&self) -> WhitespaceRenderValue {
691 Self::Basic(val) => val,
694 } => newline.or(default).unwrap_or(WhitespaceRenderValue::None),
699#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
701pub struct WhitespaceCharacters {
709impl Default for WhitespaceCharacters {
710 fn default() -> Self {
712 space: '·', // U+00B7
715 newline: '⏎', // U+23CE
721#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
722#[serde(default, rename_all = "kebab-case")]
723pub struct IndentGuidesConfig {
729impl Default for IndentGuidesConfig {
730 fn default() -> Self {
739impl Default for Config {
740 fn default() -> Self {
745 shell: if cfg!(windows) {
746 vec!["cmd".to_owned(), "/C".to_owned()]
748 vec!["sh".to_owned(), "-c".to_owned()]
750 line_number: LineNumber::Absolute,
753 gutters: GutterConfig::default(),
754 middle_click_paste: true,
755 auto_pairs: AutoPairConfig::default(),
756 auto_completion: true,
759 idle_timeout: Duration::from_millis(400),
760 completion_trigger_len: 2,
762 file_picker: FilePickerConfig::default(),
763 statusline: StatusLineConfig::default(),
764 cursor_shape: CursorShapeConfig::default(),
766 search: SearchConfig::default(),
767 lsp: LspConfig::default(),
768 terminal: get_terminal_provider(),
770 whitespace: WhitespaceConfig::default(),
771 bufferline: BufferLine::default(),
772 indent_guides: IndentGuidesConfig::default(),
774 soft_wrap: SoftWrap::default(),
779impl Default for SearchConfig {
780 fn default() -> Self {
788pub struct Motion(pub Box<dyn Fn(&mut Editor)>);
790 pub fn run(&self, e: &mut Editor) {
794impl std::fmt::Debug for Motion {
795 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
796 f.write_str("motion")
800#[derive(Debug, Clone, Default)]
801pub struct Breakpoint {
802 pub id: Option<usize>,
804 pub message: Option<String>,
807 pub column: Option<usize>,
808 pub condition: Option<String>,
809 pub hit_condition: Option<String>,
810 pub log_message: Option<String>,
813use futures_util::stream::{Flatten, Once};
816 /// Current editing mode.
819 pub next_document_id: DocumentId,
820 pub documents: BTreeMap<DocumentId, Document>,
822 // We Flatten<> to resolve the inner DocumentSavedEventFuture. For that we need a stream of streams, hence the Once<>.
823 // https://stackoverflow.com/a/66875668
824 pub saves: HashMap<DocumentId, UnboundedSender<Once<DocumentSavedEventFuture>>>,
825 pub save_queue: SelectAll<Flatten<UnboundedReceiverStream<Once<DocumentSavedEventFuture>>>>,
826 pub write_count: usize,
828 pub count: Option<std::num::NonZeroUsize>,
829 pub selected_register: Option<char>,
830 pub registers: Registers,
831 pub macro_recording: Option<(char, Vec<KeyEvent>)>,
832 pub macro_replaying: Vec<char>,
833 pub language_servers: helix_lsp::Registry,
834 pub diagnostics: BTreeMap<lsp::Url, Vec<lsp::Diagnostic>>,
835 pub diff_providers: DiffProviderRegistry,
837 pub debugger: Option<dap::Client>,
838 pub debugger_events: SelectAll<UnboundedReceiverStream<dap::Payload>>,
839 pub breakpoints: HashMap<PathBuf, Vec<Breakpoint>>,
841 pub clipboard_provider: Box<dyn ClipboardProvider>,
843 pub syn_loader: Arc<syntax::Loader>,
844 pub theme_loader: Arc<theme::Loader>,
845 /// last_theme is used for theme previews. We store the current theme here,
846 /// and if previewing is cancelled, we can return to it.
847 pub last_theme: Option<Theme>,
848 /// The currently applied editor theme. While previewing a theme, the previewed theme
851 pub last_line_number: Option<usize>,
852 pub status_msg: Option<(Cow<'static, str>, Severity)>,
853 pub autoinfo: Option<Info>,
855 pub config: Arc<dyn DynAccess<Config>>,
856 pub auto_pairs: Option<AutoPairs>,
858 pub idle_timer: Pin<Box<Sleep>>,
859 pub last_motion: Option<Motion>,
861 pub last_completion: Option<CompleteAction>,
865 pub config_events: (UnboundedSender<ConfigEvent>, UnboundedReceiver<ConfigEvent>),
866 /// Allows asynchronous tasks to control the rendering
867 /// The `Notify` allows asynchronous tasks to request the editor to perform a redraw
868 /// The `RwLock` blocks the editor from performing the render until an exclusive lock can be aquired
869 pub redraw_handle: RedrawHandle,
870 pub needs_redraw: bool,
871 /// Cached position of the cursor calculated during rendering.
872 /// The content of `cursor_cache` is returned by `Editor::cursor` if
873 /// set to `Some(_)`. The value will be cleared after it's used.
874 /// If `cursor_cache` is `None` then the `Editor::cursor` function will
875 /// calculate the cursor position.
877 /// `Some(None)` represents a cursor position outside of the visible area.
878 /// This will just cause `Editor::cursor` to return `None`.
880 /// This cache is only a performance optimization to
881 /// avoid calculating the cursor position multiple
882 /// times during rendering and should not be set by other functions.
883 pub cursor_cache: Cell<Option<Option<Position>>>,
886pub type RedrawHandle = (Arc<Notify>, Arc<RwLock<()>>);
889pub enum EditorEvent {
890 DocumentSaved(DocumentSavedEventResult),
891 ConfigEvent(ConfigEvent),
892 LanguageServerMessage((usize, Call)),
893 DebuggerEvent(dap::Payload),
897#[derive(Debug, Clone)]
898pub enum ConfigEvent {
908#[derive(Debug, Clone)]
909pub struct CompleteAction {
910 pub trigger_offset: usize,
911 pub changes: Vec<Change>,
914#[derive(Debug, Copy, Clone)]
922/// Error thrown on failed document closed
924 /// Document doesn't exist
926 /// Buffer is modified
927 BufferModified(String),
928 /// Document failed to save
929 SaveError(anyhow::Error),
935 theme_loader: Arc<theme::Loader>,
936 syn_loader: Arc<syntax::Loader>,
937 config: Arc<dyn DynAccess<Config>>,
939 let conf = config.load();
940 let auto_pairs = (&conf.auto_pairs).into();
942 // HAXX: offset the render area height by 1 to account for prompt/commandline
947 tree: Tree::new(area),
948 next_document_id: DocumentId::default(),
949 documents: BTreeMap::new(),
950 saves: HashMap::new(),
951 save_queue: SelectAll::new(),
954 selected_register: None,
955 macro_recording: None,
956 macro_replaying: Vec::new(),
957 theme: theme_loader.default(),
958 language_servers: helix_lsp::Registry::new(),
959 diagnostics: BTreeMap::new(),
960 diff_providers: DiffProviderRegistry::default(),
962 debugger_events: SelectAll::new(),
963 breakpoints: HashMap::new(),
967 last_line_number: None,
968 registers: Registers::default(),
969 clipboard_provider: get_clipboard_provider(),
972 idle_timer: Box::pin(sleep(conf.idle_timeout)),
974 last_completion: None,
978 config_events: unbounded_channel(),
979 redraw_handle: Default::default(),
981 cursor_cache: Cell::new(None),
985 /// Current editing mode for the [`Editor`].
986 pub fn mode(&self) -> Mode {
990 pub fn config(&self) -> DynGuard<Config> {
994 /// Call if the config has changed to let the editor update all
995 /// relevant members.
996 pub fn refresh_config(&mut self) {
997 let config = self.config();
998 self.auto_pairs = (&config.auto_pairs).into();
999 self.reset_idle_timer();
1003 pub fn clear_idle_timer(&mut self) {
1004 // equivalent to internal Instant::far_future() (30 years)
1007 .reset(Instant::now() + Duration::from_secs(86400 * 365 * 30));
1010 pub fn reset_idle_timer(&mut self) {
1011 let config = self.config();
1014 .reset(Instant::now() + config.idle_timeout);
1017 pub fn clear_status(&mut self) {
1018 self.status_msg = None;
1022 pub fn set_status<T: Into<Cow<'static, str>>>(&mut self, status: T) {
1023 let status = status.into();
1024 log::debug!("editor status: {}", status);
1025 self.status_msg = Some((status, Severity::Info));
1029 pub fn set_error<T: Into<Cow<'static, str>>>(&mut self, error: T) {
1030 let error = error.into();
1031 log::error!("editor error: {}", error);
1032 self.status_msg = Some((error, Severity::Error));
1036 pub fn get_status(&self) -> Option<(&Cow<'static, str>, &Severity)> {
1037 self.status_msg.as_ref().map(|(status, sev)| (status, sev))
1040 /// Returns true if the current status is an error
1042 pub fn is_err(&self) -> bool {
1045 .map(|(_, sev)| *sev == Severity::Error)
1049 pub fn unset_theme_preview(&mut self) {
1050 if let Some(last_theme) = self.last_theme.take() {
1051 self.set_theme(last_theme);
1053 // None likely occurs when the user types ":theme" and then exits before previewing
1056 pub fn set_theme_preview(&mut self, theme: Theme) {
1057 self.set_theme_impl(theme, ThemeAction::Preview);
1060 pub fn set_theme(&mut self, theme: Theme) {
1061 self.set_theme_impl(theme, ThemeAction::Set);
1064 fn set_theme_impl(&mut self, theme: Theme, preview: ThemeAction) {
1065 // `ui.selection` is the only scope required to be able to render a theme.
1066 if theme.find_scope_index_exact("ui.selection").is_none() {
1067 self.set_error("Invalid theme: `ui.selection` required");
1071 let scopes = theme.scopes();
1072 self.syn_loader.set_scopes(scopes.to_vec());
1075 ThemeAction::Preview => {
1076 let last_theme = std::mem::replace(&mut self.theme, theme);
1077 // only insert on first preview: this will be the last theme the user has saved
1078 self.last_theme.get_or_insert(last_theme);
1080 ThemeAction::Set => {
1081 self.last_theme = None;
1089 /// Refreshes the language server for a given document
1090 pub fn refresh_language_server(&mut self, doc_id: DocumentId) -> Option<()> {
1091 self.launch_language_server(doc_id)
1094 /// Launch a language server for a given document
1095 fn launch_language_server(&mut self, doc_id: DocumentId) -> Option<()> {
1096 if !self.config().lsp.enable {
1100 // if doc doesn't have a URL it's a scratch buffer, ignore it
1101 let (lang, path) = {
1102 let doc = self.document(doc_id)?;
1103 (doc.language.clone(), doc.path().cloned())
1106 // try to find a language server based on the language name
1107 let language_server = lang.as_ref().and_then(|language| {
1108 self.language_servers
1109 .get(language, path.as_ref())
1112 "Failed to initialize the LSP for `{}` {{ {} }}",
1121 let doc = self.document_mut(doc_id)?;
1122 let doc_url = doc.url()?;
1124 if let Some(language_server) = language_server {
1125 // only spawn a new lang server if the servers aren't the same
1126 if Some(language_server.id()) != doc.language_server().map(|server| server.id()) {
1127 if let Some(language_server) = doc.language_server() {
1128 tokio::spawn(language_server.text_document_did_close(doc.identifier()));
1131 let language_id = doc.language_id().map(ToOwned::to_owned).unwrap_or_default();
1133 // TODO: this now races with on_init code if the init happens too quickly
1134 tokio::spawn(language_server.text_document_did_open(
1141 doc.set_language_server(Some(language_server));
1147 fn _refresh(&mut self) {
1148 let config = self.config();
1149 for (view, _) in self.tree.views_mut() {
1150 let doc = doc_mut!(self, &view.doc);
1151 view.sync_changes(doc);
1152 view.gutters = config.gutters.clone();
1153 view.ensure_cursor_in_view(doc, config.scrolloff)
1157 fn replace_document_in_view(&mut self, current_view: ViewId, doc_id: DocumentId) {
1158 let view = self.tree.get_mut(current_view);
1160 view.offset = ViewPosition::default();
1162 let doc = doc_mut!(self, &doc_id);
1163 doc.ensure_view_init(view.id);
1164 view.sync_changes(doc);
1166 align_view(doc, view, Align::Center);
1169 pub fn switch(&mut self, id: DocumentId, action: Action) {
1170 use crate::tree::Layout;
1172 if !self.documents.contains_key(&id) {
1173 log::error!("cannot switch to document that does not exist (anymore)");
1177 self.enter_normal_mode();
1180 Action::Replace => {
1181 let (view, doc) = current_ref!(self);
1182 // If the current view is an empty scratch buffer and is not displayed in any other views, delete it.
1183 // Boolean value is determined before the call to `view_mut` because the operation requires a borrow
1184 // of `self.tree`, which is mutably borrowed when `view_mut` is called.
1185 let remove_empty_scratch = !doc.is_modified()
1186 // If the buffer has no path and is not modified, it is an empty scratch buffer.
1187 && doc.path().is_none()
1188 // If the buffer we are changing to is not this buffer
1190 // Ensure the buffer is not displayed in any other splits.
1194 .any(|(_, v)| v.doc == doc.id && v.id != view.id);
1196 let (view, doc) = current!(self);
1197 let view_id = view.id;
1199 // Append any outstanding changes to history in the old document.
1200 doc.append_changes_to_history(view);
1202 if remove_empty_scratch {
1203 // Copy `doc.id` into a variable before calling `self.documents.remove`, which requires a mutable
1204 // borrow, invalidating direct access to `doc.id`.
1206 self.documents.remove(&id);
1208 // Remove the scratch buffer from any jumplists
1209 for (view, _) in self.tree.views_mut() {
1210 view.remove_document(&id);
1213 let jump = (view.doc, doc.selection(view.id).clone());
1214 view.jumps.push(jump);
1215 // Set last accessed doc if it is a different document
1217 view.add_to_history(view.doc);
1218 // Set last modified doc if modified and last modified doc is different
1219 if std::mem::take(&mut doc.modified_since_accessed)
1220 && view.last_modified_docs[0] != Some(view.doc)
1222 view.last_modified_docs = [Some(view.doc), view.last_modified_docs[0]];
1227 self.replace_document_in_view(view_id, id);
1232 let view_id = view!(self).id;
1233 let doc = doc_mut!(self, &id);
1234 doc.ensure_view_init(view_id);
1237 Action::HorizontalSplit | Action::VerticalSplit => {
1238 // copy the current view, unless there is no view yet
1241 .try_get(self.tree.focus)
1242 .filter(|v| id == v.doc) // Different Document
1244 .unwrap_or_else(|| View::new(id, self.config().gutters.clone()));
1245 let view_id = self.tree.split(
1248 Action::HorizontalSplit => Layout::Horizontal,
1249 Action::VerticalSplit => Layout::Vertical,
1250 _ => unreachable!(),
1253 // initialize selection for view
1254 let doc = doc_mut!(self, &id);
1255 doc.ensure_view_init(view_id);
1262 /// Generate an id for a new document and register it.
1263 fn new_document(&mut self, mut doc: Document) -> DocumentId {
1264 let id = self.next_document_id;
1265 // Safety: adding 1 from 1 is fine, probably impossible to reach usize max
1266 self.next_document_id =
1267 DocumentId(unsafe { NonZeroUsize::new_unchecked(self.next_document_id.0.get() + 1) });
1269 self.documents.insert(id, doc);
1271 let (save_sender, save_receiver) = tokio::sync::mpsc::unbounded_channel();
1272 self.saves.insert(id, save_sender);
1274 let stream = UnboundedReceiverStream::new(save_receiver).flatten();
1275 self.save_queue.push(stream);
1280 fn new_file_from_document(&mut self, action: Action, doc: Document) -> DocumentId {
1281 let id = self.new_document(doc);
1282 self.switch(id, action);
1286 pub fn new_file(&mut self, action: Action) -> DocumentId {
1287 self.new_file_from_document(action, Document::default(self.config.clone()))
1290 pub fn new_file_from_stdin(&mut self, action: Action) -> Result<DocumentId, Error> {
1291 let (rope, encoding) = crate::document::from_reader(&mut stdin(), None)?;
1292 Ok(self.new_file_from_document(
1294 Document::from(rope, Some(encoding), self.config.clone()),
1298 // ??? possible use for integration tests
1299 pub fn open(&mut self, path: &Path, action: Action) -> Result<DocumentId, Error> {
1300 let path = helix_core::path::get_canonicalized_path(path)?;
1301 let id = self.document_by_path(&path).map(|doc| doc.id);
1303 let id = if let Some(id) = id {
1306 let mut doc = Document::open(
1309 Some(self.syn_loader.clone()),
1310 self.config.clone(),
1313 if let Some(diff_base) = self.diff_providers.get_diff_base(&path) {
1314 doc.set_diff_base(diff_base, self.redraw_handle.clone());
1317 let id = self.new_document(doc);
1318 let _ = self.launch_language_server(id);
1323 self.switch(id, action);
1327 pub fn close(&mut self, id: ViewId) {
1328 // Remove selections for the closed view on all documents.
1329 for doc in self.documents_mut() {
1330 doc.remove_view(id);
1332 self.tree.remove(id);
1336 pub fn close_document(&mut self, doc_id: DocumentId, force: bool) -> Result<(), CloseError> {
1337 let doc = match self.documents.get_mut(&doc_id) {
1339 None => return Err(CloseError::DoesNotExist),
1341 if !force && doc.is_modified() {
1342 return Err(CloseError::BufferModified(doc.display_name().into_owned()));
1345 // This will also disallow any follow-up writes
1346 self.saves.remove(&doc_id);
1348 if let Some(language_server) = doc.language_server() {
1349 // TODO: track error
1350 tokio::spawn(language_server.text_document_did_close(doc.identifier()));
1355 ReplaceDoc(ViewId, DocumentId),
1358 let actions: Vec<Action> = self
1361 .filter_map(|(view, _focus)| {
1362 view.remove_document(&doc_id);
1364 if view.doc == doc_id {
1365 // something was previously open in the view, switch to previous doc
1366 if let Some(prev_doc) = view.docs_access_history.pop() {
1367 Some(Action::ReplaceDoc(view.id, prev_doc))
1369 // only the document that is being closed was in the view, close it
1370 Some(Action::Close(view.id))
1378 for action in actions {
1380 Action::Close(view_id) => {
1381 self.close(view_id);
1383 Action::ReplaceDoc(view_id, doc_id) => {
1384 self.replace_document_in_view(view_id, doc_id);
1389 self.documents.remove(&doc_id);
1391 // If the document we removed was visible in all views, we will have no more views. We don't
1392 // want to close the editor just for a simple buffer close, so we need to create a new view
1393 // containing either an existing document, or a brand new document.
1394 if self.tree.views().next().is_none() {
1398 .map(|(&doc_id, _)| doc_id)
1400 .unwrap_or_else(|| self.new_document(Document::default(self.config.clone())));
1401 let view = View::new(doc_id, self.config().gutters.clone());
1402 let view_id = self.tree.insert(view);
1403 let doc = doc_mut!(self, &doc_id);
1404 doc.ensure_view_init(view_id);
1412 pub fn save<P: Into<PathBuf>>(
1417 ) -> anyhow::Result<()> {
1418 // convert a channel of futures to pipe into main queue one by one
1419 // via stream.then() ? then push into main future
1421 let path = path.map(|path| path.into());
1422 let doc = doc_mut!(self, &doc_id);
1423 let future = doc.save(path, force)?;
1425 use futures_util::stream;
1429 .ok_or_else(|| anyhow::format_err!("saves are closed for this document!"))?
1430 .send(stream::once(Box::pin(future)))
1431 .map_err(|err| anyhow!("failed to send save event: {}", err))?;
1433 self.write_count += 1;
1438 pub fn resize(&mut self, area: Rect) {
1439 if self.tree.resize(area) {
1444 pub fn focus(&mut self, view_id: ViewId) {
1445 let prev_id = std::mem::replace(&mut self.tree.focus, view_id);
1447 // if leaving the view: mode should reset and the cursor should be
1449 if prev_id != view_id {
1450 self.enter_normal_mode();
1451 self.ensure_cursor_in_view(view_id);
1453 // Update jumplist selections with new document changes.
1454 for (view, _focused) in self.tree.views_mut() {
1455 let doc = doc_mut!(self, &view.doc);
1456 view.sync_changes(doc);
1461 pub fn focus_next(&mut self) {
1462 self.focus(self.tree.next());
1465 pub fn focus_prev(&mut self) {
1466 self.focus(self.tree.prev());
1469 pub fn focus_direction(&mut self, direction: tree::Direction) {
1470 let current_view = self.tree.focus;
1471 if let Some(id) = self.tree.find_split_in_direction(current_view, direction) {
1476 pub fn swap_split_in_direction(&mut self, direction: tree::Direction) {
1477 self.tree.swap_split_in_direction(direction);
1480 pub fn transpose_view(&mut self) {
1481 self.tree.transpose();
1484 pub fn should_close(&self) -> bool {
1485 self.tree.is_empty()
1488 pub fn ensure_cursor_in_view(&mut self, id: ViewId) {
1489 let config = self.config();
1490 let view = self.tree.get_mut(id);
1491 let doc = &self.documents[&view.doc];
1492 view.ensure_cursor_in_view(doc, config.scrolloff)
1496 pub fn document(&self, id: DocumentId) -> Option<&Document> {
1497 self.documents.get(&id)
1501 pub fn document_mut(&mut self, id: DocumentId) -> Option<&mut Document> {
1502 self.documents.get_mut(&id)
1506 pub fn documents(&self) -> impl Iterator<Item = &Document> {
1507 self.documents.values()
1511 pub fn documents_mut(&mut self) -> impl Iterator<Item = &mut Document> {
1512 self.documents.values_mut()
1515 pub fn document_by_path<P: AsRef<Path>>(&self, path: P) -> Option<&Document> {
1517 .find(|doc| doc.path().map(|p| p == path.as_ref()).unwrap_or(false))
1520 pub fn document_by_path_mut<P: AsRef<Path>>(&mut self, path: P) -> Option<&mut Document> {
1521 self.documents_mut()
1522 .find(|doc| doc.path().map(|p| p == path.as_ref()).unwrap_or(false))
1525 /// Gets the primary cursor position in screen coordinates,
1526 /// or `None` if the primary cursor is not visible on screen.
1527 pub fn cursor(&self) -> (Option<Position>, CursorKind) {
1528 let config = self.config();
1529 let (view, doc) = current_ref!(self);
1533 .cursor(doc.text().slice(..));
1537 .unwrap_or_else(|| view.screen_coords_at_pos(doc, doc.text().slice(..), cursor));
1538 if let Some(mut pos) = pos {
1539 let inner = view.inner_area(doc);
1540 pos.col += inner.x as usize;
1541 pos.row += inner.y as usize;
1542 let cursorkind = config.cursor_shape.from_mode(self.mode);
1543 (Some(pos), cursorkind)
1545 (None, CursorKind::default())
1549 /// Closes language servers with timeout. The default timeout is 10000 ms, use
1550 /// `timeout` parameter to override this.
1551 pub async fn close_language_servers(
1553 timeout: Option<u64>,
1554 ) -> Result<(), tokio::time::error::Elapsed> {
1555 tokio::time::timeout(
1556 Duration::from_millis(timeout.unwrap_or(3000)),
1558 self.language_servers
1560 .map(|client| client.force_shutdown()),
1567 pub async fn wait_event(&mut self) -> EditorEvent {
1568 // the loop only runs once or twice and would be better implemented with a recursion + const generic
1569 // however due to limitations with async functions that can not be implemented right now
1574 Some(event) = self.save_queue.next() => {
1575 self.write_count -= 1;
1576 return EditorEvent::DocumentSaved(event)
1578 Some(config_event) = self.config_events.1.recv() => {
1579 return EditorEvent::ConfigEvent(config_event)
1581 Some(message) = self.language_servers.incoming.next() => {
1582 return EditorEvent::LanguageServerMessage(message)
1584 Some(event) = self.debugger_events.next() => {
1585 return EditorEvent::DebuggerEvent(event)
1588 _ = self.redraw_handle.0.notified() => {
1589 if !self.needs_redraw{
1590 self.needs_redraw = true;
1591 let timeout = Instant::now() + Duration::from_millis(96);
1592 if timeout < self.idle_timer.deadline(){
1593 self.idle_timer.as_mut().reset(timeout)
1598 _ = &mut self.idle_timer => {
1599 return EditorEvent::IdleTimer
1605 pub async fn flush_writes(&mut self) -> anyhow::Result<()> {
1606 while self.write_count > 0 {
1607 if let Some(save_event) = self.save_queue.next().await {
1608 self.write_count -= 1;
1610 let save_event = match save_event {
1613 self.set_error(err.to_string());
1618 let doc = doc_mut!(self, &save_event.doc_id);
1619 doc.set_last_saved_revision(save_event.revision);
1626 /// Switches the editor into normal mode.
1627 pub fn enter_normal_mode(&mut self) {
1628 use helix_core::{graphemes, Range};
1630 if self.mode == Mode::Normal {
1634 self.mode = Mode::Normal;
1635 let (view, doc) = current!(self);
1637 try_restore_indent(doc, view);
1639 // if leaving append mode, move cursor back by 1
1640 if doc.restore_cursor {
1641 let text = doc.text().slice(..);
1642 let selection = doc.selection(view.id).clone().transform(|range| {
1645 graphemes::prev_grapheme_boundary(text, range.to()),
1649 doc.set_selection(view.id, selection);
1650 doc.restore_cursor = false;
1655fn try_restore_indent(doc: &mut Document, view: &mut View) {
1657 chars::char_is_whitespace, line_ending::line_end_char_index, Operation, Transaction,
1660 fn inserted_a_new_blank_line(changes: &[Operation], pos: usize, line_end_pos: usize) -> bool {
1661 if let [Operation::Retain(move_pos), Operation::Insert(ref inserted_str), Operation::Retain(_)] =
1664 move_pos + inserted_str.len() == pos
1665 && inserted_str.starts_with('\n')
1666 && inserted_str.chars().skip(1).all(char_is_whitespace)
1667 && pos == line_end_pos // ensure no characters exists after current position
1673 let doc_changes = doc.changes().changes();
1674 let text = doc.text().slice(..);
1675 let range = doc.selection(view.id).primary();
1676 let pos = range.cursor(text);
1677 let line_end_pos = line_end_char_index(&text, range.cursor_line(text));
1679 if inserted_a_new_blank_line(doc_changes, pos, line_end_pos) {
1680 // Removes tailing whitespaces.
1682 Transaction::change_by_selection(doc.text(), doc.selection(view.id), |range| {
1683 let line_start_pos = text.line_to_char(range.cursor_line(text));
1684 (line_start_pos, pos, None)
1686 doc.apply(&transaction, view.id);
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