diff --git a/cmd/micro/action/actions.go b/cmd/micro/action/actions.go index ef6f59d7..425142dd 100644 --- a/cmd/micro/action/actions.go +++ b/cmd/micro/action/actions.go @@ -8,6 +8,7 @@ import ( "github.com/zyedidia/clipboard" "github.com/zyedidia/micro/cmd/micro/buffer" + "github.com/zyedidia/micro/cmd/micro/config" "github.com/zyedidia/micro/cmd/micro/screen" "github.com/zyedidia/micro/cmd/micro/util" "github.com/zyedidia/tcell" @@ -34,6 +35,57 @@ func (h *BufHandler) ScrollDown(n int) { // MousePress is the event that should happen when a normal click happens // This is almost always bound to left click func (h *BufHandler) MousePress(e *tcell.EventMouse) bool { + b := h.Buf + mx, my := e.Position() + mouseLoc := h.Win.GetMouseLoc(buffer.Loc{mx, my}) + h.Cursor.Loc = mouseLoc + if h.mouseReleased { + if b.NumCursors() > 1 { + b.ClearCursors() + h.Win.Relocate() + } + if time.Since(h.lastClickTime)/time.Millisecond < config.DoubleClickThreshold && (mouseLoc.X == h.lastLoc.X && mouseLoc.Y == h.lastLoc.Y) { + if h.doubleClick { + // Triple click + h.lastClickTime = time.Now() + + h.tripleClick = true + h.doubleClick = false + + h.Cursor.SelectLine() + h.Cursor.CopySelection("primary") + } else { + // Double click + h.lastClickTime = time.Now() + + h.doubleClick = true + h.tripleClick = false + + h.Cursor.SelectWord() + h.Cursor.CopySelection("primary") + } + } else { + h.doubleClick = false + h.tripleClick = false + h.lastClickTime = time.Now() + + h.Cursor.OrigSelection[0] = h.Cursor.Loc + h.Cursor.CurSelection[0] = h.Cursor.Loc + h.Cursor.CurSelection[1] = h.Cursor.Loc + } + h.mouseReleased = false + } else if !h.mouseReleased { + if h.tripleClick { + h.Cursor.AddLineToSelection() + } else if h.doubleClick { + h.Cursor.AddWordToSelection() + } else { + h.Cursor.SetSelectionEnd(h.Cursor.Loc) + h.Cursor.CopySelection("primary") + } + } + + h.lastLoc = mouseLoc return false } diff --git a/cmd/micro/action/bufhandler.go b/cmd/micro/action/bufhandler.go index 3f2909fa..aa49a9e1 100644 --- a/cmd/micro/action/bufhandler.go +++ b/cmd/micro/action/bufhandler.go @@ -90,6 +90,7 @@ func NewBufHandler(buf *buffer.Buffer, win display.Window) *BufHandler { h.cursors = []*buffer.Cursor{buffer.NewCursor(buf, buf.StartCursor)} h.Cursor = h.cursors[0] + h.mouseReleased = true buf.SetCursors(h.cursors) return h @@ -105,16 +106,51 @@ func (h *BufHandler) HandleEvent(event tcell.Event) { mod: e.Modifiers(), r: e.Rune(), } - done := h.DoKeyEvent(ke) - if !done && e.Key() == tcell.KeyRune { - h.DoRuneInsert(e.Rune()) + cursors := h.Buf.GetCursors() + for _, c := range cursors { + h.Buf.SetCurCursor(c.Num) + h.Cursor = c + done := h.DoKeyEvent(ke) + if !done && e.Key() == tcell.KeyRune { + h.DoRuneInsert(e.Rune()) + } } + // TODO: maybe reset curcursor to 0 case *tcell.EventMouse: + switch e.Buttons() { + case tcell.ButtonNone: + // Mouse event with no click + if !h.mouseReleased { + // Mouse was just released + + mx, my := e.Position() + mouseLoc := h.Win.GetMouseLoc(buffer.Loc{X: mx, Y: my}) + + // 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 !h.doubleClick && !h.tripleClick { + h.Cursor.Loc = mouseLoc + h.Cursor.SetSelectionEnd(h.Cursor.Loc) + h.Cursor.CopySelection("primary") + } + h.mouseReleased = true + } + } + me := MouseEvent{ btn: e.Buttons(), mod: e.Modifiers(), } - h.DoMouseEvent(me, e) + cursors := h.Buf.GetCursors() + for _, c := range cursors { + h.Buf.SetCurCursor(c.Num) + h.Cursor = c + h.DoMouseEvent(me, e) + } } } diff --git a/cmd/micro/buffer/buffer.go b/cmd/micro/buffer/buffer.go index 5dbe637c..91373ba0 100644 --- a/cmd/micro/buffer/buffer.go +++ b/cmd/micro/buffer/buffer.go @@ -77,6 +77,7 @@ type Buffer struct { *EventHandler cursors []*Cursor + curCursor int StartCursor Loc // Path to the file on disk @@ -229,9 +230,10 @@ func (b *Buffer) ReOpen() error { b.ModTime, err = GetModTime(b.Path) b.isModified = false + for _, c := range b.cursors { + c.Relocate() + } return err - // TODO: buffer cursor - // b.Cursor.Relocate() } // SetCursors resets this buffer's cursors to a new list @@ -239,9 +241,14 @@ func (b *Buffer) SetCursors(c []*Cursor) { b.cursors = c } +// SetCurCursor sets the current cursor +func (b *Buffer) SetCurCursor(n int) { + b.curCursor = n +} + // GetActiveCursor returns the main cursor in this buffer func (b *Buffer) GetActiveCursor() *Cursor { - return b.cursors[0] + return b.cursors[b.curCursor] } // GetCursor returns the nth cursor @@ -421,3 +428,49 @@ func (b *Buffer) IndentString(tabsize int) string { } return "\t" } + +// MergeCursors merges any cursors that are at the same position +// into one cursor +func (b *Buffer) MergeCursors() { + var cursors []*Cursor + for i := 0; i < len(b.cursors); i++ { + c1 := b.cursors[i] + if c1 != nil { + for j := 0; j < len(b.cursors); j++ { + c2 := b.cursors[j] + if c2 != nil && i != j && c1.Loc == c2.Loc { + b.cursors[j] = nil + } + } + cursors = append(cursors, c1) + } + } + + b.cursors = cursors + + for i := range b.cursors { + b.cursors[i].Num = i + } + + if b.curCursor >= len(b.cursors) { + b.curCursor = len(b.cursors) - 1 + } +} + +// UpdateCursors updates all the cursors indicies +func (b *Buffer) UpdateCursors() { + for i, c := range b.cursors { + c.Num = i + } +} + +// ClearCursors removes all extra cursors +func (b *Buffer) ClearCursors() { + for i := 1; i < len(b.cursors); i++ { + b.cursors[i] = nil + } + b.cursors = b.cursors[:1] + b.UpdateCursors() + b.curCursor = 0 + b.GetActiveCursor().ResetSelection() +} diff --git a/cmd/micro/config/globals.go b/cmd/micro/config/globals.go new file mode 100644 index 00000000..841ff68f --- /dev/null +++ b/cmd/micro/config/globals.go @@ -0,0 +1,6 @@ +package config + +const ( + DoubleClickThreshold = 400 // How many milliseconds to wait before a second click is not a double click + AutosaveTime = 8 // Number of seconds to wait before autosaving +) diff --git a/cmd/micro/display/infowindow.go b/cmd/micro/display/infowindow.go index d8f2fffe..cc696f62 100644 --- a/cmd/micro/display/infowindow.go +++ b/cmd/micro/display/infowindow.go @@ -81,6 +81,13 @@ func (i *InfoWindow) Relocate() bool { return false } func (i *InfoWindow) GetView() *View { return i.View } func (i *InfoWindow) SetView(v *View) {} +func (i *InfoWindow) GetMouseLoc(vloc buffer.Loc) buffer.Loc { + c := i.Buffer.GetActiveCursor() + l := i.Buffer.LineBytes(0) + n := utf8.RuneCountInString(i.Msg) + return buffer.Loc{c.GetCharPosInLine(l, vloc.X-n), 0} +} + func (i *InfoWindow) Clear() { for x := 0; x < i.width; x++ { screen.Screen.SetContent(x, i.y, ' ', nil, config.DefStyle) diff --git a/cmd/micro/display/window.go b/cmd/micro/display/window.go index 0e14c348..3bcf6bd2 100644 --- a/cmd/micro/display/window.go +++ b/cmd/micro/display/window.go @@ -24,6 +24,7 @@ type Window interface { Relocate() bool GetView() *View SetView(v *View) + GetMouseLoc(vloc buffer.Loc) buffer.Loc } // The BufWindow provides a way of displaying a certain section @@ -75,6 +76,7 @@ func (w *BufWindow) Clear() { func (w *BufWindow) Bottomline() int { // b := w.Buf + // TODO: possible non-softwrap optimization // if !b.Settings["softwrap"].(bool) { // return w.StartLine + w.Height // } @@ -130,6 +132,118 @@ func (w *BufWindow) Relocate() bool { return ret } +func (w *BufWindow) GetMouseLoc(svloc buffer.Loc) buffer.Loc { + b := w.Buf + + // TODO: possible non-softwrap optimization + // if !b.Settings["softwrap"].(bool) { + // l := b.LineBytes(svloc.Y) + // return buffer.Loc{b.GetActiveCursor().GetCharPosInLine(l, svloc.X), svloc.Y} + // } + + bufHeight := w.Height + if b.Settings["statusline"].(bool) { + bufHeight-- + } + + // 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(b.LinesNum())) + + tabsize := int(b.Settings["tabsize"].(float64)) + softwrap := b.Settings["softwrap"].(bool) + + // this represents the current draw position + // within the current window + vloc := buffer.Loc{X: 0, Y: 0} + + // this represents the current draw position in the buffer (char positions) + bloc := buffer.Loc{X: w.StartCol, Y: w.StartLine} + + for vloc.Y = 0; vloc.Y < bufHeight; vloc.Y++ { + vloc.X = 0 + if b.Settings["ruler"].(bool) { + vloc.X += maxLineNumLength + 1 + } + + if svloc.X <= vloc.X && vloc.Y == svloc.Y { + return bloc + } + + line := b.LineBytes(bloc.Y) + line, nColsBeforeStart := util.SliceVisualEnd(line, bloc.X, tabsize) + + draw := func() { + if nColsBeforeStart <= 0 { + vloc.X++ + } + nColsBeforeStart-- + } + + w.lineHeight[vloc.Y] = bloc.Y + + totalwidth := bloc.X - nColsBeforeStart + for len(line) > 0 { + if vloc.X == svloc.X && vloc.Y == svloc.Y { + return bloc + } + + r, size := utf8.DecodeRune(line) + draw() + width := 0 + + switch r { + case '\t': + ts := tabsize - (totalwidth % tabsize) + width = ts + default: + width = runewidth.RuneWidth(r) + } + + // Draw any extra characters either spaces for tabs or @ for incomplete wide runes + if width > 1 { + for i := 1; i < width; i++ { + if vloc.X == svloc.X && vloc.Y == svloc.Y { + return bloc + } + draw() + } + } + bloc.X++ + line = line[size:] + + totalwidth += width + + // If we reach the end of the window then we either stop or we wrap for softwrap + if vloc.X >= w.Width { + if !softwrap { + break + } else { + vloc.Y++ + if vloc.Y >= bufHeight { + break + } + vloc.X = 0 + w.lineHeight[vloc.Y] = bloc.Y + // This will draw an empty line number because the current line is wrapped + vloc.X += maxLineNumLength + 1 + } + } + } + if vloc.Y == svloc.Y { + return bloc + } + + bloc.X = w.StartCol + bloc.Y++ + if bloc.Y >= b.LinesNum() { + break + } + } + + return buffer.Loc{X: -1, Y: -1} +} + func (w *BufWindow) drawLineNum(lineNumStyle tcell.Style, softwrapped bool, maxLineNumLength int, vloc *buffer.Loc, bloc *buffer.Loc) { lineNum := strconv.Itoa(bloc.Y + 1) diff --git a/cmd/micro/micro.go b/cmd/micro/micro.go index ecee6847..468b63d9 100644 --- a/cmd/micro/micro.go +++ b/cmd/micro/micro.go @@ -17,11 +17,6 @@ import ( "github.com/zyedidia/tcell" ) -const ( - doubleClickThreshold = 400 // How many milliseconds to wait before a second click is not a double click - autosaveTime = 8 // Number of seconds to wait before autosaving -) - var ( // These variables should be set by the linker when compiling