1use gpui_kit::component::highlighter::{Diagnostic, DiagnosticSeverity};
5use gpui_kit::component::input::{Editor, EditorState, GutterDecoration, GutterDecorationCollection, InputEvent, Position};
6use gpui_kit::assets::IconName;
7use gpui_kit::component::{ActiveTheme as _, Icon, h_flex, v_flex};
8use gpui_kit::prelude::FluentBuilder as _;
9use gpui_kit::{
10 AppContext as _, Context, Entity, EventEmitter, FontWeight, InteractiveElement as _, IntoElement, ParentElement as _, Pixels, Render, SharedString,
11 StatefulInteractiveElement as _, Styled as _, Subscription, TestSupportExt as _, Window, div, px,
12};
13
14use crate::theme::{self, color};
15
16#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
18pub enum CodeLanguage {
19 Rust,
21 Python,
23 Json,
25 #[default]
27 Plain,
28}
29
30impl CodeLanguage {
31 pub fn name(self) -> &'static str {
33 match self {
34 CodeLanguage::Rust => "rust",
35 CodeLanguage::Python => "python",
36 CodeLanguage::Json => "json",
37 CodeLanguage::Plain => "text",
38 }
39 }
40
41 pub fn from_name(name: &str) -> Self {
44 match name.trim().to_ascii_lowercase().as_str() {
45 "rust" | "rs" => CodeLanguage::Rust,
46 "python" | "py" | "pyi" => CodeLanguage::Python,
47 "json" | "jsonc" => CodeLanguage::Json,
48 _ => CodeLanguage::Plain,
49 }
50 }
51}
52
53#[derive(Clone, Debug, PartialEq, Eq)]
55pub struct CodeChanged(pub String);
56
57pub struct CodeView {
71 editor: Entity<EditorState>,
72 language: CodeLanguage,
73 errors: Vec<(usize, String)>,
74 editable: bool,
75 max_lines: Option<usize>,
76 marks: GutterDecorationCollection,
77 pending: bool,
79 _subs: Vec<Subscription>,
80}
81
82impl EventEmitter<CodeChanged> for CodeView {}
83
84impl CodeView {
85 pub fn new(language: CodeLanguage, text: impl Into<SharedString>, window: &mut Window, cx: &mut Context<Self>) -> Self {
87 let text: SharedString = text.into();
88 let editor = cx.new(|cx| {
89 let mut state = EditorState::new(window, cx).language(language.name()).default_value(text).gutter_decorations_width(px(4.));
90 state.set_readonly(true, cx);
91 state
92 });
93 let marks = editor.update(cx, |state, cx| state.create_gutter_decorations_collection(Vec::new(), cx));
94 let sub = cx.subscribe(&editor, |this: &mut Self, editor, e: &InputEvent, cx| {
95 if matches!(e, InputEvent::Change) && this.editable {
96 let text = editor.read(cx).value().to_string();
97 cx.emit(CodeChanged(text));
99 }
100 });
101 Self { editor, language, errors: Vec::new(), editable: false, max_lines: None, marks, pending: false, _subs: vec![sub] }
102 }
103
104 pub fn errors(mut self, errors: Vec<(usize, String)>) -> Self {
106 self.errors = errors;
107 self.pending = true;
108 self
109 }
110
111 pub fn editable(mut self, editable: bool) -> Self {
113 self.editable = editable;
114 self.pending = true;
115 self
116 }
117
118 pub fn max_lines(mut self, lines: usize) -> Self {
121 self.max_lines = Some(lines.max(1));
122 self
123 }
124
125 pub fn set_errors(&mut self, errors: Vec<(usize, String)>, cx: &mut Context<Self>) {
127 self.errors = errors;
128 self.apply_errors(cx);
129 cx.notify();
130 }
131
132 pub fn error_list(&self) -> &[(usize, String)] {
134 &self.errors
135 }
136
137 pub fn set_editable(&mut self, editable: bool, cx: &mut Context<Self>) {
139 self.editable = editable;
140 self.editor.update(cx, |state, cx| state.set_readonly(!editable, cx));
141 cx.notify();
142 }
143
144 pub fn is_editable(&self) -> bool {
146 self.editable
147 }
148
149 pub fn set_text(&mut self, text: impl Into<SharedString>, window: &mut Window, cx: &mut Context<Self>) {
151 let text: SharedString = text.into();
152 self.editor.update(cx, |state, cx| state.set_value(text, window, cx));
153 self.apply_errors(cx);
154 cx.notify();
155 }
156
157 pub fn text(&self, cx: &gpui_kit::App) -> String {
159 self.editor.read(cx).value().to_string()
160 }
161
162 pub fn set_language(&mut self, language: CodeLanguage, cx: &mut Context<Self>) {
164 self.language = language;
165 self.editor.update(cx, |state, cx| state.set_highlighter(language.name(), cx));
166 cx.notify();
167 }
168
169 pub fn language(&self) -> CodeLanguage {
171 self.language
172 }
173
174 pub fn editor(&self) -> &Entity<EditorState> {
176 &self.editor
177 }
178
179 pub fn go_to_line(&mut self, line: usize, window: &mut Window, cx: &mut Context<Self>) {
181 let row = line.saturating_sub(1) as u32;
182 self.editor.update(cx, |state, cx| {
183 state.set_cursor_position(Position::new(row, 0), window, cx);
184 state.focus(window, cx);
185 });
186 }
187
188 fn apply_errors(&mut self, cx: &mut Context<Self>) {
190 let danger = color::danger();
191 let errors = self.errors.clone();
192 let marks = self.editor.update(cx, |state, cx| {
193 let text = state.value().to_string();
194 let starts: Vec<usize> = std::iter::once(0).chain(text.match_indices('\n').map(|(i, _)| i + 1)).collect();
195 let mut marks = Vec::new();
196 if let Some(set) = state.diagnostics_mut() {
197 set.clear();
198 for (line, message) in &errors {
199 let Some(&start) = starts.get(line.saturating_sub(1)) else { continue };
200 let end = text[start..].find('\n').map_or(text.len(), |i| start + i);
201 let row = line.saturating_sub(1) as u32;
202 let chars = text[start..end].encode_utf16().count() as u32;
203 set.push(Diagnostic::new(Position::new(row, 0)..Position::new(row, chars.max(1)), message.clone()).with_severity(DiagnosticSeverity::Error));
204 let range = if end > start { start..end } else { start..(start + 1).min(text.len()) };
206 marks.push(GutterDecoration::new(range).with_color(danger));
207 }
208 }
209 cx.notify();
210 marks
211 });
212 self.marks.set(marks, cx);
213 }
214
215 fn height(&self, cx: &gpui_kit::App) -> Option<Pixels> {
216 let max = self.max_lines?;
217 let lines = (self.editor.read(cx).value().matches('\n').count() + 1).clamp(1, max);
218 let row = cx.theme().mono_font_size * 1.5;
219 Some(row * lines as f32 + px(theme::MD + 2.))
220 }
221}
222
223impl Render for CodeView {
224 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
225 if std::mem::take(&mut self.pending) {
226 let editable = self.editable;
227 self.editor.update(cx, |state, cx| state.set_readonly(!editable, cx));
228 self.apply_errors(cx);
229 }
230 let height = self.height(cx);
231 let editor = Editor::new(&self.editor).readonly(!self.editable).when_some(height, |e, h| e.h(h)).when(height.is_none(), |e| e.h_full());
232 let id = cx.entity_id();
233 let list = self.errors.iter().enumerate().map(|(i, (line, message))| {
234 let line = *line;
235 h_flex()
236 .id(SharedString::from(format!("code-{id}-error-{i}")))
237 .w_full()
238 .min_h(px(theme::TARGET_MIN))
239 .px(px(theme::SM))
240 .gap(px(theme::SM))
241 .items_start()
242 .rounded(px(theme::RADIUS))
243 .cursor_pointer()
244 .hover(|d| d.bg(color::hover()))
245 .child(div().flex_none().mt(px(5.)).w(px(3.)).h(px(14.)).rounded(px(1.)).bg(color::danger()))
246 .child(div().flex_none().py(px(3.)).text_sm().font_weight(FontWeight::SEMIBOLD).text_color(color::danger()).child(format!("Line {line}")))
247 .child(div().flex_1().min_w_0().py(px(3.)).text_sm().child(message.clone()))
248 .on_click(cx.listener(move |this, _, window, cx| this.go_to_line(line, window, cx)))
249 .test_support()
250 });
251 let has_errors = !self.errors.is_empty();
252 v_flex()
253 .w_full()
254 .when(height.is_none(), |d| d.h_full())
255 .gap(px(theme::XS))
256 .child(div().w_full().min_h_0().when(height.is_none(), |d| d.flex_1()).child(editor))
257 .when(has_errors, |d| {
258 d.child(
259 v_flex()
260 .flex_none()
261 .w_full()
262 .child(
263 h_flex()
264 .gap(px(theme::XS))
265 .px(px(theme::SM))
266 .text_xs()
267 .text_color(color::text_muted())
268 .child(Icon::new(IconName::CircleAlert).size(px(12.)).text_color(color::danger()))
269 .child(if self.errors.len() == 1 { "1 error".to_string() } else { format!("{} errors", self.errors.len()) }),
270 )
271 .children(list),
272 )
273 })
274 }
275}