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.rs | 327 |
1 files changed, 160 insertions, 167 deletions
diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 7f8cff9c..d9b20012 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -1,10 +1,12 @@ use crate::{ annotations::diagnostics::{DiagnosticFilter, InlineDiagnosticsConfig}, clipboard::ClipboardProvider, + diagnostic::DiagnosticProvider, document::{ - DocumentOpenError, DocumentSavedEventFuture, DocumentSavedEventResult, Mode, SavePoint, + self, DocumentOpenError, DocumentSavedEventFuture, DocumentSavedEventResult, Mode, + SavePoint, }, - events::{DocumentDidClose, DocumentDidOpen, DocumentFocusLost}, + events::{DiagnosticsDidChange, DocumentDidClose, DocumentDidOpen, DocumentFocusLost}, graphics::{CursorKind, Rect}, handlers::Handlers, info::Info, @@ -12,14 +14,16 @@ use crate::{ register::Registers, theme::{self, Theme}, tree::{self, Tree}, - Document, DocumentId, View, ViewId, + Diagnostic, Document, DocumentId, View, ViewId, }; +use dap::StackFrame; 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 parking_lot::RwLock; use tokio_stream::wrappers::UnboundedReceiverStream; use std::{ @@ -28,7 +32,7 @@ use std::{ collections::{BTreeMap, HashMap, HashSet}, fs, io::{self, stdin}, - num::{NonZeroU8, NonZeroUsize}, + num::NonZeroU8, path::{Path, PathBuf}, pin::Pin, sync::Arc, @@ -44,15 +48,13 @@ 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, + Change, LineEnding, Position, Range, Selection, SpellingLanguage, Uri, NATIVE_LINE_ENDING, }; -use helix_dap::{self as dap, registry::DebugAdapterId}; -use helix_lsp::lsp; +use helix_dap as dap; use helix_stdx::path::canonicalize; use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer}; @@ -221,49 +223,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, @@ -321,9 +280,6 @@ pub struct Config { /// 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 '"' @@ -361,7 +317,6 @@ pub struct Config { /// 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,10 +347,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, @@ -423,19 +374,6 @@ pub struct Config { /// 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)] @@ -685,9 +623,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 @@ -1037,22 +972,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 { @@ -1072,7 +991,6 @@ impl Default for Config { auto_pairs: AutoPairConfig::default(), auto_completion: true, path_completion: true, - word_completion: WordCompletion::default(), auto_format: true, default_yank_register: '"', auto_save: AutoSave::default(), @@ -1082,7 +1000,6 @@ impl Default for Config { 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, @@ -1105,7 +1022,6 @@ impl Default for Config { 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()), @@ -1113,11 +1029,9 @@ impl Default for Config { indent_heuristic: IndentationHeuristic::default(), jump_label_alphabet: ('a'..='z').collect(), inline_diagnostics: InlineDiagnosticsConfig::default(), - end_of_line_diagnostics: DiagnosticFilter::Enable(Severity::Hint), + end_of_line_diagnostics: DiagnosticFilter::Disable, clipboard_provider: ClipboardProvider::default(), editor_config: true, - rainbow_brackets: false, - kitty_keyboard_protocol: Default::default(), } } } @@ -1146,7 +1060,7 @@ pub struct Breakpoint { use futures_util::stream::{Flatten, Once}; -type Diagnostics = BTreeMap<Uri, Vec<(lsp::Diagnostic, DiagnosticProvider)>>; +type Diagnostics = BTreeMap<Uri, Vec<Diagnostic>>; pub struct Editor { /// Current editing mode. @@ -1170,7 +1084,8 @@ pub struct Editor { pub diagnostics: Diagnostics, 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>>, @@ -1219,8 +1134,12 @@ pub struct Editor { pub mouse_down_range: Option<Range>, pub cursor_cache: CursorCache, + + pub dictionaries: Dictionaries, } +type Dictionaries = HashMap<SpellingLanguage, Arc<RwLock<spellbook::Dictionary>>>; + pub type Motion = Box<dyn Fn(&mut Editor)>; #[derive(Debug)] @@ -1228,7 +1147,7 @@ pub enum EditorEvent { DocumentSaved(DocumentSavedEventResult), ConfigEvent(ConfigEvent), LanguageServerMessage((LanguageServerId, Call)), - DebuggerEvent((DebugAdapterId, dap::Payload)), + DebuggerEvent(dap::Payload), IdleTimer, Redraw, } @@ -1315,7 +1234,8 @@ impl Editor { language_servers, diagnostics: Diagnostics::new(), diff_providers: DiffProviderRegistry::default(), - debug_adapters: dap::registry::Registry::new(), + debugger: None, + debugger_events: SelectAll::new(), breakpoints: HashMap::new(), syn_loader, theme_loader, @@ -1340,6 +1260,7 @@ impl Editor { handlers, mouse_down_range: None, cursor_cache: CursorCache::default(), + dictionaries: HashMap::new(), } } @@ -1377,16 +1298,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) { @@ -1644,7 +1560,7 @@ 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 { @@ -1654,7 +1570,7 @@ impl Editor { 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 { @@ -1819,9 +1735,7 @@ impl Editor { /// 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) }); + self.next_document_id = self.next_document_id.next(); doc.id = id; self.documents.insert(id, doc); @@ -1848,7 +1762,7 @@ impl Editor { } 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 (stdin, encoding, has_bom) = document::read_to_string(&mut stdin(), None)?; let doc = Document::from( helix_core::Rope::default(), Some((encoding, has_bom)), @@ -2044,29 +1958,28 @@ 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() { + // 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); + + // 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 view = view!(self, view_id); let doc = doc_mut!(self, &view.doc); - view.sync_changes(doc); + doc.mark_as_focused(); + let focus_lost = self.tree.get(prev_id).doc; + dispatch(DocumentFocusLost { + editor: self, + doc: focus_lost, + }); } - - 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) { @@ -2138,8 +2051,8 @@ impl Editor { 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) + ) -> impl Iterator<Item = document::Diagnostic> + 'a { + Editor::doc_diagnostics_with_filter(language_servers, diagnostics, document, |_| true) } /// Returns all supported diagnostics for the document @@ -2148,38 +2061,38 @@ impl Editor { 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 { + filter: impl Fn(&Diagnostic) -> bool + 'a, + ) -> impl Iterator<Item = document::Diagnostic> + 'a { let text = document.text().clone(); let language_config = document.language.clone(); - document - .uri() - .and_then(|uri| diagnostics.get(&uri)) + diagnostics + .get(&document.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 - } - }) + diags.iter().filter_map(move |diagnostic| { + let language_server = diagnostic + .provider + .language_server_id() + .and_then(|id| language_servers.get_by_id(id)); + + if let Some((config, server)) = language_config.as_ref().zip(language_server) { + config.language_servers.iter().find(|features| { + features.name == server.name() + && features.has_feature(LanguageServerFeature::Diagnostics) + })?; + } + if diagnostic.severity.is_some_and(|severity| { + language_config + .as_ref() + .is_some_and(|config| severity < config.diagnostic_severity) + }) { + return None; + } + + if filter(diagnostic) { + diagnostic.to_document_diagnostic(&text) + } else { + None + } }) }) .into_iter() @@ -2245,7 +2158,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) } @@ -2321,8 +2234,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, @@ -2358,6 +2273,84 @@ impl Editor { pub fn get_last_cwd(&mut self) -> Option<&Path> { self.last_cwd.as_deref() } + + pub fn handle_diagnostics( + &mut self, + provider: &DiagnosticProvider, + uri: Uri, + version: Option<i32>, + mut diagnostics: Vec<Diagnostic>, + ) { + use std::collections::btree_map::Entry; + + let doc = self.documents.values_mut().find(|doc| doc.uri() == uri); + + if let Some((version, doc)) = version.zip(doc.as_ref()) { + if version != doc.version() { + log::info!("Version ({version}) is out of date for {uri:?} (expected ({})), dropping diagnostics", doc.version()); + return; + } + } + + let mut unchanged_diag_sources = Vec::new(); + if let Some((lang_conf, old_diagnostics)) = doc + .as_ref() + .and_then(|doc| Some((doc.language_config()?, self.diagnostics.get(&uri)?))) + { + if !lang_conf.persistent_diagnostic_sources.is_empty() { + diagnostics.sort(); + } + for source in &lang_conf.persistent_diagnostic_sources { + let new_diagnostics = diagnostics + .iter() + .filter(|d| d.source.as_ref() == Some(source)); + let old_diagnostics = old_diagnostics + .iter() + .filter(|d| &d.provider == provider && d.source.as_ref() == Some(source)); + if new_diagnostics.eq(old_diagnostics) { + unchanged_diag_sources.push(source.clone()) + } + } + } + + // Insert the original lsp::Diagnostics here because we may have no open document + // for diagnostic message and so we can't calculate the exact position. + // When using them later in the diagnostics picker, we calculate them on-demand. + let diagnostics = match self.diagnostics.entry(uri) { + Entry::Occupied(o) => { + let current_diagnostics = o.into_mut(); + // there may entries of other language servers, which is why we can't overwrite the whole entry + current_diagnostics.retain(|diagnostic| &diagnostic.provider != provider); + current_diagnostics.extend(diagnostics); + current_diagnostics + // Sort diagnostics first by severity and then by line numbers. + } + Entry::Vacant(v) => v.insert(diagnostics), + }; + + diagnostics.sort(); + + if let Some(doc) = doc { + let diagnostic_of_language_server_and_not_in_unchanged_sources = + |diagnostic: &crate::Diagnostic| { + &diagnostic.provider == provider + && diagnostic + .source + .as_ref() + .map_or(true, |source| !unchanged_diag_sources.contains(source)) + }; + let diagnostics = Self::doc_diagnostics_with_filter( + &self.language_servers, + &self.diagnostics, + doc, + diagnostic_of_language_server_and_not_in_unchanged_sources, + ); + doc.replace_diagnostics(diagnostics, &unchanged_diag_sources, Some(provider)); + + let doc = doc.id(); + helix_event::dispatch(DiagnosticsDidChange { editor: self, doc }); + } + } } fn try_restore_indent(doc: &mut Document, view: &mut View) { |