qem 0.6.3

High-performance cross-platform text engine for massive files.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
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};

/// Named text encoding used for explicit open/save operations.
#[derive(Clone, Copy)]
pub struct DocumentEncoding(&'static Encoding);

impl DocumentEncoding {
    /// Returns the stable UTF-8 encoding used by Qem's default fast path.
    pub const fn utf8() -> Self {
        Self(UTF_8)
    }

    /// Returns UTF-16LE for BOM-backed reinterpret/open flows.
    pub const fn utf16le() -> Self {
        Self(UTF_16LE)
    }

    /// Returns UTF-16BE for BOM-backed reinterpret/open flows.
    pub const fn utf16be() -> Self {
        Self(UTF_16BE)
    }

    /// Looks up an encoding by label accepted by `encoding_rs`.
    pub fn from_label(label: &str) -> Option<Self> {
        Encoding::for_label(label.as_bytes()).map(Self)
    }

    /// Returns the canonical label for this encoding.
    pub fn name(self) -> &'static str {
        self.0.name()
    }

    /// Returns `true` when this is UTF-8.
    pub fn is_utf8(self) -> bool {
        self.0 == UTF_8
    }

    /// Returns `true` when `encoding_rs` can round-trip saves using this encoding.
    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())
    }
}

/// Describes how the current document encoding contract was chosen.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
pub enum DocumentEncodingOrigin {
    /// A new in-memory document starts with the default UTF-8 contract.
    #[default]
    NewDocument,
    /// The file was opened through Qem's default UTF-8 / ASCII fast path.
    Utf8FastPath,
    /// Lightweight auto-detection identified the current encoding from source bytes.
    AutoDetected,
    /// Auto-detection was requested but fell back to the UTF-8 fast path.
    AutoDetectFallbackUtf8,
    /// Auto-detection was requested and then fell back to an explicit caller override.
    AutoDetectFallbackOverride,
    /// The caller explicitly reinterpreted the source bytes through a chosen encoding.
    ExplicitReinterpretation,
    /// The current encoding contract came from an explicit save conversion.
    SaveConversion,
}

impl DocumentEncodingOrigin {
    /// Returns a stable lowercase identifier for logs, UI state, or JSON glue.
    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",
        }
    }

    /// Returns `true` when auto-detection participated in the current contract.
    pub const fn used_auto_detection(self) -> bool {
        matches!(
            self,
            Self::AutoDetected | Self::AutoDetectFallbackUtf8 | Self::AutoDetectFallbackOverride
        )
    }

    /// Returns `true` when the contract came from an explicit caller choice.
    pub const fn is_explicit(self) -> bool {
        matches!(
            self,
            Self::AutoDetectFallbackOverride
                | Self::ExplicitReinterpretation
                | Self::SaveConversion
        )
    }
}

/// Open policy for choosing between the UTF-8 mmap fast path, initial
/// BOM-backed detection, or an explicit reinterpretation encoding.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
pub enum OpenEncodingPolicy {
    /// Keep the existing UTF-8/ASCII mmap fast path and its current semantics.
    #[default]
    Utf8FastPath,
    /// Detect BOM-backed encodings on open and otherwise fall back to the
    /// normal UTF-8/ASCII fast path.
    ///
    /// This first detection slice intentionally avoids heavyweight legacy
    /// charset guessing so open-time cost stays predictable.
    AutoDetect,
    /// Detect BOM-backed encodings first and otherwise reinterpret the source
    /// through the requested fallback encoding.
    ///
    /// This keeps the cheap BOM-backed detection path while still letting a
    /// caller say "if you do not detect anything stronger, use this explicit
    /// encoding instead of plain UTF-8 fast-path behavior".
    AutoDetectOrReinterpret(DocumentEncoding),
    /// Reinterpret the source bytes through the requested encoding.
    ///
    /// This is the option to use for legacy encodings such as
    /// `windows-1251`, `Shift_JIS`, or `GB18030` when the caller already knows
    /// the intended source encoding.
    Reinterpret(DocumentEncoding),
}

/// Explicit document-open options for choosing between the default UTF-8 path
/// and encoding-aware reinterpretation.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
pub struct DocumentOpenOptions {
    encoding_policy: OpenEncodingPolicy,
}

impl DocumentOpenOptions {
    /// Creates open options that use Qem's default UTF-8 fast path.
    pub const fn new() -> Self {
        Self {
            encoding_policy: OpenEncodingPolicy::Utf8FastPath,
        }
    }

    /// Returns options that enable the initial BOM-backed auto-detect path.
    pub const fn with_auto_encoding_detection(mut self) -> Self {
        self.encoding_policy = OpenEncodingPolicy::AutoDetect;
        self
    }

    /// Returns options that try auto-detection first and otherwise reinterpret
    /// the source through `encoding`.
    pub const fn with_auto_encoding_detection_and_fallback(
        mut self,
        encoding: DocumentEncoding,
    ) -> Self {
        self.encoding_policy = OpenEncodingPolicy::AutoDetectOrReinterpret(encoding);
        self
    }

    /// Returns options that reinterpret the source through the given encoding.
    pub const fn with_reinterpretation(mut self, encoding: DocumentEncoding) -> Self {
        self.encoding_policy = OpenEncodingPolicy::Reinterpret(encoding);
        self
    }

    /// Returns options that force decoding the source through the given encoding.
    ///
    /// This is an alias for [`Self::with_reinterpretation`] kept for ergonomic
    /// compatibility with the first encoding-support release.
    pub const fn with_encoding(mut self, encoding: DocumentEncoding) -> Self {
        self.encoding_policy = OpenEncodingPolicy::Reinterpret(encoding);
        self
    }

    /// Returns the current open encoding policy.
    pub const fn encoding_policy(self) -> OpenEncodingPolicy {
        self.encoding_policy
    }

    /// Returns the explicit reinterpretation or fallback encoding, if one was requested.
    ///
    /// This compatibility helper returns `None` for the default fast path and
    /// for auto-detect mode.
    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,
        }
    }
}

/// Save policy for preserving the current document encoding or converting to a
/// different target encoding on write.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
pub enum SaveEncodingPolicy {
    /// Save using the document's current encoding contract.
    #[default]
    Preserve,
    /// Convert the current document text into the requested target encoding.
    Convert(DocumentEncoding),
}

/// Explicit document-save options.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
pub struct DocumentSaveOptions {
    encoding_policy: SaveEncodingPolicy,
}

impl DocumentSaveOptions {
    /// Creates save options that preserve the current document encoding.
    pub const fn new() -> Self {
        Self {
            encoding_policy: SaveEncodingPolicy::Preserve,
        }
    }

    /// Returns options that convert the current document text to `encoding` on save.
    pub const fn with_encoding(mut self, encoding: DocumentEncoding) -> Self {
        self.encoding_policy = SaveEncodingPolicy::Convert(encoding);
        self
    }

    /// Returns the encoding policy that will be used when saving.
    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 {
    /// Creates a new line slice and marks whether it is exact.
    pub fn new(text: String, exact: bool) -> Self {
        Self { text, exact }
    }

    /// Returns the slice text.
    pub fn text(&self) -> &str {
        &self.text
    }

    /// Consumes the slice and returns the owned text.
    pub fn into_text(self) -> String {
        self.text
    }

    /// Returns `true` if the slice was produced from exact indexes rather than heuristics.
    pub fn is_exact(&self) -> bool {
        self.exact
    }

    /// Returns `true` if the slice is empty.
    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()
    }
}

/// Text slice returned by typed range/selection reads.
///
/// The slice applies lossy UTF-8 decoding and tracks whether the underlying
/// range was anchored by exact document indexes or a heuristic mmap guess.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct TextSlice {
    text: String,
    exact: bool,
}

impl TextSlice {
    /// Creates a new text slice and marks whether it is exact.
    pub fn new(text: String, exact: bool) -> Self {
        Self { text, exact }
    }

    /// Returns the slice text.
    pub fn text(&self) -> &str {
        &self.text
    }

    /// Consumes the slice and returns the owned text.
    pub fn into_text(self) -> String {
        self.text
    }

    /// Returns `true` if the slice was produced from exact indexes rather than heuristics.
    pub fn is_exact(&self) -> bool {
        self.exact
    }

    /// Returns `true` if the slice is empty.
    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()
    }
}

/// Zero-based document position used by frontend integrations.
///
/// Qem keeps positions in document coordinates instead of screen coordinates so
/// applications remain free to implement their own cursor, scrollbar, and
/// selection rendering. `col0` uses document text columns: for UTF-8 text this
/// means Unicode scalar values, not grapheme clusters and not terminal/display
/// cells.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct TextPosition {
    line0: usize,
    col0: usize,
}

impl TextPosition {
    /// Creates a zero-based text position.
    pub const fn new(line0: usize, col0: usize) -> Self {
        Self { line0, col0 }
    }

    /// Returns the zero-based line index.
    pub const fn line0(self) -> usize {
        self.line0
    }

    /// Returns the zero-based document column index in text units.
    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)
    }
}

/// Typed text range used by edit operations.
///
/// The range is expressed as a starting position together with a text-unit
/// length, matching the semantics of
/// [`crate::document::Document::try_replace_range`]. For UTF-8 text, line-local
/// units are Unicode scalar values rather than grapheme clusters or display
/// cells. Between lines, a stored CRLF sequence still counts as one text unit.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
pub struct TextRange {
    start: TextPosition,
    len_chars: usize,
}

impl TextRange {
    /// Creates a text range from a starting position and text-unit length.
    pub const fn new(start: TextPosition, len_chars: usize) -> Self {
        Self { start, len_chars }
    }

    /// Creates an empty text range at the given position.
    pub const fn empty(start: TextPosition) -> Self {
        Self::new(start, 0)
    }

    /// Returns the starting position of the range.
    pub const fn start(self) -> TextPosition {
        self.start
    }

    /// Returns the number of text units in the range.
    pub const fn len_chars(self) -> usize {
        self.len_chars
    }

    /// Returns `true` when the range is empty.
    pub const fn is_empty(self) -> bool {
        self.len_chars == 0
    }
}

/// Typed literal-search match within the current document contents.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct SearchMatch {
    range: TextRange,
    end: TextPosition,
}

impl SearchMatch {
    /// Creates a search match from its range and end position.
    pub const fn new(range: TextRange, end: TextPosition) -> Self {
        Self { range, end }
    }

    /// Returns the typed range covered by the match.
    pub const fn range(self) -> TextRange {
        self.range
    }

    /// Returns the typed start position of the match.
    pub const fn start(self) -> TextPosition {
        self.range.start()
    }

    /// Returns the typed end position of the match.
    pub const fn end(self) -> TextPosition {
        self.end
    }

    /// Returns the match length in document text units.
    pub const fn len_chars(self) -> usize {
        self.range.len_chars()
    }

    /// Returns `true` when the match is empty.
    pub const fn is_empty(self) -> bool {
        self.range.is_empty()
    }

    /// Returns the match as an anchor/head selection.
    pub const fn selection(self) -> TextSelection {
        TextSelection::new(self.start(), self.end())
    }
}

/// Anchor/head text selection used by frontend integrations.
///
/// Qem keeps this selection in document coordinates so applications remain
/// free to own their own painting, cursor visuals, and interaction model.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
pub struct TextSelection {
    anchor: TextPosition,
    head: TextPosition,
}

impl TextSelection {
    /// Creates a selection from an anchor and active head position.
    pub const fn new(anchor: TextPosition, head: TextPosition) -> Self {
        Self { anchor, head }
    }

    /// Creates a caret selection at a single position.
    pub const fn caret(position: TextPosition) -> Self {
        Self::new(position, position)
    }

    /// Returns the anchor position.
    pub const fn anchor(self) -> TextPosition {
        self.anchor
    }

    /// Returns the active head position.
    pub const fn head(self) -> TextPosition {
        self.head
    }

    /// Returns `true` when the selection is only a caret.
    pub fn is_caret(self) -> bool {
        self.anchor == self.head
    }
}

/// Viewport request used by frontend code to read only visible rows.
#[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 {
    /// Creates a viewport request for a contiguous line range.
    ///
    /// Columns default to the full visible width of the line. Horizontal
    /// columns use the same document text-unit semantics as [`TextPosition`].
    pub const fn new(first_line0: usize, line_count: usize) -> Self {
        Self {
            first_line0,
            line_count,
            start_col: 0,
            max_cols: usize::MAX,
        }
    }

    /// Sets the horizontal slice within each requested row.
    ///
    /// `start_col` and `max_cols` count document text columns, not grapheme
    /// clusters and not display cells.
    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
    }

    /// Returns the first requested zero-based line index.
    pub const fn first_line0(self) -> usize {
        self.first_line0
    }

    /// Returns the requested number of lines.
    pub const fn line_count(self) -> usize {
        self.line_count
    }

    /// Returns the requested starting column.
    pub const fn start_col(self) -> usize {
        self.start_col
    }

    /// Returns the requested maximum number of columns.
    pub const fn max_cols(self) -> usize {
        self.max_cols
    }
}

/// One row returned by a viewport read.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ViewportRow {
    line0: usize,
    slice: LineSlice,
}

impl ViewportRow {
    /// Creates a viewport row.
    pub fn new(line0: usize, slice: LineSlice) -> Self {
        Self { line0, slice }
    }

    /// Returns the zero-based line index for this row.
    pub fn line0(&self) -> usize {
        self.line0
    }

    /// Returns the 1-based line number for display.
    pub fn line_number(&self) -> usize {
        self.line0.saturating_add(1)
    }

    /// Returns the rendered line slice.
    pub fn slice(&self) -> &LineSlice {
        &self.slice
    }

    /// Consumes the row and returns the line slice.
    pub fn into_slice(self) -> LineSlice {
        self.slice
    }

    /// Returns the row text.
    pub fn text(&self) -> &str {
        self.slice.text()
    }

    /// Returns `true` when the row is backed by exact line indexes.
    pub fn is_exact(&self) -> bool {
        self.slice.is_exact()
    }
}

/// Viewport read response returned by [`crate::document::Document::read_viewport`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Viewport {
    request: ViewportRequest,
    total_lines: LineCount,
    rows: Vec<ViewportRow>,
}

impl Viewport {
    /// Creates a viewport response.
    pub fn new(request: ViewportRequest, total_lines: LineCount, rows: Vec<ViewportRow>) -> Self {
        Self {
            request,
            total_lines,
            rows,
        }
    }

    /// Returns the request that produced this viewport.
    pub fn request(&self) -> ViewportRequest {
        self.request
    }

    /// Returns the current total document line count.
    pub fn total_lines(&self) -> LineCount {
        self.total_lines
    }

    /// Returns the visible rows.
    pub fn rows(&self) -> &[ViewportRow] {
        &self.rows
    }

    /// Consumes the viewport and returns the visible rows.
    pub fn into_rows(self) -> Vec<ViewportRow> {
        self.rows
    }

    /// Returns the number of visible rows.
    pub fn len(&self) -> usize {
        self.rows.len()
    }

    /// Returns `true` when the viewport contains no rows.
    pub fn is_empty(&self) -> bool {
        self.rows.is_empty()
    }
}

/// Result of an edit command together with the resulting cursor position.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct EditResult {
    changed: bool,
    cursor: TextPosition,
}

impl EditResult {
    /// Creates an edit result.
    pub const fn new(changed: bool, cursor: TextPosition) -> Self {
        Self { changed, cursor }
    }

    /// Returns `true` when the document changed.
    pub const fn changed(self) -> bool {
        self.changed
    }

    /// Returns the resulting cursor position.
    pub const fn cursor(self) -> TextPosition {
        self.cursor
    }
}

/// Result of cutting a selection together with the resulting edit outcome.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct CutResult {
    text: String,
    edit: EditResult,
}

impl CutResult {
    /// Creates a cut result.
    pub fn new(text: String, edit: EditResult) -> Self {
        Self { text, edit }
    }

    /// Returns the cut text.
    pub fn text(&self) -> &str {
        &self.text
    }

    /// Consumes the result and returns the owned cut text.
    pub fn into_text(self) -> String {
        self.text
    }

    /// Returns the underlying edit result.
    pub const fn edit(&self) -> EditResult {
        self.edit
    }

    /// Returns `true` when the document changed.
    pub const fn changed(&self) -> bool {
        self.edit.changed()
    }

    /// Returns the resulting cursor position.
    pub const fn cursor(&self) -> TextPosition {
        self.edit.cursor()
    }
}

/// Typed byte progress used by indexing and other document-local background work.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct ByteProgress {
    completed_bytes: usize,
    total_bytes: usize,
}

impl ByteProgress {
    /// Creates a byte progress value.
    pub const fn new(completed_bytes: usize, total_bytes: usize) -> Self {
        Self {
            completed_bytes,
            total_bytes,
        }
    }

    /// Returns the completed byte count.
    pub const fn completed_bytes(self) -> usize {
        self.completed_bytes
    }

    /// Returns the total byte count.
    pub const fn total_bytes(self) -> usize {
        self.total_bytes
    }

    /// Returns completion as a `0.0..=1.0` fraction.
    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
        }
    }
}

/// Total document line count, represented either as an exact value or as a
/// scrolling estimate while background indexing is still incomplete.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[must_use]
pub enum LineCount {
    Exact(usize),
    Estimated(usize),
}

impl LineCount {
    /// Returns the exact line count when it is known.
    pub fn exact(self) -> Option<usize> {
        match self {
            Self::Exact(lines) => Some(lines),
            Self::Estimated(_) => None,
        }
    }

    /// Returns the value that should be used for viewport sizing and scrolling.
    pub fn display_rows(self) -> usize {
        match self {
            Self::Exact(lines) | Self::Estimated(lines) => lines.max(1),
        }
    }

    /// Returns `true` when the total line count is exact.
    pub fn is_exact(self) -> bool {
        matches!(self, Self::Exact(_))
    }
}

/// Current backing mode of the document text.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DocumentBacking {
    Mmap,
    PieceTable,
    Rope,
}

impl DocumentBacking {
    /// Returns a short display label for the current backing mode.
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Mmap => "mmap",
            Self::PieceTable => "piece-table",
            Self::Rope => "rope",
        }
    }
}

/// Typed editability state for a document position or range.
#[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 {
    /// Returns `true` when an edit can proceed, possibly after a backend promotion.
    pub const fn is_editable(self) -> bool {
        !matches!(self, Self::Unsupported { .. })
    }

    /// Returns `true` when the edit would require promoting to another backing.
    pub const fn requires_promotion(self) -> bool {
        matches!(self, Self::RequiresPromotion { .. })
    }

    /// Returns the current backing mode before any edit is attempted.
    pub const fn current_backing(self) -> DocumentBacking {
        match self {
            Self::Editable { backing } | Self::Unsupported { backing, .. } => backing,
            Self::RequiresPromotion { from, .. } => from,
        }
    }

    /// Returns the resulting backing mode after promotion, if one is required.
    pub const fn target_backing(self) -> Option<DocumentBacking> {
        match self {
            Self::RequiresPromotion { to, .. } => Some(to),
            _ => None,
        }
    }

    /// Returns an unsupported-edit reason when one is available.
    pub const fn reason(self) -> Option<&'static str> {
        match self {
            Self::Unsupported { reason, .. } => Some(reason),
            _ => None,
        }
    }
}

/// Snapshot of the current document state for frontend polling.
#[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 {
    /// Creates a document status snapshot.
    #[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,
        }
    }

    /// Returns the current document path, if one is set.
    pub fn path(&self) -> Option<&Path> {
        self.path.as_deref()
    }

    /// Returns `true` when the document has unsaved changes.
    pub fn is_dirty(&self) -> bool {
        self.dirty
    }

    /// Returns the current document length in bytes.
    pub fn file_len(&self) -> usize {
        self.file_len
    }

    /// Returns the current document line count.
    pub fn line_count(&self) -> LineCount {
        self.line_count
    }

    /// Returns the exact line count when it is known.
    pub fn exact_line_count(&self) -> Option<usize> {
        self.line_count.exact()
    }

    /// Returns the best-effort line count for viewport sizing and scrolling.
    pub fn display_line_count(&self) -> usize {
        self.line_count.display_rows()
    }

    /// Returns `true` when the current line count is exact.
    pub fn is_line_count_exact(&self) -> bool {
        self.line_count.is_exact()
    }

    /// Returns `true` when background work may still upgrade the total line
    /// count from an estimate to an exact value.
    pub fn is_line_count_pending(&self) -> bool {
        self.exact_line_count_pending
    }

    /// Returns the currently detected line ending style.
    pub fn line_ending(&self) -> LineEnding {
        self.line_ending
    }

    /// Returns the current document encoding contract.
    pub fn encoding(&self) -> DocumentEncoding {
        self.encoding
    }

    /// Returns the typed reason why preserve-save would currently fail, if any.
    pub fn preserve_save_error(&self) -> Option<DocumentEncodingErrorKind> {
        self.preserve_save_error
    }

    /// Returns `true` when preserve-save is currently allowed for this document snapshot.
    pub fn can_preserve_save(&self) -> bool {
        self.preserve_save_error().is_none()
    }

    /// Returns how the current encoding contract was chosen.
    pub fn encoding_origin(&self) -> DocumentEncodingOrigin {
        self.encoding_origin
    }

    /// Returns `true` when opening the source required lossy decode replacement.
    pub fn decoding_had_errors(&self) -> bool {
        self.decoding_had_errors
    }

    /// Returns typed indexing progress while background indexing is active.
    pub fn indexing_state(&self) -> Option<ByteProgress> {
        self.indexing
    }

    /// Returns `true` when document-local indexing is still running.
    pub fn is_indexing(&self) -> bool {
        self.indexing.is_some()
    }

    /// Returns the current document backing mode.
    pub fn backing(&self) -> DocumentBacking {
        self.backing
    }

    /// Returns `true` when the document currently uses any edit buffer.
    pub fn has_edit_buffer(&self) -> bool {
        !matches!(self.backing, DocumentBacking::Mmap)
    }

    /// Returns `true` when the document is currently backed by a rope.
    pub fn has_rope(&self) -> bool {
        matches!(self.backing, DocumentBacking::Rope)
    }

    /// Returns `true` when the document is currently backed by a piece table.
    pub fn has_piece_table(&self) -> bool {
        matches!(self.backing, DocumentBacking::PieceTable)
    }
}

/// Snapshot of maintenance-oriented document state such as fragmentation and
/// compaction advice.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct DocumentMaintenanceStatus {
    backing: DocumentBacking,
    fragmentation: Option<FragmentationStats>,
    compaction: Option<CompactionRecommendation>,
}

/// High-level maintenance action suggested by the current compaction policy.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MaintenanceAction {
    /// No maintenance work is currently recommended.
    None,
    /// The frontend can run idle compaction now.
    IdleCompaction,
    /// Heavier maintenance should wait for an explicit operator/save boundary.
    ExplicitCompaction,
}

impl MaintenanceAction {
    /// Returns a stable lowercase identifier for logs, JSON output, or UI glue.
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::None => "none",
            Self::IdleCompaction => "idle-compaction",
            Self::ExplicitCompaction => "explicit-compaction",
        }
    }
}

impl DocumentMaintenanceStatus {
    /// Creates a maintenance status snapshot.
    pub const fn new(
        backing: DocumentBacking,
        fragmentation: Option<FragmentationStats>,
        compaction: Option<CompactionRecommendation>,
    ) -> Self {
        Self {
            backing,
            fragmentation,
            compaction,
        }
    }

    /// Returns the current document backing mode.
    pub const fn backing(self) -> DocumentBacking {
        self.backing
    }

    /// Returns `true` when the document currently uses a piece table.
    pub const fn has_piece_table(self) -> bool {
        matches!(self.backing, DocumentBacking::PieceTable)
    }

    /// Returns fragmentation metrics when they are meaningful for the current backing.
    pub const fn fragmentation_stats(self) -> Option<FragmentationStats> {
        self.fragmentation
    }

    /// Returns `true` when fragmentation metrics are available.
    pub const fn has_fragmentation_stats(self) -> bool {
        self.fragmentation.is_some()
    }

    /// Returns the current compaction recommendation, if any.
    pub const fn compaction_recommendation(self) -> Option<CompactionRecommendation> {
        self.compaction
    }

    /// Returns `true` when the current maintenance policy recommends compaction.
    pub const fn is_compaction_recommended(self) -> bool {
        self.compaction.is_some()
    }

    /// Returns the current compaction urgency, if a recommendation exists.
    pub fn compaction_urgency(self) -> Option<CompactionUrgency> {
        self.compaction
            .map(|recommendation| recommendation.urgency())
    }

    /// Returns the high-level maintenance action implied by this snapshot.
    pub fn recommended_action(self) -> MaintenanceAction {
        match self.compaction_urgency() {
            Some(CompactionUrgency::Deferred) => MaintenanceAction::IdleCompaction,
            Some(CompactionUrgency::Forced) => MaintenanceAction::ExplicitCompaction,
            None => MaintenanceAction::None,
        }
    }

    /// Returns `true` when idle compaction is currently recommended.
    pub fn should_run_idle_compaction(self) -> bool {
        self.recommended_action() == MaintenanceAction::IdleCompaction
    }

    /// Returns `true` when heavier maintenance should be deferred to an
    /// explicit operator/save boundary.
    pub fn should_wait_for_explicit_compaction(self) -> bool {
        self.recommended_action() == MaintenanceAction::ExplicitCompaction
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DocumentEncodingErrorKind {
    /// Opening this encoding would require a full transcode beyond the current safety limit.
    OpenTranscodeTooLarge { max_bytes: usize },
    /// Saving to this encoding would succeed, but reopening the saved document
    /// would require a full transcode beyond the current safety limit.
    SaveReopenTooLarge { max_bytes: usize },
    /// Preserving the current decoded encoding contract is not supported on save yet.
    PreserveSaveUnsupported,
    /// Preserving the current decoded encoding would cement a lossy open.
    LossyDecodedPreserve,
    /// The requested save target is not yet supported as a direct output encoding.
    UnsupportedSaveTarget,
    /// `encoding_rs` redirected the save target to a different output encoding.
    RedirectedSaveTarget { actual: DocumentEncoding },
    /// The current document text cannot be represented in the requested encoding.
    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",
            ),
        }
    }
}

/// File-system, mapping, and edit-capability errors produced by [`crate::document::Document`].
#[derive(Debug)]
pub enum DocumentError {
    /// The source file could not be opened.
    Open { path: PathBuf, source: io::Error },
    /// The source file could not be memory-mapped.
    Map { path: PathBuf, source: io::Error },
    /// A write, rename, or reload step failed.
    Write { path: PathBuf, source: io::Error },
    /// Encoding negotiation, decode, or save conversion failed.
    Encoding {
        path: PathBuf,
        operation: &'static str,
        encoding: DocumentEncoding,
        reason: DocumentEncodingErrorKind,
    },
    /// The requested edit operation is unsupported for the current document state.
    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,
        }
    }
}