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 + 1
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 {
278 for _, pl := range loadedPlugins {
279 funcName := strings.Split(runtime.FuncForPC(reflect.ValueOf(action).Pointer()).Name(), ".")
280 err := Call(pl + "_on" + funcName[len(funcName)-1])
288 case *tcell.EventPaste:
289 if v.Cursor.HasSelection() {
290 v.Cursor.DeleteSelection()
291 v.Cursor.ResetSelection()
294 v.eh.Insert(v.Cursor.Loc(), clip)
295 v.Cursor.SetLoc(v.Cursor.Loc() + Count(clip))
297 case *tcell.EventMouse:
299 x -= v.lineNumOffset - v.leftCol
302 button := e.Buttons()
307 if v.mouseReleased && !e.HasMotion() {
308 v.MoveToMouseClick(x, y)
309 if time.Since(v.lastClickTime)/time.Millisecond < doubleClickThreshold {
312 v.lastClickTime = time.Now()
315 v.doubleClick = false
317 v.Cursor.SelectLine()
320 v.lastClickTime = time.Now()
323 v.tripleClick = false
325 v.Cursor.SelectWord()
328 v.doubleClick = false
329 v.tripleClick = false
330 v.lastClickTime = time.Now()
332 loc := v.Cursor.Loc()
333 v.Cursor.origSelection[0] = loc
334 v.Cursor.curSelection[0] = loc
335 v.Cursor.curSelection[1] = loc
337 v.mouseReleased = false
338 } else if !v.mouseReleased {
339 v.MoveToMouseClick(x, y)
341 v.Cursor.AddLineToSelection()
342 } else if v.doubleClick {
343 v.Cursor.AddWordToSelection()
345 v.Cursor.curSelection[1] = v.Cursor.Loc()
348 case tcell.ButtonNone:
349 // Mouse event with no click
350 if !v.mouseReleased {
351 // Mouse was just released
353 // Relocating here isn't really necessary because the cursor will
354 // be in the right place from the last mouse event
355 // However, if we are running in a terminal that doesn't support mouse motion
356 // events, this still allows the user to make selections, except only after they
359 if !v.doubleClick && !v.tripleClick {
360 v.MoveToMouseClick(x, y)
361 v.Cursor.curSelection[1] = v.Cursor.Loc()
363 v.mouseReleased = true
365 // We don't want to relocate because otherwise the view will be relocated
366 // every time the user moves the cursor
370 scrollSpeed := int(settings["scrollspeed"].(float64))
371 v.ScrollUp(scrollSpeed)
372 // We don't want to relocate if the user is scrolling
374 case tcell.WheelDown:
376 scrollSpeed := int(settings["scrollspeed"].(float64))
377 v.ScrollDown(scrollSpeed)
378 // We don't want to relocate if the user is scrolling
386 if settings["syntax"].(bool) {
391 // GutterMessage creates a message in this view's gutter
392 func (v *View) GutterMessage(section string, lineN int, msg string, kind int) {
394 gutterMsg := GutterMessage{
399 for _, v := range v.messages {
400 for _, gmsg := range v {
401 if gmsg.lineNum == lineN {
406 messages := v.messages[section]
407 v.messages[section] = append(messages, gutterMsg)
410 // ClearGutterMessages clears all gutter messages from a given section
411 func (v *View) ClearGutterMessages(section string) {
412 v.messages[section] = []GutterMessage{}
415 // ClearAllGutterMessages clears all the gutter messages
416 func (v *View) ClearAllGutterMessages() {
417 for k := range v.messages {
418 v.messages[k] = []GutterMessage{}
422 // DisplayView renders the view to the screen
423 func (v *View) DisplayView() {
424 // The character number of the character in the top left of the screen
425 charNum := ToCharPos(0, v.Topline, v.Buf)
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(v.Buf.NumLines))
430 // + 1 for the little space after the line number
431 if settings["ruler"] == true {
432 v.lineNumOffset = maxLineLength + 1
436 var highlightStyle tcell.Style
438 var hasGutterMessages bool
439 for _, v := range v.messages {
441 hasGutterMessages = true
444 if hasGutterMessages {
448 for lineN := 0; lineN < v.height; lineN++ {
450 // If the buffer is smaller than the view height
451 // and we went too far, break
452 if lineN+v.Topline >= v.Buf.NumLines {
455 line := v.Buf.Lines[lineN+v.Topline]
457 if hasGutterMessages {
459 for k := range v.messages {
460 for _, msg := range v.messages[k] {
461 if msg.lineNum == lineN+v.Topline {
463 gutterStyle := tcell.StyleDefault
466 if style, ok := colorscheme["gutter-info"]; ok {
470 if style, ok := colorscheme["gutter-warning"]; ok {
474 if style, ok := colorscheme["gutter-error"]; ok {
478 screen.SetContent(x, lineN, '>', nil, gutterStyle)
480 screen.SetContent(x, lineN, '>', nil, gutterStyle)
482 if v.Cursor.y == lineN+v.Topline {
483 messenger.Message(msg.msg)
484 messenger.gutterMessage = true
490 screen.SetContent(x, lineN, ' ', nil, tcell.StyleDefault)
492 screen.SetContent(x, lineN, ' ', nil, tcell.StyleDefault)
494 if v.Cursor.y == lineN+v.Topline && messenger.gutterMessage {
496 messenger.gutterMessage = false
501 // Write the line number
502 lineNumStyle := defStyle
503 if style, ok := colorscheme["line-number"]; ok {
506 // Write the spaces before the line number if necessary
508 if settings["ruler"] == true {
509 lineNum = strconv.Itoa(lineN + v.Topline + 1)
510 for i := 0; i < maxLineLength-len(lineNum); i++ {
511 screen.SetContent(x, lineN, ' ', nil, lineNumStyle)
514 // Write the actual line number
515 for _, ch := range lineNum {
516 screen.SetContent(x, lineN, ch, nil, lineNumStyle)
520 if settings["ruler"] == true {
521 // Write the extra space
522 screen.SetContent(x, lineN, ' ', nil, lineNumStyle)
528 for colN, ch := range line {
529 var lineStyle tcell.Style
531 if settings["syntax"].(bool) {
532 // Syntax highlighting is enabled
533 highlightStyle = v.matches[lineN][colN]
536 if v.Cursor.HasSelection() &&
537 (charNum >= v.Cursor.curSelection[0] && charNum < v.Cursor.curSelection[1] ||
538 charNum < v.Cursor.curSelection[0] && charNum >= v.Cursor.curSelection[1]) {
540 lineStyle = tcell.StyleDefault.Reverse(true)
542 if style, ok := colorscheme["selection"]; ok {
546 lineStyle = highlightStyle
550 screen.SetContent(x+tabchars, lineN, ' ', nil, lineStyle)
551 tabSize := int(settings["tabsize"].(float64))
552 for i := 0; i < tabSize-1; i++ {
554 if x-v.leftCol+tabchars >= v.lineNumOffset {
555 screen.SetContent(x-v.leftCol+tabchars, lineN, ' ', nil, lineStyle)
559 if x-v.leftCol+tabchars >= v.lineNumOffset {
560 screen.SetContent(x-v.leftCol+tabchars, lineN, ch, nil, lineStyle)
566 // Here we are at a newline
568 // The newline may be selected, in which case we should draw the selection style
569 // with a space to represent it
570 if v.Cursor.HasSelection() &&
571 (charNum >= v.Cursor.curSelection[0] && charNum < v.Cursor.curSelection[1] ||
572 charNum < v.Cursor.curSelection[0] && charNum >= v.Cursor.curSelection[1]) {
574 selectStyle := defStyle.Reverse(true)
576 if style, ok := colorscheme["selection"]; ok {
579 screen.SetContent(x-v.leftCol+tabchars, lineN, ' ', nil, selectStyle)
586 // Display renders the view, the cursor, and statusline
587 func (v *View) Display() {
590 if settings["statusline"].(bool) {