Skip to content

Commit f1f8e5a

Browse files
committed
fix(inspector): name CSV row undo/redo consistently and preserve the Paste label
1 parent 9f95836 commit f1f8e5a

8 files changed

Lines changed: 146 additions & 29 deletions

File tree

Plugins/CSVInspectorPlugin/CSVDocument.swift

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -187,68 +187,71 @@ public final class CSVDocument: NSDocument, InspectorDocument {
187187
}
188188

189189
public func appendRow() {
190+
let name = String(localized: "Add Row")
190191
let index = store.appendRow(values: [])
191192
registerUndo { document in
192-
document.removeRow(at: index, suppressUndo: false)
193+
document.removeRow(at: index, suppressUndo: false, actionName: name)
193194
}
194-
setUndoActionName(String(localized: "Add Row"))
195+
setUndoActionName(name)
195196
onChange?()
196197
}
197198

198199
public func insertRow(at index: Int) {
200+
let name = String(localized: "Insert Row")
199201
store.insertRow([], at: index)
200202
registerUndo { document in
201-
document.removeRow(at: index, suppressUndo: false)
203+
document.removeRow(at: index, suppressUndo: false, actionName: name)
202204
}
203-
setUndoActionName(String(localized: "Insert Row"))
205+
setUndoActionName(name)
204206
onChange?()
205207
}
206208

207209
public func removeRow(at index: Int) {
208-
removeRow(at: index, suppressUndo: false)
209-
setUndoActionName(String(localized: "Delete Row"))
210+
removeRow(at: index, suppressUndo: false, actionName: String(localized: "Delete Row"))
210211
}
211212

212-
private func removeRow(at index: Int, suppressUndo: Bool) {
213+
private func removeRow(at index: Int, suppressUndo: Bool, actionName: String) {
213214
guard let removed = store.removeRow(at: index) else { return }
214215
if !suppressUndo {
215216
registerUndo { document in
216-
document.reinsertRow(removed, at: index)
217+
document.reinsertRow(removed, at: index, actionName: actionName)
217218
}
219+
setUndoActionName(actionName)
218220
}
219221
onChange?()
220222
}
221223

222-
private func reinsertRow(_ values: [String], at index: Int) {
224+
private func reinsertRow(_ values: [String], at index: Int, actionName: String) {
223225
store.insertRow(values, at: index)
224226
registerUndo { document in
225-
document.removeRow(at: index, suppressUndo: false)
227+
document.removeRow(at: index, suppressUndo: false, actionName: actionName)
226228
}
229+
setUndoActionName(actionName)
227230
onChange?()
228231
}
229232

230233
public func removeRows(at indices: IndexSet) {
231234
let removed = store.removeRows(at: indices)
232235
guard !removed.isEmpty else { return }
236+
let name = removed.count == 1
237+
? String(localized: "Delete Row")
238+
: String(localized: "Delete Rows")
233239
registerUndo { document in
234-
document.reinsertRows(removed)
240+
document.reinsertRows(removed, actionName: name)
235241
}
236-
setUndoActionName(
237-
removed.count == 1
238-
? String(localized: "Delete Row")
239-
: String(localized: "Delete Rows")
240-
)
242+
setUndoActionName(name)
241243
onChange?()
242244
}
243245

244-
private func reinsertRows(_ rows: [(index: Int, cells: [String])]) {
246+
private func reinsertRows(_ rows: [(index: Int, cells: [String])], actionName: String) {
245247
for entry in rows.sorted(by: { $0.index < $1.index }) {
246248
store.insertRow(entry.cells, at: entry.index)
247249
}
248250
let originalIndices = IndexSet(rows.map(\.index))
249251
registerUndo { document in
250252
document.removeRows(at: originalIndices)
251253
}
254+
setUndoActionName(actionName)
252255
onChange?()
253256
}
254257

TablePro/TableProApp.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -605,17 +605,17 @@ struct AppMenuCommands: Commands {
605605

606606
Divider()
607607

608-
Button(String(localized: "Insert Row Above")) {
608+
Button("Insert Row Above") {
609609
NSApp.sendAction(#selector(InspectorViewController.inspectorInsertRowAbove(_:)), to: nil, from: nil)
610610
}
611611
.disabled(!keyWindowIsInspector)
612612

613-
Button(String(localized: "Insert Row Below")) {
613+
Button("Insert Row Below") {
614614
NSApp.sendAction(#selector(InspectorViewController.inspectorInsertRowBelow(_:)), to: nil, from: nil)
615615
}
616616
.disabled(!keyWindowIsInspector)
617617

618-
Button(String(localized: "Delete Rows")) {
618+
Button("Delete Rows") {
619619
NSApp.sendAction(#selector(InspectorViewController.inspectorDeleteSelectedRows(_:)), to: nil, from: nil)
620620
}
621621
.disabled(!keyWindowIsInspector)
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
//
2+
// InspectorRowInsertion.swift
3+
// TablePro
4+
//
5+
6+
import Foundation
7+
8+
enum InspectorRowInsertion {
9+
static func storeIndex(anchorDisplayRow: Int?, below: Bool, displayToStore: [Int], rowCount: Int) -> Int {
10+
guard let displayRow = anchorDisplayRow,
11+
displayRow >= 0, displayRow < displayToStore.count else {
12+
return below ? rowCount : 0
13+
}
14+
let storeRow = displayToStore[displayRow]
15+
return below ? storeRow + 1 : storeRow
16+
}
17+
}

TablePro/Views/Inspector/InspectorViewController.swift

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -143,14 +143,14 @@ final class InspectorViewController: NSViewController, NSUserInterfaceValidation
143143

144144
let undoManager = nsDocument?.undoManager
145145
undoManager?.beginUndoGrouping()
146-
undoManager?.setActionName(String(localized: "Paste"))
147146
for row in rows {
148147
let newRowIndex = inspectorDocument.rowCount
149148
inspectorDocument.appendRow()
150149
for (column, value) in row.enumerated() {
151150
inspectorDocument.setCell(row: newRowIndex, column: column, to: value)
152151
}
153152
}
153+
undoManager?.setActionName(String(localized: "Paste"))
154154
undoManager?.endUndoGrouping()
155155
}
156156

@@ -233,20 +233,19 @@ final class InspectorViewController: NSViewController, NSUserInterfaceValidation
233233
}
234234

235235
private func insertStoreIndex(anchoredBy sender: Any?, below: Bool) -> Int {
236-
let rowCount = inspectorDocument?.rowCount ?? 0
237236
let anchorDisplayRow: Int? = if let item = sender as? NSMenuItem {
238237
item.tag
239238
} else if below {
240239
state.selectedRowIndices.max()
241240
} else {
242241
state.selectedRowIndices.min()
243242
}
244-
guard let displayRow = anchorDisplayRow,
245-
displayRow >= 0, displayRow < displayToStore.count else {
246-
return below ? rowCount : 0
247-
}
248-
let storeRow = displayToStore[displayRow]
249-
return below ? storeRow + 1 : storeRow
243+
return InspectorRowInsertion.storeIndex(
244+
anchorDisplayRow: anchorDisplayRow,
245+
below: below,
246+
displayToStore: displayToStore,
247+
rowCount: inspectorDocument?.rowCount ?? 0
248+
)
250249
}
251250

252251
@objc func inspectorAddColumn(_ sender: Any?) {
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../Plugins/CSVInspectorPlugin/CSVDocument.swift
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
//
2+
// CSVDocumentUndoTests.swift
3+
// TableProTests
4+
//
5+
// Tests CSVDocument (compiled via symlink from Plugins/CSVInspectorPlugin/).
6+
//
7+
8+
import AppKit
9+
import Foundation
10+
import TableProPluginKit
11+
import Testing
12+
13+
@MainActor
14+
@Suite("CSVDocument undo naming")
15+
struct CSVDocumentUndoTests {
16+
private func makeDocument(_ contents: String) throws -> CSVDocument {
17+
let url = FileManager.default.temporaryDirectory.appendingPathComponent("\(UUID().uuidString).csv")
18+
try contents.data(using: .utf8)!.write(to: url)
19+
let document = CSVDocument()
20+
try document.read(from: url, ofType: "public.comma-separated-values-text")
21+
try? FileManager.default.removeItem(at: url)
22+
return document
23+
}
24+
25+
@Test("Insert Row keeps its name through undo and redo")
26+
func insertRowNaming() throws {
27+
let document = try makeDocument("a,b\n1,2\n")
28+
document.insertRow(at: 0)
29+
#expect(document.undoManager?.undoActionName == "Insert Row")
30+
document.undoManager?.undo()
31+
#expect(document.undoManager?.redoActionName == "Insert Row")
32+
document.undoManager?.redo()
33+
#expect(document.undoManager?.undoActionName == "Insert Row")
34+
}
35+
36+
@Test("Delete Rows keeps its name through undo")
37+
func deleteRowsNaming() throws {
38+
let document = try makeDocument("a,b\n1,2\n3,4\n5,6\n")
39+
document.removeRows(at: IndexSet([0, 1]))
40+
#expect(document.undoManager?.undoActionName == "Delete Rows")
41+
document.undoManager?.undo()
42+
#expect(document.undoManager?.redoActionName == "Delete Rows")
43+
}
44+
45+
@Test("Add Row keeps its name through undo and redo")
46+
func addRowNaming() throws {
47+
let document = try makeDocument("a,b\n1,2\n")
48+
document.appendRow()
49+
#expect(document.undoManager?.undoActionName == "Add Row")
50+
document.undoManager?.undo()
51+
#expect(document.undoManager?.redoActionName == "Add Row")
52+
}
53+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
//
2+
// InspectorRowInsertionTests.swift
3+
// TableProTests
4+
//
5+
6+
@testable import TablePro
7+
import Testing
8+
9+
@Suite("InspectorRowInsertion")
10+
struct InspectorRowInsertionTests {
11+
@Test("A clicked row maps through displayToStore for above and below")
12+
func mapsClickedRow() {
13+
let displayToStore = [10, 11, 12]
14+
#expect(InspectorRowInsertion.storeIndex(
15+
anchorDisplayRow: 1, below: false, displayToStore: displayToStore, rowCount: 3) == 11)
16+
#expect(InspectorRowInsertion.storeIndex(
17+
anchorDisplayRow: 1, below: true, displayToStore: displayToStore, rowCount: 3) == 12)
18+
}
19+
20+
@Test("A non-identity displayToStore (filtered or sorted view) resolves the store row")
21+
func nonIdentityMapping() {
22+
let displayToStore = [4, 0, 9]
23+
#expect(InspectorRowInsertion.storeIndex(
24+
anchorDisplayRow: 0, below: false, displayToStore: displayToStore, rowCount: 10) == 4)
25+
#expect(InspectorRowInsertion.storeIndex(
26+
anchorDisplayRow: 2, below: true, displayToStore: displayToStore, rowCount: 10) == 10)
27+
}
28+
29+
@Test("No anchor inserts at the top for above and appends for below")
30+
func emptySelectionFallback() {
31+
#expect(InspectorRowInsertion.storeIndex(
32+
anchorDisplayRow: nil, below: false, displayToStore: [0, 1], rowCount: 2) == 0)
33+
#expect(InspectorRowInsertion.storeIndex(
34+
anchorDisplayRow: nil, below: true, displayToStore: [0, 1], rowCount: 2) == 2)
35+
}
36+
37+
@Test("An out-of-range anchor falls back to top or append")
38+
func outOfRangeFallback() {
39+
#expect(InspectorRowInsertion.storeIndex(
40+
anchorDisplayRow: 5, below: false, displayToStore: [0, 1], rowCount: 2) == 0)
41+
#expect(InspectorRowInsertion.storeIndex(
42+
anchorDisplayRow: -1, below: true, displayToStore: [0, 1], rowCount: 2) == 2)
43+
}
44+
}

docs/features/csv-inspector.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ Rows load in pages. The page size comes from the Default page size setting in [S
3434

3535
- Double-click a cell (or press Return on it) to edit. Return commits and closes the editor. Tab commits and moves to the next cell, wrapping to the next row.
3636
- The toolbar `Add Row` button appends a new row, scrolls to it, and selects it.
37-
- Right-click a row and choose **Insert Row Above** or **Insert Row Below** to add a blank row next to it. Both also appear in the Edit menu, where they act on the selected row.
37+
- Right-click a row and choose **Insert Row Above** or **Insert Row Below** to add a blank row next to it. Both also appear in the Edit menu, where they insert next to the selected row, or at the top or bottom when nothing is selected.
3838
- Select rows and press `Delete` (the toolbar `Delete` button, or right-click a row and choose Delete) to remove them. The next row takes selection so arrow keys keep working from where you were.
3939
- Deleting rows that hold data asks you to confirm first. Deleting blank rows skips the prompt.
4040
- Cmd+Z and Cmd+Shift+Z undo and redo every change. Each insert, a bulk delete, or a paste is a single undo step.

0 commit comments

Comments
 (0)