Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sublime-like changes #109

Open
wants to merge 37 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
d202ae3
Update Cargo.lock
msirringhaus Jun 24, 2019
83692fd
First step towards configurable keybindings: TUI can use bindings pro…
msirringhaus Jun 24, 2019
4f9d809
Copy keybindings to editor and all views. Ugly, but I cannot find a n…
msirringhaus Jun 24, 2019
2d2763e
Reshaped Command to have relative and absolute move as variant instea…
msirringhaus Jun 24, 2019
a4e9f93
Editor now holds the keymap alone and translates it for the view. All…
msirringhaus Jun 25, 2019
386f22b
Rename functions to avoid key-name <-> functionality confusion
msirringhaus Jun 25, 2019
dfd0781
Merge branch 'keybindings'
msirringhaus Jun 25, 2019
0699d1c
Prepare to switch to rust edition 2018
msirringhaus Jun 25, 2019
0bc9269
Rust 2018: run cargo fix --edition-idioms
msirringhaus Jun 25, 2019
117299f
Remove duplicate functions in editor, view and client. Now each objec…
msirringhaus Jun 26, 2019
702b140
Make Insert a command as well and let the client handle it
msirringhaus Jun 26, 2019
39995fd
Add command to hide prompt
msirringhaus Jun 26, 2019
1c11099
Add undo/redo (unfortunately, there is a bug in termion that does not…
msirringhaus Jun 26, 2019
db5488b
Add multi-cursor support! Find current selection and add cursors on e…
msirringhaus Jun 26, 2019
1424a88
Implement copy&paste
msirringhaus Jun 27, 2019
2b38760
Implement (almost) all move commands
msirringhaus Jun 27, 2019
decb28f
Embedd sublime keybindings for now. This is stupid but easier for now…
msirringhaus Jun 27, 2019
c276d9b
Restructure Command a bit so that there is a single parsing function …
msirringhaus Jun 27, 2019
b6472f1
Make open-command work with tilde and env-variables.
msirringhaus Jun 27, 2019
fbd71c2
Implement close current view (last view always remains).
msirringhaus Jun 27, 2019
574c9a9
Implement select_all command
msirringhaus Jun 28, 2019
15ce710
Unify and simplify prompt parsing a bit
msirringhaus Jun 28, 2019
495083c
Add (terminal-specific) keys for next/prev buffer.
msirringhaus Jun 28, 2019
e31365d
First somewhat functioning implementation of Find()
msirringhaus Jun 28, 2019
62a601f
Implement configurable search
msirringhaus Jun 28, 2019
6c32c20
Add explanation of how search works above the search prompt.
msirringhaus Jun 28, 2019
0e77c3d
Activate move_to linenumber as prompt command
msirringhaus Jun 28, 2019
ab35a26
Implement function to set multiple cursor with clicking middle mouse …
msirringhaus Jun 28, 2019
f19eb17
First, hacky and incomplete draft of commandprompt with suggestions
msirringhaus Jun 28, 2019
8bfcfa1
Generate nicer names for prompt suggestions. This is really ugly and …
msirringhaus Jun 28, 2019
694b3b1
Move clipboard to Editor. Had a clipboard for each view, which meant …
msirringhaus Jun 29, 2019
88c8b35
Major rework of command structure. Known bug: Keybindings of subcomma…
msirringhaus Jun 30, 2019
3790457
Remove old, left-over comment
msirringhaus Jul 2, 2019
b1f795c
Adjust logging levels to be more sensible.
msirringhaus Jul 2, 2019
e94e3e9
Make enum-names rusty and use serdes rename_all to use it for parsing.
msirringhaus Jul 3, 2019
7e1aea1
Prompt: Fix prompt parsing and close prompt after finalizing.
msirringhaus Jul 3, 2019
75dff11
Fix prompt parsing of select_lines
msirringhaus Jul 3, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
183 changes: 165 additions & 18 deletions Cargo.lock

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,6 @@ tokio = "0.1.21"
xdg = "2.2.0"
indexmap = "1.0.2"
xrl = "0.0.7"
serde = { version = "1.0.93", features = ["derive"] }
serde_json = "1.0.39"
json5 = "0.2.4"
742 changes: 742 additions & 0 deletions configs/Default (Linux).sublime-keymap

Large diffs are not rendered by default.

15 changes: 9 additions & 6 deletions src/core/cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use xrl::ViewId;

use std::str::FromStr;

#[derive(Debug)]
#[derive(Debug, PartialEq, Clone)]
pub enum Command {
/// Close the CommandPrompt.
Cancel,
Expand Down Expand Up @@ -39,6 +39,8 @@ pub enum Command {
SetTheme(String),
/// Toggle displaying line numbers.
ToggleLineNumbers,
/// Open prompt for user-input
OpenPrompt,
}

#[derive(Debug)]
Expand Down Expand Up @@ -67,18 +69,19 @@ impl FromStr for Command {
fn from_str(s: &str) -> Result<Command, Self::Err> {
match &s[..] {
"s" | "save" => Ok(Command::Save(None)),
"q" | "quit" => Ok(Command::Quit),
"b" | "back" => Ok(Command::Back),
"d" | "delete" => Ok(Command::Delete),
"bn" | "next-buffer" => Ok(Command::NextBuffer),
"bp" | "prev-buffer" => Ok(Command::PrevBuffer),
"q" | "quit" | "exit" => Ok(Command::Quit),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should refrain from adding more command aliases. We could discuss modifying the existing ones, but I think it will confusing to have too many commands that do the same thing.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, I never planned to do a full PR.
I switched to command names used by sublime texts keymap-file, but didn't want to remove the old names just yet.

"b" | "back" | "left_delete" => Ok(Command::Back),
"d" | "delete" | "right_delete" => Ok(Command::Delete),
"bn" | "next-buffer" | "next_view" => Ok(Command::NextBuffer),
"bp" | "prev-buffer" | "prev_view" => Ok(Command::PrevBuffer),
"pd" | "page-down" => Ok(Command::PageDown),
"pu" | "page-up" => Ok(Command::PageUp),
"ml" | "move-left" => Ok(Command::MoveLeft),
"mr" | "move-right" => Ok(Command::MoveRight),
"mu" | "move-up" => Ok(Command::MoveUp),
"md" | "move-down" => Ok(Command::MoveDown),
"ln" | "line-numbers" => Ok(Command::ToggleLineNumbers),
"op" | "open-prompt" | "show_overlay" => Ok(Command::OpenPrompt),
command => {
let mut parts: Vec<&str> = command.split(' ').collect();

Expand Down
135 changes: 135 additions & 0 deletions src/core/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
use crate::core::Command;
use termion::event::{Event, Key};

use serde::{Deserialize, Serialize};
use serde_json::Value;

use std::path::{Path, PathBuf};
use std::fs;
use std::collections::HashMap;

use std::str::FromStr;

#[derive(Debug, Serialize, Deserialize)]
struct Keybinding {
keys: Vec<String>,
command: String,
// For now, unstructured value
args: Option<Value>,
context: Option<Value>,
}

pub struct KeybindingConfig {
pub keymap: HashMap<Event, Command>,
pub config_path: PathBuf
}

impl KeybindingConfig {
pub fn parse(config_path: &Path) -> Result<KeybindingConfig, Box<std::error::Error + Sync + Send + 'static>> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably have an ConfigError error instead of using a trait object. Something like:

enum ConfigError {
    Parsing(...), // contains the inner error
    Invalid(....),
    .... // potentially a bunch of other variants.
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, error handling was pretty low on my list, to be honest. I basically just reused existing error types. This definetly needs a major cleanup (I actually think of removing "failure"-crate in the process, as its not really maintained anymore AFAIK)

let entries = fs::read_to_string(config_path)?;
// Read the JSON contents of the file as an instance of `User`.
let bindings: Vec<Keybinding> = json5::from_str(&entries)?;
error!("Bindings parsed!");
msirringhaus marked this conversation as resolved.
Show resolved Hide resolved

let mut keymap = HashMap::new();
let mut found_cmds = Vec::new();
for binding in bindings {
let cmd = match Command::from_str(&binding.command) {
Ok(cmd) => cmd,
// unimplemented command for now
Err(_) => continue,
};
error!("{:?} = {:?}", cmd, binding.keys);
if found_cmds.contains(&cmd) {
continue;
}
let binding = KeybindingConfig::parse_keys(&binding.keys).ok_or("Could not parse keybindings - config")?;
keymap.insert(binding, cmd.clone());
found_cmds.push(cmd);
}

Ok(KeybindingConfig{keymap: keymap, config_path: config_path.to_owned()})
}

fn parse_keys(keys: &Vec<String>) -> Option<Event> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be a Result instead of an Option, because it seems that we return None when we encounter an invalid config, no?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In principle, yes. It means the key-text is unparsable. So there would only be one Err-condition. Option seemed easier/faster, just because of this simple "either we can parse it or we don't" distinction.

if keys.len() != 1 {
return None;
}

let key = &keys[0];
match key.as_ref() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

match keys[0].as_ref() should work no?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, probably. I probably edited something back and forth and ended up with that, not noticing.

"enter" => Some(Event::Key(Key::Char('\n'))),
"tab" => Some(Event::Key(Key::Char('\t'))),
"backspace" => Some(Event::Key(Key::Backspace)),
"left" => Some(Event::Key(Key::Left)),
"right" => Some(Event::Key(Key::Right)),
"up" => Some(Event::Key(Key::Up)),
"down" => Some(Event::Key(Key::Down)),
"home" => Some(Event::Key(Key::Home)),
"end" => Some(Event::Key(Key::End)),
"pageup" => Some(Event::Key(Key::PageUp)),
"pagedown" => Some(Event::Key(Key::PageDown)),
"delete" => Some(Event::Key(Key::Delete)),
"insert" => Some(Event::Key(Key::Insert)),
"escape" => Some(Event::Key(Key::Esc)),

x if x.starts_with("f") => {
match x[1..].parse::<u8>() {
Ok(val) => Some(Event::Key(Key::F(val))),
Err(_) => {
warn!("Cannot parse {}", x);
None
}
}
}

x if x.starts_with("ctrl+") || x.starts_with("alt+") => {
let is_alt = x.starts_with("alt+");
let start_length = if is_alt {4} else {5};

let character;
// start_length + "shift+x".len() || start_length + "x".len()
if x.len() != start_length + 7 && x.len() != start_length + 1 {
warn!("Cannot parse {}. Length is = {}, which is neither {} nor {} ", x, x.len(), start_length + 1, start_length + 7);
return None
} else {
if x.len() == start_length + 7 {
// With "+shift+", so we use an upper case letter
character = x.chars().last().unwrap().to_ascii_uppercase();
} else {
character = x.chars().last().unwrap().to_ascii_lowercase();
}
}

if is_alt {
Some(Event::Key(Key::Alt(character)))
} else {
Some(Event::Key(Key::Ctrl(character)))
}
}

x => {
warn!("Completely unknown argument {}", x);
None
},
}


















}
}
3 changes: 3 additions & 0 deletions src/core/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@ pub use self::tui::{CoreEvent, Tui, TuiService, TuiServiceBuilder};

mod cmd;
pub use self::cmd::{Command, ParseCommandError};

mod config;
pub use self::config::{KeybindingConfig};
75 changes: 40 additions & 35 deletions src/core/tui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ use futures::sync::mpsc::{unbounded, UnboundedReceiver, UnboundedSender};
use futures::sync::oneshot::{self, Receiver, Sender};
use futures::{Async, Future, Poll, Sink, Stream};

use termion::event::{Event, Key};
use termion::event::{Event};
use xrl::{Client, Frontend, FrontendBuilder, MeasureWidth, XiNotification};

use failure::Error;

use core::{Command, Terminal, TerminalEvent};
use core::{Command, Terminal, TerminalEvent, KeybindingConfig};
use widgets::{CommandPrompt, Editor};

pub struct Tui {
Expand All @@ -32,18 +32,21 @@ pub struct Tui {

/// Stream of messages from Xi core.
core_events: UnboundedReceiver<CoreEvent>,

keybindings: KeybindingConfig,
}

impl Tui {
/// Create a new Tui instance.
pub fn new(client: Client, events: UnboundedReceiver<CoreEvent>) -> Result<Self, Error> {
pub fn new(client: Client, events: UnboundedReceiver<CoreEvent>, keybindings: KeybindingConfig) -> Result<Self, Error> {
Ok(Tui {
terminal: Terminal::new()?,
exit: false,
term_size: (0, 0),
editor: Editor::new(client),
prompt: None,
core_events: events,
keybindings: keybindings,
})
}

Expand All @@ -54,9 +57,8 @@ impl Tui {

pub fn run_command(&mut self, cmd: Command) {
match cmd {
Command::Cancel => {
self.prompt = None;
}
Command::OpenPrompt => self.open_prompt(),
Command::Cancel => self.prompt = None,
Command::Quit => self.exit = true,
Command::Save(view) => self.editor.save(view),
Command::Back => self.editor.back(),
Expand All @@ -75,40 +77,43 @@ impl Tui {
}
}

fn open_prompt(&mut self) {
if self.prompt.is_none() {
self.prompt = Some(CommandPrompt::default());
}
}

/// Global keybindings can be parsed here
fn handle_input(&mut self, event: Event) {
debug!("handling input {:?}", event);
match event {
Event::Key(Key::Ctrl('c')) => self.exit = true,
Event::Key(Key::Alt('x')) => {
if let Some(ref mut prompt) = self.prompt {
match prompt.handle_input(&event) {
Ok(None) => {}
Ok(Some(_)) => unreachable!(),
Err(_) => unreachable!(),
}
} else {
self.prompt = Some(CommandPrompt::default());
}

if let Some(cmd) = self.keybindings.keymap.get(&event) {
match cmd {
Command::OpenPrompt => {
if self.prompt.is_none() {
self.prompt = Some(CommandPrompt::default());
}
return; },
Command::Quit => { self.exit = true; return; },
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Formatting is a bit off here, although it does not matter too much since we can just rustfmt later.

_ => {/* Somebody else has to deal with these commands */},
}
event => {
// No command prompt is active, process the event normally.
if self.prompt.is_none() {
self.editor.handle_input(event);
return;
}
}

// A command prompt is active.
let mut prompt = self.prompt.take().unwrap();
match prompt.handle_input(&event) {
Ok(None) => {
self.prompt = Some(prompt);
}
Ok(Some(cmd)) => self.run_command(cmd),
Err(err) => {
error!("Failed to parse command: {:?}", err);
}
}
// No command prompt is active, process the event normally.
if self.prompt.is_none() {
self.editor.handle_input(event);
return;
}

// A command prompt is active.
let mut prompt = self.prompt.take().unwrap();
msirringhaus marked this conversation as resolved.
Show resolved Hide resolved
match prompt.handle_input(&event) {
Ok(None) => {
self.prompt = Some(prompt);
}
Ok(Some(cmd)) => self.run_command(cmd),
Err(err) => {
error!("Failed to parse command: {:?}", err);
}
}
}
Expand Down
11 changes: 9 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ extern crate tokio;
extern crate xdg;
extern crate xrl;

extern crate json5;
extern crate serde;
extern crate serde_json;

mod core;
mod widgets;
use xdg::BaseDirectories;
Expand All @@ -29,7 +33,7 @@ use log4rs::append::file::FileAppender;
use log4rs::config::{Appender, Config, Logger, Root};
use xrl::spawn;

use core::{Command, Tui, TuiServiceBuilder};
use core::{Command, Tui, TuiServiceBuilder, KeybindingConfig};

fn configure_logs(logfile: &str) {
let tui = FileAppender::builder().build(logfile).unwrap();
Expand Down Expand Up @@ -93,6 +97,9 @@ fn run() -> Result<(), Error> {
configure_logs(logfile);
}

let configfile = std::path::Path::new("./configs/Default (Linux).sublime-keymap").to_owned();
let keybindings = KeybindingConfig::parse(&configfile).map_err(Error::from_boxed_compat)?;

tokio::run(future::lazy(move || {
info!("starting xi-core");
let (tui_service_builder, core_events_rx) = TuiServiceBuilder::new();
Expand Down Expand Up @@ -122,7 +129,7 @@ fn run() -> Result<(), Error> {
.map_err(|e| error!("failed to send \"client_started\" {:?}", e))
.and_then(move |_| {
info!("initializing the TUI");
let mut tui = Tui::new(client_clone, core_events_rx)
let mut tui = Tui::new(client_clone, core_events_rx, keybindings)
.expect("failed to initialize the TUI");
tui.run_command(Command::Open(
matches.value_of("file").map(ToString::to_string),
Expand Down