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.
18 // A pointer to the buffer's cursor for ease of access
21 // The topmost line, used for vertical scrolling
23 // The leftmost column, used for horizontal scrolling
26 // Percentage of the terminal window that this view takes up (from 0 to 100)
30 // Actual with and height
34 // How much to offset because of line numbers
37 // Holds the list of gutter messages
38 messages map[string][]GutterMessage
45 // Since tcell doesn't differentiate between a mouse release event
46 // and a mouse move event with no keys pressed, we need to keep
47 // track of whether or not the mouse was pressed (or not released) last event to determine
48 // mouse release events
51 // This stores when the last click was
52 // This is useful for detecting double and triple clicks
53 lastClickTime time.Time
55 // lastCutTime stores when the last ctrl+k was issued.
56 // It is used for clearing the clipboard to replace it with fresh cut lines.
59 // freshClip returns true if the clipboard has never been pasted.
62 // Was the last mouse event actually a double click?
63 // Useful for detecting triple clicks -- if a double click is detected
64 // but the last mouse event was actually a double click, it's a triple click
66 // Same here, just to keep track for mouse move events
69 // Syntax highlighting matches
71 // The matches from the last frame
72 lastMatches SyntaxMatches
75 // NewView returns a new fullscreen view
76 func NewView(buf *Buffer) *View {
77 return NewViewWidthHeight(buf, 100, 100)
80 // NewViewWidthHeight returns a new view with the specified width and height percentages
81 // Note that w and h are percentages not actual values
82 func NewViewWidthHeight(buf *Buffer, w, h int) *View {
87 v.Resize(screen.Size())
91 v.messages = make(map[string][]GutterMessage)
100 // Resize recalculates the actual width and height of the view from the width and height
102 // This is usually called when the window is resized, or when a split has been added and
103 // the percentages have changed
104 func (v *View) Resize(w, h int) {
105 // Always include 1 line for the command line at the bottom
107 v.width = int(float32(w) * float32(v.widthPercent) / 100)
108 // We subtract 1 for the statusline
109 v.height = int(float32(h) * float32(v.heightPercent) / 100)
110 if settings["statusline"].(bool) {
111 // Make room for the status line if it is enabled
116 // ScrollUp scrolls the view up n lines (if possible)
117 func (v *View) ScrollUp(n int) {
118 // Try to scroll by n but if it would overflow, scroll by 1
119 if v.Topline-n >= 0 {
121 } else if v.Topline > 0 {
126 // ScrollDown scrolls the view down n lines (if possible)
127 func (v *View) ScrollDown(n int) {
128 // Try to scroll by n but if it would overflow, scroll by 1
129 if v.Topline+n <= v.Buf.NumLines-v.height {
131 } else if v.Topline < v.Buf.NumLines-v.height {
136 // CanClose returns whether or not the view can be closed
137 // If there are unsaved changes, the user will be asked if the view can be closed
138 // causing them to lose the unsaved changes
139 // The message is what to print after saying "You have unsaved changes. "
140 func (v *View) CanClose(msg string) bool {
141 if v.Buf.IsModified {
142 quit, canceled := messenger.Prompt("You have unsaved changes. " + msg)
144 if strings.ToLower(quit) == "yes" || strings.ToLower(quit) == "y" {
146 } else if strings.ToLower(quit) == "save" || strings.ToLower(quit) == "s" {
157 // OpenBuffer opens a new buffer in this view.
158 // This resets the topline, event handler and cursor.
159 func (v *View) OpenBuffer(buf *Buffer) {
161 v.Cursor = &buf.Cursor
164 v.Cursor.ResetSelection()
165 v.messages = make(map[string][]GutterMessage)
169 // Set mouseReleased to true because we assume the mouse is not being pressed when
170 // the editor is opened
171 v.mouseReleased = true
172 v.lastClickTime = time.Time{}
175 // ReOpen reloads the current buffer
176 func (v *View) ReOpen() {
177 if v.CanClose("Continue? (yes, no, save) ") {
178 file, err := ioutil.ReadFile(v.Buf.Path)
179 filename := v.Buf.Name
182 messenger.Error(err.Error())
185 buf := NewBuffer(string(file), filename)
193 // Relocate moves the view window so that the cursor is in view
194 // This is useful if the user has scrolled far away, and then starts typing
195 func (v *View) Relocate() bool {
198 scrollmargin := int(settings["scrollmargin"].(float64))
199 if cy < v.Topline+scrollmargin && cy > scrollmargin-1 {
200 v.Topline = cy - scrollmargin
202 } else if cy < v.Topline {
206 if cy > v.Topline+v.height-1-scrollmargin {
207 v.Topline = cy - v.height + 1 + scrollmargin
211 cx := v.Cursor.GetVisualX()
216 if cx+v.lineNumOffset+1 > v.leftCol+v.width {
217 v.leftCol = cx - v.width + v.lineNumOffset + 1
223 // MoveToMouseClick moves the cursor to location x, y assuming x, y were given
225 func (v *View) MoveToMouseClick(x, y int) {
226 if y-v.Topline > v.height-1 {
228 y = v.height + v.Topline - 1
230 if y >= v.Buf.NumLines {
231 y = v.Buf.NumLines - 1
240 x = v.Cursor.GetCharPosInLine(y, x)
241 if x > Count(v.Buf.Lines[y]) {
242 x = Count(v.Buf.Lines[y])
246 v.Cursor.lastVisualX = v.Cursor.GetVisualX()
249 // HandleEvent handles an event passed by the main loop
250 func (v *View) HandleEvent(event tcell.Event) {
251 // This bool determines whether the view is relocated at the end of the function
252 // By default it's true because most events should cause a relocate
255 switch e := event.(type) {
256 case *tcell.EventResize:
259 case *tcell.EventKey:
260 if e.Key() == tcell.KeyRune && e.Modifiers() == 0 {
261 // Insert a character
262 if v.Cursor.HasSelection() {
263 v.Cursor.DeleteSelection()
264 v.Cursor.ResetSelection()
266 v.Buf.Insert(v.Cursor.Loc(), string(e.Rune()))
269 for key, action := range bindings {
270 if e.Key() == key.keyCode {
271 if e.Key() == tcell.KeyRune {
272 if e.Rune() != key.r {
276 if e.Modifiers() == key.modifiers {
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])
289 case *tcell.EventPaste:
290 if v.Cursor.HasSelection() {
291 v.Cursor.DeleteSelection()
292 v.Cursor.ResetSelection()
295 v.Buf.Insert(v.Cursor.Loc(), clip)
296 v.Cursor.SetLoc(v.Cursor.Loc() + Count(clip))
298 case *tcell.EventMouse:
300 x -= v.lineNumOffset - v.leftCol
302 // Don't relocate for mouse events
305 button := e.Buttons()
311 v.MoveToMouseClick(x, y)
312 if time.Since(v.lastClickTime)/time.Millisecond < doubleClickThreshold {
315 v.lastClickTime = time.Now()
318 v.doubleClick = false
320 v.Cursor.SelectLine()
323 v.lastClickTime = time.Now()
326 v.tripleClick = false
328 v.Cursor.SelectWord()
331 v.doubleClick = false
332 v.tripleClick = false
333 v.lastClickTime = time.Now()
335 loc := v.Cursor.Loc()
336 v.Cursor.origSelection[0] = loc
337 v.Cursor.curSelection[0] = loc
338 v.Cursor.curSelection[1] = loc
340 v.mouseReleased = false
341 } else if !v.mouseReleased {
342 v.MoveToMouseClick(x, y)
344 v.Cursor.AddLineToSelection()
345 } else if v.doubleClick {
346 v.Cursor.AddWordToSelection()
348 v.Cursor.curSelection[1] = v.Cursor.Loc()
351 case tcell.ButtonNone:
352 // Mouse event with no click
353 if !v.mouseReleased {
354 // Mouse was just released
356 // Relocating here isn't really necessary because the cursor will
357 // be in the right place from the last mouse event
358 // However, if we are running in a terminal that doesn't support mouse motion
359 // events, this still allows the user to make selections, except only after they
362 if !v.doubleClick && !v.tripleClick {
363 v.MoveToMouseClick(x, y)
364 v.Cursor.curSelection[1] = v.Cursor.Loc()
366 v.mouseReleased = true
370 scrollspeed := int(settings["scrollspeed"].(float64))
371 v.ScrollUp(scrollspeed)
372 case tcell.WheelDown:
374 scrollspeed := int(settings["scrollspeed"].(float64))
375 v.ScrollDown(scrollspeed)
382 if settings["syntax"].(bool) {
387 // GutterMessage creates a message in this view's gutter
388 func (v *View) GutterMessage(section string, lineN int, msg string, kind int) {
390 gutterMsg := GutterMessage{
395 for _, v := range v.messages {
396 for _, gmsg := range v {
397 if gmsg.lineNum == lineN {
402 messages := v.messages[section]
403 v.messages[section] = append(messages, gutterMsg)
406 // ClearGutterMessages clears all gutter messages from a given section
407 func (v *View) ClearGutterMessages(section string) {
408 v.messages[section] = []GutterMessage{}
411 // ClearAllGutterMessages clears all the gutter messages
412 func (v *View) ClearAllGutterMessages() {
413 for k := range v.messages {
414 v.messages[k] = []GutterMessage{}
418 // DisplayView renders the view to the screen
419 func (v *View) DisplayView() {
420 // The character number of the character in the top left of the screen
421 charNum := ToCharPos(0, v.Topline, v.Buf)
423 // Convert the length of buffer to a string, and get the length of the string
424 // We are going to have to offset by that amount
425 maxLineLength := len(strconv.Itoa(v.Buf.NumLines))
426 // + 1 for the little space after the line number
427 if settings["ruler"] == true {
428 v.lineNumOffset = maxLineLength + 1
432 var highlightStyle tcell.Style
434 var hasGutterMessages bool
435 for _, v := range v.messages {
437 hasGutterMessages = true
440 if hasGutterMessages {
444 for lineN := 0; lineN < v.height; lineN++ {
446 // If the buffer is smaller than the view height
447 // and we went too far, break
448 if lineN+v.Topline >= v.Buf.NumLines {
451 line := v.Buf.Lines[lineN+v.Topline]
453 if hasGutterMessages {
455 for k := range v.messages {
456 for _, msg := range v.messages[k] {
457 if msg.lineNum == lineN+v.Topline {
459 gutterStyle := tcell.StyleDefault
462 if style, ok := colorscheme["gutter-info"]; ok {
466 if style, ok := colorscheme["gutter-warning"]; ok {
470 if style, ok := colorscheme["gutter-error"]; ok {
474 screen.SetContent(x, lineN, '>', nil, gutterStyle)
476 screen.SetContent(x, lineN, '>', nil, gutterStyle)
478 if v.Cursor.y == lineN+v.Topline {
479 messenger.Message(msg.msg)
480 messenger.gutterMessage = true
486 screen.SetContent(x, lineN, ' ', nil, tcell.StyleDefault)
488 screen.SetContent(x, lineN, ' ', nil, tcell.StyleDefault)
490 if v.Cursor.y == lineN+v.Topline && messenger.gutterMessage {
492 messenger.gutterMessage = false
497 // Write the line number
498 lineNumStyle := defStyle
499 if style, ok := colorscheme["line-number"]; ok {
502 // Write the spaces before the line number if necessary
504 if settings["ruler"] == true {
505 lineNum = strconv.Itoa(lineN + v.Topline + 1)
506 for i := 0; i < maxLineLength-len(lineNum); i++ {
507 screen.SetContent(x, lineN, ' ', nil, lineNumStyle)
510 // Write the actual line number
511 for _, ch := range lineNum {
512 screen.SetContent(x, lineN, ch, nil, lineNumStyle)
516 if settings["ruler"] == true {
517 // Write the extra space
518 screen.SetContent(x, lineN, ' ', nil, lineNumStyle)
524 for colN, ch := range line {
525 var lineStyle tcell.Style
527 if settings["syntax"].(bool) {
528 // Syntax highlighting is enabled
529 highlightStyle = v.matches[lineN][colN]
532 if v.Cursor.HasSelection() &&
533 (charNum >= v.Cursor.curSelection[0] && charNum < v.Cursor.curSelection[1] ||
534 charNum < v.Cursor.curSelection[0] && charNum >= v.Cursor.curSelection[1]) {
536 lineStyle = tcell.StyleDefault.Reverse(true)
538 if style, ok := colorscheme["selection"]; ok {
542 lineStyle = highlightStyle
546 lineIndentStyle := defStyle
547 if style, ok := colorscheme["indent-char"]; ok {
548 lineIndentStyle = style
550 if v.Cursor.HasSelection() &&
551 (charNum >= v.Cursor.curSelection[0] && charNum < v.Cursor.curSelection[1] ||
552 charNum < v.Cursor.curSelection[0] && charNum >= v.Cursor.curSelection[1]) {
554 lineIndentStyle = tcell.StyleDefault.Reverse(true)
556 if style, ok := colorscheme["selection"]; ok {
557 lineIndentStyle = style
560 indentChar := []rune(settings["indentchar"].(string))
561 screen.SetContent(x-v.leftCol+tabchars, lineN, indentChar[0], nil, lineIndentStyle)
562 tabSize := int(settings["tabsize"].(float64))
563 for i := 0; i < tabSize-1; i++ {
565 if x-v.leftCol+tabchars >= v.lineNumOffset {
566 screen.SetContent(x-v.leftCol+tabchars, lineN, ' ', nil, lineStyle)
570 if x-v.leftCol+tabchars >= v.lineNumOffset {
571 screen.SetContent(x-v.leftCol+tabchars, lineN, ch, nil, lineStyle)
577 // Here we are at a newline
579 // The newline may be selected, in which case we should draw the selection style
580 // with a space to represent it
581 if v.Cursor.HasSelection() &&
582 (charNum >= v.Cursor.curSelection[0] && charNum < v.Cursor.curSelection[1] ||
583 charNum < v.Cursor.curSelection[0] && charNum >= v.Cursor.curSelection[1]) {
585 selectStyle := defStyle.Reverse(true)
587 if style, ok := colorscheme["selection"]; ok {
590 screen.SetContent(x-v.leftCol+tabchars, lineN, ' ', nil, selectStyle)
597 // DisplayCursor draws the current buffer's cursor to the screen
598 func (v *View) DisplayCursor() {
599 // Don't draw the cursor if it is out of the viewport or if it has a selection
600 if (v.Cursor.y-v.Topline < 0 || v.Cursor.y-v.Topline > v.height-1) || v.Cursor.HasSelection() {
603 screen.ShowCursor(v.Cursor.GetVisualX()+v.lineNumOffset-v.leftCol, v.Cursor.y-v.Topline)
607 // Display renders the view, the cursor, and statusline
608 func (v *View) Display() {
611 if settings["statusline"].(bool) {