Compare commits

...

20 Commits

Author SHA1 Message Date
Max Brunsfeld
f28a4c7cf9 zed 0.121.4 2024-02-05 15:01:50 -08:00
Mikayla Maki
824e7e421a Disable extra frames for ProMotion when screen is not active. (#7410)
This was causing an issue where windows about 1/10 of the way across the
display would hang for a fully second after being deactivated.

Release Notes:

- N/A

Co-authored-by: max <max@zed.dev>
Co-authored-by: nathan <nathan@zed.dev>
Co-authored-by: antonio <antonio@zed.dev>
2024-02-05 15:00:23 -08:00
Antonio Scandurra
5721dec2ec zed 0.121.3 2024-02-05 11:22:47 -07:00
Thorsten Ball
9f90ebd509 Fix cmd+k in terminal and fix sporadic keybind misses (#7388)
This fixes `cmd+k` in the terminal taking 1s to have an effect. It is
now immediate.

It also fixes #7270 by ensuring that we don't set a bad state when
matching keybindings.

It matches keybindings per context and if it finds a match on a lower
context it doesn't keep pending keystrokes. If it finds two matches on
the same context level, requiring more keystrokes, then it waits.

Release Notes:

- Fixed `cmd-k` in terminal taking 1s to have an effect. Also fixed
sporadic non-matching of keybindings if there are overlapping
keybindings.
([#7270](https://github.com/zed-industries/zed/issues/7270)).

---------

Co-authored-by: Conrad <conrad@zed.dev>
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
# Conflicts:
#	crates/gpui/src/window.rs
2024-02-05 11:21:54 -07:00
Antonio Scandurra
17a529c0f5 Scope line layout cache to each window (#7235)
This improves a performance problem we were observing when having
multiple windows updating at the same time, where each window would
invalidate the other window's layout cache.

Release Notes:

- Improved performance when having multiple Zed windows open.

Co-authored-by: Max Brunsfeld <max@zed.dev>
# Conflicts:
#	crates/gpui/src/window.rs
2024-02-05 11:19:21 -07:00
Antonio Scandurra
3a0fb0d322 Mark the window as dirty when first opening it (#7384)
Otherwise we won't display anything if the window never notifies.

Release Notes:

- N/A

Co-authored-by: Nathan <nathan@zed.dev>
2024-02-05 11:15:36 -07:00
Julia
f8bc7fbe0e Use window's screen rather than window itself to start display link
Co-Authored-By: Antonio Scandurra <antonio@zed.dev>
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
2024-02-05 11:15:25 -07:00
Antonio Scandurra
a413729be9 Reintroduce ProMotion support (#7347)
This re-introduces the changes of #7305 but this time we create a
display link using the `NSScreen` associated with the window. We're
hoping we'll get these frame requests more reliably, and this seems
supported by the fact that awakening my laptop restores the frame
requests.

Release Notes:

- See #7305.

Co-authored-by: Nathan <nathan@zed.dev>
# Conflicts:
#	crates/gpui/src/window.rs
2024-02-05 11:15:05 -07:00
Antonio Scandurra
441a21ed0b Remove unnecessary focus_invalidated field (#7320)
I believe at some point this was used for tests but it doesn't seem
necessary anymore.

Release Notes:

- N/A
# Conflicts:
#	crates/gpui/src/app.rs
2024-02-05 11:13:28 -07:00
Antonio Scandurra
234673a1b9 Reduce GPU memory usage (#7319)
This pull request decreases the size of each instance buffer and shares
instance buffers across windows.

Release Notes:

- Improved GPU memory usage.

---------

Co-authored-by: Nathan Sobo <nathan@zed.dev>
# Conflicts:
#	crates/gpui/src/platform/mac/window.rs
2024-02-05 11:12:37 -07:00
Antonio Scandurra
34e846b353 Use Mutex instead of a RefCell to acquire/release instance buffers (#7291)
This fixes a panic happening when releasing an instance buffer.
Releasing the buffer happens on a different thread but the borrow
checker was not catching it because the metal buffer completion handler
API doesn't have a `Send` marker on it.

Release Notes:

- N/A
2024-02-02 11:53:26 -05:00
Thorsten Ball
889a6e2bc3 Use command_buffer.wait_until_scheduled in metal renderer (#7283)
This commit goes back to using `wait_until_scheduled` as opposed to
`wait_until_completed`. What this means, however, is that another draw
could take place before the previous one finished. When that happens we
don't want to reuse the same instance buffer because the GPU is actively
reading from it, so we use a pool instead.

Release Notes:

- Fixed a bug that caused inconsistent frame rate when scrolling on
certain hardware.

---------

Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Antonio <antonio@zed.dev>
2024-02-02 11:53:13 -05:00
Joseph T. Lyons
21b755abae zed 0.121.2 2024-02-02 11:38:33 -05:00
Antonio Scandurra
0bf444d39d Introduce a fast path for drawing quads with no borders / corner radii (#7231)
This will introduce an extra conditional but saves us from doing a bunch
of math in the simple case of drawing simple rectangles that aren't
rounded or don't have borders.


![Figure_1](https://github.com/zed-industries/zed/assets/482957/cba95ce2-2d9a-46ab-a142-35368334eb75)

Release Notes:

- Improved rendering performance.
2024-02-01 18:17:21 -05:00
Thorsten Ball
a745e9b58b zed 0.121.1 2024-02-01 16:53:24 +01:00
Ares Andrew
958fbacc2b Filter LSP github releases that have no assets to properly download LSP servers (#7189)
Fixes https://github.com/zed-industries/zed/issues/7183

Release Notes:

- Filter lsp github releases that have no assets ([7189](https://github.com/zed-industries/zed/issues/7183))
2024-02-01 16:50:14 +01:00
Thorsten Ball
bb6c06e204 assistant: render api key editor if no credentials are set (#7197)
This hopefully reduces confusion for new users. I updated the docs just
this morning, but I figured it's probably better to fix the issue
itself.

So what this does is to render the API key editor whenever the assistant
panel is opened/focused and no credentials can be found.

See: https://github.com/zed-industries/zed/discussions/6943

Release Notes:

- Fixed assistant panel not showing dialog to enter API key when opened
without saved credentials.

---------

Co-authored-by: Piotr <piotr@zed.dev>
2024-02-01 16:44:38 +01:00
Marshall Bowers
2e9f665c77 Watch the themes directory for changes (#7173)
This PR makes Zed watch the themes directory for changes.

When theme files are added or modified, we reload the theme and apply
any changes to Zed.

Release Notes:

- Added live reloading for the themes directory.
2024-01-31 18:30:23 -05:00
Conrad Irwin
3fdccaacbc disallow opening private files (#7165)
- Disallow sharing gitignored files through collab
- Show errors when failing to open files
- Show a warning to followers when view is unshared

/cc @mikaylamaki, let's update this to use your `private_files` config
before merge.


Release Notes:

- Added the ability to prevent sharing private files over collab.

---------

Co-authored-by: Piotr <piotr@zed.dev>
Co-authored-by: Mikayla <mikayla@zed.dev>
2024-01-31 12:08:09 -08:00
Joseph T. Lyons
7dd7ecd07f v0.121.x preview 2024-01-31 14:44:02 -05:00
32 changed files with 651 additions and 384 deletions

2
Cargo.lock generated
View File

@@ -10261,7 +10261,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.121.0"
version = "0.121.4"
dependencies = [
"activity_indicator",
"ai",

View File

@@ -415,7 +415,15 @@
"cmd-?": "assistant::ToggleFocus",
"cmd-alt-s": "workspace::SaveAll",
"cmd-k m": "language_selector::Toggle",
"escape": "workspace::Unfollow"
"escape": "workspace::Unfollow",
"cmd-k cmd-left": ["workspace::ActivatePaneInDirection", "Left"],
"cmd-k cmd-right": ["workspace::ActivatePaneInDirection", "Right"],
"cmd-k cmd-up": ["workspace::ActivatePaneInDirection", "Up"],
"cmd-k cmd-down": ["workspace::ActivatePaneInDirection", "Down"],
"cmd-k shift-left": ["workspace::SwapPaneInDirection", "Left"],
"cmd-k shift-right": ["workspace::SwapPaneInDirection", "Right"],
"cmd-k shift-up": ["workspace::SwapPaneInDirection", "Up"],
"cmd-k shift-down": ["workspace::SwapPaneInDirection", "Down"]
}
},
// Bindings from Sublime Text
@@ -441,18 +449,6 @@
"ctrl-alt-shift-f": "editor::SelectToNextSubwordEnd"
}
},
{
"bindings": {
"cmd-k cmd-left": ["workspace::ActivatePaneInDirection", "Left"],
"cmd-k cmd-right": ["workspace::ActivatePaneInDirection", "Right"],
"cmd-k cmd-up": ["workspace::ActivatePaneInDirection", "Up"],
"cmd-k cmd-down": ["workspace::ActivatePaneInDirection", "Down"],
"cmd-k shift-left": ["workspace::SwapPaneInDirection", "Left"],
"cmd-k shift-right": ["workspace::SwapPaneInDirection", "Right"],
"cmd-k shift-up": ["workspace::SwapPaneInDirection", "Up"],
"cmd-k shift-down": ["workspace::SwapPaneInDirection", "Down"]
}
},
// Bindings from Atom
{
"context": "Pane",

View File

@@ -199,9 +199,13 @@ impl AssistantPanel {
.update(cx, |toolbar, cx| toolbar.focus_changed(true, cx));
cx.notify();
if self.focus_handle.is_focused(cx) {
if let Some(editor) = self.active_editor() {
cx.focus_view(editor);
} else if let Some(api_key_editor) = self.api_key_editor.as_ref() {
if self.has_credentials() {
if let Some(editor) = self.active_editor() {
cx.focus_view(editor);
}
}
if let Some(api_key_editor) = self.api_key_editor.as_ref() {
cx.focus_view(api_key_editor);
}
}
@@ -777,6 +781,10 @@ impl AssistantPanel {
});
}
fn build_api_key_editor(&mut self, cx: &mut WindowContext<'_>) {
self.api_key_editor = Some(build_api_key_editor(cx));
}
fn new_conversation(&mut self, cx: &mut ViewContext<Self>) -> View<ConversationEditor> {
let editor = cx.new_view(|cx| {
ConversationEditor::new(
@@ -870,7 +878,7 @@ impl AssistantPanel {
cx.update(|cx| completion_provider.delete_credentials(cx))?
.await;
this.update(&mut cx, |this, cx| {
this.api_key_editor = Some(build_api_key_editor(cx));
this.build_api_key_editor(cx);
this.focus_handle.focus(cx);
cx.notify();
})
@@ -1136,7 +1144,7 @@ impl AssistantPanel {
}
}
fn build_api_key_editor(cx: &mut ViewContext<AssistantPanel>) -> View<Editor> {
fn build_api_key_editor(cx: &mut WindowContext) -> View<Editor> {
cx.new_view(|cx| {
let mut editor = Editor::single_line(cx);
editor.set_placeholder_text("sk-000000000000000000000000000000000000000000000000", cx);
@@ -1147,9 +1155,10 @@ fn build_api_key_editor(cx: &mut ViewContext<AssistantPanel>) -> View<Editor> {
impl Render for AssistantPanel {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
if let Some(api_key_editor) = self.api_key_editor.clone() {
const INSTRUCTIONS: [&'static str; 5] = [
const INSTRUCTIONS: [&'static str; 6] = [
"To use the assistant panel or inline assistant, you need to add your OpenAI API key.",
" - You can create an API key at: platform.openai.com/api-keys",
" - Make sure your OpenAI account has credits",
" - Having a subscription for another service like GitHub Copilot won't work.",
" ",
"Paste your OpenAI API key and press Enter to use the assistant:"
@@ -1342,7 +1351,9 @@ impl Panel for AssistantPanel {
cx.spawn(|this, mut cx| async move {
load_credentials.await;
this.update(&mut cx, |this, cx| {
if this.editors.is_empty() {
if !this.has_credentials() {
this.build_api_key_editor(cx);
} else if this.editors.is_empty() {
this.new_conversation(cx);
}
})

View File

@@ -28,6 +28,9 @@ async fn main() -> Result<()> {
Some("version") => {
println!("collab v{VERSION}");
}
Some("migrate") => {
run_migrations().await?;
}
Some("serve") => {
let config = envy::from_env::<Config>().expect("error loading config");
init_tracing(&config);

View File

@@ -5967,6 +5967,6 @@ async fn test_cmd_k_left(cx: &mut TestAppContext) {
cx.executor().advance_clock(Duration::from_secs(2));
cx.simulate_keystrokes("left");
workspace.update(cx, |workspace, cx| {
assert!(workspace.items(cx).collect::<Vec<_>>().len() == 3);
assert!(workspace.items(cx).collect::<Vec<_>>().len() == 2);
});
}

View File

@@ -185,6 +185,14 @@ impl FollowableItem for Editor {
fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant> {
let buffer = self.buffer.read(cx);
if buffer
.as_singleton()
.and_then(|buffer| buffer.read(cx).file())
.map_or(false, |file| file.is_private())
{
return None;
}
let scroll_anchor = self.scroll_manager.anchor();
let excerpts = buffer
.read(cx)

View File

@@ -3,7 +3,7 @@
use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint};
use crate::{char_kind, CharKind, EditorStyle, ToOffset, ToPoint};
use gpui::{px, Pixels, TextSystem};
use gpui::{px, Pixels, WindowTextSystem};
use language::Point;
use std::{ops::Range, sync::Arc};
@@ -22,7 +22,7 @@ pub enum FindRange {
/// TextLayoutDetails encompasses everything we need to move vertically
/// taking into account variable width characters.
pub struct TextLayoutDetails {
pub(crate) text_system: Arc<TextSystem>,
pub(crate) text_system: Arc<WindowTextSystem>,
pub(crate) editor_style: EditorStyle,
pub(crate) rem_size: Pixels,
pub anchor: Anchor,

View File

@@ -652,27 +652,20 @@ impl AppContext {
}
}
} else {
for window in self.windows.values() {
if let Some(window) = window.as_ref() {
if window.dirty {
window.platform_window.invalidate();
}
}
}
#[cfg(any(test, feature = "test-support"))]
for window in self
.windows
.values()
.filter_map(|window| {
let window = window.as_ref()?;
(window.dirty || window.focus_invalidated).then_some(window.handle)
window.dirty.get().then_some(window.handle)
})
.collect::<Vec<_>>()
{
self.update_window(window, |_, cx| cx.draw()).unwrap();
}
#[allow(clippy::collapsible_else_if)]
if self.pending_effects.is_empty() {
break;
}
@@ -749,7 +742,7 @@ impl AppContext {
fn apply_refresh_effect(&mut self) {
for window in self.windows.values_mut() {
if let Some(window) = window.as_mut() {
window.dirty = true;
window.dirty.set(true);
}
}
}

View File

@@ -62,16 +62,6 @@ use std::{
rc::Rc,
};
/// KeymatchMode controls how keybindings are resolved in the case of conflicting pending keystrokes.
/// When `Sequenced`, gpui will wait for 1s for sequences to complete.
/// When `Immediate`, gpui will immediately resolve the keybinding.
#[derive(Default, PartialEq)]
pub enum KeymatchMode {
#[default]
Sequenced,
Immediate,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub(crate) struct DispatchNodeId(usize);
@@ -84,7 +74,6 @@ pub(crate) struct DispatchTree {
keystroke_matchers: FxHashMap<SmallVec<[KeyContext; 4]>, KeystrokeMatcher>,
keymap: Rc<RefCell<Keymap>>,
action_registry: Rc<ActionRegistry>,
pub(crate) keymatch_mode: KeymatchMode,
}
#[derive(Default)]
@@ -116,7 +105,6 @@ impl DispatchTree {
keystroke_matchers: FxHashMap::default(),
keymap,
action_registry,
keymatch_mode: KeymatchMode::Sequenced,
}
}
@@ -127,7 +115,6 @@ impl DispatchTree {
self.focusable_node_ids.clear();
self.view_node_ids.clear();
self.keystroke_matchers.clear();
self.keymatch_mode = KeymatchMode::Sequenced;
}
pub fn push_node(
@@ -335,7 +322,7 @@ impl DispatchTree {
.collect()
}
// dispatch_key pushses the next keystroke into any key binding matchers.
// dispatch_key pushes the next keystroke into any key binding matchers.
// any matching bindings are returned in the order that they should be dispatched:
// * First by length of binding (so if you have a binding for "b" and "ab", the "ab" binding fires first)
// * Secondly by depth in the tree (so if Editor has a binding for "b" and workspace a
@@ -364,6 +351,11 @@ impl DispatchTree {
.or_insert_with(|| KeystrokeMatcher::new(self.keymap.clone()));
let result = keystroke_matcher.match_keystroke(keystroke, &context_stack);
if result.pending && !pending && !bindings.is_empty() {
context_stack.pop();
continue;
}
pending = result.pending || pending;
for new_binding in result.bindings {
match bindings

View File

@@ -175,7 +175,6 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
fn on_close(&self, callback: Box<dyn FnOnce()>);
fn on_appearance_changed(&self, callback: Box<dyn FnMut()>);
fn is_topmost_for_position(&self, position: Point<Pixels>) -> bool;
fn invalidate(&self);
fn draw(&self, scene: &Scene);
fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;

View File

@@ -3,6 +3,7 @@ use crate::{
Hsla, MetalAtlas, MonochromeSprite, Path, PathId, PathVertex, PolychromeSprite, PrimitiveBatch,
Quad, ScaledPixels, Scene, Shadow, Size, Surface, Underline,
};
use block::ConcreteBlock;
use cocoa::{
base::{NO, YES},
foundation::NSUInteger,
@@ -14,17 +15,19 @@ use foreign_types::ForeignType;
use media::core_video::CVMetalTextureCache;
use metal::{CommandQueue, MTLPixelFormat, MTLResourceOptions, NSRange};
use objc::{self, msg_send, sel, sel_impl};
use parking_lot::Mutex;
use smallvec::SmallVec;
use std::{ffi::c_void, mem, ptr, sync::Arc};
use std::{cell::Cell, ffi::c_void, mem, ptr, sync::Arc};
#[cfg(not(feature = "runtime_shaders"))]
const SHADERS_METALLIB: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/shaders.metallib"));
#[cfg(feature = "runtime_shaders")]
const SHADERS_SOURCE_FILE: &'static str =
include_str!(concat!(env!("OUT_DIR"), "/stitched_shaders.metal"));
const INSTANCE_BUFFER_SIZE: usize = 32 * 1024 * 1024; // This is an arbitrary decision. There's probably a more optimal value (maybe even we could adjust dynamically...)
const INSTANCE_BUFFER_SIZE: usize = 2 * 1024 * 1024; // This is an arbitrary decision. There's probably a more optimal value (maybe even we could adjust dynamically...)
pub(crate) struct MetalRenderer {
device: metal::Device,
layer: metal::MetalLayer,
command_queue: CommandQueue,
paths_rasterization_pipeline_state: metal::RenderPipelineState,
@@ -36,13 +39,14 @@ pub(crate) struct MetalRenderer {
polychrome_sprites_pipeline_state: metal::RenderPipelineState,
surfaces_pipeline_state: metal::RenderPipelineState,
unit_vertices: metal::Buffer,
instances: metal::Buffer,
#[allow(clippy::arc_with_non_send_sync)]
instance_buffer_pool: Arc<Mutex<Vec<metal::Buffer>>>,
sprite_atlas: Arc<MetalAtlas>,
core_video_texture_cache: CVMetalTextureCache,
}
impl MetalRenderer {
pub fn new(is_opaque: bool) -> Self {
pub fn new(instance_buffer_pool: Arc<Mutex<Vec<metal::Buffer>>>) -> Self {
let device: metal::Device = if let Some(device) = metal::Device::system_default() {
device
} else {
@@ -54,7 +58,7 @@ impl MetalRenderer {
layer.set_device(&device);
layer.set_pixel_format(MTLPixelFormat::BGRA8Unorm);
layer.set_presents_with_transaction(true);
layer.set_opaque(is_opaque);
layer.set_opaque(true);
unsafe {
let _: () = msg_send![&*layer, setAllowsNextDrawableTimeout: NO];
let _: () = msg_send![&*layer, setNeedsDisplayOnBoundsChange: YES];
@@ -93,10 +97,6 @@ impl MetalRenderer {
mem::size_of_val(&unit_vertices) as u64,
MTLResourceOptions::StorageModeManaged,
);
let instances = device.new_buffer(
INSTANCE_BUFFER_SIZE as u64,
MTLResourceOptions::StorageModeManaged,
);
let paths_rasterization_pipeline_state = build_path_rasterization_pipeline_state(
&device,
@@ -165,8 +165,11 @@ impl MetalRenderer {
let command_queue = device.new_command_queue();
let sprite_atlas = Arc::new(MetalAtlas::new(device.clone()));
let core_video_texture_cache =
unsafe { CVMetalTextureCache::new(device.as_ptr()).unwrap() };
Self {
device,
layer,
command_queue,
paths_rasterization_pipeline_state,
@@ -178,9 +181,9 @@ impl MetalRenderer {
polychrome_sprites_pipeline_state,
surfaces_pipeline_state,
unit_vertices,
instances,
instance_buffer_pool,
sprite_atlas,
core_video_texture_cache: unsafe { CVMetalTextureCache::new(device.as_ptr()).unwrap() },
core_video_texture_cache,
}
}
@@ -208,14 +211,24 @@ impl MetalRenderer {
);
return;
};
let mut instance_buffer = self.instance_buffer_pool.lock().pop().unwrap_or_else(|| {
self.device.new_buffer(
INSTANCE_BUFFER_SIZE as u64,
MTLResourceOptions::StorageModeManaged,
)
});
let command_queue = self.command_queue.clone();
let command_buffer = command_queue.new_command_buffer();
let mut instance_offset = 0;
let Some(path_tiles) =
self.rasterize_paths(scene.paths(), &mut instance_offset, command_buffer)
else {
panic!("failed to rasterize {} paths", scene.paths().len());
let Some(path_tiles) = self.rasterize_paths(
scene.paths(),
&mut instance_buffer,
&mut instance_offset,
command_buffer,
) else {
log::error!("failed to rasterize {} paths", scene.paths().len());
return;
};
let render_pass_descriptor = metal::RenderPassDescriptor::new();
@@ -243,22 +256,29 @@ impl MetalRenderer {
let ok = match batch {
PrimitiveBatch::Shadows(shadows) => self.draw_shadows(
shadows,
&mut instance_buffer,
&mut instance_offset,
viewport_size,
command_encoder,
),
PrimitiveBatch::Quads(quads) => self.draw_quads(
quads,
&mut instance_buffer,
&mut instance_offset,
viewport_size,
command_encoder,
),
PrimitiveBatch::Quads(quads) => {
self.draw_quads(quads, &mut instance_offset, viewport_size, command_encoder)
}
PrimitiveBatch::Paths(paths) => self.draw_paths(
paths,
&path_tiles,
&mut instance_buffer,
&mut instance_offset,
viewport_size,
command_encoder,
),
PrimitiveBatch::Underlines(underlines) => self.draw_underlines(
underlines,
&mut instance_buffer,
&mut instance_offset,
viewport_size,
command_encoder,
@@ -269,6 +289,7 @@ impl MetalRenderer {
} => self.draw_monochrome_sprites(
texture_id,
sprites,
&mut instance_buffer,
&mut instance_offset,
viewport_size,
command_encoder,
@@ -279,12 +300,14 @@ impl MetalRenderer {
} => self.draw_polychrome_sprites(
texture_id,
sprites,
&mut instance_buffer,
&mut instance_offset,
viewport_size,
command_encoder,
),
PrimitiveBatch::Surfaces(surfaces) => self.draw_surfaces(
surfaces,
&mut instance_buffer,
&mut instance_offset,
viewport_size,
command_encoder,
@@ -292,7 +315,7 @@ impl MetalRenderer {
};
if !ok {
panic!("scene too large: {} paths, {} shadows, {} quads, {} underlines, {} mono, {} poly, {} surfaces",
log::error!("scene too large: {} paths, {} shadows, {} quads, {} underlines, {} mono, {} poly, {} surfaces",
scene.paths.len(),
scene.shadows.len(),
scene.quads.len(),
@@ -300,28 +323,39 @@ impl MetalRenderer {
scene.monochrome_sprites.len(),
scene.polychrome_sprites.len(),
scene.surfaces.len(),
)
);
break;
}
}
command_encoder.end_encoding();
self.instances.did_modify_range(NSRange {
instance_buffer.did_modify_range(NSRange {
location: 0,
length: instance_offset as NSUInteger,
});
let instance_buffer_pool = self.instance_buffer_pool.clone();
let instance_buffer = Cell::new(Some(instance_buffer));
let block = ConcreteBlock::new(move |_| {
if let Some(instance_buffer) = instance_buffer.take() {
instance_buffer_pool.lock().push(instance_buffer);
}
});
let block = block.copy();
command_buffer.add_completed_handler(&block);
command_buffer.commit();
self.sprite_atlas.clear_textures(AtlasTextureKind::Path);
command_buffer.wait_until_completed();
command_buffer.wait_until_scheduled();
drawable.present();
}
fn rasterize_paths(
&mut self,
paths: &[Path<ScaledPixels>],
offset: &mut usize,
instance_buffer: &mut metal::Buffer,
instance_offset: &mut usize,
command_buffer: &metal::CommandBufferRef,
) -> Option<HashMap<PathId, AtlasTile>> {
let mut tiles = HashMap::default();
@@ -347,9 +381,9 @@ impl MetalRenderer {
}
for (texture_id, vertices) in vertices_by_texture_id {
align_offset(offset);
align_offset(instance_offset);
let vertices_bytes_len = mem::size_of_val(vertices.as_slice());
let next_offset = *offset + vertices_bytes_len;
let next_offset = *instance_offset + vertices_bytes_len;
if next_offset > INSTANCE_BUFFER_SIZE {
return None;
}
@@ -369,8 +403,8 @@ impl MetalRenderer {
command_encoder.set_render_pipeline_state(&self.paths_rasterization_pipeline_state);
command_encoder.set_vertex_buffer(
PathRasterizationInputIndex::Vertices as u64,
Some(&self.instances),
*offset as u64,
Some(instance_buffer),
*instance_offset as u64,
);
let texture_size = Size {
width: DevicePixels::from(texture.width()),
@@ -382,7 +416,8 @@ impl MetalRenderer {
&texture_size as *const Size<DevicePixels> as *const _,
);
let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) };
let buffer_contents =
unsafe { (instance_buffer.contents() as *mut u8).add(*instance_offset) };
unsafe {
ptr::copy_nonoverlapping(
vertices.as_ptr() as *const u8,
@@ -397,7 +432,7 @@ impl MetalRenderer {
vertices.len() as u64,
);
command_encoder.end_encoding();
*offset = next_offset;
*instance_offset = next_offset;
}
Some(tiles)
@@ -406,14 +441,15 @@ impl MetalRenderer {
fn draw_shadows(
&mut self,
shadows: &[Shadow],
offset: &mut usize,
instance_buffer: &mut metal::Buffer,
instance_offset: &mut usize,
viewport_size: Size<DevicePixels>,
command_encoder: &metal::RenderCommandEncoderRef,
) -> bool {
if shadows.is_empty() {
return true;
}
align_offset(offset);
align_offset(instance_offset);
command_encoder.set_render_pipeline_state(&self.shadows_pipeline_state);
command_encoder.set_vertex_buffer(
@@ -423,13 +459,13 @@ impl MetalRenderer {
);
command_encoder.set_vertex_buffer(
ShadowInputIndex::Shadows as u64,
Some(&self.instances),
*offset as u64,
Some(instance_buffer),
*instance_offset as u64,
);
command_encoder.set_fragment_buffer(
ShadowInputIndex::Shadows as u64,
Some(&self.instances),
*offset as u64,
Some(instance_buffer),
*instance_offset as u64,
);
command_encoder.set_vertex_bytes(
@@ -439,9 +475,10 @@ impl MetalRenderer {
);
let shadow_bytes_len = mem::size_of_val(shadows);
let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) };
let buffer_contents =
unsafe { (instance_buffer.contents() as *mut u8).add(*instance_offset) };
let next_offset = *offset + shadow_bytes_len;
let next_offset = *instance_offset + shadow_bytes_len;
if next_offset > INSTANCE_BUFFER_SIZE {
return false;
}
@@ -460,21 +497,22 @@ impl MetalRenderer {
6,
shadows.len() as u64,
);
*offset = next_offset;
*instance_offset = next_offset;
true
}
fn draw_quads(
&mut self,
quads: &[Quad],
offset: &mut usize,
instance_buffer: &mut metal::Buffer,
instance_offset: &mut usize,
viewport_size: Size<DevicePixels>,
command_encoder: &metal::RenderCommandEncoderRef,
) -> bool {
if quads.is_empty() {
return true;
}
align_offset(offset);
align_offset(instance_offset);
command_encoder.set_render_pipeline_state(&self.quads_pipeline_state);
command_encoder.set_vertex_buffer(
@@ -484,13 +522,13 @@ impl MetalRenderer {
);
command_encoder.set_vertex_buffer(
QuadInputIndex::Quads as u64,
Some(&self.instances),
*offset as u64,
Some(instance_buffer),
*instance_offset as u64,
);
command_encoder.set_fragment_buffer(
QuadInputIndex::Quads as u64,
Some(&self.instances),
*offset as u64,
Some(instance_buffer),
*instance_offset as u64,
);
command_encoder.set_vertex_bytes(
@@ -500,9 +538,10 @@ impl MetalRenderer {
);
let quad_bytes_len = mem::size_of_val(quads);
let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) };
let buffer_contents =
unsafe { (instance_buffer.contents() as *mut u8).add(*instance_offset) };
let next_offset = *offset + quad_bytes_len;
let next_offset = *instance_offset + quad_bytes_len;
if next_offset > INSTANCE_BUFFER_SIZE {
return false;
}
@@ -517,7 +556,7 @@ impl MetalRenderer {
6,
quads.len() as u64,
);
*offset = next_offset;
*instance_offset = next_offset;
true
}
@@ -525,7 +564,8 @@ impl MetalRenderer {
&mut self,
paths: &[Path<ScaledPixels>],
tiles_by_path_id: &HashMap<PathId, AtlasTile>,
offset: &mut usize,
instance_buffer: &mut metal::Buffer,
instance_offset: &mut usize,
viewport_size: Size<DevicePixels>,
command_encoder: &metal::RenderCommandEncoderRef,
) -> bool {
@@ -573,7 +613,7 @@ impl MetalRenderer {
if sprites.is_empty() {
break;
} else {
align_offset(offset);
align_offset(instance_offset);
let texture_id = prev_texture_id.take().unwrap();
let texture: metal::Texture = self.sprite_atlas.metal_texture(texture_id);
let texture_size = size(
@@ -583,8 +623,8 @@ impl MetalRenderer {
command_encoder.set_vertex_buffer(
SpriteInputIndex::Sprites as u64,
Some(&self.instances),
*offset as u64,
Some(instance_buffer),
*instance_offset as u64,
);
command_encoder.set_vertex_bytes(
SpriteInputIndex::AtlasTextureSize as u64,
@@ -593,20 +633,20 @@ impl MetalRenderer {
);
command_encoder.set_fragment_buffer(
SpriteInputIndex::Sprites as u64,
Some(&self.instances),
*offset as u64,
Some(instance_buffer),
*instance_offset as u64,
);
command_encoder
.set_fragment_texture(SpriteInputIndex::AtlasTexture as u64, Some(&texture));
let sprite_bytes_len = mem::size_of_val(sprites.as_slice());
let next_offset = *offset + sprite_bytes_len;
let next_offset = *instance_offset + sprite_bytes_len;
if next_offset > INSTANCE_BUFFER_SIZE {
return false;
}
let buffer_contents =
unsafe { (self.instances.contents() as *mut u8).add(*offset) };
unsafe { (instance_buffer.contents() as *mut u8).add(*instance_offset) };
unsafe {
ptr::copy_nonoverlapping(
@@ -622,7 +662,7 @@ impl MetalRenderer {
6,
sprites.len() as u64,
);
*offset = next_offset;
*instance_offset = next_offset;
sprites.clear();
}
}
@@ -632,14 +672,15 @@ impl MetalRenderer {
fn draw_underlines(
&mut self,
underlines: &[Underline],
offset: &mut usize,
instance_buffer: &mut metal::Buffer,
instance_offset: &mut usize,
viewport_size: Size<DevicePixels>,
command_encoder: &metal::RenderCommandEncoderRef,
) -> bool {
if underlines.is_empty() {
return true;
}
align_offset(offset);
align_offset(instance_offset);
command_encoder.set_render_pipeline_state(&self.underlines_pipeline_state);
command_encoder.set_vertex_buffer(
@@ -649,13 +690,13 @@ impl MetalRenderer {
);
command_encoder.set_vertex_buffer(
UnderlineInputIndex::Underlines as u64,
Some(&self.instances),
*offset as u64,
Some(instance_buffer),
*instance_offset as u64,
);
command_encoder.set_fragment_buffer(
UnderlineInputIndex::Underlines as u64,
Some(&self.instances),
*offset as u64,
Some(instance_buffer),
*instance_offset as u64,
);
command_encoder.set_vertex_bytes(
@@ -665,9 +706,10 @@ impl MetalRenderer {
);
let underline_bytes_len = mem::size_of_val(underlines);
let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) };
let buffer_contents =
unsafe { (instance_buffer.contents() as *mut u8).add(*instance_offset) };
let next_offset = *offset + underline_bytes_len;
let next_offset = *instance_offset + underline_bytes_len;
if next_offset > INSTANCE_BUFFER_SIZE {
return false;
}
@@ -686,7 +728,7 @@ impl MetalRenderer {
6,
underlines.len() as u64,
);
*offset = next_offset;
*instance_offset = next_offset;
true
}
@@ -694,14 +736,15 @@ impl MetalRenderer {
&mut self,
texture_id: AtlasTextureId,
sprites: &[MonochromeSprite],
offset: &mut usize,
instance_buffer: &mut metal::Buffer,
instance_offset: &mut usize,
viewport_size: Size<DevicePixels>,
command_encoder: &metal::RenderCommandEncoderRef,
) -> bool {
if sprites.is_empty() {
return true;
}
align_offset(offset);
align_offset(instance_offset);
let texture = self.sprite_atlas.metal_texture(texture_id);
let texture_size = size(
@@ -716,8 +759,8 @@ impl MetalRenderer {
);
command_encoder.set_vertex_buffer(
SpriteInputIndex::Sprites as u64,
Some(&self.instances),
*offset as u64,
Some(instance_buffer),
*instance_offset as u64,
);
command_encoder.set_vertex_bytes(
SpriteInputIndex::ViewportSize as u64,
@@ -731,15 +774,16 @@ impl MetalRenderer {
);
command_encoder.set_fragment_buffer(
SpriteInputIndex::Sprites as u64,
Some(&self.instances),
*offset as u64,
Some(instance_buffer),
*instance_offset as u64,
);
command_encoder.set_fragment_texture(SpriteInputIndex::AtlasTexture as u64, Some(&texture));
let sprite_bytes_len = mem::size_of_val(sprites);
let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) };
let buffer_contents =
unsafe { (instance_buffer.contents() as *mut u8).add(*instance_offset) };
let next_offset = *offset + sprite_bytes_len;
let next_offset = *instance_offset + sprite_bytes_len;
if next_offset > INSTANCE_BUFFER_SIZE {
return false;
}
@@ -758,7 +802,7 @@ impl MetalRenderer {
6,
sprites.len() as u64,
);
*offset = next_offset;
*instance_offset = next_offset;
true
}
@@ -766,14 +810,15 @@ impl MetalRenderer {
&mut self,
texture_id: AtlasTextureId,
sprites: &[PolychromeSprite],
offset: &mut usize,
instance_buffer: &mut metal::Buffer,
instance_offset: &mut usize,
viewport_size: Size<DevicePixels>,
command_encoder: &metal::RenderCommandEncoderRef,
) -> bool {
if sprites.is_empty() {
return true;
}
align_offset(offset);
align_offset(instance_offset);
let texture = self.sprite_atlas.metal_texture(texture_id);
let texture_size = size(
@@ -788,8 +833,8 @@ impl MetalRenderer {
);
command_encoder.set_vertex_buffer(
SpriteInputIndex::Sprites as u64,
Some(&self.instances),
*offset as u64,
Some(instance_buffer),
*instance_offset as u64,
);
command_encoder.set_vertex_bytes(
SpriteInputIndex::ViewportSize as u64,
@@ -803,15 +848,16 @@ impl MetalRenderer {
);
command_encoder.set_fragment_buffer(
SpriteInputIndex::Sprites as u64,
Some(&self.instances),
*offset as u64,
Some(instance_buffer),
*instance_offset as u64,
);
command_encoder.set_fragment_texture(SpriteInputIndex::AtlasTexture as u64, Some(&texture));
let sprite_bytes_len = mem::size_of_val(sprites);
let buffer_contents = unsafe { (self.instances.contents() as *mut u8).add(*offset) };
let buffer_contents =
unsafe { (instance_buffer.contents() as *mut u8).add(*instance_offset) };
let next_offset = *offset + sprite_bytes_len;
let next_offset = *instance_offset + sprite_bytes_len;
if next_offset > INSTANCE_BUFFER_SIZE {
return false;
}
@@ -830,14 +876,15 @@ impl MetalRenderer {
6,
sprites.len() as u64,
);
*offset = next_offset;
*instance_offset = next_offset;
true
}
fn draw_surfaces(
&mut self,
surfaces: &[Surface],
offset: &mut usize,
instance_buffer: &mut metal::Buffer,
instance_offset: &mut usize,
viewport_size: Size<DevicePixels>,
command_encoder: &metal::RenderCommandEncoderRef,
) -> bool {
@@ -889,16 +936,16 @@ impl MetalRenderer {
.unwrap()
};
align_offset(offset);
let next_offset = *offset + mem::size_of::<Surface>();
align_offset(instance_offset);
let next_offset = *instance_offset + mem::size_of::<Surface>();
if next_offset > INSTANCE_BUFFER_SIZE {
return false;
}
command_encoder.set_vertex_buffer(
SurfaceInputIndex::Surfaces as u64,
Some(&self.instances),
*offset as u64,
Some(instance_buffer),
*instance_offset as u64,
);
command_encoder.set_vertex_bytes(
SurfaceInputIndex::TextureSize as u64,
@@ -915,8 +962,8 @@ impl MetalRenderer {
);
unsafe {
let buffer_contents =
(self.instances.contents() as *mut u8).add(*offset) as *mut SurfaceBounds;
let buffer_contents = (instance_buffer.contents() as *mut u8).add(*instance_offset)
as *mut SurfaceBounds;
ptr::write(
buffer_contents,
SurfaceBounds {
@@ -927,7 +974,7 @@ impl MetalRenderer {
}
command_encoder.draw_primitives(metal::MTLPrimitiveType::Triangle, 0, 6);
*offset = next_offset;
*instance_offset = next_offset;
}
true
}

View File

@@ -146,6 +146,7 @@ pub(crate) struct MacPlatformState {
foreground_executor: ForegroundExecutor,
text_system: Arc<MacTextSystem>,
display_linker: MacDisplayLinker,
instance_buffer_pool: Arc<Mutex<Vec<metal::Buffer>>>,
pasteboard: id,
text_hash_pasteboard_type: id,
metadata_pasteboard_type: id,
@@ -176,6 +177,7 @@ impl MacPlatform {
foreground_executor: ForegroundExecutor::new(dispatcher),
text_system: Arc::new(MacTextSystem::new()),
display_linker: MacDisplayLinker::new(),
instance_buffer_pool: Arc::default(),
pasteboard: unsafe { NSPasteboard::generalPasteboard(nil) },
text_hash_pasteboard_type: unsafe { ns_string("zed-text-hash") },
metadata_pasteboard_type: unsafe { ns_string("zed-metadata") },
@@ -494,7 +496,13 @@ impl Platform for MacPlatform {
handle: AnyWindowHandle,
options: WindowOptions,
) -> Box<dyn PlatformWindow> {
Box::new(MacWindow::open(handle, options, self.foreground_executor()))
let instance_buffer_pool = self.0.lock().instance_buffer_pool.clone();
Box::new(MacWindow::open(
handle,
options,
self.foreground_executor(),
instance_buffer_pool,
))
}
fn set_display_link_output_callback(

View File

@@ -61,6 +61,16 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]],
constant Quad *quads
[[buffer(QuadInputIndex_Quads)]]) {
Quad quad = quads[input.quad_id];
// Fast path when the quad is not rounded and doesn't have any border.
if (quad.corner_radii.top_left == 0. && quad.corner_radii.bottom_left == 0. &&
quad.corner_radii.top_right == 0. &&
quad.corner_radii.bottom_right == 0. && quad.border_widths.top == 0. &&
quad.border_widths.left == 0. && quad.border_widths.right == 0. &&
quad.border_widths.bottom == 0.) {
return input.background_color;
}
float2 half_size =
float2(quad.bounds.size.width, quad.bounds.size.height) / 2.;
float2 center =

View File

@@ -16,8 +16,8 @@ use cocoa::{
},
base::{id, nil},
foundation::{
NSArray, NSAutoreleasePool, NSDictionary, NSFastEnumeration, NSInteger, NSPoint, NSRect,
NSSize, NSString, NSUInteger,
NSArray, NSAutoreleasePool, NSDefaultRunLoopMode, NSDictionary, NSFastEnumeration,
NSInteger, NSPoint, NSRect, NSSize, NSString, NSUInteger,
},
};
use core_graphics::display::CGRect;
@@ -168,6 +168,7 @@ unsafe fn build_classes() {
sel!(displayLayer:),
display_layer as extern "C" fn(&Object, Sel, id),
);
decl.add_method(sel!(step:), step as extern "C" fn(&Object, Sel, id));
decl.add_protocol(Protocol::get("NSTextInputClient").unwrap());
decl.add_method(
@@ -260,6 +261,10 @@ unsafe fn build_window_class(name: &'static str, superclass: &Class) -> *const C
sel!(windowDidMove:),
window_did_move as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(windowDidChangeScreen:),
window_did_change_screen as extern "C" fn(&Object, Sel, id),
);
decl.add_method(
sel!(windowDidBecomeKey:),
window_did_change_key_status as extern "C" fn(&Object, Sel, id),
@@ -320,7 +325,8 @@ struct MacWindowState {
handle: AnyWindowHandle,
executor: ForegroundExecutor,
native_window: id,
native_view: NonNull<id>,
native_view: NonNull<Object>,
display_link: id,
renderer: MetalRenderer,
kind: WindowKind,
request_frame_callback: Option<Box<dyn FnMut()>>,
@@ -458,6 +464,7 @@ impl MacWindow {
handle: AnyWindowHandle,
options: WindowOptions,
executor: ForegroundExecutor,
instance_buffer_pool: Arc<Mutex<Vec<metal::Buffer>>>,
) -> Self {
unsafe {
let pool = NSAutoreleasePool::new(nil);
@@ -521,15 +528,17 @@ impl MacWindow {
let native_view: id = msg_send![VIEW_CLASS, alloc];
let native_view = NSView::init(native_view);
assert!(!native_view.is_null());
let display_link = start_display_link(native_window.screen(), native_view);
let window = Self(Arc::new(Mutex::new(MacWindowState {
handle,
executor,
native_window,
native_view: NonNull::new_unchecked(native_view as *mut _),
renderer: MetalRenderer::new(true),
native_view: NonNull::new_unchecked(native_view),
display_link,
renderer: MetalRenderer::new(instance_buffer_pool),
kind: options.kind,
request_frame_callback: None,
event_callback: None,
@@ -663,6 +672,7 @@ impl MacWindow {
}
window.0.lock().move_traffic_light();
pool.drain();
window
@@ -683,10 +693,19 @@ impl MacWindow {
}
}
unsafe fn start_display_link(native_screen: id, native_view: id) -> id {
let display_link: id =
msg_send![native_screen, displayLinkWithTarget: native_view selector: sel!(step:)];
let main_run_loop: id = msg_send![class!(NSRunLoop), mainRunLoop];
let _: () = msg_send![display_link, addToRunLoop: main_run_loop forMode: NSDefaultRunLoopMode];
display_link
}
impl Drop for MacWindow {
fn drop(&mut self) {
let this = self.0.lock();
let mut this = self.0.lock();
let window = this.native_window;
this.display_link = nil;
this.executor
.spawn(async move {
unsafe {
@@ -1000,13 +1019,6 @@ impl PlatformWindow for MacWindow {
}
}
fn invalidate(&self) {
let this = self.0.lock();
unsafe {
let _: () = msg_send![this.native_window.contentView(), setNeedsDisplay: YES];
}
}
fn draw(&self, scene: &crate::Scene) {
let mut this = self.0.lock();
this.renderer.draw(scene);
@@ -1353,6 +1365,19 @@ extern "C" fn window_did_move(this: &Object, _: Sel, _: id) {
}
}
extern "C" fn window_did_change_screen(this: &Object, _: Sel, _: id) {
let window_state = unsafe { get_window_state(this) };
let mut lock = window_state.as_ref().lock();
unsafe {
let screen = lock.native_window.screen();
if screen == nil {
lock.display_link = nil;
} else {
lock.display_link = start_display_link(screen, lock.native_view.as_ptr());
}
}
}
extern "C" fn window_did_change_key_status(this: &Object, selector: Sel, _: id) {
let window_state = unsafe { get_window_state(this) };
let lock = window_state.lock();
@@ -1502,6 +1527,23 @@ extern "C" fn display_layer(this: &Object, _: Sel, _: id) {
}
}
extern "C" fn step(this: &Object, _: Sel, display_link: id) {
let window_state = unsafe { get_window_state(this) };
let mut lock = window_state.lock();
if lock.display_link == display_link {
if let Some(mut callback) = lock.request_frame_callback.take() {
drop(lock);
callback();
window_state.lock().request_frame_callback = Some(callback);
}
} else {
unsafe {
let _: () = msg_send![display_link, invalidate];
}
}
}
extern "C" fn valid_attributes_for_marked_text(_: &Object, _: Sel) -> id {
unsafe { msg_send![class!(NSArray), array] }
}

View File

@@ -284,8 +284,6 @@ impl PlatformWindow for TestWindow {
unimplemented!()
}
fn invalidate(&self) {}
fn draw(&self, _scene: &crate::Scene) {}
fn sprite_atlas(&self) -> sync::Arc<dyn crate::PlatformAtlas> {

View File

@@ -15,6 +15,7 @@ use crate::{
use anyhow::anyhow;
use collections::{BTreeSet, FxHashMap, FxHashSet};
use core::fmt;
use derive_more::Deref;
use itertools::Itertools;
use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard};
use smallvec::{smallvec, SmallVec};
@@ -38,9 +39,8 @@ pub struct FontFamilyId(pub usize);
pub(crate) const SUBPIXEL_VARIANTS: u8 = 4;
/// The GPUI text layout and rendering sub system.
/// The GPUI text rendering sub system.
pub struct TextSystem {
line_layout_cache: Arc<LineLayoutCache>,
platform_text_system: Arc<dyn PlatformTextSystem>,
font_ids_by_font: RwLock<FxHashMap<Font, Result<FontId>>>,
font_metrics: RwLock<FxHashMap<FontId, FontMetrics>>,
@@ -53,7 +53,6 @@ pub struct TextSystem {
impl TextSystem {
pub(crate) fn new(platform_text_system: Arc<dyn PlatformTextSystem>) -> Self {
TextSystem {
line_layout_cache: Arc::new(LineLayoutCache::new(platform_text_system.clone())),
platform_text_system,
font_metrics: RwLock::default(),
raster_bounds: RwLock::default(),
@@ -234,43 +233,66 @@ impl TextSystem {
}
}
pub(crate) fn with_view<R>(&self, view_id: EntityId, f: impl FnOnce() -> R) -> R {
self.line_layout_cache.with_view(view_id, f)
/// Returns a handle to a line wrapper, for the given font and font size.
pub fn line_wrapper(self: &Arc<Self>, font: Font, font_size: Pixels) -> LineWrapperHandle {
let lock = &mut self.wrapper_pool.lock();
let font_id = self.resolve_font(&font);
let wrappers = lock
.entry(FontIdWithSize { font_id, font_size })
.or_default();
let wrapper = wrappers.pop().unwrap_or_else(|| {
LineWrapper::new(font_id, font_size, self.platform_text_system.clone())
});
LineWrapperHandle {
wrapper: Some(wrapper),
text_system: self.clone(),
}
}
/// Layout the given line of text, at the given font_size.
/// Subsets of the line can be styled independently with the `runs` parameter.
/// Generally, you should prefer to use `TextLayout::shape_line` instead, which
/// can be painted directly.
pub fn layout_line(
&self,
text: &str,
font_size: Pixels,
runs: &[TextRun],
) -> Result<Arc<LineLayout>> {
let mut font_runs = self.font_runs_pool.lock().pop().unwrap_or_default();
for run in runs.iter() {
let font_id = self.resolve_font(&run.font);
if let Some(last_run) = font_runs.last_mut() {
if last_run.font_id == font_id {
last_run.len += run.len;
continue;
}
}
font_runs.push(FontRun {
len: run.len,
font_id,
});
/// Get the rasterized size and location of a specific, rendered glyph.
pub(crate) fn raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
let raster_bounds = self.raster_bounds.upgradable_read();
if let Some(bounds) = raster_bounds.get(params) {
Ok(*bounds)
} else {
let mut raster_bounds = RwLockUpgradableReadGuard::upgrade(raster_bounds);
let bounds = self.platform_text_system.glyph_raster_bounds(params)?;
raster_bounds.insert(params.clone(), bounds);
Ok(bounds)
}
}
let layout = self
.line_layout_cache
.layout_line(text, font_size, &font_runs);
pub(crate) fn rasterize_glyph(
&self,
params: &RenderGlyphParams,
) -> Result<(Size<DevicePixels>, Vec<u8>)> {
let raster_bounds = self.raster_bounds(params)?;
self.platform_text_system
.rasterize_glyph(params, raster_bounds)
}
}
font_runs.clear();
self.font_runs_pool.lock().push(font_runs);
/// The GPUI text layout subsystem.
#[derive(Deref)]
pub struct WindowTextSystem {
line_layout_cache: Arc<LineLayoutCache>,
#[deref]
text_system: Arc<TextSystem>,
}
Ok(layout)
impl WindowTextSystem {
pub(crate) fn new(text_system: Arc<TextSystem>) -> Self {
Self {
line_layout_cache: Arc::new(LineLayoutCache::new(
text_system.platform_text_system.clone(),
)),
text_system,
}
}
pub(crate) fn with_view<R>(&self, view_id: EntityId, f: impl FnOnce() -> R) -> R {
self.line_layout_cache.with_view(view_id, f)
}
/// Shape the given line, at the given font_size, for painting to the screen.
@@ -429,43 +451,39 @@ impl TextSystem {
self.line_layout_cache.finish_frame(reused_views)
}
/// Returns a handle to a line wrapper, for the given font and font size.
pub fn line_wrapper(self: &Arc<Self>, font: Font, font_size: Pixels) -> LineWrapperHandle {
let lock = &mut self.wrapper_pool.lock();
let font_id = self.resolve_font(&font);
let wrappers = lock
.entry(FontIdWithSize { font_id, font_size })
.or_default();
let wrapper = wrappers.pop().unwrap_or_else(|| {
LineWrapper::new(font_id, font_size, self.platform_text_system.clone())
});
LineWrapperHandle {
wrapper: Some(wrapper),
text_system: self.clone(),
}
}
/// Get the rasterized size and location of a specific, rendered glyph.
pub(crate) fn raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
let raster_bounds = self.raster_bounds.upgradable_read();
if let Some(bounds) = raster_bounds.get(params) {
Ok(*bounds)
} else {
let mut raster_bounds = RwLockUpgradableReadGuard::upgrade(raster_bounds);
let bounds = self.platform_text_system.glyph_raster_bounds(params)?;
raster_bounds.insert(params.clone(), bounds);
Ok(bounds)
}
}
pub(crate) fn rasterize_glyph(
/// Layout the given line of text, at the given font_size.
/// Subsets of the line can be styled independently with the `runs` parameter.
/// Generally, you should prefer to use `TextLayout::shape_line` instead, which
/// can be painted directly.
pub fn layout_line(
&self,
params: &RenderGlyphParams,
) -> Result<(Size<DevicePixels>, Vec<u8>)> {
let raster_bounds = self.raster_bounds(params)?;
self.platform_text_system
.rasterize_glyph(params, raster_bounds)
text: &str,
font_size: Pixels,
runs: &[TextRun],
) -> Result<Arc<LineLayout>> {
let mut font_runs = self.font_runs_pool.lock().pop().unwrap_or_default();
for run in runs.iter() {
let font_id = self.resolve_font(&run.font);
if let Some(last_run) = font_runs.last_mut() {
if last_run.font_id == font_id {
last_run.len += run.len;
continue;
}
}
font_runs.push(FontRun {
len: run.len,
font_id,
});
}
let layout = self
.line_layout_cache
.layout_line(text, font_size, &font_runs);
font_runs.clear();
self.font_runs_pool.lock().push(font_runs);
Ok(layout)
}
}

View File

@@ -143,7 +143,7 @@ impl Boundary {
#[cfg(test)]
mod tests {
use super::*;
use crate::{font, TestAppContext, TestDispatcher, TextRun, WrapBoundary};
use crate::{font, TestAppContext, TestDispatcher, TextRun, WindowTextSystem, WrapBoundary};
use rand::prelude::*;
#[test]
@@ -218,7 +218,7 @@ mod tests {
#[crate::test]
fn test_wrap_shaped_line(cx: &mut TestAppContext) {
cx.update(|cx| {
let text_system = cx.text_system().clone();
let text_system = WindowTextSystem::new(cx.text_system().clone());
let normal = TextRun {
len: 0,

View File

@@ -2,12 +2,12 @@ use crate::{
px, size, transparent_black, Action, AnyDrag, AnyView, AppContext, Arena, AsyncWindowContext,
AvailableSpace, Bounds, Context, Corners, CursorStyle, DispatchActionListener, DispatchNodeId,
DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter, FileDropEvent, Flatten,
Global, GlobalElementId, Hsla, KeyBinding, KeyContext, KeyDownEvent, KeyMatch, KeymatchMode,
KeymatchResult, Keystroke, KeystrokeEvent, Model, ModelContext, Modifiers, MouseButton,
MouseMoveEvent, MouseUpEvent, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput,
PlatformWindow, Point, PromptLevel, Render, ScaledPixels, SharedString, Size, SubscriberSet,
Subscription, TaffyLayoutEngine, Task, View, VisualContext, WeakView, WindowBounds,
WindowOptions,
Global, GlobalElementId, Hsla, KeyBinding, KeyContext, KeyDownEvent, KeyMatch, KeymatchResult,
Keystroke, KeystrokeEvent, Model, ModelContext, Modifiers, MouseButton, MouseMoveEvent,
MouseUpEvent, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformWindow, Point,
PromptLevel, Render, ScaledPixels, SharedString, Size, SubscriberSet, Subscription,
TaffyLayoutEngine, Task, View, VisualContext, WeakView, WindowBounds, WindowOptions,
WindowTextSystem,
};
use anyhow::{anyhow, Context as _, Result};
use collections::FxHashSet;
@@ -22,7 +22,7 @@ use smallvec::SmallVec;
use std::{
any::{Any, TypeId},
borrow::{Borrow, BorrowMut},
cell::RefCell,
cell::{Cell, RefCell},
collections::hash_map::Entry,
fmt::{Debug, Display},
future::Future,
@@ -34,7 +34,7 @@ use std::{
atomic::{AtomicUsize, Ordering::SeqCst},
Arc,
},
time::Duration,
time::{Duration, Instant},
};
use util::{measure, ResultExt};
@@ -251,6 +251,7 @@ pub struct Window {
pub(crate) platform_window: Box<dyn PlatformWindow>,
display_id: DisplayId,
sprite_atlas: Arc<dyn PlatformAtlas>,
text_system: Arc<WindowTextSystem>,
pub(crate) rem_size: Pixels,
pub(crate) viewport_size: Size<Pixels>,
layout_engine: Option<TaffyLayoutEngine>,
@@ -268,17 +269,15 @@ pub struct Window {
scale_factor: f32,
bounds: WindowBounds,
bounds_observers: SubscriberSet<(), AnyObserver>,
active: bool,
pub(crate) dirty: bool,
active: Rc<Cell<bool>>,
pub(crate) dirty: Rc<Cell<bool>>,
pub(crate) last_input_timestamp: Rc<Cell<Instant>>,
pub(crate) refreshing: bool,
pub(crate) drawing: bool,
activation_observers: SubscriberSet<(), AnyObserver>,
pub(crate) focus: Option<FocusId>,
focus_enabled: bool,
pending_input: Option<PendingInput>,
#[cfg(any(test, feature = "test-support"))]
pub(crate) focus_invalidated: bool,
}
#[derive(Default, Debug)]
@@ -290,10 +289,6 @@ struct PendingInput {
}
impl PendingInput {
fn is_noop(&self) -> bool {
self.bindings.is_empty() && (self.keystrokes.iter().all(|k| k.ime_key.is_none()))
}
fn input(&self) -> String {
self.keystrokes
.iter()
@@ -337,13 +332,34 @@ impl Window {
let content_size = platform_window.content_size();
let scale_factor = platform_window.scale_factor();
let bounds = platform_window.bounds();
let text_system = Arc::new(WindowTextSystem::new(cx.text_system().clone()));
let dirty = Rc::new(Cell::new(true));
let active = Rc::new(Cell::new(false));
let last_input_timestamp = Rc::new(Cell::new(Instant::now()));
platform_window.on_request_frame(Box::new({
let mut cx = cx.to_async();
let dirty = dirty.clone();
let active = active.clone();
let last_input_timestamp = last_input_timestamp.clone();
move || {
measure("frame duration", || {
handle.update(&mut cx, |_, cx| cx.draw()).log_err();
})
if dirty.get() {
measure("frame duration", || {
handle
.update(&mut cx, |_, cx| {
cx.draw();
cx.present();
})
.log_err();
})
}
// Keep presenting the current scene for 1 extra second since the
// last input to prevent the display from underclocking the refresh rate.
else if active.get()
&& last_input_timestamp.get().elapsed() < Duration::from_secs(1)
{
handle.update(&mut cx, |_, cx| cx.present()).log_err();
}
}
}));
platform_window.on_resize(Box::new({
@@ -367,7 +383,7 @@ impl Window {
move |active| {
handle
.update(&mut cx, |_, cx| {
cx.window.active = active;
cx.window.active.set(active);
cx.window
.activation_observers
.clone()
@@ -393,6 +409,7 @@ impl Window {
platform_window,
display_id,
sprite_atlas,
text_system,
rem_size: px(16.),
viewport_size: content_size,
layout_engine: Some(TaffyLayoutEngine::new()),
@@ -410,17 +427,15 @@ impl Window {
scale_factor,
bounds,
bounds_observers: SubscriberSet::new(),
active: false,
dirty: false,
active,
dirty,
last_input_timestamp,
refreshing: false,
drawing: false,
activation_observers: SubscriberSet::new(),
focus: None,
focus_enabled: true,
pending_input: None,
#[cfg(any(test, feature = "test-support"))]
focus_invalidated: false,
}
}
}
@@ -472,7 +487,7 @@ impl<'a> WindowContext<'a> {
pub fn refresh(&mut self) {
if !self.window.drawing {
self.window.refreshing = true;
self.window.dirty = true;
self.window.dirty.set(true);
}
}
@@ -505,12 +520,6 @@ impl<'a> WindowContext<'a> {
.rendered_frame
.dispatch_tree
.clear_pending_keystrokes();
#[cfg(any(test, feature = "test-support"))]
{
self.window.focus_invalidated = true;
}
self.refresh();
}
@@ -530,6 +539,11 @@ impl<'a> WindowContext<'a> {
self.window.focus_enabled = false;
}
/// Accessor for the text system.
pub fn text_system(&self) -> &Arc<WindowTextSystem> {
&self.window.text_system
}
/// Dispatch the given action on the currently focused element.
pub fn dispatch_action(&mut self, action: Box<dyn Action>) {
let focus_handle = self.focused();
@@ -741,7 +755,7 @@ impl<'a> WindowContext<'a> {
/// Returns whether this window is focused by the operating system (receiving key events).
pub fn is_window_active(&self) -> bool {
self.window.active
self.window.active.get()
}
/// Toggle zoom on the window.
@@ -927,16 +941,12 @@ impl<'a> WindowContext<'a> {
&self.window.next_frame.z_index_stack
}
/// Draw pixels to the display for this window based on the contents of its scene.
/// Produces a new frame and assigns it to `rendered_frame`. To actually show
/// the contents of the new [Scene], use [present].
pub(crate) fn draw(&mut self) {
self.window.dirty = false;
self.window.dirty.set(false);
self.window.drawing = true;
#[cfg(any(test, feature = "test-support"))]
{
self.window.focus_invalidated = false;
}
if let Some(requested_handler) = self.window.rendered_frame.requested_input_handler.as_mut()
{
let input_handler = self.window.platform_window.take_input_handler();
@@ -1001,7 +1011,7 @@ impl<'a> WindowContext<'a> {
self.window.focus,
);
self.window.next_frame.focus = self.window.focus;
self.window.next_frame.window_active = self.window.active;
self.window.next_frame.window_active = self.window.active.get();
self.window.root_view = Some(root_view);
// Set the cursor only if we're the active window.
@@ -1070,16 +1080,19 @@ impl<'a> WindowContext<'a> {
.clone()
.retain(&(), |listener| listener(&event, self));
}
self.window
.platform_window
.draw(&self.window.rendered_frame.scene);
self.window.refreshing = false;
self.window.drawing = false;
}
fn present(&self) {
self.window
.platform_window
.draw(&self.window.rendered_frame.scene);
}
/// Dispatch a mouse or keyboard event on the window.
pub fn dispatch_event(&mut self, event: PlatformInput) -> bool {
self.window.last_input_timestamp.set(Instant::now());
// Handlers may set this to false by calling `stop_propagation`.
self.app.propagate_event = true;
// Handlers may set this to true by calling `prevent_default`.
@@ -1243,21 +1256,12 @@ impl<'a> WindowContext<'a> {
.dispatch_path(node_id);
if let Some(key_down_event) = event.downcast_ref::<KeyDownEvent>() {
let KeymatchResult {
bindings,
mut pending,
} = self
let KeymatchResult { bindings, pending } = self
.window
.rendered_frame
.dispatch_tree
.dispatch_key(&key_down_event.keystroke, &dispatch_path);
if self.window.rendered_frame.dispatch_tree.keymatch_mode == KeymatchMode::Immediate
&& !bindings.is_empty()
{
pending = false;
}
if pending {
let mut currently_pending = self.window.pending_input.take().unwrap_or_default();
if currently_pending.focus.is_some() && currently_pending.focus != self.window.focus
@@ -1272,22 +1276,17 @@ impl<'a> WindowContext<'a> {
currently_pending.bindings.push(binding);
}
// for vim compatibility, we also should check "is input handler enabled"
if !currently_pending.is_noop() {
currently_pending.timer = Some(self.spawn(|mut cx| async move {
cx.background_executor.timer(Duration::from_secs(1)).await;
cx.update(move |cx| {
cx.clear_pending_keystrokes();
let Some(currently_pending) = cx.window.pending_input.take() else {
return;
};
cx.replay_pending_input(currently_pending)
})
.log_err();
}));
} else {
currently_pending.timer = None;
}
currently_pending.timer = Some(self.spawn(|mut cx| async move {
cx.background_executor.timer(Duration::from_secs(1)).await;
cx.update(move |cx| {
cx.clear_pending_keystrokes();
let Some(currently_pending) = cx.window.pending_input.take() else {
return;
};
cx.replay_pending_input(currently_pending)
})
.log_err();
}));
self.window.pending_input = Some(currently_pending);
self.propagate_event = false;
@@ -1315,8 +1314,21 @@ impl<'a> WindowContext<'a> {
}
}
self.dispatch_key_down_up_event(event, &dispatch_path);
if !self.propagate_event {
return;
}
self.dispatch_keystroke_observers(event, None);
}
fn dispatch_key_down_up_event(
&mut self,
event: &dyn Any,
dispatch_path: &SmallVec<[DispatchNodeId; 32]>,
) {
// Capture phase
for node_id in &dispatch_path {
for node_id in dispatch_path {
let node = self.window.rendered_frame.dispatch_tree.node(*node_id);
for key_listener in node.key_listeners.clone() {
@@ -1342,8 +1354,6 @@ impl<'a> WindowContext<'a> {
}
}
}
self.dispatch_keystroke_observers(event, None);
}
/// Determine whether a potential multi-stroke key binding is in progress on this window.
@@ -1380,6 +1390,24 @@ impl<'a> WindowContext<'a> {
}
}
let dispatch_path = self
.window
.rendered_frame
.dispatch_tree
.dispatch_path(node_id);
for keystroke in currently_pending.keystrokes {
let event = KeyDownEvent {
keystroke,
is_held: false,
};
self.dispatch_key_down_up_event(&event, &dispatch_path);
if !self.propagate_event {
return;
}
}
if !input.is_empty() {
if let Some(mut input_handler) = self.window.platform_window.take_input_handler() {
input_handler.flush_pending_input(&input, self);
@@ -2023,7 +2051,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
}
if !self.window.drawing {
self.window_cx.window.dirty = true;
self.window_cx.window.dirty.set(true);
self.window_cx.app.push_effect(Effect::Notify {
emitter: self.view.model.entity_id,
});
@@ -2489,7 +2517,7 @@ impl<V: 'static + Render> WindowHandle<V> {
pub fn is_active(&self, cx: &AppContext) -> Option<bool> {
cx.windows
.get(self.id)
.and_then(|window| window.as_ref().map(|window| window.active))
.and_then(|window| window.as_ref().map(|window| window.active.get()))
}
}

View File

@@ -31,11 +31,11 @@ use crate::{
prelude::*, size, AnyTooltip, AppContext, AvailableSpace, Bounds, BoxShadow, ContentMask,
Corners, CursorStyle, DevicePixels, DispatchPhase, DispatchTree, ElementId, ElementStateBox,
EntityId, FocusHandle, FocusId, FontId, GlobalElementId, GlyphId, Hsla, ImageData,
InputHandler, IsZero, KeyContext, KeyEvent, KeymatchMode, LayoutId, MonochromeSprite,
MouseEvent, PaintQuad, Path, Pixels, PlatformInputHandler, Point, PolychromeSprite, Quad,
RenderGlyphParams, RenderImageParams, RenderSvgParams, Scene, Shadow, SharedString, Size,
StackingContext, StackingOrder, Style, Surface, TextStyleRefinement, Underline, UnderlineStyle,
Window, WindowContext, SUBPIXEL_VARIANTS,
InputHandler, IsZero, KeyContext, KeyEvent, LayoutId, MonochromeSprite, MouseEvent, PaintQuad,
Path, Pixels, PlatformInputHandler, Point, PolychromeSprite, Quad, RenderGlyphParams,
RenderImageParams, RenderSvgParams, Scene, Shadow, SharedString, Size, StackingContext,
StackingOrder, Style, Surface, TextStyleRefinement, Underline, UnderlineStyle, Window,
WindowContext, SUBPIXEL_VARIANTS,
};
type AnyMouseListener = Box<dyn FnMut(&dyn Any, DispatchPhase, &mut ElementContext) + 'static>;
@@ -1143,15 +1143,6 @@ impl<'a> ElementContext<'a> {
}
}
/// keymatch mode immediate instructs GPUI to prefer shorter action bindings.
/// In the case that you have a keybinding of `"cmd-k": "terminal::Clear"` and
/// `"cmd-k left": "workspace::MoveLeft"`, GPUI will by default wait for 1s after
/// you type cmd-k to see if you're going to type left.
/// This is problematic in the terminal
pub fn keymatch_mode_immediate(&mut self) {
self.window.next_frame.dispatch_tree.keymatch_mode = KeymatchMode::Immediate;
}
/// Register a mouse event listener on the window for the next frame. The type of event
/// is determined by the first parameter of the given listener. When the next frame is rendered
/// the listener will be cleared.

View File

@@ -384,7 +384,7 @@ pub trait File: Send + Sync {
/// Converts this file into a protobuf message.
fn to_proto(&self) -> rpc::proto::File;
/// Return whether Zed considers this to be a dotenv file.
/// Return whether Zed considers this to be a private file.
fn is_private(&self) -> bool;
}
@@ -406,6 +406,11 @@ pub trait LocalFile: File {
mtime: SystemTime,
cx: &mut AppContext,
);
/// Returns true if the file should not be shared with collaborators.
fn is_private(&self, _: &AppContext) -> bool {
false
}
}
/// The auto-indent behavior associated with an editing operation.

View File

@@ -56,6 +56,7 @@ use postage::watch;
use prettier_support::{DefaultPrettier, PrettierInstance};
use project_settings::{LspSettings, ProjectSettings};
use rand::prelude::*;
use rpc::{ErrorCode, ErrorExt};
use search::SearchQuery;
use serde::Serialize;
use settings::{Settings, SettingsStore};
@@ -1760,7 +1761,7 @@ impl Project {
cx.background_executor().spawn(async move {
wait_for_loading_buffer(loading_watch)
.await
.map_err(|error| anyhow!("{project_path:?} opening failure: {error:#}"))
.map_err(|e| e.cloned())
})
}
@@ -8011,11 +8012,20 @@ impl Project {
.update(&mut cx, |this, cx| this.open_buffer_for_symbol(&symbol, cx))?
.await?;
Ok(proto::OpenBufferForSymbolResponse {
buffer_id: this.update(&mut cx, |this, cx| {
this.create_buffer_for_peer(&buffer, peer_id, cx).into()
})?,
})
this.update(&mut cx, |this, cx| {
let is_private = buffer
.read(cx)
.file()
.map(|f| f.is_private())
.unwrap_or_default();
if is_private {
Err(anyhow!(ErrorCode::UnsharedItem))
} else {
Ok(proto::OpenBufferForSymbolResponse {
buffer_id: this.create_buffer_for_peer(&buffer, peer_id, cx).into(),
})
}
})?
}
fn symbol_signature(&self, project_path: &ProjectPath) -> [u8; 32] {
@@ -8037,11 +8047,7 @@ impl Project {
let buffer = this
.update(&mut cx, |this, cx| this.open_buffer_by_id(buffer_id, cx))?
.await?;
this.update(&mut cx, |this, cx| {
Ok(proto::OpenBufferResponse {
buffer_id: this.create_buffer_for_peer(&buffer, peer_id, cx).into(),
})
})?
Project::respond_to_open_buffer_request(this, buffer, peer_id, &mut cx)
}
async fn handle_open_buffer_by_path(
@@ -8063,10 +8069,28 @@ impl Project {
})?;
let buffer = open_buffer.await?;
this.update(&mut cx, |this, cx| {
Ok(proto::OpenBufferResponse {
buffer_id: this.create_buffer_for_peer(&buffer, peer_id, cx).into(),
})
Project::respond_to_open_buffer_request(this, buffer, peer_id, &mut cx)
}
fn respond_to_open_buffer_request(
this: Model<Self>,
buffer: Model<Buffer>,
peer_id: proto::PeerId,
cx: &mut AsyncAppContext,
) -> Result<proto::OpenBufferResponse> {
this.update(cx, |this, cx| {
let is_private = buffer
.read(cx)
.file()
.map(|f| f.is_private())
.unwrap_or_default();
if is_private {
Err(anyhow!(ErrorCode::UnsharedItem))
} else {
Ok(proto::OpenBufferResponse {
buffer_id: this.create_buffer_for_peer(&buffer, peer_id, cx).into(),
})
}
})?
}

View File

@@ -31,6 +31,7 @@ theme = { path = "../theme" }
ui = { path = "../ui" }
unicase = "2.6"
util = { path = "../util" }
client = { path = "../client" }
workspace = { path = "../workspace", package = "workspace" }
[dev-dependencies]

View File

@@ -1,5 +1,6 @@
pub mod file_associations;
mod project_panel_settings;
use client::{ErrorCode, ErrorExt};
use settings::Settings;
use db::kvp::KEY_VALUE_STORE;
@@ -35,6 +36,7 @@ use unicase::UniCase;
use util::{maybe, ResultExt, TryFutureExt};
use workspace::{
dock::{DockPosition, Panel, PanelEvent},
notifications::DetachAndPromptErr,
Workspace,
};
@@ -259,6 +261,7 @@ impl ProjectPanel {
} => {
if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) {
if let Some(entry) = worktree.read(cx).entry_for_id(entry_id) {
let file_path = entry.path.clone();
workspace
.open_path(
ProjectPath {
@@ -269,7 +272,15 @@ impl ProjectPanel {
focus_opened_item,
cx,
)
.detach_and_log_err(cx);
.detach_and_prompt_err("Failed to open file", cx, move |e, _| {
match e.error_code() {
ErrorCode::UnsharedItem => Some(format!(
"{} is not shared by the host. This could be because it has been marked as `private`",
file_path.display()
)),
_ => None,
}
});
if !focus_opened_item {
if let Some(project_panel) = project_panel.upgrade() {
let focus_handle = project_panel.read(cx).focus_handle.clone();

View File

@@ -216,6 +216,7 @@ enum ErrorCode {
BadPublicNesting = 9;
CircularNesting = 10;
WrongMoveTarget = 11;
UnsharedItem = 12;
}
message Test {

View File

@@ -80,6 +80,8 @@ pub trait ErrorExt {
fn error_tag(&self, k: &str) -> Option<&str>;
/// to_proto() converts the error into a proto::Error
fn to_proto(&self) -> proto::Error;
///
fn cloned(&self) -> anyhow::Error;
}
impl ErrorExt for anyhow::Error {
@@ -106,6 +108,14 @@ impl ErrorExt for anyhow::Error {
ErrorCode::Internal.message(format!("{}", self)).to_proto()
}
}
fn cloned(&self) -> anyhow::Error {
if let Some(rpc_error) = self.downcast_ref::<RpcError>() {
rpc_error.cloned()
} else {
anyhow::anyhow!("{}", self)
}
}
}
impl From<proto::ErrorCode> for anyhow::Error {
@@ -189,6 +199,10 @@ impl ErrorExt for RpcError {
tags: self.tags.clone(),
}
}
fn cloned(&self) -> anyhow::Error {
self.clone().into()
}
}
impl std::error::Error for RpcError {

View File

@@ -4,8 +4,8 @@ use gpui::{
ElementContext, ElementId, FocusHandle, Font, FontStyle, FontWeight, HighlightStyle, Hsla,
InputHandler, InteractiveBounds, InteractiveElement, InteractiveElementState, Interactivity,
IntoElement, LayoutId, Model, ModelContext, ModifiersChangedEvent, MouseButton, MouseMoveEvent,
Pixels, Point, ShapedLine, StatefulInteractiveElement, Styled, TextRun, TextStyle, TextSystem,
UnderlineStyle, WeakView, WhiteSpace, WindowContext,
Pixels, Point, ShapedLine, StatefulInteractiveElement, Styled, TextRun, TextStyle,
UnderlineStyle, WeakView, WhiteSpace, WindowContext, WindowTextSystem,
};
use itertools::Itertools;
use language::CursorShape;
@@ -185,7 +185,7 @@ impl TerminalElement {
grid: &Vec<IndexedCell>,
text_style: &TextStyle,
// terminal_theme: &TerminalStyle,
text_system: &TextSystem,
text_system: &WindowTextSystem,
hyperlink: Option<(HighlightStyle, &RangeInclusive<AlacPoint>)>,
cx: &WindowContext<'_>,
) -> (Vec<LayoutCell>, Vec<LayoutRect>) {
@@ -776,7 +776,6 @@ impl Element for TerminalElement {
self.interactivity
.paint(bounds, bounds.size, state, cx, |_, _, cx| {
cx.handle_input(&self.focus, terminal_input_handler);
cx.keymatch_mode_immediate();
cx.on_key_event({
let this = self.terminal.clone();

View File

@@ -255,19 +255,23 @@ impl ThemeRegistry {
continue;
};
let Some(reader) = fs.open_sync(&theme_path).await.log_err() else {
continue;
};
let Some(theme) = serde_json_lenient::from_reader(reader).log_err() else {
continue;
};
self.insert_user_theme_families([theme]);
self.load_user_theme(&theme_path, fs.clone())
.await
.log_err();
}
Ok(())
}
/// Loads the user theme from the specified path and adds it to the registry.
pub async fn load_user_theme(&self, theme_path: &Path, fs: Arc<dyn Fs>) -> Result<()> {
let reader = fs.open_sync(&theme_path).await?;
let theme = serde_json_lenient::from_reader(reader)?;
self.insert_user_theme_families([theme]);
Ok(())
}
}
impl Default for ThemeRegistry {

View File

@@ -68,6 +68,6 @@ pub async fn latest_github_release(
releases
.into_iter()
.find(|release| release.pre_release == pre_release)
.find(|release| !release.assets.is_empty() && release.pre_release == pre_release)
.ok_or(anyhow!("Failed to find a release"))
}

View File

@@ -176,11 +176,19 @@ impl Member {
return div().into_any();
}
let leader = follower_states.get(pane).and_then(|state| {
let follower_state = follower_states.get(pane);
let leader = follower_state.and_then(|state| {
let room = active_call?.read(cx).room()?.read(cx);
room.remote_participant_for_peer_id(state.leader_id)
});
let is_in_unshared_view = follower_state.map_or(false, |state| {
state.active_view_id.is_some_and(|view_id| {
!state.items_by_leader_view_id.contains_key(&view_id)
})
});
let mut leader_border = None;
let mut leader_status_box = None;
let mut leader_join_data = None;
@@ -198,7 +206,14 @@ impl Member {
project_id: leader_project_id,
} => {
if Some(leader_project_id) == project.read(cx).remote_id() {
None
if is_in_unshared_view {
Some(Label::new(format!(
"{} is in an unshared pane",
leader.user.github_login
)))
} else {
None
}
} else {
leader_join_data = Some((leader_project_id, leader.user.id));
Some(Label::new(format!(

View File

@@ -2,7 +2,7 @@
description = "The fast, collaborative code editor."
edition = "2021"
name = "zed"
version = "0.121.0"
version = "0.121.4"
publish = false
license = "GPL-3.0-or-later"

View File

@@ -1 +1 @@
dev
preview

View File

@@ -11,6 +11,7 @@ use db::kvp::KEY_VALUE_STORE;
use editor::Editor;
use env_logger::Builder;
use fs::RealFs;
use fsevent::StreamFlags;
use futures::StreamExt;
use gpui::{App, AppContext, AsyncAppContext, Context, SemanticVersion, Task};
use isahc::{prelude::Configurable, Request};
@@ -171,35 +172,8 @@ fn main() {
);
assistant::init(cx);
// TODO: Should we be loading the themes in a different spot?
cx.spawn({
let fs = fs.clone();
|cx| async move {
if let Some(theme_registry) =
cx.update(|cx| ThemeRegistry::global(cx).clone()).log_err()
{
if let Some(()) = theme_registry
.load_user_themes(&paths::THEMES_DIR.clone(), fs)
.await
.log_err()
{
cx.update(|cx| {
let mut theme_settings = ThemeSettings::get_global(cx).clone();
if let Some(requested_theme) = theme_settings.requested_theme.clone() {
if let Some(_theme) =
theme_settings.switch_theme(&requested_theme, cx)
{
ThemeSettings::override_global(theme_settings, cx);
}
}
})
.log_err();
}
}
}
})
.detach();
load_user_themes_in_background(fs.clone(), cx);
watch_themes(fs.clone(), cx);
cx.spawn(|_| watch_languages(fs.clone(), languages.clone()))
.detach();
@@ -899,6 +873,81 @@ fn load_embedded_fonts(cx: &AppContext) {
.unwrap();
}
/// Spawns a background task to load the user themes from the themes directory.
fn load_user_themes_in_background(fs: Arc<dyn fs::Fs>, cx: &mut AppContext) {
cx.spawn({
let fs = fs.clone();
|cx| async move {
if let Some(theme_registry) =
cx.update(|cx| ThemeRegistry::global(cx).clone()).log_err()
{
if let Some(()) = theme_registry
.load_user_themes(&paths::THEMES_DIR.clone(), fs)
.await
.log_err()
{
cx.update(|cx| {
let mut theme_settings = ThemeSettings::get_global(cx).clone();
if let Some(requested_theme) = theme_settings.requested_theme.clone() {
if let Some(_theme) = theme_settings.switch_theme(&requested_theme, cx)
{
ThemeSettings::override_global(theme_settings, cx);
}
}
})
.log_err();
}
}
}
})
.detach();
}
/// Spawns a background task to watch the themes directory for changes.
fn watch_themes(fs: Arc<dyn fs::Fs>, cx: &mut AppContext) {
cx.spawn(|cx| async move {
let mut events = fs
.watch(&paths::THEMES_DIR.clone(), Duration::from_millis(100))
.await;
while let Some(events) = events.next().await {
for event in events {
if event.flags.contains(StreamFlags::ITEM_REMOVED) {
// Theme was removed, don't need to reload.
// We may want to remove the theme from the registry, in this case.
} else {
if let Some(theme_registry) =
cx.update(|cx| ThemeRegistry::global(cx).clone()).log_err()
{
if let Some(()) = theme_registry
.load_user_theme(&event.path, fs.clone())
.await
.log_err()
{
cx.update(|cx| {
let mut theme_settings = ThemeSettings::get_global(cx).clone();
if let Some(requested_theme) =
theme_settings.requested_theme.clone()
{
if let Some(_theme) =
theme_settings.switch_theme(&requested_theme, cx)
{
ThemeSettings::override_global(theme_settings, cx);
}
}
})
.log_err();
}
}
}
}
}
})
.detach()
}
async fn watch_languages(fs: Arc<dyn fs::Fs>, languages: Arc<LanguageRegistry>) {
let reload_debounce = Duration::from_millis(250);