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.layout_line(&field.name, font_size, &runs).width;
110
111 let Some(data) = table.data.as_ref() else {
112 widths.push(width);
113 continue;
114 };
115
116 for row in data {
117 let content = cell_content(row, &field.name);
118 runs[0].len = content.len();
119 let cell_width = window
120 .text_system()
121 .layout_line(&content, font_size, &runs)
122 .width;
123
124 width = width.max(cell_width)
125 }
126
127 widths.push(width)
128 }
129
130 let cached_clipboard_content = Self::create_clipboard_content(table);
131
132 Self {
133 table: table.clone(),
134 widths,
135 cached_clipboard_content: ClipboardItem::new_string(cached_clipboard_content),
136 }
137 }
138
139 fn create_clipboard_content(table: &TabularDataResource) -> String {
140 let data = match table.data.as_ref() {
141 Some(data) => data,
142 None => &Vec::new(),
143 };
144 let schema = table.schema.clone();
145
146 let mut markdown = format!(
147 "| {} |\n",
148 table
149 .schema
150 .fields
151 .iter()
152 .map(|field| field.name.clone())
153 .collect::<Vec<_>>()
154 .join(" | ")
155 );
156
157 markdown.push_str("|---");
158 for _ in 1..table.schema.fields.len() {
159 markdown.push_str("|---");
160 }
161 markdown.push_str("|\n");
162
163 let body = data
164 .iter()
165 .map(|record: &Value| {
166 let row_content = schema
167 .fields
168 .iter()
169 .map(|field| MarkdownEscaped(&cell_content(record, &field.name)).to_string())
170 .collect::<Vec<_>>();
171
172 row_content.join(" | ")
173 })
174 .collect::<Vec<String>>();
175
176 for row in body {
177 markdown.push_str(&format!("| {} |\n", row));
178 }
179
180 markdown
181 }
182
183 pub fn render_row(
184 &self,
185 schema: &TableSchema,
186 is_header: bool,
187 row: &Value,
188 window: &mut Window,
189 cx: &mut App,
190 ) -> AnyElement {
191 let theme = cx.theme();
192
193 let line_height = window.line_height();
194
195 let row_cells = schema
196 .fields
197 .iter()
198 .zip(self.widths.iter())
199 .map(|(field, width)| {
200 let container = match field.field_type {
201 runtimelib::datatable::FieldType::String => div(),
202
203 runtimelib::datatable::FieldType::Number
204 | runtimelib::datatable::FieldType::Integer
205 | runtimelib::datatable::FieldType::Date
206 | runtimelib::datatable::FieldType::Time
207 | runtimelib::datatable::FieldType::Datetime
208 | runtimelib::datatable::FieldType::Year
209 | runtimelib::datatable::FieldType::Duration
210 | runtimelib::datatable::FieldType::Yearmonth => v_flex().items_end(),
211
212 _ => div(),
213 };
214
215 let value = cell_content(row, &field.name);
216
217 let mut cell = container
218 .min_w(*width + px(22.))
219 .w(*width + px(22.))
220 .child(value)
221 .px_2()
222 .py((TABLE_Y_PADDING_MULTIPLE / 2.0) * line_height)
223 .border_color(theme.colors().border);
224
225 if is_header {
226 cell = cell.border_1().bg(theme.colors().border_focused)
227 } else {
228 cell = cell.border_1()
229 }
230 cell
231 })
232 .collect::<Vec<_>>();
233
234 let mut total_width = px(0.);
235 for width in self.widths.iter() {
236 // Width fudge factor: border + 2 (heading), padding
237 total_width += *width + px(22.);
238 }
239
240 h_flex()
241 .w(total_width)
242 .children(row_cells)
243 .into_any_element()
244 }
245}
246
247impl Render for TableView {
248 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
249 let data = match &self.table.data {
250 Some(data) => data,
251 None => return div().into_any_element(),
252 };
253
254 let mut headings = serde_json::Map::new();
255 for field in &self.table.schema.fields {
256 headings.insert(field.name.clone(), Value::String(field.name.clone()));
257 }
258 let header = self.render_row(
259 &self.table.schema,
260 true,
261 &Value::Object(headings),
262 window,
263 cx,
264 );
265
266 let body = data
267 .iter()
268 .map(|row| self.render_row(&self.table.schema, false, row, window, cx));
269
270 v_flex()
271 .id("table")
272 .overflow_x_scroll()
273 .w_full()
274 .child(header)
275 .children(body)
276 .into_any_element()
277 }
278}
279
280impl OutputContent for TableView {
281 fn clipboard_content(&self, _window: &Window, _cx: &App) -> Option<ClipboardItem> {
282 Some(self.cached_clipboard_content.clone())
283 }
284
285 fn has_clipboard_content(&self, _window: &Window, _cx: &App) -> bool {
286 true
287 }
288}