yy and dd copy whole rows, so pasting them back with p or P carries the rows' line formats as a whole-row paste does. A block cut and pasted leaves the cells it came from plain, as Sheets clears cut cells: blank cells under a formatted column, row or sheet get plain formatting of their own (bounded as other formatting changes are), except where the block lands. Undo restores them.
- commit
- ec1f34fce029b61131c0e2a396ae9ce68fef8f3d
- parent
- aebff00858f611693da906c096142913e8f3dd3c
- author
- Dami <dami.etoile@gmail.com>
| M | docs/ | 5 | +++- |
| M | docs/ | 2 | +- |
| M | internal/ | 39 | +++++++++++++++++++++++++++++- |
| M | internal/ | 33 | +++++++++++++++++++++++-- |
| M | internal/ | 26 | ++++++++++++++++++++ |
| M | internal/ | 9 | ++----- |
@@ -24,7 +24,10 @@ Shift+Space) copied and pasted at the top of a column, or the start of a
2424 row, take their column or row formats along, so a pasted column is2525 currency all the way down, not only where it had data; cut and pasted,2626 they move them, leaving the source columns plain. Pasted anywhere else,27-they paste as a block of the cells that hold something. Formulas reading27+they paste as a block of the cells that hold something. A block cut and28+pasted leaves the cells it came from plain, as Sheets does, even where29+a formatted column or row crosses them. Vim's `yy` and `dd` copy whole30+rows, so `p` and `P` bring their row formats along. Formulas reading2831 a column whose format changes, blank cells included, show the new format2932 at once (`=B5*2` shows currency when column B becomes currency). Copies also go to the3033 system clipboard as tab-separated text (OSC 52, so it works over SSH), and@@ -83,7 +83,7 @@ are taken.
8383 | `o` `O` | Insert a row below or above and start typing in it |8484 | `x` | Clear the cell (with a count, that many to the right) |8585 | `dd` `yy` | Cut or copy the row (with a count, that many rows); `p` pastes them back |86-| `p` `P` | Paste rows cut or copied with `dd` or `yy` as new rows below or above; other copied cells paste at the active cell |86+| `p` `P` | Paste rows cut or copied with `dd` or `yy` as new rows below or above, with their row formats; other copied cells paste at the active cell |8787 | `u`, Ctrl+R | Undo, redo |8888 | `v` `V` | Select cells or whole rows (VISUAL); motions stretch the selection |8989 | `/`, `n` `N` | Find (Enter stays on the match), next and previous match |@@ -12,7 +12,8 @@ import (
1212 // already show. Copying whole columns or rows and pasting them as whole1313 // lines (at the top of a column, the start of a row, or A1 for the whole1414 // sheet) carries the line formats themselves, and cutting them moves the15-// line formats, leaving the source lines plain.15+// line formats, leaving the source lines plain. Cutting a block leaves16+// its cells plain, over whatever lines cross it.1617 1718 // clipFormats is the formatting of a copied range, taken at copy time.1819 type clipFormats struct {@@ -256,10 +257,46 @@ func (s *Sheet) moveFormats(from *Sheet, f *clipFormats, src, dst Rect) {
256257 for n := range f.rowFmt {257258 from.setLine(true, src.From.Row+n, lineFmt{})258259 }260+ case lineBlock:261+ from.plainSource(src, dst, from == s)259262 }260263 s.pasteFormats(f, p)261264 }262265 266+// plainSource leaves the cells a block was cut from showing plain, as267+// Sheets clears them: where a formatted row, column or the sheet crosses268+// src, its blank cells get plain formatting of their own, up to269+// materializeLimit cells looked at, except those in dst when the block270+// lands on the same sheet.271+func (s *Sheet) plainSource(src, dst Rect, same bool) {272+ var areas []Rect273+ if !s.lines.sheet.IsZero() {274+ areas = []Rect{src}275+ } else {276+ for n := range linesIn(s.lines.rows, src.From.Row, src.To.Row) {277+ row := src.From.Row + n278+ areas = append(areas, Rect{From: Addr{Col: src.From.Col, Row: row}, To: Addr{Col: src.To.Col, Row: row}})279+ }280+ for n := range linesIn(s.lines.cols, src.From.Col, src.To.Col) {281+ col := src.From.Col + n282+ areas = append(areas, Rect{From: Addr{Col: col, Row: src.From.Row}, To: Addr{Col: col, Row: src.To.Row}})283+ }284+ }285+ seen := 0286+ for _, r := range areas {287+ for row := r.From.Row; row <= r.To.Row; row++ {288+ for c := r.From.Col; c <= r.To.Col; c++ {289+ if seen++; seen > materializeLimit {290+ return291+ }292+ if a := (Addr{Col: c, Row: row}); !(same && dst.Contains(a)) && s.cells.get(a) == nil {293+ s.placeFormat(a, lineFmt{})294+ }295+ }296+ }297+ }298+}299+263300 // unionKeys are the keys of a and b, in order.264301 func unionKeys(a, b map[int]lineFmt) []int {265302 keys := slices.Collect(maps.Keys(a))@@ -160,13 +160,40 @@ func TestMoveCarriesFormats(t *testing.T) {
160160 t.Errorf("Other!C1 shows %v %v; B1 %v", o.DisplayFormat(at("C1")).Kind, o.Value(at("C1")), s.DisplayFormat(at("B1")).Kind)161161 }162162 163- // A block keeps its formats; the source falls back on its lines.163+ // A block keeps its formats; the source is left plain, as Sheets164+ // clears cut cells, though its column stays currency.164165 o.Set(at("C2"), "7")165166 if _, err := o.MoveTo(s, Rect{From: at("C1"), To: at("C2")}, at("E4")); err != nil {166167 t.Fatal(err)167168 }168- if s.DisplayFormat(at("E5")).Kind != FmtCurrency || o.DisplayFormat(at("C1")).Kind != FmtCurrency {169- t.Errorf("E5 shows %v, Other!C1 %v", s.DisplayFormat(at("E5")).Kind, o.DisplayFormat(at("C1")).Kind)169+ if s.DisplayFormat(at("E5")).Kind != FmtCurrency || !o.DisplayFormat(at("C1")).IsZero() || o.DisplayFormat(at("C3")).Kind != FmtCurrency {170+ t.Errorf("E5 shows %v, Other!C1 %v, Other!C3 %v", s.DisplayFormat(at("E5")).Kind, o.DisplayFormat(at("C1")).Kind, o.DisplayFormat(at("C3")).Kind)171+ }172+}173+174+// Cutting a block on its sheet leaves the cells it left plain, over175+// formatted columns and rows, except where it lands; undo restores them.176+func TestMoveBlockLeavesSourcePlain(t *testing.T) {177+ s := New()178+ s.SetFormat(col(1), Preset(FmtCurrency))179+ s.SetStyle(rowRect(1, 1), bold) // row 2180+ s.Set(at("B1"), "5")181+ s.Set(at("B2"), "6")182+ s.Set(at("C3"), "7")183+ if _, err := s.Move(Rect{From: at("B1"), To: at("C3")}, at("C2")); err != nil {184+ t.Fatal(err)185+ }186+ for _, a := range []string{"B1", "B2", "B3", "C1"} {187+ if !s.DisplayFormat(at(a)).IsZero() || s.CellStyle(at(a)).Bold {188+ t.Errorf("%s shows %v, bold %v, want plain", a, s.DisplayFormat(at(a)).Kind, s.CellStyle(at(a)).Bold)189+ }190+ }191+ if s.DisplayFormat(at("C2")).Kind != FmtCurrency || s.DisplayFormat(at("B4")).Kind != FmtCurrency || !s.CellStyle(at("A2")).Bold {192+ t.Errorf("C2 %v (moved from B1), B4 %v, A2 bold %v", s.DisplayFormat(at("C2")).Kind, s.DisplayFormat(at("B4")).Kind, s.CellStyle(at("A2")).Bold)193+ }194+ s.Undo()195+ if s.DisplayFormat(at("B3")).Kind != FmtCurrency || !s.CellStyle(at("B2")).Bold || s.cells.get(at("B3")) != nil {196+ t.Errorf("undo: B3 %v, B2 bold %v", s.DisplayFormat(at("B3")).Kind, s.CellStyle(at("B2")).Bold)170197 }171198 }172199 @@ -6,6 +6,32 @@ import (
66 "github.com/FelineStateMachine/012/internal/sheet"77 )88 9+// Vim's yy and dd copy whole rows, so p and P carry their row formats,10+// as a whole-row paste does: the pasted row is bold far past its data.11+func TestVimPutCarriesRowFormats(t *testing.T) {12+ m := vimModel(t)13+ press(t, m, "2G", "<shift+space>")14+ m.runCommand("format.bold")15+ press(t, m, "<esc>", "yy", "G", "p")16+ far := sheet.Addr{Col: 400, Row: 6} // row 7, the pasted one17+ if input(m, "A7") != "A2" || !m.sheet.CellStyle(far).Bold {18+ t.Fatalf("p: A7 %q, row 7 bold %v", input(m, "A7"), m.sheet.CellStyle(far).Bold)19+ }20+ if m.sheet.CellStyle(sheet.Addr{Col: 400, Row: 5}).Bold {21+ t.Error("row 6 turned bold")22+ }23+ press(t, m, "u")24+ if m.sheet.CellStyle(far).Bold {25+ t.Error("undo left row 7 bold")26+ }27+ // dd then P: the row format moves with the row.28+ press(t, m, "2G", "dd", "gg", "P")29+ if input(m, "A1") != "A2" || !m.sheet.CellStyle(sheet.Addr{Col: 400}).Bold || m.sheet.CellStyle(sheet.Addr{Col: 400, Row: 1}).Bold {30+ t.Errorf("dd P: A1 %q, row 1 bold %v, row 2 bold %v", input(m, "A1"),31+ m.sheet.CellStyle(sheet.Addr{Col: 400}).Bold, m.sheet.CellStyle(sheet.Addr{Col: 400, Row: 1}).Bold)32+ }33+}34+935 // Copying a whole column and pasting it at the top of another carries1036 // its column format; cutting one moves it, one undo step each.1137 func TestCopyColumnCarriesItsFormat(t *testing.T) {@@ -37,14 +37,11 @@ func init() {
3737 )3838 }3939 40-// selectedRows is the rows of the selection across the columns in use.40+// selectedRows is the rows of the selection, whole, so a copy takes their41+// row formats along as a whole-row copy does.4142 func (m *Model) selectedRows() sheet.Rect {4243 r := m.selection()43- last := 044- if used, ok := m.sheet.UsedRange(); ok {45- last = used.To.Col46- }47- return sheet.Rect{From: sheet.Addr{Row: r.From.Row}, To: sheet.Addr{Col: last, Row: r.To.Row}}44+ return sheet.Rect{From: sheet.Addr{Row: r.From.Row}, To: sheet.Addr{Col: sheet.MaxCols - 1, Row: r.To.Row}}4845 }4946 5047 // copyRows puts rows r on the clipboard, marked as whole rows so pasting