Compare commits

...

13 Commits

Author SHA1 Message Date
Kyle Kelley
983e878cff pass through remote kernel's language on legacy selection 2024-11-22 16:18:18 -08:00
Marshall Bowers
1cfcdfa7ac Overhaul extension registration (#21083)
This PR overhauls extension registration in order to make it more
modular.

The `extension` crate now contains an `ExtensionHostProxy` that can be
used to register various proxies that the extension host can use to
interact with the rest of the system.

There are now a number of different proxy traits representing the
various pieces of functionality that can be provided by an extension.
The respective crates that provide this functionality can implement
their corresponding proxy trait in order to register a proxy that the
extension host will use to register the bits of functionality provided
by the extension.

Release Notes:

- N/A
2024-11-22 19:02:32 -05:00
Michael Sloan
c9f2c2792c Improve error handling and resource cleanup in linux/x11/window.rs (#21079)
* Fixes registration of event handler for xinput-2 device changes,
revealed by this improvement.

* Pushes `.unwrap()` panic-ing outwards to callers.

* Includes a description of what the X11 call was doing when a failure
was encountered.

* Fixes a variety of places where the X11 reply wasn't being inspected
for failures.

* Destroys windows on failure during setup. New structure makes it
possible for the caller of `open_window` to carry on despite failures,
and so partially initialized window should be removed (though all calls
I looked at also panic currently).

Considered pushing this through `linux/x11/client.rs` too but figured
it'd be nice to minimize merge conflicts with #20853.

Release Notes:

- N/A
2024-11-22 16:03:46 -07:00
Mikayla Maki
8240a52a39 Prevent panels from being resized past the edge of the workspace (#20637)
Closes #20593

Release Notes:

- Fixed a bug where it is possible to get in near-unrecoverable panel
state by resizing the panel past the edge of the workspace.

Co-authored-by: Trace <violet.white.batt@gmail.com>
2024-11-22 14:59:40 -08:00
teapo
c28f5b11f8 Allow overrides for json-language-server settings (#20748)
Closes #20739

The JSON LSP adapter now merges user settings with cached settings, and
util::merge_json_value_into pushes array contents from source to target.
2024-11-22 17:50:25 -05:00
Mikayla Maki
96854c68ea Markdown preview image rendering (#21082)
Closes https://github.com/zed-industries/zed/issues/13246

Supersedes: https://github.com/zed-industries/zed/pull/16192

I couldn't push to the git fork this user was using, so here's the exact
same PR but with some style nits implemented.


Release Notes:

- Added image rendering to the Markdown preview

---------

Co-authored-by: dovakin0007 <dovakin0007@gmail.com>
Co-authored-by: dovakin0007 <73059450+dovakin0007@users.noreply.github.com>
2024-11-22 14:49:26 -08:00
Peter Tripp
becc36380f Cleanup file_scan_inclusions in default.json (#21073) 2024-11-22 17:46:14 -05:00
Peter Tripp
1a0a8a9559 Fix picker new_path_prompt throwing "file exists" when saving (#21080)
Fix for getting File exists "os error 17" with `"use_system_path_prompts": false,`

This was reproducible when the first worktree is a non-folder worktree
(e.g. setting.json) so we were trying to create the new file with a path
under ~/.config/zed/settings.json/newfile.ext

Co-authored-by: Conrad Irwin <conrad@zed.dev>
2024-11-22 17:45:03 -05:00
Peter Tripp
2fd210bc9a Fix stale Discord invite links (#21074) 2024-11-22 21:10:51 +00:00
Peter Tripp
23321be2ce docs: Improve Dart language docs (#21071) 2024-11-22 13:58:24 -05:00
Hugo Cardante
659b1c9dcf Add the option to hide both the task and command lines in the task output (#20920)
The goal is to be able to hide these lines from a task output:

```sh
⏵ Task `...` finished successfully
⏵ Command: ...
```

---------

Co-authored-by: Peter Tripp <peter@zed.dev>
2024-11-22 13:45:42 -05:00
Marshall Bowers
cb8028c092 Use Extension trait when registering extension context servers (#21070)
This PR updates the extension context server registration to go through
the `Extension` trait for interacting with extensions rather than going
through the `WasmHost` directly.

Release Notes:

- N/A
2024-11-22 13:21:30 -05:00
william341
ca76948044 gpui: Add drop_image (#19772)
This PR adds a function, WindowContext::drop_image, to manually remove a
RenderImage from the sprite atlas. In addition, PlatformAtlas::remove
was added to support this behavior. Previously, there was no way to
request a RenderImage to be removed from the sprite atlas, and since
they are not removed automatically the sprite would remain in video
memory once added until the window was closed. This PR allows a
developer to request the image be dropped from memory manually, however
it does not add automatic removal.

Release Notes:

- N/A

---------

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
Co-authored-by: Mikayla Maki <mikayla@zed.dev>
2024-11-22 09:51:26 -08:00
76 changed files with 2278 additions and 1246 deletions

50
Cargo.lock generated
View File

@@ -2601,6 +2601,7 @@ dependencies = [
"editor",
"env_logger 0.11.5",
"envy",
"extension",
"file_finder",
"fs",
"futures 0.3.31",
@@ -2842,6 +2843,7 @@ dependencies = [
"anyhow",
"collections",
"command_palette_hooks",
"extension",
"futures 0.3.31",
"gpui",
"log",
@@ -4127,6 +4129,7 @@ dependencies = [
"language",
"log",
"lsp",
"parking_lot",
"semantic_version",
"serde",
"serde_json",
@@ -4178,6 +4181,7 @@ dependencies = [
"gpui",
"http_client",
"language",
"language_extension",
"log",
"lsp",
"node_runtime",
@@ -4196,6 +4200,7 @@ dependencies = [
"task",
"tempfile",
"theme",
"theme_extension",
"toml 0.8.19",
"url",
"util",
@@ -4209,21 +4214,15 @@ name = "extensions_ui"
version = "0.1.0"
dependencies = [
"anyhow",
"assistant_slash_command",
"client",
"collections",
"context_servers",
"db",
"editor",
"extension",
"extension_host",
"fs",
"fuzzy",
"gpui",
"indexed_docs",
"language",
"log",
"lsp",
"num-format",
"picker",
"project",
@@ -4232,12 +4231,10 @@ dependencies = [
"serde",
"settings",
"smallvec",
"snippet_provider",
"theme",
"ui",
"util",
"vim_mode_setting",
"wasmtime-wasi",
"workspace",
"zed_actions",
]
@@ -6534,6 +6531,23 @@ dependencies = [
"util",
]
[[package]]
name = "language_extension"
version = "0.1.0"
dependencies = [
"anyhow",
"async-trait",
"collections",
"extension",
"futures 0.3.31",
"gpui",
"language",
"lsp",
"serde",
"serde_json",
"util",
]
[[package]]
name = "language_model"
version = "0.1.0"
@@ -9854,6 +9868,7 @@ dependencies = [
"client",
"clock",
"env_logger 0.11.5",
"extension",
"extension_host",
"fork",
"fs",
@@ -9863,6 +9878,7 @@ dependencies = [
"gpui",
"http_client",
"language",
"language_extension",
"languages",
"libc",
"log",
@@ -11305,6 +11321,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"collections",
"extension",
"fs",
"futures 0.3.31",
"gpui",
@@ -12358,6 +12375,17 @@ dependencies = [
"uuid",
]
[[package]]
name = "theme_extension"
version = "0.1.0"
dependencies = [
"anyhow",
"extension",
"fs",
"gpui",
"theme",
]
[[package]]
name = "theme_importer"
version = "0.1.0"
@@ -15467,7 +15495,6 @@ dependencies = [
"ashpd",
"assets",
"assistant",
"assistant_slash_command",
"async-watch",
"audio",
"auto_update",
@@ -15484,12 +15511,12 @@ dependencies = [
"collections",
"command_palette",
"command_palette_hooks",
"context_servers",
"copilot",
"db",
"diagnostics",
"editor",
"env_logger 0.11.5",
"extension",
"extension_host",
"extensions_ui",
"feature_flags",
@@ -15504,11 +15531,11 @@ dependencies = [
"gpui",
"http_client",
"image_viewer",
"indexed_docs",
"inline_completion_button",
"install_cli",
"journal",
"language",
"language_extension",
"language_model",
"language_models",
"language_selector",
@@ -15557,6 +15584,7 @@ dependencies = [
"telemetry_events",
"terminal_view",
"theme",
"theme_extension",
"theme_selector",
"time",
"toolchain_selector",

View File

@@ -55,6 +55,7 @@ members = [
"crates/install_cli",
"crates/journal",
"crates/language",
"crates/language_extension",
"crates/language_model",
"crates/language_models",
"crates/language_selector",
@@ -116,6 +117,7 @@ members = [
"crates/terminal_view",
"crates/text",
"crates/theme",
"crates/theme_extension",
"crates/theme_importer",
"crates/theme_selector",
"crates/time_format",
@@ -230,6 +232,7 @@ inline_completion_button = { path = "crates/inline_completion_button" }
install_cli = { path = "crates/install_cli" }
journal = { path = "crates/journal" }
language = { path = "crates/language" }
language_extension = { path = "crates/language_extension" }
language_model = { path = "crates/language_model" }
language_models = { path = "crates/language_models" }
language_selector = { path = "crates/language_selector" }
@@ -292,6 +295,7 @@ terminal = { path = "crates/terminal" }
terminal_view = { path = "crates/terminal_view" }
text = { path = "crates/text" }
theme = { path = "crates/theme" }
theme_extension = { path = "crates/theme_extension" }
theme_importer = { path = "crates/theme_importer" }
theme_selector = { path = "crates/theme_selector" }
time_format = { path = "crates/time_format" }

View File

@@ -683,10 +683,7 @@
// ignored by git. This is useful for files that are not tracked by git,
// but are still important to your project. Note that globs that are
// overly broad can slow down Zed's file scanning. Overridden by `file_scan_exclusions`.
"file_scan_inclusions": [
".env*",
"docker-compose.*.yml"
],
"file_scan_inclusions": [".env*"],
// Git gutter behavior configuration.
"git": {
// Control whether the git gutter is shown. May take 2 values:

View File

@@ -33,7 +33,6 @@ use feature_flags::FeatureFlagAppExt;
use fs::Fs;
use gpui::impl_actions;
use gpui::{actions, AppContext, Global, SharedString, UpdateGlobal};
use indexed_docs::IndexedDocsRegistry;
pub(crate) use inline_assistant::*;
use language_model::{
LanguageModelId, LanguageModelProviderId, LanguageModelRegistry, LanguageModelResponseMessage,
@@ -275,7 +274,7 @@ pub fn init(
client.telemetry().clone(),
cx,
);
IndexedDocsRegistry::init_global(cx);
indexed_docs::init(cx);
CommandPaletteFilter::update_global(cx, |filter, _cx| {
filter.hide_namespace(Assistant::NAMESPACE);

View File

@@ -18,6 +18,7 @@ use workspace::{ui::IconName, Workspace};
pub fn init(cx: &mut AppContext) {
SlashCommandRegistry::default_global(cx);
extension_slash_command::init(cx);
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]

View File

@@ -3,17 +3,39 @@ use std::sync::{atomic::AtomicBool, Arc};
use anyhow::Result;
use async_trait::async_trait;
use extension::{Extension, WorktreeDelegate};
use gpui::{Task, WeakView, WindowContext};
use extension::{Extension, ExtensionHostProxy, ExtensionSlashCommandProxy, WorktreeDelegate};
use gpui::{AppContext, Task, WeakView, WindowContext};
use language::{BufferSnapshot, LspAdapterDelegate};
use ui::prelude::*;
use workspace::Workspace;
use crate::{
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
SlashCommandResult,
SlashCommandRegistry, SlashCommandResult,
};
pub fn init(cx: &mut AppContext) {
let proxy = ExtensionHostProxy::default_global(cx);
proxy.register_slash_command_proxy(SlashCommandRegistryProxy {
slash_command_registry: SlashCommandRegistry::global(cx),
});
}
struct SlashCommandRegistryProxy {
slash_command_registry: Arc<SlashCommandRegistry>,
}
impl ExtensionSlashCommandProxy for SlashCommandRegistryProxy {
fn register_slash_command(
&self,
extension: Arc<dyn Extension>,
command: extension::SlashCommand,
) {
self.slash_command_registry
.register_command(ExtensionSlashCommand::new(extension, command), false)
}
}
/// An adapter that allows an [`LspAdapterDelegate`] to be used as a [`WorktreeDelegate`].
struct WorktreeDelegateAdapter(Arc<dyn LspAdapterDelegate>);

View File

@@ -90,6 +90,7 @@ collections = { workspace = true, features = ["test-support"] }
ctor.workspace = true
editor = { workspace = true, features = ["test-support"] }
env_logger.workspace = true
extension.workspace = true
file_finder.workspace = true
fs = { workspace = true, features = ["test-support"] }
git = { workspace = true, features = ["test-support"] }

View File

@@ -1,6 +1,7 @@
use crate::tests::TestServer;
use call::ActiveCall;
use collections::HashSet;
use extension::ExtensionHostProxy;
use fs::{FakeFs, Fs as _};
use futures::StreamExt as _;
use gpui::{BackgroundExecutor, Context as _, SemanticVersion, TestAppContext, UpdateGlobal as _};
@@ -81,6 +82,7 @@ async fn test_sharing_an_ssh_remote_project(
http_client: remote_http_client,
node_runtime: node,
languages,
extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
},
cx,
)
@@ -243,6 +245,7 @@ async fn test_ssh_collaboration_git_branches(
http_client: remote_http_client,
node_runtime: node,
languages,
extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
},
cx,
)
@@ -400,6 +403,7 @@ async fn test_ssh_collaboration_formatting_with_prettier(
http_client: remote_http_client,
node_runtime: NodeRuntime::unavailable(),
languages,
extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
},
cx,
)

View File

@@ -15,6 +15,7 @@ path = "src/context_servers.rs"
anyhow.workspace = true
collections.workspace = true
command_palette_hooks.workspace = true
extension.workspace = true
futures.workspace = true
gpui.workspace = true
log.workspace = true

View File

@@ -1,4 +1,5 @@
pub mod client;
mod extension_context_server;
pub mod manager;
pub mod protocol;
mod registry;
@@ -19,6 +20,7 @@ pub const CONTEXT_SERVERS_NAMESPACE: &'static str = "context_servers";
pub fn init(cx: &mut AppContext) {
ContextServerSettings::register(cx);
ContextServerFactoryRegistry::default_global(cx);
extension_context_server::init(cx);
CommandPaletteFilter::update_global(cx, |filter, _cx| {
filter.hide_namespace(CONTEXT_SERVERS_NAMESPACE);

View File

@@ -0,0 +1,78 @@
use std::sync::Arc;
use extension::{Extension, ExtensionContextServerProxy, ExtensionHostProxy, ProjectDelegate};
use gpui::{AppContext, Model};
use crate::manager::ServerCommand;
use crate::ContextServerFactoryRegistry;
struct ExtensionProject {
worktree_ids: Vec<u64>,
}
impl ProjectDelegate for ExtensionProject {
fn worktree_ids(&self) -> Vec<u64> {
self.worktree_ids.clone()
}
}
pub fn init(cx: &mut AppContext) {
let proxy = ExtensionHostProxy::default_global(cx);
proxy.register_context_server_proxy(ContextServerFactoryRegistryProxy {
context_server_factory_registry: ContextServerFactoryRegistry::global(cx),
});
}
struct ContextServerFactoryRegistryProxy {
context_server_factory_registry: Model<ContextServerFactoryRegistry>,
}
impl ExtensionContextServerProxy for ContextServerFactoryRegistryProxy {
fn register_context_server(
&self,
extension: Arc<dyn Extension>,
id: Arc<str>,
cx: &mut AppContext,
) {
self.context_server_factory_registry
.update(cx, |registry, _| {
registry.register_server_factory(
id.clone(),
Arc::new({
move |project, cx| {
log::info!(
"loading command for context server {id} from extension {}",
extension.manifest().id
);
let id = id.clone();
let extension = extension.clone();
cx.spawn(|mut cx| async move {
let extension_project =
project.update(&mut cx, |project, cx| {
Arc::new(ExtensionProject {
worktree_ids: project
.visible_worktrees(cx)
.map(|worktree| worktree.read(cx).id().to_proto())
.collect(),
})
})?;
let command = extension
.context_server_command(id.clone(), extension_project)
.await?;
log::info!("loaded command for context server {id}: {command:?}");
Ok(ServerCommand {
path: command.command,
args: command.args,
env: Some(command.env.into_iter().collect()),
})
})
}
}),
)
});
}
}

View File

@@ -24,6 +24,7 @@ http_client.workspace = true
language.workspace = true
log.workspace = true
lsp.workspace = true
parking_lot.workspace = true
semantic_version.workspace = true
serde.workspace = true
serde_json.workspace = true

View File

@@ -1,4 +1,5 @@
pub mod extension_builder;
mod extension_host_proxy;
mod extension_manifest;
mod types;
@@ -9,13 +10,19 @@ use ::lsp::LanguageServerName;
use anyhow::{anyhow, bail, Context as _, Result};
use async_trait::async_trait;
use fs::normalize_path;
use gpui::Task;
use gpui::{AppContext, Task};
use language::LanguageName;
use semantic_version::SemanticVersion;
pub use crate::extension_host_proxy::*;
pub use crate::extension_manifest::*;
pub use crate::types::*;
/// Initializes the `extension` crate.
pub fn init(cx: &mut AppContext) {
ExtensionHostProxy::default_global(cx);
}
#[async_trait]
pub trait WorktreeDelegate: Send + Sync + 'static {
fn id(&self) -> u64;
@@ -25,6 +32,10 @@ pub trait WorktreeDelegate: Send + Sync + 'static {
async fn shell_env(&self) -> Vec<(String, String)>;
}
pub trait ProjectDelegate: Send + Sync + 'static {
fn worktree_ids(&self) -> Vec<u64>;
}
pub trait KeyValueStoreDelegate: Send + Sync + 'static {
fn insert(&self, key: String, docs: String) -> Task<Result<()>>;
}
@@ -87,6 +98,12 @@ pub trait Extension: Send + Sync + 'static {
worktree: Option<Arc<dyn WorktreeDelegate>>,
) -> Result<SlashCommandOutput>;
async fn context_server_command(
&self,
context_server_id: Arc<str>,
project: Arc<dyn ProjectDelegate>,
) -> Result<Command>;
async fn suggest_docs_packages(&self, provider: Arc<str>) -> Result<Vec<String>>;
async fn index_docs(

View File

@@ -0,0 +1,324 @@
use std::path::PathBuf;
use std::sync::Arc;
use anyhow::Result;
use fs::Fs;
use gpui::{AppContext, Global, ReadGlobal, SharedString, Task};
use language::{LanguageMatcher, LanguageName, LanguageServerBinaryStatus, LoadedLanguage};
use lsp::LanguageServerName;
use parking_lot::RwLock;
use crate::{Extension, SlashCommand};
#[derive(Default)]
struct GlobalExtensionHostProxy(Arc<ExtensionHostProxy>);
impl Global for GlobalExtensionHostProxy {}
/// A proxy for interacting with the extension host.
///
/// This object implements each of the individual proxy types so that their
/// methods can be called directly on it.
#[derive(Default)]
pub struct ExtensionHostProxy {
theme_proxy: RwLock<Option<Arc<dyn ExtensionThemeProxy>>>,
grammar_proxy: RwLock<Option<Arc<dyn ExtensionGrammarProxy>>>,
language_proxy: RwLock<Option<Arc<dyn ExtensionLanguageProxy>>>,
language_server_proxy: RwLock<Option<Arc<dyn ExtensionLanguageServerProxy>>>,
snippet_proxy: RwLock<Option<Arc<dyn ExtensionSnippetProxy>>>,
slash_command_proxy: RwLock<Option<Arc<dyn ExtensionSlashCommandProxy>>>,
context_server_proxy: RwLock<Option<Arc<dyn ExtensionContextServerProxy>>>,
indexed_docs_provider_proxy: RwLock<Option<Arc<dyn ExtensionIndexedDocsProviderProxy>>>,
}
impl ExtensionHostProxy {
/// Returns the global [`ExtensionHostProxy`].
pub fn global(cx: &AppContext) -> Arc<Self> {
GlobalExtensionHostProxy::global(cx).0.clone()
}
/// Returns the global [`ExtensionHostProxy`].
///
/// Inserts a default [`ExtensionHostProxy`] if one does not yet exist.
pub fn default_global(cx: &mut AppContext) -> Arc<Self> {
cx.default_global::<GlobalExtensionHostProxy>().0.clone()
}
pub fn new() -> Self {
Self {
theme_proxy: RwLock::default(),
grammar_proxy: RwLock::default(),
language_proxy: RwLock::default(),
language_server_proxy: RwLock::default(),
snippet_proxy: RwLock::default(),
slash_command_proxy: RwLock::default(),
context_server_proxy: RwLock::default(),
indexed_docs_provider_proxy: RwLock::default(),
}
}
pub fn register_theme_proxy(&self, proxy: impl ExtensionThemeProxy) {
self.theme_proxy.write().replace(Arc::new(proxy));
}
pub fn register_grammar_proxy(&self, proxy: impl ExtensionGrammarProxy) {
self.grammar_proxy.write().replace(Arc::new(proxy));
}
pub fn register_language_proxy(&self, proxy: impl ExtensionLanguageProxy) {
self.language_proxy.write().replace(Arc::new(proxy));
}
pub fn register_language_server_proxy(&self, proxy: impl ExtensionLanguageServerProxy) {
self.language_server_proxy.write().replace(Arc::new(proxy));
}
pub fn register_snippet_proxy(&self, proxy: impl ExtensionSnippetProxy) {
self.snippet_proxy.write().replace(Arc::new(proxy));
}
pub fn register_slash_command_proxy(&self, proxy: impl ExtensionSlashCommandProxy) {
self.slash_command_proxy.write().replace(Arc::new(proxy));
}
pub fn register_context_server_proxy(&self, proxy: impl ExtensionContextServerProxy) {
self.context_server_proxy.write().replace(Arc::new(proxy));
}
pub fn register_indexed_docs_provider_proxy(
&self,
proxy: impl ExtensionIndexedDocsProviderProxy,
) {
self.indexed_docs_provider_proxy
.write()
.replace(Arc::new(proxy));
}
}
pub trait ExtensionThemeProxy: Send + Sync + 'static {
fn list_theme_names(&self, theme_path: PathBuf, fs: Arc<dyn Fs>) -> Task<Result<Vec<String>>>;
fn remove_user_themes(&self, themes: Vec<SharedString>);
fn load_user_theme(&self, theme_path: PathBuf, fs: Arc<dyn Fs>) -> Task<Result<()>>;
fn reload_current_theme(&self, cx: &mut AppContext);
}
impl ExtensionThemeProxy for ExtensionHostProxy {
fn list_theme_names(&self, theme_path: PathBuf, fs: Arc<dyn Fs>) -> Task<Result<Vec<String>>> {
let Some(proxy) = self.theme_proxy.read().clone() else {
return Task::ready(Ok(Vec::new()));
};
proxy.list_theme_names(theme_path, fs)
}
fn remove_user_themes(&self, themes: Vec<SharedString>) {
let Some(proxy) = self.theme_proxy.read().clone() else {
return;
};
proxy.remove_user_themes(themes)
}
fn load_user_theme(&self, theme_path: PathBuf, fs: Arc<dyn Fs>) -> Task<Result<()>> {
let Some(proxy) = self.theme_proxy.read().clone() else {
return Task::ready(Ok(()));
};
proxy.load_user_theme(theme_path, fs)
}
fn reload_current_theme(&self, cx: &mut AppContext) {
let Some(proxy) = self.theme_proxy.read().clone() else {
return;
};
proxy.reload_current_theme(cx)
}
}
pub trait ExtensionGrammarProxy: Send + Sync + 'static {
fn register_grammars(&self, grammars: Vec<(Arc<str>, PathBuf)>);
}
impl ExtensionGrammarProxy for ExtensionHostProxy {
fn register_grammars(&self, grammars: Vec<(Arc<str>, PathBuf)>) {
let Some(proxy) = self.grammar_proxy.read().clone() else {
return;
};
proxy.register_grammars(grammars)
}
}
pub trait ExtensionLanguageProxy: Send + Sync + 'static {
fn register_language(
&self,
language: LanguageName,
grammar: Option<Arc<str>>,
matcher: LanguageMatcher,
load: Arc<dyn Fn() -> Result<LoadedLanguage> + Send + Sync + 'static>,
);
fn remove_languages(
&self,
languages_to_remove: &[LanguageName],
grammars_to_remove: &[Arc<str>],
);
}
impl ExtensionLanguageProxy for ExtensionHostProxy {
fn register_language(
&self,
language: LanguageName,
grammar: Option<Arc<str>>,
matcher: LanguageMatcher,
load: Arc<dyn Fn() -> Result<LoadedLanguage> + Send + Sync + 'static>,
) {
let Some(proxy) = self.language_proxy.read().clone() else {
return;
};
proxy.register_language(language, grammar, matcher, load)
}
fn remove_languages(
&self,
languages_to_remove: &[LanguageName],
grammars_to_remove: &[Arc<str>],
) {
let Some(proxy) = self.language_proxy.read().clone() else {
return;
};
proxy.remove_languages(languages_to_remove, grammars_to_remove)
}
}
pub trait ExtensionLanguageServerProxy: Send + Sync + 'static {
fn register_language_server(
&self,
extension: Arc<dyn Extension>,
language_server_id: LanguageServerName,
language: LanguageName,
);
fn remove_language_server(
&self,
language: &LanguageName,
language_server_id: &LanguageServerName,
);
fn update_language_server_status(
&self,
language_server_id: LanguageServerName,
status: LanguageServerBinaryStatus,
);
}
impl ExtensionLanguageServerProxy for ExtensionHostProxy {
fn register_language_server(
&self,
extension: Arc<dyn Extension>,
language_server_id: LanguageServerName,
language: LanguageName,
) {
let Some(proxy) = self.language_server_proxy.read().clone() else {
return;
};
proxy.register_language_server(extension, language_server_id, language)
}
fn remove_language_server(
&self,
language: &LanguageName,
language_server_id: &LanguageServerName,
) {
let Some(proxy) = self.language_server_proxy.read().clone() else {
return;
};
proxy.remove_language_server(language, language_server_id)
}
fn update_language_server_status(
&self,
language_server_id: LanguageServerName,
status: LanguageServerBinaryStatus,
) {
let Some(proxy) = self.language_server_proxy.read().clone() else {
return;
};
proxy.update_language_server_status(language_server_id, status)
}
}
pub trait ExtensionSnippetProxy: Send + Sync + 'static {
fn register_snippet(&self, path: &PathBuf, snippet_contents: &str) -> Result<()>;
}
impl ExtensionSnippetProxy for ExtensionHostProxy {
fn register_snippet(&self, path: &PathBuf, snippet_contents: &str) -> Result<()> {
let Some(proxy) = self.snippet_proxy.read().clone() else {
return Ok(());
};
proxy.register_snippet(path, snippet_contents)
}
}
pub trait ExtensionSlashCommandProxy: Send + Sync + 'static {
fn register_slash_command(&self, extension: Arc<dyn Extension>, command: SlashCommand);
}
impl ExtensionSlashCommandProxy for ExtensionHostProxy {
fn register_slash_command(&self, extension: Arc<dyn Extension>, command: SlashCommand) {
let Some(proxy) = self.slash_command_proxy.read().clone() else {
return;
};
proxy.register_slash_command(extension, command)
}
}
pub trait ExtensionContextServerProxy: Send + Sync + 'static {
fn register_context_server(
&self,
extension: Arc<dyn Extension>,
server_id: Arc<str>,
cx: &mut AppContext,
);
}
impl ExtensionContextServerProxy for ExtensionHostProxy {
fn register_context_server(
&self,
extension: Arc<dyn Extension>,
server_id: Arc<str>,
cx: &mut AppContext,
) {
let Some(proxy) = self.context_server_proxy.read().clone() else {
return;
};
proxy.register_context_server(extension, server_id, cx)
}
}
pub trait ExtensionIndexedDocsProviderProxy: Send + Sync + 'static {
fn register_indexed_docs_provider(&self, extension: Arc<dyn Extension>, provider_id: Arc<str>);
}
impl ExtensionIndexedDocsProviderProxy for ExtensionHostProxy {
fn register_indexed_docs_provider(&self, extension: Arc<dyn Extension>, provider_id: Arc<str>) {
let Some(proxy) = self.indexed_docs_provider_proxy.read().clone() else {
return;
};
proxy.register_indexed_docs_provider(extension, provider_id)
}
}

View File

@@ -10,6 +10,7 @@ pub use slash_command::*;
pub type EnvVars = Vec<(String, String)>;
/// A command.
#[derive(Debug)]
pub struct Command {
/// The command to execute.
pub command: String,

View File

@@ -57,7 +57,9 @@ env_logger.workspace = true
fs = { workspace = true, features = ["test-support"] }
gpui = { workspace = true, features = ["test-support"] }
language = { workspace = true, features = ["test-support"] }
language_extension.workspace = true
parking_lot.workspace = true
project = { workspace = true, features = ["test-support"] }
reqwest_client.workspace = true
theme = { workspace = true, features = ["test-support"] }
theme_extension.workspace = true

View File

@@ -1,4 +1,3 @@
pub mod extension_lsp_adapter;
pub mod extension_settings;
pub mod headless_host;
pub mod wasm_host;
@@ -12,8 +11,12 @@ use async_tar::Archive;
use client::{proto, telemetry::Telemetry, Client, ExtensionMetadata, GetExtensionsResponse};
use collections::{btree_map, BTreeMap, HashMap, HashSet};
use extension::extension_builder::{CompileExtensionOptions, ExtensionBuilder};
use extension::Extension;
pub use extension::ExtensionManifest;
use extension::{
ExtensionContextServerProxy, ExtensionGrammarProxy, ExtensionHostProxy,
ExtensionIndexedDocsProviderProxy, ExtensionLanguageProxy, ExtensionLanguageServerProxy,
ExtensionSlashCommandProxy, ExtensionSnippetProxy, ExtensionThemeProxy,
};
use fs::{Fs, RemoveOptions};
use futures::{
channel::{
@@ -24,15 +27,14 @@ use futures::{
select_biased, AsyncReadExt as _, Future, FutureExt as _, StreamExt as _,
};
use gpui::{
actions, AppContext, AsyncAppContext, Context, EventEmitter, Global, Model, ModelContext,
SharedString, Task, WeakModel,
actions, AppContext, AsyncAppContext, Context, EventEmitter, Global, Model, ModelContext, Task,
WeakModel,
};
use http_client::{AsyncBody, HttpClient, HttpClientWithUrl};
use language::{
LanguageConfig, LanguageMatcher, LanguageName, LanguageQueries, LoadedLanguage,
QUERY_FILENAME_PREFIXES,
};
use lsp::LanguageServerName;
use node_runtime::NodeRuntime;
use project::ContextProviderWithTasks;
use release_channel::ReleaseChannel;
@@ -95,82 +97,8 @@ pub fn is_version_compatible(
true
}
pub trait ExtensionRegistrationHooks: Send + Sync + 'static {
fn remove_user_themes(&self, _themes: Vec<SharedString>) {}
fn load_user_theme(&self, _theme_path: PathBuf, _fs: Arc<dyn Fs>) -> Task<Result<()>> {
Task::ready(Ok(()))
}
fn list_theme_names(
&self,
_theme_path: PathBuf,
_fs: Arc<dyn Fs>,
) -> Task<Result<Vec<String>>> {
Task::ready(Ok(Vec::new()))
}
fn reload_current_theme(&self, _cx: &mut AppContext) {}
fn register_language(
&self,
_language: LanguageName,
_grammar: Option<Arc<str>>,
_matcher: language::LanguageMatcher,
_load: Arc<dyn Fn() -> Result<LoadedLanguage> + 'static + Send + Sync>,
) {
}
fn register_lsp_adapter(
&self,
_extension: Arc<dyn Extension>,
_language_server_id: LanguageServerName,
_language: LanguageName,
) {
}
fn remove_lsp_adapter(&self, _language: &LanguageName, _server_name: &LanguageServerName) {}
fn register_wasm_grammars(&self, _grammars: Vec<(Arc<str>, PathBuf)>) {}
fn remove_languages(
&self,
_languages_to_remove: &[LanguageName],
_grammars_to_remove: &[Arc<str>],
) {
}
fn register_slash_command(
&self,
_extension: Arc<dyn Extension>,
_command: extension::SlashCommand,
) {
}
fn register_context_server(
&self,
_id: Arc<str>,
_extension: WasmExtension,
_cx: &mut AppContext,
) {
}
fn register_docs_provider(&self, _extension: Arc<dyn Extension>, _provider_id: Arc<str>) {}
fn register_snippets(&self, _path: &PathBuf, _snippet_contents: &str) -> Result<()> {
Ok(())
}
fn update_lsp_status(
&self,
_server_name: lsp::LanguageServerName,
_status: language::LanguageServerBinaryStatus,
) {
}
}
pub struct ExtensionStore {
pub registration_hooks: Arc<dyn ExtensionRegistrationHooks>,
pub proxy: Arc<ExtensionHostProxy>,
pub builder: Arc<ExtensionBuilder>,
pub extension_index: ExtensionIndex,
pub fs: Arc<dyn Fs>,
@@ -240,7 +168,7 @@ pub struct ExtensionIndexLanguageEntry {
actions!(zed, [ReloadExtensions]);
pub fn init(
registration_hooks: Arc<dyn ExtensionRegistrationHooks>,
extension_host_proxy: Arc<ExtensionHostProxy>,
fs: Arc<dyn Fs>,
client: Arc<Client>,
node_runtime: NodeRuntime,
@@ -252,7 +180,7 @@ pub fn init(
ExtensionStore::new(
paths::extensions_dir().clone(),
None,
registration_hooks,
extension_host_proxy,
fs,
client.http_client().clone(),
client.http_client().clone(),
@@ -284,7 +212,7 @@ impl ExtensionStore {
pub fn new(
extensions_dir: PathBuf,
build_dir: Option<PathBuf>,
extension_api: Arc<dyn ExtensionRegistrationHooks>,
extension_host_proxy: Arc<ExtensionHostProxy>,
fs: Arc<dyn Fs>,
http_client: Arc<HttpClientWithUrl>,
builder_client: Arc<dyn HttpClient>,
@@ -300,7 +228,7 @@ impl ExtensionStore {
let (reload_tx, mut reload_rx) = unbounded();
let (connection_registered_tx, mut connection_registered_rx) = unbounded();
let mut this = Self {
registration_hooks: extension_api.clone(),
proxy: extension_host_proxy.clone(),
extension_index: Default::default(),
installed_dir,
index_path,
@@ -312,7 +240,7 @@ impl ExtensionStore {
fs.clone(),
http_client.clone(),
node_runtime,
extension_api,
extension_host_proxy,
work_dir,
cx,
),
@@ -1113,16 +1041,16 @@ impl ExtensionStore {
grammars_to_remove.extend(extension.manifest.grammars.keys().cloned());
for (language_server_name, config) in extension.manifest.language_servers.iter() {
for language in config.languages() {
self.registration_hooks
.remove_lsp_adapter(&language, language_server_name);
self.proxy
.remove_language_server(&language, language_server_name);
}
}
}
self.wasm_extensions
.retain(|(extension, _)| !extensions_to_unload.contains(&extension.id));
self.registration_hooks.remove_user_themes(themes_to_remove);
self.registration_hooks
self.proxy.remove_user_themes(themes_to_remove);
self.proxy
.remove_languages(&languages_to_remove, &grammars_to_remove);
let languages_to_add = new_index
@@ -1157,8 +1085,7 @@ impl ExtensionStore {
}));
}
self.registration_hooks
.register_wasm_grammars(grammars_to_add);
self.proxy.register_grammars(grammars_to_add);
for (language_name, language) in languages_to_add {
let mut language_path = self.installed_dir.clone();
@@ -1166,7 +1093,7 @@ impl ExtensionStore {
Path::new(language.extension.as_ref()),
language.path.as_path(),
]);
self.registration_hooks.register_language(
self.proxy.register_language(
language_name.clone(),
language.grammar.clone(),
language.matcher.clone(),
@@ -1196,7 +1123,7 @@ impl ExtensionStore {
let fs = self.fs.clone();
let wasm_host = self.wasm_host.clone();
let root_dir = self.installed_dir.clone();
let api = self.registration_hooks.clone();
let proxy = self.proxy.clone();
let extension_entries = extensions_to_load
.iter()
.filter_map(|name| new_index.extensions.get(name).cloned())
@@ -1212,13 +1139,17 @@ impl ExtensionStore {
let fs = fs.clone();
async move {
for theme_path in themes_to_add.into_iter() {
api.load_user_theme(theme_path, fs.clone()).await.log_err();
proxy
.load_user_theme(theme_path, fs.clone())
.await
.log_err();
}
for snippets_path in &snippets_to_add {
if let Some(snippets_contents) = fs.load(snippets_path).await.log_err()
{
api.register_snippets(snippets_path, &snippets_contents)
proxy
.register_snippet(snippets_path, &snippets_contents)
.log_err();
}
}
@@ -1259,7 +1190,7 @@ impl ExtensionStore {
for (language_server_id, language_server_config) in &manifest.language_servers {
for language in language_server_config.languages() {
this.registration_hooks.register_lsp_adapter(
this.proxy.register_language_server(
extension.clone(),
language_server_id.clone(),
language.clone(),
@@ -1268,7 +1199,7 @@ impl ExtensionStore {
}
for (slash_command_name, slash_command) in &manifest.slash_commands {
this.registration_hooks.register_slash_command(
this.proxy.register_slash_command(
extension.clone(),
extension::SlashCommand {
name: slash_command_name.to_string(),
@@ -1283,21 +1214,18 @@ impl ExtensionStore {
}
for (id, _context_server_entry) in &manifest.context_servers {
this.registration_hooks.register_context_server(
id.clone(),
wasm_extension.clone(),
cx,
);
this.proxy
.register_context_server(extension.clone(), id.clone(), cx);
}
for (provider_id, _provider) in &manifest.indexed_docs_providers {
this.registration_hooks
.register_docs_provider(extension.clone(), provider_id.clone());
this.proxy
.register_indexed_docs_provider(extension.clone(), provider_id.clone());
}
}
this.wasm_extensions.extend(wasm_extensions);
this.registration_hooks.reload_current_theme(cx);
this.proxy.reload_current_theme(cx);
})
.ok();
})
@@ -1308,7 +1236,7 @@ impl ExtensionStore {
let work_dir = self.wasm_host.work_dir.clone();
let extensions_dir = self.installed_dir.clone();
let index_path = self.index_path.clone();
let extension_api = self.registration_hooks.clone();
let proxy = self.proxy.clone();
cx.background_executor().spawn(async move {
let start_time = Instant::now();
let mut index = ExtensionIndex::default();
@@ -1334,7 +1262,7 @@ impl ExtensionStore {
fs.clone(),
extension_dir,
&mut index,
extension_api.clone(),
proxy.clone(),
)
.await
.log_err();
@@ -1357,7 +1285,7 @@ impl ExtensionStore {
fs: Arc<dyn Fs>,
extension_dir: PathBuf,
index: &mut ExtensionIndex,
extension_api: Arc<dyn ExtensionRegistrationHooks>,
proxy: Arc<ExtensionHostProxy>,
) -> Result<()> {
let mut extension_manifest = ExtensionManifest::load(fs.clone(), &extension_dir).await?;
let extension_id = extension_manifest.id.clone();
@@ -1409,7 +1337,7 @@ impl ExtensionStore {
continue;
};
let Some(theme_families) = extension_api
let Some(theme_families) = proxy
.list_theme_names(theme_path.clone(), fs.clone())
.await
.log_err()

View File

@@ -1,20 +1,16 @@
use crate::extension_lsp_adapter::ExtensionLspAdapter;
use crate::{
Event, ExtensionIndex, ExtensionIndexEntry, ExtensionIndexLanguageEntry,
ExtensionIndexThemeEntry, ExtensionManifest, ExtensionSettings, ExtensionStore,
GrammarManifestEntry, SchemaVersion, RELOAD_DEBOUNCE_DURATION,
};
use anyhow::Result;
use async_compression::futures::bufread::GzipEncoder;
use collections::BTreeMap;
use extension::Extension;
use extension::ExtensionHostProxy;
use fs::{FakeFs, Fs, RealFs};
use futures::{io::BufReader, AsyncReadExt, StreamExt};
use gpui::{BackgroundExecutor, Context, SemanticVersion, SharedString, Task, TestAppContext};
use gpui::{Context, SemanticVersion, TestAppContext};
use http_client::{FakeHttpClient, Response};
use language::{
LanguageMatcher, LanguageName, LanguageRegistry, LanguageServerBinaryStatus, LoadedLanguage,
};
use language::{LanguageMatcher, LanguageRegistry, LanguageServerBinaryStatus};
use lsp::LanguageServerName;
use node_runtime::NodeRuntime;
use parking_lot::Mutex;
@@ -31,91 +27,6 @@ use std::{
use theme::ThemeRegistry;
use util::test::temp_tree;
use crate::ExtensionRegistrationHooks;
struct TestExtensionRegistrationHooks {
executor: BackgroundExecutor,
language_registry: Arc<LanguageRegistry>,
theme_registry: Arc<ThemeRegistry>,
}
impl ExtensionRegistrationHooks for TestExtensionRegistrationHooks {
fn list_theme_names(&self, path: PathBuf, fs: Arc<dyn Fs>) -> Task<Result<Vec<String>>> {
self.executor.spawn(async move {
let themes = theme::read_user_theme(&path, fs).await?;
Ok(themes.themes.into_iter().map(|theme| theme.name).collect())
})
}
fn load_user_theme(&self, theme_path: PathBuf, fs: Arc<dyn fs::Fs>) -> Task<Result<()>> {
let theme_registry = self.theme_registry.clone();
self.executor
.spawn(async move { theme_registry.load_user_theme(&theme_path, fs).await })
}
fn remove_user_themes(&self, themes: Vec<SharedString>) {
self.theme_registry.remove_user_themes(&themes);
}
fn register_language(
&self,
language: language::LanguageName,
grammar: Option<Arc<str>>,
matcher: language::LanguageMatcher,
load: Arc<dyn Fn() -> Result<LoadedLanguage> + 'static + Send + Sync>,
) {
self.language_registry
.register_language(language, grammar, matcher, load)
}
fn remove_languages(
&self,
languages_to_remove: &[language::LanguageName],
grammars_to_remove: &[Arc<str>],
) {
self.language_registry
.remove_languages(&languages_to_remove, &grammars_to_remove);
}
fn register_wasm_grammars(&self, grammars: Vec<(Arc<str>, PathBuf)>) {
self.language_registry.register_wasm_grammars(grammars)
}
fn register_lsp_adapter(
&self,
extension: Arc<dyn Extension>,
language_server_id: LanguageServerName,
language: LanguageName,
) {
self.language_registry.register_lsp_adapter(
language.clone(),
Arc::new(ExtensionLspAdapter::new(
extension,
language_server_id,
language,
)),
);
}
fn update_lsp_status(
&self,
server_name: lsp::LanguageServerName,
status: LanguageServerBinaryStatus,
) {
self.language_registry
.update_lsp_status(server_name, status);
}
fn remove_lsp_adapter(
&self,
language_name: &language::LanguageName,
server_name: &lsp::LanguageServerName,
) {
self.language_registry
.remove_lsp_adapter(language_name, server_name);
}
}
#[cfg(test)]
#[ctor::ctor]
fn init_logger() {
@@ -347,20 +258,18 @@ async fn test_extension_store(cx: &mut TestAppContext) {
.collect(),
};
let language_registry = Arc::new(LanguageRegistry::test(cx.executor()));
let proxy = Arc::new(ExtensionHostProxy::new());
let theme_registry = Arc::new(ThemeRegistry::new(Box::new(())));
let registration_hooks = Arc::new(TestExtensionRegistrationHooks {
executor: cx.executor(),
language_registry: language_registry.clone(),
theme_registry: theme_registry.clone(),
});
theme_extension::init(proxy.clone(), theme_registry.clone(), cx.executor());
let language_registry = Arc::new(LanguageRegistry::test(cx.executor()));
language_extension::init(proxy.clone(), language_registry.clone());
let node_runtime = NodeRuntime::unavailable();
let store = cx.new_model(|cx| {
ExtensionStore::new(
PathBuf::from("/the-extension-dir"),
None,
registration_hooks.clone(),
proxy.clone(),
fs.clone(),
http_client.clone(),
http_client.clone(),
@@ -485,7 +394,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
ExtensionStore::new(
PathBuf::from("/the-extension-dir"),
None,
registration_hooks,
proxy,
fs.clone(),
http_client.clone(),
http_client.clone(),
@@ -568,13 +477,11 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) {
let project = Project::test(fs.clone(), [project_dir.as_path()], cx).await;
let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
let proxy = Arc::new(ExtensionHostProxy::new());
let theme_registry = Arc::new(ThemeRegistry::new(Box::new(())));
let registration_hooks = Arc::new(TestExtensionRegistrationHooks {
executor: cx.executor(),
language_registry: language_registry.clone(),
theme_registry: theme_registry.clone(),
});
theme_extension::init(proxy.clone(), theme_registry.clone(), cx.executor());
let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
language_extension::init(proxy.clone(), language_registry.clone());
let node_runtime = NodeRuntime::unavailable();
let mut status_updates = language_registry.language_server_binary_statuses();
@@ -668,7 +575,7 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) {
ExtensionStore::new(
extensions_dir.clone(),
Some(cache_dir),
registration_hooks,
proxy,
fs.clone(),
extension_client.clone(),
builder_client,

View File

@@ -3,29 +3,18 @@ use std::{path::PathBuf, sync::Arc};
use anyhow::{anyhow, Context as _, Result};
use client::{proto, TypedEnvelope};
use collections::{HashMap, HashSet};
use extension::{Extension, ExtensionManifest};
use extension::{
Extension, ExtensionHostProxy, ExtensionLanguageProxy, ExtensionLanguageServerProxy,
ExtensionManifest,
};
use fs::{Fs, RemoveOptions, RenameOptions};
use gpui::{AppContext, AsyncAppContext, Context, Model, ModelContext, Task, WeakModel};
use http_client::HttpClient;
use language::{LanguageConfig, LanguageName, LanguageQueries, LanguageRegistry, LoadedLanguage};
use language::{LanguageConfig, LanguageName, LanguageQueries, LoadedLanguage};
use lsp::LanguageServerName;
use node_runtime::NodeRuntime;
use crate::{
extension_lsp_adapter::ExtensionLspAdapter,
wasm_host::{WasmExtension, WasmHost},
ExtensionRegistrationHooks,
};
pub struct HeadlessExtensionStore {
pub registration_hooks: Arc<dyn ExtensionRegistrationHooks>,
pub fs: Arc<dyn Fs>,
pub extension_dir: PathBuf,
pub wasm_host: Arc<WasmHost>,
pub loaded_extensions: HashMap<Arc<str>, Arc<str>>,
pub loaded_languages: HashMap<Arc<str>, Vec<LanguageName>>,
pub loaded_language_servers: HashMap<Arc<str>, Vec<(LanguageServerName, LanguageName)>>,
}
use crate::wasm_host::{WasmExtension, WasmHost};
#[derive(Clone, Debug)]
pub struct ExtensionVersion {
@@ -34,28 +23,37 @@ pub struct ExtensionVersion {
pub dev: bool,
}
pub struct HeadlessExtensionStore {
pub fs: Arc<dyn Fs>,
pub extension_dir: PathBuf,
pub proxy: Arc<ExtensionHostProxy>,
pub wasm_host: Arc<WasmHost>,
pub loaded_extensions: HashMap<Arc<str>, Arc<str>>,
pub loaded_languages: HashMap<Arc<str>, Vec<LanguageName>>,
pub loaded_language_servers: HashMap<Arc<str>, Vec<(LanguageServerName, LanguageName)>>,
}
impl HeadlessExtensionStore {
pub fn new(
fs: Arc<dyn Fs>,
http_client: Arc<dyn HttpClient>,
languages: Arc<LanguageRegistry>,
extension_dir: PathBuf,
extension_host_proxy: Arc<ExtensionHostProxy>,
node_runtime: NodeRuntime,
cx: &mut AppContext,
) -> Model<Self> {
let registration_hooks = Arc::new(HeadlessRegistrationHooks::new(languages.clone()));
cx.new_model(|cx| Self {
registration_hooks: registration_hooks.clone(),
fs: fs.clone(),
wasm_host: WasmHost::new(
fs.clone(),
http_client.clone(),
node_runtime,
registration_hooks,
extension_host_proxy.clone(),
extension_dir.join("work"),
cx,
),
extension_dir,
proxy: extension_host_proxy,
loaded_extensions: Default::default(),
loaded_languages: Default::default(),
loaded_language_servers: Default::default(),
@@ -154,7 +152,7 @@ impl HeadlessExtensionStore {
config.grammar = None;
this.registration_hooks.register_language(
this.proxy.register_language(
config.name.clone(),
None,
config.matcher.clone(),
@@ -184,7 +182,7 @@ impl HeadlessExtensionStore {
.entry(manifest.id.clone())
.or_default()
.push((language_server_id.clone(), language.clone()));
this.registration_hooks.register_lsp_adapter(
this.proxy.register_language_server(
wasm_extension.clone(),
language_server_id.clone(),
language.clone(),
@@ -202,19 +200,20 @@ impl HeadlessExtensionStore {
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
self.loaded_extensions.remove(extension_id);
let languages_to_remove = self
.loaded_languages
.remove(extension_id)
.unwrap_or_default();
self.registration_hooks
.remove_languages(&languages_to_remove, &[]);
self.proxy.remove_languages(&languages_to_remove, &[]);
for (language_server_name, language) in self
.loaded_language_servers
.remove(extension_id)
.unwrap_or_default()
{
self.registration_hooks
.remove_lsp_adapter(&language, &language_server_name);
self.proxy
.remove_language_server(&language, &language_server_name);
}
let path = self.extension_dir.join(&extension_id.to_string());
@@ -318,71 +317,3 @@ impl HeadlessExtensionStore {
Ok(proto::Ack {})
}
}
struct HeadlessRegistrationHooks {
language_registry: Arc<LanguageRegistry>,
}
impl HeadlessRegistrationHooks {
fn new(language_registry: Arc<LanguageRegistry>) -> Self {
Self { language_registry }
}
}
impl ExtensionRegistrationHooks for HeadlessRegistrationHooks {
fn register_language(
&self,
language: LanguageName,
_grammar: Option<Arc<str>>,
matcher: language::LanguageMatcher,
load: Arc<dyn Fn() -> Result<LoadedLanguage> + 'static + Send + Sync>,
) {
log::info!("registering language: {:?}", language);
self.language_registry
.register_language(language, None, matcher, load)
}
fn register_lsp_adapter(
&self,
extension: Arc<dyn Extension>,
language_server_id: LanguageServerName,
language: LanguageName,
) {
log::info!("registering lsp adapter {:?}", language);
self.language_registry.register_lsp_adapter(
language.clone(),
Arc::new(ExtensionLspAdapter::new(
extension,
language_server_id,
language,
)),
);
}
fn register_wasm_grammars(&self, grammars: Vec<(Arc<str>, PathBuf)>) {
self.language_registry.register_wasm_grammars(grammars)
}
fn remove_lsp_adapter(&self, language: &LanguageName, server_name: &LanguageServerName) {
self.language_registry
.remove_lsp_adapter(language, server_name)
}
fn remove_languages(
&self,
languages_to_remove: &[LanguageName],
_grammars_to_remove: &[Arc<str>],
) {
self.language_registry
.remove_languages(languages_to_remove, &[])
}
fn update_lsp_status(
&self,
server_name: LanguageServerName,
status: language::LanguageServerBinaryStatus,
) {
self.language_registry
.update_lsp_status(server_name, status)
}
}

View File

@@ -1,11 +1,11 @@
pub mod wit;
use crate::{ExtensionManifest, ExtensionRegistrationHooks};
use crate::ExtensionManifest;
use anyhow::{anyhow, bail, Context as _, Result};
use async_trait::async_trait;
use extension::{
CodeLabel, Command, Completion, KeyValueStoreDelegate, SlashCommand,
SlashCommandArgumentCompletion, SlashCommandOutput, Symbol, WorktreeDelegate,
CodeLabel, Command, Completion, ExtensionHostProxy, KeyValueStoreDelegate, ProjectDelegate,
SlashCommand, SlashCommandArgumentCompletion, SlashCommandOutput, Symbol, WorktreeDelegate,
};
use fs::{normalize_path, Fs};
use futures::future::LocalBoxFuture;
@@ -34,14 +34,13 @@ use wasmtime::{
};
use wasmtime_wasi::{self as wasi, WasiView};
use wit::Extension;
pub use wit::ExtensionProject;
pub struct WasmHost {
engine: Engine,
release_channel: ReleaseChannel,
http_client: Arc<dyn HttpClient>,
node_runtime: NodeRuntime,
pub registration_hooks: Arc<dyn ExtensionRegistrationHooks>,
pub(crate) proxy: Arc<ExtensionHostProxy>,
fs: Arc<dyn Fs>,
pub work_dir: PathBuf,
_main_thread_message_task: Task<()>,
@@ -238,6 +237,25 @@ impl extension::Extension for WasmExtension {
.await
}
async fn context_server_command(
&self,
context_server_id: Arc<str>,
project: Arc<dyn ProjectDelegate>,
) -> Result<Command> {
self.call(|extension, store| {
async move {
let project_resource = store.data_mut().table().push(project)?;
let command = extension
.call_context_server_command(store, context_server_id.clone(), project_resource)
.await?
.map_err(|err| anyhow!("{err}"))?;
anyhow::Ok(command.into())
}
.boxed()
})
.await
}
async fn suggest_docs_packages(&self, provider: Arc<str>) -> Result<Vec<String>> {
self.call(|extension, store| {
async move {
@@ -312,7 +330,7 @@ impl WasmHost {
fs: Arc<dyn Fs>,
http_client: Arc<dyn HttpClient>,
node_runtime: NodeRuntime,
registration_hooks: Arc<dyn ExtensionRegistrationHooks>,
proxy: Arc<ExtensionHostProxy>,
work_dir: PathBuf,
cx: &mut AppContext,
) -> Arc<Self> {
@@ -328,7 +346,7 @@ impl WasmHost {
work_dir,
http_client,
node_runtime,
registration_hooks,
proxy,
release_channel: ReleaseChannel::global(cx),
_main_thread_message_task: task,
main_thread_message_tx: tx,

View File

@@ -3,7 +3,7 @@ use crate::wasm_host::wit::since_v0_0_4;
use crate::wasm_host::WasmState;
use anyhow::Result;
use async_trait::async_trait;
use extension::WorktreeDelegate;
use extension::{ExtensionLanguageServerProxy, WorktreeDelegate};
use language::LanguageServerBinaryStatus;
use semantic_version::SemanticVersion;
use std::sync::{Arc, OnceLock};
@@ -149,8 +149,9 @@ impl ExtensionImports for WasmState {
};
self.host
.registration_hooks
.update_lsp_status(lsp::LanguageServerName(server_name.into()), status);
.proxy
.update_language_server_status(lsp::LanguageServerName(server_name.into()), status);
Ok(())
}

View File

@@ -5,7 +5,7 @@ use anyhow::{anyhow, bail, Context, Result};
use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive;
use async_trait::async_trait;
use extension::{KeyValueStoreDelegate, WorktreeDelegate};
use extension::{ExtensionLanguageServerProxy, KeyValueStoreDelegate, WorktreeDelegate};
use futures::{io::BufReader, FutureExt as _};
use futures::{lock::Mutex, AsyncReadExt};
use language::LanguageName;
@@ -495,8 +495,9 @@ impl ExtensionImports for WasmState {
};
self.host
.registration_hooks
.update_lsp_status(::lsp::LanguageServerName(server_name.into()), status);
.proxy
.update_language_server_status(::lsp::LanguageServerName(server_name.into()), status);
Ok(())
}

View File

@@ -8,7 +8,9 @@ use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive;
use async_trait::async_trait;
use context_servers::manager::ContextServerSettings;
use extension::{KeyValueStoreDelegate, WorktreeDelegate};
use extension::{
ExtensionLanguageServerProxy, KeyValueStoreDelegate, ProjectDelegate, WorktreeDelegate,
};
use futures::{io::BufReader, FutureExt as _};
use futures::{lock::Mutex, AsyncReadExt};
use language::{language_settings::AllLanguageSettings, LanguageName, LanguageServerBinaryStatus};
@@ -44,13 +46,10 @@ mod settings {
}
pub type ExtensionWorktree = Arc<dyn WorktreeDelegate>;
pub type ExtensionProject = Arc<dyn ProjectDelegate>;
pub type ExtensionKeyValueStore = Arc<dyn KeyValueStoreDelegate>;
pub type ExtensionHttpResponseStream = Arc<Mutex<::http_client::Response<AsyncBody>>>;
pub struct ExtensionProject {
pub worktree_ids: Vec<u64>,
}
pub fn linker() -> &'static Linker<WasmState> {
static LINKER: OnceLock<Linker<WasmState>> = OnceLock::new();
LINKER.get_or_init(|| super::new_linker(Extension::add_to_linker))
@@ -273,7 +272,7 @@ impl HostProject for WasmState {
project: Resource<ExtensionProject>,
) -> wasmtime::Result<Vec<u64>> {
let project = self.table.get(&project)?;
Ok(project.worktree_ids.clone())
Ok(project.worktree_ids())
}
fn drop(&mut self, _project: Resource<Project>) -> Result<()> {
@@ -685,8 +684,9 @@ impl ExtensionImports for WasmState {
};
self.host
.registration_hooks
.update_lsp_status(::lsp::LanguageServerName(server_name.into()), status);
.proxy
.update_language_server_status(::lsp::LanguageServerName(server_name.into()), status);
Ok(())
}

View File

@@ -13,21 +13,15 @@ path = "src/extensions_ui.rs"
[dependencies]
anyhow.workspace = true
assistant_slash_command.workspace = true
client.workspace = true
collections.workspace = true
context_servers.workspace = true
db.workspace = true
editor.workspace = true
extension.workspace = true
extension_host.workspace = true
fs.workspace = true
fuzzy.workspace = true
gpui.workspace = true
indexed_docs.workspace = true
language.workspace = true
log.workspace = true
lsp.workspace = true
num-format.workspace = true
picker.workspace = true
project.workspace = true
@@ -36,12 +30,10 @@ semantic_version.workspace = true
serde.workspace = true
settings.workspace = true
smallvec.workspace = true
snippet_provider.workspace = true
theme.workspace = true
ui.workspace = true
util.workspace = true
vim_mode_setting.workspace = true
wasmtime-wasi.workspace = true
workspace.workspace = true
zed_actions.workspace = true

View File

@@ -1,220 +0,0 @@
use std::{path::PathBuf, sync::Arc};
use anyhow::{anyhow, Result};
use assistant_slash_command::{ExtensionSlashCommand, SlashCommandRegistry};
use context_servers::manager::ServerCommand;
use context_servers::ContextServerFactoryRegistry;
use db::smol::future::FutureExt as _;
use extension::Extension;
use extension_host::wasm_host::ExtensionProject;
use extension_host::{extension_lsp_adapter::ExtensionLspAdapter, wasm_host};
use fs::Fs;
use gpui::{AppContext, BackgroundExecutor, Model, Task};
use indexed_docs::{ExtensionIndexedDocsProvider, IndexedDocsRegistry, ProviderId};
use language::{LanguageName, LanguageRegistry, LanguageServerBinaryStatus, LoadedLanguage};
use lsp::LanguageServerName;
use snippet_provider::SnippetRegistry;
use theme::{ThemeRegistry, ThemeSettings};
use ui::SharedString;
use wasmtime_wasi::WasiView as _;
pub struct ConcreteExtensionRegistrationHooks {
slash_command_registry: Arc<SlashCommandRegistry>,
theme_registry: Arc<ThemeRegistry>,
indexed_docs_registry: Arc<IndexedDocsRegistry>,
snippet_registry: Arc<SnippetRegistry>,
language_registry: Arc<LanguageRegistry>,
context_server_factory_registry: Model<ContextServerFactoryRegistry>,
executor: BackgroundExecutor,
}
impl ConcreteExtensionRegistrationHooks {
pub fn new(
theme_registry: Arc<ThemeRegistry>,
slash_command_registry: Arc<SlashCommandRegistry>,
indexed_docs_registry: Arc<IndexedDocsRegistry>,
snippet_registry: Arc<SnippetRegistry>,
language_registry: Arc<LanguageRegistry>,
context_server_factory_registry: Model<ContextServerFactoryRegistry>,
cx: &AppContext,
) -> Arc<dyn extension_host::ExtensionRegistrationHooks> {
Arc::new(Self {
theme_registry,
slash_command_registry,
indexed_docs_registry,
snippet_registry,
language_registry,
context_server_factory_registry,
executor: cx.background_executor().clone(),
})
}
}
impl extension_host::ExtensionRegistrationHooks for ConcreteExtensionRegistrationHooks {
fn remove_user_themes(&self, themes: Vec<SharedString>) {
self.theme_registry.remove_user_themes(&themes);
}
fn load_user_theme(&self, theme_path: PathBuf, fs: Arc<dyn fs::Fs>) -> Task<Result<()>> {
let theme_registry = self.theme_registry.clone();
self.executor
.spawn(async move { theme_registry.load_user_theme(&theme_path, fs).await })
}
fn register_slash_command(
&self,
extension: Arc<dyn Extension>,
command: extension::SlashCommand,
) {
self.slash_command_registry
.register_command(ExtensionSlashCommand::new(extension, command), false)
}
fn register_context_server(
&self,
id: Arc<str>,
extension: wasm_host::WasmExtension,
cx: &mut AppContext,
) {
self.context_server_factory_registry
.update(cx, |registry, _| {
registry.register_server_factory(
id.clone(),
Arc::new({
move |project, cx| {
log::info!(
"loading command for context server {id} from extension {}",
extension.manifest.id
);
let id = id.clone();
let extension = extension.clone();
cx.spawn(|mut cx| async move {
let extension_project =
project.update(&mut cx, |project, cx| ExtensionProject {
worktree_ids: project
.visible_worktrees(cx)
.map(|worktree| worktree.read(cx).id().to_proto())
.collect(),
})?;
let command = extension
.call({
let id = id.clone();
|extension, store| {
async move {
let project = store
.data_mut()
.table()
.push(extension_project)?;
let command = extension
.call_context_server_command(
store,
id.clone(),
project,
)
.await?
.map_err(|e| anyhow!("{}", e))?;
anyhow::Ok(command)
}
.boxed()
}
})
.await?;
log::info!("loaded command for context server {id}: {command:?}");
Ok(ServerCommand {
path: command.command,
args: command.args,
env: Some(command.env.into_iter().collect()),
})
})
}
}),
)
});
}
fn register_docs_provider(&self, extension: Arc<dyn Extension>, provider_id: Arc<str>) {
self.indexed_docs_registry
.register_provider(Box::new(ExtensionIndexedDocsProvider::new(
extension,
ProviderId(provider_id),
)));
}
fn register_snippets(&self, path: &PathBuf, snippet_contents: &str) -> Result<()> {
self.snippet_registry
.register_snippets(path, snippet_contents)
}
fn update_lsp_status(
&self,
server_name: lsp::LanguageServerName,
status: LanguageServerBinaryStatus,
) {
self.language_registry
.update_lsp_status(server_name, status);
}
fn register_lsp_adapter(
&self,
extension: Arc<dyn Extension>,
language_server_id: LanguageServerName,
language: LanguageName,
) {
self.language_registry.register_lsp_adapter(
language.clone(),
Arc::new(ExtensionLspAdapter::new(
extension,
language_server_id,
language,
)),
);
}
fn remove_lsp_adapter(
&self,
language_name: &language::LanguageName,
server_name: &lsp::LanguageServerName,
) {
self.language_registry
.remove_lsp_adapter(language_name, server_name);
}
fn remove_languages(
&self,
languages_to_remove: &[language::LanguageName],
grammars_to_remove: &[Arc<str>],
) {
self.language_registry
.remove_languages(&languages_to_remove, &grammars_to_remove);
}
fn register_wasm_grammars(&self, grammars: Vec<(Arc<str>, PathBuf)>) {
self.language_registry.register_wasm_grammars(grammars)
}
fn register_language(
&self,
language: language::LanguageName,
grammar: Option<Arc<str>>,
matcher: language::LanguageMatcher,
load: Arc<dyn Fn() -> Result<LoadedLanguage> + 'static + Send + Sync>,
) {
self.language_registry
.register_language(language, grammar, matcher, load)
}
fn reload_current_theme(&self, cx: &mut AppContext) {
ThemeSettings::reload_current_theme(cx)
}
fn list_theme_names(&self, path: PathBuf, fs: Arc<dyn Fs>) -> Task<Result<Vec<String>>> {
self.executor.spawn(async move {
let themes = theme::read_user_theme(&path, fs).await?;
Ok(themes.themes.into_iter().map(|theme| theme.name).collect())
})
}
}

View File

@@ -1,10 +1,7 @@
mod components;
mod extension_registration_hooks;
mod extension_suggest;
mod extension_version_selector;
pub use extension_registration_hooks::ConcreteExtensionRegistrationHooks;
use std::ops::DerefMut;
use std::sync::OnceLock;
use std::time::Duration;

View File

@@ -71,8 +71,16 @@ impl Match {
fn project_path(&self, project: &Project, cx: &WindowContext) -> Option<ProjectPath> {
let worktree_id = if let Some(path_match) = &self.path_match {
WorktreeId::from_usize(path_match.worktree_id)
} else if let Some(worktree) = project.visible_worktrees(cx).find(|worktree| {
worktree
.read(cx)
.root_entry()
.is_some_and(|entry| entry.is_dir())
}) {
worktree.read(cx).id()
} else {
project.worktrees(cx).next()?.read(cx).id()
// todo(): we should find_or_create a workspace.
return None;
};
let path = PathBuf::from(self.relative_path());

View File

@@ -61,4 +61,4 @@ In addition to the systems above, GPUI provides a range of smaller services that
- The `[gpui::test]` macro provides a convenient way to write tests for your GPUI applications. Tests also have their own kind of context, a `TestAppContext` which provides ways of simulating common platform input. See `app::test_context` and `test` modules for more details.
Currently, the best way to learn about these APIs is to read the Zed source code, ask us about it at a fireside hack, or drop a question in the [Zed Discord](https://discord.gg/zed-community). We're working on improving the documentation, creating more examples, and will be publishing more guides to GPUI on our [blog](https://zed.dev/blog).
Currently, the best way to learn about these APIs is to read the Zed source code, ask us about it at a fireside hack, or drop a question in the [Zed Discord](https://zed.dev/community-links). We're working on improving the documentation, creating more examples, and will be publishing more guides to GPUI on our [blog](https://zed.dev/blog).

View File

@@ -56,7 +56,7 @@
//! and [`test`] modules for more details.
//!
//! Currently, the best way to learn about these APIs is to read the Zed source code, ask us about it at a fireside hack, or drop
//! a question in the [Zed Discord](https://discord.gg/zed-community). We're working on improving the documentation, creating more examples,
//! a question in the [Zed Discord](https://zed.dev/community-links). We're working on improving the documentation, creating more examples,
//! and will be publishing more guides to GPUI on our [blog](https://zed.dev/blog).
#![deny(missing_docs)]

View File

@@ -46,6 +46,7 @@ use smallvec::SmallVec;
use std::borrow::Cow;
use std::hash::{Hash, Hasher};
use std::io::Cursor;
use std::ops;
use std::time::{Duration, Instant};
use std::{
fmt::{self, Debug},
@@ -561,6 +562,42 @@ pub(crate) trait PlatformAtlas: Send + Sync {
key: &AtlasKey,
build: &mut dyn FnMut() -> Result<Option<(Size<DevicePixels>, Cow<'a, [u8]>)>>,
) -> Result<Option<AtlasTile>>;
fn remove(&self, key: &AtlasKey);
}
struct AtlasTextureList<T> {
textures: Vec<Option<T>>,
free_list: Vec<usize>,
}
impl<T> Default for AtlasTextureList<T> {
fn default() -> Self {
Self {
textures: Vec::default(),
free_list: Vec::default(),
}
}
}
impl<T> ops::Index<usize> for AtlasTextureList<T> {
type Output = Option<T>;
fn index(&self, index: usize) -> &Self::Output {
&self.textures[index]
}
}
impl<T> AtlasTextureList<T> {
#[allow(unused)]
fn drain(&mut self) -> std::vec::Drain<Option<T>> {
self.free_list.clear();
self.textures.drain(..)
}
#[allow(dead_code)]
fn iter_mut(&mut self) -> impl DoubleEndedIterator<Item = &mut T> {
self.textures.iter_mut().flatten()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]

View File

@@ -1,6 +1,6 @@
use crate::{
AtlasKey, AtlasTextureId, AtlasTextureKind, AtlasTile, Bounds, DevicePixels, PlatformAtlas,
Point, Size,
platform::AtlasTextureList, AtlasKey, AtlasTextureId, AtlasTextureKind, AtlasTile, Bounds,
DevicePixels, PlatformAtlas, Point, Size,
};
use anyhow::Result;
use blade_graphics as gpu;
@@ -67,7 +67,7 @@ impl BladeAtlas {
pub(crate) fn clear_textures(&self, texture_kind: AtlasTextureKind) {
let mut lock = self.0.lock();
let textures = &mut lock.storage[texture_kind];
for texture in textures {
for texture in textures.iter_mut() {
texture.clear();
}
}
@@ -130,19 +130,48 @@ impl PlatformAtlas for BladeAtlas {
Ok(Some(tile))
}
}
fn remove(&self, key: &AtlasKey) {
let mut lock = self.0.lock();
let Some(id) = lock.tiles_by_key.remove(key).map(|tile| tile.texture_id) else {
return;
};
let Some(texture_slot) = lock.storage[id.kind].textures.get_mut(id.index as usize) else {
return;
};
if let Some(mut texture) = texture_slot.take() {
texture.decrement_ref_count();
if texture.is_unreferenced() {
lock.storage[id.kind]
.free_list
.push(texture.id.index as usize);
texture.destroy(&lock.gpu);
} else {
*texture_slot = Some(texture);
}
}
}
}
impl BladeAtlasState {
fn allocate(&mut self, size: Size<DevicePixels>, texture_kind: AtlasTextureKind) -> AtlasTile {
let textures = &mut self.storage[texture_kind];
textures
.iter_mut()
.rev()
.find_map(|texture| texture.allocate(size))
.unwrap_or_else(|| {
let texture = self.push_texture(size, texture_kind);
texture.allocate(size).unwrap()
})
{
let textures = &mut self.storage[texture_kind];
if let Some(tile) = textures
.iter_mut()
.rev()
.find_map(|texture| texture.allocate(size))
{
return tile;
}
}
let texture = self.push_texture(size, texture_kind);
texture.allocate(size).unwrap()
}
fn push_texture(
@@ -198,21 +227,30 @@ impl BladeAtlasState {
},
);
let textures = &mut self.storage[kind];
let texture_list = &mut self.storage[kind];
let index = texture_list.free_list.pop();
let atlas_texture = BladeAtlasTexture {
id: AtlasTextureId {
index: textures.len() as u32,
index: index.unwrap_or(texture_list.textures.len()) as u32,
kind,
},
allocator: etagere::BucketedAtlasAllocator::new(size.into()),
format,
raw,
raw_view,
live_atlas_keys: 0,
};
self.initializations.push(atlas_texture.id);
textures.push(atlas_texture);
textures.last_mut().unwrap()
if let Some(ix) = index {
texture_list.textures[ix] = Some(atlas_texture);
texture_list.textures.get_mut(ix).unwrap().as_mut().unwrap()
} else {
texture_list.textures.push(Some(atlas_texture));
texture_list.textures.last_mut().unwrap().as_mut().unwrap()
}
}
fn upload_texture(&mut self, id: AtlasTextureId, bounds: Bounds<DevicePixels>, bytes: &[u8]) {
@@ -258,13 +296,13 @@ impl BladeAtlasState {
#[derive(Default)]
struct BladeAtlasStorage {
monochrome_textures: Vec<BladeAtlasTexture>,
polychrome_textures: Vec<BladeAtlasTexture>,
path_textures: Vec<BladeAtlasTexture>,
monochrome_textures: AtlasTextureList<BladeAtlasTexture>,
polychrome_textures: AtlasTextureList<BladeAtlasTexture>,
path_textures: AtlasTextureList<BladeAtlasTexture>,
}
impl ops::Index<AtlasTextureKind> for BladeAtlasStorage {
type Output = Vec<BladeAtlasTexture>;
type Output = AtlasTextureList<BladeAtlasTexture>;
fn index(&self, kind: AtlasTextureKind) -> &Self::Output {
match kind {
crate::AtlasTextureKind::Monochrome => &self.monochrome_textures,
@@ -292,19 +330,19 @@ impl ops::Index<AtlasTextureId> for BladeAtlasStorage {
crate::AtlasTextureKind::Polychrome => &self.polychrome_textures,
crate::AtlasTextureKind::Path => &self.path_textures,
};
&textures[id.index as usize]
textures[id.index as usize].as_ref().unwrap()
}
}
impl BladeAtlasStorage {
fn destroy(&mut self, gpu: &gpu::Context) {
for mut texture in self.monochrome_textures.drain(..) {
for mut texture in self.monochrome_textures.drain().flatten() {
texture.destroy(gpu);
}
for mut texture in self.polychrome_textures.drain(..) {
for mut texture in self.polychrome_textures.drain().flatten() {
texture.destroy(gpu);
}
for mut texture in self.path_textures.drain(..) {
for mut texture in self.path_textures.drain().flatten() {
texture.destroy(gpu);
}
}
@@ -316,6 +354,7 @@ struct BladeAtlasTexture {
raw: gpu::Texture,
raw_view: gpu::TextureView,
format: gpu::TextureFormat,
live_atlas_keys: u32,
}
impl BladeAtlasTexture {
@@ -334,6 +373,7 @@ impl BladeAtlasTexture {
size,
},
};
self.live_atlas_keys += 1;
Some(tile)
}
@@ -345,6 +385,14 @@ impl BladeAtlasTexture {
fn bytes_per_pixel(&self) -> u8 {
self.format.block_info().size
}
fn decrement_ref_count(&mut self) {
self.live_atlas_keys -= 1;
}
fn is_unreferenced(&mut self) -> bool {
self.live_atlas_keys == 0
}
}
impl From<Size<DevicePixels>> for etagere::Size {

View File

@@ -776,11 +776,11 @@ impl X11Client {
},
};
let window = self.get_window(event.window)?;
window.configure(bounds);
window.configure(bounds).unwrap();
}
Event::PropertyNotify(event) => {
let window = self.get_window(event.window)?;
window.property_notify(event);
window.property_notify(event).unwrap();
}
Event::FocusIn(event) => {
let window = self.get_window(event.event)?;
@@ -1258,11 +1258,9 @@ impl LinuxClient for X11Client {
.iter()
.enumerate()
.filter_map(|(root_id, _)| {
Some(Rc::new(X11Display::new(
&state.xcb_connection,
state.scale_factor,
root_id,
)?) as Rc<dyn PlatformDisplay>)
Some(Rc::new(
X11Display::new(&state.xcb_connection, state.scale_factor, root_id).ok()?,
) as Rc<dyn PlatformDisplay>)
})
.collect()
}
@@ -1283,11 +1281,9 @@ impl LinuxClient for X11Client {
fn display(&self, id: DisplayId) -> Option<Rc<dyn PlatformDisplay>> {
let state = self.0.borrow();
Some(Rc::new(X11Display::new(
&state.xcb_connection,
state.scale_factor,
id.0 as usize,
)?))
Some(Rc::new(
X11Display::new(&state.xcb_connection, state.scale_factor, id.0 as usize).ok()?,
))
}
fn open_window(

View File

@@ -13,12 +13,17 @@ pub(crate) struct X11Display {
impl X11Display {
pub(crate) fn new(
xc: &XCBConnection,
xcb: &XCBConnection,
scale_factor: f32,
x_screen_index: usize,
) -> Option<Self> {
let screen = xc.setup().roots.get(x_screen_index).unwrap();
Some(Self {
) -> anyhow::Result<Self> {
let Some(screen) = xcb.setup().roots.get(x_screen_index) else {
return Err(anyhow::anyhow!(
"No screen found with index {}",
x_screen_index
));
};
Ok(Self {
x_screen_index,
bounds: Bounds {
origin: Default::default(),

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
use crate::{
AtlasKey, AtlasTextureId, AtlasTextureKind, AtlasTile, Bounds, DevicePixels, PlatformAtlas,
Point, Size,
platform::AtlasTextureList, AtlasKey, AtlasTextureId, AtlasTextureKind, AtlasTile, Bounds,
DevicePixels, PlatformAtlas, Point, Size,
};
use anyhow::{anyhow, Result};
use collections::FxHashMap;
@@ -42,7 +42,7 @@ impl MetalAtlas {
AtlasTextureKind::Polychrome => &mut lock.polychrome_textures,
AtlasTextureKind::Path => &mut lock.path_textures,
};
for texture in textures {
for texture in textures.iter_mut() {
texture.clear();
}
}
@@ -50,9 +50,9 @@ impl MetalAtlas {
struct MetalAtlasState {
device: AssertSend<Device>,
monochrome_textures: Vec<MetalAtlasTexture>,
polychrome_textures: Vec<MetalAtlasTexture>,
path_textures: Vec<MetalAtlasTexture>,
monochrome_textures: AtlasTextureList<MetalAtlasTexture>,
polychrome_textures: AtlasTextureList<MetalAtlasTexture>,
path_textures: AtlasTextureList<MetalAtlasTexture>,
tiles_by_key: FxHashMap<AtlasKey, AtlasTile>,
}
@@ -78,6 +78,38 @@ impl PlatformAtlas for MetalAtlas {
Ok(Some(tile))
}
}
fn remove(&self, key: &AtlasKey) {
let mut lock = self.0.lock();
let Some(id) = lock.tiles_by_key.get(key).map(|v| v.texture_id) else {
return;
};
let textures = match id.kind {
AtlasTextureKind::Monochrome => &mut lock.monochrome_textures,
AtlasTextureKind::Polychrome => &mut lock.polychrome_textures,
AtlasTextureKind::Path => &mut lock.polychrome_textures,
};
let Some(texture_slot) = textures
.textures
.iter_mut()
.find(|texture| texture.as_ref().is_some_and(|v| v.id == id))
else {
return;
};
if let Some(mut texture) = texture_slot.take() {
texture.decrement_ref_count();
if texture.is_unreferenced() {
textures.free_list.push(id.index as usize);
lock.tiles_by_key.remove(key);
} else {
*texture_slot = Some(texture);
}
}
}
}
impl MetalAtlasState {
@@ -86,20 +118,24 @@ impl MetalAtlasState {
size: Size<DevicePixels>,
texture_kind: AtlasTextureKind,
) -> Option<AtlasTile> {
let textures = match texture_kind {
AtlasTextureKind::Monochrome => &mut self.monochrome_textures,
AtlasTextureKind::Polychrome => &mut self.polychrome_textures,
AtlasTextureKind::Path => &mut self.path_textures,
};
{
let textures = match texture_kind {
AtlasTextureKind::Monochrome => &mut self.monochrome_textures,
AtlasTextureKind::Polychrome => &mut self.polychrome_textures,
AtlasTextureKind::Path => &mut self.path_textures,
};
textures
.iter_mut()
.rev()
.find_map(|texture| texture.allocate(size))
.or_else(|| {
let texture = self.push_texture(size, texture_kind);
texture.allocate(size)
})
if let Some(tile) = textures
.iter_mut()
.rev()
.find_map(|texture| texture.allocate(size))
{
return Some(tile);
}
}
let texture = self.push_texture(size, texture_kind);
texture.allocate(size)
}
fn push_texture(
@@ -140,21 +176,31 @@ impl MetalAtlasState {
texture_descriptor.set_usage(usage);
let metal_texture = self.device.new_texture(&texture_descriptor);
let textures = match kind {
let texture_list = match kind {
AtlasTextureKind::Monochrome => &mut self.monochrome_textures,
AtlasTextureKind::Polychrome => &mut self.polychrome_textures,
AtlasTextureKind::Path => &mut self.path_textures,
};
let index = texture_list.free_list.pop();
let atlas_texture = MetalAtlasTexture {
id: AtlasTextureId {
index: textures.len() as u32,
index: index.unwrap_or(texture_list.textures.len()) as u32,
kind,
},
allocator: etagere::BucketedAtlasAllocator::new(size.into()),
metal_texture: AssertSend(metal_texture),
live_atlas_keys: 0,
};
textures.push(atlas_texture);
textures.last_mut().unwrap()
if let Some(ix) = index {
texture_list.textures[ix] = Some(atlas_texture);
texture_list.textures.get_mut(ix).unwrap().as_mut().unwrap()
} else {
texture_list.textures.push(Some(atlas_texture));
texture_list.textures.last_mut().unwrap().as_mut().unwrap()
}
}
fn texture(&self, id: AtlasTextureId) -> &MetalAtlasTexture {
@@ -163,7 +209,7 @@ impl MetalAtlasState {
crate::AtlasTextureKind::Polychrome => &self.polychrome_textures,
crate::AtlasTextureKind::Path => &self.path_textures,
};
&textures[id.index as usize]
textures[id.index as usize].as_ref().unwrap()
}
}
@@ -171,6 +217,7 @@ struct MetalAtlasTexture {
id: AtlasTextureId,
allocator: BucketedAtlasAllocator,
metal_texture: AssertSend<metal::Texture>,
live_atlas_keys: u32,
}
impl MetalAtlasTexture {
@@ -189,6 +236,7 @@ impl MetalAtlasTexture {
},
padding: 0,
};
self.live_atlas_keys += 1;
Some(tile)
}
@@ -215,6 +263,14 @@ impl MetalAtlasTexture {
_ => unimplemented!(),
}
}
fn decrement_ref_count(&mut self) {
self.live_atlas_keys -= 1;
}
fn is_unreferenced(&mut self) -> bool {
self.live_atlas_keys == 0
}
}
impl From<Size<DevicePixels>> for etagere::Size {

View File

@@ -339,4 +339,9 @@ impl PlatformAtlas for TestAtlas {
Ok(Some(state.tiles[key].clone()))
}
fn remove(&self, key: &AtlasKey) {
let mut state = self.0.lock();
state.tiles.remove(key);
}
}

View File

@@ -2685,6 +2685,20 @@ impl<'a> WindowContext<'a> {
});
}
/// Removes an image from the sprite atlas.
pub fn drop_image(&mut self, data: Arc<RenderImage>) -> Result<()> {
for frame_index in 0..data.frame_count() {
let params = RenderImageParams {
image_id: data.id,
frame_index,
};
self.window.sprite_atlas.remove(&params.clone().into());
}
Ok(())
}
#[must_use]
/// Add a node to the layout tree for the current frame. Takes the `Style` of the element for which
/// layout is being requested, along with the layout ids of any children. This method is called during

View File

@@ -3,9 +3,33 @@ use std::sync::Arc;
use anyhow::Result;
use async_trait::async_trait;
use extension::Extension;
use extension::{Extension, ExtensionHostProxy, ExtensionIndexedDocsProviderProxy};
use gpui::AppContext;
use crate::{IndexedDocsDatabase, IndexedDocsProvider, PackageName, ProviderId};
use crate::{
IndexedDocsDatabase, IndexedDocsProvider, IndexedDocsRegistry, PackageName, ProviderId,
};
pub fn init(cx: &mut AppContext) {
let proxy = ExtensionHostProxy::default_global(cx);
proxy.register_indexed_docs_provider_proxy(IndexedDocsRegistryProxy {
indexed_docs_registry: IndexedDocsRegistry::global(cx),
});
}
struct IndexedDocsRegistryProxy {
indexed_docs_registry: Arc<IndexedDocsRegistry>,
}
impl ExtensionIndexedDocsProviderProxy for IndexedDocsRegistryProxy {
fn register_indexed_docs_provider(&self, extension: Arc<dyn Extension>, provider_id: Arc<str>) {
self.indexed_docs_registry
.register_provider(Box::new(ExtensionIndexedDocsProvider::new(
extension,
ProviderId(provider_id),
)));
}
}
pub struct ExtensionIndexedDocsProvider {
extension: Arc<dyn Extension>,

View File

@@ -3,7 +3,14 @@ mod providers;
mod registry;
mod store;
use gpui::AppContext;
pub use crate::extension_indexed_docs_provider::ExtensionIndexedDocsProvider;
pub use crate::providers::rustdoc::*;
pub use crate::registry::*;
pub use crate::store::*;
pub fn init(cx: &mut AppContext) {
IndexedDocsRegistry::init_global(cx);
extension_indexed_docs_provider::init(cx);
}

View File

@@ -20,7 +20,7 @@ impl IndexedDocsRegistry {
GlobalIndexedDocsRegistry::global(cx).0.clone()
}
pub fn init_global(cx: &mut AppContext) {
pub(crate) fn init_global(cx: &mut AppContext) {
GlobalIndexedDocsRegistry::set_global(
cx,
GlobalIndexedDocsRegistry(Arc::new(Self::new(cx.background_executor().clone()))),

View File

@@ -239,12 +239,7 @@ pub async fn parse_markdown_block(
Event::Start(tag) => match tag {
Tag::Paragraph => new_paragraph(text, &mut list_stack),
Tag::Heading {
level: _,
id: _,
classes: _,
attrs: _,
} => {
Tag::Heading { .. } => {
new_paragraph(text, &mut list_stack);
bold_depth += 1;
}
@@ -267,12 +262,7 @@ pub async fn parse_markdown_block(
Tag::Strikethrough => strikethrough_depth += 1,
Tag::Link {
link_type: _,
dest_url,
title: _,
id: _,
} => link_url = Some(dest_url.to_string()),
Tag::Link { dest_url, .. } => link_url = Some(dest_url.to_string()),
Tag::List(number) => {
list_stack.push((number, false));

View File

@@ -0,0 +1,25 @@
[package]
name = "language_extension"
version = "0.1.0"
edition = "2021"
publish = false
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/language_extension.rs"
[dependencies]
anyhow.workspace = true
async-trait.workspace = true
collections.workspace = true
extension.workspace = true
futures.workspace = true
gpui.workspace = true
language.workspace = true
lsp.workspace = true
serde.workspace = true
serde_json.workspace = true
util.workspace = true

View File

@@ -0,0 +1 @@
../../LICENSE-GPL

View File

@@ -1,22 +1,28 @@
use std::any::Any;
use std::ops::Range;
use std::path::PathBuf;
use std::pin::Pin;
use std::sync::Arc;
use anyhow::{Context, Result};
use async_trait::async_trait;
use collections::HashMap;
use extension::{Extension, WorktreeDelegate};
use extension::{Extension, ExtensionLanguageServerProxy, WorktreeDelegate};
use futures::{Future, FutureExt};
use gpui::AsyncAppContext;
use language::{
CodeLabel, HighlightId, Language, LanguageName, LanguageToolchainStore, LspAdapter,
LspAdapterDelegate,
CodeLabel, HighlightId, Language, LanguageName, LanguageServerBinaryStatus,
LanguageToolchainStore, LspAdapter, LspAdapterDelegate,
};
use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerBinaryOptions, LanguageServerName};
use serde::Serialize;
use serde_json::Value;
use std::ops::Range;
use std::{any::Any, path::PathBuf, pin::Pin, sync::Arc};
use util::{maybe, ResultExt};
use crate::LanguageServerRegistryProxy;
/// An adapter that allows an [`LspAdapterDelegate`] to be used as a [`WorktreeDelegate`].
pub struct WorktreeDelegateAdapter(pub Arc<dyn LspAdapterDelegate>);
struct WorktreeDelegateAdapter(pub Arc<dyn LspAdapterDelegate>);
#[async_trait]
impl WorktreeDelegate for WorktreeDelegateAdapter {
@@ -44,14 +50,50 @@ impl WorktreeDelegate for WorktreeDelegateAdapter {
}
}
pub struct ExtensionLspAdapter {
impl ExtensionLanguageServerProxy for LanguageServerRegistryProxy {
fn register_language_server(
&self,
extension: Arc<dyn Extension>,
language_server_id: LanguageServerName,
language: LanguageName,
) {
self.language_registry.register_lsp_adapter(
language.clone(),
Arc::new(ExtensionLspAdapter::new(
extension,
language_server_id,
language,
)),
);
}
fn remove_language_server(
&self,
language: &LanguageName,
language_server_id: &LanguageServerName,
) {
self.language_registry
.remove_lsp_adapter(language, language_server_id);
}
fn update_language_server_status(
&self,
language_server_id: LanguageServerName,
status: LanguageServerBinaryStatus,
) {
self.language_registry
.update_lsp_status(language_server_id, status);
}
}
struct ExtensionLspAdapter {
extension: Arc<dyn Extension>,
language_server_id: LanguageServerName,
language_name: LanguageName,
}
impl ExtensionLspAdapter {
pub fn new(
fn new(
extension: Arc<dyn Extension>,
language_server_id: LanguageServerName,
language_name: LanguageName,

View File

@@ -0,0 +1,51 @@
mod extension_lsp_adapter;
use std::path::PathBuf;
use std::sync::Arc;
use anyhow::Result;
use extension::{ExtensionGrammarProxy, ExtensionHostProxy, ExtensionLanguageProxy};
use language::{LanguageMatcher, LanguageName, LanguageRegistry, LoadedLanguage};
pub fn init(
extension_host_proxy: Arc<ExtensionHostProxy>,
language_registry: Arc<LanguageRegistry>,
) {
let language_server_registry_proxy = LanguageServerRegistryProxy { language_registry };
extension_host_proxy.register_grammar_proxy(language_server_registry_proxy.clone());
extension_host_proxy.register_language_proxy(language_server_registry_proxy.clone());
extension_host_proxy.register_language_server_proxy(language_server_registry_proxy);
}
#[derive(Clone)]
struct LanguageServerRegistryProxy {
language_registry: Arc<LanguageRegistry>,
}
impl ExtensionGrammarProxy for LanguageServerRegistryProxy {
fn register_grammars(&self, grammars: Vec<(Arc<str>, PathBuf)>) {
self.language_registry.register_wasm_grammars(grammars)
}
}
impl ExtensionLanguageProxy for LanguageServerRegistryProxy {
fn register_language(
&self,
language: LanguageName,
grammar: Option<Arc<str>>,
matcher: LanguageMatcher,
load: Arc<dyn Fn() -> Result<LoadedLanguage> + Send + Sync + 'static>,
) {
self.language_registry
.register_language(language, grammar, matcher, load);
}
fn remove_languages(
&self,
languages_to_remove: &[LanguageName],
grammars_to_remove: &[Arc<str>],
) {
self.language_registry
.remove_languages(&languages_to_remove, &grammars_to_remove);
}
}

View File

@@ -9,7 +9,7 @@ use http_client::github::{latest_github_release, GitHubLspBinaryVersion};
use language::{LanguageRegistry, LanguageToolchainStore, LspAdapter, LspAdapterDelegate};
use lsp::{LanguageServerBinary, LanguageServerName};
use node_runtime::NodeRuntime;
use project::ContextProviderWithTasks;
use project::{lsp_store::language_server_settings, ContextProviderWithTasks};
use serde_json::{json, Value};
use settings::{KeymapFile, SettingsJsonSchemaParams, SettingsStore};
use smol::{
@@ -25,7 +25,7 @@ use std::{
sync::{Arc, OnceLock},
};
use task::{TaskTemplate, TaskTemplates, VariableName};
use util::{fs::remove_matching, maybe, ResultExt};
use util::{fs::remove_matching, maybe, merge_json_value_into, ResultExt};
const SERVER_PATH: &str =
"node_modules/vscode-langservers-extracted/bin/vscode-json-language-server";
@@ -194,15 +194,26 @@ impl LspAdapter for JsonLspAdapter {
async fn workspace_configuration(
self: Arc<Self>,
_: &Arc<dyn LspAdapterDelegate>,
delegate: &Arc<dyn LspAdapterDelegate>,
_: Arc<dyn LanguageToolchainStore>,
cx: &mut AsyncAppContext,
) -> Result<Value> {
cx.update(|cx| {
let mut config = cx.update(|cx| {
self.workspace_config
.get_or_init(|| Self::get_workspace_config(self.languages.language_names(), cx))
.clone()
})
})?;
let project_options = cx.update(|cx| {
language_server_settings(delegate.as_ref(), &self.name(), cx)
.and_then(|s| s.settings.clone())
})?;
if let Some(override_options) = project_options {
merge_json_value_into(override_options, &mut config);
}
Ok(config)
}
fn language_ids(&self) -> HashMap<String, String> {

View File

@@ -13,7 +13,7 @@ pub enum ParsedMarkdownElement {
BlockQuote(ParsedMarkdownBlockQuote),
CodeBlock(ParsedMarkdownCodeBlock),
/// A paragraph of text and other inline elements.
Paragraph(ParsedMarkdownText),
Paragraph(MarkdownParagraph),
HorizontalRule(Range<usize>),
}
@@ -25,7 +25,13 @@ impl ParsedMarkdownElement {
Self::Table(table) => table.source_range.clone(),
Self::BlockQuote(block_quote) => block_quote.source_range.clone(),
Self::CodeBlock(code_block) => code_block.source_range.clone(),
Self::Paragraph(text) => text.source_range.clone(),
Self::Paragraph(text) => match &text[0] {
MarkdownParagraphChunk::Text(t) => t.source_range.clone(),
MarkdownParagraphChunk::Image(image) => match image {
Image::Web { source_range, .. } => source_range.clone(),
Image::Path { source_range, .. } => source_range.clone(),
},
},
Self::HorizontalRule(range) => range.clone(),
}
}
@@ -35,6 +41,15 @@ impl ParsedMarkdownElement {
}
}
pub type MarkdownParagraph = Vec<MarkdownParagraphChunk>;
#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq))]
pub enum MarkdownParagraphChunk {
Text(ParsedMarkdownText),
Image(Image),
}
#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq))]
pub struct ParsedMarkdown {
@@ -73,7 +88,7 @@ pub struct ParsedMarkdownCodeBlock {
pub struct ParsedMarkdownHeading {
pub source_range: Range<usize>,
pub level: HeadingLevel,
pub contents: ParsedMarkdownText,
pub contents: MarkdownParagraph,
}
#[derive(Debug, PartialEq)]
@@ -107,7 +122,7 @@ pub enum ParsedMarkdownTableAlignment {
#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq))]
pub struct ParsedMarkdownTableRow {
pub children: Vec<ParsedMarkdownText>,
pub children: Vec<MarkdownParagraph>,
}
impl Default for ParsedMarkdownTableRow {
@@ -123,7 +138,7 @@ impl ParsedMarkdownTableRow {
}
}
pub fn with_children(children: Vec<ParsedMarkdownText>) -> Self {
pub fn with_children(children: Vec<MarkdownParagraph>) -> Self {
Self { children }
}
}
@@ -135,7 +150,7 @@ pub struct ParsedMarkdownBlockQuote {
pub children: Vec<ParsedMarkdownElement>,
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct ParsedMarkdownText {
/// Where the text is located in the source Markdown document.
pub source_range: Range<usize>,
@@ -266,10 +281,112 @@ impl Display for Link {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Link::Web { url } => write!(f, "{}", url),
Link::Path {
display_path,
path: _,
} => write!(f, "{}", display_path.display()),
Link::Path { display_path, .. } => write!(f, "{}", display_path.display()),
}
}
}
/// A Markdown Image
#[derive(Debug, Clone)]
#[cfg_attr(test, derive(PartialEq))]
pub enum Image {
Web {
source_range: Range<usize>,
/// The URL of the Image.
url: String,
/// Link URL if exists.
link: Option<Link>,
/// alt text if it exists
alt_text: Option<ParsedMarkdownText>,
},
/// Image path on the filesystem.
Path {
source_range: Range<usize>,
/// The path as provided in the Markdown document.
display_path: PathBuf,
/// The absolute path to the item.
path: PathBuf,
/// Link URL if exists.
link: Option<Link>,
/// alt text if it exists
alt_text: Option<ParsedMarkdownText>,
},
}
impl Image {
pub fn identify(
source_range: Range<usize>,
file_location_directory: Option<PathBuf>,
text: String,
link: Option<Link>,
) -> Option<Image> {
if text.starts_with("http") {
return Some(Image::Web {
source_range,
url: text,
link,
alt_text: None,
});
}
let path = PathBuf::from(&text);
if path.is_absolute() {
return Some(Image::Path {
source_range,
display_path: path.clone(),
path,
link,
alt_text: None,
});
}
if let Some(file_location_directory) = file_location_directory {
let display_path = path;
let path = file_location_directory.join(text);
return Some(Image::Path {
source_range,
display_path,
path,
link,
alt_text: None,
});
}
None
}
pub fn with_alt_text(&self, alt_text: ParsedMarkdownText) -> Self {
match self {
Image::Web {
ref source_range,
ref url,
ref link,
..
} => Image::Web {
source_range: source_range.clone(),
url: url.clone(),
link: link.clone(),
alt_text: Some(alt_text),
},
Image::Path {
ref source_range,
ref display_path,
ref path,
ref link,
..
} => Image::Path {
source_range: source_range.clone(),
display_path: display_path.clone(),
path: path.clone(),
link: link.clone(),
alt_text: Some(alt_text),
},
}
}
}
impl Display for Image {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Image::Web { url, .. } => write!(f, "{}", url),
Image::Path { display_path, .. } => write!(f, "{}", display_path.display()),
}
}
}

View File

@@ -4,7 +4,7 @@ use collections::FxHashMap;
use gpui::FontWeight;
use language::LanguageRegistry;
use pulldown_cmark::{Alignment, Event, Options, Parser, Tag, TagEnd};
use std::{ops::Range, path::PathBuf, sync::Arc};
use std::{ops::Range, path::PathBuf, sync::Arc, vec};
pub async fn parse_markdown(
markdown_input: &str,
@@ -101,11 +101,11 @@ impl<'a> MarkdownParser<'a> {
| Event::Code(_)
| Event::Html(_)
| Event::FootnoteReference(_)
| Event::Start(Tag::Link { link_type: _, dest_url: _, title: _, id: _ })
| Event::Start(Tag::Link { .. })
| Event::Start(Tag::Emphasis)
| Event::Start(Tag::Strong)
| Event::Start(Tag::Strikethrough)
| Event::Start(Tag::Image { link_type: _, dest_url: _, title: _, id: _ }) => {
| Event::Start(Tag::Image { .. }) => {
true
}
_ => false,
@@ -134,12 +134,7 @@ impl<'a> MarkdownParser<'a> {
let text = self.parse_text(false, Some(source_range));
Some(vec![ParsedMarkdownElement::Paragraph(text)])
}
Tag::Heading {
level,
id: _,
classes: _,
attrs: _,
} => {
Tag::Heading { level, .. } => {
let level = *level;
self.cursor += 1;
let heading = self.parse_heading(level);
@@ -194,22 +189,23 @@ impl<'a> MarkdownParser<'a> {
&mut self,
should_complete_on_soft_break: bool,
source_range: Option<Range<usize>>,
) -> ParsedMarkdownText {
) -> MarkdownParagraph {
let source_range = source_range.unwrap_or_else(|| {
self.current()
.map(|(_, range)| range.clone())
.unwrap_or_default()
});
let mut markdown_text_like = Vec::new();
let mut text = String::new();
let mut bold_depth = 0;
let mut italic_depth = 0;
let mut strikethrough_depth = 0;
let mut link: Option<Link> = None;
let mut image: Option<Image> = None;
let mut region_ranges: Vec<Range<usize>> = vec![];
let mut regions: Vec<ParsedRegion> = vec![];
let mut highlights: Vec<(Range<usize>, MarkdownHighlight)> = vec![];
let mut link_urls: Vec<String> = vec![];
let mut link_ranges: Vec<Range<usize>> = vec![];
@@ -225,8 +221,6 @@ impl<'a> MarkdownParser<'a> {
if should_complete_on_soft_break {
break;
}
// `Some text\nSome more text` should be treated as a single line.
text.push(' ');
}
@@ -240,7 +234,6 @@ impl<'a> MarkdownParser<'a> {
Event::Text(t) => {
text.push_str(t.as_ref());
let mut style = MarkdownHighlightStyle::default();
if bold_depth > 0 {
@@ -299,7 +292,6 @@ impl<'a> MarkdownParser<'a> {
url: link.as_str().to_string(),
}),
});
last_link_len = end;
}
last_link_len
@@ -316,13 +308,63 @@ impl<'a> MarkdownParser<'a> {
}
}
if new_highlight {
highlights
.push((last_run_len..text.len(), MarkdownHighlight::Style(style)));
highlights.push((
last_run_len..text.len(),
MarkdownHighlight::Style(style.clone()),
));
}
}
}
if let Some(mut image) = image.clone() {
let is_valid_image = match image.clone() {
Image::Path { display_path, .. } => {
gpui::ImageSource::try_from(display_path).is_ok()
}
Image::Web { url, .. } => gpui::ImageSource::try_from(url).is_ok(),
};
if is_valid_image {
text.truncate(text.len() - t.len());
if !t.is_empty() {
let alt_text = ParsedMarkdownText {
source_range: source_range.clone(),
contents: t.to_string(),
highlights: highlights.clone(),
region_ranges: region_ranges.clone(),
regions: regions.clone(),
};
image = image.with_alt_text(alt_text);
} else {
let alt_text = ParsedMarkdownText {
source_range: source_range.clone(),
contents: "img".to_string(),
highlights: highlights.clone(),
region_ranges: region_ranges.clone(),
regions: regions.clone(),
};
image = image.with_alt_text(alt_text);
}
if !text.is_empty() {
let parsed_regions =
MarkdownParagraphChunk::Text(ParsedMarkdownText {
source_range: source_range.clone(),
contents: text.clone(),
highlights: highlights.clone(),
region_ranges: region_ranges.clone(),
regions: regions.clone(),
});
text = String::new();
highlights = vec![];
region_ranges = vec![];
regions = vec![];
markdown_text_like.push(parsed_regions);
}
// Note: This event means "inline code" and not "code block"
let parsed_image = MarkdownParagraphChunk::Image(image.clone());
markdown_text_like.push(parsed_image);
style = MarkdownHighlightStyle::default();
}
style.underline = true;
};
}
Event::Code(t) => {
text.push_str(t.as_ref());
region_ranges.push(prev_len..text.len());
@@ -336,46 +378,44 @@ impl<'a> MarkdownParser<'a> {
}),
));
}
regions.push(ParsedRegion {
code: true,
link: link.clone(),
});
}
Event::Start(tag) => match tag {
Tag::Emphasis => italic_depth += 1,
Tag::Strong => bold_depth += 1,
Tag::Strikethrough => strikethrough_depth += 1,
Tag::Link {
link_type: _,
dest_url,
title: _,
id: _,
} => {
Tag::Link { dest_url, .. } => {
link = Link::identify(
self.file_location_directory.clone(),
dest_url.to_string(),
);
}
Tag::Image { dest_url, .. } => {
image = Image::identify(
source_range.clone(),
self.file_location_directory.clone(),
dest_url.to_string(),
link.clone(),
);
}
_ => {
break;
}
},
Event::End(tag) => match tag {
TagEnd::Emphasis => {
italic_depth -= 1;
}
TagEnd::Strong => {
bold_depth -= 1;
}
TagEnd::Strikethrough => {
strikethrough_depth -= 1;
}
TagEnd::Emphasis => italic_depth -= 1,
TagEnd::Strong => bold_depth -= 1,
TagEnd::Strikethrough => strikethrough_depth -= 1,
TagEnd::Link => {
link = None;
}
TagEnd::Image => {
image = None;
}
TagEnd::Paragraph => {
self.cursor += 1;
break;
@@ -384,7 +424,6 @@ impl<'a> MarkdownParser<'a> {
break;
}
},
_ => {
break;
}
@@ -392,14 +431,16 @@ impl<'a> MarkdownParser<'a> {
self.cursor += 1;
}
ParsedMarkdownText {
source_range,
contents: text,
highlights,
regions,
region_ranges,
if !text.is_empty() {
markdown_text_like.push(MarkdownParagraphChunk::Text(ParsedMarkdownText {
source_range: source_range.clone(),
contents: text,
highlights,
regions,
region_ranges,
}));
}
markdown_text_like
}
fn parse_heading(&mut self, level: pulldown_cmark::HeadingLevel) -> ParsedMarkdownHeading {
@@ -708,7 +749,6 @@ impl<'a> MarkdownParser<'a> {
}
}
}
let highlights = if let Some(language) = &language {
if let Some(registry) = &self.language_registry {
let rope: language::Rope = code.as_str().into();
@@ -735,10 +775,14 @@ impl<'a> MarkdownParser<'a> {
#[cfg(test)]
mod tests {
use core::panic;
use super::*;
use gpui::BackgroundExecutor;
use language::{tree_sitter_rust, HighlightId, Language, LanguageConfig, LanguageMatcher};
use language::{
tree_sitter_rust, HighlightId, Language, LanguageConfig, LanguageMatcher, LanguageRegistry,
};
use pretty_assertions::assert_eq;
use ParsedMarkdownListItemType::*;
@@ -810,20 +854,29 @@ mod tests {
assert_eq!(parsed.children.len(), 1);
assert_eq!(
parsed.children[0],
ParsedMarkdownElement::Paragraph(ParsedMarkdownText {
source_range: 0..35,
contents: "Some bostrikethroughld text".to_string(),
highlights: Vec::new(),
region_ranges: Vec::new(),
regions: Vec::new(),
})
ParsedMarkdownElement::Paragraph(vec![MarkdownParagraphChunk::Text(
ParsedMarkdownText {
source_range: 0..35,
contents: "Some bostrikethroughld text".to_string(),
highlights: Vec::new(),
region_ranges: Vec::new(),
regions: Vec::new(),
}
)])
);
let paragraph = if let ParsedMarkdownElement::Paragraph(text) = &parsed.children[0] {
let new_text = if let ParsedMarkdownElement::Paragraph(text) = &parsed.children[0] {
text
} else {
panic!("Expected a paragraph");
};
let paragraph = if let MarkdownParagraphChunk::Text(text) = &new_text[0] {
text
} else {
panic!("Expected a text");
};
assert_eq!(
paragraph.highlights,
vec![
@@ -871,6 +924,11 @@ mod tests {
parsed.children,
vec![p("Checkout this https://zed.dev link", 0..34)]
);
}
#[gpui::test]
async fn test_image_links_detection() {
let parsed = parse("![test](https://blog.logrocket.com/wp-content/uploads/2024/04/exploring-zed-open-source-code-editor-rust-2.png)").await;
let paragraph = if let ParsedMarkdownElement::Paragraph(text) = &parsed.children[0] {
text
@@ -878,25 +936,22 @@ mod tests {
panic!("Expected a paragraph");
};
assert_eq!(
paragraph.highlights,
vec![(
14..29,
MarkdownHighlight::Style(MarkdownHighlightStyle {
underline: true,
..Default::default()
}),
)]
paragraph[0],
MarkdownParagraphChunk::Image(Image::Web {
source_range: 0..111,
url: "https://blog.logrocket.com/wp-content/uploads/2024/04/exploring-zed-open-source-code-editor-rust-2.png".to_string(),
link: None,
alt_text: Some(
ParsedMarkdownText {
source_range: 0..111,
contents: "test".to_string(),
highlights: vec![],
region_ranges: vec![],
regions: vec![],
},
),
},)
);
assert_eq!(
paragraph.regions,
vec![ParsedRegion {
code: false,
link: Some(Link::Web {
url: "https://zed.dev".to_string()
}),
}]
);
assert_eq!(paragraph.region_ranges, vec![14..29]);
}
#[gpui::test]
@@ -1169,7 +1224,7 @@ Some other content
vec![
list_item(0..8, 1, Unordered, vec![p("code", 2..8)]),
list_item(9..19, 1, Unordered, vec![p("bold", 11..19)]),
list_item(20..49, 1, Unordered, vec![p("link", 22..49)],)
list_item(20..49, 1, Unordered, vec![p("link", 22..49)],),
],
);
}
@@ -1312,7 +1367,7 @@ fn main() {
))
}
fn h1(contents: ParsedMarkdownText, source_range: Range<usize>) -> ParsedMarkdownElement {
fn h1(contents: MarkdownParagraph, source_range: Range<usize>) -> ParsedMarkdownElement {
ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
source_range,
level: HeadingLevel::H1,
@@ -1320,7 +1375,7 @@ fn main() {
})
}
fn h2(contents: ParsedMarkdownText, source_range: Range<usize>) -> ParsedMarkdownElement {
fn h2(contents: MarkdownParagraph, source_range: Range<usize>) -> ParsedMarkdownElement {
ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
source_range,
level: HeadingLevel::H2,
@@ -1328,7 +1383,7 @@ fn main() {
})
}
fn h3(contents: ParsedMarkdownText, source_range: Range<usize>) -> ParsedMarkdownElement {
fn h3(contents: MarkdownParagraph, source_range: Range<usize>) -> ParsedMarkdownElement {
ParsedMarkdownElement::Heading(ParsedMarkdownHeading {
source_range,
level: HeadingLevel::H3,
@@ -1340,14 +1395,14 @@ fn main() {
ParsedMarkdownElement::Paragraph(text(contents, source_range))
}
fn text(contents: &str, source_range: Range<usize>) -> ParsedMarkdownText {
ParsedMarkdownText {
fn text(contents: &str, source_range: Range<usize>) -> MarkdownParagraph {
vec![MarkdownParagraphChunk::Text(ParsedMarkdownText {
highlights: Vec::new(),
region_ranges: Vec::new(),
regions: Vec::new(),
source_range,
contents: contents.to_string(),
}
})]
}
fn block_quote(
@@ -1401,7 +1456,7 @@ fn main() {
}
}
fn row(children: Vec<ParsedMarkdownText>) -> ParsedMarkdownTableRow {
fn row(children: Vec<MarkdownParagraph>) -> ParsedMarkdownTableRow {
ParsedMarkdownTableRow { children }
}

View File

@@ -1,29 +1,33 @@
use crate::markdown_elements::{
HeadingLevel, Link, ParsedMarkdown, ParsedMarkdownBlockQuote, ParsedMarkdownCodeBlock,
ParsedMarkdownElement, ParsedMarkdownHeading, ParsedMarkdownListItem,
ParsedMarkdownListItemType, ParsedMarkdownTable, ParsedMarkdownTableAlignment,
ParsedMarkdownTableRow, ParsedMarkdownText,
HeadingLevel, Image, Link, MarkdownParagraph, MarkdownParagraphChunk, ParsedMarkdown,
ParsedMarkdownBlockQuote, ParsedMarkdownCodeBlock, ParsedMarkdownElement,
ParsedMarkdownHeading, ParsedMarkdownListItem, ParsedMarkdownListItemType, ParsedMarkdownTable,
ParsedMarkdownTableAlignment, ParsedMarkdownTableRow, ParsedMarkdownText,
};
use gpui::{
div, px, rems, AbsoluteLength, AnyElement, ClipboardItem, DefiniteLength, Div, Element,
ElementId, HighlightStyle, Hsla, InteractiveText, IntoElement, Keystroke, Length, Modifiers,
ParentElement, SharedString, Styled, StyledText, TextStyle, WeakView, WindowContext,
div, img, px, rems, AbsoluteLength, AnyElement, ClipboardItem, DefiniteLength, Div, Element,
ElementId, HighlightStyle, Hsla, ImageSource, InteractiveText, IntoElement, Keystroke, Length,
Modifiers, ParentElement, Resource, SharedString, Styled, StyledText, TextStyle, WeakView,
WindowContext,
};
use settings::Settings;
use std::{
ops::{Mul, Range},
path::Path,
sync::Arc,
vec,
};
use theme::{ActiveTheme, SyntaxTheme, ThemeSettings};
use ui::{
h_flex, relative, v_flex, Checkbox, Clickable, FluentBuilder, IconButton, IconName, IconSize,
InteractiveElement, LinkPreview, Selection, StatefulInteractiveElement, StyledExt, Tooltip,
VisibleOnHover,
InteractiveElement, LinkPreview, Selection, StatefulInteractiveElement, StyledExt, StyledImage,
Tooltip, VisibleOnHover,
};
use workspace::Workspace;
type CheckboxClickedCallback = Arc<Box<dyn Fn(bool, Range<usize>, &mut WindowContext)>>;
#[derive(Clone)]
pub struct RenderContext {
workspace: Option<WeakView<Workspace>>,
next_id: usize,
@@ -153,7 +157,7 @@ fn render_markdown_heading(parsed: &ParsedMarkdownHeading, cx: &mut RenderContex
.text_color(color)
.pt(rems(0.15))
.pb_1()
.child(render_markdown_text(&parsed.contents, cx))
.children(render_markdown_text(&parsed.contents, cx))
.whitespace_normal()
.into_any()
}
@@ -231,17 +235,29 @@ fn render_markdown_list_item(
cx.with_common_p(item).into_any()
}
fn paragraph_len(paragraphs: &MarkdownParagraph) -> usize {
paragraphs
.iter()
.map(|paragraph| match paragraph {
MarkdownParagraphChunk::Text(text) => text.contents.len(),
// TODO: Scale column width based on image size
MarkdownParagraphChunk::Image(_) => 1,
})
.sum()
}
fn render_markdown_table(parsed: &ParsedMarkdownTable, cx: &mut RenderContext) -> AnyElement {
let mut max_lengths: Vec<usize> = vec![0; parsed.header.children.len()];
for (index, cell) in parsed.header.children.iter().enumerate() {
let length = cell.contents.len();
let length = paragraph_len(&cell);
max_lengths[index] = length;
}
for row in &parsed.body {
for (index, cell) in row.children.iter().enumerate() {
let length = cell.contents.len();
let length = paragraph_len(&cell);
if length > max_lengths[index] {
max_lengths[index] = length;
}
@@ -307,11 +323,10 @@ fn render_markdown_table_row(
};
let max_width = max_column_widths.get(index).unwrap_or(&0.0);
let mut cell = container
.w(Length::Definite(relative(*max_width)))
.h_full()
.child(contents)
.children(contents)
.px_2()
.py_1()
.border_color(cx.border_color);
@@ -398,18 +413,219 @@ fn render_markdown_code_block(
.into_any()
}
fn render_markdown_paragraph(parsed: &ParsedMarkdownText, cx: &mut RenderContext) -> AnyElement {
fn render_markdown_paragraph(parsed: &MarkdownParagraph, cx: &mut RenderContext) -> AnyElement {
cx.with_common_p(div())
.child(render_markdown_text(parsed, cx))
.children(render_markdown_text(parsed, cx))
.flex()
.into_any_element()
}
fn render_markdown_text(parsed: &ParsedMarkdownText, cx: &mut RenderContext) -> AnyElement {
let element_id = cx.next_id(&parsed.source_range);
fn render_markdown_text(parsed_new: &MarkdownParagraph, cx: &mut RenderContext) -> Vec<AnyElement> {
let mut any_element = vec![];
// these values are cloned in-order satisfy borrow checker
let syntax_theme = cx.syntax_theme.clone();
let workspace_clone = cx.workspace.clone();
let code_span_bg_color = cx.code_span_background_color;
let text_style = cx.text_style.clone();
for parsed_region in parsed_new {
match parsed_region {
MarkdownParagraphChunk::Text(parsed) => {
let element_id = cx.next_id(&parsed.source_range);
let highlights = gpui::combine_highlights(
parsed.highlights.iter().filter_map(|(range, highlight)| {
highlight
.to_highlight_style(&syntax_theme)
.map(|style| (range.clone(), style))
}),
parsed.regions.iter().zip(&parsed.region_ranges).filter_map(
|(region, range)| {
if region.code {
Some((
range.clone(),
HighlightStyle {
background_color: Some(code_span_bg_color),
..Default::default()
},
))
} else {
None
}
},
),
);
let mut links = Vec::new();
let mut link_ranges = Vec::new();
for (range, region) in parsed.region_ranges.iter().zip(&parsed.regions) {
if let Some(link) = region.link.clone() {
links.push(link);
link_ranges.push(range.clone());
}
}
let workspace = workspace_clone.clone();
let element = div()
.child(
InteractiveText::new(
element_id,
StyledText::new(parsed.contents.clone())
.with_highlights(&text_style, highlights),
)
.tooltip({
let links = links.clone();
let link_ranges = link_ranges.clone();
move |idx, cx| {
for (ix, range) in link_ranges.iter().enumerate() {
if range.contains(&idx) {
return Some(LinkPreview::new(&links[ix].to_string(), cx));
}
}
None
}
})
.on_click(
link_ranges,
move |clicked_range_ix, window_cx| match &links[clicked_range_ix] {
Link::Web { url } => window_cx.open_url(url),
Link::Path { path, .. } => {
if let Some(workspace) = &workspace {
_ = workspace.update(window_cx, |workspace, cx| {
workspace
.open_abs_path(path.clone(), false, cx)
.detach();
});
}
}
},
),
)
.into_any();
any_element.push(element);
}
MarkdownParagraphChunk::Image(image) => {
let (link, source_range, image_source, alt_text) = match image {
Image::Web {
link,
source_range,
url,
alt_text,
} => (
link,
source_range,
Resource::Uri(url.clone().into()),
alt_text,
),
Image::Path {
link,
source_range,
path,
alt_text,
..
} => {
let image_path = Path::new(path.to_str().unwrap());
(
link,
source_range,
Resource::Path(Arc::from(image_path)),
alt_text,
)
}
};
let element_id = cx.next_id(source_range);
match link {
None => {
let fallback_workspace = workspace_clone.clone();
let fallback_syntax_theme = syntax_theme.clone();
let fallback_text_style = text_style.clone();
let fallback_alt_text = alt_text.clone();
let element_id_new = element_id.clone();
let element = div()
.child(img(ImageSource::Resource(image_source)).with_fallback({
move || {
fallback_text(
fallback_alt_text.clone().unwrap(),
element_id.clone(),
&fallback_syntax_theme,
code_span_bg_color,
fallback_workspace.clone(),
&fallback_text_style,
)
}
}))
.id(element_id_new)
.into_any();
any_element.push(element);
}
Some(link) => {
let link_click = link.clone();
let link_tooltip = link.clone();
let fallback_workspace = workspace_clone.clone();
let fallback_syntax_theme = syntax_theme.clone();
let fallback_text_style = text_style.clone();
let fallback_alt_text = alt_text.clone();
let element_id_new = element_id.clone();
let image_element = div()
.child(img(ImageSource::Resource(image_source)).with_fallback({
move || {
fallback_text(
fallback_alt_text.clone().unwrap(),
element_id.clone(),
&fallback_syntax_theme,
code_span_bg_color,
fallback_workspace.clone(),
&fallback_text_style,
)
}
}))
.id(element_id_new)
.tooltip(move |cx| LinkPreview::new(&link_tooltip.to_string(), cx))
.on_click({
let workspace = workspace_clone.clone();
move |_event, window_cx| match &link_click {
Link::Web { url } => window_cx.open_url(url),
Link::Path { path, .. } => {
if let Some(workspace) = &workspace {
_ = workspace.update(window_cx, |workspace, cx| {
workspace
.open_abs_path(path.clone(), false, cx)
.detach();
});
}
}
}
})
.into_any();
any_element.push(image_element);
}
}
}
}
}
any_element
}
fn render_markdown_rule(cx: &mut RenderContext) -> AnyElement {
let rule = div().w_full().h(px(2.)).bg(cx.border_color);
div().pt_3().pb_3().child(rule).into_any()
}
fn fallback_text(
parsed: ParsedMarkdownText,
source_range: ElementId,
syntax_theme: &theme::SyntaxTheme,
code_span_bg_color: Hsla,
workspace: Option<WeakView<Workspace>>,
text_style: &TextStyle,
) -> AnyElement {
let element_id = source_range;
let highlights = gpui::combine_highlights(
parsed.highlights.iter().filter_map(|(range, highlight)| {
let highlight = highlight.to_highlight_style(&cx.syntax_theme)?;
let highlight = highlight.to_highlight_style(syntax_theme)?;
Some((range.clone(), highlight))
}),
parsed
@@ -421,7 +637,7 @@ fn render_markdown_text(parsed: &ParsedMarkdownText, cx: &mut RenderContext) ->
Some((
range.clone(),
HighlightStyle {
background_color: Some(cx.code_span_background_color),
background_color: Some(code_span_bg_color),
..Default::default()
},
))
@@ -430,7 +646,6 @@ fn render_markdown_text(parsed: &ParsedMarkdownText, cx: &mut RenderContext) ->
}
}),
);
let mut links = Vec::new();
let mut link_ranges = Vec::new();
for (range, region) in parsed.region_ranges.iter().zip(&parsed.regions) {
@@ -439,45 +654,38 @@ fn render_markdown_text(parsed: &ParsedMarkdownText, cx: &mut RenderContext) ->
link_ranges.push(range.clone());
}
}
let workspace = cx.workspace.clone();
InteractiveText::new(
element_id,
StyledText::new(parsed.contents.clone()).with_highlights(&cx.text_style, highlights),
)
.tooltip({
let links = links.clone();
let link_ranges = link_ranges.clone();
move |idx, cx| {
for (ix, range) in link_ranges.iter().enumerate() {
if range.contains(&idx) {
return Some(LinkPreview::new(&links[ix].to_string(), cx));
let element = div()
.child(
InteractiveText::new(
element_id,
StyledText::new(parsed.contents.clone()).with_highlights(text_style, highlights),
)
.tooltip({
let links = links.clone();
let link_ranges = link_ranges.clone();
move |idx, cx| {
for (ix, range) in link_ranges.iter().enumerate() {
if range.contains(&idx) {
return Some(LinkPreview::new(&links[ix].to_string(), cx));
}
}
None
}
}
None
}
})
.on_click(
link_ranges,
move |clicked_range_ix, window_cx| match &links[clicked_range_ix] {
Link::Web { url } => window_cx.open_url(url),
Link::Path {
path,
display_path: _,
} => {
if let Some(workspace) = &workspace {
_ = workspace.update(window_cx, |workspace, cx| {
workspace.open_abs_path(path.clone(), false, cx).detach();
});
}
}
},
)
.into_any_element()
}
fn render_markdown_rule(cx: &mut RenderContext) -> AnyElement {
let rule = div().w_full().h(px(2.)).bg(cx.border_color);
div().pt_3().pb_3().child(rule).into_any()
})
.on_click(
link_ranges,
move |clicked_range_ix, window_cx| match &links[clicked_range_ix] {
Link::Web { url } => window_cx.open_url(url),
Link::Path { path, .. } => {
if let Some(workspace) = &workspace {
_ = workspace.update(window_cx, |workspace, cx| {
workspace.open_abs_path(path.clone(), false, cx).detach();
});
}
}
},
),
)
.into_any();
return element;
}

View File

@@ -238,11 +238,8 @@ impl NotificationStore {
) -> Result<()> {
this.update(&mut cx, |this, cx| {
if let Some(notification) = envelope.payload.notification {
if let Some(rpc::Notification::ChannelMessageMention {
message_id,
sender_id: _,
channel_id: _,
}) = Notification::from_proto(&notification)
if let Some(rpc::Notification::ChannelMessageMention { message_id, .. }) =
Notification::from_proto(&notification)
{
let fetch_message_task = this.channel_store.update(cx, |this, cx| {
this.fetch_channel_messages(vec![message_id], cx)

View File

@@ -174,6 +174,8 @@ impl Project {
command_label: spawn_task.command_label,
hide: spawn_task.hide,
status: TaskStatus::Running,
show_summary: spawn_task.show_summary,
show_command: spawn_task.show_command,
completion_rx,
});

View File

@@ -29,6 +29,7 @@ chrono.workspace = true
clap.workspace = true
client.workspace = true
env_logger.workspace = true
extension.workspace = true
extension_host.workspace = true
fs.workspace = true
futures.workspace = true
@@ -37,6 +38,7 @@ git_hosting_providers.workspace = true
gpui.workspace = true
http_client.workspace = true
language.workspace = true
language_extension.workspace = true
languages.workspace = true
log.workspace = true
lsp.workspace = true

View File

@@ -1,4 +1,5 @@
use anyhow::{anyhow, Result};
use extension::ExtensionHostProxy;
use extension_host::headless_host::HeadlessExtensionStore;
use fs::Fs;
use gpui::{AppContext, AsyncAppContext, Context as _, Model, ModelContext, PromptLevel};
@@ -47,6 +48,7 @@ pub struct HeadlessAppState {
pub http_client: Arc<dyn HttpClient>,
pub node_runtime: NodeRuntime,
pub languages: Arc<LanguageRegistry>,
pub extension_host_proxy: Arc<ExtensionHostProxy>,
}
impl HeadlessProject {
@@ -63,9 +65,11 @@ impl HeadlessProject {
http_client,
node_runtime,
languages,
extension_host_proxy: proxy,
}: HeadlessAppState,
cx: &mut ModelContext<Self>,
) -> Self {
language_extension::init(proxy.clone(), languages.clone());
languages::init(languages.clone(), node_runtime.clone(), cx);
let worktree_store = cx.new_model(|cx| {
@@ -152,8 +156,8 @@ impl HeadlessProject {
let extensions = HeadlessExtensionStore::new(
fs.clone(),
http_client.clone(),
languages.clone(),
paths::remote_extensions_dir().to_path_buf(),
proxy,
node_runtime,
cx,
);

View File

@@ -1,6 +1,7 @@
use crate::headless_project::HeadlessProject;
use client::{Client, UserStore};
use clock::FakeSystemClock;
use extension::ExtensionHostProxy;
use fs::{FakeFs, Fs};
use gpui::{Context, Model, SemanticVersion, TestAppContext};
use http_client::{BlockedHttpClient, FakeHttpClient};
@@ -1234,6 +1235,7 @@ pub async fn init_test(
let http_client = Arc::new(BlockedHttpClient);
let node_runtime = NodeRuntime::unavailable();
let languages = Arc::new(LanguageRegistry::new(cx.executor()));
let proxy = Arc::new(ExtensionHostProxy::new());
server_cx.update(HeadlessProject::init);
let headless = server_cx.new_model(|cx| {
client::init_settings(cx);
@@ -1245,6 +1247,7 @@ pub async fn init_test(
http_client,
node_runtime,
languages,
extension_host_proxy: proxy,
},
cx,
)

View File

@@ -3,6 +3,7 @@ use crate::HeadlessProject;
use anyhow::{anyhow, Context, Result};
use chrono::Utc;
use client::{telemetry, ProxySettings};
use extension::ExtensionHostProxy;
use fs::{Fs, RealFs};
use futures::channel::mpsc;
use futures::{select, select_biased, AsyncRead, AsyncWrite, AsyncWriteExt, FutureExt, SinkExt};
@@ -434,6 +435,9 @@ pub fn execute_run(
GitHostingProviderRegistry::set_global(git_hosting_provider_registry, cx);
git_hosting_providers::init(cx);
extension::init(cx);
let extension_host_proxy = ExtensionHostProxy::global(cx);
let project = cx.new_model(|cx| {
let fs = Arc::new(RealFs::new(Default::default(), None));
let node_settings_rx = initialize_settings(session.clone(), fs.clone(), cx);
@@ -466,6 +470,7 @@ pub fn execute_run(
http_client,
node_runtime,
languages,
extension_host_proxy,
},
cx,
)

View File

@@ -114,7 +114,7 @@ impl Cell {
id,
metadata,
source,
attachments: _,
..
} => {
let source = source.join("");

View File

@@ -258,8 +258,9 @@ impl ReplStore {
runtime_specification.kernelspec.language.to_lowercase()
== language_at_cursor.code_fence_block_name().to_lowercase()
}
KernelSpecification::Remote(_) => {
unimplemented!()
KernelSpecification::Remote(remote_spec) => {
remote_spec.kernelspec.language.to_lowercase()
== language_at_cursor.code_fence_block_name().to_lowercase()
}
})
.cloned()

View File

@@ -310,12 +310,7 @@ pub fn render_markdown_mut(
}
Event::Start(tag) => match tag {
Tag::Paragraph => new_paragraph(text, &mut list_stack),
Tag::Heading {
level: _,
id: _,
classes: _,
attrs: _,
} => {
Tag::Heading { .. } => {
new_paragraph(text, &mut list_stack);
bold_depth += 1;
}
@@ -333,12 +328,7 @@ pub fn render_markdown_mut(
Tag::Emphasis => italic_depth += 1,
Tag::Strong => bold_depth += 1,
Tag::Strikethrough => strikethrough_depth += 1,
Tag::Link {
link_type: _,
dest_url,
title: _,
id: _,
} => link_url = Some(dest_url.to_string()),
Tag::Link { dest_url, .. } => link_url = Some(dest_url.to_string()),
Tag::List(number) => {
list_stack.push((number, false));
}

View File

@@ -11,6 +11,7 @@ workspace = true
[dependencies]
anyhow.workspace = true
collections.workspace = true
extension.workspace = true
fs.workspace = true
futures.workspace = true
gpui.workspace = true

View File

@@ -0,0 +1,26 @@
use std::path::PathBuf;
use std::sync::Arc;
use anyhow::Result;
use extension::{ExtensionHostProxy, ExtensionSnippetProxy};
use gpui::AppContext;
use crate::SnippetRegistry;
pub fn init(cx: &mut AppContext) {
let proxy = ExtensionHostProxy::default_global(cx);
proxy.register_snippet_proxy(SnippetRegistryProxy {
snippet_registry: SnippetRegistry::global(cx),
});
}
struct SnippetRegistryProxy {
snippet_registry: Arc<SnippetRegistry>,
}
impl ExtensionSnippetProxy for SnippetRegistryProxy {
fn register_snippet(&self, path: &PathBuf, snippet_contents: &str) -> Result<()> {
self.snippet_registry
.register_snippets(path, snippet_contents)
}
}

View File

@@ -1,3 +1,4 @@
mod extension_snippet;
mod format;
mod registry;
@@ -18,6 +19,7 @@ use util::ResultExt;
pub fn init(cx: &mut AppContext) {
SnippetRegistry::init_global(cx);
extension_snippet::init(cx);
}
// Is `None` if the snippet file is global.

View File

@@ -51,6 +51,10 @@ pub struct SpawnInTerminal {
pub hide: HideStrategy,
/// Which shell to use when spawning the task.
pub shell: Shell,
/// Whether to show the task summary line in the task output (sucess/failure).
pub show_summary: bool,
/// Whether to show the command line in the task output.
pub show_command: bool,
}
/// A final form of the [`TaskTemplate`], that got resolved with a particualar [`TaskContext`] and now is ready to spawn the actual task.

View File

@@ -1,4 +1,5 @@
use std::path::PathBuf;
use util::serde::default_true;
use anyhow::{bail, Context};
use collections::{HashMap, HashSet};
@@ -57,6 +58,12 @@ pub struct TaskTemplate {
/// Which shell to use when spawning the task.
#[serde(default)]
pub shell: Shell,
/// Whether to show the task line in the task output.
#[serde(default = "default_true")]
pub show_summary: bool,
/// Whether to show the command line in the task output.
#[serde(default = "default_true")]
pub show_command: bool,
}
/// What to do with the terminal pane and tab, after the command was started.
@@ -230,6 +237,8 @@ impl TaskTemplate {
reveal: self.reveal,
hide: self.hide,
shell: self.shell.clone(),
show_summary: self.show_summary,
show_command: self.show_command,
}),
})
}

View File

@@ -639,6 +639,8 @@ pub struct TaskState {
pub status: TaskStatus,
pub completion_rx: Receiver<()>,
pub hide: HideStrategy,
pub show_summary: bool,
pub show_command: bool,
}
/// A status of the current terminal tab's task.
@@ -1760,11 +1762,22 @@ impl Terminal {
};
let (finished_successfully, task_line, command_line) = task_summary(task, error_code);
// SAFETY: the invocation happens on non `TaskStatus::Running` tasks, once,
// after either `AlacTermEvent::Exit` or `AlacTermEvent::ChildExit` events that are spawned
// when Zed task finishes and no more output is made.
// After the task summary is output once, no more text is appended to the terminal.
unsafe { append_text_to_term(&mut self.term.lock(), &[&task_line, &command_line]) };
let mut lines_to_show = Vec::new();
if task.show_summary {
lines_to_show.push(task_line.as_str());
}
if task.show_command {
lines_to_show.push(command_line.as_str());
}
if !lines_to_show.is_empty() {
// SAFETY: the invocation happens on non `TaskStatus::Running` tasks, once,
// after either `AlacTermEvent::Exit` or `AlacTermEvent::ChildExit` events that are spawned
// when Zed task finishes and no more output is made.
// After the task summary is output once, no more text is appended to the terminal.
unsafe { append_text_to_term(&mut self.term.lock(), &lines_to_show) };
}
match task.hide {
HideStrategy::Never => {}
HideStrategy::Always => {

View File

@@ -0,0 +1,19 @@
[package]
name = "theme_extension"
version = "0.1.0"
edition = "2021"
publish = false
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/theme_extension.rs"
[dependencies]
anyhow.workspace = true
extension.workspace = true
fs.workspace = true
gpui.workspace = true
theme.workspace = true

View File

@@ -0,0 +1 @@
../../LICENSE-GPL

View File

@@ -0,0 +1,47 @@
use std::path::PathBuf;
use std::sync::Arc;
use anyhow::Result;
use extension::{ExtensionHostProxy, ExtensionThemeProxy};
use fs::Fs;
use gpui::{AppContext, BackgroundExecutor, SharedString, Task};
use theme::{ThemeRegistry, ThemeSettings};
pub fn init(
extension_host_proxy: Arc<ExtensionHostProxy>,
theme_registry: Arc<ThemeRegistry>,
executor: BackgroundExecutor,
) {
extension_host_proxy.register_theme_proxy(ThemeRegistryProxy {
theme_registry,
executor,
});
}
struct ThemeRegistryProxy {
theme_registry: Arc<ThemeRegistry>,
executor: BackgroundExecutor,
}
impl ExtensionThemeProxy for ThemeRegistryProxy {
fn list_theme_names(&self, theme_path: PathBuf, fs: Arc<dyn Fs>) -> Task<Result<Vec<String>>> {
self.executor.spawn(async move {
let themes = theme::read_user_theme(&theme_path, fs).await?;
Ok(themes.themes.into_iter().map(|theme| theme.name).collect())
})
}
fn remove_user_themes(&self, themes: Vec<SharedString>) {
self.theme_registry.remove_user_themes(&themes);
}
fn load_user_theme(&self, theme_path: PathBuf, fs: Arc<dyn Fs>) -> Task<Result<()>> {
let theme_registry = self.theme_registry.clone();
self.executor
.spawn(async move { theme_registry.load_user_theme(&theme_path, fs).await })
}
fn reload_current_theme(&self, cx: &mut AppContext) {
ThemeSettings::reload_current_theme(cx)
}
}

View File

@@ -149,6 +149,12 @@ pub fn merge_json_value_into(source: serde_json::Value, target: &mut serde_json:
}
}
(Value::Array(source), Value::Array(target)) => {
for value in source {
target.push(value);
}
}
(source, target) => *target = source,
}
}

View File

@@ -15,7 +15,7 @@ use std::sync::Arc;
use ui::{h_flex, ContextMenu, IconButton, Tooltip};
use ui::{prelude::*, right_click_menu};
const RESIZE_HANDLE_SIZE: Pixels = Pixels(6.);
pub(crate) const RESIZE_HANDLE_SIZE: Pixels = Pixels(6.);
pub enum PanelEvent {
ZoomIn,
@@ -574,6 +574,7 @@ impl Dock {
pub fn resize_active_panel(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
if let Some(entry) = self.panel_entries.get_mut(self.active_panel_index) {
let size = size.map(|size| size.max(RESIZE_HANDLE_SIZE).round());
entry.panel.set_size(size, cx);
cx.notify();
}
@@ -593,6 +594,15 @@ impl Dock {
dispatch_context
}
pub fn clamp_panel_size(&mut self, max_size: Pixels, cx: &mut WindowContext) {
let max_size = px((max_size.0 - RESIZE_HANDLE_SIZE.0).abs());
for panel in self.panel_entries.iter().map(|entry| &entry.panel) {
if panel.size(cx) > max_size {
panel.set_size(Some(max_size.max(RESIZE_HANDLE_SIZE)), cx);
}
}
}
}
impl Render for Dock {

View File

@@ -21,7 +21,7 @@ use client::{
};
use collections::{hash_map, HashMap, HashSet};
use derive_more::{Deref, DerefMut};
use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle};
use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle, RESIZE_HANDLE_SIZE};
use futures::{
channel::{
mpsc::{self, UnboundedReceiver, UnboundedSender},
@@ -4824,7 +4824,27 @@ impl Render for Workspace {
let this = cx.view().clone();
canvas(
move |bounds, cx| {
this.update(cx, |this, _cx| this.bounds = bounds)
this.update(cx, |this, cx| {
let bounds_changed = this.bounds != bounds;
this.bounds = bounds;
if bounds_changed {
this.left_dock.update(cx, |dock, cx| {
dock.clamp_panel_size(bounds.size.width, cx)
});
this.right_dock.update(cx, |dock, cx| {
dock.clamp_panel_size(bounds.size.width, cx)
});
this.bottom_dock.update(cx, |dock, cx| {
dock.clamp_panel_size(
bounds.size.height,
cx,
)
});
}
})
},
|_, _, _| {},
)
@@ -4836,42 +4856,27 @@ impl Render for Workspace {
|workspace, e: &DragMoveEvent<DraggedDock>, cx| {
match e.drag(cx).0 {
DockPosition::Left => {
let size = e.event.position.x
- workspace.bounds.left();
workspace.left_dock.update(
resize_left_dock(
e.event.position.x
- workspace.bounds.left(),
workspace,
cx,
|left_dock, cx| {
left_dock.resize_active_panel(
Some(size),
cx,
);
},
);
}
DockPosition::Right => {
let size = workspace.bounds.right()
- e.event.position.x;
workspace.right_dock.update(
resize_right_dock(
workspace.bounds.right()
- e.event.position.x,
workspace,
cx,
|right_dock, cx| {
right_dock.resize_active_panel(
Some(size),
cx,
);
},
);
}
DockPosition::Bottom => {
let size = workspace.bounds.bottom()
- e.event.position.y;
workspace.bottom_dock.update(
resize_bottom_dock(
workspace.bounds.bottom()
- e.event.position.y,
workspace,
cx,
|bottom_dock, cx| {
bottom_dock.resize_active_panel(
Some(size),
cx,
);
},
);
}
}
@@ -4959,6 +4964,40 @@ impl Render for Workspace {
}
}
fn resize_bottom_dock(
new_size: Pixels,
workspace: &mut Workspace,
cx: &mut ViewContext<'_, Workspace>,
) {
let size = new_size.min(workspace.bounds.bottom() - RESIZE_HANDLE_SIZE);
workspace.bottom_dock.update(cx, |bottom_dock, cx| {
bottom_dock.resize_active_panel(Some(size), cx);
});
}
fn resize_right_dock(
new_size: Pixels,
workspace: &mut Workspace,
cx: &mut ViewContext<'_, Workspace>,
) {
let size = new_size.max(workspace.bounds.left() - RESIZE_HANDLE_SIZE);
workspace.right_dock.update(cx, |right_dock, cx| {
right_dock.resize_active_panel(Some(size), cx);
});
}
fn resize_left_dock(
new_size: Pixels,
workspace: &mut Workspace,
cx: &mut ViewContext<'_, Workspace>,
) {
let size = new_size.min(workspace.bounds.right() - RESIZE_HANDLE_SIZE);
workspace.left_dock.update(cx, |left_dock, cx| {
left_dock.resize_active_panel(Some(size), cx);
});
}
impl WorkspaceStore {
pub fn new(client: Arc<Client>, cx: &mut ModelContext<Self>) -> Self {
Self {

View File

@@ -19,7 +19,6 @@ activity_indicator.workspace = true
anyhow.workspace = true
assets.workspace = true
assistant.workspace = true
assistant_slash_command.workspace = true
async-watch.workspace = true
audio.workspace = true
auto_update.workspace = true
@@ -36,12 +35,12 @@ collab_ui.workspace = true
collections.workspace = true
command_palette.workspace = true
command_palette_hooks.workspace = true
context_servers.workspace = true
copilot.workspace = true
db.workspace = true
diagnostics.workspace = true
editor.workspace = true
env_logger.workspace = true
extension.workspace = true
extension_host.workspace = true
extensions_ui.workspace = true
feature_flags.workspace = true
@@ -56,11 +55,11 @@ go_to_line.workspace = true
gpui = { workspace = true, features = ["wayland", "x11", "font-kit"] }
http_client.workspace = true
image_viewer.workspace = true
indexed_docs.workspace = true
inline_completion_button.workspace = true
install_cli.workspace = true
journal.workspace = true
language.workspace = true
language_extension.workspace = true
language_model.workspace = true
language_models.workspace = true
language_selector.workspace = true
@@ -109,6 +108,7 @@ tasks_ui.workspace = true
telemetry_events.workspace = true
terminal_view.workspace = true
theme.workspace = true
theme_extension.workspace = true
theme_selector.workspace = true
time.workspace = true
toolchain_selector.workspace = true

View File

@@ -5,16 +5,15 @@ mod reliability;
mod zed;
use anyhow::{anyhow, Context as _, Result};
use assistant_slash_command::SlashCommandRegistry;
use chrono::Offset;
use clap::{command, Parser};
use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
use client::{parse_zed_link, Client, ProxySettings, UserStore};
use collab_ui::channel_view::ChannelView;
use context_servers::ContextServerFactoryRegistry;
use db::kvp::{GLOBAL_KEY_VALUE_STORE, KEY_VALUE_STORE};
use editor::Editor;
use env_logger::Builder;
use extension::ExtensionHostProxy;
use fs::{Fs, RealFs};
use futures::{future, StreamExt};
use git::GitHostingProviderRegistry;
@@ -23,7 +22,6 @@ use gpui::{
VisualContext,
};
use http_client::{read_proxy_from_env, Uri};
use indexed_docs::IndexedDocsRegistry;
use language::LanguageRegistry;
use log::LevelFilter;
use reqwest_client::ReqwestClient;
@@ -40,7 +38,6 @@ use settings::{
};
use simplelog::ConfigBuilder;
use smol::process::Command;
use snippet_provider::SnippetRegistry;
use std::{
env,
fs::OpenOptions,
@@ -284,6 +281,9 @@ fn main() {
OpenListener::set_global(cx, open_listener.clone());
extension::init(cx);
let extension_host_proxy = ExtensionHostProxy::global(cx);
let client = Client::production(cx);
cx.set_http_client(client.http_client().clone());
let mut languages = LanguageRegistry::new(cx.background_executor().clone());
@@ -317,6 +317,7 @@ fn main() {
let node_runtime = NodeRuntime::new(client.http_client(), rx);
language::init(cx);
language_extension::init(extension_host_proxy.clone(), languages.clone());
languages::init(languages.clone(), node_runtime.clone(), cx);
let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
let workspace_store = cx.new_model(|cx| WorkspaceStore::new(client.clone(), cx));
@@ -326,7 +327,6 @@ fn main() {
zed::init(cx);
project::Project::init(&client, cx);
client::init(&client, cx);
language::init(cx);
let telemetry = client.telemetry();
telemetry.start(
system_id.as_ref().map(|id| id.to_string()),
@@ -376,6 +376,11 @@ fn main() {
SystemAppearance::init(cx);
theme::init(theme::LoadThemes::All(Box::new(Assets)), cx);
theme_extension::init(
extension_host_proxy.clone(),
ThemeRegistry::global(cx),
cx.background_executor().clone(),
);
command_palette::init(cx);
let copilot_language_server_id = app_state.languages.next_language_server_id();
copilot::init(
@@ -407,17 +412,8 @@ fn main() {
app_state.client.telemetry().clone(),
cx,
);
let api = extensions_ui::ConcreteExtensionRegistrationHooks::new(
ThemeRegistry::global(cx),
SlashCommandRegistry::global(cx),
IndexedDocsRegistry::global(cx),
SnippetRegistry::global(cx),
app_state.languages.clone(),
ContextServerFactoryRegistry::global(cx),
cx,
);
extension_host::init(
api,
extension_host_proxy,
app_state.fs.clone(),
app_state.client.clone(),
app_state.node_runtime.clone(),

View File

@@ -5,9 +5,22 @@ Dart support is available through the [Dart extension](https://github.com/zed-ex
- Tree Sitter: [UserNobody14/tree-sitter-dart](https://github.com/UserNobody14/tree-sitter-dart)
- Language Server: [dart language-server](https://github.com/dart-lang/sdk)
## Pre-requisites
You will need to install the Dart SDK.
You can install dart from [dart.dev/get-dart](https://dart.dev/get-dart) or via the [Flutter Version Management CLI (fvm)](https://fvm.app/documentation/getting-started/installation)
## Configuration
The `dart` binary can be configured in a Zed settings file with:
The dart extension requires no configuration if you have `dart` in your path:
```sh
which dart
dart --version
```
If you would like to use a specific dart binary or use dart via FVM you can specify the `dart` binary in your Zed settings.jsons file:
```json
{
@@ -22,7 +35,4 @@ The `dart` binary can be configured in a Zed settings file with:
}
```
<!--
TBD: Document Dart. pubspec.yaml
- https://github.com/dart-lang/sdk/blob/main/pkg/analysis_server/tool/lsp_spec/README.md
-->
Please see the Dart documentation for more information on [dart language-server capabilities](https://github.com/dart-lang/sdk/blob/main/pkg/analysis_server/tool/lsp_spec/README.md).

View File

@@ -125,7 +125,7 @@ Each connection tries to run the development server in proxy mode. This mode wil
In the case that reconnecting fails, the daemon will not be re-used. That said, unsaved changes are by default persisted locally, so that you do not lose work. You can always reconnect to the project at a later date and Zed will restore unsaved changes.
If you are struggling with connection issues, you should be able to see more information in the Zed log `cmd-shift-p Open Log`. If you are seeing things that are unexpected, please file a [GitHub issue](https://github.com/zed-industries/zed/issues/new) or reach out in the #remoting-feedback channel in the [Zed Discord](https://discord.gg/zed-community).
If you are struggling with connection issues, you should be able to see more information in the Zed log `cmd-shift-p Open Log`. If you are seeing things that are unexpected, please file a [GitHub issue](https://github.com/zed-industries/zed/issues/new) or reach out in the #remoting-feedback channel in the [Zed Discord](https://zed.dev/community-links).
## Supported SSH Options
@@ -152,4 +152,4 @@ Note that we deliberately disallow some options (for example `-t` or `-T`) that
## Feedback
Please join the #remoting-feedback channel in the [Zed Discord](https://discord.gg/zed-community).
Please join the #remoting-feedback channel in the [Zed Discord](https://zed.dev/community-links).

View File

@@ -41,7 +41,11 @@ Zed supports ways to spawn (and rerun) commands using its integrated terminal to
// "args": ["--login"]
// }
// }
"shell": "system"
"shell": "system",
// Whether to show the task line in the output of the spawned task, defaults to `true`.
"show_summary": true,
// Whether to show the command line in the output of the spawned task, defaults to `true`.
"show_output": true
}
]
```

View File

@@ -10,4 +10,4 @@ Zed Employees are not currently working on the Windows build.
However, we welcome contributions from the community to improve Windows support.
- [GitHub Issues with 'Windows' label](https://github.com/zed-industries/zed/issues?q=is%3Aissue+is%3Aopen+label%3Awindows)
- [Zed Community Discord](https://discord.gg/zed-community) -> `#windows-port`
- [Zed Community Discord](https://zed.dev/community-links) -> `#windows-port`