Compare commits

...

3 Commits

  1. 12
      src/file.rs
  2. 39
      src/input.rs
  3. 125
      src/main.rs
  4. 156
      src/render.rs
  5. 72
      src/syntax-highlight.rs

@ -0,0 +1,12 @@
//! File management
use std::{fs::File, /* path::Path ,*/ io::Read};
/// Reads the file and turns it into a Vec of u8s
pub fn read_file(file_name: String) -> Result<Vec<u8>, String> {
let mut file_content = Vec::new();
let mut file = File::open(&file_name).expect("Unable to open file");
file.read_to_end(&mut file_content).expect("Unable to read");
Ok(file_content)
}

@ -1,5 +1,6 @@
use sdl2::keyboard::Keycode; //use sdl2::keyboard::Keycode;
/*
mod keybinds { mod keybinds {
pub(super) enum Clipboard { pub(super) enum Clipboard {
CutSelection, CutSelection,
@ -42,7 +43,41 @@ mod keybinds {
WholeLines, WholeLines,
} }
} }
*/
fn input(buffer: &str, modifiers: crate::ModifierKeys, key: Keycode) { pub struct ModifierKeys {
pub alt: bool,
pub ctrl: bool,
pub shift: bool,
}
impl ModifierKeys {
pub fn new() -> ModifierKeys {
ModifierKeys { alt: false, ctrl: false, shift: false }
}
}
pub fn delete(buffer: &mut String, cursor_position: usize, modifiers: &ModifierKeys) {
if modifiers.ctrl {
loop {
let buffer_chars: Vec<char> = buffer.chars()
.collect();
if cursor_position == buffer.len() {
break
}
if !buffer_chars[cursor_position].is_whitespace() {
buffer.remove(cursor_position);
} else {
break
}
}
} else {
buffer.remove(cursor_position);
}
}
/*
fn process_input(buffer: &str, modifiers: ModifierKeys, key: Keycode) {
} }
*/

@ -1,13 +1,14 @@
extern crate sdl2; extern crate sdl2;
use clipboard::{ClipboardProvider, ClipboardContext}; use clipboard::{ClipboardContext, ClipboardProvider};
use sdl2::{ use sdl2::{
event::{Event, WindowEvent}, event::{Event, WindowEvent},
keyboard::Keycode, keyboard::Keycode,
}; };
mod render; mod file;
mod input; mod input;
mod render;
static SCREEN_WIDTH: u32 = 1280; static SCREEN_WIDTH: u32 = 1280;
static SCREEN_HEIGHT: u32 = 720; static SCREEN_HEIGHT: u32 = 720;
@ -19,17 +20,6 @@ static REFRESH_RATE: u32 = 50;
static UNDO_TIME: u32 = 250; static UNDO_TIME: u32 = 250;
static UNDO_TIME_COUNT: u32 = (REFRESH_RATE as f32 * (UNDO_TIME as f32 / 1000.0)) as u32; static UNDO_TIME_COUNT: u32 = (REFRESH_RATE as f32 * (UNDO_TIME as f32 / 1000.0)) as u32;
struct ModifierKeys {
alt: bool,
ctrl: bool,
shift: bool,
}
// struct EditorGraphics {
// canvas: Canvas<Window>,
// glyph_atlas: GlyphAtlas,
// }
pub fn main() -> Result<(), String> { pub fn main() -> Result<(), String> {
// Initialize clipboard // Initialize clipboard
let mut clipboard_context: ClipboardContext = ClipboardProvider::new().unwrap(); let mut clipboard_context: ClipboardContext = ClipboardProvider::new().unwrap();
@ -57,12 +47,12 @@ pub fn main() -> Result<(), String> {
let mut undo_timer: u32 = UNDO_TIME_COUNT; let mut undo_timer: u32 = UNDO_TIME_COUNT;
// Initialize input values // Initialize input values
let mut modifier_keys = ModifierKeys {alt: false, ctrl: false, shift: false}; let mut modifier_keys = input::ModifierKeys::new();
let mut cursor_position = 0; let mut cursor_position = 0;
let mut selection_anchor: Option<usize> = None; let mut selection_anchor: Option<usize> = None;
// Initialize graphics data and values // Initialize graphics data and values
let glyph_atlas = render::generate_glyph_data(); let glyph_atlas = render::generate_glyph_data()?;
// Easier way to please the borrow checker // Easier way to please the borrow checker
macro_rules! draw { macro_rules! draw {
@ -86,57 +76,39 @@ pub fn main() -> Result<(), String> {
// Event::KeyDown { keycode: Some(Keycode::Escape), .. } | // Event::KeyDown { keycode: Some(Keycode::Escape), .. } |
Event::Quit { .. } => break 'mainloop, Event::Quit { .. } => break 'mainloop,
Event::KeyUp { keycode, .. } => { Event::KeyUp { keycode, .. } => match keycode {
match keycode{ Some(Keycode::LAlt) | Some(Keycode::RAlt) => modifier_keys.alt = false,
Some(Keycode::LAlt) | Some(Keycode::RAlt) => { Some(Keycode::LCtrl) | Some(Keycode::RCtrl) => modifier_keys.ctrl = false,
modifier_keys.alt = false Some(Keycode::LShift) | Some(Keycode::RShift) => modifier_keys.shift = false,
}, Some(Keycode::LGui) | Some(Keycode::RGui) => break,
Some(Keycode::LCtrl) | Some(Keycode::RCtrl) => {
modifier_keys.ctrl = false
},
Some(Keycode::LShift) | Some(Keycode::RShift) => {
modifier_keys.shift = false
},
Some(Keycode::LGui) | Some(Keycode::RGui) => {
break
},
_ => (), _ => (),
}
}, },
Event::KeyDown { keycode, .. } => { Event::KeyDown { keycode, .. } => {
match keycode { match keycode {
Some(Keycode::LAlt) | Some(Keycode::RAlt) => { Some(Keycode::LAlt) | Some(Keycode::RAlt) => modifier_keys.alt = true,
modifier_keys.alt = true Some(Keycode::LCtrl) | Some(Keycode::RCtrl) => modifier_keys.ctrl = true,
}, Some(Keycode::LShift) | Some(Keycode::RShift) => modifier_keys.shift = true,
Some(Keycode::LCtrl) | Some(Keycode::RCtrl) => { Some(Keycode::LGui) | Some(Keycode::RGui) => break,
modifier_keys.ctrl = true
},
Some(Keycode::LShift) | Some(Keycode::RShift) => {
modifier_keys.shift = true
},
Some(Keycode::LGui) | Some(Keycode::RGui) => {
break
},
_ => (),
};
match (modifier_keys.shift, modifier_keys.ctrl, modifier_keys.alt) {
// All modifiers up
(false, false, false) => {
match keycode {
// DELETE key // DELETE key
Some(Keycode::Delete) => { Some(Keycode::Delete) => {
if buffer.len() > 0 && cursor_position < buffer.len() { if buffer.len() > 0 && cursor_position < buffer.len() {
undo_timer = 0; undo_timer = 0;
selection_anchor = None; selection_anchor = None;
input::delete(&mut buffer, cursor_position, &modifier_keys);
buffer.remove(cursor_position);
draw!(); draw!();
} }
}, }
_ => (),
};
match (modifier_keys.shift, modifier_keys.ctrl, modifier_keys.alt) {
// All modifiers up
(false, false, false) => {
match keycode {
// ENTER key // ENTER key
Some(Keycode::Return) => { Some(Keycode::Return) => {
undo_timer = 0; undo_timer = 0;
@ -147,7 +119,7 @@ pub fn main() -> Result<(), String> {
cursor_position += 1; cursor_position += 1;
draw!(); draw!();
}, }
// HOME key // HOME key
Some(Keycode::Home) => { Some(Keycode::Home) => {
@ -156,7 +128,7 @@ pub fn main() -> Result<(), String> {
cursor_position = 0; cursor_position = 0;
draw!(); draw!();
}, }
// END key // END key
Some(Keycode::End) => { Some(Keycode::End) => {
@ -165,17 +137,17 @@ pub fn main() -> Result<(), String> {
cursor_position = buffer.len(); cursor_position = buffer.len();
draw!(); draw!();
}, }
// Left/Back arrow // Left/Back arrow
Some(Keycode::Left) => { Some(Keycode::Left) => {
selection_anchor = None; selection_anchor = None;
cursor_position = usize::checked_sub(cursor_position, 1) cursor_position =
.unwrap_or(0); usize::checked_sub(cursor_position, 1).unwrap_or(0);
draw!(); draw!();
}, }
// Right/Forward arrow // Right/Forward arrow
Some(Keycode::Right) => { Some(Keycode::Right) => {
@ -184,7 +156,7 @@ pub fn main() -> Result<(), String> {
cursor_position = (cursor_position + 1).min(buffer.len()); cursor_position = (cursor_position + 1).min(buffer.len());
draw!(); draw!();
}, }
// BACKSPACE key // BACKSPACE key
// Character backspace // Character backspace
@ -198,11 +170,11 @@ pub fn main() -> Result<(), String> {
draw!(); draw!();
} }
}, }
_ => (), _ => (),
} }
}, }
// CTRL down // CTRL down
(false, true, false) => { (false, true, false) => {
@ -213,7 +185,7 @@ pub fn main() -> Result<(), String> {
cursor_position = 0; cursor_position = 0;
draw!() draw!()
}, }
// Undo // Undo
Some(Keycode::Z) => { Some(Keycode::Z) => {
@ -225,7 +197,7 @@ pub fn main() -> Result<(), String> {
draw!() draw!()
} }
}, }
// TODO: Cut // TODO: Cut
Some(Keycode::X) => println!("Cut"), Some(Keycode::X) => println!("Cut"),
@ -234,7 +206,7 @@ pub fn main() -> Result<(), String> {
// TODO: Use selection // TODO: Use selection
Some(Keycode::C) => { Some(Keycode::C) => {
clipboard_context.set_contents(buffer.clone()).unwrap() clipboard_context.set_contents(buffer.clone()).unwrap()
}, }
// Paste // Paste
Some(Keycode::V) => { Some(Keycode::V) => {
@ -246,7 +218,7 @@ pub fn main() -> Result<(), String> {
undo_timer = UNDO_TIME_COUNT; undo_timer = UNDO_TIME_COUNT;
} }
draw!() draw!()
}, }
// BACKSPACE key // BACKSPACE key
// Word backspace // Word backspace
@ -256,11 +228,11 @@ pub fn main() -> Result<(), String> {
undo_timer = 0; undo_timer = 0;
selection_anchor = None; selection_anchor = None;
let buffer_chars: Vec<char> = buffer.chars() let buffer_chars: Vec<char> = buffer.chars().collect();
.collect(); while !(buffer_chars[cursor_position - 1] == ' '
while !(buffer_chars[cursor_position - 1] == ' ' || || buffer_chars[cursor_position - 1] == '\n')
buffer_chars[cursor_position - 1] == '\n') && && cursor_position > 1
cursor_position > 1 { {
buffer.remove(cursor_position - 1); buffer.remove(cursor_position - 1);
cursor_position -= 1; cursor_position -= 1;
} }
@ -269,14 +241,13 @@ pub fn main() -> Result<(), String> {
draw!() draw!()
} }
}, }
_ => (), _ => (),
} }
}, }
// SHIFT + CTRL down // SHIFT + CTRL down
(true, true, false) => { (true, true, false) => match keycode {
match keycode {
Some(Keycode::Z) => { Some(Keycode::Z) => {
if undo_position < undo_history.len() { if undo_position < undo_history.len() {
undo_position += 1; undo_position += 1;
@ -286,18 +257,17 @@ pub fn main() -> Result<(), String> {
draw!(); draw!();
} }
}, }
Some(Keycode::X) => println!("Cut line(s)"), Some(Keycode::X) => println!("Cut line(s)"),
Some(Keycode::C) => println!("Copy line(s)"), Some(Keycode::C) => println!("Copy line(s)"),
_ => (), _ => (),
}
}, },
_ => (), _ => (),
} }
}, }
// Process user input // Process user input
Event::TextInput { text, .. } => { Event::TextInput { text, .. } => {
@ -309,7 +279,7 @@ pub fn main() -> Result<(), String> {
cursor_position += 1; cursor_position += 1;
draw!(); draw!();
}, }
_ => {} _ => {}
} }
@ -331,6 +301,7 @@ pub fn main() -> Result<(), String> {
std::thread::sleep(std::time::Duration::new(0, 1_000_000_000 / REFRESH_RATE)); std::thread::sleep(std::time::Duration::new(0, 1_000_000_000 / REFRESH_RATE));
} }
format!("{selection_anchor:?}");
println!("{buffer}"); println!("{buffer}");
Ok(()) Ok(())
} }

@ -11,7 +11,7 @@ use sdl2::{
video::Window, video::Window,
}; };
use std::{fs::File, path::Path, io::Read}; use crate::file;
type Glyph = Vec<Point>; type Glyph = Vec<Point>;
@ -31,28 +31,68 @@ pub struct GlyphAtlas {
metrics: GlyphMetrics, metrics: GlyphMetrics,
} }
/// Reads the file and turns it into a Vec of u8s struct ColorRGB {
fn read_file(file_name: String) -> Vec<u8> { red: u8,
let path = Path::new(&file_name); green: u8,
blue: u8,
}
if !path.exists() { impl ColorRGB {
return String::from("Not Found!").into(); fn new(red: u8, green: u8, blue: u8) -> ColorRGB {
ColorRGB { red, green, blue }
} }
}
let mut file_content = Vec::new(); #[allow(dead_code)]
let mut file = File::open(&file_name).expect("Unable to open file"); enum Colors {
file.read_to_end(&mut file_content).expect("Unable to read"); Background,
Foreground,
Error,
Warning,
FindBg,
StdFunction,
Comment,
Keyword,
Number,
Operator,
Preprocessor,
SelectionBg,
SelectionFg,
String,
}
file_content impl Colors {
fn color(&self) -> ColorRGB {
match self {
Colors::Background => ColorRGB::new(32, 32, 32),
Colors::Foreground => ColorRGB::new(255, 255, 255),
Colors::Error => ColorRGB::new(255, 0, 0),
Colors::Warning => ColorRGB::new(255, 170, 0),
Colors::FindBg => ColorRGB::new(246, 185, 63),
Colors::StdFunction => ColorRGB::new(170, 255, 255),
Colors::Comment => ColorRGB::new(0, 255, 0),
Colors::Keyword => ColorRGB::new(0, 170, 255),
Colors::Number => ColorRGB::new(85, 255, 255),
Colors::Operator => ColorRGB::new(255, 85, 0),
Colors::Preprocessor => ColorRGB::new(127, 0, 0),
Colors::SelectionBg => ColorRGB::new(110, 161, 241),
Colors::SelectionFg => ColorRGB::new(255, 255, 255),
Colors::String => ColorRGB::new(255, 85, 255),
}
}
} }
pub fn generate_glyph_data() -> GlyphAtlas { pub fn generate_glyph_data() -> Result<GlyphAtlas, String> {
// Retrieve font data from file // Retrieve font data from file
// TODO: Get crate path instead of working directory path
let file_path = String::from("./fonts/Terminus14x8.data"); let file_path = String::from("./fonts/Terminus14x8.data");
let contents = read_file(file_path); let contents = file::read_file(file_path)?;
// Get glyph metrics // Get glyph metrics
let glyph_metrics = GlyphMetrics { width: 8, height: 16 }; let glyph_metrics = GlyphMetrics {
width: 8,
height: 16,
};
let glyph_width = glyph_metrics.width; let glyph_width = glyph_metrics.width;
// Get width of image for proper positioning of pixels // Get width of image for proper positioning of pixels
@ -60,9 +100,9 @@ pub fn generate_glyph_data() -> GlyphAtlas {
let width_right_byte = contents[1]; let width_right_byte = contents[1];
let width_bytes = [width_left_byte, width_right_byte]; let width_bytes = [width_left_byte, width_right_byte];
let width = u16::from_be_bytes(width_bytes); let width = u16::from_be_bytes(width_bytes);
// println!("Left Byte: {width_left_byte}, Right Byte: {width_right_byte}, Byte Pair: {width}"); println!("Left Byte: {width_left_byte}, Right Byte: {width_right_byte}, Byte Pair: {width}");
let gtable_prune = &contents[width as usize + 2 ..]; let pruned_glyph_table = &contents[width as usize + 2..];
// Generate the glyph atlas // Generate the glyph atlas
let mut glyph_atlas: Vec<Glyph> = vec![]; let mut glyph_atlas: Vec<Glyph> = vec![];
@ -78,30 +118,38 @@ pub fn generate_glyph_data() -> GlyphAtlas {
let offset = glyph * glyph_width as u16; let offset = glyph * glyph_width as u16;
let position = (x + multiplier + offset) as usize; let position = (x + multiplier + offset) as usize;
if gtable_prune[position] == 1 { if pruned_glyph_table[position] == 1 {
new_glyph.push(Point::new(x as i32, y as i32)); new_glyph.push(Point::new(x as i32, y as i32));
} }
} }
glyph_atlas.push(new_glyph); glyph_atlas.push(new_glyph);
} }
GlyphAtlas { glyphs: glyph_atlas, metrics: glyph_metrics } Ok(GlyphAtlas {
glyphs: glyph_atlas,
metrics: glyph_metrics,
})
} }
/// Method for generating points to render, using given string /// Method for generating points to render, using given string
fn draw_text(glyph_atlas: &GlyphAtlas, content: &str, offset: Point) -> Vec<Point> { fn draw_text(
glyph_atlas: &GlyphAtlas,
window_size: (u32, u32),
content: &str,
offset: Point,
) -> Vec<Point> {
let mut points: Vec<Point> = vec![]; let mut points: Vec<Point> = vec![];
let glyph_width = glyph_atlas.metrics.width; let glyph_width = glyph_atlas.metrics.width;
let glyph_height = glyph_atlas.metrics.height; let glyph_height = glyph_atlas.metrics.height;
let lines = content.split('\n'); let content_lines = content.split('\n');
for (y, chars) in lines.enumerate() { for (y, line) in content_lines.enumerate() {
for (x, chara) in chars.chars().enumerate() { for (x, character) in line.chars().enumerate() {
let index; let index;
if chara.is_lowercase() { if character.is_lowercase() {
index = chara.to_ascii_lowercase() as usize; index = character.to_ascii_lowercase() as usize;
} else { } else {
index = chara.to_ascii_uppercase() as usize; index = character.to_ascii_uppercase() as usize;
} }
for pixel in &glyph_atlas.glyphs[index - 32] { for pixel in &glyph_atlas.glyphs[index - 32] {
@ -110,16 +158,28 @@ fn draw_text(glyph_atlas: &GlyphAtlas, content: &str, offset: Point) -> Vec<Poin
let positioned_pixel = Point::new( let positioned_pixel = Point::new(
pixel.x + x_glyph as i32 + offset.x, pixel.x + x_glyph as i32 + offset.x,
pixel.y + y_glyph as i32 + offset.y pixel.y + y_glyph as i32 + offset.y,
); );
points.push(positioned_pixel); points.push(positioned_pixel);
} }
if x >= (window_size.0 as usize / glyph_width) - 3 {
break;
}
}
if y >= (window_size.1 as usize / glyph_height) - 3 {
break;
} }
} }
points points
} }
pub fn draw_cursor(glyph_atlas: &GlyphAtlas, mut cursor_position: usize, content: &str, offset: Point) -> (Point, Point) { pub fn draw_cursor(
glyph_atlas: &GlyphAtlas,
mut cursor_position: usize,
content: &str,
offset: Point,
) -> (Point, Point) {
let glyph_width = glyph_atlas.metrics.width; let glyph_width = glyph_atlas.metrics.width;
let glyph_height = glyph_atlas.metrics.height; let glyph_height = glyph_atlas.metrics.height;
@ -127,60 +187,68 @@ pub fn draw_cursor(glyph_atlas: &GlyphAtlas, mut cursor_position: usize, content
let mut y = 0; let mut y = 0;
if cursor_position > 0 { if cursor_position > 0 {
cursor_position = cursor_position.checked_sub(1).unwrap_or(0); cursor_position = cursor_position.checked_sub(1).unwrap_or(0);
for (idx, chara) in content.chars().enumerate() { for (idx, character) in content.chars().enumerate() {
x += 1; x += 1;
if chara == '\n' { if character == '\n' {
x = 0; x = 0;
y += 1; y += 1;
} }
if idx == cursor_position { if idx == cursor_position {
let point_a = Point::new((x * glyph_width) as i32 + offset.x, let point_a = Point::new(
(y * glyph_height) as i32 + offset.y (x * glyph_width) as i32 + offset.x,
); (y * glyph_height) as i32 + offset.y,
let point_b = Point::new(point_a.x,
point_a.y + glyph_height as i32
); );
return (point_a, point_b) let point_b = Point::new(point_a.x, point_a.y + glyph_height as i32);
return (point_a, point_b);
} }
} }
} }
(Point::new(offset.x, offset.y), Point::new(offset.x, offset.y + glyph_height as i32)) (
Point::new(offset.x, offset.y),
Point::new(offset.x, offset.y + glyph_height as i32),
)
} }
/// Draw all contents to the window /// Draw all contents to the window
pub fn draw_everything(canvas: &mut Canvas<Window>, pub fn draw_everything(
canvas: &mut Canvas<Window>,
glyph_atlas: &GlyphAtlas, glyph_atlas: &GlyphAtlas,
buffer: &str, buffer: &str,
cursor_position: usize) -> Result<(), String> { cursor_position: usize,
) -> Result<(), String> {
// Quick initialization // Quick initialization
let window_size = canvas.output_size()?; let window_size = canvas.output_size()?;
let text_offset = Point::new(10, 10); let text_offset = Point::new(10, 10);
let bg_color = Colors::Background.color();
let fg_color = Colors::Foreground.color();
// Draw background // Draw background
canvas.set_draw_color(Color::RGB(32, 32, 32)); canvas.set_draw_color(Color::RGB(bg_color.red, bg_color.green, bg_color.blue));
canvas.clear(); canvas.clear();
// Draw text // Draw text
canvas.set_draw_color(Color::RGB(240, 240, 240)); canvas.set_draw_color(Color::RGB(fg_color.red, fg_color.green, fg_color.blue));
let fb_text = draw_text(&glyph_atlas, buffer, text_offset); let fb_text = draw_text(&glyph_atlas, window_size, buffer, text_offset);
canvas.draw_points(&fb_text[..])?; canvas.draw_points(&fb_text[..])?;
// Draw info // Draw info
let status = buffer.len().to_formatted_string(&Locale::en); let status = buffer.len().to_formatted_string(&Locale::en);
let status_position = Point::new( let status_position = Point::new(
text_offset.x, text_offset.x,
window_size.1 as i32 - glyph_atlas.metrics.height as i32 * 2 window_size.1 as i32 - glyph_atlas.metrics.height as i32 * 2,
); );
canvas.set_draw_color(Color::RGB(16, 64, 64)); canvas.set_draw_color(Color::RGB(16, 64, 64));
canvas.fill_rect(Rect::new(0, canvas.fill_rect(Rect::new(
0,
status_position.y - 5, status_position.y - 5,
window_size.0, window_size.0,
glyph_atlas.metrics.height as u32 + 10 glyph_atlas.metrics.height as u32 + 10,
))?; ))?;
canvas.set_draw_color(Color::RGB(127, 240, 240)); canvas.set_draw_color(Color::RGB(127, 240, 240));
let status_bar = draw_text(&glyph_atlas, &status, status_position); let status_bar = draw_text(&glyph_atlas, window_size, &status, status_position);
canvas.draw_points(&status_bar[..])?; canvas.draw_points(&status_bar[..])?;
// Draw cursor // Draw cursor

@ -0,0 +1,72 @@
/*
pub enum Token {
ILLEGAL,
EOF,
IDENT(Vec<char>),
INT(Vec<char>),
ASSIGN(char),
PLUS(char),
COMMA(char),
SEMICOLON(char),
LPAREN(char),
RPAREN(char),
LBRACE(char),
RBRACE(char),
FUNCTION,
LET,
TRUE,
FALSE,
IF,
ELSE,
RETURN,
MINUS(char),
BANG(char),
ASTERISK(char),
SLASH(char),
LT(char),
GT(char)
}
*/
fn find_keyword(input: &str) {
match input {
"async" => {},
"await" => {},
"as" => {},
"break" => {},
"const" => {},
"continue" => {},
"crate" => {},
"dyn" => {},
"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" => {},
}
}
Loading…
Cancel
Save