mirror of
https://github.com/helix-editor/helix.git
synced 2025-10-06 00:13:28 +02:00
Compare commits
34 Commits
tree-house
...
gui
Author | SHA1 | Date | |
---|---|---|---|
|
86b1d970c2 | ||
|
ea6aa0795b | ||
|
e3e6d672bc | ||
|
46072791c1 | ||
|
89a0be10e8 | ||
|
e60e9099ca | ||
|
fa4dfba043 | ||
|
8dc9ad7997 | ||
|
9438430e98 | ||
|
3b2b9e102d | ||
|
d151567192 | ||
|
afcb78a538 | ||
|
842a5fc979 | ||
|
842cd2cc13 | ||
|
febc7ee0fa | ||
|
eadb2eaad1 | ||
|
1aa2b027d7 | ||
|
11b8f068da | ||
|
18381fcbc8 | ||
|
9dd9515a8d | ||
|
61365dfbf3 | ||
|
e0f9d86f49 | ||
|
57d4a9ba21 | ||
|
14f987807d | ||
|
c3f9d3641c | ||
|
6f8bf67fa6 | ||
|
756b001030 | ||
|
8694d60ab3 | ||
|
dcd1e9eaa3 | ||
|
d7b1c40452 | ||
|
3c0e11d69e | ||
|
7622643117 | ||
|
649a17720a | ||
|
cb5b12725e |
1540
Cargo.lock
generated
1540
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,8 @@ members = [
|
||||
"helix-view",
|
||||
"helix-term",
|
||||
"helix-tui",
|
||||
"helix-graphics",
|
||||
"helix-ui",
|
||||
"helix-lsp",
|
||||
"helix-dap",
|
||||
"helix-loader",
|
||||
@@ -11,9 +13,12 @@ members = [
|
||||
]
|
||||
|
||||
default-members = [
|
||||
"helix-term"
|
||||
"helix-term",
|
||||
"helix-ui"
|
||||
]
|
||||
|
||||
resolver = "2"
|
||||
|
||||
[profile.dev]
|
||||
split-debuginfo = "unpacked"
|
||||
|
||||
|
31
flake.lock
generated
31
flake.lock
generated
@@ -25,11 +25,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1646667754,
|
||||
"narHash": "sha256-LahZHvCC3UVzGQ55iWDRZkuDssXl1rYgqgScrPV9S38=",
|
||||
"lastModified": 1650201426,
|
||||
"narHash": "sha256-u43Xf03ImFJWKLHddtjOpCJCHbuM0SQbb6FKR5NuFhk=",
|
||||
"owner": "numtide",
|
||||
"repo": "devshell",
|
||||
"rev": "59fbe1dfc0de8c3332957c16998a7d16dff365d8",
|
||||
"rev": "cb76bc75a0ee81f2d8676fe681f268c48dbd380e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -75,16 +75,15 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1646710334,
|
||||
"narHash": "sha256-eLBcDgcbOUfeH4k6SEW5a5v0PTp2KNCn+5ZXIoWGYww=",
|
||||
"lastModified": 1650199368,
|
||||
"narHash": "sha256-35wZQDyohH6NI8ifT124p+TPDiwfRnQZkbUXc1yjGIE=",
|
||||
"owner": "nix-community",
|
||||
"repo": "dream2nix",
|
||||
"rev": "5dcfbfd3b60ce0208b894c1bdea00e2bdf80ca6a",
|
||||
"rev": "feb7eabd0c1b24d47ea6ecd38b737319e75fd513",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"ref": "main",
|
||||
"repo": "dream2nix",
|
||||
"type": "github"
|
||||
}
|
||||
@@ -131,11 +130,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1646766572,
|
||||
"narHash": "sha256-DV3+zxvAIKsMHsHedJKYFsracvFyLKpFQqurUBR86oY=",
|
||||
"lastModified": 1650262356,
|
||||
"narHash": "sha256-1ko5YhiLeFt9SjKvzq31IGrBJkHITNJc+08rod4P6lk=",
|
||||
"owner": "yusdacra",
|
||||
"repo": "nix-cargo-integration",
|
||||
"rev": "3a3f47f43ba486b7554164a698c8dfc5a38624ce",
|
||||
"rev": "924aa1e39bbf1182cdf0dd6dfdb1d7b4a047ef6f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -146,11 +145,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1646497237,
|
||||
"narHash": "sha256-Ccpot1h/rV8MgcngDp5OrdmLTMaUTbStZTR5/sI7zW0=",
|
||||
"lastModified": 1650161686,
|
||||
"narHash": "sha256-70ZWAlOQ9nAZ08OU6WY7n4Ij2kOO199dLfNlvO/+pf8=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "062a0c5437b68f950b081bbfc8a699d57a4ee026",
|
||||
"rev": "1ffba9f2f683063c2b14c9f4d12c55ad5f4ed887",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -175,11 +174,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1646792695,
|
||||
"narHash": "sha256-2drCXIKIQnJMlTZbcCfuHZAh+iPcdlRkCqtZnA6MHLY=",
|
||||
"lastModified": 1650336226,
|
||||
"narHash": "sha256-A68t/BM3JPXUDFx9JGBk24euXvsaIZuPL28+hX5TmwA=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "7f599870402c8d2a5806086c8ee0f2d92b175c54",
|
||||
"rev": "8cd3024e5b011218308eeee35c4839601af458f8",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
11
flake.nix
11
flake.nix
@@ -14,7 +14,7 @@
|
||||
};
|
||||
};
|
||||
|
||||
outputs = inputs@{ nixCargoIntegration, ... }:
|
||||
outputs = inputs@{ nixpkgs, nixCargoIntegration, ... }:
|
||||
nixCargoIntegration.lib.makeOutputs {
|
||||
root = ./.;
|
||||
renameOutputs = { "helix-term" = "helix"; };
|
||||
@@ -53,11 +53,16 @@
|
||||
};
|
||||
};
|
||||
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 ++ [
|
||||
{ name = "HELIX_RUNTIME"; eval = "$PWD/runtime"; }
|
||||
{ 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
|
||||
]); }
|
||||
];
|
||||
};
|
||||
};
|
||||
|
@@ -24,7 +24,7 @@ unicode-width = "0.1"
|
||||
unicode-general-category = "0.5"
|
||||
# slab = "0.4.2"
|
||||
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"
|
||||
arc-swap = "1"
|
||||
regex = "1"
|
||||
|
@@ -51,7 +51,7 @@ pub fn find_root(root: Option<&str>, root_markers: &[String]) -> Option<std::pat
|
||||
.cloned()
|
||||
}
|
||||
|
||||
pub use ropey::{Rope, RopeBuilder, RopeSlice};
|
||||
pub use ropey::{str_utils, Rope, RopeBuilder, RopeSlice};
|
||||
|
||||
// pub use tendril::StrTendril as Tendril;
|
||||
pub use smartstring::SmartString;
|
||||
|
23
helix-graphics/Cargo.toml
Normal file
23
helix-graphics/Cargo.toml
Normal 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" }
|
@@ -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)]
|
||||
|
@@ -16,6 +16,9 @@ build = true
|
||||
app = true
|
||||
|
||||
[features]
|
||||
default = ["dap", "lsp"]
|
||||
dap = ["helix-dap", "helix-view/dap"]
|
||||
lsp = ["helix-lsp", "helix-view/lsp"]
|
||||
unicode-lines = ["helix-core/unicode-lines"]
|
||||
|
||||
[[bin]]
|
||||
@@ -24,9 +27,9 @@ path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
helix-core = { version = "0.6", path = "../helix-core" }
|
||||
helix-view = { version = "0.6", path = "../helix-view" }
|
||||
helix-lsp = { version = "0.6", path = "../helix-lsp" }
|
||||
helix-dap = { version = "0.6", path = "../helix-dap" }
|
||||
helix-view = { version = "0.6", path = "../helix-view", features = ["term"] }
|
||||
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" }
|
||||
|
||||
anyhow = "1"
|
||||
@@ -48,27 +51,12 @@ fern = "0.6"
|
||||
chrono = { version = "0.4", default-features = false, features = ["clock"] }
|
||||
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
|
||||
toml = "0.5"
|
||||
|
||||
serde_json = "1.0"
|
||||
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
|
||||
signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] }
|
||||
|
||||
|
@@ -3,21 +3,30 @@ use helix_core::{
|
||||
config::{default_syntax_loader, user_syntax_loader},
|
||||
pos_at_coords, syntax, Selection,
|
||||
};
|
||||
|
||||
#[cfg(feature = "lsp")]
|
||||
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 crate::{
|
||||
args::Args,
|
||||
commands::apply_workspace_edit,
|
||||
compositor::Compositor,
|
||||
config::Config,
|
||||
job::Jobs,
|
||||
use helix_view::{
|
||||
align_view, editor::ConfigEvent, graphics::Rect, theme, true_color, Align, Editor,
|
||||
};
|
||||
|
||||
use crate::{args::Args, config::Config};
|
||||
|
||||
use helix_view::{
|
||||
keymap::Keymaps,
|
||||
ui::{self, overlay::overlayed},
|
||||
};
|
||||
|
||||
use log::{error, warn};
|
||||
use helix_view::{
|
||||
compositor::{Compositor, Event},
|
||||
job::Jobs,
|
||||
};
|
||||
|
||||
use std::{
|
||||
io::{stdin, stdout, Write},
|
||||
sync::Arc,
|
||||
@@ -27,7 +36,7 @@ use std::{
|
||||
use anyhow::Error;
|
||||
|
||||
use crossterm::{
|
||||
event::{DisableMouseCapture, EnableMouseCapture, Event, EventStream},
|
||||
event::{DisableMouseCapture, EnableMouseCapture, Event as CrosstermEvent, EventStream},
|
||||
execute, terminal,
|
||||
tty::IsTty,
|
||||
};
|
||||
@@ -39,8 +48,12 @@ use {
|
||||
#[cfg(windows)]
|
||||
type Signals = futures_util::stream::Empty<()>;
|
||||
|
||||
use tui::backend::{Backend, CrosstermBackend};
|
||||
type Terminal = tui::terminal::Terminal<CrosstermBackend<std::io::Stdout>>;
|
||||
|
||||
pub struct Application {
|
||||
compositor: Compositor,
|
||||
terminal: Terminal,
|
||||
editor: Editor,
|
||||
|
||||
config: Arc<ArcSwap<Config>>,
|
||||
@@ -52,6 +65,7 @@ pub struct Application {
|
||||
|
||||
signals: Signals,
|
||||
jobs: Jobs,
|
||||
#[cfg(feature = "lsp")]
|
||||
lsp_progress: LspProgressMap,
|
||||
}
|
||||
|
||||
@@ -66,7 +80,7 @@ impl Application {
|
||||
|
||||
let config = match std::fs::read_to_string(config_dir.join("config.toml")) {
|
||||
Ok(config) => toml::from_str(&config)
|
||||
.map(crate::keymap::merge_keys)
|
||||
.map(crate::config::merge_keys)
|
||||
.unwrap_or_else(|err| {
|
||||
eprintln!("Bad config: {}", err);
|
||||
eprintln!("Press <ENTER> to continue with default config");
|
||||
@@ -84,7 +98,7 @@ impl Application {
|
||||
&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
|
||||
.theme
|
||||
.as_ref()
|
||||
@@ -116,10 +130,13 @@ impl Application {
|
||||
});
|
||||
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 mut editor = Editor::new(
|
||||
compositor.size(),
|
||||
area,
|
||||
theme_loader.clone(),
|
||||
syn_loader.clone(),
|
||||
Box::new(Map::new(Arc::clone(&config), |config: &Config| {
|
||||
@@ -190,6 +207,7 @@ impl Application {
|
||||
|
||||
let app = Self {
|
||||
compositor,
|
||||
terminal,
|
||||
editor,
|
||||
|
||||
config,
|
||||
@@ -199,6 +217,8 @@ impl Application {
|
||||
|
||||
signals,
|
||||
jobs: Jobs::new(),
|
||||
|
||||
#[cfg(feature = "lsp")]
|
||||
lsp_progress: LspProgressMap::new(),
|
||||
};
|
||||
|
||||
@@ -206,13 +226,28 @@ impl Application {
|
||||
}
|
||||
|
||||
fn render(&mut self) {
|
||||
let mut cx = crate::compositor::Context {
|
||||
editor: &mut self.editor,
|
||||
jobs: &mut self.jobs,
|
||||
let area = self
|
||||
.terminal
|
||||
.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,
|
||||
};
|
||||
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) {
|
||||
@@ -239,16 +274,21 @@ impl Application {
|
||||
self.handle_signals(signal).await;
|
||||
}
|
||||
Some((id, call)) = self.editor.language_servers.incoming.next() => {
|
||||
#[cfg(feature = "lsp")]
|
||||
self.handle_language_server_message(call, id).await;
|
||||
// limit render calls for fast language server messages
|
||||
#[cfg(feature = "lsp")]
|
||||
let last = self.editor.language_servers.incoming.is_empty();
|
||||
#[cfg(feature = "lsp")]
|
||||
if last || last_render.elapsed() > deadline {
|
||||
self.render();
|
||||
last_render = Instant::now();
|
||||
}
|
||||
}
|
||||
Some(payload) = self.editor.debugger_events.next() => {
|
||||
#[cfg(feature = "dap")]
|
||||
let needs_render = self.editor.handle_debugger_message(payload).await;
|
||||
#[cfg(feature = "dap")]
|
||||
if needs_render {
|
||||
self.render();
|
||||
}
|
||||
@@ -320,7 +360,7 @@ impl Application {
|
||||
}
|
||||
|
||||
fn true_color(&self) -> bool {
|
||||
self.config.load().editor.true_color || crate::true_color()
|
||||
self.config.load().editor.true_color || true_color()
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
@@ -329,19 +369,19 @@ impl Application {
|
||||
|
||||
#[cfg(not(windows))]
|
||||
pub async fn handle_signals(&mut self, signal: i32) {
|
||||
use helix_view::graphics::Rect;
|
||||
match signal {
|
||||
signal::SIGTSTP => {
|
||||
self.compositor.save_cursor();
|
||||
self.restore_term().unwrap();
|
||||
low_level::emulate_default_handler(signal::SIGTSTP).unwrap();
|
||||
}
|
||||
signal::SIGCONT => {
|
||||
self.claim_term().await.unwrap();
|
||||
// redraw the terminal
|
||||
let Rect { width, height, .. } = self.compositor.size();
|
||||
self.compositor.resize(width, height);
|
||||
self.compositor.load_cursor();
|
||||
let area = self
|
||||
.terminal
|
||||
.size()
|
||||
.expect("Unable to determine terminal size");
|
||||
self.terminal.resize(area);
|
||||
self.render();
|
||||
}
|
||||
_ => unreachable!(),
|
||||
@@ -349,37 +389,44 @@ impl Application {
|
||||
}
|
||||
|
||||
pub fn handle_idle_timeout(&mut self) {
|
||||
use crate::compositor::EventResult;
|
||||
use helix_view::compositor::EventResult;
|
||||
let editor_view = self
|
||||
.compositor
|
||||
.find::<ui::EditorView>()
|
||||
.expect("expected at least one EditorView");
|
||||
|
||||
let mut cx = crate::compositor::Context {
|
||||
let mut cx = helix_view::compositor::Context {
|
||||
editor: &mut self.editor,
|
||||
jobs: &mut self.jobs,
|
||||
scroll: None,
|
||||
};
|
||||
if let EventResult::Consumed(_) = editor_view.handle_idle_timeout(&mut cx) {
|
||||
self.render();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_terminal_events(&mut self, event: Option<Result<Event, crossterm::ErrorKind>>) {
|
||||
let mut cx = crate::compositor::Context {
|
||||
pub fn handle_terminal_events(
|
||||
&mut self,
|
||||
event: Option<Result<CrosstermEvent, crossterm::ErrorKind>>,
|
||||
) {
|
||||
let mut cx = helix_view::compositor::Context {
|
||||
editor: &mut self.editor,
|
||||
jobs: &mut self.jobs,
|
||||
scroll: None,
|
||||
};
|
||||
// Handle key events
|
||||
let should_redraw = match event {
|
||||
Some(Ok(Event::Resize(width, height))) => {
|
||||
self.compositor.resize(width, height);
|
||||
Some(Ok(CrosstermEvent::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
|
||||
.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),
|
||||
None => panic!(),
|
||||
};
|
||||
@@ -389,6 +436,7 @@ impl Application {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "lsp")]
|
||||
pub async fn handle_language_server_message(
|
||||
&mut self,
|
||||
call: helix_lsp::Call,
|
||||
@@ -405,14 +453,17 @@ impl Application {
|
||||
|
||||
match notification {
|
||||
Notification::Initialized => {
|
||||
let language_server =
|
||||
match self.editor.language_servers.get_by_id(server_id) {
|
||||
Some(language_server) => language_server,
|
||||
None => {
|
||||
warn!("can't find language server with id `{}`", server_id);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let language_server = match self
|
||||
.editor
|
||||
.language_servers
|
||||
.get_by_id(server_id)
|
||||
{
|
||||
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.
|
||||
// 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) {
|
||||
Some(call) => call,
|
||||
None => {
|
||||
error!("Method not found {}", method);
|
||||
log::error!("Method not found {}", method);
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -687,7 +738,7 @@ impl Application {
|
||||
let language_server = match self.editor.language_servers.get_by_id(server_id) {
|
||||
Some(language_server) => language_server,
|
||||
None => {
|
||||
warn!("can't find language server with id `{}`", server_id);
|
||||
log::warn!("can't find language server with id `{}`", server_id);
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -699,7 +750,11 @@ impl Application {
|
||||
}
|
||||
|
||||
async fn claim_term(&mut self) -> Result<(), Error> {
|
||||
use helix_view::graphics::CursorKind;
|
||||
terminal::enable_raw_mode()?;
|
||||
if self.terminal.cursor_kind() == CursorKind::Hidden {
|
||||
self.terminal.backend_mut().hide_cursor().ok();
|
||||
}
|
||||
let mut stdout = stdout();
|
||||
execute!(stdout, terminal::EnterAlternateScreen)?;
|
||||
execute!(stdout, terminal::Clear(terminal::ClearType::All))?;
|
||||
@@ -710,7 +765,16 @@ impl Application {
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
// reset cursor shape
|
||||
write!(stdout, "\x1B[2 q")?;
|
||||
// Ignore errors on disabling, this might trigger on windows if we call
|
||||
@@ -740,6 +804,7 @@ impl Application {
|
||||
|
||||
self.jobs.finish().await;
|
||||
|
||||
#[cfg(feature = "lsp")]
|
||||
if self.editor.close_language_servers(None).await.is_err() {
|
||||
log::error!("Timed out waiting for language servers to shutdown");
|
||||
};
|
||||
|
@@ -1,6 +1,7 @@
|
||||
use anyhow::Result;
|
||||
use helix_core::Position;
|
||||
use std::path::{Path, PathBuf};
|
||||
use helix_view::args::parse_file;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Args {
|
||||
@@ -65,37 +66,3 @@ impl 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))
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
use crate::keymap::{default::default, merge_keys, Keymap};
|
||||
use helix_view::document::Mode;
|
||||
use helix_view::keymap::{default::default, Keymap};
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
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)]
|
||||
pub enum ConfigLoadError {
|
||||
BadConfig(TomlError),
|
||||
@@ -63,10 +72,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn parsing_keymaps_config_file() {
|
||||
use crate::keymap;
|
||||
use crate::keymap::Keymap;
|
||||
use helix_core::hashmap;
|
||||
use helix_view::document::Mode;
|
||||
use helix_view::keymap::{self, Keymap};
|
||||
|
||||
let sample_keymaps = r#"
|
||||
[keys.insert]
|
||||
@@ -104,4 +112,104 @@ mod tests {
|
||||
let default_keys = Config::default().keys;
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
@@ -3,22 +3,5 @@ extern crate helix_view;
|
||||
|
||||
pub mod application;
|
||||
pub mod args;
|
||||
pub mod commands;
|
||||
pub mod compositor;
|
||||
pub mod config;
|
||||
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
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -21,5 +21,5 @@ cassowary = "0.3"
|
||||
unicode-segmentation = "1.9"
|
||||
crossterm = { version = "0.23", optional = true }
|
||||
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" }
|
||||
|
@@ -8,7 +8,7 @@ use crossterm::{
|
||||
},
|
||||
terminal::{self, Clear, ClearType},
|
||||
};
|
||||
use helix_view::graphics::{Color, CursorKind, Modifier, Rect};
|
||||
use helix_graphics::{Color, CursorKind, Modifier, Rect};
|
||||
use std::io::{self, Write};
|
||||
|
||||
pub struct CrosstermBackend<W: Write> {
|
||||
|
@@ -2,7 +2,7 @@ use std::io;
|
||||
|
||||
use crate::buffer::Cell;
|
||||
|
||||
use helix_view::graphics::{CursorKind, Rect};
|
||||
use helix_graphics::{CursorKind, Rect};
|
||||
|
||||
#[cfg(feature = "crossterm")]
|
||||
mod crossterm;
|
||||
|
@@ -3,7 +3,7 @@ use crate::{
|
||||
buffer::{Buffer, Cell},
|
||||
};
|
||||
use helix_core::unicode::width::UnicodeWidthStr;
|
||||
use helix_view::graphics::{CursorKind, Rect};
|
||||
use helix_graphics::{CursorKind, Rect};
|
||||
use std::{fmt::Write, io};
|
||||
|
||||
/// A backend used for the integration tests.
|
||||
|
@@ -3,7 +3,7 @@ use helix_core::unicode::width::UnicodeWidthStr;
|
||||
use std::cmp::min;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
use helix_view::graphics::{Color, Modifier, Rect, Style};
|
||||
use helix_graphics::{Color, Modifier, Rect, Style};
|
||||
|
||||
/// A buffer cell
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
@@ -87,7 +87,7 @@ impl Default for 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});
|
||||
/// buf[(0, 2)].set_symbol("x");
|
||||
@@ -179,7 +179,7 @@ impl Buffer {
|
||||
///
|
||||
/// ```
|
||||
/// # use helix_tui::buffer::Buffer;
|
||||
/// # use helix_view::graphics::Rect;
|
||||
/// # use helix_graphics::Rect;
|
||||
/// let rect = Rect::new(200, 100, 10, 10);
|
||||
/// let buffer = Buffer::empty(rect);
|
||||
/// // Global coordinates inside the Buffer's area
|
||||
@@ -204,7 +204,7 @@ impl Buffer {
|
||||
///
|
||||
/// ```
|
||||
/// # use helix_tui::buffer::Buffer;
|
||||
/// # use helix_view::graphics::Rect;
|
||||
/// # use helix_graphics::Rect;
|
||||
/// let rect = Rect::new(200, 100, 10, 10);
|
||||
/// let buffer = Buffer::empty(rect);
|
||||
/// // Global coordinates to the top corner of this Buffer's area
|
||||
@@ -243,7 +243,7 @@ impl Buffer {
|
||||
///
|
||||
/// ```
|
||||
/// # use helix_tui::buffer::Buffer;
|
||||
/// # use helix_view::graphics::Rect;
|
||||
/// # use helix_graphics::Rect;
|
||||
/// let rect = Rect::new(200, 100, 10, 10);
|
||||
/// let buffer = Buffer::empty(rect);
|
||||
/// assert_eq!(buffer.pos_of(0), (200, 100));
|
||||
|
@@ -5,7 +5,7 @@ use cassowary::strength::{REQUIRED, WEAK};
|
||||
use cassowary::WeightedRelation::*;
|
||||
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)]
|
||||
pub enum Corner {
|
||||
@@ -115,7 +115,7 @@ impl Layout {
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// # use helix_tui::layout::{Constraint, Direction, Layout};
|
||||
/// # use helix_view::graphics::Rect;
|
||||
/// # use helix_graphics::Rect;
|
||||
/// let chunks = Layout::default()
|
||||
/// .direction(Direction::Vertical)
|
||||
/// .constraints([Constraint::Length(5), Constraint::Min(0)].as_ref())
|
||||
|
@@ -1,5 +1,5 @@
|
||||
use crate::{backend::Backend, buffer::Buffer};
|
||||
use helix_view::graphics::{CursorKind, Rect};
|
||||
use helix_graphics::{CursorKind, Rect};
|
||||
use std::io;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
|
@@ -21,7 +21,7 @@
|
||||
//! ```rust
|
||||
//! # use helix_tui::widgets::Block;
|
||||
//! # use helix_tui::text::{Span, Spans};
|
||||
//! # use helix_view::graphics::{Color, Style};
|
||||
//! # use helix_graphics::{Color, Style};
|
||||
//! // A simple string with no styling.
|
||||
//! // Converted to Spans(vec![
|
||||
//! // Span { content: Cow::Borrowed("My title"), style: Style { .. } }
|
||||
@@ -48,7 +48,7 @@
|
||||
//! ```
|
||||
use helix_core::line_ending::str_is_line_ending;
|
||||
use helix_core::unicode::width::UnicodeWidthStr;
|
||||
use helix_view::graphics::Style;
|
||||
use helix_graphics::Style;
|
||||
use std::borrow::Cow;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
@@ -92,7 +92,7 @@ impl<'a> Span<'a> {
|
||||
///
|
||||
/// ```rust
|
||||
/// # 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);
|
||||
/// Span::styled("My text", style);
|
||||
/// Span::styled(String::from("My text"), style);
|
||||
@@ -121,7 +121,7 @@ impl<'a> Span<'a> {
|
||||
///
|
||||
/// ```rust
|
||||
/// # use helix_tui::text::{Span, StyledGrapheme};
|
||||
/// # use helix_view::graphics::{Color, Modifier, Style};
|
||||
/// # use helix_graphics::{Color, Modifier, Style};
|
||||
/// # use std::iter::Iterator;
|
||||
/// let style = Style::default().fg(Color::Yellow);
|
||||
/// let span = Span::styled("Text", style);
|
||||
@@ -205,7 +205,7 @@ impl<'a> Spans<'a> {
|
||||
///
|
||||
/// ```rust
|
||||
/// # use helix_tui::text::{Span, Spans};
|
||||
/// # use helix_view::graphics::{Color, Style};
|
||||
/// # use helix_graphics::{Color, Style};
|
||||
/// let spans = Spans::from(vec![
|
||||
/// Span::styled("My", Style::default().fg(Color::Yellow)),
|
||||
/// Span::raw(" text"),
|
||||
@@ -259,7 +259,7 @@ impl<'a> From<Spans<'a>> for String {
|
||||
///
|
||||
/// ```rust
|
||||
/// # 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);
|
||||
///
|
||||
/// // An initial two lines of `Text` built from a `&str`
|
||||
@@ -307,7 +307,7 @@ impl<'a> Text<'a> {
|
||||
///
|
||||
/// ```rust
|
||||
/// # 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);
|
||||
/// Text::styled("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
|
||||
/// # 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 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);
|
||||
|
@@ -4,7 +4,7 @@ use crate::{
|
||||
text::{Span, Spans},
|
||||
widgets::{Borders, Widget},
|
||||
};
|
||||
use helix_view::graphics::{Rect, Style};
|
||||
use helix_graphics::{Rect, Style};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum BorderType {
|
||||
@@ -32,7 +32,7 @@ impl BorderType {
|
||||
///
|
||||
/// ```
|
||||
/// # use helix_tui::widgets::{Block, BorderType, Borders};
|
||||
/// # use helix_view::graphics::{Style, Color};
|
||||
/// # use helix_graphics::{Style, Color};
|
||||
/// Block::default()
|
||||
/// .title("Block")
|
||||
/// .borders(Borders::LEFT | Borders::RIGHT)
|
||||
|
@@ -23,7 +23,7 @@ pub use self::table::{Cell, Row, Table, TableState};
|
||||
use crate::buffer::Buffer;
|
||||
use bitflags::bitflags;
|
||||
|
||||
use helix_view::graphics::Rect;
|
||||
use helix_graphics::Rect;
|
||||
|
||||
bitflags! {
|
||||
/// Bitflags that can be composed to set the visible borders essentially on the block widget.
|
||||
|
@@ -8,7 +8,7 @@ use crate::{
|
||||
},
|
||||
};
|
||||
use helix_core::unicode::width::UnicodeWidthStr;
|
||||
use helix_view::graphics::{Rect, Style};
|
||||
use helix_graphics::{Rect, Style};
|
||||
use std::iter;
|
||||
|
||||
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::widgets::{Block, Borders, Paragraph, Wrap};
|
||||
/// # use helix_tui::layout::{Alignment};
|
||||
/// # use helix_view::graphics::{Style, Color, Modifier};
|
||||
/// # use helix_graphics::{Style, Color, Modifier};
|
||||
/// let text = vec![
|
||||
/// Spans::from(vec![
|
||||
/// Span::raw("First"),
|
||||
|
@@ -10,7 +10,7 @@ use cassowary::{
|
||||
{Expression, Solver},
|
||||
};
|
||||
use helix_core::unicode::width::UnicodeWidthStr;
|
||||
use helix_view::graphics::{Rect, Style};
|
||||
use helix_graphics::{Rect, Style};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// A [`Cell`] contains the [`Text`] to be displayed in a [`Row`] of a [`Table`].
|
||||
@@ -19,7 +19,7 @@ use std::collections::HashMap;
|
||||
/// ```rust
|
||||
/// # use helix_tui::widgets::Cell;
|
||||
/// # use helix_tui::text::{Span, Spans, Text};
|
||||
/// # use helix_view::graphics::{Style, Modifier};
|
||||
/// # use helix_graphics::{Style, Modifier};
|
||||
/// Cell::from("simple string");
|
||||
///
|
||||
/// 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:
|
||||
/// ```rust
|
||||
/// # use helix_tui::widgets::{Row, Cell};
|
||||
/// # use helix_view::graphics::{Style, Color};
|
||||
/// # use helix_graphics::{Style, Color};
|
||||
/// Row::new(vec![
|
||||
/// Cell::from("Cell1"),
|
||||
/// Cell::from("Cell2").style(Style::default().fg(Color::Yellow)),
|
||||
@@ -134,7 +134,7 @@ impl<'a> Row<'a> {
|
||||
/// ```rust
|
||||
/// # use helix_tui::widgets::{Block, Borders, Table, Row, Cell};
|
||||
/// # 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};
|
||||
/// Table::new(vec![
|
||||
/// // Row can be created from simple strings.
|
||||
|
30
helix-ui/Cargo.toml
Normal file
30
helix-ui/Cargo.toml
Normal 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
BIN
helix-ui/assets/entypo.ttf
Normal file
Binary file not shown.
BIN
helix-ui/assets/fonts/Fira Code/FiraCode-VF.ttf
Normal file
BIN
helix-ui/assets/fonts/Fira Code/FiraCode-VF.ttf
Normal file
Binary file not shown.
BIN
helix-ui/assets/fonts/Inter Variable/Inter.ttf
Normal file
BIN
helix-ui/assets/fonts/Inter Variable/Inter.ttf
Normal file
Binary file not shown.
Binary file not shown.
BIN
helix-ui/assets/fonts/Inter Variable/Single axis/Inter-roman.ttf
Normal file
BIN
helix-ui/assets/fonts/Inter Variable/Single axis/Inter-roman.ttf
Normal file
Binary file not shown.
94
helix-ui/assets/fonts/LICENSE.txt
Normal file
94
helix-ui/assets/fonts/LICENSE.txt
Normal 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.
|
BIN
helix-ui/assets/fonts/ttf/FiraCode-Bold.ttf
Normal file
BIN
helix-ui/assets/fonts/ttf/FiraCode-Bold.ttf
Normal file
Binary file not shown.
BIN
helix-ui/assets/fonts/ttf/FiraCode-Light.ttf
Normal file
BIN
helix-ui/assets/fonts/ttf/FiraCode-Light.ttf
Normal file
Binary file not shown.
BIN
helix-ui/assets/fonts/ttf/FiraCode-Medium.ttf
Normal file
BIN
helix-ui/assets/fonts/ttf/FiraCode-Medium.ttf
Normal file
Binary file not shown.
BIN
helix-ui/assets/fonts/ttf/FiraCode-Regular.ttf
Normal file
BIN
helix-ui/assets/fonts/ttf/FiraCode-Regular.ttf
Normal file
Binary file not shown.
BIN
helix-ui/assets/fonts/ttf/FiraCode-Retina.ttf
Normal file
BIN
helix-ui/assets/fonts/ttf/FiraCode-Retina.ttf
Normal file
Binary file not shown.
BIN
helix-ui/assets/fonts/ttf/FiraCode-SemiBold.ttf
Normal file
BIN
helix-ui/assets/fonts/ttf/FiraCode-SemiBold.ttf
Normal file
Binary file not shown.
965
helix-ui/src/femto.rs
Normal file
965
helix-ui/src/femto.rs
Normal 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
503
helix-ui/src/main.rs
Normal 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
28
helix-ui/src/shader.wgsl
Normal 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);
|
||||
}
|
@@ -10,16 +10,26 @@ repository = "https://github.com/helix-editor/helix"
|
||||
homepage = "https://helix-editor.com"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
term = ["crossterm"]
|
||||
# default = ["dap", "lsp"]
|
||||
lsp = ["helix-lsp", "tokio-runtime"]
|
||||
dap = ["helix-dap", "tokio-stream", "tokio-runtime"]
|
||||
tokio-runtime = ["tokio"]
|
||||
term = ["crossterm", "tui"]
|
||||
ui = []
|
||||
|
||||
[dependencies]
|
||||
bitflags = "1.3"
|
||||
anyhow = "1"
|
||||
helix-core = { version = "0.6", path = "../helix-core" }
|
||||
helix-lsp = { version = "0.6", path = "../helix-lsp" }
|
||||
helix-dap = { version = "0.6", path = "../helix-dap" }
|
||||
helix-graphics = { version = "0.6", path = "../helix-graphics" }
|
||||
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 }
|
||||
tui = { path = "../helix-tui", package = "helix-tui", default-features = false, features = ["crossterm"], optional = true }
|
||||
|
||||
# Conversion traits
|
||||
once_cell = "1.10"
|
||||
@@ -27,8 +37,6 @@ url = "2"
|
||||
|
||||
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 }
|
||||
|
||||
slotmap = "1"
|
||||
@@ -40,10 +48,25 @@ serde_json = "1.0"
|
||||
toml = "0.5"
|
||||
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"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
clipboard-win = { version = "4.4", features = ["std"] }
|
||||
|
||||
[dev-dependencies]
|
||||
helix-tui = { path = "../helix-tui" }
|
||||
|
36
helix-view/src/args.rs
Normal file
36
helix-view/src/args.rs
Normal 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))
|
||||
}
|
93
helix-view/src/backend/term.rs
Normal file
93
helix-view/src/backend/term.rs
Normal 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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -14,6 +14,7 @@ pub trait ClipboardProvider: std::fmt::Debug {
|
||||
fn set_contents(&mut self, contents: String, clipboard_type: ClipboardType) -> Result<()>;
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
macro_rules! command_provider {
|
||||
(paste => $get_prg:literal $( , $get_arg:literal )* ; copy => $set_prg:literal $( , $set_arg:literal )* ; ) => {{
|
||||
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> {
|
||||
// TODO:
|
||||
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> {
|
||||
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
|
||||
|
@@ -1,13 +1,13 @@
|
||||
use super::{Context, Editor};
|
||||
use crate::editor::Breakpoint;
|
||||
use crate::ui::{self, overlay::overlayed, FilePicker, Picker, Popup, Prompt, PromptEvent, Text};
|
||||
use crate::{
|
||||
compositor::{self, Compositor},
|
||||
job::{Callback, Jobs},
|
||||
ui::{self, overlay::overlayed, FilePicker, Picker, Popup, Prompt, PromptEvent, Text},
|
||||
};
|
||||
use helix_core::syntax::{DebugArgumentValue, DebugConfigCompletion};
|
||||
use helix_dap::{self as dap, Client};
|
||||
use helix_lsp::block_on;
|
||||
use helix_view::editor::Breakpoint;
|
||||
|
||||
use serde_json::{to_value, Value};
|
||||
use tokio_stream::wrappers::UnboundedReceiverStream;
|
||||
@@ -18,7 +18,8 @@ use std::path::PathBuf;
|
||||
|
||||
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(
|
||||
cx: &mut Context,
|
||||
@@ -474,7 +475,7 @@ pub fn dap_variables(cx: &mut Context) {
|
||||
let text_style = theme.get("ui.text.focus");
|
||||
|
||||
for scope in scopes.iter() {
|
||||
// use helix_view::graphics::Style;
|
||||
// use crate::graphics::Style;
|
||||
use tui::text::{Span, Spans};
|
||||
let response = block_on(debugger.variables(scope.variables_reference));
|
||||
|
@@ -7,12 +7,10 @@ use helix_lsp::{
|
||||
use super::{align_view, push_jump, Align, Context, Editor};
|
||||
|
||||
use helix_core::Selection;
|
||||
use helix_view::editor::Action;
|
||||
use crate::editor::Action;
|
||||
|
||||
use crate::{
|
||||
compositor::{self, Compositor},
|
||||
ui::{self, overlay::overlayed, FileLocation, FilePicker, Popup, PromptEvent},
|
||||
};
|
||||
use crate::ui::{self, overlay::overlayed, FileLocation, FilePicker, Popup, PromptEvent};
|
||||
use crate::compositor::{self, Compositor};
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
@@ -257,7 +255,7 @@ pub fn code_action(cx: &mut Context) {
|
||||
});
|
||||
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,
|
||||
horizontal: 1,
|
||||
});
|
@@ -1,11 +1,25 @@
|
||||
#[cfg(feature = "dap")]
|
||||
pub(crate) mod dap;
|
||||
#[cfg(feature = "lsp")]
|
||||
pub(crate) mod lsp;
|
||||
pub(crate) mod typed;
|
||||
|
||||
#[cfg(feature = "dap")]
|
||||
pub use dap::*;
|
||||
#[cfg(feature = "lsp")]
|
||||
pub use lsp::*;
|
||||
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::{
|
||||
comment, coords_at_pos, find_first_non_whitespace_char, find_root, graphemes,
|
||||
history::UndoKind,
|
||||
@@ -25,15 +39,10 @@ use helix_core::{
|
||||
LineEnding, Position, Range, Rope, RopeGraphemes, RopeSlice, Selection, SmallVec, Tendril,
|
||||
Transaction,
|
||||
};
|
||||
use helix_view::{
|
||||
clipboard::ClipboardType,
|
||||
document::{Mode, SCRATCH_BUFFER_NAME},
|
||||
editor::{Action, Motion},
|
||||
info::Info,
|
||||
input::KeyEvent,
|
||||
keyboard::KeyCode,
|
||||
view::View,
|
||||
Document, DocumentId, Editor, ViewId,
|
||||
|
||||
use crate::{
|
||||
compositor::{self, Component, Compositor},
|
||||
job::{self, Job, Jobs},
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, bail, ensure, Context as _};
|
||||
@@ -43,14 +52,17 @@ use movement::Movement;
|
||||
|
||||
use crate::{
|
||||
args,
|
||||
compositor::{self, Component, Compositor},
|
||||
ui::{self, overlay::overlayed, FilePicker, Picker, Popup, Prompt, PromptEvent},
|
||||
};
|
||||
|
||||
use crate::job::{self, Job, Jobs};
|
||||
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::{
|
||||
borrow::Cow,
|
||||
@@ -60,11 +72,6 @@ use std::{
|
||||
use once_cell::sync::Lazy;
|
||||
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 register: Option<char>,
|
||||
pub count: Option<NonZeroUsize>,
|
||||
@@ -91,6 +98,7 @@ impl<'a> Context<'a> {
|
||||
self.on_next_key_callback = Some(Box::new(on_next_key_callback));
|
||||
}
|
||||
|
||||
#[cfg(feature = "lsp")]
|
||||
#[inline]
|
||||
pub fn callback<T, F>(
|
||||
&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
|
||||
/// :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 {
|
||||
( $($name:ident, $doc:literal,)* ) => {
|
||||
( $($(#[cfg($attr:meta)])? $name:ident, $doc:literal,)* ) => {
|
||||
$(
|
||||
|
||||
$(#[cfg($attr)])?
|
||||
#[allow(non_upper_case_globals)]
|
||||
pub const $name: Self = Self::Static {
|
||||
name: stringify!($name),
|
||||
@@ -150,7 +160,7 @@ macro_rules! static_commands {
|
||||
)*
|
||||
|
||||
pub const STATIC_COMMAND_LIST: &'static [Self] = &[
|
||||
$( Self::$name, )*
|
||||
$( $(#[cfg($attr)])? Self::$name, )*
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -164,7 +174,6 @@ impl MappableCommand {
|
||||
let mut cx = compositor::Context {
|
||||
editor: cx.editor,
|
||||
jobs: cx.jobs,
|
||||
scroll: None,
|
||||
};
|
||||
if let Err(e) = (command.fun)(&mut cx, &args[..], PromptEvent::Validate) {
|
||||
cx.editor.set_error(format!("{}", e));
|
||||
@@ -259,9 +268,12 @@ impl MappableCommand {
|
||||
command_mode, "Enter command mode",
|
||||
file_picker, "Open file picker",
|
||||
file_picker_in_current_directory, "Open file picker at current working directory",
|
||||
#[cfg(feature = "lsp")]
|
||||
code_action, "Perform code action",
|
||||
buffer_picker, "Open buffer picker",
|
||||
#[cfg(feature = "lsp")]
|
||||
symbol_picker, "Open symbol picker",
|
||||
#[cfg(feature = "lsp")]
|
||||
workspace_symbol_picker, "Open workspace symbol picker",
|
||||
last_picker, "Open last picker",
|
||||
prepend_to_line, "Insert at start of line",
|
||||
@@ -271,16 +283,20 @@ impl MappableCommand {
|
||||
normal_mode, "Enter normal mode",
|
||||
select_mode, "Enter selection extend mode",
|
||||
exit_select_mode, "Exit selection mode",
|
||||
#[cfg(feature = "lsp")]
|
||||
goto_definition, "Goto definition",
|
||||
add_newline_above, "Add newline above",
|
||||
add_newline_below, "Add newline below",
|
||||
#[cfg(feature = "lsp")]
|
||||
goto_type_definition, "Goto type definition",
|
||||
#[cfg(feature = "lsp")]
|
||||
goto_implementation, "Goto implementation",
|
||||
goto_file_start, "Goto line number <n> else file start",
|
||||
goto_file_end, "Goto file end",
|
||||
goto_file, "Goto files in selection",
|
||||
goto_file_hsplit, "Goto files in selection (hsplit)",
|
||||
goto_file_vsplit, "Goto files in selection (vsplit)",
|
||||
#[cfg(feature = "lsp")]
|
||||
goto_reference, "Goto references",
|
||||
goto_window_top, "Goto window top",
|
||||
goto_window_center, "Goto window center",
|
||||
@@ -305,6 +321,7 @@ impl MappableCommand {
|
||||
extend_to_line_start, "Extend to line start",
|
||||
extend_to_line_end, "Extend to line end",
|
||||
extend_to_line_end_newline, "Extend to line end",
|
||||
#[cfg(feature = "lsp")]
|
||||
signature_help, "Show signature help",
|
||||
insert_tab, "Insert tab char",
|
||||
insert_newline, "Insert newline char",
|
||||
@@ -342,6 +359,7 @@ impl MappableCommand {
|
||||
keep_primary_selection, "Keep primary selection",
|
||||
remove_primary_selection, "Remove primary selection",
|
||||
completion, "Invoke completion popup",
|
||||
#[cfg(feature = "lsp")]
|
||||
hover, "Show docs for item under cursor",
|
||||
toggle_comments, "Comment/uncomment selections",
|
||||
rotate_selections_forward, "Rotate selections forward",
|
||||
@@ -390,20 +408,35 @@ impl MappableCommand {
|
||||
goto_prev_comment, "Goto previous comment",
|
||||
goto_next_paragraph, "Goto next paragraph",
|
||||
goto_prev_paragraph, "Goto previous paragraph",
|
||||
#[cfg(feature = "dap")]
|
||||
dap_launch, "Launch debug target",
|
||||
#[cfg(feature = "dap")]
|
||||
dap_toggle_breakpoint, "Toggle breakpoint",
|
||||
#[cfg(feature = "dap")]
|
||||
dap_continue, "Continue program execution",
|
||||
#[cfg(feature = "dap")]
|
||||
dap_pause, "Pause program execution",
|
||||
#[cfg(feature = "dap")]
|
||||
dap_step_in, "Step in",
|
||||
#[cfg(feature = "dap")]
|
||||
dap_step_out, "Step out",
|
||||
#[cfg(feature = "dap")]
|
||||
dap_next, "Step to next",
|
||||
#[cfg(feature = "dap")]
|
||||
dap_variables, "List variables",
|
||||
#[cfg(feature = "dap")]
|
||||
dap_terminate, "End debug session",
|
||||
#[cfg(feature = "dap")]
|
||||
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",
|
||||
#[cfg(feature = "dap")]
|
||||
dap_switch_thread, "Switch current thread",
|
||||
#[cfg(feature = "dap")]
|
||||
dap_switch_stack_frame, "Switch stack frame",
|
||||
#[cfg(feature = "dap")]
|
||||
dap_enable_exceptions, "Enable exception breakpoints",
|
||||
#[cfg(feature = "dap")]
|
||||
dap_disable_exceptions, "Disable exception breakpoints",
|
||||
shell_pipe, "Pipe selections through shell command",
|
||||
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_keep_pipe, "Filter selections with shell predicate",
|
||||
suspend, "Suspend",
|
||||
#[cfg(feature = "lsp")]
|
||||
rename_symbol, "Rename symbol",
|
||||
increment, "Increment",
|
||||
decrement, "Decrement",
|
||||
@@ -1729,7 +1763,17 @@ fn search_selection(cx: &mut Context) {
|
||||
cx.editor.set_status(msg);
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "term"))]
|
||||
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) =
|
||||
tokio::sync::mpsc::unbounded_channel::<(usize, PathBuf)>();
|
||||
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 show_picker = async move {
|
||||
use tokio_stream::wrappers::UnboundedReceiverStream;
|
||||
|
||||
let all_matches: Vec<(usize, PathBuf)> =
|
||||
UnboundedReceiverStream::new(all_matches_rx).collect().await;
|
||||
let call: job::Callback =
|
||||
@@ -2271,14 +2317,14 @@ async fn make_format_callback(
|
||||
doc_id: DocumentId,
|
||||
doc_version: i32,
|
||||
modified: Modified,
|
||||
format: impl Future<Output = helix_lsp::util::LspFormatting> + Send + 'static,
|
||||
format: impl Future<Output = Transaction> + Send + 'static,
|
||||
) -> anyhow::Result<job::Callback> {
|
||||
let format = format.await;
|
||||
let call: job::Callback = Box::new(move |editor, _compositor| {
|
||||
let view_id = view!(editor).id;
|
||||
if let Some(doc) = editor.document_mut(doc_id) {
|
||||
if doc.version() == doc_version {
|
||||
doc.apply(&Transaction::from(format), view_id);
|
||||
doc.apply(&format, view_id);
|
||||
doc.append_changes_to_history(view_id);
|
||||
if let Modified::SetUnmodified = modified {
|
||||
doc.reset_modified();
|
||||
@@ -2651,6 +2697,7 @@ pub mod insert {
|
||||
super::completion(cx);
|
||||
}
|
||||
|
||||
#[cfg(feature = "lsp")]
|
||||
fn language_server_completion(cx: &mut Context, ch: char) {
|
||||
use helix_lsp::lsp;
|
||||
// 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) {
|
||||
use helix_lsp::lsp;
|
||||
// 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)
|
||||
// this could also generically look at Transaction, but it's a bit annoying to look at
|
||||
// Operation instead of Change.
|
||||
#[cfg(feature = "lsp")]
|
||||
for hook in &[language_server_completion, signature_help] {
|
||||
// for hook in &[signature_help] {
|
||||
hook(cx, c);
|
||||
@@ -3384,6 +3433,10 @@ fn unindent(cx: &mut Context) {
|
||||
doc.apply(&transaction, view.id);
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "lsp"))]
|
||||
fn format_selections(_cx: &mut Context) {}
|
||||
|
||||
#[cfg(feature = "lsp")]
|
||||
fn format_selections(cx: &mut Context) {
|
||||
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);
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "lsp"))]
|
||||
pub fn completion(cx: &mut Context) {
|
||||
// TODO:
|
||||
}
|
||||
|
||||
#[cfg(feature = "lsp")]
|
||||
pub fn completion(cx: &mut Context) {
|
||||
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) {
|
||||
#[cfg(not(windows))]
|
||||
signal_hook::low_level::raise(signal_hook::consts::signal::SIGTSTP).unwrap();
|
||||
// #[cfg(not(windows))]
|
||||
// signal_hook::low_level::raise(signal_hook::consts::signal::SIGTSTP).unwrap();
|
||||
}
|
||||
|
||||
fn add_newline_above(cx: &mut Context) {
|
||||
@@ -4517,7 +4576,7 @@ fn record_macro(cx: &mut Context) {
|
||||
fn replay_macro(cx: &mut Context) {
|
||||
let reg = cx.register.unwrap_or('@');
|
||||
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,
|
||||
Err(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| {
|
||||
for _ in 0..count {
|
||||
for &key in keys.iter() {
|
||||
compositor.handle_event(crossterm::event::Event::Key(key.into()), cx);
|
||||
compositor.handle_event(compositor::Event::Key(key), cx);
|
||||
}
|
||||
}
|
||||
}));
|
@@ -1,6 +1,6 @@
|
||||
use super::*;
|
||||
|
||||
use helix_view::editor::{Action, ConfigEvent};
|
||||
use crate::editor::{Action, ConfigEvent};
|
||||
use ui::completers::{self, Completer};
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -205,23 +205,31 @@ fn write_impl(
|
||||
if doc.path().is_none() {
|
||||
bail!("cannot write a buffer without a filename");
|
||||
}
|
||||
let fmt = doc.auto_format().map(|fmt| {
|
||||
let shared = fmt.shared();
|
||||
let callback = make_format_callback(
|
||||
doc.id(),
|
||||
doc.version(),
|
||||
Modified::SetUnmodified,
|
||||
shared.clone(),
|
||||
);
|
||||
jobs.callback(callback);
|
||||
shared
|
||||
});
|
||||
let future = doc.format_and_save(fmt, force);
|
||||
|
||||
#[cfg(feature = "lsp")]
|
||||
let future = {
|
||||
let fmt = doc.auto_format().map(|fmt| {
|
||||
let shared = fmt.shared();
|
||||
let callback = make_format_callback(
|
||||
doc.id(),
|
||||
doc.version(),
|
||||
Modified::SetUnmodified,
|
||||
shared.clone(),
|
||||
);
|
||||
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());
|
||||
|
||||
if path.is_some() {
|
||||
let id = doc.id();
|
||||
doc.detect_language(cx.editor.syn_loader.clone());
|
||||
#[cfg(feature = "lsp")]
|
||||
let _ = cx.editor.refresh_language_server(id);
|
||||
}
|
||||
Ok(())
|
||||
@@ -259,6 +267,7 @@ fn format(
|
||||
_event: PromptEvent,
|
||||
) -> anyhow::Result<()> {
|
||||
let doc = doc!(cx.editor);
|
||||
#[cfg(feature = "lsp")]
|
||||
if let Some(format) = doc.format() {
|
||||
let callback =
|
||||
make_format_callback(doc.id(), doc.version(), Modified::LeaveModified, format);
|
||||
@@ -466,18 +475,24 @@ fn write_all_impl(
|
||||
continue;
|
||||
}
|
||||
|
||||
let fmt = doc.auto_format().map(|fmt| {
|
||||
let shared = fmt.shared();
|
||||
let callback = make_format_callback(
|
||||
doc.id(),
|
||||
doc.version(),
|
||||
Modified::SetUnmodified,
|
||||
shared.clone(),
|
||||
);
|
||||
jobs.callback(callback);
|
||||
shared
|
||||
});
|
||||
let future = doc.format_and_save(fmt, force);
|
||||
#[cfg(feature = "lsp")]
|
||||
let future = {
|
||||
let fmt = doc.auto_format().map(|fmt| {
|
||||
let shared = fmt.shared();
|
||||
let callback = make_format_callback(
|
||||
doc.id(),
|
||||
doc.version(),
|
||||
Modified::SetUnmodified,
|
||||
shared.clone(),
|
||||
);
|
||||
jobs.callback(callback);
|
||||
shared
|
||||
});
|
||||
doc.format_and_save(fmt, force)
|
||||
};
|
||||
#[cfg(not(feature = "lsp"))]
|
||||
let future = doc.save(force);
|
||||
|
||||
jobs.add(Job::new(future).wait_before_exiting());
|
||||
}
|
||||
|
||||
@@ -847,6 +862,7 @@ fn hsplit_new(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "dap")]
|
||||
fn debug_eval(
|
||||
cx: &mut compositor::Context,
|
||||
args: &[Cow<str>],
|
||||
@@ -869,6 +885,7 @@ fn debug_eval(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "dap")]
|
||||
fn debug_start(
|
||||
cx: &mut compositor::Context,
|
||||
args: &[Cow<str>],
|
||||
@@ -882,6 +899,7 @@ fn debug_start(
|
||||
dap_start_impl(cx, name.as_deref(), None, Some(args))
|
||||
}
|
||||
|
||||
#[cfg(feature = "dap")]
|
||||
fn debug_remote(
|
||||
cx: &mut compositor::Context,
|
||||
args: &[Cow<str>],
|
||||
@@ -976,6 +994,7 @@ fn set_option(
|
||||
};
|
||||
let config = serde_json::from_value(config).map_err(field_error)?;
|
||||
|
||||
#[cfg(feature = "tokio")]
|
||||
cx.editor
|
||||
.config_events
|
||||
.0
|
||||
@@ -997,6 +1016,7 @@ fn language(
|
||||
doc.set_language_by_language_id(&args[0], cx.editor.syn_loader.clone());
|
||||
|
||||
let id = doc.id();
|
||||
#[cfg(feature = "lsp")]
|
||||
cx.editor.refresh_language_server(id);
|
||||
Ok(())
|
||||
}
|
||||
@@ -1102,6 +1122,7 @@ fn refresh_config(
|
||||
_args: &[Cow<str>],
|
||||
_event: PromptEvent,
|
||||
) -> anyhow::Result<()> {
|
||||
#[cfg(feature = "tokio")]
|
||||
cx.editor.config_events.0.send(ConfigEvent::Refresh)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1435,6 +1456,7 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
|
||||
fun: tree_sitter_scopes,
|
||||
completer: None,
|
||||
},
|
||||
#[cfg(feature = "dap")]
|
||||
TypableCommand {
|
||||
name: "debug-start",
|
||||
aliases: &["dbg"],
|
||||
@@ -1442,6 +1464,7 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
|
||||
fun: debug_start,
|
||||
completer: None,
|
||||
},
|
||||
#[cfg(feature = "dap")]
|
||||
TypableCommand {
|
||||
name: "debug-remote",
|
||||
aliases: &["dbg-tcp"],
|
||||
@@ -1449,6 +1472,7 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
|
||||
fun: debug_remote,
|
||||
completer: None,
|
||||
},
|
||||
#[cfg(feature = "dap")]
|
||||
TypableCommand {
|
||||
name: "debug-eval",
|
||||
aliases: &[],
|
@@ -1,11 +1,8 @@
|
||||
// Each component declares it's own size constraints and gets fitted based on it's parent.
|
||||
// Q: how does this work with popups?
|
||||
// cursive does compositor.screen_mut().add_layer_at(pos::absolute(x, y), <component>)
|
||||
use crate::graphics::{CursorKind, Rect};
|
||||
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)>;
|
||||
|
||||
@@ -15,36 +12,78 @@ pub enum EventResult {
|
||||
Consumed(Option<Callback>),
|
||||
}
|
||||
|
||||
use helix_view::Editor;
|
||||
|
||||
use crate::job::Jobs;
|
||||
use crate::Editor;
|
||||
|
||||
pub use crate::input::Event;
|
||||
|
||||
pub struct Context<'a> {
|
||||
pub editor: &'a mut Editor,
|
||||
pub scroll: Option<usize>,
|
||||
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.
|
||||
fn handle_event(&mut self, _event: Event, _ctx: &mut Context) -> EventResult {
|
||||
EventResult::Ignored(None)
|
||||
}
|
||||
// , args: ()
|
||||
|
||||
/// Should redraw? Useful for saving redraw cycles if we know component didn't change.
|
||||
fn should_update(&self) -> bool {
|
||||
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.
|
||||
/// 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 {
|
||||
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 {
|
||||
pub fn new() -> Result<Self, Error> {
|
||||
let backend = CrosstermBackend::new(stdout());
|
||||
let terminal = Terminal::new(backend)?;
|
||||
Ok(Self {
|
||||
pub fn new(area: Rect) -> Self {
|
||||
Self {
|
||||
layers: Vec::new(),
|
||||
terminal,
|
||||
area,
|
||||
last_picker: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
self.terminal
|
||||
.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 resize(&mut self, area: Rect) {
|
||||
self.area = area;
|
||||
}
|
||||
|
||||
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 {
|
||||
// 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) {
|
||||
keys.push(key.into());
|
||||
keys.push(key);
|
||||
}
|
||||
|
||||
let mut callbacks = Vec::new();
|
||||
@@ -168,27 +184,13 @@ impl Compositor {
|
||||
consumed
|
||||
}
|
||||
|
||||
pub fn render(&mut self, cx: &mut Context) {
|
||||
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();
|
||||
|
||||
pub fn render(&mut self, area: Rect, cx: &mut RenderContext) {
|
||||
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) {
|
||||
for layer in self.layers.iter().rev() {
|
||||
if let (Some(pos), kind) = layer.cursor(area, editor) {
|
@@ -19,7 +19,9 @@ use helix_core::{
|
||||
ChangeSet, Diagnostic, LineEnding, Rope, RopeBuilder, Selection, State, Syntax, Transaction,
|
||||
DEFAULT_LINE_ENDING,
|
||||
};
|
||||
use helix_lsp::util::LspFormatting;
|
||||
|
||||
#[cfg(feature = "lsp")]
|
||||
use helix_lsp::lsp;
|
||||
|
||||
use crate::{DocumentId, Editor, ViewId};
|
||||
|
||||
@@ -119,6 +121,7 @@ pub struct Document {
|
||||
pub(crate) modified_since_accessed: bool,
|
||||
|
||||
diagnostics: Vec<Diagnostic>,
|
||||
#[cfg(feature = "lsp")]
|
||||
language_server: Option<Arc<helix_lsp::Client>>,
|
||||
}
|
||||
|
||||
@@ -142,7 +145,6 @@ impl fmt::Debug for Document {
|
||||
.field("version", &self.version)
|
||||
.field("modified_since_accessed", &self.modified_since_accessed)
|
||||
.field("diagnostics", &self.diagnostics)
|
||||
// .field("language_server", &self.language_server)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
@@ -255,17 +257,19 @@ pub fn from_reader<R: std::io::Read + ?Sized>(
|
||||
Ok((rope, encoding))
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio-runtime")]
|
||||
// The documentation and implementation of this function should be up-to-date with
|
||||
// its sibling function, `from_reader()`.
|
||||
//
|
||||
/// 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,
|
||||
/// 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,
|
||||
encoding: &'static encoding::Encoding,
|
||||
rope: &'a Rope,
|
||||
) -> Result<(), Error> {
|
||||
use tokio::io::AsyncWriteExt;
|
||||
// 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
|
||||
// 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));
|
||||
}
|
||||
|
||||
use helix_lsp::lsp;
|
||||
use url::Url;
|
||||
|
||||
impl Document {
|
||||
@@ -359,6 +362,7 @@ impl Document {
|
||||
savepoint: None,
|
||||
last_saved_revision: 0,
|
||||
modified_since_accessed: false,
|
||||
#[cfg(feature = "lsp")]
|
||||
language_server: None,
|
||||
}
|
||||
}
|
||||
@@ -394,9 +398,10 @@ impl Document {
|
||||
Ok(doc)
|
||||
}
|
||||
|
||||
#[cfg(feature = "lsp")]
|
||||
/// The same as [`format`], but only returns formatting changes if auto-formatting
|
||||
/// 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 {
|
||||
self.format()
|
||||
} else {
|
||||
@@ -404,9 +409,12 @@ impl Document {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "lsp")]
|
||||
/// If supported, returns the changes that should be applied to this document in order
|
||||
/// 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 text = self.text.clone();
|
||||
let offset_encoding = language_server.offset_encoding();
|
||||
@@ -425,11 +433,7 @@ impl Document {
|
||||
log::warn!("LSP formatting failed: {}", e);
|
||||
Default::default()
|
||||
});
|
||||
LspFormatting {
|
||||
doc: text,
|
||||
edits,
|
||||
offset_encoding,
|
||||
}
|
||||
generate_transaction_from_edits(&text, edits, offset_encoding)
|
||||
};
|
||||
Some(fut)
|
||||
}
|
||||
@@ -438,9 +442,10 @@ impl Document {
|
||||
self.save_impl::<futures_util::future::Ready<_>>(None, force)
|
||||
}
|
||||
|
||||
#[cfg(feature = "lsp")]
|
||||
pub fn format_and_save(
|
||||
&mut self,
|
||||
formatting: Option<impl Future<Output = LspFormatting>>,
|
||||
formatting: Option<impl Future<Output = Transaction>>,
|
||||
force: bool,
|
||||
) -> impl Future<Output = anyhow::Result<()>> {
|
||||
self.save_impl(formatting, force)
|
||||
@@ -452,7 +457,7 @@ impl Document {
|
||||
/// at its `path()`.
|
||||
///
|
||||
/// 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,
|
||||
formatting: Option<F>,
|
||||
force: bool,
|
||||
@@ -462,18 +467,20 @@ impl Document {
|
||||
|
||||
let mut text = self.text().clone();
|
||||
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();
|
||||
|
||||
// mark changes up to now as saved
|
||||
self.reset_modified();
|
||||
|
||||
#[cfg(feature = "tokio-runtime")]
|
||||
let encoding = self.encoding;
|
||||
|
||||
// We encode the file according to the `Document`'s encoding.
|
||||
async move {
|
||||
use tokio::fs::File;
|
||||
if let Some(parent) = path.parent() {
|
||||
// TODO: display a prompt asking the user if the directories should be created
|
||||
if !parent.exists() {
|
||||
@@ -486,7 +493,8 @@ impl Document {
|
||||
}
|
||||
|
||||
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 {
|
||||
// This shouldn't happen, because the transaction changes were generated
|
||||
// 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?;
|
||||
|
||||
#[cfg(feature = "lsp")]
|
||||
if let Some(language_server) = language_server {
|
||||
if !language_server.is_initialized() {
|
||||
return Ok(());
|
||||
@@ -624,6 +635,7 @@ impl Document {
|
||||
self.set_language(language_config, Some(config_loader));
|
||||
}
|
||||
|
||||
#[cfg(feature = "lsp")]
|
||||
/// Set the LSP.
|
||||
pub fn set_language_server(&mut self, language_server: Option<Arc<helix_lsp::Client>>) {
|
||||
self.language_server = language_server;
|
||||
@@ -692,6 +704,7 @@ impl Document {
|
||||
}
|
||||
|
||||
// emit lsp notification
|
||||
#[cfg(feature = "lsp")]
|
||||
if let Some(language_server) = self.language_server() {
|
||||
let notify = language_server.text_document_did_change(
|
||||
self.versioned_identifier(),
|
||||
@@ -869,6 +882,7 @@ impl Document {
|
||||
self.version
|
||||
}
|
||||
|
||||
#[cfg(feature = "lsp")]
|
||||
/// Language server if it has been initialized.
|
||||
pub fn language_server(&self) -> Option<&helix_lsp::Client> {
|
||||
let server = self.language_server.as_deref()?;
|
||||
@@ -907,6 +921,7 @@ impl Document {
|
||||
}
|
||||
|
||||
/// File path as a URL.
|
||||
#[cfg(feature = "lsp")]
|
||||
pub fn url(&self) -> Option<Url> {
|
||||
Url::from_file_path(self.path()?).ok()
|
||||
}
|
||||
@@ -935,15 +950,18 @@ impl Document {
|
||||
|
||||
// -- LSP methods
|
||||
|
||||
#[cfg(feature = "lsp")]
|
||||
#[inline]
|
||||
pub fn identifier(&self) -> lsp::TextDocumentIdentifier {
|
||||
lsp::TextDocumentIdentifier::new(self.url().unwrap())
|
||||
}
|
||||
|
||||
#[cfg(feature = "lsp")]
|
||||
pub fn versioned_identifier(&self) -> lsp::VersionedTextDocumentIdentifier {
|
||||
lsp::VersionedTextDocumentIdentifier::new(self.url().unwrap(), self.version)
|
||||
}
|
||||
|
||||
#[cfg(feature = "lsp")]
|
||||
pub fn position(
|
||||
&self,
|
||||
view_id: ViewId,
|
||||
@@ -1003,6 +1021,7 @@ impl Default for Document {
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[cfg(feature = "lsp")]
|
||||
#[test]
|
||||
fn changeset_to_changes_ignore_line_endings() {
|
||||
use helix_lsp::{lsp, Client, OffsetEncoding};
|
||||
@@ -1037,6 +1056,7 @@ mod test {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "lsp")]
|
||||
#[test]
|
||||
fn changeset_to_changes() {
|
||||
use helix_lsp::{lsp, Client, OffsetEncoding};
|
||||
|
@@ -9,25 +9,25 @@ use crate::{
|
||||
Document, DocumentId, View, ViewId,
|
||||
};
|
||||
|
||||
use futures_util::future;
|
||||
use futures_util::stream::select_all::SelectAll;
|
||||
use tokio_stream::wrappers::UnboundedReceiverStream;
|
||||
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
collections::{BTreeMap, HashMap},
|
||||
io::stdin,
|
||||
num::NonZeroUsize,
|
||||
path::{Path, PathBuf},
|
||||
pin::Pin,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
#[cfg(feature = "tokio-runtime")]
|
||||
use std::pin::Pin;
|
||||
#[cfg(feature = "tokio-runtime")]
|
||||
use tokio::{
|
||||
sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender},
|
||||
time::{sleep, Duration, Instant, Sleep},
|
||||
time::{sleep, Instant, Sleep},
|
||||
};
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{bail, Error};
|
||||
|
||||
pub use helix_core::diagnostic::Severity;
|
||||
@@ -38,6 +38,16 @@ use helix_core::{
|
||||
Change,
|
||||
};
|
||||
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 serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer};
|
||||
@@ -431,10 +441,15 @@ pub struct Editor {
|
||||
pub registers: Registers,
|
||||
pub macro_recording: Option<(char, Vec<KeyEvent>)>,
|
||||
pub theme: Theme,
|
||||
#[cfg(feature = "lsp")]
|
||||
pub language_servers: helix_lsp::Registry,
|
||||
|
||||
#[cfg(feature = "dap")]
|
||||
pub debugger: Option<dap::Client>,
|
||||
#[cfg(feature = "dap")]
|
||||
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 clipboard_provider: Box<dyn ClipboardProvider>,
|
||||
@@ -448,6 +463,7 @@ pub struct Editor {
|
||||
pub config: Box<dyn DynAccess<Config>>,
|
||||
pub auto_pairs: Option<AutoPairs>,
|
||||
|
||||
#[cfg(feature = "tokio-runtime")]
|
||||
pub idle_timer: Pin<Box<Sleep>>,
|
||||
pub last_motion: Option<Motion>,
|
||||
pub pseudo_pending: Option<String>,
|
||||
@@ -456,6 +472,8 @@ pub struct Editor {
|
||||
|
||||
pub exit_code: i32,
|
||||
|
||||
// TODO: do this via a signal flag instead
|
||||
#[cfg(feature = "tokio-runtime")]
|
||||
pub config_events: (UnboundedSender<ConfigEvent>, UnboundedReceiver<ConfigEvent>),
|
||||
}
|
||||
|
||||
@@ -486,7 +504,6 @@ impl Editor {
|
||||
syn_loader: Arc<syntax::Loader>,
|
||||
config: Box<dyn DynAccess<Config>>,
|
||||
) -> Self {
|
||||
let language_servers = helix_lsp::Registry::new();
|
||||
let conf = config.load();
|
||||
let auto_pairs = (&conf.auto_pairs).into();
|
||||
|
||||
@@ -501,9 +518,14 @@ impl Editor {
|
||||
selected_register: None,
|
||||
macro_recording: None,
|
||||
theme: theme_loader.default(),
|
||||
language_servers,
|
||||
#[cfg(feature = "lsp")]
|
||||
language_servers: helix_lsp::Registry::new(),
|
||||
#[cfg(feature = "dap")]
|
||||
debugger: None,
|
||||
#[cfg(feature = "dap")]
|
||||
debugger_events: SelectAll::new(),
|
||||
#[cfg(not(feature = "dap"))]
|
||||
debugger_events: futures_util::stream::empty(),
|
||||
breakpoints: HashMap::new(),
|
||||
syn_loader,
|
||||
theme_loader,
|
||||
@@ -511,6 +533,7 @@ impl Editor {
|
||||
clipboard_provider: get_clipboard_provider(),
|
||||
status_msg: None,
|
||||
autoinfo: None,
|
||||
#[cfg(feature = "tokio-runtime")]
|
||||
idle_timer: Box::pin(sleep(conf.idle_timeout)),
|
||||
last_motion: None,
|
||||
last_completion: None,
|
||||
@@ -518,6 +541,7 @@ impl Editor {
|
||||
config,
|
||||
auto_pairs,
|
||||
exit_code: 0,
|
||||
#[cfg(feature = "tokio-runtime")]
|
||||
config_events: unbounded_channel(),
|
||||
}
|
||||
}
|
||||
@@ -526,6 +550,7 @@ impl Editor {
|
||||
self.config.load()
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio-runtime")]
|
||||
pub fn clear_idle_timer(&mut self) {
|
||||
// equivalent to internal Instant::far_future() (30 years)
|
||||
self.idle_timer
|
||||
@@ -533,6 +558,7 @@ impl Editor {
|
||||
.reset(Instant::now() + Duration::from_secs(86400 * 365 * 30));
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio-runtime")]
|
||||
pub fn reset_idle_timer(&mut self) {
|
||||
let config = self.config();
|
||||
self.idle_timer
|
||||
@@ -568,12 +594,14 @@ impl Editor {
|
||||
self._refresh();
|
||||
}
|
||||
|
||||
#[cfg(feature = "lsp")]
|
||||
/// Refreshes the language server for a given document
|
||||
pub fn refresh_language_server(&mut self, doc_id: DocumentId) -> Option<()> {
|
||||
let doc = self.documents.get_mut(&doc_id)?;
|
||||
Self::launch_language_server(&mut self.language_servers, doc)
|
||||
}
|
||||
|
||||
#[cfg(feature = "lsp")]
|
||||
/// Launch a language server for a given document
|
||||
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
|
||||
@@ -611,7 +639,7 @@ impl Editor {
|
||||
doc.set_language_server(Some(language_server));
|
||||
}
|
||||
}
|
||||
Some(())
|
||||
Some(()) // TODO: what's the deal with the return type
|
||||
}
|
||||
|
||||
fn _refresh(&mut self) {
|
||||
@@ -755,6 +783,7 @@ impl Editor {
|
||||
} else {
|
||||
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);
|
||||
|
||||
self.new_document(doc)
|
||||
@@ -792,6 +821,7 @@ impl Editor {
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "lsp")]
|
||||
if let Some(language_server) = doc.language_server() {
|
||||
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
|
||||
/// `timeout` parameter to override this.
|
||||
pub async fn close_language_servers(
|
||||
|
@@ -2,7 +2,7 @@ use crate::input::KeyEvent;
|
||||
use helix_core::{register::Registers, unicode::width::UnicodeWidthStr};
|
||||
use std::{collections::BTreeSet, fmt::Write};
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
/// Info box used in editor. Rendering logic will be in other crate.
|
||||
pub struct Info {
|
||||
/// Title shown at top.
|
||||
@@ -73,3 +73,55 @@ impl Info {
|
||||
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 {}
|
||||
|
@@ -2,10 +2,54 @@
|
||||
use anyhow::{anyhow, Error};
|
||||
use helix_core::unicode::width::UnicodeWidthStr;
|
||||
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.
|
||||
// We use a newtype here because we want to customize Deserialize and Display.
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash)]
|
||||
@@ -49,8 +93,8 @@ pub(crate) mod keys {
|
||||
pub(crate) const PERCENT: &str = "percent";
|
||||
}
|
||||
|
||||
impl fmt::Display for KeyEvent {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
impl std::fmt::Display for KeyEvent {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_fmt(format_args!(
|
||||
"{}{}{}",
|
||||
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>> {
|
||||
use anyhow::Context;
|
||||
let mut keys_res: anyhow::Result<_> = Ok(Vec::new());
|
||||
|
@@ -1,6 +1,5 @@
|
||||
use helix_view::Editor;
|
||||
|
||||
use crate::compositor::Compositor;
|
||||
use crate::Editor;
|
||||
|
||||
use futures_util::future::{self, BoxFuture, Future, FutureExt};
|
||||
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) {
|
||||
if j.wait {
|
||||
self.wait_futures.push(j.future);
|
@@ -55,7 +55,6 @@ impl From<crossterm::event::KeyModifiers> for KeyModifiers {
|
||||
|
||||
/// Represents a key.
|
||||
#[derive(Debug, PartialOrd, Ord, PartialEq, Eq, Clone, Copy, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
pub enum KeyCode {
|
||||
/// Backspace key.
|
||||
Backspace,
|
||||
|
@@ -2,12 +2,11 @@ pub mod default;
|
||||
pub mod macros;
|
||||
|
||||
pub use crate::commands::MappableCommand;
|
||||
use crate::config::Config;
|
||||
use crate::{document::Mode, info::Info, input::KeyEvent};
|
||||
use arc_swap::{
|
||||
access::{DynAccess, DynGuard},
|
||||
ArcSwap,
|
||||
};
|
||||
use helix_view::{document::Mode, info::Info, input::KeyEvent};
|
||||
use serde::Deserialize;
|
||||
use std::{
|
||||
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)]
|
||||
mod tests {
|
||||
use super::macros::keymap;
|
||||
use super::*;
|
||||
use arc_swap::access::Constant;
|
||||
use helix_core::hashmap;
|
||||
|
||||
#[test]
|
||||
@@ -392,103 +381,6 @@ mod tests {
|
||||
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]
|
||||
fn aliased_modes_are_same_in_default_keymap() {
|
||||
let keymaps = Keymaps::default().map();
|
@@ -5,7 +5,7 @@ use super::{Keymap, Mode};
|
||||
use helix_core::hashmap;
|
||||
|
||||
pub fn default() -> HashMap<Mode, Keymap> {
|
||||
let normal = keymap!({ "Normal mode"
|
||||
let mut normal = keymap!({ "Normal mode"
|
||||
"h" | "left" => move_char_left,
|
||||
"j" | "down" => move_line_down,
|
||||
"k" | "up" => move_line_up,
|
||||
@@ -43,10 +43,10 @@ pub fn default() -> HashMap<Mode, Keymap> {
|
||||
"h" => goto_line_start,
|
||||
"l" => goto_line_end,
|
||||
"s" => goto_first_nonwhitespace,
|
||||
"d" => goto_definition,
|
||||
"y" => goto_type_definition,
|
||||
"r" => goto_reference,
|
||||
"i" => goto_implementation,
|
||||
// "d" => goto_definition,
|
||||
// "y" => goto_type_definition,
|
||||
// "r" => goto_reference,
|
||||
// "i" => goto_implementation,
|
||||
"t" => goto_window_top,
|
||||
"c" => goto_window_center,
|
||||
"b" => goto_window_bottom,
|
||||
@@ -198,30 +198,10 @@ pub fn default() -> HashMap<Mode, Keymap> {
|
||||
"f" => file_picker,
|
||||
"F" => file_picker_in_current_directory,
|
||||
"b" => buffer_picker,
|
||||
"s" => symbol_picker,
|
||||
"S" => workspace_symbol_picker,
|
||||
"a" => code_action,
|
||||
// "s" => symbol_picker,
|
||||
// "S" => workspace_symbol_picker,
|
||||
// "a" => code_action,
|
||||
"'" => 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"
|
||||
"C-w" | "w" => rotate_view,
|
||||
"C-s" | "s" => hsplit,
|
||||
@@ -245,8 +225,8 @@ pub fn default() -> HashMap<Mode, Keymap> {
|
||||
"P" => paste_clipboard_before,
|
||||
"R" => replace_selections_with_clipboard,
|
||||
"/" => global_search,
|
||||
"k" => hover,
|
||||
"r" => rename_symbol,
|
||||
// "k" => hover,
|
||||
// "r" => rename_symbol,
|
||||
"?" => command_palette,
|
||||
},
|
||||
"z" => { "View"
|
||||
@@ -285,6 +265,34 @@ pub fn default() -> HashMap<Mode, Keymap> {
|
||||
"C-a" => increment,
|
||||
"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();
|
||||
select.merge_nodes(keymap!({ "Select mode"
|
||||
"h" | "left" => extend_char_left,
|
60
helix-view/src/keymap/macros.rs
Normal file
60
helix-view/src/keymap/macros.rs
Normal 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;
|
@@ -1,12 +1,24 @@
|
||||
#[macro_use]
|
||||
pub mod macros;
|
||||
|
||||
pub mod args;
|
||||
pub mod backend {
|
||||
#[cfg(feature = "term")]
|
||||
pub mod term;
|
||||
}
|
||||
pub mod clipboard;
|
||||
pub mod commands;
|
||||
pub mod compositor;
|
||||
pub mod document;
|
||||
pub mod editor;
|
||||
pub mod graphics;
|
||||
pub mod ui;
|
||||
pub use helix_graphics as graphics;
|
||||
pub mod gutter;
|
||||
pub mod job;
|
||||
pub mod keymap;
|
||||
pub use keymap::macros::*;
|
||||
pub mod handlers {
|
||||
#[cfg(feature = "dap")]
|
||||
pub mod dap;
|
||||
pub mod lsp;
|
||||
}
|
||||
@@ -40,6 +52,17 @@ slotmap::new_key_type! {
|
||||
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 {
|
||||
Top,
|
||||
Center,
|
||||
|
@@ -61,3 +61,69 @@ macro_rules! doc {
|
||||
$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,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@@ -1,12 +1,14 @@
|
||||
use crate::compositor::{Component, Context, EventResult};
|
||||
use crossterm::event::{Event, KeyCode, KeyEvent};
|
||||
use helix_view::editor::CompleteAction;
|
||||
use tui::buffer::Buffer as Surface;
|
||||
use crate::compositor::{self, Component, Context, Event, EventResult};
|
||||
use crate::editor::CompleteAction;
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crate::{
|
||||
graphics::Rect,
|
||||
input::{KeyCode, KeyEvent},
|
||||
Document, Editor,
|
||||
};
|
||||
use helix_core::{Change, Transaction};
|
||||
use helix_view::{graphics::Rect, Document, Editor};
|
||||
|
||||
use crate::commands;
|
||||
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)> {
|
||||
self.popup.required_size(viewport)
|
||||
}
|
||||
}
|
||||
|
||||
fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
|
||||
self.popup.render(area, surface, cx);
|
||||
#[cfg(feature = "term")]
|
||||
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 let Some(option) = self.popup.contents().selection() {
|
||||
@@ -311,7 +316,7 @@ impl Component for Completion {
|
||||
// ---
|
||||
// option.documentation
|
||||
|
||||
let (view, doc) = current!(cx.editor);
|
||||
let (view, doc) = current_ref!(cx.editor);
|
||||
let language = doc
|
||||
.language()
|
||||
.and_then(|scope| scope.strip_prefix("source."))
|
||||
@@ -369,7 +374,7 @@ impl Component for Completion {
|
||||
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 mut width = area
|
||||
.width
|
||||
@@ -403,8 +408,8 @@ impl Component for Completion {
|
||||
|
||||
// clear area
|
||||
let background = cx.editor.theme.get("ui.popup");
|
||||
surface.clear_with(area, background);
|
||||
markdown_doc.render(area, surface, cx);
|
||||
cx.surface.clear_with(area, background);
|
||||
markdown_doc.render(area, cx);
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,11 +1,19 @@
|
||||
use crate::{
|
||||
commands,
|
||||
compositor::{Component, Context, EventResult},
|
||||
key,
|
||||
commands, compositor, key,
|
||||
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::{
|
||||
coords_at_pos, encoding,
|
||||
graphemes::{
|
||||
@@ -17,24 +25,14 @@ use helix_core::{
|
||||
unicode::width::UnicodeWidthStr,
|
||||
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 crossterm::event::{Event, MouseButton, MouseEvent, MouseEventKind};
|
||||
use tui::buffer::Buffer as Surface;
|
||||
|
||||
pub struct EditorView {
|
||||
pub keymaps: Keymaps,
|
||||
on_next_key: Option<Box<dyn FnOnce(&mut commands::Context, KeyEvent)>>,
|
||||
last_insert: (commands::MappableCommand, Vec<InsertEvent>),
|
||||
pub(crate) completion: Option<Completion>,
|
||||
#[cfg(feature = "term")]
|
||||
pub(crate) completion: Option<ui::Completion>,
|
||||
spinners: ProgressSpinners,
|
||||
}
|
||||
|
||||
@@ -57,6 +55,7 @@ impl EditorView {
|
||||
keymaps,
|
||||
on_next_key: None,
|
||||
last_insert: (commands::MappableCommand::normal_mode, Vec::new()),
|
||||
#[cfg(feature = "term")]
|
||||
completion: None,
|
||||
spinners: ProgressSpinners::default(),
|
||||
}
|
||||
@@ -65,7 +64,13 @@ impl EditorView {
|
||||
pub fn spinners_mut(&mut self) -> &mut ProgressSpinners {
|
||||
&mut self.spinners
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "term")]
|
||||
use tui::buffer::Buffer as Surface;
|
||||
|
||||
#[cfg(feature = "term")]
|
||||
impl EditorView {
|
||||
pub fn render_view(
|
||||
&self,
|
||||
editor: &Editor,
|
||||
@@ -80,6 +85,7 @@ impl EditorView {
|
||||
let theme = &editor.theme;
|
||||
|
||||
// DAP: Highlight current stack frame position
|
||||
#[cfg(feature = "dap")]
|
||||
let stack_frame = editor.debugger.as_ref().and_then(|debugger| {
|
||||
if let (Some(frame), Some(thread_id)) = (debugger.active_frame, debugger.thread_id) {
|
||||
debugger
|
||||
@@ -90,6 +96,7 @@ impl EditorView {
|
||||
None
|
||||
}
|
||||
});
|
||||
#[cfg(feature = "dap")]
|
||||
if let Some(frame) = stack_frame {
|
||||
if doc.path().is_some()
|
||||
&& frame
|
||||
@@ -352,9 +359,9 @@ impl EditorView {
|
||||
surface: &mut Surface,
|
||||
theme: &Theme,
|
||||
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
|
||||
// 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
|
||||
let visual_tab_width = tab_width - (visual_x as usize % 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)
|
||||
} else if grapheme == " " {
|
||||
@@ -776,7 +783,9 @@ impl EditorView {
|
||||
true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl EditorView {
|
||||
/// Handle events by looking them up in `self.keymaps`. Returns None
|
||||
/// if event was handled (a command was executed or a subkeymap was
|
||||
/// activated). Only KeymapResult::{NotFound, Cancelled} is returned
|
||||
@@ -890,6 +899,7 @@ impl EditorView {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "term")]
|
||||
pub fn set_completion(
|
||||
&mut self,
|
||||
editor: &mut Editor,
|
||||
@@ -900,7 +910,7 @@ impl EditorView {
|
||||
size: Rect,
|
||||
) {
|
||||
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() {
|
||||
// skip if we got no completion results
|
||||
@@ -918,6 +928,7 @@ impl EditorView {
|
||||
self.completion = Some(completion);
|
||||
}
|
||||
|
||||
#[cfg(feature = "term")]
|
||||
pub fn clear_completion(&mut self, editor: &mut Editor) {
|
||||
self.completion = None;
|
||||
|
||||
@@ -927,6 +938,7 @@ impl EditorView {
|
||||
editor.clear_idle_timer(); // don't retrigger
|
||||
}
|
||||
|
||||
#[cfg(feature = "term")]
|
||||
pub fn handle_idle_timeout(&mut self, cx: &mut crate::compositor::Context) -> EventResult {
|
||||
if self.completion.is_some()
|
||||
|| !cx.editor.config().auto_completion
|
||||
@@ -947,9 +959,7 @@ impl EditorView {
|
||||
|
||||
EventResult::Consumed(None)
|
||||
}
|
||||
}
|
||||
|
||||
impl EditorView {
|
||||
fn handle_mouse_event(
|
||||
&mut self,
|
||||
event: MouseEvent,
|
||||
@@ -957,6 +967,7 @@ impl EditorView {
|
||||
) -> EventResult {
|
||||
let config = cxt.editor.config();
|
||||
match event {
|
||||
#[cfg(feature = "dap")]
|
||||
MouseEvent {
|
||||
kind: MouseEventKind::Down(MouseButton::Left),
|
||||
row,
|
||||
@@ -974,7 +985,7 @@ impl EditorView {
|
||||
if let Some((pos, view_id)) = result {
|
||||
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();
|
||||
doc.set_selection(view_id, selection.push(Range::point(pos)));
|
||||
} else {
|
||||
@@ -1084,6 +1095,7 @@ impl EditorView {
|
||||
EventResult::Consumed(None)
|
||||
}
|
||||
|
||||
#[cfg(feature = "dap")]
|
||||
MouseEvent {
|
||||
kind: MouseEventKind::Up(MouseButton::Right),
|
||||
row,
|
||||
@@ -1104,7 +1116,7 @@ impl EditorView {
|
||||
let line = coords.row + view.offset.row;
|
||||
if let Ok(pos) = doc.text().try_line_to_char(line) {
|
||||
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);
|
||||
} else {
|
||||
commands::MappableCommand::dap_edit_condition.execute(cxt);
|
||||
@@ -1128,7 +1140,7 @@ impl EditorView {
|
||||
return EventResult::Ignored(None);
|
||||
}
|
||||
|
||||
if modifiers == crossterm::event::KeyModifiers::ALT {
|
||||
if modifiers == KeyModifiers::ALT {
|
||||
commands::MappableCommand::replace_selections_with_primary_clipboard
|
||||
.execute(cxt);
|
||||
|
||||
@@ -1177,9 +1189,9 @@ impl Component for EditorView {
|
||||
// Handling it here but not re-rendering will cause flashing
|
||||
EventResult::Consumed(None)
|
||||
}
|
||||
Event::Key(key) => {
|
||||
Event::Key(mut key) => {
|
||||
#[cfg(feature = "term")]
|
||||
cx.editor.reset_idle_timer();
|
||||
let mut key = KeyEvent::from(key);
|
||||
canonicalize_key(&mut key);
|
||||
|
||||
// clear status
|
||||
@@ -1196,12 +1208,12 @@ impl Component for EditorView {
|
||||
Mode::Insert => {
|
||||
// let completion swallow the event if necessary
|
||||
let mut consumed = false;
|
||||
#[cfg(feature = "term")]
|
||||
if let Some(completion) = &mut self.completion {
|
||||
// use a fake context here
|
||||
let mut cx = Context {
|
||||
editor: cx.editor,
|
||||
jobs: cx.jobs,
|
||||
scroll: None,
|
||||
};
|
||||
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 !consumed {
|
||||
#[cfg(feature = "term")]
|
||||
if let Some(compl) = cx.editor.last_completion.take() {
|
||||
self.last_insert.1.push(InsertEvent::CompletionApply(compl));
|
||||
}
|
||||
@@ -1227,6 +1240,7 @@ impl Component for EditorView {
|
||||
self.last_insert.1.push(InsertEvent::Key(key));
|
||||
|
||||
// lastly we recalculate completion
|
||||
#[cfg(feature = "term")]
|
||||
if let Some(completion) = &mut self.completion {
|
||||
completion.update(&mut cx);
|
||||
if completion.is_empty() {
|
||||
@@ -1274,6 +1288,7 @@ impl Component for EditorView {
|
||||
};
|
||||
self.last_insert.1.clear();
|
||||
}
|
||||
#[cfg(feature = "term")]
|
||||
(Mode::Insert, Mode::Normal) => {
|
||||
// if exiting insert mode, remove completion
|
||||
self.completion = None;
|
||||
@@ -1287,23 +1302,26 @@ impl Component for EditorView {
|
||||
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
|
||||
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();
|
||||
// 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() {
|
||||
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 let Some(mut info) = cx.editor.autoinfo.take() {
|
||||
info.render(area, surface, cx);
|
||||
cx.editor.autoinfo = Some(info)
|
||||
// TODO: drop &mut self on render
|
||||
if let Some(mut info) = cx.editor.autoinfo.clone() {
|
||||
info.render(area, cx);
|
||||
// cx.editor.autoinfo = Some(info)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1313,14 +1331,14 @@ impl Component for EditorView {
|
||||
// render status msg
|
||||
if let Some((status_msg, severity)) = &cx.editor.status_msg {
|
||||
status_msg_width = status_msg.width();
|
||||
use helix_view::editor::Severity;
|
||||
use crate::editor::Severity;
|
||||
let style = if *severity == Severity::Error {
|
||||
cx.editor.theme.get("error")
|
||||
} else {
|
||||
cx.editor.theme.get("ui.text")
|
||||
};
|
||||
|
||||
surface.set_string(
|
||||
cx.surface.set_string(
|
||||
area.x,
|
||||
area.y + area.height.saturating_sub(1),
|
||||
status_msg,
|
||||
@@ -1350,7 +1368,7 @@ impl Component for EditorView {
|
||||
} else {
|
||||
0
|
||||
};
|
||||
surface.set_string(
|
||||
cx.surface.set_string(
|
||||
area.x + area.width.saturating_sub(key_width + macro_width),
|
||||
area.y + area.height.saturating_sub(1),
|
||||
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 {
|
||||
let disp = format!("[{}]", reg);
|
||||
let style = style
|
||||
.fg(helix_view::graphics::Color::Yellow)
|
||||
.fg(crate::graphics::Color::Yellow)
|
||||
.add_modifier(Modifier::BOLD);
|
||||
surface.set_string(
|
||||
cx.surface.set_string(
|
||||
area.x + area.width.saturating_sub(3),
|
||||
area.y + area.height.saturating_sub(1),
|
||||
&disp,
|
||||
@@ -1372,7 +1390,7 @@ impl Component for EditorView {
|
||||
}
|
||||
|
||||
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) {
|
||||
if let KeyEvent {
|
||||
code: KeyCode::Char(_),
|
@@ -1,22 +1,22 @@
|
||||
use crate::compositor::{Component, Context};
|
||||
use tui::{
|
||||
buffer::Buffer as Surface,
|
||||
text::{Span, Spans, Text},
|
||||
};
|
||||
use crate::compositor::{self, Component, RenderContext};
|
||||
|
||||
#[cfg(feature = "term")]
|
||||
use tui::text::{Span, Spans, Text};
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use pulldown_cmark::{CodeBlockKind, Event, HeadingLevel, Options, Parser, Tag};
|
||||
|
||||
use crate::{
|
||||
graphics::{Margin, Rect, Style},
|
||||
Theme,
|
||||
};
|
||||
use helix_core::{
|
||||
syntax::{self, HighlightEvent, Syntax},
|
||||
Rope,
|
||||
};
|
||||
use helix_view::{
|
||||
graphics::{Margin, Rect, Style},
|
||||
Theme,
|
||||
};
|
||||
|
||||
#[cfg(feature = "term")]
|
||||
fn styled_multiline_text<'a>(text: String, style: Style) -> Text<'a> {
|
||||
let spans: Vec<_> = text
|
||||
.lines()
|
||||
@@ -26,6 +26,7 @@ fn styled_multiline_text<'a>(text: String, style: Style) -> Text<'a> {
|
||||
Text::from(spans)
|
||||
}
|
||||
|
||||
#[cfg(feature = "term")]
|
||||
pub fn highlighted_code_block<'a>(
|
||||
text: String,
|
||||
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<'_> {
|
||||
// // 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```";
|
||||
@@ -258,8 +263,9 @@ impl Markdown {
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for Markdown {
|
||||
fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
|
||||
#[cfg(feature = "term")]
|
||||
impl compositor::term::Render for Markdown {
|
||||
fn render(&mut self, area: Rect, cx: &mut RenderContext<'_>) {
|
||||
use tui::widgets::{Paragraph, Widget, Wrap};
|
||||
|
||||
let text = self.parse(Some(&cx.editor.theme));
|
||||
@@ -272,9 +278,20 @@ impl Component for Markdown {
|
||||
vertical: 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)> {
|
||||
let padding = 2;
|
||||
if padding >= viewport.1 || padding >= viewport.0 {
|
@@ -1,17 +1,15 @@
|
||||
use crate::{
|
||||
compositor::{Callback, Component, Compositor, Context, EventResult},
|
||||
ctrl, key, shift,
|
||||
use crate::compositor::{
|
||||
self, Callback, Component, Compositor, Context, Event, EventResult, RenderContext,
|
||||
};
|
||||
use crossterm::event::Event;
|
||||
use tui::{buffer::Buffer as Surface, widgets::Table};
|
||||
|
||||
pub use tui::widgets::{Cell, Row};
|
||||
use crate::{ctrl, key, shift};
|
||||
|
||||
use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
|
||||
use fuzzy_matcher::FuzzyMatcher;
|
||||
|
||||
use helix_view::{graphics::Rect, Editor};
|
||||
use crate::{graphics::Rect, Editor};
|
||||
|
||||
use tui::layout::Constraint;
|
||||
pub use tui::widgets::{Cell, Row};
|
||||
|
||||
pub trait Item {
|
||||
fn label(&self) -> &str;
|
||||
@@ -210,7 +208,7 @@ impl<T: Item + 'static> Component for Menu<T> {
|
||||
compositor.pop();
|
||||
}));
|
||||
|
||||
match event.into() {
|
||||
match event {
|
||||
// esc or ctrl-c aborts the completion and closes the menu
|
||||
key!(Esc) | ctrl!('c') => {
|
||||
(self.callback_fn)(cx.editor, self.selection(), MenuEvent::Abort);
|
||||
@@ -264,8 +262,13 @@ impl<T: Item + 'static> Component for Menu<T> {
|
||||
|
||||
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 style = theme
|
||||
.try_get("ui.menu")
|
||||
@@ -307,7 +310,7 @@ impl<T: Item + 'static> Component for Menu<T> {
|
||||
|
||||
table.render_table(
|
||||
area,
|
||||
surface,
|
||||
cx.surface,
|
||||
&mut TableState {
|
||||
offset: scroll,
|
||||
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;
|
||||
|
||||
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_style(selected);
|
||||
// cell.set_style(if is_marked { selected } else { style });
|
@@ -1,28 +1,33 @@
|
||||
#[cfg(feature = "term")]
|
||||
mod completion;
|
||||
pub(crate) mod editor;
|
||||
mod info;
|
||||
mod markdown;
|
||||
#[cfg(feature = "term")]
|
||||
pub mod menu;
|
||||
pub mod overlay;
|
||||
mod picker;
|
||||
mod popup;
|
||||
mod prompt;
|
||||
mod spinner;
|
||||
#[cfg(feature = "term")]
|
||||
mod text;
|
||||
|
||||
#[cfg(feature = "term")]
|
||||
pub use completion::Completion;
|
||||
pub use editor::EditorView;
|
||||
pub use markdown::Markdown;
|
||||
#[cfg(feature = "term")]
|
||||
pub use menu::Menu;
|
||||
pub use picker::{FileLocation, FilePicker, Picker};
|
||||
pub use popup::Popup;
|
||||
pub use prompt::{Prompt, PromptEvent};
|
||||
pub use spinner::{ProgressSpinners, Spinner};
|
||||
#[cfg(feature = "term")]
|
||||
pub use text::Text;
|
||||
|
||||
use crate::{Document, Editor, View};
|
||||
use helix_core::regex::Regex;
|
||||
use helix_core::regex::RegexBuilder;
|
||||
use helix_view::{Document, Editor, View};
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
@@ -112,7 +117,7 @@ pub fn regex_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 std::time::Instant;
|
||||
|
||||
@@ -189,12 +194,12 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi
|
||||
}
|
||||
|
||||
pub mod completers {
|
||||
use crate::document::SCRATCH_BUFFER_NAME;
|
||||
use crate::theme;
|
||||
use crate::ui::prompt::Completion;
|
||||
use crate::{editor::Config, Editor};
|
||||
use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
|
||||
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 std::borrow::Cow;
|
||||
use std::cmp::Reverse;
|
@@ -1,12 +1,11 @@
|
||||
use crossterm::event::Event;
|
||||
use helix_core::Position;
|
||||
use helix_view::{
|
||||
use crate::{
|
||||
compositor,
|
||||
graphics::{CursorKind, Rect},
|
||||
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
|
||||
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> {
|
||||
fn render(&mut self, area: Rect, frame: &mut Buffer, ctx: &mut Context) {
|
||||
#[cfg(feature = "term")]
|
||||
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);
|
||||
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)> {
|
||||
let area = Rect {
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
}
|
@@ -1,17 +1,11 @@
|
||||
use crate::compositor::{self, Component, Compositor, Context, Event, EventResult, RenderContext};
|
||||
use crate::{
|
||||
compositor::{Component, Compositor, Context, EventResult},
|
||||
ctrl, key, shift,
|
||||
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::FuzzyMatcher;
|
||||
use tui::widgets::Widget;
|
||||
|
||||
use std::time::Instant;
|
||||
use std::{
|
||||
@@ -23,12 +17,12 @@ use std::{
|
||||
};
|
||||
|
||||
use crate::ui::{Prompt, PromptEvent};
|
||||
use helix_core::{movement::Direction, Position};
|
||||
use helix_view::{
|
||||
use crate::{
|
||||
editor::Action,
|
||||
graphics::{Color, CursorKind, Margin, Modifier, Rect, Style},
|
||||
Document, Editor,
|
||||
};
|
||||
use helix_core::{movement::Direction, Position};
|
||||
|
||||
pub const MIN_AREA_WIDTH_FOR_PREVIEW: u16 = 72;
|
||||
/// Biggest file size to preview in bytes
|
||||
@@ -163,8 +157,11 @@ impl<T> FilePicker<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: 'static> Component for FilePicker<T> {
|
||||
fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
|
||||
#[cfg(feature = "term")]
|
||||
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 |
|
||||
// +---------+ | |
|
||||
@@ -177,7 +174,7 @@ impl<T: 'static> Component for FilePicker<T> {
|
||||
// clear area
|
||||
let background = cx.editor.theme.get("ui.background");
|
||||
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 {
|
||||
area.width / 2
|
||||
@@ -186,7 +183,7 @@ impl<T: 'static> Component for FilePicker<T> {
|
||||
};
|
||||
|
||||
let picker_area = area.with_width(picker_width);
|
||||
self.picker.render(picker_area, surface, cx);
|
||||
self.picker.render(picker_area, cx);
|
||||
|
||||
if !render_preview {
|
||||
return;
|
||||
@@ -205,7 +202,7 @@ impl<T: 'static> Component for FilePicker<T> {
|
||||
horizontal: 1,
|
||||
};
|
||||
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) {
|
||||
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 x = inner.x + inner.width.saturating_sub(alt_text.len() as u16) / 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;
|
||||
}
|
||||
};
|
||||
@@ -237,7 +235,7 @@ impl<T: 'static> Component for FilePicker<T> {
|
||||
doc,
|
||||
offset,
|
||||
inner,
|
||||
surface,
|
||||
cx.surface,
|
||||
&cx.editor.theme,
|
||||
highlights,
|
||||
&cx.editor.config().whitespace,
|
||||
@@ -246,7 +244,7 @@ impl<T: 'static> Component for FilePicker<T> {
|
||||
// highlight the line
|
||||
if let Some((start, end)) = range {
|
||||
let offset = start.saturating_sub(first_line) as u16;
|
||||
surface.set_style(
|
||||
cx.surface.set_style(
|
||||
Rect::new(
|
||||
inner.x,
|
||||
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 {
|
||||
// TODO: keybinds for scrolling preview
|
||||
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)> {
|
||||
let picker_width = if width > MIN_AREA_WIDTH_FOR_PREVIEW {
|
||||
width / 2
|
||||
@@ -498,7 +501,7 @@ impl<T: 'static> Component for Picker<T> {
|
||||
compositor.last_picker = compositor.pop();
|
||||
})));
|
||||
|
||||
match key_event.into() {
|
||||
match key_event {
|
||||
shift!(Tab) | key!(Up) | ctrl!('p') => {
|
||||
self.move_by(1, Direction::Backward);
|
||||
}
|
||||
@@ -551,8 +554,13 @@ impl<T: 'static> Component for Picker<T> {
|
||||
|
||||
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 selected = cx.editor.theme.get("ui.text.focus");
|
||||
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:
|
||||
// clear area
|
||||
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
|
||||
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
|
||||
let inner = block.inner(area);
|
||||
|
||||
block.render(area, surface);
|
||||
block.render(area, cx.surface);
|
||||
|
||||
// -- Render the input bar:
|
||||
|
||||
let area = inner.clip_left(1).with_height(1);
|
||||
|
||||
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.y,
|
||||
&count,
|
||||
@@ -583,13 +591,13 @@ impl<T: 'static> Component for Picker<T> {
|
||||
text_style,
|
||||
);
|
||||
|
||||
self.prompt.render(area, surface, cx);
|
||||
self.prompt.render(area, cx);
|
||||
|
||||
// -- Separator
|
||||
let sep_style = Style::default().fg(Color::Rgb(90, 89, 119));
|
||||
let borders = BorderType::line_symbols(BorderType::Plain);
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -610,7 +618,8 @@ impl<T: 'static> Component for Picker<T> {
|
||||
for (i, (_index, option)) in files.take(rows as usize).enumerate() {
|
||||
let is_active = i == (self.cursor - offset);
|
||||
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);
|
||||
@@ -620,7 +629,7 @@ impl<T: 'static> Component for Picker<T> {
|
||||
.fuzzy_indices(&formatted, self.prompt.line())
|
||||
.unwrap_or_default();
|
||||
|
||||
surface.set_string_truncated(
|
||||
cx.surface.set_string_truncated(
|
||||
inner.x,
|
||||
inner.y + i as u16,
|
||||
&formatted,
|
||||
@@ -641,6 +650,7 @@ impl<T: 'static> Component for Picker<T> {
|
||||
}
|
||||
|
||||
fn cursor(&self, area: Rect, editor: &Editor) -> (Option<Position>, CursorKind) {
|
||||
use tui::widgets::{Block, Borders};
|
||||
let block = Block::default().borders(Borders::ALL);
|
||||
// calculate the inner area inside the box
|
||||
let inner = block.inner(area);
|
||||
@@ -651,3 +661,6 @@ impl<T: 'static> Component for Picker<T> {
|
||||
self.prompt.cursor(area, editor)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui")]
|
||||
impl<T: 'static> compositor::ui::Render for Picker<T> {}
|
@@ -1,12 +1,11 @@
|
||||
use crate::{
|
||||
compositor::{Callback, Component, Context, EventResult},
|
||||
ctrl, key,
|
||||
};
|
||||
use crossterm::event::Event;
|
||||
use tui::buffer::Buffer as Surface;
|
||||
use crate::compositor::{self, Callback, Component, Context, Event, EventResult, RenderContext};
|
||||
use crate::{ctrl, key};
|
||||
|
||||
use crate::{
|
||||
graphics::{Margin, Rect},
|
||||
Editor,
|
||||
};
|
||||
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
|
||||
// a width/height hint. maybe Popup(Box<Component>)
|
||||
@@ -53,10 +52,10 @@ impl<T: Component> Popup<T> {
|
||||
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
|
||||
.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;
|
||||
|
||||
@@ -122,7 +121,7 @@ impl<T: Component> Component for Popup<T> {
|
||||
compositor.pop();
|
||||
});
|
||||
|
||||
match key.into() {
|
||||
match key {
|
||||
// esc or ctrl-c aborts the completion and closes the menu
|
||||
key!(Esc) | ctrl!('c') => {
|
||||
let _ = self.contents.handle_event(event, cx);
|
||||
@@ -176,26 +175,32 @@ impl<T: Component> Component for Popup<T> {
|
||||
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
|
||||
self.required_size((viewport.width, viewport.height));
|
||||
|
||||
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
|
||||
let area = viewport.intersection(Rect::new(rel_x, rel_y, self.size.0, self.size.1));
|
||||
|
||||
// clear area
|
||||
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);
|
||||
self.contents.render(inner, surface, cx);
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<&'static str> {
|
||||
Some(self.id)
|
||||
self.contents.render(inner, cx);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "ui")]
|
||||
impl<T: Component> compositor::ui::Render for Popup<T> {}
|
@@ -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 crossterm::event::Event;
|
||||
use helix_view::input::KeyEvent;
|
||||
use helix_view::keyboard::KeyCode;
|
||||
use std::{borrow::Cow, ops::RangeFrom};
|
||||
use tui::buffer::Buffer as Surface;
|
||||
use tui::widgets::{Block, Borders, Widget};
|
||||
|
||||
use helix_core::{
|
||||
unicode::segmentation::GraphemeCursor, unicode::width::UnicodeWidthStr, Position,
|
||||
};
|
||||
use helix_view::{
|
||||
use crate::{
|
||||
graphics::{CursorKind, Margin, Rect},
|
||||
Editor,
|
||||
};
|
||||
use helix_core::{
|
||||
unicode::segmentation::GraphemeCursor, unicode::width::UnicodeWidthStr, Position,
|
||||
};
|
||||
|
||||
pub type Completion = (RangeFrom<usize>, Cow<'static, str>);
|
||||
|
||||
@@ -326,8 +323,11 @@ impl Prompt {
|
||||
|
||||
const BASE_WIDTH: u16 = 30;
|
||||
|
||||
impl Prompt {
|
||||
pub fn render_prompt(&self, area: Rect, surface: &mut Surface, cx: &mut Context) {
|
||||
#[cfg(feature = "term")]
|
||||
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 prompt_color = theme.get("ui.text");
|
||||
let completion_color = theme.get("ui.statusline");
|
||||
@@ -367,7 +367,7 @@ impl Prompt {
|
||||
.map(|selection| selection / items * items)
|
||||
.unwrap_or_default();
|
||||
|
||||
surface.clear_with(area, background);
|
||||
cx.surface.clear_with(area, background);
|
||||
|
||||
let mut row = 0;
|
||||
let mut col = 0;
|
||||
@@ -380,7 +380,7 @@ impl Prompt {
|
||||
} else {
|
||||
completion_color
|
||||
};
|
||||
surface.set_stringn(
|
||||
cx.surface.set_stringn(
|
||||
area.x + col * (1 + col_width),
|
||||
area.y + row,
|
||||
&completion,
|
||||
@@ -413,7 +413,7 @@ impl Prompt {
|
||||
));
|
||||
|
||||
let background = theme.get("ui.help");
|
||||
surface.clear_with(area, background);
|
||||
cx.surface.clear_with(area, background);
|
||||
|
||||
let block = Block::default()
|
||||
// .title(self.title.as_str())
|
||||
@@ -425,22 +425,39 @@ impl Prompt {
|
||||
horizontal: 1,
|
||||
});
|
||||
|
||||
block.render(area, surface);
|
||||
text.render(inner, surface, cx);
|
||||
block.render(area, cx.surface);
|
||||
text.render(inner, cx);
|
||||
}
|
||||
|
||||
let line = area.height - 1;
|
||||
// render buffer text
|
||||
surface.set_string(area.x, area.y + line, &self.prompt, prompt_color);
|
||||
surface.set_string(
|
||||
cx.surface
|
||||
.set_string(area.x, area.y + line, &self.prompt, prompt_color);
|
||||
cx.surface.set_string(
|
||||
area.x + self.prompt.len() as u16,
|
||||
area.y + line,
|
||||
&self.line,
|
||||
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 {
|
||||
fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
|
||||
let event = match event {
|
||||
@@ -454,7 +471,7 @@ impl Component for Prompt {
|
||||
compositor.pop();
|
||||
})));
|
||||
|
||||
match event.into() {
|
||||
match event {
|
||||
ctrl!('c') | key!(Esc) => {
|
||||
(self.callback_fn)(cx, &self.line, PromptEvent::Abort);
|
||||
return close_fn;
|
||||
@@ -546,21 +563,4 @@ impl Component for Prompt {
|
||||
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
@@ -1,7 +1,5 @@
|
||||
use crate::compositor::{Component, Context};
|
||||
use tui::buffer::Buffer as Surface;
|
||||
|
||||
use helix_view::graphics::Rect;
|
||||
use crate::compositor::{self, Component, RenderContext};
|
||||
use crate::graphics::Rect;
|
||||
|
||||
pub struct Text {
|
||||
pub(crate) contents: tui::text::Text<'static>,
|
||||
@@ -29,16 +27,19 @@ impl From<tui::text::Text<'static>> for Text {
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for Text {
|
||||
fn render(&mut self, area: Rect, surface: &mut Surface, _cx: &mut Context) {
|
||||
#[cfg(feature = "term")]
|
||||
impl compositor::term::Render for Text {
|
||||
fn render(&mut self, area: Rect, cx: &mut RenderContext<'_>) {
|
||||
use tui::widgets::{Paragraph, Widget, Wrap};
|
||||
|
||||
let par = Paragraph::new(self.contents.clone()).wrap(Wrap { trim: false });
|
||||
// .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)> {
|
||||
if viewport != self.viewport {
|
||||
let width = std::cmp::min(self.contents.width() as u16, viewport.0);
|
@@ -1,3 +1,4 @@
|
||||
[toolchain]
|
||||
channel = "stable"
|
||||
components = ["rustfmt", "rust-src"]
|
||||
targets = [ "wasm32-unknown-unknown" ]
|
Reference in New Issue
Block a user