Merge pull request #366 from zed-industries/style-project-diagnostics

Style project diagnostics
This commit is contained in:
Nathan Sobo 2022-01-26 09:18:20 -07:00 committed by GitHub
commit e54d6f62fb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 882 additions and 355 deletions

25
Cargo.lock generated
View file

@ -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",

View file

@ -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"] }

View file

@ -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<ProjectDiagnostics>,
workspace: WeakViewHandle<Workspace>,
editor: ViewHandle<Editor>,
summary: DiagnosticSummary,
excerpts: ModelHandle<MultiBuffer>,
path_states: Vec<PathState>,
paths_to_update: BTreeSet<ProjectPath>,
@ -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<project::ProjectPath> {
@ -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<Self>) -> Option<Self>
@ -651,20 +656,43 @@ impl workspace::ItemView for ProjectDiagnosticsEditor {
fn path_header_renderer(buffer: ModelHandle<Buffer>, 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<Buffer>, 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<L: language::ToOffset, R: language::ToOffset>(
lhs: &DiagnosticEntry<L>,
rhs: &DiagnosticEntry<R>,
@ -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()))

View file

@ -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::<Tag, _, _, _>(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))

View file

@ -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()

View file

@ -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<usize>) {
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.,
},
}
}

View file

@ -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<u32>,
active_rows: &BTreeMap<u32, bool>,
@ -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<u32>,
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<u32, bool>,
highlighted_rows: Option<Range<u32>>,
@ -932,30 +949,7 @@ pub struct LayoutState {
em_width: f32,
em_advance: f32,
selections: HashMap<ReplicaId, Vec<text::Selection<DisplayPoint>>>,
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);
}

View file

@ -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<ProjectPath> {
@ -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<Self>) -> 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()
}

View file

@ -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::<Editor>()
.unwrap()
.read(cx)
.title(cx),
"bandana"
);
});
}

View file

@ -33,6 +33,13 @@ impl From<TextStyle> 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<LabelStyle>) -> Self {
Self {

View file

@ -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<Color>,
pub underline: Option<Underline>,
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub struct HighlightStyle {
pub color: Color,
pub font_properties: Properties,
pub underline: Option<Color>,
pub underline: Option<Underline>,
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub struct Underline {
pub color: Color,
pub thickness: OrderedFloat<f32>,
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<Color>,
#[serde(default)]
thickness: Option<f32>,
#[serde(default)]
squiggly: bool,
},
}
impl TextStyle {
@ -89,7 +104,7 @@ impl TextStyle {
font_family_name: impl Into<Arc<str>>,
font_size: f32,
font_properties: Properties,
underline: Option<Color>,
underline: Option<Underline>,
color: Color,
font_cache: &FontCache,
) -> anyhow::Result<Self> {
@ -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<Color> {
fn underline_from_json(json: UnderlineStyleJson, text_color: Color) -> Option<Underline> {
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,
}),
}
}

View file

@ -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::<shaders::GPUIUnderline>();
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::<shaders::GPUIUniforms>() 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 {

View file

@ -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;

View file

@ -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;
}
}

View file

@ -25,7 +25,7 @@ struct StackingContext {
pub struct Layer {
clip_bounds: Option<RectF>,
quads: Vec<Quad>,
underlines: Vec<Quad>,
underlines: Vec<Underline>,
images: Vec<Image>,
shadows: Vec<Shadow>,
glyphs: Vec<Glyph>,
@ -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<D>(deserializer: D) -> Result<Self, D::Error>
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()
}

View file

@ -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<Color>,
pub underline: Option<Underline>,
}
impl TextLayoutCache {
@ -167,7 +167,7 @@ impl<'a> Hash for CacheKeyRef<'a> {
#[derive(Default, Debug)]
pub struct Line {
layout: Arc<LineLayout>,
style_runs: SmallVec<[(u32, Color, Option<Color>); 32]>,
style_runs: SmallVec<[(u32, Color, Option<Underline>); 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,
});
}
}

View file

@ -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<Arc<Grammar>>,
pub(crate) diagnostic_processor: Option<Box<dyn DiagnosticProcessor>>,
}
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
}

View file

@ -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();

View file

@ -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<SelectionStyle>,
pub syntax: Arc<SyntaxTheme>,
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(),
}
}
}

View file

@ -70,8 +70,6 @@ pub enum Event {
Split(SplitDirection),
}
const MAX_TAB_TITLE_LEN: usize = 24;
pub struct Pane {
item_views: Vec<(usize, Box<dyn ItemViewHandle>)>,
active_item_index: usize,
@ -79,6 +77,11 @@ pub struct Pane {
nav_history: Rc<RefCell<NavHistory>>,
}
// #[derive(Debug, Eq, PartialEq)]
// pub struct State {
// pub tabs: Vec<TabState>,
// }
pub struct ItemNavHistory {
history: Rc<RefCell<NavHistory>>,
item_view: Rc<dyn WeakItemViewHandle>,
@ -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(

View file

@ -155,7 +155,7 @@ pub trait ItemView: View {
fn deactivated(&mut self, _: &mut ViewContext<Self>) {}
fn navigate(&mut self, _: Box<dyn Any>, _: &mut ViewContext<Self>) {}
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<ProjectPath>;
fn clone_on_split(&self, _: &mut ViewContext<Self>) -> Option<Self>
where
@ -223,7 +223,7 @@ pub trait WeakItemHandle {
pub trait ItemViewHandle: 'static {
fn item_handle(&self, cx: &AppContext) -> Box<dyn ItemHandle>;
fn title(&self, cx: &AppContext) -> String;
fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox;
fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
fn boxed_clone(&self) -> Box<dyn ItemViewHandle>;
fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option<Box<dyn ItemViewHandle>>;
@ -358,8 +358,8 @@ impl<T: ItemView> ItemViewHandle for ViewHandle<T> {
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<ProjectPath> {

View file

@ -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"] }

View file

@ -0,0 +1,3 @@
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5 10C7.76142 10 10 7.76142 10 5C10 2.23858 7.76142 0 5 0C2.23858 0 0 2.23858 0 5C0 7.76142 2.23858 10 5 10ZM2.68306 3.56694L4.11612 5L2.68306 6.43306L3.56694 7.31694L5 5.88388L6.43306 7.31694L7.31694 6.43306L5.88388 5L7.31694 3.56694L6.43306 2.68306L5 4.11612L3.56694 2.68306L2.68306 3.56694Z" fill="#EF4444"/>
</svg>

After

Width:  |  Height:  |  Size: 464 B

View file

@ -0,0 +1,3 @@
<svg width="13" height="13" viewBox="0 0 13 13" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.5 13C10.0899 13 13 10.0899 13 6.5C13 2.91015 10.0899 0 6.5 0C2.91015 0 0 2.91015 0 6.5C0 10.0899 2.91015 13 6.5 13ZM3.48798 4.63702L5.35095 6.5L3.48798 8.36298L4.63702 9.51202L6.5 7.64905L8.36298 9.51202L9.51202 8.36298L7.64905 6.5L9.51202 4.63702L8.36298 3.48798L6.5 5.35095L4.63702 3.48798L3.48798 4.63702Z" fill="white" fill-opacity="0.6"/>
</svg>

After

Width:  |  Height:  |  Size: 499 B

View file

@ -0,0 +1,3 @@
<svg width="13" height="13" viewBox="0 0 13 13" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M13 13H0V10.4L5.6875 0H7.3125L13 10.4V13ZM5.6875 3.46667H7.3125V7.8H5.6875V3.46667ZM5.6875 9.53333H7.3125V11.2667H5.6875V9.53333Z" fill="white" fill-opacity="0.6"/>
</svg>

After

Width:  |  Height:  |  Size: 317 B

View file

@ -0,0 +1,3 @@
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 10H0V8L4.375 0H5.625L10 8V10ZM4.375 2.66667H5.625V6H4.375V2.66667ZM4.375 7.33333H5.625V8.66667H4.375V7.33333Z" fill="#FDE047"/>
</svg>

After

Width:  |  Height:  |  Size: 284 B

View file

@ -1,3 +0,0 @@
<svg width="12" height="10" viewBox="0 0 12 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.1329 8.29967L6.76186 0.840494C6.59383 0.553602 6.29874 0.410156 6.00365 0.410156C5.70856 0.410156 5.41347 0.553602 5.22699 0.840494L0.858047 8.29967C0.540622 8.87141 0.959504 9.59068 1.63347 9.59068H10.3755C11.0468 9.59068 11.4669 8.87346 11.1329 8.29967ZM1.83512 8.60706L5.98521 1.49215L10.1718 8.60706H1.83512ZM6.00365 6.66234C5.64791 6.66234 5.35937 6.95087 5.35937 7.30662C5.35937 7.66236 5.64852 7.95089 6.00447 7.95089C6.36042 7.95089 6.64793 7.66236 6.64793 7.30662C6.64711 6.95128 6.36022 6.66234 6.00365 6.66234ZM5.51184 3.52498V5.49223C5.51184 5.76478 5.73315 5.98405 6.00365 5.98405C6.27415 5.98405 6.49546 5.76376 6.49546 5.49223V3.52498C6.49546 3.25448 6.2762 3.03316 6.00365 3.03316C5.7311 3.03316 5.51184 3.25448 5.51184 3.52498Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 877 B

View file

@ -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

View file

@ -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" }

View file

@ -7,6 +7,7 @@ extends = "_base"
[border]
0 = "#1B222B"
1 = "#FFFFFF16"
[text]
0 = { extends = "$text.base", color = "#FFFFFF" }

View file

@ -7,6 +7,7 @@ extends = "_base"
[border]
0 = "#DDDDDC"
1 = "#0000000F"
[text]
0 = { extends = "$text.base", color = "#000000" }

View file

@ -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 {

View file

@ -378,6 +378,10 @@ mod tests {
.read(cx)
.active_item()
.unwrap()
.to_any()
.downcast::<Editor>()
.unwrap()
.read(cx)
.title(cx),
"a.txt"
);
@ -408,6 +412,10 @@ mod tests {
.read(cx)
.active_item()
.unwrap()
.to_any()
.downcast::<Editor>()
.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