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/// It uses the https://specs.frictionlessdata.io/tabular-data-resource/ specification for data interchange.
70pub struct TableView {
71 pub table: TabularDataResource,
72 pub widths: Vec<Pixels>,
73 cached_clipboard_content: ClipboardItem,
74}
75
76fn cell_content(row: &Value, field: &str) -> String {
77 match row.get(field) {
78 Some(Value::String(s)) => s.clone(),
79 Some(Value::Number(n)) => n.to_string(),
80 Some(Value::Bool(b)) => b.to_string(),
81 Some(Value::Array(arr)) => format!("{:?}", arr),
82 Some(Value::Object(obj)) => format!("{:?}", obj),
83 Some(Value::Null) | None => String::new(),
84 }
85}
86
87// Declare constant for the padding multiple on the line height
88const TABLE_Y_PADDING_MULTIPLE: f32 = 0.5;
89
90impl TableView {
91 pub fn new(table: &TabularDataResource, window: &mut Window, cx: &mut App) -> Self {
92 let mut widths = Vec::with_capacity(table.schema.fields.len());
93
94 let text_system = window.text_system();
95 let text_style = window.text_style();
96 let text_font = ThemeSettings::get_global(cx).buffer_font.clone();
97 let font_size = ThemeSettings::get_global(cx).buffer_font_size(cx);
98 let mut runs = [TextRun {
99 len: 0,
100 font: text_font,
101 color: text_style.color,
102 background_color: None,
103 underline: None,
104 strikethrough: None,
105 }];
106
107 for field in table.schema.fields.iter() {
108 runs[0].len = field.name.len();
109 let mut width = text_system
110 .layout_line(&field.name, font_size, &runs, None)
111 .width;
112
113 let Some(data) = table.data.as_ref() else {
114 widths.push(width);
115 continue;
116 };
117
118 for row in data {
119 let content = cell_content(row, &field.name);
120 runs[0].len = content.len();
121 let cell_width = window
122 .text_system()
123 .layout_line(&content, font_size, &runs, None)
124 .width;
125
126 width = width.max(cell_width)
127 }
128
129 widths.push(width)
130 }
131
132 let cached_clipboard_content = Self::create_clipboard_content(table);
133
134 Self {
135 table: table.clone(),
136 widths,
137 cached_clipboard_content: ClipboardItem::new_string(cached_clipboard_content),
138 }
139 }
140
141 fn create_clipboard_content(table: &TabularDataResource) -> String {
142 let data = match table.data.as_ref() {
143 Some(data) => data,
144 None => &Vec::new(),
145 };
146 let schema = table.schema.clone();
147
148 let mut markdown = format!(
149 "| {} |\n",
150 table
151 .schema
152 .fields
153 .iter()
154 .map(|field| field.name.clone())
155 .collect::<Vec<_>>()
156 .join(" | ")
157 );
158
159 markdown.push_str("|---");
160 for _ in 1..table.schema.fields.len() {
161 markdown.push_str("|---");
162 }
163 markdown.push_str("|\n");
164
165 let body = data
166 .iter()
167 .map(|record: &Value| {
168 let row_content = schema
169 .fields
170 .iter()
171 .map(|field| MarkdownEscaped(&cell_content(record, &field.name)).to_string())
172 .collect::<Vec<_>>();
173
174 row_content.join(" | ")
175 })
176 .collect::<Vec<String>>();
177
178 for row in body {
179 markdown.push_str(&format!("| {} |\n", row));
180 }
181
182 markdown
183 }
184
185 pub fn render_row(
186 &self,
187 schema: &TableSchema,
188 is_header: bool,
189 row: &Value,
190 window: &mut Window,
191 cx: &mut App,
192 ) -> AnyElement {
193 let theme = cx.theme();
194
195 let line_height = window.line_height();
196
197 let row_cells = schema
198 .fields
199 .iter()
200 .zip(self.widths.iter())
201 .map(|(field, width)| {
202 let container = match field.field_type {
203 runtimelib::datatable::FieldType::String => div(),
204
205 runtimelib::datatable::FieldType::Number
206 | runtimelib::datatable::FieldType::Integer
207 | runtimelib::datatable::FieldType::Date
208 | runtimelib::datatable::FieldType::Time
209 | runtimelib::datatable::FieldType::Datetime
210 | runtimelib::datatable::FieldType::Year
211 | runtimelib::datatable::FieldType::Duration
212 | runtimelib::datatable::FieldType::Yearmonth => v_flex().items_end(),
213
214 _ => div(),
215 };
216
217 let value = cell_content(row, &field.name);
218
219 let mut cell = container
220 .min_w(*width + px(22.))
221 .w(*width + px(22.))
222 .child(value)
223 .px_2()
224 .py((TABLE_Y_PADDING_MULTIPLE / 2.0) * line_height)
225 .border_color(theme.colors().border);
226
227 if is_header {
228 cell = cell.border_1().bg(theme.colors().border_focused)
229 } else {
230 cell = cell.border_1()
231 }
232 cell
233 })
234 .collect::<Vec<_>>();
235
236 let mut total_width = px(0.);
237 for width in self.widths.iter() {
238 // Width fudge factor: border + 2 (heading), padding
239 total_width += *width + px(22.);
240 }
241
242 h_flex()
243 .w(total_width)
244 .children(row_cells)
245 .into_any_element()
246 }
247}
248
249impl Render for TableView {
250 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
251 let data = match &self.table.data {
252 Some(data) => data,
253 None => return div().into_any_element(),
254 };
255
256 let mut headings = serde_json::Map::new();
257 for field in &self.table.schema.fields {
258 headings.insert(field.name.clone(), Value::String(field.name.clone()));
259 }
260 let header = self.render_row(
261 &self.table.schema,
262 true,
263 &Value::Object(headings),
264 window,
265 cx,
266 );
267
268 let body = data
269 .iter()
270 .map(|row| self.render_row(&self.table.schema, false, row, window, cx));
271
272 v_flex()
273 .id("table")
274 .overflow_x_scroll()
275 .w_full()
276 .child(header)
277 .children(body)
278 .into_any_element()
279 }
280}
281
282impl OutputContent for TableView {
283 fn clipboard_content(&self, _window: &Window, _cx: &App) -> Option<ClipboardItem> {
284 Some(self.cached_clipboard_content.clone())
285 }
286
287 fn has_clipboard_content(&self, _window: &Window, _cx: &App) -> bool {
288 true
289 }
290}