use super::{CompactionRecommendation, CompactionUrgency, FragmentationStats, LineEnding};
use encoding_rs::{Encoding, UTF_16BE, UTF_16LE, UTF_8};
use std::fmt;
use std::io;
use std::ops::Deref;
use std::path::{Path, PathBuf};
#[derive(Clone, Copy)]
pub struct DocumentEncoding(&'static Encoding);
impl DocumentEncoding {
pub const fn utf8() -> Self {
Self(UTF_8)
}
pub const fn utf16le() -> Self {
Self(UTF_16LE)
}
pub const fn utf16be() -> Self {
Self(UTF_16BE)
}
pub fn from_label(label: &str) -> Option<Self> {
Encoding::for_label(label.as_bytes()).map(Self)
}
pub fn name(self) -> &'static str {
self.0.name()
}
pub fn is_utf8(self) -> bool {
self.0 == UTF_8
}
pub fn can_roundtrip_save(self) -> bool {
self.0.output_encoding() == self.0
}
pub(crate) const fn as_encoding(self) -> &'static Encoding {
self.0
}
pub(crate) const fn from_encoding_rs(encoding: &'static Encoding) -> Self {
Self(encoding)
}
}
impl Default for DocumentEncoding {
fn default() -> Self {
Self::utf8()
}
}
impl PartialEq for DocumentEncoding {
fn eq(&self, other: &Self) -> bool {
std::ptr::eq(self.0, other.0)
}
}
impl Eq for DocumentEncoding {}
impl std::hash::Hash for DocumentEncoding {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.name().hash(state);
}
}
impl fmt::Debug for DocumentEncoding {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("DocumentEncoding")
.field(&self.name())
.finish()
}
}
impl fmt::Display for DocumentEncoding {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.name())
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
pub enum DocumentEncodingOrigin {
#[default]
NewDocument,
Utf8FastPath,
AutoDetected,
AutoDetectFallbackUtf8,
AutoDetectFallbackOverride,
ExplicitReinterpretation,
SaveConversion,
}
impl DocumentEncodingOrigin {
pub const fn as_str(self) -> &'static str {
match self {
Self::NewDocument => "new-document",
Self::Utf8FastPath => "utf8-fast-path",
Self::AutoDetected => "auto-detected",
Self::AutoDetectFallbackUtf8 => "auto-detect-fallback-utf8",
Self::AutoDetectFallbackOverride => "auto-detect-fallback-override",
Self::ExplicitReinterpretation => "explicit-reinterpretation",
Self::SaveConversion => "save-conversion",
}
}
pub const fn used_auto_detection(self) -> bool {
matches!(
self,
Self::AutoDetected | Self::AutoDetectFallbackUtf8 | Self::AutoDetectFallbackOverride
)
}
pub const fn is_explicit(self) -> bool {
matches!(
self,
Self::AutoDetectFallbackOverride
| Self::ExplicitReinterpretation
| Self::SaveConversion
)
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
pub enum OpenEncodingPolicy {
#[default]
Utf8FastPath,
AutoDetect,
AutoDetectOrReinterpret(DocumentEncoding),
Reinterpret(DocumentEncoding),
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
pub struct DocumentOpenOptions {
encoding_policy: OpenEncodingPolicy,
}
impl DocumentOpenOptions {
pub const fn new() -> Self {
Self {
encoding_policy: OpenEncodingPolicy::Utf8FastPath,
}
}
pub const fn with_auto_encoding_detection(mut self) -> Self {
self.encoding_policy = OpenEncodingPolicy::AutoDetect;
self
}
pub const fn with_auto_encoding_detection_and_fallback(
mut self,
encoding: DocumentEncoding,
) -> Self {
self.encoding_policy = OpenEncodingPolicy::AutoDetectOrReinterpret(encoding);
self
}
pub const fn with_reinterpretation(mut self, encoding: DocumentEncoding) -> Self {
self.encoding_policy = OpenEncodingPolicy::Reinterpret(encoding);
self
}
pub const fn with_encoding(mut self, encoding: DocumentEncoding) -> Self {
self.encoding_policy = OpenEncodingPolicy::Reinterpret(encoding);
self
}
pub const fn encoding_policy(self) -> OpenEncodingPolicy {
self.encoding_policy
}
pub const fn encoding_override(self) -> Option<DocumentEncoding> {
match self.encoding_policy {
OpenEncodingPolicy::Reinterpret(encoding)
| OpenEncodingPolicy::AutoDetectOrReinterpret(encoding) => Some(encoding),
OpenEncodingPolicy::Utf8FastPath | OpenEncodingPolicy::AutoDetect => None,
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
pub enum SaveEncodingPolicy {
#[default]
Preserve,
Convert(DocumentEncoding),
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
pub struct DocumentSaveOptions {
encoding_policy: SaveEncodingPolicy,
}
impl DocumentSaveOptions {
pub const fn new() -> Self {
Self {
encoding_policy: SaveEncodingPolicy::Preserve,
}
}
pub const fn with_encoding(mut self, encoding: DocumentEncoding) -> Self {
self.encoding_policy = SaveEncodingPolicy::Convert(encoding);
self
}
pub const fn encoding_policy(self) -> SaveEncodingPolicy {
self.encoding_policy
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct LineSlice {
text: String,
exact: bool,
}
impl LineSlice {
pub fn new(text: String, exact: bool) -> Self {
Self { text, exact }
}
pub fn text(&self) -> &str {
&self.text
}
pub fn into_text(self) -> String {
self.text
}
pub fn is_exact(&self) -> bool {
self.exact
}
pub fn is_empty(&self) -> bool {
self.text.is_empty()
}
}
impl AsRef<str> for LineSlice {
fn as_ref(&self) -> &str {
self.text()
}
}
impl Deref for LineSlice {
type Target = str;
fn deref(&self) -> &Self::Target {
self.text()
}
}
impl fmt::Display for LineSlice {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.text())
}
}
impl From<LineSlice> for String {
fn from(value: LineSlice) -> Self {
value.into_text()
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct TextSlice {
text: String,
exact: bool,
}
impl TextSlice {
pub fn new(text: String, exact: bool) -> Self {
Self { text, exact }
}
pub fn text(&self) -> &str {
&self.text
}
pub fn into_text(self) -> String {
self.text
}
pub fn is_exact(&self) -> bool {
self.exact
}
pub fn is_empty(&self) -> bool {
self.text.is_empty()
}
}
impl AsRef<str> for TextSlice {
fn as_ref(&self) -> &str {
self.text()
}
}
impl Deref for TextSlice {
type Target = str;
fn deref(&self) -> &Self::Target {
self.text()
}
}
impl fmt::Display for TextSlice {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.text())
}
}
impl From<TextSlice> for String {
fn from(value: TextSlice) -> Self {
value.into_text()
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct TextPosition {
line0: usize,
col0: usize,
}
impl TextPosition {
pub const fn new(line0: usize, col0: usize) -> Self {
Self { line0, col0 }
}
pub const fn line0(self) -> usize {
self.line0
}
pub const fn col0(self) -> usize {
self.col0
}
}
impl From<(usize, usize)> for TextPosition {
fn from(value: (usize, usize)) -> Self {
Self::new(value.0, value.1)
}
}
impl From<TextPosition> for (usize, usize) {
fn from(value: TextPosition) -> Self {
(value.line0, value.col0)
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
pub struct TextRange {
start: TextPosition,
len_chars: usize,
}
impl TextRange {
pub const fn new(start: TextPosition, len_chars: usize) -> Self {
Self { start, len_chars }
}
pub const fn empty(start: TextPosition) -> Self {
Self::new(start, 0)
}
pub const fn start(self) -> TextPosition {
self.start
}
pub const fn len_chars(self) -> usize {
self.len_chars
}
pub const fn is_empty(self) -> bool {
self.len_chars == 0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct SearchMatch {
range: TextRange,
end: TextPosition,
}
impl SearchMatch {
pub const fn new(range: TextRange, end: TextPosition) -> Self {
Self { range, end }
}
pub const fn range(self) -> TextRange {
self.range
}
pub const fn start(self) -> TextPosition {
self.range.start()
}
pub const fn end(self) -> TextPosition {
self.end
}
pub const fn len_chars(self) -> usize {
self.range.len_chars()
}
pub const fn is_empty(self) -> bool {
self.range.is_empty()
}
pub const fn selection(self) -> TextSelection {
TextSelection::new(self.start(), self.end())
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
pub struct TextSelection {
anchor: TextPosition,
head: TextPosition,
}
impl TextSelection {
pub const fn new(anchor: TextPosition, head: TextPosition) -> Self {
Self { anchor, head }
}
pub const fn caret(position: TextPosition) -> Self {
Self::new(position, position)
}
pub const fn anchor(self) -> TextPosition {
self.anchor
}
pub const fn head(self) -> TextPosition {
self.head
}
pub fn is_caret(self) -> bool {
self.anchor == self.head
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ViewportRequest {
first_line0: usize,
line_count: usize,
start_col: usize,
max_cols: usize,
}
impl Default for ViewportRequest {
fn default() -> Self {
Self::new(0, 0)
}
}
impl ViewportRequest {
pub const fn new(first_line0: usize, line_count: usize) -> Self {
Self {
first_line0,
line_count,
start_col: 0,
max_cols: usize::MAX,
}
}
pub const fn with_columns(mut self, start_col: usize, max_cols: usize) -> Self {
self.start_col = start_col;
self.max_cols = max_cols;
self
}
pub const fn first_line0(self) -> usize {
self.first_line0
}
pub const fn line_count(self) -> usize {
self.line_count
}
pub const fn start_col(self) -> usize {
self.start_col
}
pub const fn max_cols(self) -> usize {
self.max_cols
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ViewportRow {
line0: usize,
slice: LineSlice,
}
impl ViewportRow {
pub fn new(line0: usize, slice: LineSlice) -> Self {
Self { line0, slice }
}
pub fn line0(&self) -> usize {
self.line0
}
pub fn line_number(&self) -> usize {
self.line0.saturating_add(1)
}
pub fn slice(&self) -> &LineSlice {
&self.slice
}
pub fn into_slice(self) -> LineSlice {
self.slice
}
pub fn text(&self) -> &str {
self.slice.text()
}
pub fn is_exact(&self) -> bool {
self.slice.is_exact()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Viewport {
request: ViewportRequest,
total_lines: LineCount,
rows: Vec<ViewportRow>,
}
impl Viewport {
pub fn new(request: ViewportRequest, total_lines: LineCount, rows: Vec<ViewportRow>) -> Self {
Self {
request,
total_lines,
rows,
}
}
pub fn request(&self) -> ViewportRequest {
self.request
}
pub fn total_lines(&self) -> LineCount {
self.total_lines
}
pub fn rows(&self) -> &[ViewportRow] {
&self.rows
}
pub fn into_rows(self) -> Vec<ViewportRow> {
self.rows
}
pub fn len(&self) -> usize {
self.rows.len()
}
pub fn is_empty(&self) -> bool {
self.rows.is_empty()
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct EditResult {
changed: bool,
cursor: TextPosition,
}
impl EditResult {
pub const fn new(changed: bool, cursor: TextPosition) -> Self {
Self { changed, cursor }
}
pub const fn changed(self) -> bool {
self.changed
}
pub const fn cursor(self) -> TextPosition {
self.cursor
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct CutResult {
text: String,
edit: EditResult,
}
impl CutResult {
pub fn new(text: String, edit: EditResult) -> Self {
Self { text, edit }
}
pub fn text(&self) -> &str {
&self.text
}
pub fn into_text(self) -> String {
self.text
}
pub const fn edit(&self) -> EditResult {
self.edit
}
pub const fn changed(&self) -> bool {
self.edit.changed()
}
pub const fn cursor(&self) -> TextPosition {
self.edit.cursor()
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct ByteProgress {
completed_bytes: usize,
total_bytes: usize,
}
impl ByteProgress {
pub const fn new(completed_bytes: usize, total_bytes: usize) -> Self {
Self {
completed_bytes,
total_bytes,
}
}
pub const fn completed_bytes(self) -> usize {
self.completed_bytes
}
pub const fn total_bytes(self) -> usize {
self.total_bytes
}
pub fn fraction(self) -> f32 {
if self.total_bytes == 0 {
1.0
} else {
self.completed_bytes.min(self.total_bytes) as f32 / self.total_bytes as f32
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[must_use]
pub enum LineCount {
Exact(usize),
Estimated(usize),
}
impl LineCount {
pub fn exact(self) -> Option<usize> {
match self {
Self::Exact(lines) => Some(lines),
Self::Estimated(_) => None,
}
}
pub fn display_rows(self) -> usize {
match self {
Self::Exact(lines) | Self::Estimated(lines) => lines.max(1),
}
}
pub fn is_exact(self) -> bool {
matches!(self, Self::Exact(_))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DocumentBacking {
Mmap,
PieceTable,
Rope,
}
impl DocumentBacking {
pub const fn as_str(self) -> &'static str {
match self {
Self::Mmap => "mmap",
Self::PieceTable => "piece-table",
Self::Rope => "rope",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum EditCapability {
Editable {
backing: DocumentBacking,
},
RequiresPromotion {
from: DocumentBacking,
to: DocumentBacking,
},
Unsupported {
backing: DocumentBacking,
reason: &'static str,
},
}
impl EditCapability {
pub const fn is_editable(self) -> bool {
!matches!(self, Self::Unsupported { .. })
}
pub const fn requires_promotion(self) -> bool {
matches!(self, Self::RequiresPromotion { .. })
}
pub const fn current_backing(self) -> DocumentBacking {
match self {
Self::Editable { backing } | Self::Unsupported { backing, .. } => backing,
Self::RequiresPromotion { from, .. } => from,
}
}
pub const fn target_backing(self) -> Option<DocumentBacking> {
match self {
Self::RequiresPromotion { to, .. } => Some(to),
_ => None,
}
}
pub const fn reason(self) -> Option<&'static str> {
match self {
Self::Unsupported { reason, .. } => Some(reason),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DocumentStatus {
path: Option<PathBuf>,
dirty: bool,
file_len: usize,
line_count: LineCount,
exact_line_count_pending: bool,
line_ending: LineEnding,
encoding: DocumentEncoding,
preserve_save_error: Option<DocumentEncodingErrorKind>,
encoding_origin: DocumentEncodingOrigin,
decoding_had_errors: bool,
indexing: Option<ByteProgress>,
backing: DocumentBacking,
}
impl DocumentStatus {
#[allow(clippy::too_many_arguments)]
pub fn new(
path: Option<PathBuf>,
dirty: bool,
file_len: usize,
line_count: LineCount,
exact_line_count_pending: bool,
line_ending: LineEnding,
encoding: DocumentEncoding,
preserve_save_error: Option<DocumentEncodingErrorKind>,
encoding_origin: DocumentEncodingOrigin,
decoding_had_errors: bool,
indexing: Option<ByteProgress>,
backing: DocumentBacking,
) -> Self {
Self {
path,
dirty,
file_len,
line_count,
exact_line_count_pending,
line_ending,
encoding,
preserve_save_error,
encoding_origin,
decoding_had_errors,
indexing,
backing,
}
}
pub fn path(&self) -> Option<&Path> {
self.path.as_deref()
}
pub fn is_dirty(&self) -> bool {
self.dirty
}
pub fn file_len(&self) -> usize {
self.file_len
}
pub fn line_count(&self) -> LineCount {
self.line_count
}
pub fn exact_line_count(&self) -> Option<usize> {
self.line_count.exact()
}
pub fn display_line_count(&self) -> usize {
self.line_count.display_rows()
}
pub fn is_line_count_exact(&self) -> bool {
self.line_count.is_exact()
}
pub fn is_line_count_pending(&self) -> bool {
self.exact_line_count_pending
}
pub fn line_ending(&self) -> LineEnding {
self.line_ending
}
pub fn encoding(&self) -> DocumentEncoding {
self.encoding
}
pub fn preserve_save_error(&self) -> Option<DocumentEncodingErrorKind> {
self.preserve_save_error
}
pub fn can_preserve_save(&self) -> bool {
self.preserve_save_error().is_none()
}
pub fn encoding_origin(&self) -> DocumentEncodingOrigin {
self.encoding_origin
}
pub fn decoding_had_errors(&self) -> bool {
self.decoding_had_errors
}
pub fn indexing_state(&self) -> Option<ByteProgress> {
self.indexing
}
pub fn is_indexing(&self) -> bool {
self.indexing.is_some()
}
pub fn backing(&self) -> DocumentBacking {
self.backing
}
pub fn has_edit_buffer(&self) -> bool {
!matches!(self.backing, DocumentBacking::Mmap)
}
pub fn has_rope(&self) -> bool {
matches!(self.backing, DocumentBacking::Rope)
}
pub fn has_piece_table(&self) -> bool {
matches!(self.backing, DocumentBacking::PieceTable)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct DocumentMaintenanceStatus {
backing: DocumentBacking,
fragmentation: Option<FragmentationStats>,
compaction: Option<CompactionRecommendation>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MaintenanceAction {
None,
IdleCompaction,
ExplicitCompaction,
}
impl MaintenanceAction {
pub const fn as_str(self) -> &'static str {
match self {
Self::None => "none",
Self::IdleCompaction => "idle-compaction",
Self::ExplicitCompaction => "explicit-compaction",
}
}
}
impl DocumentMaintenanceStatus {
pub const fn new(
backing: DocumentBacking,
fragmentation: Option<FragmentationStats>,
compaction: Option<CompactionRecommendation>,
) -> Self {
Self {
backing,
fragmentation,
compaction,
}
}
pub const fn backing(self) -> DocumentBacking {
self.backing
}
pub const fn has_piece_table(self) -> bool {
matches!(self.backing, DocumentBacking::PieceTable)
}
pub const fn fragmentation_stats(self) -> Option<FragmentationStats> {
self.fragmentation
}
pub const fn has_fragmentation_stats(self) -> bool {
self.fragmentation.is_some()
}
pub const fn compaction_recommendation(self) -> Option<CompactionRecommendation> {
self.compaction
}
pub const fn is_compaction_recommended(self) -> bool {
self.compaction.is_some()
}
pub fn compaction_urgency(self) -> Option<CompactionUrgency> {
self.compaction
.map(|recommendation| recommendation.urgency())
}
pub fn recommended_action(self) -> MaintenanceAction {
match self.compaction_urgency() {
Some(CompactionUrgency::Deferred) => MaintenanceAction::IdleCompaction,
Some(CompactionUrgency::Forced) => MaintenanceAction::ExplicitCompaction,
None => MaintenanceAction::None,
}
}
pub fn should_run_idle_compaction(self) -> bool {
self.recommended_action() == MaintenanceAction::IdleCompaction
}
pub fn should_wait_for_explicit_compaction(self) -> bool {
self.recommended_action() == MaintenanceAction::ExplicitCompaction
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DocumentEncodingErrorKind {
OpenTranscodeTooLarge { max_bytes: usize },
SaveReopenTooLarge { max_bytes: usize },
PreserveSaveUnsupported,
LossyDecodedPreserve,
UnsupportedSaveTarget,
RedirectedSaveTarget { actual: DocumentEncoding },
UnrepresentableText,
}
impl std::fmt::Display for DocumentEncodingErrorKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::OpenTranscodeTooLarge { max_bytes } => write!(
f,
"non-UTF8 open currently requires full transcoding and is limited to {max_bytes} bytes"
),
Self::SaveReopenTooLarge { max_bytes } => write!(
f,
"saving to this non-UTF8 target would require reopening a full transcoded buffer and is limited to {max_bytes} bytes"
),
Self::PreserveSaveUnsupported => f.write_str(
"preserve-save is not yet supported for this encoding; use DocumentSaveOptions::with_encoding(...) to convert to a supported target",
),
Self::LossyDecodedPreserve => f.write_str(
"preserve-save is rejected because opening this document already required lossy decoding; convert explicitly if you want to keep the repaired text",
),
Self::UnsupportedSaveTarget => {
f.write_str("this encoding is not yet supported as a save target")
}
Self::RedirectedSaveTarget { actual } => write!(
f,
"encoding_rs redirected this save target to `{actual}`"
),
Self::UnrepresentableText => f.write_str(
"the current document contains characters that are not representable in the target encoding",
),
}
}
}
#[derive(Debug)]
pub enum DocumentError {
Open { path: PathBuf, source: io::Error },
Map { path: PathBuf, source: io::Error },
Write { path: PathBuf, source: io::Error },
Encoding {
path: PathBuf,
operation: &'static str,
encoding: DocumentEncoding,
reason: DocumentEncodingErrorKind,
},
EditUnsupported {
path: Option<PathBuf>,
reason: &'static str,
},
}
impl std::fmt::Display for DocumentError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Open { path, source } => write!(f, "open `{}`: {source}", path.display()),
Self::Map { path, source } => write!(f, "mmap `{}`: {source}", path.display()),
Self::Write { path, source } => write!(f, "write `{}`: {source}", path.display()),
Self::Encoding {
path,
operation,
encoding,
reason,
} => write!(
f,
"{operation} `{}` with encoding `{encoding}`: {reason}",
path.display()
),
Self::EditUnsupported { path, reason } => {
if let Some(path) = path {
write!(f, "edit `{}`: {reason}", path.display())
} else {
write!(f, "edit: {reason}")
}
}
}
}
}
impl std::error::Error for DocumentError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Open { source, .. } | Self::Map { source, .. } | Self::Write { source, .. } => {
Some(source)
}
Self::Encoding { .. } | Self::EditUnsupported { .. } => None,
}
}
}