2016-06-08 19:17:12 +03:00
|
|
|
/**
|
|
|
|
* Tae Won Ha - http://taewon.de - @hataewon
|
|
|
|
* See LICENSE
|
|
|
|
*/
|
|
|
|
|
2016-08-03 22:43:05 +03:00
|
|
|
import Cocoa
|
2016-06-08 19:17:12 +03:00
|
|
|
|
|
|
|
struct Cell: CustomStringConvertible {
|
2017-04-29 09:11:34 +03:00
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
fileprivate let attributes: CellAttributes
|
2016-06-27 20:04:27 +03:00
|
|
|
|
2017-04-29 09:11:34 +03:00
|
|
|
var string: String
|
2016-06-27 20:04:27 +03:00
|
|
|
var marked: Bool
|
|
|
|
|
|
|
|
var attrs: CellAttributes {
|
2016-07-03 22:59:03 +03:00
|
|
|
return self.marked ? self.attributes.inverted : self.attributes
|
2016-06-27 20:04:27 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
init(string: String, attrs: CellAttributes, marked: Bool = false) {
|
|
|
|
self.string = string
|
|
|
|
self.attributes = attrs
|
|
|
|
self.marked = marked
|
|
|
|
}
|
2016-06-08 19:17:12 +03:00
|
|
|
|
|
|
|
var description: String {
|
|
|
|
return self.string.characters.count > 0 ? self.string : "*"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-09-27 19:02:05 +03:00
|
|
|
extension Position: CustomStringConvertible, Equatable {
|
2016-06-08 19:17:12 +03:00
|
|
|
|
|
|
|
static let zero = Position(row: 0, column: 0)
|
2016-06-29 23:28:02 +03:00
|
|
|
static let null = Position(row: -1, column: -1)
|
2017-04-29 09:11:34 +03:00
|
|
|
|
|
|
|
public static func ==(left: Position, right: Position) -> Bool {
|
|
|
|
if left.row != right.row { return false }
|
|
|
|
if left.column != right.column { return false }
|
|
|
|
|
|
|
|
return true
|
|
|
|
}
|
2016-06-08 19:17:12 +03:00
|
|
|
|
2016-07-01 22:24:42 +03:00
|
|
|
public var description: String {
|
2016-06-08 19:17:12 +03:00
|
|
|
return "Position<\(self.row):\(self.column)>"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-09-27 19:02:05 +03:00
|
|
|
struct Size: CustomStringConvertible, Equatable {
|
2016-06-08 19:17:12 +03:00
|
|
|
|
|
|
|
static let zero = Size(width: 0, height: 0)
|
2017-04-29 09:11:34 +03:00
|
|
|
|
|
|
|
static func ==(left: Size, right: Size) -> Bool {
|
|
|
|
return left.width == right.width && left.height == right.height
|
|
|
|
}
|
2016-06-08 19:17:12 +03:00
|
|
|
|
2017-04-29 09:11:34 +03:00
|
|
|
var width: Int
|
|
|
|
var height: Int
|
2016-06-08 19:17:12 +03:00
|
|
|
|
|
|
|
var description: String {
|
|
|
|
return "Size<\(self.width):\(self.height)>"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
struct Region: CustomStringConvertible {
|
|
|
|
|
|
|
|
static let zero = Region(top: 0, bottom: 0, left: 0, right: 0)
|
|
|
|
|
2017-04-29 09:11:34 +03:00
|
|
|
var top: Int
|
|
|
|
var bottom: Int
|
|
|
|
var left: Int
|
|
|
|
var right: Int
|
2016-06-08 19:17:12 +03:00
|
|
|
|
|
|
|
var description: String {
|
2016-06-15 23:11:35 +03:00
|
|
|
return "Region<\(self.top)...\(self.bottom):\(self.left)...\(self.right)>"
|
|
|
|
}
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
var rowRange: CountableClosedRange<Int> {
|
2016-06-15 23:11:35 +03:00
|
|
|
return self.top...self.bottom
|
|
|
|
}
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
var columnRange: CountableClosedRange<Int> {
|
2016-06-15 23:11:35 +03:00
|
|
|
return self.left...self.right
|
2016-06-08 19:17:12 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-06-16 19:36:26 +03:00
|
|
|
/// Almost a verbatim copy of ugrid.c of NeoVim
|
2016-06-08 19:17:12 +03:00
|
|
|
class Grid: CustomStringConvertible {
|
2016-07-31 00:04:20 +03:00
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
fileprivate(set) var region = Region.zero
|
|
|
|
fileprivate(set) var size = Size.zero
|
|
|
|
fileprivate(set) var putPosition = Position.zero
|
|
|
|
fileprivate(set) var screenCursor = Position.zero
|
2016-06-08 19:17:12 +03:00
|
|
|
|
2016-06-18 12:43:37 +03:00
|
|
|
var foreground = qDefaultForeground
|
|
|
|
var background = qDefaultBackground
|
|
|
|
var special = qDefaultSpecial
|
2016-06-08 19:17:12 +03:00
|
|
|
|
2016-06-18 12:43:37 +03:00
|
|
|
var attrs: CellAttributes = CellAttributes(
|
2016-09-25 18:50:33 +03:00
|
|
|
fontTrait: .none,
|
2016-06-18 12:43:37 +03:00
|
|
|
foreground: qDefaultForeground, background: qDefaultBackground, special: qDefaultSpecial
|
2016-06-19 22:11:03 +03:00
|
|
|
)
|
2016-06-08 19:17:12 +03:00
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
fileprivate(set) var cells: [[Cell]] = []
|
2016-06-15 23:11:35 +03:00
|
|
|
|
|
|
|
var hasData: Bool {
|
|
|
|
return !self.cells.isEmpty
|
|
|
|
}
|
2016-06-08 19:17:12 +03:00
|
|
|
|
|
|
|
var description: String {
|
|
|
|
return self.cells.reduce("<<< Grid\n") { $1.reduce($0) { $0 + $1.description } + "\n" } + ">>>"
|
|
|
|
}
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
func resize(_ size: Size) {
|
2016-06-08 19:17:12 +03:00
|
|
|
self.region = Region(top: 0, bottom: size.height - 1, left: 0, right: size.width - 1)
|
|
|
|
self.size = size
|
2016-07-01 22:24:42 +03:00
|
|
|
self.putPosition = Position.zero
|
2016-06-08 19:17:12 +03:00
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
let emptyCellAttrs = CellAttributes(fontTrait: .none,
|
2016-06-19 11:07:03 +03:00
|
|
|
foreground: self.foreground, background: self.background, special: self.special)
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
let emptyRow = Array(repeating: Cell(string: " ", attrs: emptyCellAttrs), count: size.width)
|
|
|
|
self.cells = Array(repeating: emptyRow, count: size.height)
|
2016-06-08 19:17:12 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
func clear() {
|
|
|
|
self.clearRegion(self.region)
|
|
|
|
}
|
|
|
|
|
|
|
|
func eolClear() {
|
|
|
|
self.clearRegion(
|
2016-07-03 23:08:14 +03:00
|
|
|
Region(top: self.putPosition.row, bottom: self.putPosition.row,
|
|
|
|
left: self.putPosition.column, right: self.region.right)
|
2016-06-08 19:17:12 +03:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
func setScrollRegion(_ region: Region) {
|
2016-06-08 19:17:12 +03:00
|
|
|
self.region = region
|
|
|
|
}
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
func scroll(_ count: Int) {
|
2016-06-15 21:37:02 +03:00
|
|
|
var start, stop, step : Int
|
|
|
|
if count > 0 {
|
|
|
|
start = self.region.top;
|
|
|
|
stop = self.region.bottom - count + 1;
|
|
|
|
step = 1;
|
|
|
|
} else {
|
|
|
|
start = self.region.bottom;
|
|
|
|
stop = self.region.top - count - 1;
|
|
|
|
step = -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
// copy cell data
|
|
|
|
let rangeWithinRow = self.region.left...self.region.right
|
2016-09-25 18:50:33 +03:00
|
|
|
for i in stride(from: start, to: stop, by: step) {
|
|
|
|
self.cells[i].replaceSubrange(rangeWithinRow, with: self.cells[i + count][rangeWithinRow])
|
2016-06-15 21:37:02 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// clear cells in the emptied region,
|
|
|
|
var clearTop, clearBottom: Int
|
|
|
|
if count > 0 {
|
|
|
|
clearTop = stop
|
|
|
|
clearBottom = stop + count - 1
|
|
|
|
} else {
|
|
|
|
clearBottom = stop
|
|
|
|
clearTop = stop + count + 1
|
|
|
|
}
|
|
|
|
self.clearRegion(Region(top: clearTop, bottom: clearBottom, left: self.region.left, right: self.region.right))
|
2016-06-08 19:17:12 +03:00
|
|
|
}
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
func goto(_ position: Position) {
|
2016-07-01 22:24:42 +03:00
|
|
|
self.putPosition = position
|
|
|
|
}
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
func moveCursor(_ position: Position) {
|
2016-07-01 22:24:42 +03:00
|
|
|
self.screenCursor = position
|
2016-06-08 19:17:12 +03:00
|
|
|
}
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
func put(_ string: String) {
|
2016-06-29 22:55:16 +03:00
|
|
|
// FIXME: handle the following situation:
|
|
|
|
// |abcde | <- type ㅎ
|
|
|
|
// =>
|
|
|
|
// |abcde>| <- ">" at the end of the line is wrong -> the XPC could tell the main app whether the string occupies
|
|
|
|
// |ㅎ | two cells using vim_strwidth()
|
2016-07-01 22:24:42 +03:00
|
|
|
self.cells[self.putPosition.row][self.putPosition.column] = Cell(string: string, attrs: self.attrs)
|
|
|
|
|
|
|
|
// Increment the column of the put position because neovim calls sets the position only once when drawing
|
|
|
|
// consecutive cells in the same line
|
|
|
|
self.putPosition.column += 1
|
2016-06-08 19:17:12 +03:00
|
|
|
}
|
2016-06-24 22:08:34 +03:00
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
func putMarkedText(_ string: String) {
|
2016-06-24 22:08:34 +03:00
|
|
|
// NOTE: Maybe there's a better way to indicate marked text than inverting...
|
2016-07-01 22:24:42 +03:00
|
|
|
self.cells[self.putPosition.row][self.putPosition.column] = Cell(string: string, attrs: self.attrs, marked: true)
|
|
|
|
self.putPosition.column += 1
|
2016-06-27 20:04:27 +03:00
|
|
|
}
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
func unmarkCell(_ position: Position) {
|
2016-06-29 23:28:02 +03:00
|
|
|
// NSLog("\(#function): \(position)")
|
2016-06-27 20:04:27 +03:00
|
|
|
self.cells[position.row][position.column].marked = false
|
|
|
|
}
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
func singleIndexFrom(_ position: Position) -> Int {
|
2016-06-29 23:28:02 +03:00
|
|
|
return position.row * self.size.width + position.column
|
|
|
|
}
|
|
|
|
|
2016-07-31 21:53:51 +03:00
|
|
|
func regionOfWord(at position: Position) -> Region {
|
2016-07-31 00:04:20 +03:00
|
|
|
let row = position.row
|
|
|
|
let column = position.column
|
2016-08-01 22:33:52 +03:00
|
|
|
|
|
|
|
guard row < self.size.height else {
|
|
|
|
return Region.zero
|
|
|
|
}
|
|
|
|
|
|
|
|
guard column < self.size.width else {
|
|
|
|
return Region.zero
|
|
|
|
}
|
|
|
|
|
2016-07-31 00:04:20 +03:00
|
|
|
var left = 0
|
2016-09-25 18:50:33 +03:00
|
|
|
for idx in (0..<column).reversed() {
|
2016-07-31 22:16:08 +03:00
|
|
|
let cell = self.cells[row][idx]
|
2016-09-15 21:42:43 +03:00
|
|
|
if cell.string == " " {
|
2016-08-01 22:55:36 +03:00
|
|
|
left = idx + 1
|
2016-07-31 00:04:20 +03:00
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
2016-07-31 22:16:08 +03:00
|
|
|
|
2016-08-01 22:33:52 +03:00
|
|
|
var right = self.size.width - 1
|
|
|
|
for idx in (column + 1)..<self.size.width {
|
2016-07-31 00:04:20 +03:00
|
|
|
let cell = self.cells[row][idx]
|
2016-09-15 21:42:43 +03:00
|
|
|
if cell.string == " " {
|
2016-08-01 22:55:36 +03:00
|
|
|
right = idx - 1
|
2016-07-31 00:04:20 +03:00
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return Region(top: row, bottom: row, left: left, right: right)
|
|
|
|
}
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
func positionFromSingleIndex(_ idx: Int) -> Position {
|
2016-06-29 23:28:02 +03:00
|
|
|
let row = Int(floor(Double(idx) / Double(self.size.width)))
|
|
|
|
let column = idx - row * self.size.width
|
|
|
|
|
|
|
|
return Position(row: row, column: column)
|
|
|
|
}
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
func isCellEmpty(_ position: Position) -> Bool {
|
2016-07-05 20:00:27 +03:00
|
|
|
guard self.isSane(position: position) else {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2016-06-29 23:28:02 +03:00
|
|
|
if self.cells[position.row][position.column].string.characters.count == 0 {
|
2016-06-29 21:06:31 +03:00
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
func isPreviousCellEmpty(_ position: Position) -> Bool {
|
2016-07-01 22:24:42 +03:00
|
|
|
return self.isCellEmpty(self.previousCellPosition(position))
|
2016-06-29 23:28:02 +03:00
|
|
|
}
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
func isNextCellEmpty(_ position: Position) -> Bool {
|
2016-07-01 22:24:42 +03:00
|
|
|
return self.isCellEmpty(self.nextCellPosition(position))
|
2016-06-29 23:28:02 +03:00
|
|
|
}
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
func previousCellPosition(_ position: Position) -> Position {
|
2016-07-01 22:24:42 +03:00
|
|
|
return Position(row: position.row, column: max(position.column - 1, 0))
|
|
|
|
}
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
func nextCellPosition(_ position: Position) -> Position {
|
2016-06-29 21:06:31 +03:00
|
|
|
return Position(row: position.row, column: min(position.column + 1, self.size.width - 1))
|
|
|
|
}
|
2016-06-29 23:28:02 +03:00
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
func cellForSingleIndex(_ idx: Int) -> Cell {
|
2016-06-29 23:28:02 +03:00
|
|
|
let position = self.positionFromSingleIndex(idx)
|
|
|
|
return self.cells[position.row][position.column]
|
|
|
|
}
|
2016-06-29 21:06:31 +03:00
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
fileprivate func clearRegion(_ region: Region) {
|
2016-06-19 14:39:20 +03:00
|
|
|
// FIXME: sometimes clearRegion gets called without first resizing the Grid. Should we handle this?
|
2016-06-15 23:11:35 +03:00
|
|
|
guard self.hasData else {
|
2016-06-08 19:17:12 +03:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
let clearedAttrs = CellAttributes(fontTrait: .none,
|
2016-06-18 12:43:37 +03:00
|
|
|
foreground: self.foreground, background: self.background, special: self.special)
|
2016-06-08 19:17:12 +03:00
|
|
|
|
|
|
|
let clearedCell = Cell(string: " ", attrs: clearedAttrs)
|
2016-09-25 18:50:33 +03:00
|
|
|
let clearedRow = Array(repeating: clearedCell, count: region.right - region.left + 1)
|
2016-06-08 19:17:12 +03:00
|
|
|
for i in region.top...region.bottom {
|
2016-09-25 18:50:33 +03:00
|
|
|
self.cells[i].replaceSubrange(region.left...region.right, with: clearedRow)
|
2016-06-08 19:17:12 +03:00
|
|
|
}
|
|
|
|
}
|
2016-07-05 20:00:27 +03:00
|
|
|
|
2016-09-25 18:50:33 +03:00
|
|
|
fileprivate func isSane(position: Position) -> Bool {
|
2016-07-05 20:00:27 +03:00
|
|
|
guard position.row < self.size.height && position.column < self.size.width else {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
return true
|
|
|
|
}
|
2016-06-19 11:07:03 +03:00
|
|
|
}
|