on master help sign in
Merge remote-tracking branch 'origin/master' into debug
c7759a5Dmitry Sharshakov
Cargo.lock+8−8
@@ -19,9 +19,9 @@ checksum = "28ae2b3dec75a406790005a200b1bd89785afc02517a00ca99ecfe093ee9e6cf"
19
19
20[[package]]
20[[package]]
21name = "arc-swap"
21name = "arc-swap"
22−version = "1.3.0"
22+version = "1.3.1"
23source = "registry+https://github.com/rust-lang/crates.io-index"
23source = "registry+https://github.com/rust-lang/crates.io-index"
24−checksum = "e906254e445520903e7fc9da4f709886c84ae4bc4ddaf0e093188d66df4dc820"
24+checksum = "34a23efe54373080cf871532e2d01076be41c4c896d32ef63af1b2dded924b03"
25
25
26[[package]]
26[[package]]
27name = "autocfg"
27name = "autocfg"
@@ -114,9 +114,9 @@ dependencies = [
114
114
115[[package]]
115[[package]]
116name = "crossterm"
116name = "crossterm"
117−version = "0.20.0"
117+version = "0.21.0"
118source = "registry+https://github.com/rust-lang/crates.io-index"
118source = "registry+https://github.com/rust-lang/crates.io-index"
119−checksum = "c0ebde6a9dd5e331cd6c6f48253254d117642c31653baa475e394657c59c1f7d"
119+checksum = "486d44227f71a1ef39554c0dc47e44b9f4139927c75043312690c3f476d1d788"
120dependencies = [
120dependencies = [
121 "bitflags",
121 "bitflags",
122 "crossterm_winapi",
122 "crossterm_winapi",
@@ -810,18 +810,18 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
810
810
811[[package]]
811[[package]]
812name = "serde"
812name = "serde"
813−version = "1.0.127"
813+version = "1.0.129"
814source = "registry+https://github.com/rust-lang/crates.io-index"
814source = "registry+https://github.com/rust-lang/crates.io-index"
815−checksum = "f03b9878abf6d14e6779d3f24f07b2cfa90352cfec4acc5aab8f1ac7f146fae8"
815+checksum = "d1f72836d2aa753853178eda473a3b9d8e4eefdaf20523b919677e6de489f8f1"
816dependencies = [
816dependencies = [
817 "serde_derive",
817 "serde_derive",
818]
818]
819
819
820[[package]]
820[[package]]
821name = "serde_derive"
821name = "serde_derive"
822−version = "1.0.127"
822+version = "1.0.129"
823source = "registry+https://github.com/rust-lang/crates.io-index"
823source = "registry+https://github.com/rust-lang/crates.io-index"
824−checksum = "a024926d3432516606328597e0f224a51355a493b49fdd67e9209187cbe55ecc"
824+checksum = "e57ae87ad533d9a56427558b516d0adac283614e347abf85b0dc0cbbf0a249f3"
825dependencies = [
825dependencies = [
826 "proc-macro2",
826 "proc-macro2",
827 "quote",
827 "quote",
helix-core/src/lib.rs+1
@@ -11,6 +11,7 @@ pub mod macros;
11pub mod match_brackets;
11pub mod match_brackets;
12pub mod movement;
12pub mod movement;
13pub mod object;
13pub mod object;
14+pub mod path;
14mod position;
15mod position;
15pub mod register;
16pub mod register;
16pub mod search;
17pub mod search;
helix-core/src/path.rsnew file+92
@@ -0,0 +1,92 @@
1+use std::path::{Component, Path, PathBuf};
2+
3+/// Replaces users home directory from `path` with tilde `~` if the directory
4+/// is available, otherwise returns the path unchanged.
5+pub fn fold_home_dir(path: &Path) -> PathBuf {
6+ if let Ok(home) = super::home_dir() {
7+ if path.starts_with(&home) {
8+ // it's ok to unwrap, the path starts with home dir
9+ return PathBuf::from("~").join(path.strip_prefix(&home).unwrap());
10+ }
11+ }
12+
13+ path.to_path_buf()
14+}
15+
16+/// Expands tilde `~` into users home directory if avilable, otherwise returns the path
17+/// unchanged. The tilde will only be expanded when present as the first component of the path
18+/// and only slash follows it.
19+pub fn expand_tilde(path: &Path) -> PathBuf {
20+ let mut components = path.components().peekable();
21+ if let Some(Component::Normal(c)) = components.peek() {
22+ if c == &"~" {
23+ if let Ok(home) = super::home_dir() {
24+ // it's ok to unwrap, the path starts with `~`
25+ return home.join(path.strip_prefix("~").unwrap());
26+ }
27+ }
28+ }
29+
30+ path.to_path_buf()
31+}
32+
33+/// Normalize a path, removing things like `.` and `..`.
34+///
35+/// CAUTION: This does not resolve symlinks (unlike
36+/// [`std::fs::canonicalize`]). This may cause incorrect or surprising
37+/// behavior at times. This should be used carefully. Unfortunately,
38+/// [`std::fs::canonicalize`] can be hard to use correctly, since it can often
39+/// fail, or on Windows returns annoying device paths. This is a problem Cargo
40+/// needs to improve on.
41+/// Copied from cargo: <https://github.com/rust-lang/cargo/blob/070e459c2d8b79c5b2ac5218064e7603329c92ae/crates/cargo-util/src/paths.rs#L81>
42+pub fn get_normalized_path(path: &Path) -> PathBuf {
43+ let path = expand_tilde(path);
44+ let mut components = path.components().peekable();
45+ let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
46+ components.next();
47+ PathBuf::from(c.as_os_str())
48+ } else {
49+ PathBuf::new()
50+ };
51+
52+ for component in components {
53+ match component {
54+ Component::Prefix(..) => unreachable!(),
55+ Component::RootDir => {
56+ ret.push(component.as_os_str());
57+ }
58+ Component::CurDir => {}
59+ Component::ParentDir => {
60+ ret.pop();
61+ }
62+ Component::Normal(c) => {
63+ ret.push(c);
64+ }
65+ }
66+ }
67+ ret
68+}
69+
70+/// Returns the canonical, absolute form of a path with all intermediate components normalized.
71+///
72+/// This function is used instead of `std::fs::canonicalize` because we don't want to verify
73+/// here if the path exists, just normalize it's components.
74+pub fn get_canonicalized_path(path: &Path) -> std::io::Result<PathBuf> {
75+ let path = if path.is_relative() {
76+ std::env::current_dir().map(|current_dir| current_dir.join(path))?
77+ } else {
78+ path.to_path_buf()
79+ };
80+
81+ Ok(get_normalized_path(path.as_path()))
82+}
83+
84+pub fn get_relative_path(path: &Path) -> PathBuf {
85+ let path = if path.is_absolute() {
86+ let cwdir = std::env::current_dir().expect("couldn't determine current directory");
87+ path.strip_prefix(cwdir).unwrap_or(path)
88+ } else {
89+ path
90+ };
91+ fold_home_dir(path)
92+}
helix-lsp/src/transport.rs+14−6
@@ -222,10 +222,13 @@ impl Transport {
222 loop {
222 loop {
223 match Self::recv_server_message(&mut server_stdout, &mut recv_buffer).await {
223 match Self::recv_server_message(&mut server_stdout, &mut recv_buffer).await {
224 Ok(msg) => {
224 Ok(msg) => {
225− transport
226− .process_server_message(&client_tx, msg)
225+ match transport.process_server_message(&client_tx, msg).await {
227− .await
226+ Ok(_) => {}
228− .unwrap();
227+ Err(err) => {
228+ error!("err: <- {:?}", err);
229+ break;
230+ }
231+ };
229 }
232 }
230 Err(err) => {
233 Err(err) => {
231 error!("err: <- {:?}", err);
234 error!("err: <- {:?}", err);
@@ -254,10 +257,15 @@ impl Transport {
254 mut client_rx: UnboundedReceiver<Payload>,
257 mut client_rx: UnboundedReceiver<Payload>,
255 ) {
258 ) {
256 while let Some(msg) = client_rx.recv().await {
259 while let Some(msg) = client_rx.recv().await {
257− transport
260+ match transport
258 .send_payload_to_server(&mut server_stdin, msg)
261 .send_payload_to_server(&mut server_stdin, msg)
259 .await
262 .await
260− .unwrap()
263+ {
264+ Ok(_) => {}
265+ Err(err) => {
266+ error!("err: <- {:?}", err);
267+ }
268+ }
261 }
269 }
262 }
270 }
263}
271}
helix-term/Cargo.toml+1−1
@@ -32,7 +32,7 @@ once_cell = "1.8"
32tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] }
32tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] }
33num_cpus = "1"
33num_cpus = "1"
34tui = { path = "../helix-tui", package = "helix-tui", default-features = false, features = ["crossterm"] }
34tui = { path = "../helix-tui", package = "helix-tui", default-features = false, features = ["crossterm"] }
35−crossterm = { version = "0.20", features = ["event-stream"] }
35+crossterm = { version = "0.21", features = ["event-stream"] }
36signal-hook = "0.3"
36signal-hook = "0.3"
37tokio-stream = "0.1"
37tokio-stream = "0.1"
38futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
38futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
helix-term/src/commands.rs+66−9
@@ -2006,7 +2006,24 @@ mod cmd {
2006 Ok(())
2006 Ok(())
2007 }
2007 }
2008
2008
2009− fn debug_set_logpoint(
2009+ fn vsplit(
2010+ cx: &mut compositor::Context,
2011+ args: &[&str],
2012+ _event: PromptEvent,
2013+ ) -> anyhow::Result<()> {
2014+ let (_, doc) = current!(cx.editor);
2015+ let id = doc.id();
2016+
2017+ if let Some(path) = args.get(0) {
2018+ cx.editor.open(path.into(), Action::VerticalSplit)?;
2019+ } else {
2020+ cx.editor.switch(id, Action::VerticalSplit);
2021+ }
2022+
2023+ Ok(())
2024+ }
2025+
2026+ fn hsplit(
2010 cx: &mut compositor::Context,
2027 cx: &mut compositor::Context,
2011 args: &[&str],
2028 args: &[&str],
2012 _event: PromptEvent,
2029 _event: PromptEvent,
@@ -2051,6 +2068,24 @@ mod cmd {
2051 _ => Some(args.remove(0)),
2068 _ => Some(args.remove(0)),
2052 };
2069 };
2053 dap_start_impl(&mut cx.editor, name, address, Some(args));
2070 dap_start_impl(&mut cx.editor, name, address, Some(args));
2071+
2072+ Ok(())
2073+ }
2074+
2075+ fn debug_set_logpoint(
2076+ cx: &mut compositor::Context,
2077+ args: &[&str],
2078+ _event: PromptEvent,
2079+ ) -> anyhow::Result<()> {
2080+ let (_, doc) = current!(cx.editor);
2081+ let id = doc.id();
2082+
2083+ if let Some(path) = args.get(0) {
2084+ cx.editor.open(path.into(), Action::HorizontalSplit)?;
2085+ } else {
2086+ cx.editor.switch(id, Action::HorizontalSplit);
2087+ }
2088+
2054 Ok(())
2089 Ok(())
2055 }
2090 }
2056
2091
@@ -2327,6 +2362,20 @@ mod cmd {
2327 doc: "Make current breakpoint a log point.",
2362 doc: "Make current breakpoint a log point.",
2328 fun: debug_set_logpoint,
2363 fun: debug_set_logpoint,
2329 completer: None,
2364 completer: None,
2365+ },
2366+ TypableCommand {
2367+ name: "vsplit",
2368+ alias: Some("vsp"),
2369+ doc: "Open the file in a vertical split.",
2370+ fun: vsplit,
2371+ completer: Some(completers::filename),
2372+ },
2373+ TypableCommand {
2374+ name: "hsplit",
2375+ alias: Some("sp"),
2376+ doc: "Open the file in a horizontal split.",
2377+ fun: hsplit,
2378+ completer: Some(completers::filename),
2330 }
2379 }
2331 ];
2380 ];
2332
2381
@@ -2429,16 +2478,16 @@ fn buffer_picker(cx: &mut Context) {
2429 cx.editor
2478 cx.editor
2430 .documents
2479 .documents
2431 .iter()
2480 .iter()
2432− .map(|(id, doc)| (id, doc.relative_path()))
2481+ .map(|(id, doc)| (id, doc.path().cloned()))
2433 .collect(),
2482 .collect(),
2434 move |(id, path): &(DocumentId, Option<PathBuf>)| {
2483 move |(id, path): &(DocumentId, Option<PathBuf>)| {
2435− // format_fn
2484+ let path = path.as_deref().map(helix_core::path::get_relative_path);
2436 match path.as_ref().and_then(|path| path.to_str()) {
2485 match path.as_ref().and_then(|path| path.to_str()) {
2437 Some(path) => {
2486 Some(path) => {
2438 if *id == current {
2487 if *id == current {
2439− format!("{} (*)", path).into()
2488+ format!("{} (*)", &path).into()
2440 } else {
2489 } else {
2441− path.into()
2490+ path.to_owned().into()
2442 }
2491 }
2443 }
2492 }
2444 None => "[scratch buffer]".into(),
2493 None => "[scratch buffer]".into(),
@@ -2454,7 +2503,7 @@ fn buffer_picker(cx: &mut Context) {
2454 .selection(view_id)
2503 .selection(view_id)
2455 .primary()
2504 .primary()
2456 .cursor_line(doc.text().slice(..));
2505 .cursor_line(doc.text().slice(..));
2457− Some((path.clone()?, Some(line)))
2506+ Some((path.clone()?, Some((line, line))))
2458 },
2507 },
2459 );
2508 );
2460 cx.push_layer(Box::new(picker));
2509 cx.push_layer(Box::new(picker));
@@ -2519,13 +2568,18 @@ fn symbol_picker(cx: &mut Context) {
2519 if let Some(range) =
2568 if let Some(range) =
2520 lsp_range_to_range(doc.text(), symbol.location.range, offset_encoding)
2569 lsp_range_to_range(doc.text(), symbol.location.range, offset_encoding)
2521 {
2570 {
2571+ // we flip the range so that the cursor sits on the start of the symbol
2572+ // (for example start of the function).
2522− doc.set_selection(view.id, Selection::single(range.anchor, range.head));
2573+ doc.set_selection(view.id, Selection::single(range.head, range.anchor));
2523 align_view(doc, view, Align::Center);
2574 align_view(doc, view, Align::Center);
2524 }
2575 }
2525 },
2576 },
2526 move |_editor, symbol| {
2577 move |_editor, symbol| {
2527 let path = symbol.location.uri.to_file_path().unwrap();
2578 let path = symbol.location.uri.to_file_path().unwrap();
2579+ let line = Some((
2528− let line = Some(symbol.location.range.start.line as usize);
2580+ symbol.location.range.start.line as usize,
2581+ symbol.location.range.end.line as usize,
2582+ ));
2529 Some((path, line))
2583 Some((path, line))
2530 },
2584 },
2531 );
2585 );
@@ -2958,7 +3012,10 @@ fn goto_impl(
2958 },
3012 },
2959 |_editor, location| {
3013 |_editor, location| {
2960 let path = location.uri.to_file_path().unwrap();
3014 let path = location.uri.to_file_path().unwrap();
3015+ let line = Some((
2961− let line = Some(location.range.start.line as usize);
3016+ location.range.start.line as usize,
3017+ location.range.end.line as usize,
3018+ ));
2962 Some((path, line))
3019 Some((path, line))
2963 },
3020 },
2964 );
3021 );
helix-term/src/ui/editor.rs+12−18
@@ -112,13 +112,11 @@ impl EditorView {
112
112
113 self.render_diagnostics(doc, view, inner, surface, theme, debugger);
113 self.render_diagnostics(doc, view, inner, surface, theme, debugger);
114
114
115− let area = Rect::new(
115+ let statusline_area = view
116− view.area.x,
116+ .area
117− view.area.y + view.area.height.saturating_sub(1),
117+ .clip_top(view.area.height.saturating_sub(1))
118− view.area.width,
118+ .clip_bottom(1); // -1 from bottom to remove commandline
119− 1,
120− );
121− self.render_statusline(doc, view, area, surface, theme, is_focused);
119+ self.render_statusline(doc, view, statusline_area, surface, theme, is_focused);
122 }
120 }
123
121
124 /// Get syntax highlights for a document in a view represented by the first line
122 /// Get syntax highlights for a document in a view represented by the first line
@@ -201,7 +199,9 @@ impl EditorView {
201 .find_scope_index("diagnostic")
199 .find_scope_index("diagnostic")
202 .or_else(|| theme.find_scope_index("ui.cursor"))
200 .or_else(|| theme.find_scope_index("ui.cursor"))
203 .or_else(|| theme.find_scope_index("ui.selection"))
201 .or_else(|| theme.find_scope_index("ui.selection"))
204− .expect("no selection scope found!");
202+ .expect(
203+ "at least one of the following scopes must be defined in the theme: `diagnostic`, `ui.cursor`, or `ui.selection`",
204+ );
205
205
206 doc.diagnostics()
206 doc.diagnostics()
207 .iter()
207 .iter()
@@ -226,7 +226,7 @@ impl EditorView {
226
226
227 let selection_scope = theme
227 let selection_scope = theme
228 .find_scope_index("ui.selection")
228 .find_scope_index("ui.selection")
229− .expect("no selection scope found!");
229+ .expect("could not find `ui.selection` scope in the theme!");
230 let base_cursor_scope = theme
230 let base_cursor_scope = theme
231 .find_scope_index("ui.cursor")
231 .find_scope_index("ui.cursor")
232 .unwrap_or(selection_scope);
232 .unwrap_or(selection_scope);
@@ -598,12 +598,7 @@ impl EditorView {
598 let width = 80.min(viewport.width);
598 let width = 80.min(viewport.width);
599 let height = 15.min(viewport.height);
599 let height = 15.min(viewport.height);
600 paragraph.render(
600 paragraph.render(
601− Rect::new(
602− viewport.right() - width,
601+ Rect::new(viewport.right() - width, viewport.y + 1, width, height),
603− viewport.y as u16 + 1,
604− width,
605− height,
606− ),
607 surface,
602 surface,
608 );
603 );
609 }
604 }
@@ -642,7 +637,7 @@ impl EditorView {
642 theme.get("ui.statusline.inactive")
637 theme.get("ui.statusline.inactive")
643 };
638 };
644 // statusline
639 // statusline
645− surface.set_style(Rect::new(viewport.x, viewport.y, viewport.width, 1), style);
640+ surface.set_style(viewport.with_height(1), style);
646 if is_focused {
641 if is_focused {
647 surface.set_string(viewport.x + 1, viewport.y, mode, style);
642 surface.set_string(viewport.x + 1, viewport.y, mode, style);
648 }
643 }
@@ -1071,8 +1066,7 @@ impl Component for EditorView {
1071 surface.set_style(area, cx.editor.theme.get("ui.background"));
1066 surface.set_style(area, cx.editor.theme.get("ui.background"));
1072
1067
1073 // if the terminal size suddenly changed, we need to trigger a resize
1068 // if the terminal size suddenly changed, we need to trigger a resize
1074− cx.editor
1075− .resize(Rect::new(area.x, area.y, area.width, area.height - 1)); // - 1 to account for commandline
1069+ cx.editor.resize(area.clip_bottom(1)); // -1 from bottom for commandline
1076
1070
1077 for (view, is_focused) in cx.editor.tree.views() {
1071 for (view, is_focused) in cx.editor.tree.views() {
1078 let doc = cx.editor.document(view.doc).unwrap();
1072 let doc = cx.editor.document(view.doc).unwrap();
helix-term/src/ui/info.rs+6−2
@@ -6,8 +6,12 @@ use tui::widgets::{Block, Borders, Paragraph, Widget};
6
6
7impl Component for Info {
7impl Component for Info {
8 fn render(&mut self, viewport: Rect, surface: &mut Surface, cx: &mut Context) {
8 fn render(&mut self, viewport: Rect, surface: &mut Surface, cx: &mut Context) {
9+ let get_theme = |style, fallback| {
9− let text_style = cx.editor.theme.get("ui.text.focus");
10+ let theme = &cx.editor.theme;
11+ theme.try_get(style).unwrap_or_else(|| theme.get(fallback))
12+ };
13+ let text_style = get_theme("ui.info.text", "ui.text");
10− let popup_style = text_style.patch(cx.editor.theme.get("ui.popup"));
14+ let popup_style = text_style.patch(get_theme("ui.info", "ui.popup"));
11
15
12 // Calculate the area of the terminal to modify. Because we want to
16 // Calculate the area of the terminal to modify. Because we want to
13 // render at the bottom right, we use the viewport's width and height
17 // render at the bottom right, we use the viewport's width and height
helix-term/src/ui/markdown.rs+6−3
@@ -13,7 +13,7 @@ use helix_core::{
13 Rope,
13 Rope,
14};
14};
15use helix_view::{
15use helix_view::{
16− graphics::{Color, Rect, Style},
16+ graphics::{Color, Margin, Rect, Style},
17 Theme,
17 Theme,
18};
18};
19
19
@@ -207,8 +207,11 @@ impl Component for Markdown {
207 .wrap(Wrap { trim: false })
207 .wrap(Wrap { trim: false })
208 .scroll((cx.scroll.unwrap_or_default() as u16, 0));
208 .scroll((cx.scroll.unwrap_or_default() as u16, 0));
209
209
210− let area = Rect::new(area.x + 1, area.y + 1, area.width - 2, area.height - 2);
210+ let margin = Margin {
211+ vertical: 1,
212+ horizontal: 1,
213+ };
211− par.render(area, surface);
214+ par.render(area.inner(&margin), surface);
212 }
215 }
213
216
214 fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
217 fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
helix-term/src/ui/menu.rs−8
@@ -304,14 +304,6 @@ impl<T: Item + 'static> Component for Menu<T> {
304 },
304 },
305 );
305 );
306
306
307− // // TODO: set bg for the whole row if selected
308− // if line == self.cursor {
309− // surface.set_style(
310− // Rect::new(area.x, area.y + i as u16, area.width - 1, 1),
311− // selected,
312− // )
313− // }
314−
315 for (i, _) in (scroll..(scroll + win_height).min(len)).enumerate() {
307 for (i, _) in (scroll..(scroll + win_height).min(len)).enumerate() {
316 let is_marked = i >= scroll_line && i < scroll_line + scroll_height;
308 let is_marked = i >= scroll_line && i < scroll_line + scroll_height;
317
309
helix-term/src/ui/mod.rs+1−1
@@ -208,7 +208,7 @@ pub mod completers {
208 use std::path::Path;
208 use std::path::Path;
209
209
210 let is_tilde = input.starts_with('~') && input.len() == 1;
210 let is_tilde = input.starts_with('~') && input.len() == 1;
211− let path = helix_view::document::expand_tilde(Path::new(input));
211+ let path = helix_core::path::expand_tilde(Path::new(input));
212
212
213 let (dir, file_name) = if input.ends_with('/') {
213 let (dir, file_name) = if input.ends_with('/') {
214 (path, None)
214 (path, None)
helix-term/src/ui/picker.rs+45−31
@@ -17,16 +17,15 @@ use std::{borrow::Cow, collections::HashMap, path::PathBuf};
17use crate::ui::{Prompt, PromptEvent};
17use crate::ui::{Prompt, PromptEvent};
18use helix_core::Position;
18use helix_core::Position;
19use helix_view::{
19use helix_view::{
20− document::canonicalize_path,
21 editor::Action,
20 editor::Action,
22− graphics::{Color, CursorKind, Rect, Style},
21+ graphics::{Color, CursorKind, Margin, Rect, Style},
23 Document, Editor,
22 Document, Editor,
24};
23};
25
24
26pub const MIN_SCREEN_WIDTH_FOR_PREVIEW: u16 = 80;
25pub const MIN_SCREEN_WIDTH_FOR_PREVIEW: u16 = 80;
27
26
28/// File path and line number (used to align and highlight a line)
27/// File path and line number (used to align and highlight a line)
29−type FileLocation = (PathBuf, Option<usize>);
28+type FileLocation = (PathBuf, Option<(usize, usize)>);
30
29
31pub struct FilePicker<T> {
30pub struct FilePicker<T> {
32 picker: Picker<T>,
31 picker: Picker<T>,
@@ -54,7 +53,11 @@ impl<T> FilePicker<T> {
54 self.picker
53 self.picker
55 .selection()
54 .selection()
56 .and_then(|current| (self.file_fn)(editor, current))
55 .and_then(|current| (self.file_fn)(editor, current))
57− .and_then(|(path, line)| canonicalize_path(&path).ok().zip(Some(line)))
56+ .and_then(|(path, line)| {
57+ helix_core::path::get_canonicalized_path(&path)
58+ .ok()
59+ .zip(Some(line))
60+ })
58 }
61 }
59
62
60 fn calculate_preview(&mut self, editor: &Editor) {
63 fn calculate_preview(&mut self, editor: &Editor) {
@@ -90,34 +93,40 @@ impl<T: 'static> Component for FilePicker<T> {
90 area.width
93 area.width
91 };
94 };
92
95
93− let picker_area = Rect::new(area.x, area.y, picker_width, area.height);
96+ let picker_area = area.with_width(picker_width);
94 self.picker.render(picker_area, surface, cx);
97 self.picker.render(picker_area, surface, cx);
95
98
96 if !render_preview {
99 if !render_preview {
97 return;
100 return;
98 }
101 }
99
102
100− let preview_area = Rect::new(area.x + picker_width, area.y, area.width / 2, area.height);
103+ let preview_area = area.clip_left(picker_width);
101
104
102 // don't like this but the lifetime sucks
105 // don't like this but the lifetime sucks
103 let block = Block::default().borders(Borders::ALL);
106 let block = Block::default().borders(Borders::ALL);
104
107
105 // calculate the inner area inside the box
108 // calculate the inner area inside the box
106− let mut inner = block.inner(preview_area);
109+ let inner = block.inner(preview_area);
107 // 1 column gap on either side
110 // 1 column gap on either side
108− inner.x += 1;
111+ let margin = Margin {
109− inner.width = inner.width.saturating_sub(2);
112+ vertical: 0,
113+ horizontal: 1,
114+ };
115+ let inner = inner.inner(&margin);
110
116
111 block.render(preview_area, surface);
117 block.render(preview_area, surface);
112
118
113− if let Some((doc, line)) = self.current_file(cx.editor).and_then(|(path, line)| {
119+ if let Some((doc, line)) = self.current_file(cx.editor).and_then(|(path, range)| {
114 cx.editor
120 cx.editor
115 .document_by_path(&path)
121 .document_by_path(&path)
116 .or_else(|| self.preview_cache.get(&path))
122 .or_else(|| self.preview_cache.get(&path))
117− .zip(Some(line))
123+ .zip(Some(range))
118 }) {
124 }) {
119 // align to middle
125 // align to middle
126+ let first_line = line
127+ .map(|(start, _)| start)
128+ .unwrap_or(0)
120− let first_line = line.unwrap_or(0).saturating_sub(inner.height as usize / 2);
129+ .saturating_sub(inner.height as usize / 2);
121 let offset = Position::new(first_line, 0);
130 let offset = Position::new(first_line, 0);
122
131
123 let highlights = EditorView::doc_syntax_highlights(
132 let highlights = EditorView::doc_syntax_highlights(
@@ -137,12 +146,21 @@ impl<T: 'static> Component for FilePicker<T> {
137 );
146 );
138
147
139 // highlight the line
148 // highlight the line
140− if let Some(line) = line {
149+ if let Some((start, end)) = line {
141− for x in inner.left()..inner.right() {
150+ let offset = start.saturating_sub(first_line) as u16;
142− surface
151+ surface.set_style(
152+ Rect::new(
153+ inner.x,
154+ inner.y + offset,
155+ inner.width,
143− .get_mut(x, inner.y + line.saturating_sub(first_line) as u16)
156+ (end.saturating_sub(start) as u16 + 1)
157+ .min(inner.height.saturating_sub(offset)),
158+ ),
159+ cx.editor
160+ .theme
161+ .try_get("ui.highlight")
144− .set_style(cx.editor.theme.get("ui.selection"));
162+ .unwrap_or_else(|| cx.editor.theme.get("ui.selection")),
145− }
163+ );
146 }
164 }
147 }
165 }
148 }
166 }
@@ -282,15 +300,11 @@ impl<T> Picker<T> {
282// - score all the names in relation to input
300// - score all the names in relation to input
283
301
284fn inner_rect(area: Rect) -> Rect {
302fn inner_rect(area: Rect) -> Rect {
303+ let margin = Margin {
285− let padding_vertical = area.height * 10 / 100;
304+ vertical: area.height * 10 / 100,
286− let padding_horizontal = area.width * 10 / 100;
305+ horizontal: area.width * 10 / 100,
287−
306+ };
288− Rect::new(
307+ area.inner(&margin)
289− area.x + padding_horizontal,
290− area.y + padding_vertical,
291− area.width - padding_horizontal * 2,
292− area.height - padding_vertical * 2,
293− )
294}
308}
295
309
296impl<T: 'static> Component for Picker<T> {
310impl<T: 'static> Component for Picker<T> {
@@ -410,7 +424,7 @@ impl<T: 'static> Component for Picker<T> {
410
424
411 // -- Render the input bar:
425 // -- Render the input bar:
412
426
413− let area = Rect::new(inner.x + 1, inner.y, inner.width - 1, 1);
427+ let area = inner.clip_left(1).with_height(1);
414
428
415 let count = format!("{}/{}", self.matches.len(), self.options.len());
429 let count = format!("{}/{}", self.matches.len(), self.options.len());
416 surface.set_stringn(
430 surface.set_stringn(
@@ -434,8 +448,8 @@ impl<T: 'static> Component for Picker<T> {
434 }
448 }
435
449
436 // -- Render the contents:
450 // -- Render the contents:
437− // subtract the area of the prompt (-2) and current item marker " > " (-3)
451+ // subtract area of prompt from top and current item marker " > " from left
438− let inner = Rect::new(inner.x + 3, inner.y + 2, inner.width - 3, inner.height - 2);
452+ let inner = inner.clip_top(2).clip_left(3);
439
453
440 let selected = cx.editor.theme.get("ui.text.focus");
454 let selected = cx.editor.theme.get("ui.text.focus");
441
455
@@ -474,7 +488,7 @@ impl<T: 'static> Component for Picker<T> {
474 let inner = block.inner(area);
488 let inner = block.inner(area);
475
489
476 // prompt area
490 // prompt area
477− let area = Rect::new(inner.x + 1, inner.y, inner.width - 1, 1);
491+ let area = inner.clip_left(1).with_height(1);
478
492
479 self.prompt.cursor(area, editor)
493 self.prompt.cursor(area, editor)
480 }
494 }
helix-tui/Cargo.toml+1−1
@@ -19,7 +19,7 @@ default = ["crossterm"]
19bitflags = "1.3"
19bitflags = "1.3"
20cassowary = "0.3"
20cassowary = "0.3"
21unicode-segmentation = "1.8"
21unicode-segmentation = "1.8"
22−crossterm = { version = "0.20", optional = true }
22+crossterm = { version = "0.21", optional = true }
23serde = { version = "1", "optional" = true, features = ["derive"]}
23serde = { version = "1", "optional" = true, features = ["derive"]}
24helix-view = { version = "0.4", path = "../helix-view", features = ["term"] }
24helix-view = { version = "0.4", path = "../helix-view", features = ["term"] }
25helix-core = { version = "0.4", path = "../helix-core" }
25helix-core = { version = "0.4", path = "../helix-core" }
helix-view/Cargo.toml+1−1
@@ -19,7 +19,7 @@ anyhow = "1"
19helix-core = { version = "0.4", path = "../helix-core" }
19helix-core = { version = "0.4", path = "../helix-core" }
20helix-lsp = { version = "0.4", path = "../helix-lsp"}
20helix-lsp = { version = "0.4", path = "../helix-lsp"}
21helix-dap = { version = "0.4", path = "../helix-dap"}
21helix-dap = { version = "0.4", path = "../helix-dap"}
22−crossterm = { version = "0.20", optional = true }
22+crossterm = { version = "0.21", optional = true }
23
23
24# Conversion traits
24# Conversion traits
25once_cell = "1.8"
25once_cell = "1.8"
helix-view/src/clipboard.rs+1−1
@@ -84,7 +84,7 @@ pub fn get_clipboard_provider() -> Box<dyn ClipboardProvider> {
84 // FIXME: check performance of is_exit_success
84 // FIXME: check performance of is_exit_success
85 command_provider! {
85 command_provider! {
86 paste => "xsel", "-o", "-b";
86 paste => "xsel", "-o", "-b";
87− copy => "xsel", "--nodetach", "-i", "-b";
87+ copy => "xsel", "-i", "-b";
88 primary_paste => "xsel", "-o";
88 primary_paste => "xsel", "-o";
89 primary_copy => "xsel", "-i";
89 primary_copy => "xsel", "-i";
90 }
90 }
helix-view/src/document.rs+5−92
@@ -4,7 +4,7 @@ use std::cell::Cell;
4use std::collections::HashMap;
4use std::collections::HashMap;
5use std::fmt::Display;
5use std::fmt::Display;
6use std::future::Future;
6use std::future::Future;
7−use std::path::{Component, Path, PathBuf};
7+use std::path::{Path, PathBuf};
8use std::str::FromStr;
8use std::str::FromStr;
9use std::sync::Arc;
9use std::sync::Arc;
10
10
@@ -317,87 +317,6 @@ where
317 }
317 }
318}
318}
319
319
320−/// Expands tilde `~` into users home directory if avilable, otherwise returns the path
321−/// unchanged. The tilde will only be expanded when present as the first component of the path
322−/// and only slash follows it.
323−pub fn expand_tilde(path: &Path) -> PathBuf {
324− let mut components = path.components().peekable();
325− if let Some(Component::Normal(c)) = components.peek() {
326− if c == &"~" {
327− if let Ok(home) = helix_core::home_dir() {
328− // it's ok to unwrap, the path starts with `~`
329− return home.join(path.strip_prefix("~").unwrap());
330− }
331− }
332− }
333−
334− path.to_path_buf()
335−}
336−
337−/// Replaces users home directory from `path` with tilde `~` if the directory
338−/// is available, otherwise returns the path unchanged.
339−pub fn fold_home_dir(path: &Path) -> PathBuf {
340− if let Ok(home) = helix_core::home_dir() {
341− if path.starts_with(&home) {
342− // it's ok to unwrap, the path starts with home dir
343− return PathBuf::from("~").join(path.strip_prefix(&home).unwrap());
344− }
345− }
346−
347− path.to_path_buf()
348−}
349−
350−/// Normalize a path, removing things like `.` and `..`.
351−///
352−/// CAUTION: This does not resolve symlinks (unlike
353−/// [`std::fs::canonicalize`]). This may cause incorrect or surprising
354−/// behavior at times. This should be used carefully. Unfortunately,
355−/// [`std::fs::canonicalize`] can be hard to use correctly, since it can often
356−/// fail, or on Windows returns annoying device paths. This is a problem Cargo
357−/// needs to improve on.
358−/// Copied from cargo: <https://github.com/rust-lang/cargo/blob/070e459c2d8b79c5b2ac5218064e7603329c92ae/crates/cargo-util/src/paths.rs#L81>
359−pub fn normalize_path(path: &Path) -> PathBuf {
360− let path = expand_tilde(path);
361− let mut components = path.components().peekable();
362− let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
363− components.next();
364− PathBuf::from(c.as_os_str())
365− } else {
366− PathBuf::new()
367− };
368−
369− for component in components {
370− match component {
371− Component::Prefix(..) => unreachable!(),
372− Component::RootDir => {
373− ret.push(component.as_os_str());
374− }
375− Component::CurDir => {}
376− Component::ParentDir => {
377− ret.pop();
378− }
379− Component::Normal(c) => {
380− ret.push(c);
381− }
382− }
383− }
384− ret
385−}
386−
387−/// Returns the canonical, absolute form of a path with all intermediate components normalized.
388−///
389−/// This function is used instead of `std::fs::canonicalize` because we don't want to verify
390−/// here if the path exists, just normalize it's components.
391−pub fn canonicalize_path(path: &Path) -> std::io::Result<PathBuf> {
392− let path = if path.is_relative() {
393− std::env::current_dir().map(|current_dir| current_dir.join(path))?
394− } else {
395− path.to_path_buf()
396− };
397−
398− Ok(normalize_path(&path))
399−}
400−
401use helix_lsp::lsp;
320use helix_lsp::lsp;
402use url::Url;
321use url::Url;
403
322
@@ -631,7 +550,7 @@ impl Document {
631 }
550 }
632
551
633 pub fn set_path(&mut self, path: &Path) -> Result<(), std::io::Error> {
552 pub fn set_path(&mut self, path: &Path) -> Result<(), std::io::Error> {
634− let path = canonicalize_path(path)?;
553+ let path = helix_core::path::get_canonicalized_path(path)?;
635
554
636 // if parent doesn't exist we still want to open the document
555 // if parent doesn't exist we still want to open the document
637 // and error out when document is saved
556 // and error out when document is saved
@@ -936,15 +855,9 @@ impl Document {
936 }
855 }
937
856
938 pub fn relative_path(&self) -> Option<PathBuf> {
857 pub fn relative_path(&self) -> Option<PathBuf> {
939− let cwdir = std::env::current_dir().expect("couldn't determine current directory");
940−
941− self.path.as_ref().map(|path| {
858+ self.path
942− let mut path = path.as_path();
859+ .as_deref()
943− if path.is_absolute() {
860+ .map(helix_core::path::get_relative_path)
944− path = path.strip_prefix(cwdir).unwrap_or(path)
945− };
946− fold_home_dir(path)
947− })
948 }
861 }
949
862
950 // pub fn slice<R>(&self, range: R) -> RopeSlice where R: RangeBounds {
863 // pub fn slice<R>(&self, range: R) -> RopeSlice where R: RangeBounds {
helix-view/src/editor.rs+1−1
@@ -238,7 +238,7 @@ impl Editor {
238 }
238 }
239
239
240 pub fn open(&mut self, path: PathBuf, action: Action) -> Result<DocumentId, Error> {
240 pub fn open(&mut self, path: PathBuf, action: Action) -> Result<DocumentId, Error> {
241− let path = crate::document::canonicalize_path(&path)?;
241+ let path = helix_core::path::get_canonicalized_path(&path)?;
242
242
243 let id = self
243 let id = self
244 .documents()
244 .documents()
helix-view/src/graphics.rs+86−1
@@ -24,7 +24,7 @@ pub struct Margin {
24}
24}
25
25
26/// A simple rectangle used in the computation of the layout and to give widgets an hint about the
26/// A simple rectangle used in the computation of the layout and to give widgets an hint about the
27−/// area they are supposed to render to.
27+/// area they are supposed to render to. (x, y) = (0, 0) is at the top left corner of the screen.
28#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
28#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
29pub struct Rect {
29pub struct Rect {
30 pub x: u16,
30 pub x: u16,
@@ -92,6 +92,57 @@ impl Rect {
92 self.y.saturating_add(self.height)
92 self.y.saturating_add(self.height)
93 }
93 }
94
94
95+ // Returns a new Rect with width reduced from the left side.
96+ // This changes the `x` coordinate and clamps it to the right
97+ // edge of the original Rect.
98+ pub fn clip_left(self, width: u16) -> Rect {
99+ let width = std::cmp::min(width, self.width);
100+ Rect {
101+ x: self.x.saturating_add(width),
102+ width: self.width.saturating_sub(width),
103+ ..self
104+ }
105+ }
106+
107+ // Returns a new Rect with width reduced from the right side.
108+ // This does _not_ change the `x` coordinate.
109+ pub fn clip_right(self, width: u16) -> Rect {
110+ Rect {
111+ width: self.width.saturating_sub(width),
112+ ..self
113+ }
114+ }
115+
116+ // Returns a new Rect with height reduced from the top.
117+ // This changes the `y` coordinate and clamps it to the bottom
118+ // edge of the original Rect.
119+ pub fn clip_top(self, height: u16) -> Rect {
120+ let height = std::cmp::min(height, self.height);
121+ Rect {
122+ y: self.y.saturating_add(height),
123+ height: self.height.saturating_sub(height),
124+ ..self
125+ }
126+ }
127+
128+ // Returns a new Rect with height reduced from the bottom.
129+ // This does _not_ change the `y` coordinate.
130+ pub fn clip_bottom(self, height: u16) -> Rect {
131+ Rect {
132+ height: self.height.saturating_sub(height),
133+ ..self
134+ }
135+ }
136+
137+ pub fn with_height(self, height: u16) -> Rect {
138+ // new height may make area > u16::max_value, so use new()
139+ Self::new(self.x, self.y, self.width, height)
140+ }
141+
142+ pub fn with_width(self, width: u16) -> Rect {
143+ Self::new(self.x, self.y, width, self.height)
144+ }
145+
95 pub fn inner(self, margin: &Margin) -> Rect {
146 pub fn inner(self, margin: &Margin) -> Rect {
96 if self.width < 2 * margin.horizontal || self.height < 2 * margin.vertical {
147 if self.width < 2 * margin.horizontal || self.height < 2 * margin.vertical {
97 Rect::default()
148 Rect::default()
@@ -495,6 +546,40 @@ mod tests {
495 assert_eq!(rect.height, 100);
546 assert_eq!(rect.height, 100);
496 }
547 }
497
548
549+ #[test]
550+ fn test_rect_chop_from_left() {
551+ let rect = Rect::new(0, 0, 20, 30);
552+ assert_eq!(Rect::new(10, 0, 10, 30), rect.clip_left(10));
553+ assert_eq!(
554+ Rect::new(20, 0, 0, 30),
555+ rect.clip_left(40),
556+ "x should be clamped to original width if new width is bigger"
557+ );
558+ }
559+
560+ #[test]
561+ fn test_rect_chop_from_right() {
562+ let rect = Rect::new(0, 0, 20, 30);
563+ assert_eq!(Rect::new(0, 0, 10, 30), rect.clip_right(10));
564+ }
565+
566+ #[test]
567+ fn test_rect_chop_from_top() {
568+ let rect = Rect::new(0, 0, 20, 30);
569+ assert_eq!(Rect::new(0, 10, 20, 20), rect.clip_top(10));
570+ assert_eq!(
571+ Rect::new(0, 30, 20, 0),
572+ rect.clip_top(50),
573+ "y should be clamped to original height if new height is bigger"
574+ );
575+ }
576+
577+ #[test]
578+ fn test_rect_chop_from_bottom() {
579+ let rect = Rect::new(0, 0, 20, 30);
580+ assert_eq!(Rect::new(0, 0, 20, 20), rect.clip_bottom(10));
581+ }
582+
498 fn styles() -> Vec<Style> {
583 fn styles() -> Vec<Style> {
499 vec![
584 vec![
500 Style::default(),
585 Style::default(),
helix-view/src/view.rs+1−6
@@ -83,12 +83,7 @@ impl View {
83 pub fn inner_area(&self) -> Rect {
83 pub fn inner_area(&self) -> Rect {
84 // TODO: not ideal
84 // TODO: not ideal
85 const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter
85 const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter
86− Rect::new(
87− self.area.x + OFFSET,
86+ self.area.clip_left(OFFSET).clip_bottom(1) // -1 for statusline
88− self.area.y,
89− self.area.width - OFFSET,
90− self.area.height.saturating_sub(1), // -1 for statusline
91− )
92 }
87 }
93
88
94 pub fn ensure_cursor_in_view(&mut self, doc: &Document, scrolloff: usize) {
89 pub fn ensure_cursor_in_view(&mut self, doc: &Document, scrolloff: usize) {
runtime/queries/protobuf/highlights.scm+1−1
@@ -14,6 +14,7 @@
14 "to"
14 "to"
15 "stream"
15 "stream"
16 "extend"
16 "extend"
17+ "optional"
17] @keyword
18] @keyword
18
19
19[
20[
@@ -23,7 +24,6 @@
23
24
24[
25[
25 (mapName)
26 (mapName)
26− (oneofName)
27 (enumName)
27 (enumName)
28 (messageName)
28 (messageName)
29 (extendName)
29 (extendName)
runtime/themes/monokai.tomlnew file+83
@@ -0,0 +1,83 @@
1+# Author: Shafkath Shuhan <shafkathshuhannyc@gmail.com>
2+
3+"namespace" = { fg = "type" }
4+"module" = { fg = "type" }
5+
6+"type" = { fg = "type" }
7+"type.builtin" = { fg = "#66D9EF" }
8+"type.enum.variant" = { fg = "text" }
9+"constructor" = { fg = "text" }
10+"property" = { fg = "variable" }
11+
12+"keyword" = { fg = "keyword" }
13+"keyword.directive" = { fg = "keyword" }
14+"keyword.control" = { fg = "keyword" }
15+"label" = { fg = "keyword" }
16+
17+"special" = { fg = "keyword" }
18+"operator" = { fg = "text" }
19+
20+"punctuation" = { fg = "text" }
21+"punctuation.delimiter" = { fg = "text" }
22+
23+"variable" = { fg = "variable" }
24+"variable.parameter" = { fg = "#fd971f" }
25+"variable.builtin" = { fg = "keyword" }
26+"constant" = { fg = "variable" }
27+"constant.builtin" = { fg = "#ae81ff" }
28+
29+"function" = { fg = "fn_declaration" }
30+"function.builtin" = { fg = "fn_declaration" }
31+"function.macro" = { fg = "keyword" }
32+"attribute" = { fg = "fn_declaration" }
33+
34+"comment" = { fg = "#88846F" }
35+
36+"string" = { fg = "#e6db74" }
37+"number" = { fg = "#ae81ff" }
38+"escape" = { fg = "#ae81ff" }
39+
40+"ui.background" = { fg = "text", bg = "background" }
41+
42+"ui.window" = { bg = "widget" }
43+"ui.popup" = { bg = "widget" }
44+"ui.help" = { bg = "widget" }
45+"ui.menu.selected" = { bg = "widget" }
46+
47+"ui.cursor" = { fg = "cursor", modifiers = ["reversed"] }
48+"ui.cursor.primary" = { fg = "cursor", modifiers = ["reversed"] }
49+"ui.cursor.match" = { fg = "#888888", modifiers = ["reversed"] }
50+
51+"ui.selection" = { bg = "#878b91" }
52+"ui.selection.primary" = { bg = "#575b61" }
53+
54+"ui.linenr" = { fg = "#90908a" }
55+"ui.linenr.selected" = { fg = "#c2c2bf" }
56+
57+"ui.statusline" = { fg = "active_text", bg = "#75715e" }
58+"ui.statusline.inactive" = { fg = "active_text", bg = "#75715e" }
59+
60+"ui.text" = { fg = "text", bg = "background" }
61+"ui.text.focus" = { fg = "active_text" }
62+
63+"warning" = { fg = "#cca700" }
64+"error" = { fg = "#f48771" }
65+"info" = { fg = "#75beff" }
66+"hint" = { fg = "#eeeeeeb3" }
67+
68+diagnostic = { modifiers = ["underlined"] }
69+
70+[palette]
71+type = "#A6E22E"
72+keyword = "#F92672"
73+regex = "#CE9178"
74+special = "#C586C0"
75+variable = "#F8F8F2"
76+fn_declaration = "#A6E22E"
77+
78+background = "#272822"
79+text = "#f8f8f2"
80+active_text = "#ffffff"
81+cursor = "#a6a6a6"
82+inactive_cursor = "#878b91"
83+widget = "#1e1f1c"
theme.toml+1
@@ -51,6 +51,7 @@ module = "#ff0000"
51"ui.cursor.insert" = { bg = "white" }
51"ui.cursor.insert" = { bg = "white" }
52"ui.cursor.match" = { fg = "#212121", bg = "#6C6999" }
52"ui.cursor.match" = { fg = "#212121", bg = "#6C6999" }
53"ui.cursor" = { modifiers = ["reversed"] }
53"ui.cursor" = { modifiers = ["reversed"] }
54+"ui.highlight" = { bg = "bossanova" }
54
55
55"ui.menu.selected" = { fg = "revolver", bg = "white" }
56"ui.menu.selected" = { fg = "revolver", bg = "white" }
56
57