Detailed changes
@@ -1,12 +1,12 @@
# syntax = docker/dockerfile:1.2
-FROM rust as builder
+FROM rust:1.55-bullseye as builder
WORKDIR app
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=./target \
cargo install sqlx-cli --root=/app --target-dir=/app/target --version 0.5.7
-FROM debian:buster-slim as runtime
+FROM debian:bullseye-slim as runtime
RUN apt-get update; \
apt-get install -y --no-install-recommends libssl1.1
WORKDIR app
@@ -4,6 +4,9 @@ edition = "2018"
name = "gpui"
version = "0.1.0"
+[features]
+test-support = []
+
[dependencies]
arrayvec = "0.7.1"
async-task = "4.0.3"
@@ -2335,6 +2335,16 @@ impl<V: View> ReadModel for RenderContext<'_, V> {
}
}
+impl<V: View> UpdateModel for RenderContext<'_, V> {
+ fn update_model<T, F, S>(&mut self, handle: &ModelHandle<T>, update: F) -> S
+ where
+ T: Entity,
+ F: FnOnce(&mut T, &mut ModelContext<T>) -> S,
+ {
+ self.app.update_model(handle, update)
+ }
+}
+
impl<M> AsRef<AppContext> for ViewContext<'_, M> {
fn as_ref(&self) -> &AppContext {
&self.app.cx
@@ -348,6 +348,17 @@ enum Spacing {
},
}
+impl Padding {
+ pub fn uniform(padding: f32) -> Self {
+ Self {
+ top: padding,
+ left: padding,
+ bottom: padding,
+ right: padding,
+ }
+ }
+}
+
impl ToJson for Padding {
fn to_json(&self) -> serde_json::Value {
let mut value = json!({});
@@ -116,7 +116,8 @@ impl Element for MouseEventHandler {
let hit_bounds = RectF::from_points(
bounds.origin() - vec2f(self.padding.left, self.padding.top),
bounds.lower_right() + vec2f(self.padding.right, self.padding.bottom),
- );
+ )
+ .round_out();
self.state.update(cx, |state, cx| match event {
Event::MouseMoved {
@@ -47,8 +47,9 @@ impl Element for Svg {
);
(size, Some(tree))
}
- Err(error) => {
- log::error!("{}", error);
+ Err(_error) => {
+ #[cfg(not(any(test, feature = "test-support")))]
+ log::error!("{}", _error);
(constraint.min, None)
}
}
@@ -17,7 +17,7 @@ use std::{
pub struct FamilyId(usize);
struct Family {
- name: String,
+ name: Arc<str>,
font_ids: Vec<FontId>,
}
@@ -49,7 +49,7 @@ impl FontCache {
}))
}
- pub fn family_name(&self, family_id: FamilyId) -> Result<String> {
+ pub fn family_name(&self, family_id: FamilyId) -> Result<Arc<str>> {
self.0
.read()
.families
@@ -62,7 +62,7 @@ impl FontCache {
for name in names {
let state = self.0.upgradable_read();
- if let Some(ix) = state.families.iter().position(|f| f.name == *name) {
+ if let Some(ix) = state.families.iter().position(|f| f.name.as_ref() == *name) {
return Ok(FamilyId(ix));
}
@@ -81,7 +81,7 @@ impl FontCache {
}
state.families.push(Family {
- name: String::from(*name),
+ name: Arc::from(*name),
font_ids,
});
return Ok(family_id);
@@ -141,8 +141,8 @@ impl FontCache {
pub fn bounding_box(&self, font_id: FontId, font_size: f32) -> Vector2F {
let bounding_box = self.metric(font_id, |m| m.bounding_box);
- let width = self.scale_metric(bounding_box.width(), font_id, font_size);
- let height = self.scale_metric(bounding_box.height(), font_id, font_size);
+ let width = bounding_box.width() * self.em_scale(font_id, font_size);
+ let height = bounding_box.height() * self.em_scale(font_id, font_size);
vec2f(width, height)
}
@@ -154,28 +154,28 @@ impl FontCache {
glyph_id = state.fonts.glyph_for_char(font_id, 'm').unwrap();
bounds = state.fonts.typographic_bounds(font_id, glyph_id).unwrap();
}
- self.scale_metric(bounds.width(), font_id, font_size)
+ bounds.width() * self.em_scale(font_id, font_size)
}
pub fn line_height(&self, font_id: FontId, font_size: f32) -> f32 {
let height = self.metric(font_id, |m| m.bounding_box.height());
- self.scale_metric(height, font_id, font_size)
+ (height * self.em_scale(font_id, font_size)).ceil()
}
pub fn cap_height(&self, font_id: FontId, font_size: f32) -> f32 {
- self.scale_metric(self.metric(font_id, |m| m.cap_height), font_id, font_size)
+ self.metric(font_id, |m| m.cap_height) * self.em_scale(font_id, font_size)
}
pub fn ascent(&self, font_id: FontId, font_size: f32) -> f32 {
- self.scale_metric(self.metric(font_id, |m| m.ascent), font_id, font_size)
+ self.metric(font_id, |m| m.ascent) * self.em_scale(font_id, font_size)
}
pub fn descent(&self, font_id: FontId, font_size: f32) -> f32 {
- self.scale_metric(self.metric(font_id, |m| -m.descent), font_id, font_size)
+ self.metric(font_id, |m| -m.descent) * self.em_scale(font_id, font_size)
}
- pub fn scale_metric(&self, metric: f32, font_id: FontId, font_size: f32) -> f32 {
- metric * font_size / self.metric(font_id, |m| m.units_per_em as f32)
+ pub fn em_scale(&self, font_id: FontId, font_size: f32) -> f32 {
+ font_size / self.metric(font_id, |m| m.units_per_em as f32)
}
pub fn line_wrapper(self: &Arc<Self>, font_id: FontId, font_size: f32) -> LineWrapperHandle {
@@ -1,5 +1,6 @@
use crate::{
color::Color,
+ font_cache::FamilyId,
json::{json, ToJson},
text_layout::RunStyle,
FontCache,
@@ -22,6 +23,7 @@ pub type GlyphId = u32;
pub struct TextStyle {
pub color: Color,
pub font_family_name: Arc<str>,
+ pub font_family_id: FamilyId,
pub font_id: FontId,
pub font_size: f32,
pub font_properties: Properties,
@@ -85,11 +87,12 @@ impl TextStyle {
font_cache: &FontCache,
) -> anyhow::Result<Self> {
let font_family_name = font_family_name.into();
- let family_id = font_cache.load_family(&[&font_family_name])?;
- let font_id = font_cache.select_font(family_id, &font_properties)?;
+ let font_family_id = font_cache.load_family(&[&font_family_name])?;
+ let font_id = font_cache.select_font(font_family_id, &font_properties)?;
Ok(Self {
color,
font_family_name,
+ font_family_id,
font_id,
font_size,
font_properties,
@@ -124,6 +127,32 @@ impl TextStyle {
}
})
}
+
+ pub fn line_height(&self, font_cache: &FontCache) -> f32 {
+ font_cache.line_height(self.font_id, self.font_size)
+ }
+
+ pub fn em_width(&self, font_cache: &FontCache) -> f32 {
+ font_cache.em_width(self.font_id, self.font_size)
+ }
+
+ pub fn descent(&self, font_cache: &FontCache) -> f32 {
+ font_cache.metric(self.font_id, |m| m.descent) * self.em_scale(font_cache)
+ }
+
+ fn em_scale(&self, font_cache: &FontCache) -> f32 {
+ font_cache.em_scale(self.font_id, self.font_size)
+ }
+}
+
+impl From<TextStyle> for HighlightStyle {
+ fn from(other: TextStyle) -> Self {
+ Self {
+ color: other.color,
+ font_properties: other.font_properties,
+ underline: other.underline,
+ }
+ }
}
impl HighlightStyle {
@@ -286,6 +286,14 @@ impl<'a> PaintContext<'a> {
}
}
+impl<'a> Deref for PaintContext<'a> {
+ type Target = AppContext;
+
+ fn deref(&self) -> &Self::Target {
+ self.app
+ }
+}
+
pub struct EventContext<'a> {
rendered_views: &'a mut HashMap<usize, ElementBox>,
dispatched_actions: Vec<DispatchDirective>,
@@ -965,7 +965,7 @@ mod tests {
};
use zed::{
channel::{Channel, ChannelDetails, ChannelList},
- editor::{Editor, Insert},
+ editor::{Editor, EditorStyle, Insert},
fs::{FakeFs, Fs as _},
language::LanguageRegistry,
rpc::{self, Client, Credentials, EstablishConnectionError},
@@ -1048,7 +1048,14 @@ mod tests {
.unwrap();
// Create a selection set as client B and see that selection set as client A.
- let editor_b = cx_b.add_view(window_b, |cx| Editor::for_buffer(buffer_b, settings, cx));
+ let editor_b = cx_b.add_view(window_b, |cx| {
+ Editor::for_buffer(
+ buffer_b,
+ settings,
+ |cx| EditorStyle::test(cx.font_cache()),
+ cx,
+ )
+ });
buffer_a
.condition(&cx_a, |buffer, _| buffer.selection_sets().count() == 1)
.await;
@@ -14,7 +14,7 @@ name = "Zed"
path = "src/main.rs"
[features]
-test-support = ["tempdir", "zrpc/test-support"]
+test-support = ["tempdir", "zrpc/test-support", "gpui/test-support"]
[dependencies]
anyhow = "1.0.38"
@@ -69,6 +69,7 @@ serde_json = { version = "1.0.64", features = ["preserve_order"] }
tempdir = { version = "0.3.7" }
unindent = "0.1.7"
zrpc = { path = "../zrpc", features = ["test-support"] }
+gpui = { path = "../gpui", features = ["test-support"] }
[package.metadata.bundle]
icon = ["app-icon@2x.png", "app-icon.png"]
@@ -18,7 +18,7 @@ width = 16
[workspace.tab]
text = "$text.2"
-padding = { left = 10, right = 10 }
+padding = { left = 12, right = 12 }
icon_width = 8
spacing = 10
icon_close = "$text.2.color"
@@ -107,8 +107,8 @@ shadow = { offset = [0, 2], blur = 16, color = "$shadow.0" }
background = "$surface.1"
corner_radius = 6
padding = { left = 8, right = 8, top = 7, bottom = 7 }
-text = "$text.0.color"
-placeholder_text = "$text.2.color"
+text = "$text.0"
+placeholder_text = "$text.2"
selection = "$selection.host"
border = { width = 1, color = "$border.0" }
@@ -140,8 +140,8 @@ border = { width = 1, color = "$border.0" }
background = "$surface.1"
corner_radius = 6
padding = { left = 16, right = 16, top = 7, bottom = 7 }
-text = "$text.0.color"
-placeholder_text = "$text.2.color"
+text = "$text.0"
+placeholder_text = "$text.2"
selection = "$selection.host"
border = { width = 1, color = "$border.0" }
@@ -161,7 +161,7 @@ background = "$state.hover"
text = "$text.0"
[editor]
-text = "$text.1.color"
+text = "$text.1"
background = "$surface.1"
gutter_background = "$surface.1"
active_line_background = "$state.active_line"
@@ -54,10 +54,15 @@ impl ChatPanel {
cx: &mut ViewContext<Self>,
) -> Self {
let input_editor = cx.add_view(|cx| {
- Editor::auto_height(4, settings.clone(), cx).with_style({
- let settings = settings.clone();
- move |_| settings.borrow().theme.chat_panel.input_editor.as_editor()
- })
+ Editor::auto_height(
+ 4,
+ settings.clone(),
+ {
+ let settings = settings.clone();
+ move |_| settings.borrow().theme.chat_panel.input_editor.as_editor()
+ },
+ cx,
+ )
});
let channel_select = cx.add_view(|cx| {
let channel_list = channel_list.clone();
@@ -4,8 +4,9 @@ mod element;
pub mod movement;
use crate::{
- settings::{HighlightId, Settings},
- theme::{EditorStyle, Theme},
+ language::Language,
+ settings::Settings,
+ theme::Theme,
time::ReplicaId,
util::{post_inc, Bias},
workspace,
@@ -17,15 +18,9 @@ pub use display_map::DisplayPoint;
use display_map::*;
pub use element::*;
use gpui::{
- action,
- color::Color,
- font_cache::FamilyId,
- fonts::Properties as FontProperties,
- geometry::vector::Vector2F,
- keymap::Binding,
- text_layout::{self, RunStyle},
- AppContext, ClipboardItem, Element, ElementBox, Entity, FontCache, ModelHandle,
- MutableAppContext, RenderContext, Task, TextLayoutCache, View, ViewContext, WeakViewHandle,
+ action, color::Color, fonts::TextStyle, geometry::vector::Vector2F, keymap::Binding,
+ text_layout, AppContext, ClipboardItem, Element, ElementBox, Entity, ModelHandle,
+ MutableAppContext, RenderContext, Task, View, ViewContext, WeakViewHandle,
};
use postage::watch;
use serde::{Deserialize, Serialize};
@@ -34,8 +29,6 @@ use smol::Timer;
use std::{
cell::RefCell,
cmp::{self, Ordering},
- collections::BTreeMap,
- fmt::Write,
iter::FromIterator,
mem,
ops::{Range, RangeInclusive},
@@ -278,6 +271,26 @@ pub enum EditorMode {
Full,
}
+#[derive(Clone, Deserialize)]
+pub struct EditorStyle {
+ pub text: TextStyle,
+ #[serde(default)]
+ pub placeholder_text: Option<TextStyle>,
+ pub background: Color,
+ pub selection: SelectionStyle,
+ pub gutter_background: Color,
+ pub active_line_background: Color,
+ pub line_number: Color,
+ pub line_number_active: Color,
+ pub guest_selections: Vec<SelectionStyle>,
+}
+
+#[derive(Clone, Copy, Default, Deserialize)]
+pub struct SelectionStyle {
+ pub cursor: Color,
+ pub selection: Color,
+}
+
pub struct Editor {
handle: WeakViewHandle<Self>,
buffer: ModelHandle<Buffer>,
@@ -290,10 +303,10 @@ pub struct Editor {
scroll_position: Vector2F,
scroll_top_anchor: Anchor,
autoscroll_requested: bool,
- build_style: Option<Rc<RefCell<dyn FnMut(&mut MutableAppContext) -> EditorStyle>>>,
+ build_style: Rc<RefCell<dyn FnMut(&mut MutableAppContext) -> EditorStyle>>,
settings: watch::Receiver<Settings>,
focused: bool,
- cursors_visible: bool,
+ show_local_cursors: bool,
blink_epoch: usize,
blinking_paused: bool,
mode: EditorMode,
@@ -305,8 +318,6 @@ pub struct Snapshot {
pub display_snapshot: DisplayMapSnapshot,
pub placeholder_text: Option<Arc<str>>,
pub theme: Arc<Theme>,
- pub font_family: FamilyId,
- pub font_size: f32,
is_focused: bool,
scroll_position: Vector2F,
scroll_top_anchor: Anchor,
@@ -324,9 +335,13 @@ struct ClipboardSelection {
}
impl Editor {
- pub fn single_line(settings: watch::Receiver<Settings>, cx: &mut ViewContext<Self>) -> Self {
+ pub fn single_line(
+ settings: watch::Receiver<Settings>,
+ build_style: impl 'static + FnMut(&mut MutableAppContext) -> EditorStyle,
+ cx: &mut ViewContext<Self>,
+ ) -> Self {
let buffer = cx.add_model(|cx| Buffer::new(0, String::new(), cx));
- let mut view = Self::for_buffer(buffer, settings, cx);
+ let mut view = Self::for_buffer(buffer, settings, build_style, cx);
view.mode = EditorMode::SingleLine;
view
}
@@ -334,10 +349,11 @@ impl Editor {
pub fn auto_height(
max_lines: usize,
settings: watch::Receiver<Settings>,
+ build_style: impl 'static + FnMut(&mut MutableAppContext) -> EditorStyle,
cx: &mut ViewContext<Self>,
) -> Self {
let buffer = cx.add_model(|cx| Buffer::new(0, String::new(), cx));
- let mut view = Self::for_buffer(buffer, settings, cx);
+ let mut view = Self::for_buffer(buffer, settings, build_style, cx);
view.mode = EditorMode::AutoHeight { max_lines };
view
}
@@ -345,10 +361,29 @@ impl Editor {
pub fn for_buffer(
buffer: ModelHandle<Buffer>,
settings: watch::Receiver<Settings>,
+ build_style: impl 'static + FnMut(&mut MutableAppContext) -> EditorStyle,
cx: &mut ViewContext<Self>,
) -> Self {
- let display_map =
- cx.add_model(|cx| DisplayMap::new(buffer.clone(), settings.clone(), None, cx));
+ Self::new(buffer, settings, Rc::new(RefCell::new(build_style)), cx)
+ }
+
+ fn new(
+ buffer: ModelHandle<Buffer>,
+ settings: watch::Receiver<Settings>,
+ build_style: Rc<RefCell<dyn FnMut(&mut MutableAppContext) -> EditorStyle>>,
+ cx: &mut ViewContext<Self>,
+ ) -> Self {
+ let style = build_style.borrow_mut()(cx);
+ let display_map = cx.add_model(|cx| {
+ DisplayMap::new(
+ buffer.clone(),
+ settings.borrow().tab_size,
+ style.text.font_id,
+ style.text.font_size,
+ None,
+ cx,
+ )
+ });
cx.observe(&buffer, Self::on_buffer_changed).detach();
cx.subscribe(&buffer, Self::on_buffer_event).detach();
cx.observe(&display_map, Self::on_display_map_changed)
@@ -376,13 +411,13 @@ impl Editor {
next_selection_id,
add_selections_state: None,
select_larger_syntax_node_stack: Vec::new(),
- build_style: None,
+ build_style,
scroll_position: Vector2F::zero(),
scroll_top_anchor: Anchor::min(),
autoscroll_requested: false,
settings,
focused: false,
- cursors_visible: false,
+ show_local_cursors: false,
blink_epoch: 0,
blinking_paused: false,
mode: EditorMode::Full,
@@ -390,14 +425,6 @@ impl Editor {
}
}
- pub fn with_style(
- mut self,
- f: impl 'static + FnMut(&mut MutableAppContext) -> EditorStyle,
- ) -> Self {
- self.build_style = Some(Rc::new(RefCell::new(f)));
- self
- }
-
pub fn replica_id(&self, cx: &AppContext) -> ReplicaId {
self.buffer.read(cx).replica_id()
}
@@ -416,8 +443,6 @@ impl Editor {
scroll_top_anchor: self.scroll_top_anchor.clone(),
theme: settings.theme.clone(),
placeholder_text: self.placeholder_text.clone(),
- font_family: settings.buffer_font_family,
- font_size: settings.buffer_font_size,
is_focused: self
.handle
.upgrade(cx)
@@ -425,6 +450,10 @@ impl Editor {
}
}
+ pub fn language<'a>(&self, cx: &'a AppContext) -> Option<&'a Arc<Language>> {
+ self.buffer.read(cx).language()
+ }
+
pub fn set_placeholder_text(
&mut self,
placeholder_text: impl Into<Arc<str>>,
@@ -2229,7 +2258,7 @@ impl Editor {
}
fn pause_cursor_blinking(&mut self, cx: &mut ViewContext<Self>) {
- self.cursors_visible = true;
+ self.show_local_cursors = true;
cx.notify();
let epoch = self.next_blink_epoch();
@@ -2254,7 +2283,7 @@ impl Editor {
fn blink_cursors(&mut self, epoch: usize, cx: &mut ViewContext<Self>) {
if epoch == self.blink_epoch && self.focused && !self.blinking_paused {
- self.cursors_visible = !self.cursors_visible;
+ self.show_local_cursors = !self.show_local_cursors;
cx.notify();
let epoch = self.next_blink_epoch();
@@ -2271,8 +2300,8 @@ impl Editor {
}
}
- pub fn cursors_visible(&self) -> bool {
- self.cursors_visible
+ pub fn show_local_cursors(&self) -> bool {
+ self.show_local_cursors
}
fn on_buffer_changed(&mut self, _: ModelHandle<Buffer>, cx: &mut ViewContext<Self>) {
@@ -2301,263 +2330,60 @@ impl Editor {
}
impl Snapshot {
- pub fn scroll_position(&self) -> Vector2F {
- compute_scroll_position(
- &self.display_snapshot,
- self.scroll_position,
- &self.scroll_top_anchor,
- )
+ pub fn is_empty(&self) -> bool {
+ self.display_snapshot.is_empty()
}
- pub fn max_point(&self) -> DisplayPoint {
- self.display_snapshot.max_point()
+ pub fn is_focused(&self) -> bool {
+ self.is_focused
}
- pub fn longest_row(&self) -> u32 {
- self.display_snapshot.longest_row()
+ pub fn placeholder_text(&self) -> Option<&Arc<str>> {
+ self.placeholder_text.as_ref()
}
- pub fn line_len(&self, display_row: u32) -> u32 {
- self.display_snapshot.line_len(display_row)
+ pub fn buffer_row_count(&self) -> u32 {
+ self.display_snapshot.buffer_row_count()
}
- pub fn font_ascent(&self, font_cache: &FontCache) -> f32 {
- let font_id = font_cache.default_font(self.font_family);
- let ascent = font_cache.metric(font_id, |m| m.ascent);
- font_cache.scale_metric(ascent, font_id, self.font_size)
+ pub fn buffer_rows(&self, start_row: u32) -> BufferRows {
+ self.display_snapshot.buffer_rows(start_row)
}
- pub fn font_descent(&self, font_cache: &FontCache) -> f32 {
- let font_id = font_cache.default_font(self.font_family);
- let descent = font_cache.metric(font_id, |m| m.descent);
- font_cache.scale_metric(descent, font_id, self.font_size)
+ pub fn highlighted_chunks_for_rows(
+ &mut self,
+ display_rows: Range<u32>,
+ ) -> display_map::HighlightedChunks {
+ self.display_snapshot
+ .highlighted_chunks_for_rows(display_rows)
}
- pub fn line_height(&self, font_cache: &FontCache) -> f32 {
- let font_id = font_cache.default_font(self.font_family);
- font_cache.line_height(font_id, self.font_size).ceil()
+ pub fn theme(&self) -> &Arc<Theme> {
+ &self.theme
}
- pub fn em_width(&self, font_cache: &FontCache) -> f32 {
- let font_id = font_cache.default_font(self.font_family);
- font_cache.em_width(font_id, self.font_size)
+ pub fn scroll_position(&self) -> Vector2F {
+ compute_scroll_position(
+ &self.display_snapshot,
+ self.scroll_position,
+ &self.scroll_top_anchor,
+ )
}
- // TODO: Can we make this not return a result?
- pub fn max_line_number_width(
- &self,
- font_cache: &FontCache,
- layout_cache: &TextLayoutCache,
- ) -> Result<f32> {
- let font_size = self.font_size;
- let font_id = font_cache.select_font(self.font_family, &FontProperties::new())?;
- let digit_count = (self.display_snapshot.buffer_row_count() as f32)
- .log10()
- .floor() as usize
- + 1;
-
- Ok(layout_cache
- .layout_str(
- "1".repeat(digit_count).as_str(),
- font_size,
- &[(
- digit_count,
- RunStyle {
- font_id,
- color: Color::black(),
- underline: false,
- },
- )],
- )
- .width())
+ pub fn max_point(&self) -> DisplayPoint {
+ self.display_snapshot.max_point()
}
- pub fn layout_line_numbers(
- &self,
- rows: Range<u32>,
- active_rows: &BTreeMap<u32, bool>,
- font_cache: &FontCache,
- layout_cache: &TextLayoutCache,
- theme: &Theme,
- ) -> Result<Vec<Option<text_layout::Line>>> {
- let font_id = font_cache.select_font(self.font_family, &FontProperties::new())?;
-
- let mut layouts = Vec::with_capacity(rows.len());
- let mut line_number = String::new();
- for (ix, (buffer_row, soft_wrapped)) in self
- .display_snapshot
- .buffer_rows(rows.start)
- .take((rows.end - rows.start) as usize)
- .enumerate()
- {
- let display_row = rows.start + ix as u32;
- let color = if active_rows.contains_key(&display_row) {
- theme.editor.line_number_active
- } else {
- theme.editor.line_number
- };
- if soft_wrapped {
- layouts.push(None);
- } else {
- line_number.clear();
- write!(&mut line_number, "{}", buffer_row + 1).unwrap();
- layouts.push(Some(layout_cache.layout_str(
- &line_number,
- self.font_size,
- &[(
- line_number.len(),
- RunStyle {
- font_id,
- color,
- underline: false,
- },
- )],
- )));
- }
- }
-
- Ok(layouts)
+ pub fn longest_row(&self) -> u32 {
+ self.display_snapshot.longest_row()
}
- pub fn layout_lines(
- &mut self,
- mut rows: Range<u32>,
- style: &EditorStyle,
- font_cache: &FontCache,
- layout_cache: &TextLayoutCache,
- ) -> Result<Vec<text_layout::Line>> {
- rows.end = cmp::min(rows.end, self.display_snapshot.max_point().row() + 1);
- if rows.start >= rows.end {
- return Ok(Vec::new());
- }
-
- // When the editor is empty and unfocused, then show the placeholder.
- if self.display_snapshot.is_empty() && !self.is_focused {
- let placeholder_lines = self
- .placeholder_text
- .as_ref()
- .map_or("", AsRef::as_ref)
- .split('\n')
- .skip(rows.start as usize)
- .take(rows.len());
- let font_id = font_cache
- .select_font(self.font_family, &style.placeholder_text.font_properties)?;
- return Ok(placeholder_lines
- .into_iter()
- .map(|line| {
- layout_cache.layout_str(
- line,
- self.font_size,
- &[(
- line.len(),
- RunStyle {
- font_id,
- color: style.placeholder_text.color,
- underline: false,
- },
- )],
- )
- })
- .collect());
- }
-
- let mut prev_font_properties = FontProperties::new();
- let mut prev_font_id = font_cache
- .select_font(self.font_family, &prev_font_properties)
- .unwrap();
-
- let mut layouts = Vec::with_capacity(rows.len());
- let mut line = String::new();
- let mut styles = Vec::new();
- let mut row = rows.start;
- let mut line_exceeded_max_len = false;
- let chunks = self
- .display_snapshot
- .highlighted_chunks_for_rows(rows.clone());
-
- 'outer: for (chunk, style_ix) in chunks.chain(Some(("\n", HighlightId::default()))) {
- for (ix, mut line_chunk) in chunk.split('\n').enumerate() {
- if ix > 0 {
- layouts.push(layout_cache.layout_str(&line, self.font_size, &styles));
- line.clear();
- styles.clear();
- row += 1;
- line_exceeded_max_len = false;
- if row == rows.end {
- break 'outer;
- }
- }
-
- if !line_chunk.is_empty() && !line_exceeded_max_len {
- let style = self
- .theme
- .syntax
- .highlight_style(style_ix)
- .unwrap_or(style.text.clone());
- // Avoid a lookup if the font properties match the previous ones.
- let font_id = if style.font_properties == prev_font_properties {
- prev_font_id
- } else {
- font_cache.select_font(self.font_family, &style.font_properties)?
- };
-
- if line.len() + line_chunk.len() > MAX_LINE_LEN {
- let mut chunk_len = MAX_LINE_LEN - line.len();
- while !line_chunk.is_char_boundary(chunk_len) {
- chunk_len -= 1;
- }
- line_chunk = &line_chunk[..chunk_len];
- line_exceeded_max_len = true;
- }
-
- line.push_str(line_chunk);
- styles.push((
- line_chunk.len(),
- RunStyle {
- font_id,
- color: style.color,
- underline: style.underline,
- },
- ));
- prev_font_id = font_id;
- prev_font_properties = style.font_properties;
- }
- }
- }
-
- Ok(layouts)
+ pub fn line_len(&self, display_row: u32) -> u32 {
+ self.display_snapshot.line_len(display_row)
}
- pub fn layout_line(
- &self,
- row: u32,
- font_cache: &FontCache,
- layout_cache: &TextLayoutCache,
- ) -> Result<text_layout::Line> {
- let font_id = font_cache.select_font(self.font_family, &FontProperties::new())?;
-
- let mut line = self.display_snapshot.line(row);
-
- if line.len() > MAX_LINE_LEN {
- let mut len = MAX_LINE_LEN;
- while !line.is_char_boundary(len) {
- len -= 1;
- }
- line.truncate(len);
- }
-
- Ok(layout_cache.layout_str(
- &line,
- self.font_size,
- &[(
- self.display_snapshot.line_len(row) as usize,
- RunStyle {
- font_id,
- color: Color::black(),
- underline: false,
- },
- )],
- ))
+ pub fn line(&self, display_row: u32) -> String {
+ self.display_snapshot.line(display_row)
}
pub fn prev_row_boundary(&self, point: DisplayPoint) -> (DisplayPoint, Point) {
@@ -2569,6 +2395,41 @@ impl Snapshot {
}
}
+impl EditorStyle {
+ #[cfg(any(test, feature = "test-support"))]
+ pub fn test(font_cache: &gpui::FontCache) -> Self {
+ let font_family_name = Arc::from("Monaco");
+ let font_properties = Default::default();
+ let font_family_id = font_cache.load_family(&[&font_family_name]).unwrap();
+ let font_id = font_cache
+ .select_font(font_family_id, &font_properties)
+ .unwrap();
+ Self {
+ text: TextStyle {
+ font_family_name,
+ font_family_id,
+ font_id,
+ font_size: 14.,
+ color: Color::from_u32(0xff0000ff),
+ font_properties,
+ underline: false,
+ },
+ placeholder_text: None,
+ background: Default::default(),
+ gutter_background: Default::default(),
+ active_line_background: Default::default(),
+ line_number: Default::default(),
+ line_number_active: Default::default(),
+ selection: Default::default(),
+ guest_selections: Default::default(),
+ }
+ }
+
+ fn placeholder_text(&self) -> &TextStyle {
+ self.placeholder_text.as_ref().unwrap_or(&self.text)
+ }
+}
+
fn compute_scroll_position(
snapshot: &DisplayMapSnapshot,
mut scroll_position: Vector2F,
@@ -2604,10 +2465,10 @@ impl Entity for Editor {
impl View for Editor {
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
- let style = self
- .build_style
- .as_ref()
- .map_or(Default::default(), |build| (build.borrow_mut())(cx));
+ let style = self.build_style.borrow_mut()(cx);
+ self.display_map.update(cx, |map, cx| {
+ map.set_font(style.text.font_id, style.text.font_size, cx)
+ });
EditorElement::new(self.handle.clone(), style).boxed()
}
@@ -2627,7 +2488,7 @@ impl View for Editor {
fn on_blur(&mut self, cx: &mut ViewContext<Self>) {
self.focused = false;
- self.cursors_visible = false;
+ self.show_local_cursors = false;
self.buffer.update(cx, |buffer, cx| {
buffer.set_active_selection_set(None, cx).unwrap();
});
@@ -2659,8 +2520,34 @@ impl workspace::Item for Buffer {
settings: watch::Receiver<Settings>,
cx: &mut ViewContext<Self::View>,
) -> Self::View {
- Editor::for_buffer(handle, settings.clone(), cx)
- .with_style(move |_| settings.borrow().theme.editor.clone())
+ Editor::for_buffer(
+ handle,
+ settings.clone(),
+ move |cx| {
+ let settings = settings.borrow();
+ let font_cache = cx.font_cache();
+ let font_family_id = settings.buffer_font_family;
+ let font_family_name = cx.font_cache().family_name(font_family_id).unwrap();
+ let font_properties = Default::default();
+ let font_id = font_cache
+ .select_font(font_family_id, &font_properties)
+ .unwrap();
+ let font_size = settings.buffer_font_size;
+
+ let mut theme = settings.theme.editor.clone();
+ theme.text = TextStyle {
+ color: theme.text.color,
+ font_family_name,
+ font_family_id,
+ font_id,
+ font_size,
+ font_properties,
+ underline: false,
+ };
+ theme
+ },
+ cx,
+ )
}
}
@@ -2697,10 +2584,14 @@ impl workspace::ItemView for Editor {
where
Self: Sized,
{
- let mut clone = Editor::for_buffer(self.buffer.clone(), self.settings.clone(), cx);
+ let mut clone = Editor::new(
+ self.buffer.clone(),
+ self.settings.clone(),
+ self.build_style.clone(),
+ cx,
+ );
clone.scroll_position = self.scroll_position;
clone.scroll_top_anchor = self.scroll_top_anchor.clone();
- clone.build_style = self.build_style.clone();
Some(clone)
}
@@ -2742,9 +2633,8 @@ mod tests {
fn test_selection_with_mouse(cx: &mut gpui::MutableAppContext) {
let buffer = cx.add_model(|cx| Buffer::new(0, "aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx));
let settings = settings::test(&cx).1;
- let (_, editor) = cx.add_window(Default::default(), |cx| {
- Editor::for_buffer(buffer, settings, cx)
- });
+ let (_, editor) =
+ cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
editor.update(cx, |view, cx| {
view.begin_selection(DisplayPoint::new(2, 2), false, cx);
@@ -2810,9 +2700,7 @@ mod tests {
fn test_canceling_pending_selection(cx: &mut gpui::MutableAppContext) {
let buffer = cx.add_model(|cx| Buffer::new(0, "aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx));
let settings = settings::test(&cx).1;
- let (_, view) = cx.add_window(Default::default(), |cx| {
- Editor::for_buffer(buffer, settings, cx)
- });
+ let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
view.update(cx, |view, cx| {
view.begin_selection(DisplayPoint::new(2, 2), false, cx);
@@ -2844,9 +2732,7 @@ mod tests {
fn test_cancel(cx: &mut gpui::MutableAppContext) {
let buffer = cx.add_model(|cx| Buffer::new(0, "aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx));
let settings = settings::test(&cx).1;
- let (_, view) = cx.add_window(Default::default(), |cx| {
- Editor::for_buffer(buffer, settings, cx)
- });
+ let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
view.update(cx, |view, cx| {
view.begin_selection(DisplayPoint::new(3, 4), false, cx);
@@ -2882,33 +2768,6 @@ mod tests {
});
}
- #[gpui::test]
- fn test_layout_line_numbers(cx: &mut gpui::MutableAppContext) {
- let layout_cache = TextLayoutCache::new(cx.platform().fonts());
- let font_cache = cx.font_cache().clone();
-
- let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(6, 6), cx));
-
- let settings = settings::test(&cx).1;
- let (_, editor) = cx.add_window(Default::default(), |cx| {
- Editor::for_buffer(buffer.clone(), settings.clone(), cx)
- });
-
- let layouts = editor.update(cx, |editor, cx| {
- editor
- .snapshot(cx)
- .layout_line_numbers(
- 0..6,
- &Default::default(),
- &font_cache,
- &layout_cache,
- &settings.borrow().theme,
- )
- .unwrap()
- });
- assert_eq!(layouts.len(), 6);
- }
-
#[gpui::test]
fn test_fold(cx: &mut gpui::MutableAppContext) {
let buffer = cx.add_model(|cx| {
@@ -2937,7 +2796,7 @@ mod tests {
});
let settings = settings::test(&cx).1;
let (_, view) = cx.add_window(Default::default(), |cx| {
- Editor::for_buffer(buffer.clone(), settings, cx)
+ build_editor(buffer.clone(), settings, cx)
});
view.update(cx, |view, cx| {
@@ -3005,7 +2864,7 @@ mod tests {
let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(6, 6), cx));
let settings = settings::test(&cx).1;
let (_, view) = cx.add_window(Default::default(), |cx| {
- Editor::for_buffer(buffer.clone(), settings, cx)
+ build_editor(buffer.clone(), settings, cx)
});
buffer.update(cx, |buffer, cx| {
@@ -3082,7 +2941,7 @@ mod tests {
let buffer = cx.add_model(|cx| Buffer::new(0, "βββββ\nabcde\nαβγδΡ\n", cx));
let settings = settings::test(&cx).1;
let (_, view) = cx.add_window(Default::default(), |cx| {
- Editor::for_buffer(buffer.clone(), settings, cx)
+ build_editor(buffer.clone(), settings, cx)
});
assert_eq!('β'.len_utf8(), 3);
@@ -3140,7 +2999,7 @@ mod tests {
let buffer = cx.add_model(|cx| Buffer::new(0, "βββββ\nabcd\nΞ±Ξ²Ξ³\nabcd\nβββββ\n", cx));
let settings = settings::test(&cx).1;
let (_, view) = cx.add_window(Default::default(), |cx| {
- Editor::for_buffer(buffer.clone(), settings, cx)
+ build_editor(buffer.clone(), settings, cx)
});
view.update(cx, |view, cx| {
view.select_display_ranges(&[empty_range(0, "βββββ".len())], cx)
@@ -3170,9 +3029,7 @@ mod tests {
fn test_beginning_end_of_line(cx: &mut gpui::MutableAppContext) {
let buffer = cx.add_model(|cx| Buffer::new(0, "abc\n def", cx));
let settings = settings::test(&cx).1;
- let (_, view) = cx.add_window(Default::default(), |cx| {
- Editor::for_buffer(buffer, settings, cx)
- });
+ let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
view.update(cx, |view, cx| {
view.select_display_ranges(
&[
@@ -3315,9 +3172,7 @@ mod tests {
let buffer =
cx.add_model(|cx| Buffer::new(0, "use std::str::{foo, bar}\n\n {baz.qux()}", cx));
let settings = settings::test(&cx).1;
- let (_, view) = cx.add_window(Default::default(), |cx| {
- Editor::for_buffer(buffer, settings, cx)
- });
+ let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
view.update(cx, |view, cx| {
view.select_display_ranges(
&[
@@ -3509,9 +3364,7 @@ mod tests {
let buffer =
cx.add_model(|cx| Buffer::new(0, "use one::{\n two::three::four::five\n};", cx));
let settings = settings::test(&cx).1;
- let (_, view) = cx.add_window(Default::default(), |cx| {
- Editor::for_buffer(buffer, settings, cx)
- });
+ let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
view.update(cx, |view, cx| {
view.set_wrap_width(130., cx);
@@ -3572,7 +3425,7 @@ mod tests {
});
let settings = settings::test(&cx).1;
let (_, view) = cx.add_window(Default::default(), |cx| {
- Editor::for_buffer(buffer.clone(), settings, cx)
+ build_editor(buffer.clone(), settings, cx)
});
view.update(cx, |view, cx| {
@@ -3608,7 +3461,7 @@ mod tests {
});
let settings = settings::test(&cx).1;
let (_, view) = cx.add_window(Default::default(), |cx| {
- Editor::for_buffer(buffer.clone(), settings, cx)
+ build_editor(buffer.clone(), settings, cx)
});
view.update(cx, |view, cx| {
@@ -3637,9 +3490,7 @@ mod tests {
fn test_delete_line(cx: &mut gpui::MutableAppContext) {
let settings = settings::test(&cx).1;
let buffer = cx.add_model(|cx| Buffer::new(0, "abc\ndef\nghi\n", cx));
- let (_, view) = cx.add_window(Default::default(), |cx| {
- Editor::for_buffer(buffer, settings, cx)
- });
+ let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
view.update(cx, |view, cx| {
view.select_display_ranges(
&[
@@ -3663,9 +3514,7 @@ mod tests {
let settings = settings::test(&cx).1;
let buffer = cx.add_model(|cx| Buffer::new(0, "abc\ndef\nghi\n", cx));
- let (_, view) = cx.add_window(Default::default(), |cx| {
- Editor::for_buffer(buffer, settings, cx)
- });
+ let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
view.update(cx, |view, cx| {
view.select_display_ranges(&[DisplayPoint::new(2, 0)..DisplayPoint::new(0, 1)], cx)
.unwrap();
@@ -3682,9 +3531,7 @@ mod tests {
fn test_duplicate_line(cx: &mut gpui::MutableAppContext) {
let settings = settings::test(&cx).1;
let buffer = cx.add_model(|cx| Buffer::new(0, "abc\ndef\nghi\n", cx));
- let (_, view) = cx.add_window(Default::default(), |cx| {
- Editor::for_buffer(buffer, settings, cx)
- });
+ let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
view.update(cx, |view, cx| {
view.select_display_ranges(
&[
@@ -3711,9 +3558,7 @@ mod tests {
let settings = settings::test(&cx).1;
let buffer = cx.add_model(|cx| Buffer::new(0, "abc\ndef\nghi\n", cx));
- let (_, view) = cx.add_window(Default::default(), |cx| {
- Editor::for_buffer(buffer, settings, cx)
- });
+ let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
view.update(cx, |view, cx| {
view.select_display_ranges(
&[
@@ -3739,9 +3584,7 @@ mod tests {
fn test_move_line_up_down(cx: &mut gpui::MutableAppContext) {
let settings = settings::test(&cx).1;
let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(10, 5), cx));
- let (_, view) = cx.add_window(Default::default(), |cx| {
- Editor::for_buffer(buffer, settings, cx)
- });
+ let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
view.update(cx, |view, cx| {
view.fold_ranges(
vec![
@@ -3840,7 +3683,7 @@ mod tests {
let settings = settings::test(&cx).1;
let view = cx
.add_window(Default::default(), |cx| {
- Editor::for_buffer(buffer.clone(), settings, cx)
+ build_editor(buffer.clone(), settings, cx)
})
.1;
@@ -3973,9 +3816,7 @@ mod tests {
fn test_select_all(cx: &mut gpui::MutableAppContext) {
let buffer = cx.add_model(|cx| Buffer::new(0, "abc\nde\nfgh", cx));
let settings = settings::test(&cx).1;
- let (_, view) = cx.add_window(Default::default(), |cx| {
- Editor::for_buffer(buffer, settings, cx)
- });
+ let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
view.update(cx, |view, cx| {
view.select_all(&SelectAll, cx);
assert_eq!(
@@ -3989,9 +3830,7 @@ mod tests {
fn test_select_line(cx: &mut gpui::MutableAppContext) {
let settings = settings::test(&cx).1;
let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(6, 5), cx));
- let (_, view) = cx.add_window(Default::default(), |cx| {
- Editor::for_buffer(buffer, settings, cx)
- });
+ let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
view.update(cx, |view, cx| {
view.select_display_ranges(
&[
@@ -4037,9 +3876,7 @@ mod tests {
fn test_split_selection_into_lines(cx: &mut gpui::MutableAppContext) {
let settings = settings::test(&cx).1;
let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(9, 5), cx));
- let (_, view) = cx.add_window(Default::default(), |cx| {
- Editor::for_buffer(buffer, settings, cx)
- });
+ let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
view.update(cx, |view, cx| {
view.fold_ranges(
vec![
@@ -4107,9 +3944,7 @@ mod tests {
fn test_add_selection_above_below(cx: &mut gpui::MutableAppContext) {
let settings = settings::test(&cx).1;
let buffer = cx.add_model(|cx| Buffer::new(0, "abc\ndefghi\n\njk\nlmno\n", cx));
- let (_, view) = cx.add_window(Default::default(), |cx| {
- Editor::for_buffer(buffer, settings, cx)
- });
+ let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
view.update(cx, |view, cx| {
view.select_display_ranges(&[DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)], cx)
@@ -4295,7 +4130,7 @@ mod tests {
let history = History::new(text.into());
Buffer::from_history(0, history, None, lang.cloned(), cx)
});
- let (_, view) = cx.add_window(|cx| Editor::for_buffer(buffer, settings.clone(), cx));
+ let (_, view) = cx.add_window(|cx| build_editor(buffer, settings.clone(), cx));
view.condition(&cx, |view, cx| !view.buffer.read(cx).is_parsing())
.await;
@@ -4433,6 +4268,40 @@ mod tests {
let point = DisplayPoint::new(row as u32, column as u32);
point..point
}
+
+ fn build_editor(
+ buffer: ModelHandle<Buffer>,
+ settings: watch::Receiver<Settings>,
+ cx: &mut ViewContext<Editor>,
+ ) -> Editor {
+ let style = {
+ let font_cache = cx.font_cache();
+ let settings = settings.borrow();
+ EditorStyle {
+ text: TextStyle {
+ color: Default::default(),
+ font_family_name: font_cache.family_name(settings.buffer_font_family).unwrap(),
+ font_family_id: settings.buffer_font_family,
+ font_id: font_cache
+ .select_font(settings.buffer_font_family, &Default::default())
+ .unwrap(),
+ font_size: settings.buffer_font_size,
+ font_properties: Default::default(),
+ underline: false,
+ },
+ placeholder_text: None,
+ background: Default::default(),
+ selection: Default::default(),
+ gutter_background: Default::default(),
+ active_line_background: Default::default(),
+ line_number: Default::default(),
+ line_number_active: Default::default(),
+ guest_selections: Default::default(),
+ }
+ };
+
+ Editor::for_buffer(buffer, settings, move |_| style.clone(), cx)
+ }
}
trait RangeExt<T> {
@@ -714,9 +714,11 @@ impl Buffer {
path: impl Into<Arc<Path>>,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
+ let path = path.into();
let handle = cx.handle();
let text = self.visible_text.clone();
let version = self.version.clone();
+
let save_as = worktree.update(cx, |worktree, cx| {
worktree
.as_local_mut()
@@ -727,6 +729,11 @@ impl Buffer {
cx.spawn(|this, mut cx| async move {
save_as.await.map(|new_file| {
this.update(&mut cx, |this, cx| {
+ if let Some(language) = new_file.select_language(cx) {
+ this.language = Some(language);
+ this.reparse(cx);
+ }
+
let mtime = new_file.mtime;
this.file = Some(new_file);
this.did_save(version, mtime, cx);
@@ -794,6 +801,10 @@ impl Buffer {
cx.emit(Event::FileHandleChanged);
}
+ pub fn language(&self) -> Option<&Arc<Language>> {
+ self.language.as_ref()
+ }
+
pub fn parse_count(&self) -> usize {
self.parse_count
}
@@ -871,7 +882,11 @@ impl Buffer {
cx.spawn(move |this, mut cx| async move {
let new_tree = parse_task.await;
this.update(&mut cx, move |this, cx| {
- let parse_again = this.version > parsed_version;
+ let language_changed =
+ this.language.as_ref().map_or(true, |curr_language| {
+ !Arc::ptr_eq(curr_language, &language)
+ });
+ let parse_again = this.version > parsed_version || language_changed;
*this.syntax_tree.lock() = Some(SyntaxTree {
tree: new_tree,
dirty: false,
@@ -2,14 +2,13 @@ mod fold_map;
mod tab_map;
mod wrap_map;
-use super::{buffer, Anchor, Bias, Buffer, Point, Settings, ToOffset, ToPoint};
+use super::{buffer, Anchor, Bias, Buffer, Point, ToOffset, ToPoint};
use fold_map::FoldMap;
-use gpui::{Entity, ModelContext, ModelHandle};
-use postage::watch;
+use gpui::{fonts::FontId, Entity, ModelContext, ModelHandle};
use std::ops::Range;
use tab_map::TabMap;
-pub use wrap_map::BufferRows;
use wrap_map::WrapMap;
+pub use wrap_map::{BufferRows, HighlightedChunks};
pub struct DisplayMap {
buffer: ModelHandle<Buffer>,
@@ -25,13 +24,16 @@ impl Entity for DisplayMap {
impl DisplayMap {
pub fn new(
buffer: ModelHandle<Buffer>,
- settings: watch::Receiver<Settings>,
+ tab_size: usize,
+ font_id: FontId,
+ font_size: f32,
wrap_width: Option<f32>,
cx: &mut ModelContext<Self>,
) -> Self {
let (fold_map, snapshot) = FoldMap::new(buffer.clone(), cx);
- let (tab_map, snapshot) = TabMap::new(snapshot, settings.borrow().tab_size);
- let wrap_map = cx.add_model(|cx| WrapMap::new(snapshot, settings, wrap_width, cx));
+ let (tab_map, snapshot) = TabMap::new(snapshot, tab_size);
+ let wrap_map =
+ cx.add_model(|cx| WrapMap::new(snapshot, font_id, font_size, wrap_width, cx));
cx.observe(&wrap_map, |_, _, cx| cx.notify()).detach();
DisplayMap {
buffer,
@@ -85,6 +87,11 @@ impl DisplayMap {
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
}
+ pub fn set_font(&self, font_id: FontId, font_size: f32, cx: &mut ModelContext<Self>) {
+ self.wrap_map
+ .update(cx, |map, cx| map.set_font(font_id, font_size, cx));
+ }
+
pub fn set_wrap_width(&self, width: Option<f32>, cx: &mut ModelContext<Self>) -> bool {
self.wrap_map
.update(cx, |map, cx| map.set_wrap_width(width, cx))
@@ -367,12 +374,12 @@ mod tests {
.unwrap_or(10);
let font_cache = cx.font_cache().clone();
- let settings = Settings {
- tab_size: rng.gen_range(1..=4),
- buffer_font_family: font_cache.load_family(&["Helvetica"]).unwrap(),
- buffer_font_size: 14.0,
- ..cx.read(Settings::test)
- };
+ let tab_size = rng.gen_range(1..=4);
+ let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
+ let font_id = font_cache
+ .select_font(family_id, &Default::default())
+ .unwrap();
+ let font_size = 14.0;
let max_wrap_width = 300.0;
let mut wrap_width = if rng.gen_bool(0.1) {
None
@@ -380,7 +387,7 @@ mod tests {
Some(rng.gen_range(0.0..=max_wrap_width))
};
- log::info!("tab size: {}", settings.tab_size);
+ log::info!("tab size: {}", tab_size);
log::info!("wrap width: {:?}", wrap_width);
let buffer = cx.add_model(|cx| {
@@ -388,9 +395,10 @@ mod tests {
let text = RandomCharIter::new(&mut rng).take(len).collect::<String>();
Buffer::new(0, text, cx)
});
- let settings = watch::channel_with(settings).1;
- let map = cx.add_model(|cx| DisplayMap::new(buffer.clone(), settings, wrap_width, cx));
+ let map = cx.add_model(|cx| {
+ DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, wrap_width, cx)
+ });
let (_observer, notifications) = Observer::new(&map, &mut cx);
let mut fold_count = 0;
@@ -529,26 +537,27 @@ mod tests {
}
#[gpui::test]
- async fn test_soft_wraps(mut cx: gpui::TestAppContext) {
+ fn test_soft_wraps(cx: &mut MutableAppContext) {
cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX);
cx.foreground().forbid_parking();
let font_cache = cx.font_cache();
- let settings = Settings {
- buffer_font_family: font_cache.load_family(&["Helvetica"]).unwrap(),
- buffer_font_size: 12.0,
- tab_size: 4,
- ..cx.read(Settings::test)
- };
+ let tab_size = 4;
+ let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
+ let font_id = font_cache
+ .select_font(family_id, &Default::default())
+ .unwrap();
+ let font_size = 12.0;
let wrap_width = Some(64.);
let text = "one two three four five\nsix seven eight";
let buffer = cx.add_model(|cx| Buffer::new(0, text.to_string(), cx));
- let (mut settings_tx, settings_rx) = watch::channel_with(settings);
- let map = cx.add_model(|cx| DisplayMap::new(buffer.clone(), settings_rx, wrap_width, cx));
+ let map = cx.add_model(|cx| {
+ DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, wrap_width, cx)
+ });
- let snapshot = map.update(&mut cx, |map, cx| map.snapshot(cx));
+ let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
assert_eq!(
snapshot.chunks_at(0).collect::<String>(),
"one two \nthree four \nfive\nsix seven \neight"
@@ -592,23 +601,21 @@ mod tests {
(DisplayPoint::new(2, 4), SelectionGoal::Column(10))
);
- buffer.update(&mut cx, |buffer, cx| {
+ buffer.update(cx, |buffer, cx| {
let ix = buffer.text().find("seven").unwrap();
buffer.edit(vec![ix..ix], "and ", cx);
});
- let snapshot = map.update(&mut cx, |map, cx| map.snapshot(cx));
+ let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
assert_eq!(
snapshot.chunks_at(1).collect::<String>(),
"three four \nfive\nsix and \nseven eight"
);
// Re-wrap on font size changes
- settings_tx.borrow_mut().buffer_font_size += 3.;
-
- map.next_notification(&mut cx).await;
+ map.update(cx, |map, cx| map.set_font(font_id, font_size + 3., cx));
- let snapshot = map.update(&mut cx, |map, cx| map.snapshot(cx));
+ let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
assert_eq!(
snapshot.chunks_at(1).collect::<String>(),
"three \nfour five\nsix and \nseven \neight"
@@ -619,11 +626,16 @@ mod tests {
fn test_chunks_at(cx: &mut gpui::MutableAppContext) {
let text = sample_text(6, 6);
let buffer = cx.add_model(|cx| Buffer::new(0, text, cx));
- let settings = watch::channel_with(Settings {
- tab_size: 4,
- ..Settings::test(cx)
+ let tab_size = 4;
+ let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
+ let font_id = cx
+ .font_cache()
+ .select_font(family_id, &Default::default())
+ .unwrap();
+ let font_size = 14.0;
+ let map = cx.add_model(|cx| {
+ DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, None, cx)
});
- let map = cx.add_model(|cx| DisplayMap::new(buffer.clone(), settings.1, None, cx));
buffer.update(cx, |buffer, cx| {
buffer.edit(
vec![
@@ -695,13 +707,16 @@ mod tests {
});
buffer.condition(&cx, |buf, _| !buf.is_parsing()).await;
- let settings = cx.update(|cx| {
- watch::channel_with(Settings {
- tab_size: 2,
- ..Settings::test(cx)
- })
- });
- let map = cx.add_model(|cx| DisplayMap::new(buffer, settings.1, None, cx));
+ let tab_size = 2;
+ let font_cache = cx.font_cache();
+ let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
+ let font_id = font_cache
+ .select_font(family_id, &Default::default())
+ .unwrap();
+ let font_size = 14.0;
+
+ let map =
+ cx.add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, cx));
assert_eq!(
cx.update(|cx| highlighted_chunks(0..5, &map, &theme, cx)),
vec![
@@ -782,15 +797,16 @@ mod tests {
buffer.condition(&cx, |buf, _| !buf.is_parsing()).await;
let font_cache = cx.font_cache();
- let settings = cx.update(|cx| {
- watch::channel_with(Settings {
- tab_size: 4,
- buffer_font_family: font_cache.load_family(&["Courier"]).unwrap(),
- buffer_font_size: 16.0,
- ..Settings::test(cx)
- })
- });
- let map = cx.add_model(|cx| DisplayMap::new(buffer, settings.1, Some(40.0), cx));
+
+ let tab_size = 4;
+ let family_id = font_cache.load_family(&["Courier"]).unwrap();
+ let font_id = font_cache
+ .select_font(family_id, &Default::default())
+ .unwrap();
+ let font_size = 16.0;
+
+ let map = cx
+ .add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, Some(40.0), cx));
assert_eq!(
cx.update(|cx| highlighted_chunks(0..5, &map, &theme, cx)),
[
@@ -825,11 +841,17 @@ mod tests {
let text = "\n'a', 'Ξ±',\t'β',\t'β', 'π'\n";
let display_text = "\n'a', 'Ξ±', 'β', 'β', 'π'\n";
let buffer = cx.add_model(|cx| Buffer::new(0, text, cx));
- let settings = watch::channel_with(Settings {
- tab_size: 4,
- ..Settings::test(cx)
+
+ let tab_size = 4;
+ let font_cache = cx.font_cache();
+ let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
+ let font_id = font_cache
+ .select_font(family_id, &Default::default())
+ .unwrap();
+ let font_size = 14.0;
+ let map = cx.add_model(|cx| {
+ DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, None, cx)
});
- let map = cx.add_model(|cx| DisplayMap::new(buffer.clone(), settings.1, None, cx));
let map = map.update(cx, |map, cx| map.snapshot(cx));
assert_eq!(map.text(), display_text);
@@ -863,11 +885,17 @@ mod tests {
fn test_tabs_with_multibyte_chars(cx: &mut gpui::MutableAppContext) {
let text = "β
\t\tΞ±\nΞ²\t\nπΞ²\t\tΞ³";
let buffer = cx.add_model(|cx| Buffer::new(0, text, cx));
- let settings = watch::channel_with(Settings {
- tab_size: 4,
- ..Settings::test(cx)
+ let tab_size = 4;
+ let font_cache = cx.font_cache();
+ let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
+ let font_id = font_cache
+ .select_font(family_id, &Default::default())
+ .unwrap();
+ let font_size = 14.0;
+
+ let map = cx.add_model(|cx| {
+ DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, None, cx)
});
- let map = cx.add_model(|cx| DisplayMap::new(buffer.clone(), settings.1, None, cx));
let map = map.update(cx, |map, cx| map.snapshot(cx));
assert_eq!(map.text(), "β
Ξ±\nΞ² \nπΞ² Ξ³");
assert_eq!(
@@ -924,11 +952,16 @@ mod tests {
#[gpui::test]
fn test_max_point(cx: &mut gpui::MutableAppContext) {
let buffer = cx.add_model(|cx| Buffer::new(0, "aaa\n\t\tbbb", cx));
- let settings = watch::channel_with(Settings {
- tab_size: 4,
- ..Settings::test(cx)
+ let tab_size = 4;
+ let font_cache = cx.font_cache();
+ let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
+ let font_id = font_cache
+ .select_font(family_id, &Default::default())
+ .unwrap();
+ let font_size = 14.0;
+ let map = cx.add_model(|cx| {
+ DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, None, cx)
});
- let map = cx.add_model(|cx| DisplayMap::new(buffer.clone(), settings.1, None, cx));
assert_eq!(
map.update(cx, |map, cx| map.snapshot(cx)).max_point(),
DisplayPoint::new(1, 11)
@@ -2,14 +2,14 @@ use super::{
fold_map,
tab_map::{self, Edit as TabEdit, Snapshot as TabSnapshot, TabPoint, TextSummary},
};
-use crate::{editor::Point, settings::HighlightId, util::Bias, Settings};
+use crate::{editor::Point, settings::HighlightId, util::Bias};
use gpui::{
+ fonts::FontId,
sum_tree::{self, Cursor, SumTree},
text_layout::LineWrapper,
Entity, ModelContext, Task,
};
use lazy_static::lazy_static;
-use postage::{prelude::Stream, watch};
use smol::future::yield_now;
use std::{collections::VecDeque, ops::Range, time::Duration};
@@ -18,8 +18,7 @@ pub struct WrapMap {
pending_edits: VecDeque<(TabSnapshot, Vec<TabEdit>)>,
wrap_width: Option<f32>,
background_task: Option<Task<()>>,
- _watch_settings: Task<()>,
- settings: watch::Receiver<Settings>,
+ font: (FontId, f32),
}
impl Entity for WrapMap {
@@ -76,36 +75,17 @@ pub struct BufferRows<'a> {
impl WrapMap {
pub fn new(
tab_snapshot: TabSnapshot,
- settings: watch::Receiver<Settings>,
+ font_id: FontId,
+ font_size: f32,
wrap_width: Option<f32>,
cx: &mut ModelContext<Self>,
) -> Self {
- let _watch_settings = cx.spawn_weak({
- let mut prev_font = (
- settings.borrow().buffer_font_size,
- settings.borrow().buffer_font_family,
- );
- let mut settings = settings.clone();
- move |this, mut cx| async move {
- while let Some(settings) = settings.recv().await {
- if let Some(this) = this.upgrade(&cx) {
- let font = (settings.buffer_font_size, settings.buffer_font_family);
- if font != prev_font {
- prev_font = font;
- this.update(&mut cx, |this, cx| this.rewrap(cx));
- }
- }
- }
- }
- });
-
let mut this = Self {
+ font: (font_id, font_size),
wrap_width: None,
pending_edits: Default::default(),
snapshot: Snapshot::new(tab_snapshot),
- settings,
background_task: None,
- _watch_settings,
};
this.set_wrap_width(wrap_width, cx);
@@ -128,6 +108,13 @@ impl WrapMap {
self.snapshot.clone()
}
+ pub fn set_font(&mut self, font_id: FontId, font_size: f32, cx: &mut ModelContext<Self>) {
+ if (font_id, font_size) != self.font {
+ self.font = (font_id, font_size);
+ self.rewrap(cx)
+ }
+ }
+
pub fn set_wrap_width(&mut self, wrap_width: Option<f32>, cx: &mut ModelContext<Self>) -> bool {
if wrap_width == self.wrap_width {
return false;
@@ -144,15 +131,9 @@ impl WrapMap {
if let Some(wrap_width) = self.wrap_width {
let mut new_snapshot = self.snapshot.clone();
let font_cache = cx.font_cache().clone();
- let settings = self.settings.clone();
+ let (font_id, font_size) = self.font;
let task = cx.background().spawn(async move {
- let mut line_wrapper = {
- let settings = settings.borrow();
- let font_id = font_cache
- .select_font(settings.buffer_font_family, &Default::default())
- .unwrap();
- font_cache.line_wrapper(font_id, settings.buffer_font_size)
- };
+ let mut line_wrapper = font_cache.line_wrapper(font_id, font_size);
let tab_snapshot = new_snapshot.tab_snapshot.clone();
let range = TabPoint::zero()..tab_snapshot.max_point();
new_snapshot
@@ -222,15 +203,9 @@ impl WrapMap {
let pending_edits = self.pending_edits.clone();
let mut snapshot = self.snapshot.clone();
let font_cache = cx.font_cache().clone();
- let settings = self.settings.clone();
+ let (font_id, font_size) = self.font;
let update_task = cx.background().spawn(async move {
- let mut line_wrapper = {
- let settings = settings.borrow();
- let font_id = font_cache
- .select_font(settings.buffer_font_family, &Default::default())
- .unwrap();
- font_cache.line_wrapper(font_id, settings.buffer_font_size)
- };
+ let mut line_wrapper = font_cache.line_wrapper(font_id, font_size);
for (tab_snapshot, edits) in pending_edits {
snapshot
@@ -950,13 +925,14 @@ mod tests {
} else {
Some(rng.gen_range(0.0..=1000.0))
};
- let settings = Settings {
- tab_size: rng.gen_range(1..=4),
- buffer_font_family: font_cache.load_family(&["Helvetica"]).unwrap(),
- buffer_font_size: 14.0,
- ..cx.read(Settings::test)
- };
- log::info!("Tab size: {}", settings.tab_size);
+ let tab_size = rng.gen_range(1..=4);
+ let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
+ let font_id = font_cache
+ .select_font(family_id, &Default::default())
+ .unwrap();
+ let font_size = 14.0;
+
+ log::info!("Tab size: {}", tab_size);
log::info!("Wrap width: {:?}", wrap_width);
let buffer = cx.add_model(|cx| {
@@ -965,7 +941,7 @@ mod tests {
Buffer::new(0, text, cx)
});
let (mut fold_map, folds_snapshot) = cx.read(|cx| FoldMap::new(buffer.clone(), cx));
- let (tab_map, tabs_snapshot) = TabMap::new(folds_snapshot.clone(), settings.tab_size);
+ let (tab_map, tabs_snapshot) = TabMap::new(folds_snapshot.clone(), tab_size);
log::info!(
"Unwrapped text (no folds): {:?}",
buffer.read_with(&cx, |buf, _| buf.text())
@@ -976,16 +952,13 @@ mod tests {
);
log::info!("Unwrapped text (expanded tabs): {:?}", tabs_snapshot.text());
- let font_id = font_cache
- .select_font(settings.buffer_font_family, &Default::default())
- .unwrap();
- let mut line_wrapper = LineWrapper::new(font_id, settings.buffer_font_size, font_system);
+ let mut line_wrapper = LineWrapper::new(font_id, font_size, font_system);
let unwrapped_text = tabs_snapshot.text();
let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper);
- let settings = watch::channel_with(settings).1;
- let wrap_map = cx
- .add_model(|cx| WrapMap::new(tabs_snapshot.clone(), settings.clone(), wrap_width, cx));
+ let wrap_map = cx.add_model(|cx| {
+ WrapMap::new(tabs_snapshot.clone(), font_id, font_size, wrap_width, cx)
+ });
let (_observer, notifications) = Observer::new(&wrap_map, &mut cx);
if wrap_map.read_with(&cx, |map, _| map.is_rewrapping()) {
@@ -1,5 +1,8 @@
-use super::{DisplayPoint, Editor, EditorMode, Insert, Scroll, Select, SelectPhase, Snapshot};
-use crate::{theme::EditorStyle, time::ReplicaId};
+use super::{
+ DisplayPoint, Editor, EditorMode, EditorStyle, Insert, Scroll, Select, SelectPhase, Snapshot,
+ MAX_LINE_LEN,
+};
+use crate::{theme::HighlightId, time::ReplicaId};
use gpui::{
color::Color,
geometry::{
@@ -9,7 +12,7 @@ use gpui::{
},
json::{self, ToJson},
keymap::Keystroke,
- text_layout::{self, TextLayoutCache},
+ text_layout::{self, RunStyle, TextLayoutCache},
AppContext, Axis, Border, Element, Event, EventContext, FontCache, LayoutContext,
MutableAppContext, PaintContext, Quad, Scene, SizeConstraint, ViewContext, WeakViewHandle,
};
@@ -18,6 +21,7 @@ use smallvec::SmallVec;
use std::{
cmp::{self, Ordering},
collections::{BTreeMap, HashMap},
+ fmt::Write,
ops::Range,
};
@@ -265,6 +269,7 @@ impl EditorElement {
let view = self.view(cx.app);
let settings = self.view(cx.app).settings.borrow();
let theme = &settings.theme.editor;
+ let local_replica_id = view.replica_id(cx);
let scroll_position = layout.snapshot.scroll_position();
let start_row = scroll_position.y() as u32;
let scroll_top = scroll_position.y() * layout.line_height;
@@ -334,7 +339,7 @@ impl EditorElement {
selection.paint(bounds, cx.scene);
}
- if view.cursors_visible() {
+ if view.show_local_cursors() || *replica_id != local_replica_id {
let cursor_position = selection.end;
if (start_row..end_row).contains(&cursor_position.row()) {
let cursor_row_layout =
@@ -374,6 +379,176 @@ impl EditorElement {
cx.scene.pop_layer();
}
+
+ fn max_line_number_width(&self, snapshot: &Snapshot, cx: &LayoutContext) -> f32 {
+ let digit_count = (snapshot.buffer_row_count() as f32).log10().floor() as usize + 1;
+
+ cx.text_layout_cache
+ .layout_str(
+ "1".repeat(digit_count).as_str(),
+ self.style.text.font_size,
+ &[(
+ digit_count,
+ RunStyle {
+ font_id: self.style.text.font_id,
+ color: Color::black(),
+ underline: false,
+ },
+ )],
+ )
+ .width()
+ }
+
+ fn layout_line_numbers(
+ &self,
+ rows: Range<u32>,
+ active_rows: &BTreeMap<u32, bool>,
+ snapshot: &Snapshot,
+ cx: &LayoutContext,
+ ) -> Vec<Option<text_layout::Line>> {
+ let mut layouts = Vec::with_capacity(rows.len());
+ let mut line_number = String::new();
+ for (ix, (buffer_row, soft_wrapped)) in snapshot
+ .buffer_rows(rows.start)
+ .take((rows.end - rows.start) as usize)
+ .enumerate()
+ {
+ let display_row = rows.start + ix as u32;
+ let color = if active_rows.contains_key(&display_row) {
+ self.style.line_number_active
+ } else {
+ self.style.line_number
+ };
+ if soft_wrapped {
+ layouts.push(None);
+ } else {
+ line_number.clear();
+ write!(&mut line_number, "{}", buffer_row + 1).unwrap();
+ layouts.push(Some(cx.text_layout_cache.layout_str(
+ &line_number,
+ self.style.text.font_size,
+ &[(
+ line_number.len(),
+ RunStyle {
+ font_id: self.style.text.font_id,
+ color,
+ underline: false,
+ },
+ )],
+ )));
+ }
+ }
+
+ layouts
+ }
+
+ fn layout_lines(
+ &mut self,
+ mut rows: Range<u32>,
+ snapshot: &mut Snapshot,
+ cx: &LayoutContext,
+ ) -> Vec<text_layout::Line> {
+ rows.end = cmp::min(rows.end, snapshot.max_point().row() + 1);
+ if rows.start >= rows.end {
+ return Vec::new();
+ }
+
+ // When the editor is empty and unfocused, then show the placeholder.
+ if snapshot.is_empty() && !snapshot.is_focused() {
+ let placeholder_style = self.style.placeholder_text();
+ let placeholder_text = snapshot.placeholder_text();
+ let placeholder_lines = placeholder_text
+ .as_ref()
+ .map_or("", AsRef::as_ref)
+ .split('\n')
+ .skip(rows.start as usize)
+ .take(rows.len());
+ return placeholder_lines
+ .map(|line| {
+ cx.text_layout_cache.layout_str(
+ line,
+ placeholder_style.font_size,
+ &[(
+ line.len(),
+ RunStyle {
+ font_id: placeholder_style.font_id,
+ color: placeholder_style.color,
+ underline: false,
+ },
+ )],
+ )
+ })
+ .collect();
+ }
+
+ let mut prev_font_properties = self.style.text.font_properties.clone();
+ let mut prev_font_id = self.style.text.font_id;
+
+ let theme = snapshot.theme().clone();
+ let mut layouts = Vec::with_capacity(rows.len());
+ let mut line = String::new();
+ let mut styles = Vec::new();
+ let mut row = rows.start;
+ let mut line_exceeded_max_len = false;
+ let chunks = snapshot.highlighted_chunks_for_rows(rows.clone());
+
+ 'outer: for (chunk, style_ix) in chunks.chain(Some(("\n", HighlightId::default()))) {
+ for (ix, mut line_chunk) in chunk.split('\n').enumerate() {
+ if ix > 0 {
+ layouts.push(cx.text_layout_cache.layout_str(
+ &line,
+ self.style.text.font_size,
+ &styles,
+ ));
+ line.clear();
+ styles.clear();
+ row += 1;
+ line_exceeded_max_len = false;
+ if row == rows.end {
+ break 'outer;
+ }
+ }
+
+ if !line_chunk.is_empty() && !line_exceeded_max_len {
+ let style = theme
+ .syntax
+ .highlight_style(style_ix)
+ .unwrap_or(self.style.text.clone().into());
+ // Avoid a lookup if the font properties match the previous ones.
+ let font_id = if style.font_properties == prev_font_properties {
+ prev_font_id
+ } else {
+ cx.font_cache
+ .select_font(self.style.text.font_family_id, &style.font_properties)
+ .unwrap_or(self.style.text.font_id)
+ };
+
+ if line.len() + line_chunk.len() > MAX_LINE_LEN {
+ let mut chunk_len = MAX_LINE_LEN - line.len();
+ while !line_chunk.is_char_boundary(chunk_len) {
+ chunk_len -= 1;
+ }
+ line_chunk = &line_chunk[..chunk_len];
+ line_exceeded_max_len = true;
+ }
+
+ line.push_str(line_chunk);
+ styles.push((
+ line_chunk.len(),
+ RunStyle {
+ font_id,
+ color: style.color,
+ underline: style.underline,
+ },
+ ));
+ prev_font_id = font_id;
+ prev_font_properties = style.font_properties;
+ }
+ }
+ }
+
+ layouts
+ }
}
impl Element for EditorElement {
@@ -390,30 +565,22 @@ impl Element for EditorElement {
unimplemented!("we don't yet handle an infinite width constraint on buffer elements");
}
- let font_cache = &cx.font_cache;
- let layout_cache = &cx.text_layout_cache;
let snapshot = self.snapshot(cx.app);
- let line_height = snapshot.line_height(font_cache);
+ let line_height = self.style.text.line_height(cx.font_cache);
let gutter_padding;
let gutter_width;
if snapshot.mode == EditorMode::Full {
- gutter_padding = snapshot.em_width(cx.font_cache);
- match snapshot.max_line_number_width(cx.font_cache, cx.text_layout_cache) {
- Err(error) => {
- log::error!("error computing max line number width: {}", error);
- return (size, None);
- }
- Ok(width) => gutter_width = width + gutter_padding * 2.0,
- }
+ gutter_padding = self.style.text.em_width(cx.font_cache);
+ gutter_width = self.max_line_number_width(&snapshot, cx) + gutter_padding * 2.0;
} else {
gutter_padding = 0.0;
gutter_width = 0.0
};
let text_width = size.x() - gutter_width;
- let text_offset = vec2f(-snapshot.font_descent(cx.font_cache), 0.);
- let em_width = snapshot.em_width(font_cache);
+ let text_offset = vec2f(-self.style.text.descent(cx.font_cache), 0.);
+ let em_width = self.style.text.em_width(cx.font_cache);
let overscroll = vec2f(em_width, 0.);
let wrap_width = text_width - text_offset.x() - overscroll.x() - em_width;
let snapshot = self.update_view(cx.app, |view, cx| {
@@ -488,51 +655,18 @@ impl Element for EditorElement {
});
let line_number_layouts = if snapshot.mode == EditorMode::Full {
- let settings = self
- .view
- .upgrade(cx.app)
- .unwrap()
- .read(cx.app)
- .settings
- .borrow();
- match snapshot.layout_line_numbers(
- start_row..end_row,
- &active_rows,
- cx.font_cache,
- cx.text_layout_cache,
- &settings.theme,
- ) {
- Err(error) => {
- log::error!("error laying out line numbers: {}", error);
- return (size, None);
- }
- Ok(layouts) => layouts,
- }
+ self.layout_line_numbers(start_row..end_row, &active_rows, &snapshot, cx)
} else {
Vec::new()
};
let mut max_visible_line_width = 0.0;
- let line_layouts = match snapshot.layout_lines(
- start_row..end_row,
- &self.style,
- font_cache,
- layout_cache,
- ) {
- Err(error) => {
- log::error!("error laying out lines: {}", error);
- return (size, None);
- }
- Ok(layouts) => {
- for line in &layouts {
- if line.width() > max_visible_line_width {
- max_visible_line_width = line.width();
- }
- }
-
- layouts
+ let line_layouts = self.layout_lines(start_row..end_row, &mut snapshot, cx);
+ for line in &line_layouts {
+ if line.width() > max_visible_line_width {
+ max_visible_line_width = line.width();
}
- };
+ }
let mut layout = LayoutState {
size,
@@ -542,6 +676,7 @@ impl Element for EditorElement {
overscroll,
text_offset,
snapshot,
+ style: self.style.clone(),
active_rows,
line_layouts,
line_number_layouts,
@@ -551,15 +686,18 @@ impl Element for EditorElement {
max_visible_line_width,
};
+ let scroll_max = layout.scroll_max(cx.font_cache, cx.text_layout_cache).x();
+ let scroll_width = layout.scroll_width(cx.text_layout_cache);
+ let max_glyph_width = self.style.text.em_width(&cx.font_cache);
self.update_view(cx.app, |view, cx| {
- let clamped = view.clamp_scroll_left(layout.scroll_max(font_cache, layout_cache).x());
+ let clamped = view.clamp_scroll_left(scroll_max);
let autoscrolled;
if autoscroll_horizontally {
autoscrolled = view.autoscroll_horizontally(
start_row,
layout.text_size.x(),
- layout.scroll_width(font_cache, layout_cache),
- layout.snapshot.em_width(font_cache),
+ scroll_width,
+ max_glyph_width,
&layout.line_layouts,
cx,
);
@@ -659,6 +797,7 @@ pub struct LayoutState {
gutter_size: Vector2F,
gutter_padding: f32,
text_size: Vector2F,
+ style: EditorStyle,
snapshot: Snapshot,
active_rows: BTreeMap<u32, bool>,
line_layouts: Vec<text_layout::Line>,
@@ -672,20 +811,16 @@ pub struct LayoutState {
}
impl LayoutState {
- fn scroll_width(&self, font_cache: &FontCache, layout_cache: &TextLayoutCache) -> f32 {
+ fn scroll_width(&self, layout_cache: &TextLayoutCache) -> f32 {
let row = self.snapshot.longest_row();
- let longest_line_width = self
- .snapshot
- .layout_line(row, font_cache, layout_cache)
- .unwrap()
- .width();
+ let longest_line_width = self.layout_line(row, &self.snapshot, layout_cache).width();
longest_line_width.max(self.max_visible_line_width) + self.overscroll.x()
}
fn scroll_max(&self, font_cache: &FontCache, layout_cache: &TextLayoutCache) -> Vector2F {
let text_width = self.text_size.x();
- let scroll_width = self.scroll_width(font_cache, layout_cache);
- let em_width = self.snapshot.em_width(font_cache);
+ let scroll_width = self.scroll_width(layout_cache);
+ let em_width = self.style.text.em_width(font_cache);
let max_row = self.snapshot.max_point().row();
vec2f(
@@ -693,6 +828,36 @@ impl LayoutState {
max_row.saturating_sub(1) as f32,
)
}
+
+ pub fn layout_line(
+ &self,
+ row: u32,
+ snapshot: &Snapshot,
+ layout_cache: &TextLayoutCache,
+ ) -> text_layout::Line {
+ let mut line = snapshot.line(row);
+
+ if line.len() > MAX_LINE_LEN {
+ let mut len = MAX_LINE_LEN;
+ while !line.is_char_boundary(len) {
+ len -= 1;
+ }
+ line.truncate(len);
+ }
+
+ layout_cache.layout_str(
+ &line,
+ self.style.text.font_size,
+ &[(
+ snapshot.line_len(row) as usize,
+ RunStyle {
+ font_id: self.style.text.font_id,
+ color: Color::black(),
+ underline: false,
+ },
+ )],
+ )
+ }
}
pub struct PaintState {
@@ -864,3 +1029,42 @@ fn scale_vertical_mouse_autoscroll_delta(delta: f32) -> f32 {
fn scale_horizontal_mouse_autoscroll_delta(delta: f32) -> f32 {
delta.powf(1.2) / 300.0
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::{
+ editor::{Buffer, Editor, EditorStyle},
+ settings,
+ test::sample_text,
+ };
+
+ #[gpui::test]
+ fn test_layout_line_numbers(cx: &mut gpui::MutableAppContext) {
+ let font_cache = cx.font_cache().clone();
+ let settings = settings::test(&cx).1;
+ let style = EditorStyle::test(&font_cache);
+
+ let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(6, 6), cx));
+ let (window_id, editor) = cx.add_window(Default::default(), |cx| {
+ Editor::for_buffer(
+ buffer,
+ settings.clone(),
+ {
+ let style = style.clone();
+ move |_| style.clone()
+ },
+ cx,
+ )
+ });
+ let element = EditorElement::new(editor.downgrade(), style);
+
+ let layouts = editor.update(cx, |editor, cx| {
+ let snapshot = editor.snapshot(cx);
+ let mut presenter = cx.build_presenter(window_id, 30.);
+ let mut layout_cx = presenter.build_layout_context(false, cx);
+ element.layout_line_numbers(0..6, &Default::default(), &snapshot, &mut layout_cx)
+ });
+ assert_eq!(layouts.len(), 6);
+ }
+}
@@ -180,16 +180,21 @@ fn char_kind(c: char) -> CharKind {
#[cfg(test)]
mod tests {
use super::*;
- use crate::{
- editor::{display_map::DisplayMap, Buffer},
- test::test_app_state,
- };
+ use crate::editor::{display_map::DisplayMap, Buffer};
#[gpui::test]
fn test_prev_next_word_boundary_multibyte(cx: &mut gpui::MutableAppContext) {
- let settings = test_app_state(cx).settings.clone();
+ let tab_size = 4;
+ let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
+ let font_id = cx
+ .font_cache()
+ .select_font(family_id, &Default::default())
+ .unwrap();
+ let font_size = 14.0;
+
let buffer = cx.add_model(|cx| Buffer::new(0, "a bcΞ defΞ³", cx));
- let display_map = cx.add_model(|cx| DisplayMap::new(buffer, settings, None, cx));
+ let display_map =
+ cx.add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, cx));
let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
assert_eq!(
prev_word_boundary(&snapshot, DisplayPoint::new(0, 12)).unwrap(),
@@ -275,10 +275,14 @@ impl FileFinder {
cx.observe(&workspace, Self::workspace_updated).detach();
let query_editor = cx.add_view(|cx| {
- Editor::single_line(settings.clone(), cx).with_style({
- let settings = settings.clone();
- move |_| settings.borrow().theme.selector.input_editor.as_editor()
- })
+ Editor::single_line(
+ settings.clone(),
+ {
+ let settings = settings.clone();
+ move |_| settings.borrow().theme.selector.input_editor.as_editor()
+ },
+ cx,
+ )
});
cx.subscribe(&query_editor, Self::on_query_editor_event)
.detach();
@@ -35,6 +35,10 @@ pub struct LanguageRegistry {
}
impl Language {
+ pub fn name(&self) -> &str {
+ self.config.name.as_str()
+ }
+
pub fn highlight_map(&self) -> HighlightMap {
self.highlight_map.lock().clone()
}
@@ -133,27 +137,26 @@ mod tests {
// matching file extension
assert_eq!(
- registry.select_language("zed/lib.rs").map(get_name),
+ registry.select_language("zed/lib.rs").map(|l| l.name()),
Some("Rust")
);
assert_eq!(
- registry.select_language("zed/lib.mk").map(get_name),
+ registry.select_language("zed/lib.mk").map(|l| l.name()),
Some("Make")
);
// matching filename
assert_eq!(
- registry.select_language("zed/Makefile").map(get_name),
+ registry.select_language("zed/Makefile").map(|l| l.name()),
Some("Make")
);
// matching suffix that is not the full file extension or filename
- assert_eq!(registry.select_language("zed/cars").map(get_name), None);
- assert_eq!(registry.select_language("zed/a.cars").map(get_name), None);
- assert_eq!(registry.select_language("zed/sumk").map(get_name), None);
-
- fn get_name(language: &Arc<Language>) -> &str {
- language.config.name.as_str()
- }
+ assert_eq!(registry.select_language("zed/cars").map(|l| l.name()), None);
+ assert_eq!(
+ registry.select_language("zed/a.cars").map(|l| l.name()),
+ None
+ );
+ assert_eq!(registry.select_language("zed/sumk").map(|l| l.name()), None);
}
}
@@ -417,11 +417,11 @@ impl Client {
if let Some(extract_entity_id) =
state.entity_id_extractors.get(&message.payload_type_id())
{
+ let payload_type_id = message.payload_type_id();
let entity_id = (extract_entity_id)(message.as_ref());
- if let Some(handler) = state
- .model_handlers
- .get_mut(&(message.payload_type_id(), entity_id))
- {
+ let handler_key = (payload_type_id, entity_id);
+ if let Some(mut handler) = state.model_handlers.remove(&handler_key) {
+ drop(state); // Avoid deadlocks if the handler interacts with rpc::Client
let start_time = Instant::now();
log::info!("RPC client message {}", message.payload_type_name());
(handler)(message, &mut cx);
@@ -429,6 +429,10 @@ impl Client {
"RPC message handled. duration:{:?}",
start_time.elapsed()
);
+ this.state
+ .write()
+ .model_handlers
+ .insert(handler_key, handler);
} else {
log::info!("unhandled message {}", message.payload_type_name());
}
@@ -2,6 +2,7 @@ mod highlight_map;
mod resolution;
mod theme_registry;
+use crate::editor::{EditorStyle, SelectionStyle};
use anyhow::Result;
use gpui::{
color::Color,
@@ -169,35 +170,16 @@ pub struct ContainedLabel {
pub label: LabelStyle,
}
-#[derive(Clone, Deserialize)]
-pub struct EditorStyle {
- pub text: HighlightStyle,
- #[serde(default)]
- pub placeholder_text: HighlightStyle,
- pub background: Color,
- pub selection: SelectionStyle,
- pub gutter_background: Color,
- pub active_line_background: Color,
- pub line_number: Color,
- pub line_number_active: Color,
- pub guest_selections: Vec<SelectionStyle>,
-}
-
#[derive(Clone, Deserialize)]
pub struct InputEditorStyle {
#[serde(flatten)]
pub container: ContainerStyle,
- pub text: HighlightStyle,
- pub placeholder_text: HighlightStyle,
+ pub text: TextStyle,
+ #[serde(default)]
+ pub placeholder_text: Option<TextStyle>,
pub selection: SelectionStyle,
}
-#[derive(Clone, Copy, Default, Deserialize)]
-pub struct SelectionStyle {
- pub cursor: Color,
- pub selection: Color,
-}
-
impl SyntaxTheme {
pub fn new(highlights: Vec<(String, HighlightStyle)>) -> Self {
Self { highlights }
@@ -215,30 +197,6 @@ impl SyntaxTheme {
}
}
-impl Default for EditorStyle {
- fn default() -> Self {
- Self {
- text: HighlightStyle {
- color: Color::from_u32(0xff0000ff),
- font_properties: Default::default(),
- underline: false,
- },
- placeholder_text: HighlightStyle {
- color: Color::from_u32(0x00ff00ff),
- font_properties: Default::default(),
- underline: false,
- },
- background: Default::default(),
- gutter_background: Default::default(),
- active_line_background: Default::default(),
- line_number: Default::default(),
- line_number_active: Default::default(),
- selection: Default::default(),
- guest_selections: Default::default(),
- }
- }
-}
-
impl InputEditorStyle {
pub fn as_editor(&self) -> EditorStyle {
EditorStyle {
@@ -249,7 +207,11 @@ impl InputEditorStyle {
.background_color
.unwrap_or(Color::transparent_black()),
selection: self.selection,
- ..Default::default()
+ gutter_background: Default::default(),
+ active_line_background: Default::default(),
+ line_number: Default::default(),
+ line_number_active: Default::default(),
+ guest_selections: Default::default(),
}
}
}
@@ -58,10 +58,14 @@ impl ThemeSelector {
cx: &mut ViewContext<Self>,
) -> Self {
let query_editor = cx.add_view(|cx| {
- Editor::single_line(settings.clone(), cx).with_style({
- let settings = settings.clone();
- move |_| settings.borrow().theme.selector.input_editor.as_editor()
- })
+ Editor::single_line(
+ settings.clone(),
+ {
+ let settings = settings.clone();
+ move |_| settings.borrow().theme.selector.input_editor.as_editor()
+ },
+ cx,
+ )
});
cx.subscribe(&query_editor, Self::on_query_editor_event)
@@ -1445,6 +1445,7 @@ mod tests {
editor.update(&mut cx, |editor, cx| {
assert!(!editor.is_dirty(cx.as_ref()));
assert_eq!(editor.title(cx.as_ref()), "untitled");
+ assert!(editor.language(cx).is_none());
editor.insert(&Insert("hi".into()), cx);
assert!(editor.is_dirty(cx.as_ref()));
});
@@ -1455,7 +1456,7 @@ mod tests {
});
cx.simulate_new_path_selection(|parent_dir| {
assert_eq!(parent_dir, dir.path());
- Some(parent_dir.join("the-new-name"))
+ Some(parent_dir.join("the-new-name.rs"))
});
cx.read(|cx| {
assert!(editor.is_dirty(cx));
@@ -1468,7 +1469,11 @@ mod tests {
.await;
cx.read(|cx| {
assert!(!editor.is_dirty(cx));
- assert_eq!(editor.title(cx), "the-new-name");
+ assert_eq!(editor.title(cx), "the-new-name.rs");
+ });
+ // The language is assigned based on the path
+ editor.read_with(&cx, |editor, cx| {
+ assert_eq!(editor.language(cx).unwrap().name(), "Rust")
});
// Edit the file and save it again. This time, there is no filename prompt.
@@ -1483,7 +1488,7 @@ mod tests {
editor
.condition(&cx, |editor, cx| !editor.is_dirty(cx))
.await;
- cx.read(|cx| assert_eq!(editor.title(cx), "the-new-name"));
+ cx.read(|cx| assert_eq!(editor.title(cx), "the-new-name.rs"));
// Open the same newly-created file in another pane item. The new editor should reuse
// the same buffer.
@@ -1491,7 +1496,7 @@ mod tests {
workspace.open_new_file(&OpenNew(app_state.clone()), cx);
workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
assert!(workspace
- .open_entry((tree.id(), Path::new("the-new-name").into()), cx)
+ .open_entry((tree.id(), Path::new("the-new-name.rs").into()), cx)
.is_none());
});
let editor2 = workspace.update(&mut cx, |workspace, cx| {
@@ -1507,6 +1512,47 @@ mod tests {
})
}
+ #[gpui::test]
+ async fn test_setting_language_when_saving_as_single_file_worktree(
+ mut cx: gpui::TestAppContext,
+ ) {
+ let dir = TempDir::new("test-new-file").unwrap();
+ let app_state = cx.update(test_app_state);
+ let (_, workspace) = cx.add_window(|cx| Workspace::new(&app_state, cx));
+
+ // Create a new untitled buffer
+ let editor = workspace.update(&mut cx, |workspace, cx| {
+ workspace.open_new_file(&OpenNew(app_state.clone()), cx);
+ workspace
+ .active_item(cx)
+ .unwrap()
+ .to_any()
+ .downcast::<Editor>()
+ .unwrap()
+ });
+
+ editor.update(&mut cx, |editor, cx| {
+ assert!(editor.language(cx).is_none());
+ editor.insert(&Insert("hi".into()), cx);
+ assert!(editor.is_dirty(cx.as_ref()));
+ });
+
+ // Save the buffer. This prompts for a filename.
+ workspace.update(&mut cx, |workspace, cx| {
+ workspace.save_active_item(&Save, cx)
+ });
+ cx.simulate_new_path_selection(|_| Some(dir.path().join("the-new-name.rs")));
+
+ editor
+ .condition(&cx, |editor, cx| !editor.is_dirty(cx))
+ .await;
+
+ // The language is assigned based on the path
+ editor.read_with(&cx, |editor, cx| {
+ assert_eq!(editor.language(cx).unwrap().name(), "Rust")
+ });
+ }
+
#[gpui::test]
async fn test_new_empty_workspace(mut cx: gpui::TestAppContext) {
cx.update(init);
@@ -185,13 +185,13 @@ impl Pane {
theme.workspace.tab.label.text.font_size,
);
- let mut row = Flex::row();
- for (ix, item) in self.items.iter().enumerate() {
- let is_active = ix == self.active_item;
+ enum Tabs {}
+ let tabs = MouseEventHandler::new::<Tabs, _, _, _>(0, cx, |mouse_state, cx| {
+ let mut row = Flex::row();
+ for (ix, item) in self.items.iter().enumerate() {
+ let is_active = ix == self.active_item;
- enum Tab {}
- row.add_child(
- MouseEventHandler::new::<Tab, _, _, _>(item.id(), cx, |mouse_state, cx| {
+ row.add_child({
let mut title = item.title(cx);
if title.len() > MAX_TAB_TITLE_LEN {
let mut truncated_len = MAX_TAB_TITLE_LEN;
@@ -276,7 +276,7 @@ impl Pane {
)
.with_child(
Align::new(
- ConstrainedBox::new(if is_active || mouse_state.hovered {
+ ConstrainedBox::new(if mouse_state.hovered {
let item_id = item.id();
enum TabCloseButton {}
let icon = Svg::new("icons/x.svg");
@@ -292,6 +292,7 @@ impl Pane {
}
},
)
+ .with_padding(Padding::uniform(4.))
.with_cursor_style(CursorStyle::PointingHand)
.on_click(move |cx| {
cx.dispatch_action(CloseItem(item_id))
@@ -316,36 +317,37 @@ impl Pane {
})
.boxed()
})
- .boxed(),
- )
- }
+ }
- // Ensure there's always a minimum amount of space after the last tab,
- // so that the tab's border doesn't abut the window's border.
- let mut border = Border::bottom(1.0, Color::default());
- border.color = theme.workspace.tab.container.border.color;
-
- row.add_child(
- ConstrainedBox::new(
- Container::new(Empty::new().boxed())
- .with_border(border)
- .boxed(),
- )
- .with_min_width(20.)
- .named("fixed-filler"),
- );
+ // Ensure there's always a minimum amount of space after the last tab,
+ // so that the tab's border doesn't abut the window's border.
+ let mut border = Border::bottom(1.0, Color::default());
+ border.color = theme.workspace.tab.container.border.color;
- row.add_child(
- Expanded::new(
- 0.0,
- Container::new(Empty::new().boxed())
- .with_border(border)
- .boxed(),
- )
- .named("filler"),
- );
+ row.add_child(
+ ConstrainedBox::new(
+ Container::new(Empty::new().boxed())
+ .with_border(border)
+ .boxed(),
+ )
+ .with_min_width(20.)
+ .named("fixed-filler"),
+ );
+
+ row.add_child(
+ Expanded::new(
+ 0.0,
+ Container::new(Empty::new().boxed())
+ .with_border(border)
+ .boxed(),
+ )
+ .named("filler"),
+ );
+
+ row.boxed()
+ });
- ConstrainedBox::new(row.boxed())
+ ConstrainedBox::new(tabs.boxed())
.with_height(line_height + 16.)
.named("tabs")
}
@@ -6,7 +6,7 @@ use crate::{
fs::{self, Fs},
fuzzy,
fuzzy::CharBag,
- language::LanguageRegistry,
+ language::{Language, LanguageRegistry},
rpc::{self, proto, Status},
time::{self, ReplicaId},
util::{Bias, TryFutureExt},
@@ -832,7 +832,6 @@ impl LocalWorktree {
}
});
- let languages = self.languages.clone();
let path = Arc::from(path);
cx.spawn(|this, mut cx| async move {
if let Some(existing_buffer) = existing_buffer {
@@ -841,8 +840,8 @@ impl LocalWorktree {
let (file, contents) = this
.update(&mut cx, |this, cx| this.as_local().unwrap().load(&path, cx))
.await?;
- let language = languages.select_language(&path).cloned();
let buffer = cx.add_model(|cx| {
+ let language = file.select_language(cx);
Buffer::from_history(0, History::new(contents.into()), Some(file), language, cx)
});
this.update(&mut cx, |this, _| {
@@ -1192,7 +1191,6 @@ impl RemoteWorktree {
});
let rpc = self.rpc.clone();
- let languages = self.languages.clone();
let replica_id = self.replica_id;
let remote_worktree_id = self.remote_id;
let path = path.to_string_lossy().to_string();
@@ -1204,7 +1202,7 @@ impl RemoteWorktree {
.read_with(&cx, |tree, _| tree.entry_for_path(&path).cloned())
.ok_or_else(|| anyhow!("file does not exist"))?;
let file = File::new(entry.id, handle, entry.path, entry.mtime);
- let language = languages.select_language(&path).cloned();
+ let language = cx.read(|cx| file.select_language(cx));
let response = rpc
.request(proto::OpenBuffer {
worktree_id: remote_worktree_id as u64,
@@ -1681,6 +1679,18 @@ impl File {
self.worktree.read(cx).abs_path.join(&self.path)
}
+ pub fn select_language(&self, cx: &AppContext) -> Option<Arc<Language>> {
+ let worktree = self.worktree.read(cx);
+ let mut full_path = PathBuf::new();
+ full_path.push(worktree.root_name());
+ full_path.push(&self.path);
+ let languages = match self.worktree.read(cx) {
+ Worktree::Local(worktree) => &worktree.languages,
+ Worktree::Remote(worktree) => &worktree.languages,
+ };
+ languages.select_language(&full_path).cloned()
+ }
+
/// Returns the last component of this handle's absolute path. If this handle refers to the root
/// of its worktree, then this method will return the name of the worktree itself.
pub fn file_name<'a>(&'a self, cx: &'a AppContext) -> Option<OsString> {