This adds a dedicated `AvailableLanguages` struct in preparation for a more cabable language matching based on a given language config. No functional changes, just shuffling some code around for this round. Also made the LanguageMatcher non-cloneable in favor of wrapping it in an Arc, since cloning is rather expensive for this and having a reference is sufficient in all cases right now. Release Notes: - N/A --------- Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
457 lines
15 KiB
Rust
457 lines
15 KiB
Rust
use editor::Editor;
|
|
use gpui::{
|
|
AnyElement, App, Entity, EventEmitter, FocusHandle, Focusable, Subscription, TaskExt, actions,
|
|
prelude::*,
|
|
};
|
|
use language::{Buffer, BufferEvent};
|
|
use project::{Project, ProjectItem as _};
|
|
use ui::{ButtonLike, ElevationIndex, KeyBinding, prelude::*};
|
|
use util::ResultExt as _;
|
|
use workspace::item::ItemEvent;
|
|
use workspace::{Workspace, item::Item};
|
|
|
|
use crate::jupyter_settings::JupyterSettings;
|
|
use crate::repl_store::ReplStore;
|
|
|
|
fn refresh_python_kernelspecs_for_buffer(
|
|
buffer: &Entity<Buffer>,
|
|
project: &Entity<Project>,
|
|
cx: &mut App,
|
|
) {
|
|
let buffer = buffer.read(cx);
|
|
|
|
if buffer
|
|
.language()
|
|
.is_none_or(|language| language.name() != "Python")
|
|
{
|
|
return;
|
|
};
|
|
|
|
let Some(project_path) = buffer.project_path(cx) else {
|
|
return;
|
|
};
|
|
let store = ReplStore::global(cx);
|
|
store.update(cx, |store, cx| {
|
|
store
|
|
.refresh_python_kernelspecs(project_path.worktree_id, project, cx)
|
|
.detach_and_log_err(cx);
|
|
});
|
|
}
|
|
|
|
actions!(
|
|
repl,
|
|
[
|
|
/// Runs the current cell and advances to the next one.
|
|
Run,
|
|
/// Runs the current cell without advancing.
|
|
RunInPlace,
|
|
/// Clears all outputs in the REPL.
|
|
ClearOutputs,
|
|
/// Clears the output of the cell at the current cursor position.
|
|
ClearCurrentOutput,
|
|
/// Opens the REPL sessions panel.
|
|
Sessions,
|
|
/// Interrupts the currently running kernel.
|
|
Interrupt,
|
|
/// Shuts down the current kernel.
|
|
Shutdown,
|
|
/// Restarts the current kernel.
|
|
Restart,
|
|
/// Refreshes the list of available kernelspecs.
|
|
RefreshKernelspecs
|
|
]
|
|
);
|
|
|
|
pub fn init(cx: &mut App) {
|
|
cx.observe_new(
|
|
|workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {
|
|
workspace.register_action(|workspace, _: &Sessions, window, 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, window, cx);
|
|
} else {
|
|
let repl_sessions_page = ReplSessionsPage::new(window, cx);
|
|
workspace.add_item_to_active_pane(
|
|
Box::new(repl_sessions_page),
|
|
None,
|
|
true,
|
|
window,
|
|
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(
|
|
move |editor: &mut Editor, window, cx: &mut Context<Editor>| {
|
|
let Some(window) = window else {
|
|
return;
|
|
};
|
|
|
|
if !editor.use_modal_editing() || !editor.buffer().read(cx).is_singleton() {
|
|
return;
|
|
}
|
|
|
|
cx.defer_in(window, |editor, _window, cx| {
|
|
let project = editor.project().cloned();
|
|
|
|
let is_valid_project = project
|
|
.as_ref()
|
|
.map(|project| {
|
|
let p = project.read(cx);
|
|
!p.is_via_collab()
|
|
})
|
|
.unwrap_or(false);
|
|
|
|
if !is_valid_project {
|
|
return;
|
|
}
|
|
|
|
let buffer = editor.buffer().read(cx).as_singleton();
|
|
|
|
let editor_handle = cx.entity().downgrade();
|
|
|
|
// Subscribe to the buffer's `LanguageChanged` events so remote projects,
|
|
// where language detection can complete after the editor is observed,
|
|
// still trigger a kernelspec refresh. Without this the REPL UI stays
|
|
// hidden until something else populates the global kernel list.
|
|
if let Some((buffer, project)) = buffer.zip(project) {
|
|
refresh_python_kernelspecs_for_buffer(&buffer, &project, cx);
|
|
|
|
cx.subscribe(&buffer, move |_editor, buffer, event, cx| {
|
|
if let BufferEvent::LanguageChanged(_) = event {
|
|
refresh_python_kernelspecs_for_buffer(&buffer, &project, cx);
|
|
}
|
|
})
|
|
.detach();
|
|
}
|
|
|
|
editor
|
|
.register_action({
|
|
let editor_handle = editor_handle.clone();
|
|
move |_: &Run, window, cx| {
|
|
if !JupyterSettings::enabled(cx) {
|
|
return;
|
|
}
|
|
|
|
crate::run(editor_handle.clone(), true, window, cx).log_err();
|
|
}
|
|
})
|
|
.detach();
|
|
|
|
editor
|
|
.register_action({
|
|
move |_: &RunInPlace, window, cx| {
|
|
if !JupyterSettings::enabled(cx) {
|
|
return;
|
|
}
|
|
|
|
crate::run(editor_handle.clone(), false, window, cx).log_err();
|
|
}
|
|
})
|
|
.detach();
|
|
});
|
|
},
|
|
)
|
|
.detach();
|
|
}
|
|
|
|
pub struct ReplSessionsPage {
|
|
focus_handle: FocusHandle,
|
|
_subscriptions: Vec<Subscription>,
|
|
}
|
|
|
|
impl ReplSessionsPage {
|
|
pub fn new(window: &mut Window, cx: &mut Context<Workspace>) -> Entity<Self> {
|
|
cx.new(|cx| {
|
|
let focus_handle = cx.focus_handle();
|
|
|
|
let subscriptions = vec![
|
|
cx.on_focus_in(&focus_handle, window, |_this, _window, cx| cx.notify()),
|
|
cx.on_focus_out(&focus_handle, window, |_this, _event, _window, cx| {
|
|
cx.notify()
|
|
}),
|
|
];
|
|
|
|
Self {
|
|
focus_handle,
|
|
_subscriptions: subscriptions,
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
impl EventEmitter<ItemEvent> for ReplSessionsPage {}
|
|
|
|
impl Focusable for ReplSessionsPage {
|
|
fn focus_handle(&self, _cx: &App) -> FocusHandle {
|
|
self.focus_handle.clone()
|
|
}
|
|
}
|
|
|
|
impl Item for ReplSessionsPage {
|
|
type Event = ItemEvent;
|
|
|
|
fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
|
|
"REPL Sessions".into()
|
|
}
|
|
|
|
fn telemetry_event_text(&self) -> Option<&'static str> {
|
|
Some("REPL Session Started")
|
|
}
|
|
|
|
fn show_toolbar(&self) -> bool {
|
|
false
|
|
}
|
|
|
|
fn to_item_events(event: &Self::Event, f: &mut dyn FnMut(workspace::item::ItemEvent)) {
|
|
f(*event)
|
|
}
|
|
}
|
|
|
|
impl Render for ReplSessionsPage {
|
|
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
let store = ReplStore::global(cx);
|
|
|
|
let (kernel_specifications, sessions) = store.update(cx, |store, cx| {
|
|
store.ensure_kernelspecs(cx);
|
|
(
|
|
store
|
|
.pure_jupyter_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() {
|
|
let instructions = "To start interactively running code in your editor, you need to install and configure Jupyter kernels.";
|
|
|
|
return ReplSessionsContainer::new("No Jupyter Kernels Available")
|
|
.child(Label::new(instructions))
|
|
.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://zed.dev/docs/repl#language-specific-instructions",
|
|
)
|
|
}),
|
|
),
|
|
);
|
|
}
|
|
|
|
// When there are no sessions, show the command to run code in an editor
|
|
if sessions.is_empty() {
|
|
let instructions = "To run code in a Jupyter kernel, select some code and use the 'repl::Run' command.";
|
|
|
|
return ReplSessionsContainer::new("No Jupyter Kernel Sessions").child(
|
|
v_flex()
|
|
.child(Label::new(instructions))
|
|
.child(KeyBinding::for_action(&Run, cx)),
|
|
);
|
|
}
|
|
|
|
ReplSessionsContainer::new("Jupyter Kernel Sessions").children(sessions)
|
|
}
|
|
}
|
|
|
|
#[derive(IntoElement)]
|
|
struct ReplSessionsContainer {
|
|
title: SharedString,
|
|
children: Vec<AnyElement>,
|
|
}
|
|
|
|
impl ReplSessionsContainer {
|
|
pub fn new(title: impl Into<SharedString>) -> Self {
|
|
Self {
|
|
title: title.into(),
|
|
children: Vec::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ParentElement for ReplSessionsContainer {
|
|
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
|
self.children.extend(elements)
|
|
}
|
|
}
|
|
|
|
impl RenderOnce for ReplSessionsContainer {
|
|
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
|
v_flex()
|
|
.p_4()
|
|
.gap_2()
|
|
.size_full()
|
|
.child(Label::new(self.title).size(LabelSize::Large))
|
|
.children(self.children)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
use std::{path::PathBuf, sync::Arc};
|
|
|
|
use async_trait::async_trait;
|
|
use collections::HashMap;
|
|
use editor::EditorMode;
|
|
use gpui::TestAppContext;
|
|
use language::{
|
|
Language, LanguageConfig, LanguageMatcher, LanguageName, ManifestName, Toolchain,
|
|
ToolchainList, ToolchainLister, ToolchainMetadata,
|
|
};
|
|
use multi_buffer::MultiBuffer;
|
|
use task::ShellKind;
|
|
use util::path;
|
|
|
|
struct TestPythonToolchainLister;
|
|
|
|
#[async_trait]
|
|
impl ToolchainLister for TestPythonToolchainLister {
|
|
async fn list(
|
|
&self,
|
|
_worktree_root: PathBuf,
|
|
_subroot_relative_path: Arc<util::rel_path::RelPath>,
|
|
_project_env: Option<HashMap<String, String>>,
|
|
) -> ToolchainList {
|
|
ToolchainList {
|
|
toolchains: vec![Toolchain {
|
|
name: SharedString::new_static("Test Python"),
|
|
path: SharedString::new_static("/test/python"),
|
|
language_name: LanguageName::new_static("Python"),
|
|
as_json: serde_json::Value::Null,
|
|
}],
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
async fn resolve(
|
|
&self,
|
|
_path: PathBuf,
|
|
_project_env: Option<HashMap<String, String>>,
|
|
) -> anyhow::Result<Toolchain> {
|
|
anyhow::bail!("not implemented")
|
|
}
|
|
|
|
fn activation_script(
|
|
&self,
|
|
_toolchain: &Toolchain,
|
|
_shell: ShellKind,
|
|
_cx: &App,
|
|
) -> futures::future::BoxFuture<'static, Vec<String>> {
|
|
Box::pin(async { Vec::new() })
|
|
}
|
|
|
|
fn meta(&self) -> ToolchainMetadata {
|
|
ToolchainMetadata {
|
|
term: SharedString::new_static("Python"),
|
|
new_toolchain_placeholder: SharedString::default(),
|
|
manifest_name: ManifestName::from(SharedString::new_static("pyproject.toml")),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_refreshes_python_kernelspecs_when_buffer_language_changes(
|
|
cx: &mut TestAppContext,
|
|
) {
|
|
cx.update(|cx| {
|
|
settings::init(cx);
|
|
theme_settings::init(theme::LoadThemes::JustBase, cx);
|
|
editor::init(cx);
|
|
});
|
|
|
|
let fs = project::FakeFs::new(cx.background_executor.clone());
|
|
fs.insert_tree(
|
|
path!("/project"),
|
|
serde_json::json!({
|
|
"main.txt": "print('hi')",
|
|
}),
|
|
)
|
|
.await;
|
|
|
|
let project = project::Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
|
|
let python = Arc::new(
|
|
Language::new(
|
|
LanguageConfig {
|
|
name: "Python".into(),
|
|
matcher: (LanguageMatcher {
|
|
path_suffixes: vec!["py".to_string()],
|
|
..Default::default()
|
|
})
|
|
.into(),
|
|
..Default::default()
|
|
},
|
|
None,
|
|
)
|
|
.with_manifest(Some(ManifestName::from(SharedString::new_static(
|
|
"pyproject.toml",
|
|
))))
|
|
.with_toolchain_lister(Some(Arc::new(TestPythonToolchainLister))),
|
|
);
|
|
project.read_with(cx, |project, _cx| {
|
|
project.languages().add(python.clone());
|
|
});
|
|
|
|
cx.update(|cx| crate::init(fs, cx));
|
|
|
|
let buffer = project
|
|
.update(cx, |project, cx| {
|
|
project.open_local_buffer(path!("/project/main.txt"), cx)
|
|
})
|
|
.await
|
|
.expect("failed to open buffer");
|
|
|
|
let worktree_id = buffer
|
|
.read_with(cx, |buffer, cx| {
|
|
buffer.project_path(cx).map(|path| path.worktree_id)
|
|
})
|
|
.expect("buffer should have a project path");
|
|
|
|
cx.add_window(|window, cx| {
|
|
let multi_buffer = MultiBuffer::build_from_buffer(buffer.clone(), cx);
|
|
Editor::new(
|
|
EditorMode::full(),
|
|
multi_buffer,
|
|
Some(project.clone()),
|
|
window,
|
|
cx,
|
|
)
|
|
});
|
|
cx.run_until_parked();
|
|
|
|
let store = cx.update(|cx| ReplStore::global(cx));
|
|
assert!(!cx.update(|cx| store.read(cx).has_python_kernelspecs(worktree_id)));
|
|
|
|
buffer.update(cx, |buffer, cx| {
|
|
buffer.set_language(Some(python), cx);
|
|
});
|
|
cx.run_until_parked();
|
|
|
|
assert!(cx.update(|cx| store.read(cx).has_python_kernelspecs(worktree_id)));
|
|
}
|
|
}
|