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