4 "github.com/atotto/clipboard"
5 "github.com/gdamore/tcell"
12 // The View struct stores information about a view into a buffer.
13 // It has a value for the cursor, and the window that the user sees
18 // Leftmost column. Used for horizontal scrolling
21 // Percentage of the terminal window that this view takes up
27 // How much to offset because of line numbers
37 // Syntax highlighting matches
38 matches map[int]tcell.Style
45 // NewView returns a new view with fullscreen width and height
46 func NewView(buf *Buffer, m *Messenger, s tcell.Screen) *View {
47 return NewViewWidthHeight(buf, m, s, 1, 1)
50 // NewViewWidthHeight returns a new view with the specified width and height percentages
51 func NewViewWidthHeight(buf *Buffer, m *Messenger, s tcell.Screen, w, h float32) *View {
70 v.eh = NewEventHandler(v)
79 // Resize recalculates the width and height of the view based on the width and height percentages
80 func (v *View) Resize(w, h int) {
82 v.height = int(float32(h)*v.heightPercent) - 1
83 v.width = int(float32(w) * v.widthPercent)
86 // ScrollUp scrolls the view up n lines (if possible)
87 func (v *View) ScrollUp(n int) {
88 // Try to scroll by n but if it would overflow, scroll by 1
91 } else if v.topline > 0 {
96 // ScrollDown scrolls the view down n lines (if possible)
97 func (v *View) ScrollDown(n int) {
98 // Try to scroll by n but if it would overflow, scroll by 1
99 if v.topline+n <= len(v.buf.lines)-v.height {
101 } else if v.topline < len(v.buf.lines)-v.height {
106 // PageUp scrolls the view up a page
107 func (v *View) PageUp() {
108 if v.topline > v.height {
115 // PageDown scrolls the view down a page
116 func (v *View) PageDown() {
117 if len(v.buf.lines)-(v.topline+v.height) > v.height {
118 v.ScrollDown(v.height)
120 v.topline = len(v.buf.lines) - v.height
124 // HalfPageUp scrolls the view up half a page
125 func (v *View) HalfPageUp() {
126 if v.topline > v.height/2 {
127 v.ScrollUp(v.height / 2)
133 // HalfPageDown scrolls the view down half a page
134 func (v *View) HalfPageDown() {
135 if len(v.buf.lines)-(v.topline+v.height) > v.height/2 {
136 v.ScrollDown(v.height / 2)
138 v.topline = len(v.buf.lines) - v.height
142 // HandleEvent handles an event passed by the main loop
143 // It returns an int describing how the screen needs to be redrawn
144 // 0: Screen does not need to be redrawn
145 // 1: Only the cursor/statusline needs to be redrawn
146 // 2: Everything needs to be redrawn
147 func (v *View) HandleEvent(event tcell.Event) int {
149 switch e := event.(type) {
150 case *tcell.EventResize:
154 case *tcell.EventKey:
159 quit, canceled := v.m.Prompt("You have unsaved changes. Quit anyway? ")
161 if strings.ToLower(quit) == "yes" || strings.ToLower(quit) == "y" {
192 v.eh.Insert(v.cursor.loc, "\n")
197 v.eh.Insert(v.cursor.loc, " ")
200 case tcell.KeyBackspace2:
201 // Delete a character
202 if v.cursor.HasSelection() {
203 v.cursor.DeleteSelection()
204 v.cursor.ResetSelection()
206 } else if v.cursor.loc > 0 {
207 // We have to do something a bit hacky here because we want to
208 // delete the line by first moving left and then deleting backwards
209 // but the undo redo would place the cursor in the wrong place
210 // So instead we move left, save the position, move back, delete
211 // and restore the position
213 cx, cy, cloc := v.cursor.x, v.cursor.y, v.cursor.loc
215 v.eh.Remove(v.cursor.loc-1, v.cursor.loc)
216 v.cursor.x, v.cursor.y, v.cursor.loc = cx, cy, cloc
221 v.eh.Insert(v.cursor.loc, "\t")
226 if v.buf.path == "" {
227 filename, canceled := v.m.Prompt("Filename: ")
229 v.buf.path = filename
230 v.buf.name = filename
237 v.m.Error(err.Error())
239 // Need to redraw the status line
251 if v.cursor.HasSelection() {
252 if !clipboard.Unsupported {
253 clipboard.WriteAll(v.cursor.GetSelection())
259 if v.cursor.HasSelection() {
260 if !clipboard.Unsupported {
261 clipboard.WriteAll(v.cursor.GetSelection())
262 v.cursor.DeleteSelection()
263 v.cursor.ResetSelection()
269 if !clipboard.Unsupported {
270 if v.cursor.HasSelection() {
271 v.cursor.DeleteSelection()
272 v.cursor.ResetSelection()
274 clip, _ := clipboard.ReadAll()
275 v.eh.Insert(v.cursor.loc, clip)
276 // This is a bit weird... Not sure if there's a better way
277 for i := 0; i < Count(clip); i++ {
284 v.cursor.selectionEnd = 0
285 v.cursor.selectionStart = v.buf.Len()
293 quit, canceled := v.m.Prompt("You have unsaved changes. Continue? ")
295 if strings.ToLower(quit) == "yes" || strings.ToLower(quit) == "y" {
323 // Insert a character
324 if v.cursor.HasSelection() {
325 v.cursor.DeleteSelection()
326 v.cursor.ResetSelection()
328 v.eh.Insert(v.cursor.loc, string(e.Rune()))
332 case *tcell.EventMouse:
336 // Position always seems to be off by one
340 button := e.Buttons()
345 if y-v.topline > v.height-1 {
347 y = v.height + v.topline - 1
349 if y >= len(v.buf.lines) {
350 y = len(v.buf.lines) - 1
356 x = v.cursor.GetCharPosInLine(y, x)
357 if x > Count(v.buf.lines[y]) {
358 x = Count(v.buf.lines[y])
360 d := v.cursor.Distance(x, y)
366 v.cursor.selectionStart = v.cursor.loc
367 v.cursor.selectionStartX = v.cursor.x
368 v.cursor.selectionStartY = v.cursor.y
370 v.cursor.selectionEnd = v.cursor.loc
371 v.mouseReleased = false
373 case tcell.ButtonNone:
374 // Mouse event with no click
375 v.mouseReleased = true
376 // We need to directly return here because otherwise the view will
377 // be readjusted to put the cursor in it, but there may be mouse events
378 // where the cursor is not (and should not be) be involved
381 // Scroll up two lines
384 case tcell.WheelDown:
385 // Scroll down two lines
391 // Reset the view so the cursor is in view
397 if cy > v.topline+v.height-1 {
398 v.topline = cy - v.height + 1
405 // OpenFile Prompts the user for a filename and opens the file in the current buffer
406 func (v *View) OpenFile() int {
407 filename, canceled := v.m.Prompt("File to open: ")
411 file, err := ioutil.ReadFile(filename)
414 v.m.Error(err.Error())
417 v.buf = NewBuffer(string(file), filename)
421 // Display renders the view to the screen
422 func (v *View) Display() {
425 charNum := v.cursor.loc + v.cursor.Distance(0, v.topline)
427 // Convert the length of buffer to a string, and get the length of the string
428 // We are going to have to offset by that amount
429 maxLineLength := len(strconv.Itoa(len(v.buf.lines)))
430 // + 1 for the little space after the line number
431 v.lineNumOffset = maxLineLength + 1
433 var highlightStyle tcell.Style
435 for lineN := 0; lineN < v.height; lineN++ {
436 if lineN+v.topline >= len(v.buf.lines) {
439 line := v.buf.lines[lineN+v.topline]
441 // Write the line number
442 lineNumStyle := tcell.StyleDefault
443 if _, ok := colorscheme["line-number"]; ok {
444 lineNumStyle = colorscheme["line-number"]
446 // Write the spaces before the line number if necessary
447 lineNum := strconv.Itoa(lineN + v.topline + 1)
448 for i := 0; i < maxLineLength-len(lineNum); i++ {
449 v.s.SetContent(x, lineN, ' ', nil, lineNumStyle)
452 // Write the actual line number
453 for _, ch := range lineNum {
454 v.s.SetContent(x, lineN, ch, nil, lineNumStyle)
457 // Write the extra space
458 v.s.SetContent(x, lineN, ' ', nil, lineNumStyle)
463 for _, ch := range line {
464 var lineStyle tcell.Style
465 st, ok := v.matches[charNum]
469 highlightStyle = tcell.StyleDefault
472 if v.cursor.HasSelection() &&
473 (charNum >= v.cursor.selectionStart && charNum <= v.cursor.selectionEnd ||
474 charNum <= v.cursor.selectionStart && charNum >= v.cursor.selectionEnd) {
476 lineStyle = tcell.StyleDefault.Reverse(true)
478 if _, ok := colorscheme["selection"]; ok {
479 lineStyle = colorscheme["selection"]
482 lineStyle = highlightStyle
486 v.s.SetContent(x+tabchars, lineN, ' ', nil, lineStyle)
487 for i := 0; i < tabSize-1; i++ {
489 v.s.SetContent(x+tabchars, lineN, ' ', nil, lineStyle)
492 v.s.SetContent(x+tabchars, lineN, ch, nil, lineStyle)
497 if v.cursor.HasSelection() &&
498 (charNum >= v.cursor.selectionStart && charNum <= v.cursor.selectionEnd ||
499 charNum <= v.cursor.selectionStart && charNum >= v.cursor.selectionEnd) {
501 selectStyle := tcell.StyleDefault.Reverse(true)
503 if _, ok := colorscheme["selection"]; ok {
504 selectStyle = colorscheme["selection"]
506 v.s.SetContent(x+tabchars, lineN, ' ', nil, selectStyle)
510 st, ok := v.matches[charNum]