Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 147 additions & 0 deletions TablePro/Core/Vim/VimEngine+Controls.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
//
// VimEngine+Controls.swift
// TablePro
//

import Foundation

struct VimNumberMatch {
var start: Int
var end: Int
var value: Int
var isHex: Bool
var hexUppercase: Bool
}

extension VimEngine {
func handleNormalControl(_ char: Character, in buffer: VimTextBuffer) -> Bool? {
switch char {
case "\u{01}":
adjustNumberOnLine(by: consumeCount(), in: buffer)
return true
case "\u{18}":
adjustNumberOnLine(by: -consumeCount(), in: buffer)
return true
case "\u{04}":
scrollByLines(halfVisibleLineCount(in: buffer), in: buffer)
return true
case "\u{15}":
scrollByLines(-halfVisibleLineCount(in: buffer), in: buffer)
return true
case "\u{06}":
scrollByLines(visibleLineSpan(in: buffer), in: buffer)
return true
case "\u{02}":
scrollByLines(-visibleLineSpan(in: buffer), in: buffer)
return true
case "\u{05}", "\u{19}":
return true
default:
return nil
}
}

func halfVisibleLineCount(in buffer: VimTextBuffer) -> Int {
let (first, last) = buffer.visibleLineRange()
return max(1, (last - first + 1) / 2)
}

func visibleLineSpan(in buffer: VimTextBuffer) -> Int {
let (first, last) = buffer.visibleLineRange()
return max(1, last - first + 1)
}

func scrollByLines(_ delta: Int, in buffer: VimTextBuffer) {
let pos = buffer.selectedRange().location
let (currentLine, col) = buffer.lineAndColumn(forOffset: pos)
let targetLine = max(0, min(buffer.lineCount - 1, currentLine + delta))
let offset = buffer.offset(forLine: targetLine, column: col)
buffer.setSelectedRange(NSRange(location: offset, length: 0))
goalColumn = nil
}

func adjustNumberOnLine(by delta: Int, in buffer: VimTextBuffer) {
guard delta != 0 else { return }
let pos = buffer.selectedRange().location
let lineRange = buffer.lineRange(forOffset: pos)
let lineEnd = lineRange.location + lineRange.length
let contentEnd = lineEnd > lineRange.location
&& lineEnd <= buffer.length
&& buffer.character(at: lineEnd - 1) == 0x0A ? lineEnd - 1 : lineEnd
guard let match = findNumber(from: pos, lineStart: lineRange.location, contentEnd: contentEnd, in: buffer) else {
return
}
let replacement = formatNumber(match.value + delta, hex: match.isHex, hexUppercase: match.hexUppercase)
let range = NSRange(location: match.start, length: match.end - match.start)
buffer.replaceCharacters(in: range, with: replacement)
let newEnd = match.start + (replacement as NSString).length
buffer.setSelectedRange(NSRange(location: max(match.start, newEnd - 1), length: 0))
}

func findNumber(
from cursor: Int,
lineStart: Int,
contentEnd: Int,
in buffer: VimTextBuffer
) -> VimNumberMatch? {
guard contentEnd > lineStart else { return nil }
var scan = max(cursor, lineStart)
while scan < contentEnd && !isDigitChar(buffer.character(at: scan)) {
scan += 1
}
guard scan < contentEnd else { return nil }
var start = scan
if start >= lineStart + 2
&& buffer.character(at: start - 2) == 0x30
&& (buffer.character(at: start - 1) == 0x78 || buffer.character(at: start - 1) == 0x58) {
start -= 2
}
var end = scan
let isHex = start + 1 < contentEnd
&& buffer.character(at: start) == 0x30
&& (buffer.character(at: start + 1) == 0x78 || buffer.character(at: start + 1) == 0x58)
var hexUppercase = false
if isHex {
hexUppercase = buffer.character(at: start + 1) == 0x58
end = start + 2
while end < contentEnd && isHexDigitChar(buffer.character(at: end)) {
end += 1
}
guard end > start + 2 else { return nil }
} else {
while end < contentEnd && isDigitChar(buffer.character(at: end)) {
end += 1
}
if start > lineStart && buffer.character(at: start - 1) == 0x2D {
start -= 1
}
}
let text = buffer.string(in: NSRange(location: start, length: end - start))
guard let value = parseNumberLiteral(text) else { return nil }
return VimNumberMatch(start: start, end: end, value: value, isHex: isHex, hexUppercase: hexUppercase)
}

func parseNumberLiteral(_ text: String) -> Int? {
if text.hasPrefix("-") || text.hasPrefix("+") {
return Int(text)
}
if text.hasPrefix("0x") || text.hasPrefix("0X") {
return Int(text.dropFirst(2), radix: 16)
}
return Int(text)
}

func formatNumber(_ value: Int, hex: Bool, hexUppercase: Bool) -> String {
if hex {
let body = String(value, radix: 16, uppercase: hexUppercase)
return (hexUppercase ? "0X" : "0x") + body
}
return String(value)
}

func isDigitChar(_ ch: unichar) -> Bool { ch >= 0x30 && ch <= 0x39 }

func isHexDigitChar(_ ch: unichar) -> Bool {
isDigitChar(ch) || (ch >= 0x41 && ch <= 0x46) || (ch >= 0x61 && ch <= 0x66)
}
}
155 changes: 155 additions & 0 deletions TablePro/Core/Vim/VimEngine+InsertReplace.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
//
// VimEngine+InsertReplace.swift
// TablePro
//

import Foundation

extension VimEngine {
func processInsert(_ char: Character) -> Bool {
if char == "\u{1B}" {
lastInsertOffset = buffer?.selectedRange().location
setMode(.normal)
if let buffer, buffer.selectedRange().location > 0 {
let pos = buffer.selectedRange().location
let lineRange = buffer.lineRange(forOffset: pos)
if pos > lineRange.location {
buffer.setSelectedRange(NSRange(location: pos - 1, length: 0))
}
}
return true
}
if let buffer, handleInsertModeControl(char, in: buffer) {
return true
}
return false
}

func processReplace(_ char: Character) -> Bool {
guard let buffer else { return false }
if char == "\u{1B}" {
setMode(.normal)
let pos = buffer.selectedRange().location
let lineRange = buffer.lineRange(forOffset: pos)
if pos > lineRange.location {
buffer.setSelectedRange(NSRange(location: pos - 1, length: 0))
}
return true
}
if handleInsertModeControl(char, in: buffer) { return true }
if char == "\r" || char == "\n" {
return false
}
let pos = buffer.selectedRange().location
let lineRange = buffer.lineRange(forOffset: pos)
let lineEnd = lineRange.location + lineRange.length
let contentEnd = lineEnd > lineRange.location
&& lineEnd <= buffer.length
&& buffer.character(at: lineEnd - 1) == 0x0A ? lineEnd - 1 : lineEnd
if pos < contentEnd {
buffer.replaceCharacters(in: NSRange(location: pos, length: 1), with: String(char))
} else {
buffer.replaceCharacters(in: NSRange(location: pos, length: 0), with: String(char))
}
return true
}

func processCommandLine(_ char: Character, buffer commandBuffer: String) -> Bool {
switch char {
case "\u{1B}":
setMode(.normal)
return true
case "\r", "\n":
let prefix = commandBuffer.first
let body = String(commandBuffer.dropFirst())
setMode(.normal)
if prefix == "/" {
runSearch(pattern: body, forward: true)
} else if prefix == "?" {
runSearch(pattern: body, forward: false)
} else {
onCommand?(body)
}
return true
case "\u{7F}":
if (commandBuffer as NSString).length > 1 {
setMode(.commandLine(buffer: String(commandBuffer.dropLast())))
} else {
setMode(.normal)
}
return true
default:
setMode(.commandLine(buffer: commandBuffer + String(char)))
return true
}
}

func handleInsertModeControl(_ char: Character, in buffer: VimTextBuffer) -> Bool {
switch char {
case "\u{17}":
deleteWordBackwardInInsert(in: buffer)
return true
case "\u{15}":
deleteToLineStartInInsert(in: buffer)
return true
case "\u{08}":
backspaceInInsert(in: buffer)
return true
case "\u{14}":
indentLineInInsert(outdent: false, in: buffer)
return true
case "\u{04}":
indentLineInInsert(outdent: true, in: buffer)
return true
default:
return false
}
}

func deleteWordBackwardInInsert(in buffer: VimTextBuffer) {
let pos = buffer.selectedRange().location
guard pos > 0 else { return }
let lineStart = buffer.lineRange(forOffset: pos).location
guard pos > lineStart else { return }
let target = max(lineStart, buffer.wordBoundary(forward: false, from: pos))
let range = NSRange(location: target, length: pos - target)
buffer.replaceCharacters(in: range, with: "")
buffer.setSelectedRange(NSRange(location: target, length: 0))
}

func deleteToLineStartInInsert(in buffer: VimTextBuffer) {
let pos = buffer.selectedRange().location
let lineStart = buffer.lineRange(forOffset: pos).location
guard pos > lineStart else { return }
let range = NSRange(location: lineStart, length: pos - lineStart)
buffer.replaceCharacters(in: range, with: "")
buffer.setSelectedRange(NSRange(location: lineStart, length: 0))
}

func backspaceInInsert(in buffer: VimTextBuffer) {
let pos = buffer.selectedRange().location
guard pos > 0 else { return }
buffer.replaceCharacters(in: NSRange(location: pos - 1, length: 1), with: "")
buffer.setSelectedRange(NSRange(location: pos - 1, length: 0))
}

func indentLineInInsert(outdent: Bool, in buffer: VimTextBuffer) {
let pos = buffer.selectedRange().location
let lineRange = buffer.lineRange(forOffset: pos)
let indent = buffer.indentString()
if outdent {
let line = buffer.string(in: lineRange) as NSString
var stripCount = 0
while stripCount < indent.count && stripCount < line.length
&& (line.character(at: stripCount) == 0x20 || line.character(at: stripCount) == 0x09) {
stripCount += 1
}
guard stripCount > 0 else { return }
buffer.replaceCharacters(in: NSRange(location: lineRange.location, length: stripCount), with: "")
buffer.setSelectedRange(NSRange(location: max(lineRange.location, pos - stripCount), length: 0))
} else {
buffer.replaceCharacters(in: NSRange(location: lineRange.location, length: 0), with: indent)
buffer.setSelectedRange(NSRange(location: pos + indent.count, length: 0))
}
}
}
39 changes: 39 additions & 0 deletions TablePro/Core/Vim/VimEngine+Macros.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//
// VimEngine+Macros.swift
// TablePro
//

import Foundation

extension VimEngine {
func handleMacroTarget(kind: VimMacroPendingKind, register: Character) {
switch kind {
case .recordTarget:
macroRecording = register
macroBuffers[register] = []
case .replayTarget:
let target: Character
if register == "@" { target = lastInvokedMacro ?? Character("\0") } else { target = register }
guard let keys = macroBuffers[target], !keys.isEmpty else {
pendingMacroCount = 1
return
}
lastInvokedMacro = target
let count = max(1, pendingMacroCount)
pendingMacroCount = 1
for _ in 0..<count { replayMacro(keys: keys) }
}
}

func replayMacro(keys: [(Character, Bool)]) {
guard macroPlaybackDepth < 50 else { return }
macroPlaybackDepth += 1
defer { macroPlaybackDepth -= 1 }
let saved = macroRecording
macroRecording = nil
for (char, shift) in keys {
_ = process(char, shift: shift)
}
macroRecording = saved
}
}
Loading
Loading