import (
"github.com/atotto/clipboard"
- "github.com/zyedidia/tcell"
+ "github.com/gdamore/tcell"
+ "io/ioutil"
"strconv"
+ "strings"
)
// The View struct stores information about a view into a buffer.
// 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
+ 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
- s tcell.Screen
+ // The messenger so we can send messages to the user and get input from them
+ m *Messenger
}
-// NewView returns a new view with fullscreen width and height
-func NewView(buf *Buffer, s tcell.Screen) *View {
- return NewViewWidthHeight(buf, 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, 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,
v.eh = NewEventHandler(v)
- v.sl = Statusline{
- v: 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.buf.rules, v.buf, v)
+
+ // Set mouseReleased to true because we assume the mouse is not being pressed when
+ // the editor is opened
+ v.mouseReleased = true
+
return v
}
-// Resize recalculates the width and height of the view based on the width and height percentages
+// UpdateLines sets the values for v.updateLines
+func (v *View) UpdateLines(start, end int) {
+ v.updateLines[0] = start
+ v.updateLines[1] = end
+}
+
+// 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)
}
}
+// 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
+ // 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())
- ret = 2
case *tcell.EventKey:
switch e.Key() {
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
+ v.UpdateLines(v.cursor.y-1, v.cursor.y)
case tcell.KeySpace:
+ // Insert a space
v.eh.Insert(v.cursor.loc, " ")
v.cursor.Right()
- ret = 2
+ v.UpdateLines(v.cursor.y, v.cursor.y)
case tcell.KeyBackspace2:
+ // Delete a character
if v.cursor.HasSelection() {
v.cursor.DeleteSelection()
v.cursor.ResetSelection()
- ret = 2
+ // 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
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
+ v.UpdateLines(v.cursor.y, v.cursor.y+1)
}
case tcell.KeyTab:
+ // Insert a tab
v.eh.Insert(v.cursor.loc, "\t")
v.cursor.Right()
- ret = 2
+ v.UpdateLines(v.cursor.y, v.cursor.y)
case tcell.KeyCtrlS:
- err := v.buf.Save()
- if err != nil {
- Error(err.Error())
- }
- // Need to redraw the status line
- ret = 1
+ v.Save()
case tcell.KeyCtrlZ:
v.eh.Undo()
- ret = 2
+ // Rehighlight the entire buffer
+ v.UpdateLines(v.topline, v.topline+v.height)
case tcell.KeyCtrlY:
v.eh.Redo()
- ret = 2
+ // Rehighlight the entire buffer
+ v.UpdateLines(v.topline, v.topline+v.height)
case tcell.KeyCtrlC:
- if v.cursor.HasSelection() {
- if !clipboard.Unsupported {
- clipboard.WriteAll(v.cursor.GetSelection())
- ret = 2
- }
- }
+ v.Copy()
+ // Rehighlight the entire buffer
+ v.UpdateLines(v.topline, v.topline+v.height)
case tcell.KeyCtrlX:
- if v.cursor.HasSelection() {
- if !clipboard.Unsupported {
- clipboard.WriteAll(v.cursor.GetSelection())
- v.cursor.DeleteSelection()
- v.cursor.ResetSelection()
- ret = 2
- }
- }
+ v.Cut()
+ // Rehighlight the entire buffer
+ v.UpdateLines(v.topline, v.topline+v.height)
case tcell.KeyCtrlV:
- 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()
+ // 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()
- return 2
case tcell.KeyPgDn:
v.PageDown()
- return 2
case tcell.KeyCtrlU:
v.HalfPageUp()
- return 2
case tcell.KeyCtrlD:
v.HalfPageDown()
- return 2
case tcell.KeyRune:
+ // Insert a character
if v.cursor.HasSelection() {
v.cursor.DeleteSelection()
v.cursor.ResetSelection()
}
v.eh.Insert(v.cursor.loc, string(e.Rune()))
v.cursor.Right()
- ret = 2
+ v.UpdateLines(v.cursor.y, v.cursor.y)
}
case *tcell.EventMouse:
x, y := e.Position()
switch button {
case tcell.Button1:
- 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
+ // Left click
+ v.MoveToMouseClick(x, y)
if v.mouseReleased {
v.cursor.selectionStart = v.cursor.loc
}
v.cursor.selectionEnd = v.cursor.loc
v.mouseReleased = false
- return 2
case tcell.ButtonNone:
- v.mouseReleased = true
- return 0
+ // 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
+ 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
+ // every time 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
}
}
- cy := v.cursor.y
- if cy < v.topline {
- v.topline = cy
- ret = 2
- }
- if cy > v.topline+v.height-1 {
- v.topline = cy - v.height + 1
- ret = 2
+ if relocate {
+ v.Relocate()
}
- return ret
+ // v.matches = Match(v.buf.rules, v.buf, v)
}
-// 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() {
+ matches := make(SyntaxMatches)
+ // 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
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
}
// Write the line number
lineNumStyle := tcell.StyleDefault
- if _, ok := colorscheme["line-number"]; ok {
- lineNumStyle = colorscheme["line-number"]
+ 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++ {
- 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
- st, ok := v.matches[charNum]
- if ok {
+ // Does the current character need to be syntax highlighted?
+ if st, ok := v.matches[charNum]; ok {
+ highlightStyle = st
+ } else if st, ok := v.lastMatches[charNum]; ok {
highlightStyle = st
} else {
highlightStyle = tcell.StyleDefault
}
+ matches[charNum] = highlightStyle
if v.cursor.HasSelection() &&
(charNum >= v.cursor.selectionStart && charNum <= v.cursor.selectionEnd ||
charNum <= v.cursor.selectionStart && charNum >= v.cursor.selectionEnd) {
- lineStyle = tcell.StyleDefault
- lineStyle = lineStyle.Reverse(true)
+
+ lineStyle = tcell.StyleDefault.Reverse(true)
+
+ if style, ok := colorscheme["selection"]; ok {
+ lineStyle = style
+ }
} else {
lineStyle = highlightStyle
}
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++
}
- x = 0
- st, ok := v.matches[charNum]
- if ok {
- highlightStyle = st
+ // 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) {
+
+ selectStyle := tcell.StyleDefault.Reverse(true)
+
+ if style, ok := colorscheme["selection"]; ok {
+ selectStyle = style
+ }
+ screen.SetContent(x+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()
}