aboutsummaryrefslogtreecommitdiff
path: root/helix-view
diff options
context:
space:
mode:
Diffstat (limited to 'helix-view')
-rw-r--r--helix-view/Cargo.toml15
-rw-r--r--helix-view/src/document.rs246
-rw-r--r--helix-view/src/editor.rs165
-rw-r--r--helix-view/src/graphics.rs31
-rw-r--r--helix-view/src/gutter.rs27
-rw-r--r--helix-view/src/info.rs65
-rw-r--r--helix-view/src/input.rs197
-rw-r--r--helix-view/src/keyboard.rs5
-rw-r--r--helix-view/src/theme.rs136
-rw-r--r--helix-view/src/view.rs13
10 files changed, 617 insertions, 283 deletions
diff --git a/helix-view/Cargo.toml b/helix-view/Cargo.toml
index ffe6a111..932c3321 100644
--- a/helix-view/Cargo.toml
+++ b/helix-view/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "helix-view"
-version = "0.5.0"
+version = "0.6.0"
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
edition = "2021"
license = "MPL-2.0"
@@ -16,13 +16,13 @@ term = ["crossterm"]
[dependencies]
bitflags = "1.3"
anyhow = "1"
-helix-core = { version = "0.5", path = "../helix-core" }
-helix-lsp = { version = "0.5", path = "../helix-lsp"}
-helix-dap = { version = "0.5", path = "../helix-dap"}
-crossterm = { version = "0.22", optional = true }
+helix-core = { version = "0.6", path = "../helix-core" }
+helix-lsp = { version = "0.6", path = "../helix-lsp"}
+helix-dap = { version = "0.6", path = "../helix-dap"}
+crossterm = { version = "0.23", optional = true }
# Conversion traits
-once_cell = "1.8"
+once_cell = "1.9"
url = "2"
tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] }
@@ -31,7 +31,6 @@ futures-util = { version = "0.3", features = ["std", "async-await"], default-fea
slotmap = "1"
-encoding_rs = "0.8"
chardetng = "0.1"
serde = { version = "1.0", features = ["derive"] }
@@ -41,7 +40,7 @@ log = "~0.4"
which = "4.2"
[target.'cfg(windows)'.dependencies]
-clipboard-win = { version = "4.2", features = ["std"] }
+clipboard-win = { version = "4.4", features = ["std"] }
[dev-dependencies]
helix-tui = { path = "../helix-tui" }
diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs
index 76b19a07..c0186ee5 100644
--- a/helix-view/src/document.rs
+++ b/helix-view/src/document.rs
@@ -1,5 +1,6 @@
-use anyhow::{anyhow, Context, Error};
+use anyhow::{anyhow, bail, Context, Error};
use serde::de::{self, Deserialize, Deserializer};
+use serde::Serialize;
use std::cell::Cell;
use std::collections::HashMap;
use std::fmt::Display;
@@ -9,7 +10,8 @@ use std::str::FromStr;
use std::sync::Arc;
use helix_core::{
- history::History,
+ encoding,
+ history::{History, UndoKind},
indent::{auto_detect_indent_style, IndentStyle},
line_ending::auto_detect_line_ending,
syntax::{self, LanguageConfiguration},
@@ -18,7 +20,7 @@ use helix_core::{
};
use helix_lsp::util::LspFormatting;
-use crate::{DocumentId, Theme, ViewId};
+use crate::{DocumentId, ViewId};
/// 8kB of buffer space for encoding and decoding `Rope`s.
const BUF_SIZE: usize = 8192;
@@ -29,9 +31,9 @@ pub const SCRATCH_BUFFER_NAME: &str = "[scratch]";
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum Mode {
- Normal,
- Select,
- Insert,
+ Normal = 0,
+ Select = 1,
+ Insert = 2,
}
impl Display for Mode {
@@ -52,7 +54,7 @@ impl FromStr for Mode {
"normal" => Ok(Mode::Normal),
"select" => Ok(Mode::Select),
"insert" => Ok(Mode::Insert),
- _ => Err(anyhow!("Invalid mode '{}'", s)),
+ _ => bail!("Invalid mode '{}'", s),
}
}
}
@@ -68,13 +70,22 @@ impl<'de> Deserialize<'de> for Mode {
}
}
+impl Serialize for Mode {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: serde::Serializer,
+ {
+ serializer.collect_str(self)
+ }
+}
+
pub struct Document {
pub(crate) id: DocumentId,
text: Rope,
pub(crate) selections: HashMap<ViewId, Selection>,
path: Option<PathBuf>,
- encoding: &'static encoding_rs::Encoding,
+ encoding: &'static encoding::Encoding,
/// Current editing mode.
pub mode: Mode,
@@ -104,6 +115,7 @@ pub struct Document {
last_saved_revision: usize,
version: i32, // should be usize?
+ pub(crate) modified_since_accessed: bool,
diagnostics: Vec<Diagnostic>,
language_server: Option<Arc<helix_lsp::Client>>,
@@ -127,6 +139,7 @@ impl fmt::Debug for Document {
// .field("history", &self.history)
.field("last_saved_revision", &self.last_saved_revision)
.field("version", &self.version)
+ .field("modified_since_accessed", &self.modified_since_accessed)
.field("diagnostics", &self.diagnostics)
// .field("language_server", &self.language_server)
.finish()
@@ -141,8 +154,8 @@ impl fmt::Debug for Document {
/// be used to override encoding auto-detection.
pub fn from_reader<R: std::io::Read + ?Sized>(
reader: &mut R,
- encoding: Option<&'static encoding_rs::Encoding>,
-) -> Result<(Rope, &'static encoding_rs::Encoding), 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
@@ -210,11 +223,11 @@ pub fn from_reader<R: std::io::Read + ?Sized>(
total_read += read;
total_written += written;
match result {
- encoding_rs::CoderResult::InputEmpty => {
+ encoding::CoderResult::InputEmpty => {
debug_assert_eq!(slice.len(), total_read);
break;
}
- encoding_rs::CoderResult::OutputFull => {
+ encoding::CoderResult::OutputFull => {
debug_assert!(slice.len() > total_read);
builder.append(&buf_str[..total_written]);
total_written = 0;
@@ -249,7 +262,7 @@ pub fn from_reader<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: &'static encoding_rs::Encoding,
+ encoding: &'static encoding::Encoding,
rope: &'a Rope,
) -> Result<(), Error> {
// Text inside a `Rope` is stored as non-contiguous blocks of data called
@@ -284,12 +297,12 @@ pub async fn to_writer<'a, W: tokio::io::AsyncWriteExt + Unpin + ?Sized>(
total_read += read;
total_written += written;
match result {
- encoding_rs::CoderResult::InputEmpty => {
+ encoding::CoderResult::InputEmpty => {
debug_assert_eq!(chunk.len(), total_read);
debug_assert!(buf.len() >= total_written);
break;
}
- encoding_rs::CoderResult::OutputFull => {
+ encoding::CoderResult::OutputFull => {
debug_assert!(chunk.len() > total_read);
writer.write_all(&buf[..total_written]).await?;
total_written = 0;
@@ -320,8 +333,8 @@ use helix_lsp::lsp;
use url::Url;
impl Document {
- pub fn from(text: Rope, encoding: Option<&'static encoding_rs::Encoding>) -> Self {
- let encoding = encoding.unwrap_or(encoding_rs::UTF_8);
+ 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;
@@ -344,6 +357,7 @@ impl Document {
history: Cell::new(History::default()),
savepoint: None,
last_saved_revision: 0,
+ modified_since_accessed: false,
language_server: None,
}
}
@@ -353,9 +367,8 @@ impl Document {
/// overwritten with the `encoding` parameter.
pub fn open(
path: &Path,
- encoding: Option<&'static encoding_rs::Encoding>,
- theme: Option<&Theme>,
- config_loader: Option<&syntax::Loader>,
+ 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) = if path.exists() {
@@ -363,7 +376,7 @@ impl Document {
std::fs::File::open(path).context(format!("unable to open {:?}", path))?;
from_reader(&mut file, encoding)?
} else {
- let encoding = encoding.unwrap_or(encoding_rs::UTF_8);
+ let encoding = encoding.unwrap_or(encoding::UTF_8);
(Rope::from(DEFAULT_LINE_ENDING.as_str()), encoding)
};
@@ -372,7 +385,7 @@ impl Document {
// set the path and try detecting the language
doc.set_path(Some(path))?;
if let Some(loader) = config_loader {
- doc.detect_language(theme, loader);
+ doc.detect_language(loader);
}
doc.detect_indent_and_line_ending();
@@ -383,7 +396,7 @@ impl Document {
/// The same as [`format`], but only returns formatting changes if auto-formatting
/// is configured.
pub fn auto_format(&self) -> Option<impl Future<Output = LspFormatting> + 'static> {
- if self.language_config().map(|c| c.auto_format) == Some(true) {
+ if self.language_config()?.auto_format {
self.format()
} else {
None
@@ -393,30 +406,27 @@ impl Document {
/// If supported, returns the changes that should be applied to this document in order
/// to format it nicely.
pub fn format(&self) -> Option<impl Future<Output = LspFormatting> + 'static> {
- if let Some(language_server) = self.language_server() {
- let text = self.text.clone();
- let offset_encoding = language_server.offset_encoding();
- let request = language_server.text_document_formatting(
- self.identifier(),
- lsp::FormattingOptions::default(),
- None,
- )?;
-
- let fut = async move {
- let edits = request.await.unwrap_or_else(|e| {
- log::warn!("LSP formatting failed: {}", e);
- Default::default()
- });
- LspFormatting {
- doc: text,
- edits,
- offset_encoding,
- }
- };
- Some(fut)
- } else {
- None
- }
+ let language_server = self.language_server()?;
+ let text = self.text.clone();
+ let offset_encoding = language_server.offset_encoding();
+ let request = language_server.text_document_formatting(
+ self.identifier(),
+ lsp::FormattingOptions::default(),
+ None,
+ )?;
+
+ let fut = async move {
+ let edits = request.await.unwrap_or_else(|e| {
+ log::warn!("LSP formatting failed: {}", e);
+ Default::default()
+ });
+ LspFormatting {
+ doc: text,
+ edits,
+ offset_encoding,
+ }
+ };
+ Some(fut)
}
pub fn save(&mut self) -> impl Future<Output = Result<(), anyhow::Error>> {
@@ -460,9 +470,7 @@ impl Document {
if let Some(parent) = path.parent() {
// TODO: display a prompt asking the user if the directories should be created
if !parent.exists() {
- return Err(Error::msg(
- "can't save file, parent directory does not exist",
- ));
+ bail!("can't save file, parent directory does not exist");
}
}
@@ -494,12 +502,12 @@ impl Document {
}
/// Detect the programming language based on the file type.
- pub fn detect_language(&mut self, theme: Option<&Theme>, config_loader: &syntax::Loader) {
+ 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(theme, language_config);
+ self.set_language(language_config, Some(config_loader));
}
}
@@ -509,8 +517,7 @@ impl Document {
/// line ending.
pub fn detect_indent_and_line_ending(&mut self) {
self.indent_style = auto_detect_indent_style(&self.text).unwrap_or_else(|| {
- self.language
- .as_ref()
+ self.language_config()
.and_then(|config| config.indent.as_ref())
.map_or(DEFAULT_INDENT, |config| IndentStyle::from_str(&config.unit))
});
@@ -524,7 +531,7 @@ impl Document {
// If there is no path or the path no longer exists.
if path.is_none() {
- return Err(anyhow!("can't find file to reload from"));
+ bail!("can't find file to reload from");
}
let mut file = std::fs::File::open(path.unwrap())?;
@@ -545,15 +552,13 @@ impl Document {
/// Sets the [`Document`]'s encoding with the encoding correspondent to `label`.
pub fn set_encoding(&mut self, label: &str) -> Result<(), Error> {
- match encoding_rs::Encoding::for_label(label.as_bytes()) {
- Some(encoding) => self.encoding = encoding,
- None => return Err(anyhow::anyhow!("unknown 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_rs::Encoding {
+ pub fn encoding(&self) -> &'static encoding::Encoding {
self.encoding
}
@@ -573,15 +578,13 @@ impl Document {
/// if it exists.
pub fn set_language(
&mut self,
- theme: Option<&Theme>,
language_config: Option<Arc<helix_core::syntax::LanguageConfiguration>>,
+ loader: Option<Arc<helix_core::syntax::Loader>>,
) {
- if let Some(language_config) = language_config {
- let scopes = theme.map(|theme| theme.scopes()).unwrap_or(&[]);
- if let Some(highlight_config) = language_config.highlight_config(scopes) {
- let syntax = Syntax::new(&self.text, highlight_config);
+ 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);
- // TODO: config.configure(scopes) is now delayed, is that ok?
}
self.language = Some(language_config);
@@ -593,15 +596,10 @@ impl Document {
/// 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,
- theme: Option<&Theme>,
- config_loader: Arc<syntax::Loader>,
- ) {
+ 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(theme, language_config);
+ self.set_language(language_config, Some(config_loader));
}
/// Set the LSP.
@@ -639,6 +637,8 @@ impl Document {
selection.clone().ensure_invariants(self.text.slice(..)),
);
}
+
+ self.modified_since_accessed = true;
}
if !transaction.changes().is_empty() {
@@ -680,7 +680,7 @@ impl Document {
if let Some(notify) = notify {
tokio::spawn(notify);
- } //.expect("failed to emit textDocument/didChange");
+ }
}
}
success
@@ -708,11 +708,11 @@ impl Document {
success
}
- /// Undo the last modification to the [`Document`]. Returns whether the undo was successful.
- pub fn undo(&mut self, view_id: ViewId) -> bool {
+ fn undo_redo_impl(&mut self, view_id: ViewId, undo: bool) -> bool {
let mut history = self.history.take();
- let success = if let Some(transaction) = history.undo() {
- self.apply_impl(transaction, view_id)
+ let txn = if undo { history.undo() } else { history.redo() };
+ let success = if let Some(txn) = txn {
+ self.apply_impl(txn, view_id)
} else {
false
};
@@ -725,21 +725,14 @@ impl Document {
success
}
+ /// Undo the last modification to the [`Document`]. Returns whether the undo was successful.
+ 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 sucessful.
pub fn redo(&mut self, view_id: ViewId) -> bool {
- let mut history = self.history.take();
- let success = if let Some(transaction) = history.redo() {
- self.apply_impl(transaction, view_id)
- } else {
- false
- };
- self.history.set(history);
-
- if success {
- // reset changeset to fix len
- self.changes = ChangeSet::new(self.text());
- }
- success
+ self.undo_redo_impl(view_id, false)
}
pub fn savepoint(&mut self) {
@@ -752,9 +745,12 @@ impl Document {
}
}
- /// Undo modifications to the [`Document`] according to `uk`.
- pub fn earlier(&mut self, view_id: ViewId, uk: helix_core::history::UndoKind) -> bool {
- let txns = self.history.get_mut().earlier(uk);
+ 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 {
+ self.history.get_mut().later(uk)
+ };
let mut success = false;
for txn in txns {
if self.apply_impl(&txn, view_id) {
@@ -768,20 +764,14 @@ impl Document {
success
}
+ /// Undo modifications to the [`Document`] according to `uk`.
+ 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_id: ViewId, uk: helix_core::history::UndoKind) -> bool {
- let txns = self.history.get_mut().later(uk);
- let mut success = false;
- for txn in txns {
- if self.apply_impl(&txn, view_id) {
- success = true;
- }
- }
- if success {
- // reset changeset to fix len
- self.changes = ChangeSet::new(self.text());
- }
- success
+ pub fn later(&mut self, view_id: ViewId, uk: UndoKind) -> bool {
+ self.earlier_later_impl(view_id, uk, false)
}
/// Commit pending changes to history
@@ -837,6 +827,16 @@ impl Document {
.map(|language| language.scope.as_str())
}
+ /// 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()
+ .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`].
pub fn language_config(&self) -> Option<&LanguageConfiguration> {
self.language.as_deref()
@@ -847,18 +847,10 @@ impl Document {
self.version
}
+ /// Language server if it has been initialized.
pub fn language_server(&self) -> Option<&helix_lsp::Client> {
- let server = self.language_server.as_deref();
- let initialized = server
- .map(|server| server.is_initialized())
- .unwrap_or(false);
-
- // only resolve language_server if it's initialized
- if initialized {
- server
- } else {
- None
- }
+ let server = self.language_server.as_deref()?;
+ server.is_initialized().then(|| server)
}
#[inline]
@@ -869,8 +861,7 @@ impl Document {
/// Tab size in columns.
pub fn tab_width(&self) -> usize {
- self.language
- .as_ref()
+ self.language_config()
.and_then(|config| config.indent.as_ref())
.map_or(4, |config| config.tab_width) // fallback to 4 columns
}
@@ -883,6 +874,10 @@ impl Document {
self.indent_style.as_str()
}
+ pub fn changes(&self) -> &ChangeSet {
+ &self.changes
+ }
+
#[inline]
/// File path on disk.
pub fn path(&self) -> Option<&PathBuf> {
@@ -891,7 +886,7 @@ impl Document {
/// File path as a URL.
pub fn url(&self) -> Option<Url> {
- self.path().map(|path| Url::from_file_path(path).unwrap())
+ Url::from_file_path(self.path()?).ok()
}
#[inline]
@@ -914,10 +909,6 @@ impl Document {
.map(helix_core::path::get_relative_path)
}
- // pub fn slice<R>(&self, range: R) -> RopeSlice where R: RangeBounds {
- // self.state.doc.slice
- // }
-
// transact(Fn) ?
// -- LSP methods
@@ -938,7 +929,6 @@ impl Document {
pub fn set_diagnostics(&mut self, diagnostics: Vec<Diagnostic>) {
self.diagnostics = diagnostics;
- // sort by range
self.diagnostics
.sort_unstable_by_key(|diagnostic| diagnostic.range);
}
@@ -1113,7 +1103,7 @@ mod test {
macro_rules! test_decode {
($label:expr, $label_override:expr) => {
- let encoding = encoding_rs::Encoding::for_label($label_override.as_bytes()).unwrap();
+ 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));
@@ -1132,7 +1122,7 @@ mod test {
macro_rules! test_encode {
($label:expr, $label_override:expr) => {
- let encoding = encoding_rs::Encoding::for_label($label_override.as_bytes()).unwrap();
+ 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));
diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs
index c7b3baef..2e6121bc 100644
--- a/helix-view/src/editor.rs
+++ b/helix-view/src/editor.rs
@@ -1,7 +1,9 @@
use crate::{
clipboard::{get_clipboard_provider, ClipboardProvider},
- document::SCRATCH_BUFFER_NAME,
+ document::{Mode, SCRATCH_BUFFER_NAME},
graphics::{CursorKind, Rect},
+ info::Info,
+ input::KeyEvent,
theme::{self, Theme},
tree::{self, Tree},
Document, DocumentId, View, ViewId,
@@ -22,7 +24,7 @@ use std::{
use tokio::time::{sleep, Duration, Instant, Sleep};
-use anyhow::{bail, Context, Error};
+use anyhow::{bail, Error};
pub use helix_core::diagnostic::Severity;
pub use helix_core::register::Registers;
@@ -30,7 +32,7 @@ use helix_core::syntax;
use helix_core::{Position, Selection};
use helix_dap as dap;
-use serde::Deserialize;
+use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize};
fn deserialize_duration_millis<'de, D>(deserializer: D) -> Result<Duration, D::Error>
where
@@ -40,7 +42,7 @@ where
Ok(Duration::from_millis(millis))
}
-#[derive(Debug, Clone, PartialEq, Deserialize)]
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
pub struct FilePickerConfig {
/// IgnoreOptions
@@ -80,7 +82,7 @@ impl Default for FilePickerConfig {
}
}
-#[derive(Debug, Clone, PartialEq, Deserialize)]
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
pub struct Config {
/// Padding to keep between the edge of the screen and the cursor when scrolling. Defaults to 5.
@@ -95,8 +97,6 @@ pub struct Config {
pub line_number: LineNumber,
/// Middle click paste support. Defaults to true.
pub middle_click_paste: bool,
- /// Smart case: Case insensitive searching unless pattern contains upper case characters. Defaults to true.
- pub smart_case: bool,
/// Automatic insertion of pairs to parentheses, brackets, etc. Defaults to true.
pub auto_pairs: bool,
/// Automatic auto-completion, automatically pop up without user trigger. Defaults to true.
@@ -108,18 +108,101 @@ pub struct Config {
/// Whether to display infoboxes. Defaults to true.
pub auto_info: bool,
pub file_picker: FilePickerConfig,
+ /// Shape for cursor in each mode
+ pub cursor_shape: CursorShapeConfig,
+ /// Set to `true` to override automatic detection of terminal truecolor support in the event of a false negative. Defaults to `false`.
+ pub true_color: bool,
+ /// Search configuration.
+ #[serde(default)]
+ pub search: SearchConfig,
+}
+
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
+pub struct SearchConfig {
+ /// Smart case: Case insensitive searching unless pattern contains upper case characters. Defaults to true.
+ pub smart_case: bool,
+ /// Whether the search should wrap after depleting the matches. Default to true.
+ pub wrap_around: bool,
+}
+
+// Cursor shape is read and used on every rendered frame and so needs
+// to be fast. Therefore we avoid a hashmap and use an enum indexed array.
+#[derive(Debug, Clone, PartialEq)]
+pub struct CursorShapeConfig([CursorKind; 3]);
+
+impl CursorShapeConfig {
+ pub fn from_mode(&self, mode: Mode) -> CursorKind {
+ self.get(mode as usize).copied().unwrap_or_default()
+ }
}
-#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize)]
+impl<'de> Deserialize<'de> for CursorShapeConfig {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ let m = HashMap::<Mode, CursorKind>::deserialize(deserializer)?;
+ let into_cursor = |mode: Mode| m.get(&mode).copied().unwrap_or_default();
+ Ok(CursorShapeConfig([
+ into_cursor(Mode::Normal),
+ into_cursor(Mode::Select),
+ into_cursor(Mode::Insert),
+ ]))
+ }
+}
+
+impl Serialize for CursorShapeConfig {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: serde::Serializer,
+ {
+ let mut map = serializer.serialize_map(Some(self.len()))?;
+ let modes = [Mode::Normal, Mode::Select, Mode::Insert];
+ for mode in modes {
+ map.serialize_entry(&mode, &self.from_mode(mode))?;
+ }
+ map.end()
+ }
+}
+
+impl std::ops::Deref for CursorShapeConfig {
+ type Target = [CursorKind; 3];
+
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+
+impl Default for CursorShapeConfig {
+ fn default() -> Self {
+ Self([CursorKind::Block; 3])
+ }
+}
+
+#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum LineNumber {
/// Show absolute line number
Absolute,
- /// Show relative line number to the primary cursor
+ /// If focused and in normal/select mode, show relative line number to the primary cursor.
+ /// If unfocused or in insert mode, show absolute line number.
Relative,
}
+impl std::str::FromStr for LineNumber {
+ type Err = anyhow::Error;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ match s.to_lowercase().as_str() {
+ "absolute" | "abs" => Ok(Self::Absolute),
+ "relative" | "rel" => Ok(Self::Relative),
+ _ => anyhow::bail!("Line number can only be `absolute` or `relative`."),
+ }
+ }
+}
+
impl Default for Config {
fn default() -> Self {
Self {
@@ -133,13 +216,24 @@ impl Default for Config {
},
line_number: LineNumber::Absolute,
middle_click_paste: true,
- smart_case: true,
auto_pairs: true,
auto_completion: true,
idle_timeout: Duration::from_millis(400),
completion_trigger_len: 2,
auto_info: true,
file_picker: FilePickerConfig::default(),
+ cursor_shape: CursorShapeConfig::default(),
+ true_color: false,
+ search: SearchConfig::default(),
+ }
+ }
+}
+
+impl Default for SearchConfig {
+ fn default() -> Self {
+ Self {
+ wrap_around: true,
+ smart_case: true,
}
}
}
@@ -177,6 +271,7 @@ pub struct Editor {
pub count: Option<std::num::NonZeroUsize>,
pub selected_register: Option<char>,
pub registers: Registers,
+ pub macro_recording: Option<(char, Vec<KeyEvent>)>,
pub theme: Theme,
pub language_servers: helix_lsp::Registry,
@@ -190,6 +285,7 @@ pub struct Editor {
pub theme_loader: Arc<theme::Loader>,
pub status_msg: Option<(String, Severity)>,
+ pub autoinfo: Option<Info>,
pub config: Config,
@@ -225,6 +321,7 @@ impl Editor {
documents: BTreeMap::new(),
count: None,
selected_register: None,
+ macro_recording: None,
theme: theme_loader.default(),
language_servers,
debugger: None,
@@ -235,6 +332,7 @@ impl Editor {
registers: Registers::default(),
clipboard_provider: get_clipboard_provider(),
status_msg: None,
+ autoinfo: None,
idle_timer: Box::pin(sleep(config.idle_timeout)),
last_motion: None,
config,
@@ -275,31 +373,16 @@ impl Editor {
}
let scopes = theme.scopes();
- for config in self
- .syn_loader
- .language_configs_iter()
- .filter(|cfg| cfg.is_highlight_initialized())
- {
- config.reconfigure(scopes);
- }
+ self.syn_loader.set_scopes(scopes.to_vec());
self.theme = theme;
self._refresh();
}
- pub fn set_theme_from_name(&mut self, theme: &str) -> anyhow::Result<()> {
- let theme = self
- .theme_loader
- .load(theme.as_ref())
- .with_context(|| format!("failed setting theme `{}`", theme))?;
- self.set_theme(theme);
- Ok(())
- }
-
/// Refreshes the language server for a given document
pub fn refresh_language_server(&mut self, doc_id: DocumentId) -> Option<()> {
let doc = self.documents.get_mut(&doc_id)?;
- doc.detect_language(Some(&self.theme), &self.syn_loader);
+ doc.detect_language(self.syn_loader.clone());
Self::launch_language_server(&mut self.language_servers, doc)
}
@@ -323,11 +406,8 @@ impl Editor {
if let Some(language_server) = doc.language_server() {
tokio::spawn(language_server.text_document_did_close(doc.identifier()));
}
- let language_id = doc
- .language()
- .and_then(|s| s.split('.').last()) // source.rust
- .map(ToOwned::to_owned)
- .unwrap_or_default();
+
+ let language_id = doc.language_id().map(ToOwned::to_owned).unwrap_or_default();
// TODO: this now races with on_init code if the init happens too quickly
tokio::spawn(language_server.text_document_did_open(
@@ -394,7 +474,8 @@ impl Editor {
.tree
.traverse()
.any(|(_, v)| v.doc == doc.id && v.id != view.id);
- let view = view_mut!(self);
+
+ let (view, doc) = current!(self);
if remove_empty_scratch {
// Copy `doc.id` into a variable before calling `self.documents.remove`, which requires a mutable
// borrow, invalidating direct access to `doc.id`.
@@ -403,7 +484,16 @@ impl Editor {
} else {
let jump = (view.doc, doc.selection(view.id).clone());
view.jumps.push(jump);
- view.last_accessed_doc = Some(view.doc);
+ // Set last accessed doc if it is a different document
+ if doc.id != id {
+ view.last_accessed_doc = Some(view.doc);
+ // Set last modified doc if modified and last modified doc is different
+ if std::mem::take(&mut doc.modified_since_accessed)
+ && view.last_modified_docs[0] != Some(view.doc)
+ {
+ view.last_modified_docs = [Some(view.doc), view.last_modified_docs[0]];
+ }
+ }
}
let view_id = view.id;
@@ -471,7 +561,7 @@ impl Editor {
let id = if let Some(id) = id {
id
} else {
- let mut doc = Document::open(&path, None, Some(&self.theme), Some(&self.syn_loader))?;
+ let mut doc = Document::open(&path, None, Some(self.syn_loader.clone()))?;
let _ = Self::launch_language_server(&mut self.language_servers, &mut doc);
@@ -629,9 +719,10 @@ impl Editor {
let inner = view.inner_area();
pos.col += inner.x as usize;
pos.row += inner.y as usize;
- (Some(pos), CursorKind::Hidden)
+ let cursorkind = self.config.cursor_shape.from_mode(doc.mode());
+ (Some(pos), cursorkind)
} else {
- (None, CursorKind::Hidden)
+ (None, CursorKind::default())
}
}
diff --git a/helix-view/src/graphics.rs b/helix-view/src/graphics.rs
index 0bfca04a..6d0a9292 100644
--- a/helix-view/src/graphics.rs
+++ b/helix-view/src/graphics.rs
@@ -1,10 +1,12 @@
use bitflags::bitflags;
+use serde::{Deserialize, Serialize};
use std::{
cmp::{max, min},
str::FromStr,
};
-#[derive(Debug, Clone, Copy, PartialEq)]
+#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize)]
+#[serde(rename_all = "lowercase")]
/// UNSTABLE
pub enum CursorKind {
/// █
@@ -17,6 +19,12 @@ pub enum CursorKind {
Hidden,
}
+impl Default for CursorKind {
+ fn default() -> Self {
+ Self::Block
+ }
+}
+
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Margin {
pub vertical: u16,
@@ -25,7 +33,7 @@ pub struct Margin {
/// A simple rectangle used in the computation of the layout and to give widgets an hint about the
/// area they are supposed to render to. (x, y) = (0, 0) is at the top left corner of the screen.
-#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
+#[derive(Debug, Default, Clone, Copy, Hash, PartialEq, Eq)]
pub struct Rect {
pub x: u16,
pub y: u16,
@@ -33,17 +41,6 @@ pub struct Rect {
pub height: u16,
}
-impl Default for Rect {
- fn default() -> Rect {
- Rect {
- x: 0,
- y: 0,
- width: 0,
- height: 0,
- }
- }
-}
-
impl Rect {
/// Creates a new rect, with width and height limited to keep the area under max u16.
/// If clipped, aspect ratio will be preserved.
@@ -334,7 +331,7 @@ impl FromStr for Modifier {
/// ];
/// let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
/// for style in &styles {
-/// buffer.get_mut(0, 0).set_style(*style);
+/// buffer[(0, 0)].set_style(*style);
/// }
/// assert_eq!(
/// Style {
@@ -343,7 +340,7 @@ impl FromStr for Modifier {
/// add_modifier: Modifier::BOLD,
/// sub_modifier: Modifier::empty(),
/// },
-/// buffer.get(0, 0).style(),
+/// buffer[(0, 0)].style(),
/// );
/// ```
///
@@ -359,7 +356,7 @@ impl FromStr for Modifier {
/// ];
/// let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
/// for style in &styles {
-/// buffer.get_mut(0, 0).set_style(*style);
+/// buffer[(0, 0)].set_style(*style);
/// }
/// assert_eq!(
/// Style {
@@ -368,7 +365,7 @@ impl FromStr for Modifier {
/// add_modifier: Modifier::empty(),
/// sub_modifier: Modifier::empty(),
/// },
-/// buffer.get(0, 0).style(),
+/// buffer[(0, 0)].style(),
/// );
/// ```
#[derive(Debug, Clone, Copy, PartialEq)]
diff --git a/helix-view/src/gutter.rs b/helix-view/src/gutter.rs
index e156b9e5..6a77c41f 100644
--- a/helix-view/src/gutter.rs
+++ b/helix-view/src/gutter.rs
@@ -25,7 +25,8 @@ pub fn diagnostic<'doc>(
Box::new(move |line: usize, _selected: bool, out: &mut String| {
use helix_core::diagnostic::Severity;
- if let Some(diagnostic) = diagnostics.iter().find(|d| d.line == line) {
+ if let Ok(index) = diagnostics.binary_search_by_key(&line, |d| d.line) {
+ let diagnostic = &diagnostics[index];
write!(out, "●").unwrap();
return Some(match diagnostic.severity {
Some(Severity::Error) => error,
@@ -60,29 +61,31 @@ pub fn line_number<'doc>(
.char_to_line(doc.selection(view.id).primary().cursor(text));
let config = editor.config.line_number;
+ let mode = doc.mode;
Box::new(move |line: usize, selected: bool, out: &mut String| {
if line == last_line && !draw_last {
write!(out, "{:>1$}", '~', width).unwrap();
Some(linenr)
} else {
- use crate::editor::LineNumber;
- let line = match config {
- LineNumber::Absolute => line + 1,
- LineNumber::Relative => {
- if current_line == line {
- line + 1
- } else {
- abs_diff(current_line, line)
- }
- }
+ use crate::{document::Mode, editor::LineNumber};
+
+ let relative = config == LineNumber::Relative
+ && mode != Mode::Insert
+ && is_focused
+ && current_line != line;
+
+ let display_num = if relative {
+ abs_diff(current_line, line)
+ } else {
+ line + 1
};
let style = if selected && is_focused {
linenr_select
} else {
linenr
};
- write!(out, "{:>1$}", line, width).unwrap();
+ write!(out, "{:>1$}", display_num, width).unwrap();
Some(style)
}
})
diff --git a/helix-view/src/info.rs b/helix-view/src/info.rs
index b5a002fa..5ad6a60c 100644
--- a/helix-view/src/info.rs
+++ b/helix-view/src/info.rs
@@ -1,5 +1,5 @@
use crate::input::KeyEvent;
-use helix_core::unicode::width::UnicodeWidthStr;
+use helix_core::{register::Registers, unicode::width::UnicodeWidthStr};
use std::{collections::BTreeSet, fmt::Write};
#[derive(Debug)]
@@ -16,33 +16,60 @@ pub struct Info {
}
impl Info {
- pub fn new(title: &str, body: Vec<(&str, BTreeSet<KeyEvent>)>) -> Info {
- let body = body
- .into_iter()
- .map(|(desc, events)| {
- let events = events.iter().map(ToString::to_string).collect::<Vec<_>>();
- (desc, events.join(", "))
- })
- .collect::<Vec<_>>();
+ pub fn new(title: &str, body: Vec<(String, String)>) -> Self {
+ if body.is_empty() {
+ return Self {
+ title: title.to_string(),
+ height: 1,
+ width: title.len() as u16,
+ text: "".to_string(),
+ };
+ }
- let keymaps_width = body.iter().map(|r| r.1.len()).max().unwrap();
+ let item_width = body.iter().map(|(item, _)| item.width()).max().unwrap();
let mut text = String::new();
- for (desc, keyevents) in &body {
- let _ = writeln!(
- text,
- "{:width$} {}",
- keyevents,
- desc,
- width = keymaps_width
- );
+ for (item, desc) in &body {
+ let _ = writeln!(text, "{:width$} {}", item, desc, width = item_width);
}
- Info {
+ Self {
title: title.to_string(),
width: text.lines().map(|l| l.width()).max().unwrap() as u16,
height: body.len() as u16,
text,
}
}
+
+ pub fn from_keymap(title: &str, body: Vec<(&str, BTreeSet<KeyEvent>)>) -> Self {
+ let body = body
+ .into_iter()
+ .map(|(desc, events)| {
+ let events = events.iter().map(ToString::to_string).collect::<Vec<_>>();
+ (events.join(", "), desc.to_string())
+ })
+ .collect();
+
+ Self::new(title, body)
+ }
+
+ pub fn from_registers(registers: &Registers) -> Self {
+ let body = registers
+ .inner()
+ .iter()
+ .map(|(ch, reg)| {
+ let content = reg
+ .read()
+ .get(0)
+ .and_then(|s| s.lines().next())
+ .map(String::from)
+ .unwrap_or_default();
+ (ch.to_string(), content)
+ })
+ .collect();
+
+ let mut infobox = Self::new("Registers", body);
+ infobox.width = 30; // copied content could be very long
+ infobox
+ }
}
diff --git a/helix-view/src/input.rs b/helix-view/src/input.rs
index 580204cc..14dadc3b 100644
--- a/helix-view/src/input.rs
+++ b/helix-view/src/input.rs
@@ -36,7 +36,6 @@ pub(crate) mod keys {
pub(crate) const PAGEUP: &str = "pageup";
pub(crate) const PAGEDOWN: &str = "pagedown";
pub(crate) const TAB: &str = "tab";
- pub(crate) const BACKTAB: &str = "backtab";
pub(crate) const DELETE: &str = "del";
pub(crate) const INSERT: &str = "ins";
pub(crate) const NULL: &str = "null";
@@ -82,7 +81,6 @@ impl fmt::Display for KeyEvent {
KeyCode::PageUp => f.write_str(keys::PAGEUP)?,
KeyCode::PageDown => f.write_str(keys::PAGEDOWN)?,
KeyCode::Tab => f.write_str(keys::TAB)?,
- KeyCode::BackTab => f.write_str(keys::BACKTAB)?,
KeyCode::Delete => f.write_str(keys::DELETE)?,
KeyCode::Insert => f.write_str(keys::INSERT)?,
KeyCode::Null => f.write_str(keys::NULL)?,
@@ -116,7 +114,6 @@ impl UnicodeWidthStr for KeyEvent {
KeyCode::PageUp => keys::PAGEUP.len(),
KeyCode::PageDown => keys::PAGEDOWN.len(),
KeyCode::Tab => keys::TAB.len(),
- KeyCode::BackTab => keys::BACKTAB.len(),
KeyCode::Delete => keys::DELETE.len(),
KeyCode::Insert => keys::INSERT.len(),
KeyCode::Null => keys::NULL.len(),
@@ -166,7 +163,6 @@ impl std::str::FromStr for KeyEvent {
keys::PAGEUP => KeyCode::PageUp,
keys::PAGEDOWN => KeyCode::PageDown,
keys::TAB => KeyCode::Tab,
- keys::BACKTAB => KeyCode::BackTab,
keys::DELETE => KeyCode::Delete,
keys::INSERT => KeyCode::Insert,
keys::NULL => KeyCode::Null,
@@ -220,14 +216,79 @@ impl<'de> Deserialize<'de> for KeyEvent {
#[cfg(feature = "term")]
impl From<crossterm::event::KeyEvent> for KeyEvent {
- fn from(
- crossterm::event::KeyEvent { code, modifiers }: crossterm::event::KeyEvent,
- ) -> KeyEvent {
- KeyEvent {
- code: code.into(),
- modifiers: modifiers.into(),
+ fn from(crossterm::event::KeyEvent { code, modifiers }: crossterm::event::KeyEvent) -> Self {
+ if code == crossterm::event::KeyCode::BackTab {
+ // special case for BackTab -> Shift-Tab
+ let mut modifiers: KeyModifiers = modifiers.into();
+ modifiers.insert(KeyModifiers::SHIFT);
+ Self {
+ code: KeyCode::Tab,
+ modifiers,
+ }
+ } else {
+ Self {
+ code: code.into(),
+ modifiers: modifiers.into(),
+ }
+ }
+ }
+}
+
+#[cfg(feature = "term")]
+impl From<KeyEvent> for crossterm::event::KeyEvent {
+ fn from(KeyEvent { code, modifiers }: KeyEvent) -> Self {
+ if code == KeyCode::Tab && modifiers.contains(KeyModifiers::SHIFT) {
+ // special case for Shift-Tab -> BackTab
+ let mut modifiers = modifiers;
+ modifiers.remove(KeyModifiers::SHIFT);
+ crossterm::event::KeyEvent {
+ code: crossterm::event::KeyCode::BackTab,
+ modifiers: modifiers.into(),
+ }
+ } else {
+ crossterm::event::KeyEvent {
+ code: code.into(),
+ modifiers: modifiers.into(),
+ }
+ }
+ }
+}
+
+pub fn parse_macro(keys_str: &str) -> anyhow::Result<Vec<KeyEvent>> {
+ use anyhow::Context;
+ let mut keys_res: anyhow::Result<_> = Ok(Vec::new());
+ let mut i = 0;
+ while let Ok(keys) = &mut keys_res {
+ if i >= keys_str.len() {
+ break;
+ }
+ if !keys_str.is_char_boundary(i) {
+ i += 1;
+ continue;
+ }
+
+ let s = &keys_str[i..];
+ let mut end_i = 1;
+ while !s.is_char_boundary(end_i) {
+ end_i += 1;
+ }
+ let c = &s[..end_i];
+ if c == ">" {
+ keys_res = Err(anyhow!("Unmatched '>'"));
+ } else if c != "<" {
+ keys.push(c);
+ i += end_i;
+ } else {
+ match s.find('>').context("'>' expected") {
+ Ok(end_i) => {
+ keys.push(&s[1..end_i]);
+ i += end_i + 1;
+ }
+ Err(err) => keys_res = Err(err),
+ }
}
}
+ keys_res.and_then(|keys| keys.into_iter().map(str::parse).collect())
}
#[cfg(test)]
@@ -315,4 +376,120 @@ mod test {
assert!(str::parse::<KeyEvent>("123").is_err());
assert!(str::parse::<KeyEvent>("S--").is_err());
}
+
+ #[test]
+ fn parsing_valid_macros() {
+ assert_eq!(
+ parse_macro("xdo").ok(),
+ Some(vec![
+ KeyEvent {
+ code: KeyCode::Char('x'),
+ modifiers: KeyModifiers::NONE,
+ },
+ KeyEvent {
+ code: KeyCode::Char('d'),
+ modifiers: KeyModifiers::NONE,
+ },
+ KeyEvent {
+ code: KeyCode::Char('o'),
+ modifiers: KeyModifiers::NONE,
+ },
+ ]),
+ );
+
+ assert_eq!(
+ parse_macro("<C-w>v<C-w>h<C-o>xx<A-s>").ok(),
+ Some(vec![
+ KeyEvent {
+ code: KeyCode::Char('w'),
+ modifiers: KeyModifiers::CONTROL,
+ },
+ KeyEvent {
+ code: KeyCode::Char('v'),
+ modifiers: KeyModifiers::NONE,
+ },
+ KeyEvent {
+ code: KeyCode::Char('w'),
+ modifiers: KeyModifiers::CONTROL,
+ },
+ KeyEvent {
+ code: KeyCode::Char('h'),
+ modifiers: KeyModifiers::NONE,
+ },
+ KeyEvent {
+ code: KeyCode::Char('o'),
+ modifiers: KeyModifiers::CONTROL,
+ },
+ KeyEvent {
+ code: KeyCode::Char('x'),
+ modifiers: KeyModifiers::NONE,
+ },
+ KeyEvent {
+ code: KeyCode::Char('x'),
+ modifiers: KeyModifiers::NONE,
+ },
+ KeyEvent {
+ code: KeyCode::Char('s'),
+ modifiers: KeyModifiers::ALT,
+ },
+ ])
+ );
+
+ assert_eq!(
+ parse_macro(":o foo.bar<ret>").ok(),
+ Some(vec![
+ KeyEvent {
+ code: KeyCode::Char(':'),
+ modifiers: KeyModifiers::NONE,
+ },
+ KeyEvent {
+ code: KeyCode::Char('o'),
+ modifiers: KeyModifiers::NONE,
+ },
+ KeyEvent {
+ code: KeyCode::Char(' '),
+ modifiers: KeyModifiers::NONE,
+ },
+ KeyEvent {
+ code: KeyCode::Char('f'),
+ modifiers: KeyModifiers::NONE,
+ },
+ KeyEvent {
+ code: KeyCode::Char('o'),
+ modifiers: KeyModifiers::NONE,
+ },
+ KeyEvent {
+ code: KeyCode::Char('o'),
+ modifiers: KeyModifiers::NONE,
+ },
+ KeyEvent {
+ code: KeyCode::Char('.'),
+ modifiers: KeyModifiers::NONE,
+ },
+ KeyEvent {
+ code: KeyCode::Char('b'),
+ modifiers: KeyModifiers::NONE,
+ },
+ KeyEvent {
+ code: KeyCode::Char('a'),
+ modifiers: KeyModifiers::NONE,
+ },
+ KeyEvent {
+ code: KeyCode::Char('r'),
+ modifiers: KeyModifiers::NONE,
+ },
+ KeyEvent {
+ code: KeyCode::Enter,
+ modifiers: KeyModifiers::NONE,
+ },
+ ])
+ );
+ }
+
+ #[test]
+ fn parsing_invalid_macros_fails() {
+ assert!(parse_macro("abc<C-").is_err());
+ assert!(parse_macro("abc>123").is_err());
+ assert!(parse_macro("wd<foo>").is_err());
+ }
}
diff --git a/helix-view/src/keyboard.rs b/helix-view/src/keyboard.rs
index 810aa063..f1717209 100644
--- a/helix-view/src/keyboard.rs
+++ b/helix-view/src/keyboard.rs
@@ -79,8 +79,6 @@ pub enum KeyCode {
PageDown,
/// Tab key.
Tab,
- /// Shift + Tab key.
- BackTab,
/// Delete key.
Delete,
/// Insert key.
@@ -116,7 +114,6 @@ impl From<KeyCode> for crossterm::event::KeyCode {
KeyCode::PageUp => CKeyCode::PageUp,
KeyCode::PageDown => CKeyCode::PageDown,
KeyCode::Tab => CKeyCode::Tab,
- KeyCode::BackTab => CKeyCode::BackTab,
KeyCode::Delete => CKeyCode::Delete,
KeyCode::Insert => CKeyCode::Insert,
KeyCode::F(f_number) => CKeyCode::F(f_number),
@@ -144,7 +141,7 @@ impl From<crossterm::event::KeyCode> for KeyCode {
CKeyCode::PageUp => KeyCode::PageUp,
CKeyCode::PageDown => KeyCode::PageDown,
CKeyCode::Tab => KeyCode::Tab,
- CKeyCode::BackTab => KeyCode::BackTab,
+ CKeyCode::BackTab => unreachable!("BackTab should have been handled on KeyEvent level"),
CKeyCode::Delete => KeyCode::Delete,
CKeyCode::Insert => KeyCode::Insert,
CKeyCode::F(f_number) => KeyCode::F(f_number),
diff --git a/helix-view/src/theme.rs b/helix-view/src/theme.rs
index 757316bd..00c1bbbd 100644
--- a/helix-view/src/theme.rs
+++ b/helix-view/src/theme.rs
@@ -15,6 +15,10 @@ pub use crate::graphics::{Color, Modifier, Style};
pub static DEFAULT_THEME: Lazy<Theme> = Lazy::new(|| {
toml::from_slice(include_bytes!("../../theme.toml")).expect("Failed to parse default theme")
});
+pub static BASE16_DEFAULT_THEME: Lazy<Theme> = Lazy::new(|| {
+ toml::from_slice(include_bytes!("../../base16_theme.toml"))
+ .expect("Failed to parse base 16 default theme")
+});
#[derive(Clone, Debug)]
pub struct Loader {
@@ -35,6 +39,9 @@ impl Loader {
if name == "default" {
return Ok(self.default());
}
+ if name == "base16_default" {
+ return Ok(self.base16_default());
+ }
let filename = format!("{}.toml", name);
let user_path = self.user_dir.join(&filename);
@@ -74,12 +81,20 @@ impl Loader {
pub fn default(&self) -> Theme {
DEFAULT_THEME.clone()
}
+
+ /// Returns the alternative 16-color default theme
+ pub fn base16_default(&self) -> Theme {
+ BASE16_DEFAULT_THEME.clone()
+ }
}
#[derive(Clone, Debug)]
pub struct Theme {
- scopes: Vec<String>,
+ // UI styles are stored in a HashMap
styles: HashMap<String, Style>,
+ // tree-sitter highlight styles are stored in a Vec to optimize lookups
+ scopes: Vec<String>,
+ highlights: Vec<Style>,
}
impl<'de> Deserialize<'de> for Theme {
@@ -88,6 +103,8 @@ impl<'de> Deserialize<'de> for Theme {
D: Deserializer<'de>,
{
let mut styles = HashMap::new();
+ let mut scopes = Vec::new();
+ let mut highlights = Vec::new();
if let Ok(mut colors) = HashMap::<String, Value>::deserialize(deserializer) {
// TODO: alert user of parsing failures in editor
@@ -102,24 +119,38 @@ impl<'de> Deserialize<'de> for Theme {
.unwrap_or_default();
styles.reserve(colors.len());
+ scopes.reserve(colors.len());
+ highlights.reserve(colors.len());
+
for (name, style_value) in colors {
let mut style = Style::default();
if let Err(err) = palette.parse_style(&mut style, style_value) {
warn!("{}", err);
}
- styles.insert(name, style);
+
+ // these are used both as UI and as highlights
+ styles.insert(name.clone(), style);
+ scopes.push(name);
+ highlights.push(style);
}
}
- let scopes = styles.keys().map(ToString::to_string).collect();
- Ok(Self { scopes, styles })
+ Ok(Self {
+ scopes,
+ styles,
+ highlights,
+ })
}
}
impl Theme {
+ #[inline]
+ pub fn highlight(&self, index: usize) -> Style {
+ self.highlights[index]
+ }
+
pub fn get(&self, scope: &str) -> Style {
- self.try_get(scope)
- .unwrap_or_else(|| Style::default().fg(Color::Rgb(0, 0, 255)))
+ self.try_get(scope).unwrap_or_default()
}
pub fn try_get(&self, scope: &str) -> Option<Style> {
@@ -134,6 +165,14 @@ impl Theme {
pub fn find_scope_index(&self, scope: &str) -> Option<usize> {
self.scopes().iter().position(|s| s == scope)
}
+
+ pub fn is_16_color(&self) -> bool {
+ self.styles.iter().all(|(_, style)| {
+ [style.fg, style.bg]
+ .into_iter()
+ .all(|color| !matches!(color, Some(Color::Rgb(..))))
+ })
+ }
}
struct ThemePalette {
@@ -257,53 +296,58 @@ impl TryFrom<Value> for ThemePalette {
}
}
-#[test]
-fn test_parse_style_string() {
- let fg = Value::String("#ffffff".to_string());
+#[cfg(test)]
+mod tests {
+ use super::*;
- let mut style = Style::default();
- let palette = ThemePalette::default();
- palette.parse_style(&mut style, fg).unwrap();
+ #[test]
+ fn test_parse_style_string() {
+ let fg = Value::String("#ffffff".to_string());
- assert_eq!(style, Style::default().fg(Color::Rgb(255, 255, 255)));
-}
+ let mut style = Style::default();
+ let palette = ThemePalette::default();
+ palette.parse_style(&mut style, fg).unwrap();
-#[test]
-fn test_palette() {
- use helix_core::hashmap;
- let fg = Value::String("my_color".to_string());
+ assert_eq!(style, Style::default().fg(Color::Rgb(255, 255, 255)));
+ }
- let mut style = Style::default();
- let palette =
- ThemePalette::new(hashmap! { "my_color".to_string() => Color::Rgb(255, 255, 255) });
- palette.parse_style(&mut style, fg).unwrap();
+ #[test]
+ fn test_palette() {
+ use helix_core::hashmap;
+ let fg = Value::String("my_color".to_string());
- assert_eq!(style, Style::default().fg(Color::Rgb(255, 255, 255)));
-}
+ let mut style = Style::default();
+ let palette =
+ ThemePalette::new(hashmap! { "my_color".to_string() => Color::Rgb(255, 255, 255) });
+ palette.parse_style(&mut style, fg).unwrap();
-#[test]
-fn test_parse_style_table() {
- let table = toml::toml! {
- "keyword" = {
- fg = "#ffffff",
- bg = "#000000",
- modifiers = ["bold"],
- }
- };
+ assert_eq!(style, Style::default().fg(Color::Rgb(255, 255, 255)));
+ }
- let mut style = Style::default();
- let palette = ThemePalette::default();
- if let Value::Table(entries) = table {
- for (_name, value) in entries {
- palette.parse_style(&mut style, value).unwrap();
+ #[test]
+ fn test_parse_style_table() {
+ let table = toml::toml! {
+ "keyword" = {
+ fg = "#ffffff",
+ bg = "#000000",
+ modifiers = ["bold"],
+ }
+ };
+
+ let mut style = Style::default();
+ let palette = ThemePalette::default();
+ if let Value::Table(entries) = table {
+ for (_name, value) in entries {
+ palette.parse_style(&mut style, value).unwrap();
+ }
}
- }
- assert_eq!(
- style,
- Style::default()
- .fg(Color::Rgb(255, 255, 255))
- .bg(Color::Rgb(0, 0, 0))
- .add_modifier(Modifier::BOLD)
- );
+ assert_eq!(
+ style,
+ Style::default()
+ .fg(Color::Rgb(255, 255, 255))
+ .bg(Color::Rgb(0, 0, 0))
+ .add_modifier(Modifier::BOLD)
+ );
+ }
}
diff --git a/helix-view/src/view.rs b/helix-view/src/view.rs
index 9336742b..6bc9435c 100644
--- a/helix-view/src/view.rs
+++ b/helix-view/src/view.rs
@@ -78,6 +78,13 @@ pub struct View {
pub jumps: JumpList,
/// the last accessed file before the current one
pub last_accessed_doc: Option<DocumentId>,
+ /// the last modified files before the current one
+ /// ordered from most frequent to least frequent
+ // uses two docs because we want to be able to swap between the
+ // two last modified docs which we need to manually keep track of
+ pub last_modified_docs: [Option<DocumentId>; 2],
+ /// used to store previous selections of tree-sitter objecs
+ pub object_selections: Vec<Selection>,
}
impl View {
@@ -89,6 +96,8 @@ impl View {
area: Rect::default(), // will get calculated upon inserting into tree
jumps: JumpList::new((doc, Selection::point(0))), // TODO: use actual sel
last_accessed_doc: None,
+ last_modified_docs: [None, None],
+ object_selections: Vec::new(),
}
}
@@ -370,7 +379,7 @@ mod tests {
let text = rope.slice(..);
assert_eq!(
- view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET + 0, 4),
+ view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET, 4),
Some(0)
);
@@ -403,7 +412,7 @@ mod tests {
let text = rope.slice(..);
assert_eq!(
- view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET + 0, 4),
+ view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET, 4),
Some(0)
);