diff --git a/docs/guides/working_with_data/dataframes.md b/docs/guides/working_with_data/dataframes.md
index 69a2dd6dab5..9e24a59310a 100644
--- a/docs/guides/working_with_data/dataframes.md
+++ b/docs/guides/working_with_data/dataframes.md
@@ -46,18 +46,42 @@ df
import polars as pl
df = pl.read_json(
- "https://raw.githubusercontent.com/vega/vega-datasets/master/data/cars.json"
+"https://raw.githubusercontent.com/vega/vega-datasets/master/data/cars.json"
)
df
```
///
+/// tab | live example
+
+/// marimo-embed
+ size: large
+
+```python
+@app.cell
+def __():
+ import pandas as pd
+
+ pd.read_json(
+ "https://raw.githubusercontent.com/vega/vega-datasets/master/data/cars.json"
+ )
+ return
+```
+
+///
+
+///
+
+
To opt out of the rich dataframe viewer, use [`mo.plain`][marimo.plain]:
/// tab | pandas
```python
+import pandas as pd
+import marimo as mo
+
df = pd.read_json(
"https://raw.githubusercontent.com/vega/vega-datasets/master/data/cars.json"
)
@@ -69,6 +93,9 @@ mo.plain(df)
/// tab | polars
```python
+import polars as pl
+import marimo as mo
+
df = pl.read_json(
"https://raw.githubusercontent.com/vega/vega-datasets/master/data/cars.json"
)
@@ -77,6 +104,27 @@ mo.plain(df)
///
+/// tab | live example
+
+/// marimo-embed
+ size: large
+
+```python
+@app.cell
+def __():
+ import pandas as pd
+
+ df = pd.read_json(
+ "https://raw.githubusercontent.com/vega/vega-datasets/master/data/cars.json"
+ )
+ mo.plain(df)
+ return
+```
+
+///
+
+///
+
## Transforming dataframes
### No-code transformations
@@ -94,6 +142,15 @@ notebook.
+The transformations you apply will turn into code which is accessible via the "code" tab.
+
+
+
+
+Copy the code of the transformation
+
+
+
/// tab | pandas
```python
@@ -114,6 +171,7 @@ transformed_df.value
///
+
/// tab | polars
```python
@@ -134,12 +192,33 @@ transformed_df.value
///
-
-
-
-Copy the code of the transformation
-
-
+
+/// tab | live example
+
+/// marimo-embed
+ size: large
+
+```python
+@app.cell
+def __():
+ import pandas as pd
+
+ df = pd.DataFrame({"person": ["Alice", "Bob", "Charlie"], "age": [20, 30, 40]})
+ transformed_df = mo.ui.dataframe(df)
+ transformed_df
+ return
+
+@app.cell
+def __():
+ transformed_df.value
+
+ return
+```
+
+///
+
+///
+
### Custom filters
@@ -169,6 +248,7 @@ mo.ui.table(filtered_df)
/// tab | polars
```python
+# Cell 1
import marimo as mo
import polars as pl
@@ -192,6 +272,37 @@ mo.ui.table(filtered_df)
///
+/// tab | live example
+
+
+/// marimo-embed
+ size: large
+
+```python
+@app.cell
+def __():
+ import pandas as pd
+
+ df = pd.DataFrame({"person": ["Alice", "Bob", "Charlie"], "age": [20, 30, 40]})
+ return
+
+@app.cell
+def __():
+ age_filter = mo.ui.slider(start=0, stop=100, value=50, label="Max age")
+ age_filter
+ return
+
+@app.cell
+def __():
+ filtered_df = df[df["age"] < age_filter.value]
+ mo.ui.table(filtered_df)
+ return
+```
+
+///
+
+///
+
## Select dataframe rows {#selecting-dataframes}
Display dataframes as interactive, [selectable charts](plotting.md) using
@@ -247,6 +358,30 @@ table.value
///
+
+/// tab | live example
+
+/// marimo-embed
+ size: large
+
+```python
+@app.cell
+def __():
+ import pandas as pd
+
+ df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
+ table = mo.ui.table(df, selection="multi")
+ table
+ return
+
+@app.cell
+def __():
+ table.value
+ return
+```
+
+///
+
## Dataframe panels
Dataframe outputs in marimo come with several panels to help you visualize, explore, and page through your data interactively. These panels are accessible via toggles at the bottom-left of a dataframe output. If you need further control, after opening a panel you can
@@ -296,6 +431,7 @@ altair.data_transformers.enable("vegafusion")
The chart builder toggle lets you rapidly develop charts using a GUI, while also generating Python code to insert in your notebook. Refer to the [chart builder guide](plotting.md#chart-builder) for more details.
+
## Example notebook
For a comprehensive example of using Polars with marimo, check out our [Polars example notebook](https://github.com/marimo-team/marimo/blob/main/examples/third_party/polars/polars_example.py).
diff --git a/frontend/src/components/app-config/user-config-form.tsx b/frontend/src/components/app-config/user-config-form.tsx
index 8715db8a753..7636924e903 100644
--- a/frontend/src/components/app-config/user-config-form.tsx
+++ b/frontend/src/components/app-config/user-config-form.tsx
@@ -484,6 +484,49 @@ export const UserConfigForm: React.FC = () => {
)}
/>
+ (
+
+
+
+
+ Beta
+
+ basedpyright (
+
+ docs
+
+ )
+
+
+ {
+ field.onChange(Boolean(checked));
+ }}
+ />
+
+
+
+
+ {field.value && !capabilities.basedpyright && (
+
+ basedpyright is not available in your current
+ environment. Please install{" "}
+ basedpyright in your
+ environment.
+
+ )}
+
+ )}
+ />
({
spec={spec}
width={70}
height={30}
+ renderer="svg"
// @ts-expect-error - Our `loader.load` method is broader than VegaLite's typings but is functionally supported.
loader={batchedLoader}
style={{ minWidth: "unset", maxHeight: "40px" }}
diff --git a/frontend/src/components/data-table/row-viewer-panel/row-viewer.tsx b/frontend/src/components/data-table/row-viewer-panel/row-viewer.tsx
index 71964439624..e29f50ec63b 100644
--- a/frontend/src/components/data-table/row-viewer-panel/row-viewer.tsx
+++ b/frontend/src/components/data-table/row-viewer-panel/row-viewer.tsx
@@ -209,7 +209,7 @@ export const RowViewerPanel: React.FC = ({
() => columnValue,
() => columnValue,
undefined,
- "text-left break-all",
+ "text-left break-word",
);
const copyValue =
diff --git a/frontend/src/components/editor/Cell.tsx b/frontend/src/components/editor/Cell.tsx
index 2769ef11038..029f585cb67 100644
--- a/frontend/src/components/editor/Cell.tsx
+++ b/frontend/src/components/editor/Cell.tsx
@@ -726,16 +726,16 @@ const EditableCellComponent = ({
{cellOutput === "below" && outputArea}
{serialization && (
-
+
{isToplevel && (
-
+
reusable
-
+
)}
}
>
- {(isToplevel && (
-
- )) || (
-
+ {isToplevel ? (
+
+
+
+ ) : (
+
+
+
)}
-
+
)}
{
// Column 1: cellId2 (0-80), cellId4 (80-160), cellId5 (160-240)
mockGetElementById.mockImplementation((id) => {
const idToCellId = id.replace("cell-", "");
- if (idToCellId === cellId1) return createMockElement(0, 100);
- if (idToCellId === cellId2) return createMockElement(0, 80);
- if (idToCellId === cellId3) return createMockElement(100, 100);
- if (idToCellId === cellId4) return createMockElement(80, 80);
- if (idToCellId === cellId5) return createMockElement(160, 80);
+ if (idToCellId === cellId1) {
+ return createMockElement(0, 100);
+ }
+ if (idToCellId === cellId2) {
+ return createMockElement(0, 80);
+ }
+ if (idToCellId === cellId3) {
+ return createMockElement(100, 100);
+ }
+ if (idToCellId === cellId4) {
+ return createMockElement(80, 80);
+ }
+ if (idToCellId === cellId5) {
+ return createMockElement(160, 80);
+ }
return null;
});
@@ -1580,8 +1590,9 @@ describe("useCellEditorNavigationProps", () => {
describe("keyboard shortcuts", () => {
it("should focus cell when Escape is pressed", () => {
+ const mockEditorView = { current: null };
const { result } = renderWithProvider(() =>
- useCellEditorNavigationProps(mockCellId),
+ useCellEditorNavigationProps(mockCellId, mockEditorView),
);
const mockEvent = Mocks.keyboardEvent({ key: "Escape" });
@@ -1594,9 +1605,70 @@ describe("useCellEditorNavigationProps", () => {
expect(mockEvent.continuePropagation).not.toHaveBeenCalled();
});
+ it("should clear text selection when Escape is pressed with selection", () => {
+ const mockDispatch = vi.fn();
+ const mockEditorView = {
+ current: {
+ state: {
+ selection: { main: { from: 5, to: 10 } },
+ },
+ dispatch: mockDispatch,
+ } as unknown as EditorView,
+ };
+ const { result } = renderWithProvider(() =>
+ useCellEditorNavigationProps(mockCellId, mockEditorView),
+ );
+
+ const mockEvent = Mocks.keyboardEvent({ key: "Escape" });
+
+ act(() => {
+ result.current.onKeyDown?.(mockEvent);
+ });
+
+ const mockCall = mockDispatch.mock.calls[0][0];
+ expect(mockCall.selection.ranges[0].anchor).toBe(5);
+ expect(mockCall.selection.ranges[0].head).toBe(5);
+ expect(focusCell).not.toHaveBeenCalled();
+ });
+
+ it("should close autocomplete popup when Escape is pressed with popup active", () => {
+ const mockEditorView = {
+ current: {
+ state: {
+ selection: { main: { from: 5, to: 5 } },
+ field: vi.fn().mockReturnValue({ active: [{ state: 1 }] }), // Mock active completion
+ },
+ dispatch: vi.fn(),
+ } as unknown as EditorView,
+ };
+
+ // Mock the closeCompletion function
+ const originalCloseCompletion = vi.hoisted(() => vi.fn());
+ vi.mock("@codemirror/autocomplete", () => ({
+ completionStatus: vi.fn().mockReturnValue("active"),
+ closeCompletion: originalCloseCompletion,
+ }));
+
+ const { result } = renderWithProvider(() =>
+ useCellEditorNavigationProps(mockCellId, mockEditorView),
+ );
+
+ const mockEvent = Mocks.keyboardEvent({ key: "Escape" });
+
+ act(() => {
+ result.current.onKeyDown?.(mockEvent);
+ });
+
+ expect(originalCloseCompletion).toHaveBeenCalledWith(
+ mockEditorView.current,
+ );
+ expect(focusCell).not.toHaveBeenCalled();
+ });
+
it("should continue propagation for other keys", () => {
+ const mockEditorView = { current: null };
const { result } = renderWithProvider(() =>
- useCellEditorNavigationProps(mockCellId),
+ useCellEditorNavigationProps(mockCellId, mockEditorView),
);
const mockEvent = Mocks.keyboardEvent({ key: "Enter" });
@@ -1621,8 +1693,9 @@ describe("useCellEditorNavigationProps", () => {
});
it("should focus cell when Ctrl+Escape is pressed in vim mode", () => {
+ const mockEditorView = { current: null };
const { result } = renderWithProvider(() =>
- useCellEditorNavigationProps(mockCellId),
+ useCellEditorNavigationProps(mockCellId, mockEditorView),
);
const mockEvent = Mocks.keyboardEvent({ key: "Escape", ctrlKey: true });
@@ -1636,8 +1709,9 @@ describe("useCellEditorNavigationProps", () => {
});
it("should focus cell when Cmd+Escape (metaKey) is pressed in vim mode", () => {
+ const mockEditorView = { current: null };
const { result } = renderWithProvider(() =>
- useCellEditorNavigationProps(mockCellId),
+ useCellEditorNavigationProps(mockCellId, mockEditorView),
);
const mockEvent = Mocks.keyboardEvent({ key: "Escape", metaKey: true });
@@ -1651,8 +1725,9 @@ describe("useCellEditorNavigationProps", () => {
});
it("should not focus cell when Escape (without Ctrl) is pressed in vim mode", () => {
+ const mockEditorView = { current: null };
const { result } = renderWithProvider(() =>
- useCellEditorNavigationProps(mockCellId),
+ useCellEditorNavigationProps(mockCellId, mockEditorView),
);
const mockEvent = Mocks.keyboardEvent({ key: "Escape" });
diff --git a/frontend/src/components/editor/navigation/navigation.ts b/frontend/src/components/editor/navigation/navigation.ts
index c98dc72b861..8057937d8a6 100644
--- a/frontend/src/components/editor/navigation/navigation.ts
+++ b/frontend/src/components/editor/navigation/navigation.ts
@@ -1,5 +1,7 @@
/* Copyright 2024 Marimo. All rights reserved. */
+import { closeCompletion, completionStatus } from "@codemirror/autocomplete";
+import { EditorSelection } from "@codemirror/state";
import type { EditorView } from "@codemirror/view";
import { useAtomValue, useSetAtom, useStore } from "jotai";
import { mergeProps, useFocusWithin, useKeyboard } from "react-aria";
@@ -600,23 +602,61 @@ export function useCellNavigationProps(
*
* Handles both keyboard and mouse navigation.
*/
-export function useCellEditorNavigationProps(cellId: CellId) {
+export function useCellEditorNavigationProps(
+ cellId: CellId,
+ editorView: React.RefObject,
+) {
const setTemporarilyShownCode = useSetAtom(temporarilyShownCodeAtom);
const keymapPreset = useAtomValue(keymapPresetAtom);
+ const exitToCommandMode = () => {
+ setTemporarilyShownCode(false);
+ focusCell(cellId);
+ };
+
+ const handleEscape = () => {
+ // If there is a text selection or autocomplete popup in the editor, we clear those and return.
+ // Subsequent 'Escapes' will exit to command mode.
+
+ if (!editorView.current) {
+ // If no editor, we can exit to command mode immediately
+ exitToCommandMode();
+ return;
+ }
+
+ const view = editorView.current;
+ const state = view.state;
+
+ const hasTextSelection =
+ state.selection.main.from !== state.selection.main.to;
+
+ if (hasTextSelection) {
+ view.dispatch({
+ selection: EditorSelection.single(state.selection.main.from), // Cursor to the start of the selection
+ });
+ return;
+ }
+
+ const hasAutocompletePopup = completionStatus(state) !== null;
+ if (hasAutocompletePopup) {
+ closeCompletion(view);
+ return;
+ }
+
+ exitToCommandMode();
+ };
+
const { keyboardProps } = useKeyboard({
onKeyDown: (evt) => {
// For vim mode, require Ctrl+Escape (or Cmd+Escape on Mac) to exit to command mode
if (keymapPreset === "vim") {
if (evt.key === "Escape" && (evt.ctrlKey || evt.metaKey)) {
- setTemporarilyShownCode(false);
- focusCell(cellId);
+ handleEscape();
}
} else {
// For non-vim mode, regular Escape exits to command mode
if (evt.key === "Escape") {
- setTemporarilyShownCode(false);
- focusCell(cellId);
+ handleEscape();
}
}
diff --git a/frontend/src/components/ui/combobox.tsx b/frontend/src/components/ui/combobox.tsx
index fb9eea8d9f2..446b273c2cb 100644
--- a/frontend/src/components/ui/combobox.tsx
+++ b/frontend/src/components/ui/combobox.tsx
@@ -175,8 +175,8 @@ export const Combobox = ({
)}
aria-expanded={open}
>
- {renderValue()}{" "}
-
+ {renderValue()}
+
{
);
});
+const pyrightClient = once((_: LSPConfig) => {
+ const lspClientOpts = {
+ transport: createTransport("basedpyright"),
+ rootUri: getLSPDocumentRootUri(),
+ workspaceFolders: [],
+ };
+
+ // We wrap the client in a NotebookLanguageServerClient to add some
+ // additional functionality to handle multiple cells
+ return new NotebookLanguageServerClient(
+ new LanguageServerClient({
+ ...lspClientOpts,
+ autoClose: false,
+ }),
+ {},
+ );
+});
+
/**
* Language adapter for Python.
*/
@@ -209,6 +227,9 @@ export class PythonLanguageAdapter implements LanguageAdapter<{}> {
if (lspConfig?.ty?.enabled && hasCapability("ty")) {
clients.push(tyLspClient(lspConfig));
}
+ if (lspConfig?.basedpyright?.enabled && hasCapability("basedpyright")) {
+ clients.push(pyrightClient(lspConfig));
+ }
if (clients.length > 0) {
const client =
diff --git a/frontend/src/core/codemirror/lsp/transports.ts b/frontend/src/core/codemirror/lsp/transports.ts
index 87998e83f87..b539cda89b9 100644
--- a/frontend/src/core/codemirror/lsp/transports.ts
+++ b/frontend/src/core/codemirror/lsp/transports.ts
@@ -12,7 +12,9 @@ import { getRuntimeManager } from "../../runtime/config";
* @param serverName - The name of the LSP server.
* @returns The transport.
*/
-export function createTransport(serverName: "pylsp" | "copilot" | "ty") {
+export function createTransport(
+ serverName: "pylsp" | "basedpyright" | "copilot" | "ty",
+) {
const runtimeManager = getRuntimeManager();
const transport = new WebSocketTransport(
runtimeManager.getLSPURL(serverName).toString(),
diff --git a/frontend/src/core/config/capabilities.ts b/frontend/src/core/config/capabilities.ts
index 5d87102f70c..c046149eb33 100644
--- a/frontend/src/core/config/capabilities.ts
+++ b/frontend/src/core/config/capabilities.ts
@@ -6,6 +6,7 @@ import { store } from "../state/jotai";
export const capabilitiesAtom = atom({
terminal: false,
pylsp: false,
+ basedpyright: false,
ty: false,
});
diff --git a/frontend/src/core/runtime/runtime.ts b/frontend/src/core/runtime/runtime.ts
index 40ee2ab5ed8..01216b6fd5b 100644
--- a/frontend/src/core/runtime/runtime.ts
+++ b/frontend/src/core/runtime/runtime.ts
@@ -135,7 +135,7 @@ export class RuntimeManager {
/**
* The URL of the copilot server.
*/
- getLSPURL(lsp: "pylsp" | "copilot" | "ty"): URL {
+ getLSPURL(lsp: "pylsp" | "basedpyright" | "copilot" | "ty"): URL {
if (lsp === "copilot") {
// For copilot, don't include any query parameters
const url = this.formatWsURL(`/lsp/${lsp}`);
diff --git a/frontend/src/plugins/impl/SearchableSelect.tsx b/frontend/src/plugins/impl/SearchableSelect.tsx
index 175ed443ea1..924f92c2612 100644
--- a/frontend/src/plugins/impl/SearchableSelect.tsx
+++ b/frontend/src/plugins/impl/SearchableSelect.tsx
@@ -105,7 +105,9 @@ export const SearchableSelect = (props: SearchableSelectProps): JSX.Element => {
}}
placeholder="Select..."
multiple={false}
- className={cn("w-full", { "w-full": fullWidth })}
+ className={cn({
+ "w-full": fullWidth,
+ })}
value={value ?? NONE_KEY}
onValueChange={handleValueChange}
shouldFilter={false}
diff --git a/frontend/src/plugins/impl/data-frames/forms/__tests__/__snapshots__/form.test.tsx.snap b/frontend/src/plugins/impl/data-frames/forms/__tests__/__snapshots__/form.test.tsx.snap
index 824dfe26e94..11322820044 100644
--- a/frontend/src/plugins/impl/data-frames/forms/__tests__/__snapshots__/form.test.tsx.snap
+++ b/frontend/src/plugins/impl/data-frames/forms/__tests__/__snapshots__/form.test.tsx.snap
@@ -36,10 +36,14 @@ exports[`renderZodSchema > should render a form aggregate 1`] = `
data-state="closed"
type="button"
>
- Select columns
+
+ Select columns
+