mirror of
https://github.com/zyedidia/micro.git
synced 2026-03-24 17:50:15 +09:00
now is go gettable and updated make file
This commit is contained in:
117
cmd/micro/buffer.go
Normal file
117
cmd/micro/buffer.go
Normal 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
198
cmd/micro/colorscheme.go
Normal 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
97
cmd/micro/command.go
Normal 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
308
cmd/micro/cursor.go
Normal 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
196
cmd/micro/eventhandler.go
Normal 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
104
cmd/micro/help.go
Normal 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
337
cmd/micro/highlighter.go
Normal 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
192
cmd/micro/messenger.go
Normal 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
173
cmd/micro/micro.go
Normal 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
116
cmd/micro/search.go
Normal 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
123
cmd/micro/settings.go
Normal 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
43
cmd/micro/stack.go
Normal 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
39
cmd/micro/stack_test.go
Normal 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
61
cmd/micro/statusline.go
Normal 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
77
cmd/micro/util.go
Normal 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
65
cmd/micro/util_test.go
Normal 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
737
cmd/micro/view.go
Normal 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()
|
||||
}
|
||||
Reference in New Issue
Block a user