diff options
author | Gokul Soumya | 2021-12-18 03:03:15 +0000 |
---|---|---|
committer | Gokul Soumya | 2021-12-18 03:03:15 +0000 |
commit | d4fb1d06333315a3c6e9dc7f0ad8055d91551e3a (patch) | |
tree | f43da59eec8b897359b84390b1c7d5b69f3bb169 /helix-view/src | |
parent | 016640f4fb6f620df13a2cab15e749d623197a51 (diff) | |
parent | 3ef115d4203fab93f7efe4f65d8dd63cc4535b91 (diff) |
Merge branch 'master' into cursor-shape-new
Diffstat (limited to 'helix-view/src')
-rw-r--r-- | helix-view/src/document.rs | 6 | ||||
-rw-r--r-- | helix-view/src/editor.rs | 187 | ||||
-rw-r--r-- | helix-view/src/graphics.rs | 13 | ||||
-rw-r--r-- | helix-view/src/gutter.rs | 96 | ||||
-rw-r--r-- | helix-view/src/input.rs | 44 | ||||
-rw-r--r-- | helix-view/src/keyboard.rs | 5 | ||||
-rw-r--r-- | helix-view/src/lib.rs | 15 | ||||
-rw-r--r-- | helix-view/src/theme.rs | 51 | ||||
-rw-r--r-- | helix-view/src/view.rs | 30 |
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() { |