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.rs671
1 files changed, 114 insertions, 557 deletions
diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs
index 7f8cff9c..eca488e7 100644
--- a/helix-view/src/editor.rs
+++ b/helix-view/src/editor.rs
@@ -1,10 +1,6 @@
use crate::{
- annotations::diagnostics::{DiagnosticFilter, InlineDiagnosticsConfig},
- clipboard::ClipboardProvider,
- document::{
- DocumentOpenError, DocumentSavedEventFuture, DocumentSavedEventResult, Mode, SavePoint,
- },
- events::{DocumentDidClose, DocumentDidOpen, DocumentFocusLost},
+ align_view,
+ document::{DocumentSavedEventFuture, DocumentSavedEventResult, Mode, SavePoint},
graphics::{CursorKind, Rect},
handlers::Handlers,
info::Info,
@@ -12,23 +8,23 @@ use crate::{
register::Registers,
theme::{self, Theme},
tree::{self, Tree},
- Document, DocumentId, View, ViewId,
+ view::ViewPosition,
+ Align, Document, DocumentId, View, ViewId,
};
-use helix_event::dispatch;
+use dap::StackFrame;
use helix_vcs::DiffProviderRegistry;
use futures_util::stream::select_all::SelectAll;
use futures_util::{future, StreamExt};
-use helix_lsp::{Call, LanguageServerId};
+use helix_lsp::Call;
use tokio_stream::wrappers::UnboundedReceiverStream;
use std::{
borrow::Cow,
cell::Cell,
- collections::{BTreeMap, HashMap, HashSet},
- fs,
- io::{self, stdin},
- num::{NonZeroU8, NonZeroUsize},
+ collections::{BTreeMap, HashMap},
+ io::stdin,
+ num::NonZeroUsize,
path::{Path, PathBuf},
pin::Pin,
sync::Arc,
@@ -44,25 +40,15 @@ use anyhow::{anyhow, bail, Error};
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,
+ syntax::{self, AutoPairConfig, IndentationHeuristic, LanguageServerFeature, SoftWrap},
+ Change, LineEnding, Position, Selection, NATIVE_LINE_ENDING,
};
-use helix_dap::{self as dap, registry::DebugAdapterId};
+use helix_dap as dap;
use helix_lsp::lsp;
-use helix_stdx::path::canonicalize;
use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer};
-use arc_swap::{
- access::{DynAccess, DynGuard},
- ArcSwap,
-};
-
-pub const DEFAULT_AUTO_SAVE_DELAY: u64 = 3000;
+use arc_swap::access::{DynAccess, DynGuard};
fn deserialize_duration_millis<'de, D>(deserializer: D) -> Result<Duration, D::Error>
where
@@ -223,74 +209,6 @@ impl Default for FilePickerConfig {
#[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)]
pub struct Config {
/// Padding to keep between the edge of the screen and the cursor when scrolling. Defaults to 5.
pub scrolloff: usize,
@@ -316,23 +234,10 @@ pub struct Config {
pub auto_pairs: AutoPairConfig,
/// 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,
+ /// Automatic save on focus lost. Defaults to false.
+ pub auto_save: bool,
/// Set a global text_width
pub text_width: usize,
/// Time in milliseconds since last keypress before idle timers trigger.
@@ -355,13 +260,9 @@ pub struct Config {
/// 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
@@ -392,16 +293,6 @@ pub struct Config {
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.
@@ -409,37 +300,10 @@ pub struct Config {
/// 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)]
+#[serde(rename_all = "kebab-case", default)]
pub struct SmartTabConfig {
pub enable: bool,
pub supersede_menu: bool,
@@ -486,7 +350,7 @@ pub fn get_terminal_provider() -> Option<TerminalConfig> {
})
}
-#[cfg(not(any(windows, target_arch = "wasm32")))]
+#[cfg(not(any(windows, target_os = "wasm32")))]
pub fn get_terminal_provider() -> Option<TerminalConfig> {
use helix_stdx::env::{binary_exists, env_var_is_set};
@@ -512,9 +376,7 @@ pub fn get_terminal_provider() -> Option<TerminalConfig> {
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
+ /// Display LSP progress messages below statusline
pub display_messages: bool,
/// Enable automatic pop up of signature help (parameter hints)
pub auto_signature_help: bool,
@@ -522,11 +384,6 @@ pub struct LspConfig {
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
@@ -537,15 +394,12 @@ impl Default for LspConfig {
fn default() -> Self {
Self {
enable: true,
- display_progress_messages: false,
- display_messages: true,
+ display_messages: false,
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,
}
}
}
@@ -567,8 +421,6 @@ pub struct StatusLineConfig {
pub right: Vec<StatusLineElement>,
pub separator: String,
pub mode: ModeConfig,
- pub diagnostics: Vec<Severity>,
- pub workspace_diagnostics: Vec<Severity>,
}
impl Default for StatusLineConfig {
@@ -593,8 +445,6 @@ impl Default for StatusLineConfig {
],
separator: String::from("│"),
mode: ModeConfig::default(),
- diagnostics: vec![Severity::Warning, Severity::Error],
- workspace_diagnostics: vec![Severity::Warning, Severity::Error],
}
}
}
@@ -632,9 +482,6 @@ pub enum StatusLineElement {
/// The relative file path
FileName,
- /// The file absolute path
- FileAbsolutePath,
-
// The file modification indicator
FileModificationIndicator,
@@ -647,9 +494,6 @@ pub enum StatusLineElement {
/// The file line endings (CRLF or LF)
FileLineEnding,
- /// The file indentation style
- FileIndentStyle,
-
/// The file type (language ID or "text")
FileType,
@@ -685,9 +529,6 @@ pub enum StatusLineElement {
/// 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
@@ -833,7 +674,6 @@ pub enum WhitespaceRender {
default: Option<WhitespaceRenderValue>,
space: Option<WhitespaceRenderValue>,
nbsp: Option<WhitespaceRenderValue>,
- nnbsp: Option<WhitespaceRenderValue>,
tab: Option<WhitespaceRenderValue>,
newline: Option<WhitespaceRenderValue>,
},
@@ -865,14 +705,6 @@ impl WhitespaceRender {
}
}
}
- 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,
@@ -891,67 +723,11 @@ impl WhitespaceRender {
}
}
-#[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,
@@ -962,7 +738,6 @@ impl Default for WhitespaceCharacters {
Self {
space: '·', // U+00B7
nbsp: '⍽', // U+237D
- nnbsp: '␣', // U+2423
tab: '→', // U+2192
newline: '⏎', // U+23CE
tabpad: ' ',
@@ -1037,22 +812,6 @@ pub enum PopupBorderConfig {
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 {
@@ -1071,18 +830,14 @@ impl Default for Config {
middle_click_paste: true,
auto_pairs: AutoPairConfig::default(),
auto_completion: true,
- path_completion: true,
- word_completion: WordCompletion::default(),
auto_format: true,
- default_yank_register: '"',
- auto_save: AutoSave::default(),
+ auto_save: false,
idle_timeout: Duration::from_millis(250),
completion_timeout: Duration::from_millis(250),
preview_completion_insert: true,
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,
@@ -1101,23 +856,12 @@ impl Default for Config {
},
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(),
}
}
}
@@ -1146,8 +890,6 @@ pub struct Breakpoint {
use futures_util::stream::{Flatten, Once};
-type Diagnostics = BTreeMap<Uri, Vec<(lsp::Diagnostic, DiagnosticProvider)>>;
-
pub struct Editor {
/// Current editing mode.
pub mode: Mode,
@@ -1167,13 +909,14 @@ pub struct Editor {
pub macro_recording: Option<(char, Vec<KeyEvent>)>,
pub macro_replaying: Vec<char>,
pub language_servers: helix_lsp::Registry,
- pub diagnostics: Diagnostics,
+ pub diagnostics: BTreeMap<lsp::Url, Vec<(lsp::Diagnostic, usize)>>,
pub diff_providers: DiffProviderRegistry,
- pub debug_adapters: dap::registry::Registry,
+ pub debugger: Option<dap::Client>,
+ pub debugger_events: SelectAll<UnboundedReceiverStream<dap::Payload>>,
pub breakpoints: HashMap<PathBuf, Vec<Breakpoint>>,
- 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.
@@ -1197,7 +940,6 @@ pub struct Editor {
redraw_timer: Pin<Box<Sleep>>,
last_motion: Option<Motion>,
pub last_completion: Option<CompleteAction>,
- last_cwd: Option<PathBuf>,
pub exit_code: i32,
@@ -1215,10 +957,8 @@ pub struct Editor {
/// 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 cursor_cache: Cell<Option<Option<Position>>>,
pub handlers: Handlers,
-
- pub mouse_down_range: Option<Range>,
- pub cursor_cache: CursorCache,
}
pub type Motion = Box<dyn Fn(&mut Editor)>;
@@ -1227,8 +967,8 @@ pub type Motion = Box<dyn Fn(&mut Editor)>;
pub enum EditorEvent {
DocumentSaved(DocumentSavedEventResult),
ConfigEvent(ConfigEvent),
- LanguageServerMessage((LanguageServerId, Call)),
- DebuggerEvent((DebugAdapterId, dap::Payload)),
+ LanguageServerMessage((usize, Call)),
+ DebuggerEvent(dap::Payload),
IdleTimer,
Redraw,
}
@@ -1255,7 +995,6 @@ pub enum CompleteAction {
Applied {
trigger_offset: usize,
changes: Vec<Change>,
- placeholder: bool,
},
}
@@ -1288,7 +1027,7 @@ impl Editor {
pub fn new(
mut area: Rect,
theme_loader: Arc<theme::Loader>,
- syn_loader: Arc<ArcSwap<syntax::Loader>>,
+ syn_loader: Arc<syntax::Loader>,
config: Arc<dyn DynAccess<Config>>,
handlers: Handlers,
) -> Self {
@@ -1313,33 +1052,29 @@ impl Editor {
macro_replaying: Vec::new(),
theme: theme_loader.default(),
language_servers,
- diagnostics: Diagnostics::new(),
+ diagnostics: BTreeMap::new(),
diff_providers: DiffProviderRegistry::default(),
- debug_adapters: dap::registry::Registry::new(),
+ debugger: None,
+ debugger_events: SelectAll::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,
- ))),
+ registers: Registers::default(),
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,
config,
auto_pairs,
exit_code: 0,
config_events: unbounded_channel(),
needs_redraw: false,
+ cursor_cache: Cell::new(None),
handlers,
- mouse_down_range: None,
- cursor_cache: CursorCache::default(),
}
}
@@ -1377,16 +1112,11 @@ impl Editor {
/// Call if the config has changed to let the editor update all
/// relevant members.
- pub fn refresh_config(&mut self, old_config: &Config) {
+ pub fn refresh_config(&mut self) {
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) {
@@ -1422,13 +1152,6 @@ impl Editor {
}
#[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))
}
@@ -1459,13 +1182,13 @@ impl Editor {
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() {
+ if theme.find_scope_index_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());
+ self.syn_loader.set_scopes(scopes.to_vec());
match preview {
ThemeAction::Preview => {
@@ -1483,13 +1206,8 @@ impl Editor {
}
#[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)
+ pub fn language_server_by_id(&self, language_server_id: usize) -> Option<&helix_lsp::Client> {
+ self.language_servers.get_by_id(language_server_id)
}
/// Refreshes the language server for a given document
@@ -1497,100 +1215,6 @@ impl Editor {
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);
- 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 {
@@ -1617,12 +1241,12 @@ impl Editor {
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,
+ "Language server not found for `{}` {} {}", language.scope(), lang, err,
);
} else {
log::error!(
"Failed to initialize the language servers for `{}` - `{}` {{ {} }}",
- language.scope,
+ language.scope(),
lang,
err
);
@@ -1633,7 +1257,7 @@ impl Editor {
.collect::<HashMap<_, _>>()
});
- if language_servers.is_empty() && doc.language_servers.is_empty() {
+ if language_servers.is_empty() {
return;
}
@@ -1644,27 +1268,27 @@ impl Editor {
doc.language_servers.iter().filter(|(name, doc_ls)| {
language_servers
.get(*name)
- .is_none_or(|ls| ls.id() != doc_ls.id())
+ .map_or(true, |ls| ls.id() != doc_ls.id())
});
for (_, language_server) in doc_language_servers_not_in_registry {
- language_server.text_document_did_close(doc.identifier());
+ tokio::spawn(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())
+ .map_or(true, |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(
+ tokio::spawn(language_server.text_document_did_open(
doc_url.clone(),
doc.version(),
doc.text(),
language_id.clone(),
- );
+ ));
}
doc.language_servers = language_servers;
@@ -1694,17 +1318,16 @@ impl Editor {
}
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);
+ view.offset = ViewPosition::default();
+ 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)
+ align_view(doc, view, Align::Center);
}
pub fn switch(&mut self, id: DocumentId, action: Action) {
@@ -1715,11 +1338,9 @@ impl Editor {
return;
}
- if !matches!(action, Action::Load) {
- self.enter_normal_mode();
- }
+ 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.
@@ -1769,10 +1390,6 @@ impl Editor {
self.replace_document_in_view(view_id, id);
- dispatch(DocumentFocusLost {
- editor: self,
- doc: id,
- });
return;
}
Action::Load => {
@@ -1783,7 +1400,6 @@ impl Editor {
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
@@ -1803,17 +1419,10 @@ impl Editor {
let doc = doc_mut!(self, &id);
doc.ensure_view_init(view_id);
doc.mark_as_focused();
- focus_lost
}
- };
+ }
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.
@@ -1841,10 +1450,7 @@ impl Editor {
}
pub fn new_file(&mut self, action: Action) -> DocumentId {
- self.new_file_from_document(
- action,
- Document::default(self.config.clone(), self.syn_loader.clone()),
- )
+ self.new_file_from_document(action, Document::default(self.config.clone()))
}
pub fn new_file_from_stdin(&mut self, action: Action) -> Result<DocumentId, Error> {
@@ -1853,7 +1459,6 @@ impl Editor {
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);
@@ -1867,14 +1472,10 @@ impl Editor {
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> {
+ pub fn open(&mut self, path: &Path, action: Action) -> Result<DocumentId, Error> {
let path = helix_stdx::path::canonicalize(path);
- let id = self.document_id_by_path(&path);
+ let id = self.document_by_path(&path).map(|doc| doc.id);
let id = if let Some(id) = id {
id
@@ -1882,9 +1483,8 @@ impl Editor {
let mut doc = Document::open(
&path,
None,
- true,
+ Some(self.syn_loader.clone()),
self.config.clone(),
- self.syn_loader.clone(),
)?;
let diagnostics =
@@ -1899,16 +1499,10 @@ impl Editor {
let id = self.new_document(doc);
self.launch_language_servers(id);
- helix_event::dispatch(DocumentDidOpen {
- editor: self,
- doc: id,
- });
-
id
};
self.switch(id, action);
-
Ok(id)
}
@@ -1922,7 +1516,7 @@ impl Editor {
}
pub fn close_document(&mut self, doc_id: DocumentId, force: bool) -> Result<(), CloseError> {
- let doc = match self.documents.get(&doc_id) {
+ let doc = match self.documents.get_mut(&doc_id) {
Some(doc) => doc,
None => return Err(CloseError::DoesNotExist),
};
@@ -1933,6 +1527,11 @@ impl Editor {
// This will also disallow any follow-up writes
self.saves.remove(&doc_id);
+ for language_server in doc.language_servers() {
+ // TODO: track error
+ tokio::spawn(language_server.text_document_did_close(doc.identifier()));
+ }
+
enum Action {
Close(ViewId),
ReplaceDoc(ViewId, DocumentId),
@@ -1969,7 +1568,7 @@ impl Editor {
}
}
- let doc = self.documents.remove(&doc_id).unwrap();
+ self.documents.remove(&doc_id);
// 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
@@ -1980,12 +1579,7 @@ impl Editor {
.iter()
.map(|(&doc_id, _)| doc_id)
.next()
- .unwrap_or_else(|| {
- self.new_document(Document::default(
- self.config.clone(),
- self.syn_loader.clone(),
- ))
- });
+ .unwrap_or_else(|| self.new_document(Document::default(self.config.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);
@@ -1995,8 +1589,6 @@ impl Editor {
self._refresh();
- helix_event::dispatch(DocumentDidClose { editor: self, doc });
-
Ok(())
}
@@ -2044,29 +1636,24 @@ impl Editor {
}
pub fn focus(&mut self, view_id: ViewId) {
- if self.tree.focus == view_id {
- return;
- }
+ let prev_id = std::mem::replace(&mut self.tree.focus, view_id);
- // 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);
- }
+ // if leaving the view: mode should reset and the cursor should be
+ // within view
+ if prev_id != view_id {
+ self.enter_normal_mode();
+ self.ensure_cursor_in_view(view_id);
- let prev_id = std::mem::replace(&mut self.tree.focus, view_id);
- doc_mut!(self).mark_as_focused();
+ // 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 focus_lost = self.tree.get(prev_id).doc;
- dispatch(DocumentFocusLost {
- editor: self,
- doc: focus_lost,
- });
+ let view = view!(self, view_id);
+ let doc = doc_mut!(self, &view.doc);
+ doc.mark_as_focused();
}
pub fn focus_next(&mut self) {
@@ -2098,8 +1685,8 @@ 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);
+ let view = self.tree.get_mut(id);
+ let doc = &self.documents[&view.doc];
view.ensure_cursor_in_view(doc, config.scrolloff)
}
@@ -2136,7 +1723,7 @@ impl Editor {
/// Returns all supported diagnostics for the document
pub fn doc_diagnostics<'a>(
language_servers: &'a helix_lsp::Registry,
- diagnostics: &'a Diagnostics,
+ diagnostics: &'a BTreeMap<lsp::Url, Vec<(lsp::Diagnostic, usize)>>,
document: &Document,
) -> impl Iterator<Item = helix_core::Diagnostic> + 'a {
Editor::doc_diagnostics_with_filter(language_servers, diagnostics, document, |_, _| true)
@@ -2146,19 +1733,20 @@ impl Editor {
/// 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,
+ diagnostics: &'a BTreeMap<lsp::Url, Vec<(lsp::Diagnostic, usize)>>,
+
document: &Document,
- filter: impl Fn(&lsp::Diagnostic, &DiagnosticProvider) -> bool + 'a,
+ filter: impl Fn(&lsp::Diagnostic, usize) -> bool + 'a,
) -> impl Iterator<Item = helix_core::Diagnostic> + 'a {
let text = document.text().clone();
let language_config = document.language.clone();
document
- .uri()
+ .path()
+ .and_then(|path| url::Url::from_file_path(path).ok()) // TODO log error?
.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)?;
+ diags.iter().filter_map(move |(diagnostic, lsp_id)| {
+ let ls = language_servers.get_by_id(*lsp_id)?;
language_config
.as_ref()
.and_then(|c| {
@@ -2168,12 +1756,12 @@ impl Editor {
})
})
.and_then(|_| {
- if filter(diagnostic, provider) {
+ if filter(diagnostic, *lsp_id) {
Document::lsp_diagnostic_to_diagnostic(
&text,
language_config.as_deref(),
diagnostic,
- provider.clone(),
+ *lsp_id,
ls.offset_encoding(),
)
} else {
@@ -2191,7 +1779,15 @@ impl Editor {
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 cursor = doc
+ .selection(view.id)
+ .primary()
+ .cursor(doc.text().slice(..));
+ let pos = self
+ .cursor_cache
+ .get()
+ .unwrap_or_else(|| view.screen_coords_at_pos(doc, doc.text().slice(..), cursor));
+ if let Some(mut pos) = pos {
let inner = view.inner_area(doc);
pos.col += inner.x as usize;
pos.row += inner.y as usize;
@@ -2245,7 +1841,7 @@ impl Editor {
Some(message) = self.language_servers.incoming.next() => {
return EditorEvent::LanguageServerMessage(message)
}
- Some(event) = self.debug_adapters.incoming.next() => {
+ Some(event) = self.debugger_events.next() => {
return EditorEvent::DebuggerEvent(event)
}
@@ -2284,7 +1880,7 @@ impl Editor {
};
let doc = doc_mut!(self, &save_event.doc_id);
- doc.set_last_saved_revision(save_event.revision, save_event.save_time);
+ doc.set_last_saved_revision(save_event.revision);
}
}
@@ -2293,7 +1889,7 @@ impl Editor {
/// Switches the editor into normal mode.
pub fn enter_normal_mode(&mut self) {
- use helix_core::graphemes;
+ use helix_core::{graphemes, Range};
if self.mode == Mode::Normal {
return;
@@ -2308,12 +1904,10 @@ impl Editor {
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)
+ Range::new(
+ range.from(),
+ graphemes::prev_grapheme_boundary(text, range.to()),
+ )
});
doc.set_selection(view.id, selection);
@@ -2321,8 +1915,10 @@ impl Editor {
}
}
- pub fn current_stack_frame(&self) -> Option<&dap::StackFrame> {
- self.debug_adapters.current_stack_frame()
+ pub fn current_stack_frame(&self) -> Option<&StackFrame> {
+ self.debugger
+ .as_ref()
+ .and_then(|debugger| debugger.current_stack_frame())
}
/// Returns the id of a view that this doc contains a selection for,
@@ -2348,34 +1944,20 @@ impl Editor {
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,
+ chars::char_is_whitespace, line_ending::line_end_char_index, 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))
+ && inserted_str.starts_with('\n')
+ && inserted_str.chars().skip(1).all(char_is_whitespace)
&& pos == line_end_pos // ensure no characters exists after current position
} else {
false
@@ -2398,28 +1980,3 @@ fn try_restore_indent(doc: &mut Document, view: &mut View) {
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)
- }
-}