├── .gitignore ├── test.bat ├── run.bat ├── install.bat ├── assets ├── logo.png ├── main.png ├── header.png ├── build-window.png ├── file-picker.png ├── search-grep.png ├── split-windows.png └── build-window-max.png ├── fonts ├── jetbrainsmono.ttf └── liberationmono-regular.ttf ├── Makefile ├── sample-config ├── cmd └── preditor │ └── main.go ├── byteutils ├── byteutils_test.go └── byteutils.go ├── stack.go ├── go.mod ├── ripgrep.go ├── README.md ├── filetypes.go ├── CHANGELOG.txt ├── buffer_test.go ├── go.sum ├── config.go ├── lists.go ├── defaults.go ├── preditor.go └── buffer.go /.gitignore: -------------------------------------------------------------------------------- 1 | core 2 | .idea -------------------------------------------------------------------------------- /test.bat: -------------------------------------------------------------------------------- 1 | go test -v ./... 2 | 3 | -------------------------------------------------------------------------------- /run.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | go run ./cmd/preditor 4 | -------------------------------------------------------------------------------- /install.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | go install ./cmd/preditor -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amirrezaask/Preditor/HEAD/assets/logo.png -------------------------------------------------------------------------------- /assets/main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amirrezaask/Preditor/HEAD/assets/main.png -------------------------------------------------------------------------------- /assets/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amirrezaask/Preditor/HEAD/assets/header.png -------------------------------------------------------------------------------- /assets/build-window.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amirrezaask/Preditor/HEAD/assets/build-window.png -------------------------------------------------------------------------------- /assets/file-picker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amirrezaask/Preditor/HEAD/assets/file-picker.png -------------------------------------------------------------------------------- /assets/search-grep.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amirrezaask/Preditor/HEAD/assets/search-grep.png -------------------------------------------------------------------------------- /fonts/jetbrainsmono.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amirrezaask/Preditor/HEAD/fonts/jetbrainsmono.ttf -------------------------------------------------------------------------------- /assets/split-windows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amirrezaask/Preditor/HEAD/assets/split-windows.png -------------------------------------------------------------------------------- /assets/build-window-max.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amirrezaask/Preditor/HEAD/assets/build-window-max.png -------------------------------------------------------------------------------- /fonts/liberationmono-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amirrezaask/Preditor/HEAD/fonts/liberationmono-regular.ttf -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | go build ./cmd/preditor 3 | run: 4 | go run ./cmd/preditor 5 | install: 6 | go install ./cmd/preditor 7 | -------------------------------------------------------------------------------- /sample-config: -------------------------------------------------------------------------------- 1 | font Consolas 2 | font_size 20 3 | 4 | background #062329 5 | foreground #d3b58d 6 | statusbar_background #d3b58d 7 | statusbar_foreground #000000 8 | line_numbers_foreground #ffffff -------------------------------------------------------------------------------- /cmd/preditor/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/amirrezaask/preditor" 5 | ) 6 | 7 | func main() { 8 | setKeyBindings() 9 | 10 | // creates new instance of the editor 11 | editor, err := preditor.New() 12 | if err != nil { 13 | panic(err) 14 | } 15 | 16 | setKeyBindings() 17 | 18 | // start main loop 19 | editor.StartMainLoop() 20 | 21 | } 22 | 23 | // Sample 24 | func setKeyBindings() { 25 | // preditor.GlobalKeymap.BindKey(preditor.Key{K: "\\", Alt: true}, func(c *preditor.Context) { preditor.VSplit(c) }) 26 | // preditor.GlobalKeymap.BindKey(preditor.Key{K: "=", Alt: true}, func(c *preditor.Context) { preditor.HSplit(c) }) 27 | } 28 | -------------------------------------------------------------------------------- /byteutils/byteutils_test.go: -------------------------------------------------------------------------------- 1 | package byteutils 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestFindMatchingClosedForward(t *testing.T) { 10 | assert.Equal(t, 5, FindMatchingClosedForward([]byte(`({[}])`), 0)) 11 | assert.Equal(t, 3, FindMatchingClosedForward([]byte(`({[}])`), 1)) 12 | assert.Equal(t, 4, FindMatchingClosedForward([]byte(`({[}])`), 2)) 13 | } 14 | 15 | func TestFindMatchingOpenBackward(t *testing.T) { 16 | assert.Equal(t, 0, FindMatchingOpenBackward([]byte(`({[}])`), 5)) 17 | assert.Equal(t, 1, FindMatchingOpenBackward([]byte(`({[}])`), 3)) 18 | assert.Equal(t, 2, FindMatchingOpenBackward([]byte(`({[}])`), 4)) 19 | } 20 | -------------------------------------------------------------------------------- /stack.go: -------------------------------------------------------------------------------- 1 | package preditor 2 | 3 | import "errors" 4 | 5 | type Stack[T any] struct { 6 | data []T 7 | size int 8 | } 9 | 10 | func NewStack[T any](size int) *Stack[T] { 11 | return &Stack[T]{data: make([]T, size), size: size} 12 | } 13 | 14 | var ( 15 | EmptyStack = errors.New("empty stack") 16 | ) 17 | 18 | func (s *Stack[T]) Pop() (T, error) { 19 | if len(s.data) == 0 { 20 | return *new(T), EmptyStack 21 | } 22 | last := s.data[len(s.data)-1] 23 | s.data = s.data[:len(s.data)-1] 24 | return last, nil 25 | } 26 | 27 | func (s *Stack[T]) Top() (T, error) { 28 | if len(s.data) == 0 { 29 | return *new(T), EmptyStack 30 | } 31 | last := s.data[len(s.data)-1] 32 | return last, nil 33 | } 34 | 35 | func (s *Stack[T]) Push(e T) { 36 | s.data = append(s.data, e) 37 | if len(s.data) > s.size { 38 | s.data = []T{e} 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/amirrezaask/preditor 2 | 3 | go 1.21.3 4 | 5 | require ( 6 | github.com/davecgh/go-spew v1.1.1 7 | github.com/flopp/go-findfont v0.1.0 8 | github.com/gen2brain/raylib-go/raylib v0.0.0-20231118131254-fed470e4458f 9 | github.com/lithammer/fuzzysearch v1.1.8 10 | github.com/smacker/go-tree-sitter v0.0.0-20231215063300-06670b6cd560 11 | golang.design/x/clipboard v0.7.0 12 | ) 13 | 14 | require ( 15 | github.com/ebitengine/purego v0.5.0 // indirect 16 | github.com/pmezard/go-difflib v1.0.0 // indirect 17 | github.com/stretchr/testify v1.8.4 // indirect 18 | golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 // indirect 19 | golang.org/x/image v0.6.0 // indirect 20 | golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c // indirect 21 | golang.org/x/sys v0.14.0 // indirect 22 | golang.org/x/text v0.9.0 // indirect 23 | gopkg.in/yaml.v3 v3.0.1 // indirect 24 | ) 25 | -------------------------------------------------------------------------------- /ripgrep.go: -------------------------------------------------------------------------------- 1 | package preditor 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os/exec" 7 | "strings" 8 | ) 9 | 10 | func RipgrepAsync(pattern string, dir string) chan [][]byte { 11 | output := make(chan [][]byte) 12 | go func() { 13 | cmd := exec.Command("rg", "--vimgrep", pattern) 14 | stdError := &bytes.Buffer{} 15 | stdOut := &bytes.Buffer{} 16 | 17 | cmd.Stderr = stdError 18 | cmd.Stdout = stdOut 19 | cmd.Dir = dir 20 | if err := cmd.Run(); err != nil { 21 | fmt.Println("ERROR running rg:", err.Error()) 22 | return 23 | } 24 | 25 | output <- bytes.Split(stdOut.Bytes(), []byte("\n")) 26 | }() 27 | 28 | return output 29 | } 30 | 31 | func RipgrepFiles(cwd string) []string { 32 | cmd := exec.Command("rg", "--files") 33 | stdError := &bytes.Buffer{} 34 | stdOut := &bytes.Buffer{} 35 | 36 | cmd.Stderr = stdError 37 | cmd.Stdout = stdOut 38 | cmd.Dir = cwd 39 | if err := cmd.Run(); err != nil { 40 | fmt.Println("ERROR running rg files:", err.Error()) 41 | return nil 42 | } 43 | 44 | return strings.Split(stdOut.String(), "\n") 45 | } 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Preditor 2 | ## Disclaimer 3 | I don't recommend using this as your editor since I did that as an educational project for graphics rendering and GUI applications. I learnt alot about graphics and rendering in general in this project, and I will use that knowledge in my future projects, but in the mean time if you want a battle tested text editor I always recommend *Gnu Emacs*. 4 | 5 | ![Logo](assets/header.png) 6 | ## Programmable Editor 7 | Simple text editor implemented in Golang using Raylib with the goal of replacing Emacs for me, easier to extend and much faster and better language to work with than Elisp. 8 | 9 | # Screenshots 10 | ![Main](assets/main.png) 11 | #### File Picker 12 | ![Main](assets/file-picker.png) 13 | #### Searching text (ripgrep backend) 14 | ![Main](assets/search-grep.png) 15 | #### Split windows 16 | ![Main](assets/split-windows.png) 17 | #### Build window 18 | ![Main](assets/build-window.png) 19 | ![Main](assets/build-window-max.png) 20 | 21 | ## Credits 22 | - Allen Webster for 4coder editor which I took the default colorscheme and basic idea of having an editor that is extensible with an "actual" language. 23 | - Casey Muratori for handmadehero which I learnt a lot 24 | - Jonathan Blow for his inspiring character 25 | - Emacs/VSCode/Neovim which I took ideas from 26 | - Raylib for being an awesome yet simple graphics library -------------------------------------------------------------------------------- /filetypes.go: -------------------------------------------------------------------------------- 1 | package preditor 2 | 3 | import ( 4 | "github.com/smacker/go-tree-sitter/golang" 5 | "github.com/smacker/go-tree-sitter/php" 6 | "go/format" 7 | ) 8 | 9 | /* 10 | Treesitter captures: 11 | - type 12 | - string 13 | - ident 14 | - function_name 15 | */ 16 | 17 | var GoFileType = FileType{ 18 | Name: "Go", 19 | TabSize: 4, 20 | TSLanguage: golang.GetLanguage(), 21 | BeforeSave: func(e *BufferView) error { 22 | newBytes, err := format.Source(e.Buffer.Content) 23 | if err != nil { 24 | return err 25 | } 26 | 27 | e.Buffer.Content = newBytes 28 | return nil 29 | }, 30 | TSHighlightQuery: []byte(` 31 | [ 32 | "break" 33 | "case" 34 | "chan" 35 | "const" 36 | "continue" 37 | "default" 38 | "defer" 39 | "else" 40 | "fallthrough" 41 | "for" 42 | "func" 43 | "go" 44 | "goto" 45 | "if" 46 | "import" 47 | "interface" 48 | "map" 49 | "package" 50 | "range" 51 | "return" 52 | "select" 53 | "struct" 54 | "switch" 55 | "type" 56 | "var" 57 | ] @keyword 58 | 59 | (type_identifier) @type 60 | (comment) @comment 61 | [(interpreted_string_literal) (raw_string_literal)] @string 62 | (function_declaration name: (_) @function_name) 63 | (call_expression function: (_) @function_name) 64 | `), 65 | DefaultCompileCommand: "go build -v ./...", 66 | } 67 | 68 | var PHPFileType = FileType{ 69 | Name: "PHP", 70 | TabSize: 4, 71 | TSLanguage: php.GetLanguage(), 72 | TSHighlightQuery: []byte(``), 73 | } 74 | -------------------------------------------------------------------------------- /CHANGELOG.txt: -------------------------------------------------------------------------------- 1 | v0.5 2 | ===================== 3 | - New command to run grep and output in a buffer 4 | - Open locations outputed by compilers and grep programs 5 | - Treesitter syntax highlighting 6 | - Build window has 3 states now 7 | - Improve Line numbers rendering, now line numbers are rendered in a fixed sized space and lines don't move when line number width increases 8 | - ISearch renamed to Search since it's now using same prompt as other features which makes the implementation much simpler 9 | - Remove Lexers to decrease complexity, since for syntax highlighting we now have treesitter 10 | - Better complete for file pickers 11 | - More theme updates thanks to @JonathanBlow and @AllenWebster ( 4Coder, 4Coder_Fleury, Naysayer, Solarized_Dark, Solarized_Light) 12 | - Now buffers and their view are seperated and we can have multiple views (windows) for same buffer and editing at the same time 13 | - Highlight matching open/close parens/braces/brackets 14 | - QueryReplace Command: functionality similar to emacs 15 | - ActiveStatusbar* Colors to better differentiate between active and non active windows 16 | - Remove word lexer and calculate word boundaries in real time 17 | - Improve CWD detection in various places ( compilation buffers, list files ) 18 | - Add icon and header 19 | - Include line numbers when listing buffers 20 | - RevertBufferCommand: revert buffer to disk state. 21 | - Different strategy for searching in normal files and large files 22 | - Cursor Blinking 23 | - Handling of CRLF files 24 | - ToggleStatusbar 25 | - Remove Multiple Cursors, we will add macros in future. 26 | - Drag&Drop files into the editor 27 | - When you kill a bufferview, editor tries to find a suitable replacement for it. 28 | - Grep Buffers 29 | 30 | - Fix bug when doing ISearch first visible line was hidden behind ISearch prompt. 31 | - Fix line numbers bug where Goto line jumped to wrong line 32 | - Fix line numbers bug where line in statusbar was wrong 33 | - Fix and Improvements undo 34 | - Fix copy command not including last character 35 | - Fix moveRight and moveLeft functions to reset any selection. 36 | 37 | v0.4 38 | ===================== 39 | - BuildWindow: Special window rendered at bottom of screen for showing compile results. 40 | - Lexer infrastructure for rich features based on syntax. 41 | - Compilation: Compilation prompt && Compilation buffer. 42 | - CompileAsk and CompileNoAsk methods for Buffers. 43 | - Fix Horizontal Splits. 44 | - Improve font loading code, now we have default fonts embedded in executable. 45 | - Refactor Syntax Highlighting 46 | - Move statusbar to the top 47 | - Refactor Isearch 48 | - Improve DeleteWordBackward 49 | - Mouse Click now switches to correct window 50 | - Theme System 51 | - Decrease CPU consumption 52 | - C-l centralizes the view 53 | - 4coder_fleury theme. 54 | - Naysayer theme based on Jonathan Blow Emacs theme 55 | 56 | 57 | v0.3 58 | ====================== 59 | Multiple Cursors (WIP): 60 | Another selection on match 61 | Another selection next line 62 | Moving all selections in a direction: up/down/left/right 63 | Multiple Windows (WIP): 64 | spliting vertically 65 | spliting horizontaly (WIP) 66 | Closing window 67 | 68 | 69 | v0.2 70 | ====================== 71 | - Fuzzy File finder 72 | - Handling resizing events correctly in text buffer 73 | - zoom in/out 74 | - crashlogs 75 | - undo 76 | - grep buffer 77 | - revert from disk 78 | - scroll if needed 79 | 80 | 81 | v0.1 82 | ====================== 83 | - Basic syntax highlighting 84 | - Cut/Copy/Paste 85 | - Selecting text 86 | - config file 87 | - mouse/keyboard cursor movement 88 | - handle tabs correctly 89 | - buffers, keymaps and commands ( infrastructure ) 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /byteutils/byteutils.go: -------------------------------------------------------------------------------- 1 | package byteutils 2 | 3 | import ( 4 | "unicode" 5 | ) 6 | 7 | func SeekNextNonLetter(bs []byte, idx int) int { 8 | for i := idx + 1; i < len(bs); i++ { 9 | if i == len(bs) { 10 | continue 11 | } 12 | if !unicode.IsLetter(rune(bs[i])) { 13 | return i 14 | } 15 | } 16 | 17 | return len(bs) - 1 18 | } 19 | 20 | func SeekPreviousNonLetter(bs []byte, idx int) int { 21 | for i := idx - 1; i >= 0; i-- { 22 | if i == len(bs) { 23 | continue 24 | } 25 | if !unicode.IsLetter(rune(bs[i])) { 26 | return i 27 | } 28 | } 29 | return 0 30 | } 31 | 32 | func SeekPreviousLetter(bs []byte, idx int) int { 33 | for i := idx - 1; i < len(bs); i++ { 34 | if i == len(bs) { 35 | continue 36 | } 37 | if unicode.IsLetter(rune(bs[i])) { 38 | return i 39 | } 40 | } 41 | return -1 42 | } 43 | func SeekNextLetter(bs []byte, idx int) int { 44 | for i := idx + 1; i >= 0; i-- { 45 | if i == len(bs) { 46 | continue 47 | } 48 | if unicode.IsLetter(rune(bs[i])) { 49 | return i 50 | } 51 | } 52 | return -1 53 | } 54 | 55 | func PreviousWordInBuffer(bs []byte, idx int) int { 56 | var sawWord bool 57 | var sawWhitespaces bool 58 | for i := idx - 1; i >= 0; i-- { 59 | if sawWord { 60 | return i + 1 61 | } 62 | if i == 0 { 63 | return i 64 | } 65 | 66 | if !unicode.IsLetter(rune(bs[i])) { 67 | sawWhitespaces = true 68 | } else { 69 | if sawWhitespaces { 70 | sawWord = true 71 | if sawWord && sawWhitespaces { 72 | return i 73 | } 74 | } 75 | } 76 | } 77 | 78 | return -1 79 | 80 | } 81 | func NextWordInBuffer(bs []byte, idx int) int { 82 | var sawWord bool 83 | var sawWhitespaces bool 84 | for i := idx + 1; i < len(bs); i++ { 85 | if sawWord { 86 | return i + 1 87 | } 88 | if !unicode.IsLetter(rune(bs[i])) { 89 | sawWhitespaces = true 90 | } else { 91 | if sawWhitespaces { 92 | sawWord = true 93 | if sawWord && sawWhitespaces { 94 | return i 95 | } 96 | } 97 | } 98 | } 99 | 100 | return -1 101 | } 102 | 103 | func FindMatchingClosedForward(data []byte, idx int) int { 104 | in := data[idx] 105 | var matching byte 106 | if in == '[' { 107 | matching = ']' 108 | } else if in == '(' { 109 | matching = ')' 110 | } else if in == '{' { 111 | matching = '}' 112 | } else { 113 | return -1 114 | } 115 | 116 | getCharDelta := func(c byte) int { 117 | if c == in { 118 | return 1 119 | } else if c == matching { 120 | return -1 121 | } else { 122 | return 0 123 | } 124 | } 125 | 126 | state := 1 127 | for i := idx + 1; i < len(data); i++ { 128 | c := data[i] 129 | state += getCharDelta(c) 130 | if state == 0 { 131 | return i 132 | } 133 | } 134 | 135 | return -1 136 | } 137 | func FindMatchingOpenBackward(data []byte, idx int) int { 138 | in := data[idx] 139 | var matching byte 140 | if in == ')' { 141 | matching = '(' 142 | } else if in == ']' { 143 | matching = '[' 144 | } else if in == '}' { 145 | matching = '{' 146 | } else { 147 | return -1 148 | } 149 | 150 | getCharDelta := func(c byte) int { 151 | if c == in { 152 | return -1 153 | } else if c == matching { 154 | return 1 155 | } else { 156 | return 0 157 | } 158 | } 159 | 160 | state := -1 161 | for i := idx - 1; i >= 0; i-- { 162 | c := data[i] 163 | state += getCharDelta(c) 164 | if state == 0 { 165 | return i 166 | } 167 | } 168 | 169 | return -1 170 | } 171 | 172 | func FindMatching(data []byte, idx int) int { 173 | if len(data) == 0 || len(data) <= idx { 174 | return -1 175 | } 176 | switch data[idx] { 177 | case '{', '[', '(': 178 | return FindMatchingClosedForward(data, idx) 179 | case '}', ']', ')': 180 | return FindMatchingOpenBackward(data, idx) 181 | default: 182 | return -1 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /buffer_test.go: -------------------------------------------------------------------------------- 1 | package preditor 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestSearch(t *testing.T) { 9 | matched := matchPatternCaseInsensitive([]byte("Hello World"), []byte("Hell")) 10 | assert.Equal(t, [][]int{{0, 3}}, matched) 11 | } 12 | 13 | // func Test_Gotoline(t *testing.T) {} 14 | 15 | // ///////////// These are the buggy ones 16 | func Test_BufferInsertChar(t *testing.T) { 17 | bufferView := BufferView{ 18 | Buffer: &Buffer{ 19 | File: "", 20 | Content: []byte("12"), 21 | CRLF: false, 22 | }, 23 | Cursor: Cursor{ 24 | Point: 0, 25 | Mark: 0, 26 | }, 27 | ActionStack: NewStack[BufferAction](10), 28 | } 29 | BufferInsertChar(&bufferView, '0') 30 | assert.Equal(t, []byte("012"), bufferView.Buffer.Content) 31 | bufferView.Cursor.SetBoth(3) 32 | BufferInsertChar(&bufferView, '3') 33 | assert.Equal(t, []byte("0123"), bufferView.Buffer.Content) 34 | 35 | RevertLastBufferAction(&bufferView) 36 | assert.Equal(t, []byte("012"), bufferView.Buffer.Content) 37 | RevertLastBufferAction(&bufferView) 38 | assert.Equal(t, []byte("12"), bufferView.Buffer.Content) 39 | } 40 | 41 | func Test_RemoveRange(t *testing.T) { 42 | bufferView := BufferView{ 43 | Buffer: &Buffer{ 44 | File: "", 45 | Content: []byte("012345678\n012345678"), 46 | CRLF: false, 47 | }, 48 | Cursor: Cursor{ 49 | Point: 2, 50 | Mark: 2, 51 | }, 52 | ActionStack: NewStack[BufferAction](10), 53 | } 54 | 55 | bufferView.RemoveRange(3, 8, true) 56 | assert.Equal(t, "0128\n012345678", string(bufferView.Buffer.Content)) 57 | RevertLastBufferAction(&bufferView) 58 | assert.Equal(t, "012345678\n012345678", string(bufferView.Buffer.Content)) 59 | } 60 | 61 | func Test_KillLine(t *testing.T) { 62 | bufferView := BufferView{ 63 | Buffer: &Buffer{ 64 | File: "", 65 | Content: []byte("012345678\n012345678"), 66 | CRLF: false, 67 | }, 68 | Cursor: Cursor{ 69 | Point: 2, 70 | Mark: 2, 71 | }, 72 | ActionStack: NewStack[BufferAction](10), 73 | } 74 | bufferView.calcRenderState() 75 | KillLine(&bufferView) 76 | assert.Equal(t, "01\n012345678", string(bufferView.Buffer.Content)) 77 | RevertLastBufferAction(&bufferView) 78 | assert.Equal(t, "012345678\n012345678", string(bufferView.Buffer.Content)) 79 | 80 | } 81 | 82 | func Test_Copy(t *testing.T) { 83 | t.Run("test_copy_selection", func(t *testing.T) { 84 | bufferView := BufferView{ 85 | Buffer: &Buffer{ 86 | File: "", 87 | Content: []byte("012345678\n012345678"), 88 | CRLF: false, 89 | }, 90 | Cursor: Cursor{ 91 | Point: 2, 92 | Mark: 5, 93 | }, 94 | ActionStack: NewStack[BufferAction](10), 95 | } 96 | Copy(&bufferView) 97 | copiedValue := GetClipboardContent() 98 | assert.Equal(t, "2345", string(copiedValue)) 99 | }) 100 | 101 | t.Run("test_copy_line", func(t *testing.T) { 102 | bufferView := BufferView{ 103 | Buffer: &Buffer{ 104 | File: "", 105 | Content: []byte("012345678\n012345678"), 106 | CRLF: false, 107 | }, 108 | Cursor: Cursor{ 109 | Point: 2, 110 | Mark: 2, 111 | }, 112 | ActionStack: NewStack[BufferAction](10), 113 | } 114 | bufferView.calcRenderState() 115 | Copy(&bufferView) 116 | copiedValue := GetClipboardContent() 117 | assert.Equal(t, "012345678\n", string(copiedValue)) 118 | 119 | }) 120 | } 121 | 122 | func Test_Paste(t *testing.T) { 123 | bufferView := BufferView{ 124 | Buffer: &Buffer{ 125 | File: "", 126 | Content: []byte("01\n012345678"), 127 | CRLF: false, 128 | }, 129 | Cursor: Cursor{ 130 | Point: 2, 131 | Mark: 2, 132 | }, 133 | ActionStack: NewStack[BufferAction](10), 134 | } 135 | WriteToClipboard([]byte("2345678")) 136 | Paste(&bufferView) 137 | assert.Equal(t, "012345678\n012345678", string(bufferView.Buffer.Content)) 138 | RevertLastBufferAction(&bufferView) 139 | assert.Equal(t, "01\n012345678", string(bufferView.Buffer.Content)) 140 | } 141 | 142 | func Test_DeleteCharBackward(t *testing.T) { 143 | bufferView := BufferView{ 144 | Buffer: &Buffer{ 145 | File: "", 146 | Content: []byte("012345678\n012345678"), 147 | CRLF: false, 148 | }, 149 | Cursor: Cursor{ 150 | Point: 2, 151 | Mark: 2, 152 | }, 153 | ActionStack: NewStack[BufferAction](10), 154 | } 155 | DeleteCharBackward(&bufferView) 156 | assert.Equal(t, "02345678\n012345678", string(bufferView.Buffer.Content)) 157 | RevertLastBufferAction(&bufferView) 158 | assert.Equal(t, "012345678\n012345678", string(bufferView.Buffer.Content)) 159 | 160 | } 161 | 162 | func Test_DeleteCharForeward(t *testing.T) { 163 | bufferView := BufferView{ 164 | Buffer: &Buffer{ 165 | File: "", 166 | Content: []byte("012345678\n012345678"), 167 | CRLF: false, 168 | }, 169 | Cursor: Cursor{ 170 | Point: 2, 171 | Mark: 2, 172 | }, 173 | ActionStack: NewStack[BufferAction](10), 174 | } 175 | 176 | DeleteCharForward(&bufferView) 177 | assert.Equal(t, "01345678\n012345678", string(bufferView.Buffer.Content)) 178 | RevertLastBufferAction(&bufferView) 179 | assert.Equal(t, "012345678\n012345678", string(bufferView.Buffer.Content)) 180 | } 181 | 182 | func Test_WordAtPoint(t *testing.T) { 183 | bufferView := BufferView{ 184 | Buffer: &Buffer{ 185 | File: "", 186 | Content: []byte("hello world"), 187 | CRLF: false, 188 | }, 189 | Cursor: Cursor{ 190 | Point: 2, 191 | Mark: 2, 192 | }, 193 | ActionStack: NewStack[BufferAction](10), 194 | } 195 | 196 | start, end := WordAtPoint(&bufferView) 197 | assert.Equal(t, 0, start) 198 | assert.Equal(t, 4, end) 199 | } 200 | 201 | //func Test_LeftWord(t *testing.T) {} 202 | //func Test_RightWord(t *testing.T) {} 203 | //func Test_DeleteWordBackward(t *testing.T) {} 204 | //func Test_Indent(t *testing.T) {} 205 | //func Test_ScrollUp(t *testing.T) {} 206 | //func Test_ScrollToTop(t *testing.T) {} 207 | //func Test_ScrollToBottom(t *testing.T) {} 208 | //func Test_ScrollDown(t *testing.T) {} 209 | //func Test_PointLeft(t *testing.T) {} 210 | //func Test_PointRight(t *testing.T) {} 211 | //func Test_PointUp(t *testing.T) {} 212 | //func Test_PointDown(t *testing.T) {} 213 | //func Test_CentralizePoint(t *testing.T) {} 214 | //func Test_PointToBeginningOfLine(t *testing.T) {} 215 | //func Test_PointToEndOfLine(t *testing.T) {} 216 | //func Test_PointToMatchingChar(t *testing.T) {} 217 | //func Test_MarkRight(t *testing.T) {} 218 | //func Test_MarkLeft(t *testing.T) {} 219 | //func Test_MarkUp(t *testing.T) {} 220 | //func Test_MarkDown(t *testing.T) {} 221 | //func Test_MarkPreviousWord(t *testing.T) {} 222 | //func Test_MarkNextWord(t *testing.T) {} 223 | //func Test_MarkToEndOfLine(t *testing.T) {} 224 | //func Test_MarkToBeginningOfLine(t *testing.T) {} 225 | //func Test_MarkToMatchingChar(t *testing.T) {} 226 | //func Test_PointForwardWord(t *testing.T) {} 227 | //func Test_PointBackwardWord(t *testing.T) {} 228 | //func Test_Copy(t *testing.T) {} 229 | //func Test_Search(t *testing.T) {} 230 | //func Test_QueryReplace(t *testing.T) {} 231 | //func Test_IsvalidCursorPosition(t *testing.T) {} 232 | //func Test_AnotherSelectionOnMatch(t *testing.T) {} 233 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/ebitengine/purego v0.5.0 h1:JrMGKfRIAM4/QVKaesIIT7m/UVjTj5GYhRSQYwfVdpo= 6 | github.com/ebitengine/purego v0.5.0/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ= 7 | github.com/flopp/go-findfont v0.1.0 h1:lPn0BymDUtJo+ZkV01VS3661HL6F4qFlkhcJN55u6mU= 8 | github.com/flopp/go-findfont v0.1.0/go.mod h1:wKKxRDjD024Rh7VMwoU90i6ikQRCr+JTHB5n4Ejkqvw= 9 | github.com/gen2brain/raylib-go/raylib v0.0.0-20231118131254-fed470e4458f h1:U0LqZN/IhdH/w7xA6dE68hBoWKmNEpsnlCct8Lcc6s4= 10 | github.com/gen2brain/raylib-go/raylib v0.0.0-20231118131254-fed470e4458f/go.mod h1:OrILUkoha5TCD4Btbw0YPoxe1sQj3q8xpFBqAoeRWyo= 11 | github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4= 12 | github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4= 13 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 14 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 15 | github.com/smacker/go-tree-sitter v0.0.0-20231215063300-06670b6cd560 h1:fX/cV47CFFuPsZv9Mz3OPKpwoWBymPZqOKT60zLznUI= 16 | github.com/smacker/go-tree-sitter v0.0.0-20231215063300-06670b6cd560/go.mod h1:q99oHDsbP0xRwmn7Vmob8gbSMNyvJ83OauXPSuHQuKE= 17 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 18 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 19 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 20 | github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 21 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 22 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 23 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 24 | golang.design/x/clipboard v0.7.0 h1:4Je8M/ys9AJumVnl8m+rZnIvstSnYj1fvzqYrU3TXvo= 25 | golang.design/x/clipboard v0.7.0/go.mod h1:PQIvqYO9GP29yINEfsEn5zSQKAz3UgXmZKzDA6dnq2E= 26 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 27 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 28 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 29 | golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 h1:estk1glOnSVeJ9tdEZZc5mAMDZk5lNJNyJ6DvrBkTEU= 30 | golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= 31 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 32 | golang.org/x/image v0.6.0 h1:bR8b5okrPI3g/gyZakLZHeWxAR8Dn5CyxXv1hLH5g/4= 33 | golang.org/x/image v0.6.0/go.mod h1:MXLdDR43H7cDJq5GEGXEVeeNhPgi+YYEQ2pC1byI1x0= 34 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 35 | golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c h1:Gk61ECugwEHL6IiyyNLXNzmu8XslmRP2dS0xjIYhbb4= 36 | golang.org/x/mobile v0.0.0-20230301163155-e0f57694e12c/go.mod h1:aAjjkJNdrh3PMckS4B10TGS2nag27cbKR1y2BpUxsiY= 37 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 38 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 39 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 40 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 41 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 42 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 43 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 44 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 45 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 46 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 47 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 48 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 49 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 50 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 51 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 52 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 53 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 54 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 55 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 56 | golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= 57 | golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 58 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 59 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 60 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 61 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 62 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 63 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 64 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 65 | golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 66 | golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= 67 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 68 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 69 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 70 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 71 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 72 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 73 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 74 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 75 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 76 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 77 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 78 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package preditor 2 | 3 | import ( 4 | _ "embed" 5 | "errors" 6 | "fmt" 7 | "reflect" 8 | 9 | "image/color" 10 | "os" 11 | "strconv" 12 | "strings" 13 | ) 14 | 15 | //go:embed fonts/liberationmono-regular.ttf 16 | var liberationMonoRegularTTF []byte 17 | 18 | //go:embed fonts/jetbrainsmono.ttf 19 | var jetbrainsMonoTTF []byte 20 | 21 | type CursorShape int 22 | 23 | const ( 24 | CURSOR_SHAPE_BLOCK CursorShape = 1 25 | CURSOR_SHAPE_OUTLINE CursorShape = 2 26 | CURSOR_SHAPE_LINE CursorShape = 3 27 | ) 28 | 29 | func (c CursorShape) String() string { 30 | switch c { 31 | case CURSOR_SHAPE_BLOCK: 32 | return "block" 33 | case CURSOR_SHAPE_OUTLINE: 34 | return "outline" 35 | case CURSOR_SHAPE_LINE: 36 | return "bar" 37 | default: 38 | return "" 39 | } 40 | } 41 | 42 | type Theme struct { 43 | Name string 44 | Colors Colors 45 | } 46 | 47 | func (t Theme) String() string { 48 | var colors []string 49 | v := reflect.ValueOf(t.Colors) 50 | typ := reflect.TypeOf(t.Colors) 51 | for i := 0; i < v.NumField(); i++ { 52 | colors = append(colors, typ.Field(i).Name, v.Field(i).String()) 53 | } 54 | //return fmt.Sprintf("Theme: %s\n%s", t.Name, strings.Join(colors, "\n")) 55 | return t.Name 56 | } 57 | 58 | type Config struct { 59 | Themes []Theme 60 | CurrentTheme string 61 | TabSize int 62 | LineNumbers bool 63 | FontName string 64 | FontSize int 65 | CursorShape CursorShape 66 | CursorBlinking bool 67 | EnableSyntaxHighlighting bool 68 | HighlightMatchingParen bool 69 | CursorLineHighlight bool 70 | BuildWindowNormalHeight float64 71 | BuildWindowMaximizedHeight float64 72 | } 73 | 74 | func (c *Config) String() string { 75 | var output []string 76 | v := reflect.ValueOf(c).Elem() 77 | t := reflect.TypeOf(c).Elem() 78 | for i := 0; i < v.NumField(); i++ { 79 | field := v.Field(i) 80 | typ := t.Field(i) 81 | if typ.Type.String() == "color.RGBA" { 82 | rgbaColor := v.Interface().(color.RGBA) 83 | colorAsHex := fmt.Sprintf("#%02x%02x%02x%02x", rgbaColor.R, rgbaColor.G, rgbaColor.B, rgbaColor.A) 84 | output = append(output, fmt.Sprintf("%s = %s", typ.Name, colorAsHex)) 85 | 86 | } else { 87 | output = append(output, fmt.Sprintf("%s = %v", typ.Name, field.Interface())) 88 | } 89 | } 90 | 91 | return strings.Join(output, "\n") 92 | } 93 | 94 | func mustParseHexColor(hex string) RGBA { 95 | c, err := parseHexColor(hex) 96 | if err != nil { 97 | panic(err) 98 | } 99 | return RGBA(c) 100 | } 101 | 102 | var defaultConfig = Config{ 103 | CurrentTheme: "4Coder_Fleury", 104 | Themes: []Theme{ 105 | { 106 | Name: "Default_Dark", 107 | Colors: Colors{ 108 | Background: mustParseHexColor("#0c0c0c"), 109 | Foreground: mustParseHexColor("#90B090"), 110 | SelectionBackground: mustParseHexColor("#FF44DD"), 111 | SelectionForeground: mustParseHexColor("#ffffff"), 112 | Prompts: mustParseHexColor("#333333"), 113 | StatusBarBackground: mustParseHexColor("#888888"), 114 | StatusBarForeground: mustParseHexColor("#000000"), 115 | ActiveStatusBarBackground: mustParseHexColor("#BBBBBB"), 116 | ActiveStatusBarForeground: mustParseHexColor("#000000"), 117 | LineNumbersForeground: mustParseHexColor("#F2F2F2"), 118 | ActiveWindowBorder: mustParseHexColor("#292929"), 119 | Cursor: mustParseHexColor("#00ff00"), 120 | CursorLineBackground: mustParseHexColor("#52534E"), 121 | HighlightMatching: mustParseHexColor("#00ff00"), 122 | SyntaxColors: SyntaxColors{ 123 | "type": mustParseHexColor("#90B090"), 124 | "keyword": mustParseHexColor("#D08F20"), 125 | "string": mustParseHexColor("#50FF30"), 126 | "comment": mustParseHexColor("#2090F0"), 127 | }, 128 | }, 129 | }, 130 | 131 | { 132 | Name: "VisualStudio_Light", 133 | Colors: Colors{ 134 | Background: mustParseHexColor("#ffffff"), 135 | Foreground: mustParseHexColor("#000000"), 136 | SelectionBackground: mustParseHexColor("#ADD6FF"), 137 | SelectionForeground: mustParseHexColor("#000000"), 138 | Prompts: mustParseHexColor("#333333"), 139 | StatusBarBackground: mustParseHexColor("#696969"), 140 | StatusBarForeground: mustParseHexColor("#000000"), 141 | LineNumbersForeground: mustParseHexColor("#010101"), 142 | ActiveWindowBorder: mustParseHexColor("#8cde94"), 143 | Cursor: mustParseHexColor("#171717"), 144 | CursorLineBackground: mustParseHexColor("#52534E"), 145 | HighlightMatching: mustParseHexColor("#171717"), 146 | SyntaxColors: SyntaxColors{ 147 | "ident": mustParseHexColor("#000000"), 148 | "type": mustParseHexColor("#0000ff"), 149 | "keyword": mustParseHexColor("#0000ff"), 150 | "string": mustParseHexColor("#a31515"), 151 | "comment": mustParseHexColor("#008000"), 152 | }, 153 | }, 154 | }, 155 | { 156 | Name: "4Coder_Fleury", 157 | Colors: Colors{ 158 | Background: mustParseHexColor("#020202"), 159 | Foreground: mustParseHexColor("#b99468"), 160 | SelectionBackground: mustParseHexColor("#FF44DD"), 161 | SelectionForeground: mustParseHexColor("#ffffff"), 162 | Prompts: mustParseHexColor("#333333"), 163 | StatusBarBackground: mustParseHexColor("#1f1f27"), 164 | StatusBarForeground: mustParseHexColor("#cb9401"), 165 | ActiveStatusBarBackground: mustParseHexColor("#1f1f27"), 166 | ActiveStatusBarForeground: mustParseHexColor("#cb9401"), 167 | LineNumbersForeground: mustParseHexColor("#010101"), 168 | ActiveWindowBorder: mustParseHexColor("#8cde94"), 169 | Cursor: mustParseHexColor("#e0741b"), 170 | CursorLineBackground: mustParseHexColor("#52534E"), 171 | HighlightMatching: mustParseHexColor("#e0741b"), 172 | SyntaxColors: SyntaxColors{ 173 | "ident": mustParseHexColor("#90B090"), 174 | "type": mustParseHexColor("#d8a51d"), 175 | "keyword": mustParseHexColor("#f0c674"), 176 | "string": mustParseHexColor("#ffa900"), 177 | "comment": mustParseHexColor("#666666"), 178 | }, 179 | }, 180 | }, 181 | { 182 | Name: "Naysayer", 183 | Colors: Colors{ 184 | Background: mustParseHexColor("#072626"), 185 | Foreground: mustParseHexColor("#d3b58d"), 186 | SelectionBackground: mustParseHexColor("#0000ff"), 187 | SelectionForeground: mustParseHexColor("#d3b58d"), 188 | Prompts: mustParseHexColor("#333333"), 189 | StatusBarBackground: mustParseHexColor("#ffffff"), 190 | StatusBarForeground: mustParseHexColor("#000000"), 191 | ActiveStatusBarBackground: mustParseHexColor("#d3b58d"), 192 | ActiveStatusBarForeground: mustParseHexColor("#000000"), 193 | LineNumbersForeground: mustParseHexColor("#d3b58d"), 194 | ActiveWindowBorder: mustParseHexColor("#8cde94"), 195 | Cursor: mustParseHexColor("#90ee90"), 196 | CursorLineBackground: mustParseHexColor("#52534E"), 197 | HighlightMatching: mustParseHexColor("#90ee90"), 198 | SyntaxColors: SyntaxColors{ 199 | "ident": mustParseHexColor("#c8d4ec"), 200 | "type": mustParseHexColor("#8cde94"), 201 | "keyword": mustParseHexColor("#d4d4d4"), 202 | "string": mustParseHexColor("#0fdfaf"), 203 | "comment": mustParseHexColor("#3fdf1f"), 204 | "function_name": mustParseHexColor("#ffffff"), 205 | }, 206 | }, 207 | }, 208 | { 209 | Name: "Solarized_Dark", 210 | Colors: Colors{ 211 | Background: mustParseHexColor("#002B36"), 212 | Foreground: mustParseHexColor("#839496"), 213 | SelectionBackground: mustParseHexColor("#274642"), 214 | SelectionForeground: mustParseHexColor("#d3b58d"), 215 | Prompts: mustParseHexColor("#333333"), 216 | StatusBarBackground: mustParseHexColor("#00212B"), 217 | StatusBarForeground: mustParseHexColor("#93A1A1"), 218 | ActiveStatusBarBackground: mustParseHexColor("#00212B"), 219 | ActiveStatusBarForeground: mustParseHexColor("#93A1A1"), 220 | LineNumbersForeground: mustParseHexColor("#d3b58d"), 221 | ActiveWindowBorder: mustParseHexColor("#8cde94"), 222 | Cursor: mustParseHexColor("#D30102"), 223 | CursorLineBackground: mustParseHexColor("#073642"), 224 | HighlightMatching: mustParseHexColor("#cdcdcd"), 225 | SyntaxColors: SyntaxColors{ 226 | "ident": mustParseHexColor("#268BD2"), 227 | "type": mustParseHexColor("#CB4B16"), 228 | "keyword": mustParseHexColor("#859900"), 229 | "string": mustParseHexColor("#2AA198"), 230 | "comment": mustParseHexColor("#586E75"), 231 | }, 232 | }, 233 | }, 234 | { 235 | Name: "Solarized_Light", 236 | Colors: Colors{ 237 | Background: mustParseHexColor("#FDF6E3"), 238 | Foreground: mustParseHexColor("#657B83"), 239 | SelectionBackground: mustParseHexColor("#EEE8D5"), 240 | SelectionForeground: mustParseHexColor("#d3b58d"), 241 | Prompts: mustParseHexColor("#333333"), 242 | StatusBarBackground: mustParseHexColor("#EEE8D5"), 243 | StatusBarForeground: mustParseHexColor("#586E75"), 244 | ActiveStatusBarBackground: mustParseHexColor("#EEE8D5"), 245 | ActiveStatusBarForeground: mustParseHexColor("#586E75"), 246 | LineNumbersForeground: mustParseHexColor("#d3b58d"), 247 | ActiveWindowBorder: mustParseHexColor("#8cde94"), 248 | Cursor: mustParseHexColor("#657B83"), 249 | CursorLineBackground: mustParseHexColor("#EEE8D5"), 250 | HighlightMatching: mustParseHexColor("#cdcdcd"), 251 | SyntaxColors: SyntaxColors{ 252 | "ident": mustParseHexColor("#268BD2"), 253 | "type": mustParseHexColor("#CB4B16"), 254 | "keyword": mustParseHexColor("#859900"), 255 | "string": mustParseHexColor("#2AA198"), 256 | "comment": mustParseHexColor("#93A1A1"), 257 | }, 258 | }, 259 | }, 260 | }, 261 | CursorLineHighlight: true, 262 | TabSize: 4, 263 | LineNumbers: true, 264 | EnableSyntaxHighlighting: true, 265 | CursorShape: CURSOR_SHAPE_BLOCK, 266 | CursorBlinking: false, 267 | FontName: "LiberationMono-Regular", 268 | HighlightMatchingParen: true, 269 | FontSize: 17, 270 | BuildWindowNormalHeight: 0.2, 271 | BuildWindowMaximizedHeight: 0.5, 272 | } 273 | 274 | func (c *Config) CurrentThemeColors() *Colors { 275 | for _, theme := range c.Themes { 276 | if theme.Name == c.CurrentTheme { 277 | return &theme.Colors 278 | } 279 | } 280 | return &c.Themes[0].Colors 281 | } 282 | 283 | func addToConfig(cfg *Config, key string, value string) error { 284 | switch key { 285 | case "syntax": 286 | cfg.EnableSyntaxHighlighting = value == "true" 287 | case "theme": 288 | cfg.CurrentTheme = value 289 | case "cursor_shape": 290 | switch value { 291 | case "block": 292 | cfg.CursorShape = CURSOR_SHAPE_BLOCK 293 | 294 | case "bar": 295 | cfg.CursorShape = CURSOR_SHAPE_LINE 296 | 297 | case "outline": 298 | cfg.CursorShape = CURSOR_SHAPE_OUTLINE 299 | } 300 | case "line_numbers": 301 | cfg.LineNumbers = value == "true" 302 | case "cursor_blinking": 303 | cfg.CursorBlinking = value == "true" 304 | case "font": 305 | cfg.FontName = value 306 | case "cursor_line_highlight": 307 | cfg.CursorLineHighlight = value == "true" 308 | case "hl_matching_char": 309 | cfg.HighlightMatchingParen = value == "true" 310 | case "font_size": 311 | 312 | var err error 313 | cfg.FontSize, err = strconv.Atoi(value) 314 | if err != nil { 315 | return err 316 | } 317 | } 318 | 319 | return nil 320 | } 321 | 322 | func ReadConfig(cfgPath string, startTheme string) (*Config, error) { 323 | cfg := defaultConfig 324 | if _, err := os.Stat(cfgPath); errors.Is(err, os.ErrNotExist) { 325 | return &cfg, nil 326 | } 327 | bs, err := os.ReadFile(cfgPath) 328 | if err != nil { 329 | return nil, err 330 | } 331 | 332 | lines := strings.Split(string(bs), "\n") 333 | 334 | for _, line := range lines { 335 | splitted := strings.SplitN(line, " ", 2) 336 | if len(splitted) != 2 { 337 | continue 338 | } 339 | key := splitted[0] 340 | value := splitted[1] 341 | key = strings.Trim(key, " \t\r") 342 | value = strings.Trim(value, " \t\r") 343 | addToConfig(&cfg, key, value) 344 | } 345 | 346 | if startTheme != "" { 347 | cfg.CurrentTheme = startTheme 348 | } 349 | 350 | return &cfg, nil 351 | } 352 | -------------------------------------------------------------------------------- /lists.go: -------------------------------------------------------------------------------- 1 | package preditor 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "github.com/amirrezaask/preditor/byteutils" 7 | "golang.design/x/clipboard" 8 | "os" 9 | "path" 10 | "path/filepath" 11 | 12 | rl "github.com/gen2brain/raylib-go/raylib" 13 | "github.com/lithammer/fuzzysearch/fuzzy" 14 | ) 15 | 16 | type ScoredItem[T any] struct { 17 | Item T 18 | Score int 19 | } 20 | 21 | func getClipboardContent() []byte { 22 | return clipboard.Read(clipboard.FmtText) 23 | } 24 | 25 | func writeToClipboard(bs []byte) { 26 | clipboard.Write(clipboard.FmtText, bytes.Clone(bs)) 27 | } 28 | 29 | type List[T any] struct { 30 | BaseDrawable 31 | cfg *Config 32 | parent *Context 33 | keymaps []Keymap 34 | VisibleStart int 35 | Items []T 36 | Selection int 37 | maxHeight int32 38 | maxWidth int32 39 | UserInput []byte 40 | ZeroLocation rl.Vector2 41 | Idx int 42 | LastInput string 43 | LastInputWeRanUpdateFor string 44 | UpdateList func(list *List[T], input string) 45 | OpenSelection func(ctx *Context, t T) error 46 | ItemRepr func(item T) string 47 | } 48 | 49 | func (l *List[T]) SetNewUserInput(bs []byte) { 50 | l.LastInput = string(l.UserInput) 51 | l.UserInput = bs 52 | l.Idx += len(l.UserInput) 53 | 54 | if l.Idx >= len(l.UserInput) { 55 | l.Idx = len(l.UserInput) 56 | } else if l.Idx < 0 { 57 | l.Idx = 0 58 | } 59 | 60 | } 61 | func (l *List[T]) InsertCharAtBuffer(char byte) error { 62 | l.SetNewUserInput(append(l.UserInput, char)) 63 | return nil 64 | } 65 | 66 | func (l *List[T]) CursorRight(n int) error { 67 | if l.Idx >= len(l.UserInput) { 68 | return nil 69 | } 70 | 71 | l.Idx += n 72 | 73 | return nil 74 | } 75 | 76 | func (l *List[T]) Paste() error { 77 | content := getClipboardContent() 78 | l.UserInput = append(l.UserInput[:l.Idx], append(content, l.UserInput[l.Idx+1:]...)...) 79 | 80 | return nil 81 | } 82 | 83 | func (l *List[T]) KillLine() error { 84 | l.SetNewUserInput(l.UserInput[:l.Idx]) 85 | return nil 86 | } 87 | 88 | func (l *List[T]) Copy() error { 89 | writeToClipboard(l.UserInput) 90 | 91 | return nil 92 | } 93 | 94 | func (l *List[T]) BeginningOfTheLine() error { 95 | l.Idx = 0 96 | return nil 97 | } 98 | 99 | func (l *List[T]) EndOfTheLine() error { 100 | l.Idx = len(l.UserInput) 101 | return nil 102 | } 103 | 104 | func (l *List[T]) NextWordStart() error { 105 | if idx := byteutils.NextWordInBuffer(l.UserInput, l.Idx); idx != -1 { 106 | l.Idx = idx 107 | } 108 | 109 | return nil 110 | } 111 | 112 | func (l *List[T]) CursorLeft(n int) error { 113 | 114 | if l.Idx <= 0 { 115 | return nil 116 | } 117 | 118 | l.Idx -= n 119 | 120 | return nil 121 | } 122 | 123 | func (l *List[T]) PreviousWord() error { 124 | if idx := byteutils.PreviousWordInBuffer(l.UserInput, l.Idx); idx != -1 { 125 | l.Idx = idx 126 | } 127 | 128 | return nil 129 | } 130 | 131 | func (l *List[T]) DeleteCharBackward() error { 132 | if l.Idx <= 0 { 133 | return nil 134 | } 135 | if len(l.UserInput) <= l.Idx { 136 | l.SetNewUserInput(l.UserInput[:l.Idx-1]) 137 | } else { 138 | l.SetNewUserInput(append(l.UserInput[:l.Idx-1], l.UserInput[l.Idx:]...)) 139 | } 140 | return nil 141 | } 142 | 143 | func (l *List[T]) DeleteWordBackward() error { 144 | previousWordEndIdx := byteutils.PreviousWordInBuffer(l.UserInput, l.Idx) 145 | if len(l.UserInput) > l.Idx+1 { 146 | l.SetNewUserInput(append(l.UserInput[:previousWordEndIdx+1], l.UserInput[l.Idx+1:]...)) 147 | } else { 148 | l.SetNewUserInput(l.UserInput[:previousWordEndIdx+1]) 149 | } 150 | return nil 151 | } 152 | func (l *List[T]) DeleteWordForward() error { 153 | nextWordStartIdx := byteutils.NextWordInBuffer(l.UserInput, l.Idx) 154 | if len(l.UserInput) > nextWordStartIdx+1 { 155 | l.SetNewUserInput(append(l.UserInput[:l.Idx+1], l.UserInput[nextWordStartIdx+1:]...)) 156 | } else { 157 | l.SetNewUserInput(l.UserInput[:l.Idx]) 158 | } 159 | 160 | return nil 161 | } 162 | func (l *List[T]) DeleteCharForward() error { 163 | if l.Idx < 0 { 164 | return nil 165 | } 166 | l.SetNewUserInput(append(l.UserInput[:l.Idx], l.UserInput[l.Idx+1:]...)) 167 | return nil 168 | } 169 | 170 | func (l *List[T]) NextItem() { 171 | l.Selection++ 172 | if l.Selection >= len(l.Items) { 173 | l.Selection = len(l.Items) - 1 174 | } 175 | 176 | } 177 | 178 | func (l *List[T]) PrevItem() { 179 | l.Selection-- 180 | if l.Selection < 0 { 181 | l.Selection = 0 182 | } 183 | 184 | if l.Selection < l.VisibleStart { 185 | l.VisibleStart-- 186 | if l.VisibleStart < 0 { 187 | l.VisibleStart = 0 188 | } 189 | } 190 | 191 | } 192 | func (l *List[T]) Scroll(n int) { 193 | l.VisibleStart += n 194 | 195 | if l.VisibleStart < 0 { 196 | l.VisibleStart = 0 197 | } 198 | 199 | } 200 | func (l *List[T]) VisibleView(maxLine int) []T { 201 | if l.Selection < l.VisibleStart { 202 | l.VisibleStart -= maxLine / 3 203 | if l.VisibleStart < 0 { 204 | l.VisibleStart = 0 205 | } 206 | } 207 | 208 | if l.Selection >= l.VisibleStart+maxLine { 209 | l.VisibleStart += maxLine / 3 210 | if l.VisibleStart >= len(l.Items) { 211 | l.VisibleStart = len(l.Items) 212 | } 213 | } 214 | 215 | if len(l.Items) > l.VisibleStart+maxLine { 216 | return l.Items[l.VisibleStart : l.VisibleStart+maxLine] 217 | } else { 218 | return l.Items[l.VisibleStart:len(l.Items)] 219 | } 220 | } 221 | 222 | func (l List[T]) Keymaps() []Keymap { 223 | return l.keymaps 224 | } 225 | 226 | func (l List[T]) String() string { 227 | return fmt.Sprintf("List: %T", *new(T)) 228 | } 229 | 230 | func NewList[T any]( 231 | parent *Context, 232 | cfg *Config, 233 | updateList func(list *List[T], input string), 234 | openSelection func(preditor *Context, t T) error, 235 | repr func(t T) string, 236 | initialList func() []T, 237 | ) *List[T] { 238 | ifb := &List[T]{ 239 | cfg: cfg, 240 | parent: parent, 241 | keymaps: []Keymap{makeKeymap[T]()}, 242 | UpdateList: updateList, 243 | OpenSelection: openSelection, 244 | ItemRepr: repr, 245 | } 246 | if initialList != nil { 247 | iList := initialList() 248 | ifb.Items = iList 249 | } 250 | 251 | ifb.keymaps = append(ifb.keymaps, MakeInsertionKeys(func(c *Context, b byte) { 252 | ifb.InsertCharAtBuffer(b) 253 | })) 254 | return ifb 255 | } 256 | 257 | func (l *List[T]) Render(zeroLocation rl.Vector2, maxH float64, maxW float64) { 258 | if l.LastInputWeRanUpdateFor != string(l.UserInput) { 259 | l.LastInputWeRanUpdateFor = string(l.UserInput) 260 | l.UpdateList(l, string(l.UserInput)) 261 | } 262 | charSize := measureTextSize(l.parent.Font, ' ', l.parent.FontSize, 0) 263 | 264 | //draw input box 265 | rl.DrawRectangleLines(int32(zeroLocation.X), int32(zeroLocation.Y), int32(maxW), int32(charSize.Y)*2, l.cfg.CurrentThemeColors().StatusBarBackground.ToColorRGBA()) 266 | rl.DrawTextEx(l.parent.Font, string(l.UserInput), rl.Vector2{ 267 | X: zeroLocation.X, Y: zeroLocation.Y + charSize.Y/2, 268 | }, float32(l.parent.FontSize), 0, l.cfg.CurrentThemeColors().Foreground.ToColorRGBA()) 269 | 270 | switch l.cfg.CursorShape { 271 | case CURSOR_SHAPE_OUTLINE: 272 | rl.DrawRectangleLines(int32(float64(zeroLocation.X)+float64(charSize.X))*int32(l.Idx), int32(zeroLocation.Y+charSize.Y/2), int32(charSize.X), int32(charSize.Y), rl.Fade(rl.Red, 0.5)) 273 | case CURSOR_SHAPE_BLOCK: 274 | rl.DrawRectangle(int32(float64(zeroLocation.X)+float64(charSize.X))*int32(l.Idx), int32(zeroLocation.Y+charSize.Y/2), int32(charSize.X), int32(charSize.Y), rl.Fade(rl.Red, 0.5)) 275 | case CURSOR_SHAPE_LINE: 276 | rl.DrawRectangleLines(int32(float64(zeroLocation.X)+float64(charSize.X))*int32(l.Idx), int32(zeroLocation.Y+charSize.Y/2), 2, int32(charSize.Y), rl.Fade(rl.Red, 0.5)) 277 | } 278 | 279 | startOfListY := int32(zeroLocation.Y) + int32(3*(charSize.Y)) 280 | maxLine := int(int32((maxH+float64(zeroLocation.Y))-float64(startOfListY)) / int32(charSize.Y)) 281 | 282 | //draw list of items 283 | for idx, item := range l.VisibleView(maxLine) { 284 | rl.DrawTextEx(l.parent.Font, l.ItemRepr(item), rl.Vector2{ 285 | X: zeroLocation.X, Y: float32(startOfListY) + float32(idx)*charSize.Y, 286 | }, float32(l.parent.FontSize), 0, l.cfg.CurrentThemeColors().Foreground.ToColorRGBA()) 287 | } 288 | if len(l.Items) > 0 { 289 | rl.DrawRectangle(int32(zeroLocation.X), int32(int(startOfListY)+(l.Selection-l.VisibleStart)*int(charSize.Y)), int32(maxW), int32(charSize.Y), rl.Fade(l.cfg.CurrentThemeColors().SelectionBackground.ToColorRGBA(), 0.2)) 290 | } 291 | } 292 | 293 | func makeKeymap[T any]() Keymap { 294 | return Keymap{ 295 | 296 | Key{K: "f", Control: true}: MakeCommand(func(e *List[T]) { 297 | e.CursorRight(1) 298 | }), 299 | Key{K: "v", Control: true}: MakeCommand(func(e *List[T]) { 300 | e.Paste() 301 | }), 302 | Key{K: "c", Control: true}: MakeCommand(func(e *List[T]) { 303 | e.Copy() 304 | }), 305 | Key{K: "a", Control: true}: MakeCommand(func(e *List[T]) { 306 | e.BeginningOfTheLine() 307 | }), 308 | Key{K: "e", Control: true}: MakeCommand(func(e *List[T]) { 309 | e.EndOfTheLine() 310 | }), 311 | Key{K: "g", Control: true}: MakeCommand(func(e *List[T]) { 312 | e.parent.KillDrawable(e.ID) 313 | }), 314 | 315 | Key{K: ""}: MakeCommand(func(e *List[T]) { 316 | e.CursorRight(1) 317 | }), 318 | Key{K: "", Control: true}: MakeCommand(func(e *List[T]) { 319 | e.NextWordStart() 320 | }), 321 | Key{K: ""}: MakeCommand(func(e *List[T]) { 322 | e.CursorLeft(1) 323 | }), 324 | Key{K: "", Control: true}: MakeCommand(func(e *List[T]) { 325 | e.PreviousWord() 326 | }), 327 | 328 | Key{K: "p", Control: true}: MakeCommand(func(e *List[T]) { 329 | e.PrevItem() 330 | }), 331 | Key{K: "n", Control: true}: MakeCommand(func(e *List[T]) { 332 | e.NextItem() 333 | }), 334 | Key{K: ""}: MakeCommand(func(e *List[T]) { 335 | e.PrevItem() 336 | 337 | }), 338 | Key{K: ""}: MakeCommand(func(e *List[T]) { 339 | e.NextItem() 340 | }), 341 | Key{K: "b", Control: true}: MakeCommand(func(e *List[T]) { 342 | e.CursorLeft(1) 343 | }), 344 | Key{K: ""}: MakeCommand(func(e *List[T]) { 345 | e.BeginningOfTheLine() 346 | }), 347 | 348 | Key{K: ""}: MakeCommand(func(e *List[T]) { 349 | if len(e.Items) > 0 && len(e.Items) > e.Selection { 350 | e.OpenSelection(e.parent, e.Items[e.Selection]) 351 | } 352 | 353 | }), 354 | Key{K: ""}: MakeCommand(func(e *List[T]) { e.DeleteCharBackward() }), 355 | Key{K: "", Control: true}: MakeCommand(func(e *List[T]) { e.DeleteWordBackward() }), 356 | Key{K: "d", Control: true}: MakeCommand(func(e *List[T]) { e.DeleteCharForward() }), 357 | Key{K: "d", Alt: true}: MakeCommand(func(e *List[T]) { e.DeleteWordForward() }), 358 | Key{K: ""}: MakeCommand(func(e *List[T]) { e.DeleteCharForward() }), 359 | } 360 | } 361 | 362 | func NewBufferList(parent *Context, cfg *Config) *List[ScoredItem[Drawable]] { 363 | updateList := func(l *List[ScoredItem[Drawable]], input string) { 364 | for idx, item := range l.Items { 365 | l.Items[idx].Score = fuzzy.RankMatchNormalizedFold(input, fmt.Sprint(item.Item)) 366 | } 367 | 368 | sortme(l.Items, func(t1 ScoredItem[Drawable], t2 ScoredItem[Drawable]) bool { 369 | return t1.Score > t2.Score 370 | }) 371 | 372 | } 373 | openSelection := func(parent *Context, item ScoredItem[Drawable]) error { 374 | parent.KillDrawable(parent.ActiveDrawable().GetID()) 375 | parent.MarkDrawableAsActive(item.Item.GetID()) 376 | 377 | return nil 378 | } 379 | initialList := func() []ScoredItem[Drawable] { 380 | var buffers []ScoredItem[Drawable] 381 | for _, v := range parent.Drawables { 382 | if v != nil { 383 | buffers = append(buffers, ScoredItem[Drawable]{Item: v}) 384 | } 385 | } 386 | 387 | return buffers 388 | } 389 | repr := func(s ScoredItem[Drawable]) string { 390 | return s.Item.String() 391 | } 392 | return NewList[ScoredItem[Drawable]]( 393 | parent, 394 | cfg, 395 | updateList, 396 | openSelection, 397 | repr, 398 | initialList, 399 | ) 400 | 401 | } 402 | 403 | func NewThemeList(parent *Context, cfg *Config) *List[ScoredItem[string]] { 404 | updateList := func(l *List[ScoredItem[string]], input string) { 405 | for idx, item := range l.Items { 406 | l.Items[idx].Score = fuzzy.RankMatchNormalizedFold(input, fmt.Sprint(item.Item)) 407 | } 408 | 409 | sortme(l.Items, func(t1 ScoredItem[string], t2 ScoredItem[string]) bool { 410 | return t1.Score > t2.Score 411 | }) 412 | 413 | } 414 | openSelection := func(parent *Context, item ScoredItem[string]) error { 415 | parent.Cfg.CurrentTheme = item.Item 416 | parent.KillDrawable(parent.ActiveDrawableID()) 417 | return nil 418 | } 419 | initialList := func() []ScoredItem[string] { 420 | var themes []ScoredItem[string] 421 | for _, v := range parent.Cfg.Themes { 422 | themes = append(themes, ScoredItem[string]{Item: v.Name}) 423 | } 424 | 425 | return themes 426 | } 427 | repr := func(s ScoredItem[string]) string { 428 | return s.Item 429 | } 430 | return NewList[ScoredItem[string]]( 431 | parent, 432 | cfg, 433 | updateList, 434 | openSelection, 435 | repr, 436 | initialList, 437 | ) 438 | 439 | } 440 | 441 | type GrepLocationItem struct { 442 | Filename string 443 | Text string 444 | Line int 445 | Col int 446 | } 447 | 448 | type LocationItem struct { 449 | Filename string 450 | } 451 | 452 | func NewFuzzyFileList(parent *Context, cfg *Config, cwd string) *List[ScoredItem[LocationItem]] { 453 | updateList := func(l *List[ScoredItem[LocationItem]], input string) { 454 | for idx, item := range l.Items { 455 | l.Items[idx].Score = fuzzy.RankMatchNormalizedFold(input, item.Item.Filename) 456 | } 457 | 458 | sortme(l.Items, func(t1 ScoredItem[LocationItem], t2 ScoredItem[LocationItem]) bool { 459 | return t1.Score > t2.Score 460 | }) 461 | 462 | } 463 | openSelection := func(parent *Context, item ScoredItem[LocationItem]) error { 464 | err := SwitchOrOpenFileInCurrentWindow(parent, parent.Cfg, path.Join(cwd, item.Item.Filename), nil) 465 | if err != nil { 466 | panic(err) 467 | } 468 | return nil 469 | } 470 | 471 | repr := func(g ScoredItem[LocationItem]) string { 472 | return fmt.Sprintf("%s", g.Item.Filename) 473 | } 474 | 475 | initialList := func() []ScoredItem[LocationItem] { 476 | var locationItems []ScoredItem[LocationItem] 477 | files := RipgrepFiles(cwd) 478 | for _, file := range files { 479 | locationItems = append(locationItems, ScoredItem[LocationItem]{Item: LocationItem{Filename: file}}) 480 | } 481 | 482 | return locationItems 483 | 484 | } 485 | 486 | return NewList[ScoredItem[LocationItem]]( 487 | parent, 488 | cfg, 489 | updateList, 490 | openSelection, 491 | repr, 492 | initialList, 493 | ) 494 | } 495 | 496 | func NewFileList(parent *Context, cfg *Config, initialInput string) *List[LocationItem] { 497 | updateList := func(l *List[LocationItem], input string) { 498 | matches, err := filepath.Glob(string(input) + "*") 499 | if err != nil { 500 | return 501 | } 502 | 503 | l.Items = nil 504 | 505 | for _, match := range matches { 506 | stat, err := os.Stat(match) 507 | if err == nil { 508 | isDir := stat.IsDir() 509 | _ = isDir 510 | } 511 | l.Items = append(l.Items, LocationItem{ 512 | Filename: match, 513 | }) 514 | } 515 | 516 | if l.Selection >= len(l.Items) { 517 | l.Selection = len(l.Items) - 1 518 | } 519 | 520 | if l.Selection < 0 { 521 | l.Selection = 0 522 | } 523 | 524 | return 525 | 526 | } 527 | openUserInput := func(parent *Context, userInput string) { 528 | parent.KillDrawable(parent.ActiveDrawableID()) 529 | err := SwitchOrOpenFileInCurrentWindow(parent, parent.Cfg, userInput, nil) 530 | if err != nil { 531 | panic(err) 532 | } 533 | } 534 | openSelection := func(parent *Context, item LocationItem) error { 535 | parent.KillDrawable(parent.ActiveDrawableID()) 536 | err := SwitchOrOpenFileInCurrentWindow(parent, parent.Cfg, item.Filename, nil) 537 | if err != nil { 538 | panic(err) 539 | } 540 | return nil 541 | } 542 | 543 | repr := func(g LocationItem) string { 544 | return fmt.Sprintf("%s", g.Filename) 545 | } 546 | 547 | tryComplete := func(f *List[LocationItem]) { 548 | input := f.UserInput 549 | 550 | matches, err := filepath.Glob(string(input) + "*") 551 | if err != nil { 552 | return 553 | } 554 | 555 | if f.Selection < len(f.Items) { 556 | stat, err := os.Stat(matches[f.Selection]) 557 | if err == nil { 558 | if stat.IsDir() { 559 | matches[f.Selection] += "/" 560 | } 561 | } 562 | f.UserInput = []byte(matches[f.Selection]) 563 | f.CursorRight(len(f.UserInput) - len(input)) 564 | } 565 | return 566 | } 567 | 568 | ifb := NewList[LocationItem]( 569 | parent, 570 | cfg, 571 | updateList, 572 | openSelection, 573 | repr, 574 | nil, 575 | ) 576 | 577 | ifb.keymaps[0][Key{K: "", Control: true}] = func(preditor *Context) { 578 | input := preditor.ActiveDrawable().(*List[LocationItem]).UserInput 579 | openUserInput(preditor, string(input)) 580 | } 581 | ifb.keymaps[0][Key{K: ""}] = MakeCommand(tryComplete) 582 | var absRoot string 583 | var err error 584 | if initialInput == "" { 585 | absRoot = parent.getCWD() 586 | } else { 587 | absRoot, err = filepath.Abs(initialInput) 588 | if err != nil { 589 | panic(err) 590 | } 591 | } 592 | ifb.SetNewUserInput([]byte(absRoot)) 593 | 594 | return ifb 595 | } 596 | -------------------------------------------------------------------------------- /defaults.go: -------------------------------------------------------------------------------- 1 | package preditor 2 | 3 | import rl "github.com/gen2brain/raylib-go/raylib" 4 | 5 | func MakeInsertionKeys(insertor func(c *Context, b byte)) Keymap { 6 | return Keymap{ 7 | Key{K: "a"}: func(c *Context) { insertor(c, 'a') }, 8 | Key{K: "b"}: func(c *Context) { insertor(c, 'b') }, 9 | Key{K: "c"}: func(c *Context) { insertor(c, 'c') }, 10 | Key{K: "d"}: func(c *Context) { insertor(c, 'd') }, 11 | Key{K: "e"}: func(c *Context) { insertor(c, 'e') }, 12 | Key{K: "f"}: func(c *Context) { insertor(c, 'f') }, 13 | Key{K: "g"}: func(c *Context) { insertor(c, 'g') }, 14 | Key{K: "h"}: func(c *Context) { insertor(c, 'h') }, 15 | Key{K: "i"}: func(c *Context) { insertor(c, 'i') }, 16 | Key{K: "j"}: func(c *Context) { insertor(c, 'j') }, 17 | Key{K: "k"}: func(c *Context) { insertor(c, 'k') }, 18 | Key{K: "l"}: func(c *Context) { insertor(c, 'l') }, 19 | Key{K: "m"}: func(c *Context) { insertor(c, 'm') }, 20 | Key{K: "n"}: func(c *Context) { insertor(c, 'n') }, 21 | Key{K: "o"}: func(c *Context) { insertor(c, 'o') }, 22 | Key{K: "p"}: func(c *Context) { insertor(c, 'p') }, 23 | Key{K: "q"}: func(c *Context) { insertor(c, 'q') }, 24 | Key{K: "r"}: func(c *Context) { insertor(c, 'r') }, 25 | Key{K: "s"}: func(c *Context) { insertor(c, 's') }, 26 | Key{K: "t"}: func(c *Context) { insertor(c, 't') }, 27 | Key{K: "u"}: func(c *Context) { insertor(c, 'u') }, 28 | Key{K: "v"}: func(c *Context) { insertor(c, 'v') }, 29 | Key{K: "w"}: func(c *Context) { insertor(c, 'w') }, 30 | Key{K: "x"}: func(c *Context) { insertor(c, 'x') }, 31 | Key{K: "y"}: func(c *Context) { insertor(c, 'y') }, 32 | Key{K: "z"}: func(c *Context) { insertor(c, 'z') }, 33 | Key{K: "0"}: func(c *Context) { insertor(c, '0') }, 34 | Key{K: "1"}: func(c *Context) { insertor(c, '1') }, 35 | Key{K: "2"}: func(c *Context) { insertor(c, '2') }, 36 | Key{K: "3"}: func(c *Context) { insertor(c, '3') }, 37 | Key{K: "4"}: func(c *Context) { insertor(c, '4') }, 38 | Key{K: "5"}: func(c *Context) { insertor(c, '5') }, 39 | Key{K: "6"}: func(c *Context) { insertor(c, '6') }, 40 | Key{K: "7"}: func(c *Context) { insertor(c, '7') }, 41 | Key{K: "8"}: func(c *Context) { insertor(c, '8') }, 42 | Key{K: "9"}: func(c *Context) { insertor(c, '9') }, 43 | Key{K: "\\"}: func(c *Context) { insertor(c, '\\') }, 44 | Key{K: "\\", Shift: true}: func(c *Context) { insertor(c, '|') }, 45 | Key{K: "0", Shift: true}: func(c *Context) { insertor(c, ')') }, 46 | Key{K: "1", Shift: true}: func(c *Context) { insertor(c, '!') }, 47 | Key{K: "2", Shift: true}: func(c *Context) { insertor(c, '@') }, 48 | Key{K: "3", Shift: true}: func(c *Context) { insertor(c, '#') }, 49 | Key{K: "4", Shift: true}: func(c *Context) { insertor(c, '$') }, 50 | Key{K: "5", Shift: true}: func(c *Context) { insertor(c, '%') }, 51 | Key{K: "6", Shift: true}: func(c *Context) { insertor(c, '^') }, 52 | Key{K: "7", Shift: true}: func(c *Context) { insertor(c, '&') }, 53 | Key{K: "8", Shift: true}: func(c *Context) { insertor(c, '*') }, 54 | Key{K: "9", Shift: true}: func(c *Context) { insertor(c, '(') }, 55 | Key{K: "a", Shift: true}: func(c *Context) { insertor(c, 'A') }, 56 | Key{K: "b", Shift: true}: func(c *Context) { insertor(c, 'B') }, 57 | Key{K: "c", Shift: true}: func(c *Context) { insertor(c, 'C') }, 58 | Key{K: "d", Shift: true}: func(c *Context) { insertor(c, 'D') }, 59 | Key{K: "e", Shift: true}: func(c *Context) { insertor(c, 'E') }, 60 | Key{K: "f", Shift: true}: func(c *Context) { insertor(c, 'F') }, 61 | Key{K: "g", Shift: true}: func(c *Context) { insertor(c, 'G') }, 62 | Key{K: "h", Shift: true}: func(c *Context) { insertor(c, 'H') }, 63 | Key{K: "i", Shift: true}: func(c *Context) { insertor(c, 'I') }, 64 | Key{K: "j", Shift: true}: func(c *Context) { insertor(c, 'J') }, 65 | Key{K: "k", Shift: true}: func(c *Context) { insertor(c, 'K') }, 66 | Key{K: "l", Shift: true}: func(c *Context) { insertor(c, 'L') }, 67 | Key{K: "m", Shift: true}: func(c *Context) { insertor(c, 'M') }, 68 | Key{K: "n", Shift: true}: func(c *Context) { insertor(c, 'N') }, 69 | Key{K: "o", Shift: true}: func(c *Context) { insertor(c, 'O') }, 70 | Key{K: "p", Shift: true}: func(c *Context) { insertor(c, 'P') }, 71 | Key{K: "q", Shift: true}: func(c *Context) { insertor(c, 'Q') }, 72 | Key{K: "r", Shift: true}: func(c *Context) { insertor(c, 'R') }, 73 | Key{K: "s", Shift: true}: func(c *Context) { insertor(c, 'S') }, 74 | Key{K: "t", Shift: true}: func(c *Context) { insertor(c, 'T') }, 75 | Key{K: "u", Shift: true}: func(c *Context) { insertor(c, 'U') }, 76 | Key{K: "v", Shift: true}: func(c *Context) { insertor(c, 'V') }, 77 | Key{K: "w", Shift: true}: func(c *Context) { insertor(c, 'W') }, 78 | Key{K: "x", Shift: true}: func(c *Context) { insertor(c, 'X') }, 79 | Key{K: "y", Shift: true}: func(c *Context) { insertor(c, 'Y') }, 80 | Key{K: "z", Shift: true}: func(c *Context) { insertor(c, 'Z') }, 81 | Key{K: "["}: func(c *Context) { insertor(c, '[') }, 82 | Key{K: "]"}: func(c *Context) { insertor(c, ']') }, 83 | Key{K: "[", Shift: true}: func(c *Context) { insertor(c, '{') }, 84 | Key{K: "]", Shift: true}: func(c *Context) { insertor(c, '}') }, 85 | Key{K: ";"}: func(c *Context) { insertor(c, ';') }, 86 | Key{K: ";", Shift: true}: func(c *Context) { insertor(c, ':') }, 87 | Key{K: "'"}: func(c *Context) { insertor(c, '\'') }, 88 | Key{K: "'", Shift: true}: func(c *Context) { insertor(c, '"') }, 89 | Key{K: "\""}: func(c *Context) { insertor(c, '"') }, 90 | Key{K: ","}: func(c *Context) { insertor(c, ',') }, 91 | Key{K: "."}: func(c *Context) { insertor(c, '.') }, 92 | Key{K: ",", Shift: true}: func(c *Context) { insertor(c, '<') }, 93 | Key{K: ".", Shift: true}: func(c *Context) { insertor(c, '>') }, 94 | Key{K: "/"}: func(c *Context) { insertor(c, '/') }, 95 | Key{K: "/", Shift: true}: func(c *Context) { insertor(c, '?') }, 96 | Key{K: "-"}: func(c *Context) { insertor(c, '-') }, 97 | Key{K: "="}: func(c *Context) { insertor(c, '=') }, 98 | Key{K: "-", Shift: true}: func(c *Context) { insertor(c, '_') }, 99 | Key{K: "=", Shift: true}: func(c *Context) { insertor(c, '+') }, 100 | Key{K: "`"}: func(c *Context) { insertor(c, '`') }, 101 | Key{K: "`", Shift: true}: func(c *Context) { insertor(c, '~') }, 102 | Key{K: "", Shift: true}: func(c *Context) { insertor(c, ' ') }, 103 | Key{K: ""}: func(c *Context) { insertor(c, ' ') }, 104 | } 105 | } 106 | 107 | func setupDefaults() { 108 | PromptKeymap.SetKeys(MakeInsertionKeys(func(c *Context, b byte) { 109 | c.Prompt.UserInput += string(b) 110 | if c.Prompt.ChangeHook != nil { 111 | c.Prompt.ChangeHook(c.Prompt.UserInput, c) 112 | } 113 | })) 114 | PromptKeymap.BindKey(Key{K: ""}, func(c *Context) { 115 | userInput := c.Prompt.UserInput 116 | if c.Prompt.DoneHook != nil { 117 | c.Prompt.IsActive = false 118 | c.Prompt.UserInput = "" 119 | hook := c.Prompt.DoneHook 120 | c.Prompt.DoneHook = nil 121 | hook(userInput, c) 122 | } 123 | }) 124 | 125 | PromptKeymap.BindKey(Key{K: ""}, func(c *Context) { 126 | c.Prompt.UserInput = c.Prompt.UserInput[:len(c.Prompt.UserInput)-1] 127 | if c.Prompt.ChangeHook != nil { 128 | c.Prompt.ChangeHook(c.Prompt.UserInput, c) 129 | } 130 | }) 131 | 132 | PromptKeymap.BindKey(Key{K: ""}, func(c *Context) { 133 | c.ResetPrompt() 134 | }) 135 | 136 | BufferKeymap.SetKeys(MakeInsertionKeys(func(c *Context, b byte) { 137 | BufferInsertChar(c.ActiveDrawable().(*BufferView), b) 138 | })) 139 | 140 | BufferKeymap.BindKey(Key{K: ",", Shift: true, Control: true}, MakeCommand(ScrollToTop)) 141 | BufferKeymap.BindKey(Key{K: "l", Control: true}, MakeCommand(CentralizePoint)) 142 | BufferKeymap.BindKey(Key{K: ";", Control: true}, MakeCommand(CompileNoAsk)) 143 | BufferKeymap.BindKey(Key{K: ";", Control: true, Shift: true}, MakeCommand(CompileAskForCommand)) 144 | BufferKeymap.BindKey(Key{K: "g", Alt: true}, MakeCommand(GrepAsk)) 145 | BufferKeymap.BindKey(Key{K: ".", Shift: true, Control: true}, MakeCommand(ScrollToBottom)) 146 | BufferKeymap.BindKey(Key{K: "", Shift: true}, MakeCommand(func(e *BufferView) { MarkRight(e, 1) })) 147 | BufferKeymap.BindKey(Key{K: "", Shift: true, Control: true}, MakeCommand(MarkNextWord)) 148 | BufferKeymap.BindKey(Key{K: "", Shift: true, Control: true}, MakeCommand(MarkPreviousWord)) 149 | BufferKeymap.BindKey(Key{K: "", Shift: true}, MakeCommand(func(e *BufferView) { MarkLeft(e, 1) })) 150 | BufferKeymap.BindKey(Key{K: "", Shift: true}, MakeCommand(func(e *BufferView) { MarkUp(e, 1) })) 151 | BufferKeymap.BindKey(Key{K: "", Shift: true}, MakeCommand(func(e *BufferView) { MarkDown(e, 1) })) 152 | BufferKeymap.BindKey(Key{K: "n", Shift: true, Control: true}, MakeCommand(func(e *BufferView) { MarkDown(e, 1) })) 153 | BufferKeymap.BindKey(Key{K: "p", Shift: true, Control: true}, MakeCommand(func(e *BufferView) { MarkUp(e, 1) })) 154 | BufferKeymap.BindKey(Key{K: "f", Shift: true, Control: true}, MakeCommand(func(e *BufferView) { MarkRight(e, 1) })) 155 | BufferKeymap.BindKey(Key{K: "b", Shift: true, Control: true}, MakeCommand(func(e *BufferView) { MarkLeft(e, 1) })) 156 | BufferKeymap.BindKey(Key{K: "a", Shift: true, Control: true}, MakeCommand(MarkToBeginningOfLine)) 157 | BufferKeymap.BindKey(Key{K: "e", Shift: true, Control: true}, MakeCommand(MarkToEndOfLine)) 158 | BufferKeymap.BindKey(Key{K: "5", Shift: true, Control: true}, MakeCommand(MarkToMatchingChar)) 159 | BufferKeymap.BindKey(Key{K: "m", Shift: true, Control: true}, MakeCommand(MarkToMatchingChar)) 160 | BufferKeymap.BindKey(Key{K: "r", Control: true}, MakeCommand(QueryReplaceActivate)) 161 | BufferKeymap.BindKey(Key{K: "r", Control: true, Shift: true}, MakeCommand(RevertBuffer)) 162 | BufferKeymap.BindKey(Key{K: "r", Alt: true}, MakeCommand(func(e *BufferView) { 163 | e.readFileFromDisk() 164 | })) 165 | BufferKeymap.BindKey(Key{K: "z", Control: true}, MakeCommand(func(e *BufferView) { 166 | RevertLastBufferAction(e) 167 | })) 168 | BufferKeymap.BindKey(Key{K: "f", Control: true}, MakeCommand(func(e *BufferView) { 169 | PointRight(e, 1) 170 | })) 171 | BufferKeymap.BindKey(Key{K: "x", Control: true}, MakeCommand(func(e *BufferView) { 172 | Cut(e) 173 | })) 174 | BufferKeymap.BindKey(Key{K: "v", Control: true}, MakeCommand(func(e *BufferView) { 175 | Paste(e) 176 | })) 177 | BufferKeymap.BindKey(Key{K: "k", Control: true}, MakeCommand(func(e *BufferView) { 178 | KillLine(e) 179 | })) 180 | BufferKeymap.BindKey(Key{K: "g", Control: true}, MakeCommand(func(e *BufferView) { 181 | InteractiveGotoLine(e) 182 | })) 183 | BufferKeymap.BindKey(Key{K: "c", Control: true}, MakeCommand(func(e *BufferView) { 184 | Copy(e) 185 | })) 186 | BufferKeymap.BindKey(Key{K: "c", Alt: true}, MakeCommand(func(a *BufferView) { 187 | CompileAskForCommand(a) 188 | })) 189 | BufferKeymap.BindKey(Key{K: "s", Control: true}, MakeCommand(func(a *BufferView) { 190 | SearchActivate(a) 191 | })) 192 | BufferKeymap.BindKey(Key{K: "w", Control: true}, MakeCommand(func(a *BufferView) { 193 | Write(a) 194 | })) 195 | BufferKeymap.BindKey(Key{K: "-click"}, MakeCommand(func(e *BufferView) { 196 | e.moveCursorTo(rl.GetMousePosition()) 197 | })) 198 | 199 | BufferKeymap.BindKey(Key{K: ""}, MakeCommand(func(e *BufferView) { 200 | ScrollDown(e, 5) 201 | })) 202 | BufferKeymap.BindKey(Key{K: ""}, MakeCommand(func(e *BufferView) { 203 | ScrollUp(e, 5) 204 | })) 205 | BufferKeymap.BindKey(Key{K: "-hold"}, MakeCommand(func(e *BufferView) { 206 | e.moveCursorTo(rl.GetMousePosition()) 207 | })) 208 | 209 | BufferKeymap.BindKey(Key{K: "a", Control: true}, MakeCommand(func(e *BufferView) { 210 | PointToBeginningOfLine(e) 211 | })) 212 | BufferKeymap.BindKey(Key{K: "e", Control: true}, MakeCommand(func(e *BufferView) { 213 | PointToEndOfLine(e) 214 | })) 215 | BufferKeymap.BindKey(Key{K: "5", Control: true}, MakeCommand(func(e *BufferView) { 216 | PointToMatchingChar(e) 217 | })) 218 | BufferKeymap.BindKey(Key{K: "m", Control: true}, MakeCommand(func(e *BufferView) { 219 | PointToMatchingChar(e) 220 | })) 221 | BufferKeymap.BindKey(Key{K: "p", Control: true}, MakeCommand(func(e *BufferView) { 222 | PointUp(e) 223 | })) 224 | BufferKeymap.BindKey(Key{K: "n", Control: true}, MakeCommand(func(e *BufferView) { 225 | PointDown(e) 226 | })) 227 | 228 | BufferKeymap.BindKey(Key{K: ""}, MakeCommand(func(e *BufferView) { 229 | PointUp(e) 230 | })) 231 | BufferKeymap.BindKey(Key{K: ""}, MakeCommand(func(e *BufferView) { 232 | PointDown(e) 233 | })) 234 | BufferKeymap.BindKey(Key{K: ""}, MakeCommand(func(e *BufferView) { 235 | PointRight(e, 1) 236 | })) 237 | BufferKeymap.BindKey(Key{K: "", Control: true}, MakeCommand(func(e *BufferView) { 238 | PointRightWord(e) 239 | })) 240 | BufferKeymap.BindKey(Key{K: ""}, MakeCommand(func(e *BufferView) { 241 | PointLeft(e, 1) 242 | })) 243 | BufferKeymap.BindKey(Key{K: "", Control: true}, MakeCommand(func(e *BufferView) { 244 | PointLeftWord(e) 245 | })) 246 | 247 | BufferKeymap.BindKey(Key{K: "b", Control: true}, MakeCommand(func(e *BufferView) { 248 | PointLeft(e, 1) 249 | })) 250 | BufferKeymap.BindKey(Key{K: ""}, MakeCommand(func(e *BufferView) { 251 | PointToBeginningOfLine(e) 252 | })) 253 | BufferKeymap.BindKey(Key{K: ""}, MakeCommand(func(e *BufferView) { 254 | ScrollDown(e, 1) 255 | })) 256 | BufferKeymap.BindKey(Key{K: ""}, MakeCommand(func(e *BufferView) { 257 | ScrollUp(e, 1) 258 | })) 259 | BufferKeymap.BindKey(Key{K: ""}, MakeCommand(func(e *BufferView) { 260 | BufferInsertChar(e, '\n') 261 | })) 262 | BufferKeymap.BindKey(Key{K: "", Control: true}, MakeCommand(func(e *BufferView) { 263 | DeleteWordBackward(e) 264 | })) 265 | BufferKeymap.BindKey(Key{K: ""}, MakeCommand(func(e *BufferView) { 266 | DeleteCharBackward(e) 267 | })) 268 | BufferKeymap.BindKey(Key{K: "", Shift: true}, MakeCommand(func(e *BufferView) { 269 | DeleteCharBackward(e) 270 | })) 271 | BufferKeymap.BindKey(Key{K: "d", Control: true}, MakeCommand(func(e *BufferView) { 272 | DeleteCharForward(e) 273 | })) 274 | BufferKeymap.BindKey(Key{K: ""}, MakeCommand(func(e *BufferView) { 275 | DeleteCharForward(e) 276 | })) 277 | BufferKeymap.BindKey(Key{K: ""}, MakeCommand(func(e *BufferView) { Indent(e) })) 278 | 279 | CompileKeymap.BindKey(Key{K: ""}, BufferOpenLocationInCurrentLine) 280 | 281 | GlobalKeymap.BindKey(Key{K: "\\", Alt: true}, func(c *Context) { VSplit(c) }) 282 | GlobalKeymap.BindKey(Key{K: "=", Alt: true}, func(c *Context) { HSplit(c) }) 283 | GlobalKeymap.BindKey(Key{K: ";", Control: true}, func(c *Context) { Compile(c) }) 284 | GlobalKeymap.BindKey(Key{K: "q", Alt: true}, func(c *Context) { c.CloseWindow(c.ActiveWindowIndex) }) 285 | GlobalKeymap.BindKey(Key{K: "q", Alt: true, Shift: true}, Exit) 286 | GlobalKeymap.BindKey(Key{K: "0", Control: true}, func(c *Context) { c.CloseWindow(c.ActiveWindowIndex) }) 287 | GlobalKeymap.BindKey(Key{K: "1", Control: true}, func(c *Context) { c.BuildWindowToggleState() }) 288 | GlobalKeymap.BindKey(Key{K: "k", Alt: true}, func(c *Context) { c.KillDrawable(c.ActiveDrawableID()) }) 289 | GlobalKeymap.BindKey(Key{K: "t", Alt: true}, func(c *Context) { c.OpenThemesList() }) 290 | GlobalKeymap.BindKey(Key{K: "o", Control: true}, func(c *Context) { c.OpenFileList() }) 291 | GlobalKeymap.BindKey(Key{K: "b", Alt: true}, func(c *Context) { c.OpenBufferList() }) 292 | GlobalKeymap.BindKey(Key{K: "", Control: true}, func(c *Context) { c.DecreaseFontSize(2) }) 293 | GlobalKeymap.BindKey(Key{K: "", Control: true}, func(c *Context) { c.IncreaseFontSize(2) }) 294 | GlobalKeymap.BindKey(Key{K: "=", Control: true}, func(c *Context) { c.IncreaseFontSize(2) }) 295 | GlobalKeymap.BindKey(Key{K: "-", Control: true}, func(c *Context) { c.DecreaseFontSize(2) }) 296 | GlobalKeymap.BindKey(Key{K: "w", Alt: true}, func(c *Context) { c.OtherWindow() }) 297 | GlobalKeymap.BindKey(Key{K:"i", Control: true}, ToggleGlobalNoStatusbar) 298 | 299 | // Search 300 | SearchKeymap.BindKey(Key{K: ""}, MakeCommand(func(editor *BufferView) { 301 | SearchNextMatch(editor) 302 | })) 303 | SearchKeymap.BindKey(Key{K: "s", Control: true}, MakeCommand(func(editor *BufferView) { 304 | SearchNextMatch(editor) 305 | })) 306 | SearchKeymap.BindKey(Key{K: "r", Control: true}, MakeCommand(func(editor *BufferView) { 307 | SearchPreviousMatch(editor) 308 | })) 309 | SearchKeymap.BindKey(Key{K: "", Control: true}, MakeCommand(func(editor *BufferView) { 310 | SearchPreviousMatch(editor) 311 | })) 312 | SearchKeymap.BindKey(Key{K: ""}, MakeCommand(func(editor *BufferView) { 313 | SearchExit(editor) 314 | })) 315 | SearchKeymap.BindKey(Key{K: ""}, MakeCommand(func(e *BufferView) { 316 | e.Search.MovedAwayFromCurrentMatch = true 317 | ScrollUp(e, 30) 318 | })) 319 | SearchKeymap.BindKey(Key{K: ""}, MakeCommand(func(e *BufferView) { 320 | e.Search.MovedAwayFromCurrentMatch = true 321 | ScrollDown(e, 30) 322 | })) 323 | SearchKeymap.BindKey(Key{K: "-click"}, MakeCommand(func(editor *BufferView) { 324 | SearchNextMatch(editor) 325 | })) 326 | SearchKeymap.BindKey(Key{K: "-click"}, MakeCommand(func(editor *BufferView) { 327 | SearchPreviousMatch(editor) 328 | })) 329 | SearchKeymap.BindKey(Key{K: ""}, MakeCommand(func(e *BufferView) { 330 | e.Search.MovedAwayFromCurrentMatch = true 331 | ScrollDown(e, 1) 332 | })) 333 | SearchKeymap.BindKey(Key{K: ""}, MakeCommand(func(e *BufferView) { 334 | e.Search.MovedAwayFromCurrentMatch = true 335 | ScrollUp(e, 1) 336 | })) 337 | 338 | // Query replace 339 | QueryReplaceKeymap.BindKey(Key{K: "r", Control: true}, MakeCommand(func(editor *BufferView) { 340 | QueryReplaceReplaceThisMatch(editor) 341 | })) 342 | 343 | QueryReplaceKeymap.BindKey(Key{K: ""}, MakeCommand(func(editor *BufferView) { 344 | QueryReplaceReplaceThisMatch(editor) 345 | })) 346 | QueryReplaceKeymap.BindKey(Key{K: "y"}, MakeCommand(func(editor *BufferView) { 347 | QueryReplaceReplaceThisMatch(editor) 348 | })) 349 | QueryReplaceKeymap.BindKey(Key{K: "", Control: true}, MakeCommand(func(editor *BufferView) { 350 | QueryReplaceIgnoreThisMatch(editor) 351 | })) 352 | QueryReplaceKeymap.BindKey(Key{K: ""}, MakeCommand(func(editor *BufferView) { 353 | QueryReplaceExit(editor) 354 | })) 355 | QueryReplaceKeymap.BindKey(Key{K: "-click"}, MakeCommand(func(e *BufferView) { 356 | e.moveCursorTo(rl.GetMousePosition()) 357 | })) 358 | QueryReplaceKeymap.BindKey(Key{K: ""}, MakeCommand(func(e *BufferView) { 359 | e.QueryReplace.MovedAwayFromCurrentMatch = true 360 | ScrollUp(e, 30) 361 | })) 362 | QueryReplaceKeymap.BindKey(Key{K: ""}, MakeCommand(func(e *BufferView) { 363 | e.QueryReplace.MovedAwayFromCurrentMatch = true 364 | ScrollDown(e, 30) 365 | })) 366 | QueryReplaceKeymap.BindKey(Key{K: ""}, MakeCommand(func(e *BufferView) { 367 | e.QueryReplace.MovedAwayFromCurrentMatch = true 368 | ScrollDown(e, 1) 369 | })) 370 | QueryReplaceKeymap.BindKey(Key{K: ""}, MakeCommand(func(e *BufferView) { 371 | e.QueryReplace.MovedAwayFromCurrentMatch = true 372 | ScrollUp(e, 1) 373 | })) 374 | 375 | } 376 | -------------------------------------------------------------------------------- /preditor.go: -------------------------------------------------------------------------------- 1 | package preditor 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | _ "embed" 7 | "errors" 8 | "flag" 9 | "fmt" 10 | // "image" 11 | "image/color" 12 | "math/rand" 13 | "os" 14 | "path" 15 | "path/filepath" 16 | "runtime/debug" 17 | "strconv" 18 | "strings" 19 | "time" 20 | 21 | "github.com/flopp/go-findfont" 22 | 23 | "github.com/davecgh/go-spew/spew" 24 | "golang.design/x/clipboard" 25 | 26 | rl "github.com/gen2brain/raylib-go/raylib" 27 | ) 28 | 29 | //go:embed assets/logo.png 30 | var logoBytes []byte 31 | 32 | type RGBA color.RGBA 33 | 34 | func (r RGBA) String() string { 35 | colorAsHex := fmt.Sprintf("#%02x%02x%02x%02x", r.R, r.G, r.B, r.A) 36 | return colorAsHex 37 | } 38 | 39 | func (r RGBA) ToColorRGBA() color.RGBA { 40 | return color.RGBA(r) 41 | } 42 | 43 | type SyntaxColors map[string]RGBA 44 | 45 | type Colors struct { 46 | Background RGBA 47 | Foreground RGBA 48 | SelectionBackground RGBA 49 | SelectionForeground RGBA 50 | Prompts RGBA 51 | StatusBarBackground RGBA 52 | StatusBarForeground RGBA 53 | ActiveStatusBarBackground RGBA 54 | ActiveStatusBarForeground RGBA 55 | LineNumbersForeground RGBA 56 | ActiveWindowBorder RGBA 57 | Cursor RGBA 58 | CursorLineBackground RGBA 59 | HighlightMatching RGBA 60 | SyntaxColors SyntaxColors 61 | } 62 | 63 | type Drawable interface { 64 | GetID() int 65 | SetID(int) 66 | Render(zeroLocation rl.Vector2, maxHeight float64, maxWidth float64) 67 | //TODO: instead of returning a keymap slice we should have smth like 68 | // Binding(Key) Command 69 | Keymaps() []Keymap 70 | fmt.Stringer 71 | } 72 | 73 | type BaseDrawable struct { 74 | ID int 75 | } 76 | 77 | func (b BaseDrawable) GetID() int { 78 | return b.ID 79 | } 80 | func (b *BaseDrawable) SetID(i int) { 81 | b.ID = i 82 | } 83 | 84 | type Window struct { 85 | ID int 86 | DrawableID int 87 | ZeroLocationX float64 88 | ZeroLocationY float64 89 | Width float64 90 | Height float64 91 | } 92 | 93 | func (w *Window) Render(c *Context, zeroLocation rl.Vector2, maxHeight float64, maxWidth float64) { 94 | if buf := c.GetDrawable(w.DrawableID); buf != nil { 95 | buf.Render(zeroLocation, maxHeight, maxWidth) 96 | } 97 | } 98 | 99 | type Prompt struct { 100 | IsActive bool 101 | Text string 102 | UserInput string 103 | Keymap Keymap 104 | DoneHook func(userInput string, c *Context) 105 | ChangeHook func(userInput string, c *Context) 106 | NoRender bool 107 | } 108 | 109 | const ( 110 | BuildWindowState_Hide = iota 111 | BuildWindowState_Normal 112 | BuildWindowState_Maximized 113 | BuildWindowStateStateCount 114 | ) 115 | 116 | type BuildWindow struct { 117 | Window 118 | State int 119 | } 120 | 121 | var PromptKeymap = Keymap{} 122 | 123 | type Context struct { 124 | CWD string 125 | Cfg *Config 126 | ScratchBufferID int 127 | MessageDrawableID int 128 | Buffers map[string]*Buffer 129 | GlobalNoStatusbar bool 130 | Drawables []Drawable 131 | DrawablesStack *Stack[int] 132 | GlobalKeymap Keymap 133 | GlobalVariables Variables 134 | Commands Commands 135 | FontData []byte 136 | Font rl.Font 137 | FontSize int32 138 | OSWindowHeight float64 139 | OSWindowWidth float64 140 | Windows [][]*Window 141 | BuildWindow BuildWindow 142 | Prompt Prompt 143 | ActiveWindowIndex int 144 | } 145 | 146 | var GlobalKeymap = Keymap{} 147 | 148 | func (c *Context) GetBufferByFilename(filename string) *Buffer { 149 | return c.Buffers[filename] 150 | } 151 | 152 | func (c *Context) OpenFileAsBuffer(filename string) *Buffer { 153 | content, err := os.ReadFile(filename) 154 | if err != nil { 155 | fmt.Println("ERROR: cannot read file", err.Error()) 156 | } 157 | 158 | buf := Buffer{ 159 | File: filename, 160 | State: State_Clean, 161 | } 162 | 163 | //replace CRLF with LF 164 | if bytes.Index(content, []byte("\r\n")) != -1 { 165 | content = bytes.Replace(content, []byte("\r"), []byte(""), -1) 166 | buf.CRLF = true 167 | } 168 | 169 | fileType, exists := FileTypes[path.Ext(buf.File)] 170 | if exists { 171 | buf.fileType = fileType 172 | buf.needParsing = true 173 | } 174 | buf.Content = content 175 | c.Buffers[filename] = &buf 176 | 177 | return &buf 178 | } 179 | 180 | func (c *Context) BuildWindowIsVisible() bool { 181 | return c.BuildWindow.State != BuildWindowState_Hide && c.GetDrawable(c.BuildWindow.DrawableID) != nil 182 | } 183 | 184 | func (c *Context) BuildWindowMaximized() { 185 | c.BuildWindow.State = BuildWindowState_Maximized 186 | } 187 | 188 | func (c *Context) BuildWindowNormal() { 189 | c.BuildWindow.State = BuildWindowState_Normal 190 | } 191 | func (c *Context) BuildWindowHide() { 192 | c.BuildWindow.State = BuildWindowState_Hide 193 | } 194 | func (c *Context) BuildWindowToggleState() { 195 | c.BuildWindow.State++ 196 | if c.BuildWindow.State >= BuildWindowStateStateCount { 197 | c.BuildWindow.State = 0 198 | } 199 | } 200 | func (c *Context) ResetPrompt() { 201 | c.Prompt.IsActive = false 202 | c.Prompt.UserInput = "" 203 | c.Prompt.DoneHook = nil 204 | c.Prompt.ChangeHook = nil 205 | 206 | } 207 | func (c *Context) SetPrompt(text string, 208 | changeHook func(userInput string, c *Context), 209 | doneHook func(userInput string, c *Context), keymap *Keymap, defaultValue string) { 210 | c.Prompt.IsActive = true 211 | c.Prompt.Text = text 212 | c.Prompt.DoneHook = doneHook 213 | c.Prompt.UserInput = defaultValue 214 | c.Prompt.ChangeHook = changeHook 215 | if keymap != nil { 216 | c.Prompt.Keymap = *keymap 217 | } else { 218 | c.Prompt.Keymap = PromptKeymap 219 | } 220 | } 221 | 222 | func (c *Context) ActiveWindow() *Window { 223 | w := c.GetWindow(c.ActiveWindowIndex) 224 | return w 225 | } 226 | 227 | func (c *Context) ActiveDrawable() Drawable { 228 | if win := c.GetWindow(c.ActiveWindowIndex); win != nil { 229 | return c.GetDrawable(win.DrawableID) 230 | } 231 | 232 | return nil 233 | } 234 | func (c *Context) ActiveDrawableID() int { 235 | if win := c.GetWindow(c.ActiveWindowIndex); win != nil { 236 | drawableID := win.DrawableID 237 | return drawableID 238 | } 239 | 240 | return -1 241 | } 242 | 243 | var charSizeCache = map[byte]rl.Vector2{} 244 | 245 | func measureTextSize(font rl.Font, s byte, size int32, spacing float32) rl.Vector2 { 246 | if charSize, exists := charSizeCache[s]; exists { 247 | return charSize 248 | } 249 | charSize := rl.MeasureTextEx(font, string(s), float32(size), spacing) 250 | charSizeCache[s] = charSize 251 | return charSize 252 | } 253 | 254 | func (c *Context) WriteMessage(msg string) { 255 | c.GetDrawable(c.MessageDrawableID).(*BufferView).Buffer.Content = append(c.GetDrawable(c.MessageDrawableID).(*BufferView).Buffer.Content, []byte(fmt.Sprintln(msg))...) 256 | } 257 | 258 | func (c *Context) getCWD() string { 259 | if tb, isTextBuffer := c.ActiveDrawable().(*BufferView); isTextBuffer { 260 | if strings.Contains(tb.Buffer.File, "*Grep") || strings.Contains(tb.Buffer.File, "*Compilation") { 261 | segs := strings.Split(tb.Buffer.File, "@") 262 | if len(segs) > 1 { 263 | return segs[1] 264 | } 265 | } else { 266 | wd, _ := filepath.Abs(tb.Buffer.File) 267 | wd = filepath.Dir(wd) 268 | return wd 269 | } 270 | } 271 | return c.CWD 272 | } 273 | func (c *Context) AddDrawable(b Drawable) { 274 | id := rand.Intn(10000) 275 | b.SetID(id) 276 | c.Drawables = append(c.Drawables, b) 277 | c.DrawablesStack.Push(id) 278 | } 279 | 280 | func (c *Context) windowCount() int { 281 | var count int 282 | for _, col := range c.Windows { 283 | for range col { 284 | count++ 285 | } 286 | } 287 | 288 | return count 289 | } 290 | 291 | func (c *Context) AddWindowInANewColumn(w *Window) { 292 | c.Windows = append(c.Windows, []*Window{w}) 293 | w.ID = c.windowCount() 294 | } 295 | 296 | func (c *Context) AddWindowInANewColumnAndSwitchToIt(w *Window) { 297 | c.Windows = append(c.Windows, []*Window{w}) 298 | w.ID = c.windowCount() 299 | c.ActiveWindowIndex = w.ID 300 | } 301 | 302 | func (c *Context) AddWindowInCurrentColumn(w *Window) { 303 | currentColIndex := -1 304 | HERE: 305 | for i, col := range c.Windows { 306 | for _, win := range col { 307 | if win.ID == c.ActiveWindowIndex { 308 | currentColIndex = i 309 | break HERE 310 | } 311 | } 312 | } 313 | 314 | if currentColIndex != -1 { 315 | c.Windows[currentColIndex] = append(c.Windows[currentColIndex], w) 316 | w.ID = c.windowCount() 317 | } 318 | } 319 | 320 | func (c *Context) MarkWindowAsActive(id int) { 321 | c.ActiveWindowIndex = id 322 | } 323 | 324 | func (c *Context) MarkDrawableAsActive(id int) { 325 | c.ActiveWindow().DrawableID = id 326 | } 327 | 328 | func (c *Context) GetDrawable(id int) Drawable { 329 | for _, d := range c.Drawables { 330 | if d != nil && d.GetID() == id { 331 | return d 332 | } 333 | } 334 | 335 | return nil 336 | } 337 | 338 | func (c *Context) KillDrawable(id int) { 339 | for i, drawable := range c.Drawables { 340 | if drawable != nil && drawable.GetID() == id { 341 | b, ok := drawable.(*BufferView) 342 | if ok && (b.Buffer.File == "*Messages*" || b.Buffer.File == "*Scratch*") { 343 | return 344 | } 345 | c.Drawables[i] = nil 346 | break 347 | } 348 | } 349 | 350 | for { 351 | drawableID, err := c.DrawablesStack.Top() 352 | if err != nil { 353 | break 354 | } 355 | drawable := c.GetDrawable(drawableID) 356 | if drawable == nil { 357 | c.DrawablesStack.Pop() 358 | continue 359 | 360 | } 361 | b, ok := drawable.(*BufferView) 362 | if ok { 363 | c.ActiveWindow().DrawableID = NewBufferViewFromFilename(c, c.Cfg, b.Buffer.File).ID 364 | break 365 | } 366 | 367 | } 368 | 369 | } 370 | 371 | func (c *Context) LoadFont(name string, size int32) error { 372 | switch strings.ToLower(name) { 373 | case "liberationmono-regular": 374 | c.FontSize = size 375 | c.FontData = liberationMonoRegularTTF 376 | case "jetbrainsmono": 377 | c.FontSize = size 378 | c.FontData = jetbrainsMonoTTF 379 | default: 380 | var err error 381 | path, err := findfont.Find(name + ".ttf") 382 | if err != nil { 383 | return err 384 | } 385 | c.FontData, err = os.ReadFile(path) 386 | if err != nil { 387 | return err 388 | } 389 | } 390 | 391 | c.FontSize = size 392 | c.Font = rl.LoadFontFromMemory(".ttf", c.FontData, int32(len(c.FontData)), c.FontSize, nil, 0) 393 | return nil 394 | } 395 | 396 | func (c *Context) IncreaseFontSize(n int) { 397 | c.FontSize += int32(n) 398 | c.Font = rl.LoadFontFromMemory(".ttf", c.FontData, int32(len(c.FontData)), c.FontSize, nil, 0) 399 | charSizeCache = map[byte]rl.Vector2{} 400 | } 401 | 402 | func (c *Context) DecreaseFontSize(n int) { 403 | c.FontSize -= int32(n) 404 | c.Font = rl.LoadFontFromMemory(".ttf", c.FontData, int32(len(c.FontData)), c.FontSize, nil, 0) 405 | charSizeCache = map[byte]rl.Vector2{} 406 | 407 | } 408 | 409 | type Command func(*Context) 410 | type Variables map[string]any 411 | type Key struct { 412 | Control bool 413 | Alt bool 414 | Shift bool 415 | Super bool 416 | K string 417 | } 418 | 419 | func (k Key) IsEmpty() bool { 420 | return k.K == "" 421 | } 422 | 423 | type Keymap map[Key]Command 424 | 425 | func (k Keymap) Clone() Keymap { 426 | cloned := Keymap{} 427 | for i, v := range k { 428 | cloned[i] = v 429 | } 430 | 431 | return cloned 432 | } 433 | 434 | func (k Keymap) BindKey(key Key, command Command) { 435 | k[key] = command 436 | } 437 | func (k Keymap) SetKeys(k2 Keymap) { 438 | for b, f := range k2 { 439 | k[b] = f 440 | } 441 | } 442 | 443 | type Commands map[string]Command 444 | type Position struct { 445 | Line int 446 | Column int 447 | } 448 | 449 | func (p Position) String() string { 450 | return fmt.Sprintf("Line: %d Column:%d\n", p.Line, p.Column) 451 | } 452 | 453 | func parseHexColor(v string) (out color.RGBA, err error) { 454 | if len(v) != 7 { 455 | return out, errors.New("hex color must be 7 characters") 456 | } 457 | if v[0] != '#' { 458 | return out, errors.New("hex color must start with '#'") 459 | } 460 | var red, redError = strconv.ParseUint(v[1:3], 16, 8) 461 | if redError != nil { 462 | return out, errors.New("red component invalid") 463 | } 464 | out.R = uint8(red) 465 | var green, greenError = strconv.ParseUint(v[3:5], 16, 8) 466 | if greenError != nil { 467 | return out, errors.New("green component invalid") 468 | } 469 | out.G = uint8(green) 470 | var blue, blueError = strconv.ParseUint(v[5:7], 16, 8) 471 | if blueError != nil { 472 | return out, errors.New("blue component invalid") 473 | } 474 | out.B = uint8(blue) 475 | out.A = 255 476 | return 477 | } 478 | 479 | func (c *Context) HandleKeyEvents() { 480 | defer handlePanicAndWriteMessage(c) 481 | key := getKey() 482 | if !key.IsEmpty() { 483 | 484 | keymaps := []Keymap{c.GlobalKeymap} 485 | if c.ActiveDrawable() != nil { 486 | keymaps = append(keymaps, c.ActiveDrawable().Keymaps()...) 487 | } 488 | if c.Prompt.IsActive { 489 | keymaps = append(keymaps, c.Prompt.Keymap) 490 | } 491 | for i := len(keymaps) - 1; i >= 0; i-- { 492 | cmd := keymaps[i][key] 493 | if cmd != nil { 494 | cmd(c) 495 | break 496 | } 497 | } 498 | } 499 | 500 | } 501 | 502 | func (c *Context) GetWindow(id int) *Window { 503 | if id == -10 { 504 | return &c.BuildWindow.Window 505 | } 506 | for _, col := range c.Windows { 507 | for _, win := range col { 508 | if win.ID == id { 509 | return win 510 | } 511 | } 512 | } 513 | 514 | return nil 515 | } 516 | 517 | func isVisibleInWindow(posX float64, posY float64, zeroLocation rl.Vector2, maxH float64, maxW float64) bool { 518 | return float32(posX) >= zeroLocation.X && 519 | float64(posX) <= (float64(zeroLocation.X)+maxW) && 520 | float64(posY) >= float64(zeroLocation.Y) && 521 | float64(posY) <= float64(zeroLocation.Y)+maxH 522 | } 523 | 524 | func (c *Context) Render() { 525 | rl.BeginDrawing() 526 | rl.ClearBackground(c.Cfg.CurrentThemeColors().Background.ToColorRGBA()) 527 | height := c.OSWindowHeight 528 | var buildWindowHeightRatio float64 529 | if c.BuildWindow.State == BuildWindowState_Normal { 530 | buildWindowHeightRatio = c.Cfg.BuildWindowNormalHeight 531 | } else if c.BuildWindow.State == BuildWindowState_Maximized { 532 | buildWindowHeightRatio = c.Cfg.BuildWindowMaximizedHeight 533 | } else if c.BuildWindow.State == BuildWindowState_Hide { 534 | buildWindowHeightRatio = 0 535 | } 536 | charsize := measureTextSize(c.Font, ' ', c.FontSize, 0) 537 | if c.Prompt.IsActive && !c.Prompt.NoRender { 538 | height -= float64(charsize.Y) 539 | } 540 | if c.BuildWindowIsVisible() { 541 | height -= float64(buildWindowHeightRatio * c.OSWindowHeight) 542 | } 543 | for i, column := range c.Windows { 544 | columnWidth := c.OSWindowWidth / float64(len(c.Windows)) 545 | columnZeroX := float64(i) * float64(columnWidth) 546 | for j, win := range column { 547 | if win == nil { 548 | continue 549 | } 550 | winHeight := height / float64(len(column)) 551 | winZeroY := float64(j) * winHeight 552 | zeroLocation := rl.Vector2{X: float32(columnZeroX), Y: float32(winZeroY)} 553 | win.Width = columnWidth 554 | win.Height = winHeight 555 | win.ZeroLocationX = float64(zeroLocation.X) 556 | win.ZeroLocationY = float64(zeroLocation.Y) 557 | win.Render(c, zeroLocation, winHeight, columnWidth) 558 | if c.ActiveWindowIndex == win.ID { 559 | rl.DrawRectangleLines(int32(columnZeroX), int32(winZeroY), int32(columnWidth), int32(winHeight), c.Cfg.CurrentThemeColors().ActiveWindowBorder.ToColorRGBA()) 560 | } 561 | 562 | } 563 | } 564 | 565 | c.BuildWindow.ZeroLocationX = 0 566 | c.BuildWindow.Width = c.OSWindowWidth 567 | 568 | c.BuildWindow.ZeroLocationY = c.OSWindowHeight - (c.OSWindowHeight * buildWindowHeightRatio) 569 | c.BuildWindow.Height = c.OSWindowHeight * buildWindowHeightRatio 570 | 571 | if c.BuildWindowIsVisible() { 572 | buf := c.GetDrawable(c.BuildWindow.DrawableID) 573 | buf.Render(rl.Vector2{X: float32(c.BuildWindow.ZeroLocationX), Y: float32(c.BuildWindow.ZeroLocationY)}, c.BuildWindow.Height, c.BuildWindow.Width) 574 | } 575 | 576 | if c.Prompt.IsActive && !c.Prompt.NoRender { 577 | rl.DrawRectangle(0, int32(height), int32(c.OSWindowWidth), int32(charsize.Y), c.Cfg.CurrentThemeColors().Prompts.ToColorRGBA()) 578 | rl.DrawTextEx(c.Font, fmt.Sprintf("%s: %s", c.Prompt.Text, c.Prompt.UserInput), rl.Vector2{ 579 | X: 0, 580 | Y: float32(height), 581 | }, float32(c.FontSize), 0, rl.White) 582 | } 583 | rl.DrawTextEx(c.Font, fmt.Sprint(rl.GetFPS()), rl.Vector2{ 584 | X: float32(c.OSWindowWidth - float64(charsize.X*3)), 585 | Y: float32(c.OSWindowHeight - float64(charsize.Y)), 586 | }, float32(c.FontSize), 0, rl.Red) 587 | rl.EndDrawing() 588 | } 589 | 590 | func (c *Context) HandleWindowResize() { 591 | c.OSWindowHeight = float64(rl.GetRenderHeight()) 592 | c.OSWindowWidth = float64(rl.GetRenderWidth()) 593 | } 594 | 595 | func (c *Context) HandleMouseEvents() { 596 | defer handlePanicAndWriteMessage(c) 597 | 598 | key := getMouseKey() 599 | if !key.IsEmpty() { 600 | //first check if mouse position is in the window context otherwise switch 601 | pos := rl.GetMousePosition() 602 | win := c.GetWindow(c.ActiveWindowIndex) 603 | if float64(pos.X) < win.ZeroLocationX || 604 | float64(pos.Y) < win.ZeroLocationY || 605 | float64(pos.X) > win.ZeroLocationX+win.Width || 606 | float64(pos.Y) > win.ZeroLocationY+win.Height { 607 | for _, col := range c.Windows { 608 | for _, win := range col { 609 | if float64(pos.X) >= win.ZeroLocationX && 610 | float64(pos.Y) >= win.ZeroLocationY && 611 | float64(pos.X) <= win.ZeroLocationX+win.Width && 612 | float64(pos.Y) <= win.ZeroLocationY+win.Height { 613 | c.ActiveWindowIndex = win.ID 614 | break 615 | } 616 | } 617 | } 618 | } 619 | // handle build window 620 | if c.BuildWindowIsVisible() && float64(pos.Y) >= c.BuildWindow.ZeroLocationY { 621 | c.ActiveWindowIndex = c.BuildWindow.ID 622 | } 623 | 624 | keymaps := []Keymap{c.GlobalKeymap} 625 | if c.ActiveDrawable() != nil { 626 | keymaps = append(keymaps, c.ActiveDrawable().Keymaps()...) 627 | } 628 | for i := len(keymaps) - 1; i >= 0; i-- { 629 | cmd := keymaps[i][key] 630 | if cmd != nil { 631 | cmd(c) 632 | break 633 | } 634 | } 635 | } 636 | } 637 | 638 | type modifierKeyState struct { 639 | control bool 640 | alt bool 641 | shift bool 642 | super bool 643 | } 644 | 645 | func getModifierKeyState() modifierKeyState { 646 | state := modifierKeyState{} 647 | if rl.IsKeyDown(rl.KeyLeftControl) || rl.IsKeyDown(rl.KeyRightControl) { 648 | state.control = true 649 | } 650 | if rl.IsKeyDown(rl.KeyLeftAlt) || rl.IsKeyDown(rl.KeyRightAlt) { 651 | state.alt = true 652 | } 653 | if rl.IsKeyDown(rl.KeyLeftShift) || rl.IsKeyDown(rl.KeyRightShift) { 654 | state.shift = true 655 | } 656 | if rl.IsKeyDown(rl.KeyLeftSuper) || rl.IsKeyDown(rl.KeyRightSuper) { 657 | state.super = true 658 | } 659 | 660 | return state 661 | } 662 | 663 | func getKey() Key { 664 | modifierState := getModifierKeyState() 665 | key := getKeyPressedString() 666 | 667 | k := Key{ 668 | Control: modifierState.control, 669 | Alt: modifierState.alt, 670 | Super: modifierState.super, 671 | Shift: modifierState.shift, 672 | K: key, 673 | } 674 | 675 | return k 676 | } 677 | 678 | func getMouseKey() Key { 679 | modifierState := getModifierKeyState() 680 | var key string 681 | switch { 682 | case rl.IsMouseButtonPressed(rl.MouseButtonLeft): 683 | key = "-click" 684 | case rl.IsMouseButtonPressed(rl.MouseButtonMiddle): 685 | key = "-click" 686 | case rl.IsMouseButtonPressed(rl.MouseButtonRight): 687 | key = "-click" 688 | case rl.IsMouseButtonDown(rl.MouseButtonLeft): 689 | key = "-hold" 690 | 691 | case rl.IsMouseButtonDown(rl.MouseButtonMiddle): 692 | key = "-hold" 693 | 694 | case rl.IsMouseButtonDown(rl.MouseButtonRight): 695 | key = "-hold" 696 | } 697 | 698 | if wheel := rl.GetMouseWheelMoveV(); wheel.X != 0 || wheel.Y != 0 { 699 | if wheel.Y != 0 { 700 | if wheel.Y < 0 { 701 | key = "" 702 | } 703 | if wheel.Y > 0 { 704 | key = "" 705 | } 706 | 707 | } 708 | } 709 | 710 | if key == "" { 711 | return Key{} 712 | } 713 | 714 | k := Key{ 715 | Control: modifierState.control, 716 | Alt: modifierState.alt, 717 | Super: modifierState.super, 718 | Shift: modifierState.shift, 719 | K: key, 720 | } 721 | 722 | return k 723 | 724 | } 725 | 726 | func isPressed(key int32) bool { 727 | return rl.IsKeyPressed(key) || rl.IsKeyPressedRepeat(key) 728 | } 729 | 730 | func getKeyPressedString() string { 731 | switch { 732 | case isPressed(rl.KeyGrave): 733 | return "`" 734 | case isPressed(rl.KeyApostrophe): 735 | return "'" 736 | case isPressed(rl.KeySpace): 737 | return "" 738 | case isPressed(rl.KeyEscape): 739 | return "" 740 | case isPressed(rl.KeyEnter): 741 | return "" 742 | case isPressed(rl.KeyTab): 743 | return "" 744 | case isPressed(rl.KeyBackspace): 745 | return "" 746 | case isPressed(rl.KeyInsert): 747 | return "" 748 | case isPressed(rl.KeyDelete): 749 | return "" 750 | case isPressed(rl.KeyRight): 751 | return "" 752 | case isPressed(rl.KeyLeft): 753 | return "" 754 | case isPressed(rl.KeyDown): 755 | return "" 756 | case isPressed(rl.KeyUp): 757 | return "" 758 | case isPressed(rl.KeyPageUp): 759 | return "" 760 | case isPressed(rl.KeyPageDown): 761 | return "" 762 | case isPressed(rl.KeyHome): 763 | return "" 764 | case isPressed(rl.KeyEnd): 765 | return "" 766 | case isPressed(rl.KeyCapsLock): 767 | return "" 768 | case isPressed(rl.KeyScrollLock): 769 | return "" 770 | case isPressed(rl.KeyNumLock): 771 | return "" 772 | case isPressed(rl.KeyPrintScreen): 773 | return "" 774 | case isPressed(rl.KeyPause): 775 | return "" 776 | case isPressed(rl.KeyF1): 777 | return "" 778 | case isPressed(rl.KeyF2): 779 | return "" 780 | case isPressed(rl.KeyF3): 781 | return "" 782 | case isPressed(rl.KeyF4): 783 | return "" 784 | case isPressed(rl.KeyF5): 785 | return "" 786 | case isPressed(rl.KeyF6): 787 | return "" 788 | case isPressed(rl.KeyF7): 789 | return "" 790 | case isPressed(rl.KeyF8): 791 | return "" 792 | case isPressed(rl.KeyF9): 793 | return "" 794 | case isPressed(rl.KeyF10): 795 | return "" 796 | case isPressed(rl.KeyF11): 797 | return "" 798 | case isPressed(rl.KeyF12): 799 | return "" 800 | case isPressed(rl.KeyLeftBracket): 801 | return "[" 802 | case isPressed(rl.KeyBackSlash): 803 | return "\\" 804 | case isPressed(rl.KeyRightBracket): 805 | return "]" 806 | case isPressed(rl.KeyKp0): 807 | return "0" 808 | case isPressed(rl.KeyKp1): 809 | return "1" 810 | case isPressed(rl.KeyKp2): 811 | return "2" 812 | case isPressed(rl.KeyKp3): 813 | return "3" 814 | case isPressed(rl.KeyKp4): 815 | return "4" 816 | case isPressed(rl.KeyKp5): 817 | return "5" 818 | case isPressed(rl.KeyKp6): 819 | return "6" 820 | case isPressed(rl.KeyKp7): 821 | return "7" 822 | case isPressed(rl.KeyKp8): 823 | return "8" 824 | case isPressed(rl.KeyKp9): 825 | return "9" 826 | case isPressed(rl.KeyKpDecimal): 827 | return "." 828 | case isPressed(rl.KeyKpDivide): 829 | return "/" 830 | case isPressed(rl.KeyKpMultiply): 831 | return "*" 832 | case isPressed(rl.KeyKpSubtract): 833 | return "-" 834 | case isPressed(rl.KeyKpAdd): 835 | return "+" 836 | case isPressed(rl.KeyKpEnter): 837 | return "" 838 | case isPressed(rl.KeyKpEqual): 839 | return "=" 840 | case isPressed(rl.KeyApostrophe): 841 | return "'" 842 | case isPressed(rl.KeyComma): 843 | return "," 844 | case isPressed(rl.KeyMinus): 845 | return "-" 846 | case isPressed(rl.KeyPeriod): 847 | return "." 848 | case isPressed(rl.KeySlash): 849 | return "/" 850 | case isPressed(rl.KeyZero): 851 | return "0" 852 | case isPressed(rl.KeyOne): 853 | return "1" 854 | case isPressed(rl.KeyTwo): 855 | return "2" 856 | case isPressed(rl.KeyThree): 857 | return "3" 858 | case isPressed(rl.KeyFour): 859 | return "4" 860 | case isPressed(rl.KeyFive): 861 | return "5" 862 | case isPressed(rl.KeySix): 863 | return "6" 864 | case isPressed(rl.KeySeven): 865 | return "7" 866 | case isPressed(rl.KeyEight): 867 | return "8" 868 | case isPressed(rl.KeyNine): 869 | return "9" 870 | case isPressed(rl.KeySemicolon): 871 | return ";" 872 | case isPressed(rl.KeyEqual): 873 | return "=" 874 | case isPressed(rl.KeyA): 875 | return "a" 876 | case isPressed(rl.KeyB): 877 | return "b" 878 | case isPressed(rl.KeyC): 879 | return "c" 880 | case isPressed(rl.KeyD): 881 | return "d" 882 | case isPressed(rl.KeyE): 883 | return "e" 884 | case isPressed(rl.KeyF): 885 | return "f" 886 | case isPressed(rl.KeyG): 887 | return "g" 888 | case isPressed(rl.KeyH): 889 | return "h" 890 | case isPressed(rl.KeyI): 891 | return "i" 892 | case isPressed(rl.KeyJ): 893 | return "j" 894 | case isPressed(rl.KeyK): 895 | return "k" 896 | case isPressed(rl.KeyL): 897 | return "l" 898 | case isPressed(rl.KeyM): 899 | return "m" 900 | case isPressed(rl.KeyN): 901 | return "n" 902 | case isPressed(rl.KeyO): 903 | return "o" 904 | case isPressed(rl.KeyP): 905 | return "p" 906 | case isPressed(rl.KeyQ): 907 | return "q" 908 | case isPressed(rl.KeyR): 909 | return "r" 910 | case isPressed(rl.KeyS): 911 | return "s" 912 | case isPressed(rl.KeyT): 913 | return "t" 914 | case isPressed(rl.KeyU): 915 | return "u" 916 | case isPressed(rl.KeyV): 917 | return "v" 918 | case isPressed(rl.KeyW): 919 | return "w" 920 | case isPressed(rl.KeyX): 921 | return "x" 922 | case isPressed(rl.KeyY): 923 | return "y" 924 | case isPressed(rl.KeyZ): 925 | return "z" 926 | default: 927 | return "" 928 | } 929 | } 930 | 931 | func setupRaylib(cfg *Config) { 932 | // basic setup 933 | rl.SetConfigFlags(rl.FlagWindowResizable | rl.FlagWindowMaximized | rl.FlagVsyncHint) 934 | rl.SetTraceLogLevel(rl.LogError) 935 | rl.InitWindow(800, 600, "Preditor") 936 | rl.SetTargetFPS(60) 937 | rl.SetTextLineSpacing(cfg.FontSize) 938 | // img, _, err := image.Decode(bytes.NewReader(logoBytes)) 939 | // if err != nil { 940 | // panic(err) 941 | // } 942 | 943 | // rlImage := rl.NewImageFromImage(img) 944 | // rl.SetWindowIcon(*rlImage) 945 | rl.SetExitKey(0) 946 | 947 | } 948 | 949 | func New() (*Context, error) { 950 | var configPath string 951 | var startTheme string 952 | flag.StringVar(&configPath, "cfg", path.Join(os.Getenv("HOME"), ".preditor"), "path to config file, defaults to: ~/.preditor") 953 | flag.StringVar(&startTheme, "theme", "", "Start theme to use overrides the config and editor defaults.") 954 | flag.Parse() 955 | 956 | // read config file 957 | cfg, err := ReadConfig(configPath, startTheme) 958 | if err != nil { 959 | panic(err) 960 | } 961 | 962 | // create editor 963 | setupRaylib(cfg) 964 | 965 | if err := clipboard.Init(); err != nil { 966 | panic(err) 967 | } 968 | 969 | wd, err := os.Getwd() 970 | if err != nil { 971 | return nil, err 972 | } 973 | wd, err = filepath.Abs(wd) 974 | p := &Context{ 975 | Cfg: cfg, 976 | CWD: wd, 977 | Drawables: []Drawable{}, 978 | OSWindowHeight: float64(rl.GetRenderHeight()), 979 | OSWindowWidth: float64(rl.GetRenderWidth()), 980 | Windows: [][]*Window{}, 981 | Buffers: map[string]*Buffer{}, 982 | DrawablesStack: NewStack[int](1000), 983 | } 984 | 985 | setupDefaults() 986 | err = p.LoadFont(cfg.FontName, int32(cfg.FontSize)) 987 | if err != nil { 988 | return nil, err 989 | } 990 | scratch := NewBufferViewFromFilename(p, p.Cfg, "*Scratch*") 991 | message := NewBufferViewFromFilename(p, p.Cfg, "*Messages*") 992 | message.Buffer.Readonly = true 993 | message.Buffer.Content = append(message.Buffer.Content, []byte(fmt.Sprintf("Loaded Configuration from '%s':\n%s\n", configPath, cfg))...) 994 | 995 | p.AddDrawable(scratch) 996 | p.AddDrawable(message) 997 | 998 | p.MessageDrawableID = message.ID 999 | p.ScratchBufferID = scratch.ID 1000 | 1001 | mainWindow := Window{} 1002 | p.AddWindowInANewColumn(&mainWindow) 1003 | 1004 | p.MarkWindowAsActive(mainWindow.ID) 1005 | 1006 | p.MarkDrawableAsActive(scratch.ID) 1007 | 1008 | p.GlobalKeymap = GlobalKeymap 1009 | 1010 | p.BuildWindow = BuildWindow{ 1011 | Window: Window{ID: -10}, 1012 | State: BuildWindowState_Normal, 1013 | } 1014 | // handle command line argument 1015 | filename := "" 1016 | if len(flag.Args()) > 0 { 1017 | filename = flag.Args()[0] 1018 | if filename == "-" { 1019 | //stdin 1020 | tb := NewBufferViewFromFilename(p, cfg, "stdin") 1021 | p.AddDrawable(tb) 1022 | p.MarkDrawableAsActive(tb.ID) 1023 | tb.Buffer.Readonly = true 1024 | go func() { 1025 | r := bufio.NewReader(os.Stdin) 1026 | for { 1027 | b, err := r.ReadByte() 1028 | if err != nil { 1029 | p.WriteMessage(err.Error()) 1030 | break 1031 | } 1032 | tb.Buffer.Content = append(tb.Buffer.Content, b) 1033 | } 1034 | }() 1035 | } else { 1036 | err = SwitchOrOpenFileInCurrentWindow(p, cfg, filename, nil) 1037 | if err != nil { 1038 | panic(err) 1039 | } 1040 | } 1041 | } 1042 | 1043 | return p, nil 1044 | } 1045 | 1046 | func Exit(c *Context) { 1047 | rl.CloseWindow() 1048 | } 1049 | 1050 | func (c *Context) StartMainLoop() { 1051 | defer func() { 1052 | if r := recover(); r != nil { 1053 | err := os.WriteFile(path.Join(os.Getenv("HOME"), 1054 | fmt.Sprintf("preditor-crashlog-%d", time.Now().Unix())), 1055 | []byte(fmt.Sprintf("%v\n%s\n%s", r, string(debug.Stack()), spew.Sdump(c))), 0644) 1056 | if err != nil { 1057 | fmt.Println("we are doomed") 1058 | fmt.Println(err) 1059 | } 1060 | 1061 | fmt.Printf("%v\n%s\n", r, string(debug.Stack())) 1062 | } 1063 | }() 1064 | //TODO: Check for drag and dropped files rl.IsFileDropped() 1065 | for !rl.WindowShouldClose() { 1066 | if rl.IsFileDropped() { 1067 | files := rl.LoadDroppedFiles() 1068 | if len(files) > 0 { 1069 | for _, file := range files { 1070 | SwitchOrOpenFileInCurrentWindow(c, c.Cfg, file, nil) 1071 | } 1072 | } 1073 | } 1074 | c.HandleWindowResize() 1075 | c.HandleMouseEvents() 1076 | c.HandleKeyEvents() 1077 | c.Render() 1078 | } 1079 | } 1080 | 1081 | func MakeCommand[T Drawable](f func(t T)) Command { 1082 | return func(c *Context) { 1083 | f(c.ActiveDrawable().(T)) 1084 | 1085 | } 1086 | 1087 | } 1088 | 1089 | func (c *Context) MaxHeightToMaxLine(maxH int32) int32 { 1090 | return maxH / int32(measureTextSize(c.Font, ' ', c.FontSize, 0).Y) 1091 | } 1092 | func (c *Context) MaxWidthToMaxColumn(maxW int32) int32 { 1093 | return maxW / int32(measureTextSize(c.Font, ' ', c.FontSize, 0).X) 1094 | } 1095 | 1096 | func (c *Context) OpenFileList() { 1097 | ofb := NewFileList(c, c.Cfg, c.getCWD()) 1098 | c.AddDrawable(ofb) 1099 | c.MarkDrawableAsActive(ofb.ID) 1100 | } 1101 | 1102 | func (c *Context) OpenFuzzyFileList() { 1103 | ofb := NewFuzzyFileList(c, c.Cfg, c.getCWD()) 1104 | c.AddDrawable(ofb) 1105 | c.MarkDrawableAsActive(ofb.ID) 1106 | 1107 | } 1108 | func (c *Context) OpenBufferList() { 1109 | ofb := NewBufferList(c, c.Cfg) 1110 | c.AddDrawable(ofb) 1111 | c.MarkDrawableAsActive(ofb.ID) 1112 | } 1113 | 1114 | func (c *Context) OpenThemesList() { 1115 | ofb := NewThemeList(c, c.Cfg) 1116 | c.AddDrawable(ofb) 1117 | c.MarkDrawableAsActive(ofb.ID) 1118 | } 1119 | 1120 | func SwitchOrOpenFileInWindow(parent *Context, cfg *Config, filename string, startingPos *Position, window *Window) error { 1121 | bufferView := NewBufferViewFromFilename(parent, cfg, filename) 1122 | parent.AddDrawable(bufferView) 1123 | window.DrawableID = bufferView.ID 1124 | bufferView.MoveToPositionInNextRender = startingPos 1125 | return nil 1126 | } 1127 | 1128 | func SwitchOrOpenFileInCurrentWindow(parent *Context, cfg *Config, filename string, startingPos *Position) error { 1129 | return SwitchOrOpenFileInWindow(parent, cfg, filename, startingPos, parent.ActiveWindow()) 1130 | } 1131 | 1132 | func (c *Context) openCompilationBuffer(command string) error { 1133 | cb, err := NewCompilationBuffer(c, c.Cfg, command) 1134 | if err != nil { 1135 | return err 1136 | } 1137 | c.AddDrawable(cb) 1138 | c.MarkDrawableAsActive(cb.ID) 1139 | 1140 | return nil 1141 | } 1142 | 1143 | func (c *Context) OpenCompilationBufferInAVSplit(command string) error { 1144 | win := VSplit(c) 1145 | cb, err := NewCompilationBuffer(c, c.Cfg, command) 1146 | if err != nil { 1147 | return err 1148 | } 1149 | c.AddDrawable(cb) 1150 | win.DrawableID = cb.ID 1151 | return nil 1152 | } 1153 | 1154 | func (c *Context) OpenCompilationBufferInAHSplit(command string) error { 1155 | win := HSplit(c) 1156 | cb, err := NewCompilationBuffer(c, c.Cfg, command) 1157 | if err != nil { 1158 | return err 1159 | } 1160 | c.AddDrawable(cb) 1161 | win.DrawableID = cb.ID 1162 | return nil 1163 | } 1164 | 1165 | func (c *Context) OpenCompilationBufferInSensibleSplit(command string) error { 1166 | var window *Window 1167 | for _, col := range c.Windows { 1168 | for _, win := range col { 1169 | if buf := c.GetDrawable(win.DrawableID); buf != nil { 1170 | if b, is := buf.(*BufferView); is { 1171 | if b.Buffer.File == "*Compilation*" { 1172 | window = win 1173 | } 1174 | } 1175 | } 1176 | } 1177 | } 1178 | 1179 | if window == nil { 1180 | window = HSplit(c) 1181 | } 1182 | cb, err := NewCompilationBuffer(c, c.Cfg, command) 1183 | if err != nil { 1184 | return err 1185 | } 1186 | c.AddDrawable(cb) 1187 | window.DrawableID = cb.ID 1188 | 1189 | return nil 1190 | } 1191 | 1192 | func Compile(c *Context) { 1193 | c.SetPrompt("Compile", nil, func(userInput string, c *Context) { 1194 | if err := c.openCompilationBuffer(userInput); err != nil { 1195 | return 1196 | } 1197 | 1198 | return 1199 | }, nil, "") 1200 | 1201 | } 1202 | 1203 | func (c *Context) OpenGrepBufferInSensibleSplit(command string) error { 1204 | var window *Window 1205 | for _, col := range c.Windows { 1206 | for _, win := range col { 1207 | if buf := c.GetDrawable(win.DrawableID); buf != nil { 1208 | if b, is := buf.(*BufferView); is { 1209 | if b.Buffer.File == "*Grep*" { 1210 | window = win 1211 | } 1212 | } 1213 | } 1214 | } 1215 | } 1216 | 1217 | if window == nil { 1218 | window = VSplit(c) 1219 | } 1220 | cb, err := NewGrepBuffer(c, c.Cfg, command) 1221 | if err != nil { 1222 | return err 1223 | } 1224 | c.AddDrawable(cb) 1225 | window.DrawableID = cb.ID 1226 | 1227 | return nil 1228 | } 1229 | 1230 | func (c *Context) OpenCompilationBufferInBuildWindow(command string) error { 1231 | cb, err := NewCompilationBuffer(c, c.Cfg, command) 1232 | if err != nil { 1233 | return err 1234 | } 1235 | 1236 | c.AddDrawable(cb) 1237 | 1238 | c.BuildWindow.DrawableID = cb.ID 1239 | 1240 | return nil 1241 | } 1242 | 1243 | func VSplit(c *Context) *Window { 1244 | win := &Window{} 1245 | c.AddWindowInANewColumn(win) 1246 | return win 1247 | } 1248 | 1249 | func HSplit(c *Context) *Window { 1250 | win := &Window{} 1251 | c.AddWindowInCurrentColumn(win) 1252 | return win 1253 | } 1254 | 1255 | func (c *Context) OtherWindow() { 1256 | for i, col := range c.Windows { 1257 | for j, win := range col { 1258 | if c.ActiveWindowIndex == -10 { 1259 | c.ActiveWindowIndex = win.ID 1260 | break 1261 | } 1262 | if win.ID == c.ActiveWindowIndex { 1263 | if j+1 < len(col) { 1264 | c.ActiveWindowIndex = col[j+1].ID 1265 | return 1266 | } else { 1267 | if i+1 < len(c.Windows) { 1268 | c.ActiveWindowIndex = c.Windows[i+1][0].ID 1269 | return 1270 | 1271 | } else { 1272 | c.ActiveWindowIndex = c.Windows[0][0].ID 1273 | return 1274 | 1275 | } 1276 | } 1277 | } 1278 | } 1279 | } 1280 | } 1281 | 1282 | func removeSliceIndex[T any](s []T, i int) []T { 1283 | if i == len(s)-1 { 1284 | s = s[:i] 1285 | } else { 1286 | s = append(s[:i], s[i+1:]...) 1287 | } 1288 | 1289 | return s 1290 | } 1291 | 1292 | func (c *Context) CloseWindow(id int) { 1293 | if c.windowCount() < 2 { 1294 | return 1295 | } 1296 | for i := 0; i < len(c.Windows); i++ { 1297 | checkCol := -1 1298 | for j := 0; j < len(c.Windows[i]); j++ { 1299 | if c.Windows[i][j].ID == id { 1300 | c.Windows[i] = removeSliceIndex(c.Windows[i], j) 1301 | checkCol = i 1302 | break 1303 | } 1304 | } 1305 | 1306 | if checkCol != -1 && len(c.Windows[checkCol]) == 0 { 1307 | c.Windows = removeSliceIndex(c.Windows, checkCol) 1308 | } 1309 | 1310 | } 1311 | 1312 | for _, col := range c.Windows { 1313 | for _, win := range col { 1314 | c.ActiveWindowIndex = win.ID 1315 | break 1316 | } 1317 | } 1318 | } 1319 | 1320 | func handlePanicAndWriteMessage(p *Context) { 1321 | r := recover() 1322 | if r != nil { 1323 | msg := fmt.Sprintf("%v\n%s", r, string(debug.Stack())) 1324 | fmt.Println(msg) 1325 | p.WriteMessage(msg) 1326 | } 1327 | } 1328 | 1329 | func ToggleGlobalNoStatusbar(c *Context) { 1330 | c.GlobalNoStatusbar = !c.GlobalNoStatusbar 1331 | } 1332 | -------------------------------------------------------------------------------- /buffer.go: -------------------------------------------------------------------------------- 1 | package preditor 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | "fmt" 8 | "github.com/smacker/go-tree-sitter/golang" 9 | "image/color" 10 | "math" 11 | "os" 12 | "os/exec" 13 | "path/filepath" 14 | "sort" 15 | "strconv" 16 | "strings" 17 | "time" 18 | "unicode" 19 | 20 | "golang.design/x/clipboard" 21 | 22 | "github.com/amirrezaask/preditor/byteutils" 23 | sitter "github.com/smacker/go-tree-sitter" 24 | 25 | rl "github.com/gen2brain/raylib-go/raylib" 26 | ) 27 | 28 | var BufferKeymap = Keymap{} 29 | var SearchKeymap = Keymap{} 30 | var QueryReplaceKeymap = Keymap{} 31 | var CompileKeymap = Keymap{} 32 | 33 | func BufferOpenLocationInCurrentLine(c *Context) { 34 | b, ok := c.ActiveDrawable().(*BufferView) 35 | if !ok { 36 | return 37 | } 38 | 39 | line := BufferGetCurrentLine(b) 40 | if line == nil || len(line) < 1 { 41 | return 42 | } 43 | 44 | segs := bytes.SplitN(line, []byte(":"), 4) 45 | if len(segs) < 2 { 46 | return 47 | 48 | } 49 | 50 | var targetWindow *Window 51 | for _, col := range c.Windows { 52 | for _, win := range col { 53 | if c.ActiveWindowIndex != win.ID { 54 | targetWindow = win 55 | break 56 | } 57 | } 58 | } 59 | 60 | filename := segs[0] 61 | var lineNum int 62 | var col int 63 | var err error 64 | switch len(segs) { 65 | case 3: 66 | //filename:line: text 67 | lineNum, err = strconv.Atoi(string(segs[1])) 68 | if err != nil { 69 | } 70 | case 4: 71 | //filename:line:col: text 72 | lineNum, err = strconv.Atoi(string(segs[1])) 73 | if err != nil { 74 | } 75 | col, err = strconv.Atoi(string(segs[2])) 76 | if err != nil { 77 | } 78 | 79 | } 80 | _ = SwitchOrOpenFileInWindow(c, c.Cfg, filepath.Join(c.getCWD(), string(filename)), &Position{Line: lineNum, Column: col}, targetWindow) 81 | 82 | c.ActiveWindowIndex = targetWindow.ID 83 | return 84 | } 85 | 86 | func NewGrepBuffer(parent *Context, cfg *Config, pattern string) (*BufferView, error) { 87 | cwd := parent.getCWD() 88 | bufferView := NewBufferViewFromFilename(parent, cfg, fmt.Sprintf("*Grep*@%s", cwd)) 89 | 90 | bufferView.Buffer.Readonly = true 91 | runCompileCommand := func() { 92 | bufferView.Buffer.Content = nil 93 | bufferView.Buffer.Content = append(bufferView.Buffer.Content, []byte(fmt.Sprintf("Pattern: %s\n", pattern))...) 94 | bufferView.Buffer.Content = append(bufferView.Buffer.Content, []byte(fmt.Sprintf("Dir: %s\n", cwd))...) 95 | go func() { 96 | cmd := exec.Command("rg", []string{"--vimgrep", pattern}...) 97 | cmd.Dir = cwd 98 | since := time.Now() 99 | output, err := cmd.CombinedOutput() 100 | if err != nil { 101 | bufferView.Buffer.Content = append(bufferView.Buffer.Content, []byte(err.Error())...) 102 | bufferView.Buffer.Content = append(bufferView.Buffer.Content, '\n') 103 | } 104 | if bytes.Contains(output, []byte("\r")) { 105 | output = bytes.Replace(output, []byte("\r"), []byte(""), -1) 106 | } 107 | bufferView.Buffer.Content = append(bufferView.Buffer.Content, output...) 108 | bufferView.Buffer.Content = append(bufferView.Buffer.Content, []byte(fmt.Sprintf("Done in %s\n", time.Since(since)))...) 109 | 110 | }() 111 | 112 | } 113 | 114 | thisKeymap := CompileKeymap.Clone() 115 | thisKeymap.BindKey(Key{K: "g"}, MakeCommand(func(b *BufferView) { 116 | runCompileCommand() 117 | })) 118 | 119 | bufferView.keymaps.Push(thisKeymap) 120 | 121 | runCompileCommand() 122 | return bufferView, nil 123 | } 124 | 125 | func NewCompilationBuffer(parent *Context, cfg *Config, command string) (*BufferView, error) { 126 | cwd := parent.getCWD() 127 | bufferView := NewBufferViewFromFilename(parent, cfg, fmt.Sprintf("*Compilation*@%s", cwd)) 128 | 129 | bufferView.Buffer.Readonly = true 130 | runCompileCommand := func() { 131 | bufferView.Buffer.Content = nil 132 | bufferView.Buffer.Content = append(bufferView.Buffer.Content, []byte(fmt.Sprintf("Command: %s\n", command))...) 133 | bufferView.Buffer.Content = append(bufferView.Buffer.Content, []byte(fmt.Sprintf("Dir: %s\n", cwd))...) 134 | go func() { 135 | segs := strings.Split(command, " ") 136 | var args []string 137 | bin := segs[0] 138 | if len(segs) > 1 { 139 | args = segs[1:] 140 | } 141 | cmd := exec.Command(bin, args...) 142 | cmd.Dir = cwd 143 | since := time.Now() 144 | output, err := cmd.CombinedOutput() 145 | if err != nil { 146 | bufferView.Buffer.Content = append(bufferView.Buffer.Content, []byte(err.Error())...) 147 | bufferView.Buffer.Content = append(bufferView.Buffer.Content, '\n') 148 | } 149 | if bytes.Contains(output, []byte("\r")) { 150 | output = bytes.Replace(output, []byte("\r"), []byte(""), -1) 151 | } 152 | bufferView.Buffer.Content = append(bufferView.Buffer.Content, output...) 153 | bufferView.Buffer.Content = append(bufferView.Buffer.Content, []byte(fmt.Sprintf("Done in %s\n", time.Since(since)))...) 154 | 155 | }() 156 | 157 | } 158 | 159 | thisKeymap := CompileKeymap.Clone() 160 | thisKeymap.BindKey(Key{K: "g"}, MakeCommand(func(b *BufferView) { 161 | runCompileCommand() 162 | })) 163 | 164 | bufferView.keymaps.Push(thisKeymap) 165 | 166 | runCompileCommand() 167 | return bufferView, nil 168 | } 169 | 170 | func NewBufferViewFromFilename(parent *Context, cfg *Config, filename string) *BufferView { 171 | buffer := parent.GetBufferByFilename(filename) 172 | if buffer == nil { 173 | buffer = parent.OpenFileAsBuffer(filename) 174 | } 175 | 176 | //check if we have a buffer view for this file that is not shown in any window 177 | OUTER: 178 | for _, d := range parent.Drawables { 179 | bufView, ok := d.(*BufferView) 180 | if !ok { 181 | continue 182 | } 183 | if bufView.Buffer == buffer { 184 | for _, col := range parent.Windows { 185 | for _, win := range col { 186 | if win.DrawableID == bufView.ID { 187 | continue OUTER 188 | } 189 | } 190 | } 191 | 192 | // bufView is not shown in any window so it's suitable for us to return it 193 | return bufView 194 | } 195 | 196 | } 197 | 198 | return NewBufferView(parent, cfg, buffer) 199 | } 200 | 201 | type FileType struct { 202 | Name string 203 | TSLanguage *sitter.Language 204 | TabSize int 205 | BeforeSave func(*BufferView) error 206 | AfterSave func(*BufferView) error 207 | DefaultCompileCommand string 208 | CommentLineBeginingChars []byte 209 | FindRootOfProject func(currentFilePath string) (string, error) 210 | TSHighlightQuery []byte 211 | } 212 | 213 | var FileTypes map[string]FileType 214 | 215 | func init() { 216 | FileTypes = map[string]FileType{ 217 | ".go": GoFileType, 218 | ".php": PHPFileType, 219 | } 220 | } 221 | 222 | func NewBufferView(parent *Context, cfg *Config, buffer *Buffer) *BufferView { 223 | t := BufferView{cfg: cfg} 224 | t.parent = parent 225 | t.Buffer = buffer 226 | t.LastCompileCommand = buffer.fileType.DefaultCompileCommand 227 | t.keymaps = NewStack[Keymap](5) 228 | t.keymaps.Push(BufferKeymap) 229 | t.ActionStack = NewStack[BufferAction](1000) 230 | t.Cursor = Cursor{Point: 0, Mark: 0} 231 | t.replaceTabsWithSpaces() 232 | return &t 233 | } 234 | 235 | const ( 236 | State_Clean = 0 237 | State_Dirty = 1 238 | ) 239 | 240 | type Cursor struct { 241 | Point int 242 | Mark int 243 | } 244 | 245 | func (r *Cursor) SetBoth(n int) { 246 | r.Point = n 247 | r.Mark = n 248 | } 249 | 250 | func (r *Cursor) AddToBoth(n int) { 251 | r.Point += n 252 | r.Mark += n 253 | } 254 | 255 | func (r *Cursor) AddToStart(n int) { 256 | if r.Point > r.Mark { 257 | r.Mark += n 258 | } else { 259 | r.Point += n 260 | } 261 | } 262 | func (r *Cursor) AddToEnd(n int) { 263 | if r.Point > r.Mark { 264 | r.Point += n 265 | } else { 266 | r.Mark += n 267 | } 268 | } 269 | func (r *Cursor) Start() int { 270 | if r.Point > r.Mark { 271 | return r.Mark 272 | } else { 273 | return r.Point 274 | } 275 | } 276 | func (r *Cursor) End() int { 277 | if r.Point > r.Mark { 278 | return r.Point 279 | } else { 280 | return r.Mark 281 | } 282 | } 283 | 284 | type Search struct { 285 | IsSearching bool 286 | LastSearchString string 287 | SearchString string 288 | SearchMatches [][]int 289 | CurrentMatch int 290 | MovedAwayFromCurrentMatch bool 291 | } 292 | 293 | type Buffer struct { 294 | File string 295 | Content []byte 296 | CRLF bool 297 | State int 298 | Readonly bool 299 | 300 | oldTSTree *sitter.Tree 301 | highlights []highlight 302 | needParsing bool 303 | fileType FileType 304 | } 305 | 306 | type QueryReplace struct { 307 | IsQueryReplace bool 308 | SearchString string 309 | ReplaceString string 310 | SearchMatches [][]int 311 | CurrentMatch int 312 | MovedAwayFromCurrentMatch bool 313 | } 314 | 315 | type BufferView struct { 316 | BaseDrawable 317 | Buffer *Buffer 318 | cfg *Config 319 | parent *Context 320 | maxLine int32 321 | maxColumn int32 322 | NoStatusbar bool 323 | zeroLocation rl.Vector2 324 | textZeroLocation rl.Vector2 325 | bufferLines []BufferLine 326 | VisibleStart int32 327 | MoveToPositionInNextRender *Position 328 | OldBufferContentLen int 329 | 330 | keymaps *Stack[Keymap] 331 | 332 | // Cursor 333 | Cursor Cursor 334 | lastCursorTime time.Time 335 | showCursors bool 336 | 337 | // Searching 338 | Search Search 339 | 340 | QueryReplace QueryReplace 341 | 342 | LastCompileCommand string 343 | 344 | ActionStack *Stack[BufferAction] 345 | } 346 | 347 | const ( 348 | BufferActionType_Insert = iota + 1 349 | BufferActionType_Delete 350 | ) 351 | 352 | type BufferAction struct { 353 | Type int 354 | Idx int 355 | Data []byte 356 | } 357 | 358 | func (e *BufferView) String() string { 359 | return fmt.Sprintf("%s:%d", e.Buffer.File, e.Cursor.Point) 360 | } 361 | 362 | func (e *BufferView) Keymaps() []Keymap { 363 | return e.keymaps.data 364 | } 365 | 366 | func (e *BufferView) IsSpecial() bool { 367 | return e.Buffer.File == "" || e.Buffer.File[0] == '*' 368 | } 369 | 370 | func (e *BufferView) AddBufferAction(a BufferAction) { 371 | a.Data = bytes.Clone(a.Data) 372 | e.ActionStack.Push(a) 373 | } 374 | func (e *BufferView) SetStateDirty() { 375 | e.Buffer.State = State_Dirty 376 | e.Buffer.needParsing = true 377 | } 378 | 379 | func (e *BufferView) SetStateClean() { 380 | e.Buffer.State = State_Clean 381 | e.Buffer.needParsing = true 382 | } 383 | 384 | func (e *BufferView) replaceTabsWithSpaces() { 385 | tabSize := e.Buffer.fileType.TabSize 386 | if e.Buffer.fileType.TabSize == 0 { 387 | tabSize = 4 388 | } 389 | e.Buffer.Content = bytes.Replace(e.Buffer.Content, []byte("\t"), []byte(strings.Repeat(" ", tabSize)), -1) 390 | } 391 | 392 | func (e *BufferView) getBufferLineForIndex(i int) BufferLine { 393 | for _, line := range e.bufferLines { 394 | if line.startIndex <= i && line.endIndex >= i { 395 | return line 396 | } 397 | } 398 | 399 | if len(e.bufferLines) > 0 { 400 | lastLine := e.bufferLines[len(e.bufferLines)-1] 401 | lastLine.endIndex++ 402 | return lastLine 403 | } 404 | return BufferLine{} 405 | } 406 | 407 | func (e *BufferView) BufferIndexToPosition(i int) Position { 408 | if len(e.bufferLines) == 0 { 409 | return Position{Line: 0, Column: i} 410 | } 411 | 412 | line := e.getBufferLineForIndex(i) 413 | return Position{ 414 | Line: line.Index, 415 | Column: i - line.startIndex, 416 | } 417 | 418 | } 419 | 420 | func (e *BufferView) PositionToBufferIndex(pos Position) int { 421 | if len(e.bufferLines) <= pos.Line { 422 | return len(e.Buffer.Content) 423 | } 424 | 425 | return e.bufferLines[pos.Line].startIndex + pos.Column 426 | } 427 | 428 | func (e *BufferView) Destroy() error { 429 | return nil 430 | } 431 | 432 | type highlight struct { 433 | start int 434 | end int 435 | Color color.RGBA 436 | } 437 | 438 | type BufferLine struct { 439 | Index int 440 | startIndex int 441 | endIndex int 442 | ActualLine int 443 | Length int 444 | } 445 | 446 | func sortme[T any](slice []T, pred func(t1 T, t2 T) bool) { 447 | sort.Slice(slice, func(i, j int) bool { 448 | return pred(slice[i], slice[j]) 449 | }) 450 | } 451 | 452 | func (e *BufferView) moveCursorTo(pos rl.Vector2) error { 453 | if len(e.bufferLines) < 1 { 454 | return nil 455 | } 456 | 457 | charSize := measureTextSize(e.parent.Font, ' ', e.parent.FontSize, 0) 458 | apprLine := math.Floor(float64((pos.Y - e.textZeroLocation.Y) / charSize.Y)) 459 | apprColumn := math.Floor(float64((pos.X - e.textZeroLocation.X) / charSize.X)) 460 | 461 | line := int(apprLine) + int(e.VisibleStart) 462 | col := int(apprColumn) 463 | if line >= len(e.bufferLines) { 464 | line = len(e.bufferLines) - 1 465 | } 466 | 467 | col -= e.getLineNumbersMaxLength() 468 | 469 | if line < 0 { 470 | line = 0 471 | } 472 | 473 | // check if cursor should be moved back 474 | if col > e.bufferLines[line].Length { 475 | col = e.bufferLines[line].Length 476 | } 477 | if col < 0 { 478 | col = 0 479 | } 480 | 481 | e.Cursor.SetBoth(e.PositionToBufferIndex(Position{Line: line, Column: col})) 482 | 483 | return nil 484 | } 485 | 486 | func (e *BufferView) VisibleEnd() int32 { 487 | return e.VisibleStart + e.maxLine 488 | } 489 | 490 | func (e *BufferView) renderTextRange(zeroLocation rl.Vector2, idx1 int, idx2 int, maxH float64, maxW float64, color color.RGBA) { 491 | charSize := measureTextSize(e.parent.Font, ' ', e.parent.FontSize, 0) 492 | var start Position 493 | var end Position 494 | if idx1 > idx2 { 495 | start = e.BufferIndexToPosition(idx2) 496 | end = e.BufferIndexToPosition(idx1) 497 | } else if idx2 > idx1 { 498 | start = e.BufferIndexToPosition(idx1) 499 | end = e.BufferIndexToPosition(idx2) 500 | } 501 | for i := start.Line; i <= end.Line; i++ { 502 | if len(e.bufferLines) <= i { 503 | break 504 | } 505 | var thisLineEnd int 506 | var thisLineStart int 507 | line := e.bufferLines[i] 508 | if i == start.Line { 509 | thisLineStart = start.Column 510 | } else { 511 | thisLineStart = 0 512 | } 513 | 514 | if i < int(e.VisibleStart) { 515 | break 516 | } 517 | 518 | if i < end.Line { 519 | thisLineEnd = line.Length - 1 520 | } else { 521 | thisLineEnd = end.Column 522 | } 523 | posX := int32(thisLineStart)*int32(charSize.X) + int32(zeroLocation.X) 524 | if e.cfg.LineNumbers { 525 | posX += int32(e.getLineNumbersMaxLength()) * int32(charSize.X) 526 | } 527 | posY := int32(i-int(e.VisibleStart))*int32(charSize.Y) + int32(zeroLocation.Y) 528 | rl.DrawTextEx(e.parent.Font, 529 | string(e.Buffer.Content[thisLineStart+line.startIndex:thisLineEnd+line.startIndex]), 530 | rl.Vector2{ 531 | X: float32(posX), Y: float32(posY), 532 | }, float32(e.parent.FontSize), 0, color) 533 | } 534 | } 535 | 536 | func (e *BufferView) AddBytesAtIndex(data []byte, idx int, addBufferAction bool) { 537 | if idx >= len(e.Buffer.Content) { 538 | e.Buffer.Content = append(e.Buffer.Content, data...) 539 | } else { 540 | e.Buffer.Content = append(e.Buffer.Content[:idx], append(data, e.Buffer.Content[idx:]...)...) 541 | } 542 | if addBufferAction { 543 | e.AddBufferAction(BufferAction{ 544 | Type: BufferActionType_Insert, 545 | Idx: idx, 546 | Data: data, 547 | }) 548 | } 549 | } 550 | 551 | func (e *BufferView) RemoveRange(start int, end int, addBufferAction bool) { 552 | if start < 0 { 553 | start = 0 554 | } 555 | if end >= len(e.Buffer.Content) { 556 | end = len(e.Buffer.Content) 557 | } 558 | rangeData := bytes.Clone(e.Buffer.Content[start:end]) 559 | if len(e.Buffer.Content) <= end { 560 | e.Buffer.Content = e.Buffer.Content[:start] 561 | } else { 562 | e.Buffer.Content = append(e.Buffer.Content[:start], e.Buffer.Content[end:]...) 563 | } 564 | if addBufferAction { 565 | e.AddBufferAction(BufferAction{ 566 | Type: BufferActionType_Delete, 567 | Idx: start, 568 | Data: rangeData, 569 | }) 570 | } 571 | 572 | } 573 | 574 | func (e *BufferView) getLineNumbersMaxLength() int { 575 | return len(fmt.Sprint(len(e.bufferLines))) + 1 576 | } 577 | 578 | func BufferGetCurrentLine(e *BufferView) []byte { 579 | pos := e.BufferIndexToPosition(e.Cursor.Point) 580 | 581 | if pos.Line < len(e.bufferLines) { 582 | line := e.bufferLines[pos.Line] 583 | return e.Buffer.Content[line.startIndex:line.endIndex] 584 | } 585 | 586 | return nil 587 | } 588 | 589 | func (e *BufferView) highlightBetweenTwoIndexes(zeroLocation rl.Vector2, idx1 int, idx2 int, maxH float64, maxW float64, bg color.RGBA, fg color.RGBA) { 590 | charSize := measureTextSize(e.parent.Font, ' ', e.parent.FontSize, 0) 591 | var start Position 592 | var end Position 593 | if idx1 > idx2 { 594 | start = e.BufferIndexToPosition(idx2) 595 | end = e.BufferIndexToPosition(idx1) 596 | } else if idx2 > idx1 { 597 | start = e.BufferIndexToPosition(idx1) 598 | end = e.BufferIndexToPosition(idx2) 599 | } 600 | for i := start.Line; i <= end.Line; i++ { 601 | if len(e.bufferLines) <= i { 602 | break 603 | } 604 | var thisLineEnd int 605 | var thisLineStart int 606 | line := e.bufferLines[i] 607 | if i == start.Line { 608 | thisLineStart = start.Column 609 | } else { 610 | thisLineStart = 0 611 | } 612 | 613 | if i < end.Line { 614 | thisLineEnd = line.Length - 1 615 | } else { 616 | thisLineEnd = end.Column 617 | } 618 | posX := int32(thisLineStart)*int32(charSize.X) + int32(zeroLocation.X) 619 | if e.cfg.LineNumbers { 620 | posX += int32(e.getLineNumbersMaxLength()) * int32(charSize.X) 621 | } 622 | 623 | posY := int32(i-int(e.VisibleStart))*int32(charSize.Y) + int32(zeroLocation.Y) 624 | if !isVisibleInWindow(float64(posX), float64(posY), zeroLocation, maxH, maxW) { 625 | continue 626 | } 627 | rl.DrawTextEx(e.parent.Font, string(e.Buffer.Content[thisLineStart+line.startIndex:thisLineEnd+line.startIndex+1]), 628 | rl.Vector2{X: float32(posX), Y: float32(posY)}, float32(e.parent.FontSize), 0, fg) 629 | 630 | for j := thisLineStart; j <= thisLineEnd; j++ { 631 | posX := int32(j)*int32(charSize.X) + int32(zeroLocation.X) 632 | if e.cfg.LineNumbers { 633 | posX += int32(e.getLineNumbersMaxLength()) * int32(charSize.X) 634 | } 635 | posY := int32(i-int(e.VisibleStart))*int32(charSize.Y) + int32(zeroLocation.Y) 636 | if !isVisibleInWindow(float64(posX), float64(posY), zeroLocation, maxH, maxW) { 637 | continue 638 | } 639 | rl.DrawRectangle(posX, posY, int32(charSize.X), int32(charSize.Y), rl.Fade(bg, 0.5)) 640 | } 641 | } 642 | 643 | } 644 | 645 | func (e *BufferView) convertBufferIndexToLineAndColumn(idx int) *Position { 646 | for lineIndex, line := range e.bufferLines { 647 | if line.startIndex <= idx && line.endIndex >= idx { 648 | return &Position{ 649 | Line: lineIndex, 650 | Column: idx - line.startIndex, 651 | } 652 | } 653 | } 654 | 655 | return nil 656 | } 657 | 658 | func matchPatternCaseInsensitive(data []byte, pattern []byte) [][]int { 659 | if len(data) == 0 || len(pattern) == 0 { 660 | return nil 661 | } 662 | var matched [][]int 663 | var buf []byte 664 | start := -1 665 | for i, b := range data { 666 | 667 | if len(pattern) == len(buf) { 668 | matched = append(matched, []int{start, i - 1}) 669 | buf = nil 670 | start = -1 671 | } 672 | idxToCheck := len(buf) 673 | if idxToCheck == 0 { 674 | start = i 675 | } 676 | if unicode.ToLower(rune(pattern[idxToCheck])) == unicode.ToLower(rune(b)) { 677 | buf = append(buf, b) 678 | } else { 679 | buf = nil 680 | start = -1 681 | } 682 | } 683 | 684 | return matched 685 | } 686 | 687 | func findNextMatch(data []byte, idx int, pattern []byte) []int { 688 | var buf []byte 689 | start := -1 690 | for i := idx; i < len(data); i++ { 691 | if len(pattern) == len(buf) { 692 | return []int{start, i - 1} 693 | } 694 | idxToCheck := len(buf) 695 | if idxToCheck == 0 { 696 | start = i 697 | } 698 | if unicode.ToLower(rune(pattern[idxToCheck])) == unicode.ToLower(rune(data[i])) { 699 | buf = append(buf, data[i]) 700 | } else { 701 | buf = nil 702 | start = -1 703 | } 704 | } 705 | 706 | return nil 707 | } 708 | 709 | func matchPatternAsync(dst *[][]int, data []byte, pattern []byte) { 710 | go func() { 711 | *dst = matchPatternCaseInsensitive(data, pattern) 712 | }() 713 | } 714 | 715 | func TSHighlights(fileType *FileType, cfg *Config, queryString []byte, prev *sitter.Tree, code []byte) ([]highlight, *sitter.Tree, error) { 716 | var highlights []highlight 717 | parser := sitter.NewParser() 718 | if fileType.TSLanguage == nil { 719 | return nil, nil, nil 720 | } 721 | parser.SetLanguage(fileType.TSLanguage) 722 | 723 | tree, err := parser.ParseCtx(context.Background(), prev, code) 724 | if err != nil { 725 | return nil, nil, err 726 | } 727 | 728 | query, err := sitter.NewQuery(queryString, golang.GetLanguage()) 729 | if err != nil { 730 | return nil, tree, err 731 | } 732 | 733 | qc := sitter.NewQueryCursor() 734 | qc.Exec(query, tree.RootNode()) 735 | for { 736 | qm, exists := qc.NextMatch() 737 | if !exists { 738 | break 739 | } 740 | for _, capture := range qm.Captures { 741 | captureName := query.CaptureNameForId(capture.Index) 742 | if c, exists := cfg.CurrentThemeColors().SyntaxColors[captureName]; exists { 743 | highlights = append(highlights, highlight{ 744 | start: int(capture.Node.StartByte()), 745 | end: int(capture.Node.EndByte()), 746 | Color: c.ToColorRGBA(), 747 | }) 748 | } 749 | } 750 | } 751 | 752 | return highlights, tree, nil 753 | } 754 | 755 | func safeSlice[T any](s []T, start int, end int) []T { 756 | if len(s) == 0 { 757 | return nil 758 | } 759 | if start < 0 { 760 | start = 0 761 | } 762 | if end >= len(s) { 763 | end = len(s) - 1 764 | } 765 | 766 | return s[start:end] 767 | } 768 | 769 | type bufferRenderContext struct { 770 | textZeroLocation rl.Vector2 771 | zeroLocation rl.Vector2 772 | } 773 | 774 | func (e *BufferView) calcRenderState() { 775 | e.bufferLines = []BufferLine{} 776 | totalVisualLines := 0 777 | lineCharCounter := 0 778 | var actualLineIndex = 1 779 | var start int 780 | for idx, char := range e.Buffer.Content { 781 | lineCharCounter++ 782 | if char == '\n' { 783 | line := BufferLine{ 784 | Index: totalVisualLines, 785 | startIndex: start, 786 | endIndex: idx, 787 | Length: idx - start + 1, 788 | ActualLine: actualLineIndex, 789 | } 790 | e.bufferLines = append(e.bufferLines, line) 791 | totalVisualLines++ 792 | actualLineIndex++ 793 | lineCharCounter = 0 794 | start = idx + 1 795 | } 796 | if idx == len(e.Buffer.Content)-1 { 797 | // last index 798 | line := BufferLine{ 799 | Index: totalVisualLines, 800 | startIndex: start, 801 | endIndex: idx + 1, 802 | Length: idx - start + 1, 803 | ActualLine: actualLineIndex, 804 | } 805 | e.bufferLines = append(e.bufferLines, line) 806 | totalVisualLines++ 807 | actualLineIndex++ 808 | lineCharCounter = 0 809 | start = idx + 1 810 | } 811 | if e.maxColumn != 0 && int32(lineCharCounter) > e.maxColumn-5 { 812 | line := BufferLine{ 813 | Index: totalVisualLines, 814 | startIndex: start, 815 | endIndex: idx, 816 | Length: idx - start + 1, 817 | ActualLine: actualLineIndex, 818 | } 819 | e.bufferLines = append(e.bufferLines, line) 820 | totalVisualLines++ 821 | lineCharCounter = 0 822 | start = idx + 1 823 | } 824 | } 825 | 826 | } 827 | 828 | func (e *BufferView) Render(zeroLocation rl.Vector2, maxH float64, maxW float64) { 829 | oldMaxLine := e.maxLine 830 | oldMaxColumn := e.maxColumn 831 | oldBufferContentLen := e.OldBufferContentLen 832 | charSize := measureTextSize(e.parent.Font, ' ', e.parent.FontSize, 0) 833 | e.maxColumn = int32(maxW / float64(charSize.X)) 834 | e.maxLine = int32(maxH / float64(charSize.Y)) 835 | e.maxLine-- //reserve one line of screen for statusbar 836 | textZeroLocation := zeroLocation 837 | if e.Search.IsSearching || e.QueryReplace.IsQueryReplace { 838 | textZeroLocation.Y += charSize.Y 839 | } 840 | if e.Buffer.needParsing || len(e.Buffer.Content) != oldBufferContentLen || (e.maxLine != oldMaxLine) || (e.maxColumn != oldMaxColumn) { 841 | e.calcRenderState() 842 | } 843 | e.OldBufferContentLen = len(e.Buffer.Content) 844 | if !e.NoStatusbar && !e.parent.GlobalNoStatusbar { 845 | var sections []string 846 | 847 | file := e.Buffer.File 848 | 849 | var state string 850 | if e.Buffer.State == State_Dirty { 851 | state = "U" 852 | } else { 853 | state = "" 854 | } 855 | sections = append(sections, fmt.Sprintf("%s %s", state, file)) 856 | 857 | if e.Cursor.Start() == e.Cursor.End() { 858 | selStart := e.BufferIndexToPosition(e.Cursor.Start()) 859 | if len(e.bufferLines) > selStart.Line { 860 | selLine := e.bufferLines[selStart.Line] 861 | sections = append(sections, fmt.Sprintf("L#%d C#%d", selLine.ActualLine, selStart.Column)) 862 | } else { 863 | sections = append(sections, fmt.Sprintf("L#%d C#%d", selStart.Line, selStart.Column)) 864 | } 865 | 866 | } else { 867 | selEnd := e.BufferIndexToPosition(e.Cursor.End()) 868 | sections = append(sections, fmt.Sprintf("L#%d C#%d (Selected %d)", selEnd.Line, selEnd.Column, int(math.Abs(float64(e.Cursor.Start()-e.Cursor.End()))))) 869 | } 870 | 871 | if e.Search.IsSearching { 872 | sections = append(sections, fmt.Sprintf("Search: Match#%d Of %d", e.Search.CurrentMatch+1, len(e.Search.SearchMatches)+1)) 873 | } 874 | 875 | bg := e.cfg.CurrentThemeColors().StatusBarBackground.ToColorRGBA() 876 | fg := e.cfg.CurrentThemeColors().StatusBarForeground.ToColorRGBA() 877 | if win := e.parent.ActiveWindow(); win != nil && win.DrawableID == e.ID { 878 | bg = e.cfg.CurrentThemeColors().ActiveStatusBarBackground.ToColorRGBA() 879 | fg = e.cfg.CurrentThemeColors().ActiveStatusBarForeground.ToColorRGBA() 880 | } 881 | rl.DrawRectangle( 882 | int32(zeroLocation.X), 883 | int32(zeroLocation.Y), 884 | int32(maxW), 885 | int32(charSize.Y), 886 | bg, 887 | ) 888 | rl.DrawRectangleLines(int32(zeroLocation.X), 889 | int32(zeroLocation.Y), 890 | int32(maxW), 891 | int32(charSize.Y), e.cfg.CurrentThemeColors().Foreground.ToColorRGBA()) 892 | 893 | rl.DrawTextEx(e.parent.Font, 894 | strings.Join(sections, " "), 895 | rl.Vector2{X: zeroLocation.X, Y: float32(zeroLocation.Y)}, 896 | float32(e.parent.FontSize), 897 | 0, 898 | fg) 899 | 900 | textZeroLocation.Y += measureTextSize(e.parent.Font, ' ', e.parent.FontSize, 0).Y 901 | zeroLocation.Y += measureTextSize(e.parent.Font, ' ', e.parent.FontSize, 0).Y 902 | 903 | } 904 | 905 | if e.MoveToPositionInNextRender != nil { 906 | var bufferIndex int 907 | for _, line := range e.bufferLines { 908 | if line.ActualLine == e.MoveToPositionInNextRender.Line { 909 | bufferIndex = line.startIndex + e.MoveToPositionInNextRender.Column 910 | e.VisibleStart = int32(e.MoveToPositionInNextRender.Line) - e.maxLine/2 911 | } 912 | } 913 | e.Cursor.SetBoth(bufferIndex) 914 | e.MoveToPositionInNextRender = nil 915 | } 916 | if e.Buffer.needParsing { 917 | var err error 918 | e.Buffer.highlights, e.Buffer.oldTSTree, err = TSHighlights(&e.Buffer.fileType, e.cfg, e.Buffer.fileType.TSHighlightQuery, nil, e.Buffer.Content) //TODO: see how we can use old tree 919 | if err != nil { 920 | panic(err) 921 | } 922 | e.Buffer.needParsing = false 923 | } 924 | 925 | if e.Search.IsSearching { 926 | for idx, match := range e.Search.SearchMatches { 927 | if idx == e.Search.CurrentMatch { 928 | matchStartLine := e.BufferIndexToPosition(match[0]) 929 | matchEndLine := e.BufferIndexToPosition(match[0]) 930 | if !(e.VisibleStart < int32(matchStartLine.Line) && e.VisibleEnd() > int32(matchEndLine.Line)) && !e.Search.MovedAwayFromCurrentMatch { 931 | // current match is not in view 932 | // move the view 933 | e.VisibleStart = int32(matchStartLine.Line) - e.maxLine/2 934 | if e.VisibleStart < 0 { 935 | e.VisibleStart = int32(matchStartLine.Line) 936 | } 937 | 938 | } 939 | } 940 | e.highlightBetweenTwoIndexes(textZeroLocation, match[0], match[1], maxH, maxW, e.cfg.CurrentThemeColors().SelectionBackground.ToColorRGBA(), e.cfg.CurrentThemeColors().SelectionForeground.ToColorRGBA()) 941 | } 942 | e.Search.LastSearchString = e.Search.SearchString 943 | if len(e.Search.SearchMatches) > 0 { 944 | e.Cursor.Point = e.Search.SearchMatches[e.Search.CurrentMatch][0] 945 | e.Cursor.Mark = e.Search.SearchMatches[e.Search.CurrentMatch][0] 946 | } 947 | 948 | rl.DrawRectangle(int32(zeroLocation.X), int32(zeroLocation.Y), int32(maxW), int32(charSize.Y), e.cfg.CurrentThemeColors().Prompts.ToColorRGBA()) 949 | rl.DrawTextEx(e.parent.Font, fmt.Sprintf("Search: %s", e.Search.SearchString), rl.Vector2{ 950 | X: zeroLocation.X, 951 | Y: zeroLocation.Y, 952 | }, float32(e.parent.FontSize), 0, rl.White) 953 | 954 | } 955 | 956 | //QueryReplace 957 | if e.QueryReplace.IsQueryReplace { 958 | for idx, match := range e.QueryReplace.SearchMatches { 959 | if idx == e.QueryReplace.CurrentMatch { 960 | matchStartLine := e.BufferIndexToPosition(match[0]) 961 | matchEndLine := e.BufferIndexToPosition(match[0]) 962 | if !(e.VisibleStart < int32(matchStartLine.Line) && e.VisibleEnd() > int32(matchEndLine.Line)) && !e.Search.MovedAwayFromCurrentMatch { 963 | // current match is not in view 964 | // move the view 965 | e.VisibleStart = int32(matchStartLine.Line) - e.maxLine/2 966 | if e.VisibleStart < 0 { 967 | e.VisibleStart = int32(matchStartLine.Line) 968 | } 969 | 970 | } 971 | } 972 | e.highlightBetweenTwoIndexes(textZeroLocation, match[0], match[1], maxH, maxW, e.cfg.CurrentThemeColors().SelectionBackground.ToColorRGBA(), e.cfg.CurrentThemeColors().SelectionForeground.ToColorRGBA()) 973 | } 974 | if len(e.QueryReplace.SearchMatches) > 0 { 975 | e.Cursor.Point = e.QueryReplace.SearchMatches[e.QueryReplace.CurrentMatch][0] 976 | e.Cursor.Mark = e.QueryReplace.SearchMatches[e.QueryReplace.CurrentMatch][0] 977 | } 978 | 979 | rl.DrawRectangle(int32(zeroLocation.X), int32(zeroLocation.Y), int32(maxW), int32(charSize.Y), e.cfg.CurrentThemeColors().Prompts.ToColorRGBA()) 980 | rl.DrawTextEx(e.parent.Font, fmt.Sprintf("QueryReplace: %s -> %s", e.QueryReplace.SearchString, e.QueryReplace.ReplaceString), rl.Vector2{ 981 | X: zeroLocation.X, 982 | Y: zeroLocation.Y, 983 | }, float32(e.parent.FontSize), 0, rl.White) 984 | 985 | } 986 | 987 | var visibleLines []BufferLine 988 | if e.VisibleStart < 0 { 989 | e.VisibleStart = 0 990 | } 991 | 992 | if e.VisibleEnd() > int32(len(e.bufferLines)) { 993 | visibleLines = e.bufferLines[e.VisibleStart:] 994 | } else { 995 | visibleLines = e.bufferLines[e.VisibleStart:e.VisibleEnd()] 996 | } 997 | 998 | //TODO: @Perf we should check and re render if view has changed or buffer lines has changed 999 | for idx, line := range visibleLines { 1000 | if e.cfg.LineNumbers { 1001 | rl.DrawTextEx(e.parent.Font, 1002 | fmt.Sprintf("%d", line.ActualLine), 1003 | rl.Vector2{X: textZeroLocation.X, Y: textZeroLocation.Y + float32(idx)*charSize.Y}, 1004 | float32(e.parent.FontSize), 1005 | 0, 1006 | e.cfg.CurrentThemeColors().LineNumbersForeground.ToColorRGBA()) 1007 | } 1008 | e.renderTextRange(textZeroLocation, line.startIndex, line.endIndex, maxH, maxW, e.cfg.CurrentThemeColors().Foreground.ToColorRGBA()) 1009 | } 1010 | if e.cfg.EnableSyntaxHighlighting { 1011 | if len(e.bufferLines) > 0 { 1012 | visibleStartChar := e.bufferLines[e.VisibleStart].startIndex 1013 | 1014 | var visibleEndChar int 1015 | if len(e.bufferLines) > int(e.VisibleEnd()) { 1016 | visibleEndChar = e.bufferLines[e.VisibleEnd()].endIndex 1017 | } else { 1018 | visibleEndChar = len(e.Buffer.Content) 1019 | } 1020 | 1021 | for _, h := range e.Buffer.highlights { 1022 | if visibleStartChar <= h.start && visibleEndChar >= h.end { 1023 | e.renderTextRange(textZeroLocation, h.start, h.end, maxH, maxW, h.Color) 1024 | } 1025 | } 1026 | } 1027 | } 1028 | 1029 | // render cursors 1030 | cursorBlinkDeltaTime := time.Since(e.lastCursorTime) 1031 | if cursorBlinkDeltaTime > time.Millisecond*800 { 1032 | e.showCursors = !e.showCursors 1033 | e.lastCursorTime = time.Now() 1034 | } 1035 | 1036 | if !e.cfg.CursorBlinking { 1037 | e.showCursors = true 1038 | } 1039 | 1040 | if e.parent.ActiveDrawableID() == e.ID { 1041 | if e.Cursor.Start() == e.Cursor.End() { 1042 | cursor := e.BufferIndexToPosition(e.Cursor.Start()) 1043 | cursorView := Position{ 1044 | Line: cursor.Line - int(e.VisibleStart), 1045 | Column: cursor.Column, 1046 | } 1047 | posX := int32(cursorView.Column)*int32(charSize.X) + int32(textZeroLocation.X) 1048 | if e.cfg.LineNumbers { 1049 | posX += int32(e.getLineNumbersMaxLength()) * int32(charSize.X) 1050 | } 1051 | posY := int32(cursorView.Line)*int32(charSize.Y) + int32(textZeroLocation.Y) 1052 | 1053 | // if !isVisibleInWindow(float64(posX), float64(posY), zeroLocation, maxH, maxW) { 1054 | // continue 1055 | // } 1056 | if e.showCursors { 1057 | switch e.cfg.CursorShape { 1058 | case CURSOR_SHAPE_OUTLINE: 1059 | rl.DrawRectangleLines(posX, posY, int32(charSize.X), int32(charSize.Y), e.cfg.CurrentThemeColors().Cursor.ToColorRGBA()) 1060 | case CURSOR_SHAPE_BLOCK: 1061 | rl.DrawRectangle(posX, posY, int32(charSize.X), int32(charSize.Y), e.cfg.CurrentThemeColors().Cursor.ToColorRGBA()) 1062 | if len(e.Buffer.Content)-1 >= e.Cursor.Point { 1063 | rl.DrawTextEx(e.parent.Font, string(e.Buffer.Content[e.Cursor.Point]), rl.Vector2{X: float32(posX), Y: float32(posY)}, float32(e.parent.FontSize), 0, e.cfg.CurrentThemeColors().Background.ToColorRGBA()) 1064 | } 1065 | 1066 | case CURSOR_SHAPE_LINE: 1067 | rl.DrawRectangleLines(posX, posY, 2, int32(charSize.Y), e.cfg.CurrentThemeColors().Cursor.ToColorRGBA()) 1068 | } 1069 | } 1070 | if e.cfg.CursorLineHighlight { 1071 | rl.DrawRectangle(int32(textZeroLocation.X), int32(cursorView.Line)*int32(charSize.Y)+int32(textZeroLocation.Y), e.maxColumn*int32(charSize.X), int32(charSize.Y), rl.Fade(e.cfg.CurrentThemeColors().CursorLineBackground.ToColorRGBA(), 0.15)) 1072 | } 1073 | 1074 | // highlight matching char 1075 | if e.cfg.HighlightMatchingParen { 1076 | matchingIdx := byteutils.FindMatching(e.Buffer.Content, e.Cursor.Point) 1077 | if matchingIdx != -1 { 1078 | idxPosition := e.BufferIndexToPosition(matchingIdx) 1079 | idxPositionView := Position{ 1080 | Line: idxPosition.Line - int(e.VisibleStart), 1081 | Column: idxPosition.Column, 1082 | } 1083 | posX := int32(idxPositionView.Column)*int32(charSize.X) + int32(textZeroLocation.X) 1084 | if e.cfg.LineNumbers { 1085 | posX += int32(e.getLineNumbersMaxLength()) * int32(charSize.X) 1086 | } 1087 | posY := int32(idxPositionView.Line)*int32(charSize.Y) + int32(textZeroLocation.Y) 1088 | 1089 | rl.DrawRectangle(posX, posY, int32(charSize.X), int32(charSize.Y), rl.Fade(e.cfg.CurrentThemeColors().HighlightMatching.ToColorRGBA(), 0.4)) 1090 | } 1091 | } 1092 | 1093 | } else { 1094 | e.highlightBetweenTwoIndexes(textZeroLocation, e.Cursor.Start(), e.Cursor.End(), maxH, maxW, e.cfg.CurrentThemeColors().SelectionBackground.ToColorRGBA(), e.cfg.CurrentThemeColors().SelectionForeground.ToColorRGBA()) 1095 | } 1096 | } 1097 | 1098 | e.zeroLocation = zeroLocation 1099 | e.textZeroLocation = textZeroLocation 1100 | } 1101 | 1102 | func (e *BufferView) isValidCursorPosition(newPosition Position) bool { 1103 | if newPosition.Line < 0 { 1104 | return false 1105 | } 1106 | if len(e.bufferLines) == 0 && newPosition.Line == 0 && newPosition.Column >= 0 && int32(newPosition.Column) < e.maxColumn-int32(len(fmt.Sprint(newPosition.Line)))-1 { 1107 | return true 1108 | } 1109 | if newPosition.Line >= len(e.bufferLines) && (len(e.bufferLines) != 0) { 1110 | return false 1111 | } 1112 | 1113 | if newPosition.Column < 0 { 1114 | return false 1115 | } 1116 | if newPosition.Column > e.bufferLines[newPosition.Line].Length+1 { 1117 | return false 1118 | } 1119 | 1120 | return true 1121 | } 1122 | 1123 | func (e *BufferView) deleteSelectionsIfAnySelection() { 1124 | if e.Buffer.Readonly { 1125 | return 1126 | } 1127 | old := len(e.Buffer.Content) 1128 | if e.Cursor.Start() == e.Cursor.End() { 1129 | return 1130 | } 1131 | e.AddBufferAction(BufferAction{ 1132 | Type: BufferActionType_Delete, 1133 | Idx: e.Cursor.Start(), 1134 | Data: e.Buffer.Content[e.Cursor.Start() : e.Cursor.End()+1], 1135 | }) 1136 | e.Buffer.Content = append(e.Buffer.Content[:e.Cursor.Start()], e.Buffer.Content[e.Cursor.End()+1:]...) 1137 | e.Cursor.Point = e.Cursor.Mark 1138 | PointLeft(e, old-1-len(e.Buffer.Content)) 1139 | } 1140 | 1141 | func (e *BufferView) ScrollIfNeeded() { 1142 | pos := e.BufferIndexToPosition(e.Cursor.End()) 1143 | if int32(pos.Line) <= e.VisibleStart { 1144 | e.VisibleStart = int32(pos.Line) - e.maxLine/3 1145 | 1146 | } 1147 | 1148 | if pos.Line > int(e.VisibleEnd()) { 1149 | e.VisibleStart += e.maxLine / 3 1150 | } 1151 | 1152 | if int(e.VisibleEnd()) >= len(e.bufferLines) { 1153 | e.VisibleStart = int32(len(e.bufferLines)-1) - e.maxLine 1154 | } 1155 | 1156 | if e.VisibleStart < 0 { 1157 | e.VisibleStart = 0 1158 | } 1159 | } 1160 | 1161 | func (e *BufferView) readFileFromDisk() error { 1162 | bs, err := os.ReadFile(e.Buffer.File) 1163 | if err != nil { 1164 | return nil 1165 | } 1166 | 1167 | //replace CRLF with LF 1168 | if bytes.Index(bs, []byte("\r\n")) != -1 { 1169 | bs = bytes.Replace(bs, []byte("\r"), []byte(""), -1) 1170 | e.Buffer.CRLF = true 1171 | } 1172 | e.Buffer.Content = bs 1173 | e.replaceTabsWithSpaces() 1174 | e.SetStateClean() 1175 | return nil 1176 | } 1177 | 1178 | // Things that change buffer content 1179 | 1180 | func BufferInsertChar(e *BufferView, char byte) { 1181 | if e.Buffer.Readonly { 1182 | return 1183 | } 1184 | e.deleteSelectionsIfAnySelection() 1185 | e.AddBytesAtIndex([]byte{char}, e.Cursor.Point, true) 1186 | PointRight(e, 1) 1187 | e.SetStateDirty() 1188 | e.ScrollIfNeeded() 1189 | return 1190 | } 1191 | 1192 | func DeleteCharBackward(e *BufferView) error { 1193 | if e.Buffer.Readonly { 1194 | return nil 1195 | } 1196 | e.deleteSelectionsIfAnySelection() 1197 | e.RemoveRange(e.Cursor.Point-1, e.Cursor.Point, true) 1198 | PointLeft(e, 1) 1199 | e.SetStateDirty() 1200 | return nil 1201 | } 1202 | 1203 | func DeleteCharForward(e *BufferView) error { 1204 | if e.Buffer.Readonly { 1205 | return nil 1206 | } 1207 | e.deleteSelectionsIfAnySelection() 1208 | e.RemoveRange(e.Cursor.Point, e.Cursor.Point+1, true) 1209 | 1210 | e.SetStateDirty() 1211 | return nil 1212 | } 1213 | func RevertLastBufferAction(e *BufferView) { 1214 | last, err := e.ActionStack.Pop() 1215 | if err != nil { 1216 | if errors.Is(err, EmptyStack) { 1217 | e.SetStateClean() 1218 | } 1219 | return 1220 | } 1221 | switch last.Type { 1222 | case BufferActionType_Insert: 1223 | e.RemoveRange(last.Idx, last.Idx+len(last.Data), false) 1224 | case BufferActionType_Delete: 1225 | e.AddBytesAtIndex(last.Data, last.Idx, false) 1226 | } 1227 | e.SetStateDirty() 1228 | 1229 | if len(e.ActionStack.data) < 1 { 1230 | e.SetStateClean() 1231 | } 1232 | } 1233 | 1234 | func WordAtPoint(e *BufferView) (int, int) { 1235 | currentWordStart := byteutils.SeekPreviousNonLetter(e.Buffer.Content, e.Cursor.Point) 1236 | if currentWordStart != 0 { 1237 | currentWordStart++ 1238 | } 1239 | currentWordEnd := byteutils.SeekNextNonLetter(e.Buffer.Content, e.Cursor.Point) 1240 | if currentWordEnd != len(e.Buffer.Content)-1 { 1241 | currentWordEnd-- 1242 | } 1243 | 1244 | return currentWordStart, currentWordEnd 1245 | } 1246 | 1247 | func LeftWord(e *BufferView) (int, int) { 1248 | leftWordEnd := byteutils.SeekPreviousNonLetter(e.Buffer.Content, e.Cursor.Point) 1249 | leftWordStart := byteutils.SeekPreviousNonLetter(e.Buffer.Content, leftWordEnd-1) + 1 1250 | 1251 | return leftWordStart, leftWordEnd 1252 | } 1253 | 1254 | func RightWord(e *BufferView) (int, int) { 1255 | rightWordStart := byteutils.SeekNextNonLetter(e.Buffer.Content, e.Cursor.Point) + 1 1256 | rightWordEnd := byteutils.SeekNextNonLetter(e.Buffer.Content, rightWordStart) 1257 | return rightWordStart, rightWordEnd 1258 | } 1259 | 1260 | func DeleteWordBackward(e *BufferView) { 1261 | if e.Buffer.Readonly { 1262 | return 1263 | } 1264 | leftWordStart, leftWordEnd := LeftWord(e) 1265 | if leftWordStart == -1 || leftWordEnd == -1 { 1266 | return 1267 | } 1268 | old := len(e.Buffer.Content) 1269 | e.RemoveRange(leftWordStart, e.Cursor.Point, true) 1270 | e.Cursor.SetBoth(e.Cursor.Point + (len(e.Buffer.Content) - old)) 1271 | 1272 | e.SetStateDirty() 1273 | } 1274 | 1275 | func Indent(e *BufferView) error { 1276 | e.AddBytesAtIndex([]byte(strings.Repeat(" ", e.Buffer.fileType.TabSize)), e.Cursor.Point, true) 1277 | PointRight(e, e.Buffer.fileType.TabSize) 1278 | e.SetStateDirty() 1279 | 1280 | return nil 1281 | } 1282 | 1283 | func KillLine(e *BufferView) { 1284 | if e.Buffer.Readonly { 1285 | return 1286 | } 1287 | var lastChange int 1288 | old := len(e.Buffer.Content) 1289 | PointLeft(e, lastChange) 1290 | line := e.getBufferLineForIndex(e.Cursor.Start()) 1291 | e.RemoveRange(e.Cursor.Point, line.endIndex, true) 1292 | lastChange += -1 * (len(e.Buffer.Content) - old) 1293 | e.SetStateDirty() 1294 | 1295 | } 1296 | func Cut(e *BufferView) error { 1297 | if e.Buffer.Readonly { 1298 | return nil 1299 | } 1300 | if e.Cursor.Start() != e.Cursor.End() { 1301 | // Copy selection 1302 | WriteToClipboard(e.Buffer.Content[e.Cursor.Start() : e.Cursor.End()+1]) 1303 | e.RemoveRange(e.Cursor.Start(), e.Cursor.End()+1, true) 1304 | e.Cursor.Mark = e.Cursor.Point 1305 | } else { 1306 | line := e.getBufferLineForIndex(e.Cursor.Start()) 1307 | WriteToClipboard(e.Buffer.Content[line.startIndex : line.endIndex+1]) 1308 | e.RemoveRange(line.startIndex, line.endIndex+1, true) 1309 | } 1310 | e.SetStateDirty() 1311 | 1312 | return nil 1313 | } 1314 | 1315 | func Paste(e *BufferView) error { 1316 | if e.Buffer.Readonly { 1317 | return nil 1318 | } 1319 | contentToPaste := GetClipboardContent() 1320 | e.AddBytesAtIndex(contentToPaste, e.Cursor.Start(), true) 1321 | e.SetStateDirty() 1322 | PointRight(e, len(contentToPaste)) 1323 | return nil 1324 | } 1325 | 1326 | func InteractiveGotoLine(e *BufferView) { 1327 | doneHook := func(userInput string, c *Context) { 1328 | number, err := strconv.Atoi(userInput) 1329 | if err != nil { 1330 | return 1331 | } 1332 | 1333 | for _, line := range e.bufferLines { 1334 | if line.ActualLine == number { 1335 | e.Cursor.SetBoth(line.startIndex) 1336 | e.ScrollIfNeeded() 1337 | } 1338 | } 1339 | 1340 | return 1341 | } 1342 | e.parent.SetPrompt("Goto", nil, doneHook, nil, "") 1343 | 1344 | return 1345 | } 1346 | 1347 | // @Scroll 1348 | 1349 | func ScrollUp(e *BufferView, n int) { 1350 | if e.VisibleStart <= 0 { 1351 | return 1352 | } 1353 | e.VisibleStart += int32(-1 * n) 1354 | 1355 | if e.VisibleStart < 0 { 1356 | e.VisibleStart = 0 1357 | } 1358 | 1359 | return 1360 | 1361 | } 1362 | 1363 | func ScrollToTop(e *BufferView) { 1364 | e.VisibleStart = 0 1365 | e.Cursor.SetBoth(0) 1366 | 1367 | return 1368 | } 1369 | 1370 | func ScrollToBottom(e *BufferView) { 1371 | e.VisibleStart = int32(len(e.bufferLines) - 1 - int(e.maxLine)) 1372 | e.Cursor.SetBoth(e.bufferLines[len(e.bufferLines)-1].startIndex) 1373 | 1374 | return 1375 | } 1376 | 1377 | func ScrollDown(e *BufferView, n int) error { 1378 | if int(e.VisibleEnd()) >= len(e.bufferLines) { 1379 | return nil 1380 | } 1381 | e.VisibleStart += int32(n) 1382 | if int(e.VisibleEnd()) >= len(e.bufferLines) { 1383 | e.VisibleStart = int32(len(e.bufferLines)-1) - e.maxLine 1384 | } 1385 | 1386 | return nil 1387 | 1388 | } 1389 | 1390 | // @Point 1391 | 1392 | func PointLeft(e *BufferView, n int) error { 1393 | e.Cursor.Point -= n 1394 | if e.Cursor.Point < 0 { 1395 | e.Cursor.SetBoth(0) 1396 | } 1397 | e.Cursor.Mark = e.Cursor.Point 1398 | e.ScrollIfNeeded() 1399 | return nil 1400 | } 1401 | 1402 | func PointRight(e *BufferView, n int) error { 1403 | e.Cursor.Point += n 1404 | if e.Cursor.Point > len(e.Buffer.Content) { 1405 | e.Cursor.SetBoth(0) 1406 | } 1407 | e.Cursor.Mark = e.Cursor.Point 1408 | e.ScrollIfNeeded() 1409 | return nil 1410 | } 1411 | 1412 | func PointUp(e *BufferView) error { 1413 | currentLine := e.getBufferLineForIndex(e.Cursor.Point) 1414 | prevLineIndex := currentLine.Index - 1 1415 | if prevLineIndex < 0 { 1416 | return nil 1417 | } 1418 | 1419 | prevLine := e.bufferLines[prevLineIndex] 1420 | col := e.Cursor.Point - currentLine.startIndex 1421 | newidx := prevLine.startIndex + col 1422 | if newidx > prevLine.endIndex { 1423 | newidx = prevLine.endIndex 1424 | } 1425 | e.Cursor.SetBoth(newidx) 1426 | e.ScrollIfNeeded() 1427 | 1428 | return nil 1429 | } 1430 | 1431 | func PointDown(e *BufferView) error { 1432 | currentLine := e.getBufferLineForIndex(e.Cursor.Point) 1433 | nextLineIndex := currentLine.Index + 1 1434 | if nextLineIndex >= len(e.bufferLines) { 1435 | return nil 1436 | } 1437 | 1438 | nextLine := e.bufferLines[nextLineIndex] 1439 | col := e.Cursor.Point - currentLine.startIndex 1440 | newIndex := nextLine.startIndex + col 1441 | if newIndex > nextLine.endIndex { 1442 | newIndex = nextLine.endIndex 1443 | } 1444 | e.Cursor.SetBoth(newIndex) 1445 | e.ScrollIfNeeded() 1446 | 1447 | return nil 1448 | } 1449 | 1450 | func CentralizePoint(e *BufferView) { 1451 | pos := e.convertBufferIndexToLineAndColumn(e.Cursor.Start()) 1452 | e.VisibleStart = int32(pos.Line) - (e.maxLine / 2) 1453 | if e.VisibleStart < 0 { 1454 | e.VisibleStart = 0 1455 | } 1456 | } 1457 | 1458 | func PointToBeginningOfLine(e *BufferView) error { 1459 | line := e.getBufferLineForIndex(e.Cursor.Start()) 1460 | e.Cursor.SetBoth(line.startIndex) 1461 | return nil 1462 | } 1463 | 1464 | func PointToEndOfLine(e *BufferView) error { 1465 | line := e.getBufferLineForIndex(e.Cursor.Start()) 1466 | e.Cursor.SetBoth(line.endIndex) 1467 | return nil 1468 | } 1469 | 1470 | func PointToMatchingChar(e *BufferView) error { 1471 | matching := byteutils.FindMatching(e.Buffer.Content, e.Cursor.Point) 1472 | if matching != -1 { 1473 | e.Cursor.SetBoth(matching) 1474 | } 1475 | 1476 | return nil 1477 | 1478 | } 1479 | 1480 | // @Mark 1481 | 1482 | func MarkRight(e *BufferView, n int) { 1483 | e.Cursor.Mark += n 1484 | if e.Cursor.Mark >= len(e.Buffer.Content) { 1485 | e.Cursor.Mark = len(e.Buffer.Content) 1486 | } 1487 | e.ScrollIfNeeded() 1488 | } 1489 | 1490 | func MarkLeft(e *BufferView, n int) { 1491 | e.Cursor.Mark -= n 1492 | if e.Cursor.Mark < 0 { 1493 | e.Cursor.Mark = 0 1494 | } 1495 | e.ScrollIfNeeded() 1496 | } 1497 | 1498 | func MarkUp(e *BufferView, n int) { 1499 | currentLine := e.getBufferLineForIndex(e.Cursor.Mark) 1500 | nextLineIndex := currentLine.Index - n 1501 | if nextLineIndex >= len(e.bufferLines) || nextLineIndex < 0 { 1502 | return 1503 | } 1504 | 1505 | nextLine := e.bufferLines[nextLineIndex] 1506 | newcol := nextLine.startIndex 1507 | e.Cursor.Mark = newcol 1508 | e.ScrollIfNeeded() 1509 | } 1510 | 1511 | func MarkDown(e *BufferView, n int) { 1512 | currentLine := e.getBufferLineForIndex(e.Cursor.Mark) 1513 | nextLineIndex := currentLine.Index + n 1514 | if nextLineIndex >= len(e.bufferLines) { 1515 | return 1516 | } 1517 | 1518 | nextLine := e.bufferLines[nextLineIndex] 1519 | newcol := nextLine.startIndex 1520 | e.Cursor.Mark = newcol 1521 | e.ScrollIfNeeded() 1522 | 1523 | return 1524 | } 1525 | 1526 | func MarkPreviousWord(e *BufferView) { 1527 | j := byteutils.SeekPreviousNonLetter(e.Buffer.Content, e.Cursor.Mark) 1528 | if j != -1 { 1529 | e.Cursor.Mark = j 1530 | e.ScrollIfNeeded() 1531 | } 1532 | 1533 | return 1534 | } 1535 | 1536 | func MarkNextWord(e *BufferView) { 1537 | j := byteutils.SeekNextNonLetter(e.Buffer.Content, e.Cursor.Mark) 1538 | if j != -1 { 1539 | e.Cursor.Mark = j 1540 | } 1541 | e.ScrollIfNeeded() 1542 | 1543 | } 1544 | 1545 | func MarkToEndOfLine(e *BufferView) { 1546 | line := e.getBufferLineForIndex(e.Cursor.End()) 1547 | e.Cursor.Mark = line.endIndex 1548 | 1549 | return 1550 | } 1551 | 1552 | func MarkToBeginningOfLine(e *BufferView) { 1553 | line := e.getBufferLineForIndex(e.Cursor.End()) 1554 | e.Cursor.Mark = line.startIndex 1555 | } 1556 | func MarkToMatchingChar(e *BufferView) { 1557 | matching := byteutils.FindMatching(e.Buffer.Content, e.Cursor.Point) 1558 | if matching != -1 { 1559 | e.Cursor.Mark = matching 1560 | } 1561 | 1562 | } 1563 | 1564 | // @Cursors 1565 | 1566 | func PointRightWord(e *BufferView) { 1567 | e.Cursor.SetBoth(e.Cursor.Point) 1568 | j := byteutils.SeekNextNonLetter(e.Buffer.Content, e.Cursor.Point) 1569 | if j != -1 { 1570 | e.Cursor.SetBoth(j) 1571 | } 1572 | e.ScrollIfNeeded() 1573 | 1574 | return 1575 | } 1576 | 1577 | func PointLeftWord(e *BufferView) { 1578 | e.Cursor.SetBoth(e.Cursor.Point) 1579 | j := byteutils.SeekPreviousNonLetter(e.Buffer.Content, e.Cursor.Point) 1580 | if j != -1 { 1581 | e.Cursor.SetBoth(j) 1582 | } 1583 | e.ScrollIfNeeded() 1584 | } 1585 | 1586 | func Write(e *BufferView) { 1587 | if e.Buffer.Readonly && e.IsSpecial() { 1588 | return 1589 | } 1590 | 1591 | if e.Buffer.fileType.TabSize != 0 { 1592 | e.Buffer.Content = bytes.Replace(e.Buffer.Content, []byte(strings.Repeat(" ", e.Buffer.fileType.TabSize)), []byte("\t"), -1) 1593 | } 1594 | 1595 | if e.Buffer.fileType.BeforeSave != nil { 1596 | _ = e.Buffer.fileType.BeforeSave(e) 1597 | } 1598 | 1599 | if e.Buffer.CRLF { 1600 | e.Buffer.Content = bytes.Replace(e.Buffer.Content, []byte("\n"), []byte("\r\n"), -1) 1601 | } 1602 | 1603 | if err := os.WriteFile(e.Buffer.File, e.Buffer.Content, 0644); err != nil { 1604 | return 1605 | } 1606 | e.SetStateClean() 1607 | e.replaceTabsWithSpaces() 1608 | if e.Buffer.CRLF { 1609 | e.Buffer.Content = bytes.Replace(e.Buffer.Content, []byte("\r\n"), []byte("\n"), -1) 1610 | } 1611 | if e.Buffer.fileType.AfterSave != nil { 1612 | _ = e.Buffer.fileType.AfterSave(e) 1613 | 1614 | } 1615 | 1616 | return 1617 | } 1618 | 1619 | func Copy(e *BufferView) error { 1620 | if e.Cursor.Start() != e.Cursor.End() { 1621 | // Copy selection 1622 | end := e.Cursor.End() + 1 1623 | if end >= len(e.Buffer.Content) { 1624 | end = len(e.Buffer.Content) - 1 1625 | } 1626 | WriteToClipboard(e.Buffer.Content[e.Cursor.Start():end]) 1627 | } else { 1628 | line := e.getBufferLineForIndex(e.Cursor.Start()) 1629 | WriteToClipboard(e.Buffer.Content[line.startIndex : line.endIndex+1]) 1630 | } 1631 | 1632 | return nil 1633 | } 1634 | 1635 | func CompileAskForCommand(a *BufferView) { 1636 | a.parent.SetPrompt("Compile", nil, func(userInput string, c *Context) { 1637 | a.LastCompileCommand = userInput 1638 | if err := a.parent.OpenCompilationBufferInBuildWindow(userInput); err != nil { 1639 | return 1640 | } 1641 | 1642 | return 1643 | }, nil, a.LastCompileCommand) 1644 | 1645 | return 1646 | } 1647 | 1648 | func CompileNoAsk(a *BufferView) { 1649 | if a.LastCompileCommand == "" { 1650 | CompileAskForCommand(a) 1651 | } 1652 | 1653 | if err := a.parent.OpenCompilationBufferInBuildWindow(a.LastCompileCommand); err != nil { 1654 | return 1655 | } 1656 | 1657 | return 1658 | } 1659 | 1660 | func GrepAsk(a *BufferView) { 1661 | a.parent.SetPrompt("Grep", nil, func(userInput string, c *Context) { 1662 | if err := a.parent.OpenGrepBufferInSensibleSplit(userInput); err != nil { 1663 | return 1664 | } 1665 | 1666 | return 1667 | }, nil, "") 1668 | 1669 | return 1670 | } 1671 | 1672 | const BIG_FILE_SEARCH_THRESHOLD = 1024 * 1024 1673 | 1674 | func SearchActivate(bufferView *BufferView) { 1675 | if len(bufferView.Buffer.Content) < BIG_FILE_SEARCH_THRESHOLD { 1676 | thisPromptKeymap := PromptKeymap.Clone() 1677 | thisPromptKeymap.BindKey(Key{K: ""}, func(c *Context) { 1678 | c.ResetPrompt() 1679 | SearchExit(bufferView) 1680 | }) 1681 | thisPromptKeymap.BindKey(Key{K: ""}, func(c *Context) { 1682 | SearchNextMatch(bufferView) 1683 | }) 1684 | bufferView.parent.SetPrompt("ISearch", func(query string, c *Context) { 1685 | if !bufferView.Search.IsSearching { 1686 | bufferView.Search.IsSearching = true 1687 | bufferView.keymaps.Push(SearchKeymap) 1688 | } 1689 | bufferView.Search.SearchString = query 1690 | matchPatternAsync(&bufferView.Search.SearchMatches, bufferView.Buffer.Content, []byte(query)) 1691 | }, nil, &thisPromptKeymap, "") 1692 | bufferView.parent.Prompt.NoRender = true 1693 | } else { 1694 | bufferView.parent.SetPrompt("Search", nil, func(query string, c *Context) { 1695 | if !bufferView.Search.IsSearching { 1696 | bufferView.Search.IsSearching = true 1697 | bufferView.keymaps.Push(SearchKeymap) 1698 | } 1699 | bufferView.Search.SearchString = query 1700 | matchPatternAsync(&bufferView.Search.SearchMatches, bufferView.Buffer.Content, []byte(query)) 1701 | }, nil, "") 1702 | } 1703 | } 1704 | 1705 | func SearchExit(editor *BufferView) error { 1706 | if len(editor.Buffer.Content) < BIG_FILE_SEARCH_THRESHOLD { 1707 | editor.keymaps.Pop() 1708 | editor.parent.ResetPrompt() 1709 | editor.Search.IsSearching = false 1710 | editor.Search.SearchMatches = nil 1711 | editor.Search.CurrentMatch = 0 1712 | editor.Search.MovedAwayFromCurrentMatch = false 1713 | editor.parent.Prompt.NoRender = false 1714 | return nil 1715 | } else { 1716 | editor.Search.IsSearching = false 1717 | editor.Search.SearchMatches = nil 1718 | editor.Search.CurrentMatch = 0 1719 | editor.Search.MovedAwayFromCurrentMatch = false 1720 | editor.keymaps.Pop() 1721 | } 1722 | 1723 | return nil 1724 | } 1725 | 1726 | func SearchNextMatch(editor *BufferView) error { 1727 | editor.Search.CurrentMatch++ 1728 | if editor.Search.CurrentMatch >= len(editor.Search.SearchMatches) { 1729 | editor.Search.CurrentMatch = 0 1730 | } 1731 | editor.Search.MovedAwayFromCurrentMatch = false 1732 | return nil 1733 | } 1734 | 1735 | func SearchPreviousMatch(editor *BufferView) error { 1736 | editor.Search.CurrentMatch-- 1737 | if editor.Search.CurrentMatch >= len(editor.Search.SearchMatches) { 1738 | editor.Search.CurrentMatch = 0 1739 | } 1740 | if editor.Search.CurrentMatch < 0 { 1741 | editor.Search.CurrentMatch = len(editor.Search.SearchMatches) - 1 1742 | } 1743 | editor.Search.MovedAwayFromCurrentMatch = false 1744 | return nil 1745 | } 1746 | 1747 | func QueryReplaceActivate(bufferView *BufferView) { 1748 | bufferView.parent.SetPrompt("Query", nil, func(query string, c *Context) { 1749 | bufferView.parent.SetPrompt("Replace", nil, func(replace string, c *Context) { 1750 | bufferView.QueryReplace.IsQueryReplace = true 1751 | bufferView.QueryReplace.SearchString = query 1752 | bufferView.QueryReplace.ReplaceString = replace 1753 | bufferView.keymaps.Push(QueryReplaceKeymap) 1754 | matchPatternAsync(&bufferView.QueryReplace.SearchMatches, bufferView.Buffer.Content, []byte(query)) 1755 | }, nil, "") 1756 | }, nil, "") 1757 | } 1758 | 1759 | func QueryReplaceReplaceThisMatch(bufferView *BufferView) { 1760 | match := bufferView.QueryReplace.SearchMatches[bufferView.QueryReplace.CurrentMatch] 1761 | bufferView.RemoveRange(match[0], match[1]+1, false) 1762 | bufferView.AddBytesAtIndex([]byte(bufferView.QueryReplace.ReplaceString), match[0], false) 1763 | bufferView.QueryReplace.CurrentMatch++ 1764 | bufferView.QueryReplace.MovedAwayFromCurrentMatch = false 1765 | if bufferView.QueryReplace.CurrentMatch >= len(bufferView.QueryReplace.SearchMatches) { 1766 | QueryReplaceExit(bufferView) 1767 | } 1768 | } 1769 | func QueryReplaceIgnoreThisMatch(bufferView *BufferView) { 1770 | bufferView.QueryReplace.CurrentMatch++ 1771 | bufferView.QueryReplace.MovedAwayFromCurrentMatch = false 1772 | if bufferView.QueryReplace.CurrentMatch >= len(bufferView.QueryReplace.SearchMatches) { 1773 | QueryReplaceExit(bufferView) 1774 | } 1775 | } 1776 | func QueryReplaceExit(bufferView *BufferView) { 1777 | bufferView.QueryReplace.IsQueryReplace = false 1778 | bufferView.QueryReplace.SearchString = "" 1779 | bufferView.QueryReplace.ReplaceString = "" 1780 | bufferView.QueryReplace.SearchMatches = nil 1781 | bufferView.QueryReplace.CurrentMatch = 0 1782 | bufferView.QueryReplace.MovedAwayFromCurrentMatch = false 1783 | bufferView.keymaps.Pop() 1784 | } 1785 | 1786 | func GetClipboardContent() []byte { 1787 | return clipboard.Read(clipboard.FmtText) 1788 | } 1789 | 1790 | func WriteToClipboard(bs []byte) { 1791 | clipboard.Write(clipboard.FmtText, bytes.Clone(bs)) 1792 | } 1793 | 1794 | func RevertBuffer(bufferView *BufferView) { 1795 | bufferView.readFileFromDisk() 1796 | } 1797 | --------------------------------------------------------------------------------