diff --git a/.gitignore b/.gitignore index 6c399e2..13239cc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ bin/ -lib-rg/target +**/target diff --git a/Makefile b/Makefile index 44c9c44..5926359 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,13 @@ all: editor -editor: src/*.odin rg +editor: src/*.odin grep odin_highlighter buffer_search odin build src/ -out:bin/editor -lld -rg: - cargo b --manifest-path=lib-rg/Cargo.toml +buffer_search: + odin build plugins/buffer_search/ -build-mode:dll -no-entry-point -out:bin/buffer_search +odin_highlighter: + odin build plugins/highlighter/src/ -build-mode:dll -no-entry-point -out:bin/highlighter + +grep: + cargo b --manifest-path=plugins/grep/Cargo.toml + cp plugins/grep/target/debug/libgrep_plugin.dylib bin/ diff --git a/lib-rg/src/lib.rs b/lib-rg/src/lib.rs deleted file mode 100644 index d37b338..0000000 --- a/lib-rg/src/lib.rs +++ /dev/null @@ -1,220 +0,0 @@ -use std::{ - error::Error, - ffi::{CStr, OsString}, - os::raw::c_char, - str::FromStr, -}; - -use grep::{ - regex::RegexMatcher, - searcher::{BinaryDetection, SearcherBuilder, Sink, SinkError}, -}; -use walkdir::WalkDir; - -#[derive(Debug)] -pub enum SimpleSinkError { - StandardError, - NoLine, - BadString, -} - -impl SinkError for SimpleSinkError { - fn error_message(message: T) -> Self { - eprintln!("{message}"); - - Self::StandardError - } -} - -#[repr(C)] -pub struct UnsafeMatch { - text_str: *mut u8, - text_len: usize, - text_cap: usize, - - path_str: *mut u8, - path_len: usize, - path_cap: usize, - - line_number: u64, - column: u64, -} - -impl From for UnsafeMatch { - fn from(value: Match) -> Self { - let mut text_boxed = Box::new(value.text); - let text_str = text_boxed.as_mut_ptr(); - let text_len = text_boxed.len(); - let text_cap = text_boxed.capacity(); - Box::leak(text_boxed); - - let mut path_boxed = Box::new(value.path); - let path_str = path_boxed.as_mut_ptr(); - let path_len = path_boxed.len(); - let path_cap = path_boxed.capacity(); - Box::leak(path_boxed); - - Self { - text_str, - text_len, - text_cap, - path_str, - path_len, - path_cap, - line_number: value.line_number.unwrap_or_default(), - column: value.column, - } - } -} - -struct Match { - text: Vec, - path: String, - line_number: Option, - column: u64, -} -impl Match { - fn from_sink_match_with_path( - 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; - - Ok(Self { - text: line, - path: path.unwrap_or_default(), - line_number: value.line_number(), - column, - }) - } -} - -#[derive(Default)] -struct SimpleSink { - current_path: Option, - matches: Vec, -} -impl Sink for SimpleSink { - type Error = SimpleSinkError; - - fn matched( - &mut self, - _searcher: &grep::searcher::Searcher, - mat: &grep::searcher::SinkMatch<'_>, - ) -> Result { - self.matches.push(Match::from_sink_match_with_path( - mat, - self.current_path.clone(), - )?); - - Ok(true) - } -} - -#[repr(C)] -pub struct UnsafeMatchArray { - matches: *mut UnsafeMatch, - len: usize, - capacity: usize, -} - -impl Default for UnsafeMatchArray { - fn default() -> Self { - Self { - matches: std::ptr::null_mut(), - len: 0, - capacity: 0, - } - } -} - -impl From for UnsafeMatchArray { - fn from(value: SimpleSink) -> Self { - let matches: Vec = value.matches.into_iter().map(Into::into).collect(); - let mut boxed_vec = Box::new(matches); - - let ptr = boxed_vec.as_mut_ptr(); - let len = boxed_vec.len(); - let capacity = boxed_vec.capacity(); - Box::leak(boxed_vec); - - Self { - matches: ptr, - len, - capacity, - } - } -} - -/// # Safety -/// Who knows what'll happen if you don't pass valid strings -#[no_mangle] -pub unsafe extern "C" fn rg_search( - pattern: *const c_char, - path: *const c_char, -) -> UnsafeMatchArray { - let pattern = CStr::from_ptr(pattern); - let path = CStr::from_ptr(path); - if let (Ok(path), Ok(pattern)) = (path.to_str(), pattern.to_str()) { - if let Ok(path) = OsString::from_str(path) { - return match search(pattern, &[path]) { - Ok(sink) => sink.into(), - Err(err) => { - eprintln!("rg search failed: {}", err); - Default::default() - } - }; - } - } - - Default::default() -} - -/// # Safety -/// Who knows what'll happen if you don't pass back the same vec -#[no_mangle] -pub unsafe extern "C" fn drop_match_array(match_array: UnsafeMatchArray) { - let matches = Vec::from_raw_parts(match_array.matches, match_array.len, match_array.capacity); - for mat in matches { - let _ = String::from_raw_parts(mat.text_str, mat.text_len, mat.text_cap); - let _ = String::from_raw_parts(mat.path_str, mat.path_len, mat.path_cap); - } -} - -fn search(pattern: &str, paths: &[OsString]) -> Result> { - let matcher = RegexMatcher::new_line_matcher(pattern)?; - let mut searcher = SearcherBuilder::new() - .binary_detection(BinaryDetection::quit(b'\x00')) - .line_number(true) - .build(); - - let mut sink = SimpleSink::default(); - for path in paths { - for result in WalkDir::new(path) { - let dent = match result { - Ok(dent) => dent, - Err(err) => { - eprintln!("{}", err); - continue; - } - }; - if !dent.file_type().is_file() { - continue; - } - sink.current_path = Some(dent.path().to_string_lossy().into()); - - let result = searcher.search_path(&matcher, dent.path(), &mut sink); - - if let Err(err) = result { - eprintln!("{}: {:?}", dent.path().display(), err); - } - } - } - - Ok(sink) -} diff --git a/plugin-rs-bindings/Cargo.lock b/plugin-rs-bindings/Cargo.lock new file mode 100644 index 0000000..730d5d6 --- /dev/null +++ b/plugin-rs-bindings/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "plugin_rs_bindings" +version = "0.1.0" diff --git a/plugin-rs-bindings/Cargo.toml b/plugin-rs-bindings/Cargo.toml new file mode 100644 index 0000000..8030a20 --- /dev/null +++ b/plugin-rs-bindings/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "plugin_rs_bindings" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/plugin-rs-bindings/src/lib.rs b/plugin-rs-bindings/src/lib.rs new file mode 100644 index 0000000..4018c0f --- /dev/null +++ b/plugin-rs-bindings/src/lib.rs @@ -0,0 +1,509 @@ +use std::{ + borrow::Cow, + ffi::{c_char, c_void, CStr}, + path::Path, +}; + +#[macro_export] +macro_rules! Closure { + (($($arg: ident: $type: ty),+) => $body: expr) => { + { + extern "C" fn f($($arg: $type),+) { + $body + } + f + } + }; + (($($arg: ident: $type: ty),+) -> $return_type: ty => $body: expr) => { + { + extern "C" fn f($($arg: $type),+) -> $return_type { + $body + } + f + } + }; +} + +#[repr(C)] +#[derive(Clone, Copy)] +pub struct InputMap { + internal: *const std::ffi::c_void, +} + +#[repr(C)] +#[derive(Debug)] +pub struct BufferIndex { + pub slice_index: isize, + pub content_index: isize, +} + +#[repr(C)] +#[derive(Debug)] +pub struct Cursor { + pub col: isize, + pub line: isize, + pub index: BufferIndex, +} + +#[repr(C)] +#[derive(Debug)] +struct InternalBufferIter { + cursor: Cursor, + buffer: *const c_void, + hit_end: bool, +} + +#[repr(C)] +pub struct IterateResult { + pub char: u8, + pub should_continue: bool, +} + +#[repr(C)] +#[derive(Debug)] +pub struct BufferInput { + bytes: *const u8, + length: isize, +} + +impl BufferInput { + pub fn try_as_str(&self) -> Option> { + if self.bytes.is_null() { + None + } else { + let slice = unsafe { std::slice::from_raw_parts(self.bytes, self.length as usize) }; + + Some(String::from_utf8_lossy(slice)) + } + } +} + +#[repr(C)] +#[derive(Debug)] +pub struct BufferInfo { + pub buffer: Buffer, + pub file_path: *const i8, + pub input: BufferInput, + + pub cursor: Cursor, + + pub glyph_buffer_width: isize, + pub glyph_buffer_height: isize, + pub top_line: isize, +} + +#[repr(C)] +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Buffer { + internal: *const c_void, +} + +impl Buffer { + pub fn null() -> Buffer { + Buffer { + internal: std::ptr::null(), + } + } +} + +#[repr(C)] +pub struct BufferVTable { + pub get_num_buffers: extern "C" fn() -> isize, + get_buffer_info: extern "C" fn(buffer: Buffer) -> BufferInfo, + pub get_buffer_info_from_index: extern "C" fn(buffer_index: isize) -> BufferInfo, + pub color_char_at: extern "C" fn( + buffer: *const c_void, + start_cursor: Cursor, + end_cursor: Cursor, + palette_index: i32, + ), + pub set_current_buffer: extern "C" fn(buffer_index: isize), + + open_buffer: extern "C" fn(path: *const u8, line: isize, col: isize), + open_virtual_buffer: extern "C" fn() -> *const c_void, + free_virtual_buffer: extern "C" fn(buffer: Buffer), +} + +impl BufferVTable { + pub fn get_buffer_info(&self, buffer: Buffer) -> Option { + if buffer.internal.is_null() { + None + } else { + Some((self.get_buffer_info)(buffer)) + } + } + pub fn open_buffer(&self, path: impl AsRef, line: i32, col: i32) { + let c_str = path.as_ref().to_string_lossy().as_ptr(); + (self.open_buffer)(c_str, line as isize, col as isize); + } + pub fn open_virtual_buffer(&self) -> Buffer { + Buffer { + internal: (self.open_virtual_buffer)(), + } + } + pub fn free_virtual_buffer(&self, buffer: Buffer) { + (self.free_virtual_buffer)(buffer); + } +} + +#[repr(C)] +pub struct IteratorVTable { + get_current_buffer_iterator: extern "C" fn() -> InternalBufferIter, + get_buffer_iterator: extern "C" fn(buffer: *const c_void) -> InternalBufferIter, + get_char_at_iter: extern "C" fn(it: *const InternalBufferIter) -> u8, + get_buffer_list_iter: extern "C" fn(prev_buffer: *const isize) -> isize, + + iterate_buffer: extern "C" fn(it: *mut InternalBufferIter) -> IterateResult, + iterate_buffer_reverse: extern "C" fn(it: *mut InternalBufferIter) -> IterateResult, + iterate_buffer_until: extern "C" fn(it: *mut InternalBufferIter, until_proc: *const c_void), + iterate_buffer_until_reverse: + extern "C" fn(it: *mut InternalBufferIter, until_proc: *const c_void), + iterate_buffer_peek: extern "C" fn(it: *mut InternalBufferIter) -> IterateResult, + + pub until_line_break: *const c_void, + pub until_single_quote: *const c_void, + pub until_double_quote: *const c_void, + pub until_end_of_word: *const c_void, +} + +type OnColorBufferProc = extern "C" fn(plugin: Plugin, buffer: *const c_void); +type OnHookProc = extern "C" fn(plugin: Plugin, buffer: Buffer); +type InputGroupProc = extern "C" fn(plugin: Plugin, input_map: InputMap); +type InputActionProc = extern "C" fn(plugin: Plugin); +type WindowDrawProc = extern "C" fn(plugin: Plugin, window: *const c_void); +type WindowFreeProc = extern "C" fn(plugin: Plugin, window: *const c_void); +type WindowGetBufferProc = extern "C" fn(plugin: Plugin, window: *const c_void) -> Buffer; +#[repr(C)] +pub struct Plugin { + state: *const c_void, + pub iter_table: IteratorVTable, + pub buffer_table: BufferVTable, + + pub register_hook: extern "C" fn(hook: Hook, on_hook: OnHookProc), + pub register_highlighter: + extern "C" fn(extension: *const c_char, on_color_buffer: OnColorBufferProc), + + pub register_input_group: + extern "C" fn(input_map: InputMap, key: Key, register_group: InputGroupProc), + pub register_input: extern "C" fn( + input_map: InputMap, + key: Key, + input_action: InputActionProc, + description: *const u8, + ), + + pub create_window: extern "C" fn( + user_data: *const c_void, + register_group: InputGroupProc, + draw_proc: WindowDrawProc, + free_window_proc: WindowFreeProc, + get_buffer_proc: *const (), + ) -> *const c_void, + get_window: extern "C" fn() -> *const c_void, + + pub request_window_close: extern "C" fn(), + pub get_screen_width: extern "C" fn() -> isize, + pub get_screen_height: extern "C" fn() -> isize, + pub get_font_width: extern "C" fn() -> isize, + pub get_font_height: extern "C" fn() -> isize, + get_current_directory: extern "C" fn() -> *const c_char, + pub enter_insert_mode: extern "C" fn(), + + pub draw_rect: extern "C" fn(x: i32, y: i32, width: i32, height: i32, color: PaletteColor), + pub draw_text: extern "C" fn(text: *const c_char, x: f32, y: f32, color: PaletteColor), + pub draw_buffer_from_index: extern "C" fn( + buffer_index: isize, + x: isize, + y: isize, + glyph_buffer_width: isize, + glyph_buffer_height: isize, + show_line_numbers: bool, + ), + pub draw_buffer: extern "C" fn( + buffer: Buffer, + x: isize, + y: isize, + glyph_buffer_width: isize, + glyph_buffer_height: isize, + show_line_numbers: bool, + ), +} + +pub struct BufferIter { + iter: InternalBufferIter, + iter_table: IteratorVTable, +} + +impl BufferIter { + pub fn new(plugin: Plugin, buffer: Buffer) -> Self { + let buffer_info = (plugin.buffer_table.get_buffer_info)(buffer); + + Self { + iter: InternalBufferIter { + cursor: buffer_info.cursor, + buffer: buffer.internal, + hit_end: false, + }, + iter_table: plugin.iter_table, + } + } +} + +impl Iterator for BufferIter { + type Item = char; + + fn next(&mut self) -> Option { + let iter_ptr = (&mut self.iter) as *mut InternalBufferIter; + + let result = (self.iter_table.iterate_buffer)(iter_ptr); + if result.should_continue { + Some(result.char as char) + } else { + None + } + } +} + +pub struct BufferListIter { + index: isize, + next_fn: extern "C" fn(prev_buffer: *const isize) -> isize, +} + +impl From<&Plugin> for BufferListIter { + fn from(value: &Plugin) -> Self { + BufferListIter { + index: 0, + next_fn: value.iter_table.get_buffer_list_iter, + } + } +} + +impl Iterator for BufferListIter { + type Item = isize; + + fn next(&mut self) -> Option { + if self.index == -1 { + return None; + } + + Some((self.next_fn)(&mut self.index)) + } +} + +impl Plugin { + pub fn get_current_directory(&self) -> Cow { + unsafe { + let c_str = CStr::from_ptr((self.get_current_directory)()); + + c_str.to_string_lossy() + } + } + + /// # Safety + /// If `W` is not the same type as given in `self.create_window`, it will result in undefined + /// behavior. `W` can also be a different type if another plugin has created a window. + pub unsafe fn get_window<'a, W>(&self) -> Option<&'a mut W> { + let window_ptr = (self.get_window)() as *mut W; + + if window_ptr.is_null() { + None + } else { + let window = Box::from_raw(window_ptr); + Some(Box::leak(window)) + } + } + + pub fn create_window( + &self, + window: W, + register_group: InputGroupProc, + draw_proc: WindowDrawProc, + free_window_proc: WindowFreeProc, + get_buffer_proc: Option, + ) { + let boxed = Box::new(window); + (self.create_window)( + Box::into_raw(boxed) as *const std::ffi::c_void, + register_group, + draw_proc, + free_window_proc, + if let Some(proc) = get_buffer_proc { + proc as *const () + } else { + std::ptr::null() + }, + ); + } + pub fn register_hook(&self, hook: Hook, on_hook: OnHookProc) { + (self.register_hook)(hook, on_hook) + } + pub fn register_input_group( + &self, + input_map: Option, + key: Key, + register_group: InputGroupProc, + ) { + let input_map = match input_map { + Some(input_map) => input_map, + None => InputMap { + internal: std::ptr::null(), + }, + }; + + (self.register_input_group)(input_map, key, register_group); + } +} + +#[repr(i32)] +pub enum Hook { + BufferInput, +} + +#[repr(i32)] +pub enum Key { + KeyNull = 0, // Key: NULL, used for no key pressed + // Alphanumeric keys + Apostrophe = 39, // key: ' + Comma = 44, // Key: , + Minus = 45, // Key: - + Period = 46, // Key: . + Slash = 47, // Key: / + Zero = 48, // Key: 0 + One = 49, // Key: 1 + Two = 50, // Key: 2 + Three = 51, // Key: 3 + Four = 52, // Key: 4 + Five = 53, // Key: 5 + Six = 54, // Key: 6 + Seven = 55, // Key: 7 + Eight = 56, // Key: 8 + Nine = 57, // Key: 9 + Semicolon = 59, // Key: ; + Equal = 61, // Key: = + A = 65, // Key: A | a + B = 66, // Key: B | b + C = 67, // Key: C | c + D = 68, // Key: D | d + E = 69, // Key: E | e + F = 70, // Key: F | f + G = 71, // Key: G | g + H = 72, // Key: H | h + I = 73, // Key: I | i + J = 74, // Key: J | j + K = 75, // Key: K | k + L = 76, // Key: L | l + M = 77, // Key: M | m + N = 78, // Key: N | n + O = 79, // Key: O | o + P = 80, // Key: P | p + Q = 81, // Key: Q | q + R = 82, // Key: R | r + S = 83, // Key: S | s + T = 84, // Key: T | t + U = 85, // Key: U | u + V = 86, // Key: V | v + W = 87, // Key: W | w + X = 88, // Key: X | x + Y = 89, // Key: Y | y + Z = 90, // Key: Z | z + LeftBracket = 91, // Key: [ + Backslash = 92, // Key: '\' + RightBracket = 93, // Key: ] + Grave = 96, // Key: ` + // Function keys + Space = 32, // Key: Space + Escape = 256, // Key: Esc + Enter = 257, // Key: Enter + Tab = 258, // Key: Tab + Backspace = 259, // Key: Backspace + Insert = 260, // Key: Ins + Delete = 261, // Key: Del + Right = 262, // Key: Cursor right + Left = 263, // Key: Cursor left + Down = 264, // Key: Cursor down + Up = 265, // Key: Cursor up + PageUp = 266, // Key: Page up + PageDown = 267, // Key: Page down + Home = 268, // Key: Home + End = 269, // Key: End + CapsLock = 280, // Key: Caps lock + ScrollLock = 281, // Key: Scroll down + NumLock = 282, // Key: Num lock + PrintScreen = 283, // Key: Print screen + Pause = 284, // Key: Pause + F1 = 290, // Key: F1 + F2 = 291, // Key: F2 + F3 = 292, // Key: F3 + F4 = 293, // Key: F4 + F5 = 294, // Key: F5 + F6 = 295, // Key: F6 + F7 = 296, // Key: F7 + F8 = 297, // Key: F8 + F9 = 298, // Key: F9 + F10 = 299, // Key: F10 + F11 = 300, // Key: F11 + F12 = 301, // Key: F12 + LeftShift = 340, // Key: Shift left + LeftControl = 341, // Key: Control left + LeftAlt = 342, // Key: Alt left + LeftSuper = 343, // Key: Super left + RightShift = 344, // Key: Shift right + RightControl = 345, // Key: Control right + RightAlt = 346, // Key: Alt right + RightSuper = 347, // Key: Super right + KbMenu = 348, // Key: KB menu + // Keypad keys + Kp0 = 320, // Key: Keypad 0 + Kp1 = 321, // Key: Keypad 1 + Kp2 = 322, // Key: Keypad 2 + Kp3 = 323, // Key: Keypad 3 + Kp4 = 324, // Key: Keypad 4 + Kp5 = 325, // Key: Keypad 5 + Kp6 = 326, // Key: Keypad 6 + Kp7 = 327, // Key: Keypad 7 + Kp8 = 328, // Key: Keypad 8 + Kp9 = 329, // Key: Keypad 9 + KpDecimal = 330, // Key: Keypad . + KpDivide = 331, // Key: Keypad / + KpMultiply = 332, // Key: Keypad * + KpSubtract = 333, // Key: Keypad - + KpAdd = 334, // Key: Keypad + + KpEnter = 335, // Key: Keypad Enter + KpEqual = 336, // Key: Keypad = + // Android key buttons + Back = 4, // Key: Android back button + VolumeUp = 24, // Key: Android volume up button + VolumeDown = 25, // Key: Android volume down button +} + +#[repr(i32)] +pub enum PaletteColor { + Background, + Foreground, + + Background1, + Background2, + Background3, + Background4, + + Foreground1, + Foreground2, + Foreground3, + Foreground4, + + Red, + Green, + Yellow, + Blue, + Purple, + Aqua, + Gray, + + BrightRed, + BrightGreen, + BrightYellow, + BrightBlue, + BrightPurple, + BrightAqua, + BrightGray, +} diff --git a/plugins/buffer_search/plugin.odin b/plugins/buffer_search/plugin.odin new file mode 100644 index 0000000..41d71fc --- /dev/null +++ b/plugins/buffer_search/plugin.odin @@ -0,0 +1,177 @@ +// A simple window to view/search open buffers +package buffer_search; + +import "core:runtime" +import "core:fmt" +import "core:path/filepath" +import "vendor:raylib" + +import p "../../src/plugin" +import "../../src/theme" + +Plugin :: p.Plugin; +Iterator :: p.Iterator; +BufferIter :: p.BufferIter; +BufferIndex :: p.BufferIndex; +Key :: p.Key; + +BufferListWindow :: struct { + selected_index: int, +} + +@export +OnInitialize :: proc "c" (plugin: Plugin) { + context = runtime.default_context(); + fmt.println("builtin buffer search plugin initialized!"); + + plugin.register_input_group(nil, .SPACE, proc "c" (plugin: Plugin, input_map: rawptr) { + plugin.register_input(input_map, .B, open_buffer_window, "show list of open buffers"); + }); +} + +@export +OnExit :: proc "c" (plugin: Plugin) { + context = runtime.default_context(); +} + +open_buffer_window :: proc "c" (plugin: Plugin) { + context = runtime.default_context(); + + window := new(BufferListWindow); + window^ = BufferListWindow {}; + + plugin.create_window(window, proc "c" (plugin: Plugin, input_map: rawptr) { + plugin.register_input(input_map, .K, proc "c" (plugin: Plugin) { + context = runtime.default_context(); + + win := cast(^BufferListWindow)plugin.get_window(); + if win != nil { + if win.selected_index > 0 { + win.selected_index -= 1; + } else { + win.selected_index = plugin.buffer.get_num_buffers()-1; + } + } + }, "move selection up"); + plugin.register_input(input_map, .J, proc "c" (plugin: Plugin) { + context = runtime.default_context(); + + win := cast(^BufferListWindow)plugin.get_window(); + if win != nil { + if win.selected_index < plugin.buffer.get_num_buffers()-1 { + win.selected_index += 1; + } else { + win.selected_index = 0; + } + } + }, "move selection down"); + plugin.register_input(input_map, .ENTER, proc "c" (plugin: Plugin) { + context = runtime.default_context(); + + win := cast(^BufferListWindow)plugin.get_window(); + if win != nil { + plugin.buffer.set_current_buffer(win.selected_index); + } + + plugin.request_window_close(); + }, "switch to buffer") + }, draw_buffer_window, free_buffer_window, nil); +} + +free_buffer_window :: proc "c" (plugin: Plugin, win: rawptr) { + context = runtime.default_context(); + win := cast(^BufferListWindow)plugin.get_window(); + if win == nil { + return; + } + + free(win); +} + +buffer_list_iter :: proc(plugin: Plugin, buffer_index: ^int) -> (int, int, bool) { + if buffer_index^ == -1 { + return 0, 0, false; + } + + index := plugin.iter.get_buffer_list_iter(buffer_index); + return index, 0, true; +} + +draw_buffer_window :: proc "c" (plugin: Plugin, win: rawptr) { + context = runtime.default_context(); + win := cast(^BufferListWindow)win; + if win == nil { + return; + } + + screen_width := plugin.get_screen_width(); + screen_height := plugin.get_screen_height(); + source_font_width := plugin.get_font_width(); + source_font_height := plugin.get_font_height(); + + win_rec := raylib.Rectangle { + x = f32(screen_width/8), + y = f32(screen_height/8), + width = f32(screen_width - screen_width/4), + height = f32(screen_height - screen_height/4), + }; + plugin.draw_rect( + i32(win_rec.x), + i32(win_rec.y), + i32(win_rec.width), + i32(win_rec.height), + .Background4 + ); + + win_margin := raylib.Vector2 { f32(source_font_width), f32(source_font_height) }; + + buffer_prev_width := (win_rec.width - win_margin.x*2) / 2; + buffer_prev_height := win_rec.height - win_margin.y*2; + + glyph_buffer_width := int(buffer_prev_width) / source_font_width - 1; + glyph_buffer_height := int(buffer_prev_height) / source_font_height; + + directory := string(plugin.get_current_directory()); + + plugin.draw_rect( + i32(win_rec.x + win_rec.width / 2), + i32(win_rec.y + win_margin.y), + i32(buffer_prev_width), + i32(buffer_prev_height), + .Background2, + ); + + _buffer_index := 0; + for index in buffer_list_iter(plugin, &_buffer_index) { + buffer := plugin.buffer.get_buffer_info_from_index(index); + relative_file_path, _ := filepath.rel(directory, string(buffer.file_path), context.temp_allocator) + text := fmt.ctprintf("%s:%d", relative_file_path, buffer.cursor.line+1); + text_width := len(text) * source_font_width; + + if index == win.selected_index { + plugin.draw_buffer_from_index( + index, + int(win_rec.x + win_margin.x + win_rec.width / 2), + int(win_rec.y + win_margin.y), + glyph_buffer_width, + glyph_buffer_height, + false); + + plugin.draw_rect( + i32(win_rec.x + win_margin.x), + i32(win_rec.y + win_margin.y) + i32(index * source_font_height), + i32(text_width), + i32(source_font_height), + .Background2, + ); + } + + plugin.draw_text( + text, + win_rec.x + win_margin.x, win_rec.y + win_margin.y + f32(index * source_font_height), + .Foreground2 + ); + + runtime.free_all(context.temp_allocator); + } +} diff --git a/lib-rg/Cargo.lock b/plugins/grep/Cargo.lock similarity index 98% rename from lib-rg/Cargo.lock rename to plugins/grep/Cargo.lock index c167218..144e9a4 100644 --- a/lib-rg/Cargo.lock +++ b/plugins/grep/Cargo.lock @@ -138,6 +138,16 @@ dependencies = [ "memmap2", ] +[[package]] +name = "grep_plugin" +version = "0.1.0" +dependencies = [ + "grep", + "plugin_rs_bindings", + "termcolor", + "walkdir", +] + [[package]] name = "itoa" version = "1.0.10" @@ -171,6 +181,10 @@ dependencies = [ "libc", ] +[[package]] +name = "plugin_rs_bindings" +version = "0.1.0" + [[package]] name = "proc-macro2" version = "1.0.74" @@ -206,15 +220,6 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" -[[package]] -name = "rg" -version = "0.1.0" -dependencies = [ - "grep", - "termcolor", - "walkdir", -] - [[package]] name = "ryu" version = "1.0.16" diff --git a/lib-rg/Cargo.toml b/plugins/grep/Cargo.toml similarity index 66% rename from lib-rg/Cargo.toml rename to plugins/grep/Cargo.toml index b08f4a8..d9c7c43 100644 --- a/lib-rg/Cargo.toml +++ b/plugins/grep/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "rg" +name = "grep_plugin" version = "0.1.0" edition = "2021" publish = false @@ -11,3 +11,4 @@ crate-type = ["cdylib"] grep = "0.3.1" termcolor = "1.4.0" walkdir = "2.4.0" +plugin_rs_bindings = { path = "../../plugin-rs-bindings" } diff --git a/plugins/grep/src/lib.rs b/plugins/grep/src/lib.rs new file mode 100644 index 0000000..323a7ec --- /dev/null +++ b/plugins/grep/src/lib.rs @@ -0,0 +1,419 @@ +use std::{ + error::Error, + ffi::OsString, + path::Path, + str::FromStr, + sync::mpsc::{Receiver, Sender}, + thread, +}; + +use grep::{ + regex::{RegexMatcher, RegexMatcherBuilder}, + searcher::{BinaryDetection, SearcherBuilder, Sink, SinkError}, +}; +use plugin_rs_bindings::{Buffer, Closure, Hook, InputMap, Key, PaletteColor, Plugin}; +use std::sync::mpsc::channel; +use walkdir::WalkDir; + +#[derive(Debug)] +pub enum SimpleSinkError { + StandardError, + NoLine, + BadString, +} + +impl SinkError for SimpleSinkError { + fn error_message(message: T) -> Self { + eprintln!("{message}"); + + Self::StandardError + } +} + +#[derive(Debug)] +struct Match { + text: Vec, + path: String, + line_number: Option, + column: u64, +} +impl Match { + fn from_sink_match_with_path( + 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; + + Ok(Self { + text: line, + path: path.unwrap_or_default(), + line_number: value.line_number(), + column, + }) + } +} + +#[derive(Default, Debug)] +struct SimpleSink { + current_path: Option, + matches: Vec, +} +impl Sink for SimpleSink { + type Error = SimpleSinkError; + + fn matched( + &mut self, + _searcher: &grep::searcher::Searcher, + mat: &grep::searcher::SinkMatch<'_>, + ) -> Result { + self.matches.push(Match::from_sink_match_with_path( + mat, + self.current_path.clone(), + )?); + + Ok(true) + } +} + +fn search(pattern: &str, paths: &[OsString]) -> Result> { + 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::default(); + for path in paths { + for result in WalkDir::new(path).into_iter().filter_entry(|dent| { + if dent.file_type().is_dir() + && (dent.path().ends_with("target") || dent.path().ends_with(".git")) + { + return false; + } + + true + }) { + let dent = match result { + Ok(dent) => dent, + Err(err) => { + eprintln!("{}", err); + continue; + } + }; + if !dent.file_type().is_file() { + continue; + } + sink.current_path = Some(dent.path().to_string_lossy().into()); + + let result = searcher.search_path(&matcher, dent.path(), &mut sink); + + if let Err(err) = result { + eprintln!("{}: {:?}", dent.path().display(), err); + } + } + } + + Ok(sink) +} + +enum Message { + Search((String, Vec)), + Quit, +} + +struct GrepWindow { + sink: Option, + selected_match: usize, + top_index: usize, + input_buffer: Option, + + tx: Sender, + rx: Receiver, +} + +#[no_mangle] +pub extern "C" fn OnInitialize(plugin: Plugin) { + println!("Grep Plugin Initialized"); + plugin.register_hook(Hook::BufferInput, on_buffer_input); + plugin.register_input_group( + None, + Key::Space, + Closure!((plugin: Plugin, input_map: InputMap) => { + (plugin.register_input)( + input_map, + Key::R, + Closure!((plugin: Plugin) => { + let (window_tx, thread_rx) = channel(); + let (thread_tx, window_rx) = channel(); + create_search_thread(thread_tx, thread_rx); + + let window = GrepWindow { + selected_match: 0, + top_index: 0, + input_buffer: Some(plugin.buffer_table.open_virtual_buffer()), + sink: None, + tx: window_tx, + rx: window_rx, + }; + + plugin.create_window(window, Closure!((plugin: Plugin, input_map: InputMap) => { + (plugin.enter_insert_mode)(); + + (plugin.register_input)(input_map, Key::I, Closure!((plugin: Plugin) => { + (plugin.enter_insert_mode)() + }), "\0".as_ptr()); + (plugin.register_input)(input_map, Key::Enter, Closure!((plugin: Plugin) => { + if let Some(window) = unsafe { plugin.get_window::() } { + match &window.sink { + Some(sink) => if window.selected_match < sink.matches.len() { + let mat = unsafe { &sink.matches.get_unchecked(window.selected_match) }; + plugin.buffer_table.open_buffer(&mat.path, (mat.line_number.unwrap_or(1)-1) as i32, 0); + (plugin.request_window_close)(); + }, + None => {}, + } + } + }), "move selection up\0".as_ptr()); + (plugin.register_input)(input_map, Key::K, Closure!((plugin: Plugin) => { + if let Some(window) = unsafe { plugin.get_window::() } { + + if window.selected_match > 0 { + window.selected_match -= 1; + + if window.selected_match < window.top_index { + window.top_index = window.selected_match; + } + } else { + window.selected_match = match &window.sink { + Some(sink) => sink.matches.len()-1, + None => 0, + }; + + window.top_index = window.selected_match; + } + } + }), "move selection up\0".as_ptr()); + (plugin.register_input)(input_map, Key::J, Closure!((plugin: Plugin) => { + if let Some(window) = unsafe { plugin.get_window::() } { + let screen_height = (plugin.get_screen_height)() as i32; + let font_height = (plugin.get_font_height)() as i32; + let height = screen_height - screen_height / 4; + let max_mats_to_draw = (height - font_height * 2) / (font_height) - 1; + + let match_count = match &window.sink { + Some(sink) => sink.matches.len(), + None => 0, + }; + + let index_threshold = std::cmp::max(max_mats_to_draw-4, 0) as usize; + + if window.selected_match < match_count-1 { + window.selected_match += 1; + + if window.selected_match - window.top_index > index_threshold { + window.top_index += 1; + } + } else { + window.selected_match = 0; + window.top_index = 0; + } + } + }), "move selection down\0".as_ptr()); + }), draw_window, free_window, Some(Closure!((_plugin: Plugin, window: *const std::ffi::c_void) -> Buffer => { + let window = Box::leak(unsafe { Box::::from_raw(window as *mut GrepWindow) }); + + if let Some(buffer) = window.input_buffer { + return buffer; + } else { + return Buffer::null(); + } + }))); + }), + "Open Grep Window\0".as_ptr(), + ); + }), + ); +} + +fn create_search_thread(tx: Sender, rx: Receiver) { + thread::spawn(move || { + while let Ok(message) = rx.recv() { + match message { + Message::Search((pattern, paths)) => { + if let Ok(sink) = search(&pattern, &paths) { + if let Err(err) = tx.send(sink) { + eprintln!("error getting grep results: {err:?}"); + return; + } + } + } + Message::Quit => return, + } + } + }); +} + +#[no_mangle] +pub extern "C" fn OnExit(_plugin: Plugin) { + println!("Grep Plugin Exiting"); +} + +extern "C" fn draw_window(plugin: Plugin, window: *const std::ffi::c_void) { + let window = Box::leak(unsafe { Box::::from_raw(window as *mut GrepWindow) }); + + let screen_width = (plugin.get_screen_width)() as i32; + let screen_height = (plugin.get_screen_height)() as i32; + let font_width = (plugin.get_font_width)() as i32; + let font_height = (plugin.get_font_height)() as i32; + + let x = screen_width / 8; + let y = screen_height / 8; + let width = screen_width - screen_width / 4; + let height = screen_height - screen_height / 4; + + let buffer_prev_width = (width - font_width * 2) / 2; + + let glyph_buffer_width = buffer_prev_width / font_width - 1; + let glyph_buffer_height = 1; + + let dir = plugin.get_current_directory(); + let directory = Path::new(dir.as_ref()); + + (plugin.draw_rect)(x, y, width, height, PaletteColor::Background4); + (plugin.draw_rect)( + x + font_width, + y + font_height, + width - font_width * 2, + height - font_height * 3, + PaletteColor::Background3, + ); + + if let Some(buffer) = window.input_buffer { + (plugin.draw_rect)( + x + font_width, + y + height - font_height * 2, + buffer_prev_width, + font_height, + PaletteColor::Background2, + ); + (plugin.draw_buffer)( + buffer, + (x + font_width) as isize, + (y + height - font_height * 2) as isize, + (glyph_buffer_width) as isize, + (glyph_buffer_height) as isize, + false, + ); + } + + if let Ok(sink) = window.rx.try_recv() { + window.sink = Some(sink); + } + + if let Some(sink) = &window.sink { + if !sink.matches.is_empty() { + let num_mats_to_draw = std::cmp::min( + (sink.matches.len() - window.top_index) as i32, + (height - font_height * 2) / (font_height) - 1, + ); + let max_mat_length = (width - font_width * 2) / font_width; + + for (i, mat) in sink.matches[window.top_index..].iter().enumerate() { + let index = i + window.top_index; + if i as i32 >= num_mats_to_draw { + break; + } + + let path = Path::new(&mat.path); + let relative_file_path = path + .strip_prefix(directory) + .unwrap_or(path) + .to_str() + .unwrap_or(""); + + let matched_text = String::from_utf8_lossy(&mat.text); + let text = match mat.line_number { + Some(line_number) => format!( + "{}:{}:{}: {}", + relative_file_path, line_number, mat.column, matched_text + ), + None => format!("{}:{}: {}", relative_file_path, mat.column, matched_text), + }; + let text = if text.len() > max_mat_length as usize { + text.as_str().split_at(max_mat_length as usize).0 + } else { + &text + }; + + let text = format!("{text}\0"); + + if index == window.selected_match { + (plugin.draw_rect)( + x + font_width, + y + font_height + ((index - window.top_index) as i32) * font_height, + (text.chars().count() as i32) * font_width, + font_height, + PaletteColor::Background2, + ); + } + + (plugin.draw_text)( + text.as_ptr() as *const i8, + (x + font_width) as f32, + (y + font_height + ((index - window.top_index) as i32) * font_height) as f32, + PaletteColor::Foreground2, + ); + } + } + } +} + +extern "C" fn on_buffer_input(plugin: Plugin, buffer: Buffer) { + // NOTE(pcleavelin): this is super jank, because another plugin could have a window open when + // this gets called, however its fine here because we aren't manipulating any data, and a check + // is made between the buffer pointers which will only be correct if its our window. + if let Some(window) = unsafe { plugin.get_window::() } { + if window.input_buffer == Some(buffer) { + window.selected_match = 0; + window.top_index = 0; + + if let Some(buffer_info) = plugin.buffer_table.get_buffer_info(buffer) { + if let Some(input) = buffer_info.input.try_as_str() { + let directory = OsString::from_str(plugin.get_current_directory().as_ref()); + + match directory { + Ok(dir) => { + if let Err(err) = window + .tx + .send(Message::Search((input.to_string(), vec![dir]))) + { + eprintln!("failed to grep: {err:?}"); + } + } + Err(_) => { + eprintln!("failed to parse directory"); + } + }; + } + } + } + } +} + +extern "C" fn free_window(plugin: Plugin, window: *const std::ffi::c_void) { + let mut window = unsafe { Box::::from_raw(window as *mut GrepWindow) }; + let _ = window.tx.send(Message::Quit); + + if let Some(buffer) = window.input_buffer { + plugin.buffer_table.free_virtual_buffer(buffer); + window.input_buffer = None; + } +} diff --git a/plugins/highlighter/src/plugin.odin b/plugins/highlighter/src/plugin.odin new file mode 100644 index 0000000..1b50204 --- /dev/null +++ b/plugins/highlighter/src/plugin.odin @@ -0,0 +1,420 @@ +// The default syntax highlighter plugin for Odin & Rust +package highlighter; + +import "core:runtime" +import "core:fmt" + +import p "../../../src/plugin" + +Plugin :: p.Plugin; +Iterator :: p.Iterator; +BufferIter :: p.BufferIter; +BufferIndex :: p.BufferIndex; + +@export +OnInitialize :: proc "c" (plugin: Plugin) { + context = runtime.default_context(); + fmt.println("builtin highlighter plugin initialized!"); + + plugin.register_highlighter(".odin", color_buffer_odin); + plugin.register_highlighter(".rs", color_buffer_rust); +} + +@export +OnExit :: proc "c" () { + context = runtime.default_context(); + fmt.println("Goodbye from the Odin Highlighter Plugin!"); +} + +@export +OnDraw :: proc "c" (plugin: Plugin) { + context = runtime.default_context(); +} + +iterate_buffer :: proc(iter_funcs: Iterator, it: ^BufferIter) -> (character: u8, idx: BufferIndex, cond: bool) { + result := iter_funcs.iterate_buffer(it); + + return result.char, it.cursor.index, result.should_continue; +} + +iterate_buffer_reverse :: proc(iter_funcs: Iterator, it: ^BufferIter) -> (character: u8, idx: BufferIndex, cond: bool) { + result := iter_funcs.iterate_buffer_reverse(it); + + return result.char, it.cursor.index, result.should_continue; +} + +iterate_buffer_until :: proc(plugin: Plugin, it: ^BufferIter, until_proc: rawptr) { + plugin.iter.iterate_buffer_until(it, until_proc); +} + +iterate_buffer_peek :: proc(plugin: Plugin, it: ^BufferIter) -> (character: u8, idx: BufferIndex, cond: bool) { + result := plugin.iter.iterate_buffer_peek(it); + + return result.char, it.cursor.index, result.should_continue; +} + +is_odin_keyword :: proc(plugin: Plugin, start: BufferIter, end: BufferIter) -> (matches: bool) { + keywords := []string { + "using", + "transmute", + "cast", + "distinct", + "opaque", + "where", + "struct", + "enum", + "union", + "bit_field", + "bit_set", + "if", + "when", + "else", + "do", + "for", + "switch", + "case", + "continue", + "break", + "size_of", + "offset_of", + "type_info_of", + "typeid_of", + "type_of", + "align_of", + "or_return", + "or_else", + "inline", + "no_inline", + "string", + "cstring", + "bool", + "b8", + "b16", + "b32", + "b64", + "rune", + "any", + "rawptr", + "f16", + "f32", + "f64", + "f16le", + "f16be", + "f32le", + "f32be", + "f64le", + "f64be", + "u8", + "u16", + "u32", + "u64", + "u128", + "u16le", + "u32le", + "u64le", + "u128le", + "u16be", + "u32be", + "u64be", + "u128be", + "uint", + "uintptr", + "i8", + "i16", + "i32", + "i64", + "i128", + "i16le", + "i32le", + "i64le", + "i128le", + "i16be", + "i32be", + "i64be", + "i128be", + "int", + "complex", + "complex32", + "complex64", + "complex128", + "quaternion", + "quaternion64", + "quaternion128", + "quaternion256", + "matrix", + "typeid", + "true", + "false", + "nil", + "dynamic", + "map", + "proc", + "in", + "notin", + "not_in", + "import", + "export", + "foreign", + "const", + "package", + "return", + "defer", + }; + + for keyword in keywords { + it := start; + keyword_index := 0; + + for character in iterate_buffer(plugin.iter, &it) { + if character != keyword[keyword_index] { + break; + } + + keyword_index += 1; + if keyword_index >= len(keyword)-1 && it == end { + if plugin.iter.get_char_at_iter(&it) == keyword[keyword_index] { + matches = true; + } + + break; + } else if keyword_index >= len(keyword)-1 { + break; + } else if it == end { + break; + } + } + + if matches { + break; + } + } + + return; +} + +is_rust_keyword :: proc(plugin: Plugin, start: BufferIter, end: BufferIter) -> (matches: bool) { + keywords := []string { + "as", + "break", + "const", + "continue", + "crate", + "else", + "enum", + "extern", + "false", + "fn", + "for", + "if", + "impl", + "in", + "let", + "loop", + "match", + "mod", + "move", + "mut", + "pub", + "ref", + "return", + "self", + "Self", + "static", + "struct", + "super", + "trait", + "true", + "type", + "unsafe", + "use", + "where", + "while", + "u8", + "i8", + "u16", + "i16", + "u32", + "i32", + "u64", + "i64", + "bool", + "usize", + "isize", + "str", + "String", + "Option", + "Result", + }; + + for keyword in keywords { + it := start; + keyword_index := 0; + + for character in iterate_buffer(plugin.iter, &it) { + if character != keyword[keyword_index] { + break; + } + + keyword_index += 1; + if keyword_index >= len(keyword)-1 && it == end { + if plugin.iter.get_char_at_iter(&it) == keyword[keyword_index] { + matches = true; + } + + break; + } else if keyword_index >= len(keyword)-1 { + break; + } else if it == end { + break; + } + } + + if matches { + break; + } + } + + return; +} + +color_buffer_odin :: proc "c" (plugin: Plugin, buffer: rawptr) { + context = runtime.default_context(); + + buffer := plugin.buffer.get_buffer_info(buffer); + + start_it := plugin.iter.get_buffer_iterator(buffer.buffer); + it := plugin.iter.get_buffer_iterator(buffer.buffer); + + for character in iterate_buffer(plugin.iter, &it) { + if it.cursor.line > buffer.glyph_buffer_height && (it.cursor.line - buffer.top_line) > buffer.glyph_buffer_height { + break; + } + + if character == '/' { + start_it = it; + // need to go back one character because `it` is on the next character + iterate_buffer_reverse(plugin.iter, &start_it); + + character, _, succ := iterate_buffer(plugin.iter, &it); + if !succ { break; } + + if character == '/' { + iterate_buffer_until(plugin, &it, plugin.iter.until_line_break); + plugin.buffer.color_char_at(it.buffer, start_it.cursor, it.cursor, 9); + } else if character == '*' { + // TODO: block comments + } + } else if character == '\'' { + start_it = it; + // need to go back one character because `it` is on the next character + iterate_buffer_reverse(plugin.iter, &start_it); + + // jump into the quoted text + iterate_buffer_until(plugin, &it, plugin.iter.until_single_quote); + plugin.buffer.color_char_at(it.buffer, start_it.cursor, it.cursor, 12); + + iterate_buffer(plugin.iter, &it); + } else if character == '"' { + start_it = it; + // need to go back one character because `it` is on the next character + iterate_buffer_reverse(plugin.iter, &start_it); + + // jump into the quoted text + iterate_buffer_until(plugin, &it, plugin.iter.until_double_quote); + plugin.buffer.color_char_at(it.buffer, start_it.cursor, it.cursor, 12); + + iterate_buffer(plugin.iter, &it); + } else if (character >= 'a' && character <= 'z') || (character >= 'A' && character <= 'Z') || character == '_' { + start_it = it; + // need to go back one character because `it` is on the next character + iterate_buffer_reverse(plugin.iter, &start_it); + it = start_it; + + iterate_buffer_until(plugin, &it, plugin.iter.until_end_of_word); + + if is_odin_keyword(plugin, start_it, it) { + plugin.buffer.color_char_at(it.buffer, start_it.cursor, it.cursor, 13); + + iterate_buffer(plugin.iter, &it); + } else if character, _, cond := iterate_buffer_peek(plugin, &it); cond { + if character == '(' { + plugin.buffer.color_char_at(it.buffer, start_it.cursor, it.cursor, 11); + iterate_buffer(plugin.iter, &it); + } + } else { + break; + } + } + } +} + +color_buffer_rust :: proc "c" (plugin: Plugin, buffer: rawptr) { + context = runtime.default_context(); + + buffer := plugin.buffer.get_buffer_info(buffer); + + start_it := plugin.iter.get_buffer_iterator(buffer.buffer); + it := plugin.iter.get_buffer_iterator(buffer.buffer); + + for character in iterate_buffer(plugin.iter, &it) { + if it.cursor.line > buffer.glyph_buffer_height && (it.cursor.line - buffer.top_line) > buffer.glyph_buffer_height { + break; + } + + if character == '/' { + start_it = it; + // need to go back one character because `it` is on the next character + iterate_buffer_reverse(plugin.iter, &start_it); + + character, _, succ := iterate_buffer(plugin.iter, &it); + if !succ { break; } + + if character == '/' { + iterate_buffer_until(plugin, &it, plugin.iter.until_line_break); + plugin.buffer.color_char_at(it.buffer, start_it.cursor, it.cursor, 9); + } else if character == '*' { + // TODO: block comments + } + } else if character == '\'' && false { + start_it = it; + // need to go back one character because `it` is on the next character + iterate_buffer_reverse(plugin.iter, &start_it); + + // jump into the quoted text + iterate_buffer_until(plugin, &it, plugin.iter.until_single_quote); + plugin.buffer.color_char_at(it.buffer, start_it.cursor, it.cursor, 12); + + iterate_buffer(plugin.iter, &it); + } else if character == '"' { + start_it = it; + // need to go back one character because `it` is on the next character + iterate_buffer_reverse(plugin.iter, &start_it); + + // jump into the quoted text + iterate_buffer_until(plugin, &it, plugin.iter.until_double_quote); + plugin.buffer.color_char_at(it.buffer, start_it.cursor, it.cursor, 12); + + iterate_buffer(plugin.iter, &it); + } else if (character >= 'a' && character <= 'z') || (character >= 'A' && character <= 'Z') || character == '_' { + start_it = it; + // need to go back one character because `it` is on the next character + iterate_buffer_reverse(plugin.iter, &start_it); + it = start_it; + + iterate_buffer_until(plugin, &it, plugin.iter.until_end_of_word); + + if is_rust_keyword(plugin, start_it, it) { + plugin.buffer.color_char_at(it.buffer, start_it.cursor, it.cursor, 13); + + iterate_buffer(plugin.iter, &it); + } else if character, _, cond := iterate_buffer_peek(plugin, &it); cond { + if character == '(' || character == '<' || character == '!' { + plugin.buffer.color_char_at(it.buffer, start_it.cursor, it.cursor, 11); + iterate_buffer(plugin.iter, &it); + } + } else { + break; + } + } + } +} diff --git a/src/core/core.odin b/src/core/core.odin index d8bf620..9c0fe01 100644 --- a/src/core/core.odin +++ b/src/core/core.odin @@ -1,24 +1,26 @@ package core +import "core:runtime" import "core:fmt" import "vendor:raylib" +import "../plugin" + Mode :: enum { Normal, Insert, } -WindowDrawProc :: proc(win: ^Window, state: ^State); -WindowFreeProc :: proc(win: ^Window, state: ^State); -WindowGetBufferProc :: proc(win: ^Window) -> ^FileBuffer; Window :: struct { input_map: InputMap, - draw: WindowDrawProc, - free: WindowFreeProc, + draw: plugin.WindowDrawProc, + free_user_data: plugin.WindowFreeProc, - get_buffer: WindowGetBufferProc, + get_buffer: plugin.WindowGetBufferProc, // TODO: create hook for when mode changes happen + + user_data: rawptr, } request_window_close :: proc(state: ^State) { state.should_close_window = true; @@ -26,8 +28,8 @@ request_window_close :: proc(state: ^State) { close_window_and_free :: proc(state: ^State) { if state.window != nil { - if state.window.free != nil { - state.window->free(state); + if state.window.free_user_data != nil { + state.window.free_user_data(state.plugin_vtable, state.window.user_data); } delete_input_map(&state.window.input_map); @@ -39,6 +41,8 @@ close_window_and_free :: proc(state: ^State) { } State :: struct { + ctx: runtime.Context, + mode: Mode, should_close: bool, screen_height: int, @@ -59,10 +63,24 @@ State :: struct { input_map: InputMap, current_input_map: ^InputMap, + + plugins: [dynamic]plugin.Interface, + plugin_vtable: plugin.Plugin, + highlighters: map[string]plugin.OnColorBufferProc, + hooks: map[plugin.Hook][dynamic]plugin.OnHookProc, } +add_hook :: proc(state: ^State, hook: plugin.Hook, hook_proc: plugin.OnHookProc) { + if _, exists := state.hooks[hook]; !exists { + state.hooks[hook] = make([dynamic]plugin.OnHookProc); + } + + runtime.append(&state.hooks[hook], hook_proc); +} + +PluginEditorAction :: proc "c" (plugin: plugin.Plugin); EditorAction :: proc(state: ^State); -InputGroup :: union {EditorAction, InputMap} +InputGroup :: union {PluginEditorAction, EditorAction, InputMap} Action :: struct { action: InputGroup, description: string, @@ -88,6 +106,18 @@ delete_input_map :: proc(input_map: ^InputMap) { // NOTE(pcleavelin): might be a bug in the compiler where it can't coerce // `EditorAction` to `InputGroup` when given as a proc parameter, that is why there // are two functions +register_plugin_key_action_single :: proc(input_map: ^InputMap, key: raylib.KeyboardKey, action: PluginEditorAction, description: string = "") { + if ok := key in input_map.key_actions; ok { + // TODO: log that key is already registered + fmt.eprintln("plugin key already registered with single action", key); + } + + input_map.key_actions[key] = Action { + action = action, + description = description, + }; +} + register_key_action_single :: proc(input_map: ^InputMap, key: raylib.KeyboardKey, action: EditorAction, description: string = "") { if ok := key in input_map.key_actions; ok { // TODO: log that key is already registered @@ -136,5 +166,5 @@ register_ctrl_key_action_group :: proc(input_map: ^InputMap, key: raylib.Keyboar }; } -register_key_action :: proc{register_key_action_single, register_key_action_group}; +register_key_action :: proc{register_plugin_key_action_single, register_key_action_single, register_key_action_group}; register_ctrl_key_action :: proc{register_ctrl_key_action_single, register_ctrl_key_action_group}; diff --git a/src/core/error.odin b/src/core/error.odin index f65d6f4..e8dfe14 100644 --- a/src/core/error.odin +++ b/src/core/error.odin @@ -5,6 +5,7 @@ import "core:runtime" ErrorType :: enum { None, FileIOError, + PluginLoadError, } Error :: struct { diff --git a/src/core/file_buffer.odin b/src/core/file_buffer.odin index 91eeea3..7c9b347 100644 --- a/src/core/file_buffer.odin +++ b/src/core/file_buffer.odin @@ -11,6 +11,7 @@ import "core:strings" import "vendor:raylib" import "../theme" +import "../plugin" ScrollDir :: enum { Up, @@ -48,6 +49,8 @@ FileBuffer :: struct { directory: string, file_path: string, + extension: string, + top_line: int, cursor: Cursor, @@ -584,6 +587,8 @@ new_file_buffer :: proc(allocator: mem.Allocator, file_path: string, base_dir: s dir = filepath.dir(fi.fullpath); } + extension := filepath.ext(fi.fullpath); + if original_content, success := os.read_entire_file_from_handle(fd); success { width := 256; height := 256; @@ -592,6 +597,7 @@ new_file_buffer :: proc(allocator: mem.Allocator, file_path: string, base_dir: s allocator = allocator, directory = dir, file_path = fi.fullpath, + extension = extension, original_content = slice.clone_to_dynamic(original_content), added_content = make([dynamic]u8, 0, 1024*1024), @@ -612,6 +618,44 @@ new_file_buffer :: proc(allocator: mem.Allocator, file_path: string, base_dir: s } } +next_buffer :: proc(state: ^State, prev_buffer: ^int) -> int { + index := prev_buffer^; + + if prev_buffer^ >= len(state.buffers)-1 { + prev_buffer^ = -1; + } else { + prev_buffer^ += 1; + } + + return index; +} + +into_buffer_info :: proc(state: ^State, buffer: ^FileBuffer) -> plugin.BufferInfo { + return plugin.BufferInfo { + buffer = buffer, + input = plugin.BufferInput { + bytes = raw_data(buffer.input_buffer), + length = len(buffer.input_buffer), + }, + cursor = plugin.Cursor { + col = buffer.cursor.col, + line = buffer.cursor.line, + index = plugin.BufferIndex { + slice_index = buffer.cursor.index.slice_index, + content_index = buffer.cursor.index.content_index, + } + }, + file_path = strings.clone_to_cstring(buffer.file_path, context.temp_allocator), + glyph_buffer_width = buffer.glyph_buffer_width, + glyph_buffer_height = buffer.glyph_buffer_height, + top_line = buffer.top_line, + }; +} +into_buffer_info_from_index :: proc(state: ^State, buffer_index: int) -> plugin.BufferInfo { + buffer := &state.buffers[buffer_index]; + return into_buffer_info(state, buffer); +} + free_file_buffer :: proc(buffer: ^FileBuffer) { delete(buffer.original_content); delete(buffer.added_content); @@ -620,145 +664,6 @@ free_file_buffer :: proc(buffer: ^FileBuffer) { delete(buffer.input_buffer); } -is_keyword :: proc(start: FileBufferIter, end: FileBufferIter) -> (matches: bool) { - keywords := []string { - "using", - "transmute", - "cast", - "distinct", - "opaque", - "where", - "struct", - "enum", - "union", - "bit_field", - "bit_set", - "if", - "when", - "else", - "do", - "for", - "switch", - "case", - "continue", - "break", - "size_of", - "offset_of", - "type_info_of", - "typeid_of", - "type_of", - "align_of", - "or_return", - "or_else", - "inline", - "no_inline", - "string", - "cstring", - "bool", - "b8", - "b16", - "b32", - "b64", - "rune", - "any", - "rawptr", - "f16", - "f32", - "f64", - "f16le", - "f16be", - "f32le", - "f32be", - "f64le", - "f64be", - "u8", - "u16", - "u32", - "u64", - "u128", - "u16le", - "u32le", - "u64le", - "u128le", - "u16be", - "u32be", - "u64be", - "u128be", - "uint", - "uintptr", - "i8", - "i16", - "i32", - "i64", - "i128", - "i16le", - "i32le", - "i64le", - "i128le", - "i16be", - "i32be", - "i64be", - "i128be", - "int", - "complex", - "complex32", - "complex64", - "complex128", - "quaternion", - "quaternion64", - "quaternion128", - "quaternion256", - "matrix", - "typeid", - "true", - "false", - "nil", - "dynamic", - "map", - "proc", - "in", - "notin", - "not_in", - "import", - "export", - "foreign", - "const", - "package", - "return", - "defer", - }; - - for keyword in keywords { - it := start; - keyword_index := 0; - - for character in iterate_file_buffer(&it) { - if character != keyword[keyword_index] { - break; - } - - keyword_index += 1; - if keyword_index >= len(keyword)-1 && it == end { - if get_character_at_iter(it) == keyword[keyword_index] { - matches = true; - } - - break; - } else if keyword_index >= len(keyword)-1 { - break; - } else if it == end { - break; - } - } - - if matches { - break; - } - } - - return; -} - color_character :: proc(buffer: ^FileBuffer, start: Cursor, end: Cursor, palette_index: theme.PaletteColor) { start, end := start, end; @@ -794,73 +699,6 @@ color_character :: proc(buffer: ^FileBuffer, start: Cursor, end: Cursor, palette } } -color_buffer :: proc(buffer: ^FileBuffer) { - start_it := new_file_buffer_iter(buffer); - it := new_file_buffer_iter(buffer); - - for character in iterate_file_buffer(&it) { - if it.cursor.line > it.buffer.glyph_buffer_height && (it.cursor.line - it.buffer.top_line) > it.buffer.glyph_buffer_height { - break; - } - - if character == '/' { - start_it = it; - // need to go back one character because `it` is on the next character - iterate_file_buffer_reverse(&start_it); - - character, _, succ := iterate_file_buffer(&it); - if !succ { break; } - - if character == '/' { - iterate_file_buffer_until(&it, until_line_break); - color_character(buffer, start_it.cursor, it.cursor, .Foreground4); - } else if character == '*' { - // TODO: block comments - } - } else if character == '\'' { - start_it = it; - // need to go back one character because `it` is on the next character - iterate_file_buffer_reverse(&start_it); - - // jump into the quoted text - iterate_file_buffer_until(&it, until_single_quote); - color_character(buffer, start_it.cursor, it.cursor, .Yellow); - - iterate_file_buffer(&it); - } else if character == '"' { - start_it = it; - // need to go back one character because `it` is on the next character - iterate_file_buffer_reverse(&start_it); - - // jump into the quoted text - iterate_file_buffer_until(&it, until_double_quote); - color_character(buffer, start_it.cursor, it.cursor, .Yellow); - - iterate_file_buffer(&it); - } else if (character >= 'a' && character <= 'z') || (character >= 'A' && character <= 'Z') || character == '_' { - start_it = it; - // need to go back one character because `it` is on the next character - iterate_file_buffer_reverse(&start_it); - it = start_it; - - iterate_file_buffer_until(&it, until_end_of_word); - - // TODO: color keywords - if is_keyword(start_it, it) { - color_character(buffer, start_it.cursor, it.cursor, .Blue); - } else if character, _, cond := iterate_peek(&it, iterate_file_buffer); cond { - if character == '(' { - color_character(buffer, start_it.cursor, it.cursor, .Green); - } - } else { - break; - } - - iterate_file_buffer(&it); - } - } -} - update_glyph_buffer :: proc(buffer: ^FileBuffer) { for &glyph in buffer.glyph_buffer { glyph = Glyph{}; @@ -916,7 +754,9 @@ update_glyph_buffer :: proc(buffer: ^FileBuffer) { draw_file_buffer :: proc(state: ^State, buffer: ^FileBuffer, x: int, y: int, font: raylib.Font, show_line_numbers: bool = true) { update_glyph_buffer(buffer); - color_buffer(buffer); + if highlighter, exists := state.highlighters[buffer.extension]; exists { + highlighter(state.plugin_vtable, buffer); + } padding := 0; if show_line_numbers { @@ -960,7 +800,7 @@ draw_file_buffer :: proc(state: ^State, buffer: ^FileBuffer, x: int, y: int, fon text_y := y + state.source_font_height * j; if show_line_numbers { - raylib.DrawTextEx(font, raylib.TextFormat("%d", begin + j + 1), raylib.Vector2 { f32(x), f32(text_y) }, f32(state.source_font_height), 0, theme.get_palette_raylib_color(.Background3)); + raylib.DrawTextEx(font, raylib.TextFormat("%d", begin + j + 1), raylib.Vector2 { f32(x), f32(text_y) }, f32(state.source_font_height), 0, theme.get_palette_raylib_color(.Background4)); } for i in 0.. 0 { if key >= 32 && key <= 125 && len(buffer.input_buffer) < 1024-1 { append(&buffer.input_buffer, u8(key)); + + for hook_proc in state.hooks[plugin.Hook.BufferInput] { + hook_proc(state.plugin_vtable, buffer); + } } key = raylib.GetCharPressed(); @@ -74,6 +85,10 @@ do_insert_mode :: proc(state: ^State, buffer: ^FileBuffer) { if raylib.IsKeyPressed(.BACKSPACE) { core.delete_content(buffer, 1); + + for hook_proc in state.hooks[plugin.Hook.BufferInput] { + hook_proc(state.plugin_vtable, buffer); + } } } @@ -87,15 +102,6 @@ switch_to_buffer :: proc(state: ^State, item: ^ui.MenuBarItem) { } register_default_leader_actions :: proc(input_map: ^core.InputMap) { - core.register_key_action(input_map, .B, proc(state: ^State) { - state.window = ui.create_buffer_list_window(); - state.current_input_map = &state.window.input_map; - }, "show list of open buffers"); - core.register_key_action(input_map, .R, proc(state: ^State) { - state.window = ui.create_grep_window(); - state.current_input_map = &state.window.input_map; - state.mode = .Insert; - }, "live grep"); core.register_key_action(input_map, .Q, proc(state: ^State) { state.current_input_map = &state.input_map; }, "close this help"); @@ -185,14 +191,519 @@ register_default_input_actions :: proc(input_map: ^core.InputMap) { register_default_go_actions(&(&input_map.key_actions[.G]).action.(core.InputMap)); } +load_plugin :: proc(info: os.File_Info, in_err: os.Errno, state: rawptr) -> (err: os.Errno, skip_dir: bool) { + state := cast(^State)state; + + relative_file_path, rel_error := filepath.rel(state.directory, info.fullpath); + extension := filepath.ext(info.fullpath); + + if extension == ".dylib" || extension == ".dll" || extension == ".so" { + if loaded_plugin, succ := plugin.try_load_plugin(info.fullpath); succ { + append(&state.plugins, loaded_plugin); + + if rel_error == .None { + fmt.println("Loaded", relative_file_path); + } else { + fmt.println("Loaded", info.fullpath); + } + } + } + + return in_err, skip_dir; +} + main :: proc() { - state := State { + state = State { + ctx = context, source_font_width = 8, source_font_height = 16, input_map = core.new_input_map(), window = nil, - directory = os.get_current_directory(), + plugins = make([dynamic]plugin.Interface), + highlighters = make(map[string]plugin.OnColorBufferProc), + hooks = make(map[plugin.Hook][dynamic]plugin.OnHookProc), + }; + state.plugin_vtable = plugin.Plugin { + state = cast(rawptr)&state, + register_hook = proc "c" (hook: plugin.Hook, on_hook: plugin.OnHookProc) { + context = state.ctx; + + core.add_hook(&state, hook, on_hook); + }, + register_highlighter = proc "c" (extension: cstring, on_color_buffer: plugin.OnColorBufferProc) { + context = state.ctx; + + extension := strings.clone(string(extension)); + + if _, exists := state.highlighters[extension]; exists { + fmt.eprintln("Highlighter already registered for", extension, "files"); + } else { + state.highlighters[extension] = on_color_buffer; + } + }, + register_input_group = proc "c" (input_map: rawptr, key: plugin.Key, register_group: plugin.InputGroupProc) { + context = state.ctx; + + to_be_edited_map: ^core.InputMap = nil; + key := raylib.KeyboardKey(int(key)); + + if input_map != nil { + to_be_edited_map = transmute(^core.InputMap)input_map; + } else { + to_be_edited_map = state.current_input_map; + } + + if action, exists := to_be_edited_map.key_actions[key]; exists { + switch value in action.action { + case core.PluginEditorAction: + fmt.eprintln("Plugin attempted to register input group on existing key action (added from Plugin)"); + case core.EditorAction: + fmt.eprintln("Plugin attempted to register input group on existing key action"); + case core.InputMap: + input_map := &(&to_be_edited_map.key_actions[key]).action.(core.InputMap); + register_group(state.plugin_vtable, transmute(rawptr)input_map); + } + } else { + core.register_key_action(to_be_edited_map, key, core.new_input_map(), "PLUGIN INPUT GROUP"); + register_group(state.plugin_vtable, &(&to_be_edited_map.key_actions[key]).action.(core.InputMap)); + } + }, + register_input = proc "c" (input_map: rawptr, key: plugin.Key, input_action: plugin.InputActionProc, description: cstring) { + context = state.ctx; + + to_be_edited_map: ^core.InputMap = nil; + key := raylib.KeyboardKey(int(key)); + description := strings.clone(string(description)); + + if input_map != nil { + to_be_edited_map = transmute(^core.InputMap)input_map; + } else { + to_be_edited_map = state.current_input_map; + } + + if action, exists := to_be_edited_map.key_actions[key]; exists { + switch value in action.action { + case core.PluginEditorAction: + fmt.eprintln("Plugin attempted to register key action on existing key action (added from Plugin)"); + case core.EditorAction: + fmt.eprintln("Plugin attempted to register input key action on existing key action"); + case core.InputMap: + fmt.eprintln("Plugin attempted to register input key action on existing input group"); + } + } else { + core.register_key_action(to_be_edited_map, key, input_action, description); + } + }, + create_window = proc "c" (user_data: rawptr, register_group: plugin.InputGroupProc, draw_proc: plugin.WindowDrawProc, free_window_proc: plugin.WindowFreeProc, get_buffer_proc: plugin.WindowGetBufferProc) -> rawptr { + context = state.ctx; + window := new(core.Window); + window^ = core.Window { + input_map = core.new_input_map(), + draw = draw_proc, + get_buffer = get_buffer_proc, + free_user_data = free_window_proc, + + user_data = user_data, + }; + + register_group(state.plugin_vtable, transmute(rawptr)&window.input_map); + + state.window = window; + state.current_input_map = &window.input_map; + + return window; + }, + get_window = proc "c" () -> rawptr { + if state.window != nil { + return state.window.user_data; + } + + return nil; + }, + request_window_close = proc "c" () { + context = state.ctx; + + core.request_window_close(&state); + }, + get_screen_width = proc "c" () -> int { + return state.screen_width; + }, + get_screen_height = proc "c" () -> int { + return state.screen_height; + }, + get_font_width = proc "c" () -> int { + return state.source_font_width; + }, + get_font_height = proc "c" () -> int { + return state.source_font_height; + }, + get_current_directory = proc "c" () -> cstring { + context = state.ctx; + + return strings.clone_to_cstring(state.directory, context.temp_allocator); + }, + enter_insert_mode = proc "c" () { + state.mode = .Insert; + }, + draw_rect = proc "c" (x: i32, y: i32, width: i32, height: i32, color: theme.PaletteColor) { + context = state.ctx; + + raylib.DrawRectangle(x, y, width, height, theme.get_palette_raylib_color(color)); + }, + draw_text = proc "c" (text: cstring, x: f32, y: f32, color: theme.PaletteColor) { + context = state.ctx; + + text := string(text); + for codepoint, index in text { + raylib.DrawTextCodepoint( + state.font, + rune(codepoint), + raylib.Vector2 { x + f32(index * state.source_font_width), y }, + f32(state.source_font_height), + theme.get_palette_raylib_color(color) + ); + } + }, + draw_buffer_from_index = proc "c" (buffer_index: int, x: int, y: int, glyph_buffer_width: int, glyph_buffer_height: int, show_line_numbers: bool) { + context = state.ctx; + state.buffers[buffer_index].glyph_buffer_width = glyph_buffer_width; + state.buffers[buffer_index].glyph_buffer_height = glyph_buffer_height; + + core.draw_file_buffer( + &state, + &state.buffers[buffer_index], + x, + y, + state.font, + show_line_numbers); + }, + draw_buffer = proc "c" (buffer: rawptr, x: int, y: int, glyph_buffer_width: int, glyph_buffer_height: int, show_line_numbers: bool) { + context = state.ctx; + + buffer := transmute(^core.FileBuffer)buffer; + buffer.glyph_buffer_width = glyph_buffer_width; + buffer.glyph_buffer_height = glyph_buffer_height; + + core.draw_file_buffer( + &state, + buffer, + x, + y, + state.font, + show_line_numbers); + }, + iter = plugin.Iterator { + get_current_buffer_iterator = proc "c" () -> plugin.BufferIter { + context = state.ctx; + + it := core.new_file_buffer_iter(&state.buffers[state.current_buffer]); + + // TODO: make this into a function + return plugin.BufferIter { + cursor = plugin.Cursor { + col = it.cursor.col, + line = it.cursor.line, + index = plugin.BufferIndex { + slice_index = it.cursor.index.slice_index, + content_index = it.cursor.index.content_index, + } + }, + buffer = cast(rawptr)it.buffer, + hit_end = it.hit_end, + } + }, + get_buffer_iterator = proc "c" (buffer: rawptr) -> plugin.BufferIter { + buffer := cast(^core.FileBuffer)buffer; + context = state.ctx; + + it := core.new_file_buffer_iter(buffer); + + // TODO: make this into a function + return plugin.BufferIter { + cursor = plugin.Cursor { + col = it.cursor.col, + line = it.cursor.line, + index = plugin.BufferIndex { + slice_index = it.cursor.index.slice_index, + content_index = it.cursor.index.content_index, + } + }, + buffer = cast(rawptr)it.buffer, + hit_end = it.hit_end, + } + }, + get_char_at_iter = proc "c" (it: ^plugin.BufferIter) -> u8 { + context = state.ctx; + + internal_it := core.FileBufferIter { + cursor = core.Cursor { + col = it.cursor.col, + line = it.cursor.line, + index = core.FileBufferIndex { + slice_index = it.cursor.index.slice_index, + content_index = it.cursor.index.content_index, + } + }, + buffer = cast(^core.FileBuffer)it.buffer, + hit_end = it.hit_end, + } + + return core.get_character_at_iter(internal_it); + }, + get_buffer_list_iter = proc "c" (prev_buffer: ^int) -> int { + context = state.ctx; + + return core.next_buffer(&state, prev_buffer); + }, + iterate_buffer = proc "c" (it: ^plugin.BufferIter) -> plugin.IterateResult { + context = state.ctx; + + // TODO: make this into a function + internal_it := core.FileBufferIter { + cursor = core.Cursor { + col = it.cursor.col, + line = it.cursor.line, + index = core.FileBufferIndex { + slice_index = it.cursor.index.slice_index, + content_index = it.cursor.index.content_index, + } + }, + buffer = cast(^core.FileBuffer)it.buffer, + hit_end = it.hit_end, + } + + char, _, cond := core.iterate_file_buffer(&internal_it); + + it^ = plugin.BufferIter { + cursor = plugin.Cursor { + col = internal_it.cursor.col, + line = internal_it.cursor.line, + index = plugin.BufferIndex { + slice_index = internal_it.cursor.index.slice_index, + content_index = internal_it.cursor.index.content_index, + } + }, + buffer = cast(rawptr)internal_it.buffer, + hit_end = internal_it.hit_end, + }; + + return plugin.IterateResult { + char = char, + should_continue = cond, + }; + }, + iterate_buffer_reverse = proc "c" (it: ^plugin.BufferIter) -> plugin.IterateResult { + context = state.ctx; + + // TODO: make this into a function + internal_it := core.FileBufferIter { + cursor = core.Cursor { + col = it.cursor.col, + line = it.cursor.line, + index = core.FileBufferIndex { + slice_index = it.cursor.index.slice_index, + content_index = it.cursor.index.content_index, + } + }, + buffer = cast(^core.FileBuffer)it.buffer, + hit_end = it.hit_end, + } + + char, _, cond := core.iterate_file_buffer_reverse(&internal_it); + + it^ = plugin.BufferIter { + cursor = plugin.Cursor { + col = internal_it.cursor.col, + line = internal_it.cursor.line, + index = plugin.BufferIndex { + slice_index = internal_it.cursor.index.slice_index, + content_index = internal_it.cursor.index.content_index, + } + }, + buffer = cast(rawptr)internal_it.buffer, + hit_end = internal_it.hit_end, + }; + + return plugin.IterateResult { + char = char, + should_continue = cond, + }; + }, + iterate_buffer_until = proc "c" (it: ^plugin.BufferIter, until_proc: rawptr) { + context = state.ctx; + + // TODO: make this into a function + internal_it := core.FileBufferIter { + cursor = core.Cursor { + col = it.cursor.col, + line = it.cursor.line, + index = core.FileBufferIndex { + slice_index = it.cursor.index.slice_index, + content_index = it.cursor.index.content_index, + } + }, + buffer = cast(^core.FileBuffer)it.buffer, + hit_end = it.hit_end, + } + + core.iterate_file_buffer_until(&internal_it, transmute(core.UntilProc)until_proc); + + it^ = plugin.BufferIter { + cursor = plugin.Cursor { + col = internal_it.cursor.col, + line = internal_it.cursor.line, + index = plugin.BufferIndex { + slice_index = internal_it.cursor.index.slice_index, + content_index = internal_it.cursor.index.content_index, + } + }, + buffer = cast(rawptr)internal_it.buffer, + hit_end = internal_it.hit_end, + }; + }, + iterate_buffer_peek = proc "c" (it: ^plugin.BufferIter) -> plugin.IterateResult { + context = state.ctx; + + // TODO: make this into a function + internal_it := core.FileBufferIter { + cursor = core.Cursor { + col = it.cursor.col, + line = it.cursor.line, + index = core.FileBufferIndex { + slice_index = it.cursor.index.slice_index, + content_index = it.cursor.index.content_index, + } + }, + buffer = cast(^core.FileBuffer)it.buffer, + hit_end = it.hit_end, + } + + char, _, cond := core.iterate_peek(&internal_it, core.iterate_file_buffer); + + it^ = plugin.BufferIter { + cursor = plugin.Cursor { + col = internal_it.cursor.col, + line = internal_it.cursor.line, + index = plugin.BufferIndex { + slice_index = internal_it.cursor.index.slice_index, + content_index = internal_it.cursor.index.content_index, + } + }, + buffer = cast(rawptr)internal_it.buffer, + hit_end = internal_it.hit_end, + }; + + return plugin.IterateResult { + char = char, + should_continue = cond, + }; + }, + until_line_break = transmute(rawptr)core.until_line_break, + until_single_quote = transmute(rawptr)core.until_single_quote, + until_double_quote = transmute(rawptr)core.until_double_quote, + until_end_of_word = transmute(rawptr)core.until_end_of_word, + }, + buffer = plugin.Buffer { + get_num_buffers = proc "c" () -> int { + context = state.ctx; + + return len(state.buffers); + }, + get_buffer_info = proc "c" (buffer: rawptr) -> plugin.BufferInfo { + context = state.ctx; + buffer := cast(^core.FileBuffer)buffer; + + return core.into_buffer_info(&state, buffer); + }, + get_buffer_info_from_index = proc "c" (buffer_index: int) -> plugin.BufferInfo { + context = state.ctx; + buffer := &state.buffers[buffer_index]; + + return core.into_buffer_info(&state, buffer); + }, + color_char_at = proc "c" (buffer: rawptr, start_cursor: plugin.Cursor, end_cursor: plugin.Cursor, palette_index: i32) { + buffer := cast(^core.FileBuffer)buffer; + context = state.ctx; + + start_cursor := core.Cursor { + col = start_cursor.col, + line = start_cursor.line, + index = core.FileBufferIndex { + slice_index = start_cursor.index.slice_index, + content_index = start_cursor.index.content_index, + } + }; + end_cursor := core.Cursor { + col = end_cursor.col, + line = end_cursor.line, + index = core.FileBufferIndex { + slice_index = end_cursor.index.slice_index, + content_index = end_cursor.index.content_index, + } + }; + + core.color_character(buffer, start_cursor, end_cursor, cast(theme.PaletteColor)palette_index); + }, + set_current_buffer = proc "c" (buffer_index: int) { + state.current_buffer = buffer_index; + }, + open_buffer = proc "c" (path: cstring, line: int, col: int) { + context = state.ctx; + + path := string(path); + should_create_buffer := true; + for buffer, index in state.buffers { + if strings.compare(buffer.file_path, path) == 0 { + state.current_buffer = index; + should_create_buffer = false; + break; + } + } + + buffer: ^core.FileBuffer = nil; + err := core.no_error(); + + if should_create_buffer { + new_buffer, err := core.new_file_buffer(context.allocator, strings.clone(path)); + if err.type != .None { + fmt.println("Failed to open/create file buffer:", err); + } else { + runtime.append(&state.buffers, new_buffer); + state.current_buffer = len(state.buffers)-1; + buffer = &state.buffers[state.current_buffer]; + } + } else { + buffer = &state.buffers[state.current_buffer]; + } + + if buffer != nil { + buffer.cursor.line = line; + buffer.cursor.col = col; + buffer.glyph_buffer_height = math.min(256, int((state.screen_height - state.source_font_height*2) / state.source_font_height)) + 1; + buffer.glyph_buffer_width = math.min(256, int((state.screen_width - state.source_font_width) / state.source_font_width)); + core.update_file_buffer_index_from_cursor(buffer); + } + }, + open_virtual_buffer = proc "c" () -> rawptr { + context = state.ctx; + + buffer := new(FileBuffer); + buffer^ = core.new_virtual_file_buffer(context.allocator); + + return buffer; + }, + free_virtual_buffer = proc "c" (buffer: rawptr) { + context = state.ctx; + + if buffer != nil { + buffer := cast(^core.FileBuffer)buffer; + + core.free_file_buffer(buffer); + free(buffer); + } + }, + } }; state.current_input_map = &state.input_map; register_default_input_actions(&state.input_map); @@ -216,6 +727,16 @@ main :: proc() { runtime.append(&buffer_items, item); } + // Load plugins + // TODO(pcleavelin): Get directory of binary instead of shells current working directory + filepath.walk(filepath.join({ os.get_current_directory(), "bin" }), load_plugin, transmute(rawptr)&state); + + for plugin in state.plugins { + if plugin.on_initialize != nil { + plugin.on_initialize(state.plugin_vtable); + } + } + raylib.InitWindow(640, 480, "odin_editor - [back to basics]"); raylib.SetWindowState({ .WINDOW_RESIZABLE, .VSYNC_HINT }); raylib.SetTargetFPS(60); @@ -248,6 +769,14 @@ main :: proc() { defer raylib.EndDrawing(); raylib.ClearBackground(theme.get_palette_raylib_color(.Background)); + + // TODO: be more granular in /what/ is being draw by the plugin + for plugin in state.plugins { + if plugin.on_initialize != nil { + //plugin.on_draw(plugin.plugin); + } + } + core.draw_file_buffer(&state, buffer, 32, state.source_font_height, state.font); ui.draw_menu_bar(&state, &menu_bar_state, 0, 0, i32(state.screen_width), i32(state.screen_height), state.source_font_height); @@ -320,7 +849,7 @@ main :: proc() { theme.get_palette_raylib_color(.Background1)); if state.window != nil && state.window.draw != nil { - state.window->draw(&state); + state.window.draw(state.plugin_vtable, state.window.user_data); } if state.current_input_map != &state.input_map { @@ -374,13 +903,15 @@ main :: proc() { switch state.mode { case .Normal: if state.window != nil && state.window.get_buffer != nil { - do_normal_mode(&state, state.window->get_buffer()); + buffer := transmute(^core.FileBuffer)(state.window.get_buffer(state.plugin_vtable, state.window.user_data)); + do_normal_mode(&state, buffer); } else { do_normal_mode(&state, buffer); } case .Insert: if state.window != nil && state.window.get_buffer != nil { - do_insert_mode(&state, state.window->get_buffer()); + buffer := transmute(^core.FileBuffer)(state.window.get_buffer(state.plugin_vtable, state.window.user_data)); + do_insert_mode(&state, buffer); } else { do_insert_mode(&state, buffer); } @@ -392,5 +923,13 @@ main :: proc() { } ui.test_menu_bar(&state, &menu_bar_state, 0,0, mouse_pos, raylib.IsMouseButtonReleased(.LEFT), state.source_font_height); + + runtime.free_all(context.temp_allocator); + } + + for plugin in state.plugins { + if plugin.on_exit != nil { + plugin.on_exit(); + } } } diff --git a/src/plugin/plugin.odin b/src/plugin/plugin.odin new file mode 100644 index 0000000..83365d6 --- /dev/null +++ b/src/plugin/plugin.odin @@ -0,0 +1,275 @@ +package plugin; + +import "core:intrinsics" +import "core:dynlib" +import "core:fmt" +import "vendor:raylib" + +import "../theme" + +OnInitializeProc :: proc "c" (plugin: Plugin); +OnExitProc :: proc "c" (/* probably needs some state eventually */); +OnDrawProc :: proc "c" (plugin: Plugin); +Interface :: struct { + on_initialize: OnInitializeProc, + on_exit: OnExitProc, + on_draw: OnDrawProc, +} + +BufferIndex :: struct { + slice_index: int, + content_index: int, +} + +Cursor :: struct { + col: int, + line: int, + index: BufferIndex, +} + +BufferIter :: struct { + cursor: Cursor, + buffer: rawptr, + hit_end: bool, +} + +IterateResult :: struct { + char: u8, + should_continue: bool, +} + +BufferInput :: struct { + bytes: [^]u8, + length: int, +} + +BufferInfo :: struct { + buffer: rawptr, + file_path: cstring, + input: BufferInput, + + cursor: Cursor, + + glyph_buffer_width: int, + glyph_buffer_height: int, + top_line: int, +} + +Buffer :: struct { + get_num_buffers: proc "c" () -> int, + get_buffer_info: proc "c" (buffer: rawptr) -> BufferInfo, + get_buffer_info_from_index: proc "c" (buffer_index: int) -> BufferInfo, + color_char_at: proc "c" (buffer: rawptr, start_cursor: Cursor, end_cursor: Cursor, palette_index: i32), + set_current_buffer: proc "c" (buffer_index: int), + + open_buffer: proc "c" (path: cstring, line: int, col: int), + open_virtual_buffer: proc "c" () -> rawptr, + free_virtual_buffer: proc "c" (buffer: rawptr), +} + +Iterator :: struct { + get_current_buffer_iterator: proc "c" () -> BufferIter, + get_buffer_iterator: proc "c" (buffer: rawptr) -> BufferIter, + get_char_at_iter: proc "c" (it: ^BufferIter) -> u8, + get_buffer_list_iter: proc "c" (prev_buffer: ^int) -> int, + + iterate_buffer: proc "c" (it: ^BufferIter) -> IterateResult, + iterate_buffer_reverse: proc "c" (it: ^BufferIter) -> IterateResult, + iterate_buffer_until: proc "c" (it: ^BufferIter, until_proc: rawptr), + iterate_buffer_until_reverse: proc "c" (it: ^BufferIter, until_proc: rawptr), + iterate_buffer_peek: proc "c" (it: ^BufferIter) -> IterateResult, + + until_line_break: rawptr, + until_single_quote: rawptr, + until_double_quote: rawptr, + until_end_of_word: rawptr, +} + +OnColorBufferProc :: proc "c" (plugin: Plugin, buffer: rawptr); +InputGroupProc :: proc "c" (plugin: Plugin, input_map: rawptr); +InputActionProc :: proc "c" (plugin: Plugin); +OnHookProc :: proc "c" (plugin: Plugin, buffer: rawptr); + +WindowInputProc :: proc "c" (plugin: Plugin, window: rawptr); +WindowDrawProc :: proc "c" (plugin: Plugin, window: rawptr); +WindowGetBufferProc :: proc(plugin: Plugin, window: rawptr) -> rawptr; +WindowFreeProc :: proc "c" (plugin: Plugin, window: rawptr); +Plugin :: struct { + state: rawptr, + iter: Iterator, + buffer: Buffer, + + register_hook: proc "c" (hook: Hook, on_hook: OnHookProc), + register_highlighter: proc "c" (extension: cstring, on_color_buffer: OnColorBufferProc), + + register_input_group: proc "c" (input_map: rawptr, key: Key, register_group: InputGroupProc), + register_input: proc "c" (input_map: rawptr, key: Key, input_action: InputActionProc, description: cstring), + + create_window: proc "c" (user_data: rawptr, register_group: InputGroupProc, draw_proc: WindowDrawProc, free_window_proc: WindowFreeProc, get_buffer_proc: WindowGetBufferProc) -> rawptr, + get_window: proc "c" () -> rawptr, + + request_window_close: proc "c" (), + get_screen_width: proc "c" () -> int, + get_screen_height: proc "c" () -> int, + get_font_width: proc "c" () -> int, + get_font_height: proc "c" () -> int, + get_current_directory: proc "c" () -> cstring, + enter_insert_mode: proc "c" (), + + draw_rect: proc "c" (x: i32, y: i32, width: i32, height: i32, color: theme.PaletteColor), + draw_text: proc "c" (text: cstring, x: f32, y: f32, color: theme.PaletteColor), + draw_buffer_from_index: proc "c" (buffer_index: int, x: int, y: int, glyph_buffer_width: int, glyph_buffer_height: int, show_line_numbers: bool), + draw_buffer: proc "c" (buffer: rawptr, x: int, y: int, glyph_buffer_width: int, glyph_buffer_height: int, show_line_numbers: bool), +} + +Hook :: enum { + BufferInput = 0, +} + +Key :: enum { + KEY_NULL = 0, // Key: NULL, used for no key pressed + // Alphanumeric keys + APOSTROPHE = 39, // Key: ' + COMMA = 44, // Key: , + MINUS = 45, // Key: - + PERIOD = 46, // Key: . + SLASH = 47, // Key: / + ZERO = 48, // Key: 0 + ONE = 49, // Key: 1 + TWO = 50, // Key: 2 + THREE = 51, // Key: 3 + FOUR = 52, // Key: 4 + FIVE = 53, // Key: 5 + SIX = 54, // Key: 6 + SEVEN = 55, // Key: 7 + EIGHT = 56, // Key: 8 + NINE = 57, // Key: 9 + SEMICOLON = 59, // Key: ; + EQUAL = 61, // Key: = + A = 65, // Key: A | a + B = 66, // Key: B | b + C = 67, // Key: C | c + D = 68, // Key: D | d + E = 69, // Key: E | e + F = 70, // Key: F | f + G = 71, // Key: G | g + H = 72, // Key: H | h + I = 73, // Key: I | i + J = 74, // Key: J | j + K = 75, // Key: K | k + L = 76, // Key: L | l + M = 77, // Key: M | m + N = 78, // Key: N | n + O = 79, // Key: O | o + P = 80, // Key: P | p + Q = 81, // Key: Q | q + R = 82, // Key: R | r + S = 83, // Key: S | s + T = 84, // Key: T | t + U = 85, // Key: U | u + V = 86, // Key: V | v + W = 87, // Key: W | w + X = 88, // Key: X | x + Y = 89, // Key: Y | y + Z = 90, // Key: Z | z + LEFT_BRACKET = 91, // Key: [ + BACKSLASH = 92, // Key: '\' + RIGHT_BRACKET = 93, // Key: ] + GRAVE = 96, // Key: ` + // Function keys + SPACE = 32, // Key: Space + ESCAPE = 256, // Key: Esc + ENTER = 257, // Key: Enter + TAB = 258, // Key: Tab + BACKSPACE = 259, // Key: Backspace + INSERT = 260, // Key: Ins + DELETE = 261, // Key: Del + RIGHT = 262, // Key: Cursor right + LEFT = 263, // Key: Cursor left + DOWN = 264, // Key: Cursor down + UP = 265, // Key: Cursor up + PAGE_UP = 266, // Key: Page up + PAGE_DOWN = 267, // Key: Page down + HOME = 268, // Key: Home + END = 269, // Key: End + CAPS_LOCK = 280, // Key: Caps lock + SCROLL_LOCK = 281, // Key: Scroll down + NUM_LOCK = 282, // Key: Num lock + PRINT_SCREEN = 283, // Key: Print screen + PAUSE = 284, // Key: Pause + F1 = 290, // Key: F1 + F2 = 291, // Key: F2 + F3 = 292, // Key: F3 + F4 = 293, // Key: F4 + F5 = 294, // Key: F5 + F6 = 295, // Key: F6 + F7 = 296, // Key: F7 + F8 = 297, // Key: F8 + F9 = 298, // Key: F9 + F10 = 299, // Key: F10 + F11 = 300, // Key: F11 + F12 = 301, // Key: F12 + LEFT_SHIFT = 340, // Key: Shift left + LEFT_CONTROL = 341, // Key: Control left + LEFT_ALT = 342, // Key: Alt left + LEFT_SUPER = 343, // Key: Super left + RIGHT_SHIFT = 344, // Key: Shift right + RIGHT_CONTROL = 345, // Key: Control right + RIGHT_ALT = 346, // Key: Alt right + RIGHT_SUPER = 347, // Key: Super right + KB_MENU = 348, // Key: KB menu + // Keypad keys + KP_0 = 320, // Key: Keypad 0 + KP_1 = 321, // Key: Keypad 1 + KP_2 = 322, // Key: Keypad 2 + KP_3 = 323, // Key: Keypad 3 + KP_4 = 324, // Key: Keypad 4 + KP_5 = 325, // Key: Keypad 5 + KP_6 = 326, // Key: Keypad 6 + KP_7 = 327, // Key: Keypad 7 + KP_8 = 328, // Key: Keypad 8 + KP_9 = 329, // Key: Keypad 9 + KP_DECIMAL = 330, // Key: Keypad . + KP_DIVIDE = 331, // Key: Keypad / + KP_MULTIPLY = 332, // Key: Keypad * + KP_SUBTRACT = 333, // Key: Keypad - + KP_ADD = 334, // Key: Keypad + + KP_ENTER = 335, // Key: Keypad Enter + KP_EQUAL = 336, // Key: Keypad = + // Android key buttons + BACK = 4, // Key: Android back button + MENU = 82, // Key: Android menu button + VOLUME_UP = 24, // Key: Android volume up button + VOLUME_DOWN = 25, // Key: Android volume down button +} + + +load_proc_address :: proc(lib_path: string, library: dynlib.Library, symbol: string, $ProcType: typeid) -> ProcType + where intrinsics.type_is_proc(ProcType) +{ + if address, found := dynlib.symbol_address(library, symbol); found { + return transmute(ProcType)address; + } else { + fmt.println("Could not find symbol", symbol, "in library", lib_path); + } + + return nil; +} + +try_load_plugin :: proc(lib_path: string) -> (plugin: Interface, success: bool) { + library, ok := dynlib.load_library(lib_path) + if !ok { + return {}, false; + } + + interface := Interface { + on_initialize = load_proc_address(lib_path, library, "OnInitialize", OnInitializeProc), + on_exit = load_proc_address(lib_path, library, "OnExit", OnExitProc), + on_draw = load_proc_address(lib_path, library, "OnDraw", OnDrawProc), + }; + + if interface.on_initialize == nil do return interface, false; + if interface.on_exit == nil do return interface, false; + + return interface, true +} diff --git a/src/ui/buffer_list_window.odin b/src/ui/buffer_list_window.odin deleted file mode 100644 index 346c0fb..0000000 --- a/src/ui/buffer_list_window.odin +++ /dev/null @@ -1,122 +0,0 @@ -package ui; - -import "core:math" -import "core:path/filepath" -import "vendor:raylib" - -import "../core" -import "../theme" - -BufferListWindow :: struct { - using window: core.Window, - - selected_buffer: int, -} - -create_buffer_list_window :: proc() -> ^BufferListWindow { - input_map := core.new_input_map(); - - core.register_key_action(&input_map, .K, proc(state: ^core.State) { - win := cast(^BufferListWindow)(state.window); - - if win.selected_buffer > 0 { - win.selected_buffer -= 1; - } else { - win.selected_buffer = len(state.buffers)-1; - } - - }, "move selection up"); - core.register_key_action(&input_map, .J, proc(state: ^core.State) { - win := cast(^BufferListWindow)(state.window); - - if win.selected_buffer >= len(state.buffers)-1 { - win.selected_buffer = 0; - } else { - win.selected_buffer += 1; - } - }, "move selection down"); - core.register_key_action(&input_map, .ENTER, proc(state: ^core.State) { - win := cast(^BufferListWindow)(state.window); - - state.current_buffer = win.selected_buffer; - - core.request_window_close(state); - }, "switch to file"); - - core.register_key_action(&input_map, .Q, proc(state: ^core.State) { - core.request_window_close(state); - }, "close window"); - - list_window := new(BufferListWindow); - list_window^ = BufferListWindow { - window = core.Window { - input_map = input_map, - draw = draw_buffer_list_window, - }, - }; - - return list_window; -} - -draw_buffer_list_window :: proc(win: ^core.Window, state: ^core.State) { - win := cast(^BufferListWindow)(win); - - win_rec := raylib.Rectangle { - x = f32(state.screen_width/8), - y = f32(state.screen_height/8), - width = f32(state.screen_width - state.screen_width/4), - height = f32(state.screen_height - state.screen_height/4), - }; - raylib.DrawRectangleRec( - win_rec, - theme.get_palette_raylib_color(.Background4)); - - win_margin := raylib.Vector2 { f32(state.source_font_width), f32(state.source_font_height) }; - - buffer_prev_width := (win_rec.width - win_margin.x*2) / 2; - buffer_prev_height := win_rec.height - win_margin.y*2; - - glyph_buffer_width := int(buffer_prev_width) / state.source_font_width - 1; - glyph_buffer_height := int(buffer_prev_height) / state.source_font_height; - - raylib.DrawRectangle( - i32(win_rec.x + win_rec.width / 2), - i32(win_rec.y + win_margin.y), - i32(buffer_prev_width), - i32(buffer_prev_height), - theme.get_palette_raylib_color(.Background2)); - - for _, index in state.buffers { - buffer := &state.buffers[index]; - relative_file_path, _ := filepath.rel(state.directory, buffer.file_path) - text := raylib.TextFormat("%s:%d", relative_file_path, buffer.cursor.line+1); - text_width := raylib.MeasureTextEx(state.font, text, f32(state.source_font_height), 0); - - if index == win.selected_buffer { - buffer.glyph_buffer_height = glyph_buffer_height; - buffer.glyph_buffer_width = glyph_buffer_width; - core.draw_file_buffer( - state, - buffer, - int(win_rec.x + win_margin.x + win_rec.width / 2), - int(win_rec.y + win_margin.y), - state.font, - show_line_numbers = false); - - raylib.DrawRectangle( - i32(win_rec.x + win_margin.x), - i32(win_rec.y + win_margin.y) + i32(index * state.source_font_height), - i32(text_width.x), - i32(state.source_font_height), - theme.get_palette_raylib_color(.Background2)); - } - - raylib.DrawTextEx( - state.font, - text, - raylib.Vector2 { win_rec.x + win_margin.x, win_rec.y + win_margin.y + f32(index * state.source_font_height) }, - f32(state.source_font_height), - 0, - theme.get_palette_raylib_color(.Foreground2)); - } -} diff --git a/src/ui/grep_window.odin b/src/ui/grep_window.odin deleted file mode 100644 index 3f5c016..0000000 --- a/src/ui/grep_window.odin +++ /dev/null @@ -1,285 +0,0 @@ -package ui; - -@(extra_linker_flags="-L./lib-rg/target/debug/") -foreign import rg "system:rg" - -ExternMatch :: struct { - text_ptr: [^]u8, - text_len: int, - text_cap: int, - - path_ptr: [^]u8, - path_len: int, - path_cap: int, - - line: u64, - col: u64 -} - -ExternMatchArray :: struct { - matches: [^]ExternMatch, - len: uint, - capacity: uint, -} - -foreign rg { - rg_search :: proc (pattern: cstring, path: cstring) -> ExternMatchArray --- - drop_match_array :: proc(match_array: ExternMatchArray) --- -} - -import "core:os" -import "core:path/filepath" -import "core:math" -import "core:fmt" -import "core:runtime" -import "core:strings" -import "vendor:raylib" - -import "../core" -import "../theme" - -GrepMatch :: struct { - text: string, - path: string, - line: int, - col: int, -} - -transmute_extern_matches :: proc(extern_matches: ExternMatchArray, dest: ^[dynamic]GrepMatch) { - if extern_matches.matches != nil { - for i in 0.. 0 { - path, _ = filepath.abs(strings.string_from_ptr(match.path_ptr, match.path_len)); - } - - text: string = ""; - if match.text_ptr != nil && match.text_len > 0 { - text = strings.string_from_ptr(match.text_ptr, match.text_len); - } - - cloned := GrepMatch { - text = text, - path = path, - line = int(match.line), - col = int(match.col) - }; - - append(dest, cloned); - } - } -} - -GrepWindow :: struct { - using window: core.Window, - - input_buffer: core.FileBuffer, - - selected_match: int, - - extern_matches: ExternMatchArray, - matches: [dynamic]GrepMatch, -} - -create_grep_window :: proc() -> ^GrepWindow { - input_map := core.new_input_map(); - - core.register_key_action(&input_map, .ENTER, proc(state: ^core.State) { - win := cast(^GrepWindow)(state.window); - - if win.matches != nil && len(win.matches) > 0 { - should_create_buffer := true; - for buffer, index in state.buffers { - if strings.compare(buffer.file_path, win.matches[win.selected_match].path) == 0 { - state.current_buffer = index; - should_create_buffer = false; - break; - } - } - - buffer: ^core.FileBuffer = nil; - err := core.no_error(); - - if should_create_buffer { - new_buffer, err := core.new_file_buffer(context.allocator, strings.clone(win.matches[win.selected_match].path)); - if err.type != .None { - fmt.println("Failed to open/create file buffer:", err); - } else { - runtime.append(&state.buffers, new_buffer); - state.current_buffer = len(state.buffers)-1; - buffer = &state.buffers[state.current_buffer]; - } - } else { - buffer = &state.buffers[state.current_buffer]; - } - - if buffer != nil { - buffer.cursor.line = win.matches[win.selected_match].line-1; - buffer.cursor.col = 0; - buffer.glyph_buffer_height = math.min(256, int((state.screen_height - state.source_font_height*2) / state.source_font_height)) + 1; - buffer.glyph_buffer_width = math.min(256, int((state.screen_width - state.source_font_width) / state.source_font_width)); - core.update_file_buffer_index_from_cursor(buffer); - - core.request_window_close(state); - } - } - }, "jump to location"); - - core.register_key_action(&input_map, .I, proc(state: ^core.State) { - state.mode = .Insert; - }, "enter insert mode"); - - core.register_key_action(&input_map, .T, proc(state: ^core.State) { - win := cast(^GrepWindow)(state.window); - - grep_files(win, state); - }, "example search"); - core.register_key_action(&input_map, .K, proc(state: ^core.State) { - win := cast(^GrepWindow)(state.window); - - if win.selected_match > 0 { - win.selected_match -= 1; - } else { - win.selected_match = len(win.matches)-1; - } - - }, "move selection up"); - core.register_key_action(&input_map, .J, proc(state: ^core.State) { - win := cast(^GrepWindow)(state.window); - - if win.selected_match >= len(win.matches)-1 { - win.selected_match = 0; - } else { - win.selected_match += 1; - } - }, "move selection down"); - - grep_window := new(GrepWindow); - grep_window^ = GrepWindow { - window = core.Window { - input_map = input_map, - draw = draw_grep_window, - get_buffer = grep_window_get_buffer, - free = free_grep_window, - }, - - input_buffer = core.new_virtual_file_buffer(context.allocator), - matches = make([dynamic]GrepMatch), - }; - - return grep_window; -} - -free_grep_window :: proc(win: ^core.Window, state: ^core.State) { - win := cast(^GrepWindow)(win); - - if win.extern_matches.matches != nil { - drop_match_array(win.extern_matches); - win.extern_matches.matches = nil; - win.extern_matches.len = 0; - win.extern_matches.capacity = 0; - } - - delete(win.matches); - core.free_file_buffer(&win.input_buffer); -} - -grep_window_get_buffer :: proc(win: ^core.Window) -> ^core.FileBuffer { - win := cast(^GrepWindow)(win); - - return &win.input_buffer; -} - -@private -grep_files :: proc(win: ^core.Window, state: ^core.State) { - win := cast(^GrepWindow)(win); - - if win.extern_matches.matches != nil { - drop_match_array(win.extern_matches); - win.extern_matches.matches = nil; - win.extern_matches.len = 0; - win.extern_matches.capacity = 0; - } - - if win.matches != nil { - clear_dynamic_array(&win.matches); - } else { - win.matches = make([dynamic]GrepMatch); - } - - builder := strings.builder_make(); - it := core.new_file_buffer_iter(&win.input_buffer); - for character in core.iterate_file_buffer(&it) { - if character == '\n' { break; } - - strings.write_rune(&builder, rune(character)); - } - pattern := strings.clone_to_cstring(strings.to_string(builder)); - - win.extern_matches = rg_search(pattern, strings.clone_to_cstring(state.directory)); - transmute_extern_matches(win.extern_matches, &win.matches); -} - -draw_grep_window :: proc(win: ^core.Window, state: ^core.State) { - win := cast(^GrepWindow)(win); - - win_rec := raylib.Rectangle { - x = f32(state.screen_width/8), - y = f32(state.screen_height/8), - width = f32(state.screen_width - state.screen_width/4), - height = f32(state.screen_height - state.screen_height/4), - }; - raylib.DrawRectangleRec( - win_rec, - theme.get_palette_raylib_color(.Background4)); - - win_margin := raylib.Vector2 { f32(state.source_font_width), f32(state.source_font_height) }; - - buffer_prev_width := (win_rec.width - win_margin.x*2) / 2; - buffer_prev_height := win_rec.height - win_margin.y*2; - - glyph_buffer_width := int(buffer_prev_width) / state.source_font_width - 1; - glyph_buffer_height := 1; - - raylib.DrawRectangle( - i32(win_rec.x + win_margin.x), - i32(win_rec.y + win_rec.height - win_margin.y * 2), - i32(buffer_prev_width), - i32(state.source_font_height), - theme.get_palette_raylib_color(.Background2)); - - win.input_buffer.glyph_buffer_height = glyph_buffer_height; - win.input_buffer.glyph_buffer_width = glyph_buffer_width; - core.draw_file_buffer( - state, - &win.input_buffer, - int(win_rec.x + win_margin.x), - int(win_rec.y + win_rec.height - win_margin.y * 2), - state.font, - show_line_numbers = false); - - for match, index in win.matches { - relative_file_path, _ := filepath.rel(state.directory, match.path) - text := raylib.TextFormat("%s:%d:%d: %s", relative_file_path, match.line, match.col, match.text); - text_width := raylib.MeasureTextEx(state.font, text, f32(state.source_font_height), 0); - - if index == win.selected_match { - raylib.DrawRectangle( - i32(win_rec.x + win_margin.x), - i32(win_rec.y + win_margin.y) + i32(index * state.source_font_height), - i32(text_width.x), - i32(state.source_font_height), - theme.get_palette_raylib_color(.Background2)); - } - - raylib.DrawTextEx( - state.font, - text, - raylib.Vector2 { win_rec.x + win_margin.x, win_rec.y + win_margin.y + f32(index * state.source_font_height) }, - f32(state.source_font_height), - 0, - theme.get_palette_raylib_color(.Foreground2)); - } -}