on 2158366 help sign in
helix/helix-view/src/editor.rs
389 lines 12 kB history raw
1use crate::{
2 clipboard::{get_clipboard_provider, ClipboardProvider},
3 graphics::{CursorKind, Rect},
4 theme::{self, Theme},
5 tree::Tree,
6 Document, DocumentId, RegisterSelection, View, ViewId,
7};
9use futures_util::future;
10use futures_util::stream::select_all::SelectAll;
11use tokio_stream::wrappers::UnboundedReceiverStream;
13use std::{
14 path::{Path, PathBuf},
15 sync::Arc,
16 time::Duration,
17};
19use slotmap::SlotMap;
21use anyhow::Error;
23pub use helix_core::diagnostic::Severity;
24pub use helix_core::register::Registers;
25use helix_core::syntax;
26use helix_core::Position;
28use serde::Deserialize;
30#[derive(Debug, Clone, PartialEq, Deserialize)]
31#[serde(rename_all = "kebab-case", default)]
32pub struct Config {
33 /// Padding to keep between the edge of the screen and the cursor when scrolling. Defaults to 5.
34 pub scrolloff: usize,
35 /// Number of lines to scroll at once. Defaults to 3
36 pub scroll_lines: isize,
37 /// Mouse support. Defaults to true.
38 pub mouse: bool,
39 /// Line number mode.
40 pub line_number: LineNumber,
41 /// Middle click paste support. Defaults to true
42 pub middle_click_paste: bool,
45#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
46#[serde(rename_all = "kebab-case")]
47pub enum LineNumber {
48 /// Show absolute line number
49 Absolute,
51 /// Show relative line number to the primary cursor
52 Relative,
55impl Default for Config {
56 fn default() -> Self {
57 Self {
58 scrolloff: 5,
59 scroll_lines: 3,
60 mouse: true,
61 line_number: LineNumber::Absolute,
62 middle_click_paste: true,
63 }
64 }
67#[derive(Debug)]
68pub struct Editor {
69 pub tree: Tree,
70 pub documents: SlotMap<DocumentId, Document>,
71 pub count: Option<std::num::NonZeroUsize>,
72 pub selected_register: RegisterSelection,
73 pub registers: Registers,
74 pub theme: Theme,
75 pub language_servers: helix_lsp::Registry,
77 pub debugger: Option<helix_dap::Client>,
78 pub debugger_events: SelectAll<UnboundedReceiverStream<helix_dap::Payload>>,
80 pub clipboard_provider: Box<dyn ClipboardProvider>,
82 pub syn_loader: Arc<syntax::Loader>,
83 pub theme_loader: Arc<theme::Loader>,
85 pub status_msg: Option<(String, Severity)>,
87 pub config: Config,
90#[derive(Debug, Copy, Clone)]
91pub enum Action {
92 Load,
93 Replace,
94 HorizontalSplit,
95 VerticalSplit,
98impl Editor {
99 pub fn new(
100 mut area: Rect,
101 themes: Arc<theme::Loader>,
102 config_loader: Arc<syntax::Loader>,
103 config: Config,
104 ) -> Self {
105 let language_servers = helix_lsp::Registry::new();
107 // HAXX: offset the render area height by 1 to account for prompt/commandline
108 area.height -= 1;
110 Self {
111 tree: Tree::new(area),
112 documents: SlotMap::with_key(),
113 count: None,
114 selected_register: RegisterSelection::default(),
115 theme: themes.default(),
116 language_servers,
117 debugger: None,
118 debugger_events: SelectAll::new(),
119 syn_loader: config_loader,
120 theme_loader: themes,
121 registers: Registers::default(),
122 clipboard_provider: get_clipboard_provider(),
123 status_msg: None,
124 config,
125 }
126 }
128 pub fn clear_status(&mut self) {
129 self.status_msg = None;
130 }
132 pub fn set_status(&mut self, status: String) {
133 self.status_msg = Some((status, Severity::Info));
134 }
136 pub fn set_error(&mut self, error: String) {
137 self.status_msg = Some((error, Severity::Error));
138 }
140 pub fn set_theme(&mut self, theme: Theme) {
141 let scopes = theme.scopes();
142 for config in self
143 .syn_loader
144 .language_configs_iter()
145 .filter(|cfg| cfg.is_highlight_initialized())
146 {
147 config.reconfigure(scopes);
148 }
150 self.theme = theme;
151 self._refresh();
152 }
154 pub fn set_theme_from_name(&mut self, theme: &str) -> anyhow::Result<()> {
155 use anyhow::Context;
156 let theme = self
157 .theme_loader
158 .load(theme.as_ref())
159 .with_context(|| format!("failed setting theme `{}`", theme))?;
160 self.set_theme(theme);
161 Ok(())
162 }
164 fn _refresh(&mut self) {
165 for (view, _) in self.tree.views_mut() {
166 let doc = &self.documents[view.doc];
167 view.ensure_cursor_in_view(doc, self.config.scrolloff)
168 }
169 }
171 pub fn switch(&mut self, id: DocumentId, action: Action) {
172 use crate::tree::Layout;
173 use helix_core::Selection;
175 if !self.documents.contains_key(id) {
176 log::error!("cannot switch to document that does not exist (anymore)");
177 return;
178 }
180 match action {
181 Action::Replace => {
182 let view = view!(self);
183 let jump = (
184 view.doc,
185 self.documents[view.doc].selection(view.id).clone(),
186 );
188 let view = view_mut!(self);
189 view.jumps.push(jump);
190 view.last_accessed_doc = Some(view.doc);
191 view.doc = id;
192 view.offset = Position::default();
194 let (view, doc) = current!(self);
196 // initialize selection for view
197 doc.selections
198 .entry(view.id)
199 .or_insert_with(|| Selection::point(0));
200 // TODO: reuse align_view
201 let pos = doc
202 .selection(view.id)
203 .primary()
204 .cursor(doc.text().slice(..));
205 let line = doc.text().char_to_line(pos);
206 view.offset.row = line.saturating_sub(view.inner_area().height as usize / 2);
208 return;
209 }
210 Action::Load => {
211 return;
212 }
213 Action::HorizontalSplit => {
214 let view = View::new(id);
215 let view_id = self.tree.split(view, Layout::Horizontal);
216 // initialize selection for view
217 let doc = &mut self.documents[id];
218 doc.selections.insert(view_id, Selection::point(0));
219 }
220 Action::VerticalSplit => {
221 let view = View::new(id);
222 let view_id = self.tree.split(view, Layout::Vertical);
223 // initialize selection for view
224 let doc = &mut self.documents[id];
225 doc.selections.insert(view_id, Selection::point(0));
226 }
227 }
229 self._refresh();
230 }
232 pub fn new_file(&mut self, action: Action) -> DocumentId {
233 let doc = Document::default();
234 let id = self.documents.insert(doc);
235 self.documents[id].id = id;
236 self.switch(id, action);
237 id
238 }
240 pub fn open(&mut self, path: PathBuf, action: Action) -> Result<DocumentId, Error> {
241 let path = crate::document::canonicalize_path(&path)?;
243 let id = self
244 .documents()
245 .find(|doc| doc.path() == Some(&path))
246 .map(|doc| doc.id);
248 let id = if let Some(id) = id {
249 id
250 } else {
251 let mut doc = Document::open(&path, None, Some(&self.theme), Some(&self.syn_loader))?;
253 // try to find a language server based on the language name
254 let language_server = doc
255 .language
256 .as_ref()
257 .and_then(|language| self.language_servers.get(language).ok());
259 if let Some(language_server) = language_server {
260 doc.set_language_server(Some(language_server.clone()));
262 let language_id = doc
263 .language()
264 .and_then(|s| s.split('.').last()) // source.rust
265 .map(ToOwned::to_owned)
266 .unwrap_or_default();
268 tokio::spawn(language_server.text_document_did_open(
269 doc.url().unwrap(),
270 doc.version(),
271 doc.text(),
272 language_id,
273 ));
274 }
276 let id = self.documents.insert(doc);
277 self.documents[id].id = id;
278 id
279 };
281 self.switch(id, action);
282 Ok(id)
283 }
285 pub fn close(&mut self, id: ViewId, close_buffer: bool) {
286 let view = self.tree.get(self.tree.focus);
287 // remove selection
288 self.documents[view.doc].selections.remove(&id);
290 if close_buffer {
291 // get around borrowck issues
292 let language_servers = &mut self.language_servers;
293 let doc = &self.documents[view.doc];
295 let language_server = doc
296 .language
297 .as_ref()
298 .and_then(|language| language_servers.get(language).ok());
299 if let Some(language_server) = language_server {
300 tokio::spawn(language_server.text_document_did_close(doc.identifier()));
301 }
302 self.documents.remove(view.doc);
303 }
305 self.tree.remove(id);
306 self._refresh();
307 }
309 pub fn resize(&mut self, area: Rect) {
310 if self.tree.resize(area) {
311 self._refresh();
312 };
313 }
315 pub fn focus_next(&mut self) {
316 self.tree.focus_next();
317 }
319 pub fn should_close(&self) -> bool {
320 self.tree.is_empty()
321 }
323 pub fn ensure_cursor_in_view(&mut self, id: ViewId) {
324 let view = self.tree.get_mut(id);
325 let doc = &self.documents[view.doc];
326 view.ensure_cursor_in_view(doc, self.config.scrolloff)
327 }
329 pub fn document(&self, id: DocumentId) -> Option<&Document> {
330 self.documents.get(id)
331 }
333 pub fn document_mut(&mut self, id: DocumentId) -> Option<&mut Document> {
334 self.documents.get_mut(id)
335 }
337 pub fn documents(&self) -> impl Iterator<Item = &Document> {
338 self.documents.iter().map(|(_id, doc)| doc)
339 }
341 pub fn documents_mut(&mut self) -> impl Iterator<Item = &mut Document> {
342 self.documents.iter_mut().map(|(_id, doc)| doc)
343 }
345 pub fn document_by_path<P: AsRef<Path>>(&self, path: P) -> Option<&Document> {
346 self.documents()
347 .find(|doc| doc.path().map(|p| p == path.as_ref()).unwrap_or(false))
348 }
350 // pub fn current_document(&self) -> Document {
351 // let id = self.view().doc;
352 // let doc = &mut editor.documents[id];
353 // }
355 pub fn cursor(&self) -> (Option<Position>, CursorKind) {
356 let view = view!(self);
357 let doc = &self.documents[view.doc];
358 let cursor = doc
359 .selection(view.id)
360 .primary()
361 .cursor(doc.text().slice(..));
362 if let Some(mut pos) = view.screen_coords_at_pos(doc, doc.text().slice(..), cursor) {
363 let inner = view.inner_area();
364 pos.col += inner.x as usize;
365 pos.row += inner.y as usize;
366 (Some(pos), CursorKind::Hidden)
367 } else {
368 (None, CursorKind::Hidden)
369 }
370 }
372 /// Closes language servers with timeout. The default timeout is 500 ms, use
373 /// `timeout` parameter to override this.
374 pub async fn close_language_servers(
375 &self,
376 timeout: Option<u64>,
377 ) -> Result<(), tokio::time::error::Elapsed> {
378 tokio::time::timeout(
379 Duration::from_millis(timeout.unwrap_or(500)),
380 future::join_all(
381 self.language_servers
382 .iter_clients()
383 .map(|client| client.force_shutdown()),
384 ),
385 )
386 .await
387 .map(|_| ())
388 }