1pub mod extension_lsp_adapter;
2pub mod extension_settings;
3pub mod wasm_host;
4
5use crate::{extension_lsp_adapter::ExtensionLspAdapter, wasm_host::wit};
6use anyhow::{anyhow, bail, Context as _, Result};
7use async_compression::futures::bufread::GzipDecoder;
8use async_tar::Archive;
9use client::{telemetry::Telemetry, Client, ExtensionMetadata, GetExtensionsResponse};
10use collections::{btree_map, BTreeMap, HashSet};
11use extension::extension_builder::{CompileExtensionOptions, ExtensionBuilder};
12pub use extension::ExtensionManifest;
13use fs::{Fs, RemoveOptions};
14use futures::{
15 channel::{
16 mpsc::{unbounded, UnboundedSender},
17 oneshot,
18 },
19 io::BufReader,
20 select_biased, AsyncReadExt as _, Future, FutureExt as _, StreamExt as _,
21};
22use gpui::{
23 actions, AppContext, AsyncAppContext, Context, EventEmitter, Global, Model, ModelContext,
24 SharedString, Task, WeakModel,
25};
26use http_client::{AsyncBody, HttpClient, HttpClientWithUrl};
27use language::{
28 LanguageConfig, LanguageMatcher, LanguageName, LanguageQueries, LoadedLanguage,
29 QUERY_FILENAME_PREFIXES,
30};
31use node_runtime::NodeRuntime;
32use project::ContextProviderWithTasks;
33use release_channel::ReleaseChannel;
34use semantic_version::SemanticVersion;
35use serde::{Deserialize, Serialize};
36use settings::Settings;
37use std::ops::RangeInclusive;
38use std::str::FromStr;
39use std::{
40 cmp::Ordering,
41 path::{self, Path, PathBuf},
42 sync::Arc,
43 time::{Duration, Instant},
44};
45use url::Url;
46use util::ResultExt;
47use wasm_host::{
48 wit::{is_supported_wasm_api_version, wasm_api_version_range},
49 WasmExtension, WasmHost,
50};
51
52pub use extension::{
53 ExtensionLibraryKind, GrammarManifestEntry, OldExtensionManifest, SchemaVersion,
54};
55pub use extension_settings::ExtensionSettings;
56
57pub const RELOAD_DEBOUNCE_DURATION: Duration = Duration::from_millis(200);
58const FS_WATCH_LATENCY: Duration = Duration::from_millis(100);
59
60/// The current extension [`SchemaVersion`] supported by Zed.
61const CURRENT_SCHEMA_VERSION: SchemaVersion = SchemaVersion(1);
62
63/// Returns the [`SchemaVersion`] range that is compatible with this version of Zed.
64pub fn schema_version_range() -> RangeInclusive<SchemaVersion> {
65 SchemaVersion::ZERO..=CURRENT_SCHEMA_VERSION
66}
67
68/// Returns whether the given extension version is compatible with this version of Zed.
69pub fn is_version_compatible(
70 release_channel: ReleaseChannel,
71 extension_version: &ExtensionMetadata,
72) -> bool {
73 let schema_version = extension_version.manifest.schema_version.unwrap_or(0);
74 if CURRENT_SCHEMA_VERSION.0 < schema_version {
75 return false;
76 }
77
78 if let Some(wasm_api_version) = extension_version
79 .manifest
80 .wasm_api_version
81 .as_ref()
82 .and_then(|wasm_api_version| SemanticVersion::from_str(wasm_api_version).ok())
83 {
84 if !is_supported_wasm_api_version(release_channel, wasm_api_version) {
85 return false;
86 }
87 }
88
89 true
90}
91
92pub trait DocsDatabase: Send + Sync + 'static {
93 fn insert(&self, key: String, docs: String) -> Task<Result<()>>;
94}
95
96pub trait ExtensionRegistrationHooks: Send + Sync + 'static {
97 fn remove_user_themes(&self, _themes: Vec<SharedString>) {}
98
99 fn load_user_theme(&self, _theme_path: PathBuf, _fs: Arc<dyn Fs>) -> Task<Result<()>> {
100 Task::ready(Ok(()))
101 }
102
103 fn list_theme_names(
104 &self,
105 _theme_path: PathBuf,
106 _fs: Arc<dyn Fs>,
107 ) -> Task<Result<Vec<String>>> {
108 Task::ready(Ok(Vec::new()))
109 }
110
111 fn reload_current_theme(&self, _cx: &mut AppContext) {}
112
113 fn register_language(
114 &self,
115 _language: LanguageName,
116 _grammar: Option<Arc<str>>,
117 _matcher: language::LanguageMatcher,
118 _load: Arc<dyn Fn() -> Result<LoadedLanguage> + 'static + Send + Sync>,
119 ) {
120 }
121
122 fn register_lsp_adapter(&self, _language: LanguageName, _adapter: ExtensionLspAdapter) {}
123
124 fn remove_lsp_adapter(
125 &self,
126 _language: &LanguageName,
127 _server_name: &language::LanguageServerName,
128 ) {
129 }
130
131 fn register_wasm_grammars(&self, _grammars: Vec<(Arc<str>, PathBuf)>) {}
132
133 fn remove_languages(
134 &self,
135 _languages_to_remove: &[LanguageName],
136 _grammars_to_remove: &[Arc<str>],
137 ) {
138 }
139
140 fn register_slash_command(
141 &self,
142 _slash_command: wit::SlashCommand,
143 _extension: WasmExtension,
144 _host: Arc<WasmHost>,
145 ) {
146 }
147
148 fn register_context_server(
149 &self,
150 _id: Arc<str>,
151 _extension: WasmExtension,
152 _host: Arc<WasmHost>,
153 ) {
154 }
155
156 fn register_docs_provider(
157 &self,
158 _extension: WasmExtension,
159 _host: Arc<WasmHost>,
160 _provider_id: Arc<str>,
161 ) {
162 }
163
164 fn register_snippets(&self, _path: &PathBuf, _snippet_contents: &str) -> Result<()> {
165 Ok(())
166 }
167
168 fn update_lsp_status(
169 &self,
170 _server_name: language::LanguageServerName,
171 _status: language::LanguageServerBinaryStatus,
172 ) {
173 }
174}
175
176pub struct ExtensionStore {
177 pub registration_hooks: Arc<dyn ExtensionRegistrationHooks>,
178 pub builder: Arc<ExtensionBuilder>,
179 pub extension_index: ExtensionIndex,
180 pub fs: Arc<dyn Fs>,
181 pub http_client: Arc<HttpClientWithUrl>,
182 pub telemetry: Option<Arc<Telemetry>>,
183 pub reload_tx: UnboundedSender<Option<Arc<str>>>,
184 pub reload_complete_senders: Vec<oneshot::Sender<()>>,
185 pub installed_dir: PathBuf,
186 pub outstanding_operations: BTreeMap<Arc<str>, ExtensionOperation>,
187 pub index_path: PathBuf,
188 pub modified_extensions: HashSet<Arc<str>>,
189 pub wasm_host: Arc<WasmHost>,
190 pub wasm_extensions: Vec<(Arc<ExtensionManifest>, WasmExtension)>,
191 pub tasks: Vec<Task<()>>,
192}
193
194#[derive(Clone, Copy)]
195pub enum ExtensionOperation {
196 Upgrade,
197 Install,
198 Remove,
199}
200
201#[derive(Clone)]
202pub enum Event {
203 ExtensionsUpdated,
204 StartedReloading,
205 ExtensionInstalled(Arc<str>),
206 ExtensionFailedToLoad(Arc<str>),
207}
208
209impl EventEmitter<Event> for ExtensionStore {}
210
211struct GlobalExtensionStore(Model<ExtensionStore>);
212
213impl Global for GlobalExtensionStore {}
214
215#[derive(Debug, Deserialize, Serialize, Default, PartialEq, Eq)]
216pub struct ExtensionIndex {
217 pub extensions: BTreeMap<Arc<str>, ExtensionIndexEntry>,
218 pub themes: BTreeMap<Arc<str>, ExtensionIndexThemeEntry>,
219 pub languages: BTreeMap<LanguageName, ExtensionIndexLanguageEntry>,
220}
221
222#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
223pub struct ExtensionIndexEntry {
224 pub manifest: Arc<ExtensionManifest>,
225 pub dev: bool,
226}
227
228#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Deserialize, Serialize)]
229pub struct ExtensionIndexThemeEntry {
230 pub extension: Arc<str>,
231 pub path: PathBuf,
232}
233
234#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Deserialize, Serialize)]
235pub struct ExtensionIndexLanguageEntry {
236 pub extension: Arc<str>,
237 pub path: PathBuf,
238 pub matcher: LanguageMatcher,
239 pub grammar: Option<Arc<str>>,
240}
241
242actions!(zed, [ReloadExtensions]);
243
244pub fn init(
245 registration_hooks: Arc<dyn ExtensionRegistrationHooks>,
246 fs: Arc<dyn Fs>,
247 client: Arc<Client>,
248 node_runtime: NodeRuntime,
249 cx: &mut AppContext,
250) {
251 ExtensionSettings::register(cx);
252
253 let store = cx.new_model(move |cx| {
254 ExtensionStore::new(
255 paths::extensions_dir().clone(),
256 None,
257 registration_hooks,
258 fs,
259 client.http_client().clone(),
260 client.http_client().clone(),
261 Some(client.telemetry().clone()),
262 node_runtime,
263 cx,
264 )
265 });
266
267 cx.on_action(|_: &ReloadExtensions, cx| {
268 let store = cx.global::<GlobalExtensionStore>().0.clone();
269 store.update(cx, |store, cx| drop(store.reload(None, cx)));
270 });
271
272 cx.set_global(GlobalExtensionStore(store));
273}
274
275impl ExtensionStore {
276 pub fn try_global(cx: &AppContext) -> Option<Model<Self>> {
277 cx.try_global::<GlobalExtensionStore>()
278 .map(|store| store.0.clone())
279 }
280
281 pub fn global(cx: &AppContext) -> Model<Self> {
282 cx.global::<GlobalExtensionStore>().0.clone()
283 }
284
285 #[allow(clippy::too_many_arguments)]
286 pub fn new(
287 extensions_dir: PathBuf,
288 build_dir: Option<PathBuf>,
289 extension_api: Arc<dyn ExtensionRegistrationHooks>,
290 fs: Arc<dyn Fs>,
291 http_client: Arc<HttpClientWithUrl>,
292 builder_client: Arc<dyn HttpClient>,
293 telemetry: Option<Arc<Telemetry>>,
294 node_runtime: NodeRuntime,
295 cx: &mut ModelContext<Self>,
296 ) -> Self {
297 let work_dir = extensions_dir.join("work");
298 let build_dir = build_dir.unwrap_or_else(|| extensions_dir.join("build"));
299 let installed_dir = extensions_dir.join("installed");
300 let index_path = extensions_dir.join("index.json");
301
302 let (reload_tx, mut reload_rx) = unbounded();
303 let mut this = Self {
304 registration_hooks: extension_api.clone(),
305 extension_index: Default::default(),
306 installed_dir,
307 index_path,
308 builder: Arc::new(ExtensionBuilder::new(builder_client, build_dir)),
309 outstanding_operations: Default::default(),
310 modified_extensions: Default::default(),
311 reload_complete_senders: Vec::new(),
312 wasm_host: WasmHost::new(
313 fs.clone(),
314 http_client.clone(),
315 node_runtime,
316 extension_api,
317 work_dir,
318 cx,
319 ),
320 wasm_extensions: Vec::new(),
321 fs,
322 http_client,
323 telemetry,
324 reload_tx,
325 tasks: Vec::new(),
326 };
327
328 // The extensions store maintains an index file, which contains a complete
329 // list of the installed extensions and the resources that they provide.
330 // This index is loaded synchronously on startup.
331 let (index_content, index_metadata, extensions_metadata) =
332 cx.background_executor().block(async {
333 futures::join!(
334 this.fs.load(&this.index_path),
335 this.fs.metadata(&this.index_path),
336 this.fs.metadata(&this.installed_dir),
337 )
338 });
339
340 // Normally, there is no need to rebuild the index. But if the index file
341 // is invalid or is out-of-date according to the filesystem mtimes, then
342 // it must be asynchronously rebuilt.
343 let mut extension_index = ExtensionIndex::default();
344 let mut extension_index_needs_rebuild = true;
345 if let Ok(index_content) = index_content {
346 if let Some(index) = serde_json::from_str(&index_content).log_err() {
347 extension_index = index;
348 if let (Ok(Some(index_metadata)), Ok(Some(extensions_metadata))) =
349 (index_metadata, extensions_metadata)
350 {
351 if index_metadata.mtime > extensions_metadata.mtime {
352 extension_index_needs_rebuild = false;
353 }
354 }
355 }
356 }
357
358 // Immediately load all of the extensions in the initial manifest. If the
359 // index needs to be rebuild, then enqueue
360 let load_initial_extensions = this.extensions_updated(extension_index, cx);
361 let mut reload_future = None;
362 if extension_index_needs_rebuild {
363 reload_future = Some(this.reload(None, cx));
364 }
365
366 cx.spawn(|this, mut cx| async move {
367 if let Some(future) = reload_future {
368 future.await;
369 }
370 this.update(&mut cx, |this, cx| this.auto_install_extensions(cx))
371 .ok();
372 this.update(&mut cx, |this, cx| this.check_for_updates(cx))
373 .ok();
374 })
375 .detach();
376
377 // Perform all extension loading in a single task to ensure that we
378 // never attempt to simultaneously load/unload extensions from multiple
379 // parallel tasks.
380 this.tasks.push(cx.spawn(|this, mut cx| {
381 async move {
382 load_initial_extensions.await;
383
384 let mut index_changed = false;
385 let mut debounce_timer = cx
386 .background_executor()
387 .spawn(futures::future::pending())
388 .fuse();
389 loop {
390 select_biased! {
391 _ = debounce_timer => {
392 if index_changed {
393 let index = this
394 .update(&mut cx, |this, cx| this.rebuild_extension_index(cx))?
395 .await;
396 this.update(&mut cx, |this, cx| this.extensions_updated(index, cx))?
397 .await;
398 index_changed = false;
399 }
400 }
401 extension_id = reload_rx.next() => {
402 let Some(extension_id) = extension_id else { break; };
403 this.update(&mut cx, |this, _| {
404 this.modified_extensions.extend(extension_id);
405 })?;
406 index_changed = true;
407 debounce_timer = cx
408 .background_executor()
409 .timer(RELOAD_DEBOUNCE_DURATION)
410 .fuse();
411 }
412 }
413 }
414
415 anyhow::Ok(())
416 }
417 .map(drop)
418 }));
419
420 // Watch the installed extensions directory for changes. Whenever changes are
421 // detected, rebuild the extension index, and load/unload any extensions that
422 // have been added, removed, or modified.
423 this.tasks.push(cx.background_executor().spawn({
424 let fs = this.fs.clone();
425 let reload_tx = this.reload_tx.clone();
426 let installed_dir = this.installed_dir.clone();
427 async move {
428 let (mut paths, _) = fs.watch(&installed_dir, FS_WATCH_LATENCY).await;
429 while let Some(events) = paths.next().await {
430 for event in events {
431 let Ok(event_path) = event.path.strip_prefix(&installed_dir) else {
432 continue;
433 };
434
435 if let Some(path::Component::Normal(extension_dir_name)) =
436 event_path.components().next()
437 {
438 if let Some(extension_id) = extension_dir_name.to_str() {
439 reload_tx.unbounded_send(Some(extension_id.into())).ok();
440 }
441 }
442 }
443 }
444 }
445 }));
446
447 this
448 }
449
450 pub fn reload(
451 &mut self,
452 modified_extension: Option<Arc<str>>,
453 cx: &mut ModelContext<Self>,
454 ) -> impl Future<Output = ()> {
455 let (tx, rx) = oneshot::channel();
456 self.reload_complete_senders.push(tx);
457 self.reload_tx
458 .unbounded_send(modified_extension)
459 .expect("reload task exited");
460 cx.emit(Event::StartedReloading);
461
462 async move {
463 rx.await.ok();
464 }
465 }
466
467 fn extensions_dir(&self) -> PathBuf {
468 self.installed_dir.clone()
469 }
470
471 pub fn outstanding_operations(&self) -> &BTreeMap<Arc<str>, ExtensionOperation> {
472 &self.outstanding_operations
473 }
474
475 pub fn installed_extensions(&self) -> &BTreeMap<Arc<str>, ExtensionIndexEntry> {
476 &self.extension_index.extensions
477 }
478
479 pub fn dev_extensions(&self) -> impl Iterator<Item = &Arc<ExtensionManifest>> {
480 self.extension_index
481 .extensions
482 .values()
483 .filter_map(|extension| extension.dev.then_some(&extension.manifest))
484 }
485
486 /// Returns the names of themes provided by extensions.
487 pub fn extension_themes<'a>(
488 &'a self,
489 extension_id: &'a str,
490 ) -> impl Iterator<Item = &'a Arc<str>> {
491 self.extension_index
492 .themes
493 .iter()
494 .filter_map(|(name, theme)| theme.extension.as_ref().eq(extension_id).then_some(name))
495 }
496
497 pub fn fetch_extensions(
498 &self,
499 search: Option<&str>,
500 cx: &mut ModelContext<Self>,
501 ) -> Task<Result<Vec<ExtensionMetadata>>> {
502 let version = CURRENT_SCHEMA_VERSION.to_string();
503 let mut query = vec![("max_schema_version", version.as_str())];
504 if let Some(search) = search {
505 query.push(("filter", search));
506 }
507
508 self.fetch_extensions_from_api("/extensions", &query, cx)
509 }
510
511 pub fn fetch_extensions_with_update_available(
512 &mut self,
513 cx: &mut ModelContext<Self>,
514 ) -> Task<Result<Vec<ExtensionMetadata>>> {
515 let schema_versions = schema_version_range();
516 let wasm_api_versions = wasm_api_version_range(ReleaseChannel::global(cx));
517 let extension_settings = ExtensionSettings::get_global(cx);
518 let extension_ids = self
519 .extension_index
520 .extensions
521 .iter()
522 .filter(|(id, entry)| !entry.dev && extension_settings.should_auto_update(id))
523 .map(|(id, _)| id.as_ref())
524 .collect::<Vec<_>>()
525 .join(",");
526 let task = self.fetch_extensions_from_api(
527 "/extensions/updates",
528 &[
529 ("min_schema_version", &schema_versions.start().to_string()),
530 ("max_schema_version", &schema_versions.end().to_string()),
531 (
532 "min_wasm_api_version",
533 &wasm_api_versions.start().to_string(),
534 ),
535 ("max_wasm_api_version", &wasm_api_versions.end().to_string()),
536 ("ids", &extension_ids),
537 ],
538 cx,
539 );
540 cx.spawn(move |this, mut cx| async move {
541 let extensions = task.await?;
542 this.update(&mut cx, |this, _cx| {
543 extensions
544 .into_iter()
545 .filter(|extension| {
546 this.extension_index.extensions.get(&extension.id).map_or(
547 true,
548 |installed_extension| {
549 installed_extension.manifest.version != extension.manifest.version
550 },
551 )
552 })
553 .collect()
554 })
555 })
556 }
557
558 pub fn fetch_extension_versions(
559 &self,
560 extension_id: &str,
561 cx: &mut ModelContext<Self>,
562 ) -> Task<Result<Vec<ExtensionMetadata>>> {
563 self.fetch_extensions_from_api(&format!("/extensions/{extension_id}"), &[], cx)
564 }
565
566 /// Installs any extensions that should be included with Zed by default.
567 ///
568 /// This can be used to make certain functionality provided by extensions
569 /// available out-of-the-box.
570 pub fn auto_install_extensions(&mut self, cx: &mut ModelContext<Self>) {
571 let extension_settings = ExtensionSettings::get_global(cx);
572
573 let extensions_to_install = extension_settings
574 .auto_install_extensions
575 .keys()
576 .filter(|extension_id| extension_settings.should_auto_install(extension_id))
577 .filter(|extension_id| {
578 let is_already_installed = self
579 .extension_index
580 .extensions
581 .contains_key(extension_id.as_ref());
582 !is_already_installed
583 })
584 .cloned()
585 .collect::<Vec<_>>();
586
587 cx.spawn(move |this, mut cx| async move {
588 for extension_id in extensions_to_install {
589 this.update(&mut cx, |this, cx| {
590 this.install_latest_extension(extension_id.clone(), cx);
591 })
592 .ok();
593 }
594 })
595 .detach();
596 }
597
598 pub fn check_for_updates(&mut self, cx: &mut ModelContext<Self>) {
599 let task = self.fetch_extensions_with_update_available(cx);
600 cx.spawn(move |this, mut cx| async move {
601 Self::upgrade_extensions(this, task.await?, &mut cx).await
602 })
603 .detach();
604 }
605
606 async fn upgrade_extensions(
607 this: WeakModel<Self>,
608 extensions: Vec<ExtensionMetadata>,
609 cx: &mut AsyncAppContext,
610 ) -> Result<()> {
611 for extension in extensions {
612 let task = this.update(cx, |this, cx| {
613 if let Some(installed_extension) =
614 this.extension_index.extensions.get(&extension.id)
615 {
616 let installed_version =
617 SemanticVersion::from_str(&installed_extension.manifest.version).ok()?;
618 let latest_version =
619 SemanticVersion::from_str(&extension.manifest.version).ok()?;
620
621 if installed_version >= latest_version {
622 return None;
623 }
624 }
625
626 Some(this.upgrade_extension(extension.id, extension.manifest.version, cx))
627 })?;
628
629 if let Some(task) = task {
630 task.await.log_err();
631 }
632 }
633 anyhow::Ok(())
634 }
635
636 fn fetch_extensions_from_api(
637 &self,
638 path: &str,
639 query: &[(&str, &str)],
640 cx: &mut ModelContext<'_, ExtensionStore>,
641 ) -> Task<Result<Vec<ExtensionMetadata>>> {
642 let url = self.http_client.build_zed_api_url(path, query);
643 let http_client = self.http_client.clone();
644 cx.spawn(move |_, _| async move {
645 let mut response = http_client
646 .get(url?.as_ref(), AsyncBody::empty(), true)
647 .await?;
648
649 let mut body = Vec::new();
650 response
651 .body_mut()
652 .read_to_end(&mut body)
653 .await
654 .context("error reading extensions")?;
655
656 if response.status().is_client_error() {
657 let text = String::from_utf8_lossy(body.as_slice());
658 bail!(
659 "status error {}, response: {text:?}",
660 response.status().as_u16()
661 );
662 }
663
664 let response: GetExtensionsResponse = serde_json::from_slice(&body)?;
665 Ok(response.data)
666 })
667 }
668
669 pub fn install_extension(
670 &mut self,
671 extension_id: Arc<str>,
672 version: Arc<str>,
673 cx: &mut ModelContext<Self>,
674 ) {
675 self.install_or_upgrade_extension(extension_id, version, ExtensionOperation::Install, cx)
676 .detach_and_log_err(cx);
677 }
678
679 fn install_or_upgrade_extension_at_endpoint(
680 &mut self,
681 extension_id: Arc<str>,
682 url: Url,
683 operation: ExtensionOperation,
684 cx: &mut ModelContext<Self>,
685 ) -> Task<Result<()>> {
686 let extension_dir = self.installed_dir.join(extension_id.as_ref());
687 let http_client = self.http_client.clone();
688 let fs = self.fs.clone();
689
690 match self.outstanding_operations.entry(extension_id.clone()) {
691 btree_map::Entry::Occupied(_) => return Task::ready(Ok(())),
692 btree_map::Entry::Vacant(e) => e.insert(operation),
693 };
694 cx.notify();
695
696 cx.spawn(move |this, mut cx| async move {
697 let _finish = util::defer({
698 let this = this.clone();
699 let mut cx = cx.clone();
700 let extension_id = extension_id.clone();
701 move || {
702 this.update(&mut cx, |this, cx| {
703 this.outstanding_operations.remove(extension_id.as_ref());
704 cx.notify();
705 })
706 .ok();
707 }
708 });
709
710 let mut response = http_client
711 .get(url.as_ref(), Default::default(), true)
712 .await
713 .map_err(|err| anyhow!("error downloading extension: {}", err))?;
714
715 fs.remove_dir(
716 &extension_dir,
717 RemoveOptions {
718 recursive: true,
719 ignore_if_not_exists: true,
720 },
721 )
722 .await?;
723
724 let content_length = response
725 .headers()
726 .get(http_client::http::header::CONTENT_LENGTH)
727 .and_then(|value| value.to_str().ok()?.parse::<usize>().ok());
728
729 let mut body = BufReader::new(response.body_mut());
730 let mut tar_gz_bytes = Vec::new();
731 body.read_to_end(&mut tar_gz_bytes).await?;
732
733 if let Some(content_length) = content_length {
734 let actual_len = tar_gz_bytes.len();
735 if content_length != actual_len {
736 bail!("downloaded extension size {actual_len} does not match content length {content_length}");
737 }
738 }
739 let decompressed_bytes = GzipDecoder::new(BufReader::new(tar_gz_bytes.as_slice()));
740 let archive = Archive::new(decompressed_bytes);
741 archive.unpack(extension_dir).await?;
742 this.update(&mut cx, |this, cx| {
743 this.reload(Some(extension_id.clone()), cx)
744 })?
745 .await;
746
747 if let ExtensionOperation::Install = operation {
748 this.update(&mut cx, |_, cx| {
749 cx.emit(Event::ExtensionInstalled(extension_id));
750 })
751 .ok();
752 }
753
754 anyhow::Ok(())
755 })
756 }
757
758 pub fn install_latest_extension(
759 &mut self,
760 extension_id: Arc<str>,
761 cx: &mut ModelContext<Self>,
762 ) {
763 log::info!("installing extension {extension_id} latest version");
764
765 let schema_versions = schema_version_range();
766 let wasm_api_versions = wasm_api_version_range(ReleaseChannel::global(cx));
767
768 let Some(url) = self
769 .http_client
770 .build_zed_api_url(
771 &format!("/extensions/{extension_id}/download"),
772 &[
773 ("min_schema_version", &schema_versions.start().to_string()),
774 ("max_schema_version", &schema_versions.end().to_string()),
775 (
776 "min_wasm_api_version",
777 &wasm_api_versions.start().to_string(),
778 ),
779 ("max_wasm_api_version", &wasm_api_versions.end().to_string()),
780 ],
781 )
782 .log_err()
783 else {
784 return;
785 };
786
787 self.install_or_upgrade_extension_at_endpoint(
788 extension_id,
789 url,
790 ExtensionOperation::Install,
791 cx,
792 )
793 .detach_and_log_err(cx);
794 }
795
796 pub fn upgrade_extension(
797 &mut self,
798 extension_id: Arc<str>,
799 version: Arc<str>,
800 cx: &mut ModelContext<Self>,
801 ) -> Task<Result<()>> {
802 self.install_or_upgrade_extension(extension_id, version, ExtensionOperation::Upgrade, cx)
803 }
804
805 fn install_or_upgrade_extension(
806 &mut self,
807 extension_id: Arc<str>,
808 version: Arc<str>,
809 operation: ExtensionOperation,
810 cx: &mut ModelContext<Self>,
811 ) -> Task<Result<()>> {
812 log::info!("installing extension {extension_id} {version}");
813 let Some(url) = self
814 .http_client
815 .build_zed_api_url(
816 &format!("/extensions/{extension_id}/{version}/download"),
817 &[],
818 )
819 .log_err()
820 else {
821 return Task::ready(Ok(()));
822 };
823
824 self.install_or_upgrade_extension_at_endpoint(extension_id, url, operation, cx)
825 }
826
827 pub fn uninstall_extension(&mut self, extension_id: Arc<str>, cx: &mut ModelContext<Self>) {
828 let extension_dir = self.installed_dir.join(extension_id.as_ref());
829 let work_dir = self.wasm_host.work_dir.join(extension_id.as_ref());
830 let fs = self.fs.clone();
831
832 match self.outstanding_operations.entry(extension_id.clone()) {
833 btree_map::Entry::Occupied(_) => return,
834 btree_map::Entry::Vacant(e) => e.insert(ExtensionOperation::Remove),
835 };
836
837 cx.spawn(move |this, mut cx| async move {
838 let _finish = util::defer({
839 let this = this.clone();
840 let mut cx = cx.clone();
841 let extension_id = extension_id.clone();
842 move || {
843 this.update(&mut cx, |this, cx| {
844 this.outstanding_operations.remove(extension_id.as_ref());
845 cx.notify();
846 })
847 .ok();
848 }
849 });
850
851 fs.remove_dir(
852 &work_dir,
853 RemoveOptions {
854 recursive: true,
855 ignore_if_not_exists: true,
856 },
857 )
858 .await?;
859
860 fs.remove_dir(
861 &extension_dir,
862 RemoveOptions {
863 recursive: true,
864 ignore_if_not_exists: true,
865 },
866 )
867 .await?;
868
869 this.update(&mut cx, |this, cx| this.reload(None, cx))?
870 .await;
871 anyhow::Ok(())
872 })
873 .detach_and_log_err(cx)
874 }
875
876 pub fn install_dev_extension(
877 &mut self,
878 extension_source_path: PathBuf,
879 cx: &mut ModelContext<Self>,
880 ) -> Task<Result<()>> {
881 let extensions_dir = self.extensions_dir();
882 let fs = self.fs.clone();
883 let builder = self.builder.clone();
884
885 cx.spawn(move |this, mut cx| async move {
886 let mut extension_manifest =
887 ExtensionManifest::load(fs.clone(), &extension_source_path).await?;
888 let extension_id = extension_manifest.id.clone();
889
890 if !this.update(&mut cx, |this, cx| {
891 match this.outstanding_operations.entry(extension_id.clone()) {
892 btree_map::Entry::Occupied(_) => return false,
893 btree_map::Entry::Vacant(e) => e.insert(ExtensionOperation::Remove),
894 };
895 cx.notify();
896 true
897 })? {
898 return Ok(());
899 }
900
901 let _finish = util::defer({
902 let this = this.clone();
903 let mut cx = cx.clone();
904 let extension_id = extension_id.clone();
905 move || {
906 this.update(&mut cx, |this, cx| {
907 this.outstanding_operations.remove(extension_id.as_ref());
908 cx.notify();
909 })
910 .ok();
911 }
912 });
913
914 cx.background_executor()
915 .spawn({
916 let extension_source_path = extension_source_path.clone();
917 async move {
918 builder
919 .compile_extension(
920 &extension_source_path,
921 &mut extension_manifest,
922 CompileExtensionOptions { release: false },
923 )
924 .await
925 }
926 })
927 .await?;
928
929 let output_path = &extensions_dir.join(extension_id.as_ref());
930 if let Some(metadata) = fs.metadata(output_path).await? {
931 if metadata.is_symlink {
932 fs.remove_file(
933 output_path,
934 RemoveOptions {
935 recursive: false,
936 ignore_if_not_exists: true,
937 },
938 )
939 .await?;
940 } else {
941 bail!("extension {extension_id} is already installed");
942 }
943 }
944
945 fs.create_symlink(output_path, extension_source_path)
946 .await?;
947
948 this.update(&mut cx, |this, cx| this.reload(None, cx))?
949 .await;
950 Ok(())
951 })
952 }
953
954 pub fn rebuild_dev_extension(&mut self, extension_id: Arc<str>, cx: &mut ModelContext<Self>) {
955 let path = self.installed_dir.join(extension_id.as_ref());
956 let builder = self.builder.clone();
957 let fs = self.fs.clone();
958
959 match self.outstanding_operations.entry(extension_id.clone()) {
960 btree_map::Entry::Occupied(_) => return,
961 btree_map::Entry::Vacant(e) => e.insert(ExtensionOperation::Upgrade),
962 };
963
964 cx.notify();
965 let compile = cx.background_executor().spawn(async move {
966 let mut manifest = ExtensionManifest::load(fs, &path).await?;
967 builder
968 .compile_extension(
969 &path,
970 &mut manifest,
971 CompileExtensionOptions { release: true },
972 )
973 .await
974 });
975
976 cx.spawn(|this, mut cx| async move {
977 let result = compile.await;
978
979 this.update(&mut cx, |this, cx| {
980 this.outstanding_operations.remove(&extension_id);
981 cx.notify();
982 })?;
983
984 if result.is_ok() {
985 this.update(&mut cx, |this, cx| this.reload(Some(extension_id), cx))?
986 .await;
987 }
988
989 result
990 })
991 .detach_and_log_err(cx)
992 }
993
994 /// Updates the set of installed extensions.
995 ///
996 /// First, this unloads any themes, languages, or grammars that are
997 /// no longer in the manifest, or whose files have changed on disk.
998 /// Then it loads any themes, languages, or grammars that are newly
999 /// added to the manifest, or whose files have changed on disk.
1000 fn extensions_updated(
1001 &mut self,
1002 new_index: ExtensionIndex,
1003 cx: &mut ModelContext<Self>,
1004 ) -> Task<()> {
1005 let old_index = &self.extension_index;
1006
1007 // Determine which extensions need to be loaded and unloaded, based
1008 // on the changes to the manifest and the extensions that we know have been
1009 // modified.
1010 let mut extensions_to_unload = Vec::default();
1011 let mut extensions_to_load = Vec::default();
1012 {
1013 let mut old_keys = old_index.extensions.iter().peekable();
1014 let mut new_keys = new_index.extensions.iter().peekable();
1015 loop {
1016 match (old_keys.peek(), new_keys.peek()) {
1017 (None, None) => break,
1018 (None, Some(_)) => {
1019 extensions_to_load.push(new_keys.next().unwrap().0.clone());
1020 }
1021 (Some(_), None) => {
1022 extensions_to_unload.push(old_keys.next().unwrap().0.clone());
1023 }
1024 (Some((old_key, _)), Some((new_key, _))) => match old_key.cmp(new_key) {
1025 Ordering::Equal => {
1026 let (old_key, old_value) = old_keys.next().unwrap();
1027 let (new_key, new_value) = new_keys.next().unwrap();
1028 if old_value != new_value || self.modified_extensions.contains(old_key)
1029 {
1030 extensions_to_unload.push(old_key.clone());
1031 extensions_to_load.push(new_key.clone());
1032 }
1033 }
1034 Ordering::Less => {
1035 extensions_to_unload.push(old_keys.next().unwrap().0.clone());
1036 }
1037 Ordering::Greater => {
1038 extensions_to_load.push(new_keys.next().unwrap().0.clone());
1039 }
1040 },
1041 }
1042 }
1043 self.modified_extensions.clear();
1044 }
1045
1046 if extensions_to_load.is_empty() && extensions_to_unload.is_empty() {
1047 return Task::ready(());
1048 }
1049
1050 let reload_count = extensions_to_unload
1051 .iter()
1052 .filter(|id| extensions_to_load.contains(id))
1053 .count();
1054
1055 log::info!(
1056 "extensions updated. loading {}, reloading {}, unloading {}",
1057 extensions_to_load.len() - reload_count,
1058 reload_count,
1059 extensions_to_unload.len() - reload_count
1060 );
1061
1062 if let Some(telemetry) = &self.telemetry {
1063 for extension_id in &extensions_to_load {
1064 if let Some(extension) = new_index.extensions.get(extension_id) {
1065 telemetry.report_extension_event(
1066 extension_id.clone(),
1067 extension.manifest.version.clone(),
1068 );
1069 }
1070 }
1071 }
1072
1073 let themes_to_remove = old_index
1074 .themes
1075 .iter()
1076 .filter_map(|(name, entry)| {
1077 if extensions_to_unload.contains(&entry.extension) {
1078 Some(name.clone().into())
1079 } else {
1080 None
1081 }
1082 })
1083 .collect::<Vec<_>>();
1084 let languages_to_remove = old_index
1085 .languages
1086 .iter()
1087 .filter_map(|(name, entry)| {
1088 if extensions_to_unload.contains(&entry.extension) {
1089 Some(name.clone())
1090 } else {
1091 None
1092 }
1093 })
1094 .collect::<Vec<_>>();
1095 let mut grammars_to_remove = Vec::new();
1096 for extension_id in &extensions_to_unload {
1097 let Some(extension) = old_index.extensions.get(extension_id) else {
1098 continue;
1099 };
1100 grammars_to_remove.extend(extension.manifest.grammars.keys().cloned());
1101 for (language_server_name, config) in extension.manifest.language_servers.iter() {
1102 for language in config.languages() {
1103 self.registration_hooks
1104 .remove_lsp_adapter(&language, language_server_name);
1105 }
1106 }
1107 }
1108
1109 self.wasm_extensions
1110 .retain(|(extension, _)| !extensions_to_unload.contains(&extension.id));
1111 self.registration_hooks.remove_user_themes(themes_to_remove);
1112 self.registration_hooks
1113 .remove_languages(&languages_to_remove, &grammars_to_remove);
1114
1115 let languages_to_add = new_index
1116 .languages
1117 .iter()
1118 .filter(|(_, entry)| extensions_to_load.contains(&entry.extension))
1119 .collect::<Vec<_>>();
1120 let mut grammars_to_add = Vec::new();
1121 let mut themes_to_add = Vec::new();
1122 let mut snippets_to_add = Vec::new();
1123 for extension_id in &extensions_to_load {
1124 let Some(extension) = new_index.extensions.get(extension_id) else {
1125 continue;
1126 };
1127
1128 grammars_to_add.extend(extension.manifest.grammars.keys().map(|grammar_name| {
1129 let mut grammar_path = self.installed_dir.clone();
1130 grammar_path.extend([extension_id.as_ref(), "grammars"]);
1131 grammar_path.push(grammar_name.as_ref());
1132 grammar_path.set_extension("wasm");
1133 (grammar_name.clone(), grammar_path)
1134 }));
1135 themes_to_add.extend(extension.manifest.themes.iter().map(|theme_path| {
1136 let mut path = self.installed_dir.clone();
1137 path.extend([Path::new(extension_id.as_ref()), theme_path.as_path()]);
1138 path
1139 }));
1140 snippets_to_add.extend(extension.manifest.snippets.iter().map(|snippets_path| {
1141 let mut path = self.installed_dir.clone();
1142 path.extend([Path::new(extension_id.as_ref()), snippets_path.as_path()]);
1143 path
1144 }));
1145 }
1146
1147 self.registration_hooks
1148 .register_wasm_grammars(grammars_to_add);
1149
1150 for (language_name, language) in languages_to_add {
1151 let mut language_path = self.installed_dir.clone();
1152 language_path.extend([
1153 Path::new(language.extension.as_ref()),
1154 language.path.as_path(),
1155 ]);
1156 self.registration_hooks.register_language(
1157 language_name.clone(),
1158 language.grammar.clone(),
1159 language.matcher.clone(),
1160 Arc::new(move || {
1161 let config = std::fs::read_to_string(language_path.join("config.toml"))?;
1162 let config: LanguageConfig = ::toml::from_str(&config)?;
1163 let queries = load_plugin_queries(&language_path);
1164 let context_provider =
1165 std::fs::read_to_string(language_path.join("tasks.json"))
1166 .ok()
1167 .and_then(|contents| {
1168 let definitions =
1169 serde_json_lenient::from_str(&contents).log_err()?;
1170 Some(Arc::new(ContextProviderWithTasks::new(definitions)) as Arc<_>)
1171 });
1172
1173 Ok(LoadedLanguage {
1174 config,
1175 queries,
1176 context_provider,
1177 toolchain_provider: None,
1178 })
1179 }),
1180 );
1181 }
1182
1183 let fs = self.fs.clone();
1184 let wasm_host = self.wasm_host.clone();
1185 let root_dir = self.installed_dir.clone();
1186 let api = self.registration_hooks.clone();
1187 let extension_entries = extensions_to_load
1188 .iter()
1189 .filter_map(|name| new_index.extensions.get(name).cloned())
1190 .collect::<Vec<_>>();
1191
1192 self.extension_index = new_index;
1193 cx.notify();
1194 cx.emit(Event::ExtensionsUpdated);
1195
1196 cx.spawn(|this, mut cx| async move {
1197 cx.background_executor()
1198 .spawn({
1199 let fs = fs.clone();
1200 async move {
1201 for theme_path in themes_to_add.into_iter() {
1202 api.load_user_theme(theme_path, fs.clone()).await.log_err();
1203 }
1204
1205 for snippets_path in &snippets_to_add {
1206 if let Some(snippets_contents) = fs.load(snippets_path).await.log_err()
1207 {
1208 api.register_snippets(snippets_path, &snippets_contents)
1209 .log_err();
1210 }
1211 }
1212 }
1213 })
1214 .await;
1215
1216 let mut wasm_extensions = Vec::new();
1217 for extension in extension_entries {
1218 if extension.manifest.lib.kind.is_none() {
1219 continue;
1220 };
1221
1222 let extension_path = root_dir.join(extension.manifest.id.as_ref());
1223 let wasm_extension = WasmExtension::load(
1224 extension_path,
1225 &extension.manifest,
1226 wasm_host.clone(),
1227 &cx,
1228 )
1229 .await;
1230
1231 if let Some(wasm_extension) = wasm_extension.log_err() {
1232 wasm_extensions.push((extension.manifest.clone(), wasm_extension));
1233 } else {
1234 this.update(&mut cx, |_, cx| {
1235 cx.emit(Event::ExtensionFailedToLoad(extension.manifest.id.clone()))
1236 })
1237 .ok();
1238 }
1239 }
1240
1241 this.update(&mut cx, |this, cx| {
1242 this.reload_complete_senders.clear();
1243
1244 for (manifest, wasm_extension) in &wasm_extensions {
1245 for (language_server_id, language_server_config) in &manifest.language_servers {
1246 for language in language_server_config.languages() {
1247 this.registration_hooks.register_lsp_adapter(
1248 language.clone(),
1249 ExtensionLspAdapter {
1250 extension: wasm_extension.clone(),
1251 host: this.wasm_host.clone(),
1252 language_server_id: language_server_id.clone(),
1253 config: wit::LanguageServerConfig {
1254 name: language_server_id.0.to_string(),
1255 language_name: language.to_string(),
1256 },
1257 },
1258 );
1259 }
1260 }
1261
1262 for (slash_command_name, slash_command) in &manifest.slash_commands {
1263 this.registration_hooks.register_slash_command(
1264 crate::wit::SlashCommand {
1265 name: slash_command_name.to_string(),
1266 description: slash_command.description.to_string(),
1267 // We don't currently expose this as a configurable option, as it currently drives
1268 // the `menu_text` on the `SlashCommand` trait, which is not used for slash commands
1269 // defined in extensions, as they are not able to be added to the menu.
1270 tooltip_text: String::new(),
1271 requires_argument: slash_command.requires_argument,
1272 },
1273 wasm_extension.clone(),
1274 this.wasm_host.clone(),
1275 );
1276 }
1277
1278 for (id, _context_server_entry) in &manifest.context_servers {
1279 this.registration_hooks.register_context_server(
1280 id.clone(),
1281 wasm_extension.clone(),
1282 this.wasm_host.clone(),
1283 );
1284 }
1285
1286 for (provider_id, _provider) in &manifest.indexed_docs_providers {
1287 this.registration_hooks.register_docs_provider(
1288 wasm_extension.clone(),
1289 this.wasm_host.clone(),
1290 provider_id.clone(),
1291 );
1292 }
1293 }
1294
1295 this.wasm_extensions.extend(wasm_extensions);
1296 this.registration_hooks.reload_current_theme(cx);
1297 })
1298 .ok();
1299 })
1300 }
1301
1302 fn rebuild_extension_index(&self, cx: &mut ModelContext<Self>) -> Task<ExtensionIndex> {
1303 let fs = self.fs.clone();
1304 let work_dir = self.wasm_host.work_dir.clone();
1305 let extensions_dir = self.installed_dir.clone();
1306 let index_path = self.index_path.clone();
1307 let extension_api = self.registration_hooks.clone();
1308 cx.background_executor().spawn(async move {
1309 let start_time = Instant::now();
1310 let mut index = ExtensionIndex::default();
1311
1312 fs.create_dir(&work_dir).await.log_err();
1313 fs.create_dir(&extensions_dir).await.log_err();
1314
1315 let extension_paths = fs.read_dir(&extensions_dir).await;
1316 if let Ok(mut extension_paths) = extension_paths {
1317 while let Some(extension_dir) = extension_paths.next().await {
1318 let Ok(extension_dir) = extension_dir else {
1319 continue;
1320 };
1321
1322 if extension_dir
1323 .file_name()
1324 .map_or(false, |file_name| file_name == ".DS_Store")
1325 {
1326 continue;
1327 }
1328
1329 Self::add_extension_to_index(
1330 fs.clone(),
1331 extension_dir,
1332 &mut index,
1333 extension_api.clone(),
1334 )
1335 .await
1336 .log_err();
1337 }
1338 }
1339
1340 if let Ok(index_json) = serde_json::to_string_pretty(&index) {
1341 fs.save(&index_path, &index_json.as_str().into(), Default::default())
1342 .await
1343 .context("failed to save extension index")
1344 .log_err();
1345 }
1346
1347 log::info!("rebuilt extension index in {:?}", start_time.elapsed());
1348 index
1349 })
1350 }
1351
1352 async fn add_extension_to_index(
1353 fs: Arc<dyn Fs>,
1354 extension_dir: PathBuf,
1355 index: &mut ExtensionIndex,
1356 extension_api: Arc<dyn ExtensionRegistrationHooks>,
1357 ) -> Result<()> {
1358 let mut extension_manifest = ExtensionManifest::load(fs.clone(), &extension_dir).await?;
1359 let extension_id = extension_manifest.id.clone();
1360
1361 // TODO: distinguish dev extensions more explicitly, by the absence
1362 // of a checksum file that we'll create when downloading normal extensions.
1363 let is_dev = fs
1364 .metadata(&extension_dir)
1365 .await?
1366 .ok_or_else(|| anyhow!("directory does not exist"))?
1367 .is_symlink;
1368
1369 if let Ok(mut language_paths) = fs.read_dir(&extension_dir.join("languages")).await {
1370 while let Some(language_path) = language_paths.next().await {
1371 let language_path = language_path?;
1372 let Ok(relative_path) = language_path.strip_prefix(&extension_dir) else {
1373 continue;
1374 };
1375 let Ok(Some(fs_metadata)) = fs.metadata(&language_path).await else {
1376 continue;
1377 };
1378 if !fs_metadata.is_dir {
1379 continue;
1380 }
1381 let config = fs.load(&language_path.join("config.toml")).await?;
1382 let config = ::toml::from_str::<LanguageConfig>(&config)?;
1383
1384 let relative_path = relative_path.to_path_buf();
1385 if !extension_manifest.languages.contains(&relative_path) {
1386 extension_manifest.languages.push(relative_path.clone());
1387 }
1388
1389 index.languages.insert(
1390 config.name.clone(),
1391 ExtensionIndexLanguageEntry {
1392 extension: extension_id.clone(),
1393 path: relative_path,
1394 matcher: config.matcher,
1395 grammar: config.grammar,
1396 },
1397 );
1398 }
1399 }
1400
1401 if let Ok(mut theme_paths) = fs.read_dir(&extension_dir.join("themes")).await {
1402 while let Some(theme_path) = theme_paths.next().await {
1403 let theme_path = theme_path?;
1404 let Ok(relative_path) = theme_path.strip_prefix(&extension_dir) else {
1405 continue;
1406 };
1407
1408 let Some(theme_families) = extension_api
1409 .list_theme_names(theme_path.clone(), fs.clone())
1410 .await
1411 .log_err()
1412 else {
1413 continue;
1414 };
1415
1416 let relative_path = relative_path.to_path_buf();
1417 if !extension_manifest.themes.contains(&relative_path) {
1418 extension_manifest.themes.push(relative_path.clone());
1419 }
1420
1421 for theme_name in theme_families {
1422 index.themes.insert(
1423 theme_name.into(),
1424 ExtensionIndexThemeEntry {
1425 extension: extension_id.clone(),
1426 path: relative_path.clone(),
1427 },
1428 );
1429 }
1430 }
1431 }
1432
1433 let extension_wasm_path = extension_dir.join("extension.wasm");
1434 if fs.is_file(&extension_wasm_path).await {
1435 extension_manifest
1436 .lib
1437 .kind
1438 .get_or_insert(ExtensionLibraryKind::Rust);
1439 }
1440
1441 index.extensions.insert(
1442 extension_id.clone(),
1443 ExtensionIndexEntry {
1444 dev: is_dev,
1445 manifest: Arc::new(extension_manifest),
1446 },
1447 );
1448
1449 Ok(())
1450 }
1451}
1452
1453fn load_plugin_queries(root_path: &Path) -> LanguageQueries {
1454 let mut result = LanguageQueries::default();
1455 if let Some(entries) = std::fs::read_dir(root_path).log_err() {
1456 for entry in entries {
1457 let Some(entry) = entry.log_err() else {
1458 continue;
1459 };
1460 let path = entry.path();
1461 if let Some(remainder) = path.strip_prefix(root_path).ok().and_then(|p| p.to_str()) {
1462 if !remainder.ends_with(".scm") {
1463 continue;
1464 }
1465 for (name, query) in QUERY_FILENAME_PREFIXES {
1466 if remainder.starts_with(name) {
1467 if let Some(contents) = std::fs::read_to_string(&path).log_err() {
1468 match query(&mut result) {
1469 None => *query(&mut result) = Some(contents.into()),
1470 Some(r) => r.to_mut().push_str(contents.as_ref()),
1471 }
1472 }
1473 break;
1474 }
1475 }
1476 }
1477 }
1478 }
1479 result
1480}