Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
53 changes: 53 additions & 0 deletions src/core_editor/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,59 @@ impl Editor {
self.line_buffer.get_buffer()
}

/// Check if a position in the buffer is inside an unclosed string literal
pub fn is_inside_string_literal(&self, position: usize) -> bool {
let buffer = self.get_buffer();

if buffer.is_empty() || position == 0 {
return false;
}
if !buffer.contains('"') && !buffer.contains('\'') {
return false;
}

let target_byte_pos = buffer
.char_indices()
.nth(position)
.map(|(byte_idx, _)| byte_idx)
.unwrap_or(buffer.len());

let bytes = buffer.as_bytes();
let mut in_single_quote = false;
let mut in_double_quote = false;
let mut escaped = false;
let mut byte_pos = 0;

for &byte in bytes {
if byte_pos > target_byte_pos {
break;
}

if escaped {
escaped = false;
byte_pos += 1;
continue;
}

match byte {
b'\\' => {
escaped = true;
}
b'\'' if !in_double_quote => {
in_single_quote = !in_single_quote;
}
b'"' if !in_single_quote => {
in_double_quote = !in_double_quote;
}
_ => {}
}

byte_pos += 1;
}

in_single_quote || in_double_quote
}

/// Edit the [`LineBuffer`] in an undo-safe manner.
pub fn edit_buffer<F>(&mut self, func: F, undo_behavior: UndoBehavior)
where
Expand Down
76 changes: 75 additions & 1 deletion src/engine.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::path::PathBuf;
use std::{collections::HashMap, path::PathBuf};

use itertools::Itertools;
use nu_ansi_term::{Color, Style};
Expand Down Expand Up @@ -187,6 +187,8 @@ pub struct Reedline {
// Engine Menus
menus: Vec<ReedlineMenu>,

abbreviations: HashMap<String, String>,

// Text editor used to open the line buffer for editing
buffer_editor: Option<BufferEditor>,

Expand Down Expand Up @@ -287,6 +289,7 @@ impl Reedline {
mouse_click_mode: MouseClickMode::default(),
cwd: None,
menus: Vec::new(),
abbreviations: HashMap::new(),
buffer_editor: None,
cursor_shapes: None,
bracketed_paste: BracketedPasteGuard::default(),
Expand Down Expand Up @@ -619,6 +622,14 @@ impl Reedline {
self
}

/// A builder that adds abbreviations to the Reedline engine
///
/// Overwrites any existing abbreviations with the same key.
pub fn with_abbreviations(mut self, abbreviations: HashMap<String, String>) -> Self {
self.abbreviations.extend(abbreviations);
self
}

/// A builder that adds the history item id
#[must_use]
pub fn with_history_session_id(mut self, session: Option<HistorySessionId>) -> Self {
Expand Down Expand Up @@ -1278,6 +1289,9 @@ impl Reedline {
if let Some(event) = self.parse_bang_command() {
return self.handle_editor_event(prompt, event);
}
if let Some(event) = self.try_expand_abbreviation_at_cursor(true) {
return self.handle_editor_event(prompt, event);
}

let buffer = self.editor.get_buffer().to_string();
match self.validator.as_mut().map(|v| v.validate(&buffer)) {
Expand All @@ -1294,13 +1308,21 @@ impl Reedline {
if let Some(event) = self.parse_bang_command() {
return self.handle_editor_event(prompt, event);
}
if let Some(event) = self.try_expand_abbreviation_at_cursor(true) {
return self.handle_editor_event(prompt, event);
}

Ok(self.submit_buffer(prompt)?)
}
ReedlineEvent::SubmitOrNewline => {
#[cfg(feature = "bashisms")]
if let Some(event) = self.parse_bang_command() {
return self.handle_editor_event(prompt, event);
}
if let Some(event) = self.try_expand_abbreviation_at_cursor(true) {
return self.handle_editor_event(prompt, event);
}

let cursor_position_in_buffer = self.editor.insertion_point();
let buffer = self.editor.get_buffer().to_string();
if cursor_position_in_buffer < buffer.len() {
Expand All @@ -1323,6 +1345,13 @@ impl Reedline {
}
ReedlineEvent::Edit(commands) => {
self.run_edit_commands(&commands);

// Check if a space was just inserted and try to expand abbreviations
if let Some(EditCommand::InsertChar(' ')) = commands.first() {
if let Some(event) = self.try_expand_abbreviation_at_cursor(false) {
return self.handle_editor_event(prompt, event);
}
}
if let Some(menu) = self.menus.iter_mut().find(|men| men.is_active()) {
if self.quick_completions && menu.can_quick_complete() {
match commands.first() {
Expand Down Expand Up @@ -1729,6 +1758,51 @@ impl Reedline {
}
}

/// Expands an abbreviation at the word before the cursor, if any exists
///
/// Note, expansion does not occur when inside a string.
fn try_expand_abbreviation_at_cursor(&mut self, submitted: bool) -> Option<ReedlineEvent> {
let buffer = self.editor.get_buffer();
let cursor_position_in_buffer = self.editor.insertion_point();

let chars: Vec<char> = buffer.chars().collect();
let (offset, suffix) = match submitted {
true => (0, ""), // expand on <enter>
false => (1, " "), // expand on <space>
};

let mut word_start = cursor_position_in_buffer - 1;
while word_start > 0 && !chars[word_start - 1].is_whitespace() {
word_start -= 1;
}
let word_end = cursor_position_in_buffer - offset;

if word_start >= word_end {
// The first char in the buffer is a space or there are consecutive spaces
return None;
}
if self.editor.is_inside_string_literal(word_start) {
return None;
}

let word: String = chars[word_start..word_end].iter().collect();
if let Some(expansion) = self.abbreviations.get(&word) {
return Some(ReedlineEvent::Edit(vec![
EditCommand::MoveToPosition {
position: word_start,
select: false,
},
EditCommand::MoveToPosition {
position: word_end,
select: true,
},
EditCommand::InsertString(format!("{}{}", expansion, suffix)),
]));
}

None
}

#[cfg(feature = "bashisms")]
/// Parses the ! command to replace entries from the history
fn parse_bang_command(&mut self) -> Option<ReedlineEvent> {
Expand Down