2016-06-05 10:49:14 +03:00
|
|
|
|
/**
|
|
|
|
|
* Tae Won Ha - http://taewon.de - @hataewon
|
|
|
|
|
* See LICENSE
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import Cocoa
|
2016-06-18 12:43:37 +03:00
|
|
|
|
|
2016-06-19 11:07:03 +03:00
|
|
|
|
/// Contiguous piece of cells of a row that has the same attributes.
|
2016-10-07 19:23:54 +03:00
|
|
|
|
fileprivate struct RowRun: CustomStringConvertible {
|
2016-06-18 12:43:37 +03:00
|
|
|
|
|
|
|
|
|
let row: Int
|
2016-09-25 18:50:33 +03:00
|
|
|
|
let range: CountableClosedRange<Int>
|
2016-06-18 12:43:37 +03:00
|
|
|
|
let attrs: CellAttributes
|
|
|
|
|
|
|
|
|
|
var description: String {
|
2016-06-19 11:07:03 +03:00
|
|
|
|
return "RowRun<\(row): \(range)\n\(attrs)>"
|
2016-06-18 12:43:37 +03:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-10-07 19:23:54 +03:00
|
|
|
|
public class NeoVimView: NSView, NSUserInterfaceValidations {
|
2016-09-24 17:31:14 +03:00
|
|
|
|
|
2016-09-25 09:55:26 +03:00
|
|
|
|
public struct Config {
|
|
|
|
|
let useInteractiveZsh: Bool
|
2016-09-24 17:31:14 +03:00
|
|
|
|
|
2016-09-25 09:55:26 +03:00
|
|
|
|
public init(useInteractiveZsh: Bool) {
|
|
|
|
|
self.useInteractiveZsh = useInteractiveZsh
|
|
|
|
|
}
|
|
|
|
|
}
|
2016-07-29 19:25:32 +03:00
|
|
|
|
|
2016-10-07 19:23:54 +03:00
|
|
|
|
public static let minFontSize = CGFloat(4)
|
|
|
|
|
public static let maxFontSize = CGFloat(128)
|
|
|
|
|
public static let defaultFont = NSFont.userFixedPitchFont(ofSize: 13)!
|
2016-10-26 23:59:48 +03:00
|
|
|
|
public static let defaultLinespacing = CGFloat(1)
|
|
|
|
|
|
|
|
|
|
public static let minLinespacing = CGFloat(0.5)
|
|
|
|
|
public static let maxLinespacing = CGFloat(8)
|
2016-07-14 22:53:20 +03:00
|
|
|
|
|
2016-10-07 19:23:54 +03:00
|
|
|
|
public let uuid = UUID().uuidString
|
|
|
|
|
public weak var delegate: NeoVimViewDelegate?
|
2016-07-30 10:49:39 +03:00
|
|
|
|
|
2016-10-07 19:23:54 +03:00
|
|
|
|
public fileprivate(set) var mode = Mode.Normal
|
2016-08-21 21:00:29 +03:00
|
|
|
|
|
2016-10-07 19:23:54 +03:00
|
|
|
|
public var usesLigatures = false {
|
2016-08-21 21:00:29 +03:00
|
|
|
|
didSet {
|
|
|
|
|
self.drawer.usesLigatures = self.usesLigatures
|
|
|
|
|
self.needsDisplay = true
|
|
|
|
|
}
|
|
|
|
|
}
|
2016-10-26 23:59:48 +03:00
|
|
|
|
|
|
|
|
|
public var linespacing: CGFloat {
|
|
|
|
|
get {
|
|
|
|
|
return self._linespacing
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
set {
|
|
|
|
|
guard newValue >= NeoVimView.minLinespacing && newValue <= NeoVimView.maxLinespacing else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self._linespacing = newValue
|
|
|
|
|
self.drawer.linespacing = self.linespacing
|
|
|
|
|
|
|
|
|
|
self.updateFontMetaData()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-10-07 19:23:54 +03:00
|
|
|
|
public var font: NSFont {
|
2016-08-21 21:00:29 +03:00
|
|
|
|
get {
|
|
|
|
|
return self._font
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
set {
|
2016-09-25 18:50:33 +03:00
|
|
|
|
guard newValue.isFixedPitch else {
|
2016-08-21 21:00:29 +03:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let size = newValue.pointSize
|
|
|
|
|
guard size >= NeoVimView.minFontSize && size <= NeoVimView.maxFontSize else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self._font = newValue
|
|
|
|
|
self.drawer.font = self.font
|
2016-10-26 23:42:49 +03:00
|
|
|
|
|
2016-10-26 23:59:48 +03:00
|
|
|
|
self.updateFontMetaData()
|
2016-08-21 21:00:29 +03:00
|
|
|
|
}
|
|
|
|
|
}
|
2016-09-02 19:22:40 +03:00
|
|
|
|
|
2016-10-07 19:23:54 +03:00
|
|
|
|
public var cwd: URL {
|
2016-09-02 19:22:40 +03:00
|
|
|
|
get {
|
2016-09-25 18:50:33 +03:00
|
|
|
|
return URL(fileURLWithPath: self.agent.vimCommandOutput("silent pwd"))
|
2016-09-02 19:22:40 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
set {
|
2016-09-25 18:50:33 +03:00
|
|
|
|
let path = newValue.path
|
2016-09-02 19:22:40 +03:00
|
|
|
|
let escapedCwd = self.agent.escapedFileName(path)
|
2016-09-03 11:17:22 +03:00
|
|
|
|
self.agent.vimCommand("silent cd \(escapedCwd)")
|
2016-09-02 19:22:40 +03:00
|
|
|
|
}
|
|
|
|
|
}
|
2016-09-27 19:02:05 +03:00
|
|
|
|
|
2016-10-07 19:23:54 +03:00
|
|
|
|
override public var acceptsFirstResponder: Bool {
|
2016-09-27 19:02:05 +03:00
|
|
|
|
return true
|
|
|
|
|
}
|
2016-10-18 23:45:44 +03:00
|
|
|
|
|
|
|
|
|
fileprivate static let emojis: [UInt32] = [
|
|
|
|
|
0x1F600...0x1F64F,
|
|
|
|
|
0x1F910...0x1F918,
|
|
|
|
|
0x1F980...0x1F984,
|
|
|
|
|
0x1F9C0...0x1F9C0
|
|
|
|
|
].flatMap { $0 }
|
2016-08-25 00:06:39 +03:00
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
fileprivate var _font = NeoVimView.defaultFont
|
2016-10-26 23:59:48 +03:00
|
|
|
|
fileprivate var _linespacing = NeoVimView.defaultLinespacing
|
2016-08-25 16:26:45 +03:00
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
fileprivate let agent: NeoVimAgent
|
|
|
|
|
fileprivate let drawer: TextDrawer
|
|
|
|
|
fileprivate let fontManager = NSFontManager.shared()
|
|
|
|
|
fileprivate let pasteboard = NSPasteboard.general()
|
2016-07-10 15:14:02 +03:00
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
fileprivate let grid = Grid()
|
2016-07-16 01:07:50 +03:00
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
fileprivate var markedText: String?
|
2016-06-24 22:08:34 +03:00
|
|
|
|
|
2016-07-02 22:38:52 +03:00
|
|
|
|
/// We store the last marked text because Cocoa's text input system does the following:
|
|
|
|
|
/// 하 -> hanja popup -> insertText(하) -> attributedSubstring...() -> setMarkedText(下) -> ...
|
|
|
|
|
/// We want to return "하" in attributedSubstring...()
|
2016-09-25 18:50:33 +03:00
|
|
|
|
fileprivate var lastMarkedText: String?
|
2016-07-02 22:38:52 +03:00
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
fileprivate var markedPosition = Position.null
|
|
|
|
|
fileprivate var keyDownDone = true
|
2016-07-14 00:08:45 +03:00
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
fileprivate var lastClickedCellPosition = Position.null
|
2016-06-06 19:25:03 +03:00
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
fileprivate var xOffset = CGFloat(0)
|
|
|
|
|
fileprivate var yOffset = CGFloat(0)
|
|
|
|
|
fileprivate var cellSize = CGSize.zero
|
|
|
|
|
fileprivate var descent = CGFloat(0)
|
|
|
|
|
fileprivate var leading = CGFloat(0)
|
|
|
|
|
|
|
|
|
|
fileprivate let maxScrollDeltaX = 30
|
|
|
|
|
fileprivate let maxScrollDeltaY = 30
|
|
|
|
|
fileprivate let scrollLimiterX = CGFloat(20)
|
|
|
|
|
fileprivate let scrollLimiterY = CGFloat(20)
|
|
|
|
|
fileprivate var scrollGuardCounterX = 5
|
|
|
|
|
fileprivate var scrollGuardCounterY = 5
|
|
|
|
|
fileprivate let scrollGuardYield = 5
|
2016-07-29 23:01:09 +03:00
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
fileprivate var isCurrentlyPinching = false
|
|
|
|
|
fileprivate var pinchTargetScale = CGFloat(1)
|
|
|
|
|
fileprivate var pinchImage = NSImage()
|
2016-09-24 17:31:14 +03:00
|
|
|
|
|
2016-09-27 19:02:05 +03:00
|
|
|
|
fileprivate var currentlyResizing = false
|
2016-10-18 23:45:44 +03:00
|
|
|
|
fileprivate var currentEmoji = "😎"
|
|
|
|
|
fileprivate let emojiAttrs = [
|
|
|
|
|
NSFontAttributeName: NSFont(name: "AppleColorEmoji", size: 72)!
|
|
|
|
|
]
|
|
|
|
|
fileprivate let resizeTextAttrs = [
|
|
|
|
|
NSFontAttributeName: NSFont.systemFont(ofSize: 18),
|
|
|
|
|
NSForegroundColorAttributeName: NSColor.darkGray
|
|
|
|
|
]
|
2016-09-27 19:02:05 +03:00
|
|
|
|
|
2016-09-25 09:55:26 +03:00
|
|
|
|
public init(frame rect: NSRect, config: Config) {
|
2016-10-26 23:42:49 +03:00
|
|
|
|
self.drawer = TextDrawer(font: self._font)
|
2016-07-10 15:14:02 +03:00
|
|
|
|
self.agent = NeoVimAgent(uuid: self.uuid)
|
2016-07-14 22:53:20 +03:00
|
|
|
|
|
2016-09-24 17:31:14 +03:00
|
|
|
|
super.init(frame: CGRect.zero)
|
2016-09-27 19:02:05 +03:00
|
|
|
|
|
2016-06-15 23:11:35 +03:00
|
|
|
|
self.wantsLayer = true
|
2016-06-18 12:43:37 +03:00
|
|
|
|
self.cellSize = self.drawer.cellSize
|
2016-06-19 14:39:20 +03:00
|
|
|
|
self.descent = self.drawer.descent
|
|
|
|
|
self.leading = self.drawer.leading
|
2016-07-09 23:52:59 +03:00
|
|
|
|
|
|
|
|
|
// We cannot set bridge in init since self is not available before super.init()...
|
|
|
|
|
self.agent.bridge = self
|
2016-09-25 09:55:26 +03:00
|
|
|
|
self.agent.useInteractiveZsh = config.useInteractiveZsh
|
2016-08-26 18:45:58 +03:00
|
|
|
|
let noErrorDuringInitialization = self.agent.runLocalServerAndNeoVim()
|
2016-08-25 23:52:31 +03:00
|
|
|
|
|
2016-08-21 18:32:35 +03:00
|
|
|
|
// Neovim is ready now: resize neovim to bounds.
|
|
|
|
|
DispatchUtils.gui {
|
2016-08-25 23:52:31 +03:00
|
|
|
|
if noErrorDuringInitialization == false {
|
|
|
|
|
let alert = NSAlert()
|
2016-09-25 18:50:33 +03:00
|
|
|
|
alert.alertStyle = .warning
|
2016-08-25 23:52:31 +03:00
|
|
|
|
alert.messageText = "Error during initialization"
|
|
|
|
|
alert.informativeText = "There was an error during the initialization of NeoVim. "
|
|
|
|
|
+ "Use :messages to view the error messages."
|
|
|
|
|
alert.runModal()
|
|
|
|
|
}
|
|
|
|
|
|
2016-08-21 18:32:35 +03:00
|
|
|
|
self.resizeNeoVimUiTo(size: self.bounds.size)
|
|
|
|
|
}
|
2016-07-09 23:52:59 +03:00
|
|
|
|
}
|
2016-07-17 15:41:53 +03:00
|
|
|
|
|
2016-09-24 17:31:14 +03:00
|
|
|
|
convenience override init(frame rect: NSRect) {
|
2016-09-25 09:55:26 +03:00
|
|
|
|
self.init(frame: rect, config: Config(useInteractiveZsh: false))
|
2016-09-24 17:31:14 +03:00
|
|
|
|
}
|
|
|
|
|
|
2016-08-12 15:54:37 +03:00
|
|
|
|
required public init?(coder: NSCoder) {
|
|
|
|
|
fatalError("init(coder:) has not been implemented")
|
|
|
|
|
}
|
2016-06-15 23:11:35 +03:00
|
|
|
|
|
2016-10-07 19:23:54 +03:00
|
|
|
|
@IBAction public func debug1(_ sender: AnyObject!) {
|
2016-09-03 11:17:22 +03:00
|
|
|
|
NSLog("DEBUG 1 - Start")
|
2016-10-25 00:30:21 +03:00
|
|
|
|
let buffers = self.agent.tabs().map { $0.allBuffers() }.flatMap { $0 }
|
|
|
|
|
NSLog("\(Set(buffers))")
|
2016-09-03 11:17:22 +03:00
|
|
|
|
NSLog("DEBUG 1 - End")
|
2016-08-04 21:01:03 +03:00
|
|
|
|
}
|
2016-10-26 23:59:48 +03:00
|
|
|
|
|
|
|
|
|
fileprivate func updateFontMetaData() {
|
|
|
|
|
self.cellSize = self.drawer.cellSize
|
|
|
|
|
self.descent = self.drawer.descent
|
|
|
|
|
self.leading = self.drawer.leading
|
|
|
|
|
|
|
|
|
|
self.resizeNeoVimUiTo(size: self.bounds.size)
|
|
|
|
|
}
|
2016-08-12 15:54:37 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: - API
|
|
|
|
|
extension NeoVimView {
|
2016-09-27 19:02:05 +03:00
|
|
|
|
|
|
|
|
|
public func enterResizeMode() {
|
|
|
|
|
self.currentlyResizing = true
|
|
|
|
|
self.needsDisplay = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public func exitResizeMode() {
|
|
|
|
|
self.currentlyResizing = false
|
|
|
|
|
self.needsDisplay = true
|
|
|
|
|
self.resizeNeoVimUiTo(size: self.bounds.size)
|
|
|
|
|
}
|
2016-10-12 20:41:18 +03:00
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
- returns: nil when for exampls a quickfix panel is open.
|
|
|
|
|
*/
|
|
|
|
|
public func currentBuffer() -> NeoVimBuffer? {
|
2016-10-22 18:35:36 +03:00
|
|
|
|
return self.agent.buffers().filter { $0.isCurrent }.first
|
2016-08-20 20:02:16 +03:00
|
|
|
|
}
|
2016-08-12 15:54:37 +03:00
|
|
|
|
|
|
|
|
|
public func hasDirtyDocs() -> Bool {
|
|
|
|
|
return self.agent.hasDirtyDocs()
|
|
|
|
|
}
|
2016-08-13 00:07:16 +03:00
|
|
|
|
|
|
|
|
|
public func isCurrentBufferDirty() -> Bool {
|
2016-10-12 20:41:18 +03:00
|
|
|
|
let curBuf = self.currentBuffer()
|
2016-10-22 18:35:36 +03:00
|
|
|
|
return curBuf?.isDirty ?? true
|
2016-08-13 00:07:16 +03:00
|
|
|
|
}
|
2016-08-12 15:54:37 +03:00
|
|
|
|
|
|
|
|
|
public func newTab() {
|
2016-08-20 17:59:30 +03:00
|
|
|
|
self.exec(command: "tabe")
|
2016-08-12 15:54:37 +03:00
|
|
|
|
}
|
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
public func open(urls: [URL]) {
|
2016-10-23 20:42:51 +03:00
|
|
|
|
let tabs = self.agent.tabs()
|
|
|
|
|
let buffers = tabs.map { $0.allBuffers() }.flatMap { $0 }
|
|
|
|
|
let currentBufferIsTransient = buffers.filter { $0.isCurrent }.first?.isTransient ?? false
|
2016-08-12 15:54:37 +03:00
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
urls.enumerated().forEach { (idx, url) in
|
2016-10-23 20:42:51 +03:00
|
|
|
|
let path = url.path
|
|
|
|
|
if buffers.filter({ $0.fileName == path }).first != nil {
|
|
|
|
|
for window in tabs.map({ $0.windows }).flatMap({ $0 }) {
|
|
|
|
|
|
|
|
|
|
if window.buffer.fileName == path {
|
|
|
|
|
Swift.print(window)
|
|
|
|
|
self.agent.select(window)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-08-12 15:54:37 +03:00
|
|
|
|
if idx == 0 && currentBufferIsTransient {
|
2016-08-20 17:59:30 +03:00
|
|
|
|
self.open(url, cmd: "e")
|
2016-08-12 15:54:37 +03:00
|
|
|
|
} else {
|
2016-08-20 17:59:30 +03:00
|
|
|
|
self.open(url, cmd: "tabe")
|
2016-08-12 15:54:37 +03:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
public func openInNewTab(urls: [URL]) {
|
2016-08-20 17:59:30 +03:00
|
|
|
|
urls.forEach { self.open($0, cmd: "tabe") }
|
2016-08-12 15:54:37 +03:00
|
|
|
|
}
|
2016-08-20 20:02:16 +03:00
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
public func openInCurrentTab(url: URL) {
|
2016-08-20 20:02:16 +03:00
|
|
|
|
self.open(url, cmd: "e")
|
|
|
|
|
}
|
2016-08-13 00:07:16 +03:00
|
|
|
|
|
|
|
|
|
public func closeCurrentTab() {
|
2016-08-20 17:59:30 +03:00
|
|
|
|
self.exec(command: "q")
|
2016-08-13 00:07:16 +03:00
|
|
|
|
}
|
2016-08-20 20:02:16 +03:00
|
|
|
|
|
|
|
|
|
public func saveCurrentTab() {
|
|
|
|
|
self.exec(command: "w")
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
public func saveCurrentTab(url: URL) {
|
|
|
|
|
let path = url.path
|
2016-08-25 00:06:39 +03:00
|
|
|
|
let escapedFileName = self.agent.escapedFileName(path)
|
2016-08-20 20:02:16 +03:00
|
|
|
|
self.exec(command: "w \(escapedFileName)")
|
|
|
|
|
}
|
2016-08-13 00:07:16 +03:00
|
|
|
|
|
|
|
|
|
public func closeCurrentTabWithoutSaving() {
|
2016-08-20 17:59:30 +03:00
|
|
|
|
self.exec(command: "q!")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public func closeAllWindows() {
|
|
|
|
|
self.exec(command: "qa")
|
2016-08-13 00:07:16 +03:00
|
|
|
|
}
|
2016-08-12 15:54:37 +03:00
|
|
|
|
|
2016-08-12 19:00:05 +03:00
|
|
|
|
public func closeAllWindowsWithoutSaving() {
|
2016-08-20 17:59:30 +03:00
|
|
|
|
self.exec(command: "qa!")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Does the following
|
|
|
|
|
/// - `Mode.Normal`: `:command<CR>`
|
|
|
|
|
/// - else: `:<Esc>:command<CR>`
|
2016-09-25 18:50:33 +03:00
|
|
|
|
fileprivate func exec(command cmd: String) {
|
2016-08-12 19:00:05 +03:00
|
|
|
|
switch self.mode {
|
|
|
|
|
case .Normal:
|
2016-08-20 17:59:30 +03:00
|
|
|
|
self.agent.vimInput(":\(cmd)<CR>")
|
2016-08-12 19:00:05 +03:00
|
|
|
|
default:
|
2016-08-20 17:59:30 +03:00
|
|
|
|
self.agent.vimInput("<Esc>:\(cmd)<CR>")
|
2016-08-12 19:00:05 +03:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
fileprivate func open(_ url: URL, cmd: String) {
|
|
|
|
|
let path = url.path
|
2016-08-25 00:06:39 +03:00
|
|
|
|
let escapedFileName = self.agent.escapedFileName(path)
|
2016-08-20 17:59:30 +03:00
|
|
|
|
self.exec(command: "\(cmd) \(escapedFileName)")
|
2016-08-12 15:54:37 +03:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: - Resizing
|
|
|
|
|
extension NeoVimView {
|
2016-06-27 19:42:37 +03:00
|
|
|
|
|
2016-10-07 19:23:54 +03:00
|
|
|
|
override public func setFrameSize(_ newSize: NSSize) {
|
2016-07-06 20:11:53 +03:00
|
|
|
|
super.setFrameSize(newSize)
|
|
|
|
|
|
|
|
|
|
// initial resizing is done when grid has data
|
|
|
|
|
guard self.grid.hasData else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-27 19:02:05 +03:00
|
|
|
|
if self.inLiveResize || self.currentlyResizing {
|
2016-08-01 08:46:04 +03:00
|
|
|
|
// TODO: Turn off live resizing for now.
|
2016-07-06 20:11:53 +03:00
|
|
|
|
// self.resizeNeoVimUiTo(size: newSize)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// There can be cases where the frame is resized not by live resizing, eg when the window is resized by window
|
|
|
|
|
// management tools. Thus, we make sure that the resize call is made when this happens.
|
|
|
|
|
self.resizeNeoVimUiTo(size: newSize)
|
|
|
|
|
}
|
2016-07-04 23:06:39 +03:00
|
|
|
|
|
2016-10-07 19:23:54 +03:00
|
|
|
|
override public func viewDidEndLiveResize() {
|
2016-07-04 23:06:39 +03:00
|
|
|
|
super.viewDidEndLiveResize()
|
|
|
|
|
self.resizeNeoVimUiTo(size: self.bounds.size)
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
fileprivate func resizeNeoVimUiTo(size: CGSize) {
|
2016-10-18 23:45:44 +03:00
|
|
|
|
self.currentEmoji = self.randomEmoji()
|
|
|
|
|
|
2016-07-10 14:51:00 +03:00
|
|
|
|
// NSLog("\(#function): \(size)")
|
2016-08-15 22:53:40 +03:00
|
|
|
|
let discreteSize = self.discreteSize(size: size)
|
2016-07-04 23:06:39 +03:00
|
|
|
|
|
2016-09-27 19:02:05 +03:00
|
|
|
|
if discreteSize == self.grid.size {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2016-07-04 23:06:39 +03:00
|
|
|
|
self.xOffset = floor((size.width - self.cellSize.width * CGFloat(discreteSize.width)) / 2)
|
|
|
|
|
self.yOffset = floor((size.height - self.cellSize.height * CGFloat(discreteSize.height)) / 2)
|
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
self.agent.resize(toWidth: Int32(discreteSize.width), height: Int32(discreteSize.height))
|
2016-07-04 23:06:39 +03:00
|
|
|
|
}
|
2016-08-15 22:53:40 +03:00
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
fileprivate func discreteSize(size: CGSize) -> Size {
|
2016-08-15 22:53:40 +03:00
|
|
|
|
return Size(width: Int(floor(size.width / self.cellSize.width)),
|
|
|
|
|
height: Int(floor(size.height / self.cellSize.height)))
|
|
|
|
|
}
|
2016-08-12 15:54:37 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: - Drawing
|
|
|
|
|
extension NeoVimView {
|
2016-07-04 23:06:39 +03:00
|
|
|
|
|
2016-10-07 19:23:54 +03:00
|
|
|
|
override public func draw(_ dirtyUnionRect: NSRect) {
|
2016-06-15 23:11:35 +03:00
|
|
|
|
guard self.grid.hasData else {
|
|
|
|
|
return
|
|
|
|
|
}
|
2016-06-27 19:42:37 +03:00
|
|
|
|
|
2016-09-27 19:02:05 +03:00
|
|
|
|
if self.inLiveResize || self.currentlyResizing {
|
2016-09-25 18:50:33 +03:00
|
|
|
|
NSColor.windowBackgroundColor.set()
|
2016-07-04 23:06:39 +03:00
|
|
|
|
dirtyUnionRect.fill()
|
2016-08-15 22:53:40 +03:00
|
|
|
|
|
|
|
|
|
let boundsSize = self.bounds.size
|
2016-10-18 23:45:44 +03:00
|
|
|
|
|
|
|
|
|
let emojiSize = self.currentEmoji.size(withAttributes: self.emojiAttrs)
|
|
|
|
|
let emojiX = (boundsSize.width - emojiSize.width) / 2
|
|
|
|
|
let emojiY = (boundsSize.height - emojiSize.height) / 2
|
|
|
|
|
|
2016-08-15 22:53:40 +03:00
|
|
|
|
let discreteSize = self.discreteSize(size: boundsSize)
|
|
|
|
|
let displayStr = "\(discreteSize.width) × \(discreteSize.height)"
|
2016-10-18 23:45:44 +03:00
|
|
|
|
|
|
|
|
|
let size = displayStr.size(withAttributes: self.resizeTextAttrs)
|
2016-08-15 22:53:40 +03:00
|
|
|
|
let x = (boundsSize.width - size.width) / 2
|
2016-10-18 23:45:44 +03:00
|
|
|
|
let y = emojiY - size.height
|
|
|
|
|
|
|
|
|
|
self.currentEmoji.draw(at: CGPoint(x: emojiX, y: emojiY), withAttributes: self.emojiAttrs)
|
|
|
|
|
displayStr.draw(at: CGPoint(x: x, y: y), withAttributes: self.resizeTextAttrs)
|
|
|
|
|
|
2016-07-04 23:06:39 +03:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2016-07-03 20:12:25 +03:00
|
|
|
|
// NSLog("\(#function): \(dirtyUnionRect)")
|
2016-09-25 18:50:33 +03:00
|
|
|
|
let context = NSGraphicsContext.current()!.cgContext
|
2016-07-29 23:01:09 +03:00
|
|
|
|
|
|
|
|
|
if self.isCurrentlyPinching {
|
|
|
|
|
let boundsSize = self.bounds.size
|
|
|
|
|
let targetSize = CGSize(width: boundsSize.width * self.pinchTargetScale,
|
|
|
|
|
height: boundsSize.height * self.pinchTargetScale)
|
2016-09-25 18:50:33 +03:00
|
|
|
|
self.pinchImage.draw(in: CGRect(origin: self.bounds.origin, size: targetSize))
|
2016-07-29 23:01:09 +03:00
|
|
|
|
return
|
|
|
|
|
}
|
2016-06-08 19:17:12 +03:00
|
|
|
|
|
2016-10-25 00:45:33 +03:00
|
|
|
|
// When both anti-aliasing and font smoothing is turned on, then the "Use LCD font smoothing when available" setting
|
|
|
|
|
// is used to render texts, cf. chapter 11 from "Programming with Quartz".
|
|
|
|
|
context.setShouldSmoothFonts(true);
|
2016-09-25 18:50:33 +03:00
|
|
|
|
context.textMatrix = CGAffineTransform.identity;
|
|
|
|
|
context.setTextDrawingMode(.fill);
|
2016-06-08 19:17:12 +03:00
|
|
|
|
|
2016-06-16 19:36:26 +03:00
|
|
|
|
let dirtyRects = self.rectsBeingDrawn()
|
2016-07-31 00:04:20 +03:00
|
|
|
|
// NSLog("\(dirtyRects)")
|
2016-06-18 12:43:37 +03:00
|
|
|
|
|
2016-09-11 19:22:25 +03:00
|
|
|
|
self.rowRunIntersecting(rects: dirtyRects).forEach { self.draw(rowRun: $0, context: context) }
|
|
|
|
|
self.drawCursor(context: context)
|
|
|
|
|
}
|
|
|
|
|
|
2016-10-18 23:45:44 +03:00
|
|
|
|
fileprivate func randomEmoji() -> String {
|
|
|
|
|
let idx = Int(arc4random_uniform(UInt32(NeoVimView.emojis.count)))
|
|
|
|
|
guard let scalar = UnicodeScalar(NeoVimView.emojis[idx]) else {
|
|
|
|
|
return "😎"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return String(scalar)
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
fileprivate func draw(rowRun rowFrag: RowRun, context: CGContext) {
|
2016-09-11 19:22:25 +03:00
|
|
|
|
// For background drawing we don't filter out the put(0, 0)s: in some cases only the put(0, 0)-cells should be
|
|
|
|
|
// redrawn. => FIXME: probably we have to consider this also when drawing further down, ie when the range starts
|
|
|
|
|
// with '0'...
|
|
|
|
|
self.drawBackground(positions: rowFrag.range.map { self.pointInViewFor(row: rowFrag.row, column: $0) },
|
|
|
|
|
background: rowFrag.attrs.background)
|
2016-06-18 12:43:37 +03:00
|
|
|
|
|
2016-09-11 19:22:25 +03:00
|
|
|
|
let positions = rowFrag.range
|
|
|
|
|
// filter out the put(0, 0)s (after a wide character)
|
|
|
|
|
.filter { self.grid.cells[rowFrag.row][$0].string.characters.count > 0 }
|
|
|
|
|
.map { self.pointInViewFor(row: rowFrag.row, column: $0) }
|
|
|
|
|
|
|
|
|
|
if positions.isEmpty {
|
|
|
|
|
return
|
2016-06-08 19:17:12 +03:00
|
|
|
|
}
|
2016-06-29 20:54:06 +03:00
|
|
|
|
|
2016-09-11 19:22:25 +03:00
|
|
|
|
let string = self.grid.cells[rowFrag.row][rowFrag.range].reduce("") { $0 + $1.string }
|
2016-10-26 23:42:49 +03:00
|
|
|
|
let offset = self.drawer.baselineOffset
|
|
|
|
|
let glyphPositions = positions.map { CGPoint(x: $0.x, y: $0.y + offset) }
|
2016-09-15 21:42:43 +03:00
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
self.drawer.draw(string,
|
2016-10-09 17:09:09 +03:00
|
|
|
|
positions: UnsafeMutablePointer(mutating: glyphPositions), positionsCount: positions.count,
|
|
|
|
|
highlightAttrs: rowFrag.attrs,
|
|
|
|
|
context: context)
|
2016-06-29 20:54:06 +03:00
|
|
|
|
}
|
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
fileprivate func cursorRegion() -> Region {
|
2016-08-07 17:15:33 +03:00
|
|
|
|
let cursorPosition = self.mode == .Cmdline ? self.grid.putPosition : self.grid.screenCursor
|
2016-07-11 20:37:06 +03:00
|
|
|
|
// NSLog("\(#function): \(cursorPosition)")
|
2016-06-29 20:54:06 +03:00
|
|
|
|
|
2016-09-19 09:22:46 +03:00
|
|
|
|
let saneRow = max(0, min(cursorPosition.row, self.grid.size.height - 1))
|
|
|
|
|
let saneColumn = max(0, min(cursorPosition.column, self.grid.size.width - 1))
|
|
|
|
|
|
|
|
|
|
var cursorRegion = Region(top: saneRow, bottom: saneRow, left: saneColumn, right: saneColumn)
|
2016-09-11 19:22:25 +03:00
|
|
|
|
|
2016-06-29 21:06:31 +03:00
|
|
|
|
if self.grid.isNextCellEmpty(cursorPosition) {
|
2016-09-11 19:22:25 +03:00
|
|
|
|
cursorRegion = Region(top: cursorPosition.row,
|
|
|
|
|
bottom: cursorPosition.row,
|
|
|
|
|
left: cursorPosition.column,
|
|
|
|
|
right: min(self.grid.size.width - 1, cursorPosition.column + 1))
|
2016-06-29 20:54:06 +03:00
|
|
|
|
}
|
|
|
|
|
|
2016-09-11 19:22:25 +03:00
|
|
|
|
return cursorRegion
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
fileprivate func drawCursor(context: CGContext) {
|
2016-09-11 19:22:25 +03:00
|
|
|
|
// FIXME: for now do some rudimentary cursor drawing
|
|
|
|
|
let cursorRegion = self.cursorRegion()
|
|
|
|
|
let cursorRow = cursorRegion.top
|
|
|
|
|
let cursorColumnStart = cursorRegion.left
|
2016-09-15 21:42:43 +03:00
|
|
|
|
let attrsAtCursor = self.grid.cells[cursorRow][cursorColumnStart].attrs
|
|
|
|
|
let attrs = CellAttributes(fontTrait: attrsAtCursor.fontTrait,
|
|
|
|
|
foreground: self.grid.background,
|
|
|
|
|
background: self.grid.foreground,
|
|
|
|
|
special: self.grid.special)
|
|
|
|
|
|
|
|
|
|
// FIXME: take ligatures into account (is it a good idea to do this?)
|
|
|
|
|
let rowRun = RowRun(row: cursorRegion.top, range: cursorRegion.columnRange, attrs: attrs)
|
2016-09-11 19:22:25 +03:00
|
|
|
|
self.draw(rowRun: rowRun, context: context)
|
2016-06-15 23:11:35 +03:00
|
|
|
|
}
|
2016-06-08 19:17:12 +03:00
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
fileprivate func drawBackground(positions: [CGPoint], background: UInt32) {
|
2016-07-03 22:59:03 +03:00
|
|
|
|
ColorUtils.colorIgnoringAlpha(background).set()
|
2016-08-01 22:33:52 +03:00
|
|
|
|
// NSColor(calibratedRed: CGFloat(drand48()), green: CGFloat(drand48()), blue: CGFloat(drand48()), alpha: 1.0).set()
|
2016-06-16 19:36:26 +03:00
|
|
|
|
let backgroundRect = CGRect(
|
|
|
|
|
x: positions[0].x, y: positions[0].y,
|
2016-07-13 21:16:04 +03:00
|
|
|
|
width: CGFloat(positions.count) * self.cellSize.width, height: self.cellSize.height
|
2016-06-16 19:36:26 +03:00
|
|
|
|
)
|
|
|
|
|
backgroundRect.fill()
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
fileprivate func rowRunIntersecting(rects: [CGRect]) -> [RowRun] {
|
2016-06-15 23:11:35 +03:00
|
|
|
|
return rects
|
2016-09-25 18:50:33 +03:00
|
|
|
|
.map { rect -> (CountableClosedRange<Int>, CountableClosedRange<Int>) in
|
2016-06-19 14:39:20 +03:00
|
|
|
|
// Get all Regions that intersects with the given rects. There can be overlaps between the Regions, but for the
|
|
|
|
|
// time being we ignore them; probably not necessary to optimize them away.
|
2016-07-06 20:02:30 +03:00
|
|
|
|
let region = self.regionFor(rect: rect)
|
|
|
|
|
return (region.rowRange, region.columnRange)
|
2016-06-19 14:39:20 +03:00
|
|
|
|
}
|
2016-07-06 20:02:30 +03:00
|
|
|
|
.map { self.rowRunsFor(rowRange: $0, columnRange: $1) } // All RowRuns for all Regions grouped by their row range.
|
|
|
|
|
.flatMap { $0 } // Flattened RowRuns for all Regions.
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
fileprivate func rowRunsFor(rowRange: CountableClosedRange<Int>, columnRange: CountableClosedRange<Int>) -> [RowRun] {
|
2016-07-06 20:02:30 +03:00
|
|
|
|
return rowRange
|
|
|
|
|
.map { (row) -> [RowRun] in
|
|
|
|
|
let rowCells = self.grid.cells[row]
|
2016-09-25 18:50:33 +03:00
|
|
|
|
let startIdx = columnRange.lowerBound
|
2016-07-06 20:02:30 +03:00
|
|
|
|
|
|
|
|
|
var result = [ RowRun(row: row, range: startIdx...startIdx, attrs: rowCells[startIdx].attrs) ]
|
|
|
|
|
columnRange.forEach { idx in
|
|
|
|
|
if rowCells[idx].attrs == result.last!.attrs {
|
|
|
|
|
let last = result.popLast()!
|
2016-09-25 18:50:33 +03:00
|
|
|
|
result.append(RowRun(row: row, range: last.range.lowerBound...idx, attrs: last.attrs))
|
2016-07-06 20:02:30 +03:00
|
|
|
|
} else {
|
|
|
|
|
result.append(RowRun(row: row, range: idx...idx, attrs: rowCells[idx].attrs))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result // All RowRuns for a row in a Region.
|
|
|
|
|
} // All RowRuns for all rows in a Region grouped by row.
|
|
|
|
|
.flatMap { $0 } // Flattened RowRuns for a Region.
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
fileprivate func regionFor(rect: CGRect) -> Region {
|
2016-08-01 08:46:04 +03:00
|
|
|
|
let cellWidth = self.cellSize.width
|
|
|
|
|
let cellHeight = self.cellSize.height
|
|
|
|
|
|
2016-07-06 20:02:30 +03:00
|
|
|
|
let rowStart = max(
|
2016-08-01 08:46:04 +03:00
|
|
|
|
Int(floor((self.bounds.height - self.yOffset - (rect.origin.y + rect.size.height)) / cellHeight)), 0
|
2016-07-06 20:02:30 +03:00
|
|
|
|
)
|
|
|
|
|
let rowEnd = min(
|
2016-08-03 23:33:33 +03:00
|
|
|
|
Int(ceil((self.bounds.height - self.yOffset - rect.origin.y) / cellHeight)) - 1, self.grid.size.height - 1
|
2016-07-06 20:02:30 +03:00
|
|
|
|
)
|
|
|
|
|
let columnStart = max(
|
2016-08-01 08:46:04 +03:00
|
|
|
|
Int(floor((rect.origin.x - self.xOffset) / cellWidth)), 0
|
2016-07-06 20:02:30 +03:00
|
|
|
|
)
|
|
|
|
|
let columnEnd = min(
|
2016-08-01 08:46:04 +03:00
|
|
|
|
Int(ceil((rect.origin.x - self.xOffset + rect.size.width) / cellWidth)) - 1, self.grid.size.width - 1
|
2016-07-06 20:02:30 +03:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return Region(top: rowStart, bottom: rowEnd, left: columnStart, right: columnEnd)
|
2016-06-05 10:49:14 +03:00
|
|
|
|
}
|
2016-07-01 22:24:42 +03:00
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
fileprivate func pointInViewFor(position: Position) -> CGPoint {
|
2016-07-05 21:13:46 +03:00
|
|
|
|
return self.pointInViewFor(row: position.row, column: position.column)
|
2016-07-01 22:24:42 +03:00
|
|
|
|
}
|
2016-06-05 10:49:14 +03:00
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
fileprivate func pointInViewFor(row: Int, column: Int) -> CGPoint {
|
2016-06-08 19:17:12 +03:00
|
|
|
|
return CGPoint(
|
2016-07-31 00:04:20 +03:00
|
|
|
|
x: self.xOffset + CGFloat(column) * self.cellSize.width,
|
2016-08-01 08:46:04 +03:00
|
|
|
|
y: self.bounds.size.height - self.yOffset - CGFloat(row) * self.cellSize.height - self.cellSize.height
|
2016-06-08 19:17:12 +03:00
|
|
|
|
)
|
|
|
|
|
}
|
2016-06-24 22:08:34 +03:00
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
fileprivate func cellRectFor(row: Int, column: Int) -> CGRect {
|
2016-07-05 21:13:46 +03:00
|
|
|
|
return CGRect(origin: self.pointInViewFor(row: row, column: column), size: self.cellSize)
|
2016-06-24 22:08:34 +03:00
|
|
|
|
}
|
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
fileprivate func regionRectFor(region: Region) -> CGRect {
|
2016-07-04 20:04:49 +03:00
|
|
|
|
let top = CGFloat(region.top)
|
|
|
|
|
let bottom = CGFloat(region.bottom)
|
|
|
|
|
let left = CGFloat(region.left)
|
|
|
|
|
let right = CGFloat(region.right)
|
|
|
|
|
|
|
|
|
|
let width = right - left + 1
|
|
|
|
|
let height = bottom - top + 1
|
|
|
|
|
|
2016-07-31 00:04:20 +03:00
|
|
|
|
let cellWidth = self.cellSize.width
|
|
|
|
|
let cellHeight = self.cellSize.height
|
|
|
|
|
|
2016-07-04 20:04:49 +03:00
|
|
|
|
return CGRect(
|
2016-07-31 00:04:20 +03:00
|
|
|
|
x: self.xOffset + left * cellWidth,
|
|
|
|
|
y: self.bounds.size.height - self.yOffset - top * cellHeight - height * cellHeight,
|
|
|
|
|
width: width * cellWidth,
|
|
|
|
|
height: height * cellHeight
|
2016-07-04 20:04:49 +03:00
|
|
|
|
)
|
2016-06-27 19:42:37 +03:00
|
|
|
|
}
|
2016-06-27 20:04:27 +03:00
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
fileprivate func wrapNamedKeys(_ string: String) -> String {
|
2016-06-27 20:04:27 +03:00
|
|
|
|
return "<\(string)>"
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
fileprivate func vimPlainString(_ string: String) -> String {
|
|
|
|
|
return string.replacingOccurrences(of: "<", with: self.wrapNamedKeys("lt"))
|
2016-06-27 20:04:27 +03:00
|
|
|
|
}
|
2016-06-05 10:49:14 +03:00
|
|
|
|
}
|
2016-07-16 01:07:50 +03:00
|
|
|
|
|
2016-08-05 00:34:09 +03:00
|
|
|
|
// MARK: - NSUserInterfaceValidationsProtocol
|
|
|
|
|
extension NeoVimView {
|
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
public func validateUserInterfaceItem(_ item: NSValidatedUserInterfaceItem) -> Bool {
|
2016-08-09 09:22:10 +03:00
|
|
|
|
let canUndoOrRedo = self.mode == .Insert || self.mode == .Replace || self.mode == .Normal || self.mode == .Visual
|
|
|
|
|
let canCopyOrCut = self.mode == .Normal || self.mode == .Visual
|
2016-09-25 18:50:33 +03:00
|
|
|
|
let canPaste = self.pasteboard.string(forType: NSPasteboardTypeString) != nil
|
2016-08-09 09:22:10 +03:00
|
|
|
|
let canDelete = self.mode == .Visual || self.mode == .Normal
|
|
|
|
|
let canSelectAll = self.mode == .Insert || self.mode == .Replace || self.mode == .Normal || self.mode == .Visual
|
2016-08-05 00:34:09 +03:00
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
guard let action = item.action else {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch action {
|
2016-09-25 19:10:07 +03:00
|
|
|
|
case #selector(undo(_:)), #selector(redo(_:)):
|
2016-08-09 09:22:10 +03:00
|
|
|
|
return canUndoOrRedo
|
2016-09-25 19:10:07 +03:00
|
|
|
|
case #selector(copy(_:)), #selector(cut(_:)):
|
2016-08-05 00:34:09 +03:00
|
|
|
|
return canCopyOrCut
|
2016-09-25 19:10:07 +03:00
|
|
|
|
case #selector(paste(_:)):
|
2016-08-09 09:22:10 +03:00
|
|
|
|
return canPaste
|
2016-09-25 19:10:07 +03:00
|
|
|
|
case #selector(delete(_:)):
|
2016-08-09 09:22:10 +03:00
|
|
|
|
return canDelete
|
2016-09-25 19:10:07 +03:00
|
|
|
|
case #selector(selectAll(_:)):
|
2016-08-09 09:22:10 +03:00
|
|
|
|
return canSelectAll
|
2016-08-05 00:34:09 +03:00
|
|
|
|
default:
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-08-04 21:01:03 +03:00
|
|
|
|
// MARK: - Edit Menu Items
|
|
|
|
|
extension NeoVimView {
|
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
@IBAction func undo(_ sender: AnyObject!) {
|
2016-08-09 09:22:10 +03:00
|
|
|
|
switch self.mode {
|
|
|
|
|
case .Insert, .Replace:
|
|
|
|
|
self.agent.vimInput("<Esc>ui")
|
|
|
|
|
case .Normal, .Visual:
|
|
|
|
|
self.agent.vimInput("u")
|
|
|
|
|
default:
|
2016-08-04 21:01:03 +03:00
|
|
|
|
return
|
|
|
|
|
}
|
2016-08-09 09:22:10 +03:00
|
|
|
|
}
|
2016-08-04 21:01:03 +03:00
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
@IBAction func redo(_ sender: AnyObject!) {
|
2016-08-09 09:22:10 +03:00
|
|
|
|
switch self.mode {
|
|
|
|
|
case .Insert, .Replace:
|
|
|
|
|
self.agent.vimInput("<Esc><C-r>i")
|
|
|
|
|
case .Normal, .Visual:
|
|
|
|
|
self.agent.vimInput("<C-r>")
|
|
|
|
|
default:
|
|
|
|
|
return
|
|
|
|
|
}
|
2016-08-04 21:01:03 +03:00
|
|
|
|
}
|
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
@IBAction func cut(_ sender: AnyObject!) {
|
2016-08-09 09:22:10 +03:00
|
|
|
|
switch self.mode {
|
2016-08-09 19:50:35 +03:00
|
|
|
|
case .Visual, .Normal:
|
2016-08-09 09:22:10 +03:00
|
|
|
|
self.agent.vimInput("\"+d")
|
|
|
|
|
default:
|
2016-08-04 21:01:03 +03:00
|
|
|
|
return
|
|
|
|
|
}
|
2016-08-09 09:22:10 +03:00
|
|
|
|
}
|
2016-08-04 21:01:03 +03:00
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
@IBAction func copy(_ sender: AnyObject!) {
|
2016-08-09 09:22:10 +03:00
|
|
|
|
switch self.mode {
|
2016-08-09 19:50:35 +03:00
|
|
|
|
case .Visual, .Normal:
|
2016-08-09 09:22:10 +03:00
|
|
|
|
self.agent.vimInput("\"+y")
|
|
|
|
|
default:
|
|
|
|
|
return
|
|
|
|
|
}
|
2016-08-04 21:01:03 +03:00
|
|
|
|
}
|
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
@IBAction func paste(_ sender: AnyObject!) {
|
|
|
|
|
guard let content = self.pasteboard.string(forType: NSPasteboardTypeString) else {
|
2016-08-04 21:01:03 +03:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch self.mode {
|
2016-08-09 09:22:10 +03:00
|
|
|
|
case .Cmdline, .Insert, .Replace:
|
2016-08-04 21:01:03 +03:00
|
|
|
|
self.agent.vimInput(self.vimPlainString(content))
|
2016-08-09 09:22:10 +03:00
|
|
|
|
case .Normal, .Visual:
|
|
|
|
|
self.agent.vimInput("\"+p")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
@IBAction func delete(_ sender: AnyObject!) {
|
2016-08-09 09:22:10 +03:00
|
|
|
|
switch self.mode {
|
|
|
|
|
case .Normal, .Visual:
|
|
|
|
|
self.agent.vimInput("x")
|
|
|
|
|
default:
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-10-07 19:23:54 +03:00
|
|
|
|
@IBAction public override func selectAll(_ sender: Any?) {
|
2016-08-09 09:22:10 +03:00
|
|
|
|
switch self.mode {
|
|
|
|
|
case .Insert, .Visual:
|
|
|
|
|
self.agent.vimInput("<Esc>ggVG")
|
2016-08-04 21:01:03 +03:00
|
|
|
|
default:
|
2016-08-09 09:22:10 +03:00
|
|
|
|
self.agent.vimInput("ggVG")
|
2016-08-04 21:01:03 +03:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-07-16 01:07:50 +03:00
|
|
|
|
// MARK: - Key Events
|
|
|
|
|
extension NeoVimView: NSTextInputClient {
|
|
|
|
|
|
2016-10-07 19:23:54 +03:00
|
|
|
|
override public func keyDown(with event: NSEvent) {
|
2016-07-16 01:07:50 +03:00
|
|
|
|
self.keyDownDone = false
|
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
let context = NSTextInputContext.current()!
|
2016-07-16 01:07:50 +03:00
|
|
|
|
let cocoaHandledEvent = context.handleEvent(event)
|
|
|
|
|
if self.keyDownDone && cocoaHandledEvent {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NSLog("\(#function): \(event)")
|
|
|
|
|
|
|
|
|
|
let modifierFlags = event.modifierFlags
|
2016-09-25 18:50:33 +03:00
|
|
|
|
let capslock = modifierFlags.contains(.capsLock)
|
|
|
|
|
let shift = modifierFlags.contains(.shift)
|
2016-07-16 01:07:50 +03:00
|
|
|
|
let chars = event.characters!
|
2016-09-25 18:50:33 +03:00
|
|
|
|
let charsIgnoringModifiers = shift || capslock ? event.charactersIgnoringModifiers!.lowercased()
|
2016-07-16 01:07:50 +03:00
|
|
|
|
: event.charactersIgnoringModifiers!
|
|
|
|
|
|
|
|
|
|
if KeyUtils.isSpecial(key: charsIgnoringModifiers) {
|
|
|
|
|
if let vimModifiers = self.vimModifierFlags(modifierFlags) {
|
|
|
|
|
self.agent.vimInput(self.wrapNamedKeys(vimModifiers + KeyUtils.namedKeyFrom(key: charsIgnoringModifiers)))
|
|
|
|
|
} else {
|
|
|
|
|
self.agent.vimInput(self.wrapNamedKeys(KeyUtils.namedKeyFrom(key: charsIgnoringModifiers)))
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
if let vimModifiers = self.vimModifierFlags(modifierFlags) {
|
|
|
|
|
self.agent.vimInput(self.wrapNamedKeys(vimModifiers + charsIgnoringModifiers))
|
|
|
|
|
} else {
|
|
|
|
|
self.agent.vimInput(self.vimPlainString(chars))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.keyDownDone = true
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
public func insertText(_ aString: Any, replacementRange: NSRange) {
|
2016-07-16 01:07:50 +03:00
|
|
|
|
// NSLog("\(#function): \(replacementRange): '\(aString)'")
|
|
|
|
|
|
|
|
|
|
switch aString {
|
|
|
|
|
case let string as String:
|
|
|
|
|
self.agent.vimInput(self.vimPlainString(string))
|
|
|
|
|
case let attributedString as NSAttributedString:
|
|
|
|
|
self.agent.vimInput(self.vimPlainString(attributedString.string))
|
|
|
|
|
default:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// unmarkText()
|
|
|
|
|
self.lastMarkedText = self.markedText
|
|
|
|
|
self.markedText = nil
|
|
|
|
|
self.markedPosition = Position.null
|
|
|
|
|
self.keyDownDone = true
|
|
|
|
|
}
|
|
|
|
|
|
2016-10-07 19:23:54 +03:00
|
|
|
|
public override func doCommand(by aSelector: Selector) {
|
2016-07-16 01:07:50 +03:00
|
|
|
|
// NSLog("\(#function): \(aSelector)");
|
|
|
|
|
|
|
|
|
|
// FIXME: handle when ㅎ -> delete
|
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
if self.responds(to: aSelector) {
|
2016-07-16 01:07:50 +03:00
|
|
|
|
Swift.print("\(#function): calling \(aSelector)")
|
2016-09-25 18:50:33 +03:00
|
|
|
|
self.perform(aSelector, with: self)
|
2016-07-16 01:07:50 +03:00
|
|
|
|
self.keyDownDone = true
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NSLog("\(#function): "\(aSelector) not implemented, forwarding input to vim")
|
|
|
|
|
self.keyDownDone = false
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
public func setMarkedText(_ aString: Any, selectedRange: NSRange, replacementRange: NSRange) {
|
2016-07-16 01:07:50 +03:00
|
|
|
|
if self.markedText == nil {
|
|
|
|
|
self.markedPosition = self.grid.putPosition
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// eg 하 -> hanja popup, cf comment for self.lastMarkedText
|
|
|
|
|
if replacementRange.length > 0 {
|
|
|
|
|
self.agent.deleteCharacters(replacementRange.length)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch aString {
|
|
|
|
|
case let string as String:
|
|
|
|
|
self.markedText = string
|
|
|
|
|
case let attributedString as NSAttributedString:
|
|
|
|
|
self.markedText = attributedString.string
|
|
|
|
|
default:
|
2016-09-25 18:50:33 +03:00
|
|
|
|
self.markedText = String(describing: aString) // should not occur
|
2016-07-16 01:07:50 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NSLog("\(#function): \(self.markedText), \(selectedRange), \(replacementRange)")
|
|
|
|
|
|
|
|
|
|
self.agent.vimInputMarkedText(self.markedText!)
|
|
|
|
|
self.keyDownDone = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public func unmarkText() {
|
|
|
|
|
// NSLog("\(#function): ")
|
|
|
|
|
self.markedText = nil
|
|
|
|
|
self.markedPosition = Position.null
|
|
|
|
|
self.keyDownDone = true
|
|
|
|
|
|
|
|
|
|
// TODO: necessary?
|
2016-09-25 18:50:33 +03:00
|
|
|
|
self.setNeedsDisplay(self.cellRectFor(row: self.grid.putPosition.row, column: self.grid.putPosition.column))
|
2016-07-16 01:07:50 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Return the current selection (or the position of the cursor with empty-length range). For example when you enter
|
|
|
|
|
/// "Cmd-Ctrl-Return" you'll get the Emoji-popup at the rect by firstRectForCharacterRange(actualRange:) where the
|
|
|
|
|
/// first range is the result of this method.
|
|
|
|
|
public func selectedRange() -> NSRange {
|
|
|
|
|
// When the app starts and the Hangul input method is selected, this method gets called very early...
|
|
|
|
|
guard self.grid.hasData else {
|
|
|
|
|
// NSLog("\(#function): not found")
|
|
|
|
|
return NSRange(location: NSNotFound, length: 0)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let result = NSRange(location: self.grid.singleIndexFrom(self.grid.putPosition), length: 0)
|
|
|
|
|
// NSLog("\(#function): \(result)")
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public func markedRange() -> NSRange {
|
|
|
|
|
// FIXME: do we have to handle positions at the column borders?
|
|
|
|
|
if let markedText = self.markedText {
|
|
|
|
|
let result = NSRange(location: self.grid.singleIndexFrom(self.markedPosition),
|
|
|
|
|
length: markedText.characters.count)
|
|
|
|
|
// NSLog("\(#function): \(result)")
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
NSLog("\(#function): returning empty range")
|
|
|
|
|
return NSRange(location: NSNotFound, length: 0)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public func hasMarkedText() -> Bool {
|
|
|
|
|
// NSLog("\(#function)")
|
|
|
|
|
return self.markedText != nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// FIXME: take into account the "return nil"-case
|
|
|
|
|
// FIXME: just fix me, PLEASE...
|
2016-09-25 18:50:33 +03:00
|
|
|
|
public func attributedSubstring(forProposedRange aRange: NSRange, actualRange: NSRangePointer?) -> NSAttributedString? {
|
2016-07-16 01:07:50 +03:00
|
|
|
|
// NSLog("\(#function): \(aRange), \(actualRange[0])")
|
|
|
|
|
if aRange.location == NSNotFound {
|
|
|
|
|
// NSLog("\(#function): range not found: returning nil")
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
guard let lastMarkedText = self.lastMarkedText else {
|
|
|
|
|
// NSLog("\(#function): no last marked text: returning nil")
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// we only support last marked text, thus fill dummy characters when Cocoa asks for more characters than marked...
|
|
|
|
|
let fillCount = aRange.length - lastMarkedText.characters.count
|
|
|
|
|
guard fillCount >= 0 else {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let fillChars = Array(0..<fillCount).reduce("") { (result, _) in return result + " " }
|
|
|
|
|
|
|
|
|
|
// NSLog("\(#function): \(aRange), \(actualRange[0]): \(fillChars + lastMarkedText)")
|
|
|
|
|
return NSAttributedString(string: fillChars + lastMarkedText)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public func validAttributesForMarkedText() -> [String] {
|
|
|
|
|
return []
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
public func firstRect(forCharacterRange aRange: NSRange, actualRange: NSRangePointer?) -> NSRect {
|
2016-07-16 01:07:50 +03:00
|
|
|
|
let position = self.grid.positionFromSingleIndex(aRange.location)
|
|
|
|
|
|
|
|
|
|
// NSLog("\(#function): \(aRange),\(actualRange[0]) -> \(position.row):\(position.column)")
|
|
|
|
|
|
|
|
|
|
let resultInSelf = self.cellRectFor(row: position.row, column: position.column)
|
2016-09-25 18:50:33 +03:00
|
|
|
|
let result = self.window?.convertToScreen(self.convert(resultInSelf, to: nil))
|
2016-07-16 01:07:50 +03:00
|
|
|
|
|
|
|
|
|
return result!
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
public func characterIndex(for aPoint: NSPoint) -> Int {
|
2016-07-16 01:07:50 +03:00
|
|
|
|
// NSLog("\(#function): \(aPoint)")
|
|
|
|
|
return 1
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
fileprivate func vimModifierFlags(_ modifierFlags: NSEventModifierFlags) -> String? {
|
2016-07-16 01:07:50 +03:00
|
|
|
|
var result = ""
|
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
let control = modifierFlags.contains(.control)
|
|
|
|
|
let option = modifierFlags.contains(.option)
|
|
|
|
|
let command = modifierFlags.contains(.command)
|
2016-07-16 01:07:50 +03:00
|
|
|
|
|
|
|
|
|
if control {
|
|
|
|
|
result += "C-"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if option {
|
|
|
|
|
result += "M-"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if command {
|
|
|
|
|
result += "D-"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if result.characters.count > 0 {
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-07-29 19:25:32 +03:00
|
|
|
|
// MARK: - Gesture Events
|
|
|
|
|
extension NeoVimView {
|
|
|
|
|
|
2016-10-07 19:23:54 +03:00
|
|
|
|
override public func magnify(with event: NSEvent) {
|
2016-07-29 19:25:32 +03:00
|
|
|
|
let factor = 1 + event.magnification
|
2016-07-29 23:01:09 +03:00
|
|
|
|
let pinchTargetScale = self.pinchTargetScale * factor
|
|
|
|
|
let resultingFontSize = round(pinchTargetScale * self._font.pointSize)
|
|
|
|
|
if resultingFontSize >= NeoVimView.minFontSize && resultingFontSize <= NeoVimView.maxFontSize {
|
|
|
|
|
self.pinchTargetScale = pinchTargetScale
|
2016-07-29 19:25:32 +03:00
|
|
|
|
}
|
|
|
|
|
|
2016-07-29 23:01:09 +03:00
|
|
|
|
switch event.phase {
|
2016-09-25 18:50:33 +03:00
|
|
|
|
case NSEventPhase.began:
|
|
|
|
|
let pinchImageRep = self.bitmapImageRepForCachingDisplay(in: self.bounds)!
|
|
|
|
|
self.cacheDisplay(in: self.bounds, to: pinchImageRep)
|
2016-07-29 23:01:09 +03:00
|
|
|
|
self.pinchImage = NSImage()
|
|
|
|
|
self.pinchImage.addRepresentation(pinchImageRep)
|
|
|
|
|
|
|
|
|
|
self.isCurrentlyPinching = true
|
|
|
|
|
self.needsDisplay = true
|
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
case NSEventPhase.ended, NSEventPhase.cancelled:
|
2016-07-29 23:01:09 +03:00
|
|
|
|
self.isCurrentlyPinching = false
|
2016-09-25 18:50:33 +03:00
|
|
|
|
self.font = self.fontManager.convert(self._font, toSize: resultingFontSize)
|
2016-07-29 23:01:09 +03:00
|
|
|
|
self.pinchTargetScale = 1
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
self.needsDisplay = true
|
2016-07-29 19:25:32 +03:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-07-16 01:07:50 +03:00
|
|
|
|
// MARK: - Mouse Events
|
|
|
|
|
extension NeoVimView {
|
|
|
|
|
|
2016-10-07 19:23:54 +03:00
|
|
|
|
override public func mouseDown(with event: NSEvent) {
|
2016-09-27 19:02:05 +03:00
|
|
|
|
// self.window?.makeFirstResponder(self)
|
2016-07-16 01:07:50 +03:00
|
|
|
|
self.mouse(event: event, vimName:"LeftMouse")
|
|
|
|
|
}
|
|
|
|
|
|
2016-10-07 19:23:54 +03:00
|
|
|
|
override public func mouseUp(with event: NSEvent) {
|
2016-07-16 01:07:50 +03:00
|
|
|
|
self.mouse(event: event, vimName:"LeftRelease")
|
|
|
|
|
}
|
|
|
|
|
|
2016-10-07 19:23:54 +03:00
|
|
|
|
override public func mouseDragged(with event: NSEvent) {
|
2016-07-16 01:07:50 +03:00
|
|
|
|
self.mouse(event: event, vimName:"LeftDrag")
|
|
|
|
|
}
|
|
|
|
|
|
2016-10-07 19:23:54 +03:00
|
|
|
|
override public func scrollWheel(with event: NSEvent) {
|
2016-07-16 01:07:50 +03:00
|
|
|
|
let (deltaX, deltaY) = (event.scrollingDeltaX, event.scrollingDeltaY)
|
|
|
|
|
if deltaX == 0 && deltaY == 0 {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let cellPosition = self.cellPositionFor(event: event)
|
|
|
|
|
let (vimInputX, vimInputY) = self.vimScrollInputFor(deltaX: deltaX, deltaY: deltaY,
|
|
|
|
|
modifierFlags: event.modifierFlags,
|
|
|
|
|
cellPosition: cellPosition)
|
|
|
|
|
|
|
|
|
|
let (absDeltaX, absDeltaY) = (abs(deltaX), abs(deltaY))
|
|
|
|
|
|
|
|
|
|
// The absolute delta values can get very very big when you use two finger scrolling on the trackpad:
|
|
|
|
|
// Cap them using heuristic values...
|
2016-07-16 19:38:54 +03:00
|
|
|
|
let numX = deltaX != 0 ? max(1, min(Int(absDeltaX / self.scrollLimiterX), self.maxScrollDeltaX)) : 0
|
|
|
|
|
let numY = deltaY != 0 ? max(1, min(Int(absDeltaY / self.scrollLimiterY), self.maxScrollDeltaY)) : 0
|
2016-07-16 01:07:50 +03:00
|
|
|
|
|
|
|
|
|
for i in 0..<max(numX, numY) {
|
|
|
|
|
if i < numX {
|
|
|
|
|
self.throttleScrollX(absDelta: absDeltaX, vimInput: vimInputX)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if i < numY {
|
|
|
|
|
self.throttleScrollY(absDelta: absDeltaY, vimInput: vimInputY)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
fileprivate func cellPositionFor(event: NSEvent) -> Position {
|
|
|
|
|
let location = self.convert(event.locationInWindow, from: nil)
|
2016-08-25 17:08:54 +03:00
|
|
|
|
let row = Int((location.x - self.xOffset) / self.cellSize.width)
|
|
|
|
|
let column = Int((self.bounds.size.height - location.y - self.yOffset) / self.cellSize.height)
|
2016-07-16 01:07:50 +03:00
|
|
|
|
|
2016-08-25 17:08:54 +03:00
|
|
|
|
let cellPosition = Position(row: min(max(0, row), self.grid.size.width - 1),
|
|
|
|
|
column: min(max(0, column), self.grid.size.height - 1))
|
2016-07-16 01:07:50 +03:00
|
|
|
|
return cellPosition
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
fileprivate func mouse(event: NSEvent, vimName: String) {
|
2016-07-16 01:07:50 +03:00
|
|
|
|
let cellPosition = self.cellPositionFor(event: event)
|
|
|
|
|
guard self.shouldFireVimInputFor(event: event, newCellPosition: cellPosition) else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let vimMouseLocation = self.wrapNamedKeys("\(cellPosition.row),\(cellPosition.column)")
|
|
|
|
|
let vimClickCount = self.vimClickCountFrom(event: event)
|
|
|
|
|
|
|
|
|
|
let result: String
|
|
|
|
|
if let vimModifiers = self.vimModifierFlags(event.modifierFlags) {
|
|
|
|
|
result = self.wrapNamedKeys("\(vimModifiers)\(vimClickCount)\(vimName)") + vimMouseLocation
|
|
|
|
|
} else {
|
|
|
|
|
result = self.wrapNamedKeys("\(vimClickCount)\(vimName)") + vimMouseLocation
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NSLog("\(#function): \(result)")
|
|
|
|
|
self.agent.vimInput(result)
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
fileprivate func shouldFireVimInputFor(event:NSEvent, newCellPosition: Position) -> Bool {
|
2016-07-16 01:07:50 +03:00
|
|
|
|
let type = event.type
|
2016-09-25 18:50:33 +03:00
|
|
|
|
guard type == .leftMouseDragged || type == .rightMouseDragged || type == .otherMouseDragged else {
|
2016-07-16 01:07:50 +03:00
|
|
|
|
self.lastClickedCellPosition = newCellPosition
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if self.lastClickedCellPosition == newCellPosition {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.lastClickedCellPosition = newCellPosition
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
fileprivate func vimClickCountFrom(event: NSEvent) -> String {
|
2016-07-16 01:07:50 +03:00
|
|
|
|
let clickCount = event.clickCount
|
|
|
|
|
|
|
|
|
|
guard 2 <= clickCount && clickCount <= 4 else {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch event.type {
|
2016-09-25 18:50:33 +03:00
|
|
|
|
case .leftMouseDown, .leftMouseUp, .rightMouseDown, .rightMouseUp:
|
2016-07-16 01:07:50 +03:00
|
|
|
|
return "\(clickCount)-"
|
|
|
|
|
default:
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
fileprivate func vimScrollEventNamesFor(deltaX: CGFloat, deltaY: CGFloat) -> (String, String) {
|
2016-07-16 01:07:50 +03:00
|
|
|
|
let typeY: String
|
|
|
|
|
if deltaY > 0 {
|
|
|
|
|
typeY = "ScrollWheelUp"
|
|
|
|
|
} else {
|
|
|
|
|
typeY = "ScrollWheelDown"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let typeX: String
|
|
|
|
|
if deltaX < 0 {
|
|
|
|
|
typeX = "ScrollWheelRight"
|
|
|
|
|
} else {
|
|
|
|
|
typeX = "ScrollWheelLeft"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (typeX, typeY)
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
fileprivate func vimScrollInputFor(deltaX: CGFloat, deltaY: CGFloat,
|
2016-07-16 01:07:50 +03:00
|
|
|
|
modifierFlags: NSEventModifierFlags,
|
|
|
|
|
cellPosition: Position) -> (String, String)
|
|
|
|
|
{
|
|
|
|
|
let vimMouseLocation = self.wrapNamedKeys("\(cellPosition.row),\(cellPosition.column)")
|
|
|
|
|
|
|
|
|
|
let (typeX, typeY) = self.vimScrollEventNamesFor(deltaX: deltaX, deltaY: deltaY)
|
|
|
|
|
let resultX: String
|
|
|
|
|
let resultY: String
|
|
|
|
|
if let vimModifiers = self.vimModifierFlags(modifierFlags) {
|
|
|
|
|
resultX = self.wrapNamedKeys("\(vimModifiers)\(typeX)") + vimMouseLocation
|
|
|
|
|
resultY = self.wrapNamedKeys("\(vimModifiers)\(typeY)") + vimMouseLocation
|
|
|
|
|
} else {
|
|
|
|
|
resultX = self.wrapNamedKeys("\(typeX)") + vimMouseLocation
|
|
|
|
|
resultY = self.wrapNamedKeys("\(typeY)") + vimMouseLocation
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (resultX, resultY)
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
fileprivate func throttleScrollX(absDelta absDeltaX: CGFloat, vimInput: String) {
|
2016-07-16 01:07:50 +03:00
|
|
|
|
if absDeltaX == 0 {
|
|
|
|
|
self.scrollGuardCounterX = self.scrollGuardYield - 1
|
|
|
|
|
} else if absDeltaX <= 2 {
|
|
|
|
|
// Poor man's throttle for scroll value = 1 or 2
|
|
|
|
|
if self.scrollGuardCounterX % self.scrollGuardYield == 0 {
|
|
|
|
|
self.agent.vimInput(vimInput)
|
|
|
|
|
self.scrollGuardCounterX = 1
|
|
|
|
|
} else {
|
|
|
|
|
self.scrollGuardCounterX += 1
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
self.agent.vimInput(vimInput)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
fileprivate func throttleScrollY(absDelta absDeltaY: CGFloat, vimInput: String) {
|
2016-07-16 01:07:50 +03:00
|
|
|
|
if absDeltaY == 0 {
|
|
|
|
|
self.scrollGuardCounterY = self.scrollGuardYield - 1
|
|
|
|
|
} else if absDeltaY <= 2 {
|
|
|
|
|
// Poor man's throttle for scroll value = 1 or 2
|
|
|
|
|
if self.scrollGuardCounterY % self.scrollGuardYield == 0 {
|
|
|
|
|
self.agent.vimInput(vimInput)
|
|
|
|
|
self.scrollGuardCounterY = 1
|
|
|
|
|
} else {
|
|
|
|
|
self.scrollGuardCounterY += 1
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
self.agent.vimInput(vimInput)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: - NeoVimUiBridgeProtocol
|
|
|
|
|
extension NeoVimView: NeoVimUiBridgeProtocol {
|
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
public func resize(toWidth width: Int32, height: Int32) {
|
2016-07-16 01:07:50 +03:00
|
|
|
|
DispatchUtils.gui {
|
2016-08-13 00:44:16 +03:00
|
|
|
|
// NSLog("\(#function): \(width):\(height)")
|
2016-07-16 01:07:50 +03:00
|
|
|
|
self.grid.resize(Size(width: Int(width), height: Int(height)))
|
|
|
|
|
self.needsDisplay = true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public func clear() {
|
|
|
|
|
DispatchUtils.gui {
|
|
|
|
|
self.grid.clear()
|
|
|
|
|
self.needsDisplay = true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public func eolClear() {
|
|
|
|
|
DispatchUtils.gui {
|
|
|
|
|
self.grid.eolClear()
|
|
|
|
|
|
|
|
|
|
let origin = self.pointInViewFor(position: self.grid.putPosition)
|
|
|
|
|
let size = CGSize(
|
|
|
|
|
width: CGFloat(self.grid.region.right - self.grid.putPosition.column + 1) * self.cellSize.width,
|
|
|
|
|
height: self.cellSize.height
|
|
|
|
|
)
|
|
|
|
|
let rect = CGRect(origin: origin, size: size)
|
2016-09-25 18:50:33 +03:00
|
|
|
|
self.setNeedsDisplay(rect)
|
2016-07-16 01:07:50 +03:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
public func gotoPosition(_ position: Position, screenCursor: Position) {
|
2016-07-16 01:07:50 +03:00
|
|
|
|
DispatchUtils.gui {
|
|
|
|
|
// NSLog("\(#function): \(position), \(screenCursor)")
|
|
|
|
|
|
2016-09-15 21:42:43 +03:00
|
|
|
|
let curScreenCursor = self.grid.screenCursor
|
|
|
|
|
|
2016-08-01 22:33:52 +03:00
|
|
|
|
// Because neovim fills blank space with "Space" and when we enter "Space" we don't get the puts, thus we have to
|
|
|
|
|
// redraw the put position.
|
|
|
|
|
if self.usesLigatures {
|
|
|
|
|
self.setNeedsDisplay(region: self.grid.regionOfWord(at: self.grid.putPosition))
|
2016-09-15 21:42:43 +03:00
|
|
|
|
self.setNeedsDisplay(region: self.grid.regionOfWord(at: curScreenCursor))
|
2016-09-11 19:12:43 +03:00
|
|
|
|
self.setNeedsDisplay(region: self.grid.regionOfWord(at: position))
|
2016-08-01 22:33:52 +03:00
|
|
|
|
self.setNeedsDisplay(region: self.grid.regionOfWord(at: screenCursor))
|
|
|
|
|
} else {
|
|
|
|
|
self.setNeedsDisplay(cellPosition: self.grid.putPosition)
|
2016-09-11 19:12:43 +03:00
|
|
|
|
// Redraw where the cursor has been till now, ie remove the current cursor.
|
2016-09-15 21:42:43 +03:00
|
|
|
|
self.setNeedsDisplay(cellPosition: curScreenCursor)
|
|
|
|
|
if self.grid.isPreviousCellEmpty(curScreenCursor) {
|
|
|
|
|
self.setNeedsDisplay(cellPosition: self.grid.previousCellPosition(curScreenCursor))
|
|
|
|
|
}
|
|
|
|
|
if self.grid.isNextCellEmpty(curScreenCursor) {
|
|
|
|
|
self.setNeedsDisplay(cellPosition: self.grid.nextCellPosition(curScreenCursor))
|
|
|
|
|
}
|
2016-09-11 19:12:43 +03:00
|
|
|
|
self.setNeedsDisplay(cellPosition: position)
|
|
|
|
|
self.setNeedsDisplay(cellPosition: screenCursor)
|
2016-08-01 22:33:52 +03:00
|
|
|
|
}
|
2016-07-16 01:07:50 +03:00
|
|
|
|
|
|
|
|
|
self.grid.goto(position)
|
|
|
|
|
self.grid.moveCursor(screenCursor)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public func updateMenu() {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public func busyStart() {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public func busyStop() {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public func mouseOn() {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public func mouseOff() {
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
public func modeChange(_ mode: Mode) {
|
2016-08-11 22:19:03 +03:00
|
|
|
|
// NSLog("mode changed to: %02x", mode.rawValue)
|
2016-08-03 22:30:41 +03:00
|
|
|
|
self.mode = mode
|
2016-07-16 01:07:50 +03:00
|
|
|
|
}
|
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
public func setScrollRegionToTop(_ top: Int32, bottom: Int32, left: Int32, right: Int32) {
|
2016-07-16 01:07:50 +03:00
|
|
|
|
DispatchUtils.gui {
|
|
|
|
|
let region = Region(top: Int(top), bottom: Int(bottom), left: Int(left), right: Int(right))
|
|
|
|
|
self.grid.setScrollRegion(region)
|
|
|
|
|
self.setNeedsDisplay(region: region)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
public func scroll(_ count: Int32) {
|
2016-07-16 01:07:50 +03:00
|
|
|
|
DispatchUtils.gui {
|
|
|
|
|
self.grid.scroll(Int(count))
|
|
|
|
|
self.setNeedsDisplay(region: self.grid.region)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
public func highlightSet(_ attrs: CellAttributes) {
|
2016-07-16 01:07:50 +03:00
|
|
|
|
DispatchUtils.gui {
|
|
|
|
|
self.grid.attrs = attrs
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
public func put(_ string: String, screenCursor: Position) {
|
2016-07-16 01:07:50 +03:00
|
|
|
|
DispatchUtils.gui {
|
|
|
|
|
let curPos = self.grid.putPosition
|
|
|
|
|
// NSLog("\(#function): \(curPos) -> \(string)")
|
|
|
|
|
self.grid.put(string)
|
2016-07-31 00:04:20 +03:00
|
|
|
|
|
|
|
|
|
if self.usesLigatures {
|
|
|
|
|
if string == " " {
|
|
|
|
|
self.setNeedsDisplay(cellPosition: curPos)
|
|
|
|
|
} else {
|
2016-07-31 21:53:51 +03:00
|
|
|
|
self.setNeedsDisplay(region: self.grid.regionOfWord(at: curPos))
|
2016-07-31 00:04:20 +03:00
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
self.setNeedsDisplay(cellPosition: curPos)
|
|
|
|
|
}
|
|
|
|
|
|
2016-08-15 21:30:44 +03:00
|
|
|
|
self.updateCursorWhenPutting(currentPosition: curPos, screenCursor: screenCursor)
|
2016-07-16 01:07:50 +03:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
public func putMarkedText(_ markedText: String, screenCursor: Position) {
|
2016-07-16 01:07:50 +03:00
|
|
|
|
DispatchUtils.gui {
|
2016-09-15 21:42:43 +03:00
|
|
|
|
NSLog("\(#function): '\(markedText)' -> \(screenCursor)")
|
|
|
|
|
|
2016-07-16 01:07:50 +03:00
|
|
|
|
let curPos = self.grid.putPosition
|
|
|
|
|
self.grid.putMarkedText(markedText)
|
|
|
|
|
|
|
|
|
|
self.setNeedsDisplay(position: curPos)
|
|
|
|
|
// When the cursor is in the command line, then we need this...
|
|
|
|
|
self.setNeedsDisplay(cellPosition: self.grid.nextCellPosition(curPos))
|
|
|
|
|
if markedText.characters.count == 0 {
|
|
|
|
|
self.setNeedsDisplay(position: self.grid.previousCellPosition(curPos))
|
|
|
|
|
}
|
2016-08-15 21:30:44 +03:00
|
|
|
|
|
|
|
|
|
self.updateCursorWhenPutting(currentPosition: curPos, screenCursor: screenCursor)
|
2016-07-16 01:07:50 +03:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
public func unmarkRow(_ row: Int32, column: Int32) {
|
2016-07-16 01:07:50 +03:00
|
|
|
|
DispatchUtils.gui {
|
|
|
|
|
let position = Position(row: Int(row), column: Int(column))
|
|
|
|
|
|
2016-09-15 22:10:13 +03:00
|
|
|
|
// NSLog("\(#function): \(position)")
|
2016-07-16 01:07:50 +03:00
|
|
|
|
|
|
|
|
|
self.grid.unmarkCell(position)
|
|
|
|
|
self.setNeedsDisplay(position: position)
|
|
|
|
|
|
|
|
|
|
self.setNeedsDisplay(screenCursor: self.grid.screenCursor)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public func bell() {
|
|
|
|
|
DispatchUtils.gui {
|
|
|
|
|
NSBeep()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public func visualBell() {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public func flush() {
|
|
|
|
|
// NSLog("\(#function)")
|
|
|
|
|
}
|
|
|
|
|
|
2016-10-04 20:26:15 +03:00
|
|
|
|
public func updateForeground(_ fg: Int32) {
|
2016-07-16 01:07:50 +03:00
|
|
|
|
DispatchUtils.gui {
|
|
|
|
|
self.grid.foreground = UInt32(bitPattern: fg)
|
2016-09-15 21:42:43 +03:00
|
|
|
|
// NSLog("\(ColorUtils.colorIgnoringAlpha(UInt32(fg)))")
|
2016-07-16 01:07:50 +03:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-10-04 20:26:15 +03:00
|
|
|
|
public func updateBackground(_ bg: Int32) {
|
2016-07-16 01:07:50 +03:00
|
|
|
|
DispatchUtils.gui {
|
|
|
|
|
self.grid.background = UInt32(bitPattern: bg)
|
2016-09-25 18:50:33 +03:00
|
|
|
|
self.layer?.backgroundColor = ColorUtils.colorIgnoringAlpha(self.grid.background).cgColor
|
2016-09-15 21:42:43 +03:00
|
|
|
|
// NSLog("\(ColorUtils.colorIgnoringAlpha(UInt32(bg)))")
|
2016-07-16 01:07:50 +03:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-10-04 20:26:15 +03:00
|
|
|
|
public func updateSpecial(_ sp: Int32) {
|
2016-07-16 01:07:50 +03:00
|
|
|
|
DispatchUtils.gui {
|
|
|
|
|
self.grid.special = UInt32(bitPattern: sp)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public func suspend() {
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
public func setTitle(_ title: String) {
|
2016-07-16 01:07:50 +03:00
|
|
|
|
DispatchUtils.gui {
|
2016-09-27 01:17:53 +03:00
|
|
|
|
self.delegate?.set(title: title)
|
2016-07-16 01:07:50 +03:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
public func setIcon(_ icon: String) {
|
2016-07-16 01:07:50 +03:00
|
|
|
|
}
|
2016-08-19 00:11:33 +03:00
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
public func setDirtyStatus(_ dirty: Bool) {
|
2016-08-19 00:11:33 +03:00
|
|
|
|
DispatchUtils.gui {
|
2016-09-27 01:17:53 +03:00
|
|
|
|
self.delegate?.set(dirtyStatus: dirty)
|
2016-08-19 00:11:33 +03:00
|
|
|
|
}
|
|
|
|
|
}
|
2016-09-07 21:12:18 +03:00
|
|
|
|
|
|
|
|
|
public func cwdChanged() {
|
|
|
|
|
DispatchUtils.gui {
|
|
|
|
|
self.delegate?.cwdChanged()
|
|
|
|
|
}
|
|
|
|
|
}
|
2016-07-16 01:07:50 +03:00
|
|
|
|
|
|
|
|
|
public func stop() {
|
2016-07-17 15:41:53 +03:00
|
|
|
|
DispatchUtils.gui {
|
|
|
|
|
self.delegate?.neoVimStopped()
|
2016-08-12 19:00:05 +03:00
|
|
|
|
self.agent.quit()
|
2016-07-17 15:41:53 +03:00
|
|
|
|
}
|
2016-07-16 01:07:50 +03:00
|
|
|
|
}
|
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
fileprivate func updateCursorWhenPutting(currentPosition curPos: Position, screenCursor: Position) {
|
2016-08-15 21:30:44 +03:00
|
|
|
|
if self.mode == .Cmdline {
|
|
|
|
|
// When the cursor is in the command line, then we need this...
|
2016-09-15 21:42:43 +03:00
|
|
|
|
self.setNeedsDisplay(cellPosition: self.grid.previousCellPosition(curPos))
|
2016-08-15 21:30:44 +03:00
|
|
|
|
self.setNeedsDisplay(cellPosition: self.grid.nextCellPosition(curPos))
|
|
|
|
|
self.setNeedsDisplay(screenCursor: self.grid.screenCursor)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.setNeedsDisplay(screenCursor: screenCursor)
|
|
|
|
|
self.setNeedsDisplay(cellPosition: self.grid.screenCursor)
|
|
|
|
|
self.grid.moveCursor(screenCursor)
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
fileprivate func setNeedsDisplay(region: Region) {
|
|
|
|
|
self.setNeedsDisplay(self.regionRectFor(region: region))
|
2016-07-16 01:07:50 +03:00
|
|
|
|
}
|
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
fileprivate func setNeedsDisplay(cellPosition position: Position) {
|
2016-07-16 01:07:50 +03:00
|
|
|
|
self.setNeedsDisplay(position: position)
|
|
|
|
|
|
|
|
|
|
if self.grid.isCellEmpty(position) {
|
|
|
|
|
self.setNeedsDisplay(position: self.grid.previousCellPosition(position))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if self.grid.isNextCellEmpty(position) {
|
|
|
|
|
self.setNeedsDisplay(position: self.grid.nextCellPosition(position))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
fileprivate func setNeedsDisplay(position: Position) {
|
2016-07-16 01:07:50 +03:00
|
|
|
|
self.setNeedsDisplay(row: position.row, column: position.column)
|
|
|
|
|
}
|
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
fileprivate func setNeedsDisplay(row: Int, column: Int) {
|
2016-07-16 01:07:50 +03:00
|
|
|
|
// Swift.print("\(#function): \(row):\(column)")
|
2016-09-25 18:50:33 +03:00
|
|
|
|
self.setNeedsDisplay(self.cellRectFor(row: row, column: column))
|
2016-07-16 01:07:50 +03:00
|
|
|
|
}
|
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
|
fileprivate func setNeedsDisplay(screenCursor position: Position) {
|
2016-07-16 01:07:50 +03:00
|
|
|
|
self.setNeedsDisplay(position: position)
|
|
|
|
|
if self.grid.isNextCellEmpty(position) {
|
|
|
|
|
self.setNeedsDisplay(position: self.grid.nextCellPosition(position))
|
|
|
|
|
}
|
|
|
|
|
}
|
2016-07-30 10:49:39 +03:00
|
|
|
|
}
|