now is go gettable and updated make file

This commit is contained in:
aerth
2016-04-18 10:59:41 +00:00
parent 1c30153b0c
commit 337f162360
19 changed files with 5 additions and 4 deletions

117
cmd/micro/buffer.go Normal file
View File

@@ -0,0 +1,117 @@
package main
import (
"github.com/vinzmay/go-rope"
"io/ioutil"
"strings"
)
// Buffer stores the text for files that are loaded into the text editor
// It uses a rope to efficiently store the string and contains some
// simple functions for saving and wrapper functions for modifying the rope
type Buffer struct {
// Stores the text of the buffer
r *rope.Rope
// Path to the file on disk
path string
// Name of the buffer on the status line
name string
// This is the text stored every time the buffer is saved to check if the buffer is modified
savedText string
// Provide efficient and easy access to text and lines so the rope String does not
// need to be constantly recalculated
// These variables are updated in the update() function
text string
lines []string
// Syntax highlighting rules
rules []SyntaxRule
// The buffer's filetype
filetype string
}
// NewBuffer creates a new buffer from `txt` with path and name `path`
func NewBuffer(txt, path string) *Buffer {
b := new(Buffer)
if txt == "" {
b.r = new(rope.Rope)
} else {
b.r = rope.New(txt)
}
b.path = path
b.name = path
b.savedText = txt
b.Update()
b.UpdateRules()
return b
}
// UpdateRules updates the syntax rules and filetype for this buffer
// This is called when the colorscheme changes
func (b *Buffer) UpdateRules() {
b.rules, b.filetype = GetRules(b)
}
// Update fetches the string from the rope and updates the `text` and `lines` in the buffer
func (b *Buffer) Update() {
if b.r.Len() == 0 {
b.text = ""
} else {
b.text = b.r.String()
}
b.lines = strings.Split(b.text, "\n")
}
// Save saves the buffer to its default path
func (b *Buffer) Save() error {
return b.SaveAs(b.path)
}
// SaveAs saves the buffer to a specified path (filename), creating the file if it does not exist
func (b *Buffer) SaveAs(filename string) error {
b.UpdateRules()
err := ioutil.WriteFile(filename, []byte(b.text), 0644)
if err == nil {
b.savedText = b.text
}
return err
}
// IsDirty returns whether or not the buffer has been modified compared to the one on disk
func (b *Buffer) IsDirty() bool {
return b.savedText != b.text
}
// Insert a string into the rope
func (b *Buffer) Insert(idx int, value string) {
b.r = b.r.Insert(idx, value)
b.Update()
}
// Remove a slice of the rope from start to end (exclusive)
// Returns the string that was removed
func (b *Buffer) Remove(start, end int) string {
if start < 0 {
start = 0
}
if end > b.Len() {
end = b.Len()
}
removed := b.text[start:end]
// The rope implenentation I am using wants indicies starting at 1 instead of 0
start++
end++
b.r = b.r.Delete(start, end-start)
b.Update()
return removed
}
// Len gives the length of the buffer
func (b *Buffer) Len() int {
return b.r.Len()
}

198
cmd/micro/colorscheme.go Normal file
View File

@@ -0,0 +1,198 @@
package main
import (
"fmt"
"github.com/gdamore/tcell"
"github.com/mitchellh/go-homedir"
"io/ioutil"
"regexp"
"strconv"
"strings"
)
// Colorscheme is a map from string to style -- it represents a colorscheme
type Colorscheme map[string]tcell.Style
// The current colorscheme
var colorscheme Colorscheme
// InitColorscheme picks and initializes the colorscheme when micro starts
func InitColorscheme() {
LoadDefaultColorscheme()
}
// LoadDefaultColorscheme loads the default colorscheme from ~/.micro/colorschemes
func LoadDefaultColorscheme() {
dir, err := homedir.Dir()
if err != nil {
TermMessage("Error finding your home directory\nCan't load runtime files")
return
}
LoadColorscheme(settings.Colorscheme, dir+"/.micro/colorschemes")
}
// LoadColorscheme loads the given colorscheme from a directory
func LoadColorscheme(colorschemeName, dir string) {
files, _ := ioutil.ReadDir(dir)
for _, f := range files {
if f.Name() == colorschemeName+".micro" {
text, err := ioutil.ReadFile(dir + "/" + f.Name())
if err != nil {
fmt.Println("Error loading colorscheme:", err)
continue
}
colorscheme = ParseColorscheme(string(text))
}
}
}
// ParseColorscheme parses the text definition for a colorscheme and returns the corresponding object
// Colorschemes are made up of color-link statements linking a color group to a list of colors
// For example, color-link keyword (blue,red) makes all keywords have a blue foreground and
// red background
func ParseColorscheme(text string) Colorscheme {
parser := regexp.MustCompile(`color-link\s+(\S*)\s+"(.*)"`)
lines := strings.Split(text, "\n")
c := make(Colorscheme)
for _, line := range lines {
if strings.TrimSpace(line) == "" ||
strings.TrimSpace(line)[0] == '#' {
// Ignore this line
continue
}
matches := parser.FindSubmatch([]byte(line))
if len(matches) == 3 {
link := string(matches[1])
colors := string(matches[2])
c[link] = StringToStyle(colors)
} else {
fmt.Println("Color-link statement is not valid:", line)
}
}
return c
}
// StringToStyle returns a style from a string
// The strings must be in the format "extra foregroundcolor,backgroundcolor"
// The 'extra' can be bold, reverse, or underline
func StringToStyle(str string) tcell.Style {
var fg string
bg := "default"
split := strings.Split(str, ",")
if len(split) > 1 {
fg, bg = split[0], split[1]
} else {
fg = split[0]
}
fg = strings.TrimSpace(fg)
bg = strings.TrimSpace(bg)
style := tcell.StyleDefault.Foreground(StringToColor(fg)).Background(StringToColor(bg))
if strings.Contains(str, "bold") {
style = style.Bold(true)
}
if strings.Contains(str, "reverse") {
style = style.Reverse(true)
}
if strings.Contains(str, "underline") {
style = style.Underline(true)
}
return style
}
// StringToColor returns a tcell color from a string representation of a color
// We accept either bright... or light... to mean the brighter version of a color
func StringToColor(str string) tcell.Color {
switch str {
case "black":
return tcell.ColorBlack
case "red":
return tcell.ColorMaroon
case "green":
return tcell.ColorGreen
case "yellow":
return tcell.ColorOlive
case "blue":
return tcell.ColorNavy
case "magenta":
return tcell.ColorPurple
case "cyan":
return tcell.ColorTeal
case "white":
return tcell.ColorSilver
case "brightblack", "lightblack":
return tcell.ColorGray
case "brightred", "lightred":
return tcell.ColorRed
case "brightgreen", "lightgreen":
return tcell.ColorLime
case "brightyellow", "lightyellow":
return tcell.ColorYellow
case "brightblue", "lightblue":
return tcell.ColorBlue
case "brightmagenta", "lightmagenta":
return tcell.ColorFuchsia
case "brightcyan", "lightcyan":
return tcell.ColorAqua
case "brightwhite", "lightwhite":
return tcell.ColorWhite
case "default":
return tcell.ColorDefault
default:
// Check if this is a 256 color
if num, err := strconv.Atoi(str); err == nil {
return GetColor256(num)
}
// Probably a truecolor hex value
return tcell.GetColor(str)
}
}
// GetColor256 returns the tcell color for a number between 0 and 255
func GetColor256(color int) tcell.Color {
colors := []tcell.Color{tcell.ColorBlack, tcell.ColorMaroon, tcell.ColorGreen,
tcell.ColorOlive, tcell.ColorNavy, tcell.ColorPurple,
tcell.ColorTeal, tcell.ColorSilver, tcell.ColorGray,
tcell.ColorRed, tcell.ColorLime, tcell.ColorYellow,
tcell.ColorBlue, tcell.ColorFuchsia, tcell.ColorAqua,
tcell.ColorWhite, tcell.Color16, tcell.Color17, tcell.Color18, tcell.Color19, tcell.Color20,
tcell.Color21, tcell.Color22, tcell.Color23, tcell.Color24, tcell.Color25, tcell.Color26, tcell.Color27, tcell.Color28,
tcell.Color29, tcell.Color30, tcell.Color31, tcell.Color32, tcell.Color33, tcell.Color34, tcell.Color35, tcell.Color36,
tcell.Color37, tcell.Color38, tcell.Color39, tcell.Color40, tcell.Color41, tcell.Color42, tcell.Color43, tcell.Color44,
tcell.Color45, tcell.Color46, tcell.Color47, tcell.Color48, tcell.Color49, tcell.Color50, tcell.Color51, tcell.Color52,
tcell.Color53, tcell.Color54, tcell.Color55, tcell.Color56, tcell.Color57, tcell.Color58, tcell.Color59, tcell.Color60,
tcell.Color61, tcell.Color62, tcell.Color63, tcell.Color64, tcell.Color65, tcell.Color66, tcell.Color67, tcell.Color68,
tcell.Color69, tcell.Color70, tcell.Color71, tcell.Color72, tcell.Color73, tcell.Color74, tcell.Color75, tcell.Color76,
tcell.Color77, tcell.Color78, tcell.Color79, tcell.Color80, tcell.Color81, tcell.Color82, tcell.Color83, tcell.Color84,
tcell.Color85, tcell.Color86, tcell.Color87, tcell.Color88, tcell.Color89, tcell.Color90, tcell.Color91, tcell.Color92,
tcell.Color93, tcell.Color94, tcell.Color95, tcell.Color96, tcell.Color97, tcell.Color98, tcell.Color99, tcell.Color100,
tcell.Color101, tcell.Color102, tcell.Color103, tcell.Color104, tcell.Color105, tcell.Color106, tcell.Color107, tcell.Color108,
tcell.Color109, tcell.Color110, tcell.Color111, tcell.Color112, tcell.Color113, tcell.Color114, tcell.Color115, tcell.Color116,
tcell.Color117, tcell.Color118, tcell.Color119, tcell.Color120, tcell.Color121, tcell.Color122, tcell.Color123, tcell.Color124,
tcell.Color125, tcell.Color126, tcell.Color127, tcell.Color128, tcell.Color129, tcell.Color130, tcell.Color131, tcell.Color132,
tcell.Color133, tcell.Color134, tcell.Color135, tcell.Color136, tcell.Color137, tcell.Color138, tcell.Color139, tcell.Color140,
tcell.Color141, tcell.Color142, tcell.Color143, tcell.Color144, tcell.Color145, tcell.Color146, tcell.Color147, tcell.Color148,
tcell.Color149, tcell.Color150, tcell.Color151, tcell.Color152, tcell.Color153, tcell.Color154, tcell.Color155, tcell.Color156,
tcell.Color157, tcell.Color158, tcell.Color159, tcell.Color160, tcell.Color161, tcell.Color162, tcell.Color163, tcell.Color164,
tcell.Color165, tcell.Color166, tcell.Color167, tcell.Color168, tcell.Color169, tcell.Color170, tcell.Color171, tcell.Color172,
tcell.Color173, tcell.Color174, tcell.Color175, tcell.Color176, tcell.Color177, tcell.Color178, tcell.Color179, tcell.Color180,
tcell.Color181, tcell.Color182, tcell.Color183, tcell.Color184, tcell.Color185, tcell.Color186, tcell.Color187, tcell.Color188,
tcell.Color189, tcell.Color190, tcell.Color191, tcell.Color192, tcell.Color193, tcell.Color194, tcell.Color195, tcell.Color196,
tcell.Color197, tcell.Color198, tcell.Color199, tcell.Color200, tcell.Color201, tcell.Color202, tcell.Color203, tcell.Color204,
tcell.Color205, tcell.Color206, tcell.Color207, tcell.Color208, tcell.Color209, tcell.Color210, tcell.Color211, tcell.Color212,
tcell.Color213, tcell.Color214, tcell.Color215, tcell.Color216, tcell.Color217, tcell.Color218, tcell.Color219, tcell.Color220,
tcell.Color221, tcell.Color222, tcell.Color223, tcell.Color224, tcell.Color225, tcell.Color226, tcell.Color227, tcell.Color228,
tcell.Color229, tcell.Color230, tcell.Color231, tcell.Color232, tcell.Color233, tcell.Color234, tcell.Color235, tcell.Color236,
tcell.Color237, tcell.Color238, tcell.Color239, tcell.Color240, tcell.Color241, tcell.Color242, tcell.Color243, tcell.Color244,
tcell.Color245, tcell.Color246, tcell.Color247, tcell.Color248, tcell.Color249, tcell.Color250, tcell.Color251, tcell.Color252,
tcell.Color253, tcell.Color254, tcell.Color255,
}
return colors[color]
}

97
cmd/micro/command.go Normal file
View File

@@ -0,0 +1,97 @@
package main
import (
"os"
"regexp"
"strings"
)
// HandleCommand handles input from the user
func HandleCommand(input string, view *View) {
inputCmd := strings.Split(input, " ")[0]
args := strings.Split(input, " ")[1:]
commands := []string{"set", "quit", "save", "replace"}
i := 0
cmd := inputCmd
for _, c := range commands {
if strings.HasPrefix(c, inputCmd) {
i++
cmd = c
}
}
if i == 1 {
inputCmd = cmd
}
switch inputCmd {
case "set":
SetOption(view, args)
case "quit":
if view.CanClose("Quit anyway? ") {
screen.Fini()
os.Exit(0)
}
case "save":
view.Save()
case "replace":
r := regexp.MustCompile(`"[^"\\]*(?:\\.[^"\\]*)*"|[^\s]*`)
replaceCmd := r.FindAllString(strings.Join(args, " "), -1)
if len(replaceCmd) < 2 {
messenger.Error("Invalid replace statement: " + strings.Join(args, " "))
return
}
var flags string
if len(replaceCmd) == 3 {
// The user included some flags
flags = replaceCmd[2]
}
search := string(replaceCmd[0])
replace := string(replaceCmd[1])
if strings.HasPrefix(search, `"`) && strings.HasSuffix(search, `"`) {
search = search[1 : len(search)-1]
}
if strings.HasPrefix(replace, `"`) && strings.HasSuffix(replace, `"`) {
replace = replace[1 : len(replace)-1]
}
search = strings.Replace(search, `\"`, `"`, -1)
replace = strings.Replace(replace, `\"`, `"`, -1)
// messenger.Error(search + " -> " + replace)
regex, err := regexp.Compile(search)
if err != nil {
messenger.Error(err.Error())
return
}
found := false
for {
match := regex.FindStringIndex(view.buf.text)
if match == nil {
break
}
found = true
if strings.Contains(flags, "c") {
// // The 'check' flag was used
// if messenger.YesNoPrompt("Perform replacement?") {
// view.eh.Replace(match[0], match[1], replace)
// } else {
// continue
// }
}
view.eh.Replace(match[0], match[1], replace)
}
if !found {
messenger.Message("Nothing matched " + search)
}
default:
messenger.Error("Unknown command: " + inputCmd)
}
}

308
cmd/micro/cursor.go Normal file
View File

@@ -0,0 +1,308 @@
package main
import (
"strings"
)
// FromCharPos converts from a character position to an x, y position
func FromCharPos(loc int, buf *Buffer) (int, int) {
return FromCharPosStart(0, 0, 0, loc, buf)
}
// FromCharPosStart converts from a character position to an x, y position, starting at the specified character location
func FromCharPosStart(startLoc, startX, startY, loc int, buf *Buffer) (int, int) {
charNum := startLoc
x, y := startX, startY
lineLen := Count(buf.lines[y]) + 1
for charNum+lineLen <= loc {
charNum += lineLen
y++
lineLen = Count(buf.lines[y]) + 1
}
x = loc - charNum
return x, y
}
// ToCharPos converts from an x, y position to a character position
func ToCharPos(x, y int, buf *Buffer) int {
loc := 0
for i := 0; i < y; i++ {
// + 1 for the newline
loc += Count(buf.lines[i]) + 1
}
loc += x
return loc
}
// The Cursor struct stores the location of the cursor in the view
// The complicated part about the cursor is storing its location.
// The cursor must be displayed at an x, y location, but since the buffer
// uses a rope to store text, to insert text we must have an index. It
// is also simpler to use character indicies for other tasks such as
// selection.
type Cursor struct {
v *View
// The cursor display location
x int
y int
// Last cursor x position
lastVisualX int
// The current selection as a range of character numbers (inclusive)
curSelection [2]int
// The original selection as a range of character numbers
// This is used for line and word selection where it is necessary
// to know what the original selection was
origSelection [2]int
}
// SetLoc sets the location of the cursor in terms of character number
// and not x, y location
// It's just a simple wrapper of FromCharPos
func (c *Cursor) SetLoc(loc int) {
c.x, c.y = FromCharPos(loc, c.v.buf)
}
// Loc gets the cursor location in terms of character number instead
// of x, y location
// It's just a simple wrapper of ToCharPos
func (c *Cursor) Loc() int {
return ToCharPos(c.x, c.y, c.v.buf)
}
// ResetSelection resets the user's selection
func (c *Cursor) ResetSelection() {
c.curSelection[0] = 0
c.curSelection[1] = 0
}
// HasSelection returns whether or not the user has selected anything
func (c *Cursor) HasSelection() bool {
return c.curSelection[0] != c.curSelection[1]
}
// DeleteSelection deletes the currently selected text
func (c *Cursor) DeleteSelection() {
if c.curSelection[0] > c.curSelection[1] {
c.v.eh.Remove(c.curSelection[1], c.curSelection[0])
c.SetLoc(c.curSelection[1])
} else {
c.v.eh.Remove(c.curSelection[0], c.curSelection[1])
c.SetLoc(c.curSelection[0])
}
}
// GetSelection returns the cursor's selection
func (c *Cursor) GetSelection() string {
if c.curSelection[0] > c.curSelection[1] {
return string([]rune(c.v.buf.text)[c.curSelection[1]:c.curSelection[0]])
}
return string([]rune(c.v.buf.text)[c.curSelection[0]:c.curSelection[1]])
}
// SelectLine selects the current line
func (c *Cursor) SelectLine() {
c.Start()
c.curSelection[0] = c.Loc()
c.End()
c.curSelection[1] = c.Loc()
c.origSelection = c.curSelection
}
// AddLineToSelection adds the current line to the selection
func (c *Cursor) AddLineToSelection() {
loc := c.Loc()
if loc < c.origSelection[0] {
c.Start()
c.curSelection[0] = c.Loc()
c.curSelection[1] = c.origSelection[1]
}
if loc > c.origSelection[1] {
c.End()
c.curSelection[1] = c.Loc()
c.curSelection[0] = c.origSelection[0]
}
if loc < c.origSelection[1] && loc > c.origSelection[0] {
c.curSelection = c.origSelection
}
}
// SelectWord selects the word the cursor is currently on
func (c *Cursor) SelectWord() {
if len(c.v.buf.lines[c.y]) == 0 {
return
}
if !IsWordChar(string(c.RuneUnder(c.x))) {
loc := c.Loc()
c.curSelection[0] = loc
c.curSelection[1] = loc + 1
c.origSelection = c.curSelection
return
}
forward, backward := c.x, c.x
for backward > 0 && IsWordChar(string(c.RuneUnder(backward-1))) {
backward--
}
c.curSelection[0] = ToCharPos(backward, c.y, c.v.buf)
c.origSelection[0] = c.curSelection[0]
for forward < Count(c.v.buf.lines[c.y])-1 && IsWordChar(string(c.RuneUnder(forward+1))) {
forward++
}
c.curSelection[1] = ToCharPos(forward, c.y, c.v.buf) + 1
c.origSelection[1] = c.curSelection[1]
}
// AddWordToSelection adds the word the cursor is currently on to the selection
func (c *Cursor) AddWordToSelection() {
loc := c.Loc()
if loc > c.origSelection[0] && loc < c.origSelection[1] {
c.curSelection = c.origSelection
return
}
if loc < c.origSelection[0] {
backward := c.x
for backward > 0 && IsWordChar(string(c.RuneUnder(backward-1))) {
backward--
}
c.curSelection[0] = ToCharPos(backward, c.y, c.v.buf)
c.curSelection[1] = c.origSelection[1]
}
if loc > c.origSelection[1] {
forward := c.x
for forward < Count(c.v.buf.lines[c.y])-1 && IsWordChar(string(c.RuneUnder(forward+1))) {
forward++
}
c.curSelection[1] = ToCharPos(forward, c.y, c.v.buf) + 1
c.curSelection[0] = c.origSelection[0]
}
}
// RuneUnder returns the rune under the given x position
func (c *Cursor) RuneUnder(x int) rune {
line := []rune(c.v.buf.lines[c.y])
if x >= len(line) {
x = len(line) - 1
} else if x < 0 {
x = 0
}
return line[x]
}
// Up moves the cursor up one line (if possible)
func (c *Cursor) Up() {
if c.y > 0 {
c.y--
runes := []rune(c.v.buf.lines[c.y])
c.x = c.GetCharPosInLine(c.y, c.lastVisualX)
if c.x > len(runes) {
c.x = len(runes)
}
}
}
// Down moves the cursor down one line (if possible)
func (c *Cursor) Down() {
if c.y < len(c.v.buf.lines)-1 {
c.y++
runes := []rune(c.v.buf.lines[c.y])
c.x = c.GetCharPosInLine(c.y, c.lastVisualX)
if c.x > len(runes) {
c.x = len(runes)
}
}
}
// Left moves the cursor left one cell (if possible) or to the last line if it is at the beginning
func (c *Cursor) Left() {
if c.Loc() == 0 {
return
}
if c.x > 0 {
c.x--
} else {
c.Up()
c.End()
}
c.lastVisualX = c.GetVisualX()
}
// Right moves the cursor right one cell (if possible) or to the next line if it is at the end
func (c *Cursor) Right() {
if c.Loc() == c.v.buf.Len() {
return
}
if c.x < Count(c.v.buf.lines[c.y]) {
c.x++
} else {
c.Down()
c.Start()
}
c.lastVisualX = c.GetVisualX()
}
// End moves the cursor to the end of the line it is on
func (c *Cursor) End() {
c.x = Count(c.v.buf.lines[c.y])
c.lastVisualX = c.GetVisualX()
}
// Start moves the cursor to the start of the line it is on
func (c *Cursor) Start() {
c.x = 0
c.lastVisualX = c.GetVisualX()
}
// GetCharPosInLine gets the char position of a visual x y coordinate (this is necessary because tabs are 1 char but 4 visual spaces)
func (c *Cursor) GetCharPosInLine(lineNum, visualPos int) int {
// Get the tab size
tabSize := settings.TabSize
// This is the visual line -- every \t replaced with the correct number of spaces
visualLine := strings.Replace(c.v.buf.lines[lineNum], "\t", "\t"+Spaces(tabSize-1), -1)
if visualPos > Count(visualLine) {
visualPos = Count(visualLine)
}
numTabs := NumOccurences(visualLine[:visualPos], '\t')
if visualPos >= (tabSize-1)*numTabs {
return visualPos - (tabSize-1)*numTabs
}
return visualPos / tabSize
}
// GetVisualX returns the x value of the cursor in visual spaces
func (c *Cursor) GetVisualX() int {
runes := []rune(c.v.buf.lines[c.y])
tabSize := settings.TabSize
return c.x + NumOccurences(string(runes[:c.x]), '\t')*(tabSize-1)
}
// Display draws the cursor to the screen at the correct position
func (c *Cursor) Display() {
// Don't draw the cursor if it is out of the viewport or if it has a selection
if (c.y-c.v.topline < 0 || c.y-c.v.topline > c.v.height-1) || c.HasSelection() {
screen.HideCursor()
} else {
screen.ShowCursor(c.GetVisualX()+c.v.lineNumOffset-c.v.leftCol, c.y-c.v.topline)
}
}

196
cmd/micro/eventhandler.go Normal file
View File

@@ -0,0 +1,196 @@
package main
import (
"time"
)
const (
// Opposite and undoing events must have opposite values
// TextEventInsert repreasents an insertion event
TextEventInsert = 1
// TextEventRemove represents a deletion event
TextEventRemove = -1
)
// TextEvent holds data for a manipulation on some text that can be undone
type TextEvent struct {
c Cursor
eventType int
text string
start int
end int
buf *Buffer
time time.Time
}
// ExecuteTextEvent runs a text event
func ExecuteTextEvent(t *TextEvent) {
if t.eventType == TextEventInsert {
t.buf.Insert(t.start, t.text)
} else if t.eventType == TextEventRemove {
t.text = t.buf.Remove(t.start, t.end)
}
}
// UndoTextEvent undoes a text event
func UndoTextEvent(t *TextEvent) {
t.eventType = -t.eventType
ExecuteTextEvent(t)
}
// EventHandler executes text manipulations and allows undoing and redoing
type EventHandler struct {
v *View
undo *Stack
redo *Stack
}
// NewEventHandler returns a new EventHandler
func NewEventHandler(v *View) *EventHandler {
eh := new(EventHandler)
eh.undo = new(Stack)
eh.redo = new(Stack)
eh.v = v
return eh
}
// Insert creates an insert text event and executes it
func (eh *EventHandler) Insert(start int, text string) {
e := &TextEvent{
c: eh.v.cursor,
eventType: TextEventInsert,
text: text,
start: start,
end: start + Count(text),
buf: eh.v.buf,
time: time.Now(),
}
eh.Execute(e)
}
// Remove creates a remove text event and executes it
func (eh *EventHandler) Remove(start, end int) {
e := &TextEvent{
c: eh.v.cursor,
eventType: TextEventRemove,
start: start,
end: end,
buf: eh.v.buf,
time: time.Now(),
}
eh.Execute(e)
}
// Replace deletes from start to end and replaces it with the given string
func (eh *EventHandler) Replace(start, end int, replace string) {
eh.Remove(start, end)
eh.Insert(start, replace)
}
// Execute a textevent and add it to the undo stack
func (eh *EventHandler) Execute(t *TextEvent) {
if eh.redo.Len() > 0 {
eh.redo = new(Stack)
}
eh.undo.Push(t)
ExecuteTextEvent(t)
}
// Undo the first event in the undo stack
func (eh *EventHandler) Undo() {
t := eh.undo.Peek()
if t == nil {
return
}
te := t.(*TextEvent)
startTime := t.(*TextEvent).time.UnixNano() / int64(time.Millisecond)
eh.UndoOneEvent()
for {
t = eh.undo.Peek()
if t == nil {
return
}
te = t.(*TextEvent)
if startTime-(te.time.UnixNano()/int64(time.Millisecond)) > undoThreshold {
return
}
eh.UndoOneEvent()
}
}
// UndoOneEvent undoes one event
func (eh *EventHandler) UndoOneEvent() {
// This event should be undone
// Pop it off the stack
t := eh.undo.Pop()
if t == nil {
return
}
te := t.(*TextEvent)
// Undo it
// Modifies the text event
UndoTextEvent(te)
// Set the cursor in the right place
teCursor := te.c
te.c = eh.v.cursor
eh.v.cursor = teCursor
// Push it to the redo stack
eh.redo.Push(te)
}
// Redo the first event in the redo stack
func (eh *EventHandler) Redo() {
t := eh.redo.Peek()
if t == nil {
return
}
te := t.(*TextEvent)
startTime := t.(*TextEvent).time.UnixNano() / int64(time.Millisecond)
eh.RedoOneEvent()
for {
t = eh.redo.Peek()
if t == nil {
return
}
te = t.(*TextEvent)
if (te.time.UnixNano()/int64(time.Millisecond))-startTime > undoThreshold {
return
}
eh.RedoOneEvent()
}
}
// RedoOneEvent redoes one event
func (eh *EventHandler) RedoOneEvent() {
t := eh.redo.Pop()
if t == nil {
return
}
te := t.(*TextEvent)
// Modifies the text event
UndoTextEvent(te)
teCursor := te.c
te.c = eh.v.cursor
eh.v.cursor = teCursor
eh.undo.Push(te)
}

104
cmd/micro/help.go Normal file
View File

@@ -0,0 +1,104 @@
package main
import (
"github.com/gdamore/tcell"
"strings"
)
const helpTxt = `Press Ctrl-q to quit help
Micro keybindings:
Ctrl-q: Quit
Ctrl-s: Save
Ctrl-o: Open file
Ctrl-z: Undo
Ctrl-y: Redo
Ctrl-f: Find
Ctrl-n: Find next
Ctrl-p: Find previous
Ctrl-a: Select all
Ctrl-c: Copy
Ctrl-x: Cut
Ctrl-v: Paste
Ctrl-h: Open help
Ctrl-u: Half page up
Ctrl-d: Half page down
PageUp: Page up
PageDown: Page down
Ctrl-e: Execute a command
Possible commands:
'quit': Quits micro
'save': saves the current buffer
'replace "search" "value"': This will replace 'search' with 'value'.
Note that 'search' must be a valid regex. If one of the arguments
does not have any spaces in it, you may omit the quotes.
'set option value': sets the option to value. Please see the next section for a list of options you can set
Micro options:
colorscheme: loads the colorscheme stored in ~/.micro/colorschemes/'option'.micro
default value: 'default'
tabsize: sets the tab size to 'option'
default value: '4'
syntax: turns syntax on or off
default value: 'on'
`
// DisplayHelp displays the help txt
// It blocks the main loop
func DisplayHelp() {
topline := 0
_, height := screen.Size()
screen.HideCursor()
totalLines := strings.Split(helpTxt, "\n")
for {
screen.Clear()
lineEnd := topline + height
if lineEnd > len(totalLines) {
lineEnd = len(totalLines)
}
lines := totalLines[topline:lineEnd]
for y, line := range lines {
for x, ch := range line {
st := defStyle
screen.SetContent(x, y, ch, nil, st)
}
}
screen.Show()
event := screen.PollEvent()
switch e := event.(type) {
case *tcell.EventResize:
_, height = e.Size()
case *tcell.EventKey:
switch e.Key() {
case tcell.KeyUp:
if topline > 0 {
topline--
}
case tcell.KeyDown:
if topline < len(totalLines)-height {
topline++
}
case tcell.KeyCtrlQ, tcell.KeyCtrlW, tcell.KeyEscape, tcell.KeyCtrlC:
return
}
}
}
}

337
cmd/micro/highlighter.go Normal file
View File

@@ -0,0 +1,337 @@
package main
import (
"github.com/gdamore/tcell"
"github.com/mitchellh/go-homedir"
"io/ioutil"
"path/filepath"
"regexp"
"strings"
)
// FileTypeRules represents a complete set of syntax rules for a filetype
type FileTypeRules struct {
filetype string
rules []SyntaxRule
}
// SyntaxRule represents a regex to highlight in a certain style
type SyntaxRule struct {
// What to highlight
regex *regexp.Regexp
// Any flags
flags string
// Whether this regex is a start=... end=... regex
startend bool
// How to highlight it
style tcell.Style
}
var syntaxFiles map[[2]*regexp.Regexp]FileTypeRules
// LoadSyntaxFiles loads the syntax files from the default directory ~/.micro
func LoadSyntaxFiles() {
home, err := homedir.Dir()
if err != nil {
TermMessage("Error finding your home directory\nCan't load syntax files")
return
}
LoadSyntaxFilesFromDir(home + "/.micro/syntax")
}
// JoinRule takes a syntax rule (which can be multiple regular expressions)
// and joins it into one regular expression by ORing everything together
func JoinRule(rule string) string {
split := strings.Split(rule, `" "`)
joined := strings.Join(split, ")|(")
joined = "(" + joined + ")"
return joined
}
// LoadSyntaxFile loads the specified syntax file
// A syntax file is a list of syntax rules, explaining how to color certain
// regular expressions
// Example: color comment "//.*"
// This would color all strings that match the regex "//.*" in the comment color defined
// by the colorscheme
func LoadSyntaxFile(filename string) {
text, err := ioutil.ReadFile(filename)
if err != nil {
TermMessage("Error loading syntax file " + filename + ": " + err.Error())
return
}
lines := strings.Split(string(text), "\n")
// Regex for parsing syntax statements
syntaxParser := regexp.MustCompile(`syntax "(.*?)"\s+"(.*)"+`)
// Regex for parsing header statements
headerParser := regexp.MustCompile(`header "(.*)"`)
// Regex for parsing standard syntax rules
ruleParser := regexp.MustCompile(`color (.*?)\s+(?:\((.*?)\)\s+)?"(.*)"`)
// Regex for parsing syntax rules with start="..." end="..."
ruleStartEndParser := regexp.MustCompile(`color (.*?)\s+(?:\((.*?)\)\s+)?start="(.*)"\s+end="(.*)"`)
var syntaxRegex *regexp.Regexp
var headerRegex *regexp.Regexp
var filetype string
var rules []SyntaxRule
for lineNum, line := range lines {
if strings.TrimSpace(line) == "" ||
strings.TrimSpace(line)[0] == '#' {
// Ignore this line
continue
}
if strings.HasPrefix(line, "syntax") {
// Syntax statement
syntaxMatches := syntaxParser.FindSubmatch([]byte(line))
if len(syntaxMatches) == 3 {
if syntaxRegex != nil {
// Add the current rules to the syntaxFiles variable
regexes := [2]*regexp.Regexp{syntaxRegex, headerRegex}
syntaxFiles[regexes] = FileTypeRules{filetype, rules}
}
rules = rules[:0]
filetype = string(syntaxMatches[1])
extensions := JoinRule(string(syntaxMatches[2]))
syntaxRegex, err = regexp.Compile(extensions)
if err != nil {
TermError(filename, lineNum, err.Error())
continue
}
} else {
TermError(filename, lineNum, "Syntax statement is not valid: "+line)
continue
}
} else if strings.HasPrefix(line, "header") {
// Header statement
headerMatches := headerParser.FindSubmatch([]byte(line))
if len(headerMatches) == 2 {
header := JoinRule(string(headerMatches[1]))
headerRegex, err = regexp.Compile(header)
if err != nil {
TermError(filename, lineNum, "Regex error: "+err.Error())
continue
}
} else {
TermError(filename, lineNum, "Header statement is not valid: "+line)
continue
}
} else {
// Syntax rule, but it could be standard or start-end
if ruleParser.MatchString(line) {
// Standard syntax rule
// Parse the line
submatch := ruleParser.FindSubmatch([]byte(line))
var color string
var regexStr string
var flags string
if len(submatch) == 4 {
// If len is 4 then the user specified some additional flags to use
color = string(submatch[1])
flags = string(submatch[2])
regexStr = "(?" + flags + ")" + JoinRule(string(submatch[3]))
} else if len(submatch) == 3 {
// If len is 3, no additional flags were given
color = string(submatch[1])
regexStr = JoinRule(string(submatch[2]))
} else {
// If len is not 3 or 4 there is a problem
TermError(filename, lineNum, "Invalid statement: "+line)
continue
}
// Compile the regex
regex, err := regexp.Compile(regexStr)
if err != nil {
TermError(filename, lineNum, err.Error())
continue
}
// Get the style
// The user could give us a "color" that is really a part of the colorscheme
// in which case we should look that up in the colorscheme
// They can also just give us a straight up color
st := defStyle
if _, ok := colorscheme[color]; ok {
st = colorscheme[color]
} else {
st = StringToStyle(color)
}
// Add the regex, flags, and style
// False because this is not start-end
rules = append(rules, SyntaxRule{regex, flags, false, st})
} else if ruleStartEndParser.MatchString(line) {
// Start-end syntax rule
submatch := ruleStartEndParser.FindSubmatch([]byte(line))
var color string
var start string
var end string
// Use m and s flags by default
flags := "ms"
if len(submatch) == 5 {
// If len is 5 the user provided some additional flags
color = string(submatch[1])
flags += string(submatch[2])
start = string(submatch[3])
end = string(submatch[4])
} else if len(submatch) == 4 {
// If len is 4 the user did not provide additional flags
color = string(submatch[1])
start = string(submatch[2])
end = string(submatch[3])
} else {
// If len is not 4 or 5 there is a problem
TermError(filename, lineNum, "Invalid statement: "+line)
continue
}
// Compile the regex
regex, err := regexp.Compile("(?" + flags + ")" + "(" + start + ").*?(" + end + ")")
if err != nil {
TermError(filename, lineNum, err.Error())
continue
}
// Get the style
// The user could give us a "color" that is really a part of the colorscheme
// in which case we should look that up in the colorscheme
// They can also just give us a straight up color
st := defStyle
if _, ok := colorscheme[color]; ok {
st = colorscheme[color]
} else {
st = StringToStyle(color)
}
// Add the regex, flags, and style
// True because this is start-end
rules = append(rules, SyntaxRule{regex, flags, true, st})
}
}
}
if syntaxRegex != nil {
// Add the current rules to the syntaxFiles variable
regexes := [2]*regexp.Regexp{syntaxRegex, headerRegex}
syntaxFiles[regexes] = FileTypeRules{filetype, rules}
}
}
// LoadSyntaxFilesFromDir loads the syntax files from a specified directory
// To load the syntax files, we must fill the `syntaxFiles` map
// This involves finding the regex for syntax and if it exists, the regex
// for the header. Then we must get the text for the file and the filetype.
func LoadSyntaxFilesFromDir(dir string) {
InitColorscheme()
syntaxFiles = make(map[[2]*regexp.Regexp]FileTypeRules)
files, _ := ioutil.ReadDir(dir)
for _, f := range files {
if filepath.Ext(f.Name()) == ".micro" {
LoadSyntaxFile(dir + "/" + f.Name())
}
}
}
// GetRules finds the syntax rules that should be used for the buffer
// and returns them. It also returns the filetype of the file
func GetRules(buf *Buffer) ([]SyntaxRule, string) {
for r := range syntaxFiles {
if r[0] != nil && r[0].MatchString(buf.path) {
return syntaxFiles[r].rules, syntaxFiles[r].filetype
} else if r[1] != nil && r[1].MatchString(buf.lines[0]) {
return syntaxFiles[r].rules, syntaxFiles[r].filetype
}
}
return nil, "Unknown"
}
// SyntaxMatches is an alias to a map from character numbers to styles,
// so map[3] represents the style of the third character
type SyntaxMatches [][]tcell.Style
// Match takes a buffer and returns the syntax matches a map specifying how it should be syntax highlighted
// We need to check the start-end regexes for the entire buffer every time Match is called, but for the
// non start-end rules, we only have to update the updateLines provided by the view
func Match(v *View) SyntaxMatches {
buf := v.buf
rules := v.buf.rules
viewStart := v.topline
viewEnd := v.topline + v.height
if viewEnd > len(buf.lines) {
viewEnd = len(buf.lines)
}
// updateStart := v.updateLines[0]
// updateEnd := v.updateLines[1]
//
// if updateEnd > len(buf.lines) {
// updateEnd = len(buf.lines)
// }
// if updateStart < 0 {
// updateStart = 0
// }
lines := buf.lines[viewStart:viewEnd]
// updateLines := buf.lines[updateStart:updateEnd]
matches := make(SyntaxMatches, len(lines))
for i, line := range lines {
matches[i] = make([]tcell.Style, len(line)+1)
}
// We don't actually check the entire buffer, just from synLinesUp to synLinesDown
totalStart := v.topline - synLinesUp
totalEnd := v.topline + v.height + synLinesDown
if totalStart < 0 {
totalStart = 0
}
if totalEnd > len(buf.lines) {
totalEnd = len(buf.lines)
}
str := strings.Join(buf.lines[totalStart:totalEnd], "\n")
startNum := ToCharPos(0, totalStart, v.buf)
toplineNum := ToCharPos(0, v.topline, v.buf)
for _, rule := range rules {
if rule.startend {
if indicies := rule.regex.FindAllStringIndex(str, -1); indicies != nil {
for _, value := range indicies {
value[0] += startNum
value[1] += startNum
for i := value[0]; i < value[1]; i++ {
if i < toplineNum {
continue
}
colNum, lineNum := FromCharPosStart(toplineNum, 0, v.topline, i, buf)
if lineNum == -1 || colNum == -1 {
continue
}
lineNum -= viewStart
if lineNum >= 0 && lineNum < v.height {
matches[lineNum][colNum] = rule.style
}
}
}
}
} else {
for lineN, line := range lines {
if indicies := rule.regex.FindAllStringIndex(line, -1); indicies != nil {
for _, value := range indicies {
for i := value[0]; i < value[1]; i++ {
// matches[lineN+updateStart][i] = rule.style
matches[lineN][i] = rule.style
}
}
}
}
}
}
return matches
}

192
cmd/micro/messenger.go Normal file
View File

@@ -0,0 +1,192 @@
package main
import (
"bufio"
"fmt"
"github.com/gdamore/tcell"
"os"
"strconv"
)
// TermMessage sends a message to the user in the terminal. This usually occurs before
// micro has been fully initialized -- ie if there is an error in the syntax highlighting
// regular expressions
// The function must be called when the screen is not initialized
// This will write the message, and wait for the user
// to press and key to continue
func TermMessage(msg string) {
fmt.Println(msg)
fmt.Print("\nPress enter to continue")
reader := bufio.NewReader(os.Stdin)
reader.ReadString('\n')
}
// TermError sends an error to the user in the terminal. Like TermMessage except formatted
// as an error
func TermError(filename string, lineNum int, err string) {
TermMessage(filename + ", " + strconv.Itoa(lineNum) + ": " + err)
}
// Messenger is an object that makes it easy to send messages to the user
// and get input from the user
type Messenger struct {
// Are we currently prompting the user?
hasPrompt bool
// Is there a message to print
hasMessage bool
// Message to print
message string
// The user's response to a prompt
response string
// style to use when drawing the message
style tcell.Style
// We have to keep track of the cursor for prompting
cursorx int
}
// Message sends a message to the user
func (m *Messenger) Message(msg string) {
m.message = msg
m.style = defStyle
if _, ok := colorscheme["message"]; ok {
m.style = colorscheme["message"]
}
m.hasMessage = true
}
// Error sends an error message to the user
func (m *Messenger) Error(msg string) {
m.message = msg
m.style = defStyle.
Foreground(tcell.ColorBlack).
Background(tcell.ColorMaroon)
if _, ok := colorscheme["error-message"]; ok {
m.style = colorscheme["error-message"]
}
m.hasMessage = true
}
// YesNoPrompt asks the user a yes or no question (waits for y or n) and returns the result
func (m *Messenger) YesNoPrompt(prompt string) bool {
m.Message(prompt)
for {
m.Clear()
m.Display()
screen.Show()
event := screen.PollEvent()
switch e := event.(type) {
case *tcell.EventKey:
if e.Key() == tcell.KeyRune {
if e.Rune() == 'y' {
return true
} else if e.Rune() == 'n' {
return false
}
}
}
}
}
// Prompt sends the user a message and waits for a response to be typed in
// This function blocks the main loop while waiting for input
func (m *Messenger) Prompt(prompt string) (string, bool) {
m.hasPrompt = true
m.Message(prompt)
response, canceled := "", true
for m.hasPrompt {
m.Clear()
m.Display()
event := screen.PollEvent()
switch e := event.(type) {
case *tcell.EventKey:
switch e.Key() {
case tcell.KeyCtrlQ, tcell.KeyCtrlC, tcell.KeyEscape:
// Cancel
m.hasPrompt = false
case tcell.KeyEnter:
// User is done entering their response
m.hasPrompt = false
response, canceled = m.response, false
}
}
m.HandleEvent(event)
if m.cursorx < 0 {
// Cancel
m.hasPrompt = false
}
}
m.Reset()
return response, canceled
}
// HandleEvent handles an event for the prompter
func (m *Messenger) HandleEvent(event tcell.Event) {
switch e := event.(type) {
case *tcell.EventKey:
switch e.Key() {
case tcell.KeyLeft:
if m.cursorx > 0 {
m.cursorx--
}
case tcell.KeyRight:
if m.cursorx < Count(m.response) {
m.cursorx++
}
case tcell.KeyBackspace2:
if m.cursorx > 0 {
m.response = string([]rune(m.response)[:m.cursorx-1]) + string(m.response[m.cursorx:])
}
m.cursorx--
case tcell.KeySpace:
m.response += " "
m.cursorx++
case tcell.KeyRune:
m.response = Insert(m.response, m.cursorx, string(e.Rune()))
m.cursorx++
}
}
}
// Reset resets the messenger's cursor, message and response
func (m *Messenger) Reset() {
m.cursorx = 0
m.message = ""
m.response = ""
}
// Clear clears the line at the bottom of the editor
func (m *Messenger) Clear() {
w, h := screen.Size()
for x := 0; x < w; x++ {
screen.SetContent(x, h-1, ' ', nil, defStyle)
}
}
// Display displays messages or prompts
func (m *Messenger) Display() {
_, h := screen.Size()
if m.hasMessage {
runes := []rune(m.message + m.response)
for x := 0; x < len(runes); x++ {
screen.SetContent(x, h-1, runes[x], nil, m.style)
}
}
if m.hasPrompt {
screen.ShowCursor(Count(m.message)+m.cursorx, h-1)
screen.Show()
}
}

173
cmd/micro/micro.go Normal file
View File

@@ -0,0 +1,173 @@
package main
import (
"fmt"
"github.com/gdamore/tcell"
"github.com/go-errors/errors"
"github.com/mattn/go-isatty"
"io/ioutil"
"os"
)
const (
synLinesUp = 75 // How many lines up to look to do syntax highlighting
synLinesDown = 75 // How many lines down to look to do syntax highlighting
doubleClickThreshold = 400 // How many milliseconds to wait before a second click is not a double click
undoThreshold = 500 // If two events are less than n milliseconds apart, undo both of them
)
var (
// The main screen
screen tcell.Screen
// Object to send messages and prompts to the user
messenger *Messenger
// The default style
defStyle tcell.Style
)
// LoadInput loads the file input for the editor
func LoadInput() (string, []byte, error) {
// There are a number of ways micro should start given its input
// 1. If it is given a file in os.Args, it should open that
// 2. If there is no input file and the input is not a terminal, that means
// something is being piped in and the stdin should be opened in an
// empty buffer
// 3. If there is no input file and the input is a terminal, an empty buffer
// should be opened
// These are empty by default so if we get to option 3, we can just returns the
// default values
var filename string
var input []byte
var err error
if len(os.Args) > 1 {
// Option 1
filename = os.Args[1]
// Check that the file exists
if _, e := os.Stat(filename); e == nil {
input, err = ioutil.ReadFile(filename)
}
} else if !isatty.IsTerminal(os.Stdin.Fd()) {
// Option 2
// The input is not a terminal, so something is being piped in
// and we should read from stdin
input, err = ioutil.ReadAll(os.Stdin)
}
// Option 3, or just return whatever we got
return filename, input, err
}
func main() {
filename, input, err := LoadInput()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
InitSettings()
// Load the syntax files, including the colorscheme
LoadSyntaxFiles()
// Should we enable true color?
truecolor := os.Getenv("MICRO_TRUECOLOR") == "1"
// In order to enable true color, we have to set the TERM to `xterm-truecolor` when
// initializing tcell, but after that, we can set the TERM back to whatever it was
oldTerm := os.Getenv("TERM")
if truecolor {
os.Setenv("TERM", "xterm-truecolor")
}
// Initilize tcell
screen, err = tcell.NewScreen()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
if err = screen.Init(); err != nil {
fmt.Println(err)
os.Exit(1)
}
// Now we can put the TERM back to what it was before
if truecolor {
os.Setenv("TERM", oldTerm)
}
// This is just so if we have an error, we can exit cleanly and not completely
// mess up the terminal being worked in
defer func() {
if err := recover(); err != nil {
screen.Fini()
fmt.Println("Micro encountered an error:", err)
// Print the stack trace too
fmt.Print(errors.Wrap(err, 2).ErrorStack())
os.Exit(1)
}
}()
// Default style
defStyle = tcell.StyleDefault.
Foreground(tcell.ColorDefault).
Background(tcell.ColorDefault)
// There may be another default style defined in the colorscheme
if style, ok := colorscheme["default"]; ok {
defStyle = style
}
screen.SetStyle(defStyle)
screen.EnableMouse()
messenger = new(Messenger)
view := NewView(NewBuffer(string(input), filename))
for {
// Display everything
screen.Clear()
view.Display()
messenger.Display()
screen.Show()
// Wait for the user's action
event := screen.PollEvent()
if searching {
HandleSearchEvent(event, view)
} else {
// Check if we should quit
switch e := event.(type) {
case *tcell.EventKey:
switch e.Key() {
case tcell.KeyCtrlQ:
// Make sure not to quit if there are unsaved changes
if view.CanClose("Quit anyway? ") {
screen.Fini()
os.Exit(0)
}
case tcell.KeyCtrlE:
input, canceled := messenger.Prompt("> ")
if !canceled {
HandleCommand(input, view)
}
case tcell.KeyCtrlH:
DisplayHelp()
// Make sure to resize the view if the user resized the terminal while looking at the help text
view.Resize(screen.Size())
}
}
// Send it to the view
view.HandleEvent(event)
}
}
}

116
cmd/micro/search.go Normal file
View File

@@ -0,0 +1,116 @@
package main
import (
"github.com/gdamore/tcell"
"regexp"
)
var (
// What was the last search
lastSearch string
// Where should we start the search down from (or up from)
searchStart int
// Is there currently a search in progress
searching bool
)
// BeginSearch starts a search
func BeginSearch() {
searching = true
messenger.hasPrompt = true
messenger.Message("Find: ")
}
// EndSearch stops the current search
func EndSearch() {
searching = false
messenger.hasPrompt = false
messenger.Clear()
messenger.Reset()
}
// HandleSearchEvent takes an event and a view and will do a real time match from the messenger's output
// to the current buffer. It searches down the buffer.
func HandleSearchEvent(event tcell.Event, v *View) {
switch e := event.(type) {
case *tcell.EventKey:
switch e.Key() {
case tcell.KeyCtrlQ, tcell.KeyCtrlC, tcell.KeyEscape, tcell.KeyEnter:
// Done
EndSearch()
return
}
}
messenger.HandleEvent(event)
if messenger.cursorx < 0 {
// Done
EndSearch()
return
}
if messenger.response == "" {
v.cursor.ResetSelection()
// We don't end the search though
return
}
Search(messenger.response, v, true)
return
}
// Search searches in the view for the given regex. The down bool
// specifies whether it should search down from the searchStart position
// or up from there
func Search(searchStr string, v *View, down bool) {
if searchStr == "" {
return
}
var str string
var charPos int
if down {
str = v.buf.text[searchStart:]
charPos = searchStart
} else {
str = v.buf.text[:searchStart]
}
r, err := regexp.Compile(searchStr)
if err != nil {
return
}
matches := r.FindAllStringIndex(str, -1)
var match []int
if matches == nil {
// Search the entire buffer now
matches = r.FindAllStringIndex(v.buf.text, -1)
charPos = 0
if matches == nil {
v.cursor.ResetSelection()
return
}
if !down {
match = matches[len(matches)-1]
} else {
match = matches[0]
}
}
if !down {
match = matches[len(matches)-1]
} else {
match = matches[0]
}
v.cursor.curSelection[0] = charPos + match[0]
v.cursor.curSelection[1] = charPos + match[1]
v.cursor.x, v.cursor.y = FromCharPos(charPos+match[1]-1, v.buf)
if v.Relocate() {
v.matches = Match(v)
}
lastSearch = searchStr
}

123
cmd/micro/settings.go Normal file
View File

@@ -0,0 +1,123 @@
package main
import (
"encoding/json"
"github.com/mitchellh/go-homedir"
"io/ioutil"
"os"
"strconv"
"strings"
)
// The options that the user can set
var settings Settings
// All the possible settings
var possibleSettings = []string{"colorscheme", "tabsize", "autoindent", "syntax"}
// The Settings struct contains the settings for micro
type Settings struct {
Colorscheme string `json:"colorscheme"`
TabSize int `json:"tabsize"`
AutoIndent bool `json:"autoindent"`
Syntax bool `json:"syntax"`
}
// InitSettings initializes the options map and sets all options to their default values
func InitSettings() {
home, err := homedir.Dir()
if err != nil {
TermMessage("Error finding your home directory\nCan't load settings file")
return
}
filename := home + "/.micro/settings.json"
if _, e := os.Stat(filename); e == nil {
input, err := ioutil.ReadFile(filename)
if err != nil {
TermMessage("Error reading settings.json file: " + err.Error())
return
}
json.Unmarshal(input, &settings)
} else {
settings = DefaultSettings()
err := WriteSettings(filename)
if err != nil {
TermMessage("Error writing settings.json file: " + err.Error())
}
}
}
// WriteSettings writes the settings to the specified filename as JSON
func WriteSettings(filename string) error {
var err error
home, err := homedir.Dir()
if err != nil {
return err
}
if _, e := os.Stat(home + "/.micro"); e == nil {
txt, _ := json.MarshalIndent(settings, "", " ")
err = ioutil.WriteFile(filename, txt, 0644)
}
return err
}
// DefaultSettings returns the default settings for micro
func DefaultSettings() Settings {
return Settings{
Colorscheme: "default",
TabSize: 4,
AutoIndent: true,
Syntax: true,
}
}
// SetOption prompts the user to set an option and checks that the response is valid
func SetOption(view *View, args []string) {
home, err := homedir.Dir()
if err != nil {
messenger.Error("Error finding your home directory\nCan't load settings file")
}
filename := home + "/.micro/settings.json"
if len(args) == 2 {
option := strings.TrimSpace(args[0])
value := strings.TrimSpace(args[1])
if Contains(possibleSettings, option) {
if option == "tabsize" {
tsize, err := strconv.Atoi(value)
if err != nil {
messenger.Error("Invalid value for " + option)
return
}
settings.TabSize = tsize
} else if option == "colorscheme" {
settings.Colorscheme = value
LoadSyntaxFiles()
view.buf.UpdateRules()
} else if option == "syntax" {
if value == "on" {
settings.Syntax = true
} else if value == "off" {
settings.Syntax = false
} else {
messenger.Error("Invalid value for " + option)
return
}
LoadSyntaxFiles()
view.buf.UpdateRules()
}
err := WriteSettings(filename)
if err != nil {
messenger.Error("Error writing to settings.json: " + err.Error())
return
}
} else {
messenger.Error("Option " + option + " does not exist")
}
} else {
messenger.Error("Invalid option, please use option value")
}
}

43
cmd/micro/stack.go Normal file
View File

@@ -0,0 +1,43 @@
package main
// Stack is a simple implementation of a LIFO stack
type Stack struct {
top *Element
size int
}
// An Element which is stored in the Stack
type Element struct {
value interface{} // All types satisfy the empty interface, so we can store anything here.
next *Element
}
// Len returns the stack's length
func (s *Stack) Len() int {
return s.size
}
// Push a new element onto the stack
func (s *Stack) Push(value interface{}) {
s.top = &Element{value, s.top}
s.size++
}
// Pop removes the top element from the stack and returns its value
// If the stack is empty, return nil
func (s *Stack) Pop() (value interface{}) {
if s.size > 0 {
value, s.top = s.top.value, s.top.next
s.size--
return
}
return nil
}
// Peek returns the top element of the stack without removing it
func (s *Stack) Peek() interface{} {
if s.size > 0 {
return s.top.value
}
return nil
}

39
cmd/micro/stack_test.go Normal file
View File

@@ -0,0 +1,39 @@
package main
import "testing"
func TestStack(t *testing.T) {
stack := new(Stack)
if stack.Len() != 0 {
t.Errorf("Len failed")
}
stack.Push(5)
stack.Push("test")
stack.Push(10)
if stack.Len() != 3 {
t.Errorf("Len failed")
}
var popped interface{}
popped = stack.Pop()
if popped != 10 {
t.Errorf("Pop failed")
}
popped = stack.Pop()
if popped != "test" {
t.Errorf("Pop failed")
}
stack.Push("test")
popped = stack.Pop()
if popped != "test" {
t.Errorf("Pop failed")
}
stack.Pop()
popped = stack.Pop()
if popped != nil {
t.Errorf("Pop failed")
}
}

61
cmd/micro/statusline.go Normal file
View File

@@ -0,0 +1,61 @@
package main
import (
"strconv"
)
// Statusline represents the information line at the bottom
// of each view
// It gives information such as filename, whether the file has been
// modified, filetype, cursor location
type Statusline struct {
view *View
}
// Display draws the statusline to the screen
func (sline *Statusline) Display() {
// We'll draw the line at the lowest line in the view
y := sline.view.height
file := sline.view.buf.name
// If the name is empty, use 'No name'
if file == "" {
file = "No name"
}
// If the buffer is dirty (has been modified) write a little '+'
if sline.view.buf.IsDirty() {
file += " +"
}
// Add one to cursor.x and cursor.y because (0,0) is the top left,
// but users will be used to (1,1) (first line,first column)
// We use GetVisualX() here because otherwise we get the column number in runes
// so a '\t' is only 1, when it should be tabSize
columnNum := strconv.Itoa(sline.view.cursor.GetVisualX() + 1)
lineNum := strconv.Itoa(sline.view.cursor.y + 1)
file += " (" + lineNum + "," + columnNum + ")"
// Add the filetype
file += " " + sline.view.buf.filetype
centerText := "Press Ctrl-h for help"
statusLineStyle := defStyle.Reverse(true)
if style, ok := colorscheme["statusline"]; ok {
statusLineStyle = style
}
// Maybe there is a unicode filename?
fileRunes := []rune(file)
for x := 0; x < sline.view.width; x++ {
if x < len(fileRunes) {
screen.SetContent(x, y, fileRunes[x], nil, statusLineStyle)
} else if x >= sline.view.width/2-len(centerText)/2 && x < len(centerText)+sline.view.width/2-len(centerText)/2 {
screen.SetContent(x, y, []rune(centerText)[x-sline.view.width/2+len(centerText)/2], nil, statusLineStyle)
} else {
screen.SetContent(x, y, ' ', nil, statusLineStyle)
}
}
}

77
cmd/micro/util.go Normal file
View File

@@ -0,0 +1,77 @@
package main
import (
"unicode/utf8"
)
// Util.go is a collection of utility functions that are used throughout
// the program
// Count returns the length of a string in runes
// This is exactly equivalent to utf8.RuneCountInString(), just less characters
func Count(s string) int {
return utf8.RuneCountInString(s)
}
// NumOccurences counts the number of occurences of a byte in a string
func NumOccurences(s string, c byte) int {
var n int
for i := 0; i < len(s); i++ {
if s[i] == c {
n++
}
}
return n
}
// Spaces returns a string with n spaces
func Spaces(n int) string {
var str string
for i := 0; i < n; i++ {
str += " "
}
return str
}
// Min takes the min of two ints
func Min(a, b int) int {
if a > b {
return b
}
return a
}
// Max takes the max of two ints
func Max(a, b int) int {
if a > b {
return a
}
return b
}
// IsWordChar returns whether or not the string is a 'word character'
// If it is a unicode character, then it does not match
// Word characters are defined as [A-Za-z0-9_]
func IsWordChar(str string) bool {
if len(str) > 1 {
// Unicode
return false
}
c := str[0]
return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c == '_')
}
// Contains returns whether or not a string array contains a given string
func Contains(list []string, a string) bool {
for _, b := range list {
if b == a {
return true
}
}
return false
}
// Insert makes a simple insert into a string at the given position
func Insert(str string, pos int, value string) string {
return string([]rune(str)[:pos]) + value + string([]rune(str)[pos:])
}

65
cmd/micro/util_test.go Normal file
View File

@@ -0,0 +1,65 @@
package main
import "testing"
func TestNumOccurences(t *testing.T) {
var tests = []struct {
inputStr string
inputChar byte
want int
}{
{"aaaa", 'a', 4},
{"\trfd\ta", '\t', 2},
{"∆ƒ\tø ® \t\t", '\t', 3},
}
for _, test := range tests {
if got := NumOccurences(test.inputStr, test.inputChar); got != test.want {
t.Errorf("NumOccurences(%s, %c) = %d", test.inputStr, test.inputChar, got)
}
}
}
func TestSpaces(t *testing.T) {
var tests = []struct {
input int
want string
}{
{4, " "},
{0, ""},
}
for _, test := range tests {
if got := Spaces(test.input); got != test.want {
t.Errorf("Spaces(%d) = \"%s\"", test.input, got)
}
}
}
func TestIsWordChar(t *testing.T) {
if IsWordChar("t") == false {
t.Errorf("IsWordChar(t) = false")
}
if IsWordChar("T") == false {
t.Errorf("IsWordChar(T) = false")
}
if IsWordChar("5") == false {
t.Errorf("IsWordChar(5) = false")
}
if IsWordChar("_") == false {
t.Errorf("IsWordChar(_) = false")
}
if IsWordChar("~") == true {
t.Errorf("IsWordChar(~) = true")
}
if IsWordChar(" ") == true {
t.Errorf("IsWordChar( ) = true")
}
if IsWordChar("ß") == true {
t.Errorf("IsWordChar(ß) = true")
}
if IsWordChar(")") == true {
t.Errorf("IsWordChar()) = true")
}
if IsWordChar("\n") == true {
t.Errorf("IsWordChar(\n)) = true")
}
}

737
cmd/micro/view.go Normal file
View File

@@ -0,0 +1,737 @@
package main
import (
"github.com/atotto/clipboard"
"github.com/gdamore/tcell"
"io/ioutil"
"strconv"
"strings"
"time"
)
// The View struct stores information about a view into a buffer.
// It has a stores information about the cursor, and the viewport
// that the user sees the buffer from.
type View struct {
cursor Cursor
// The topmost line, used for vertical scrolling
topline int
// The leftmost column, used for horizontal scrolling
leftCol int
// Percentage of the terminal window that this view takes up (from 0 to 100)
widthPercent int
heightPercent int
// Actual with and height
width int
height int
// How much to offset because of line numbers
lineNumOffset int
// The eventhandler for undo/redo
eh *EventHandler
// The buffer
buf *Buffer
// The statusline
sline Statusline
// Since tcell doesn't differentiate between a mouse release event
// and a mouse move event with no keys pressed, we need to keep
// track of whether or not the mouse was pressed (or not released) last event to determine
// mouse release events
mouseReleased bool
// This stores when the last click was
// This is useful for detecting double and triple clicks
lastClickTime time.Time
// Was the last mouse event actually a double click?
// Useful for detecting triple clicks -- if a double click is detected
// but the last mouse event was actually a double click, it's a triple click
doubleClick bool
// Same here, just to keep track for mouse move events
tripleClick bool
// Syntax highlighting matches
matches SyntaxMatches
// The matches from the last frame
lastMatches SyntaxMatches
// This is the range of lines that should have their syntax highlighting updated
updateLines [2]int
}
// NewView returns a new fullscreen view
func NewView(buf *Buffer) *View {
return NewViewWidthHeight(buf, 100, 100)
}
// NewViewWidthHeight returns a new view with the specified width and height percentages
// Note that w and h are percentages not actual values
func NewViewWidthHeight(buf *Buffer, w, h int) *View {
v := new(View)
v.buf = buf
v.widthPercent = w
v.heightPercent = h
v.Resize(screen.Size())
v.topline = 0
// Put the cursor at the first spot
v.cursor = Cursor{
x: 0,
y: 0,
v: v,
}
v.cursor.ResetSelection()
v.eh = NewEventHandler(v)
v.sline = Statusline{
view: v,
}
// Update the syntax highlighting for the entire buffer at the start
v.UpdateLines(v.topline, v.topline+v.height)
v.matches = Match(v)
// Set mouseReleased to true because we assume the mouse is not being pressed when
// the editor is opened
v.mouseReleased = true
v.lastClickTime = time.Time{}
return v
}
// UpdateLines sets the values for v.updateLines
func (v *View) UpdateLines(start, end int) {
v.updateLines[0] = start
v.updateLines[1] = end + 1
}
// Resize recalculates the actual width and height of the view from the width and height
// percentages
// This is usually called when the window is resized, or when a split has been added and
// the percentages have changed
func (v *View) Resize(w, h int) {
// Always include 1 line for the command line at the bottom
h--
v.width = int(float32(w) * float32(v.widthPercent) / 100)
// We subtract 1 for the statusline
v.height = int(float32(h)*float32(v.heightPercent)/100) - 1
}
// ScrollUp scrolls the view up n lines (if possible)
func (v *View) ScrollUp(n int) {
// Try to scroll by n but if it would overflow, scroll by 1
if v.topline-n >= 0 {
v.topline -= n
} else if v.topline > 0 {
v.topline--
}
}
// ScrollDown scrolls the view down n lines (if possible)
func (v *View) ScrollDown(n int) {
// Try to scroll by n but if it would overflow, scroll by 1
if v.topline+n <= len(v.buf.lines)-v.height {
v.topline += n
} else if v.topline < len(v.buf.lines)-v.height {
v.topline++
}
}
// PageUp scrolls the view up a page
func (v *View) PageUp() {
if v.topline > v.height {
v.ScrollUp(v.height)
} else {
v.topline = 0
}
}
// PageDown scrolls the view down a page
func (v *View) PageDown() {
if len(v.buf.lines)-(v.topline+v.height) > v.height {
v.ScrollDown(v.height)
} else {
if len(v.buf.lines) >= v.height {
v.topline = len(v.buf.lines) - v.height
}
}
}
// HalfPageUp scrolls the view up half a page
func (v *View) HalfPageUp() {
if v.topline > v.height/2 {
v.ScrollUp(v.height / 2)
} else {
v.topline = 0
}
}
// HalfPageDown scrolls the view down half a page
func (v *View) HalfPageDown() {
if len(v.buf.lines)-(v.topline+v.height) > v.height/2 {
v.ScrollDown(v.height / 2)
} else {
if len(v.buf.lines) >= v.height {
v.topline = len(v.buf.lines) - v.height
}
}
}
// CanClose returns whether or not the view can be closed
// If there are unsaved changes, the user will be asked if the view can be closed
// causing them to lose the unsaved changes
// The message is what to print after saying "You have unsaved changes. "
func (v *View) CanClose(msg string) bool {
if v.buf.IsDirty() {
quit, canceled := messenger.Prompt("You have unsaved changes. " + msg)
if !canceled {
if strings.ToLower(quit) == "yes" || strings.ToLower(quit) == "y" {
return true
}
}
} else {
return true
}
return false
}
// Save the buffer to disk
func (v *View) Save() {
// If this is an empty buffer, ask for a filename
if v.buf.path == "" {
filename, canceled := messenger.Prompt("Filename: ")
if !canceled {
v.buf.path = filename
v.buf.name = filename
} else {
return
}
}
err := v.buf.Save()
if err != nil {
messenger.Error(err.Error())
} else {
messenger.Message("Saved " + v.buf.path)
}
}
// Copy the selection to the system clipboard
func (v *View) Copy() {
if v.cursor.HasSelection() {
if !clipboard.Unsupported {
clipboard.WriteAll(v.cursor.GetSelection())
} else {
messenger.Error("Clipboard is not supported on your system")
}
}
}
// Cut the selection to the system clipboard
func (v *View) Cut() {
if v.cursor.HasSelection() {
if !clipboard.Unsupported {
clipboard.WriteAll(v.cursor.GetSelection())
v.cursor.DeleteSelection()
v.cursor.ResetSelection()
} else {
messenger.Error("Clipboard is not supported on your system")
}
}
}
// Paste whatever is in the system clipboard into the buffer
// Delete and paste if the user has a selection
func (v *View) Paste() {
if !clipboard.Unsupported {
if v.cursor.HasSelection() {
v.cursor.DeleteSelection()
v.cursor.ResetSelection()
}
clip, _ := clipboard.ReadAll()
v.eh.Insert(v.cursor.Loc(), clip)
v.cursor.SetLoc(v.cursor.Loc() + Count(clip))
} else {
messenger.Error("Clipboard is not supported on your system")
}
}
// SelectAll selects the entire buffer
func (v *View) SelectAll() {
v.cursor.curSelection[1] = 0
v.cursor.curSelection[0] = v.buf.Len()
// Put the cursor at the beginning
v.cursor.x = 0
v.cursor.y = 0
}
// OpenFile opens a new file in the current view
// It makes sure that the current buffer can be closed first (unsaved changes)
func (v *View) OpenFile() {
if v.CanClose("Continue? ") {
filename, canceled := messenger.Prompt("File to open: ")
if canceled {
return
}
file, err := ioutil.ReadFile(filename)
if err != nil {
messenger.Error(err.Error())
return
}
v.buf = NewBuffer(string(file), filename)
}
}
// Relocate moves the view window so that the cursor is in view
// This is useful if the user has scrolled far away, and then starts typing
func (v *View) Relocate() bool {
ret := false
cy := v.cursor.y
if cy < v.topline {
v.topline = cy
ret = true
}
if cy > v.topline+v.height-1 {
v.topline = cy - v.height + 1
ret = true
}
cx := v.cursor.GetVisualX()
if cx < v.leftCol {
v.leftCol = cx
ret = true
}
if cx+v.lineNumOffset+1 > v.leftCol+v.width {
v.leftCol = cx - v.width + v.lineNumOffset + 1
ret = true
}
return ret
}
// MoveToMouseClick moves the cursor to location x, y assuming x, y were given
// by a mouse click
func (v *View) MoveToMouseClick(x, y int) {
if y-v.topline > v.height-1 {
v.ScrollDown(1)
y = v.height + v.topline - 1
}
if y >= len(v.buf.lines) {
y = len(v.buf.lines) - 1
}
if x < 0 {
x = 0
}
x = v.cursor.GetCharPosInLine(y, x)
if x > Count(v.buf.lines[y]) {
x = Count(v.buf.lines[y])
}
v.cursor.x = x
v.cursor.y = y
v.cursor.lastVisualX = v.cursor.GetVisualX()
}
// HandleEvent handles an event passed by the main loop
func (v *View) HandleEvent(event tcell.Event) {
// This bool determines whether the view is relocated at the end of the function
// By default it's true because most events should cause a relocate
relocate := true
// By default we don't update and syntax highlighting
v.UpdateLines(-2, 0)
switch e := event.(type) {
case *tcell.EventResize:
// Window resized
v.Resize(e.Size())
case *tcell.EventKey:
switch e.Key() {
case tcell.KeyUp:
// Cursor up
v.cursor.ResetSelection()
v.cursor.Up()
case tcell.KeyDown:
// Cursor down
v.cursor.ResetSelection()
v.cursor.Down()
case tcell.KeyLeft:
// Cursor left
v.cursor.ResetSelection()
v.cursor.Left()
case tcell.KeyRight:
// Cursor right
v.cursor.ResetSelection()
v.cursor.Right()
case tcell.KeyEnter:
// Insert a newline
if v.cursor.HasSelection() {
v.cursor.DeleteSelection()
v.cursor.ResetSelection()
}
v.eh.Insert(v.cursor.Loc(), "\n")
v.cursor.Right()
// Rehighlight the entire buffer
v.UpdateLines(v.topline, v.topline+v.height)
v.cursor.lastVisualX = v.cursor.GetVisualX()
// v.UpdateLines(v.cursor.y-1, v.cursor.y)
case tcell.KeySpace:
// Insert a space
if v.cursor.HasSelection() {
v.cursor.DeleteSelection()
v.cursor.ResetSelection()
}
v.eh.Insert(v.cursor.Loc(), " ")
v.cursor.Right()
v.UpdateLines(v.cursor.y, v.cursor.y)
case tcell.KeyBackspace2:
// Delete a character
if v.cursor.HasSelection() {
v.cursor.DeleteSelection()
v.cursor.ResetSelection()
// Rehighlight the entire buffer
v.UpdateLines(v.topline, v.topline+v.height)
} else if v.cursor.Loc() > 0 {
// We have to do something a bit hacky here because we want to
// delete the line by first moving left and then deleting backwards
// but the undo redo would place the cursor in the wrong place
// So instead we move left, save the position, move back, delete
// and restore the position
v.cursor.Left()
cx, cy := v.cursor.x, v.cursor.y
v.cursor.Right()
loc := v.cursor.Loc()
v.eh.Remove(loc-1, loc)
v.cursor.x, v.cursor.y = cx, cy
// Rehighlight the entire buffer
v.UpdateLines(v.topline, v.topline+v.height)
// v.UpdateLines(v.cursor.y, v.cursor.y+1)
}
v.cursor.lastVisualX = v.cursor.GetVisualX()
case tcell.KeyTab:
// Insert a tab
if v.cursor.HasSelection() {
v.cursor.DeleteSelection()
v.cursor.ResetSelection()
}
v.eh.Insert(v.cursor.Loc(), "\t")
v.cursor.Right()
v.UpdateLines(v.cursor.y, v.cursor.y)
case tcell.KeyCtrlS:
v.Save()
case tcell.KeyCtrlF:
if v.cursor.HasSelection() {
searchStart = v.cursor.curSelection[1]
} else {
searchStart = ToCharPos(v.cursor.x, v.cursor.y, v.buf)
}
BeginSearch()
case tcell.KeyCtrlN:
if v.cursor.HasSelection() {
searchStart = v.cursor.curSelection[1]
} else {
searchStart = ToCharPos(v.cursor.x, v.cursor.y, v.buf)
}
messenger.Message("Find: " + lastSearch)
Search(lastSearch, v, true)
case tcell.KeyCtrlP:
if v.cursor.HasSelection() {
searchStart = v.cursor.curSelection[0]
} else {
searchStart = ToCharPos(v.cursor.x, v.cursor.y, v.buf)
}
messenger.Message("Find: " + lastSearch)
Search(lastSearch, v, false)
case tcell.KeyCtrlZ:
v.eh.Undo()
// Rehighlight the entire buffer
v.UpdateLines(v.topline, v.topline+v.height)
case tcell.KeyCtrlY:
v.eh.Redo()
// Rehighlight the entire buffer
v.UpdateLines(v.topline, v.topline+v.height)
case tcell.KeyCtrlC:
v.Copy()
// Rehighlight the entire buffer
v.UpdateLines(v.topline, v.topline+v.height)
case tcell.KeyCtrlX:
v.Cut()
// Rehighlight the entire buffer
v.UpdateLines(v.topline, v.topline+v.height)
case tcell.KeyCtrlV:
v.Paste()
// Rehighlight the entire buffer
v.UpdateLines(v.topline, v.topline+v.height)
case tcell.KeyCtrlA:
v.SelectAll()
case tcell.KeyCtrlO:
v.OpenFile()
// Rehighlight the entire buffer
v.UpdateLines(v.topline, v.topline+v.height)
case tcell.KeyPgUp:
v.PageUp()
relocate = false
case tcell.KeyPgDn:
v.PageDown()
relocate = false
case tcell.KeyCtrlU:
v.HalfPageUp()
relocate = false
case tcell.KeyCtrlD:
v.HalfPageDown()
relocate = false
case tcell.KeyRune:
// Insert a character
if v.cursor.HasSelection() {
v.cursor.DeleteSelection()
v.cursor.ResetSelection()
// Rehighlight the entire buffer
v.UpdateLines(v.topline, v.topline+v.height)
} else {
v.UpdateLines(v.cursor.y, v.cursor.y)
}
v.eh.Insert(v.cursor.Loc(), string(e.Rune()))
v.cursor.Right()
}
case *tcell.EventMouse:
x, y := e.Position()
x -= v.lineNumOffset - v.leftCol
y += v.topline
// Position always seems to be off by one
x--
y--
button := e.Buttons()
switch button {
case tcell.Button1:
// Left click
origX, origY := v.cursor.x, v.cursor.y
v.MoveToMouseClick(x, y)
if v.mouseReleased {
if (time.Since(v.lastClickTime)/time.Millisecond < doubleClickThreshold) &&
(origX == v.cursor.x && origY == v.cursor.y) {
if v.doubleClick {
// Triple click
v.lastClickTime = time.Now()
v.tripleClick = true
v.doubleClick = false
v.cursor.SelectLine()
} else {
// Double click
v.lastClickTime = time.Now()
v.doubleClick = true
v.tripleClick = false
v.cursor.SelectWord()
}
} else {
v.doubleClick = false
v.tripleClick = false
v.lastClickTime = time.Now()
loc := v.cursor.Loc()
v.cursor.curSelection[0] = loc
v.cursor.curSelection[1] = loc
}
} else {
if v.tripleClick {
v.cursor.AddLineToSelection()
} else if v.doubleClick {
v.cursor.AddWordToSelection()
} else {
v.cursor.curSelection[1] = v.cursor.Loc()
}
}
v.mouseReleased = false
case tcell.ButtonNone:
// Mouse event with no click
if !v.mouseReleased {
// Mouse was just released
// Relocating here isn't really necessary because the cursor will
// be in the right place from the last mouse event
// However, if we are running in a terminal that doesn't support mouse motion
// events, this still allows the user to make selections, except only after they
// release the mouse
if !v.doubleClick && !v.tripleClick {
v.MoveToMouseClick(x, y)
v.cursor.curSelection[1] = v.cursor.Loc()
}
v.mouseReleased = true
}
// We don't want to relocate because otherwise the view will be relocated
// every time the user moves the cursor
relocate = false
case tcell.WheelUp:
// Scroll up two lines
v.ScrollUp(2)
// We don't want to relocate if the user is scrolling
relocate = false
// Rehighlight the entire buffer
v.UpdateLines(v.topline, v.topline+v.height)
case tcell.WheelDown:
// Scroll down two lines
v.ScrollDown(2)
// We don't want to relocate if the user is scrolling
relocate = false
// Rehighlight the entire buffer
v.UpdateLines(v.topline, v.topline+v.height)
}
}
if relocate {
v.Relocate()
}
if settings.Syntax {
v.matches = Match(v)
}
}
// DisplayView renders the view to the screen
func (v *View) DisplayView() {
// matches := make(SyntaxMatches, len(v.buf.lines))
//
// viewStart := v.topline
// viewEnd := v.topline + v.height
// if viewEnd > len(v.buf.lines) {
// viewEnd = len(v.buf.lines)
// }
//
// lines := v.buf.lines[viewStart:viewEnd]
// for i, line := range lines {
// matches[i] = make([]tcell.Style, len(line))
// }
// The character number of the character in the top left of the screen
charNum := ToCharPos(0, v.topline, v.buf)
// Convert the length of buffer to a string, and get the length of the string
// We are going to have to offset by that amount
maxLineLength := len(strconv.Itoa(len(v.buf.lines)))
// + 1 for the little space after the line number
v.lineNumOffset = maxLineLength + 1
var highlightStyle tcell.Style
for lineN := 0; lineN < v.height; lineN++ {
var x int
// If the buffer is smaller than the view height
// and we went too far, break
if lineN+v.topline >= len(v.buf.lines) {
break
}
line := v.buf.lines[lineN+v.topline]
// Write the line number
lineNumStyle := defStyle
if style, ok := colorscheme["line-number"]; ok {
lineNumStyle = style
}
// Write the spaces before the line number if necessary
lineNum := strconv.Itoa(lineN + v.topline + 1)
for i := 0; i < maxLineLength-len(lineNum); i++ {
screen.SetContent(x, lineN, ' ', nil, lineNumStyle)
x++
}
// Write the actual line number
for _, ch := range lineNum {
screen.SetContent(x, lineN, ch, nil, lineNumStyle)
x++
}
// Write the extra space
screen.SetContent(x, lineN, ' ', nil, lineNumStyle)
x++
// Write the line
tabchars := 0
runes := []rune(line)
for colN := v.leftCol; colN < v.leftCol+v.width; colN++ {
if colN >= len(runes) {
break
}
ch := runes[colN]
var lineStyle tcell.Style
// Does the current character need to be syntax highlighted?
// if lineN >= v.updateLines[0] && lineN < v.updateLines[1] {
if settings.Syntax {
highlightStyle = v.matches[lineN][colN]
}
// } else if lineN < len(v.lastMatches) && colN < len(v.lastMatches[lineN]) {
// highlightStyle = v.lastMatches[lineN][colN]
// } else {
// highlightStyle = defStyle
// }
if v.cursor.HasSelection() &&
(charNum >= v.cursor.curSelection[0] && charNum < v.cursor.curSelection[1] ||
charNum < v.cursor.curSelection[0] && charNum >= v.cursor.curSelection[1]) {
lineStyle = defStyle.Reverse(true)
if style, ok := colorscheme["selection"]; ok {
lineStyle = style
}
} else {
lineStyle = highlightStyle
}
// matches[lineN][colN] = highlightStyle
if ch == '\t' {
screen.SetContent(x+tabchars, lineN, ' ', nil, lineStyle)
tabSize := settings.TabSize
for i := 0; i < tabSize-1; i++ {
tabchars++
if x-v.leftCol+tabchars >= v.lineNumOffset {
screen.SetContent(x-v.leftCol+tabchars, lineN, ' ', nil, lineStyle)
}
}
} else {
if x-v.leftCol+tabchars >= v.lineNumOffset {
screen.SetContent(x-v.leftCol+tabchars, lineN, ch, nil, lineStyle)
}
}
charNum++
x++
}
// Here we are at a newline
// The newline may be selected, in which case we should draw the selection style
// with a space to represent it
if v.cursor.HasSelection() &&
(charNum >= v.cursor.curSelection[0] && charNum < v.cursor.curSelection[1] ||
charNum < v.cursor.curSelection[0] && charNum >= v.cursor.curSelection[1]) {
selectStyle := defStyle.Reverse(true)
if style, ok := colorscheme["selection"]; ok {
selectStyle = style
}
screen.SetContent(x-v.leftCol+tabchars, lineN, ' ', nil, selectStyle)
}
charNum++
}
// v.lastMatches = matches
}
// Display renders the view, the cursor, and statusline
func (v *View) Display() {
v.DisplayView()
v.cursor.Display()
v.sline.Display()
}