mirror of
https://github.com/zyedidia/micro
synced 2024-10-01 13:44:36 +00:00
Switch to go in order to use tcell
This commit is contained in:
parent
e9e25d0c85
commit
1781766e21
9
.gitignore
vendored
Executable file → Normal file
9
.gitignore
vendored
Executable file → Normal file
|
@ -1,10 +1 @@
|
|||
.dub
|
||||
docs.json
|
||||
__dummy.html
|
||||
*.o
|
||||
*.obj
|
||||
|
||||
dub.selections.json
|
||||
micro
|
||||
|
||||
.DS_STORE
|
||||
|
|
61
buffer.go
Normal file
61
buffer.go
Normal file
|
@ -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
|
||||
}
|
147
cursor.go
Normal file
147
cursor.go
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
5
dub.sdl
5
dub.sdl
|
@ -1,5 +0,0 @@
|
|||
name "micro"
|
||||
description "A minimal D application."
|
||||
copyright "Copyright © 2016, zachary"
|
||||
authors "zachary"
|
||||
dependency "termbox" version="0.0.5"
|
76
micro.go
Normal file
76
micro.go
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
112
rope.go
Normal file
112
rope.go
Normal file
|
@ -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()
|
||||
}
|
74
src/buffer.d
74
src/buffer.d
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
20
src/cursor.d
20
src/cursor.d
|
@ -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;
|
||||
}
|
||||
}
|
68
src/main.d
68
src/main.d
|
@ -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();
|
||||
}
|
140
src/rope.d
140
src/rope.d
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
17
src/util.d
17
src/util.d
|
@ -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;
|
||||
}
|
253
src/view.d
253
src/view.d
|
@ -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();
|
||||
}
|
||||
}
|
27
util.go
Normal file
27
util.go
Normal file
|
@ -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
|
||||
}
|
164
view.go
Normal file
164
view.go
Normal file
|
@ -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++
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue