Compare commits

...

34 Commits

Author SHA1 Message Date
Blaž Hrastnik
86b1d970c2 fix build 2022-05-01 11:41:17 +09:00
Blaž Hrastnik
ea6aa0795b Use font data provided by parley 2022-05-01 11:41:17 +09:00
Blaž Hrastnik
e3e6d672bc nix: update flake 2022-05-01 11:41:16 +09:00
Blaž Hrastnik
46072791c1 nix: Use vulkan instead of GLES 2022-05-01 11:41:16 +09:00
Blaž Hrastnik
89a0be10e8 Working parley layout 2022-05-01 11:41:16 +09:00
Blaž Hrastnik
e60e9099ca MSAA 2022-05-01 11:41:16 +09:00
Blaž Hrastnik
fa4dfba043 checkpoint 2022-05-01 11:41:16 +09:00
Blaž Hrastnik
8dc9ad7997 wip 2022-05-01 11:41:16 +09:00
Blaž Hrastnik
9438430e98 wip 2022-05-01 11:41:16 +09:00
Blaž Hrastnik
3b2b9e102d clippy lint 2022-05-01 11:41:15 +09:00
Blaž Hrastnik
d151567192 it compiles! 2022-05-01 11:41:15 +09:00
Blaž Hrastnik
afcb78a538 LSP feature gates 2022-05-01 11:41:15 +09:00
Blaž Hrastnik
842a5fc979 Closer to compiling 2022-05-01 11:41:15 +09:00
Blaž Hrastnik
842cd2cc13 Geate more code to get it all building 2022-05-01 11:41:15 +09:00
Blaž Hrastnik
febc7ee0fa placeholder completion fn if no LSP 2022-05-01 11:41:15 +09:00
Blaž Hrastnik
eadb2eaad1 Split out render & cursor methods into a tui renderer 2022-05-01 11:41:15 +09:00
Blaž Hrastnik
1aa2b027d7 Move ui, keymap & commands to helix-view 2022-05-01 11:41:14 +09:00
Blaž Hrastnik
11b8f068da Extract compositor & jobs into helix-view 2022-05-01 11:41:14 +09:00
Blaž Hrastnik
18381fcbc8 Fix code after rebase 2022-05-01 11:41:14 +09:00
Blaž Hrastnik
9dd9515a8d Term backend 2022-05-01 11:41:14 +09:00
Blaž Hrastnik
61365dfbf3 Add a custom event type that's shared across backends 2022-05-01 11:41:14 +09:00
Blaž Hrastnik
e0f9d86f49 Move terminal out of compositor 2022-05-01 11:41:14 +09:00
Blaž Hrastnik
57d4a9ba21 This term specific behavior really doesn't belong to compositor 2022-05-01 11:41:14 +09:00
Blaž Hrastnik
14f987807d Drop terminal interaction in compositor.size() 2022-05-01 11:41:14 +09:00
Blaž Hrastnik
c3f9d3641c fix helix-term build 2022-05-01 11:41:13 +09:00
Blaž Hrastnik
6f8bf67fa6 helix-view now builds on WASM 2022-05-01 11:41:13 +09:00
Blaž Hrastnik
756b001030 helix-term: Start feature gating lsp 2022-05-01 11:41:13 +09:00
Blaž Hrastnik
8694d60ab3 Simplify LSP formatting, feature gate lsp in helix-view 2022-05-01 11:41:13 +09:00
Blaž Hrastnik
dcd1e9eaa3 Successfully feature gate DAP 2022-05-01 11:41:13 +09:00
Blaž Hrastnik
d7b1c40452 Pass surface as part of RenderCtx 2022-05-01 11:41:13 +09:00
Blaž Hrastnik
3c0e11d69e Implement a separate RenderContext 2022-05-01 11:41:13 +09:00
Blaž Hrastnik
7622643117 wasm 2022-05-01 11:41:12 +09:00
Blaž Hrastnik
649a17720a WIP: try compiling helix-core for WASM 2022-05-01 11:41:12 +09:00
Blaž Hrastnik
cb5b12725e wip add helix-ui 2022-05-01 11:41:12 +09:00
76 changed files with 4460 additions and 921 deletions

1540
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,6 +4,8 @@ members = [
"helix-view", "helix-view",
"helix-term", "helix-term",
"helix-tui", "helix-tui",
"helix-graphics",
"helix-ui",
"helix-lsp", "helix-lsp",
"helix-dap", "helix-dap",
"helix-loader", "helix-loader",
@@ -11,9 +13,12 @@ members = [
] ]
default-members = [ default-members = [
"helix-term" "helix-term",
"helix-ui"
] ]
resolver = "2"
[profile.dev] [profile.dev]
split-debuginfo = "unpacked" split-debuginfo = "unpacked"

31
flake.lock generated
View File

@@ -25,11 +25,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1646667754, "lastModified": 1650201426,
"narHash": "sha256-LahZHvCC3UVzGQ55iWDRZkuDssXl1rYgqgScrPV9S38=", "narHash": "sha256-u43Xf03ImFJWKLHddtjOpCJCHbuM0SQbb6FKR5NuFhk=",
"owner": "numtide", "owner": "numtide",
"repo": "devshell", "repo": "devshell",
"rev": "59fbe1dfc0de8c3332957c16998a7d16dff365d8", "rev": "cb76bc75a0ee81f2d8676fe681f268c48dbd380e",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -75,16 +75,15 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1646710334, "lastModified": 1650199368,
"narHash": "sha256-eLBcDgcbOUfeH4k6SEW5a5v0PTp2KNCn+5ZXIoWGYww=", "narHash": "sha256-35wZQDyohH6NI8ifT124p+TPDiwfRnQZkbUXc1yjGIE=",
"owner": "nix-community", "owner": "nix-community",
"repo": "dream2nix", "repo": "dream2nix",
"rev": "5dcfbfd3b60ce0208b894c1bdea00e2bdf80ca6a", "rev": "feb7eabd0c1b24d47ea6ecd38b737319e75fd513",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "nix-community", "owner": "nix-community",
"ref": "main",
"repo": "dream2nix", "repo": "dream2nix",
"type": "github" "type": "github"
} }
@@ -131,11 +130,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1646766572, "lastModified": 1650262356,
"narHash": "sha256-DV3+zxvAIKsMHsHedJKYFsracvFyLKpFQqurUBR86oY=", "narHash": "sha256-1ko5YhiLeFt9SjKvzq31IGrBJkHITNJc+08rod4P6lk=",
"owner": "yusdacra", "owner": "yusdacra",
"repo": "nix-cargo-integration", "repo": "nix-cargo-integration",
"rev": "3a3f47f43ba486b7554164a698c8dfc5a38624ce", "rev": "924aa1e39bbf1182cdf0dd6dfdb1d7b4a047ef6f",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -146,11 +145,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1646497237, "lastModified": 1650161686,
"narHash": "sha256-Ccpot1h/rV8MgcngDp5OrdmLTMaUTbStZTR5/sI7zW0=", "narHash": "sha256-70ZWAlOQ9nAZ08OU6WY7n4Ij2kOO199dLfNlvO/+pf8=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "062a0c5437b68f950b081bbfc8a699d57a4ee026", "rev": "1ffba9f2f683063c2b14c9f4d12c55ad5f4ed887",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -175,11 +174,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1646792695, "lastModified": 1650336226,
"narHash": "sha256-2drCXIKIQnJMlTZbcCfuHZAh+iPcdlRkCqtZnA6MHLY=", "narHash": "sha256-A68t/BM3JPXUDFx9JGBk24euXvsaIZuPL28+hX5TmwA=",
"owner": "oxalica", "owner": "oxalica",
"repo": "rust-overlay", "repo": "rust-overlay",
"rev": "7f599870402c8d2a5806086c8ee0f2d92b175c54", "rev": "8cd3024e5b011218308eeee35c4839601af458f8",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@@ -14,7 +14,7 @@
}; };
}; };
outputs = inputs@{ nixCargoIntegration, ... }: outputs = inputs@{ nixpkgs, nixCargoIntegration, ... }:
nixCargoIntegration.lib.makeOutputs { nixCargoIntegration.lib.makeOutputs {
root = ./.; root = ./.;
renameOutputs = { "helix-term" = "helix"; }; renameOutputs = { "helix-term" = "helix"; };
@@ -53,11 +53,16 @@
}; };
}; };
shell = common: prev: { shell = common: prev: {
packages = prev.packages ++ (with common.pkgs; [ lld_13 lldb cargo-tarpaulin cargo-flamegraph ]); packages = prev.packages ++ (with common.pkgs; [ lld_13 lldb cargo-tarpaulin cargo-flamegraph vulkan-tools ]);
env = prev.env ++ [ env = prev.env ++ [
{ name = "HELIX_RUNTIME"; eval = "$PWD/runtime"; } { name = "HELIX_RUNTIME"; eval = "$PWD/runtime"; }
{ name = "RUST_BACKTRACE"; value = "1"; } { name = "RUST_BACKTRACE"; value = "1"; }
{ name = "RUSTFLAGS"; value = "-C link-arg=-fuse-ld=lld -C target-cpu=native -Clink-arg=-Wl,--no-rosegment"; } # { name = "RUSTFLAGS"; value = "-C link-arg=-fuse-ld=lld -C target-cpu=native -Clink-arg=-Wl,--no-rosegment"; }
{ name = "LD_LIBRARY_PATH"; value = nixpkgs.lib.makeLibraryPath (with common.pkgs; [
wayland libxkbcommon xorg.libxcb
vulkan-loader # vulkan
# libGL # GLES instead of vulkan
]); }
]; ];
}; };
}; };

View File

@@ -24,7 +24,7 @@ unicode-width = "0.1"
unicode-general-category = "0.5" unicode-general-category = "0.5"
# slab = "0.4.2" # slab = "0.4.2"
slotmap = "1.0" slotmap = "1.0"
tree-sitter = "0.20" tree-sitter = "0.20" # TODO: use tree-sitter-facade / web-tree-sitter-sys on wasm32
once_cell = "1.10" once_cell = "1.10"
arc-swap = "1" arc-swap = "1"
regex = "1" regex = "1"

View File

@@ -51,7 +51,7 @@ pub fn find_root(root: Option<&str>, root_markers: &[String]) -> Option<std::pat
.cloned() .cloned()
} }
pub use ropey::{Rope, RopeBuilder, RopeSlice}; pub use ropey::{str_utils, Rope, RopeBuilder, RopeSlice};
// pub use tendril::StrTendril as Tendril; // pub use tendril::StrTendril as Tendril;
pub use smartstring::SmartString; pub use smartstring::SmartString;

23
helix-graphics/Cargo.toml Normal file
View File

@@ -0,0 +1,23 @@
[package]
name = "helix-graphics"
version = "0.6.0"
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
edition = "2021"
license = "MPL-2.0"
repository = "https://github.com/helix-editor/helix"
homepage = "https://helix-editor.com"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/
[features]
term = ["crossterm"]
[dependencies]
bitflags = "1.3"
serde = { version = "1.0", features = ["derive"] }
crossterm = { version = "0.23", optional = true }
# TODO: graphics.rs tests rely on this, but we should remove that
# [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
# helix-tui = { path = "../helix-tui" }

View File

@@ -199,22 +199,6 @@ pub mod util {
}), }),
) )
} }
/// The result of asking the language server to format the document. This can be turned into a
/// `Transaction`, but the advantage of not doing that straight away is that this one is
/// `Send` and `Sync`.
#[derive(Clone, Debug)]
pub struct LspFormatting {
pub doc: Rope,
pub edits: Vec<lsp::TextEdit>,
pub offset_encoding: OffsetEncoding,
}
impl From<LspFormatting> for Transaction {
fn from(fmt: LspFormatting) -> Transaction {
generate_transaction_from_edits(&fmt.doc, fmt.edits, fmt.offset_encoding)
}
}
} }
#[derive(Debug, PartialEq, Clone)] #[derive(Debug, PartialEq, Clone)]

View File

@@ -16,6 +16,9 @@ build = true
app = true app = true
[features] [features]
default = ["dap", "lsp"]
dap = ["helix-dap", "helix-view/dap"]
lsp = ["helix-lsp", "helix-view/lsp"]
unicode-lines = ["helix-core/unicode-lines"] unicode-lines = ["helix-core/unicode-lines"]
[[bin]] [[bin]]
@@ -24,9 +27,9 @@ path = "src/main.rs"
[dependencies] [dependencies]
helix-core = { version = "0.6", path = "../helix-core" } helix-core = { version = "0.6", path = "../helix-core" }
helix-view = { version = "0.6", path = "../helix-view" } helix-view = { version = "0.6", path = "../helix-view", features = ["term"] }
helix-lsp = { version = "0.6", path = "../helix-lsp" } helix-lsp = { version = "0.6", path = "../helix-lsp", optional = true }
helix-dap = { version = "0.6", path = "../helix-dap" } helix-dap = { version = "0.6", path = "../helix-dap", optional = true }
helix-loader = { version = "0.6", path = "../helix-loader" } helix-loader = { version = "0.6", path = "../helix-loader" }
anyhow = "1" anyhow = "1"
@@ -48,27 +51,12 @@ fern = "0.6"
chrono = { version = "0.4", default-features = false, features = ["clock"] } chrono = { version = "0.4", default-features = false, features = ["clock"] }
log = "0.4" log = "0.4"
# File picker
fuzzy-matcher = "0.3"
ignore = "0.4"
# markdown doc rendering
pulldown-cmark = { version = "0.9", default-features = false }
# file type detection
content_inspector = "0.2.4"
# config # config
toml = "0.5" toml = "0.5"
serde_json = "1.0" serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
# ripgrep for global search
grep-regex = "0.1.9"
grep-searcher = "0.1.8"
# Remove once retain_mut lands in stable rust
retain_mut = "0.1.7"
[target.'cfg(not(windows))'.dependencies] # https://github.com/vorner/signal-hook/issues/100 [target.'cfg(not(windows))'.dependencies] # https://github.com/vorner/signal-hook/issues/100
signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] } signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] }

View File

@@ -3,21 +3,30 @@ use helix_core::{
config::{default_syntax_loader, user_syntax_loader}, config::{default_syntax_loader, user_syntax_loader},
pos_at_coords, syntax, Selection, pos_at_coords, syntax, Selection,
}; };
#[cfg(feature = "lsp")]
use helix_lsp::{lsp, util::lsp_pos_to_pos, LspProgressMap}; use helix_lsp::{lsp, util::lsp_pos_to_pos, LspProgressMap};
use helix_view::{align_view, editor::ConfigEvent, theme, Align, Editor}; #[cfg(feature = "lsp")]
use helix_view::commands::apply_workspace_edit;
#[cfg(feature = "lsp")]
use serde_json::json; use serde_json::json;
use crate::{ use helix_view::{
args::Args, align_view, editor::ConfigEvent, graphics::Rect, theme, true_color, Align, Editor,
commands::apply_workspace_edit, };
compositor::Compositor,
config::Config, use crate::{args::Args, config::Config};
job::Jobs,
use helix_view::{
keymap::Keymaps, keymap::Keymaps,
ui::{self, overlay::overlayed}, ui::{self, overlay::overlayed},
}; };
use log::{error, warn}; use helix_view::{
compositor::{Compositor, Event},
job::Jobs,
};
use std::{ use std::{
io::{stdin, stdout, Write}, io::{stdin, stdout, Write},
sync::Arc, sync::Arc,
@@ -27,7 +36,7 @@ use std::{
use anyhow::Error; use anyhow::Error;
use crossterm::{ use crossterm::{
event::{DisableMouseCapture, EnableMouseCapture, Event, EventStream}, event::{DisableMouseCapture, EnableMouseCapture, Event as CrosstermEvent, EventStream},
execute, terminal, execute, terminal,
tty::IsTty, tty::IsTty,
}; };
@@ -39,8 +48,12 @@ use {
#[cfg(windows)] #[cfg(windows)]
type Signals = futures_util::stream::Empty<()>; type Signals = futures_util::stream::Empty<()>;
use tui::backend::{Backend, CrosstermBackend};
type Terminal = tui::terminal::Terminal<CrosstermBackend<std::io::Stdout>>;
pub struct Application { pub struct Application {
compositor: Compositor, compositor: Compositor,
terminal: Terminal,
editor: Editor, editor: Editor,
config: Arc<ArcSwap<Config>>, config: Arc<ArcSwap<Config>>,
@@ -52,6 +65,7 @@ pub struct Application {
signals: Signals, signals: Signals,
jobs: Jobs, jobs: Jobs,
#[cfg(feature = "lsp")]
lsp_progress: LspProgressMap, lsp_progress: LspProgressMap,
} }
@@ -66,7 +80,7 @@ impl Application {
let config = match std::fs::read_to_string(config_dir.join("config.toml")) { let config = match std::fs::read_to_string(config_dir.join("config.toml")) {
Ok(config) => toml::from_str(&config) Ok(config) => toml::from_str(&config)
.map(crate::keymap::merge_keys) .map(crate::config::merge_keys)
.unwrap_or_else(|err| { .unwrap_or_else(|err| {
eprintln!("Bad config: {}", err); eprintln!("Bad config: {}", err);
eprintln!("Press <ENTER> to continue with default config"); eprintln!("Press <ENTER> to continue with default config");
@@ -84,7 +98,7 @@ impl Application {
&helix_loader::runtime_dir(), &helix_loader::runtime_dir(),
)); ));
let true_color = config.editor.true_color || crate::true_color(); let true_color = config.editor.true_color || true_color();
let theme = config let theme = config
.theme .theme
.as_ref() .as_ref()
@@ -116,10 +130,13 @@ impl Application {
}); });
let syn_loader = std::sync::Arc::new(syntax::Loader::new(syn_loader_conf)); let syn_loader = std::sync::Arc::new(syntax::Loader::new(syn_loader_conf));
let mut compositor = Compositor::new()?; let backend = CrosstermBackend::new(stdout());
let terminal = Terminal::new(backend)?;
let area = terminal.size().expect("couldn't get terminal size");
let mut compositor = Compositor::new(area);
let config = Arc::new(ArcSwap::from_pointee(config)); let config = Arc::new(ArcSwap::from_pointee(config));
let mut editor = Editor::new( let mut editor = Editor::new(
compositor.size(), area,
theme_loader.clone(), theme_loader.clone(),
syn_loader.clone(), syn_loader.clone(),
Box::new(Map::new(Arc::clone(&config), |config: &Config| { Box::new(Map::new(Arc::clone(&config), |config: &Config| {
@@ -190,6 +207,7 @@ impl Application {
let app = Self { let app = Self {
compositor, compositor,
terminal,
editor, editor,
config, config,
@@ -199,6 +217,8 @@ impl Application {
signals, signals,
jobs: Jobs::new(), jobs: Jobs::new(),
#[cfg(feature = "lsp")]
lsp_progress: LspProgressMap::new(), lsp_progress: LspProgressMap::new(),
}; };
@@ -206,13 +226,28 @@ impl Application {
} }
fn render(&mut self) { fn render(&mut self) {
let mut cx = crate::compositor::Context { let area = self
editor: &mut self.editor, .terminal
jobs: &mut self.jobs, .autoresize()
.expect("Unable to determine terminal size");
// if the terminal size suddenly changed, we need to trigger a resize
self.editor.resize(area.clip_bottom(1)); // -1 from bottom for commandline
let surface = self.terminal.current_buffer_mut();
// let area = *surface.area();
let mut render_cx = helix_view::compositor::RenderContext {
editor: &self.editor,
surface,
scroll: None, scroll: None,
}; };
self.compositor.render(area, &mut render_cx);
self.compositor.render(&mut cx); let (pos, kind) = self.compositor.cursor(area, &self.editor);
let pos = pos.map(|pos| (pos.col as u16, pos.row as u16));
self.terminal.draw(pos, kind).unwrap();
} }
pub async fn event_loop(&mut self) { pub async fn event_loop(&mut self) {
@@ -239,16 +274,21 @@ impl Application {
self.handle_signals(signal).await; self.handle_signals(signal).await;
} }
Some((id, call)) = self.editor.language_servers.incoming.next() => { Some((id, call)) = self.editor.language_servers.incoming.next() => {
#[cfg(feature = "lsp")]
self.handle_language_server_message(call, id).await; self.handle_language_server_message(call, id).await;
// limit render calls for fast language server messages // limit render calls for fast language server messages
#[cfg(feature = "lsp")]
let last = self.editor.language_servers.incoming.is_empty(); let last = self.editor.language_servers.incoming.is_empty();
#[cfg(feature = "lsp")]
if last || last_render.elapsed() > deadline { if last || last_render.elapsed() > deadline {
self.render(); self.render();
last_render = Instant::now(); last_render = Instant::now();
} }
} }
Some(payload) = self.editor.debugger_events.next() => { Some(payload) = self.editor.debugger_events.next() => {
#[cfg(feature = "dap")]
let needs_render = self.editor.handle_debugger_message(payload).await; let needs_render = self.editor.handle_debugger_message(payload).await;
#[cfg(feature = "dap")]
if needs_render { if needs_render {
self.render(); self.render();
} }
@@ -320,7 +360,7 @@ impl Application {
} }
fn true_color(&self) -> bool { fn true_color(&self) -> bool {
self.config.load().editor.true_color || crate::true_color() self.config.load().editor.true_color || true_color()
} }
#[cfg(windows)] #[cfg(windows)]
@@ -329,19 +369,19 @@ impl Application {
#[cfg(not(windows))] #[cfg(not(windows))]
pub async fn handle_signals(&mut self, signal: i32) { pub async fn handle_signals(&mut self, signal: i32) {
use helix_view::graphics::Rect;
match signal { match signal {
signal::SIGTSTP => { signal::SIGTSTP => {
self.compositor.save_cursor();
self.restore_term().unwrap(); self.restore_term().unwrap();
low_level::emulate_default_handler(signal::SIGTSTP).unwrap(); low_level::emulate_default_handler(signal::SIGTSTP).unwrap();
} }
signal::SIGCONT => { signal::SIGCONT => {
self.claim_term().await.unwrap(); self.claim_term().await.unwrap();
// redraw the terminal // redraw the terminal
let Rect { width, height, .. } = self.compositor.size(); let area = self
self.compositor.resize(width, height); .terminal
self.compositor.load_cursor(); .size()
.expect("Unable to determine terminal size");
self.terminal.resize(area);
self.render(); self.render();
} }
_ => unreachable!(), _ => unreachable!(),
@@ -349,37 +389,44 @@ impl Application {
} }
pub fn handle_idle_timeout(&mut self) { pub fn handle_idle_timeout(&mut self) {
use crate::compositor::EventResult; use helix_view::compositor::EventResult;
let editor_view = self let editor_view = self
.compositor .compositor
.find::<ui::EditorView>() .find::<ui::EditorView>()
.expect("expected at least one EditorView"); .expect("expected at least one EditorView");
let mut cx = crate::compositor::Context { let mut cx = helix_view::compositor::Context {
editor: &mut self.editor, editor: &mut self.editor,
jobs: &mut self.jobs, jobs: &mut self.jobs,
scroll: None,
}; };
if let EventResult::Consumed(_) = editor_view.handle_idle_timeout(&mut cx) { if let EventResult::Consumed(_) = editor_view.handle_idle_timeout(&mut cx) {
self.render(); self.render();
} }
} }
pub fn handle_terminal_events(&mut self, event: Option<Result<Event, crossterm::ErrorKind>>) { pub fn handle_terminal_events(
let mut cx = crate::compositor::Context { &mut self,
event: Option<Result<CrosstermEvent, crossterm::ErrorKind>>,
) {
let mut cx = helix_view::compositor::Context {
editor: &mut self.editor, editor: &mut self.editor,
jobs: &mut self.jobs, jobs: &mut self.jobs,
scroll: None,
}; };
// Handle key events // Handle key events
let should_redraw = match event { let should_redraw = match event {
Some(Ok(Event::Resize(width, height))) => { Some(Ok(CrosstermEvent::Resize(width, height))) => {
self.compositor.resize(width, height); self.terminal
.resize(Rect::new(0, 0, width, height))
.expect("Unable to resize terminal");
let area = self.terminal.size().expect("couldn't get terminal size");
self.compositor.resize(area);
self.compositor self.compositor
.handle_event(Event::Resize(width, height), &mut cx) .handle_event(Event::Resize(width, height), &mut cx)
} }
Some(Ok(event)) => self.compositor.handle_event(event, &mut cx), Some(Ok(event)) => self.compositor.handle_event(event.into(), &mut cx),
Some(Err(x)) => panic!("{}", x), Some(Err(x)) => panic!("{}", x),
None => panic!(), None => panic!(),
}; };
@@ -389,6 +436,7 @@ impl Application {
} }
} }
#[cfg(feature = "lsp")]
pub async fn handle_language_server_message( pub async fn handle_language_server_message(
&mut self, &mut self,
call: helix_lsp::Call, call: helix_lsp::Call,
@@ -405,14 +453,17 @@ impl Application {
match notification { match notification {
Notification::Initialized => { Notification::Initialized => {
let language_server = let language_server = match self
match self.editor.language_servers.get_by_id(server_id) { .editor
Some(language_server) => language_server, .language_servers
None => { .get_by_id(server_id)
warn!("can't find language server with id `{}`", server_id); {
return; Some(language_server) => language_server,
} None => {
}; log::warn!("can't find language server with id `{}`", server_id);
return;
}
};
// Trigger a workspace/didChangeConfiguration notification after initialization. // Trigger a workspace/didChangeConfiguration notification after initialization.
// This might not be required by the spec but Neovim does this as well, so it's // This might not be required by the spec but Neovim does this as well, so it's
@@ -615,7 +666,7 @@ impl Application {
let call = match MethodCall::parse(&method, params) { let call = match MethodCall::parse(&method, params) {
Some(call) => call, Some(call) => call,
None => { None => {
error!("Method not found {}", method); log::error!("Method not found {}", method);
return; return;
} }
}; };
@@ -687,7 +738,7 @@ impl Application {
let language_server = match self.editor.language_servers.get_by_id(server_id) { let language_server = match self.editor.language_servers.get_by_id(server_id) {
Some(language_server) => language_server, Some(language_server) => language_server,
None => { None => {
warn!("can't find language server with id `{}`", server_id); log::warn!("can't find language server with id `{}`", server_id);
return; return;
} }
}; };
@@ -699,7 +750,11 @@ impl Application {
} }
async fn claim_term(&mut self) -> Result<(), Error> { async fn claim_term(&mut self) -> Result<(), Error> {
use helix_view::graphics::CursorKind;
terminal::enable_raw_mode()?; terminal::enable_raw_mode()?;
if self.terminal.cursor_kind() == CursorKind::Hidden {
self.terminal.backend_mut().hide_cursor().ok();
}
let mut stdout = stdout(); let mut stdout = stdout();
execute!(stdout, terminal::EnterAlternateScreen)?; execute!(stdout, terminal::EnterAlternateScreen)?;
execute!(stdout, terminal::Clear(terminal::ClearType::All))?; execute!(stdout, terminal::Clear(terminal::ClearType::All))?;
@@ -710,7 +765,16 @@ impl Application {
} }
fn restore_term(&mut self) -> Result<(), Error> { fn restore_term(&mut self) -> Result<(), Error> {
use helix_view::graphics::CursorKind;
if self.terminal.cursor_kind() == CursorKind::Hidden {
self.terminal
.backend_mut()
.show_cursor(CursorKind::Block)
.ok();
}
let mut stdout = stdout(); let mut stdout = stdout();
// reset cursor shape // reset cursor shape
write!(stdout, "\x1B[2 q")?; write!(stdout, "\x1B[2 q")?;
// Ignore errors on disabling, this might trigger on windows if we call // Ignore errors on disabling, this might trigger on windows if we call
@@ -740,6 +804,7 @@ impl Application {
self.jobs.finish().await; self.jobs.finish().await;
#[cfg(feature = "lsp")]
if self.editor.close_language_servers(None).await.is_err() { if self.editor.close_language_servers(None).await.is_err() {
log::error!("Timed out waiting for language servers to shutdown"); log::error!("Timed out waiting for language servers to shutdown");
}; };

View File

@@ -1,6 +1,7 @@
use anyhow::Result; use anyhow::Result;
use helix_core::Position; use helix_core::Position;
use std::path::{Path, PathBuf}; use helix_view::args::parse_file;
use std::path::PathBuf;
#[derive(Default)] #[derive(Default)]
pub struct Args { pub struct Args {
@@ -65,37 +66,3 @@ impl Args {
Ok(args) Ok(args)
} }
} }
/// Parse arg into [`PathBuf`] and position.
pub(crate) fn parse_file(s: &str) -> (PathBuf, Position) {
let def = || (PathBuf::from(s), Position::default());
if Path::new(s).exists() {
return def();
}
split_path_row_col(s)
.or_else(|| split_path_row(s))
.unwrap_or_else(def)
}
/// Split file.rs:10:2 into [`PathBuf`], row and col.
///
/// Does not validate if file.rs is a file or directory.
fn split_path_row_col(s: &str) -> Option<(PathBuf, Position)> {
let mut s = s.rsplitn(3, ':');
let col: usize = s.next()?.parse().ok()?;
let row: usize = s.next()?.parse().ok()?;
let path = s.next()?.into();
let pos = Position::new(row.saturating_sub(1), col.saturating_sub(1));
Some((path, pos))
}
/// Split file.rs:10 into [`PathBuf`] and row.
///
/// Does not validate if file.rs is a file or directory.
fn split_path_row(s: &str) -> Option<(PathBuf, Position)> {
let (path, row) = s.rsplit_once(':')?;
let row: usize = row.parse().ok()?;
let path = path.into();
let pos = Position::new(row.saturating_sub(1), 0);
Some((path, pos))
}

View File

@@ -1,5 +1,5 @@
use crate::keymap::{default::default, merge_keys, Keymap};
use helix_view::document::Mode; use helix_view::document::Mode;
use helix_view::keymap::{default::default, Keymap};
use serde::Deserialize; use serde::Deserialize;
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt::Display; use std::fmt::Display;
@@ -27,6 +27,15 @@ impl Default for Config {
} }
} }
/// Merge default config keys with user overwritten keys for custom user config.
pub fn merge_keys(mut config: Config) -> Config {
let mut delta = std::mem::replace(&mut config.keys, default());
for (mode, keys) in &mut config.keys {
keys.merge(delta.remove(mode).unwrap_or_default())
}
config
}
#[derive(Debug)] #[derive(Debug)]
pub enum ConfigLoadError { pub enum ConfigLoadError {
BadConfig(TomlError), BadConfig(TomlError),
@@ -63,10 +72,9 @@ mod tests {
#[test] #[test]
fn parsing_keymaps_config_file() { fn parsing_keymaps_config_file() {
use crate::keymap;
use crate::keymap::Keymap;
use helix_core::hashmap; use helix_core::hashmap;
use helix_view::document::Mode; use helix_view::document::Mode;
use helix_view::keymap::{self, Keymap};
let sample_keymaps = r#" let sample_keymaps = r#"
[keys.insert] [keys.insert]
@@ -104,4 +112,104 @@ mod tests {
let default_keys = Config::default().keys; let default_keys = Config::default().keys;
assert_eq!(default_keys, default()); assert_eq!(default_keys, default());
} }
use arc_swap::access::Constant;
use helix_core::hashmap;
#[test]
fn merge_partial_keys() {
let config = Config {
keys: hashmap! {
Mode::Normal => Keymap::new(
keymap!({ "Normal mode"
"i" => normal_mode,
"" => insert_mode,
"z" => jump_backward,
"g" => { "Merge into goto mode"
"$" => goto_line_end,
"g" => delete_char_forward,
},
})
)
},
..Default::default()
};
let mut merged_config = merge_keys(config.clone());
assert_ne!(config, merged_config);
let mut keymap = Keymaps::new(Box::new(Constant(merged_config.keys.clone())));
assert_eq!(
keymap.get(Mode::Normal, key!('i')),
KeymapResult::Matched(MappableCommand::normal_mode),
"Leaf should replace leaf"
);
assert_eq!(
keymap.get(Mode::Normal, key!('无')),
KeymapResult::Matched(MappableCommand::insert_mode),
"New leaf should be present in merged keymap"
);
// Assumes that z is a node in the default keymap
assert_eq!(
keymap.get(Mode::Normal, key!('z')),
KeymapResult::Matched(MappableCommand::jump_backward),
"Leaf should replace node"
);
let keymap = merged_config.keys.get_mut(&Mode::Normal).unwrap();
// Assumes that `g` is a node in default keymap
assert_eq!(
keymap.root().search(&[key!('g'), key!('$')]).unwrap(),
&KeyTrie::Leaf(MappableCommand::goto_line_end),
"Leaf should be present in merged subnode"
);
// Assumes that `gg` is in default keymap
assert_eq!(
keymap.root().search(&[key!('g'), key!('g')]).unwrap(),
&KeyTrie::Leaf(MappableCommand::delete_char_forward),
"Leaf should replace old leaf in merged subnode"
);
// Assumes that `ge` is in default keymap
assert_eq!(
keymap.root().search(&[key!('g'), key!('e')]).unwrap(),
&KeyTrie::Leaf(MappableCommand::goto_last_line),
"Old leaves in subnode should be present in merged node"
);
assert!(merged_config.keys.get(&Mode::Normal).unwrap().len() > 1);
assert!(merged_config.keys.get(&Mode::Insert).unwrap().len() > 0);
}
#[test]
fn order_should_be_set() {
let config = Config {
keys: hashmap! {
Mode::Normal => Keymap::new(
keymap!({ "Normal mode"
"space" => { ""
"s" => { ""
"v" => vsplit,
"c" => hsplit,
},
},
})
)
},
..Default::default()
};
let mut merged_config = merge_keys(config.clone());
assert_ne!(config, merged_config);
let keymap = merged_config.keys.get_mut(&Mode::Normal).unwrap();
// Make sure mapping works
assert_eq!(
keymap
.root()
.search(&[key!(' '), key!('s'), key!('v')])
.unwrap(),
&KeyTrie::Leaf(MappableCommand::vsplit),
"Leaf should be present in merged subnode"
);
// Make sure an order was set during merge
let node = keymap.root().search(&[helix_view::key!(' ')]).unwrap();
assert!(!node.node().unwrap().order().is_empty())
}
} }

View File

@@ -1,127 +0,0 @@
#[macro_export]
macro_rules! key {
($key:ident) => {
::helix_view::input::KeyEvent {
code: ::helix_view::keyboard::KeyCode::$key,
modifiers: ::helix_view::keyboard::KeyModifiers::NONE,
}
};
($($ch:tt)*) => {
::helix_view::input::KeyEvent {
code: ::helix_view::keyboard::KeyCode::Char($($ch)*),
modifiers: ::helix_view::keyboard::KeyModifiers::NONE,
}
};
}
#[macro_export]
macro_rules! shift {
($key:ident) => {
::helix_view::input::KeyEvent {
code: ::helix_view::keyboard::KeyCode::$key,
modifiers: ::helix_view::keyboard::KeyModifiers::SHIFT,
}
};
($($ch:tt)*) => {
::helix_view::input::KeyEvent {
code: ::helix_view::keyboard::KeyCode::Char($($ch)*),
modifiers: ::helix_view::keyboard::KeyModifiers::SHIFT,
}
};
}
#[macro_export]
macro_rules! ctrl {
($key:ident) => {
::helix_view::input::KeyEvent {
code: ::helix_view::keyboard::KeyCode::$key,
modifiers: ::helix_view::keyboard::KeyModifiers::CONTROL,
}
};
($($ch:tt)*) => {
::helix_view::input::KeyEvent {
code: ::helix_view::keyboard::KeyCode::Char($($ch)*),
modifiers: ::helix_view::keyboard::KeyModifiers::CONTROL,
}
};
}
#[macro_export]
macro_rules! alt {
($key:ident) => {
::helix_view::input::KeyEvent {
code: ::helix_view::keyboard::KeyCode::$key,
modifiers: ::helix_view::keyboard::KeyModifiers::ALT,
}
};
($($ch:tt)*) => {
::helix_view::input::KeyEvent {
code: ::helix_view::keyboard::KeyCode::Char($($ch)*),
modifiers: ::helix_view::keyboard::KeyModifiers::ALT,
}
};
}
/// Macro for defining the root of a `Keymap` object. Example:
///
/// ```
/// # use helix_core::hashmap;
/// # use helix_term::keymap;
/// # use helix_term::keymap::Keymap;
/// let normal_mode = keymap!({ "Normal mode"
/// "i" => insert_mode,
/// "g" => { "Goto"
/// "g" => goto_file_start,
/// "e" => goto_file_end,
/// },
/// "j" | "down" => move_line_down,
/// });
/// let keymap = Keymap::new(normal_mode);
/// ```
#[macro_export]
macro_rules! keymap {
(@trie $cmd:ident) => {
$crate::keymap::KeyTrie::Leaf($crate::commands::MappableCommand::$cmd)
};
(@trie
{ $label:literal $(sticky=$sticky:literal)? $($($key:literal)|+ => $value:tt,)+ }
) => {
keymap!({ $label $(sticky=$sticky)? $($($key)|+ => $value,)+ })
};
(@trie [$($cmd:ident),* $(,)?]) => {
$crate::keymap::KeyTrie::Sequence(vec![$($crate::commands::Command::$cmd),*])
};
(
{ $label:literal $(sticky=$sticky:literal)? $($($key:literal)|+ => $value:tt,)+ }
) => {
// modified from the hashmap! macro
{
let _cap = hashmap!(@count $($($key),+),*);
let mut _map = ::std::collections::HashMap::with_capacity(_cap);
let mut _order = ::std::vec::Vec::with_capacity(_cap);
$(
$(
let _key = $key.parse::<::helix_view::input::KeyEvent>().unwrap();
let _duplicate = _map.insert(
_key,
keymap!(@trie $value)
);
assert!(_duplicate.is_none(), "Duplicate key found: {:?}", _duplicate.unwrap());
_order.push(_key);
)+
)*
let mut _node = $crate::keymap::KeyTrieNode::new($label, _map, _order);
$( _node.is_sticky = $sticky; )?
$crate::keymap::KeyTrie::Node(_node)
}
};
}
pub use alt;
pub use ctrl;
pub use key;
pub use keymap;
pub use shift;

View File

@@ -3,22 +3,5 @@ extern crate helix_view;
pub mod application; pub mod application;
pub mod args; pub mod args;
pub mod commands;
pub mod compositor;
pub mod config; pub mod config;
pub mod health; pub mod health;
pub mod job;
pub mod keymap;
pub mod ui;
pub use keymap::macros::*;
#[cfg(not(windows))]
fn true_color() -> bool {
std::env::var("COLORTERM")
.map(|v| matches!(v.as_str(), "truecolor" | "24bit"))
.unwrap_or(false)
}
#[cfg(windows)]
fn true_color() -> bool {
true
}

View File

@@ -1,41 +0,0 @@
use crate::compositor::{Component, Context};
use helix_view::graphics::{Margin, Rect};
use helix_view::info::Info;
use tui::buffer::Buffer as Surface;
use tui::widgets::{Block, Borders, Paragraph, Widget};
impl Component for Info {
fn render(&mut self, viewport: Rect, surface: &mut Surface, cx: &mut Context) {
let text_style = cx.editor.theme.get("ui.text.info");
let popup_style = cx.editor.theme.get("ui.popup.info");
// Calculate the area of the terminal to modify. Because we want to
// render at the bottom right, we use the viewport's width and height
// which evaluate to the most bottom right coordinate.
let width = self.width + 2 + 2; // +2 for border, +2 for margin
let height = self.height + 2; // +2 for border
let area = viewport.intersection(Rect::new(
viewport.width.saturating_sub(width),
viewport.height.saturating_sub(height + 2), // +2 for statusline
width,
height,
));
surface.clear_with(area, popup_style);
let block = Block::default()
.title(self.title.as_str())
.borders(Borders::ALL)
.border_style(popup_style);
let margin = Margin {
vertical: 0,
horizontal: 1,
};
let inner = block.inner(area).inner(&margin);
block.render(area, surface);
Paragraph::new(self.text.as_str())
.style(text_style)
.render(inner, surface);
}
}

View File

@@ -21,5 +21,5 @@ cassowary = "0.3"
unicode-segmentation = "1.9" unicode-segmentation = "1.9"
crossterm = { version = "0.23", optional = true } crossterm = { version = "0.23", optional = true }
serde = { version = "1", "optional" = true, features = ["derive"]} serde = { version = "1", "optional" = true, features = ["derive"]}
helix-view = { version = "0.6", path = "../helix-view", features = ["term"] } helix-graphics = { version = "0.6", path = "../helix-graphics", features = ["term"] }
helix-core = { version = "0.6", path = "../helix-core" } helix-core = { version = "0.6", path = "../helix-core" }

View File

@@ -8,7 +8,7 @@ use crossterm::{
}, },
terminal::{self, Clear, ClearType}, terminal::{self, Clear, ClearType},
}; };
use helix_view::graphics::{Color, CursorKind, Modifier, Rect}; use helix_graphics::{Color, CursorKind, Modifier, Rect};
use std::io::{self, Write}; use std::io::{self, Write};
pub struct CrosstermBackend<W: Write> { pub struct CrosstermBackend<W: Write> {

View File

@@ -2,7 +2,7 @@ use std::io;
use crate::buffer::Cell; use crate::buffer::Cell;
use helix_view::graphics::{CursorKind, Rect}; use helix_graphics::{CursorKind, Rect};
#[cfg(feature = "crossterm")] #[cfg(feature = "crossterm")]
mod crossterm; mod crossterm;

View File

@@ -3,7 +3,7 @@ use crate::{
buffer::{Buffer, Cell}, buffer::{Buffer, Cell},
}; };
use helix_core::unicode::width::UnicodeWidthStr; use helix_core::unicode::width::UnicodeWidthStr;
use helix_view::graphics::{CursorKind, Rect}; use helix_graphics::{CursorKind, Rect};
use std::{fmt::Write, io}; use std::{fmt::Write, io};
/// A backend used for the integration tests. /// A backend used for the integration tests.

View File

@@ -3,7 +3,7 @@ use helix_core::unicode::width::UnicodeWidthStr;
use std::cmp::min; use std::cmp::min;
use unicode_segmentation::UnicodeSegmentation; use unicode_segmentation::UnicodeSegmentation;
use helix_view::graphics::{Color, Modifier, Rect, Style}; use helix_graphics::{Color, Modifier, Rect, Style};
/// A buffer cell /// A buffer cell
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
@@ -87,7 +87,7 @@ impl Default for Cell {
/// ///
/// ``` /// ```
/// use helix_tui::buffer::{Buffer, Cell}; /// use helix_tui::buffer::{Buffer, Cell};
/// use helix_view::graphics::{Rect, Color, Style, Modifier}; /// use helix_graphics::{Rect, Color, Style, Modifier};
/// ///
/// let mut buf = Buffer::empty(Rect{x: 0, y: 0, width: 10, height: 5}); /// let mut buf = Buffer::empty(Rect{x: 0, y: 0, width: 10, height: 5});
/// buf[(0, 2)].set_symbol("x"); /// buf[(0, 2)].set_symbol("x");
@@ -179,7 +179,7 @@ impl Buffer {
/// ///
/// ``` /// ```
/// # use helix_tui::buffer::Buffer; /// # use helix_tui::buffer::Buffer;
/// # use helix_view::graphics::Rect; /// # use helix_graphics::Rect;
/// let rect = Rect::new(200, 100, 10, 10); /// let rect = Rect::new(200, 100, 10, 10);
/// let buffer = Buffer::empty(rect); /// let buffer = Buffer::empty(rect);
/// // Global coordinates inside the Buffer's area /// // Global coordinates inside the Buffer's area
@@ -204,7 +204,7 @@ impl Buffer {
/// ///
/// ``` /// ```
/// # use helix_tui::buffer::Buffer; /// # use helix_tui::buffer::Buffer;
/// # use helix_view::graphics::Rect; /// # use helix_graphics::Rect;
/// let rect = Rect::new(200, 100, 10, 10); /// let rect = Rect::new(200, 100, 10, 10);
/// let buffer = Buffer::empty(rect); /// let buffer = Buffer::empty(rect);
/// // Global coordinates to the top corner of this Buffer's area /// // Global coordinates to the top corner of this Buffer's area
@@ -243,7 +243,7 @@ impl Buffer {
/// ///
/// ``` /// ```
/// # use helix_tui::buffer::Buffer; /// # use helix_tui::buffer::Buffer;
/// # use helix_view::graphics::Rect; /// # use helix_graphics::Rect;
/// let rect = Rect::new(200, 100, 10, 10); /// let rect = Rect::new(200, 100, 10, 10);
/// let buffer = Buffer::empty(rect); /// let buffer = Buffer::empty(rect);
/// assert_eq!(buffer.pos_of(0), (200, 100)); /// assert_eq!(buffer.pos_of(0), (200, 100));

View File

@@ -5,7 +5,7 @@ use cassowary::strength::{REQUIRED, WEAK};
use cassowary::WeightedRelation::*; use cassowary::WeightedRelation::*;
use cassowary::{Constraint as CassowaryConstraint, Expression, Solver, Variable}; use cassowary::{Constraint as CassowaryConstraint, Expression, Solver, Variable};
use helix_view::graphics::{Margin, Rect}; use helix_graphics::{Margin, Rect};
#[derive(Debug, Hash, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Hash, Clone, Copy, PartialEq, Eq)]
pub enum Corner { pub enum Corner {
@@ -115,7 +115,7 @@ impl Layout {
/// # Examples /// # Examples
/// ``` /// ```
/// # use helix_tui::layout::{Constraint, Direction, Layout}; /// # use helix_tui::layout::{Constraint, Direction, Layout};
/// # use helix_view::graphics::Rect; /// # use helix_graphics::Rect;
/// let chunks = Layout::default() /// let chunks = Layout::default()
/// .direction(Direction::Vertical) /// .direction(Direction::Vertical)
/// .constraints([Constraint::Length(5), Constraint::Min(0)].as_ref()) /// .constraints([Constraint::Length(5), Constraint::Min(0)].as_ref())

View File

@@ -1,5 +1,5 @@
use crate::{backend::Backend, buffer::Buffer}; use crate::{backend::Backend, buffer::Buffer};
use helix_view::graphics::{CursorKind, Rect}; use helix_graphics::{CursorKind, Rect};
use std::io; use std::io;
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]

View File

@@ -21,7 +21,7 @@
//! ```rust //! ```rust
//! # use helix_tui::widgets::Block; //! # use helix_tui::widgets::Block;
//! # use helix_tui::text::{Span, Spans}; //! # use helix_tui::text::{Span, Spans};
//! # use helix_view::graphics::{Color, Style}; //! # use helix_graphics::{Color, Style};
//! // A simple string with no styling. //! // A simple string with no styling.
//! // Converted to Spans(vec![ //! // Converted to Spans(vec![
//! // Span { content: Cow::Borrowed("My title"), style: Style { .. } } //! // Span { content: Cow::Borrowed("My title"), style: Style { .. } }
@@ -48,7 +48,7 @@
//! ``` //! ```
use helix_core::line_ending::str_is_line_ending; use helix_core::line_ending::str_is_line_ending;
use helix_core::unicode::width::UnicodeWidthStr; use helix_core::unicode::width::UnicodeWidthStr;
use helix_view::graphics::Style; use helix_graphics::Style;
use std::borrow::Cow; use std::borrow::Cow;
use unicode_segmentation::UnicodeSegmentation; use unicode_segmentation::UnicodeSegmentation;
@@ -92,7 +92,7 @@ impl<'a> Span<'a> {
/// ///
/// ```rust /// ```rust
/// # use helix_tui::text::Span; /// # use helix_tui::text::Span;
/// # use helix_view::graphics::{Color, Modifier, Style}; /// # use helix_graphics::{Color, Modifier, Style};
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC); /// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
/// Span::styled("My text", style); /// Span::styled("My text", style);
/// Span::styled(String::from("My text"), style); /// Span::styled(String::from("My text"), style);
@@ -121,7 +121,7 @@ impl<'a> Span<'a> {
/// ///
/// ```rust /// ```rust
/// # use helix_tui::text::{Span, StyledGrapheme}; /// # use helix_tui::text::{Span, StyledGrapheme};
/// # use helix_view::graphics::{Color, Modifier, Style}; /// # use helix_graphics::{Color, Modifier, Style};
/// # use std::iter::Iterator; /// # use std::iter::Iterator;
/// let style = Style::default().fg(Color::Yellow); /// let style = Style::default().fg(Color::Yellow);
/// let span = Span::styled("Text", style); /// let span = Span::styled("Text", style);
@@ -205,7 +205,7 @@ impl<'a> Spans<'a> {
/// ///
/// ```rust /// ```rust
/// # use helix_tui::text::{Span, Spans}; /// # use helix_tui::text::{Span, Spans};
/// # use helix_view::graphics::{Color, Style}; /// # use helix_graphics::{Color, Style};
/// let spans = Spans::from(vec![ /// let spans = Spans::from(vec![
/// Span::styled("My", Style::default().fg(Color::Yellow)), /// Span::styled("My", Style::default().fg(Color::Yellow)),
/// Span::raw(" text"), /// Span::raw(" text"),
@@ -259,7 +259,7 @@ impl<'a> From<Spans<'a>> for String {
/// ///
/// ```rust /// ```rust
/// # use helix_tui::text::Text; /// # use helix_tui::text::Text;
/// # use helix_view::graphics::{Color, Modifier, Style}; /// # use helix_graphics::{Color, Modifier, Style};
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC); /// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
/// ///
/// // An initial two lines of `Text` built from a `&str` /// // An initial two lines of `Text` built from a `&str`
@@ -307,7 +307,7 @@ impl<'a> Text<'a> {
/// ///
/// ```rust /// ```rust
/// # use helix_tui::text::Text; /// # use helix_tui::text::Text;
/// # use helix_view::graphics::{Color, Modifier, Style}; /// # use helix_graphics::{Color, Modifier, Style};
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC); /// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
/// Text::styled("The first line\nThe second line", style); /// Text::styled("The first line\nThe second line", style);
/// Text::styled(String::from("The first line\nThe second line"), style); /// Text::styled(String::from("The first line\nThe second line"), style);
@@ -357,7 +357,7 @@ impl<'a> Text<'a> {
/// ///
/// ```rust /// ```rust
/// # use helix_tui::text::Text; /// # use helix_tui::text::Text;
/// # use helix_view::graphics::{Color, Modifier, Style}; /// # use helix_graphics::{Color, Modifier, Style};
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC); /// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
/// let mut raw_text = Text::raw("The first line\nThe second line"); /// let mut raw_text = Text::raw("The first line\nThe second line");
/// let styled_text = Text::styled(String::from("The first line\nThe second line"), style); /// let styled_text = Text::styled(String::from("The first line\nThe second line"), style);

View File

@@ -4,7 +4,7 @@ use crate::{
text::{Span, Spans}, text::{Span, Spans},
widgets::{Borders, Widget}, widgets::{Borders, Widget},
}; };
use helix_view::graphics::{Rect, Style}; use helix_graphics::{Rect, Style};
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
pub enum BorderType { pub enum BorderType {
@@ -32,7 +32,7 @@ impl BorderType {
/// ///
/// ``` /// ```
/// # use helix_tui::widgets::{Block, BorderType, Borders}; /// # use helix_tui::widgets::{Block, BorderType, Borders};
/// # use helix_view::graphics::{Style, Color}; /// # use helix_graphics::{Style, Color};
/// Block::default() /// Block::default()
/// .title("Block") /// .title("Block")
/// .borders(Borders::LEFT | Borders::RIGHT) /// .borders(Borders::LEFT | Borders::RIGHT)

View File

@@ -23,7 +23,7 @@ pub use self::table::{Cell, Row, Table, TableState};
use crate::buffer::Buffer; use crate::buffer::Buffer;
use bitflags::bitflags; use bitflags::bitflags;
use helix_view::graphics::Rect; use helix_graphics::Rect;
bitflags! { bitflags! {
/// Bitflags that can be composed to set the visible borders essentially on the block widget. /// Bitflags that can be composed to set the visible borders essentially on the block widget.

View File

@@ -8,7 +8,7 @@ use crate::{
}, },
}; };
use helix_core::unicode::width::UnicodeWidthStr; use helix_core::unicode::width::UnicodeWidthStr;
use helix_view::graphics::{Rect, Style}; use helix_graphics::{Rect, Style};
use std::iter; use std::iter;
fn get_line_offset(line_width: u16, text_area_width: u16, alignment: Alignment) -> u16 { fn get_line_offset(line_width: u16, text_area_width: u16, alignment: Alignment) -> u16 {
@@ -27,7 +27,7 @@ fn get_line_offset(line_width: u16, text_area_width: u16, alignment: Alignment)
/// # use helix_tui::text::{Text, Spans, Span}; /// # use helix_tui::text::{Text, Spans, Span};
/// # use helix_tui::widgets::{Block, Borders, Paragraph, Wrap}; /// # use helix_tui::widgets::{Block, Borders, Paragraph, Wrap};
/// # use helix_tui::layout::{Alignment}; /// # use helix_tui::layout::{Alignment};
/// # use helix_view::graphics::{Style, Color, Modifier}; /// # use helix_graphics::{Style, Color, Modifier};
/// let text = vec![ /// let text = vec![
/// Spans::from(vec![ /// Spans::from(vec![
/// Span::raw("First"), /// Span::raw("First"),

View File

@@ -10,7 +10,7 @@ use cassowary::{
{Expression, Solver}, {Expression, Solver},
}; };
use helix_core::unicode::width::UnicodeWidthStr; use helix_core::unicode::width::UnicodeWidthStr;
use helix_view::graphics::{Rect, Style}; use helix_graphics::{Rect, Style};
use std::collections::HashMap; use std::collections::HashMap;
/// A [`Cell`] contains the [`Text`] to be displayed in a [`Row`] of a [`Table`]. /// A [`Cell`] contains the [`Text`] to be displayed in a [`Row`] of a [`Table`].
@@ -19,7 +19,7 @@ use std::collections::HashMap;
/// ```rust /// ```rust
/// # use helix_tui::widgets::Cell; /// # use helix_tui::widgets::Cell;
/// # use helix_tui::text::{Span, Spans, Text}; /// # use helix_tui::text::{Span, Spans, Text};
/// # use helix_view::graphics::{Style, Modifier}; /// # use helix_graphics::{Style, Modifier};
/// Cell::from("simple string"); /// Cell::from("simple string");
/// ///
/// Cell::from(Span::from("span")); /// Cell::from(Span::from("span"));
@@ -71,7 +71,7 @@ where
/// But if you need a bit more control over individual cells, you can explicitly create [`Cell`]s: /// But if you need a bit more control over individual cells, you can explicitly create [`Cell`]s:
/// ```rust /// ```rust
/// # use helix_tui::widgets::{Row, Cell}; /// # use helix_tui::widgets::{Row, Cell};
/// # use helix_view::graphics::{Style, Color}; /// # use helix_graphics::{Style, Color};
/// Row::new(vec![ /// Row::new(vec![
/// Cell::from("Cell1"), /// Cell::from("Cell1"),
/// Cell::from("Cell2").style(Style::default().fg(Color::Yellow)), /// Cell::from("Cell2").style(Style::default().fg(Color::Yellow)),
@@ -134,7 +134,7 @@ impl<'a> Row<'a> {
/// ```rust /// ```rust
/// # use helix_tui::widgets::{Block, Borders, Table, Row, Cell}; /// # use helix_tui::widgets::{Block, Borders, Table, Row, Cell};
/// # use helix_tui::layout::Constraint; /// # use helix_tui::layout::Constraint;
/// # use helix_view::graphics::{Style, Color, Modifier}; /// # use helix_graphics::{Style, Color, Modifier};
/// # use helix_tui::text::{Text, Spans, Span}; /// # use helix_tui::text::{Text, Spans, Span};
/// Table::new(vec![ /// Table::new(vec![
/// // Row can be created from simple strings. /// // Row can be created from simple strings.

30
helix-ui/Cargo.toml Normal file
View File

@@ -0,0 +1,30 @@
[package]
name = "helix-ui"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
helix-view = { version = "0.6", path = "../helix-view", features = ["ui"] }
# femtovg = { path = "../../femtovg" }
# femtovg = "0.3.3"
winit = { version = "0.26.1", default-features = false, features = ["wayland"] } # TODO: figure out wayland-dlopen
resource = "0.5.0"
image = { version = "0.24.0", default-features = false, features = ["jpeg", "png"] }
console_error_panic_hook = "0.1.7"
glutin = "0.28.0"
instant = "0.1.12"
# swash = "0.1.4"
swash = { git = "https://github.com/dfrg/swash" }
# parley = { git = "https://github.com/dfrg/parley" }
parley = { path = "../../parley" }
lyon = "0.17.10"
wgpu = "0.12"
pollster = "0.2"
glam = "0.20"
env_logger = "0.6"
bytemuck = { version = "1.9.1", features = ["derive"] }

BIN
helix-ui/assets/entypo.ttf Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,94 @@
Copyright (c) 2016-2020 The Inter Project Authors.
"Inter" is trademark of Rasmus Andersson.
https://github.com/rsms/inter
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION AND CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

965
helix-ui/src/femto.rs Normal file
View File

@@ -0,0 +1,965 @@
use resource::resource;
use instant::Instant;
use winit::event::{ElementState, Event, KeyboardInput, MouseButton, VirtualKeyCode, WindowEvent};
use winit::event_loop::{ControlFlow, EventLoop};
use winit::window::WindowBuilder;
//use glutin::{GlRequest, Api};
use femtovg::{
//CompositeOperation,
renderer::OpenGl,
Align,
Baseline,
Canvas,
Color,
FontId,
ImageFlags,
Paint,
Path,
Renderer,
Solidity,
};
// mezzopiano
// —
// 03/13/2022
// I'm also assuming that there's some logic bugs in the demo application, which wasn't built with this in mind; I have a much simpler application that I'm happy to show if that would be helpful (would need to extract an example). As a rough solution, I apply the following transformation on Winit's WindowEvent::ScaleFactorChanged:
// /* ... in an application struct/impl ... */
// pub fn rescale(
// &mut self,
// new_size: winit::dpi::PhysicalSize<u32>,
// new_scale_factor: f64,
// ) {
// // Update translation
// // (TODO: This is a guestimate and not well-tested;
// // the size updates might turn out ot be completely unnecessary)
// let shift = self.size.height as f32 - new_size.height as f32;
// self.canvas.translate(0.0, -shift);
// // Update properties
// self.size = new_size;
// self.scale_factor = new_scale_factor;
// self.canvas.set_size(
// self.size.width,
// self.size.height,
// self.scale_factor as f32,
// )
// }
// With this, the canvas position and scale is preserved while the window is moved across screens, but as I'd like to apply further translations and keeping track of everything is getting very hard 😅 . I'm wondering if I'm making a mistake somewhere, or if there might be some way to do this in femto.
pub fn quantize(a: f32, d: f32) -> f32 {
(a / d + 0.5).trunc() * d
}
struct Fonts {
regular: FontId,
bold: FontId,
icons: FontId,
}
fn main() {
// This provides better error messages in debug mode.
// It's disabled in release mode so it doesn't bloat up the file size.
#[cfg(all(debug_assertions, target_arch = "wasm32"))]
console_error_panic_hook::set_once();
let el = EventLoop::new();
#[cfg(not(target_arch = "wasm32"))]
let (renderer, windowed_context) = {
use glutin::ContextBuilder;
let wb = WindowBuilder::new()
.with_inner_size(winit::dpi::LogicalSize::<f32>::new(1000., 600.))
.with_title("femtovg demo");
//let windowed_context = ContextBuilder::new().with_gl(GlRequest::Specific(Api::OpenGlEs, (2, 0))).with_vsync(false).build_windowed(wb, &el).unwrap();
//let windowed_context = ContextBuilder::new().with_vsync(false).with_multisampling(8).build_windowed(wb, &el).unwrap();
let windowed_context = ContextBuilder::new()
.with_vsync(true) // TODO: set to true?
.build_windowed(wb, &el)
.unwrap();
let windowed_context = unsafe { windowed_context.make_current().unwrap() };
let renderer =
OpenGl::new_from_glutin_context(&windowed_context).expect("Cannot create renderer");
(renderer, windowed_context)
};
#[cfg(target_arch = "wasm32")]
let (renderer, window) = {
use wasm_bindgen::JsCast;
let canvas = web_sys::window()
.unwrap()
.document()
.unwrap()
.get_element_by_id("canvas")
.unwrap()
.dyn_into::<web_sys::HtmlCanvasElement>()
.unwrap();
use winit::platform::web::WindowBuilderExtWebSys;
let renderer = OpenGl::new_from_html_canvas(&canvas).expect("Cannot create renderer");
let window = WindowBuilder::new()
.with_canvas(Some(canvas))
.build(&el)
.unwrap();
(renderer, window)
};
let mut canvas = Canvas::new(renderer).expect("Cannot create canvas");
// TODO: better femtovg support for variable fonts
let fonts = Fonts {
regular: canvas
.add_font_mem(&resource!("assets/fonts/Inter\ Variable/Inter.ttf"))
.expect("Cannot add font"),
bold: canvas
.add_font_mem(&resource!("assets/fonts/Inter Variable/Inter.ttf"))
.expect("Cannot add font"),
icons: canvas
.add_font_mem(&resource!("assets/entypo.ttf"))
.expect("Cannot add font"),
};
//canvas.add_font("/usr/share/fonts/noto/NotoSansArabic-Regular.ttf").expect("Cannot add font");
//let image_id = canvas.create_image_file("assets/RoomRender.jpg", ImageFlags::FLIP_Y).expect("Cannot create image");
//canvas.blur_image(image_id, 10, 1050, 710, 200, 200);
//let image_id = canvas.load_image_file("assets/RoomRender.jpg", ImageFlags::FLIP_Y).expect("Cannot create image");
// let images = vec![
// canvas
// .load_image_mem(&resource!("assets/images/image1.jpg"), ImageFlags::empty())
// .unwrap(),
// canvas
// .load_image_mem(&resource!("assets/images/image2.jpg"), ImageFlags::empty())
// .unwrap(),
// ];
let mut screenshot_image_id = None;
let start = Instant::now();
let mut prevt = start;
let mut mousex = 0.0;
let mut mousey = 0.0;
let mut dragging = false;
let mut perf = PerfGraph::new();
el.run(move |event, _, control_flow| {
#[cfg(not(target_arch = "wasm32"))]
let window = windowed_context.window();
*control_flow = ControlFlow::Poll;
match event {
Event::LoopDestroyed => return,
Event::WindowEvent { ref event, .. } => match event {
#[cfg(not(target_arch = "wasm32"))]
WindowEvent::Resized(physical_size) => {
println!("resized!");
// TODO: use DPI here?
windowed_context.resize(*physical_size);
}
WindowEvent::CursorMoved {
device_id: _,
position,
..
} => {
if dragging {
let p0 = canvas
.transform()
.inversed()
.transform_point(mousex, mousey);
let p1 = canvas
.transform()
.inversed()
.transform_point(position.x as f32, position.y as f32);
canvas.translate(p1.0 - p0.0, p1.1 - p0.1);
}
mousex = position.x as f32;
mousey = position.y as f32;
}
WindowEvent::MouseWheel {
device_id: _,
delta,
..
} => match delta {
winit::event::MouseScrollDelta::LineDelta(_, y) => {
let pt = canvas
.transform()
.inversed()
.transform_point(mousex, mousey);
canvas.translate(pt.0, pt.1);
canvas.scale(1.0 + (y / 10.0), 1.0 + (y / 10.0));
canvas.translate(-pt.0, -pt.1);
}
winit::event::MouseScrollDelta::PixelDelta(pos) => {
let y = pos.y as f32;
let pt = canvas
.transform()
.inversed()
.transform_point(mousex, mousey);
let rate = 2000.0;
canvas.translate(pt.0, pt.1);
canvas.scale(1.0 + (y / rate), 1.0 + (y / rate));
canvas.translate(-pt.0, -pt.1);
}
},
WindowEvent::MouseInput {
button: MouseButton::Left,
state,
..
} => match state {
ElementState::Pressed => dragging = true,
ElementState::Released => dragging = false,
},
WindowEvent::KeyboardInput {
input:
KeyboardInput {
virtual_keycode: Some(VirtualKeyCode::S),
state: ElementState::Pressed,
..
},
..
} => {
if let Some(screenshot_image_id) = screenshot_image_id {
canvas.delete_image(screenshot_image_id);
}
if let Ok(image) = canvas.screenshot() {
screenshot_image_id = Some(
canvas
.create_image(image.as_ref(), ImageFlags::empty())
.unwrap(),
);
}
}
WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit,
_ => (),
},
Event::RedrawRequested(_) => {
let now = Instant::now();
let dt = (now - prevt).as_secs_f32();
prevt = now;
perf.update(dt);
let dpi_factor = window.scale_factor();
// println!("DPI {}", dpi_factor);
// let dpi_factor = 0.5f64;
let size = window.inner_size();
// let size: winit::dpi::LogicalSize<u32> = window.inner_size().to_logical(dpi_factor); // TODO: adjust for dpi
// window.set_inner_size(size);
// let size = window.inner_size();
// let t = start.elapsed().as_secs_f32();
canvas.set_size(size.width as u32, size.height as u32, dpi_factor as f32);
canvas.clear_rect(
0,
0,
size.width as u32,
size.height as u32,
Color::rgbf(0.3, 0.3, 0.32),
);
// let height = size.height as f32;
// let width = size.width as f32;
let winit::dpi::LogicalSize { width, height: _ } =
size.to_logical::<f32>(dpi_factor);
let pt = canvas
.transform()
.inversed()
.transform_point(mousex, mousey);
let rel_mousex = pt.0;
let rel_mousey = pt.1;
draw_paragraph(
&mut canvas,
fonts.regular,
width - 450.0,
50.0,
150.0,
100.0,
rel_mousex,
rel_mousey,
);
draw_window(
&mut canvas,
&fonts,
"Widgets `n Stuff",
50.0,
50.0,
300.0,
400.0,
);
let x = 60.0;
let mut y = 95.0;
draw_search_box(&mut canvas, &fonts, "Search", x, y, 280.0, 25.0);
y += 40.0;
draw_drop_down(&mut canvas, &fonts, "Effects", 60.0, 135.0, 280.0, 28.0);
y += 45.0;
draw_label(&mut canvas, &fonts, "Login", x, y, 280.0, 20.0);
y += 25.0;
draw_edit_box(&mut canvas, &fonts, "Email", x, y, 280.0, 28.0);
y += 35.0;
draw_edit_box(&mut canvas, &fonts, "Password", x, y, 280.0, 28.0);
y += 38.0;
draw_check_box(&mut canvas, &fonts, "Remember me", x, y, 140.0, 28.0);
draw_button(
&mut canvas,
&fonts,
Some("\u{E740}"),
"Sign in",
x + 138.0,
y,
140.0,
28.0,
Color::rgba(0, 96, 128, 255),
);
y += 45.0;
// Slider
draw_label(&mut canvas, &fonts, "Diameter", x, y, 280.0, 20.0);
y += 25.0;
draw_edit_box_num(
&mut canvas,
&fonts,
"123.00",
"px",
x + 180.0,
y,
100.0,
28.0,
);
y += 55.0;
draw_button(
&mut canvas,
&fonts,
Some("\u{E729}"),
"Delete",
x,
y,
160.0,
28.0,
Color::rgba(128, 16, 8, 255),
);
draw_button(
&mut canvas,
&fonts,
None,
"Cancel",
x + 170.0,
y,
110.0,
28.0,
Color::rgba(0, 0, 0, 0),
);
/*
draw_spinner(&mut canvas, 15.0, 285.0, 10.0, t);
*/
if let Some(image_id) = screenshot_image_id {
let x = size.width as f32 - 512.0;
let y = size.height as f32 - 512.0;
let paint = Paint::image(image_id, x, y, 512.0, 512.0, 0.0, 1.0);
let mut path = Path::new();
path.rect(x, y, 512.0, 512.0);
canvas.fill_path(&mut path, paint);
canvas.stroke_path(&mut path, Paint::color(Color::hex("454545")));
}
// if true {
// let paint = Paint::image(image_id, size.width as f32, 15.0, 1920.0, 1080.0, 0.0, 1.0);
// let mut path = Path::new();
// path.rect(size.width as f32, 15.0, 1920.0, 1080.0);
// canvas.fill_path(&mut path, paint);
// }
canvas.save_with(|canvas| {
canvas.reset();
perf.render(canvas, 5.0, 5.0);
});
//canvas.restore();
canvas.flush();
#[cfg(not(target_arch = "wasm32"))]
windowed_context.swap_buffers().unwrap();
}
Event::MainEventsCleared => {
//scroll = 1.0;
window.request_redraw()
}
_ => (),
}
});
}
fn draw_paragraph<T: Renderer>(
canvas: &mut Canvas<T>,
font: FontId,
x: f32,
y: f32,
width: f32,
_height: f32,
mx: f32,
my: f32,
) {
let text = "This is longer chunk of text.\n\nWould have used lorem ipsum but she was busy jumping over the lazy dog with the fox and all the men who came to the aid of the party.🎉";
canvas.save();
let mut paint = Paint::color(Color::rgba(255, 255, 255, 255));
paint.set_font_size(14.0);
paint.set_font(&[font]);
paint.set_text_align(Align::Left);
paint.set_text_baseline(Baseline::Top);
let mut gutter_y = 0.0;
let mut gutter = 0;
let mut y = y;
let mut px;
let mut caret_x;
let lines = canvas
.break_text_vec(width, text, paint)
.expect("Cannot break text");
for (line_num, line_range) in lines.into_iter().enumerate() {
if let Ok(res) = canvas.fill_text(x, y, &text[line_range], paint) {
let hit = mx > x && mx < (x + width) && my >= y && my < (y + res.height());
if hit {
caret_x = if mx < x + res.width() / 2.0 {
x
} else {
x + res.width()
};
px = x;
for glyph in &res.glyphs {
let x0 = glyph.x;
let x1 = x0 + glyph.width;
let gx = x0 * 0.3 + x1 * 0.7;
if mx >= px && mx < gx {
caret_x = glyph.x;
}
px = gx;
}
let mut path = Path::new();
path.rect(caret_x, y, 1.0, res.height());
canvas.fill_path(&mut path, Paint::color(Color::rgba(255, 192, 0, 255)));
gutter = line_num + 1;
gutter_y = y + 14.0 / 2.0;
}
y += res.height();
}
}
if gutter > 0 {
let mut paint = Paint::color(Color::rgba(255, 192, 0, 255));
paint.set_font_size(12.0);
paint.set_font(&[font]);
paint.set_text_align(Align::Right);
paint.set_text_baseline(Baseline::Middle);
let text = format!("{}", gutter);
if let Ok(res) = canvas.measure_text(x - 10.0, gutter_y, &text, paint) {
let mut path = Path::new();
path.rounded_rect(
res.x - 4.0,
res.y - 2.0,
res.width() + 8.0,
res.height() + 4.0,
(res.height() + 4.0) / 2.0 - 1.0,
);
canvas.fill_path(&mut path, paint);
paint.set_color(Color::rgba(32, 32, 32, 255));
let _ = canvas.fill_text(x - 10.0, gutter_y, &text, paint);
}
}
// let mut start = 0;
// while start < text.len() {
// let substr = &text[start..];
// if let Ok(index) = canvas.break_text(width, substr, paint) {
// if let Ok(res) = canvas.fill_text(x, y, &substr[0..index], paint) {
// y += res.height;
// }
// start += &substr[0..index].len();
// } else {
// break;
// }
// }
canvas.restore();
}
fn draw_window<T: Renderer>(
canvas: &mut Canvas<T>,
fonts: &Fonts,
title: &str,
x: f32,
y: f32,
w: f32,
h: f32,
) {
let corner_radius = 3.0;
canvas.save();
//canvas.global_composite_operation(CompositeOperation::Lighter);
// Window
let mut path = Path::new();
path.rounded_rect(x, y, w, h, corner_radius);
canvas.fill_path(&mut path, Paint::color(Color::rgba(28, 30, 34, 192)));
// Drop shadow
let shadow_paint = Paint::box_gradient(
x,
y + 2.0,
w,
h,
corner_radius * 2.0,
10.0,
Color::rgba(0, 0, 0, 128),
Color::rgba(0, 0, 0, 0),
);
let mut path = Path::new();
path.rect(x - 10.0, y - 10.0, w + 20.0, h + 30.0);
path.rounded_rect(x, y, w, h, corner_radius);
path.solidity(Solidity::Hole);
canvas.fill_path(&mut path, shadow_paint);
// Header
let header_paint = Paint::linear_gradient(
x,
y,
x,
y + 15.0,
Color::rgba(255, 255, 255, 8),
Color::rgba(0, 0, 0, 16),
);
let mut path = Path::new();
path.rounded_rect(x + 1.0, y + 1.0, w - 2.0, 30.0, corner_radius - 1.0);
canvas.fill_path(&mut path, header_paint);
let mut path = Path::new();
path.move_to(x + 0.5, y + 0.5 + 30.0);
path.line_to(x + 0.5 + w - 1.0, y + 0.5 + 30.0);
canvas.stroke_path(&mut path, Paint::color(Color::rgba(0, 0, 0, 32)));
let mut text_paint = Paint::color(Color::rgba(0, 0, 0, 32));
text_paint.set_font_size(16.0);
text_paint.set_font(&[fonts.bold]);
text_paint.set_text_align(Align::Center);
text_paint.set_color(Color::rgba(220, 220, 220, 160));
let _ = canvas.fill_text(x + (w / 2.0), y + 19.0, title, text_paint);
// let bounds = canvas.text_bounds(x + (w / 2.0), y + 19.0, title, text_paint);
//
// let mut path = Path::new();
// path.rect(bounds[0], bounds[1], bounds[2] - bounds[0], bounds[3] - bounds[1]);
// canvas.stroke_path(&mut path, Paint::color(Color::rgba(0, 0, 0, 255)));
canvas.restore();
}
fn draw_search_box<T: Renderer>(
canvas: &mut Canvas<T>,
fonts: &Fonts,
title: &str,
x: f32,
y: f32,
w: f32,
h: f32,
) {
let corner_radius = (h / 2.0) - 1.0;
let bg = Paint::box_gradient(
x,
y + 1.5,
w,
h,
h / 2.0,
5.0,
Color::rgba(0, 0, 0, 16),
Color::rgba(0, 0, 0, 92),
);
let mut path = Path::new();
path.rounded_rect(x, y, w, h, corner_radius);
canvas.fill_path(&mut path, bg);
let mut text_paint = Paint::color(Color::rgba(255, 255, 255, 64));
text_paint.set_font_size((h * 1.3).round());
text_paint.set_font(&[fonts.icons]);
text_paint.set_text_align(Align::Center);
text_paint.set_text_baseline(Baseline::Middle);
let _ = canvas.fill_text(x + h * 0.55, y + h * 0.55, "\u{1F50D}", text_paint);
let mut text_paint = Paint::color(Color::rgba(255, 255, 255, 32));
text_paint.set_font_size(16.0);
text_paint.set_font(&[fonts.regular]);
text_paint.set_text_align(Align::Left);
text_paint.set_text_baseline(Baseline::Middle);
let _ = canvas.fill_text(x + h, y + h * 0.5, title, text_paint);
let mut text_paint = Paint::color(Color::rgba(255, 255, 255, 32));
text_paint.set_font_size((h * 1.3).round());
text_paint.set_font(&[fonts.icons]);
text_paint.set_text_align(Align::Center);
text_paint.set_text_baseline(Baseline::Middle);
let _ = canvas.fill_text(x + w - h * 0.55, y + h * 0.45, "\u{2716}", text_paint);
}
fn draw_drop_down<T: Renderer>(
canvas: &mut Canvas<T>,
fonts: &Fonts,
title: &str,
x: f32,
y: f32,
w: f32,
h: f32,
) {
let corner_radius = 4.0;
let bg = Paint::linear_gradient(
x,
y,
x,
y + h,
Color::rgba(255, 255, 255, 16),
Color::rgba(0, 0, 0, 16),
);
let mut path = Path::new();
path.rounded_rect(x + 1.0, y + 1.0, w - 2.0, h - 2.0, corner_radius);
canvas.fill_path(&mut path, bg);
let mut path = Path::new();
path.rounded_rect(x + 0.5, y + 0.5, w - 1.0, h - 1.0, corner_radius - 0.5);
canvas.stroke_path(&mut path, Paint::color(Color::rgba(0, 0, 0, 48)));
let mut text_paint = Paint::color(Color::rgba(255, 255, 255, 160));
text_paint.set_font_size(16.0);
text_paint.set_font(&[fonts.regular]);
text_paint.set_text_align(Align::Left);
text_paint.set_text_baseline(Baseline::Middle);
let _ = canvas.fill_text(x + h * 0.3, y + h * 0.5, title, text_paint);
let mut text_paint = Paint::color(Color::rgba(255, 255, 255, 64));
text_paint.set_font_size((h * 1.3).round());
text_paint.set_font(&[fonts.icons]);
text_paint.set_text_align(Align::Center);
text_paint.set_text_baseline(Baseline::Middle);
let _ = canvas.fill_text(x + w - h * 0.5, y + h * 0.45, "\u{E75E}", text_paint);
}
fn draw_label<T: Renderer>(
canvas: &mut Canvas<T>,
fonts: &Fonts,
title: &str,
x: f32,
y: f32,
_w: f32,
h: f32,
) {
let mut text_paint = Paint::color(Color::rgba(255, 255, 255, 128));
text_paint.set_font_size(14.0);
text_paint.set_font(&[fonts.regular]);
text_paint.set_text_align(Align::Left);
text_paint.set_text_baseline(Baseline::Middle);
let _ = canvas.fill_text(x, y + h * 0.5, title, text_paint);
}
fn draw_edit_box_base<T: Renderer>(canvas: &mut Canvas<T>, x: f32, y: f32, w: f32, h: f32) {
let paint = Paint::box_gradient(
x + 1.0,
y + 2.5,
w - 2.0,
h - 2.0,
3.0,
4.0,
Color::rgba(255, 255, 255, 32),
Color::rgba(32, 32, 32, 32),
);
let mut path = Path::new();
path.rounded_rect(x + 1.0, y + 1.0, w - 2.0, h - 2.0, 3.0);
canvas.fill_path(&mut path, paint);
let mut path = Path::new();
path.rounded_rect(x + 0.5, y + 0.5, w - 1.0, h - 1.0, 3.5);
canvas.stroke_path(&mut path, Paint::color(Color::rgba(0, 0, 0, 48)));
}
fn draw_edit_box<T: Renderer>(
canvas: &mut Canvas<T>,
fonts: &Fonts,
title: &str,
x: f32,
y: f32,
w: f32,
h: f32,
) {
draw_edit_box_base(canvas, x, y, w, h);
let mut text_paint = Paint::color(Color::rgba(255, 255, 255, 64));
text_paint.set_font_size(16.0);
text_paint.set_font(&[fonts.regular]);
text_paint.set_text_align(Align::Left);
text_paint.set_text_baseline(Baseline::Middle);
let _ = canvas.fill_text(x + h * 0.5, y + h * 0.5, title, text_paint);
}
fn draw_edit_box_num<T: Renderer>(
canvas: &mut Canvas<T>,
fonts: &Fonts,
title: &str,
units: &str,
x: f32,
y: f32,
w: f32,
h: f32,
) {
draw_edit_box_base(canvas, x, y, w, h);
let mut paint = Paint::color(Color::rgba(255, 255, 255, 64));
paint.set_font_size(14.0);
paint.set_font(&[fonts.regular]);
paint.set_text_align(Align::Right);
paint.set_text_baseline(Baseline::Middle);
if let Ok(layout) = canvas.measure_text(0.0, 0.0, units, paint) {
let _ = canvas.fill_text(x + w - h * 0.3, y + h * 0.5, units, paint);
paint.set_font_size(16.0);
paint.set_color(Color::rgba(255, 255, 255, 128));
let _ = canvas.fill_text(x + w - layout.width() - h * 0.5, y + h * 0.5, title, paint);
}
}
fn draw_check_box<T: Renderer>(
canvas: &mut Canvas<T>,
fonts: &Fonts,
text: &str,
x: f32,
y: f32,
_w: f32,
h: f32,
) {
let mut paint = Paint::color(Color::rgba(255, 255, 255, 160));
paint.set_font_size(14.0);
paint.set_font(&[fonts.regular]);
paint.set_text_baseline(Baseline::Middle);
let _ = canvas.fill_text(x + 28.0, y + h * 0.5, text, paint);
paint = Paint::box_gradient(
x + 1.0,
y + (h * 0.5).floor() - 9.0 + 1.0,
18.0,
18.0,
3.0,
3.0,
Color::rgba(0, 0, 0, 32),
Color::rgba(0, 0, 0, 92),
);
let mut path = Path::new();
path.rounded_rect(x + 1.0, y + (h * 0.5).floor() - 9.0, 18.0, 18.0, 3.0);
canvas.fill_path(&mut path, paint);
paint = Paint::color(Color::rgba(255, 255, 255, 128));
paint.set_font_size(36.0);
paint.set_font(&[fonts.icons]);
paint.set_text_align(Align::Center);
paint.set_text_baseline(Baseline::Middle);
let _ = canvas.fill_text(x + 9.0 + 2.0, y + h * 0.5, "\u{2713}", paint);
}
fn draw_button<T: Renderer>(
canvas: &mut Canvas<T>,
fonts: &Fonts,
preicon: Option<&str>,
text: &str,
x: f32,
y: f32,
w: f32,
h: f32,
color: Color,
) {
let corner_radius = 4.0;
let a = if color.is_black() { 16 } else { 32 };
let bg = Paint::linear_gradient(
x,
y,
x,
y + h,
Color::rgba(255, 255, 255, a),
Color::rgba(0, 0, 0, a),
);
let mut path = Path::new();
path.rounded_rect(x + 1.0, y + 1.0, w - 2.0, h - 2.0, corner_radius - 1.0);
if !color.is_black() {
canvas.fill_path(&mut path, Paint::color(color));
}
canvas.fill_path(&mut path, bg);
let mut path = Path::new();
path.rounded_rect(x + 0.5, y + 0.5, w - 1.0, h - 1.0, corner_radius - 0.5);
canvas.stroke_path(&mut path, Paint::color(Color::rgba(0, 0, 0, 48)));
let mut paint = Paint::color(Color::rgba(255, 255, 255, 96));
paint.set_font_size(15.0);
paint.set_font(&[fonts.bold]);
paint.set_text_align(Align::Left);
paint.set_text_baseline(Baseline::Middle);
let tw = if let Ok(layout) = canvas.measure_text(0.0, 0.0, text, paint) {
layout.width()
} else {
0.0
};
let mut iw = 0.0;
if let Some(icon) = preicon {
paint.set_font(&[fonts.icons]);
paint.set_font_size(h * 1.3);
if let Ok(layout) = canvas.measure_text(0.0, 0.0, icon, paint) {
iw = layout.width() + (h * 0.15);
}
let _ = canvas.fill_text(x + w * 0.5 - tw * 0.5 - iw * 0.75, y + h * 0.5, icon, paint);
}
paint.set_font_size(15.0);
paint.set_font(&[fonts.regular]);
paint.set_color(Color::rgba(0, 0, 0, 160));
let _ = canvas.fill_text(
x + w * 0.5 - tw * 0.5 + iw * 0.25,
y + h * 0.5 - 1.0,
text,
paint,
);
paint.set_color(Color::rgba(255, 255, 255, 160));
let _ = canvas.fill_text(x + w * 0.5 - tw * 0.5 + iw * 0.25, y + h * 0.5, text, paint);
}
struct PerfGraph {
history_count: usize,
values: Vec<f32>,
head: usize,
}
impl PerfGraph {
fn new() -> Self {
Self {
history_count: 100,
values: vec![0.0; 100],
head: Default::default(),
}
}
fn update(&mut self, frame_time: f32) {
self.head = (self.head + 1) % self.history_count;
self.values[self.head] = frame_time;
}
fn get_average(&self) -> f32 {
self.values.iter().map(|v| *v).sum::<f32>() / self.history_count as f32
}
fn render<T: Renderer>(&self, canvas: &mut Canvas<T>, x: f32, y: f32) {
let avg = self.get_average();
let w = 200.0;
let h = 35.0;
let mut path = Path::new();
path.rect(x, y, w, h);
canvas.fill_path(&mut path, Paint::color(Color::rgba(0, 0, 0, 128)));
let mut path = Path::new();
path.move_to(x, y + h);
for i in 0..self.history_count {
let mut v = 1.0 / (0.00001 + self.values[(self.head + i) % self.history_count]);
if v > 80.0 {
v = 80.0;
}
let vx = x + (i as f32 / (self.history_count - 1) as f32) * w;
let vy = y + h - ((v / 80.0) * h);
path.line_to(vx, vy);
}
path.line_to(x + w, y + h);
canvas.fill_path(&mut path, Paint::color(Color::rgba(255, 192, 0, 128)));
let mut text_paint = Paint::color(Color::rgba(240, 240, 240, 255));
text_paint.set_font_size(12.0);
let _ = canvas.fill_text(x + 5.0, y + 13.0, "Frame time", text_paint);
let mut text_paint = Paint::color(Color::rgba(240, 240, 240, 255));
text_paint.set_font_size(14.0);
text_paint.set_text_align(Align::Right);
text_paint.set_text_baseline(Baseline::Top);
let _ = canvas.fill_text(x + w - 5.0, y, &format!("{:.2} FPS", 1.0 / avg), text_paint);
let mut text_paint = Paint::color(Color::rgba(240, 240, 240, 200));
text_paint.set_font_size(12.0);
text_paint.set_text_align(Align::Right);
text_paint.set_text_baseline(Baseline::Alphabetic);
let _ = canvas.fill_text(
x + w - 5.0,
y + h - 5.0,
&format!("{:.2} ms", avg * 1000.0),
text_paint,
);
}
}

503
helix-ui/src/main.rs Normal file
View File

@@ -0,0 +1,503 @@
use parley::{
layout::Alignment,
style::{FontFamily, FontStack, StyleProperty},
FontContext, LayoutContext,
};
use std::borrow::Cow;
use winit::{
event::{Event, WindowEvent},
event_loop::{ControlFlow, EventLoop},
window::Window,
};
use wgpu::util::DeviceExt;
// new femto-like framework:
// wgpu renderer
// kurbo, (alternative is euclid + lyon)
// vector math: glam? and drop euclid (glam is faster https://docs.rs/glam/latest/glam/)
// swash + parley for text
// imgref, bitflags
// fnv, rgb
// resource, image
// usvg for svg
use swash::{
scale::ScaleContext,
shape::ShapeContext,
text::Script,
zeno::{Vector, Verb},
Attributes, CacheKey, Charmap, FontRef,
};
use lyon::{
math::{point, Transform},
path::{builder::*, Path},
tessellation::{BuffersBuilder, FillOptions, FillTessellator, FillVertex, VertexBuffers},
};
use bytemuck::{Pod, Zeroable};
// Vertex for lines drawn by lyon
#[repr(C)]
#[derive(Clone, Copy, Debug, Pod, Zeroable)]
struct Vertex {
position: [f32; 2],
// color: [f32; 4], // Use this when I want more colors
}
#[repr(C)]
#[derive(Clone, Copy, Debug, Pod, Zeroable)]
struct View {
size: [f32; 2],
}
fn font() -> VertexBuffers<Vertex, u16> {
let data = std::fs::read("assets/fonts/ttf/FiraCode-Regular.ttf").unwrap();
// -- Shaping
// let mut context = ShapeContext::new();
// let mut shaper = context
// .builder(font)
// .script(Script::Latin)
// .size(12.)
// .variations(&[("wght", 400.0)])
// .build();
// shaper.add_str("a quick brown fox?");
// add_str with boundary analysis
// use swash::text::{analyze, Script};
// use swash::text::cluster::{CharInfo, Parser, Token};
// let text = "a quick brown fox?";
// let mut parser = Parser::new(
// Script::Latin,
// text.char_indices()
// // Call analyze passing the same text and zip
// // the results
// .zip(analyze(text.chars()))
// // Analyze yields the tuple (Properties, Boundary)
// .map(|((i, ch), (props, boundary))| Token {
// ch,
// offset: i as u32,
// len: ch.len_utf8() as u8,
// // Create character information from properties and boundary
// info: CharInfo::new(props, boundary),
// data: 0,
// }),
// );
// shaper.shape_with(|c| {
// // use the glyph cluster
// // c.glyphs
// });
// -- Scaling
// let mut scaler = scale_ctx
// .builder(font)
// .hint(true)
// .size(12.)
// .variations(&[("wght", 400.0)])
// .build();
// let glyph_id = font.charmap().map('H');
// let outline = scaler.scale_outline(glyph_id).unwrap();
// -- Layout
let mut font_ctx = FontContext::new();
let font_family = font_ctx.register_fonts(data).unwrap();
let mut layout_ctx: LayoutContext<[u8; 4]> = LayoutContext::new();
// Encode glyphs into lyon paths
let mut encoder = Path::builder();
let mut encoder = encoder.transformed(Transform::default());
let mut builder = layout_ctx.ranged_builder(&mut font_ctx, "fn draw_edit_box_base<T: Renderer>(canvas: &mut Canvas<T>, x: f32, y: f32, w: f32, h: f32) { ", 1.);
builder.push_default(&StyleProperty::FontStack(FontStack::Single(
FontFamily::Named(&font_family),
)));
builder.push_default(&StyleProperty::FontSize(12.));
builder.push_default(&StyleProperty::Brush([255, 255, 255, 255]));
// builder.push() with range to set styling
let mut layout = builder.build();
let max_width = None;
layout.break_all_lines(max_width, Alignment::Start);
let mut scale_ctx = ScaleContext::new();
for line in layout.lines() {
let mut last_x = 0.0;
let mut last_y = 0.0;
for glyph_run in line.glyph_runs() {
let run = glyph_run.run();
let color = &glyph_run.style().brush;
let font = run.font();
let font = font.as_ref();
let mut first = true;
// TODO: handle .variations(&[("wght", 400.0)])
let mut scaler = scale_ctx.builder(font).size(run.font_size()).build();
for glyph in glyph_run.positioned_glyphs() {
let delta_x = glyph.x - last_x;
let delta_y = glyph.y - last_y;
last_x = glyph.x;
last_y = glyph.y;
if first {
// TODO: handle underline
// TODO: handle strikethrough
}
first = false;
encoder.set_transform(Transform::new(
1.0, 0.0, //
0.0, -1.0, // invert y axis
glyph.x, glyph.y,
));
if let Some(outline) = scaler.scale_outline(glyph.id) {
append_outline(&mut encoder, outline.verbs(), outline.points());
};
}
}
}
// -- Tesselation
let path = encoder.build();
let mut geometry: VertexBuffers<Vertex, u16> = VertexBuffers::new();
let mut tessellator = FillTessellator::new();
{
// Compute the tessellation.
tessellator
.tessellate_path(
&path,
&FillOptions::non_zero().with_tolerance(0.01), // defaults to 0.1, compare further
&mut BuffersBuilder::new(&mut geometry, |vertex: FillVertex| Vertex {
position: vertex.position().to_array(),
}),
)
.unwrap();
}
geometry
}
fn append_outline<T: lyon::path::builder::PathBuilder>(
encoder: &mut T,
verbs: &[Verb],
points: &[Vector],
) {
let mut i = 0;
for verb in verbs {
match verb {
Verb::MoveTo => {
let p = points[i];
// TODO: can MoveTo appear halfway through?
encoder.begin(point(p.x, p.y));
i += 1;
}
Verb::LineTo => {
let p = points[i];
encoder.line_to(point(p.x, p.y));
i += 1;
}
Verb::QuadTo => {
let p1 = points[i];
let p2 = points[i + 1];
encoder.quadratic_bezier_to(point(p1.x, p1.y), point(p2.x, p2.y));
i += 2;
}
Verb::CurveTo => {
let p1 = points[i];
let p2 = points[i + 1];
let p3 = points[i + 2];
encoder.cubic_bezier_to(point(p1.x, p1.y), point(p2.x, p2.y), point(p3.x, p3.y));
i += 3;
}
Verb::Close => {
encoder.close();
}
}
}
}
fn create_multisampled_framebuffer(
device: &wgpu::Device,
config: &wgpu::SurfaceConfiguration,
sample_count: u32,
) -> wgpu::TextureView {
let multisampled_texture_extent = wgpu::Extent3d {
width: config.width,
height: config.height,
depth_or_array_layers: 1,
};
let multisampled_frame_descriptor = &wgpu::TextureDescriptor {
size: multisampled_texture_extent,
mip_level_count: 1,
sample_count,
dimension: wgpu::TextureDimension::D2,
format: config.format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
label: None,
};
device
.create_texture(multisampled_frame_descriptor)
.create_view(&wgpu::TextureViewDescriptor::default())
}
async fn run(event_loop: EventLoop<()>, window: Window) {
let sample_count = 4;
let size = window.inner_size();
let instance = wgpu::Instance::new(wgpu::Backends::all());
let surface = unsafe { instance.create_surface(&window) };
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::default(), // TODO: select based on backend
force_fallback_adapter: false,
// Request an adapter which can render to our surface
compatible_surface: Some(&surface),
})
.await
.expect("Failed to find an appropriate adapter");
// Create the logical device and command queue
let (device, queue) = adapter
.request_device(
&wgpu::DeviceDescriptor {
label: None,
features: wgpu::Features::empty(),
// Make sure we use the texture resolution limits from the adapter, so we can support images the size of the swapchain.
limits: wgpu::Limits::downlevel_webgl2_defaults()
.using_resolution(adapter.limits()),
},
None,
)
.await
.expect("Failed to create device");
// Load the shaders from disk
let shader = device.create_shader_module(&wgpu::ShaderModuleDescriptor {
label: None,
source: wgpu::ShaderSource::Wgsl(Cow::Borrowed(include_str!("shader.wgsl"))),
});
// ---
let geometry = font();
let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Vertex Buffer"),
contents: bytemuck::cast_slice(&geometry.vertices),
usage: wgpu::BufferUsages::VERTEX,
});
let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Index Buffer"),
contents: bytemuck::cast_slice(&geometry.indices),
usage: wgpu::BufferUsages::INDEX,
});
//
// TODO: use size fetched before
let data = View { size: [0.0, 0.0] };
let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("Uniform Buffer"),
contents: bytemuck::cast_slice(&[data]),
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
});
let uniform_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}],
label: Some("uniform_bind_group_layout"),
});
let uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
layout: &uniform_bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: uniform_buffer.as_entire_binding(),
}],
label: Some("uniform_bind_group"),
});
//
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: None,
bind_group_layouts: &[&uniform_bind_group_layout], // &texture_bind_group_layout
push_constant_ranges: &[], // TODO: could use push constants for uniforms but that's not available on web
});
let swapchain_format = surface.get_preferred_format(&adapter).unwrap();
let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: None,
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: "vs_main",
buffers: &[wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &wgpu::vertex_attr_array![0 => Float32x2],
}],
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: "fs_main",
targets: &[swapchain_format.into()],
}),
primitive: wgpu::PrimitiveState::default(),
depth_stencil: None,
multisample: wgpu::MultisampleState {
count: sample_count,
..Default::default()
},
multiview: None,
});
let mut config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format: swapchain_format,
width: size.width,
height: size.height,
present_mode: wgpu::PresentMode::Mailbox,
};
let mut multisampled_framebuffer =
create_multisampled_framebuffer(&device, &config, sample_count);
surface.configure(&device, &config);
//
event_loop.run(move |event, _, control_flow| {
// Have the closure take ownership of the resources.
// `event_loop.run` never returns, therefore we must do this to ensure
// the resources are properly cleaned up.
let _ = (&instance, &adapter, &shader, &pipeline_layout);
*control_flow = ControlFlow::Wait;
match event {
Event::WindowEvent {
event: WindowEvent::Resized(size),
..
} => {
// Reconfigure the surface with the new size
config.width = size.width;
config.height = size.height;
multisampled_framebuffer =
create_multisampled_framebuffer(&device, &config, sample_count);
surface.configure(&device, &config);
// On macos the window needs to be redrawn manually after resizing
window.request_redraw();
}
Event::RedrawRequested(_) => {
let frame = surface
.get_current_texture()
.expect("Failed to acquire next swap chain texture");
let view = frame
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
let mut encoder =
device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
// TODO: need to use queue.write_buffer or staging_belt to write to it
// Pass the current window size in
let dpi_factor = window.scale_factor();
let size = window.inner_size();
let winit::dpi::LogicalSize { width, height } = size.to_logical::<f32>(dpi_factor);
let data = View {
size: [width, height],
};
queue.write_buffer(&uniform_buffer, 0, bytemuck::cast_slice(&[data]));
{
let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: None,
color_attachments: &[wgpu::RenderPassColorAttachment {
view: &multisampled_framebuffer,
resolve_target: Some(&view),
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
store: true,
},
}],
depth_stencil_attachment: None,
});
// rpass.set_viewport();
rpass.set_pipeline(&render_pipeline);
rpass.set_bind_group(0, &uniform_bind_group, &[]);
rpass.set_index_buffer(index_buffer.slice(..), wgpu::IndexFormat::Uint16);
rpass.set_vertex_buffer(0, vertex_buffer.slice(..));
rpass.draw_indexed(0..(geometry.indices.len() as u32), 0, 0..1);
}
queue.submit(Some(encoder.finish()));
frame.present();
}
Event::WindowEvent {
event: WindowEvent::CloseRequested,
..
} => *control_flow = ControlFlow::Exit,
_ => {}
}
});
}
fn main() {
let event_loop = EventLoop::new();
let window = winit::window::Window::new(&event_loop).unwrap();
#[cfg(not(target_arch = "wasm32"))]
{
env_logger::init();
// Temporarily avoid srgb formats for the swapchain on the web
pollster::block_on(run(event_loop, window));
}
#[cfg(target_arch = "wasm32")]
{
std::panic::set_hook(Box::new(console_error_panic_hook::hook));
console_log::init().expect("could not initialize logger");
use winit::platform::web::WindowExtWebSys;
// On wasm, append the canvas to the document body
web_sys::window()
.and_then(|win| win.document())
.and_then(|doc| doc.body())
.and_then(|body| {
body.append_child(&web_sys::Element::from(window.canvas()))
.ok()
})
.expect("couldn't append canvas to document body");
wasm_bindgen_futures::spawn_local(run(event_loop, window));
}
}

28
helix-ui/src/shader.wgsl Normal file
View File

@@ -0,0 +1,28 @@
// struct Vertex {
// [[location(0)]] position: vec2<f32>;
// };
struct View {
size: vec2<f32>;
};
[[group(0), binding(0)]]
var<uniform> view: View;
[[stage(vertex)]]
fn vs_main([[location(0)]] input: vec2<f32>) -> [[builtin(position)]] vec4<f32> {
// TODO: scale by hidpi factor?
return vec4<f32>(
2.0 * input.x / view.size.x - 1.0,
1.0 - 2.0 * input.y / view.size.y,
// input.xy / view.size.xy * 2.0,
0.0, 1.0
);
}
[[stage(fragment)]]
fn fs_main() -> [[location(0)]] vec4<f32> {
return vec4<f32>(1.0, 1.0, 1.0, 1.0);
}

View File

@@ -10,16 +10,26 @@ repository = "https://github.com/helix-editor/helix"
homepage = "https://helix-editor.com" homepage = "https://helix-editor.com"
[features] [features]
default = [] # default = ["dap", "lsp"]
term = ["crossterm"] lsp = ["helix-lsp", "tokio-runtime"]
dap = ["helix-dap", "tokio-stream", "tokio-runtime"]
tokio-runtime = ["tokio"]
term = ["crossterm", "tui"]
ui = []
[dependencies] [dependencies]
bitflags = "1.3" bitflags = "1.3"
anyhow = "1" anyhow = "1"
helix-core = { version = "0.6", path = "../helix-core" } helix-core = { version = "0.6", path = "../helix-core" }
helix-lsp = { version = "0.6", path = "../helix-lsp" } helix-graphics = { version = "0.6", path = "../helix-graphics" }
helix-dap = { version = "0.6", path = "../helix-dap" } helix-lsp = { version = "0.6", path = "../helix-lsp", optional = true }
helix-dap = { version = "0.6", path = "../helix-dap", optional = true }
helix-loader = { version = "0.6", path = "../helix-loader" }
tokio-stream = { version = "0.1", optional = true }
tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"], optional = true }
crossterm = { version = "0.23", optional = true } crossterm = { version = "0.23", optional = true }
tui = { path = "../helix-tui", package = "helix-tui", default-features = false, features = ["crossterm"], optional = true }
# Conversion traits # Conversion traits
once_cell = "1.10" once_cell = "1.10"
@@ -27,8 +37,6 @@ url = "2"
arc-swap = { version = "1.5.0" } arc-swap = { version = "1.5.0" }
tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] }
tokio-stream = "0.1"
futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false } futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
slotmap = "1" slotmap = "1"
@@ -40,10 +48,25 @@ serde_json = "1.0"
toml = "0.5" toml = "0.5"
log = "~0.4" log = "~0.4"
# Command dependencies
# File picker
fuzzy-matcher = "0.3"
ignore = "0.4"
# markdown doc rendering
pulldown-cmark = { version = "0.9", default-features = false }
# file type detection
content_inspector = "0.2.4"
# ripgrep for global search
grep-regex = "0.1.9"
grep-searcher = "0.1.8"
# Remove once retain_mut lands in stable rust
retain_mut = "0.1.7"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
which = "4.2" which = "4.2"
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
clipboard-win = { version = "4.4", features = ["std"] } clipboard-win = { version = "4.4", features = ["std"] }
[dev-dependencies]
helix-tui = { path = "../helix-tui" }

36
helix-view/src/args.rs Normal file
View File

@@ -0,0 +1,36 @@
use helix_core::Position;
use std::path::{Path, PathBuf};
/// Parse arg into [`PathBuf`] and position.
pub fn parse_file(s: &str) -> (PathBuf, Position) {
let def = || (PathBuf::from(s), Position::default());
if Path::new(s).exists() {
return def();
}
split_path_row_col(s)
.or_else(|| split_path_row(s))
.unwrap_or_else(def)
}
/// Split file.rs:10:2 into [`PathBuf`], row and col.
///
/// Does not validate if file.rs is a file or directory.
fn split_path_row_col(s: &str) -> Option<(PathBuf, Position)> {
let mut s = s.rsplitn(3, ':');
let col: usize = s.next()?.parse().ok()?;
let row: usize = s.next()?.parse().ok()?;
let path = s.next()?.into();
let pos = Position::new(row.saturating_sub(1), col.saturating_sub(1));
Some((path, pos))
}
/// Split file.rs:10 into [`PathBuf`] and row.
///
/// Does not validate if file.rs is a file or directory.
fn split_path_row(s: &str) -> Option<(PathBuf, Position)> {
let (path, row) = s.rsplit_once(':')?;
let row: usize = row.parse().ok()?;
let path = path.into();
let pos = Position::new(row.saturating_sub(1), 0);
Some((path, pos))
}

View File

@@ -0,0 +1,93 @@
use crate::input::{
Event, KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind,
};
impl From<crossterm::event::Event> for Event {
fn from(event: crossterm::event::Event) -> Self {
match event {
crossterm::event::Event::Key(key) => Self::Key(key.into()),
crossterm::event::Event::Mouse(mouse) => Self::Mouse(mouse.into()),
crossterm::event::Event::Resize(w, h) => Self::Resize(w, h),
}
}
}
impl From<crossterm::event::MouseEvent> for MouseEvent {
fn from(
crossterm::event::MouseEvent {
kind,
column,
row,
modifiers,
}: crossterm::event::MouseEvent,
) -> Self {
Self {
kind: kind.into(),
column,
row,
modifiers: modifiers.into(),
}
}
}
impl From<crossterm::event::MouseEventKind> for MouseEventKind {
fn from(kind: crossterm::event::MouseEventKind) -> Self {
match kind {
crossterm::event::MouseEventKind::Down(button) => Self::Down(button.into()),
crossterm::event::MouseEventKind::Up(button) => Self::Down(button.into()),
crossterm::event::MouseEventKind::Drag(button) => Self::Drag(button.into()),
crossterm::event::MouseEventKind::Moved => Self::Moved,
crossterm::event::MouseEventKind::ScrollDown => Self::ScrollDown,
crossterm::event::MouseEventKind::ScrollUp => Self::ScrollUp,
}
}
}
#[cfg(feature = "term")]
impl From<crossterm::event::MouseButton> for MouseButton {
fn from(button: crossterm::event::MouseButton) -> Self {
match button {
crossterm::event::MouseButton::Left => MouseButton::Left,
crossterm::event::MouseButton::Right => MouseButton::Right,
crossterm::event::MouseButton::Middle => MouseButton::Middle,
}
}
}
impl From<crossterm::event::KeyEvent> for KeyEvent {
fn from(crossterm::event::KeyEvent { code, modifiers }: crossterm::event::KeyEvent) -> Self {
if code == crossterm::event::KeyCode::BackTab {
// special case for BackTab -> Shift-Tab
let mut modifiers: KeyModifiers = modifiers.into();
modifiers.insert(KeyModifiers::SHIFT);
Self {
code: KeyCode::Tab,
modifiers,
}
} else {
Self {
code: code.into(),
modifiers: modifiers.into(),
}
}
}
}
impl From<KeyEvent> for crossterm::event::KeyEvent {
fn from(KeyEvent { code, modifiers }: KeyEvent) -> Self {
if code == KeyCode::Tab && modifiers.contains(KeyModifiers::SHIFT) {
// special case for Shift-Tab -> BackTab
let mut modifiers = modifiers;
modifiers.remove(KeyModifiers::SHIFT);
crossterm::event::KeyEvent {
code: crossterm::event::KeyCode::BackTab,
modifiers: modifiers.into(),
}
} else {
crossterm::event::KeyEvent {
code: code.into(),
modifiers: modifiers.into(),
}
}
}
}

View File

@@ -14,6 +14,7 @@ pub trait ClipboardProvider: std::fmt::Debug {
fn set_contents(&mut self, contents: String, clipboard_type: ClipboardType) -> Result<()>; fn set_contents(&mut self, contents: String, clipboard_type: ClipboardType) -> Result<()>;
} }
#[cfg(not(target_arch = "wasm32"))]
macro_rules! command_provider { macro_rules! command_provider {
(paste => $get_prg:literal $( , $get_arg:literal )* ; copy => $set_prg:literal $( , $set_arg:literal )* ; ) => {{ (paste => $get_prg:literal $( , $get_arg:literal )* ; copy => $set_prg:literal $( , $set_arg:literal )* ; ) => {{
Box::new(provider::command::Provider { Box::new(provider::command::Provider {
@@ -75,13 +76,13 @@ pub fn get_clipboard_provider() -> Box<dyn ClipboardProvider> {
} }
} }
#[cfg(target_os = "wasm32")] #[cfg(target_arch = "wasm32")]
pub fn get_clipboard_provider() -> Box<dyn ClipboardProvider> { pub fn get_clipboard_provider() -> Box<dyn ClipboardProvider> {
// TODO: // TODO:
Box::new(provider::NopProvider::new()) Box::new(provider::NopProvider::new())
} }
#[cfg(not(any(windows, target_os = "wasm32", target_os = "macos")))] #[cfg(not(any(windows, target_os = "macos", target_arch = "wasm32")))]
pub fn get_clipboard_provider() -> Box<dyn ClipboardProvider> { pub fn get_clipboard_provider() -> Box<dyn ClipboardProvider> {
use provider::command::{env_var_is_set, exists, is_exit_success}; use provider::command::{env_var_is_set, exists, is_exit_success};
// TODO: support for user-defined provider, probably when we have plugin support by setting a // TODO: support for user-defined provider, probably when we have plugin support by setting a

View File

@@ -1,13 +1,13 @@
use super::{Context, Editor}; use super::{Context, Editor};
use crate::editor::Breakpoint;
use crate::ui::{self, overlay::overlayed, FilePicker, Picker, Popup, Prompt, PromptEvent, Text};
use crate::{ use crate::{
compositor::{self, Compositor}, compositor::{self, Compositor},
job::{Callback, Jobs}, job::{Callback, Jobs},
ui::{self, overlay::overlayed, FilePicker, Picker, Popup, Prompt, PromptEvent, Text},
}; };
use helix_core::syntax::{DebugArgumentValue, DebugConfigCompletion}; use helix_core::syntax::{DebugArgumentValue, DebugConfigCompletion};
use helix_dap::{self as dap, Client}; use helix_dap::{self as dap, Client};
use helix_lsp::block_on; use helix_lsp::block_on;
use helix_view::editor::Breakpoint;
use serde_json::{to_value, Value}; use serde_json::{to_value, Value};
use tokio_stream::wrappers::UnboundedReceiverStream; use tokio_stream::wrappers::UnboundedReceiverStream;
@@ -18,7 +18,8 @@ use std::path::PathBuf;
use anyhow::{anyhow, bail}; use anyhow::{anyhow, bail};
use helix_view::handlers::dap::{breakpoints_changed, jump_to_stack_frame, select_thread_id}; use crate::debugger;
use crate::handlers::dap::{breakpoints_changed, jump_to_stack_frame, select_thread_id};
fn thread_picker( fn thread_picker(
cx: &mut Context, cx: &mut Context,
@@ -474,7 +475,7 @@ pub fn dap_variables(cx: &mut Context) {
let text_style = theme.get("ui.text.focus"); let text_style = theme.get("ui.text.focus");
for scope in scopes.iter() { for scope in scopes.iter() {
// use helix_view::graphics::Style; // use crate::graphics::Style;
use tui::text::{Span, Spans}; use tui::text::{Span, Spans};
let response = block_on(debugger.variables(scope.variables_reference)); let response = block_on(debugger.variables(scope.variables_reference));

View File

@@ -7,12 +7,10 @@ use helix_lsp::{
use super::{align_view, push_jump, Align, Context, Editor}; use super::{align_view, push_jump, Align, Context, Editor};
use helix_core::Selection; use helix_core::Selection;
use helix_view::editor::Action; use crate::editor::Action;
use crate::{ use crate::ui::{self, overlay::overlayed, FileLocation, FilePicker, Popup, PromptEvent};
compositor::{self, Compositor}, use crate::compositor::{self, Compositor};
ui::{self, overlay::overlayed, FileLocation, FilePicker, Popup, PromptEvent},
};
use std::borrow::Cow; use std::borrow::Cow;
@@ -257,7 +255,7 @@ pub fn code_action(cx: &mut Context) {
}); });
picker.move_down(); // pre-select the first item picker.move_down(); // pre-select the first item
let popup = Popup::new("code-action", picker).margin(helix_view::graphics::Margin { let popup = Popup::new("code-action", picker).margin(crate::graphics::Margin {
vertical: 1, vertical: 1,
horizontal: 1, horizontal: 1,
}); });

View File

@@ -1,11 +1,25 @@
#[cfg(feature = "dap")]
pub(crate) mod dap; pub(crate) mod dap;
#[cfg(feature = "lsp")]
pub(crate) mod lsp; pub(crate) mod lsp;
pub(crate) mod typed; pub(crate) mod typed;
#[cfg(feature = "dap")]
pub use dap::*; pub use dap::*;
#[cfg(feature = "lsp")]
pub use lsp::*; pub use lsp::*;
pub use typed::*; pub use typed::*;
use crate::{
clipboard::ClipboardType,
document::{Mode, SCRATCH_BUFFER_NAME},
editor::{Action, Motion},
info::Info,
input::KeyEvent,
keyboard::KeyCode,
view::View,
Document, DocumentId, Editor, ViewId,
};
use helix_core::{ use helix_core::{
comment, coords_at_pos, find_first_non_whitespace_char, find_root, graphemes, comment, coords_at_pos, find_first_non_whitespace_char, find_root, graphemes,
history::UndoKind, history::UndoKind,
@@ -25,15 +39,10 @@ use helix_core::{
LineEnding, Position, Range, Rope, RopeGraphemes, RopeSlice, Selection, SmallVec, Tendril, LineEnding, Position, Range, Rope, RopeGraphemes, RopeSlice, Selection, SmallVec, Tendril,
Transaction, Transaction,
}; };
use helix_view::{
clipboard::ClipboardType, use crate::{
document::{Mode, SCRATCH_BUFFER_NAME}, compositor::{self, Component, Compositor},
editor::{Action, Motion}, job::{self, Job, Jobs},
info::Info,
input::KeyEvent,
keyboard::KeyCode,
view::View,
Document, DocumentId, Editor, ViewId,
}; };
use anyhow::{anyhow, bail, ensure, Context as _}; use anyhow::{anyhow, bail, ensure, Context as _};
@@ -43,14 +52,17 @@ use movement::Movement;
use crate::{ use crate::{
args, args,
compositor::{self, Component, Compositor},
ui::{self, overlay::overlayed, FilePicker, Picker, Popup, Prompt, PromptEvent}, ui::{self, overlay::overlayed, FilePicker, Picker, Popup, Prompt, PromptEvent},
}; };
use crate::job::{self, Job, Jobs};
use futures_util::{FutureExt, StreamExt}; use futures_util::{FutureExt, StreamExt};
use std::{collections::HashMap, fmt, future::Future};
use std::{collections::HashSet, num::NonZeroUsize}; use std::{
collections::{HashMap, HashSet},
fmt,
future::Future,
num::NonZeroUsize,
};
use std::{ use std::{
borrow::Cow, borrow::Cow,
@@ -60,11 +72,6 @@ use std::{
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use serde::de::{self, Deserialize, Deserializer}; use serde::de::{self, Deserialize, Deserializer};
use grep_regex::RegexMatcherBuilder;
use grep_searcher::{sinks, BinaryDetection, SearcherBuilder};
use ignore::{DirEntry, WalkBuilder, WalkState};
use tokio_stream::wrappers::UnboundedReceiverStream;
pub struct Context<'a> { pub struct Context<'a> {
pub register: Option<char>, pub register: Option<char>,
pub count: Option<NonZeroUsize>, pub count: Option<NonZeroUsize>,
@@ -91,6 +98,7 @@ impl<'a> Context<'a> {
self.on_next_key_callback = Some(Box::new(on_next_key_callback)); self.on_next_key_callback = Some(Box::new(on_next_key_callback));
} }
#[cfg(feature = "lsp")]
#[inline] #[inline]
pub fn callback<T, F>( pub fn callback<T, F>(
&mut self, &mut self,
@@ -119,7 +127,7 @@ impl<'a> Context<'a> {
} }
} }
use helix_view::{align_view, Align}; use crate::{align_view, Align};
/// A MappableCommand is either a static command like "jump_view_up" or a Typable command like /// A MappableCommand is either a static command like "jump_view_up" or a Typable command like
/// :format. It causes a side-effect on the state (usually by creating and applying a transaction). /// :format. It causes a side-effect on the state (usually by creating and applying a transaction).
@@ -139,8 +147,10 @@ pub enum MappableCommand {
} }
macro_rules! static_commands { macro_rules! static_commands {
( $($name:ident, $doc:literal,)* ) => { ( $($(#[cfg($attr:meta)])? $name:ident, $doc:literal,)* ) => {
$( $(
$(#[cfg($attr)])?
#[allow(non_upper_case_globals)] #[allow(non_upper_case_globals)]
pub const $name: Self = Self::Static { pub const $name: Self = Self::Static {
name: stringify!($name), name: stringify!($name),
@@ -150,7 +160,7 @@ macro_rules! static_commands {
)* )*
pub const STATIC_COMMAND_LIST: &'static [Self] = &[ pub const STATIC_COMMAND_LIST: &'static [Self] = &[
$( Self::$name, )* $( $(#[cfg($attr)])? Self::$name, )*
]; ];
} }
} }
@@ -164,7 +174,6 @@ impl MappableCommand {
let mut cx = compositor::Context { let mut cx = compositor::Context {
editor: cx.editor, editor: cx.editor,
jobs: cx.jobs, jobs: cx.jobs,
scroll: None,
}; };
if let Err(e) = (command.fun)(&mut cx, &args[..], PromptEvent::Validate) { if let Err(e) = (command.fun)(&mut cx, &args[..], PromptEvent::Validate) {
cx.editor.set_error(format!("{}", e)); cx.editor.set_error(format!("{}", e));
@@ -259,9 +268,12 @@ impl MappableCommand {
command_mode, "Enter command mode", command_mode, "Enter command mode",
file_picker, "Open file picker", file_picker, "Open file picker",
file_picker_in_current_directory, "Open file picker at current working directory", file_picker_in_current_directory, "Open file picker at current working directory",
#[cfg(feature = "lsp")]
code_action, "Perform code action", code_action, "Perform code action",
buffer_picker, "Open buffer picker", buffer_picker, "Open buffer picker",
#[cfg(feature = "lsp")]
symbol_picker, "Open symbol picker", symbol_picker, "Open symbol picker",
#[cfg(feature = "lsp")]
workspace_symbol_picker, "Open workspace symbol picker", workspace_symbol_picker, "Open workspace symbol picker",
last_picker, "Open last picker", last_picker, "Open last picker",
prepend_to_line, "Insert at start of line", prepend_to_line, "Insert at start of line",
@@ -271,16 +283,20 @@ impl MappableCommand {
normal_mode, "Enter normal mode", normal_mode, "Enter normal mode",
select_mode, "Enter selection extend mode", select_mode, "Enter selection extend mode",
exit_select_mode, "Exit selection mode", exit_select_mode, "Exit selection mode",
#[cfg(feature = "lsp")]
goto_definition, "Goto definition", goto_definition, "Goto definition",
add_newline_above, "Add newline above", add_newline_above, "Add newline above",
add_newline_below, "Add newline below", add_newline_below, "Add newline below",
#[cfg(feature = "lsp")]
goto_type_definition, "Goto type definition", goto_type_definition, "Goto type definition",
#[cfg(feature = "lsp")]
goto_implementation, "Goto implementation", goto_implementation, "Goto implementation",
goto_file_start, "Goto line number <n> else file start", goto_file_start, "Goto line number <n> else file start",
goto_file_end, "Goto file end", goto_file_end, "Goto file end",
goto_file, "Goto files in selection", goto_file, "Goto files in selection",
goto_file_hsplit, "Goto files in selection (hsplit)", goto_file_hsplit, "Goto files in selection (hsplit)",
goto_file_vsplit, "Goto files in selection (vsplit)", goto_file_vsplit, "Goto files in selection (vsplit)",
#[cfg(feature = "lsp")]
goto_reference, "Goto references", goto_reference, "Goto references",
goto_window_top, "Goto window top", goto_window_top, "Goto window top",
goto_window_center, "Goto window center", goto_window_center, "Goto window center",
@@ -305,6 +321,7 @@ impl MappableCommand {
extend_to_line_start, "Extend to line start", extend_to_line_start, "Extend to line start",
extend_to_line_end, "Extend to line end", extend_to_line_end, "Extend to line end",
extend_to_line_end_newline, "Extend to line end", extend_to_line_end_newline, "Extend to line end",
#[cfg(feature = "lsp")]
signature_help, "Show signature help", signature_help, "Show signature help",
insert_tab, "Insert tab char", insert_tab, "Insert tab char",
insert_newline, "Insert newline char", insert_newline, "Insert newline char",
@@ -342,6 +359,7 @@ impl MappableCommand {
keep_primary_selection, "Keep primary selection", keep_primary_selection, "Keep primary selection",
remove_primary_selection, "Remove primary selection", remove_primary_selection, "Remove primary selection",
completion, "Invoke completion popup", completion, "Invoke completion popup",
#[cfg(feature = "lsp")]
hover, "Show docs for item under cursor", hover, "Show docs for item under cursor",
toggle_comments, "Comment/uncomment selections", toggle_comments, "Comment/uncomment selections",
rotate_selections_forward, "Rotate selections forward", rotate_selections_forward, "Rotate selections forward",
@@ -390,20 +408,35 @@ impl MappableCommand {
goto_prev_comment, "Goto previous comment", goto_prev_comment, "Goto previous comment",
goto_next_paragraph, "Goto next paragraph", goto_next_paragraph, "Goto next paragraph",
goto_prev_paragraph, "Goto previous paragraph", goto_prev_paragraph, "Goto previous paragraph",
#[cfg(feature = "dap")]
dap_launch, "Launch debug target", dap_launch, "Launch debug target",
#[cfg(feature = "dap")]
dap_toggle_breakpoint, "Toggle breakpoint", dap_toggle_breakpoint, "Toggle breakpoint",
#[cfg(feature = "dap")]
dap_continue, "Continue program execution", dap_continue, "Continue program execution",
#[cfg(feature = "dap")]
dap_pause, "Pause program execution", dap_pause, "Pause program execution",
#[cfg(feature = "dap")]
dap_step_in, "Step in", dap_step_in, "Step in",
#[cfg(feature = "dap")]
dap_step_out, "Step out", dap_step_out, "Step out",
#[cfg(feature = "dap")]
dap_next, "Step to next", dap_next, "Step to next",
#[cfg(feature = "dap")]
dap_variables, "List variables", dap_variables, "List variables",
#[cfg(feature = "dap")]
dap_terminate, "End debug session", dap_terminate, "End debug session",
#[cfg(feature = "dap")]
dap_edit_condition, "Edit condition of the breakpoint on the current line", dap_edit_condition, "Edit condition of the breakpoint on the current line",
#[cfg(feature = "dap")]
dap_edit_log, "Edit log message of the breakpoint on the current line", dap_edit_log, "Edit log message of the breakpoint on the current line",
#[cfg(feature = "dap")]
dap_switch_thread, "Switch current thread", dap_switch_thread, "Switch current thread",
#[cfg(feature = "dap")]
dap_switch_stack_frame, "Switch stack frame", dap_switch_stack_frame, "Switch stack frame",
#[cfg(feature = "dap")]
dap_enable_exceptions, "Enable exception breakpoints", dap_enable_exceptions, "Enable exception breakpoints",
#[cfg(feature = "dap")]
dap_disable_exceptions, "Disable exception breakpoints", dap_disable_exceptions, "Disable exception breakpoints",
shell_pipe, "Pipe selections through shell command", shell_pipe, "Pipe selections through shell command",
shell_pipe_to, "Pipe selections into shell command, ignoring command output", shell_pipe_to, "Pipe selections into shell command, ignoring command output",
@@ -411,6 +444,7 @@ impl MappableCommand {
shell_append_output, "Append output of shell command after each selection", shell_append_output, "Append output of shell command after each selection",
shell_keep_pipe, "Filter selections with shell predicate", shell_keep_pipe, "Filter selections with shell predicate",
suspend, "Suspend", suspend, "Suspend",
#[cfg(feature = "lsp")]
rename_symbol, "Rename symbol", rename_symbol, "Rename symbol",
increment, "Increment", increment, "Increment",
decrement, "Decrement", decrement, "Decrement",
@@ -1729,7 +1763,17 @@ fn search_selection(cx: &mut Context) {
cx.editor.set_status(msg); cx.editor.set_status(msg);
} }
#[cfg(not(feature = "term"))]
fn global_search(cx: &mut Context) { fn global_search(cx: &mut Context) {
// TODO
}
#[cfg(feature = "term")]
fn global_search(cx: &mut Context) {
use grep_regex::RegexMatcherBuilder;
use grep_searcher::{sinks, BinaryDetection, SearcherBuilder};
use ignore::{DirEntry, WalkBuilder, WalkState};
let (all_matches_sx, all_matches_rx) = let (all_matches_sx, all_matches_rx) =
tokio::sync::mpsc::unbounded_channel::<(usize, PathBuf)>(); tokio::sync::mpsc::unbounded_channel::<(usize, PathBuf)>();
let config = cx.editor.config(); let config = cx.editor.config();
@@ -1820,6 +1864,8 @@ fn global_search(cx: &mut Context) {
let current_path = doc_mut!(cx.editor).path().cloned(); let current_path = doc_mut!(cx.editor).path().cloned();
let show_picker = async move { let show_picker = async move {
use tokio_stream::wrappers::UnboundedReceiverStream;
let all_matches: Vec<(usize, PathBuf)> = let all_matches: Vec<(usize, PathBuf)> =
UnboundedReceiverStream::new(all_matches_rx).collect().await; UnboundedReceiverStream::new(all_matches_rx).collect().await;
let call: job::Callback = let call: job::Callback =
@@ -2271,14 +2317,14 @@ async fn make_format_callback(
doc_id: DocumentId, doc_id: DocumentId,
doc_version: i32, doc_version: i32,
modified: Modified, modified: Modified,
format: impl Future<Output = helix_lsp::util::LspFormatting> + Send + 'static, format: impl Future<Output = Transaction> + Send + 'static,
) -> anyhow::Result<job::Callback> { ) -> anyhow::Result<job::Callback> {
let format = format.await; let format = format.await;
let call: job::Callback = Box::new(move |editor, _compositor| { let call: job::Callback = Box::new(move |editor, _compositor| {
let view_id = view!(editor).id; let view_id = view!(editor).id;
if let Some(doc) = editor.document_mut(doc_id) { if let Some(doc) = editor.document_mut(doc_id) {
if doc.version() == doc_version { if doc.version() == doc_version {
doc.apply(&Transaction::from(format), view_id); doc.apply(&format, view_id);
doc.append_changes_to_history(view_id); doc.append_changes_to_history(view_id);
if let Modified::SetUnmodified = modified { if let Modified::SetUnmodified = modified {
doc.reset_modified(); doc.reset_modified();
@@ -2651,6 +2697,7 @@ pub mod insert {
super::completion(cx); super::completion(cx);
} }
#[cfg(feature = "lsp")]
fn language_server_completion(cx: &mut Context, ch: char) { fn language_server_completion(cx: &mut Context, ch: char) {
use helix_lsp::lsp; use helix_lsp::lsp;
// if ch matches completion char, trigger completion // if ch matches completion char, trigger completion
@@ -2675,6 +2722,7 @@ pub mod insert {
} }
} }
#[cfg(feature = "lsp")]
fn signature_help(cx: &mut Context, ch: char) { fn signature_help(cx: &mut Context, ch: char) {
use helix_lsp::lsp; use helix_lsp::lsp;
// if ch matches signature_help char, trigger // if ch matches signature_help char, trigger
@@ -2751,6 +2799,7 @@ pub mod insert {
// TODO: need a post insert hook too for certain triggers (autocomplete, signature help, etc) // TODO: need a post insert hook too for certain triggers (autocomplete, signature help, etc)
// this could also generically look at Transaction, but it's a bit annoying to look at // this could also generically look at Transaction, but it's a bit annoying to look at
// Operation instead of Change. // Operation instead of Change.
#[cfg(feature = "lsp")]
for hook in &[language_server_completion, signature_help] { for hook in &[language_server_completion, signature_help] {
// for hook in &[signature_help] { // for hook in &[signature_help] {
hook(cx, c); hook(cx, c);
@@ -3384,6 +3433,10 @@ fn unindent(cx: &mut Context) {
doc.apply(&transaction, view.id); doc.apply(&transaction, view.id);
} }
#[cfg(not(feature = "lsp"))]
fn format_selections(_cx: &mut Context) {}
#[cfg(feature = "lsp")]
fn format_selections(cx: &mut Context) { fn format_selections(cx: &mut Context) {
use helix_lsp::{lsp, util::range_to_lsp_range}; use helix_lsp::{lsp, util::range_to_lsp_range};
@@ -3528,6 +3581,12 @@ fn remove_primary_selection(cx: &mut Context) {
doc.set_selection(view.id, selection); doc.set_selection(view.id, selection);
} }
#[cfg(not(feature = "lsp"))]
pub fn completion(cx: &mut Context) {
// TODO:
}
#[cfg(feature = "lsp")]
pub fn completion(cx: &mut Context) { pub fn completion(cx: &mut Context) {
use helix_lsp::{lsp, util::pos_to_lsp_pos}; use helix_lsp::{lsp, util::pos_to_lsp_pos};
@@ -4357,8 +4416,8 @@ fn shell_prompt(cx: &mut Context, prompt: Cow<'static, str>, behavior: ShellBeha
} }
fn suspend(_cx: &mut Context) { fn suspend(_cx: &mut Context) {
#[cfg(not(windows))] // #[cfg(not(windows))]
signal_hook::low_level::raise(signal_hook::consts::signal::SIGTSTP).unwrap(); // signal_hook::low_level::raise(signal_hook::consts::signal::SIGTSTP).unwrap();
} }
fn add_newline_above(cx: &mut Context) { fn add_newline_above(cx: &mut Context) {
@@ -4517,7 +4576,7 @@ fn record_macro(cx: &mut Context) {
fn replay_macro(cx: &mut Context) { fn replay_macro(cx: &mut Context) {
let reg = cx.register.unwrap_or('@'); let reg = cx.register.unwrap_or('@');
let keys: Vec<KeyEvent> = if let Some([keys_str]) = cx.editor.registers.read(reg) { let keys: Vec<KeyEvent> = if let Some([keys_str]) = cx.editor.registers.read(reg) {
match helix_view::input::parse_macro(keys_str) { match crate::input::parse_macro(keys_str) {
Ok(keys) => keys, Ok(keys) => keys,
Err(err) => { Err(err) => {
cx.editor.set_error(format!("Invalid macro: {}", err)); cx.editor.set_error(format!("Invalid macro: {}", err));
@@ -4533,7 +4592,7 @@ fn replay_macro(cx: &mut Context) {
cx.callback = Some(Box::new(move |compositor, cx| { cx.callback = Some(Box::new(move |compositor, cx| {
for _ in 0..count { for _ in 0..count {
for &key in keys.iter() { for &key in keys.iter() {
compositor.handle_event(crossterm::event::Event::Key(key.into()), cx); compositor.handle_event(compositor::Event::Key(key), cx);
} }
} }
})); }));

View File

@@ -1,6 +1,6 @@
use super::*; use super::*;
use helix_view::editor::{Action, ConfigEvent}; use crate::editor::{Action, ConfigEvent};
use ui::completers::{self, Completer}; use ui::completers::{self, Completer};
#[derive(Clone)] #[derive(Clone)]
@@ -205,23 +205,31 @@ fn write_impl(
if doc.path().is_none() { if doc.path().is_none() {
bail!("cannot write a buffer without a filename"); bail!("cannot write a buffer without a filename");
} }
let fmt = doc.auto_format().map(|fmt| {
let shared = fmt.shared(); #[cfg(feature = "lsp")]
let callback = make_format_callback( let future = {
doc.id(), let fmt = doc.auto_format().map(|fmt| {
doc.version(), let shared = fmt.shared();
Modified::SetUnmodified, let callback = make_format_callback(
shared.clone(), doc.id(),
); doc.version(),
jobs.callback(callback); Modified::SetUnmodified,
shared shared.clone(),
}); );
let future = doc.format_and_save(fmt, force); jobs.callback(callback);
shared
});
doc.format_and_save(fmt, force)
};
#[cfg(not(feature = "lsp"))]
let future = doc.save(force);
cx.jobs.add(Job::new(future).wait_before_exiting()); cx.jobs.add(Job::new(future).wait_before_exiting());
if path.is_some() { if path.is_some() {
let id = doc.id(); let id = doc.id();
doc.detect_language(cx.editor.syn_loader.clone()); doc.detect_language(cx.editor.syn_loader.clone());
#[cfg(feature = "lsp")]
let _ = cx.editor.refresh_language_server(id); let _ = cx.editor.refresh_language_server(id);
} }
Ok(()) Ok(())
@@ -259,6 +267,7 @@ fn format(
_event: PromptEvent, _event: PromptEvent,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let doc = doc!(cx.editor); let doc = doc!(cx.editor);
#[cfg(feature = "lsp")]
if let Some(format) = doc.format() { if let Some(format) = doc.format() {
let callback = let callback =
make_format_callback(doc.id(), doc.version(), Modified::LeaveModified, format); make_format_callback(doc.id(), doc.version(), Modified::LeaveModified, format);
@@ -466,18 +475,24 @@ fn write_all_impl(
continue; continue;
} }
let fmt = doc.auto_format().map(|fmt| { #[cfg(feature = "lsp")]
let shared = fmt.shared(); let future = {
let callback = make_format_callback( let fmt = doc.auto_format().map(|fmt| {
doc.id(), let shared = fmt.shared();
doc.version(), let callback = make_format_callback(
Modified::SetUnmodified, doc.id(),
shared.clone(), doc.version(),
); Modified::SetUnmodified,
jobs.callback(callback); shared.clone(),
shared );
}); jobs.callback(callback);
let future = doc.format_and_save(fmt, force); shared
});
doc.format_and_save(fmt, force)
};
#[cfg(not(feature = "lsp"))]
let future = doc.save(force);
jobs.add(Job::new(future).wait_before_exiting()); jobs.add(Job::new(future).wait_before_exiting());
} }
@@ -847,6 +862,7 @@ fn hsplit_new(
Ok(()) Ok(())
} }
#[cfg(feature = "dap")]
fn debug_eval( fn debug_eval(
cx: &mut compositor::Context, cx: &mut compositor::Context,
args: &[Cow<str>], args: &[Cow<str>],
@@ -869,6 +885,7 @@ fn debug_eval(
Ok(()) Ok(())
} }
#[cfg(feature = "dap")]
fn debug_start( fn debug_start(
cx: &mut compositor::Context, cx: &mut compositor::Context,
args: &[Cow<str>], args: &[Cow<str>],
@@ -882,6 +899,7 @@ fn debug_start(
dap_start_impl(cx, name.as_deref(), None, Some(args)) dap_start_impl(cx, name.as_deref(), None, Some(args))
} }
#[cfg(feature = "dap")]
fn debug_remote( fn debug_remote(
cx: &mut compositor::Context, cx: &mut compositor::Context,
args: &[Cow<str>], args: &[Cow<str>],
@@ -976,6 +994,7 @@ fn set_option(
}; };
let config = serde_json::from_value(config).map_err(field_error)?; let config = serde_json::from_value(config).map_err(field_error)?;
#[cfg(feature = "tokio")]
cx.editor cx.editor
.config_events .config_events
.0 .0
@@ -997,6 +1016,7 @@ fn language(
doc.set_language_by_language_id(&args[0], cx.editor.syn_loader.clone()); doc.set_language_by_language_id(&args[0], cx.editor.syn_loader.clone());
let id = doc.id(); let id = doc.id();
#[cfg(feature = "lsp")]
cx.editor.refresh_language_server(id); cx.editor.refresh_language_server(id);
Ok(()) Ok(())
} }
@@ -1102,6 +1122,7 @@ fn refresh_config(
_args: &[Cow<str>], _args: &[Cow<str>],
_event: PromptEvent, _event: PromptEvent,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
#[cfg(feature = "tokio")]
cx.editor.config_events.0.send(ConfigEvent::Refresh)?; cx.editor.config_events.0.send(ConfigEvent::Refresh)?;
Ok(()) Ok(())
} }
@@ -1435,6 +1456,7 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
fun: tree_sitter_scopes, fun: tree_sitter_scopes,
completer: None, completer: None,
}, },
#[cfg(feature = "dap")]
TypableCommand { TypableCommand {
name: "debug-start", name: "debug-start",
aliases: &["dbg"], aliases: &["dbg"],
@@ -1442,6 +1464,7 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
fun: debug_start, fun: debug_start,
completer: None, completer: None,
}, },
#[cfg(feature = "dap")]
TypableCommand { TypableCommand {
name: "debug-remote", name: "debug-remote",
aliases: &["dbg-tcp"], aliases: &["dbg-tcp"],
@@ -1449,6 +1472,7 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
fun: debug_remote, fun: debug_remote,
completer: None, completer: None,
}, },
#[cfg(feature = "dap")]
TypableCommand { TypableCommand {
name: "debug-eval", name: "debug-eval",
aliases: &[], aliases: &[],

View File

@@ -1,11 +1,8 @@
// Each component declares it's own size constraints and gets fitted based on it's parent. // Each component declares it's own size constraints and gets fitted based on it's parent.
// Q: how does this work with popups? // Q: how does this work with popups?
// cursive does compositor.screen_mut().add_layer_at(pos::absolute(x, y), <component>) // cursive does compositor.screen_mut().add_layer_at(pos::absolute(x, y), <component>)
use crate::graphics::{CursorKind, Rect};
use helix_core::Position; use helix_core::Position;
use helix_view::graphics::{CursorKind, Rect};
use crossterm::event::Event;
use tui::buffer::Buffer as Surface;
pub type Callback = Box<dyn FnOnce(&mut Compositor, &mut Context)>; pub type Callback = Box<dyn FnOnce(&mut Compositor, &mut Context)>;
@@ -15,36 +12,78 @@ pub enum EventResult {
Consumed(Option<Callback>), Consumed(Option<Callback>),
} }
use helix_view::Editor;
use crate::job::Jobs; use crate::job::Jobs;
use crate::Editor;
pub use crate::input::Event;
pub struct Context<'a> { pub struct Context<'a> {
pub editor: &'a mut Editor, pub editor: &'a mut Editor,
pub scroll: Option<usize>,
pub jobs: &'a mut Jobs, pub jobs: &'a mut Jobs,
} }
pub trait Component: Any + AnyComponent { #[cfg(feature = "term")]
pub mod term {
use super::*;
pub use tui::buffer::Buffer as Surface;
pub struct RenderContext<'a> {
pub editor: &'a Editor,
pub surface: &'a mut Surface,
pub scroll: Option<usize>,
}
pub trait Render {
/// Render the component onto the provided surface.
fn render(&mut self, area: Rect, ctx: &mut RenderContext);
// TODO: make required_size be layout() instead and part of this trait?
/// Get cursor position and cursor kind.
fn cursor(&self, _area: Rect, _ctx: &Editor) -> (Option<Position>, CursorKind) {
(None, CursorKind::Hidden)
}
}
}
#[cfg(feature = "ui")]
pub mod ui {
use super::*;
pub type Surface = ();
pub struct RenderContext<'a> {
pub editor: &'a Editor,
// pub surface: &'a mut Surface,
pub scroll: Option<usize>,
}
pub trait Render {
/// Render the component onto the provided surface.
fn render(&mut self, area: Rect, ctx: &mut RenderContext) {
// TODO:
}
// TODO: make required_size be layout() instead and part of this trait?
}
}
#[cfg(feature = "term")]
pub use term::*;
#[cfg(feature = "ui")]
pub use ui::*;
pub trait Component: Any + AnyComponent + Render {
/// Process input events, return true if handled. /// Process input events, return true if handled.
fn handle_event(&mut self, _event: Event, _ctx: &mut Context) -> EventResult { fn handle_event(&mut self, _event: Event, _ctx: &mut Context) -> EventResult {
EventResult::Ignored(None) EventResult::Ignored(None)
} }
// , args: ()
/// Should redraw? Useful for saving redraw cycles if we know component didn't change. /// Should redraw? Useful for saving redraw cycles if we know component didn't change.
fn should_update(&self) -> bool { fn should_update(&self) -> bool {
true true
} }
/// Render the component onto the provided surface.
fn render(&mut self, area: Rect, frame: &mut Surface, ctx: &mut Context);
/// Get cursor position and cursor kind.
fn cursor(&self, _area: Rect, _ctx: &Editor) -> (Option<Position>, CursorKind) {
(None, CursorKind::Hidden)
}
/// May be used by the parent component to compute the child area. /// May be used by the parent component to compute the child area.
/// viewport is the maximum allowed area, and the child should stay within those bounds. /// viewport is the maximum allowed area, and the child should stay within those bounds.
/// ///
@@ -63,52 +102,29 @@ pub trait Component: Any + AnyComponent {
} }
} }
use anyhow::Error;
use std::io::stdout;
use tui::backend::{Backend, CrosstermBackend};
type Terminal = tui::terminal::Terminal<CrosstermBackend<std::io::Stdout>>;
pub struct Compositor { pub struct Compositor {
layers: Vec<Box<dyn Component>>, layers: Vec<Box<dyn Component>>,
terminal: Terminal, area: Rect,
pub(crate) last_picker: Option<Box<dyn Component>>, // TODO: remove pub
pub last_picker: Option<Box<dyn Component>>,
} }
impl Compositor { impl Compositor {
pub fn new() -> Result<Self, Error> { pub fn new(area: Rect) -> Self {
let backend = CrosstermBackend::new(stdout()); Self {
let terminal = Terminal::new(backend)?;
Ok(Self {
layers: Vec::new(), layers: Vec::new(),
terminal, area,
last_picker: None, last_picker: None,
}) }
} }
pub fn size(&self) -> Rect { pub fn size(&self) -> Rect {
self.terminal.size().expect("couldn't get terminal size") self.area
} }
pub fn resize(&mut self, width: u16, height: u16) { pub fn resize(&mut self, area: Rect) {
self.terminal self.area = area;
.resize(Rect::new(0, 0, width, height))
.expect("Unable to resize terminal")
}
pub fn save_cursor(&mut self) {
if self.terminal.cursor_kind() == CursorKind::Hidden {
self.terminal
.backend_mut()
.show_cursor(CursorKind::Block)
.ok();
}
}
pub fn load_cursor(&mut self) {
if self.terminal.cursor_kind() == CursorKind::Hidden {
self.terminal.backend_mut().hide_cursor().ok();
}
} }
pub fn push(&mut self, mut layer: Box<dyn Component>) { pub fn push(&mut self, mut layer: Box<dyn Component>) {
@@ -135,7 +151,7 @@ impl Compositor {
pub fn handle_event(&mut self, event: Event, cx: &mut Context) -> bool { pub fn handle_event(&mut self, event: Event, cx: &mut Context) -> bool {
// If it is a key event and a macro is being recorded, push the key event to the recording. // If it is a key event and a macro is being recorded, push the key event to the recording.
if let (Event::Key(key), Some((_, keys))) = (event, &mut cx.editor.macro_recording) { if let (Event::Key(key), Some((_, keys))) = (event, &mut cx.editor.macro_recording) {
keys.push(key.into()); keys.push(key);
} }
let mut callbacks = Vec::new(); let mut callbacks = Vec::new();
@@ -168,27 +184,13 @@ impl Compositor {
consumed consumed
} }
pub fn render(&mut self, cx: &mut Context) { pub fn render(&mut self, area: Rect, cx: &mut RenderContext) {
self.terminal
.autoresize()
.expect("Unable to determine terminal size");
// TODO: need to recalculate view tree if necessary
let surface = self.terminal.current_buffer_mut();
let area = *surface.area();
for layer in &mut self.layers { for layer in &mut self.layers {
layer.render(area, surface, cx); layer.render(area, cx);
} }
let (pos, kind) = self.cursor(area, cx.editor);
let pos = pos.map(|pos| (pos.col as u16, pos.row as u16));
self.terminal.draw(pos, kind).unwrap();
} }
#[cfg(feature = "term")]
pub fn cursor(&self, area: Rect, editor: &Editor) -> (Option<Position>, CursorKind) { pub fn cursor(&self, area: Rect, editor: &Editor) -> (Option<Position>, CursorKind) {
for layer in self.layers.iter().rev() { for layer in self.layers.iter().rev() {
if let (Some(pos), kind) = layer.cursor(area, editor) { if let (Some(pos), kind) = layer.cursor(area, editor) {

View File

@@ -19,7 +19,9 @@ use helix_core::{
ChangeSet, Diagnostic, LineEnding, Rope, RopeBuilder, Selection, State, Syntax, Transaction, ChangeSet, Diagnostic, LineEnding, Rope, RopeBuilder, Selection, State, Syntax, Transaction,
DEFAULT_LINE_ENDING, DEFAULT_LINE_ENDING,
}; };
use helix_lsp::util::LspFormatting;
#[cfg(feature = "lsp")]
use helix_lsp::lsp;
use crate::{DocumentId, Editor, ViewId}; use crate::{DocumentId, Editor, ViewId};
@@ -119,6 +121,7 @@ pub struct Document {
pub(crate) modified_since_accessed: bool, pub(crate) modified_since_accessed: bool,
diagnostics: Vec<Diagnostic>, diagnostics: Vec<Diagnostic>,
#[cfg(feature = "lsp")]
language_server: Option<Arc<helix_lsp::Client>>, language_server: Option<Arc<helix_lsp::Client>>,
} }
@@ -142,7 +145,6 @@ impl fmt::Debug for Document {
.field("version", &self.version) .field("version", &self.version)
.field("modified_since_accessed", &self.modified_since_accessed) .field("modified_since_accessed", &self.modified_since_accessed)
.field("diagnostics", &self.diagnostics) .field("diagnostics", &self.diagnostics)
// .field("language_server", &self.language_server)
.finish() .finish()
} }
} }
@@ -255,17 +257,19 @@ pub fn from_reader<R: std::io::Read + ?Sized>(
Ok((rope, encoding)) Ok((rope, encoding))
} }
#[cfg(feature = "tokio-runtime")]
// The documentation and implementation of this function should be up-to-date with // The documentation and implementation of this function should be up-to-date with
// its sibling function, `from_reader()`. // its sibling function, `from_reader()`.
// //
/// Encodes the text inside `rope` into the given `encoding` and writes the /// Encodes the text inside `rope` into the given `encoding` and writes the
/// encoded output into `writer.` As a `Rope` can only contain valid UTF-8, /// encoded output into `writer.` As a `Rope` can only contain valid UTF-8,
/// replacement characters may appear in the encoded text. /// replacement characters may appear in the encoded text.
pub async fn to_writer<'a, W: tokio::io::AsyncWriteExt + Unpin + ?Sized>( pub async fn to_writer<'a, W: tokio::io::AsyncWrite + Unpin + ?Sized>(
writer: &'a mut W, writer: &'a mut W,
encoding: &'static encoding::Encoding, encoding: &'static encoding::Encoding,
rope: &'a Rope, rope: &'a Rope,
) -> Result<(), Error> { ) -> Result<(), Error> {
use tokio::io::AsyncWriteExt;
// Text inside a `Rope` is stored as non-contiguous blocks of data called // Text inside a `Rope` is stored as non-contiguous blocks of data called
// chunks. The absolute size of each chunk is unknown, thus it is impossible // chunks. The absolute size of each chunk is unknown, thus it is impossible
// to predict the end of the chunk iterator ahead of time. Instead, it is // to predict the end of the chunk iterator ahead of time. Instead, it is
@@ -330,7 +334,6 @@ where
*mut_ref = f(mem::take(mut_ref)); *mut_ref = f(mem::take(mut_ref));
} }
use helix_lsp::lsp;
use url::Url; use url::Url;
impl Document { impl Document {
@@ -359,6 +362,7 @@ impl Document {
savepoint: None, savepoint: None,
last_saved_revision: 0, last_saved_revision: 0,
modified_since_accessed: false, modified_since_accessed: false,
#[cfg(feature = "lsp")]
language_server: None, language_server: None,
} }
} }
@@ -394,9 +398,10 @@ impl Document {
Ok(doc) Ok(doc)
} }
#[cfg(feature = "lsp")]
/// The same as [`format`], but only returns formatting changes if auto-formatting /// The same as [`format`], but only returns formatting changes if auto-formatting
/// is configured. /// is configured.
pub fn auto_format(&self) -> Option<impl Future<Output = LspFormatting> + 'static> { pub fn auto_format(&self) -> Option<impl Future<Output = Transaction> + 'static> {
if self.language_config()?.auto_format { if self.language_config()?.auto_format {
self.format() self.format()
} else { } else {
@@ -404,9 +409,12 @@ impl Document {
} }
} }
#[cfg(feature = "lsp")]
/// If supported, returns the changes that should be applied to this document in order /// If supported, returns the changes that should be applied to this document in order
/// to format it nicely. /// to format it nicely.
pub fn format(&self) -> Option<impl Future<Output = LspFormatting> + 'static> { pub fn format(&self) -> Option<impl Future<Output = Transaction> + 'static> {
use helix_lsp::util::generate_transaction_from_edits;
let language_server = self.language_server()?; let language_server = self.language_server()?;
let text = self.text.clone(); let text = self.text.clone();
let offset_encoding = language_server.offset_encoding(); let offset_encoding = language_server.offset_encoding();
@@ -425,11 +433,7 @@ impl Document {
log::warn!("LSP formatting failed: {}", e); log::warn!("LSP formatting failed: {}", e);
Default::default() Default::default()
}); });
LspFormatting { generate_transaction_from_edits(&text, edits, offset_encoding)
doc: text,
edits,
offset_encoding,
}
}; };
Some(fut) Some(fut)
} }
@@ -438,9 +442,10 @@ impl Document {
self.save_impl::<futures_util::future::Ready<_>>(None, force) self.save_impl::<futures_util::future::Ready<_>>(None, force)
} }
#[cfg(feature = "lsp")]
pub fn format_and_save( pub fn format_and_save(
&mut self, &mut self,
formatting: Option<impl Future<Output = LspFormatting>>, formatting: Option<impl Future<Output = Transaction>>,
force: bool, force: bool,
) -> impl Future<Output = anyhow::Result<()>> { ) -> impl Future<Output = anyhow::Result<()>> {
self.save_impl(formatting, force) self.save_impl(formatting, force)
@@ -452,7 +457,7 @@ impl Document {
/// at its `path()`. /// at its `path()`.
/// ///
/// If `formatting` is present, it supplies some changes that we apply to the text before saving. /// If `formatting` is present, it supplies some changes that we apply to the text before saving.
fn save_impl<F: Future<Output = LspFormatting>>( fn save_impl<F: Future<Output = Transaction>>(
&mut self, &mut self,
formatting: Option<F>, formatting: Option<F>,
force: bool, force: bool,
@@ -462,18 +467,20 @@ impl Document {
let mut text = self.text().clone(); let mut text = self.text().clone();
let path = self.path.clone().expect("Can't save with no path set!"); let path = self.path.clone().expect("Can't save with no path set!");
let identifier = self.identifier();
#[cfg(feature = "lsp")]
let identifier = self.identifier();
#[cfg(feature = "lsp")]
let language_server = self.language_server.clone(); let language_server = self.language_server.clone();
// mark changes up to now as saved // mark changes up to now as saved
self.reset_modified(); self.reset_modified();
#[cfg(feature = "tokio-runtime")]
let encoding = self.encoding; let encoding = self.encoding;
// We encode the file according to the `Document`'s encoding. // We encode the file according to the `Document`'s encoding.
async move { async move {
use tokio::fs::File;
if let Some(parent) = path.parent() { if let Some(parent) = path.parent() {
// TODO: display a prompt asking the user if the directories should be created // TODO: display a prompt asking the user if the directories should be created
if !parent.exists() { if !parent.exists() {
@@ -486,7 +493,8 @@ impl Document {
} }
if let Some(fmt) = formatting { if let Some(fmt) = formatting {
let success = Transaction::from(fmt.await).changes().apply(&mut text); let transaction = fmt.await;
let success = transaction.changes().apply(&mut text);
if !success { if !success {
// This shouldn't happen, because the transaction changes were generated // This shouldn't happen, because the transaction changes were generated
// from the same text we're saving. // from the same text we're saving.
@@ -494,9 +502,12 @@ impl Document {
} }
} }
let mut file = File::create(path).await?; #[cfg(feature = "tokio-runtime")]
let mut file = tokio::fs::File::create(path).await?;
#[cfg(feature = "tokio-runtime")]
to_writer(&mut file, encoding, &text).await?; to_writer(&mut file, encoding, &text).await?;
#[cfg(feature = "lsp")]
if let Some(language_server) = language_server { if let Some(language_server) = language_server {
if !language_server.is_initialized() { if !language_server.is_initialized() {
return Ok(()); return Ok(());
@@ -624,6 +635,7 @@ impl Document {
self.set_language(language_config, Some(config_loader)); self.set_language(language_config, Some(config_loader));
} }
#[cfg(feature = "lsp")]
/// Set the LSP. /// Set the LSP.
pub fn set_language_server(&mut self, language_server: Option<Arc<helix_lsp::Client>>) { pub fn set_language_server(&mut self, language_server: Option<Arc<helix_lsp::Client>>) {
self.language_server = language_server; self.language_server = language_server;
@@ -692,6 +704,7 @@ impl Document {
} }
// emit lsp notification // emit lsp notification
#[cfg(feature = "lsp")]
if let Some(language_server) = self.language_server() { if let Some(language_server) = self.language_server() {
let notify = language_server.text_document_did_change( let notify = language_server.text_document_did_change(
self.versioned_identifier(), self.versioned_identifier(),
@@ -869,6 +882,7 @@ impl Document {
self.version self.version
} }
#[cfg(feature = "lsp")]
/// Language server if it has been initialized. /// Language server if it has been initialized.
pub fn language_server(&self) -> Option<&helix_lsp::Client> { pub fn language_server(&self) -> Option<&helix_lsp::Client> {
let server = self.language_server.as_deref()?; let server = self.language_server.as_deref()?;
@@ -907,6 +921,7 @@ impl Document {
} }
/// File path as a URL. /// File path as a URL.
#[cfg(feature = "lsp")]
pub fn url(&self) -> Option<Url> { pub fn url(&self) -> Option<Url> {
Url::from_file_path(self.path()?).ok() Url::from_file_path(self.path()?).ok()
} }
@@ -935,15 +950,18 @@ impl Document {
// -- LSP methods // -- LSP methods
#[cfg(feature = "lsp")]
#[inline] #[inline]
pub fn identifier(&self) -> lsp::TextDocumentIdentifier { pub fn identifier(&self) -> lsp::TextDocumentIdentifier {
lsp::TextDocumentIdentifier::new(self.url().unwrap()) lsp::TextDocumentIdentifier::new(self.url().unwrap())
} }
#[cfg(feature = "lsp")]
pub fn versioned_identifier(&self) -> lsp::VersionedTextDocumentIdentifier { pub fn versioned_identifier(&self) -> lsp::VersionedTextDocumentIdentifier {
lsp::VersionedTextDocumentIdentifier::new(self.url().unwrap(), self.version) lsp::VersionedTextDocumentIdentifier::new(self.url().unwrap(), self.version)
} }
#[cfg(feature = "lsp")]
pub fn position( pub fn position(
&self, &self,
view_id: ViewId, view_id: ViewId,
@@ -1003,6 +1021,7 @@ impl Default for Document {
mod test { mod test {
use super::*; use super::*;
#[cfg(feature = "lsp")]
#[test] #[test]
fn changeset_to_changes_ignore_line_endings() { fn changeset_to_changes_ignore_line_endings() {
use helix_lsp::{lsp, Client, OffsetEncoding}; use helix_lsp::{lsp, Client, OffsetEncoding};
@@ -1037,6 +1056,7 @@ mod test {
); );
} }
#[cfg(feature = "lsp")]
#[test] #[test]
fn changeset_to_changes() { fn changeset_to_changes() {
use helix_lsp::{lsp, Client, OffsetEncoding}; use helix_lsp::{lsp, Client, OffsetEncoding};

View File

@@ -9,25 +9,25 @@ use crate::{
Document, DocumentId, View, ViewId, Document, DocumentId, View, ViewId,
}; };
use futures_util::future;
use futures_util::stream::select_all::SelectAll;
use tokio_stream::wrappers::UnboundedReceiverStream;
use std::{ use std::{
borrow::Cow, borrow::Cow,
collections::{BTreeMap, HashMap}, collections::{BTreeMap, HashMap},
io::stdin, io::stdin,
num::NonZeroUsize, num::NonZeroUsize,
path::{Path, PathBuf}, path::{Path, PathBuf},
pin::Pin,
sync::Arc, sync::Arc,
}; };
#[cfg(feature = "tokio-runtime")]
use std::pin::Pin;
#[cfg(feature = "tokio-runtime")]
use tokio::{ use tokio::{
sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}, sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender},
time::{sleep, Duration, Instant, Sleep}, time::{sleep, Instant, Sleep},
}; };
use std::time::Duration;
use anyhow::{bail, Error}; use anyhow::{bail, Error};
pub use helix_core::diagnostic::Severity; pub use helix_core::diagnostic::Severity;
@@ -38,6 +38,16 @@ use helix_core::{
Change, Change,
}; };
use helix_core::{Position, Selection}; use helix_core::{Position, Selection};
#[cfg(feature = "lsp")]
use futures_util::future;
#[cfg(feature = "dap")]
use futures_util::stream::select_all::SelectAll;
#[cfg(feature = "dap")]
use tokio_stream::wrappers::UnboundedReceiverStream;
#[cfg(feature = "dap")]
use helix_dap as dap; use helix_dap as dap;
use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer}; use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer};
@@ -431,10 +441,15 @@ pub struct Editor {
pub registers: Registers, pub registers: Registers,
pub macro_recording: Option<(char, Vec<KeyEvent>)>, pub macro_recording: Option<(char, Vec<KeyEvent>)>,
pub theme: Theme, pub theme: Theme,
#[cfg(feature = "lsp")]
pub language_servers: helix_lsp::Registry, pub language_servers: helix_lsp::Registry,
#[cfg(feature = "dap")]
pub debugger: Option<dap::Client>, pub debugger: Option<dap::Client>,
#[cfg(feature = "dap")]
pub debugger_events: SelectAll<UnboundedReceiverStream<dap::Payload>>, pub debugger_events: SelectAll<UnboundedReceiverStream<dap::Payload>>,
#[cfg(not(feature = "dap"))]
pub debugger_events: futures_util::stream::Empty<()>,
pub breakpoints: HashMap<PathBuf, Vec<Breakpoint>>, pub breakpoints: HashMap<PathBuf, Vec<Breakpoint>>,
pub clipboard_provider: Box<dyn ClipboardProvider>, pub clipboard_provider: Box<dyn ClipboardProvider>,
@@ -448,6 +463,7 @@ pub struct Editor {
pub config: Box<dyn DynAccess<Config>>, pub config: Box<dyn DynAccess<Config>>,
pub auto_pairs: Option<AutoPairs>, pub auto_pairs: Option<AutoPairs>,
#[cfg(feature = "tokio-runtime")]
pub idle_timer: Pin<Box<Sleep>>, pub idle_timer: Pin<Box<Sleep>>,
pub last_motion: Option<Motion>, pub last_motion: Option<Motion>,
pub pseudo_pending: Option<String>, pub pseudo_pending: Option<String>,
@@ -456,6 +472,8 @@ pub struct Editor {
pub exit_code: i32, pub exit_code: i32,
// TODO: do this via a signal flag instead
#[cfg(feature = "tokio-runtime")]
pub config_events: (UnboundedSender<ConfigEvent>, UnboundedReceiver<ConfigEvent>), pub config_events: (UnboundedSender<ConfigEvent>, UnboundedReceiver<ConfigEvent>),
} }
@@ -486,7 +504,6 @@ impl Editor {
syn_loader: Arc<syntax::Loader>, syn_loader: Arc<syntax::Loader>,
config: Box<dyn DynAccess<Config>>, config: Box<dyn DynAccess<Config>>,
) -> Self { ) -> Self {
let language_servers = helix_lsp::Registry::new();
let conf = config.load(); let conf = config.load();
let auto_pairs = (&conf.auto_pairs).into(); let auto_pairs = (&conf.auto_pairs).into();
@@ -501,9 +518,14 @@ impl Editor {
selected_register: None, selected_register: None,
macro_recording: None, macro_recording: None,
theme: theme_loader.default(), theme: theme_loader.default(),
language_servers, #[cfg(feature = "lsp")]
language_servers: helix_lsp::Registry::new(),
#[cfg(feature = "dap")]
debugger: None, debugger: None,
#[cfg(feature = "dap")]
debugger_events: SelectAll::new(), debugger_events: SelectAll::new(),
#[cfg(not(feature = "dap"))]
debugger_events: futures_util::stream::empty(),
breakpoints: HashMap::new(), breakpoints: HashMap::new(),
syn_loader, syn_loader,
theme_loader, theme_loader,
@@ -511,6 +533,7 @@ impl Editor {
clipboard_provider: get_clipboard_provider(), clipboard_provider: get_clipboard_provider(),
status_msg: None, status_msg: None,
autoinfo: None, autoinfo: None,
#[cfg(feature = "tokio-runtime")]
idle_timer: Box::pin(sleep(conf.idle_timeout)), idle_timer: Box::pin(sleep(conf.idle_timeout)),
last_motion: None, last_motion: None,
last_completion: None, last_completion: None,
@@ -518,6 +541,7 @@ impl Editor {
config, config,
auto_pairs, auto_pairs,
exit_code: 0, exit_code: 0,
#[cfg(feature = "tokio-runtime")]
config_events: unbounded_channel(), config_events: unbounded_channel(),
} }
} }
@@ -526,6 +550,7 @@ impl Editor {
self.config.load() self.config.load()
} }
#[cfg(feature = "tokio-runtime")]
pub fn clear_idle_timer(&mut self) { pub fn clear_idle_timer(&mut self) {
// equivalent to internal Instant::far_future() (30 years) // equivalent to internal Instant::far_future() (30 years)
self.idle_timer self.idle_timer
@@ -533,6 +558,7 @@ impl Editor {
.reset(Instant::now() + Duration::from_secs(86400 * 365 * 30)); .reset(Instant::now() + Duration::from_secs(86400 * 365 * 30));
} }
#[cfg(feature = "tokio-runtime")]
pub fn reset_idle_timer(&mut self) { pub fn reset_idle_timer(&mut self) {
let config = self.config(); let config = self.config();
self.idle_timer self.idle_timer
@@ -568,12 +594,14 @@ impl Editor {
self._refresh(); self._refresh();
} }
#[cfg(feature = "lsp")]
/// Refreshes the language server for a given document /// Refreshes the language server for a given document
pub fn refresh_language_server(&mut self, doc_id: DocumentId) -> Option<()> { pub fn refresh_language_server(&mut self, doc_id: DocumentId) -> Option<()> {
let doc = self.documents.get_mut(&doc_id)?; let doc = self.documents.get_mut(&doc_id)?;
Self::launch_language_server(&mut self.language_servers, doc) Self::launch_language_server(&mut self.language_servers, doc)
} }
#[cfg(feature = "lsp")]
/// Launch a language server for a given document /// Launch a language server for a given document
fn launch_language_server(ls: &mut helix_lsp::Registry, doc: &mut Document) -> Option<()> { fn launch_language_server(ls: &mut helix_lsp::Registry, doc: &mut Document) -> Option<()> {
// if doc doesn't have a URL it's a scratch buffer, ignore it // if doc doesn't have a URL it's a scratch buffer, ignore it
@@ -611,7 +639,7 @@ impl Editor {
doc.set_language_server(Some(language_server)); doc.set_language_server(Some(language_server));
} }
} }
Some(()) Some(()) // TODO: what's the deal with the return type
} }
fn _refresh(&mut self) { fn _refresh(&mut self) {
@@ -755,6 +783,7 @@ impl Editor {
} else { } else {
let mut doc = Document::open(&path, None, Some(self.syn_loader.clone()))?; let mut doc = Document::open(&path, None, Some(self.syn_loader.clone()))?;
#[cfg(feature = "lsp")]
let _ = Self::launch_language_server(&mut self.language_servers, &mut doc); let _ = Self::launch_language_server(&mut self.language_servers, &mut doc);
self.new_document(doc) self.new_document(doc)
@@ -792,6 +821,7 @@ impl Editor {
); );
} }
#[cfg(feature = "lsp")]
if let Some(language_server) = doc.language_server() { if let Some(language_server) = doc.language_server() {
tokio::spawn(language_server.text_document_did_close(doc.identifier())); tokio::spawn(language_server.text_document_did_close(doc.identifier()));
} }
@@ -941,6 +971,7 @@ impl Editor {
} }
} }
#[cfg(feature = "lsp")]
/// Closes language servers with timeout. The default timeout is 500 ms, use /// Closes language servers with timeout. The default timeout is 500 ms, use
/// `timeout` parameter to override this. /// `timeout` parameter to override this.
pub async fn close_language_servers( pub async fn close_language_servers(

View File

@@ -2,7 +2,7 @@ use crate::input::KeyEvent;
use helix_core::{register::Registers, unicode::width::UnicodeWidthStr}; use helix_core::{register::Registers, unicode::width::UnicodeWidthStr};
use std::{collections::BTreeSet, fmt::Write}; use std::{collections::BTreeSet, fmt::Write};
#[derive(Debug)] #[derive(Debug, Clone)]
/// Info box used in editor. Rendering logic will be in other crate. /// Info box used in editor. Rendering logic will be in other crate.
pub struct Info { pub struct Info {
/// Title shown at top. /// Title shown at top.
@@ -73,3 +73,55 @@ impl Info {
infobox infobox
} }
} }
// term
use crate::{
compositor::{self, Component, RenderContext},
graphics::{Margin, Rect},
};
#[cfg(feature = "term")]
use tui::widgets::{Block, Borders, Paragraph, Widget};
#[cfg(feature = "term")]
impl compositor::term::Render for Info {
fn render(&mut self, viewport: Rect, cx: &mut RenderContext<'_>) {
let text_style = cx.editor.theme.get("ui.text.info");
let popup_style = cx.editor.theme.get("ui.popup.info");
// Calculate the area of the terminal to modify. Because we want to
// render at the bottom right, we use the viewport's width and height
// which evaluate to the most bottom right coordinate.
let width = self.width + 2 + 2; // +2 for border, +2 for margin
let height = self.height + 2; // +2 for border
let area = viewport.intersection(Rect::new(
viewport.width.saturating_sub(width),
viewport.height.saturating_sub(height + 2), // +2 for statusline
width,
height,
));
cx.surface.clear_with(area, popup_style);
let block = Block::default()
.title(self.title.as_str())
.borders(Borders::ALL)
.border_style(popup_style);
let margin = Margin {
vertical: 0,
horizontal: 1,
};
let inner = block.inner(area).inner(&margin);
block.render(area, cx.surface);
Paragraph::new(self.text.as_str())
.style(text_style)
.render(inner, cx.surface);
}
}
#[cfg(feature = "ui")]
impl compositor::ui::Render for Info {}
impl Component for Info {}

View File

@@ -2,10 +2,54 @@
use anyhow::{anyhow, Error}; use anyhow::{anyhow, Error};
use helix_core::unicode::width::UnicodeWidthStr; use helix_core::unicode::width::UnicodeWidthStr;
use serde::de::{self, Deserialize, Deserializer}; use serde::de::{self, Deserialize, Deserializer};
use std::fmt;
use crate::keyboard::{KeyCode, KeyModifiers}; pub use crate::keyboard::{KeyCode, KeyModifiers};
#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)]
pub enum Event {
Key(KeyEvent),
Mouse(MouseEvent),
Resize(u16, u16),
}
#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)]
pub struct MouseEvent {
/// The kind of mouse event that was caused.
pub kind: MouseEventKind,
/// The column that the event occurred on.
pub column: u16,
/// The row that the event occurred on.
pub row: u16,
/// The key modifiers active when the event occurred.
pub modifiers: KeyModifiers,
}
#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)]
pub enum MouseEventKind {
/// Pressed mouse button. Contains the button that was pressed.
Down(MouseButton),
/// Released mouse button. Contains the button that was released.
Up(MouseButton),
/// Moved the mouse cursor while pressing the contained mouse button.
Drag(MouseButton),
/// Moved the mouse cursor while not pressing a mouse button.
Moved,
/// Scrolled mouse wheel downwards (towards the user).
ScrollDown,
/// Scrolled mouse wheel upwards (away from the user).
ScrollUp,
}
/// Represents a mouse button.
#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)]
pub enum MouseButton {
/// Left mouse button.
Left,
/// Right mouse button.
Right,
/// Middle mouse button.
Middle,
}
/// Represents a key event. /// Represents a key event.
// We use a newtype here because we want to customize Deserialize and Display. // We use a newtype here because we want to customize Deserialize and Display.
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash)] #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash)]
@@ -49,8 +93,8 @@ pub(crate) mod keys {
pub(crate) const PERCENT: &str = "percent"; pub(crate) const PERCENT: &str = "percent";
} }
impl fmt::Display for KeyEvent { impl std::fmt::Display for KeyEvent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!( f.write_fmt(format_args!(
"{}{}{}", "{}{}{}",
if self.modifiers.contains(KeyModifiers::SHIFT) { if self.modifiers.contains(KeyModifiers::SHIFT) {
@@ -214,46 +258,6 @@ impl<'de> Deserialize<'de> for KeyEvent {
} }
} }
#[cfg(feature = "term")]
impl From<crossterm::event::KeyEvent> for KeyEvent {
fn from(crossterm::event::KeyEvent { code, modifiers }: crossterm::event::KeyEvent) -> Self {
if code == crossterm::event::KeyCode::BackTab {
// special case for BackTab -> Shift-Tab
let mut modifiers: KeyModifiers = modifiers.into();
modifiers.insert(KeyModifiers::SHIFT);
Self {
code: KeyCode::Tab,
modifiers,
}
} else {
Self {
code: code.into(),
modifiers: modifiers.into(),
}
}
}
}
#[cfg(feature = "term")]
impl From<KeyEvent> for crossterm::event::KeyEvent {
fn from(KeyEvent { code, modifiers }: KeyEvent) -> Self {
if code == KeyCode::Tab && modifiers.contains(KeyModifiers::SHIFT) {
// special case for Shift-Tab -> BackTab
let mut modifiers = modifiers;
modifiers.remove(KeyModifiers::SHIFT);
crossterm::event::KeyEvent {
code: crossterm::event::KeyCode::BackTab,
modifiers: modifiers.into(),
}
} else {
crossterm::event::KeyEvent {
code: code.into(),
modifiers: modifiers.into(),
}
}
}
}
pub fn parse_macro(keys_str: &str) -> anyhow::Result<Vec<KeyEvent>> { pub fn parse_macro(keys_str: &str) -> anyhow::Result<Vec<KeyEvent>> {
use anyhow::Context; use anyhow::Context;
let mut keys_res: anyhow::Result<_> = Ok(Vec::new()); let mut keys_res: anyhow::Result<_> = Ok(Vec::new());

View File

@@ -1,6 +1,5 @@
use helix_view::Editor;
use crate::compositor::Compositor; use crate::compositor::Compositor;
use crate::Editor;
use futures_util::future::{self, BoxFuture, Future, FutureExt}; use futures_util::future::{self, BoxFuture, Future, FutureExt};
use futures_util::stream::{FuturesUnordered, StreamExt}; use futures_util::stream::{FuturesUnordered, StreamExt};
@@ -77,13 +76,6 @@ impl Jobs {
} }
} }
pub async fn next_job(&mut self) -> Option<anyhow::Result<Option<Callback>>> {
tokio::select! {
event = self.futures.next() => { event }
event = self.wait_futures.next() => { event }
}
}
pub fn add(&self, j: Job) { pub fn add(&self, j: Job) {
if j.wait { if j.wait {
self.wait_futures.push(j.future); self.wait_futures.push(j.future);

View File

@@ -55,7 +55,6 @@ impl From<crossterm::event::KeyModifiers> for KeyModifiers {
/// Represents a key. /// Represents a key.
#[derive(Debug, PartialOrd, Ord, PartialEq, Eq, Clone, Copy, Hash)] #[derive(Debug, PartialOrd, Ord, PartialEq, Eq, Clone, Copy, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum KeyCode { pub enum KeyCode {
/// Backspace key. /// Backspace key.
Backspace, Backspace,

View File

@@ -2,12 +2,11 @@ pub mod default;
pub mod macros; pub mod macros;
pub use crate::commands::MappableCommand; pub use crate::commands::MappableCommand;
use crate::config::Config; use crate::{document::Mode, info::Info, input::KeyEvent};
use arc_swap::{ use arc_swap::{
access::{DynAccess, DynGuard}, access::{DynAccess, DynGuard},
ArcSwap, ArcSwap,
}; };
use helix_view::{document::Mode, info::Info, input::KeyEvent};
use serde::Deserialize; use serde::Deserialize;
use std::{ use std::{
borrow::Cow, borrow::Cow,
@@ -361,20 +360,10 @@ impl Default for Keymaps {
} }
} }
/// Merge default config keys with user overwritten keys for custom user config.
pub fn merge_keys(mut config: Config) -> Config {
let mut delta = std::mem::replace(&mut config.keys, default());
for (mode, keys) in &mut config.keys {
keys.merge(delta.remove(mode).unwrap_or_default())
}
config
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::macros::keymap; use super::macros::keymap;
use super::*; use super::*;
use arc_swap::access::Constant;
use helix_core::hashmap; use helix_core::hashmap;
#[test] #[test]
@@ -392,103 +381,6 @@ mod tests {
Keymaps::default(); Keymaps::default();
} }
#[test]
fn merge_partial_keys() {
let config = Config {
keys: hashmap! {
Mode::Normal => Keymap::new(
keymap!({ "Normal mode"
"i" => normal_mode,
"" => insert_mode,
"z" => jump_backward,
"g" => { "Merge into goto mode"
"$" => goto_line_end,
"g" => delete_char_forward,
},
})
)
},
..Default::default()
};
let mut merged_config = merge_keys(config.clone());
assert_ne!(config, merged_config);
let mut keymap = Keymaps::new(Box::new(Constant(merged_config.keys.clone())));
assert_eq!(
keymap.get(Mode::Normal, key!('i')),
KeymapResult::Matched(MappableCommand::normal_mode),
"Leaf should replace leaf"
);
assert_eq!(
keymap.get(Mode::Normal, key!('无')),
KeymapResult::Matched(MappableCommand::insert_mode),
"New leaf should be present in merged keymap"
);
// Assumes that z is a node in the default keymap
assert_eq!(
keymap.get(Mode::Normal, key!('z')),
KeymapResult::Matched(MappableCommand::jump_backward),
"Leaf should replace node"
);
let keymap = merged_config.keys.get_mut(&Mode::Normal).unwrap();
// Assumes that `g` is a node in default keymap
assert_eq!(
keymap.root().search(&[key!('g'), key!('$')]).unwrap(),
&KeyTrie::Leaf(MappableCommand::goto_line_end),
"Leaf should be present in merged subnode"
);
// Assumes that `gg` is in default keymap
assert_eq!(
keymap.root().search(&[key!('g'), key!('g')]).unwrap(),
&KeyTrie::Leaf(MappableCommand::delete_char_forward),
"Leaf should replace old leaf in merged subnode"
);
// Assumes that `ge` is in default keymap
assert_eq!(
keymap.root().search(&[key!('g'), key!('e')]).unwrap(),
&KeyTrie::Leaf(MappableCommand::goto_last_line),
"Old leaves in subnode should be present in merged node"
);
assert!(merged_config.keys.get(&Mode::Normal).unwrap().len() > 1);
assert!(merged_config.keys.get(&Mode::Insert).unwrap().len() > 0);
}
#[test]
fn order_should_be_set() {
let config = Config {
keys: hashmap! {
Mode::Normal => Keymap::new(
keymap!({ "Normal mode"
"space" => { ""
"s" => { ""
"v" => vsplit,
"c" => hsplit,
},
},
})
)
},
..Default::default()
};
let mut merged_config = merge_keys(config.clone());
assert_ne!(config, merged_config);
let keymap = merged_config.keys.get_mut(&Mode::Normal).unwrap();
// Make sure mapping works
assert_eq!(
keymap
.root()
.search(&[key!(' '), key!('s'), key!('v')])
.unwrap(),
&KeyTrie::Leaf(MappableCommand::vsplit),
"Leaf should be present in merged subnode"
);
// Make sure an order was set during merge
let node = keymap.root().search(&[crate::key!(' ')]).unwrap();
assert!(!node.node().unwrap().order().is_empty())
}
#[test] #[test]
fn aliased_modes_are_same_in_default_keymap() { fn aliased_modes_are_same_in_default_keymap() {
let keymaps = Keymaps::default().map(); let keymaps = Keymaps::default().map();

View File

@@ -5,7 +5,7 @@ use super::{Keymap, Mode};
use helix_core::hashmap; use helix_core::hashmap;
pub fn default() -> HashMap<Mode, Keymap> { pub fn default() -> HashMap<Mode, Keymap> {
let normal = keymap!({ "Normal mode" let mut normal = keymap!({ "Normal mode"
"h" | "left" => move_char_left, "h" | "left" => move_char_left,
"j" | "down" => move_line_down, "j" | "down" => move_line_down,
"k" | "up" => move_line_up, "k" | "up" => move_line_up,
@@ -43,10 +43,10 @@ pub fn default() -> HashMap<Mode, Keymap> {
"h" => goto_line_start, "h" => goto_line_start,
"l" => goto_line_end, "l" => goto_line_end,
"s" => goto_first_nonwhitespace, "s" => goto_first_nonwhitespace,
"d" => goto_definition, // "d" => goto_definition,
"y" => goto_type_definition, // "y" => goto_type_definition,
"r" => goto_reference, // "r" => goto_reference,
"i" => goto_implementation, // "i" => goto_implementation,
"t" => goto_window_top, "t" => goto_window_top,
"c" => goto_window_center, "c" => goto_window_center,
"b" => goto_window_bottom, "b" => goto_window_bottom,
@@ -198,30 +198,10 @@ pub fn default() -> HashMap<Mode, Keymap> {
"f" => file_picker, "f" => file_picker,
"F" => file_picker_in_current_directory, "F" => file_picker_in_current_directory,
"b" => buffer_picker, "b" => buffer_picker,
"s" => symbol_picker, // "s" => symbol_picker,
"S" => workspace_symbol_picker, // "S" => workspace_symbol_picker,
"a" => code_action, // "a" => code_action,
"'" => last_picker, "'" => last_picker,
"d" => { "Debug (experimental)" sticky=true
"l" => dap_launch,
"b" => dap_toggle_breakpoint,
"c" => dap_continue,
"h" => dap_pause,
"i" => dap_step_in,
"o" => dap_step_out,
"n" => dap_next,
"v" => dap_variables,
"t" => dap_terminate,
"C-c" => dap_edit_condition,
"C-l" => dap_edit_log,
"s" => { "Switch"
"t" => dap_switch_thread,
"f" => dap_switch_stack_frame,
// sl, sb
},
"e" => dap_enable_exceptions,
"E" => dap_disable_exceptions,
},
"w" => { "Window" "w" => { "Window"
"C-w" | "w" => rotate_view, "C-w" | "w" => rotate_view,
"C-s" | "s" => hsplit, "C-s" | "s" => hsplit,
@@ -245,8 +225,8 @@ pub fn default() -> HashMap<Mode, Keymap> {
"P" => paste_clipboard_before, "P" => paste_clipboard_before,
"R" => replace_selections_with_clipboard, "R" => replace_selections_with_clipboard,
"/" => global_search, "/" => global_search,
"k" => hover, // "k" => hover,
"r" => rename_symbol, // "r" => rename_symbol,
"?" => command_palette, "?" => command_palette,
}, },
"z" => { "View" "z" => { "View"
@@ -285,6 +265,34 @@ pub fn default() -> HashMap<Mode, Keymap> {
"C-a" => increment, "C-a" => increment,
"C-x" => decrement, "C-x" => decrement,
}); });
// DAP
#[cfg(feature = "dap")]
normal.merge_nodes(keymap!({ "Normal mode"
"space" => { "Space"
"d" => { "Debug (experimental)" sticky=true
"l" => dap_launch,
"b" => dap_toggle_breakpoint,
"c" => dap_continue,
"h" => dap_pause,
"i" => dap_step_in,
"o" => dap_step_out,
"n" => dap_next,
"v" => dap_variables,
"t" => dap_terminate,
"C-c" => dap_edit_condition,
"C-l" => dap_edit_log,
"s" => { "Switch"
"t" => dap_switch_thread,
"f" => dap_switch_stack_frame,
// sl, sb
},
"e" => dap_enable_exceptions,
"E" => dap_disable_exceptions,
},
},
}));
let mut select = normal.clone(); let mut select = normal.clone();
select.merge_nodes(keymap!({ "Select mode" select.merge_nodes(keymap!({ "Select mode"
"h" | "left" => extend_char_left, "h" | "left" => extend_char_left,

View File

@@ -0,0 +1,60 @@
/// Macro for defining the root of a `Keymap` object. Example:
///
/// ```
/// # use helix_core::hashmap;
/// # use helix_term::keymap;
/// # use helix_term::keymap::Keymap;
/// let normal_mode = keymap!({ "Normal mode"
/// "i" => insert_mode,
/// "g" => { "Goto"
/// "g" => goto_file_start,
/// "e" => goto_file_end,
/// },
/// "j" | "down" => move_line_down,
/// });
/// let keymap = Keymap::new(normal_mode);
/// ```
#[macro_export]
macro_rules! keymap {
(@trie $cmd:ident) => {
$crate::keymap::KeyTrie::Leaf($crate::commands::MappableCommand::$cmd)
};
(@trie
{ $label:literal $(sticky=$sticky:literal)? $($($key:literal)|+ => $value:tt,)+ }
) => {
keymap!({ $label $(sticky=$sticky)? $($($key)|+ => $value,)+ })
};
(@trie [$($cmd:ident),* $(,)?]) => {
$crate::keymap::KeyTrie::Sequence(vec![$($crate::commands::Command::$cmd),*])
};
(
{ $label:literal $(sticky=$sticky:literal)? $($($key:literal)|+ => $value:tt,)+ }
) => {
// modified from the hashmap! macro
{
let _cap = hashmap!(@count $($($key),+),*);
let mut _map = ::std::collections::HashMap::with_capacity(_cap);
let mut _order = ::std::vec::Vec::with_capacity(_cap);
$(
$(
let _key = $key.parse::<$crate::input::KeyEvent>().unwrap();
let _duplicate = _map.insert(
_key,
keymap!(@trie $value)
);
assert!(_duplicate.is_none(), "Duplicate key found: {:?}", _duplicate.unwrap());
_order.push(_key);
)+
)*
let mut _node = $crate::keymap::KeyTrieNode::new($label, _map, _order);
$( _node.is_sticky = $sticky; )?
$crate::keymap::KeyTrie::Node(_node)
}
};
}
pub use crate::{alt, ctrl, key, shift};
pub use keymap;

View File

@@ -1,12 +1,24 @@
#[macro_use] #[macro_use]
pub mod macros; pub mod macros;
pub mod args;
pub mod backend {
#[cfg(feature = "term")]
pub mod term;
}
pub mod clipboard; pub mod clipboard;
pub mod commands;
pub mod compositor;
pub mod document; pub mod document;
pub mod editor; pub mod editor;
pub mod graphics; pub mod ui;
pub use helix_graphics as graphics;
pub mod gutter; pub mod gutter;
pub mod job;
pub mod keymap;
pub use keymap::macros::*;
pub mod handlers { pub mod handlers {
#[cfg(feature = "dap")]
pub mod dap; pub mod dap;
pub mod lsp; pub mod lsp;
} }
@@ -40,6 +52,17 @@ slotmap::new_key_type! {
pub struct ViewId; pub struct ViewId;
} }
#[cfg(not(windows))]
pub fn true_color() -> bool {
std::env::var("COLORTERM")
.map(|v| matches!(v.as_str(), "truecolor" | "24bit"))
.unwrap_or(false)
}
#[cfg(windows)]
pub fn true_color() -> bool {
true
}
pub enum Align { pub enum Align {
Top, Top,
Center, Center,

View File

@@ -61,3 +61,69 @@ macro_rules! doc {
$crate::current_ref!($editor).1 $crate::current_ref!($editor).1
}}; }};
} }
// Keymap macros
#[macro_export]
macro_rules! key {
($key:ident) => {
$crate::input::KeyEvent {
code: $crate::keyboard::KeyCode::$key,
modifiers: $crate::keyboard::KeyModifiers::NONE,
}
};
($($ch:tt)*) => {
$crate::input::KeyEvent {
code: $crate::keyboard::KeyCode::Char($($ch)*),
modifiers: $crate::keyboard::KeyModifiers::NONE,
}
};
}
#[macro_export]
macro_rules! shift {
($key:ident) => {
$crate::input::KeyEvent {
code: $crate::keyboard::KeyCode::$key,
modifiers: $crate::keyboard::KeyModifiers::SHIFT,
}
};
($($ch:tt)*) => {
$crate::input::KeyEvent {
code: $crate::keyboard::KeyCode::Char($($ch)*),
modifiers: $crate::keyboard::KeyModifiers::SHIFT,
}
};
}
#[macro_export]
macro_rules! ctrl {
($key:ident) => {
$crate::input::KeyEvent {
code: $crate::keyboard::KeyCode::$key,
modifiers: $crate::keyboard::KeyModifiers::CONTROL,
}
};
($($ch:tt)*) => {
$crate::input::KeyEvent {
code: $crate::keyboard::KeyCode::Char($($ch)*),
modifiers: $crate::keyboard::KeyModifiers::CONTROL,
}
};
}
#[macro_export]
macro_rules! alt {
($key:ident) => {
$crate::input::KeyEvent {
code: $crate::keyboard::KeyCode::$key,
modifiers: $crate::keyboard::KeyModifiers::ALT,
}
};
($($ch:tt)*) => {
$crate::input::KeyEvent {
code: $crate::keyboard::KeyCode::Char($($ch)*),
modifiers: $crate::keyboard::KeyModifiers::ALT,
}
};
}

View File

@@ -1,12 +1,14 @@
use crate::compositor::{Component, Context, EventResult}; use crate::compositor::{self, Component, Context, Event, EventResult};
use crossterm::event::{Event, KeyCode, KeyEvent}; use crate::editor::CompleteAction;
use helix_view::editor::CompleteAction;
use tui::buffer::Buffer as Surface;
use std::borrow::Cow; use std::borrow::Cow;
use crate::{
graphics::Rect,
input::{KeyCode, KeyEvent},
Document, Editor,
};
use helix_core::{Change, Transaction}; use helix_core::{Change, Transaction};
use helix_view::{graphics::Rect, Document, Editor};
use crate::commands; use crate::commands;
use crate::ui::{menu, Markdown, Menu, Popup, PromptEvent}; use crate::ui::{menu, Markdown, Menu, Popup, PromptEvent};
@@ -300,9 +302,12 @@ impl Component for Completion {
fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> { fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
self.popup.required_size(viewport) self.popup.required_size(viewport)
} }
}
fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { #[cfg(feature = "term")]
self.popup.render(area, surface, cx); impl compositor::term::Render for Completion {
fn render(&mut self, area: Rect, cx: &mut compositor::term::RenderContext<'_>) {
self.popup.render(area, cx);
// if we have a selection, render a markdown popup on top/below with info // if we have a selection, render a markdown popup on top/below with info
if let Some(option) = self.popup.contents().selection() { if let Some(option) = self.popup.contents().selection() {
@@ -311,7 +316,7 @@ impl Component for Completion {
// --- // ---
// option.documentation // option.documentation
let (view, doc) = current!(cx.editor); let (view, doc) = current_ref!(cx.editor);
let language = doc let language = doc
.language() .language()
.and_then(|scope| scope.strip_prefix("source.")) .and_then(|scope| scope.strip_prefix("source."))
@@ -369,7 +374,7 @@ impl Component for Completion {
None => return, None => return,
}; };
let (popup_x, popup_y) = self.popup.get_rel_position(area, cx); let (popup_x, popup_y) = self.popup.get_rel_position(area, cx.editor);
let (popup_width, _popup_height) = self.popup.get_size(); let (popup_width, _popup_height) = self.popup.get_size();
let mut width = area let mut width = area
.width .width
@@ -403,8 +408,8 @@ impl Component for Completion {
// clear area // clear area
let background = cx.editor.theme.get("ui.popup"); let background = cx.editor.theme.get("ui.popup");
surface.clear_with(area, background); cx.surface.clear_with(area, background);
markdown_doc.render(area, surface, cx); markdown_doc.render(area, cx);
} }
} }
} }

View File

@@ -1,11 +1,19 @@
use crate::{ use crate::{
commands, commands, compositor, key,
compositor::{Component, Context, EventResult},
key,
keymap::{KeymapResult, Keymaps}, keymap::{KeymapResult, Keymaps},
ui::{Completion, ProgressSpinners}, ui::{self, ProgressSpinners},
}; };
use crate::compositor::{Component, Context, Event, EventResult};
use crate::{
document::{Mode, SCRATCH_BUFFER_NAME},
editor::{CompleteAction, CursorShapeConfig},
graphics::{CursorKind, Modifier, Rect, Style},
input::{KeyEvent, MouseButton, MouseEvent, MouseEventKind},
keyboard::{KeyCode, KeyModifiers},
Document, Editor, Theme, View,
};
use helix_core::{ use helix_core::{
coords_at_pos, encoding, coords_at_pos, encoding,
graphemes::{ graphemes::{
@@ -17,24 +25,14 @@ use helix_core::{
unicode::width::UnicodeWidthStr, unicode::width::UnicodeWidthStr,
LineEnding, Position, Range, Selection, Transaction, LineEnding, Position, Range, Selection, Transaction,
}; };
use helix_view::{
document::{Mode, SCRATCH_BUFFER_NAME},
editor::{CompleteAction, CursorShapeConfig},
graphics::{CursorKind, Modifier, Rect, Style},
input::KeyEvent,
keyboard::{KeyCode, KeyModifiers},
Document, Editor, Theme, View,
};
use std::borrow::Cow; use std::borrow::Cow;
use crossterm::event::{Event, MouseButton, MouseEvent, MouseEventKind};
use tui::buffer::Buffer as Surface;
pub struct EditorView { pub struct EditorView {
pub keymaps: Keymaps, pub keymaps: Keymaps,
on_next_key: Option<Box<dyn FnOnce(&mut commands::Context, KeyEvent)>>, on_next_key: Option<Box<dyn FnOnce(&mut commands::Context, KeyEvent)>>,
last_insert: (commands::MappableCommand, Vec<InsertEvent>), last_insert: (commands::MappableCommand, Vec<InsertEvent>),
pub(crate) completion: Option<Completion>, #[cfg(feature = "term")]
pub(crate) completion: Option<ui::Completion>,
spinners: ProgressSpinners, spinners: ProgressSpinners,
} }
@@ -57,6 +55,7 @@ impl EditorView {
keymaps, keymaps,
on_next_key: None, on_next_key: None,
last_insert: (commands::MappableCommand::normal_mode, Vec::new()), last_insert: (commands::MappableCommand::normal_mode, Vec::new()),
#[cfg(feature = "term")]
completion: None, completion: None,
spinners: ProgressSpinners::default(), spinners: ProgressSpinners::default(),
} }
@@ -65,7 +64,13 @@ impl EditorView {
pub fn spinners_mut(&mut self) -> &mut ProgressSpinners { pub fn spinners_mut(&mut self) -> &mut ProgressSpinners {
&mut self.spinners &mut self.spinners
} }
}
#[cfg(feature = "term")]
use tui::buffer::Buffer as Surface;
#[cfg(feature = "term")]
impl EditorView {
pub fn render_view( pub fn render_view(
&self, &self,
editor: &Editor, editor: &Editor,
@@ -80,6 +85,7 @@ impl EditorView {
let theme = &editor.theme; let theme = &editor.theme;
// DAP: Highlight current stack frame position // DAP: Highlight current stack frame position
#[cfg(feature = "dap")]
let stack_frame = editor.debugger.as_ref().and_then(|debugger| { let stack_frame = editor.debugger.as_ref().and_then(|debugger| {
if let (Some(frame), Some(thread_id)) = (debugger.active_frame, debugger.thread_id) { if let (Some(frame), Some(thread_id)) = (debugger.active_frame, debugger.thread_id) {
debugger debugger
@@ -90,6 +96,7 @@ impl EditorView {
None None
} }
}); });
#[cfg(feature = "dap")]
if let Some(frame) = stack_frame { if let Some(frame) = stack_frame {
if doc.path().is_some() if doc.path().is_some()
&& frame && frame
@@ -352,9 +359,9 @@ impl EditorView {
surface: &mut Surface, surface: &mut Surface,
theme: &Theme, theme: &Theme,
highlights: H, highlights: H,
whitespace: &helix_view::editor::WhitespaceConfig, whitespace: &crate::editor::WhitespaceConfig,
) { ) {
use helix_view::editor::WhitespaceRenderValue; use crate::editor::WhitespaceRenderValue;
// It's slightly more efficient to produce a full RopeSlice from the Rope, then slice that a bunch // It's slightly more efficient to produce a full RopeSlice from the Rope, then slice that a bunch
// of times than it is to always call Rope::slice/get_slice (it will internally always hit RSEnum::Light). // of times than it is to always call Rope::slice/get_slice (it will internally always hit RSEnum::Light).
@@ -448,7 +455,7 @@ impl EditorView {
// make sure we display tab as appropriate amount of spaces // make sure we display tab as appropriate amount of spaces
let visual_tab_width = tab_width - (visual_x as usize % tab_width); let visual_tab_width = tab_width - (visual_x as usize % tab_width);
let grapheme_tab_width = let grapheme_tab_width =
ropey::str_utils::char_to_byte_idx(&tab, visual_tab_width); helix_core::str_utils::char_to_byte_idx(&tab, visual_tab_width);
(&tab[..grapheme_tab_width], visual_tab_width) (&tab[..grapheme_tab_width], visual_tab_width)
} else if grapheme == " " { } else if grapheme == " " {
@@ -776,7 +783,9 @@ impl EditorView {
true, true,
); );
} }
}
impl EditorView {
/// Handle events by looking them up in `self.keymaps`. Returns None /// Handle events by looking them up in `self.keymaps`. Returns None
/// if event was handled (a command was executed or a subkeymap was /// if event was handled (a command was executed or a subkeymap was
/// activated). Only KeymapResult::{NotFound, Cancelled} is returned /// activated). Only KeymapResult::{NotFound, Cancelled} is returned
@@ -890,6 +899,7 @@ impl EditorView {
} }
} }
#[cfg(feature = "term")]
pub fn set_completion( pub fn set_completion(
&mut self, &mut self,
editor: &mut Editor, editor: &mut Editor,
@@ -900,7 +910,7 @@ impl EditorView {
size: Rect, size: Rect,
) { ) {
let mut completion = let mut completion =
Completion::new(editor, items, offset_encoding, start_offset, trigger_offset); ui::Completion::new(editor, items, offset_encoding, start_offset, trigger_offset);
if completion.is_empty() { if completion.is_empty() {
// skip if we got no completion results // skip if we got no completion results
@@ -918,6 +928,7 @@ impl EditorView {
self.completion = Some(completion); self.completion = Some(completion);
} }
#[cfg(feature = "term")]
pub fn clear_completion(&mut self, editor: &mut Editor) { pub fn clear_completion(&mut self, editor: &mut Editor) {
self.completion = None; self.completion = None;
@@ -927,6 +938,7 @@ impl EditorView {
editor.clear_idle_timer(); // don't retrigger editor.clear_idle_timer(); // don't retrigger
} }
#[cfg(feature = "term")]
pub fn handle_idle_timeout(&mut self, cx: &mut crate::compositor::Context) -> EventResult { pub fn handle_idle_timeout(&mut self, cx: &mut crate::compositor::Context) -> EventResult {
if self.completion.is_some() if self.completion.is_some()
|| !cx.editor.config().auto_completion || !cx.editor.config().auto_completion
@@ -947,9 +959,7 @@ impl EditorView {
EventResult::Consumed(None) EventResult::Consumed(None)
} }
}
impl EditorView {
fn handle_mouse_event( fn handle_mouse_event(
&mut self, &mut self,
event: MouseEvent, event: MouseEvent,
@@ -957,6 +967,7 @@ impl EditorView {
) -> EventResult { ) -> EventResult {
let config = cxt.editor.config(); let config = cxt.editor.config();
match event { match event {
#[cfg(feature = "dap")]
MouseEvent { MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left), kind: MouseEventKind::Down(MouseButton::Left),
row, row,
@@ -974,7 +985,7 @@ impl EditorView {
if let Some((pos, view_id)) = result { if let Some((pos, view_id)) = result {
let doc = editor.document_mut(editor.tree.get(view_id).doc).unwrap(); let doc = editor.document_mut(editor.tree.get(view_id).doc).unwrap();
if modifiers == crossterm::event::KeyModifiers::ALT { if modifiers == KeyModifiers::ALT {
let selection = doc.selection(view_id).clone(); let selection = doc.selection(view_id).clone();
doc.set_selection(view_id, selection.push(Range::point(pos))); doc.set_selection(view_id, selection.push(Range::point(pos)));
} else { } else {
@@ -1084,6 +1095,7 @@ impl EditorView {
EventResult::Consumed(None) EventResult::Consumed(None)
} }
#[cfg(feature = "dap")]
MouseEvent { MouseEvent {
kind: MouseEventKind::Up(MouseButton::Right), kind: MouseEventKind::Up(MouseButton::Right),
row, row,
@@ -1104,7 +1116,7 @@ impl EditorView {
let line = coords.row + view.offset.row; let line = coords.row + view.offset.row;
if let Ok(pos) = doc.text().try_line_to_char(line) { if let Ok(pos) = doc.text().try_line_to_char(line) {
doc.set_selection(view_id, Selection::point(pos)); doc.set_selection(view_id, Selection::point(pos));
if modifiers == crossterm::event::KeyModifiers::ALT { if modifiers == KeyModifiers::ALT {
commands::MappableCommand::dap_edit_log.execute(cxt); commands::MappableCommand::dap_edit_log.execute(cxt);
} else { } else {
commands::MappableCommand::dap_edit_condition.execute(cxt); commands::MappableCommand::dap_edit_condition.execute(cxt);
@@ -1128,7 +1140,7 @@ impl EditorView {
return EventResult::Ignored(None); return EventResult::Ignored(None);
} }
if modifiers == crossterm::event::KeyModifiers::ALT { if modifiers == KeyModifiers::ALT {
commands::MappableCommand::replace_selections_with_primary_clipboard commands::MappableCommand::replace_selections_with_primary_clipboard
.execute(cxt); .execute(cxt);
@@ -1177,9 +1189,9 @@ impl Component for EditorView {
// Handling it here but not re-rendering will cause flashing // Handling it here but not re-rendering will cause flashing
EventResult::Consumed(None) EventResult::Consumed(None)
} }
Event::Key(key) => { Event::Key(mut key) => {
#[cfg(feature = "term")]
cx.editor.reset_idle_timer(); cx.editor.reset_idle_timer();
let mut key = KeyEvent::from(key);
canonicalize_key(&mut key); canonicalize_key(&mut key);
// clear status // clear status
@@ -1196,12 +1208,12 @@ impl Component for EditorView {
Mode::Insert => { Mode::Insert => {
// let completion swallow the event if necessary // let completion swallow the event if necessary
let mut consumed = false; let mut consumed = false;
#[cfg(feature = "term")]
if let Some(completion) = &mut self.completion { if let Some(completion) = &mut self.completion {
// use a fake context here // use a fake context here
let mut cx = Context { let mut cx = Context {
editor: cx.editor, editor: cx.editor,
jobs: cx.jobs, jobs: cx.jobs,
scroll: None,
}; };
let res = completion.handle_event(event, &mut cx); let res = completion.handle_event(event, &mut cx);
@@ -1217,6 +1229,7 @@ impl Component for EditorView {
// if completion didn't take the event, we pass it onto commands // if completion didn't take the event, we pass it onto commands
if !consumed { if !consumed {
#[cfg(feature = "term")]
if let Some(compl) = cx.editor.last_completion.take() { if let Some(compl) = cx.editor.last_completion.take() {
self.last_insert.1.push(InsertEvent::CompletionApply(compl)); self.last_insert.1.push(InsertEvent::CompletionApply(compl));
} }
@@ -1227,6 +1240,7 @@ impl Component for EditorView {
self.last_insert.1.push(InsertEvent::Key(key)); self.last_insert.1.push(InsertEvent::Key(key));
// lastly we recalculate completion // lastly we recalculate completion
#[cfg(feature = "term")]
if let Some(completion) = &mut self.completion { if let Some(completion) = &mut self.completion {
completion.update(&mut cx); completion.update(&mut cx);
if completion.is_empty() { if completion.is_empty() {
@@ -1274,6 +1288,7 @@ impl Component for EditorView {
}; };
self.last_insert.1.clear(); self.last_insert.1.clear();
} }
#[cfg(feature = "term")]
(Mode::Insert, Mode::Normal) => { (Mode::Insert, Mode::Normal) => {
// if exiting insert mode, remove completion // if exiting insert mode, remove completion
self.completion = None; self.completion = None;
@@ -1287,23 +1302,26 @@ impl Component for EditorView {
Event::Mouse(event) => self.handle_mouse_event(event, &mut cx), Event::Mouse(event) => self.handle_mouse_event(event, &mut cx),
} }
} }
}
fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { #[cfg(feature = "term")]
impl compositor::term::Render for EditorView {
fn render(&mut self, area: Rect, cx: &mut compositor::term::RenderContext<'_>) {
// clear with background color // clear with background color
surface.set_style(area, cx.editor.theme.get("ui.background")); cx.surface
.set_style(area, cx.editor.theme.get("ui.background"));
let config = cx.editor.config(); let config = cx.editor.config();
// if the terminal size suddenly changed, we need to trigger a resize
cx.editor.resize(area.clip_bottom(1)); // -1 from bottom for commandline
for (view, is_focused) in cx.editor.tree.views() { for (view, is_focused) in cx.editor.tree.views() {
let doc = cx.editor.document(view.doc).unwrap(); let doc = cx.editor.document(view.doc).unwrap();
self.render_view(cx.editor, doc, view, area, surface, is_focused); self.render_view(cx.editor, doc, view, area, cx.surface, is_focused);
} }
if config.auto_info { if config.auto_info {
if let Some(mut info) = cx.editor.autoinfo.take() { // TODO: drop &mut self on render
info.render(area, surface, cx); if let Some(mut info) = cx.editor.autoinfo.clone() {
cx.editor.autoinfo = Some(info) info.render(area, cx);
// cx.editor.autoinfo = Some(info)
} }
} }
@@ -1313,14 +1331,14 @@ impl Component for EditorView {
// render status msg // render status msg
if let Some((status_msg, severity)) = &cx.editor.status_msg { if let Some((status_msg, severity)) = &cx.editor.status_msg {
status_msg_width = status_msg.width(); status_msg_width = status_msg.width();
use helix_view::editor::Severity; use crate::editor::Severity;
let style = if *severity == Severity::Error { let style = if *severity == Severity::Error {
cx.editor.theme.get("error") cx.editor.theme.get("error")
} else { } else {
cx.editor.theme.get("ui.text") cx.editor.theme.get("ui.text")
}; };
surface.set_string( cx.surface.set_string(
area.x, area.x,
area.y + area.height.saturating_sub(1), area.y + area.height.saturating_sub(1),
status_msg, status_msg,
@@ -1350,7 +1368,7 @@ impl Component for EditorView {
} else { } else {
0 0
}; };
surface.set_string( cx.surface.set_string(
area.x + area.width.saturating_sub(key_width + macro_width), area.x + area.width.saturating_sub(key_width + macro_width),
area.y + area.height.saturating_sub(1), area.y + area.height.saturating_sub(1),
disp.get(disp.len().saturating_sub(key_width as usize)..) disp.get(disp.len().saturating_sub(key_width as usize)..)
@@ -1360,9 +1378,9 @@ impl Component for EditorView {
if let Some((reg, _)) = cx.editor.macro_recording { if let Some((reg, _)) = cx.editor.macro_recording {
let disp = format!("[{}]", reg); let disp = format!("[{}]", reg);
let style = style let style = style
.fg(helix_view::graphics::Color::Yellow) .fg(crate::graphics::Color::Yellow)
.add_modifier(Modifier::BOLD); .add_modifier(Modifier::BOLD);
surface.set_string( cx.surface.set_string(
area.x + area.width.saturating_sub(3), area.x + area.width.saturating_sub(3),
area.y + area.height.saturating_sub(1), area.y + area.height.saturating_sub(1),
&disp, &disp,
@@ -1372,7 +1390,7 @@ impl Component for EditorView {
} }
if let Some(completion) = self.completion.as_mut() { if let Some(completion) = self.completion.as_mut() {
completion.render(area, surface, cx); completion.render(area, cx);
} }
} }
@@ -1385,6 +1403,9 @@ impl Component for EditorView {
} }
} }
#[cfg(feature = "ui")]
impl compositor::ui::Render for EditorView {}
fn canonicalize_key(key: &mut KeyEvent) { fn canonicalize_key(key: &mut KeyEvent) {
if let KeyEvent { if let KeyEvent {
code: KeyCode::Char(_), code: KeyCode::Char(_),

View File

@@ -1,22 +1,22 @@
use crate::compositor::{Component, Context}; use crate::compositor::{self, Component, RenderContext};
use tui::{
buffer::Buffer as Surface, #[cfg(feature = "term")]
text::{Span, Spans, Text}, use tui::text::{Span, Spans, Text};
};
use std::sync::Arc; use std::sync::Arc;
use pulldown_cmark::{CodeBlockKind, Event, HeadingLevel, Options, Parser, Tag}; use pulldown_cmark::{CodeBlockKind, Event, HeadingLevel, Options, Parser, Tag};
use crate::{
graphics::{Margin, Rect, Style},
Theme,
};
use helix_core::{ use helix_core::{
syntax::{self, HighlightEvent, Syntax}, syntax::{self, HighlightEvent, Syntax},
Rope, Rope,
}; };
use helix_view::{
graphics::{Margin, Rect, Style},
Theme,
};
#[cfg(feature = "term")]
fn styled_multiline_text<'a>(text: String, style: Style) -> Text<'a> { fn styled_multiline_text<'a>(text: String, style: Style) -> Text<'a> {
let spans: Vec<_> = text let spans: Vec<_> = text
.lines() .lines()
@@ -26,6 +26,7 @@ fn styled_multiline_text<'a>(text: String, style: Style) -> Text<'a> {
Text::from(spans) Text::from(spans)
} }
#[cfg(feature = "term")]
pub fn highlighted_code_block<'a>( pub fn highlighted_code_block<'a>(
text: String, text: String,
language: &str, language: &str,
@@ -143,6 +144,10 @@ impl Markdown {
} }
} }
#[cfg(not(feature = "term"))]
fn parse(&self, theme: Option<&Theme>) -> () {}
#[cfg(feature = "term")]
fn parse(&self, theme: Option<&Theme>) -> tui::text::Text<'_> { fn parse(&self, theme: Option<&Theme>) -> tui::text::Text<'_> {
// // also 2021-03-04T16:33:58.553 helix_lsp::transport [INFO] <- {"contents":{"kind":"markdown","value":"\n```rust\ncore::num\n```\n\n```rust\npub const fn saturating_sub(self, rhs:Self) ->Self\n```\n\n---\n\n```rust\n```"},"range":{"end":{"character":61,"line":101},"start":{"character":47,"line":101}}} // // also 2021-03-04T16:33:58.553 helix_lsp::transport [INFO] <- {"contents":{"kind":"markdown","value":"\n```rust\ncore::num\n```\n\n```rust\npub const fn saturating_sub(self, rhs:Self) ->Self\n```\n\n---\n\n```rust\n```"},"range":{"end":{"character":61,"line":101},"start":{"character":47,"line":101}}}
// let text = "\n```rust\ncore::iter::traits::iterator::Iterator\n```\n\n```rust\nfn collect<B: FromIterator<Self::Item>>(self) -> B\nwhere\n Self: Sized,\n```\n\n---\n\nTransforms an iterator into a collection.\n\n`collect()` can take anything iterable, and turn it into a relevant\ncollection. This is one of the more powerful methods in the standard\nlibrary, used in a variety of contexts.\n\nThe most basic pattern in which `collect()` is used is to turn one\ncollection into another. You take a collection, call [`iter`](https://doc.rust-lang.org/nightly/core/iter/traits/iterator/trait.Iterator.html) on it,\ndo a bunch of transformations, and then `collect()` at the end.\n\n`collect()` can also create instances of types that are not typical\ncollections. For example, a [`String`](https://doc.rust-lang.org/nightly/core/iter/std/string/struct.String.html) can be built from [`char`](type@char)s,\nand an iterator of [`Result<T, E>`](https://doc.rust-lang.org/nightly/core/result/enum.Result.html) items can be collected\ninto `Result<Collection<T>, E>`. See the examples below for more.\n\nBecause `collect()` is so general, it can cause problems with type\ninference. As such, `collect()` is one of the few times you'll see\nthe syntax affectionately known as the 'turbofish': `::<>`. This\nhelps the inference algorithm understand specifically which collection\nyou're trying to collect into.\n\n# Examples\n\nBasic usage:\n\n```rust\nlet a = [1, 2, 3];\n\nlet doubled: Vec<i32> = a.iter()\n .map(|&x| x * 2)\n .collect();\n\nassert_eq!(vec![2, 4, 6], doubled);\n```\n\nNote that we needed the `: Vec<i32>` on the left-hand side. This is because\nwe could collect into, for example, a [`VecDeque<T>`](https://doc.rust-lang.org/nightly/core/iter/std/collections/struct.VecDeque.html) instead:\n\n```rust\nuse std::collections::VecDeque;\n\nlet a = [1, 2, 3];\n\nlet doubled: VecDeque<i32> = a.iter().map(|&x| x * 2).collect();\n\nassert_eq!(2, doubled[0]);\nassert_eq!(4, doubled[1]);\nassert_eq!(6, doubled[2]);\n```\n\nUsing the 'turbofish' instead of annotating `doubled`:\n\n```rust\nlet a = [1, 2, 3];\n\nlet doubled = a.iter().map(|x| x * 2).collect::<Vec<i32>>();\n\nassert_eq!(vec![2, 4, 6], doubled);\n```\n\nBecause `collect()` only cares about what you're collecting into, you can\nstill use a partial type hint, `_`, with the turbofish:\n\n```rust\nlet a = [1, 2, 3];\n\nlet doubled = a.iter().map(|x| x * 2).collect::<Vec<_>>();\n\nassert_eq!(vec![2, 4, 6], doubled);\n```\n\nUsing `collect()` to make a [`String`](https://doc.rust-lang.org/nightly/core/iter/std/string/struct.String.html):\n\n```rust\nlet chars = ['g', 'd', 'k', 'k', 'n'];\n\nlet hello: String = chars.iter()\n .map(|&x| x as u8)\n .map(|x| (x + 1) as char)\n .collect();\n\nassert_eq!(\"hello\", hello);\n```\n\nIf you have a list of [`Result<T, E>`](https://doc.rust-lang.org/nightly/core/result/enum.Result.html)s, you can use `collect()` to\nsee if any of them failed:\n\n```rust\nlet results = [Ok(1), Err(\"nope\"), Ok(3), Err(\"bad\")];\n\nlet result: Result<Vec<_>, &str> = results.iter().cloned().collect();\n\n// gives us the first error\nassert_eq!(Err(\"nope\"), result);\n\nlet results = [Ok(1), Ok(3)];\n\nlet result: Result<Vec<_>, &str> = results.iter().cloned().collect();\n\n// gives us the list of answers\nassert_eq!(Ok(vec![1, 3]), result);\n```"; // let text = "\n```rust\ncore::iter::traits::iterator::Iterator\n```\n\n```rust\nfn collect<B: FromIterator<Self::Item>>(self) -> B\nwhere\n Self: Sized,\n```\n\n---\n\nTransforms an iterator into a collection.\n\n`collect()` can take anything iterable, and turn it into a relevant\ncollection. This is one of the more powerful methods in the standard\nlibrary, used in a variety of contexts.\n\nThe most basic pattern in which `collect()` is used is to turn one\ncollection into another. You take a collection, call [`iter`](https://doc.rust-lang.org/nightly/core/iter/traits/iterator/trait.Iterator.html) on it,\ndo a bunch of transformations, and then `collect()` at the end.\n\n`collect()` can also create instances of types that are not typical\ncollections. For example, a [`String`](https://doc.rust-lang.org/nightly/core/iter/std/string/struct.String.html) can be built from [`char`](type@char)s,\nand an iterator of [`Result<T, E>`](https://doc.rust-lang.org/nightly/core/result/enum.Result.html) items can be collected\ninto `Result<Collection<T>, E>`. See the examples below for more.\n\nBecause `collect()` is so general, it can cause problems with type\ninference. As such, `collect()` is one of the few times you'll see\nthe syntax affectionately known as the 'turbofish': `::<>`. This\nhelps the inference algorithm understand specifically which collection\nyou're trying to collect into.\n\n# Examples\n\nBasic usage:\n\n```rust\nlet a = [1, 2, 3];\n\nlet doubled: Vec<i32> = a.iter()\n .map(|&x| x * 2)\n .collect();\n\nassert_eq!(vec![2, 4, 6], doubled);\n```\n\nNote that we needed the `: Vec<i32>` on the left-hand side. This is because\nwe could collect into, for example, a [`VecDeque<T>`](https://doc.rust-lang.org/nightly/core/iter/std/collections/struct.VecDeque.html) instead:\n\n```rust\nuse std::collections::VecDeque;\n\nlet a = [1, 2, 3];\n\nlet doubled: VecDeque<i32> = a.iter().map(|&x| x * 2).collect();\n\nassert_eq!(2, doubled[0]);\nassert_eq!(4, doubled[1]);\nassert_eq!(6, doubled[2]);\n```\n\nUsing the 'turbofish' instead of annotating `doubled`:\n\n```rust\nlet a = [1, 2, 3];\n\nlet doubled = a.iter().map(|x| x * 2).collect::<Vec<i32>>();\n\nassert_eq!(vec![2, 4, 6], doubled);\n```\n\nBecause `collect()` only cares about what you're collecting into, you can\nstill use a partial type hint, `_`, with the turbofish:\n\n```rust\nlet a = [1, 2, 3];\n\nlet doubled = a.iter().map(|x| x * 2).collect::<Vec<_>>();\n\nassert_eq!(vec![2, 4, 6], doubled);\n```\n\nUsing `collect()` to make a [`String`](https://doc.rust-lang.org/nightly/core/iter/std/string/struct.String.html):\n\n```rust\nlet chars = ['g', 'd', 'k', 'k', 'n'];\n\nlet hello: String = chars.iter()\n .map(|&x| x as u8)\n .map(|x| (x + 1) as char)\n .collect();\n\nassert_eq!(\"hello\", hello);\n```\n\nIf you have a list of [`Result<T, E>`](https://doc.rust-lang.org/nightly/core/result/enum.Result.html)s, you can use `collect()` to\nsee if any of them failed:\n\n```rust\nlet results = [Ok(1), Err(\"nope\"), Ok(3), Err(\"bad\")];\n\nlet result: Result<Vec<_>, &str> = results.iter().cloned().collect();\n\n// gives us the first error\nassert_eq!(Err(\"nope\"), result);\n\nlet results = [Ok(1), Ok(3)];\n\nlet result: Result<Vec<_>, &str> = results.iter().cloned().collect();\n\n// gives us the list of answers\nassert_eq!(Ok(vec![1, 3]), result);\n```";
@@ -258,8 +263,9 @@ impl Markdown {
} }
} }
impl Component for Markdown { #[cfg(feature = "term")]
fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { impl compositor::term::Render for Markdown {
fn render(&mut self, area: Rect, cx: &mut RenderContext<'_>) {
use tui::widgets::{Paragraph, Widget, Wrap}; use tui::widgets::{Paragraph, Widget, Wrap};
let text = self.parse(Some(&cx.editor.theme)); let text = self.parse(Some(&cx.editor.theme));
@@ -272,9 +278,20 @@ impl Component for Markdown {
vertical: 1, vertical: 1,
horizontal: 1, horizontal: 1,
}; };
par.render(area.inner(&margin), surface); par.render(area.inner(&margin), cx.surface);
}
}
#[cfg(feature = "ui")]
impl compositor::ui::Render for Markdown {}
impl Component for Markdown {
#[cfg(not(feature = "term"))]
fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
None
} }
#[cfg(feature = "term")]
fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> { fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
let padding = 2; let padding = 2;
if padding >= viewport.1 || padding >= viewport.0 { if padding >= viewport.1 || padding >= viewport.0 {

View File

@@ -1,17 +1,15 @@
use crate::{ use crate::compositor::{
compositor::{Callback, Component, Compositor, Context, EventResult}, self, Callback, Component, Compositor, Context, Event, EventResult, RenderContext,
ctrl, key, shift,
}; };
use crossterm::event::Event; use crate::{ctrl, key, shift};
use tui::{buffer::Buffer as Surface, widgets::Table};
pub use tui::widgets::{Cell, Row};
use fuzzy_matcher::skim::SkimMatcherV2 as Matcher; use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
use fuzzy_matcher::FuzzyMatcher; use fuzzy_matcher::FuzzyMatcher;
use helix_view::{graphics::Rect, Editor}; use crate::{graphics::Rect, Editor};
use tui::layout::Constraint; use tui::layout::Constraint;
pub use tui::widgets::{Cell, Row};
pub trait Item { pub trait Item {
fn label(&self) -> &str; fn label(&self) -> &str;
@@ -210,7 +208,7 @@ impl<T: Item + 'static> Component for Menu<T> {
compositor.pop(); compositor.pop();
})); }));
match event.into() { match event {
// esc or ctrl-c aborts the completion and closes the menu // esc or ctrl-c aborts the completion and closes the menu
key!(Esc) | ctrl!('c') => { key!(Esc) | ctrl!('c') => {
(self.callback_fn)(cx.editor, self.selection(), MenuEvent::Abort); (self.callback_fn)(cx.editor, self.selection(), MenuEvent::Abort);
@@ -264,8 +262,13 @@ impl<T: Item + 'static> Component for Menu<T> {
Some(self.size) Some(self.size)
} }
}
#[cfg(feature = "term")]
impl<T: Item + 'static> compositor::term::Render for Menu<T> {
fn render(&mut self, area: Rect, cx: &mut RenderContext<'_>) {
use tui::widgets::Table;
fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
let theme = &cx.editor.theme; let theme = &cx.editor.theme;
let style = theme let style = theme
.try_get("ui.menu") .try_get("ui.menu")
@@ -307,7 +310,7 @@ impl<T: Item + 'static> Component for Menu<T> {
table.render_table( table.render_table(
area, area,
surface, cx.surface,
&mut TableState { &mut TableState {
offset: scroll, offset: scroll,
selected: self.cursor, selected: self.cursor,
@@ -320,7 +323,7 @@ impl<T: Item + 'static> Component for Menu<T> {
let is_marked = i >= scroll_line && i < scroll_line + scroll_height; let is_marked = i >= scroll_line && i < scroll_line + scroll_height;
if !fits && is_marked { if !fits && is_marked {
let cell = &mut surface[(area.x + area.width - 2, area.y + i as u16)]; let cell = &mut cx.surface[(area.x + area.width - 2, area.y + i as u16)];
cell.set_symbol(""); cell.set_symbol("");
// cell.set_style(selected); // cell.set_style(selected);
// cell.set_style(if is_marked { selected } else { style }); // cell.set_style(if is_marked { selected } else { style });

View File

@@ -1,28 +1,33 @@
#[cfg(feature = "term")]
mod completion; mod completion;
pub(crate) mod editor; pub(crate) mod editor;
mod info;
mod markdown; mod markdown;
#[cfg(feature = "term")]
pub mod menu; pub mod menu;
pub mod overlay; pub mod overlay;
mod picker; mod picker;
mod popup; mod popup;
mod prompt; mod prompt;
mod spinner; mod spinner;
#[cfg(feature = "term")]
mod text; mod text;
#[cfg(feature = "term")]
pub use completion::Completion; pub use completion::Completion;
pub use editor::EditorView; pub use editor::EditorView;
pub use markdown::Markdown; pub use markdown::Markdown;
#[cfg(feature = "term")]
pub use menu::Menu; pub use menu::Menu;
pub use picker::{FileLocation, FilePicker, Picker}; pub use picker::{FileLocation, FilePicker, Picker};
pub use popup::Popup; pub use popup::Popup;
pub use prompt::{Prompt, PromptEvent}; pub use prompt::{Prompt, PromptEvent};
pub use spinner::{ProgressSpinners, Spinner}; pub use spinner::{ProgressSpinners, Spinner};
#[cfg(feature = "term")]
pub use text::Text; pub use text::Text;
use crate::{Document, Editor, View};
use helix_core::regex::Regex; use helix_core::regex::Regex;
use helix_core::regex::RegexBuilder; use helix_core::regex::RegexBuilder;
use helix_view::{Document, Editor, View};
use std::path::PathBuf; use std::path::PathBuf;
@@ -112,7 +117,7 @@ pub fn regex_prompt(
cx.push_layer(Box::new(prompt)); cx.push_layer(Box::new(prompt));
} }
pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePicker<PathBuf> { pub fn file_picker(root: PathBuf, config: &crate::editor::Config) -> FilePicker<PathBuf> {
use ignore::{types::TypesBuilder, WalkBuilder}; use ignore::{types::TypesBuilder, WalkBuilder};
use std::time::Instant; use std::time::Instant;
@@ -189,12 +194,12 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi
} }
pub mod completers { pub mod completers {
use crate::document::SCRATCH_BUFFER_NAME;
use crate::theme;
use crate::ui::prompt::Completion; use crate::ui::prompt::Completion;
use crate::{editor::Config, Editor};
use fuzzy_matcher::skim::SkimMatcherV2 as Matcher; use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
use fuzzy_matcher::FuzzyMatcher; use fuzzy_matcher::FuzzyMatcher;
use helix_view::document::SCRATCH_BUFFER_NAME;
use helix_view::theme;
use helix_view::{editor::Config, Editor};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use std::borrow::Cow; use std::borrow::Cow;
use std::cmp::Reverse; use std::cmp::Reverse;

View File

@@ -1,12 +1,11 @@
use crossterm::event::Event; use crate::{
use helix_core::Position; compositor,
use helix_view::{
graphics::{CursorKind, Rect}, graphics::{CursorKind, Rect},
Editor, Editor,
}; };
use tui::buffer::Buffer; use helix_core::Position;
use crate::compositor::{Component, Context, EventResult}; use crate::compositor::{Component, Context, Event, EventResult, RenderContext};
/// Contains a component placed in the center of the parent component /// Contains a component placed in the center of the parent component
pub struct Overlay<T> { pub struct Overlay<T> {
@@ -43,12 +42,23 @@ fn clip_rect_relative(rect: Rect, percent_horizontal: u8, percent_vertical: u8)
} }
} }
impl<T: Component + 'static> Component for Overlay<T> { #[cfg(feature = "term")]
fn render(&mut self, area: Rect, frame: &mut Buffer, ctx: &mut Context) { impl<T: Component + 'static> compositor::term::Render for Overlay<T> {
fn render(&mut self, area: Rect, ctx: &mut RenderContext<'_>) {
let dimensions = (self.calc_child_size)(area); let dimensions = (self.calc_child_size)(area);
self.content.render(dimensions, frame, ctx) self.content.render(dimensions, ctx)
} }
fn cursor(&self, area: Rect, ctx: &Editor) -> (Option<Position>, CursorKind) {
let dimensions = (self.calc_child_size)(area);
self.content.cursor(dimensions, ctx)
}
}
#[cfg(feature = "ui")]
impl<T: Component + 'static> compositor::ui::Render for Overlay<T> {}
impl<T: Component + 'static> Component for Overlay<T> {
fn required_size(&mut self, (width, height): (u16, u16)) -> Option<(u16, u16)> { fn required_size(&mut self, (width, height): (u16, u16)) -> Option<(u16, u16)> {
let area = Rect { let area = Rect {
x: 0, x: 0,
@@ -65,9 +75,4 @@ impl<T: Component + 'static> Component for Overlay<T> {
fn handle_event(&mut self, event: Event, ctx: &mut Context) -> EventResult { fn handle_event(&mut self, event: Event, ctx: &mut Context) -> EventResult {
self.content.handle_event(event, ctx) self.content.handle_event(event, ctx)
} }
fn cursor(&self, area: Rect, ctx: &Editor) -> (Option<Position>, CursorKind) {
let dimensions = (self.calc_child_size)(area);
self.content.cursor(dimensions, ctx)
}
} }

View File

@@ -1,17 +1,11 @@
use crate::compositor::{self, Component, Compositor, Context, Event, EventResult, RenderContext};
use crate::{ use crate::{
compositor::{Component, Compositor, Context, EventResult},
ctrl, key, shift, ctrl, key, shift,
ui::{self, EditorView}, ui::{self, EditorView},
}; };
use crossterm::event::Event;
use tui::{
buffer::Buffer as Surface,
widgets::{Block, BorderType, Borders},
};
use fuzzy_matcher::skim::SkimMatcherV2 as Matcher; use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
use fuzzy_matcher::FuzzyMatcher; use fuzzy_matcher::FuzzyMatcher;
use tui::widgets::Widget;
use std::time::Instant; use std::time::Instant;
use std::{ use std::{
@@ -23,12 +17,12 @@ use std::{
}; };
use crate::ui::{Prompt, PromptEvent}; use crate::ui::{Prompt, PromptEvent};
use helix_core::{movement::Direction, Position}; use crate::{
use helix_view::{
editor::Action, editor::Action,
graphics::{Color, CursorKind, Margin, Modifier, Rect, Style}, graphics::{Color, CursorKind, Margin, Modifier, Rect, Style},
Document, Editor, Document, Editor,
}; };
use helix_core::{movement::Direction, Position};
pub const MIN_AREA_WIDTH_FOR_PREVIEW: u16 = 72; pub const MIN_AREA_WIDTH_FOR_PREVIEW: u16 = 72;
/// Biggest file size to preview in bytes /// Biggest file size to preview in bytes
@@ -163,8 +157,11 @@ impl<T> FilePicker<T> {
} }
} }
impl<T: 'static> Component for FilePicker<T> { #[cfg(feature = "term")]
fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { impl<T: 'static> compositor::term::Render for FilePicker<T> {
fn render(&mut self, area: Rect, cx: &mut RenderContext<'_>) {
use tui::widgets::Widget;
use tui::widgets::{Block, Borders};
// +---------+ +---------+ // +---------+ +---------+
// |prompt | |preview | // |prompt | |preview |
// +---------+ | | // +---------+ | |
@@ -177,7 +174,7 @@ impl<T: 'static> Component for FilePicker<T> {
// clear area // clear area
let background = cx.editor.theme.get("ui.background"); let background = cx.editor.theme.get("ui.background");
let text = cx.editor.theme.get("ui.text"); let text = cx.editor.theme.get("ui.text");
surface.clear_with(area, background); cx.surface.clear_with(area, background);
let picker_width = if render_preview { let picker_width = if render_preview {
area.width / 2 area.width / 2
@@ -186,7 +183,7 @@ impl<T: 'static> Component for FilePicker<T> {
}; };
let picker_area = area.with_width(picker_width); let picker_area = area.with_width(picker_width);
self.picker.render(picker_area, surface, cx); self.picker.render(picker_area, cx);
if !render_preview { if !render_preview {
return; return;
@@ -205,7 +202,7 @@ impl<T: 'static> Component for FilePicker<T> {
horizontal: 1, horizontal: 1,
}; };
let inner = inner.inner(&margin); let inner = inner.inner(&margin);
block.render(preview_area, surface); block.render(preview_area, cx.surface);
if let Some((path, range)) = self.current_file(cx.editor) { if let Some((path, range)) = self.current_file(cx.editor) {
let preview = self.get_preview(&path, cx.editor); let preview = self.get_preview(&path, cx.editor);
@@ -215,7 +212,8 @@ impl<T: 'static> Component for FilePicker<T> {
let alt_text = preview.placeholder(); let alt_text = preview.placeholder();
let x = inner.x + inner.width.saturating_sub(alt_text.len() as u16) / 2; let x = inner.x + inner.width.saturating_sub(alt_text.len() as u16) / 2;
let y = inner.y + inner.height / 2; let y = inner.y + inner.height / 2;
surface.set_stringn(x, y, alt_text, inner.width as usize, text); cx.surface
.set_stringn(x, y, alt_text, inner.width as usize, text);
return; return;
} }
}; };
@@ -237,7 +235,7 @@ impl<T: 'static> Component for FilePicker<T> {
doc, doc,
offset, offset,
inner, inner,
surface, cx.surface,
&cx.editor.theme, &cx.editor.theme,
highlights, highlights,
&cx.editor.config().whitespace, &cx.editor.config().whitespace,
@@ -246,7 +244,7 @@ impl<T: 'static> Component for FilePicker<T> {
// highlight the line // highlight the line
if let Some((start, end)) = range { if let Some((start, end)) = range {
let offset = start.saturating_sub(first_line) as u16; let offset = start.saturating_sub(first_line) as u16;
surface.set_style( cx.surface.set_style(
Rect::new( Rect::new(
inner.x, inner.x,
inner.y + offset, inner.y + offset,
@@ -263,15 +261,20 @@ impl<T: 'static> Component for FilePicker<T> {
} }
} }
fn cursor(&self, area: Rect, ctx: &Editor) -> (Option<Position>, CursorKind) {
self.picker.cursor(area, ctx)
}
}
#[cfg(feature = "ui")]
impl<T: 'static> compositor::ui::Render for FilePicker<T> {}
impl<T: 'static> Component for FilePicker<T> {
fn handle_event(&mut self, event: Event, ctx: &mut Context) -> EventResult { fn handle_event(&mut self, event: Event, ctx: &mut Context) -> EventResult {
// TODO: keybinds for scrolling preview // TODO: keybinds for scrolling preview
self.picker.handle_event(event, ctx) self.picker.handle_event(event, ctx)
} }
fn cursor(&self, area: Rect, ctx: &Editor) -> (Option<Position>, CursorKind) {
self.picker.cursor(area, ctx)
}
fn required_size(&mut self, (width, height): (u16, u16)) -> Option<(u16, u16)> { fn required_size(&mut self, (width, height): (u16, u16)) -> Option<(u16, u16)> {
let picker_width = if width > MIN_AREA_WIDTH_FOR_PREVIEW { let picker_width = if width > MIN_AREA_WIDTH_FOR_PREVIEW {
width / 2 width / 2
@@ -498,7 +501,7 @@ impl<T: 'static> Component for Picker<T> {
compositor.last_picker = compositor.pop(); compositor.last_picker = compositor.pop();
}))); })));
match key_event.into() { match key_event {
shift!(Tab) | key!(Up) | ctrl!('p') => { shift!(Tab) | key!(Up) | ctrl!('p') => {
self.move_by(1, Direction::Backward); self.move_by(1, Direction::Backward);
} }
@@ -551,8 +554,13 @@ impl<T: 'static> Component for Picker<T> {
EventResult::Consumed(None) EventResult::Consumed(None)
} }
}
fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { #[cfg(feature = "term")]
impl<T: 'static> compositor::term::Render for Picker<T> {
fn render(&mut self, area: Rect, cx: &mut RenderContext<'_>) {
use tui::widgets::Widget;
use tui::widgets::{Block, BorderType, Borders};
let text_style = cx.editor.theme.get("ui.text"); let text_style = cx.editor.theme.get("ui.text");
let selected = cx.editor.theme.get("ui.text.focus"); let selected = cx.editor.theme.get("ui.text.focus");
let highlighted = cx.editor.theme.get("special").add_modifier(Modifier::BOLD); let highlighted = cx.editor.theme.get("special").add_modifier(Modifier::BOLD);
@@ -560,7 +568,7 @@ impl<T: 'static> Component for Picker<T> {
// -- Render the frame: // -- Render the frame:
// clear area // clear area
let background = cx.editor.theme.get("ui.background"); let background = cx.editor.theme.get("ui.background");
surface.clear_with(area, background); cx.surface.clear_with(area, background);
// don't like this but the lifetime sucks // don't like this but the lifetime sucks
let block = Block::default().borders(Borders::ALL); let block = Block::default().borders(Borders::ALL);
@@ -568,14 +576,14 @@ impl<T: 'static> Component for Picker<T> {
// calculate the inner area inside the box // calculate the inner area inside the box
let inner = block.inner(area); let inner = block.inner(area);
block.render(area, surface); block.render(area, cx.surface);
// -- Render the input bar: // -- Render the input bar:
let area = inner.clip_left(1).with_height(1); let area = inner.clip_left(1).with_height(1);
let count = format!("{}/{}", self.matches.len(), self.options.len()); let count = format!("{}/{}", self.matches.len(), self.options.len());
surface.set_stringn( cx.surface.set_stringn(
(area.x + area.width).saturating_sub(count.len() as u16 + 1), (area.x + area.width).saturating_sub(count.len() as u16 + 1),
area.y, area.y,
&count, &count,
@@ -583,13 +591,13 @@ impl<T: 'static> Component for Picker<T> {
text_style, text_style,
); );
self.prompt.render(area, surface, cx); self.prompt.render(area, cx);
// -- Separator // -- Separator
let sep_style = Style::default().fg(Color::Rgb(90, 89, 119)); let sep_style = Style::default().fg(Color::Rgb(90, 89, 119));
let borders = BorderType::line_symbols(BorderType::Plain); let borders = BorderType::line_symbols(BorderType::Plain);
for x in inner.left()..inner.right() { for x in inner.left()..inner.right() {
if let Some(cell) = surface.get_mut(x, inner.y + 1) { if let Some(cell) = cx.surface.get_mut(x, inner.y + 1) {
cell.set_symbol(borders.horizontal).set_style(sep_style); cell.set_symbol(borders.horizontal).set_style(sep_style);
} }
} }
@@ -610,7 +618,8 @@ impl<T: 'static> Component for Picker<T> {
for (i, (_index, option)) in files.take(rows as usize).enumerate() { for (i, (_index, option)) in files.take(rows as usize).enumerate() {
let is_active = i == (self.cursor - offset); let is_active = i == (self.cursor - offset);
if is_active { if is_active {
surface.set_string(inner.x.saturating_sub(2), inner.y + i as u16, ">", selected); cx.surface
.set_string(inner.x.saturating_sub(2), inner.y + i as u16, ">", selected);
} }
let formatted = (self.format_fn)(option); let formatted = (self.format_fn)(option);
@@ -620,7 +629,7 @@ impl<T: 'static> Component for Picker<T> {
.fuzzy_indices(&formatted, self.prompt.line()) .fuzzy_indices(&formatted, self.prompt.line())
.unwrap_or_default(); .unwrap_or_default();
surface.set_string_truncated( cx.surface.set_string_truncated(
inner.x, inner.x,
inner.y + i as u16, inner.y + i as u16,
&formatted, &formatted,
@@ -641,6 +650,7 @@ impl<T: 'static> Component for Picker<T> {
} }
fn cursor(&self, area: Rect, editor: &Editor) -> (Option<Position>, CursorKind) { fn cursor(&self, area: Rect, editor: &Editor) -> (Option<Position>, CursorKind) {
use tui::widgets::{Block, Borders};
let block = Block::default().borders(Borders::ALL); let block = Block::default().borders(Borders::ALL);
// calculate the inner area inside the box // calculate the inner area inside the box
let inner = block.inner(area); let inner = block.inner(area);
@@ -651,3 +661,6 @@ impl<T: 'static> Component for Picker<T> {
self.prompt.cursor(area, editor) self.prompt.cursor(area, editor)
} }
} }
#[cfg(feature = "ui")]
impl<T: 'static> compositor::ui::Render for Picker<T> {}

View File

@@ -1,12 +1,11 @@
use crate::{ use crate::compositor::{self, Callback, Component, Context, Event, EventResult, RenderContext};
compositor::{Callback, Component, Context, EventResult}, use crate::{ctrl, key};
ctrl, key,
};
use crossterm::event::Event;
use tui::buffer::Buffer as Surface;
use crate::{
graphics::{Margin, Rect},
Editor,
};
use helix_core::Position; use helix_core::Position;
use helix_view::graphics::{Margin, Rect};
// TODO: share logic with Menu, it's essentially Popup(render_fn), but render fn needs to return // TODO: share logic with Menu, it's essentially Popup(render_fn), but render fn needs to return
// a width/height hint. maybe Popup(Box<Component>) // a width/height hint. maybe Popup(Box<Component>)
@@ -53,10 +52,10 @@ impl<T: Component> Popup<T> {
self self
} }
pub fn get_rel_position(&mut self, viewport: Rect, cx: &Context) -> (u16, u16) { pub fn get_rel_position(&mut self, viewport: Rect, editor: &Editor) -> (u16, u16) {
let position = self let position = self
.position .position
.get_or_insert_with(|| cx.editor.cursor().0.unwrap_or_default()); .get_or_insert_with(|| editor.cursor().0.unwrap_or_default());
let (width, height) = self.size; let (width, height) = self.size;
@@ -122,7 +121,7 @@ impl<T: Component> Component for Popup<T> {
compositor.pop(); compositor.pop();
}); });
match key.into() { match key {
// esc or ctrl-c aborts the completion and closes the menu // esc or ctrl-c aborts the completion and closes the menu
key!(Esc) | ctrl!('c') => { key!(Esc) | ctrl!('c') => {
let _ = self.contents.handle_event(event, cx); let _ = self.contents.handle_event(event, cx);
@@ -176,26 +175,32 @@ impl<T: Component> Component for Popup<T> {
Some(self.size) Some(self.size)
} }
fn render(&mut self, viewport: Rect, surface: &mut Surface, cx: &mut Context) { fn id(&self) -> Option<&'static str> {
Some(self.id)
}
}
#[cfg(feature = "term")]
impl<T: Component> compositor::term::Render for Popup<T> {
fn render(&mut self, viewport: Rect, cx: &mut RenderContext<'_>) {
// trigger required_size so we recalculate if the child changed // trigger required_size so we recalculate if the child changed
self.required_size((viewport.width, viewport.height)); self.required_size((viewport.width, viewport.height));
cx.scroll = Some(self.scroll); cx.scroll = Some(self.scroll);
let (rel_x, rel_y) = self.get_rel_position(viewport, cx); let (rel_x, rel_y) = self.get_rel_position(viewport, cx.editor);
// clip to viewport // clip to viewport
let area = viewport.intersection(Rect::new(rel_x, rel_y, self.size.0, self.size.1)); let area = viewport.intersection(Rect::new(rel_x, rel_y, self.size.0, self.size.1));
// clear area // clear area
let background = cx.editor.theme.get("ui.popup"); let background = cx.editor.theme.get("ui.popup");
surface.clear_with(area, background); cx.surface.clear_with(area, background);
let inner = area.inner(&self.margin); let inner = area.inner(&self.margin);
self.contents.render(inner, surface, cx); self.contents.render(inner, cx);
}
fn id(&self) -> Option<&'static str> {
Some(self.id)
} }
} }
#[cfg(feature = "ui")]
impl<T: Component> compositor::ui::Render for Popup<T> {}

View File

@@ -1,19 +1,16 @@
use crate::compositor::{Component, Compositor, Context, EventResult}; use crate::compositor::{self, Component, Compositor, Context, Event, EventResult, RenderContext};
use crate::input::KeyEvent;
use crate::keyboard::KeyCode;
use crate::{alt, ctrl, key, shift, ui}; use crate::{alt, ctrl, key, shift, ui};
use crossterm::event::Event;
use helix_view::input::KeyEvent;
use helix_view::keyboard::KeyCode;
use std::{borrow::Cow, ops::RangeFrom}; use std::{borrow::Cow, ops::RangeFrom};
use tui::buffer::Buffer as Surface;
use tui::widgets::{Block, Borders, Widget};
use helix_core::{ use crate::{
unicode::segmentation::GraphemeCursor, unicode::width::UnicodeWidthStr, Position,
};
use helix_view::{
graphics::{CursorKind, Margin, Rect}, graphics::{CursorKind, Margin, Rect},
Editor, Editor,
}; };
use helix_core::{
unicode::segmentation::GraphemeCursor, unicode::width::UnicodeWidthStr, Position,
};
pub type Completion = (RangeFrom<usize>, Cow<'static, str>); pub type Completion = (RangeFrom<usize>, Cow<'static, str>);
@@ -326,8 +323,11 @@ impl Prompt {
const BASE_WIDTH: u16 = 30; const BASE_WIDTH: u16 = 30;
impl Prompt { #[cfg(feature = "term")]
pub fn render_prompt(&self, area: Rect, surface: &mut Surface, cx: &mut Context) { impl compositor::term::Render for Prompt {
fn render(&mut self, area: Rect, cx: &mut RenderContext<'_>) {
use tui::widgets::{Block, Borders, Widget};
let theme = &cx.editor.theme; let theme = &cx.editor.theme;
let prompt_color = theme.get("ui.text"); let prompt_color = theme.get("ui.text");
let completion_color = theme.get("ui.statusline"); let completion_color = theme.get("ui.statusline");
@@ -367,7 +367,7 @@ impl Prompt {
.map(|selection| selection / items * items) .map(|selection| selection / items * items)
.unwrap_or_default(); .unwrap_or_default();
surface.clear_with(area, background); cx.surface.clear_with(area, background);
let mut row = 0; let mut row = 0;
let mut col = 0; let mut col = 0;
@@ -380,7 +380,7 @@ impl Prompt {
} else { } else {
completion_color completion_color
}; };
surface.set_stringn( cx.surface.set_stringn(
area.x + col * (1 + col_width), area.x + col * (1 + col_width),
area.y + row, area.y + row,
&completion, &completion,
@@ -413,7 +413,7 @@ impl Prompt {
)); ));
let background = theme.get("ui.help"); let background = theme.get("ui.help");
surface.clear_with(area, background); cx.surface.clear_with(area, background);
let block = Block::default() let block = Block::default()
// .title(self.title.as_str()) // .title(self.title.as_str())
@@ -425,22 +425,39 @@ impl Prompt {
horizontal: 1, horizontal: 1,
}); });
block.render(area, surface); block.render(area, cx.surface);
text.render(inner, surface, cx); text.render(inner, cx);
} }
let line = area.height - 1; let line = area.height - 1;
// render buffer text // render buffer text
surface.set_string(area.x, area.y + line, &self.prompt, prompt_color); cx.surface
surface.set_string( .set_string(area.x, area.y + line, &self.prompt, prompt_color);
cx.surface.set_string(
area.x + self.prompt.len() as u16, area.x + self.prompt.len() as u16,
area.y + line, area.y + line,
&self.line, &self.line,
prompt_color, prompt_color,
); );
} }
fn cursor(&self, area: Rect, _editor: &Editor) -> (Option<Position>, CursorKind) {
let line = area.height as usize - 1;
(
Some(Position::new(
area.y as usize + line,
area.x as usize
+ self.prompt.len()
+ UnicodeWidthStr::width(&self.line[..self.cursor]),
)),
CursorKind::Block,
)
}
} }
#[cfg(feature = "ui")]
impl compositor::ui::Render for Prompt {}
impl Component for Prompt { impl Component for Prompt {
fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult { fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
let event = match event { let event = match event {
@@ -454,7 +471,7 @@ impl Component for Prompt {
compositor.pop(); compositor.pop();
}))); })));
match event.into() { match event {
ctrl!('c') | key!(Esc) => { ctrl!('c') | key!(Esc) => {
(self.callback_fn)(cx, &self.line, PromptEvent::Abort); (self.callback_fn)(cx, &self.line, PromptEvent::Abort);
return close_fn; return close_fn;
@@ -546,21 +563,4 @@ impl Component for Prompt {
EventResult::Consumed(None) EventResult::Consumed(None)
} }
fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
self.render_prompt(area, surface, cx)
}
fn cursor(&self, area: Rect, _editor: &Editor) -> (Option<Position>, CursorKind) {
let line = area.height as usize - 1;
(
Some(Position::new(
area.y as usize + line,
area.x as usize
+ self.prompt.len()
+ UnicodeWidthStr::width(&self.line[..self.cursor]),
)),
CursorKind::Block,
)
}
} }

View File

@@ -1,7 +1,5 @@
use crate::compositor::{Component, Context}; use crate::compositor::{self, Component, RenderContext};
use tui::buffer::Buffer as Surface; use crate::graphics::Rect;
use helix_view::graphics::Rect;
pub struct Text { pub struct Text {
pub(crate) contents: tui::text::Text<'static>, pub(crate) contents: tui::text::Text<'static>,
@@ -29,16 +27,19 @@ impl From<tui::text::Text<'static>> for Text {
} }
} }
impl Component for Text { #[cfg(feature = "term")]
fn render(&mut self, area: Rect, surface: &mut Surface, _cx: &mut Context) { impl compositor::term::Render for Text {
fn render(&mut self, area: Rect, cx: &mut RenderContext<'_>) {
use tui::widgets::{Paragraph, Widget, Wrap}; use tui::widgets::{Paragraph, Widget, Wrap};
let par = Paragraph::new(self.contents.clone()).wrap(Wrap { trim: false }); let par = Paragraph::new(self.contents.clone()).wrap(Wrap { trim: false });
// .scroll(x, y) offsets // .scroll(x, y) offsets
par.render(area, surface); par.render(area, cx.surface);
} }
}
impl Component for Text {
fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> { fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
if viewport != self.viewport { if viewport != self.viewport {
let width = std::cmp::min(self.contents.width() as u16, viewport.0); let width = std::cmp::min(self.contents.width() as u16, viewport.0);

View File

@@ -1,3 +1,4 @@
[toolchain] [toolchain]
channel = "stable" channel = "stable"
components = ["rustfmt", "rust-src"] components = ["rustfmt", "rust-src"]
targets = [ "wasm32-unknown-unknown" ]