micro
+!cmd/micro
binaries/
build: syn-files
- go get -d ./src
- go build -o micro ./src
+ go get -d ./cmd/micro
+ go build -o micro ./cmd/micro
install: syn-files build
mv micro $(GOBIN)
cp -r runtime/* ~/.micro
test:
- go get -d ./src
- go test ./src
+ go get -d ./cmd/micro
+ go test ./cmd/micro
clean:
rm -f micro
--- /dev/null
+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()
+}
--- /dev/null
+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]
+}
--- /dev/null
+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)
+ }
+}
--- /dev/null
+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)
+ }
+}
--- /dev/null
+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)
+}
--- /dev/null
+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
+ }
+ }
+ }
+}
--- /dev/null
+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
+}
--- /dev/null
+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()
+ }
+}
--- /dev/null
+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)
+ }
+ }
+}
--- /dev/null
+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
+}
--- /dev/null
+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")
+ }
+}
--- /dev/null
+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
+}
--- /dev/null
+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")
+ }
+}
--- /dev/null
+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)
+ }
+ }
+}
--- /dev/null
+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:])
+}
--- /dev/null
+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")
+ }
+}
--- /dev/null
+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()
+}
+++ /dev/null
-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()
-}
+++ /dev/null
-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]
-}
+++ /dev/null
-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)
- }
-}
+++ /dev/null
-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)
- }
-}
+++ /dev/null
-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)
-}
+++ /dev/null
-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
- }
- }
- }
-}
+++ /dev/null
-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
-}
+++ /dev/null
-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()
- }
-}
+++ /dev/null
-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)
- }
- }
-}
+++ /dev/null
-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
-}
+++ /dev/null
-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")
- }
-}
+++ /dev/null
-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
-}
+++ /dev/null
-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")
- }
-}
+++ /dev/null
-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)
- }
- }
-}
+++ /dev/null
-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:])
-}
+++ /dev/null
-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")
- }
-}
+++ /dev/null
-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()
-}