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