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
grep:
cargo fmt --manifest-path "src/pkg/grep_lib/Cargo.toml"
cargo build --manifest-path "src/pkg/grep_lib/Cargo.toml"
test: src/**/*.odin

View File

@ -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,

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) {
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) {

View File

@ -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))

View File

@ -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,77 @@ 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)
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) {
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 +192,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 +378,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 +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) {
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) {

View File

@ -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) ---
}
@ -283,7 +290,7 @@ rs_grep_as_results :: proc(results: ^RS_GrepResults, allocator := context.alloca
query_results := make([]core.GrepQueryResult, results.len)
for i in 0..<results.len {
for i in 0..<len(query_results) {
r := results.results[i]
query_results[i] = core.GrepQueryResult {

View File

@ -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<String>,
) -> Result<Self, SimpleSinkError> {
let line = value
.lines()
.next()
.ok_or(SimpleSinkError::NoLine)?
.to_vec();
let column = value.bytes_range_in_buffer().len() as u64;
) -> Result<Vec<Self>, 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<String>,
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;
fn matched(
@ -66,16 +75,47 @@ impl Sink for SimpleSink {
_searcher: &grep::searcher::Searcher,
mat: &grep::searcher::SinkMatch<'_>,
) -> Result<bool, Self::Error> {
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<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()
.case_smart(true)
.fixed_strings(true)
@ -85,7 +125,26 @@ fn search(pattern: &str, paths: &[&str]) -> Result<SimpleSink, Box<dyn Error>> {
.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<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 result in WalkDir::new(path).into_iter().filter_entry(|dent| {
if dent.file_type().is_dir()
@ -147,9 +206,17 @@ impl From<Match> for GrepResult {
impl From<GrepResult> 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::<Vec<_>>()
.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 {

View File

@ -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,
)

View File

@ -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