micro/internal/action/bufpane.go

888 lines
26 KiB
Go
Raw Normal View History

2018-08-28 02:53:08 +03:00
package action
import (
"strings"
2018-08-28 02:53:08 +03:00
"time"
2019-03-20 01:28:51 +03:00
luar "layeh.com/gopher-luar"
2019-08-04 01:49:05 +03:00
lua "github.com/yuin/gopher-lua"
2020-05-04 17:16:15 +03:00
"github.com/zyedidia/micro/v2/internal/buffer"
"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 06:38:09 +03:00
"github.com/zyedidia/micro/v2/internal/util"
2020-09-05 21:52:35 +03:00
"github.com/zyedidia/tcell/v2"
2018-08-28 02:53:08 +03:00
)
type BufAction interface{}
2021-08-22 00:58:30 +03:00
// BufKeyAction represents an action bound to a key.
2019-01-19 23:37:59 +03:00
type BufKeyAction func(*BufPane) bool
2021-08-22 00:58:30 +03:00
// BufMouseAction is an action that must be bound to a mouse event.
2019-01-19 23:37:59 +03:00
type BufMouseAction func(*BufPane, *tcell.EventMouse) bool
2018-08-28 02:53:08 +03:00
2021-08-22 00:58:30 +03:00
// BufBindings stores the bindings for the buffer pane type.
var BufBindings *KeyTree
2018-08-28 02:53:08 +03:00
2021-08-22 00:58:30 +03: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-22 00:58:30 +03: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-28 02:53:08 +03:00
func init() {
BufBindings = NewKeyTree()
2018-08-28 02:53:08 +03:00
}
// LuaAction makes an action from a lua function. It returns either a BufKeyAction
// or a BufMouseAction depending on the event type.
func LuaAction(fn string, k Event) BufAction {
2019-08-04 01:49:05 +03:00
luaFn := strings.Split(fn, ".")
2019-08-26 21:47:27 +03:00
if len(luaFn) <= 1 {
return nil
}
2019-08-04 01:49:05 +03:00
plName, plFn := luaFn[0], luaFn[1]
pl := config.FindPlugin(plName)
2019-08-26 21:47:27 +03:00
if pl == nil {
return nil
}
var action BufAction
switch k.(type) {
case KeyEvent, KeySequenceEvent, RawEvent:
action = BufKeyAction(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)
}
})
case MouseEvent:
action = BufMouseAction(func(h *BufPane, te *tcell.EventMouse) bool {
val, err := pl.Call(plFn, luar.New(ulua.L, h), luar.New(ulua.L, te))
if err != nil {
screen.TermMessage(err)
}
if v, ok := val.(lua.LBool); !ok {
return false
} else {
return bool(v)
}
})
}
return action
}
// BufMapEvent maps an event to an action
func BufMapEvent(k Event, action string) {
config.Bindings["buffer"][k.Name()] = action
var actionfns []BufAction
var names []string
var types []byte
for i := 0; ; i++ {
if action == "" {
break
}
2019-12-27 08:43:45 +03: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 = ""
}
var afn BufAction
2019-12-27 08:43:45 +03:00
if strings.HasPrefix(a, "command:") {
2019-08-17 21:49:42 +03:00
a = strings.SplitN(a, ":", 2)[1]
afn = CommandAction(a)
names = append(names, "")
2019-08-17 21:49:42 +03:00
} else if strings.HasPrefix(a, "command-edit:") {
a = strings.SplitN(a, ":", 2)[1]
afn = CommandEditAction(a)
names = append(names, "")
2019-08-17 21:49:42 +03:00
} else if strings.HasPrefix(a, "lua:") {
a = strings.SplitN(a, ":", 2)[1]
afn = LuaAction(a, k)
if afn == nil {
2019-12-27 08:43:45 +03:00
screen.TermMessage("Lua Error:", a, "does not exist")
continue
}
2020-02-06 19:12:34 +03: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 21:49:42 +03:00
} else if f, ok := BufKeyActions[a]; ok {
afn = f
names = append(names, a)
} else if f, ok := BufMouseActions[a]; ok {
afn = f
names = append(names, a)
2019-08-17 21:49:42 +03:00
} else {
2020-04-26 00:01:16 +03:00
screen.TermMessage("Error in bindings: action", a, "does not exist")
continue
2019-08-17 21:49:42 +03:00
}
actionfns = append(actionfns, afn)
2019-08-17 21:49:42 +03:00
}
bufAction := func(h *BufPane, te *tcell.EventMouse) 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, te)
} else {
break
}
}
// if the action changed the current pane, update the reference
h = MainTab().CurPane()
success = innerSuccess
2019-08-17 21:49:42 +03:00
}
return true
2018-08-29 06:30:39 +03:00
}
switch e := k.(type) {
case KeyEvent, KeySequenceEvent, RawEvent:
BufBindings.RegisterKeyBinding(e, BufKeyActionGeneral(func(h *BufPane) bool {
return bufAction(h, nil)
}))
case MouseEvent:
BufBindings.RegisterMouseBinding(e, BufMouseActionGeneral(bufAction))
2018-08-29 06:30:39 +03:00
}
2018-08-28 02:53:08 +03: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 23:37:59 +03:00
// The BufPane connects the buffer and the window
2018-08-28 02:53:08 +03: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 23:37:59 +03:00
type BufPane struct {
2019-01-14 08:57:39 +03:00
display.BWindow
2019-01-11 05:26:58 +03:00
// Buf is the buffer this BufPane views
2018-08-28 02:53:08 +03:00
Buf *buffer.Buffer
// Bindings stores the association of key events and actions
bindings *KeyTree
2018-08-28 02:53:08 +03:00
// Cursor is the currently active buffer cursor
Cursor *buffer.Cursor
2018-08-28 02:53:08 +03:00
// Since tcell doesn't differentiate between a mouse press event
// and a mouse move event with button pressed (nor between a mouse
// release event and a mouse move event with no buttons pressed),
// we need to keep track of whether or not the mouse was previously
// pressed, to determine mouse release and mouse drag events.
// Moreover, since in case of a release event tcell doesn't tell us
// which button was released, we need to keep track of which
// (possibly multiple) buttons were pressed previously.
mousePressed map[MouseEvent]bool
2018-08-28 02:53:08 +03:00
// 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 23:35:24 +03:00
2019-01-05 05:48:19 +03: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-05 01:40:56 +03:00
splitID uint64
2020-02-06 01:16:31 +03:00
tab *Tab
2019-06-15 21:44:03 +03: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 06:38:09 +03: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-28 02:53:08 +03: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 06:38:09 +03:00
func newBufPane(buf *buffer.Buffer, win display.BWindow, tab *Tab) *BufPane {
2019-01-19 23:37:59 +03:00
h := new(BufPane)
2018-08-29 01:44:52 +03:00
h.Buf = buf
2019-01-14 08:57:39 +03:00
h.BWindow = win
2020-02-06 01:16:31 +03:00
h.tab = tab
2018-08-28 02:53:08 +03:00
h.Cursor = h.Buf.GetActiveCursor()
h.mousePressed = make(map[MouseEvent]bool)
2018-08-28 02:53:08 +03: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 06:38:09 +03:00
return h
}
2019-03-20 01:28:51 +03: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 06:38:09 +03: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-29 01:44:52 +03:00
return h
2018-08-28 02:53:08 +03:00
}
2021-08-22 00:58:30 +03:00
// NewBufPaneFromBuf constructs a new pane from the given buffer and automatically
// creates a buf window.
2020-02-06 01:16:31 +03:00
func NewBufPaneFromBuf(buf *buffer.Buffer, tab *Tab) *BufPane {
2019-01-19 23:37:59 +03: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 06:38:09 +03: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-06 01:16:31 +03:00
}
2021-08-22 00:58:30 +03:00
// SetTab sets this pane's tab.
2020-02-06 01:16:31 +03:00
func (h *BufPane) SetTab(t *Tab) {
h.tab = t
}
2021-08-22 00:58:30 +03:00
// Tab returns this pane's tab.
2020-02-06 01:16:31 +03: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 23:37:59 +03:00
}
2021-08-22 00:58:30 +03:00
// PluginCB calls all plugin callbacks with a certain name and displays an
// error if there is one and returns the aggregate boolean response
2019-08-03 00:48:59 +03:00
func (h *BufPane) PluginCB(cb string) bool {
b, err := config.RunPluginFnBool(h.Buf.Settings, cb, luar.New(ulua.L, h))
2019-08-03 00:48:59 +03:00
if err != nil {
screen.TermMessage(err)
}
return b
}
2021-08-22 00:58:30 +03:00
// PluginCBRune is the same as PluginCB but also passes a rune to the plugins
2019-08-03 00:48:59 +03: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-03 00:48:59 +03:00
if err != nil {
screen.TermMessage(err)
}
return b
}
func (h *BufPane) resetMouse() {
for me := range h.mousePressed {
delete(h.mousePressed, me)
}
}
2021-08-22 00:58:30 +03:00
// OpenBuffer opens the given buffer in this pane.
2019-01-19 23:37:59 +03:00
func (h *BufPane) OpenBuffer(b *buffer.Buffer) {
2019-01-14 08:57:39 +03:00
h.Buf.Close()
h.Buf = b
h.BWindow.SetBuffer(b)
h.Cursor = b.GetActiveCursor()
2019-06-16 01:00:24 +03: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 06:38:09 +03:00
h.initialRelocate()
2021-08-22 00:58:30 +03:00
// Set mouseReleased to true because we assume the mouse is not being
// pressed when the editor is opened
h.resetMouse()
2021-08-22 00:58:30 +03:00
// Set isOverwriteMode to false, because we assume we are in the default
// mode when editor is opened
2019-01-14 08:57:39 +03: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 06:38:09 +03: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-22 00:58:30 +03:00
// ID returns this pane's split id.
2019-01-19 23:37:59 +03:00
func (h *BufPane) ID() uint64 {
2019-01-11 22:49:22 +03:00
return h.splitID
}
2021-08-22 00:58:30 +03:00
// SetID sets the split ID of this pane.
2019-01-19 23:37:59 +03:00
func (h *BufPane) SetID(i uint64) {
h.splitID = i
}
2021-08-22 00:58:30 +03:00
// Name returns the BufPane's name.
2019-01-19 23:37:59 +03:00
func (h *BufPane) Name() string {
n := h.Buf.GetName()
if h.Buf.Modified() {
n += " +"
}
return n
2019-01-11 22:49:22 +03:00
}
func (h *BufPane) getReloadSetting() string {
reloadSetting := h.Buf.Settings["reload"]
return reloadSetting.(string)
}
2018-08-28 02:53:08 +03:00
// HandleEvent executes the tcell event properly
2019-01-19 23:37:59 +03: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-28 02:53:08 +03:00
switch e := event.(type) {
2020-01-01 06:34:43 +03:00
case *tcell.EventRaw:
re := RawEvent{
esc: e.EscSeq(),
}
h.DoKeyEvent(re)
2020-01-01 04:15:45 +03:00
case *tcell.EventPaste:
h.paste(e.Text())
h.Relocate()
2018-08-28 02:53:08 +03:00
case *tcell.EventKey:
ke := KeyEvent{
code: e.Key(),
mod: metaToAlt(e.Modifiers()),
2018-08-28 02:53:08 +03:00
r: e.Rune(),
}
2019-01-03 07:26:40 +03:00
done := h.DoKeyEvent(ke)
if !done && e.Key() == tcell.KeyRune {
h.DoRuneInsert(e.Rune())
2018-08-29 06:30:39 +03:00
}
2018-08-28 02:53:08 +03:00
case *tcell.EventMouse:
if e.Buttons() != tcell.ButtonNone {
me := MouseEvent{
btn: e.Buttons(),
mod: metaToAlt(e.Modifiers()),
state: MousePress,
}
isDrag := len(h.mousePressed) > 0
2019-01-03 01:39:50 +03:00
if e.Buttons() & ^(tcell.WheelUp|tcell.WheelDown|tcell.WheelLeft|tcell.WheelRight) != tcell.ButtonNone {
h.mousePressed[me] = true
2019-01-03 01:39:50 +03:00
}
if isDrag {
me.state = MouseDrag
}
h.DoMouseEvent(me, e)
} else {
// Mouse event with no click - mouse was just released.
// If there were multiple mouse buttons pressed, we don't know which one
// was actually released, so we assume they all were released.
for me := range h.mousePressed {
delete(h.mousePressed, me)
me.state = MouseRelease
h.DoMouseEvent(me, e)
}
2018-08-28 02:53:08 +03:00
}
2018-08-29 01:44:52 +03:00
}
2019-01-05 05:50:11 +03:00
h.Buf.MergeCursors()
2019-01-14 01:22:11 +03:00
2019-08-04 09:53:33 +03: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-14 01:22:11 +03:00
}
}
cursors := h.Buf.GetCursors()
for _, c := range cursors {
if c.NewTrailingWsY != c.Y && (!c.HasSelection() ||
(c.NewTrailingWsY != c.CurSelection[0].Y && c.NewTrailingWsY != c.CurSelection[1].Y)) {
c.NewTrailingWsY = -1
}
}
2018-08-29 01:44:52 +03:00
}
2021-08-22 00:58:30 +03: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 23:27:43 +03: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 23:37:59 +03: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 BufAction, name string, cursor int, te *tcell.EventMouse) bool {
2020-01-04 23:51:15 +03:00
if name != "Autocomplete" && name != "CycleAutocompleteBack" {
h.Buf.HasSuggestions = false
}
_, isMulti := MultiActions[name]
if (!isMulti && cursor == 0) || isMulti {
if h.PluginCB("pre" + name) {
var success bool
switch a := action.(type) {
case BufKeyAction:
success = a(h)
case BufMouseAction:
success = a(h, te)
}
success = success && h.PluginCB("on"+name)
2019-08-05 06:23:32 +03:00
if isMulti {
2021-08-22 01:04:08 +03:00
if recordingMacro {
if name != "ToggleMacro" && name != "PlayMacro" {
curmacro = append(curmacro, action)
2019-08-05 06:23:32 +03:00
}
2019-01-03 07:26:40 +03:00
}
}
return success
2019-01-03 07:26:40 +03:00
}
2018-08-29 01:44:52 +03:00
}
2018-08-29 01:44:52 +03:00
return false
}
func (h *BufPane) completeAction(action string) {
h.PluginCB("on" + action)
}
2019-01-19 23:37:59 +03:00
func (h *BufPane) HasKeyEvent(e Event) bool {
// TODO
return true
// _, ok := BufKeyBindings[e]
// return ok
2019-01-14 01:58:08 +03:00
}
2019-01-03 23:27:43 +03:00
// DoMouseEvent executes a mouse event by finding the action it is bound
// to and executing it
2019-01-19 23:37:59 +03: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 08:18:15 +03:00
action(h)
binds.ResetEvents()
2018-08-29 01:44:52 +03:00
return true
2018-08-28 02:53:08 +03:00
}
// TODO
2018-08-29 01:44:52 +03: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-28 02:53:08 +03:00
}
2019-01-03 23:27:43 +03:00
// DoRuneInsert inserts a given rune into the current buffer
// (possibly multiple times for multiple cursors)
2019-01-19 23:37:59 +03:00
func (h *BufPane) DoRuneInsert(r rune) {
2019-01-03 07:26:40 +03:00
cursors := h.Buf.GetCursors()
for _, c := range cursors {
// Insert a character
2019-01-17 01:52:30 +03:00
h.Buf.SetCurCursor(c.Num)
2019-08-05 06:23:32 +03:00
h.Cursor = c
2019-08-03 00:48:59 +03:00
if !h.PluginCBRune("preRune", r) {
continue
}
2019-01-03 07:26:40 +03:00
if c.HasSelection() {
c.DeleteSelection()
c.ResetSelection()
}
2018-08-29 06:30:39 +03:00
2019-01-03 07:26:40 +03:00
if h.isOverwriteMode {
next := c.Loc
next.X++
h.Buf.Replace(c.Loc, next, string(r))
2019-01-03 07:26:40 +03:00
} else {
h.Buf.Insert(c.Loc, string(r))
2019-01-03 07:26:40 +03:00
}
2021-08-22 01:04:08 +03:00
if recordingMacro {
2019-08-05 06:23:32 +03:00
curmacro = append(curmacro, r)
}
2020-02-14 23:52:20 +03:00
h.Relocate()
2019-08-03 00:48:59 +03:00
h.PluginCBRune("onRune", r)
2018-08-29 06:30:39 +03:00
}
}
2021-08-22 00:58:30 +03:00
// VSplitIndex opens the given buffer in a vertical split on the given side.
2020-02-06 01:16:31 +03: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 04:07:18 +03:00
MainTab().Panes = append(MainTab().Panes, e)
MainTab().Resize()
MainTab().SetActive(len(MainTab().Panes) - 1)
2019-08-06 06:43:34 +03:00
return e
2019-01-05 01:40:56 +03:00
}
2021-08-22 00:58:30 +03:00
// HSplitIndex opens the given buffer in a horizontal split on the given side.
2020-02-06 01:16:31 +03: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 04:07:18 +03:00
MainTab().Panes = append(MainTab().Panes, e)
MainTab().Resize()
MainTab().SetActive(len(MainTab().Panes) - 1)
2019-08-06 06:43:34 +03:00
return e
2019-01-05 01:40:56 +03:00
}
2020-02-06 01:16:31 +03:00
2021-08-22 00:58:30 +03:00
// VSplitBuf opens the given buffer in a new vertical split.
2020-02-06 01:16:31 +03:00
func (h *BufPane) VSplitBuf(buf *buffer.Buffer) *BufPane {
return h.VSplitIndex(buf, h.Buf.Settings["splitright"].(bool))
}
2021-08-22 00:58:30 +03:00
// HSplitBuf opens the given buffer in a new horizontal split.
2020-02-06 01:16:31 +03:00
func (h *BufPane) HSplitBuf(buf *buffer.Buffer) *BufPane {
return h.HSplitIndex(buf, h.Buf.Settings["splitbottom"].(bool))
}
2021-08-22 00:58:30 +03:00
// Close this pane.
2019-01-19 23:37:59 +03:00
func (h *BufPane) Close() {
2019-01-14 05:06:58 +03:00
h.Buf.Close()
}
2019-01-05 01:40:56 +03:00
2021-08-22 00:58:30 +03:00
// SetActive marks this pane as active.
2019-08-04 09:53:33 +03: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 23:27:43 +03:00
// BufKeyActions contains the list of all possible key actions the bufhandler could execute
2018-08-28 02:53:08 +03:00
var BufKeyActions = map[string]BufKeyAction{
2020-04-30 07:54:02 +03: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 07:54:02 +03:00
"FindNext": (*BufPane).FindNext,
"FindPrevious": (*BufPane).FindPrevious,
"DiffNext": (*BufPane).DiffNext,
"DiffPrevious": (*BufPane).DiffPrevious,
2020-04-30 07:54:02 +03: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 07:54:02 +03: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 05:55:49 +03:00
"ForceQuit": (*BufPane).ForceQuit,
2020-04-30 07:54:02 +03: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 07:54:02 +03:00
"None": (*BufPane).None,
2018-08-28 02:53:08 +03:00
// This was changed to InsertNewline but I don't want to break backwards compatibility
2020-04-30 07:54:02 +03:00
"InsertEnter": (*BufPane).InsertNewline,
2018-08-28 02:53:08 +03:00
}
2019-01-03 23:27:43 +03:00
// BufMouseActions contains the list of all possible mouse actions the bufhandler could execute
2018-08-28 02:53:08 +03:00
var BufMouseActions = map[string]BufMouseAction{
2019-01-19 23:37:59 +03:00
"MousePress": (*BufPane).MousePress,
"MouseDrag": (*BufPane).MouseDrag,
"MouseRelease": (*BufPane).MouseRelease,
2019-01-19 23:37:59 +03:00
"MouseMultiCursor": (*BufPane).MouseMultiCursor,
2018-08-28 02:53:08 +03:00
}
2019-01-03 07:26:40 +03: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 07:26:40 +03:00
}