diff --git a/.gitignore b/.gitignore old mode 100755 new mode 100644 index 91fc4cb3..aaf0cae5 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1 @@ -.dub -docs.json -__dummy.html -*.o -*.obj - -dub.selections.json micro - -.DS_STORE diff --git a/buffer.go b/buffer.go new file mode 100644 index 00000000..7e59cf33 --- /dev/null +++ b/buffer.go @@ -0,0 +1,61 @@ +package main + +import ( + "io/ioutil" + "strings" +) + +type Buffer struct { + r *Rope + + // Path to the file on disk + path string + // Name of the buffer on the status line + name string + + // This is the text stored everytime the buffer is saved to check if the buffer is modified + savedText string + + text string + lines []string +} + +func newBuffer(txt, path string) *Buffer { + b := new(Buffer) + b.r = newRope(txt) + b.path = path + b.name = path + b.savedText = txt + + b.update() + + return b +} + +func (b *Buffer) update() { + b.text = b.r.toString() + b.lines = strings.Split(b.text, "\n") +} + +func (b *Buffer) save() error { + return b.saveAs(b.path) +} + +func (b *Buffer) saveAs(filename string) error { + err := ioutil.WriteFile(filename, []byte(b.text), 0644) + return err +} + +func (b *Buffer) insert(idx int, value string) { + b.r.insert(idx, value) + b.update() +} + +func (b *Buffer) remove(start, end int) { + b.r.remove(start, end) + b.update() +} + +func (b *Buffer) length() int { + return b.r.len +} diff --git a/cursor.go b/cursor.go new file mode 100644 index 00000000..d5aa48d8 --- /dev/null +++ b/cursor.go @@ -0,0 +1,147 @@ +package main + +import ( + "strings" +) + +// Cursor stores the location of the cursor in the view +type Cursor struct { + v *View + + x int + y int + loc int + + selectionStart int + selectionEnd int +} + +func (c *Cursor) resetSelection() { + c.selectionStart = 0 + c.selectionEnd = 0 +} + +func (c *Cursor) hasSelection() bool { + return (c.selectionEnd - c.selectionStart) > 0 +} + +func (c *Cursor) deleteSelected() { + // TODO: Implement this +} + +func (c *Cursor) up() { + if c.y > 0 { + c.loc -= count(c.v.buf.lines[c.y][:c.x]) + // Count the newline + c.loc-- + c.y-- + + if c.x > count(c.v.buf.lines[c.y]) { + c.x = count(c.v.buf.lines[c.y]) + } + + c.loc -= count(c.v.buf.lines[c.y][c.x:]) + } +} +func (c *Cursor) down() { + if c.y < len(c.v.buf.lines)-1 { + c.loc += count(c.v.buf.lines[c.y][c.x:]) + // Count the newline + c.loc++ + c.y++ + + if c.x > count(c.v.buf.lines[c.y]) { + c.x = count(c.v.buf.lines[c.y]) + } + + c.loc += count(c.v.buf.lines[c.y][:c.x]) + } +} +func (c *Cursor) left() { + if c.x > 0 { + c.loc-- + c.x-- + } else { + c.up() + c.end() + } +} +func (c *Cursor) right() { + if c.x < count(c.v.buf.lines[c.y]) { + c.loc++ + c.x++ + } else { + c.down() + c.start() + } +} + +func (c *Cursor) end() { + c.loc += count(c.v.buf.lines[c.y][c.x:]) + c.x = count(c.v.buf.lines[c.y]) +} + +func (c *Cursor) start() { + c.loc -= count(c.v.buf.lines[c.y][:c.x]) + c.x = 0 +} + +func (c *Cursor) getCharPos(lineNum, visualPos int) int { + visualLine := strings.Replace(c.v.buf.lines[lineNum], "\t", "\t"+emptyString(tabSize-1), -1) + if visualPos > count(visualLine) { + visualPos = count(visualLine) + } + numTabs := numOccurences(visualLine[:visualPos], '\t') + return visualPos - (tabSize-1)*numTabs +} + +func (c *Cursor) distance(x, y int) int { + // Same line + if y == c.y { + return x - c.x + } + + var distance int + if y > c.y { + distance += count(c.v.buf.lines[c.y][c.x:]) + // Newline + distance++ + i := 1 + for y != c.y+i { + distance += count(c.v.buf.lines[c.y+i]) + // Newline + distance++ + i++ + } + if x < count(c.v.buf.lines[y]) { + distance += count(c.v.buf.lines[y][:x]) + } else { + distance += count(c.v.buf.lines[y]) + } + return distance + } + + distance -= count(c.v.buf.lines[c.y][:c.x]) + // Newline + distance-- + i := 1 + for y != c.y-i { + distance -= count(c.v.buf.lines[c.y-i]) + // Newline + distance-- + i++ + } + if x > 0 { + distance -= count(c.v.buf.lines[y][x:]) + } + return distance +} + +func (c *Cursor) display() { + if c.y-c.v.topline < 0 || c.y-c.v.topline > c.v.linesN-1 { + c.v.s.HideCursor() + } else { + voffset := numOccurences(c.v.buf.lines[c.y][:c.x], '\t') * (tabSize - 1) + c.v.s.ShowCursor(c.x+voffset, c.y-c.v.topline) + } +} diff --git a/dub.sdl b/dub.sdl deleted file mode 100644 index dad7a22c..00000000 --- a/dub.sdl +++ /dev/null @@ -1,5 +0,0 @@ -name "micro" -description "A minimal D application." -copyright "Copyright © 2016, zachary" -authors "zachary" -dependency "termbox" version="0.0.5" diff --git a/micro.go b/micro.go new file mode 100644 index 00000000..0b6e21fb --- /dev/null +++ b/micro.go @@ -0,0 +1,76 @@ +package main + +import ( + "fmt" + "github.com/mattn/go-isatty" + "io/ioutil" + "os" + + "github.com/gdamore/tcell" +) + +const ( + tabSize = 4 +) + +func main() { + var input []byte + var filename string + + if len(os.Args) > 1 { + filename = os.Args[1] + var err error + input, err = ioutil.ReadFile(filename) + + if err != nil { + fmt.Println(err) + os.Exit(1) + } + } else if !isatty.IsTerminal(os.Stdin.Fd()) { + bytes, err := ioutil.ReadAll(os.Stdin) + if err != nil { + fmt.Println("Error reading stdin") + os.Exit(1) + } + input = bytes + } + + s, e := tcell.NewScreen() + if e != nil { + fmt.Fprintf(os.Stderr, "%v\n", e) + os.Exit(1) + } + if e := s.Init(); e != nil { + fmt.Fprintf(os.Stderr, "%v\n", e) + os.Exit(1) + } + s.EnableMouse() + + v := newViewFromBuffer(newBuffer(string(input), filename), s) + + // Initially everything needs to be drawn + redraw := 2 + for { + if redraw == 2 { + s.Clear() + v.display() + v.cursor.display() + s.Show() + } else if redraw == 1 { + v.cursor.display() + s.Show() + } + + event := s.PollEvent() + + switch e := event.(type) { + case *tcell.EventKey: + if e.Key() == tcell.KeyCtrlQ { + s.Fini() + os.Exit(0) + } + } + + redraw = v.handleEvent(event) + } +} diff --git a/rope.go b/rope.go new file mode 100644 index 00000000..29980289 --- /dev/null +++ b/rope.go @@ -0,0 +1,112 @@ +package main + +import ( + // "fmt" + "math" + "unicode/utf8" +) + +const ( + ropeSplitLength = 1000 + ropeJoinLength = 500 + ropeRebalanceRatio = 1.2 +) + +func min(a, b int) int { + if a > b { + return b + } + return a +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} + +type Rope struct { + left *Rope + right *Rope + value string + valueNil bool + + len int +} + +func newRope(str string) *Rope { + r := new(Rope) + r.value = str + r.valueNil = false + r.len = utf8.RuneCountInString(r.value) + + r.adjust() + + return r +} + +func (r *Rope) adjust() { + if !r.valueNil { + if r.len > ropeSplitLength { + divide := int(math.Floor(float64(r.len) / 2)) + r.left = newRope(r.value[:divide]) + r.right = newRope(r.value[divide:]) + r.valueNil = true + } + } else { + if r.len < ropeJoinLength { + r.value = r.left.toString() + r.right.toString() + r.valueNil = false + r.left = nil + r.right = nil + } + } +} + +func (r *Rope) toString() string { + if !r.valueNil { + return r.value + } + return r.left.toString() + r.right.toString() +} + +func (r *Rope) remove(start, end int) { + if !r.valueNil { + r.value = string(append([]rune(r.value)[:start], []rune(r.value)[end:]...)) + r.valueNil = false + r.len = utf8.RuneCountInString(r.value) + } else { + leftStart := min(start, r.left.len) + leftEnd := min(end, r.left.len) + rightStart := max(0, min(start-r.left.len, r.right.len)) + rightEnd := max(0, min(end-r.left.len, r.right.len)) + if leftStart < r.left.len { + r.left.remove(leftStart, leftEnd) + } + if rightEnd > 0 { + r.right.remove(rightStart, rightEnd) + } + r.len = r.left.len + r.right.len + } + + r.adjust() +} + +func (r *Rope) insert(pos int, value string) { + if !r.valueNil { + first := append([]rune(r.value)[:pos], []rune(value)...) + r.value = string(append(first, []rune(r.value)[pos:]...)) + r.valueNil = false + r.len = utf8.RuneCountInString(r.value) + } else { + if pos < r.left.len { + r.left.insert(pos, value) + r.len = r.left.len + r.right.len + } else { + r.right.insert(pos-r.left.len, value) + } + } + + r.adjust() +} diff --git a/src/buffer.d b/src/buffer.d deleted file mode 100644 index e191992b..00000000 --- a/src/buffer.d +++ /dev/null @@ -1,74 +0,0 @@ -import rope; - -import std.string: split; -import std.stdio; - -class Buffer { - private Rope text; - - string path; - string savedText; - - private string value; - - string[] lines; - - this(string txt, string path) { - text = new Rope(txt); - savedText = txt; - this.path = path; - update(); - } - - void save() { - saveAs(path); - } - - void saveAs(string filename) { - string bufTxt = text.toString(); - File f = File(filename, "w"); - f.write(bufTxt); - f.close(); - savedText = bufTxt; - } - - override - string toString() { - return value; - } - - void update() { - value = text.toString(); - if (value == "") { - lines = [""]; - } else { - lines = value.split("\n"); - } - } - - @property ulong length() { - return text.length; - } - - void remove(ulong start, ulong end) { - text.remove(start, end); - update(); - } - void insert(ulong position, string value) { - text.insert(position, value); - update(); - } - string substring(ulong start, ulong end = -1) { - if (end == -1) { - update(); - return text.substring(start, text.length); - } else { - update(); - return text.substring(start, end); - } - } - char charAt(ulong pos) { - update(); - return text.charAt(pos); - } -} diff --git a/src/clipboard.d b/src/clipboard.d deleted file mode 100644 index 3af3998b..00000000 --- a/src/clipboard.d +++ /dev/null @@ -1,63 +0,0 @@ -import std.process: execute, spawnProcess, pipe; - -class Clipboard { - static bool supported; - version(OSX) { - static bool init() { - return supported = true; - } - - static void write(string txt) { - auto p = pipe(); - p.writeEnd.write(txt); - spawnProcess("pbcopy", p.readEnd()); - } - - static string read() { - return execute("pbpaste").output; - } - } - - version(linux) { - import std.exception: collectException; - static string[] copyCmd; - static string[] pasteCmd; - - static bool init() { - if (collectException(execute(["xsel", "-h"]))) { - if (collectException(execute(["xclip", "-h"]))) { - return supported = false; - } else { - copyCmd = ["xclip", "-in", "-selection", "clipboard"]; - pasteCmd = ["xclip", "-out", "-selection", "clipboard"]; - return supported = true; - } - } else { - copyCmd = ["xsel", "--input", "--clipboard"]; - pasteCmd = ["xsel", "--output", "--clipboard"]; - return supported = true; - } - } - - static void write(string txt) { - auto p = pipe(); - p.writeEnd.write(txt); - spawnProcess(copyCmd, p.readEnd()); - } - - static string read() { - return execute(pasteCmd).output; - } - } - - version(Windows) { - // No windows support yet - } -} - -unittest { - string text = "æêáóìëæêî"; - assert(Clipboard.init()); - Clipboard.write(text); - assert(Clipboard.read() == text); -} diff --git a/src/cursor.d b/src/cursor.d deleted file mode 100644 index 3cf02f2e..00000000 --- a/src/cursor.d +++ /dev/null @@ -1,20 +0,0 @@ -import termbox; - -class Cursor { - int x, y; - int lastX; - - uint selectionStart; - uint selectionEnd; - - this() {} - - this(int x, int y) { - this.x = x; - this.y = y; - } - - void hide() { - x = y = -1; - } -} diff --git a/src/main.d b/src/main.d deleted file mode 100644 index e32a240e..00000000 --- a/src/main.d +++ /dev/null @@ -1,68 +0,0 @@ -import termbox; -import buffer; -import cursor; -import view; -import clipboard; - -import std.stdio; -import std.file: readText, exists, isDir; - -extern(C) int isatty(int); - -void main(string[] args) { - string filename = ""; - string fileTxt = ""; - - if (args.length > 1) { - filename = args[1]; - if (exists(filename)) { - if (isDir(filename)) { - writeln(filename, " is a directory"); - return; - } - fileTxt = readText(filename); - if (fileTxt is null) { - fileTxt = ""; - } - } - } else { - if (!isatty(0)) { - foreach (line; stdin.byLine()) { - fileTxt ~= line ~ "\n"; - } - } - } - Clipboard.init(); - - Buffer buf = new Buffer(fileTxt, filename); - init(); - - Cursor cursor = new Cursor(); - View v = new View(buf, cursor); - - setInputMode(InputMode.mouse); - - Event e; - try { - while (e.key != Key.ctrlQ) { - clear(); - - v.display(); - - flush(); - pollEvent(&e); - - v.update(e); - } - } catch (object.Error e) { - shutdown(); - writeln(e); - return; - } catch (Exception e) { - shutdown(); - writeln(e); - return; - } - - shutdown(); -} diff --git a/src/rope.d b/src/rope.d deleted file mode 100644 index 074d2210..00000000 --- a/src/rope.d +++ /dev/null @@ -1,140 +0,0 @@ -import std.string, std.stdio; -import std.algorithm: min, max; -import std.conv: to; -import std.math: floor; - -// Rope data structure to store the text in the buffer -class Rope { - private Rope left; - private Rope right; - private string value = null; - - ulong length; - - const int splitLength = 1000; - const int joinLength = 500; - const double rebalanceRatio = 1.2; - - this(string str) { - this.value = str; - this.length = str.count; - - adjust(); - } - - void adjust() { - if (value !is null) { - if (length > splitLength) { - auto divide = cast(int) floor(length / 2.0); - left = new Rope(value[0 .. divide]); - right = new Rope(value[divide .. $]); - value = null; - } - } else { - if (length < joinLength) { - value = left.toString() ~ right.toString(); - left = null; - right = null; - } - } - } - - override - string toString() { - if (value !is null) { - return value; - } else { - return left.toString ~ right.toString(); - } - } - - void remove(ulong start, ulong end) { - if (value !is null) { - value = to!string(value.to!dstring[0 .. start] ~ value.to!dstring[end .. $]); - if (value is null) { - value = ""; - } - length = value.count; - } else { - auto leftStart = min(start, left.length); - auto leftEnd = min(end, left.length); - auto rightStart = max(0, min(start - left.length, right.length)); - auto rightEnd = max(0, min(end - left.length, right.length)); - if (leftStart < left.length) { - left.remove(leftStart, leftEnd); - } - if (rightEnd > 0) { - right.remove(rightStart, rightEnd); - } - length = left.length + right.length; - } - - adjust(); - } - - void insert(ulong position, string value) { - if (this.value !is null) { - this.value = to!string(this.value.to!dstring[0 .. position] ~ value.to!dstring ~ this.value.to!dstring[position .. $]); - length = this.value.count; - } else { - if (position < left.length) { - left.insert(position, value); - length = left.length + right.length; - } else { - right.insert(position - left.length, value); - } - } - - adjust(); - } - - void rebuild() { - if (value is null) { - value = left.toString() ~ right.toString(); - left = null; - right = null; - adjust(); - } - } - - void rebalance() { - if (value is null) { - if (left.length / right.length > rebalanceRatio || - right.length / left.length > rebalanceRatio) { - rebuild(); - } else { - left.rebalance(); - right.rebalance(); - } - } - } - - string substring(ulong start, ulong end) { - if (value !is null) { - return value[start .. end]; - } else { - auto leftStart = min(start, left.length); - auto leftEnd = min(end, left.length); - auto rightStart = max(0, min(start - left.length, right.length)); - auto rightEnd = max(0, min(end - left.length, right.length)); - - if (leftStart != leftEnd) { - if (rightStart != rightEnd) { - return left.substring(leftStart, leftEnd) ~ right.substring(rightStart, rightEnd); - } else { - return left.substring(leftStart, leftEnd); - } - } else { - if (rightStart != rightEnd) { - return right.substring(rightStart, rightEnd); - } else { - return ""; - } - } - } - } - - char charAt(ulong pos) { - return to!char(substring(pos, pos + 1)); - } -} diff --git a/src/statusline.d b/src/statusline.d deleted file mode 100644 index f3bb0670..00000000 --- a/src/statusline.d +++ /dev/null @@ -1,29 +0,0 @@ -import termbox; -import view; - -class StatusLine { - View view; - - this(View v) { - this.view = v; - } - - void display() { - int y = view.height; - string file = view.buf.path; - if (file == "") { - file = "untitled"; - } - if (view.buf.toString != view.buf.savedText) { - file ~= " +"; - } - file ~= " (" ~ to!string(view.cursor.y + 1) ~ "," ~ to!string(view.cursor.x + 1) ~ ")"; - foreach (x; 0 .. view.width) { - if (x >= 1 && x < 1 + file.length) { - setCell(x, y, cast(uint) file[x - 1], Color.black, Color.blue); - } else { - setCell(x, y, ' ', Color.black, Color.blue); - } - } - } -} diff --git a/src/util.d b/src/util.d deleted file mode 100644 index d1754d6d..00000000 --- a/src/util.d +++ /dev/null @@ -1,17 +0,0 @@ -string emptyString(int size) { - string str; - foreach (i; 0 .. size) { - str ~= " "; - } - return str; -} - -int numOccurences(string str, char c) { - int n; - foreach (letter; str) { - if (letter == c) { - n++; - } - } - return n; -} diff --git a/src/view.d b/src/view.d deleted file mode 100644 index 7465776d..00000000 --- a/src/view.d +++ /dev/null @@ -1,253 +0,0 @@ -import termbox; -import buffer; -import clipboard; -import cursor; -import statusline; -import util; - -import std.regex: regex, replaceAll; -import std.conv: to; -import std.utf: count; - -enum tabSize = 4; - -class View { - uint topline; - uint xOffset; - - uint width; - uint height; - - Buffer buf; - Cursor cursor; - StatusLine sl; - - this(Buffer buf, Cursor cursor, uint topline = 0, uint width = termbox.width(), uint height = termbox.height() - 2) { - this.topline = topline; - this.width = width; - this.height = height; - - this.buf = buf; - this.cursor = cursor; - this.sl = new StatusLine(this); - } - - uint toCharNumber(int x, int y) { - int loc; - foreach (i; 0 .. y) { - loc += buf.lines[i].count + 1; - } - loc += x; - return loc; - } - - int[] fromCharNumber(uint value) { - int x, y; - int loc; - foreach (lineNum, l; buf.lines) { - if (loc + l.count+1 > value) { - y = cast(int) lineNum; - x = value - loc; - return [x, y]; - } else { - loc += l.count+1; - } - } - return [-1, -1]; - } - - uint cursorLoc() { - return toCharNumber(cursor.x, cursor.y); - } - - void setCursorLoc(uint charNum) { - int[] xy = fromCharNumber(charNum); - cursor.x = xy[0]; - cursor.y = xy[1]; - } - - int getCharPosition(int lineNum, int visualPosition) { - string visualLine = buf.lines[lineNum].replaceAll(regex("\t"), "\t" ~ emptyString(tabSize-1)); - if (visualPosition > visualLine.length) { - visualPosition = cast(int) visualLine.length; - } - int numTabs = numOccurences(visualLine[0 .. visualPosition], '\t'); - return visualPosition - (tabSize-1) * numTabs; - } - - void cursorUp() { - if (cursor.y > 0) { - cursor.y--; - cursor.x = cursor.lastX; - if (cursor.x > buf.lines[cursor.y].length) { - cursor.x = cast(int) buf.lines[cursor.y].length; - } - } - } - - void cursorDown() { - if (cursor.y < buf.lines.length - 1) { - cursor.y++; - cursor.x = cursor.lastX; - if (cursor.x > buf.lines[cursor.y].length) { - cursor.x = cast(int) buf.lines[cursor.y].length; - } - } - } - - void cursorRight() { - if (cursor.x < buf.lines[cursor.y].length) { - if (buf.lines[cursor.y][cursor.x] == '\t') { - cursor.x++; - } else { - cursor.x++; - } - cursor.lastX = cursor.x; - } - } - - void cursorLeft() { - if (cursor.x > 0) { - if (buf.lines[cursor.y][cursor.x-1] == '\t') { - cursor.x--; - } else { - cursor.x--; - } - cursor.lastX = cursor.x; - } - } - - void update(Event e) { - uint cloc = cursorLoc(); - if (e.key == Key.mouseWheelUp) { - if (topline > 0) - topline--; - } else if (e.key == Key.mouseWheelDown) { - if (buf.lines.length > height && topline < buf.lines.length - height) - topline++; - } else { - if (e.key == Key.arrowUp) { - cursorUp(); - } else if (e.key == Key.arrowDown) { - cursorDown(); - } else if (e.key == Key.arrowRight) { - cursorRight(); - } else if (e.key == Key.arrowLeft) { - cursorLeft(); - } else if (e.key == Key.mouseLeft) { - cursor.y = e.y + topline; - if (cursor.y - topline > height-1) { - cursor.y = height + topline-1; - } - if (cursor.y > buf.lines.length) { - cursor.y = cast(int) buf.lines.length-1; - } - cursor.x = getCharPosition(cursor.y, e.x - xOffset); - cursor.lastX = cursor.x; - - cursor.selectionStart = 0; - cursor.selectionEnd = 0; - } else if (e.key == Key.mouseRelease) { - auto y = e.y + topline; - if (y - topline > height-1) { - y = height + topline-1; - } - if (y > buf.lines.length) { - y = cast(int) buf.lines.length-1; - } - auto x = getCharPosition(y, e.x - xOffset); - - cursor.selectionStart = toCharNumber(cursor.x, cursor.y); - cursor.selectionEnd = toCharNumber(x, y); - } else if (e.key == Key.ctrlS) { - if (buf.path != "") { - buf.save(); - } - } else if (e.key == Key.ctrlV) { - if (Clipboard.supported) { - buf.insert(cloc, Clipboard.read()); - } - } else { - if (e.ch != 0) { - buf.insert(cloc, to!string(to!dchar(e.ch))); - cursorRight(); - } else if (e.key == Key.space) { - buf.insert(cursorLoc(), " "); - cursorRight(); - } else if (e.key == Key.enter) { - buf.insert(cloc, "\n"); - cursorDown(); - cursor.x = 0; - cursor.lastX = cursor.x; - } else if (e.key == Key.tab) { - buf.insert(cloc, "\t"); - cursorRight(); - } else if (e.key == Key.backspace2) { - if (cloc > 0) { - buf.remove(cloc-1, cloc); - setCursorLoc(cloc - 1); - cursor.lastX = cursor.x; - } - } - } - - if (cursor.y < topline) { - topline = cursor.y; - } - - if (cursor.y > topline + height-1) { - topline = cursor.y - height+1; - } - - } - } - - void display() { - uint x, y; - - string[] lines; - if (topline + height > buf.lines.length) { - lines = buf.lines[topline .. $]; - } else { - lines = buf.lines[topline .. topline + height]; - } - - ulong maxLength = to!string(buf.lines.length).length; - xOffset = cast(int) maxLength + 1; - - int chNum; - foreach (i, line; lines) { - // Write the line number - string lineNum = to!string(i + topline + 1); - foreach (_; 0 .. maxLength - lineNum.length) { - setCell(cast(int) x++, cast(int) y, ' ', Color.basic, Color.basic); - } - foreach (dchar ch; lineNum) { - setCell(cast(int) x++, cast(int) y, ch, Color.basic, Color.basic); - } - setCell(cast(int) x++, cast(int) y, ' ', Color.basic, Color.basic); - - // Write the line - foreach (dchar ch; line.replaceAll(regex("\t"), emptyString(tabSize))) { - auto color = Color.basic; - if (chNum > cursor.selectionStart && chNum < cursor.selectionEnd) { - color = cast(Color) (Color.basic | Attribute.reverse); - } - setCell(x++, y, ch, color, color); - chNum++; - } - y++; - x = 0; - chNum++; - } - - if (cursor.y - topline < 0 || cursor.y - topline > height-1) { - hideCursor(); - } else { - auto voffset = buf.lines[cursor.y][0 .. cursor.x].numOccurences('\t') * (tabSize-1); - setCursor(cursor.x + xOffset + voffset, cursor.y - topline); - } - - sl.display(); - } -} diff --git a/util.go b/util.go new file mode 100644 index 00000000..e78561a3 --- /dev/null +++ b/util.go @@ -0,0 +1,27 @@ +package main + +import ( + "unicode/utf8" +) + +func count(s string) int { + return utf8.RuneCountInString(s) +} + +func numOccurences(s string, c byte) int { + var n int + for i := 0; i < len(s); i++ { + if s[i] == c { + n++ + } + } + return n +} + +func emptyString(n int) string { + var str string + for i := 0; i < n; i++ { + str += " " + } + return str +} diff --git a/view.go b/view.go new file mode 100644 index 00000000..67d6cf7e --- /dev/null +++ b/view.go @@ -0,0 +1,164 @@ +package main + +import ( + "github.com/gdamore/tcell" + "strings" +) + +type View struct { + cursor Cursor + topline int + linesN int + colsN int + + buf *Buffer + mouseReleased bool + + s tcell.Screen +} + +func newViewFromBuffer(buf *Buffer, s tcell.Screen) *View { + v := new(View) + + v.buf = buf + v.s = s + w, h := s.Size() + + v.topline = 0 + v.linesN = h + v.colsN = w + v.cursor = Cursor{ + x: 0, + y: 0, + loc: 0, + v: v, + } + + return v +} + +// Returns an int describing how the screen needs to be redrawn +// 0: Screen does not need to be redrawn +// 1: Only the cursor needs to be redrawn +// 2: Everything needs to be redrawn +func (v *View) handleEvent(event tcell.Event) int { + var ret int + switch e := event.(type) { + case *tcell.EventKey: + switch e.Key() { + case tcell.KeyUp: + v.cursor.up() + ret = 1 + case tcell.KeyDown: + v.cursor.down() + ret = 1 + case tcell.KeyLeft: + v.cursor.left() + ret = 1 + case tcell.KeyRight: + v.cursor.right() + ret = 1 + case tcell.KeyEnter: + v.buf.insert(v.cursor.loc, "\n") + v.cursor.right() + ret = 2 + case tcell.KeyBackspace2: + if v.cursor.loc > 0 { + v.cursor.left() + v.buf.remove(v.cursor.loc, v.cursor.loc+1) + ret = 2 + } + case tcell.KeyTab: + v.buf.insert(v.cursor.loc, "\t") + v.cursor.right() + ret = 2 + case tcell.KeyRune: + v.buf.insert(v.cursor.loc, string(e.Rune())) + v.cursor.right() + ret = 2 + } + case *tcell.EventMouse: + x, y := e.Position() + y += v.topline + // Position always seems to be off by one + x-- + y-- + + button := e.Buttons() + + switch button { + case tcell.Button1: + if y-v.topline > v.linesN-1 { + y = v.linesN + v.topline - 1 + } + if y > len(v.buf.lines) { + y = len(v.buf.lines) - 1 + } + if x > count(v.buf.lines[y]) { + x = count(v.buf.lines[y]) + } + + x = v.cursor.getCharPos(y, x) + d := v.cursor.distance(x, y) + v.cursor.loc += d + v.cursor.x = x + v.cursor.y = y + + if v.mouseReleased { + v.cursor.selectionStart = v.cursor.loc + } + v.cursor.selectionEnd = v.cursor.loc + v.mouseReleased = false + ret = 2 + case tcell.ButtonNone: + v.mouseReleased = true + case tcell.WheelUp: + if v.topline > 0 { + v.topline-- + return 2 + } else { + return 0 + } + case tcell.WheelDown: + if v.topline < len(v.buf.lines)-v.linesN { + v.topline++ + return 2 + } else { + return 0 + } + } + } + + cy := v.cursor.y + if cy < v.topline { + v.topline = cy + ret = 2 + } + if cy > v.topline+v.linesN-1 { + v.topline = cy - v.linesN + 1 + ret = 2 + } + + return ret +} + +func (v *View) display() { + + var charNum int + for lineN := 0; lineN < v.linesN; lineN++ { + if lineN+v.topline >= len(v.buf.lines) { + break + } + line := strings.Replace(v.buf.lines[lineN+v.topline], "\t", emptyString(tabSize), -1) + for colN, ch := range line { + st := tcell.StyleDefault + if v.cursor.hasSelection() && charNum >= v.cursor.selectionStart && charNum <= v.cursor.selectionEnd { + st = st.Reverse(true) + } + + v.s.SetContent(colN, lineN, ch, nil, st) + charNum++ + } + charNum++ + } +}