table.rs

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