From 339cdad594fc2dcd0dd673a132cee4da172ef433 Mon Sep 17 00:00:00 2001 From: Zachary Yedidia Date: Fri, 25 Mar 2016 12:14:22 -0400 Subject: [PATCH] Major cleanup --- src/colorscheme.go | 46 +++- src/cursor.go | 8 +- src/eventhandler.go | 2 + src/highlighter.go | 15 +- src/{message.go => messenger.go} | 63 +++-- src/micro.go | 134 +++++---- src/rope.go | 23 +- src/stack.go | 8 - src/statusline.go | 52 ++-- src/util.go | 24 +- src/util_test.go | 35 +++ src/view.go | 448 +++++++++++++++++-------------- 12 files changed, 509 insertions(+), 349 deletions(-) rename src/{message.go => messenger.go} (67%) create mode 100644 src/util_test.go diff --git a/src/colorscheme.go b/src/colorscheme.go index 8690ed82..3f31076c 100644 --- a/src/colorscheme.go +++ b/src/colorscheme.go @@ -6,6 +6,7 @@ import ( "io/ioutil" "os/user" "regexp" + "strconv" "strings" ) @@ -45,6 +46,9 @@ func LoadColorscheme(colorschemeName, dir string) { } // 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+"(.*)"`) @@ -74,6 +78,8 @@ func ParseColorscheme(text string) Colorscheme { } // 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 var bg string @@ -83,6 +89,8 @@ func StringToStyle(str string) tcell.Style { } 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") { @@ -98,6 +106,7 @@ func StringToStyle(str string) tcell.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": @@ -116,25 +125,46 @@ func StringToColor(str string) tcell.Color { return tcell.ColorTeal case "white": return tcell.ColorSilver - case "brightblack": + case "brightblack", "lightblack": return tcell.ColorGray - case "brightred": + case "brightred", "lightred": return tcell.ColorRed - case "brightgreen": + case "brightgreen", "lightgreen": return tcell.ColorLime - case "brightyellow": + case "brightyellow", "lightyellow": return tcell.ColorYellow - case "brightblue": + case "brightblue", "lightblue": return tcell.ColorBlue - case "brightmagenta": + case "brightmagenta", "lightmagenta": return tcell.ColorFuchsia - case "brightcyan": + case "brightcyan", "lightcyan": return tcell.ColorAqua - case "brightwhite": + 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 { + ansiColors := []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} + + if color >= 0 && color <= 15 { + return ansiColors[color] + } + + return tcell.GetColor("Color" + strconv.Itoa(color)) +} diff --git a/src/cursor.go b/src/cursor.go index 8ca60f57..089ac1c7 100644 --- a/src/cursor.go +++ b/src/cursor.go @@ -149,7 +149,7 @@ func (c *Cursor) Start() { // 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 { - visualLine := strings.Replace(c.v.buf.lines[lineNum], "\t", "\t"+EmptyString(tabSize-1), -1) + visualLine := strings.Replace(c.v.buf.lines[lineNum], "\t", "\t"+Spaces(tabSize-1), -1) if visualPos > Count(visualLine) { visualPos = Count(visualLine) } @@ -213,10 +213,8 @@ func (c *Cursor) Distance(x, y int) int { // Display draws the cursor to the screen at the correct position func (c *Cursor) Display() { if c.y-c.v.topline < 0 || c.y-c.v.topline > c.v.height-1 { - c.v.s.HideCursor() + screen.HideCursor() } else { - c.v.s.ShowCursor(c.GetVisualX()+c.v.lineNumOffset, c.y-c.v.topline) - // cursorStyle := tcell.StyleDefault.Reverse(true) - // c.v.s.SetContent(c.x+voffset, c.y-c.v.topline, c.runeUnder(), nil, cursorStyle) + screen.ShowCursor(c.GetVisualX()+c.v.lineNumOffset, c.y-c.v.topline) } } diff --git a/src/eventhandler.go b/src/eventhandler.go index 825542d3..4616e2b7 100644 --- a/src/eventhandler.go +++ b/src/eventhandler.go @@ -5,6 +5,8 @@ import ( ) const ( + // Opposite and undoing events must have opposite values + // TextEventInsert repreasents an insertion event TextEventInsert = 1 // TextEventRemove represents a deletion event diff --git a/src/highlighter.go b/src/highlighter.go index f7112750..8ef9ad4a 100644 --- a/src/highlighter.go +++ b/src/highlighter.go @@ -10,13 +10,17 @@ import ( "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 + // How to highlight it style tcell.Style } @@ -154,11 +158,12 @@ func GetRules(buf *Buffer) ([]SyntaxRule, string) { return nil, "Unknown" } -// Match takes a buffer and returns a map specifying how it should be syntax highlighted -// The map is from character numbers to styles, so map[3] represents the style change -// at the third character in the buffer -// Note that this map only stores changes in styles, not each character's style -func Match(rules []SyntaxRule, buf *Buffer, v *View) map[int]tcell.Style { +// SyntaxMatches is an alias to a map from character numbers to styles, +// so map[3] represents the style of the third character +type SyntaxMatches map[int]tcell.Style + +// Match takes a buffer and returns the syntax matches a map specifying how it should be syntax highlighted +func Match(rules []SyntaxRule, buf *Buffer, v *View) SyntaxMatches { start := v.topline - synLinesUp end := v.topline + v.height + synLinesDown if start < 0 { diff --git a/src/message.go b/src/messenger.go similarity index 67% rename from src/message.go rename to src/messenger.go index 93122971..ca8eba2e 100644 --- a/src/message.go +++ b/src/messenger.go @@ -1,28 +1,43 @@ package main import ( + "bufio" + "fmt" "github.com/gdamore/tcell" + "os" ) -// Messenger is an object that can send messages to the user and get input from the user (with a prompt) -type Messenger struct { - hasPrompt bool - hasMessage bool +// 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") - message string - response string - style tcell.Style - - cursorx int - - s tcell.Screen + reader := bufio.NewReader(os.Stdin) + reader.ReadString('\n') } -// NewMessenger returns a new Messenger struct -func NewMessenger(s tcell.Screen) *Messenger { - m := new(Messenger) - m.s = s - return m +// 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 @@ -61,7 +76,7 @@ func (m *Messenger) Prompt(prompt string) (string, bool) { m.Clear() m.Display() - event := m.s.PollEvent() + event := screen.PollEvent() switch e := event.(type) { case *tcell.EventKey: @@ -124,23 +139,23 @@ func (m *Messenger) Reset() { // Clear clears the line at the bottom of the editor func (m *Messenger) Clear() { - w, h := m.s.Size() + w, h := screen.Size() for x := 0; x < w; x++ { - m.s.SetContent(x, h-1, ' ', nil, tcell.StyleDefault) + screen.SetContent(x, h-1, ' ', nil, tcell.StyleDefault) } } -// Display displays and messages or prompts +// Display displays messages or prompts func (m *Messenger) Display() { - _, h := m.s.Size() + _, h := screen.Size() if m.hasMessage { runes := []rune(m.message + m.response) for x := 0; x < len(runes); x++ { - m.s.SetContent(x, h-1, runes[x], nil, m.style) + screen.SetContent(x, h-1, runes[x], nil, m.style) } } if m.hasPrompt { - m.s.ShowCursor(Count(m.message)+m.cursorx, h-1) - m.s.Show() + screen.ShowCursor(Count(m.message)+m.cursorx, h-1) + screen.Show() } } diff --git a/src/micro.go b/src/micro.go index 2a593396..7f460908 100644 --- a/src/micro.go +++ b/src/micro.go @@ -10,100 +10,136 @@ import ( ) const ( - tabSize = 4 - synLinesUp = 75 - synLinesDown = 75 + tabSize = 4 // This should be configurable + synLinesUp = 75 // How many lines up to look to do syntax highlighting + synLinesDown = 75 // How many lines down to look to do syntax highlighting ) -func main() { - var input []byte +// The main screen +var screen tcell.Screen + +// 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 _, err := os.Stat(filename); err == nil { - var err error input, err = ioutil.ReadFile(filename) - - if err != nil { - fmt.Println(err) - os.Exit(1) - } } } else if !isatty.IsTerminal(os.Stdin.Fd()) { - bytes, err := ioutil.ReadAll(os.Stdin) - if err != nil { - fmt.Println("Error reading stdin") - os.Exit(1) - } - input = bytes + // 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) } - LoadSyntaxFiles() + // 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) + } + + // 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") } - s, e := tcell.NewTerminfoScreen() - if e != nil { - fmt.Fprintf(os.Stderr, "%v\n", e) + // Initilize tcell + screen, err = tcell.NewScreen() + if err != nil { + fmt.Println(err) os.Exit(1) } - if e := s.Init(); e != nil { - fmt.Fprintf(os.Stderr, "%v\n", e) + 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 { - s.Fini() + 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. - Background(tcell.ColorDefault). - Foreground(tcell.ColorDefault) + Foreground(tcell.ColorDefault). + Background(tcell.ColorDefault) - if _, ok := colorscheme["default"]; ok { - defStyle = colorscheme["default"] + // There may be another default style defined in the colorscheme + if style, ok := colorscheme["default"]; ok { + defStyle = style } - s.SetStyle(defStyle) - s.EnableMouse() + screen.SetStyle(defStyle) + screen.EnableMouse() - m := NewMessenger(s) - v := NewView(NewBuffer(string(input), filename), m, s) + messenger := new(Messenger) + view := NewView(NewBuffer(string(input), filename), messenger) - // Initially everything needs to be drawn - redraw := 2 for { - if redraw == 2 { - v.matches = Match(v.buf.rules, v.buf, v) - s.Clear() - v.Display() - v.cursor.Display() - v.sl.Display() - m.Display() - s.Show() - } else if redraw == 1 { - v.cursor.Display() - v.sl.Display() - m.Display() - s.Show() + // Display everything + screen.Clear() + + view.Display() + messenger.Display() + + screen.Show() + + // Wait for the user's action + event := screen.PollEvent() + + // Check if we should quit + switch e := event.(type) { + case *tcell.EventKey: + if e.Key() == tcell.KeyCtrlQ { + // Make sure not to quit if there are unsaved changes + if view.CanClose("Quit anyway? ") { + screen.Fini() + os.Exit(0) + } + } } - event := s.PollEvent() - redraw = v.HandleEvent(event) + // Send it to the view + view.HandleEvent(event) } } diff --git a/src/rope.go b/src/rope.go index ad653983..d1cecd48 100644 --- a/src/rope.go +++ b/src/rope.go @@ -2,7 +2,6 @@ package main import ( "math" - "unicode/utf8" ) const ( @@ -13,22 +12,6 @@ const ( // RopeRebalanceRatio = 1.2 ) -// 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 -} - // A Rope is a data structure for efficiently manipulating large strings type Rope struct { left *Rope @@ -44,7 +27,7 @@ func NewRope(str string) *Rope { r := new(Rope) r.value = str r.valueNil = false - r.len = utf8.RuneCountInString(r.value) + r.len = Count(r.value) r.Adjust() @@ -83,7 +66,7 @@ func (r *Rope) Remove(start, end int) { if !r.valueNil { r.value = string(append([]rune(r.value)[:start], []rune(r.value)[end:]...)) r.valueNil = false - r.len = utf8.RuneCountInString(r.value) + r.len = Count(r.value) } else { leftStart := Min(start, r.left.len) leftEnd := Min(end, r.left.len) @@ -107,7 +90,7 @@ func (r *Rope) Insert(pos int, value string) { first := append([]rune(r.value)[:pos], []rune(value)...) r.value = string(append(first, []rune(r.value)[pos:]...)) r.valueNil = false - r.len = utf8.RuneCountInString(r.value) + r.len = Count(r.value) } else { if pos < r.left.len { r.left.Insert(pos, value) diff --git a/src/stack.go b/src/stack.go index 7b1dd668..b3200e1a 100644 --- a/src/stack.go +++ b/src/stack.go @@ -33,11 +33,3 @@ func (s *Stack) Pop() (value interface{}) { } return nil } - -// Peek lets you see and edit the top value of the stack without popping it -func (s *Stack) Peek() *interface{} { - if s.size > 0 { - return &s.top.value - } - return nil -} diff --git a/src/statusline.go b/src/statusline.go index 336090fe..0374b4f9 100644 --- a/src/statusline.go +++ b/src/statusline.go @@ -5,39 +5,51 @@ import ( "strconv" ) -// Statusline represents the blue line at the bottom of the -// editor that gives information about the buffer +// 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 { - v *View + view *View } // Display draws the statusline to the screen -func (sl *Statusline) Display() { - y := sl.v.height +func (sline *Statusline) Display() { + // We'll draw the line at the lowest line in the view + y := sline.view.height - file := sl.v.buf.name + file := sline.view.buf.name + // If the name is empty, use 'No name' if file == "" { - file = "Untitled" + file = "No name" } - if sl.v.buf.IsDirty() { + + // If the buffer is dirty (has been modified) write a little '+' + if sline.view.buf.IsDirty() { file += " +" } - file += " (" + strconv.Itoa(sl.v.cursor.y+1) + "," + strconv.Itoa(sl.v.cursor.GetVisualX()+1) + ")" - filetype := sl.v.buf.filetype - file += " " + filetype + + // 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 statusLineStyle := tcell.StyleDefault.Reverse(true) - if _, ok := colorscheme["statusline"]; ok { - statusLineStyle = colorscheme["statusline"] - } - for x := 0; x < sl.v.width; x++ { - if x < Count(file) { - sl.v.s.SetContent(x, y, []rune(file)[x], nil, statusLineStyle) - // } else if x > sl.v.width-Count(filetype)-1 { - // sl.v.s.SetContent(x, y, []rune(filetype)[Count(filetype)-(sl.v.width-1-x)-1], nil, statusLineStyle) + // 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 { - sl.v.s.SetContent(x, y, ' ', nil, statusLineStyle) + screen.SetContent(x, y, ' ', nil, statusLineStyle) } } } diff --git a/src/util.go b/src/util.go index aaea4f25..bb27e4ac 100644 --- a/src/util.go +++ b/src/util.go @@ -4,7 +4,11 @@ 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) } @@ -20,11 +24,27 @@ func NumOccurences(s string, c byte) int { return n } -// EmptyString returns an empty string n spaces long -func EmptyString(n int) string { +// 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 +} diff --git a/src/util_test.go b/src/util_test.go new file mode 100644 index 00000000..e1955aa3 --- /dev/null +++ b/src/util_test.go @@ -0,0 +1,35 @@ +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) + } + } +} diff --git a/src/view.go b/src/view.go index d191764a..a5c88ece 100644 --- a/src/view.go +++ b/src/view.go @@ -4,7 +4,6 @@ import ( "github.com/atotto/clipboard" "github.com/gdamore/tcell" "io/ioutil" - "os" "strconv" "strings" ) @@ -13,53 +12,65 @@ import ( // It has a value for the cursor, and the window that the user sees // the buffer from. type View struct { - cursor Cursor + cursor Cursor + + // The topmost line, used for vertical scrolling topline int - // Leftmost column. Used for horizontal scrolling + // The leftmost column, used for horizontal scrolling leftCol int - // Percentage of the terminal window that this view takes up - heightPercent float32 - widthPercent float32 - height int - width 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 - sl Statusline + // 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 - // Syntax highlighting matches - matches map[int]tcell.Style + // Syntax higlighting matches + matches SyntaxMatches + // The messenger so we can send messages to the user and get input from them m *Messenger - - s tcell.Screen } -// NewView returns a new view with fullscreen width and height -func NewView(buf *Buffer, m *Messenger, s tcell.Screen) *View { - return NewViewWidthHeight(buf, m, s, 1, 1) +// NewView returns a new fullscreen view +func NewView(buf *Buffer, m *Messenger) *View { + return NewViewWidthHeight(buf, m, 100, 100) } // NewViewWidthHeight returns a new view with the specified width and height percentages -func NewViewWidthHeight(buf *Buffer, m *Messenger, s tcell.Screen, w, h float32) *View { +// Note that w and h are percentages not actual values +func NewViewWidthHeight(buf *Buffer, m *Messenger, w, h int) *View { v := new(View) v.buf = buf - v.s = s + // Messenger v.m = m v.widthPercent = w v.heightPercent = h - v.Resize(s.Size()) + v.Resize(screen.Size()) v.topline = 0 + // Put the cursor at the first spot v.cursor = Cursor{ x: 0, y: 0, @@ -69,18 +80,23 @@ func NewViewWidthHeight(buf *Buffer, m *Messenger, s tcell.Screen, w, h float32) v.eh = NewEventHandler(v) - v.sl = Statusline{ - v: v, + v.sline = Statusline{ + view: v, } return v } -// Resize recalculates the width and height of the view based on the width and height percentages +// 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.height = int(float32(h)*v.heightPercent) - 1 - v.width = int(float32(w) * v.widthPercent) + 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) @@ -139,70 +155,185 @@ func (v *View) HalfPageDown() { } } +// 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 := v.m.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 := v.m.Prompt("Filename: ") + if !canceled { + v.buf.path = filename + v.buf.name = filename + } else { + return + } + } + err := v.buf.Save() + if err != nil { + v.m.Error(err.Error()) + } +} + +// Copy the selection to the system clipboard +func (v *View) Copy() { + if v.cursor.HasSelection() { + if !clipboard.Unsupported { + clipboard.WriteAll(v.cursor.GetSelection()) + } else { + v.m.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 { + v.m.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) + // This is a bit weird... Not sure if there's a better way + for i := 0; i < Count(clip); i++ { + v.cursor.Right() + } + } else { + v.m.Error("Clipboard is not supported on your system") + } +} + +// SelectAll selects the entire buffer +func (v *View) SelectAll() { + v.cursor.selectionEnd = 0 + v.cursor.selectionStart = v.buf.Len() + // Put the cursor at the beginning + v.cursor.x = 0 + v.cursor.y = 0 + v.cursor.loc = 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 := v.m.Prompt("File to open: ") + if canceled { + return + } + file, err := ioutil.ReadFile(filename) + + if err != nil { + v.m.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() { + cy := v.cursor.y + if cy < v.topline { + v.topline = cy + } + if cy > v.topline+v.height-1 { + v.topline = cy - v.height + 1 + } +} + +// 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]) + } + d := v.cursor.Distance(x, y) + v.cursor.loc += d + v.cursor.x = x + v.cursor.y = y +} + // HandleEvent handles an event passed by the main loop -// It returns an int describing how the screen needs to be redrawn -// 0: Screen does not need to be redrawn -// 1: Only the cursor/statusline needs to be redrawn -// 2: Everything needs to be redrawn -func (v *View) HandleEvent(event tcell.Event) int { - var ret int +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 switch e := event.(type) { case *tcell.EventResize: // Window resized v.Resize(e.Size()) - ret = 2 case *tcell.EventKey: switch e.Key() { - case tcell.KeyCtrlQ: - // Quit - if v.buf.IsDirty() { - quit, canceled := v.m.Prompt("You have unsaved changes. Quit anyway? ") - if !canceled { - if strings.ToLower(quit) == "yes" || strings.ToLower(quit) == "y" { - v.s.Fini() - os.Exit(0) - } else { - return 2 - } - } else { - return 2 - } - } else { - v.s.Fini() - os.Exit(0) - } case tcell.KeyUp: // Cursor up v.cursor.Up() - ret = 1 case tcell.KeyDown: // Cursor down v.cursor.Down() - ret = 1 case tcell.KeyLeft: // Cursor left v.cursor.Left() - ret = 1 case tcell.KeyRight: // Cursor right v.cursor.Right() - ret = 1 case tcell.KeyEnter: // Insert a newline v.eh.Insert(v.cursor.loc, "\n") v.cursor.Right() - ret = 2 case tcell.KeySpace: // Insert a space v.eh.Insert(v.cursor.loc, " ") v.cursor.Right() - ret = 2 case tcell.KeyBackspace2: // Delete a character if v.cursor.HasSelection() { v.cursor.DeleteSelection() v.cursor.ResetSelection() - ret = 2 } 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 @@ -214,111 +345,35 @@ func (v *View) HandleEvent(event tcell.Event) int { v.cursor.Right() v.eh.Remove(v.cursor.loc-1, v.cursor.loc) v.cursor.x, v.cursor.y, v.cursor.loc = cx, cy, cloc - ret = 2 } case tcell.KeyTab: // Insert a tab v.eh.Insert(v.cursor.loc, "\t") v.cursor.Right() - ret = 2 case tcell.KeyCtrlS: - // Save - if v.buf.path == "" { - filename, canceled := v.m.Prompt("Filename: ") - if !canceled { - v.buf.path = filename - v.buf.name = filename - } else { - return 2 - } - } - err := v.buf.Save() - if err != nil { - v.m.Error(err.Error()) - } - // Need to redraw the status line - ret = 1 + v.Save() case tcell.KeyCtrlZ: - // Undo v.eh.Undo() - ret = 2 case tcell.KeyCtrlY: - // Redo v.eh.Redo() - ret = 2 case tcell.KeyCtrlC: - // Copy - if v.cursor.HasSelection() { - if !clipboard.Unsupported { - clipboard.WriteAll(v.cursor.GetSelection()) - ret = 2 - } - } + v.Copy() case tcell.KeyCtrlX: - // Cut - if v.cursor.HasSelection() { - if !clipboard.Unsupported { - clipboard.WriteAll(v.cursor.GetSelection()) - v.cursor.DeleteSelection() - v.cursor.ResetSelection() - ret = 2 - } - } + v.Cut() case tcell.KeyCtrlV: - // Paste - if !clipboard.Unsupported { - if v.cursor.HasSelection() { - v.cursor.DeleteSelection() - v.cursor.ResetSelection() - } - clip, _ := clipboard.ReadAll() - v.eh.Insert(v.cursor.loc, clip) - // This is a bit weird... Not sure if there's a better way - for i := 0; i < Count(clip); i++ { - v.cursor.Right() - } - ret = 2 - } + v.Paste() case tcell.KeyCtrlA: - // Select all - v.cursor.selectionEnd = 0 - v.cursor.selectionStart = v.buf.Len() - v.cursor.x = 0 - v.cursor.y = 0 - v.cursor.loc = 0 - ret = 2 + v.SelectAll() case tcell.KeyCtrlO: - // Open file - if v.buf.IsDirty() { - quit, canceled := v.m.Prompt("You have unsaved changes. Continue? ") - if !canceled { - if strings.ToLower(quit) == "yes" || strings.ToLower(quit) == "y" { - return v.OpenFile() - } else { - return 2 - } - } else { - return 2 - } - } else { - return v.OpenFile() - } + v.OpenFile() case tcell.KeyPgUp: - // Page up v.PageUp() - return 2 case tcell.KeyPgDn: - // Page down v.PageDown() - return 2 case tcell.KeyCtrlU: - // Half page up v.HalfPageUp() - return 2 case tcell.KeyCtrlD: - // Half page down v.HalfPageDown() - return 2 case tcell.KeyRune: // Insert a character if v.cursor.HasSelection() { @@ -327,7 +382,6 @@ func (v *View) HandleEvent(event tcell.Event) int { } v.eh.Insert(v.cursor.loc, string(e.Rune())) v.cursor.Right() - ret = 2 } case *tcell.EventMouse: x, y := e.Position() @@ -342,25 +396,7 @@ func (v *View) HandleEvent(event tcell.Event) int { switch button { case tcell.Button1: // Left click - 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]) - } - d := v.cursor.Distance(x, y) - v.cursor.loc += d - v.cursor.x = x - v.cursor.y = y + v.MoveToMouseClick(x, y) if v.mouseReleased { v.cursor.selectionStart = v.cursor.loc @@ -369,59 +405,44 @@ func (v *View) HandleEvent(event tcell.Event) int { } v.cursor.selectionEnd = v.cursor.loc v.mouseReleased = false - return 2 case tcell.ButtonNone: // Mouse event with no click - v.mouseReleased = true - // We need to directly return here because otherwise the view will - // be readjusted to put the cursor in it, but there may be mouse events - // where the cursor is not (and should not be) be involved - return 0 + 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 + v.MoveToMouseClick(x, y) + v.cursor.selectionEnd = v.cursor.loc + v.mouseReleased = true + } + // We don't want to relocate because otherwise the view will be relocated + // everytime the user moves the cursor + relocate = false case tcell.WheelUp: // Scroll up two lines v.ScrollUp(2) - return 2 + // We don't want to relocate if the user is scrolling + relocate = false case tcell.WheelDown: // Scroll down two lines v.ScrollDown(2) - return 2 + // We don't want to relocate if the user is scrolling + relocate = false } } - // Reset the view so the cursor is in view - cy := v.cursor.y - if cy < v.topline { - v.topline = cy - ret = 2 + if relocate { + v.Relocate() } - if cy > v.topline+v.height-1 { - v.topline = cy - v.height + 1 - ret = 2 - } - - return ret } -// OpenFile Prompts the user for a filename and opens the file in the current buffer -func (v *View) OpenFile() int { - filename, canceled := v.m.Prompt("File to open: ") - if canceled { - return 2 - } - file, err := ioutil.ReadFile(filename) - - if err != nil { - v.m.Error(err.Error()) - return 2 - } - v.buf = NewBuffer(string(file), filename) - return 2 -} - -// Display renders the view to the screen -func (v *View) Display() { - var x int - +// DisplayView renders the view to the screen +func (v *View) DisplayView() { + // The character number of the character in the top left of the screen charNum := v.cursor.loc + v.cursor.Distance(0, v.topline) // Convert the length of buffer to a string, and get the length of the string @@ -433,6 +454,9 @@ func (v *View) Display() { 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 } @@ -446,22 +470,23 @@ func (v *View) Display() { // Write the spaces before the line number if necessary lineNum := strconv.Itoa(lineN + v.topline + 1) for i := 0; i < maxLineLength-len(lineNum); i++ { - v.s.SetContent(x, lineN, ' ', nil, lineNumStyle) + screen.SetContent(x, lineN, ' ', nil, lineNumStyle) x++ } // Write the actual line number for _, ch := range lineNum { - v.s.SetContent(x, lineN, ch, nil, lineNumStyle) + screen.SetContent(x, lineN, ch, nil, lineNumStyle) x++ } // Write the extra space - v.s.SetContent(x, lineN, ' ', nil, lineNumStyle) + screen.SetContent(x, lineN, ' ', nil, lineNumStyle) x++ // Write the line tabchars := 0 for _, ch := range line { var lineStyle tcell.Style + // Does the current character need to be syntax highlighted? st, ok := v.matches[charNum] if ok { highlightStyle = st @@ -483,17 +508,21 @@ func (v *View) Display() { } if ch == '\t' { - v.s.SetContent(x+tabchars, lineN, ' ', nil, lineStyle) + screen.SetContent(x+tabchars, lineN, ' ', nil, lineStyle) for i := 0; i < tabSize-1; i++ { tabchars++ - v.s.SetContent(x+tabchars, lineN, ' ', nil, lineStyle) + screen.SetContent(x+tabchars, lineN, ' ', nil, lineStyle) } } else { - v.s.SetContent(x+tabchars, lineN, ch, nil, lineStyle) + screen.SetContent(x+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.selectionStart && charNum <= v.cursor.selectionEnd || charNum <= v.cursor.selectionStart && charNum >= v.cursor.selectionEnd) { @@ -503,14 +532,17 @@ func (v *View) Display() { if _, ok := colorscheme["selection"]; ok { selectStyle = colorscheme["selection"] } - v.s.SetContent(x+tabchars, lineN, ' ', nil, selectStyle) + screen.SetContent(x+tabchars, lineN, ' ', nil, selectStyle) } x = 0 - st, ok := v.matches[charNum] - if ok { - highlightStyle = st - } charNum++ } } + +// Display renders the view, the cursor, and statusline +func (v *View) Display() { + v.DisplayView() + v.cursor.Display() + v.sline.Display() +}