diff --git a/Cargo.lock b/Cargo.lock index dc7b6921a7..9417057bba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -107,9 +107,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "0.7.15" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" dependencies = [ "memchr", ] @@ -1412,6 +1412,7 @@ dependencies = [ "postage", "project", "serde_json", + "theme", "unindent", "util", "workspace", @@ -2814,9 +2815,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.3.4" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" +checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" [[package]] name = "memmap2" @@ -2922,9 +2923,9 @@ dependencies = [ [[package]] name = "nom" -version = "6.2.1" +version = "6.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c5c51b9083a3c620fa67a2a635d1ce7d95b897e957d6b28ff9a5da960a103a6" +checksum = "e7413f999671bd4745a7b624bd370a569fb6bc574b23c83a3c5ed2e453f3d5e2" dependencies = [ "bitvec", "funty", @@ -3765,21 +3766,20 @@ dependencies = [ [[package]] name = "regex" -version = "1.4.3" +version = "1.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9251239e129e16308e70d853559389de218ac275b515068abc96829d05b948a" +checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" dependencies = [ "aho-corasick", "memchr", "regex-syntax", - "thread_local", ] [[package]] name = "regex-syntax" -version = "0.6.22" +version = "0.6.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5eb417147ba9860a96cfe72a0b93bf88fee1744b5636ec99ab20c1aa9376581" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" [[package]] name = "remove_dir_all" @@ -4451,7 +4451,7 @@ checksum = "6d86e3c77ff882a828346ba401a7ef4b8e440df804491c6064fe8295765de71c" dependencies = [ "lazy_static", "maplit", - "nom 6.2.1", + "nom 6.1.2", "regex", "unicode_categories", ] @@ -5748,6 +5748,7 @@ dependencies = [ "project", "project_panel", "rand 0.8.3", + "regex", "rpc", "rsa", "rust-embed", diff --git a/crates/diagnostics/Cargo.toml b/crates/diagnostics/Cargo.toml index 5da4c9c8fa..df3022ef43 100644 --- a/crates/diagnostics/Cargo.toml +++ b/crates/diagnostics/Cargo.toml @@ -13,6 +13,7 @@ editor = { path = "../editor" } language = { path = "../language" } gpui = { path = "../gpui" } project = { path = "../project" } +theme = { path = "../theme" } util = { path = "../util" } workspace = { path = "../workspace" } postage = { version = "0.4", features = ["futures-traits"] } diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 62fc3bf321..299eed77c1 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -3,18 +3,22 @@ pub mod items; use anyhow::Result; use collections::{BTreeSet, HashMap, HashSet}; use editor::{ - diagnostic_block_renderer, diagnostic_style, + diagnostic_block_renderer, display_map::{BlockDisposition, BlockId, BlockProperties, RenderBlock}, + highlight_diagnostic_message, items::BufferItemHandle, Autoscroll, BuildSettings, Editor, ExcerptId, ExcerptProperties, MultiBuffer, ToOffset, }; use gpui::{ - action, elements::*, keymap::Binding, AnyViewHandle, AppContext, Entity, ModelHandle, - MutableAppContext, RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle, + action, elements::*, fonts::TextStyle, keymap::Binding, AnyViewHandle, AppContext, Entity, + ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext, ViewHandle, + WeakViewHandle, +}; +use language::{ + Bias, Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, Point, Selection, SelectionGoal, }; -use language::{Bias, Buffer, Diagnostic, DiagnosticEntry, Point, Selection, SelectionGoal}; use postage::watch; -use project::{Project, ProjectPath}; +use project::{DiagnosticSummary, Project, ProjectPath}; use std::{ any::{Any, TypeId}, cmp::Ordering, @@ -54,6 +58,7 @@ struct ProjectDiagnosticsEditor { model: ModelHandle, workspace: WeakViewHandle, editor: ViewHandle, + summary: DiagnosticSummary, excerpts: ModelHandle, path_states: Vec, paths_to_update: BTreeSet, @@ -127,8 +132,10 @@ impl ProjectDiagnosticsEditor { let project = model.read(cx).project.clone(); cx.subscribe(&project, |this, _, event, cx| match event { project::Event::DiskBasedDiagnosticsFinished => { + this.summary = this.model.read(cx).project.read(cx).diagnostic_summary(cx); let paths = mem::take(&mut this.paths_to_update); this.update_excerpts(paths, cx); + cx.emit(Event::TitleChanged); } project::Event::DiagnosticsUpdated(path) => { this.paths_to_update.insert(path.clone()); @@ -144,13 +151,11 @@ impl ProjectDiagnosticsEditor { cx.subscribe(&editor, |_, _, event, cx| cx.emit(*event)) .detach(); - let paths_to_update = project - .read(cx) - .diagnostic_summaries(cx) - .map(|e| e.0) - .collect(); + let project = project.read(cx); + let paths_to_update = project.diagnostic_summaries(cx).map(|e| e.0).collect(); let this = Self { model, + summary: project.diagnostic_summary(cx), workspace, excerpts, editor, @@ -344,17 +349,16 @@ impl ProjectDiagnosticsEditor { if is_first_excerpt_for_group { is_first_excerpt_for_group = false; - let primary = &group.entries[group.primary_ix].diagnostic; - let mut header = primary.clone(); - header.message = + let mut primary = + group.entries[group.primary_ix].diagnostic.clone(); + primary.message = primary.message.split('\n').next().unwrap().to_string(); group_state.block_count += 1; blocks_to_add.push(BlockProperties { position: header_position, height: 2, render: diagnostic_header_renderer( - header, - true, + primary, self.build_settings.clone(), ), disposition: BlockDisposition::Above, @@ -554,8 +558,12 @@ impl workspace::ItemView for ProjectDiagnosticsEditor { self.model.clone() } - fn title(&self, _: &AppContext) -> String { - "Project Diagnostics".to_string() + fn tab_content(&self, style: &theme::Tab, _: &AppContext) -> ElementBox { + render_summary( + &self.summary, + &style.label.text, + &self.settings.borrow().theme.project_diagnostics, + ) } fn project_path(&self, _: &AppContext) -> Option { @@ -601,10 +609,7 @@ impl workspace::ItemView for ProjectDiagnosticsEditor { } fn should_update_tab_on_event(event: &Event) -> bool { - matches!( - event, - Event::Saved | Event::Dirtied | Event::FileHandleChanged - ) + matches!(event, Event::Saved | Event::Dirtied | Event::TitleChanged) } fn clone_on_split(&self, cx: &mut ViewContext) -> Option @@ -651,20 +656,43 @@ impl workspace::ItemView for ProjectDiagnosticsEditor { fn path_header_renderer(buffer: ModelHandle, build_settings: BuildSettings) -> RenderBlock { Arc::new(move |cx| { let settings = build_settings(cx); - let file_path = if let Some(file) = buffer.read(&**cx).file() { - file.path().to_string_lossy().to_string() - } else { - "untitled".to_string() - }; - let mut text_style = settings.style.text.clone(); let style = settings.style.diagnostic_path_header; - text_style.color = style.text; - Label::new(file_path, text_style) + let font_size = (style.text_scale_factor * settings.style.text.font_size).round(); + + let mut filename = None; + let mut path = None; + if let Some(file) = buffer.read(&**cx).file() { + filename = file + .path() + .file_name() + .map(|f| f.to_string_lossy().to_string()); + path = file + .path() + .parent() + .map(|p| p.to_string_lossy().to_string() + "/"); + } + + Flex::row() + .with_child( + Label::new( + filename.unwrap_or_else(|| "untitled".to_string()), + style.filename.text.clone().with_font_size(font_size), + ) + .contained() + .with_style(style.filename.container) + .boxed(), + ) + .with_children(path.map(|path| { + Label::new(path, style.path.text.clone().with_font_size(font_size)) + .contained() + .with_style(style.path.container) + .boxed() + })) .aligned() .left() .contained() - .with_style(style.header) - .with_padding_left(cx.line_number_x) + .with_style(style.container) + .with_padding_left(cx.gutter_padding + cx.scroll_x * cx.em_width) .expanded() .named("path header block") }) @@ -672,21 +700,52 @@ fn path_header_renderer(buffer: ModelHandle, build_settings: BuildSettin fn diagnostic_header_renderer( diagnostic: Diagnostic, - is_valid: bool, build_settings: BuildSettings, ) -> RenderBlock { + let (message, highlights) = highlight_diagnostic_message(&diagnostic.message); Arc::new(move |cx| { let settings = build_settings(cx); - let mut text_style = settings.style.text.clone(); - let diagnostic_style = diagnostic_style(diagnostic.severity, is_valid, &settings.style); - text_style.color = diagnostic_style.text; - Text::new(diagnostic.message.clone(), text_style) - .with_soft_wrap(false) - .aligned() - .left() + let style = &settings.style.diagnostic_header; + let font_size = (style.text_scale_factor * settings.style.text.font_size).round(); + let icon_width = cx.em_width * style.icon_width_factor; + let icon = if diagnostic.severity == DiagnosticSeverity::ERROR { + Svg::new("icons/diagnostic-error-10.svg") + .with_color(settings.style.error_diagnostic.message.text.color) + } else { + Svg::new("icons/diagnostic-warning-10.svg") + .with_color(settings.style.warning_diagnostic.message.text.color) + }; + + Flex::row() + .with_child( + icon.constrained() + .with_width(icon_width) + .aligned() + .contained() + .boxed(), + ) + .with_child( + Label::new( + message.clone(), + style.message.label.clone().with_font_size(font_size), + ) + .with_highlights(highlights.clone()) + .contained() + .with_style(style.message.container) + .with_margin_left(cx.gutter_padding) + .aligned() + .boxed(), + ) + .with_children(diagnostic.code.clone().map(|code| { + Label::new(code, style.code.text.clone().with_font_size(font_size)) + .contained() + .with_style(style.code.container) + .aligned() + .boxed() + })) .contained() - .with_style(diagnostic_style.header) - .with_padding_left(cx.line_number_x) + .with_style(style.container) + .with_padding_left(cx.gutter_padding + cx.scroll_x * cx.em_width) .expanded() .named("diagnostic header") }) @@ -698,11 +757,60 @@ fn context_header_renderer(build_settings: BuildSettings) -> RenderBlock { let text_style = settings.style.text.clone(); Label::new("…".to_string(), text_style) .contained() - .with_padding_left(cx.line_number_x) + .with_padding_left(cx.gutter_padding + cx.scroll_x * cx.em_width) .named("collapsed context") }) } +pub(crate) fn render_summary( + summary: &DiagnosticSummary, + text_style: &TextStyle, + theme: &theme::ProjectDiagnostics, +) -> ElementBox { + let icon_width = theme.tab_icon_width; + let icon_spacing = theme.tab_icon_spacing; + let summary_spacing = theme.tab_summary_spacing; + Flex::row() + .with_children([ + Svg::new("icons/diagnostic-summary-error.svg") + .with_color(text_style.color) + .constrained() + .with_width(icon_width) + .aligned() + .contained() + .with_margin_right(icon_spacing) + .named("no-icon"), + Label::new( + summary.error_count.to_string(), + LabelStyle { + text: text_style.clone(), + highlight_text: None, + }, + ) + .aligned() + .boxed(), + Svg::new("icons/diagnostic-summary-warning.svg") + .with_color(text_style.color) + .constrained() + .with_width(icon_width) + .aligned() + .contained() + .with_margin_left(summary_spacing) + .with_margin_right(icon_spacing) + .named("warn-icon"), + Label::new( + summary.warning_count.to_string(), + LabelStyle { + text: text_style.clone(), + highlight_text: None, + }, + ) + .aligned() + .boxed(), + ]) + .boxed() +} + fn compare_diagnostics( lhs: &DiagnosticEntry, rhs: &DiagnosticEntry, @@ -1144,7 +1252,11 @@ mod tests { .render(&BlockContext { cx, anchor_x: 0., - line_number_x: 0., + scroll_x: 0., + gutter_padding: 0., + gutter_width: 0., + line_height: 0., + em_width: 0., }) .name() .map(|s| (row, s.to_string())) diff --git a/crates/diagnostics/src/items.rs b/crates/diagnostics/src/items.rs index 3390f74a84..80291cde3d 100644 --- a/crates/diagnostics/src/items.rs +++ b/crates/diagnostics/src/items.rs @@ -1,9 +1,9 @@ +use crate::render_summary; use gpui::{ elements::*, platform::CursorStyle, Entity, ModelHandle, RenderContext, View, ViewContext, }; use postage::watch; use project::Project; -use std::fmt::Write; use workspace::{Settings, StatusItemView}; pub struct DiagnosticSummary { @@ -20,7 +20,6 @@ impl DiagnosticSummary { ) -> Self { cx.subscribe(project, |this, project, event, cx| match event { project::Event::DiskBasedDiagnosticsUpdated => { - this.summary = project.read(cx).diagnostic_summary(cx); cx.notify(); } project::Event::DiskBasedDiagnosticsStarted => { @@ -28,6 +27,7 @@ impl DiagnosticSummary { cx.notify(); } project::Event::DiskBasedDiagnosticsFinished => { + this.summary = project.read(cx).diagnostic_summary(cx); this.in_progress = false; cx.notify(); } @@ -55,21 +55,20 @@ impl View for DiagnosticSummary { enum Tag {} let theme = &self.settings.borrow().theme.project_diagnostics; - let mut message = String::new(); - if self.in_progress { - message.push_str("Checking... "); - } - write!( - message, - "Errors: {}, Warnings: {}", - self.summary.error_count, self.summary.warning_count - ) - .unwrap(); + + let in_progress = self.in_progress; MouseEventHandler::new::(0, cx, |_, _| { - Label::new(message, theme.status_bar_item.text.clone()) + if in_progress { + Label::new( + "Checking... ".to_string(), + theme.status_bar_item.text.clone(), + ) .contained() .with_style(theme.status_bar_item.container) .boxed() + } else { + render_summary(&self.summary, &theme.status_bar_item.text, &theme) + } }) .with_cursor_style(CursorStyle::PointingHand) .on_click(|cx| cx.dispatch_action(crate::Deploy)) diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index d660307eb1..f00dd89651 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -69,7 +69,11 @@ where pub struct BlockContext<'a> { pub cx: &'a AppContext, pub anchor_x: f32, - pub line_number_x: f32, + pub scroll_x: f32, + pub gutter_width: f32, + pub gutter_padding: f32, + pub em_width: f32, + pub line_height: f32, } #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] @@ -947,7 +951,11 @@ mod tests { .render(&BlockContext { cx, anchor_x: 0., - line_number_x: 0., + gutter_padding: 0., + scroll_x: 0., + gutter_width: 0., + line_height: 0., + em_width: 0., }) .name() .unwrap() diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 0c393d93ab..3f38f82aa7 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -551,6 +551,19 @@ impl Editor { &self.buffer } + pub fn title(&self, cx: &AppContext) -> String { + let filename = self + .buffer() + .read(cx) + .file(cx) + .map(|file| file.file_name(cx)); + if let Some(name) = filename { + name.to_string_lossy().into() + } else { + "untitled".into() + } + } + pub fn snapshot(&mut self, cx: &mut MutableAppContext) -> EditorSnapshot { EditorSnapshot { mode: self.mode, @@ -3762,8 +3775,8 @@ impl Editor { language::Event::Edited => cx.emit(Event::Edited), language::Event::Dirtied => cx.emit(Event::Dirtied), language::Event::Saved => cx.emit(Event::Saved), - language::Event::FileHandleChanged => cx.emit(Event::FileHandleChanged), - language::Event::Reloaded => cx.emit(Event::FileHandleChanged), + language::Event::FileHandleChanged => cx.emit(Event::TitleChanged), + language::Event::Reloaded => cx.emit(Event::TitleChanged), language::Event::Closed => cx.emit(Event::Closed), _ => {} } @@ -3803,6 +3816,8 @@ impl Deref for EditorSnapshot { impl EditorSettings { #[cfg(any(test, feature = "test-support"))] pub fn test(cx: &AppContext) -> Self { + use theme::{ContainedLabel, ContainedText, DiagnosticHeader, DiagnosticPathHeader}; + Self { tab_size: 4, soft_wrap: SoftWrap::None, @@ -3814,19 +3829,26 @@ impl EditorSettings { let font_id = font_cache .select_font(font_family_id, &font_properties) .unwrap(); + let text = gpui::fonts::TextStyle { + font_family_name, + font_family_id, + font_id, + font_size: 14., + color: gpui::color::Color::from_u32(0xff0000ff), + font_properties, + underline: None, + }; + let default_diagnostic_style = DiagnosticStyle { + message: text.clone().into(), + header: Default::default(), + text_scale_factor: 1., + }; EditorStyle { - text: gpui::fonts::TextStyle { - font_family_name, - font_family_id, - font_id, - font_size: 14., - color: gpui::color::Color::from_u32(0xff0000ff), - font_properties, - underline: None, - }, + text: text.clone(), placeholder_text: None, background: Default::default(), gutter_background: Default::default(), + gutter_padding_factor: 2., active_line_background: Default::default(), highlighted_line_background: Default::default(), line_number: Default::default(), @@ -3834,15 +3856,39 @@ impl EditorSettings { selection: Default::default(), guest_selections: Default::default(), syntax: Default::default(), - diagnostic_path_header: Default::default(), - error_diagnostic: Default::default(), - invalid_error_diagnostic: Default::default(), - warning_diagnostic: Default::default(), - invalid_warning_diagnostic: Default::default(), - information_diagnostic: Default::default(), - invalid_information_diagnostic: Default::default(), - hint_diagnostic: Default::default(), - invalid_hint_diagnostic: Default::default(), + diagnostic_path_header: DiagnosticPathHeader { + container: Default::default(), + filename: ContainedText { + container: Default::default(), + text: text.clone(), + }, + path: ContainedText { + container: Default::default(), + text: text.clone(), + }, + text_scale_factor: 1., + }, + diagnostic_header: DiagnosticHeader { + container: Default::default(), + message: ContainedLabel { + container: Default::default(), + label: text.clone().into(), + }, + code: ContainedText { + container: Default::default(), + text: text.clone(), + }, + icon_width_factor: 1., + text_scale_factor: 1., + }, + error_diagnostic: default_diagnostic_style.clone(), + invalid_error_diagnostic: default_diagnostic_style.clone(), + warning_diagnostic: default_diagnostic_style.clone(), + invalid_warning_diagnostic: default_diagnostic_style.clone(), + information_diagnostic: default_diagnostic_style.clone(), + invalid_information_diagnostic: default_diagnostic_style.clone(), + hint_diagnostic: default_diagnostic_style.clone(), + invalid_hint_diagnostic: default_diagnostic_style.clone(), } }, } @@ -3870,7 +3916,7 @@ pub enum Event { Blurred, Dirtied, Saved, - FileHandleChanged, + TitleChanged, Closed, } @@ -3983,33 +4029,73 @@ pub fn diagnostic_block_renderer( is_valid: bool, build_settings: BuildSettings, ) -> RenderBlock { + let mut highlighted_lines = Vec::new(); + for line in diagnostic.message.lines() { + highlighted_lines.push(highlight_diagnostic_message(line)); + } + Arc::new(move |cx: &BlockContext| { let settings = build_settings(cx); - let mut text_style = settings.style.text.clone(); - text_style.color = diagnostic_style(diagnostic.severity, is_valid, &settings.style).text; - Text::new(diagnostic.message.clone(), text_style) - .with_soft_wrap(false) - .contained() - .with_margin_left(cx.anchor_x) + let style = diagnostic_style(diagnostic.severity, is_valid, &settings.style); + let font_size = (style.text_scale_factor * settings.style.text.font_size).round(); + Flex::column() + .with_children(highlighted_lines.iter().map(|(line, highlights)| { + Label::new( + line.clone(), + style.message.clone().with_font_size(font_size), + ) + .with_highlights(highlights.clone()) + .contained() + .with_margin_left(cx.anchor_x) + .boxed() + })) + .aligned() + .left() .boxed() }) } +pub fn highlight_diagnostic_message(message: &str) -> (String, Vec) { + let mut message_without_backticks = String::new(); + let mut prev_offset = 0; + let mut inside_block = false; + let mut highlights = Vec::new(); + for (match_ix, (offset, _)) in message + .match_indices('`') + .chain([(message.len(), "")]) + .enumerate() + { + message_without_backticks.push_str(&message[prev_offset..offset]); + if inside_block { + highlights.extend(prev_offset - match_ix..offset - match_ix); + } + + inside_block = !inside_block; + prev_offset = offset + 1; + } + + (message_without_backticks, highlights) +} + pub fn diagnostic_style( severity: DiagnosticSeverity, valid: bool, style: &EditorStyle, ) -> DiagnosticStyle { match (severity, valid) { - (DiagnosticSeverity::ERROR, true) => style.error_diagnostic, - (DiagnosticSeverity::ERROR, false) => style.invalid_error_diagnostic, - (DiagnosticSeverity::WARNING, true) => style.warning_diagnostic, - (DiagnosticSeverity::WARNING, false) => style.invalid_warning_diagnostic, - (DiagnosticSeverity::INFORMATION, true) => style.information_diagnostic, - (DiagnosticSeverity::INFORMATION, false) => style.invalid_information_diagnostic, - (DiagnosticSeverity::HINT, true) => style.hint_diagnostic, - (DiagnosticSeverity::HINT, false) => style.invalid_hint_diagnostic, - _ => Default::default(), + (DiagnosticSeverity::ERROR, true) => style.error_diagnostic.clone(), + (DiagnosticSeverity::ERROR, false) => style.invalid_error_diagnostic.clone(), + (DiagnosticSeverity::WARNING, true) => style.warning_diagnostic.clone(), + (DiagnosticSeverity::WARNING, false) => style.invalid_warning_diagnostic.clone(), + (DiagnosticSeverity::INFORMATION, true) => style.information_diagnostic.clone(), + (DiagnosticSeverity::INFORMATION, false) => style.invalid_information_diagnostic.clone(), + (DiagnosticSeverity::HINT, true) => style.hint_diagnostic.clone(), + (DiagnosticSeverity::HINT, false) => style.invalid_hint_diagnostic.clone(), + _ => DiagnosticStyle { + message: style.text.clone().into(), + header: Default::default(), + text_scale_factor: 1., + }, } } diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index ff4b792338..db67903e57 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -8,7 +8,7 @@ use collections::{BTreeMap, HashMap}; use gpui::{ color::Color, elements::layout_highlighted_chunks, - fonts::HighlightStyle, + fonts::{HighlightStyle, Underline}, geometry::{ rect::RectF, vector::{vec2f, Vector2F}, @@ -17,7 +17,7 @@ use gpui::{ json::{self, ToJson}, keymap::Keystroke, text_layout::{self, RunStyle, TextLayoutCache}, - AppContext, Axis, Border, Element, ElementBox, Event, EventContext, FontCache, LayoutContext, + AppContext, Axis, Border, Element, ElementBox, Event, EventContext, LayoutContext, MutableAppContext, PaintContext, Quad, Scene, SizeConstraint, ViewContext, WeakViewHandle, }; use json::json; @@ -140,18 +140,14 @@ impl EditorElement { )) } - let font_cache = cx.font_cache.clone(); - let text_layout_cache = cx.text_layout_cache.clone(); let snapshot = self.snapshot(cx.app); let (position, overshoot) = paint.point_for_position(&snapshot, layout, position); cx.dispatch_action(Select(SelectPhase::Update { position, overshoot, - scroll_position: (snapshot.scroll_position() + scroll_delta).clamp( - Vector2F::zero(), - layout.scroll_max(&font_cache, &text_layout_cache), - ), + scroll_position: (snapshot.scroll_position() + scroll_delta) + .clamp(Vector2F::zero(), layout.scroll_max), })); true } else { @@ -192,8 +188,6 @@ impl EditorElement { } let snapshot = self.snapshot(cx.app); - let font_cache = &cx.font_cache; - let layout_cache = &cx.text_layout_cache; let max_glyph_width = layout.em_width; if !precise { delta *= vec2f(max_glyph_width, layout.line_height); @@ -202,10 +196,7 @@ impl EditorElement { let scroll_position = snapshot.scroll_position(); let x = (scroll_position.x() * max_glyph_width - delta.x()) / max_glyph_width; let y = (scroll_position.y() * layout.line_height - delta.y()) / layout.line_height; - let scroll_position = vec2f(x, y).clamp( - Vector2F::zero(), - layout.scroll_max(font_cache, layout_cache), - ); + let scroll_position = vec2f(x, y).clamp(Vector2F::zero(), layout.scroll_max); cx.dispatch_action(Scroll(scroll_position)); @@ -459,7 +450,7 @@ impl EditorElement { .width() } - fn layout_rows( + fn layout_line_numbers( &self, rows: Range, active_rows: &BTreeMap, @@ -549,7 +540,12 @@ impl EditorElement { .chunks(rows.clone(), Some(&style.syntax)) .map(|chunk| { let highlight = if let Some(severity) = chunk.diagnostic { - let underline = Some(super::diagnostic_style(severity, true, style).text); + let diagnostic_style = super::diagnostic_style(severity, true, style); + let underline = Some(Underline { + color: diagnostic_style.message.text.color, + thickness: 1.0.into(), + squiggly: true, + }); if let Some(mut highlight) = chunk.highlight_style { highlight.underline = underline; Some(highlight) @@ -581,7 +577,9 @@ impl EditorElement { rows: Range, snapshot: &EditorSnapshot, width: f32, - line_number_x: f32, + gutter_padding: f32, + gutter_width: f32, + em_width: f32, text_x: f32, line_height: f32, style: &EditorStyle, @@ -609,7 +607,11 @@ impl EditorElement { let mut element = block.render(&BlockContext { cx, anchor_x, - line_number_x, + gutter_padding, + line_height, + scroll_x: snapshot.scroll_position.x(), + gutter_width, + em_width, }); element.layout( SizeConstraint { @@ -645,7 +647,7 @@ impl Element for EditorElement { let gutter_padding; let gutter_width; if snapshot.mode == EditorMode::Full { - gutter_padding = style.text.em_width(cx.font_cache); + gutter_padding = style.text.em_width(cx.font_cache) * style.gutter_padding_factor; gutter_width = self.max_line_number_width(&snapshot, cx) + gutter_padding * 2.0; } else { gutter_padding = 0.0; @@ -761,7 +763,8 @@ impl Element for EditorElement { } }); - let line_number_layouts = self.layout_rows(start_row..end_row, &active_rows, &snapshot, cx); + let line_number_layouts = + self.layout_line_numbers(start_row..end_row, &active_rows, &snapshot, cx); let mut max_visible_line_width = 0.0; let line_layouts = self.layout_lines(start_row..end_row, &mut snapshot, cx); @@ -771,52 +774,32 @@ impl Element for EditorElement { } } - let blocks = self.layout_blocks( - start_row..end_row, + let style = self.settings.style.clone(); + let longest_line_width = layout_line( + snapshot.longest_row(), &snapshot, - size.x(), - gutter_padding, - gutter_width + text_offset.x(), - line_height, &style, - &line_layouts, - cx, + cx.text_layout_cache, + ) + .width(); + let scroll_width = longest_line_width.max(max_visible_line_width) + overscroll.x(); + let em_width = style.text.em_width(cx.font_cache); + let max_row = snapshot.max_point().row(); + let scroll_max = vec2f( + ((scroll_width - text_size.x()) / em_width).max(0.0), + max_row.saturating_sub(1) as f32, ); - let mut layout = LayoutState { - size, - gutter_size, - gutter_padding, - text_size, - overscroll, - text_offset, - snapshot, - style: self.settings.style.clone(), - active_rows, - highlighted_rows, - line_layouts, - line_number_layouts, - blocks, - line_height, - em_width, - em_advance, - selections, - 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 = style.text.em_width(&cx.font_cache); self.update_view(cx.app, |view, cx| { - let clamped = view.clamp_scroll_left(scroll_max); + let clamped = view.clamp_scroll_left(scroll_max.x()); let autoscrolled; if autoscroll_horizontally { autoscrolled = view.autoscroll_horizontally( start_row, - layout.text_size.x(), + text_size.x(), scroll_width, - max_glyph_width, - &layout.line_layouts, + em_width, + &line_layouts, cx, ); } else { @@ -824,11 +807,45 @@ impl Element for EditorElement { } if clamped || autoscrolled { - layout.snapshot = view.snapshot(cx); + snapshot = view.snapshot(cx); } }); - (size, Some(layout)) + let blocks = self.layout_blocks( + start_row..end_row, + &snapshot, + size.x().max(scroll_width + gutter_width), + gutter_padding, + gutter_width, + em_width, + gutter_width + text_offset.x(), + line_height, + &style, + &line_layouts, + cx, + ); + + ( + size, + Some(LayoutState { + size, + scroll_max, + gutter_size, + gutter_padding, + text_size, + text_offset, + snapshot, + active_rows, + highlighted_rows, + line_layouts, + line_number_layouts, + blocks, + line_height, + em_width, + em_advance, + selections, + }), + ) } fn paint( @@ -918,10 +935,10 @@ impl Element for EditorElement { pub struct LayoutState { size: Vector2F, + scroll_max: Vector2F, gutter_size: Vector2F, gutter_padding: f32, text_size: Vector2F, - style: EditorStyle, snapshot: EditorSnapshot, active_rows: BTreeMap, highlighted_rows: Option>, @@ -932,30 +949,7 @@ pub struct LayoutState { em_width: f32, em_advance: f32, selections: HashMap>>, - overscroll: Vector2F, text_offset: Vector2F, - max_visible_line_width: f32, -} - -impl LayoutState { - fn scroll_width(&self, layout_cache: &TextLayoutCache) -> f32 { - let row = self.snapshot.longest_row(); - let longest_line_width = - layout_line(row, &self.snapshot, &self.style, 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(layout_cache); - let em_width = self.style.text.em_width(font_cache); - let max_row = self.snapshot.max_point().row(); - - vec2f( - ((scroll_width - text_width) / em_width).max(0.0), - max_row.saturating_sub(1) as f32, - ) - } } fn layout_line( @@ -1187,7 +1181,7 @@ mod tests { 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_rows(0..6, &Default::default(), &snapshot, &mut layout_cx) + element.layout_line_numbers(0..6, &Default::default(), &snapshot, &mut layout_cx) }); assert_eq!(layouts.len(), 6); } diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index a2413f248a..1dafec32c6 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -121,13 +121,9 @@ impl ItemView for Editor { } } - fn title(&self, cx: &AppContext) -> String { - let file = self.buffer().read(cx).file(cx); - if let Some(file) = file { - file.file_name(cx).to_string_lossy().into() - } else { - "untitled".into() - } + fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox { + let title = self.title(cx); + Label::new(title, style.label.clone()).boxed() } fn project_path(&self, cx: &AppContext) -> Option { @@ -207,10 +203,7 @@ impl ItemView for Editor { } fn should_update_tab_on_event(event: &Event) -> bool { - matches!( - event, - Event::Saved | Event::Dirtied | Event::FileHandleChanged - ) + matches!(event, Event::Saved | Event::Dirtied | Event::TitleChanged) } } @@ -337,24 +330,13 @@ impl View for DiagnosticMessage { fn render(&mut self, _: &mut RenderContext) -> ElementBox { if let Some(diagnostic) = &self.diagnostic { let theme = &self.settings.borrow().theme.workspace.status_bar; - Flex::row() - .with_child( - Svg::new("icons/warning.svg") - .with_color(theme.diagnostic_icon_color) - .constrained() - .with_height(theme.diagnostic_icon_size) - .contained() - .with_margin_right(theme.diagnostic_icon_spacing) - .boxed(), - ) - .with_child( - Label::new( - diagnostic.message.lines().next().unwrap().to_string(), - theme.diagnostic_message.clone(), - ) - .boxed(), - ) - .boxed() + Label::new( + diagnostic.message.lines().next().unwrap().to_string(), + theme.diagnostic_message.clone(), + ) + .contained() + .with_margin_left(theme.item_spacing) + .boxed() } else { Empty::new().boxed() } diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 588a80593e..3bf489df91 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -492,7 +492,15 @@ mod tests { .await; cx.read(|cx| { let active_item = active_pane.read(cx).active_item().unwrap(); - assert_eq!(active_item.title(cx), "bandana"); + assert_eq!( + active_item + .to_any() + .downcast::() + .unwrap() + .read(cx) + .title(cx), + "bandana" + ); }); } diff --git a/crates/gpui/src/elements/label.rs b/crates/gpui/src/elements/label.rs index f78e3973e9..20180c380d 100644 --- a/crates/gpui/src/elements/label.rs +++ b/crates/gpui/src/elements/label.rs @@ -33,6 +33,13 @@ impl From for LabelStyle { } } +impl LabelStyle { + pub fn with_font_size(mut self, font_size: f32) -> Self { + self.text.font_size = font_size; + self + } +} + impl Label { pub fn new(text: String, style: impl Into) -> Self { Self { diff --git a/crates/gpui/src/fonts.rs b/crates/gpui/src/fonts.rs index 25e16b7170..2768b9f986 100644 --- a/crates/gpui/src/fonts.rs +++ b/crates/gpui/src/fonts.rs @@ -10,6 +10,7 @@ pub use font_kit::{ metrics::Metrics, properties::{Properties, Stretch, Style, Weight}, }; +use ordered_float::OrderedFloat; use serde::{de, Deserialize}; use serde_json::Value; use std::{cell::RefCell, sync::Arc}; @@ -27,14 +28,21 @@ pub struct TextStyle { pub font_id: FontId, pub font_size: f32, pub font_properties: Properties, - pub underline: Option, + pub underline: Option, } #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] pub struct HighlightStyle { pub color: Color, pub font_properties: Properties, - pub underline: Option, + pub underline: Option, +} + +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] +pub struct Underline { + pub color: Color, + pub thickness: OrderedFloat, + pub squiggly: bool, } #[allow(non_camel_case_types)] @@ -81,7 +89,14 @@ struct HighlightStyleJson { #[serde(untagged)] enum UnderlineStyleJson { Underlined(bool), - UnderlinedWithColor(Color), + UnderlinedWithProperties { + #[serde(default)] + color: Option, + #[serde(default)] + thickness: Option, + #[serde(default)] + squiggly: bool, + }, } impl TextStyle { @@ -89,7 +104,7 @@ impl TextStyle { font_family_name: impl Into>, font_size: f32, font_properties: Properties, - underline: Option, + underline: Option, color: Color, font_cache: &FontCache, ) -> anyhow::Result { @@ -107,6 +122,11 @@ impl TextStyle { }) } + pub fn with_font_size(mut self, font_size: f32) -> Self { + self.font_size = font_size; + self + } + pub fn to_run(&self) -> RunStyle { RunStyle { font_id: self.font_id, @@ -271,11 +291,23 @@ impl<'de> Deserialize<'de> for HighlightStyle { } } -fn underline_from_json(json: UnderlineStyleJson, text_color: Color) -> Option { +fn underline_from_json(json: UnderlineStyleJson, text_color: Color) -> Option { match json { UnderlineStyleJson::Underlined(false) => None, - UnderlineStyleJson::Underlined(true) => Some(text_color), - UnderlineStyleJson::UnderlinedWithColor(color) => Some(color), + UnderlineStyleJson::Underlined(true) => Some(Underline { + color: text_color, + thickness: 1.0.into(), + squiggly: false, + }), + UnderlineStyleJson::UnderlinedWithProperties { + color, + thickness, + squiggly, + } => Some(Underline { + color: color.unwrap_or(text_color), + thickness: thickness.unwrap_or(1.).into(), + squiggly, + }), } } diff --git a/crates/gpui/src/platform/mac/renderer.rs b/crates/gpui/src/platform/mac/renderer.rs index 2a97f4820c..07d425af3e 100644 --- a/crates/gpui/src/platform/mac/renderer.rs +++ b/crates/gpui/src/platform/mac/renderer.rs @@ -6,7 +6,7 @@ use crate::{ vector::{vec2f, vec2i, Vector2F}, }, platform, - scene::{Glyph, Icon, Image, Layer, Quad, Scene, Shadow}, + scene::{Glyph, Icon, Image, Layer, Quad, Scene, Shadow, Underline}, }; use cocoa::foundation::NSUInteger; use metal::{MTLPixelFormat, MTLResourceOptions, NSRange}; @@ -26,6 +26,7 @@ pub struct Renderer { sprite_pipeline_state: metal::RenderPipelineState, image_pipeline_state: metal::RenderPipelineState, path_atlas_pipeline_state: metal::RenderPipelineState, + underline_pipeline_state: metal::RenderPipelineState, unit_vertices: metal::Buffer, instances: metal::Buffer, } @@ -109,6 +110,14 @@ impl Renderer { "path_atlas_fragment", MTLPixelFormat::R16Float, ); + let underline_pipeline_state = build_pipeline_state( + &device, + &library, + "underline", + "underline_vertex", + "underline_fragment", + pixel_format, + ); Self { sprite_cache, image_cache, @@ -118,6 +127,7 @@ impl Renderer { sprite_pipeline_state, image_pipeline_state, path_atlas_pipeline_state, + underline_pipeline_state, unit_vertices, instances, } @@ -324,6 +334,13 @@ impl Renderer { drawable_size, command_encoder, ); + self.render_underlines( + layer.underlines(), + scale_factor, + offset, + drawable_size, + command_encoder, + ); self.render_sprites( layer.glyphs(), layer.icons(), @@ -339,13 +356,6 @@ impl Renderer { drawable_size, command_encoder, ); - self.render_quads( - layer.underlines(), - scale_factor, - offset, - drawable_size, - command_encoder, - ); } command_encoder.end_encoding(); @@ -821,6 +831,76 @@ impl Renderer { ); *offset = next_offset; } + + fn render_underlines( + &mut self, + underlines: &[Underline], + scale_factor: f32, + offset: &mut usize, + drawable_size: Vector2F, + command_encoder: &metal::RenderCommandEncoderRef, + ) { + if underlines.is_empty() { + return; + } + align_offset(offset); + let next_offset = *offset + underlines.len() * mem::size_of::(); + assert!( + next_offset <= INSTANCE_BUFFER_SIZE, + "instance buffer exhausted" + ); + + command_encoder.set_render_pipeline_state(&self.underline_pipeline_state); + command_encoder.set_vertex_buffer( + shaders::GPUIUnderlineInputIndex_GPUIUnderlineInputIndexVertices as u64, + Some(&self.unit_vertices), + 0, + ); + command_encoder.set_vertex_buffer( + shaders::GPUIUnderlineInputIndex_GPUIUnderlineInputIndexUnderlines as u64, + Some(&self.instances), + *offset as u64, + ); + command_encoder.set_vertex_bytes( + shaders::GPUIUnderlineInputIndex_GPUIUnderlineInputIndexUniforms as u64, + mem::size_of::() as u64, + [shaders::GPUIUniforms { + viewport_size: drawable_size.to_float2(), + }] + .as_ptr() as *const c_void, + ); + + let buffer_contents = unsafe { + (self.instances.contents() as *mut u8).offset(*offset as isize) + as *mut shaders::GPUIUnderline + }; + for (ix, underline) in underlines.iter().enumerate() { + let origin = underline.origin * scale_factor; + let mut height = underline.thickness; + if underline.squiggly { + height *= 3.; + } + let size = vec2f(underline.width, height) * scale_factor; + let shader_underline = shaders::GPUIUnderline { + origin: origin.round().to_float2(), + size: size.round().to_float2(), + thickness: underline.thickness * scale_factor, + color: underline.color.to_uchar4(), + squiggly: underline.squiggly as u8, + }; + unsafe { + *(buffer_contents.offset(ix as isize)) = shader_underline; + } + } + + command_encoder.draw_primitives_instanced( + metal::MTLPrimitiveType::Triangle, + 0, + 6, + underlines.len() as u64, + ); + *offset = next_offset; + } } fn build_path_atlas_texture_descriptor() -> metal::TextureDescriptor { diff --git a/crates/gpui/src/platform/mac/shaders/shaders.h b/crates/gpui/src/platform/mac/shaders/shaders.h index 1b6ad3f26f..3f5096f37c 100644 --- a/crates/gpui/src/platform/mac/shaders/shaders.h +++ b/crates/gpui/src/platform/mac/shaders/shaders.h @@ -104,3 +104,19 @@ typedef struct vector_uchar4 border_color; float corner_radius; } GPUIImage; + +typedef enum +{ + GPUIUnderlineInputIndexVertices = 0, + GPUIUnderlineInputIndexUnderlines = 1, + GPUIUnderlineInputIndexUniforms = 2, +} GPUIUnderlineInputIndex; + +typedef struct +{ + vector_float2 origin; + vector_float2 size; + float thickness; + vector_uchar4 color; + uint8_t squiggly; +} GPUIUnderline; diff --git a/crates/gpui/src/platform/mac/shaders/shaders.metal b/crates/gpui/src/platform/mac/shaders/shaders.metal index 0cf7d290f2..385c8d25d8 100644 --- a/crates/gpui/src/platform/mac/shaders/shaders.metal +++ b/crates/gpui/src/platform/mac/shaders/shaders.metal @@ -66,21 +66,13 @@ float4 quad_sdf(QuadFragmentInput input) { border_width = vertical_border; } - float4 color; - if (border_width == 0.) { - color = input.background_color; - } else { - float4 border_color = float4(mix(float3(input.background_color), float3(input.border_color), input.border_color.a), 1.); + float4 color = input.background_color * float4(1., 1., 1., saturate(0.5 - distance)); + if (border_width != 0.) { float inset_distance = distance + border_width; - color = mix( - border_color, - input.background_color, - saturate(0.5 - inset_distance) - ); + color = mix(input.border_color, color, saturate(0.5 - inset_distance)); } - float4 coverage = float4(1., 1., 1., saturate(0.5 - distance)); - return coverage * color; + return color; } vertex QuadFragmentInput quad_vertex( @@ -304,3 +296,55 @@ fragment float4 path_atlas_fragment( float alpha = saturate(0.5 - distance); return float4(alpha, 0., 0., 1.); } + +struct UnderlineFragmentInput { + float4 position [[position]]; + float2 origin; + float2 size; + float thickness; + float4 color; + bool squiggly; +}; + +vertex UnderlineFragmentInput underline_vertex( + uint unit_vertex_id [[vertex_id]], + uint underline_id [[instance_id]], + constant float2 *unit_vertices [[buffer(GPUIUnderlineInputIndexVertices)]], + constant GPUIUnderline *underlines [[buffer(GPUIUnderlineInputIndexUnderlines)]], + constant GPUIUniforms *uniforms [[buffer(GPUIUnderlineInputIndexUniforms)]] +) { + float2 unit_vertex = unit_vertices[unit_vertex_id]; + GPUIUnderline underline = underlines[underline_id]; + float2 position = unit_vertex * underline.size + underline.origin; + float4 device_position = to_device_position(position, uniforms->viewport_size); + + return UnderlineFragmentInput { + device_position, + underline.origin, + underline.size, + underline.thickness, + coloru_to_colorf(underline.color), + underline.squiggly != 0, + }; +} + +fragment float4 underline_fragment( + UnderlineFragmentInput input [[stage_in]] +) { + if (input.squiggly) { + float half_thickness = input.thickness * 0.5; + float2 st = ((input.position.xy - input.origin) / input.size.y) - float2(0., 0.5); + float frequency = (M_PI_F * (3. * input.thickness)) / 8.; + float amplitude = 1. / (2. * input.thickness); + float sine = sin(st.x * frequency) * amplitude; + float dSine = cos(st.x * frequency) * amplitude * frequency; + float distance = (st.y - sine) / sqrt(1. + dSine * dSine); + float distance_in_pixels = distance * input.size.y; + float distance_from_top_border = distance_in_pixels - half_thickness; + float distance_from_bottom_border = distance_in_pixels + half_thickness; + float alpha = saturate(0.5 - max(-distance_from_bottom_border, distance_from_top_border)); + return input.color * float4(1., 1., 1., alpha); + } else { + return input.color; + } +} diff --git a/crates/gpui/src/scene.rs b/crates/gpui/src/scene.rs index b833ffe627..a5b2f6c8b8 100644 --- a/crates/gpui/src/scene.rs +++ b/crates/gpui/src/scene.rs @@ -25,7 +25,7 @@ struct StackingContext { pub struct Layer { clip_bounds: Option, quads: Vec, - underlines: Vec, + underlines: Vec, images: Vec, shadows: Vec, glyphs: Vec, @@ -76,6 +76,15 @@ pub struct Border { pub left: bool, } +#[derive(Clone, Copy, Default, Debug)] +pub struct Underline { + pub origin: Vector2F, + pub width: f32, + pub thickness: f32, + pub color: Color, + pub squiggly: bool, +} + impl<'de> Deserialize<'de> for Border { fn deserialize(deserializer: D) -> Result where @@ -183,7 +192,7 @@ impl Scene { self.active_layer().push_image(image) } - pub fn push_underline(&mut self, underline: Quad) { + pub fn push_underline(&mut self, underline: Underline) { self.active_layer().push_underline(underline) } @@ -277,11 +286,11 @@ impl Layer { self.quads.as_slice() } - fn push_underline(&mut self, underline: Quad) { + fn push_underline(&mut self, underline: Underline) { self.underlines.push(underline); } - pub fn underlines(&self) -> &[Quad] { + pub fn underlines(&self) -> &[Underline] { self.underlines.as_slice() } diff --git a/crates/gpui/src/text_layout.rs b/crates/gpui/src/text_layout.rs index 9c975ea491..6e371437bf 100644 --- a/crates/gpui/src/text_layout.rs +++ b/crates/gpui/src/text_layout.rs @@ -1,6 +1,6 @@ use crate::{ color::Color, - fonts::{FontId, GlyphId}, + fonts::{FontId, GlyphId, Underline}, geometry::{ rect::RectF, vector::{vec2f, Vector2F}, @@ -28,7 +28,7 @@ pub struct TextLayoutCache { pub struct RunStyle { pub color: Color, pub font_id: FontId, - pub underline: Option, + pub underline: Option, } impl TextLayoutCache { @@ -167,7 +167,7 @@ impl<'a> Hash for CacheKeyRef<'a> { #[derive(Default, Debug)] pub struct Line { layout: Arc, - style_runs: SmallVec<[(u32, Color, Option); 32]>, + style_runs: SmallVec<[(u32, Color, Option); 32]>, } #[derive(Default, Debug)] @@ -265,14 +265,20 @@ impl Line { let mut finished_underline = None; if glyph.index >= run_end { - if let Some((run_len, run_color, run_underline_color)) = style_runs.next() { - if let Some((_, underline_color)) = underline { - if *run_underline_color != Some(underline_color) { + if let Some((run_len, run_color, run_underline)) = style_runs.next() { + if let Some((_, underline_style)) = underline { + if *run_underline != Some(underline_style) { finished_underline = underline.take(); } } - if let Some(run_underline_color) = run_underline_color { - underline.get_or_insert((glyph_origin, *run_underline_color)); + if let Some(run_underline) = run_underline { + underline.get_or_insert(( + vec2f( + glyph_origin.x(), + origin.y() + baseline_offset.y() + 0.618 * self.layout.descent, + ), + *run_underline, + )); } run_end += *run_len as usize; @@ -288,12 +294,13 @@ impl Line { continue; } - if let Some((underline_origin, underline_color)) = finished_underline { - cx.scene.push_underline(scene::Quad { - bounds: RectF::from_points(underline_origin, glyph_origin + vec2f(0., 1.)), - background: Some(underline_color), - border: Default::default(), - corner_radius: 0., + if let Some((underline_origin, underline_style)) = finished_underline { + cx.scene.push_underline(scene::Underline { + origin: underline_origin, + width: glyph_origin.x() - underline_origin.x(), + thickness: underline_style.thickness.into(), + color: underline_style.color, + squiggly: underline_style.squiggly, }); } @@ -307,14 +314,14 @@ impl Line { } } - if let Some((underline_start, underline_color)) = underline.take() { - let line_end = origin + baseline_offset + vec2f(self.layout.width, 0.); - - cx.scene.push_underline(scene::Quad { - bounds: RectF::from_points(underline_start, line_end + vec2f(0., 1.)), - background: Some(underline_color), - border: Default::default(), - corner_radius: 0., + if let Some((underline_start, underline_style)) = underline.take() { + let line_end_x = origin.x() + self.layout.width; + cx.scene.push_underline(scene::Underline { + origin: underline_start, + width: line_end_x - underline_start.x(), + color: underline_style.color, + thickness: underline_style.thickness.into(), + squiggly: underline_style.squiggly, }); } } diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 72b883df0c..91d774f2a4 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -39,6 +39,10 @@ pub trait ToPointUtf16 { fn to_point_utf16(self) -> PointUtf16; } +pub trait DiagnosticProcessor: 'static + Send + Sync { + fn process_diagnostics(&self, diagnostics: &mut lsp::PublishDiagnosticsParams); +} + #[derive(Default, Deserialize)] pub struct LanguageConfig { pub name: String, @@ -69,6 +73,7 @@ pub struct BracketPair { pub struct Language { pub(crate) config: LanguageConfig, pub(crate) grammar: Option>, + pub(crate) diagnostic_processor: Option>, } pub struct Grammar { @@ -135,6 +140,7 @@ impl Language { highlight_map: Default::default(), }) }), + diagnostic_processor: None, } } @@ -178,6 +184,11 @@ impl Language { Ok(self) } + pub fn with_diagnostics_processor(mut self, processor: impl DiagnosticProcessor) -> Self { + self.diagnostic_processor = Some(Box::new(processor)); + self + } + pub fn name(&self) -> &str { self.config.name.as_str() } @@ -225,6 +236,12 @@ impl Language { .and_then(|config| config.disk_based_diagnostics_progress_token.as_ref()) } + pub fn process_diagnostics(&self, diagnostics: &mut lsp::PublishDiagnosticsParams) { + if let Some(processor) = self.diagnostic_processor.as_ref() { + processor.process_diagnostics(diagnostics); + } + } + pub fn brackets(&self) -> &[BracketPair] { &self.config.brackets } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 8cba48132c..e94c78ed49 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -822,7 +822,8 @@ impl Project { send.await.log_err(); } } - LspEvent::DiagnosticsUpdate(params) => { + LspEvent::DiagnosticsUpdate(mut params) => { + language.process_diagnostics(&mut params); this.update(&mut cx, |this, cx| { this.update_diagnostics(params, &disk_based_sources, cx) .log_err(); diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 3bd78f2634..c3d37b950c 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -109,10 +109,8 @@ pub struct StatusBar { #[serde(flatten)] pub container: ContainerStyle, pub height: f32, + pub item_spacing: f32, pub cursor_position: TextStyle, - pub diagnostic_icon_size: f32, - pub diagnostic_icon_spacing: f32, - pub diagnostic_icon_color: Color, pub diagnostic_message: TextStyle, } @@ -221,7 +219,7 @@ pub struct ContainedText { pub text: TextStyle, } -#[derive(Deserialize, Default)] +#[derive(Clone, Deserialize, Default)] pub struct ContainedLabel { #[serde(flatten)] pub container: ContainerStyle, @@ -235,6 +233,9 @@ pub struct ProjectDiagnostics { pub container: ContainerStyle, pub empty_message: TextStyle, pub status_bar_item: ContainedText, + pub tab_icon_width: f32, + pub tab_icon_spacing: f32, + pub tab_summary_spacing: f32, } #[derive(Clone, Deserialize, Default)] @@ -245,13 +246,15 @@ pub struct EditorStyle { pub background: Color, pub selection: SelectionStyle, pub gutter_background: Color, + pub gutter_padding_factor: f32, pub active_line_background: Color, pub highlighted_line_background: Color, pub line_number: Color, pub line_number_active: Color, pub guest_selections: Vec, pub syntax: Arc, - pub diagnostic_path_header: DiagnosticStyle, + pub diagnostic_path_header: DiagnosticPathHeader, + pub diagnostic_header: DiagnosticHeader, pub error_diagnostic: DiagnosticStyle, pub invalid_error_diagnostic: DiagnosticStyle, pub warning_diagnostic: DiagnosticStyle, @@ -262,11 +265,31 @@ pub struct EditorStyle { pub invalid_hint_diagnostic: DiagnosticStyle, } -#[derive(Copy, Clone, Deserialize, Default)] +#[derive(Clone, Deserialize, Default)] +pub struct DiagnosticPathHeader { + #[serde(flatten)] + pub container: ContainerStyle, + pub filename: ContainedText, + pub path: ContainedText, + pub text_scale_factor: f32, +} + +#[derive(Clone, Deserialize, Default)] +pub struct DiagnosticHeader { + #[serde(flatten)] + pub container: ContainerStyle, + pub message: ContainedLabel, + pub code: ContainedText, + pub text_scale_factor: f32, + pub icon_width_factor: f32, +} + +#[derive(Clone, Deserialize, Default)] pub struct DiagnosticStyle { - pub text: Color, + pub message: LabelStyle, #[serde(default)] pub header: ContainerStyle, + pub text_scale_factor: f32, } #[derive(Clone, Copy, Default, Deserialize)] @@ -302,6 +325,11 @@ impl EditorStyle { impl InputEditorStyle { pub fn as_editor(&self) -> EditorStyle { + let default_diagnostic_style = DiagnosticStyle { + message: self.text.clone().into(), + header: Default::default(), + text_scale_factor: 1., + }; EditorStyle { text: self.text.clone(), placeholder_text: self.placeholder_text.clone(), @@ -311,21 +339,46 @@ impl InputEditorStyle { .unwrap_or(Color::transparent_black()), selection: self.selection, gutter_background: Default::default(), + gutter_padding_factor: Default::default(), active_line_background: Default::default(), highlighted_line_background: Default::default(), line_number: Default::default(), line_number_active: Default::default(), guest_selections: Default::default(), syntax: Default::default(), - diagnostic_path_header: Default::default(), - error_diagnostic: Default::default(), - invalid_error_diagnostic: Default::default(), - warning_diagnostic: Default::default(), - invalid_warning_diagnostic: Default::default(), - information_diagnostic: Default::default(), - invalid_information_diagnostic: Default::default(), - hint_diagnostic: Default::default(), - invalid_hint_diagnostic: Default::default(), + diagnostic_path_header: DiagnosticPathHeader { + container: Default::default(), + filename: ContainedText { + container: Default::default(), + text: self.text.clone(), + }, + path: ContainedText { + container: Default::default(), + text: self.text.clone(), + }, + text_scale_factor: 1., + }, + diagnostic_header: DiagnosticHeader { + container: Default::default(), + message: ContainedLabel { + container: Default::default(), + label: self.text.clone().into(), + }, + code: ContainedText { + container: Default::default(), + text: self.text.clone(), + }, + icon_width_factor: Default::default(), + text_scale_factor: 1., + }, + error_diagnostic: default_diagnostic_style.clone(), + invalid_error_diagnostic: default_diagnostic_style.clone(), + warning_diagnostic: default_diagnostic_style.clone(), + invalid_warning_diagnostic: default_diagnostic_style.clone(), + information_diagnostic: default_diagnostic_style.clone(), + invalid_information_diagnostic: default_diagnostic_style.clone(), + hint_diagnostic: default_diagnostic_style.clone(), + invalid_hint_diagnostic: default_diagnostic_style.clone(), } } } diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 17f370ac4f..739d07aab1 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -70,8 +70,6 @@ pub enum Event { Split(SplitDirection), } -const MAX_TAB_TITLE_LEN: usize = 24; - pub struct Pane { item_views: Vec<(usize, Box)>, active_item_index: usize, @@ -79,6 +77,11 @@ pub struct Pane { nav_history: Rc>, } +// #[derive(Debug, Eq, PartialEq)] +// pub struct State { +// pub tabs: Vec, +// } + pub struct ItemNavHistory { history: Rc>, item_view: Rc, @@ -373,15 +376,12 @@ impl Pane { let is_active = ix == self.active_item_index; row.add_child({ - let mut title = item_view.title(cx); - if title.len() > MAX_TAB_TITLE_LEN { - let mut truncated_len = MAX_TAB_TITLE_LEN; - while !title.is_char_boundary(truncated_len) { - truncated_len -= 1; - } - title.truncate(truncated_len); - title.push('…'); - } + let tab_style = if is_active { + theme.workspace.active_tab.clone() + } else { + theme.workspace.tab.clone() + }; + let title = item_view.tab_content(&tab_style, cx); let mut style = if is_active { theme.workspace.active_tab.clone() @@ -430,29 +430,16 @@ impl Pane { .boxed(), ) .with_child( - Container::new( - Align::new( - Label::new( - title, - if is_active { - theme.workspace.active_tab.label.clone() - } else { - theme.workspace.tab.label.clone() - }, - ) - .boxed(), - ) - .boxed(), - ) - .with_style(ContainerStyle { - margin: Margin { - left: style.spacing, - right: style.spacing, + Container::new(Align::new(title).boxed()) + .with_style(ContainerStyle { + margin: Margin { + left: style.spacing, + right: style.spacing, + ..Default::default() + }, ..Default::default() - }, - ..Default::default() - }) - .boxed(), + }) + .boxed(), ) .with_child( Align::new( diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 86f399a86a..48dcb4907b 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -155,7 +155,7 @@ pub trait ItemView: View { fn deactivated(&mut self, _: &mut ViewContext) {} fn navigate(&mut self, _: Box, _: &mut ViewContext) {} fn item_handle(&self, cx: &AppContext) -> Self::ItemHandle; - fn title(&self, cx: &AppContext) -> String; + fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox; fn project_path(&self, cx: &AppContext) -> Option; fn clone_on_split(&self, _: &mut ViewContext) -> Option where @@ -223,7 +223,7 @@ pub trait WeakItemHandle { pub trait ItemViewHandle: 'static { fn item_handle(&self, cx: &AppContext) -> Box; - fn title(&self, cx: &AppContext) -> String; + fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox; fn project_path(&self, cx: &AppContext) -> Option; fn boxed_clone(&self) -> Box; fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option>; @@ -358,8 +358,8 @@ impl ItemViewHandle for ViewHandle { Box::new(self.read(cx).item_handle(cx)) } - fn title(&self, cx: &AppContext) -> String { - self.read(cx).title(cx) + fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox { + self.read(cx).tab_content(style, cx) } fn project_path(&self, cx: &AppContext) -> Option { diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 6ad90e8f6e..d288d22885 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -73,6 +73,7 @@ num_cpus = "1.13.0" parking_lot = "0.11.1" postage = { version = "0.4.1", features = ["futures-traits"] } rand = "0.8.3" +regex = "1.5" rsa = "0.4" rust-embed = { version = "6.2", features = ["include-exclude"] } serde = { version = "1", features = ["derive"] } diff --git a/crates/zed/assets/icons/diagnostic-error-10.svg b/crates/zed/assets/icons/diagnostic-error-10.svg new file mode 100644 index 0000000000..d01bc3a23d --- /dev/null +++ b/crates/zed/assets/icons/diagnostic-error-10.svg @@ -0,0 +1,3 @@ + + + diff --git a/crates/zed/assets/icons/diagnostic-summary-error.svg b/crates/zed/assets/icons/diagnostic-summary-error.svg new file mode 100644 index 0000000000..0180762a9e --- /dev/null +++ b/crates/zed/assets/icons/diagnostic-summary-error.svg @@ -0,0 +1,3 @@ + + + diff --git a/crates/zed/assets/icons/diagnostic-summary-warning.svg b/crates/zed/assets/icons/diagnostic-summary-warning.svg new file mode 100644 index 0000000000..fead4db839 --- /dev/null +++ b/crates/zed/assets/icons/diagnostic-summary-warning.svg @@ -0,0 +1,3 @@ + + + diff --git a/crates/zed/assets/icons/diagnostic-warning-10.svg b/crates/zed/assets/icons/diagnostic-warning-10.svg new file mode 100644 index 0000000000..bfd9ed7c6f --- /dev/null +++ b/crates/zed/assets/icons/diagnostic-warning-10.svg @@ -0,0 +1,3 @@ + + + diff --git a/crates/zed/assets/icons/warning.svg b/crates/zed/assets/icons/warning.svg deleted file mode 100644 index 09ebc28669..0000000000 --- a/crates/zed/assets/icons/warning.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/crates/zed/assets/themes/_base.toml b/crates/zed/assets/themes/_base.toml index c4c3cf5125..7f343ba1ac 100644 --- a/crates/zed/assets/themes/_base.toml +++ b/crates/zed/assets/themes/_base.toml @@ -77,11 +77,9 @@ border = { width = 1, color = "$border.0", left = true } [workspace.status_bar] padding = { left = 6, right = 6 } height = 24 +item_spacing = 24 cursor_position = "$text.2" diagnostic_message = "$text.2" -diagnostic_icon_size = 18 -diagnostic_icon_spacing = 8 -diagnostic_icon_color = "$text.2.color" [panel] padding = { top = 12, left = 12, bottom = 12, right = 12 } @@ -244,6 +242,7 @@ text = "$text.0" text = "$text.1" background = "$surface.1" gutter_background = "$surface.1" +gutter_padding_factor = 2.5 active_line_background = "$state.active_line" highlighted_line_background = "$state.highlighted_line" line_number = "$text.2.color" @@ -251,33 +250,70 @@ line_number_active = "$text.0.color" selection = "$selection.host" guest_selections = "$selection.guests" error_color = "$status.bad" -invalid_error_diagnostic = { text = "$text.3.color" } -invalid_warning_diagnostic = { text = "$text.3.color" } -invalid_information_diagnostic = { text = "$text.3.color" } -invalid_hint_diagnostic = { text = "$text.3.color" } [editor.diagnostic_path_header] -text = "$text.0.color" -header.background = "#ffffff08" -header.border = { width = 1, top = true, color = "$border.0" } +background = "$state.active_line" +filename = { extends = "$text.0", size = 14 } +path = { extends = "$text.2", size = 14, margin.left = 12 } +text_scale_factor = 0.857 + +[editor.diagnostic_header] +border = { width = 1, top = true, bottom = true, color = "$border.1" } +code = { extends = "$text.2", size = 14, margin.left = 10 } +icon_width_factor = 1.5 +text_scale_factor = 0.857 + +[editor.diagnostic_header.message] +text = { extends = "$text.1", size = 14 } +highlight_text = { extends = "$text.0", size = 14, weight = "bold" } [editor.error_diagnostic] -text = "$status.bad" header.border = { width = 1, top = true, color = "$border.0" } +text_scale_factor = 0.857 + +[editor.error_diagnostic.message] +text = { extends = "$editor.text", size = 14, color = "$status.bad" } +highlight_text = { extends = "$editor.text", size = 14, color = "$status.bad", weight = "bold" } [editor.warning_diagnostic] -text = "$status.warn" -header.border = { width = 1, top = true, color = "$border.0" } +extends = "$editor.error_diagnostic" +message.text.color = "$status.warn" +message.highlight_text.color = "$status.warn" [editor.information_diagnostic] -text = "$status.info" -border = { width = 1, top = true, color = "$border.0" } +extends = "$editor.error_diagnostic" +message.text.color = "$status.info" +message.highlight_text.color = "$status.info" [editor.hint_diagnostic] -text = "$status.info" -border = { width = 1, top = true, color = "$border.0" } +extends = "$editor.error_diagnostic" +message.text.color = "$status.info" +message.highlight_text.color = "$status.info" + +[editor.invalid_error_diagnostic] +extends = "$editor.error_diagnostic" +message.text.color = "$text.3.color" +message.highlight_text.color = "$text.3.color" + +[editor.invalid_warning_diagnostic] +extends = "$editor.warning_diagnostic" +message.text.color = "$text.3.color" +message.highlight_text.color = "$text.3.color" + +[editor.invalid_information_diagnostic] +extends = "$editor.information_diagnostic" +message.text.color = "$text.3.color" +message.highlight_text.color = "$text.3.color" + +[editor.invalid_hint_diagnostic] +extends = "$editor.hint_diagnostic" +message.text.color = "$text.3.color" +message.highlight_text.color = "$text.3.color" [project_diagnostics] background = "$surface.1" empty_message = "$text.0" status_bar_item = { extends = "$text.2", margin.right = 10 } +tab_icon_width = 13 +tab_icon_spacing = 4 +tab_summary_spacing = 10 diff --git a/crates/zed/assets/themes/black.toml b/crates/zed/assets/themes/black.toml index a822fa7d33..47fce47b06 100644 --- a/crates/zed/assets/themes/black.toml +++ b/crates/zed/assets/themes/black.toml @@ -1,12 +1,13 @@ extends = "_base" [surface] -0 = "#222324" -1 = "#141516" +0 = "#222222" +1 = "#0f0b0c" 2 = "#131415" [border] -0 = "#0F1011" +0 = "#000000B2" +1 = "#FFFFFF16" [text] 0 = { extends = "$text.base", color = "#ffffff" } @@ -36,7 +37,7 @@ warn = "#faca50" bad = "#b7372e" [state] -active_line = "#00000033" +active_line = "#161313" highlighted_line = "#faca5033" hover = "#00000033" @@ -50,7 +51,6 @@ comment = "#6a9955" property = "#4e94ce" variant = "#4fc1ff" constant = "#9cdcfe" - title = { color = "#9cdcfe", weight = "bold" } emphasis = "#4ec9b0" "emphasis.strong" = { color = "#4ec9b0", weight = "bold" } diff --git a/crates/zed/assets/themes/dark.toml b/crates/zed/assets/themes/dark.toml index 9d65a160eb..bd6e473a7a 100644 --- a/crates/zed/assets/themes/dark.toml +++ b/crates/zed/assets/themes/dark.toml @@ -7,6 +7,7 @@ extends = "_base" [border] 0 = "#1B222B" +1 = "#FFFFFF16" [text] 0 = { extends = "$text.base", color = "#FFFFFF" } diff --git a/crates/zed/assets/themes/light.toml b/crates/zed/assets/themes/light.toml index 18134501ec..3113a69110 100644 --- a/crates/zed/assets/themes/light.toml +++ b/crates/zed/assets/themes/light.toml @@ -7,6 +7,7 @@ extends = "_base" [border] 0 = "#DDDDDC" +1 = "#0000000F" [text] 0 = { extends = "$text.base", color = "#000000" } diff --git a/crates/zed/src/language.rs b/crates/zed/src/language.rs index 98f6ab93d2..90ce03f57a 100644 --- a/crates/zed/src/language.rs +++ b/crates/zed/src/language.rs @@ -1,4 +1,6 @@ pub use language::*; +use lazy_static::lazy_static; +use regex::Regex; use rust_embed::RustEmbed; use std::borrow::Cow; use std::{str, sync::Arc}; @@ -7,6 +9,30 @@ use std::{str, sync::Arc}; #[folder = "languages"] struct LanguageDir; +struct RustDiagnosticProcessor; + +impl DiagnosticProcessor for RustDiagnosticProcessor { + fn process_diagnostics(&self, params: &mut lsp::PublishDiagnosticsParams) { + lazy_static! { + static ref REGEX: Regex = Regex::new("(?m)`([^`]+)\n`").unwrap(); + } + + for diagnostic in &mut params.diagnostics { + for message in diagnostic + .related_information + .iter_mut() + .flatten() + .map(|info| &mut info.message) + .chain([&mut diagnostic.message]) + { + if let Cow::Owned(sanitized) = REGEX.replace_all(message, "`$1`") { + *message = sanitized; + } + } + } + } +} + pub fn build_language_registry() -> LanguageRegistry { let mut languages = LanguageRegistry::default(); languages.add(Arc::new(rust())); @@ -26,6 +52,7 @@ fn rust() -> Language { .unwrap() .with_outline_query(load_query("rust/outline.scm").as_ref()) .unwrap() + .with_diagnostics_processor(RustDiagnosticProcessor) } fn markdown() -> Language { diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index fc7ae9a3c5..d51376afb0 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -378,6 +378,10 @@ mod tests { .read(cx) .active_item() .unwrap() + .to_any() + .downcast::() + .unwrap() + .read(cx) .title(cx), "a.txt" ); @@ -408,6 +412,10 @@ mod tests { .read(cx) .active_item() .unwrap() + .to_any() + .downcast::() + .unwrap() + .read(cx) .title(cx), "b.txt" ); @@ -491,14 +499,14 @@ 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.is_dirty(cx)); + assert_eq!(editor.title(cx), "untitled"); assert!(Arc::ptr_eq( editor.language(cx).unwrap(), &language::PLAIN_TEXT )); editor.handle_input(&editor::Input("hi".into()), cx); - assert!(editor.is_dirty(cx.as_ref())); + assert!(editor.is_dirty(cx)); }); // Save the buffer. This prompts for a filename. @@ -509,7 +517,7 @@ mod tests { }); cx.read(|cx| { assert!(editor.is_dirty(cx)); - assert_eq!(editor.title(cx), "untitled"); + assert_eq!(editor.read(cx).title(cx), "untitled"); }); // When the save completes, the buffer's title is updated and the language is assigned based