micro/internal/action/bufpane.go

870 lines
26 KiB
Go
Raw Normal View History

2018-08-27 23:53:08 +00:00
package action
import (
"strings"
2018-08-27 23:53:08 +00:00
"time"
2019-03-19 22:28:51 +00:00
luar "layeh.com/gopher-luar"
2019-08-03 22:49:05 +00:00
lua "github.com/yuin/gopher-lua"
2020-05-04 14:16:15 +00:00
"github.com/zyedidia/micro/v2/internal/buffer"
"github.com/zyedidia/micro/v2/internal/clipboard"
2020-05-04 14:16:15 +00:00
"github.com/zyedidia/micro/v2/internal/config"
"github.com/zyedidia/micro/v2/internal/display"
ulua "github.com/zyedidia/micro/v2/internal/lua"
"github.com/zyedidia/micro/v2/internal/screen"
Improve buffer view relocation after jumping to a far-away location (#2628) * Improve buffer view relocation after jumping to a far-away location When the cursor is moved to a location which is far away from the current location (e.g. after a search or a goto line), the buffer view is always relocated in such a way that the cursor is at the bottom or at the top (minus scrollmargin), i.e. as if we just scrolled to this location. It's not like in other editors, and IMHO it's annoying. When we jump to a new location far away, we usually want to see more of its context, so the cursor should be placed closer to the center of the view, not near its edges. This change implements the behavior similar to other editors: - If the distance between the new and the old location is less than one frame (i.e. the view either doesn't change or just slightly "shifts") then the current behavior remains unchanged. - Otherwise the current line is placed at 25% of the window height. * Postpone calling onBufPaneOpen until the initial resize It is currently not possible to find out the geometry of a newly created bufpane in onBufPaneOpen lua callback: bp:GetView() returns {0,0,0,0} instead of the actual window. The reason is that the bufpane view is not properly initialized yet when the bufpane is created and the callback is triggered. It is initialized a bit later, at the initial resize. So postpone calling onBufPaneOpen until after the initial resize. * Improve buffer view relocation when opening a file at a far-away location When a file is opened with the initial cursor location at a given line which is far away from the beginning of the file, the buffer view is relocated so that the cursor is at the bottom (minus scrollmargin) as if we just scrolled to this line, which is annoying since we'd rather like to see more of the context of this initial location. So implement the behavior similar to the earlier commit (which addresses a similar issue about jumping far away after a search or goto): - If the initial cursor location is less than one frame away from the beginning of the buffer, keep the existing behavior i.e. just display the beginning of the buffer. - Otherwise place the cursor location at 25% of the window height.
2022-12-03 03:38:09 +00:00
"github.com/zyedidia/micro/v2/internal/util"
2020-09-05 18:52:35 +00:00
"github.com/zyedidia/tcell/v2"
2018-08-27 23:53:08 +00:00
)
2021-08-21 21:58:30 +00:00
// BufKeyAction represents an action bound to a key.
2019-01-19 20:37:59 +00:00
type BufKeyAction func(*BufPane) bool
2021-08-21 21:58:30 +00:00
// BufMouseAction is an action that must be bound to a mouse event.
2019-01-19 20:37:59 +00:00
type BufMouseAction func(*BufPane, *tcell.EventMouse) bool
2018-08-27 23:53:08 +00:00
2021-08-21 21:58:30 +00:00
// BufBindings stores the bindings for the buffer pane type.
var BufBindings *KeyTree
2018-08-27 23:53:08 +00:00
2021-08-21 21:58:30 +00:00
// BufKeyActionGeneral makes a general pane action from a BufKeyAction.
func BufKeyActionGeneral(a BufKeyAction) PaneKeyAction {
return func(p Pane) bool {
return a(p.(*BufPane))
}
}
2021-08-21 21:58:30 +00:00
// BufMouseActionGeneral makes a general pane mouse action from a BufKeyAction.
func BufMouseActionGeneral(a BufMouseAction) PaneMouseAction {
return func(p Pane, me *tcell.EventMouse) bool {
return a(p.(*BufPane), me)
}
}
2018-08-27 23:53:08 +00:00
func init() {
BufBindings = NewKeyTree()
2018-08-27 23:53:08 +00:00
}
2021-08-21 21:58:30 +00:00
// LuaAction makes a BufKeyAction from a lua function.
2019-08-03 22:49:05 +00:00
func LuaAction(fn string) func(*BufPane) bool {
luaFn := strings.Split(fn, ".")
2019-08-26 18:47:27 +00:00
if len(luaFn) <= 1 {
return nil
}
2019-08-03 22:49:05 +00:00
plName, plFn := luaFn[0], luaFn[1]
pl := config.FindPlugin(plName)
2019-08-26 18:47:27 +00:00
if pl == nil {
return nil
}
2019-08-03 22:49:05 +00:00
return func(h *BufPane) bool {
val, err := pl.Call(plFn, luar.New(ulua.L, h))
if err != nil {
screen.TermMessage(err)
}
if v, ok := val.(lua.LBool); !ok {
return false
} else {
return bool(v)
}
}
}
// BufMapKey maps an event to an action
func BufMapEvent(k Event, action string) {
config.Bindings["buffer"][k.Name()] = action
switch e := k.(type) {
case KeyEvent, KeySequenceEvent, RawEvent:
bufMapKey(e, action)
case MouseEvent:
bufMapMouse(e, action)
}
}
func bufMapKey(k Event, action string) {
var actionfns []func(*BufPane) bool
var names []string
var types []byte
for i := 0; ; i++ {
if action == "" {
break
}
2019-12-27 05:43:45 +00:00
// TODO: fix problem when complex bindings have these
// characters (escape them?)
idx := strings.IndexAny(action, "&|,")
a := action
if idx >= 0 {
a = action[:idx]
types = append(types, action[idx])
action = action[idx+1:]
} else {
types = append(types, ' ')
action = ""
}
2019-08-17 18:49:42 +00:00
var afn func(*BufPane) bool
2019-12-27 05:43:45 +00:00
if strings.HasPrefix(a, "command:") {
2019-08-17 18:49:42 +00:00
a = strings.SplitN(a, ":", 2)[1]
afn = CommandAction(a)
names = append(names, "")
2019-08-17 18:49:42 +00:00
} else if strings.HasPrefix(a, "command-edit:") {
a = strings.SplitN(a, ":", 2)[1]
afn = CommandEditAction(a)
names = append(names, "")
2019-08-17 18:49:42 +00:00
} else if strings.HasPrefix(a, "lua:") {
a = strings.SplitN(a, ":", 2)[1]
afn = LuaAction(a)
if afn == nil {
2019-12-27 05:43:45 +00:00
screen.TermMessage("Lua Error:", a, "does not exist")
continue
}
2020-02-06 16:12:34 +00:00
split := strings.SplitN(a, ".", 2)
if len(split) > 1 {
a = strings.Title(split[0]) + strings.Title(split[1])
} else {
a = strings.Title(a)
}
names = append(names, a)
2019-08-17 18:49:42 +00:00
} else if f, ok := BufKeyActions[a]; ok {
afn = f
names = append(names, a)
2019-08-17 18:49:42 +00:00
} else {
2020-04-25 21:01:16 +00:00
screen.TermMessage("Error in bindings: action", a, "does not exist")
continue
2019-08-17 18:49:42 +00:00
}
actionfns = append(actionfns, afn)
2019-08-17 18:49:42 +00:00
}
bufAction := func(h *BufPane) bool {
cursors := h.Buf.GetCursors()
success := true
for i, a := range actionfns {
innerSuccess := true
for j, c := range cursors {
if c == nil {
continue
}
h.Buf.SetCurCursor(c.Num)
h.Cursor = c
if i == 0 || (success && types[i-1] == '&') || (!success && types[i-1] == '|') || (types[i-1] == ',') {
innerSuccess = innerSuccess && h.execAction(a, names[i], j)
} else {
break
}
}
// if the action changed the current pane, update the reference
h = MainTab().CurPane()
success = innerSuccess
2019-08-17 18:49:42 +00:00
}
return true
2018-08-29 03:30:39 +00:00
}
BufBindings.RegisterKeyBinding(k, BufKeyActionGeneral(bufAction))
2018-08-27 23:53:08 +00:00
}
2019-01-03 20:27:43 +00:00
// BufMapMouse maps a mouse event to an action
func bufMapMouse(k MouseEvent, action string) {
2018-08-29 03:30:39 +00:00
if f, ok := BufMouseActions[action]; ok {
BufBindings.RegisterMouseBinding(k, BufMouseActionGeneral(f))
2018-08-29 03:30:39 +00:00
} else {
// TODO
// delete(BufMouseBindings, k)
bufMapKey(k, action)
2018-08-29 03:30:39 +00:00
}
2018-08-27 23:53:08 +00:00
}
// BufUnmap unmaps a key or mouse event from any action
func BufUnmap(k Event) {
// TODO
// delete(BufKeyBindings, k)
//
// switch e := k.(type) {
// case MouseEvent:
// delete(BufMouseBindings, e)
// }
}
2019-01-19 20:37:59 +00:00
// The BufPane connects the buffer and the window
2018-08-27 23:53:08 +00:00
// It provides a cursor (or multiple) and defines a set of actions
// that can be taken on the buffer
// The ActionHandler can access the window for necessary info about
// visual positions for mouse clicks and scrolling
2019-01-19 20:37:59 +00:00
type BufPane struct {
2019-01-14 05:57:39 +00:00
display.BWindow
2019-01-11 02:26:58 +00:00
// Buf is the buffer this BufPane views
2018-08-27 23:53:08 +00:00
Buf *buffer.Buffer
// Bindings stores the association of key events and actions
bindings *KeyTree
2018-08-27 23:53:08 +00:00
// Cursor is the currently active buffer cursor
Cursor *buffer.Cursor
2018-08-27 23:53:08 +00:00
// 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 buffer.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
2019-01-03 20:35:24 +00:00
2019-01-05 02:48:19 +00:00
// Should the current multiple cursor selection search based on word or
// based on selection (false for selection, true for word)
multiWord bool
2019-01-04 22:40:56 +00:00
splitID uint64
2020-02-05 22:16:31 +00:00
tab *Tab
2019-06-15 18:44:03 +00:00
// remember original location of a search in case the search is canceled
searchOrig buffer.Loc
Improve buffer view relocation after jumping to a far-away location (#2628) * Improve buffer view relocation after jumping to a far-away location When the cursor is moved to a location which is far away from the current location (e.g. after a search or a goto line), the buffer view is always relocated in such a way that the cursor is at the bottom or at the top (minus scrollmargin), i.e. as if we just scrolled to this location. It's not like in other editors, and IMHO it's annoying. When we jump to a new location far away, we usually want to see more of its context, so the cursor should be placed closer to the center of the view, not near its edges. This change implements the behavior similar to other editors: - If the distance between the new and the old location is less than one frame (i.e. the view either doesn't change or just slightly "shifts") then the current behavior remains unchanged. - Otherwise the current line is placed at 25% of the window height. * Postpone calling onBufPaneOpen until the initial resize It is currently not possible to find out the geometry of a newly created bufpane in onBufPaneOpen lua callback: bp:GetView() returns {0,0,0,0} instead of the actual window. The reason is that the bufpane view is not properly initialized yet when the bufpane is created and the callback is triggered. It is initialized a bit later, at the initial resize. So postpone calling onBufPaneOpen until after the initial resize. * Improve buffer view relocation when opening a file at a far-away location When a file is opened with the initial cursor location at a given line which is far away from the beginning of the file, the buffer view is relocated so that the cursor is at the bottom (minus scrollmargin) as if we just scrolled to this line, which is annoying since we'd rather like to see more of the context of this initial location. So implement the behavior similar to the earlier commit (which addresses a similar issue about jumping far away after a search or goto): - If the initial cursor location is less than one frame away from the beginning of the buffer, keep the existing behavior i.e. just display the beginning of the buffer. - Otherwise place the cursor location at 25% of the window height.
2022-12-03 03:38:09 +00:00
// The pane may not yet be fully initialized after its creation
// since we may not know the window geometry yet. In such case we finish
// its initialization a bit later, after the initial resize.
initialized bool
2018-08-27 23:53:08 +00:00
}
Improve buffer view relocation after jumping to a far-away location (#2628) * Improve buffer view relocation after jumping to a far-away location When the cursor is moved to a location which is far away from the current location (e.g. after a search or a goto line), the buffer view is always relocated in such a way that the cursor is at the bottom or at the top (minus scrollmargin), i.e. as if we just scrolled to this location. It's not like in other editors, and IMHO it's annoying. When we jump to a new location far away, we usually want to see more of its context, so the cursor should be placed closer to the center of the view, not near its edges. This change implements the behavior similar to other editors: - If the distance between the new and the old location is less than one frame (i.e. the view either doesn't change or just slightly "shifts") then the current behavior remains unchanged. - Otherwise the current line is placed at 25% of the window height. * Postpone calling onBufPaneOpen until the initial resize It is currently not possible to find out the geometry of a newly created bufpane in onBufPaneOpen lua callback: bp:GetView() returns {0,0,0,0} instead of the actual window. The reason is that the bufpane view is not properly initialized yet when the bufpane is created and the callback is triggered. It is initialized a bit later, at the initial resize. So postpone calling onBufPaneOpen until after the initial resize. * Improve buffer view relocation when opening a file at a far-away location When a file is opened with the initial cursor location at a given line which is far away from the beginning of the file, the buffer view is relocated so that the cursor is at the bottom (minus scrollmargin) as if we just scrolled to this line, which is annoying since we'd rather like to see more of the context of this initial location. So implement the behavior similar to the earlier commit (which addresses a similar issue about jumping far away after a search or goto): - If the initial cursor location is less than one frame away from the beginning of the buffer, keep the existing behavior i.e. just display the beginning of the buffer. - Otherwise place the cursor location at 25% of the window height.
2022-12-03 03:38:09 +00:00
func newBufPane(buf *buffer.Buffer, win display.BWindow, tab *Tab) *BufPane {
2019-01-19 20:37:59 +00:00
h := new(BufPane)
2018-08-28 22:44:52 +00:00
h.Buf = buf
2019-01-14 05:57:39 +00:00
h.BWindow = win
2020-02-05 22:16:31 +00:00
h.tab = tab
2018-08-27 23:53:08 +00:00
h.Cursor = h.Buf.GetActiveCursor()
2019-01-02 22:39:50 +00:00
h.mouseReleased = true
2018-08-27 23:53:08 +00:00
Improve buffer view relocation after jumping to a far-away location (#2628) * Improve buffer view relocation after jumping to a far-away location When the cursor is moved to a location which is far away from the current location (e.g. after a search or a goto line), the buffer view is always relocated in such a way that the cursor is at the bottom or at the top (minus scrollmargin), i.e. as if we just scrolled to this location. It's not like in other editors, and IMHO it's annoying. When we jump to a new location far away, we usually want to see more of its context, so the cursor should be placed closer to the center of the view, not near its edges. This change implements the behavior similar to other editors: - If the distance between the new and the old location is less than one frame (i.e. the view either doesn't change or just slightly "shifts") then the current behavior remains unchanged. - Otherwise the current line is placed at 25% of the window height. * Postpone calling onBufPaneOpen until the initial resize It is currently not possible to find out the geometry of a newly created bufpane in onBufPaneOpen lua callback: bp:GetView() returns {0,0,0,0} instead of the actual window. The reason is that the bufpane view is not properly initialized yet when the bufpane is created and the callback is triggered. It is initialized a bit later, at the initial resize. So postpone calling onBufPaneOpen until after the initial resize. * Improve buffer view relocation when opening a file at a far-away location When a file is opened with the initial cursor location at a given line which is far away from the beginning of the file, the buffer view is relocated so that the cursor is at the bottom (minus scrollmargin) as if we just scrolled to this line, which is annoying since we'd rather like to see more of the context of this initial location. So implement the behavior similar to the earlier commit (which addresses a similar issue about jumping far away after a search or goto): - If the initial cursor location is less than one frame away from the beginning of the buffer, keep the existing behavior i.e. just display the beginning of the buffer. - Otherwise place the cursor location at 25% of the window height.
2022-12-03 03:38:09 +00:00
return h
}
2019-03-19 22:28:51 +00:00
Improve buffer view relocation after jumping to a far-away location (#2628) * Improve buffer view relocation after jumping to a far-away location When the cursor is moved to a location which is far away from the current location (e.g. after a search or a goto line), the buffer view is always relocated in such a way that the cursor is at the bottom or at the top (minus scrollmargin), i.e. as if we just scrolled to this location. It's not like in other editors, and IMHO it's annoying. When we jump to a new location far away, we usually want to see more of its context, so the cursor should be placed closer to the center of the view, not near its edges. This change implements the behavior similar to other editors: - If the distance between the new and the old location is less than one frame (i.e. the view either doesn't change or just slightly "shifts") then the current behavior remains unchanged. - Otherwise the current line is placed at 25% of the window height. * Postpone calling onBufPaneOpen until the initial resize It is currently not possible to find out the geometry of a newly created bufpane in onBufPaneOpen lua callback: bp:GetView() returns {0,0,0,0} instead of the actual window. The reason is that the bufpane view is not properly initialized yet when the bufpane is created and the callback is triggered. It is initialized a bit later, at the initial resize. So postpone calling onBufPaneOpen until after the initial resize. * Improve buffer view relocation when opening a file at a far-away location When a file is opened with the initial cursor location at a given line which is far away from the beginning of the file, the buffer view is relocated so that the cursor is at the bottom (minus scrollmargin) as if we just scrolled to this line, which is annoying since we'd rather like to see more of the context of this initial location. So implement the behavior similar to the earlier commit (which addresses a similar issue about jumping far away after a search or goto): - If the initial cursor location is less than one frame away from the beginning of the buffer, keep the existing behavior i.e. just display the beginning of the buffer. - Otherwise place the cursor location at 25% of the window height.
2022-12-03 03:38:09 +00:00
// NewBufPane creates a new buffer pane with the given window.
func NewBufPane(buf *buffer.Buffer, win display.BWindow, tab *Tab) *BufPane {
h := newBufPane(buf, win, tab)
h.finishInitialize()
2018-08-28 22:44:52 +00:00
return h
2018-08-27 23:53:08 +00:00
}
2021-08-21 21:58:30 +00:00
// NewBufPaneFromBuf constructs a new pane from the given buffer and automatically
// creates a buf window.
2020-02-05 22:16:31 +00:00
func NewBufPaneFromBuf(buf *buffer.Buffer, tab *Tab) *BufPane {
2019-01-19 20:37:59 +00:00
w := display.NewBufWindow(0, 0, 0, 0, buf)
Improve buffer view relocation after jumping to a far-away location (#2628) * Improve buffer view relocation after jumping to a far-away location When the cursor is moved to a location which is far away from the current location (e.g. after a search or a goto line), the buffer view is always relocated in such a way that the cursor is at the bottom or at the top (minus scrollmargin), i.e. as if we just scrolled to this location. It's not like in other editors, and IMHO it's annoying. When we jump to a new location far away, we usually want to see more of its context, so the cursor should be placed closer to the center of the view, not near its edges. This change implements the behavior similar to other editors: - If the distance between the new and the old location is less than one frame (i.e. the view either doesn't change or just slightly "shifts") then the current behavior remains unchanged. - Otherwise the current line is placed at 25% of the window height. * Postpone calling onBufPaneOpen until the initial resize It is currently not possible to find out the geometry of a newly created bufpane in onBufPaneOpen lua callback: bp:GetView() returns {0,0,0,0} instead of the actual window. The reason is that the bufpane view is not properly initialized yet when the bufpane is created and the callback is triggered. It is initialized a bit later, at the initial resize. So postpone calling onBufPaneOpen until after the initial resize. * Improve buffer view relocation when opening a file at a far-away location When a file is opened with the initial cursor location at a given line which is far away from the beginning of the file, the buffer view is relocated so that the cursor is at the bottom (minus scrollmargin) as if we just scrolled to this line, which is annoying since we'd rather like to see more of the context of this initial location. So implement the behavior similar to the earlier commit (which addresses a similar issue about jumping far away after a search or goto): - If the initial cursor location is less than one frame away from the beginning of the buffer, keep the existing behavior i.e. just display the beginning of the buffer. - Otherwise place the cursor location at 25% of the window height.
2022-12-03 03:38:09 +00:00
h := newBufPane(buf, w, tab)
// Postpone finishing initializing the pane until we know the actual geometry
// of the buf window.
return h
}
// TODO: make sure splitID and tab are set before finishInitialize is called
func (h *BufPane) finishInitialize() {
h.initialRelocate()
h.initialized = true
config.RunPluginFn("onBufPaneOpen", luar.New(ulua.L, h))
}
// Resize resizes the pane
func (h *BufPane) Resize(width, height int) {
h.BWindow.Resize(width, height)
if !h.initialized {
h.finishInitialize()
}
2020-02-05 22:16:31 +00:00
}
2021-08-21 21:58:30 +00:00
// SetTab sets this pane's tab.
2020-02-05 22:16:31 +00:00
func (h *BufPane) SetTab(t *Tab) {
h.tab = t
}
2021-08-21 21:58:30 +00:00
// Tab returns this pane's tab.
2020-02-05 22:16:31 +00:00
func (h *BufPane) Tab() *Tab {
return h.tab
}
func (h *BufPane) ResizePane(size int) {
n := h.tab.GetNode(h.splitID)
n.ResizeSplit(size)
h.tab.Resize()
2019-01-19 20:37:59 +00:00
}
2021-08-21 21:58:30 +00:00
// PluginCB calls all plugin callbacks with a certain name and displays an
// error if there is one and returns the aggregrate boolean response
2019-08-02 21:48:59 +00:00
func (h *BufPane) PluginCB(cb string) bool {
b, err := config.RunPluginFnBool(h.Buf.Settings, cb, luar.New(ulua.L, h))
2019-08-02 21:48:59 +00:00
if err != nil {
screen.TermMessage(err)
}
return b
}
2021-08-21 21:58:30 +00:00
// PluginCBRune is the same as PluginCB but also passes a rune to the plugins
2019-08-02 21:48:59 +00:00
func (h *BufPane) PluginCBRune(cb string, r rune) bool {
b, err := config.RunPluginFnBool(h.Buf.Settings, cb, luar.New(ulua.L, h), luar.New(ulua.L, string(r)))
2019-08-02 21:48:59 +00:00
if err != nil {
screen.TermMessage(err)
}
return b
}
2021-08-21 21:58:30 +00:00
// OpenBuffer opens the given buffer in this pane.
2019-01-19 20:37:59 +00:00
func (h *BufPane) OpenBuffer(b *buffer.Buffer) {
2019-01-14 05:57:39 +00:00
h.Buf.Close()
h.Buf = b
h.BWindow.SetBuffer(b)
h.Cursor = b.GetActiveCursor()
2019-06-15 22:00:24 +00:00
h.Resize(h.GetView().Width, h.GetView().Height)
Improve buffer view relocation after jumping to a far-away location (#2628) * Improve buffer view relocation after jumping to a far-away location When the cursor is moved to a location which is far away from the current location (e.g. after a search or a goto line), the buffer view is always relocated in such a way that the cursor is at the bottom or at the top (minus scrollmargin), i.e. as if we just scrolled to this location. It's not like in other editors, and IMHO it's annoying. When we jump to a new location far away, we usually want to see more of its context, so the cursor should be placed closer to the center of the view, not near its edges. This change implements the behavior similar to other editors: - If the distance between the new and the old location is less than one frame (i.e. the view either doesn't change or just slightly "shifts") then the current behavior remains unchanged. - Otherwise the current line is placed at 25% of the window height. * Postpone calling onBufPaneOpen until the initial resize It is currently not possible to find out the geometry of a newly created bufpane in onBufPaneOpen lua callback: bp:GetView() returns {0,0,0,0} instead of the actual window. The reason is that the bufpane view is not properly initialized yet when the bufpane is created and the callback is triggered. It is initialized a bit later, at the initial resize. So postpone calling onBufPaneOpen until after the initial resize. * Improve buffer view relocation when opening a file at a far-away location When a file is opened with the initial cursor location at a given line which is far away from the beginning of the file, the buffer view is relocated so that the cursor is at the bottom (minus scrollmargin) as if we just scrolled to this line, which is annoying since we'd rather like to see more of the context of this initial location. So implement the behavior similar to the earlier commit (which addresses a similar issue about jumping far away after a search or goto): - If the initial cursor location is less than one frame away from the beginning of the buffer, keep the existing behavior i.e. just display the beginning of the buffer. - Otherwise place the cursor location at 25% of the window height.
2022-12-03 03:38:09 +00:00
h.initialRelocate()
2021-08-21 21:58:30 +00:00
// Set mouseReleased to true because we assume the mouse is not being
// pressed when the editor is opened
2019-01-14 05:57:39 +00:00
h.mouseReleased = true
2021-08-21 21:58:30 +00:00
// Set isOverwriteMode to false, because we assume we are in the default
// mode when editor is opened
2019-01-14 05:57:39 +00:00
h.isOverwriteMode = false
h.lastClickTime = time.Time{}
}
Improve buffer view relocation after jumping to a far-away location (#2628) * Improve buffer view relocation after jumping to a far-away location When the cursor is moved to a location which is far away from the current location (e.g. after a search or a goto line), the buffer view is always relocated in such a way that the cursor is at the bottom or at the top (minus scrollmargin), i.e. as if we just scrolled to this location. It's not like in other editors, and IMHO it's annoying. When we jump to a new location far away, we usually want to see more of its context, so the cursor should be placed closer to the center of the view, not near its edges. This change implements the behavior similar to other editors: - If the distance between the new and the old location is less than one frame (i.e. the view either doesn't change or just slightly "shifts") then the current behavior remains unchanged. - Otherwise the current line is placed at 25% of the window height. * Postpone calling onBufPaneOpen until the initial resize It is currently not possible to find out the geometry of a newly created bufpane in onBufPaneOpen lua callback: bp:GetView() returns {0,0,0,0} instead of the actual window. The reason is that the bufpane view is not properly initialized yet when the bufpane is created and the callback is triggered. It is initialized a bit later, at the initial resize. So postpone calling onBufPaneOpen until after the initial resize. * Improve buffer view relocation when opening a file at a far-away location When a file is opened with the initial cursor location at a given line which is far away from the beginning of the file, the buffer view is relocated so that the cursor is at the bottom (minus scrollmargin) as if we just scrolled to this line, which is annoying since we'd rather like to see more of the context of this initial location. So implement the behavior similar to the earlier commit (which addresses a similar issue about jumping far away after a search or goto): - If the initial cursor location is less than one frame away from the beginning of the buffer, keep the existing behavior i.e. just display the beginning of the buffer. - Otherwise place the cursor location at 25% of the window height.
2022-12-03 03:38:09 +00:00
// GotoLoc moves the cursor to a new location and adjusts the view accordingly.
// Use GotoLoc when the new location may be far away from the current location.
func (h *BufPane) GotoLoc(loc buffer.Loc) {
sloc := h.SLocFromLoc(loc)
d := h.Diff(h.SLocFromLoc(h.Cursor.Loc), sloc)
h.Cursor.GotoLoc(loc)
// If the new location is far away from the previous one,
// ensure the cursor is at 25% of the window height
height := h.BufView().Height
if util.Abs(d) >= height {
v := h.GetView()
v.StartLine = h.Scroll(sloc, -height/4)
h.ScrollAdjust()
v.StartCol = 0
}
h.Relocate()
}
func (h *BufPane) initialRelocate() {
sloc := h.SLocFromLoc(h.Cursor.Loc)
height := h.BufView().Height
// If the initial cursor location is far away from the beginning
// of the buffer, ensure the cursor is at 25% of the window height
v := h.GetView()
if h.Diff(display.SLoc{0, 0}, sloc) < height {
v.StartLine = display.SLoc{0, 0}
} else {
v.StartLine = h.Scroll(sloc, -height/4)
h.ScrollAdjust()
}
v.StartCol = 0
h.Relocate()
}
2021-08-21 21:58:30 +00:00
// ID returns this pane's split id.
2019-01-19 20:37:59 +00:00
func (h *BufPane) ID() uint64 {
2019-01-11 19:49:22 +00:00
return h.splitID
}
2021-08-21 21:58:30 +00:00
// SetID sets the split ID of this pane.
2019-01-19 20:37:59 +00:00
func (h *BufPane) SetID(i uint64) {
h.splitID = i
}
2021-08-21 21:58:30 +00:00
// Name returns the BufPane's name.
2019-01-19 20:37:59 +00:00
func (h *BufPane) Name() string {
n := h.Buf.GetName()
if h.Buf.Modified() {
n += " +"
}
return n
2019-01-11 19:49:22 +00:00
}
func (h *BufPane) getReloadSetting() string {
reloadSetting := h.Buf.Settings["reload"]
return reloadSetting.(string)
}
2018-08-27 23:53:08 +00:00
// HandleEvent executes the tcell event properly
2019-01-19 20:37:59 +00:00
func (h *BufPane) HandleEvent(event tcell.Event) {
if h.Buf.ExternallyModified() && !h.Buf.ReloadDisabled {
reload := h.getReloadSetting()
if reload == "prompt" {
InfoBar.YNPrompt("The file on disk has changed. Reload file? (y,n,esc)", func(yes, canceled bool) {
if canceled {
h.Buf.DisableReload()
}
if !yes || canceled {
h.Buf.UpdateModTime()
} else {
h.Buf.ReOpen()
}
})
} else if reload == "auto" {
h.Buf.ReOpen()
} else if reload == "disabled" {
h.Buf.DisableReload()
} else {
InfoBar.Message("Invalid reload setting")
}
}
2018-08-27 23:53:08 +00:00
switch e := event.(type) {
2020-01-01 03:34:43 +00:00
case *tcell.EventRaw:
re := RawEvent{
esc: e.EscSeq(),
}
h.DoKeyEvent(re)
2020-01-01 01:15:45 +00:00
case *tcell.EventPaste:
h.paste(e.Text())
h.Relocate()
2018-08-27 23:53:08 +00:00
case *tcell.EventKey:
ke := KeyEvent{
code: e.Key(),
mod: metaToAlt(e.Modifiers()),
2018-08-27 23:53:08 +00:00
r: e.Rune(),
}
2019-01-03 04:26:40 +00:00
done := h.DoKeyEvent(ke)
if !done && e.Key() == tcell.KeyRune {
h.DoRuneInsert(e.Rune())
2018-08-29 03:30:39 +00:00
}
2018-08-27 23:53:08 +00:00
case *tcell.EventMouse:
cancel := false
2019-01-02 22:39:50 +00:00
switch e.Buttons() {
case tcell.Button1:
_, my := e.Position()
2020-06-07 19:46:12 +00:00
if h.Buf.Type.Kind != buffer.BTInfo.Kind && h.Buf.Settings["statusline"].(bool) && my >= h.GetView().Y+h.GetView().Height-1 {
cancel = true
}
2019-01-02 22:39:50 +00:00
case tcell.ButtonNone:
// Mouse event with no click
if !h.mouseReleased {
// Mouse was just released
// mx, my := e.Position()
// mouseLoc := h.LocFromVisual(buffer.Loc{X: mx, Y: my})
// we could finish the selection based on the release location as described
// below but when the mouse click is within the scroll margin this will
// cause a scroll and selection even for a simple mouse click which is
// not good
// for terminals that don't support mouse motion events, selection via
// the mouse won't work but this is ok
2019-01-02 22:39:50 +00:00
// 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.SetSelectionEnd(h.Cursor.Loc)
// }
if h.Cursor.HasSelection() {
h.Cursor.CopySelection(clipboard.PrimaryReg)
}
2019-01-02 22:39:50 +00:00
h.mouseReleased = true
}
}
if !cancel {
me := MouseEvent{
btn: e.Buttons(),
mod: metaToAlt(e.Modifiers()),
}
h.DoMouseEvent(me, e)
2018-08-27 23:53:08 +00:00
}
2018-08-28 22:44:52 +00:00
}
2019-01-05 02:50:11 +00:00
h.Buf.MergeCursors()
2019-01-13 22:22:11 +00:00
2019-08-04 06:53:33 +00:00
if h.IsActive() {
// Display any gutter messages for this line
c := h.Buf.GetActiveCursor()
none := true
for _, m := range h.Buf.Messages {
if c.Y == m.Start.Y || c.Y == m.End.Y {
InfoBar.GutterMessage(m.Msg)
none = false
break
}
}
if none && InfoBar.HasGutter {
InfoBar.ClearGutter()
2019-01-13 22:22:11 +00:00
}
}
2018-08-28 22:44:52 +00:00
}
2021-08-21 21:58:30 +00:00
// Bindings returns the current bindings tree for this buffer.
func (h *BufPane) Bindings() *KeyTree {
if h.bindings != nil {
return h.bindings
}
return BufBindings
}
2019-01-03 20:27:43 +00:00
// DoKeyEvent executes a key event by finding the action it is bound
// to and executing it (possibly multiple times for multiple cursors)
2019-01-19 20:37:59 +00:00
func (h *BufPane) DoKeyEvent(e Event) bool {
binds := h.Bindings()
action, more := binds.NextEvent(e, nil)
if action != nil && !more {
action(h)
binds.ResetEvents()
return true
} else if action == nil && !more {
binds.ResetEvents()
}
return more
}
func (h *BufPane) execAction(action func(*BufPane) bool, name string, cursor int) bool {
2020-01-04 20:51:15 +00:00
if name != "Autocomplete" && name != "CycleAutocompleteBack" {
h.Buf.HasSuggestions = false
}
_, isMulti := MultiActions[name]
if (!isMulti && cursor == 0) || isMulti {
if h.PluginCB("pre" + name) {
success := action(h)
success = success && h.PluginCB("on"+name)
2019-08-05 03:23:32 +00:00
if isMulti {
2021-08-21 22:04:08 +00:00
if recordingMacro {
if name != "ToggleMacro" && name != "PlayMacro" {
curmacro = append(curmacro, action)
2019-08-05 03:23:32 +00:00
}
2019-01-03 04:26:40 +00:00
}
}
return success
2019-01-03 04:26:40 +00:00
}
2018-08-28 22:44:52 +00:00
}
2018-08-28 22:44:52 +00:00
return false
}
func (h *BufPane) completeAction(action string) {
h.PluginCB("on" + action)
}
2019-01-19 20:37:59 +00:00
func (h *BufPane) HasKeyEvent(e Event) bool {
// TODO
return true
// _, ok := BufKeyBindings[e]
// return ok
2019-01-13 22:58:08 +00:00
}
2019-01-03 20:27:43 +00:00
// DoMouseEvent executes a mouse event by finding the action it is bound
// to and executing it
2019-01-19 20:37:59 +00:00
func (h *BufPane) DoMouseEvent(e MouseEvent, te *tcell.EventMouse) bool {
binds := h.Bindings()
action, _ := binds.NextEvent(e, te)
if action != nil {
2020-08-12 05:18:15 +00:00
action(h)
binds.ResetEvents()
2018-08-28 22:44:52 +00:00
return true
2018-08-27 23:53:08 +00:00
}
// TODO
2018-08-28 22:44:52 +00:00
return false
// if action, ok := BufMouseBindings[e]; ok {
// if action(h, te) {
// h.Relocate()
// }
// return true
// } else if h.HasKeyEvent(e) {
// return h.DoKeyEvent(e)
// }
// return false
2018-08-27 23:53:08 +00:00
}
2019-01-03 20:27:43 +00:00
// DoRuneInsert inserts a given rune into the current buffer
// (possibly multiple times for multiple cursors)
2019-01-19 20:37:59 +00:00
func (h *BufPane) DoRuneInsert(r rune) {
2019-01-03 04:26:40 +00:00
cursors := h.Buf.GetCursors()
for _, c := range cursors {
// Insert a character
2019-01-16 22:52:30 +00:00
h.Buf.SetCurCursor(c.Num)
2019-08-05 03:23:32 +00:00
h.Cursor = c
2019-08-02 21:48:59 +00:00
if !h.PluginCBRune("preRune", r) {
continue
}
2019-01-03 04:26:40 +00:00
if c.HasSelection() {
c.DeleteSelection()
c.ResetSelection()
}
2018-08-29 03:30:39 +00:00
2019-01-03 04:26:40 +00:00
if h.isOverwriteMode {
next := c.Loc
next.X++
h.Buf.Replace(c.Loc, next, string(r))
2019-01-03 04:26:40 +00:00
} else {
h.Buf.Insert(c.Loc, string(r))
2019-01-03 04:26:40 +00:00
}
2021-08-21 22:04:08 +00:00
if recordingMacro {
2019-08-05 03:23:32 +00:00
curmacro = append(curmacro, r)
}
2020-02-14 20:52:20 +00:00
h.Relocate()
2019-08-02 21:48:59 +00:00
h.PluginCBRune("onRune", r)
2018-08-29 03:30:39 +00:00
}
}
2021-08-21 21:58:30 +00:00
// VSplitIndex opens the given buffer in a vertical split on the given side.
2020-02-05 22:16:31 +00:00
func (h *BufPane) VSplitIndex(buf *buffer.Buffer, right bool) *BufPane {
e := NewBufPaneFromBuf(buf, h.tab)
e.splitID = MainTab().GetNode(h.splitID).VSplit(right)
2019-01-10 01:07:18 +00:00
MainTab().Panes = append(MainTab().Panes, e)
MainTab().Resize()
MainTab().SetActive(len(MainTab().Panes) - 1)
2019-08-06 03:43:34 +00:00
return e
2019-01-04 22:40:56 +00:00
}
2021-08-21 21:58:30 +00:00
// HSplitIndex opens the given buffer in a horizontal split on the given side.
2020-02-05 22:16:31 +00:00
func (h *BufPane) HSplitIndex(buf *buffer.Buffer, bottom bool) *BufPane {
e := NewBufPaneFromBuf(buf, h.tab)
e.splitID = MainTab().GetNode(h.splitID).HSplit(bottom)
2019-01-10 01:07:18 +00:00
MainTab().Panes = append(MainTab().Panes, e)
MainTab().Resize()
MainTab().SetActive(len(MainTab().Panes) - 1)
2019-08-06 03:43:34 +00:00
return e
2019-01-04 22:40:56 +00:00
}
2020-02-05 22:16:31 +00:00
2021-08-21 21:58:30 +00:00
// VSplitBuf opens the given buffer in a new vertical split.
2020-02-05 22:16:31 +00:00
func (h *BufPane) VSplitBuf(buf *buffer.Buffer) *BufPane {
return h.VSplitIndex(buf, h.Buf.Settings["splitright"].(bool))
}
2021-08-21 21:58:30 +00:00
// HSplitBuf opens the given buffer in a new horizontal split.
2020-02-05 22:16:31 +00:00
func (h *BufPane) HSplitBuf(buf *buffer.Buffer) *BufPane {
return h.HSplitIndex(buf, h.Buf.Settings["splitbottom"].(bool))
}
2021-08-21 21:58:30 +00:00
// Close this pane.
2019-01-19 20:37:59 +00:00
func (h *BufPane) Close() {
2019-01-14 02:06:58 +00:00
h.Buf.Close()
}
2019-01-04 22:40:56 +00:00
2021-08-21 21:58:30 +00:00
// SetActive marks this pane as active.
2019-08-04 06:53:33 +00:00
func (h *BufPane) SetActive(b bool) {
h.BWindow.SetActive(b)
if b {
// Display any gutter messages for this line
c := h.Buf.GetActiveCursor()
none := true
for _, m := range h.Buf.Messages {
if c.Y == m.Start.Y || c.Y == m.End.Y {
InfoBar.GutterMessage(m.Msg)
none = false
break
}
}
if none && InfoBar.HasGutter {
InfoBar.ClearGutter()
}
}
}
2019-01-03 20:27:43 +00:00
// BufKeyActions contains the list of all possible key actions the bufhandler could execute
2018-08-27 23:53:08 +00:00
var BufKeyActions = map[string]BufKeyAction{
2020-04-30 04:54:02 +00:00
"CursorUp": (*BufPane).CursorUp,
"CursorDown": (*BufPane).CursorDown,
"CursorPageUp": (*BufPane).CursorPageUp,
"CursorPageDown": (*BufPane).CursorPageDown,
"CursorLeft": (*BufPane).CursorLeft,
"CursorRight": (*BufPane).CursorRight,
"CursorStart": (*BufPane).CursorStart,
"CursorEnd": (*BufPane).CursorEnd,
"SelectToStart": (*BufPane).SelectToStart,
"SelectToEnd": (*BufPane).SelectToEnd,
"SelectUp": (*BufPane).SelectUp,
"SelectDown": (*BufPane).SelectDown,
"SelectLeft": (*BufPane).SelectLeft,
"SelectRight": (*BufPane).SelectRight,
"WordRight": (*BufPane).WordRight,
"WordLeft": (*BufPane).WordLeft,
"SelectWordRight": (*BufPane).SelectWordRight,
"SelectWordLeft": (*BufPane).SelectWordLeft,
"DeleteWordRight": (*BufPane).DeleteWordRight,
"DeleteWordLeft": (*BufPane).DeleteWordLeft,
"SelectLine": (*BufPane).SelectLine,
"SelectToStartOfLine": (*BufPane).SelectToStartOfLine,
"SelectToStartOfText": (*BufPane).SelectToStartOfText,
"SelectToStartOfTextToggle": (*BufPane).SelectToStartOfTextToggle,
"SelectToEndOfLine": (*BufPane).SelectToEndOfLine,
"ParagraphPrevious": (*BufPane).ParagraphPrevious,
"ParagraphNext": (*BufPane).ParagraphNext,
"InsertNewline": (*BufPane).InsertNewline,
"Backspace": (*BufPane).Backspace,
"Delete": (*BufPane).Delete,
"InsertTab": (*BufPane).InsertTab,
"Save": (*BufPane).Save,
"SaveAll": (*BufPane).SaveAll,
"SaveAs": (*BufPane).SaveAs,
"Find": (*BufPane).Find,
"FindLiteral": (*BufPane).FindLiteral,
2020-04-30 04:54:02 +00:00
"FindNext": (*BufPane).FindNext,
"FindPrevious": (*BufPane).FindPrevious,
"DiffNext": (*BufPane).DiffNext,
"DiffPrevious": (*BufPane).DiffPrevious,
2020-04-30 04:54:02 +00:00
"Center": (*BufPane).Center,
"Undo": (*BufPane).Undo,
"Redo": (*BufPane).Redo,
"Copy": (*BufPane).Copy,
"CopyLine": (*BufPane).CopyLine,
"Cut": (*BufPane).Cut,
"CutLine": (*BufPane).CutLine,
"DuplicateLine": (*BufPane).DuplicateLine,
"DeleteLine": (*BufPane).DeleteLine,
"MoveLinesUp": (*BufPane).MoveLinesUp,
"MoveLinesDown": (*BufPane).MoveLinesDown,
"IndentSelection": (*BufPane).IndentSelection,
"OutdentSelection": (*BufPane).OutdentSelection,
"Autocomplete": (*BufPane).Autocomplete,
"CycleAutocompleteBack": (*BufPane).CycleAutocompleteBack,
"OutdentLine": (*BufPane).OutdentLine,
"IndentLine": (*BufPane).IndentLine,
"Paste": (*BufPane).Paste,
"PastePrimary": (*BufPane).PastePrimary,
"SelectAll": (*BufPane).SelectAll,
"OpenFile": (*BufPane).OpenFile,
"Start": (*BufPane).Start,
"End": (*BufPane).End,
"PageUp": (*BufPane).PageUp,
"PageDown": (*BufPane).PageDown,
"SelectPageUp": (*BufPane).SelectPageUp,
"SelectPageDown": (*BufPane).SelectPageDown,
"HalfPageUp": (*BufPane).HalfPageUp,
"HalfPageDown": (*BufPane).HalfPageDown,
"StartOfText": (*BufPane).StartOfText,
"StartOfTextToggle": (*BufPane).StartOfTextToggle,
"StartOfLine": (*BufPane).StartOfLine,
"EndOfLine": (*BufPane).EndOfLine,
"ToggleHelp": (*BufPane).ToggleHelp,
"ToggleKeyMenu": (*BufPane).ToggleKeyMenu,
"ToggleDiffGutter": (*BufPane).ToggleDiffGutter,
"ToggleRuler": (*BufPane).ToggleRuler,
"ToggleHighlightSearch": (*BufPane).ToggleHighlightSearch,
"UnhighlightSearch": (*BufPane).UnhighlightSearch,
2020-04-30 04:54:02 +00:00
"ClearStatus": (*BufPane).ClearStatus,
"ShellMode": (*BufPane).ShellMode,
"CommandMode": (*BufPane).CommandMode,
"ToggleOverwriteMode": (*BufPane).ToggleOverwriteMode,
"Escape": (*BufPane).Escape,
"Quit": (*BufPane).Quit,
"QuitAll": (*BufPane).QuitAll,
2021-03-02 02:55:49 +00:00
"ForceQuit": (*BufPane).ForceQuit,
2020-04-30 04:54:02 +00:00
"AddTab": (*BufPane).AddTab,
"PreviousTab": (*BufPane).PreviousTab,
"NextTab": (*BufPane).NextTab,
"NextSplit": (*BufPane).NextSplit,
"PreviousSplit": (*BufPane).PreviousSplit,
"Unsplit": (*BufPane).Unsplit,
"VSplit": (*BufPane).VSplitAction,
"HSplit": (*BufPane).HSplitAction,
"ToggleMacro": (*BufPane).ToggleMacro,
"PlayMacro": (*BufPane).PlayMacro,
"Suspend": (*BufPane).Suspend,
"ScrollUp": (*BufPane).ScrollUpAction,
"ScrollDown": (*BufPane).ScrollDownAction,
"SpawnMultiCursor": (*BufPane).SpawnMultiCursor,
"SpawnMultiCursorUp": (*BufPane).SpawnMultiCursorUp,
"SpawnMultiCursorDown": (*BufPane).SpawnMultiCursorDown,
"SpawnMultiCursorSelect": (*BufPane).SpawnMultiCursorSelect,
"RemoveMultiCursor": (*BufPane).RemoveMultiCursor,
"RemoveAllMultiCursors": (*BufPane).RemoveAllMultiCursors,
"SkipMultiCursor": (*BufPane).SkipMultiCursor,
"JumpToMatchingBrace": (*BufPane).JumpToMatchingBrace,
"JumpLine": (*BufPane).JumpLine,
"Deselect": (*BufPane).Deselect,
"ClearInfo": (*BufPane).ClearInfo,
2020-04-30 04:54:02 +00:00
"None": (*BufPane).None,
2018-08-27 23:53:08 +00:00
// This was changed to InsertNewline but I don't want to break backwards compatibility
2020-04-30 04:54:02 +00:00
"InsertEnter": (*BufPane).InsertNewline,
2018-08-27 23:53:08 +00:00
}
2019-01-03 20:27:43 +00:00
// BufMouseActions contains the list of all possible mouse actions the bufhandler could execute
2018-08-27 23:53:08 +00:00
var BufMouseActions = map[string]BufMouseAction{
2019-01-19 20:37:59 +00:00
"MousePress": (*BufPane).MousePress,
"MouseMultiCursor": (*BufPane).MouseMultiCursor,
2018-08-27 23:53:08 +00:00
}
2019-01-03 04:26:40 +00:00
// MultiActions is a list of actions that should be executed multiple
// times if there are multiple cursors (one per cursor)
// Generally actions that modify global editor state like quitting or
// saving should not be included in this list
var MultiActions = map[string]bool{
"CursorUp": true,
"CursorDown": true,
"CursorPageUp": true,
"CursorPageDown": true,
"CursorLeft": true,
"CursorRight": true,
"CursorStart": true,
"CursorEnd": true,
"SelectToStart": true,
"SelectToEnd": true,
"SelectUp": true,
"SelectDown": true,
"SelectLeft": true,
"SelectRight": true,
"WordRight": true,
"WordLeft": true,
"SelectWordRight": true,
"SelectWordLeft": true,
"DeleteWordRight": true,
"DeleteWordLeft": true,
"SelectLine": true,
"SelectToStartOfLine": true,
"SelectToStartOfText": true,
"SelectToStartOfTextToggle": true,
"SelectToEndOfLine": true,
"ParagraphPrevious": true,
"ParagraphNext": true,
"InsertNewline": true,
"Backspace": true,
"Delete": true,
"InsertTab": true,
"FindNext": true,
"FindPrevious": true,
"CopyLine": true,
"Copy": true,
"Cut": true,
"CutLine": true,
"DuplicateLine": true,
"DeleteLine": true,
"MoveLinesUp": true,
"MoveLinesDown": true,
"IndentSelection": true,
"OutdentSelection": true,
"OutdentLine": true,
"IndentLine": true,
"Paste": true,
"PastePrimary": true,
"SelectPageUp": true,
"SelectPageDown": true,
"StartOfLine": true,
"StartOfText": true,
"StartOfTextToggle": true,
"EndOfLine": true,
"JumpToMatchingBrace": true,
2019-01-03 04:26:40 +00:00
}