repl: Replace REPL panel with sessions view (#14981)
This PR removes the REPL panel and replaces it with a new sessions view that gets displayed in its own pane. The sessions view can be opened with the `repl: sessions` command (we can adjust the name, as needed). There was a rather in-depth refactoring needed to extricate the various REPL functionality on the editor from the `RuntimePanel`. <img width="1136" alt="Screenshot 2024-07-22 at 4 12 12 PM" src="https://github.com/user-attachments/assets/ac0da351-778e-4200-b08c-39f9e77d78bf"> <img width="1136" alt="Screenshot 2024-07-22 at 4 12 17 PM" src="https://github.com/user-attachments/assets/6ca53476-6ac4-4f8b-afc8-f7863f7065c7"> Release Notes: - N/A
This commit is contained in:
parent
8f20ea1093
commit
d8a42bbf63
9 changed files with 474 additions and 637 deletions
|
@ -2,8 +2,8 @@ use std::time::Duration;
|
||||||
|
|
||||||
use gpui::{percentage, Animation, AnimationExt, AnyElement, Transformation, View};
|
use gpui::{percentage, Animation, AnimationExt, AnyElement, Transformation, View};
|
||||||
use repl::{
|
use repl::{
|
||||||
ExecutionState, JupyterSettings, Kernel, KernelSpecification, KernelStatus, RuntimePanel,
|
ExecutionState, JupyterSettings, Kernel, KernelSpecification, KernelStatus, Session,
|
||||||
Session, SessionSupport,
|
SessionSupport,
|
||||||
};
|
};
|
||||||
use ui::{
|
use ui::{
|
||||||
prelude::*, ButtonLike, ContextMenu, IconWithIndicator, Indicator, IntoElement, PopoverMenu,
|
prelude::*, ButtonLike, ContextMenu, IconWithIndicator, Indicator, IntoElement, PopoverMenu,
|
||||||
|
@ -39,15 +39,7 @@ impl QuickActionBar {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let workspace = self.workspace.upgrade()?.read(cx);
|
let editor = self.active_editor()?;
|
||||||
|
|
||||||
let (editor, repl_panel) = if let (Some(editor), Some(repl_panel)) =
|
|
||||||
(self.active_editor(), workspace.panel::<RuntimePanel>(cx))
|
|
||||||
{
|
|
||||||
(editor, repl_panel)
|
|
||||||
} else {
|
|
||||||
return None;
|
|
||||||
};
|
|
||||||
|
|
||||||
let has_nonempty_selection = {
|
let has_nonempty_selection = {
|
||||||
editor.update(cx, |this, cx| {
|
editor.update(cx, |this, cx| {
|
||||||
|
@ -62,10 +54,7 @@ impl QuickActionBar {
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
let session = repl_panel.update(cx, |repl_panel, cx| {
|
let session = repl::session(editor.downgrade(), cx);
|
||||||
repl_panel.session(editor.downgrade(), cx)
|
|
||||||
});
|
|
||||||
|
|
||||||
let session = match session {
|
let session = match session {
|
||||||
SessionSupport::ActiveSession(session) => session,
|
SessionSupport::ActiveSession(session) => session,
|
||||||
SessionSupport::Inactive(spec) => {
|
SessionSupport::Inactive(spec) => {
|
||||||
|
@ -84,18 +73,15 @@ impl QuickActionBar {
|
||||||
|
|
||||||
let element_id = |suffix| ElementId::Name(format!("{}-{}", id, suffix).into());
|
let element_id = |suffix| ElementId::Name(format!("{}-{}", id, suffix).into());
|
||||||
|
|
||||||
let panel_clone = repl_panel.clone();
|
let editor = editor.downgrade();
|
||||||
let editor_clone = editor.downgrade();
|
|
||||||
let dropdown_menu = PopoverMenu::new(element_id("menu"))
|
let dropdown_menu = PopoverMenu::new(element_id("menu"))
|
||||||
.menu(move |cx| {
|
.menu(move |cx| {
|
||||||
let panel_clone = panel_clone.clone();
|
let editor = editor.clone();
|
||||||
let editor_clone = editor_clone.clone();
|
|
||||||
let session = session.clone();
|
let session = session.clone();
|
||||||
ContextMenu::build(cx, move |menu, cx| {
|
ContextMenu::build(cx, move |menu, cx| {
|
||||||
let menu_state = session_state(session, cx);
|
let menu_state = session_state(session, cx);
|
||||||
let status = menu_state.status;
|
let status = menu_state.status;
|
||||||
let editor_clone = editor_clone.clone();
|
let editor = editor.clone();
|
||||||
let panel_clone = panel_clone.clone();
|
|
||||||
|
|
||||||
menu.when_else(
|
menu.when_else(
|
||||||
status.is_connected(),
|
status.is_connected(),
|
||||||
|
@ -139,7 +125,6 @@ impl QuickActionBar {
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.separator()
|
.separator()
|
||||||
// Run
|
|
||||||
.custom_entry(
|
.custom_entry(
|
||||||
move |_cx| {
|
move |_cx| {
|
||||||
Label::new(if has_nonempty_selection {
|
Label::new(if has_nonempty_selection {
|
||||||
|
@ -150,17 +135,12 @@ impl QuickActionBar {
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
let panel_clone = panel_clone.clone();
|
let editor = editor.clone();
|
||||||
let editor_clone = editor_clone.clone();
|
|
||||||
move |cx| {
|
move |cx| {
|
||||||
let editor_clone = editor_clone.clone();
|
repl::run(editor.clone(), cx).log_err();
|
||||||
panel_clone.update(cx, |this, cx| {
|
|
||||||
this.run(editor_clone.clone(), cx).log_err();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
// Interrupt
|
|
||||||
.custom_entry(
|
.custom_entry(
|
||||||
move |_cx| {
|
move |_cx| {
|
||||||
Label::new("Interrupt")
|
Label::new("Interrupt")
|
||||||
|
@ -169,17 +149,12 @@ impl QuickActionBar {
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
let panel_clone = panel_clone.clone();
|
let editor = editor.clone();
|
||||||
let editor_clone = editor_clone.clone();
|
|
||||||
move |cx| {
|
move |cx| {
|
||||||
let editor_clone = editor_clone.clone();
|
repl::interrupt(editor.clone(), cx);
|
||||||
panel_clone.update(cx, |this, cx| {
|
|
||||||
this.interrupt(editor_clone, cx);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
// Clear Outputs
|
|
||||||
.custom_entry(
|
.custom_entry(
|
||||||
move |_cx| {
|
move |_cx| {
|
||||||
Label::new("Clear Outputs")
|
Label::new("Clear Outputs")
|
||||||
|
@ -188,13 +163,9 @@ impl QuickActionBar {
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
let panel_clone = panel_clone.clone();
|
let editor = editor.clone();
|
||||||
let editor_clone = editor_clone.clone();
|
|
||||||
move |cx| {
|
move |cx| {
|
||||||
let editor_clone = editor_clone.clone();
|
repl::clear_outputs(editor.clone(), cx);
|
||||||
panel_clone.update(cx, |this, cx| {
|
|
||||||
this.clear_outputs(editor_clone, cx);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -207,7 +178,6 @@ impl QuickActionBar {
|
||||||
)
|
)
|
||||||
// TODO: Add Restart action
|
// TODO: Add Restart action
|
||||||
// .action("Restart", Box::new(gpui::NoAction))
|
// .action("Restart", Box::new(gpui::NoAction))
|
||||||
// Shut down kernel
|
|
||||||
.custom_entry(
|
.custom_entry(
|
||||||
move |_cx| {
|
move |_cx| {
|
||||||
Label::new("Shut Down Kernel")
|
Label::new("Shut Down Kernel")
|
||||||
|
@ -216,13 +186,9 @@ impl QuickActionBar {
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
let panel_clone = panel_clone.clone();
|
let editor = editor.clone();
|
||||||
let editor_clone = editor_clone.clone();
|
|
||||||
move |cx| {
|
move |cx| {
|
||||||
let editor_clone = editor_clone.clone();
|
repl::shutdown(editor.clone(), cx);
|
||||||
panel_clone.update(cx, |this, cx| {
|
|
||||||
this.shutdown(editor_clone, cx);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
@ -5,21 +5,9 @@ use gpui::AppContext;
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use settings::{Settings, SettingsSources};
|
use settings::{Settings, SettingsSources};
|
||||||
use ui::Pixels;
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
|
||||||
#[serde(rename_all = "snake_case")]
|
|
||||||
pub enum JupyterDockPosition {
|
|
||||||
Left,
|
|
||||||
#[default]
|
|
||||||
Right,
|
|
||||||
Bottom,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct JupyterSettings {
|
pub struct JupyterSettings {
|
||||||
pub dock: JupyterDockPosition,
|
|
||||||
pub default_width: Pixels,
|
|
||||||
pub kernel_selections: HashMap<String, String>,
|
pub kernel_selections: HashMap<String, String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,31 +22,15 @@ impl JupyterSettings {
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
|
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
|
||||||
pub struct JupyterSettingsContent {
|
pub struct JupyterSettingsContent {
|
||||||
/// Where to dock the Jupyter panel.
|
|
||||||
///
|
|
||||||
/// Default: `right`
|
|
||||||
dock: Option<JupyterDockPosition>,
|
|
||||||
/// Default width in pixels when the jupyter panel is docked to the left or right.
|
|
||||||
///
|
|
||||||
/// Default: 640
|
|
||||||
pub default_width: Option<f32>,
|
|
||||||
/// Default kernels to select for each language.
|
/// Default kernels to select for each language.
|
||||||
///
|
///
|
||||||
/// Default: `{}`
|
/// Default: `{}`
|
||||||
pub kernel_selections: Option<HashMap<String, String>>,
|
pub kernel_selections: Option<HashMap<String, String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl JupyterSettingsContent {
|
|
||||||
pub fn set_dock(&mut self, dock: JupyterDockPosition) {
|
|
||||||
self.dock = Some(dock);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for JupyterSettingsContent {
|
impl Default for JupyterSettingsContent {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
JupyterSettingsContent {
|
JupyterSettingsContent {
|
||||||
dock: Some(JupyterDockPosition::Right),
|
|
||||||
default_width: Some(640.0),
|
|
||||||
kernel_selections: Some(HashMap::new()),
|
kernel_selections: Some(HashMap::new()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -79,14 +51,6 @@ impl Settings for JupyterSettings {
|
||||||
let mut settings = JupyterSettings::default();
|
let mut settings = JupyterSettings::default();
|
||||||
|
|
||||||
for value in sources.defaults_and_customizations() {
|
for value in sources.defaults_and_customizations() {
|
||||||
if let Some(dock) = value.dock {
|
|
||||||
settings.dock = dock;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(default_width) = value.default_width {
|
|
||||||
settings.default_width = Pixels::from(default_width);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(source) = &value.kernel_selections {
|
if let Some(source) = &value.kernel_selections {
|
||||||
for (k, v) in source {
|
for (k, v) in source {
|
||||||
settings.kernel_selections.insert(k.clone(), v.clone());
|
settings.kernel_selections.insert(k.clone(), v.clone());
|
||||||
|
@ -114,14 +78,6 @@ mod tests {
|
||||||
JupyterSettings::register(cx);
|
JupyterSettings::register(cx);
|
||||||
|
|
||||||
assert_eq!(JupyterSettings::enabled(cx), false);
|
assert_eq!(JupyterSettings::enabled(cx), false);
|
||||||
assert_eq!(
|
|
||||||
JupyterSettings::get_global(cx).dock,
|
|
||||||
JupyterDockPosition::Right
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
JupyterSettings::get_global(cx).default_width,
|
|
||||||
Pixels::from(640.0)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Setting a custom setting through user settings
|
// Setting a custom setting through user settings
|
||||||
SettingsStore::update_global(cx, |store, cx| {
|
SettingsStore::update_global(cx, |store, cx| {
|
||||||
|
@ -140,13 +96,5 @@ mod tests {
|
||||||
});
|
});
|
||||||
|
|
||||||
assert_eq!(JupyterSettings::enabled(cx), true);
|
assert_eq!(JupyterSettings::enabled(cx), true);
|
||||||
assert_eq!(
|
|
||||||
JupyterSettings::get_global(cx).dock,
|
|
||||||
JupyterDockPosition::Left
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
JupyterSettings::get_global(cx).default_width,
|
|
||||||
Pixels::from(800.0)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,15 +7,16 @@ use std::{sync::Arc, time::Duration};
|
||||||
mod jupyter_settings;
|
mod jupyter_settings;
|
||||||
mod kernels;
|
mod kernels;
|
||||||
mod outputs;
|
mod outputs;
|
||||||
|
mod repl_editor;
|
||||||
|
mod repl_sessions_ui;
|
||||||
mod repl_store;
|
mod repl_store;
|
||||||
mod runtime_panel;
|
|
||||||
mod session;
|
mod session;
|
||||||
mod stdio;
|
mod stdio;
|
||||||
|
|
||||||
pub use jupyter_settings::JupyterSettings;
|
pub use jupyter_settings::JupyterSettings;
|
||||||
pub use kernels::{Kernel, KernelSpecification, KernelStatus};
|
pub use kernels::{Kernel, KernelSpecification, KernelStatus};
|
||||||
pub use runtime_panel::{ClearOutputs, Interrupt, Run, Shutdown};
|
pub use repl_editor::*;
|
||||||
pub use runtime_panel::{RuntimePanel, SessionSupport};
|
pub use repl_sessions_ui::{ClearOutputs, Interrupt, ReplSessionsPage, Run, Shutdown};
|
||||||
pub use runtimelib::ExecutionState;
|
pub use runtimelib::ExecutionState;
|
||||||
pub use session::Session;
|
pub use session::Session;
|
||||||
|
|
||||||
|
@ -48,7 +49,7 @@ fn zed_dispatcher(cx: &mut AppContext) -> impl Dispatcher {
|
||||||
pub fn init(fs: Arc<dyn Fs>, cx: &mut AppContext) {
|
pub fn init(fs: Arc<dyn Fs>, cx: &mut AppContext) {
|
||||||
set_dispatcher(zed_dispatcher(cx));
|
set_dispatcher(zed_dispatcher(cx));
|
||||||
JupyterSettings::register(cx);
|
JupyterSettings::register(cx);
|
||||||
editor::init_settings(cx);
|
::editor::init_settings(cx);
|
||||||
runtime_panel::init(cx);
|
repl_sessions_ui::init(cx);
|
||||||
ReplStore::init(fs, cx);
|
ReplStore::init(fs, cx);
|
||||||
}
|
}
|
||||||
|
|
185
crates/repl/src/repl_editor.rs
Normal file
185
crates/repl/src/repl_editor.rs
Normal file
|
@ -0,0 +1,185 @@
|
||||||
|
//! REPL operations on an [`Editor`].
|
||||||
|
|
||||||
|
use std::ops::Range;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use editor::{Anchor, Editor, RangeToAnchorExt};
|
||||||
|
use gpui::{prelude::*, AppContext, View, WeakView, WindowContext};
|
||||||
|
use language::{Language, Point};
|
||||||
|
use multi_buffer::MultiBufferRow;
|
||||||
|
|
||||||
|
use crate::repl_store::ReplStore;
|
||||||
|
use crate::session::SessionEvent;
|
||||||
|
use crate::{KernelSpecification, Session};
|
||||||
|
|
||||||
|
pub fn run(editor: WeakView<Editor>, cx: &mut WindowContext) -> Result<()> {
|
||||||
|
let store = ReplStore::global(cx);
|
||||||
|
if !store.read(cx).is_enabled() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let (selected_text, language, anchor_range) = match snippet(editor.clone(), cx) {
|
||||||
|
Some(snippet) => snippet,
|
||||||
|
None => return Ok(()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let entity_id = editor.entity_id();
|
||||||
|
|
||||||
|
let kernel_specification = store.update(cx, |store, cx| {
|
||||||
|
store
|
||||||
|
.kernelspec(&language, cx)
|
||||||
|
.with_context(|| format!("No kernel found for language: {}", language.name()))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let fs = store.read(cx).fs().clone();
|
||||||
|
let session = if let Some(session) = store.read(cx).get_session(entity_id).cloned() {
|
||||||
|
session
|
||||||
|
} else {
|
||||||
|
let session = cx.new_view(|cx| Session::new(editor.clone(), fs, kernel_specification, cx));
|
||||||
|
|
||||||
|
editor.update(cx, |_editor, cx| {
|
||||||
|
cx.notify();
|
||||||
|
|
||||||
|
cx.subscribe(&session, {
|
||||||
|
let store = store.clone();
|
||||||
|
move |_this, _session, event, cx| match event {
|
||||||
|
SessionEvent::Shutdown(shutdown_event) => {
|
||||||
|
store.update(cx, |store, _cx| {
|
||||||
|
store.remove_session(shutdown_event.entity_id());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
})?;
|
||||||
|
|
||||||
|
store.update(cx, |store, _cx| {
|
||||||
|
store.insert_session(entity_id, session.clone());
|
||||||
|
});
|
||||||
|
|
||||||
|
session
|
||||||
|
};
|
||||||
|
|
||||||
|
session.update(cx, |session, cx| {
|
||||||
|
session.execute(&selected_text, anchor_range, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
anyhow::Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum SessionSupport {
|
||||||
|
ActiveSession(View<Session>),
|
||||||
|
Inactive(Box<KernelSpecification>),
|
||||||
|
RequiresSetup(Arc<str>),
|
||||||
|
Unsupported,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn session(editor: WeakView<Editor>, cx: &mut AppContext) -> SessionSupport {
|
||||||
|
let store = ReplStore::global(cx);
|
||||||
|
let entity_id = editor.entity_id();
|
||||||
|
|
||||||
|
if let Some(session) = store.read(cx).get_session(entity_id).cloned() {
|
||||||
|
return SessionSupport::ActiveSession(session);
|
||||||
|
};
|
||||||
|
|
||||||
|
let language = get_language(editor, cx);
|
||||||
|
let language = match language {
|
||||||
|
Some(language) => language,
|
||||||
|
None => return SessionSupport::Unsupported,
|
||||||
|
};
|
||||||
|
let kernelspec = store.update(cx, |store, cx| store.kernelspec(&language, cx));
|
||||||
|
|
||||||
|
match kernelspec {
|
||||||
|
Some(kernelspec) => SessionSupport::Inactive(Box::new(kernelspec)),
|
||||||
|
None => match language.name().as_ref() {
|
||||||
|
"TypeScript" | "Python" => SessionSupport::RequiresSetup(language.name()),
|
||||||
|
_ => SessionSupport::Unsupported,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_outputs(editor: WeakView<Editor>, cx: &mut WindowContext) {
|
||||||
|
let store = ReplStore::global(cx);
|
||||||
|
let entity_id = editor.entity_id();
|
||||||
|
if let Some(session) = store.read(cx).get_session(entity_id).cloned() {
|
||||||
|
session.update(cx, |session, cx| {
|
||||||
|
session.clear_outputs(cx);
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn interrupt(editor: WeakView<Editor>, cx: &mut WindowContext) {
|
||||||
|
let store = ReplStore::global(cx);
|
||||||
|
let entity_id = editor.entity_id();
|
||||||
|
if let Some(session) = store.read(cx).get_session(entity_id).cloned() {
|
||||||
|
session.update(cx, |session, cx| {
|
||||||
|
session.interrupt(cx);
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn shutdown(editor: WeakView<Editor>, cx: &mut WindowContext) {
|
||||||
|
let store = ReplStore::global(cx);
|
||||||
|
let entity_id = editor.entity_id();
|
||||||
|
if let Some(session) = store.read(cx).get_session(entity_id).cloned() {
|
||||||
|
session.update(cx, |session, cx| {
|
||||||
|
session.shutdown(cx);
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn snippet(
|
||||||
|
editor: WeakView<Editor>,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) -> Option<(String, Arc<Language>, Range<Anchor>)> {
|
||||||
|
let editor = editor.upgrade()?;
|
||||||
|
let editor = editor.read(cx);
|
||||||
|
|
||||||
|
let buffer = editor.buffer().read(cx).snapshot(cx);
|
||||||
|
|
||||||
|
let selection = editor.selections.newest::<usize>(cx);
|
||||||
|
let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||||
|
|
||||||
|
let range = if selection.is_empty() {
|
||||||
|
let cursor = selection.head();
|
||||||
|
|
||||||
|
let cursor_row = multi_buffer_snapshot.offset_to_point(cursor).row;
|
||||||
|
let start_offset = multi_buffer_snapshot.point_to_offset(Point::new(cursor_row, 0));
|
||||||
|
|
||||||
|
let end_point = Point::new(
|
||||||
|
cursor_row,
|
||||||
|
multi_buffer_snapshot.line_len(MultiBufferRow(cursor_row)),
|
||||||
|
);
|
||||||
|
let end_offset = start_offset.saturating_add(end_point.column as usize);
|
||||||
|
|
||||||
|
// Create a range from the start to the end of the line
|
||||||
|
start_offset..end_offset
|
||||||
|
} else {
|
||||||
|
selection.range()
|
||||||
|
};
|
||||||
|
|
||||||
|
let anchor_range = range.to_anchors(&multi_buffer_snapshot);
|
||||||
|
|
||||||
|
let selected_text = buffer
|
||||||
|
.text_for_range(anchor_range.clone())
|
||||||
|
.collect::<String>();
|
||||||
|
|
||||||
|
let start_language = buffer.language_at(anchor_range.start)?;
|
||||||
|
let end_language = buffer.language_at(anchor_range.end)?;
|
||||||
|
if start_language != end_language {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some((selected_text, start_language.clone(), anchor_range))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_language(editor: WeakView<Editor>, cx: &mut AppContext) -> Option<Arc<Language>> {
|
||||||
|
let editor = editor.upgrade()?;
|
||||||
|
let selection = editor.read(cx).selections.newest::<usize>(cx);
|
||||||
|
let buffer = editor.read(cx).buffer().read(cx).snapshot(cx);
|
||||||
|
buffer.language_at(selection.head()).cloned()
|
||||||
|
}
|
257
crates/repl/src/repl_sessions_ui.rs
Normal file
257
crates/repl/src/repl_sessions_ui.rs
Normal file
|
@ -0,0 +1,257 @@
|
||||||
|
use editor::Editor;
|
||||||
|
use gpui::{
|
||||||
|
actions, prelude::*, AppContext, EventEmitter, FocusHandle, FocusableView, Subscription, View,
|
||||||
|
};
|
||||||
|
use ui::{prelude::*, ButtonLike, ElevationIndex, KeyBinding};
|
||||||
|
use util::ResultExt as _;
|
||||||
|
use workspace::item::ItemEvent;
|
||||||
|
use workspace::WorkspaceId;
|
||||||
|
use workspace::{item::Item, Workspace};
|
||||||
|
|
||||||
|
use crate::jupyter_settings::JupyterSettings;
|
||||||
|
use crate::repl_store::ReplStore;
|
||||||
|
|
||||||
|
actions!(
|
||||||
|
repl,
|
||||||
|
[
|
||||||
|
Run,
|
||||||
|
ClearOutputs,
|
||||||
|
Sessions,
|
||||||
|
Interrupt,
|
||||||
|
Shutdown,
|
||||||
|
RefreshKernelspecs
|
||||||
|
]
|
||||||
|
);
|
||||||
|
actions!(repl_panel, [ToggleFocus]);
|
||||||
|
|
||||||
|
pub fn init(cx: &mut AppContext) {
|
||||||
|
cx.observe_new_views(
|
||||||
|
|workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
|
||||||
|
workspace.register_action(|workspace, _: &Sessions, cx| {
|
||||||
|
let existing = workspace
|
||||||
|
.active_pane()
|
||||||
|
.read(cx)
|
||||||
|
.items()
|
||||||
|
.find_map(|item| item.downcast::<ReplSessionsPage>());
|
||||||
|
|
||||||
|
if let Some(existing) = existing {
|
||||||
|
workspace.activate_item(&existing, true, true, cx);
|
||||||
|
} else {
|
||||||
|
let extensions_page = ReplSessionsPage::new(cx);
|
||||||
|
workspace.add_item_to_active_pane(Box::new(extensions_page), None, true, cx)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
workspace.register_action(|_workspace, _: &RefreshKernelspecs, cx| {
|
||||||
|
let store = ReplStore::global(cx);
|
||||||
|
store.update(cx, |store, cx| {
|
||||||
|
store.refresh_kernelspecs(cx).detach();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
cx.observe_new_views(move |editor: &mut Editor, cx: &mut ViewContext<Editor>| {
|
||||||
|
if !editor.use_modal_editing() || !editor.buffer().read(cx).is_singleton() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let editor_handle = cx.view().downgrade();
|
||||||
|
|
||||||
|
editor
|
||||||
|
.register_action({
|
||||||
|
let editor_handle = editor_handle.clone();
|
||||||
|
move |_: &Run, cx| {
|
||||||
|
if !JupyterSettings::enabled(cx) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
crate::run(editor_handle.clone(), cx).log_err();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
editor
|
||||||
|
.register_action({
|
||||||
|
let editor_handle = editor_handle.clone();
|
||||||
|
move |_: &ClearOutputs, cx| {
|
||||||
|
if !JupyterSettings::enabled(cx) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
crate::clear_outputs(editor_handle.clone(), cx);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
editor
|
||||||
|
.register_action({
|
||||||
|
let editor_handle = editor_handle.clone();
|
||||||
|
move |_: &Interrupt, cx| {
|
||||||
|
if !JupyterSettings::enabled(cx) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
crate::interrupt(editor_handle.clone(), cx);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
editor
|
||||||
|
.register_action({
|
||||||
|
let editor_handle = editor_handle.clone();
|
||||||
|
move |_: &Shutdown, cx| {
|
||||||
|
if !JupyterSettings::enabled(cx) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
crate::shutdown(editor_handle.clone(), cx);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ReplSessionsPage {
|
||||||
|
focus_handle: FocusHandle,
|
||||||
|
_subscriptions: Vec<Subscription>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReplSessionsPage {
|
||||||
|
pub fn new(cx: &mut ViewContext<Workspace>) -> View<Self> {
|
||||||
|
cx.new_view(|cx: &mut ViewContext<Self>| {
|
||||||
|
let focus_handle = cx.focus_handle();
|
||||||
|
|
||||||
|
let subscriptions = vec![
|
||||||
|
cx.on_focus_in(&focus_handle, |_this, cx| cx.notify()),
|
||||||
|
cx.on_focus_out(&focus_handle, |_this, _event, cx| cx.notify()),
|
||||||
|
];
|
||||||
|
|
||||||
|
Self {
|
||||||
|
focus_handle,
|
||||||
|
_subscriptions: subscriptions,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventEmitter<ItemEvent> for ReplSessionsPage {}
|
||||||
|
|
||||||
|
impl FocusableView for ReplSessionsPage {
|
||||||
|
fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
|
||||||
|
self.focus_handle.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Item for ReplSessionsPage {
|
||||||
|
type Event = ItemEvent;
|
||||||
|
|
||||||
|
fn tab_content_text(&self, _cx: &WindowContext) -> Option<SharedString> {
|
||||||
|
Some("REPL Sessions".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn telemetry_event_text(&self) -> Option<&'static str> {
|
||||||
|
Some("repl sessions")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_toolbar(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clone_on_split(
|
||||||
|
&self,
|
||||||
|
_workspace_id: Option<WorkspaceId>,
|
||||||
|
_: &mut ViewContext<Self>,
|
||||||
|
) -> Option<View<Self>> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {
|
||||||
|
f(*event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for ReplSessionsPage {
|
||||||
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
|
let store = ReplStore::global(cx);
|
||||||
|
|
||||||
|
let (kernel_specifications, sessions) = store.update(cx, |store, _cx| {
|
||||||
|
(
|
||||||
|
store.kernel_specifications().cloned().collect::<Vec<_>>(),
|
||||||
|
store.sessions().cloned().collect::<Vec<_>>(),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
// When there are no kernel specifications, show a link to the Zed docs explaining how to
|
||||||
|
// install kernels. It can be assumed they don't have a running kernel if we have no
|
||||||
|
// specifications.
|
||||||
|
if kernel_specifications.is_empty() {
|
||||||
|
return v_flex()
|
||||||
|
.p_4()
|
||||||
|
.size_full()
|
||||||
|
.gap_2()
|
||||||
|
.child(Label::new("No Jupyter Kernels Available").size(LabelSize::Large))
|
||||||
|
.child(
|
||||||
|
Label::new("To start interactively running code in your editor, you need to install and configure Jupyter kernels.")
|
||||||
|
.size(LabelSize::Default),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
h_flex().w_full().p_4().justify_center().gap_2().child(
|
||||||
|
ButtonLike::new("install-kernels")
|
||||||
|
.style(ButtonStyle::Filled)
|
||||||
|
.size(ButtonSize::Large)
|
||||||
|
.layer(ElevationIndex::ModalSurface)
|
||||||
|
.child(Label::new("Install Kernels"))
|
||||||
|
.on_click(move |_, cx| {
|
||||||
|
cx.open_url(
|
||||||
|
"https://docs.jupyter.org/en/latest/install/kernels.html",
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.into_any_element();
|
||||||
|
}
|
||||||
|
|
||||||
|
// When there are no sessions, show the command to run code in an editor
|
||||||
|
if sessions.is_empty() {
|
||||||
|
return v_flex()
|
||||||
|
.p_4()
|
||||||
|
.size_full()
|
||||||
|
.gap_2()
|
||||||
|
.child(Label::new("No Jupyter Kernel Sessions").size(LabelSize::Large))
|
||||||
|
.child(
|
||||||
|
v_flex().child(
|
||||||
|
Label::new("To run code in a Jupyter kernel, select some code and use the 'repl::Run' command.")
|
||||||
|
.size(LabelSize::Default)
|
||||||
|
)
|
||||||
|
.children(
|
||||||
|
KeyBinding::for_action(&Run, cx)
|
||||||
|
.map(|binding|
|
||||||
|
binding.into_any_element()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.child(Label::new("Kernels available").size(LabelSize::Large))
|
||||||
|
.children(
|
||||||
|
kernel_specifications.into_iter().map(|spec| {
|
||||||
|
h_flex().gap_2().child(Label::new(spec.name.clone()))
|
||||||
|
.child(Label::new(spec.kernelspec.language.clone()).color(Color::Muted))
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
.into_any_element();
|
||||||
|
}
|
||||||
|
|
||||||
|
v_flex()
|
||||||
|
.p_4()
|
||||||
|
.child(Label::new("Jupyter Kernel Sessions").size(LabelSize::Large))
|
||||||
|
.children(
|
||||||
|
sessions
|
||||||
|
.into_iter()
|
||||||
|
.map(|session| session.clone().into_any_element()),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
}
|
|
@ -28,6 +28,10 @@ impl ReplStore {
|
||||||
pub(crate) fn init(fs: Arc<dyn Fs>, cx: &mut AppContext) {
|
pub(crate) fn init(fs: Arc<dyn Fs>, cx: &mut AppContext) {
|
||||||
let store = cx.new_model(move |cx| Self::new(fs, cx));
|
let store = cx.new_model(move |cx| Self::new(fs, cx));
|
||||||
|
|
||||||
|
store
|
||||||
|
.update(cx, |store, cx| store.refresh_kernelspecs(cx))
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
|
||||||
cx.set_global(GlobalReplStore(store))
|
cx.set_global(GlobalReplStore(store))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,6 +53,10 @@ impl ReplStore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn fs(&self) -> &Arc<dyn Fs> {
|
||||||
|
&self.fs
|
||||||
|
}
|
||||||
|
|
||||||
pub fn is_enabled(&self) -> bool {
|
pub fn is_enabled(&self) -> bool {
|
||||||
self.enabled
|
self.enabled
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,523 +0,0 @@
|
||||||
use crate::repl_store::ReplStore;
|
|
||||||
use crate::{
|
|
||||||
jupyter_settings::{JupyterDockPosition, JupyterSettings},
|
|
||||||
kernels::KernelSpecification,
|
|
||||||
session::{Session, SessionEvent},
|
|
||||||
};
|
|
||||||
use anyhow::{Context as _, Result};
|
|
||||||
use editor::{Anchor, Editor, RangeToAnchorExt};
|
|
||||||
use gpui::{
|
|
||||||
actions, prelude::*, AppContext, AsyncWindowContext, EventEmitter, FocusHandle, FocusOutEvent,
|
|
||||||
FocusableView, Subscription, Task, View, WeakView,
|
|
||||||
};
|
|
||||||
use language::{Language, Point};
|
|
||||||
use multi_buffer::MultiBufferRow;
|
|
||||||
use project::Fs;
|
|
||||||
use settings::Settings as _;
|
|
||||||
use std::{ops::Range, sync::Arc};
|
|
||||||
use ui::{prelude::*, ButtonLike, ElevationIndex, KeyBinding};
|
|
||||||
use util::ResultExt as _;
|
|
||||||
use workspace::{
|
|
||||||
dock::{Panel, PanelEvent},
|
|
||||||
Workspace,
|
|
||||||
};
|
|
||||||
|
|
||||||
actions!(
|
|
||||||
repl,
|
|
||||||
[Run, ClearOutputs, Interrupt, Shutdown, RefreshKernelspecs]
|
|
||||||
);
|
|
||||||
actions!(repl_panel, [ToggleFocus]);
|
|
||||||
|
|
||||||
pub enum SessionSupport {
|
|
||||||
ActiveSession(View<Session>),
|
|
||||||
Inactive(Box<KernelSpecification>),
|
|
||||||
RequiresSetup(Arc<str>),
|
|
||||||
Unsupported,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn init(cx: &mut AppContext) {
|
|
||||||
cx.observe_new_views(
|
|
||||||
|workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
|
|
||||||
workspace.register_action(|workspace, _: &ToggleFocus, cx| {
|
|
||||||
workspace.toggle_panel_focus::<RuntimePanel>(cx);
|
|
||||||
});
|
|
||||||
|
|
||||||
workspace.register_action(|_workspace, _: &RefreshKernelspecs, cx| {
|
|
||||||
let store = ReplStore::global(cx);
|
|
||||||
store.update(cx, |store, cx| {
|
|
||||||
store.refresh_kernelspecs(cx).detach();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.detach();
|
|
||||||
|
|
||||||
cx.observe_new_views(move |editor: &mut Editor, cx: &mut ViewContext<Editor>| {
|
|
||||||
// Only allow editors that support vim mode and are singleton buffers
|
|
||||||
if !editor.use_modal_editing() || !editor.buffer().read(cx).is_singleton() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
editor
|
|
||||||
.register_action(cx.listener(
|
|
||||||
move |editor: &mut Editor, _: &Run, cx: &mut ViewContext<Editor>| {
|
|
||||||
if !JupyterSettings::enabled(cx) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let Some(workspace) = editor.workspace() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let Some(panel) = workspace.read(cx).panel::<RuntimePanel>(cx) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let weak_editor = cx.view().downgrade();
|
|
||||||
panel.update(cx, |_, cx| {
|
|
||||||
cx.defer(|panel, cx| {
|
|
||||||
panel.run(weak_editor, cx).log_err();
|
|
||||||
});
|
|
||||||
})
|
|
||||||
},
|
|
||||||
))
|
|
||||||
.detach();
|
|
||||||
|
|
||||||
editor
|
|
||||||
.register_action(cx.listener(
|
|
||||||
move |editor: &mut Editor, _: &ClearOutputs, cx: &mut ViewContext<Editor>| {
|
|
||||||
if !JupyterSettings::enabled(cx) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let Some(workspace) = editor.workspace() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let Some(panel) = workspace.read(cx).panel::<RuntimePanel>(cx) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let weak_editor = cx.view().downgrade();
|
|
||||||
panel.update(cx, |_, cx| {
|
|
||||||
cx.defer(|panel, cx| {
|
|
||||||
panel.clear_outputs(weak_editor, cx);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
},
|
|
||||||
))
|
|
||||||
.detach();
|
|
||||||
|
|
||||||
editor
|
|
||||||
.register_action(cx.listener(
|
|
||||||
move |editor: &mut Editor, _: &Interrupt, cx: &mut ViewContext<Editor>| {
|
|
||||||
if !JupyterSettings::enabled(cx) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let Some(workspace) = editor.workspace() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let Some(panel) = workspace.read(cx).panel::<RuntimePanel>(cx) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let weak_editor = cx.view().downgrade();
|
|
||||||
panel.update(cx, |_, cx| {
|
|
||||||
cx.defer(|panel, cx| {
|
|
||||||
panel.interrupt(weak_editor, cx);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
},
|
|
||||||
))
|
|
||||||
.detach();
|
|
||||||
|
|
||||||
editor
|
|
||||||
.register_action(cx.listener(
|
|
||||||
move |editor: &mut Editor, _: &Shutdown, cx: &mut ViewContext<Editor>| {
|
|
||||||
if !JupyterSettings::enabled(cx) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let Some(workspace) = editor.workspace() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let Some(panel) = workspace.read(cx).panel::<RuntimePanel>(cx) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let weak_editor = cx.view().downgrade();
|
|
||||||
panel.update(cx, |_, cx| {
|
|
||||||
cx.defer(|panel, cx| {
|
|
||||||
panel.shutdown(weak_editor, cx);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
},
|
|
||||||
))
|
|
||||||
.detach();
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct RuntimePanel {
|
|
||||||
fs: Arc<dyn Fs>,
|
|
||||||
focus_handle: FocusHandle,
|
|
||||||
width: Option<Pixels>,
|
|
||||||
_subscriptions: Vec<Subscription>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RuntimePanel {
|
|
||||||
pub fn load(
|
|
||||||
workspace: WeakView<Workspace>,
|
|
||||||
cx: AsyncWindowContext,
|
|
||||||
) -> Task<Result<View<Self>>> {
|
|
||||||
cx.spawn(|mut cx| async move {
|
|
||||||
let view = workspace.update(&mut cx, |workspace, cx| {
|
|
||||||
cx.new_view::<Self>(|cx| {
|
|
||||||
let focus_handle = cx.focus_handle();
|
|
||||||
|
|
||||||
let fs = workspace.app_state().fs.clone();
|
|
||||||
|
|
||||||
let subscriptions = vec![
|
|
||||||
cx.on_focus_in(&focus_handle, Self::focus_in),
|
|
||||||
cx.on_focus_out(&focus_handle, Self::focus_out),
|
|
||||||
];
|
|
||||||
|
|
||||||
let runtime_panel = Self {
|
|
||||||
fs,
|
|
||||||
width: None,
|
|
||||||
focus_handle,
|
|
||||||
_subscriptions: subscriptions,
|
|
||||||
};
|
|
||||||
|
|
||||||
runtime_panel
|
|
||||||
})
|
|
||||||
})?;
|
|
||||||
|
|
||||||
view.update(&mut cx, |_panel, cx| {
|
|
||||||
let store = ReplStore::global(cx);
|
|
||||||
store.update(cx, |store, cx| store.refresh_kernelspecs(cx))
|
|
||||||
})?
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(view)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn focus_in(&mut self, cx: &mut ViewContext<Self>) {
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn focus_out(&mut self, _event: FocusOutEvent, cx: &mut ViewContext<Self>) {
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn snippet(
|
|
||||||
editor: WeakView<Editor>,
|
|
||||||
cx: &mut ViewContext<Self>,
|
|
||||||
) -> Option<(String, Arc<Language>, Range<Anchor>)> {
|
|
||||||
let editor = editor.upgrade()?;
|
|
||||||
let editor = editor.read(cx);
|
|
||||||
|
|
||||||
let buffer = editor.buffer().read(cx).snapshot(cx);
|
|
||||||
|
|
||||||
let selection = editor.selections.newest::<usize>(cx);
|
|
||||||
let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
|
|
||||||
|
|
||||||
let range = if selection.is_empty() {
|
|
||||||
let cursor = selection.head();
|
|
||||||
|
|
||||||
let cursor_row = multi_buffer_snapshot.offset_to_point(cursor).row;
|
|
||||||
let start_offset = multi_buffer_snapshot.point_to_offset(Point::new(cursor_row, 0));
|
|
||||||
|
|
||||||
let end_point = Point::new(
|
|
||||||
cursor_row,
|
|
||||||
multi_buffer_snapshot.line_len(MultiBufferRow(cursor_row)),
|
|
||||||
);
|
|
||||||
let end_offset = start_offset.saturating_add(end_point.column as usize);
|
|
||||||
|
|
||||||
// Create a range from the start to the end of the line
|
|
||||||
start_offset..end_offset
|
|
||||||
} else {
|
|
||||||
selection.range()
|
|
||||||
};
|
|
||||||
|
|
||||||
let anchor_range = range.to_anchors(&multi_buffer_snapshot);
|
|
||||||
|
|
||||||
let selected_text = buffer
|
|
||||||
.text_for_range(anchor_range.clone())
|
|
||||||
.collect::<String>();
|
|
||||||
|
|
||||||
let start_language = buffer.language_at(anchor_range.start)?;
|
|
||||||
let end_language = buffer.language_at(anchor_range.end)?;
|
|
||||||
if start_language != end_language {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
Some((selected_text, start_language.clone(), anchor_range))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn language(editor: WeakView<Editor>, cx: &mut ViewContext<Self>) -> Option<Arc<Language>> {
|
|
||||||
let editor = editor.upgrade()?;
|
|
||||||
let selection = editor.read(cx).selections.newest::<usize>(cx);
|
|
||||||
let buffer = editor.read(cx).buffer().read(cx).snapshot(cx);
|
|
||||||
buffer.language_at(selection.head()).cloned()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn run(&mut self, editor: WeakView<Editor>, cx: &mut ViewContext<Self>) -> Result<()> {
|
|
||||||
let store = ReplStore::global(cx);
|
|
||||||
|
|
||||||
if !store.read(cx).is_enabled() {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let (selected_text, language, anchor_range) = match Self::snippet(editor.clone(), cx) {
|
|
||||||
Some(snippet) => snippet,
|
|
||||||
None => return Ok(()),
|
|
||||||
};
|
|
||||||
|
|
||||||
let entity_id = editor.entity_id();
|
|
||||||
|
|
||||||
let kernel_specification = store.update(cx, |store, cx| {
|
|
||||||
store
|
|
||||||
.kernelspec(&language, cx)
|
|
||||||
.with_context(|| format!("No kernel found for language: {}", language.name()))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let session = if let Some(session) = store.read(cx).get_session(entity_id).cloned() {
|
|
||||||
session
|
|
||||||
} else {
|
|
||||||
let session =
|
|
||||||
cx.new_view(|cx| Session::new(editor, self.fs.clone(), kernel_specification, cx));
|
|
||||||
cx.notify();
|
|
||||||
|
|
||||||
let subscription = cx.subscribe(&session, {
|
|
||||||
let store = store.clone();
|
|
||||||
move |_this, _session, event, cx| match event {
|
|
||||||
SessionEvent::Shutdown(shutdown_event) => {
|
|
||||||
store.update(cx, |store, _cx| {
|
|
||||||
store.remove_session(shutdown_event.entity_id());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
subscription.detach();
|
|
||||||
|
|
||||||
store.update(cx, |store, _cx| {
|
|
||||||
store.insert_session(entity_id, session.clone());
|
|
||||||
});
|
|
||||||
|
|
||||||
session
|
|
||||||
};
|
|
||||||
|
|
||||||
session.update(cx, |session, cx| {
|
|
||||||
session.execute(&selected_text, anchor_range, cx);
|
|
||||||
});
|
|
||||||
|
|
||||||
anyhow::Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn session(
|
|
||||||
&mut self,
|
|
||||||
editor: WeakView<Editor>,
|
|
||||||
cx: &mut ViewContext<Self>,
|
|
||||||
) -> SessionSupport {
|
|
||||||
let store = ReplStore::global(cx);
|
|
||||||
let entity_id = editor.entity_id();
|
|
||||||
|
|
||||||
if let Some(session) = store.read(cx).get_session(entity_id).cloned() {
|
|
||||||
return SessionSupport::ActiveSession(session);
|
|
||||||
};
|
|
||||||
|
|
||||||
let language = Self::language(editor, cx);
|
|
||||||
let language = match language {
|
|
||||||
Some(language) => language,
|
|
||||||
None => return SessionSupport::Unsupported,
|
|
||||||
};
|
|
||||||
let kernelspec = store.update(cx, |store, cx| store.kernelspec(&language, cx));
|
|
||||||
|
|
||||||
match kernelspec {
|
|
||||||
Some(kernelspec) => SessionSupport::Inactive(Box::new(kernelspec)),
|
|
||||||
None => match language.name().as_ref() {
|
|
||||||
"TypeScript" | "Python" => SessionSupport::RequiresSetup(language.name()),
|
|
||||||
_ => SessionSupport::Unsupported,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn clear_outputs(&mut self, editor: WeakView<Editor>, cx: &mut ViewContext<Self>) {
|
|
||||||
let store = ReplStore::global(cx);
|
|
||||||
let entity_id = editor.entity_id();
|
|
||||||
if let Some(session) = store.read(cx).get_session(entity_id).cloned() {
|
|
||||||
session.update(cx, |session, cx| {
|
|
||||||
session.clear_outputs(cx);
|
|
||||||
});
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn interrupt(&mut self, editor: WeakView<Editor>, cx: &mut ViewContext<Self>) {
|
|
||||||
let store = ReplStore::global(cx);
|
|
||||||
let entity_id = editor.entity_id();
|
|
||||||
if let Some(session) = store.read(cx).get_session(entity_id).cloned() {
|
|
||||||
session.update(cx, |session, cx| {
|
|
||||||
session.interrupt(cx);
|
|
||||||
});
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn shutdown(&self, editor: WeakView<Editor>, cx: &mut ViewContext<Self>) {
|
|
||||||
let store = ReplStore::global(cx);
|
|
||||||
let entity_id = editor.entity_id();
|
|
||||||
if let Some(session) = store.read(cx).get_session(entity_id).cloned() {
|
|
||||||
session.update(cx, |session, cx| {
|
|
||||||
session.shutdown(cx);
|
|
||||||
});
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Panel for RuntimePanel {
|
|
||||||
fn persistent_name() -> &'static str {
|
|
||||||
"RuntimePanel"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn position(&self, cx: &ui::WindowContext) -> workspace::dock::DockPosition {
|
|
||||||
match JupyterSettings::get_global(cx).dock {
|
|
||||||
JupyterDockPosition::Left => workspace::dock::DockPosition::Left,
|
|
||||||
JupyterDockPosition::Right => workspace::dock::DockPosition::Right,
|
|
||||||
JupyterDockPosition::Bottom => workspace::dock::DockPosition::Bottom,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn position_is_valid(&self, _position: workspace::dock::DockPosition) -> bool {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_position(
|
|
||||||
&mut self,
|
|
||||||
position: workspace::dock::DockPosition,
|
|
||||||
cx: &mut ViewContext<Self>,
|
|
||||||
) {
|
|
||||||
settings::update_settings_file::<JupyterSettings>(self.fs.clone(), cx, move |settings| {
|
|
||||||
let dock = match position {
|
|
||||||
workspace::dock::DockPosition::Left => JupyterDockPosition::Left,
|
|
||||||
workspace::dock::DockPosition::Right => JupyterDockPosition::Right,
|
|
||||||
workspace::dock::DockPosition::Bottom => JupyterDockPosition::Bottom,
|
|
||||||
};
|
|
||||||
settings.set_dock(dock);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn size(&self, cx: &ui::WindowContext) -> Pixels {
|
|
||||||
let settings = JupyterSettings::get_global(cx);
|
|
||||||
|
|
||||||
self.width.unwrap_or(settings.default_width)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_size(&mut self, size: Option<ui::Pixels>, _cx: &mut ViewContext<Self>) {
|
|
||||||
self.width = size;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn icon(&self, cx: &ui::WindowContext) -> Option<ui::IconName> {
|
|
||||||
let store = ReplStore::global(cx);
|
|
||||||
|
|
||||||
if !store.read(cx).is_enabled() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(IconName::Code)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn icon_tooltip(&self, _cx: &ui::WindowContext) -> Option<&'static str> {
|
|
||||||
Some("Runtime Panel")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn toggle_action(&self) -> Box<dyn gpui::Action> {
|
|
||||||
Box::new(ToggleFocus)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EventEmitter<PanelEvent> for RuntimePanel {}
|
|
||||||
|
|
||||||
impl FocusableView for RuntimePanel {
|
|
||||||
fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
|
|
||||||
self.focus_handle.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for RuntimePanel {
|
|
||||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
|
||||||
let store = ReplStore::global(cx);
|
|
||||||
|
|
||||||
let (kernel_specifications, sessions) = store.update(cx, |store, _cx| {
|
|
||||||
(
|
|
||||||
store.kernel_specifications().cloned().collect::<Vec<_>>(),
|
|
||||||
store.sessions().cloned().collect::<Vec<_>>(),
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
// When there are no kernel specifications, show a link to the Zed docs explaining how to
|
|
||||||
// install kernels. It can be assumed they don't have a running kernel if we have no
|
|
||||||
// specifications.
|
|
||||||
if kernel_specifications.is_empty() {
|
|
||||||
return v_flex()
|
|
||||||
.p_4()
|
|
||||||
.size_full()
|
|
||||||
.gap_2()
|
|
||||||
.child(Label::new("No Jupyter Kernels Available").size(LabelSize::Large))
|
|
||||||
.child(
|
|
||||||
Label::new("To start interactively running code in your editor, you need to install and configure Jupyter kernels.")
|
|
||||||
.size(LabelSize::Default),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
h_flex().w_full().p_4().justify_center().gap_2().child(
|
|
||||||
ButtonLike::new("install-kernels")
|
|
||||||
.style(ButtonStyle::Filled)
|
|
||||||
.size(ButtonSize::Large)
|
|
||||||
.layer(ElevationIndex::ModalSurface)
|
|
||||||
.child(Label::new("Install Kernels"))
|
|
||||||
.on_click(move |_, cx| {
|
|
||||||
cx.open_url(
|
|
||||||
"https://docs.jupyter.org/en/latest/install/kernels.html",
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.into_any_element();
|
|
||||||
}
|
|
||||||
|
|
||||||
// When there are no sessions, show the command to run code in an editor
|
|
||||||
if sessions.is_empty() {
|
|
||||||
return v_flex()
|
|
||||||
.p_4()
|
|
||||||
.size_full()
|
|
||||||
.gap_2()
|
|
||||||
.child(Label::new("No Jupyter Kernel Sessions").size(LabelSize::Large))
|
|
||||||
.child(
|
|
||||||
v_flex().child(
|
|
||||||
Label::new("To run code in a Jupyter kernel, select some code and use the 'repl::Run' command.")
|
|
||||||
.size(LabelSize::Default)
|
|
||||||
)
|
|
||||||
.children(
|
|
||||||
KeyBinding::for_action(&Run, cx)
|
|
||||||
.map(|binding|
|
|
||||||
binding.into_any_element()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.child(Label::new("Kernels available").size(LabelSize::Large))
|
|
||||||
.children(
|
|
||||||
kernel_specifications.into_iter().map(|spec| {
|
|
||||||
h_flex().gap_2().child(Label::new(spec.name.clone()))
|
|
||||||
.child(Label::new(spec.kernelspec.language.clone()).color(Color::Muted))
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
.into_any_element();
|
|
||||||
}
|
|
||||||
|
|
||||||
v_flex()
|
|
||||||
.p_4()
|
|
||||||
.child(Label::new("Jupyter Kernel Sessions").size(LabelSize::Large))
|
|
||||||
.children(
|
|
||||||
sessions
|
|
||||||
.into_iter()
|
|
||||||
.map(|session| session.clone().into_any_element()),
|
|
||||||
)
|
|
||||||
.into_any_element()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -80,7 +80,7 @@ impl EditorBlock {
|
||||||
position: code_range.end,
|
position: code_range.end,
|
||||||
height: execution_view.num_lines(cx).saturating_add(1),
|
height: execution_view.num_lines(cx).saturating_add(1),
|
||||||
style: BlockStyle::Sticky,
|
style: BlockStyle::Sticky,
|
||||||
render: Self::create_output_area_render(execution_view.clone(), on_close.clone()),
|
render: Self::create_output_area_renderer(execution_view.clone(), on_close.clone()),
|
||||||
disposition: BlockDisposition::Below,
|
disposition: BlockDisposition::Below,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -111,7 +111,7 @@ impl EditorBlock {
|
||||||
self.block_id,
|
self.block_id,
|
||||||
(
|
(
|
||||||
Some(self.execution_view.num_lines(cx).saturating_add(1)),
|
Some(self.execution_view.num_lines(cx).saturating_add(1)),
|
||||||
Self::create_output_area_render(
|
Self::create_output_area_renderer(
|
||||||
self.execution_view.clone(),
|
self.execution_view.clone(),
|
||||||
self.on_close.clone(),
|
self.on_close.clone(),
|
||||||
),
|
),
|
||||||
|
@ -122,7 +122,7 @@ impl EditorBlock {
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_output_area_render(
|
fn create_output_area_renderer(
|
||||||
execution_view: View<ExecutionView>,
|
execution_view: View<ExecutionView>,
|
||||||
on_close: CloseBlockFn,
|
on_close: CloseBlockFn,
|
||||||
) -> RenderBlock {
|
) -> RenderBlock {
|
||||||
|
|
|
@ -199,8 +199,6 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
|
||||||
let assistant_panel =
|
let assistant_panel =
|
||||||
assistant::AssistantPanel::load(workspace_handle.clone(), cx.clone());
|
assistant::AssistantPanel::load(workspace_handle.clone(), cx.clone());
|
||||||
|
|
||||||
let runtime_panel = repl::RuntimePanel::load(workspace_handle.clone(), cx.clone());
|
|
||||||
|
|
||||||
let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone());
|
let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone());
|
||||||
let outline_panel = OutlinePanel::load(workspace_handle.clone(), cx.clone());
|
let outline_panel = OutlinePanel::load(workspace_handle.clone(), cx.clone());
|
||||||
let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone());
|
let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone());
|
||||||
|
@ -218,7 +216,6 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
|
||||||
outline_panel,
|
outline_panel,
|
||||||
terminal_panel,
|
terminal_panel,
|
||||||
assistant_panel,
|
assistant_panel,
|
||||||
runtime_panel,
|
|
||||||
channels_panel,
|
channels_panel,
|
||||||
chat_panel,
|
chat_panel,
|
||||||
notification_panel,
|
notification_panel,
|
||||||
|
@ -227,7 +224,6 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
|
||||||
outline_panel,
|
outline_panel,
|
||||||
terminal_panel,
|
terminal_panel,
|
||||||
assistant_panel,
|
assistant_panel,
|
||||||
runtime_panel,
|
|
||||||
channels_panel,
|
channels_panel,
|
||||||
chat_panel,
|
chat_panel,
|
||||||
notification_panel,
|
notification_panel,
|
||||||
|
@ -235,7 +231,6 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
|
||||||
|
|
||||||
workspace_handle.update(&mut cx, |workspace, cx| {
|
workspace_handle.update(&mut cx, |workspace, cx| {
|
||||||
workspace.add_panel(assistant_panel, cx);
|
workspace.add_panel(assistant_panel, cx);
|
||||||
workspace.add_panel(runtime_panel, cx);
|
|
||||||
workspace.add_panel(project_panel, cx);
|
workspace.add_panel(project_panel, cx);
|
||||||
workspace.add_panel(outline_panel, cx);
|
workspace.add_panel(outline_panel, cx);
|
||||||
workspace.add_panel(terminal_panel, cx);
|
workspace.add_panel(terminal_panel, cx);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue