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.rs2062
1 files changed, 355 insertions, 1707 deletions
diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs
index e52dbe0f..671ceb75 100644
--- a/helix-view/src/document.rs
+++ b/helix-view/src/document.rs
@@ -1,63 +1,32 @@
-use anyhow::{anyhow, bail, Error};
-use arc_swap::access::DynAccess;
-use arc_swap::ArcSwap;
-use futures_util::future::BoxFuture;
-use futures_util::FutureExt;
+use anyhow::{anyhow, bail, Context, Error};
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_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};
use serde::Serialize;
-use std::borrow::Cow;
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::str::FromStr;
-use std::sync::{Arc, Weak};
-use std::time::SystemTime;
+use std::sync::Arc;
use helix_core::{
- editor_config::EditorConfig,
encoding,
- history::{History, State, UndoKind},
+ history::{History, UndoKind},
indent::{auto_detect_indent_style, IndentStyle},
line_ending::auto_detect_line_ending,
- syntax::{self, config::LanguageConfiguration},
- ChangeSet, Diagnostic, LineEnding, Range, Rope, RopeBuilder, Selection, Syntax, Transaction,
+ syntax::{self, LanguageConfiguration},
+ ChangeSet, Diagnostic, LineEnding, Rope, RopeBuilder, Selection, State, Syntax, Transaction,
+ DEFAULT_LINE_ENDING,
};
+use helix_lsp::util::LspFormatting;
-use crate::{
- editor::Config,
- events::{DocumentDidChange, SelectionDidChange},
- expansion,
- view::ViewPosition,
- DocumentId, Editor, Theme, View, ViewId,
-};
+use crate::{DocumentId, Editor, 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";
+const DEFAULT_INDENT: IndentStyle = IndentStyle::Spaces(4);
pub const SCRATCH_BUFFER_NAME: &str = "[scratch]";
@@ -110,67 +79,28 @@ 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,
-}
-
-pub type DocumentSavedEventResult = Result<DocumentSavedEvent, anyhow::Error>;
-pub type DocumentSavedEventFuture = BoxFuture<'static, DocumentSavedEventResult>;
-
-#[derive(Debug)]
-pub struct SavePoint {
- /// The view this savepoint is associated with
- pub view: ViewId,
- 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,
+ pub(crate) selections: HashMap<ViewId, Selection>,
path: Option<PathBuf>,
- relative_path: OnceCell<Option<PathBuf>>,
encoding: &'static encoding::Encoding,
- has_bom: bool,
+ /// Current editing mode.
+ pub mode: Mode,
pub restore_cursor: bool,
/// Current indent style.
pub indent_style: IndentStyle,
- editor_config: EditorConfig,
/// The document's default line ending.
pub line_ending: LineEnding,
- pub syntax: Option<Syntax>,
+ syntax: Option<Syntax>,
/// Corresponding language scope name. Usually `source.<lang>`.
- pub language: Option<Arc<LanguageConfiguration>>,
+ pub(crate) language: Option<Arc<LanguageConfiguration>>,
/// Pending changes since last history commit.
changes: ChangeSet,
@@ -181,116 +111,15 @@ pub struct Document {
// it back as it separated from the edits. We could split out the parts manually but that will
// be more troublesome.
pub history: Cell<History>,
- pub config: Arc<dyn DynAccess<Config>>,
-
- savepoints: Vec<Weak<SavePoint>>,
- // Last time we wrote to the file. This will carry the time the file was last opened if there
- // were no saves.
- last_saved_time: SystemTime,
+ pub savepoint: Option<Transaction>,
last_saved_revision: usize,
version: i32, // should be usize?
pub(crate) modified_since_accessed: bool,
- pub(crate) diagnostics: Vec<Diagnostic>,
- pub(crate) language_servers: HashMap<LanguageServerName, Arc<Client>>,
-
- diff_handle: Option<DiffHandle>,
- version_control_head: Option<Arc<ArcSwap<Box<str>>>>,
-
- // when document was used for most-recent-used buffer picker
- 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.
-///
-/// There are `*_inlay_hints` field for each kind of hints an LSP can send since we offer the
-/// option to style theme differently in the theme according to the (currently supported) kinds
-/// (`type`, `parameter` and the rest).
-///
-/// Inlay hints are always `InlineAnnotation`s, not overlays or line-ones: LSP may choose to place
-/// them anywhere in the text and will sometime offer config options to move them where the user
-/// wants them but it shouldn't be Helix who decides that so we use the most precise positioning.
-///
-/// The padding for inlay hints needs to be stored separately for before and after (the LSP spec
-/// uses 'left' and 'right' but not all text is left to right so let's be correct) padding because
-/// the 'before' padding must be added to a layer *before* the regular inlay hints and the 'after'
-/// padding comes ... after.
-#[derive(Debug, Clone)]
-pub struct DocumentInlayHints {
- /// Identifier for the inlay hints stored in this structure. To be checked to know if they have
- /// to be recomputed on idle or not.
- pub id: DocumentInlayHintsId,
-
- /// Inlay hints of `TYPE` kind, if any.
- pub type_inlay_hints: Vec<InlineAnnotation>,
-
- /// Inlay hints of `PARAMETER` kind, if any.
- pub parameter_inlay_hints: Vec<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>,
-
- /// 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>,
-}
-
-impl DocumentInlayHints {
- /// Generate an empty list of inlay hints with the given ID.
- 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(),
- }
- }
-}
-
-/// Associated with a [`Document`] and [`ViewId`], uniquely identifies the state of inlay hints for
-/// for that document and view: if this changed since the last save, the inlay hints for the view
-/// should be recomputed.
-///
-/// We can't store the `ViewOffset` instead of the first and last asked-for lines because if
-/// softwrapping changes, the `ViewOffset` may not change while the displayed lines will.
-#[derive(Copy, Clone, PartialEq, Eq)]
-pub struct DocumentInlayHintsId {
- /// First line for which the inlay hints were requested.
- pub first_line: usize,
- /// Last line for which the inlay hints were requested.
- pub last_line: usize,
+ diagnostics: Vec<Diagnostic>,
+ language_server: Option<Arc<helix_lsp::Client>>,
}
use std::{fmt, mem};
@@ -300,18 +129,15 @@ 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)
.field("encoding", &self.encoding)
+ .field("mode", &self.mode)
.field("restore_cursor", &self.restore_cursor)
.field("syntax", &self.syntax)
.field("language", &self.language)
.field("changes", &self.changes)
.field("old_state", &self.old_state)
// .field("history", &self.history)
- .field("last_saved_time", &self.last_saved_time)
.field("last_saved_revision", &self.last_saved_revision)
.field("version", &self.version)
.field("modified_since_accessed", &self.modified_since_accessed)
@@ -321,121 +147,16 @@ impl fmt::Debug for Document {
}
}
-impl fmt::Debug for DocumentInlayHintsId {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- // Much more agreable to read when debugging
- f.debug_struct("DocumentInlayHintsId")
- .field("lines", &(self.first_line..self.last_line))
- .finish()
- }
-}
-
-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,
- EncodingRs(encoding::Encoder),
-}
-
-impl Encoder {
- fn from_encoding(encoding: &'static encoding::Encoding) -> Self {
- if encoding == encoding::UTF_16BE {
- Self::Utf16Be
- } else if encoding == encoding::UTF_16LE {
- Self::Utf16Le
- } else {
- Self::EncodingRs(encoding.new_encoder())
- }
- }
-
- fn encode_from_utf8(
- &mut self,
- src: &str,
- dst: &mut [u8],
- is_empty: bool,
- ) -> (encoding::CoderResult, usize, usize) {
- if src.is_empty() {
- return (encoding::CoderResult::InputEmpty, 0, 0);
- }
- let mut write_to_buf = |convert: fn(u16) -> [u8; 2]| {
- let to_write = src.char_indices().map(|(indice, char)| {
- let mut encoded: [u16; 2] = [0, 0];
- (
- indice,
- char.encode_utf16(&mut encoded)
- .iter_mut()
- .flat_map(|char| convert(*char))
- .collect::<Vec<u8>>(),
- )
- });
-
- let mut total_written = 0usize;
-
- for (indice, utf16_bytes) in to_write {
- let character_size = utf16_bytes.len();
-
- if dst.len() <= (total_written + character_size) {
- return (encoding::CoderResult::OutputFull, indice, total_written);
- }
-
- for character in utf16_bytes {
- dst[total_written] = character;
- total_written += 1;
- }
- }
-
- (encoding::CoderResult::InputEmpty, src.len(), total_written)
- };
-
- match self {
- Self::Utf16Be => write_to_buf(u16::to_be_bytes),
- Self::Utf16Le => write_to_buf(u16::to_le_bytes),
- Self::EncodingRs(encoder) => {
- let (code_result, read, written, ..) = encoder.encode_from_utf8(src, dst, is_empty);
-
- (code_result, read, written)
- }
- }
- }
-}
-
-// Apply BOM if encoding permit it, return the number of bytes written at the start of buf
-fn apply_bom(encoding: &'static encoding::Encoding, buf: &mut [u8; BUF_SIZE]) -> usize {
- if encoding == encoding::UTF_8 {
- buf[0] = 0xef;
- buf[1] = 0xbb;
- buf[2] = 0xbf;
- 3
- } else if encoding == encoding::UTF_16BE {
- buf[0] = 0xfe;
- buf[1] = 0xff;
- 2
- } else if encoding == encoding::UTF_16LE {
- buf[0] = 0xff;
- buf[1] = 0xfe;
- 2
- } else {
- 0
- }
-}
-
// The documentation and implementation of this function should be up-to-date with
// its sibling function, `to_writer()`.
//
/// Decodes a stream of bytes into UTF-8, returning a `Rope` and the
-/// encoding it was decoded as with BOM information. The optional `encoding`
-/// parameter can be used to override encoding auto-detection.
+/// encoding it was decoded as. The optional `encoding` parameter can
+/// be used to override encoding auto-detection.
pub fn from_reader<R: std::io::Read + ?Sized>(
reader: &mut R,
- encoding: Option<&'static Encoding>,
-) -> Result<(Rope, &'static Encoding, bool), io::Error> {
+ encoding: Option<&'static encoding::Encoding>,
+) -> Result<(Rope, &'static encoding::Encoding), 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
@@ -445,11 +166,26 @@ pub fn from_reader<R: std::io::Read + ?Sized>(
let mut buf_out = [0u8; BUF_SIZE];
let mut builder = RopeBuilder::new();
- let (encoding, has_bom, mut decoder, read) =
- read_and_detect_encoding(reader, encoding, &mut buf)?;
+ // By default, the encoding of the text is auto-detected via the
+ // `chardetng` crate which requires sample data from the reader.
+ // As a manual override to this auto-detection is possible, the
+ // same data is read into `buf` to ensure symmetry in the upcoming
+ // loop.
+ let (encoding, mut decoder, mut slice, mut is_empty) = {
+ let read = reader.read(&mut buf)?;
+ let is_empty = read == 0;
+ let encoding = encoding.unwrap_or_else(|| {
+ let mut encoding_detector = chardetng::EncodingDetector::new();
+ encoding_detector.feed(&buf, is_empty);
+ encoding_detector.guess(None, true)
+ });
+ let decoder = encoding.new_decoder();
- let mut slice = &buf[..read];
- let mut is_empty = read == 0;
+ // If the amount of bytes read from the reader is less than
+ // `buf.len()`, it is undesirable to read the bytes afterwards.
+ let slice = &buf[..read];
+ (encoding, decoder, slice, is_empty)
+ };
// `RopeBuilder::append()` expects a `&str`, so this is the "real"
// output buffer. When decoding, the number of bytes in the output
@@ -516,82 +252,7 @@ pub fn from_reader<R: std::io::Read + ?Sized>(
is_empty = read == 0;
}
let rope = builder.finish();
- Ok((rope, encoding, has_bom))
-}
-
-pub fn read_to_string<R: std::io::Read + ?Sized>(
- reader: &mut R,
- encoding: Option<&'static Encoding>,
-) -> Result<(String, &'static Encoding, bool), Error> {
- let mut buf = [0u8; BUF_SIZE];
-
- let (encoding, has_bom, mut decoder, read) =
- read_and_detect_encoding(reader, encoding, &mut buf)?;
-
- let mut slice = &buf[..read];
- let mut is_empty = read == 0;
- let mut buf_string = String::with_capacity(buf.len());
-
- loop {
- let mut total_read = 0usize;
-
- loop {
- let (result, read, ..) =
- decoder.decode_to_string(&slice[total_read..], &mut buf_string, is_empty);
-
- total_read += read;
-
- match result {
- encoding::CoderResult::InputEmpty => {
- debug_assert_eq!(slice.len(), total_read);
- break;
- }
- encoding::CoderResult::OutputFull => {
- debug_assert!(slice.len() > total_read);
- buf_string.reserve(buf.len())
- }
- }
- }
-
- if is_empty {
- debug_assert_eq!(reader.read(&mut buf)?, 0);
- break;
- }
-
- let read = reader.read(&mut buf)?;
- slice = &buf[..read];
- is_empty = read == 0;
- }
- Ok((buf_string, encoding, has_bom))
-}
-
-/// Reads the first chunk from a Reader into the given buffer
-/// and detects the encoding.
-///
-/// By default, the encoding of the text is auto-detected by
-/// `encoding_rs` for_bom, and if it fails, from `chardetng`
-/// crate which requires sample data from the reader.
-/// As a manual override to this auto-detection is possible, the
-/// same data is read into `buf` to ensure symmetry in the upcoming
-/// loop.
-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> {
- let read = reader.read(buf)?;
- let is_empty = read == 0;
- let (encoding, has_bom) = encoding
- .map(|encoding| (encoding, false))
- .or_else(|| encoding::Encoding::for_bom(buf).map(|(encoding, _bom_size)| (encoding, true)))
- .unwrap_or_else(|| {
- let mut encoding_detector = chardetng::EncodingDetector::new();
- encoding_detector.feed(buf, is_empty);
- (encoding_detector.guess(None, true), false)
- });
- let decoder = encoding.new_decoder();
-
- Ok((encoding, has_bom, decoder, read))
+ Ok((rope, encoding))
}
// The documentation and implementation of this function should be up-to-date with
@@ -602,7 +263,7 @@ fn read_and_detect_encoding<R: std::io::Read + ?Sized>(
/// replacement characters may appear in the encoded text.
pub async fn to_writer<'a, W: tokio::io::AsyncWriteExt + Unpin + ?Sized>(
writer: &'a mut W,
- encoding_with_bom_info: (&'static Encoding, bool),
+ encoding: &'static encoding::Encoding,
rope: &'a Rope,
) -> Result<(), Error> {
// Text inside a `Rope` is stored as non-contiguous blocks of data called
@@ -611,22 +272,13 @@ pub async fn to_writer<'a, W: tokio::io::AsyncWriteExt + Unpin + ?Sized>(
// determined by filtering the iterator to remove all empty chunks and then
// appending an empty chunk to it. This is valuable for detecting when all
// chunks in the `Rope` have been iterated over in the subsequent loop.
- let (encoding, has_bom) = encoding_with_bom_info;
-
let iter = rope
.chunks()
.filter(|c| !c.is_empty())
.chain(std::iter::once(""));
let mut buf = [0u8; BUF_SIZE];
-
- let mut total_written = if has_bom {
- apply_bom(encoding, &mut buf)
- } else {
- 0
- };
-
- let mut encoder = Encoder::from_encoding(encoding);
-
+ let mut encoder = encoding.new_encoder();
+ let mut total_written = 0usize;
for chunk in iter {
let is_empty = chunk.is_empty();
let mut total_read = 0usize;
@@ -667,7 +319,6 @@ pub async fn to_writer<'a, W: tokio::io::AsyncWriteExt + Unpin + ?Sized>(
break;
}
}
-
Ok(())
}
@@ -679,36 +330,24 @@ where
*mut_ref = f(mem::take(mut_ref));
}
-use helix_lsp::{lsp, Client, LanguageServerId, LanguageServerName};
+use helix_lsp::lsp;
use url::Url;
impl Document {
- pub fn from(
- 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();
- let changes = ChangeSet::new(text.slice(..));
+ pub fn from(text: Rope, encoding: Option<&'static encoding::Encoding>) -> Self {
+ let encoding = encoding.unwrap_or(encoding::UTF_8);
+ let changes = ChangeSet::new(&text);
let old_state = None;
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,
+ line_ending: DEFAULT_LINE_ENDING,
+ mode: Mode::Normal,
restore_cursor: false,
syntax: None,
language: None,
@@ -717,78 +356,39 @@ impl Document {
diagnostics: Vec::new(),
version: 0,
history: Cell::new(History::default()),
- savepoints: Vec::new(),
- last_saved_time: SystemTime::now(),
+ savepoint: None,
last_saved_revision: 0,
modified_since_accessed: false,
- language_servers: HashMap::new(),
- diff_handle: None,
- config,
- 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(),
+ language_server: None,
}
}
- pub fn default(
- config: Arc<dyn DynAccess<Config>>,
- syn_loader: Arc<ArcSwap<syntax::Loader>>,
- ) -> 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)
- }
-
// TODO: async fn?
/// Create a new document from `path`. Encoding is auto-detected, but it can be manually
/// overwritten with the `encoding` parameter.
pub fn open(
path: &Path,
- mut encoding: Option<&'static Encoding>,
- detect_language: bool,
- 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);
-
+ encoding: Option<&'static encoding::Encoding>,
+ config_loader: Option<Arc<syntax::Loader>>,
+ ) -> 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 (rope, encoding) = if path.exists() {
+ 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 encoding = encoding.unwrap_or(encoding::UTF_8);
- (Rope::from(line_ending.as_str()), encoding, false)
+ (Rope::from(DEFAULT_LINE_ENDING.as_str()), encoding)
};
- 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));
// set the path and try detecting the language
- doc.set_path(Some(path));
- if detect_language {
- doc.detect_language(&loader);
+ doc.set_path(Some(path))?;
+ 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 +396,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<impl Future<Output = LspFormatting> + 'static> {
if self.language_config()?.auto_format {
- self.format(editor)
+ self.format()
} else {
None
}
@@ -809,520 +406,206 @@ impl Document {
/// If supported, returns the changes that should be applied to this document in order
/// 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>>> {
- if let Some((fmt_cmd, fmt_args)) = self
- .language_config()
- .and_then(|c| c.formatter.as_ref())
- .and_then(|formatter| {
- Some((
- helix_stdx::env::which(&formatter.command).ok()?,
- &formatter.args,
- ))
- })
- {
- 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))
- .stdin(Stdio::piped())
- .stdout(Stdio::piped())
- .stderr(Stdio::piped());
-
- let formatting_future = async move {
- let mut process = process
- .spawn()
- .map_err(|e| FormatterError::SpawningFailed {
- command: fmt_cmd.to_string_lossy().into(),
- error: e.kind(),
- })?;
-
- 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)?;
-
- if !output.status.success() {
- if !output.stderr.is_empty() {
- let err = String::from_utf8_lossy(&output.stderr).to_string();
- log::error!("Formatter error: {}", err);
- return Err(FormatterError::NonZeroExitStatus(Some(err)));
- }
-
- return Err(FormatterError::NonZeroExitStatus(None));
- } else if !output.stderr.is_empty() {
- log::debug!(
- "Formatter printed to stderr: {}",
- String::from_utf8_lossy(&output.stderr)
- );
- }
-
- let str = std::str::from_utf8(&output.stdout)
- .map_err(|_| FormatterError::InvalidUtf8Output)?;
-
- Ok(helix_core::diff::compare_ropes(&text, &Rope::from(str)))
- };
- return Some(formatting_future.boxed());
- };
-
+ pub fn format(&self) -> Option<impl Future<Output = LspFormatting> + 'static> {
+ let language_server = self.language_server()?;
let text = self.text.clone();
- // finds first language server that supports formatting and then formats
- let language_server = self
- .language_servers_with_feature(LanguageServerFeature::Format)
- .next()?;
let offset_encoding = language_server.offset_encoding();
let request = language_server.text_document_formatting(
self.identifier(),
- lsp::FormattingOptions {
- tab_size: self.tab_width() as u32,
- insert_spaces: matches!(self.indent_style, IndentStyle::Spaces(_)),
- ..Default::default()
- },
+ lsp::FormattingOptions::default(),
None,
)?;
let fut = async move {
- let edits = request
- .await
- .unwrap_or_else(|e| {
- log::warn!("LSP formatting failed: {}", e);
- Default::default()
- })
- .unwrap_or_default();
- Ok(helix_lsp::util::generate_transaction_from_edits(
- &text,
+ let edits = request.await.unwrap_or_else(|e| {
+ log::warn!("LSP formatting failed: {}", e);
+ Default::default()
+ });
+ LspFormatting {
+ doc: text,
edits,
offset_encoding,
- ))
+ }
};
- Some(fut.boxed())
+ Some(fut)
}
- pub fn save<P: Into<PathBuf>>(
- &mut self,
- path: Option<P>,
- force: bool,
- ) -> Result<
- impl Future<Output = Result<DocumentSavedEvent, anyhow::Error>> + 'static + Send,
- anyhow::Error,
- > {
- let path = path.map(|path| path.into());
- self.save_impl(path, force)
+ pub fn save(&mut self) -> impl Future<Output = Result<(), anyhow::Error>> {
+ self.save_impl::<futures_util::future::Ready<_>>(None)
+ }
- // futures_util::future::Ready<_>,
+ pub fn format_and_save(
+ &mut self,
+ formatting: Option<impl Future<Output = LspFormatting>>,
+ ) -> impl Future<Output = anyhow::Result<()>> {
+ self.save_impl(formatting)
}
+ // TODO: do we need some way of ensuring two save operations on the same doc can't run at once?
+ // or is that handled by the OS/async layer
/// The `Document`'s text is encoded according to its encoding and written to the file located
/// at its `path()`.
- fn save_impl(
+ ///
+ /// If `formatting` is present, it supplies some changes that we apply to the text before saving.
+ fn save_impl<F: Future<Output = LspFormatting>>(
&mut self,
- path: Option<PathBuf>,
- force: bool,
- ) -> Result<
- impl Future<Output = Result<DocumentSavedEvent, anyhow::Error>> + 'static + Send,
- anyhow::Error,
- > {
- log::debug!(
- "submitting save of doc '{:?}'",
- self.path().map(|path| path.to_string_lossy())
- );
-
+ formatting: Option<F>,
+ ) -> impl Future<Output = Result<(), anyhow::Error>> {
// we clone and move text + path into the future so that we asynchronously save the current
// state without blocking any further edits.
- let text = self.text().clone();
- let path = match path {
- Some(path) => helix_stdx::path::canonicalize(path),
- None => {
- if self.path.is_none() {
- bail!("Can't save with no path set!");
- }
- self.path.as_ref().unwrap().clone()
- }
- };
+ let mut text = self.text().clone();
+ let path = self.path.clone().expect("Can't save with no path set!");
+ let identifier = self.identifier();
- let identifier = self.path().map(|_| self.identifier());
- let language_servers: Vec<_> = self.language_servers.values().cloned().collect();
+ let language_server = self.language_server.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;
+ self.reset_modified();
- let encoding_with_bom_info = (self.encoding, self.has_bom);
- let last_saved_time = self.last_saved_time;
+ let encoding = self.encoding;
// We encode the file according to the `Document`'s encoding.
- let future = async move {
- use tokio::fs;
+ async move {
+ use tokio::fs::File;
if let Some(parent) = path.parent() {
// TODO: display a prompt asking the user if the directories should be created
if !parent.exists() {
- if force {
- std::fs::DirBuilder::new().recursive(true).create(parent)?;
- } else {
- bail!("can't save file, parent directory does not exist (use :w! to create it)");
- }
- }
- }
-
- // Protect against overwriting changes made externally
- if !force {
- if let Ok(metadata) = fs::metadata(&path).await {
- if let Ok(mtime) = metadata.modified() {
- if last_saved_time < mtime {
- bail!("file modified by an external process, use :w! to overwrite");
- }
- }
+ bail!("can't save file, parent directory does not exist");
}
}
- 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;
+ if let Some(fmt) = formatting {
+ let success = Transaction::from(fmt.await).changes().apply(&mut text);
+ if !success {
+ // This shouldn't happen, because the transaction changes were generated
+ // from the same text we're saving.
+ log::error!("failed to apply format changes before saving");
}
}
- write_result?;
+ let mut file = File::create(path).await?;
+ to_writer(&mut file, encoding, &text).await?;
- let event = DocumentSavedEvent {
- revision: current_rev,
- save_time,
- doc_id,
- path,
- text: text.clone(),
- };
-
- for language_server in language_servers {
+ if let Some(language_server) = language_server {
if !language_server.is_initialized() {
- continue;
+ return Ok(());
}
- if let Some(id) = identifier.clone() {
- language_server.text_document_did_save(id, &text);
+ if let Some(notification) =
+ language_server.text_document_did_save(identifier, &text)
+ {
+ notification.await?;
}
}
- Ok(event)
- };
-
- Ok(future)
- }
-
- /// 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);
+ Ok(())
+ }
}
/// 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())
+ pub fn detect_language(&mut self, config_loader: Arc<syntax::Loader>) {
+ if let Some(path) = &self.path {
+ let language_config = config_loader
+ .language_config_for_file_name(path)
+ .or_else(|| config_loader.language_config_for_shebang(self.text()));
+ self.set_language(language_config, Some(config_loader));
+ }
}
/// 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.
+ /// configured in `languages.toml`, with a fallback to 4 space indentation if it isn't
+ /// specified. Line ending is likewise auto-detected, and will fallback to the default OS
+ /// line ending.
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.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()
- }
- },
- None => SystemTime::now(),
- };
- }
-
- // Detect if the file is readonly and change the readonly field if necessary (unix only)
- pub fn detect_readonly(&mut self) {
- // Allows setting the flag for files the user cannot modify, like root files
- self.readonly = match &self.path {
- None => false,
- Some(p) => readonly(p),
- };
+ 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))
+ });
+ self.line_ending = auto_detect_line_ending(&self.text).unwrap_or(DEFAULT_LINE_ENDING);
}
/// Reload the document from its path.
- pub fn reload(
- &mut self,
- view: &mut View,
- 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()),
- },
- };
+ pub fn reload(&mut self, view_id: ViewId) -> Result<(), Error> {
+ let encoding = &self.encoding;
+ let path = self.path().filter(|path| path.exists());
- // Once we have a valid path we check if its readonly status has changed
- self.detect_readonly();
+ // If there is no path or the path no longer exists.
+ if path.is_none() {
+ bail!("can't find file to reload from");
+ }
- let mut file = std::fs::File::open(&path)?;
+ let mut file = std::fs::File::open(path.unwrap())?;
let (rope, ..) = from_reader(&mut file, Some(encoding))?;
// Calculate the difference between the buffer and source text, and apply it.
// This is not considered a modification of the contents of the file regardless
// of the encoding.
let transaction = helix_core::diff::compare_ropes(self.text(), &rope);
- self.apply(&transaction, view.id);
- self.append_changes_to_history(view);
+ self.apply(&transaction, view_id);
+ self.append_changes_to_history(view_id);
self.reset_modified();
- self.pickup_last_saved_time();
- self.detect_indent_and_line_ending();
- match provider_registry.get_diff_base(&path) {
- Some(diff_base) => self.set_diff_base(diff_base),
- None => self.diff_handle = None,
- }
-
- self.version_control_head = provider_registry.get_current_head_name(&path);
+ self.detect_indent_and_line_ending();
Ok(())
}
/// Sets the [`Document`]'s encoding with the encoding correspondent to `label`.
pub fn set_encoding(&mut self, label: &str) -> Result<(), Error> {
- let encoding =
- Encoding::for_label(label.as_bytes()).ok_or_else(|| anyhow!("unknown encoding"))?;
-
- self.encoding = encoding;
-
+ self.encoding = encoding::Encoding::for_label(label.as_bytes())
+ .ok_or_else(|| anyhow!("unknown encoding"))?;
Ok(())
}
/// Returns the [`Document`]'s current encoding.
- pub fn encoding(&self) -> &'static Encoding {
+ pub fn encoding(&self) -> &'static encoding::Encoding {
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();
+ pub fn set_path(&mut self, path: Option<&Path>) -> Result<(), std::io::Error> {
+ let path = path
+ .map(helix_core::path::get_canonicalized_path)
+ .transpose()?;
// 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();
+ Ok(())
}
/// 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()) {
+ let syntax = Syntax::new(&self.text, highlight_config, loader);
+ self.syntax = Some(syntax);
+ }
+
+ 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.
- pub fn set_language_by_language_id(
- &mut self,
- language_id: &str,
- loader: &syntax::Loader,
- ) -> anyhow::Result<()> {
- let language = loader
- .language_for_name(language_id)
- .ok_or_else(|| anyhow!("invalid language id: {}", language_id))?;
- let config = loader.language(language).config().clone();
- self.set_language(Some(config), loader);
- Ok(())
+ /// Set the programming language for the file if you know the name (scope) but don't have the
+ /// [`syntax::LanguageConfiguration`] for it.
+ pub fn set_language2(&mut self, scope: &str, config_loader: Arc<syntax::Loader>) {
+ let language_config = config_loader.language_config_for_scope(scope);
+
+ self.set_language(language_config, Some(config_loader));
+ }
+
+ /// Set the LSP.
+ pub fn set_language_server(&mut self, language_server: Option<Arc<helix_lsp::Client>>) {
+ self.language_server = language_server;
}
/// Select text within the [`Document`].
@@ -1330,230 +613,82 @@ impl Document {
// TODO: use a transaction?
self.selections
.insert(view_id, selection.ensure_invariants(self.text().slice(..)));
- helix_event::dispatch(SelectionDidChange {
- doc: self,
- view: view_id,
- })
- }
-
- /// Find the origin selection of the text in a document, i.e. where
- /// a single cursor would go if it were on the first grapheme. If
- /// the text is empty, returns (0, 0).
- pub fn origin(&self) -> Range {
- if self.text().len_chars() == 0 {
- return Range::new(0, 0);
- }
-
- Range::new(0, 1).grapheme_aligned(self.text().slice(..))
- }
-
- /// Reset the view's selection on this document to the
- /// [origin](Document::origin) cursor.
- pub fn reset_selection(&mut self, view_id: ViewId) {
- let origin = self.origin();
- 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.
- pub fn ensure_view_init(&mut self, view_id: ViewId) {
- if !self.selections.contains_key(&view_id) {
- self.reset_selection(view_id);
- }
-
- self.view_data_mut(view_id);
- }
-
- /// Mark document as recent used for MRU sorting
- pub fn mark_as_focused(&mut self) {
- self.focused_at = std::time::Instant::now();
- }
-
- /// Remove a view's selection and inlay hints from this 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.
- fn apply_impl(
- &mut self,
- transaction: &Transaction,
- view_id: ViewId,
- emit_lsp_notification: bool,
- ) -> bool {
- use helix_core::Assoc;
-
+ fn apply_impl(&mut self, transaction: &Transaction, view_id: ViewId) -> bool {
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 {
+ for selection in self.selections.values_mut() {
+ *selection = selection
+ .clone()
+ // Map through changes
+ .map(transaction.changes())
+ // Ensure all selections accross 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,
selection.clone().ensure_invariants(self.text.slice(..)),
);
- helix_event::dispatch(SelectionDidChange {
- doc: self,
- 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);
- }
-
- // 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,
- })
- }
+ if !transaction.changes().is_empty() {
+ self.version += 1;
- // 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;
+ // generate revert to savepoint
+ if self.savepoint.is_some() {
+ take_with(&mut self.savepoint, |prev_revert| {
+ let revert = transaction.invert(&old_doc);
+ Some(revert.compose(prev_revert.unwrap()))
+ });
}
- }
- // 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);
- }
-
- // 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;
+ // update tree-sitter syntax tree
+ if let Some(syntax) = &mut self.syntax {
+ // TODO: no unwrap
+ syntax
+ .update(&old_doc, &self.text, transaction.changes())
+ .unwrap();
}
- 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(),
- )
- });
-
- // 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);
- }
+ // map state.diagnostics over changes::map_pos too
+ for diagnostic in &mut self.diagnostics {
+ use helix_core::Assoc;
+ let changes = transaction.changes();
+ diagnostic.range.start = changes.map_pos(diagnostic.range.start, Assoc::After);
+ diagnostic.range.end = changes.map_pos(diagnostic.range.end, Assoc::After);
+ diagnostic.line = self.text.char_to_line(diagnostic.range.start);
+ }
- helix_event::dispatch(DocumentDidChange {
- doc: self,
- view: view_id,
- old_text: &old_doc,
- changes,
- ghost_transaction: !emit_lsp_notification,
- });
+ // emit lsp notification
+ if let Some(language_server) = self.language_server() {
+ let notify = language_server.text_document_did_change(
+ self.versioned_identifier(),
+ &old_doc,
+ self.text(),
+ transaction.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(
- &mut self,
- transaction: &Transaction,
- view_id: ViewId,
- emit_lsp_notification: bool,
- ) -> bool {
+ /// Apply a [`Transaction`] to the [`Document`] to change its text.
+ pub fn apply(&mut self, transaction: &Transaction, view_id: ViewId) -> bool {
// store the state just before any changes are made. This allows us to undo to the
// state just before a transaction was applied.
if self.changes.is_empty() && !transaction.changes().is_empty() {
@@ -1563,7 +698,7 @@ impl Document {
});
}
- let success = self.apply_impl(transaction, view_id, emit_lsp_notification);
+ let success = self.apply_impl(transaction, view_id);
if !transaction.changes().is_empty() {
// Compose this transaction with the previous one
@@ -1573,28 +708,12 @@ impl Document {
}
success
}
- /// Apply a [`Transaction`] to the [`Document`] to change its text.
- pub fn apply(&mut self, transaction: &Transaction, view_id: ViewId) -> bool {
- self.apply_inner(transaction, view_id, true)
- }
-
- /// Apply a [`Transaction`] to the [`Document`] to change its text
- /// without notifying the language servers. This is useful for temporary transactions
- /// that must not influence the server.
- pub fn apply_temporary(&mut self, transaction: &Transaction, view_id: ViewId) -> bool {
- self.apply_inner(transaction, view_id, false)
- }
- 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;
- }
+ fn undo_redo_impl(&mut self, view_id: ViewId, undo: bool) -> bool {
let mut history = self.history.take();
let txn = if undo { history.undo() } else { history.redo() };
let success = if let Some(txn) = txn {
- self.apply_impl(txn, view.id, true)
+ self.apply_impl(txn, view_id)
} else {
false
};
@@ -1602,80 +721,32 @@ impl Document {
if success {
// reset changeset to fix len
- self.changes = ChangeSet::new(self.text().slice(..));
- // Sync with changes with the jumplist selections.
- view.sync_changes(self);
+ self.changes = ChangeSet::new(self.text());
}
success
}
/// Undo the last modification to the [`Document`]. Returns whether the undo was successful.
- pub fn undo(&mut self, view: &mut View) -> bool {
- self.undo_redo_impl(view, true)
+ pub fn undo(&mut self, view_id: ViewId) -> bool {
+ self.undo_redo_impl(view_id, true)
}
- /// Redo the last modification to the [`Document`]. Returns whether the redo was successful.
- pub fn redo(&mut self, view: &mut View) -> bool {
- self.undo_redo_impl(view, false)
+ /// Redo the last modification to the [`Document`]. Returns whether the redo was sucessful.
+ pub fn redo(&mut self, view_id: ViewId) -> bool {
+ self.undo_redo_impl(view_id, false)
}
- /// Creates a reference counted snapshot (called savpepoint) of the document.
- ///
- /// The snapshot will remain valid (and updated) idenfinitly as long as ereferences to it exist.
- /// Restoring the snapshot will restore the selection and the contents of the document to
- /// the state it had when this function was called.
- pub fn savepoint(&mut self, view: &View) -> Arc<SavePoint> {
- let revert = Transaction::new(self.text()).with_selection(self.selection(view.id).clone());
- // check if there is already an existing (identical) savepoint around
- if let Some(savepoint) = self
- .savepoints
- .iter()
- .rev()
- .find_map(|savepoint| savepoint.upgrade())
- {
- let transaction = savepoint.revert.lock();
- if savepoint.view == view.id
- && transaction.changes().is_empty()
- && transaction.selection() == revert.selection()
- {
- drop(transaction);
- return savepoint;
- }
- }
- let savepoint = Arc::new(SavePoint {
- view: view.id,
- revert: Mutex::new(revert),
- });
- self.savepoints.push(Arc::downgrade(&savepoint));
- savepoint
+ pub fn savepoint(&mut self) {
+ self.savepoint = Some(Transaction::new(self.text()));
}
- pub fn restore(&mut self, view: &mut View, savepoint: &SavePoint, emit_lsp_notification: bool) {
- assert_eq!(
- savepoint.view, view.id,
- "Savepoint must not be used with a different view!"
- );
- // search and remove savepoint using a ptr comparison
- // this avoids a deadlock as we need to lock the mutex
- let savepoint_idx = self
- .savepoints
- .iter()
- .position(|savepoint_ref| std::ptr::eq(savepoint_ref.as_ptr(), savepoint))
- .expect("Savepoint must belong to this document");
-
- let savepoint_ref = self.savepoints.remove(savepoint_idx);
- let mut revert = savepoint.revert.lock();
- self.apply_inner(&revert, view.id, emit_lsp_notification);
- *revert = Transaction::new(self.text()).with_selection(self.selection(view.id).clone());
- self.savepoints.push(savepoint_ref)
- }
-
- 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;
+ pub fn restore(&mut self, view_id: ViewId) {
+ if let Some(revert) = self.savepoint.take() {
+ self.apply(&revert, view_id);
}
+ }
+
+ fn earlier_later_impl(&mut self, view_id: ViewId, uk: UndoKind, earlier: bool) -> bool {
let txns = if earlier {
self.history.get_mut().earlier(uk)
} else {
@@ -1683,41 +754,39 @@ impl Document {
};
let mut success = false;
for txn in txns {
- if self.apply_impl(&txn, view.id, true) {
+ if self.apply_impl(&txn, view_id) {
success = true;
}
}
if success {
// reset changeset to fix len
- self.changes = ChangeSet::new(self.text().slice(..));
- // Sync with changes with the jumplist selections.
- view.sync_changes(self);
+ self.changes = ChangeSet::new(self.text());
}
success
}
/// Undo modifications to the [`Document`] according to `uk`.
- pub fn earlier(&mut self, view: &mut View, uk: UndoKind) -> bool {
- self.earlier_later_impl(view, uk, true)
+ pub fn earlier(&mut self, view_id: ViewId, uk: UndoKind) -> bool {
+ self.earlier_later_impl(view_id, uk, true)
}
/// Redo modifications to the [`Document`] according to `uk`.
- pub fn later(&mut self, view: &mut View, uk: UndoKind) -> bool {
- self.earlier_later_impl(view, uk, false)
+ pub fn later(&mut self, view_id: ViewId, uk: UndoKind) -> bool {
+ self.earlier_later_impl(view_id, uk, false)
}
/// Commit pending changes to history
- pub fn append_changes_to_history(&mut self, view: &mut View) {
+ pub fn append_changes_to_history(&mut self, view_id: ViewId) {
if self.changes.is_empty() {
return;
}
- let new_changeset = ChangeSet::new(self.text().slice(..));
+ let new_changeset = ChangeSet::new(self.text());
let changes = std::mem::replace(&mut self.changes, new_changeset);
// Instead of doing this messy merge we could always commit, and based on transaction
// annotations either add a new layer or compose into the previous one.
let transaction =
- Transaction::from(changes).with_selection(self.selection(view.id).clone());
+ Transaction::from(changes).with_selection(self.selection(view_id).clone());
// HAXX: we need to reconstruct the state as it was before the changes..
let old_state = self.old_state.take().expect("no old_state available");
@@ -1725,9 +794,6 @@ impl Document {
let mut history = self.history.take();
history.commit_revision(&transaction, &old_state);
self.history.set(history);
-
- // Update jumplist entries in the view.
- view.apply(&transaction, self);
}
pub fn id(&self) -> DocumentId {
@@ -1739,12 +805,6 @@ impl Document {
let history = self.history.take();
let current_revision = history.current_revision();
self.history.set(history);
- log::debug!(
- "id {} modified - last saved: {}, current: {}",
- self.id,
- self.last_saved_revision,
- current_revision
- );
current_revision != self.last_saved_revision || !self.changes.is_empty()
}
@@ -1756,53 +816,26 @@ impl Document {
self.last_saved_revision = current_revision;
}
- /// Set the document's latest saved revision to the given one.
- pub fn set_last_saved_revision(&mut self, rev: usize, save_time: SystemTime) {
- log::debug!(
- "doc {} revision updated {} -> {}",
- self.id,
- self.last_saved_revision,
- rev
- );
- self.last_saved_revision = rev;
- self.last_saved_time = save_time;
- }
-
- /// Get the document's latest saved revision.
- pub fn get_last_saved_revision(&mut self) -> usize {
- self.last_saved_revision
- }
-
- /// Get the current revision number
- pub fn get_current_revision(&mut self) -> usize {
- let history = self.history.take();
- let current_revision = history.current_revision();
- self.history.set(history);
- current_revision
+ /// Current editing mode for the [`Document`].
+ pub fn mode(&self) -> Mode {
+ self.mode
}
/// Corresponding language scope name. Usually `source.<lang>`.
- pub fn language_scope(&self) -> Option<&str> {
+ pub fn language(&self) -> Option<&str> {
self.language
.as_ref()
.map(|language| language.scope.as_str())
}
- /// Language name for the document. Corresponds to the `name` key in
- /// `languages.toml` configuration.
- pub fn language_name(&self) -> Option<&str> {
- self.language
- .as_ref()
- .map(|language| language.language_id.as_str())
- }
-
- /// Language ID for the document. Either the `language-id`,
- /// or the document language name if no `language-id` has been specified.
+ /// Language ID for the document. Either the `language-id` from the
+ /// `language-server` configuration, or the document language if no
+ /// `language-id` has been specified.
pub fn language_id(&self) -> Option<&str> {
- self.language_config()?
- .language_server_language_id
- .as_deref()
- .or_else(|| self.language_name())
+ self.language_config()
+ .and_then(|config| config.language_server.as_ref())
+ .and_then(|lsp_config| lsp_config.language_id.as_deref())
+ .or_else(|| Some(self.language()?.rsplit_once('.')?.1))
}
/// Corresponding [`LanguageConfiguration`].
@@ -1815,85 +848,10 @@ 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| {
- config.language_servers.iter().filter_map(move |features| {
- let ls = &**self.language_servers.get(&features.name)?;
- if ls.is_initialized() {
- Some(ls)
- } else {
- None
- }
- })
- })
- }
-
- pub fn remove_language_server_by_name(&mut self, name: &str) -> Option<Arc<Client>> {
- self.language_servers.remove(name)
- }
-
- pub fn language_servers_with_feature(
- &self,
- feature: LanguageServerFeature,
- ) -> impl Iterator<Item = &helix_lsp::Client> {
- self.language_config().into_iter().flat_map(move |config| {
- config.language_servers.iter().filter_map(move |features| {
- let ls = &**self.language_servers.get(&features.name)?;
- if ls.is_initialized()
- && ls.supports_feature(feature)
- && features.has_feature(feature)
- {
- Some(ls)
- } else {
- None
- }
- })
- })
- }
-
- pub fn supports_language_server(&self, id: LanguageServerId) -> bool {
- self.language_servers().any(|l| l.id() == id)
- }
-
- pub fn diff_handle(&self) -> Option<&DiffHandle> {
- self.diff_handle.as_ref()
- }
-
- /// Intialize/updates the differ for this document with a new base.
- pub fn set_diff_base(&mut self, diff_base: Vec<u8>) {
- if let Ok((diff_base, ..)) = from_reader(&mut diff_base.as_slice(), Some(self.encoding)) {
- if let Some(differ) = &self.diff_handle {
- differ.update_diff_base(diff_base);
- return;
- }
- self.diff_handle = Some(DiffHandle::new(diff_base, self.text.clone()))
- } else {
- self.diff_handle = None;
- }
- }
-
- pub fn version_control_head(&self) -> Option<Arc<Box<str>>> {
- self.version_control_head.as_ref().map(|a| a.load_full())
- }
-
- pub fn set_version_control_head(
- &mut self,
- version_control_head: Option<Arc<ArcSwap<Box<str>>>>,
- ) {
- self.version_control_head = version_control_head;
+ /// Language server if it has been initialized.
+ pub fn language_server(&self) -> Option<&helix_lsp::Client> {
+ let server = self.language_server.as_deref()?;
+ server.is_initialized().then(|| server)
}
#[inline]
@@ -1902,35 +860,19 @@ impl Document {
self.syntax.as_ref()
}
- /// The width that the tab character is rendered at
+ /// Tab size in columns.
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)
- })
- }
-
- // The width (in spaces) of a level of indentation.
- pub fn indent_width(&self) -> usize {
- 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)
+ self.language_config()
+ .and_then(|config| config.indent.as_ref())
+ .map_or(4, |config| config.tab_width) // fallback to 4 columns
}
- /// 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)
+ /// Returns a string containing a single level of indentation.
+ ///
+ /// TODO: we might not need this function anymore, since the information
+ /// is conveniently available in `Document::indent_style` now.
+ pub fn indent_unit(&self) -> &'static str {
+ self.indent_style.as_str()
}
pub fn changes(&self) -> &ChangeSet {
@@ -1948,10 +890,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
@@ -1962,46 +900,14 @@ impl Document {
&self.selections[&view_id]
}
- #[inline]
pub fn selections(&self) -> &HashMap<ViewId, Selection> {
&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()
- }
-
- pub fn display_name(&self) -> Cow<'_, str> {
- self.relative_path()
- .map_or_else(|| SCRATCH_BUFFER_NAME.into(), |path| path.to_string_lossy())
+ .map(helix_core::path::get_relative_path)
}
// transact(Fn) ?
@@ -2031,139 +937,15 @@ impl Document {
)
}
- pub fn lsp_diagnostic_to_diagnostic(
- text: &Rope,
- language_config: Option<&LanguageConfiguration>,
- diagnostic: &helix_lsp::lsp::Diagnostic,
- provider: DiagnosticProvider,
- offset_encoding: helix_lsp::OffsetEncoding,
- ) -> Option<Diagnostic> {
- use helix_core::diagnostic::{Range, Severity::*};
-
- // TODO: convert inside server
- let start =
- if let Some(start) = lsp_pos_to_pos(text, diagnostic.range.start, offset_encoding) {
- start
- } else {
- log::warn!("lsp position out of bounds - {:?}", diagnostic);
- return None;
- };
-
- let end = if let Some(end) = lsp_pos_to_pos(text, diagnostic.range.end, offset_encoding) {
- end
- } else {
- log::warn!("lsp position out of bounds - {:?}", diagnostic);
- 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
- }
- });
-
- if let Some(lang_conf) = language_config {
- if let Some(severity) = severity {
- if severity < lang_conf.diagnostic_severity {
- return None;
- }
- }
- };
- use helix_core::diagnostic::{DiagnosticTag, NumberOrString};
-
- let code = match diagnostic.code.clone() {
- Some(x) => match x {
- lsp::NumberOrString::Number(x) => Some(NumberOrString::Number(x)),
- lsp::NumberOrString::String(x) => Some(NumberOrString::String(x)),
- },
- None => None,
- };
-
- let tags = if let Some(tags) = &diagnostic.tags {
- let new_tags = tags
- .iter()
- .filter_map(|tag| match *tag {
- lsp::DiagnosticTag::DEPRECATED => Some(DiagnosticTag::Deprecated),
- lsp::DiagnosticTag::UNNECESSARY => Some(DiagnosticTag::Unnecessary),
- _ => None,
- })
- .collect();
-
- new_tags
- } else {
- Vec::new()
- };
-
- 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);
-
- Some(Diagnostic {
- range: Range { start, end },
- ends_at_word,
- starts_at_word,
- zero_width: start == end,
- line: diagnostic.range.start.line as usize,
- message: diagnostic.message.clone(),
- severity,
- code,
- tags,
- source: diagnostic.source.clone(),
- data: diagnostic.data.clone(),
- provider,
- })
- }
-
#[inline]
pub fn diagnostics(&self) -> &[Diagnostic] {
&self.diagnostics
}
- pub fn replace_diagnostics(
- &mut self,
- diagnostics: impl IntoIterator<Item = Diagnostic>,
- unchanged_sources: &[String],
- provider: Option<&DiagnosticProvider>,
- ) {
- if unchanged_sources.is_empty() {
- if let Some(provider) = provider {
- self.diagnostics
- .retain(|diagnostic| &diagnostic.provider != provider);
- } else {
- self.diagnostics.clear();
- }
- } else {
- self.diagnostics.retain(|d| {
- if provider.is_some_and(|provider| provider != &d.provider) {
- return true;
- }
-
- if let Some(source) = &d.source {
- unchanged_sources.contains(source)
- } else {
- false
- }
- });
- }
- self.diagnostics.extend(diagnostics);
- self.diagnostics.sort_by_key(|diagnostic| {
- (
- diagnostic.range,
- diagnostic.severity,
- diagnostic.provider.clone(),
- )
- });
- }
-
- /// 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) {
+ pub fn set_diagnostics(&mut self, diagnostics: Vec<Diagnostic>) {
+ self.diagnostics = diagnostics;
self.diagnostics
- .retain(|d| d.provider.language_server_id() != Some(id));
+ .sort_unstable_by_key(|diagnostic| diagnostic.range);
}
/// Get the document's auto pairs. If the document has a recognized
@@ -2187,166 +969,24 @@ impl Document {
None => global_config,
}
}
-
- 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
- .language_config()
- .and_then(|config| {
- config
- .soft_wrap
- .as_ref()
- .and_then(|soft_wrap| soft_wrap.wrap_at_text_width)
- })
- .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;
- }
- }
- let config = self.config.load();
- let editor_soft_wrap = &config.soft_wrap;
- let language_soft_wrap = self
- .language
- .as_ref()
- .and_then(|config| config.soft_wrap.as_ref());
- let enable_soft_wrap = language_soft_wrap
- .and_then(|soft_wrap| soft_wrap.enable)
- .or(editor_soft_wrap.enable)
- .unwrap_or(false);
- let max_wrap = language_soft_wrap
- .and_then(|soft_wrap| soft_wrap.max_wrap)
- .or(config.soft_wrap.max_wrap)
- .unwrap_or(20);
- let max_indent_retain = language_soft_wrap
- .and_then(|soft_wrap| soft_wrap.max_indent_retain)
- .or(editor_soft_wrap.max_indent_retain)
- .unwrap_or(40);
- let wrap_indicator = language_soft_wrap
- .and_then(|soft_wrap| soft_wrap.wrap_indicator.clone())
- .or_else(|| config.soft_wrap.wrap_indicator.clone())
- .unwrap_or_else(|| "↪ ".into());
- let tab_width = self.tab_width() as u16;
- TextFormat {
- soft_wrap: enable_soft_wrap && viewport_width > 10,
- tab_width,
- max_wrap: max_wrap.min(viewport_width / 4),
- max_indent_retain: max_indent_retain.min(viewport_width * 2 / 5),
- // avoid spinning forever when the window manager
- // sets the size to something tiny
- 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,
- }
- }
-
- /// 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)
- }
-
- /// 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)]
-pub struct ViewData {
- view_position: ViewPosition,
-}
-
-#[derive(Clone, Debug)]
-pub enum FormatterError {
- SpawningFailed {
- command: String,
- error: std::io::ErrorKind,
- },
- BrokenStdin,
- WaitForOutputFailed,
- InvalidUtf8Output,
- NonZeroExitStatus(Option<String>),
}
-impl std::error::Error for FormatterError {}
-
-impl Display for FormatterError {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- match self {
- Self::SpawningFailed { command, error } => {
- write!(f, "Failed to spawn formatter {}: {:?}", command, error)
- }
- 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::NonZeroExitStatus(Some(output)) => write!(f, "Formatter error: {}", output),
- Self::NonZeroExitStatus(None) => {
- write!(f, "Formatter exited with non zero exit status")
- }
- }
+impl Default for Document {
+ fn default() -> Self {
+ let text = Rope::from(DEFAULT_LINE_ENDING.as_str());
+ Self::from(text, None)
}
}
#[cfg(test)]
mod test {
- use arc_swap::ArcSwap;
-
use super::*;
#[test]
fn changeset_to_changes_ignore_line_endings() {
use helix_lsp::{lsp, Client, OffsetEncoding};
let text = Rope::from("hello\r\nworld");
- let mut doc = Document::from(
- text,
- None,
- Arc::new(ArcSwap::new(Arc::new(Config::default()))),
- Arc::new(ArcSwap::from_pointee(syntax::Loader::default())),
- );
+ let mut doc = Document::from(text, None);
let view = ViewId::default();
doc.set_selection(view, Selection::single(0, 0));
@@ -2380,12 +1020,7 @@ mod test {
fn changeset_to_changes() {
use helix_lsp::{lsp, Client, OffsetEncoding};
let text = Rope::from("hello");
- let mut doc = Document::from(
- text,
- None,
- Arc::new(ArcSwap::new(Arc::new(Config::default()))),
- Arc::new(ArcSwap::from_pointee(syntax::Loader::default())),
- );
+ let mut doc = Document::from(text, None);
let view = ViewId::default();
doc.set_selection(view, Selection::single(5, 5));
@@ -2498,76 +1133,89 @@ 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(),
- helix_core::NATIVE_LINE_ENDING.as_str()
+ Document::default().text().to_string(),
+ DEFAULT_LINE_ENDING.as_str()
);
}
- macro_rules! decode {
+ macro_rules! test_decode {
+ ($label:expr, $label_override:expr) => {
+ let encoding = encoding::Encoding::for_label($label_override.as_bytes()).unwrap();
+ let base_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/encoding");
+ let path = base_path.join(format!("{}_in.txt", $label));
+ let ref_path = base_path.join(format!("{}_in_ref.txt", $label));
+ assert!(path.exists());
+ assert!(ref_path.exists());
+
+ let mut file = std::fs::File::open(path).unwrap();
+ let text = from_reader(&mut file, Some(encoding))
+ .unwrap()
+ .0
+ .to_string();
+ let expectation = std::fs::read_to_string(ref_path).unwrap();
+ assert_eq!(text[..], expectation[..]);
+ };
+ }
+
+ macro_rules! test_encode {
+ ($label:expr, $label_override:expr) => {
+ let encoding = encoding::Encoding::for_label($label_override.as_bytes()).unwrap();
+ let base_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/encoding");
+ let path = base_path.join(format!("{}_out.txt", $label));
+ let ref_path = base_path.join(format!("{}_out_ref.txt", $label));
+ assert!(path.exists());
+ assert!(ref_path.exists());
+
+ let text = Rope::from_str(&std::fs::read_to_string(path).unwrap());
+ let mut buf: Vec<u8> = Vec::new();
+ helix_lsp::block_on(to_writer(&mut buf, encoding, &text)).unwrap();
+
+ let expectation = std::fs::read(ref_path).unwrap();
+ assert_eq!(buf, expectation);
+ };
+ }
+
+ macro_rules! test_decode_fn {
($name:ident, $label:expr, $label_override:expr) => {
#[test]
fn $name() {
- let encoding = encoding::Encoding::for_label($label_override.as_bytes()).unwrap();
- let base_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/encoding");
- let path = base_path.join(format!("{}_in.txt", $label));
- let ref_path = base_path.join(format!("{}_in_ref.txt", $label));
- assert!(path.exists());
- assert!(ref_path.exists());
-
- let mut file = std::fs::File::open(path).unwrap();
- let text = from_reader(&mut file, Some(encoding.into()))
- .unwrap()
- .0
- .to_string();
- let expectation = std::fs::read_to_string(ref_path).unwrap();
- assert_eq!(text[..], expectation[..]);
+ test_decode!($label, $label_override);
}
};
($name:ident, $label:expr) => {
- decode!($name, $label, $label);
+ #[test]
+ fn $name() {
+ test_decode!($label, $label);
+ }
};
}
- macro_rules! encode {
+ macro_rules! test_encode_fn {
($name:ident, $label:expr, $label_override:expr) => {
#[test]
fn $name() {
- let encoding = encoding::Encoding::for_label($label_override.as_bytes()).unwrap();
- let base_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/encoding");
- let path = base_path.join(format!("{}_out.txt", $label));
- let ref_path = base_path.join(format!("{}_out_ref.txt", $label));
- assert!(path.exists());
- assert!(ref_path.exists());
-
- let text = Rope::from_str(&std::fs::read_to_string(path).unwrap());
- let mut buf: Vec<u8> = Vec::new();
- helix_lsp::block_on(to_writer(&mut buf, (encoding, false), &text)).unwrap();
-
- let expectation = std::fs::read(ref_path).unwrap();
- assert_eq!(buf, expectation);
+ test_encode!($label, $label_override);
}
};
($name:ident, $label:expr) => {
- encode!($name, $label, $label);
+ #[test]
+ fn $name() {
+ test_encode!($label, $label);
+ }
};
}
- decode!(big5_decode, "big5");
- encode!(big5_encode, "big5");
- decode!(euc_kr_decode, "euc_kr", "EUC-KR");
- encode!(euc_kr_encode, "euc_kr", "EUC-KR");
- decode!(gb18030_decode, "gb18030");
- encode!(gb18030_encode, "gb18030");
- decode!(iso_2022_jp_decode, "iso_2022_jp", "ISO-2022-JP");
- encode!(iso_2022_jp_encode, "iso_2022_jp", "ISO-2022-JP");
- decode!(jis0208_decode, "jis0208", "EUC-JP");
- encode!(jis0208_encode, "jis0208", "EUC-JP");
- decode!(jis0212_decode, "jis0212", "EUC-JP");
- decode!(shift_jis_decode, "shift_jis");
- encode!(shift_jis_encode, "shift_jis");
+ test_decode_fn!(test_big5_decode, "big5");
+ test_encode_fn!(test_big5_encode, "big5");
+ test_decode_fn!(test_euc_kr_decode, "euc_kr", "EUC-KR");
+ test_encode_fn!(test_euc_kr_encode, "euc_kr", "EUC-KR");
+ test_decode_fn!(test_gb18030_decode, "gb18030");
+ test_encode_fn!(test_gb18030_encode, "gb18030");
+ test_decode_fn!(test_iso_2022_jp_decode, "iso_2022_jp", "ISO-2022-JP");
+ test_encode_fn!(test_iso_2022_jp_encode, "iso_2022_jp", "ISO-2022-JP");
+ test_decode_fn!(test_jis0208_decode, "jis0208", "EUC-JP");
+ test_encode_fn!(test_jis0208_encode, "jis0208", "EUC-JP");
+ test_decode_fn!(test_jis0212_decode, "jis0212", "EUC-JP");
+ test_decode_fn!(test_shift_jis_decode, "shift_jis");
+ test_encode_fn!(test_shift_jis_encode, "shift_jis");
}