Merge branch 'main' into styles-in-typescript

This commit is contained in:
Nathan Sobo
2022-04-01 09:46:51 -06:00
37 changed files with 1430 additions and 630 deletions
Generated
+14
View File
@@ -719,6 +719,19 @@ dependencies = [
"once_cell",
]
[[package]]
name = "breadcrumbs"
version = "0.1.0"
dependencies = [
"collections",
"editor",
"gpui",
"language",
"search",
"theme",
"workspace",
]
[[package]]
name = "brotli"
version = "3.3.0"
@@ -5963,6 +5976,7 @@ dependencies = [
"async-compression",
"async-recursion",
"async-trait",
"breadcrumbs",
"chat_panel",
"client",
"clock",
+22
View File
@@ -0,0 +1,22 @@
[package]
name = "breadcrumbs"
version = "0.1.0"
edition = "2021"
[lib]
path = "src/breadcrumbs.rs"
doctest = false
[dependencies]
collections = { path = "../collections" }
editor = { path = "../editor" }
gpui = { path = "../gpui" }
language = { path = "../language" }
search = { path = "../search" }
theme = { path = "../theme" }
workspace = { path = "../workspace" }
[dev-dependencies]
editor = { path = "../editor", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
workspace = { path = "../workspace", features = ["test-support"] }
+146
View File
@@ -0,0 +1,146 @@
use editor::{Anchor, Editor};
use gpui::{
elements::*, AppContext, Entity, RenderContext, Subscription, View, ViewContext, ViewHandle,
};
use language::{BufferSnapshot, OutlineItem};
use search::ProjectSearchView;
use std::borrow::Cow;
use theme::SyntaxTheme;
use workspace::{ItemHandle, Settings, ToolbarItemLocation, ToolbarItemView};
pub enum Event {
UpdateLocation,
}
pub struct Breadcrumbs {
editor: Option<ViewHandle<Editor>>,
project_search: Option<ViewHandle<ProjectSearchView>>,
subscriptions: Vec<Subscription>,
}
impl Breadcrumbs {
pub fn new() -> Self {
Self {
editor: Default::default(),
subscriptions: Default::default(),
project_search: Default::default(),
}
}
fn active_symbols(
&self,
theme: &SyntaxTheme,
cx: &AppContext,
) -> Option<(BufferSnapshot, Vec<OutlineItem<Anchor>>)> {
let editor = self.editor.as_ref()?.read(cx);
let cursor = editor.newest_anchor_selection().head();
let (buffer, symbols) = editor
.buffer()
.read(cx)
.read(cx)
.symbols_containing(cursor, Some(theme))?;
Some((buffer, symbols))
}
}
impl Entity for Breadcrumbs {
type Event = Event;
}
impl View for Breadcrumbs {
fn ui_name() -> &'static str {
"Breadcrumbs"
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
let theme = cx.global::<Settings>().theme.clone();
let (buffer, symbols) =
if let Some((buffer, symbols)) = self.active_symbols(&theme.editor.syntax, cx) {
(buffer, symbols)
} else {
return Empty::new().boxed();
};
let filename = if let Some(path) = buffer.path() {
path.to_string_lossy()
} else {
Cow::Borrowed("untitled")
};
Flex::row()
.with_child(Label::new(filename.to_string(), theme.breadcrumbs.text.clone()).boxed())
.with_children(symbols.into_iter().flat_map(|symbol| {
[
Label::new("".to_string(), theme.breadcrumbs.text.clone()).boxed(),
Text::new(symbol.text, theme.breadcrumbs.text.clone())
.with_highlights(symbol.highlight_ranges)
.boxed(),
]
}))
.contained()
.with_style(theme.breadcrumbs.container)
.aligned()
.left()
.boxed()
}
}
impl ToolbarItemView for Breadcrumbs {
fn set_active_pane_item(
&mut self,
active_pane_item: Option<&dyn ItemHandle>,
cx: &mut ViewContext<Self>,
) -> ToolbarItemLocation {
cx.notify();
self.subscriptions.clear();
self.editor = None;
self.project_search = None;
if let Some(item) = active_pane_item {
if let Some(editor) = item.act_as::<Editor>(cx) {
self.subscriptions
.push(cx.subscribe(&editor, |_, _, event, cx| match event {
editor::Event::BufferEdited => cx.notify(),
editor::Event::SelectionsChanged { local } if *local => cx.notify(),
_ => {}
}));
self.editor = Some(editor);
if let Some(project_search) = item.downcast::<ProjectSearchView>() {
self.subscriptions
.push(cx.subscribe(&project_search, |_, _, _, cx| {
cx.emit(Event::UpdateLocation);
}));
self.project_search = Some(project_search.clone());
if project_search.read(cx).has_matches() {
ToolbarItemLocation::Secondary
} else {
ToolbarItemLocation::Hidden
}
} else {
ToolbarItemLocation::PrimaryLeft { flex: None }
}
} else {
ToolbarItemLocation::Hidden
}
} else {
ToolbarItemLocation::Hidden
}
}
fn location_for_event(
&self,
_: &Event,
current_location: ToolbarItemLocation,
cx: &AppContext,
) -> ToolbarItemLocation {
if let Some(project_search) = self.project_search.as_ref() {
if project_search.read(cx).has_matches() {
ToolbarItemLocation::Secondary
} else {
ToolbarItemLocation::Hidden
}
} else {
current_location
}
}
}
+1 -1
View File
@@ -219,7 +219,7 @@ impl ChatPanel {
Empty::new().boxed()
};
Flexible::new(1., true, messages).boxed()
FlexItem::new(messages).flex(1., true).boxed()
}
fn render_message(&self, message: &ChannelMessage, cx: &AppContext) -> ElementBox {
+1 -1
View File
@@ -212,7 +212,7 @@ impl ContactsPanel {
}));
}
})
.flexible(1., true)
.flex(1., true)
.boxed()
})
.constrained()
+13
View File
@@ -141,6 +141,7 @@ action!(ToggleCodeActions, bool);
action!(ConfirmCompletion, Option<usize>);
action!(ConfirmCodeAction, Option<usize>);
action!(OpenExcerpts);
action!(RestartLanguageServer);
enum DocumentHighlightRead {}
enum DocumentHighlightWrite {}
@@ -302,6 +303,7 @@ pub fn init(cx: &mut MutableAppContext) {
Binding::new("ctrl-space", ShowCompletions, Some("Editor")),
Binding::new("cmd-.", ToggleCodeActions(false), Some("Editor")),
Binding::new("alt-enter", OpenExcerpts, Some("Editor")),
Binding::new("cmd-f10", RestartLanguageServer, Some("Editor")),
]);
cx.add_action(Editor::open_new);
@@ -377,6 +379,7 @@ pub fn init(cx: &mut MutableAppContext) {
cx.add_action(Editor::show_completions);
cx.add_action(Editor::toggle_code_actions);
cx.add_action(Editor::open_excerpts);
cx.add_action(Editor::restart_language_server);
cx.add_async_action(Editor::confirm_completion);
cx.add_async_action(Editor::confirm_code_action);
cx.add_async_action(Editor::rename);
@@ -4867,6 +4870,16 @@ impl Editor {
self.pending_rename.as_ref()
}
fn restart_language_server(&mut self, _: &RestartLanguageServer, cx: &mut ViewContext<Self>) {
if let Some(project) = self.project.clone() {
self.buffer.update(cx, |multi_buffer, cx| {
project.update(cx, |project, cx| {
project.restart_language_servers_for_buffers(multi_buffer.all_buffers(), cx);
});
})
}
}
fn refresh_active_diagnostics(&mut self, cx: &mut ViewContext<Editor>) {
if let Some(active_diagnostics) = self.active_diagnostics.as_mut() {
let buffer = self.buffer.read(cx).snapshot(cx);
+27
View File
@@ -2264,6 +2264,33 @@ impl MultiBufferSnapshot {
))
}
pub fn symbols_containing<T: ToOffset>(
&self,
offset: T,
theme: Option<&SyntaxTheme>,
) -> Option<(BufferSnapshot, Vec<OutlineItem<Anchor>>)> {
let anchor = self.anchor_before(offset);
let excerpt_id = anchor.excerpt_id();
let excerpt = self.excerpt(excerpt_id)?;
Some((
excerpt.buffer.clone(),
excerpt
.buffer
.symbols_containing(anchor.text_anchor, theme)
.into_iter()
.flatten()
.map(|item| OutlineItem {
depth: item.depth,
range: self.anchor_in_excerpt(excerpt_id.clone(), item.range.start)
..self.anchor_in_excerpt(excerpt_id.clone(), item.range.end),
text: item.text,
highlight_ranges: item.highlight_ranges,
name_ranges: item.name_ranges,
})
.collect(),
))
}
fn excerpt<'a>(&'a self, excerpt_id: &'a ExcerptId) -> Option<&'a Excerpt> {
let mut cursor = self.excerpts.cursor::<Option<&ExcerptId>>();
cursor.seek(&Some(excerpt_id), Bias::Left, &());
+18 -18
View File
@@ -78,7 +78,11 @@ impl View for FileFinder {
.with_style(settings.theme.selector.input_editor.container)
.boxed(),
)
.with_child(Flexible::new(1.0, false, self.render_matches(cx)).boxed())
.with_child(
FlexItem::new(self.render_matches(cx))
.flex(1., false)
.boxed(),
)
.boxed(),
)
.with_style(settings.theme.selector.container)
@@ -166,23 +170,19 @@ impl FileFinder {
// .boxed(),
// )
.with_child(
Flexible::new(
1.0,
false,
Flex::column()
.with_child(
Label::new(file_name.to_string(), style.label.clone())
.with_highlights(file_name_positions)
.boxed(),
)
.with_child(
Label::new(full_path, style.label.clone())
.with_highlights(full_path_positions)
.boxed(),
)
.boxed(),
)
.boxed(),
Flex::column()
.with_child(
Label::new(file_name.to_string(), style.label.clone())
.with_highlights(file_name_positions)
.boxed(),
)
.with_child(
Label::new(full_path, style.label.clone())
.with_highlights(full_path_positions)
.boxed(),
)
.flex(1., false)
.boxed(),
)
.boxed(),
)
+4
View File
@@ -41,6 +41,10 @@ impl Color {
Self(ColorU::from_u32(0x0000ffff))
}
pub fn yellow() -> Self {
Self(ColorU::from_u32(0x00ffffff))
}
pub fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
Self(ColorU::new(r, g, b, a))
}
+9 -2
View File
@@ -139,11 +139,18 @@ pub trait Element {
Expanded::new(self.boxed())
}
fn flexible(self, flex: f32, expanded: bool) -> Flexible
fn flex(self, flex: f32, expanded: bool) -> FlexItem
where
Self: 'static + Sized,
{
Flexible::new(flex, expanded, self.boxed())
FlexItem::new(self.boxed()).flex(flex, expanded)
}
fn flex_float(self) -> FlexItem
where
Self: 'static + Sized,
{
FlexItem::new(self.boxed()).float()
}
}
+72 -46
View File
@@ -34,7 +34,7 @@ impl Flex {
fn layout_flex_children(
&mut self,
expanded: bool,
layout_expanded: bool,
constraint: SizeConstraint,
remaining_space: &mut f32,
remaining_flex: &mut f32,
@@ -44,32 +44,33 @@ impl Flex {
let cross_axis = self.axis.invert();
for child in &mut self.children {
if let Some(metadata) = child.metadata::<FlexParentData>() {
if metadata.expanded != expanded {
continue;
}
if let Some((flex, expanded)) = metadata.flex {
if expanded != layout_expanded {
continue;
}
let flex = metadata.flex;
let child_max = if *remaining_flex == 0.0 {
*remaining_space
} else {
let space_per_flex = *remaining_space / *remaining_flex;
space_per_flex * flex
};
let child_min = if expanded { child_max } else { 0. };
let child_constraint = match self.axis {
Axis::Horizontal => SizeConstraint::new(
vec2f(child_min, constraint.min.y()),
vec2f(child_max, constraint.max.y()),
),
Axis::Vertical => SizeConstraint::new(
vec2f(constraint.min.x(), child_min),
vec2f(constraint.max.x(), child_max),
),
};
let child_size = child.layout(child_constraint, cx);
*remaining_space -= child_size.along(self.axis);
*remaining_flex -= flex;
*cross_axis_max = cross_axis_max.max(child_size.along(cross_axis));
let child_max = if *remaining_flex == 0.0 {
*remaining_space
} else {
let space_per_flex = *remaining_space / *remaining_flex;
space_per_flex * flex
};
let child_min = if expanded { child_max } else { 0. };
let child_constraint = match self.axis {
Axis::Horizontal => SizeConstraint::new(
vec2f(child_min, constraint.min.y()),
vec2f(child_max, constraint.max.y()),
),
Axis::Vertical => SizeConstraint::new(
vec2f(constraint.min.x(), child_min),
vec2f(constraint.max.x(), child_max),
),
};
let child_size = child.layout(child_constraint, cx);
*remaining_space -= child_size.along(self.axis);
*remaining_flex -= flex;
*cross_axis_max = cross_axis_max.max(child_size.along(cross_axis));
}
}
}
}
@@ -82,7 +83,7 @@ impl Extend<ElementBox> for Flex {
}
impl Element for Flex {
type LayoutState = bool;
type LayoutState = f32;
type PaintState = ();
fn layout(
@@ -96,8 +97,11 @@ impl Element for Flex {
let cross_axis = self.axis.invert();
let mut cross_axis_max: f32 = 0.0;
for child in &mut self.children {
if let Some(metadata) = child.metadata::<FlexParentData>() {
*total_flex.get_or_insert(0.) += metadata.flex;
if let Some(flex) = child
.metadata::<FlexParentData>()
.and_then(|metadata| metadata.flex.map(|(flex, _)| flex))
{
*total_flex.get_or_insert(0.) += flex;
} else {
let child_constraint = match self.axis {
Axis::Horizontal => SizeConstraint::new(
@@ -115,12 +119,12 @@ impl Element for Flex {
}
}
let mut remaining_space = constraint.max_along(self.axis) - fixed_space;
let mut size = if let Some(mut remaining_flex) = total_flex {
if constraint.max_along(self.axis).is_infinite() {
if remaining_space.is_infinite() {
panic!("flex contains flexible children but has an infinite constraint along the flex axis");
}
let mut remaining_space = constraint.max_along(self.axis) - fixed_space;
self.layout_flex_children(
false,
constraint,
@@ -156,38 +160,47 @@ impl Element for Flex {
size.set_y(size.y().max(constraint.min.y()));
}
let mut overflowing = false;
if size.x() > constraint.max.x() {
size.set_x(constraint.max.x());
overflowing = true;
}
if size.y() > constraint.max.y() {
size.set_y(constraint.max.y());
overflowing = true;
}
(size, overflowing)
(size, remaining_space)
}
fn paint(
&mut self,
bounds: RectF,
visible_bounds: RectF,
overflowing: &mut Self::LayoutState,
remaining_space: &mut Self::LayoutState,
cx: &mut PaintContext,
) -> Self::PaintState {
if *overflowing {
let overflowing = *remaining_space < 0.;
if overflowing {
cx.scene.push_layer(Some(bounds));
}
let mut child_origin = bounds.origin();
for child in &mut self.children {
if *remaining_space > 0. {
if let Some(metadata) = child.metadata::<FlexParentData>() {
if metadata.float {
match self.axis {
Axis::Horizontal => child_origin += vec2f(*remaining_space, 0.0),
Axis::Vertical => child_origin += vec2f(0.0, *remaining_space),
}
*remaining_space = 0.;
}
}
}
child.paint(child_origin, visible_bounds, cx);
match self.axis {
Axis::Horizontal => child_origin += vec2f(child.size().x(), 0.0),
Axis::Vertical => child_origin += vec2f(0.0, child.size().y()),
}
}
if *overflowing {
if overflowing {
cx.scene.pop_layer();
}
}
@@ -224,25 +237,38 @@ impl Element for Flex {
}
struct FlexParentData {
flex: f32,
expanded: bool,
flex: Option<(f32, bool)>,
float: bool,
}
pub struct Flexible {
pub struct FlexItem {
metadata: FlexParentData,
child: ElementBox,
}
impl Flexible {
pub fn new(flex: f32, expanded: bool, child: ElementBox) -> Self {
Flexible {
metadata: FlexParentData { flex, expanded },
impl FlexItem {
pub fn new(child: ElementBox) -> Self {
FlexItem {
metadata: FlexParentData {
flex: None,
float: false,
},
child,
}
}
pub fn flex(mut self, flex: f32, expanded: bool) -> Self {
self.metadata.flex = Some((flex, expanded));
self
}
pub fn float(mut self) -> Self {
self.metadata.float = true;
self
}
}
impl Element for Flexible {
impl Element for FlexItem {
type LayoutState = ();
type PaintState = ();
+28 -6
View File
@@ -1674,6 +1674,32 @@ impl BufferSnapshot {
}
pub fn outline(&self, theme: Option<&SyntaxTheme>) -> Option<Outline<Anchor>> {
self.outline_items_containing(0..self.len(), theme)
.map(Outline::new)
}
pub fn symbols_containing<T: ToOffset>(
&self,
position: T,
theme: Option<&SyntaxTheme>,
) -> Option<Vec<OutlineItem<Anchor>>> {
let position = position.to_offset(&self);
let mut items =
self.outline_items_containing(position.saturating_sub(1)..position + 1, theme)?;
let mut prev_depth = None;
items.retain(|item| {
let result = prev_depth.map_or(true, |prev_depth| item.depth > prev_depth);
prev_depth = Some(item.depth);
result
});
Some(items)
}
fn outline_items_containing(
&self,
range: Range<usize>,
theme: Option<&SyntaxTheme>,
) -> Option<Vec<OutlineItem<Anchor>>> {
let tree = self.tree.as_ref()?;
let grammar = self
.language
@@ -1681,6 +1707,7 @@ impl BufferSnapshot {
.and_then(|language| language.grammar.as_ref())?;
let mut cursor = QueryCursorHandle::new();
cursor.set_byte_range(range);
let matches = cursor.matches(
&grammar.outline_query,
tree.root_node(),
@@ -1773,12 +1800,7 @@ impl BufferSnapshot {
})
})
.collect::<Vec<_>>();
if items.is_empty() {
None
} else {
Some(Outline::new(items))
}
Some(items)
}
pub fn enclosing_bracket_ranges<T: ToOffset>(
+2
View File
@@ -245,6 +245,7 @@ impl LanguageRegistry {
pub fn start_language_server(
&self,
server_id: usize,
language: Arc<Language>,
root_path: Arc<Path>,
http_client: Arc<dyn HttpClient>,
@@ -324,6 +325,7 @@ impl LanguageRegistry {
let server_binary_path = server_binary_path.await?;
let server_args = adapter.server_args();
let server = lsp::LanguageServer::new(
server_id,
&server_binary_path,
server_args,
&root_path,
+1 -1
View File
@@ -10,7 +10,7 @@ pub struct Outline<T> {
path_candidate_prefixes: Vec<usize>,
}
#[derive(Clone, Debug)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct OutlineItem<T> {
pub depth: usize,
pub range: Range<T>,
+115 -31
View File
@@ -282,36 +282,6 @@ async fn test_reparse(cx: &mut gpui::TestAppContext) {
#[gpui::test]
async fn test_outline(cx: &mut gpui::TestAppContext) {
let language = Arc::new(
rust_lang()
.with_outline_query(
r#"
(struct_item
"struct" @context
name: (_) @name) @item
(enum_item
"enum" @context
name: (_) @name) @item
(enum_variant
name: (_) @name) @item
(field_declaration
name: (_) @name) @item
(impl_item
"impl" @context
trait: (_) @name
"for" @context
type: (_) @name) @item
(function_item
"fn" @context
name: (_) @name) @item
(mod_item
"mod" @context
name: (_) @name) @item
"#,
)
.unwrap(),
);
let text = r#"
struct Person {
name: String,
@@ -339,7 +309,8 @@ async fn test_outline(cx: &mut gpui::TestAppContext) {
"#
.unindent();
let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx));
let buffer =
cx.add_model(|cx| Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx));
let outline = buffer
.read_with(cx, |buffer, _| buffer.snapshot().outline(None))
.unwrap();
@@ -413,6 +384,93 @@ async fn test_outline(cx: &mut gpui::TestAppContext) {
}
}
#[gpui::test]
async fn test_symbols_containing(cx: &mut gpui::TestAppContext) {
let text = r#"
impl Person {
fn one() {
1
}
fn two() {
2
}fn three() {
3
}
}
"#
.unindent();
let buffer =
cx.add_model(|cx| Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx));
let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
// point is at the start of an item
assert_eq!(
symbols_containing(Point::new(1, 4), &snapshot),
vec![
(
"impl Person".to_string(),
Point::new(0, 0)..Point::new(10, 1)
),
("fn one".to_string(), Point::new(1, 4)..Point::new(3, 5))
]
);
// point is in the middle of an item
assert_eq!(
symbols_containing(Point::new(2, 8), &snapshot),
vec![
(
"impl Person".to_string(),
Point::new(0, 0)..Point::new(10, 1)
),
("fn one".to_string(), Point::new(1, 4)..Point::new(3, 5))
]
);
// point is at the end of an item
assert_eq!(
symbols_containing(Point::new(3, 5), &snapshot),
vec![
(
"impl Person".to_string(),
Point::new(0, 0)..Point::new(10, 1)
),
("fn one".to_string(), Point::new(1, 4)..Point::new(3, 5))
]
);
// point is in between two adjacent items
assert_eq!(
symbols_containing(Point::new(7, 5), &snapshot),
vec![
(
"impl Person".to_string(),
Point::new(0, 0)..Point::new(10, 1)
),
("fn two".to_string(), Point::new(5, 4)..Point::new(7, 5))
]
);
fn symbols_containing<'a>(
position: Point,
snapshot: &'a BufferSnapshot,
) -> Vec<(String, Range<Point>)> {
snapshot
.symbols_containing(position, None)
.unwrap()
.into_iter()
.map(|item| {
(
item.text,
item.range.start.to_point(snapshot)..item.range.end.to_point(snapshot),
)
})
.collect()
}
}
#[gpui::test]
fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) {
let buffer = cx.add_model(|cx| {
@@ -889,6 +947,32 @@ fn rust_lang() -> Language {
"#,
)
.unwrap()
.with_outline_query(
r#"
(struct_item
"struct" @context
name: (_) @name) @item
(enum_item
"enum" @context
name: (_) @name) @item
(enum_variant
name: (_) @name) @item
(field_declaration
name: (_) @name) @item
(impl_item
"impl" @context
trait: (_)? @name
"for"? @context
type: (_) @name) @item
(function_item
"fn" @context
name: (_) @name) @item
(mod_item
"mod" @context
name: (_) @name) @item
"#,
)
.unwrap()
}
fn empty(point: Point) -> Range<Point> {
+23 -12
View File
@@ -34,6 +34,7 @@ type NotificationHandler =
type ResponseHandler = Box<dyn Send + FnOnce(Result<&str, Error>)>;
pub struct LanguageServer {
server_id: usize,
next_id: AtomicUsize,
outbound_tx: channel::Sender<Vec<u8>>,
name: String,
@@ -113,6 +114,7 @@ struct Error {
impl LanguageServer {
pub fn new(
server_id: usize,
binary_path: &Path,
args: &[&str],
root_path: &Path,
@@ -133,7 +135,8 @@ impl LanguageServer {
.spawn()?;
let stdin = server.stdin.take().unwrap();
let stdout = server.stdout.take().unwrap();
let mut server = Self::new_internal(stdin, stdout, root_path, options, background);
let mut server =
Self::new_internal(server_id, stdin, stdout, root_path, options, background);
if let Some(name) = binary_path.file_name() {
server.name = name.to_string_lossy().to_string();
}
@@ -141,6 +144,7 @@ impl LanguageServer {
}
fn new_internal<Stdin, Stdout>(
server_id: usize,
stdin: Stdin,
stdout: Stdout,
root_path: &Path,
@@ -240,6 +244,7 @@ impl LanguageServer {
});
Self {
server_id,
notification_handlers,
response_handlers,
name: Default::default(),
@@ -446,6 +451,10 @@ impl LanguageServer {
&self.capabilities
}
pub fn server_id(&self) -> usize {
self.server_id
}
pub fn request<T: request::Request>(
self: &Arc<Self>,
params: T::Params,
@@ -606,8 +615,14 @@ impl LanguageServer {
});
let executor = cx.background().clone();
let server =
Self::new_internal(stdin_writer, stdout_reader, Path::new("/"), None, executor);
let server = Self::new_internal(
0,
stdin_writer,
stdout_reader,
Path::new("/"),
None,
executor,
);
(server, fake)
}
}
@@ -666,17 +681,13 @@ impl FakeLanguageServer {
let output_task = cx.background().spawn(async move {
let mut stdout = smol::io::BufWriter::new(stdout);
while let Some(message) = outgoing_rx.next().await {
stdout
.write_all(CONTENT_LEN_HEADER.as_bytes())
.await
.unwrap();
stdout.write_all(CONTENT_LEN_HEADER.as_bytes()).await?;
stdout
.write_all((format!("{}", message.len())).as_bytes())
.await
.unwrap();
stdout.write_all("\r\n\r\n".as_bytes()).await.unwrap();
stdout.write_all(&message).await.unwrap();
stdout.flush().await.unwrap();
.await?;
stdout.write_all("\r\n\r\n".as_bytes()).await?;
stdout.write_all(&message).await?;
stdout.flush().await?;
}
Ok(())
});
+5 -1
View File
@@ -77,7 +77,11 @@ impl View for OutlineView {
.with_style(settings.theme.selector.input_editor.container)
.boxed(),
)
.with_child(Flexible::new(1.0, false, self.render_matches(cx)).boxed())
.with_child(
FlexItem::new(self.render_matches(cx))
.flex(1.0, false)
.boxed(),
)
.contained()
.with_style(settings.theme.selector.container)
.constrained()
+116 -4
View File
@@ -1308,6 +1308,7 @@ impl Project {
.or_insert_with(|| {
let server_id = post_inc(&mut self.next_language_server_id);
let language_server = self.languages.start_language_server(
server_id,
language.clone(),
worktree_path,
self.client.http_client(),
@@ -1507,6 +1508,59 @@ impl Project {
});
}
pub fn restart_language_servers_for_buffers(
&mut self,
buffers: impl IntoIterator<Item = ModelHandle<Buffer>>,
cx: &mut ModelContext<Self>,
) -> Option<()> {
let language_server_lookup_info: HashSet<(WorktreeId, Arc<Path>, PathBuf)> = buffers
.into_iter()
.filter_map(|buffer| {
let file = File::from_dyn(buffer.read(cx).file())?;
let worktree = file.worktree.read(cx).as_local()?;
let worktree_id = worktree.id();
let worktree_abs_path = worktree.abs_path().clone();
let full_path = file.full_path(cx);
Some((worktree_id, worktree_abs_path, full_path))
})
.collect();
for (worktree_id, worktree_abs_path, full_path) in language_server_lookup_info {
let language = self.languages.select_language(&full_path)?;
self.restart_language_server(worktree_id, worktree_abs_path, language, cx);
}
None
}
fn restart_language_server(
&mut self,
worktree_id: WorktreeId,
worktree_path: Arc<Path>,
language: Arc<Language>,
cx: &mut ModelContext<Self>,
) {
let key = (worktree_id, language.name());
let server_to_shutdown = self.language_servers.remove(&key);
self.started_language_servers.remove(&key);
server_to_shutdown
.as_ref()
.map(|server| self.language_server_statuses.remove(&server.server_id()));
cx.spawn_weak(|this, mut cx| async move {
if let Some(this) = this.upgrade(&cx) {
if let Some(server_to_shutdown) = server_to_shutdown {
if let Some(shutdown_task) = server_to_shutdown.shutdown() {
shutdown_task.await;
}
}
this.update(&mut cx, |this, cx| {
this.start_language_server(worktree_id, worktree_path, language, cx);
});
}
})
.detach();
}
fn on_lsp_event(
&mut self,
language_server_id: usize,
@@ -4596,7 +4650,7 @@ impl Item for Buffer {
mod tests {
use super::{Event, *};
use fs::RealFs;
use futures::StreamExt;
use futures::{future, StreamExt};
use gpui::test::subscribe;
use language::{
tree_sitter_rust, Diagnostic, LanguageConfig, LanguageServerConfig, OffsetRangeExt, Point,
@@ -4606,7 +4660,7 @@ mod tests {
use serde_json::json;
use std::{cell::RefCell, os::unix, path::PathBuf, rc::Rc};
use unindent::Unindent as _;
use util::test::temp_tree;
use util::{assert_set_eq, test::temp_tree};
use worktree::WorktreeHandle as _;
#[gpui::test]
@@ -4802,8 +4856,7 @@ mod tests {
.await
.unwrap();
// Another language server is started up, and it is notified about
// all three open buffers.
// A json language server is started up and is only notified about the json buffer.
let mut fake_json_server = fake_json_servers.next().await.unwrap();
assert_eq!(
fake_json_server
@@ -4877,6 +4930,65 @@ mod tests {
)
);
// Restart language servers
project.update(cx, |project, cx| {
project.restart_language_servers_for_buffers(
vec![rust_buffer.clone(), json_buffer.clone()],
cx,
);
});
let mut rust_shutdown_requests = fake_rust_server
.handle_request::<lsp::request::Shutdown, _, _>(|_, _| future::ready(()));
let mut json_shutdown_requests = fake_json_server
.handle_request::<lsp::request::Shutdown, _, _>(|_, _| future::ready(()));
futures::join!(rust_shutdown_requests.next(), json_shutdown_requests.next());
let mut fake_rust_server = fake_rust_servers.next().await.unwrap();
let mut fake_json_server = fake_json_servers.next().await.unwrap();
// Ensure both rust documents are reopened in new rust language server without worrying about order
assert_set_eq!(
[
fake_rust_server
.receive_notification::<lsp::notification::DidOpenTextDocument>()
.await
.text_document,
fake_rust_server
.receive_notification::<lsp::notification::DidOpenTextDocument>()
.await
.text_document,
],
[
lsp::TextDocumentItem {
uri: lsp::Url::from_file_path("/the-root/test.rs").unwrap(),
version: 1,
text: rust_buffer.read_with(cx, |buffer, _| buffer.text()),
language_id: Default::default()
},
lsp::TextDocumentItem {
uri: lsp::Url::from_file_path("/the-root/test2.rs").unwrap(),
version: 1,
text: rust_buffer2.read_with(cx, |buffer, _| buffer.text()),
language_id: Default::default()
},
]
);
// Ensure json document is reopened in new json language server
assert_eq!(
fake_json_server
.receive_notification::<lsp::notification::DidOpenTextDocument>()
.await
.text_document,
lsp::TextDocumentItem {
uri: lsp::Url::from_file_path("/the-root/package.json").unwrap(),
version: 0,
text: json_buffer.read_with(cx, |buffer, _| buffer.text()),
language_id: Default::default()
}
);
// Close notifications are reported only to servers matching the buffer's language.
cx.update(|_| drop(json_buffer));
let close_message = lsp::DidCloseTextDocumentParams {
@@ -76,7 +76,11 @@ impl View for ProjectSymbolsView {
.with_style(settings.theme.selector.input_editor.container)
.boxed(),
)
.with_child(Flexible::new(1.0, false, self.render_matches(cx)).boxed())
.with_child(
FlexItem::new(self.render_matches(cx))
.flex(1., false)
.boxed(),
)
.contained()
.with_style(settings.theme.selector.container)
.constrained()
+157 -117
View File
@@ -2,43 +2,52 @@ use crate::{active_match_index, match_index_for_direction, Direction, SearchOpti
use collections::HashMap;
use editor::{display_map::ToDisplayPoint, Anchor, Autoscroll, Bias, Editor};
use gpui::{
action, elements::*, keymap::Binding, platform::CursorStyle, Entity, MutableAppContext,
RenderContext, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
action, elements::*, keymap::Binding, platform::CursorStyle, AppContext, Entity,
MutableAppContext, RenderContext, Subscription, Task, View, ViewContext, ViewHandle,
WeakViewHandle,
};
use language::OffsetRangeExt;
use project::search::SearchQuery;
use std::ops::Range;
use workspace::{ItemHandle, Pane, Settings, Toolbar, Workspace};
use workspace::{ItemHandle, Pane, Settings, ToolbarItemLocation, ToolbarItemView};
action!(Deploy, bool);
action!(Dismiss);
action!(FocusEditor);
action!(ToggleSearchOption, SearchOption);
pub enum Event {
UpdateLocation,
}
pub fn init(cx: &mut MutableAppContext) {
cx.add_bindings([
Binding::new("cmd-f", Deploy(true), Some("Editor && mode == full")),
Binding::new("cmd-e", Deploy(false), Some("Editor && mode == full")),
Binding::new("escape", Dismiss, Some("SearchBar")),
Binding::new("cmd-f", FocusEditor, Some("SearchBar")),
Binding::new("enter", SelectMatch(Direction::Next), Some("SearchBar")),
Binding::new("escape", Dismiss, Some("BufferSearchBar")),
Binding::new("cmd-f", FocusEditor, Some("BufferSearchBar")),
Binding::new(
"enter",
SelectMatch(Direction::Next),
Some("BufferSearchBar"),
),
Binding::new(
"shift-enter",
SelectMatch(Direction::Prev),
Some("SearchBar"),
Some("BufferSearchBar"),
),
Binding::new("cmd-g", SelectMatch(Direction::Next), Some("Pane")),
Binding::new("cmd-shift-G", SelectMatch(Direction::Prev), Some("Pane")),
]);
cx.add_action(SearchBar::deploy);
cx.add_action(SearchBar::dismiss);
cx.add_action(SearchBar::focus_editor);
cx.add_action(SearchBar::toggle_search_option);
cx.add_action(SearchBar::select_match);
cx.add_action(SearchBar::select_match_on_pane);
cx.add_action(BufferSearchBar::deploy);
cx.add_action(BufferSearchBar::dismiss);
cx.add_action(BufferSearchBar::focus_editor);
cx.add_action(BufferSearchBar::toggle_search_option);
cx.add_action(BufferSearchBar::select_match);
cx.add_action(BufferSearchBar::select_match_on_pane);
}
struct SearchBar {
pub struct BufferSearchBar {
query_editor: ViewHandle<Editor>,
active_editor: Option<ViewHandle<Editor>>,
active_match_index: Option<usize>,
@@ -52,13 +61,13 @@ struct SearchBar {
dismissed: bool,
}
impl Entity for SearchBar {
type Event = ();
impl Entity for BufferSearchBar {
type Event = Event;
}
impl View for SearchBar {
impl View for BufferSearchBar {
fn ui_name() -> &'static str {
"SearchBar"
"BufferSearchBar"
}
fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
@@ -74,12 +83,44 @@ impl View for SearchBar {
};
Flex::row()
.with_child(
ChildView::new(&self.query_editor)
Flex::row()
.with_child(
ChildView::new(&self.query_editor)
.aligned()
.left()
.flex(1., true)
.boxed(),
)
.with_children(self.active_editor.as_ref().and_then(|editor| {
let matches = self.editors_with_matches.get(&editor.downgrade())?;
let message = if let Some(match_ix) = self.active_match_index {
format!("{}/{}", match_ix + 1, matches.len())
} else {
"No matches".to_string()
};
Some(
Label::new(message, theme.search.match_index.text.clone())
.contained()
.with_style(theme.search.match_index.container)
.aligned()
.boxed(),
)
}))
.contained()
.with_style(editor_container)
.aligned()
.constrained()
.with_min_width(theme.search.editor.min_width)
.with_max_width(theme.search.editor.max_width)
.flex(1., false)
.boxed(),
)
.with_child(
Flex::row()
.with_child(self.render_nav_button("<", Direction::Prev, cx))
.with_child(self.render_nav_button(">", Direction::Next, cx))
.aligned()
.boxed(),
)
.with_child(
@@ -92,43 +133,19 @@ impl View for SearchBar {
.aligned()
.boxed(),
)
.with_child(
Flex::row()
.with_child(self.render_nav_button("<", Direction::Prev, cx))
.with_child(self.render_nav_button(">", Direction::Next, cx))
.aligned()
.boxed(),
)
.with_children(self.active_editor.as_ref().and_then(|editor| {
let matches = self.editors_with_matches.get(&editor.downgrade())?;
let message = if let Some(match_ix) = self.active_match_index {
format!("{}/{}", match_ix + 1, matches.len())
} else {
"No matches".to_string()
};
Some(
Label::new(message, theme.search.match_index.text.clone())
.contained()
.with_style(theme.search.match_index.container)
.aligned()
.boxed(),
)
}))
.contained()
.with_style(theme.search.container)
.constrained()
.with_height(theme.workspace.toolbar.height)
.named("search bar")
}
}
impl Toolbar for SearchBar {
fn active_item_changed(
impl ToolbarItemView for BufferSearchBar {
fn set_active_pane_item(
&mut self,
item: Option<Box<dyn ItemHandle>>,
item: Option<&dyn ItemHandle>,
cx: &mut ViewContext<Self>,
) -> bool {
) -> ToolbarItemLocation {
cx.notify();
self.active_editor_subscription.take();
self.active_editor.take();
self.pending_search.take();
@@ -139,26 +156,31 @@ impl Toolbar for SearchBar {
Some(cx.subscribe(&editor, Self::on_active_editor_event));
self.active_editor = Some(editor);
self.update_matches(false, cx);
return true;
if !self.dismissed {
return ToolbarItemLocation::Secondary;
}
}
}
false
ToolbarItemLocation::Hidden
}
fn on_dismiss(&mut self, cx: &mut ViewContext<Self>) {
self.dismissed = true;
for (editor, _) in &self.editors_with_matches {
if let Some(editor) = editor.upgrade(cx) {
editor.update(cx, |editor, cx| {
editor.clear_background_highlights::<Self>(cx)
});
}
fn location_for_event(
&self,
_: &Self::Event,
_: ToolbarItemLocation,
_: &AppContext,
) -> ToolbarItemLocation {
if self.active_editor.is_some() && !self.dismissed {
ToolbarItemLocation::Secondary
} else {
ToolbarItemLocation::Hidden
}
}
}
impl SearchBar {
fn new(cx: &mut ViewContext<Self>) -> Self {
impl BufferSearchBar {
pub fn new(cx: &mut ViewContext<Self>) -> Self {
let query_editor = cx.add_view(|cx| {
Editor::auto_height(2, Some(|theme| theme.search.editor.input.clone()), cx)
});
@@ -176,10 +198,75 @@ impl SearchBar {
regex: false,
pending_search: None,
query_contains_error: false,
dismissed: false,
dismissed: true,
}
}
fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext<Self>) {
self.dismissed = true;
for (editor, _) in &self.editors_with_matches {
if let Some(editor) = editor.upgrade(cx) {
editor.update(cx, |editor, cx| {
editor.clear_background_highlights::<Self>(cx)
});
}
}
if let Some(active_editor) = self.active_editor.as_ref() {
cx.focus(active_editor);
}
cx.emit(Event::UpdateLocation);
cx.notify();
}
fn show(&mut self, focus: bool, cx: &mut ViewContext<Self>) -> bool {
let editor = if let Some(editor) = self.active_editor.clone() {
editor
} else {
return false;
};
let display_map = editor
.update(cx, |editor, cx| editor.snapshot(cx))
.display_snapshot;
let selection = editor
.read(cx)
.newest_selection_with_snapshot::<usize>(&display_map.buffer_snapshot);
let mut text: String;
if selection.start == selection.end {
let point = selection.start.to_display_point(&display_map);
let range = editor::movement::surrounding_word(&display_map, point);
let range = range.start.to_offset(&display_map, Bias::Left)
..range.end.to_offset(&display_map, Bias::Right);
text = display_map.buffer_snapshot.text_for_range(range).collect();
if text.trim().is_empty() {
text = String::new();
}
} else {
text = display_map
.buffer_snapshot
.text_for_range(selection.start..selection.end)
.collect();
}
if !text.is_empty() {
self.set_query(&text, cx);
}
if focus {
let query_editor = self.query_editor.clone();
query_editor.update(cx, |query_editor, cx| {
query_editor.select_all(&editor::SelectAll, cx);
});
cx.focus_self();
}
self.dismissed = false;
cx.notify();
cx.emit(Event::UpdateLocation);
true
}
fn set_query(&mut self, query: &str, cx: &mut ViewContext<Self>) {
self.query_editor.update(cx, |query_editor, cx| {
query_editor.buffer().update(cx, |query_buffer, cx| {
@@ -238,61 +325,13 @@ impl SearchBar {
.boxed()
}
fn deploy(workspace: &mut Workspace, Deploy(focus): &Deploy, cx: &mut ViewContext<Workspace>) {
workspace.active_pane().update(cx, |pane, cx| {
pane.show_toolbar(cx, |cx| SearchBar::new(cx));
if let Some(search_bar) = pane
.active_toolbar()
.and_then(|toolbar| toolbar.downcast::<Self>())
{
search_bar.update(cx, |search_bar, _| search_bar.dismissed = false);
let editor = pane.active_item().unwrap().act_as::<Editor>(cx).unwrap();
let display_map = editor
.update(cx, |editor, cx| editor.snapshot(cx))
.display_snapshot;
let selection = editor
.read(cx)
.newest_selection_with_snapshot::<usize>(&display_map.buffer_snapshot);
let mut text: String;
if selection.start == selection.end {
let point = selection.start.to_display_point(&display_map);
let range = editor::movement::surrounding_word(&display_map, point);
let range = range.start.to_offset(&display_map, Bias::Left)
..range.end.to_offset(&display_map, Bias::Right);
text = display_map.buffer_snapshot.text_for_range(range).collect();
if text.trim().is_empty() {
text = String::new();
}
} else {
text = display_map
.buffer_snapshot
.text_for_range(selection.start..selection.end)
.collect();
}
if !text.is_empty() {
search_bar.update(cx, |search_bar, cx| search_bar.set_query(&text, cx));
}
if *focus {
let query_editor = search_bar.read(cx).query_editor.clone();
query_editor.update(cx, |query_editor, cx| {
query_editor.select_all(&editor::SelectAll, cx);
});
cx.focus(&search_bar);
}
} else {
cx.propagate_action();
fn deploy(pane: &mut Pane, Deploy(focus): &Deploy, cx: &mut ViewContext<Pane>) {
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
if search_bar.update(cx, |search_bar, cx| search_bar.show(*focus, cx)) {
return;
}
});
}
fn dismiss(pane: &mut Pane, _: &Dismiss, cx: &mut ViewContext<Pane>) {
if pane.toolbar::<SearchBar>().is_some() {
pane.dismiss_toolbar(cx);
}
cx.propagate_action();
}
fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext<Self>) {
@@ -346,7 +385,7 @@ impl SearchBar {
}
fn select_match_on_pane(pane: &mut Pane, action: &SelectMatch, cx: &mut ViewContext<Pane>) {
if let Some(search_bar) = pane.toolbar::<SearchBar>() {
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
search_bar.update(cx, |search_bar, cx| search_bar.select_match(action, cx));
}
}
@@ -540,8 +579,9 @@ mod tests {
});
let search_bar = cx.add_view(Default::default(), |cx| {
let mut search_bar = SearchBar::new(cx);
search_bar.active_item_changed(Some(Box::new(editor.clone())), cx);
let mut search_bar = BufferSearchBar::new(cx);
search_bar.set_active_pane_item(Some(&editor), cx);
search_bar.show(false, cx);
search_bar
});
+293 -194
View File
@@ -6,8 +6,8 @@ use collections::HashMap;
use editor::{Anchor, Autoscroll, Editor, MultiBuffer, SelectAll};
use gpui::{
action, elements::*, keymap::Binding, platform::CursorStyle, AppContext, ElementBox, Entity,
ModelContext, ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext,
ViewHandle, WeakModelHandle, WeakViewHandle,
ModelContext, ModelHandle, MutableAppContext, RenderContext, Subscription, Task, View,
ViewContext, ViewHandle, WeakModelHandle, WeakViewHandle,
};
use project::{search::SearchQuery, Project};
use std::{
@@ -16,7 +16,9 @@ use std::{
path::PathBuf,
};
use util::ResultExt as _;
use workspace::{Item, ItemNavHistory, Settings, Workspace};
use workspace::{
Item, ItemNavHistory, Pane, Settings, ToolbarItemLocation, ToolbarItemView, Workspace,
};
action!(Deploy);
action!(Search);
@@ -31,29 +33,21 @@ struct ActiveSearches(HashMap<WeakModelHandle<Project>, WeakViewHandle<ProjectSe
pub fn init(cx: &mut MutableAppContext) {
cx.set_global(ActiveSearches::default());
cx.add_bindings([
Binding::new("cmd-shift-F", ToggleFocus, Some("ProjectSearchView")),
Binding::new("cmd-f", ToggleFocus, Some("ProjectSearchView")),
Binding::new("cmd-shift-F", ToggleFocus, Some("Pane")),
Binding::new("cmd-f", ToggleFocus, Some("Pane")),
Binding::new("cmd-shift-F", Deploy, Some("Workspace")),
Binding::new("enter", Search, Some("ProjectSearchView")),
Binding::new("cmd-enter", SearchInNew, Some("ProjectSearchView")),
Binding::new(
"cmd-g",
SelectMatch(Direction::Next),
Some("ProjectSearchView"),
),
Binding::new(
"cmd-shift-G",
SelectMatch(Direction::Prev),
Some("ProjectSearchView"),
),
Binding::new("enter", Search, Some("ProjectSearchBar")),
Binding::new("cmd-enter", SearchInNew, Some("ProjectSearchBar")),
Binding::new("cmd-g", SelectMatch(Direction::Next), Some("Pane")),
Binding::new("cmd-shift-G", SelectMatch(Direction::Prev), Some("Pane")),
]);
cx.add_action(ProjectSearchView::deploy);
cx.add_action(ProjectSearchView::search);
cx.add_action(ProjectSearchView::search_in_new);
cx.add_action(ProjectSearchView::toggle_search_option);
cx.add_action(ProjectSearchView::select_match);
cx.add_action(ProjectSearchView::toggle_focus);
cx.capture_action(ProjectSearchView::tab);
cx.add_action(ProjectSearchBar::search);
cx.add_action(ProjectSearchBar::search_in_new);
cx.add_action(ProjectSearchBar::toggle_search_option);
cx.add_action(ProjectSearchBar::select_match);
cx.add_action(ProjectSearchBar::toggle_focus);
cx.capture_action(ProjectSearchBar::tab);
}
struct ProjectSearch {
@@ -64,7 +58,7 @@ struct ProjectSearch {
active_query: Option<SearchQuery>,
}
struct ProjectSearchView {
pub struct ProjectSearchView {
model: ModelHandle<ProjectSearch>,
query_editor: ViewHandle<Editor>,
results_editor: ViewHandle<Editor>,
@@ -75,6 +69,11 @@ struct ProjectSearchView {
active_match_index: Option<usize>,
}
pub struct ProjectSearchBar {
active_project_search: Option<ViewHandle<ProjectSearchView>>,
subscription: Option<Subscription>,
}
impl Entity for ProjectSearch {
type Event = ();
}
@@ -139,7 +138,7 @@ impl ProjectSearch {
}
}
enum ViewEvent {
pub enum ViewEvent {
UpdateTab,
}
@@ -154,7 +153,7 @@ impl View for ProjectSearchView {
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
let model = &self.model.read(cx);
let results = if model.match_ranges.is_empty() {
if model.match_ranges.is_empty() {
let theme = &cx.global::<Settings>().theme;
let text = if self.query_editor.read(cx).text(cx).is_empty() {
""
@@ -167,18 +166,11 @@ impl View for ProjectSearchView {
.aligned()
.contained()
.with_background_color(theme.editor.background)
.flexible(1., true)
.flex(1., true)
.boxed()
} else {
ChildView::new(&self.results_editor)
.flexible(1., true)
.boxed()
};
Flex::column()
.with_child(self.render_query_editor(cx))
.with_child(results)
.boxed()
ChildView::new(&self.results_editor).flex(1., true).boxed()
}
}
fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
@@ -402,45 +394,12 @@ impl ProjectSearchView {
}
}
fn search(&mut self, _: &Search, cx: &mut ViewContext<Self>) {
fn search(&mut self, cx: &mut ViewContext<Self>) {
if let Some(query) = self.build_search_query(cx) {
self.model.update(cx, |model, cx| model.search(query, cx));
}
}
fn search_in_new(workspace: &mut Workspace, _: &SearchInNew, cx: &mut ViewContext<Workspace>) {
if let Some(search_view) = workspace
.active_item(cx)
.and_then(|item| item.downcast::<ProjectSearchView>())
{
let new_query = search_view.update(cx, |search_view, cx| {
let new_query = search_view.build_search_query(cx);
if new_query.is_some() {
if let Some(old_query) = search_view.model.read(cx).active_query.clone() {
search_view.query_editor.update(cx, |editor, cx| {
editor.set_text(old_query.as_str(), cx);
});
search_view.regex = old_query.is_regex();
search_view.whole_word = old_query.whole_word();
search_view.case_sensitive = old_query.case_sensitive();
}
}
new_query
});
if let Some(new_query) = new_query {
let model = cx.add_model(|cx| {
let mut model = ProjectSearch::new(workspace.project().clone(), cx);
model.search(new_query, cx);
model
});
workspace.add_item(
Box::new(cx.add_view(|cx| ProjectSearchView::new(model, cx))),
cx,
);
}
}
}
fn build_search_query(&mut self, cx: &mut ViewContext<Self>) -> Option<SearchQuery> {
let text = self.query_editor.read(cx).text(cx);
if self.regex {
@@ -461,22 +420,7 @@ impl ProjectSearchView {
}
}
fn toggle_search_option(
&mut self,
ToggleSearchOption(option): &ToggleSearchOption,
cx: &mut ViewContext<Self>,
) {
let value = match option {
SearchOption::WholeWord => &mut self.whole_word,
SearchOption::CaseSensitive => &mut self.case_sensitive,
SearchOption::Regex => &mut self.regex,
};
*value = !*value;
self.search(&Search, cx);
cx.notify();
}
fn select_match(&mut self, &SelectMatch(direction): &SelectMatch, cx: &mut ViewContext<Self>) {
fn select_match(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
if let Some(index) = self.active_match_index {
let model = self.model.read(cx);
let results_editor = self.results_editor.read(cx);
@@ -495,26 +439,6 @@ impl ProjectSearchView {
}
}
fn toggle_focus(&mut self, _: &ToggleFocus, cx: &mut ViewContext<Self>) {
if self.query_editor.is_focused(cx) {
if !self.model.read(cx).match_ranges.is_empty() {
self.focus_results_editor(cx);
}
} else {
self.focus_query_editor(cx);
}
}
fn tab(&mut self, _: &editor::Tab, cx: &mut ViewContext<Self>) {
if self.query_editor.is_focused(cx) {
if !self.model.read(cx).match_ranges.is_empty() {
self.focus_results_editor(cx);
}
} else {
cx.propagate_action()
}
}
fn focus_query_editor(&self, cx: &mut ViewContext<Self>) {
self.query_editor.update(cx, |query_editor, cx| {
query_editor.select_all(&SelectAll, cx);
@@ -564,93 +488,126 @@ impl ProjectSearchView {
}
}
fn render_query_editor(&self, cx: &mut RenderContext<Self>) -> ElementBox {
let theme = cx.global::<Settings>().theme.clone();
let editor_container = if self.query_contains_error {
theme.search.invalid_editor
pub fn has_matches(&self) -> bool {
self.active_match_index.is_some()
}
}
impl ProjectSearchBar {
pub fn new() -> Self {
Self {
active_project_search: Default::default(),
subscription: Default::default(),
}
}
fn search(&mut self, _: &Search, cx: &mut ViewContext<Self>) {
if let Some(search_view) = self.active_project_search.as_ref() {
search_view.update(cx, |search_view, cx| search_view.search(cx));
}
}
fn search_in_new(workspace: &mut Workspace, _: &SearchInNew, cx: &mut ViewContext<Workspace>) {
if let Some(search_view) = workspace
.active_item(cx)
.and_then(|item| item.downcast::<ProjectSearchView>())
{
let new_query = search_view.update(cx, |search_view, cx| {
let new_query = search_view.build_search_query(cx);
if new_query.is_some() {
if let Some(old_query) = search_view.model.read(cx).active_query.clone() {
search_view.query_editor.update(cx, |editor, cx| {
editor.set_text(old_query.as_str(), cx);
});
search_view.regex = old_query.is_regex();
search_view.whole_word = old_query.whole_word();
search_view.case_sensitive = old_query.case_sensitive();
}
}
new_query
});
if let Some(new_query) = new_query {
let model = cx.add_model(|cx| {
let mut model = ProjectSearch::new(workspace.project().clone(), cx);
model.search(new_query, cx);
model
});
workspace.add_item(
Box::new(cx.add_view(|cx| ProjectSearchView::new(model, cx))),
cx,
);
}
}
}
fn select_match(
pane: &mut Pane,
&SelectMatch(direction): &SelectMatch,
cx: &mut ViewContext<Pane>,
) {
if let Some(search_view) = pane
.active_item()
.and_then(|item| item.downcast::<ProjectSearchView>())
{
search_view.update(cx, |search_view, cx| {
search_view.select_match(direction, cx);
});
} else {
theme.search.editor.input.container
};
Flex::row()
.with_child(
ChildView::new(&self.query_editor)
.contained()
.with_style(editor_container)
.aligned()
.constrained()
.with_max_width(theme.search.editor.max_width)
.boxed(),
)
.with_child(
Flex::row()
.with_child(self.render_option_button("Case", SearchOption::CaseSensitive, cx))
.with_child(self.render_option_button("Word", SearchOption::WholeWord, cx))
.with_child(self.render_option_button("Regex", SearchOption::Regex, cx))
.contained()
.with_style(theme.search.option_button_group)
.aligned()
.boxed(),
)
.with_children({
self.active_match_index.into_iter().flat_map(|match_ix| {
[
Flex::row()
.with_child(self.render_nav_button("<", Direction::Prev, cx))
.with_child(self.render_nav_button(">", Direction::Next, cx))
.aligned()
.boxed(),
Label::new(
format!(
"{}/{}",
match_ix + 1,
self.model.read(cx).match_ranges.len()
),
theme.search.match_index.text.clone(),
)
.contained()
.with_style(theme.search.match_index.container)
.aligned()
.boxed(),
]
})
})
.contained()
.with_style(theme.search.container)
.constrained()
.with_height(theme.workspace.toolbar.height)
.named("project search")
cx.propagate_action();
}
}
fn render_option_button(
&self,
icon: &str,
option: SearchOption,
cx: &mut RenderContext<Self>,
) -> ElementBox {
let is_active = self.is_option_enabled(option);
MouseEventHandler::new::<Self, _, _>(option as usize, cx, |state, cx| {
let theme = &cx.global::<Settings>().theme.search;
let style = match (is_active, state.hovered) {
(false, false) => &theme.option_button,
(false, true) => &theme.hovered_option_button,
(true, false) => &theme.active_option_button,
(true, true) => &theme.active_hovered_option_button,
};
Label::new(icon.to_string(), style.text.clone())
.contained()
.with_style(style.container)
.boxed()
})
.on_click(move |cx| cx.dispatch_action(ToggleSearchOption(option)))
.with_cursor_style(CursorStyle::PointingHand)
.boxed()
fn toggle_focus(pane: &mut Pane, _: &ToggleFocus, cx: &mut ViewContext<Pane>) {
if let Some(search_view) = pane
.active_item()
.and_then(|item| item.downcast::<ProjectSearchView>())
{
search_view.update(cx, |search_view, cx| {
if search_view.query_editor.is_focused(cx) {
if !search_view.model.read(cx).match_ranges.is_empty() {
search_view.focus_results_editor(cx);
}
} else {
search_view.focus_query_editor(cx);
}
});
} else {
cx.propagate_action();
}
}
fn is_option_enabled(&self, option: SearchOption) -> bool {
match option {
SearchOption::WholeWord => self.whole_word,
SearchOption::CaseSensitive => self.case_sensitive,
SearchOption::Regex => self.regex,
fn tab(&mut self, _: &editor::Tab, cx: &mut ViewContext<Self>) {
if let Some(search_view) = self.active_project_search.as_ref() {
search_view.update(cx, |search_view, cx| {
if search_view.query_editor.is_focused(cx) {
if !search_view.model.read(cx).match_ranges.is_empty() {
search_view.focus_results_editor(cx);
}
} else {
cx.propagate_action();
}
});
} else {
cx.propagate_action();
}
}
fn toggle_search_option(
&mut self,
ToggleSearchOption(option): &ToggleSearchOption,
cx: &mut ViewContext<Self>,
) {
if let Some(search_view) = self.active_project_search.as_ref() {
search_view.update(cx, |search_view, cx| {
let value = match option {
SearchOption::WholeWord => &mut search_view.whole_word,
SearchOption::CaseSensitive => &mut search_view.case_sensitive,
SearchOption::Regex => &mut search_view.regex,
};
*value = !*value;
search_view.search(cx);
});
cx.notify();
}
}
@@ -677,6 +634,148 @@ impl ProjectSearchView {
.with_cursor_style(CursorStyle::PointingHand)
.boxed()
}
fn render_option_button(
&self,
icon: &str,
option: SearchOption,
cx: &mut RenderContext<Self>,
) -> ElementBox {
let is_active = self.is_option_enabled(option, cx);
MouseEventHandler::new::<ProjectSearchBar, _, _>(option as usize, cx, |state, cx| {
let theme = &cx.global::<Settings>().theme.search;
let style = match (is_active, state.hovered) {
(false, false) => &theme.option_button,
(false, true) => &theme.hovered_option_button,
(true, false) => &theme.active_option_button,
(true, true) => &theme.active_hovered_option_button,
};
Label::new(icon.to_string(), style.text.clone())
.contained()
.with_style(style.container)
.boxed()
})
.on_click(move |cx| cx.dispatch_action(ToggleSearchOption(option)))
.with_cursor_style(CursorStyle::PointingHand)
.boxed()
}
fn is_option_enabled(&self, option: SearchOption, cx: &AppContext) -> bool {
if let Some(search) = self.active_project_search.as_ref() {
let search = search.read(cx);
match option {
SearchOption::WholeWord => search.whole_word,
SearchOption::CaseSensitive => search.case_sensitive,
SearchOption::Regex => search.regex,
}
} else {
false
}
}
}
impl Entity for ProjectSearchBar {
type Event = ();
}
impl View for ProjectSearchBar {
fn ui_name() -> &'static str {
"ProjectSearchBar"
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
if let Some(search) = self.active_project_search.as_ref() {
let search = search.read(cx);
let theme = cx.global::<Settings>().theme.clone();
let editor_container = if search.query_contains_error {
theme.search.invalid_editor
} else {
theme.search.editor.input.container
};
Flex::row()
.with_child(
Flex::row()
.with_child(
ChildView::new(&search.query_editor)
.aligned()
.left()
.flex(1., true)
.boxed(),
)
.with_children(search.active_match_index.map(|match_ix| {
Label::new(
format!(
"{}/{}",
match_ix + 1,
search.model.read(cx).match_ranges.len()
),
theme.search.match_index.text.clone(),
)
.contained()
.with_style(theme.search.match_index.container)
.aligned()
.boxed()
}))
.contained()
.with_style(editor_container)
.aligned()
.constrained()
.with_min_width(theme.search.editor.min_width)
.with_max_width(theme.search.editor.max_width)
.flex(1., false)
.boxed(),
)
.with_child(
Flex::row()
.with_child(self.render_nav_button("<", Direction::Prev, cx))
.with_child(self.render_nav_button(">", Direction::Next, cx))
.aligned()
.boxed(),
)
.with_child(
Flex::row()
.with_child(self.render_option_button(
"Case",
SearchOption::CaseSensitive,
cx,
))
.with_child(self.render_option_button("Word", SearchOption::WholeWord, cx))
.with_child(self.render_option_button("Regex", SearchOption::Regex, cx))
.contained()
.with_style(theme.search.option_button_group)
.aligned()
.boxed(),
)
.contained()
.with_style(theme.search.container)
.aligned()
.left()
.named("project search")
} else {
Empty::new().boxed()
}
}
}
impl ToolbarItemView for ProjectSearchBar {
fn set_active_pane_item(
&mut self,
active_pane_item: Option<&dyn workspace::ItemHandle>,
cx: &mut ViewContext<Self>,
) -> ToolbarItemLocation {
cx.notify();
self.subscription = None;
self.active_project_search = None;
if let Some(search) = active_pane_item.and_then(|i| i.downcast::<ProjectSearchView>()) {
self.subscription = Some(cx.observe(&search, |_, _, cx| cx.notify()));
self.active_project_search = Some(search);
ToolbarItemLocation::PrimaryLeft {
flex: Some((1., false)),
}
} else {
ToolbarItemLocation::Hidden
}
}
}
#[cfg(test)]
@@ -726,7 +825,7 @@ mod tests {
search_view
.query_editor
.update(cx, |query_editor, cx| query_editor.set_text("TWO", cx));
search_view.search(&Search, cx);
search_view.search(cx);
});
search_view.next_notification(&cx).await;
search_view.update(cx, |search_view, cx| {
@@ -763,7 +862,7 @@ mod tests {
[DisplayPoint::new(2, 32)..DisplayPoint::new(2, 35)]
);
search_view.select_match(&SelectMatch(Direction::Next), cx);
search_view.select_match(Direction::Next, cx);
});
search_view.update(cx, |search_view, cx| {
@@ -774,7 +873,7 @@ mod tests {
.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
[DisplayPoint::new(2, 37)..DisplayPoint::new(2, 40)]
);
search_view.select_match(&SelectMatch(Direction::Next), cx);
search_view.select_match(Direction::Next, cx);
});
search_view.update(cx, |search_view, cx| {
@@ -785,7 +884,7 @@ mod tests {
.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
[DisplayPoint::new(5, 6)..DisplayPoint::new(5, 9)]
);
search_view.select_match(&SelectMatch(Direction::Next), cx);
search_view.select_match(Direction::Next, cx);
});
search_view.update(cx, |search_view, cx| {
@@ -796,7 +895,7 @@ mod tests {
.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
[DisplayPoint::new(2, 32)..DisplayPoint::new(2, 35)]
);
search_view.select_match(&SelectMatch(Direction::Prev), cx);
search_view.select_match(Direction::Prev, cx);
});
search_view.update(cx, |search_view, cx| {
@@ -807,7 +906,7 @@ mod tests {
.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
[DisplayPoint::new(5, 6)..DisplayPoint::new(5, 9)]
);
search_view.select_match(&SelectMatch(Direction::Prev), cx);
search_view.select_match(Direction::Prev, cx);
});
search_view.update(cx, |search_view, cx| {
+6 -5
View File
@@ -1,13 +1,14 @@
pub use buffer_search::BufferSearchBar;
use editor::{Anchor, MultiBufferSnapshot};
use gpui::{action, MutableAppContext};
pub use project_search::{ProjectSearchBar, ProjectSearchView};
use std::{
cmp::{self, Ordering},
ops::Range,
};
use editor::{Anchor, MultiBufferSnapshot};
use gpui::{action, MutableAppContext};
mod buffer_search;
mod project_search;
pub mod buffer_search;
pub mod project_search;
pub fn init(cx: &mut MutableAppContext) {
buffer_search::init(cx);
+1 -1
View File
@@ -4,7 +4,7 @@ use anyhow::Result;
use std::{cmp::Ordering, fmt::Debug, ops::Range};
use sum_tree::Bias;
#[derive(Clone, Eq, PartialEq, Debug, Hash)]
#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash)]
pub struct Anchor {
pub timestamp: clock::Local,
pub offset: usize,
+5
View File
@@ -26,6 +26,7 @@ pub struct Theme {
pub editor: Editor,
pub search: Search,
pub project_diagnostics: ProjectDiagnostics,
pub breadcrumbs: ContainedText,
}
#[derive(Deserialize, Default)]
@@ -94,7 +95,10 @@ pub struct Tab {
#[derive(Clone, Deserialize, Default)]
pub struct Toolbar {
#[serde(flatten)]
pub container: ContainerStyle,
pub height: f32,
pub item_spacing: f32,
}
#[derive(Clone, Deserialize, Default)]
@@ -119,6 +123,7 @@ pub struct Search {
pub struct FindEditor {
#[serde(flatten)]
pub input: FieldEditor,
pub min_width: f32,
pub max_width: f32,
}
+5 -1
View File
@@ -310,7 +310,11 @@ impl View for ThemeSelector {
.with_style(theme.selector.input_editor.container)
.boxed(),
)
.with_child(Flexible::new(1.0, false, self.render_matches(cx)).boxed())
.with_child(
FlexItem::new(self.render_matches(cx))
.flex(1., false)
.boxed(),
)
.boxed(),
)
.with_style(theme.selector.container)
+7 -46
View File
@@ -1,10 +1,12 @@
use std::{
collections::HashMap,
ops::Range,
path::{Path, PathBuf},
};
mod assertions;
mod marked_text;
use std::path::{Path, PathBuf};
use tempdir::TempDir;
pub use assertions::*;
pub use marked_text::*;
pub fn temp_tree(tree: serde_json::Value) -> TempDir {
let dir = TempDir::new("").unwrap();
write_tree(dir.path(), tree);
@@ -52,44 +54,3 @@ pub fn sample_text(rows: usize, cols: usize, start_char: char) -> String {
}
text
}
pub fn marked_text_by(
marked_text: &str,
markers: Vec<char>,
) -> (String, HashMap<char, Vec<usize>>) {
let mut extracted_markers: HashMap<char, Vec<usize>> = Default::default();
let mut unmarked_text = String::new();
for char in marked_text.chars() {
if markers.contains(&char) {
let char_offsets = extracted_markers.entry(char).or_insert(Vec::new());
char_offsets.push(unmarked_text.len());
} else {
unmarked_text.push(char);
}
}
(unmarked_text, extracted_markers)
}
pub fn marked_text(marked_text: &str) -> (String, Vec<usize>) {
let (unmarked_text, mut markers) = marked_text_by(marked_text, vec!['|']);
(unmarked_text, markers.remove(&'|').unwrap_or_else(Vec::new))
}
pub fn marked_text_ranges(marked_text: &str) -> (String, Vec<Range<usize>>) {
let (unmarked_text, mut markers) = marked_text_by(marked_text, vec!['[', ']']);
let opens = markers.remove(&'[').unwrap_or_default();
let closes = markers.remove(&']').unwrap_or_default();
assert_eq!(opens.len(), closes.len(), "marked ranges are unbalanced");
let ranges = opens
.into_iter()
.zip(closes)
.map(|(open, close)| {
assert!(close >= open, "marked ranges must be disjoint");
open..close
})
.collect();
(unmarked_text, ranges)
}
+19
View File
@@ -0,0 +1,19 @@
#[macro_export]
macro_rules! assert_set_eq {
($left:expr,$right:expr) => {{
let left = $left;
let right = $right;
for left_value in left.iter() {
if !right.contains(left_value) {
panic!("assertion failed: `(left == right)`\n left: {:?}\nright: {:?}\nright does not contain {:?}", left, right, left_value);
}
}
for right_value in right.iter() {
if !left.contains(right_value) {
panic!("assertion failed: `(left == right)`\n left: {:?}\nright: {:?}\nleft does not contain {:?}", left, right, right_value);
}
}
}};
}
+42
View File
@@ -0,0 +1,42 @@
use std::{collections::HashMap, ops::Range};
pub fn marked_text_by(
marked_text: &str,
markers: Vec<char>,
) -> (String, HashMap<char, Vec<usize>>) {
let mut extracted_markers: HashMap<char, Vec<usize>> = Default::default();
let mut unmarked_text = String::new();
for char in marked_text.chars() {
if markers.contains(&char) {
let char_offsets = extracted_markers.entry(char).or_insert(Vec::new());
char_offsets.push(unmarked_text.len());
} else {
unmarked_text.push(char);
}
}
(unmarked_text, extracted_markers)
}
pub fn marked_text(marked_text: &str) -> (String, Vec<usize>) {
let (unmarked_text, mut markers) = marked_text_by(marked_text, vec!['|']);
(unmarked_text, markers.remove(&'|').unwrap_or_else(Vec::new))
}
pub fn marked_text_ranges(marked_text: &str) -> (String, Vec<Range<usize>>) {
let (unmarked_text, mut markers) = marked_text_by(marked_text, vec!['[', ']']);
let opens = markers.remove(&'[').unwrap_or_default();
let closes = markers.remove(&']').unwrap_or_default();
assert_eq!(opens.len(), closes.len(), "marked ranges are unbalanced");
let ranges = opens
.into_iter()
.zip(closes)
.map(|(open, close)| {
assert!(close >= open, "marked ranges must be disjoint");
open..close
})
.collect();
(unmarked_text, ranges)
}
+22 -117
View File
@@ -1,5 +1,5 @@
use super::{ItemHandle, SplitDirection};
use crate::{Item, Settings, WeakItemHandle, Workspace};
use crate::{toolbar::Toolbar, Item, Settings, WeakItemHandle, Workspace};
use collections::{HashMap, VecDeque};
use gpui::{
action,
@@ -7,16 +7,11 @@ use gpui::{
geometry::{rect::RectF, vector::vec2f},
keymap::Binding,
platform::{CursorStyle, NavigationDirection},
AnyViewHandle, AppContext, Entity, MutableAppContext, Quad, RenderContext, Task, View,
ViewContext, ViewHandle, WeakViewHandle,
AppContext, Entity, MutableAppContext, Quad, RenderContext, Task, View, ViewContext,
ViewHandle, WeakViewHandle,
};
use project::{ProjectEntryId, ProjectPath};
use std::{
any::{Any, TypeId},
cell::RefCell,
cmp, mem,
rc::Rc,
};
use std::{any::Any, cell::RefCell, cmp, mem, rc::Rc};
use util::ResultExt;
action!(Split, SplitDirection);
@@ -101,28 +96,7 @@ pub struct Pane {
items: Vec<Box<dyn ItemHandle>>,
active_item_index: usize,
nav_history: Rc<RefCell<NavHistory>>,
toolbars: HashMap<TypeId, Box<dyn ToolbarHandle>>,
active_toolbar_type: Option<TypeId>,
active_toolbar_visible: bool,
}
pub trait Toolbar: View {
fn active_item_changed(
&mut self,
item: Option<Box<dyn ItemHandle>>,
cx: &mut ViewContext<Self>,
) -> bool;
fn on_dismiss(&mut self, cx: &mut ViewContext<Self>);
}
trait ToolbarHandle {
fn active_item_changed(
&self,
item: Option<Box<dyn ItemHandle>>,
cx: &mut MutableAppContext,
) -> bool;
fn on_dismiss(&self, cx: &mut MutableAppContext);
fn to_any(&self) -> AnyViewHandle;
toolbar: ViewHandle<Toolbar>,
}
pub struct ItemNavHistory {
@@ -158,14 +132,12 @@ pub struct NavigationEntry {
}
impl Pane {
pub fn new() -> Self {
pub fn new(cx: &mut ViewContext<Self>) -> Self {
Self {
items: Vec::new(),
active_item_index: 0,
nav_history: Default::default(),
toolbars: Default::default(),
active_toolbar_type: Default::default(),
active_toolbar_visible: false,
toolbar: cx.add_view(|_| Toolbar::new()),
}
}
@@ -402,7 +374,7 @@ impl Pane {
self.items[prev_active_item_ix].deactivated(cx);
cx.emit(Event::ActivateItem { local });
}
self.update_active_toolbar(cx);
self.update_toolbar(cx);
if local {
self.focus_active_item(cx);
self.activate(cx);
@@ -487,7 +459,7 @@ impl Pane {
self.focus_active_item(cx);
self.activate(cx);
}
self.update_active_toolbar(cx);
self.update_toolbar(cx);
cx.notify();
}
@@ -502,63 +474,18 @@ impl Pane {
cx.emit(Event::Split(direction));
}
pub fn show_toolbar<F, V>(&mut self, cx: &mut ViewContext<Self>, build_toolbar: F)
where
F: FnOnce(&mut ViewContext<V>) -> V,
V: Toolbar,
{
let type_id = TypeId::of::<V>();
if self.active_toolbar_type != Some(type_id) {
self.dismiss_toolbar(cx);
let active_item = self.active_item();
self.toolbars
.entry(type_id)
.or_insert_with(|| Box::new(cx.add_view(build_toolbar)));
self.active_toolbar_type = Some(type_id);
self.active_toolbar_visible =
self.toolbars[&type_id].active_item_changed(active_item, cx);
cx.notify();
}
pub fn toolbar(&self) -> &ViewHandle<Toolbar> {
&self.toolbar
}
pub fn dismiss_toolbar(&mut self, cx: &mut ViewContext<Self>) {
if let Some(active_toolbar_type) = self.active_toolbar_type.take() {
self.toolbars
.get_mut(&active_toolbar_type)
.unwrap()
.on_dismiss(cx);
self.active_toolbar_visible = false;
self.focus_active_item(cx);
cx.notify();
}
}
pub fn toolbar<T: Toolbar>(&self) -> Option<ViewHandle<T>> {
self.toolbars
.get(&TypeId::of::<T>())
.and_then(|toolbar| toolbar.to_any().downcast())
}
pub fn active_toolbar(&self) -> Option<AnyViewHandle> {
let type_id = self.active_toolbar_type?;
let toolbar = self.toolbars.get(&type_id)?;
if self.active_toolbar_visible {
Some(toolbar.to_any())
} else {
None
}
}
fn update_active_toolbar(&mut self, cx: &mut ViewContext<Self>) {
let active_item = self.items.get(self.active_item_index);
for (toolbar_type_id, toolbar) in &self.toolbars {
let visible = toolbar.active_item_changed(active_item.cloned(), cx);
if Some(*toolbar_type_id) == self.active_toolbar_type {
self.active_toolbar_visible = visible;
}
}
fn update_toolbar(&mut self, cx: &mut ViewContext<Self>) {
let active_item = self
.items
.get(self.active_item_index)
.map(|item| item.as_ref());
self.toolbar.update(cx, |toolbar, cx| {
toolbar.set_active_pane_item(active_item, cx);
});
}
fn render_tabs(&self, cx: &mut RenderContext<Self>) -> ElementBox {
@@ -685,7 +612,7 @@ impl Pane {
Empty::new()
.contained()
.with_border(theme.workspace.tab.container.border)
.flexible(0., true)
.flex(0., true)
.named("filler"),
);
@@ -713,12 +640,8 @@ impl View for Pane {
EventHandler::new(if let Some(active_item) = self.active_item() {
Flex::column()
.with_child(self.render_tabs(cx))
.with_children(
self.active_toolbar()
.as_ref()
.map(|view| ChildView::new(view).boxed()),
)
.with_child(ChildView::new(active_item).flexible(1., true).boxed())
.with_child(ChildView::new(&self.toolbar).boxed())
.with_child(ChildView::new(active_item).flex(1., true).boxed())
.boxed()
} else {
Empty::new().boxed()
@@ -740,24 +663,6 @@ impl View for Pane {
}
}
impl<T: Toolbar> ToolbarHandle for ViewHandle<T> {
fn active_item_changed(
&self,
item: Option<Box<dyn ItemHandle>>,
cx: &mut MutableAppContext,
) -> bool {
self.update(cx, |this, cx| this.active_item_changed(item, cx))
}
fn on_dismiss(&self, cx: &mut MutableAppContext) {
self.update(cx, |this, cx| this.on_dismiss(cx));
}
fn to_any(&self) -> AnyViewHandle {
self.into()
}
}
impl ItemNavHistory {
pub fn new<T: Item>(history: Rc<RefCell<NavHistory>>, item: &ViewHandle<T>) -> Self {
Self {
+1 -1
View File
@@ -248,7 +248,7 @@ impl PaneAxis {
member = Container::new(member).with_border(border).boxed();
}
Flexible::new(1.0, true, member).boxed()
FlexItem::new(member).flex(1.0, true).boxed()
}))
.boxed()
}
+1 -1
View File
@@ -138,7 +138,7 @@ impl Sidebar {
let width = self.width.clone();
move |size, _| *width.borrow_mut() = size.x()
})
.flexible(1., false)
.flex(1., false)
.boxed(),
);
if matches!(self.side, Side::Left) {
+1 -1
View File
@@ -47,12 +47,12 @@ impl View for StatusBar {
.with_margin_right(theme.item_spacing)
.boxed()
}))
.with_child(Empty::new().flexible(1., true).boxed())
.with_children(self.right_items.iter().map(|i| {
ChildView::new(i.as_ref())
.aligned()
.contained()
.with_margin_left(theme.item_spacing)
.flex_float()
.boxed()
}))
.contained()
+193
View File
@@ -0,0 +1,193 @@
use crate::{ItemHandle, Settings};
use gpui::{
elements::*, AnyViewHandle, AppContext, ElementBox, Entity, MutableAppContext, RenderContext,
View, ViewContext, ViewHandle,
};
pub trait ToolbarItemView: View {
fn set_active_pane_item(
&mut self,
active_pane_item: Option<&dyn crate::ItemHandle>,
cx: &mut ViewContext<Self>,
) -> ToolbarItemLocation;
fn location_for_event(
&self,
_event: &Self::Event,
current_location: ToolbarItemLocation,
_cx: &AppContext,
) -> ToolbarItemLocation {
current_location
}
}
trait ToolbarItemViewHandle {
fn id(&self) -> usize;
fn to_any(&self) -> AnyViewHandle;
fn set_active_pane_item(
&self,
active_pane_item: Option<&dyn ItemHandle>,
cx: &mut MutableAppContext,
) -> ToolbarItemLocation;
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum ToolbarItemLocation {
Hidden,
PrimaryLeft { flex: Option<(f32, bool)> },
PrimaryRight { flex: Option<(f32, bool)> },
Secondary,
}
pub struct Toolbar {
active_pane_item: Option<Box<dyn ItemHandle>>,
items: Vec<(Box<dyn ToolbarItemViewHandle>, ToolbarItemLocation)>,
}
impl Entity for Toolbar {
type Event = ();
}
impl View for Toolbar {
fn ui_name() -> &'static str {
"Toolbar"
}
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
let theme = &cx.global::<Settings>().theme.workspace.toolbar;
let mut primary_left_items = Vec::new();
let mut primary_right_items = Vec::new();
let mut secondary_item = None;
for (item, position) in &self.items {
match *position {
ToolbarItemLocation::Hidden => {}
ToolbarItemLocation::PrimaryLeft { flex } => {
let left_item = ChildView::new(item.as_ref())
.aligned()
.contained()
.with_margin_right(theme.item_spacing);
if let Some((flex, expanded)) = flex {
primary_left_items.push(left_item.flex(flex, expanded).boxed());
} else {
primary_left_items.push(left_item.boxed());
}
}
ToolbarItemLocation::PrimaryRight { flex } => {
let right_item = ChildView::new(item.as_ref())
.aligned()
.contained()
.with_margin_left(theme.item_spacing)
.flex_float();
if let Some((flex, expanded)) = flex {
primary_right_items.push(right_item.flex(flex, expanded).boxed());
} else {
primary_right_items.push(right_item.boxed());
}
}
ToolbarItemLocation::Secondary => {
secondary_item = Some(
ChildView::new(item.as_ref())
.constrained()
.with_height(theme.height)
.boxed(),
);
}
}
}
Flex::column()
.with_child(
Flex::row()
.with_children(primary_left_items)
.with_children(primary_right_items)
.constrained()
.with_height(theme.height)
.boxed(),
)
.with_children(secondary_item)
.contained()
.with_style(theme.container)
.boxed()
}
}
impl Toolbar {
pub fn new() -> Self {
Self {
active_pane_item: None,
items: Default::default(),
}
}
pub fn add_item<T>(&mut self, item: ViewHandle<T>, cx: &mut ViewContext<Self>)
where
T: 'static + ToolbarItemView,
{
let location = item.set_active_pane_item(self.active_pane_item.as_deref(), cx);
cx.subscribe(&item, |this, item, event, cx| {
if let Some((_, current_location)) =
this.items.iter_mut().find(|(i, _)| i.id() == item.id())
{
let new_location = item
.read(cx)
.location_for_event(event, *current_location, cx);
if new_location != *current_location {
*current_location = new_location;
cx.notify();
}
}
})
.detach();
self.items.push((Box::new(item), location));
cx.notify();
}
pub fn set_active_pane_item(
&mut self,
pane_item: Option<&dyn ItemHandle>,
cx: &mut ViewContext<Self>,
) {
self.active_pane_item = pane_item.map(|item| item.boxed_clone());
for (toolbar_item, current_location) in self.items.iter_mut() {
let new_location = toolbar_item.set_active_pane_item(pane_item, cx);
if new_location != *current_location {
*current_location = new_location;
cx.notify();
}
}
}
pub fn item_of_type<T: ToolbarItemView>(&self) -> Option<ViewHandle<T>> {
self.items
.iter()
.find_map(|(item, _)| item.to_any().downcast())
}
}
impl<T: ToolbarItemView> ToolbarItemViewHandle for ViewHandle<T> {
fn id(&self) -> usize {
self.id()
}
fn to_any(&self) -> AnyViewHandle {
self.into()
}
fn set_active_pane_item(
&self,
active_pane_item: Option<&dyn ItemHandle>,
cx: &mut MutableAppContext,
) -> ToolbarItemLocation {
self.update(cx, |this, cx| {
this.set_active_pane_item(active_pane_item, cx)
})
}
}
impl Into<AnyViewHandle> for &dyn ToolbarItemViewHandle {
fn into(self) -> AnyViewHandle {
self.to_any()
}
}
+23 -16
View File
@@ -5,6 +5,7 @@ pub mod pane_group;
pub mod settings;
pub mod sidebar;
mod status_bar;
mod toolbar;
use anyhow::{anyhow, Context, Result};
use client::{
@@ -47,6 +48,7 @@ use std::{
},
};
use theme::{Theme, ThemeRegistry};
pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
use util::ResultExt;
type ProjectItemBuilders = HashMap<
@@ -650,6 +652,10 @@ impl WorkspaceParams {
}
}
pub enum Event {
PaneAdded(ViewHandle<Pane>),
}
pub struct Workspace {
weak_self: WeakViewHandle<Self>,
client: Arc<Client>,
@@ -716,7 +722,7 @@ impl Workspace {
})
.detach();
let pane = cx.add_view(|_| Pane::new());
let pane = cx.add_view(|cx| Pane::new(cx));
let pane_id = pane.id();
cx.observe(&pane, move |me, _, cx| {
let active_entry = me.active_project_path(cx);
@@ -729,6 +735,7 @@ impl Workspace {
})
.detach();
cx.focus(&pane);
cx.emit(Event::PaneAdded(pane.clone()));
let status_bar = cx.add_view(|cx| StatusBar::new(&pane, cx));
let mut current_user = params.user_store.read(cx).watch_current_user().clone();
@@ -1047,7 +1054,7 @@ impl Workspace {
}
fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> ViewHandle<Pane> {
let pane = cx.add_view(|_| Pane::new());
let pane = cx.add_view(|cx| Pane::new(cx));
let pane_id = pane.id();
cx.observe(&pane, move |me, _, cx| {
let active_entry = me.active_project_path(cx);
@@ -1061,6 +1068,7 @@ impl Workspace {
.detach();
self.panes.push(pane.clone());
self.activate_pane(pane.clone(), cx);
cx.emit(Event::PaneAdded(pane.clone()));
pane
}
@@ -1916,7 +1924,7 @@ impl Workspace {
}
impl Entity for Workspace {
type Event = ();
type Event = Event;
}
impl View for Workspace {
@@ -1938,36 +1946,35 @@ impl View for Workspace {
if let Some(element) =
self.left_sidebar.render_active_item(&theme, cx)
{
content.add_child(Flexible::new(0.8, false, element).boxed());
content
.add_child(FlexItem::new(element).flex(0.8, false).boxed());
}
content.add_child(
Flex::column()
.with_child(
Flexible::new(
1.,
true,
self.center.render(
&theme,
&self.follower_states_by_leader,
self.project.read(cx).collaborators(),
),
)
FlexItem::new(self.center.render(
&theme,
&self.follower_states_by_leader,
self.project.read(cx).collaborators(),
))
.flex(1., true)
.boxed(),
)
.with_child(ChildView::new(&self.status_bar).boxed())
.flexible(1., true)
.flex(1., true)
.boxed(),
);
if let Some(element) =
self.right_sidebar.render_active_item(&theme, cx)
{
content.add_child(Flexible::new(0.8, false, element).boxed());
content
.add_child(FlexItem::new(element).flex(0.8, false).boxed());
}
content.add_child(self.right_sidebar.render(&theme, cx));
content.boxed()
})
.with_children(self.modal.as_ref().map(|m| ChildView::new(m).boxed()))
.flexible(1.0, true)
.flex(1.0, true)
.boxed(),
)
.contained()
+1
View File
@@ -29,6 +29,7 @@ test-support = [
]
[dependencies]
breadcrumbs = { path = "../breadcrumbs" }
chat_panel = { path = "../chat_panel" }
collections = { path = "../collections" }
client = { path = "../client" }
+14 -6
View File
@@ -85,7 +85,15 @@ diagnostic_message = "$text.2"
lsp_message = "$text.2"
[workspace.toolbar]
height = 44
background = "$surface.1"
border = { color = "$border.0", width = 1, left = false, right = false, bottom = true, top = false }
height = 34
item_spacing = 8
padding = { left = 16, right = 8, top = 4, bottom = 4 }
[breadcrumbs]
extends = "$text.1"
padding = { left = 6 }
[panel]
padding = { top = 12, left = 12, bottom = 12, right = 12 }
@@ -354,7 +362,6 @@ tab_summary_spacing = 10
[search]
match_background = "$state.highlighted_line"
background = "$surface.1"
results_status = { extends = "$text.0", size = 18 }
tab_icon_width = 14
tab_icon_spacing = 4
@@ -384,15 +391,16 @@ extends = "$search.option_button"
background = "$surface.2"
[search.match_index]
extends = "$text.1"
extends = "$text.2"
padding = 6
[search.editor]
max_width = 400
min_width = 200
max_width = 500
background = "$surface.0"
corner_radius = 6
padding = { left = 13, right = 13, top = 3, bottom = 3 }
margin = { top = 5, bottom = 5, left = 5, right = 5 }
padding = { left = 14, right = 14, top = 3, bottom = 3 }
margin = { right = 5 }
text = "$text.0"
placeholder_text = "$text.2"
selection = "$selection.host"
+17
View File
@@ -4,6 +4,7 @@ pub mod menus;
#[cfg(any(test, feature = "test-support"))]
pub mod test;
use breadcrumbs::Breadcrumbs;
use chat_panel::ChatPanel;
pub use client;
pub use contacts_panel;
@@ -21,6 +22,7 @@ pub use lsp;
use project::Project;
pub use project::{self, fs};
use project_panel::ProjectPanel;
use search::{BufferSearchBar, ProjectSearchBar};
use std::{path::PathBuf, sync::Arc};
pub use workspace;
use workspace::{AppState, Settings, Workspace, WorkspaceParams};
@@ -104,6 +106,21 @@ pub fn build_workspace(
app_state: &Arc<AppState>,
cx: &mut ViewContext<Workspace>,
) -> Workspace {
cx.subscribe(&cx.handle(), |_, _, event, cx| {
let workspace::Event::PaneAdded(pane) = event;
pane.update(cx, |pane, cx| {
pane.toolbar().update(cx, |toolbar, cx| {
let breadcrumbs = cx.add_view(|_| Breadcrumbs::new());
toolbar.add_item(breadcrumbs, cx);
let buffer_search_bar = cx.add_view(|cx| BufferSearchBar::new(cx));
toolbar.add_item(buffer_search_bar, cx);
let project_search_bar = cx.add_view(|_| ProjectSearchBar::new());
toolbar.add_item(project_search_bar, cx);
})
});
})
.detach();
let workspace_params = WorkspaceParams {
project,
client: app_state.client.clone(),