From 9f8e9e539fb62f7513f33ff110d1a3253bad2c3c Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy <mmstickman@gmail.com> Date: Wed, 1 Nov 2017 15:08:12 -0400 Subject: [PATCH] Refactor readln logic into it's own module This will move the readln logic from `shell::binary` into `shell::binary::readln`. --- src/shell/binary/mod.rs | 174 ++----------------------------------- src/shell/binary/readln.rs | 168 +++++++++++++++++++++++++++++++++++ 2 files changed, 174 insertions(+), 168 deletions(-) create mode 100644 src/shell/binary/readln.rs diff --git a/src/shell/binary/mod.rs b/src/shell/binary/mod.rs index 31fef2ff..f2b6f0e3 100644 --- a/src/shell/binary/mod.rs +++ b/src/shell/binary/mod.rs @@ -1,26 +1,23 @@ //! Contains the binary logic of Ion. mod prompt; +mod readln; use self::prompt::{prompt, prompt_fn}; -use super::{DirectoryStack, FlowLogic, JobControl, Shell, ShellHistory, Variables}; -use super::completer::*; +use self::readln::readln; +use super::{FlowLogic, JobControl, Shell, ShellHistory}; use super::flags::*; use super::flow_control::Statement; use super::library::IonLibrary; use super::status::*; -use liner::{BasicCompleter, Buffer, Context, CursorPosition, Event, EventKind}; +use liner::{Buffer, Context}; use parser::QuoteTerminator; -use smallstring::SmallString; use smallvec::SmallVec; use std::env; use std::fs::File; use std::io::{self, ErrorKind, Write}; use std::iter::{self, FromIterator}; -use std::mem; -use std::path::{Path, PathBuf}; +use std::path::Path; use std::process; -use sys; -use types::*; pub(crate) trait Binary { /// Launches the shell, parses arguments, and then diverges into one of the `execution` @@ -50,135 +47,7 @@ impl Binary for Shell { fn prompt_fn(&mut self) -> Option<String> { prompt_fn(self) } - fn readln(&mut self) -> Option<String> { - { - let vars_ptr = &self.variables as *const Variables; - let dirs_ptr = &self.directory_stack as *const DirectoryStack; - - // Collects the current list of values from history for completion. - let history = &self.context.as_ref().unwrap().history.buffers.iter() - // Map each underlying `liner::Buffer` into a `String`. - .map(|x| x.chars().cloned().collect()) - // Collect each result into a vector to avoid borrowing issues. - .collect::<Vec<SmallString>>(); - - loop { - let prompt = self.prompt(); - let funcs = &self.functions; - let vars = &self.variables; - let builtins = &self.builtins; - - let line = self.context.as_mut().unwrap().read_line( - prompt, - &mut move |Event { editor, kind }| { - if let EventKind::BeforeComplete = kind { - let (words, pos) = editor.get_words_and_cursor_position(); - - let filename = match pos { - CursorPosition::InWord(index) => index > 0, - CursorPosition::InSpace(Some(_), _) => true, - CursorPosition::InSpace(None, _) => false, - CursorPosition::OnWordLeftEdge(index) => index >= 1, - CursorPosition::OnWordRightEdge(index) => { - match (words.into_iter().nth(index), env::current_dir()) { - (Some((start, end)), Ok(file)) => { - let filename = - editor.current_buffer().range(start, end); - complete_as_file(file, filename, index) - } - _ => false, - } - } - }; - - if filename { - if let Ok(current_dir) = env::current_dir() { - if let Some(url) = current_dir.to_str() { - let completer = - IonFileCompleter::new(Some(url), dirs_ptr, vars_ptr); - mem::replace( - &mut editor.context().completer, - Some(Box::new(completer)), - ); - } - } - } else { - // Creates a list of definitions from the shell environment that - // will be used - // in the creation of a custom completer. - let words = builtins.keys().iter() - // Add built-in commands to the completer's definitions. - .map(|&s| Identifier::from(s)) - // Add the history list to the completer's definitions. - .chain(history.iter().cloned()) - // Add the aliases to the completer's definitions. - .chain(vars.aliases.keys().cloned()) - // Add the list of available functions to the completer's definitions. - .chain(funcs.keys().cloned()) - // Add the list of available variables to the completer's definitions. - // TODO: We should make it free to do String->SmallString - // and mostly free to go back (free if allocated) - .chain(vars.get_vars().map(|s| ["$", &s].concat().into())) - .collect(); - - // Initialize a new completer from the definitions collected. - let custom_completer = BasicCompleter::new(words); - - // Creates completers containing definitions from all directories - // listed - // in the environment's **$PATH** variable. - let mut file_completers = if let Ok(val) = env::var("PATH") { - val.split(sys::PATH_SEPARATOR) - .map(|s| IonFileCompleter::new(Some(s), dirs_ptr, vars_ptr)) - .collect() - } else { - vec![IonFileCompleter::new(Some("/bin/"), dirs_ptr, vars_ptr)] - }; - - // Also add files/directories in the current directory to the - // completion list. - if let Ok(current_dir) = env::current_dir() { - if let Some(url) = current_dir.to_str() { - file_completers.push( - IonFileCompleter::new(Some(url), dirs_ptr, vars_ptr), - ); - } - } - - // Merge the collected definitions with the file path definitions. - let completer = - MultiCompleter::new(file_completers, custom_completer); - - // Replace the shell's current completer with the newly-created - // completer. - mem::replace( - &mut editor.context().completer, - Some(Box::new(completer)), - ); - } - } - }, - ); - - match line { - Ok(line) => return Some(line), - // Handles Ctrl + C - Err(ref err) if err.kind() == ErrorKind::Interrupted => return None, - // Handles Ctrl + D - Err(ref err) if err.kind() == ErrorKind::UnexpectedEof => break, - Err(err) => { - let stderr = io::stderr(); - let mut stderr = stderr.lock(); - let _ = writeln!(stderr, "ion: liner: {}", err); - return None; - } - } - } - } - - let previous_status = self.previous_status; - self.exit(previous_status); - } + fn readln(&mut self) -> Option<String> { readln(self) } fn terminate_script_quotes<I: Iterator<Item = String>>(&mut self, mut lines: I) -> i32 { while let Some(command) = lines.next() { @@ -400,34 +269,3 @@ fn word_divide(buf: &Buffer) -> Vec<(usize, usize)> { } res } - -/// Infer if the given filename is actually a partial filename -fn complete_as_file(current_dir: PathBuf, filename: String, index: usize) -> bool { - let filename = filename.trim(); - let mut file = current_dir.clone(); - file.push(&filename); - // If the user explicitly requests a file through this syntax then complete as a file - if filename.starts_with(".") { - return true; - } - // If the file starts with a dollar sign, it's a variable, not a file - if filename.starts_with("$") { - return false; - } - // Once we are beyond the first string, assume its a file - if index > 0 { - return true; - } - // If we are referencing a file that exists then just complete to that file - if file.exists() { - return true; - } - // If we have a partial file inside an existing directory, e.g. /foo/b when /foo/bar - // exists, then treat it as file as long as `foo` isn't the current directory, otherwise - // this would apply to any string `foo` - if let Some(parent) = file.parent() { - return parent.exists() && parent != current_dir; - } - // By default assume its not a file - false -} diff --git a/src/shell/binary/readln.rs b/src/shell/binary/readln.rs new file mode 100644 index 00000000..fa45e5dc --- /dev/null +++ b/src/shell/binary/readln.rs @@ -0,0 +1,168 @@ +use super::super::{Binary, DirectoryStack, Shell, Variables}; +use super::super::completer::*; +use liner::{BasicCompleter, CursorPosition, Event, EventKind}; +use smallstring::SmallString; +use std::env; +use std::io::{self, ErrorKind, Write}; +use std::mem; +use std::path::PathBuf; +use sys; +use types::*; + +pub(crate) fn readln(shell: &mut Shell) -> Option<String> { + { + let vars_ptr = &shell.variables as *const Variables; + let dirs_ptr = &shell.directory_stack as *const DirectoryStack; + + // Collects the current list of values from history for completion. + let history = &shell.context.as_ref().unwrap().history.buffers.iter() + // Map each underlying `liner::Buffer` into a `String`. + .map(|x| x.chars().cloned().collect()) + // Collect each result into a vector to avoid borrowing issues. + .collect::<Vec<SmallString>>(); + + loop { + let prompt = shell.prompt(); + let funcs = &shell.functions; + let vars = &shell.variables; + let builtins = &shell.builtins; + + let line = shell.context.as_mut().unwrap().read_line( + prompt, + &mut move |Event { editor, kind }| { + if let EventKind::BeforeComplete = kind { + let (words, pos) = editor.get_words_and_cursor_position(); + + let filename = match pos { + CursorPosition::InWord(index) => index > 0, + CursorPosition::InSpace(Some(_), _) => true, + CursorPosition::InSpace(None, _) => false, + CursorPosition::OnWordLeftEdge(index) => index >= 1, + CursorPosition::OnWordRightEdge(index) => { + match (words.into_iter().nth(index), env::current_dir()) { + (Some((start, end)), Ok(file)) => { + let filename = editor.current_buffer().range(start, end); + complete_as_file(file, filename, index) + } + _ => false, + } + } + }; + + if filename { + if let Ok(current_dir) = env::current_dir() { + if let Some(url) = current_dir.to_str() { + let completer = + IonFileCompleter::new(Some(url), dirs_ptr, vars_ptr); + mem::replace( + &mut editor.context().completer, + Some(Box::new(completer)), + ); + } + } + } else { + // Creates a list of definitions from the shell environment that + // will be used + // in the creation of a custom completer. + let words = builtins.keys().iter() + // Add built-in commands to the completer's definitions. + .map(|&s| Identifier::from(s)) + // Add the history list to the completer's definitions. + .chain(history.iter().cloned()) + // Add the aliases to the completer's definitions. + .chain(vars.aliases.keys().cloned()) + // Add the list of available functions to the completer's definitions. + .chain(funcs.keys().cloned()) + // Add the list of available variables to the completer's definitions. + // TODO: We should make it free to do String->SmallString + // and mostly free to go back (free if allocated) + .chain(vars.get_vars().map(|s| ["$", &s].concat().into())) + .collect(); + + // Initialize a new completer from the definitions collected. + let custom_completer = BasicCompleter::new(words); + + // Creates completers containing definitions from all directories + // listed + // in the environment's **$PATH** variable. + let mut file_completers = if let Ok(val) = env::var("PATH") { + val.split(sys::PATH_SEPARATOR) + .map(|s| IonFileCompleter::new(Some(s), dirs_ptr, vars_ptr)) + .collect() + } else { + vec![IonFileCompleter::new(Some("/bin/"), dirs_ptr, vars_ptr)] + }; + + // Also add files/directories in the current directory to the + // completion list. + if let Ok(current_dir) = env::current_dir() { + if let Some(url) = current_dir.to_str() { + file_completers + .push(IonFileCompleter::new(Some(url), dirs_ptr, vars_ptr)); + } + } + + // Merge the collected definitions with the file path definitions. + let completer = MultiCompleter::new(file_completers, custom_completer); + + // Replace the shell's current completer with the newly-created + // completer. + mem::replace( + &mut editor.context().completer, + Some(Box::new(completer)), + ); + } + } + }, + ); + + match line { + Ok(line) => return Some(line), + // Handles Ctrl + C + Err(ref err) if err.kind() == ErrorKind::Interrupted => return None, + // Handles Ctrl + D + Err(ref err) if err.kind() == ErrorKind::UnexpectedEof => break, + Err(err) => { + let stderr = io::stderr(); + let mut stderr = stderr.lock(); + let _ = writeln!(stderr, "ion: liner: {}", err); + return None; + } + } + } + } + + let previous_status = shell.previous_status; + shell.exit(previous_status); +} + +/// Infer if the given filename is actually a partial filename +fn complete_as_file(current_dir: PathBuf, filename: String, index: usize) -> bool { + let filename = filename.trim(); + let mut file = current_dir.clone(); + file.push(&filename); + // If the user explicitly requests a file through this syntax then complete as a file + if filename.starts_with(".") { + return true; + } + // If the file starts with a dollar sign, it's a variable, not a file + if filename.starts_with("$") { + return false; + } + // Once we are beyond the first string, assume its a file + if index > 0 { + return true; + } + // If we are referencing a file that exists then just complete to that file + if file.exists() { + return true; + } + // If we have a partial file inside an existing directory, e.g. /foo/b when /foo/bar + // exists, then treat it as file as long as `foo` isn't the current directory, otherwise + // this would apply to any string `foo` + if let Some(parent) = file.parent() { + return parent.exists() && parent != current_dir; + } + // By default assume its not a file + false +} -- GitLab