Compare commits

...

2 Commits

Author SHA1 Message Date
Patrick Cleaveliln 79dafaf1a0 move to next search result while typing 2025-07-26 18:59:08 +00:00
Patrick Cleaveliln 39a26c3c01 in-buffer search 2025-07-26 18:50:25 +00:00
9 changed files with 351 additions and 64 deletions

View File

@ -7,6 +7,7 @@ editor: grep src/**/*.odin
odin build src/ -out:bin/editor -debug odin build src/ -out:bin/editor -debug
grep: grep:
cargo fmt --manifest-path "src/pkg/grep_lib/Cargo.toml"
cargo build --manifest-path "src/pkg/grep_lib/Cargo.toml" cargo build --manifest-path "src/pkg/grep_lib/Cargo.toml"
test: src/**/*.odin test: src/**/*.odin

View File

@ -103,6 +103,13 @@ FileBufferPanel :: struct {
buffer: FileBuffer, buffer: FileBuffer,
viewed_symbol: Maybe(string), 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 // only used for initialization
file_path: string, file_path: string,
line, col: int, line, col: int,

View File

@ -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) { iterate_file_buffer :: proc(it: ^FileBufferIter) -> (character: u8, idx: PieceTableIndex, cond: bool) {
character, idx, cond = iterate_piece_table_iter(&it.piter) 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; 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) { move_cursor_start_of_line :: proc(buffer: ^FileBuffer, cursor: Maybe(^Cursor) = nil) {
cursor := cursor; cursor := cursor;
@ -541,13 +578,13 @@ move_cursor_left :: proc(buffer: ^FileBuffer, cursor: Maybe(^Cursor) = nil) {
cursor = &buffer.history.cursor; cursor = &buffer.history.cursor;
} }
buffer.last_col = cursor.?.col
if cursor.?.col > 0 { if cursor.?.col > 0 {
it := new_file_buffer_iter_with_cursor(buffer, cursor.?^); it := new_file_buffer_iter_with_cursor(buffer, cursor.?^);
iterate_file_buffer_reverse(&it); iterate_file_buffer_reverse(&it);
cursor.?^ = it.cursor; 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) { 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; cursor = &buffer.history.cursor;
} }
buffer.last_col = cursor.?.col
it := new_file_buffer_iter_with_cursor(buffer, cursor.?^); it := new_file_buffer_iter_with_cursor(buffer, cursor.?^);
line_length := file_buffer_line_length(buffer, it.cursor.index); 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; cursor.?^ = it.cursor;
} }
} }
buffer.last_col = cursor.?.col
} }
move_cursor_forward_start_of_word :: proc(buffer: ^FileBuffer, cursor: Maybe(^Cursor) = nil) { move_cursor_forward_start_of_word :: proc(buffer: ^FileBuffer, cursor: Maybe(^Cursor) = nil) {

View File

@ -102,6 +102,20 @@ recover_snapshot :: proc(history: ^FileHistory) {
history.cursor = history.snapshots[history.next].cursor 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 { clone_chunk :: proc(chunks: [dynamic][]u8) -> [dynamic][]u8 {
new_chunks := make([dynamic][]u8, len(chunks), len(chunks)) new_chunks := make([dynamic][]u8, len(chunks), len(chunks))

View File

@ -3,6 +3,8 @@ package panels
import "base:runtime" import "base:runtime"
import "core:log" import "core:log"
import "core:fmt" import "core:fmt"
import "core:mem"
import "core:strings"
import "core:path/filepath" import "core:path/filepath"
import "vendor:sdl2" import "vendor:sdl2"
@ -12,6 +14,26 @@ import "../core"
import "../ui" import "../ui"
make_file_buffer_panel :: proc(file_path: string, line: int = 0, col: int = 0) -> core.Panel { 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 { return core.Panel {
type = core.FileBufferPanel { type = core.FileBufferPanel {
file_path = file_path, 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 context.allocator = panel.allocator
panel_state := &panel.type.(core.FileBufferPanel) 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.input_map = core.new_input_map()
panel_state.search_buffer = core.new_virtual_file_buffer(panel.allocator)
if len(panel_state.file_path) == 0 { if len(panel_state.file_path) == 0 {
panel_state.buffer = core.new_virtual_file_buffer(panel.allocator) panel_state.buffer = core.new_virtual_file_buffer(panel.allocator)
@ -62,13 +93,57 @@ 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) { buffer = proc(panel: ^core.Panel, state: ^core.State) -> (buffer: ^core.FileBuffer, ok: bool) {
panel_state := &panel.type.(core.FileBufferPanel) panel_state := &panel.type.(core.FileBufferPanel)
if panel_state.is_searching {
return &panel_state.search_buffer, true
} else {
return &panel_state.buffer, true 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)
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)
}
}
}
}, },
render = proc(panel: ^core.Panel, state: ^core.State) -> (ok: bool) { render = proc(panel: ^core.Panel, state: ^core.State) -> (ok: bool) {
panel_state := &panel.type.(core.FileBufferPanel) panel_state := &panel.type.(core.FileBufferPanel)
s := transmute(^ui.State)state.ui s := transmute(^ui.State)state.ui
ui.open_element(s, nil,
{
dir = .TopToBottom,
kind = {ui.Grow{}, ui.Grow{}},
},
)
{
render_file_buffer(state, s, &panel_state.buffer) 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 { if viewed_symbol, ok := panel_state.viewed_symbol.?; ok {
ui.open_element(s, nil, ui.open_element(s, nil,
@ -87,6 +162,8 @@ make_file_buffer_panel :: proc(file_path: string, line: int = 0, col: int = 0) -
} }
ui.close_element(s) ui.close_element(s)
} }
}
ui.close_element(s)
return true return true
} }
@ -115,6 +192,11 @@ render_file_buffer :: proc(state: ^core.State, s: ^ui.State, buffer: ^core.FileB
{ {
kind = {ui.Grow{}, ui.Grow{}} kind = {ui.Grow{}, ui.Grow{}}
}, },
style = {
border = {.Left, .Right, .Top, .Bottom},
border_color = .Background4,
background_color = .Background1,
},
) )
ui.close_element(s) ui.close_element(s)
@ -296,6 +378,20 @@ file_buffer_input_actions :: proc(input_map: ^core.InputActions) {
file_buffer_delete_actions(&delete_actions); file_buffer_delete_actions(&delete_actions);
core.register_key_action(input_map, .D, delete_actions, "Delete commands"); 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) { core.register_key_action(input_map, .V, proc(state: ^core.State, user_data: rawptr) {
buffer := &(&(transmute(^core.Panel)user_data).type.(core.FileBufferPanel)).buffer buffer := &(&(transmute(^core.Panel)user_data).type.(core.FileBufferPanel)).buffer
@ -308,10 +404,35 @@ file_buffer_input_actions :: proc(input_map: ^core.InputActions) {
core.register_key_action(input_map, .ESCAPE, proc(state: ^core.State, user_data: rawptr) { core.register_key_action(input_map, .ESCAPE, proc(state: ^core.State, user_data: rawptr) {
panel := transmute(^core.Panel)user_data panel := transmute(^core.Panel)user_data
panel_state := &panel.type.(core.FileBufferPanel) 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 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) { file_buffer_visual_actions :: proc(input_map: ^core.InputActions) {

View File

@ -29,8 +29,10 @@ make_grep_panel :: proc() -> core.Panel {
context.allocator = mem.arena_allocator(&panel_state.query_arena) context.allocator = mem.arena_allocator(&panel_state.query_arena)
search_query := core.buffer_to_string(buffer)
if len(search_query) > 0 {
rs_results := grep( rs_results := grep(
strings.clone_to_cstring(core.buffer_to_string(buffer)), strings.clone_to_cstring(search_query),
strings.clone_to_cstring(directory) strings.clone_to_cstring(directory)
); );
@ -44,6 +46,10 @@ make_grep_panel :: proc() -> core.Panel {
panel_state.query_results[panel_state.selected_result].line, panel_state.query_results[panel_state.selected_result].line,
) )
} }
} else {
panel_state.selected_result = 0
panel_state.query_results = nil
}
} }
return core.Panel { return core.Panel {
@ -259,6 +265,7 @@ foreign import grep_lib "../pkg/grep_lib/target/debug/libgrep.a"
@(default_calling_convention = "c") @(default_calling_convention = "c")
foreign grep_lib { foreign grep_lib {
grep :: proc (pattern: cstring, directory: cstring) -> RS_GrepResults --- 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) --- free_grep_results :: proc(results: RS_GrepResults) ---
} }
@ -283,7 +290,7 @@ rs_grep_as_results :: proc(results: ^RS_GrepResults, allocator := context.alloca
query_results := make([]core.GrepQueryResult, results.len) query_results := make([]core.GrepQueryResult, results.len)
for i in 0..<results.len { for i in 0..<len(query_results) {
r := results.results[i] r := results.results[i]
query_results[i] = core.GrepQueryResult { query_results[i] = core.GrepQueryResult {

View File

@ -1,6 +1,6 @@
use std::{ use std::{
error::Error, error::Error,
ffi::CStr, ffi::{CStr, c_void},
}; };
use grep::{ use grep::{
@ -33,32 +33,41 @@ struct Match {
} }
impl Match { impl Match {
fn from_sink_match_with_path( fn from_sink_match_with_path(
pattern: &str,
value: &grep::searcher::SinkMatch<'_>, value: &grep::searcher::SinkMatch<'_>,
path: Option<String>, path: Option<String>,
) -> Result<Self, SimpleSinkError> { ) -> Result<Vec<Self>, SimpleSinkError> {
let line = value let line = String::from_utf8_lossy(value.lines().next().ok_or(SimpleSinkError::NoLine)?);
.lines()
.next()
.ok_or(SimpleSinkError::NoLine)?
.to_vec();
let column = value.bytes_range_in_buffer().len() as u64;
Ok(Self { Ok(line
// TODO: only return N-lines of context instead of the entire freakin' buffer .match_indices(pattern)
.into_iter()
.map(|(index, _)| Self {
text: value.buffer().to_vec(), text: value.buffer().to_vec(),
path: path.unwrap_or_default(), path: path.clone().unwrap_or_default(),
line_number: value.line_number(), line_number: value.line_number(),
column, column: index as u64 + 1,
}) })
.collect())
} }
} }
#[derive(Default, Debug)] #[derive(Debug)]
struct SimpleSink { struct SimpleSink<'a> {
search_pattern: &'a str,
current_path: Option<String>, current_path: Option<String>,
matches: Vec<Match>, matches: Vec<Match>,
} }
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; type Error = SimpleSinkError;
fn matched( fn matched(
@ -66,16 +75,47 @@ impl Sink for SimpleSink {
_searcher: &grep::searcher::Searcher, _searcher: &grep::searcher::Searcher,
mat: &grep::searcher::SinkMatch<'_>, mat: &grep::searcher::SinkMatch<'_>,
) -> Result<bool, Self::Error> { ) -> Result<bool, Self::Error> {
self.matches.push(Match::from_sink_match_with_path( let mut matches =
mat, Match::from_sink_match_with_path(self.search_pattern, mat, self.current_path.clone())?;
self.current_path.clone(),
)?); self.matches.append(&mut matches);
Ok(true) Ok(true)
} }
} }
fn search(pattern: &str, paths: &[&str]) -> Result<SimpleSink, Box<dyn Error>> { #[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<usize> {
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<SimpleSink<'a>, Box<dyn Error>> {
let matcher = RegexMatcherBuilder::new() let matcher = RegexMatcherBuilder::new()
.case_smart(true) .case_smart(true)
.fixed_strings(true) .fixed_strings(true)
@ -85,7 +125,26 @@ fn search(pattern: &str, paths: &[&str]) -> Result<SimpleSink, Box<dyn Error>> {
.line_number(true) .line_number(true)
.build(); .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<SimpleSink<'a>, Box<dyn Error>> {
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 path in paths {
for result in WalkDir::new(path).into_iter().filter_entry(|dent| { for result in WalkDir::new(path).into_iter().filter_entry(|dent| {
if dent.file_type().is_dir() if dent.file_type().is_dir()
@ -147,9 +206,17 @@ impl From<Match> for GrepResult {
impl From<GrepResult> for Match { impl From<GrepResult> for Match {
fn from(value: GrepResult) -> Self { fn from(value: GrepResult) -> Self {
unsafe { 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); let path = String::from_utf8_unchecked(path);
Self { Self {
@ -182,8 +249,6 @@ extern "C" fn grep(
) )
}; };
println!("pattern: '{pattern}', directory: '{directory}'");
let boxed = search(&pattern, &[&directory]) let boxed = search(&pattern, &[&directory])
.into_iter() .into_iter()
.map(|sink| sink.matches.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::<Vec<_>>()
.into_boxed_slice();
let len = boxed.len() as u32;
GrepResults {
results: Box::into_raw(boxed) as _,
len,
}
}
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
extern "C" fn free_grep_results(results: GrepResults) { extern "C" fn free_grep_results(results: GrepResults) {
unsafe { 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); let array = Box::from_raw(array);
for v in array { for v in array {

View File

@ -451,9 +451,9 @@ draw :: proc(state: ^State, core_state: ^core.State) {
if .Right in e.style.border { if .Right in e.style.border {
core.draw_line( core.draw_line(
core_state, 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.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.layout.pos.y + e.layout.size.y,
e.style.border_color, e.style.border_color,
) )

View File

@ -17,6 +17,8 @@
- [ ] Finish writing tests for all current user actions - [ ] Finish writing tests for all current user actions
- Vim-like Macro replays - Vim-like Macro replays
- [ ] Simple File Search (vim /) - [ ] Simple File Search (vim /)
- [x] Forward Search
- [ ] Backward Search
- Modify input system to allow for keybinds that take input - Modify input system to allow for keybinds that take input
- Vim's f and F movement commands - Vim's f and F movement commands
- Vim's r command - Vim's r command