use std::collections::BTreeSet;
use serde::{Deserialize, Serialize};
use crate::helix::graphemes::ensure_grapheme_boundary_prev;
use crate::helix::history::History;
use crate::helix::line_ending::auto_detect_line_ending;
use crate::helix::{ChangeSet, LineEnding, Range, Rope, Selection, SmallVec};
use crate::layout::{OutlineLayout, WrapCache};
use crate::marks::{Clipboard, MarkDelta, MarkId, Marks};
use crate::outline::{OutlineCache, OutlineConfig};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Config {
pub tab_width: u16,
pub soft_wrap: bool,
pub line_ending: LineEnding,
#[serde(default, skip_serializing_if = "ExternalUndo::is_default")]
pub external_undo: ExternalUndo,
}
impl Default for Config {
fn default() -> Self {
Config { tab_width: 4, soft_wrap: true, line_ending: LineEnding::LF, external_undo: ExternalUndo::default() }
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ExternalUndo {
#[default]
Transform,
Barrier,
}
impl ExternalUndo {
fn is_default(&self) -> bool {
*self == ExternalUndo::Transform
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ViewConfig {
pub status_bar: bool,
pub scrolloff: u16,
#[serde(default, skip_serializing_if = "Follow::is_default")]
pub follow: Follow,
}
impl Default for ViewConfig {
fn default() -> Self {
ViewConfig { status_bar: true, scrolloff: 2, follow: Follow::Margin }
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Follow {
#[default]
Margin,
Typewriter { percent: u8 },
}
impl Follow {
fn is_default(&self) -> bool {
*self == Follow::Margin
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Viewport {
pub width: u16,
pub height: u16,
}
impl Viewport {
pub fn text_rows(&self) -> usize {
self.height.saturating_sub(1) as usize
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Scroll {
pub line: usize,
pub row: usize,
pub col: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RunKind {
Typing,
DeleteBackward,
DeleteForward,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct EditRun {
pub kind: RunKind,
pub revision: usize,
pub at_ms: u64,
#[serde(default)]
pub chars: usize,
#[serde(default, skip_serializing_if = "is_zero_usize")]
pub view: usize,
}
fn is_zero_u16(n: &u16) -> bool {
*n == 0
}
fn is_zero_usize(n: &usize) -> bool {
*n == 0
}
pub const RUN_GAP_MS: u64 = 1500;
pub const RUN_MAX_CHARS: usize = 256;
pub const RUN_WORD_BREAK_CHARS: usize = 128;
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct Document {
#[serde(serialize_with = "rope_as_string::serialize")]
pub text: Rope,
pub history: History,
pub saved_revision: Option<usize>,
pub saving: Option<usize>,
pub dirty: bool,
pub path: Option<String>,
pub config: Config,
pub clipboard: Clipboard,
pub now_ms: u64,
pub run: Option<EditRun>,
#[serde(skip_serializing_if = "Marks::is_unused")]
pub marks: Marks,
#[serde(skip_serializing_if = "mark_log_is_empty")]
pub mark_log: Vec<MarkDelta>,
#[serde(skip_serializing_if = "Option::is_none")]
pub outline: Option<OutlineConfig>,
#[serde(rename = "doc_rev", skip_serializing_if = "is_zero")]
pub rev: u64,
#[serde(skip_serializing_if = "std::ops::Not::not")]
pub undo_floor: bool,
#[serde(skip)]
pub(crate) derived: OutlineCache,
#[serde(skip)]
pub(crate) edits: EditCount,
#[serde(skip)]
pub(crate) change_log: ChangeLog,
#[serde(skip)]
pub(crate) touched: Touched,
#[serde(skip)]
pub(crate) host: crate::host::Host,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct View {
pub selection: Selection,
pub scroll: Scroll,
pub viewport: Viewport,
pub config: ViewConfig,
#[serde(skip_serializing_if = "Option::is_none")]
pub status: Option<String>,
#[serde(skip_serializing_if = "std::ops::Not::not")]
pub quit_armed: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub word_drag: Option<(usize, usize)>,
#[serde(skip_serializing_if = "BTreeSet::is_empty")]
pub folds: BTreeSet<MarkId>,
#[serde(skip_serializing_if = "std::ops::Not::not")]
pub read_only: bool,
#[serde(skip_serializing_if = "is_true")]
pub focused: bool,
#[serde(skip_serializing_if = "std::ops::Not::not")]
pub free: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub layout: Option<OutlineLayout>,
#[serde(skip_serializing_if = "is_zero_u16")]
pub frame_clock: u16,
#[serde(skip)]
pub(crate) wrap: WrapCache,
}
impl Default for View {
fn default() -> Self {
View::new(Viewport { width: 80, height: 24 })
}
}
impl View {
pub fn new(viewport: Viewport) -> View {
View {
selection: Selection::point(0),
scroll: Scroll::default(),
viewport: Viewport { width: viewport.width.max(1), height: viewport.height.max(1) },
config: ViewConfig::default(),
status: None,
quit_armed: false,
word_drag: None,
folds: BTreeSet::new(),
read_only: false,
focused: true,
free: false,
layout: None,
frame_clock: 0,
wrap: WrapCache::default(),
}
}
pub fn frame_rate(&self) -> Option<u16> {
(self.frame_clock > 0).then_some(self.frame_clock)
}
pub fn read_only(mut self, on: bool) -> View {
self.read_only = on;
self
}
pub fn text_rows(&self) -> usize {
if self.config.status_bar {
self.viewport.text_rows()
} else {
self.viewport.height as usize
}
}
pub fn caret(&self) -> usize {
self.selection.primary().head
}
pub fn fit(&mut self, doc: &Document) {
let text = doc.text.slice(..);
let len = text.len_chars();
let fix = |pos: usize| ensure_grapheme_boundary_prev(text, pos.min(len));
let mut ranges: SmallVec<[Range; 1]> = self
.selection
.ranges()
.iter()
.map(|r| Range { anchor: fix(r.anchor), head: fix(r.head), old_visual_position: r.old_visual_position })
.collect();
if ranges.is_empty() {
ranges.push(Range::point(0));
}
let primary = self.selection.primary_index().min(ranges.len() - 1);
let selection = Selection::new(ranges, primary);
if selection != self.selection {
self.selection = selection;
}
self.viewport.width = self.viewport.width.max(1);
self.viewport.height = self.viewport.height.max(1);
let last_line = doc.text.len_lines().saturating_sub(1);
if self.scroll.line > last_line {
self.scroll = Scroll::default();
}
self.folds.retain(|id| doc.marks.contains(*id));
if let Some((a, b)) = self.word_drag {
if a > b || b > len {
self.word_drag = None;
}
}
}
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct State {
pub doc: Document,
pub view: View,
}
impl Default for Document {
fn default() -> Self {
Document::new("", None)
}
}
impl Document {
pub fn new(text: &str, path: Option<String>) -> Document {
let rope = Rope::from(text);
let line_ending = auto_detect_line_ending(&rope).unwrap_or(LineEnding::LF);
let mut doc = Document {
text: rope,
history: History::default(),
saved_revision: Some(0),
saving: None,
dirty: false,
path,
config: Config { line_ending, ..Config::default() },
clipboard: Clipboard::default(),
now_ms: 0,
run: None,
marks: Marks::default(),
mark_log: vec![MarkDelta::default()],
outline: None,
rev: 0,
undo_floor: false,
derived: OutlineCache::default(),
edits: EditCount::default(),
change_log: ChangeLog::default(),
touched: Touched::all(),
host: crate::host::Host::default(),
};
doc.dirty = doc.compute_dirty();
doc
}
pub fn compute_dirty(&self) -> bool {
self.saved_revision != Some(self.history.current_revision())
}
pub(crate) fn fit_mark_log(&mut self) {
self.mark_log.resize(self.history.len(), MarkDelta::default());
}
pub fn sanitize(&mut self) {
self.config.tab_width = self.config.tab_width.max(1);
if self.history.current_revision() >= self.history.len() {
self.history = History::default();
self.mark_log.clear();
}
self.marks.repair(self.text.slice(..));
self.fit_mark_log();
self.derived.clear();
self.touched = Touched::all();
if self.outline.is_some() {
crate::outline::mint_missing(self);
}
self.dirty = self.compute_dirty();
}
pub fn set_host(&mut self, host: crate::host::Host) {
self.host = host;
}
pub fn host(&self) -> &crate::host::Host {
&self.host
}
pub fn name(&self) -> String {
match &self.path {
Some(p) => std::path::Path::new(p)
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| p.clone()),
None => "[scratch]".to_string(),
}
}
}
impl<'de> Deserialize<'de> for Document {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
StateInput::deserialize(d).map(|i| State::from(i).doc)
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct StateInput {
#[serde(default)]
pub text: String,
#[serde(default)]
pub selection: Option<Selection>,
#[serde(default)]
pub scroll: Scroll,
#[serde(default)]
pub viewport: Option<Viewport>,
#[serde(default)]
pub clipboard: Clipboard,
#[serde(default)]
pub path: Option<String>,
#[serde(default)]
pub history: Option<History>,
#[serde(default, deserialize_with = "present")]
pub saved_revision: Option<Option<usize>>,
#[serde(default)]
pub saving: Option<usize>,
#[serde(default)]
pub dirty: Option<bool>,
#[serde(default)]
pub config: Option<ConfigInput>,
#[serde(default)]
pub status: Option<String>,
#[serde(default)]
pub now_ms: u64,
#[serde(default)]
pub run: Option<EditRun>,
#[serde(default)]
pub quit_armed: bool,
#[serde(default)]
pub marks: Marks,
#[serde(default)]
pub mark_log: Vec<MarkDelta>,
#[serde(default)]
pub outline: Option<OutlineConfig>,
#[serde(default)]
pub word_drag: Option<(usize, usize)>,
#[serde(default)]
pub doc_rev: u64,
#[serde(default)]
pub undo_floor: bool,
#[serde(default)]
pub folds: BTreeSet<MarkId>,
#[serde(default)]
pub read_only: bool,
#[serde(default = "yes")]
pub focused: bool,
#[serde(default)]
pub free: bool,
#[serde(default)]
pub layout: Option<OutlineLayout>,
#[serde(default)]
pub frame_clock: u16,
}
#[derive(Debug, Clone, Default, Deserialize)]
pub struct ConfigInput {
pub tab_width: Option<u16>,
pub soft_wrap: Option<bool>,
pub scrolloff: Option<u16>,
pub line_ending: Option<LineEnding>,
pub status_bar: Option<bool>,
pub follow: Option<Follow>,
pub external_undo: Option<ExternalUndo>,
}
fn present<'de, D, T>(d: D) -> Result<Option<Option<T>>, D::Error>
where
D: serde::Deserializer<'de>,
T: Deserialize<'de>,
{
Option::<T>::deserialize(d).map(Some)
}
fn yes() -> bool {
true
}
fn is_true(b: &bool) -> bool {
*b
}
fn is_zero(n: &u64) -> bool {
*n == 0
}
impl From<StateInput> for State {
fn from(input: StateInput) -> State {
let text = Rope::from(input.text.as_str());
let d = Config::default();
let vd = ViewConfig::default();
let c = input.config.unwrap_or_default();
let config = Config {
tab_width: c.tab_width.unwrap_or(d.tab_width),
soft_wrap: c.soft_wrap.unwrap_or(d.soft_wrap),
line_ending: c.line_ending.or_else(|| auto_detect_line_ending(&text)).unwrap_or(d.line_ending),
external_undo: c.external_undo.unwrap_or_default(),
};
let view_config = ViewConfig {
status_bar: c.status_bar.unwrap_or(vd.status_bar),
scrolloff: c.scrolloff.unwrap_or(vd.scrolloff),
follow: c.follow.unwrap_or_default(),
};
let history = input.history.unwrap_or_default();
let saved_revision = input.saved_revision.unwrap_or(Some(history.current_revision()));
let doc = Document {
text,
history,
saved_revision,
saving: input.saving,
dirty: false,
path: input.path,
config,
clipboard: input.clipboard,
now_ms: input.now_ms,
run: input.run,
marks: input.marks,
mark_log: input.mark_log,
outline: input.outline,
rev: input.doc_rev,
undo_floor: input.undo_floor,
derived: OutlineCache::default(),
edits: EditCount::default(),
change_log: ChangeLog::default(),
touched: Touched::all(),
host: crate::host::Host::default(),
};
let view = View {
selection: input.selection.unwrap_or_else(|| Selection::point(0)),
scroll: input.scroll,
viewport: input.viewport.unwrap_or(Viewport { width: 80, height: 24 }),
config: view_config,
status: input.status,
quit_armed: input.quit_armed,
word_drag: input.word_drag,
folds: input.folds,
read_only: input.read_only,
focused: input.focused,
free: input.free,
layout: input.layout,
frame_clock: input.frame_clock,
wrap: WrapCache::default(),
};
let mut state = State { doc, view };
state.doc.marks.repair(state.doc.text.slice(..));
state.doc.fit_mark_log();
if state.doc.outline.is_some() {
crate::outline::mint_missing(&mut state.doc);
}
state.doc.dirty = state.doc.compute_dirty();
state
}
}
impl<'de> Deserialize<'de> for State {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
StateInput::deserialize(d).map(State::from)
}
}
#[derive(Serialize)]
struct StateOut<'a> {
#[serde(serialize_with = "rope_as_string::serialize")]
text: &'a Rope,
selection: &'a Selection,
scroll: &'a Scroll,
viewport: &'a Viewport,
clipboard: &'a Clipboard,
path: &'a Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
history: Option<&'a History>,
saved_revision: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
saving: Option<Option<usize>>,
dirty: bool,
config: ConfigOut<'a>,
status: &'a Option<String>,
now_ms: u64,
#[serde(skip_serializing_if = "Option::is_none")]
run: Option<&'a Option<EditRun>>,
quit_armed: bool,
#[serde(skip_serializing_if = "Marks::is_unused")]
marks: &'a Marks,
#[serde(skip_serializing_if = "mark_log_is_empty")]
mark_log: &'a [MarkDelta],
#[serde(skip_serializing_if = "Option::is_none")]
outline: &'a Option<OutlineConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
word_drag: &'a Option<(usize, usize)>,
#[serde(skip_serializing_if = "is_zero")]
doc_rev: u64,
#[serde(skip_serializing_if = "std::ops::Not::not")]
undo_floor: bool,
#[serde(skip_serializing_if = "BTreeSet::is_empty")]
folds: &'a BTreeSet<MarkId>,
#[serde(skip_serializing_if = "std::ops::Not::not")]
read_only: bool,
#[serde(skip_serializing_if = "is_true")]
focused: bool,
#[serde(skip_serializing_if = "std::ops::Not::not")]
free: bool,
#[serde(skip_serializing_if = "Option::is_none")]
layout: &'a Option<OutlineLayout>,
#[serde(skip_serializing_if = "is_zero_u16")]
frame_clock: u16,
}
#[derive(Serialize)]
struct ConfigOut<'a> {
tab_width: u16,
soft_wrap: bool,
scrolloff: u16,
line_ending: LineEnding,
status_bar: bool,
#[serde(skip_serializing_if = "Follow::is_default")]
follow: Follow,
#[serde(skip_serializing_if = "ExternalUndo::is_default")]
external_undo: &'a ExternalUndo,
}
impl Serialize for State {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
self.state_out(true).serialize(s)
}
}
pub struct WithoutHistory<'a>(&'a State);
impl Serialize for WithoutHistory<'_> {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
self.0.state_out(false).serialize(s)
}
}
#[derive(Serialize)]
pub struct HistoryPart<'a> {
pub history: &'a History,
pub saved_revision: Option<usize>,
pub saving: Option<usize>,
pub run: &'a Option<EditRun>,
#[serde(skip_serializing_if = "mark_log_is_empty")]
pub mark_log: &'a [MarkDelta],
#[serde(skip_serializing_if = "std::ops::Not::not")]
pub undo_floor: bool,
}
impl State {
pub fn without_history(&self) -> WithoutHistory<'_> {
WithoutHistory(self)
}
pub fn history_part(&self) -> HistoryPart<'_> {
let d = &self.doc;
HistoryPart {
history: &d.history,
saved_revision: d.saved_revision,
saving: d.saving,
run: &d.run,
mark_log: &d.mark_log,
undo_floor: d.undo_floor,
}
}
fn state_out(&self, history: bool) -> StateOut<'_> {
let (d, v) = (&self.doc, &self.view);
let empty: &'static [MarkDelta] = &[];
StateOut {
text: &d.text,
selection: &v.selection,
scroll: &v.scroll,
viewport: &v.viewport,
clipboard: &d.clipboard,
path: &d.path,
history: history.then_some(&d.history),
saved_revision: if history { d.saved_revision } else { (!d.dirty).then_some(0) },
saving: history.then_some(d.saving),
dirty: d.dirty,
config: ConfigOut {
tab_width: d.config.tab_width,
soft_wrap: d.config.soft_wrap,
scrolloff: v.config.scrolloff,
line_ending: d.config.line_ending,
status_bar: v.config.status_bar,
follow: v.config.follow,
external_undo: &d.config.external_undo,
},
status: &v.status,
now_ms: d.now_ms,
run: history.then_some(&d.run),
quit_armed: v.quit_armed,
marks: &d.marks,
mark_log: if history { &d.mark_log } else { empty },
outline: &d.outline,
word_drag: &v.word_drag,
doc_rev: d.rev,
undo_floor: history && d.undo_floor,
folds: &v.folds,
read_only: v.read_only,
focused: v.focused,
free: v.free,
layout: &v.layout,
frame_clock: v.frame_clock,
}
}
}
impl State {
pub fn new(text: &str, path: Option<String>, viewport: Viewport) -> State {
State { doc: Document::new(text, path), view: View::new(viewport) }
}
pub fn from_parts(doc: Document, view: View) -> State {
State { doc, view }
}
pub fn into_parts(self) -> (Document, View) {
(self.doc, self.view)
}
pub fn to_json(&self) -> String {
serde_json::to_string_pretty(self).expect("state serializes")
}
pub fn from_json(json: &str) -> Result<State, serde_json::Error> {
let mut state: State = serde_json::from_str(json)?;
state.sanitize();
Ok(state)
}
pub fn sanitize(&mut self) {
self.doc.sanitize();
self.view.wrap.clear();
self.view.fit(&self.doc);
self.outline_changed();
}
pub(crate) fn fit_mark_log(&mut self) {
self.doc.fit_mark_log();
}
pub fn compute_dirty(&self) -> bool {
self.doc.compute_dirty()
}
pub fn text_rows(&self) -> usize {
self.view.text_rows()
}
pub fn caret(&self) -> usize {
self.view.caret()
}
pub fn name(&self) -> String {
self.doc.name()
}
}
#[derive(Debug, Clone, Copy, Default)]
pub(crate) struct EditCount(pub u64);
impl PartialEq for EditCount {
fn eq(&self, _: &EditCount) -> bool {
true
}
}
#[derive(Debug, Clone)]
pub struct Touched {
range: Option<(usize, usize)>,
all: bool,
}
impl Default for Touched {
fn default() -> Touched {
Touched::all()
}
}
impl PartialEq for Touched {
fn eq(&self, _: &Touched) -> bool {
true
}
}
impl Touched {
pub(crate) fn all() -> Touched {
Touched { range: None, all: true }
}
pub(crate) fn record(&mut self, cs: &ChangeSet) {
if !self.all && !cs.is_empty() {
self.range = Some(changed_span(self.range, cs));
}
}
}
pub(crate) fn changed_span(range: Option<(usize, usize)>, cs: &ChangeSet) -> (usize, usize) {
use crate::helix::transaction::{Assoc, Operation};
let mut pos = 0usize;
let mut span: Option<(usize, usize)> = None;
for op in cs.changes() {
match op {
Operation::Retain(n) => pos += n,
Operation::Delete(_) => {
let (a, b) = span.unwrap_or((pos, pos));
span = Some((a.min(pos), b.max(pos)));
}
Operation::Insert(t) => {
let end = pos + t.chars().count();
let (a, b) = span.unwrap_or((pos, end));
span = Some((a.min(pos), b.max(end)));
pos = end;
}
}
}
let mapped = range.map(|(x, y)| (cs.map_pos(x, Assoc::Before), cs.map_pos(y, Assoc::After)));
match (mapped, span) {
(Some((x, y)), Some((a, b))) => (x.min(a), y.max(b)),
(Some(r), None) | (None, Some(r)) => r,
(None, None) => (pos, pos),
}
}
impl Document {
pub fn take_touched(&mut self) -> Option<(usize, usize)> {
let t = std::mem::replace(&mut self.touched, Touched { range: None, all: false });
let n = self.text.len_chars();
if t.all {
return Some((0, n));
}
t.range.map(|(a, b)| (a.min(n), b.min(n)))
}
pub fn touch_all(&mut self) {
self.touched = Touched::all();
}
}
#[derive(Debug, Clone, Default)]
pub(crate) struct ChangeLog(pub Vec<ChangeSet>);
impl PartialEq for ChangeLog {
fn eq(&self, _: &ChangeLog) -> bool {
true
}
}
fn mark_log_is_empty(log: &[MarkDelta]) -> bool {
log.iter().all(MarkDelta::is_empty)
}
mod rope_as_string {
use crate::helix::Rope;
use serde::Serializer;
pub fn serialize<S: Serializer>(rope: &Rope, s: S) -> Result<S::Ok, S::Error> {
s.collect_str(rope)
}
}