diff --git a/Cargo.lock b/Cargo.lock index c157dda..214ce93 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1757,6 +1757,7 @@ dependencies = [ "tracing", "tracing-appender", "tracing-subscriber", + "tree-sitter", "tui-big-text", ] diff --git a/Cargo.toml b/Cargo.toml index 457d340..5fbe2a2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,3 +16,5 @@ serde = { version = "1.0.197", features = ["derive"] } serde_json = "1.0.115" reqwest = { version = "0.12", features = ["json"] } ratatui = { version = "0.26.1", features = ["all-widgets", "crossterm"] } +tree-sitter = "0.22.5" +tree-sitter-json = "0.21" diff --git a/reqtui/Cargo.toml b/reqtui/Cargo.toml index 267c643..08c12be 100644 --- a/reqtui/Cargo.toml +++ b/reqtui/Cargo.toml @@ -12,9 +12,9 @@ tokio.workspace = true reqwest.workspace = true serde_json.workspace = true ratatui.workspace = true +tree-sitter.workspace = true +tree-sitter-json.workspace = true -tree-sitter = "0.22.5" -tree-sitter-json = "0.21" ropey = "1.6.1" lazy_static = "1.4" diff --git a/reqtui/src/net/request_manager.rs b/reqtui/src/net/request_manager.rs index 38543c4..1f174d0 100644 --- a/reqtui/src/net/request_manager.rs +++ b/reqtui/src/net/request_manager.rs @@ -54,10 +54,9 @@ pub fn handle_request( let mut highlighter = HIGHLIGHTER.write().unwrap(); let body = body.to_string(); - let tree = highlighter.parse(&pretty_body); - let highlight = highlighter.apply(&pretty_body, tree.as_ref(), &tokens); - let pretty_body = TextObject::from(&pretty_body).with_highlight(highlight); + let _highlight = highlighter.apply(&pretty_body, tree.as_ref(), &tokens); + let pretty_body = TextObject::from(&pretty_body); response_tx .send(ReqtuiNetRequest::Response(ReqtuiResponse { diff --git a/reqtui/src/schema/types.rs b/reqtui/src/schema/types.rs index 3fefc31..bd002dc 100644 --- a/reqtui/src/schema/types.rs +++ b/reqtui/src/schema/types.rs @@ -1,7 +1,6 @@ use std::{hash::Hash, path::PathBuf}; use serde::{Deserialize, Serialize}; -use serde_json::Value; #[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] pub struct Schema { @@ -54,7 +53,9 @@ pub struct Request { pub method: RequestMethod, pub name: String, pub uri: String, - pub body: Option, + pub body: Option, + #[serde(rename = "bodyType")] + pub body_type: Option, } impl Hash for Request { diff --git a/reqtui/src/text_object/text_object.rs b/reqtui/src/text_object/text_object.rs index 3436ff2..684ddec 100644 --- a/reqtui/src/text_object/text_object.rs +++ b/reqtui/src/text_object/text_object.rs @@ -1,31 +1,33 @@ -use ratatui::{ - style::Styled, - text::{Line, Span}, -}; use ropey::Rope; -use crate::syntax::highlighter::ColorInfo; - -#[derive(Debug, PartialEq)] +#[derive(Debug, Clone, PartialEq)] pub struct Readonly; -#[derive(Debug, PartialEq)] +#[derive(Debug, Clone, PartialEq)] pub struct Write; -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone)] pub struct TextObject { content: Rope, state: std::marker::PhantomData, - pub display: Vec>, - pub longest_line: usize, +} + +impl Default for TextObject { + fn default() -> Self { + let content = String::default(); + + TextObject { + content: Rope::from_str(&content), + state: std::marker::PhantomData, + } + } } impl TextObject { pub fn from(content: &str) -> TextObject { + let content = Rope::from_str(content); TextObject:: { - display: vec![Line::from(content.to_string())], - content: Rope::from_str(content), + content, state: std::marker::PhantomData::, - longest_line: 0, } } @@ -33,55 +35,11 @@ impl TextObject { TextObject:: { content: self.content, state: std::marker::PhantomData, - display: self.display, - longest_line: self.longest_line, - } - } -} - -fn build_stylized_line(c: char, i: usize, colors: &[ColorInfo]) -> Span<'static> { - c.to_string().set_style( - colors - .iter() - .find(|color| color.start <= i && color.end >= i) - .map(|c| c.style) - .unwrap_or_default(), - ) -} - -impl TextObject { - pub fn with_highlight(self, colors: Vec) -> Self { - let mut display: Vec = vec![]; - let mut current_line: Vec = vec![]; - let mut longest_line = 0; - - self.to_string() - .chars() - .enumerate() - .for_each(|(i, c)| match c { - '\n' => { - longest_line = longest_line.max(current_line.len()); - current_line.push(build_stylized_line(c, i, &colors)); - display.push(current_line.clone().into()); - current_line.clear(); - } - _ => current_line.push(build_stylized_line(c, i, &colors)), - }); - - if !self.to_string().ends_with('\n') { - display.push(current_line.clone().into()); - } - - Self { - content: self.content, - state: std::marker::PhantomData, - display, - longest_line, } } } -impl std::fmt::Display for TextObject { +impl std::fmt::Display for TextObject { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(&self.content.to_string()) } diff --git a/tui/Cargo.toml b/tui/Cargo.toml index 529075a..1118d5a 100644 --- a/tui/Cargo.toml +++ b/tui/Cargo.toml @@ -15,6 +15,7 @@ tracing.workspace = true reqwest.workspace = true serde_json.workspace = true ratatui.workspace = true +tree-sitter.workspace = true futures = "0.3.30" tui-big-text = { version = "0.4.3" } diff --git a/tui/src/components/api_explorer/api_explorer.rs b/tui/src/components/api_explorer/api_explorer.rs index 605b8d3..df5bb40 100644 --- a/tui/src/components/api_explorer/api_explorer.rs +++ b/tui/src/components/api_explorer/api_explorer.rs @@ -19,7 +19,7 @@ use reqtui::{ net::request_manager::{ReqtuiNetRequest, ReqtuiResponse}, schema::types::{Directory, Request, RequestKind, Schema}, }; -use std::{collections::HashMap, ops::Add}; +use std::{cell::RefCell, collections::HashMap, ops::Add, rc::Rc}; use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}; use super::req_editor::{ReqEditor, ReqEditorState, ReqEditorTabs}; @@ -38,6 +38,12 @@ enum VisitNode { Prev, } +#[derive(PartialEq, Debug)] +enum EditorMode { + Insert, + Normal, +} + #[derive(Debug, PartialEq)] enum PaneFocus { Sidebar, @@ -53,7 +59,7 @@ pub struct ApiExplorer<'a> { layout: ExplorerLayout, response_rx: UnboundedReceiver, request_tx: UnboundedSender, - selected_request: Option, + selected_request: Option>>, hovered_request: Option, dirs_expanded: HashMap, focused_pane: PaneFocus, @@ -62,7 +68,10 @@ pub struct ApiExplorer<'a> { preview_tab: ResViewerTabs, raw_preview_scroll: usize, + editor: ReqEditor<'a>, editor_tab: ReqEditorTabs, + editor_body_scroll: usize, + editor_mode: EditorMode, responses_map: HashMap, } @@ -74,7 +83,7 @@ impl<'a> ApiExplorer<'a> { let selected_request = schema.requests.as_ref().and_then(|requests| { requests.first().and_then(|req| { if let RequestKind::Single(req) = req { - Some(req.clone()) + Some(Rc::new(RefCell::new(req.clone()))) } else { None } @@ -95,6 +104,11 @@ impl<'a> ApiExplorer<'a> { layout, colors, + editor: ReqEditor::new(colors, selected_request.clone()), + editor_tab: ReqEditorTabs::Request, + editor_body_scroll: 0, + editor_mode: EditorMode::Normal, + hovered_request, selected_request, dirs_expanded: HashMap::default(), @@ -103,8 +117,6 @@ impl<'a> ApiExplorer<'a> { preview_tab: ResViewerTabs::Preview, raw_preview_scroll: 0, - editor_tab: ReqEditorTabs::Request, - response_rx, request_tx, } @@ -121,7 +133,9 @@ impl<'a> ApiExplorer<'a> { *entry = !*entry; } RequestKind::Single(req) => { - self.selected_request = Some(req.clone()); + self.selected_request = Some(Rc::new(RefCell::new(req.clone()))); + self.editor = + ReqEditor::new(self.colors, self.selected_request.clone()); } } } @@ -161,16 +175,43 @@ impl<'a> ApiExplorer<'a> { fn handle_req_uri_key_event(&mut self, key_event: KeyEvent) -> anyhow::Result> { match key_event.code { KeyCode::Char('i') => self.selected_pane = Some(PaneFocus::Preview), - KeyCode::Enter => reqtui::net::handle_request( - self.selected_request.as_ref().unwrap().clone(), - self.request_tx.clone(), - self.colors.tokens.clone(), - ), + KeyCode::Enter => { + if let Some(req) = self.selected_request.as_ref() { + reqtui::net::handle_request( + req.clone().borrow().clone(), + self.request_tx.clone(), + self.colors.tokens.clone(), + ) + } + } _ => {} } Ok(None) } + fn handle_editor_key_event(&mut self, key_event: KeyEvent) -> anyhow::Result> { + match key_event.code { + KeyCode::Char(c) => { + if let Some(req) = self.selected_request.as_mut() { + req.borrow_mut() + .body + .as_mut() + .map(|body| body.push(c)) + .or_else(|| { + req.borrow_mut().body = Some(c.to_string()); + Some(()) + }); + + tracing::debug!("{:#?}", self.selected_request); + }; + } + KeyCode::Enter => {} + _ => {} + }; + + Ok(None) + } + fn draw_background(&self, size: Rect, frame: &mut Frame) { frame.render_widget(Clear, size); frame.render_widget(Block::default().bg(self.colors.primary.background), size); @@ -179,7 +220,7 @@ impl<'a> ApiExplorer<'a> { fn draw_sidebar(&mut self, frame: &mut Frame) { let mut state = SidebarState::new( self.schema.requests.as_deref(), - self.selected_request.as_ref(), + &self.selected_request, self.hovered_request.as_ref(), &mut self.dirs_expanded, self.focused_pane == PaneFocus::Sidebar, @@ -190,7 +231,7 @@ impl<'a> ApiExplorer<'a> { fn draw_req_uri(&mut self, frame: &mut Frame) { let mut state = ReqUriState::new( - self.selected_request.as_ref(), + &self.selected_request, self.focused_pane == PaneFocus::ReqUri, ); ReqUri::new(self.colors).render(self.layout.req_uri, frame.buffer_mut(), &mut state); @@ -200,7 +241,7 @@ impl<'a> ApiExplorer<'a> { let current_response = self .selected_request .as_ref() - .and_then(|selected| self.responses_map.get(selected)); + .and_then(|selected| self.responses_map.get(&*selected.borrow())); let mut state = ResViewerState::new( self.focused_pane.eq(&PaneFocus::Preview), @@ -221,8 +262,6 @@ impl<'a> ApiExplorer<'a> { } fn draw_req_editor(&mut self, frame: &mut Frame) { - let current_request = self.selected_request.as_ref(); - let mut state = ReqEditorState::new( self.focused_pane.eq(&PaneFocus::Editor), self.selected_pane @@ -230,20 +269,16 @@ impl<'a> ApiExplorer<'a> { .map(|sel| sel.eq(&PaneFocus::Editor)) .unwrap_or(false), &self.editor_tab, + &mut self.editor_body_scroll, ); - frame.render_stateful_widget( - ReqEditor::new(self.colors), - self.layout.req_editor, - &mut state, - ) + frame.render_stateful_widget(self.editor.clone(), self.layout.req_editor, &mut state) } fn drain_response_rx(&mut self) { while let Ok(ReqtuiNetRequest::Response(res)) = self.response_rx.try_recv() { if let Some(ref req) = self.selected_request { - tracing::debug!("{req:?}"); - self.responses_map.insert(req.clone(), res); + self.responses_map.insert(req.borrow().clone(), res); } } } @@ -302,7 +337,7 @@ impl Component for ApiExplorer<'_> { PaneFocus::Sidebar => self.handle_sidebar_key_event(key_event), PaneFocus::ReqUri => self.handle_req_uri_key_event(key_event), PaneFocus::Preview => self.handle_preview_key_event(key_event), - PaneFocus::Editor => todo!(), + PaneFocus::Editor => self.handle_editor_key_event(key_event), } } } @@ -416,6 +451,8 @@ mod tests { method: RequestMethod::Get, name: "Root1".to_string(), uri: "/root1".to_string(), + body_type: None, + body: None, }) } @@ -424,6 +461,8 @@ mod tests { method: RequestMethod::Post, name: "Child1".to_string(), uri: "/nested1/child1".to_string(), + body_type: None, + body: None, }) } @@ -432,6 +471,8 @@ mod tests { method: RequestMethod::Put, name: "Child2".to_string(), uri: "/nested1/child2".to_string(), + body_type: None, + body: None, }) } @@ -440,6 +481,8 @@ mod tests { method: RequestMethod::Put, name: "NotUsed".to_string(), uri: "/not/used".to_string(), + body_type: None, + body: None, }) } @@ -459,6 +502,8 @@ mod tests { method: RequestMethod::Delete, name: "Root2".to_string(), uri: "/root2".to_string(), + body_type: None, + body: None, }) } diff --git a/tui/src/components/api_explorer/req_editor.rs b/tui/src/components/api_explorer/req_editor.rs index 54e3756..37c4827 100644 --- a/tui/src/components/api_explorer/req_editor.rs +++ b/tui/src/components/api_explorer/req_editor.rs @@ -1,10 +1,25 @@ use ratatui::{ buffer::Buffer, layout::{Constraint, Direction, Layout, Rect}, - style::Style, - widgets::{Block, Borders, StatefulWidget, Tabs, Widget}, + style::{Style, Styled, Stylize}, + text::{Line, Span}, + widgets::{ + Block, Borders, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState, StatefulWidget, + Tabs, Widget, + }, }; -use std::{fmt::Display, ops::Add}; +use reqtui::{ + schema::types::Request, + syntax::highlighter::{ColorInfo, HIGHLIGHTER}, + text_object::{TextObject, Write}, +}; +use std::{ + cell::RefCell, + fmt::Display, + ops::{Add, Deref}, + rc::Rc, +}; +use tree_sitter::Tree; #[derive(Debug, Default, Clone)] pub enum ReqEditorTabs { @@ -52,36 +67,139 @@ pub struct ReqEditorState<'a> { is_focused: bool, is_selected: bool, curr_tab: &'a ReqEditorTabs, + body_scroll: &'a mut usize, } impl<'a> ReqEditorState<'a> { - pub fn new(is_focused: bool, is_selected: bool, curr_tab: &'a ReqEditorTabs) -> Self { + pub fn new( + is_focused: bool, + is_selected: bool, + curr_tab: &'a ReqEditorTabs, + body_scroll: &'a mut usize, + ) -> Self { ReqEditorState { is_focused, curr_tab, is_selected, + body_scroll, } } } +#[derive(Debug, Clone)] pub struct ReqEditor<'a> { colors: &'a colors::Colors, + request: Option>>, + body: TextObject, + tree: Option, + styled_display: Vec>, } impl<'a> ReqEditor<'a> { - pub fn new(colors: &'a colors::Colors) -> Self { - Self { colors } + pub fn new(colors: &'a colors::Colors, request: Option>>) -> Self { + let (body, tree) = + if let Some(body) = request.as_ref().and_then(|req| req.borrow().body.clone()) { + let mut highlighter = HIGHLIGHTER.write().unwrap(); + let tree = highlighter.parse(&body); + + (TextObject::from(&body).with_write(), tree) + } else { + (TextObject::default(), None) + }; + + let body_str = body.to_string(); + let highlights = + HIGHLIGHTER + .read() + .unwrap() + .apply(&body_str, tree.as_ref(), &colors.tokens); + let mut styled_display: Vec = vec![]; + let mut current_line: Vec = vec![]; + + body_str.chars().enumerate().for_each(|(i, c)| match c { + '\n' => { + styled_display.push(current_line.clone().into()); + current_line.clear(); + } + _ => current_line.push(build_stylized_line(c, i, &highlights)), + }); + + if !body_str.ends_with('\n') { + styled_display.push(current_line.into()); + } + + Self { + colors, + request, + body, + tree, + styled_display, + } } - fn draw_editor(&self, state: &mut ReqEditorState, buf: &mut Buffer, size: Rect) {} + fn draw_editor(&self, state: &mut ReqEditorState, buf: &mut Buffer, size: Rect) { + let [request_pane, scrollbar_pane] = build_preview_layout(size); + + self.draw_scrollbar( + self.styled_display.len(), + *state.body_scroll, + buf, + scrollbar_pane, + ); + + if state + .body_scroll + .deref() + .ge(&self.styled_display.len().saturating_sub(1)) + { + *state.body_scroll = self.styled_display.len().saturating_sub(1); + } + + let lines_in_view = self + .styled_display + .clone() + .into_iter() + .skip(*state.body_scroll) + .chain(std::iter::repeat(Line::from( + "~".fg(self.colors.bright.black), + ))) + .take(size.height.into()) + .collect::>(); + + Paragraph::new(lines_in_view).render(request_pane, buf); + } - fn draw_current_tab(&self, state: &mut ReqEditorState, buf: &mut Buffer, size: Rect) { + fn draw_current_tab( + &self, + state: &mut ReqEditorState, + buf: &mut Buffer, + size: Rect, + ) -> anyhow::Result<()> { match state.curr_tab { ReqEditorTabs::Request => self.draw_editor(state, buf, size), ReqEditorTabs::Headers => {} ReqEditorTabs::Query => {} ReqEditorTabs::Auth => {} } + + Ok(()) + } + + fn draw_scrollbar( + &self, + total_ines: usize, + current_scroll: usize, + buf: &mut Buffer, + size: Rect, + ) { + let mut scrollbar_state = ScrollbarState::new(total_ines).position(current_scroll); + + let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight) + .style(Style::default().fg(self.colors.normal.red)) + .begin_symbol(Some("↑")) + .end_symbol(Some("↓")); + + scrollbar.render(size, buf, &mut scrollbar_state); } fn draw_tabs(&self, buf: &mut Buffer, state: &ReqEditorState, size: Rect) { @@ -119,7 +237,7 @@ impl<'a> StatefulWidget for ReqEditor<'a> { self.draw_container(size, buf, state); self.draw_tabs(buf, state, layout.tabs_pane); - self.draw_current_tab(state, buf, layout.content_pane); + self.draw_current_tab(state, buf, layout.content_pane).ok(); } } @@ -158,3 +276,13 @@ fn build_preview_layout(size: Rect) -> [Rect; 2] { [request_pane, scrollbar_pane] } + +fn build_stylized_line(c: char, i: usize, colors: &[ColorInfo]) -> Span<'static> { + c.to_string().set_style( + colors + .iter() + .find(|color| color.start <= i && color.end >= i) + .map(|c| c.style) + .unwrap_or_default(), + ) +} diff --git a/tui/src/components/api_explorer/req_uri.rs b/tui/src/components/api_explorer/req_uri.rs index 0381d22..97564e0 100644 --- a/tui/src/components/api_explorer/req_uri.rs +++ b/tui/src/components/api_explorer/req_uri.rs @@ -1,3 +1,5 @@ +use std::{cell::RefCell, rc::Rc}; + use ratatui::{ buffer::Buffer, layout::Rect, @@ -8,12 +10,12 @@ use reqtui::schema::types::Request; #[derive(Debug)] pub struct ReqUriState<'a> { - selected_request: Option<&'a Request>, + selected_request: &'a Option>>, is_focused: bool, } impl<'a> ReqUriState<'a> { - pub fn new(selected_request: Option<&'a Request>, is_focused: bool) -> Self { + pub fn new(selected_request: &'a Option>>, is_focused: bool) -> Self { Self { selected_request, is_focused, @@ -43,7 +45,7 @@ impl<'a> StatefulWidget for ReqUri<'a> { }; if let Some(req) = state.selected_request { - Paragraph::new(req.uri.clone()) + Paragraph::new(req.borrow().uri.clone()) .fg(self.colors.normal.white) .block( Block::default() diff --git a/tui/src/components/api_explorer/res_viewer.rs b/tui/src/components/api_explorer/res_viewer.rs index 26e045a..b48682a 100644 --- a/tui/src/components/api_explorer/res_viewer.rs +++ b/tui/src/components/api_explorer/res_viewer.rs @@ -171,7 +171,9 @@ impl<'a> ResViewer<'a> { fn draw_pretty_response(&self, state: &mut ResViewerState, buf: &mut Buffer, size: Rect) { if let Some(response) = state.response { - let lines = response.pretty_body.display.clone(); + // let lines = response.pretty_body.display.clone(); + // + let lines = vec![]; if state.raw_scroll.deref().ge(&lines.len().saturating_sub(1)) { *state.raw_scroll = lines.len().saturating_sub(1); diff --git a/tui/src/components/api_explorer/sidebar.rs b/tui/src/components/api_explorer/sidebar.rs index 49e4a6d..a015925 100644 --- a/tui/src/components/api_explorer/sidebar.rs +++ b/tui/src/components/api_explorer/sidebar.rs @@ -7,11 +7,11 @@ use ratatui::{ text::{Line, Span}, widgets::{Block, Borders, Paragraph, StatefulWidget, Widget}, }; -use std::collections::HashMap; +use std::{cell::RefCell, collections::HashMap, rc::Rc}; pub struct SidebarState<'a> { requests: Option<&'a [RequestKind]>, - selected_request: Option<&'a Request>, + selected_request: &'a Option>>, hovered_requet: Option<&'a RequestKind>, dirs_expanded: &'a mut HashMap, is_focused: bool, @@ -20,7 +20,7 @@ pub struct SidebarState<'a> { impl<'a> SidebarState<'a> { pub fn new( requests: Option<&'a [RequestKind]>, - selected_request: Option<&'a Request>, + selected_request: &'a Option>>, hovered_requet: Option<&'a RequestKind>, dirs_expanded: &'a mut HashMap, is_focused: bool, @@ -59,7 +59,7 @@ impl<'a> StatefulWidget for Sidebar<'a> { let lines = build_lines( state.requests, 0, - state.selected_request, + &state.selected_request, state.hovered_requet, state.dirs_expanded, self.colors, @@ -96,7 +96,7 @@ impl<'a> StatefulWidget for Sidebar<'a> { fn build_lines( requests: Option<&[RequestKind]>, level: usize, - selected_request: Option<&Request>, + selected_request: &Option>>, hovered_request: Option<&RequestKind>, dirs_expanded: &mut HashMap, colors: &colors::Colors, @@ -147,7 +147,9 @@ fn build_lines( } RequestKind::Single(req) => { let gap = " ".repeat(level * 2); - let is_selected = selected_request.is_some_and(|selected| *selected == *req); + let is_selected = selected_request + .as_ref() + .is_some_and(|selected| &*selected.borrow() == req); let is_hovered = hovered_request.is_some_and(|req| *req == *item); let req_style = match (is_selected, is_hovered) {