1//! # Table Output for REPL
2//!
3//! This module provides functionality to render tabular data in Zed's REPL output.
4//!
5//! It supports the [Frictionless Data Table Schema](https://specs.frictionlessdata.io/table-schema/)
6//! for data interchange, implemented by Pandas in Python and Polars for Deno.
7//!
8//! # Python Example
9//!
10//! Tables can be created and displayed in two main ways:
11//!
12//! 1. Using raw JSON data conforming to the Tabular Data Resource specification.
13//! 2. Using Pandas DataFrames (in Python kernels).
14//!
15//! ## Raw JSON Method
16//!
17//! To create a table using raw JSON, you need to provide a JSON object that conforms
18//! to the Tabular Data Resource specification. Here's an example:
19//!
20//! ```json
21//! {
22//! "schema": {
23//! "fields": [
24//! {"name": "id", "type": "integer"},
25//! {"name": "name", "type": "string"},
26//! {"name": "age", "type": "integer"}
27//! ]
28//! },
29//! "data": [
30//! {"id": 1, "name": "Alice", "age": 30},
31//! {"id": 2, "name": "Bob", "age": 28},
32//! {"id": 3, "name": "Charlie", "age": 35}
33//! ]
34//! }
35//! ```
36//!
37//! ## Pandas Method
38//!
39//! To create a table using Pandas in a Python kernel, you can use the following steps:
40//!
41//! ```python
42//! import pandas as pd
43//!
44//! # Enable table schema output
45//! pd.set_option('display.html.table_schema', True)
46//!
47//! # Create a DataFrame
48//! df = pd.DataFrame({
49//! 'id': [1, 2, 3],
50//! 'name': ['Alice', 'Bob', 'Charlie'],
51//! 'age': [30, 28, 35]
52//! })
53//!
54//! # Display the DataFrame
55//! display(df)
56//! ```
57use gpui::{AnyElement, ClipboardItem, TextRun};
58use runtimelib::datatable::TableSchema;
59use runtimelib::media::datatable::TabularDataResource;
60use serde_json::Value;
61use settings::Settings;
62use theme::ThemeSettings;
63use ui::{IntoElement, Styled, div, prelude::*, v_flex};
64use util::markdown::MarkdownEscaped;
65
66use crate::outputs::OutputContent;
67
68/// TableView renders a static table inline in a buffer.
69///
70/// It uses the <https://specs.frictionlessdata.io/tabular-data-resource/>
71/// specification for data interchange.
72pub struct TableView {
73 pub table: TabularDataResource,
74 pub widths: Vec<Pixels>,
75 cached_clipboard_content: ClipboardItem,
76}
77
78fn cell_content(row: &Value, field: &str) -> String {
79 match row.get(field) {
80 Some(Value::String(s)) => s.clone(),
81 Some(Value::Number(n)) => n.to_string(),
82 Some(Value::Bool(b)) => b.to_string(),
83 Some(Value::Array(arr)) => format!("{:?}", arr),
84 Some(Value::Object(obj)) => format!("{:?}", obj),
85 Some(Value::Null) | None => String::new(),
86 }
87}
88
89// Declare constant for the padding multiple on the line height
90const TABLE_Y_PADDING_MULTIPLE: f32 = 0.5;
91
92impl TableView {
93 pub fn new(table: &TabularDataResource, window: &mut Window, cx: &mut App) -> Self {
94 let mut widths = Vec::with_capacity(table.schema.fields.len());
95
96 let text_system = window.text_system();
97 let text_style = window.text_style();
98 let text_font = ThemeSettings::get_global(cx).buffer_font.clone();
99 let font_size = ThemeSettings::get_global(cx).buffer_font_size(cx);
100 let mut runs = [TextRun {
101 len: 0,
102 font: text_font,
103 color: text_style.color,
104 background_color: None,
105 underline: None,
106 strikethrough: None,
107 }];
108
109 for field in table.schema.fields.iter() {
110 runs[0].len = field.name.len();
111 let mut width = text_system
112 .layout_line(&field.name, font_size, &runs, None)
113 .width;
114
115 let Some(data) = table.data.as_ref() else {
116 widths.push(width);
117 continue;
118 };
119
120 for row in data {
121 let content = cell_content(row, &field.name);
122 runs[0].len = content.len();
123 let cell_width = window
124 .text_system()
125 .layout_line(&content, font_size, &runs, None)
126 .width;
127
128 width = width.max(cell_width)
129 }
130
131 widths.push(width)
132 }
133
134 let cached_clipboard_content = Self::create_clipboard_content(table);
135
136 Self {
137 table: table.clone(),
138 widths,
139 cached_clipboard_content: ClipboardItem::new_string(cached_clipboard_content),
140 }
141 }
142
143 fn create_clipboard_content(table: &TabularDataResource) -> String {
144 let data = match table.data.as_ref() {
145 Some(data) => data,
146 None => &Vec::new(),
147 };
148 let schema = table.schema.clone();
149
150 let mut markdown = format!(
151 "| {} |\n",
152 table
153 .schema
154 .fields
155 .iter()
156 .map(|field| field.name.clone())
157 .collect::<Vec<_>>()
158 .join(" | ")
159 );
160
161 markdown.push_str("|---");
162 for _ in 1..table.schema.fields.len() {
163 markdown.push_str("|---");
164 }
165 markdown.push_str("|\n");
166
167 let body = data
168 .iter()
169 .map(|record: &Value| {
170 let row_content = schema
171 .fields
172 .iter()
173 .map(|field| MarkdownEscaped(&cell_content(record, &field.name)).to_string())
174 .collect::<Vec<_>>();
175
176 row_content.join(" | ")
177 })
178 .collect::<Vec<String>>();
179
180 for row in body {
181 markdown.push_str(&format!("| {} |\n", row));
182 }
183
184 markdown
185 }
186
187 pub fn render_row(
188 &self,
189 schema: &TableSchema,
190 is_header: bool,
191 row: &Value,
192 window: &mut Window,
193 cx: &mut App,
194 ) -> AnyElement {
195 let theme = cx.theme();
196
197 let line_height = window.line_height();
198
199 let row_cells = schema
200 .fields
201 .iter()
202 .zip(self.widths.iter())
203 .map(|(field, width)| {
204 let container = match field.field_type {
205 runtimelib::datatable::FieldType::String => div(),
206
207 runtimelib::datatable::FieldType::Number
208 | runtimelib::datatable::FieldType::Integer
209 | runtimelib::datatable::FieldType::Date
210 | runtimelib::datatable::FieldType::Time
211 | runtimelib::datatable::FieldType::Datetime
212 | runtimelib::datatable::FieldType::Year
213 | runtimelib::datatable::FieldType::Duration
214 | runtimelib::datatable::FieldType::Yearmonth => v_flex().items_end(),
215
216 _ => div(),
217 };
218
219 let value = cell_content(row, &field.name);
220
221 let mut cell = container
222 .min_w(*width + px(22.))
223 .w(*width + px(22.))
224 .child(value)
225 .px_2()
226 .py((TABLE_Y_PADDING_MULTIPLE / 2.0) * line_height)
227 .border_color(theme.colors().border);
228
229 if is_header {
230 cell = cell.border_1().bg(theme.colors().border_focused)
231 } else {
232 cell = cell.border_1()
233 }
234 cell
235 })
236 .collect::<Vec<_>>();
237
238 let mut total_width = px(0.);
239 for width in self.widths.iter() {
240 // Width fudge factor: border + 2 (heading), padding
241 total_width += *width + px(22.);
242 }
243
244 h_flex()
245 .w(total_width)
246 .children(row_cells)
247 .into_any_element()
248 }
249}
250
251impl Render for TableView {
252 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
253 let data = match &self.table.data {
254 Some(data) => data,
255 None => return div().into_any_element(),
256 };
257
258 let mut headings = serde_json::Map::new();
259 for field in &self.table.schema.fields {
260 headings.insert(field.name.clone(), Value::String(field.name.clone()));
261 }
262 let header = self.render_row(
263 &self.table.schema,
264 true,
265 &Value::Object(headings),
266 window,
267 cx,
268 );
269
270 let body = data
271 .iter()
272 .map(|row| self.render_row(&self.table.schema, false, row, window, cx));
273
274 v_flex()
275 .id("table")
276 .overflow_x_scroll()
277 .w_full()
278 .child(header)
279 .children(body)
280 .into_any_element()
281 }
282}
283
284impl OutputContent for TableView {
285 fn clipboard_content(&self, _window: &Window, _cx: &App) -> Option<ClipboardItem> {
286 Some(self.cached_clipboard_content.clone())
287 }
288
289 fn has_clipboard_content(&self, _window: &Window, _cx: &App) -> bool {
290 true
291 }
292}