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::{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}