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::{div, prelude::*, v_flex, IntoElement, Styled};
64use util::markdown::MarkdownString;
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, cx: &mut WindowContext) -> Self {
92 let mut widths = Vec::with_capacity(table.schema.fields.len());
93
94 let text_system = cx.text_system();
95 let text_style = cx.text_style();
96 let text_font = ThemeSettings::get_global(cx).buffer_font.clone();
97 let font_size = ThemeSettings::get_global(cx).buffer_font_size;
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)
111 .map(|layout| layout.width)
112 .unwrap_or(px(0.));
113
114 let Some(data) = table.data.as_ref() else {
115 widths.push(width);
116 continue;
117 };
118
119 for row in data {
120 let content = cell_content(row, &field.name);
121 runs[0].len = content.len();
122 let cell_width = cx
123 .text_system()
124 .layout_line(&content, font_size, &runs)
125 .map(|layout| layout.width)
126 .unwrap_or(px(0.));
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| MarkdownString::escape(&cell_content(record, &field.name)).0)
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 cx: &WindowContext,
193 ) -> AnyElement {
194 let theme = cx.theme();
195
196 let line_height = cx.line_height();
197
198 let row_cells = schema
199 .fields
200 .iter()
201 .zip(self.widths.iter())
202 .map(|(field, width)| {
203 let container = match field.field_type {
204 runtimelib::datatable::FieldType::String => div(),
205
206 runtimelib::datatable::FieldType::Number
207 | runtimelib::datatable::FieldType::Integer
208 | runtimelib::datatable::FieldType::Date
209 | runtimelib::datatable::FieldType::Time
210 | runtimelib::datatable::FieldType::Datetime
211 | runtimelib::datatable::FieldType::Year
212 | runtimelib::datatable::FieldType::Duration
213 | runtimelib::datatable::FieldType::Yearmonth => v_flex().items_end(),
214
215 _ => div(),
216 };
217
218 let value = cell_content(row, &field.name);
219
220 let mut cell = container
221 .min_w(*width + px(22.))
222 .w(*width + px(22.))
223 .child(value)
224 .px_2()
225 .py((TABLE_Y_PADDING_MULTIPLE / 2.0) * line_height)
226 .border_color(theme.colors().border);
227
228 if is_header {
229 cell = cell.border_1().bg(theme.colors().border_focused)
230 } else {
231 cell = cell.border_1()
232 }
233 cell
234 })
235 .collect::<Vec<_>>();
236
237 let mut total_width = px(0.);
238 for width in self.widths.iter() {
239 // Width fudge factor: border + 2 (heading), padding
240 total_width += *width + px(22.);
241 }
242
243 h_flex()
244 .w(total_width)
245 .children(row_cells)
246 .into_any_element()
247 }
248}
249
250impl Render for TableView {
251 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
252 let data = match &self.table.data {
253 Some(data) => data,
254 None => return div().into_any_element(),
255 };
256
257 let mut headings = serde_json::Map::new();
258 for field in &self.table.schema.fields {
259 headings.insert(field.name.clone(), Value::String(field.name.clone()));
260 }
261 let header = self.render_row(&self.table.schema, true, &Value::Object(headings), cx);
262
263 let body = data
264 .iter()
265 .map(|row| self.render_row(&self.table.schema, false, row, cx));
266
267 v_flex()
268 .id("table")
269 .overflow_x_scroll()
270 .w_full()
271 .child(header)
272 .children(body)
273 .into_any_element()
274 }
275}
276
277impl OutputContent for TableView {
278 fn clipboard_content(&self, _cx: &WindowContext) -> Option<ClipboardItem> {
279 Some(self.cached_clipboard_content.clone())
280 }
281
282 fn has_clipboard_content(&self, _cx: &WindowContext) -> bool {
283 true
284 }
285}