1use std::{
2 sync::{Arc, Mutex},
3 time::Duration,
4};
5
6use anyhow::Context as _;
7use context_server::manager::{ContextServerManager, ContextServerStatus};
8use editor::{Editor, EditorElement, EditorStyle};
9use extension::ContextServerConfiguration;
10use gpui::{
11 Animation, AnimationExt, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Task,
12 TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, percentage,
13};
14use language::{Language, LanguageRegistry};
15use markdown::{Markdown, MarkdownElement, MarkdownStyle};
16use notifications::status_toast::{StatusToast, ToastIcon};
17use settings::{Settings as _, update_settings_file};
18use theme::ThemeSettings;
19use ui::{KeyBinding, Modal, ModalFooter, ModalHeader, Section, prelude::*};
20use util::ResultExt;
21use workspace::{ModalView, Workspace};
22
23pub(crate) struct ConfigureContextServerModal {
24 workspace: WeakEntity<Workspace>,
25 context_servers_to_setup: Vec<ConfigureContextServer>,
26 context_server_manager: Entity<ContextServerManager>,
27}
28
29struct ConfigureContextServer {
30 id: Arc<str>,
31 installation_instructions: Entity<markdown::Markdown>,
32 settings_validator: Option<jsonschema::Validator>,
33 settings_editor: Entity<Editor>,
34 last_error: Option<SharedString>,
35 waiting_for_context_server: bool,
36}
37
38impl ConfigureContextServerModal {
39 pub fn new(
40 configurations: impl Iterator<Item = (Arc<str>, ContextServerConfiguration)>,
41 jsonc_language: Option<Arc<Language>>,
42 context_server_manager: Entity<ContextServerManager>,
43 language_registry: Arc<LanguageRegistry>,
44 workspace: WeakEntity<Workspace>,
45 window: &mut Window,
46 cx: &mut App,
47 ) -> Option<Self> {
48 let context_servers_to_setup = configurations
49 .map(|(id, manifest)| {
50 let jsonc_language = jsonc_language.clone();
51 let settings_validator = jsonschema::validator_for(&manifest.settings_schema)
52 .context("Failed to load JSON schema for context server settings")
53 .log_err();
54 ConfigureContextServer {
55 id: id.clone(),
56 installation_instructions: cx.new(|cx| {
57 Markdown::new(
58 manifest.installation_instructions.clone().into(),
59 Some(language_registry.clone()),
60 None,
61 cx,
62 )
63 }),
64 settings_validator,
65 settings_editor: cx.new(|cx| {
66 let mut editor = Editor::auto_height(16, window, cx);
67 editor.set_text(manifest.default_settings.trim(), window, cx);
68 if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
69 buffer.update(cx, |buffer, cx| buffer.set_language(jsonc_language, cx))
70 }
71 editor
72 }),
73 waiting_for_context_server: false,
74 last_error: None,
75 }
76 })
77 .collect::<Vec<_>>();
78
79 if context_servers_to_setup.is_empty() {
80 return None;
81 }
82
83 Some(Self {
84 workspace,
85 context_servers_to_setup,
86 context_server_manager,
87 })
88 }
89}
90
91impl ConfigureContextServerModal {
92 pub fn confirm(&mut self, cx: &mut Context<Self>) {
93 if self.context_servers_to_setup.is_empty() {
94 return;
95 }
96
97 let Some(workspace) = self.workspace.upgrade() else {
98 return;
99 };
100
101 let configuration = &mut self.context_servers_to_setup[0];
102 if configuration.waiting_for_context_server {
103 return;
104 }
105
106 let settings_value = match serde_json_lenient::from_str::<serde_json::Value>(
107 &configuration.settings_editor.read(cx).text(cx),
108 ) {
109 Ok(value) => value,
110 Err(error) => {
111 configuration.last_error = Some(error.to_string().into());
112 cx.notify();
113 return;
114 }
115 };
116
117 if let Some(validator) = configuration.settings_validator.as_ref() {
118 if let Err(error) = validator.validate(&settings_value) {
119 configuration.last_error = Some(error.to_string().into());
120 cx.notify();
121 return;
122 }
123 }
124 let id = configuration.id.clone();
125
126 let settings_changed = context_server::ContextServerSettings::get_global(cx)
127 .context_servers
128 .get(&id)
129 .map_or(true, |config| {
130 config.settings.as_ref() != Some(&settings_value)
131 });
132
133 let is_running = self.context_server_manager.read(cx).status_for_server(&id)
134 == Some(ContextServerStatus::Running);
135
136 if !settings_changed && is_running {
137 self.complete_setup(id, cx);
138 return;
139 }
140
141 configuration.waiting_for_context_server = true;
142
143 let task = wait_for_context_server(&self.context_server_manager, id.clone(), cx);
144 cx.spawn({
145 let id = id.clone();
146 async move |this, cx| {
147 let result = task.await;
148 this.update(cx, |this, cx| match result {
149 Ok(_) => {
150 this.complete_setup(id, cx);
151 }
152 Err(err) => {
153 if let Some(configuration) = this.context_servers_to_setup.get_mut(0) {
154 configuration.last_error = Some(err.into());
155 configuration.waiting_for_context_server = false;
156 } else {
157 this.dismiss(cx);
158 }
159 cx.notify();
160 }
161 })
162 }
163 })
164 .detach();
165
166 // When we write the settings to the file, the context server will be restarted.
167 update_settings_file::<context_server::ContextServerSettings>(
168 workspace.read(cx).app_state().fs.clone(),
169 cx,
170 {
171 let id = id.clone();
172 |settings, _| {
173 if let Some(server_config) = settings.context_servers.get_mut(&id) {
174 server_config.settings = Some(settings_value);
175 } else {
176 settings.context_servers.insert(
177 id,
178 context_server::ServerConfig {
179 settings: Some(settings_value),
180 ..Default::default()
181 },
182 );
183 }
184 }
185 },
186 );
187 }
188
189 fn complete_setup(&mut self, id: Arc<str>, cx: &mut Context<Self>) {
190 self.context_servers_to_setup.remove(0);
191 cx.notify();
192
193 if !self.context_servers_to_setup.is_empty() {
194 return;
195 }
196
197 self.workspace
198 .update(cx, {
199 |workspace, cx| {
200 let status_toast = StatusToast::new(
201 format!("{} configured successfully.", id),
202 cx,
203 |this, _cx| {
204 this.icon(ToastIcon::new(IconName::Hammer).color(Color::Muted))
205 .action("Dismiss", |_, _| {})
206 },
207 );
208
209 workspace.toggle_status_toast(status_toast, cx);
210 }
211 })
212 .log_err();
213
214 self.dismiss(cx);
215 }
216
217 fn dismiss(&self, cx: &mut Context<Self>) {
218 cx.emit(DismissEvent);
219 }
220}
221
222fn wait_for_context_server(
223 context_server_manager: &Entity<ContextServerManager>,
224 context_server_id: Arc<str>,
225 cx: &mut App,
226) -> Task<Result<(), Arc<str>>> {
227 let (tx, rx) = futures::channel::oneshot::channel();
228 let tx = Arc::new(Mutex::new(Some(tx)));
229
230 let subscription = cx.subscribe(context_server_manager, move |_, event, _cx| match event {
231 context_server::manager::Event::ServerStatusChanged { server_id, status } => match status {
232 Some(ContextServerStatus::Running) => {
233 if server_id == &context_server_id {
234 if let Some(tx) = tx.lock().unwrap().take() {
235 let _ = tx.send(Ok(()));
236 }
237 }
238 }
239 Some(ContextServerStatus::Error(error)) => {
240 if server_id == &context_server_id {
241 if let Some(tx) = tx.lock().unwrap().take() {
242 let _ = tx.send(Err(error.clone()));
243 }
244 }
245 }
246 _ => {}
247 },
248 });
249
250 cx.spawn(async move |_cx| {
251 let result = rx.await.unwrap();
252 drop(subscription);
253 result
254 })
255}
256
257impl Render for ConfigureContextServerModal {
258 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
259 let Some(configuration) = self.context_servers_to_setup.first() else {
260 return div().child("No context servers to setup");
261 };
262
263 let focus_handle = self.focus_handle(cx);
264
265 div()
266 .elevation_3(cx)
267 .w(rems(34.))
268 .key_context("ConfigureContextServerModal")
269 .on_action(cx.listener(|this, _: &menu::Confirm, _window, cx| this.confirm(cx)))
270 .on_action(cx.listener(|this, _: &menu::Cancel, _window, cx| this.dismiss(cx)))
271 .capture_any_mouse_down(cx.listener(|this, _, window, cx| {
272 this.focus_handle(cx).focus(window);
273 }))
274 .child(
275 Modal::new("configure-context-server", None)
276 .header(ModalHeader::new().headline(format!("Configure {}", configuration.id)))
277 .section(
278 Section::new()
279 .child(div().pb_2().text_sm().child(MarkdownElement::new(
280 configuration.installation_instructions.clone(),
281 default_markdown_style(window, cx),
282 )))
283 .child(
284 div()
285 .p_2()
286 .rounded_md()
287 .border_1()
288 .border_color(cx.theme().colors().border_variant)
289 .bg(cx.theme().colors().editor_background)
290 .gap_1()
291 .child({
292 let settings = ThemeSettings::get_global(cx);
293 let text_style = TextStyle {
294 color: cx.theme().colors().text,
295 font_family: settings.buffer_font.family.clone(),
296 font_fallbacks: settings.buffer_font.fallbacks.clone(),
297 font_size: settings.buffer_font_size(cx).into(),
298 font_weight: settings.buffer_font.weight,
299 line_height: relative(
300 settings.buffer_line_height.value(),
301 ),
302 ..Default::default()
303 };
304 EditorElement::new(
305 &configuration.settings_editor,
306 EditorStyle {
307 background: cx.theme().colors().editor_background,
308 local_player: cx.theme().players().local(),
309 text: text_style,
310 syntax: cx.theme().syntax().clone(),
311 ..Default::default()
312 },
313 )
314 })
315 .when_some(configuration.last_error.clone(), |this, error| {
316 this.child(
317 h_flex()
318 .gap_2()
319 .px_2()
320 .py_1()
321 .child(
322 Icon::new(IconName::Warning)
323 .size(IconSize::XSmall)
324 .color(Color::Warning),
325 )
326 .child(
327 div().w_full().child(
328 Label::new(error)
329 .size(LabelSize::Small)
330 .color(Color::Muted),
331 ),
332 ),
333 )
334 }),
335 )
336 .when(configuration.waiting_for_context_server, |this| {
337 this.child(
338 h_flex()
339 .gap_1p5()
340 .child(
341 Icon::new(IconName::ArrowCircle)
342 .size(IconSize::XSmall)
343 .color(Color::Info)
344 .with_animation(
345 "arrow-circle",
346 Animation::new(Duration::from_secs(2)).repeat(),
347 |icon, delta| {
348 icon.transform(Transformation::rotate(
349 percentage(delta),
350 ))
351 },
352 )
353 .into_any_element(),
354 )
355 .child(
356 Label::new("Waiting for Context Server")
357 .size(LabelSize::Small)
358 .color(Color::Muted),
359 ),
360 )
361 }),
362 )
363 .footer(
364 ModalFooter::new().end_slot(
365 h_flex()
366 .gap_1()
367 .child(
368 Button::new("cancel", "Cancel")
369 .key_binding(
370 KeyBinding::for_action_in(
371 &menu::Cancel,
372 &focus_handle,
373 window,
374 cx,
375 )
376 .map(|kb| kb.size(rems_from_px(12.))),
377 )
378 .on_click(cx.listener(|this, _event, _window, cx| {
379 this.dismiss(cx)
380 })),
381 )
382 .child(
383 Button::new("configure-server", "Configure MCP")
384 .disabled(configuration.waiting_for_context_server)
385 .key_binding(
386 KeyBinding::for_action_in(
387 &menu::Confirm,
388 &focus_handle,
389 window,
390 cx,
391 )
392 .map(|kb| kb.size(rems_from_px(12.))),
393 )
394 .on_click(cx.listener(|this, _event, _window, cx| {
395 this.confirm(cx)
396 })),
397 ),
398 ),
399 ),
400 )
401 }
402}
403
404pub(crate) fn default_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
405 let theme_settings = ThemeSettings::get_global(cx);
406 let colors = cx.theme().colors();
407 let mut text_style = window.text_style();
408 text_style.refine(&TextStyleRefinement {
409 font_family: Some(theme_settings.ui_font.family.clone()),
410 font_fallbacks: theme_settings.ui_font.fallbacks.clone(),
411 font_features: Some(theme_settings.ui_font.features.clone()),
412 font_size: Some(TextSize::XSmall.rems(cx).into()),
413 color: Some(colors.text_muted),
414 ..Default::default()
415 });
416
417 MarkdownStyle {
418 base_text_style: text_style.clone(),
419 selection_background_color: cx.theme().players().local().selection,
420 link: TextStyleRefinement {
421 background_color: Some(colors.editor_foreground.opacity(0.025)),
422 underline: Some(UnderlineStyle {
423 color: Some(colors.text_accent.opacity(0.5)),
424 thickness: px(1.),
425 ..Default::default()
426 }),
427 ..Default::default()
428 },
429 ..Default::default()
430 }
431}
432
433impl ModalView for ConfigureContextServerModal {}
434impl EventEmitter<DismissEvent> for ConfigureContextServerModal {}
435impl Focusable for ConfigureContextServerModal {
436 fn focus_handle(&self, cx: &App) -> FocusHandle {
437 if let Some(current) = self.context_servers_to_setup.first() {
438 current.settings_editor.read(cx).focus_handle(cx)
439 } else {
440 cx.focus_handle()
441 }
442 }
443}