From 2947128857e492ca73b6fc22e6afcb3713fca828 Mon Sep 17 00:00:00 2001 From: Luke Wilson Date: Fri, 20 Aug 2021 18:58:09 -0500 Subject: [PATCH] A lot of changes that I wasn't able to properly commit months ago --- .gitignore | 1 + cmd/qedit.go | 510 ++++++++++++++++++++++++++++++++++ go.sum | 0 internal/ui/gotolinedialog.go | 0 pkg/buffer/buffer.go | 10 + pkg/buffer/cursor.go | 58 +++- pkg/buffer/highlighter.go | 0 pkg/buffer/language.go | 0 pkg/buffer/rope.go | 34 +++ pkg/ui/messagedialog.go | 0 pkg/ui/panel.go | 0 pkg/ui/panelcontainer.go | 0 pkg/ui/textedit.go | 14 +- 13 files changed, 621 insertions(+), 6 deletions(-) create mode 100644 cmd/qedit.go mode change 100755 => 100644 go.sum mode change 100755 => 100644 internal/ui/gotolinedialog.go mode change 100755 => 100644 pkg/buffer/buffer.go mode change 100755 => 100644 pkg/buffer/cursor.go mode change 100755 => 100644 pkg/buffer/highlighter.go mode change 100755 => 100644 pkg/buffer/language.go mode change 100755 => 100644 pkg/buffer/rope.go mode change 100755 => 100644 pkg/ui/messagedialog.go mode change 100755 => 100644 pkg/ui/panel.go mode change 100755 => 100644 pkg/ui/panelcontainer.go mode change 100755 => 100644 pkg/ui/textedit.go diff --git a/.gitignore b/.gitignore index 07fc224..fcf5a02 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # Block executable qedit* +!cmd/qedit.go # Unblock screenshots !screenshots/* diff --git a/cmd/qedit.go b/cmd/qedit.go new file mode 100644 index 0000000..c90ee8a --- /dev/null +++ b/cmd/qedit.go @@ -0,0 +1,510 @@ +package main + +import ( + "errors" + "flag" + "fmt" + "io/fs" + "io/ioutil" + "log" + "os" + "runtime" + "runtime/pprof" + + "github.com/fivemoreminix/qedit/internal/clipboard" + internal_ui "github.com/fivemoreminix/qedit/internal/ui" + "github.com/fivemoreminix/qedit/pkg/ui" + "github.com/gdamore/tcell/v2" +) + +var ( + cpuprofile = flag.String("cpuprofile", "", "write cpu profile to `file`") + memprofile = flag.String("memprofile", "", "write memory profile to `file`") +) + +var theme = ui.Theme{ + "StatusBar": tcell.Style{}.Foreground(tcell.ColorBlack).Background(tcell.ColorLightGray), +} + +var ( + screen *tcell.Screen + + menuBar *ui.MenuBar + panelContainer *ui.PanelContainer + dialog ui.Component // nil if not present (has exclusive focus) + + focusedComponent ui.Component = nil +) + +func changeFocus(to ui.Component) { + if focusedComponent != nil { + focusedComponent.SetFocused(false) + } + focusedComponent = to + to.SetFocused(true) +} + +func showErrorDialog(title string, message string, callback func()) { + dialog = ui.NewMessageDialog(title, message, ui.MessageKindError, nil, &theme, func(string) { + if callback != nil { + callback() + } else { + dialog = nil + changeFocus(panelContainer) // Default behavior: focus panelContainer + } + }) + changeFocus(dialog) +} + +func getActiveTabContainer() *ui.TabContainer { + if panelContainer.GetSelected() != nil { + return panelContainer.GetSelected().(*ui.TabContainer) + } + return nil +} + +// returns nil if no TextEdit is visible +func getActiveTextEdit() *ui.TextEdit { + tabContainer := getActiveTabContainer() + if tabContainer != nil && tabContainer.GetTabCount() > 0 { + tab := tabContainer.GetTab(tabContainer.GetSelectedTabIdx()) + te := tab.Child.(*ui.TextEdit) + return te + } + return nil +} + +// Shows the Save As... dialog for saving unnamed files +func saveAs() { + callback := func(filePaths []string) { + te := getActiveTextEdit() // te should have value if we are here + tabContainer := getActiveTabContainer() + tab := tabContainer.GetTab(tabContainer.GetSelectedTabIdx()) + + // If we got the callback, it is safe to assume there are one or more files + f, err := os.OpenFile(filePaths[0], os.O_WRONLY|os.O_CREATE|os.O_TRUNC, fs.ModePerm) + if err != nil { + showErrorDialog("Could not open file for writing", fmt.Sprintf("File at %#v could not be opened with write permissions. Maybe another program has it open? %v", filePaths[0], err), nil) + return + } + defer f.Close() + + _, err = te.Buffer.WriteTo(f) + if err != nil { + showErrorDialog("Failed to write to file", fmt.Sprintf("File at %#v was opened for writing, but an error occurred while writing the buffer. %v", filePaths[0], err), nil) + return + } + te.Dirty = false + + te.FilePath = filePaths[0] + tab.Name = filePaths[0] + + dialog = nil // Hide the file selector + changeFocus(panelContainer) + tab.Name = filePaths[0] + } + + dialog = ui.NewFileSelectorDialog( + screen, + "Select a file to overwrite", + false, + &theme, + callback, + func() { // Dialog canceled + dialog = nil + changeFocus(panelContainer) + }, + ) + changeFocus(dialog) +} + +func main() { + flag.Parse() + if *cpuprofile != "" { + f, err := os.Create(*cpuprofile) + if err != nil { + log.Fatal("Could not create CPU profile: ", err) + } + defer f.Close() + if err := pprof.StartCPUProfile(f); err != nil { + log.Fatal("Could not start CPU profile: ", err) + } + defer pprof.StopCPUProfile() + } + + s, err := tcell.NewScreen() + screen = &s + if err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + os.Exit(1) + } + if err := s.Init(); err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + os.Exit(1) + } + defer s.Fini() // Useful for handling panics + + var closing bool + sizex, sizey := s.Size() + + panelContainer = ui.NewPanelContainer(&theme) + panelContainer.SetPos(0, 1) + panelContainer.SetSize(sizex, sizey-2) + + panelContainer.SetSelected(ui.NewTabContainer(&theme)) + + changeFocus(panelContainer) // panelContainer focused by default + + // Open files from command-line arguments + if flag.NArg() > 0 { + for i := 0; i < flag.NArg(); i++ { + arg := flag.Arg(i) + _, err := os.Stat(arg) + + var dirty bool + var bytes []byte + + if errors.Is(err, os.ErrNotExist) { // If the file does not exist... + dirty = true + } else { // If the file exists... + file, err := os.Open(arg) + if err != nil { + showErrorDialog("File could not be opened", fmt.Sprintf("File at %#v could not be opened. %v", arg, err), nil) + continue + } + defer file.Close() + + bytes, err = ioutil.ReadAll(file) + if err != nil { + showErrorDialog("Could not read file", fmt.Sprintf("File at %#v was opened, but could not be read. %v", arg, err), nil) + continue + } + } + + textEdit := ui.NewTextEdit(screen, arg, bytes, &theme) + textEdit.Dirty = dirty + getActiveTabContainer().AddTab(arg, textEdit) + } + panelContainer.SetFocused(true) // Lets any opened TextEdit component know to be focused + } + + _, err = clipboard.ClipInitialize(clipboard.ClipExternal) + if err != nil { + showErrorDialog("Error Initializing Clipboard", fmt.Sprintf("%v\n\nAn internal clipboard will be used, instead.", err), nil) + } + + menuBar = ui.NewMenuBar(&theme) + + fileMenu := ui.NewMenu("File", 0, &theme) + + fileMenu.AddItems([]ui.Item{&ui.ItemEntry{Name: "New File", Shortcut: "Ctrl+N", Callback: func() { + textEdit := ui.NewTextEdit(screen, "", []byte{}, &theme) // No file path, no contents + tabContainer := getActiveTabContainer() + if tabContainer == nil { + tabContainer = ui.NewTabContainer(&theme) + panelContainer.SetSelected(tabContainer) + } + tabContainer.AddTab("noname", textEdit) + tabContainer.FocusTab(tabContainer.GetTabCount() - 1) + changeFocus(panelContainer) + }}, &ui.ItemEntry{Name: "Open...", Shortcut: "Ctrl+O", Callback: func() { + callback := func(filePaths []string) { + tabContainer := getActiveTabContainer() + + var errOccurred bool + for _, path := range filePaths { + file, err := os.Open(path) + if err != nil { + showErrorDialog("File could not be opened", fmt.Sprintf("File at %#v could not be opened. %v", path, err), nil) + errOccurred = true + continue + } + defer file.Close() + + bytes, err := ioutil.ReadAll(file) + if err != nil { + showErrorDialog("Could not read file", fmt.Sprintf("File at %#v was opened, but could not be read. %v", path, err), nil) + errOccurred = true + continue + } + + textEdit := ui.NewTextEdit(screen, path, bytes, &theme) + if tabContainer == nil { + tabContainer = ui.NewTabContainer(&theme) + panelContainer.SetSelected(tabContainer) + } + tabContainer.AddTab(path, textEdit) + } + + if !errOccurred { // Prevent hiding the error dialog + dialog = nil // Hide the file selector + changeFocus(panelContainer) + if tabContainer.GetTabCount() > 0 { + tabContainer.FocusTab(tabContainer.GetTabCount() - 1) + } + } + } + dialog = ui.NewFileSelectorDialog( + screen, + "Comma-separated files or a directory", + true, + &theme, + callback, + func() { // Dialog is canceled + dialog = nil + changeFocus(panelContainer) + }, + ) + changeFocus(dialog) + }}, &ui.ItemEntry{Name: "Save", Shortcut: "Ctrl+S", Callback: func() { + te := getActiveTextEdit() + if te != nil { + if te.FilePath != "" { + f, err := os.OpenFile(te.FilePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, fs.ModePerm) + if err != nil { + showErrorDialog("Could not open file for writing", fmt.Sprintf("File at %#v could not be opened with write permissions. Maybe another program has it open? %v", te.FilePath, err), nil) + return + } + defer f.Close() + + _, err = te.Buffer.WriteTo(f) // TODO: check count + if err != nil { + showErrorDialog("Failed to write to file", fmt.Sprintf("File at %#v was opened for writing, but an error occurred while writing the buffer. %v", te.FilePath, err), nil) + return + } + te.Dirty = false + + changeFocus(panelContainer) + } else { + saveAs() + } + } + }}, &ui.ItemEntry{Name: "Save As...", QuickChar: 5, Callback: saveAs}, &ui.ItemSeparator{}, + &ui.ItemEntry{Name: "Close", Shortcut: "Ctrl+Q", Callback: func() { + tabContainer := getActiveTabContainer() + if tabContainer != nil && tabContainer.GetTabCount() > 0 { + tabContainer.RemoveTab(tabContainer.GetSelectedTabIdx()) + } else { + // if the selected is root: close editor. otherwise close panel + if panelContainer.IsRootSelected() { + closing = true + } else { + panelContainer.DeleteSelected() + } + } + }}}) + + panelMenu := ui.NewMenu("Panel", 0, &theme) + + panelMenu.AddItems([]ui.Item{&ui.ItemEntry{Name: "Focus Next", Shortcut: "Alt+.", Callback: func() { + panelContainer.SelectNext() + changeFocus(panelContainer) + }}, &ui.ItemEntry{Name: "Focus Prev", Shortcut: "Alt+,", Callback: func() { + panelContainer.SelectPrev() + changeFocus(panelContainer) + }}, &ui.ItemEntry{Name: "Focus Up", QuickChar: -1, Shortcut: "Alt+Up", Callback: func() { + + }}, &ui.ItemEntry{Name: "Focus Down", QuickChar: -1, Shortcut: "Alt+Down", Callback: func() { + + }}, &ui.ItemEntry{Name: "Focus Left", QuickChar: -1, Shortcut: "Alt+Left", Callback: func() { + + }}, &ui.ItemEntry{Name: "Focus Right", QuickChar: -1, Shortcut: "Alt+Right", Callback: func() { + + }}, &ui.ItemSeparator{}, &ui.ItemEntry{Name: "Split Top", QuickChar: 6, Callback: func() { + panelContainer.SplitSelected(ui.SplitVertical, ui.NewTabContainer(&theme)) + panelContainer.SwapNeighborsSelected() + panelContainer.SelectPrev() + changeFocus(panelContainer) + }}, &ui.ItemEntry{Name: "Split Bottom", QuickChar: 6, Callback: func() { + panelContainer.SplitSelected(ui.SplitVertical, ui.NewTabContainer(&theme)) + panelContainer.SelectNext() + changeFocus(panelContainer) + }}, &ui.ItemEntry{Name: "Split Left", QuickChar: 6, Callback: func() { + panelContainer.SplitSelected(ui.SplitHorizontal, ui.NewTabContainer(&theme)) + panelContainer.SwapNeighborsSelected() + panelContainer.SelectPrev() + changeFocus(panelContainer) + }}, &ui.ItemEntry{Name: "Split Right", QuickChar: 6, Callback: func() { + panelContainer.SplitSelected(ui.SplitHorizontal, ui.NewTabContainer(&theme)) + panelContainer.SelectNext() + changeFocus(panelContainer) + }}, &ui.ItemSeparator{}, &ui.ItemEntry{Name: "Move", Shortcut: "Ctrl+M", Callback: func() { + + }}, &ui.ItemEntry{Name: "Resize", Shortcut: "Ctrl+R", Callback: func() { + + }}, &ui.ItemEntry{Name: "Toggle Floating", Callback: func() { + panelContainer.FloatSelected() + if !panelContainer.GetFloatingFocused() { + panelContainer.SetFloatingFocused(true) + } + changeFocus(panelContainer) + }}}) + + editMenu := ui.NewMenu("Edit", 0, &theme) + + editMenu.AddItems([]ui.Item{&ui.ItemEntry{Name: "Cut", Shortcut: "Ctrl+X", Callback: func() { + te := getActiveTextEdit() + if te != nil { + bytes := te.GetSelectedBytes() + var err error + if len(bytes) > 0 { // If something is selected... + err = clipboard.ClipWrite(string(bytes)) // Add the selectedStr to clipboard + if err != nil { + showErrorDialog("Clipboard Failure", fmt.Sprintf("%v", err), nil) + } + te.Delete(false) // Delete selection + } + if err == nil { // Prevent hiding error dialog + changeFocus(panelContainer) + } + } + }}, &ui.ItemEntry{Name: "Copy", Shortcut: "Ctrl+C", Callback: func() { + te := getActiveTextEdit() + if te != nil { + bytes := te.GetSelectedBytes() + var err error + if len(bytes) > 0 { // If there is something selected... + err = clipboard.ClipWrite(string(bytes)) // Add selectedStr to clipboard + if err != nil { + showErrorDialog("Clipboard Failure", fmt.Sprintf("%v", err), nil) + } + } + if err == nil { + changeFocus(panelContainer) + } + } + }}, &ui.ItemEntry{Name: "Paste", Shortcut: "Ctrl+V", Callback: func() { + te := getActiveTextEdit() + if te != nil { + contents, err := clipboard.ClipRead() + if err != nil { + showErrorDialog("Clipboard Failure", fmt.Sprintf("%v", err), nil) + } else { + te.Insert(contents) + changeFocus(panelContainer) + } + } + }}, &ui.ItemSeparator{}, &ui.ItemEntry{Name: "Select All", QuickChar: 7, Shortcut: "Ctrl+A", Callback: func() { + + }}, &ui.ItemEntry{Name: "Select Line", QuickChar: 7, Callback: func() { + + }}}) + + searchMenu := ui.NewMenu("Search", 0, &theme) + + searchMenu.AddItems([]ui.Item{&ui.ItemEntry{Name: "Find and Replace...", Shortcut: "Ctrl+F", Callback: func() { + s.Beep() + }}, &ui.ItemEntry{Name: "Find in Directory...", QuickChar: 8, Callback: func() { + + }}, &ui.ItemSeparator{}, &ui.ItemEntry{Name: "Go to line...", Shortcut: "Ctrl+G", Callback: func() { + te := getActiveTextEdit() + if te != nil { + callback := func(line int) { + te := getActiveTextEdit() + te.SetCursor(te.GetCursor().SetLineCol(line-1, 0)) + // Hide dialog + dialog = nil + changeFocus(panelContainer) + } + dialog = internal_ui.NewGotoLineDialog(screen, &theme, callback, func() { + // Dialog canceled + dialog = nil + changeFocus(panelContainer) + }) + changeFocus(dialog) + } + }}}) + + menuBar.AddMenu(fileMenu) + menuBar.AddMenu(panelMenu) + menuBar.AddMenu(editMenu) + menuBar.AddMenu(searchMenu) + + for !closing { + s.Clear() + + // Draw background (grey and black checkerboard) + // TODO: draw checkered background on panics with error dialog + //ui.DrawRect(screen, 0, 0, sizex, sizey, '▚', tcell.Style{}.Foreground(tcell.ColorGrey).Background(tcell.ColorBlack)) + ui.DrawRect(s, 0, 1, sizex, sizey-1, ' ', tcell.Style{}.Background(tcell.ColorBlack)) + + panelContainer.Draw(s) + menuBar.Draw(s) + + if dialog != nil { + // Update fileSelector dialog pos and size + diagMinX, diagMinY := dialog.GetMinSize() + dialog.SetSize(diagMinX, diagMinY) + dialog.SetPos(sizex/2-diagMinX/2, sizey/2-diagMinY/2) // Center + + dialog.Draw(s) + } + + // Draw statusbar + ui.DrawRect(s, 0, sizey-1, sizex, 1, ' ', theme["StatusBar"]) + if te := getActiveTextEdit(); te != nil { + var delim string + if te.IsCRLF { + delim = "CRLF" + } else { + delim = "LF" + } + + line, col := te.GetCursor().GetLineCol() + + var tabs string + if te.UseHardTabs { + tabs = "Tabs: Hard" + } else { + tabs = "Tabs: Spaces" + } + + str := fmt.Sprintf(" Filetype: %s %d, %d %s %s", "None", line+1, col+1, delim, tabs) + ui.DrawStr(s, 0, sizey-1, str, theme["StatusBar"]) + } + + s.Show() + + switch ev := s.PollEvent().(type) { + case *tcell.EventResize: + sizex, sizey = s.Size() + + menuBar.SetSize(sizex, 1) + panelContainer.SetSize(sizex, sizey-2) + + s.Sync() // Redraw everything + case *tcell.EventKey: + // On Escape, we change focus between editor and the MenuBar. + if dialog == nil { + if ev.Key() == tcell.KeyEscape { + if focusedComponent == panelContainer { + changeFocus(menuBar) + } else { + changeFocus(panelContainer) + } + } + + if ev.Modifiers()&tcell.ModCtrl != 0 { + handled := menuBar.HandleEvent(ev) + if handled { + continue // Avoid passing the event to the focusedComponent + } + } + } + + focusedComponent.HandleEvent(ev) + } + } + + if *memprofile != "" { + f, err := os.Create(*memprofile) + if err != nil { + log.Fatal("Could not create memory profile: ", err) + } + defer f.Close() + runtime.GC() // Get updated statistics + if err := pprof.WriteHeapProfile(f); err != nil { + log.Fatal("Could not write memory profile: ", err) + } + } +} diff --git a/go.sum b/go.sum old mode 100755 new mode 100644 diff --git a/internal/ui/gotolinedialog.go b/internal/ui/gotolinedialog.go old mode 100755 new mode 100644 diff --git a/pkg/buffer/buffer.go b/pkg/buffer/buffer.go old mode 100755 new mode 100644 index 284b2ab..13599bd --- a/pkg/buffer/buffer.go +++ b/pkg/buffer/buffer.go @@ -23,6 +23,16 @@ type Buffer interface { // so do not write to it. Slice(startLine, startCol, endLine, endCol int) []byte + // RuneAtPos returns the UTF-8 rune at the byte position `pos` of the buffer. The + // position must be a correct position, otherwise zero is returned. + RuneAtPos(pos int) rune + + // EachRuneAtPos executes the function `f` at each rune after byte position `pos`. + // This function should be used as opposed to performing a "per character" operation + // manually, as it enables caching buffer operations and safety checks. The function + // returns when the end of the buffer is met or `f` returns true. + EachRuneAtPos(pos int, f func(pos int, r rune) bool) + // Bytes returns all of the bytes in the buffer. This function is very likely // to copy all of the data in the buffer. Use sparingly. Try using other methods, // where possible. diff --git a/pkg/buffer/cursor.go b/pkg/buffer/cursor.go old mode 100755 new mode 100644 index 2a61a95..7edc03d --- a/pkg/buffer/cursor.go +++ b/pkg/buffer/cursor.go @@ -1,6 +1,9 @@ package buffer -import "math" +import ( + "math" + "unicode" +) // So why is the code for moving the cursor in the buffer package, and not in the // TextEdit component? Well, it used to be, but it sucked that way. The cursor @@ -86,6 +89,41 @@ func (c Cursor) Down() Cursor { return c } +// NextWordBoundaryEnd proceeds to the position after the last character of the +// next word boundary to the right of the Cursor. A word boundary is the +// beginning or end of any sequence of similar or same-classed characters. +// Whitespace is skipped. +func (c Cursor) NextWordBoundaryEnd() Cursor { + // Get position of cursor in buffer as pos + // get classification of character at pos or assume none if whitespace + // for each pos until end of buffer: pos + 1 (at end) + // if pos char is not of previous pos char class: + // set cursor position as pos + // + + // only skip contiguous characters for word characters + // jump to position *after* any symbols + + pos := (*c.buffer).LineColToPos(c.line, c.col) + startClass := getRuneCharclass((*c.buffer).RuneAtPos(pos)) + pos++ + (*c.buffer).EachRuneAtPos(pos, func(rpos int, r rune) bool { + class := getRuneCharclass(r) + if class != startClass && class != charwhitespace { + return true + } + return false + }) + + c.line, c.col = (*c.buffer).PosToLineCol(pos) + + return c +} + +func (c Cursor) PrevWordBoundaryStart() Cursor { + return c +} + func (c Cursor) GetLineCol() (line, col int) { return c.line, c.col } @@ -101,3 +139,21 @@ func (c Cursor) SetLineCol(line, col int) Cursor { func (c Cursor) Eq(other Cursor) bool { return c.buffer == other.buffer && c.line == other.line && c.col == other.col } + +type charclass uint8 + +const ( + charwhitespace charclass = iota + charword + charsymbol +) + +func getRuneCharclass(r rune) charclass { + if unicode.IsSpace(r) { + return charwhitespace + } else if r == '_' || unicode.IsLetter(r) || unicode.IsDigit(r) { + return charword + } else { + return charsymbol + } +} diff --git a/pkg/buffer/highlighter.go b/pkg/buffer/highlighter.go old mode 100755 new mode 100644 diff --git a/pkg/buffer/language.go b/pkg/buffer/language.go old mode 100755 new mode 100644 diff --git a/pkg/buffer/rope.go b/pkg/buffer/rope.go old mode 100755 new mode 100644 index 7cdbeac..ea6af6a --- a/pkg/buffer/rope.go +++ b/pkg/buffer/rope.go @@ -96,6 +96,40 @@ func (b *RopeBuffer) Slice(startLine, startCol, endLine, endCol int) []byte { return b.rope.Slice(b.LineColToPos(startLine, startCol), endPos+1) } +// RuneAtPos returns the UTF-8 rune at the byte position `pos` of the buffer. The +// position must be a correct position, otherwise zero is returned. +func (b *RopeBuffer) RuneAtPos(pos int) (val rune) { + _, r := b.rope.SplitAt(pos) + l, _ := r.SplitAt(b.rope.Len() - pos) + + l.EachLeaf(func(n *ropes.Node) bool { + data := n.Value() // Reference; not a copy. + val, _ = utf8.DecodeRune(data[0:]) + return true + }) + + return 0 +} + +// EachRuneAtPos executes the function `f` at each rune after byte position `pos`. +// This function should be used as opposed to performing a "per character" operation +// manually, as it enables caching buffer operations and safety checks. The function +// returns when the end of the buffer is met or `f` returns true. +func (b *RopeBuffer) EachRuneAtPos(pos int, f func(pos int, r rune) bool) { + _, r := b.rope.SplitAt(pos) + l, _ := r.SplitAt(b.rope.Len() - pos) + + l.EachLeaf(func(n *ropes.Node) bool { + data := n.Value() // Reference; not a copy. + for i, r := range string(data) { + if f(pos+i, r) { + return true + } + } + return false + }) +} + // Bytes returns all of the bytes in the buffer. This function is very likely // to copy all of the data in the buffer. Use sparingly. Try using other methods, // where possible. diff --git a/pkg/ui/messagedialog.go b/pkg/ui/messagedialog.go old mode 100755 new mode 100644 diff --git a/pkg/ui/panel.go b/pkg/ui/panel.go old mode 100755 new mode 100644 diff --git a/pkg/ui/panelcontainer.go b/pkg/ui/panelcontainer.go old mode 100755 new mode 100644 diff --git a/pkg/ui/textedit.go b/pkg/ui/textedit.go old mode 100755 new mode 100644 index fb56354..42df785 --- a/pkg/ui/textedit.go +++ b/pkg/ui/textedit.go @@ -505,7 +505,7 @@ func (t *TextEdit) HandleEvent(event tcell.Event) bool { switch ev.Key() { // Cursor movement case tcell.KeyUp: - if ev.Modifiers() == tcell.ModShift { + if ev.Modifiers()&tcell.ModShift != 0 { if !t.selectMode { var endCursor buffer.Cursor if cursLine, _ := t.cursor.GetLineCol(); cursLine != 0 { @@ -534,7 +534,7 @@ func (t *TextEdit) HandleEvent(event tcell.Event) bool { } t.ScrollToCursor() case tcell.KeyDown: - if ev.Modifiers() == tcell.ModShift { + if ev.Modifiers()&tcell.ModShift != 0 { if !t.selectMode { t.selection.Start = t.cursor t.SetCursor(t.cursor.Down()) @@ -557,7 +557,7 @@ func (t *TextEdit) HandleEvent(event tcell.Event) bool { } t.ScrollToCursor() case tcell.KeyLeft: - if ev.Modifiers() == tcell.ModShift { + if ev.Modifiers()&tcell.ModShift != 0 { if !t.selectMode { t.SetCursor(t.cursor.Left()) t.selection.Start, t.selection.End = t.cursor, t.cursor @@ -579,7 +579,7 @@ func (t *TextEdit) HandleEvent(event tcell.Event) bool { } t.ScrollToCursor() case tcell.KeyRight: - if ev.Modifiers() == tcell.ModShift { + if ev.Modifiers()&tcell.ModShift != 0 { if !t.selectMode { t.selection.Start, t.selection.End = t.cursor, t.cursor t.selectMode = true @@ -595,7 +595,11 @@ func (t *TextEdit) HandleEvent(event tcell.Event) bool { } } else { t.selectMode = false - t.SetCursor(t.cursor.Right()) + if ev.Modifiers()&tcell.ModCtrl != 0 { + t.SetCursor(t.cursor.NextWordBoundaryEnd()) + } else { + t.SetCursor(t.cursor.Right()) + } } t.ScrollToCursor() case tcell.KeyHome: