├── .github └── workflows │ ├── ci.yml │ └── docs_ci.yml ├── LICENSE ├── README.md ├── buffer.v ├── buffer_commands.v ├── command └── command.v ├── cursor.v ├── docs └── vee.gif ├── examples └── tuieditor │ └── main.v ├── magnet.v ├── math.v ├── tests ├── small_test.v └── unicode_test.v ├── types.v ├── utils.v ├── v.mod └── vee.v /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Code CI 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - "**.md" 7 | pull_request: 8 | paths-ignore: 9 | - "**.md" 10 | 11 | concurrency: 12 | group: code-${{ github.event.pull_request.number || github.sha }} 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | v-compiles-vee-examples: 17 | runs-on: ubuntu-latest 18 | timeout-minutes: 30 19 | env: 20 | VFLAGS: -cc tcc -no-retry-compilation 21 | steps: 22 | - name: Install V 23 | uses: vlang/setup-v@v1 24 | with: 25 | check-latest: true 26 | 27 | - name: Checkout vee 28 | uses: actions/checkout@v2 29 | with: 30 | path: vee 31 | 32 | - name: Link local SDL folder in ~/.vmodules/vee 33 | run: | 34 | cd vee 35 | mkdir -p ~/.vmodules 36 | ln -s $(pwd) ~/.vmodules/vee 37 | 38 | - name: Test code formatting 39 | run: | 40 | cd vee 41 | v test-fmt 42 | v fmt -verify . 43 | 44 | - name: Run tests 45 | run: v test vee 46 | 47 | - name: Build vee shared 48 | run: v -shared -g vee 49 | 50 | - name: Build vee examples 51 | run: | 52 | v should-compile-all vee/examples 53 | -------------------------------------------------------------------------------- /.github/workflows/docs_ci.yml: -------------------------------------------------------------------------------- 1 | name: Docs CI 2 | 3 | ### Run on *EVERY* commit. The documentation *SHOULD* stay valid, and 4 | ### the developers should receive early warning if they break it. 5 | on: [push, pull_request] 6 | 7 | jobs: 8 | check-markdown: 9 | runs-on: ubuntu-latest 10 | timeout-minutes: 5 11 | steps: 12 | - name: Install V 13 | uses: vlang/setup-v@v1 14 | with: 15 | check-latest: true 16 | 17 | - name: Checkout project 18 | uses: actions/checkout@v2 19 | 20 | - name: Check markdown line length & code examples 21 | run: v check-md -hide-warnings . 22 | ## NB: -hide-warnings is used here, so that the output is less noisy, 23 | ## thus real errors are easier to spot. 24 | 25 | report-missing-fn-doc: 26 | runs-on: ubuntu-latest 27 | timeout-minutes: 5 28 | env: 29 | MOPTIONS: --verify --relative-paths 30 | steps: 31 | - name: Install V 32 | uses: vlang/setup-v@v1 33 | with: 34 | check-latest: true 35 | 36 | - name: Checkout project 37 | uses: actions/checkout@v2 38 | 39 | - name: Check for missing documentation 40 | run: v missdoc $MOPTIONS . 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2022 Lars Pontoppidan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vee 2 | 3 | V Editor Engine 4 | 5 | This is a V module providing the guts of a text editor. 6 | 7 | Progammed to make it easier, faster and richer to develop 8 | editors, text areas, consoles for games etc. 9 | 10 | ![Example](docs/vee.gif) 11 | 12 | ## Features 13 | 14 | * Multiple buffers 15 | * Full undo/redo support 16 | * Cursor magnet 17 | * Movement by words 18 | * Unicode support 19 | -------------------------------------------------------------------------------- /buffer.v: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2022 Lars Pontoppidan. All rights reserved. 2 | // Use of this source code is governed by the MIT license distributed with this software. 3 | module vee 4 | 5 | import encoding.utf8 6 | 7 | const rune_digits = [`0`, `1`, `2`, `3`, `4`, `5`, `6`, `7`, `8`, `9`] 8 | 9 | struct Position { 10 | pub mut: 11 | x int 12 | y int 13 | } 14 | 15 | struct Selection { 16 | buffer &Buffer 17 | mut: 18 | from Position 19 | to Position 20 | } 21 | 22 | @[heap] 23 | struct Buffer { 24 | line_break string = '\n' 25 | pub: 26 | tab_width int = 4 27 | mut: 28 | mode Mode = .edit 29 | selections []Selection 30 | pub mut: 31 | lines []string 32 | cursor Cursor 33 | magnet Magnet 34 | } 35 | 36 | pub struct BufferConfig { 37 | line_break string = '\n' 38 | tab_width int = 4 39 | } 40 | 41 | // new_buffer returns a new heap-allocated `Buffer` instance. 42 | pub fn new_buffer(config BufferConfig) &Buffer { 43 | mut b := &Buffer{ 44 | line_break: config.line_break 45 | } 46 | m := Magnet{ 47 | buffer: b 48 | } 49 | b.magnet = m 50 | return b 51 | } 52 | 53 | // set_mode sets the edit mode to `mode`. 54 | pub fn (mut b Buffer) set_mode(mode Mode) { 55 | b.mode = mode 56 | } 57 | 58 | // flatten returns `s` as a "flat" `string`. 59 | // Escape characters are visible. 60 | pub fn (b Buffer) flatten(s string) string { 61 | return s.replace(b.line_break, r'\n').replace('\t', r'\t') 62 | } 63 | 64 | // flat returns the buffer as a "flat" `string`. 65 | // Escape characters are visible. 66 | pub fn (b Buffer) flat() string { 67 | return b.flatten(b.raw()) 68 | } 69 | 70 | // raw returns the contents of the buffer as-is. 71 | pub fn (b Buffer) raw() string { 72 | return b.lines.join(b.line_break) 73 | } 74 | 75 | // eol returns `true` if the cursor is at the end of a line. 76 | pub fn (b Buffer) eol() bool { 77 | x, y := b.cursor.xy() 78 | line := b.line(y).runes() 79 | return x >= line.len 80 | } 81 | 82 | // eof returns `true` if the cursor is at the end of the file/buffer. 83 | pub fn (b Buffer) eof() bool { 84 | _, y := b.cursor.xy() 85 | return y >= b.lines.len - 1 86 | } 87 | 88 | // cur_char returns the character at the cursor. 89 | pub fn (b Buffer) cur_char() string { 90 | x, y := b.cursor.xy() 91 | line := b.line(y).runes() 92 | if x >= line.len { 93 | return '' 94 | } 95 | // TODO check if this is needed 96 | return [line[x]].string() 97 | } 98 | 99 | // cur_rune returns the character at the cursor. 100 | pub fn (b Buffer) cur_rune() rune { 101 | c := b.cur_char().runes() 102 | if c.len > 0 { 103 | return c[0] 104 | } 105 | return rune(0) 106 | } 107 | 108 | // prev_rune returns the previous rune. 109 | pub fn (b Buffer) prev_rune() rune { 110 | c := b.prev_char().runes() 111 | if c.len > 0 { 112 | return c[0] 113 | } 114 | return rune(0) 115 | } 116 | 117 | // prev_char returns the previous character. 118 | pub fn (b Buffer) prev_char() string { 119 | mut x, y := b.cursor.xy() 120 | x-- 121 | line := b.line(y).runes() 122 | if x >= line.len || x < 0 { 123 | return '' 124 | } 125 | // TODO check if this is needed 126 | return [line[x]].string() 127 | } 128 | 129 | // cur_slice returns the contents from start of 130 | // the current line up until the cursor. 131 | pub fn (b Buffer) cur_slice() string { 132 | x, y := b.cursor.xy() 133 | line := b.line(y).runes() 134 | if x == 0 || x > line.len { 135 | return '' 136 | } 137 | return line[..x].string() 138 | } 139 | 140 | // line returns the contents of the line at `y`. 141 | pub fn (b Buffer) line(y int) string { 142 | if y < 0 || y >= b.lines.len { 143 | return '' 144 | } 145 | return b.lines[y] 146 | } 147 | 148 | // line returns the contents of the line the cursor is at. 149 | pub fn (b Buffer) cur_line() string { 150 | _, y := b.cursor.xy() 151 | return b.line(y) 152 | } 153 | 154 | // cur_line_flat returns the "flat" contents of the line the cursor is at. 155 | pub fn (b Buffer) cur_line_flat() string { 156 | return b.flatten(b.cur_line()) 157 | } 158 | 159 | // cursor_index returns the linear index of the cursor. 160 | pub fn (b Buffer) cursor_index() int { 161 | mut i := 0 162 | for y, line in b.lines { 163 | if b.cursor.pos.y == y { 164 | i += b.cursor.pos.x 165 | break 166 | } 167 | i += line.runes().len + 1 168 | } 169 | return i 170 | } 171 | 172 | // put adds `input` to the buffer. 173 | pub fn (mut b Buffer) put(ipt InputType) { 174 | s := ipt.str() 175 | dbg(@MOD + '.' + @STRUCT + '::' + @FN + ' "${b.flatten(s)}"') 176 | 177 | // println('s.len: $s.len, s.runes().len: ${s.runes().len} `$s.runes()`') 178 | 179 | has_line_ending := s.contains(b.line_break) 180 | x, y := b.cursor.xy() 181 | if b.lines.len == 0 { 182 | b.lines.prepend('') 183 | } 184 | line := b.lines[y].runes() 185 | l, r := line[..x].string(), line[x..].string() 186 | if has_line_ending { 187 | mut lines := s.split(b.line_break) 188 | lines[0] = l + lines[0] 189 | lines[lines.len - 1] += r 190 | b.lines.delete(y) 191 | b.lines.insert(y, lines) 192 | last := lines[lines.len - 1].runes() 193 | b.cursor.set(last.len, y + lines.len - 1) 194 | if s == b.line_break { 195 | b.cursor.set(0, b.cursor.pos.y) 196 | } 197 | } else { 198 | b.lines[y] = l + s + r 199 | b.cursor.set(x + s.runes().len, y) 200 | } 201 | b.magnet.record() 202 | // dbg(@MOD+'.'+@STRUCT+'::'+@FN+' "${b.flat()}"') 203 | } 204 | 205 | // put_line_break adds a line break to the buffer. 206 | pub fn (mut b Buffer) put_line_break() { 207 | b.put(b.line_break) 208 | dbg(@MOD + '.' + @STRUCT + '::' + @FN + ' "${b.flat()}"') 209 | } 210 | 211 | // del deletes `amount` of characters from the buffer. 212 | // An `amount` > 0 will delete `amount` characters to the *right* of the cursor. 213 | // An `amount` < 0 will delete `amount` characters to the *left* of the cursor. 214 | pub fn (mut b Buffer) del(amount int) string { 215 | if amount == 0 { 216 | return '' 217 | } 218 | x, y := b.cursor.xy() 219 | if amount < 0 { // don't delete left if we're at 0,0 220 | if x == 0 && y == 0 { 221 | return '' 222 | } 223 | } else if x >= b.cur_line().runes().len && y >= b.lines.len - 1 { 224 | return '' 225 | } 226 | mut removed := '' 227 | if amount < 0 { // backspace (backward) 228 | i := b.cursor_index() 229 | raw_runes := b.raw().runes() 230 | // println('raw_runes: $raw_runes') 231 | // println('amount: $amount, i: $i') 232 | 233 | removed = raw_runes[i + amount..i].string() 234 | mut left := amount * -1 235 | 236 | // println('removed: `$removed`') 237 | // println(@MOD+'.'+@STRUCT+'::'+@FN+' "${b.flat()}" (${b.cursor.pos.x},${b.cursor.pos.y}/$i) $amount') 238 | 239 | for li := y; li >= 0 && left > 0; li-- { 240 | ln := b.lines[li].runes() 241 | // println(@MOD+'.'+@STRUCT+'::'+@FN+' left: $left, line length: $ln.len') 242 | if left == ln.len + 1 { // All of the line + 1 - since we're going backwards the "+1" is the line break delimiter. 243 | b.lines.delete(li) 244 | left = 0 245 | if y == 0 { 246 | return '' 247 | } 248 | line_above := b.lines[li - 1].runes() 249 | b.cursor.pos.x = line_above.len 250 | b.cursor.pos.y-- 251 | break 252 | } else if left > ln.len { 253 | b.lines.delete(li) 254 | if ln.len == 0 { // line break delimiter 255 | left-- 256 | if y == 0 { 257 | return '' 258 | } 259 | line_above := b.lines[li - 1].runes() 260 | b.cursor.pos.x = line_above.len 261 | } else { 262 | left -= ln.len 263 | } 264 | b.cursor.pos.y-- 265 | } else { 266 | if x == 0 { 267 | if y == 0 { 268 | return '' 269 | } 270 | line_above := b.lines[li - 1].runes() 271 | if ln.len == 0 { // at line break 272 | b.lines.delete(li) 273 | b.cursor.pos.y-- 274 | b.cursor.pos.x = line_above.len 275 | } else { 276 | b.lines[li - 1] = line_above.string() + ln.string() 277 | b.lines.delete(li) 278 | b.cursor.pos.y-- 279 | b.cursor.pos.x = line_above.len 280 | } 281 | } else if x == 1 { 282 | runes := b.lines[li].runes() 283 | b.lines[li] = runes[left..].string() 284 | b.cursor.pos.x = 0 285 | } else { 286 | b.lines[li] = ln[..x - left].string() + ln[x..].string() 287 | b.cursor.pos.x -= left 288 | } 289 | left = 0 290 | break 291 | } 292 | } 293 | } else { // delete (forward) 294 | i := b.cursor_index() + 1 295 | raw_buffer := b.raw().runes() 296 | from_i := i 297 | mut to_i := i + amount 298 | 299 | if to_i > raw_buffer.len { 300 | to_i = raw_buffer.len 301 | } 302 | removed = raw_buffer[from_i..to_i].string() 303 | 304 | mut left := amount 305 | for li := y; li >= 0 && left > 0; li++ { 306 | ln := b.lines[li].runes() 307 | if x == ln.len { // at line end 308 | if y + 1 <= b.lines.len { 309 | b.lines[li] = ln.string() + b.lines[y + 1] 310 | b.lines.delete(y + 1) 311 | left-- 312 | b.del(left) 313 | } 314 | } else if left > ln.len { 315 | b.lines.delete(li) 316 | left -= ln.len 317 | } else { 318 | b.lines[li] = ln[..x].string() + ln[x + left..].string() 319 | left = 0 320 | } 321 | } 322 | } 323 | b.magnet.record() 324 | // dbg(@MOD+'.'+@STRUCT+'::'+@FN+' "${b.flat()}"') 325 | 326 | dbg(@MOD + '.' + @STRUCT + '::' + @FN + ' "${b.flat()}"-"${b.flatten(removed)}"') 327 | 328 | return removed 329 | } 330 | 331 | fn (b Buffer) dmp() { 332 | eprintln('${b.cursor.pos}\n${b.raw()}') 333 | } 334 | 335 | // free frees all buffer memory 336 | fn (mut b Buffer) free() { 337 | dbg(@MOD + '.' + @STRUCT + '::' + @FN) 338 | 339 | unsafe { 340 | for line in b.lines { 341 | line.free() 342 | } 343 | b.lines.free() 344 | } 345 | } 346 | 347 | // cursor_to sets the cursor within the buffer bounds 348 | pub fn (mut b Buffer) cursor_to(x int, y int) { 349 | b.cursor.set(x, y) 350 | b.sync_cursor() 351 | b.magnet.record() 352 | } 353 | 354 | // sync_cursor syncs the cursor position to be within the buffer bounds 355 | fn (mut b Buffer) sync_cursor() { 356 | x, y := b.cursor.xy() 357 | if x < 0 { 358 | b.cursor.pos.x = 0 359 | } 360 | if y < 0 { 361 | b.cursor.pos.y = 0 362 | } 363 | line := b.cur_line().runes() 364 | if x >= line.len { 365 | if line.len <= 0 { 366 | b.cursor.pos.x = 0 367 | } else { 368 | b.cursor.pos.x = line.len 369 | } 370 | } 371 | if y > b.lines.len { 372 | if b.lines.len <= 0 { 373 | b.cursor.pos.y = 0 374 | } else { 375 | b.cursor.pos.y = b.lines.len - 1 376 | } 377 | } 378 | } 379 | 380 | // move_cursor navigates the cursor within the buffer bounds 381 | pub fn (mut b Buffer) move_cursor(amount int, movement Movement) { 382 | pos := b.cursor.pos 383 | match movement { 384 | .up { 385 | if pos.y - amount >= 0 { 386 | b.cursor.move(0, -amount) 387 | b.sync_cursor() 388 | // b.magnet.activate() 389 | } 390 | } 391 | .down { 392 | if pos.y + amount < b.lines.len { 393 | b.cursor.move(0, amount) 394 | b.sync_cursor() 395 | // b.magnet.activate() 396 | } 397 | } 398 | .left { 399 | if pos.x - amount >= 0 { 400 | b.cursor.move(-amount, 0) 401 | b.sync_cursor() 402 | b.magnet.record() 403 | } 404 | } 405 | .right { 406 | if pos.x + amount <= b.cur_line().runes().len { 407 | b.cursor.move(amount, 0) 408 | b.sync_cursor() 409 | b.magnet.record() 410 | } 411 | } 412 | .page_up { 413 | dlines := imin(b.cursor.pos.y, amount) 414 | b.cursor.move(0, -dlines) 415 | b.sync_cursor() 416 | // b.magnet.activate() 417 | } 418 | .page_down { 419 | dlines := imin(b.lines.len - 1, b.cursor.pos.y + amount) - b.cursor.pos.y 420 | b.cursor.move(0, dlines) 421 | b.sync_cursor() 422 | // b.magnet.activate() 423 | } 424 | .home { 425 | b.cursor.set(0, b.cursor.pos.y) 426 | b.sync_cursor() 427 | b.magnet.record() 428 | } 429 | .end { 430 | b.cursor.set(b.cur_line().runes().len, b.cursor.pos.y) 431 | b.sync_cursor() 432 | b.magnet.record() 433 | } 434 | } 435 | } 436 | 437 | // move_to_word navigates the cursor to the nearst word in the given direction. 438 | pub fn (mut b Buffer) move_to_word(movement Movement) { 439 | a := if movement == .left { -1 } else { 1 } 440 | 441 | mut line := b.cur_line().runes() 442 | mut x, mut y := b.cursor.pos.x, b.cursor.pos.y 443 | if x + a < 0 && y > 0 { 444 | y-- 445 | line = b.line(b.cursor.pos.y - 1).runes() 446 | x = line.len 447 | } else if x + a >= line.len && y + 1 < b.lines.len { 448 | y++ 449 | line = b.line(b.cursor.pos.y + 1).runes() 450 | x = 0 451 | } 452 | // first, move past all non-`a-zA-Z0-9_` characters 453 | for x + a >= 0 && x + a < line.len && !(utf8.is_letter(line[x + a]) 454 | || line[x + a] in rune_digits || line[x + a] == `_`) { 455 | x += a 456 | } 457 | // then, move past all the letters and numbers 458 | for x + a >= 0 && x + a < line.len && (utf8.is_letter(line[x + a]) 459 | || line[x + a] in rune_digits || line[x + a] == `_`) { 460 | x += a 461 | } 462 | // if the cursor is out of bounds, move it to the next/previous line 463 | if x + a >= 0 && x + a <= line.len { 464 | x += a 465 | } else if a < 0 && y + 1 > b.lines.len && y - 1 >= 0 { 466 | y += a 467 | x = 0 468 | } 469 | b.cursor.set(x, y) 470 | b.magnet.record() 471 | } 472 | 473 | /* 474 | * Selections 475 | */ 476 | // set_default_select sets the default selection. 477 | pub fn (mut b Buffer) set_default_select(from Position, to Position) { 478 | b.set_select(0, from, to) 479 | } 480 | 481 | // set_select sets the selection `index`. 482 | pub fn (mut b Buffer) set_select(index int, from Position, to Position) { 483 | if b.mode != .@select { 484 | b.mode = .@select 485 | } 486 | if b.selections.len == 0 { 487 | b.selections << Selection{ 488 | from: from 489 | to: to 490 | buffer: b 491 | } 492 | } else { 493 | // TODO bounds check or map ?? 494 | b.selections[index].from = from 495 | b.selections[index].to = to 496 | } 497 | } 498 | 499 | // selection_at sets the default selection at `index`. 500 | pub fn (b Buffer) selection_at(index int) Selection { 501 | // TODO bounds check or map ?? 502 | return b.selections[index] 503 | } 504 | -------------------------------------------------------------------------------- /buffer_commands.v: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2022 Lars Pontoppidan. All rights reserved. 2 | // Use of this source code is governed by the MIT license distributed with this software. 3 | module vee 4 | 5 | /* 6 | * Buffer commands 7 | */ 8 | struct PutCmd { 9 | mut: 10 | buffer &Buffer 11 | input InputType 12 | } 13 | 14 | fn (cmd PutCmd) str() string { 15 | return @STRUCT + ' { 16 | buffer: ${ptr_str(cmd.buffer)} 17 | input: ${cmd.input} 18 | }' 19 | } 20 | 21 | fn (mut cmd PutCmd) do() { 22 | mut b := cmd.buffer 23 | b.put(cmd.input) 24 | } 25 | 26 | fn (mut cmd PutCmd) undo() { 27 | dbg(@MOD + '.' + @STRUCT + '::' + @FN) 28 | 29 | mut b := cmd.buffer 30 | b.del(-cmd.input.len()) 31 | } 32 | 33 | fn (mut cmd PutCmd) redo() { 34 | dbg(@MOD + '.' + @STRUCT + '::' + @FN) 35 | 36 | mut b := cmd.buffer 37 | b.put(cmd.input) 38 | } 39 | 40 | // 41 | struct PutLineBreakCmd { 42 | mut: 43 | buffer &Buffer 44 | } 45 | 46 | fn (cmd PutLineBreakCmd) str() string { 47 | return @STRUCT + ' { 48 | buffer: ${ptr_str(cmd.buffer)} 49 | }' 50 | } 51 | 52 | fn (mut cmd PutLineBreakCmd) do() { 53 | cmd.buffer.put_line_break() 54 | } 55 | 56 | fn (mut cmd PutLineBreakCmd) undo() { 57 | dbg(@MOD + '.' + @STRUCT + '::' + @FN) 58 | 59 | mut b := cmd.buffer 60 | b.del(-1) 61 | } 62 | 63 | fn (mut cmd PutLineBreakCmd) redo() { 64 | dbg(@MOD + '.' + @STRUCT + '::' + @FN) 65 | 66 | cmd.buffer.put_line_break() 67 | } 68 | 69 | // 70 | struct DelCmd { 71 | mut: 72 | buffer &Buffer 73 | amount int 74 | deleted string 75 | pos Position 76 | } 77 | 78 | fn (cmd DelCmd) str() string { 79 | return @STRUCT + 80 | ' { 81 | buffer: ${ptr_str(cmd.buffer)} 82 | amount: ${cmd.amount} 83 | deleted: ${cmd.deleted} 84 | pos: ${cmd.pos} 85 | }' 86 | } 87 | 88 | fn (mut cmd DelCmd) do() { 89 | dbg(@MOD + '.' + @STRUCT + '::' + @FN) 90 | 91 | mut b := cmd.buffer 92 | cmd.pos = b.cursor.pos 93 | cmd.deleted = b.del(cmd.amount) 94 | } 95 | 96 | fn (mut cmd DelCmd) undo() { 97 | dbg(@MOD + '.' + @STRUCT + '::' + @FN) 98 | 99 | mut b := cmd.buffer 100 | if cmd.amount < 0 { 101 | b.put(cmd.deleted) 102 | } else { 103 | b.cursor_to(cmd.pos.x, cmd.pos.y) 104 | b.put(cmd.deleted) 105 | b.cursor_to(cmd.pos.x, cmd.pos.y) 106 | } 107 | } 108 | 109 | fn (mut cmd DelCmd) redo() { 110 | dbg(@MOD + '.' + @STRUCT + '::' + @FN) 111 | 112 | mut b := cmd.buffer 113 | if cmd.amount < 0 { 114 | b.del(cmd.amount) 115 | } else { 116 | b.cursor_to(cmd.pos.x, cmd.pos.y) 117 | b.del(cmd.amount) 118 | } 119 | } 120 | 121 | // 122 | struct MoveCursorCmd { 123 | mut: 124 | buffer &Buffer 125 | amount int 126 | movement Movement 127 | from_pos Position 128 | to_pos Position 129 | } 130 | 131 | fn (cmd MoveCursorCmd) str() string { 132 | return @STRUCT + ' { 133 | buffer: ${ptr_str(cmd.buffer)} 134 | movement: ${cmd.movement} 135 | }' 136 | } 137 | 138 | fn (mut cmd MoveCursorCmd) do() { 139 | dbg(@MOD + '.' + @STRUCT + '::' + @FN) 140 | 141 | mut b := cmd.buffer 142 | cmd.from_pos = b.cursor.pos 143 | b.move_cursor(cmd.amount, cmd.movement) 144 | cmd.to_pos = b.cursor.pos 145 | } 146 | 147 | fn (mut cmd MoveCursorCmd) undo() { 148 | dbg(@MOD + '.' + @STRUCT + '::' + @FN) 149 | 150 | mut b := cmd.buffer 151 | b.cursor_to(cmd.from_pos.x, cmd.from_pos.y) 152 | } 153 | 154 | fn (mut cmd MoveCursorCmd) redo() { 155 | dbg(@MOD + '.' + @STRUCT + '::' + @FN) 156 | 157 | mut b := cmd.buffer 158 | b.cursor_to(cmd.to_pos.x, cmd.to_pos.y) 159 | } 160 | 161 | /* 162 | * MoveToWord 163 | */ 164 | struct MoveToWordCmd { 165 | mut: 166 | buffer &Buffer 167 | movement Movement 168 | from_pos Position 169 | to_pos Position 170 | } 171 | 172 | fn (cmd MoveToWordCmd) str() string { 173 | return @STRUCT + ' { 174 | buffer: ${ptr_str(cmd.buffer)} 175 | movement: ${cmd.movement} 176 | }' 177 | } 178 | 179 | fn (mut cmd MoveToWordCmd) do() { 180 | dbg(@MOD + '.' + @STRUCT + '::' + @FN) 181 | 182 | mut b := cmd.buffer 183 | cmd.from_pos = b.cursor.pos 184 | b.move_to_word(cmd.movement) 185 | cmd.to_pos = b.cursor.pos 186 | } 187 | 188 | fn (mut cmd MoveToWordCmd) undo() { 189 | dbg(@MOD + '.' + @STRUCT + '::' + @FN) 190 | 191 | mut b := cmd.buffer 192 | b.cursor_to(cmd.from_pos.x, cmd.from_pos.y) 193 | } 194 | 195 | fn (mut cmd MoveToWordCmd) redo() { 196 | dbg(@MOD + '.' + @STRUCT + '::' + @FN) 197 | 198 | mut b := cmd.buffer 199 | b.cursor_to(cmd.to_pos.x, cmd.to_pos.y) 200 | } 201 | -------------------------------------------------------------------------------- /command/command.v: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2022 Lars Pontoppidan. All rights reserved. 2 | // Use of this source code is governed by the MIT license distributed with this software. 3 | module command 4 | 5 | pub enum QueueType { 6 | execute 7 | undo 8 | redo 9 | } 10 | 11 | pub interface ICommand { 12 | mut: 13 | do() 14 | undo() 15 | redo() 16 | } 17 | 18 | pub struct Invoker { 19 | mut: 20 | command_queue []ICommand 21 | undo_stack []ICommand 22 | redo_stack []ICommand 23 | } 24 | 25 | // add_and_execute adds and then executes `cmd`. 26 | pub fn (mut i Invoker) add_and_execute(cmd ICommand) { 27 | i.add(cmd) 28 | i.execute() 29 | } 30 | 31 | // add adds `cmd` to the `Invoker`. 32 | pub fn (mut i Invoker) add(cmd ICommand) { 33 | i.command_queue << cmd 34 | } 35 | 36 | // peek peeks the next command. 37 | pub fn (mut i Invoker) peek(queue_type QueueType) ?ICommand { 38 | dbg(@MOD + '.' + @STRUCT + '::' + @FN + '(' + queue_type.str() + ')') 39 | 40 | match queue_type { 41 | .execute { 42 | if i.command_queue.len > 0 { 43 | return i.command_queue.last() 44 | } 45 | } 46 | .undo { 47 | if i.undo_stack.len > 0 { 48 | return i.undo_stack.last() 49 | } 50 | } 51 | .redo { 52 | if i.redo_stack.len > 0 { 53 | return i.redo_stack.last() 54 | } 55 | } 56 | } 57 | 58 | return none 59 | } 60 | 61 | // execute executes the next command. 62 | pub fn (mut i Invoker) execute() bool { 63 | if i.command_queue.len > 0 { 64 | dbg(@MOD + '.' + @STRUCT + '::' + @FN) 65 | 66 | i.redo_stack.clear() 67 | mut cmd := i.command_queue.pop() 68 | cmd.do() 69 | i.undo_stack << cmd 70 | return true 71 | } 72 | return false 73 | } 74 | 75 | // undo undo the last executed command. 76 | pub fn (mut i Invoker) undo() ?ICommand { 77 | if i.undo_stack.len > 0 { 78 | dbg(@MOD + '.' + @STRUCT + '::' + @FN) 79 | 80 | mut cmd := i.undo_stack.pop() 81 | cmd.undo() 82 | i.redo_stack << cmd 83 | return cmd 84 | } 85 | return none 86 | } 87 | 88 | // redo redo the last undone command. 89 | pub fn (mut i Invoker) redo() ?ICommand { 90 | if i.redo_stack.len > 0 { 91 | dbg(@MOD + '.' + @STRUCT + '::' + @FN) 92 | 93 | mut cmd := i.redo_stack.pop() 94 | cmd.redo() 95 | i.undo_stack << cmd 96 | return cmd 97 | } 98 | return none 99 | } 100 | 101 | @[if vee_debug ?] 102 | fn dbg(str string) { 103 | eprintln(str) 104 | } 105 | -------------------------------------------------------------------------------- /cursor.v: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2022 Lars Pontoppidan. All rights reserved. 2 | // Use of this source code is governed by the MIT license distributed with this software. 3 | module vee 4 | 5 | struct Cursor { 6 | pub mut: 7 | pos Position 8 | } 9 | 10 | fn (mut c Cursor) set(x int, y int) { 11 | c.pos.x = x 12 | c.pos.y = y 13 | } 14 | 15 | fn (mut c Cursor) move(x int, y int) { 16 | c.pos.x += x 17 | c.pos.y += y 18 | } 19 | 20 | fn (c Cursor) xy() (int, int) { 21 | return c.pos.x, c.pos.y 22 | } 23 | -------------------------------------------------------------------------------- /docs/vee.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/larpon/vee/e7640e0ad48f56c3edeeb140447cb80ed6838fb1/docs/vee.gif -------------------------------------------------------------------------------- /examples/tuieditor/main.v: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | import os 4 | import term 5 | import term.ui 6 | import vee 7 | import encoding.utf8.east_asian 8 | 9 | const space_unicode = [ 10 | `\u0009`, // U+0009 CHARACTER TABULATION 11 | `\u0020`, // U+0020 SPACE 12 | `\u00ad`, // U+00AD SOFT HYPHEN 13 | `\u115f`, // U+115F HANGUL CHOSEONG FILLER 14 | `\u1160`, // U+1160 HANGUL JUNGSEONG FILLER 15 | `\u2000`, // U+2000 EN QUAD 16 | `\u2001`, // U+2001 EM QUAD 17 | `\u2002`, // U+2002 EN SPACE 18 | `\u2003`, // U+2003 EM SPACE 19 | `\u2004`, // U+2004 THREE-PER-EM SPACE 20 | `\u2005`, // U+2005 FOUR-PER-EM SPACE 21 | `\u2006`, // U+2006 SIX-PER-EM SPACE 22 | `\u2007`, // U+2007 FIGURE SPACE 23 | `\u2008`, // U+2008 PUNCTUATION SPACE 24 | `\u2009`, // U+2009 THIN SPACE 25 | //`\u200a`, // U+200A HAIR SPACE 26 | `\u202f`, // U+202F NARROW NO-BREAK SPACE 27 | `\u205f`, // U+205F MEDIUM MATHEMATICAL SPACE 28 | `\u3000`, // U+3000 IDEOGRAPHIC SPACE 29 | `\u2800`, // U+2800 BRAILLE PATTERN BLANK 30 | `\u3164`, // U+3164 HANGUL FILLER 31 | `\uffa0`, // U+FFA0 HALFWIDTH HANGUL FILLER 32 | ] 33 | 34 | const no_space_unicode = [ 35 | `\u034f`, // U+034F COMBINING GRAPHEME JOINER 36 | `\u061c`, // U+061C ARABIC LETTER MARK 37 | `\u17b4`, // U+17B4 KHMER VOWEL INHERENT AQ 38 | `\u17b5`, // U+17B5 KHMER VOWEL INHERENT AA 39 | `\u200a`, // U+200A HAIR SPACE 40 | `\u200b`, // U+200B ZERO WIDTH SPACE 41 | `\u200c`, // U+200C ZERO WIDTH NON-JOINER 42 | `\u200d`, // U+200D ZERO WIDTH JOINER 43 | `\u200e`, // U+200E LEFT-TO-RIGHT MARK 44 | `\u200f`, // U+200F RIGHT-TO-LEFT MARK 45 | `\u2060`, // U+2060 WORD JOINER 46 | `\u2061`, // U+2061 FUNCTION APPLICATION 47 | `\u2062`, // U+2062 INVISIBLE TIMES 48 | `\u2063`, // U+2063 INVISIBLE SEPARATOR 49 | `\u2064`, // U+2064 INVISIBLE PLUS 50 | `\u206a`, // U+206A INHIBIT SYMMETRIC SWAPPING 51 | `\u206b`, // U+206B ACTIVATE SYMMETRIC SWAPPING 52 | `\u206c`, // U+206C INHIBIT ARABIC FORM SHAPING 53 | `\u206d`, // U+206D ACTIVATE ARABIC FORM SHAPING 54 | `\u206e`, // U+206E NATIONAL DIGIT SHAPES 55 | `\u206f`, // U+206F NOMINAL DIGIT SHAPES 56 | `\ufeff`, // U+FEFF ZERO WIDTH NO-BREAK SPACE 57 | ] 58 | 59 | const invisible_unicode = [ 60 | `\u0009`, // U+0009 CHARACTER TABULATION 61 | `\u0020`, // U+0020 SPACE 62 | `\u00ad`, // U+00AD SOFT HYPHEN 63 | `\u034f`, // U+034F COMBINING GRAPHEME JOINER 64 | `\u061c`, // U+061C ARABIC LETTER MARK 65 | `\u115f`, // U+115F HANGUL CHOSEONG FILLER 66 | `\u1160`, // U+1160 HANGUL JUNGSEONG FILLER 67 | `\u17b4`, // U+17B4 KHMER VOWEL INHERENT AQ 68 | `\u17b5`, // U+17B5 KHMER VOWEL INHERENT AA 69 | `\u180e`, // U+180E MONGOLIAN VOWEL SEPARATOR 70 | `\u2000`, // U+2000 EN QUAD 71 | `\u2001`, // U+2001 EM QUAD 72 | `\u2002`, // U+2002 EN SPACE 73 | `\u2003`, // U+2003 EM SPACE 74 | `\u2004`, // U+2004 THREE-PER-EM SPACE 75 | `\u2005`, // U+2005 FOUR-PER-EM SPACE 76 | `\u2006`, // U+2006 SIX-PER-EM SPACE 77 | `\u2007`, // U+2007 FIGURE SPACE 78 | `\u2008`, // U+2008 PUNCTUATION SPACE 79 | `\u2009`, // U+2009 THIN SPACE 80 | `\u200a`, // U+200A HAIR SPACE 81 | `\u200b`, // U+200B ZERO WIDTH SPACE 82 | `\u200c`, // U+200C ZERO WIDTH NON-JOINER 83 | `\u200d`, // U+200D ZERO WIDTH JOINER 84 | `\u200e`, // U+200E LEFT-TO-RIGHT MARK 85 | `\u200f`, // U+200F RIGHT-TO-LEFT MARK 86 | `\u202f`, // U+202F NARROW NO-BREAK SPACE 87 | `\u205f`, // U+205F MEDIUM MATHEMATICAL SPACE 88 | `\u2060`, // U+2060 WORD JOINER 89 | `\u2061`, // U+2061 FUNCTION APPLICATION 90 | `\u2062`, // U+2062 INVISIBLE TIMES 91 | `\u2063`, // U+2063 INVISIBLE SEPARATOR 92 | `\u2064`, // U+2064 INVISIBLE PLUS 93 | `\u206a`, // U+206A INHIBIT SYMMETRIC SWAPPING 94 | `\u206b`, // U+206B ACTIVATE SYMMETRIC SWAPPING 95 | `\u206c`, // U+206C INHIBIT ARABIC FORM SHAPING 96 | `\u206d`, // U+206D ACTIVATE ARABIC FORM SHAPING 97 | `\u206e`, // U+206E NATIONAL DIGIT SHAPES 98 | `\u206f`, // U+206F NOMINAL DIGIT SHAPES 99 | `\u3000`, // U+3000 IDEOGRAPHIC SPACE 100 | `\u2800`, // U+2800 BRAILLE PATTERN BLANK 101 | `\u3164`, // U+3164 HANGUL FILLER 102 | `\ufeff`, // U+FEFF ZERO WIDTH NO-BREAK SPACE 103 | `\uffa0`, // U+FFA0 HALFWIDTH HANGUL FILLER 104 | /* 105 | `\u1d159`, // U+1D159 MUSICAL SYMBOL NULL NOTEHEAD 106 | `\u1d173`, // U+1D173 MUSICAL SYMBOL BEGIN BEAM 107 | `\u1d174`, // U+1D174 MUSICAL SYMBOL END BEAM 108 | `\u1d175`, // U+1D175 MUSICAL SYMBOL BEGIN TIE 109 | `\u1d176`, // U+1D176 MUSICAL SYMBOL END TIE 110 | `\u1d177`, // U+1D177 MUSICAL SYMBOL BEGIN SLUR 111 | `\u1d178`, // U+1D178 MUSICAL SYMBOL END SLUR 112 | `\u1d179`, // U+1D179 MUSICAL SYMBOL BEGIN PHRASE 113 | `\u1d17a`, // U+1D17A MUSICAL SYMBOL END PHRASE 114 | */ 115 | ] 116 | 117 | struct App { 118 | mut: 119 | tui &ui.Context = unsafe { nil } 120 | ed &vee.Vee = unsafe { nil } 121 | file string 122 | status string 123 | status_timeout int 124 | footer_height int = 2 125 | viewport int 126 | debug_mode bool = true 127 | } 128 | 129 | fn (mut a App) set_status(msg string, duration_ms int) { 130 | a.status = msg 131 | a.status_timeout = duration_ms 132 | } 133 | 134 | fn (mut a App) undo() { 135 | if a.ed.undo() { 136 | a.set_status('Undid', 2000) 137 | } 138 | } 139 | 140 | fn (mut a App) redo() { 141 | if a.ed.redo() { 142 | a.set_status('Redid', 2000) 143 | } 144 | } 145 | 146 | fn (mut a App) save() { 147 | if a.file.len > 0 { 148 | b := a.ed.active_buffer() 149 | os.write_file(a.file, b.raw()) or { panic(err) } 150 | a.set_status('Saved', 2000) 151 | } else { 152 | a.set_status('No file loaded', 4000) 153 | } 154 | } 155 | 156 | fn (a &App) view_height() int { 157 | return a.tui.window_height - a.footer_height - 1 158 | } 159 | 160 | fn (mut a App) footer() { 161 | w, h := a.tui.window_width, a.tui.window_height 162 | // term.set_cursor_position({x: 0, y: h-1}) 163 | 164 | mut b := a.ed.active_buffer() 165 | 166 | mut finfo := '' 167 | if a.file.len > 0 { 168 | finfo = ' (' + os.file_name(a.file) + ')' 169 | } 170 | 171 | mut status := a.status 172 | a.tui.draw_text(0, h - 1, '─'.repeat(w)) 173 | footer := '${finfo} Line ${b.cursor.pos.y + 1:4}/${b.lines.len:-4}, Column ${b.cursor.pos.x + 1:3}/${b.cur_line().len:-3} index: ${b.cursor_index():5} (ESC = quit, Ctrl+s = save)' 174 | if footer.len < w { 175 | a.tui.draw_text((w - footer.len) / 2, h, footer) 176 | } else if footer.len == w { 177 | a.tui.draw_text(0, h, footer) 178 | } else { 179 | a.tui.draw_text(0, h, footer[..w]) 180 | } 181 | if a.status_timeout <= 0 { 182 | status = '' 183 | } else { 184 | a.tui.set_bg_color( 185 | r: 200 186 | g: 200 187 | b: 200 188 | ) 189 | a.tui.set_color( 190 | r: 0 191 | g: 0 192 | b: 0 193 | ) 194 | a.tui.draw_text((w + 4 - status.len) / 2, h - 1, ' ${status} ') 195 | a.tui.reset() 196 | 197 | if a.status_timeout <= 0 { 198 | a.status_timeout = 0 199 | } else { 200 | a.status_timeout -= int(1000 / 60) // a.tui.cfg.frame_rate 201 | } 202 | } 203 | 204 | if a.status_timeout <= 0 { 205 | status = '' 206 | } 207 | } 208 | 209 | fn (mut a App) dbg_overlay() { 210 | if !a.debug_mode { 211 | return 212 | } 213 | w, h := a.tui.window_width, a.tui.window_height 214 | w050 := int(f32(w) * 0.5) 215 | // term.set_cursor_position({x: w050, y: 0}) 216 | mut b := a.ed.active_buffer() 217 | 218 | cur_line := b.cur_line_flat().runes() 219 | line_snippet := if cur_line.len > w050 - 30 { cur_line[..w050 - 30 - 1] } else { cur_line }.string() 220 | // buffer_flat := b.flat() 221 | // buffer_snippet := if flat.len > w050-30 { flat[..w050-30] } else { flat } 222 | 223 | text := 'PID ${os.getpid()} 224 | Char bytes "${b.cur_char().bytes()}" 225 | PrevChar "${literal(b.prev_char())}/${east_asian.display_width(b.prev_char(), 226 | 1)}" 227 | Char "${literal(b.cur_char())}/${east_asian.display_width(b.cur_char(), 228 | 1)}" 229 | Slice "${literal(b.cur_slice())} ${b.cur_slice().len}/${b.cur_slice().runes().len}/${east_asian.display_width(b.cur_slice(), 230 | 1)}" 231 | Line "${line_snippet} ${line_snippet.len}/${line_snippet.runes().len}/${east_asian.display_width(line_snippet, 232 | 1)}" 233 | EOL ${b.eol()} 234 | EOF ${b.eof()} 235 | Buffer lines ${b.lines.len} 236 | ${flatten(b.cursor.str())} 237 | ${flatten(b.magnet.str())} 238 | ' 239 | a.tui.reset_bg_color() 240 | a.tui.reset_color() 241 | a.tui.draw_rect(w050, 0, w, h - a.footer_height) 242 | lines := text.split('\n') 243 | for i, line in lines { 244 | a.tui.draw_text(w050 + 2, 1 + i, line) 245 | } 246 | a.tui.set_bg_color( 247 | r: 200 248 | g: 200 249 | b: 200 250 | ) 251 | a.tui.draw_line(w050, 0, w050, h - a.footer_height) 252 | a.tui.reset_bg_color() 253 | } 254 | 255 | fn flatten(s string) string { 256 | return s.replace('\t', ' ').replace(' ', ' ').replace('\n', ' ').replace(' ', ' ').replace(' ', 257 | ' ').replace(' ', ' ') 258 | } 259 | 260 | fn literal(s string) string { 261 | return s.replace('\t', r'\t').replace('\n', r'\n') 262 | } 263 | 264 | fn init(x voidptr) { 265 | mut a := unsafe { &App(x) } 266 | a.ed = vee.new(vee.VeeConfig{}) 267 | mut init_x := 0 268 | mut init_y := 0 269 | if a.file.len > 0 { 270 | if !os.is_file(a.file) && a.file.contains(':') { 271 | // support the file:line:col: format 272 | fparts := a.file.split(':') 273 | if fparts.len > 0 { 274 | a.file = fparts[0] 275 | } 276 | if fparts.len > 1 { 277 | init_y = fparts[1].int() - 1 278 | } 279 | if fparts.len > 2 { 280 | init_x = fparts[2].int() - 1 281 | } 282 | } 283 | if os.is_file(a.file) { 284 | a.tui.set_window_title(a.file) 285 | mut b := a.ed.active_buffer() 286 | content := os.read_file(a.file) or { panic(err) } 287 | b.put(content) 288 | b.cursor_to(init_x, init_y) 289 | } 290 | } 291 | } 292 | 293 | fn frame(x voidptr) { 294 | mut a := unsafe { &App(x) } 295 | mut buf := a.ed.active_buffer() 296 | a.tui.clear() 297 | scroll_limit := a.view_height() 298 | // scroll down 299 | if buf.cursor.pos.y > a.viewport + scroll_limit { // scroll down 300 | a.viewport = buf.cursor.pos.y - scroll_limit 301 | } else if buf.cursor.pos.y < a.viewport { // scroll up 302 | a.viewport = buf.cursor.pos.y 303 | } 304 | view := a.ed.view(a.viewport, scroll_limit + a.viewport) 305 | 306 | a.tui.draw_text(0, 0, view.raw) 307 | a.footer() 308 | a.dbg_overlay() 309 | 310 | mut ch_x := view.cursor.pos.x 311 | 312 | mut sl := buf.cur_slice().replace('\t', ' '.repeat(buf.tab_width)) 313 | if sl.len > 0 { 314 | sl = sl.runes().filter(it !in no_space_unicode).string() 315 | ch_x = east_asian.display_width(sl, 1) 316 | } 317 | ch_x++ 318 | 319 | a.tui.set_cursor_position(ch_x, buf.cursor.pos.y + 1 - a.viewport) 320 | a.tui.flush() 321 | } 322 | 323 | fn cleanup(x voidptr) { 324 | mut a := unsafe { &App(x) } 325 | a.ed.free() 326 | unsafe { 327 | free(a) 328 | } 329 | } 330 | 331 | fn fail(error string) { 332 | eprintln(error) 333 | } 334 | 335 | fn event(e &ui.Event, x voidptr) { 336 | mut a := unsafe { &App(x) } 337 | eprintln(e) 338 | if e.typ == .key_down { 339 | match e.code { 340 | .escape { 341 | term.set_cursor_position(x: 0, y: 0) 342 | exit(0) 343 | } 344 | .backspace { 345 | a.ed.del(-1) 346 | } 347 | .delete { 348 | a.ed.del(1) 349 | } 350 | .left { 351 | if e.modifiers == .ctrl { 352 | a.ed.move_to_word(.left) 353 | } else { 354 | a.ed.move_cursor(1, .left) 355 | } 356 | } 357 | .right { 358 | if e.modifiers == .ctrl { 359 | a.ed.move_to_word(.right) 360 | } else { 361 | a.ed.move_cursor(1, .right) 362 | } 363 | } 364 | .up { 365 | a.ed.move_cursor(1, .up) 366 | } 367 | .down { 368 | a.ed.move_cursor(1, .down) 369 | } 370 | .page_up { 371 | a.ed.move_cursor(a.view_height(), .page_up) 372 | } 373 | .page_down { 374 | a.ed.move_cursor(a.view_height(), .page_down) 375 | } 376 | .home { 377 | a.ed.move_cursor(1, .home) 378 | } 379 | .end { 380 | a.ed.move_cursor(1, .end) 381 | } 382 | 48...57, 97...122 { // 0-9a-zA-Z 383 | if e.modifiers.has(.ctrl) { 384 | if e.code == .d { 385 | a.debug_mode = !a.debug_mode 386 | } 387 | if e.code == .s { 388 | a.save() 389 | } 390 | if e.code == .z { 391 | a.undo() 392 | } 393 | if e.code == .y { 394 | a.redo() 395 | } 396 | } else if e.modifiers.has(.alt) { 397 | if e.code == .z { 398 | a.redo() 399 | } 400 | } else if e.modifiers.has(.shift) || e.modifiers.is_empty() { 401 | a.ed.put(e.ascii.ascii_str()) 402 | } 403 | } 404 | else { 405 | a.ed.put(e.utf8) 406 | } 407 | } 408 | } else if e.typ == .mouse_scroll { 409 | direction := if e.direction == .up { vee.Movement.down } else { vee.Movement.up } 410 | a.ed.move_cursor(3, direction) 411 | } 412 | } 413 | 414 | fn main() { 415 | mut file := '' 416 | if os.args.len > 1 { 417 | file = os.args[1] 418 | } 419 | mut a := &App{ 420 | file: file 421 | } 422 | a.tui = ui.init( 423 | user_data: a 424 | init_fn: init 425 | frame_fn: frame 426 | cleanup_fn: cleanup 427 | event_fn: event 428 | fail_fn: fail 429 | capture_events: true 430 | frame_rate: 60 431 | ) 432 | a.tui.run() or { panic(err) } 433 | } 434 | -------------------------------------------------------------------------------- /magnet.v: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2022 Lars Pontoppidan. All rights reserved. 2 | // Use of this source code is governed by the MIT license distributed with this software. 3 | module vee 4 | 5 | struct Magnet { 6 | mut: 7 | buffer &Buffer = unsafe { nil } 8 | // record bool = true 9 | x int 10 | } 11 | 12 | // str returns a string representation of the `Magnet`. 13 | pub fn (m Magnet) str() string { 14 | mut s := @MOD + '.Magnet{ 15 | x: ${m.x}' 16 | s += '\n}' 17 | return s 18 | } 19 | 20 | // activate will adjust the cursor to as close valuses as the magnet as possible 21 | pub fn (mut m Magnet) activate() { 22 | if m.x == 0 || isnil(m.buffer) { 23 | return 24 | } 25 | mut b := m.buffer 26 | // x, _ := m.buffer.cursor.xy() 27 | // line := b.cur_line() 28 | 29 | // if line.len == 0 { 30 | // b.cursor.pos.x = 0 31 | //} else { 32 | b.cursor.pos.x = m.x 33 | //} 34 | b.sync_cursor() 35 | } 36 | 37 | // record will record the placement of the cursor 38 | fn (mut m Magnet) record() { 39 | //!m.record || 40 | if isnil(m.buffer) { 41 | return 42 | } 43 | m.x = m.buffer.cursor.pos.x 44 | } 45 | 46 | /* 47 | fn (mut m Magnet) move_offrecord(amount int, movement Movement) { 48 | if isnil(m.buffer) { return } 49 | prev_recording_state := m.record 50 | m.record = false 51 | m.buffer.move_cursor(amount, movement) 52 | m.record = prev_recording_state 53 | }*/ 54 | -------------------------------------------------------------------------------- /math.v: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2022 Lars Pontoppidan. All rights reserved. 2 | // Use of this source code is governed by the MIT license distributed with this software. 3 | module vee 4 | 5 | @[inline] 6 | fn imax(x int, y int) int { 7 | return if x < y { y } else { x } 8 | } 9 | 10 | @[inline] 11 | fn imin(x int, y int) int { 12 | return if x < y { x } else { y } 13 | } 14 | -------------------------------------------------------------------------------- /tests/small_test.v: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | import vee 4 | 5 | fn test_basics() { 6 | mut ed := vee.new(vee.VeeConfig{}) 7 | mut buf := ed.active_buffer() 8 | 9 | ed.put('Hello World') 10 | assert buf.flat() == 'Hello World' 11 | 12 | ed.del(-1) // 'Hello Worl' 13 | assert buf.flat() == 'Hello Worl' 14 | 15 | ed.del(-5) // 'Hello' 16 | assert buf.flat() == 'Hello' 17 | 18 | ed.move_cursor(1, .left) //@ 19 | ed.del(1) // 'Hell' 20 | assert buf.flat() == 'Hell' 21 | 22 | ed.put('\nand hallo Vee') 23 | // println('"${buf.flat()}" (${buf.cursor.pos.x},${buf.cursor.pos.y})/${buf.cursor_index()}') 24 | // 'Hell' 25 | // 'and hallo Vee' 26 | assert buf.flat() == r'Hell\nand hallo Vee' 27 | 28 | ed.move_cursor(4, .left) //@< >Vee 29 | ed.del(4) 30 | // 'Hell' 31 | // 'and hallo' 32 | assert buf.flat() == r'Hell\nand hallo' 33 | 34 | ed.move_cursor(9, .left) //@nd hal... 35 | ed.del(-1) // 'Helland hallo' 36 | assert buf.flat() == 'Helland hallo' 37 | // println('"${buf.flat()}" (${buf.cursor.pos.x},${buf.cursor.pos.y}/${buf.cursor_index()})') 38 | 39 | ed.move_cursor(1, .home) //@elland ... 40 | // println('"${buf.raw()}" (${buf.cursor.pos.x},${buf.cursor.pos.y}/${buf.cursor_index()})') 41 | ed.del(9) // 'allo' 42 | ed.put('H') // 'H█allo' 43 | assert buf.flat() == 'Hallo' 44 | 45 | ed.move_cursor(1, .end) 46 | ed.put(' again') // 'Hallo again█' 47 | assert buf.flat() == 'Hallo again' 48 | 49 | ed.put('\nTEST') // 'Hallo again\nTEST█' 50 | // println('"${buf.flat()}" (${buf.cursor.pos.x},${buf.cursor.pos.y}/${buf.cursor_index()})') 51 | 52 | ed.undo() // Undo all put commands so far (which is ' again','\nTEST') 53 | assert buf.flat() == 'Hallo' 54 | 55 | ed.redo() 56 | assert buf.flat() == r'Hallo again\nTEST' 57 | 58 | ed.undo() 59 | assert buf.flat() == 'Hallo' 60 | 61 | ed.put('\n') // 'Hallo\n█' 62 | assert buf.flat() == r'Hallo\n' 63 | ed.put_line_break() // 'Hallo\n\n█' 64 | assert buf.flat() == r'Hallo\n\n' 65 | ed.put_line_break() // 'Hallo\n\n█' 66 | assert buf.flat() == r'Hallo\n\n\n' 67 | 68 | ed.undo() 69 | assert buf.flat() == 'Hallo' 70 | 71 | // println('"${buf.raw()}" (${buf.cursor.pos.x},${buf.cursor.pos.y}/${buf.cursor_index()})') 72 | 73 | ed.free() 74 | } 75 | -------------------------------------------------------------------------------- /tests/unicode_test.v: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | import vee 4 | 5 | fn test_unicode() { 6 | mut ed := vee.new(vee.VeeConfig{}) 7 | buf := ed.active_buffer() 8 | 9 | ed.put('Hello World') 10 | assert buf.flat() == 'Hello World' 11 | 12 | assert buf.cursor_index() == 11 13 | 14 | ed.del(-5) // 'Hello ' 15 | assert buf.cursor_index() == 6 16 | ed.put('🌐') 17 | assert buf.flat() == 'Hello 🌐' 18 | assert buf.cursor_index() == 7 19 | 20 | ed.del(-1) // 'Hello ' 21 | assert buf.cursor_index() == 6 22 | ed.put('World') 23 | assert buf.flat() == 'Hello World' 24 | assert buf.cursor_index() == 11 25 | 26 | ed.del(-5) // 'Hello ' 27 | assert buf.cursor_index() == 6 28 | ed.put('🌐 and 🌐') 29 | assert buf.flat() == 'Hello 🌐 and 🌐' 30 | assert buf.cursor_index() == 13 31 | 32 | ed.move_cursor(7, .left) //@<🌐> and... 33 | assert buf.cursor_index() == 6 34 | ed.del(2) // 'Hello and 🌐' 35 | assert buf.cursor_index() == 6 36 | assert buf.flat() == 'Hello and 🌐' 37 | 38 | // Hello |and 🌐 39 | ed.put('"ƒ ✔ ❤ ☆" ') 40 | assert buf.flat() == 'Hello "ƒ ✔ ❤ ☆" and 🌐' 41 | 42 | // Hello "ƒ ✔ ❤ ☆" |and 🌐 43 | assert buf.cursor_index() == 16 44 | 45 | ed.del(-1) // 'Hello "ƒ ✔ ❤ ☆"and 🌐' 46 | assert buf.flat() == 'Hello "ƒ ✔ ❤ ☆"and 🌐' 47 | ed.del(5) // 'Hello "ƒ ✔ ❤ ☆"' 48 | assert buf.flat() == 'Hello "ƒ ✔ ❤ ☆"' 49 | 50 | assert buf.cursor_index() == 15 51 | 52 | ed.move_cursor(9, .left) 53 | ed.del(9) // 'Hello ' 54 | assert buf.flat() == 'Hello ' 55 | 56 | ed.put('ƒ ✔ ❤') 57 | assert buf.flat() == 'Hello ƒ ✔ ❤' 58 | 59 | // Hello| ƒ ✔ ❤ 60 | ed.move_to_word(.left) 61 | assert buf.cursor_index() == 5 62 | } 63 | -------------------------------------------------------------------------------- /types.v: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2022 Lars Pontoppidan. All rights reserved. 2 | // Use of this source code is governed by the MIT license distributed with this software. 3 | module vee 4 | 5 | pub enum Movement { 6 | up 7 | down 8 | left 9 | right 10 | page_up 11 | page_down 12 | home 13 | end 14 | } 15 | 16 | pub enum Mode { 17 | edit 18 | @select 19 | } 20 | 21 | type InputType = rune | string | u8 22 | 23 | fn (ipt InputType) len() int { 24 | match ipt { 25 | u8, rune { 26 | return 1 27 | } 28 | string { 29 | return ipt.len 30 | } 31 | } 32 | } 33 | 34 | fn (ipt InputType) str() string { 35 | match ipt { 36 | u8 { 37 | return ipt.ascii_str() 38 | } 39 | rune { 40 | return [ipt].string() 41 | } 42 | string { 43 | return ipt.str() 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /utils.v: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2022 Lars Pontoppidan. All rights reserved. 2 | // Use of this source code is governed by the MIT license distributed with this software. 3 | module vee 4 | 5 | @[if debug_vee ?] 6 | fn dbg(str string) { 7 | eprintln(str) 8 | } 9 | -------------------------------------------------------------------------------- /v.mod: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /vee.v: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020-2022 Lars Pontoppidan. All rights reserved. 2 | // Use of this source code is governed by the MIT license distributed with this software. 3 | module vee 4 | 5 | import strings 6 | import vee.command 7 | 8 | @[heap] 9 | pub struct Vee { 10 | mut: 11 | buffers []&Buffer 12 | active_buffer_id int 13 | invoker command.Invoker 14 | } 15 | 16 | pub struct View { 17 | pub: 18 | raw string 19 | cursor Cursor 20 | } 21 | 22 | pub struct VeeConfig { 23 | } 24 | 25 | // new returns a new heap-allocated `Vee` instance. 26 | pub fn new(config VeeConfig) &Vee { 27 | ed := &Vee{} 28 | return ed 29 | } 30 | 31 | // view returns a `View` of the buffer between `from` and `to`. 32 | pub fn (mut v Vee) view(from int, to int) View { 33 | mut b := v.active_buffer() 34 | 35 | b.magnet.activate() 36 | 37 | slice := b.cur_slice().runes() 38 | mut tabs := 0 39 | mut vx := 0 40 | for i := 0; i < slice.len; i++ { 41 | if slice[i] == `\t` { 42 | vx += b.tab_width 43 | tabs++ 44 | continue 45 | } 46 | vx++ 47 | } 48 | x := vx 49 | 50 | /* 51 | if tabs > 0 && x > b.magnet.x { 52 | x = b.magnet.x 53 | b.cursor.pos.x = x 54 | }*/ 55 | 56 | mut lines := []string{} 57 | for i, line in b.lines { 58 | if i >= from && i <= to { 59 | lines << line 60 | } 61 | } 62 | raw := lines.join(b.line_break) 63 | return View{ 64 | raw: raw.replace('\t', strings.repeat(` `, b.tab_width)) 65 | cursor: Cursor{ 66 | pos: Position{ 67 | x: x 68 | y: b.cursor.pos.y 69 | } 70 | } 71 | } 72 | } 73 | 74 | // free frees resources from this `Vee` instance. 75 | pub fn (mut v Vee) free() { 76 | dbg(@MOD + '.' + @STRUCT + '::' + @FN) 77 | unsafe { 78 | for b in v.buffers { 79 | b.free() 80 | free(b) 81 | } 82 | v.buffers.free() 83 | } 84 | } 85 | 86 | // new_buffer creates a new buffer and returns it's id for later reference. 87 | pub fn (mut v Vee) new_buffer() int { 88 | b := new_buffer(BufferConfig{}) 89 | return v.add_buffer(b) 90 | } 91 | 92 | // buffer_at returns the `Buffer` instance with `id`. 93 | pub fn (mut v Vee) buffer_at(id int) &Buffer { 94 | mut buf_idx := id 95 | // dbg(@MOD+'.'+@STRUCT+'::'+@FN+' get buffer $id/${v.buffers.len}') 96 | if v.buffers.len == 0 { 97 | // Add default buffer 98 | buf_idx = v.new_buffer() 99 | dbg(@MOD + '.' + @STRUCT + '::' + @FN + ' added initial buffer') 100 | } 101 | if buf_idx < 0 || buf_idx >= v.buffers.len { 102 | dbg(@MOD + '.' + @STRUCT + '::' + @FN + ' invalid index "${buf_idx}". Returning active') 103 | // TODO also check that the active index can be reached 104 | buf_idx = v.active_buffer_id 105 | } 106 | return v.buffers[buf_idx] 107 | } 108 | 109 | // active_buffer returns the currently active `Buffer` instance. 110 | pub fn (mut v Vee) active_buffer() &Buffer { 111 | return v.buffer_at(v.active_buffer_id) 112 | } 113 | 114 | // dmp dumps all buffers to std_err. 115 | pub fn (v Vee) dmp() { 116 | for buffer in v.buffers { 117 | buffer.dmp() 118 | } 119 | } 120 | 121 | // add_buffer adds the `Buffer` `b` and returns it's `id`. 122 | pub fn (mut v Vee) add_buffer(b &Buffer) int { 123 | v.buffers << b 124 | // TODO signal_buffer_added(b) 125 | return v.buffers.len - 1 // buffers.len-1, i.e. the index serves as the id 126 | } 127 | 128 | /* 129 | * Cursor movement 130 | */ 131 | // cursor_to move the cursor position to `pos`. 132 | pub fn (mut v Vee) cursor_to(pos Position) { 133 | mut b := v.active_buffer() 134 | b.cursor_to(pos.x, pos.y) 135 | } 136 | 137 | // move_cursor navigates the cursor within the buffer bounds 138 | pub fn (mut v Vee) move_cursor(amount int, movement Movement) { 139 | // TODO CRITICAL it should be on the stack but there's a bug with interfaces preventing/corrupting the value of "vee" 140 | // NOTE that these aren't freed 141 | // See: https://discord.com/channels/592103645835821068/592294828432424960/842463741308436530 142 | mut cmd := &MoveCursorCmd{ 143 | buffer: v.active_buffer() 144 | amount: amount 145 | movement: movement 146 | } 147 | v.invoker.add_and_execute(cmd) 148 | } 149 | 150 | // move_to_word navigates the cursor to the nearst word in the given direction. 151 | pub fn (mut v Vee) move_to_word(movement Movement) { 152 | // v.active_buffer().move_to_word(movement) 153 | 154 | // TODO CRITICAL it should be on the stack but there's a bug with interfaces preventing/corrupting the value of "vee" 155 | // NOTE that these aren't freed 156 | // See: https://discord.com/channels/592103645835821068/592294828432424960/842463741308436530 157 | mut cmd := &MoveToWordCmd{ 158 | buffer: v.active_buffer() 159 | movement: movement 160 | } 161 | v.invoker.add_and_execute(cmd) 162 | } 163 | 164 | /* 165 | * Undo/redo -able buffer commands 166 | */ 167 | // put adds `input` to the active `Buffer`. 168 | pub fn (mut v Vee) put(input InputType) { 169 | // TODO CRITICAL it should be on the stack but there's a bug with interfaces preventing/corrupting the value of "vee" 170 | // NOTE that these aren't freed 171 | // See: https://discord.com/channels/592103645835821068/592294828432424960/842463741308436530 172 | b := v.active_buffer() 173 | if input is string && input.str() == b.line_break { 174 | mut cmd := &PutLineBreakCmd{ 175 | buffer: b 176 | } 177 | v.invoker.add(cmd) 178 | } else { 179 | mut cmd := &PutCmd{ 180 | buffer: b 181 | input: input 182 | } 183 | v.invoker.add(cmd) 184 | } 185 | v.invoker.execute() 186 | } 187 | 188 | // put_line_break adds a line break to the active `Buffer`. 189 | pub fn (mut v Vee) put_line_break() { 190 | // TODO CRITICAL it should be on the stack but there's a bug with interfaces preventing/corrupting the value of "vee" 191 | // NOTE that these aren't freed 192 | // See: https://discord.com/channels/592103645835821068/592294828432424960/842463741308436530 193 | mut cmd := &PutLineBreakCmd{ 194 | buffer: v.active_buffer() 195 | } 196 | v.invoker.add_and_execute(cmd) 197 | } 198 | 199 | // del deletes `amount` of characters from the active `Buffer`. 200 | // An `amount` > 0 will delete `amount` characters to the *right* of the cursor. 201 | // An `amount` < 0 will delete `amount` characters to the *left* of the cursor. 202 | pub fn (mut v Vee) del(amount int) { 203 | // TODO CRITICAL it should be on the stack but there's a bug with interfaces preventing/corrupting the value of "vee" 204 | // NOTE that these aren't freed 205 | // See: https://discord.com/channels/592103645835821068/592294828432424960/842463741308436530 206 | mut cmd := &DelCmd{ 207 | buffer: v.active_buffer() 208 | amount: amount 209 | } 210 | v.invoker.add_and_execute(cmd) 211 | } 212 | 213 | // undo undo the last executed command. 214 | pub fn (mut v Vee) undo() bool { 215 | dbg(@MOD + '.' + @STRUCT + '::' + @FN) 216 | 217 | mut cmd := v.invoker.undo() or { return false } 218 | 219 | match cmd { 220 | MoveCursorCmd, MoveToWordCmd { 221 | cmd = v.invoker.peek(.undo) or { return true } 222 | for cmd is MoveCursorCmd || cmd is MoveToWordCmd { 223 | dbg(@MOD + '.' + @STRUCT + '::' + @FN + ' MoveXXXCmd streak') 224 | 225 | v.invoker.undo() or { return true } 226 | cmd = v.invoker.peek(.undo) or { return true } 227 | } 228 | } 229 | PutCmd { 230 | cmd = v.invoker.peek(.undo) or { return true } 231 | for cmd is PutCmd { 232 | dbg(@MOD + '.' + @STRUCT + '::' + @FN + ' PutCmd streak') 233 | 234 | v.invoker.undo() or { return true } 235 | cmd = v.invoker.peek(.undo) or { return true } 236 | } 237 | } 238 | PutLineBreakCmd { 239 | cmd = v.invoker.peek(.undo) or { return true } 240 | for cmd is PutLineBreakCmd { 241 | dbg(@MOD + '.' + @STRUCT + '::' + @FN + ' PutLineBreakCmd streak') 242 | 243 | v.invoker.undo() or { return true } 244 | cmd = v.invoker.peek(.undo) or { return true } 245 | } 246 | } 247 | DelCmd { 248 | cmd = v.invoker.peek(.undo) or { return true } 249 | for cmd is DelCmd { 250 | dbg(@MOD + '.' + @STRUCT + '::' + @FN + ' DelCmd streak') 251 | 252 | v.invoker.undo() or { return true } 253 | cmd = v.invoker.peek(.undo) or { return true } 254 | } 255 | } 256 | else { 257 | return true 258 | } 259 | } 260 | 261 | return true 262 | } 263 | 264 | // redo redo the last undone command. 265 | pub fn (mut v Vee) redo() bool { 266 | dbg(@MOD + '.' + @STRUCT + '::' + @FN) 267 | 268 | mut cmd := v.invoker.redo() or { return false } 269 | match cmd { 270 | MoveCursorCmd, MoveToWordCmd { 271 | cmd = v.invoker.peek(.redo) or { return true } 272 | for cmd is MoveCursorCmd || cmd is MoveToWordCmd { 273 | dbg(@MOD + '.' + @STRUCT + '::' + @FN + ' MoveXXXCmd streak') 274 | 275 | v.invoker.redo() or { return true } 276 | cmd = v.invoker.peek(.redo) or { return true } 277 | } 278 | } 279 | PutCmd { 280 | cmd = v.invoker.peek(.redo) or { return true } 281 | for cmd is PutCmd { 282 | dbg(@MOD + '.' + @STRUCT + '::' + @FN + ' PutCmd streak') 283 | 284 | v.invoker.redo() or { return true } 285 | cmd = v.invoker.peek(.redo) or { return true } 286 | } 287 | } 288 | PutLineBreakCmd { 289 | cmd = v.invoker.peek(.redo) or { return true } 290 | for cmd is PutLineBreakCmd { 291 | dbg(@MOD + '.' + @STRUCT + '::' + @FN + ' PutLineBreakCmd streak') 292 | 293 | v.invoker.redo() or { return true } 294 | cmd = v.invoker.peek(.redo) or { return true } 295 | } 296 | } 297 | DelCmd { 298 | cmd = v.invoker.peek(.redo) or { return true } 299 | for cmd is DelCmd { 300 | dbg(@MOD + '.' + @STRUCT + '::' + @FN + ' DelCmd streak') 301 | 302 | v.invoker.redo() or { return true } 303 | cmd = v.invoker.peek(.redo) or { return true } 304 | } 305 | } 306 | else { 307 | return true 308 | } 309 | } 310 | return true 311 | } 312 | --------------------------------------------------------------------------------