Skip to content

Commit

Permalink
Add support for Python cells (#2936)
Browse files Browse the repository at this point in the history
Co-authored-by: José Valim <[email protected]>
  • Loading branch information
jonatanklosko and josevalim authored Feb 18, 2025
1 parent 5309030 commit 015b44f
Show file tree
Hide file tree
Showing 40 changed files with 1,731 additions and 447 deletions.
77 changes: 53 additions & 24 deletions assets/css/js_interop.css
Original file line number Diff line number Diff line change
Expand Up @@ -124,33 +124,64 @@ server in solely client-side operations.
}
}

[data-el-cell][data-type="setup"]:not(
[data-js-focused],
[data-eval-validity="fresh"]:not([data-js-empty]),
[data-eval-errored],
[data-js-changed]
/* When all setup cells satisfy collapse conditions, collapse the
first one and hide the later ones */
[data-el-setup-section]:not(
:has(
[data-el-cell][data-setup]:is(
[data-js-focused],
[data-eval-validity="fresh"]:not([data-js-empty]),
[data-eval-errored],
[data-js-changed]
)
),
:focus-within
) {
[data-el-editor-box] {
[data-el-cell][data-setup]:not(:first-child) {
@apply hidden;
}

[data-el-outputs-container] {
@apply hidden;
[data-el-cell][data-setup]:first-child {
[data-el-editor-box] {
@apply hidden;
}

[data-el-outputs-container] {
@apply hidden;
}

[data-el-cell-indicator] {
@apply bg-gray-50 border-gray-200 text-gray-500;
}
}

[data-el-cell-indicator] {
@apply bg-gray-50 border-gray-200 text-gray-500;
[data-el-language-buttons] {
@apply hidden;
}
}

[data-el-cell][data-type="setup"]:is(
[data-js-focused],
[data-eval-validity="fresh"]:not([data-js-empty]),
[data-eval-errored],
[data-js-changed]
)
[data-el-info-box] {
@apply hidden;
/* This is "else" for the above */
[data-el-setup-section]:is(
:has(
[data-el-cell][data-setup]:is(
[data-js-focused],
[data-eval-validity="fresh"]:not([data-js-empty]),
[data-eval-errored],
[data-js-changed]
)
),
:focus-within
) {
[data-el-cell][data-setup] {
[data-el-info-box] {
@apply hidden;
}

/* Make the primary actions visible for all cells */
[data-el-actions][data-primary] {
@apply opacity-100;
}
}
}

/* Outputs */
Expand Down Expand Up @@ -299,13 +330,11 @@ server in solely client-side operations.
}

&[data-js-hide-code] {
[data-el-cell]:is(
[data-type="code"],
[data-type="setup"],
[data-type="smart"]
):not([data-js-insert-mode]) {
[data-el-cell]:is([data-type="code"], [data-type="smart"]):not(
[data-js-insert-mode]
) {
[data-el-editor-box],
&[data-type="setup"] [data-el-info-box],
&[data-setup] [data-el-info-box],
&[data-type="smart"] [data-el-ui-box] {
@apply hidden;
}
Expand Down
9 changes: 5 additions & 4 deletions assets/js/hooks/cell.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,11 @@ const Cell = {

// Setup action handlers

if (["code", "smart"].includes(this.props.type)) {
const amplifyButton = this.el.querySelector(
`[data-el-amplify-outputs-button]`,
);
const amplifyButton = this.el.querySelector(
`[data-el-amplify-outputs-button]`,
);

if (amplifyButton) {
amplifyButton.addEventListener("click", (event) => {
this.el.toggleAttribute("data-js-amplified");
});
Expand Down
12 changes: 12 additions & 0 deletions assets/js/hooks/cell_editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,18 @@ const CellEditor = {
this.el.querySelector(`[data-el-editor-container]`).removeAttribute("id");
},

updated() {
const prevProps = this.props;
this.props = this.getProps();

if (
this.props.language !== prevProps.language ||
this.props.intellisense !== prevProps.intellisense
) {
this.liveEditor.setLanguage(this.props.language, this.props.intellisense);
}
},

destroyed() {
if (this.connection) {
this.connection.destroy();
Expand Down
60 changes: 40 additions & 20 deletions assets/js/hooks/cell_editor/live_editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
lineNumbers,
highlightActiveLineGutter,
} from "@codemirror/view";
import { EditorState, EditorSelection } from "@codemirror/state";
import { EditorState, EditorSelection, Compartment } from "@codemirror/state";
import {
indentOnInput,
bracketMatching,
Expand Down Expand Up @@ -236,6 +236,15 @@ export default class LiveEditor {
this.deltaSubscription.destroy();
}

setLanguage(language, intellisense) {
this.language = language;
this.intellisense = intellisense;

this.view.dispatch({
effects: this.languageCompartment.reconfigure(this.languageExtensions()),
});
}

/**
* Either adds or updates doctest indicators.
*/
Expand Down Expand Up @@ -322,13 +331,6 @@ export default class LiveEditor {
},
});

const lineWrappingEnabled =
this.language === "markdown" && settings.editor_markdown_word_wrap;

const language =
this.language &&
LanguageDescription.matchLanguageName(languages, this.language, false);

const customKeymap = [
{ key: "Escape", run: exitMulticursor },
{ key: "Alt-Enter", run: insertBlankLineAndCloseHints },
Expand All @@ -338,6 +340,8 @@ export default class LiveEditor {
this.handleViewUpdate(update),
);

this.languageCompartment = new Compartment();

this.view = new EditorView({
parent: this.container,
doc: this.source,
Expand Down Expand Up @@ -365,7 +369,6 @@ export default class LiveEditor {
keymap.of(vscodeKeymap),
EditorState.tabSize.of(2),
EditorState.lineSeparator.of("\n"),
lineWrappingEnabled ? EditorView.lineWrapping : [],
// We bind tab to actions within the editor, which would trap
// the user if they tabbed into the editor, so we remove it
// from the tab navigation
Expand All @@ -379,19 +382,9 @@ export default class LiveEditor {
activateOnTyping: settings.editor_auto_completion,
defaultKeymap: false,
}),
this.intellisense
? [
autocompletion({ override: [this.completionSource.bind(this)] }),
hoverDetails(this.docsHoverTooltipSource.bind(this)),
signature(this.signatureSource.bind(this), {
activateOnTyping: settings.editor_auto_signature,
}),
formatter(this.formatterSource.bind(this)),
]
: [],
settings.editor_mode === "vim" ? [vim()] : [],
settings.editor_mode === "emacs" ? [emacs()] : [],
language ? language.support : [],
this.languageCompartment.of(this.languageExtensions()),
EditorView.domEventHandlers({
click: this.handleEditorClick.bind(this),
keydown: this.handleEditorKeydown.bind(this),
Expand All @@ -404,6 +397,33 @@ export default class LiveEditor {
});
}

/** @private */
languageExtensions() {
const settings = settingsStore.get();

const lineWrappingEnabled =
this.language === "markdown" && settings.editor_markdown_word_wrap;

const language =
this.language &&
LanguageDescription.matchLanguageName(languages, this.language, false);

return [
lineWrappingEnabled ? EditorView.lineWrapping : [],
language ? language.support : [],
this.intellisense
? [
autocompletion({ override: [this.completionSource.bind(this)] }),
hoverDetails(this.docsHoverTooltipSource.bind(this)),
signature(this.signatureSource.bind(this), {
activateOnTyping: settings.editor_auto_signature,
}),
formatter(this.formatterSource.bind(this)),
]
: [],
];
}

/** @private */
handleEditorClick(event) {
const cmd = isMacOS() ? event.metaKey : event.ctrlKey;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { javascript } from "@codemirror/lang-javascript";
import { erlang } from "@codemirror/legacy-modes/mode/erlang";
import { dockerFile } from "@codemirror/legacy-modes/mode/dockerfile";
import { shell } from "@codemirror/legacy-modes/mode/shell";
import { toml } from "@codemirror/legacy-modes/mode/toml";
import { elixir } from "codemirror-lang-elixir";

export const elixirDesc = LanguageDescription.of({
Expand Down Expand Up @@ -77,6 +78,12 @@ const shellDesc = LanguageDescription.of({
support: new LanguageSupport(StreamLanguage.define(shell)),
});

const tomlDesc = LanguageDescription.of({
name: "TOML",
alias: ["pyproject.toml"],
support: new LanguageSupport(StreamLanguage.define(toml)),
});

const markdownDesc = LanguageDescription.of({
name: "Markdown",
support: markdown({
Expand All @@ -94,6 +101,7 @@ const markdownDesc = LanguageDescription.of({
javascriptDesc,
dockerfileDesc,
shellDesc,
tomlDesc,
],
}),
});
Expand All @@ -111,5 +119,6 @@ export const languages = [
javascriptDesc,
dockerfileDesc,
shellDesc,
tomlDesc,
markdownDesc,
];
4 changes: 2 additions & 2 deletions assets/js/lib/notebook.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
* Checks if the given cell type is eligible for evaluation.
*/
export function isEvaluable(cellType) {
return ["code", "smart", "setup"].includes(cellType);
return ["code", "smart"].includes(cellType);
}

/**
* Checks if the given cell type has primary editable editor.
*/
export function isDirectlyEditable(cellType) {
return ["markdown", "code", "setup"].includes(cellType);
return ["markdown", "code"].includes(cellType);
}
17 changes: 11 additions & 6 deletions lib/livebook/live_markdown/export.ex
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ defmodule Livebook.LiveMarkdown.Export do
end

defp render_notebook(notebook, ctx) do
%{setup_section: %{cells: [setup_cell]}} = notebook
%{setup_section: %{cells: setup_cells}} = notebook

comments =
Enum.map(notebook.leading_comments, fn
Expand All @@ -65,13 +65,13 @@ defmodule Livebook.LiveMarkdown.Export do
end)

name = ["# ", notebook.name]
setup_cell = render_setup_cell(setup_cell, %{ctx | include_outputs?: false})
setup_cells = render_setup_cells(setup_cells, %{ctx | include_outputs?: false})
sections = Enum.map(notebook.sections, &render_section(&1, notebook, ctx))

metadata = notebook_metadata(notebook)

notebook_with_metadata =
[name, setup_cell | sections]
[name | setup_cells ++ sections]
|> Enum.reject(&is_nil/1)
|> Enum.intersperse("\n\n")
|> prepend_metadata(metadata)
Expand Down Expand Up @@ -175,8 +175,13 @@ defmodule Livebook.LiveMarkdown.Export do
%{"branch_parent_index" => parent_idx}
end

defp render_setup_cell(%{source: ""}, _ctx), do: nil
defp render_setup_cell(cell, ctx), do: render_cell(cell, ctx)
defp render_setup_cells([%{source: ""}], _ctx), do: []

defp render_setup_cells(cells, ctx) do
Enum.map(cells, fn cell ->
render_cell(cell, ctx)
end)
end

defp render_cell(%Cell.Markdown{} = cell, _ctx) do
metadata = cell_metadata(cell)
Expand Down Expand Up @@ -322,7 +327,7 @@ defmodule Livebook.LiveMarkdown.Export do
defp add_markdown_annotation_before_elixir_block(ast) do
Enum.flat_map(ast, fn
{"pre", _, [{"code", [{"class", language}], [_source], %{}}], %{}} = ast_node
when language in ["elixir", "erlang"] ->
when language in ["elixir", "erlang", "python", "pyproject.toml"] ->
[{:comment, [], [~s/livebook:{"force_markdown":true}/], %{comment: true}}, ast_node]

ast_node ->
Expand Down
Loading

0 comments on commit 015b44f

Please sign in to comment.