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.rs327
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) {