diff --git a/Cargo.lock b/Cargo.lock index 31307a6848..86e023be39 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8274,6 +8274,7 @@ dependencies = [ "assistant", "editor", "gpui", + "repl", "search", "settings", "ui", diff --git a/assets/icons/repl_neutral.svg b/assets/icons/repl_neutral.svg new file mode 100644 index 0000000000..cb0c37d335 --- /dev/null +++ b/assets/icons/repl_neutral.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/assets/icons/repl_off.svg b/assets/icons/repl_off.svg new file mode 100644 index 0000000000..51ada0db46 --- /dev/null +++ b/assets/icons/repl_off.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/repl_pause.svg b/assets/icons/repl_pause.svg new file mode 100644 index 0000000000..2ac327df3b --- /dev/null +++ b/assets/icons/repl_pause.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/assets/icons/repl_play.svg b/assets/icons/repl_play.svg new file mode 100644 index 0000000000..d23b899112 --- /dev/null +++ b/assets/icons/repl_play.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/crates/quick_action_bar/Cargo.toml b/crates/quick_action_bar/Cargo.toml index 3d5d56a318..83dd3ae0f0 100644 --- a/crates/quick_action_bar/Cargo.toml +++ b/crates/quick_action_bar/Cargo.toml @@ -20,6 +20,7 @@ search.workspace = true settings.workspace = true ui.workspace = true workspace.workspace = true +repl.workspace = true [dev-dependencies] editor = { workspace = true, features = ["test-support"] } diff --git a/crates/quick_action_bar/src/quick_action_bar.rs b/crates/quick_action_bar/src/quick_action_bar.rs index 2edff6b5df..89e57ab812 100644 --- a/crates/quick_action_bar/src/quick_action_bar.rs +++ b/crates/quick_action_bar/src/quick_action_bar.rs @@ -20,8 +20,11 @@ use workspace::{ item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, }; +mod repl_menu; + pub struct QuickActionBar { buffer_search_bar: View, + repl_menu: Option>, toggle_settings_menu: Option>, toggle_selections_menu: Option>, active_item: Option>, @@ -40,6 +43,7 @@ impl QuickActionBar { buffer_search_bar, toggle_settings_menu: None, toggle_selections_menu: None, + repl_menu: None, active_item: None, _inlay_hints_enabled_subscription: None, workspace: workspace.weak_handle(), @@ -290,9 +294,13 @@ impl Render for QuickActionBar { .child( h_flex() .gap(Spacing::Medium.rems(cx)) + .children(self.render_repl_menu(cx)) .children(editor_selections_dropdown) .child(editor_settings_dropdown), ) + .when_some(self.repl_menu.as_ref(), |el, repl_menu| { + el.child(Self::render_menu_overlay(repl_menu)) + }) .when_some( self.toggle_settings_menu.as_ref(), |el, toggle_settings_menu| { diff --git a/crates/quick_action_bar/src/repl_menu.rs b/crates/quick_action_bar/src/repl_menu.rs new file mode 100644 index 0000000000..49027a617b --- /dev/null +++ b/crates/quick_action_bar/src/repl_menu.rs @@ -0,0 +1,116 @@ +use gpui::AnyElement; +use repl::{ + ExecutionState, JupyterSettings, Kernel, KernelSpecification, RuntimePanel, Session, + SessionSupport, +}; +use ui::{prelude::*, ButtonLike, IconWithIndicator, IntoElement, Tooltip}; + +use crate::QuickActionBar; + +const ZED_REPL_DOCUMENTATION: &str = "https://zed.dev/docs/repl"; + +impl QuickActionBar { + pub fn render_repl_menu(&self, cx: &mut ViewContext) -> Option { + if !JupyterSettings::enabled(cx) { + return None; + } + + let workspace = self.workspace.upgrade()?.read(cx); + + let (editor, repl_panel) = if let (Some(editor), Some(repl_panel)) = + (self.active_editor(), workspace.panel::(cx)) + { + (editor, repl_panel) + } else { + return None; + }; + + let session = repl_panel.update(cx, |repl_panel, cx| { + repl_panel.session(editor.downgrade(), cx) + }); + + let session = match session { + SessionSupport::ActiveSession(session) => session.read(cx), + SessionSupport::Inactive(spec) => { + return self.render_repl_launch_menu(spec, cx); + } + SessionSupport::RequiresSetup(language) => { + return self.render_repl_setup(&language, cx); + } + SessionSupport::Unsupported => return None, + }; + + let kernel_name: SharedString = session.kernel_specification.name.clone().into(); + let kernel_language: SharedString = session + .kernel_specification + .kernelspec + .language + .clone() + .into(); + + let tooltip = |session: &Session| match &session.kernel { + Kernel::RunningKernel(kernel) => match &kernel.execution_state { + ExecutionState::Idle => { + format!("Run code on {} ({})", kernel_name, kernel_language) + } + ExecutionState::Busy => format!("Interrupt {} ({})", kernel_name, kernel_language), + }, + Kernel::StartingKernel(_) => format!("{} is starting", kernel_name), + Kernel::ErroredLaunch(e) => format!("Error with kernel {}: {}", kernel_name, e), + Kernel::ShuttingDown => format!("{} is shutting down", kernel_name), + Kernel::Shutdown => "Nothing running".to_string(), + }; + + let tooltip_text: SharedString = SharedString::from(tooltip(&session).clone()); + + let button = ButtonLike::new("toggle_repl_icon") + .child( + IconWithIndicator::new(Icon::new(IconName::Play), Some(session.kernel.dot())) + .indicator_border_color(Some(cx.theme().colors().border)), + ) + .size(ButtonSize::Compact) + .style(ButtonStyle::Subtle) + .tooltip(move |cx| Tooltip::text(tooltip_text.clone(), cx)) + .on_click(|_, cx| cx.dispatch_action(Box::new(repl::Run {}))) + .on_click(|_, cx| cx.dispatch_action(Box::new(repl::Run {}))) + .into_any_element(); + + Some(button) + } + + pub fn render_repl_launch_menu( + &self, + kernel_specification: KernelSpecification, + _cx: &mut ViewContext, + ) -> Option { + let tooltip: SharedString = + SharedString::from(format!("Start REPL for {}", kernel_specification.name)); + + Some( + IconButton::new("toggle_repl_icon", IconName::Play) + .size(ButtonSize::Compact) + .icon_color(Color::Muted) + .style(ButtonStyle::Subtle) + .tooltip(move |cx| Tooltip::text(tooltip.clone(), cx)) + .on_click(|_, cx| cx.dispatch_action(Box::new(repl::Run {}))) + .into_any_element(), + ) + } + + pub fn render_repl_setup( + &self, + language: &str, + _cx: &mut ViewContext, + ) -> Option { + let tooltip: SharedString = SharedString::from(format!("Setup Zed REPL for {}", language)); + Some( + IconButton::new("toggle_repl_icon", IconName::Play) + .size(ButtonSize::Compact) + .icon_color(Color::Muted) + .style(ButtonStyle::Subtle) + .tooltip(move |cx| Tooltip::text(tooltip.clone(), cx)) + .on_click(|_, cx| cx.open_url(ZED_REPL_DOCUMENTATION)) + .into_any_element(), + ) + } +} diff --git a/crates/repl/src/kernels.rs b/crates/repl/src/kernels.rs index 87f75fbc38..c91e4f5247 100644 --- a/crates/repl/src/kernels.rs +++ b/crates/repl/src/kernels.rs @@ -82,7 +82,7 @@ pub enum Kernel { } impl Kernel { - pub fn dot(&mut self) -> Indicator { + pub fn dot(&self) -> Indicator { match self { Kernel::RunningKernel(kernel) => match kernel.execution_state { ExecutionState::Idle => Indicator::dot().color(Color::Success), diff --git a/crates/repl/src/repl.rs b/crates/repl/src/repl.rs index 0c402cfc48..3ad8a7a715 100644 --- a/crates/repl/src/repl.rs +++ b/crates/repl/src/repl.rs @@ -11,7 +11,11 @@ mod session; mod stdio; pub use jupyter_settings::JupyterSettings; -pub use runtime_panel::RuntimePanel; +pub use kernels::{Kernel, KernelSpecification}; +pub use runtime_panel::Run; +pub use runtime_panel::{RuntimePanel, SessionSupport}; +pub use runtimelib::ExecutionState; +pub use session::Session; fn zed_dispatcher(cx: &mut AppContext) -> impl Dispatcher { struct ZedDispatcher { diff --git a/crates/repl/src/runtime_panel.rs b/crates/repl/src/runtime_panel.rs index fc09f06329..4662153b1b 100644 --- a/crates/repl/src/runtime_panel.rs +++ b/crates/repl/src/runtime_panel.rs @@ -241,6 +241,17 @@ impl RuntimePanel { Some((selected_text, language_name, anchor_range)) } + pub fn language( + &self, + editor: WeakView, + cx: &mut ViewContext, + ) -> Option> { + match self.snippet(editor, cx) { + Some((_, language, _)) => Some(language), + None => None, + } + } + pub fn refresh_kernelspecs(&mut self, cx: &mut ViewContext) -> Task> { let kernel_specifications = kernel_specifications(self.fs.clone()); cx.spawn(|this, mut cx| async move { @@ -336,6 +347,50 @@ impl RuntimePanel { } } +pub enum SessionSupport { + ActiveSession(View), + Inactive(KernelSpecification), + RequiresSetup(String), + Unsupported, +} + +impl RuntimePanel { + pub fn session( + &mut self, + editor: WeakView, + cx: &mut ViewContext, + ) -> SessionSupport { + let entity_id = editor.entity_id(); + let session = self.sessions.get(&entity_id).cloned(); + + match session { + Some(session) => SessionSupport::ActiveSession(session), + None => { + let language = self.language(editor, cx); + let language = match language { + Some(language) => language, + None => return SessionSupport::Unsupported, + }; + // Check for kernelspec + let kernelspec = self.kernelspec(&language, cx); + + match kernelspec { + Some(kernelspec) => SessionSupport::Inactive(kernelspec), + None => { + let language: String = language.to_lowercase(); + // If no kernelspec but language is one of typescript, python, r, or julia + // then we return RequiresSetup + match language.as_str() { + "typescript" | "python" => SessionSupport::RequiresSetup(language), + _ => SessionSupport::Unsupported, + } + } + } + } + } + } +} + impl Panel for RuntimePanel { fn persistent_name() -> &'static str { "RuntimePanel" diff --git a/crates/repl/src/session.rs b/crates/repl/src/session.rs index bab69f6584..4db0c73d0b 100644 --- a/crates/repl/src/session.rs +++ b/crates/repl/src/session.rs @@ -22,11 +22,11 @@ use theme::{ActiveTheme, ThemeSettings}; use ui::{h_flex, prelude::*, v_flex, ButtonLike, ButtonStyle, Label}; pub struct Session { - editor: WeakView, - kernel: Kernel, + pub editor: WeakView, + pub kernel: Kernel, blocks: HashMap, - messaging_task: Task<()>, - kernel_specification: KernelSpecification, + pub messaging_task: Task<()>, + pub kernel_specification: KernelSpecification, } struct EditorBlock { @@ -310,7 +310,7 @@ impl Session { } } - fn interrupt(&mut self, cx: &mut ViewContext) { + pub fn interrupt(&mut self, cx: &mut ViewContext) { match &mut self.kernel { Kernel::RunningKernel(_kernel) => { self.send(InterruptRequest {}.into(), cx).ok(); @@ -322,7 +322,7 @@ impl Session { } } - fn shutdown(&mut self, cx: &mut ViewContext) { + pub fn shutdown(&mut self, cx: &mut ViewContext) { let kernel = std::mem::replace(&mut self.kernel, Kernel::ShuttingDown); match kernel { diff --git a/crates/ui/src/components/icon.rs b/crates/ui/src/components/icon.rs index e8c4ad31ee..fde5523147 100644 --- a/crates/ui/src/components/icon.rs +++ b/crates/ui/src/components/icon.rs @@ -160,11 +160,11 @@ pub enum IconName { Font, FontSize, FontWeight, - Github, - GenericMinimize, - GenericMaximize, GenericClose, + GenericMaximize, + GenericMinimize, GenericRestore, + Github, Hash, HistoryRerun, Indicator, @@ -194,6 +194,10 @@ pub enum IconName { PullRequest, Quote, Regex, + ReplPlay, + ReplOff, + ReplPause, + ReplNeutral, Replace, ReplaceAll, ReplaceNext, @@ -231,12 +235,12 @@ pub enum IconName { Trash, TriangleRight, Update, + Visible, WholeWord, XCircle, ZedAssistant, ZedAssistantFilled, ZedXCopilot, - Visible, } impl IconName { @@ -308,11 +312,11 @@ impl IconName { IconName::Font => "icons/font.svg", IconName::FontSize => "icons/font_size.svg", IconName::FontWeight => "icons/font_weight.svg", - IconName::Github => "icons/github.svg", - IconName::GenericMinimize => "icons/generic_minimize.svg", - IconName::GenericMaximize => "icons/generic_maximize.svg", IconName::GenericClose => "icons/generic_close.svg", + IconName::GenericMaximize => "icons/generic_maximize.svg", + IconName::GenericMinimize => "icons/generic_minimize.svg", IconName::GenericRestore => "icons/generic_restore.svg", + IconName::Github => "icons/github.svg", IconName::Hash => "icons/hash.svg", IconName::HistoryRerun => "icons/history_rerun.svg", IconName::Indicator => "icons/indicator.svg", @@ -342,6 +346,10 @@ impl IconName { IconName::PullRequest => "icons/pull_request.svg", IconName::Quote => "icons/quote.svg", IconName::Regex => "icons/regex.svg", + IconName::ReplPlay => "icons/repl_play.svg", + IconName::ReplPause => "icons/repl_pause.svg", + IconName::ReplNeutral => "icons/repl_neutral.svg", + IconName::ReplOff => "icons/repl_off.svg", IconName::Replace => "icons/replace.svg", IconName::ReplaceAll => "icons/replace_all.svg", IconName::ReplaceNext => "icons/replace_next.svg", @@ -379,12 +387,12 @@ impl IconName { IconName::Trash => "icons/trash.svg", IconName::TriangleRight => "icons/triangle_right.svg", IconName::Update => "icons/update.svg", + IconName::Visible => "icons/visible.svg", IconName::WholeWord => "icons/word_search.svg", IconName::XCircle => "icons/error.svg", IconName::ZedAssistant => "icons/zed_assistant.svg", IconName::ZedAssistantFilled => "icons/zed_assistant_filled.svg", IconName::ZedXCopilot => "icons/zed_x_copilot.svg", - IconName::Visible => "icons/visible.svg", } } } diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 91fc796d8c..48c2a589b8 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -22,6 +22,7 @@ - [Collaboration](./collaboration.md) - [Tasks](./tasks.md) - [Remote Development](./remote-development.md) +- [Repl](./repl.md) # Language Support diff --git a/docs/src/repl.md b/docs/src/repl.md new file mode 100644 index 0000000000..e7fdc0c035 --- /dev/null +++ b/docs/src/repl.md @@ -0,0 +1,72 @@ +# REPL + +Read. Eval. Print. Loop. + +
+ +This feature is in active development. Details may change. We're delighted to get feedback as the REPL feature evolves. + +
+ + +The built-in REPL for Zed allows you to run code interactively in your editor similarly to a notebook with your own text files. + + + +To start using the REPL, add the following to your Zed `settings.json` to bring the power of [Jupyter kernels](https://docs.jupyter.org/en/latest/projects/kernels.html) to your editor: + +```json +{ + "jupyter": { + "enabled": true + } +} +``` + +After that, install any of the supported kernels: + +* [Python](#python) +* [TypeScript via Deno](#deno) + +## Python + +### Global environment + +To setup your current python to have an available kernel, run: + +``` +python -m ipykernel install --user +``` + +### Conda Environment + +``` +source activate myenv +conda install ipykernel +python -m ipykernel install --user --name myenv --display-name "Python (myenv)" +``` + + +### Virtualenv with pip + +``` +source activate myenv +pip install ipykernel +python -m ipykernel install --user --name myenv --display-name "Python (myenv)" +``` + +## Deno + +[Install Deno](https://docs.deno.com/runtime/manual/getting_started/installation/) and then install the Deno jupyter kernel: + +``` +deno jupyter --unstable --install +``` + +## Other languages + +* [Julia](https://github.com/JuliaLang/IJulia.jl) +* R + - [Ark Kernel from Positron, formerly RStudio](https://github.com/posit-dev/ark) + - [Xeus-R](https://github.com/jupyter-xeus/xeus-r) +* [Scala](https://almond.sh/docs/quick-start-install)