TUI editor and editor backend written in Zig
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

add rudimentary support for selections

reykjalin.org e35cdfba 220cd30b

verified
+117
+50
src/libfn/Editor.zig
··· 353 353 } 354 354 } 355 355 356 + pub fn extendSelectionsRight(self: *Editor) void { 357 + for (self.selections.items) |*s| { 358 + s.cursor.col = s.cursor.col +| 1; 359 + 360 + const line = self.getLine(s.cursor.row); 361 + 362 + if (s.isCursor()) s.anchor.col = @min(s.anchor.col -| 1, line.len); 363 + 364 + if (s.cursor.row >= self.lineCount() and s.cursor.col > line.len) { 365 + s.cursor.col = line.len; 366 + } else if (s.cursor.col > line.len) { 367 + s.cursor.row = s.cursor.row +| 1; 368 + s.cursor.col = 0; 369 + } 370 + } 371 + } 372 + 373 + pub fn extendSelectionsLeft(self: *Editor) void { 374 + for (self.selections.items) |*s| { 375 + if (s.cursor.col == 0 and s.cursor.row == 0) continue; 376 + 377 + if (s.isCursor()) { 378 + const line = self.getLine(s.cursor.row); 379 + s.anchor.col = @min(s.anchor.col + 1, line.len); 380 + } 381 + 382 + if (s.cursor.col == 0) { 383 + s.cursor.row -= 1; 384 + const line = self.getLine(s.cursor.row); 385 + s.cursor.col = line.len; 386 + } else { 387 + s.cursor.col -= 1; 388 + } 389 + } 390 + } 391 + 392 + pub fn extendSelectionsDown(self: *Editor) void { 393 + for (self.selections.items) |*s| { 394 + if (s.cursor.row >= self.lineCount()) continue; 395 + s.cursor.row += 1; 396 + } 397 + } 398 + 399 + pub fn extendSelectionsUp(self: *Editor) void { 400 + for (self.selections.items) |*s| { 401 + if (s.cursor.row == 0) continue; 402 + s.cursor.row -= 1; 403 + } 404 + } 405 + 356 406 /// Selects the word that comes after each selection's cursor. Behavior varies depending on cursor 357 407 /// location for each selection: 358 408 ///
+67
src/tui/main.zig
··· 13 13 insert, 14 14 goto, 15 15 maybe_exit_insert, 16 + select, 16 17 }; 17 18 18 19 const Event = union(enum) { ··· 216 217 .maybe_exit_insert => {}, // Just keep whatever cursor is currently active. 217 218 .normal => ctx.root_win.setCursorShape(.block), 218 219 .goto => ctx.root_win.setCursorShape(.block), 220 + .select => ctx.root_win.setCursorShape(.block), 219 221 } 220 222 } 221 223 ··· 240 242 } 241 243 242 244 if (key.matches('g', .{})) state.mode = .goto; 245 + 246 + if (key.matches('v', .{})) state.mode = .select; 243 247 244 248 if (key.matches('h', .{})) state.editor.moveSelectionsLeft(); 245 249 if (key.matches(vaxis.Key.left, .{})) state.editor.moveSelectionsLeft(); ··· 297 301 if (key.text) |text| try state.editor.insertTextAtCursors(state.gpa, text); 298 302 state.mode = .insert; 299 303 } 304 + } else if (state.mode == .select) { 305 + if (key.matches('h', .{})) state.editor.extendSelectionsLeft(); 306 + if (key.matches('j', .{})) state.editor.extendSelectionsDown(); 307 + if (key.matches('k', .{})) state.editor.extendSelectionsUp(); 308 + if (key.matches('l', .{})) state.editor.extendSelectionsRight(); 309 + 310 + if (key.matches('d', .{})) { 311 + try state.editor.deleteInsideSelections(state.gpa); 312 + state.mode = .normal; 313 + } 314 + 315 + if (key.matches('v', .{})) state.mode = .normal; 316 + if (key.matches(vaxis.Key.escape, .{})) state.mode = .normal; 300 317 } 301 318 302 319 if (key.matches('s', .{ .super = true })) try state.editor.saveFile(state.gpa); ··· 337 354 else => {}, 338 355 } 339 356 357 + // Draw text. 358 + // FIXME: no need to draw to the end of the file. 340 359 for (state.v_scroll..state.editor.lineCount()) |idx| { 341 360 const line = state.editor.getLine(idx); 342 361 ··· 346 365 .{ .text = line[state.h_scroll..] }, 347 366 .{ .row_offset = @intCast(idx -| state.v_scroll), .wrap = .none }, 348 367 ); 368 + } 369 + 370 + // Draw selections. 371 + for (state.editor.selections.items) |s| { 372 + // If no part of this selection is visible we can skip rendering it. 373 + if ((s.cursor.row < state.v_scroll or s.cursor.row > state.v_scroll +| container.height) and 374 + (s.anchor.row < state.v_scroll or s.anchor.row > state.v_scroll +| container.height)) 375 + { 376 + continue; 377 + } 378 + 379 + const row_start = s.toRange().before().row -| state.v_scroll; 380 + const line_start = state.editor.getLine(row_start); 381 + const col_start = if (s.toRange().before().row < state.v_scroll) 382 + 0 383 + else 384 + @min(s.toRange().before().col -| state.h_scroll, line_start.len -| state.h_scroll); 385 + 386 + const row_end = @min(s.toRange().after().row -| state.v_scroll, state.v_scroll +| container.height); 387 + const line_end = state.editor.getLine(row_end); 388 + const col_end = if (s.toRange().after().row > state.v_scroll +| container.height) 389 + line_end.len -| state.h_scroll 390 + else 391 + s.toRange().after().col -| state.h_scroll; 392 + 393 + var row_it = row_start; 394 + 395 + std.debug.assert( 396 + row_start < row_end or 397 + (row_start == row_end and col_start <= col_end), 398 + ); 399 + 400 + while (row_it <= row_end) { 401 + const line = state.editor.getLine(row_it); 402 + 403 + const start = if (row_it == row_start) col_start else 0; 404 + const end = if (row_it == row_end) col_end else line.len -| state.h_scroll; 405 + 406 + for (start..end) |cell_col| { 407 + if (container.readCell(@intCast(cell_col), @intCast(row_it))) |cell| { 408 + var new_cell = cell; 409 + new_cell.style = .{ .reverse = true }; 410 + container.writeCell(@intCast(cell_col), @intCast(row_it), new_cell); 411 + } 412 + } 413 + 414 + row_it +|= 1; 415 + } 349 416 } 350 417 } 351 418