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.rs995
1 files changed, 323 insertions, 672 deletions
diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs
index e52dbe0f..88653948 100644
--- a/helix-view/src/document.rs
+++ b/helix-view/src/document.rs
@@ -1,23 +1,16 @@
-use anyhow::{anyhow, bail, Error};
+use anyhow::{anyhow, bail, Context, Error};
use arc_swap::access::DynAccess;
use arc_swap::ArcSwap;
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::text_annotations::{InlineAnnotation, Overlay};
-use helix_event::TaskController;
+use helix_core::syntax::{Highlight, LanguageServerFeature};
+use helix_core::text_annotations::{InlineAnnotation, TextAnnotations};
use helix_lsp::util::lsp_pos_to_pos;
-use helix_stdx::faccess::{copy_metadata, readonly};
use helix_vcs::{DiffHandle, DiffProviderRegistry};
-use once_cell::sync::OnceCell;
-use thiserror;
use ::parking_lot::Mutex;
use serde::de::{self, Deserialize, Deserializer};
@@ -27,35 +20,29 @@ use std::cell::Cell;
use std::collections::HashMap;
use std::fmt::Display;
use std::future::Future;
-use std::io;
use std::path::{Path, PathBuf};
+use std::rc::Rc;
use std::str::FromStr;
use std::sync::{Arc, Weak};
use std::time::SystemTime;
use helix_core::{
- editor_config::EditorConfig,
encoding,
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,
-};
+use crate::editor::Config;
+use crate::events::{DocumentDidChange, SelectionDidChange};
+use crate::{DocumentId, Editor, Theme, View, ViewId};
/// 8kB of buffer space for encoding and decoding `Rope`s.
const BUF_SIZE: usize = 8192;
const DEFAULT_INDENT: IndentStyle = IndentStyle::Tabs;
-const DEFAULT_TAB_WIDTH: usize = 4;
pub const DEFAULT_LANGUAGE_NAME: &str = "text";
@@ -110,11 +97,11 @@ impl Serialize for Mode {
serializer.collect_str(self)
}
}
+
/// A snapshot of the text of a document that we want to write out to disk
#[derive(Debug, Clone)]
pub struct DocumentSavedEvent {
pub revision: usize,
- pub save_time: SystemTime,
pub doc_id: DocumentId,
pub path: PathBuf,
pub text: Rope,
@@ -130,32 +117,20 @@ pub struct SavePoint {
revert: Mutex<Transaction>,
}
-#[derive(Debug, thiserror::Error)]
-pub enum DocumentOpenError {
- #[error("path must be a regular file, symlink, or directory")]
- IrregularFile,
- #[error(transparent)]
- IoError(#[from] io::Error),
-}
-
pub struct Document {
pub(crate) id: DocumentId,
text: Rope,
selections: HashMap<ViewId, Selection>,
- view_data: HashMap<ViewId, ViewData>,
- pub active_snippet: Option<ActiveSnippet>,
/// Inlay hints annotations for the document, by view.
///
/// 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>>,
encoding: &'static encoding::Encoding,
has_bom: bool,
@@ -163,7 +138,6 @@ pub struct Document {
/// Current indent style.
pub indent_style: IndentStyle,
- editor_config: EditorConfig,
/// The document's default line ending.
pub line_ending: LineEnding,
@@ -203,27 +177,6 @@ pub struct Document {
pub focused_at: std::time::Instant,
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>>,
-}
-
-#[derive(Debug, Clone, Default)]
-pub struct DocumentColorSwatches {
- pub color_swatches: Vec<InlineAnnotation>,
- pub colors: Vec<syntax::Highlight>,
- pub color_swatches_padding: Vec<InlineAnnotation>,
}
/// Inlay hints for a single `(Document, View)` combo.
@@ -247,22 +200,22 @@ pub struct DocumentInlayHints {
pub id: DocumentInlayHintsId,
/// Inlay hints of `TYPE` kind, if any.
- pub type_inlay_hints: Vec<InlineAnnotation>,
+ pub type_inlay_hints: Rc<[InlineAnnotation]>,
/// Inlay hints of `PARAMETER` kind, if any.
- pub parameter_inlay_hints: Vec<InlineAnnotation>,
+ pub parameter_inlay_hints: Rc<[InlineAnnotation]>,
/// Inlay hints that are neither `TYPE` nor `PARAMETER`.
///
/// LSPs are not required to associate a kind to their inlay hints, for example Rust-Analyzer
/// currently never does (February 2023) and the LSP spec may add new kinds in the future that
/// we want to display even if we don't have some special highlighting for them.
- pub other_inlay_hints: Vec<InlineAnnotation>,
+ pub other_inlay_hints: Rc<[InlineAnnotation]>,
/// Inlay hint padding. When creating the final `TextAnnotations`, the `before` padding must be
/// added first, then the regular inlay hints, then the `after` padding.
- pub padding_before_inlay_hints: Vec<InlineAnnotation>,
- pub padding_after_inlay_hints: Vec<InlineAnnotation>,
+ pub padding_before_inlay_hints: Rc<[InlineAnnotation]>,
+ pub padding_after_inlay_hints: Rc<[InlineAnnotation]>,
}
impl DocumentInlayHints {
@@ -270,11 +223,11 @@ impl DocumentInlayHints {
pub fn empty_with_id(id: DocumentInlayHintsId) -> Self {
Self {
id,
- type_inlay_hints: Vec::new(),
- parameter_inlay_hints: Vec::new(),
- other_inlay_hints: Vec::new(),
- padding_before_inlay_hints: Vec::new(),
- padding_after_inlay_hints: Vec::new(),
+ type_inlay_hints: Rc::new([]),
+ parameter_inlay_hints: Rc::new([]),
+ other_inlay_hints: Rc::new([]),
+ padding_before_inlay_hints: Rc::new([]),
+ padding_after_inlay_hints: Rc::new([]),
}
}
}
@@ -302,7 +255,6 @@ impl fmt::Debug for Document {
.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)
.field("encoding", &self.encoding)
.field("restore_cursor", &self.restore_cursor)
@@ -330,14 +282,6 @@ impl fmt::Debug for DocumentInlayHintsId {
}
}
-impl Editor {
- pub(crate) fn clear_doc_relative_paths(&mut self) {
- for doc in self.documents_mut() {
- doc.relative_path.take();
- }
- }
-}
-
enum Encoder {
Utf16Be,
Utf16Le,
@@ -435,7 +379,7 @@ fn apply_bom(encoding: &'static encoding::Encoding, buf: &mut [u8; BUF_SIZE]) ->
pub fn from_reader<R: std::io::Read + ?Sized>(
reader: &mut R,
encoding: Option<&'static Encoding>,
-) -> Result<(Rope, &'static Encoding, bool), io::Error> {
+) -> Result<(Rope, &'static Encoding, bool), Error> {
// These two buffers are 8192 bytes in size each and are used as
// intermediaries during the decoding process. Text read into `buf`
// from `reader` is decoded into `buf_out` as UTF-8. Once either
@@ -578,7 +522,7 @@ fn read_and_detect_encoding<R: std::io::Read + ?Sized>(
reader: &mut R,
encoding: Option<&'static Encoding>,
buf: &mut [u8],
-) -> Result<(&'static Encoding, bool, encoding::Decoder, usize), io::Error> {
+) -> Result<(&'static Encoding, bool, encoding::Decoder, usize), Error> {
let read = reader.read(buf)?;
let is_empty = read == 0;
let (encoding, has_bom) = encoding
@@ -679,7 +623,7 @@ where
*mut_ref = f(mem::take(mut_ref));
}
-use helix_lsp::{lsp, Client, LanguageServerId, LanguageServerName};
+use helix_lsp::{lsp, Client, LanguageServerName};
use url::Url;
impl Document {
@@ -687,7 +631,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();
@@ -696,18 +639,14 @@ impl Document {
Self {
id: DocumentId::default(),
- active_snippet: None,
path: None,
- relative_path: OnceCell::new(),
encoding,
has_bom,
text,
selections: HashMap::default(),
inlay_hints: HashMap::default(),
inlay_hints_oudated: false,
- view_data: Default::default(),
indent_style: DEFAULT_INDENT,
- editor_config: EditorConfig::default(),
line_ending,
restore_cursor: false,
syntax: None,
@@ -727,22 +666,13 @@ impl Document {
version_control_head: None,
focused_at: std::time::Instant::now(),
readonly: false,
- 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?
@@ -750,45 +680,29 @@ impl Document {
/// overwritten with the `encoding` parameter.
pub fn open(
path: &Path,
- mut encoding: Option<&'static Encoding>,
- detect_language: bool,
+ encoding: Option<&'static Encoding>,
+ config_loader: Option<Arc<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()) {
- return Err(DocumentOpenError::IrregularFile);
- }
-
- let editor_config = if config.load().editor_config {
- EditorConfig::find(path)
- } else {
- EditorConfig::default()
- };
- encoding = encoding.or(editor_config.encoding);
-
+ ) -> Result<Self, Error> {
// Open the file if it exists, otherwise assume it is a new file (and thus empty).
let (rope, encoding, has_bom) = if path.exists() {
- let mut file = std::fs::File::open(path)?;
+ let mut file =
+ std::fs::File::open(path).context(format!("unable to open {:?}", path))?;
from_reader(&mut file, encoding)?
} else {
- let line_ending = editor_config
- .line_ending
- .unwrap_or_else(|| config.load().default_line_ending.into());
+ let line_ending: LineEnding = config.load().default_line_ending.into();
let encoding = encoding.unwrap_or(encoding::UTF_8);
(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;
doc.detect_indent_and_line_ending();
Ok(doc)
@@ -796,12 +710,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 +722,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())
@@ -825,34 +733,11 @@ impl Document {
))
})
{
- log::debug!(
- "formatting '{}' with command '{}', args {fmt_args:?}",
- self.display_name(),
- fmt_cmd.display(),
- );
use std::process::Stdio;
let text = self.text().clone();
-
let mut process = tokio::process::Command::new(&fmt_cmd);
-
- if let Some(doc_dir) = self.path.as_ref().and_then(|path| path.parent()) {
- 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());
@@ -864,21 +749,17 @@ impl Document {
command: fmt_cmd.to_string_lossy().into(),
error: e.kind(),
})?;
+ {
+ let mut stdin = process.stdin.take().ok_or(FormatterError::BrokenStdin)?;
+ to_writer(&mut stdin, (encoding::UTF_8, false), &text)
+ .await
+ .map_err(|_| FormatterError::BrokenStdin)?;
+ }
- let mut stdin = process.stdin.take().ok_or(FormatterError::BrokenStdin)?;
- let input_text = text.clone();
- let input_task = tokio::spawn(async move {
- to_writer(&mut stdin, (encoding::UTF_8, false), &input_text).await
- // Note that `stdin` is dropped here, causing the pipe to close. This can
- // avoid a deadlock with `wait_with_output` below if the process is waiting on
- // stdin to close before exiting.
- });
- let (input_result, output_result) = tokio::join! {
- input_task,
- process.wait_with_output(),
- };
- let _ = input_result.map_err(|_| FormatterError::BrokenStdin)?;
- let output = output_result.map_err(|_| FormatterError::WaitForOutputFailed)?;
+ let output = process
+ .wait_with_output()
+ .await
+ .map_err(|_| FormatterError::WaitForOutputFailed)?;
if !output.status.success() {
if !output.stderr.is_empty() {
@@ -891,7 +772,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()
);
}
@@ -920,13 +801,10 @@ impl Document {
)?;
let fut = async move {
- let edits = request
- .await
- .unwrap_or_else(|e| {
- log::warn!("LSP formatting failed: {}", e);
- Default::default()
- })
- .unwrap_or_default();
+ let edits = request.await.unwrap_or_else(|e| {
+ log::warn!("LSP formatting failed: {}", e);
+ Default::default()
+ });
Ok(helix_lsp::util::generate_transaction_from_edits(
&text,
edits,
@@ -980,19 +858,18 @@ 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;
// We encode the file according to the `Document`'s encoding.
let future = async move {
- use tokio::fs;
+ use tokio::{fs, fs::File};
if let Some(parent) = path.parent() {
// TODO: display a prompt asking the user if the directories should be created
if !parent.exists() {
@@ -1014,122 +891,27 @@ impl Document {
}
}
}
- let write_path = tokio::fs::read_link(&path)
- .await
- .ok()
- .and_then(|p| {
- if p.is_relative() {
- path.parent().map(|parent| parent.join(p))
- } else {
- Some(p)
- }
- })
- .unwrap_or_else(|| path.clone());
-
- if readonly(&write_path) {
- bail!(std::io::Error::new(
- std::io::ErrorKind::PermissionDenied,
- "Path is read only"
- ));
- }
-
- // 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 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
- // the whole automatically delete path on drop thing
- // since the path doesn't exist yet, we just want
- // the path
- tokio::task::spawn_blocking(move || -> Option<PathBuf> {
- let mut builder = tempfile::Builder::new();
- builder.prefix(path_.file_name()?).suffix(".bck");
-
- let backup_path = if is_hardlink {
- builder
- .make_in(path_.parent()?, |backup| std::fs::copy(&path_, backup))
- .ok()?
- .into_temp_path()
- } else {
- builder
- .make_in(path_.parent()?, |backup| std::fs::rename(&path_, backup))
- .ok()?
- .into_temp_path()
- };
-
- backup_path.keep().ok()
- })
- .await
- .ok()
- .flatten()
- } else {
- None
- };
-
- let write_result: anyhow::Result<_> = async {
- let mut dst = tokio::fs::File::create(&write_path).await?;
- to_writer(&mut dst, encoding_with_bom_info, &text).await?;
- dst.sync_all().await?;
- Ok(())
- }
- .await;
-
- let save_time = match fs::metadata(&write_path).await {
- Ok(metadata) => metadata.modified().map_or(SystemTime::now(), |mtime| mtime),
- Err(_) => SystemTime::now(),
- };
-
- if let Some(backup) = backup {
- if is_hardlink {
- let mut delete = true;
- if write_result.is_err() {
- // Restore backup
- let _ = tokio::fs::copy(&backup, &write_path).await.map_err(|e| {
- delete = false;
- log::error!("Failed to restore backup on write failure: {e}")
- });
- }
-
- if delete {
- // Delete backup
- let _ = tokio::fs::remove_file(backup)
- .await
- .map_err(|e| log::error!("Failed to remove backup file on write: {e}"));
- }
- } else if write_result.is_err() {
- // restore backup
- let _ = tokio::fs::rename(&backup, &write_path)
- .await
- .map_err(|e| log::error!("Failed to restore backup on write failure: {e}"));
- } else {
- // copy metadata and delete backup
- let _ = tokio::task::spawn_blocking(move || {
- let _ = copy_metadata(&backup, &write_path)
- .map_err(|e| log::error!("Failed to copy metadata on write: {e}"));
- let _ = std::fs::remove_file(backup)
- .map_err(|e| log::error!("Failed to remove backup file on write: {e}"));
- })
- .await;
- }
- }
- write_result?;
+ let mut file = File::create(&path).await?;
+ to_writer(&mut file, encoding_with_bom_info, &text).await?;
let event = DocumentSavedEvent {
revision: current_rev,
- save_time,
doc_id,
path,
text: text.clone(),
};
- for language_server in language_servers {
+ for (_, language_server) in language_servers {
if !language_server.is_initialized() {
- continue;
+ return Ok(event);
}
- if let Some(id) = identifier.clone() {
- language_server.text_document_did_save(id, &text);
+ if let Some(identifier) = &identifier {
+ if let Some(notification) =
+ language_server.text_document_did_save(identifier.clone(), &text)
+ {
+ notification.await?;
+ }
}
}
@@ -1140,77 +922,66 @@ 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<syntax::Loader>) {
+ self.set_language(
+ self.detect_language_config(&config_loader),
+ Some(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
/// configured in `languages.toml`, with a fallback to tabs if it isn't specified. Line ending
/// is likewise auto-detected, and will remain unchanged if no line endings were detected.
pub fn detect_indent_and_line_ending(&mut self) {
- self.indent_style = if let Some(indent_style) = self.editor_config.indent_style {
- indent_style
- } else {
- auto_detect_indent_style(&self.text).unwrap_or_else(|| {
- self.language_config()
- .and_then(|config| config.indent.as_ref())
- .map_or(DEFAULT_INDENT, |config| IndentStyle::from_str(&config.unit))
- })
- };
- if let Some(line_ending) = self
- .editor_config
- .line_ending
- .or_else(|| auto_detect_line_ending(&self.text))
- {
+ self.indent_style = auto_detect_indent_style(&self.text).unwrap_or_else(|| {
+ self.language_config()
+ .and_then(|config| config.indent.as_ref())
+ .map_or(DEFAULT_INDENT, |config| IndentStyle::from_str(&config.unit))
+ });
+ if let Some(line_ending) = auto_detect_line_ending(&self.text) {
self.line_ending = line_ending;
}
}
- pub 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);
- }
- }
- }
-
- pub fn pickup_last_saved_time(&mut self) {
- self.last_saved_time = match self.path() {
- Some(path) => match path.metadata() {
- Ok(metadata) => match metadata.modified() {
- Ok(mtime) => mtime,
- Err(err) => {
- log::debug!("Could not fetch file system's mtime, falling back to current system time: {}", err);
- SystemTime::now()
- }
- },
- Err(err) => {
- log::debug!("Could not fetch file system's mtime, falling back to current system time: {}", err);
- SystemTime::now()
- }
+ #[cfg(unix)]
+ // Detect if the file is readonly and change the readonly field if necessary (unix only)
+ pub fn detect_readonly(&mut self) {
+ use rustix::fs::{access, Access};
+ // Allows setting the flag for files the user cannot modify, like root files
+ self.readonly = match &self.path {
+ None => false,
+ Some(p) => match access(p, Access::WRITE_OK) {
+ Ok(_) => false,
+ Err(err) if err.kind() == std::io::ErrorKind::NotFound => false,
+ Err(_) => true,
},
- None => SystemTime::now(),
};
}
- // Detect if the file is readonly and change the readonly field if necessary (unix only)
+ #[cfg(not(unix))]
+ // Detect if the file is readonly and change the readonly field if necessary (non-unix os)
pub fn detect_readonly(&mut self) {
- // Allows setting the flag for files the user cannot modify, like root files
+ // TODO Use the Windows' function `CreateFileW` to check if a file is readonly
+ // Discussion: https://github.com/helix-editor/helix/pull/7740#issuecomment-1656806459
+ // Vim implementation: https://github.com/vim/vim/blob/4c0089d696b8d1d5dc40568f25ea5738fa5bbffb/src/os_win32.c#L7665
+ // Windows binding: https://microsoft.github.io/windows-docs-rs/doc/windows/Win32/Storage/FileSystem/fn.CreateFileW.html
self.readonly = match &self.path {
None => false,
- Some(p) => readonly(p),
+ Some(p) => match std::fs::metadata(p) {
+ Err(err) if err.kind() == std::io::ErrorKind::NotFound => false,
+ Err(_) => false,
+ Ok(metadata) => metadata.permissions().readonly(),
+ },
};
}
@@ -1221,13 +992,11 @@ impl Document {
provider_registry: &DiffProviderRegistry,
) -> Result<(), Error> {
let encoding = self.encoding;
- let path = match self.path() {
- None => return Ok(()),
- Some(path) => match path.exists() {
- true => path.to_owned(),
- false => bail!("can't find file to reload from {:?}", self.display_name()),
- },
- };
+ let path = self
+ .path()
+ .filter(|path| path.exists())
+ .ok_or_else(|| anyhow!("can't find file to reload from {:?}", self.display_name()))?
+ .to_owned();
// Once we have a valid path we check if its readonly status has changed
self.detect_readonly();
@@ -1242,7 +1011,9 @@ impl Document {
self.apply(&transaction, view.id);
self.append_changes_to_history(view);
self.reset_modified();
- self.pickup_last_saved_time();
+
+ self.last_saved_time = SystemTime::now();
+
self.detect_indent_and_line_ending();
match provider_registry.get_diff_base(&path) {
@@ -1270,58 +1041,46 @@ impl Document {
self.encoding
}
- /// sets the document path without sending events to various
- /// observers (like LSP), in most cases `Editor::set_doc_path`
- /// should be used instead
pub fn set_path(&mut self, path: Option<&Path>) {
let path = path.map(helix_stdx::path::canonicalize);
- // `take` to remove any prior relative path that may have existed.
- // This will get set in `relative_path()`.
- self.relative_path.take();
-
// if parent doesn't exist we still want to open the document
// and error out when document is saved
self.path = path;
self.detect_readonly();
- self.pickup_last_saved_time();
}
/// Set the programming language for the file and load associated data (e.g. highlighting)
/// 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<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.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<syntax::Loader>,
) -> anyhow::Result<()> {
- let language = loader
- .language_for_name(language_id)
+ let language_config = config_loader
+ .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(())
}
@@ -1354,14 +1113,12 @@ impl Document {
self.set_selection(view_id, Selection::single(origin.anchor, origin.head));
}
- /// Initializes a new selection and view_data for the given view
- /// if it does not already have them.
+ /// Initializes a new selection for the given view if it does not
+ /// already have one.
pub fn ensure_view_init(&mut self, view_id: ViewId) {
- if !self.selections.contains_key(&view_id) {
+ if self.selections.get(&view_id).is_none() {
self.reset_selection(view_id);
}
-
- self.view_data_mut(view_id);
}
/// Mark document as recent used for MRU sorting
@@ -1373,7 +1130,6 @@ impl Document {
pub fn remove_view(&mut self, view_id: ViewId) {
self.selections.remove(&view_id);
self.inlay_hints.remove(&view_id);
- self.jump_labels.remove(&view_id);
}
/// Apply a [`Transaction`] to the [`Document`] to change its text.
@@ -1386,12 +1142,28 @@ impl Document {
use helix_core::Assoc;
let old_doc = self.text().clone();
- let changes = transaction.changes();
- if !changes.apply(&mut self.text) {
- return false;
- }
- if changes.is_empty() {
+ let success = transaction.changes().apply(&mut self.text);
+
+ if success {
+ if emit_lsp_notification {
+ helix_event::dispatch(DocumentDidChange {
+ doc: self,
+ view: view_id,
+ old_text: &old_doc,
+ });
+ }
+
+ for selection in self.selections.values_mut() {
+ *selection = selection
+ .clone()
+ // Map through changes
+ .map(transaction.changes())
+ // Ensure all selections across all views still adhere to invariants.
+ .ensure_invariants(self.text.slice(..));
+ }
+
+ // if specified, the current selection should instead be replaced by transaction.selection
if let Some(selection) = transaction.selection() {
self.selections.insert(
view_id,
@@ -1402,150 +1174,134 @@ impl Document {
view: view_id,
});
}
- return true;
- }
-
- self.modified_since_accessed = true;
- self.version += 1;
- for selection in self.selections.values_mut() {
- *selection = selection
- .clone()
- // Map through changes
- .map(transaction.changes())
- // Ensure all selections across all views still adhere to invariants.
- .ensure_invariants(self.text.slice(..));
+ self.modified_since_accessed = true;
}
- for view_data in self.view_data.values_mut() {
- view_data.view_position.anchor = transaction
- .changes()
- .map_pos(view_data.view_position.anchor, Assoc::Before);
- }
+ if !transaction.changes().is_empty() {
+ self.version += 1;
+ // start computing the diff in parallel
+ if let Some(diff_handle) = &self.diff_handle {
+ diff_handle.update_document(self.text.clone(), false);
+ }
- // generate revert to savepoint
- if !self.savepoints.is_empty() {
- let revert = transaction.invert(&old_doc);
- self.savepoints
- .retain_mut(|save_point| match save_point.upgrade() {
- Some(savepoint) => {
- let mut revert_to_savepoint = savepoint.revert.lock();
- *revert_to_savepoint =
- revert.clone().compose(mem::take(&mut revert_to_savepoint));
- true
- }
- None => false,
- })
- }
+ // generate revert to savepoint
+ if !self.savepoints.is_empty() {
+ let revert = transaction.invert(&old_doc);
+ self.savepoints
+ .retain_mut(|save_point| match save_point.upgrade() {
+ Some(savepoint) => {
+ let mut revert_to_savepoint = savepoint.revert.lock();
+ *revert_to_savepoint =
+ revert.clone().compose(mem::take(&mut revert_to_savepoint));
+ true
+ }
+ None => false,
+ })
+ }
- // update tree-sitter syntax tree
- if let Some(syntax) = &mut self.syntax {
- let loader = self.syn_loader.load();
- if let Err(err) = syntax.update(
- old_doc.slice(..),
- self.text.slice(..),
- transaction.changes(),
- &loader,
- ) {
- log::error!("TS parser failed, disabling TS for the current buffer: {err}");
- self.syntax = None;
+ // update tree-sitter syntax tree
+ if let Some(syntax) = &mut self.syntax {
+ // TODO: no unwrap
+ let res = syntax.update(
+ old_doc.slice(..),
+ self.text.slice(..),
+ transaction.changes(),
+ );
+ if res.is_err() {
+ log::error!("TS parser failed, disabling TS for the current buffer: {res:?}");
+ self.syntax = None;
+ }
}
- }
- // TODO: all of that should likely just be hooks
- // start computing the diff in parallel
- if let Some(diff_handle) = &self.diff_handle {
- diff_handle.update_document(self.text.clone(), false);
- }
+ let changes = transaction.changes();
- // map diagnostics over changes too
- changes.update_positions(self.diagnostics.iter_mut().map(|diagnostic| {
- let assoc = if diagnostic.starts_at_word {
- Assoc::BeforeWord
- } else {
- Assoc::After
- };
- (&mut diagnostic.range.start, assoc)
- }));
- changes.update_positions(self.diagnostics.iter_mut().filter_map(|diagnostic| {
- if diagnostic.zero_width {
- // for zero width diagnostics treat the diagnostic as a point
- // rather than a range
- return None;
- }
- let assoc = if diagnostic.ends_at_word {
- Assoc::AfterWord
- } else {
- Assoc::Before
- };
- Some((&mut diagnostic.range.end, assoc))
- }));
- self.diagnostics.retain_mut(|diagnostic| {
- if diagnostic.zero_width {
- diagnostic.range.end = diagnostic.range.start
- } else if diagnostic.range.start >= diagnostic.range.end {
- return false;
- }
- diagnostic.line = self.text.char_to_line(diagnostic.range.start);
- true
- });
+ // map diagnostics over changes too
+ changes.update_positions(self.diagnostics.iter_mut().map(|diagnostic| {
+ let assoc = if diagnostic.starts_at_word {
+ Assoc::BeforeWord
+ } else {
+ Assoc::After
+ };
+ (&mut diagnostic.range.start, assoc)
+ }));
+ changes.update_positions(self.diagnostics.iter_mut().filter_map(|diagnostic| {
+ if diagnostic.zero_width {
+ // for zero width diagnostics treat the diagnostic as a point
+ // rather than a range
+ return None;
+ }
+ let assoc = if diagnostic.ends_at_word {
+ Assoc::AfterWord
+ } else {
+ Assoc::Before
+ };
+ Some((&mut diagnostic.range.end, assoc))
+ }));
+ self.diagnostics.retain_mut(|diagnostic| {
+ if diagnostic.zero_width {
+ diagnostic.range.end = diagnostic.range.start
+ } else if diagnostic.range.start >= diagnostic.range.end {
+ return false;
+ }
+ diagnostic.line = self.text.char_to_line(diagnostic.range.start);
+ true
+ });
- self.diagnostics.sort_by_key(|diagnostic| {
- (
- diagnostic.range,
- diagnostic.severity,
- diagnostic.provider.clone(),
- )
- });
+ self.diagnostics.sort_unstable_by_key(|diagnostic| {
+ (
+ diagnostic.range,
+ diagnostic.severity,
+ diagnostic.language_server_id,
+ )
+ });
- // 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)),
- );
- };
+ // Update the inlay hint annotations' positions, helping ensure they are displayed in the proper place
+ let apply_inlay_hint_changes = |annotations: &mut Rc<[InlineAnnotation]>| {
+ if let Some(data) = Rc::get_mut(annotations) {
+ changes.update_positions(
+ data.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);
- }
+ 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,
- old_text: &old_doc,
- changes,
- ghost_transaction: !emit_lsp_notification,
- });
+ if emit_lsp_notification {
+ // TODO: move to hook
+ // emit lsp notification
+ for language_server in self.language_servers() {
+ let notify = language_server.text_document_did_change(
+ self.versioned_identifier(),
+ &old_doc,
+ self.text(),
+ changes,
+ );
- // if specified, the current selection should instead be replaced by transaction.selection
- if let Some(selection) = transaction.selection() {
- self.selections.insert(
- view_id,
- selection.clone().ensure_invariants(self.text.slice(..)),
- );
- helix_event::dispatch(SelectionDidChange {
- doc: self,
- view: view_id,
- });
+ if let Some(notify) = notify {
+ tokio::spawn(notify);
+ }
+ }
+ }
}
-
- true
+ success
}
fn apply_inner(
@@ -1586,11 +1342,6 @@ impl Document {
}
fn undo_redo_impl(&mut self, view: &mut View, undo: bool) -> bool {
- if undo {
- self.append_changes_to_history(view);
- } else if !self.changes.is_empty() {
- return false;
- }
let mut history = self.history.take();
let txn = if undo { history.undo() } else { history.redo() };
let success = if let Some(txn) = txn {
@@ -1660,7 +1411,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);
@@ -1671,11 +1422,6 @@ impl Document {
}
fn earlier_later_impl(&mut self, view: &mut View, uk: UndoKind, earlier: bool) -> bool {
- if earlier {
- self.append_changes_to_history(view);
- } else if !self.changes.is_empty() {
- return false;
- }
let txns = if earlier {
self.history.get_mut().earlier(uk)
} else {
@@ -1757,7 +1503,7 @@ impl Document {
}
/// Set the document's latest saved revision to the given one.
- pub fn set_last_saved_revision(&mut self, rev: usize, save_time: SystemTime) {
+ pub fn set_last_saved_revision(&mut self, rev: usize) {
log::debug!(
"doc {} revision updated {} -> {}",
self.id,
@@ -1765,7 +1511,7 @@ impl Document {
rev
);
self.last_saved_revision = rev;
- self.last_saved_time = save_time;
+ self.last_saved_time = SystemTime::now();
}
/// Get the document's latest saved revision.
@@ -1815,18 +1561,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)
- .unwrap_or_else(|| self.config.load().path_completion)
- }
-
/// maintains the order as configured in the language_servers TOML array
pub fn language_servers(&self) -> impl Iterator<Item = &helix_lsp::Client> {
self.language_config().into_iter().flat_map(move |config| {
@@ -1864,7 +1598,7 @@ impl Document {
})
}
- pub fn supports_language_server(&self, id: LanguageServerId) -> bool {
+ pub fn supports_language_server(&self, id: usize) -> bool {
self.language_servers().any(|l| l.id() == id)
}
@@ -1904,14 +1638,9 @@ impl Document {
/// The width that the tab character is rendered at
pub fn tab_width(&self) -> usize {
- self.editor_config
- .tab_width
- .map(|n| n.get() as usize)
- .unwrap_or_else(|| {
- self.language_config()
- .and_then(|config| config.indent.as_ref())
- .map_or(DEFAULT_TAB_WIDTH, |config| config.tab_width)
- })
+ self.language_config()
+ .and_then(|config| config.indent.as_ref())
+ .map_or(4, |config| config.tab_width) // fallback to 4 columns
}
// The width (in spaces) of a level of indentation.
@@ -1919,20 +1648,6 @@ impl Document {
self.indent_style.indent_width(self.tab_width())
}
- /// Whether the document should have a trailing line ending appended on save.
- pub fn insert_final_newline(&self) -> bool {
- self.editor_config
- .insert_final_newline
- .unwrap_or_else(|| self.config.load().insert_final_newline)
- }
-
- /// Whether the document should trim whitespace preceding line endings on save.
- pub fn trim_trailing_whitespace(&self) -> bool {
- self.editor_config
- .trim_trailing_whitespace
- .unwrap_or_else(|| self.config.load().trim_trailing_whitespace)
- }
-
pub fn changes(&self) -> &ChangeSet {
&self.changes
}
@@ -1948,10 +1663,6 @@ impl Document {
Url::from_file_path(self.path()?).ok()
}
- pub fn uri(&self) -> Option<helix_core::Uri> {
- Some(self.path()?.clone().into())
- }
-
#[inline]
pub fn text(&self) -> &Rope {
&self.text
@@ -1967,41 +1678,16 @@ impl Document {
&self.selections
}
- fn view_data(&self, view_id: ViewId) -> &ViewData {
- self.view_data
- .get(&view_id)
- .expect("This should only be called after ensure_view_init")
- }
-
- fn view_data_mut(&mut self, view_id: ViewId) -> &mut ViewData {
- self.view_data.entry(view_id).or_default()
- }
-
- pub(crate) fn get_view_offset(&self, view_id: ViewId) -> Option<ViewPosition> {
- Some(self.view_data.get(&view_id)?.view_position)
- }
-
- pub fn view_offset(&self, view_id: ViewId) -> ViewPosition {
- self.view_data(view_id).view_position
- }
-
- pub fn set_view_offset(&mut self, view_id: ViewId, new_offset: ViewPosition) {
- self.view_data_mut(view_id).view_position = new_offset;
- }
-
- pub fn relative_path(&self) -> Option<&Path> {
- self.relative_path
- .get_or_init(|| {
- self.path
- .as_ref()
- .map(|path| helix_stdx::path::get_relative_path(path).to_path_buf())
- })
+ pub fn relative_path(&self) -> Option<PathBuf> {
+ self.path
.as_deref()
+ .map(helix_stdx::path::get_relative_path)
}
- pub fn display_name(&self) -> Cow<'_, str> {
+ pub fn display_name(&self) -> Cow<'static, str> {
self.relative_path()
- .map_or_else(|| SCRATCH_BUFFER_NAME.into(), |path| path.to_string_lossy())
+ .map(|path| path.to_string_lossy().to_string().into())
+ .unwrap_or_else(|| SCRATCH_BUFFER_NAME.into())
}
// transact(Fn) ?
@@ -2035,7 +1721,7 @@ impl Document {
text: &Rope,
language_config: Option<&LanguageConfiguration>,
diagnostic: &helix_lsp::lsp::Diagnostic,
- provider: DiagnosticProvider,
+ language_server_id: usize,
offset_encoding: helix_lsp::OffsetEncoding,
) -> Option<Diagnostic> {
use helix_core::diagnostic::{Range, Severity::*};
@@ -2056,15 +1742,12 @@ impl Document {
return None;
};
- let severity = diagnostic.severity.and_then(|severity| match severity {
- lsp::DiagnosticSeverity::ERROR => Some(Error),
- lsp::DiagnosticSeverity::WARNING => Some(Warning),
- lsp::DiagnosticSeverity::INFORMATION => Some(Info),
- lsp::DiagnosticSeverity::HINT => Some(Hint),
- severity => {
- log::error!("unrecognized diagnostic severity: {:?}", severity);
- None
- }
+ let severity = diagnostic.severity.map(|severity| match severity {
+ lsp::DiagnosticSeverity::ERROR => Error,
+ lsp::DiagnosticSeverity::WARNING => Warning,
+ lsp::DiagnosticSeverity::INFORMATION => Info,
+ lsp::DiagnosticSeverity::HINT => Hint,
+ severity => unreachable!("unrecognized diagnostic severity: {:?}", severity),
});
if let Some(lang_conf) = language_config {
@@ -2100,8 +1783,8 @@ impl Document {
};
let ends_at_word =
- start != end && end != 0 && text.get_char(end - 1).is_some_and(char_is_word);
- let starts_at_word = start != end && text.get_char(start).is_some_and(char_is_word);
+ start != end && end != 0 && text.get_char(end - 1).map_or(false, char_is_word);
+ let starts_at_word = start != end && text.get_char(start).map_or(false, char_is_word);
Some(Diagnostic {
range: Range { start, end },
@@ -2115,7 +1798,7 @@ impl Document {
tags,
source: diagnostic.source.clone(),
data: diagnostic.data.clone(),
- provider,
+ language_server_id,
})
}
@@ -2128,18 +1811,13 @@ impl Document {
&mut self,
diagnostics: impl IntoIterator<Item = Diagnostic>,
unchanged_sources: &[String],
- provider: Option<&DiagnosticProvider>,
+ language_server_id: Option<usize>,
) {
if unchanged_sources.is_empty() {
- if let Some(provider) = provider {
- self.diagnostics
- .retain(|diagnostic| &diagnostic.provider != provider);
- } else {
- self.diagnostics.clear();
- }
+ self.clear_diagnostics(language_server_id);
} else {
self.diagnostics.retain(|d| {
- if provider.is_some_and(|provider| provider != &d.provider) {
+ if language_server_id.map_or(false, |id| id != d.language_server_id) {
return true;
}
@@ -2151,19 +1829,22 @@ impl Document {
});
}
self.diagnostics.extend(diagnostics);
- self.diagnostics.sort_by_key(|diagnostic| {
+ self.diagnostics.sort_unstable_by_key(|diagnostic| {
(
diagnostic.range,
diagnostic.severity,
- diagnostic.provider.clone(),
+ diagnostic.language_server_id,
)
});
}
/// clears diagnostics for a given language server id if set, otherwise all diagnostics are cleared
- pub fn clear_diagnostics_for_language_server(&mut self, id: LanguageServerId) {
- self.diagnostics
- .retain(|d| d.provider.language_server_id() != Some(id));
+ pub fn clear_diagnostics(&mut self, language_server_id: Option<usize>) {
+ if let Some(id) = language_server_id {
+ self.diagnostics.retain(|d| d.language_server_id != id);
+ } else {
+ self.diagnostics.clear();
+ }
}
/// Get the document's auto pairs. If the document has a recognized
@@ -2188,28 +1869,13 @@ impl Document {
}
}
- pub fn snippet_ctx(&self) -> SnippetRenderCtx {
- SnippetRenderCtx {
- // TODO snippet variable resolution
- resolve_var: Box::new(|_| None),
- tab_width: self.tab_width(),
- indent_style: self.indent_style,
- line_ending: self.line_ending.as_str(),
- }
- }
-
- pub fn text_width(&self) -> usize {
- self.editor_config
- .max_line_length
- .map(|n| n.get() as usize)
- .or_else(|| self.language_config().and_then(|config| config.text_width))
- .unwrap_or_else(|| self.config.load().text_width)
- }
-
pub fn text_format(&self, mut viewport_width: u16, theme: Option<&Theme>) -> TextFormat {
let config = self.config.load();
- let text_width = self.text_width();
- let mut soft_wrap_at_text_width = self
+ let text_width = self
+ .language_config()
+ .and_then(|config| config.text_width)
+ .unwrap_or(config.text_width);
+ let soft_wrap_at_text_width = self
.language_config()
.and_then(|config| {
config
@@ -2220,13 +1886,12 @@ impl Document {
.or(config.soft_wrap.wrap_at_text_width)
.unwrap_or(false);
if soft_wrap_at_text_width {
- // if the viewport is smaller than the specified
- // width then this setting has no effcet
- if text_width >= viewport_width as usize {
- soft_wrap_at_text_width = false;
- } else {
- viewport_width = text_width as u16;
- }
+ // We increase max_line_len by 1 because softwrap considers the newline character
+ // as part of the line length while the "typical" expectation is that this is not the case.
+ // In particular other commands like :reflow do not count the line terminator.
+ // This is technically inconsistent for the last line as that line never has a line terminator
+ // but having the last visual line exceed the width by 1 seems like a rare edge case.
+ viewport_width = viewport_width.min(text_width as u16 + 1)
}
let config = self.config.load();
let editor_soft_wrap = &config.soft_wrap;
@@ -2261,24 +1926,22 @@ impl Document {
viewport_width,
wrap_indicator: wrap_indicator.into_boxed_str(),
wrap_indicator_highlight: theme
- .and_then(|theme| theme.find_highlight("ui.virtual.wrap")),
- soft_wrap_at_text_width,
+ .and_then(|theme| theme.find_scope_index("ui.virtual.wrap"))
+ .map(Highlight),
}
}
+ /// Get the text annotations that apply to the whole document, those that do not apply to any
+ /// specific view.
+ pub fn text_annotations(&self, _theme: Option<&Theme>) -> TextAnnotations {
+ TextAnnotations::default()
+ }
+
/// Set the inlay hints for this document and `view_id`.
pub fn set_inlay_hints(&mut self, view_id: ViewId, inlay_hints: DocumentInlayHints) {
self.inlay_hints.insert(view_id, inlay_hints);
}
- pub fn set_jump_labels(&mut self, view_id: ViewId, labels: Vec<Overlay>) {
- self.jump_labels.insert(view_id, labels);
- }
-
- pub fn remove_jump_labels(&mut self, view_id: ViewId) {
- self.jump_labels.remove(&view_id);
- }
-
/// Get the inlay hints for this document and `view_id`.
pub fn inlay_hints(&self, view_id: ViewId) -> Option<&DocumentInlayHints> {
self.inlay_hints.get(&view_id)
@@ -2289,15 +1952,6 @@ impl Document {
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)]
-pub struct ViewData {
- view_position: ViewPosition,
}
#[derive(Clone, Debug)]
@@ -2309,6 +1963,7 @@ pub enum FormatterError {
BrokenStdin,
WaitForOutputFailed,
InvalidUtf8Output,
+ DiskReloadError(String),
NonZeroExitStatus(Option<String>),
}
@@ -2323,6 +1978,7 @@ impl Display for FormatterError {
Self::BrokenStdin => write!(f, "Could not write to formatter stdin"),
Self::WaitForOutputFailed => write!(f, "Waiting for formatter output failed"),
Self::InvalidUtf8Output => write!(f, "Invalid UTF-8 formatter output"),
+ Self::DiskReloadError(error) => write!(f, "Error reloading file from disk: {}", error),
Self::NonZeroExitStatus(Some(output)) => write!(f, "Formatter error: {}", output),
Self::NonZeroExitStatus(None) => {
write!(f, "Formatter exited with non zero exit status")
@@ -2345,7 +2001,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 +2039,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 +2152,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()
);
}