Cairn.Text.Proofs
lean/Cairn/Text/Proofs.lean
Theorems about Cairn.Text.Field
- In bounds.
Field.wf_empty,Field.wf_ofString,Field.wf_apply,Field.wf_applyAll,Field.wf_reachable,Field.view_wf: caret, anchor, marked range and every undo and redo entry stay within0 … length, from any well-formed field, for every op. - Round trip.
Field.insert_deleteBackward,Field.insert_deleteBackward': typingsat a collapsed caret, then deleting backwards.lengthtimes, restores the text and selection. - Undo.
Field.undo_apply(one edit, outside a typing run or composition),Field.undo_typing_run(a run of coalesced typing),Field.undo_composition(a whole IME composition),Field.redo_undo. - Selection replace.
Field.insert_replaces_selection,Field.insert_replaces_selection',Field.insert_replace. - Copy is pure.
Field.apply_copy,Field.selected_spec. - Movement never edits.
Field.apply_movement_text. - Single line.
Field.singleLine_apply,Field.view_no_newline(andWF.single). - Secure fields.
SecureField.apply_no_clipboard(copy and cut, and every other op, never set the clipboard;SecureField.apply_copy,SecureField.apply_cutbeep and change nothing),SecureField.history_empty,SecureField.applyAll_history(no undo or redo history, ever),SecureField.apply_edit(every other op edits asField.applydoes),SecureField.wf_apply,SecureField.view_text(the view holds a bullet per scalar) andSecureField.view_agree(the view depends on the text only through its length).
Imports Cairn.Text.Field
Definitions 5
isMovement : TextOp → Bool
isHistory : TextOp → Bool
isTyping : TextOp → Bool
Ops that join an open typing step.
ChangesEach : Field → List TextOp → Prop
Every op in ops changes the text when applied in turn.
isRefused : TextOp → Bool
Ops a secure field refuses.
Theorems 72
span_le (p : Char → Bool) (l : List Char) : span p l ≤ l.length
wordLeft_le (t : List Char) (i : Nat) : wordLeft t i ≤ i
le_wordRight (t : List Char) (i : Nat) : i ≤ wordRight t i
wordRight_le (t : List Char) (i : Nat) (h : i ≤ t.length) : wordRight t i ≤ t.length
length_splice (t s : List Char) (a b : Nat) (hab : a ≤ b) (hb : b ≤ t.length) : (splice t a b s).length = a + s.length + (t.length - b)
singleLine_splice {t s : List Char} (a b : Nat) (ht : SingleLine t)
(hs : SingleLine s) : SingleLine (splice t a b s)
singleLine_nil : SingleLine []
singleLine_flatten (c : Char) : isLineBreak (flatten c) = false
singleLine_clean (s : String) : SingleLine (clean s)
length_clean (s : String) : (clean s).length = s.length
map_flatten_of_singleLine {l : List Char} : SingleLine l → List.map flatten l = l
clean_of_singleLine {s : String} (h : SingleLine s.toList) : clean s = s.toList
wf_empty (p : String) : (empty p).WF
wf_ofString (s p : String) : (ofString s p).WF
snapshot_wf {f : Field} (h : f.WF) : f.snapshot.WF
lo_le_hi (f : Field) : f.lo ≤ f.hi
hi_le {f : Field} (h : f.WF) : f.hi ≤ f.text.length
target_wf {f : Field} (h : f.WF) :
f.target.fst ≤ f.target.snd ∧ f.target.snd ≤ f.text.length
targetOr_wf {f : Field} (h : f.WF) (r : Option (Nat × Nat)) :
(f.targetOr r).fst ≤ (f.targetOr r).snd ∧ (f.targetOr r).snd ≤ f.text.length
edit_text (f : Field) (t : List Char) (c a : Nat) (p ty : Bool) : (f.edit t c a p ty).text = t
edit_caret (f : Field) (t : List Char) (c a : Nat) (p ty : Bool) : (f.edit t c a p ty).caret = c
edit_anchor (f : Field) (t : List Char) (c a : Nat) (p ty : Bool) : (f.edit t c a p ty).anchor = a
edit_marked (f : Field) (t : List Char) (c a : Nat) (p ty : Bool) : (f.edit t c a p ty).marked = none
wf_edit {f : Field} (h : f.WF) {t : List Char} {c a : Nat} (p ty : Bool)
(ht : SingleLine t) (hc : c ≤ t.length) (ha : a ≤ t.length) : (f.edit t c a p ty).WF
wf_moveTo {f : Field} (h : f.WF) {i : Nat} (hi : i ≤ f.text.length)
(ext : Bool) : (f.moveTo i ext).WF
wf_deleteRange {f : Field} (h : f.WF) {a b : Nat} (hab : a ≤ b)
(hb : b ≤ f.text.length) (co : Bool := false) : (f.deleteRange a b co).WF
wf_deleteSelOr {f : Field} (h : f.WF) {a b : Nat} (hab : a ≤ b)
(hb : b ≤ f.text.length) (co : Bool := false) : (f.deleteSelOr a b co).WF
wf_insert {f : Field} (h : f.WF) (s : String)
(r : Option (Nat × Nat) := none) : (f.insert s r).WF
wf_paste {f : Field} (h : f.WF) (s : String) : (f.paste s).WF
mem_push {p : Prop} [Decidable p] {t x : Snapshot} {l : List Snapshot}
(h : t ∈ if p then x :: l else l) : t = x ∨ t ∈ l
mem_clear {p : Prop} [Decidable p] {t : Snapshot} {l : List Snapshot}
(h : t ∈ if p then [] else l) : t ∈ l
wf_setMarked {f : Field} (h : f.WF) (s : String) (i n : Nat)
(r : Option (Nat × Nat) := none) : (f.setMarked s i n r).WF
wf_undo {f : Field} (h : f.WF) : f.undo.fst.WF
wf_redo {f : Field} (h : f.WF) : f.redo.fst.WF
wf_apply (op : TextOp) {f : Field} (h : f.WF) : (apply op f).fst.WF
Every op keeps every index (caret, anchor, marked range, and every undo and redo entry)
within 0 … length, and the text a single line.
wf_applyAll (ops : List TextOp) {f : Field} : f.WF → (f.applyAll ops).WF
wf_reachable (p : String) (ops : List TextOp) : ((empty p).applyAll ops).WF
Every field reachable from an empty one is well-formed.
view_wf {f : Field} (h : f.WF) (focused : Bool) :
(f.view focused).caret ≤ (f.view focused).text.length ∧
(f.view focused).anchor ≤ (f.view focused).text.length ∧
∀ (a b : Nat),
(f.view focused).marked = some (a, b) → a ≤ b ∧ b ≤ (f.view focused).text.lengthWhat the view layer is given is in bounds too.
singleLine_apply (op : TextOp) {f : Field} (h : f.WF) : SingleLine (apply op f).fst.text
view_no_newline {f : Field} (h : f.WF) (focused : Bool) :
¬'\n' ∈ (f.view focused).text.toList
apply_movement_text (op : TextOp) (hop : op.isMovement = true) (f : Field) : (apply op f).fst.text = f.text ∧ (apply op f).snd = []
apply_unmark_text (f : Field) : (apply TextOp.unmark f).fst.text = f.text
Unmark and copy don't edit either.
apply_copy (f : Field) : apply TextOp.copy f = (f, if f.lo < f.hi then [Effect.setClipboard (String.ofList f.selected)] else [])
selected_spec {f : Field} (h : f.WF) :
List.take f.lo f.text ++ f.selected ++ List.drop f.hi f.text = f.text ∧
f.selected.length = f.hi - f.loThe copied string is exactly text[lo, hi).
insert_replaces_selection (f : Field) (s : String) (hm : f.marked = none) : (apply (TextOp.insert s) f).fst.text = List.take f.lo f.text ++ clean s ++ List.drop f.hi f.text ∧ (apply (TextOp.insert s) f).fst.caret = f.lo + (clean s).length ∧ (apply (TextOp.insert s) f).fst.anchor = f.lo + (clean s).length
Inserting over a selection (with no composition) gives before ++ s ++ after, with line
breaks removed from s, and the caret after s.
insert_replaces_selection' (f : Field) (s : String) (hm : f.marked = none) (hs : SingleLine s.toList) : (apply (TextOp.insert s) f).fst.text = List.take f.lo f.text ++ s.toList ++ List.drop f.hi f.text
The same, for a string with no line breaks: exactly before ++ s ++ after.
insert_replace (f : Field) (s : String) {a b : Nat} (hab : a ≤ b) (hb : b ≤ f.text.length) : (f.insert s (some (a, b))).text = List.take a f.text ++ clean s ++ List.drop b f.text ∧ (f.insert s (some (a, b))).caret = a + (clean s).length
With a replacement range from the input system, insert replaces exactly that range.
deleteBackward_collapsed (g : Field) (A B : List Char) (x : Char) (ht : g.text = A ++ x :: B) (hc : g.caret = A.length + 1) (ha : g.anchor = g.caret) : (apply TextOp.deleteBackward g).fst.text = A ++ B ∧ (apply TextOp.deleteBackward g).fst.caret = A.length ∧ (apply TextOp.deleteBackward g).fst.anchor = A.length
iterate_deleteBackward (B u A : List Char) (g : Field) : g.text = A ++ u ++ B → g.caret = A.length + u.length → g.anchor = g.caret → (iterate TextOp.deleteBackward u.length g).text = A ++ B ∧ (iterate TextOp.deleteBackward u.length g).caret = A.length ∧ (iterate TextOp.deleteBackward u.length g).anchor = A.length
insert_deleteBackward {f : Field} (h : f.WF) (s : String)
(hc : f.anchor = f.caret) (hm : f.marked = none) :
have g := iterate TextOp.deleteBackward (clean s).length (apply (TextOp.insert s) f).fst;
g.text = f.text ∧ g.caret = f.caret ∧ g.anchor = f.anchorTyping a non-empty string at a collapsed caret, then deleting backward once per scalar, restores the text and the caret.
insert_deleteBackward' {f : Field} (h : f.WF) (s : String)
(hc : f.anchor = f.caret) (hm : f.marked = none) :
have g := iterate TextOp.deleteBackward s.length (apply (TextOp.insert s) f).fst;
g.text = f.text ∧ g.caret = f.caret ∧ g.anchor = f.anchorThe same in terms of the string typed: s.length deletions (each line break in s became
one space, so the count is unchanged). For s without line breaks, the text in between is
exactly before ++ s ++ after (insert_replaces_selection').
edit_undoStack_of_ne (f : Field) {t : List Char} (c a : Nat) (ty : Bool) (ht : t ≠ f.text) : (f.edit t c a true ty).undoStack = f.snapshot :: f.undoStack
apply_pushes (op : TextOp) (f : Field) (hop : op.isHistory = false) (htyping : f.typing = false) (hm : f.marked = none) (hch : (apply op f).fst.text ≠ f.text) : (apply op f).fst.undoStack = f.snapshot :: f.undoStack
An edit that changes the text and starts a new undo step pushes exactly the state before it.
undo_apply (op : TextOp) (f : Field) (hop : op.isHistory = false) (htyping : f.typing = false) (hm : f.marked = none) (hch : (apply op f).fst.text ≠ f.text) : have g := (apply op f).fst.undo; g.fst.text = f.text ∧ g.fst.caret = f.caret ∧ g.fst.anchor = f.anchor ∧ g.fst.marked = none ∧ g.fst.undoStack = f.undoStack ∧ g.snd = []
Undo restores. After an op that changes the text, from a field that is not mid-typing
(typing = false) and not composing (marked = none), undo restores the text and the
selection exactly, ends any composition, and can be redone.
undo_of_stack (k : Field) {s : Snapshot} {rest : List Snapshot} (h : k.undoStack = s :: rest) : k.undo.fst.text = s.text ∧ k.undo.fst.caret = s.caret ∧ k.undo.fst.anchor = s.anchor
Undo restores whatever is on top of the undo stack.
typing_step (op : TextOp) (g : Field) (hop : op.isTyping = true) (hm : g.marked = none) (hc : g.anchor = g.caret) (hch : (apply op g).fst.text ≠ g.text) : (apply op g).fst.typing = true ∧ (apply op g).fst.marked = none ∧ (apply op g).fst.anchor = (apply op g).fst.caret ∧ (apply op g).fst.undoStack = if g.typing = true then g.undoStack else g.snapshot :: g.undoStack
One typing op at a collapsed caret that changes the text: it leaves the caret collapsed, no composition, the typing step open, and pushes the state before it only if no typing step was open.
typing_run (ops : List TextOp) (g : Field) (s : Snapshot) (rest : List Snapshot) (hops : ∀ (o : TextOp), o ∈ ops → o.isTyping = true) (hch : g.ChangesEach ops) (ht : g.typing = true) (hm : g.marked = none) (hc : g.anchor = g.caret) (hu : g.undoStack = s :: rest) : (g.applyAll ops).undoStack = s :: rest
undo_typing_run (f : Field) (op : TextOp) (ops : List TextOp) (hop : op.isTyping = true) (hops : ∀ (o : TextOp), o ∈ ops → o.isTyping = true) (htyping : f.typing = false) (hm : f.marked = none) (hc : f.anchor = f.caret) (hch : f.ChangesEach (op :: ops)) : have g := (f.applyAll (op :: ops)).undo.fst; g.text = f.text ∧ g.caret = f.caret ∧ g.anchor = f.anchor
Coalesced typing. A run of typing (inserts, and single-scalar deletes) at a collapsed caret, each step changing the text, is one undo step: undo after the whole run restores the text and selection from before its first keystroke.
undo_composition (f : Field) (hm : f.marked = none) (s : String) (i n : Nat) (more : List (String × Nat × Nat)) (hs : 0 < s.length) (hmore : ∀ (x : String × Nat × Nat), x ∈ more → 0 < x.fst.length) : have ops := TextOp.setMarked s i n :: List.map (fun x => TextOp.setMarked x.fst x.snd.fst x.snd.snd) more ++ [TextOp.unmark]; have g := (f.applyAll ops).undo.fst; g.text = f.text ∧ g.caret = f.caret ∧ g.anchor = f.anchor
redo_undo (f : Field) (hu : f.undoStack ≠ []) : have g := f.undo.fst.redo.fst; g.text = f.text ∧ g.caret = f.caret ∧ g.anchor = f.anchor
Redo undoes undo. After an undo, redo restores the text and selection undo replaced.
field_apply_effects (op : TextOp) (hop : op.isRefused = false) (f : Field) : (Field.apply op f).snd = []
The ops it doesn't refuse emit nothing on a plain field.
apply_refused (op : TextOp) (hop : op.isRefused = true) (f : SecureField) : apply op f = ({ field := forget f.field }, [Effect.beep])
apply_other (op : TextOp) (hop : op.isRefused = false) (f : SecureField) : apply op f = ({ field := forget (Field.apply op f.field).fst }, [])
apply_copy (f : SecureField) : apply TextOp.copy f = ({ field := forget f.field }, [Effect.beep])
Copy is refused: it beeps, and the clipboard and the text are untouched.
apply_cut (f : SecureField) : apply TextOp.cut f = ({ field := forget f.field }, [Effect.beep])
Cut is refused: it beeps and deletes nothing.
apply_effects (op : TextOp) (f : SecureField) (e : Effect) : e ∈ (apply op f).snd → e = Effect.beep
No op on a secure field sets the clipboard: the only effect any op can have is a beep.
apply_no_clipboard (op : TextOp) (f : SecureField) (s : String) : ¬Effect.setClipboard s ∈ (apply op f).snd
history_empty (op : TextOp) (f : SecureField) : (apply op f).fst.field.undoStack = [] ∧ (apply op f).fst.field.redoStack = []
No history: after any op, the undo and redo stacks are empty.
applyAll_history (p : String) (ops : List TextOp) : ((empty p).applyAll ops).field.undoStack = [] ∧ ((empty p).applyAll ops).field.redoStack = []
From an empty field, history stays empty through any ops.
apply_edit (op : TextOp) (hop : op.isRefused = false) (f : SecureField) : (apply op f).fst.field.text = (Field.apply op f.field).fst.text ∧ (apply op f).fst.field.caret = (Field.apply op f.field).fst.caret ∧ (apply op f).fst.field.anchor = (Field.apply op f.field).fst.anchor ∧ (apply op f).fst.field.marked = (Field.apply op f.field).fst.marked
Editing is a field's: every op a secure field doesn't refuse changes its text, selection
and composition exactly as Field.apply does.
forget_wf {f : Field} (h : f.WF) : (forget f).WF
wf_apply (op : TextOp) {f : SecureField} (h : f.field.WF) : (apply op f).fst.field.WF