Files
zyedidia.micro/cmd/micro/view.go
Zachary Yedidia 9094c174cc Initial support for terminal within micro
This commit adds beta support for running a shell or other program
within a micro view.

Use the `> term` command. With no arguments, `term` will open your
shell in interactive mode. You can also run an arbitrary command
with `> term cmd` and the command with be executed and output
shown. One issue at the moment is the terminal window will close
immediately after the process dies.

No mouse events are sent to programs running within micro.

Ref #243
2018-01-04 17:03:08 -05:00

1164 lines
29 KiB
Go

package main
import (
"fmt"
"os"
"os/exec"
"reflect"
"strconv"
"strings"
"time"
"github.com/james4k/terminal"
"github.com/zyedidia/tcell"
)
// The ViewType defines what kind of view this is
type ViewType struct {
Kind int
Readonly bool // The file cannot be edited
Scratch bool // The file cannot be saved
}
var (
vtDefault = ViewType{0, false, false}
vtHelp = ViewType{1, true, true}
vtLog = ViewType{2, true, true}
vtScratch = ViewType{3, false, true}
vtRaw = ViewType{4, true, true}
vtTerm = ViewType{5, true, true}
)
// The View struct stores information about a view into a buffer.
// It stores information about the cursor, and the viewport
// that the user sees the buffer from.
type View struct {
// A pointer to the buffer's cursor for ease of access
Cursor *Cursor
// The topmost line, used for vertical scrolling
Topline int
// The leftmost column, used for horizontal scrolling
leftCol int
// Specifies whether or not this view holds a help buffer
Type ViewType
// Actual width and height
Width int
Height int
LockWidth bool
LockHeight bool
// Where this view is located
x, y int
// How much to offset because of line numbers
lineNumOffset int
// Holds the list of gutter messages
messages map[string][]GutterMessage
// This is the index of this view in the views array
Num int
// What tab is this view stored in
TabNum int
// The buffer
Buf *Buffer
// 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
// We need to keep track of insert key press toggle
isOverwriteMode bool
// This stores when the last click was
// This is useful for detecting double and triple clicks
lastClickTime time.Time
lastLoc Loc
// lastCutTime stores when the last ctrl+k was issued.
// It is used for clearing the clipboard to replace it with fresh cut lines.
lastCutTime time.Time
// freshClip returns true if the clipboard has never been pasted.
freshClip bool
// Was the last mouse event actually a double click?
// Useful for detecting triple clicks -- if a double click is detected
// but the last mouse event was actually a double click, it's a triple click
doubleClick bool
// Same here, just to keep track for mouse move events
tripleClick bool
cellview *CellView
splitNode *LeafNode
scrollbar *ScrollBar
termState terminal.State
pty *os.File
term *terminal.VT
termtitle string
}
// NewView returns a new fullscreen view
func NewView(buf *Buffer) *View {
screenW, screenH := screen.Size()
return NewViewWidthHeight(buf, screenW, screenH)
}
// NewViewWidthHeight returns a new view with the specified width and height
// Note that w and h are raw column and row values
func NewViewWidthHeight(buf *Buffer, w, h int) *View {
v := new(View)
v.x, v.y = 0, 0
v.Width = w
v.Height = h
v.cellview = new(CellView)
v.ToggleTabbar()
v.OpenBuffer(buf)
v.messages = make(map[string][]GutterMessage)
v.sline = &Statusline{
view: v,
}
v.scrollbar = &ScrollBar{
view: v,
}
if v.Buf.Settings["statusline"].(bool) {
v.Height--
}
for pl := range loadedPlugins {
_, err := Call(pl+".onViewOpen", v)
if err != nil && !strings.HasPrefix(err.Error(), "function does not exist") {
TermMessage(err)
continue
}
}
return v
}
// ToggleStatusLine creates an extra row for the statusline if necessary
func (v *View) ToggleStatusLine() {
if v.Buf.Settings["statusline"].(bool) {
v.Height--
} else {
v.Height++
}
}
// StartTerminal execs a command in this view
func (v *View) StartTerminal(execCmd []string) error {
// cmd := exec.Command(os.Getenv("SHELL"), "-i")
if len(execCmd) <= 0 {
return nil
}
cmd := exec.Command(execCmd[0], execCmd[1:]...)
term, pty, err := terminal.Start(&v.termState, cmd)
if err != nil {
return err
}
term.Resize(v.Width, v.Height)
v.Type = vtTerm
v.term = term
v.termtitle = execCmd[0]
v.pty = pty
go func() {
for {
err := term.Parse()
if err != nil {
fmt.Fprintln(os.Stderr, err)
break
}
updateterm <- true
}
closeterm <- v.Num
}()
return nil
}
// CloseTerminal shuts down the tty running in this view
// and returns it to the default view type
func (v *View) CloseTerminal() {
v.pty.Close()
v.term.Close()
v.Type = vtDefault
}
// ToggleTabbar creates an extra row for the tabbar if necessary
func (v *View) ToggleTabbar() {
if len(tabs) > 1 {
if v.y == 0 {
// Include one line for the tab bar at the top
v.Height--
v.y = 1
}
} else {
if v.y == 1 {
v.y = 0
v.Height++
}
}
}
func (v *View) paste(clip string) {
leadingWS := GetLeadingWhitespace(v.Buf.Line(v.Cursor.Y))
if v.Cursor.HasSelection() {
v.Cursor.DeleteSelection()
v.Cursor.ResetSelection()
}
clip = strings.Replace(clip, "\n", "\n"+leadingWS, -1)
v.Buf.Insert(v.Cursor.Loc, clip)
// v.Cursor.Loc = v.Cursor.Loc.Move(Count(clip), v.Buf)
v.freshClip = false
messenger.Message("Pasted clipboard")
}
// ScrollUp scrolls the view up n lines (if possible)
func (v *View) ScrollUp(n int) {
// Try to scroll by n but if it would overflow, scroll by 1
if v.Topline-n >= 0 {
v.Topline -= n
} else if v.Topline > 0 {
v.Topline--
}
}
// ScrollDown scrolls the view down n lines (if possible)
func (v *View) ScrollDown(n int) {
// Try to scroll by n but if it would overflow, scroll by 1
if v.Topline+n <= v.Buf.NumLines {
v.Topline += n
} else if v.Topline < v.Buf.NumLines-1 {
v.Topline++
}
}
// 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
func (v *View) CanClose() bool {
if v.Type == vtDefault && v.Buf.Modified() {
var choice bool
var canceled bool
if v.Buf.Settings["autosave"].(bool) {
choice = true
} else {
choice, canceled = messenger.YesNoPrompt("Save changes to " + v.Buf.GetName() + " before closing? (y,n,esc) ")
}
if !canceled {
//if char == 'y' {
if choice {
v.Save(true)
}
} else {
return false
}
}
return true
}
// OpenBuffer opens a new buffer in this view.
// This resets the topline, event handler and cursor.
func (v *View) OpenBuffer(buf *Buffer) {
screen.Clear()
v.CloseBuffer()
v.Buf = buf
v.Cursor = &buf.Cursor
v.Topline = 0
v.leftCol = 0
v.Cursor.ResetSelection()
v.Relocate()
v.Center(false)
v.messages = make(map[string][]GutterMessage)
// Set mouseReleased to true because we assume the mouse is not being pressed when
// the editor is opened
v.mouseReleased = true
// Set isOverwriteMode to false, because we assume we are in the default mode when editor
// is opened
v.isOverwriteMode = false
v.lastClickTime = time.Time{}
}
// Open opens the given file in the view
func (v *View) Open(filename string) {
filename = ReplaceHome(filename)
file, err := os.Open(filename)
fileInfo, _ := os.Stat(filename)
if err == nil && fileInfo.IsDir() {
messenger.Error(filename, " is a directory")
return
}
defer file.Close()
var buf *Buffer
if err != nil {
messenger.Message(err.Error())
// File does not exist -- create an empty buffer with that name
buf = NewBufferFromString("", filename)
} else {
buf = NewBuffer(file, FSize(file), filename)
}
v.OpenBuffer(buf)
}
// CloseBuffer performs any closing functions on the buffer
func (v *View) CloseBuffer() {
if v.Buf != nil {
v.Buf.Serialize()
}
}
// ReOpen reloads the current buffer
func (v *View) ReOpen() {
if v.CanClose() {
screen.Clear()
v.Buf.ReOpen()
v.Relocate()
}
}
// HSplit opens a horizontal split with the given buffer
func (v *View) HSplit(buf *Buffer) {
i := 0
if v.Buf.Settings["splitbottom"].(bool) {
i = 1
}
v.splitNode.HSplit(buf, v.Num+i)
}
// VSplit opens a vertical split with the given buffer
func (v *View) VSplit(buf *Buffer) {
i := 0
if v.Buf.Settings["splitright"].(bool) {
i = 1
}
v.splitNode.VSplit(buf, v.Num+i)
}
// HSplitIndex opens a horizontal split with the given buffer at the given index
func (v *View) HSplitIndex(buf *Buffer, splitIndex int) {
v.splitNode.HSplit(buf, splitIndex)
}
// VSplitIndex opens a vertical split with the given buffer at the given index
func (v *View) VSplitIndex(buf *Buffer, splitIndex int) {
v.splitNode.VSplit(buf, splitIndex)
}
// GetSoftWrapLocation gets the location of a visual click on the screen and converts it to col,line
func (v *View) GetSoftWrapLocation(vx, vy int) (int, int) {
if !v.Buf.Settings["softwrap"].(bool) {
if vy >= v.Buf.NumLines {
vy = v.Buf.NumLines - 1
}
vx = v.Cursor.GetCharPosInLine(vy, vx)
return vx, vy
}
screenX, screenY := 0, v.Topline
for lineN := v.Topline; lineN < v.Bottomline(); lineN++ {
line := v.Buf.Line(lineN)
if lineN >= v.Buf.NumLines {
return 0, v.Buf.NumLines - 1
}
colN := 0
for _, ch := range line {
if screenX >= v.Width-v.lineNumOffset {
screenX = 0
screenY++
}
if screenX == vx && screenY == vy {
return colN, lineN
}
if ch == '\t' {
screenX += int(v.Buf.Settings["tabsize"].(float64)) - 1
}
screenX++
colN++
}
if screenY == vy {
return colN, lineN
}
screenX = 0
screenY++
}
return 0, 0
}
// Bottomline returns the line number of the lowest line in the view
// You might think that this is obviously just v.Topline + v.Height
// but if softwrap is enabled things get complicated since one buffer
// line can take up multiple lines in the view
func (v *View) Bottomline() int {
if !v.Buf.Settings["softwrap"].(bool) {
return v.Topline + v.Height
}
screenX, screenY := 0, 0
numLines := 0
for lineN := v.Topline; lineN < v.Topline+v.Height; lineN++ {
line := v.Buf.Line(lineN)
colN := 0
for _, ch := range line {
if screenX >= v.Width-v.lineNumOffset {
screenX = 0
screenY++
}
if ch == '\t' {
screenX += int(v.Buf.Settings["tabsize"].(float64)) - 1
}
screenX++
colN++
}
screenX = 0
screenY++
numLines++
if screenY >= v.Height {
break
}
}
return numLines + v.Topline
}
// 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() bool {
height := v.Bottomline() - v.Topline
ret := false
cy := v.Cursor.Y
scrollmargin := int(v.Buf.Settings["scrollmargin"].(float64))
if cy < v.Topline+scrollmargin && cy > scrollmargin-1 {
v.Topline = cy - scrollmargin
ret = true
} else if cy < v.Topline {
v.Topline = cy
ret = true
}
if cy > v.Topline+height-1-scrollmargin && cy < v.Buf.NumLines-scrollmargin {
v.Topline = cy - height + 1 + scrollmargin
ret = true
} else if cy >= v.Buf.NumLines-scrollmargin && cy > height {
v.Topline = v.Buf.NumLines - height
ret = true
}
if !v.Buf.Settings["softwrap"].(bool) {
cx := v.Cursor.GetVisualX()
if cx < v.leftCol {
v.leftCol = cx
ret = true
}
if cx+v.lineNumOffset+1 > v.leftCol+v.Width {
v.leftCol = cx - v.Width + v.lineNumOffset + 1
ret = true
}
}
return ret
}
// 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 < 0 {
y = 0
}
if x < 0 {
x = 0
}
x, y = v.GetSoftWrapLocation(x, y)
// x = v.Cursor.GetCharPosInLine(y, x)
if x > Count(v.Buf.Line(y)) {
x = Count(v.Buf.Line(y))
}
v.Cursor.X = x
v.Cursor.Y = y
v.Cursor.LastVisualX = v.Cursor.GetVisualX()
}
// Execute actions executes the supplied actions
func (v *View) ExecuteActions(actions []func(*View, bool) bool) bool {
relocate := false
readonlyBindingsList := []string{"Delete", "Insert", "Backspace", "Cut", "Play", "Paste", "Move", "Add", "DuplicateLine", "Macro"}
for _, action := range actions {
readonlyBindingsResult := false
funcName := ShortFuncName(action)
if v.Type.Readonly == true {
// check for readonly and if true only let key bindings get called if they do not change the contents.
for _, readonlyBindings := range readonlyBindingsList {
if strings.Contains(funcName, readonlyBindings) {
readonlyBindingsResult = true
}
}
}
if !readonlyBindingsResult {
// call the key binding
relocate = action(v, true) || relocate
// Macro
if funcName != "ToggleMacro" && funcName != "PlayMacro" {
if recordingMacro {
curMacro = append(curMacro, action)
}
}
}
}
return relocate
}
// SetCursor sets the view's and buffer's cursor
func (v *View) SetCursor(c *Cursor) bool {
if c == nil {
return false
}
v.Cursor = c
v.Buf.curCursor = c.Num
return true
}
// HandleEvent handles an event passed by the main loop
func (v *View) HandleEvent(event tcell.Event) {
if v.Type == vtTerm {
if _, ok := event.(*tcell.EventMouse); !ok {
v.pty.WriteString(event.EscSeq())
}
return
}
if v.Type == vtRaw {
v.Buf.Insert(v.Cursor.Loc, reflect.TypeOf(event).String()[7:])
v.Buf.Insert(v.Cursor.Loc, fmt.Sprintf(": %q\n", event.EscSeq()))
switch e := event.(type) {
case *tcell.EventKey:
if e.Key() == tcell.KeyCtrlQ {
v.Quit(true)
}
}
return
}
// 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
v.Buf.CheckModTime()
switch e := event.(type) {
case *tcell.EventRaw:
for key, actions := range bindings {
if key.keyCode == -1 {
if e.EscSeq() == key.escape {
for _, c := range v.Buf.cursors {
ok := v.SetCursor(c)
if !ok {
break
}
relocate = false
relocate = v.ExecuteActions(actions) || relocate
}
v.SetCursor(&v.Buf.Cursor)
v.Buf.MergeCursors()
break
}
}
}
case *tcell.EventKey:
// Check first if input is a key binding, if it is we 'eat' the input and don't insert a rune
isBinding := false
for key, actions := range bindings {
if e.Key() == key.keyCode {
if e.Key() == tcell.KeyRune {
if e.Rune() != key.r {
continue
}
}
if e.Modifiers() == key.modifiers {
for _, c := range v.Buf.cursors {
ok := v.SetCursor(c)
if !ok {
break
}
relocate = false
isBinding = true
relocate = v.ExecuteActions(actions) || relocate
}
v.SetCursor(&v.Buf.Cursor)
v.Buf.MergeCursors()
break
}
}
}
if !isBinding && e.Key() == tcell.KeyRune {
// Check viewtype if readonly don't insert a rune (readonly help and log view etc.)
if v.Type.Readonly == false {
for _, c := range v.Buf.cursors {
v.SetCursor(c)
// Insert a character
if v.Cursor.HasSelection() {
v.Cursor.DeleteSelection()
v.Cursor.ResetSelection()
}
if v.isOverwriteMode {
next := v.Cursor.Loc
next.X++
v.Buf.Replace(v.Cursor.Loc, next, string(e.Rune()))
} else {
v.Buf.Insert(v.Cursor.Loc, string(e.Rune()))
}
for pl := range loadedPlugins {
_, err := Call(pl+".onRune", string(e.Rune()), v)
if err != nil && !strings.HasPrefix(err.Error(), "function does not exist") {
TermMessage(err)
}
}
if recordingMacro {
curMacro = append(curMacro, e.Rune())
}
}
v.SetCursor(&v.Buf.Cursor)
}
}
case *tcell.EventPaste:
// Check viewtype if readonly don't paste (readonly help and log view etc.)
if v.Type.Readonly == false {
if !PreActionCall("Paste", v) {
break
}
for _, c := range v.Buf.cursors {
v.SetCursor(c)
v.paste(e.Text())
}
v.SetCursor(&v.Buf.Cursor)
PostActionCall("Paste", v)
}
case *tcell.EventMouse:
// Don't relocate for mouse events
relocate = false
button := e.Buttons()
for key, actions := range bindings {
if button == key.buttons && e.Modifiers() == key.modifiers {
for _, c := range v.Buf.cursors {
ok := v.SetCursor(c)
if !ok {
break
}
relocate = v.ExecuteActions(actions) || relocate
}
v.SetCursor(&v.Buf.Cursor)
v.Buf.MergeCursors()
}
}
for key, actions := range mouseBindings {
if button == key.buttons && e.Modifiers() == key.modifiers {
for _, action := range actions {
action(v, true, e)
}
}
}
switch button {
case tcell.ButtonNone:
// Mouse event with no click
if !v.mouseReleased {
// Mouse was just released
x, y := e.Position()
x -= v.lineNumOffset - v.leftCol + v.x
y += v.Topline - v.y
// 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
if !v.doubleClick && !v.tripleClick {
v.MoveToMouseClick(x, y)
v.Cursor.SetSelectionEnd(v.Cursor.Loc)
v.Cursor.CopySelection("primary")
}
v.mouseReleased = true
}
}
}
if relocate {
v.Relocate()
// We run relocate again because there's a bug with relocating with softwrap
// when for example you jump to the bottom of the buffer and it tries to
// calculate where to put the topline so that the bottom line is at the bottom
// of the terminal and it runs into problems with visual lines vs real lines.
// This is (hopefully) a temporary solution
v.Relocate()
}
}
func (v *View) mainCursor() bool {
return v.Buf.curCursor == len(v.Buf.cursors)-1
}
// GutterMessage creates a message in this view's gutter
func (v *View) GutterMessage(section string, lineN int, msg string, kind int) {
lineN--
gutterMsg := GutterMessage{
lineNum: lineN,
msg: msg,
kind: kind,
}
for _, v := range v.messages {
for _, gmsg := range v {
if gmsg.lineNum == lineN {
return
}
}
}
messages := v.messages[section]
v.messages[section] = append(messages, gutterMsg)
}
// ClearGutterMessages clears all gutter messages from a given section
func (v *View) ClearGutterMessages(section string) {
v.messages[section] = []GutterMessage{}
}
// ClearAllGutterMessages clears all the gutter messages
func (v *View) ClearAllGutterMessages() {
for k := range v.messages {
v.messages[k] = []GutterMessage{}
}
}
// Opens the given help page in a new horizontal split
func (v *View) openHelp(helpPage string) {
if data, err := FindRuntimeFile(RTHelp, helpPage).Data(); err != nil {
TermMessage("Unable to load help text", helpPage, "\n", err)
} else {
helpBuffer := NewBufferFromString(string(data), helpPage+".md")
helpBuffer.name = "Help"
if v.Type == vtHelp {
v.OpenBuffer(helpBuffer)
} else {
v.HSplit(helpBuffer)
CurView().Type = vtHelp
}
}
}
// DisplayTerm draws a terminal in this window
// The view's type must be vtTerm
func (v *View) DisplayTerm() {
divider := 0
if v.x != 0 {
divider = 1
dividerStyle := defStyle
if style, ok := colorscheme["divider"]; ok {
dividerStyle = style
}
for i := 0; i < v.Height; i++ {
screen.SetContent(v.x, v.y+i, '|', nil, dividerStyle.Reverse(true))
}
}
v.termState.Lock()
defer v.termState.Unlock()
for y := 0; y < v.Height; y++ {
for x := 0; x < v.Width; x++ {
c, f, b := v.termState.Cell(x, y)
fg, bg := int(f), int(b)
if f == terminal.DefaultFG {
fg = int(tcell.ColorDefault)
}
if b == terminal.DefaultBG {
bg = int(tcell.ColorDefault)
}
st := tcell.StyleDefault.Foreground(GetColor256(int(fg))).Background(GetColor256(int(bg)))
screen.SetContent(v.x+x+divider, v.y+y, c, nil, st)
}
}
if v.termState.CursorVisible() && tabs[curTab].CurView == v.Num {
curx, cury := v.termState.Cursor()
screen.ShowCursor(curx+v.x+divider, cury+v.y)
}
}
// DisplayView draws the view to the screen
func (v *View) DisplayView() {
if v.Type == vtTerm {
v.DisplayTerm()
return
}
if v.Buf.Settings["softwrap"].(bool) && v.leftCol != 0 {
v.leftCol = 0
}
if v.Type == vtLog || v.Type == vtRaw {
// Log or raw views should always follow the cursor...
v.Relocate()
}
// We need to know the string length of the largest line number
// so we can pad appropriately when displaying line numbers
maxLineNumLength := len(strconv.Itoa(v.Buf.NumLines))
if v.Buf.Settings["ruler"] == true {
// + 1 for the little space after the line number
v.lineNumOffset = maxLineNumLength + 1
} else {
v.lineNumOffset = 0
}
// We need to add to the line offset if there are gutter messages
var hasGutterMessages bool
for _, v := range v.messages {
if len(v) > 0 {
hasGutterMessages = true
}
}
if hasGutterMessages {
v.lineNumOffset += 2
}
divider := 0
if v.x != 0 {
// One space for the extra split divider
v.lineNumOffset++
divider = 1
}
xOffset := v.x + v.lineNumOffset
yOffset := v.y
height := v.Height
width := v.Width
left := v.leftCol
top := v.Topline
v.cellview.Draw(v.Buf, top, height, left, width-v.lineNumOffset)
screenX := v.x
realLineN := top - 1
visualLineN := 0
var line []*Char
for visualLineN, line = range v.cellview.lines {
var firstChar *Char
if len(line) > 0 {
firstChar = line[0]
}
var softwrapped bool
if firstChar != nil {
if firstChar.realLoc.Y == realLineN {
softwrapped = true
}
realLineN = firstChar.realLoc.Y
} else {
realLineN++
}
colorcolumn := int(v.Buf.Settings["colorcolumn"].(float64))
if colorcolumn != 0 && xOffset+colorcolumn-v.leftCol < v.Width {
style := GetColor("color-column")
fg, _, _ := style.Decompose()
st := defStyle.Background(fg)
screen.SetContent(xOffset+colorcolumn-v.leftCol, yOffset+visualLineN, ' ', nil, st)
}
screenX = v.x
// If there are gutter messages we need to display the '>>' symbol here
if hasGutterMessages {
// msgOnLine stores whether or not there is a gutter message on this line in particular
msgOnLine := false
for k := range v.messages {
for _, msg := range v.messages[k] {
if msg.lineNum == realLineN {
msgOnLine = true
gutterStyle := defStyle
switch msg.kind {
case GutterInfo:
if style, ok := colorscheme["gutter-info"]; ok {
gutterStyle = style
}
case GutterWarning:
if style, ok := colorscheme["gutter-warning"]; ok {
gutterStyle = style
}
case GutterError:
if style, ok := colorscheme["gutter-error"]; ok {
gutterStyle = style
}
}
screen.SetContent(screenX, yOffset+visualLineN, '>', nil, gutterStyle)
screenX++
screen.SetContent(screenX, yOffset+visualLineN, '>', nil, gutterStyle)
screenX++
if v.Cursor.Y == realLineN && !messenger.hasPrompt {
messenger.Message(msg.msg)
messenger.gutterMessage = true
}
}
}
}
// If there is no message on this line we just display an empty offset
if !msgOnLine {
screen.SetContent(screenX, yOffset+visualLineN, ' ', nil, defStyle)
screenX++
screen.SetContent(screenX, yOffset+visualLineN, ' ', nil, defStyle)
screenX++
if v.Cursor.Y == realLineN && messenger.gutterMessage {
messenger.Reset()
messenger.gutterMessage = false
}
}
}
lineNumStyle := defStyle
if v.Buf.Settings["ruler"] == true {
// Write the line number
if style, ok := colorscheme["line-number"]; ok {
lineNumStyle = style
}
if style, ok := colorscheme["current-line-number"]; ok {
if realLineN == v.Cursor.Y && tabs[curTab].CurView == v.Num && !v.Cursor.HasSelection() {
lineNumStyle = style
}
}
lineNum := strconv.Itoa(realLineN + 1)
// Write the spaces before the line number if necessary
for i := 0; i < maxLineNumLength-len(lineNum); i++ {
screen.SetContent(screenX+divider, yOffset+visualLineN, ' ', nil, lineNumStyle)
screenX++
}
if softwrapped && visualLineN != 0 {
// Pad without the line number because it was written on the visual line before
for range lineNum {
screen.SetContent(screenX+divider, yOffset+visualLineN, ' ', nil, lineNumStyle)
screenX++
}
} else {
// Write the actual line number
for _, ch := range lineNum {
screen.SetContent(screenX+divider, yOffset+visualLineN, ch, nil, lineNumStyle)
screenX++
}
}
// Write the extra space
screen.SetContent(screenX+divider, yOffset+visualLineN, ' ', nil, lineNumStyle)
screenX++
}
var lastChar *Char
cursorSet := false
for _, char := range line {
if char != nil {
lineStyle := char.style
colorcolumn := int(v.Buf.Settings["colorcolumn"].(float64))
if colorcolumn != 0 && char.visualLoc.X == colorcolumn {
style := GetColor("color-column")
fg, _, _ := style.Decompose()
lineStyle = lineStyle.Background(fg)
}
charLoc := char.realLoc
for _, c := range v.Buf.cursors {
v.SetCursor(c)
if v.Cursor.HasSelection() &&
(charLoc.GreaterEqual(v.Cursor.CurSelection[0]) && charLoc.LessThan(v.Cursor.CurSelection[1]) ||
charLoc.LessThan(v.Cursor.CurSelection[0]) && charLoc.GreaterEqual(v.Cursor.CurSelection[1])) {
// The current character is selected
lineStyle = defStyle.Reverse(true)
if style, ok := colorscheme["selection"]; ok {
lineStyle = style
}
}
}
v.SetCursor(&v.Buf.Cursor)
if v.Buf.Settings["cursorline"].(bool) && tabs[curTab].CurView == v.Num &&
!v.Cursor.HasSelection() && v.Cursor.Y == realLineN {
style := GetColor("cursor-line")
fg, _, _ := style.Decompose()
lineStyle = lineStyle.Background(fg)
}
screen.SetContent(xOffset+char.visualLoc.X, yOffset+char.visualLoc.Y, char.drawChar, nil, lineStyle)
for i, c := range v.Buf.cursors {
v.SetCursor(c)
if tabs[curTab].CurView == v.Num && !v.Cursor.HasSelection() &&
v.Cursor.Y == char.realLoc.Y && v.Cursor.X == char.realLoc.X && (!cursorSet || i != 0) {
ShowMultiCursor(xOffset+char.visualLoc.X, yOffset+char.visualLoc.Y, i)
cursorSet = true
}
}
v.SetCursor(&v.Buf.Cursor)
lastChar = char
}
}
lastX := 0
var realLoc Loc
var visualLoc Loc
var cx, cy int
if lastChar != nil {
lastX = xOffset + lastChar.visualLoc.X + lastChar.width
for i, c := range v.Buf.cursors {
v.SetCursor(c)
if tabs[curTab].CurView == v.Num && !v.Cursor.HasSelection() &&
v.Cursor.Y == lastChar.realLoc.Y && v.Cursor.X == lastChar.realLoc.X+1 {
ShowMultiCursor(lastX, yOffset+lastChar.visualLoc.Y, i)
cx, cy = lastX, yOffset+lastChar.visualLoc.Y
}
}
v.SetCursor(&v.Buf.Cursor)
realLoc = Loc{lastChar.realLoc.X + 1, realLineN}
visualLoc = Loc{lastX - xOffset, lastChar.visualLoc.Y}
} else if len(line) == 0 {
for i, c := range v.Buf.cursors {
v.SetCursor(c)
if tabs[curTab].CurView == v.Num && !v.Cursor.HasSelection() &&
v.Cursor.Y == realLineN {
ShowMultiCursor(xOffset, yOffset+visualLineN, i)
cx, cy = xOffset, yOffset+visualLineN
}
}
v.SetCursor(&v.Buf.Cursor)
lastX = xOffset
realLoc = Loc{0, realLineN}
visualLoc = Loc{0, visualLineN}
}
if v.Cursor.HasSelection() &&
(realLoc.GreaterEqual(v.Cursor.CurSelection[0]) && realLoc.LessThan(v.Cursor.CurSelection[1]) ||
realLoc.LessThan(v.Cursor.CurSelection[0]) && realLoc.GreaterEqual(v.Cursor.CurSelection[1])) {
// The current character is selected
selectStyle := defStyle.Reverse(true)
if style, ok := colorscheme["selection"]; ok {
selectStyle = style
}
screen.SetContent(xOffset+visualLoc.X, yOffset+visualLoc.Y, ' ', nil, selectStyle)
}
if v.Buf.Settings["cursorline"].(bool) && tabs[curTab].CurView == v.Num &&
!v.Cursor.HasSelection() && v.Cursor.Y == realLineN {
for i := lastX; i < xOffset+v.Width-v.lineNumOffset; i++ {
style := GetColor("cursor-line")
fg, _, _ := style.Decompose()
style = style.Background(fg)
if !(tabs[curTab].CurView == v.Num && !v.Cursor.HasSelection() && i == cx && yOffset+visualLineN == cy) {
screen.SetContent(i, yOffset+visualLineN, ' ', nil, style)
}
}
}
}
if divider != 0 {
dividerStyle := defStyle
if style, ok := colorscheme["divider"]; ok {
dividerStyle = style
}
for i := 0; i < v.Height; i++ {
screen.SetContent(v.x, yOffset+i, '|', nil, dividerStyle.Reverse(true))
}
}
}
// ShowMultiCursor will display a cursor at a location
// If i == 0 then the terminal cursor will be used
// Otherwise a fake cursor will be drawn at the position
func ShowMultiCursor(x, y, i int) {
if i == 0 {
screen.ShowCursor(x, y)
} else {
r, _, _, _ := screen.GetContent(x, y)
screen.SetContent(x, y, r, nil, defStyle.Reverse(true))
}
}
// Display renders the view, the cursor, and statusline
func (v *View) Display() {
if globalSettings["termtitle"].(bool) {
screen.SetTitle("micro: " + v.Buf.GetName())
}
v.DisplayView()
// Don't draw the cursor if it is out of the viewport or if it has a selection
if v.Num == tabs[curTab].CurView && (v.Cursor.Y-v.Topline < 0 || v.Cursor.Y-v.Topline > v.Height-1 || v.Cursor.HasSelection()) {
screen.HideCursor()
}
_, screenH := screen.Size()
if v.Buf.Settings["scrollbar"].(bool) {
v.scrollbar.Display()
}
if v.Buf.Settings["statusline"].(bool) {
v.sline.Display()
} else if (v.y + v.Height) != screenH-1 {
for x := 0; x < v.Width; x++ {
screen.SetContent(v.x+x, v.y+v.Height, '-', nil, defStyle.Reverse(true))
}
}
}