Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'helix-view/src/document.rs')
| -rw-r--r-- | helix-view/src/document.rs | 212 |
1 files changed, 69 insertions, 143 deletions
diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index e52dbe0f..7dde7985 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -5,12 +5,11 @@ use futures_util::future::BoxFuture; use futures_util::FutureExt; use helix_core::auto_pairs::AutoPairs; use helix_core::chars::char_is_word; -use helix_core::command_line::Token; use helix_core::diagnostic::DiagnosticProvider; use helix_core::doc_formatter::TextFormat; use helix_core::encoding::Encoding; use helix_core::snippets::{ActiveSnippet, SnippetRenderCtx}; -use helix_core::syntax::config::LanguageServerFeature; +use helix_core::syntax::{Highlight, LanguageServerFeature}; use helix_core::text_annotations::{InlineAnnotation, Overlay}; use helix_event::TaskController; use helix_lsp::util::lsp_pos_to_pos; @@ -39,14 +38,13 @@ use helix_core::{ history::{History, State, UndoKind}, indent::{auto_detect_indent_style, IndentStyle}, line_ending::auto_detect_line_ending, - syntax::{self, config::LanguageConfiguration}, + syntax::{self, LanguageConfiguration}, ChangeSet, Diagnostic, LineEnding, Range, Rope, RopeBuilder, Selection, Syntax, Transaction, }; use crate::{ editor::Config, events::{DocumentDidChange, SelectionDidChange}, - expansion, view::ViewPosition, DocumentId, Editor, Theme, View, ViewId, }; @@ -150,9 +148,6 @@ pub struct Document { /// To know if they're up-to-date, check the `id` field in `DocumentInlayHints`. pub(crate) inlay_hints: HashMap<ViewId, DocumentInlayHints>, pub(crate) jump_labels: HashMap<ViewId, Vec<Overlay>>, - /// Set to `true` when the document is updated, reset to `false` on the next inlay hints - /// update from the LSP - pub inlay_hints_oudated: bool, path: Option<PathBuf>, relative_path: OnceCell<Option<PathBuf>>, @@ -204,25 +199,19 @@ pub struct Document { pub readonly: bool, - pub previous_diagnostic_id: Option<String>, - /// Annotations for LSP document color swatches pub color_swatches: Option<DocumentColorSwatches>, // NOTE: ideally this would live on the handler for color swatches. This is blocked on a // large refactor that would make `&mut Editor` available on the `DocumentDidChange` event. pub color_swatch_controller: TaskController, - pub pull_diagnostic_controller: TaskController, - - // NOTE: this field should eventually go away - we should use the Editor's syn_loader instead - // of storing a copy on every doc. Then we can remove the surrounding `Arc` and use the - // `ArcSwap` directly. - syn_loader: Arc<ArcSwap<syntax::Loader>>, + // NOTE: ideally this would live on the handler for inlay hints, see the comment above. + pub inlay_hint_controllers: HashMap<ViewId, TaskController>, } #[derive(Debug, Clone, Default)] pub struct DocumentColorSwatches { pub color_swatches: Vec<InlineAnnotation>, - pub colors: Vec<syntax::Highlight>, + pub colors: Vec<Highlight>, pub color_swatches_padding: Vec<InlineAnnotation>, } @@ -300,7 +289,6 @@ impl fmt::Debug for Document { .field("id", &self.id) .field("text", &self.text) .field("selections", &self.selections) - .field("inlay_hints_oudated", &self.inlay_hints_oudated) .field("text_annotations", &self.inlay_hints) .field("view_data", &self.view_data) .field("path", &self.path) @@ -687,7 +675,6 @@ impl Document { text: Rope, encoding_with_bom_info: Option<(&'static Encoding, bool)>, config: Arc<dyn DynAccess<Config>>, - syn_loader: Arc<ArcSwap<syntax::Loader>>, ) -> Self { let (encoding, has_bom) = encoding_with_bom_info.unwrap_or((encoding::UTF_8, false)); let line_ending = config.load().default_line_ending.into(); @@ -704,7 +691,7 @@ impl Document { text, selections: HashMap::default(), inlay_hints: HashMap::default(), - inlay_hints_oudated: false, + inlay_hint_controllers: HashMap::default(), view_data: Default::default(), indent_style: DEFAULT_INDENT, editor_config: EditorConfig::default(), @@ -730,19 +717,13 @@ impl Document { jump_labels: HashMap::new(), color_swatches: None, color_swatch_controller: TaskController::new(), - syn_loader, - previous_diagnostic_id: None, - pull_diagnostic_controller: TaskController::new(), } } - pub fn default( - config: Arc<dyn DynAccess<Config>>, - syn_loader: Arc<ArcSwap<syntax::Loader>>, - ) -> Self { + pub fn default(config: Arc<dyn DynAccess<Config>>) -> Self { let line_ending: LineEnding = config.load().default_line_ending.into(); let text = Rope::from(line_ending.as_str()); - Self::from(text, None, config, syn_loader) + Self::from(text, None, config) } // TODO: async fn? @@ -751,9 +732,8 @@ impl Document { pub fn open( path: &Path, mut encoding: Option<&'static Encoding>, - detect_language: bool, + config_loader: Option<Arc<ArcSwap<syntax::Loader>>>, config: Arc<dyn DynAccess<Config>>, - syn_loader: Arc<ArcSwap<syntax::Loader>>, ) -> Result<Self, DocumentOpenError> { // If the path is not a regular file (e.g.: /dev/random) it should not be opened. if path.metadata().is_ok_and(|metadata| !metadata.is_file()) { @@ -779,13 +759,12 @@ impl Document { (Rope::from(line_ending.as_str()), encoding, false) }; - let loader = syn_loader.load(); - let mut doc = Self::from(rope, Some((encoding, has_bom)), config, syn_loader); + let mut doc = Self::from(rope, Some((encoding, has_bom)), config); // set the path and try detecting the language doc.set_path(Some(path)); - if detect_language { - doc.detect_language(&loader); + if let Some(loader) = config_loader { + doc.detect_language(loader); } doc.editor_config = editor_config; @@ -796,12 +775,9 @@ impl Document { /// The same as [`format`], but only returns formatting changes if auto-formatting /// is configured. - pub fn auto_format( - &self, - editor: &Editor, - ) -> Option<BoxFuture<'static, Result<Transaction, FormatterError>>> { + pub fn auto_format(&self) -> Option<BoxFuture<'static, Result<Transaction, FormatterError>>> { if self.language_config()?.auto_format { - self.format(editor) + self.format() } else { None } @@ -811,10 +787,7 @@ impl Document { /// to format it nicely. // We can't use anyhow::Result here since the output of the future has to be // clonable to be used as shared future. So use a custom error type. - pub fn format( - &self, - editor: &Editor, - ) -> Option<BoxFuture<'static, Result<Transaction, FormatterError>>> { + pub fn format(&self) -> Option<BoxFuture<'static, Result<Transaction, FormatterError>>> { if let Some((fmt_cmd, fmt_args)) = self .language_config() .and_then(|c| c.formatter.as_ref()) @@ -839,20 +812,8 @@ impl Document { process.current_dir(doc_dir); } - let args = match fmt_args - .iter() - .map(|content| expansion::expand(editor, Token::expand(content))) - .collect::<Result<Vec<_>, _>>() - { - Ok(args) => args, - Err(err) => { - log::error!("Failed to expand formatter arguments: {err}"); - return None; - } - }; - process - .args(args.iter().map(AsRef::as_ref)) + .args(fmt_args) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()); @@ -891,7 +852,7 @@ impl Document { } else if !output.stderr.is_empty() { log::debug!( "Formatter printed to stderr: {}", - String::from_utf8_lossy(&output.stderr) + String::from_utf8_lossy(&output.stderr).to_string() ); } @@ -980,12 +941,11 @@ impl Document { }; let identifier = self.path().map(|_| self.identifier()); - let language_servers: Vec<_> = self.language_servers.values().cloned().collect(); + let language_servers = self.language_servers.clone(); // mark changes up to now as saved let current_rev = self.get_current_revision(); let doc_id = self.id(); - let atomic_save = self.config.load().atomic_save; let encoding_with_bom_info = (self.encoding, self.has_bom); let last_saved_time = self.last_saved_time; @@ -1035,7 +995,7 @@ impl Document { // Assume it is a hardlink to prevent data loss if the metadata cant be read (e.g. on certain Windows configurations) let is_hardlink = helix_stdx::faccess::hardlink_count(&write_path).unwrap_or(2) > 1; - let backup = if path.exists() && atomic_save { + let backup = if path.exists() { let path_ = write_path.clone(); // hacks: we use tempfile to handle the complex task of creating // non clobbered temporary path for us we don't want @@ -1124,7 +1084,7 @@ impl Document { text: text.clone(), }; - for language_server in language_servers { + for (_, language_server) in language_servers { if !language_server.is_initialized() { continue; } @@ -1140,20 +1100,22 @@ impl Document { } /// Detect the programming language based on the file type. - pub fn detect_language(&mut self, loader: &syntax::Loader) { - self.set_language(self.detect_language_config(loader), loader); + pub fn detect_language(&mut self, config_loader: Arc<ArcSwap<syntax::Loader>>) { + let loader = config_loader.load(); + self.set_language( + self.detect_language_config(&loader), + Some(Arc::clone(&config_loader)), + ); } /// Detect the programming language based on the file type. pub fn detect_language_config( &self, - loader: &syntax::Loader, - ) -> Option<Arc<syntax::config::LanguageConfiguration>> { - let language = loader - .language_for_filename(self.path.as_ref()?) - .or_else(|| loader.language_for_shebang(self.text().slice(..)))?; - - Some(loader.language(language).config().clone()) + config_loader: &syntax::Loader, + ) -> Option<Arc<helix_core::syntax::LanguageConfiguration>> { + config_loader + .language_config_for_file_name(self.path.as_ref()?) + .or_else(|| config_loader.language_config_for_shebang(self.text().slice(..))) } /// Detect the indentation used in the file, or otherwise defaults to the language indentation @@ -1178,7 +1140,7 @@ impl Document { } } - pub fn detect_editor_config(&mut self) { + pub(crate) fn detect_editor_config(&mut self) { if self.config.load().editor_config { if let Some(path) = self.path.as_ref() { self.editor_config = EditorConfig::find(path); @@ -1292,36 +1254,35 @@ impl Document { /// if it exists. pub fn set_language( &mut self, - language_config: Option<Arc<syntax::config::LanguageConfiguration>>, - loader: &syntax::Loader, + language_config: Option<Arc<helix_core::syntax::LanguageConfiguration>>, + loader: Option<Arc<ArcSwap<helix_core::syntax::Loader>>>, ) { - self.language = language_config; - self.syntax = self.language.as_ref().and_then(|config| { - Syntax::new(self.text.slice(..), config.language(), loader) - .map_err(|err| { - // `NoRootConfig` means that there was an issue loading the language/syntax - // config for the root language of the document. An error must have already - // been logged by `LanguageData::syntax_config`. - if err != syntax::HighlighterError::NoRootConfig { - log::warn!("Error building syntax for '{}': {err}", self.display_name()); - } - }) - .ok() - }); + if let (Some(language_config), Some(loader)) = (language_config, loader) { + if let Some(highlight_config) = + language_config.highlight_config(&(*loader).load().scopes()) + { + self.syntax = Syntax::new(self.text.slice(..), highlight_config, loader); + } + + self.language = Some(language_config); + } else { + self.syntax = None; + self.language = None; + }; } /// Set the programming language for the file if you know the language but don't have the - /// [`syntax::config::LanguageConfiguration`] for it. + /// [`syntax::LanguageConfiguration`] for it. pub fn set_language_by_language_id( &mut self, language_id: &str, - loader: &syntax::Loader, + config_loader: Arc<ArcSwap<syntax::Loader>>, ) -> anyhow::Result<()> { - let language = loader - .language_for_name(language_id) + let language_config = (*config_loader) + .load() + .language_config_for_language_id(language_id) .ok_or_else(|| anyhow!("invalid language id: {}", language_id))?; - let config = loader.language(language).config().clone(); - self.set_language(Some(config), loader); + self.set_language(Some(language_config), Some(config_loader)); Ok(()) } @@ -1440,14 +1401,14 @@ impl Document { // update tree-sitter syntax tree if let Some(syntax) = &mut self.syntax { - let loader = self.syn_loader.load(); - if let Err(err) = syntax.update( + // TODO: no unwrap + let res = syntax.update( old_doc.slice(..), self.text.slice(..), transaction.changes(), - &loader, - ) { - log::error!("TS parser failed, disabling TS for the current buffer: {err}"); + ); + if res.is_err() { + log::error!("TS parser failed, disabling TS for the current buffer: {res:?}"); self.syntax = None; } } @@ -1498,33 +1459,6 @@ impl Document { ) }); - // Update the inlay hint annotations' positions, helping ensure they are displayed in the proper place - let apply_inlay_hint_changes = |annotations: &mut Vec<InlineAnnotation>| { - changes.update_positions( - annotations - .iter_mut() - .map(|annotation| (&mut annotation.char_idx, Assoc::After)), - ); - }; - - self.inlay_hints_oudated = true; - for text_annotation in self.inlay_hints.values_mut() { - let DocumentInlayHints { - id: _, - type_inlay_hints, - parameter_inlay_hints, - other_inlay_hints, - padding_before_inlay_hints, - padding_after_inlay_hints, - } = text_annotation; - - apply_inlay_hint_changes(padding_before_inlay_hints); - apply_inlay_hint_changes(type_inlay_hints); - apply_inlay_hint_changes(parameter_inlay_hints); - apply_inlay_hint_changes(other_inlay_hints); - apply_inlay_hint_changes(padding_after_inlay_hints); - } - helix_event::dispatch(DocumentDidChange { doc: self, view: view_id, @@ -1660,7 +1594,7 @@ impl Document { let savepoint_idx = self .savepoints .iter() - .position(|savepoint_ref| std::ptr::eq(savepoint_ref.as_ptr(), savepoint)) + .position(|savepoint_ref| savepoint_ref.as_ptr() == savepoint as *const _) .expect("Savepoint must belong to this document"); let savepoint_ref = self.savepoints.remove(savepoint_idx); @@ -1815,12 +1749,6 @@ impl Document { self.version } - pub fn word_completion_enabled(&self) -> bool { - self.language_config() - .and_then(|lang_config| lang_config.word_completion.and_then(|c| c.enable)) - .unwrap_or_else(|| self.config.load().word_completion.enable) - } - pub fn path_completion_enabled(&self) -> bool { self.language_config() .and_then(|lang_config| lang_config.path_completion) @@ -2261,7 +2189,8 @@ impl Document { viewport_width, wrap_indicator: wrap_indicator.into_boxed_str(), wrap_indicator_highlight: theme - .and_then(|theme| theme.find_highlight("ui.virtual.wrap")), + .and_then(|theme| theme.find_scope_index("ui.virtual.wrap")) + .map(Highlight), soft_wrap_at_text_width, } } @@ -2284,15 +2213,17 @@ impl Document { self.inlay_hints.get(&view_id) } + pub fn inlay_hints_mut(&mut self) -> impl Iterator<Item = (ViewId, &mut DocumentInlayHints)> { + self.inlay_hints + .iter_mut() + .map(|(view_id, hints)| (*view_id, hints)) + } + /// Completely removes all the inlay hints saved for the document, dropping them to free memory /// (since it often means inlay hints have been fully deactivated). pub fn reset_all_inlay_hints(&mut self) { self.inlay_hints = Default::default(); } - - pub fn has_language_server_with_feature(&self, feature: LanguageServerFeature) -> bool { - self.language_servers_with_feature(feature).next().is_some() - } } #[derive(Debug, Default)] @@ -2345,7 +2276,6 @@ mod test { text, None, Arc::new(ArcSwap::new(Arc::new(Config::default()))), - Arc::new(ArcSwap::from_pointee(syntax::Loader::default())), ); let view = ViewId::default(); doc.set_selection(view, Selection::single(0, 0)); @@ -2384,7 +2314,6 @@ mod test { text, None, Arc::new(ArcSwap::new(Arc::new(Config::default()))), - Arc::new(ArcSwap::from_pointee(syntax::Loader::default())), ); let view = ViewId::default(); doc.set_selection(view, Selection::single(5, 5)); @@ -2498,12 +2427,9 @@ mod test { #[test] fn test_line_ending() { assert_eq!( - Document::default( - Arc::new(ArcSwap::new(Arc::new(Config::default()))), - Arc::new(ArcSwap::from_pointee(syntax::Loader::default())) - ) - .text() - .to_string(), + Document::default(Arc::new(ArcSwap::new(Arc::new(Config::default())))) + .text() + .to_string(), helix_core::NATIVE_LINE_ENDING.as_str() ); } |