aboutsummaryrefslogtreecommitdiff
path: root/helix-view/src
diff options
context:
space:
mode:
Diffstat (limited to 'helix-view/src')
-rw-r--r--helix-view/src/document.rs6
-rw-r--r--helix-view/src/editor.rs187
-rw-r--r--helix-view/src/graphics.rs13
-rw-r--r--helix-view/src/gutter.rs96
-rw-r--r--helix-view/src/input.rs44
-rw-r--r--helix-view/src/keyboard.rs5
-rw-r--r--helix-view/src/lib.rs15
-rw-r--r--helix-view/src/theme.rs51
-rw-r--r--helix-view/src/view.rs30
9 files changed, 324 insertions, 123 deletions
diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs
index 01975452..a0315bed 100644
--- a/helix-view/src/document.rs
+++ b/helix-view/src/document.rs
@@ -104,6 +104,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 +128,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()
@@ -344,6 +346,7 @@ impl Document {
history: Cell::new(History::default()),
savepoint: None,
last_saved_revision: 0,
+ modified_since_accessed: false,
language_server: None,
}
}
@@ -639,6 +642,9 @@ impl Document {
selection.clone().ensure_invariants(self.text.slice(..)),
);
}
+
+ // set modified since accessed
+ self.modified_since_accessed = true;
}
if !transaction.changes().is_empty() {
diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs
index a121a836..fff4792d 100644
--- a/helix-view/src/editor.rs
+++ b/helix-view/src/editor.rs
@@ -2,6 +2,7 @@ use crate::{
clipboard::{get_clipboard_provider, ClipboardProvider},
document::{Mode, SCRATCH_BUFFER_NAME},
graphics::{CursorKind, Rect},
+ input::KeyEvent,
theme::{self, Theme},
tree::{self, Tree},
Document, DocumentId, View, ViewId,
@@ -11,6 +12,7 @@ use futures_util::future;
use std::{
collections::{BTreeMap, HashMap},
io::stdin,
+ num::NonZeroUsize,
path::{Path, PathBuf},
pin::Pin,
sync::Arc,
@@ -18,7 +20,7 @@ use std::{
use tokio::time::{sleep, Duration, Instant, Sleep};
-use anyhow::Error;
+use anyhow::{bail, Error};
pub use helix_core::diagnostic::Severity;
pub use helix_core::register::Registers;
@@ -105,6 +107,8 @@ pub struct Config {
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,
}
// Cursor shape is read and used on every rendered frame and so needs
@@ -141,7 +145,7 @@ impl Default for CursorShapeConfig {
}
}
-#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
+#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum LineNumber {
/// Show absolute line number
@@ -171,6 +175,7 @@ impl Default for Config {
auto_info: true,
file_picker: FilePickerConfig::default(),
cursor_shape: CursorShapeConfig::default(),
+ true_color: false,
}
}
}
@@ -190,11 +195,12 @@ impl std::fmt::Debug for Motion {
#[derive(Debug)]
pub struct Editor {
pub tree: Tree,
- pub next_document_id: usize,
+ pub next_document_id: DocumentId,
pub documents: BTreeMap<DocumentId, Document>,
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,
pub clipboard_provider: Box<dyn ClipboardProvider>,
@@ -223,8 +229,8 @@ pub enum Action {
impl Editor {
pub fn new(
mut area: Rect,
- themes: Arc<theme::Loader>,
- config_loader: Arc<syntax::Loader>,
+ theme_loader: Arc<theme::Loader>,
+ syn_loader: Arc<syntax::Loader>,
config: Config,
) -> Self {
let language_servers = helix_lsp::Registry::new();
@@ -234,14 +240,15 @@ impl Editor {
Self {
tree: Tree::new(area),
- next_document_id: 0,
+ next_document_id: DocumentId::default(),
documents: BTreeMap::new(),
count: None,
selected_register: None,
- theme: themes.default(),
+ macro_recording: None,
+ theme: theme_loader.default(),
language_servers,
- syn_loader: config_loader,
- theme_loader: themes,
+ syn_loader,
+ theme_loader,
registers: Registers::default(),
clipboard_provider: get_clipboard_provider(),
status_msg: None,
@@ -297,14 +304,51 @@ impl Editor {
self._refresh();
}
- pub fn set_theme_from_name(&mut self, theme: &str) -> anyhow::Result<()> {
- use anyhow::Context;
- 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);
+ Self::launch_language_server(&mut self.language_servers, doc)
+ }
+
+ /// Launch a language server for a given document
+ fn launch_language_server(ls: &mut helix_lsp::Registry, doc: &mut Document) -> Option<()> {
+ // try to find a language server based on the language name
+ let language_server = doc.language.as_ref().and_then(|language| {
+ ls.get(language)
+ .map_err(|e| {
+ log::error!(
+ "Failed to initialize the LSP for `{}` {{ {} }}",
+ language.scope(),
+ e
+ )
+ })
+ .ok()
+ });
+ if let Some(language_server) = language_server {
+ // only spawn a new lang server if the servers aren't the same
+ if Some(language_server.id()) != doc.language_server().map(|server| server.id()) {
+ 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();
+
+ // TODO: this now races with on_init code if the init happens too quickly
+ tokio::spawn(language_server.text_document_did_open(
+ doc.url().unwrap(),
+ doc.version(),
+ doc.text(),
+ language_id,
+ ));
+
+ doc.set_language_server(Some(language_server));
+ }
+ }
+ Some(())
}
fn _refresh(&mut self) {
@@ -358,7 +402,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`.
@@ -367,7 +412,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(id)
+ {
+ view.last_modified_docs = [Some(view.doc), view.last_modified_docs[0]];
+ }
+ }
}
let view_id = view.id;
@@ -377,23 +431,22 @@ impl Editor {
}
Action::Load => {
let view_id = view!(self).id;
- if let Some(doc) = self.document_mut(id) {
- if doc.selections().is_empty() {
- doc.selections.insert(view_id, Selection::point(0));
- }
+ let doc = self.documents.get_mut(&id).unwrap();
+ if doc.selections().is_empty() {
+ doc.selections.insert(view_id, Selection::point(0));
}
return;
}
- Action::HorizontalSplit => {
- let view = View::new(id);
- let view_id = self.tree.split(view, Layout::Horizontal);
- // initialize selection for view
- let doc = self.documents.get_mut(&id).unwrap();
- doc.selections.insert(view_id, Selection::point(0));
- }
- Action::VerticalSplit => {
+ Action::HorizontalSplit | Action::VerticalSplit => {
let view = View::new(id);
- let view_id = self.tree.split(view, Layout::Vertical);
+ let view_id = self.tree.split(
+ view,
+ match action {
+ Action::HorizontalSplit => Layout::Horizontal,
+ Action::VerticalSplit => Layout::Vertical,
+ _ => unreachable!(),
+ },
+ );
// initialize selection for view
let doc = self.documents.get_mut(&id).unwrap();
doc.selections.insert(view_id, Selection::point(0));
@@ -403,16 +456,19 @@ impl Editor {
self._refresh();
}
- fn new_document(&mut self, mut document: Document) -> DocumentId {
- let id = DocumentId(self.next_document_id);
- self.next_document_id += 1;
- document.id = id;
- self.documents.insert(id, document);
+ /// Generate an id for a new document and register it.
+ fn new_document(&mut self, mut doc: Document) -> DocumentId {
+ let id = self.next_document_id;
+ // Safety: adding 1 from 1 is fine, probably impossible to reach usize max
+ self.next_document_id =
+ DocumentId(unsafe { NonZeroUsize::new_unchecked(self.next_document_id.0.get() + 1) });
+ doc.id = id;
+ self.documents.insert(id, doc);
id
}
- fn new_file_from_document(&mut self, action: Action, document: Document) -> DocumentId {
- let id = self.new_document(document);
+ fn new_file_from_document(&mut self, action: Action, doc: Document) -> DocumentId {
+ let id = self.new_document(doc);
self.switch(id, action);
id
}
@@ -428,54 +484,16 @@ impl Editor {
pub fn open(&mut self, path: PathBuf, action: Action) -> Result<DocumentId, Error> {
let path = helix_core::path::get_canonicalized_path(&path)?;
-
- let id = self
- .documents()
- .find(|doc| doc.path() == Some(&path))
- .map(|doc| doc.id);
+ let id = self.document_by_path(&path).map(|doc| doc.id);
let id = if let Some(id) = id {
id
} else {
let mut doc = Document::open(&path, None, Some(&self.theme), Some(&self.syn_loader))?;
- // try to find a language server based on the language name
- let language_server = doc.language.as_ref().and_then(|language| {
- self.language_servers
- .get(language)
- .map_err(|e| {
- log::error!(
- "Failed to initialize the LSP for `{}` {{ {} }}",
- language.scope(),
- e
- )
- })
- .ok()
- });
-
- if let Some(language_server) = language_server {
- let language_id = doc
- .language()
- .and_then(|s| s.split('.').last()) // source.rust
- .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(
- doc.url().unwrap(),
- doc.version(),
- doc.text(),
- language_id,
- ));
-
- doc.set_language_server(Some(language_server));
- }
+ let _ = Self::launch_language_server(&mut self.language_servers, &mut doc);
- let id = DocumentId(self.next_document_id);
- self.next_document_id += 1;
- doc.id = id;
- self.documents.insert(id, doc);
- id
+ self.new_document(doc)
};
self.switch(id, action);
@@ -498,11 +516,11 @@ impl Editor {
pub fn close_document(&mut self, doc_id: DocumentId, force: bool) -> anyhow::Result<()> {
let doc = match self.documents.get(&doc_id) {
Some(doc) => doc,
- None => anyhow::bail!("document does not exist"),
+ None => bail!("document does not exist"),
};
if !force && doc.is_modified() {
- anyhow::bail!(
+ bail!(
"buffer {:?} is modified",
doc.relative_path()
.map(|path| path.to_string_lossy().to_string())
@@ -535,7 +553,7 @@ impl Editor {
// If the document we removed was visible in all views, we will have no more views. We don't
// want to close the editor just for a simple buffer close, so we need to create a new view
// containing either an existing document, or a brand new document.
- if self.tree.views().peekable().peek().is_none() {
+ if self.tree.views().next().is_none() {
let doc_id = self
.documents
.iter()
@@ -620,8 +638,7 @@ impl Editor {
}
pub fn cursor(&self) -> (Option<Position>, CursorKind) {
- let view = view!(self);
- let doc = &self.documents[&view.doc];
+ let (view, doc) = current_ref!(self);
let cursor = doc
.selection(view.id)
.primary()
diff --git a/helix-view/src/graphics.rs b/helix-view/src/graphics.rs
index acdaa696..892aa646 100644
--- a/helix-view/src/graphics.rs
+++ b/helix-view/src/graphics.rs
@@ -33,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,
@@ -41,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.
diff --git a/helix-view/src/gutter.rs b/helix-view/src/gutter.rs
new file mode 100644
index 00000000..af016c56
--- /dev/null
+++ b/helix-view/src/gutter.rs
@@ -0,0 +1,96 @@
+use std::fmt::Write;
+
+use crate::{editor::Config, graphics::Style, Document, Theme, View};
+
+pub type GutterFn<'doc> = Box<dyn Fn(usize, bool, &mut String) -> Option<Style> + 'doc>;
+pub type Gutter =
+ for<'doc> fn(&'doc Document, &View, &Theme, &Config, bool, usize) -> GutterFn<'doc>;
+
+pub fn diagnostic<'doc>(
+ doc: &'doc Document,
+ _view: &View,
+ theme: &Theme,
+ _config: &Config,
+ _is_focused: bool,
+ _width: usize,
+) -> GutterFn<'doc> {
+ let warning = theme.get("warning");
+ let error = theme.get("error");
+ let info = theme.get("info");
+ let hint = theme.get("hint");
+ let diagnostics = doc.diagnostics();
+
+ Box::new(move |line: usize, _selected: bool, out: &mut String| {
+ use helix_core::diagnostic::Severity;
+ 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,
+ Some(Severity::Warning) | None => warning,
+ Some(Severity::Info) => info,
+ Some(Severity::Hint) => hint,
+ });
+ }
+ None
+ })
+}
+
+pub fn line_number<'doc>(
+ doc: &'doc Document,
+ view: &View,
+ theme: &Theme,
+ config: &Config,
+ is_focused: bool,
+ width: usize,
+) -> GutterFn<'doc> {
+ let text = doc.text().slice(..);
+ let last_line = view.last_line(doc);
+ // Whether to draw the line number for the last line of the
+ // document or not. We only draw it if it's not an empty line.
+ let draw_last = text.line_to_byte(last_line) < text.len_bytes();
+
+ let linenr = theme.get("ui.linenr");
+ let linenr_select: Style = theme.try_get("ui.linenr.selected").unwrap_or(linenr);
+
+ let current_line = doc
+ .text()
+ .char_to_line(doc.selection(view.id).primary().cursor(text));
+
+ let config = config.line_number;
+
+ 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)
+ }
+ }
+ };
+ let style = if selected && is_focused {
+ linenr_select
+ } else {
+ linenr
+ };
+ write!(out, "{:>1$}", line, width).unwrap();
+ Some(style)
+ }
+ })
+}
+
+#[inline(always)]
+const fn abs_diff(a: usize, b: usize) -> usize {
+ if a > b {
+ a - b
+ } else {
+ b - a
+ }
+}
diff --git a/helix-view/src/input.rs b/helix-view/src/input.rs
index 580204cc..92caa517 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,12 +216,40 @@ 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(),
+ }
}
}
}
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/lib.rs b/helix-view/src/lib.rs
index 3e779356..a56c914d 100644
--- a/helix-view/src/lib.rs
+++ b/helix-view/src/lib.rs
@@ -5,6 +5,7 @@ pub mod clipboard;
pub mod document;
pub mod editor;
pub mod graphics;
+pub mod gutter;
pub mod info;
pub mod input;
pub mod keyboard;
@@ -12,8 +13,18 @@ pub mod theme;
pub mod tree;
pub mod view;
-#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)]
-pub struct DocumentId(usize);
+use std::num::NonZeroUsize;
+
+// uses NonZeroUsize so Option<DocumentId> use a byte rather than two
+#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
+pub struct DocumentId(NonZeroUsize);
+
+impl Default for DocumentId {
+ fn default() -> DocumentId {
+ // Safety: 1 is non-zero
+ DocumentId(unsafe { NonZeroUsize::new_unchecked(1) })
+ }
+}
slotmap::new_key_type! {
pub struct ViewId;
diff --git a/helix-view/src/theme.rs b/helix-view/src/theme.rs
index 757316bd..4a2ecbba 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 {
diff --git a/helix-view/src/view.rs b/helix-view/src/view.rs
index a77f1562..94d67acd 100644
--- a/helix-view/src/view.rs
+++ b/helix-view/src/view.rs
@@ -1,6 +1,10 @@
use std::borrow::Cow;
-use crate::{graphics::Rect, Document, DocumentId, ViewId};
+use crate::{
+ graphics::Rect,
+ gutter::{self, Gutter},
+ Document, DocumentId, ViewId,
+};
use helix_core::{
graphemes::{grapheme_width, RopeGraphemes},
line_ending::line_end_char_index,
@@ -60,6 +64,8 @@ impl JumpList {
}
}
+const GUTTERS: &[(Gutter, usize)] = &[(gutter::diagnostic, 1), (gutter::line_number, 5)];
+
#[derive(Debug)]
pub struct View {
pub id: ViewId,
@@ -69,6 +75,11 @@ 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],
}
impl View {
@@ -80,13 +91,23 @@ 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],
}
}
+ pub fn gutters(&self) -> &[(Gutter, usize)] {
+ GUTTERS
+ }
+
pub fn inner_area(&self) -> Rect {
- // TODO: not ideal
- const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter
- self.area.clip_left(OFFSET).clip_bottom(1) // -1 for statusline
+ // TODO: cache this
+ let offset = self
+ .gutters()
+ .iter()
+ .map(|(_, width)| *width as u16)
+ .sum::<u16>()
+ + 1; // +1 for some space between gutters and line
+ self.area.clip_left(offset).clip_bottom(1) // -1 for statusline
}
//
@@ -276,6 +297,7 @@ mod tests {
use super::*;
use helix_core::Rope;
const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter
+ // const OFFSET: u16 = GUTTERS.iter().map(|(_, width)| *width as u16).sum();
#[test]
fn test_text_pos_at_screen_coords() {