import UIKit import SwiftUI import Runestone final class EditorCommandHandler: UIViewController { weak var textView: Runestone.TextView? var onSave: (() -> Void)? var onBuild: (() -> Void)? var onRun: (() -> Void)? var onToggleNavigator: (() -> Void)? var onToggleConsole: (() -> Void)? var onFind: (() -> Void)? var onJumpToLine: (() -> Void)? var onFindInProject: (() -> Void)? override var canBecomeFirstResponder: Bool { true } override var keyCommands: [UIKeyCommand]? { let cmds: [(String, String, UIKeyModifierFlags)] = [ ("Save", "s", .command), ("Undo", "z", .command), ("Redo", "z", [.command, .shift]), ("Indent", "]", .command), ("Unindent", "[", .command), ("Toggle Comment", "/", .command), ("Move Line Up", UIKeyCommand.inputUpArrow, [.command, .alternate]), ("Move Line Down", UIKeyCommand.inputDownArrow, [.command, .alternate]), ("Duplicate Line", "d", [.command, .shift]), ("Delete Line", "k", [.command, .shift]), ("Find", "f", .command), ("Find in Project", "f", [.command, .shift]), ("Find Next", "g", .command), ("Find Prev", "g", [.command, .shift]), ("Jump to Line", "l", .command), ("Top", UIKeyCommand.inputUpArrow, .command), ("Bottom", UIKeyCommand.inputDownArrow, .command), ("Build", "b", .command), ("Run", "r", .command), ("Navigator", "0", .command), ("Console", "y", [.command, .shift]), ("Font+", "+", .command), ("Font-", "-", .command), ] return cmds.map { title, input, mods in let sel = selectorFor(title) let kc = UIKeyCommand(title: title, action: sel, input: input, modifierFlags: mods, discoverabilityTitle: title) if [UIKeyCommand.inputUpArrow, UIKeyCommand.inputDownArrow].contains(input) { kc.wantsPriorityOverSystemBehavior = true } return kc } } private func selectorFor(_ title: String) -> Selector { switch title { case "Save": return #selector(cmdSave) case "Undo": return #selector(cmdUndo) case "Redo": return #selector(cmdRedo) case "Indent": return #selector(cmdIndent) case "Unindent": return #selector(cmdUnindent) case "Toggle Comment": return #selector(cmdComment) case "Move Line Up": return #selector(cmdMoveUp) case "Move Line Down": return #selector(cmdMoveDown) case "Duplicate Line": return #selector(cmdDuplicate) case "Delete Line": return #selector(cmdDeleteLine) case "Find": return #selector(cmdFind) case "Find in Project": return #selector(cmdFindProject) case "Find Next": return #selector(cmdFindNext) case "Find Prev": return #selector(cmdFindPrev) case "Jump to Line": return #selector(cmdJumpToLine) case "Top": return #selector(cmdTop) case "Bottom": return #selector(cmdBottom) case "Build": return #selector(cmdBuild) case "Run": return #selector(cmdRun) case "Navigator": return #selector(cmdNavigator) case "Console": return #selector(cmdConsole) case "Font+": return #selector(cmdFontBigger) default: return #selector(cmdFontSmaller) } } @objc func cmdSave() { onSave?() } @objc func cmdUndo() { textView?.undoManager?.undo() } @objc func cmdRedo() { textView?.undoManager?.redo() } @objc func cmdIndent() { textView?.shiftRight() } @objc func cmdUnindent() { textView?.shiftLeft() } @objc func cmdFind() { onFind?() } @objc func cmdFindProject(){ onFindInProject?() } @objc func cmdFindNext() {} @objc func cmdFindPrev() {} @objc func cmdJumpToLine() { onJumpToLine?() } @objc func cmdBuild() { onBuild?() } @objc func cmdRun() { onRun?() } @objc func cmdNavigator() { onToggleNavigator?() } @objc func cmdConsole() { onToggleConsole?() } @objc func cmdFontBigger() {} @objc func cmdFontSmaller(){} @objc func cmdComment() { guard let tv = textView, let sel = tv.selectedRange else { return } let text = tv.text as NSString; let lr = text.lineRange(for: sel) let line = text.substring(with: lr); let trimmed = line.trimmingCharacters(in: .whitespaces) let indent = String(line.prefix(while: { $0 == " " || $0 == "\t" })) let new: String if trimmed.hasPrefix("// ") { new = line.replacingFirstOccurrence(of: "// ", with: "") } else if trimmed.hasPrefix("//") { new = line.replacingFirstOccurrence(of: "//", with: "") } else { new = indent + "// " + line.dropFirst(indent.count) } tv.replace(lr, withText: new) } @objc func cmdMoveUp() { guard let tv = textView, let sel = tv.selectedRange else { return } let text = tv.text as NSString; let cur = text.lineRange(for: sel) guard cur.location > 0 else { return } let prev = text.lineRange(for: NSRange(location: cur.location - 1, length: 0)) let curLine = text.substring(with: cur); let prevLine = text.substring(with: prev) tv.replace(NSRange(location: prev.location, length: prev.length + cur.length), withText: curLine + prevLine) tv.selectedRange = NSRange(location: prev.location + curLine.count - 1, length: 0) } @objc func cmdMoveDown() { guard let tv = textView, let sel = tv.selectedRange else { return } let text = tv.text as NSString; let cur = text.lineRange(for: sel) let end = cur.location + cur.length; guard end < text.length else { return } let next = text.lineRange(for: NSRange(location: end, length: 0)) let curLine = text.substring(with: cur); let nextLine = text.substring(with: next) tv.replace(NSRange(location: cur.location, length: cur.length + next.length), withText: nextLine + curLine) tv.selectedRange = NSRange(location: cur.location + next.length, length: 0) } @objc func cmdDuplicate() { guard let tv = textView, let sel = tv.selectedRange else { return } let text = tv.text as NSString; let lr = text.lineRange(for: sel) let line = text.substring(with: lr) tv.replace(NSRange(location: lr.location + lr.length, length: 0), withText: line) } @objc func cmdDeleteLine() { guard let tv = textView, let sel = tv.selectedRange else { return } let text = tv.text as NSString tv.replace(text.lineRange(for: sel), withText: "") } @objc func cmdTop() { textView?.selectedRange = NSRange(location: 0, length: 0) textView?.scrollRangeToVisible(NSRange(location: 0, length: 0)) } @objc func cmdBottom() { guard let tv = textView else { return } let end = NSRange(location: (tv.text as NSString).length, length: 0) tv.selectedRange = end; tv.scrollRangeToVisible(end) } }