diff --git a/Makefile b/Makefile index ebd5741..dacbd56 100644 --- a/Makefile +++ b/Makefile @@ -7,6 +7,7 @@ editor: grep src/**/*.odin odin build src/ -out:bin/editor -debug grep: + cargo fmt --manifest-path "src/pkg/grep_lib/Cargo.toml" cargo build --manifest-path "src/pkg/grep_lib/Cargo.toml" test: src/**/*.odin diff --git a/src/core/core.odin b/src/core/core.odin index 15f9fe1..e3001da 100644 --- a/src/core/core.odin +++ b/src/core/core.odin @@ -103,6 +103,13 @@ FileBufferPanel :: struct { buffer: FileBuffer, viewed_symbol: Maybe(string), + search_buffer: FileBuffer, + query_arena: mem.Arena, + query_region: mem.Arena_Temp_Memory, + query_results: []GrepQueryResult, + selected_result: int, + is_searching: bool, + // only used for initialization file_path: string, line, col: int, diff --git a/src/core/file_buffer.odin b/src/core/file_buffer.odin index 5d807b2..7be2a7b 100644 --- a/src/core/file_buffer.odin +++ b/src/core/file_buffer.odin @@ -97,6 +97,22 @@ file_buffer_end :: proc(buffer: ^FileBuffer) -> Cursor { }; } +FileBufferIterResult :: struct { + character: u8, + done: bool, +} + +iterate_file_buffer_c :: proc "c" (it: ^FileBufferIter) -> FileBufferIterResult { + context = runtime.default_context() + + character, _, cond := iterate_file_buffer(it) + + return FileBufferIterResult { + character = character, + done = !cond, + } +} + iterate_file_buffer :: proc(it: ^FileBufferIter) -> (character: u8, idx: PieceTableIndex, cond: bool) { character, idx, cond = iterate_piece_table_iter(&it.piter) @@ -405,6 +421,27 @@ file_buffer_line_length :: proc(buffer: ^FileBuffer, index: PieceTableIndex) -> return line_length; } +move_cursor_to_location :: proc(buffer: ^FileBuffer, line, col: int, cursor: Maybe(^Cursor) = nil) { + cursor := cursor; + + if cursor == nil { + cursor = &buffer.history.cursor; + } + + it := new_file_buffer_iter(buffer); + for _ in iterate_file_buffer(&it) { + if (it.cursor.line == line && it.cursor.col >= col) || it.cursor.line > line { + break; + } + } + + cursor.?^ = it.cursor + + update_file_buffer_scroll(buffer, cursor) + + buffer.last_col = cursor.?.col +} + move_cursor_start_of_line :: proc(buffer: ^FileBuffer, cursor: Maybe(^Cursor) = nil) { cursor := cursor; @@ -541,13 +578,13 @@ move_cursor_left :: proc(buffer: ^FileBuffer, cursor: Maybe(^Cursor) = nil) { cursor = &buffer.history.cursor; } - buffer.last_col = cursor.?.col - if cursor.?.col > 0 { it := new_file_buffer_iter_with_cursor(buffer, cursor.?^); iterate_file_buffer_reverse(&it); cursor.?^ = it.cursor; } + + buffer.last_col = cursor.?.col } move_cursor_right :: proc(buffer: ^FileBuffer, stop_at_end: bool = true, amt: int = 1, cursor: Maybe(^Cursor) = nil) { @@ -557,8 +594,6 @@ move_cursor_right :: proc(buffer: ^FileBuffer, stop_at_end: bool = true, amt: in cursor = &buffer.history.cursor; } - buffer.last_col = cursor.?.col - it := new_file_buffer_iter_with_cursor(buffer, cursor.?^); line_length := file_buffer_line_length(buffer, it.cursor.index); @@ -568,6 +603,8 @@ move_cursor_right :: proc(buffer: ^FileBuffer, stop_at_end: bool = true, amt: in cursor.?^ = it.cursor; } } + + buffer.last_col = cursor.?.col } move_cursor_forward_start_of_word :: proc(buffer: ^FileBuffer, cursor: Maybe(^Cursor) = nil) { diff --git a/src/core/history.odin b/src/core/history.odin index 2e06d89..54e8332 100644 --- a/src/core/history.odin +++ b/src/core/history.odin @@ -102,6 +102,20 @@ recover_snapshot :: proc(history: ^FileHistory) { history.cursor = history.snapshots[history.next].cursor } +first_snapshot :: proc(history: ^FileHistory) { + context.allocator = history.allocator + + new_next :: 0 + + if history.snapshots[new_next].chunks == nil do return + history.next = new_next + + delete(history.piece_table.chunks) + + history.piece_table.chunks = clone_chunk(history.snapshots[history.next].chunks) + history.cursor = history.snapshots[history.next].cursor +} + clone_chunk :: proc(chunks: [dynamic][]u8) -> [dynamic][]u8 { new_chunks := make([dynamic][]u8, len(chunks), len(chunks)) diff --git a/src/panels/file_buffer.odin b/src/panels/file_buffer.odin index 785b0d7..604b9ce 100644 --- a/src/panels/file_buffer.odin +++ b/src/panels/file_buffer.odin @@ -3,6 +3,8 @@ package panels import "base:runtime" import "core:log" import "core:fmt" +import "core:mem" +import "core:strings" import "core:path/filepath" import "vendor:sdl2" @@ -12,6 +14,26 @@ import "../core" import "../ui" make_file_buffer_panel :: proc(file_path: string, line: int = 0, col: int = 0) -> core.Panel { + run_query :: proc(panel_state: ^core.FileBufferPanel, buffer: ^core.FileBuffer) { + if panel_state.query_region.arena != nil { + mem.end_arena_temp_memory(panel_state.query_region) + } + panel_state.query_region = mem.begin_arena_temp_memory(&panel_state.query_arena) + + context.allocator = mem.arena_allocator(&panel_state.query_arena) + + it := core.new_file_buffer_iter(buffer) + + rs_results := grep_buffer( + strings.clone_to_cstring(core.buffer_to_string(&panel_state.search_buffer)), + &it, + core.iterate_file_buffer_c + ); + + panel_state.selected_result = 0 + panel_state.query_results = rs_grep_as_results(&rs_results) + } + return core.Panel { type = core.FileBufferPanel { file_path = file_path, @@ -27,7 +49,16 @@ make_file_buffer_panel :: proc(file_path: string, line: int = 0, col: int = 0) - context.allocator = panel.allocator panel_state := &panel.type.(core.FileBufferPanel) + + arena_bytes, err := make([]u8, 1024*1024*2) + if err != nil { + log.errorf("failed to allocate arena for file buffer panel: '%v'", err) + return + } + mem.arena_init(&panel_state.query_arena, arena_bytes) + panel.input_map = core.new_input_map() + panel_state.search_buffer = core.new_virtual_file_buffer(panel.allocator) if len(panel_state.file_path) == 0 { panel_state.buffer = core.new_virtual_file_buffer(panel.allocator) @@ -62,31 +93,61 @@ make_file_buffer_panel :: proc(file_path: string, line: int = 0, col: int = 0) - buffer = proc(panel: ^core.Panel, state: ^core.State) -> (buffer: ^core.FileBuffer, ok: bool) { panel_state := &panel.type.(core.FileBufferPanel) - return &panel_state.buffer, true + if panel_state.is_searching { + return &panel_state.search_buffer, true + } else { + return &panel_state.buffer, true + } + }, + on_buffer_input = proc(panel: ^core.Panel, state: ^core.State) { + panel_state := &panel.type.(core.FileBufferPanel) + run_query(panel_state, &panel_state.buffer) }, render = proc(panel: ^core.Panel, state: ^core.State) -> (ok: bool) { panel_state := &panel.type.(core.FileBufferPanel) s := transmute(^ui.State)state.ui - render_file_buffer(state, s, &panel_state.buffer) - if viewed_symbol, ok := panel_state.viewed_symbol.?; ok { - ui.open_element(s, nil, - { - dir = .TopToBottom, - kind = {ui.Fit{}, ui.Fit{}}, - floating = true, - }, - style = { - background_color = .Background2, - }, - ) + ui.open_element(s, nil, { - ui.open_element(s, viewed_symbol, {}) + dir = .TopToBottom, + kind = {ui.Grow{}, ui.Grow{}}, + }, + ) + { + render_file_buffer(state, s, &panel_state.buffer) + if panel_state.is_searching { + ui.open_element(s, nil, + { + dir = .TopToBottom, + kind = {ui.Grow{}, ui.Exact(state.source_font_height)}, + }, + ) + { + render_raw_buffer(state, s, &panel_state.search_buffer) + } + ui.close_element(s) + } + + if viewed_symbol, ok := panel_state.viewed_symbol.?; ok { + ui.open_element(s, nil, + { + dir = .TopToBottom, + kind = {ui.Fit{}, ui.Fit{}}, + floating = true, + }, + style = { + background_color = .Background2, + }, + ) + { + ui.open_element(s, viewed_symbol, {}) + ui.close_element(s) + } ui.close_element(s) } - ui.close_element(s) } + ui.close_element(s) return true } @@ -115,6 +176,11 @@ render_file_buffer :: proc(state: ^core.State, s: ^ui.State, buffer: ^core.FileB { kind = {ui.Grow{}, ui.Grow{}} }, + style = { + border = {.Left, .Right, .Top, .Bottom}, + border_color = .Background4, + background_color = .Background1, + }, ) ui.close_element(s) @@ -296,6 +362,20 @@ file_buffer_input_actions :: proc(input_map: ^core.InputActions) { file_buffer_delete_actions(&delete_actions); core.register_key_action(input_map, .D, delete_actions, "Delete commands"); + core.register_key_action(input_map, .SLASH, proc(state: ^core.State, user_data: rawptr) { + panel_state := &(transmute(^core.Panel)user_data).type.(core.FileBufferPanel) + + core.first_snapshot(&panel_state.search_buffer.history) + core.push_new_snapshot(&panel_state.search_buffer.history) + + core.reset_input_map(state) + + state.mode = .Insert; + sdl2.StartTextInput(); + + panel_state.is_searching = true + }, "search buffer") + core.register_key_action(input_map, .V, proc(state: ^core.State, user_data: rawptr) { buffer := &(&(transmute(^core.Panel)user_data).type.(core.FileBufferPanel)).buffer @@ -308,10 +388,35 @@ file_buffer_input_actions :: proc(input_map: ^core.InputActions) { core.register_key_action(input_map, .ESCAPE, proc(state: ^core.State, user_data: rawptr) { panel := transmute(^core.Panel)user_data panel_state := &panel.type.(core.FileBufferPanel) - buffer := &panel_state.buffer + + if panel_state.is_searching { + panel_state.is_searching = false + sdl2.StopTextInput() + } panel_state.viewed_symbol = nil }); + + core.register_key_action(input_map, .N, proc(state: ^core.State, user_data: rawptr) { + panel := transmute(^core.Panel)user_data + panel_state := &panel.type.(core.FileBufferPanel) + + if len(panel_state.query_results) > 0 { + for result, i in panel_state.query_results { + cursor := panel_state.buffer.history.cursor + + if result.line > cursor.line || (result.line == cursor.line && result.col > cursor.col) { + core.move_cursor_to_location(&panel_state.buffer, result.line, result.col) + break + } + + if i == len(panel_state.query_results)-1 { + result := panel_state.query_results[0] + core.move_cursor_to_location(&panel_state.buffer, result.line, result.col) + } + } + } + }); } file_buffer_visual_actions :: proc(input_map: ^core.InputActions) { diff --git a/src/panels/grep.odin b/src/panels/grep.odin index c9d8dd6..cfe0043 100644 --- a/src/panels/grep.odin +++ b/src/panels/grep.odin @@ -29,20 +29,26 @@ make_grep_panel :: proc() -> core.Panel { context.allocator = mem.arena_allocator(&panel_state.query_arena) - rs_results := grep( - strings.clone_to_cstring(core.buffer_to_string(buffer)), - strings.clone_to_cstring(directory) - ); + search_query := core.buffer_to_string(buffer) + if len(search_query) > 0 { + rs_results := grep( + strings.clone_to_cstring(search_query), + strings.clone_to_cstring(directory) + ); - panel_state.query_results = rs_grep_as_results(&rs_results) + panel_state.query_results = rs_grep_as_results(&rs_results) - panel_state.selected_result = 0 - if len(panel_state.query_results) > 0 { - core.update_glyph_buffer_from_bytes( - &panel_state.glyphs, - transmute([]u8)panel_state.query_results[panel_state.selected_result].file_context, - panel_state.query_results[panel_state.selected_result].line, - ) + panel_state.selected_result = 0 + if len(panel_state.query_results) > 0 { + core.update_glyph_buffer_from_bytes( + &panel_state.glyphs, + transmute([]u8)panel_state.query_results[panel_state.selected_result].file_context, + panel_state.query_results[panel_state.selected_result].line, + ) + } + } else { + panel_state.selected_result = 0 + panel_state.query_results = nil } } @@ -259,6 +265,7 @@ foreign import grep_lib "../pkg/grep_lib/target/debug/libgrep.a" @(default_calling_convention = "c") foreign grep_lib { grep :: proc (pattern: cstring, directory: cstring) -> RS_GrepResults --- + grep_buffer :: proc (pattern: cstring, it: ^core.FileBufferIter, func: proc "c" (it: ^core.FileBufferIter) -> core.FileBufferIterResult) -> RS_GrepResults --- free_grep_results :: proc(results: RS_GrepResults) --- } diff --git a/src/pkg/grep_lib/src/lib.rs b/src/pkg/grep_lib/src/lib.rs index 2345cbd..4bee332 100644 --- a/src/pkg/grep_lib/src/lib.rs +++ b/src/pkg/grep_lib/src/lib.rs @@ -1,6 +1,6 @@ use std::{ error::Error, - ffi::CStr, + ffi::{CStr, c_void}, }; use grep::{ @@ -33,32 +33,41 @@ struct Match { } impl Match { fn from_sink_match_with_path( + pattern: &str, value: &grep::searcher::SinkMatch<'_>, path: Option, - ) -> Result { - let line = value - .lines() - .next() - .ok_or(SimpleSinkError::NoLine)? - .to_vec(); - let column = value.bytes_range_in_buffer().len() as u64; + ) -> Result, SimpleSinkError> { + let line = String::from_utf8_lossy(value.lines().next().ok_or(SimpleSinkError::NoLine)?); - Ok(Self { - // TODO: only return N-lines of context instead of the entire freakin' buffer - text: value.buffer().to_vec(), - path: path.unwrap_or_default(), - line_number: value.line_number(), - column, - }) + Ok(line + .match_indices(pattern) + .into_iter() + .map(|(index, _)| Self { + text: value.buffer().to_vec(), + path: path.clone().unwrap_or_default(), + line_number: value.line_number(), + column: index as u64 + 1, + }) + .collect()) } } -#[derive(Default, Debug)] -struct SimpleSink { +#[derive(Debug)] +struct SimpleSink<'a> { + search_pattern: &'a str, current_path: Option, matches: Vec, } -impl Sink for SimpleSink { +impl<'a> SimpleSink<'a> { + fn new(pattern: &'a str) -> Self { + Self { + search_pattern: pattern, + current_path: None, + matches: vec![], + } + } +} +impl Sink for SimpleSink<'_> { type Error = SimpleSinkError; fn matched( @@ -66,16 +75,47 @@ impl Sink for SimpleSink { _searcher: &grep::searcher::Searcher, mat: &grep::searcher::SinkMatch<'_>, ) -> Result { - self.matches.push(Match::from_sink_match_with_path( - mat, - self.current_path.clone(), - )?); + let mut matches = + Match::from_sink_match_with_path(self.search_pattern, mat, self.current_path.clone())?; + + self.matches.append(&mut matches); Ok(true) } } -fn search(pattern: &str, paths: &[&str]) -> Result> { +#[repr(C)] +#[derive(Clone, Copy)] +struct FileBufferIter(*const c_void); + +#[repr(C)] +struct FileBufferIterResult { + character: u8, + done: bool, +} + +struct BufferIter { + iter: FileBufferIter, + iter_func: extern "C" fn(it: FileBufferIter) -> FileBufferIterResult, +} + +impl std::io::Read for BufferIter { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + let result = (self.iter_func)(self.iter); + + if result.done || buf.len() < 1 { + return Ok(0); + } else { + buf[0] = result.character; + return Ok(1); + } + } +} + +fn search_buffer<'a>( + pattern: &'a str, + buffer: BufferIter, +) -> Result, Box> { let matcher = RegexMatcherBuilder::new() .case_smart(true) .fixed_strings(true) @@ -85,7 +125,26 @@ fn search(pattern: &str, paths: &[&str]) -> Result> { .line_number(true) .build(); - let mut sink = SimpleSink::default(); + let mut sink = SimpleSink::new(pattern); + let result = searcher.search_reader(matcher, buffer, &mut sink); + if let Err(err) = result { + eprintln!("{:?}", err); + } + + Ok(sink) +} + +fn search<'a>(pattern: &'a str, paths: &[&str]) -> Result, Box> { + let matcher = RegexMatcherBuilder::new() + .case_smart(true) + .fixed_strings(true) + .build(pattern)?; + let mut searcher = SearcherBuilder::new() + .binary_detection(BinaryDetection::quit(b'\x00')) + .line_number(true) + .build(); + + let mut sink = SimpleSink::new(pattern); for path in paths { for result in WalkDir::new(path).into_iter().filter_entry(|dent| { if dent.file_type().is_dir() @@ -147,9 +206,17 @@ impl From for GrepResult { impl From for Match { fn from(value: GrepResult) -> Self { unsafe { - let text = Box::from_raw(std::slice::from_raw_parts_mut(value.text as *mut _, value.text_len as usize)).to_vec(); + let text = Box::from_raw(std::slice::from_raw_parts_mut( + value.text as *mut _, + value.text_len as usize, + )) + .to_vec(); - let path = Box::from_raw(std::slice::from_raw_parts_mut(value.path as *mut _, value.path_len as usize)).to_vec(); + let path = Box::from_raw(std::slice::from_raw_parts_mut( + value.path as *mut _, + value.path_len as usize, + )) + .to_vec(); let path = String::from_utf8_unchecked(path); Self { @@ -182,8 +249,6 @@ extern "C" fn grep( ) }; - println!("pattern: '{pattern}', directory: '{directory}'"); - let boxed = search(&pattern, &[&directory]) .into_iter() .map(|sink| sink.matches.into_iter()) @@ -200,10 +265,43 @@ extern "C" fn grep( } } +#[unsafe(no_mangle)] +extern "C" fn grep_buffer( + pattern: *const std::ffi::c_char, + it: FileBufferIter, + iter_func: extern "C" fn(it: FileBufferIter) -> FileBufferIterResult, +) -> GrepResults { + let pattern = unsafe { CStr::from_ptr(pattern).to_string_lossy() }; + + let boxed = search_buffer( + &pattern, + BufferIter { + iter: it, + iter_func, + }, + ) + .into_iter() + .map(|sink| sink.matches.into_iter()) + .flatten() + .map(|v| GrepResult::from(v)) + .collect::>() + .into_boxed_slice(); + + let len = boxed.len() as u32; + + GrepResults { + results: Box::into_raw(boxed) as _, + len, + } +} + #[unsafe(no_mangle)] extern "C" fn free_grep_results(results: GrepResults) { unsafe { - let mut array = std::slice::from_raw_parts_mut(results.results as *mut GrepResult, results.len as usize); + let mut array = std::slice::from_raw_parts_mut( + results.results as *mut GrepResult, + results.len as usize, + ); let array = Box::from_raw(array); for v in array { diff --git a/src/ui/ui.odin b/src/ui/ui.odin index ae261ac..1bfd244 100644 --- a/src/ui/ui.odin +++ b/src/ui/ui.odin @@ -451,9 +451,9 @@ draw :: proc(state: ^State, core_state: ^core.State) { if .Right in e.style.border { core.draw_line( core_state, - e.layout.pos.x + e.layout.size.x, + e.layout.pos.x + e.layout.size.x - 1, e.layout.pos.y, - e.layout.pos.x + e.layout.size.x, + e.layout.pos.x + e.layout.size.x - 1, e.layout.pos.y + e.layout.size.y, e.style.border_color, ) diff --git a/todo.md b/todo.md index cbac4bc..69cb851 100644 --- a/todo.md +++ b/todo.md @@ -17,6 +17,8 @@ - [ ] Finish writing tests for all current user actions - Vim-like Macro replays - [ ] Simple File Search (vim /) + - [x] Forward Search + - [ ] Backward Search - Modify input system to allow for keybinds that take input - Vim's f and F movement commands - Vim's r command