Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'helix-view/src/editor.rs')
-rw-r--r--helix-view/src/editor.rs2325
1 files changed, 166 insertions, 2159 deletions
diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs
index 7f8cff9c..52fca6d2 100644
--- a/helix-view/src/editor.rs
+++ b/helix-view/src/editor.rs
@@ -1,68 +1,30 @@
use crate::{
- annotations::diagnostics::{DiagnosticFilter, InlineDiagnosticsConfig},
- clipboard::ClipboardProvider,
- document::{
- DocumentOpenError, DocumentSavedEventFuture, DocumentSavedEventResult, Mode, SavePoint,
- },
- events::{DocumentDidClose, DocumentDidOpen, DocumentFocusLost},
+ clipboard::{get_clipboard_provider, ClipboardProvider},
graphics::{CursorKind, Rect},
- handlers::Handlers,
- info::Info,
- input::KeyEvent,
- register::Registers,
theme::{self, Theme},
- tree::{self, Tree},
+ tree::Tree,
Document, DocumentId, View, ViewId,
};
-use helix_event::dispatch;
-use helix_vcs::DiffProviderRegistry;
-
-use futures_util::stream::select_all::SelectAll;
-use futures_util::{future, StreamExt};
-use helix_lsp::{Call, LanguageServerId};
-use tokio_stream::wrappers::UnboundedReceiverStream;
+use futures_util::future;
use std::{
- borrow::Cow,
- cell::Cell,
- collections::{BTreeMap, HashMap, HashSet},
- fs,
- io::{self, stdin},
- num::{NonZeroU8, NonZeroUsize},
path::{Path, PathBuf},
pin::Pin,
sync::Arc,
};
-use tokio::{
- sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender},
- time::{sleep, Duration, Instant, Sleep},
-};
-
-use anyhow::{anyhow, bail, Error};
+use tokio::time::{sleep, Duration, Instant, Sleep};
-pub use helix_core::diagnostic::Severity;
-use helix_core::{
- auto_pairs::AutoPairs,
- diagnostic::DiagnosticProvider,
- syntax::{
- self,
- config::{AutoPairConfig, IndentationHeuristic, LanguageServerFeature, SoftWrap},
- },
- Change, LineEnding, Position, Range, Selection, Uri, NATIVE_LINE_ENDING,
-};
-use helix_dap::{self as dap, registry::DebugAdapterId};
-use helix_lsp::lsp;
-use helix_stdx::path::canonicalize;
+use slotmap::SlotMap;
-use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer};
+use anyhow::Error;
-use arc_swap::{
- access::{DynAccess, DynGuard},
- ArcSwap,
-};
+pub use helix_core::diagnostic::Severity;
+pub use helix_core::register::Registers;
+use helix_core::syntax;
+use helix_core::Position;
-pub const DEFAULT_AUTO_SAVE_DELAY: u64 = 3000;
+use serde::Deserialize;
fn deserialize_duration_millis<'de, D>(deserializer: D) -> Result<Duration, D::Error>
where
@@ -72,225 +34,8 @@ where
Ok(Duration::from_millis(millis))
}
-fn serialize_duration_millis<S>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error>
-where
- S: Serializer,
-{
- serializer.serialize_u64(
- duration
- .as_millis()
- .try_into()
- .map_err(|_| serde::ser::Error::custom("duration value overflowed u64"))?,
- )
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
-pub struct GutterConfig {
- /// Gutter Layout
- pub layout: Vec<GutterType>,
- /// Options specific to the "line-numbers" gutter
- pub line_numbers: GutterLineNumbersConfig,
-}
-
-impl Default for GutterConfig {
- fn default() -> Self {
- Self {
- layout: vec![
- GutterType::Diagnostics,
- GutterType::Spacer,
- GutterType::LineNumbers,
- GutterType::Spacer,
- GutterType::Diff,
- ],
- line_numbers: GutterLineNumbersConfig::default(),
- }
- }
-}
-
-impl From<Vec<GutterType>> for GutterConfig {
- fn from(x: Vec<GutterType>) -> Self {
- GutterConfig {
- layout: x,
- ..Default::default()
- }
- }
-}
-
-fn deserialize_gutter_seq_or_struct<'de, D>(deserializer: D) -> Result<GutterConfig, D::Error>
-where
- D: Deserializer<'de>,
-{
- struct GutterVisitor;
-
- impl<'de> serde::de::Visitor<'de> for GutterVisitor {
- type Value = GutterConfig;
-
- fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
- write!(
- formatter,
- "an array of gutter names or a detailed gutter configuration"
- )
- }
-
- fn visit_seq<S>(self, mut seq: S) -> Result<Self::Value, S::Error>
- where
- S: serde::de::SeqAccess<'de>,
- {
- let mut gutters = Vec::new();
- while let Some(gutter) = seq.next_element::<String>()? {
- gutters.push(
- gutter
- .parse::<GutterType>()
- .map_err(serde::de::Error::custom)?,
- )
- }
-
- Ok(gutters.into())
- }
-
- fn visit_map<M>(self, map: M) -> Result<Self::Value, M::Error>
- where
- M: serde::de::MapAccess<'de>,
- {
- let deserializer = serde::de::value::MapAccessDeserializer::new(map);
- Deserialize::deserialize(deserializer)
- }
- }
-
- deserializer.deserialize_any(GutterVisitor)
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
-pub struct GutterLineNumbersConfig {
- /// Minimum number of characters to use for line number gutter. Defaults to 3.
- pub min_width: usize,
-}
-
-impl Default for GutterLineNumbersConfig {
- fn default() -> Self {
- Self { min_width: 3 }
- }
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
-pub struct FilePickerConfig {
- /// IgnoreOptions
- /// Enables ignoring hidden files.
- /// Whether to hide hidden files in file picker and global search results. Defaults to true.
- pub hidden: bool,
- /// Enables following symlinks.
- /// Whether to follow symbolic links in file picker and file or directory completions. Defaults to true.
- pub follow_symlinks: bool,
- /// Hides symlinks that point into the current directory. Defaults to true.
- pub deduplicate_links: bool,
- /// Enables reading ignore files from parent directories. Defaults to true.
- pub parents: bool,
- /// Enables reading `.ignore` files.
- /// Whether to hide files listed in .ignore in file picker and global search results. Defaults to true.
- pub ignore: bool,
- /// Enables reading `.gitignore` files.
- /// Whether to hide files listed in .gitignore in file picker and global search results. Defaults to true.
- pub git_ignore: bool,
- /// Enables reading global .gitignore, whose path is specified in git's config: `core.excludefile` option.
- /// Whether to hide files listed in global .gitignore in file picker and global search results. Defaults to true.
- pub git_global: bool,
- /// Enables reading `.git/info/exclude` files.
- /// Whether to hide files listed in .git/info/exclude in file picker and global search results. Defaults to true.
- pub git_exclude: bool,
- /// WalkBuilder options
- /// Maximum Depth to recurse directories in file picker and global search. Defaults to `None`.
- pub max_depth: Option<usize>,
-}
-
-impl Default for FilePickerConfig {
- fn default() -> Self {
- Self {
- hidden: true,
- follow_symlinks: true,
- deduplicate_links: true,
- parents: true,
- ignore: true,
- git_ignore: true,
- git_global: true,
- git_exclude: true,
- max_depth: None,
- }
- }
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
-pub struct FileExplorerConfig {
- /// IgnoreOptions
- /// Enables ignoring hidden files.
- /// Whether to hide hidden files in file explorer and global search results. Defaults to false.
- pub hidden: bool,
- /// Enables following symlinks.
- /// Whether to follow symbolic links in file picker and file or directory completions. Defaults to false.
- pub follow_symlinks: bool,
- /// Enables reading ignore files from parent directories. Defaults to false.
- pub parents: bool,
- /// Enables reading `.ignore` files.
- /// Whether to hide files listed in .ignore in file picker and global search results. Defaults to false.
- pub ignore: bool,
- /// Enables reading `.gitignore` files.
- /// Whether to hide files listed in .gitignore in file picker and global search results. Defaults to false.
- pub git_ignore: bool,
- /// Enables reading global .gitignore, whose path is specified in git's config: `core.excludefile` option.
- /// Whether to hide files listed in global .gitignore in file picker and global search results. Defaults to false.
- pub git_global: bool,
- /// Enables reading `.git/info/exclude` files.
- /// Whether to hide files listed in .git/info/exclude in file picker and global search results. Defaults to false.
- pub git_exclude: bool,
- /// Whether to flatten single-child directories in file explorer. Defaults to true.
- pub flatten_dirs: bool,
-}
-
-impl Default for FileExplorerConfig {
- fn default() -> Self {
- Self {
- hidden: false,
- follow_symlinks: false,
- parents: false,
- ignore: false,
- git_ignore: false,
- git_global: false,
- git_exclude: false,
- flatten_dirs: true,
- }
- }
-}
-
-fn serialize_alphabet<S>(alphabet: &[char], serializer: S) -> Result<S::Ok, S::Error>
-where
- S: Serializer,
-{
- let alphabet: String = alphabet.iter().collect();
- serializer.serialize_str(&alphabet)
-}
-
-fn deserialize_alphabet<'de, D>(deserializer: D) -> Result<Vec<char>, D::Error>
-where
- D: Deserializer<'de>,
-{
- use serde::de::Error;
-
- let str = String::deserialize(deserializer)?;
- let chars: Vec<_> = str.chars().collect();
- let unique_chars: HashSet<_> = chars.iter().copied().collect();
- if unique_chars.len() != chars.len() {
- return Err(<D::Error as Error>::custom(
- "jump-label-alphabet must contain unique characters",
- ));
- }
- Ok(chars)
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
+#[derive(Debug, Clone, PartialEq, Deserialize)]
+#[serde(rename_all = "kebab-case", default)]
pub struct Config {
/// Padding to keep between the edge of the screen and the cursor when scrolling. Defaults to 5.
pub scrolloff: usize,
@@ -302,757 +47,30 @@ pub struct Config {
pub shell: Vec<String>,
/// Line number mode.
pub line_number: LineNumber,
- /// Highlight the lines cursors are currently on. Defaults to false.
- pub cursorline: bool,
- /// Highlight the columns cursors are currently on. Defaults to false.
- pub cursorcolumn: bool,
- #[serde(deserialize_with = "deserialize_gutter_seq_or_struct")]
- pub gutters: GutterConfig,
/// Middle click paste support. Defaults to true.
pub middle_click_paste: bool,
- /// Automatic insertion of pairs to parentheses, brackets,
- /// etc. Optionally, this can be a list of 2-tuples to specify a
- /// global list of characters to pair. Defaults to true.
- pub auto_pairs: AutoPairConfig,
+ /// Smart case: Case insensitive searching unless pattern contains upper case characters. Defaults to true.
+ pub smart_case: bool,
+ /// Automatic insertion of pairs to parentheses, brackets, etc. Defaults to true.
+ pub auto_pairs: bool,
/// Automatic auto-completion, automatically pop up without user trigger. Defaults to true.
pub auto_completion: bool,
- /// Enable filepath completion.
- /// Show files and directories if an existing path at the cursor was recognized,
- /// either absolute or relative to the current opened document or current working directory (if the buffer is not yet saved).
- /// Defaults to true.
- pub path_completion: bool,
- /// Configures completion of words from open buffers.
- /// Defaults to enabled with a trigger length of 7.
- pub word_completion: WordCompletion,
- /// Automatic formatting on save. Defaults to true.
- pub auto_format: bool,
- /// Default register used for yank/paste. Defaults to '"'
- pub default_yank_register: char,
- /// Automatic save on focus lost and/or after delay.
- /// Time delay in milliseconds since last edit after which auto save timer triggers.
- /// Time delay defaults to false with 3000ms delay. Focus lost defaults to false.
- #[serde(deserialize_with = "deserialize_auto_save")]
- pub auto_save: AutoSave,
- /// Set a global text_width
- pub text_width: usize,
- /// Time in milliseconds since last keypress before idle timers trigger.
- /// Used for various UI timeouts. Defaults to 250ms.
- #[serde(
- serialize_with = "serialize_duration_millis",
- deserialize_with = "deserialize_duration_millis"
- )]
+ /// Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant. Defaults to 400ms.
+ #[serde(skip_serializing, deserialize_with = "deserialize_duration_millis")]
pub idle_timeout: Duration,
- /// Time in milliseconds after typing a word character before auto completions
- /// are shown, set to 5 for instant. Defaults to 250ms.
- #[serde(
- serialize_with = "serialize_duration_millis",
- deserialize_with = "deserialize_duration_millis"
- )]
- pub completion_timeout: Duration,
- /// Whether to insert the completion suggestion on hover. Defaults to true.
- pub preview_completion_insert: bool,
pub completion_trigger_len: u8,
- /// Whether to instruct the LSP to replace the entire word when applying a completion
- /// or to only insert new text
- pub completion_replace: bool,
- /// `true` if helix should automatically add a line comment token if you're currently in a comment
- /// and press `enter`.
- pub continue_comments: bool,
- /// Whether to display infoboxes. Defaults to true.
- pub auto_info: bool,
- pub file_picker: FilePickerConfig,
- pub file_explorer: FileExplorerConfig,
- /// Configuration of the statusline elements
- pub statusline: StatusLineConfig,
- /// Shape for cursor in each mode
- pub cursor_shape: CursorShapeConfig,
- /// Set to `true` to override automatic detection of terminal truecolor support in the event of a false negative. Defaults to `false`.
- pub true_color: bool,
- /// Set to `true` to override automatic detection of terminal undercurl support in the event of a false negative. Defaults to `false`.
- pub undercurl: bool,
- /// Search configuration.
- #[serde(default)]
- pub search: SearchConfig,
- pub lsp: LspConfig,
- pub terminal: Option<TerminalConfig>,
- /// Column numbers at which to draw the rulers. Defaults to `[]`, meaning no rulers.
- pub rulers: Vec<u16>,
- #[serde(default)]
- pub whitespace: WhitespaceConfig,
- /// Persistently display open buffers along the top
- pub bufferline: BufferLine,
- /// Vertical indent width guides.
- pub indent_guides: IndentGuidesConfig,
- /// Whether to color modes with different colors. Defaults to `false`.
- pub color_modes: bool,
- pub soft_wrap: SoftWrap,
- /// Workspace specific lsp ceiling dirs
- pub workspace_lsp_roots: Vec<PathBuf>,
- /// Which line ending to choose for new documents. Defaults to `native`. i.e. `crlf` on Windows, otherwise `lf`.
- pub default_line_ending: LineEndingConfig,
- /// Whether to automatically insert a trailing line-ending on write if missing. Defaults to `true`.
- pub insert_final_newline: bool,
- /// Whether to use atomic operations to write documents to disk.
- /// This prevents data loss if the editor is interrupted while writing the file, but may
- /// confuse some file watching/hot reloading programs. Defaults to `true`.
- pub atomic_save: bool,
- /// Whether to automatically remove all trailing line-endings after the final one on write.
- /// Defaults to `false`.
- pub trim_final_newlines: bool,
- /// Whether to automatically remove all whitespace characters preceding line-endings on write.
- /// Defaults to `false`.
- pub trim_trailing_whitespace: bool,
- /// Enables smart tab
- pub smart_tab: Option<SmartTabConfig>,
- /// Draw border around popups.
- pub popup_border: PopupBorderConfig,
- /// Which indent heuristic to use when a new line is inserted
- #[serde(default)]
- pub indent_heuristic: IndentationHeuristic,
- /// labels characters used in jumpmode
- #[serde(
- serialize_with = "serialize_alphabet",
- deserialize_with = "deserialize_alphabet"
- )]
- pub jump_label_alphabet: Vec<char>,
- /// Display diagnostic below the line they occur.
- pub inline_diagnostics: InlineDiagnosticsConfig,
- pub end_of_line_diagnostics: DiagnosticFilter,
- // Set to override the default clipboard provider
- pub clipboard_provider: ClipboardProvider,
- /// Whether to read settings from [EditorConfig](https://editorconfig.org) files. Defaults to
- /// `true`.
- pub editor_config: bool,
- /// Whether to render rainbow colors for matching brackets. Defaults to `false`.
- pub rainbow_brackets: bool,
- /// Whether to enable Kitty Keyboard Protocol
- pub kitty_keyboard_protocol: KittyKeyboardProtocolConfig,
-}
-
-#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize, Clone, Copy)]
-#[serde(rename_all = "kebab-case")]
-pub enum KittyKeyboardProtocolConfig {
- #[default]
- Auto,
- Disabled,
- Enabled,
-}
-
-#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Eq, PartialOrd, Ord)]
-#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
-pub struct SmartTabConfig {
- pub enable: bool,
- pub supersede_menu: bool,
-}
-
-impl Default for SmartTabConfig {
- fn default() -> Self {
- SmartTabConfig {
- enable: true,
- supersede_menu: false,
- }
- }
-}
-
-#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
-pub struct TerminalConfig {
- pub command: String,
- #[serde(default)]
- #[serde(skip_serializing_if = "Vec::is_empty")]
- pub args: Vec<String>,
-}
-
-#[cfg(windows)]
-pub fn get_terminal_provider() -> Option<TerminalConfig> {
- use helix_stdx::env::binary_exists;
-
- if binary_exists("wt") {
- return Some(TerminalConfig {
- command: "wt".to_string(),
- args: vec![
- "new-tab".to_string(),
- "--title".to_string(),
- "DEBUG".to_string(),
- "cmd".to_string(),
- "/C".to_string(),
- ],
- });
- }
-
- Some(TerminalConfig {
- command: "conhost".to_string(),
- args: vec!["cmd".to_string(), "/C".to_string()],
- })
-}
-
-#[cfg(not(any(windows, target_arch = "wasm32")))]
-pub fn get_terminal_provider() -> Option<TerminalConfig> {
- use helix_stdx::env::{binary_exists, env_var_is_set};
-
- if env_var_is_set("TMUX") && binary_exists("tmux") {
- return Some(TerminalConfig {
- command: "tmux".to_string(),
- args: vec!["split-window".to_string()],
- });
- }
-
- if env_var_is_set("WEZTERM_UNIX_SOCKET") && binary_exists("wezterm") {
- return Some(TerminalConfig {
- command: "wezterm".to_string(),
- args: vec!["cli".to_string(), "split-pane".to_string()],
- });
- }
-
- None
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
-pub struct LspConfig {
- /// Enables LSP
- pub enable: bool,
- /// Display LSP messagess from $/progress below statusline
- pub display_progress_messages: bool,
- /// Display LSP messages from window/showMessage below statusline
- pub display_messages: bool,
- /// Enable automatic pop up of signature help (parameter hints)
- pub auto_signature_help: bool,
- /// Display docs under signature help popup
- pub display_signature_help_docs: bool,
- /// Display inlay hints
- pub display_inlay_hints: bool,
- /// Maximum displayed length of inlay hints (excluding the added trailing `…`).
- /// If it's `None`, there's no limit
- pub inlay_hints_length_limit: Option<NonZeroU8>,
- /// Display document color swatches
- pub display_color_swatches: bool,
- /// Whether to enable snippet support
- pub snippets: bool,
- /// Whether to include declaration in the goto reference query
- pub goto_reference_include_declaration: bool,
-}
-
-impl Default for LspConfig {
- fn default() -> Self {
- Self {
- enable: true,
- display_progress_messages: false,
- display_messages: true,
- auto_signature_help: true,
- display_signature_help_docs: true,
- display_inlay_hints: false,
- inlay_hints_length_limit: None,
- snippets: true,
- goto_reference_include_declaration: true,
- display_color_swatches: true,
- }
- }
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
-pub struct SearchConfig {
- /// Smart case: Case insensitive searching unless pattern contains upper case characters. Defaults to true.
- pub smart_case: bool,
- /// Whether the search should wrap after depleting the matches. Default to true.
- pub wrap_around: bool,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
-pub struct StatusLineConfig {
- pub left: Vec<StatusLineElement>,
- pub center: Vec<StatusLineElement>,
- pub right: Vec<StatusLineElement>,
- pub separator: String,
- pub mode: ModeConfig,
- pub diagnostics: Vec<Severity>,
- pub workspace_diagnostics: Vec<Severity>,
-}
-
-impl Default for StatusLineConfig {
- fn default() -> Self {
- use StatusLineElement as E;
-
- Self {
- left: vec![
- E::Mode,
- E::Spinner,
- E::FileName,
- E::ReadOnlyIndicator,
- E::FileModificationIndicator,
- ],
- center: vec![],
- right: vec![
- E::Diagnostics,
- E::Selections,
- E::Register,
- E::Position,
- E::FileEncoding,
- ],
- separator: String::from("│"),
- mode: ModeConfig::default(),
- diagnostics: vec![Severity::Warning, Severity::Error],
- workspace_diagnostics: vec![Severity::Warning, Severity::Error],
- }
- }
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
-pub struct ModeConfig {
- pub normal: String,
- pub insert: String,
- pub select: String,
-}
-
-impl Default for ModeConfig {
- fn default() -> Self {
- Self {
- normal: String::from("NOR"),
- insert: String::from("INS"),
- select: String::from("SEL"),
- }
- }
-}
-
-#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "kebab-case")]
-pub enum StatusLineElement {
- /// The editor mode (Normal, Insert, Visual/Selection)
- Mode,
-
- /// The LSP activity spinner
- Spinner,
-
- /// The file basename (the leaf of the open file's path)
- FileBaseName,
-
- /// The relative file path
- FileName,
-
- /// The file absolute path
- FileAbsolutePath,
-
- // The file modification indicator
- FileModificationIndicator,
-
- /// An indicator that shows `"[readonly]"` when a file cannot be written
- ReadOnlyIndicator,
-
- /// The file encoding
- FileEncoding,
-
- /// The file line endings (CRLF or LF)
- FileLineEnding,
-
- /// The file indentation style
- FileIndentStyle,
-
- /// The file type (language ID or "text")
- FileType,
-
- /// A summary of the number of errors and warnings
- Diagnostics,
-
- /// A summary of the number of errors and warnings on file and workspace
- WorkspaceDiagnostics,
-
- /// The number of selections (cursors)
- Selections,
-
- /// The number of characters currently in primary selection
- PrimarySelectionLength,
-
- /// The cursor position
- Position,
-
- /// The separator string
- Separator,
-
- /// The cursor position as a percent of the total file
- PositionPercentage,
-
- /// The total line numbers of the current file
- TotalLineNumbers,
-
- /// A single space
- Spacer,
-
- /// Current version control information
- VersionControl,
-
- /// Indicator for selected register
- Register,
-
- /// The base of current working directory
- CurrentWorkingDirectory,
-}
-
-// Cursor shape is read and used on every rendered frame and so needs
-// to be fast. Therefore we avoid a hashmap and use an enum indexed array.
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct CursorShapeConfig([CursorKind; 3]);
-
-impl CursorShapeConfig {
- pub fn from_mode(&self, mode: Mode) -> CursorKind {
- self.get(mode as usize).copied().unwrap_or_default()
- }
}
-impl<'de> Deserialize<'de> for CursorShapeConfig {
- fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
- where
- D: Deserializer<'de>,
- {
- let m = HashMap::<Mode, CursorKind>::deserialize(deserializer)?;
- let into_cursor = |mode: Mode| m.get(&mode).copied().unwrap_or_default();
- Ok(CursorShapeConfig([
- into_cursor(Mode::Normal),
- into_cursor(Mode::Select),
- into_cursor(Mode::Insert),
- ]))
- }
-}
-
-impl Serialize for CursorShapeConfig {
- fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
- where
- S: serde::Serializer,
- {
- let mut map = serializer.serialize_map(Some(self.len()))?;
- let modes = [Mode::Normal, Mode::Select, Mode::Insert];
- for mode in modes {
- map.serialize_entry(&mode, &self.from_mode(mode))?;
- }
- map.end()
- }
-}
-
-impl std::ops::Deref for CursorShapeConfig {
- type Target = [CursorKind; 3];
-
- fn deref(&self) -> &Self::Target {
- &self.0
- }
-}
-
-impl Default for CursorShapeConfig {
- fn default() -> Self {
- Self([CursorKind::Block; 3])
- }
-}
-
-/// bufferline render modes
-#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "kebab-case")]
-pub enum BufferLine {
- /// Don't render bufferline
- #[default]
- Never,
- /// Always render
- Always,
- /// Only if multiple buffers are open
- Multiple,
-}
-
-#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum LineNumber {
/// Show absolute line number
Absolute,
- /// If focused and in normal/select mode, show relative line number to the primary cursor.
- /// If unfocused or in insert mode, show absolute line number.
+ /// Show relative line number to the primary cursor
Relative,
}
-impl std::str::FromStr for LineNumber {
- type Err = anyhow::Error;
-
- fn from_str(s: &str) -> Result<Self, Self::Err> {
- match s.to_lowercase().as_str() {
- "absolute" | "abs" => Ok(Self::Absolute),
- "relative" | "rel" => Ok(Self::Relative),
- _ => anyhow::bail!("Line number can only be `absolute` or `relative`."),
- }
- }
-}
-
-#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "kebab-case")]
-pub enum GutterType {
- /// Show diagnostics and other features like breakpoints
- Diagnostics,
- /// Show line numbers
- LineNumbers,
- /// Show one blank space
- Spacer,
- /// Highlight local changes
- Diff,
-}
-
-impl std::str::FromStr for GutterType {
- type Err = anyhow::Error;
-
- fn from_str(s: &str) -> Result<Self, Self::Err> {
- match s.to_lowercase().as_str() {
- "diagnostics" => Ok(Self::Diagnostics),
- "spacer" => Ok(Self::Spacer),
- "line-numbers" => Ok(Self::LineNumbers),
- "diff" => Ok(Self::Diff),
- _ => anyhow::bail!(
- "Gutter type can only be `diagnostics`, `spacer`, `line-numbers` or `diff`."
- ),
- }
- }
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(default)]
-pub struct WhitespaceConfig {
- pub render: WhitespaceRender,
- pub characters: WhitespaceCharacters,
-}
-
-impl Default for WhitespaceConfig {
- fn default() -> Self {
- Self {
- render: WhitespaceRender::Basic(WhitespaceRenderValue::None),
- characters: WhitespaceCharacters::default(),
- }
- }
-}
-
-#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(untagged, rename_all = "kebab-case")]
-pub enum WhitespaceRender {
- Basic(WhitespaceRenderValue),
- Specific {
- default: Option<WhitespaceRenderValue>,
- space: Option<WhitespaceRenderValue>,
- nbsp: Option<WhitespaceRenderValue>,
- nnbsp: Option<WhitespaceRenderValue>,
- tab: Option<WhitespaceRenderValue>,
- newline: Option<WhitespaceRenderValue>,
- },
-}
-
-#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "kebab-case")]
-pub enum WhitespaceRenderValue {
- None,
- // TODO
- // Selection,
- All,
-}
-
-impl WhitespaceRender {
- pub fn space(&self) -> WhitespaceRenderValue {
- match *self {
- Self::Basic(val) => val,
- Self::Specific { default, space, .. } => {
- space.or(default).unwrap_or(WhitespaceRenderValue::None)
- }
- }
- }
- pub fn nbsp(&self) -> WhitespaceRenderValue {
- match *self {
- Self::Basic(val) => val,
- Self::Specific { default, nbsp, .. } => {
- nbsp.or(default).unwrap_or(WhitespaceRenderValue::None)
- }
- }
- }
- pub fn nnbsp(&self) -> WhitespaceRenderValue {
- match *self {
- Self::Basic(val) => val,
- Self::Specific { default, nnbsp, .. } => {
- nnbsp.or(default).unwrap_or(WhitespaceRenderValue::None)
- }
- }
- }
- pub fn tab(&self) -> WhitespaceRenderValue {
- match *self {
- Self::Basic(val) => val,
- Self::Specific { default, tab, .. } => {
- tab.or(default).unwrap_or(WhitespaceRenderValue::None)
- }
- }
- }
- pub fn newline(&self) -> WhitespaceRenderValue {
- match *self {
- Self::Basic(val) => val,
- Self::Specific {
- default, newline, ..
- } => newline.or(default).unwrap_or(WhitespaceRenderValue::None),
- }
- }
-}
-
-#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize)]
-#[serde(rename_all = "kebab-case")]
-pub struct AutoSave {
- /// Auto save after a delay in milliseconds. Defaults to disabled.
- #[serde(default)]
- pub after_delay: AutoSaveAfterDelay,
- /// Auto save on focus lost. Defaults to false.
- #[serde(default)]
- pub focus_lost: bool,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
-#[serde(deny_unknown_fields)]
-pub struct AutoSaveAfterDelay {
- #[serde(default)]
- /// Enable auto save after delay. Defaults to false.
- pub enable: bool,
- #[serde(default = "default_auto_save_delay")]
- /// Time delay in milliseconds. Defaults to [DEFAULT_AUTO_SAVE_DELAY].
- pub timeout: u64,
-}
-
-impl Default for AutoSaveAfterDelay {
- fn default() -> Self {
- Self {
- enable: false,
- timeout: DEFAULT_AUTO_SAVE_DELAY,
- }
- }
-}
-
-fn default_auto_save_delay() -> u64 {
- DEFAULT_AUTO_SAVE_DELAY
-}
-
-fn deserialize_auto_save<'de, D>(deserializer: D) -> Result<AutoSave, D::Error>
-where
- D: serde::Deserializer<'de>,
-{
- #[derive(Deserialize, Serialize)]
- #[serde(untagged, deny_unknown_fields, rename_all = "kebab-case")]
- enum AutoSaveToml {
- EnableFocusLost(bool),
- AutoSave(AutoSave),
- }
-
- match AutoSaveToml::deserialize(deserializer)? {
- AutoSaveToml::EnableFocusLost(focus_lost) => Ok(AutoSave {
- focus_lost,
- ..Default::default()
- }),
- AutoSaveToml::AutoSave(auto_save) => Ok(auto_save),
- }
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(default)]
-pub struct WhitespaceCharacters {
- pub space: char,
- pub nbsp: char,
- pub nnbsp: char,
- pub tab: char,
- pub tabpad: char,
- pub newline: char,
-}
-
-impl Default for WhitespaceCharacters {
- fn default() -> Self {
- Self {
- space: '·', // U+00B7
- nbsp: '⍽', // U+237D
- nnbsp: '␣', // U+2423
- tab: '→', // U+2192
- newline: '⏎', // U+23CE
- tabpad: ' ',
- }
- }
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(default, rename_all = "kebab-case")]
-pub struct IndentGuidesConfig {
- pub render: bool,
- pub character: char,
- pub skip_levels: u8,
-}
-
-impl Default for IndentGuidesConfig {
- fn default() -> Self {
- Self {
- skip_levels: 0,
- render: false,
- character: '│',
- }
- }
-}
-
-/// Line ending configuration.
-#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "lowercase")]
-pub enum LineEndingConfig {
- /// The platform's native line ending.
- ///
- /// `crlf` on Windows, otherwise `lf`.
- #[default]
- Native,
- /// Line feed.
- LF,
- /// Carriage return followed by line feed.
- Crlf,
- /// Form feed.
- #[cfg(feature = "unicode-lines")]
- FF,
- /// Carriage return.
- #[cfg(feature = "unicode-lines")]
- CR,
- /// Next line.
- #[cfg(feature = "unicode-lines")]
- Nel,
-}
-
-impl From<LineEndingConfig> for LineEnding {
- fn from(line_ending: LineEndingConfig) -> Self {
- match line_ending {
- LineEndingConfig::Native => NATIVE_LINE_ENDING,
- LineEndingConfig::LF => LineEnding::LF,
- LineEndingConfig::Crlf => LineEnding::Crlf,
- #[cfg(feature = "unicode-lines")]
- LineEndingConfig::FF => LineEnding::FF,
- #[cfg(feature = "unicode-lines")]
- LineEndingConfig::CR => LineEnding::CR,
- #[cfg(feature = "unicode-lines")]
- LineEndingConfig::Nel => LineEnding::Nel,
- }
- }
-}
-
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "kebab-case")]
-pub enum PopupBorderConfig {
- None,
- All,
- Popup,
- Menu,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
-pub struct WordCompletion {
- pub enable: bool,
- pub trigger_length: NonZeroU8,
-}
-
-impl Default for WordCompletion {
- fn default() -> Self {
- Self {
- enable: true,
- trigger_length: NonZeroU8::new(7).unwrap(),
- }
- }
-}
-
impl Default for Config {
fn default() -> Self {
Self {
@@ -1065,198 +83,35 @@ impl Default for Config {
vec!["sh".to_owned(), "-c".to_owned()]
},
line_number: LineNumber::Absolute,
- cursorline: false,
- cursorcolumn: false,
- gutters: GutterConfig::default(),
middle_click_paste: true,
- auto_pairs: AutoPairConfig::default(),
+ smart_case: true,
+ auto_pairs: true,
auto_completion: true,
- path_completion: true,
- word_completion: WordCompletion::default(),
- auto_format: true,
- default_yank_register: '"',
- auto_save: AutoSave::default(),
- idle_timeout: Duration::from_millis(250),
- completion_timeout: Duration::from_millis(250),
- preview_completion_insert: true,
+ idle_timeout: Duration::from_millis(400),
completion_trigger_len: 2,
- auto_info: true,
- file_picker: FilePickerConfig::default(),
- file_explorer: FileExplorerConfig::default(),
- statusline: StatusLineConfig::default(),
- cursor_shape: CursorShapeConfig::default(),
- true_color: false,
- undercurl: false,
- search: SearchConfig::default(),
- lsp: LspConfig::default(),
- terminal: get_terminal_provider(),
- rulers: Vec::new(),
- whitespace: WhitespaceConfig::default(),
- bufferline: BufferLine::default(),
- indent_guides: IndentGuidesConfig::default(),
- color_modes: false,
- soft_wrap: SoftWrap {
- enable: Some(false),
- ..SoftWrap::default()
- },
- text_width: 80,
- completion_replace: false,
- continue_comments: true,
- workspace_lsp_roots: Vec::new(),
- default_line_ending: LineEndingConfig::default(),
- insert_final_newline: true,
- atomic_save: true,
- trim_final_newlines: false,
- trim_trailing_whitespace: false,
- smart_tab: Some(SmartTabConfig::default()),
- popup_border: PopupBorderConfig::None,
- indent_heuristic: IndentationHeuristic::default(),
- jump_label_alphabet: ('a'..='z').collect(),
- inline_diagnostics: InlineDiagnosticsConfig::default(),
- end_of_line_diagnostics: DiagnosticFilter::Enable(Severity::Hint),
- clipboard_provider: ClipboardProvider::default(),
- editor_config: true,
- rainbow_brackets: false,
- kitty_keyboard_protocol: Default::default(),
- }
- }
-}
-
-impl Default for SearchConfig {
- fn default() -> Self {
- Self {
- wrap_around: true,
- smart_case: true,
}
}
}
-#[derive(Debug, Clone, Default)]
-pub struct Breakpoint {
- pub id: Option<usize>,
- pub verified: bool,
- pub message: Option<String>,
-
- pub line: usize,
- pub column: Option<usize>,
- pub condition: Option<String>,
- pub hit_condition: Option<String>,
- pub log_message: Option<String>,
-}
-
-use futures_util::stream::{Flatten, Once};
-
-type Diagnostics = BTreeMap<Uri, Vec<(lsp::Diagnostic, DiagnosticProvider)>>;
-
+#[derive(Debug)]
pub struct Editor {
- /// Current editing mode.
- pub mode: Mode,
pub tree: Tree,
- pub next_document_id: DocumentId,
- pub documents: BTreeMap<DocumentId, Document>,
-
- // We Flatten<> to resolve the inner DocumentSavedEventFuture. For that we need a stream of streams, hence the Once<>.
- // https://stackoverflow.com/a/66875668
- pub saves: HashMap<DocumentId, UnboundedSender<Once<DocumentSavedEventFuture>>>,
- pub save_queue: SelectAll<Flatten<UnboundedReceiverStream<Once<DocumentSavedEventFuture>>>>,
- pub write_count: usize,
-
+ pub documents: SlotMap<DocumentId, Document>,
pub count: Option<std::num::NonZeroUsize>,
pub selected_register: Option<char>,
pub registers: Registers,
- pub macro_recording: Option<(char, Vec<KeyEvent>)>,
- pub macro_replaying: Vec<char>,
+ pub theme: Theme,
pub language_servers: helix_lsp::Registry,
- pub diagnostics: Diagnostics,
- pub diff_providers: DiffProviderRegistry,
-
- pub debug_adapters: dap::registry::Registry,
- pub breakpoints: HashMap<PathBuf, Vec<Breakpoint>>,
+ pub clipboard_provider: Box<dyn ClipboardProvider>,
- pub syn_loader: Arc<ArcSwap<syntax::Loader>>,
+ pub syn_loader: Arc<syntax::Loader>,
pub theme_loader: Arc<theme::Loader>,
- /// last_theme is used for theme previews. We store the current theme here,
- /// and if previewing is cancelled, we can return to it.
- pub last_theme: Option<Theme>,
- /// The currently applied editor theme. While previewing a theme, the previewed theme
- /// is set here.
- pub theme: Theme,
-
- /// The primary Selection prior to starting a goto_line_number preview. This is
- /// restored when the preview is aborted, or added to the jumplist when it is
- /// confirmed.
- pub last_selection: Option<Selection>,
- pub status_msg: Option<(Cow<'static, str>, Severity)>,
- pub autoinfo: Option<Info>,
+ pub status_msg: Option<(String, Severity)>,
- pub config: Arc<dyn DynAccess<Config>>,
- pub auto_pairs: Option<AutoPairs>,
+ pub config: Config,
pub idle_timer: Pin<Box<Sleep>>,
- redraw_timer: Pin<Box<Sleep>>,
- last_motion: Option<Motion>,
- pub last_completion: Option<CompleteAction>,
- last_cwd: Option<PathBuf>,
-
- pub exit_code: i32,
-
- pub config_events: (UnboundedSender<ConfigEvent>, UnboundedReceiver<ConfigEvent>),
- pub needs_redraw: bool,
- /// Cached position of the cursor calculated during rendering.
- /// The content of `cursor_cache` is returned by `Editor::cursor` if
- /// set to `Some(_)`. The value will be cleared after it's used.
- /// If `cursor_cache` is `None` then the `Editor::cursor` function will
- /// calculate the cursor position.
- ///
- /// `Some(None)` represents a cursor position outside of the visible area.
- /// This will just cause `Editor::cursor` to return `None`.
- ///
- /// This cache is only a performance optimization to
- /// avoid calculating the cursor position multiple
- /// times during rendering and should not be set by other functions.
- pub handlers: Handlers,
-
- pub mouse_down_range: Option<Range>,
- pub cursor_cache: CursorCache,
-}
-
-pub type Motion = Box<dyn Fn(&mut Editor)>;
-
-#[derive(Debug)]
-pub enum EditorEvent {
- DocumentSaved(DocumentSavedEventResult),
- ConfigEvent(ConfigEvent),
- LanguageServerMessage((LanguageServerId, Call)),
- DebuggerEvent((DebugAdapterId, dap::Payload)),
- IdleTimer,
- Redraw,
-}
-
-#[derive(Debug, Clone)]
-pub enum ConfigEvent {
- Refresh,
- Update(Box<Config>),
-}
-
-enum ThemeAction {
- Set,
- Preview,
-}
-
-#[derive(Debug, Clone)]
-pub enum CompleteAction {
- Triggered,
- /// A savepoint of the currently selected completion. The savepoint
- /// MUST be restored before sending any event to the LSP
- Selected {
- savepoint: Arc<SavePoint>,
- },
- Applied {
- trigger_offset: usize,
- changes: Vec<Change>,
- placeholder: bool,
- },
}
#[derive(Debug, Copy, Clone)]
@@ -1267,128 +122,35 @@ pub enum Action {
VerticalSplit,
}
-impl Action {
- /// Whether to align the view to the cursor after executing this action
- pub fn align_view(&self, view: &View, new_doc: DocumentId) -> bool {
- !matches!((self, view.doc == new_doc), (Action::Load, false))
- }
-}
-
-/// Error thrown on failed document closed
-pub enum CloseError {
- /// Document doesn't exist
- DoesNotExist,
- /// Buffer is modified
- BufferModified(String),
- /// Document failed to save
- SaveError(anyhow::Error),
-}
-
impl Editor {
pub fn new(
mut area: Rect,
- theme_loader: Arc<theme::Loader>,
- syn_loader: Arc<ArcSwap<syntax::Loader>>,
- config: Arc<dyn DynAccess<Config>>,
- handlers: Handlers,
+ themes: Arc<theme::Loader>,
+ config_loader: Arc<syntax::Loader>,
+ config: Config,
) -> Self {
- let language_servers = helix_lsp::Registry::new(syn_loader.clone());
- let conf = config.load();
- let auto_pairs = (&conf.auto_pairs).into();
+ let language_servers = helix_lsp::Registry::new();
// HAXX: offset the render area height by 1 to account for prompt/commandline
area.height -= 1;
Self {
- mode: Mode::Normal,
tree: Tree::new(area),
- next_document_id: DocumentId::default(),
- documents: BTreeMap::new(),
- saves: HashMap::new(),
- save_queue: SelectAll::new(),
- write_count: 0,
+ documents: SlotMap::with_key(),
count: None,
selected_register: None,
- macro_recording: None,
- macro_replaying: Vec::new(),
- theme: theme_loader.default(),
+ theme: themes.default(),
language_servers,
- diagnostics: Diagnostics::new(),
- diff_providers: DiffProviderRegistry::default(),
- debug_adapters: dap::registry::Registry::new(),
- breakpoints: HashMap::new(),
- syn_loader,
- theme_loader,
- last_theme: None,
- last_selection: None,
- registers: Registers::new(Box::new(arc_swap::access::Map::new(
- Arc::clone(&config),
- |config: &Config| &config.clipboard_provider,
- ))),
+ syn_loader: config_loader,
+ theme_loader: themes,
+ registers: Registers::default(),
+ clipboard_provider: get_clipboard_provider(),
status_msg: None,
- autoinfo: None,
- idle_timer: Box::pin(sleep(conf.idle_timeout)),
- redraw_timer: Box::pin(sleep(Duration::MAX)),
- last_motion: None,
- last_completion: None,
- last_cwd: None,
+ idle_timer: Box::pin(sleep(config.idle_timeout)),
config,
- auto_pairs,
- exit_code: 0,
- config_events: unbounded_channel(),
- needs_redraw: false,
- handlers,
- mouse_down_range: None,
- cursor_cache: CursorCache::default(),
}
}
- pub fn popup_border(&self) -> bool {
- self.config().popup_border == PopupBorderConfig::All
- || self.config().popup_border == PopupBorderConfig::Popup
- }
-
- pub fn menu_border(&self) -> bool {
- self.config().popup_border == PopupBorderConfig::All
- || self.config().popup_border == PopupBorderConfig::Menu
- }
-
- pub fn apply_motion<F: Fn(&mut Self) + 'static>(&mut self, motion: F) {
- motion(self);
- self.last_motion = Some(Box::new(motion));
- }
-
- pub fn repeat_last_motion(&mut self, count: usize) {
- if let Some(motion) = self.last_motion.take() {
- for _ in 0..count {
- motion(self);
- }
- self.last_motion = Some(motion);
- }
- }
- /// Current editing mode for the [`Editor`].
- pub fn mode(&self) -> Mode {
- self.mode
- }
-
- pub fn config(&self) -> DynGuard<Config> {
- self.config.load()
- }
-
- /// Call if the config has changed to let the editor update all
- /// relevant members.
- pub fn refresh_config(&mut self, old_config: &Config) {
- let config = self.config();
- self.auto_pairs = (&config.auto_pairs).into();
- self.reset_idle_timer();
- self._refresh();
- helix_event::dispatch(crate::events::ConfigDidChange {
- editor: self,
- old: old_config,
- new: &config,
- })
- }
-
pub fn clear_idle_timer(&mut self) {
// equivalent to internal Instant::far_future() (30 years)
self.idle_timer
@@ -1397,644 +159,190 @@ impl Editor {
}
pub fn reset_idle_timer(&mut self) {
- let config = self.config();
self.idle_timer
.as_mut()
- .reset(Instant::now() + config.idle_timeout);
+ .reset(Instant::now() + self.config.idle_timeout);
}
pub fn clear_status(&mut self) {
self.status_msg = None;
}
- #[inline]
- pub fn set_status<T: Into<Cow<'static, str>>>(&mut self, status: T) {
- let status = status.into();
- log::debug!("editor status: {}", status);
+ pub fn set_status(&mut self, status: String) {
self.status_msg = Some((status, Severity::Info));
}
- #[inline]
- pub fn set_error<T: Into<Cow<'static, str>>>(&mut self, error: T) {
- let error = error.into();
- log::debug!("editor error: {}", error);
+ pub fn set_error(&mut self, error: String) {
self.status_msg = Some((error, Severity::Error));
}
- #[inline]
- pub fn set_warning<T: Into<Cow<'static, str>>>(&mut self, warning: T) {
- let warning = warning.into();
- log::warn!("editor warning: {}", warning);
- self.status_msg = Some((warning, Severity::Warning));
- }
-
- #[inline]
- pub fn get_status(&self) -> Option<(&Cow<'static, str>, &Severity)> {
- self.status_msg.as_ref().map(|(status, sev)| (status, sev))
- }
-
- /// Returns true if the current status is an error
- #[inline]
- pub fn is_err(&self) -> bool {
- self.status_msg
- .as_ref()
- .map(|(_, sev)| *sev == Severity::Error)
- .unwrap_or(false)
- }
-
- pub fn unset_theme_preview(&mut self) {
- if let Some(last_theme) = self.last_theme.take() {
- self.set_theme(last_theme);
- }
- // None likely occurs when the user types ":theme" and then exits before previewing
- }
-
- pub fn set_theme_preview(&mut self, theme: Theme) {
- self.set_theme_impl(theme, ThemeAction::Preview);
- }
-
pub fn set_theme(&mut self, theme: Theme) {
- self.set_theme_impl(theme, ThemeAction::Set);
- }
-
- fn set_theme_impl(&mut self, theme: Theme, preview: ThemeAction) {
- // `ui.selection` is the only scope required to be able to render a theme.
- if theme.find_highlight_exact("ui.selection").is_none() {
- self.set_error("Invalid theme: `ui.selection` required");
- return;
- }
-
let scopes = theme.scopes();
- (*self.syn_loader).load().set_scopes(scopes.to_vec());
-
- match preview {
- ThemeAction::Preview => {
- let last_theme = std::mem::replace(&mut self.theme, theme);
- // only insert on first preview: this will be the last theme the user has saved
- self.last_theme.get_or_insert(last_theme);
- }
- ThemeAction::Set => {
- self.last_theme = None;
- self.theme = theme;
- }
+ for config in self
+ .syn_loader
+ .language_configs_iter()
+ .filter(|cfg| cfg.is_highlight_initialized())
+ {
+ config.reconfigure(scopes);
}
+ self.theme = theme;
self._refresh();
}
- #[inline]
- pub fn language_server_by_id(
- &self,
- language_server_id: LanguageServerId,
- ) -> Option<&helix_lsp::Client> {
- self.language_servers
- .get_by_id(language_server_id)
- .map(|client| &**client)
- }
-
- /// Refreshes the language server for a given document
- pub fn refresh_language_servers(&mut self, doc_id: DocumentId) {
- self.launch_language_servers(doc_id)
- }
-
- /// moves/renames a path, invoking any event handlers (currently only lsp)
- /// and calling `set_doc_path` if the file is open in the editor
- pub fn move_path(&mut self, old_path: &Path, new_path: &Path) -> io::Result<()> {
- let new_path = canonicalize(new_path);
- // sanity check
- if old_path == new_path {
- return Ok(());
- }
- let is_dir = old_path.is_dir();
- let language_servers: Vec<_> = self
- .language_servers
- .iter_clients()
- .filter(|client| client.is_initialized())
- .cloned()
- .collect();
- for language_server in language_servers {
- let Some(request) = language_server.will_rename(old_path, &new_path, is_dir) else {
- continue;
- };
- let edit = match helix_lsp::block_on(request) {
- Ok(edit) => edit.unwrap_or_default(),
- Err(err) => {
- log::error!("invalid willRename response: {err:?}");
- continue;
- }
- };
- if let Err(err) = self.apply_workspace_edit(language_server.offset_encoding(), &edit) {
- log::error!("failed to apply workspace edit: {err:?}")
- }
- }
-
- if old_path.exists() {
- fs::rename(old_path, &new_path)?;
- }
-
- if let Some(doc) = self.document_by_path(old_path) {
- self.set_doc_path(doc.id(), &new_path);
- }
- let is_dir = new_path.is_dir();
- for ls in self.language_servers.iter_clients() {
- // A new language server might have been started in `set_doc_path` and won't
- // be initialized yet. Skip the `did_rename` notification for this server.
- if !ls.is_initialized() {
- continue;
- }
- ls.did_rename(old_path, &new_path, is_dir);
- }
- self.language_servers
- .file_event_handler
- .file_changed(old_path.to_owned());
- self.language_servers
- .file_event_handler
- .file_changed(new_path);
+ pub fn set_theme_from_name(&mut self, theme: &str) -> anyhow::Result<()> {
+ use anyhow::Context;
+ let theme = self
+ .theme_loader
+ .load(theme.as_ref())
+ .with_context(|| format!("failed setting theme `{}`", theme))?;
+ self.set_theme(theme);
Ok(())
}
- pub fn set_doc_path(&mut self, doc_id: DocumentId, path: &Path) {
- let doc = doc_mut!(self, &doc_id);
- let old_path = doc.path();
-
- if let Some(old_path) = old_path {
- // sanity check, should not occur but some callers (like an LSP) may
- // create bogus calls
- if old_path == path {
- return;
- }
- // if we are open in LSPs send did_close notification
- for language_server in doc.language_servers() {
- language_server.text_document_did_close(doc.identifier());
- }
- }
- // we need to clear the list of language servers here so that
- // refresh_doc_language/refresh_language_servers doesn't resend
- // text_document_did_close. Since we called `text_document_did_close`
- // we have fully unregistered this document from its LS
- doc.language_servers.clear();
- doc.set_path(Some(path));
- doc.detect_editor_config();
- self.refresh_doc_language(doc_id)
- }
-
- pub fn refresh_doc_language(&mut self, doc_id: DocumentId) {
- let loader = self.syn_loader.load();
- let doc = doc_mut!(self, &doc_id);
- doc.detect_language(&loader);
- doc.detect_editor_config();
- doc.detect_indent_and_line_ending();
- self.refresh_language_servers(doc_id);
- let doc = doc_mut!(self, &doc_id);
- let diagnostics = Editor::doc_diagnostics(&self.language_servers, &self.diagnostics, doc);
- doc.replace_diagnostics(diagnostics, &[], None);
- doc.reset_all_inlay_hints();
- }
-
- /// Launch a language server for a given document
- fn launch_language_servers(&mut self, doc_id: DocumentId) {
- if !self.config().lsp.enable {
- return;
- }
- // if doc doesn't have a URL it's a scratch buffer, ignore it
- let Some(doc) = self.documents.get_mut(&doc_id) else {
- return;
- };
- let Some(doc_url) = doc.url() else {
- return;
- };
- let (lang, path) = (doc.language.clone(), doc.path().cloned());
- let config = doc.config.load();
- let root_dirs = &config.workspace_lsp_roots;
-
- // store only successfully started language servers
- let language_servers = lang.as_ref().map_or_else(HashMap::default, |language| {
- self.language_servers
- .get(language, path.as_ref(), root_dirs, config.lsp.snippets)
- .filter_map(|(lang, client)| match client {
- Ok(client) => Some((lang, client)),
- Err(err) => {
- if let helix_lsp::Error::ExecutableNotFound(err) = err {
- // Silence by default since some language servers might just not be installed
- log::debug!(
- "Language server not found for `{}` {} {}", language.scope, lang, err,
- );
- } else {
- log::error!(
- "Failed to initialize the language servers for `{}` - `{}` {{ {} }}",
- language.scope,
- lang,
- err
- );
- }
- None
- }
- })
- .collect::<HashMap<_, _>>()
- });
-
- if language_servers.is_empty() && doc.language_servers.is_empty() {
- return;
- }
-
- let language_id = doc.language_id().map(ToOwned::to_owned).unwrap_or_default();
-
- // only spawn new language servers if the servers aren't the same
- let doc_language_servers_not_in_registry =
- doc.language_servers.iter().filter(|(name, doc_ls)| {
- language_servers
- .get(*name)
- .is_none_or(|ls| ls.id() != doc_ls.id())
- });
-
- for (_, language_server) in doc_language_servers_not_in_registry {
- language_server.text_document_did_close(doc.identifier());
- }
-
- let language_servers_not_in_doc = language_servers.iter().filter(|(name, ls)| {
- doc.language_servers
- .get(*name)
- .is_none_or(|doc_ls| ls.id() != doc_ls.id())
- });
-
- for (_, language_server) in language_servers_not_in_doc {
- // TODO: this now races with on_init code if the init happens too quickly
- language_server.text_document_did_open(
- doc_url.clone(),
- doc.version(),
- doc.text(),
- language_id.clone(),
- );
- }
-
- doc.language_servers = language_servers;
- }
-
fn _refresh(&mut self) {
- let config = self.config();
-
- // Reset the inlay hints annotations *before* updating the views, that way we ensure they
- // will disappear during the `.sync_change(doc)` call below.
- //
- // We can't simply check this config when rendering because inlay hints are only parts of
- // the possible annotations, and others could still be active, so we need to selectively
- // drop the inlay hints.
- if !config.lsp.display_inlay_hints {
- for doc in self.documents_mut() {
- doc.reset_all_inlay_hints();
- }
- }
-
for (view, _) in self.tree.views_mut() {
- let doc = doc_mut!(self, &view.doc);
- view.sync_changes(doc);
- view.gutters = config.gutters.clone();
- view.ensure_cursor_in_view(doc, config.scrolloff)
+ let doc = &self.documents[view.doc];
+ view.ensure_cursor_in_view(doc, self.config.scrolloff)
}
}
- fn replace_document_in_view(&mut self, current_view: ViewId, doc_id: DocumentId) {
- let scrolloff = self.config().scrolloff;
- let view = self.tree.get_mut(current_view);
-
- view.doc = doc_id;
- let doc = doc_mut!(self, &doc_id);
-
- doc.ensure_view_init(view.id);
- view.sync_changes(doc);
- doc.mark_as_focused();
-
- view.ensure_cursor_in_view(doc, scrolloff)
- }
-
pub fn switch(&mut self, id: DocumentId, action: Action) {
use crate::tree::Layout;
+ use helix_core::Selection;
- if !self.documents.contains_key(&id) {
+ if !self.documents.contains_key(id) {
log::error!("cannot switch to document that does not exist (anymore)");
return;
}
- if !matches!(action, Action::Load) {
- self.enter_normal_mode();
- }
-
- let focust_lost = match action {
+ match action {
Action::Replace => {
- let (view, doc) = current_ref!(self);
- // If the current view is an empty scratch buffer and is not displayed in any other views, delete it.
- // Boolean value is determined before the call to `view_mut` because the operation requires a borrow
- // of `self.tree`, which is mutably borrowed when `view_mut` is called.
- let remove_empty_scratch = !doc.is_modified()
- // If the buffer has no path and is not modified, it is an empty scratch buffer.
- && doc.path().is_none()
- // If the buffer we are changing to is not this buffer
- && id != doc.id
- // Ensure the buffer is not displayed in any other splits.
- && !self
- .tree
- .traverse()
- .any(|(_, v)| v.doc == doc.id && v.id != view.id);
-
- let (view, doc) = current!(self);
- let view_id = view.id;
-
- // Append any outstanding changes to history in the old document.
- doc.append_changes_to_history(view);
+ let view = view!(self);
+ let jump = (
+ view.doc,
+ self.documents[view.doc].selection(view.id).clone(),
+ );
- if remove_empty_scratch {
- // Copy `doc.id` into a variable before calling `self.documents.remove`, which requires a mutable
- // borrow, invalidating direct access to `doc.id`.
- let id = doc.id;
- self.documents.remove(&id);
+ let view = view_mut!(self);
+ view.jumps.push(jump);
+ view.last_accessed_doc = Some(view.doc);
+ view.doc = id;
+ view.offset = Position::default();
- // Remove the scratch buffer from any jumplists
- for (view, _) in self.tree.views_mut() {
- view.remove_document(&id);
- }
- } else {
- let jump = (view.doc, doc.selection(view.id).clone());
- view.jumps.push(jump);
- // Set last accessed doc if it is a different document
- if doc.id != id {
- view.add_to_history(view.doc);
- // Set last modified doc if modified and last modified doc is different
- if std::mem::take(&mut doc.modified_since_accessed)
- && view.last_modified_docs[0] != Some(view.doc)
- {
- view.last_modified_docs = [Some(view.doc), view.last_modified_docs[0]];
- }
- }
- }
+ let (view, doc) = current!(self);
- self.replace_document_in_view(view_id, id);
+ // initialize selection for view
+ doc.selections
+ .entry(view.id)
+ .or_insert_with(|| Selection::point(0));
+ // TODO: reuse align_view
+ let pos = doc
+ .selection(view.id)
+ .primary()
+ .cursor(doc.text().slice(..));
+ let line = doc.text().char_to_line(pos);
+ view.offset.row = line.saturating_sub(view.inner_area().height as usize / 2);
- dispatch(DocumentFocusLost {
- editor: self,
- doc: id,
- });
return;
}
Action::Load => {
- let view_id = view!(self).id;
- let doc = doc_mut!(self, &id);
- doc.ensure_view_init(view_id);
- doc.mark_as_focused();
return;
}
- Action::HorizontalSplit | Action::VerticalSplit => {
- let focus_lost = self.tree.try_get(self.tree.focus).map(|view| view.doc);
- // copy the current view, unless there is no view yet
- let view = self
- .tree
- .try_get(self.tree.focus)
- .filter(|v| id == v.doc) // Different Document
- .cloned()
- .unwrap_or_else(|| View::new(id, self.config().gutters.clone()));
- let view_id = self.tree.split(
- view,
- match action {
- Action::HorizontalSplit => Layout::Horizontal,
- Action::VerticalSplit => Layout::Vertical,
- _ => unreachable!(),
- },
- );
+ Action::HorizontalSplit => {
+ let view = View::new(id);
+ let view_id = self.tree.split(view, Layout::Horizontal);
// initialize selection for view
- let doc = doc_mut!(self, &id);
- doc.ensure_view_init(view_id);
- doc.mark_as_focused();
- focus_lost
+ let doc = &mut self.documents[id];
+ doc.selections.insert(view_id, Selection::point(0));
+ }
+ Action::VerticalSplit => {
+ let view = View::new(id);
+ let view_id = self.tree.split(view, Layout::Vertical);
+ // initialize selection for view
+ let doc = &mut self.documents[id];
+ doc.selections.insert(view_id, Selection::point(0));
}
- };
-
- self._refresh();
- if let Some(focus_lost) = focust_lost {
- dispatch(DocumentFocusLost {
- editor: self,
- doc: focus_lost,
- });
}
- }
-
- /// Generate an id for a new document and register it.
- fn new_document(&mut self, mut doc: Document) -> DocumentId {
- let id = self.next_document_id;
- // Safety: adding 1 from 1 is fine, probably impossible to reach usize max
- self.next_document_id =
- DocumentId(unsafe { NonZeroUsize::new_unchecked(self.next_document_id.0.get() + 1) });
- doc.id = id;
- self.documents.insert(id, doc);
-
- let (save_sender, save_receiver) = tokio::sync::mpsc::unbounded_channel();
- self.saves.insert(id, save_sender);
- let stream = UnboundedReceiverStream::new(save_receiver).flatten();
- self.save_queue.push(stream);
-
- id
+ self._refresh();
}
- fn new_file_from_document(&mut self, action: Action, doc: Document) -> DocumentId {
- let id = self.new_document(doc);
+ pub fn new_file(&mut self, action: Action) -> DocumentId {
+ let doc = Document::default();
+ let id = self.documents.insert(doc);
+ self.documents[id].id = id;
self.switch(id, action);
id
}
- pub fn new_file(&mut self, action: Action) -> DocumentId {
- self.new_file_from_document(
- action,
- Document::default(self.config.clone(), self.syn_loader.clone()),
- )
- }
+ pub fn open(&mut self, path: PathBuf, action: Action) -> Result<DocumentId, Error> {
+ let path = helix_core::path::get_canonicalized_path(&path)?;
- pub fn new_file_from_stdin(&mut self, action: Action) -> Result<DocumentId, Error> {
- let (stdin, encoding, has_bom) = crate::document::read_to_string(&mut stdin(), None)?;
- let doc = Document::from(
- helix_core::Rope::default(),
- Some((encoding, has_bom)),
- self.config.clone(),
- self.syn_loader.clone(),
- );
- let doc_id = self.new_file_from_document(action, doc);
- let doc = doc_mut!(self, &doc_id);
- let view = view_mut!(self);
- doc.ensure_view_init(view.id);
- let transaction =
- helix_core::Transaction::insert(doc.text(), doc.selection(view.id), stdin.into())
- .with_selection(Selection::point(0));
- doc.apply(&transaction, view.id);
- doc.append_changes_to_history(view);
- Ok(doc_id)
- }
-
- pub fn document_id_by_path(&self, path: &Path) -> Option<DocumentId> {
- self.document_by_path(path).map(|doc| doc.id)
- }
-
- // ??? possible use for integration tests
- pub fn open(&mut self, path: &Path, action: Action) -> Result<DocumentId, DocumentOpenError> {
- let path = helix_stdx::path::canonicalize(path);
- let id = self.document_id_by_path(&path);
+ let id = self
+ .documents()
+ .find(|doc| doc.path() == Some(&path))
+ .map(|doc| doc.id);
let id = if let Some(id) = id {
id
} else {
- let mut doc = Document::open(
- &path,
- None,
- true,
- self.config.clone(),
- self.syn_loader.clone(),
- )?;
+ let mut doc = Document::open(&path, None, Some(&self.theme), Some(&self.syn_loader))?;
- let diagnostics =
- Editor::doc_diagnostics(&self.language_servers, &self.diagnostics, &doc);
- doc.replace_diagnostics(diagnostics, &[], None);
+ // try to find a language server based on the language name
+ let language_server = doc.language.as_ref().and_then(|language| {
+ self.language_servers
+ .get(language)
+ .map_err(|e| {
+ log::error!("Failed to get LSP, {}, for `{}`", e, language.scope())
+ })
+ .ok()
+ });
- if let Some(diff_base) = self.diff_providers.get_diff_base(&path) {
- doc.set_diff_base(diff_base);
+ if let Some(language_server) = language_server {
+ let language_id = doc
+ .language()
+ .and_then(|s| s.split('.').last()) // source.rust
+ .map(ToOwned::to_owned)
+ .unwrap_or_default();
+
+ // TODO: this now races with on_init code if the init happens too quickly
+ tokio::spawn(language_server.text_document_did_open(
+ doc.url().unwrap(),
+ doc.version(),
+ doc.text(),
+ language_id,
+ ));
+
+ doc.set_language_server(Some(language_server));
}
- doc.set_version_control_head(self.diff_providers.get_current_head_name(&path));
-
- let id = self.new_document(doc);
- self.launch_language_servers(id);
-
- helix_event::dispatch(DocumentDidOpen {
- editor: self,
- doc: id,
- });
+ let id = self.documents.insert(doc);
+ self.documents[id].id = id;
id
};
self.switch(id, action);
-
Ok(id)
}
- pub fn close(&mut self, id: ViewId) {
- // Remove selections for the closed view on all documents.
- for doc in self.documents_mut() {
- doc.remove_view(id);
- }
- self.tree.remove(id);
- self._refresh();
- }
-
- pub fn close_document(&mut self, doc_id: DocumentId, force: bool) -> Result<(), CloseError> {
- let doc = match self.documents.get(&doc_id) {
- Some(doc) => doc,
- None => return Err(CloseError::DoesNotExist),
- };
- if !force && doc.is_modified() {
- return Err(CloseError::BufferModified(doc.display_name().into_owned()));
- }
+ pub fn close(&mut self, id: ViewId, close_buffer: bool) {
+ let view = self.tree.get(self.tree.focus);
+ // remove selection
+ self.documents[view.doc].selections.remove(&id);
- // This will also disallow any follow-up writes
- self.saves.remove(&doc_id);
-
- enum Action {
- Close(ViewId),
- ReplaceDoc(ViewId, DocumentId),
- }
+ if close_buffer {
+ // get around borrowck issues
+ let doc = &self.documents[view.doc];
- let actions: Vec<Action> = self
- .tree
- .views_mut()
- .filter_map(|(view, _focus)| {
- view.remove_document(&doc_id);
-
- if view.doc == doc_id {
- // something was previously open in the view, switch to previous doc
- if let Some(prev_doc) = view.docs_access_history.pop() {
- Some(Action::ReplaceDoc(view.id, prev_doc))
- } else {
- // only the document that is being closed was in the view, close it
- Some(Action::Close(view.id))
- }
- } else {
- None
- }
- })
- .collect();
-
- for action in actions {
- match action {
- Action::Close(view_id) => {
- self.close(view_id);
- }
- Action::ReplaceDoc(view_id, doc_id) => {
- self.replace_document_in_view(view_id, doc_id);
- }
+ if let Some(language_server) = doc.language_server() {
+ tokio::spawn(language_server.text_document_did_close(doc.identifier()));
}
+ self.documents.remove(view.doc);
}
- let doc = self.documents.remove(&doc_id).unwrap();
-
- // If the document we removed was visible in all views, we will have no more views. We don't
- // want to close the editor just for a simple buffer close, so we need to create a new view
- // containing either an existing document, or a brand new document.
- if self.tree.views().next().is_none() {
- let doc_id = self
- .documents
- .iter()
- .map(|(&doc_id, _)| doc_id)
- .next()
- .unwrap_or_else(|| {
- self.new_document(Document::default(
- self.config.clone(),
- self.syn_loader.clone(),
- ))
- });
- let view = View::new(doc_id, self.config().gutters.clone());
- let view_id = self.tree.insert(view);
- let doc = doc_mut!(self, &doc_id);
- doc.ensure_view_init(view_id);
- doc.mark_as_focused();
- }
-
+ self.tree.remove(id);
self._refresh();
-
- helix_event::dispatch(DocumentDidClose { editor: self, doc });
-
- Ok(())
- }
-
- pub fn save<P: Into<PathBuf>>(
- &mut self,
- doc_id: DocumentId,
- path: Option<P>,
- force: bool,
- ) -> anyhow::Result<()> {
- // convert a channel of futures to pipe into main queue one by one
- // via stream.then() ? then push into main future
-
- let path = path.map(|path| path.into());
- let doc = doc_mut!(self, &doc_id);
- let doc_save_future = doc.save(path, force)?;
-
- // When a file is written to, notify the file event handler.
- // Note: This can be removed once proper file watching is implemented.
- let handler = self.language_servers.file_event_handler.clone();
- let future = async move {
- let res = doc_save_future.await;
- if let Ok(event) = &res {
- handler.file_changed(event.path.clone());
- }
- res
- };
-
- use futures_util::stream;
-
- self.saves
- .get(&doc_id)
- .ok_or_else(|| anyhow::format_err!("saves are closed for this document!"))?
- .send(stream::once(Box::pin(future)))
- .map_err(|err| anyhow!("failed to send save event: {}", err))?;
-
- self.write_count += 1;
-
- Ok(())
}
pub fn resize(&mut self, area: Rect) {
@@ -2043,53 +351,8 @@ impl Editor {
};
}
- pub fn focus(&mut self, view_id: ViewId) {
- if self.tree.focus == view_id {
- return;
- }
-
- // Reset mode to normal and ensure any pending changes are committed in the old document.
- self.enter_normal_mode();
- let (view, doc) = current!(self);
- doc.append_changes_to_history(view);
- self.ensure_cursor_in_view(view_id);
- // Update jumplist selections with new document changes.
- for (view, _focused) in self.tree.views_mut() {
- let doc = doc_mut!(self, &view.doc);
- view.sync_changes(doc);
- }
-
- let prev_id = std::mem::replace(&mut self.tree.focus, view_id);
- doc_mut!(self).mark_as_focused();
-
- let focus_lost = self.tree.get(prev_id).doc;
- dispatch(DocumentFocusLost {
- editor: self,
- doc: focus_lost,
- });
- }
-
pub fn focus_next(&mut self) {
- self.focus(self.tree.next());
- }
-
- pub fn focus_prev(&mut self) {
- self.focus(self.tree.prev());
- }
-
- pub fn focus_direction(&mut self, direction: tree::Direction) {
- let current_view = self.tree.focus;
- if let Some(id) = self.tree.find_split_in_direction(current_view, direction) {
- self.focus(id)
- }
- }
-
- pub fn swap_split_in_direction(&mut self, direction: tree::Direction) {
- self.tree.swap_split_in_direction(direction);
- }
-
- pub fn transpose_view(&mut self) {
- self.tree.transpose();
+ self.tree.focus_next();
}
pub fn should_close(&self) -> bool {
@@ -2097,20 +360,19 @@ impl Editor {
}
pub fn ensure_cursor_in_view(&mut self, id: ViewId) {
- let config = self.config();
- let view = self.tree.get(id);
- let doc = doc_mut!(self, &view.doc);
- view.ensure_cursor_in_view(doc, config.scrolloff)
+ let view = self.tree.get_mut(id);
+ let doc = &self.documents[view.doc];
+ view.ensure_cursor_in_view(doc, self.config.scrolloff)
}
#[inline]
pub fn document(&self, id: DocumentId) -> Option<&Document> {
- self.documents.get(&id)
+ self.documents.get(id)
}
#[inline]
pub fn document_mut(&mut self, id: DocumentId) -> Option<&mut Document> {
- self.documents.get_mut(&id)
+ self.documents.get_mut(id)
}
#[inline]
@@ -2133,91 +395,31 @@ impl Editor {
.find(|doc| doc.path().map(|p| p == path.as_ref()).unwrap_or(false))
}
- /// Returns all supported diagnostics for the document
- pub fn doc_diagnostics<'a>(
- language_servers: &'a helix_lsp::Registry,
- diagnostics: &'a Diagnostics,
- document: &Document,
- ) -> impl Iterator<Item = helix_core::Diagnostic> + 'a {
- Editor::doc_diagnostics_with_filter(language_servers, diagnostics, document, |_, _| true)
- }
-
- /// Returns all supported diagnostics for the document
- /// filtered by `filter` which is invocated with the raw `lsp::Diagnostic` and the language server id it came from
- pub fn doc_diagnostics_with_filter<'a>(
- language_servers: &'a helix_lsp::Registry,
- diagnostics: &'a Diagnostics,
- document: &Document,
- filter: impl Fn(&lsp::Diagnostic, &DiagnosticProvider) -> bool + 'a,
- ) -> impl Iterator<Item = helix_core::Diagnostic> + 'a {
- let text = document.text().clone();
- let language_config = document.language.clone();
- document
- .uri()
- .and_then(|uri| diagnostics.get(&uri))
- .map(|diags| {
- diags.iter().filter_map(move |(diagnostic, provider)| {
- let server_id = provider.language_server_id()?;
- let ls = language_servers.get_by_id(server_id)?;
- language_config
- .as_ref()
- .and_then(|c| {
- c.language_servers.iter().find(|features| {
- features.name == ls.name()
- && features.has_feature(LanguageServerFeature::Diagnostics)
- })
- })
- .and_then(|_| {
- if filter(diagnostic, provider) {
- Document::lsp_diagnostic_to_diagnostic(
- &text,
- language_config.as_deref(),
- diagnostic,
- provider.clone(),
- ls.offset_encoding(),
- )
- } else {
- None
- }
- })
- })
- })
- .into_iter()
- .flatten()
- }
-
- /// Gets the primary cursor position in screen coordinates,
- /// or `None` if the primary cursor is not visible on screen.
pub fn cursor(&self) -> (Option<Position>, CursorKind) {
- let config = self.config();
- let (view, doc) = current_ref!(self);
- if let Some(mut pos) = self.cursor_cache.get(view, doc) {
- let inner = view.inner_area(doc);
+ let view = view!(self);
+ let doc = &self.documents[view.doc];
+ let cursor = doc
+ .selection(view.id)
+ .primary()
+ .cursor(doc.text().slice(..));
+ if let Some(mut pos) = view.screen_coords_at_pos(doc, doc.text().slice(..), cursor) {
+ let inner = view.inner_area();
pos.col += inner.x as usize;
pos.row += inner.y as usize;
- let cursorkind = config.cursor_shape.from_mode(self.mode);
- (Some(pos), cursorkind)
+ (Some(pos), CursorKind::Hidden)
} else {
- (None, CursorKind::default())
+ (None, CursorKind::Hidden)
}
}
- /// Closes language servers with timeout. The default timeout is 10000 ms, use
+ /// Closes language servers with timeout. The default timeout is 500 ms, use
/// `timeout` parameter to override this.
pub async fn close_language_servers(
&self,
timeout: Option<u64>,
) -> Result<(), tokio::time::error::Elapsed> {
- // Remove all language servers from the file event handler.
- // Note: this is non-blocking.
- for client in self.language_servers.iter_clients() {
- self.language_servers
- .file_event_handler
- .remove_client(client.id());
- }
-
tokio::time::timeout(
- Duration::from_millis(timeout.unwrap_or(3000)),
+ Duration::from_millis(timeout.unwrap_or(500)),
future::join_all(
self.language_servers
.iter_clients()
@@ -2227,199 +429,4 @@ impl Editor {
.await
.map(|_| ())
}
-
- pub async fn wait_event(&mut self) -> EditorEvent {
- // the loop only runs once or twice and would be better implemented with a recursion + const generic
- // however due to limitations with async functions that can not be implemented right now
- loop {
- tokio::select! {
- biased;
-
- Some(event) = self.save_queue.next() => {
- self.write_count -= 1;
- return EditorEvent::DocumentSaved(event)
- }
- Some(config_event) = self.config_events.1.recv() => {
- return EditorEvent::ConfigEvent(config_event)
- }
- Some(message) = self.language_servers.incoming.next() => {
- return EditorEvent::LanguageServerMessage(message)
- }
- Some(event) = self.debug_adapters.incoming.next() => {
- return EditorEvent::DebuggerEvent(event)
- }
-
- _ = helix_event::redraw_requested() => {
- if !self.needs_redraw{
- self.needs_redraw = true;
- let timeout = Instant::now() + Duration::from_millis(33);
- if timeout < self.idle_timer.deadline() && timeout < self.redraw_timer.deadline(){
- self.redraw_timer.as_mut().reset(timeout)
- }
- }
- }
-
- _ = &mut self.redraw_timer => {
- self.redraw_timer.as_mut().reset(Instant::now() + Duration::from_secs(86400 * 365 * 30));
- return EditorEvent::Redraw
- }
- _ = &mut self.idle_timer => {
- return EditorEvent::IdleTimer
- }
- }
- }
- }
-
- pub async fn flush_writes(&mut self) -> anyhow::Result<()> {
- while self.write_count > 0 {
- if let Some(save_event) = self.save_queue.next().await {
- self.write_count -= 1;
-
- let save_event = match save_event {
- Ok(event) => event,
- Err(err) => {
- self.set_error(err.to_string());
- bail!(err);
- }
- };
-
- let doc = doc_mut!(self, &save_event.doc_id);
- doc.set_last_saved_revision(save_event.revision, save_event.save_time);
- }
- }
-
- Ok(())
- }
-
- /// Switches the editor into normal mode.
- pub fn enter_normal_mode(&mut self) {
- use helix_core::graphemes;
-
- if self.mode == Mode::Normal {
- return;
- }
-
- self.mode = Mode::Normal;
- let (view, doc) = current!(self);
-
- try_restore_indent(doc, view);
-
- // if leaving append mode, move cursor back by 1
- if doc.restore_cursor {
- let text = doc.text().slice(..);
- let selection = doc.selection(view.id).clone().transform(|range| {
- let mut head = range.to();
- if range.head > range.anchor {
- head = graphemes::prev_grapheme_boundary(text, head);
- }
-
- Range::new(range.from(), head)
- });
-
- doc.set_selection(view.id, selection);
- doc.restore_cursor = false;
- }
- }
-
- pub fn current_stack_frame(&self) -> Option<&dap::StackFrame> {
- self.debug_adapters.current_stack_frame()
- }
-
- /// Returns the id of a view that this doc contains a selection for,
- /// making sure it is synced with the current changes
- /// if possible or there are no selections returns current_view
- /// otherwise uses an arbitrary view
- pub fn get_synced_view_id(&mut self, id: DocumentId) -> ViewId {
- let current_view = view_mut!(self);
- let doc = self.documents.get_mut(&id).unwrap();
- if doc.selections().contains_key(&current_view.id) {
- // only need to sync current view if this is not the current doc
- if current_view.doc != id {
- current_view.sync_changes(doc);
- }
- current_view.id
- } else if let Some(view_id) = doc.selections().keys().next() {
- let view_id = *view_id;
- let view = self.tree.get_mut(view_id);
- view.sync_changes(doc);
- view_id
- } else {
- doc.ensure_view_init(current_view.id);
- current_view.id
- }
- }
-
- pub fn set_cwd(&mut self, path: &Path) -> std::io::Result<()> {
- self.last_cwd = helix_stdx::env::set_current_working_dir(path)?;
- self.clear_doc_relative_paths();
- Ok(())
- }
-
- pub fn get_last_cwd(&mut self) -> Option<&Path> {
- self.last_cwd.as_deref()
- }
-}
-
-fn try_restore_indent(doc: &mut Document, view: &mut View) {
- use helix_core::{
- chars::char_is_whitespace,
- line_ending::{line_end_char_index, str_is_line_ending},
- unicode::segmentation::UnicodeSegmentation,
- Operation, Transaction,
- };
-
- fn inserted_a_new_blank_line(changes: &[Operation], pos: usize, line_end_pos: usize) -> bool {
- if let [Operation::Retain(move_pos), Operation::Insert(ref inserted_str), Operation::Retain(_)] =
- changes
- {
- let mut graphemes = inserted_str.graphemes(true);
- move_pos + inserted_str.len() == pos
- && graphemes.next().is_some_and(str_is_line_ending)
- && graphemes.all(|g| g.chars().all(char_is_whitespace))
- && pos == line_end_pos // ensure no characters exists after current position
- } else {
- false
- }
- }
-
- let doc_changes = doc.changes().changes();
- let text = doc.text().slice(..);
- let range = doc.selection(view.id).primary();
- let pos = range.cursor(text);
- let line_end_pos = line_end_char_index(&text, range.cursor_line(text));
-
- if inserted_a_new_blank_line(doc_changes, pos, line_end_pos) {
- // Removes tailing whitespaces.
- let transaction =
- Transaction::change_by_selection(doc.text(), doc.selection(view.id), |range| {
- let line_start_pos = text.line_to_char(range.cursor_line(text));
- (line_start_pos, pos, None)
- });
- doc.apply(&transaction, view.id);
- }
-}
-
-#[derive(Default)]
-pub struct CursorCache(Cell<Option<Option<Position>>>);
-
-impl CursorCache {
- pub fn get(&self, view: &View, doc: &Document) -> Option<Position> {
- if let Some(pos) = self.0.get() {
- return pos;
- }
-
- let text = doc.text().slice(..);
- let cursor = doc.selection(view.id).primary().cursor(text);
- let res = view.screen_coords_at_pos(doc, text, cursor);
- self.set(res);
- res
- }
-
- pub fn set(&self, cursor_pos: Option<Position>) {
- self.0.set(Some(cursor_pos))
- }
-
- pub fn reset(&self) {
- self.0.set(None)
- }
}