11 "github.com/zyedidia/tcell"
14 // The View struct stores information about a view into a buffer.
15 // It stores information about the cursor, and the viewport
16 // that the user sees the buffer from.
20 // The topmost line, used for vertical scrolling
22 // The leftmost column, used for horizontal scrolling
25 // Percentage of the terminal window that this view takes up (from 0 to 100)
29 // Actual with and height
33 // How much to offset because of line numbers
36 // The eventhandler for undo/redo
39 // Holds the list of gutter messages
40 messages map[string][]GutterMessage
47 // Since tcell doesn't differentiate between a mouse release event
48 // and a mouse move event with no keys pressed, we need to keep
49 // track of whether or not the mouse was pressed (or not released) last event to determine
50 // mouse release events
53 // This stores when the last click was
54 // This is useful for detecting double and triple clicks
55 lastClickTime time.Time
57 // lastCutTime stores when the last ctrl+k was issued.
58 // It is used for clearing the clipboard to replace it with fresh cut lines.
61 // freshClip returns true if the clipboard has never been pasted.
64 // Was the last mouse event actually a double click?
65 // Useful for detecting triple clicks -- if a double click is detected
66 // but the last mouse event was actually a double click, it's a triple click
68 // Same here, just to keep track for mouse move events
71 // Syntax highlighting matches
73 // The matches from the last frame
74 lastMatches SyntaxMatches
77 // NewView returns a new fullscreen view
78 func NewView(buf *Buffer) *View {
79 return NewViewWidthHeight(buf, 100, 100)
82 // NewViewWidthHeight returns a new view with the specified width and height percentages
83 // Note that w and h are percentages not actual values
84 func NewViewWidthHeight(buf *Buffer, w, h int) *View {
89 v.Resize(screen.Size())
93 v.eh = NewEventHandler(v)
95 v.messages = make(map[string][]GutterMessage)
104 // Resize recalculates the actual width and height of the view from the width and height
106 // This is usually called when the window is resized, or when a split has been added and
107 // the percentages have changed
108 func (v *View) Resize(w, h int) {
109 // Always include 1 line for the command line at the bottom
111 v.width = int(float32(w) * float32(v.widthPercent) / 100)
112 // We subtract 1 for the statusline
113 v.height = int(float32(h) * float32(v.heightPercent) / 100)
114 if settings["statusline"].(bool) {
115 // Make room for the status line if it is enabled
120 // ScrollUp scrolls the view up n lines (if possible)
121 func (v *View) ScrollUp(n int) {
122 // Try to scroll by n but if it would overflow, scroll by 1
123 if v.Topline-n >= 0 {
125 } else if v.Topline > 0 {
130 // ScrollDown scrolls the view down n lines (if possible)
131 func (v *View) ScrollDown(n int) {
132 // Try to scroll by n but if it would overflow, scroll by 1
133 if v.Topline+n <= v.Buf.NumLines-v.height {
135 } else if v.Topline < v.Buf.NumLines-v.height {
140 // CanClose returns whether or not the view can be closed
141 // If there are unsaved changes, the user will be asked if the view can be closed
142 // causing them to lose the unsaved changes
143 // The message is what to print after saying "You have unsaved changes. "
144 func (v *View) CanClose(msg string) bool {
145 if v.Buf.IsModified {
146 quit, canceled := messenger.Prompt("You have unsaved changes. " + msg)
148 if strings.ToLower(quit) == "yes" || strings.ToLower(quit) == "y" {
150 } else if strings.ToLower(quit) == "save" || strings.ToLower(quit) == "s" {
161 // OpenBuffer opens a new buffer in this view.
162 // This resets the topline, event handler and cursor.
163 func (v *View) OpenBuffer(buf *Buffer) {
167 // Put the cursor at the first spot
173 v.Cursor.ResetSelection()
174 v.messages = make(map[string][]GutterMessage)
176 v.eh = NewEventHandler(v)
179 // Set mouseReleased to true because we assume the mouse is not being pressed when
180 // the editor is opened
181 v.mouseReleased = true
182 v.lastClickTime = time.Time{}
185 // ReOpen reloads the current buffer
186 func (v *View) ReOpen() {
187 if v.CanClose("Continue? (yes, no, save) ") {
188 file, err := ioutil.ReadFile(v.Buf.Path)
189 filename := v.Buf.Name
192 messenger.Error(err.Error())
195 buf := NewBuffer(string(file), filename)
203 // Relocate moves the view window so that the cursor is in view
204 // This is useful if the user has scrolled far away, and then starts typing
205 func (v *View) Relocate() bool {
212 if cy > v.Topline+v.height-1 {
213 v.Topline = cy - v.height + 5
217 cx := v.Cursor.GetVisualX()
222 if cx+v.lineNumOffset+1 > v.leftCol+v.width {
223 v.leftCol = cx - v.width + v.lineNumOffset + 1
229 // MoveToMouseClick moves the cursor to location x, y assuming x, y were given
231 func (v *View) MoveToMouseClick(x, y int) {
232 if y-v.Topline > v.height-1 {
234 y = v.height + v.Topline - 1
236 if y >= v.Buf.NumLines {
237 y = v.Buf.NumLines - 1
246 x = v.Cursor.GetCharPosInLine(y, x)
247 if x > Count(v.Buf.Lines[y]) {
248 x = Count(v.Buf.Lines[y])
252 v.Cursor.lastVisualX = v.Cursor.GetVisualX()
255 // HandleEvent handles an event passed by the main loop
256 func (v *View) HandleEvent(event tcell.Event) {
257 // This bool determines whether the view is relocated at the end of the function
258 // By default it's true because most events should cause a relocate
261 switch e := event.(type) {
262 case *tcell.EventResize:
265 case *tcell.EventKey:
266 if e.Key() == tcell.KeyRune {
267 // Insert a character
268 if v.Cursor.HasSelection() {
269 v.Cursor.DeleteSelection()
270 v.Cursor.ResetSelection()
272 v.eh.Insert(v.Cursor.Loc(), string(e.Rune()))
275 for key, action := range bindings {
276 if e.Key() == key.keyCode {
277 if e.Modifiers() == key.modifiers {
279 for _, pl := range loadedPlugins {
280 funcName := strings.Split(runtime.FuncForPC(reflect.ValueOf(action).Pointer()).Name(), ".")
281 err := Call(pl + "_on" + funcName[len(funcName)-1])
290 case *tcell.EventPaste:
291 if v.Cursor.HasSelection() {
292 v.Cursor.DeleteSelection()
293 v.Cursor.ResetSelection()
296 v.eh.Insert(v.Cursor.Loc(), clip)
297 v.Cursor.SetLoc(v.Cursor.Loc() + Count(clip))
299 case *tcell.EventMouse:
301 x -= v.lineNumOffset - v.leftCol
303 // Don't relocate for mouse events
306 button := e.Buttons()
312 v.MoveToMouseClick(x, y)
313 if time.Since(v.lastClickTime)/time.Millisecond < doubleClickThreshold {
316 v.lastClickTime = time.Now()
319 v.doubleClick = false
321 v.Cursor.SelectLine()
324 v.lastClickTime = time.Now()
327 v.tripleClick = false
329 v.Cursor.SelectWord()
332 v.doubleClick = false
333 v.tripleClick = false
334 v.lastClickTime = time.Now()
336 loc := v.Cursor.Loc()
337 v.Cursor.origSelection[0] = loc
338 v.Cursor.curSelection[0] = loc
339 v.Cursor.curSelection[1] = loc
341 v.mouseReleased = false
342 } else if !v.mouseReleased {
343 v.MoveToMouseClick(x, y)
345 v.Cursor.AddLineToSelection()
346 } else if v.doubleClick {
347 v.Cursor.AddWordToSelection()
349 v.Cursor.curSelection[1] = v.Cursor.Loc()
352 case tcell.ButtonNone:
353 // Mouse event with no click
354 if !v.mouseReleased {
355 // Mouse was just released
357 // Relocating here isn't really necessary because the cursor will
358 // be in the right place from the last mouse event
359 // However, if we are running in a terminal that doesn't support mouse motion
360 // events, this still allows the user to make selections, except only after they
363 if !v.doubleClick && !v.tripleClick {
364 v.MoveToMouseClick(x, y)
365 v.Cursor.curSelection[1] = v.Cursor.Loc()
367 v.mouseReleased = true
371 scrollSpeed := int(settings["scrollSpeed"].(float64))
372 v.ScrollUp(scrollSpeed)
373 case tcell.WheelDown:
375 scrollSpeed := int(settings["scrollSpeed"].(float64))
376 v.ScrollDown(scrollSpeed)
383 if settings["syntax"].(bool) {
388 // GutterMessage creates a message in this view's gutter
389 func (v *View) GutterMessage(section string, lineN int, msg string, kind int) {
391 gutterMsg := GutterMessage{
396 for _, v := range v.messages {
397 for _, gmsg := range v {
398 if gmsg.lineNum == lineN {
403 messages := v.messages[section]
404 v.messages[section] = append(messages, gutterMsg)
407 // ClearGutterMessages clears all gutter messages from a given section
408 func (v *View) ClearGutterMessages(section string) {
409 v.messages[section] = []GutterMessage{}
412 // ClearAllGutterMessages clears all the gutter messages
413 func (v *View) ClearAllGutterMessages() {
414 for k := range v.messages {
415 v.messages[k] = []GutterMessage{}
419 // DisplayView renders the view to the screen
420 func (v *View) DisplayView() {
421 // The character number of the character in the top left of the screen
422 charNum := ToCharPos(0, v.Topline, v.Buf)
424 // Convert the length of buffer to a string, and get the length of the string
425 // We are going to have to offset by that amount
426 maxLineLength := len(strconv.Itoa(v.Buf.NumLines))
427 // + 1 for the little space after the line number
428 if settings["ruler"] == true {
429 v.lineNumOffset = maxLineLength + 1
433 var highlightStyle tcell.Style
435 var hasGutterMessages bool
436 for _, v := range v.messages {
438 hasGutterMessages = true
441 if hasGutterMessages {
445 for lineN := 0; lineN < v.height; lineN++ {
447 // If the buffer is smaller than the view height
448 // and we went too far, break
449 if lineN+v.Topline >= v.Buf.NumLines {
452 line := v.Buf.Lines[lineN+v.Topline]
454 if hasGutterMessages {
456 for k := range v.messages {
457 for _, msg := range v.messages[k] {
458 if msg.lineNum == lineN+v.Topline {
460 gutterStyle := tcell.StyleDefault
463 if style, ok := colorscheme["gutter-info"]; ok {
467 if style, ok := colorscheme["gutter-warning"]; ok {
471 if style, ok := colorscheme["gutter-error"]; ok {
475 screen.SetContent(x, lineN, '>', nil, gutterStyle)
477 screen.SetContent(x, lineN, '>', nil, gutterStyle)
479 if v.Cursor.y == lineN+v.Topline {
480 messenger.Message(msg.msg)
481 messenger.gutterMessage = true
487 screen.SetContent(x, lineN, ' ', nil, tcell.StyleDefault)
489 screen.SetContent(x, lineN, ' ', nil, tcell.StyleDefault)
491 if v.Cursor.y == lineN+v.Topline && messenger.gutterMessage {
493 messenger.gutterMessage = false
498 // Write the line number
499 lineNumStyle := defStyle
500 if style, ok := colorscheme["line-number"]; ok {
503 // Write the spaces before the line number if necessary
505 if settings["ruler"] == true {
506 lineNum = strconv.Itoa(lineN + v.Topline + 1)
507 for i := 0; i < maxLineLength-len(lineNum); i++ {
508 screen.SetContent(x, lineN, ' ', nil, lineNumStyle)
511 // Write the actual line number
512 for _, ch := range lineNum {
513 screen.SetContent(x, lineN, ch, nil, lineNumStyle)
517 if settings["ruler"] == true {
518 // Write the extra space
519 screen.SetContent(x, lineN, ' ', nil, lineNumStyle)
525 for colN, ch := range line {
526 var lineStyle tcell.Style
528 if settings["syntax"].(bool) {
529 // Syntax highlighting is enabled
530 highlightStyle = v.matches[lineN][colN]
533 if v.Cursor.HasSelection() &&
534 (charNum >= v.Cursor.curSelection[0] && charNum < v.Cursor.curSelection[1] ||
535 charNum < v.Cursor.curSelection[0] && charNum >= v.Cursor.curSelection[1]) {
537 lineStyle = tcell.StyleDefault.Reverse(true)
539 if style, ok := colorscheme["selection"]; ok {
543 lineStyle = highlightStyle
547 lineIndentStyle := defStyle
548 if style, ok := colorscheme["indent-char"]; ok {
549 lineIndentStyle = style
551 if v.Cursor.HasSelection() &&
552 (charNum >= v.Cursor.curSelection[0] && charNum < v.Cursor.curSelection[1] ||
553 charNum < v.Cursor.curSelection[0] && charNum >= v.Cursor.curSelection[1]) {
555 lineIndentStyle = tcell.StyleDefault.Reverse(true)
557 if style, ok := colorscheme["selection"]; ok {
558 lineIndentStyle = style
561 indentChar := []rune(settings["indentchar"].(string))
562 screen.SetContent(x-v.leftCol+tabchars, lineN, indentChar[0], nil, lineIndentStyle)
563 tabSize := int(settings["tabsize"].(float64))
564 for i := 0; i < tabSize-1; i++ {
566 if x-v.leftCol+tabchars >= v.lineNumOffset {
567 screen.SetContent(x-v.leftCol+tabchars, lineN, ' ', nil, lineStyle)
571 if x-v.leftCol+tabchars >= v.lineNumOffset {
572 screen.SetContent(x-v.leftCol+tabchars, lineN, ch, nil, lineStyle)
578 // Here we are at a newline
580 // The newline may be selected, in which case we should draw the selection style
581 // with a space to represent it
582 if v.Cursor.HasSelection() &&
583 (charNum >= v.Cursor.curSelection[0] && charNum < v.Cursor.curSelection[1] ||
584 charNum < v.Cursor.curSelection[0] && charNum >= v.Cursor.curSelection[1]) {
586 selectStyle := defStyle.Reverse(true)
588 if style, ok := colorscheme["selection"]; ok {
591 screen.SetContent(x-v.leftCol+tabchars, lineN, ' ', nil, selectStyle)
598 // Display renders the view, the cursor, and statusline
599 func (v *View) Display() {
602 if settings["statusline"].(bool) {