krz/orgstar

A native macOS editor for org-mode files. editor org-mode swift

Commit 4c75f0eea5

4c75f0eea5342f7f5a56227654fb6f40d0ef56e6

parent: 1dda0291d2

Verified · cmc

cmc <hello@cleberg.net> · 2026-10-09 01:27 UTC

Fill at the cursor with M-q in visual block state

Evil turns transient-mark-mode off in block state, so org-fill-paragraph sees no region and fills the paragraph at the cursor; with no change the block stays. Block state now reads its cursor from the last motion as the other visual states do, so motions past the second line extend the block.

Layout: unified · split

Sources/OrgEditorAppKit/Modal.swift +14 −8
@@ -34,7 +34,7 @@ extension OrgEditor {
3434 if keys.count > 1 { onKeysPending?(nil, []) }
3535 let context = editContext()
3636 if let binding = candidates.first(where: { modalRuns($0, in: context) }) {
37 if binding.command == "org.fill-paragraph", vim.mode == .visual || vim.mode == .visualLine {
37 if binding.command == "org.fill-paragraph", vim.mode == .visual || vim.mode == .visualLine || vim.mode == .visualBlock {
3838 fillVisual()
3939 } else {
4040 run(binding.command)
@@ -57,15 +57,21 @@ extension OrgEditor {
5757 }
5858 }
5959
60 /// M-q in visual states: `org-fill-paragraph` on the region, the cursor kept. A change
61 /// deactivates the region, which leaves visual state.
60 /// M-q in visual states: `org-fill-paragraph` on the region, the cursor kept. Evil turns
61 /// `transient-mark-mode` off in block state, so there it fills the paragraph at the cursor.
62 /// A change deactivates the region, which leaves visual state.
6263 private func fillVisual() {
63 let selected = textView.selectedRange()
64 let cursor = visualCaret ?? selected.location
64 let selected = textView.selectedRanges
65 let cursor = visualCaret ?? textView.selectedRange().location
6566 let text = document.text
66 perform(FillParagraph(point: cursor))
67 if vim.mode == .visualBlock {
68 textView.setSelectedRange(NSRange(location: cursor, length: 0))
69 perform(FillParagraph())
70 } else {
71 perform(FillParagraph(point: cursor))
72 }
6773 if document.text == text {
68 textView.setSelectedRange(selected)
74 textView.selectedRanges = selected
6975 visualCaret = cursor
7076 } else {
7177 vim.reset()
@@ -91,7 +97,7 @@ extension OrgEditor {
9197 }
9298 }
9399 let wasInsert = vim.mode == .insert
94 let visual = vim.mode == .visual || vim.mode == .visualLine
100 let visual = vim.mode == .visual || vim.mode == .visualLine || vim.mode == .visualBlock
95101 let caret = visual ? (visualCaret ?? textView.selectedRange().location) : textView.selectedRange().location
96102 if jumpToFileMark(key) { continue }
97103 let clipboard = NSPasteboard.general.string(forType: .string)
Tests/OrgEditorAppKitTests/ModalTests.swift +29
@@ -161,6 +161,35 @@ struct ModalTests {
161161 #expect(lines.editor.vim.mode == .normal)
162162 }
163163
164 /// Evil turns `transient-mark-mode` off in visual block state, so M-q there fills the
165 /// paragraph at the cursor (checked against evil with `org-fill-paragraph`).
166 @Test func fillParagraphInVisualBlockFillsAtTheCursor() {
167 let h = doom("aaa\nbbb\n\nccc\nddd\n\neee\nfff\n")
168 h.caret(at: 0)
169 type(h, "C-v j j j")
170 #expect(h.textView.selectedRanges.first?.rangeValue == NSRange(location: 0, length: 1) && h.textView.selectedRanges.last?.rangeValue == NSRange(location: 9, length: 1))
171 KeyTests().press(h, "q", option: KeyEvents.leftOption, typed: "œ")
172 #expect(h.editor.document.text == "aaa\nbbb\n\nccc ddd\n\neee\nfff\n")
173 #expect(h.textView.selectedRange() == NSRange(location: 9, length: 0))
174 #expect(h.editor.vim.mode == .normal)
175 let up = doom("aaa\nbbb\nccc\n\nx\ny\n")
176 up.caret(at: 8)
177 type(up, "C-v k")
178 KeyTests().press(up, "q", option: KeyEvents.leftOption, typed: "œ")
179 #expect(up.editor.document.text == "aaa bbb ccc\n\nx\ny\n")
180 #expect(up.textView.selectedRange() == NSRange(location: 4, length: 0))
181 #expect(up.editor.vim.mode == .normal)
182 // Nothing to fill: the block stays.
183 let filled = doom("aaa bbb\n\nccc\n")
184 filled.caret(at: 0)
185 type(filled, "C-v l")
186 let block = filled.textView.selectedRanges
187 KeyTests().press(filled, "q", option: KeyEvents.leftOption, typed: "œ")
188 #expect(filled.editor.document.text == "aaa bbb\n\nccc\n")
189 #expect(filled.textView.selectedRanges == block)
190 #expect(filled.editor.vim.mode == .visualBlock)
191 }
192
164193 @Test func zzSavesAndClosesAsWq() {
165194 let h = doom("text\n")
166195 var app: [String] = []